slackhq/nebula · error

newTunFromFd not supported in FreeBSD

Error message

newTunFromFd not supported in FreeBSD

What it means

newTunFromFd is the constructor that adopts a caller-supplied file descriptor for a tun device. On FreeBSD this constructor is intentionally unimplemented and always returns this error, because the platform tun code manages its own device lifecycle (opening /dev/tun, nonblocking setup, shutdown pipe) and cannot safely adopt a foreign fd.

Source

Thrown at overlay/tun_freebsd.go:282

			ifreq := ifreqDestroy{Name: t.deviceBytes()}
			err = ioctl(uintptr(s), syscall.SIOCIFDESTROY, uintptr(unsafe.Pointer(&ifreq)))
		}
		if err != nil {
			t.l.Error("Error destroying tunnel", "error", err)
		}
	}()

	// wait up to 1 second so we start blocking at the ioctl
	select {
	case <-c:
	case <-time.After(1 * time.Second):
	}

	return nil
}

func newTunFromFd(_ *config.C, _ *slog.Logger, _ int, _ []netip.Prefix) (*tun, error) {
	return nil, fmt.Errorf("newTunFromFd not supported in FreeBSD")
}

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
	}

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Do not pass a pre-opened fd on FreeBSD; use newTun() (config-driven) which opens /dev/tun itself
  2. Remove or gate the fd-based construction path behind a runtime.GOOS check
  3. If you must share an fd, open the device via newTun and extract its fd instead of the reverse

Example fix

// before
tunDev, err := newTunFromFd(cfg, logger, myFd, prefixes)
// after
if runtime.GOOS == "freebsd" {
    tunDev, err = newTun(cfg, logger, prefixes, false)
} else {
    tunDev, err = newTunFromFd(cfg, logger, myFd, prefixes)
}
Defensive patterns

Strategy: validation

Validate before calling

if runtime.GOOS == "freebsd" && usingFdConstruction {
    return errors.New("fd-based tun construction is unsupported on freebsd; use config-driven newTun")
}

Type guard

func fdTunSupported() bool { return runtime.GOOS != "freebsd" }

Try / catch

tunDev, err := newTunFromFd(cfg, log, fd, prefixes)
if err != nil && strings.Contains(err.Error(), "not supported in FreeBSD") {
    tunDev, err = newTun(cfg, log, prefixes, false)
}
if err != nil { return err }

Prevention

When it happens

Trigger: Calling the library's device constructor that takes an existing fd (newTunFromFd with a *config.C, logger, int fd, and prefix list) on a FreeBSD build. Any code path that requests fd-based device creation on darwin-freeBSD platforms triggers it unconditionally.

Common situations: Porting Linux code that opens its own /dev/tun fd and hands it to the library; using a config option like tun.fd or a 'dev' pre-opened descriptor; test harnesses that pre-create tun fds for fast setup.

Related errors


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