fix(podmanx,docs): close the host.containers.internal back-channel for container runs #271
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab!271
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/216"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #216
What was wrong
ADR-0052 claimed pasta gives "full egress with no route back to the host's services … never a host port." That held only for loopback-bound host services. Podman appends
--map-guest-addr 169.254.1.2to its pasta defaults, which mapshost.containers.internalonto the host's global address — so every wildcard-bound (0.0.0.0) host service, lab's own operator server on--addr :8080included, was reachable from inside every run container.--no-map-gw(podman's default) closes only the loopback/gateway path.Reproduced live from inside a run container on the dev host:
host.containers.internal:8080→ HTTP 200, while the guest's own127.0.0.1:8080failed.What changed
RunArgv— the single renderer for all three shapes (run pane, login pane, non-interactive CLI poke) — now pins:Both are needed, and this is the one place the PR goes beyond the issue's stated mechanism. The pasta option alone is not sufficient: verified in containers/common + podman source, when no address is mapped podman does not drop the
/etc/hostsnames —GetHostContainersInternalIPfalls through toutil.GetLocalIPExcluding, which returns the first other global-unicast host address (RFC1918 included; only loopback/link-local/multicast are ruled out). On the dev host that fallback lands on the bridge address172.17.0.1, where lab's:8080also answers — measured, HTTP 200 — so the option by itself would have left the leak intact on this exact host. The--add-hostpins claim both names first; podman'saddEntriesIfNotExiststhen skips its automatic entry for names already taken. Two flags rather than thename;name:ipform, sinceparseExtraHostssplits on;only on recent podman and the preflight floor is podman >= 4.The pasta option composes with podman's other defaults rather than replacing them (containers/common skips only the
--map-guest-addrit would otherwise append, still adding--no-map-gw,-t/-u/-T/-U none,--dns-forward 169.254.1.1), so egress and DNS forwarding are untouched.Preflight
The existing
pastacheck widens in place — same check id, now "missing or too old", mirroring howpodmanalready covers missing/unrunnable/too-old. It runspasta --map-guest-addr none --versionand judges exit status alone; it only runs when pasta is on PATH, so a missing pasta is never blamed twice.It probes the exact option and value the argv uses, deliberately, because the floor has two stages:
--map-guest-addrnone2024_08_212024_08_21…2025_03_20conf_nat()matched the literal without returning, fell intoinet_pton, diedInvalid address to remap to host: none)2025_04_15A version gate — or a
pasta --helpgrep — would call that middle window green, and podman's backwards-compat retry covers only the default it appends, never a user-supplied option, so those hosts would fail at container start, once per spawn, behind a green preflight. The argument order is load-bearing (--versionfirst would exit 0 on every passt ever built); the test suite pins it, verified with a negative control.Residual, documented not papered over
pasta still grants full egress, so a wildcard-bound host service on a non-shadowed host address stays reachable by raw IP (
172.17.0.1:8080still answers). The host's own primary address is unreachable — the guest shadows it. Binding co-located host services to127.0.0.1is therefore the control on a multi-address host, not defense in depth; egress filtering is explicitly out of scope for #216. ADR-0052,docs/ops.mdand the DoD all say this plainly now, and the DoD's manual-residue probe records both the expected failure (host.containers.internal) and the expected success (raw IP).Verification
go build ./...,go vet ./...,go test ./...— all greengolangci-lint run ./...at CI's pinned v2.12.2 — 0 issues;gofmt -lcleaninternal/instance's argv tests build their expectation throughRunArgv, so they cover the change automaticallyconf.c/passt.1at the specific release tags and against containers/common + podman source, not from memory.[autoland] verdict: pass
PASS — validated against the validation core.
Signal relied on:
labctl pr checks 271 --wait→ aggregate success (ci / native (pull_request), 12m17s). That job is unconditional onpull_requestand runs the SPA lint/test/build,go build+go test(tags ui), and golangci-lint v2.12.2 — so its reach covers every file in this diff.ci-nixis path-gated on**/*.nix/flake.lock/go.modandagent-toolsoncontainers/**; this diff touches none of those, so their absence is correct, not a coverage gap. No re-run of what CI already ran.Conventions: Conventional Commits title (
fix(podmanx,docs):) ✓ ·Closes #216present in the body ✓ · headafk/216matches the linked issue ✓ · 1 commit ahead oforigin/main, 0 behind, merge-base is main's tip — no conflict.Diff-scope: matches #216's agent brief. All three
RunArgvshapes lose the mapping, the five golden argv tests are updated (podmanx run/login/CLI + providercli login/CLI), the preflight widens in place under the samepastacheck id, and ADR-0052 / ops.md / the DoD are rewritten. The--add-hostpins go beyond the brief's literal mechanism, but they serve its stated desired behavior ('no host.containers.internal mapping at all') and the PR flags them explicitly — not a drive-by.Independently verified every load-bearing upstream claim against real source rather than taking the PR's word:
passt
conf.c—--version(case 14, l.1478) and--map-guest-addr(case 22 →conf_nat, l.1530) are handled in the SAME getopt_long pass, which endscase '?': default: usage(argv[0], stderr, EXIT_FAILURE)(l.1819). So the probe's argument order really is load-bearing exactly as documented: old passt → '?' → non-zero; middle-window passt →conf_natdies before--versioncan exit 0; modern passt → parsesnone, then exits 0. Reversed, it would be a guaranteed false green.conf_nat'snonebranch has the earlyreturn, and the fall-throughdie("Invalid address to remap to host: %s")matches the test fixture's string verbatim.containers/common
libnetwork/pasta/pasta_linux.go— the composition claim holds: podman appends its--map-guest-addr 169.254.1.2only when the user supplied none (l.272-275), while--no-map-gw(l.261) is cleared solely by the pseudo-option--map-gw, never by--map-guest-addr;--dns-forwardand the-t/-u/-T/-U nonedefaults are untouched. Egress and DNS are genuinely unaffected. The backwards-compat retry (l.76-84) is explicitly gated onmapGuestAddrIPs[0] == mapGuestAddrIpv4, so it never fires for a user-supplied value — which is precisely why the preflight check has to exist.containers/common
libnetwork/etchosts/hosts.go—writeHostFilewrites user--add-hostentries first and registers their names, thenaddEntriesIfNotExistsfilters podman's automatic entry down to its free names. Podman's entry carries bothhost.containers.internalandhost.docker.internal, so pinning both leavesfreeNamesempty and podman writes nothing — pinning only one would have left the other live on the fallback address. The two-flag form also sidestepsparseExtraHosts';splitting, consistent with the podman >= 4 floor.Test coverage is proportionate: preflight gains both floor cases (option unknown / value rejected) plus a strengthened pasta-missing case proving the probe is skipped so one host is never blamed twice under one check id.
The documented residual (raw-IP reachability of wildcard-bound services on non-shadowed host addresses) is stated plainly in ADR-0052, ops.md and the DoD rather than papered over, and egress filtering is explicitly out of scope for #216. No findings rise to CONCERNS.