Skip to content

Add support of PROXY protocol for TLS (and without as well) web server #11252

@socketpair

Description

@socketpair

Sometimes it's required to pass TLS connections through a balancer without terminating TLS there.

https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt

Support of such protocol is easy. Although some security measures should be applied. For example, users of AioHTTP should explicitly mark somehow that PROXY protocol expected. Without this (i.e. autodetection) malicious user may send PROXY protocol to alter source ip information.

class _SSLProtocolProxyMonkeyPatch(asyncio.sslproto.SSLProtocol):
    def __init__(self, *args, **kwargs) -> None:
        self._probe_buffer = bytearray()
        self._ssl_buffer: bytearray
        self._ssl_buffer_view: memoryview
        super().__init__(*args, **kwargs)
        self._original_buffer_updated = super().buffer_updated

    @override
    def buffer_updated(self, nbytes: int) -> None:  # pylint: disable=method-hidden
        self._probe_buffer.extend(self._ssl_buffer_view[:nbytes])
        if len(self._probe_buffer) > 5 and b'PROXY' not in self._probe_buffer:
            self.__restore_original(self._probe_buffer)
            return

        # Handles textual protocol .There is another version of PROXY protocol - binary. it has fixed record length
        if b'\r\n' not in self._probe_buffer:
            if len(self._probe_buffer) <= 108: # 108 is maximal length for IPv4, make it better
                return
            raise RuntimeError(f'PROXY header is too long: {len(self._probe_buffer)} bytes, expected at most 108 bytes.')

        (header, data) = self._probe_buffer.split(b'\r\n', maxsplit=1)
        unused_proxy, protocol, source_ip, unused_destination_ip, source_port, unused_destination_port = header.decode('ascii').strip().split()
        # TODO: check protocol value, i.e. TCP4 for example
        self._extra['peername'] = (source_ip, int(source_port))
        self.__restore_original(data)

    def __restore_original(self, data: bytearray) -> None:
        self.buffer_updated = self._original_buffer_updated  # type: ignore[method-assign]
        delattr(self, '_original_buffer_updated')
        delattr(self, '_probe_buffer')
        if data:
            self._ssl_buffer[: len(data)] = data
            self.buffer_updated(len(data))

asyncio.sslproto.SSLProtocol = _SSLProtocolProxyMonkeyPatch  # type: ignore[misc]

This is how we monkey-patched. Normal fix is expected.

Related component

Server

Code of Conduct

  • I agree to follow the aio-libs Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions