"""The Docker host's own LAN address, for links to published ports. A port published on 0.0.0.0 is reachable at the host's address, but the browser only knows the address it reached StackPilot at. Behind a reverse proxy that is the proxy's domain, and ``domain:8080`` lands on the proxy, not on the container. So the host's address is read here instead: the IPv4 address on the interface that carries the host's default route. """ from __future__ import annotations import ipaddress import os import socket import struct from typing import Optional from config import settings _RTF_UP = 0x1 def _host_net_file(name: str) -> Optional[str]: """``${HOST_PROC_PATH}/1/net/`` — the host's namespace, see ``port_service._proc_net_path``. There is deliberately no fallback to ``/proc/net``: in a bridged container that is the container's own namespace, whose 172.x address no browser can reach.""" path = os.path.join(settings.HOST_PROC_PATH, "1", "net", name) return path if os.path.isfile(path) else None def _hex_ip(value: str) -> ipaddress.IPv4Address: """/proc/net/route prints addresses as little-endian hex.""" return ipaddress.IPv4Address(socket.inet_ntoa(struct.pack(" list[ipaddress.IPv4Network]: """The on-link networks of the interface with the best default route.""" defaults: list[tuple[int, str]] = [] links: list[tuple[str, ipaddress.IPv4Network]] = [] for line in route_text.splitlines()[1:]: cols = line.split() if len(cols) < 8: continue iface, dest, _gw, flags, _ref, _use, metric, mask = cols[:8] try: if not int(flags, 16) & _RTF_UP: continue dest_ip, mask_ip = _hex_ip(dest), _hex_ip(mask) metric_n = int(metric) except (ValueError, struct.error, OSError): continue if int(dest_ip) == 0 and int(mask_ip) == 0: defaults.append((metric_n, iface)) else: try: links.append((iface, ipaddress.IPv4Network(f"{dest_ip}/{mask_ip}"))) except ValueError: continue if not defaults: return [] iface = min(defaults)[1] return [net for name, net in links if name == iface] def _local_addresses(fib_text: str) -> list[ipaddress.IPv4Address]: """Addresses the kernel lists as ``/32 host LOCAL`` in fib_trie.""" found: list[ipaddress.IPv4Address] = [] last: Optional[str] = None for line in fib_text.splitlines(): stripped = line.strip() if stripped.startswith("|--"): last = stripped[3:].strip() elif last and stripped.startswith("/32 host LOCAL"): try: addr = ipaddress.IPv4Address(last) except ValueError: continue if not addr.is_loopback and addr not in found: found.append(addr) return found def detect_host_ip() -> Optional[str]: """The host's address on its default-route interface, or None when the host's /proc is not mounted or the host has no IPv4 default route.""" route_path, fib_path = _host_net_file("route"), _host_net_file("fib_trie") if not route_path or not fib_path: return None try: with open(route_path, "r", encoding="utf-8") as fh: networks = _default_route_networks(fh.read()) with open(fib_path, "r", encoding="utf-8") as fh: addresses = _local_addresses(fh.read()) except OSError: return None for addr in addresses: if any(addr in net for net in networks): return str(addr) return None