netbirdio/netbird · error
delete dnat rule: %w
Error message
delete dnat rule: %w
What it means
DeleteDNATRule issues an immediate NFT_MSG_DELRULE for the cached _dnat rule (the redirect in netbird-rt-redirect). This error wraps the kernel's negative ACK and is accumulated into a multierror together with the snat deletion and flush; on failure the map entries survive and the forwarding reference is kept for a later retry.
Source
Thrown at client/firewall/nftables/router_linux.go:1816
if err := r.refreshRulesMap(); err != nil {
return fmt.Errorf(refreshRulesMapError, err)
}
_, hadDNAT := r.rules[ruleKey+dnatSuffix]
_, hadSNAT := r.rules[ruleKey+snatSuffix]
if !hadDNAT && !hadSNAT {
return nil
}
var merr *multierror.Error
var needsFlush bool
if dnatRule, exists := r.rules[ruleKey+dnatSuffix]; exists {
if dnatRule.Handle == 0 {
log.Warnf("dnat rule %s has no handle, removing stale entry", ruleKey+dnatSuffix)
delete(r.rules, ruleKey+dnatSuffix)
} else if err := r.conn.DelRule(dnatRule); err != nil {
merr = multierror.Append(merr, fmt.Errorf("delete dnat rule: %w", err))
} else {
needsFlush = true
}
}
if masqRule, exists := r.rules[ruleKey+snatSuffix]; exists {
if masqRule.Handle == 0 {
log.Warnf("snat rule %s has no handle, removing stale entry", ruleKey+snatSuffix)
delete(r.rules, ruleKey+snatSuffix)
} else if err := r.conn.DelRule(masqRule); err != nil {
merr = multierror.Append(merr, fmt.Errorf("delete snat rule: %w", err))
} else {
needsFlush = true
}
}
if needsFlush {
if err := r.conn.Flush(); err != nil {View on GitHub (pinned to 93e97f4bf1)
Solutions
- Confirm with 'sudo nft list chain <table> netbird-rt-redirect' whether the rule is already gone; ENOENT then means success.
- Retry DeleteDNATRule: refreshRulesMap re-reads handles and the second attempt deletes what remains.
- Ensure the agent has root or CAP_NET_ADMIN.
- Stop external tooling from rewriting the netbird tables while routes exist.
Example fix
// before
} else if err := r.conn.DelRule(dnatRule); err != nil {
merr = multierror.Append(merr, fmt.Errorf("delete dnat rule: %w", err))
}
// after: a vanished rule satisfies the deletion intent
} else if err := r.conn.DelRule(dnatRule); err != nil {
if errors.Is(err, unix.ENOENT) {
log.Warnf("dnat rule %s already absent", ruleKey+dnatSuffix)
} else {
merr = multierror.Append(merr, fmt.Errorf("delete dnat rule: %w", err))
}
} Defensive patterns
Strategy: try-catch
Try / catch
} else if err := r.conn.DelRule(dnatRule); err != nil {
if errors.Is(err, unix.ENOENT) {
log.Warnf("dnat rule %s already absent", ruleKey+dnatSuffix)
} else {
merr = multierror.Append(merr, fmt.Errorf("delete dnat rule: %w", err))
}
} Prevention
- Rely on DeleteDNATRule's built-in refresh rather than caching handles across long periods.
- Prevent concurrent deletions of the same forward rule from orchestrators.
- Treat ENOENT on delete paths as success when reconcilers converge on 'rule absent'.
When it happens
Trigger: The cached handle is stale because the rule was already removed externally (ENOENT is the common case), the netlink socket fails, or the process lacks CAP_NET_ADMIN.
Common situations: External ruleset flushes between refreshRulesMap and DelRule; concurrent deletions of the same forward rule; containerized agents without NET_ADMIN.
Related errors
- decrement set counter: %w
- error adding set %s: %w
- flush error: %w
- error adding prefixes (%d) to set %s: %w
- add nat rule: %w
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/6f8a9b49da0b833a.
Report an issue: GitHub.