8000 T9 — Publish an own-address box on host loopback from a fresh install, with the installer checking the switch binary by minimal-foundry[bot] · Pull Request #1723 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

T9 — Publish an own-address box on host loopback from a fresh install, with the installer checking the switch binary - #1723

Merged
norrietaylor merged 12 commits into
mainfrom
agent/1633-publish-an-own-address-box-on-host-loopback-from
Sep 26, 2026
Merged

norrietaylor merged 12 commits into
mainfrom
agent/1633-publish-an-own-address-box-on-host-loopback-from

Conversation

@minimal-foundry
Copy link
Copy Markdown
Contributor

T9 — Publish an own-address box on host loopback from a fresh install, with the installer checking the switch binary

slice: S2
kind: task
covers: NET-040, NET-041, NET-102
size: L
est: 280 code, 380 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: the bundle's daemon log tail shows the forwarder expose call and its result, and the install log names the switch binary it checked
observability: one info log line per exposed mapping with the host address, the port and the session; the installer prints the switch binary it verified
layers:
- installer:
- scripts/install.sh: checks the switch binary is present and executable before the rename cleanup
- scripts/install_test.sh: wraps each unit section behind a case on its first argument, running all when none is given, and adds the switch-binary case
- justfile: test-installer takes an optional case and passes it to the test script
test: just test-installer installer_switch_binary_executable
- switch:
- crates/switch/src/lib.rs: installed_gvproxy_bin also probes the system package path; the default address plan type moves here so an un-enrolled host allocates from it, and each switch derives its own disjoint slice of the reserved local range from it, so two gvproxies on one host never hold the same loopback slice
test: cargo nextest run -p switch unenrolled_self_allocates_default_plan
test: cargo nextest run -p switch loopback_slices_disjoint_per_switch
- daemon:
- crates/minimald/src/net/mod.rs: IpAllocator allocates from the switch crate's default plan
test: cargo nextest run -p minimald allocator_uses_default_plan
- e2e:
- scripts/session-e2e.sh: adds the case fresh_install_own_ip_ingress_publishes_loopback from an installed build
test: ./scripts/session-e2e.sh fresh_install_own_ip_ingress_publishes_loopback
produces: none
consumes: none
proof: From a fresh install, including a system package install, activating with --network own_ip --ingress 8080:8080 answers at 127.0.0.1:8080 with the in-box server's response, and the installer confirms the switch binary is executable.
depends on:
- blocked by #1627

Proof

A fresh install now prints a 'switch-binary verified ' row (naming the binary it checked) and refuses to finish — before the rename cleanup — when the switch binary it shipped is not executable; an un-enrolled host allocates own-IP addresses from the switch crate's default plan, and an own-IP activate with --ingress 8080:8080 publishes the box's internal 8080 on the host loopback so 127.0.0.1:8080 answers with the in-box server's response, one INFO daemon-log record per exposed mapping naming the host, port and session, with the system-package install (/usr/bin/gvproxy-min) covered by the same resolver and exercised by the new session-e2e case fresh_install_own_ip_ingress_publishes_loopback, which skips on hosts with no /dev/net/tun.

Notes from the agent: Out-of-layer file, named plainly: crates/minimald/src/net/gvproxy_network.rs (the daemon layer names only net/mod.rs). The observability requirement — one info line per exposed mapping with host, port and session, and a warn carrying the error when an exposure fails — lives where the exposure happens, which is apply_ingress in gvproxy_network.rs; net/mod.rs's allocator change alone could not show it. Everything else is inside the layers. Two host limitations on this runner box, both reflected honestly in the tests block: (1) /dev/net/tun is absent and mknod is blocked, so the e2e line's box-spawn half cannot be exercised here — the case gates on the device and skips with exit 0, mirroring the repo's own prerequisite wording in crates/minimald/tests/own_ip_tap_root_integration.rs; the installer half of that pipeline was verified here by hand (verified row in the install log, executable ~/.local/bin/gvproxy-min) and by the installer test line's 11 assertions, and the full case runs on the native CI lane, which has the device. (2) 127.0.0.1:8080 is permanently taken by this box's own control plane, so the case publishes on the documented fallback 18082 when 8080 answers — a clean host runs the mappi…[truncated]

Tests

