Skip to content

Linux GUI and Apply: full parity with the Windows flow - #17

Merged
ghostpsalm merged 8 commits into
mainfrom
linux/gui
Aug 10, 2026
Merged

Linux GUI and Apply: full parity with the Windows flow#17
ghostpsalm merged 8 commits into
mainfrom
linux/gui

Conversation

@ghostpsalm

Copy link
Copy Markdown
Owner

Linux was CLI-only after #16. It now boots to the same window Windows does —
the same rule table, filters, scope chips, detail drawer and CSV export —
with --no-ui keeping the text report.

ui.rs picks linux::bridge or pipeline at compile time, so the worker
is still written once rather than branching on the OS at every call site.

The mapping that needed care

Windows ingests events: a rule with no hits was watched and found idle.
Linux reads counters, and a rule the backend could not count has no hits for
the opposite reason. Both reach the UI as usage: None.

RuleRow now carries hits_known, and the zero-hit filter — the list a user
works through deleting things from — excludes anything never measured.
Without it, every firewalld rich rule and counter-less nft rule would have
appeared as a disable candidate.

Four things the screenshots caught that no test would have

  • Scope chips rendered a firewalld zone as "ANY". ANY means every
    scope, so a rule confined to one zone was labelled as the exact opposite
    of the truth. Chips now carry the zone's own name.
  • "Last seen: never" beside a rule with 143 hits. Counters carry no
    timestamps, so it now reads "not recorded" when there are hits and "—"
    when the rule was never measured. "never" is reserved for what it means.
  • The enable screen offered to turn on Windows Filtering Platform auditing
    and quoted ~40 MB/day of security log.
  • The header counted "events" for "this run" when Linux counters are
    cumulative packets — understating the evidence rather than overstating it,
    but wrong either way.

Apply

Refused on Linux with a reason rather than silently no-oping. The Windows
path goes through Set-NetFirewallRule; changing a ufw or firewalld rule
means deleting and recreating it, which is a different operation with a
different blast radius and is not built. Whether to build it is an open
question
— it would make Firebreak able to delete firewall rules on Linux.

Verified against live firewalld under Xvfb with real traffic; host restored
afterwards. Tests 141 → 146.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rujf8EVWdDJ1cXMjssiv7Q

Linux was CLI-only. It now boots to the same window Windows does — same
table, filters, scope chips, detail drawer, CSV export — with `--no-ui`
keeping the text report. The UI worker picks `linux::bridge` or `pipeline`
at compile time, so it is still written once.

The mapping that needed care is unmeasurable-versus-idle. Windows ingests
events, so a rule with no hits was watched and found idle. Linux reads
counters, and a rule the backend could not count has no hits for the
opposite reason. Both reach the UI as `usage: None`, so RuleRow now carries
`hits_known` and the zero-hit filter — the list a user works through
deleting things — excludes what was never measured.

Four things the screenshots caught, which no test would have:

- Scope chips rendered a firewalld zone as "ANY". ANY means *every* scope,
  so a rule confined to one zone was labelled as the opposite of the truth.
  Chips now carry the zone's own name.
- "Last seen: never" appeared beside a rule with 143 hits. Counters have no
  timestamps at all, so it now reads "not recorded" when there are hits and
  "—" when the rule was never measured. "never" is reserved for what it
  actually means.
- The enable screen offered to turn on Windows Filtering Platform auditing
  and quoted ~40 MB/day of security log.
- The header counted "events" for "this run" when Linux counters are
  cumulative packets, which understated the evidence.

Apply is refused on Linux with a reason rather than silently no-oping:
changing a ufw or firewalld rule means deleting and recreating it, which is
a different operation with a different blast radius and is not built.
Verified against live firewalld under Xvfb; host restored afterwards.
Completes parity with the Windows Apply flow. The button, the confirm
dialog, the per-rule progress and the backup-first discipline are the same;
what runs underneath is not.

The mismatch worth stating plainly: Windows sets a flag the rule survives.
firewalld removes a service or port from a zone and can add it back. ufw and
nftables have no per-rule off switch at all, so disabling DELETES the rule.
`Reversibility` carries that into the confirm dialog, because a dialog that
says "disable" over a deletion is how someone loses a rule they meant to
keep. Every Apply writes a full config snapshot first and re-reads the rule
set afterwards to confirm the target actually went.

Scope edits are firewalld-only, since it is the only backend with zones.
Emptying a rule's zones is refused rather than silently performed — that is
a deletion wearing an edit's clothing — and zones are added before the old
one is removed, so a failure leaves the rule live rather than gone from
everywhere.

Every command was run against a live backend rather than derived from the
manual, which caught a real bug: `ufw delete allow port 8080 proto tcp` is
rejected outright ("Need 'to' or 'from' clause"). The unit test had asserted
my own wrong reconstruction. The correct form always carries `to any`, and
ufw removes a rule's v4 and v6 twins together — both noted in the code.

Verified: nftables handle deletion removes exactly the target; firewalld
--remove-service/--remove-port confirmed against the live daemon on an
inactive zone, restored exactly afterwards; the active zone never touched.
@ghostpsalm ghostpsalm changed the title Linux GUI: render the counter backends in the shared window Linux GUI and Apply: full parity with the Windows flow Aug 9, 2026
Reviewed marks were dropped. The bridge hardcoded ReviewState::No, so
ticking a rule off wrote to the store and then silently reset on the next
refresh — worse than not offering the feature. Marks are now loaded back and
go stale on a fingerprint change, exactly as on Windows.

