{ ... }: { _class = "clan.service"; manifest.name = "clan-core/yggdrasil"; manifest.description = "Yggdrasil encrypted IPv6 routing overlay network"; manifest.readme = builtins.readFile ./README.md; roles.default = { description = "Placeholder role to apply the yggdrasil service"; 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.extraPeers = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; description = '' Additional static peers to configure for this host. If you use a VPN clan service, it will automatically be added as peers to other hosts. Local peers are also auto-discovered and don't need to be added. ''; 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, roles, exports, ... }: { nixosModule = { config, pkgs, lib, clan-core, ... }: let mkPeers = ip: [ # "tcp://${ip}:6443" "quic://${ip}:6443" "ws://${ip}:6443" "tls://${ip}:6443" ]; select' = clan-core.inputs.nix-select.lib.select; # TODO make it nicer @lassulus, @picnoir wants microlens # Get a list of all exported IPs from all VPN modules exportedPeerIPs = builtins.foldl' ( acc: e: if e == { } then acc else acc ++ (lib.flatten (builtins.filter (s: s != "") (lib.attrValues (select' "peers.*.plain" e)))) ) [ ] (lib.attrValues (select' "instances.*.networking.?peers.*.host.?plain" exports)); # Construct a list of peers in yggdrasil format exportedPeers = lib.flatten (map mkPeers exportedPeerIPs); in { # Set . for all hosts. # Networking modules will then add themselves as peers, so we can # always use this to resolve a host via the best possible route, # doing fail-over if needed. networking.extraHosts = lib.strings.concatStringsSep "\n" ( lib.filter (n: n != "") ( map ( name: let ipPath = "${config.clan.core.settings.directory}/vars/per-machine/${name}/yggdrasil/address/value"; in if builtins.pathExists ipPath then "${builtins.readFile ipPath} ${name}.${config.clan.core.settings.tld}" else "" ) (lib.attrNames roles.default.machines) ) ); 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 ourselves. This option creates an unnecesary 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 = lib.lists.unique (exportedPeers ++ settings.extraPeers); 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 ]; }; }; }; }