8000 feat(minimal): keep box names answering across a reboot and a second VM (T119) by minimal-foundry[bot] · Pull Request #2722 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

feat(minimal): keep box names answering across a reboot and a second VM (T119) - #2722

Merged
norrietaylor merged 4 commits into
mainfrom
agent/2700-box-names-keep-answering-across-a-reboot-and-a-s
Oct 29, 2026
Merged

norrietaylor merged 4 commits into
mainfrom
agent/2700-box-names-keep-answering-across-a-reboot-and-a-s

Conversation

@minimal-foundry
@minimal-foundry minimal-foundry Bot commented Oct 28, 2026 •
Copy link
Copy Markdown
Contributor

T119 — Box names keep answering across a reboot and a second VM

slice: S19
kind: task
covers: NET-027, NET-122
size: L
est: 360 code, 360 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: min finalize-install --show lists the boot unit as missing when it is absent, with the content "box names stop resolving after a restart until min finalize-install runs again", pinned by finalize_install_names_missing_items_and_runs_the_script; the minvmd log names which node holds the answerer port and which registered into it
observability: one info line when the boot unit re-applies the resolver link, in the system journal; one info line per node registration and one warn line when a node waits past its bound for the holder's channel
layers:
- hook:
- crates/minimal/src/resolver.rs: the Linux names steps install a root-owned systemd unit that re-creates the minzone0 link and its resolvectl settings on every boot after systemd-resolved starts, the undo removes it, and the names verdict reads the unit's absence as a missing part of the item (L2)
- crates/minimal/src/cmd/finalize_install.rs: the names item names the missing boot unit in its summary
test: cargo nextest run -p minimal finalize_install_names_missing_items_and_runs_the_script
test: cargo nextest run -p minimal finalize_install_undo_removes_what_the_step_installs
test: cargo nextest run -p minimal finalize_install_refuses_another_users_service
test: cargo nextest run -p minimal linux_zone_link_restored_at_boot
- answerer:
- crates/minvmd/src/net/answerer.rs: a VM host daemon that finds the answerer port held waits a bounded window for the holder's interim channel and registers into it, instead of reporting the port held with no channel; exactly one node holds the port when two start together (minimal#2629)
- crates/minvmd/src/control.rs: the answerer status read reports starting until the node's first publish is acknowledged, so an activation never reads a registration in flight as a failure
test: cargo nextest run -p minvmd second_vm_registers_into_the_running_holder
test: cargo nextest run -p minimald two_daemons_route_hostnames_concurrently
produces: none
consumes: none
proof: After a Linux host restarts, <name>.min.internal resolves without running min finalize-install again, and a second named VM's boxes resolve beside the default VM's on every activation.
depends on:
- assumes OQ2 (one host DNS user per machine, the current behaviour; host setup refuses on a host whose installed service names another user)

Layer exception

scripts/install.sh is outside T119's layers. Its uninstall advice now removes the minzone0-link.service boot unit before the link, matching --undo. scripts/install_test.sh ties that advice to linux_undo_command, so the change needs this hunk. The arch review at f1b53c5 says the same.

Design ruling

§7.1 line 251 says the Linux per-link routing domain is "persisted by whichever manager owns the host's links". The owner ruled (2026-10-28) that Minimal's own root-owned boot unit owns the dedicated minzone0 link it creates, with the same custody line 258 requires of the macOS LaunchDaemon. So minzone0-link.service satisfies line 251, and no systemd-networkd or NetworkManager configuration is needed.

Proof

On Linux, min finalize-install now writes and enables /etc/systemd/system/minzone0-link.service. At every boot, after systemd-resolved starts, this unit re-creates the minzone0 link and its resolvectl settings, so <name>.min.internal resolves after a restart with no second run. min finalize-install --show lists the item as missing when the link routes but the unit is absent, with the line "box names stop resolving after a restart until min finalize-install runs again". --undo removes the unit before it deletes the link. A second VM host daemon that loses the answerer port to a daemon that started beside it now waits up to 3 s for the holder's channel and registers into it. In that window its status read says starting, not 'port held with no channel', so the boxes of both VMs resolve on every activation. The new minvmd test proves this: two daemons started together end with one holder and one registrant, and the box names of both VMs answer.

