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
- Do not pass a pre-opened fd on FreeBSD; use newTun() (config-driven) which opens /dev/tun itself
- Remove or gate the fd-based construction path behind a runtime.GOOS check
- 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
- Branch device construction on runtime.GOOS before choosing newTunFromFd
- Use config-driven tun creation as the portable path
- Add a build-tag or startup assertion for fd-based tun usage
- Keep the fd-based path behind a Linux/darwin-only feature flag
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
- failed to set tun device as nonblocking: %w
- failed to create shutdown pipe: %w
- newTunFromFd not supported in Darwin
- 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/8d23e92775fa9de8.
Report an issue: GitHub.