slackhq/nebula · error
/dev/net/tun doesn't exist, failed to mkdir -p /dev/net: %w
Error message
/dev/net/tun doesn't exist, failed to mkdir -p /dev/net: %w
What it means
openTunDev (overlay/tun_linux.go:94) opens /dev/net/tun; if it does not exist, it creates the /dev/net directory with MkdirAll. This error wraps a failure of that mkdir, so the TUN character device cannot be provisioned.
Source
Thrown at overlay/tun_linux.go:94
func newTunFromFd(c *config.C, l *slog.Logger, deviceFd int, vpnNetworks []netip.Prefix) (*tun, error) {
// We don't know what flags the caller opened this fd with and can't turn
// on IFF_VNET_HDR after TUNSETIFF, so skip offload on inherited fds.
return newTunGeneric(c, l, deviceFd, false, 0, vpnNetworks, "tun0")
}
// openTunDev opens /dev/net/tun, creating the device node first if it's
// missing (docker containers occasionally omit it).
func openTunDev() (int, error) {
fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0)
if err == nil {
return fd, nil
}
if !os.IsNotExist(err) {
return -1, err
}
if err = os.MkdirAll("/dev/net", 0755); err != nil {
return -1, fmt.Errorf("/dev/net/tun doesn't exist, failed to mkdir -p /dev/net: %w", err)
}
if err = unix.Mknod("/dev/net/tun", unix.S_IFCHR|0600, int(unix.Mkdev(10, 200))); err != nil {
return -1, fmt.Errorf("failed to create /dev/net/tun: %w", err)
}
fd, err = unix.Open("/dev/net/tun", os.O_RDWR, 0)
if err != nil {
return -1, fmt.Errorf("created /dev/net/tun, but still failed: %w", err)
}
return fd, nil
}
// tunSetIff runs TUNSETIFF with the given flags and returns the kernel-chosen device name on success.
func tunSetIff(fd int, name string, flags uint16) (string, error) {
var req ifReq
req.Flags = flags
copy(req.Name[:], name)
if err := ioctl(uintptr(fd), uintptr(unix.TUNSETIFF), uintptr(unsafe.Pointer(&req))); err != nil {
return "", errView on GitHub (pinned to dd8f660c0a)
Solutions
- Run the container with --privileged (or at least --device /dev/net/tun and a writable /dev).
- Ensure the process runs as root with CAP_MKNOD and CAP_NET_ADMIN.
- Pre-create /dev/net/tun on the host or in the image (mknod /dev/net/tun c 10 200) so the mkdir path is never taken.
- Check LSM policies (SELinux/AppArmor) that may block directory creation under /dev.
Example fix
// before $ docker run nebula-image // after $ docker run --device /dev/net/tun --cap-add NET_ADMIN nebula-image
Defensive patterns
Strategy: validation
Validate before calling
if _, err := os.Stat("/dev/net/tun"); os.IsNotExist(err) {
if st, serr := os.Stat("/dev"); serr != nil || st.Mode()&os.ModePerm == 0 || isReadOnlyFs("/dev") {
return errors.New("/dev/net/tun missing and /dev not writable; use --device /dev/net/tun or privileged mode")
}
}
if os.Geteuid() != 0 {
return errors.New("provisioning /dev/net/tun requires root")
} Try / catch
fd, err := openTunDev()
if err != nil && strings.Contains(err.Error(), "failed to mkdir -p /dev/net") {
// fall back to requesting the device from the container runtime / operator
} Prevention
- Run containers with --device /dev/net/tun or --privileged.
- Bake the device node into images where /dev is writable at build time.
- Ensure the host kernel has the tun module loaded so the device exists.
- Check SELinux/AppArmor policies before deploying to hardened hosts.
When it happens
Trigger: os.MkdirAll("/dev/net", 0755) fails after /dev/net/tun was found missing: read-only /dev (typical in containers), or EACCES/EPERM without root privileges.
Common situations: Running nebula in a Docker/Kubernetes container without --privileged and with a read-only /dev; minimal chroot/initramfs environments lacking /dev/net; SELinux/AppArmor policies denying writes under /dev.
Related errors
- failed to create /dev/net/tun: %w
- created /dev/net/tun, but still failed: %w
- failed to bring the tun device up: %s
- failed to enable offload on multiqueue tun fd: %w
- failed to get tun address list: %s
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/221e5e1740e6f2d0.
Report an issue: GitHub.