{"record":{"id":"77e91e10e45390bf","repo":"netbirdio/netbird","slug":"flush-delete-inbound-dnat-rule-w","errorCode":null,"errorMessage":"flush delete inbound DNAT rule: %w","messagePattern":"flush delete inbound DNAT rule: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/firewall/nftables/router_linux.go","lineNumber":1977,"sourceCode":"\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,\n\t\tHooknum:  nftables.ChainHookOutput,\n\t\tPriority: nftables.ChainPriorityNATDest,\n\t\tType:     nftables.ChainTypeNAT,","sourceCodeStart":1959,"sourceCodeEnd":1995,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/client/firewall/nftables/router_linux.go#L1959-L1995","documentation":"RemoveInboundDNAT commits the queued rule deletion with conn.Flush(); 'flush delete inbound DNAT rule' wraps that commit failure. The map entry is deleted only after a successful flush, so on failure the entry survives and a retry re-attempts the whole removal.","triggerScenarios":"The kernel rejects the delete inside the batch (stale handle, ENOENT), netlink exchange failure at commit time, or missing CAP_NET_ADMIN.","commonSituations":"Ruleset churn between DelRule and commit; netlink pressure on hosts with many rules; unprivileged environments.","solutions":["Read the wrapped errno to separate privilege issues from state skew.","Retry RemoveInboundDNAT; the surviving map entry makes it idempotent.","Confirm with 'sudo nft list chain <table> netbird-rt-redirect' whether the rule is actually gone despite the error.","Check for seccomp or LSM policies blocking netlink sendmsg."],"exampleFix":"// before\nif err := r.conn.Flush(); err != nil {\n    return fmt.Errorf(\"flush delete inbound DNAT rule: %w\", err)\n}\n\n// after: downgrade ENOENT (rule already gone) and keep the entry only for real failures\nif err := r.conn.Flush(); err != nil {\n    if errors.Is(err, unix.ENOENT) {\n        log.Warnf(\"inbound DNAT delete flush: rule already absent\")\n    } else {\n        return fmt.Errorf(\"flush delete inbound DNAT rule: %w\", err)\n    }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := r.conn.Flush(); err != nil {\n    if errors.Is(err, unix.ENOENT) {\n        log.Warnf(\"inbound DNAT delete flush: rule already absent\")\n        delete(r.rules, ruleID)\n        return nil\n    }\n    return fmt.Errorf(\"flush delete inbound DNAT rule: %w\", err)\n}","preventionTips":["Keep the map delete after the flush so failed commits remain retryable, mirroring RemoveInboundDNAT.","Verify with 'nft list chain' after failed delete flushes to distinguish real rules from stale handles.","Ensure netlink sendmsg is permitted in container security profiles."],"tags":["nftables","netlink","batch-commit","dnat"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}