netbirdio/netbird · error
set promiscuous mode: %s
Error message
set promiscuous mode: %s
What it means
Returned when the gVisor stack refuses SetPromiscuousMode(nicID, true), which enables the NIC to accept packets addressed to any destination. The stack returns an error if the NIC ID is unknown (already removed) or the NIC endpoint does not permit the operation. Here the NIC was just created a few lines above, so failure indicates a lifecycle race or an endpoint (the WireGuard device-backed linkEndpoint) that was closed underneath the constructor.
Source
Thrown at client/firewall/uspfilter/forwarder/forwarder.go:129
defaultSubnet, err := tcpip.NewSubnet(
tcpip.AddrFrom4([4]byte{0, 0, 0, 0}),
tcpip.MaskFromBytes([]byte{0, 0, 0, 0}),
)
if err != nil {
return nil, fmt.Errorf("creating default subnet: %w", err)
}
defaultSubnetV6, err := tcpip.NewSubnet(
tcpip.AddrFrom16([16]byte{}),
tcpip.MaskFromBytes(make([]byte, 16)),
)
if err != nil {
return nil, fmt.Errorf("creating default v6 subnet: %w", err)
}
if err := s.SetPromiscuousMode(nicID, true); err != nil {
return nil, fmt.Errorf("set promiscuous mode: %s", err)
}
if err := s.SetSpoofing(nicID, true); err != nil {
return nil, fmt.Errorf("set spoofing: %s", err)
}
s.SetRouteTable([]tcpip.Route{
{Destination: defaultSubnet, NIC: nicID},
{Destination: defaultSubnetV6, NIC: nicID},
})
ctx, cancel := context.WithCancel(context.Background())
f := &Forwarder{
logger: logger,
flowLogger: flowLogger,
stack: s,
endpoint: endpoint,
udpForwarder: newUDPForwarder(mtu, logger, flowLogger),
ctx: ctx,View on GitHub (pinned to 93e97f4bf1)
Solutions
- Serialize interface bring-up and teardown so Close fully joins the old forwarder's goroutines before a new New() runs
- Check that iface.GetWGDevice() returns a live device before constructing the Forwarder; skip or retry when it is nil
- Retry forwarder.New once after the engine settles if the error text wraps ErrUnknownNIC/unknown device
- Upgrade gVisor if the wrapped error suggests a promiscuous-mode capability regression
Defensive patterns
Strategy: try-catch
Validate before calling
// ensure the device backing the endpoint is alive before constructing
if iface.GetWGDevice() == nil {
return errors.New("wireguard device not ready; retry after interface up")
} Type guard
func deviceReady(iface common.IFaceMapper) bool {
return iface != nil && iface.GetWGDevice() != nil
} Try / catch
f, err := forwarder.New(...)
if err != nil {
if strings.Contains(err.Error(), "set promiscuous mode") && deviceReady(iface) {
// teardown race: recreate interface and retry once
}
return err
} Prevention
- Hold the engine lifecycle mutex across New/Stop so they cannot interleave
- Join the previous forwarder's goroutines (done channel) before rebuilding
- Retry bring-up once after the interface settles instead of failing hard on the race
When it happens
Trigger: Concurrent Close/teardown of the engine while forwarder.New runs; the wrapped endpoint (iface.GetWGDevice()) becoming nil or closed mid-construction; a second New() after the previous forwarder tore down the same stack.
Common situations: Rapid netbird up/down cycles or back-to-back BindUpdate operations racing the userspace firewall initialization; mobile platforms (iOS/Android) where the interface is recreated on connectivity changes and the device handle can be invalidated quickly.
Related errors
- set spoofing: %s
- add IPv6 protocol address: %s
- creating default subnet: %w
- creating default v6 subnet: %w
- panic: %v
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/51769aa920b445ec.
Report an issue: GitHub.