slackhq/nebula · error
failed to create shutdown pipe
Error message
failed to create shutdown pipe: %w
What it means
newTun creates a nonblocking CLOEXEC pipe used as a shutdown signal: Close() writes to it so goroutines blocked in Poll wake up. If unix.Pipe2 with O_CLOEXEC|O_NONBLOCK fails, the tun fd is closed and this wrapped error is returned. Without the pipe, graceful shutdown of the device reader would hang, so construction is aborted.
Solutions
- Free file descriptors / fix the fd leak; check with lsof or procstat how many fds the process holds
- Raise the soft RLIMIT_NOFILE (ulimit -n) for the daemon
- Check the wrapped errno to distinguish EMFILE (per-process) from ENFILE (system-wide)
- Retry device creation after shedding load
Defensive patterns
Strategy: retry
Validate before calling
var rl syscall.Rlimit
syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rl)
if int(rl.Cur)-countOpenFds() < 8 {
return errors.New("too few free file descriptors to create tun + shutdown pipe")
} Try / catch
tunDev, err := newTun(cfg, log, prefixes, false)
if err != nil && strings.Contains(err.Error(), "failed to create shutdown pipe") {
log.Error("pipe creation failed (likely fd exhaustion)", "err", err)
// alert/rotate, then retry after fds are freed
} Prevention
- Fix fd leaks; audit with lsof/procstat on long-running processes
- Raise ulimit -n for the daemon's service unit
- Distinguish EMFILE (process) vs ENFILE (system) from the wrapped errno
- Keep shutdown-pipe creation close to device open to limit fd pressure window
When it happens
Trigger: unix.Pipe2(pipeFds[:], unix.O_CLOEXEC|unix.O_NONBLOCK) fails during newTun — practically always due to hitting the process or system file descriptor limit (EMFILE/ENFILE), or an old kernel lacking pipe2.
Common situations: Long-running daemons leaking fds until pipe() returns EMFILE; heavily loaded hosts at the kernel file-max; running on stripped-down FreeBSD environments where pipe2 support or fd limits are constrained.
Related errors
- failed to set tun device as nonblocking
- 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/bb3a34b2e6b44593.
Report an issue: GitHub.
Appendix: source
Thrown at overlay/tun_freebsd.go:310
}
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)
}
}()
// Read the name of the interface
var name [16]byte
arg := fiodgnameArg{length: 16, buf: unsafe.Pointer(&name)}
ctrlErr := ioctl(uintptr(fd), FIODGNAME, uintptr(unsafe.Pointer(&arg)))
if ctrlErr == nil {View on GitHub (pinned to dd8f660c0a)