Repository navigation
fix(classifier): decode masked ct-mark writes in the install's conflict scan - #3004
Conversation
…ct scan nft prints an iptables-nft CONNMARK --save-mark --nfmask M --ctmask M through libxtables as `ct mark set mark and 0xM`, which drops the --ctmask. The scan read it as a write of every bit and refused to install over Tailscale's rp_filter workaround rules. The kernel keeps every connection-mark bit outside --ctmask, so those rules change the bits M alone. The scan now decodes each `ct mark set` value for the bits it can change: the libxtables save spelling changes M, a value read from the ct mark changes the bits an `and` clears and an `or`/`xor` names, and any other value replaces the whole mark. An `and` that clears the install's bits is now a conflict too. Refs gominimal/inbox#1201 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the
Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
|
@coderabbitai review |
|
/review |
Action performed
|
|
Approved: no findings at Nothing to do. Clobber: none seen. |
… saves The mask 0x00030000 lies in Tailscale's well-known range, so the install refused it before the conflict scan ran, and the assertion held whatever the decoder did. The table now runs `ct mark set mark and 0xM` against the default bits, over them and beside them, and each refusal must be the conflict scan's. Refs gominimal/inbox#1201 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
|
/review |
|
@coderabbitai review |
Action performed
|
…ict scan The scan decoded only the first `ct mark set` on a line and read later ones for 0x values alone, so a whole-mark write after a masked one (`ct mark set mark and 0xff0000 ct mark set meta mark`) passed. Each statement is now decoded from where the previous value ended. Refs gominimal/inbox#1201 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
|
/review |
|
@coderabbitai review |
Action performed
|
|
/review |
A rule iptables-nft wrote is an xt compat target in the kernel (`[ target name CONNMARK rev 2 ]`), and `nft list` prints it through libxtables, which drops a --ctmask or names no bits at all. The scan now reads `nft -a --debug=netlink list ruleset`, finds each rule whose kernel form carries an xt target by family, table, chain and handle, and decodes it from the nf_tables iptables-save and ip6tables-save listings instead of its nft text. A CONNMARK no listing covers is refused, because the step cannot see which bits it keeps. ip6tables-save is now read the way iptables-save is, for both backends. Refs gominimal/inbox#1201 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
|
@coderabbitai review |
Action not completed
|
…tlink form An nft that cannot run `nft -a --debug=netlink list ruleset` blocked every install, though the plain listing had just been read. The scan now uses the plain listing there, and finds an untranslated CONNMARK by its `xt target CONNMARK` text, so one no iptables-save listing covers is still refused. Refs gominimal/inbox#1201 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
|
@coderabbitai review |
Action not completed
|
The conflict scan now also runs `nft -a --debug=netlink list ruleset`, and the installer harness's stand-in nft answered it with nothing, so the staged conflict was never read. The stand-in serves the same fixture for it, and the scan falls back to the plain listing when the annotated one carries no table the plain one shows. Refs gominimal/inbox#1201 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
|
@coderabbitai review |
Action performed
|
…ct scan (#3004) * fix(classifier): decode masked ct-mark writes in the install's conflict scan nft prints an iptables-nft CONNMARK --save-mark --nfmask M --ctmask M through libxtables as `ct mark set mark and 0xM`, which drops the --ctmask. The scan read it as a write of every bit and refused to install over Tailscale's rp_filter workaround rules. The kernel keeps every connection-mark bit outside --ctmask, so those rules change the bits M alone. The scan now decodes each `ct mark set` value for the bits it can change: the libxtables save spelling changes M, a value read from the ct mark changes the bits an `and` clears and an `or`/`xor` names, and any other value replaces the whole mark. An `and` that clears the install's bits is now a conflict too. Refs gominimal/inbox#1201 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3 * test(minimald): pin the conflict scan's verdict on libxtables ct-mark saves The mask 0x00030000 lies in Tailscale's well-known range, so the install refused it before the conflict scan ran, and the assertion held whatever the decoder did. The table now runs `ct mark set mark and 0xM` against the default bits, over them and beside them, and each refusal must be the conflict scan's. Refs gominimal/inbox#1201 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3 * fix(classifier): decode every ct mark set on a rule line in the conflict scan The scan decoded only the first `ct mark set` on a line and read later ones for 0x values alone, so a whole-mark write after a masked one (`ct mark set mark and 0xff0000 ct mark set meta mark`) passed. Each statement is now decoded from where the previous value ended. Refs gominimal/inbox#1201 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3 * fix(classifier): read xt compat CONNMARK masks from iptables-save A rule iptables-nft wrote is an xt compat target in the kernel (`[ target name CONNMARK rev 2 ]`), and `nft list` prints it through libxtables, which drops a --ctmask or names no bits at all. The scan now reads `nft -a --debug=netlink list ruleset`, finds each rule whose kernel form carries an xt target by family, table, chain and handle, and decodes it from the nf_tables iptables-save and ip6tables-save listings instead of its nft text. A CONNMARK no listing covers is refused, because the step cannot see which bits it keeps. ip6tables-save is now read the way iptables-save is, for both backends. Refs gominimal/inbox#1201 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3 * fix(classifier): fall back to the plain listing when nft prints no netlink form An nft that cannot run `nft -a --debug=netlink list ruleset` blocked every install, though the plain listing had just been read. The scan now uses the plain listing there, and finds an untranslated CONNMARK by its `xt target CONNMARK` text, so one no iptables-save listing covers is still refused. Refs gominimal/inbox#1201 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3 * test(install): serve the annotated ruleset listing from the stand-in nft The conflict scan now also runs `nft -a --debug=netlink list ruleset`, and the installer harness's stand-in nft answered it with nothing, so the staged conflict was never read. The stand-in serves the same fixture for it, and the scan falls back to the plain listing when the annotated one carries no table the plain one shows. Refs gominimal/inbox#1201 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3 --------- Co-authored-by: Karl Godard <karl@minimal.dev> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit fce331f) Ported to the 0.7 line without main's QUIC443 work, which the line doesn't have: host_ruleset for kept_quic443_leaves, the nft -j list set stub branch and the BEP-018 tests next to the new test helper.
…sifier-race Brings in #3004 (the Tailscale ct-mark decode in install-host-classifier.sh), so the classifier step no longer refuses on every Tailscale host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
Refs gominimal/inbox#1201
Root cause (proven on the host)
On a host where tailscaled runs in iptables-nft mode,
min finalize-installrefused deny-all egress. The ct-mark conflict scan inscripts/install-host-classifier.shread Tailscale's OUTPUT rule as a write of every bit of the connection mark. The kernel does not run that rule.nft --debug=netlink list chain ip mangle OUTPUTon the affected host (posted on the issue) shows the rule ends in[ target name CONNMARK rev 2 ]. That is an xt compat target, not a nativect set mark. The ip6 rule is identical.iptables-saveon the same host gives them:-j CONNMARK --save-mark --nfmask 0xff0000 --ctmask 0xff0000. xt_CONNMARK computesctmark = (ctmark & ~ctmask) ^ (nfmark & nfmask). Every bit outside0x00ff0000is kept, including the default bits0x30000000.nft listprints an xt target through libxtables'xlatecallback. Inlibxt_CONNMARK.c(iptables 1.8.11),connmark_tg_xlate_v2prints a save with nfmask == ctmask asct mark set mark and 0x%x. That leaves out thect mark & ~ctmaskterm that keeps the other bits. For masks libxtables cannot translate (revision 1 with masks, or nfmask != ctmask), it prints no bits at all.nft list rulesettext as if it were the kernel rule. It treatedct mark set mark and 0xff0000as a full overwrite, so it refused under every--ct-mark-mask.The fix
nft -a --debug=netlink list ruleset(xt_target_rules_in).nft list ruleset, so this needs nothing new.<family> <table> <chain> <handle>) is matched to the-atext line that has the same# handle N.[ target name … ]has its nft text skipped. The rule is read instead from its family's nf_tables iptables-save listing, which spells every mask.iptables-nft-save/ip6tables-nft-save, oriptables-save/ip6tables-savewhen--versionreportsnf_tables(nft_backed_save).*tableheader for it. iptables-nft prints a table it cannot read as a comment instead.ip6tables-saveis now read the wayiptables-saveis, for both the nf_tables and the legacy backend (legacy_save).nft -a --debug=netlink list rulesetfails, the scan falls back to the plain listing. It then finds an untranslated CONNMARK by itsxt target CONNMARKtext and refuses it when no save listing covers its table. A translated CONNMARK is decoded from its libxtables text, which is exact because libxtables prints a masked save only when the two masks agree.ct mark setis decoded for the bits it can change (ct_set_writes, from the first commits):and Kchanges the bits outside K.or/xorV changes the bits of V.mark and 0xMchanges M.ct mark seton a line is decoded.ct mark set ct mark & 0xcfffffff, which clears the default bits and used to pass.Step 3 of the issue: running the QUIC helper on its own (not done, and why)
The QUIC block helper depends on the classifier:
nft add|delete element inet <classifier table> quic443_blocked_leaves …(render_quic443_helper).render_ruleset).install_quic443_helper).Installed alone, the helper would fail at runtime. The other finalize items run before the classifier block, so nothing independent was skipped.
Messages
New message text, all printed by
scan_ct_mark_conflictsinscripts/install-host-classifier.sh:ct_mark_conflicts_inawk):table <family> <table>, chain <chain>: <rule as nft prints it> (an iptables rule whose masks iptables-save did not list)error: refusing to install over an iptables CONNMARK rule whose masks this step cannot read, so it may write the bits this install uses. Install iptables with its nf_tables backend, so that iptables-save and ip6tables-save list the rule, then run min finalize-install again.iptables-nft-save/ip6tables-nft-save(or an nf_tablesiptables-save) lists the table,nft_backed_savecovers it and the rule is decoded from its masks.The new IPv6 save listing names its rules
ip6tables table <table>, chain <chain>: …, in the same form as the existingiptables table ….The new line and the new refusal are pinned by
install_reads_xt_connmark_masks_from_iptables_save. The existing refusal strings are unchanged.Tests
install_reads_xt_connmark_masks_from_iptables_save. Its fixture is the netlink dump from the issue (OUTPUT, plus PREROUTING built the same way) for ip and ip6, the-atext, and the fouriptables-savelines. It checks that:xt target "CONNMARK"in nft,--nfmask 0xff0000 --ctmask 0x30ff0000saved) is refused in ip and in ip6.--ctmask 0x00ffff00installs.host_toolsstands in fornft(plain and annotated),iptables-save,ip6tables-saveand the-nft-savepair, each with a--versionanswer, so no real tool on the test host is read. The existinghost_ruleset_toolskeeps its old behaviour on top of it.install_decodes_masked_ct_mark_writesandinstall_refuses_a_whole_mark_ct_writerstill pass.just test-one install_ -p minimald: 6 passed, also withawkresolved togawk --posix.just test-one classifier -p minimald: 67 passed.just test-one host_ip -p minimald: 26 passed.just test-one classifier -p minimal: 8 passed (first round).cargo clippy -p minimald --all-targets -D warnings,just clippy-strictandjust lint-shell: clean.just test-root-integration(needs root) and the fulljust ci.Open
nft -a --debug=netlink list rulesetinterleaving: nft writes the netlink lines during the cache fetch, before the text, and the scan reads both streams together. The pairing relies on the rule handle, not on order. A real nft run oflist ruleset(not onlylist chain) with this flag has not been checked on a host with iptables-nft rules.🤖 Generated with Claude Code
https://claude.ai/code/session_016CYvTvgTNC876h72BpHfV3
Summary by CodeRabbit