We configure a network namespace (as described in the WireGuard documentation) containing solely a WireGuard interface whereto systemd services can be confined.
(This section was updated on to account for a change in systemd version 254. Thanks to Sidharth K. for the tip.)
systemd does not support running services in a particular network namespace. A naïve solution would be using ip netns exec
to start the process (see man 5 systemd.unit
) :
{ ... }: {
systemd.services.<service>.serviceConfig.ExecStart = ''
ip netns exec <namespace> <command>
'';
}
However, that requires root privileges, and for security reasons we might want to run the process as unpriviledged user.
While systemd does not support confining services to a named system-wide network namespace, it will create a new network namespace ad hoc if the PrivateNetwork
service option is set. Multiple units can share such a namespace with the JoinsNamespaceOf
unit option.
We write a systemd instance template netns@.service
that creates a network namespace with a given name.
{ pkgs, ... }: {
config.systemd.services."netns@" = {
description = "%I network namespace";
# Delay network.target until this unit has finished starting up.
before = [ "network.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
PrivateNetwork = true;
ExecStart = "${pkgs.writers.writeDash "netns-up" ''
${pkgs.iproute}/bin/ip netns add $1
${pkgs.utillinux}/bin/umount /var/run/netns/$1
${pkgs.utillinux}/bin/mount --bind /proc/self/ns/net /var/run/netns/$1
''} %I";
ExecStop = "${pkgs.iproute}/bin/ip netns del %I";
# This is required since systemd commit c2da3bf, shipped in systemd 254.
# See discussion at https://github.com/systemd/systemd/issues/28686
PrivateMounts = false;
};
};
}
(This section was updated on to properly wait for a network connection.)
Next, we write a service wg
requiring netns@wg.service
(thus, a wg
namespace will be created) and setting up a WireGuard interface in the wg
network namespace. We define some options in our module for configuring the interface.
{ config, lib, pkgs, ... }: let
cfg = config.my.wireguard;
in {
options.my.wireguard = with lib; {
enable = mkEnableOption ""; # I'm too lazy to fill those out
address = mkOption {};
peer = mkOption {};
endpoint = mkOption {};
privateKey = mkOption {
type = types.path;
};
};
config.systemd.services.wg = {
description = "wg network interface";
# Absolutely require the wg network namespace to exist.
bindsTo = [ "netns@wg.service" ];
# Require a network connection.
requires = [ "network-online.target" "nss-lookup.target" ];
# Start after and stop before those units.
after = [ "netns@wg.service" "network-online.target" "nss-lookup.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = pkgs.writers.writeDash "wg-up" ''
${pkgs.iproute}/bin/ip link add wg type wireguard
${pkgs.wireguard}/bin/wg set wg \
private-key ${cfg.privateKey} \
peer ${cfg.peer} \
allowed-ips 0.0.0.0/0,::/0 \
endpoint ${cfg.endpoint}
${pkgs.iproute}/bin/ip link set wg netns wg up
${pkgs.iproute}/bin/ip -n wg address add ${cfg.address.IPv4} dev wg
${pkgs.iproute}/bin/ip -n wg -6 address add ${cfg.address.IPv6} dev wg
${pkgs.iproute}/bin/ip -n wg route add default dev wg
${pkgs.iproute}/bin/ip -n wg -6 route add default dev wg
'';
ExecStop = pkgs.writers.writeDash "wg-down" ''
${pkgs.iproute}/bin/ip -n wg link del wg
${pkgs.iproute}/bin/ip -n wg route del default dev wg
'';
};
};
}
Services can be confined to the wg
network namespace with JoinsNamespaceOf = netns@wg.service
. To require the WireGuard interface be up and running, we bind to wg.service
. Also, remember to set the options under my.wireguard
.
{ ... }: {
systemd.services.<service> = {
bindsTo = [ "wg.service" ];
after = [ "wg.service" ];
unitConfig.JoinsNamespaceOf = "netns@wg.service";
serviceConfig.PrivateNetwork = true;
};
}