Repository navigation
Per-rule disable/enable with a persisted spec - #17
Merged
Merged
Conversation
nftables has no disabled state: a rule is in the ruleset or it is not. So disabling can only mean deleting the rule while remembering enough to put it back exactly where it was, and that memory has to survive a reboot or the feature is a delete with a friendly name. The memory goes in the file the Save action already writes, as comment lines `nft -f` ignores and this tool parses. The live half stays nft's own listing, byte for byte, so the boot path is untouched and a machine that loses this tool still boots the rules it had; a file written before this existed still loads, and a ruleset with nothing disabled still writes that same v1 file, so nobody who does not press D sees the format at all. D disables the selected rule and D again puts it back at the recorded index. The statement replayed on the way back is captured from the live match when the rule was disabled, by the same rebuild a move uses, so the refusals are the ones E already makes: a rule this tool does not hold in full is refused before anything is deleted. The disabled rule stays on screen as a greyed-out row drawn from the spec rather than from the kernel. Disabling does not join a staged batch. The ruleset change and the record of it have to agree, and a batch defers the one past the other; so it applies on its own and then offers the save, with the header saying `1 disabled, unsaved` until that save happens. Everything read off disk is guarded before it becomes an nft statement or a table cell, with two fuzz targets on the invariants. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCypLQF5AD8JD831Ea9S9q
The Save preview diffs the capture against the file at the save path, and only one of those two is this tool's own output. The other is whatever is on disk, and the diff is a quadratic longest-common-subsequence over lines: a file that is not a table listing — a log somebody redirected there, a ruleset a hundred times this tool's own — asks for a table quadratic in its size before a character is drawn. CodeQL flags the allocation, and it is right to. Past the bound the diff degrades to "the whole file is replaced", which is what installing over it does anyway. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCypLQF5AD8JD831Ea9S9q
CodeQL follows the line count of a file this tool did not write into the allocation and does not read the product of two int64s as a bound on either. A plain per-side limit says the same thing and is the same fix. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCypLQF5AD8JD831Ea9S9q
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
nftables has no per-rule off switch: a rule is in the ruleset or it is not. So
Ddisables a rule by deleting it and remembering enough to put it backexactly where it was — and that memory has to survive a reboot, or the feature
is a delete with a friendly name.
The format
The record goes in the file
Walready writes, as comment linesnft -fignores and this tool parses:
untouched and a machine that loses this tool still boots the rules it had.
so nobody who does not press
Dsees the format at all, or a diff for it.is refused while it stands: overwriting a file whose disabled rules could not
be read would delete them.
A sidecar file was the other option and was rejected — two files drift, the
boot path would have to learn about the second one, and copying
tui-firewall.nftto another machine would silently leave half theconfiguration behind.
DESIGN.mdcarries the reasoning in full, and replacesthe "not implemented" note it used to hold.
Behaviour
Ddisables;Don the greyed-out row enables, replaying the capturedstatement with
nft insert rule … index N(oradd, when the chain hasbecome shorter since). Fresh handle, counter at zero, and the preview says so.
machinery a move uses, so the refusal set is the one
Ealready makes: anunmodelled match, a NAT translation, a verdict other than accept/drop/reject,
a log statement carrying a level or an nflog group. Refused before anything
is deleted.
STATEcolumn that appearsonly once something is switched off. It is drawn from the spec, not from the
kernel.
have to agree, and a batch defers the one past the other. It applies on its
own as one nft command, refused while staging collects or a batch awaits its
keep, and then offers the save — with
rules: 1 disabled, unsaved (W)in theheader until that save happens.
d,E,K/Jandlon a disabled row say so instead of failing on ahandle that does not exist.
Verification
make checkgreen (vet, exec boundary, golangci-lint 0 issues, tests),make manifestandmake readmeclean. New tests: spec round trip and v1/v2loading, disable→enable restoring the position, the append fallback on a
shrunken chain, every refusal, the injection guards on what comes off disk, and
the full
--demogesture through the UI (greyed row, header fact, auto-offeredsave, staging refusal, ufw not offering the key). Two fuzz targets —
FuzzParseSpecon the guards andFuzzSaveFileRoundTripon write/readagreement — run clean at 25s and 20s locally on top of the seed corpus.
Not done: the spec is not reconciled against a ruleset something else changed
behind this tool's back. The position past the end of a shorter chain is
handled; the rest is written up as open in
DESIGN.md.🤖 Generated with Claude Code
https://claude.ai/code/session_01MCypLQF5AD8JD831Ea9S9q