{"record":{"id":"89847000f8dcbe03","repo":"netbirdio/netbird","slug":"add-inbound-dnat-rule-w-898470","errorCode":null,"errorMessage":"add inbound DNAT rule: %w","messagePattern":"add inbound DNAT rule: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/firewall/nftables/router_linux.go","lineNumber":1946,"sourceCode":"\t\t&expr.NAT{\n\t\t\tType:        expr.NATTypeDestNAT,\n\t\t\tFamily:      uint32(r.af.tableFamily),\n\t\t\tRegAddrMin:  1,\n\t\t\tRegProtoMin: 2,\n\t\t\tRegProtoMax: 0,\n\t\t},\n\t)\n\n\tdnatRule := &nftables.Rule{\n\t\tTable:    r.workTable,\n\t\tChain:    r.chains[chainNameRoutingRdr],\n\t\tExprs:    exprs,\n\t\tUserData: []byte(ruleID),\n\t}\n\tr.conn.AddRule(dnatRule)\n\n\tif err := r.conn.Flush(); err != nil {\n\t\treturn fmt.Errorf(\"add inbound DNAT rule: %w\", err)\n\t}\n\n\tr.rules[ruleID] = dnatRule\n\n\treturn nil\n}\n\n// RemoveInboundDNAT removes an inbound DNAT rule.\nfunc (r *router) RemoveInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {\n\tif err := r.refreshRulesMap(); err != nil {\n\t\treturn fmt.Errorf(refreshRulesMapError, err)\n\t}\n\n\truleID := fmt.Sprintf(\"inbound-dnat-%s-%s-%d-%d\", localAddr.String(), protocol, originalPort, translatedPort)\n\n\trule, exists := r.rules[ruleID]\n\tif !exists {\n\t\treturn nil","sourceCodeStart":1928,"sourceCodeEnd":1964,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/client/firewall/nftables/router_linux.go#L1928-L1964","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif err := r.conn.Flush(); err != nil {\n    return fmt.Errorf(\"add inbound DNAT rule: %w\", err)\n}\n\n// after: tolerate a pre-existing identical rule (idempotent add)\nif err := r.conn.Flush(); err != nil {\n    if errors.Is(err, unix.EEXIST) {\n        log.Warnf(\"inbound DNAT rule %s already present\", ruleID)\n        r.rules[ruleID] = dnatRule\n        return nil\n    }\n    return fmt.Errorf(\"add inbound DNAT rule: %w\", err)\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := r.conn.Flush(); err != nil {\n    if errors.Is(err, unix.EEXIST) {\n        // identical rule already in kernel: adopt it instead of failing\n        r.rules[ruleID] = dnatRule\n        return nil\n    }\n    return fmt.Errorf(\"add inbound DNAT rule: %w\", err)\n}","preventionTips":["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."],"tags":["nftables","netlink","dnat","batch-commit"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}