Wireguard VPN with Guix

Needing a VPN

Recently I changed my ISP, and the new one uses Carrier-grade NAT, or CGNAT, by default. While this sounds fancy and professional, it is in fact even worse than conventional NAT: Not only do all my devices share the same IPv4, but I share one IPv4 with several other customers! Apparently I am only assigned a few out of the 65535 ports, and this assignment may change from day to day, which implies that I cannot connect from the outside to any of my home devices. However, I do have a separate IPv4 of my own for a virtual machine at Aquilenet, and it should be possible to use this as a trampoline to access my home through a virtual private network. We are already employing WireGuard for one of the Guix build farms, so it felt like a natural choice. Guix provides the wireguard-service-type, which is documented with all its options in the manual; but without an explanation of the general concepts behind the service it is a bit difficult to set up. The Guix Cookbook has an entry on WireGuard, but it is concerned with kernel modules and connecting to an existing WireGuard VPN, while my goal was to set one up in the first place. This turned out to be surprisingly easy.

The wireguard-tools package comes with an executable wg, and running wg --help is enough to guess how WireGuard works; essentially we need the following two subcommands:

genkey: Generates a new private key and writes it to stdout
pubkey: Reads a private key from stdin and writes a public key to stdout

Unlike other VPN, WireGuard appears to be more symmetric in the sense that it does not distinguish between servers and clients; to talk to each other, two participants just need to create a pair of public and private keys each, and then to be made aware of the other's public key. In our asymmetric situation in which only one of them has a public IPv4, we will nevertheless distinguish the server, which is reachable from everywhere thanks to its IP, and the clients hidden behind the CGNAT.

Creating key pairs

In a first step, we create a private key for the server. In Guix, secrets are (so far) not handled through the world-readable store, but as state directly on the machine, and wireguard-service-type expects by default the private key in the file /etc/wireguard/private.key. So we connect as root to the server machine and execute

mkdir /etc/wireguard
umask 077
wg genkey > /etc/wireguard/private.key

The call to umask is needed (at least with my shell settings) to placate the WireGuard warning that the private key file is world-readable, which indeed defies its purpose. The file contains a short base64 encoded number such as GEhlpFGslXfo9We9jhrXham4LztmqSmpdE4ivML4qXc=. Given the size (or rather lack thereof) of this number, it looks like WireGuard uses elliptic curve cryptography with a fixed elliptic curve and a fixed basepoint of 256 bits, so with a security level of 128 bits.

In a second step, we need to create the corresponding public key using the command

wg pubkey < /etc/wireguard/private.key

This outputs a base64 encoded number of similar size; in our example, AFL8UecS3GFX3hK8e6yWOK4s5RVrTpvTq2A0pdGuylQ=. This public key is in fact not needed by the server, but only by the clients wishing to connect to it (following a basic principle of asymmetric cryptography), so we need to stow it away in a file. But since the process of deriving the public key from a private key is deterministic, we may actually forget the public key and recreate it when needed. And notice that the public key is so short that it could even be exchanged on a postcard or over the phone.

This process of creating a key pair needs to be repeated on each client, or more generally, each participant in the VPN. Let us assume we have one client with private key 0G4uhLLeY5NYmg/FobRB0p75wMrGwmmzhuoAdfX243I= in /etc/wireguard/private.key and corresponding public key BgMzEZPUGAtbSqVPRdzgdLVhAPMLaOzHe7uNFAMVLCk=.

Setting up the server

Each participant in the WireGuard network uses a private IPv4, usually from the 10.0.0.0/8 range. We give 10.0.0.1 to the server and 10.0.0.2 to the client, and can now follow the documentation (you need to scroll down a bit) of wireguard-service-type to write the corresponding block in the Guix operating system configuration of the server:

