netbirdio/netbird · error

remove inbound DNAT: %w

Error message

remove inbound DNAT: %w

What it means

RemoveInboundDNAT for a v6 local address on a manager without the v6 half. It wraps firewall.ErrIPv6NotInitialized like the add path; note that in the current code this fires only when hasIPv6() is false - if the manager has v6, removal is delegated to router6 and cannot produce this sentinel.

Source

Thrown at client/firewall/iptables/manager_linux.go:486

	defer m.mutex.Unlock()

	if localAddr.Is6() {
		if !m.hasIPv6() {
			return fmt.Errorf("add inbound DNAT: %w", firewall.ErrIPv6NotInitialized)
		}
		return m.router6.AddInboundDNAT(localAddr, protocol, originalPort, translatedPort)
	}
	return m.router.AddInboundDNAT(localAddr, protocol, originalPort, translatedPort)
}

// RemoveInboundDNAT removes an inbound DNAT rule.
func (m *Manager) RemoveInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	if localAddr.Is6() {
		if !m.hasIPv6() {
			return fmt.Errorf("remove inbound DNAT: %w", firewall.ErrIPv6NotInitialized)
		}
		return m.router6.RemoveInboundDNAT(localAddr, protocol, originalPort, translatedPort)
	}
	return m.router.RemoveInboundDNAT(localAddr, protocol, originalPort, translatedPort)
}

// AddOutputDNAT adds an OUTPUT chain DNAT rule for locally-generated traffic.
func (m *Manager) AddOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	if localAddr.Is6() {
		if !m.hasIPv6() {
			return fmt.Errorf("add output DNAT: %w", firewall.ErrIPv6NotInitialized)
		}
		return m.router6.AddOutputDNAT(localAddr, protocol, originalPort, translatedPort)
	}
	return m.router.AddOutputDNAT(localAddr, protocol, originalPort, translatedPort)

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Treat as a skip in callers: the rule cannot exist on this manager, so nothing to remove
  2. Match errors.Is(err, firewall.ErrIPv6NotInitialized) and continue teardown of the remaining rules
  3. Restore v6 on the peer if v6 inbound DNAT must keep working

Example fix

// before
if err := mgr.RemoveInboundDNAT(localAddr, proto, origPort, xlatePort); err != nil {
	return err // aborts teardown of remaining rules
}

// after
if err := mgr.RemoveInboundDNAT(localAddr, proto, origPort, xlatePort); err != nil {
	if errors.Is(err, firewall.ErrIPv6NotInitialized) {
		log.Debugf("no v6 manager; v6 inbound DNAT rule cannot exist")
	} else {
		return err
	}
}
Defensive patterns

Strategy: validation

Validate before calling

if localAddr.Is6() && !wgIface.Address().HasIPv6() {
    return nil // the rule cannot exist on this manager; nothing to remove
}

Try / catch

if err := mgr.RemoveInboundDNAT(localAddr, proto, origPort, xlatePort); err != nil {
    if errors.Is(err, firewall.ErrIPv6NotInitialized) {
        log.Debugf("v6 inbound DNAT rule cannot exist without v6 manager")
        return nil // continue teardown of remaining rules
    }
    return err
}

Prevention

When it happens

Trigger: Calling RemoveInboundDNAT with localAddr.Is6() true while m.ipv6Client == nil - e.g. tearing down a v6 inbound rule after the peer restarted without v6.

Common situations: Service teardown after the agent reconnected v4-only; configuration drift where stored rules reference v6 addresses the current overlay cannot manage.

Related errors


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