Linux GUI and Apply: full parity with the Windows flow - #17
Merged
Conversation
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.
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.
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.
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-uikeeping the text report.ui.rspickslinux::bridgeorpipelineat compile time, so the workeris 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.RuleRownow carrieshits_known, and the zero-hit filter — the list a userworks 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, so a rule confined to one zone was labelled as the exact opposite
of the truth. Chips now carry the zone's own name.
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.
and quoted ~40 MB/day of security log.
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 rulemeans 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