diff --git a/nixos/modules/services/networking/yggdrasil.nix b/nixos/modules/services/networking/yggdrasil.nix index 5d2dc2b8cd41..d6331eeb3326 100644 --- a/nixos/modules/services/networking/yggdrasil.nix +++ b/nixos/modules/services/networking/yggdrasil.nix @@ -49,10 +49,7 @@ let cleanSettings = lib.filterAttrs (n: v: v != null) baseSettings; # Generate configuration file from user settings - configFile = pkgs.writeTextFile { - name = "yggdrasil.conf"; - text = builtins.toJSON cleanSettings; - }; + configFile = pkgs.writers.writeJSON "yggdrasil.conf" cleanSettings; in { imports = [ diff --git a/nixos/tests/yggdrasil.nix b/nixos/tests/yggdrasil.nix index 01a9c4cf1d8a..5b0762c7f08f 100644 --- a/nixos/tests/yggdrasil.nix +++ b/nixos/tests/yggdrasil.nix @@ -1,8 +1,20 @@ let + privateKeys = { + alice = '' + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIKhn+eB45M5Y0xDPWs1GItdZ4qId8H4db8OAoqJkiUgN + -----END PRIVATE KEY----- + ''; + bob = '' + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIAxKJKzTQCcizpJ37RefSgS4lbSVhkk8Jfuu1gZT2FfW + -----END PRIVATE KEY----- + ''; + }; + aliceIp6 = "202:b70:9b0b:cf34:f93c:8f18:bbfd:7034"; aliceKeys = { PublicKey = "3e91ec9e861960d86e1ce88051f97c435bdf2859640ab681dfa906eb45ad5182"; - PrivateKey = "a867f9e078e4ce58d310cf5acd4622d759e2a21df07e1d6fc380a2a26489480d3e91ec9e861960d86e1ce88051f97c435bdf2859640ab681dfa906eb45ad5182"; }; # Frank has a legacy keys.json that should be migrated # This is the same key as Alice but in the old hex format @@ -11,7 +23,7 @@ let # The old format stored PrivateKey as 128 hex chars (64 bytes = seed + pubkey) # This corresponds to Alice's key PrivateKey = - "a867f9e078e4ce58d310cf5acd4622d759e2a21df07e1d6fc380a2a264894809" + aliceKeys.PublicKey; + "a867f9e078e4ce58d310cf5acd4622d759e2a21df07e1d6fc380a2a26489480d" + aliceKeys.PublicKey; PublicKey = aliceKeys.PublicKey; }; bobIp6 = "202:a483:73a4:9f2d:a559:4a19:bc9:8458"; @@ -30,7 +42,6 @@ let } ]; PublicKey = "2b6f918b6c1a4b54d6bcde86cf74e074fb32ead4ee439b7930df2aa60c825186"; - PrivateKey = "0c4a24acd3402722ce9277ed179f4a04b895b49586493c25fbaed60653d857d62b6f918b6c1a4b54d6bcde86cf74e074fb32ead4ee439b7930df2aa60c825186"; }; danIp6 = bobPrefix + "::2"; @@ -69,13 +80,9 @@ in settings = { Listen = [ "tcp://0.0.0.0:12345" ]; MulticastInterfaces = [ ]; - }; - configFile = toString ( - pkgs.writeTextFile { - name = "yggdrasil-alice-conf"; - text = builtins.toJSON aliceKeys; - } - ); + PrivateKeyPath = "${pkgs.writeText "private" (privateKeys.alice)}"; + } + // aliceKeys; }; }; @@ -88,12 +95,9 @@ in services.yggdrasil = { enable = true; openMulticastPort = true; - configFile = toString ( - pkgs.writeTextFile { - name = "yggdrasil-bob-conf"; - text = builtins.toJSON bobConfig; - } - ); + settings = bobConfig // { + PrivateKeyPath = pkgs.writeText "private" (privateKeys.bob); + }; }; boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1; @@ -158,8 +162,8 @@ in Port = 43210; } ]; - openMulticastPort = true; }; + openMulticastPort = true; persistentKeys = true; }; }; @@ -194,13 +198,23 @@ in networking.firewall.allowedTCPPorts = [ 43212 ]; # Pre-populate the legacy keys.json file before the service starts - system.activationScripts.yggdrasil-legacy-keys = '' - mkdir -p /var/lib/yggdrasil - cat > /var/lib/yggdrasil/keys.json << 'EOF' - ${builtins.toJSON frankLegacyKeys} - EOF - chmod 600 /var/lib/yggdrasil/keys.json - ''; + systemd.services.yggdrasil-legacy-keys = { + wantedBy = [ "yggdrasil-persistent-keys.service" ]; + before = [ "yggdrasil-persistent-keys.service" ]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + + script = '' + mkdir -p /var/lib/yggdrasil + cat > /var/lib/yggdrasil/keys.json << 'EOF' + ${builtins.toJSON frankLegacyKeys} + EOF + chmod 600 /var/lib/yggdrasil/keys.json + ''; + }; services.yggdrasil = { enable = true; @@ -221,68 +235,71 @@ in }; }; - testScript = '' - import re + testScript = + # python + '' + import re - # Give Alice a head start so she is ready when Bob calls. - alice.start() - alice.wait_for_unit("yggdrasil.service") + # Give Alice a head start so she is ready when Bob calls. + alice.start() + alice.wait_for_unit("yggdrasil.service") - bob.start() - carol.start() - eve.start() - frank.start() - bob.wait_for_unit("default.target") - carol.wait_for_unit("yggdrasil.service") + bob.start() + carol.start() + eve.start() + bob.wait_for_unit("default.target") + carol.wait_for_unit("yggdrasil.service") - # Eve uses persistentKeys - verify the key generation service ran - eve.wait_for_unit("yggdrasil-persistent-keys.service") - eve.wait_for_unit("yggdrasil.service") - eve.succeed("test -f /var/lib/yggdrasil/private.pem") - eve.succeed("grep -q 'BEGIN PRIVATE KEY' /var/lib/yggdrasil/private.pem") + # Eve uses persistentKeys - verify the key generation service ran + eve.wait_for_unit("yggdrasil-persistent-keys.service") + eve.wait_for_unit("yggdrasil.service") + eve.succeed("test -f /var/lib/yggdrasil/private.pem") + eve.succeed("grep -q 'BEGIN PRIVATE KEY' /var/lib/yggdrasil/private.pem") - # Frank tests migration from legacy keys.json format - frank.wait_for_unit("yggdrasil-persistent-keys.service") - frank.wait_for_unit("yggdrasil.service") - # Verify migration happened: private.pem should exist - frank.succeed("test -f /var/lib/yggdrasil/private.pem") - frank.succeed("grep -q 'BEGIN PRIVATE KEY' /var/lib/yggdrasil/private.pem") - # Legacy file should still exist (not deleted, user should verify and remove) - frank.succeed("test -f /var/lib/yggdrasil/keys.json") + ip_addr_show = "ip -o -6 addr show dev ygg0 scope global" + carol.wait_until_succeeds(f"[ `{ip_addr_show} | grep -v tentative | wc -l` -ge 1 ]") + carol_ip6 = re.split(" +|/", carol.succeed(ip_addr_show))[3] - ip_addr_show = "ip -o -6 addr show dev ygg0 scope global" - carol.wait_until_succeeds(f"[ `{ip_addr_show} | grep -v tentative | wc -l` -ge 1 ]") - carol_ip6 = re.split(" +|/", carol.succeed(ip_addr_show))[3] + eve.wait_until_succeeds(f"[ `{ip_addr_show} | grep -v tentative | wc -l` -ge 1 ]") + eve_ip6 = re.split(" +|/", eve.succeed(ip_addr_show))[3] - eve.wait_until_succeeds(f"[ `{ip_addr_show} | grep -v tentative | wc -l` -ge 1 ]") - eve_ip6 = re.split(" +|/", eve.succeed(ip_addr_show))[3] + # If Alice can talk to Carol, then Bob's outbound peering and Carol's + # local peering have succeeded and everybody is connected. + alice.wait_until_succeeds(f"ping -c 1 {carol_ip6}") + alice.succeed("ping -c 1 ${bobIp6}") - # Verify Frank got the expected IP after migration (same key as Alice = same IP) - frank.wait_until_succeeds(f"[ `{ip_addr_show} | grep -v tentative | wc -l` -ge 1 ]") - frank_ip6 = re.split(" +|/", frank.succeed(ip_addr_show))[3] - assert frank_ip6 == "${frankIp6}", f"Frank's IP {frank_ip6} doesn't match expected ${frankIp6} after migration" + bob.succeed("ping -c 1 ${aliceIp6}") + bob.succeed(f"ping -c 1 {carol_ip6}") - # If Alice can talk to Carol, then Bob's outbound peering and Carol's - # local peering have succeeded and everybody is connected. - alice.wait_until_succeeds(f"ping -c 1 {carol_ip6}") - alice.succeed("ping -c 1 ${bobIp6}") + carol.succeed("ping -c 1 ${aliceIp6}") + carol.succeed("ping -c 1 ${bobIp6}") + carol.succeed("ping -c 1 ${bobPrefix}::1") + carol.succeed("ping -c 8 ${danIp6}") - bob.succeed("ping -c 1 ${aliceIp6}") - bob.succeed(f"ping -c 1 {carol_ip6}") + carol.fail("journalctl -u dhcpcd | grep ygg0") - carol.succeed("ping -c 1 ${aliceIp6}") - carol.succeed("ping -c 1 ${bobIp6}") - carol.succeed("ping -c 1 ${bobPrefix}::1") - carol.succeed("ping -c 8 ${danIp6}") + # Eve should be able to communicate with the network via multicast peering + eve.wait_until_succeeds(f"ping -c 1 {carol_ip6}") + carol.wait_until_succeeds(f"ping -c 1 {eve_ip6}") - carol.fail("journalctl -u dhcpcd | grep ygg0") + alice.wait_for_unit("httpd.service") + carol.succeed("curl --fail -g http://[${aliceIp6}]") + carol.succeed("curl --fail -g http://[${danIp6}]") - # Eve should be able to communicate with the network via multicast peering - eve.wait_until_succeeds(f"ping -c 1 {carol_ip6}") - carol.wait_until_succeeds(f"ping -c 1 {eve_ip6}") + with subtest("legacy key migration"): + frank.start() + # Frank tests migration from legacy keys.json format + frank.wait_for_unit("yggdrasil-persistent-keys.service") + frank.wait_for_unit("yggdrasil.service") + # Verify migration happened: private.pem should exist + frank.succeed("test -f /var/lib/yggdrasil/private.pem") + frank.succeed("grep -q 'BEGIN PRIVATE KEY' /var/lib/yggdrasil/private.pem") + # Legacy file should still exist (not deleted, user should verify and remove) + frank.succeed("test -f /var/lib/yggdrasil/keys.json") - alice.wait_for_unit("httpd.service") - carol.succeed("curl --fail -g http://[${aliceIp6}]") - carol.succeed("curl --fail -g http://[${danIp6}]") - ''; + # Verify Frank got the expected IP after migration (same key as Alice = same IP) + frank.wait_until_succeeds(f"[ `{ip_addr_show} | grep -v tentative | wc -l` -ge 1 ]") + frank_ip6 = re.split(" +|/", frank.succeed(ip_addr_show))[3] + assert frank_ip6 == "${frankIp6}", f"Frank's IP {frank_ip6} doesn't match expected ${frankIp6} after migration" + ''; }