(service wireguard-service-type
  (wireguard-configuration
    (addresses '("10.0.0.1/32"))
    (peers
      (list
        (wireguard-peer
          (name "client")
          (public-key "BgMzEZPUGAtbSqVPRdzgdLVhAPMLaOzHe7uNFAMVLCk=")
          (allowed-ips '("10.0.0.2/32")))))))

The addresses field is in fact the default; there is also an optional port field with default 51820. The peers field is a list of, well, peers in the VPN which are allowed to connect to the server (so it is in theory possible to create strange connection graphs); in this case, we register only one client peer with an arbitrary name, its public key created above, and its assigned private IP address. That is all! Now we can guix system reconfigure, and the server is ready.

Setting up the client

As stated above, in principle there does not seem to be a distinction between clients and server in WireGuard, so the operating system declaration on the client is similar to that on the server. But I think that nevertheless, it is necessary to bootstrap the network topology. And in our case, the inherent distinction between the server machine which is, say, publicly reachable on the IPv4 198.51.100.0 under the name vpn.example.org, and the client hidden by CGNAT needs to be taken into account. So we need the client to punch a hole into the NAT and to reach out to the server, which leads to the following service block:

(service wireguard-service-type
  (wireguard-configuration
    (addresses '("10.0.0.2/32"))
    (peers
      (list
        (wireguard-peer
          (name "server")
          (public-key "AFL8UecS3GFX3hK8e6yWOK4s5RVrTpvTq2A0pdGuylQ=")
          (allowed-ips '("10.0.0.1/32"))
          (endpoint "198.51.100.0:51820")
          (keep-alive 60))))))

The first fields are symmetric to the corresponding fields on the server. But the additional endpoint and keep-alive fields tell the client to connect to the server on its public IPv4 address (and the default port 51820) for the initial handshake establishing the session, and to keep it alive by reconnecting every 60 seconds. I have tried to use the host name vpn.example.org instead of the IPv4, but this ended up being resolved to an IPv6, which did not work. So guix system reconfigure the client, wait for at most one minute, and the VPN is running!

Looking behind the scenes

The following is not necessary for setting up the VPN, but it may be helpful for trouble shooting; and I was curious to see how the VPN manifested itself. Running ifconfig as root on the client, say, shows a new interface

wg0 Link encap:(hwtype unknown)
    inet addr:10.0.0.2  P-t-P:10.0.0.2  Mask:255.255.255.255
…

and running wg (again as root) shows the information about the VPN that we entered into the service description:

interface: wg0
  public key: BgMzEZPUGAtbSqVPRdzgdLVhAPMLaOzHe7uNFAMVLCk=
  private key: (hidden)
  listening port: 51820

peer: AFL8UecS3GFX3hK8e6yWOK4s5RVrTpvTq2A0pdGuylQ=
  endpoint: 198.51.100.0:51820
  allowed ips: 10.0.0.1/32
  latest handshake: 1 minute, 15 seconds ago
  transfer: 555.77 KiB received, 1.38 MiB sent
  persistent keepalive: every 1 minute

Finally, connecting from the outside!

But let us not get carried away by the beauty of technology (and cryptography), but get back to our initial concern: Connect from the outside to the machine in the home network, which we know under the name of client. This is now just a matter of two hops with ssh: First do an ssh vpn.example.org, and once on the VPN server machine, a second ssh 10.0.0.2. This can be automated by the following entry in .ssh/config on the machine from which we try to connect:

Host client
  Hostname 10.0.0.2
  ProxyJump vpn.example.org

so that from now on, ssh client will send us into the home network. Voilà, problem solved!

Epilogue

After installing my WireGuard VPN, I talked with a fellow geek from Aquilenet, who has the same ISP, and who suggested an alternative solution to me. I should call the hotline and pronounce the magic words that I would like an “IP rollback” so that servers at home become accessible. This helps passing the barrier of the first support level. The next level then initiates the “rollback”, which means going from IPv6 (plus IPv4 with CGNAT) back to regular IPv4 (without IPv6). After a few hours or days, the new more or less static (not guaranteed to be so, but actually not changing) IPv4 address is established, and IPv6 is disabled in the wireless router provided by the ISP. One can then reenable IPv6 in the router and lives in the best of all worlds – with a static IPv4 address, IPv6 and a WireGuard VPN on top of it all.