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
- Check the wrapped errno (%w target) — EBADF means the fd from the open was invalid; EMFILE/ENFILE means fd exhaustion (raise ulimit -n)
- Retry tun device creation once the fd limit issue is resolved
- Verify the process has permission to open /dev/tun and that the device node exists
- 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
- Monitor and raise RLIMIT_NOFILE for long-running daemons
- Ensure the fd from device open is used immediately, not closed concurrently
- Check process permissions on /dev/tun before construction
- Log the wrapped errno to distinguish EBADF from EMFILE
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
- newTunFromFd not supported in FreeBSD
- failed to create shutdown pipe: %w
- SetNonblock: %v
- unable to determine IP version from packet
- failed to set tun address %s: %s
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/6b9a4d705530049b.
Report an issue: GitHub.