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
+1 -1
View File
@@ -23,7 +23,7 @@ slocker_lite = executable('slocker-lite',
'src/pid_file.cpp', 'src/exec_session.cpp', 'src/env_spec.cpp', 'src/daemonize.cpp',
'src/sandbox_process.cpp', 'src/session_cgroup.cpp', 'src/kill_session.cpp',
'src/network_subnet.cpp', 'src/persistent_netns.cpp', 'src/network_bridge.cpp',
'src/network_join.cpp'],
'src/network_join.cpp', 'src/port_forward.cpp'],
include_directories : include_directories('.'),
dependencies : [fmt_dep, catch2_dep, yaml_dep, archive_dep, json_dep, spdlog_dep],
install : true)