Document the -p/--port-forward extern-connectivity fix
Records the root-cause investigation and fix for -p/--port-forward not reaching a server on an extern network (host root had no route to the container subnet, and the route alone wasn't consulted without a matching ip rule -- Android's policy routing has no default "lookup main" rule), plus the related uplink-rollback-on-failure robustness bug found while testing it, matching the level of detail already recorded for the other networking fixes in this document. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
@@ -649,6 +649,79 @@ complete trace that correctly captured the real bug.
|
||||
networks joined simultaneously in one session, all gateways reachable at 0%
|
||||
packet loss, clean teardown, no leftover state.
|
||||
|
||||
## Resolved: `-p/--port-forward` had no connectivity at all on `extern`
|
||||
|
||||
**Reported after the fixes above had already shipped**: a server listening
|
||||
on an `extern` network wasn't reachable via `-p` at all — not from the host,
|
||||
and not from a real outside client — even though that same network's own
|
||||
gateway and outside connectivity (the uplink fix earlier in this document)
|
||||
worked correctly. `intern` was never affected (`-p` only ever targets
|
||||
`extern` networks by design, since an `intern` network's bridge has no path
|
||||
from the host at all).
|
||||
|
||||
**Root cause**: moving `extern`'s bridge into a private namespace (the fix
|
||||
above) restored gateway reachability but, as a direct side effect, also left
|
||||
host root with no route to the container subnet whatsoever — confirmed
|
||||
directly: `ip route get <container-ip>` from host root fell through to
|
||||
whatever the host's own default route happened to be (the real LAN
|
||||
gateway), not the container's actual namespace. `-p`'s own `DNAT` rule
|
||||
(`port_forward.cpp`) is added in host root and targets the container's real
|
||||
IP directly, so the rewritten packet had nowhere to go — the connection
|
||||
simply timed out, both from `curl <real LAN IP>:<port>` on the host itself
|
||||
and from a separate machine on the same LAN.
|
||||
|
||||
**Fix, two independently necessary pieces** — confirmed by testing, the same
|
||||
"a route alone isn't enough on Android" lesson the uplink's own
|
||||
outbound/return-path `ip rule`s (above) already learned, just for the
|
||||
container subnet instead of the uplink's own transit subnet:
|
||||
|
||||
1. A host-root route to the container subnet, through the uplink's own
|
||||
netns-side address (`ip route add <container-subnet> via <uplink-netns-
|
||||
addr> dev <uplink-root-tap>`). From there, no second NAT stage is
|
||||
needed: the private namespace already forwards arriving traffic to the
|
||||
bridge on its own (a fresh namespace's `FORWARD` policy is `ACCEPT` by
|
||||
default, and `ip_forward` is already enabled), and the reply's return
|
||||
path is already covered by the private namespace's own default route
|
||||
back out through this same uplink, plus host root's own conntrack
|
||||
correctly reversing the original `DNAT` on the way back out.
|
||||
2. A matching `ip rule add priority 100 to <container-subnet> lookup main`.
|
||||
Without this, the route added in step 1 is silently never consulted at
|
||||
all: confirmed via `ip rule show` on the real device that Android's own
|
||||
policy routing has no default "lookup main" rule anywhere in its list (it
|
||||
ends in a catch-all `unreachable` before priority 32766, where that
|
||||
default would normally live) — a generic `fwmark 0/0x10000 lookup 99`
|
||||
rule (matching any untouched/forwarded packet, arriving on a real
|
||||
interface rather than locally generated) intercepts the packet first and
|
||||
routes it into an unrelated table with no route to the container subnet,
|
||||
long before rule evaluation would ever reach `main`.
|
||||
|
||||
**A related robustness bug found while testing this, not the original bug
|
||||
itself**: reproduced directly during testing when two concurrent test
|
||||
invocations collided (an incidental accident of testing on a live device,
|
||||
not a deliberate scenario) — a first failed `ensure_uplink_provisioned()`
|
||||
call only stopped the tap relay, leaving every `ip rule`/`iptables` piece it
|
||||
had already added (all deterministic, hash-derived names tied to the
|
||||
network name) live on the host. That leftover state then made every
|
||||
subsequent attempt to create a network with the *same name* fail
|
||||
identically and permanently — `ip rule add` returning `File exists` against
|
||||
a rule nothing had ever removed — with no way to recover short of a manual
|
||||
fix or a full device reboot. Fixed by recording the relay's pid to the
|
||||
uplink state file as soon as it's known (before any of the steps that can
|
||||
fail), so a failure can call the exact same `teardown_uplink_state()` a real
|
||||
`--delete-network-full` would use to roll back everything already added,
|
||||
instead of a second, partial copy of that cleanup logic.
|
||||
|
||||
**Verified end-to-end on the real target device**: a busybox `httpd` on a
|
||||
freshly (re-)created `extern` network, reached via `-p 18080:80` — a `curl`
|
||||
from the device's own shell against its real LAN IP got a genuine HTTP
|
||||
response, and, separately, a `curl` from a completely different machine on
|
||||
the same LAN got the same result. Reproduced reliably across repeated fresh
|
||||
network creations, including after a full device reboot with no leftover
|
||||
state. `curl 127.0.0.1:<port>` (from the host, against loopback
|
||||
specifically) still doesn't work — that's the pre-existing, already-
|
||||
documented NAT-hairpinning limitation above (see "Port forwarding (`-p`)"),
|
||||
unrelated to this fix and out of scope here.
|
||||
|
||||
## IPv6: no NAT (MASQUERADE), by design
|
||||
|
||||
**Trigger**: the real target device's `ip6tables` build doesn't support a
|
||||
|
||||
Reference in New Issue
Block a user