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
- Reproduce with the panic's runtime stack: temporarily log debug.Stack() in the recover block to capture where it originated
- Serialize calls: do not invoke UpdateLocalIPs concurrently with interface address changes; hook it after the address swap completes
- If using a custom common.IFaceMapper, make its Address() method return a value copy safe against concurrent mutation
- 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
- Treat UpdateLocalIPs as best-effort: never fail the caller's flow on it
- Serialize refreshes with interface address changes to shrink the race window
- Capture debug.Stack() in your own recover blocks so panics stay diagnosable
- Never pass an IFaceMapper implementation whose Address() can panic on concurrent reads
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.