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.