]> git.ipfire.org Git - ipfire.org.git/commitdiff
nopaste: Implement PROXY protocol on the TCP service
authorMichael Tremer <michael.tremer@ipfire.org>
Fri, 23 Feb 2024 17:51:24 +0000 (17:51 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Fri, 23 Feb 2024 17:53:11 +0000 (17:53 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
src/backend/nopaste.py

index ecb31959e4f4f8b53fb626099acf3b4f0e6c730f..364f9a2c0d621791e886271d54112fd1770ed38d 100644 (file)
@@ -3,8 +3,10 @@
 import asyncio
 import datetime
 import io
+import ipaddress
 import logging
 import magic
+import struct
 import tornado.iostream
 import tornado.tcpserver
 
@@ -253,16 +255,54 @@ class Paste(Object):
                )
 
 
+# PROXY Protocol Implementation
+PROXY_CMD_LOCAL     = 0
+PROXY_CMD_PROXY     = 1
+
+PROXY_FAMILY_UNSPEC = 0
+PROXY_FAMILY_INET   = 1
+PROXY_FAMILY_INET6  = 2
+
+PROXY_PROTO_UNSPEC  = 0
+PROXY_PROTO_STREAM  = 1
+PROXY_PROTO_DGRAM   = 2
+
+class ProxyError(Exception):
+       pass
+
+
+class ProxyUnsupportedError(ProxyError):
+       pass
+
+
 class Service(tornado.tcpserver.TCPServer):
-       def __init__(self, config, **kwargs):
+       def __init__(self, config, use_proxy=True, **kwargs):
                # Initialize backend
                self.backend = base.Backend(config)
 
+               # Expect PROXY headers?
+               self.use_proxy = use_proxy
+
                super().__init__(**kwargs)
 
        async def handle_stream(self, stream, address):
                buffer = io.BytesIO()
 
+               # Parse the PROXY header
+               if self.use_proxy:
+                       try:
+                               address = await self._parse_proxyv2_header(stream, address)
+
+                       # Close the stream on any proxy errors
+                       except ProxyError as e:
+                               log.error("Proxy Error: %s" % e)
+
+                               return stream.close()
+
+                       # If we received no result, this connection is not supposed to be relayed
+                       if not address:
+                               return
+
                # Read the entire stream
                try:
                        while True:
@@ -288,17 +328,136 @@ class Service(tornado.tcpserver.TCPServer):
 
                # Store this into the database
                with self.backend.db.transaction():
-                       uuid = self.backend.nopaste.create(
+                       paste = self.backend.nopaste.create(
                                buffer.getvalue(),
                                subject="Streamed Upload",
                                address=address,
                        )
 
                # Format a response message
-               message = "https://nopaste.ipfire.org/view/%s\n" % uuid
+               message = "https://nopaste.ipfire.org/view/%s\n" % paste.uuid
 
                # Send the message
                await stream.write(message.encode("utf-8"))
 
                # We are done, close the stream
                stream.close()
+
+       async def _parse_proxyv2_header(self, stream, address):
+               """
+                       Parses the PROXYv2 header and returns the real client's IP address
+               """
+               src_address, src_port, dst_address, dst_port = None, None, None, None
+
+               log.debug("Parsing PROXY connection from %s:%s" % address)
+
+               # Header
+               proxy_hdr_v2  = struct.Struct("!12BBBH")
+               proxy_addr_v6 = struct.Struct("!16B16BHH")
+               proxy_addr_v4 = struct.Struct("!IIHH")
+
+               # Try to read the header into a buffer
+               buffer = await stream.read_bytes(proxy_hdr_v2.size)
+
+               if len(buffer) < proxy_hdr_v2.size:
+                       raise ProxyError("Header too short")
+
+               # Parse the header
+               *signature, ver_cmd, fam_prot, length = proxy_hdr_v2.unpack(buffer)
+
+               # Check signature
+               if not signature == [13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10]:
+                       raise ProxyError("Incorrect signature")
+
+               # Extract version and command
+               version = (ver_cmd >> 4)
+               command = (ver_cmd & 0x0f)
+
+               # Check protocol version
+               if not version == 2:
+                       raise ProxyError("Incorrect protocol version")
+
+               # Handle LOCAL commands
+               if command == PROXY_CMD_LOCAL:
+                       pass
+
+               # Handle PROXY commands
+               elif command == PROXY_CMD_PROXY:
+                       pass # Fallthrough
+
+               # We don't know any other commands
+               else:
+                       log.debug("Unknown PROXY command %02x" % command)
+                       return
+
+               # Extract protocol
+               family   = (fam_prot >> 4)
+               protocol = (fam_prot & 0x0f)
+
+               # LOCAL command use family == AF_UNSPEC
+               if command == PROXY_CMD_LOCAL and family == PROXY_FAMILY_UNSPEC:
+                       pass
+
+               # We accept IPv6 and IPv4
+               elif family in (PROXY_FAMILY_INET6, PROXY_FAMILY_INET):
+                       pass
+
+               # Everything else we don't know how to handle here
+               else:
+                       raise ProxyError("Unknown family %s" % family)
+
+               # LOCAL commands use protocol == UNSPEC
+               if command == PROXY_CMD_LOCAL and protocol == PROXY_PROTO_UNSPEC:
+                       pass
+
+               # Otherwise we only support TCP
+               elif protocol == PROXY_PROTO_STREAM:
+                       pass
+
+               # We don't know how to handle anything else here
+               else:
+                       raise ProxyUnsupportedError("Unknown or unsupported protocol %s" % protocol)
+
+               # Read the next part of the header into the buffer
+               buffer = await stream.read_bytes(length)
+
+               # Check if we read enough data
+               if len(buffer) < length:
+                       raise ProxyError("Header too short")
+
+               # Unpack IPv6 addresses
+               if family == PROXY_FAMILY_INET6:
+                       addresses = proxy_addr_v6.unpack(buffer[:proxy_addr_v6.size])
+
+                       src_address = ipaddress.IPv6Address(bytes(addresses[:16]))
+                       src_port    = addresses[-2]
+                       dst_address = ipaddress.IPv6Address(bytes(addresses[16:32]))
+                       dst_port    = addresses[-1]
+
+                       # Truncate buffer
+                       buffer = buffer[proxy_addr_v6.size:]
+
+               # Unpack IPv4 addresses
+               elif family == PROXY_FAMILY_INET:
+                       src_address, dst_address, src_port, dst_port = proxy_addr_v4.unpack(
+                               buffer[:proxy_addr_v4.size],
+                       )
+
+                       # Convert IP addresses
+                       src_address = ipaddress.IPv4Address(src_address)
+                       dst_address = ipaddress.IPv4Address(dst_address)
+
+                       # Truncate buffer
+                       buffer = buffer[proxy_addr_v4.size:]
+
+               # Handle UNSPEC
+               elif family == PROXY_FAMILY_UNSPEC:
+                       pass
+
+               # Log the result
+               if src_address and dst_address:
+                       log.debug("Accepted new connection from %s:%s to %s:%s" \
+                               % (src_address, src_port, dst_address, dst_port))
+
+               # Return the source address
+               return "%s" % src_address, src_port