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
+5 -4
View File
@@ -95,6 +95,7 @@ slocker-lite -V|--version
| `-n, --network <name>` | Create/manage a persistent named network: requires exactly one of `--extern` (a real Linux bridge in the host's own namespace, with NAT/forwarding set up so containers on it reach the host's real network) or `--intern` (a bridge inside its own dedicated, routeless namespace, only reachable by other containers on the same network). `--subnet <cidr>` overrides the auto-allocated IPv4 range (`10.168.0.0/24`, incrementing per network); `--no-ipv6` disables (and `--subnet6 <cidr>` overrides) the auto-allocated IPv6 range, on by default. With `--run`, instead joins `<name>` to the container as its own `eth<N>` interface with an address from the network's subnet; repeatable, no membership limit. Root-only for now. See [`docs/networking-design.md`](docs/networking-design.md). |
| `--list-networks` | List all named networks (see `-n/--network`) with their kind, IPv4 subnet, and IPv6 subnet (or `(no ipv6)`). |
| `--delete-network <name>` | Remove a named network from the config. |
| `-p, --port-forward [<network>:]<host-port>:<container-port>` | With `--run`, forward a TCP port from the host into the container. `<network>` is optional, defaulting to the container's sole `--extern` network (an error if it joined more than one without specifying). Repeatable. Reachable via the host's real, externally-facing IP; `localhost`/loopback access has a known NAT-hairpinning limitation (see [`docs/networking-design.md`](docs/networking-design.md)). |
| `--list-processes` | List running `--run` sessions found by their pid files under `$XDG_STATE_HOME/slocker-lite/run/`, with their pid, container name, and status (`running` or `exited`). |
| `--clean-processes` | Remove stale pid files (see `--list-processes`) left behind by sessions that are no longer running. |
| `-w, --write-config` | Write a complete config file (creating it, and its parent directory, if missing), filling in every option's current or default value. Useful to bootstrap one for hand-editing. Prints the config file's full path. |
@@ -206,10 +207,10 @@ is likewise managed by `-n/--network` rather than hand-edited — see
persistent-network feature design. `-n/--network` both creates a network
(standing up its real bridge/iptables state, root-only) and, combined with
`-r/--run`, joins a container to one or more of them with a real veth
interface and address on each. `-p` port forwarding isn't implemented yet
(a later commit), so nothing outside the container can reach in until then.
A missing config file is fine either way (nothing is overridden, and one
gets created the first time `-v/--volume`/`-n/--network` is used).
interface and address on each; `-p/--port-forward` then forwards a host port
into a container on one of its `--extern` networks. A missing config file is
fine either way (nothing is overridden, and one gets created the first time
`-v/--volume`/`-n/--network` is used).
Run `-w/--write-config` to bootstrap a config file: it writes out every
supported option explicitly (filling in the current or default value for