slackhq/nebula · error
failed to set tun device as nonblocking
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.
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
- failed to create shutdown pipe
- newTunFromFd not supported in FreeBSD
- error closing tun file
- error closing tun file
- failed to get syscall conn for tun
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/6b9a4d705530049b.
Report an issue: GitHub.
Appendix: 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)