netbirdio/netbird · error
flush rules: %w
Error message
flush rules: %w
What it means
After addDnatRedirect and addDnatMasq buffer NFT_MSG_NEWRULE messages on the shared conn, AddDNATRule commits them with Flush. 'flush rules' means the kernel rejected the batch or the netlink exchange failed. The code rolls back the forwarding refcount and drops the buffered map entries, so the DNAT rule is not considered installed.
Source
Thrown at client/firewall/nftables/router_linux.go:1594
log.Warnf("rollback forwarding refcount: %v", rerr)
}
return nil, err
}
r.addDnatMasq(rule, protoNum, ruleKey)
// Unlike iptables, there's no point in adding "out" rules in the forward chain here as our policy is ACCEPT.
// To overcome DROP policies in other chains, we'd have to add rules to the chains there.
// We also cannot just add "oif <iface> accept" there and filter in our own table as we don't know what is supposed to be allowed.
// TODO: find chains with drop policies and add rules there
if err := r.conn.Flush(); err != nil {
if rerr := r.ipFwdState.ReleaseForwarding(v6); rerr != nil {
log.Warnf("rollback forwarding refcount: %v", rerr)
}
delete(r.rules, ruleKey+dnatSuffix)
delete(r.rules, ruleKey+snatSuffix)
return nil, fmt.Errorf("flush rules: %w", err)
}
return &rule, nil
}
func (r *router) addDnatRedirect(rule firewall.ForwardRule, protoNum uint8, ruleKey string) error {
dnatExprs := []expr.Any{
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
&expr.Cmp{
Op: expr.CmpOpNeq,
Register: 1,
Data: ifname(r.wgIface.Name()),
},
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte{protoNum},View on GitHub (pinned to 93e97f4bf1)
Solutions
- Read the wrapped errno: EPERM means missing privilege, EINVAL means the kernel rejected the NAT expression for this family, EEXIST means the rule already exists.
- Check the kernel version: IPv6 DNAT requires Linux 4.18 or newer.
- Reproduce with the equivalent 'nft add rule ... dnat to' command to see the kernel's own complaint.
- For EEXIST, retry once after refreshRulesMap resynchronizes handles.
- Verify NET_ADMIN in containerized deployments.
Example fix
// before
if err := r.conn.Flush(); err != nil {
return nil, fmt.Errorf("flush rules: %w", err)
}
// after: classify the errno so operators can act on it
if err := r.conn.Flush(); err != nil {
switch {
case errors.Is(err, unix.EPERM):
return nil, fmt.Errorf("flush rules (missing CAP_NET_ADMIN): %w", err)
case errors.Is(err, unix.EINVAL):
return nil, fmt.Errorf("flush rules (kernel rejected NAT expression; v6 NAT needs >= 4.18): %w", err)
default:
return nil, fmt.Errorf("flush rules: %w", err)
}
} Defensive patterns
Strategy: try-catch
Validate before calling
// Rough capability preflight before NAT rule installation
func natPrerequisitesOk(v6 bool) error {
if v6 {
var uts unix.Utsname
if err := unix.Uname(&uts); err != nil {
return err
}
rel := unix.ByteSliceToString(uts.Release[:])
var major, minor int
if _, err := fmt.Sscanf(rel, "%d.%d", &major, &minor); err == nil {
if major < 4 || (major == 4 && minor < 18) {
return fmt.Errorf("IPv6 NAT requires kernel >= 4.18, running %s", rel)
}
}
}
return nil
} Try / catch
if err := r.conn.Flush(); err != nil {
switch {
case errors.Is(err, unix.EPERM):
return nil, fmt.Errorf("flush rules: missing CAP_NET_ADMIN: %w", err)
case errors.Is(err, unix.EEXIST):
// duplicate in kernel but not in map: resync and retry once
if rerr := r.refreshRulesMap(); rerr == nil {
return r.AddDNATRule(rule)
}
return nil, err
default:
return nil, fmt.Errorf("flush rules: %w", err)
}
} Prevention
- Check kernel NAT support (4.18+ for IPv6) before deploying v6 port forwarding.
- Keep exactly one writer to the netbird table to avoid EEXIST duplicates.
- Surface wrapped errnos in logs so EPERM vs EINVAL is distinguishable in the field.
When it happens
Trigger: Kernel lacks the requested NAT feature (IPv6 NAT needs Linux 4.18+), a malformed expression triggers EINVAL, duplicate rules yield EEXIST, CAP_NET_ADMIN is missing, or the netlink batch is oversized.
Common situations: Old or cut-down container kernels; concurrent duplicate forward rules; state skew where the rule already exists in kernel but not in r.rules after external changes; user namespaces without NET_ADMIN.
Related errors
- add nat rule: %w
- insert rules for %s: %w
- remove prerouting rule %s -> %s: %w
- add inbound DNAT rule: %w
- flush delete inbound DNAT rule: %w
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/0233aaef85645056.
Report an issue: GitHub.