8000 T148 — A native own-address box reaches a host exposure under the current table by minimal-foundry[bot] · Pull Request #2970 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

T148 — A native own-address box reaches a host exposure under the current table - #2970

Merged
norrietaylor merged 18 commits into
mainfrom
agent/2937-a-native-own-address-box-reaches-a-host-exposure
Nov 5, 2026
Merged

norrietaylor merged 18 commits into
mainfrom
agent/2937-a-native-own-address-box-reaches-a-host-exposure

Conversation

@minimal-foundry
Copy link
Copy Markdown
Contributor

T148 — A native own-address box reaches a host exposure under the current table

slice: S21
kind: task
covers: NET-160, NET-161
size: M
est: 160 code, 200 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: min bug bundles carry the native daemon log, where each refused flow from an own-address box to host.min.internal names the box, the port and whether the table or the box's rules refused it
observability: the native relay's rate-limited refusal line names the rule host-alias and the box; each table reload is logged at debug with its entry count
layers:
- relay:
- crates/client-config/src/host_exposures.rs (new): parses the [host-exposures] table of the client config.toml, each entry a port and a proto that is tcp when the entry names none, and reads it again when the file changes
- crates/client-config/src/lib.rs: exports the table reader
- crates/minimald/src/net/switch.rs: the native relay admits a flow from an own-address box to host.min.internal only at a port the current table opens, and only when the box's egress.allow_dns_hosts names host.min.internal or its allow_subnets covers the alias; a change to the table decides each later flow, and an exposure removed ends the established flows to it; a native host-address box is untouched (it shares the host's namespace, design §7.1 Lanes)
test: cargo nextest run -p minimald native_own_ip_host_exposure_needs_the_box_to_name_the_host
test: cargo nextest run -p minimald native_own_ip_follows_the_current_host_exposures_table
test: cargo nextest run -p minimald native_own_ip_exposure_removal_ends_established_flows
produces: none
consumes: none
proof: On a native Linux host, an own-address box that names host.min.internal reaches a port the user's table opens, reaches it no more once the entry is removed, and a box that does not name the host never reaches it.

Proof

On a native Linux host, an own-address box whose egress.allow_dns_hosts names host.min.internal (or whose allow_subnets covers the alias) reaches host.min.internal at a port the [host-exposures] table of the user's config.toml opens. After the entry is removed from the file, the next flow is refused and an established flow is reset at both ends. A box that does not name the host never reaches the port. Each refusal is a rate-limited log line with rule_matched="host-alias", the box, the port and refused_by ("host-exposures table" or "box egress rules"). The three relay tests in crates/minimald/src/net/switch.rs show this.

Notes from the agent: Outside the layers: crates/minimald/Cargo.toml gains client-config.workspace = true and Cargo.lock gains that edge. minimald did not depend on client-config, and switch.rs needs the table reader. Gap for the next task: client_config::config::Config has #[serde(deny_unknown_fields)], so the existing config reader (and so min) rejects a config.toml that has a [host-exposures] section. The new reader reads only that table and ignores the other sections, so minimald works. The user-facing proof needs a host_exposures field on Config (config.rs, not in this task's layers), most likely as part of the NET-159 task. Design choices: only a local-socket attach (attach_to_switch, the native DM2 path) sets the table on the gate. A vsock attach in a VM guest keeps the old behaviour, because minvmd applies the host's table there. The table is checked by stat (mtime, length, inode) on each frame to or from the alias, and the file is read again only when the stat changes. A table that does not parse is treated as empty (fail closed) with a warn log, and each reload is logged at debug with its entry count. A box's deny_subnets that covers the alias also refuses. allow_protocols is not checked for the alias. An established flow to a removed exposure ends at its next segment in either direction, with a reset to each end. A flow…[truncated]

Tests

line exit tail
cargo nextest run -p minimald native_own_ip_host_exposure_needs_the_box_to_name_the_host 0 Applying loadouts: default Running task runtime-implement-test-0 in 8000 session task-runtime-implement-test-0-1fd3... 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 native_own_ip_follows_the_current_host_exposures_table 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-1acf... 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 native_own_ip_exposure_removal_ends_established_flows 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-34bb... 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]).

Out of scope

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

  • crates/minimald/Cargo.toml

Closes #2937

