Firewall
Ze manages nftables packet filter and NAT rules from a single firewall { } YANG
section. The abstract data model describes matches (from) and actions (then);
the nft backend lowers them to nftables kernel expressions.
Backend
| Backend | Platform | Default | Mechanism |
|---|---|---|---|
nft |
Linux | ✓ | google/nftables netlink library |
vpp |
Linux with VPP | ✕ | GoVPP ACL classify pipeline and NAT44-ED |
firewall {
backend nft;
}
Clean-shutdown teardown
flush-on-shutdown controls whether stopping the ze process removes ze-owned
tables from the kernel. It defaults to true: an orderly stop (SIGTERM, e.g.
systemctl stop ze) tears the tables down so a stopped daemon leaves no rules
behind. It keys off how the process exits, and is unrelated to BGP graceful
restart -- that concerns only RIB/FIB route retention while a peer re-establishes
and never restarts the daemon, so it never reaches this path.
Set it to false to use ze as a one-shot provisioner: apply the firewall
(and interface) configuration, let the process exit, and leave the rules running
in the kernel -- the way nft -f programs a ruleset and returns. Nothing is torn
down when ze quits.
firewall {
backend nft;
flush-on-shutdown false; # one-shot: program the rules, exit, leave them running
}
A crash (SIGKILL, panic, power loss) never runs the shutdown path, so ze-owned
tables always persist across a crash regardless of this setting. The option
governs every ze table producer that shares the backend -- the firewall
component, control-plane-protection, policy-routes, and ddos-local -- and
the firewall component performs the teardown as a single ordered actor so
plugin table removal never races the backend close.
The reconcile deadline
Ze serializes the whole firewall reconcile: a snapshot of every owner's tables
followed by one Backend.Apply, under one process-wide lock. The owners are the
firewall component, control-plane-protection, policy-routes, and ddos-local.
A dataplane call that never returns therefore does not stall one owner, it stalls
all of them.
Each backend bounds one dataplane round-trip. Neither bound can be turned off. A
value below the floor or above the ceiling clamps, and an unparseable value falls
back to the default, because an unbounded call is the failure this guard exists to
prevent. The ceiling is one shared constant, firewall.MaxBackendDeadline, which
the latency histogram's last finite bucket is also derived from.
| Backend | Bounds | Environment variable | Default | Range |
|---|---|---|---|---|
nft |
One nftables netlink round-trip | ze.firewall.nft.netlink-timeout |
10s | 1s to 60s |
vpp |
One VPP binary-API round-trip | ze.firewall.vpp.reply-timeout |
10s | 1s to 60s |
The VPP backend previously ran with no bound at all. govpp's default reply timeout
is 0, which govpp documents as disabling the timeout, and Channel.ReceiveReply
has no context arm, so a wedged VPP held the process-wide reconcile lock
indefinitely.
When the deadline expires the apply fails, and ze logs an error saying the dataplane did not answer within the backend deadline and its ruleset is now behind the registry. That log sits outside the metrics path, so it is written whether or not telemetry is enabled.
A dataplane that is ABSENT is a different condition and is deliberately not reported as a timeout: VPP not running, or a connect wait that ran out, means the reconcile failed for a reason with a different fix, and there is no ruleset to be behind.
| Metric | Type | Labels | Meaning |
|---|---|---|---|
ze_firewall_apply_duration_seconds |
histogram | result |
Time spent in Backend.Apply. result is ok, timeout, error, or panic. |
ze_firewall_apply_timeout_total |
counter | Reconciles that failed because the dataplane did not answer within the backend deadline. |
The result label is what separates a healthy-but-slow apply from one that gave
up: a backend deadline of 10s and a 10s successful reconcile land in the same
latency bucket, and only the label tells them apart. Both signals derive from one
result value, so they cannot drift apart. A backend that panics is recorded as
panic rather than lost or filed as healthy.
Tables and Chains
Ze owns all tables whose kernel name starts with ze_. A table contains one or
more chains; a base chain has a type, hook, priority, and default policy.
firewall {
backend nft;
table wan {
family inet;
chain input {
type filter;
hook input;
priority 0;
policy drop;
term allow-ssh {
from {
destination-port 22;
protocol tcp;
}
then {
accept;
}
}
}
}
}
Table Families
inet (dual-stack), ip, ip6, arp, bridge, netdev.
Chain Types
filter, nat, route.
Hooks
input, output, forward, prerouting, postrouting, ingress, egress.
Match Types (from block)
| Config key | Match | Example |
|---|---|---|
source-address |
IP prefix | source-address 10.0.0.0/8; |
destination-address |
IP prefix | destination-address 192.168.1.0/24; |
source-port |
Port or range | source-port 1024-65535; |
destination-port |
Port or range | destination-port 22; or destination-port 80,443; |
protocol |
L4 protocol | protocol tcp; |
input-interface |
Interface name | input-interface eth0; |
output-interface |
Interface name | output-interface "l2tp*"; |
icmp-type |
ICMP type (name or number) | icmp-type echo-request; |
icmpv6-type |
ICMPv6 type (name or number) | icmpv6-type nd-neighbor-solicit; |
connection-state |
Conntrack states | connection-state established,related; |
connection-mark |
Mark value/mask | connection-mark 0x10/0xff; |
mark |
Packet mark value/mask | mark 0x10/0xff; |
dscp |
DSCP value (name or number) | dscp ef; |
tcp-flags |
TCP header flags | tcp-flags syn; |
source-address @set |
Named set lookup | source-address @blocked; |
ICMP Type Names
Symbolic names for icmp-type: echo-reply, destination-unreachable,
source-quench, redirect, echo-request, router-advertisement,
router-solicitation, time-exceeded, parameter-problem,
timestamp-request, timestamp-reply, info-request, info-reply,
address-mask-request, address-mask-reply. Numeric values (0-255)
are also accepted.
Symbolic names for icmpv6-type: destination-unreachable, packet-too-big,
time-exceeded, parameter-problem, echo-request, echo-reply,
mld-listener-query, mld-listener-report, mld-listener-done,
nd-router-solicit, nd-router-advert, nd-neighbor-solicit,
nd-neighbor-advert, nd-redirect, mld2-listener-report.
Numeric values (0-255) are also accepted.
Interface Wildcard
A trailing * on an interface name produces a prefix match. For example,
input-interface "l2tp*" matches any interface whose name starts with l2tp
(l2tp0, l2tp1, l2tp-peer42, etc.). Without the *, the match is exact.
Action Types (then block)
| Config key | Action | Example |
|---|---|---|
accept |
Accept packet | accept; |
drop |
Drop packet | drop; |
reject |
Reject with ICMP | reject { with icmp; code 3; } |
jump |
Jump to chain | jump helper; |
goto |
Goto chain | goto cleanup; |
return |
Return from chain | return; |
snat |
Source NAT | snat { to "10.0.0.1"; } |
dnat |
Destination NAT | dnat { to "10.1.1.1:8080"; } |
masquerade |
Masquerade | masquerade; or masquerade { port-range "1024-65535"; } or masquerade { random; } |
redirect |
Redirect to port | redirect { to 8080; } |
notrack |
Disable conntrack | notrack; |
flow-offload |
Hardware offload | flow-offload { flowtable ft0; } |
mark-set |
Set packet mark | mark-set { value 0x10; } |
connection-mark-set |
Set connmark | connection-mark-set { value 0x20/0xff; } |
dscp-set |
Set DSCP | dscp-set 46; |
tcp-mss-set |
Clamp TCP MSS | tcp-mss-set 1400; |
counter |
Count packets/bytes | counter; |
log |
Log packet | log { prefix "DROPPED"; } |
limit-rate |
Rate limit | limit-rate { rate 10/second; burst 5; } |
exclude |
Skip NAT (Return) | exclude; |
NAT Exclude
In a NAT chain, exclude emits a Return verdict so matched traffic skips
the NAT translation. This replaces the VyOS nat destination rule N exclude
pattern.
firewall {
backend nft;
table nat-rules {
family ip;
chain prerouting {
type nat;
hook prerouting;
priority -100;
policy accept;
term skip-local {
from {
destination-address 10.0.0.0/8;
}
then {
exclude;
}
}
term dnat-web {
from {
destination-port 80;
}
then {
dnat { to "10.1.1.1"; }
}
}
}
}
}
SNAT Address Ranges
SNAT and DNAT accept address ranges for pool-based NAT:
then {
snat { to "10.0.0.1-10.0.0.10"; }
}
Named Sets
firewall {
backend nft;
table wan {
family inet;
set blocked {
type ipv4;
element 10.0.0.1;
element 10.0.0.2 { timeout 3600; }
}
chain input {
type filter;
hook input;
priority 0;
policy drop;
term block-list {
from {
source-address @blocked;
}
then {
drop;
}
}
}
}
}
Global Options
The global-options container provides keyword toggles for common network
security defaults. Each keyword maps to a kernel sysctl. At
config apply time, the firewall component emits these as sysctl defaults via
EventBus. Explicit sysctl { setting { ... } } entries always override
global-options (three-layer priority: config > transient > default).
firewall {
backend nft;
global-options {
all-ping enable;
syn-cookies enable;
source-validation strict;
log-martians enable;
}
}
| Keyword | Sysctl | enable | disable |
|---|---|---|---|
all-ping |
net.ipv4.icmp_echo_ignore_all |
0 (allow) | 1 (ignore) |
broadcast-ping |
net.ipv4.icmp_echo_ignore_broadcasts |
0 (allow) | 1 (ignore) |
syn-cookies |
net.ipv4.tcp_syncookies |
1 | 0 |
receive-redirects |
net.ipv4.conf.all.accept_redirects |
1 | 0 |
send-redirects |
net.ipv4.conf.all.send_redirects |
1 | 0 |
source-validation |
net.ipv4.conf.all.rp_filter |
disable=0, strict=1, loose=2 | - |
log-martians |
net.ipv4.conf.all.log_martians |
1 | 0 |
ipv6-receive-redirects |
net.ipv6.conf.all.accept_redirects |
1 | 0 |
ipv6-src-route |
net.ipv6.conf.all.accept_source_route |
1 | 0 |
Note: all-ping and broadcast-ping have inverted semantics because the
underlying sysctl controls "ignore" behavior.
IRR Prefix-List Filtering
Firewall rules can match traffic by ASN or AS-SET using IRR-resolved prefix
lists. The firewall-irr plugin resolves references via the IRR whois client,
caches results in zefs, and populates nftables interval sets.
Operator Workflow
- Fetch prefix data:
update firewall irr asn 13335 - Inspect cached data:
show firewall irr - Commit config with
source-asn 13335in a term's from-block - Refresh all cached entries:
update firewall irr all
Config Leaves
| Leaf | Type | Description |
|---|---|---|
source-asn |
uint32 (1-4294967294) | Match source address against IRR-resolved prefixes for this ASN |
source-as-set |
string | Match source address against IRR-resolved prefixes for this AS-SET |
destination-asn |
uint32 (1-4294967294) | Match destination address against IRR-resolved prefixes for this ASN |
destination-as-set |
string | Match destination address against IRR-resolved prefixes for this AS-SET |
IRR Policy
firewall {
irr {
server whois.radb.net;
peeringdb-url https://www.peeringdb.com;
refresh-interval 0; /* 0 = manual only; 60-86400 = auto-refresh seconds */
}
}
Config commit rejects if a referenced ASN/AS-SET has no cached prefix data, with an actionable error naming the missing entry and the command to run.
Auto-refresh (when refresh-interval > 0) is fail-closed: a failed IRR query
preserves the last-good cache and logs an error.
Per-Interface Source Validation
Bind an AS-SET to a customer-facing interface. Packets arriving on that interface with source addresses not in the AS-SET's IRR-resolved prefixes are dropped (ingress source validation, BCP 38).
firewall {
irr {
interface eth1 {
source-as-set AS-CUSTOMER-A;
}
interface eth2 {
source-as-set AS-CUSTOMER-B;
}
}
}
The plugin generates a ze_irr_iface table with a prerouting base chain.
For each bound interface, accept terms match input-interface + source-address
in set for both IPv4 and IPv6, followed by a drop term for that interface.
Unconfigured interfaces pass through unfiltered (chain policy accept).
Same fail-closed semantics apply: config commit rejects if any bound AS-SET has no cached prefix data. Removing an interface binding removes its filter on the next apply.
CLI
| Command | Description |
|---|---|
ze firewall show |
Display all firewall tables and rules |
ze firewall counters |
Show per-term packet and byte counters |
show firewall irr |
Show IRR filter status for all cached entries |
show firewall irr prefix <name> |
List cached prefixes for an ASN or AS-SET |
update firewall irr asn <N> |
Fetch/refresh IRR prefix-list for an ASN |
update firewall irr as-set <name> |
Fetch/refresh IRR prefix-list for an AS-SET |
update firewall irr all |
Refresh all cached IRR entries |
Lifecycle
The firewall component registers with ze's engine via registry.Register.
On boot and config reload, the reactor parses the firewall { } section,
loads the selected backend, and calls Apply([]Table). On failure,
sdk.Journal triggers a rollback to the previous state.
On a clean shutdown the component removes every ze-owned table (unless
flush-on-shutdown false; see Clean-shutdown teardown),
then closes the backend. Because the firewall component owns the shared backend,
it is the single actor that performs this teardown -- plugins that register
tables (control-plane-protection, policy-routes, ddos-local) do not run
their own shutdown withdrawal, so table removal never races the backend close.
A crash bypasses this path entirely, leaving tables in place.