netbirdio/netbird · error · firewall.ErrIPv6NotInitialized

add peer filtering for %s: %w

Error message

add peer filtering for %s: %w

What it means

AddPeerFiltering was called with an IPv6 peer IP (ip.To4() == nil) while the manager has no v6 half: m.ipv6Client is nil because wgIface.Address().HasIPv6() was false when Create ran. It wraps the sentinel firewall.ErrIPv6NotInitialized (client/firewall/manager/firewall.go:17), so the mismatch is programmatically detectable with errors.Is. The check fails fast rather than silently installing a v6 address into the v4 chain or ipset.

Source

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

//
// Comment will be ignored because some system this feature is not supported
func (m *Manager) AddPeerFiltering(
	id []byte,
	ip net.IP,
	proto firewall.Protocol,
	sPort *firewall.Port,
	dPort *firewall.Port,
	action firewall.Action,
	ipsetName string,
) ([]firewall.Rule, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	if ip.To4() != nil {
		return m.aclMgr.AddPeerFiltering(id, ip, proto, sPort, dPort, action, ipsetName)
	}
	if !m.hasIPv6() {
		return nil, fmt.Errorf("add peer filtering for %s: %w", ip, firewall.ErrIPv6NotInitialized)
	}
	return m.aclMgr6.AddPeerFiltering(id, ip, proto, sPort, dPort, action, ipsetName)
}

func (m *Manager) AddRouteFiltering(
	id []byte,
	sources []netip.Prefix,
	destination firewall.Network,
	proto firewall.Protocol,
	sPort, dPort *firewall.Port,
	action firewall.Action,
) (firewall.Rule, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	if isIPv6RouteRule(sources, destination) {
		if !m.hasIPv6() {
			return nil, fmt.Errorf("add route filtering: %w", firewall.ErrIPv6NotInitialized)

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Give the peer an IPv6 address (enable the v6 range in the management network) and restart/reconnect so Create builds the v6 half
  2. If v6 is unsupported locally, stop distributing v6 ACLs/routes to this peer from management
  3. In custom callers, skip v6 peers when the interface has no v6 - the engine already does this at client/internal/engine.go:1842

Example fix

// before
rules, err := mgr.AddPeerFiltering(id, peerIP, proto, sPort, dPort, action, ipset)
if err != nil {
	return err // v6 peer on v4-only manager aborts the whole ACL apply
}

// after
if peerIP.To4() == nil && !wgIface.Address().HasIPv6() {
	log.Debugf("skipping v6 peer %s: no v6 overlay", peerIP)
	continue
}
rules, err := mgr.AddPeerFiltering(id, peerIP, proto, sPort, dPort, action, ipset)
Defensive patterns

Strategy: validation

Validate before calling

// skip v6 peer rules before touching the firewall manager
func canFilterPeer(mgrFacingV6 bool, peerIP net.IP) bool {
    if peerIP.To4() != nil {
        return true
    }
    return mgrFacingV6 // mgrFacingV6 == wgIface.Address().HasIPv6() at Create time
}

Type guard

func isFilterableIP(ip net.IP, ifaceHasV6 bool) bool {
    return ip.To4() != nil || ifaceHasV6
}

Try / catch

rules, err := mgr.AddPeerFiltering(id, ip, proto, sPort, dPort, action, ipsetName)
if err != nil {
    if errors.Is(err, firewall.ErrIPv6NotInitialized) {
        log.Debugf("skipping v6 peer %s: no v6 firewall", ip)
        continue
    }
    return err
}

Prevention

When it happens

Trigger: Passing a 16-byte net.IP (a v6 peer address from the network map) to AddPeerFiltering on a manager constructed while the local overlay address had no IPv6 part.

Common situations: Management network has an IPv6 range so peers carry v6 addresses, but this peer was assigned a v4-only address; the peer connected before v6 was enabled and was not restarted; the kernel or interface had no v6 at startup so the WgAddr lacks the v6 component.

Related errors


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