PROXY protocol

23 views
Skip to first unread message

haliphax

unread,
Feb 24, 2023, 11:47:38 AM2/24/23
to asyncssh-users
I'm working with a project where my AsyncSSH server is behind a Traefik TCP proxy, and I'd like to pull the original source IP address from the PROXY protocol [1] header information. However, it doesn't seem at first glance as though you would be able to parse headers (using raw data receive callbacks) and _then_ flip over to an SSHServer implementation.

I'm not necessarily asking you to figure it out for me, but any hints as to a good approach or where to start would be greatly appreciated. Love the project!

(If you're curious, here's what I'm working on: https://github.com/haliphax/xthulu)

haliphax

unread,
Feb 24, 2023, 11:52:48 AM2/24/23
to asyncssh-users
Actually, linking directly to this file probably would have been better if anyone wants to poke at it. This is the SSH server, connection handler, etc. https://github.com/haliphax/xthulu/blob/9965e24e0ce51d66e3b28af4cd8b78d566b4f44a/xthulu/ssh.py

haliphax

unread,
Feb 24, 2023, 4:33:32 PM2/24/23
to asyncssh-users
I have managed to solve this in the meantime with go-mmproxy [1], but would nonetheless appreciate any input on how this might be done internally within my application.

Ron Frederick

unread,
Feb 25, 2023, 5:28:06 PM2/25/23
to haliphax, asyncssh-users
Which version of the proxy protocol are you using? Version 1 is pretty simple to write your own parser for, but version 2 would be a bit more involved.

There is a Python package called “proxyprotocol” which is designed to work with asyncio, but it looks like it operates at the “stream” level (using StreamReader and StreamWriter objects), while AsyncSSH would want to integrate at the transport/protocol level using callbacks. So, I’m not sure that could be easily used here.

It's possible to do this using the “tunnel” option in AsyncSSH, but it’s a bit involved, and it relies on knowing some internal details about how AsyncSSH’s SSH tunnels work. So, I can’t guarantee that this won’t break in the future. The debug logging is also slightly wrong, reporting these connections as coming in over an SSH tunnel. Here’s some example code, though:

import asyncio, asyncssh, sys

class ProxyProtocolSession:
    def __init__(self, conn_factory):
        self._conn_factory = conn_factory
        self._conn = None
        self._peername = None
        self._transport = None
        self._inpbuf = []

    def connection_made(self, transport):
        self._transport = transport

    def connection_lost(self, exc):
        if self._conn:
            self._conn.connection_lost(exc)

        self.close()

    def get_extra_info(self, name, default = None):
        if name == 'peername':
            return self._peername
        else:
            return self._transport.get_extra_info(name, default)

    def data_received(self, data):
        if self._conn:
            self._conn.data_received(data)
        else:
            idx = data.find(b'\r\n')

            if idx >= 0:
                self._inpbuf.append(data[:idx])
                data = data[idx+2:]

                conn_info = b''.join(self._inpbuf).split()
                self._inpbuf.clear()

                self._peername = (conn_info[2].decode('ascii'),
                                  int(conn_info[4]))
                self._conn = self._conn_factory('', 0)
                self._conn.connection_made(self)

                if data:
                    self._conn.data_received(data)
            else:
                self._inpbuf.append(data)

    def eof_received(self):
        if self._conn:
            self._conn.eof_received()

    def write(self,  data):
        if self._transport:
            self._transport.write(data)

    def is_closing(self):
        return self._transport.is_closing()

    def abort(self):
        self.close()

    def close(self):
        if self._transport:
            self._transport.close()

class ProxyProtocolListener:
    async def create_server(self, conn_factory, listen_host, listen_port):
        def tunnel_factory():
            return ProxyProtocolSession(conn_factory)

        return await asyncio.get_event_loop().create_server(
            tunnel_factory, listen_host, listen_port)

def handle_client(process):
    process.stdout.write('Welcome to my SSH server, %s!\n' %
                         process.get_extra_info('username'))
    process.exit(0)

async def start_server():
    server = await asyncssh.listen('', 10022, server_host_keys=['ssh_host_key'],
                                   authorized_client_keys='ssh_user_ca',
                                   tunnel=ProxyProtocolListener(),
                                   process_factory=handle_client)

loop = asyncio.new_event_loop()

try:
    loop.run_until_complete(start_server())
except (OSError, asyncssh.Error) as exc:
    sys.exit('Error starting server: ' + str(exc))

loop.run_forever()

The new code here is the ProxyProtocolSession and ProxyProtocolListener classes and the use of the “tunnel” argument in asyncssh.listen(). Everything else is taken from a standard server example.

Most of the code is just passing data between accepted socket connections and SSHServerConnection objects, plus the little bit of code needed to find the end of the proxy protocol headers and parse out the address information. This connection object is created only after the proxy protocol header is read, and the associated peername information is stored for the connection object to query it later.
-- 
Ron Frederick
ro...@timeheart.net



haliphax

unread,
Feb 26, 2023, 12:51:05 PM2/26/23
to Ron Frederick, asyncssh-users
My plan is to implement version 1 initially, working my way up to version 2 at dinner point in the future (for exactly the reasons of complexity you alluded to in your reply).

Thank you for the example code; it was far more than I expected. You are a gem.

haliphax

unread,
Feb 26, 2023, 12:52:10 PM2/26/23
to Ron Frederick, asyncssh-users
Some point in the future, not dinner point in the future. Phones are silly these days.

# Todd

Ron Frederick

unread,
Feb 28, 2023, 11:06:11 PM2/28/23
to haliphax, asyncssh-users
Hi Todd,

Is there something in particular in proxy protocol v2 that you’re thinking about using? I’m not sure there’s anything other than the original client IP & port that AsyncSSH would really have any interest in, and looking over the other v2 type values, I’m not sure most of those would even apply. So, you may be better off just sticking with version 1.

haliphax

unread,
Mar 1, 2023, 9:29:26 AM3/1/23
to Ron Frederick, asyncssh-users
I came to a similar conclusion. The remote address is all I'm really interested in for my use case. I may look into implementing V2 simply to avoid deprecation at some point, but it's wholly unnecessary for the time being.

The code you posted integrated very well, by the way! Thank you so much for that.

# Todd
Reply all
Reply to author
Forward
0 new messages