wb5/posts/wireguard.md

18 KiB

wireguard vpn server setup guide for linux

2024-06-17

Recently, I decided to set up a WireGuard VPN server for personal use. I found that existing guides about this subject are all lacking in some way. The server setup tutorials I found all go through the commands and configurations needed very throughly, but don't explain why things work. Meanwhile, WireGuard's Conceptual Overview focuses on the protocol itself and how it is different from others.

So, this post is a summary of my mental model of how WireGuard works, plus a tutorial for setting up the server. I assume that you have some knowledge of networking, but aren't familiar with WireGuard. I also assume Linux knowledge. This guide should work generally, but I tested it on Arch Linux.

CIDR notation:

In this post, if you see an /24 at the end of an IP address, that's CIDR notation. 192.168.0.0/24 could be written as "any address that fits the pattern 192.168.0.*". Because there are 32 bits in an IPv4 address, the /24 at the end means that the first 24 bits (the first three bytes 192.168.0) are fixed, and the remaining 8 bits can be anything. Also, a /32 pattern matches only one address.

what is a vpn?

A VPN (virtual private network) is often advertised to the average user as an encrypted tunnel for their Internet connection. Mostly, this is useful for changing your IP address.

However, this is not the full picture of what a VPN is. In corporate networks, there are many important services that can't be exposed publicly, which is why they're only available on an internal, private network. Some off-site users, especially remote workers, need to access the internal network, but from the outside Internet. This is where a VPN can be useful: users can virtually be on the private network, as if they were on site. Using a VPN is more secure than publicly exposing services, as authentication allows fine-grained control over who can access the network.

For someone like me, VPNs are useful because I self-host private services. Normally, I would need to expose them to the public Internet to access them. Instead, I can access them solely through the local network or the VPN, preventing strangers from even seeing the login pages of my services. This reduces the attack surface and makes things more secure.

wireguard setup

WireGuard is a relatively recent protocol for implementing VPNs. It's shiny and new, and also has a slimmer codebase than older, more established protocols.