Notes from the agent: Design: the boot unit state is a new Hook.boot_unit field (BootUnit::NotRead / Absent / Installed{port}). Only the Linux detection reads it, from the unit file and its multi-user.target.wants link. The verdict also counts a unit that names another port as missing. The boot-unit fact is said only while the hook routes, because a hook that does not route already prints its own fact and the step installs both. The Linux step text of the names item now ends '... on 127.0.0.1: at every boot'. I updated the two finalize_install.rs tests that pin it and also linux_advisory_command_is_unchanged and session_advisory_agrees_with_the_hook_it_detected in resolver.rs. The boot unit's journal line is an ExecStartPost=echo (info priority, default for service stdout). minvmd: HOLDER_CHANNEL_WAIT = 3 s. Box address requests that arrive in the wait are queued and served after the registration, or refused with the old reason when the wait runs out. AnswererStatus gains reported(), which contr…[truncated]

Tests

line exit tail
cargo nextest run -p minimal finalize_install_names_missing_items_and_runs_the_script 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-0dec... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling args v0.0.1 (/workbench/crates/args) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimal finalize_install_undo_removes_what_the_step_installs 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-9ba2... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimal finalize_install_refuses_another_users_service 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-e0ea... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimal linux_zone_link_restored_at_boot 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-7f60... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minvmd second_vm_registers_into_the_running_holder 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-0a78... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling args v0.0.1 (/workbench/crates/args) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimald two_daemons_route_hostnames_concurrently 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-cf8d... Compiling ot v0.0.1 (/workbench/crates/ot) Compiling args v0.0.1 (/workbench/crates/args) Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workb

Verify

min task run agent-verify exited 100 under rustc 1.99.0, which the repository does not pin (1.99.1 in rust-toolchain.toml); the PR is not a draft on that alone — CI, which honours the pin, is the gate.

kind": "GetNetSwitch",
                "parent_span_id": "4f56504c01148c8a",
                "span_id": "e2b133fe880d956f",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "GetDenyAllOptOut",
                "parent_span_id": "4f56504c01148c8a",
                "span_id": "eaf9dce507410234",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "GetPaths",
                "parent_span_id": "4f56504c01148c8a",
                "span_id": "0b2d413907328f86",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "GetRecord",
                "parent_span_id": "4f56504c01148c8a",
                "queued_behind": "0",
                "span_id": "a2c8870d33d92e47",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "DeleteSession",
                "parent_span_id": "4f56504c01148c8a",
                "queued_behind": "0",
                "span_id": "1282bcf8d1fc131b",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "Destroy",
                "parent_span_id": "1282bcf8d1fc131b",
                "span_id": "270e0481fae4d091",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
    ]
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

  Cancelling due to test failure: 3 tests still running
        PASS [   3.013s] ( 813/1441) minvmd box_registry::tests::a_closed_shuttle_detaches_rows_until_the_grace
        PASS [   2.217s] ( 814/1441) minvmd box_registry::tests::a_reattributed_box_keeps_its_row
        PASS [   5.008s] ( 815/1441) minvmd box_registry::tests::a_registration_refused_for_a_held_address_spends_no_switch_address
────────────
     Summary [  34.795s] 815/1441 tests run: 814 passed, 1 failed, 2 skipped
        FAIL [   2.714s] ( 812/1441) minimal::otel_cli exec_run_attach_and_task_run_share_the_commands_trace
warning: 626/1441 tests were not run due to test failure (run with --no-fail-fast to run all tests, or run with --max-fail)
error: test run failed

