slackhq/nebula · error
failed to create /dev/net/tun: %w
Error message
failed to create /dev/net/tun: %w
What it means
openTunDev (overlay/tun_linux.go:97) falls back to creating the /dev/net/tun character device node (mknod, major 10 minor 200) when it is missing. This error wraps a mknod failure, meaning the TUN device node could not be provisioned.
Source
Thrown at overlay/tun_linux.go:97
// 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 "", err
}
return strings.Trim(string(req.Name[:]), "\x00"), nil
}View on GitHub (pinned to dd8f660c0a)
Solutions
- Mount the host device into the container: --device /dev/net/tun (or a Kubernetes device plugin / hostPath volume).
- Run with the capabilities needed to create device nodes (CAP_MKNOD) plus NET_ADMIN for TUNSETIFF.
- Pre-create the node in the image build (RUN mknod /dev/net/tun c 10 200) where /dev is writable at build time.
- If EEXIST due to a race, retry the open; the node already exists.
Example fix
// before $ docker run --cap-add NET_ADMIN nebula-image # still no CAP_MKNOD, /dev/net/tun missing // after $ docker run --cap-add NET_ADMIN --device /dev/net/tun nebula-image
Defensive patterns
Strategy: validation
Validate before calling
if os.Geteuid() != 0 {
return errors.New("creating /dev/net/tun requires root + CAP_MKNOD")
}
if _, err := os.Stat("/dev/net/tun"); err == nil {
// node exists; no mknod needed
} Try / catch
fd, err := openTunDev()
if err != nil && strings.Contains(err.Error(), "failed to create /dev/net/tun") {
if errors.Is(errors.Unwrap(err), unix.EEXIST) {
// raced with another creator; safe to retry open
}
} Prevention
- Prefer mounting the host device (--device /dev/net/tun) over mknod at runtime.
- Grant CAP_MKNOD only if runtime node creation is truly required.
- Avoid read-only /dev in images that rely on runtime mknod.
- Handle EEXIST races by retrying the open instead of failing.
When it happens
Trigger: unix.Mknod("/dev/net/tun", S_IFCHR|0600, Mkdev(10,200)) fails: EPERM without CAP_MKNOD (the norm in unprivileged containers), read-only /dev, or EEXIST if a race created it concurrently.
Common situations: Unprivileged containers that hide or lack /dev/net/tun and block mknod; rootless podman/Kubernetes without device plugins; hardened sandbox images with read-only /dev.
Related errors
- /dev/net/tun doesn't exist, failed to mkdir -p /dev/net: %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/e6c8a0a6c35c1f10.
Report an issue: GitHub.