slackhq/nebula · error

failed to set the tun fd to non-blocking mode: %w

Error message

failed to set the tun fd to non-blocking mode: %w

What it means

newTunFromFd (overlay/tun_ios.go:41) receives a TUN file descriptor from the iOS NetworkExtension and must put it into non-blocking mode with unix.SetNonblock. This error wraps a failure of that call, aborting tun setup; the fd is closed before returning.

Source

Thrown at overlay/tun_ios.go:41

)

type tun struct {
	io.ReadWriteCloser
	vpnNetworks []netip.Prefix
	Routes      atomic.Pointer[[]Route]
	routeTree   atomic.Pointer[bart.Table[routing.Gateways]]
	l           *slog.Logger
}

func newTun(_ *config.C, _ *slog.Logger, _ []netip.Prefix, _ bool) (*tun, error) {
	return nil, fmt.Errorf("newTun not supported in iOS")
}

func newTunFromFd(c *config.C, l *slog.Logger, deviceFd int, vpnNetworks []netip.Prefix) (*tun, error) {
	if err := unix.SetNonblock(deviceFd, true); err != nil {
		// We own the fd from the moment it is handed to us, same as the reload error path below
		_ = unix.Close(deviceFd)
		return nil, fmt.Errorf("failed to set the tun fd to non-blocking mode: %w", err)
	}

	file := os.NewFile(uintptr(deviceFd), "/dev/tun")
	t := &tun{
		vpnNetworks:     vpnNetworks,
		ReadWriteCloser: &tunReadCloser{f: file},
		l:               l,
	}

	err := t.reload(c, true)
	if err != nil {
		_ = file.Close()
		return nil, err
	}

	c.RegisterReloadCallback(func(c *config.C) {
		err := t.reload(c, false)
		if err != nil {

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Verify the fd value passed to newTunFromFd comes directly from the NEPacketTunnelProvider packetFlow and is still open.
  2. Check that your extension code does not close or overwrite the fd before nebula takes ownership.
  3. Inspect the wrapped errno (%w) to distinguish EBADF (bad fd) from other fcntl failures.
  4. Ensure the fd is handed over once, exactly at tunnel startup, matching the ownership transfer expectation in the code.

Example fix

// before
fd := 0 // placeholder/wrong fd
// after
fd := int(packetFlow.ValueForKeyPath("fileDescriptor")) // valid fd from packet flow
if fd <= 0 { return errors.New("invalid tun fd") }
Defensive patterns

Strategy: validation

Validate before calling

if deviceFd <= 0 {
	return errors.New("invalid tun file descriptor from NetworkExtension")
}
// confirm fd is live before handing to newTunFromFd
if _, err := unix.FcntlInt(uintptr(deviceFd), unix.F_GETFD, 0); err != nil {
	return fmt.Errorf("tun fd not usable: %w", err)
}

Type guard

func validFd(fd int) bool {
	_, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0)
	return err == nil
}

Try / catch

t, err := newTunFromFd(c, l, deviceFd, vpnNetworks)
if err != nil && strings.Contains(err.Error(), "non-blocking mode") {
	// fd was bad or already closed; reacquire from packetFlow before retry
}

Prevention

When it happens

Trigger: unix.SetNonblock(deviceFd, true) fails, e.g. the fd is invalid (EBADF) because the NetworkExtension handed over a closed or wrong fd, or an underlying fcntl error on the inherited descriptor.

Common situations: Passing an fd that was already closed or duplicated incorrectly in the Packet Tunnel Provider extension; fd inheritance issues when the provider extension reconfigures the flow; passing a non-socket/non-file integer by mistake.

Related errors


AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03). Data as JSON: /api/errors/67d165c62ce88b93. Report an issue: GitHub.