netbirdio/netbird · error
add v6 NAT rule: %w
Error message
add v6 NAT rule: %w
What it means
Dynamic-route NAT is mirrored into both families: the v4 rule was installed successfully, then the v6 mirror (firewall.ToV6NatPair) failed inside router6.AddNatRule. This leaves partial state - v4 NAT present, v6 NAT missing - so dynamic destinations resolving to v6 would not be masqueraded. The wrapped error comes from ip6tables operations on the nat table chains (NETBIRD-RT-NAT / POSTROUTING).
Source
Thrown at client/firewall/iptables/manager_linux.go:288
if pair.Destination.IsPrefix() && pair.Destination.Prefix.Addr().Is6() {
if !m.hasIPv6() {
return fmt.Errorf("add NAT rule: %w", firewall.ErrIPv6NotInitialized)
}
return m.router6.AddNatRule(pair)
}
if err := m.router.AddNatRule(pair); err != nil {
return err
}
// Dynamic routes need NAT in both tables since resolved IPs can be
// either v4 or v6. This covers both DomainSet (modern) and the legacy
// wildcard 0.0.0.0/0 destination where the client resolves DNS.
if m.hasIPv6() && pair.Dynamic {
v6Pair := firewall.ToV6NatPair(pair)
if err := m.router6.AddNatRule(v6Pair); err != nil {
return fmt.Errorf("add v6 NAT rule: %w", err)
}
}
return nil
}
func (m *Manager) RemoveNatRule(pair firewall.RouterPair) error {
m.mutex.Lock()
defer m.mutex.Unlock()
if pair.Destination.IsPrefix() && pair.Destination.Prefix.Addr().Is6() {
if !m.hasIPv6() {
return nil
}
return m.router6.RemoveNatRule(pair)
}
var merr *multierror.ErrorView on GitHub (pinned to 93e97f4bf1)
Solutions
- Roll back the v4 half with RemoveNatRule(pair) so state is consistent, then retry AddNatRule
- If persistent, inspect ip6tables -t nat -S | grep NETBIRD to see whether the routing chains still exist
- Restart the agent: Init re-creates all NETBIRD chains and the route manager re-applies pairs
- Check daemon logs for the wrapped ip6tables error to identify the missing module or chain
Example fix
// before
if err := mgr.AddNatRule(pair); err != nil {
return err // v4 rule stays installed, v6 missing
}
// after
if err := mgr.AddNatRule(pair); err != nil {
if rmErr := mgr.RemoveNatRule(pair); rmErr != nil {
log.Warnf("rollback v4 NAT after failed add: %v", rmErr)
}
return err
} Defensive patterns
Strategy: retry
Validate before calling
// sanity-check the v6 chains still exist before mirroring dynamic NAT
func v6ChainsPresent() bool {
out, err := exec.Command("ip6tables", "-t", "nat", "-S").Output()
return err == nil && strings.Contains(string(out), "NETBIRD-RT-NAT")
} Try / catch
if err := mgr.AddNatRule(pair); err != nil {
if !errors.Is(err, firewall.ErrIPv6NotInitialized) {
// v4 half is installed; roll it back before surfacing the error
if rmErr := mgr.RemoveNatRule(pair); rmErr != nil {
log.Warnf("rollback v4 NAT: %v", rmErr)
}
}
return err
} Prevention
- Roll back the v4 half when the v6 mirror fails so NAT state stays symmetric
- Re-add is idempotent (rules are keyed by pair content), so retry after transient ip6tables errors
- Prevent external tools from flushing NETBIRD chains while the agent runs
When it happens
Trigger: AddNatRule with pair.Dynamic true on a manager with hasIPv6() true, and a failing ip6tables Append - typically the NETBIRD chains were flushed by an external tool between Init and this call, or the kernel lacks the nat table/module.
Common situations: Docker/Podman/firewalld reload or iptables-restore running concurrently and wiping custom chains; containers with iptables but incomplete netfilter modules for v6.
Related errors
- add NAT rule: %w
- remove v6 NAT rule: %w
- update v6 set: %w
- create v6 acl manager: %w
- add peer filtering for %s: %w
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/ef02eb7b0316da40.
Report an issue: GitHub.