8000 fix(minimald): close the host classifier's mark, IPv6 and probe gaps (0.7.0 backport) by norrietaylor · Pull Request #2742 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

fix(minimald): close the host classifier's mark, IPv6 and probe gaps (0.7.0 backport) - #2742

Merged
norrietaylor merged 7 commits into
release/0.7.0from
fix/net-host-classifier-gaps-0.7.0
Oct 29, 2026
Merged

norrietaylor merged 7 commits into
release/0.7.0from
fix/net-host-classifier-gaps-0.7.0

Conversation

@norrietaylor
Copy link
Copy Markdown
Member

Backports #2712 (task T105, #2692) to release/0.7.0. It is a security fix in the native-Linux host classifier:

  • The install refuses a conntrack mark writer that would clobber the classifier's whole mark, and a mark range other tools use; the recorded mask is reapplied at boot.
  • A host-address box's IPv6 egress is refused, not left unclassified.
  • The per-launch probe returns without waiting out a TCP retransmit.

Stacked on #2678; merge that first. #2678 carries the 0.7.3 version bump and the pulldown-cmark advisory fix. Until it merges, this diff also shows #2678's commits.

Commits

Squash-merge the pair. Every test #2712 adds is kept. Only its edits to finalize_install_names_the_quic_block_helper are left out, because that test and the QUIC helper item are not on this branch. User-facing text is main's.

Verification (Linux, Lima)

gominimal-aw-bot Bot and others added 7 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 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.
…(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)
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.
@norrietaylor
norrietaylor requested a review from a team as a code owner October 29, 2026 04:22
@coderabbitai
coderabbitai Bot commented Oct 29, 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: 733a4874-645a-4f8f-b0d5-e4f89d512b62

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 29, 2026 •
Copy link
Copy Ma 8000 rkdown
Contributor

Approved: no findings at b0643956841b.

Nothing to do.

Clobber: none found.

@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 29, 2026
@norrietaylor
norrietaylor merged commit c30a542 into release/0.7.0 Oct 29, 2026
34 checks passed
@norrietaylor
norrietaylor deleted the fix/net-host-classifier-gaps-0.7.0 branch October 29, 2026 23:57
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.

2 participants

0