Closes #2700


Generated by Claude Code

A VM host daemon that loses the answerer port's bind to a daemon that
started beside it now waits a bounded window for the holder's interim
channel and registers into it, instead of naming the port as held with
no channel (minimal#2629). The control socket's status read says
starting until the node's first publish is acknowledged or the node
gives up past its bound.
The Linux names step now installs a root-owned systemd unit,
minzone0-link.service, that re-creates the minzone0 link and its
resolvectl settings after systemd-resolved starts, so <name>.min.internal
keeps resolving after a restart. The undo removes the unit before the
link. The names item reads a routing hook with no boot unit behind it
as a missing part of the item, and says that box names stop resolving
after a restart.
@coderabbitai
coderabbitai Bot commented Oct 28, 2026 •
Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

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

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6b8854f5-a425-4071-b2e2-4575efdfe1ed

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
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@minimal-foundry
minimal-foundry Bot commented Oct 28, 2026 •
Copy link
Copy Markdown
Contributor Author

Approved: every review thread resolved at f1b53c57810b.

@norrietaylor resolved the last thread, which releases the approval this review had withheld.

Nothing to do.

@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 — No findings.

Panel: 2 of 3 personas ran (security, supply-chain); did not complete: correctness.

@minimal-foundry minimal-foundry Bot added the review:approved minimal-review approved the current head label Oct 28, 2026
The undo removes minzone0-link.service and its enable link. The
uninstall advice must name the same paths, or install_test.sh fails
and a person who follows the advice keeps a unit that re-creates the
minzone0 link at every boot.
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

arch-review: CONFIRM · the full ruling

The answerer change (NET-027) and the finalize-install item (NET-122) conform to §7.1 lines 250 and 251, apart from one design question. Line 251 says the Linux hook is persisted by whichever manager owns the host's links, and it names NetworkManager as one. This change persists the link with its own oneshot unit, minzone0-link.service, and does not use the host's network manager. The task text asks for that unit, so the coder cannot settle this. I filed an escalation for a design decision, and this review is not a ruling until the design answers it. If the design accepts the unit, this head needs no change.

Ruled at f1b53c5 against gominimal/arch@109541a48c31 by claude-opus-5-5.

Comment thread crates/minimal/src/resolver.rs Resolved
@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.

APPROVE withheld: 1 unresolved thread — minimal-arch[bot] crates/minimal/src/resolver.rs:102.

minimal-review — advisory 8000 review

Verdict COMMENT — No findings.

Panel: 2 of 3 personas ran (correctness, supply-chain); did not complete: security.

@minimal-foundry minimal-foundry Bot removed the review:approved minimal-review approved the current head label Oct 28, 2026
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

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

line exit tail
cargo nextest run -p minimal finalize_install_names_missing_items_and_runs_the_script 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-b9f0... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling args v0.0.1 (/workbench/crates/args) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimal finalize_install_undo_removes_what_the_step_installs 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-bac2... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimal finalize_install_refuses_another_users_service 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-2c0a... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minimal linux_zone_link_restored_at_boot 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-8fbc... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minvmd second_vm_registers_into_the_running_holder 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-2d2e... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimald two_daemons_route_hostnames_concurrently 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-00b8... Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workb

Verify min task run agent-verify exited 100 under rustc 1.99.0, which the repository does not pin (1.99.1 in rust-toolchain.toml); the PR is not a draft on that alone — CI, which honours the pin, is the gate.

kind": "GetNetSwitch",
                "parent_span_id": "a5180f769380aa92",
                "span_id": "647c4affd9c8b9fe",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "GetDenyAllOptOut",
                "parent_span_id": "a5180f769380aa92",
                "span_id": "ad55e58d56b5d964",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "GetPaths",
                "parent_span_id": "a5180f769380aa92",
                "span_id": "1b677a3fa02b57aa",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "GetRecord",
                "parent_span_id": "a5180f769380aa92",
                "queued_behind": "0",
                "span_id": "9c4ac38abbae160e",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "DeleteSession",
                "parent_span_id": "a5180f769380aa92",
                "queued_behind": "0",
                "span_id": "7eb6706adde0089f",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "Destroy",
                "parent_span_id": "7eb6706adde0089f",
                "span_id": "9a3fd344abb5814c",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
    ]
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

  Cancelling due to test failure: 3 tests still running
        PASS [   3.016s] ( 813/1443) minvmd box_registry::tests::a_closed_shuttle_detaches_rows_until_the_grace
        PASS [   2.217s] ( 814/1443) minvmd box_registry::tests::a_reattributed_box_keeps_its_row
        PASS [   5.010s] ( 815/1443) minvmd box_registry::tests::a_registration_refused_for_a_held_address_spends_no_switch_address
