77> and keys may move without notice until 1.0. Pin versions, and report what
88> breaks.
99
10- A terminal UI for the Linux firewall — ** ufw** and ** firewalld** — that shows
11- the rules you actually have and ** previews the exact command line of every
12- change before running it** .
10+ A terminal UI for the Linux firewall — ** ufw** , ** firewalld** and ** nftables ** —
11+ that shows the rules you actually have and ** previews the exact command line of
12+ every change before running it** .
1313
14- Neither firewall has a TUI: you get the CLI, or a desktop GUI. ` tui-firewall `
14+ None of them has a TUI: you get the CLI, or a desktop GUI. ` tui-firewall `
1515fills the gap on a server or a tiling desktop, in the
1616[ Omarchy] ( https://omarchy.org ) visual style. It picks the backend your machine
1717actually runs, and the screen is built from what that backend can do — one rule
1818list and three default policies for ufw, one group per zone with its target for
19- firewalld.
19+ firewalld, and for a machine neither of them manages, the raw nftables ruleset
20+ read straight from ` nft ` : filter chains by hook, a NAT view, and named sets as
21+ aliases.
2022
2123![ Rules table] ( docs/screenshots/tui-firewall-main.png )
2224
@@ -185,13 +187,16 @@ you quit.
185187``` sh
186188tui-firewall --demo # an in-memory ufw
187189tui-firewall --demo=firewalld # an in-memory firewalld
190+ tui-firewall --demo=nftables # a sample router ruleset
188191```
189192
190193` --demo ` runs against an in-memory sample firewall. Every key works, every
191194command is built and previewed for real, and nothing touches your system. The
192- two demos are the two real backends over fake output, not two skins: the
193- firewalld one parses firewalld-shaped text with the same parser and builds its
194- commands with the same builder.
195+ three demos are the three real backends over fake output, not three skins: each
196+ one parses backend-shaped output with the same parser and builds its commands
197+ with the same builder. The nftables demo starts from a router ruleset — filter
198+ chains for input, forward and output, the two NAT chains, and the named sets the
199+ rules use as aliases.
195200
196201## Every change is previewed
197202
@@ -207,10 +212,12 @@ is what executes.
207212tui-firewall # drive the real firewall
208213tui-firewall --demo # sample ufw data, no privileges needed
209214tui-firewall --demo=firewalld # sample firewalld data
215+ tui-firewall --demo=nftables # sample router ruleset
210216tui-firewall --check # read the firewall, print JSON, exit
211217tui-firewall --report # print what a bug report needs, exit
212218tui-firewall --backend ufw # skip autodetection
213219tui-firewall --backend firewalld
220+ tui-firewall --backend nftables
214221tui-firewall --theme ~ /mytheme/colors.toml
215222tui-firewall --sudo " " # run the firewall command directly (as root)
216223tui-firewall --version
@@ -380,9 +387,84 @@ collects the answers and previews whatever comes back. That is the same reason
380387` e ` is absent on firewalld — starting and stopping a system service is
381388` systemctl ` 's job, and the tool says so instead of doing it quietly.
382389
390+ ## nftables
391+
392+ ![ The nftables rule list] ( docs/screenshots/tui-firewall-nftables.png )
393+
394+ On a machine that runs neither ufw nor firewalld, ` tui-firewall ` reads `nft -j
395+ list ruleset` and drives ` nft` itself. It is the third backend, picked
396+ automatically when the other two are absent, and the screen is built from the
397+ ruleset it finds rather than from a fixed layout.
398+
399+ Because nftables is not a policy manager but the packet path itself, the rule
400+ list reads the way a router's rule list reads: the verdict, the ** interfaces**
401+ the packet passes between (` iifname ` / ` oifname ` ), the ** connection state** it
402+ matches (` ct state established,related ` ), the protocol and port, and the
403+ ** counter** that says whether the rule has ever fired. A chain has a view of its
404+ own, one group per hooked chain, stepped through with ` [ ` and ` ] ` or picked from
405+ ` v ` ; base chains show their ** policy** even when they hold no rules, because the
406+ policy is a fact worth seeing.
407+
408+ Two chain types get a view shaped for the question they answer:
409+
410+ - ** NAT.** Address translation is read as * what does this become* , not * what is
411+ allowed* , so the masquerade, SNAT and DNAT rules of the ` nat ` chains are shown
412+ in their own view with the translated target as the column the eye lands on.
413+
414+ ![ The NAT view] ( docs/screenshots/tui-firewall-nftables-nat.png )
415+
416+ - ** Aliases.** A named set is an alias a rule refers to by name. Each is listed
417+ with what it holds, its members, and how many rules use it — a set nothing
418+ refers to is dimmed as dead weight, and one four rules refer to is four rules
419+ you change by editing it.
420+
421+ ![ Named sets as aliases] ( docs/screenshots/tui-firewall-nftables-aliases.png )
422+
423+ The add-rule form carries the matches a router rule needs that ufw and firewalld
424+ never expose: inbound and outbound interface, connection state, ICMP type,
425+ address family, and an alias as the source.
426+
427+ ![ The add-rule form on nftables] ( docs/screenshots/tui-firewall-nftables-add.png )
428+
429+ ### What it writes, and where it will not
430+
431+ nftables has no permanent/runtime split and no manager to answer to, so a write
432+ is a change to the live ruleset. That makes it easy to lock yourself out, and
433+ ` tui-firewall ` is deliberately narrow about where it writes: only to a ** base
434+ chain whose policy nft reported** , or to ** its own table** . A chain that some
435+ other manager owns is shown but marked read-only, and the tool says why in the
436+ chain's own description rather than letting you find out at the confirm dialog.
437+ Every rule is deleted by its ** handle** , which ` nft -j ` carries in the ruleset,
438+ because a rule's position shifts the moment anything is inserted above it.
439+
440+ ### Staged atomic apply, with a connectivity-safe rollback
441+
442+ Changing many rules on a router one command at a time is how a remote session
443+ dies halfway through, with the box in a state that is neither the old ruleset nor
444+ the new one. nftables can do better, and this backend uses it: press ` s ` to turn
445+ ** staging** on, and every change is collected instead of applied. ` S ` reviews
446+ the pending set.
447+
448+ ![ The staged changes] ( docs/screenshots/tui-firewall-nftables-staging.png )
449+
450+ Applying the batch is one ` nft -f ` transaction: ** all of the changes, or none** ,
451+ never a half-applied ruleset. Before it runs, the whole transaction is previewed
452+ — the script that goes to ` nft ` 's standard input, which no single command line
453+ could show.
454+
455+ ![ The atomic apply preview] ( docs/screenshots/tui-firewall-nftables-apply.png )
456+
457+ The apply is also ** connectivity-safe** . Just before the batch runs, the tool
458+ snapshots the current ruleset; then it applies, and starts a timer. If you press
459+ ` k ` to say you still have access, the batch stays. If you do not — because the
460+ change you just made cut off the session you are reading this in — the snapshot
461+ is restored automatically when the timer expires, and you are back on the ruleset
462+ that was working. It is the standard ` iptables-apply ` idea, done as one nft
463+ transaction.
464+
383465## What v0.1 can do
384466
385- ** Both backends **
467+ ** Every backend **
386468
387469- Read the live firewall and show status, default policies and logging.
388470- List rules with action, source, destination, ports, protocol, service,
@@ -416,6 +498,22 @@ collects the answers and previews whatever comes back. That is the same reason
416498 from the same guided form.
417499- Zone target, log-denied value, reload, and the actions menu above.
418500
501+ ** nftables**
502+
503+ - Read ` nft -j list ruleset ` and show every base and used regular chain, its
504+ hook and its policy, as one group each.
505+ - The router reading of a rule: inbound and outbound interface, connection
506+ state, protocol, port, address family, counter and comment.
507+ - A NAT view for masquerade, SNAT and DNAT port forwards, and an aliases view of
508+ the named sets with the number of rules using each.
509+ - Add a rule with an action (accept/drop/reject), an interface pair, a
510+ connection-state match, an ICMP type, an alias as the source, and a family;
511+ add a masquerade, a port forward or a named set from the actions menu.
512+ - Write only to a base chain nft reported a policy for, or to the tool's own
513+ table, and say why when it will not. Delete by handle.
514+ - Stage changes and apply them as one atomic ` nft -f ` transaction, with an
515+ automatic connectivity-safe rollback if the apply is not kept in time.
516+
419517## What v0.1 cannot do
420518
421519- ** No rule editing.** Change a rule by deleting it and adding the new one.
@@ -427,7 +525,10 @@ collects the answers and previews whatever comes back. That is the same reason
427525- ** A firewalld policy object is read-only-ish** : its entries are listed and can
428526 be added and removed, but its target and its ingress/egress zones are not
429527 editable here.
430- - ** No live log tail** , no packet counters, no nft view.
528+ - ** No live log tail.**
529+ - ** No nftables rule editing or reordering** , and no set-element editing: a rule
530+ is changed by deleting it and adding the new one, and a named set is created or
531+ used, not edited member by member.
431532- Rules follow the backend's own order; there is no reordering key.
432533
433534## Compatibility
@@ -488,6 +589,14 @@ own smoke test appends to when it runs against a real machine in
488589[ tui-lab] ( https://github.com/tui-tools/tui-lab ) .
489590<!-- compat:end -->
490591
592+ The nftables backend shows no tested version yet because no smoke run has
593+ appended one to ` compat/results.jsonl ` . It has been exercised end to end in the
594+ [ tui-lab] ( https://github.com/tui-tools/tui-lab ) ` router-topology ` scenario — a
595+ libvirt router on Ubuntu noble, where the read path, the router matches, the NAT
596+ and alias views and the staged atomic apply were driven against a real ` nft ` ,
597+ alongside the network-namespace tests in this repo. A tested version will land
598+ here once that run records one through the smoke test.
599+
491600## Configuration
492601
493602` /etc/tui-firewall/config.toml ` , then ` ~/.config/tui-firewall/config.toml ` (the
@@ -536,14 +645,21 @@ ufw exposes a single group (`rules`) carrying the global in/out/routed
536645policies; the group selector stays hidden when there is only one. firewalld
537646exposes one group per zone with the zone target as its policy, plus one per
538647policy object — the full mapping is documented at the top of
539- ` internal/firewalld/firewalld.go ` .
648+ ` internal/firewalld/firewalld.go ` . nftables exposes one group per hooked or used
649+ chain with its policy, plus a NAT view and an aliases view; the ` Extra ` map is
650+ where the router-shaped fields ride — interface, connection state, counter,
651+ translated target, set membership — so the same ` Rule ` serves every backend
652+ without the model growing a column per feature.
540653
541654Mutations are ` firewall.Change ` values produced by the backend: an ordered list
542655of ` runner.Command ` s with the description and the danger flag the dialog is
543656painted from. The list exists because firewalld needs two invocations to apply
544657one change, and making that a list rather than a hidden second exec keeps the
545658promise intact — every line the dialog shows is a line that runs, and nothing
546- else does. On confirmation the UI hands those commands back to the
659+ else does. On nftables a staged batch is the same idea taken to its limit: the
660+ whole set of changes is one ` nft -f ` transaction, previewed as the script it
661+ sends, applied all-or-nothing, and rolled back to a pre-apply snapshot if the
662+ operator does not confirm they still have access in time. On confirmation the UI hands those commands back to the
547663[ kit runner] ( https://github.com/tui-tools/tui-kit#the-contract-preview-confirm-run ) ,
548664which resolves the binary and the privilege prefix. That is the whole trust
549665boundary.
0 commit comments