Let's go through the process of setting up a VPN to access an internal network. The setup for tunneling all of a device's Internet traffic is similar, and I will explain it too later. We will have a Client device, and a Server device. The Server is in the internal network (let's say in the 192.168.0.0/24 subnet), and the Client is outside of it. There is also a publicly accessible domain vpn.example.com that resolves to the Server.

To recap, here is a diagram of what we're trying to do:

 
   ╭────────────╮
   │ VPN Client │
   ╰─────┬──────╯
         │                  ╭────────────────────────────────────────────────╮
         │                  │ the private network (192.168.0.0/24)           │
         │                  │      ╭───────────────────╮                     │
 ╭───────┴─────────╮        │      │ VPN Server        │                     │
 │ public internet ├────────┼──────┤ (vpn.example.com) ├────── ⋯ other hosts │
 ╰─────────────────╯        │      ╰─┬───────────────┬─╯                     │
                            │ ╭──────┴──────╮   ╭────┴────────╮              │
                            │ │ machine 1   │   │ machine 2   │              │
                            │ │ 192.168.0.4 │   │ 192.168.0.7 │              │
                            │ ╰─────────────╯   ╰─────────────╯              │
                            │                                                │
                            ╰────────────────────────────────────────────────╯
                           

On Linux, one of the quicker ways to set up WireGuard is through wg-quick, which is an utility that takes a config file and sets up WireGuard with it. In Arch Linux, this comes in the wireguard-tools package:

# pacman -S wireguard-tools

wg-quick stores config files under /etc/wireguard/. Each config file has the name [name].conf, where the file name is used as the interface name. These are the same network interfaces as wlan0 or eth0; WireGuard typically uses names like wg0.

server configuration

An important thing to understand about WireGuard is that it makes no distinction of server or client: every device is a peer. To authenticate with each other, each peer has a private key, and a list of public keys of the peers it talks to. This is similar to how SSH's authorized_keys works. To generate both the private key and the public key at once, use this command:

$ wg genkey | tee /dev/tty | wg pubkey
WDax5fhKcKwdeuiYjgi4w/34ig2aZuDjmLHYnWUtfGc=
pJApgMHuIvMMsApTNXA3MMq+82nQ30XuVbAk9jsBNRs=

The private key will be on the first line, and the public key on the second. Keep these keys around, as they will be useful later.

Let's now see the config file for the Server:

# SERVER config
# /etc/wireguard/wg0.conf

[Interface]
Address = 10.0.0.1/32
# REPLACE THIS!
PrivateKey = [private key]
ListenPort = 51820

# make sure to replace eth0 by the appropriate interface
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

[Peer]
PublicKey = xZGlY8HIJt+rhGfbDK/T2xq0LQoR3kL6tGGVijaRBDI=
AllowedIPs = 10.0.0.2/32

First, let's look at the [Interface] section, which contains information about the Server. It has the private key, and also the UDP port that WireGuard listens on (remember to open this in the firewall). The address marked here is the Server's address. Its subnet is completely different from the LAN the Server is actually on (192.168.0.0/24). This is because we're creating a brand new private network (10.0.0.0/24) inside the VPN connection, where our Client and Server will coexist.

Note: 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/24 are all entirely reserved for private subnets, and you will not see them on the open Internet. To avoid collisions, the new VPN subnet should be different from the real LAN subnets for all peers. OpenVPN recommends obscure subnets like 10.66.77.0/24, which are equivalent to the middle of nowhere in IP address space.

There is also the PostUp and PostDown fields. These are commands run in bash after starting and stopping the VPN Server. I'm not going to go into the details, but I'll explain in general what they mean:

  • iptables -A means to add a firewall rule when starting the VPN, and -D is to delete the rule when the VPN stops.
  • The %i variable is part of wg-quick, and it expands to the VPN interface name (e.g. wg0).
  • These rules in general allow the Server to forward packets, while masquerading. Essentially, just as a device can access the Internet through a router, the Client accesses the internal network through the Server. To do this, the Server will act like a router and perform NAT.
  • -o eth0 means the internal network is accessed over the eth0 interface.

Meanwhile, in the [Peer] section, we write the Client's public key, which allows it to talk to the Server. Our Client has an address of 10.0.0.2, but instead of an Address field, we use AllowedIPs. These are the IP addresses that can be routed to and from this peer. Here are some examples to clarify what that means:

  • Let's say the Server wants to send a packet to 10.0.0.2. WireGuard sees that peer xZGlY... (the Client) has this IP in its allowlist, so the Server sends the packet to the Client.
  • The Server wants to send a packet to archlinux.org (95.217.163.246). This IP is not in the peer's allowlist, so it will not route the packet to the Client.
  • The Client sends a packet to the Server, with the source being its IP address 10.0.0.2. The Server sees that this IP is in the allowlist, so it accepts the packet from the Client.
  • The Client, who is now evil, decides to send a packet to the Server but impersonating another IP address: 10.0.0.69. This IP not being in the Server's allowlist, it rejects the packet.

In general, you can see that the AllowedIPs field is what determines where packets can and can't go. By setting AllowedIPs = 10.0.0.2, the server knows that the Client only has control over packets directed to or from that address. It is not allowed packets to or from any other IP. This concept of pairing the allowlist with public keys to manage packet routing is called cryptokey routing in WireGuard.

Also, by default, Linux disables IP forwarding. To enable it, edit /etc/sysctl.conf and add/uncomment the line

net.ipv4.ip_forward = 1

and run

# sysctl -p

to load the new configuration. If your VPN server is on the public internet, be sure to have sane firewall rules before doing this.

Note: If you use UFW as a firewall like me, note that it has its own sysctl.conf, which lives at /etc/ufw/sysctl.conf. This will override the regular sysctl if you follow the instructions above. To prevent it from erasing your changes, uncomment the relevant line:

# /etc/ufw/sysctl.conf

# Uncomment this to allow this host to route packets between interfaces
net/ipv4/ip_forward=1

client configuration

Let's now examine the Client's configuration file. If the client is running Linux, it will also be in /etc/wireguard/*.conf, However, the wg-quick configuration format can be read by many other clients, like the WireGuard Android app.

Anyways, here is the configuration file:

# CLIENT config
# wg0.conf

[Interface]
Address = 10.0.0.2/32
# REPLACE THIS!
PrivateKey = [private key]

# be careful not to use CIDR notation here
DNS = 10.0.0.1

[Peer]
PublicKey = M/HD4qJYi1RlMH/K9xQ12yW6Cu62LuGasyZhfnVsbUE=

# do not forget the port here
Endpoint = vpn.example.com:51820

AllowedIPs = 192.168.0.0/24

You'll see this configuration is very similar to the Server's, which is natural as WireGuard devices are all peers. There are a few differences, though.

First, there is a DNS field, which can be used to prevent DNS leaks. This only works if the VPN server provides a DNS server too. Otherwise, set it to some other DNS server, or remove the line.

There is also the Endpoint field. This marks the Server's public address on the open Internet. The Endpoint field is omitted in the Server configuration, as it is implicit: the Server will find out the endpoint IP when the Client reaches out to it for the first time.

Note: technically, the Endpoint field not always strictly necessary. If SaveConfig is enabled in the [Interface] section, and either the Server or Client changes IP address while connected, roaming allows WireGuard to keep working and update Endpoint to the new address in the config file. I haven't tested this, though.

Most importantly, we also have the AllowedIPs for the server. Here, it's an entire subnet, and not just a single address. What this means is that if the Client wants to send a packet to a device on the internal network, say 192.168.0.4, WireGuard matches the IP to the subnet 192.168.0.0/24, takes the packet and routes it through to the server, which then forwards these packets to the machine at 192.168.0.4 in the internal network.

If the machine then replies with a packet, WireGuard sees it is from the 192.168.0.0/24 subnet, then routes it back to the Client. Using the VPN, the Client will be able to communicate with machines in the internal network as if it were there.

running the vpn

wg-quick

To start/stop the VPN for both Client and Server (if they run Linux), you can use

# wg-quick up wg0
# wg-quick down wg0

where wg0 should be the name of your configuration file. Alternatively, there is a SystemD service you can use:

# systemctl start wg-quick@wg0
# systemctl stop wg-quick@wg0

or, to have WireGuard start at boot:

# systemctl enable wg-quick@wg0
# systemctl disable wg-quick@wg0

To diagnose issues with WireGuard, running wg show can be useful:

# wg show
interface: wg0
  public key: [snip]
  private key: (hidden)
  listening port: 49595

peer: [snip]
  endpoint: [snip]:51820
  allowed ips: 0.0.0.0/0
  latest handshake: 57 seconds ago
  transfer: 4.83 GiB received, 261.16 MiB sent
  persistent keepalive: every 25 seconds

This can show that there is no handshake at all between peers, and thus that there is no connection.

networkmanager

There is also the option of using NetworkManager, which is neat if you already use it to manage your existing network connections. To do this, save your configuration file, and then import it through nmcli:

$ nmcli con import type wireguard file wg0.conf

Then, you can manage the connection as usual through nmcli:

$ nmcli con down wg0
$ nmcli con up wg0
$ nmcli con edit wg0

mobile devices

On mobile devices, WireGuard apps can also use these configuration files:

You can securely transfer the config files in a normal way, but I recommend using QR codes because it's way simpler. Install the qrencode package, and do this:

$ cat wg0.conf | qrencode -t ansiutf8

█████████████████████████████████████
█████████████████████████████████████
████ ▄▄▄▄▄ █▄▄▄ ▀▀▀▄▀▄█▀██ ▄▄▄▄▄ ████
████ █   █ ██▄▀ █ ██▀▀▄▄▀█ █   █ ████
████ █▄▄▄█ ██▀▄ ▄▀▄█▄▄ ▄▀█ █▄▄▄█ ████
████▄▄▄▄▄▄▄█ ▀▄█ ▀▄▀ █▄█▄█▄▄▄▄▄▄▄████
████  ▀  ▀▄▀█▄▀█▄▄▄▄█ ▀ ▄▀▄▀▀█▀▀▄████
████▀█▄ ▀█▄  ▄██▄█ ▄   ▀▀▀▀▄ ▀▄█▀████
████▄█▄▄█▄▄█▀▄ █▀▄ ▀█▀▄█ █ ▀▀▀▀▀ ████
████▀ ▀█ █▄▄▄█▀█▀▀  █▄█▄▄▄▄  █ █ ████
████▀▄▀▀▄▀▄██   ▄██ ▄▀█  ███▀▄▀▀█████
████  ██▀▄▄█▀▀█▀▄ ▄▄ █▀▄█▀▄▀█ ▀█▄████
████▄▄▄▄█▄▄█▀ ▀▄▀ █ ▀ ▄█ ▄▄▄  ▄▄█████
████ ▄▄▄▄▄ ██▀▄ ▀ █ ▄ █  █▄█ ▄█▀█████
████ █   █ █ ▄ ▄▄ ▀ ▀█▀▄▄▄▄▄▄▄██ ████
████ █▄▄▄█ █▀   ▄▄█▄▀█▀▄█ █  ▀ ▄ ████
████▄▄▄▄▄▄▄█▄▄████▄▄▄▄█▄▄▄▄▄█▄███████
█████████████████████████████████████
█████████████████████████████████████

and the QR code will display in your terminal. On your phone, select the option to add a connection via QR code, and scan it. You can pipe any data to qrencode this way to generate a code, and it also creates PNG files if you select that format.

encrypted internet tunnel

I mentioned earlier that to the average user, a VPN is an encrypted tunnel they can run their Internet traffic through. The topology is

Client -> VPN Server -> public internet -> website

which makes it look to the website that the traffic originates from the VPN server. This is actually very similar to our internal network set from before, which boils down to

Client -> VPN Server -> public internet -> internal network

To achieve the Internet tunnel topology, we just need to modify the client configuration:

[Peer]
PublicKey = M/HD4qJYi1RlMH/K9xQ12yW6Cu62LuGasyZhfnVsbUE=
Endpoint = vpn.example.com:51820

- AllowedIPs = 192.168.0.0/24
+ AllowedIPs = 0.0.0.0/0

This [Peer] section refers to the Server. We modify AllowedIPs so that it includes all possible IPs (0.0.0.0/0), rather than just the internal subnet's IPs.

With this, every time the Client tries to communicate on the Internet, for instance it requests archlinux.org (95.217.163.246), WireGuard will match that IP to the 0.0.0.0/0 (all IPs) mask, and route the packet to the Server. Then, the Server will forward that packet to archlinux.org.

Thus, we've made a VPN that works just like NordVPN or ProtonVPN, and all the others.

further reading

This guide distills my knowledge with the network topologies I use, i.e. connecting to an internal network, and connecting to the Internet via WireGuard. However, that's of course not the only way you can use a VPN. Here are some resources that were useful to me:

Thank you for reading this article!