────────────
     Summary [  35.895s] 815/1443 tests run: 814 passed, 1 failed, 2 skipped
        FAIL [   2.819s] ( 812/1443) minimal::otel_cli exec_run_attach_and_task_run_share_the_commands_trace
warning: 628/1443 tests were not run due to test failure (run with --no-fail-fast to run all tests, or run with --max-fail)
error: test run failed

Outside the task's layers::

  • scripts/install.sh

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

After a Linux host restarts, the minzone0-link.service boot unit re-creates the minzone0 link and its resolvectl settings, so .min.internal resolves without running min finalize-install again. A second named VM registers into the holder's channel, so its boxes resolve beside the default VM's boxes. The uninstall advice from install.sh now also removes the boot unit and its enable link.

Notes from the agent: The failed check was the workspace suite's common::shell_harnesses::all_script_test_harnesses_pass. In it, scripts/install_test.sh checks that the uninstall advice in scripts/install.sh names every absolute path constant in resolver.rs. The two new constants ZONE_LINK_UNIT_PATH and ZONE_LINK_UNIT_WANTS_PATH were not in the advice. I added both paths to the Linux _ns_files list in scripts/install.sh. This file is outside the task's layers. I touched it because the check fails without it, and because a person who follows the old advice keeps a unit that re-creates minzone0 at every boot. The rm -f before systemctl daemon-reload removes the enable link, so no separate systemctl disable is necessary. scripts/install_test.sh does not seed the two unit files in its removal scenario. I did not extend it, because it is also outside the layers. The leaky test cmd::stop::tests::a_stop_near_its_deadline_keeps_a_sigterm_window in minvmd is not related to this branch and passes under the ci profile. I did not run the workspace suite. I ran the minimal, minvmd and common crates in full under --profile ci, and all of them pass.

@minimal-foundry
minimal-foundry Bot marked this pull request as draft October 28, 2026 08:24
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 2 of 10 on a failed check suite: the coder changed nothing. Revise round 2. I changed nothing this round. The failed checks are test-kvm, ci-linux-kvm-success, await-kvm and ci-macos-success. The macOS failures only follow the KVM lane: await-kvm says the mac e2e did not run because the KVM lane failed. The test-kvm excerpt in the directive shows only the echo lines of the job's scripts and the post-job cleanup. It does not show which step failed or any error output, so I cannot find a cause from it. I checked the branch for a cause the VM lane can hit. All six test: lines pass. cargo nextest run -p minvmd --lib passes (627 tests). The three zone-answerer log strings that scripts/session-e2e.sh greps for are still in crates/minvmd/src/net/answerer.rs. The new 3 s wait for the holder's channel is inside the script's 20 s poll. This box has no /dev/kvm, so I did not run the VM harnesses or the session e2e. A person must re-run the Linux/KVM lane and read the failing step's log. If it fails again, the next round needs that step's out…[truncated]