The native relay admits an own-address box's flow to the host alias only
at a port the client config.toml's [host-exposures] table opens, and only
when the box names host.min.internal in allow_dns_hosts or covers the
alias in allow_subnets. The table is read again when the file changes,
and a removed exposure resets the flows the box opened to it.

client-config gains the table reader, and minimald depends on
client-config for it.
@coderabbitai
coderabbitai Bot commented Nov 4, 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: 4d04abae-40cc-4f1f-8a3a-4f48e81a4a69

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

@minimal-foundry
minimal-foundry Bot commented Nov 4, 2026 •
Copy link
Copy Markdown
Contributor Author

Approved: every review thread resolved at d47e9940f36c.

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

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

Comment thread crates/minimald/src/net/switch.rs Resolved
Comment thread crates/minimald/src/net/switch.rs Resolved
@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: CONDITIONS · 4 conditions · the full ruling

The native relay gates host.min.internal by the current table and the box's half, and the placement is right. Two parts do not conform to §7.1. First, an admitted alias frame skips the box's allow_protocols. So box→host reach is wider than box→box (lines 248–249). Make the change in C1. Second, a removal cannot terminate a flow that the tracker does not hold, and the tracker frees no slot on FIN or RST (line 250). Make the change in C2. Then file the two residual issues in C3 and C4, and name them in the PR body.

Ruled at 02b26da against gominimal/arch@7a86ec7bb3c0 by claude-opus-5-5.

Comment thread crates/minimald/src/net/switch.rs Resolved
Comment thread crates/minimald/src/net/switch.rs Resolved
Comment thread crates/minimald/src/net/switch.rs Resolved
A host-alias admission skipped the box's egress verdict, so a box that
declares TCP alone could reach a UDP exposure. The gate now checks the
box's allow_protocols there too.

The [host-exposures] reader also re-reads a file whose modification time
is too close to the last read to prove it unchanged, so an edit that
keeps the length inside one timestamp tick is not missed.
@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 crates/minimald/src/net/switch.rs Resolved
@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: CONDITIONS · 3 conditions · the full ruling

The native relay applies the table and the box's egress rules correctly, and the placement conforms. One gap remains. After 1024 tracked flows, the gate admits a new flow that it does not track. When its exposure is removed, nothing ends that flow, and the host's segments still reach the box. Section 7.1 line 250 says a removed exposure's established flows are terminated. C1 refuses a flow that the gate cannot track. C2 and C3 file the two gaps that the PR body names without an issue, and list them as residuals.

Ruled at ae6537f against gominimal/arch@7a86ec7bb3c0 by claude-opus-5-5.

Comment thread crates/minimald/src/net/switch.rs Resolved
Comment thread crates/minimald/src/net/switch.rs Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 1 of 10 on the runtime's review findings, pushed as ae6537f2845d.

line exit tail
cargo nextest run -p minimald native_own_ip_host_exposure_needs_the_box_to_name_the_host 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-01ce... 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 native_own_ip_follows_the_current_host_exposures_table 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-63ac... 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 graph v0
cargo nextest run -p minimald native_own_ip_exposure_removal_ends_established_flows 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-92cf... 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.

Review threads the coder judged:

  • 👍 valid — crates/minimald/src/net/switch.rs:988: host_alias_egress now admits only over a protocol the box's allow_protocols declares (new field host_protocols). A TCP-only box is refused at a UDP exposure, and the test pins it. The reply_admits_frame half is unchanged: that path skipped egress::verdict before this PR too (NET-040).
  • 👎 invalid — PRRT_kwDOPdfcxM6zaQz5: The default is deliberate. The spec note on NET-160 says allow_subnets coverage of the alias stays the fall-through, so an allow-all box reaches an exposure as it reaches a sibling box. A box with no allow_subnets list is allow-all, so it covers the alias. The user's table still has to open the port (not posted on the thread: not an open thread on the pull request)
  • 👍 valid — PRRT_kwDOPdfcxM6zaQz9: HostExposuresFile::current now re-reads the file when its mtime is within 2 s of the last read, and compares the text. A new unit test edits 8080 to 8081 with the same length, inode and mtime, and checks that 8080 closes. (not posted on the thread: not an open thread on the pull request)

On a native Linux host, an own-address box that names host.min.internal reaches a port the user's [host-exposures] table opens, over a protocol its allow_protocols declares. It reaches the port no more once the entry is removed, also when the edit keeps the file length inside one timestamp tick. A box that does not name the host never reaches it.

