netbirdio/netbird · error
remove prerouting rule %s -> %s: %w
Error message
remove prerouting rule %s -> %s: %w
What it means
Returned by removeNatRule when nftables.Conn.DelRule fails to delete a cached prerouting NAT rule from the netbird routing chains. DelRule sends an NFT_MSG_DELRULE netlink request that executes immediately (unlike AddRule, which buffers until Flush), so this is a real kernel ACK error for the handle stored in r.rules. The message names the routing pair's source and destination networks.
Source
Thrown at client/firewall/nftables/router_linux.go:1514
ruleKey := firewall.GenKey(firewall.PreroutingFormat, pair)
rule, exists := r.rules[ruleKey]
if !exists {
log.Debugf("prerouting rule %s not found", ruleKey)
return nil
}
if rule.Handle == 0 {
log.Warnf("prerouting rule %s has no handle, removing stale entry", ruleKey)
if err := r.decrementSetCounter(rule); err != nil {
log.Warnf("decrement set counter for stale rule %s: %v", ruleKey, err)
}
delete(r.rules, ruleKey)
return nil
}
if err := r.conn.DelRule(rule); err != nil {
return fmt.Errorf("remove prerouting rule %s -> %s: %w", pair.Source, pair.Destination, err)
}
log.Debugf("removed prerouting rule %s -> %s", pair.Source, pair.Destination)
delete(r.rules, ruleKey)
if err := r.decrementSetCounter(rule); err != nil {
return fmt.Errorf("decrement set counter: %w", err)
}
return nil
}
// refreshRulesMap rebuilds the rule map from the kernel. This removes stale entries
// (e.g. from failed flushes) and updates handles for all existing rules.
func (r *router) refreshRulesMap() error {
var merr *multierror.Error
newRules := make(map[string]*nftables.Rule)View on GitHub (pinned to 93e97f4bf1)
Solutions
- Check 'sudo nft list ruleset' for the netbird prerouting rule: if it is already gone, the error is benign state skew.
- Treat ENOENT as success (errors.Is(err, unix.ENOENT)) since a missing rule is the desired end state.
- Ensure only one NetBird agent instance manages nftables on the host and stop external tooling from flushing its tables.
- Verify the daemon runs as root or with CAP_NET_ADMIN.
- Restart the agent to rebuild the rules map from the kernel if every removal fails.
Example fix
// before
if err := r.conn.DelRule(rule); err != nil {
return fmt.Errorf("remove prerouting rule %s -> %s: %w", pair.Source, pair.Destination, err)
}
// after: a rule that is already gone satisfies the removal intent
if err := r.conn.DelRule(rule); err != nil {
if errors.Is(err, unix.ENOENT) {
log.Warnf("prerouting rule %s -> %s already absent", pair.Source, pair.Destination)
delete(r.rules, ruleKey)
return nil
}
return fmt.Errorf("remove prerouting rule %s -> %s: %w", pair.Source, pair.Destination, err)
} Defensive patterns
Strategy: try-catch
Validate before calling
// Before removing, confirm the rule still exists under its UserData key
func preroutingRuleExists(conn *nftables.Conn, table *nftables.Table, chains map[string]*nftables.Chain, key string) (bool, error) {
for _, chain := range chains {
rules, err := conn.GetRules(table, chain)
if err != nil {
return false, err
}
for _, rl := range rules {
if string(rl.UserData) == key {
return true, nil
}
}
}
return false, nil
} Try / catch
if err := removeNat(pair); err != nil {
if errors.Is(err, unix.ENOENT) {
// rule already gone from the kernel: desired state reached
return nil
}
return fmt.Errorf("remove prerouting rule %s -> %s: %w", pair.Source, pair.Destination, err)
} Prevention
- Run exactly one NetBird agent per host and keep external nftables tooling from touching the netbird tables.
- Grant root or CAP_NET_ADMIN to the daemon.
- Prefer APIs that refresh handles (refreshRulesMap-backed removals) over cached-handle deletes when orchestrating from outside.
- Monitor logs for ENOENT clusters: they indicate a second writer on the ruleset.
When it happens
Trigger: RemoveNatRule on a masquerading pair (or its inverse pair) where the cached rule handle no longer matches kernel state: the rule was already removed by an external tool, the netlink socket errored, or the process lacks CAP_NET_ADMIN.
Common situations: firewalld reload, 'nft flush ruleset', iptables-nft or a second agent instance wiped the netbird table between refreshRulesMap and DelRule; agent containerized without NET_ADMIN; host hardening scripts rewriting nftables under the agent.
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/a0157c9b2c5d8f1d.
Report an issue: GitHub.