8000 fix(minimald): proxy runtime-published ports like a direct connection (0.7.0 backport) by norrietaylor · Pull Request #2724 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content
{"props":{"contextRegion":{"crumbs":[{"crumb_type":"organization","label":"gominimal","is_root":false,"href":"/gominimal"},{"crumb_type":"repository","label":"minimal","is_root":false,"href":"/gominimal/minimal"}],"localNavigation":[{"id":"code","icon":"code","label":"Code","href":"/gominimal/minimal","selectedLinks":["repo_source","repo_downloads","repo_commits","repo_releases","repo_tags","repo_branches","repo_packages","repo_deployments","repo_attestations"],"popoverTarget":false,"commandId":"repositories:go-to-code","reactNav":{"appTarget":"code-view","anchor":"code-view-repo-link"},"turboNav":{"frame":"repo-content-turbo-frame"}},{"id":"issues","icon":"issue-opened","label":"Issues","href":"/gominimal/minimal/issues","selectedLinks":["repo_issues","repo_labels","repo_milestones"],"count":240,"popoverTarget":false,"commandId":"repositories:go-to-issues","reactNav":{"appTarget":"repo","anchor":null},"turboNav":{"frame":"repo-content-turbo-frame"}},{"id":"pull-requests","icon":"git-pull-request","label":"Pull requests","href":"/gominimal/minimal/pulls","selectedLinks":["repo_pulls","checks"],"count":31,"popoverTarget":false,"commandId":"repositories:go-to-pull-requests","reactNav":{"appTarget":null,"anchor":null},"turboNav":{"frame":"repo-content-turbo-frame"}},{"id":"discussions","icon":"comment-discussion","label":"Discussions","href":"/gominimal/minimal/discussions","selectedLinks":["repo_discussions"],"popoverTarget":false,"commandId":"repositories:go-to-discussions","reactNav":{"appTarget":null,"anchor":null},"turboNav":{"frame":"repo-content-turbo-frame"}},{"id":"actions","icon":"play","label":"Actions","href":"/gominimal/minimal/actions","selectedLinks":["repo_actions"],"popoverTarget":false,"commandId":"repositories:go-to-actions","reactNav":{"appTarget":"repo","anchor":null},"turboNav":{"frame":"repo-content-turbo-frame"}},{"id":"security-and-quality","icon":"shield","label":"Security and quality","href":"/gominimal/minimal/security","selectedLinks":["security","overview","alerts","policy","token_scanning","code_scanning"],"count":0,"popoverTarget":false,"commandId":"repositories:go-to-security","reactNav":{"appTarget":null,"anchor":null},"turboNav":{"frame":"repo-content-turbo-frame"}},{"id":"insights","icon":"graph","label":"Insights","href":"/gominimal/minimal/pulse","selectedLinks":["repo_graphs","repo_contributors","dependency_graph","dependabot_updates","pulse","people","community"],"popoverTarget":false,"commandId":"repositories:go-to-insights","reactNav":{"appTarget":null,"anchor":null},"turboNav":{"frame":"repo-content-turbo-frame"}}],"localNavigationUpdateChannel":null,"selectedLink":"repo_pulls"},"owner":null,"headerLogo":{"href":"/","aria-label":"Homepage "},"notifications":{"indicatorMode":"disabled","websocketChannel":null,"fetchIndicatorSrc":"/notifications/indicator","fetchIndicatorEnabled":false},"issues":{"href":"/issues"},"pulls":{"href":"/pulls"},"contributedRepos":{"href":"/repos"},"copilot":{"show":false,"showAgentsButton":false,"showRelaunchAnnouncement":false,"copilotChatUrl":null,"copilotApiUrl":"https://api.githubcopilot.com"},"search":{"show":true,"showCommandPalette":false,"isSearchPage":false,"isJumpToSearch":false,"searchContext":{"scope":"repo:gominimal/minimal","current_repo_name":"minimal","current_repo_nwo":"gominimal/minimal","current_repo_org":"gominimal","user_id":"gominimal"}},"commandPalette":null,"enterpriseBar":{"show":false},"globalTransactionalMessage":[],"payloadsUrl":"/_global-navigation/payloads.json?can_toggle_site_admin_and_employee_status=0\u0026is_admin_mode_on=0\u0026is_ui_opted_out=0\u0026show_ui_opt_out=0\u0026v=8","contextRegionUrl":"/_global-navigation/context-region.json"}}

