netbirdio/netbird · error
add inverse nat rule: %w
Error message
add inverse nat rule: %w
What it means
Returned by AddNatRule (router_linux.go:699) when the second addNatRule call — for firewall.GetInversePair(pair), the rule that marks return traffic with PreroutingFwmarkMasqueradeReturn so postrouting masquerades replies exiting the overlay interface — fails. Causes are identical in shape to 691 (set creation for the inverted source/destination, or prerouting rule replacement). By this point the forward rule is already queued on the conn buffer, so failing here leaves an asymmetric state unless the caller clears it; the pair is only committed or rolled back together at the Flush on line 704.
Source
Thrown at client/firewall/nftables/router_linux.go:700
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{
firewall.GenKey(firewall.ForwardingFormat, pair),
firewall.GenKey(firewall.PreroutingFormat, pair),
firewall.GenKey(firewall.PreroutingFormat, firewall.GetInversePair(pair)),
}View on GitHub (pinned to 93e97f4bf1)
Solutions
- Treat 691 and 692 identically: fix the wrapped apply/remove error, then re-invoke AddNatRule which replaces both rules atomically at its final Flush.
- Call refreshRulesMap before AddNatRule (it does this itself at line 683) so the inverse rule key maps to a live handle and removal succeeds.
- Ensure the connection buffer is clean after failure so the queued forward rule is not orphaned.
- Restart the agent if stale map entries keep reproducing the failure.
Example fix
// before
if err := r.addNatRule(firewall.GetInversePair(pair)); err != nil {
return fmt.Errorf("add inverse nat rule: %w", err)
}
// after
if err := r.addNatRule(firewall.GetInversePair(pair)); err != nil {
// forward rule is queued but not committed; roll it back so the pair stays symmetric
r.rollbackRules(pair)
return fmt.Errorf("add inverse nat rule: %w", err)
} Defensive patterns
Strategy: validation
Validate before calling
// Validate the inverse pair too — it shares the same networks, swapped
inv := firewall.GetInversePair(pair)
if err := validatePairNetworks(inv); err != nil {
return fmt.Errorf("inverse pair invalid: %w", err)
} Type guard
func isNatRuleRetryable(err error) bool {
return isErrno(err, unix.EEXIST, unix.ENOENT, unix.EAGAIN)
} Try / catch
if err := r.addNatRule(firewall.GetInversePair(pair)); err != nil {
r.rollbackRules(pair) // forward rule is queued; drop it so the pair stays symmetric
if isNatRuleRetryable(err) {
return r.AddNatRule(pair)
}
return fmt.Errorf("add inverse nat rule: %w", err)
} Prevention
- Keep forward and inverse rules symmetric: on any failure of either half, roll both back before retrying.
- Apply the same network validation to the inverse pair — identical sets, different match direction.
- Ensure prerouting keys for both directions exist in r.rules so replacement (not duplication) happens on re-adds.
- Alert when only one direction of a masquerade pair is present in the kernel — asymmetric NAT breaks return traffic.
When it happens
Trigger: Re-applying a masqueraded route where the inverse pair's rule key already exists with a stale handle (removeNatRule fails), or the inverse pair's set (same prefixes, swapped roles) failing EEXIST/EINVAL during creation.
Common situations: Idempotent re-application of NAT routes during network-map refreshes; peers that previously experienced a failed flush leaving handle-less prerouting entries; large bidirectional ranges.
Related errors
- add nat rule: %w
- remove prerouting rule: %w
- insert rules for %s: %w
- remove legacy routing rule: %w
- decrement set counter: %w
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/77cb2b46b220582e.
Report an issue: GitHub.