slackhq/nebula · error

failed to set tun device as nonblocking: %w

Error message

failed to set tun device as nonblocking: %w

What it means

After opening the FreeBSD tun device fd, newTun puts it into non-blocking mode with unix.SetNonblock so reads/writes integrate with Poll. If that fcntl call fails, the fd is closed and this wrapped error is returned. The wrapped inner error (from syscall) gives the actual cause.

Source

Thrown at overlay/tun_freebsd.go:303

func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*tun, error) {
	// Try to open existing tun device
	var fd int
	var err error
	deviceName := c.GetString("tun.dev", "")
	if deviceName != "" {
		fd, err = unix.Open("/dev/"+deviceName, os.O_RDWR, 0)
	}
	if errors.Is(err, fs.ErrNotExist) || deviceName == "" {
		// If the device doesn't already exist, request a new one and rename it
		fd, err = unix.Open("/dev/tun", os.O_RDWR, 0)
	}
	if err != nil {
		return nil, err
	}

	if err = unix.SetNonblock(fd, true); err != nil {
		_ = unix.Close(fd)
		return nil, fmt.Errorf("failed to set tun device as nonblocking: %w", err)
	}

	// Shutdown pipe lets Close wake any reader/writer blocked in Poll.
	var pipeFds [2]int
	if err = unix.Pipe2(pipeFds[:], unix.O_CLOEXEC|unix.O_NONBLOCK); err != nil {
		_ = unix.Close(fd)
		return nil, fmt.Errorf("failed to create shutdown pipe: %w", err)
	}
	shutdownR, shutdownW := pipeFds[0], pipeFds[1]

	closeOnErr := true
	defer func() {
		if closeOnErr {
			_ = unix.Close(fd)
			_ = unix.Close(shutdownR)
			_ = unix.Close(shutdownW)
		}
	}()

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Check the wrapped errno (%w target) — EBADF means the fd from the open was invalid; EMFILE/ENFILE means fd exhaustion (raise ulimit -n)
  2. Retry tun device creation once the fd limit issue is resolved
  3. Verify the process has permission to open /dev/tun and that the device node exists
  4. Ensure no other goroutine closed the fd concurrently before SetNonblock runs

Example fix

// before
if err = unix.SetNonblock(fd, true); err != nil {
    _ = unix.Close(fd)
    return nil, fmt.Errorf("failed to set tun device as nonblocking: %w", err)
}
// after (surface fd limit guidance)
if err = unix.SetNonblock(fd, true); err != nil {
    _ = unix.Close(fd)
    var rl syscall.Rlimit
    syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rl)
    return nil, fmt.Errorf("failed to set tun device as nonblocking (fd=%d, nofile cur=%d): %w", fd, rl.Cur, err)
}
Defensive patterns

Strategy: retry

Validate before calling

var rl syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rl); err == nil && rl.Cur < 1024 {
    rl.Cur = 4096
    _ = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rl)
}

Try / catch

tunDev, err := newTun(cfg, log, prefixes, false)
if err != nil && strings.Contains(err.Error(), "set tun device as nonblocking") {
    // fd invalid or limit hit; log wrapped errno, free fds, retry once
    log.Error("tun nonblock setup failed", "err", err)
    time.Sleep(500 * time.Millisecond)
    tunDev, err = newTun(cfg, log, prefixes, false)
}

Prevention

When it happens

Trigger: unix.SetNonblock(fd, true) returns an error immediately after opening the tun device — typically an EBADF/EBUSY from the freshly opened /dev/tun fd, or exhaustion of descriptors.

Common situations: File descriptor table exhaustion under heavy load; opening a device path that yielded an invalid fd; sandbox/container environments restricting fcntl on device nodes; races where another process closed the fd.

Related errors


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