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.Error

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Roll back the v4 half with RemoveNatRule(pair) so state is consistent, then retry AddNatRule
  2. If persistent, inspect ip6tables -t nat -S | grep NETBIRD to see whether the routing chains still exist
  3. Restart the agent: Init re-creates all NETBIRD chains and the route manager re-applies pairs
  4. 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

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


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