netbirdio/netbird · error
add nat rule: %w
Error message
add nat rule: %w
What it means
Returned by AddNatRule (router_linux.go:695) when addNatRule fails for the forward pair. addNatRule (line 732) converts pair.Source/pair.Destination into match expressions (errors 694/695 when a prefix set cannot be created) and may first remove an existing prerouting rule (error 696). Note that addNatRule only queues the InsertRule — no Flush happens inside — so on success nothing is committed yet; on this error path any netlink messages queued by a failed sub-step remain in the conn buffer and a later caller's Flush could commit them (the hazard documented at lines 1566-1568).
Source
Thrown at client/firewall/nftables/router_linux.go:696
return nil
}
// AddNatRule appends a nftables rule pair to the nat chain
func (r *router) AddNatRule(pair firewall.RouterPair) error {
if err := r.refreshRulesMap(); err != nil {
return fmt.Errorf(refreshRulesMapError, err)
}
if r.legacyManagement {
log.Warnf("This peer is connected to a NetBird Management service with an older version. Allowing all traffic for %s", pair.Destination)
if err := r.addLegacyRouteRule(pair); err != nil {
return fmt.Errorf("add legacy routing rule: %w", err)
}
}
if pair.Masquerade {
if err := r.addNatRule(pair); err != nil {
return fmt.Errorf("add nat rule: %w", err)
}
if err := r.addNatRule(firewall.GetInversePair(pair)); err != nil {
return fmt.Errorf("add inverse nat rule: %w", err)
}
}
if err := r.conn.Flush(); err != nil {
r.rollbackRules(pair)
return fmt.Errorf("insert rules for %s: %w", pair.Destination, err)
}
return nil
}
// rollbackRules cleans up unflushed rules and their set counters after a flush failure.
func (r *router) rollbackRules(pair firewall.RouterPair) {
keys := []string{View on GitHub (pinned to 93e97f4bf1)
Solutions
- Resolve the wrapped cause first: "apply source"/"apply destination" (694/695) or "remove prerouting rule" (696).
- After any addNatRule failure, flush the connection or rebuild it so orphaned queued messages are not committed by the next operation.
- Merge/shrink the network's prefix list if set creation is the cause.
- Retry AddNatRule after refreshRulesMap resynchronized handles.
Example fix
// before
if err := r.addNatRule(pair); err != nil {
return fmt.Errorf("add nat rule: %w", err)
}
// after
if err := r.addNatRule(pair); err != nil {
// drop any netlink messages queued by the failed sub-steps so a later
// Flush cannot half-commit this route
r.conn = nftables.Conn{...reinit...}
r.refreshRulesMap()
return fmt.Errorf("add nat rule: %w", err)
} Defensive patterns
Strategy: validation
Validate before calling
// Caller-side: validate the pair's networks before AddNatRule
func validatePairNetworks(pair firewall.RouterPair) error {
for _, n := range []firewall.Network{pair.Source, pair.Destination} {
if n.IsSet() && len(firewall.MergeIPRanges(n.Set.Prefixes())) > 1500 {
return fmt.Errorf("network set too large for nftables batch limit")
}
if n.IsPrefix() && !n.Prefix.IsValid() {
return fmt.Errorf("invalid prefix %v", n.Prefix)
}
}
return nil
} Type guard
func isNatRuleRetryable(err error) bool {
return isErrno(err, unix.EEXIST, unix.ENOENT, unix.EAGAIN)
} Try / catch
if err := r.addNatRule(pair); err != nil {
// messages queued by failed sub-steps must not leak into the next flush
_ = r.refreshRulesMap()
if isNatRuleRetryable(err) {
return r.AddNatRule(pair) // one bounded retry
}
return fmt.Errorf("add nat rule: %w", err)
} Prevention
- Pre-validate route pair networks (parse, family, prefix count) at the routemanager layer before touching nftables.
- Remember addNatRule only queues — never assume failure left nothing buffered; refresh state after errors.
- Merge network ranges upstream so set creation stays under the 1500-prefix batch budget.
- Monitor AddNatRule failure rates per peer; spikes correlate with external ruleset interference.
When it happens
Trigger: Adding a masqueraded network route where the source or destination network is a Set that fails to create, or re-adding a route whose prerouting rule replacement (removeNatRule) fails on a stale handle.
Common situations: Management pushing a masqueraded route with a many-prefix network range; route updates after a previous flush failure; peers where the work table was externally manipulated between updates.
Related errors
- add inverse nat rule: %w
- insert rules for %s: %w
- remove prerouting rule: %w
- decrement set counter: %w
- error adding set %s: %w
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/a71b3694d952c32a.
Report an issue: GitHub.