Skip to content

firewall: switch to nftables - #3817

Open
mkg20001 wants to merge 17 commits into
freifunk-gluon:nextfrom
ffgraz:nft-v2
Open

firewall: switch to nftables#3817
mkg20001 wants to merge 17 commits into
freifunk-gluon:nextfrom
ffgraz:nft-v2

Conversation

@mkg20001

Copy link
Copy Markdown
Member

New PR superseeding #2783

mkg20001 and others added 14 commits August 23, 2026 22:24
…firewall-*

Rename all firewall packages to implementation-neutral names before
switching the implementation from ebtables/iptables to nftables:

  gluon-ebtables                   -> gluon-firewall
  gluon-ebtables-filter-multicast  -> gluon-firewall-filter-multicast
  gluon-ebtables-filter-ra-dhcp    -> gluon-firewall-filter-ra-dhcp
  gluon-ebtables-limit-arp         -> gluon-firewall-limit-arp
  gluon-ebtables-source-filter     -> gluon-firewall-source-filter
  gluon-iptables-clamp-mss-to-pmtu -> gluon-firewall-clamp-mss-to-pmtu

The corresponding image-customization feature names change from
ebtables-* to firewall-*. No functional changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rQSpgPimcLD7qJnT2sQLA
…bles/firewall4

Replace the ebtables init script with an upgrade script that assembles
a bridge-family nftables table ('bridge gluon') from Lua rule snippets
in /lib/gluon/nftables and registers it as a firewall4 nftables
include (ruleset-prepend). Rule files can also register additional
fw4 includes via include().

The base chains mirror the previously used ebtables chains: input,
forward and output run at filter priority, prerouting at dstnat
priority (equivalent to the ebtables nat table).

The multicast direction chains (IN_ONLY, OUT_ONLY, MULTICAST_*) move
into the new gluon-firewall-multicast package, keeping gluon-firewall
a mesh-agnostic framework. Unlike the ebtables version, the IN_ONLY/
OUT_ONLY drop rules only apply to packets on br-client, matching the
previous RETURN policy for other bridges.

Rules are (re)generated on gluon-reconfigure; the loader runs as
800-firewall so it sees configuration staged by earlier upgrade
scripts (e.g. gluon_bat0.gw_mode from 310-gluon-mesh-batman-adv-mesh).

firewall3 and the legacy iptables packages are dropped from the
default package selection in favor of firewall4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rQSpgPimcLD7qJnT2sQLA
The iptables OUTPUT DNAT rule redirecting DNS queries of the
gluon-mesh-vpn group to gluon-wan-dnsmasq becomes an nftables
ruleset-append include with its own ip-family table, as firewall4
provides no output-hook NAT chain.

The stale iptables include is removed from existing configurations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rQSpgPimcLD7qJnT2sQLA
The ip6tables mangle FORWARD TCPMSS rule becomes a firewall4
chain-append include on mangle_forward, still restricted to IPv6 like
the previous family=ipv6 include. The stale iptables include is
removed from existing configurations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rQSpgPimcLD7qJnT2sQLA
Straight translation of the ebtables rules. The DROP policy of the
LOCAL_FORWARD chain becomes an explicit final drop rule, added after
all allow rules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rQSpgPimcLD7qJnT2sQLA
Translation of the ebtables rules, including the meshtastic allow
rules and the Gluon VXLAN multicast group drop.

Notable differences:

- nftables cannot compare two packet fields, so the bridge loop
  avoidance rules match only on the BLA group MAC destination and
  drop the --arp-gratuitous check.
- masked MAC matches need the value pre-masked, as nftables compares
  against the literal constant.

The package now depends on gluon-firewall-multicast, which provides
the MULTICAST_OUT chains.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rQSpgPimcLD7qJnT2sQLA
Straight translation of the ebtables rules. The package now depends
on gluon-firewall-multicast, which provides the IN_ONLY/OUT_ONLY
chains.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rQSpgPimcLD7qJnT2sQLA
Straight translation of the next-node isolation, RA input/output and
brmldproxy MLD report allow rules. The next-node PREROUTING rules run
in the prerouting base chain, which uses dstnat priority like the
previous ebtables nat table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rQSpgPimcLD7qJnT2sQLA
The daemon now manages a regular chain in the bridge gluon table:
instead of flushing the ebtables RADV_FILTER chain (policy DROP) and
adding an ACCEPT rule for the chosen router, it flushes the
radv_filter chain and adds a single 'ether saddr != <router> drop'
rule. As before, the chain accepts everything until a router has been
chosen, and is reset to accept-all on daemon shutdown.

