{ ... }: { _class = "clan.service"; manifest.name = "clan-core/zerotier"; manifest.description = "Configuration of the secure and efficient Zerotier VPN"; manifest.categories = [ "Utility" ]; manifest.readme = builtins.readFile ./README.md; roles.default = { interface = { lib, ... }: { options.networkId = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = '' The zerotier network id to use. If not set, a network will be generated for you. If you administrate your zerotier network via my.zerotier.com, you should set this. ''; example = "8056c2e21c000001"; }; }; perInstance = { instanceName, settings, ... }: { nixosModule = { config, pkgs, lib, ... }: { config = lib.mkMerge [ # code to start/configure zerotier ({ config, ... }: { services.zerotierone = { enable = true; joinNetworks = [ config.clan."zerotier_${instanceName}".networkId ]; }; systemd.network.networks."09-zerotier" = { matchConfig.Name = "zt*"; networkConfig = { LLDP = true; MulticastDNS = true; KeepConfiguration = "static"; }; }; systemd.services.zerotierone.serviceConfig.ExecStartPre = [ "+${pkgs.writeShellScript "init-zerotier" '' # compare hashes of the current identity secret and the one in the config hash1=$(sha256sum /var/lib/zerotier-one/identity.secret | cut -d ' ' -f 1) hash2=$(sha256sum ${config.clan.core.vars.generators.zerotier.files.zerotier-identity-secret.path} | cut -d ' ' -f 1) if [[ "$hash1" != "$hash2" ]]; then echo "Identity secret has changed, backing up old identity to /var/lib/zerotier-one/identity.secret.bac" cp /var/lib/zerotier-one/identity.secret /var/lib/zerotier-one/identity.secret.bac cp /var/lib/zerotier-one/identity.public /var/lib/zerotier-one/identity.public.bac cp ${config.clan.core.vars.generators.zerotier.files.zerotier-identity-secret.path} /var/lib/zerotier-one/identity.secret zerotier-idtool getpublic /var/lib/zerotier-one/identity.secret > /var/lib/zerotier-one/identity.public fi # cleanup old networks if [[ -d /var/lib/zerotier-one/networks.d ]]; then find /var/lib/zerotier-one/networks.d \ -type f \ -name "*.conf" \ -not \( ${ lib.concatMapStringsSep " -o " ( netId: ''-name "${netId}.conf"'' ) config.services.zerotierone.joinNetworks } \) \ -delete fi ''}" ]; }) ]; }; }; }; roles.controller = { interface = { lib, ... }: { options = { allowedIps = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; description = '' Extra machines by their zerotier ip that the zerotier controller should accept. These could be external machines. ''; example = '' [ "fd5d:bbe3:cbc5:fe6b:f699:935d:bbe3:cbc5" ] ''; }; settings = lib.mkOption { description = "override the network config in /var/lib/zerotier/bla/$network.json"; type = lib.types.json; }; }; }; perInstance = { instanceName, roles, settings, ... }: { nixosModule = { config, lib, pkgs, ... }: let uniqueStrings = list: builtins.attrNames (builtins.groupBy lib.id list); in { systemd.services.zerotierone.serviceConfig.ExecStartPre = [ "+${pkgs.writeShellScript "init-zerotier-${instanceName}" '' mkdir -p /var/lib/zerotier-one/controller.d/${instanceName} ln -sfT ${pkgs.writeText "net.json" (builtins.toJSON settings.settings)} /var/lib/zerotier-one/controller.d/network/${config.clan."zerotier_${instanceName}".networkId}.json ''}" ]; systemd.services.zerotierone.serviceConfig.ExecStartPost = [ "+${pkgs.writeShellScript "whitelist-controller" '' ${config.clan.core.clanPkgs.zerotier-members}/bin/zerotier-members allow ${ builtins.substring 0 10 config.clan."zerotier_${instanceName}".networkId } ''}" ]; systemd.services.zerotier-inventory-autoaccept = let machines = uniqueStrings ( (lib.optionals (roles ? moon) (lib.attrNames roles.moon.machines)) ++ (lib.optionals (roles ? controller) (lib.attrNames roles.controller.machines)) ++ (lib.optionals (roles ? peer) (lib.attrNames roles.peer.machines)) ); networkIps = builtins.foldl' ( ips: name: if builtins.pathExists "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value" then ips ++ [ (builtins.readFile "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value") ] else ips ) [ ] machines; allHostIPs = settings.allowedIps ++ networkIps; in { wantedBy = [ "multi-user.target" ]; after = [ "zerotierone.service" ]; path = [ config.clan.core.clanPkgs.zerotierone ]; serviceConfig.ExecStart = pkgs.writeShellScript "zerotier-inventory-autoaccept" '' ${lib.concatMapStringsSep "\n" (host: '' ${config.clan.core.clanPkgs.zerotier-members}/bin/zerotier-members allow --member-ip ${host} '') allHostIPs} ''; }; }; }; }; # there a bunch of different scenarios that can happen which we need to take care of ## controller is not a peer (the controller doesn't have an ipv4 and is not part of the network) ## controller has multiple networks (currently the code only creates a single network per controller, so we need to figure out the API call to get another network created) ## controller has multiple networks but is peer in only some ## I guess we need to make an attrset of controllers to networks and then create the networks with the controller key # every controller key will be shared, so all network specific ids can be shared perMachine = { instances, machine, lib, ... }: let # an attrset of { controller1 = { instance1 = {}; instance2 = {}; }; controller2 = { instance3 = {}; }; } controllerNetworks = lib.foldlAttrs (acc: instanceName: instance: lib.recursiveUpdate acc { ${lib.head (lib.attrNames instance.roles.controller.machines)} = { ${instanceName} = {}; }; }) {} instances; getInstanceController = instance: lib.head (lib.attrNames instance.roles.controller.machines); in { nixosModule = { pkgs, config, ... }: { config = lib.mkMerge [ { # every controller gets a shared network key, from which we can derive multiple network ids # we have this var shared, so we can create it when evaluating any machine clan.core.vars.generators = lib.mapAttrs' (controllerName: _: lib.nameValuePair "zerotier_controller_${controllerName}" { shared = true; files.zerotier-identity-secret.deploy = false; runtimeInputs = [ config.services.zerotierone.package pkgs.python3 ]; script = '' python3 ${./generate.py} --mode network \ --ip "$out/zerotier-ip" \ --identity-secret "$out/zerotier-identity-secret" ''; }) controllerNetworks; } { # every instance in a controller gets a network id, which is derived from the controller's shared network key clan.core.vars.generators = lib.mapAttrs' (instanceName: instance: lib.nameValuePair "zerotier_network_${instanceName}" { shared = true; files.zerotier-network-id.secret = false; dependencies = [ config.clan.core.vars.generators."zerotier_controller_${getInstanceController instance}" ]; runtimeInputs = [ config.services.zerotierone.package pkgs.python3 ]; script = '' python3 ${./generate.py} --mode network-id \ --network-id "$out/zerotier-network-id" # TODO we need to pass in the controller key ''; }) instances; } { # define nixos options, which we need to propagate certain information to other roles (like controller) options.clan = lib.mapAttrs' (instanceName: _: lib.nameValuePair "zerotier_${instanceName}" { networkId = lib.mkOption { type = lib.types.str; description = "The zerotier network id assigned to this machine"; default = if instances.${instanceName}.settings.networkId != null then instances.${instanceName}.settings.networkId else "generated-per-machine"; }; }); } # if we have set null as networkId, we assume that we have a controller and generate a shared key for it # (lib.mkIf (settings.networkId == null) { # clan.core.vars.generators."zerotier_${instanceName}" = { # files.zerotier-network-id.secret = false; # files.zerotier-identity-secret.deploy = false; # shared = true; # runtimeInputs = [ # config.services.zerotierone.package # pkgs.python3 # ]; # script = '' # source ${(pkgs.callPackage ../../../pkgs/minifakeroot { })}/share/minifakeroot/rc # python3 ${./generate.py} --mode network \ # --ip "$out/zerotier-ip" \ # --identity-secret "$out/zerotier-identity-secret" \ # --network-id "$out/zerotier-network-id" # ''; # }; # clan."zerotier_${instanceName}".networkId = # config.clan.core.vars.generators."zerotier_${instanceName}".files.zerotier-network-id.value; # }) ]; }; }; }; }