Repository navigation
Conversation
`nft -j list ruleset` is the source of truth: one JSON read, decoded into tables, base chains with their hook and policy, rules whose expressions become the columns a router rule list needs — verdict, interfaces, protocol, addresses, ports, counter, comment — and named sets with the number of rules that refer to each. An expression this package does not model is rendered as text beside the columns rather than dropped, because a rule shown half of is worse than a rule shown verbatim. Writing is the narrow half. Every change is one nft invocation with an argv built here and previewed by the same runner that executes it, and the guard refuses anything outside a base chain with a policy or outside the table this tool owns, with the sentence saying which of the two it was and why it matters. Selection learns the third backend. nftables is the answer for a machine nothing else manages, so it is chosen last, and when the loaded ruleset carries ufw's chains or firewalld's table the detector names that tool instead and says so in the detail --check and --report now print. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCypLQF5AD8JD831Ea9S9q
Four fixtures, all of them bytes nft itself wrote. `nft -j list ruleset` needs root and these tests do not have it, but a user namespace with a network namespace of its own carries CAP_NET_ADMIN over that namespace's empty ruleset — so `unshare -rn nft -f x.nft && nft -j -a list ruleset` produces a real listing while touching nothing on the host. The .nft source of each sits beside its JSON, so anyone can produce them again. The fuzz targets found four things worth having found. A chain with no hook could arrive carrying a policy, which is a promise the kernel is not making and the mutation guard reads. A control character in a rule or in a set element went straight into a table cell, where a newline breaks the table rather than the cell. An expression with several fields lost all but one of them. And a set name counted before sanitising was a reference to something the user could not see. Their inputs are seeds now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCypLQF5AD8JD831Ea9S9q
A filter rule, an address translation and a named set are three different things to read, and one set of columns is honest about none of them. So a group now names the layout it wants: the rule table gains the interface, port and counter columns when the backend fills them — which is the OPNsense reading of a rule, and which ufw and firewalld would only answer with dashes — while NAT gets the translation target up front and the aliases get their member count and, more to the point, the number of rules that would break if one went away. v opens the group picker, which the update loop could already answer and nothing had ever asked. Nothing else changed keys. The three keys nftables cannot honour stop being advertised on it, because a key that only ever says "not supported here" is worse than no key. The manifest floor is 0.9 and the reason is in the notes beside it: that is where nft grew `-j` and stamped the schema this backend reads. Below it there is no ruleset to read in any form this tool will parse, and parsing nft's human output is not a thing it will do. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCypLQF5AD8JD831Ea9S9q
The other two arms of the smoke test count what the firewall itself reports and compare. This one does the same with `nft list ruleset`: the base chains, the sets, the version that printed the JSON. A tool that fetched the output and failed to parse it reports zero, and a tool that misread a hook reports the wrong number; both fail here. It also stops claiming the NAT and alias views are writable on a machine with no table of ours. They are not: everything they create lives in that table, and the refusal now points at the action that creates it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCypLQF5AD8JD831Ea9S9q
Close the seven UI gaps the phase-1 lab run surfaced, and add the connectivity-safe batch apply. Filter rules gain interface (iifname/oifname), connection-state (ct state established,related) and ICMP (ip protocol icmp / ip6 nexthdr ipv6-icmp, with an optional type) matches, each built for the table's family. Masquerade takes an optional source network. A rule that matches an alias reads its family off the set's element type instead of demanding the Family field. The actions menu can delete a chain of the tool's own table and drop the whole table, guarded so it never removes someone else's. Pressing a in the NAT and alias views opens the actions those views add through instead of a refusal. Staging (internal/nftables/staging) collects mutations into a pending set, applies them as one `nft -f` transaction — all-or-nothing — after a single preview and confirm, snapshots the ruleset first, and rolls it back on its own if the operator does not confirm they still have access within a keep window (default 60s). Driven from the TUI with s/S/k, and reported by --check and --report. Covered by table-driven tests, staging unit tests with a hand-driven timer, two new fuzz targets plus ct-state/icmp parser seeds (which caught a whitespace-only ct state), and a real-nft run in a `unshare -rn` namespace that applies a forward policy drop with its accept atomically and restores the snapshot on a missed keep. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCypLQF5AD8JD831Ea9S9q
Add the six nftables --demo frames the README now references — the flow rule list, the NAT and alias views, the phase-2 add form, and the staged atomic apply with its transaction preview — and wire them into the screenshots target and the manifest. Write the nftables section: the third backend and when it is picked, the router reading of a rule (interfaces, connection state, counter), the NAT and alias views, where it will and will not write, and the staged atomic apply with its connectivity-safe rollback. Note the router-topology lab coverage where the compat table still shows no tested version. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCypLQF5AD8JD831Ea9S9q
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the nftables backend (phases 1 and 2) to main, a third backend beside ufw and firewalld.
The third backend
On a machine that runs neither ufw nor firewalld,
tui-firewallreadsnft -j list rulesetand drivesnftitself. It is picked automatically when the other two are absent, and the screen is built from the ruleset it finds:It writes only to a base chain whose policy nft reported, or to its own table, and says why when it will not. Deletes go by handle, which
nft -jcarries.Phase 2: router-shaped matches
The rule list reads the way a router's does: the verdict, the inbound/outbound interfaces (
iifname/oifname), the connection state (ct state established,related), protocol and port, address family, and the per-rule counter. The add-rule form gained the matches ufw and firewalld never expose: interface pair, connection state, ICMP type, address family, and an alias as the source. Delete-table and delete-chain actions round it out.Staged atomic apply, with connectivity-safe rollback
Changing many rules on a router one command at a time is how a remote session dies halfway through.
sturns staging on; changes are collected instead of applied.Sreviews the pending set. Applying is onenft -ftransaction — all of the changes or none — previewed as the exact script it sends. Just before it runs the tool snapshots the ruleset; after applying it starts a timer.kkeeps the batch; if you lose access and cannot, the snapshot is restored automatically. It is theiptables-applyidea done as one nft transaction.Acceptance
Proven in the lab: the tui-lab
router-topologybranch ran the full flow against a realnfton a libvirt router (Ubuntu noble) — read path, router matches, NAT and alias views, and the staged atomic apply, 36/36 three times. In this repo the backend is covered by network-namespace tests plus the parser/fuzz/render suites. The compat table still shows no tested version for nftables because no smoke run has appended one tocompat/results.jsonlyet; the lab coverage is noted in the README instead.This PR's own change
Documents the backend in the README (a third-backend section, the staged atomic apply, updated intro/usage/demo/architecture) and renders the six new
--demo=nftablesscreens the README references — the flow rule list, the NAT and alias views, the phase-2 add form, the staging review and the atomic-apply preview — wired into thescreenshotstarget and the manifest.make checkis green, exec boundary clean, pre-push scan empty.🤖 Generated with Claude Code
https://claude.ai/code/session_01MCypLQF5AD8JD831Ea9S9q