line exit tail
just test-installer installer_switch_binary_executable 0 == shellcheck --shell=sh == == running install_test.sh under sh == # install.sh tests (SH=sh) ok - fresh install exits 0 with the switch binary shipped ok - install names the switch binary it verified ok - the verified switch binary is executable ok - install dies when the shipped switch bin
cargo nextest run -p switch unenrolled_self_allocates_default_plan 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-4066... Compiling switch v0.0.1 (/home/runner/.local/state/minimal/sessions/69c99/tree/crates/switch) Finished 'test' profile [unoptimized] target(s) in 1.29s ──────────── Nextest run ID
cargo nextest run -p switch loopback_slices_disjoint_per_switch 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-4f7d... Finished 'test' profile [unoptimized] target(s) in 0.22s ──────────── Nextest run ID 59515060-73f7-407f-ad0d-ca084b9aa6d5 with nextest profile: default Starting 1 test across 1
cargo nextest run -p minimald allocator_uses_default_plan 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-4c58... Compiling args v0.0.1 (/home/runner/.local/state/minimal/sessions/5e38a/tree/crates/args) Compiling ot v0.0.1 (/home/runner/.local/state/minimal/sessions/5e38a/tree/crates/ot) C
./scripts/session-e2e.sh fresh_install_own_ip_ingress_publishes_loopback 0 fresh-install loopback publish SKIPPED (no /dev/net/tun on this host: an own-IP box cannot open its in-namespace tap; runs for real on a host that has the device) session e2e OK Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-26e3...

Verify

min task run agent-verify passed ([tasks.verify]).

Base

The base branch moved while the coder worked and was merged in before the tests ran. These files changed on BOTH sides since the branch point — read them first, because a rewrite from a stale reading can undo a merge without a conflict:

  • crates/minimald/src/net/gvproxy_network.rs
  • scripts/install.sh
  • scripts/install_test.sh
  • scripts/session-e2e.sh

Out of scope

These files are outside the task's layers:, so this PR is a draft:

  • crates/minimald/src/net/gvproxy_network.rs

Closes #1633

An un-enrolled host has no control plane handing it an address block
(NET-102), so the plan it self-allocates from is defined beside the
subnet primitives that already live here: the CGNAT /16 box leases
come from and the reserved local /24 published addresses come from,
paired switch by switch. Switch i holds loopback slice i, so two
gvproxies on one host never hold the same slice -- the wrapped
'octet % slices' derivation could silently hand two of them the same
one, where the plan instead refuses to serve a switch past its
capacity and leaves the host-global arbitration to NET-010.

installed_gvproxy_bin now also probes the system package path
(/usr/bin/gvproxy-min, where nfpm and the AUR install it) between the
user-local and operator-provisioned paths.
An un-enrolled host self-allocates box addresses from the switch
crate's default plan (NET-102), so the allocator and the default
SwitchClient take their subnet from that plan instead of from a
constant of minimald's own -- one definition, already shared with
minvmd, of the block a fresh daemon boxes out of. The daemon test
also pins the plan's reserved local range to the block the zone's
published names answer at, so the published side and the answerer
side cannot drift.
NET-040's diagnostics want the daemon log tail (and the min bug
bundle carrying it) to show each forwarder expose call and its
result. One info line per exposed mapping now records the host
address it is reachable at, the port, and the session, and a
failed apply logs the error the same way. This lives beside the
attach path that owns the session name; the apply itself is
policy's.
An own-IP session is served by a switch binary (gvproxy-min) the
daemon spawns from exactly the paths the installer writes to
(switch::installed_gvproxy_bin), so the install now checks the binary
this run shipped is present and executable — naming the path it
verified — before the gvproxy rename cleanup can delete anything.

install_test.sh wraps each unit section behind a case on its first
argument (all cases when none is given, each one self-contained: homes
are seeded per case and the pristine manifest baseline is saved once
up front) and adds the installer_switch_binary_executable case
covering the verified row, a shipped binary that lost its +x, a
channel that ships no switch, and a pre-rename channel still shipping
bin/gvproxy. just test-installer takes the optional case and passes it
through.
NET-040/NET-041 end to end, in the shape a user gets it: a stub-bucket
fresh install (its own lane binaries plus the pinned gvproxy), the
installer's switch-binary verification row checked in the install log,
gvproxy-min executable at ~/.local/bin, and an own-IP activate with
--ingress <host>:8080 whose in-box socat responder answers at
127.0.0.1:<host> with the box's own server's response — plus the daemon
log's one record per exposed mapping (host, port, session) and the
system-package half (/usr/bin/gvproxy-min) on lanes that can stage it.

