Add yggdrasil clanService

This commit is contained in:
pinpox
2025-09-01 22:31:09 +02:00
parent d6c891750d
commit 8e279a1e71
24 changed files with 350 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
This module sets up [yggdrasil](https://yggdrasil-network.github.io/) across
your clan.
Yggdrasil is designed to be a future-proof and decentralised alternative to
the structured routing protocols commonly used today on the internet. Inside
your clan, it will allow you to reach all of your machines.
## Example Usage
While you can specify statically configured peers for each host, yggdrasil does
auto-discovery of local peers.
```nix
inventory = {
machines = {
peer1 = { };
peer2 = { };
};
instances = {
yggdrasil = {
# Deploy on all machines
roles.default.tags.all = { };
# Or individual hosts
roles.default.machines.peer1 = { };
roles.default.machines.peer2 = { };
};
};
};
```

View File

@@ -0,0 +1,125 @@
{ ... }:
{
_class = "clan.service";
manifest.name = "clan-core/yggdrasil";
manifest.description = "Yggdrasil encrypted IPv6 routing overlay network";
roles.default = {
interface =
{ lib, ... }:
{
options.extraMulticastInterfaces = lib.mkOption {
type = lib.types.listOf lib.types.attrs;
default = [ ];
description = ''
Additional interfaces to use for Multicast. See
https://yggdrasil-network.github.io/configurationref.html#multicastinterfaces
for reference.
'';
example = [
{
Regex = "(wg).*";
Beacon = true;
Listen = true;
Port = 5400;
Priority = 1020;
}
];
};
options.peers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Static peers to configure for this host.
If not set, local peers will be auto-discovered
'';
example = [
"tcp://192.168.1.1:6443"
"quic://192.168.1.1:6443"
"tls://192.168.1.1:6443"
"ws://192.168.1.1:6443"
];
};
};
perInstance =
{ settings, ... }:
{
nixosModule =
{
config,
pkgs,
...
}:
{
clan.core.vars.generators.yggdrasil = {
files.privateKey = { };
files.publicKey.secret = false;
files.address.secret = false;
runtimeInputs = with pkgs; [
yggdrasil
jq
openssl
];
script = ''
# Generate private key
openssl genpkey -algorithm Ed25519 -out $out/privateKey
# Generate corresponding public key
openssl pkey -in $out/privateKey -pubout -out $out/publicKey
# Derive IPv6 address from key
echo "{\"PrivateKeyPath\": \"$out/privateKey\"}" | yggdrasil -useconf -address | tr -d '\n' > $out/address
'';
};
systemd.services.yggdrasil.serviceConfig.BindReadOnlyPaths = [
"%d/key:/key"
];
systemd.services.yggdrasil.serviceConfig.LoadCredential =
"key:${config.clan.core.vars.generators.yggdrasil.files.privateKey.path}";
services.yggdrasil = {
enable = true;
openMulticastPort = true;
# We don't need this option, because we persist our keys with
# vars by ourselfs. This option creates an unnessesary additional
# systemd service to save/load the keys and should be removed
# from the NixOS module entirely, as it can be replaced by the
# (at the time of writing undocumented) PrivateKeyPath= setting.
# See https://github.com/NixOS/nixpkgs/pull/440910#issuecomment-3301835895 for details.
persistentKeys = false;
settings = {
PrivateKeyPath = "/key";
IfName = "ygg";
Peers = settings.peers;
MulticastInterfaces = [
# Ethernet is preferred over WIFI
{
Regex = "(eth|en).*";
Beacon = true;
Listen = true;
Port = 5400;
Priority = 1024;
}
{
Regex = "(wl).*";
Beacon = true;
Listen = true;
Port = 5400;
Priority = 1025;
}
]
++ settings.extraMulticastInterfaces;
};
};
networking.firewall.allowedTCPPorts = [ 5400 ];
};
};
};
}

View File

@@ -0,0 +1,24 @@
{
self,
lib,
...
}:
let
module = lib.modules.importApply ./default.nix {
inherit (self) packages;
};
in
{
clan.modules = {
yggdrasil = module;
};
perSystem =
{ ... }:
{
clan.nixosTests.yggdrasil = {
imports = [ ./tests/vm/default.nix ];
clan.modules.yggdrasil = module;
};
};
}

View File

