From c37d1f09c62c77cd1faa39c1131e5db42b84b0eb Mon Sep 17 00:00:00 2001 From: anti Date: Mon, 20 Apr 2026 23:37:59 -0400 Subject: [PATCH] feat(deployer): warn when userland-proxy masks attacker source IPs MazeNET publishes gateway ports on the host via Docker. With the default userland-proxy enabled, attacker connections appear to originate from the bridge gateway instead of the real remote IP. Log a soft warning at deploy time when the topology publishes any ports and docker info reports UserlandProxy=true, pointing the operator at the daemon.json toggle. Best-effort: daemon talk failures silently no-op. --- decnet/engine/deployer.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/decnet/engine/deployer.py b/decnet/engine/deployer.py index c9c6bbab..8db85014 100644 --- a/decnet/engine/deployer.py +++ b/decnet/engine/deployer.py @@ -311,6 +311,33 @@ def _topology_compose_path(topology_id: str) -> Path: return Path(f"decnet-topology-{topology_id[:8]}-compose.yml") +def _warn_if_userland_proxy_enabled(hydrated: dict) -> None: + """Soft warning: docker-proxy masks attacker source IPs. + + Only log if the topology will publish ports (gateway deckies with + ``forwards_l3=True``) — no point scaring operators on port-less + topologies. Best-effort: any failure talking to the daemon is + silently ignored. + """ + publishes = any( + (d.get("decky_config") or {}).get("forwards_l3") + for d in hydrated.get("deckies", []) + ) + if not publishes: + return + try: + info = docker.from_env().info() + except Exception: + return + if info.get("UserlandProxy") or info.get("Userland Proxy"): + log.warning( + "[USERLAND_PROXY] docker-proxy is enabled; attacker source IPs " + "will appear as the bridge gateway. Set " + '"userland-proxy": false in /etc/docker/daemon.json to preserve ' + "real source IPs." + ) + + @_traced("engine.deploy_topology") async def deploy_topology(repo, topology_id: str, *, dry_run: bool = False) -> None: """Deploy a persisted MazeNET topology. @@ -349,6 +376,8 @@ async def deploy_topology(repo, topology_id: str, *, dry_run: bool = False) -> N for w in check_no_host_port_collision(hydrated): log.warning("[%s] %s", w.code, w.message) + _warn_if_userland_proxy_enabled(hydrated) + await transition_status(repo, topology_id, TopologyStatus.DEPLOYING) client = docker.from_env()