Review threads the coder judged:

  • 👎 invalid — crates/minimal/src/resolver.rs:102: No change this round, as the comment asks. The task's hook layer names this exact unit: a root-owned systemd unit that re-creates minzone0 and its resolvectl settings at every boot. So the code follows the approved plan. Whether a Minimal-owned boot unit counts as the manager that owns the host's li

@norrietaylor
norrietaylor marked this pull request as ready for review October 28, 2026 19:07
@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 — every review thread is resolved; @norrietaylor closed the last one. The review of f1b53c57810b withheld this approval on the threads open then. Posted as a comment, as the review itself was; the check and the label carry it.

@minimal-foundry minimal-foundry Bot added the review:approved minimal-review approved the current head label Oct 28, 2026
@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.

Babysitter review at f1b53c57. I read the answerer and finalize-install changes against T119's plan block and arch's ruling at this head (arch_rulings 513, CONFIRM pending one design question). NET-027: a node that loses the port bind waits HOLDER_CHANNEL_WAIT for the holder's interim channel and publishes through the same connect_and_publish, so one answerer serves both nodes and every error path settles the status. NET-122: the names item counts an absent, unenabled or wrong-port boot unit as missing, and the script installs minzone0-link.service with the custody line 258 requires (root-owned, 0644, literal ip/resolvectl, no config read); --undo and the install.sh advice remove it. The design question is answered: the owner ruled (2026-10-28) that Minimal's own boot unit satisfies §7.1 line 251 for the dedicated link, recorded in the PR body. The install.sh hunk is a recorded layer exception. No findings.


Generated by Claude Code

@norrietaylor norrietaylor changed the title T119 — Box names keep answering across a reboot and a second VM feat(minimal): keep box names answering across a reboot and a second VM (T119) Oct 29, 2026
@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.

Babysitter approval at f1b53c5: every merge gate passes at this head (arch PASS at f1b53c5, zero open threads, every lane aggregator green). Squash title feat(minimal): keep box names answering across a reboot and a second VM (T119) and the squashed message pass commitlint. Queued for squash merge.


Generated by Claude Code

