Skip to content

Commit ee682ff

Browse files
edimarlnxclaude
andauthored
Release 0.5.0: iptables backend; tui-kit v0.4.0 (#25)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MbCfX3T8khyd9hsrN5kMo3
1 parent 00e649c commit ee682ff

12 files changed

Lines changed: 82 additions & 32 deletions

File tree

‎README.md‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/tui-tools/tui-firewall/badge)](https://scorecard.dev/viewer/?uri=github.com/tui-tools/tui-firewall)
44
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/14368/badge)](https://www.bestpractices.dev/projects/14368)
55

6+
<!-- stability:start -->
67
> **Beta.** The family is days old and still changing. Package names, flags
78
> and keys may move without notice until 1.0. Pin versions, and report what
89
> breaks.
10+
<!-- stability:end -->
911
1012
A terminal UI for the Linux firewall — **ufw**, **firewalld**, **iptables** and **nftables** —
1113
that shows the rules you actually have and **previews the exact command line of
@@ -133,7 +135,7 @@ Upgrades then arrive with the rest of your system updates.
133135
### Any distribution, static binary
134136

135137
```sh
136-
curl -fsSL https://github.com/tui-tools/tui-firewall/releases/download/v0.4.1/tui-firewall_0.4.1_linux_amd64.tar.gz | tar -xz tui-firewall
138+
curl -fsSL https://github.com/tui-tools/tui-firewall/releases/download/v0.5.0/tui-firewall_0.5.0_linux_amd64.tar.gz | tar -xz tui-firewall
137139
sudo install -m0755 tui-firewall /usr/local/bin/tui-firewall
138140
```
139141

@@ -456,7 +458,9 @@ rules in the preview:
456458
![Persisting with a diff preview](docs/screenshots/tui-firewall-iptables-persist.png)
457459

458460
The command is the persistence layer's own: `netfilter-persistent save` on
459-
Debian and Ubuntu, `service iptables save` and `service ip6tables save` on RHEL.
461+
Debian and Ubuntu, `/usr/libexec/iptables/iptables.init save` and
462+
`ip6tables.init save` on Fedora and RHEL (what `service iptables save` runs,
463+
without needing the `service` wrapper installed).
460464
The comparison covers the filter table and leaves out the chains other daemons
461465
rebuild, so a host where tailscaled added `ts-input` after the last save still
462466
reads as in sync.
@@ -693,7 +697,7 @@ are capped, so a firewall under a scan cannot grow it without bound.
693697
- Insert a rule right before the chain's catch-all REJECT or DROP, with the
694698
position and the reason in the preview; refuse a position after it. Delete by
695699
specification.
696-
- Persist with `netfilter-persistent save` or `service iptables save`, previewed
700+
- Persist with `netfilter-persistent save` or iptables-services' `iptables.init save`, previewed
697701
with the diff against the saved files; report runtime-vs-saved drift in the
698702
header, the status line and `--check`.
699703
- Stage changes and apply them through `iptables-restore --noflush`, with the
@@ -808,7 +812,7 @@ hidden; one below the minimum is marked as such and the tool still runs.
808812
| Binary | `iptables` |
809813
| Version read with | `iptables --version` |
810814
| Minimum | 1.6 |
811-
| Tested | `1.8.10` |
815+
| Tested | `1.8.10`, `1.8.11` |
812816

813817
| Versions | What changes |
814818
| --- | --- |

‎compat/results.jsonl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
{"backend": "ufw", "date": "2026-08-30", "distro": "ubuntu-24.04", "result": "pass", "suite": "smoke", "tool": "tui-firewall", "version": "0.36.2"}
33
{"backend": "firewalld", "date": "2026-08-30", "distro": "fedora-44", "result": "pass", "suite": "smoke", "tool": "tui-firewall", "version": "2.4.4"}
44
{"backend": "iptables", "date": "2026-09-25", "distro": "ubuntu-24.04", "result": "pass", "suite": "smoke", "tool": "tui-firewall", "version": "1.8.10"}
5+
{"backend": "iptables", "date": "2026-09-25", "distro": "fedora-44", "result": "pass", "suite": "smoke", "tool": "tui-firewall", "version": "1.8.11"}

‎go.mod‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/charmbracelet/bubbles v1.0.0
77
github.com/charmbracelet/bubbletea v1.3.10
88
github.com/charmbracelet/lipgloss v1.1.0
9-
github.com/tui-tools/tui-kit v0.3.1
9+
github.com/tui-tools/tui-kit v0.4.0
1010
)
1111

