{"record":{"id":"4e7a53fd0a7c8a02","repo":"netbirdio/netbird","slug":"delete-inbound-dnat-rule-s-w","errorCode":null,"errorMessage":"delete inbound DNAT rule %s: %w","messagePattern":"delete inbound DNAT rule (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/firewall/nftables/router_linux.go","lineNumber":1974,"sourceCode":"\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\n\t}\n\n\tif rule.Handle == 0 {\n\t\tlog.Warnf(\"inbound DNAT rule %s has no handle, removing stale entry\", ruleID)\n\t\tdelete(r.rules, ruleID)\n\t\treturn nil\n\t}\n\n\tif err := r.conn.DelRule(rule); err != nil {\n\t\treturn fmt.Errorf(\"delete inbound DNAT rule %s: %w\", ruleID, err)\n\t}\n\tif err := r.conn.Flush(); err != nil {\n\t\treturn fmt.Errorf(\"flush delete inbound DNAT rule: %w\", err)\n\t}\n\tdelete(r.rules, ruleID)\n\n\treturn nil\n}\n\n// ensureNATOutputChain lazily creates the OUTPUT NAT chain on first use.\nfunc (r *router) ensureNATOutputChain() error {\n\tif _, exists := r.chains[chainNameNATOutput]; exists {\n\t\treturn nil\n\t}\n\n\tr.chains[chainNameNATOutput] = r.conn.AddChain(&nftables.Chain{\n\t\tName:     chainNameNATOutput,\n\t\tTable:    r.workTable,","sourceCodeStart":1956,"sourceCodeEnd":1992,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/client/firewall/nftables/router_linux.go#L1956-L1992","documentation":"RemoveInboundDNAT issues an immediate NFT_MSG_DELRULE for the cached inbound-dnat rule after refreshing handles. This error wraps the kernel's negative ACK. The Handle==0 stale-entry case is handled separately above it, so this path means a real handle was sent and the kernel refused to delete it.","triggerScenarios":"The handle went stale between refreshRulesMap and DelRule (rule removed externally, ENOENT), netlink socket failure, or missing CAP_NET_ADMIN.","commonSituations":"External nft flush or firewalld reload racing the removal; second cleanup caller deleting the same rule; containerized agent without NET_ADMIN.","solutions":["Check 'sudo nft list chain <table> netbird-rt-redirect': an absent rule means ENOENT and success.","Call RemoveInboundDNAT again; the refresh at its top re-reads handles.","Verify root/CAP_NET_ADMIN.","Prevent external tooling from rewriting the netbird tables during teardown."],"exampleFix":"// before\nif err := r.conn.DelRule(rule); err != nil {\n    return fmt.Errorf(\"delete inbound DNAT rule %s: %w\", ruleID, err)\n}\n\n// after: treat an already-deleted rule as success\nif err := r.conn.DelRule(rule); err != nil {\n    if errors.Is(err, unix.ENOENT) {\n        log.Warnf(\"inbound DNAT rule %s already absent\", ruleID)\n        delete(r.rules, ruleID)\n        return nil\n    }\n    return fmt.Errorf(\"delete inbound DNAT rule %s: %w\", ruleID, err)\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := r.conn.DelRule(rule); err != nil {\n    if errors.Is(err, unix.ENOENT) {\n        log.Warnf(\"inbound DNAT rule %s already absent\", ruleID)\n        delete(r.rules, ruleID)\n        return nil\n    }\n    return fmt.Errorf(\"delete inbound DNAT rule %s: %w\", ruleID, err)\n}","preventionTips":["Always remove inbound DNAT rules through RemoveInboundDNAT so handles are refreshed first.","Treat ENOENT during cleanup as convergence to the desired absent state.","Avoid double-teardown paths that delete the same ruleID concurrently."],"tags":["nftables","netlink","dnat","cleanup"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}