Skip to content

Commit a669161

Browse files
edimarlnxclaude
andcommitted
Document the nftables backend and render its screens
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
1 parent c8e5a62 commit a669161

9 files changed

Lines changed: 163 additions & 14 deletions

‎Makefile‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ tidy:
6262
$(GO) mod tidy
6363

6464
## screenshots: re-render the README frames from --demo (needs chrome/chromium).
65-
## Both backends are covered: the ufw demo for the shared screens, the
66-
## firewalld one for the zone view and the actions menu that only it has.
65+
## Every backend is covered: the ufw demo for the shared screens, the firewalld
66+
## one for the zone view and the actions menu that only it has, and the
67+
## nftables one for the router screens — the flow rule list, the NAT and alias
68+
## views, the phase-2 add form and the staged atomic apply.
6769
screenshots: build
6870
python3 $(KIT)/tools/render-screenshots.py \
6971
--bin $(BIN)/$(TOOL) --name $(TOOL) --out docs/screenshots \
@@ -73,6 +75,13 @@ screenshots: build
7375
--bin $(BIN)/$(TOOL) --name $(TOOL) --out docs/screenshots \
7476
--args="--demo=firewalld" \
7577
--screen firewalld= --screen firewalld-actions=x
78+
python3 $(KIT)/tools/render-screenshots.py \
79+
--bin $(BIN)/$(TOOL) --name $(TOOL) --out docs/screenshots \
80+
--args="--demo=nftables" \
81+
--screen nftables= --screen nftables-add=a \
82+
--screen 'nftables-nat=[[' --screen 'nftables-aliases=[' \
83+
--screen nftables-staging=sdjdS \
84+
--screen 'nftables-apply=sdjdS\r'
7685

7786
## readme: regenerate the generated README sections from tool.json.
7887
readme:

‎README.md‎

Lines changed: 128 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@
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`
1515
fills the gap on a server or a tiling desktop, in the
1616
[Omarchy](https://omarchy.org) visual style. It picks the backend your machine
1717
actually runs, and the screen is built from what that backend can do — one rule
1818
list 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
186188
tui-firewall --demo # an in-memory ufw
187189
tui-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
191194
command 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.
207212
tui-firewall # drive the real firewall
208213
tui-firewall --demo # sample ufw data, no privileges needed
209214
tui-firewall --demo=firewalld # sample firewalld data
215+
tui-firewall --demo=nftables # sample router ruleset
210216
tui-firewall --check # read the firewall, print JSON, exit
211217
tui-firewall --report # print what a bug report needs, exit
212218
tui-firewall --backend ufw # skip autodetection
213219
tui-firewall --backend firewalld
220+
tui-firewall --backend nftables
214221
tui-firewall --theme ~/mytheme/colors.toml
215222
tui-firewall --sudo "" # run the firewall command directly (as root)
216223
tui-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
536645
policies; the group selector stays hidden when there is only one. firewalld
537646
exposes one group per zone with the zone target as its policy, plus one per
538647
policy 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

541654
Mutations are `firewall.Change` values produced by the backend: an ordered list
542655
of `runner.Command`s with the description and the danger flag the dialog is
543656
painted from. The list exists because firewalld needs two invocations to apply
544657
one change, and making that a list rather than a hidden second exec keeps the
545658
promise 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),
548664
which resolves the binary and the privilege prefix. That is the whole trust
549665
boundary.
48.2 KB
Loading
44.5 KB
Loading
26 KB
Loading
55.2 KB
Loading
22.3 KB
Loading
59.3 KB
Loading

‎tool.json‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,30 @@
3939
"path": "docs/screenshots/tui-firewall-firewalld-actions.png",
4040
"caption": "The firewalld actions menu: default zone, interfaces, masquerading, panic mode"
4141
},
42+
{
43+
"path": "docs/screenshots/tui-firewall-nftables.png",
44+
"caption": "The nftables input chain read straight from nft, with interfaces, connection state and per-rule counters"
45+
},
46+
{
47+
"path": "docs/screenshots/tui-firewall-nftables-nat.png",
48+
"caption": "The NAT view: masquerade, SNAT and DNAT port forwards, each read as what it translates to"
49+
},
50+
{
51+
"path": "docs/screenshots/tui-firewall-nftables-aliases.png",
52+
"caption": "Named sets as aliases, with what each holds and how many rules use it"
53+
},
54+
{
55+
"path": "docs/screenshots/tui-firewall-nftables-add.png",
56+
"caption": "The add-rule form on nftables: connection state and inbound/outbound interface matches"
57+
},
58+
{
59+
"path": "docs/screenshots/tui-firewall-nftables-staging.png",
60+
"caption": "Staged changes collected for one atomic apply, reviewed before anything runs"
61+
},
62+
{
63+
"path": "docs/screenshots/tui-firewall-nftables-apply.png",
64+
"caption": "The atomic apply preview: the whole nft transaction, with a connectivity-safe rollback window"
65+
},
4266
{
4367
"path": "docs/screenshots/tui-firewall-help.png",
4468
"caption": "The help screen, generated from the same action table as the key map"

0 commit comments

Comments
 (0)