netbirdio/netbird · error
create NIC: %v
Error message
create NIC: %v
What it means
Returned by forwarder.New (client/firewall/uspfilter/forwarder/forwarder.go:84) when the gVisor netstack rejects s.CreateNIC(1, endpoint): tcpip errors such as errInvalidNICID (duplicate NIC 1 when New is called twice on one stack), a nil/misbehaving endpoint (LinkEndpoint hooks), or stack resource limits. The endpoint wraps the WireGuard device for injecting packets, so a device that cannot be attached also lands here. Note the wrapper uses %v, so the tcpip error string is embedded without an error chain.
Source
Thrown at client/firewall/uspfilter/forwarder/forwarder.go:84
},
TransportProtocols: []stack.TransportProtocolFactory{
tcp.NewProtocol,
udp.NewProtocol,
icmp.NewProtocol4,
icmp.NewProtocol6,
},
HandleLocal: false,
})
nicID := tcpip.NICID(1)
endpoint := &endpoint{
logger: logger,
device: iface.GetWGDevice(),
}
endpoint.mtu.Store(uint32(mtu))
if err := s.CreateNIC(nicID, endpoint); err != nil {
return nil, fmt.Errorf("create NIC: %v", err)
}
protoAddr := tcpip.ProtocolAddress{
Protocol: ipv4.ProtocolNumber,
AddressWithPrefix: tcpip.AddressWithPrefix{
Address: tcpip.AddrFrom4(iface.Address().IP.As4()),
PrefixLen: iface.Address().Network.Bits(),
},
}
if err := s.AddProtocolAddress(nicID, protoAddr, stack.AddressProperties{}); err != nil {
return nil, fmt.Errorf("failed to add protocol address: %s", err)
}
if v6 := iface.Address().IPv6; v6.IsValid() {
v6Addr := tcpip.ProtocolAddress{
Protocol: ipv6.ProtocolNumber,
AddressWithPrefix: tcpip.AddressWithPrefix{View on GitHub (pinned to 93e97f4bf1)
Solutions
- Guarantee single construction: initForwarder already returns early when m.forwarder is non-nil - ensure all callers go through it under the manager mutex
- Validate the device endpoint before New (non-nil device, mtu set) and fail with a precise message
- Use a fresh stack per forwarder instance so NICID 1 can never collide
- If hit after an upgrade, check the gvisor version's CreateNIC contract for new error conditions
Example fix
// before
if err := s.CreateNIC(nicID, endpoint); err != nil {
return nil, fmt.Errorf("create NIC: %v", err)
}
// after - wrap the error and include the NIC id for diagnosis
if err := s.CreateNIC(nicID, endpoint); err != nil {
return nil, fmt.Errorf("create NIC %d: %w", nicID, err)
} Defensive patterns
Strategy: validation
Validate before calling
if wgIface.GetWGDevice() == nil {
return fmt.Errorf("cannot init forwarder: no userspace device to attach")
}
if mtu == 0 {
mtu = iface.DefaultMTU
}
_ = fw.EnableRouting() Type guard
func canCreateForwarder(i common.IFaceMapper, mtu int) bool {
return i.GetWGDevice() != nil && i.Address().IP.IsValid() && mtu > 0
} Try / catch
if err := fw.EnableRouting(); err != nil {
if strings.Contains(err.Error(), "create NIC") {
// stack is in an unknown state; rebuild the forwarder once
fw.Reset()
if retryErr := fw.EnableRouting(); retryErr == nil {
return nil
}
}
return err
} Prevention
- Ensure only one forwarder is ever constructed per manager (guard with atomic pointer check)
- Give each forwarder its own gVisor stack to make NICID collisions impossible
- Wrap tcpip errors with %w so NIC creation failures are matchable by callers
When it happens
Trigger: initForwarder racing so forwarder.New runs twice against a shared stack (NIC 1 already exists); endpoint.device nil because GetWGDevice() returned a non-nil but unusable device; gVisor stack created with options incompatible with a custom endpoint.
Common situations: Concurrent EnableRouting/determineRouting paths constructing the forwarder twice; version drift in gvisor/netstack changing CreateNIC semantics; embedded netstack mode with a partially wired device.
Related errors
- create forwarder: %w
- failed to add protocol address: %s
- add IPv6 protocol address: %s
- creating default subnet: %w
- creating default v6 subnet: %w
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/69ea3076548feeed.
Report an issue: GitHub.