@norrietaylor
norrietaylor added this pull request to the merge queue Oct 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 29, 2026
@norrietaylor
norrietaylor added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit 1f4bfd6 Oct 29, 2026
46 of 50 checks passed
@norrietaylor
norrietaylor deleted the agent/2700-box-names-keep-answering-across-a-reboot-and-a-s branch October 29, 2026 01:35
norrietaylor added a commit that referenced this pull request Oct 29, 2026
…(0.7.0 backport) (#2742)

* fix(minimal): hold the answerer channel socket owner-only (#2297)

* fix(minimal,minvmd): hold the answerer channel socket owner-only

The box-zone answerer's channel socket was world-connectable: the
launchd plist bound it SockPathMode 0666 and the systemd socket unit
SocketMode=0666, leaving the peer-uid gate the only check on who may
connect. Both units now bind it owner-only -- SockPathOwner the
operator, SockPathMode 0600 on launchd; SocketUser the operator,
SocketMode=0600 on systemd -- with the uid gate kept as defence in
depth.

An install whose socket node carries group or other bits -- one that
predates the mode, or a hand that relaxed it -- now fails custody
through a channel_mode fact, so the advisory re-surfaces and a step
re-run re-binds the socket. The accept loop's foreign-refusal warn is
rate-limited to a few lines a second, so a connect storm cannot flood
the log; the refusals themselves are never limited.

* fix(minimal,minimald,minvmd): name the other account on a refused channel connect

A daemon whose connect the owner-only channel socket refuses now says
the host's DNS service belongs to another account, not a bare
Permission denied. The answerer step needs the operator's account to
resolve, with no stand-in uid, and custody fails a channel socket the
operator does not own.

* fix(minimal,minvmd): leave another operator's DNS service alone

A channel socket owned by another existing, non-root account is that
operator's service: the answerer step is now unavailable with the same
sentence the daemons say on a refused connect, instead of a custody
failure whose step would reinstall the unit and take the service over.
A root-owned socket, or one whose uid names no account, still fails
custody so the step re-binds it.

The minvmd tests that take the process-global foreign-refusal slots
now run one at a time and start and end with every slot free, and the
warn-rate test bounds the burst at two windows' worth of lines, so a
slow runner that spreads the burst does not fail it.

* fix(minimal): offer the step over a socket another account left behind

A channel socket another account owns refuses the answerer step only
when a service is installed; with none, the step's install removes the
leftover socket before it loads the unit, so it is offered.

---------

Co-authored-by: gominimal-aw-bot[bot] <281738952+gominimal-aw-bot[bot]@users.noreply.github.com>
(cherry picked from commit 746770d)

* fix(net): refuse out-of-zone names at a guest deny-subtree DNS listener (#2300)

* fix(net): refuse out-of-zone names at a guest deny-subtree DNS listener

A deny-all host-address box on a VM-backed guest resolved out-of-zone
names at the switch gateway: the interim NET-079 carve-out admitted the
gateway's port 53 whole, UDP and TCP, so a box that admits no destination
could still ask a resolver beyond the machine — and DNS over TCP could
retry past the verdict.

The guest's deny subtree now steers its UDP-53 lookups onto a loopback
listener on the answerer's port (NET-141 slice 2): deny_out admits the
listener beside the reply direction, the dstnat match carries no
destination address (the box asks the gateway like every other
host-address box; the steer is the carve-out), and the listener itself
answers REFUSED for every name outside the box zone — apex and a
non-lowercase zone suffix among them — NODATA for an in-zone non-A
question, and forwards the zone's A questions to the switch gateway
alone. DNS over TCP is neither steered nor admitted.

The guest daemon starts the listener after its classifier table loads;
the daemon's own lookups are untouched (cgroup-keyed match, deny path
only). The interim --gateway-resolver installer flag and every test,
digest and pin that spelled the interim carve-out follow the new shape.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(net): bound and pin the guest DNS listener's forwards

Connect each forward socket to the gateway so a datagram from any other
sender is dropped, cap forwards in flight so a flood cannot exhaust the
daemon's descriptors, and drop the needless to_string calls clippy
rejects in the guest classifier integration test.

* fix(net): refuse questionless and non-QUERY messages at the guest DNS listener

decide() forwarded a message with no question, and an UPDATE or NOTIFY
whose zone section named an in-zone A, because every per-question check
passed vacuously. Answer the first REFUSED and the rest NOTIMP, so the
forward path carries only standard lookups.

Bind the listener directly so its log does not read as the box-zone
answerer, pin the integration test's boot-log check to the exact
example.com REFUSED line, and cite NET-079 (the deny-all case) rather
than NET-141 at the lines this change added.

---------

Co-authored-by: gominimal-aw-bot[bot] <281738952+gominimal-aw-bot[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 2c2f8cc)

* fix(net): spawn the guest DNS tasks with tokio::spawn on release/0.7.0

crate::traced landed on main after the release branch was cut, so the
cherry-pick of #2300 did not compile here.

* build: declare 0.7.3 for the release/0.7.0 backports

v0.7.2 is tagged at the branch head, so the version check requires the
next patch release.

* chore(deps): update pulldown-cmark to 0.13.5 for RUSTSEC-2026-0414

* fix(minimald): close the host classifier's mark, IPv6 and probe gaps (T105) (#2712)

* fix(minimald): close the classifier's mark, IPv6 and probe gaps

The install's conflict scan now treats a ct-mark write with no mask
as a user of every bit, names each rule with its table and chain,
and --ct-mark-mask refuses the ranges Tailscale and kube-proxy class
with. The table refuses non-loopback IPv6 from the boxes subtree and
counts that refusal in the same named counter as the deny chain.

The per-launch probe reads with a UDP datagram, so a launch no longer
waits the SYN retransmit a refused TCP connect pays. The reading at
daemon start keeps its TCP leg. Each probe logs its leg and duration.

* test(minimald): prove the classifier's mark, IPv6 and probe gaps live

Three root-lane proofs load the real table: the install refuses a
whole-mark ct writer and a mask in a well-known range, and an allow
box's IPv6 connection to a peer is refused and counted. The live probe
proof also checks the per-launch reading waits no retransmit.

* fix(minimald): pass shellcheck and pin the new mask refusals

The rerun hint's empty assignment tripped SC1007 in the shell lint
gate. The installer test pinned the old remedy text and escaped with
0x0000c000, the kube-proxy range the install now refuses: it now
pins the new remedy, escapes with 0x03000000, and pins the
well-known range refusal. The IPv6 refusal comment names the
condition that replaces it.

* fix(minimald): re-apply the recorded ct-mark mask and name min finalize-install

With no --ct-mark-mask, the install step adopts the mask a previous
install recorded under the tree. Mask refusals now name
min finalize-install --ct-mark-mask <mask>. The conflict scan joins the
nft and iptables-save hits with a newline.

* feat(minimal): add min finalize-install --ct-mark-mask and keep it at boot

The mask record lives in cgroupfs, so a reboot empties it. The classifier
step's block now passes the chosen or recorded mask to the one-time run
and to the boot unit's ExecStart, so the operator's bits survive a
reboot. A requested mask that differs from the loaded table's re-runs
the step.

* fix(minimald): refuse a CONNMARK --restore-mark that copies the whole mark

The install's ct-mark scan now treats --restore-mark with no --mask,
--ctmask or --nfmask, or an all-ones one, as a whole-mark writer, as it
does --save-mark. A restore with a narrow mask is still no conflict.

* fix(minimald): allow --restore-mark, bind probe datagrams first

A CONNMARK --restore-mark copies the connection mark into the packet
mark and never writes the connection mark, which is all the classifier
reads. This reverts 16d9160 and pins a restore with no mask as no
conflict.

hold_probe_listeners now binds its datagram endpoints before it spawns
any TCP drain thread, so a failed datagram bind returns before a
listener serves.

---------

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
(cherry picked from commit 354548e)

* fix(minimald): adapt the host classifier gaps backport to release/0.7.0

The cherry-pick before this one keeps main's text for every line #2712
changes. Main's neighbouring code comes from the QUIC block work
(#2513, #2553, #2601, #2644), the box-name placement (#2722) and the
crate renames, none of which release/0.7.0 has. The conflicts were
resolved onto release's code without importing that work:

- classifier.rs: the four new unit tests are appended after release's
  last test, without the QUIC cohort tests main has above them. Their
  sandbox::classifier paths are sandbox2::classifier here.
- install-host-classifier.sh: the table gains the refused counter. The
  QUIC leaf set that main declares beside it is not on release, so it
  stays out. The --help range (2,87p) matches release's header too.
- finalize_install.rs: release has one classifier item, built by
  classifier_item_on_this_host, not main's classifier_items_on_this_host
  with the QUIC block helper item. That function takes --ct-mark-mask
  and fills recorded_mask and requested_mask itself. plan_for_this_host
  keeps release's names item placement and only passes the mask on.
- #2712's edits to finalize_install_names_the_quic_block_helper are
  dropped: release has no QUIC block helper item or test.
- install_test.sh: run_hc puts HC_PATH_PREFIX ahead of PATH, the line
  #2601 added on main. Without it the new --restore-mark case never
  reaches its stand-in iptables-save, and shellcheck flags the variable
  as unused.

The user-facing text is main's, unchanged.

---------

Co-authored-by: gominimal-aw-bot[bot] <281738952+gominimal-aw-bot[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review:approved minimal-review approved the current head

Projects

None yet

Development

Successfully merging this pull request may close these issues.

T119 — Box names keep answering across a reboot and a second VM

1 participant

0