Step 1 of 5 · Reading · ~3 min
Read
VPN Fundamentals
What VPNs Solve
After this lesson your program can say exactly how many bytes one packet costs once it travels through a WireGuard-style tunnel. Every later stage of this course builds one more piece of that tunnel, and this number decides your MTU, your fragmentation and your bandwidth bill.
The problem
Two machines on different networks want to behave as if they shared one private network, say 10.0.0.0/24, while the path between them (the internet) is untrusted. A VPN does this with a virtual network interface at each end:
- An application sends a normal IP packet to 10.0.0.1. The kernel routes it to the virtual interface.
- Your program reads that packet from the interface. This is the inner packet.
- It encrypts and authenticates the inner packet and sends the result as the payload of an ordinary UDP datagram to the other machine's public address. That datagram is the outer packet.
- The other end checks and decrypts the payload, and writes the inner packet into its own virtual interface. The kernel there delivers it as if it had arrived on a cable.
Remote access, site-to-site links, hiding traffic from a coffee-shop network and linking a data centre to a cloud network are all this same loop. Tailscale is WireGuard plus a service that helps peers find each other. OpenVPN and IPsec solve the same problem with far more negotiation and options.
What one packet costs
A WireGuard transport message (WireGuard whitepaper, section 5.4.6) is the encrypted inner packet with a 16 byte header in front and a 16 byte authentication tag behind:
| header 16 | encrypted padded inner packet | tag 16 |
The outer packet adds a UDP header (8 bytes) and an IP header (20 bytes for IPv4, 40 for IPv6). The encrypted part is the inner packet padded with zero bytes up to a multiple of 16, but never padded beyond the tunnel MTU, which is the largest inner packet the interface accepts. An empty inner packet is a keepalive and is not padded at all.
Worked trace for an 84 byte inner packet (a ping with 56 bytes of data), tunnel MTU 1420, IPv4 outer:
| Step | Bytes | Why |
|---|---|---|
| inner packet | 84 | read from the virtual interface |
| padded | 96 | next multiple of 16 is 96, below the MTU |
| plus header | 112 | 16 byte header |
| plus tag | 128 | 16 byte tag: this is the transport message |
| plus UDP | 136 | 8 byte UDP header |
| plus IPv4 | 156 | 20 byte IPv4 header: the datagram on the wire |
A keepalive is 0 + 16 + 16 = 32 bytes of message, 60 bytes as an IPv4 datagram.
Why 1420 is the usual tunnel MTU: the worst case outer packet is IPv6, so 1420 + 32 + 8 + 40 = 1500, exactly one Ethernet frame. A larger tunnel MTU would force the outer packet to be fragmented.
Common mistakes
- A flat constant. Adding a fixed number of bytes to every packet is wrong for most sizes. An inner packet of 100 bytes becomes a 144 byte message, not 100 plus a constant that was correct for some other size.
- Padding an exact multiple. 16 stays 16 and 32 stays 32. Only a size that is not a multiple moves up.
- Padding the keepalive. An empty packet stays empty, so its message is 32 bytes and not 48.
- Ignoring the MTU cap. With MTU 1420 an inner packet of 1420 bytes is not padded to 1424. The cap wins over the multiple of 16.
- IPv4 header for an IPv6 outer packet. The outer family decides 20 or 40 bytes, not the inner packet.
Your exercise
Your program reads one MTU line and then one line per inner packet, and prints the message size and the datagram size for each. The starter already parses the input. The most likely mistake is rounding up a size that is already a multiple of 16.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…