Add -p/--port-forward: iptables DNAT into extern-joined containers

Commit 5/6 of the network isolation feature (docs/networking-design.md).
port_forward.{h,cpp}: parse_port_forward_spec() parses
"[<network>:]<host-port>:<container-port>"; add_port_forward()
resolves the network (by name, or the container's sole extern network)
against join_networks()'s result and adds the DNAT/FORWARD rules;
remove_port_forward() undoes them. join_networks() (network_join.{h,cpp})
now returns the joined networks with their assigned IPs (was a bare
bool) so port-forward setup knows where to send traffic. -p requires
-r, is repeatable, network names may no longer contain ':' (needed to
keep the spec syntax unambiguous -- is_valid_network_name(),
network_subnet.h).

Two real corrections from testing, not assumed:
- The DNAT rule needs both nat PREROUTING and nat OUTPUT -- PREROUTING
  never sees locally-generated packets (e.g. curl run on the same
  host), only OUTPUT does. PREROUTING-only left the host's own real IP
  connection-refused despite the container being directly reachable.
- curl localhost:<port> still doesn't work even with both chains --
  a separate problem, NAT hairpinning: the container sees an inbound
  packet claiming a loopback source arriving on a non-loopback
  interface and drops it as martian. A net.ipv4.conf.*.route_localnet
  sysctl was tried and confirmed not to fix this alone, then removed
  rather than left in as dead code. Not solved here (would need scoped
  source masquerading or a userland proxy); curl <host's real IP> is
  the actually-relevant, verified-working path for real clients.

Also surfaced (unrelated to -p, found while testing it, not fixed
here): -x/--exec doesn't join the net namespace -- written when this
project never isolated networking at all -- so it currently sees the
host's own network stack instead of a network-isolated session's own.

Verified end-to-end as root (via a scoped doas rule): a container
serving HTTP on an extern network with -p 8080:80 was reachable via
curl <host's real IP>:8080; the rule was confirmed gone after the
session was killed.
This commit is contained in:
2026-08-30 13:28:23 +00:00
parent 8cc967e748
commit 5c87cac430
13 changed files with 528 additions and 49 deletions
+13
View File
@@ -266,6 +266,19 @@ pass.
- Verify: `-p 8080:80` against a container on an extern network answering
on port 80 is reachable via `curl localhost:8080` from the host; the
rule is gone after the container exits.
- **Landed with two real corrections found by testing** (see
`CLAUDE.md`'s `port_forward.{h,cpp}` entry for the full detail): the
`DNAT` rule needs both `PREROUTING` *and* `OUTPUT` (locally-generated
traffic never traverses `PREROUTING`); and `curl localhost:<port>`
specifically still doesn't work even so (NAT hairpinning — the
container sees an inbound packet claiming a loopback source on a
non-loopback interface and drops it as martian) — verified instead via
`curl <host's real IP>:<port>`, the actually-relevant path for real
clients. Also surfaced, unrelated to `-p` itself but found while
testing it: `-x/--exec` doesn't join the `net` namespace (written back
when this project never isolated networking at all), so it currently
sees the *host's* network stack, not a network-isolated session's own —
not fixed as part of this commit.
6. **Crash-orphan cleanup sweep**
- Extend `--clean-processes` (or add a dedicated `--clean-networks`,