{"record":{"id":"77cb2b46b220582e","repo":"netbirdio/netbird","slug":"add-inverse-nat-rule-w-77cb2b","errorCode":null,"errorMessage":"add inverse nat rule: %w","messagePattern":"add inverse nat rule: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/firewall/nftables/router_linux.go","lineNumber":700,"sourceCode":"func (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{\n\t\tfirewall.GenKey(firewall.ForwardingFormat, pair),\n\t\tfirewall.GenKey(firewall.PreroutingFormat, pair),\n\t\tfirewall.GenKey(firewall.PreroutingFormat, firewall.GetInversePair(pair)),\n\t}","sourceCodeStart":682,"sourceCodeEnd":718,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/client/firewall/nftables/router_linux.go#L682-L718","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif err := r.addNatRule(firewall.GetInversePair(pair)); err != nil {\n    return fmt.Errorf(\"add inverse nat rule: %w\", err)\n}\n\n// after\nif err := r.addNatRule(firewall.GetInversePair(pair)); err != nil {\n    // forward rule is queued but not committed; roll it back so the pair stays symmetric\n    r.rollbackRules(pair)\n    return fmt.Errorf(\"add inverse nat rule: %w\", err)\n}","handlingStrategy":"validation","validationCode":"// Validate the inverse pair too — it shares the same networks, swapped\ninv := firewall.GetInversePair(pair)\nif err := validatePairNetworks(inv); err != nil {\n    return fmt.Errorf(\"inverse pair invalid: %w\", err)\n}","typeGuard":"func isNatRuleRetryable(err error) bool {\n\treturn isErrno(err, unix.EEXIST, unix.ENOENT, unix.EAGAIN)\n}","tryCatchPattern":"if err := r.addNatRule(firewall.GetInversePair(pair)); err != nil {\n    r.rollbackRules(pair) // forward rule is queued; drop it so the pair stays symmetric\n    if isNatRuleRetryable(err) {\n        return r.AddNatRule(pair)\n    }\n    return fmt.Errorf(\"add inverse nat rule: %w\", err)\n}","preventionTips":["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."],"tags":["go","nftables","nat","masquerade","routing","firewall","linux"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}