Skip to content

0.6.2: firewalld --check in one parallel wave of whole listings (#29) - #30

Merged
edimarlnx merged 4 commits into
mainfrom
fix/29-check-latency
Sep 26, 2026
Merged

edimarlnx merged 4 commits into
mainfrom
fix/29-check-latency

Conversation

@edimarlnx

@edimarlnx edimarlnx commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

tui-firewall --check took about 8 s on the lab's Fedora 44 guest (firewalld), and tui-wireguard and tui-tailscale read it on every reload.

Where the time went

0.6.1 ran firewall-cmd --version (compat probe) and then, one after another, --state, --get-default-zone, --get-active-zones, --list-all-zones, --permanent --list-all-zones, --get-services, --get-log-denied, --query-panic, --query-lockdown, --get-policies and two --policy=X --list-all reads per policy object. Fedora 44 ships 7 policy objects out of the box (allow-host-ipv6 and six gateway-*), so that is 25 processes in a row.

Per read on the lab's fedora guest (Fedora 44 Cloud, firewalld 2.4.4, 2 vCPUs), profiled with sudo -n python3 -m cProfile /usr/bin/firewall-cmd .... Wall time, D-Bus round trips (blocking calls plus proxy introspections) and the time spent waiting on them:

read wall D-Bus round trips D-Bus wait
--state 533 ms 24 45 ms
--get-default-zone 414 ms 24 47 ms
--get-active-zones 492 ms 25 74 ms
--list-all-zones 440 ms 38 59 ms
--permanent --list-all-zones 608 ms 79 180 ms
--get-services 407 ms 24 41 ms
--get-log-denied 423 ms 24 44 ms
--query-panic 417 ms 24 47 ms
--query-lockdown 379 ms 22 4 ms
--get-policies 411 ms 24 51 ms
--list-all-policies 500 ms 31 97 ms
--permanent --list-all-policies 531 ms 48 123 ms
--policy=allow-host-ipv6 --list-all 521 ms 25 57 ms
--permanent --policy=allow-host-ipv6 --list-all 467 ms 27 50 ms
--version 378 ms 22 3 ms

(profiler overhead included; without it each read is about 300 ms on this guest, and the 0.6.1 sequence sums to 7.5 s.) Around 85% of every read is the Python interpreter starting and importing the firewalld client; D-Bus is 40 to 180 ms of it. The cost is the number of processes in a row, so the fix is fewer processes and no queue.

What changed

  • Load (the one read path, used by --check and by the TUI's startup and R reload alike) now starts eight reads together: --list-all-zones and --list-all-policies, runtime and permanent, plus --get-services, --get-log-denied, --query-panic, --query-lockdown.
  • The default zone and the active zones come from the (default, active) header flags of the runtime zone listing. firewall-cmd computes those flags from the same daemon calls --get-default-zone and --get-active-zones make, and the active zones' interfaces and sources are the section's own. A firewalld that prints no flags falls back to the two queries.
  • --list-all-policies replaces --get-policies + 2 reads per policy (same bound of 32, same name check). A firewalld without it falls back to the per-policy reads, also started together.
  • --state is asked only when the runtime zone listing fails for a reason other than "FirewallD is not running": a successful runtime listing already proves the daemon is up. With firewalld stopped, each firewall-cmd waits about 10 s for the daemon on D-Bus, so the stopped case is recognised from the listing's own message instead of paying that twice.
  • --check runs the version probe and the backend survey (systemctl is-active/is-enabled) while the backend loads.
  • Still driven through firewall-cmd in internal/firewalld/ only (check-exec clean). A direct D-Bus read would be faster still but would break the "the command you see is the command that runs" rule the package doc explains, so it was not done.

Evidence

Lab (tui-lab guests fedora and ubuntu26)

--check from 0.6.1 (main) and from this branch, back to back, as the lab user with sudo -n. IDENTICAL is cmp of the two full JSON outputs:

--- fedora: Fedora Linux 44 (Cloud Edition), 2 CPUs, firewalld 2.4.4   (18 groups, 61 rules, 1859 lines of JSON)
 old rc=0  8380 ms   new rc=0  1662 ms   IDENTICAL
 old rc=0  8007 ms   new rc=0  1575 ms   IDENTICAL
 old rc=0  7779 ms   new rc=0  1704 ms   IDENTICAL
 old rc=0  8243 ms   new rc=0  1669 ms   IDENTICAL
 old rc=0  7426 ms   new rc=0  1707 ms   IDENTICAL

--- ubuntu26: Ubuntu 26.04.1 LTS, 2 CPUs, ufw 0.36.2
 old rc=0   526 ms   new rc=0   315 ms   IDENTICAL
 old rc=0   595 ms   new rc=0   366 ms   IDENTICAL
 old rc=0   498 ms   new rc=0   356 ms   IDENTICAL

On fedora the median dropped from 8.0 s to 1.67 s, under the 2 s target. Because of that, the extra cut (skipping --query-lockdown on firewalld 2.2 and later) was not needed and is not in this PR.

./lab.sh test tui-firewall fedora ubuntu26 --bin <this branch> with this branch's test/smoke.sh: fedora 21 passed, 0 failed (including the active zones are the ones --get-active-zones lists (public) and --check answers in under 3 s (took 1592 ms)); ubuntu26 10 passed, 0 failed. Compat lines: firewalld 2.4.4 on fedora-44 pass, ufw 0.36.2 on ubuntu-26.04 pass.

Before the lab was reachable (container stand-in)

Earlier I checked the same things in rootless privileged podman containers that I created and then removed. The firewalld one was Fedora 44 with firewalld 2.4.4, limited to 2 CPUs, and seeded with three zones plus services, ports, a rich rule, a bound source, masquerade, a forward port, a custom policy and entries in only one of the two scopes. The ufw one was Ubuntu 26.04. Results: --check went from 2.7 s to 1.0 s with identical JSON over 5 pairs. The output was also identical for all four --demo backends and with firewalld stopped (12.2 s before, 10.8 s after: every firewall-cmd waits about 10 s for a stopped daemon, and the new code does not wait a second time for --state). The smoke passed 21/21 and 10/10.

Unit tests: load_test.go replays captured firewall-cmd output (2.3.2 and 2.4.4 fixtures, plus the new --list-all-policies pair) through both the 0.6.1 read sequence and the new one and asserts the same firewall.Model. It also covers the fallbacks (no header flags, no --list-all-policies, no policy objects at all, zone listing refused, daemon stopped) and the read count (8, with none of the fallbacks asked).

Release notes

  • firewalld: --check and the reload in the UI read the firewall in under 2 s instead of about 8 s. The tool now asks firewalld for whole zone and policy listings and runs those reads in parallel. The output is unchanged.
  • README: the --check section says what --check costs on each backend, and that callers can keep its answer for a few seconds.

Version 0.6.2.

Closes #29.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MbCfX3T8khyd9hsrN5kMo3

edimarlnx and others added 4 commits September 26, 2026 11:37
tui-firewall --check took about 8 s on a firewalld host, and sibling tools
(tui-wireguard, tui-tailscale) call it on every reload. Each firewall-cmd
is a Python interpreter plus D-Bus calls that firewalld answers one at a
time, each checked with polkit, and Load ran 10 + 2 per policy object of
them one after another, with the compat probe's --version before them.

Load now starts nine reads together: --state, --list-all-zones and
--list-all-policies (runtime and permanent), --get-services,
--get-log-denied, --query-panic and --query-lockdown. The default zone and
the active zones come from the "(default, active)" header flags of the
runtime zone listing, which firewall-cmd computes from the same daemon
calls --get-default-zone and --get-active-zones make; a firewalld that
prints no flags, or lacks --list-all-policies, falls back to the old reads
for that part only. --check also runs the version probe and the backend
survey while the backend loads.

The parsed model is unchanged: unit tests build it both ways from
captured firewall-cmd 2.3.2 and 2.4.4 output and compare, and the smoke
test checks the active zones against --get-active-zones and bounds the
time --check takes. Version 0.6.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbCfX3T8khyd9hsrN5kMo3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbCfX3T8khyd9hsrN5kMo3
The runtime zone listing is answered only by a running daemon, so its
success already says what --state would; --state is now asked only when
that listing fails for another reason, which saves one Python process per
load. A stopped daemon is recognised from the listing's own "FirewallD is
not running", because each firewall-cmd waits about 10 s for the daemon on
D-Bus and asking --state afterwards would double that wait.

The README's --check section now states what --check costs per backend and
that callers polling it on every reload may keep the answer for a few
seconds. The smoke timing uses EPOCHREALTIME, since the uutils date on
newer distributions does not expand %3N.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbCfX3T8khyd9hsrN5kMo3
The lab's 2-vCPU Fedora 44 guest runs --check in about 1.7 s (was 8 s);
the README and the smoke comment now quote that rather than the faster
container stand-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbCfX3T8khyd9hsrN5kMo3
@edimarlnx
edimarlnx merged commit 53f2c9e into main Sep 26, 2026
8 checks passed
@edimarlnx
edimarlnx deleted the fix/29-check-latency branch September 26, 2026 18:27
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.

--check takes about 8 s on a firewalld host

1 participant