VPNs and Network Tunneling
On this page
Wrapping One Network Inside Another#
A tunnel takes a packet from one network and carries it as the payload of a packet on another network. The outer network only sees ordinary traffic between two tunnel endpoints; the inner packet's addresses, and often its contents, are hidden inside. Tunnels are the basis of VPNs, site-to-site links between offices and clouds, SSH port forwarding, and the overlay networks that run Kubernetes and cloud VPCs.
What a VPN Actually Does#
A Virtual Private Network is an encrypted tunnel between your device and a VPN gateway. Your device routes traffic into the tunnel; the gateway decrypts it and forwards it, either into a private network (corporate VPN) or onto the internet (consumer VPN). Two consequences follow:
- Anyone on the path between you and the gateway, such as café Wi-Fi or your ISP, sees only encrypted packets to one IP address.
- The gateway sees everything a normal ISP would. A VPN moves trust from your ISP to the VPN operator; it does not remove it.
Tunnel Modes#
- Full tunnel: all traffic, including general internet browsing, goes through the VPN. Common for privacy and for companies that want to inspect all employee traffic.
- Split tunnel: only specific networks (say
10.0.0.0/8) go through the VPN, and everything else goes out directly. Faster and cheaper, but internet traffic is no longer protected or inspected. - Site-to-site: two gateways (an office firewall and a cloud VPN gateway, for example) keep a permanent tunnel up. Hosts on either side reach each other with plain routing and need no VPN software.
The difference is just routing. A full tunnel installs a default route into the tunnel interface, and a split tunnel installs only the specific prefixes. ip route get <address> tells you which one a given destination will use, which the routing lesson covers.
The Main Protocols#
| Protocol | Transport | Encryption | Where it fits |
|---|---|---|---|
| WireGuard | UDP (commonly port 51820) | Always on, fixed modern suite | New remote-access and site-to-site deployments, mesh VPNs |
| IPsec (IKEv2 + ESP) | IKE on UDP 500/4500, ESP as IP protocol 50 | Negotiated suites | Enterprise and cloud site-to-site, firewall interoperability |
| OpenVPN | UDP or TCP (default 1194) | TLS-based | Mature remote access, restrictive firewalls |
| GRE | IP protocol 47 | None | Carrying routing protocols and multicast, usually inside IPsec |
| SSH tunnels | TCP 22 | SSH | Ad-hoc access to a single service |
IPsec has two modes. Transport mode encrypts only the payload and keeps the original IP header, and is used host-to-host. Tunnel mode encrypts the entire original packet and wraps it in a new IP header between the two gateways, and is what site-to-site VPNs use. IKEv2 negotiates keys and security associations; ESP carries the encrypted data. When a NAT is in the path, ESP is wrapped in UDP port 4500 (NAT traversal) because NAT devices cannot track ESP, which has no ports.
WireGuard takes the opposite approach to IPsec's negotiation: one fixed set of primitives (Curve25519, ChaCha20-Poly1305, BLAKE2s, built on the Noise protocol framework), no cipher negotiation, and a small codebase that lives in the Linux kernel. There is no "connection" as such. Peers are identified purely by public keys.
A Working WireGuard Setup#
Generate a key pair on each side:
wg genkey | tee server.key | wg pubkey > server.pub
wg genkey | tee laptop.key | wg pubkey > laptop.pub
Server /etc/wireguard/wg0.conf:
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <contents of server.key>
[Peer]
# laptop
PublicKey = <contents of laptop.pub>
AllowedIPs = 10.8.0.2/32
Laptop /etc/wireguard/wg0.conf:
[Interface]
Address = 10.8.0.2/24
PrivateKey = <contents of laptop.key>
DNS = 10.8.0.1
[Peer]
PublicKey = <contents of server.pub>
Endpoint = vpn.example.com:51820
# split tunnel: only the VPN subnet and the office LAN
AllowedIPs = 10.8.0.0/24, 10.20.0.0/16
PersistentKeepalive = 25
Bring it up with sudo wg-quick up wg0 and check it with sudo wg show. The fields to look at are latest handshake (should be under about two minutes on an active tunnel) and transfer (bytes received and sent should both grow).
The most important line is AllowedIPs, which WireGuard calls cryptokey routing. It works in both directions. Outbound, it acts as a routing table: packets for those prefixes are encrypted to this peer. Inbound, it acts as an access list: a decrypted packet whose source address is not in the peer's AllowedIPs is dropped. Setting it to 0.0.0.0/0, ::/0 on the laptop turns the split tunnel into a full tunnel. On the server, forwarding into the office LAN also needs net.ipv4.ip_forward=1 and either a return route on the LAN or NAT.
PersistentKeepalive = 25 sends a small packet every 25 seconds. Without it, a laptop behind NAT goes quiet, the NAT mapping expires, and the server can no longer reach it until the laptop sends something first.
SSH Tunnels for One-Off Access#
For reaching a single service, SSH is often enough and needs no VPN software:
# Local forward: localhost:5432 on your laptop -> db.internal:5432 via the bastion
ssh -N -L 5432:db.internal:5432 user@bastion.example.com
# Dynamic forward: a SOCKS5 proxy on localhost:1080 that exits at the bastion
ssh -N -D 1080 user@bastion.example.com
-N means "no remote command, just forward". Point a database client at localhost:5432, or a browser at the SOCKS proxy on localhost:1080.
Encapsulation and the MTU Problem#
Every tunnel adds headers, and the underlying link still has a maximum packet size, usually 1,500 bytes. The inner packet must shrink by the overhead:
# Bytes each encapsulation adds in front of (or around) the inner IP packet
OVERHEAD = {
"outer IPv4": 20,
"outer IPv6": 40,
"UDP": 8,
"GRE": 4,
"WireGuard": 32, # 16-byte header + 16-byte Poly1305 tag
"VXLAN + inner Ethernet": 8 + 14,
}
def inner_mtu(link_mtu, layers):
return link_mtu - sum(OVERHEAD[name] for name in layers)
def tcp_mss(mtu, ipv6=False):
return mtu - (40 if ipv6 else 20) - 20 # minus inner IP and TCP headers
stacks = {
"WireGuard over IPv4": ["outer IPv4", "UDP", "WireGuard"],
"WireGuard over IPv6": ["outer IPv6", "UDP", "WireGuard"],
"GRE over IPv4": ["outer IPv4", "GRE"],
"VXLAN over IPv4": ["outer IPv4", "UDP", "VXLAN + inner Ethernet"],
}
for name, layers in stacks.items():
mtu = inner_mtu(1500, layers)
print(f"{name:<22} tunnel MTU {mtu} TCP MSS {tcp_mss(mtu)}")
WireGuard over IPv4 tunnel MTU 1440 TCP MSS 1400
WireGuard over IPv6 tunnel MTU 1420 TCP MSS 1380
GRE over IPv4 tunnel MTU 1476 TCP MSS 1436
VXLAN over IPv4 tunnel MTU 1450 TCP MSS 1410
This is why wg-quick defaults to an MTU of 1420: it covers the worst case of an IPv6 outer header. Stack another layer (PPPoE on the ISP link, or a tunnel inside a tunnel) and the budget shrinks further.
The symptom is distinctive: small things work and big things hang. Ping works, SSH logs in, but scp stalls, web pages load partially, and TLS handshakes freeze right after the ClientHello, because the server's certificate chain is the first large packet. It happens when oversized packets are dropped and the ICMP "fragmentation needed" message that should tell the sender to shrink is blocked by a firewall (a PMTUD black hole).
Find the real path MTU by pinging with the don't-fragment bit set and a payload that is 28 bytes (IPv4 + ICMP headers) smaller than the MTU you are testing:
ping -M do -s 1472 10.20.0.10 # Linux: tests a 1500-byte packet
ping -M do -s 1392 10.20.0.10 # tests 1420
ping -f -l 1392 10.20.0.10 # Windows equivalent
Lower the size until replies come back. Fix it by setting the tunnel interface MTU to what works and clamping TCP MSS on the gateway, so TCP endpoints never try to send segments that would not fit:
sudo iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu
Other Ways Tunnels Break#
- Overlapping address space. If your home LAN and the office both use
192.168.1.0/24, traffic for office hosts goes out the home interface, because the connected route wins. Choose unusual private ranges for corporate networks, or use NAT on the VPN. - DNS leaks. A split-tunnel client that keeps using the local resolver cannot resolve internal names, and on a full tunnel it reveals which sites you visit to the local network. Push a DNS server through the tunnel, as with the
DNS =line above. - TCP over TCP. Running a TCP-based VPN (OpenVPN in TCP mode) over a lossy link stacks two retransmission mechanisms, which can collapse throughput. Prefer UDP transports.
- Routing loop on full tunnels. If the route to the VPN server itself points into the tunnel, the tunnel carries itself and dies.
wg-quickavoids this with policy routing; hand-built setups need a specific route to the endpoint via the real gateway. - Firewalls blocking the transport. UDP 51820 or IPsec may be blocked on hotel and corporate guest networks. TLS-based VPNs on TCP 443 get through where others fail, at some cost in performance.
Overlays: Tunnels Without the Privacy Goal#
Cloud providers and Kubernetes use the same encapsulation idea for a different purpose: VXLAN and Geneve tunnels between hypervisors make thousands of hosts look like private, isolated networks for each tenant. That is covered in virtual networks and overlays.
When a VPN Is the Right Tool#
VPNs are the right tool for joining networks and for remote access to private services. They are a weak access model on their own: once connected, a user is often "inside" and can reach far more than they need. Many organisations now pair or replace them with per-application access that authenticates every request, the model described in zero trust. For personal privacy, a VPN hides your traffic from the local network and ISP, but it does nothing about cookies, logins, or browser fingerprinting on the sites you visit.
Check your understanding
4 quick questions on this lesson. Get them all right and the lesson is marked complete.