8000 fix(minimald): close the host classifier's mark, IPv6 and probe gaps (T105) by minimal-foundry[bot] · Pull Request #2712 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

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

Merged
norrietaylor merged 14 commits into
mainfrom
agent/2692-close-the-host-classifier-s-mark-ipv6-and-probe
Oct 29, 2026
Merged

norrietaylor merged 14 commits into
mainfrom
agent/2692-close-the-host-classifier-s-mark-ipv6-and-probe

Conversation

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

T105 — Close the host classifier's mark, IPv6 and probe gaps

slice: S16
kind: task
covers: NET-079
size: L
est: 220 code, 420 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: when the install step refuses a ct-mark writer or a mark range, it names the rule or the range and the component it belongs to, and says to rerun min finalize-install --ct-mark-mask <mask> with two free bits, pinned by install_refuses_a_whole_mark_ct_writer
observability: the daemon logs one line per per-launch probe with its outcome and how long it took, and the classifier's IPv6 refusal counts in the same drop counter its IPv4 refusals use
layers:
- install:
- scripts/install-host-classifier.sh: the conflict scan treats a ct-mark write without an explicit mask as overlapping every bit; the mask override refuses the ranges well-known components use; the table rejects non-loopback IPv6 egress from the boxes subtree
- crates/minimald/tests/classifier_root_integration.rs: one root-lane proof per gap that loads the real table and observes the effect
test: cargo nextest run -p minimald install_refuses_a_whole_mark_ct_writer
test: cargo nextest run -p minimald install_refuses_a_well_known_mark_range
test: cargo nextest run -p minimald host_ip_box_ipv6_egress_refused
- classifier:
- crates/minimald/src/net/classifier.rs: the per-launch probe no longer waits a SYN retransmit on its TCP leg; the TCP probe stays at daemon start
test: cargo nextest run -p minimald per_launch_probe_returns_without_tcp_retransmit_wait
test: cargo nextest run -p minimald host_ip_deny_all_no_outbound
produces: none
consumes: none
proof: On a native host with the classifier installed, an allowed host-address box's IPv6 connection to a peer is refused, and installing over a firewall rule that copies whole ct marks is refused with the rule named.
depends on:
- assumes OQ3 (the spec's interim holds: a native non-deny-all host-address box resolves through the host's resolver and its allow list is decided by address rules only)

Proof