@@ -0,0 +1,67 @@
{
name = "yggdrasil";
clan = {
test.useContainers = false;
directory = ./.;
inventory = {
machines.peer1 = { };
machines.peer2 = { };
instances."yggdrasil" = {
module.name = "yggdrasil";
module.input = "self";
# Assign the roles to the two machines
roles.default.machines.peer1 = { };
roles.default.machines.peer2 = { };
};
};
};
testScript = ''
start_all()
# Wait for both machines to be ready
peer1.wait_for_unit("multi-user.target")
peer2.wait_for_unit("multi-user.target")
# Check that yggdrasil service is running on both machines
peer1.wait_for_unit("yggdrasil")
peer2.wait_for_unit("yggdrasil")
peer1.succeed("systemctl is-active yggdrasil")
peer2.succeed("systemctl is-active yggdrasil")
# Check that both machines have yggdrasil network interfaces
peer1.wait_until_succeeds("ip link show | grep -E 'ygg'", 30)
peer2.wait_until_succeeds("ip link show | grep -E 'ygg'", 30)
# Get yggdrasil IPv6 addresses from both machines
peer1_ygg_ip = peer1.succeed("yggdrasilctl -json getself | jq -r '.address'").strip()
peer2_ygg_ip = peer2.succeed("yggdrasilctl -json getself | jq -r '.address'").strip()
# Compare runtime addresses with saved addresses from vars
expected_peer1_ip = "${builtins.readFile ./vars/per-machine/peer1/yggdrasil/address/value}"
expected_peer2_ip = "${builtins.readFile ./vars/per-machine/peer2/yggdrasil/address/value}"
print(f"peer1 yggdrasil IP: {peer1_ygg_ip}")
print(f"peer2 yggdrasil IP: {peer2_ygg_ip}")
print(f"peer1 expected IP: {expected_peer1_ip}")
print(f"peer2 expected IP: {expected_peer2_ip}")
# Verify that runtime addresses match expected addresses
assert peer1_ygg_ip == expected_peer1_ip, f"peer1 runtime IP {peer1_ygg_ip} != expected IP {expected_peer1_ip}"
assert peer2_ygg_ip == expected_peer2_ip, f"peer2 runtime IP {peer2_ygg_ip} != expected IP {expected_peer2_ip}"
# Wait a bit for the yggdrasil network to establish connectivity
import time
time.sleep(10)
# Test connectivity: peer1 should be able to ping peer2 via yggdrasil
peer1.succeed(f"ping -6 -c 3 {peer2_ygg_ip}")
# Test connectivity: peer2 should be able to ping peer1 via yggdrasil
peer2.succeed(f"ping -6 -c 3 {peer1_ygg_ip}")
'';
}

View File

@@ -0,0 +1,6 @@
[
{
"publickey": "age1p8trv2dmpanl3gnzj294c4t5uysu7d6rfjncp5lmn6redyda8fns6p7kca",
"type": "age"
}
]

View File

@@ -0,0 +1,6 @@
[
{
"publickey": "age107mprppm3r9u7f26e6t5mhtdny0h5ugfmfjy8kac2tw9nrh9a3ksex0xca",
"type": "age"
}
]

View File

