8000 fix(minimald): refuse a host-exposure flow past the tracking bound · gominimal/minimal@b3404e9 · GitHub
[go: up one dir, main page]

Skip to content

fix(minimald): refuse a host-exposure flow past the tracking bound

Minimal Arch / arch-review failed Nov 4, 2026

CONDITIONS

Baseline. This re-reviews b3404e9d against the CONDITIONS on ae6537f2 (this PR). I checked these facts. since.diff equals git diff ae6537f2 b3404e9d. pr.diff equals git diff d61577fd b3404e9d. The merge 0685ca19 is clean (git diff-tree --cc is empty), so the crates/minvmd/src/box_registry.rs hunks in the compare come from main (#2967) and are not ruled here. The one branch commit makes record_host_flow() return whether it tracked the flow, and refuses a new flow past HOST_FLOWS_PER_BOX. The ledger staged both baseline why fields cut short, and gh is not on this host. So I did not read the baseline condition text word for word. I re-read the whole change against the design and ruled it fresh. C1 and C2 can be new findings, not departures from the baseline.

Governs: specs/networking/deployment-and-egress-gateway.md §7.1, degraded-mode profile. Line 248: box→host reach over the alias is default-deny except configured exposures, under the box's egress rules. Line 250: the host-side creator reads the table as the current table for each decision, and a removed exposure is revoked with its established flows terminated. Line 251: the box half is host.min.internal in allow_dns_hosts, with CIDR coverage of the alias as the fall-through. Line 252: Minimal's own listeners are never exposures, and the exclusion is decided at each flow ahead of the exposure openings. Line 253: the rule applies to own_ip on every host, and a native host_ip box is not gated. The checkout 7a86ec7b has no arch-map.md, so I found the sections by search.

Ruling. NET-160 conforms. The box half, the deny_subnets subtraction, the allow_protocols hold and the fail-closed parse match lines 248 and 251. The gate is only on the native own-address attach (line 253). NET-161 conforms on the alias itself, and the compare closes the untracked-flow gap. Two gaps remain. C1: the change opens the native alias at any port the table names, ahead of egress::verdict. The native gvproxy NATs the alias to 127.0.0.1, where minimald binds its hostname proxy and box-zone answerer. A table entry at either port then opens a Minimal listener to a box, and line 252 forbids that at every flow. C2: on a carved switch, host_alias_ingress() does not match the documented literal as a source, so a host segment from the literal on a revoked flow reaches the box (line 250).

Residual. Config still rejects [host-exposures], so min breaks on the file that this gate reads. #2689 owns that field (commit d7a44b99 on its branch).

Not ruled. NET-162's load-time half (refuse and name the holder when the table loads, and report a shadowed entry in min host show) belongs to the task that covers NET-162. Spec 18 lists only a minvmd verify line for NET-162. The #2689 branch adds a second reader of this table, crates/minvmd/src/net/host_exposures.rs HostExposures. It skips a bad entry, but this reader empties the whole table. Both fail closed. Line 250 makes this one table per user per machine, so whichever PR lands second needs to make one reader serve both. That is a plan question for the merge order, not a design question, so I filed no escalation.

Not checked. I did not run the three relay tests (no build). I did not check whether a UDP reply from an admitted alias flow clears blocked_udp() after host_alias_ingress() returns Admit. The tests check only the egress leg for UDP.

Conditions

  • C1: crates/minimald/src/net/switch.rs BoxGate::host_alias_egress() refuses a frame whose destination port is held by the daemon's own loopback listeners, the hostname proxy (hostname_proxy_port) and the box-zone answerer (zone_answerer_port), before it reads table.current(), and decides this at each flow, not once. Hand the held ports to the gate at the native attach in attach_to_switch(), beside use_host_exposures(), and read them live so a listener that binds later is excluded from that moment. The refusal line keeps rule_matched="host-alias" and names the holder in refused_by. Add a relay test: a box that names the host is refused at a table entry on the hostname proxy's port.
  • C2: crates/minimald/src/net/switch.rs BoxGate::host_alias_ingress() treats a frame whose source is the documented host literal as a frame from the alias when the switch maps the literal (SwitchSubnet::documented_host_literal() is Some), so a host segment from the literal on a revoked exposure is refused and both ends get the reset. Store the mapped literal on the gate at construction, as LegacyHostNotice::for_gate() does. Add a test on a carved subnet: after the entry is removed, a host-first segment from 100.64.255.254 does not reach the box.

Ruled at b3404e9 against gominimal/arch@7a86ec7bb3c0 by claude-opus-5-5; arch_rulings 841, job 5aab3c56-dc97-4ede-b53f-f4ab493f6b48.

0