How does a program get packets it was never sent?
Every program you have written in this track received bytes because someone addressed
them to its socket. A VPN daemon breaks that rule. Your browser opens a connection to
100.64.0.2. It has no idea a VPN exists. The kernel builds the IP packet,
consults the routing table, and then the packet appears in the daemon's memory.
Not in a socket's receive buffer. In a buffer the daemon passed to read.
You know two things that make this possible. From the file-descriptors lesson: anything the kernel can hand you bytes from is an fd. From the routing lesson: a route can point at a virtual interface. A TUN device is where those two facts meet.
An interface with a file descriptor on the other end
To the kernel, tun0 looks like eth0. It has an IP address.
It shows up in ip addr. Routes can name it. When the routing table
picks it, the kernel does exactly what it does for a real interface: it finishes the
IP packet and hands it to the device driver.
The difference is what the driver does next. The eth0 driver writes
the frame into the network card's transmit ring. The tun driver
appends the packet to a queue attached to an open file descriptor. Your next
read on that fd returns the packet. The whole packet: IP header first,
then whatever came after it. Byte 0 of your buffer is byte 0 of the IP header you
learned to parse in lesson 01.
It works in reverse too. write one IP packet to the fd and the kernel
treats it as a packet that just arrived on tun0. It parses the
header, looks at the destination, and delivers it to whichever socket is bound there.
The receiving program sees a packet from the network. It came from your buffer.
tun0 come out of your read. Packets you
write go into the kernel as if a wire delivered them. The kernel does the
routing and the socket delivery. You decide what happens in between.
Creating one
You open /dev/net/tun like any file. That fd is not yet an interface.
One ioctl turns it into one: TUNSETIFF takes an
ifreq struct with the name you want and two flags. IFF_TUN
means IP packets, not Ethernet frames. IFF_NO_PI tells the driver not
to prepend a 4-byte "packet info" header, so byte 0 really is the IP version field.
After the ioctl returns, tun0 exists but has no address
and is down. Configuring it is the same as for any interface. The route is the
important line: it is what makes the kernel send 100.64.0.0/10 traffic
into your fd instead of out the real NIC. On Linux, adding the address with a prefix
length installs that route for you; here it is spelled out so you can see it.
CAP_NET_ADMIN.
Run as root during development, or grant the capability once with
sudo setcap cap_net_admin+ep ./vpnd. Without it,
ioctl(TUNSETIFF) fails with EPERM and your program has an
ordinary fd to /dev/net/tun that will never produce a packet.
The interface also disappears the moment the fd is closed, including when your
process crashes. That is a feature: no leaked interfaces.
What read() hands you
One read returns one complete IP packet. Never half of one, never two.
The driver preserves packet boundaries the way UDP does, so a 2000-byte buffer and a
loop is all you need. Byte 0 holds version and header length. Bytes 2–3 are the total
length, big-endian. Byte 9 is the protocol. Bytes 12–15 and 16–19 are the source and
destination addresses. This is the header from lesson 01, and it is sitting in your
char array.
Run it, then in another terminal ping 100.64.0.2. The kernel routes
the echo request to tun0. Your program prints
100.64.0.1 -> 100.64.0.2 proto 1 len 84 once a second. Nobody answers,
because nobody is on the other end yet. You are the other end.
read as exactly one packet.
Never assume a fixed size, never try to read "the rest" of a packet with a second
call, and never trust total from the header without checking it against
n. A buffer of 2048 bytes covers any packet on a standard 1500-byte MTU
link with room to spare.
The path of one packet
Step through the visualization. One datagram goes from an app on machine A to a server on machine B. Watch which process holds the bytes at each step, which routing-table row wins, and how the inner packet becomes the payload of an outer one.
Two fds, one loop
A daemon that only reads packets is a packet sink. To build a tunnel it needs a second
fd: a UDP socket to the peer's real address. Then the job is symmetric. A packet from
tun0 goes out through the socket. A datagram from the socket goes into
tun0. Two fds, both can block, so you watch them with poll
exactly as in lesson 08.
Run this on two machines with the right peer addresses and ping 100.64.0.2
gets replies. Forty lines. This is, with one omission, the data plane of
WireGuard-go and Tailscale: read from the TUN, send over UDP, receive over UDP,
write to the TUN. The omission is what happens to buf between the
read and the sendto. Right now it is nothing. Every inner
packet crosses the internet in the clear, and anyone who can reach your UDP port can
write arbitrary packets into your kernel. That is the next lesson.
tun0 lower than the real link.
The outer IP and UDP headers add 28 bytes, and encryption will add more. A 1500-byte
inner packet becomes a 1528-byte datagram, which the real link cannot carry in one
frame. It gets fragmented or dropped. ip link set tun0 mtu 1420 makes
the kernel build inner packets that still fit after wrapping. WireGuard's default is
exactly that number.
IFF_TAP instead of IFF_TUN, each read
starts with the 14-byte Ethernet header from lesson 01, and your program has to deal
with MAC addresses and ARP. Useful for bridging virtual machines onto a LAN.
For a VPN that routes at the IP level, TUN is simpler and 14 bytes per packet
cheaper.
A TUN device is a network interface whose wire is a file descriptor: the kernel routes IP packets into your read, and whatever you write arrives as if from the network.