summary refs log tree commit diff
diff options
context:
space:
mode:
authorGemini Lasswell <gazally@runbox.com>2019-06-27 08:56:10 -0700
committerEmery Hemingway <ehmry@posteo.net>2019-10-26 13:51:31 +0200
commitb8cb8c39d6aa8a8e9ec9a95f95dd480478b60f4a (patch)
tree486b6c9c1ece5156447495000ed2cb6f011525f7
parent3cc9bcb3386cec3a29af58cf361da2dc2fad7164 (diff)
downloadnixpkgs-b8cb8c39d6aa8a8e9ec9a95f95dd480478b60f4a.tar
nixpkgs-b8cb8c39d6aa8a8e9ec9a95f95dd480478b60f4a.tar.gz
nixpkgs-b8cb8c39d6aa8a8e9ec9a95f95dd480478b60f4a.tar.bz2
nixpkgs-b8cb8c39d6aa8a8e9ec9a95f95dd480478b60f4a.tar.lz
nixpkgs-b8cb8c39d6aa8a8e9ec9a95f95dd480478b60f4a.tar.xz
nixpkgs-b8cb8c39d6aa8a8e9ec9a95f95dd480478b60f4a.tar.zst
nixpkgs-b8cb8c39d6aa8a8e9ec9a95f95dd480478b60f4a.zip
nixos/yggdrasil: add service
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/services/networking/yggdrasil.nix181
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/yggdrasil.nix123
4 files changed, 306 insertions, 0 deletions
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 247039b848d..d3739ae0960 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -730,6 +730,7 @@
   ./services/networking/xinetd.nix
   ./services/networking/xl2tpd.nix
   ./services/networking/xrdp.nix
+  ./services/networking/yggdrasil.nix
   ./services/networking/zerobin.nix
   ./services/networking/zeronet.nix
   ./services/networking/zerotierone.nix
diff --git a/nixos/modules/services/networking/yggdrasil.nix b/nixos/modules/services/networking/yggdrasil.nix
new file mode 100644
index 00000000000..e11f21e60fc
--- /dev/null
+++ b/nixos/modules/services/networking/yggdrasil.nix
@@ -0,0 +1,181 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.yggdrasil;
+  configProvided = (cfg.config != {});
+  configAsFile = (if configProvided then
+                   toString (pkgs.writeTextFile {
+                     name = "yggdrasil-conf";
+                     text = builtins.toJSON cfg.config;
+                   })
+                   else null);
+  configFileProvided = (cfg.configFile != null);
+  generateConfig = (
+    if configProvided && configFileProvided then
+      "${pkgs.jq}/bin/jq -s add /run/yggdrasil/configFile.json ${configAsFile}"
+    else if configProvided then
+      "cat ${configAsFile}"
+    else if configFileProvided then
+      "cat /run/yggdrasil/configFile.json"
+    else
+      "${cfg.package}/bin/yggdrasil -genconf"
+  );
+
+in {
+  options = with types; {
+    services.yggdrasil = {
+      enable = mkEnableOption "the yggdrasil system service";
+
+      configFile = mkOption {
+        type =  nullOr str;
+        default = null;
+        example = "/run/keys/yggdrasil.conf";
+        description = ''
+          A file which contains JSON configuration for yggdrasil.
+
+          You do not have to supply a complete configuration, as
+          yggdrasil will use default values for anything which is
+          omitted.  If the encryption and signing keys are omitted,
+          yggdrasil will generate new ones each time the service is
+          started, resulting in a random IPv6 address on the yggdrasil
+          network each time.
+
+          If both this option and <option>config</option> are
+          supplied, they will be combined, with values from
+          <option>config</option> taking precedence.
+
+          You can use the command <code>nix-shell -p yggdrasil --run
+          "yggdrasil -genconf -json"</code> to generate a default
+          JSON configuration.
+        '';
+      };
+
+      config = mkOption {
+        type = attrs;
+        default = {};
+        example = {
+          Peers = [
+            "tcp://aa.bb.cc.dd:eeeee"
+            "tcp://[aaaa:bbbb:cccc:dddd::eeee]:fffff"
+          ];
+          Listen = [
+            "tcp://0.0.0.0:xxxxx"
+          ];
+        };
+        description = ''
+          Configuration for yggdrasil, as a Nix attribute set.
+
+          Warning: this is stored in the WORLD-READABLE Nix store!
+          Therefore, it is not appropriate for private keys.  If you
+          do not specify the keys, yggdrasil will generate a new set
+          each time the service is started, creating a random IPv6
+          address on the yggdrasil network each time.
+
+          If you wish to specify the keys, use
+          <option>configFile</option>.  If both
+          <option>configFile</option> and <option>config</option> are
+          supplied, they will be combined, with values from
+          <option>config</option> taking precedence.
+
+          You can use the command <code>nix-shell -p yggdrasil --run
+          "yggdrasil -genconf"</code> to generate default
+          configuration values with documentation.
+        '';
+      };
+
+      openMulticastPort = mkOption {
+        type = bool;
+        default = false;
+        description = ''
+          Whether to open the UDP port used for multicast peer
+          discovery. The NixOS firewall blocks link-local
+          communication, so in order to make local peering work you
+          will also need to set <code>LinkLocalTCPPort</code> in your
+          yggdrasil configuration (<option>config</option> or
+          <option>configFile</option>) to a port number other than 0,
+          and then add that port to
+          <option>networking.firewall.allowedTCPPorts</option>.
+        '';
+      };
+
+      denyDhcpcdInterfaces = mkOption {
+        type = listOf str;
+        default = [];
+        example = [ "tap*" ];
+        description = ''
+          Disable the DHCP client for any interface whose name matches
+          any of the shell glob patterns in this list.  Use this
+          option to prevent the DHCP client from broadcasting requests
+          on the yggdrasil network.  It is only necessary to do so
+          when yggdrasil is running in TAP mode, because TUN
+          interfaces do not support broadcasting.
+        '';
+      };
+
+      package = mkOption {
+        type = package;
+        default = pkgs.yggdrasil;
+        defaultText = "pkgs.yggdrasil";
+        description = "Yggdrasil package to use.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = config.networking.enableIPv6;
+        message = "networking.enableIPv6 must be true for yggdrasil to work";
+      }
+    ];
+
+    environment.etc."yggdrasil.conf" = {
+      enable = true;
+      mode = "symlink";
+      source = "/run/yggdrasil/yggdrasil.conf";
+    };
+
+    systemd.services.yggdrasil = {
+      description = "Yggdrasil Network Service";
+      path = [ cfg.package ] ++ optional (configProvided && configFileProvided) pkgs.jq;
+      bindsTo = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        ${generateConfig} | yggdrasil -normaliseconf -useconf > /run/yggdrasil/yggdrasil.conf
+      '';
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/yggdrasil -useconffile /etc/yggdrasil.conf";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        Restart = "always";
+
+        RuntimeDirectory = "yggdrasil";
+        RuntimeDirectoryMode = "0700";
+        BindReadOnlyPaths = mkIf configFileProvided
+          [ "${cfg.configFile}:/run/yggdrasil/configFile.json" ];
+
+        DynamicUser = true;
+        AmbientCapabilities = "CAP_NET_ADMIN";
+        CapabilityBoundingSet = "CAP_NET_ADMIN";
+        MemoryDenyWriteExecute = true;
+        ProtectControlGroups = true;
+        ProtectHome = "tmpfs";
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @resources";
+      };
+    };
+
+    networking.dhcpcd.denyInterfaces = cfg.denyDhcpcdInterfaces;
+    networking.firewall.allowedUDPPorts = mkIf cfg.openMulticastPort [ 9001 ];
+
+    # Make yggdrasilctl available on the command line.
+    environment.systemPackages = [ cfg.package ];
+  };
+  meta.maintainers = with lib.maintainers; [ gazally ];
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index ea1490ad13a..10564e063c6 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -293,5 +293,6 @@ in
   xrdp = handleTest ./xrdp.nix {};
   xss-lock = handleTest ./xss-lock.nix {};
   yabar = handleTest ./yabar.nix {};
+  yggdrasil = handleTest ./yggdrasil.nix {};
   zookeeper = handleTest ./zookeeper.nix {};
 }
diff --git a/nixos/tests/yggdrasil.nix b/nixos/tests/yggdrasil.nix
new file mode 100644
index 00000000000..ddff35cce3a
--- /dev/null
+++ b/nixos/tests/yggdrasil.nix
@@ -0,0 +1,123 @@
+let
+  aliceIp6 = "200:3b91:b2d8:e708:fbf3:f06:fdd5:90d0";
+  aliceKeys = {
+    EncryptionPublicKey = "13e23986fe76bc3966b42453f479bc563348b7ff76633b7efcb76e185ec7652f";
+    EncryptionPrivateKey = "9f86947b15e86f9badac095517a1982e39a2db37ca726357f95987b898d82208";
+    SigningPublicKey = "e2c43349083bc1e998e4ec4535b4c6a8f44ca9a5a8e07336561267253b2be5f4";
+    SigningPrivateKey = "fe3add8da35316c05f6d90d3ca79bd2801e6ccab6d37e5339fef4152589398abe2c43349083bc1e998e4ec4535b4c6a8f44ca9a5a8e07336561267253b2be5f4";
+  };
+  bobIp6 = "201:ebbd:bde9:f138:c302:4afa:1fb6:a19a";
+  bobConfig = {
+    InterfacePeers = {
+      eth1 = [ "tcp://192.168.1.200:12345" ];
+    };
+    MulticastInterfaces = [ "eth1" ];
+    LinkLocalTCPPort = 54321;
+    EncryptionPublicKey = "c99d6830111e12d1b004c52fe9e5a2eef0f6aefca167aca14589a370b7373279";
+    EncryptionPrivateKey = "2e698a53d3fdce5962d2ff37de0fe77742a5c8b56cd8259f5da6aa792f6e8ba3";
+    SigningPublicKey = "de111da0ec781e45bf6c63ecb45a78c24d7d4655abfaeea83b26c36eb5c0fd5b";
+    SigningPrivateKey = "2a6c21550f3fca0331df50668ffab66b6dce8237bcd5728e571e8033b363e247de111da0ec781e45bf6c63ecb45a78c24d7d4655abfaeea83b26c36eb5c0fd5b";
+  };
+
+in import ./make-test.nix ({ pkgs, ...} : {
+  name = "yggdrasil";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ gazally ];
+  };
+
+  nodes = rec {
+    # Alice is listening for peerings on a specified port,
+    # but has multicast peering disabled.  Alice has part of her
+    # yggdrasil config in Nix and part of it in a file.
+    alice =
+      { ... }:
+      {
+        networking = {
+          interfaces.eth1.ipv4.addresses = [{
+            address = "192.168.1.200";
+            prefixLength = 24;
+          }];
+          firewall.allowedTCPPorts = [ 80 12345 ];
+        };
+        services.httpd.enable = true;
+        services.httpd.adminAddr = "foo@example.org";
+
+        services.yggdrasil = {
+          enable = true;
+          config = {
+            Listen = ["tcp://0.0.0.0:12345"];
+            MulticastInterfaces = [ ];
+          };
+          configFile = toString (pkgs.writeTextFile {
+                         name = "yggdrasil-alice-conf";
+                         text = builtins.toJSON aliceKeys;
+                       });
+        };
+      };
+
+    # Bob is set up to peer with Alice, and also to do local multicast
+    # peering.  Bob's yggdrasil config is in a file.
+    bob =
+      { ... }:
+      {
+        networking.firewall.allowedTCPPorts = [ 54321 ];
+        services.yggdrasil = {
+          enable = true;
+          openMulticastPort = true;
+          configFile = toString (pkgs.writeTextFile {
+                         name = "yggdrasil-bob-conf";
+                         text = builtins.toJSON bobConfig;
+                       });
+        };
+      };
+
+    # Carol only does local peering.  Carol's yggdrasil config is all Nix.
+    carol =
+      { ... }:
+      {
+        networking.firewall.allowedTCPPorts = [ 43210 ];
+        services.yggdrasil = {
+          enable = true;
+          denyDhcpcdInterfaces = [ "ygg0" ];
+          config = {
+            IfTAPMode = true;
+            IFName = "ygg0";
+            MulticastInterfaces = [ "eth1" ];
+            LinkLocalTCPPort = 43210;
+          };
+        };
+      };
+    };
+
+  testScript =
+    ''
+      # Give Alice a head start so she is ready when Bob calls.
+      $alice->start;
+      $alice->waitForUnit("yggdrasil.service");
+
+      $bob->start;
+      $carol->start;
+      $bob->waitForUnit("yggdrasil.service");
+      $carol->waitForUnit("yggdrasil.service");
+
+      $carol->waitUntilSucceeds("[ `ip -o -6 addr show dev ygg0 scope global | grep -v tentative | wc -l` -ge 1 ]");
+      my $carolIp6 = (split /[ \/]+/, $carol->succeed("ip -o -6 addr show dev ygg0 scope global"))[3];
+
+      # If Alice can talk to Carol, then Bob's outbound peering and Carol's
+      # local peering have succeeded and everybody is connected.
+      $alice->waitUntilSucceeds("ping -c 1 $carolIp6");
+      $alice->succeed("ping -c 1 ${bobIp6}");
+
+      $bob->succeed("ping -c 1 ${aliceIp6}");
+      $bob->succeed("ping -c 1 $carolIp6");
+
+      $carol->succeed("ping -c 1 ${aliceIp6}");
+      $carol->succeed("ping -c 1 ${bobIp6}");
+
+      $carol->fail("journalctl -u dhcpcd | grep ygg0");
+
+      $alice->waitForUnit("httpd.service");
+      $carol->succeed("curl --fail -g http://[${aliceIp6}]");
+
+    '';
+})