Notes from the agent: Revise round 1. The host-alias admission now checks the box's allow_protocols. The table reader re-reads a file whose mtime is within 2 s of the last read and compares the text. I added a test case to native_own_ip_host_exposure_needs_the_box_to_name_the_host and a unit test an_edit_that_keeps_the_stamp_is_read_again in host_exposures.rs. The directive names 4 findings but lists 3 threads. I judged the 3 threads. I did not run just clippy-strict.

@norrietaylor
Copy link
Copy Markdown
Member

/revise Deliver arch C1 in crates/minimald/src/net/switch.rs. For arch C2 and C3, add a Residuals heading to the PR body and change no other file. C2: #2871 (T102) adds the [host-exposures] field to Config, so name it. C3: #2981 (T153) owns the Minimal-listener exclusion on the native relay, so name it.

An untracked flow gets no reset when its exposure is removed, so the
gate now refuses it. record_host_flow returns whether it tracked the
flow, and the refusal log names the flow limit.
@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 crates/minimald/src/net/switch.rs Resolved
@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: CONDITIONS · 2 conditions · the full ruling

The native gate follows the current table, and the compare refuses a flow that it cannot track. Two gaps remain. Section 7.1 line 252 says Minimal's own listeners are never exposures, decided at each flow. But host_alias_egress() admits any port that the table names, and the alias maps to 127.0.0.1, where the hostname proxy and the box-zone answerer listen. C1 fixes this. On a carved switch, a host segment from the documented literal skips the removal check, against line 250. C2 fixes this.

Ruled at b3404e9 against gominimal/arch@7a86ec7bb3c0 by claude-opus-5-5.

Comment thread crates/minimald/src/net/switch.rs Resolved
Comment thread crates/minimald/src/net/switch.rs Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

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

line exit tail
cargo nextest run -p minimald native_own_ip_host_exposure_needs_the_box_to_name_the_host 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-e105... 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 native_own_ip_follows_the_current_host_exposures_table 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-bee5... 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 native_own_ip_exposure_removal_ends_established_flows 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-11e5... 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.

Review threads the coder judged:

  • 👎 invalid — crates/minimald/src/net/switch.rs: This is already fixed on this branch. Commit ae6537f added the declared check: host_alias_egress refuses the frame when the box declares allow_protocols and the list does not hold the frame's protocol.
  • 👍 valid — crates/minimald/src/net/switch.rs:1952: record_host_flow now returns false past the bound, and host_alias_egress refuses that opening frame. Records still expire by idle age, not on FIN or RST.
  • 👍 valid — crates/minimald/src/net/switch.rs:456: No code change here. The PR body names T153 — A native own-address box never reaches Minimal's own listener through a host exposure #2981 (T153) under Residuals. That issue owns the Minimal-listener exclusion on the native relay.
  • 👍 valid — crates/client-config/src/host_exposures.rs:122: No code change here. The PR body names T102 — Flows to host.min.internal are refused outside the design's openings #2871 (T102) under Residuals. That issue adds the [host-exposures] field to Config.
  • 👎 invalid — crates/minimald/src/net/switch.rs:1648: This is deliberate. The NET-005 rationale in the spec says an exposure is local reach under the box's egress rules, so an allow-all box reaches only the exposures. An undeclared allow_subnets is allow-all, so it covers the alias. deny_subnets that cover the alias still refuse it.
  • 👍 valid — crates/minimald/src/net/switch.rs: record_host_flow now returns bool, and host_alias_egress refuses the frame when the flow is not recorded. The removal test checks this at the bound.
  • 👍 valid — crates/minimald/src/net/switch.rs: The HOST_FLOWS_PER_BOX comment now says a new flow past the bound is refused.
  • 👍 valid — crates/client-config/src/host_exposures.rs:122: No code change here. The PR body lists T102 — Flows to host.min.internal are refused outside the design's openings #2871 (T102) under Residuals as the issue for the Config field.
  • 👍 valid — crates/client-config/src/host_exposures.rs:144: No code change here. The PR body lists T153 — A native own-address box never reaches Minimal's own listener through a host exposure #2981 (T153) under Residuals for the Minimal-listener exclusion (NET-162) on the native relay.