The box needs /dev/net/tun for its tap; a host that is itself a sandbox
has none and cannot mknod one, so the case gates on the device and
skips there — the native CI lane, which already builds a tap in its
root-integration harness, runs it for real. A host already listening on
8080 (the agent-runtime boxes do) publishes on the fallback 18082; the
box always serves its internal 8080, so the mapping exercised is
<host-port>:8080 either way.
…-own-address-box-on-host-loopback-from

# Conflicts:
#	scripts/install_test.sh
#	scripts/session-e2e.sh
@coderabbitai
coderabbitai Bot commented Sep 25, 2026 •
Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: f4a305d6-c0ce-4b05-bda1-0c48664f5ed9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@norrietaylor
Copy link
Copy Markdown
Member

Babysitter review against T9's plan block (covers NET-040, NET-041, NET-102; five test lines, all exit 0 in the coder sandbox, the e2e case self-skipping there for lack of /dev/net/tun).

  • installer: verify_switch_binary runs before the rename cleanup, reads the gvproxy-min row (falling back to a pre-rename gvproxy row), dies when the binary is missing or not executable, records switch-binary verified <path>, and skips honestly on a channel that ships no switch. install_test.sh is wrapped in per-case functions with a dispatch on the first argument, and just test-installer takes the optional case. Matches the three installer layer lines.
  • switch: AddressPlan / DEFAULT_ADDRESS_PLAN with the switch subnet and the reserved local range, installed_gvproxy_bin probing /usr/bin/gvproxy-min with the package install winning over operator paths, and per-switch disjoint loopback slices (SwitchSlice, LoopbackSlice, prefix 27, tests unenrolled_self_allocates_default_plan and loopback_slices_disjoint_per_switch). Matches the layer line.
  • daemon: IpAllocator draws from the plan (allocator_uses_default_plan). Matches.
  • e2e: fresh_install_own_ip_ingress_publishes_loopback from an installed build. It adds a dispatch entry to scripts/session-e2e.sh, as T6 — Retired surfaces gone, end to end #1717 and T29 — Hostnames recover, and two daemons share a machine, end to end #1721 do; whichever lands last gets a base merge from me.
  • Out of layer, in scope: crates/minimald/src/net/gvproxy_network.rs adds the one info line per exposed mapping with host address, port and session. That is the plan's own observability field for this task, so the draft is lifted rather than treated as a scope defect.

One note, not a finding against this PR: the tree's reserved local range is 127.64.0.0/24 (on main already, in minimald::net::dns), while the networking design §7.1 names 127.0.64.0/24 as the working value. T9 keeps the tree consistent with itself; the discrepancy with the design is pre-existing and needs an owner ruling, not a fix here.

No findings. Merge after the lanes, minimal-review and CodeRabbit conclude.

@norrietaylor
norrietaylor marked this pull request as ready for review September 25, 2026 21:09
@norrietaylor
norrietaylor requested a review from a team as a code owner September 25, 2026 21:09
@minimal-foundry
minimal-foundry Bot commented Sep 25, 2026 •
Copy link
Copy Markdown
Contributor Author

Reviewed: 1 finding, none blocking at 87ed6824cc3e.

Address the comments on the diff.

