nixos/yggdrasil: Fix test and replace activationScript by systemd service

See https://github.com/NixOS/nixpkgs/issues/475305
This commit is contained in:
r-vdp
2026-01-27 14:56:13 +01:00
parent baea0b2ddc
commit ad52471850
2 changed files with 93 additions and 79 deletions
@@ -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 = [
+92 -75
View File
@@ -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"
'';
}