ZFW v1.0.26 — blocklist feeds, INPUT chain position, and an outbound chain that would not go away
LatestBlocklist feeds, the INPUT chain position, and an outbound chain that would not go away
A rule can take a blocklist feed as its source
A feed is a third-party list of networks known for abuse, kept in /DATA/zfw/feeds/
and matched as an ipset. The catalogue is fixed in the daemon — currently
FireHOL Level 1 and
Spamhaus DROP — and there is deliberately no URL
field: a feed decides what your firewall drops, and a URL you can edit would hand
that decision to whoever controls the far end.
Said plainly, because it is easy to oversell: on a default-deny host chain a feed
adds nothing. Everything not explicitly allowed is dropped anyway. Feeds earn their
keep on ports you open to the world, on published container ports, and outbound —
a deny rule with direction outbound and a feed source stops a compromised container
from reaching known-bad infrastructure. Behind a Cloudflare or Pangolin tunnel a feed
sees only the tunnel's address, exactly like geo-blocking.
Guardrails, measured rather than assumed. FireHOL Level 1 ships 0.0.0.0/8,
10.0.0.0/8, 100.64.0.0/10 (Tailscale), 127.0.0.0/8, 169.254.0.0/16,
172.16.0.0/12, 192.168.0.0/16 and 224.0.0.0/3. Loaded verbatim it would drop
every LAN and Tailscale packet, so special-use ranges are removed unconditionally —
and so are the LAN and host address from rules.json and every sync peer, because a
feed that happened to list your own public range must not be able to lock you out.
GET /api/feeds reports how many entries were removed on each account.
Refresh never touches the rules. Every 12 hours (ZFW_FEEDS_REFRESH, 0
disables) and on demand via POST /api/feeds/refresh, the new content is loaded into
a temporary set and swapped in, so compiled.sh and the live chains stay byte for
byte what they were. The swap is not taste: loading into the live set directly leaves
it half-filled for a moment — measured, 111 000 of 200 000 entries mid-load.
The Firewall tab gains a status card: entry count, packets matched, next refresh and
Update now.
The status grid says where ZFW sits in INPUT
ZFW inserts itself first and never shoves back, so a tool installed later — another
blocklist module, a VPN's own chain — can end up ahead of it. Harmless if it is
stricter, a silent bypass if it accepts. The Firewall tab now names what runs ahead,
the Audit tab carries it as finding M9, and ZFW does not fight for the slot.
Fixed: deleting the last outbound rule left the chain running
Present since v0.5.6, found by testing the feed apply path on a real host. Both
emitters skipped the whole outbound block when no outbound rule existed — correct for
a host that never had one, wrong for a host where one was just removed: ZFW-OUT
stayed populated and stayed hooked into OUTPUT. The kernel kept dropping traffic
that no rule asked for, and neither the UI nor rules.json said so. Both paths — the
line-by-line script and the atomic iptables-restore one the engine prefers — now
unhook, flush and delete the chain when it is no longer needed, each with a test that
fails without it.
Also fixed: build.sh cleaned tarballs but not sbom.json, so on a host without
cyclonedx-gomod the fallback quietly packed the previous release's SBOM. A
missing tool now produces a missing SBOM rather than an outdated one.
Verified on a ZimaOS host, not only in tests
ZimaCube, ZimaOS, kernel 6.18.9, iptables 1.8.11 (nf_tables), ipset 7.24:
-m set --match-setmatches under the nft backend, including insideDOCKER-USER—
counter-only probe rules, set-match and a plain-sbyte-identical at 105 packets /
6795 bytes, negative control 0.- Saving a feed rule fetches and renders it (4691 raw lines − 33 comments − 8
special-use = 4650 networks, loaded into the kernel in 10 ms) and applies nothing. - Applying installs the ipset and the
match-setrule; removing the rule takes both
away again.