On a native Linux host, an own-address box that names host.min.internal reaches a port the user's [host-exposures] table opens, and reaches it no more once the entry is removed. A box that does not name the host never reaches it. Each flow the gate admits is now one it tracks, so a removal ends every admitted flow.

Notes from the agent: ## Residuals

This round: record_host_flow returns whether it tracked the flow. host_alias_egress refuses a new flow past HOST_FLOWS_PER_BOX, and the rate-limited refusal line says refused_by = "host flow limit". The removal test now fills the bound and checks that a tracked flow still passes and a new SYN is refused. Records are still dropped by idle age when the bound is reached, not on FIN or RST. That part of thread PRRT_kwDOPdfcxM6zaTio is not done this round. cargo clippy -p minimald --all-targets is clean. just clippy-strict was not run.

@norrietaylor
Copy link
Copy Markdown
Member

/revise Deliver arch C1 and C2 in this PR. T153 (#2981) is folded into T148, so the Minimal-listener exclusion is yours, and #2937 now covers it with its two new test lines. Remove #2981 from the Residuals heading. #2871 (T102) lands first. After it merges, read the table through one shared reader in client-config: crates/minvmd/src/net/host_exposures.rs is now in this task's layers.

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

That /revise was refused. T148 is blocked by still-open #2689. The plan's order is the point of having one..

Round 3 of 10 was not counted; #2937 carries the same sentence.

@norrietaylor
Copy link
Copy Markdown
Member

/revise T102 (#2871) has merged, so this task is unblocked. Merge origin/main. Deliver arch C1 and C2 at this head in crates/minimald/src/net/switch.rs, using the test lines in #2937. T153 is folded into this task, so remove #2981 from the Residuals heading. Read the table through one shared reader in client-config: crates/minvmd/src/net/host_exposures.rs is in this task's layers, and a bad entry is handled one way for both daemons. An allow-all box reaches an exposure as it reaches a sibling, as for_box does in crates/minvmd/src/net/egress_gate.rs.

minvmd now reads the [host-exposures] table through the shared reader in
client-config, so a bad entry is skipped and logged the same way on both
daemons. A broken file keeps its text, so the same text is not parsed or
logged again for each frame.
On a native own-address box the relay refuses a flow to host.min.internal
at a port that the box-name answerer or the hostname proxy holds, ahead of
the table's openings, and names the listener in the refusal. A listener
held later ends the established flows to it. On a carved switch a host
segment from the documented literal of a removed exposure is refused too.
@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 crates/minimald/src/net/switch.rs Resolved
@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: CONDITIONS · 3 conditions · the full ruling

The shared reader and the native table gate conform to §7.1 lines 248-251. The new listener check on the native relay does not fully meet line 252. That line says Minimal's own listeners are never exposures, and it decides this at each flow, also for a listener that binds later. The gate builds its listener list once. The list holds the answerer at a constant port and the hostname proxy only if it served at attach time. It does not hold the published forwarders on 127.0.0.1. Make the changes in C1, C2 and C3.

Ruled at 580abde against gominimal/arch@7a86ec7bb3c0 by claude-opus-5-5.

Comment thread crates/minimald/src/net/switch.rs Resolved
Comment thread crates/minimald/src/net/switch.rs Resolved
Comment thread crates/minimald/src/net/switch.rs Resolved
Comment thread crates/minimald/src/net/switch.rs Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 3 of 10 on a /revise note, pushed as 580abde5bd3f.

line exit tail
cargo nextest run -p minimald native_own_ip_host_exposure_needs_the_box_to_name_the_host 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-9dba... 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 native_own_ip_follows_the_current_host_exposures_table 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-3d48... 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 native_own_ip_exposure_removal_ends_established_flows 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-0cc5... 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 native_own_ip_exposure_on_minimal_listener_refused 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-82a6... 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 native_own_ip_exposure_shadowed_by_a_later_minimal_listener 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-ac7f... 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::

  • crates/minvmd/Cargo.toml

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

Review threads the coder judged:

  • 👎 invalid — PRRT_kwDOPdfcxM6zblQD: This is deliberate. NET-160 says that allow_subnets coverage of the alias stays the fall-through, so an allow-all box reaches an exposure as it reaches a sibling box. That is what for_box does in minvmd's egress_gate.rs. A box with no egress policy still needs the user's table to open the port. (not posted on the thread: not an open thread on the pull request)
  • 👍 valid — PRRT_kwDOPdfcxM6zblQM: HostExposuresFile now stores the body it read (missing, unread, or text) even when the parse fails. The same broken text keeps its table and logs nothing more. a_broken_file_is_parsed_once_for_each_version pins this. (not posted on the thread: not an open thread on the pull request)
  • 👍 valid — crates/minimald/src/net/switch.rs: host_alias_egress now refuses a port that the box-name answerer or the hostname proxy holds before it checks the table's openings, and names the listener in refused_by. A later listener (hold_minimal_listener) ends the established flows to its port. Tests: native_own_ip_exposure_on_minimal_listener_
  • 👍 valid — crates/minimald/src/net/switch.rs: host_alias_ingress now matches the alias and the documented literal that the switch maps (is_host_alias). A literal segment of a removed exposure is refused. Test: native_own_ip_exposure_removal_ends_established_flows_from_the_literal.

On a native Linux host, an own-address box that names host.min.internal reaches a port that the user's [host-exposures] table opens. After the entry is removed, the box reaches it no more, and its established flows end, also on a carved switch where the host answers from the documented literal. A box that does not name the host never reaches the port. An entry at a port that the box-name answerer or the hostname proxy holds opens nothing, and the refusal and the daemon log name that listener.

Notes from the agent: The origin/main merge (T102) was already on the branch at the start of this round. C1: the native gate starts with the box-name answerer's port and the hostname proxy port that the box's rules are compiled with. Each flow checks these ports before the table's openings. BoxGate::hold_minimal_listener is the hook for a listener that binds later. No production code calls it yet, because the hostname proxy's later re-bind lives in crates/minimald/src/net/mod.rs and proxy.rs, which are outside the layers. The shadowed test drives the hook directly. The native daemon also has no load-time refusal with a log line. It logs once at the first flow that finds the shadowed entry. C2: the ingress filter matches the alias and the documented literal that the switch maps (documented_host_literal). Shared reader: minvmd now reads through client_config::host_exposures::HostExposuresFile. Both daemons skip a bad entry, log it and keep the other entries. A file that does not parse, or a host-exposures key that is not a table, opens no exposure. The client-config reader formerly refused the whole table for one bad entry, and that changed. Outside the layers, I added client-config as a dependency in crates/minvmd/Cargo.toml so that minvmd can use the shared reader. The PR body is written by the runner, so I did not edit it. Remove #2981 from its Residuals heading, as the /revise note asks. Extra commands I ran: cargo test -p client-config host_exposures (6 passed),…[truncated]

@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: CONDITIONS · 2 conditions · the full ruling

The forward hold in policy.rs conforms to §7.1 line 252. The new names_host_alias() arm refuses an allow-all box at host.min.internal. Under the in-force default the gate never sees an absent allow_subnets. It sees one only for an allow-all box behind the NET-077 opt-out, and that box reaches siblings. Line 251 says an allow-all box reaches exposures as it reaches siblings. Restore is_none_or (C1) and change the test so it proves line 251 (C2).

Ruled at c061bc6 against gominimal/arch@7a86ec7bb3c0 by claude-opus-5-5.

Comment thread crates/minimald/src/net/switch.rs Resolved
Comment thread crates/minimald/src/net/switch.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.

minimal-review — advisory review

Verdict COMMENT — 2 findings, 2 inline.

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

… once

The host exposures reader now compares the change time and the device
too, so a restored modification time does not hide an edit. A
modification time after the read stays racy for one confirming read
only, so a lookup does not read the whole file each time.
A compiled allow_subnets of None is the allow-all dimension, so it
covers the host alias, as the VM host's gate decides. The test now
asserts that a box with no egress rules, or no allow_subnets, reaches
the exposure, and that a deny-all box does not.
@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 crates/minimald/src/net/switch.rs Resolved
@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 design §7.1 for NET-160 and NET-161. Commit e409c14b makes an allow-all box reach an exposure, as line 251 says. Commit 12b637d7 reads the table again after any edit, so each flow uses the current table (line 250). The gate decides Minimal's own listeners first and fails closed. A native host-address box is not affected. No conditions are open.

Ruled at e409c14 against gominimal/arch@7a86ec7bb3c0 by claude-opus-5-5.

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 7 of 10 on the runtime's review findings, pushed as e409c14bad12.

line exit tail
cargo nextest run -p minimald native_own_ip_host_exposure_needs_the_box_to_name_the_host 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-f5ff... 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 native_own_ip_follows_the_current_host_exposures_table 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-a8e4... 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 native_own_ip_exposure_removal_ends_established_flows 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-86e6... 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 native_own_ip_exposure_on_minimal_listener_refused 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-97e2... 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 native_own_ip_exposure_shadowed_by_a_later_minimal_listener 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-0f49... 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 native_own_ip_runtime_published_box_port_held_until_it_unbinds 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-480f... 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::

  • crates/minimald/src/server.rs

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

Review threads the coder judged:

  • 👍 valid — crates/minimald/src/net/switch.rs: names_host_alias now uses is_none_or on the compiled allow_subnets, so an allow-all box covers the alias, as the minvmd gate does. The comment now says why.
  • 👍 valid — crates/minimald/src/net/switch.rs: The test now asserts that egress: None and allow_subnets = None reach the exposure, and a new EgressPolicy::deny_all() case asserts refusal.
  • 👍 valid — PRRT_kwDOPdfcxM6z0X3o: A modification time after the read is now racy only until one re-read finds the same stamp and text. Then the stamp is proof. New test a_future_mtime_is_proof_after_one_confirming_read. (not posted on the thread: not an open thread on the pull request)
  • 👍 valid — PRRT_kwDOPdfcxM6z0X3s: The stamp now holds ctime, ctime_nsec and the device beside mtime, length and inode. New test an_edit_that_restores_an_old_mtime_is_read_again. (not posted on the thread: not an open thread on the pull request)

On a native Linux host, an own-address box that names host.min.internal, or that is allow-all, reaches a port the user's [host-exposures] table opens. It reaches the port no more once the entry is removed. A box whose rules do not cover the host, a deny-all box among them, never reaches it. An entry at a port that one of Minimal's own listeners holds opens nothing.

Notes from the agent: Revise round 7. I ran the six test lines as one nextest invocation with the shared prefix filter native_own_ip_, because each line is a prefix of that filter. All passed. cargo clippy -p minimald -p client-config --all-targets and cargo fmt --check were clean. I did not run just clippy-strict.

@minimal-foundry
minimal-foundry Bot marked this pull request as draft November 4, 2026 22:19
@minimal-foundry minimal-foundry Bot added the needs-human An agent handed off; a human must act, then clear this label. Set by defer and handoffs. label Nov 4, 2026
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

The runtime will not revise this pull request again on its own: the diff is +1954/-157 (2111 lines), past 3x the plan's estimate of 700 lines (est: 300 code, 400 tests, as #2937 reads now), at round 7 (the last trigger was the runtime's review findings). A person decides now. Remove needs-human from this pull request to let /revise <note> or the automatic rounds resume, or finish #2937 by hand.

@norrietaylor
norrietaylor marked this pull request as ready for review November 4, 2026 22:29
@norrietaylor
Copy link
Copy Markdown
Member

/revise One change, in crates/minimald/src/net/switch.rs. The minimal-review thread on record_host_flow holds: a closed flow stays in host_flows until the idle sweep, so 1024 short connections in 5 minutes refuse every new flow with host flow limit, although the table opens the port. Remove the entry when a FIN or RST is seen in either direction. Add a test that opens and closes more than HOST_FLOWS_PER_BOX flows and shows the next flow is admitted. Change nothing else: arch passes at this head.

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

That /revise is held, not dropped. The container platform had no instance to give the coder: its pool of prepared containers was full, which happens most just after a deploy. Your note is kept and asked again every 10 minutes for up to 30; a wait that outlives that is refused here with the reason.

What the platform said

could not start the coder: Error: there is no container instance that can be provided to this durable object

Nothing to type again: the command gets a rocket when its round starts, or a refusal that names the reason. No round was counted for it yet.

…wn-address-box-reaches-a-host-exposure

# Conflicts:
#	crates/minimald/src/net/switch.rs
A flow to a host exposure stayed tracked until the idle sweep, so 1024
short connections in five minutes refused every new flow with the flow
limit, although the table opened the port. A reset either way now
removes the flow. A FIN either way marks it closing: the last ACK still
finds it, and the sweep at the bound drops closing flows first.
@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 compare from the PASS on e409c14b conforms. The merge with #3002 keeps the native table gate on the local-socket attach and #3002's rules on the vsock attach, so the paths do not overlap. Commit 14f7dd6e frees the place of a closed flow under the per-box bound. A closing flow stays tracked, so a table removal still ends it, as section 7.1 line 250 requires. There are no conditions.

Ruled at d47e994 against gominimal/arch@7a86ec7bb3c0 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 — approval withheld

Verdict COMMENT — APPROVE withheld: 19 unresolved threads — minimal-arch[bot] crates/minimald/src/net/switch.rs, minimal-arch[bot] crates/minimald/src/net/switch.rs:2189, minimal-arch[bot] crates/minimald/src/net/switch.rs:456, minimal-arch[bot] crates/client-config/src/host_exposures.rs, minimal-arch[bot] crates/minimald/src/net/switch.rs, …. No findings.

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

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 8 of 10 on a /revise note, pushed as d47e9940f36c.

Resolved conflicts in: crates/minimald/src/net/switch.rs

line exit tail
cargo nextest run -p minimald native_own_ip_host_exposure_needs_the_box_to_name_the_host 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-9b82... 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 native_own_ip_follows_the_current_host_exposures_table 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-60b6... 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 native_own_ip_exposure_removal_ends_established_flows 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-59cc... 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 native_own_ip_exposure_on_minimal_listener_refused 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-06f6... 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 native_own_ip_exposure_shadowed_by_a_later_minimal_listener 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-d7ac... 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 native_own_ip_runtime_published_box_port_held_until_it_unbinds 0 Applying loadouts: default Running task runtime-implement-test-5 in session task-runtime-implement-test-5-fbf0... 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::

  • crates/minimald/src/server.rs

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

Review threads the coder judged:

  • 👍 valid — crates/minimald/src/net/switch.rs: A reset either way now removes the flow, and a FIN marks it closing. The sweep at the bound drops closing flows first, so short connections no longer fill it. Test: native_own_ip_closed_host_flows_do_not_fill_the_flow_bound.

On a native Linux host, an own-address box that names host.min.internal reaches a port the user's table opens. It reaches it no more once the entry is removed. A box that does not name the host never reaches it, and an entry at a port that one of Minimal's own listeners holds opens nothing. A box that opens and closes many short connections to an exposed port still opens the next one.

Notes from the agent: Merge: switch.rs conflicted with origin/main (T154, the VM-host relay). I kept both sides. The native gate decides the alias when a table is set. Otherwise the egress verdict now runs on main's relay_egress(), so the VM path is unchanged. main's duplicate HOST_ALIAS_RULE const and its [u8; 4] host_alias field were removed in favour of the branch's const and its Ipv4Addr field. drop_rule compares against host_alias.octets(). In the constructor, the names_host() call comes before the local names_host binding, so the local does not shadow the function. Fix: the directive asked for removal on a FIN or RST. A reset removes the entry. A FIN marks the flow closing and does not remove it, because the box's last ACK after the FINs goes through record_host_flow and would add the flow back. Removal on FIN would leak the same entry again. A closing flow stays tracked, so a removal still ends a half-closed flow. The sweep at HOST_FLOWS_PER_BOX drops every closing flow first. A bare SYN on the same ports clears the mark.

@minimal-foundry
minimal-foundry Bot marked this pull request as draft November 4, 2026 23:26
@norrietaylor norrietaylor removed the needs-human An agent handed off; a human must act, then clear this label. Set by defer and handoffs. label Nov 5, 2026
@norrietaylor
norrietaylor marked this pull request as ready for review November 5, 2026 00:05
@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 d47e9940f36c withheld this approval on the threads open then. Posted as a comment: the App cannot approve its own pull request; the check and the label carry it.

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

Read at d47e994. The listener registry, the hold on every forward, the allow_protocols check and the flow removal on FIN or RST are all in. An allow-all box reaches an exposure as arch ruled. Arch passes.

@minimal-foundry minimal-foundry Bot added the review:approved minimal-review approved the current head label Nov 5, 2026
@norrietaylor
norrietaylor added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit e00e7ab Nov 5, 2026
36 checks passed
@norrietaylor
norrietaylor deleted the agent/2937-a-native-own-address-box-reaches-a-host-exposure branch November 5, 2026 00:42
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.

T148 — A native own-address box reaches a host exposure under the current table

1 participant

0