summary refs log tree commit diff
diff options
context:
space:
mode:
authorLorenzo Manacorda <lorenzo@mailbox.org>2019-09-19 22:54:38 +0200
committerLorenzo Manacorda <lorenzo@mailbox.org>2019-11-09 11:59:14 +0100
commit412f6a967d0d545686e284bfb3fdfe6015eb8bb1 (patch)
tree6263413597082ce7dc124c05e9348b4910e6f247
parentb943338ea582aeb9b0a406d7fb75f3f62bc16a9d (diff)
wireguard: add creation and destination namespaces
The two new options make it possible to create the interface in one namespace
and move it to a different one, as explained at https://www.wireguard.com/netns/.
-rw-r--r--nixos/modules/services/networking/wireguard.nix66
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/wireguard/namespaces.nix80
3 files changed, 136 insertions, 11 deletions
diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix
index 4176da2c8cb8f..980961225c9e0 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 <option>interfaceNamespace</option>. When
+        <literal>null</literal>, the interface is created in the init namespace.
+        See <link
+        xlink:href="https://www.wireguard.com/netns/">documentation</link>.
+        '';
+      };
+
+      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 <literal>init</literal> means
+        the init namespace. When <literal>null</literal>, the interface is not
+        moved.
+        See <link
+        xlink:href="https://www.wireguard.com/netns/">documentation</link>.
+        '';
+      };
     };
 
   };
@@ -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 04323317a9946..dc7225456b6a2 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 0000000000000..94f993d9475dd
--- /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");
+  '';
+})