WireGuard in NetworkManager

WireGuard in NetworkManager

NetworkManager 1.16 got native support for WireGuard VPN tunnels (NEWS). WireGuard is a novel VPN tunnel protocol and implementation that spawned a lot of interest. Here I will not explain how WireGuard itself works. You can find very good documentation and introduction at wireguard.com.

Having support in NetworkManager is great for two main reasons:

  • NetworkManager provides a de facto standard API for configuring networking on the host. This allows different tools to integrate and interoperate — from cli, tui, GUI, to cockpit. All these different components may now make use of the API also for configuring WireGuard. One advantage for the end user is that a GUI for WireGuard is now within reach.
  • By configuring WireGuard with NetworkManager you get other features beyond the plain WireGuard tunnel setup. Most notably you get DNS and firewalld setup in a consistent manner.
alice
For Alice it is now easy to configure WireGuard with NetworkManager.

NetworkManager’s support for WireGuard requires the kernel module for Linux. As of March 2019, it is not yet upstream in mainline kernel but easy to install on most distributions.

Import an existing WireGuard profile

The WireGuard project provides a wg-quick tool to setup WireGuard tunnels. If you are using WireGuard already, chances are that you use this tool. In that case you would have a configuration file and issue wg-quick up. Here is the example configuration file from wg-quick’s manual page:

[Interface]
Address = 10.192.122.1/24
Address = 10.10.0.1/16
SaveConfig = true
PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=
ListenPort = 51820

[Peer]
PublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=
AllowedIPs = 10.192.122.3/32, 10.192.124.1/24

[Peer]
PublicKey = TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0=
AllowedIPs = 10.192.122.4/32, 192.168.0.0/16

[Peer]
PublicKey = gN65BkIKy1eCE9pP1wdc8ROUtkHLF2PfAqYdyYBz6EA=
AllowedIPs = 10.10.10.230/32

Let’s import this into NetworkManager:

$ CONF_FILE="wg0.conf"
$ nmcli connection import type wireguard file "$CONF_FILE"
Connection 'wg0' (125d4b76-d230-47b0-9c31-bb7b9ebca861) successfully added.

Note that the PreUp, PostUp, PreDown, and PostDown keys are ignored during import.

You may delete the profile again with

$ nmcli connection delete wg0
Connection 'wg0' (125d4b76-d230-47b0-9c31-bb7b9ebca861) successfully deleted.

About Connection Profiles

Note that wg-quick up wg0.conf does something fundamentally different from what nmcli connection import does. When you run wg-quick up, it reads the file, configures the WireGuard tunnel, sets up addresses and routes, and exits.

This is not what “connection import” does. NetworkManager is profile based. That means you create profiles instead of issuing ad-hoc commands that configure ephemeral settings (like ip address add, wg set, or wg-quick up). NetworkManager calls these profiles “connections”. Configuring something in NetworkManager usually boils down to create a suitable profile and “activate” it for the settings to take effect.

nmcli connection import is just one way to create a profile. Note that the imported profile is configured to autoconnect, so quite possibly the profile gets activated right away. But regardless of that, think of “import” creating just a profile. You would only do this step once, but afterwards activate the profile many times.

There is no difference to NetworkManager how the profile was created. You could also create a WireGuard profile from scratch.

$ nmcli connection add type wireguard ifname wg0 con-name my-wg0
Connection 'my-wg0' (0d2aed05-2c7f-40ec-81ad-b1b4edd898fc) successfully added.

And let’s look at the profile:

$ nmcli --show-secrets connection show my-wg0
connection.id:                       my-wg0
connection.uuid:                     0d2aed05-2c7f-40ec-81ad-b1b4edd898fc
connection.stable-id:                --
connection.type:                     wireguard
connection.interface-name:           wg0
connection.autoconnect:              yes
[...]
ipv4.method:                         disabled
[...]
ipv6.method:                         ignore
[...]
wireguard.private-key:               --
wireguard.private-key-flags:         0 (none)
wireguard.listen-port:               0
wireguard.fwmark:                    0x0
wireguard.peer-routes:               yes
wireguard.mtu:                       0
[...]

and finally let’s activate it. Note you will be asked to enter the private key that you may generate with wg genkey:

$ nmcli --show-secrets --ask connection up my-wg0
Secrets are required to connect WireGuard VPN 'my-wg0'
WireGuard private-key (wireguard.private-key): eD8wqjLABmg6ClC+6egB/dnMLbbUYSMMrDsrHUwmQlI=
Connection successfully activated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/30)

Confirm that the VPN tunnel is now up:

$ nmcli
[...]
wg0: connected to my-wg0
        "wg0"
        wireguard, sw, mtu 1420
        inet6 fe80::720b:6576:1650:d26/64
        route6 ff00::/8
        route6 fe80::/64
$ ip link show wg0
34: wg0:  mtu 1420 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/none
$ sudo WG_HIDE_KEYS=never wg
interface: wg0
  public key: SymChsQwTX5yZrtwtsWpYfHLMgnJpOJ25YOfs7/ImT0=
  private key: eD8wqjLABmg6ClC+6egB/dnMLbbUYSMMrDsrHUwmQlI=
  listening port: 56389

Note that above wireguard.private-key-flags are set to 0. The secret flags determine whether the secret is not-required, to be stored to disk or a keyring, or always asked. In this case, the private key got stored to disk in /etc/NetworkManager/system-connections/.

This connection isn’t right yet. Let’s adjust it:

$ nmcli connection modify my-wg0 \
    autoconnect yes \
    ipv4.method manual \
    ipv4.addresses 192.168.7.5/24 \
    wireguard.listen-port 50000 \
    ...

Check the manual for available NetworkManager settings in the profile. Compare what you configured until the profile is to your liking:

$ nmcli --overview connection show my-wg0
connection.id:                          my-wg0
connection.uuid:                        0d2aed05-2c7f-40ec-81ad-b1b4edd898fc
connection.type:                        wireguard
connection.interface-name:              wg0
connection.timestamp:                   1551171032
ipv4.method:                            manual
ipv4.addresses:                         192.168.7.5/24
ipv6.method:                            ignore
wireguard.private-key-flags:            0 (none)
wireguard.listen-port:                  50000
GENERAL.NAME:                           my-wg0
GENERAL.UUID:                           0d2aed05-2c7f-40ec-81ad-b1b4edd898fc
GENERAL.DEVICES:                        wg0
GENERAL.STATE:                          activated
GENERAL.DEFAULT:                        no
GENERAL.DEFAULT6:                       no
GENERAL.SPEC-OBJECT:                    --
GENERAL.VPN:                            no
GENERAL.DBUS-PATH:                      /org/freedesktop/NetworkManager/ActiveConnection/30
GENERAL.CON-PATH:                       /org/freedesktop/NetworkManager/Settings/60
GENERAL.ZONE:                           --
GENERAL.MASTER-PATH:                    --
IP6.ADDRESS[1]:                         fe80::720b:6576:1650:d26/64
IP6.ROUTE[1]:                           dst = ff00::/8, nh = ::, mt = 256, table=255
IP6.ROUTE[2]:                           dst = fe80::/64, nh = ::, mt = 256

Note that above output also shows the current device information with upper-cased properties. This is because the profile is currently activated. As you modify the profile, you’ll note that the changes don’t take effect immediately. For that you have to (re-) activate the profile with

$ nmcli connection up my-wg0

Note that this time we don’t need to provide the private key. The key was stored to disk according to the secret flags. This will allow the profile to automatically connect in the future upon boot.

Configuring Peers

As of now, nmcli does not yet support configuring peers. This is a missing feature. Until this is implemented you have the following possibilities, which are all a bit inconvenient.

1.) Import Peers from a wg-quick configuration file

See above. This does not allow you to modify an existing profile, as nmcli connection import always creates a new profile.

2.) Use the Python Example Script nm-wg-set

There is a python example script. It uses pygobject with libnm and accepts similar parameters as wg set. I mention this example script to give you an idea how you could use NetworkManager from python (in this case based on libnm and pygobject).

$ python nm-wg-set my-wg0 \
    fwmark 0x500 \
    peer llG3xkDWcEP4KODf45zjntuvUX0oXieRyxXdl5POYX4= \
    endpoint my-wg.example.com:4001 \
    allowed-ips 192.168.7.0/24 \
    persistent-keepalive 120 \
    peer 2Gl0SATbfrrzxfrSkhNoRR9Jg56y533y07KtIVngAk0= \
    preshared-key \
      <(echo qoNbN/6ABe4wWyz4jh+uwX7vqRpNeGEtgAnUbwNjEug=) \
    preshared-key-flags 0 \
    ...
Success
$ WG_HIDE_KEYS=never python nm-wg-set my-wg0 
interface:                    wg0
uuid:                         0d2aed05-2c7f-40ec-81ad-b1b4edd898fc
id:                           my-wg0
private-key:                  eD8wqjLABmg6ClC+6egB/dnMLbbUYSMMrDsrHUwmQlI=
private-key-flags:            0 (none)
listen-port:                  50000
fwmark:                       0x500
peer[0].public-key:           llG3xkDWcEP4KODf45zjntuvUX0oXieRyxXdl5POYX4=
peer[0].preshared-key:        
peer[0].preshared-key-flags:  4 (not-required)
peer[0].endpoint:             my-wg.example.com:4001
peer[0].persistent-keepalive: 120
peer[0].allowed-ips:          192.168.7.0/24
peer[1].public-key:           2Gl0SATbfrrzxfrSkhNoRR9Jg56y533y07KtIVngAk0=
peer[1].preshared-key:        qoNbN/6ABe4wWyz4jh+uwX7vqRpNeGEtgAnUbwNjEug=
peer[1].preshared-key-flags:  0 (none)
peer[1].endpoint:             
peer[1].persistent-keepalive: 0
peer[1].allowed-ips:                    

3.) Use libnm directly

libnm is the client library for NetworkManager. It gained API for fully configuring WireGuard profiles. This is what the nm-wg-set example script above uses.

4.) Use D-Bus directly

NetworkManager’s D-Bus API is what all clients use — from libnm, nmcli to GUIs. NetworkManager is really all about the (D-Bus) API that it provides. Everything that a tool does with NetworkManager will always be possible by using D-Bus directly. When NetworkManager 1.16 introduces WireGuard support, then the tools are still lacking, but the API is ready for implementing them.

5.) Edit the Profile on Disk

NetworkManager persists WireGuard profiles in the keyfile format. These are files under /etc/NetworkManager/system-connections and it is always fully supported that you just edit these files by hand. This is the other, file-base API of NetworkManager beside D-Bus. This leaves you with the problem to know what to edit there exactly. Let’s look at what we got so far:

$ sudo cat \
    /etc/NetworkManager/system-connections/my-wg0.nmconnection
[connection]
id=my-wg0
uuid=0d2aed05-2c7f-40ec-81ad-b1b4edd898fc
type=wireguard
interface-name=wg0
permissions=
timestamp=1551172232

[wireguard]
fwmark=1280
listen-port=50000
private-key=eD8wqjLABmg6ClC+6egB/dnMLbbUYSMMrDsrHUwmQlI=

[wireguard-peer.llG3xkDWcEP4KODf45zjntuvUX0oXieRyxXdl5POYX4=]
endpoint=my-wg.example.com:4001
persistent-keepalive=120
allowed-ips=192.168.7.0/24;

[wireguard-peer.2Gl0SATbfrrzxfrSkhNoRR9Jg56y533y07KtIVngAk0=]
preshared-key=qoNbN/6ABe4wWyz4jh+uwX7vqRpNeGEtgAnUbwNjEug=
preshared-key-flags=0

[ipv4]
address1=192.168.7.5/24
dns-search=
method=manual

[ipv6]
addr-gen-mode=stable-privacy
dns-search=
method=ignore

The WireGuard peer settings should be pretty straight forward. See also NetworkManager’s keyfile documentation. Edit the file and issue sudo nmcli connection reload or sudo nmcli connection load /etc/NetworkManager/system-connection/my-wg0.nmconnection. This causes NetworkManager to update the profile with the changes from disk.

Finally, reactivate the profile and check the result:

$ nmcli connection up my-wg0 
Connection successfully activated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/31)
$ sudo WG_HIDE_KEYS=never wg
interface: wg0
  public key: SymChsQwTX5yZrtwtsWpYfHLMgnJpOJ25YOfs7/ImT0=
  private key: eD8wqjLABmg6ClC+6egB/dnMLbbUYSMMrDsrHUwmQlI=
  listening port: 50000
  fwmark: 0x500

peer: llG3xkDWcEP4KODf45zjntuvUX0oXieRyxXdl5POYX4=
  allowed ips: 192.168.7.0/24
  persistent keepalive: every 2 minutes

peer: 2Gl0SATbfrrzxfrSkhNoRR9Jg56y533y07KtIVngAk0=
  preshared key: qoNbN/6ABe4wWyz4jh+uwX7vqRpNeGEtgAnUbwNjEug=
  allowed ips: (none)

Reapply and Runtime Configuration

We said that after modifying a profile we have to fully reactivate the profile for the changes to take effect. That’s not the only way. NetworkManager supports nmcli device reapply wg0 which makes changes to the profile effective without doing a full re-activation cycle. That is less disruptive as the interface does not go down. Likewise, nmcli device modify wg0 allows you to change only the runtime configuration, without modifying the profile. It is fully supported to modify WireGuard settings of an active tunnel via reapply.

Dynamically Resolving Endpoints

In WireGuard, peers may have an endpoint configured but also roaming is built-in. NetworkManager supports peer endpoints specified as DNS names: it will resolve the names before configuring the IP address in kernel. NetworkManager resolves endpoint names every 30 minutes or whenever the DNS configuration of the host changes, in order to pick up changes to the endpoint’s IP address.

MTU

In the NetworkManager profile you can configure wireguard.mtu for the MTU. In absence of an explicit configuration, the default is used. That is different from wg-quick up, which tries to autodetect the MTU by looking at how to reach all peers. NetworkManager does not do such automatism.

Peer Routes, AllowedIPs and Cryptokey Routing

In WireGuard you need to configure the “AllowedIPs” ranges for the peers. This is what WireGuard calls Cryptokey Routing. It also implies, that you usually configure direct routes for these “AllowedIPs” ranges via the WireGuard tunnel. NetworkManager will add those routes automatically if wireguard.peer-routes option of the profile is enabled (which it is by default).

Routing All Your Traffic

When routing all traffic via the WireGuard tunnel, then peer endpoints must be still reached outside the tunnel.

For other VPN plugins NetworkManager adds a direct route to the external VPN gateway on the device that has the default route. That works well in most cases, but is an ugly hack because NetworkManager doesn’t reliably know the correct direct route in unusual scenarios.

NetworkManager currently does not provide any additional automatism to help you with that. As workaround you could manually add an explicit route to the profile of the device via which the endpoint is reachable:

$ WG_ENDPOINT_ADDR=...
$ nmcli connection modify eth0 \
    +ipv4.routes "$WG_ENDPOINT_ADDR/32 192.168.1.1"

An alternative solution is to configure policy routing. The wg-quick tool does this with the Table=auto setting (which is the default).

NetworkManager supports configuring routes in other routing tables than the “main” table. Hence, using policy-routing works in parts by configuring "ipv4.route-table" and "ipv6.route-table". The problem is that currently NetworkManager does not support configuring the routing policy rules themselves. For now, the rules must be configured outside of NetworkManager. You could do so via a dispatcher script in /etc/NetworkManager/dispatcher.d, but yes, this is lacking. See the NetworkManager manual about dispatcher scripts.

update 2019/08/02 NetworkManager supports since 1.18.0 configuring policy routing rules in the profile. However, there are still two caveats to manually configure what wg-quick does with TABLE=auto (and what WireGuard calls “Improved Rule-based Routing“). First, it requires the suppress_prefixlength rule attribute. That attribute is only supported since NetworkManager 1.20.0. The second problem is that it requires to put the default-route in a dedicated table. While you can configure the routing table for manual routes in NetworkManager, you currently cannot configure a default route (with prefix lenth 0) like a manual route. That needs fixing. On the upside, 1.20.0 brings also new options wireguard.ip4-auto-default-route and wireguard.ip6-auto-default-route. These options are enabled by default and NetworkManager will now automatically configure policy routing like wg-quick with TABLE=auto. This automatism offers a nice solution for the problem.

Key, Peer, and IP Address Management

The beauty of WireGuard is its simplicity. But it also leaves all questions about key distribution, peer management and IP address assignment to the upper layers. For the moment NetworkManager does not provide additional logic on top of WireGuard and exposes just the plain settings. This leaves the user (or external tools) to manually distribute private keys and configure peers, IP addresses and routing. I expect that as WireGuard matures there will be schemes for simplifying this and NetworkManager may implement such protocols or functionality. But NetworkManager won’t come up with a homegrown, non-standard way of doing this.

WireGuard is Layer3 only. That means you cannot run DHCP on a WireGuard link and ipv4.method=auto is not a valid configuration. Instead, you have to configure static addresses or IPv6 link local addresses.

Namespaces

WireGuard, like most tunnel based solutions, have neat applications regarding networking namespaces. This is not implemented in NetworkManager yet, but we would be interested to do so. Note that this isn’t specific to WireGuard tunnels and namespace isolation would be a useful feature in general.

What’s next?

  • Add support for policy-routing rules (rhbz#1652653).
  • Automatically help avoiding routing loops when routing all traffic.
  • Add nmcli support for configuring WireGuard peers.
  • Add WireGuard support to other NetworkManager clients, like nm-connection-editor.
  • See where management tools for WireGuard go and what NetworkManager can do to simplify management of keys, peers and addressing.
  • Provide an API in NetworkManager to isolate networks via networking namespaces. This is not specific to WireGuard but will be useful in that context.