@@ -0,0 +1,15 @@
{
"data": "ENC[AES256_GCM,data:jDEog7FFXl28Le3rh5VTiY0DFmLhIy2ZccFjuYWx+OQrKNEqTLI1fzaeMWIcgu6ln6wfGUk640d3IhmrF45MVZiJGkpkOU8UFx0=,iv:4oGaoxhFQwr9OQfdLL7y1N/gJo/uGkTPG/xicVprIAQ=,tag:Smu0/P2bQB66w+0J2Bjlxw==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpQ2hib2Mrb0plWUVwNWU5\nWmpxNlduaUVJckhuQlhQbUJpanloWGFLelJ3CjJJMlBGbGRTWEhGUHh2VVkzUzNa\nL3FGVkF3R3JJT051UTg4UlkwOHRNanMKLS0tIDVWcHU4NmFMUWp3STFTYmg5YmNp\nVzd1Uzg2Wkp5QnJ3V1Qyb2lwSXdBRDgK/V5lgw2TePhUC9ngW53ZapIMkcwPvJus\ns0jUYkStHXjsvEiN7BG8cG7/vRbLD8CnKXnmieM20mT6o7GHGfhHMg==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-09-17T07:12:51Z",
"mac": "ENC[AES256_GCM,data:gy/1NFmpFz/tdhgU/Vr+xg46DUjy9ZbrAtCBnIxclwZLJ/fneBpblv8TFgdysY4Ay6jp1S/TOc8eyr+KLHMqcBlje09wd1ac/Y3ee6GccXitB+/c5ayuXX/ShVCdicsr/9COw7vfndAQPU8XIz6tdy0dbL7jgVTyViZW/P5CXEU=,iv:BQ/INwTTCshl5BVnJbVzHW8rwafERS6bKh2JAJsMv9s=,tag:QhsbjeEBivbl8fQLHjiKtQ==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../users/admin

View File

@@ -0,0 +1,15 @@
{
"data": "ENC[AES256_GCM,data:1rVgSwg2qPHuXUOQCgOunaNYiBbsh99dZ2y0BV4TxzACwdb3lb6/XnLeDenLELOpKruZQoNJax/NziRr+VHzmh/TlQhNgTkS71A=,iv:Wi5/cFOETb1rhAYeyzkpppzSSm+S+8cCQYc7zkp74FY=,tag:JQHFZJwYMQH4jUqSw6Ld8Q==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGV3ZLZmNrejlvTFF6NDZW\nYmljS0VXQUtCR3IzMG9tMksvSllVVkIxVTEwCi9Fd1dBbmFlYmF2cE1raVJoS3RR\nWmxQY3RwanRZUE5aN1Q2SzhJOFU1elEKLS0tIG9RMElDMEo3TFJjU0RvU3FMQk12\nT1pNc1VjeUliejk3YmJ6d29zUU15aDQKuZ62Q/ywLrpyu1jB34OCPKQEDd150qH6\nHzyw+MasUlzKNs0ZrALwfhnCKiNb/Pq0Lu660Dx5/sFxI/TAqC7NGg==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-09-17T07:13:24Z",
"mac": "ENC[AES256_GCM,data:N7mmiEZxinOgWdd7QcZBAumnWaApjlQVww4EzAQ1/JH5i8r8CIfPh/7lGMQntlJj5ob+UgrS96nl6XKdvs3Bt7z34zPq7KV3c0mSmclEctRfcZiG4F+rZ0QIMIRJjq7xJL/M9WupSn8Lgms7qHJMdJyHdDkw47bmXz3MIw9c9zo=,iv:ZYPoo5jTIGnZ1HcAWlr26gloVhSjfhwbO/xH5YCbgF0=,tag:UKMVMGEfqyfo04cIkuKD0A==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../users/admin

View File

@@ -0,0 +1,4 @@
{
"publickey": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"type": "age"
}

View File

@@ -0,0 +1 @@
200:fa3b:ad0e:6821:9a51:3ad5:62a4:9ab1

View File

@@ -0,0 +1 @@
../../../../../../sops/machines/peer1

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:oLWxmCqCLyKCdN90eScYC4eMsY5mcpXexk03P+KfQX9hCUg7iOSv+BTujvAFuJA0z2Pgg7B/L0noYSImE3208gDC2exdZme+szpNwxD6oh1bBx5XuYGc/TDzEl7IdPrf1piuqCkWgD6wNb9aFVA9NB/CrNaszY8=,iv:pU8OGFwtqkxy0+iyhWaOnWdSVvaxYBUgnQwv89CTfKc=,tag:V/GVEvjEpeaf3Tl3XCVL7w==,type:str]",
"sops": {
"age": [
{
"recipient": "age1p8trv2dmpanl3gnzj294c4t5uysu7d6rfjncp5lmn6redyda8fns6p7kca",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBidXZxMVg2bkFCekFza3FV\naGVJTjRSSnQrVkgyTVU4Vk9YZFR2Nk1Xcnl3ClNTZnJsR2xZc3hGRFA0NGp2L2Uy\nU2w0N1ZON0xQRXRKL0NId1J3dWNSeXcKLS0tIFlKNk5walJrM2FGM0ozYTBUNVN1\nNHd0N3g2VWFjTUZtbHExdmsvSjJQUGMKDHgRMyTf4CuoIRdsfl6TmPLgcjxHdR4N\nzNmnSmT6QXJr4gBQ7e/3zpMdq4sKzyAOjJPkQra8nJ/KvhpFwXkdtg==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1L3hBYnNTdEhTK1M0TEUx\nM1BFb3IwWDZPc2tkN0JUNXRPWkhEWHpXaFFNCkFlZ3Ixb1hhVWMvUmpKdCtjbW5h\nTVpLSkdxUXI4SVE3MW5LZkFVSmM3dDgKLS0tICtKWGYzbFQ1VGQydktHSDVadVQy\nM3JTSGc0NWVHWGl4akV2VUNNVjVvT00KKmbkGJ2KclTBb7NI0MamWZnlWMaXscws\nYO261RHc/j4s3KdbWATklh2KTicKpgta0mh294Y+hKYLGgBpnXjtOw==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-09-17T07:12:51Z",
"mac": "ENC[AES256_GCM,data:YG4k+PvsF0NVfjqKY/MBhCOwn9Kcn5orq8aD41X3NEPBT+wU5RlN0XqXhH2nkZ52ZHdCN84Ulbd4guwBcJcUtGvxqOpfE4SJ9RFE+SjYw+2+S9wlArQx5R5Zd+Sr+Rgtaip3moENbxETxHZazbEK3u5o0aXKrZvoXH2xgPRYUOg=,iv:lDnNELLpqykrsmoORMW7p6gF43C2QvPwf0PZep2pEo8=,tag:CZNOBx3Aeoq5WeDbANqFVw==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../../../../sops/users/admin

View File

@@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAguIpeMvvMtdilU6tsqdBClci2YUgBKEUPQ5ID55zOVY=
-----END PUBLIC KEY-----

View File

@@ -0,0 +1 @@
200:3975:b69f:fd15:f39a:bf24:f644:432b

View File

@@ -0,0 +1 @@
../../../../../../sops/machines/peer2

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:Ik+mzD41SwfGZrqGi5oy0u8ZS8qCKgV34aohIt7J/11S3ayhKKN8451cYPltGg+2i/+2DWRSkqYZsctgIahvthnvb30r91XHRUCtE3oT8iPOKwEuW2xnZBL2dK0qHrKFGn8B/o3Bl/+4cWuR5Jt9QHlN4oF3o50=,iv:a9mSIr7TT9im19P5XKiDGL71hMLfQE8IB6NV8WABZ+s=,tag:dVQ0HZdpbkcmFA37Il+VIw==,type:str]",
"sops": {
"age": [
{
"recipient": "age107mprppm3r9u7f26e6t5mhtdny0h5ugfmfjy8kac2tw9nrh9a3ksex0xca",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3WEtncHIrd2YrbHduUm4y\nYkh1U0NERDR0MzRNQXZ0elNMZzl0ZmJPMlVVCnJ1UC9uNnk0dC9JVzdReUxLZmdR\nMGIwSjBKUW8yQmQ1R3dqU29ZM2czMEUKLS0tIFpMM3l6YnVFRUtLOFRvSTBXRXU0\nQUJWNk5obzBINS90dWwybXlXdEZFZjgKrZtViJwgPpKT+Bhx6ymEhf6QmnMmZ6Jf\n3kIxzDWxGbxrDTrrdXm33gYs9bRvtXsTACRT3TRAKIwSjIV/ycCUBA==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArY1A4blFZNjdTU3lGUFA3\nb1hscVhLVmtuQnJMS3VHZVVEOXBDRktXNVhjCkdLZEFFZXhubFJrZTU3RjRDN1lh\nVzgyd3E3a0NGTGh1N2lPQWZBVG9BaW8KLS0tIFN1eEhpMk5mUkNaL29MS2NlWDdP\nVXFtb3c0Y25MT3NLK0N2RStkY1R0MGsKLv8yOSl2iQx39JRJA/e0dOtIz7ZND0+o\nzmqxapD6pxbj8RGswj0szLrxaveWT6Vkmmy/a4fz35uw3xuF7uiJJA==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-09-17T07:13:24Z",
"mac": "ENC[AES256_GCM,data:KCBHfavyg1U/Z1e3rr0jdHivUa5/rl42N/K6c0BR47srdYqcpAS+SWW6tH7kPI3EO+P7TgXVoGlmcXaKgrlRpe37eNzdAT6PJYm7rhe6rm2UgZ1yw/NFqA11p/i3dDbTzYHaHMNGGGg9t/BdT1tD0zCICfptGQnSSBx/BEmT3ak=,iv:GOk8/z1mLIVKxFX4ayt9HvA90gv0Af1KqcDbdJRUYu4=,tag:hJpOBFRESQXmE7iMqDqO6g==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../../../../sops/users/admin

View File

@@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA40UksAF1BjKgbYTd3motmeXa/belhNOoRfaaPQTXy8E=
-----END PUBLIC KEY-----

View File

@@ -172,6 +172,7 @@ nav:
- reference/clanServices/users.md - reference/clanServices/users.md
- reference/clanServices/wifi.md - reference/clanServices/wifi.md
- reference/clanServices/wireguard.md - reference/clanServices/wireguard.md
- reference/clanServices/yggdrasil.md
- reference/clanServices/zerotier.md - reference/clanServices/zerotier.md
- Community: community/services/index.md - Community: community/services/index.md