netbirdio/netbird · error
list rules for chain %s: %w
Error message
list rules for chain %s: %w
What it means
refreshRulesMap rebuilds r.rules by dumping every netbird chain with nftables.Conn.GetRules; this error wraps a per-chain NFT_MSG_GETRULE dump failure. The function deliberately keeps the previously cached entries for the failing chain, continues with the other chains, and returns all failures as a multierror. It gates AddNatRule, RemoveNatRule, DeleteDNATRule and RemoveInboundDNAT, so those surface it as 'refresh rules map: list rules for chain ...'.
Source
Thrown at client/firewall/nftables/router_linux.go:1536
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)
for _, chain := range r.chains {
rules, err := r.conn.GetRules(chain.Table, chain)
if err != nil {
merr = multierror.Append(merr, fmt.Errorf("list rules for chain %s: %w", chain.Name, err))
// preserve existing entries for this chain since we can't verify their state
for k, v := range r.rules {
if v.Chain != nil && v.Chain.Name == chain.Name {
newRules[k] = v
}
}
continue
}
for _, rule := range rules {
if len(rule.UserData) > 0 {
newRules[string(rule.UserData)] = rule
}
}
}
r.rules = newRules
return nberrors.FormatErrorOrNil(merr)
}
View on GitHub (pinned to 93e97f4bf1)
Solutions
- Inspect kernel state with 'sudo nft list tables' and 'sudo nft list table <netbird table>' to see whether the chain still exists.
- Run the agent as root or grant CAP_NET_ADMIN for netlink dumps.
- If the table was flushed externally, restart the agent so init recreates tables, chains and rules.
- Match the chain name in the nested error against 'nft list chains' to spot renames or backend switches.
Example fix
// before
if err := r.refreshRulesMap(); err != nil {
return fmt.Errorf(refreshRulesMapError, err)
}
// after: per-chain failures already preserve cached entries; report and let callers retry
if err := r.refreshRulesMap(); err != nil {
log.Warnf("rules map refresh incomplete, cached handles kept: %v", err)
} Defensive patterns
Strategy: retry
Validate before calling
// Preflight: verify the netbird chains still exist before a refresh-dependent operation
func chainsPresent(conn *nftables.Conn, table *nftables.Table, names []string) error {
for _, name := range names {
if _, err := conn.GetChain(table, name); err != nil {
return fmt.Errorf("chain %s missing: %w", name, err)
}
}
return nil
} Try / catch
if err := r.refreshRulesMap(); err != nil {
if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.EAGAIN) {
// dump raced a ruleset change: retry once before failing
if err2 := r.refreshRulesMap(); err2 == nil {
return nil
}
}
return fmt.Errorf(refreshRulesMapError, err)
} Prevention
- Never flush the netbird tables externally while routes are configured; resync tooling with the agent lifecycle instead.
- Run the agent with CAP_NET_ADMIN so dumps cannot fail on permissions.
- Keep rulesets modest in size or raise netlink buffer limits on hosts with many rules.
When it happens
Trigger: GetRules returns a netlink error: the table or chain no longer exists (ENOENT after an external flush), the process lacks CAP_NET_ADMIN, or a very large ruleset truncates the netlink dump.
Common situations: Configuration management or host hardening wipes nftables state under the agent; agent in a container without NET_ADMIN; firewalld switching backends and recreating tables; huge ACL rulesets.
Related errors
- decrement set counter: %w
- error adding set %s: %w
- flush error: %w
- error adding prefixes (%d) to set %s: %w
- remove prerouting rule %s -> %s: %w
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/79ec4f1a9f7ad2d9.
Report an issue: GitHub.