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

  1. 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.
  2. Check the kernel version: IPv6 DNAT requires Linux 4.18 or newer.
  3. Reproduce with the equivalent 'nft add rule ... dnat to' command to see the kernel's own complaint.
  4. For EEXIST, retry once after refreshRulesMap resynchronizes handles.
  5. 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

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


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/0233aaef85645056. Report an issue: GitHub.