fix(minimald): proxy runtime-published ports like a direct connection (0.7.0 backport) - #2724

Closed
norrietaylor wants to merge 9 commits into
release/0.7.xfrom
fix/net-proxy-runtime-ports-0.7.0
Closed

norrietaylor wants to merge 9 commits into
release/0.7.xfrom
fix/net-proxy-runtime-ports-0.7.0

Conversation

@norrietaylor
@norrietaylor norrietaylor commented Oct 28, 2026 •
Copy link
Copy Markdown
Member

Backports #2714 (task T99, #2688) to release/0.7.0. Through the hostname proxy, a port published inside a box at runtime (min net expose or listen-publish) now opens as it does on a direct connection. The proxy's verdict equals the direct connection's in every network mode.

Stacked on #2678; merge that first. #2678 carries the 0.7.3 version bump the release branch's version check needs. Until it merges, this diff also shows #2678's commits.

Commits

  • f614510c6 is the cherry-pick, with conflicts resolved to main's text. It does not compile alone. Squash-merge the pair.
  • a69ee0758 adapts it to the release branch:
    • BoxGate, BoxPolicy and boxes::DynamicIngress become SessionGate, sessions::SessionPolicy and sessions::DynamicIngress.
    • crate::traced::spawn becomes tokio::spawn, since traced is not on this branch.
    • The release branch's spawn_backend_on takes a full SocketAddr, so the new test picks its port with a probe listener.
    • The parity proptest checks against the gate that carries the runtime publishes, using admits_tcp.

All of #2714's tests are kept.

Verification (Linux, Lima)

  • cargo clippy -p minimald --all-targets --locked -- -D warnings is clean.
  • just test-one proxy_ -p minimald: 43 passed. The runtime-publish, expose and parity tests also pass.
  • The whole minimald suite: 997 passed, 1 failed. The failure is session_host::tests::a_hook_launch_does_not_advise_unenforced_placement, which fails identically on the unmodified fix(net): backport the deny-all DNS refusal and the owner-only answerer socket to 0.7.0 #2678 head in the same VM: the VM's cgroup tree refuses placement with EACCES. It is not caused by this change.
  • just fmt-check passes.

gominimal-aw-bot Bot and others added 4 commits October 27, 2026 20:37
* fix(minimal,minvmd): hold the answerer channel socket owner-only

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: gominimal-aw-bot[bot] <281738952+gominimal-aw-bot[bot]@users.noreply.github.com>
(cherry picked from commit 746770d)
…er (#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 
8000
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)
crate::traced landed on main after the release branch was cut, so the
cherry-pick of #2300 did not compile here.
v0.7.2 is tagged at the branch head, so the version check requires the
next patch release.
@norrietaylor
norrietaylor requested a review from a team as a code owner October 28, 2026 09:20
@coderabbitai
coderabbitai Bot commented Oct 28, 2026 •
Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. 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: Essentials
  • Run ID: 4df10bca-9ca3-403e-be79-eb3841db4f46

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

Reviewed: no findings; approval withheld, 5 threads unresolved at 0a9cefc18e9d.

APPROVE withheld: 5 unresolved threads — 0chroma crates/minimal/src/cmd/finalize_install.rs:1814, 0chroma crates/minimal/src/resolver.rs:397, 0chroma crates/minimal/src/resolver.rs:1344, 0chroma crates/minimal/src/resolver.rs:1875, 0chroma crates/minimald/src/net/dns.rs:224. Resolving the last thread releases the approval on this head.