The table the install loads now has an ipv6_out chain. It refuses every non-loopback IPv6 connection from the boxes subtree, allow boxes included, and counts each refusal in the same named counter refused as the deny-all chain. When a host has a rule that copies or sets the whole ct mark (for example wg-quick's ct mark set meta mark), the install refuses. The refusal names the rule with its table and chain, and no --ct-mark-mask gets past it. The unit tests show both over the rendered table and a stand-in nft. The live proof on a native host is the root-lane tests host_ip_box_ipv6_egress_refused and install_refuses_a_whole_mark_ct_writer. They passed in the minimald-root-integration job on f7cbcee, with nft present and the table loaded: https://github.com/gominimal/minimal/actions/runs/40651618713/job/122692831268 (install_refuses_a_whole_mark_ct_writer 2.24 s, install_refuses_a_well_known_mark_range 3.35 s, host_ip_box_ipv6_egress_refused 3.56 s, the_installers_table_refuses_the_probe_over_a_scratch_tree 3.61 s, classifier_table_loads_on_this_kernel 1.98 s).

Notes from the agent: Root-lane proofs not run: this box has no nft, no iptables-save, no sudo and no root. So the three new #[ignore]d proofs in classifier_root_integration.rs and the new timing assert in the_installers_table_refuses_the_probe_over_a_scratch_tree are unverified, and the new nft syntax is not checked against a live kernel. That syntax is a counter refused { packets 0 bytes 0 } object, counter name "refused" statements, and a second filter base chain at the output hook. The just test-root-integration lane owes that evidence.

Printed text: the task's diagnostics line says to rerun min finalize-install --ct-mark-mask <mask>, but min finalize-install has no --ct-mark-mask flag (crates/minimal/src/cli.rs). That command cannot run as written, so the refusals name the step's own command with its other arguments kept: sudo <step path> ... --ct-mark-mask <mask>. Note that finalize-install and its boot unit run the step with no mask, so a chosen mask is not kept across a reboot. Adding the flag is outside this task's layers (crates/minimal). A whole-mark writer refusal does not suggest another mask, because no mask avoids it. It says to change the rule to write only its bits, then run the step again.

Probe: the per-launch readings (read_filter, read_held_filter, wh…[truncated]

Layer exception

scripts/install_test.sh is outside T105's layers. It is the shell harness for scripts/install-host-classifier.sh, which is in the install layer. The layer change rewrote the mask-conflict refusal (mask_rerun_hint) and added WELL_KNOWN_MARK_RANGES, which refuses the harness's old escape mask 0x0000c000 (kube-proxy). Round 1 updated the harness to the new refusal text and an escape mask outside every well-known range, so common::shell_harnesses all_script_test_harnesses_pass passes again. It changes no behaviour of its own.

crates/minimal/src/cli.rs, crates/minimal/src/cmd/finalize_install.rs and crates/minimal/src/tests.rs are also outside the layers. Arch condition C2 (arch_rulings 505) found that a chosen --ct-mark-mask was lost at reboot, and the owner chose to fix it in this PR rather than file a residual. The boot unit finalize_install.rs writes now carries the mask, min finalize-install --ct-mark-mask <mask> exists so the refusal's remedy runs as written, and cli.rs and tests.rs carry the flag and its tests.

Tests

line exit tail
cargo nextest run -p minimald install_refuses_a_whole_mark_ct_writer 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-bc94... 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 install_refuses_a_well_known_mark_range 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-6696... 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 minimald host_ip_box_ipv6_egress_refused 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-b5b4... 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 minimald per_launch_probe_returns_without_tcp_retransmit_wait 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-3735... 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 minimald host_ip_deny_all_no_outbound 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-7dcf... 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.

Verify

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

Base

The base branch moved while the coder worked; it was merged in before the tests ran.

Closes #2692


Generated by Claude Code

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.
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.
@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: 4443d292-dc07-4aa8-a462-89fe64a72d86

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: no findings at 62b1ec1eb700.

Nothing to do.

Verdict APPROVE, posted as a comment: the App cannot review its own pull request; the check and the label carry it.

Clobber: none found in the touched code.

@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.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

@minimal-foundry minimal-foundry Bot added the review:approved minimal-review approved the current head label Oct 28, 2026
@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: PASS_WITH_CONDITIONS · 4 conditions · the full ruling

The change conforms to design §4.1 line 54 and fails closed at each new check. The live proofs did not run, so C1 needs the root lane and the KVM lane green on this head. The PR body names a residual with no issue: the ct-mark mask is lost at reboot, and §7.4 line 275 asks for a one-time install. C2 files it. The IPv6 refusal is the v1 posture that §4.2 line 69 and §12 item 5 describe. C3 puts its flip condition in the script, and C4 files the missing spec line.

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

Comment thread scripts/install-host-classifier.sh Resolved
Comment thread scripts/install-host-classifier.sh Resolved
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.
@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: PASS_WITH_CONDITIONS · 2 conditions · the full ruling

The IPv6 refusal and the ct-mark refusals conform to §4.1 and §7. They fail closed, and the IPv6 interim now names arch#43 as its flip condition. Two items stay open. The live root-lane proofs did not run on this head. Run the root-lane job with nft present and link the run (C1). A chosen --ct-mark-mask is lost at reboot, because the boot unit passes no mask. File an issue for it and link it from the PR body (C2).

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

Comment thread scripts/install-host-classifier.sh 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.

minimal-review — advisory review

Verdict COMMENT — 1 finding, 1 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread scripts/install-host-classifier.sh Resolved
@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 f7cbcee5ef81.

line exit tail
cargo nextest run -p minimald install_refuses_a_whole_mark_ct_writer 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-072e... 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 install_refuses_a_well_known_mark_range 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-c110... 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 minimald host_ip_box_ipv6_egress_refused 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-a636... 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 minimald per_launch_probe_returns_without_tcp_retransmit_wait 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-3df5... 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 minimald host_ip_deny_all_no_outbound 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-c7c5... 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.

Verify min task run agent-verify passed.

Outside the task's layers::

  • scripts/install_test.sh

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

Review threads the coder judged:

  • 👍 valid — scripts/install-host-classifier.sh:915: The comment above render_ruleset now says the IPv6 refusal is the v1 posture. It stays until the IPv6 admission path of design section 12 item 5 (gominimal/arch#43) replaces it.
  • 👎 invalid — scripts/install-host-classifier.sh:1269: The premise holds: the boot unit's ExecStart passes no mask, so the next boot goes back to 0x30000000. The fix is outside this task's layers. It needs crates/minimal/src/cmd/finalize_install.rs and a new --ct-mark-mask flag on min finalize-install. This session cannot file issues, so a person must f
  • 👍 valid — crates/minimald/tests/classifier_root_integration.rs:2412: Not closed this round. This box runs as uid 1000 with no nft. With --run-ignored the three root proofs return in about 7 ms without testing anything. Someone with a root host or the CI root lane must run just test-root-integration on this head and record the result. render_ruleset is unchanged apart

On a native host with the classifier installed, the table refuses an allowed host-address box's IPv6 connection to a non-loopback peer. An install over a firewall rule that writes the whole ct mark is refused, and the error names the table, the chain and the rule. This round makes the CI gates green again. The root-lane proofs that show both effects on a real table did not run on this box, so they are still owed by the root lane.

Notes from the agent: Cause of the CI failure: shellcheck SC1007 on local kept=() skip= a in scripts/install-host-classifier.sh, caught by crates/common/tests/shell_lint.rs. Fixed. scripts/install_test.sh is outside the layers. I changed it because it pinned text this task changed: the old remedy --ct-mark-mask <two contiguous bits nothing else classes with>, and an escape with 0x0000c000, which is now refused as the kube-proxy range. It now escapes with 0x03000000 and has a new case that pins the well-known range refusal. Diagnostics wording: the task says the refusal tells the user to run min finalize-install --ct-mark-mask <mask>. min finalize-install has no such flag, so the message names the step's own command (sudo <script> ... --ct-mark-mask <mask>), which a person can run as written. For a ct-mark writer that uses the whole mark, no mask helps, so that refusal tells the user to change the rule. The root-integration proofs (#[ignore]) cannot run here: uid 1000 and no nft. With --run-ignored they return in about 7 ms without testing anything. Someone must run just test-root-integration on a root host or check the CI root lane, and record the result in the PR body. Thread C2 needs a follow-up issue: mask persistence across the boot unit plus a min finalize-install --ct-mark-mask flag (crates/minimal/src/cmd/finalize_install.rs). I cannot file issues from this session.

@minimal-foundry
minimal-foundry Bot marked this pull request as draft October 28, 2026 07:54
@norrietaylor
norrietaylor marked this pull request as ready for review October 28, 2026 08:02
Copy link
Copy Markdown
Member

/revise Fix arch-review condition C2 at f7cbcee (arch_rulings 505) in this PR. The owner chose an in-PR fix over a residual issue, so the files outside T105's layers are allowed for this. C1 is closed: the root-lane run on f7cbcee is now in the PR body. Change nothing else.

  • Gap: an operator's chosen --ct-mark-mask is lost at reboot. The boot unit that crates/minimal/src/cmd/finalize_install.rs writes runs ExecStart={CLASSIFIER_PROGRAM_PATH} --user '<operator>' with no mask, and scripts/install-host-classifier.sh (line ~204) falls back to DEFAULT_CT_MARK_MASK without reading the recorded ct-mark-mask-*. On a host where the default conflicts, the boot install refuses and leaves no table.
  • Fix: make the boot path use the recorded mask: either the installer reads the recorded ct-mark-mask-* when no --ct-mark-mask is given, or the boot unit passes the recorded mask. Then add min finalize-install --ct-mark-mask <mask> so the remedy the diagnostics name can run as written, and make the refusal texts name that command.
  • Tests: a unit test that the boot path re-applies a recorded non-default mask; a scripts/install_test.sh case for the installer reading the record; a CLI test for the new flag. Update the PR body's notes and add a "Layer exception" line for crates/minimal.
  • Keep every commit message line at or under 100 characters.

Generated by Claude Code

…ze-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.
… 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.
@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: PASS · the full ruling

The change conforms to the design at this head. The deny-all verdict and the IPv6 refusal are unchanged since f7cbcee5, and they follow §4.1 line 54 and §7 line 231. The new min finalize-install --ct-mark-mask flag and the mask in the boot unit are on ground that the design does not cover. They close the reboot gap that the earlier PR body reported. Update the PR body: its notes still say that min finalize-install has no --ct-mark-mask flag.

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

@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 — 2 findings, 2 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread scripts/install-host-classifier.sh Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 2 of 10 on a /revise note, pushed as ee454d3c256b.

line exit tail
cargo nextest run -p minimald install_refuses_a_whole_mark_ct_writer 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-efdb... 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 install_refuses_a_well_known_mark_range 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-e24c... 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 minimald host_ip_box_ipv6_egress_refused 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-1364... 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 minimald per_launch_probe_returns_without_tcp_retransmit_wait 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-78a1... 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 minimald host_ip_deny_all_no_outbound 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-eca7... 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.

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.

_span_id": "9561a76e8e9484e2",
                "span_id": "e8e3dd71a04dd419",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "GetDenyAllOptOut",
                "parent_span_id": "9561a76e8e9484e2",
                "span_id": "bd1fc080dde82b30",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "GetPaths",
                "parent_span_id": "9561a76e8e9484e2",
                "span_id": "b9cb531b78e329f8",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "GetRecord",
                "parent_span_id": "9561a76e8e9484e2",
                "queued_behind": "0",
                "span_id": "39d36fdfa1889c8d",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "DeleteSession",
                "parent_span_id": "9561a76e8e9484e2",
                "queued_behind": "0",
                "span_id": "f6cf959106b42f78",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "Destroy",
                "parent_span_id": "f6cf959106b42f78",
                "span_id": "0e99c22051490933",
                "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 [   2.784s] ( 832/1972) minimald box_actor::tests::a_box_nobody_handed_an_address_publishes_at_the_answerers_grant
        PASS [   3.148s] ( 833/1972) minimald box_actor::tests::a_box_re_registered_inside_the_landing_window_keeps_its_grant
        PASS [   3.392s] ( 834/1972) minimald box_actor::tests::a_destroy_queued_behind_a_waiting_finalize_runs_after_the_promotion
────────────
     Summary [  58.098s] 834/1972 tests run: 833 passed, 1 failed, 23 skipped
        FAIL [   4.164s] ( 831/1972) minimal::otel_cli exec_run_attach_and_task_run_share_the_commands_trace
warning: 1138/1972 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::

  • crates/minimal/src/cli.rs
  • crates/minimal/src/cmd/finalize_install.rs
  • crates/minimal/src/tests.rs
  • scripts/install_test.sh

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

Review threads the coder judged:

  • 👍 valid — crates/minimald/tests/classifier_root_integration.rs:2413: The root-lane run on f7cbcee is in the PR body (owner's note: C1 closed). This round does not change host_ip_box_ipv6_egress_refused.
  • 👍 valid — crates/minimald/tests/classifier_root_integration.rs:2257: The root-lane run on f7cbcee, with nft present, is in the PR body (owner's note: C1 closed). The whole-mark refusal text this test pins is unchanged.
  • 👍 valid — scripts/install-host-classifier.sh: Fixed in this PR: min finalize-install --ct-mark-mask writes the mask into the boot unit's ExecStart, the installer re-applies a recorded mask, and the hints name that command.
  • 👍 valid — PRRT_kwDOPdfcxM6xBqth: scan_ct_mark_conflicts now joins the nft hits and the iptables-save hits with a newline when both are non-empty. (not posted on the thread: not an open thread on the pull request)

On a native host with the classifier installed, an allowed host-address box's IPv6 connection to a peer is refused. An install over a firewall rule that copies whole ct marks is refused, and the refusal names the rule. In this round, an operator can run min finalize-install --ct-mark-mask <mask>. The boot unit that this command writes passes the mask, so the chosen bits survive a reboot. A re-run of the install step with no mask uses the recorded mask. The mask refusals name min finalize-install --ct-mark-mask <mask>.

Notes from the agent: Revise round 2, C2 only. Layer exception: crates/minimal (cli.rs, cmd/finalize_install.rs, tests.rs) and scripts/install_test.sh, as the owner's /revise note allows. Please add a 'Layer exception' line for crates/minimal to the PR body. Design: the ct-mark-mask-* record is a cgroup under the cgroupfs tree, and a reboot empties it. Because of this, the installer's record read alone cannot fix the boot path. Both halves are in place. (1) The classifier step's block passes --ct-mark-mask 0x........ to the one-time run and to the boot unit's ExecStart. The mask comes from --ct-mark-mask, or else from the loaded table's record, so a refresh keeps an earlier choice. (2) The installer adopts the one recorded mask when it gets no --ct-mark-mask, and it prints a note that names the mask. A --ct-mark-mask that differs from the loaded table's record makes the item missing ('egress enforcement uses the connection-mark bits X, and --ct-mark-mask asks for Y'). Changed refusal texts: 'Choose two contiguous bits outside and run: min finalize-install --ct-mark-mask ', 'Choose two contiguous bits that no rule above uses and run: min finalize-install --ct-mark-mask '. The whole-mark refusal now ends with 'then run min finalize-install again.' The install_test.sh case that expected a default-mask re-run to refuse again now proves the re-run adopts the recorded 0x03000000. An explicit --ct-mark-mask 0x30000000 st…[truncated]

@minimal-foundry
minimal-foundry Bot marked this pull request as draft October 28, 2026 20:29
@norrietaylor
norrietaylor marked this pull request as ready for review October 28, 2026 20:54
@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 ee454d3c. I read the classifier changes against T105's plan block and arch C1–C2 (arch_rulings 505). The install's table now refuses non-loopback IPv6 from the boxes subtree in an ipv6_out chain that counts into the shared refused counter, refuses a whole-mark ct writer and a well-known mark range before the transaction, and the per-launch probe uses UDP with the reject-set rule. C1: the root-lane run on f7cbcee (job 122692831268) passed the three named proofs with the table loaded; its URL is in the Proof section. C2, fixed in-PR as the owner chose: scripts/install-host-classifier.sh re-applies the recorded mask when --ct-mark-mask is absent, the boot unit carries the mask, and min finalize-install --ct-mark-mask <mask> exists so the refusal's remedy runs as written. The out-of-layer files are recorded as a layer exception. No findings.


Generated by Claude Code

Copy link
Copy Markdown
Member

/revise Fix the HIGH security thread 4405414975 on scripts/install-host-classifier.sh ct_mark_conflicts_in (line ~1218). The owner chose to fix it as defence in depth. Change nothing else.

  • Finding: the whole-mark scan treats -j CONNMARK --save-mark with no --mask/--ctmask, or an all-ones one, as whole = 1, but not -j CONNMARK --restore-mark with no --ctmask/--nfmask or an all-ones one.
  • Fix: treat --restore-mark with no mask, or with an all-ones --ctmask/--nfmask/--mask, as a whole-mark writer too (whole = 1), the same way --save-mark is handled. Name it in the refusal like the other hits.
  • Tests: add scripts/install_test.sh cases (and the matching unit twin, if one renders the scan) for --restore-mark with no mask and with --ctmask 0xffffffff, both refused, and one with a narrow mask that is not.
  • Reply on thread 4405414975 with the commit. Keep every commit message line at or under 100 characters.

Generated by Claude Code

… 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.
@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: PASS · the full ruling

This push merges main cleanly and adds one refusal arm to the install's ct-mark scan. A CONNMARK --restore-mark with no mask or an all-ones mask now blocks the install, as --save-mark does. The arm fails closed, and the deny-all verdict stays in the box host (§4.1 line 54). NET-079 still conforms, as it did on ee454d3c. No condition is open.

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

@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 — 2 findings, 2 inline.

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

Comment thread scripts/install-host-classifier.sh Resolved
Comment thread crates/minimald/src/net/classifier.rs Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 3 of 10 on a /revise note, pushed as 16d91608c513.

line exit tail
cargo nextest run -p minimald install_refuses_a_whole_mark_ct_writer 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-7f14... 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 install_refuses_a_well_known_mark_range 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-7c1a... 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 minimald host_ip_box_ipv6_egress_refused 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-3d84... 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 minimald per_launch_probe_returns_without_tcp_retransmit_wait 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-d7f9... 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 minimald host_ip_deny_all_no_outbound 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-8ac4... 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.

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.

_span_id": "3b0a555ec1dcecc5",
                "span_id": "62dbf30029133b22",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "GetDenyAllOptOut",
                "parent_span_id": "3b0a555ec1dcecc5",
                "span_id": "48032078937e5fa8",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "GetPaths",
                "parent_span_id": "3b0a555ec1dcecc5",
                "span_id": "94a781abafdd926b",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "GetRecord",
                "parent_span_id": "3b0a555ec1dcecc5",
                "queued_behind": "0",
                "span_id": "2c8e53e6cabc48f9",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "DeleteSession",
                "parent_span_id": "3b0a555ec1dcecc5",
                "queued_behind": "0",
                "span_id": "d6f86f76b2f9e36c",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "Destroy",
                "parent_span_id": "d6f86f76b2f9e36c",
                "span_id": "4872073620980e13",
                "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 [   2.737s] ( 832/1979) minimald box_actor::tests::a_box_nobody_handed_an_address_publishes_at_the_answerers_grant
        PASS [   2.725s] ( 833/1979) minimald box_actor::tests::a_box_re_registered_inside_the_landing_window_keeps_its_grant
        PASS [   3.125s] ( 834/1979) minimald box_actor::tests::a_destroy_queued_behind_a_waiting_finalize_runs_after_the_promotion
────────────
     Summary [  62.289s] 834/1979 tests run: 833 passed, 1 failed, 23 skipped
        FAIL [   4.396s] ( 831/1979) minimal::otel_cli exec_run_attach_and_task_run_share_the_commands_trace
warning: 1145/1979 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::

  • crates/minimal/src/cli.rs
  • crates/minimal/src/cmd/finalize_install.rs
  • crates/minimal/src/tests.rs
  • scripts/install_test.sh

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

Review threads the coder judged:

  • 👍 valid — PRRT_kwDOPdfcxM6xS_mG: Fixed in 16d9160 as defence in depth. ct_mark_conflicts_in now sets whole = 1 for --restore-mark with no --mask/--ctmask/--nfmask, or an all-ones one. Cases in scripts/install_test.sh and install_refuses_a_whole_mark_ct_writer cover no mask, --ctmask 0xffffffff (both refused) and a narrow mask (not (not posted on the thread: not an open thread on the pull request)

On a native host with the classifier installed, an allowed host-address box's IPv6 connection to a peer is refused. Installing over a firewall rule that copies whole ct marks is refused with the rule named. This now includes a CONNMARK --restore-mark with no mask or an all-ones mask.

Notes from the agent: Revise round 3 changes only the restore-mark scan, as the note asks. scripts/install_test.sh is not in the task's layers. The directive asks for its cases explicitly, so I added them there. Wording: the directive says to name the restore-mark hit like the other hits. So it prints '(writes every bit of the connection mark)'. That is not exact: --restore-mark writes the packet mark from every bit of the connection mark, and does not write the connection mark. A reviewer can change the label for this case if they want it exact. The commit for thread 4405414975 is 16d9160.

@minimal-foundry
minimal-foundry Bot marked this pull request as draft October 29, 2026 01:16
Copy link
Copy Markdown
Member

/revise Two changes the epic owner ruled on at 16d9160. Change nothing else.

  1. Revert the --restore-mark whole-mark clause (MEDIUM thread 4407535596). A CONNMARK --restore-mark copies the connection mark into the packet mark and never writes the connection mark. The classifier reads only the connection mark (crates/minimald/src/net/classifier.rs, the ct mark rules near lines 4098-4137), so such a rule cannot clobber it, and the install should not refuse it.
    • In ct_mark_conflicts_in (scripts/install-host-classifier.sh, about lines 1204-1207 and 1222-1225), remove the --restore-mark block and its comment sentences. Keep the --save-mark handling as it is.
    • Remove the scripts/install_test.sh cases (and any unit twin in classifier.rs) that expect a --restore-mark with no mask or an all-ones mask to be refused. Keep or add one case where a --restore-mark with no mask is not a conflict.
    • Reply on threads 4405414975 and 4407535596 with the commit: the owner ruled the HIGH a false positive because a restore writes only the packet mark.
  2. Bind the probe's datagram endpoints before the TCP drain threads (LOW thread 4407535601). In hold_probe_listeners (crates/minimald/src/net/classifier.rs, about line 2650), bind the datagram sockets before spawning the TCP drain threads, so a failed datagram bind returns before any thread runs and HELD_PROBE_ENDPOINTS is never left unset while TCP listeners serve. Add or extend a unit test if one covers this function. Reply on 4407535601 with the commit.
  • Tests: bash scripts/install_test.sh (or the harness that runs it) and cargo nextest run -p minimald for the touched tests.
  • Keep every commit message line at or under 100 characters.

Generated by Claude Code

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.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 4 of 10 on a /revise note, pushed as 62b1ec1eb700.

line exit tail
cargo nextest run -p minimald install_refuses_a_whole_mark_ct_writer 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-847e... 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 install_refuses_a_well_known_mark_range 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-c8e3... 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 minimald host_ip_box_ipv6_egress_refused 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-8a3f... 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 minimald per_launch_probe_returns_without_tcp_retransmit_wait 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-3141... 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 minimald host_ip_deny_all_no_outbound 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-0efb... 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.

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.

_span_id": "c9af2b8f0bff4644",
                "span_id": "f936f3442b0cfbc3",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "GetDenyAllOptOut",
                "parent_span_id": "c9af2b8f0bff4644",
                "span_id": "a0d5620624e0d565",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "GetPaths",
                "parent_span_id": "c9af2b8f0bff4644",
                "span_id": "8bb68bb27357906f",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "GetRecord",
                "parent_span_id": "c9af2b8f0bff4644",
                "queued_behind": "0",
                "span_id": "93b9ef227c90ba58",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "sessions.manager.message",
            fields: {
                "kind": "DeleteSession",
                "parent_span_id": "c9af2b8f0bff4644",
                "queued_behind": "0",
                "span_id": "c1673282d193a586",
                "trace_id": "0af7651916cd43dd8448eb211c803111",
            },
            parent: None,
            closed: true,
        },
        LoggedSpan {
            name: "session.message",
            fields: {
                "kind": "Destroy",
                "parent_span_id": "c1673282d193a586",
                "span_id": "c9544d393652e4c7",
                "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 [   2.563s] ( 836/1988) minimald box_actor::tests::a_box_nobody_handed_an_address_publishes_at_the_answerers_grant
        PASS [   2.592s] ( 837/1988) minimald box_actor::tests::a_box_re_registered_inside_the_landing_window_keeps_its_grant
        PASS [   2.730s] ( 838/1988) minimald box_actor::tests::a_destroy_queued_behind_a_waiting_finalize_runs_after_the_promotion
────────────
     Summary [  42.231s] 838/1988 tests run: 837 passed, 1 failed, 23 skipped
        FAIL [   3.473s] ( 835/1988) minimal::otel_cli exec_run_attach_and_task_run_share_the_commands_trace
warning: 1150/1988 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::

  • crates/minimal/src/cli.rs
  • crates/minimal/src/cmd/finalize_install.rs
  • crates/minimal/src/tests.rs
  • scripts/install_test.sh

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

Review threads the coder judged:

  • 👍 valid — scripts/install-host-classifier.sh: Reverted the --restore-mark whole-mark clause in 62b1ec1. A restore writes only the packet mark and the classifier reads only the ct mark. The owner ruled the earlier HIGH a false positive. A restore with no mask now installs, and a test pins this.
  • 👍 valid — crates/minimald/src/net/classifier.rs: In 62b1ec1, hold_probe_listeners binds the datagram endpoints before any TCP drain thread spawns. A failed datagram bind now returns before a listener serves.

On a native host with the classifier installed, an allowed host-address box's IPv6 connection to a peer is refused. An install over a firewall rule that copies whole ct marks into the connection mark is refused with the rule named. An install over a CONNMARK --restore-mark with no mask now succeeds, because that rule writes only the packet mark.

Notes from the agent: Revise round 4, commit 62b1ec1. (1) I reverted 16d9160: ct_mark_conflicts_in no longer has the --restore-mark clause, and the --save-mark handling is unchanged. I removed the shell and unit cases that expected a --restore-mark refusal. I added one case in scripts/install_test.sh and one in install_refuses_a_whole_mark_ct_writer: a --restore-mark with no mask installs. Thread 4405414975 was not in this round's unresolved list, so it has no entry here. The owner's ruling for it is the same as for 4407535596: a false positive, because a restore writes only the packet mark. (2) hold_probe_listeners now binds the datagram endpoints before it spawns any TCP drain thread. No unit test can make the datagram bind fail without a fault-injection seam, so I added no new test. The existing guest_effect_probe tests that call the function pass. scripts/install_test.sh is outside the task's layers. The directive asked for the change there, and an earlier round already touched it. Both changes are in one commit.

@norrietaylor
norrietaylor marked this pull request as ready for review October 29, 2026 02:38
@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: PASS · the full ruling

This head reverts the CONNMARK --restore-mark refusal and binds the probe datagram endpoints before any drain thread starts. A restore writes only the packet mark. The table reads only ct mark, so the §4.1 line 54 classification stays safe. The scan still refuses a masked restore that touches the classifier bits. The bind order makes a failed bind return before the daemon serves an endpoint. NET-079 conforms, as on the 16d91608 baseline.

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

@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.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

@minimal-foundry minimal-foundry Bot added the review:approved minimal-review approved the current head label 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 review at 62b1ec1e. I read T105's mark, IPv6 and probe changes against the plan block and arch's rulings on ee454d3c, 16d91608 and this head (arch_rulings: PASS, no condition or residual open).

  • ct-mark scan: whole-mark writers stay refused: an nft ct mark set of a value other than ct mark, a --set-mark/--set-xmark with no mask, and a --save-mark with no mask. A CONNMARK --restore-mark is no conflict again, on the owner's ruling that the HIGH was a false positive: a restore writes only the packet mark, and the classifier reads only ct mark. I checked both in 62b1ec1e and in classifier.rs.
  • Probe listeners: hold_probe_listeners binds the datagram endpoints before any TCP drain thread spawns, so a failed bind returns before an endpoint is served (LOW 4407535601).
  • min finalize-install --ct-mark-mask: the recorded mask is re-applied at boot, and an overlapping mask is refused with the command the user can run. The Layer exception section covers the crates/minimal files.
  • Threads: zero open. I verified each fix in code before resolving it.
  • CI: every lane aggregator is green at this head; the root-lane proof is in the PR body.

No further findings.


Generated by Claude Code

@norrietaylor norrietaylor changed the title T105 — Close the host classifier's mark, IPv6 and probe gaps fix(minimald): close the host classifier's mark, IPv6 and probe gaps (T105) 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 62b1ec1: every merge gate passes at this head (arch PASS at 62b1ec1, zero open threads, every lane aggregator green). Squash title fix(minimald): close the host classifier's mark, IPv6 and probe gaps (T105) 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
Merged via the queue into main with commit 354548e Oct 29, 2026
38 checks passed
@norrietaylor
norrietaylor deleted the agent/2692-close-the-host-classifier-s-mark-ipv6-and-probe branch October 29, 2026 04:00
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] <2817389
BD0C
52+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.

T105 — Close the host classifier's mark, IPv6 and probe gaps

1 participant

0