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 nilView on GitHub (pinned to 93e97f4bf1)
Solutions
- Classify the errno: EPERM (privileges), EINVAL (unsupported NAT expression for the family), EEXIST (stale duplicate).
- Reproduce with an equivalent 'nft add rule ... redirect to' invocation.
- For EEXIST, flush the stale rule or restart the agent to resynchronize the map with the kernel.
- 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
- Ensure clean teardown of inbound DNAT rules on shutdown so restarts do not hit EEXIST.
- Verify kernel 4.18+ before deploying IPv6 local-service forwarding.
- Reserve NET_ADMIN for the daemon; flush errors with EPERM always trace back to privileges.
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
- flush delete inbound DNAT rule: %w
- flush rules: %w
- delete dnat rule: %w
- delete inbound DNAT rule %s: %w
- add output DNAT rule: %w
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/89847000f8dcbe03.
Report an issue: GitHub.