Why WireGuard
WireGuard appeared in 2016 and landed in the mainline kernel with 5.6 (2020). The goal was a VPN without the hyper-complexity of [[ipsec-ike|IPsec]] and without the overhead of [[openvpn|OpenVPN]]. What came out:
- One packet type (UDP), one cipher suite (ChaCha20-Poly1305 + Curve25519), no negotiation. If future primitives become obsolete, WireGuard 2 will ship.
- Stateless from the admin's perspective: a key pair on each side, the peer's public key is the only identifier.
- AllowedIPs acts as both an ACL (what a peer may send) and a routing table (where to forward packets destined for that peer).
- Roaming: a peer can change its IP or port without reconnecting, because authentication is by key, not by 5-tuple.
Basic Config
Server side /etc/wireguard/wg0.conf:
[Interface]
PrivateKey = <server-private-key>
Address = 10.10.0.1/24
ListenPort = 51820
PostUp = iptables -t nat -A POSTROUTING -s 10.10.0.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -s 10.10.0.0/24 -o eth0 -j MASQUERADE
[Peer]
PublicKey = <client-public-key>
AllowedIPs = 10.10.0.2/32
Client wg0.conf:
[Interface]
PrivateKey = <client-private-key>
Address = 10.10.0.2/24
DNS = 1.1.1.1
[Peer]
PublicKey = <server-public-key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0 # full-tunnel; for split-tunnel use specific subnets
PersistentKeepalive = 25 # behind NAT, keeps the conntrack entry alive
Bring it up:
wg-quick up wg0
systemctl enable wg-quick@wg0 # start on boot
Key Generation
umask 077
wg genkey | tee privatekey | wg pubkey > publickey
The private key never leaves the machine. The config stores it in plain text, so
keep /etc/wireguard/ at mode 600. Always generate the private key on the peer
itself, never on the server.
PSK (optional, for quantum resistance):
wg genpsk > preshared.key
Add to [Peer]: PresharedKey = <psk>.
AllowedIPs: the Most Important Field
AllowedIPs = 10.10.0.2/32 # single host
AllowedIPs = 10.10.0.0/24, 192.168.5.0/24 # multiple subnets
AllowedIPs = 0.0.0.0/0, ::/0 # everything - full-tunnel
- Incoming packet from a peer: the source address must fall inside AllowedIPs, otherwise the packet is dropped. This is the trust-boundary check.
- Outgoing packet: if the destination matches a peer's AllowedIPs, wg0 forwards it to that peer. Equivalent to a route lookup.
The same subnet 10.10.0.0/24 cannot appear in the AllowedIPs of two peers at once;
that creates a routing conflict.
Topologies: Hub-and-Spoke vs Full Mesh
At L3, WireGuard has no "server" or "client" concept. Both sides are equal peers.
Topology is determined by who has whom in [Peer]:
- Hub: one peer holds all peer blocks; each client gets a narrow AllowedIPs. A simple model, the classic VPN server.
- Mesh: every node knows every other node. Harder to provision, but traffic flows directly without a hub hop.
- Tailscale/Headscale automate mesh provisioning on top of WireGuard.
NAT and Keepalive
Behind NAT, a conntrack entry dies after 30-180 seconds with no traffic. WireGuard
handles this: PersistentKeepalive = 25 sends an empty keepalive every 25 seconds
to keep the NAT hole open.
A hub behind a static IP does not need PersistentKeepalive. A client behind CGNAT
(mobile carrier) does.
Diagnostics
wg show # all interfaces and peers
wg show wg0 # details for one interface
wg show wg0 dump # machine-readable output
wg show wg0 latest-handshakes # when the last handshake occurred
wg show wg0 transfer # rx/tx bytes per peer
The key health indicator is latest handshake: it should update roughly every 2 minutes. If it shows "30 minutes ago", the peer is unreachable.
WireGuard vs OpenVPN vs IPsec
| Feature | WireGuard | openvpn | ipsec-ike |
|---|---|---|---|
| Code base | ~4 KLOC | ~600 KLOC | millions (strongSwan) |
| Where it runs | in kernel | userspace | kernel + IKE userspace |
| Config | flat ini | nested + certs | swanctl.conf + certificates |
| Cipher | ChaCha20-Poly1305 (fixed) | TLS suite | IKEv2-negotiated |
| UDP | yes | yes or TCP | yes (ESP) |
| Roaming | native | no | mobile-extension |
| Performance | noticeably faster | slower (userspace) | comparable to WG |
| Production maturity | 2020+ | 2002+ | 1995+ |
| Certificates / PKI | no | yes | yes |
When to skip WireGuard:
- You need PKI (corporate CRL/OCSP): use IPsec or OpenVPN.
- You need TCP fallback (port 443 as camouflage): use OpenVPN.
- You need per-user authentication with RADIUS/LDAP: use OpenVPN.
- You need interop with Cisco/Juniper: use IPsec.
Troubleshooting
No such device wg0: the module is not loaded. On kernel 5.6+ it is built in; on older kernels installwireguard-dkmsand runmodprobe wireguard.- Handshake never happens: UDP 51820 is blocked somewhere. Check with
nc -u -z -v server 51820, review the host firewall and NAT. - Handshake completes but traffic does not flow:
ip routehas no route to the VPN subnet; or ip-forwarding is disabled on the server; or MASQUERADE is missing. - Oversized packets are dropped (MTU): WireGuard adds 60 bytes of overhead (UDP
- headers). Set
MTU = 1420explicitly when the path MTU is 1500.
- headers). Set
Address already in useon wg-quick up: the IP is already assigned to another interface (an old wg0 is still up). Runwg-quick down wg0first.- AllowedIPs overlap across two peers: the last entry wins and the others become unreachable. Do not overlap subnets.
PersistentKeepalivetoo frequent: wastes battery on mobile clients. 25 seconds is a reasonable compromise with typical NAT timeouts (60 s).