netbirdio/netbird · error

panic: %v

Error message

panic: %v

What it means

UpdateLocalIPs recovers any panic in the local-IP snapshot rebuild and converts it to a regular error string. The recover guard exists because the function reads live interface state (iface.Address(), net.Interfaces, per-interface Addrs) that can mutate concurrently when the overlay interface is being reconfigured or torn down mid-enumeration. The %v formatting flattens the panic value, so the original stack trace is lost, making the cause hard to pinpoint from the message alone.

Source

Thrown at client/firewall/uspfilter/localip.go:66

		}

		parsed, ok := netip.AddrFromSlice(ip)
		if !ok {
			log.Warnf("invalid IP address %s in interface %s", ip.String(), iface.Name)
			continue
		}

		parsed = parsed.Unmap()
		ips[parsed] = struct{}{}
		*addresses = append(*addresses, parsed)
	}
}

// UpdateLocalIPs rebuilds the local IP snapshot and swaps it in atomically.
func (m *localIPManager) UpdateLocalIPs(iface common.IFaceMapper) (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("panic: %v", r)
		}
	}()

	ips := make(map[netip.Addr]struct{})
	var addresses []netip.Addr

	// loopback
	ips[netip.AddrFrom4([4]byte{127, 0, 0, 1})] = struct{}{}
	ips[netip.IPv6Loopback()] = struct{}{}

	if iface != nil {
		ip := iface.Address().IP
		ips[ip] = struct{}{}
		addresses = append(addresses, ip)
		if v6 := iface.Address().IPv6; v6.IsValid() {
			ips[v6] = struct{}{}
			addresses = append(addresses, v6)
		}

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Reproduce with the panic's runtime stack: temporarily log debug.Stack() in the recover block to capture where it originated
  2. Serialize calls: do not invoke UpdateLocalIPs concurrently with interface address changes; hook it after the address swap completes
  3. If using a custom common.IFaceMapper, make its Address() method return a value copy safe against concurrent mutation
  4. Upgrade NetBird if the panic originates inside shipped iface code, and report the captured stack

Example fix

// before
defer func() {
    if r := recover(); r != nil {
        err = fmt.Errorf("panic: %v", r)
    }
}()

// after
defer func() {
    if r := recover(); r != nil {
        err = fmt.Errorf("panic: %v\n%s", r, debug.Stack())
    }
}()
Defensive patterns

Strategy: fallback

Validate before calling

// keep the previous snapshot when a refresh fails
if err := m.localIPs.UpdateLocalIPs(iface); err != nil {
    log.Warnf("local IP refresh failed, keeping previous snapshot: %v", err)
}

Type guard

func (m *localIPManager) hasSnapshot() bool {
    return m.snapshot.Load() != nil
}

Try / catch

// the recover is inside the library; callers just handle the error
if err := m.UpdateLocalIPs(iface); err != nil {
    if strings.HasPrefix(err.Error(), "panic:") {
        log.Warnf("snapshot refresh panicked; retaining last-known IPs: %v", err)
        return // previous snapshot still serves IsLocalIP
    }
    return err
}

Prevention

When it happens

Trigger: A panic inside iface.Address() when the address mapper is being swapped concurrently; a panic while iterating interfaces that disappear mid-iteration on platforms with volatile network state; theoretically any nil-dereference on the IFaceMapper implementation provided by a different package version.

Common situations: Interface reconfiguration races (address change, v6 enable/disable toggling) while the periodic local-IP refresh runs; VPN or container interfaces appearing/vanishing during enumeration on macOS/Windows; version skew between the uspfilter package and a custom IFaceMapper implementation.

Related errors


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