From a687c05f6c978c1474ff0ae3247b068625817673 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:20:03 +0000 Subject: [PATCH] Fix _get_listening_ports() to reliably detect wildcard-bound ports (80/443) Rewrite the ss output parser to: - Skip header lines (State/Netid) explicitly - Only process LISTEN/UNCONN state lines - Always read parts[3] for local address (the ss column layout is fixed) - Defensively skip wildcard (*) port values The previous fix (PR #235) tried both parts[3] and parts[4], but reading parts[4] (peer address column) was unnecessary. The ss LISTEN output always places the local address at index 3 when split by whitespace, for all address formats: 0.0.0.0:PORT, *:PORT, [::]:PORT, 127.0.0.1:PORT. Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/f7ab1d7c-d624-4f1a-9e62-5a9ce4fd4446 Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com> --- app/sovran_systemsos_web/server.py | 38 ++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py index 78efd4a..c170813 100644 --- a/app/sovran_systemsos_web/server.py +++ b/app/sovran_systemsos_web/server.py @@ -751,6 +751,14 @@ def _get_listening_ports() -> dict[str, set[int]]: Uses ``ss -tlnp`` for TCP and ``ss -ulnp`` for UDP. Returns a dict with keys ``"tcp"`` and ``"udp"`` whose values are sets of integer port numbers. + + The ``ss`` LISTEN/UNCONN output has a fixed column layout when split on + whitespace: ``State Recv-Q Send-Q Local_Address:Port Peer_Address:Port ...`` + The local address is always at index 3, regardless of address format + (``0.0.0.0:PORT``, ``*:PORT``, ``[::]:PORT``, ``127.0.0.1:PORT``). + + Header lines (``State``/``Netid``) and non-LISTEN/UNCONN rows are skipped + so only truly active listeners are returned. """ result: dict[str, set[int]] = {"tcp": set(), "udp": set()} for proto, flag in (("tcp", "-tlnp"), ("udp", "-ulnp")): @@ -760,21 +768,27 @@ def _get_listening_ports() -> dict[str, set[int]]: capture_output=True, text=True, timeout=10, ) for line in proc.stdout.splitlines(): - # The local address:port column varies by ss output format: - # - "0.0.0.0:PORT" style lines have extra spacing that puts the - # local address at index 4 (zero-based). - # - "*:PORT" style lines (dual-stack/wildcard, used by Caddy) - # have the local address at index 3, with the peer at index 4. - # Try both columns so neither format is silently skipped. parts = line.split() if len(parts) < 5: continue - for addr in (parts[3], parts[4]): - port_str = addr.rsplit(":", 1)[-1] - try: - result[proto].add(int(port_str)) - except ValueError: - pass + # Skip header lines + if parts[0] in ("State", "Netid"): + continue + # Only process LISTEN (TCP) or UNCONN (UDP) state lines + if parts[0] not in ("LISTEN", "UNCONN"): + continue + # Local address is always at column index 3: + # State Recv-Q Send-Q Local_Address:Port Peer_Address:Port ... + # Formats: 0.0.0.0:443, *:443, [::]:443, 127.0.0.1:443 + local_addr = parts[3] + port_str = local_addr.rsplit(":", 1)[-1] + # Defensively skip wildcard port (e.g. an unbound socket showing *:*) + if port_str == "*": + continue + try: + result[proto].add(int(port_str)) + except ValueError: + pass except Exception: pass return result