Wireguard for the Initiated

Anders Brownworth
5 min readApr 21, 2021

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.

Wireguard

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: 57593
peer: 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.

--

--

Anders Brownworth

Applied CBDC Research — formerly Federal Reserve, USDC @ Circle.com, Bandwidth.com. MIT / Podcaster / Runner / Helicopter Pilot