Skip to content

Commit

Permalink
Implement Haproxy PROXY protocol v1 and v2
Browse files Browse the repository at this point in the history
Signed-off-by: Frank Villaro-Dixon <frank.villaro@infomaniak.com>
  • Loading branch information
pascal-dubois authored and Frankkkkk committed Oct 26, 2021
1 parent 5793ee5 commit 76bbd99
Show file tree
Hide file tree
Showing 6 changed files with 625 additions and 0 deletions.
56 changes: 56 additions & 0 deletions demo/proxy_protocol_untrusted.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env python

# Copyright (C) 2007 Giampaolo Rodola' <g.rodola@gmail.com>.
# Use of this source code is governed by MIT license that can be
# found in the LICENSE file.

"""A basic FTP server which uses a DummyAuthorizer for managing 'virtual
users', setting a limit for incoming connections and a range of passive
ports. Accepts connections from proxies implementing the PROXY protocol.
"""

import os

from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer


def main():
# Instantiate a dummy authorizer for managing 'virtual' users
authorizer = DummyAuthorizer()

# Define a new user having full r/w permissions and a read-only
# anonymous user
authorizer.add_user('user', '12345', os.getcwd(), perm='elradfmwMT')
authorizer.add_anonymous(os.getcwd())

# Instantiate FTP handler class
handler = FTPHandler
handler.authorizer = authorizer

# Define a customized banner (string returned when client connects)
handler.banner = "pyftpdlib based ftpd ready."

# Specify a masquerade address and the range of ports to use for
# passive connections. Decomment in case you're behind a NAT.
# handler.masquerade_address = '151.25.42.11'
handler.passive_ports = range(20000, 22535)


# Instantiate FTP server class and listen on 0.0.0.0:2121
address = ('', 2121)
FTPServer.proxy_proto_enabled = True
FTPServer.proxy_proto_allow_untrusted = True
server = FTPServer(address, handler)

# set a limit for connections
server.max_cons = 256
server.max_cons_per_ip = 5

# start ftp server
server.serve_forever()


if __name__ == '__main__':
main()
16 changes: 16 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,22 @@ Server (acceptor)
Number of maximum connections accepted for the same IP address (default
``0`` == no limit).

.. data:: proxy_proto_enabled

Whether to enable the PROXY protocol for incoming connections (default ``False``)

.. data:: proxy_proto_trusted_nets

Contains a list of trusted proxies (written as strings) allowed to communicate
with the server. Use a /32 (or /128) network mask if you want to declare a
single IP, for example ``['10.11.12.13/32', 'fc88::2/128']`` (defaults to ``[]``).

.. data:: proxy_proto_allow_untrusted

Whether or not to parse non trusted (as defined by :data:`proxy_proto_trusted_nets`)
proxies headers (defaults to ``False``).


.. method:: serve_forever(timeout=None, blocking=True, handle_exit=True, worker_processes=1)

Starts the asynchronous IO loop.
Expand Down
10 changes: 10 additions & 0 deletions pyftpdlib/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,7 @@ def __init__(self, conn, server, ioloop=None):
self.remote_ip = ""
self.remote_port = ""
self.started = time.time()
self.proxy_proto_obj = self.server.proxy_proto_obj

# private session attributes
self._last_response = ""
Expand Down Expand Up @@ -1294,6 +1295,15 @@ def __init__(self, conn, server, ioloop=None):
self.handle_error()
return
else:
if self.server.proxy_proto_enabled and self.proxy_proto_obj and \
(self.proxy_proto_obj.trusted or self.server.proxy_proto_allow_untrusted):
# Override the socket values if PROXY protocol data is
# available and comes from a trusted proxy
if self.proxy_proto_obj.remote_ip:
self.remote_ip = self.proxy_proto_obj.remote_ip
if self.proxy_proto_obj.remote_port:
self.remote_port = self.proxy_proto_obj.remote_port

self.log("FTP session opened (connect)")

# try to handle urgent data inline
Expand Down
38 changes: 38 additions & 0 deletions pyftpdlib/ioloop.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def handle_accepted(self, sock, addr):
from .log import debug
from .log import is_logging_configured
from .log import logger
from .proxy_proto import ProxyProtocol


timer = getattr(time, 'monotonic', time.time)
Expand Down Expand Up @@ -977,8 +978,30 @@ def add_channel(self, map=None, events=None):
class Acceptor(AsyncChat):
"""Same as base AsyncChat and supposed to be used to
accept new connections.
All relevant PROXY protocol information is stored in class attributes
described below.
- (bool) proxy_proto_enabled:
enable the use of PROXY protocol (defaults to False).
- (list) proxy_proto_trusted_nets:
the IP networks (written as strings) of the proxies you want to
trust. Use a /32 (or /128) network mask if you want to declare a
single IP (defaults to []).
- (bool) proxy_proto_allow_untrusted:
whether or not to parse untrusted proxies headers (defaults to False).
- (instance) proxy_proto_obj:
the ProxyProtocol instance populated with header's information
"""

proxy_proto_enabled = False
proxy_proto_trusted_nets = []
proxy_proto_allow_untrusted = False
proxy_proto_obj = None

def add_channel(self, map=None, events=None):
AsyncChat.add_channel(self, map=map, events=self.ioloop.READ)

Expand Down Expand Up @@ -1045,6 +1068,21 @@ def handle_accept(self):
debug("call: handle_accept(); accept() returned ECONNABORTED",
self)
else:
if self.proxy_proto_enabled:
# Retrieve a populated PROXY protocol object. If no exception
# is raised the returned object is considered valid
try:
ProxyProtocol.trusted_networks = self.proxy_proto_trusted_nets
ProxyProtocol.allow_untrusted = self.proxy_proto_allow_untrusted
self.proxy_proto_obj = ProxyProtocol.create(sock)
except Exception as e:
logger.error("proxy: {}".format(e))
return

if self.proxy_proto_obj.trusted or self.proxy_proto_allow_untrusted:
# Could result in a (None, None) tuple
addr = (self.proxy_proto_obj.remote_ip, self.proxy_proto_obj.remote_port)

# sometimes addr == None instead of (ip, port) (see issue 104)
if addr is not None:
self.handle_accepted(sock, addr)
Expand Down

0 comments on commit 76bbd99

Please sign in to comment.