Wireguard for the Initiated
Wireguard is insanely great. Modern crypto, lightweight, in-kernel, UDP for everything and seamless roaming if your IP changes. What’s not to love?
But if you are just starting out with it and have a strong TCP/IP and Linux background, you might appreciate a concise explanation. This attempts to be that.
All it Does
Wireguard creates a virtual network interface, the first of which is called wg0
, which encrypts and decrypts traffic. The kernel retains a list of peers and associated public keys. If the kernel gets a packet destined to one of the peers, it gets encrypted and sent via UDP to the last known IP / port of that peer. The reverse happens if encrypted traffic arrives from a peer. Additionally, if a properly encrypted packet arrives from an unexpected IP, that peer is updated and all response packets will now flow back to that new IP. That’s how roaming is supported — just like mosh
. That’s all it does. Everything else — complicated IP subnetting, routing or any fancy port mappings— is done with netfilter, iproute2
or other standard kernel strategies. Wireguard’s elegance is in doing only one thing — encrypting and decrypting traffic — really well.
Install
Get the package however you prefer. In Ubuntu you could:
apt install wireguard
Now you presumably have the kernel module and the related userland utilities wg
and the helper application wg-quick
.
Keys
Wireguard uses Curve25519 public / secret key pairs which can be generated with wg
thusly:
wg genkey | tee secretkey | wg pubkey | tee publickey
We’ll do this on two machines, “A” and “B”. For purposes of this example, here’s the contents of those key files. (obviously don’t use these)
# Machine A (1.1.1.1)
Private Key: EF7nGyEOl1OlZwoQgYLN41SQuDJTMpJeht9CMarHC1k=
Public Key: atv4BKui/BSG+Wz+3xZDvyqZi5fUsvZXqAyqo6JeaC8=# Machine B (2.2.2.2)
Private Key: 8DIa2XfsMX7YqS8hH6DL3i0OXn3TrWNKKw8KCOQsPHs=
Public Key: AS5fnTaBkVjsu6MBj02tzrldSh0d9bEYvGJ3TjiHxj0=
Let’s bring up a wireguard interface on machine “A” named wg0
with 10.0.0.1/24
on it and pass it our secret key:
# Machine A (1.1.1.1)
ip link add wg0 type wireguard
ip addr add 10.0.0.1/24 dev wg0
wg set wg0 private-key ./secretkey
ip link set wg0 up
We’ll do the same using a different IP on machine “B”.
# Machine B (2.2.2.2)
ip link add wg0 type wireguard
ip addr add 10.0.0.2/24 dev wg0
wg set wg0 private-key ./secretkey
ip link set wg0 up
We can see what wireguard interface settings look like using wg
with no arguments:
# Machine B (2.2.2.2)
$ wg
interface: wg0
public key: AS5fnTaBkVjsu6MBj02tzrldSh0d9bEYvGJ3TjiHxj0=
private key: (hidden)
listening port: 50764
Peers
Now let’s tell machine “A” about the public key and initial IP / port combination to use to get to machine “B”:
# Machine A (1.1.1.1) <VPN 10.0.0.1>
wg set wg0 peer AS5fnTaBkVjsu6MBj02tzrldSh0d9bEYvGJ3TjiHxj0= allowed-ips 10.0.0.2/32 endpoint 2.2.2.2:50764
And machine “B” about the same for “A”:
# Machine B (2.2.2.2) <VPN 10.0.0.2>
wg set wg0 peer atv4BKui/BSG+Wz+3xZDvyqZi5fUsvZXqAyqo6JeaC8= allowed-ips 10.0.0.1/32 endpoint 1.1.1.1:57593
Peers are known by their public key. The associated IP / port combination can change as valid packets start to arrive from alternate addresses. Roaming clients are handled this way so you can just slap your laptop shut on one network and open it up on another without re-negotiating your VPN session.
The “allowed-ips” lets the interface know what network it can get to by encrypting packets with the given key and sending it to the given endpoint. Right now it is just the single machine 10.0.0.1/32
but you could send all 10.0.0.0/24
traffic or simply 0.0.0.0/0
. You can think about it like the route entry for this key / IP / port combination.
Now try pinging 10.0.0.2
from 10.0.0.1
and you should get responses:
# Machine A (1.1.1.1) <VPN 10.0.0.1>
$ ping 10.0.0.2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.489 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.519 ms
64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=1.39 ms
^C
--- 10.0.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2029ms
rtt min/avg/max/mdev = 0.489/0.799/1.390/0.417 ms
Check on the peers your machine knows about with the wg
command — you’ll see peers listed there now:
# Machine A (1.1.1.1) <VPN 10.0.0.1>
$ wg
interface: wg0
public key: atv4BKui/BSG+Wz+3xZDvyqZi5fUsvZXqAyqo6JeaC8=
private key: (hidden)
listening port: 57593peer: AS5fnTaBkVjsu6MBj02tzrldSh0d9bEYvGJ3TjiHxj0=
endpoint: 2.2.2.2:50764
allowed ips: 10.0.0.1/32
latest handshake: 2 minutes, 15 seconds ago
transfer: 1.09 KiB received, 11.18 KiB sent
That’s it (Oh, one more thing!)
There is a helper app called wg-quick
which reads config files from /etc/wireguard/
which makes setting things up a little easier. (and more painlessly survive reboots) Here’s a minimal example of a wg0.conf
:
# Machine A (1.1.1.1) <VPN 10.0.0.1>
# /etc/wireguard/wg0.conf
[Interface]
PrivateKey = EF7nGyEOl1OlZwoQgYLN41SQuDJTMpJeht9CMarHC1k=
Address = 10.0.0.1/24
ListenPort = 57593
SaveConfig = false[Peer]
PublicKey = AS5fnTaBkVjsu6MBj02tzrldSh0d9bEYvGJ3TjiHxj0=
AllowedIPs = 10.0.0.2/32
Now, fire up wg-quick
and see if you get the same setup:
wg-quick up wg0
And wg-quick down wg0
will do the opposite. On Ubuntu, you might make that survive reboots with:
systemctl enable wg-quick@wg0
The above wg0.conf
is something you might use on the “hub” of a “hub and spoke” setup which does not stipulate the IP addresses of the spokes. The corresponding spoke setup would want to codify the hub’s IP (or DNS name) with the EndPoint
directive thusly:
# Machine B (2.2.2.2) <VPN 10.0.0.2>
# /etc/wireguard/wg0.conf
[Interface]
PrivateKey = 8DIa2XfsMX7YqS8hH6DL3i0OXn3TrWNKKw8KCOQsPHs=
Address = 10.0.0.1/24
ListenPort = 57593
SaveConfig = false[Peer]
PublicKey = atv4BKui/BSG+Wz+3xZDvyqZi5fUsvZXqAyqo6JeaC8=
AllowedIPs = 10.0.0.1/32
EndPoint = 1.1.1.1:57593
DNS
If you end up routing most or all traffic through a Wireguard tunnel, you might want to automatically drop the IP of a DNS cache in there and maybe set a search domain. You can do that fairly concisely with:
# Machine A (1.1.1.1) <VPN 10.0.0.1>
# /etc/wireguard/wg0.conf
[Interface]
PrivateKey = EF7nGyEOl1OlZwoQgYLN41SQuDJTMpJeht9CMarHC1k=
Address = 10.0.0.1/24
ListenPort = 57593
SaveConfig = false
DNS = 8.8.8.8, example.com[Peer]
PublicKey = AS5fnTaBkVjsu6MBj02tzrldSh0d9bEYvGJ3TjiHxj0=
AllowedIPs = 10.0.0.2/32
You’ll notice 8.8.8.8
which is the DNS cache and example.com
is how you specify your default search domain. Numbers are caches and names are search domains, all separated by commas.
Hub and Spoke
It is common to designate one peer as a “server” and have other clients peer through that to get to other peers. You do this using traditional iproute2
strategies (echo 1 > /proc/sys/net/ipv4/ip_forward
and ip route
entries) so there isn’t anything special here. I’ve been very happy with the minimal amount of CPU encrypting / decrypting to accomplish this costs. Even a $200 embedded appliance holds up fairly well at the center. You can see what I use for this in A Home Network which is a bit more broad article.
Conclusion
Wireguard is one of those “insanely great” things. It just does what it says on the can and nothing else. You can coax it to sending keepalive packets every once in a while (just add persistent-keepalive 25
to the wg
command or PersistentKeepalive = 25
to the wg0.conf
file to get a packet every 25 seconds) but beyond that it is totally silent unless traffic is requested. It handles roaming clients the way you want — just picks up where you left off if the packets are encrypted properly with the correct sequence number. And it doesn’t try to re-solve problems — all routing or interesting network configurations are handled the standard iproute2
way. It is the breath of fresh air you need if you are still stuck in IPSec / PPP / CHAP authentication hell.