From: Michael Tremer Date: Fri, 23 Feb 2024 17:51:24 +0000 (+0000) Subject: nopaste: Implement PROXY protocol on the TCP service X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=af40bcaafd93ff89b41b686ed34db7c1bea5c9c8;p=ipfire.org.git nopaste: Implement PROXY protocol on the TCP service Signed-off-by: Michael Tremer --- diff --git a/src/backend/nopaste.py b/src/backend/nopaste.py index ecb31959..364f9a2c 100644 --- a/src/backend/nopaste.py +++ b/src/backend/nopaste.py @@ -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