OpenVPN stops with an errno 2 complaint about /dev/net/tun while lsmod happily lists the tun module. Both facts are true at the same time. The module provides a driver, and the path the client opens is a device node that something else has to create. Treating those as one object is why the obvious repair of reloading the module changes nothing at all.

Two objects, two different owners

A kernel module and a device node are independent objects with independent lifecycles. Loading the module registers a driver, and on a system with devtmpfs the kernel asks for the matching node to be created. On a system without devtmpfs nothing creates anything, and the driver sits there with no path pointing at it. The module can be loaded, healthy and completely unreachable through the filesystem at the same moment.

That is why the error text and the module listing do not contradict each other. The client opens a path, the path does not exist, and the kernel returns ENOENT before the tun driver is ever consulted. Reloading the module cannot repair a missing path, and creating the path cannot repair a missing driver.

lsmod | grep -w tun
stat -c "%F %t:%T" /dev/net/tun
grep " /dev " /proc/mounts

devtmpfs versus an explicit mknod

Two mechanisms normally create that node, and knowing which one is supposed to be responsible tells you what to repair. devtmpfs has the kernel create device nodes in a mounted instance, and udev applies ownership and permissions on top of them. An explicit mknod is the manual path for images and namespaces where neither of those runs at boot.

Compare outcomes rather than intentions. If devtmpfs is mounted at /dev and the node is still absent, the driver is not the one you assume, or something removed the node after boot. If /dev is a plain directory inside an unpacked rootfs, no automatic mechanism exists and whoever builds the image has to create the node.

stat -c "%F major=%t minor=%T" /dev/net/tun
# expect: character special file major=a minor=c8

Where neither mechanism runs, the node has to be made by hand, and the order matters. Create the parent directory first, because mknod cannot invent one, then the character device, then a mode that leaves it writable by root alone. Reload the module before you create the node, and only remove an existing path after ls has shown that it is a directory rather than a device.

sudo modprobe tun
sudo mkdir -p /dev/net
sudo mknod /dev/net/tun c 10 200
sudo chmod 600 /dev/net/tun
ls -l /dev/net/tun

The type reported by stat is the fastest discriminator, and a directory at that path is the most common surprise in minimal images. A well-meant mkdir in a Dockerfile leaves exactly that, and no amount of module loading replaces a directory with a character device. Major 10 with minor 200 is the registered pair for tun, expressed in hex as a and c8.

Four traces, four causes

Four causes account for almost every case, and each one leaves a different trace in the logs. Start with the boot environment. Diskless images, embedded boards and initramfs stages often mount neither devtmpfs nor a udev-managed /dev, so no process ever writes the node. Then read the mount options on whatever hosts /dev, because nodev permits the path to exist while refusing to open it, and a bind mount taken from a hardened host carries the flag without announcing itself. A read-only mount fails earlier and louder: mknod returns EROFS there, while EPERM means the process lacks the capability rather than the filesystem protecting itself. Last, a container runtime with its own /dev tmpfs needs the node injected into its own namespace, and its device cgroup must permit the major and minor pair.

Check the policy layer before changing permissions again. SELinux and AppArmor can refuse an open that every file attribute allows, and that refusal appears in the audit log rather than in the client output. A permission change that never appears in a denial is time spent on the wrong layer entirely.

sudo journalctl -k -n 20 --no-pager
sudo ausearch -m AVC -ts recent | tail -5

Prove it with an interface, not a listing

A successful listing proves that a path exists and nothing more. Open a healthy tun node for reading and the call comes back with an error, because the device has no read semantics and is not a regular file. That error is the success signal here, and recognising it keeps you from chasing it as a fault.

head -c 1 /dev/net/tun
head: error reading '/dev/net/tun': File descriptor in bad state

The decisive test creates an interface, because that exercises the driver, the node and your privileges in a single step. If the add succeeds and the link appears, all three are working. When it fails, the errno separates the causes: ENOENT means there is no usable node, EPERM points at capability, mount flags or policy, and EBUSY means another process already owns the interface.

sudo ip tuntap add dev tun7 mode tun
ip -brief link show tun7
sudo ip tuntap del dev tun7 mode tun

Run that test in the context that actually fails, because a service unit, a container or a scheduled job can carry a different capability set than your login shell. A test that passes in the wrong context proves nothing about the failing one.

When the device is not the problem

If the interface test succeeds and the client still fails, the remaining problem is the profile, the service context or the route, and the device is fine. That is the moment to stop editing device nodes, because further changes only add distance from a working system.

Run the same five checks every time rather than inventing a new one: is the module loaded without a rival module, is the path a character device with the right major and minor, are the mount options clean, does the process context carry the capability, and is there a policy denial at the timestamp of the failure. A fault that survives all five is not a device problem, whatever the message mentions.

Аффингæй хæстæг статьялтæ