netbirdio/netbird · error
delete snat rule: %w
Error message
delete snat rule: %w
What it means
DeleteDNATRule issues an immediate NFT_MSG_DELRULE for the cached _snat rule (the masquerade rule in netbird-rt-postrouting). This error wraps the kernel's negative ACK, is appended to the same multierror as the dnat deletion and flush, and on failure the map entry and forwarding reference are intentionally kept so a retry completes the teardown.
Source
Thrown at client/firewall/nftables/router_linux.go:1827
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 {
merr = multierror.Append(merr, fmt.Errorf(flushError, err))
}
}
// Release the refcount only once the rules are gone from the kernel. On
// failure (including the refreshRulesMap error above) the rules and their
// map entries remain, keeping forwarding on until a retry removes them.
if merr == nil {
delete(r.rules, ruleKey+dnatSuffix)
delete(r.rules, ruleKey+snatSuffix)
View on GitHub (pinned to 93e97f4bf1)
Solutions
- Check 'sudo nft list chain <table> netbird-rt-postrouting' for the masquerade rule; if absent, ENOENT is benign.
- Retry DeleteDNATRule so refreshRulesMap picks up current handles for both halves.
- Verify root/CAP_NET_ADMIN.
- After a successful retry, confirm forwarding was released (the refcount drop happens only when merr is nil).
Example fix
// before
} else if err := r.conn.DelRule(masqRule); err != nil {
merr = multierror.Append(merr, fmt.Errorf("delete snat rule: %w", err))
}
// after: tolerate an externally-removed masquerade rule
} else if err := r.conn.DelRule(masqRule); err != nil {
if errors.Is(err, unix.ENOENT) {
log.Warnf("snat rule %s already absent", ruleKey+snatSuffix)
} else {
merr = multierror.Append(merr, fmt.Errorf("delete snat rule: %w", err))
}
} Defensive patterns
Strategy: try-catch
Try / catch
} else if err := r.conn.DelRule(masqRule); err != nil {
if errors.Is(err, unix.ENOENT) {
log.Warnf("snat rule %s already absent", ruleKey+snatSuffix)
} else {
merr = multierror.Append(merr, fmt.Errorf("delete snat rule: %w", err))
}
} Prevention
- Delete dnat and snat halves through the same DeleteDNATRule call so refreshes stay consistent.
- After teardown, verify forwarding was released; a kept reference means one half failed.
- Keep external tools away from netbird-rt-postrouting while routes are configured.
When it happens
Trigger: Stale handle (the masquerade rule was removed externally, ENOENT), netlink socket failure, or missing CAP_NET_ADMIN.
Common situations: External nftables rewrites racing the deletion; the dnat rule deletion succeeded but the snat half failed halfway through teardown; unprivileged containers.
Related errors
- add nat rule: %w
- decrement set counter: %w
- error adding set %s: %w
- flush error: %w
- error adding prefixes (%d) to set %s: %w
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/ee0f47d9a1bc0fd7.
Report an issue: GitHub.