nftui v0.9.0
The release-infrastructure milestone: integration test harness for the live netlink path, GitHub Actions CI, a reproducible Goreleaser release pipeline, Nix flake packaging, and virtualized rule-list rendering for large chains. (Originally targeted as v1.0.0; renamed after a strict audit surfaced enough hardening items that bumping straight to a 1.0 stable was premature — v1.0.0 picks from the post-v0.9.0 candidate pool.)
Added
- Integration test harness under the
integrationbuild tag (nft/integration_test.go). Skips when not root, applies a uniquely-named inet table vianft -f, reads it back throughListTables/ListChainsOfTable/ListRulesOfChain, and asserts on per-rule comment text (UserData TLV) plus rendered tokens. The table is torn down int.Cleanupso a failing assertion still leaves the host clean. New README "Integration tests" subsection covers the invocation (sudo -E go test -tags=integration ./nft/ -v). The first-run output uncovered the anonymous-set renderer bug fixed below. - GitHub Actions CI workflow (
.github/workflows/ci.yml). Two jobs run on every push / PR tomainanddevelop: build-and-test (gofmt check,go vetfor the default andintegrationbuild tag sets,go build,go test -race ./...,timeout-minutes: 15) and integration-test (apt-installs thenftablespackage, thensudo -E env PATH=$PATH go test -tags=integration -v ./nft/so the harness getsCAP_NET_ADMINand the elevated process can still resolve the setup-go-provisioned toolchain,timeout-minutes: 20). The integration job is gated byneeds: build-and-testso it doesn't burn runner minutes on a commit that fails the cheap checks.actions/setup-go@v5reads the Go version fromgo.mod(go-version-file), keeping CI and the module's declared toolchain in lockstep. Aconcurrencygroup withcancel-in-progress: trueretires superseded runs on the same ref. README's new "Continuous integration" subsection documents what the workflow runs. - Goreleaser configuration (
.goreleaser.yaml) and matching release pipeline (.github/workflows/release.yml). Builds reproducible Linuxamd64/arm64binaries (CGO_ENABLED=0,-trimpath,-ldflags='-s -w',mod_timestamppinned to the commit time so byte-identical rebuilds are possible given the same Go toolchain version), bundles each binary withLICENSE,README.md,CHANGELOG.md, andman/nftui.1into atar.gz, and emits a SHA-256checksums.txt. The release workflow fires onv*tags (timeout-minutes: 30), extracts the matching## [X.Y.Z]section fromCHANGELOG.mdvia a literal-match awk one-liner (index($0, header) == 1, never a regex — so.in1.0.0can't match an arbitrary character), and passes it to Goreleaser via--release-notesso the published GitHub Release body is the curated changelog text (fallback placeholder uses${GITHUB_REPOSITORY}so the URL is correct on any fork). The pre-build hook isgo mod verify(read-only checksum assertion) —go mod tidywas deliberately rejected because it could rewritego.summid-release and silently diverge the published binary from committed source state.release.github.{owner,name}is omitted so Goreleaser auto-detects upstream from the git remote; forks can release without editing the config.goreleaser-actionis pinned toversion: '~> v2'(tracks v2.x, immune to a v3 breaking change ambushing release day), and the config uses the currentarchives.formats/archives.idsschema (migrated from the deprecated singularformat/buildsafter the second pre-tag audit; validated withgoreleaser checkagainst v2.16.0 — the same major series the workflow pulls).dist/added to.gitignoreso snapshot builds don't pollute the working tree. README's new "Release process" subsection documents the tag → publish flow and the localgoreleaser check/goreleaser release --snapshot --clean --skip=publishvalidation commands. - Nix flake (
flake.nix) packaging nftui for Nix users.packages.defaultis abuildGoModulederivation forx86_64-linuxandaarch64-linuxmatching the Goreleaser shape (CGO_ENABLED=0by default,-s -w), runs unit tests viadoCheck = true, and installsman/nftui.1to$out/share/man/man1/soman nftuiworks afternix profile install. Version derivation is timestamp-based (0-YYYYMMDD-shortRevfromself.lastModifiedDate+self.shortRev) so the Nix store path is at least date-informative; falls back to0-00000000-dirtywhen neither field is set. A proper semver source (VERSION file or--argstr version) is tracked as a post-v0.9.0 candidate.apps.defaultexposesnix run.devShells.defaultmirrors the CI toolchain —go,gopls,goreleaser,nftables(for integration tests),mandoc(formandoc -Tlint man/nftui.1).vendorHashislib.fakeHashon first commit per the standard pattern: the initialnix buildfails with the realsha256-…to paste in; the Nix path is intentionally independent of the Goreleaser release pipeline, so an outdatedvendorHashnever blocks publishing. Pinned tonixpkgs/nixos-unstablefor current Go toolchain; users on stable channels can--override-input nixpkgs <ref>. README's "Installation" section gains a "Nix flake" subsection coveringnix build/nix run/nix developand the first-build hash-pinning step. - Test-coverage pass ahead of tagging — overall unit-statement coverage lifted from 16.0% to 45.5%. Four pieces: (1)
nft/nftserializergained its first test file (ruleset_test.go, 0% → 38.6%):SerializeRulewas split into a netlink-freeserializeRuleExprscore (sets injected by the caller) so the expression dispatch is unit-testable, with table-driven cases for chain/set declaration rendering, the common expression classes, lookup fallbacks, comment TLV append, and the unknown-expr marker. (2) A generic FieldEditor contract harness (ui/field_editor_harness_test.go) drives every editor registered in the rule editor's tabs (~140) through the full FocusSlots / Focus / Update / View / Blur / Changed / Save interface, constructed from both a populated and an empty rule (ui6.6% → 42.3%), plus a whole-editor walk (F6 tab cycling, focus wrap-around, F2 save validation — the kernel-apply cmd is returned, never executed) and a value-pinning test for the verdict editor. (3) The CI integration job now writes a coverage profile (-coverpkg=./nft/...) and prints the total in the job log — the live netlink path is invisible to the unit profile, and measured 35.6% over thenfttree in a local root run. (4) Update state-machine tests for the chain create / edit dialogs (ui/chain_dialog_test.go, following the set-dialog test pattern): focus cycling with wrap-around, the kind toggle growing/shrinking the slot count, hook-option resync on chain-type change (including the invalid-hook fallback being flagged as a change), empty-name rejection, the no-op-save short-circuit that skips the kernel, and spec building for base vs regular chains —chain_create.go0% → 95.4%,chain_edit.go1.9% → 90.6%. The kernel-touching create/update cmds are returned by the dialogs but never executed in tests. - Second test-coverage wave on top of the first — total lifted to 56.5%. Rule-view render tests (
ui/rule_view_test.go: all four tabs, every action type, the CT value-type switch, network payload/meta/exthdr/SCTP blocks, the reject/log/verdict/set-action/objref/set-lookup helpers —rule_view.go2.3% → 90.1%). Table-tree state-machine tests (ui/table_tree_test.go: navigation, expand/collapse, scroll-follow, incremental search incl. match cycling, read-only guards, delete-confirm flow, row-selection messages —main_window_table_list.go17.7% → 74.7%). MainWindow router tests (ui/main_window_route_test.go: view-switch messages, reload-batch messages, tree-refresh fan-out with--tablefilter preservation, quit-confirm overlay). chainView Update/View tests (ui/chain_view_update_test.go) through a new netlink-freenewChainViewWithRulesseam —chain_view.go24.0% → 80.3%.RuleToHumanReadablegained the same injected-sets seam (ruleToHumanReadableWithSets) plus render tests; the pure parse helpers (natToAction/redirToAction/rejectToAction/queueToAction/extractValueFromCt/exthdrProtoToType) and the CT string converters (events / state / status / label-bit masks) got direct table tests;SerializeRedirect/SerializeRejectcovered in-package —nft/rule.go55.4% → 73.8%,nft/expr/ct.go53.4% → 63.5%. - Third test-coverage wave — total lifted to 61.8% (
nft/expr58.2% → 73.5%,nft51.7% → 57.2%,ui57.8% → 61.7%). The 22 zero-coverage pure serializers innft/expr/(dup / notrack / tproxy / flow_offload / hash / secmark / target / connlimit / match / socket / immediate / synproxy / queue / dynset / rt / numgen / quota / exthdr / fib / bitwise / nat / log, plusformatElementand the objref pair) got exact-output or token tests (serializers_misc_test.go,objref_test.go) — writing them surfaced the connlimit polarity bug fixed below. The table-create dialog got the same state-machine treatment as the chain dialogs (ui/table_create_test.go: focus cycle, name validation, family mapping, input routing —table_create.go0% → 85.7%).RejectFieldgot targeted tests beyond the generic harness (type-change code-rebuild with round-trip code restore, all four Save variants against the wire constants, family option/code mappings —field_reject.go38.3% → 95.8%;field_ct_helpers.go0% → 100%).setViewgained a netlink-freenewSetViewWithElementsseam (same pattern asnewChainViewWithRules) and a full Update/View suite (ui/set_view_test.go: add-prompt loop incl. parse errors and the map key/value tab flow, delete confirm, read-only guards, element/flag/hint formatting, interval-range rendering —set_view.go27.6% → 78.9%). The purenfthelpers got direct tables: chain hook/policy/type string conversions and the per-family chain-type/hook validity matrices (chain.go30.0% → 96.6%), and the set-element parsers (ParseSetElementKeybranch sweep,ParseSetElementVal,KeyTypeFromString, the supported-type lists —set.go47.6% → 68.7%). The remaining cold spots are the netlink cmd closures (nft_linux.go,main_window_stats.go) — integration-lane territory by design. - Fourth test-coverage wave — total lifted to 73.9% (
nft/expr73.5% → 92.2%,nft/nftserializer38.6% → 71.5%,ui61.7% → 73.8%,nft57.2% → 62.0%).serializeRuleExprsgot 25 more dispatch-arm cases (objref / redirect / nat / quota / dynset / match / target / connlimit / flow-offload / hash / synproxy / secmark / fib / numgen / rt / dup / notrack / tproxy / socket / bitwise / immediate / range), lifting it 34.7% → 90.9%. The CT converters and decoders got exhaustive branch sweeps (ct_decode_test.go,ct_expr_test.go):DecodeCTValueacross every key,formatCtValue,FormatDuration,ExprCtToCt's Cmp/Bitwise/Lookup/Range arms, andfillCtFieldover all ~30 key/type combinations. Thenftparse helpers got direct tables (masqToActionport-range,dynsetOpToString,exthdrFieldName,regValueFieldLabel,formatMACBytes) andruleToHumanReadableWithSetsgained 13 more render cases (CIDR-via-bitwise, set-lookup qualifier, log, dynset, connlimit, range). The biggest single win: a Save-path harness (ui/field_save_harness_test.go) that builds a rule carrying every CT expression group, forces a net change on each editor, then Saves into it — exercising the ~30 field-editorSavebodies' expr-finding loops that the contract harness left cold (most CTSavemethods jumped from <15% to 50–94%). One incomplete-code finding flagged (not fixed):nftserializer.SerializeRulesetis unreferenced dead code that hard-codes emptysets/rulesslices and needs a live*nftables.Conn, so it stays untested. - Fifth test-coverage wave, focused on the
nftpackage — 62.0% → 73.5% (overall aggregate 74.2% → 75.7%). The pure string mappers innft_linux.gogot exhaustive switch tables (logLevelToString/payloadBaseToString/verdictKindToString/KeyTypeToString/Icmpv6TypeToString—nft_linux_helpers_test.go).identifyPayloadFieldgot a one-row-per-cell table across every payload base and l4proto dispatch (ARP / IPv4 / IPv6 / ICMP / ICMPv6 / SCTP / DCCP / AH / ESP / IPComp / TCP / UDP / Ethernet —identify_payload_test.go, → 100%). The low-level set parsers got direct error-path tables (parseIP4/parseIP6/parseInetService/parseUintBE/cidrToRange/dashRangeToBytes—set_parsers_test.go) andParseSetElementKeygained an error-branch + valid/oversized-MAC sweep (47.6→95.7%). The big dispatcherNftablesToRuleDefinitiongot a kitchen-sink rule driving every type-switch arm in one pass plus targeted assertions (→ 98.8%), and thecompareToConditionfamily was finished off via direct calls:payloadCompareToCondition's bit-packed refinements (version / hdrlength / dscp / flowlabel / doff / vlan id-cfi-pcp / dccp-type + CIDR / prefix address forms —payload_compare_test.go, → 97.6%),rangeToCondition's CT and unsupported arms,ctCompareToCondition's Pattern-B normalization and reply direction, andsctpChunkCompareToCondition's presence / known-field / unknown-field arms (all → 100%).ruleToHumanReadableWithSetsgained the ip-protocol / icmp-type / iif special-cased Cmp arms plus a todo-arm sweep over the no-op render cases (78→95%), anddecodePayloadValuegot a full per-field-class table (→ 100%). The remaining cold spots are netlink-bound (RuleToHumanReadable's set-fetch wrapper,GetSets, the 0%-coverednft_linux.gocmd functions) or value-collision dead branches (ChainHookNumToStringingress/egress,parseIP6's unreachable not-IPv6 guard) — integration-lane territory by design. - Sixth test-coverage wave, across
nft/nftserializerandui(aggregate 75.7% → 76.7%).nftserializer71.5% → 76.8%:serializeRuleExprsreached 100% by adding its missing dispatch arms (Ct, standaloneCmpwith no pending register,Exthdr) plus the empty-Immediateskip and an anonymous-set-matched-by-ID lookup fallback (ruleset_test.go).ui73.8% → 75.1%: the pure code↔name mappers and formatters got exhaustive tables (dccpTypeCodeToName/icmpTypeCodeToName/icmpv6TypeCodeToName/formatICMP/formatICMPv6/arphrdCodeToName/nfprotoCodeToName/pkttypeCodeToName/parseEtherTypeText—field_mappers_test.go); the TCP-flags byte↔names round-trip (tcpFlagsByteToNames/tcpFlagsNamesToByte); every dialog/view keymap'sShortHelp+FullHelpdriven through thehelp.KeyMapinterface from zero-value instances (keymap_help_test.go, 11 keymaps); the pureparseMAChelper with all error branches plusEtherAddrField.ValidateForSavedriven through a constructed field (field_ether_test.go); andEtherTypeField.ValidateForSave(incl. the parse-error branch reachable only when the field originally held a value),MetaUintField.isCleared, and theNewUdpSportField/NewUdpDportFieldconstructor wrappers (field_validate_test.go). No production bugs surfaced (pure additive tests).nftserializer.SerializeRulesetstays untested — confirmed still unreferenced, incomplete dead code (hard-codes emptysets/rules, needs a live*nftables.Conn); the remaininguicold spots are the per-fieldSavebodies that require Select-widget manipulation to flipChanged(), and the netlink/event-loop closures. - Seventh test-coverage wave, across the root
mainpackage andui(aggregate 76.7% → 78.0%). Rootnftui44.9% → 60.3%:applyStartupOptionsreached 100% (empty-options no-op, missing---configpropagation, the--tablevalidation branch) andvalidateTableFiltergained its error path (main_test.go) — the unprivileged unit run hits theListTablesEPERM branch with the capability-advice message; the not-found branch andmainitself stay integration/entry-point territory.ui75.1% → 76.6%: the previously-noted Select-driven blocker was lifted bySelect.SetValue+ the embeddedNumberInput'sSetValue, unlockingValidateForSavebodies for the verdict / queue / nat / quota / meta-select / meta-iftype editors (field_save_validate_test.go— jump-without-chain, queue range/fanout, NAT empty/garbage/family-mismatch, zero-quota) plus theverdictKindToExprmapper. The biggest single win was direct branch coverage forapplyIPAddrSave/applyIP6AddrSave(3.4% / 3.5% → high) — every Payload→[Bitwise→]Cmp rewrite shape (in-place Cmp, Bitwise insert/remove, prepend-new, the dangling-Payload early returns) driven by calling the package-level functions with crafted rules (field_ip_addr_save_test.go). No production bugs surfaced (pure additive tests). Remaining cold spots: the per-fieldSavebodies (e.g.field_ct_count,field_log,field_nat) and the netlink/TUI-event-loop closures. - Eighth test-coverage wave — the per-field
Save()bodies inui(aggregate 78.0% → 80.7%;ui76.6% → 80.3%, crossing 80%). The contract harness drives every editor but undoes its edits, so eachSaveshort-circuited on!Changed();field_save_test.goinstead sets widget state directly (now possible viaSelect.SetValue/NumberInput.SetValue/textinput.SetValue/MultiSelect.SetValues) and Saves into rules shaped to hit each branch — insert-fresh, overwrite-in-place, and remove. Covered:CtCountField(connlimit insert-before-verdict / update),CtProtocolField&CtL3ProtoField(Ct+Cmp insert / update),MetaUintField(append / overwrite / clear-removes),QuotaField(enabled-add / disabled-strip), the CT countersCtBytes/CtPkts/CtAvgpkt(the original/reply/none direction encodings),IcmpType/IcmpCode/Icmpv6Type/Icmpv6Code(l4proto-prefix insertion + payload pair, clear),TcpFlagsField&TransportUintField&TcpDoffField(payload[+bitwise]+cmp shapes),EtherAddrField&EtherTypeField(LL payload append / update),MetaIifField/MetaIifnameField/MetaOifnameField/MetaIftypeField(meta+cmp insert / overwrite),CtExpirationField(single / range / set duration forms),IP6DscpField&IP6FlowlabelField(the split-byte bitwise encodings),DccpTypeField(dccp type bit-packed match),ExthdrField(IPv6 ext-header field match), plusextractMasqStateand the remainingLogField/MetaIifFieldValidateForSaveerror branches. No production bugs surfaced (pure additive tests). The residual uncovered code is now almost entirely the two structural blockers — the netlink/CLI I/O layer (nft_linux.go, the set/named-object mutators, theuinetlink command closures) and the Bubble Tea TUI event loop (main_window.Update,main()) — both covered by the integration suite (run as root) and manual testing by design. - Ninth test-coverage wave — extended the integration suite (
nft/integration_test.go) from the read-only readback path to the full netlink mutation path, lifting thenftpackage's coverage under the integration suite from 75.1% to 86.9% (the unit-only profile is unchanged — these tests are root- andnft-binary-gated behind theintegrationbuild tag). The previously 0%-covered mutators now run 60–90%. Four new tests, each creating a uniquely-named inet table torn down int.Cleanup(host verified clean afterward):TestIntegration_TableLifecycle(CreateTable→RenameTable→DeleteTable, plus the same-name-rename no-op andnftCLIFamily's inet branch);TestIntegration_ChainLifecycle(CreateChainregular + base, all threeUpdateChainpaths — CLI rename, the minimal-netlink policy-only update, and therecreateBaseChaindump/rewrite path forced by a priority change —DeleteChain, and the Table-less-chain guard);TestIntegration_RuleLifecycle(AddNewRuleToChainincl. the returned kernel handle,InsertNewRuleBeforefor both the idx==0InsertRuleand idx>0Position+AddRulepaths,MoveRuleUp/MoveRuleDownincl. the out-of-range no-op guards,DeleteRule); andTestIntegration_BulkReadersAndLoadConfig(LoadConfig'snft -fwrapper applied to a fixture that only creates the test's own table, thenListRulesOfTable,getAllRulesviaGetAllRulesWithAccept/GetAllRulesWithDrop, andCountRulesByType).FlushRules(nft flush ruleset, would wipe the host firewall) andLoadExamples(bundled relative-path ruleset the test cannot reliably clean up) are deliberately left uncovered, documented in the test file's header. No production bugs surfaced (pure additive tests). - CI security scanning (audit E-7): a dedicated govulncheck job in
.github/workflows/ci.ymlscans the module and the Go standard library against the vulnerability database on every push / PR — its own parallel check, failing only on a vulnerability reachable from nftui's own call graph (informational findings in unused dependency code don't break the build). A new.github/dependabot.ymlopens weekly dependency- and GitHub-Actions-update PRs as upstream releases and security fixes land, withgithub.com/google/nftablesexcluded to honour its intentional pin. README's "Continuous integration" subsection documents both. - Release supply-chain attestation (audit E-6): the Goreleaser pipeline now signs
checksums.txtwith cosign (keyless — bound to the release workflow's OIDC identity via Fulcio/Rekor, no stored private key), emits a Syft SBOM per archive, and records a SLSA build-provenance attestation (GitHub-nativeactions/attest-build-provenance) for the archives and checksums..goreleaser.yamlgainssigns:+sboms:blocks;release.ymlgainsid-token: write+attestations: writeand installscosign+syft. Validated withgoreleaser check. README's "Release process" subsection documents verification withcosign verify-blobandgh attestation verify. - Security policy (
SECURITY.md, audit E-8): a vulnerability-disclosure policy covering private reporting via GitHub's "Report a vulnerability" (no public issue, no personal email to harvest), the supported-version policy (pre-1.0 — fixes target the most recent release), and nftui's security model. Because nftui has no authentication of its own and relies onCAP_NET_ADMIN, the policy spells out scope: ruleset-injection and parser crashes on untrusted data are in scope; needing root to change the firewall, and asetcap'd binary being runnable by any local user, are out of scope (deployment-side concerns). - Optional mutation audit log (audit E-9): setting the
NFTUI_AUDIT_LOGenvironment variable to a writable path makes nftui append one JSON object per applied ruleset change (create / delete / rename table, chain and set; add / insert / move / delete / edit rule; add / delete set element; delete / reset named object;--configload; ruleset flush). Each record carries the UTC timestamp, effective UID + user, the human operator behindsudo(SUDO_USER), the operation, the target object, and the outcome (resultok/error, with the kernel's message on failure — rejected attempts are logged too) — covering the SOC 2 / PCI-DSS change-management expectation. The log is append-only (no rotation/truncation, never read back), created 0600, and fails open (a misconfigured path prints one warning and disables auditing rather than blocking firewall management). Unset/empty leaves the mutation path untouched. Implemented as a netlink-freenft.auditEventseam wired into every mutator at its kernel-commit point (puremarshalLine+ file-sink unit tests, plus an end-to-end integration test asserting real CreateTable/DeleteTable records). New README "Audit logging" section documentsNFTUI_AUDIT_LOG. - Project governance docs (audit E-10): a
CONTRIBUTING.md(development setup, the CI gate —gofmt,go veton both build-tag sets,go test -race; one-change-per-PR; tests-first; English-only; CHANGELOG append-only; the nftables netlink-encoding correctness rule) and aCODE_OF_CONDUCT.mdadopting the Contributor Covenant v2.1, with the enforcement contact set to the maintainer's public GitHub handle (no personal email, consistent withSECURITY.md).CONTRIBUTING.mdlinks only to tracked repo files (CODE_OF_CONDUCT.md,SECURITY.md,LICENSE,README.md). - Deployment-hardening documentation (audit E-11): a new README "Privilege model & deployment hardening" section spells out that nftui has no access control of its own and relies on
CAP_NET_ADMIN, then documents the two recommended ways to grant that privilege safely —sudowith a group-restrictedsudoersrule (absolute path, password-logged invocations, a--read-onlyrule for browse-only roles,SUDO_USERcaptured in the audit log) and a group-restrictedsetcapbinary (chmod 750+ dedicated group, with an explicit warning againstsetcapon a world-executable binary), plus defense-in-depth notes (audit log,--read-only, PAM-layer re-auth/MFA). This closes the "no RBAC / deployment guidance" governance gap by documenting the OS-layer controls rather than building authz into the tool.SECURITY.md's out-of-scope note now points to the section.
Changed
- Refreshed dependencies to their latest minor/patch releases:
golang.org/x/sys0.44.0 → 0.46.0,golang.org/x/net0.54.0 → 0.56.0,golang.org/x/text0.37.0 → 0.38.0,golang.org/x/sync0.20.0 → 0.21.0,github.com/mattn/go-runewidth0.0.23 → 0.0.24,github.com/mdlayher/socket0.6.0 → 0.6.1.github.com/google/nftablesis intentionally left at its pinned snapshot (see the comment on itsrequireline ingo.mod), and the Bubble Tea libraries were already current. Validated against the full-racesuite and the root integration run.
Fixed
- Deleting or moving a rule now re-reads the chain and verifies the rule still exists before mutating, instead of acting blindly on the handle from a possibly-stale view. If the ruleset changed underneath the TUI (a concurrent edit, an external
nftcommand) the operation fails with a clear "rule no longer exists — the ruleset changed; refresh and retry" message rather than a cryptic netlink error, and a move now re-plans against the current neighbour. Because nftables handles are never reused, this can't silently hit the wrong rule; the kernel transaction stays atomic, so a change in the remaining tiny race window fails cleanly without corrupting the ruleset. Surfaced by the enterprise-readiness audit (finding R3). - A netlink read failure during the initial table-tree load or when opening a chain no longer crashes the TUI.
initialTableTreeModelandnewChainViewcalledpanic(err)on any enumeration error; at startup (the tree load runs before the Bubble Tea program) this printed a raw Go stack trace instead of the styled "needs CAP_NET_ADMIN — run with sudo / setcap" advice, and opening a chain mid-run crashed the program. Both now return an error: a failed initial load is captured into the main view and rendered through the existing permission-advice path, and a failed chain open keeps the user on the tree with the error shown. Theuiandnftpackages now contain nopanic/log.Fatalin non-test code. Surfaced by the enterprise-readiness audit (finding R2). - Table / chain / set names entered in the TUI dialogs are now validated against a strict identifier allowlist (
nft.ValidateIdentifier: must start with a letter, then only letters, digits,_,.,-, max 255 bytes) before they reach the kernel or annft -fscript. Previously the dialogs only rejected an empty name, so a name containing nft-script metacharacters ({,},;, newlines, …) could inject statements into the privilegednft -f -transaction the table-rename / base-chain-recreate paths build, or silently corrupt the ruleset. Wired into all five name-entry dialogs (table create, chain create, chain edit, table rename, set create). Surfaced by the enterprise-readiness audit (finding S1). - Opening or refreshing a set no longer risks crashing the whole TUI on a transient netlink error.
nft.GetSetElementscalledlog.Fatalon any read failure, which terminated the process viaos.Exit(1)and skipped Bubble Tea's terminal restore — leaving the terminal in raw mode (the user had to runreset). It now returns([]nftables.SetElement, error):newSetViewtreats an error as an empty element list (the documented best-effort fetch for unreferenced anonymous sets) andRefreshElementskeeps the previously-loaded elements while showing the error in the status line. The netlink-free post-processing (vmapVerdictDatadecode + interval pairing) was split out into a puredecodeSetElementsseam and unit-tested directly. Surfaced by the enterprise-readiness audit (finding R1). ct countpolarity was inverted across all three layers — the TUI wrote the opposite rule to the kernel from what it displayed. nft CLI encoding (verified live vianft --debug=netlink):ct count over 5setsNFT_CONNLIMIT_F_INV(matches when the connection count exceeds N), plainct count 7leaves flags 0 (matches while count ≤ N). The codebase used the reverse convention consistently innftexpr.SerializeConnlimit(rule-list rendering),rule_view.go's CT tab, andCtCountField(both the initial read andSave— so editing a rule re-saved it with the opposite kernel semantics while the UI kept displaying the user's intent, masking the bug). All three sites flipped, the two tests that pinned the wrong convention corrected, and the live-verified encoding documented at each site. Found by the third-wave serializer tests.chainView.View()no longer re-fetches the chain's entire rule list over netlink on every render. The "N rules" stat box called a helper that did a synchronousListRulesOfChainper keystroke — and panicked the whole TUI on a transient netlink error. The count now comes from the already-fetchedc.rules(kept current byRefreshRulesafter every mutation); the panicking helper is gone. Found by the second-waveView()render test.RuleToHumanReadablenow rendersmasquerade— theexpr.Masqcase was an empty TODO, so a masquerade rule showed nothing for the masq statement in the chain's rule list. Routed through the already-testednftexpr.SerializeMasq.nftserializer.SerializeRulecould loop forever on a rule whose set lookup failed to resolve: theexpr.Lookupbranch re-fetched the set over netlink andcontinued without advancing the expression index on error, and its element fetch went through a helper thatlog.Fatals on failure — killing the whole TUI process. Rewritten to resolve the lookup against the table's already-fetched sets slice with an@<setName>fallback (mirroring theSerializeLookupfallback fixed below) and a plain error-checked element fetch. Found while writing the package's first unit tests; regression-pinned byTestSerializeRuleExprs_LookupTerminates.RuleToHumanReadablenow rendersexpr.Lookup(set lookups) instead of silently dropping them. The previous case body was commented out, sotcp dport { 80, 443 } acceptcame back as justtcp accept. New behavior: standaloneLookupreads the source-register description fromregMapand runs it throughnftexpr.SerializeLookup; thePayloadcase forsaddr/daddrgained aLookuplookahead soip saddr @blocklistkeeps theipqualifier (the standalone branch would emit a baresaddr, since theipprefix is added inline in the Payload case for IPv4 network-header reads). The integration test fixture was upgraded from scalar to anonymous-set form (tcp dport { 80, 443 }) to pin the fix and prevent regression. Unit coverage added innft/expr/lookup_test.go.nftexpr.SerializeLookupWithKey(the CT-typed sibling ofSerializeLookup, used forct state @setnamestyle lookups) received the same two fallback fixes asSerializeLookupbelow — it was missed when the original fix landed: a set absent from the caller'ssetsslice now renders as@<setName>instead of a register with trailing whitespace, and the empty-SetNamefallback lost its stray leading@(@set_N→set_N, so the post-format@prefix doesn't double up). Surfaced by the second pre-tag audit; pinned by three new unit tests innft/expr/lookup_test.go.- Second pre-tag audit cleanup, docs and comments: README's "Release history" section no longer lists v0.8.0 as "in progress" (tagged 2026-05-30) and describes the renamed v0.9.0 milestone instead of the stale v1.0.0 entry; Hungarian comments translated to English as they surfaced (
nft/expr/lookup.go,nft/expr/bitwise.go, three more innft/rule.go); two misleading comments corrected —.goreleaser.yaml(thechangelog:block is bypassed whenever--release-notesis passed; the archive'sCHANGELOG.mdcomes fromfiles:, not from that block) andflake.nix(the devShell'spkgs.gois the nixpkgs-unstable pin, not the exact go.mod toolchain version CI resolves). nftexpr.SerializeLookupno longer returns a register with trailing whitespace when the looked-up set isn't in the caller'ssetsslice. The previous code wroteelementsStringonly on a successful match — a miss left it empty, andfmt.Sprintf("%s %s", register, elementsString)produced"tcp dport ". New behavior: if no match was found and no elements were resolved, fall back to@<setName>so the rendered form is always syntactically nft-CLI shaped. ThesetNamefallback for emptyLookup.SetNamealso lost its stray leading@(was@set_N, nowset_N) so the post-format"@" + setNameproduces a clean@set_N.- Virtualized rule-list rendering in
chainView. Replaces the off-by-manymaxHeight := c.height - 20(which assumed 1 line per rule and let the cursor scroll off-screen with the rest of the entries below it clipped by the content box) with a dynamicmaxVisibleRules()that divides the available content-box height by the actualruleEntryLines = 4and subtracts aheaderLines()count computed from the chain's optional hook / priority / policy fields and the optional filter-prompt block. The render loop, the Down handler, and the filter-mode Down handler all route throughmaxVisibleRules()so a 1000-rule chain iterates the same~5–10entries as a 10-rule one. NewmatchCache map[uint64]stringonchainViewmemoizes the lowercaseRuleToHumanReadable + ExtractCommenthaystack perrule.Handle;ruleMatchesFilterreads it on every keystroke and only re-serializes on a cache miss.RefreshRulesclears the cache (handles survive a rule edit but the rendered text behind them does not). Manual-test fixture:examples/example-nftables-01.confsection 48 (table inet large_chain_demo→chain many_rules) carries 60 hookless rules withrule NN — ...comments for scroll and filter exercises. Unit tests pin themaxVisibleRules/headerLinesmath and the cache hit / no-cache-on-empty-query behavior.
Security
- Bumped
github.com/mdlayher/netlinkoff the upstream-retractedv1.11.1to the suggestedv1.11.2. A retracted version is one the author flagged "do not use"; the bump is the onlygo.modchange (the netlink transport sits under every kernel read/write, so the change was validated against the full-racesuite and the root integration run). Surfaced by the enterprise-readiness audit (supply-chain, finding E-13). - Defense-in-depth identifier validation inside the
nftpackage (audit E-12).CreateTable,RenameTable,CreateChain,UpdateChain, andCreateSetnow callnft.ValidateIdentifieron the caller-supplied name as their first step — returning an error rather than touching the kernel or building annft -f -script when the name carries script metacharacters. This complements the E-2 dialog-level validation: the guard now also covers any future caller (a new dialog, a config-import path, a programmatic API consumer) that reaches the name-interpolating mutators without going through a validated input field. Existing kernel-sourced names are not re-validated (they may be legitimately quoted identifiers); only the new, caller-chosen name is checked. Unit tests drive each mutator with an injection identifier and assert it is rejected pre-kernel; the root integration suite (valid names) stays green.