diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix index 4176da2c8cb..980961225c9 100644 --- a/nixos/modules/services/networking/wireguard.nix +++ b/nixos/modules/services/networking/wireguard.nix @@ -112,6 +112,32 @@ let Determines whether to add allowed IPs as routes or not. ''; }; + + socketNamespace = mkOption { + default = null; + type = with types; nullOr str; + example = "container"; + description = ''The pre-existing network namespace in which the + WireGuard interface is created, and which retains the socket even if the + interface is moved via . When + null, the interface is created in the init namespace. + See documentation. + ''; + }; + + interfaceNamespace = mkOption { + default = null; + type = with types; nullOr str; + example = "init"; + description = ''The pre-existing network namespace the WireGuard + interface is moved to. The special value init means + the init namespace. When null, the interface is not + moved. + See documentation. + ''; + }; }; }; @@ -239,6 +265,10 @@ let if peer.presharedKey != null then pkgs.writeText "wg-psk" peer.presharedKey else peer.presharedKeyFile; + src = interfaceCfg.socketNamespace; + dst = interfaceCfg.interfaceNamespace; + ip = nsWrap "ip" src dst; + wg = nsWrap "wg" src dst; in nameValuePair "wireguard-${interfaceName}-peer-${unitName}" { description = "WireGuard Peer - ${interfaceName} - ${peer.publicKey}"; @@ -255,16 +285,16 @@ let }; script = let - wg_setup = "wg set ${interfaceName} peer ${peer.publicKey}" + + wg_setup = "${wg} set ${interfaceName} peer ${peer.publicKey}" + optionalString (psk != null) " preshared-key ${psk}" + optionalString (peer.endpoint != null) " endpoint ${peer.endpoint}" + optionalString (peer.persistentKeepalive != null) " persistent-keepalive ${toString peer.persistentKeepalive}" + optionalString (peer.allowedIPs != []) " allowed-ips ${concatStringsSep "," peer.allowedIPs}"; route_setup = - optionalString (interfaceCfg.allowedIPsAsRoutes != false) + optionalString interfaceCfg.allowedIPsAsRoutes (concatMapStringsSep "\n" (allowedIP: - "ip route replace ${allowedIP} dev ${interfaceName} table ${interfaceCfg.table}" + "${ip} route replace ${allowedIP} dev ${interfaceName} table ${interfaceCfg.table}" ) peer.allowedIPs); in '' ${wg_setup} @@ -272,13 +302,13 @@ let ''; postStop = let - route_destroy = optionalString (interfaceCfg.allowedIPsAsRoutes != false) + route_destroy = optionalString interfaceCfg.allowedIPsAsRoutes (concatMapStringsSep "\n" (allowedIP: - "ip route delete ${allowedIP} dev ${interfaceName} table ${interfaceCfg.table}" + "${ip} route delete ${allowedIP} dev ${interfaceName} table ${interfaceCfg.table}" ) peer.allowedIPs); in '' - wg set ${interfaceName} peer ${peer.publicKey} remove + ${wg} set ${interfaceName} peer ${peer.publicKey} remove ${route_destroy} ''; }; @@ -287,6 +317,13 @@ let # exactly one way to specify the private key must be set #assert (values.privateKey != null) != (values.privateKeyFile != null); let privKey = if values.privateKeyFile != null then values.privateKeyFile else pkgs.writeText "wg-key" values.privateKey; + src = values.socketNamespace; + dst = values.interfaceNamespace; + ipPreMove = nsWrap "ip" src null; + ipPostMove = nsWrap "ip" src dst; + wg = nsWrap "wg" src dst; + ns = if dst == "init" then "1" else dst; + in nameValuePair "wireguard-${name}" { @@ -307,26 +344,33 @@ let ${values.preSetup} - ip link add dev ${name} type wireguard + ${ipPreMove} link add dev ${name} type wireguard + ${optionalString (values.interfaceNamespace != null && values.interfaceNamespace != values.socketNamespace) "${ipPreMove} link set ${name} netns ${ns}"} ${concatMapStringsSep "\n" (ip: - "ip address add ${ip} dev ${name}" + "${ipPostMove} address add ${ip} dev ${name}" ) values.ips} - wg set ${name} private-key ${privKey} ${ + ${wg} set ${name} private-key ${privKey} ${ optionalString (values.listenPort != null) " listen-port ${toString values.listenPort}"} - ip link set up dev ${name} + ${ipPostMove} link set up dev ${name} ${values.postSetup} ''; postStop = '' - ip link del dev ${name} + ${ipPostMove} link del dev ${name} ${values.postShutdown} ''; }; + nsWrap = cmd: src: dst: + let + nsList = filter (ns: ns != null) [ src dst ]; + ns = last nsList; + in + if (length nsList > 0 && ns != "init") then "ip netns exec ${ns} ${cmd}" else cmd; in { diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 04323317a99..dc7225456b6 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -280,6 +280,7 @@ in virtualbox = handleTestOn ["x86_64-linux"] ./virtualbox.nix {}; wireguard = handleTest ./wireguard {}; wireguard-generated = handleTest ./wireguard/generated.nix {}; + wireguard-namespaces = handleTest ./wireguard/namespaces.nix {}; wordpress = handleTest ./wordpress.nix {}; xautolock = handleTest ./xautolock.nix {}; xdg-desktop-portal = handleTest ./xdg-desktop-portal.nix {}; diff --git a/nixos/tests/wireguard/namespaces.nix b/nixos/tests/wireguard/namespaces.nix new file mode 100644 index 00000000000..94f993d9475 --- /dev/null +++ b/nixos/tests/wireguard/namespaces.nix @@ -0,0 +1,80 @@ +let + listenPort = 12345; + socketNamespace = "foo"; + interfaceNamespace = "bar"; + node = { + networking.wireguard.interfaces.wg0 = { + listenPort = listenPort; + ips = [ "10.10.10.1/24" ]; + privateKeyFile = "/etc/wireguard/private"; + generatePrivateKeyFile = true; + }; + }; + +in + +import ../make-test.nix ({ pkgs, ...} : { + name = "wireguard-with-namespaces"; + meta = with pkgs.stdenv.lib.maintainers; { + maintainers = [ asymmetric ]; + }; + + nodes = { + # interface should be created in the socketNamespace + # and not moved from there + peer0 = pkgs.lib.attrsets.recursiveUpdate node { + networking.wireguard.interfaces.wg0 = { + preSetup = '' + ip netns add ${socketNamespace} + ''; + inherit socketNamespace; + }; + }; + # interface should be created in the init namespace + # and moved to the interfaceNamespace + peer1 = pkgs.lib.attrsets.recursiveUpdate node { + networking.wireguard.interfaces.wg0 = { + preSetup = '' + ip netns add ${interfaceNamespace} + ''; + inherit interfaceNamespace; + }; + }; + # interface should be created in the socketNamespace + # and moved to the interfaceNamespace + peer2 = pkgs.lib.attrsets.recursiveUpdate node { + networking.wireguard.interfaces.wg0 = { + preSetup = '' + ip netns add ${socketNamespace} + ip netns add ${interfaceNamespace} + ''; + inherit socketNamespace interfaceNamespace; + }; + }; + # interface should be created in the socketNamespace + # and moved to the init namespace + peer3 = pkgs.lib.attrsets.recursiveUpdate node { + networking.wireguard.interfaces.wg0 = { + preSetup = '' + ip netns add ${socketNamespace} + ''; + inherit socketNamespace; + interfaceNamespace = "init"; + }; + }; + }; + + testScript = '' + startAll(); + + $peer0->waitForUnit("wireguard-wg0.service"); + $peer1->waitForUnit("wireguard-wg0.service"); + $peer2->waitForUnit("wireguard-wg0.service"); + $peer3->waitForUnit("wireguard-wg0.service"); + + $peer0->succeed("ip -n ${socketNamespace} link show wg0"); + $peer1->succeed("ip -n ${interfaceNamespace} link show wg0"); + $peer2->succeed("ip -n ${interfaceNamespace} link show wg0"); + $peer3->succeed("ip link show wg0"); + ''; +})