{"record":{"id":"a71b3694d952c32a","repo":"netbirdio/netbird","slug":"add-nat-rule-w-a71b36","errorCode":null,"errorMessage":"add nat rule: %w","messagePattern":"add nat rule: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/firewall/nftables/router_linux.go","lineNumber":696,"sourceCode":"\treturn nil\n}\n\n// AddNatRule appends a nftables rule pair to the nat chain\nfunc (r *router) AddNatRule(pair firewall.RouterPair) error {\n\tif err := r.refreshRulesMap(); err != nil {\n\t\treturn fmt.Errorf(refreshRulesMapError, err)\n\t}\n\n\tif r.legacyManagement {\n\t\tlog.Warnf(\"This peer is connected to a NetBird Management service with an older version. Allowing all traffic for %s\", pair.Destination)\n\t\tif err := r.addLegacyRouteRule(pair); err != nil {\n\t\t\treturn fmt.Errorf(\"add legacy routing rule: %w\", err)\n\t\t}\n\t}\n\n\tif pair.Masquerade {\n\t\tif err := r.addNatRule(pair); err != nil {\n\t\t\treturn fmt.Errorf(\"add nat rule: %w\", err)\n\t\t}\n\n\t\tif err := r.addNatRule(firewall.GetInversePair(pair)); err != nil {\n\t\t\treturn fmt.Errorf(\"add inverse nat rule: %w\", err)\n\t\t}\n\t}\n\n\tif err := r.conn.Flush(); err != nil {\n\t\tr.rollbackRules(pair)\n\t\treturn fmt.Errorf(\"insert rules for %s: %w\", pair.Destination, err)\n\t}\n\n\treturn nil\n}\n\n// rollbackRules cleans up unflushed rules and their set counters after a flush failure.\nfunc (r *router) rollbackRules(pair firewall.RouterPair) {\n\tkeys := []string{","sourceCodeStart":678,"sourceCodeEnd":714,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/client/firewall/nftables/router_linux.go#L678-L714","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif err := r.addNatRule(pair); err != nil {\n    return fmt.Errorf(\"add nat rule: %w\", err)\n}\n\n// after\nif err := r.addNatRule(pair); err != nil {\n    // drop any netlink messages queued by the failed sub-steps so a later\n    // Flush cannot half-commit this route\n    r.conn = nftables.Conn{...reinit...}\n    r.refreshRulesMap()\n    return fmt.Errorf(\"add nat rule: %w\", err)\n}","handlingStrategy":"validation","validationCode":"// Caller-side: validate the pair's networks before AddNatRule\nfunc validatePairNetworks(pair firewall.RouterPair) error {\n    for _, n := range []firewall.Network{pair.Source, pair.Destination} {\n        if n.IsSet() && len(firewall.MergeIPRanges(n.Set.Prefixes())) > 1500 {\n            return fmt.Errorf(\"network set too large for nftables batch limit\")\n        }\n        if n.IsPrefix() && !n.Prefix.IsValid() {\n            return fmt.Errorf(\"invalid prefix %v\", n.Prefix)\n        }\n    }\n    return nil\n}","typeGuard":"func isNatRuleRetryable(err error) bool {\n\treturn isErrno(err, unix.EEXIST, unix.ENOENT, unix.EAGAIN)\n}","tryCatchPattern":"if err := r.addNatRule(pair); err != nil {\n    // messages queued by failed sub-steps must not leak into the next flush\n    _ = r.refreshRulesMap()\n    if isNatRuleRetryable(err) {\n        return r.AddNatRule(pair) // one bounded retry\n    }\n    return fmt.Errorf(\"add nat rule: %w\", err)\n}","preventionTips":["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."],"tags":["go","nftables","netlink","nat","masquerade","firewall","routing"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}