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

  1. Guarantee single construction: initForwarder already returns early when m.forwarder is non-nil - ensure all callers go through it under the manager mutex
  2. Validate the device endpoint before New (non-nil device, mtu set) and fail with a precise message
  3. Use a fresh stack per forwarder instance so NICID 1 can never collide
  4. 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

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


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