1212
require (
@@ -22,12 +22,12 @@ require (
2222
github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
2323
github.com/mattn/go-isatty v0.0.24 // indirect
2424
github.com/mattn/go-localereader v0.0.1 // indirect
25-
github.com/mattn/go-runewidth v0.0.29 // indirect
25+
github.com/mattn/go-runewidth v0.0.30 // indirect
2626
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
2727
github.com/muesli/cancelreader v0.2.2 // indirect
2828
github.com/muesli/termenv v0.16.0 // indirect
2929
github.com/rivo/uniseg v0.4.7 // indirect
30-
github.com/xo/terminfo v1.0.0 // indirect
31-
golang.org/x/sys v0.47.0 // indirect
32-
golang.org/x/text v0.41.0 // indirect
30+
github.com/xo/terminfo v1.2.0 // indirect
31+
golang.org/x/sys v0.48.0 // indirect
32+
golang.org/x/text v0.42.0 // indirect
3333
)

‎go.sum‎

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsRe
2828
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
2929
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
3030
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
31-
github.com/mattn/go-runewidth v0.0.29 h1:3oGF3R/S2N9DQ3ptftzVIvg2eicmojCzlwBEmqEPDfQ=
32-
github.com/mattn/go-runewidth v0.0.29/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
31+
github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=
32+
github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
3333
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
3434
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
3535
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
@@ -38,14 +38,12 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc
3838
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
3939
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
4040
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
41-
github.com/tui-tools/tui-kit v0.3.1 h1:0dxrN6+nEvzyWwSHxYxYjn/wGPygHneqG0GMBiuqoFs=
42-
github.com/tui-tools/tui-kit v0.3.1/go.mod h1:Vpg1NcQ5ktylMvXTzsixdHjLUNxhntR3ZYSOYaQ7/FQ=
43-
github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY=
44-
github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
45-
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
46-
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
41+
github.com/tui-tools/tui-kit v0.4.0 h1:dSbT796aca6wOIZdO7XV1EA2rraT/J7iYHGF9IWRyy4=
42+
github.com/tui-tools/tui-kit v0.4.0/go.mod h1:Vpg1NcQ5ktylMvXTzsixdHjLUNxhntR3ZYSOYaQ7/FQ=
43+
github.com/xo/terminfo v1.2.0 h1:d0ZTOCpuGE0lwSAOs0zcJjwz3jWQyqcRt9XbGJpCOl4=
44+
github.com/xo/terminfo v1.2.0/go.mod h1:lGzkSo8Fe7IRh/w+Gqz7n5mDog4FVXCj3gy/DYTBqio=
4745
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
48-
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
49-
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
50-
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
51-
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
46+
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
47+
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
48+
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
49+
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=

‎internal/backends/select.go‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,10 +346,17 @@ func activeDetail(name string, sudoPrefix []string) string {
346346
return "the " + name + " service is running on this machine"
347347
}
348348
layout := iptablesLayout()
349-
detail := layout.Unit() + " is enabled and restores " + layout.V4Path +
349+
// The unit is named after its package on Debian (netfilter-persistent),
350+
// but on RHEL the iptables unit comes from iptables-services: name the
351+
// package too, since "iptables is enabled" alone reads like a tautology.
352+
loader := layout.Unit()
353+
if loader != layout.Kind {
354+
loader = "the " + loader + " unit (" + layout.Kind + ")"
355+
}
356+
detail := loader + " is enabled and restores " + layout.V4Path +
350357
" at boot, so iptables is the firewall in charge of this machine"
351358
if !layout.Enabled {
352-
detail = layout.Unit() + " restored " + layout.V4Path +
359+
detail = loader + " restored " + layout.V4Path +
353360
" on this boot, so iptables is the firewall in charge of this machine"
354361
}
355362
return detail + nativeTablesNote(sudoPrefix)

‎internal/backends/select_test.go‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,29 @@ func TestResolveAutoPicksIptablesWhenItsLoaderRan(t *testing.T) {
379379
}
380380
}
381381

