user2064000
user2064000

Reputation:

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?

Upvotes: 7

Views: 1587

Answers (1)

Hannu
Hannu

Reputation: 12205

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.

Upvotes: 1

Related Questions