The respondd provider parses the gateway MAC from the nft chain
listing instead of the ebtables listing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rQSpgPimcLD7qJnT2sQLA
The daemon now maintains two plain nftables sets (limitmac with the
client MACs from the translation table, datips with the DAT cache
IPs) instead of per-address ebtables rules. The rate limiting itself
happens in a static chain: a dynamic set keyed by source MAC provides
the per-client 6/minute limit for members of limitmac (matching the
previous per-client ebtables limit rules), followed by the node-wide
1/second limit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rQSpgPimcLD7qJnT2sQLA
Update the developer and user documentation for the nftables-based
firewall and drop the ebtables luacheck configuration, as no ebtables
rule files remain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rQSpgPimcLD7qJnT2sQLA
olsr-site enables the firewall filter features, but all three depend on
gluon-mesh-batman-adv, so configuring an olsrd image fails. It also
sets next_node.ip4 without a prefix4, which gluon-client-bridge's
check_site requires in that case - image assembly then fails in that
package's post-install script.

Neither showed up so far because no CI job builds this site config.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RypBvVYcqtWU1E88btmLA
firewall4 depends on nftables-json, while the alias added with the
nftables migration pointed at nftables-nojson - the two variants
conflict, so configuring an image with the firewall stack failed with
"unable to enable package 'nftables-nojson'".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RypBvVYcqtWU1E88btmLA
(cherry picked from commit d48ac7d)
800-firewall and 800-clamp-mss-to-pmtu were installed mode 644, so
gluon's upgrade runner skipped them without a word: no firewall4
includes were registered, the bridge gluon table was never created and
the whole gluon firewall layer - multicast egress filtering, RA/DHCP
direction filters, ARP rate limiting - was missing at runtime on every
nftables image. bridge.nft held nothing but its generated header.

Also give the drop rules a counter, so what the firewall does is
visible on the device the same way the ebtables backend has always
been.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RypBvVYcqtWU1E88btmLA
@mkg20001 mkg20001 changed the title firewall: switch to nftables - #2783 firewall: switch to nftables Aug 24, 2026
The gluon-ebtables-* -> gluon-firewall-* rename left three :doc: refs in
historical release notes pointing at documents that no longer exist,
which Sphinx reports as ref.doc warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdNkzDnm8hEvgmPFS4b6Qs
@mkg20001

Copy link
Copy Markdown
Member Author

@neocturne I've created a new PR. Regarding the uci-less way: I would almost prefer wiping /etc/config/firewall on reconfigure and only keeping user rules with a special prefix user_* or similar, as we already have packages adding firewall rules and removing the package keeps the firewall rule arround.

We can't really avoid uci as we need to inject into chains of firewall4 occasionally and those could be injected via extra rules in /etc/nftables.d but since firewall4 manages cleanup of it's own table I'm not sure how stable that would be

mkg20001 and others added 2 commits August 25, 2026 00:52
Renaming the packages to gluon-firewall-* renamed the feature flags
with them, and an unhandled flag falls through to 'gluon-' .. flag, so
every site that still asked for ebtables-filter-multicast and friends
stopped resolving to any package at all - a build failure with nothing
pointing at the rename. Every existing site config had to change in
lockstep with the Gluon update.

Feature definition files gain deprecated(old, new), which substitutes
the new flag for the old one and warns. Sites keep building unchanged
and can migrate on their own schedule.

The substitution happens as the definition files are evaluated, so it
covers both the default gluon-$flag fallback and any _'flag' test that
comes after it - hence the deprecations at the top of package/features.

Verified against the real thing: Freifunk Lübeck's site config, which
still names all four ebtables features, resolves to exactly the package
set the firewall-* names produce.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RypBvVYcqtWU1E88btmLA
fw4 rebuilds 'table bridge gluon' on every reload, which empties the
limitmac and datips sets. gluon-arp-limiter fills those from batman-adv's
translation table and DAT cache and only calls nft when its own view of
them changes, so it never refills a set emptied behind its back.

The visible symptom is a failed delete for an element that is long gone:

  gluon-arp-limiter: Error: Could not process rule: No such file or directory
  gluon-arp-limiter: delete element bridge gluon datips { 10.130.21.212 }

The quiet half is worse. With limitmac empty, 'arp saddr ether @limitmac'
cannot match, so the per-client ARP rate limit stops applying and only
the node-wide 1/second limit is left. On a node two reloads into its
uptime, limitmac held nothing while batman-adv listed eight clients.

fw4 runs script includes after the new ruleset is in place, which is
exactly where the daemon has to be told to start over. Verified on a
running node: straight after a reload both sets read 0, and the hook
brings them back to 82 DAT entries and 2 client MACs, matching
batman-adv.

The existing reload.d hooks do not cover this - they run on a gluon
reload, while this is triggered by anything that reloads the firewall,
an interface coming up included.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RypBvVYcqtWU1E88btmLA
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant