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.