netbirdio/netbird · error
create NAT output chain: %w
Error message
create NAT output chain: %w
What it means
ensureNATOutputChain lazily creates the netbird-nat-output chain (NAT type, OUTPUT hook, NAT-destination priority) on first AddOutputDNAT and commits it with Flush. 'create NAT output chain' wraps that commit failure; the chain is removed from r.chains on failure so a later call retries creation from a clean slate.
Source
Thrown at client/firewall/nftables/router_linux.go:2000
}
// ensureNATOutputChain lazily creates the OUTPUT NAT chain on first use.
func (r *router) ensureNATOutputChain() error {
if _, exists := r.chains[chainNameNATOutput]; exists {
return nil
}
r.chains[chainNameNATOutput] = r.conn.AddChain(&nftables.Chain{
Name: chainNameNATOutput,
Table: r.workTable,
Hooknum: nftables.ChainHookOutput,
Priority: nftables.ChainPriorityNATDest,
Type: nftables.ChainTypeNAT,
})
if err := r.conn.Flush(); err != nil {
delete(r.chains, chainNameNATOutput)
return fmt.Errorf("create NAT output chain: %w", err)
}
return nil
}
// AddOutputDNAT adds an OUTPUT chain DNAT rule for locally-generated traffic.
func (r *router) AddOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
ruleID := fmt.Sprintf("output-dnat-%s-%s-%d-%d", localAddr.String(), protocol, originalPort, translatedPort)
if _, exists := r.rules[ruleID]; exists {
return nil
}
if err := r.ensureNATOutputChain(); err != nil {
return err
}
protoNum, err := r.af.protoNum(protocol)
if err != nil {View on GitHub (pinned to 93e97f4bf1)
Solutions
- Check 'sudo nft list chains' for netbird-nat-output; if present but unknown to the agent, remove it or restart the agent to adopt it.
- Verify the parent table still exists ('sudo nft list tables').
- Ensure root or CAP_NET_ADMIN.
- Retry the operation that triggered chain creation; the code path is self-healing after the map rollback.
Example fix
// before
if err := r.conn.Flush(); err != nil {
delete(r.chains, chainNameNATOutput)
return fmt.Errorf("create NAT output chain: %w", err)
}
// after: adopt a pre-existing chain instead of failing
if err := r.conn.Flush(); err != nil {
if errors.Is(err, unix.EEXIST) {
log.Warnf("NAT output chain already exists, adopting it")
return nil
}
delete(r.chains, chainNameNATOutput)
return fmt.Errorf("create NAT output chain: %w", err)
} Defensive patterns
Strategy: retry
Validate before calling
// Confirm the chain is absent before triggering lazy creation
func natOutputChainExists(conn *nftables.Conn, table *nftables.Table) bool {
_, err := conn.GetChain(table, chainNameNATOutput)
return err == nil
} Try / catch
if err := r.conn.Flush(); err != nil {
if errors.Is(err, unix.EEXIST) {
// chain already in kernel: adopt instead of failing
return nil
}
delete(r.chains, chainNameNATOutput)
return fmt.Errorf("create NAT output chain: %w", err)
} Prevention
- Clean up netbird chains on agent shutdown so later runs do not meet EEXIST.
- Keep external nftables files from defining chains in the netbird table.
- Retry the triggering operation after a failed chain creation; the rollback makes it safe.
When it happens
Trigger: EEXIST when the chain already exists in the kernel but not in the in-memory map (state skew after external changes or restart); the workTable was flushed so NEWCHAIN targets a missing table; EPERM without CAP_NET_ADMIN.
Common situations: Leftover chains from a previous agent run; firewalld or hand-written nftables files defining conflicting chains; containerized agents lacking privileges.
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/15cf7bd2f759b853.
Report an issue: GitHub.