netbirdio/netbird · error
delete inbound DNAT rule %s: %w
Error message
delete inbound DNAT rule %s: %w
What it means
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.
Source
Thrown at client/firewall/nftables/router_linux.go:1974
if err := r.refreshRulesMap(); err != nil {
return fmt.Errorf(refreshRulesMapError, err)
}
ruleID := fmt.Sprintf("inbound-dnat-%s-%s-%d-%d", localAddr.String(), protocol, originalPort, translatedPort)
rule, exists := r.rules[ruleID]
if !exists {
return nil
}
if rule.Handle == 0 {
log.Warnf("inbound DNAT rule %s has no handle, removing stale entry", ruleID)
delete(r.rules, ruleID)
return nil
}
if err := r.conn.DelRule(rule); err != nil {
return fmt.Errorf("delete inbound DNAT rule %s: %w", ruleID, err)
}
if err := r.conn.Flush(); err != nil {
return fmt.Errorf("flush delete inbound DNAT rule: %w", err)
}
delete(r.rules, ruleID)
return nil
}
// ensureNATOutputChain lazily creates the OUTPUT NAT chain on first use.
func (r *router) ensureNATOutputChain() error {
if _, exists := r.chains[chainNameNATOutput]; exists {
return nil
}
r.chains[chainNameNATOutput] = r.conn.AddChain(&nftables.Chain{
Name: chainNameNATOutput,
Table: r.workTable,View on GitHub (pinned to 93e97f4bf1)
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.
Example fix
// before
if err := r.conn.DelRule(rule); err != nil {
return fmt.Errorf("delete inbound DNAT rule %s: %w", ruleID, err)
}
// after: treat an already-deleted rule as success
if err := r.conn.DelRule(rule); err != nil {
if errors.Is(err, unix.ENOENT) {
log.Warnf("inbound DNAT rule %s already absent", ruleID)
delete(r.rules, ruleID)
return nil
}
return fmt.Errorf("delete inbound DNAT rule %s: %w", ruleID, err)
} Defensive patterns
Strategy: try-catch
Try / catch
if err := r.conn.DelRule(rule); err != nil {
if errors.Is(err, unix.ENOENT) {
log.Warnf("inbound DNAT rule %s already absent", ruleID)
delete(r.rules, ruleID)
return nil
}
return fmt.Errorf("delete inbound DNAT rule %s: %w", ruleID, err)
} Prevention
- 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.
When it happens
Trigger: The handle went stale between refreshRulesMap and DelRule (rule removed externally, ENOENT), netlink socket failure, or missing CAP_NET_ADMIN.
Common situations: External nft flush or firewalld reload racing the removal; second cleanup caller deleting the same rule; containerized agent without NET_ADMIN.
Related errors
- delete output DNAT rule %s: %w
- delete dnat rule: %w
- add inbound DNAT rule: %w
- flush delete inbound DNAT rule: %w
- add output DNAT rule: %w
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/4e7a53fd0a7c8a02.
Report an issue: GitHub.