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}]"); + + ''; +})