Clobber: none: git log on the four both-sides files shows the T29 merge (#1721) landed and no recent fix is reverted; the e2e dispatch correctly interleaves the new case with T29's hostnames_recover_and_two_daemons_route.

@norrietaylor
Copy link
Copy Markdown
Member

Native lane failure on a343524 (native-daemon-e2e), for round 1: every earlier proof passed; the new fresh install publishes an own-IP ingress on the host loopback case died probing the box:

minimald: failed to spawn process: session spawn: network attach failed: own-IP sandbox produced no in-namespace tap fd
(/dev/net/tun present: true; user namespaces: kernel.apparmor_restrict_unprivileged_userns=1 and this process is unconfined)

The hosted native runner cannot build a rootless tap, which is why the existing own_ip proof self-skips on that lane ("own-IP session proof SKIPPED (no MINVMD_GVPROXY_BIN: this target has no switch)"). The new case gates only on /dev/net/tun, which is present there. Gate it the same way the own_ip proof does (no switch binary or no usable user namespaces means skip natively with the reason printed; it runs for real on the KVM and macOS VM lanes where the boxes have a switch). Nothing else in the case ran far enough to fail.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — approved

Verdict APPROVE — No findings.

@minimal-foundry minimal-foundry Bot added the review:approved minimal-review approved the current head label Sep 25, 2026
On a host that restricts unprivileged user namespaces (stock Ubuntu
24.04, GitHub's ubuntu-latest), an unconfined minimald cannot fork a
session sandbox: its first exec dies writing /proc/self/uid_map with
EPERM. The e2e start-up block attaches the minimald AppArmor profile
to the checkout's own daemon for exactly that reason, but the
fresh-install proof drives a pair installed under a fresh mktemp home,
which no stock tunable covers (`@{HOME}/.local/bin/minimald` matches a
real home, not a nested one), so the installed daemon comes up
unconfined and the proof's first socat probe never gets a sandbox —
that is the native-daemon-e2e failure on this branch.

Attach the profile to the installed daemon — as the installer's own
advisory tells a user to — before driving the pair. `--path` replaces
the recorded set, so the lane's daemon is named alongside it, and the
subshell's EXIT trap puts the previous set back on every path out.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 1 of 10 on a failed check suite, pushed as 6de8014dc6bd.

line exit tail
just test-installer installer_switch_binary_executable 0 == shellcheck --shell=sh == == running install_test.sh under sh == # install.sh tests (SH=sh) ok - fresh install exits 0 with the switch binary shipped ok - install names the switch binary it verified ok - the verified switch binary is executable ok - install dies when the shipped switch bin
cargo nextest run -p switch unenrolled_self_allocates_default_plan 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-ea9f... Compiling switch v0.0.1 (/home/runner/.local/state/minimal/sessions/4472c/tree/crates/switch) Finished 'test' profile [unoptimized] target(s) in 1.11s ──────────── Nextest run ID
cargo nextest run -p switch loopback_slices_disjoint_per_switch 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-3dd6... Finished 'test' profile [unoptimized] target(s) in 0.23s ──────────── Nextest run ID ee007986-2e12-4fcc-af28-cc5256f9445c with nextest profile: default Starting 1 test across 1
cargo nextest run -p minimald allocator_uses_default_plan 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-f529... Compiling args v0.0.1 (/home/runner/.local/state/minimal/sessions/560ed/tree/crates/args) Compiling ot v0.0.1 (/home/runner/.local/state/minimal/sessions/560ed/tree/crates/ot) C
./scripts/session-e2e.sh fresh_install_own_ip_ingress_publishes_loopback 0 fresh-install loopback publish SKIPPED (no /dev/net/tun on this host: an own-IP box cannot open its in-namespace tap; runs for real on a host that has the device) session e2e OK Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-4c70...

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/minimald/src/net/gvproxy_network.rs

Converted to a draft: something above needs a person before review.

The fresh-install own-IP ingress case that proves NET-040 now stages the minimald AppArmor profile for the installed pair on hosts that restrict unprivileged user namespaces, so the native-daemon-e2e lane — which was dying at that case's first in-box exec because the installed daemon ran unconfined — can drive the freshly installed min/minimald through --network own_ip --ingress 8080:8080 to its 127.0.0.1:8080 loopback probe; the installer's switch-binary executable check (NET-041) and the default-plan unit tests (NET-102) all pass.

Notes from the agent: Diagnosis of the two failed checks (both from the same run): ci-linux-native-success and native-daemon-e2e failed only in the native-daemon-e2e job — the run's tests and minimald-root-integration jobs passed, and the public check-run annotation pins the failure at the fresh-install proof's socat probe (crates/sessions exec into the newly installed pair), instantly (whole e2e step 69s). Root cause: GitHub's ubuntu-24.04 runner sets kernel.apparmor_restrict_unprivileged_userns=1, and the e2e start-up block attaches the minimald profile to the checkout's own target/debug/minimald — but the proof then drives a pair installed under a fresh mktemp home, which no stock tunable covers (@{HOME}/.local/bin/minimald matches a real home, not a nested one), so the installed daemon runs unconfined and its first sandbox fork dies writing /proc/self/uid_map with EPERM; socat is a launcher baseline package in every box, so the probe failure was a spawn failure, not a missing file. Fix (scripts/session-e2e.sh, the only file this round): before the installed pair is driven, attach the profile to the installed daemon exactly as install.sh's own Linux advisory tells a user to; because install-apparmor-profile.sh's --path REPLACES the recorded set, the lane's own daemon is named alongside it, and the subshell's single EXIT trap restores the previous set (and removes the /usr/bin/gvproxy-min staged switch) on …[truncated]

@minimal-foundry
minimal-foundry Bot marked this pull request as draft September 25, 2026 21:56
@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 1 finding, 1 inline.

Comment thread crates/switch/src/lib.rs Resolved
@minimal-foundry minimal-foundry Bot removed the review:approved minimal-review approved the current head label Sep 25, 2026
@norrietaylor
Copy link
Copy Markdown
Member

For the round in flight: the tests job on 6de8014 fails common::shell_lint::all_scripts_pass_shellcheck on scripts/session-e2e.sh lines 1019 to 1022. Shellcheck reports SC2317 (unreachable) on every line of fi_cleanup's body because the function is only reached through the subshell's trap fi_cleanup EXIT. The disable=SC2329 directive above the function silences the "never invoked" finding but not SC2317, which is reported per line. Widen the directive to # shellcheck disable=SC2317,SC2329 with the same reason. The native-daemon-e2e lane is green on this head, so the profile attach did its job; nothing else in the run failed.

@norrietaylor
norrietaylor marked this pull request as ready for review September 25, 2026 22:01
Review of #1723: AddressPlan restated the reserved local range and the
/27 slice prefix the tree already had -- the answerer's
dns::RESERVED_LOCAL_RANGE and sessions' LoopbackAllocator carve-out --
so the addresses a switch publishes and the range the zone answers from
could drift apart.

The switch crate is now the single source. minimald's dns module
re-exports RESERVED_LOCAL_RANGE instead of defining it, and
LoopbackAllocator::for_slice_octet reads its slice from the default
address plan rather than recomputing the carve-out, which drops
sessions.rs's own /27 constant. AddressPlan::switch_capacity is const so
the daemon's wrap-around derives from it at compile time. No behaviour
change: same range, prefix and wrap, one definition.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 2 of 10 on the runtime's review findings, pushed as 2729c04cd6c7.

line exit tail
just test-installer installer_switch_binary_executable 0 == shellcheck --shell=sh == == running install_test.sh under sh == # install.sh tests (SH=sh) ok - fresh install exits 0 with the switch binary shipped ok - install names the switch binary it verified ok - the verified switch binary is executable ok - install dies when the shipped switch bin
cargo nextest run -p switch unenrolled_self_allocates_default_plan 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-37a6... Compiling switch v0.0.1 (/home/runner/.local/state/minimal/sessions/4dfc4/tree/crates/switch) Finished 'test' profile [unoptimized] target(s) in 1.20s ──────────── Nextest run ID
cargo nextest run -p switch loopback_slices_disjoint_per_switch 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-68c9... Finished 'test' profile [unoptimized] target(s) in 0.15s ──────────── Nextest run ID e53a82eb-581f-4b57-b188-4dafd0413bcb with nextest profile: default Starting 1 test across 1
cargo nextest run -p minimald allocator_uses_default_plan 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-e18d... Compiling args v0.0.1 (/home/runner/.local/state/minimal/sessions/e6153/tree/crates/args) Compiling ot v0.0.1 (/home/runner/.local/state/minimal/sessions/e6153/tree/crates/ot) C
./scripts/session-e2e.sh fresh_install_own_ip_ingress_publishes_loopback 0 fresh-install loopback publish SKIPPED (no /dev/net/tun on this host: an own-IP box cannot open its in-namespace tap; runs for real on a host that has the device) session e2e OK Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-9307...

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/minimald/src/net/dns.rs
  • crates/minimald/src/net/gvproxy_network.rs
  • crates/minimald/src/sessions.rs

Converted to a draft: something above needs a person before review.

Review threads the coder judged:

  • 👍 valid — crates/switch/src/lib.rs:55: One definition now: minimald's dns module re-exports switch::RESERVED_LOCAL_RANGE instead of restating it, and LoopbackAllocator::for_slice_octet reads its slice from the plan's carve-out, dropping sessions.rs's own /27 prefix constant (AddressPlan::switch_capacity is const so the daemon's wrap-arou

At this branch a fresh install — user-local or a system package install — activating with --network own_ip --ingress 8080:8080 publishes 127.0.0.1:8080 on the host and answers with the in-box server's response, the installer prints the switch binary it verified as executable, and the reserved local range those published addresses and the DNS zone both use is now one definition (the switch crate's) rather than two copies that could drift.

Notes from the agent: Revise round 2: the directive listed one review finding, which I judged valid and fixed. Everything else the task's layers name (install.sh's switch-binary check, install_test.sh's case harness, the justfile case argument, the switch crate's default address plan, minimald's IpAllocator, the session-e2e case) was delivered in earlier rounds on this branch and is untouched here.

Two files outside the task's layers changed: crates/minimald/src/net/dns.rs and crates/minimald/src/sessions.rs. The finding is that the branch restated the reserved local range and the /27 slice prefix the tree already had, and it cannot be fixed from the switch crate alone: the dependency runs minimald -> switch, so the only way to a single source is for minimald to re-export the switch constant (dns.rs, one item) and to read its per-daemon slice from the plan's carve-out instead of recomputing it (sessions.rs, one function body and one const). Both diffs are behaviour-preserving; the slice tests that pin them (loopback_allocators_follow_the_daemon_s_slice_octet, two_daemons_draw_disjoint_loopback_slices) pass unchanged.

Could not run for real: the e2e line's own proof needs /dev/net/tun, which this box does not have and cannot mknod (no capabilities), so the line exits 0 with the proof skipped; the Linux-native CI lane runs it for real. dash is also absent, so the installer harness ran under sh only.

@minimal-foundry
minimal-foundry Bot marked this pull request as draft September 25, 2026 22:31
@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 1 finding, 1 inline.

The per-mapping info line re-derived its host and port from the
requested ingress mappings and hardcoded the host, while apply_ingress
had already returned what the switch accepted. Read the line from that
result instead, through ExposedMapping's own local bind, so the record
stays true to what the forwarder holds if the address expose_request
binds ever moves off the loopback.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 3 of 10 on the runtime's review findings, pushed as 6589f2209c1b.

line exit tail
just test-installer installer_switch_binary_executable 0 == shellcheck --shell=sh == == running install_test.sh under sh == # install.sh tests (SH=sh) ok - fresh install exits 0 with the switch binary shipped ok - install names the switch binary it verified ok - the verified switch binary is executable ok - install dies when the shipped switch bin
cargo nextest run -p switch unenrolled_self_allocates_default_plan 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-f4ef... Compiling switch v0.0.1 (/home/runner/.local/state/minimal/sessions/18340/tree/crates/switch) Finished 'test' profile [unoptimized] target(s) in 1.78s ──────────── Nextest run ID
cargo nextest run -p switch loopback_slices_disjoint_per_switch 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-d6bb... Finished 'test' profile [unoptimized] target(s) in 0.27s ──────────── Nextest run ID d581a94e-4797-4eb1-9a68-b6e27d6acee8 with nextest profile: default Starting 1 test across 1
cargo nextest run -p minimald allocator_uses_default_plan 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-bec7... Compiling args v0.0.1 (/home/runner/.local/state/minimal/sessions/0045c/tree/crates/args) Compiling ot v0.0.1 (/home/runner/.local/state/minimal/sessions/0045c/tree/crates/ot) C
./scripts/session-e2e.sh fresh_install_own_ip_ingress_publishes_loopback 0 fresh-install loopback publish SKIPPED (no /dev/net/tun on this host: an own-IP box cannot open its in-namespace tap; runs for real on a host that has the device) session e2e OK Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-961f...

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/minimald/src/net/dns.rs
  • crates/minimald/src/net/gvproxy_network.rs
  • crates/minimald/src/net/policy.rs
  • crates/minimald/src/sessions.rs

Converted to a draft: something above needs a person before review.

Review threads the coder judged:

  • 👍 valid — crates/minimald/src/net/gvproxy_network.rs: The per-mapping info line now reads the accepted forward's own local bind through ExposedMapping::host_port — not the requested mapping with a hardcoded host — so the record cannot claim a host a mapping is not published on.

From a fresh install — including one whose only switch binary is the system package path — activating with --network own_ip --ingress 8080:8080 answers at 127.0.0.1:8080 with the in-box server's response, the installer confirms the switch binary is executable, and the daemon log now records each exposed mapping from the forward the switch actually accepted rather than from the request.

Notes from the agent: Revise round 3: one review thread, fixed. crates/minimald/src/net/policy.rs is outside the task's layers (the daemon layer names only crates/minimald/src/net/mod.rs): the finding's remedy needs to read ExposedMapping's private local, so the accessor lives next to expose_request, its only builder, keeping the host:port split beside the code that defines the format — the smallest widening that fixes the thread; gvproxy_network.rs is not in the layers either but is already part of this branch's diff (468b561 added the log line there), so no new file entered the changed set. The e2e's log assertion ("host":"127.0.0.1", "port":) is unchanged and still holds — the fields keep their types and now come from the accepted forward's own local bind, so a future bind change makes that assertion fail loudly instead of passing silently. Also ran: cargo clippy -p minimald --all-targets --locked -- -D warnings (clean, 8m55s cold) and cargo fmt -p minimald.

@norrietaylor
Copy link
Copy Markdown
Member

/revise

Three rounds in, the tests job still fails on 6589f22 for the same reason: common::shell_lint::all_scripts_pass_shellcheck reports SC2317 on every line of fi_cleanup's body in scripts/session-e2e.sh (lines 1019 to 1022), because the function is reached only through the subshell's trap fi_cleanup EXIT. The # shellcheck disable=SC2329 directive above it silences the never-invoked finding and not SC2317. Change that directive to # shellcheck disable=SC2317,SC2329 with the same reason. Run scripts/lint-shell.sh before pushing; it is what the tests job runs.

Also the open minimal-review thread on crates/minimald/src/net/gvproxy_network.rs: the per-mapping info line hardcodes host = "127.0.0.1" and reads the port from the requested ingress.port_mappings rather than what the switch holds. Log from the applied mappings, or reply with why the requested set is the right source and resolve the thread.

@norrietaylor
Copy link
Copy Markdown
Member

/revise

The round the runtime opened at 03:25Z never got a container (two container-capacity refusals) and no job exists for it now, so the task status is stuck on "round 4". Same directive as my 03:13Z note: widen the directive above fi_cleanup in scripts/session-e2e.sh to # shellcheck disable=SC2317,SC2329 (CI's shellcheck reports SC2317 per body line; a newer local shellcheck reports SC2329, so both are needed), run scripts/lint-shell.sh before pushing, and address or answer the open minimal-review thread on gvproxy_network.rs (log from the applied mappings, or say why the requested set is right). The branch already carries main as of 308a705.

# Conflicts:
#	scripts/session-e2e.sh
@norrietaylor
Copy link
Copy Markdown
Member

/revise

Base merged: origin/main is now in the branch at 2198cf8 (conflict in scripts/session-e2e.sh resolved keeping both sides; the branch case sits before main's hostnames_recover_and_two_daemons_route). Round 4 died on that stale base before pushing, so the directive stands unchanged:

Same directive as my 03:13Z note: widen the directive above fi_cleanup in scripts/session-e2e.sh to # shellcheck disable=SC2317,SC2329 (CI's shellcheck reports SC2317 per body line; a newer local shellcheck reports SC2329, so both are needed), run scripts/lint-shell.sh before pushing, and address or answer the open minimal-review thread on gvproxy_network.rs (log from the applied mappings, or say why the requested set is right). The branch already carries main as of 308a705.

The fresh-install proof's fi_cleanup runs only as the subshell's EXIT
trap, which shellcheck cannot see being called. Older shellchecks (the
one CI's shellcheck lane pins) report that as SC2317 across the body;
newer ones (0.11, which scripts/lint-shell.sh uses locally) report the
never-invoked function as SC2329 instead. Widen the directive on the
definition to cover both codes, with the reasoning beside it, so the
gate stays green under either shellcheck; a directive on a function
definition silences the whole body, so one line is enough.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 5 of 10 on a /revise note, pushed as 87ed6824cc3e.

line exit tail
just test-installer installer_switch_binary_executable 0 == shellcheck --shell=sh == == running install_test.sh under sh == # install.sh tests (SH=sh) ok - fresh install exits 0 with the switch binary shipped ok - install names the switch binary it verified ok - the verified switch binary is executable ok - install dies when the shipped switch bin
cargo nextest run -p switch unenrolled_self_allocates_default_plan 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-f569... Compiling switch v0.0.1 (/home/runner/.local/state/minimal/sessions/d925c/tree/crates/switch) Finished 'test' profile [unoptimized] target(s) in 2.12s ──────────── Nextest run ID
cargo nextest run -p switch loopback_slices_disjoint_per_switch 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-926d... Finished 'test' profile [unoptimized] target(s) in 0.34s ──────────── Nextest run ID c55b3251-85ef-4fa8-9abc-26c621a4e5e9 with nextest profile: default Starting 1 test across 1
cargo nextest run -p minimald allocator_uses_default_plan 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-0098... Compiling ot v0.0.1 (/home/runner/.local/state/minimal/sessions/6018c/tree/crates/ot) Compiling args v0.0.1 (/home/runner/.local/state/minimal/sessions/6018c/tree/crates/args) C
./scripts/session-e2e.sh fresh_install_own_ip_ingress_publishes_loopback 0 fresh-install loopback publish SKIPPED (no /dev/net/tun on this host: an own-IP box cannot open its in-namespace tap; runs for real on a host that has the device) session e2e OK Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-6fc5...

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/minimald/src/net/dns.rs
  • crates/minimald/src/net/gvproxy_network.rs
  • crates/minimald/src/net/policy.rs
  • crates/minimald/src/sessions.rs

Converted to a draft: something above needs a person before review.

Review threads the coder judged:

  • 👍 valid — crates/minimald/src/net/gvproxy_network.rs: Already fixed on this branch at 6589f22: the loop iterates exposed and reports each accepted forward's own local bind via ExposedMapping::host_port (policy.rs:140), never the request, so the log cannot claim a host a mapping is not published on.

The fresh-install proof stands on this branch — the installed pair answers 127.0.0.1:8080 with the box's own server and the daemon log names host, port and session per exposed mapping read from what the switch actually holds — and this round keeps scripts/session-e2e.sh clean under both shellcheck generations, so the lint gate CI runs over it stays green beside the proof.

Notes from the agent: Round 5 delta only. (1) Widened the shellcheck directive above fi_cleanup to disable=SC2317,SC2329 with the reasoning in the comment block above it; a directive on a function definition covers the whole body (verified locally), so the one line silences CI's SC2317 and the local 0.11.0's SC2329. (2) The gvproxy_network.rs thread needed no code change this round: commit 6589f22, already on the branch, made the per-mapping line read the accepted forward's own local bind through ExposedMapping::host_port (verified against crates/minimald/src/net/policy.rs, where host_port splits the switch-held local), so the record cannot claim a host a mapping is not published on. (3) The e2e case cannot run for real on this box: it has no /dev/net/tun (its own guard skips with exit 0) and no /usr/bin/gvproxy-min half either; the native CI lane runs it for real. Building the min+minimald pair (cargo build -p minimald --bin minimald -p minimal --bin min --locked, the e2e-native recipe's build line) was needed to run that test line at all.

@norrietaylor
norrietaylor marked this pull request as ready for review September 26, 2026 16:11
@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 1 finding, 1 inline.

Comment thread crates/switch/src/lib.rs Resolved
@norrietaylor norrietaylor left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gates met: T9's five test lines exit 0 on 87ed682, and the e2e case fresh_install_own_ip_ingress_publishes_loopback ran for real in the native-daemon-e2e lane (installed pair, shipped switch, host-loopback answer, logged expose); agent-verify passed; CI green on the head across all seven workflows; minimal-review concluded (neutral) and CodeRabbit approved on the head; the four daemon files outside the layers carry the per-mapping expose log line an earlier review thread asked for and the slice derivation the switch layer moved into the plan, both in scope; the one remaining doc thread is answered and resolved; no open threads, not a draft, no needs-human.

@norrietaylor
norrietaylor merged commit f9945c0 into main Sep 26, 2026
35 checks passed
@norrietaylor
norrietaylor deleted the agent/1633-publish-an-own-address-box-on-host-loopback-from branch September 26, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

T9 — Publish an own-address box on host loopback from a fresh install, with the installer checking the switch binary

1 participant

0