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

  1. Inspect kernel state with 'sudo nft list tables' and 'sudo nft list table <netbird table>' to see whether the chain still exists.
  2. Run the agent as root or grant CAP_NET_ADMIN for netlink dumps.
  3. If the table was flushed externally, restart the agent so init recreates tables, chains and rules.
  4. 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

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


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/79ec4f1a9f7ad2d9. Report an issue: GitHub.