Resolve the open threads. Resolving the last one releases the approval on this head; no new review is required.

Clobber: none seen.

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

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
],
install: Some(AnswererInstall {
operator: "op".to_string(),
operator_uid: 1000,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm noticing lots of hard-coded 1000s, I think it's better to have this as a const

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Declining this one here. This PR is a cherry-pick of #2714 onto release/0.7.0, and a backport carries the change exactly as it landed on main, so the release branch doesn't drift from main. If you think the change is worth making, please raise it against main. It reaches the release branch only if it is itself backported.

\t\t\t<string>Unix</string>
\t\t\t<key>SockPathName</key>
\t\t\t<string>__CHANNEL__</string>
\t\t\t<key>SockPathOwner</key>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I know this was already here, but I feel like having this inline is so cursed. Can use the inline_str! macro so this can be a separate file, or maybe there's a templating crate we can pull in if there's a good one

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Declining this one here. This PR is a cherry-pick of #2714 onto release/0.7.0, and a backport carries the change exactly as it landed on main, so the release branch doesn't drift from main. If you think the change is worth making, please raise it against main. It reaches the release branch only if it is itself backported.

/// What a channel socket another operator's account owns means: the
/// host's DNS service is that account's. The same sentence the daemons
/// say when the socket refuses their connect.
const FOREIGN_OPERATOR_SERVICE: &str = "the host's DNS service belongs to another account; \
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

seems like this is only used in a function and a test case, not sure if this needs to be a constant. If you feel it should, we should probably have all the strings as consts in a module for consistency

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Declining this one here. This PR is a cherry-pick of #2714 onto release/0.7.0, and a backport carries the change exactly as it landed on main, so the release branch doesn't drift from main. If you think the change is worth making, please raise it against main. It reaches the release branch only if it is itself backported.

Comment on lines +1873 to +1875
if matches!(step, AnswererStep::SourceUnavailable { .. }) {
return step;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this feels a bit silly, better to just make this a case statement and encorporate the implicit return in with _ => answerer_step_with_source(...) than to do an early return

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Declining this one here. This PR is a cherry-pick of #2714 onto release/0.7.0, and a backport carries the change exactly as it landed on main, so the release branch doesn't drift from main. If you think the change is worth making, please raise it against main. It reaches the release branch only if it is itself backported.

impl RouteSource {
/// The value the proxy's log lines carry in their `route_source` field.
#[must_use]
pub fn as_str(self) -> &'static str {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

better to just impl the display trait here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Declining this one here. This PR is a cherry-pick of #2714 onto release/0.7.0, and a backport carries the change exactly as it landed on main, so the release branch doesn't drift from main. If you think the change is worth making, please raise it against main. It reaches the release branch only if it is itself backported.

norrietaylor and others added 3 commits October 28, 2026 12:49
… (T99) (#2714)

* feat(minimald): forward runtime-published ports through the proxy

The hostname proxy now reads the target box's runtime-published ports
(an allowed expose, a published listener) from its live relay gate when
the name resolves. The proxy verdict then admits exactly the ports a
direct connection reaches. Admitted requests log their route source, and
refusals record route_source=none.

* test(minimald): prove an allowed expose opens through the proxy

expose_allow_publishes_and_lists now registers the box's relay gate and
lease, then shows the port refused through the proxy before the expose
and reachable through it right after.

* style(minimald): format the proxy and expose tests

* fix(minimald): log admitted proxy requests at info

The daemon's default filter is info, so a debug line never reached the
log a min bug bundle carries.

---------

Co-authored-by: minimal-foundry[bot] <327719912+minimal-foundry[bot]@users.noreply.github.com>
(cherry picked from commit 7097b96)
The cherry-picked change names main's types and helpers. Release/0.7.0
predates the box renames and the traced spawn wrapper, so its tests and
one doc link use the release's own names:

- BoxGate is SessionGate, BoxPolicy is SessionPolicy, and
  boxes::DynamicIngress is sessions::DynamicIngress.
- crate::traced does not exist here, so the new tests spawn with
  tokio::spawn, as the release's other proxy tests do.
- The release's spawn_backend_on takes a full address and returns
  nothing, so proxy_forwards_runtime_published_port picks the backend
  port with a probe listener, the pattern the release's tests use.
- The proptest's no-wider-reach check uses the outer gate, which now
  carries the runtime publishes, and asks admits_tcp. On this branch
  admits_direct_tcp covers the declared ports only, while admits_tcp
  also covers the listen-published and exposed ports. That is the set
  main's admits_tcp covers.

The behaviour is #2714's: runtime-published ports join the proxy's
route set, and the proxy's verdict equals the direct connection's.
@norrietaylor
norrietaylor force-pushed the fix/net-proxy-runtime-ports-0.7.0 branch from f15bf61 to a69ee07 Compare October 28, 2026 19:50
@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor

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: 5 unresolved threads — 0chroma crates/minimal/src/cmd/finalize_install.rs:1750, 0chroma crates/minimal/src/resolver.rs:397, 0chroma crates/minimal/src/resolver.rs:1344, 0chroma crates/minimal/src/resolver.rs:1875, 0chroma crates/minimald/src/net/dns.rs:224. No findings.

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

@minimal-foundry minimal-foundry Bot removed the review:approved minimal-review approved the current head label Oct 28, 2026
… a direct connection

NET-069 covers ports a box publishes while it runs. The new test publishes a
listener's port through the listen watcher's step (the gate's admission), sees
it forwarded, withdraws it, and checks that the proxied request then meets the
direct connection's refusal: the same verdict and the same logged rule as a
port the box never published.

Adapted from main to release/0.7.0: SessionGate and SessionPolicy for BoxGate
and BoxPolicy, tokio::spawn for crate::traced::spawn, a probe listener for the
backend port because this branch's spawn_backend_on takes a full address, and
no registry dynamic hold, which this branch's listen watcher does not record.
@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor

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: 5 unresolved threads — 0chroma crates/minimal/src/cmd/finalize_install.rs:1750, 0chroma crates/minimal/src/resolver.rs:397, 0chroma crates/minimal/src/resolver.rs:1344, 0chroma crates/minimal/src/resolver.rs:1875, 0chroma crates/minimald/src/net/dns.rs:224. No findings.

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

…y-runtime-ports-0.7.0

# Conflicts:
#	scripts/install-host-classifier.sh
@norrietaylor norrietaylor changed the title feat(minimald): proxy runtime-published ports like a direct connection (0.7.0 backport) fix(minimald): proxy runtime-published ports like a direct connection (0.7.0 backport) Nov 4, 2026
norrietaylor pushed a commit that referenced this pull request Nov 5, 2026
…#2714)

T99. The hostname proxy reads the target box's runtime-published ports (an allowed
expose, a published listener) from its live relay gate when the name resolves, so its
verdict admits exactly the ports a direct connection reaches. Admitted requests log
their route source at info, and refusals record route_source=none.

Backport of #2714, folded from #2724.

Adapted to the line:
- BoxGate, BoxPolicy and boxes::DynamicIngress become SessionGate,
  sessions::SessionPolicy and sessions::DynamicIngress.
- crate::traced::spawn becomes tokio::spawn in the new tests.
- The line's spawn_backend_on takes a full address, so
  proxy_forwards_runtime_published_port picks its port with a probe listener.
- The parity proptest checks the gate that carries the runtime publishes and asks
  admits_tcp, since admits_direct_tcp covers only the declared ports here.

Also keeps proxy_refuses_a_withdrawn_port_like_direct (NET-069, from #2732): it
publishes and withdraws through the gate only, because the line's listen watcher has no
record_dynamic_hold.

(cherry picked from commit 7097b96)
@norrietaylor
Copy link
Copy Markdown
Member Author

Folded into #3019, which landed on release/0.7.x as ac7491e84.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0