382+
func TestResolveAutoIptablesNamesIptablesServices(t *testing.T) {
383+
// On RHEL the loader is the iptables unit of iptables-services: the
384+
// sentence names the package, not only a unit called iptables.
385+
stubAllProbes(t, probeState{}, probeState{installed: true}, probeState{installed: true})
386+
stubIptables(t, probeState{installed: true, active: true, enabled: true},
387+
iptables.Layout{Kind: iptables.LayoutIptablesServices,
388+
V4Path: "/etc/sysconfig/iptables", V6Path: "/etc/sysconfig/ip6tables",
389+
Enabled: true})
390+
391+
selection, err := Resolve(autoConfig())
392+
if err != nil {
393+
t.Fatalf("Resolve: %v", err)
394+
}
395+
if selection.Name != BackendIptables {
396+
t.Fatalf("Resolve = %q, want iptables", selection.Name)
397+
}
398+
for _, want := range []string{"the iptables unit (iptables-services)", "/etc/sysconfig/iptables"} {
399+
if !strings.Contains(selection.Detail, want) {
400+
t.Errorf("detail %q should mention %q", selection.Detail, want)
401+
}
402+
}
403+
}
404+
382405
func TestResolveAutoIptablesSaysWhenNativeTablesExistToo(t *testing.T) {
383406
// A host with both: the iptables backend is chosen, and the sentence says
384407
// there is a native nft table it will not show.

‎internal/iptables/backend.go‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,13 @@ func (r *Real) runnerFor(bin string) (*runner.Runner, error) {
6868
return nil, err
6969
}
7070
search := sbinPaths(bin)
71-
if bin == "cat" {
71+
switch {
72+
case bin == "cat":
7273
search = []string{"/usr/bin/cat", "/bin/cat"}
74+
case strings.HasPrefix(bin, "/"):
75+
// An absolute path, like the iptables-services init scripts, is its
76+
// own location: there is nothing to search for.
77+
search = nil
7378
}
7479
run, err := runner.New(runner.Options{
7580
Bin: bin,

‎internal/iptables/detect.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func DetectLayout() Layout {
6868
layout.Enabled = unitEnabled("netfilter-persistent")
6969
return layout
7070
}
71-
if fileExists("/usr/libexec/iptables/iptables.init") {
71+
if fileExists(iptablesInit) {
7272
layout := iptablesServicesLayout
7373
layout.Enabled = unitEnabled("iptables")
7474
return layout

‎internal/iptables/fake.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,9 @@ func (f *Fake) apply(cmd firewall.Command) (string, error) {
195195
return "run-parts: executing /usr/share/netfilter-persistent/plugins.d/15-ip4tables save\n" +
196196
"run-parts: executing /usr/share/netfilter-persistent/plugins.d/25-ip6tables save", nil
197197
}
198-
case "service":
198+
case iptablesInit, ip6tablesInit:
199199
f.saveLocked()
200-
return "iptables: Saving firewall rules: [ OK ]", nil
200+
return "iptables: Saving firewall rules to /etc/sysconfig/iptables: [ OK ]", nil
201201
}
202202
return "", errorf("%s is not something the demo applies", cmd.String())
203203
}

‎internal/iptables/persist.go‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,19 @@ const (
1717
LayoutNetfilterPersistent = "netfilter-persistent"
1818
// LayoutIptablesServices is Fedora and RHEL's iptables-services: the
1919
// iptables and ip6tables units restore /etc/sysconfig/iptables and
20-
// ip6tables, and `service iptables save` writes them.
20+
// ip6tables, and the package's init scripts write them on `save`.
2121
LayoutIptablesServices = "iptables-services"
2222
)
2323

24+
// The init scripts iptables-services ships. `service iptables save` is only a
25+
// wrapper around them (initscripts' legacy action runs `iptables.init save`),
26+
// and `service` comes from initscripts-service, which iptables-services does
27+
// not require: calling the scripts directly works on a minimal install too.
28+
const (
29+
iptablesInit = "/usr/libexec/iptables/iptables.init"
30+
ip6tablesInit = "/usr/libexec/iptables/ip6tables.init"
31+
)
32+
2433
// Layout is one persistence layer: its name, the files it restores, and
2534
// whether systemd starts it at boot.
2635
type Layout struct {
@@ -309,12 +318,12 @@ func BuildPersist(state State) (firewall.Change, error) {
309318
}
310319
note += daemonNote(state)
311320
commands := []firewall.Command{{
312-
Argv: []string{"service", "iptables", "save"},
321+
Argv: []string{iptablesInit, "save"},
313322
Description: "Save the running iptables rules",
314323
}}
315324
if state.HasV6 {
316325
commands = append(commands, firewall.Command{
317-
Argv: []string{"service", "ip6tables", "save"},
326+
Argv: []string{ip6tablesInit, "save"},
318327
Description: "Save the running ip6tables rules",
319328
})
320329
}

0 commit comments

Comments
 (0)