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
- Verify the fd value passed to newTunFromFd comes directly from the NEPacketTunnelProvider packetFlow and is still open.
- Check that your extension code does not close or overwrite the fd before nebula takes ownership.
- Inspect the wrapped errno (%w) to distinguish EBADF (bad fd) from other fcntl failures.
- 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
- Pass the fd straight from NEPacketTunnelProvider; never close or duplicate it first.
- Validate fd > 0 before calling newTunFromFd.
- Hand over ownership exactly once during tunnel startup.
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
- unable to determine IP version from packet
- newTunFromFd not supported in FreeBSD
- failed to set tun device as nonblocking: %w
- failed to create shutdown pipe: %w
- newTun not supported in iOS
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/67d165c62ce88b93.
Report an issue: GitHub.