netbirdio/netbird · error

add inbound DNAT rule: %w

Error message

add inbound DNAT rule: %w

What it means

AddInboundDNAT buffers the redirect rule into the netbird-rt-redirect chain and commits it with Flush; 'add inbound DNAT rule' wraps that commit failure. The map write into r.rules happens only after a successful Flush, so the rule is not tracked and a later call re-attempts the add.

Source

Thrown at client/firewall/nftables/router_linux.go:1946

		&expr.NAT{
			Type:        expr.NATTypeDestNAT,
			Family:      uint32(r.af.tableFamily),
			RegAddrMin:  1,
			RegProtoMin: 2,
			RegProtoMax: 0,
		},
	)

	dnatRule := &nftables.Rule{
		Table:    r.workTable,
		Chain:    r.chains[chainNameRoutingRdr],
		Exprs:    exprs,
		UserData: []byte(ruleID),
	}
	r.conn.AddRule(dnatRule)

	if err := r.conn.Flush(); err != nil {
		return fmt.Errorf("add inbound DNAT rule: %w", err)
	}

	r.rules[ruleID] = dnatRule

	return nil
}

// RemoveInboundDNAT removes an inbound DNAT rule.
func (r *router) RemoveInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
	if err := r.refreshRulesMap(); err != nil {
		return fmt.Errorf(refreshRulesMapError, err)
	}

	ruleID := fmt.Sprintf("inbound-dnat-%s-%s-%d-%d", localAddr.String(), protocol, originalPort, translatedPort)

	rule, exists := r.rules[ruleID]
	if !exists {
		return nil

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Classify the errno: EPERM (privileges), EINVAL (unsupported NAT expression for the family), EEXIST (stale duplicate).
  2. Reproduce with an equivalent 'nft add rule ... redirect to' invocation.
  3. For EEXIST, flush the stale rule or restart the agent to resynchronize the map with the kernel.
  4. Confirm kernel 4.18+ for IPv6 DNAT.

Example fix

// before
if err := r.conn.Flush(); err != nil {
    return fmt.Errorf("add inbound DNAT rule: %w", err)
}

// after: tolerate a pre-existing identical rule (idempotent add)
if err := r.conn.Flush(); err != nil {
    if errors.Is(err, unix.EEXIST) {
        log.Warnf("inbound DNAT rule %s already present", ruleID)
        r.rules[ruleID] = dnatRule
        return nil
    }
    return fmt.Errorf("add inbound DNAT rule: %w", err)
}
Defensive patterns

Strategy: try-catch

Try / catch

if err := r.conn.Flush(); err != nil {
    if errors.Is(err, unix.EEXIST) {
        // identical rule already in kernel: adopt it instead of failing
        r.rules[ruleID] = dnatRule
        return nil
    }
    return fmt.Errorf("add inbound DNAT rule: %w", err)
}

Prevention

When it happens

Trigger: The kernel rejects the NAT expression (IPv6 NAT on kernels before 4.18, register layout it dislikes), EPERM from missing CAP_NET_ADMIN, EEXIST when an identical rule already lives in the kernel but not in the map after external state changes.

Common situations: Old or minimal kernels; v6 local-service DNAT on hosts with IPv6 disabled at the nftables level; rules left behind by a previous agent run after an unclean shutdown.

Related errors


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