netbirdio/netbird · error
flush delete output DNAT rule: %w
Error message
flush delete output DNAT rule: %w
What it means
Returned by RemoveOutputDNAT (client/firewall/nftables/router_linux.go:2105) when the conn.Flush() following DelRule fails. DelRule only buffers the delete netlink message; Flush commits it, so this is the kernel's asynchronous verdict on the deletion: EPERM, ENOENT (rule handle already gone), or a netlink send failure (buffer full, socket closed).
Source
Thrown at client/firewall/nftables/router_linux.go:2105
ruleID := fmt.Sprintf("output-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("output 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 output DNAT rule %s: %w", ruleID, err)
}
if err := r.conn.Flush(); err != nil {
return fmt.Errorf("flush delete output DNAT rule: %w", err)
}
delete(r.rules, ruleID)
return nil
}
// applyNetwork generates nftables expressions for networks (CIDR) or sets
func (r *router) applyNetwork(
network firewall.Network,
setPrefixes []netip.Prefix,
isSource bool,
) ([]expr.Any, error) {
if network.IsSet() {
exprs, err := r.getIpSet(network.Set, setPrefixes, isSource)
if err != nil {
return nil, fmt.Errorf("source: %w", err)
}
return exprs, nilView on GitHub (pinned to 93e97f4bf1)
Solutions
- Retry the removal after a fresh refreshRulesMap when the kernel reports ENOENT - the rule is gone and the map just needs resyncing
- Recreate the nftables manager if the socket is dead, then retry; the re-created table contains no stale DNAT rule so the removal goal is already met
- Keep delete batches small or flush incrementally to avoid ENOBUFS on netlink
- Verify the process retains CAP_NET_ADMIN for the whole daemon lifetime, not just at start
Example fix
// before
if err := r.conn.Flush(); err != nil {
return fmt.Errorf("flush delete output DNAT rule: %w", err)
}
// after
if err := r.conn.Flush(); err != nil {
if isNotExistErr(err) {
delete(r.rules, ruleID)
return nil
}
return fmt.Errorf("flush delete output DNAT rule: %w", err)
}
delete(r.rules, ruleID) Defensive patterns
Strategy: fallback
Try / catch
if err := router.RemoveOutputDNAT(...); err != nil {
switch {
case isNotExistErr(rootCause(err)):
return nil
case isPermissionErr(rootCause(err)):
return fmt.Errorf("need CAP_NET_ADMIN to remove DNAT: %w", err)
default:
// socket may be dead; a recreated manager has no stale rule to remove
firewall.Reset()
return nil
}
} Prevention
- Keep netlink batches small during mass teardown
- Never assume DelRule success until the following Flush returns nil
- On daemon shutdown, remove DNAT rules before dropping capabilities
When it happens
Trigger: The same rule deleted externally between DelRule and Flush; netlink socket invalidated (network namespace teardown, daemon fork); too many buffered operations in one batch exceeding netlink buffer limits; missing capabilities.
Common situations: Container teardown where the network namespace is being destroyed concurrently; large batch removals of DNAT rules; agent shutdown racing with system nftables cleanup scripts.
Related errors
- delete output DNAT rule %s: %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/8fa9fb074e7d8775.
Report an issue: GitHub.