Repository navigation
T45 — Reach the proxy only as a credentialed lane's infrastructure - #1885
norrietaylor merged 37 commits into
Conversation
The session policy gains a declaration of a credentialed upstream (NET-134): a box that carries it marks the Box Egress Proxy's listener as its own infrastructure. The field is a serde table with no required content, so the proxy document can extend it when it lands, and it is skipped while absent so every stored policy and every reading client keeps the exact shape it already had. `min session activate --credentialed-upstream` carries the declaration onto the wire registration; a client that predates the field declares nothing. minimald passes it into its effective policies, and the literals across minimald's registration paths follow the new field. Whether the declaration holds at the gate is the host's decision, later in this change.
The schema round's new declaration test wrapped a one-line assert across three; rustfmt puts it back on one.
The Box Egress Proxy's listener is a credentialed lane's infrastructure (NET-134), the one host-side destination a box reaches by declaring the upstream and never by allowing its address. The row now carries the declaration — reduced from the client's registration to a fact beside the rules, the way the DNS name dimension travels — and the attachment carries it with the box's other host facts, said aloud on the issue line. The gate reads the row's own fact, never its rules: a frame to the proxy's address is admitted for a row that declared the lane, a deny-all row included, and dropped under the box-to-host default-deny for every source that carries none — an allow-all box, an interim source with no row, and the node plane, whose baseline set holds the address for no category. Each refused frame says so once, rate-limited, naming the box, the proxy's address, the port and the rule. The registration spec literals in minimal follow the new field, as does the activating client's picker path, which creates its sessions without a lane.
Two session-e2e cases carry NET-134 onto a real switch: a deny-all box that declared the upstream reaches the stand-in acceptor at the proxy's address on its listener port and nothing else on the host, and a box with no lane — allow-all rules, so only the lane's absence can refuse — gets silence at the proxy's address while the same box's probe to the host alias is reset, the control that proves the fabric answering. The refusal's half also pins the observability contract: one rate-limited warn line per refused frame naming the box and the reason, the probe's retransmits inside the window notwithstanding. Both cases gate on MINVMD_GVPROXY_BIN like the other switch cases and print what they cannot assert where no switch exists.
…proxy-only-as-a-credentialed-lane-s-in
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
The KVM lane died at the steering case: NET-134's gate drops every frame to the proxy's address from a box that declares no upstream, so the three NET-132 cases whose boxes probe that address lane-less never reached the peer whose steering, resolution or delivery they read. Their boxes now declare the upstream — the lane is the precondition, not the subject — and the ARP case records that its connect is now dropped, since its one fact, the resolution entry, is decided before and apart from the IP gate. The two NET-134 cases keep their shape and get the audit's corrections: the lane case's refused probes go to the host alias itself (100.64.255.254, the host's exposure address) instead of the node's own address, and the no-lane case's control keeps the node's address (100.64.255.253, the in-VM daemon's, reset by the VM's own stack) under its right name. The comments now say where each drop really happens — the box's own deny is decided in its in-VM relay (NET-062) before the switch, the gate's lane arm on the far side — and the warn line's fields (source, rule) and rate limit as the gate emits them. The lane case's positive probe demands the whole path: NET-134's "whatever its egress rules" binds the box's in-VM relay as well as the VM host's gate, and the relay's shared verdict has no lane arm yet, so on a KVM host that probe holds only once the relay carves the proxy's address out of the box's own denies. That carve-out lives in files this task's layers do not name, so the case is left asserting the requirement rather than the tree.
|
Round 1 of 6 on a failed check suite, pushed as
Verify Outside the task's
Converted to a draft: something above needs a person before review. A box without a credentialed lane has every frame to the proxy's address (100.64.255.252) dropped at the VM host's gate — the no-lane e2e case drives that end to end (control reset at the node's own address, silence at the proxy's address, one rate-limited warn line naming the frame's source and the egress-uncredentialed-proxy-destination rule), and a deny-all lane box reaches no other host port; but the positive half is NOT yet delivered end to end: a deny-all box's connection to the proxy's acceptor is dropped inside the guest by the box's in-VM relay, which enforces the box's own 0.0.0.0/0 (NET-062, crates/minimald/src/net/switch.rs applying the shared verdict from crates/sessions/src/core/egress.rs — a verdict with no lane arm) before the frame ever reaches the host gate's lane arm, so deny_all_box_reaches_proxy_and_no_other_host_port fails at its positive probe on a KVM host until that carve-out lands. Both files are outside this task's layers, so the case is left asserting NET-134's "whatever its egress rules" rather than the tree, and its failure message now names both legs so the red is attributable. Notes from the agent: THE FAILED CHECKS, diagnosed: both ci-linux-kvm-success and test-kvm died in the session e2e at proof_switch_steers_proxy_mac_frames_to_the_host_stack (the run's annotation: a SYN to 100.64.255.252 produced no RST while ARP had resolved the MAC) — this branch's own gate semantics: NET-134 drops every frame to the proxy's address from a box that declares no upstream, and the three pre-existing NET-132 cases probe that address with lane-less boxes. Fixed in scope: the steer and proxy-source cases' activations now pass --credentialed-upstream (the lane is their precondition, not their subject; the flag is parsed and activates — validated locally end to end against the native daemon with the exact command lines, including the lane case's --deny-subnets 0.0.0.0/0 --credentialed-upstream spelling), and the ARP case documents that its…[truncated] |
NET-134 binds both legs a box's frame must clear to reach the Box Egress Proxy's listener: the host-side gate decides the frame outside the VM, and the in-VM relay -- which enforces the box's declared egress (NET-062) before the frame ever reaches the shared switch -- must carry the same verdict, or a deny-all box that declared the lane dies inside the guest and the gate's lane arm is never reached. The pure verdict is the single implementation the relay applies, so the lane's infrastructure admission joins the resolver carve-out there: granted by the box's own declaration rather than by the system, and address-wide, exactly as the host-side gate admits it. EgressRules::new and from_policy keep building no lane, so the Kani oracle over rule sets holds unchanged.
The Kani harness's iff is stated over every rule set the compile can produce; a lane is one more thing it can produce now, so the harness symbolizes the declaration (an address a boolean chooses to carry) and adds the same term to its oracle -- the proof stays exhaustive over deny-all boxes that declared the upstream, where the admission the verdict gained is the whole point of NET-134.
The session policy's credentialed-upstream declaration (NET-134) is carried verbatim to the guest; this hands it to the one compile the relay's egress leg decides by, with the Box Egress Proxy's address drawn from the box's own switch subnet -- the first leg a box's frame must clear on its way to the proxy's acceptor, and the one the e2e's deny-all lane case was dying at in the KVM and macOS lanes: the connection ended inside the guest before the host-side gate's lane arm was ever reached. The relay-level proof pins the leg: a deny-all box with the lane has its frame to the proxy's address forwarded verbatim while the same rules drop the host alias and everything else, and the same rules without the declaration drop the proxy's address too.
|
Round 2 of 6 on a failed check suite, pushed as
Verify Outside the task's
Converted to a draft: something above needs a person before review. Both legs of the credentialed lane now admit the proxy's address: a deny-all box that declared the lane has its frame to the proxy's address forwarded verbatim by the in-VM relay and admitted by the host-side gate's ProxyLane arm, so the connect that CI's KVM and macOS lanes failed no longer has a drop anywhere on its path to the acceptor — and the box reaches no other host port, its frames to the host alias dying at the relay. A box without a lane still has every frame to the proxy's address dropped at the gate — one rate-limited warn line naming the box — and the baseline set never admits the address at all. Notes from the agent: Directive: the two failed checks were ci-linux-kvm-success and test-kvm, both dying at the deny-all lane case — 'the deny-all lane box's connection to the proxy's address did not complete'. Root cause: the frame died inside the guest. The in-VM relay (minimald, crates/minimald/src/net/switch.rs) applied the box's 0.0.0.0/0 deny before any lane admission, so the frame never reached the host-side gate whose ProxyLane arm round 1 had added. NET-134's covered text binds both legs — 'the host-side gate enforces it outside the boundary and the in-VM relay carries the same verdict' — but this task's layers name only the host-side half, so the fix had to go outside them. Out-of-layer files (both required for the proof to hold; no change inside the named layers was needed this round): crates/sessions/src/core/egress.rs — EgressRules gains with_credentialed_upstream([u8;4]) and verdict_ipv4 admits the declared lane's proxy address after the resolver carve-out and before the deny arms, plus a unit test under deny-all and an extended Kani oracle. crates/minimald/src/net/switch.rs — compiled_egress compiles the policy's declared lane into the box's rules (subnet.box_egress_proxy_address()), and a relay-level test proves the deny-all lane box's frame to 100.64.255.252:8118 forwards verbatim while the host alias is dropped not reset, and t…[truncated] |
The no-lane case asserted the daemon's warn names the box's own switch address, grepping the daemon's file log for a text-shape `source=<addr>` field. The detached daemon's file sink is the shared flat-JSON layer (`minvmd.log.<date>` -- the same log the box-register proof greps for `"box":"..."`), which renders the field as `"source":"<addr>"`. The text grep matched nothing the daemon ever wrote, so the case failed deterministically on every lane that runs it -- the KVM lane's test-kvm among them. Read the field in the shape the sink writes. No code path changed: the gate's warn, its rate limit, and the lane's admission are as the earlier commits left them; the four unit lines still pass.
|
Round 3 of 6 on a failed check suite, pushed as
Verify Outside the task's
Converted to a draft: something above needs a person before review. A deny-all box with a credentialed lane reaches the proxy's acceptor and no other host port, and a box without one has every frame to the proxy's address dropped at the gate with one rate-limited warn naming the box — the e2e case that asserts the drop now reads the daemon's log in the JSON shape the sink actually writes, so the KVM and macOS lanes' run of it can pass. Notes from the agent: Round 3 addressed the failed ci-linux-kvm-success / test-kvm check. That run's log is unreachable from this box (no gh credential) and the lane itself is unrunnable here (no /dev/kvm), so the defect was found by reading the case against the code: the no-lane case grepped the detached daemon's file log for a text-shape 'source=' field, but that sink is the shared flat-JSON layer (minvmd.log., the same log the CI-proven box-register proof greps as '"box":"e2e-box-reg"'), which renders the field as '"source":""' — the text grep matched nothing the daemon ever wrote, a deterministic failure on every lane that runs the case, test-kvm included. Fixed in 4e4e34a; no code path changed. Evidence the fix is right: mlog's json_file_layer contract test pins the file-log shape; warn_proxy_lane emits source = %Ipv4Addr::from(src), serialized as "source":"100.64.255.9"; a synthetic-line sanity check matched the fixed grep for the right address and rejected a wrong one; bash -n passes. Re-audited by reading, no changes needed: the stand-in's pool share wires from every registered row (RegisteredBoxes over the live table, only the node namespace excluded) so the lane case's delivery holds; ARP frames admit under deny-all before the rules; the rule string has exactly one daemon-side emit site, so the case's warn_lines==1 count holds; the 60s rate-limit window covers the case's 15s probe. The two e2e lines are the lane's evidence, not this box's: they skip without MINVMD_GVPROXY_BIN (just e2e-native deliberately sets none, which is why the native lane is green) and n…[truncated] |
|
/revise Round 4. All lanes are green at 4e4e34a, and the KVM e2e lane case passes. The design check found that the lane is too wide. The two out-of-layer changes (
Out of scope: the box id (T81 #1879), the UDP/443 reject (the BEP re-plan), and any egress allow-list semantics. |
|
Plan amended (T45 #1665): the two out-of-layer files are now layers. |
…s address NET-134 admits connections to the Box Egress Proxy's listener, and the design counts that opening among §5.3's port-scoped ones: an address-wide admit would open every other service at the host-gateway address to a credentialed box, past the box-to-host default-deny. So the relay leg's rules now carry the listener as the whole (address, port, protocol) triple — TCP to the proxy's own port — and admit exactly that: a TCP frame to another port at the proxy's address and a UDP datagram to the listener's port both fall through to the box's rules, which drop them under a deny-all the way they drop any other host destination. The minimald relay compiles the triple in beside the policy (the number is the listener 8BD0 's own, spelled where the verdict lives because the switch crate is no dependency of it) and its relay-level pin now proves the narrowing on the frames a deny-all box sends: the listener forwards, the same box's TCP to 443 and UDP to the listener's port do not. The Kani oracle restates the triple over the same symbolic frame, so a verdict that admits the address at any port or protocol fails the proof. The lane arm still runs after the lease check (NET-084), and the pin now says so: a frame to the listener wearing a foreign source is the lease check's drop, never the lane's admit.
The host-side gate's lane arm admitted every frame to the Box Egress Proxy's address, which opened every other service at that address to a credentialed box — an address-wide admit where NET-134 admits the listener, one of the design's port-scoped openings. The arm now reads the whole (address, port, protocol) triple: TCP to bep_host::PROXY_PORT, the port the acceptor listens at, read from the stack that runs it so the gate cannot drift from the listener it guards. Every other frame to the address is refused under the proxy rule, laned or not — another port, another protocol — and the deny-all row's rules stay out of it, exactly as before. The arm's position is the anti-spoof contract, now written down: it sits after source attribution, so a frame to the listener wearing a source no lane-declaring row holds never rides one. Two pins hold it — the laned box's frame to 443 and its UDP datagram to the listener's port never reach the switch, and a frame to the listener from a sibling row, an interim-admitted address and an out-of-plan address is dropped, the last by the unknown-source refusal before the lane arm can be reached at all. The drop line names the box: the plan's diagnostics ask for each refused frame logged with the box and the reason, and the warning carried the source alone. A row's name rides the line now — the row the frame's source resolved to — and the refusal's message covers both reasons a frame takes the rule: no lane, or not the listener a lane opens.
The lane's e2e case now reaches past the host alias and holds the lane's own narrowing end to end: TCP to 443 at the proxy's address — a port the listener does not bind, so the declaration never admitted it — runs out its window with no reset, on both legs, because the relay's arm and the gate's both read the listener and not the address. No UDP leg, on purpose: a datagram to the proxy's address is dropped silently today, but BEP-018's re-plan has the proxy answer one with an ICMP port-unreachable reject, and the case must not pin today's silence against that flip. The unit tests hold the datagram's posture meanwhile. The refusal's case now reads the drop line for the box beside the source — the row's own name, in the same flat-JSON shape the source field is read in — so the daemon log tail carries the refused frame with the box and the reason, the diagnostics line the plan asks for.
…proxy-only-as-a-credentialed-lane-s-in # Conflicts: # scripts/session-e2e.sh
|
Round 4 of 6 on a /revise note, pushed as Resolved conflicts in:
Verify Outside the task's
Converted to a draft: something above needs a person before review. A deny-all box with a credentialed lane now reaches the proxy's acceptor on TCP to the listener's own port and nowhere else at that address — the gate and the relay each admit the whole (address, port, protocol) triple — while every other frame to the proxy's address, from a lane-less box, a laned box at another port or protocol, or a source no lane-declaring row holds, is dropped at the gate under the proxy rule with the box's own name on the drop line. Notes from the agent: All three directive items are done, on top of the two out-of-layer changes the directive accepted (kept): (1) the lane is the listener — EgressRules carries ProxyListener (addr, port, proto), the relay arm and the gate arm admit exactly TCP to the listener's port (the gate reads switch::bep_host::PROXY_PORT, the relay carries its own pinned spelling because it cannot depend on switch; the e2e connect is the drift check), and every other frame to the proxy's address, laned or not, drops under PROXY_LANE_RULE; 443 is the probe port because PROXY_PORT is 8118. (2) the anti-spoof ordering is written down and pinned on both legs: the arm sits after source attribution at the gate and after the lease check in the verdict, proven by the new spoofed_source_to_proxy_listener_dropped (sibling row, interim-admitted and out-of-plan sources all dropped; the out-of-plan one by the unknown-source refusal, before the lane arm) and by the foreign-source assertion in the sessions test. (3) warn_proxy_lane logs box = row.name() when a row holds the source, and uncredentialed_box_dropped_at_proxy_address asserts box="bare". The two e2e script lines ran as written but this box has no KVM (no MINVMD_GVPROXY_BIN), so after building the min+minimald pair they printed SKIPPED and exited 0: the new e2e assertions (TCP 443 silence at $proxy_ip, and "box":"e2e-cred-nolane" on the daemon's flat-JSON drop line) are exercised only by the KVM CI lane, which was green at the round's start. No UDP probe was added, p…[truncated] |
The KVM lane's whole-lane e2e died in the proxy-MAC steering case: the case's probe connected to the proxy's address on an unlistened port and read the peer's RST, but NET-134's narrowing — this branch's own subject — drops every frame to that address except the listener's, on both legs, so the RST the case waited for is one the gate refuses to let happen by design. The lane's own case pins exactly that silence at the same port. The case now reads the one destination the lane still admits, the way the proxy-source and credentialed-lane cases already do: it stops both daemons, boots its own under MINVMD_BEP_STUB (the listener's real acceptor is a later task, and without the stand-in the pool resets the connection it just accepted — the acceptor-down answer, a race no case can read), asserts the stand-in's socket, probes the listener, and reads two facts — the stand-in's answer line naming the box's own switch address as the source and the proxy's address as the destination, and the ARP entry the resolution left naming the peer's MAC, not the gateway's. The flag is dropped on every exit, and the two comments elsewhere in the file that described the case's old RST shape are brought in step.
…s name The task's core and daemon test lines select proxy_lane_admits_only_the_listener and compiled_egress_carries_the_credentialed_lane; the tests that pin those layers were spelled, when they landed, as the deny-all shapes they read (credentialed_lane_admits_the_proxy_address_under_deny_all and credentialed_lane_reaches_the_proxy_address_under_deny_all), so both lines matched no test and could only ever report 'no tests to run'. The bodies are the witnesses the lines are after — the listener triple, the other ports and protocols left to the rules — so this renames the two to the names the lines select. No assertion changes.
|
Round 5 of 6 on a failed check suite, pushed as
Verify Outside the task's
Converted to a draft: something above needs a person before review. The KVM lane's whole-lane session e2e now runs past the proxy-MAC steering case that killed it and reaches the two NET-134 cases at its end: a deny-all box that declares a credentialed upstream completes a delivered connection to the proxy's listener — answered from its own switch address — and meets silence at every other port at the proxy's address and at the host alias, while a box with no lane meets silence at the listener itself and one rate-limited drop line in the daemon's log naming its address, its box and the rule; every leg of that is pinned by the eight unit-test lines above, and the lane is the evidence for the end-to-end half. Notes from the agent: Round 5 fixed the failed ci-linux-kvm-success/test-kvm run: test-kvm's session-e2e step died in proof_switch_steers_proxy_mac_frames_to_the_host_stack, whose probe read a peer RST at an unlistened port (100.64.255.252:443) — an answer round 4's task-mandated listener-only narrowing refuses on both legs by design, so the case could never pass again. It now stops both daemons, boots its own under MINVMD_BEP_STUB (the listener's real acceptor is a later task; without the stand-in the pool resets the connection it just accepted, a race), asserts the stand-in's socket, probes the listener, and reads the delivery's answer line (source = the box's own switch address, destination = the proxy's address:8118) plus the ARP entry naming the peer's MAC — the same discipline the proxy-source and credentialed-lane cases use and CI has already proven. The ARP case that reuses this case's stub-flagged daemon is unaffected (its no-lane :443 probe is dropped at the gate before the pool; only the ARP entry is asserted) and the github-only case's fetches never touch the listener, which is why it passes on lanes where every listener connect is reset. The e2e edit is inside the e2e layer's named file but in a case the layer note did not name: the whole-lane run the failing check executes dies there, so this is the dir…[truncated] |
|
/revise Round 6, the last. ARCH design check at 0e834d4 returned conditions. Round 4's narrowing is right on the gate. Two asks, nothing else. Merge main first.
Out of scope: the |
…proxy-only-as-a-credentialed-lane-s-in
The --credentialed-upstream help said the frame is dropped "at the host-side gate", which only exists on a VM-backed lane: on a native host the relay leg makes the refusal. The text now names both — the VM host's gate on a VM-backed host, the relay's own egress-uncredentialed-proxy-destination drop on a native one — so a user debugging the refusal looks for the log line their topology actually writes. Review finding on PR #1885 (cli.rs:531).
|
Round 3 of 6 on the runtime's review findings, pushed as
Verify Outside the task's
Converted to a draft: something above needs a person before review. Review threads the coder judged:
At this head a deny-all box with a credentialed lane reaches the proxy's acceptor and no other port at the proxy's address or anywhere else on the host, and a box without the lane has every frame to the proxy's address refused and never reaches the acceptor — all ten of the task's test lines green — and the --credentialed-upstream help now tells a user debugging that refusal which leg makes it on their topology: the VM host's egress gate on a VM-backed host, the relay's own egress-uncredentialed-proxy-destination drop on a native one. Notes from the agent: Round 3 judged the single review thread and found it valid: the --credentialed-upstream help text said a lane-less box's frame is dropped 'at the host-side gate', but that gate exists only on a VM-backed lane — on a native host the relay leg (sessions::core::egress's UncredentialedProxyDestination) makes the refusal, as the PR's own code comments state. crates/minimal/src/cli.rs is not named in task.md's layers but the flag's help text was authored by this task's round 1 and is already part of the open PR's diff, so the rewording stays inside the PR's own file set. The change is comment-only; cargo clippy -p minimal --all-targets and cargo fmt --check -p minimal both pass at the head. just ci and clippy-strict were not run this round (directive-bounded comment-only delta; the eight nextest lines and both e2e lines were run instead). |
|
Un-drafting. Round 3 added only a doc-comment fix to |
There was a problem hiding this comment.
Own review + ARCH (no objection) at a81bada; all lanes green; minimal-review concluded; 0 open threads (lane-display LOW filed as gominimal/inbox#853).
… reach GitHub (#2497) * feat(minimal): collect the proxy log and audit segmen E99C ts in min bug The support bundle now carries providers/<name>/bep.log, beside the VM host daemon's run.log, and the newest segments of the Box Egress Proxy's audit log under bep/audit/. * feat(minvmd): start bep serve beside gvproxy with the per-boot token A production macOS boot now starts the Box Egress Proxy on the socket the switch peer dials. The token goes to its standard input, its output to bep.log beside the socket, and the pool binds the boxes' shares once the proxy is started. The stand-in stays behind MINVMD_BEP_STUB. * build(just): build and sign bep beside minvmd on macOS minvmd starts bep serve from its own directory, so minvmd-build now builds bep there and signs it before minvmd's own codesign. * test(e2e): run the BEP grant legs against the live proxy Adds bep_git_and_gh_against_private_repo. The T14 and T16 grant legs now share one gate: off macOS, without gvproxy or without a sign-in they record NOT RUN. On macOS the proxy that minvmd starts must answer on its control socket, or the case fails with the proxy's log. * fix(minvmd): bind proxy shares only while bep listens, for lane rows The pool now reads a listening flag the watcher keeps: set while bep runs and its listener socket is new, cleared when it exits. Shares go only to rows that declare a credentialed upstream, the same rows the gate's proxy_lane_admits lets reach the proxy address. The comments name that gate and say the installer does not ship bep yet. * test(e2e): record the missing git credential helper in the BEP case git sends no Authorization header without a credential helper, and no box ships one yet. The comment names the gap so the clone step's failure on a live lane is not read as a proxy fault. * test(minvmd): pin which rows CredentialedBoxes gives a share A row with no credentialed upstream gets no share while bep listens, a declaring row gets its share, and no row gets one while bep does not listen. The BEP_BIN_ENV and BepProxy docs now name #2498 and #2285 (T38). * docs(e2e): name the issues that own the BEP-015 residuals The private-repo case names #2499 (git credential helper) and #2500 (macOS lane with bep, a sign-in and E2E_BEP_PRIVATE_REPO). switch's bep_host comments name the T45 gate, egress::proxy_lane_admits (#1885). * docs(switch): say which box source a boot wires for the proxy A boot with no listening acceptor wires NoBoxes. A boot whose bep serve listens wires CredentialedBoxes, which shares only the rows that declare a credentialed upstream. * fix(minvmd): stop the proxy when its watcher cannot own it When the watcher thread cannot start, kill and reap bep serve before the error returns. When try_wait fails, the watcher stops the child before it ends, because the handle's drop reaches the child only through the watcher. * fix(minvmd): stop bep serve when the supervisor dies without unwinding Arm PR_SET_PDEATHSIG(SIGTERM) in the proxy child on Linux, so a SIGKILL'd or crashed supervisor leaves no proxy holding the old boot token and its socket. macOS has no parent-death signal; #2501 owns it. * fix(minvmd): move a full proxy log aside when the proxy starts bep.log is appended across boots and nothing capped it, so a long-lived host could fill its state dir. A start now moves a log past 8 MiB to bep.log.old. The support bundle legend says so. * test(minimal): inject the audit dir so the bundle test stays hermetic --------- Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com> Co-authored-by: Norrie Taylor <91171431+norrietaylor@users.noreply.github.com>
…ominimal#2504) * feat(minimal): collect the proxy log and audit segments in min bug The support bundle now carries providers/<name>/bep.log, beside the VM host daemon's run.log, and the newest segments of the Box Egress Proxy's audit log under bep/audit/. * feat(minvmd): start bep serve beside gvproxy with the per-boot token A production macOS boot now starts the Box Egress Proxy on the socket the switch peer dials. The token goes to its standard input, its output to bep.log beside the socket, and the pool binds the boxes' shares once the proxy is started. The stand-in stays behind MINVMD_BEP_STUB. * build(just): build and sign bep beside minvmd on macOS minvmd starts bep serve from its own directory, so minvmd-build now builds bep there and signs it before minvmd's own codesign. * test(e2e): run the BEP grant legs against the live proxy Adds bep_git_and_gh_against_private_repo. The T14 and T16 grant legs now share one gate: off macOS, without gvproxy or without a sign-in they record NOT RUN. On macOS the proxy that minvmd starts must answer on its control socket, or the case fails with the proxy's log. * fix(minvmd): bind proxy shares only while bep listens, for lane rows The pool now reads a listening flag the watcher keeps: set while bep runs and its listener socket is new, cleared when it exits. Shares go only to rows that declare a credentialed upstream, the same rows the gate's proxy_lane_admits lets reach the proxy address. The comments name that gate and say the installer does not ship bep yet. * test(e2e): record the missing git credential helper in the BEP case git sends no Authorization header without a credential helper, and no box ships one yet. The comment names the gap so the clone step's failure on a live lane is not read as a proxy fault. * test(minvmd): pin which rows CredentialedBoxes gives a share A row with no credentialed upstream gets no share while bep listens, a declaring row gets its share, and no row gets one while bep does not listen. The BEP_BIN_ENV and BepProxy docs now name gominimal#2498 and gominimal#2285 (T38). * docs(e2e): name the issues that own the BEP-015 residuals The private-repo case names gominimal#2499 (git credential helper) and gominimal#2500 (macOS lane with bep, a sign-in and E2E_BEP_PRIVATE_REPO). switch's bep_host comments name the T45 gate, egress::proxy_lane_admits (gominimal#1885). * docs(switch): say which box source a boot wires for the proxy A boot with no listening acceptor wires NoBoxes. A boot whose bep serve listens wires CredentialedBoxes, which shares only the rows that declare a credentialed upstream. * fix(minvmd): stop the proxy when its watcher cannot own it When the watcher thread cannot start, kill and reap bep serve before the error returns. When try_wait fails, the watcher stops the child before it ends, because the handle's drop reaches the child only through the watcher. * fix(minvmd): stop bep serve when the supervisor dies without unwinding Arm PR_SET_PDEATHSIG(SIGTERM) in the proxy child on Linux, so a SIGKILL'd or crashed supervisor leaves no proxy holding the old boot token and its socket. macOS has no parent-death signal; gominimal#2501 owns it. * fix(minvmd): move a full proxy log aside when the proxy starts bep.log is appended across boots and nothing capped it, so a long-lived host could fill its state dir. A start now moves a log past 8 MiB to bep.log.old. The support bundle legend says so. * test(minimal): inject the audit dir so the bundle test stays hermetic * fix(minvmd): stop or reap a bep serve left behind by a minvmd crash When minvmd died without unwinding (SIGKILL, a crash), nothing stopped the `bep serve` it started on macOS: the proxy ran on with the dead boot's token and held its listener socket, and `just reap` did not match it. minvmd now starts the proxy with `--exit-on-stdin-close` and keeps the write end of the token pipe open after the token, inside the child the watcher owns. `bep serve` with that flag reads its standard input to the end on a thread of its own (`bep::supervisor`) and stops serving when it closes. Linux keeps PR_SET_PDEATHSIG as it was; the pipe backs it up there. Why a pipe at end of file over polling getppid() for reparenting: - The kernel closes a dead process's descriptors whatever killed it, so end of file arrives at once, with no polling interval to tune and no window in which the proxy serves a dead boot. - It tracks the supervisor process itself. getppid() answers whether the parent pid changed, which a reparent to a subreaper or launchd shows only after the fact, and never if the proxy was started through an intermediate process. - It needs no platform code, so the same mechanism runs on macOS and Linux, and its tests run on Linux: a SIGKILL'd child holding the only write end proves the end of file this design depends on. The flag keeps it opt-in, so a person who pipes the token in by hand still gets a proxy that outlives the pipe. scripts/reap-vms.sh gains a pattern for this checkout's `bep serve`, pinned to the ERE-quoted checkout root and to the VM's `gvproxy-bep.sock` (in the per-name directory under `--vm`), sent with `pkill -9`, user-side and through `sudo -n` like the others. AGENTS.md's leaked-VMM footgun names the proxy too. Closes gominimal#2501 Claude-Session: https://claude.ai/code/session_01WhCC5TEBNzPeWgytRsU4rc * build(deps): crypto-bigint 0.7.7 and ctutils 0.4.5, for RUSTSEC-2026-0387 and -0385 Port of gominimal#2517 and gominimal#2503 so cargo-deny's advisories check passes on this branch. The same Cargo.lock changes; they become no-ops once both are on main. Claude-Session: https://claude.ai/code/session_01WhCC5TEBNzPeWgytRsU4rc * fix(scripts): reap only a VM's own bep serve in the bare reap The bare proxy pattern matched this checkout's `bep serve` on any listener named gvproxy-bep.sock, so `just reap` also SIGKILLed one started by hand on another path. It now requires the listener under a VM's `local-minvmd<N>/` directory, directly or one name below, as the comment above it already says. reap_scoped_per_checkout gains the case of a listener outside every VM's directory. Claude-Session: https://claude.ai/code/session_01WhCC5TEBNzPeWgytRsU4rc --------- Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
T45 — Reach the proxy only as a credentialed lane's infrastructure
slice: S10
kind: task
covers: NET-134
size: L
est: 300 code, 320 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: the bundle's daemon log tail carries each frame to the proxy's address refused at the gate with the box and the reason
observability: one rate-limited warn log line per refused frame to the proxy's address from a box without a credentialed lane
layers:
- schema:
- crates/sessions/src/lib.rs: the session policy gains a declaration of a credentialed upstream, the minimum the proxy document extends
- crates/minimald-rpc/src/lib.rs: the session policy literals follow
test: cargo nextest run -p sessions credentialed_upstream_declaration_parses
- core:
- crates/sessions/src/core/egress.rs: the relay's frame verdict admits TCP to the proxy's listener port at the proxy's address for a box whose rules carry the credentialed lane, after the source check and before the deny rules, and nothing else at that address; the Kani oracle models the address, port and protocol together (out of layer in the first round, accepted: the in-VM relay carries the same verdict as the host-side gate, NET-062)
test: cargo nextest run -p sessions proxy_lane_admits_only_the_listener
- daemon:
- crates/minimald/src/net/switch.rs: compiled_egress attaches the credentialed lane, the proxy's listener address and port, to a box's rules when its policy declares a credentialed upstream, so a deny-all box's frame to the listener survives the in-VM relay (out of layer in the first round, accepted for the same reason)
test: cargo nextest run -p minimald compiled_egress_carries_the_credentialed_lane
- vmhost:
- crates/minvmd/src/box_registry.rs: a row carries whether its box declares a credentialed upstream, filled from the client's registration
- crates/minvmd/src/net/egress_gate.rs: gate_verdict admits a TCP frame to the proxy's listener port at the proxy's address as infrastructure, whatever the row's egress rules, when the row declares a credentialed upstream, and drops it under the box-to-host default-deny otherwise; the baseline set never admits the proxy's address; every other frame to that address is dropped, and a frame whose source is not the box's own address is dropped before the lane is considered; each drop is logged with the box and the reason
- crates/minvmd/src/bep_attach.rs: an attachment carries the lane
test: cargo nextest run -p minvmd credentialed_box_reaches_proxy_address_under_deny_all
test: cargo nextest run -p minvmd uncredentialed_box_dropped_at_proxy_address
test: cargo nextest run -p minvmd baseline_set_never_admits_the_proxy_address
test: cargo nextest run -p minvmd credentialed_box_dropped_at_proxy_address_other_port
test: cargo nextest run -p minvmd spoofed_source_to_proxy_listener_dropped
- e2e:
- scripts/session-e2e.sh: adds the credentialed and uncredentialed cases; the credentialed case also probes a non-listener TCP port at the proxy's address and expects silence with no reset
test: ./scripts/session-e2e.sh deny_all_box_reaches_proxy_and_no_other_host_port
test: ./scripts/session-e2e.sh box_without_credentialed_lane_cannot_reach_proxy
produces: none
consumes:
- crates/minvmd/src/bep_attach.rs
- crates/minvmd/src/net/egress_gate.rs
proof: A deny-all box with a credentialed lane reaches the proxy's acceptor and no other port at the proxy's address or elsewhere on the host, and a box without one has every frame to the proxy's address dropped at the gate and cannot reach the acceptor at all.
depends on:
- blocked by #1627
- blocked by #1660
Proof
Both halves hold at the head, enforced at two points: the box's relay on every host (native and VM-backed) drops a laneless box's frame to the proxy's address and admits only the listener triple for a laned one, and on a VM-backed host minvmd's host-side gate applies the same verdict outside the boundary. Each half is pinned at both points and carried end to end. The credentialed case's refused probes must run out a drop window (5 s floor) and must not fail with an active reject. A deny-all box that declared a credentialed upstream (
min session activate --credentialed-upstream, a serde table on the session policy that travels the registration wire into the host-side row) has TCP to the proxy's listener (100.64.255.252:8118) admitted as that lane's infrastructure; every other frame to the proxy's address — another port, another protocol, laned or not — is dropped at the gate underegress-uncredentialed-proxy-destination, and the same rules hold every other host port and destination. A box without the declaration has every frame to the proxy's address dropped at the host-side gate — a drop, not a reset — with one rate-limited warn line per refused frame naming the box by its row name (box=), the proxy's address and the reason. Five minvmd tests pin the decision (lane reach under deny-all, the no-lane drop beside allow-all rules, the baseline set admitting the address for no category,credentialed_box_dropped_at_proxy_address_other_port,spoofed_source_to_proxy_listener_dropped); the relay leg admits only the listener triple (proxy_lane_admits_only_the_listener). test-kvm ran both session-e2e cases green at 0e834d4.Notes from the agent: Out-of-scope files, all forced, in two groups. (1) The client side the layers omit: crates/minimal/src/cli.rs adds the
--credentialed-upstreamflag and crates/minimal/src/cmd/session.rs wires it into the session policy and the wire registration — the credentialed e2e case has no other path to declare a lane, and without it the gate's laned arm is unreachable from any real activation. crates/minimald/src/session.rs mirrors every policy field into the effective policy the wire carries, so the declaration had to travel it. crates/minvmd/src/control.rs builds the registration's ClientBoxSpec from the wire request and had to carry the field. (2) Mechanical one-line struct-literal follows of the two new fields (SessionPolicy, RegisterBoxRequest, ClientBoxSpec), no behavior: crates/minimald/src/net/{dns_gate,gvproxy_networ…[truncated]Tests
cargo nextest run -p sessions credentialed_upstream_declaration_parsescargo nextest run -p sessions proxy_lane_admits_only_the_listenercargo nextest run -p minimald compiled_egress_carries_the_credentialed_lanecargo nextest run -p minvmd credentialed_box_reaches_proxy_address_under_deny_allcargo nextest run -p minvmd uncredentialed_box_dropped_at_proxy_addresscargo nextest run -p minvmd baseline_set_never_admits_the_proxy_addresscargo nextest run -p minvmd credentialed_box_dropped_at_proxy_address_other_portcargo nextest run -p minvmd spoofed_source_to_proxy_listener_dropped./scripts/session-e2e.sh deny_all_box_reaches_proxy_and_no_other_host_port./scripts/session-e2e.sh box_without_credentialed_lane_cannot_reach_proxyThe two session-e2e lines skip in the coder's sandbox (no switch); test-kvm ran both green at 0e834d4.
Deviations
--credentialed-upstreamclient flag (crates/minimal/src/cli.rs,cmd/session.rs) and its one-line struct-literal follows: the layers omit the client side, and the e2e has no other way to declare a lane.proof_switch_steers_proxy_mac_frames_to_the_host_stackwas rewritten to probe the listener underMINVMD_BEP_STUBand require a delivered answer naming the box's address andproxy:8118; the narrowing removed the :443 reset it used to read.sessions::core::egress::PROXY_LISTENER_PORTandswitch::bep_host::PROXY_PORT); only the e2e checks that they agree.Verify
min task run agent-verifypassed ([tasks.verify]).Base
Merged main before round 5; head 0e834d4.
Out of scope
These files are outside the task's
layers::crates/minimal/src/cli.rscrates/minimal/src/cmd/list.rscrates/minimal/src/cmd/session.rscrates/minimal/tests/cli.rscrates/minimald/src/net/dns_gate.rscrates/minimald/src/net/gvproxy_network.rscrates/minimald/src/net/proxy.rscrates/minimald/src/session.rscrates/minimald/src/session/tests.rscrates/minimald/tests/netns_root_integration.rscrates/minvmd/src/cmd/run.rscrates/minvmd/src/control.rscrates/minvmd/src/net/bep_stub.rsCloses #1665