Windows-only options were silently ignored. --collect, --dump-filters and
--export-support fell through to opening the window, so asking for a bundle
got you a GUI. Each now says what it is and why Linux has no equivalent.

The broadest rule on a Fedora host was the one rule nothing flagged. The
structural "broad inbound allow" check only fires when there is no port
restriction at all, so 1025-65535/tcp — 64,511 ports, shipped open by
default — sailed past it. A wide-port-span check now catches it, with the
threshold set above the privileged range so a legitimate multi-port service
is not flagged.
Found by driving the real GUI under Xvfb rather than reasoning about it: the
Apply dialog told a Linux user their backup was written to
%ProgramData%\firebreak\backups\, and claimed changes are recorded in the
audit log, which Firebreak writes nothing to on Linux.

The same run is the end-to-end proof Apply works: staging a disable, the
confirm dialog showing the destructive warning, the rule being deleted, the
other rules untouched, and the backup restoring the deleted rule when fed
back to nft.
Checking whether Defender-applied rules are distinguishable turned up a
plain bug underneath the question: `Get-NetFirewallRule` with no
-PolicyStore returns PersistentStore, which is local rules only. On a
domain-joined or Intune-managed machine Firebreak was auditing an
incomplete firewall — every Group Policy and Windows Service Hardening rule
missing from the list, and any traffic matching one of them pushed into the
unattributed bucket with no explanation.

Enumeration now asks for ActiveStore, the resultant set of local + GPO/RSOP
+ service stores, and -TracePolicyStore fills in where each rule came from.
A rule that is not Local is flagged "Managed centrally", because disabling
it here lasts only until the next policy refresh — without that, someone
switches the same rule off every week and never asks why it comes back. The
offline collector gathers the same fields so an imported bundle agrees with
a live run, and the CSV gains a Source column.

Two things this does NOT establish, both written into the verification
checklist rather than assumed: whether ActiveStore includes Intune's MDM
store (Microsoft documents it separately, and if it is excluded enumeration
must union the two), and that Defender for Endpoint's network protection is
a WFP callout in WTD.sys rather than a firewall rule — so its blocks can
never attribute to a rule and will always appear unattributed. None of this
is verifiable from Linux.
…y rows

Three things, one theme: a rule's usefulness depends on where it is defined,
and the table never said.

RuleSource makes that explicit rather than sniffing strings. Local, Group
Policy, Intune/MDM and Windows Service Hardening on Windows; the owning
manager (ufw / firewalld / nftables) on Linux, which has no policy store but
does have a subsystem that owns the rule. A new Source column shows it, bold
for anything not local, because acting on a managed rule from here lasts
only until the next policy refresh.

WFP filters that are not firewall rules now appear as read-only rows.
Defender for Endpoint's network protection, VPN clients and third-party
security software all enforce through WFP callouts, so traffic they drop
matches no rule and previously landed in the unattributed bucket with
nothing to name it. Providers are enumerated so a filter can say who owns it
instead of showing a GUID, and filters collapse by (provider, name) — a host
has thousands of WFP filters but only a handful of distinct things doing the
filtering, and a row each would bury the firewall rules the table exists to
show.

Such a row is not a rule anyone can change, so it carries no checkbox, never
reaches an apply plan, and is excluded from the zero-hit list — the one a
user works through deleting things from.

Also fixes a regression from the scope generalisation, caught by looking at
the preview rather than by a test: --ui-preview on a Linux build rendered its
Windows fixtures with an empty scope vocabulary, so the profile chips and the
Domain/Private/Public filter row silently vanished — from the screen whose
whole job is reviewing that UI.
The updater was Windows-only: WinHTTP for transport, a .exe asset name, and
the signature check itself gated behind cfg(windows). Now every platform
publishes its own asset to the same release, so a Linux host downloads
firebreak-linux-x86_64 rather than an .exe, and the verification gate is
shared rather than duplicated — there is one place that decides whether a
download is trustworthy, and it fails closed on both.

Linux transport is curl spawned by absolute path rather than a linked HTTP
client. This binary runs as root; adding a second TLS stack to it to save a
subprocess is a poor trade, and the tool already reaches system tools this
way. --proto =https and --proto-redir =https stop a redirect downgrading the
transport.

Two things that would each have broken the upgrade silently: a downloaded
file is not executable on Unix, so without an explicit chmod the swap
succeeds and the *next* launch fails looking like a corrupt update; and REPO
had the wrong capitalisation for the actual repository.

--check-update and --update give both platforms a CLI path. On a headless
Linux server the About box is unreachable, so a GUI-only updater is no
updater at all.
Three components, with the last as the release counter that simply goes up.
What the app reports is now exactly what Cargo and the release tag say: the
old fourth component was the git commit count, so the running version could
not be predicted from the source tree or typed from memory.
@ghostpsalm
ghostpsalm merged commit 5956b62 into main Aug 10, 2026
1 check passed
@ghostpsalm
ghostpsalm deleted the linux/gui branch August 10, 2026 03:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant