diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 247039b848d0..d3739ae0960f 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 000000000000..e11f21e60fc6
--- /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 are
+ supplied, they will be combined, with values from
+ taking precedence.
+
+ You can use the command nix-shell -p yggdrasil --run
+ "yggdrasil -genconf -json"
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
+ . If both
+ and are
+ supplied, they will be combined, with values from
+ taking precedence.
+
+ You can use the command nix-shell -p yggdrasil --run
+ "yggdrasil -genconf"
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 LinkLocalTCPPort
in your
+ yggdrasil configuration ( or
+ ) to a port number other than 0,
+ and then add that port to
+ .
+ '';
+ };
+
+ 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 ea1490ad13a9..10564e063c69 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 000000000000..ddff35cce3a1
--- /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}]");
+
+ '';
+})