diff --git a/server.py b/server.py index 91e4561a..147d61d3 100644 --- a/server.py +++ b/server.py @@ -44,39 +44,34 @@ from protocol import BinaryEventTypes # Import cache control middleware from middleware.cache_middleware import cache_control +class NoListenSocket(socket.socket): + """ + A socket that pretends to be already listening. + The overridden ``listen`` simply returns without calling the kernel. + """ + def listen(self, backlog: int = socket.SOMAXCONN, *args: Any, **kwargs: Any) -> None: # type: ignore[override] + # If the socket is already in LISTEN state the kernel will reject a + # second listen() with EPERM (which is what triggered the SELinux + # denial). By turning it into a no‑op we avoid that system call. + # The socket is still usable by asyncio because it was created by + # systemd with ``listen`` already performed. + logging.debug("NoListenSocket.listen() called – no‑op (socket already listening)") + return None + # Helper -------------------------------------------------------------------- def _fd_to_socket(fd: int) -> socket.socket: """ - Convert a file‑descriptor received from systemd into a ready‑to‑use - ``socket.socket`` object. + Convert a file‑descriptor received from systemd into a socket that aiohttp + can use *without* performing a second listen(). - * The fd already contains the correct address family, socket type and - protocol – passing ``fileno=fd`` makes the constructor adopt them. - * aiohttp (and the rest of the code) expects the socket to be **non‑blocking**, - so we set that flag explicitly. - * Any OSError while wrapping the descriptor is turned into a RuntimeError - with a clear message; the caller can log it. """ try: - # ``socket.socket`` with the ``fileno`` argument re‑uses the existing OS - # socket. The resulting object automatically reports the correct - # ``family``, ``type`` and ``proto`` attributes. - sock = socket.socket(fileno=fd) - - # Allow aiohttp to call listen() again without EPERM. - # On many Linux kernels the flag must be set *before* the first listen(), - # but systemd binds the socket without it. Setting it now is sufficient - # for the second listen() performed by aiohttp. - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - - # aiohttp’s SockSite registers the socket with the event loop, which - # requires a non‑blocking descriptor. + sock = NoListenSocket(fileno=fd) sock.setblocking(False) - return sock except OSError as exc: - # Raising a RuntimeError makes the caller’s ``except`` clause simpler. - raise RuntimeError(f"Could not wrap fd {fd} as a socket: {exc}") from exc + raise RuntimeError(f"Could not wrap fd {fd} as a NoListenSocket: {exc}") from exc + async def send_socket_catch_exception(function, message): try: