Releases: aafeher/nftui
nftui v1.1.0
Makes the TUI usable on small terminals and hardens the supply chain. Adds an 80x24 minimum with frame clamping and a resize prompt, alternate-screen rendering, scroll-to-focus in the rule editor and scrolling in the rule view, and a compact chain header; fixes quitting flushing the live ruleset and the rule list rendering every rule twice; and lands a security/CI wave — OpenSSF Scorecard, CodeQL, Codecov, Go fuzz targets, SHA-pinned actions, and hardened workflow token permissions.
Added
- Code coverage reporting via Codecov. The CI
build-and-testjob now writes a unit coverage profile (go test -race -covermode=atomic -coverprofile) and uploads it to Codecov (flagunit); theintegration-testjob uploads its live-netlink coverage profile too (flagintegration). Uploads use theCODECOV_TOKENrepo secret and are non-fatal (fail_ci_if_error: false), so a Codecov hiccup never blocks the build gate. A coverage badge was added to the README header. - OpenSSF Scorecard supply-chain security analysis (
.github/workflows/scorecard.yml). Theossf/scorecard-actionscores the repo against security best-practice checks (pinned dependencies, signed releases, token-permission hygiene, dangerous-workflow patterns, SAST, branch protection, …), uploads a SARIF report to the GitHub code-scanning (Security) tab, and publishes the result to the OpenSSF API (publish_results: true) that backs the new README Scorecard badge. Runs on pushes tomain, a weekly schedule, and branch-protection-rule changes. - CodeQL static analysis (SAST) for the Go code (
.github/workflows/codeql.yml). GitHub's CodeQL engine analyzes nftui's own source (autobuild mode, Go toolchain fromgo.mod) and reports findings to the code-scanning (Security) tab — complementing thegovulncheckjob, which covers known-vulnerable dependency calls rather than first-party bug/security patterns. Runs on push / PR tomainanddevelopplus a weekly schedule; workflow-level permissions are read-only, withsecurity-events: writegranted only on the analysis job. Closes the OpenSSF Scorecard SAST finding. - Go native fuzz targets for the package's least-trusted string parsers (
nft/fuzz_test.go):FuzzValidateIdentifierasserts the injection-safety invariant (any accepted name is non-empty, within the length bound, letter-led, and free of nft-script metacharacters) andFuzzParseSetElementKeydrives the set-element key parser across every key type, asserting it never panics and only ever returns correctly-sized element bytes (coveringparseIP4/parseIP6/parseInetService/parseUintBE/cidrToRange/dashRangeToBytes). The seed corpus runs under the normalgo testgate; extended runs usego test -run='^$' -fuzz=… ./nft/. Continues the E-series "don't crash on untrusted input" hardening and closes the OpenSSF Scorecard Fuzzing finding. examples/example-host-firewall.conf— a hardened, good-practice single-host firewall (not a router): oneinettable covering IPv4+IPv6, stateful filtering with invalid-drop, loopback anti-spoofing, ICMPv6 NDP allowed, rate-limited ping and SSH, exposed services (22/80/443) with default-deny inbound, unrestricted outbound, and forwarding denied. Verified withnft -c -f. Mentioned in the README "Example ruleset" section.
Fixed
- Views no longer scroll their top off-screen on short terminals. nftui uses no alternate screen, so a frame taller than the terminal made it scroll and pushed the title/header/tabs off the top (a chain view measured 27 lines at a 24-row terminal).
MainWindow.Viewnow clamps every frame to the terminal size (MaxWidth/MaxHeight), so the top always stays visible, and below the supported minimum of 80x24 a centered "Terminal too small — resize" prompt replaces the broken layout. The global quit-confirmation dialog is now rendered as its own terminal-sized frame (quitConfirmView) so the clamp can't hide it — previously it was stacked below the base view in a two-screen-tall frame, which the clamp would have truncated away. A new## Requirementsnote documents the minimum. Pinned byTestMainWindow_TooSmallGuard,TestMainWindow_FrameFitsTerminal, andTestMainWindow_QuitConfirmVisible. - The chain view fits the terminal; its footer is no longer clipped at the minimum size. The chain's metadata header was 14 fixed lines (Chain / Table / Type / Hook / Priority / Default policy on their own lines + a multi-line "Rules by type" block), which at 80x24 left room for only one rule and overflowed the frame by ~3 lines. The header is now compact — name on one line, all metadata (table·family·type·hook·priority·policy) on a second, and the rule-type counts on a third — and the content box is budgeted so the frame fits exactly (3 rules visible at 80x24 instead of 1).
headerLines()andmaxVisibleRules()were updated to match. Pinned byTestChainView_FitsTerminal(plus updatedTestChainView_MaxVisibleRules/TestChainView_HeaderLines). - The TUI now runs on the alternate screen.
tea.WithAltScreen()makes nftui a proper full-screen app that restores the terminal and the user's scrollback on exit instead of leaving its last frame behind. Frames are clamped to the terminal size, so nothing overflows the alternate buffer. - The rule editor's tall tabs now scroll to keep the focused field visible. On a short terminal the General (and other) tabs render more fields than fit, and the field area didn't scroll — only the top was visible and
Tabcould move focus to a field below the fold that you couldn't see. The editor now keeps the title and tab bar fixed and scrolls the field area so theTab/Shift+Tab-focused field always stays fully on screen (the focused field is located via an invisible sentinel injected by a newfviewhelper; the body is windowed to the box height). Pinned byTestRuleEdit_ScrollToFocus. - The read-only rule view scrolls for rules taller than the screen. It has no focus to follow, so it gained explicit
↑/kand↓/jscroll keys (shown in the footer): the title and tab bar stay fixed while the detail body scrolls, with the offset clamped at both ends and reset on tab switch, so a large rule no longer hides its lower half. Pinned byTestRuleView_Scroll. - Quitting no longer flushes the live ruleset (data loss). Confirming the "Are you sure you want to quit?" dialog called
nft.FlushRules()before exiting, wiping the entire kernel ruleset on every quit — a direct violation of nftui's contract to never mutate state the user didn't explicitly change. Exit now simply quits; no kernel call. Regression-pinned byTestMainWindow_QuitDoesNotFlush(bug B-2 in ROADMAP). - Chain view no longer renders every rule twice. The rule list printed each rule on two lines — once via
nft.RuleToHumanReadableand once vianftserializer.SerializeRule. Only the canonicalRuleToHumanReadableform (the one the rule view and the filter already use, with set resolution) is kept;ruleEntryLinesshrank 4 → 3 so the visible-window math stays correct. Pinned byTestChainView_RuleRenderedOnce(bug B-1 in ROADMAP).
Security
- Hardened GitHub Actions token permissions in the release workflow: the workflow level now defaults to read-only (
contents: read) and the write scopes (contents/id-token/attestations) are granted only on thegoreleaserjob that needs them, shrinking the token blast radius. No behavior change — the single release job still gets exactly the scopes it requires (closes the OpenSSF Scorecard Token-Permissions finding). - Pinned all build dependencies by digest. Every GitHub Action across
ci.yml/release.yml/scorecard.yml/codeql.ymlis now referenced by full commit SHA with a# vNcomment (so a moving tag can no longer change what runs), the Dockerfile base images are pinned by digest (golang:1.25.8-alpine@sha256:…,alpine:3.22@sha256:…), and the CIgovulncheckinstall is pinned to@v1.4.0instead of@latest. Dependabot gains adockerecosystem so the digest pins still receive update PRs (it already bumps the SHA-pinned actions via thegithub-actionsecosystem). Closes the OpenSSF Scorecard Pinned-Dependencies finding.sigstore/cosign-installeris held at v3 (cosign 2.x) and excluded from Dependabot major bumps — cosign 4.x'ssign-blobdefaults break Goreleaser'ssigns:block — until that block is made cosign-4 compatible. - Expanded
SECURITY.mdwith a concrete response and disclosure policy: a direct "Report a vulnerability" link, response SLAs (acknowledgement within 3 business days, triage within 7 days), a 90-day coordinated-disclosure window, a published-advisory link, and a supported-versions table. Strengthens the OpenSSF Scorecard Security-Policy signal (it scores a policy higher when it contains links and concrete disclosure timelines).
nftui v1.0.0
The first stable release. Broadens the installation paths (Debian / RPM, Alpine / Arch / OpenWrt packages, a Docker image, plus community Gentoo / AUR references), proves the reproducibility and Nix-flake claims v0.9.0 only asserted with dedicated CI lanes, makes the binary self-identifying (--version), ships a Go-module dependency tarball for offline source builds, and closes the last renderer gap (IPv6 source / destination addresses). Everything in v0.9.0 plus the full post-v0.9.0 candidate pool.
Added
--versionCLI flag (v1.0.0 item V-1):nftui --versionprintsnftui <version>to stdout and exits 0. The release version is injected at build time via Goreleaser-ldflags '-X main.version={{ .Version }}'; a source build falls back to the Go build-info module version (set forgo install <module>@vX.Y.Z), and finally todevfor a plaingo build. Pre-scanned beforeflag.Parse(like--help), so it works regardless of other flags; surfaced in--helpoutput and the man page OPTIONS. PureresolveVersion/writeVersionseams inflags.goare unit-tested.- Debian / RPM packages (v1.0.0 item V-2): every release now attaches
.deband.rpmpackages foramd64andarm64, built from the samenftuibinary as the archives via annfpms:block in.goreleaser.yaml(nfpm, pure-Go — norpmbuild/dpkg-devneeded). They install the binary to/usr/bin, the man page to/usr/share/man/man1, and the docs under/usr/share/doc/nftui, and declare a runtime dependency onnftables(nftui shells out tonft(8)for--configload and table/chain rename). The packages are folded intochecksums.txt, so the existing keyless cosign signature over the checksum file covers them too. New README "Prebuilt packages" install subsection. - Alpine, Arch, and OpenWrt packages (v1.0.0 item V-3): the
nfpms:block now also emits.apk(Alpine),.pkg.tar.zst(Arch — via nfpm'sarchlinuxformat, installable withpacman -U, no AUR account needed), and.ipk(OpenWrt/opkg) — five package formats total, all from the samenftuibinary and folded intochecksums.txt. OpenWrt's migration fromopkgtoapkmeans the.apkshould also serve newer apk-based OpenWrt on matching architectures. nftui deliberately does not auto-publish to the AUR; instead a community-maintainable referencepackaging/aur/PKGBUILD(a-binpackage over the release tarball) is provided, and the release.pkg.tar.zstinstalls natively without the AUR. README "Prebuilt packages" subsection expanded with a per-format install table. Community-maintainable reference packaging for source distros is also provided on a not-published basis:packaging/aur/PKGBUILD(Arch-bin) and a Gentoo packaging set underpackaging/gentoo/—nftui-0.9.0.ebuild(from source viago-module.eclass) andnftui-bin-0.9.0.ebuild(prebuilt binary), which block each other since both install/usr/bin/nftui, a sharedmetadata.xml, and aREADME.mdcovering overlay setup and the source build's dependency-tarball requirement. - Reproducible-build CI check (v1.0.0 item V-4): a new
reproducibilityjob in.github/workflows/ci.ymlbuilds the release binaries twice withgoreleaser build --snapshotand fails if the two differ, verifying that themod_timestamp+-trimpath+CGO_ENABLED=0build is byte-for-byte reproducible (only the compiled binaries are compared; archive / package containers are out of scope). Gated on the unit-test job so it skips a non-compiling commit. Validated locally: two independent builds produced identicalamd64andarm64binaries. - Nix flake CI lane (v1.0.0 item V-5): a new
nixjob in.github/workflows/ci.ymlinstalls Nix (cachix/install-nix-action) and runsnix flake check+nix build .#default, buildingflake.nixend-to-end on every push — closing the gap that the flake was hand-validated against thebuildGoModuleschema in v0.9.0 but never actually built. Gated on the unit-test job. One-time bootstrap:flake.nixshipsvendorHash = lib.fakeHash, so the first CI run fails and prints the realsha256-...to pin intoflake.nix; the lane then guards the hash againstgo.sumdrift. - Go module dependency tarball in the release (v1.0.0 item V-7):
.github/workflows/release.ymlnow generates a reproduciblenftui-<ver>-deps.tar.xz(viascripts/gen-deps-tarball.sh— theGOMODCACHEfromgo mod download -modcacherw, packed with deterministictar+ single-threadedxz, mtime pinned to the tagged commit) andgh release uploads it to each release from v1.0.0 onward. It makes offline source builds work without a maintainer-hosted tarball — chiefly the from-source Gentoo ebuild, whosego-module.eclassforbids network access during the build. Integrity is rooted ingo.sum(every module hash is pinned, andgo.sumships in the repo + source archive) and the tarball is covered by the SLSA build-provenance attestation (gh attestation verify); it is not added tochecksums.txt(cosign has already signed that by then). Verified locally to be byte-reproducible across two builds. - IPv6 source/destination address rendering (v1.0.0 item V-6): the rule renderer's
payloadToHumanReadablepath now recognizes IPv6saddr/daddr(the 16-byte network-header fields at offsets 8 / 24) and renders them with theip6qualifier and a proper v6 address / CIDR — previously these fell through to a rawpayload[network header+8:16] == 0x…hex form. Covers exact addresses, byte-aligned / bitwise CIDRs, and anonymous-set forms (ip6 saddr { … }). Test-first:payloadToHumanReadable+ruleToHumanReadableWithSetsunit cases, new manual-test fixtures inexamples/example-nftables-01.conf(ip6 ip6_hdr_demo), and a root integration test (TestIntegration_IPv6AddressRoundtrip) round-tripping the forms through the live kernel. Resolves the v0.9.0 audit follow-up L-6. - Docker image: a root
Dockerfile(multi-stage —golang:1.25.8-alpinebuild,alpine:3.22runtime) produces a small (~17 MB) image that bundles thenft(8)CLI nftui shells out to at runtime; the version string is injectable via--build-arg VERSION=<tag>into the same-ldflags '-X main.version=…'used by the other release paths (defaults todev). Because nftui manages the host ruleset, the container is run with--network host --cap-add NET_ADMINand an interactive TTY. Adocker-compose.ymlwires the same options up (docker compose run --rm nftui), and a.dockerignorekeeps the build context to the Go sources + man page. Verified locally: image builds,nftui --versionprints the injected string, the bundlednftresolves, anddocker compose configvalidates. New README "Docker" installation subsection. This was previously a declined packaging candidate; reconsidered and added at the user's request.
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 table...