Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I implement port forwarding in a Paramiko server?

A "direct-tcpip" request (commonly known as port-forwarding) occurs when you run SSH as ssh user@host -L <local port>:<remote host>:<remote port> and then try to connect over the local port.

I'm trying to implement direct-tcpip on a custom SSH server, and Paramiko offers the check_channel_direct_tcpip_request in the ServerInterface class in order to check if the "direct-tcpip" request should be allowed, which can be implemented as follows:

class Server(paramiko.ServerInterface):
    # ...
    def check_channel_direct_tcpip_request(self, chanid, origin, destination):
        return paramiko.OPEN_SUCCEEDED

However, when I use the aforementioned SSH command, and connect over the local port, nothing happens, probably because I need to implement the connection handling myself.

Reading the documentation, it also appears that the channel is only opened after OPEN_SUCCEDED has been returned.

How can I handle the direct-tcpip request after returning OPEN_SUCCEEDED for the request?

like image 843
user2064000 Avatar asked Jun 01 '20 04:06

user2064000


People also ask

What is TCP forwarding in SSH?

OpenSSH provides TCP port forwarding, also known as tunnelling, which allows other TCP applications to forward their network data over a secure SSH connection.

Does Paramiko use OpenSSH?

Paramiko does not itself leverage OpenSSH-style config file directives, but it does implement a parser for the format, which users can honor themselves (and is used by higher-level libraries, such as Fabric).

How do I connect to Paramiko?

A Paramiko SSH Example: Connect to Your Server Using a Password. This section shows you how to authenticate to a remote server with a username and password. To begin, create a new file named first_experiment.py and add the contents of the example file. Ensure that you update the file with your own Linode's details.


1 Answers

You indeed do need to set up your own connection handler. This is a lengthy answer to explain the steps I took - some of it you will not need if your server code already works. The whole running server example in its entirety is here: https://controlc.com/25439153

I used the Paramiko example server code from here https://github.com/paramiko/paramiko/blob/master/demos/demo_server.py as a basis and implanted some socket code on that. This does not have any error handling, thread related niceties or anything else "proper" for that matter but it allows you to use the port forwarder.

This also has a lot of things you do not need as I did not want to start tidying up a dummy example code. Apologies for that.

To start with, we need the forwarder tools. This creates a thread to run the "tunnel" forwarder. This also answers to your question where you get your channel. You accept() it from the transport but you need to do that in the forwarder thread. As you stated in your OP, it is not there yet in the check_channel_direct_tcpip_request() function but it will be eventually available to the thread.

def tunnel(sock, chan, chunk_size=1024):
    while True:
        r, w, x = select.select([sock, chan], [], [])

        if sock in r:
            data = sock.recv(chunk_size)
            if len(data) == 0:
                break
            chan.send(data)

        if chan in r:
            data = chan.recv(chunk_size)
            if len(data) == 0:
                break
            sock.send(data)

    chan.close()
    sock.close()


class ForwardClient(threading.Thread):
    daemon = True
    # chanid = 0

    def __init__(self, address, transport, chanid):
        threading.Thread.__init__(self)

        self.socket = socket.create_connection(address)
        self.transport = transport
        self.chanid = chanid

    def run(self):
        while True:
            chan = self.transport.accept(10)
            if chan == None:
                continue

            print("Got new channel (id: %i).", chan.get_id())

            if chan.get_id() == self.chanid:
                break

        peer = self.socket.getpeername()
        try:
            tunnel(self.socket, chan)
        except:
            pass

Back to the example server code. Your server class needs to have transport as a parameter, unlike in the example code:

class Server(paramiko.ServerInterface):
    # 'data' is the output of base64.b64encode(key)
    # (using the "user_rsa_key" files)
    data = (
    b"AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp"
    b"fAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMC"
    b"KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT"
    b"UWT10hcuO4Ks8="
    )
    good_pub_key = paramiko.RSAKey(data=decodebytes(data))

    def __init__(self, transport):
        self.transport = transport
        self.event = threading.Event()

Then you will override the relevant method and create the forwarder there:

def check_channel_direct_tcpip_request(self, chanid, origin, destination):
    print(chanid, origin, destination)
    f = ForwardClient(destination, self.transport, chanid)
    f.start()

    return paramiko.OPEN_SUCCEEDED

You need to add transport parameter to the creation of the server class:

t.add_server_key(host_key)
server = Server(t)

This example server requires you to have a RSA private key in the directory named test_rsa.key. Create any RSA key there, you do not need it but I did not bother to strip the use of it off the code.

You can then run your server (runs on port 2200) and issue

ssh -p 2200 -L 2300:www.google.com:80  robey@localhost

(password is foo)

Now when you try

telnet localhost 2300

and type something there, you will get a response from Google.

like image 146
Hannu Avatar answered Oct 20 '22 00:10

Hannu