/ darwinModules / zerotier.nix
zerotier.nix
1 { 2 self, 3 config, 4 lib, 5 pkgs, 6 ... 7 }: 8 let 9 cfg = config.clan.core.networking.zerotier; 10 11 ztDataDir = "/Library/Application Support/ZeroTier/One"; 12 ztCli = "/usr/local/bin/zerotier-cli"; 13 ztIdTool = "${pkgs.zerotierone}/bin/zerotier-idtool"; 14 15 # Access clan-core lib for getPublicValue 16 clanLib = self.inputs.clan-core.lib; 17 18 # Get network ID from controller machine's vars 19 getNetworkId = 20 if cfg.controller.machineName != null then 21 clanLib.getPublicValue { 22 flake = config.clan.core.settings.directory; 23 machine = cfg.controller.machineName; 24 generator = "zerotier"; 25 file = "zerotier-network-id"; 26 default = null; 27 } 28 else 29 null; 30 31 # Helper to read clan vars 32 readVarFile = 33 machine: generator: file: 34 let 35 path = self + "/vars/per-machine/${machine}/${generator}/${file}/value"; 36 in 37 if builtins.pathExists path then lib.strings.trim (builtins.readFile path) else null; 38 39 # ZeroTier IPs for .i domain 40 zerotierIPs = { 41 taps = readVarFile "taps" "zerotier" "zerotier-ip"; 42 malt = readVarFile "malt" "zerotier" "zerotier-ip"; 43 pint = readVarFile "pint" "zerotier" "zerotier-ip"; 44 rhesus = readVarFile "rhesus" "zerotier" "zerotier-ip"; 45 }; 46 47 tapsZerotierIP = zerotierIPs.taps; 48 49 mkHostsEntries = 50 ips: domain: 51 lib.concatStringsSep "\n" ( 52 lib.filter (x: x != "") ( 53 lib.mapAttrsToList (name: ip: if ip != null then "${ip} ${name}.${domain}" else "") ips 54 ) 55 ); 56 in 57 { 58 options.clan.core.networking.zerotier = { 59 enable = lib.mkEnableOption "ZeroTier networking for Darwin"; 60 61 networkId = lib.mkOption { 62 type = lib.types.nullOr lib.types.str; 63 default = getNetworkId; 64 description = "ZeroTier network ID (auto-detected from controller if not set)"; 65 }; 66 67 controller.machineName = lib.mkOption { 68 type = lib.types.nullOr lib.types.str; 69 default = null; 70 description = "Name of the machine that acts as ZeroTier controller"; 71 }; 72 }; 73 74 options.services.zerotierone = { 75 enable = lib.mkEnableOption "ZeroTier One"; 76 77 joinNetworks = lib.mkOption { 78 type = lib.types.listOf lib.types.str; 79 default = lib.optional (cfg.networkId != null) cfg.networkId; 80 description = "List of ZeroTier network IDs to join on startup"; 81 }; 82 83 identitySecretFile = lib.mkOption { 84 type = lib.types.nullOr lib.types.path; 85 default = null; 86 description = "Path to ZeroTier identity.secret file"; 87 }; 88 }; 89 90 config = lib.mkMerge [ 91 (lib.mkIf cfg.enable { 92 # Vars generator for zerotier identity 93 clan.core.vars.generators.zerotier = { 94 files.zerotier-identity-secret = { 95 secret = true; 96 }; 97 files.zerotier-ip = { }; 98 script = '' 99 ${pkgs.zerotierone}/bin/zerotier-idtool generate "$out/zerotier-identity-secret" 100 IDENTITY_PUBLIC=$(${pkgs.zerotierone}/bin/zerotier-idtool getpublic "$out/zerotier-identity-secret") 101 NODE_ID=$(echo "$IDENTITY_PUBLIC" | cut -d: -f1) 102 NETWORK_ID="${toString cfg.networkId}" 103 if [ -n "$NETWORK_ID" ]; then 104 PREFIX="fd''${NETWORK_ID:0:2}:''${NETWORK_ID:2:4}:''${NETWORK_ID:6:4}:''${NETWORK_ID:10:4}" 105 SUFFIX="''${NODE_ID:0:2}''${NODE_ID:2:2}:''${NODE_ID:4:4}:''${NODE_ID:6:2}''${NODE_ID:8:2}" 106 echo "$PREFIX:$SUFFIX" > "$out/zerotier-ip" 107 fi 108 ''; 109 }; 110 111 # Link clan options to service options 112 services.zerotierone = { 113 enable = true; 114 joinNetworks = lib.mkIf (cfg.networkId != null) [ cfg.networkId ]; 115 identitySecretFile = config.clan.core.vars.generators.zerotier.files.zerotier-identity-secret.path; 116 }; 117 }) 118 119 (lib.mkIf config.services.zerotierone.enable { 120 environment.systemPackages = [ pkgs.zerotierone ]; 121 122 # DNS resolver for .i domain → taps ZeroTier IP 123 environment.etc."resolver/i" = lib.mkIf (tapsZerotierIP != null) { 124 text = "nameserver ${tapsZerotierIP}\n"; 125 }; 126 127 # /etc/hosts entries via clan-core launchd daemon 128 clan.core.networking.extraHosts.zerotier = mkHostsEntries zerotierIPs "i"; 129 130 # Install identity and join networks on activation 131 system.activationScripts.postActivation.text = lib.mkAfter '' 132 echo "Setting up ZeroTier..." 133 134 # Ensure ZeroTier data directory exists 135 mkdir -p "${ztDataDir}" 136 137 ${lib.optionalString (config.services.zerotierone.identitySecretFile != null) '' 138 # Install clan-managed identity if different from current 139 if [ -f "${config.services.zerotierone.identitySecretFile}" ]; then 140 CURRENT_IDENTITY="" 141 if [ -f "${ztDataDir}/identity.secret" ]; then 142 CURRENT_IDENTITY=$(cat "${ztDataDir}/identity.secret" 2>/dev/null || true) 143 fi 144 NEW_IDENTITY=$(cat "${config.services.zerotierone.identitySecretFile}") 145 146 if [ "$CURRENT_IDENTITY" != "$NEW_IDENTITY" ]; then 147 echo "Installing clan-managed ZeroTier identity..." 148 launchctl unload /Library/LaunchDaemons/com.zerotier.one.plist 2>/dev/null || true 149 sleep 1 150 151 if [ -f "${ztDataDir}/identity.secret" ]; then 152 cp "${ztDataDir}/identity.secret" "${ztDataDir}/identity.secret.bak.$(date +%s)" 153 rm -f "${ztDataDir}/identity.public" 154 fi 155 cp "${config.services.zerotierone.identitySecretFile}" "${ztDataDir}/identity.secret" 156 chmod 600 "${ztDataDir}/identity.secret" 157 ${ztIdTool} getpublic "${ztDataDir}/identity.secret" > "${ztDataDir}/identity.public" 158 159 echo "Restarting ZeroTier daemon with new identity..." 160 launchctl load /Library/LaunchDaemons/com.zerotier.one.plist 2>/dev/null || true 161 fi 162 fi 163 ''} 164 165 # Wait for zerotier daemon to be ready 166 for i in {1..15}; do 167 if ${ztCli} info >/dev/null 2>&1; then 168 break 169 fi 170 echo "Waiting for ZeroTier daemon... ($i/15)" 171 sleep 1 172 done 173 174 ${lib.concatMapStringsSep "\n" (network: '' 175 if ! ${ztCli} listnetworks 2>/dev/null | grep -q "${network}"; then 176 echo "Joining ZeroTier network ${network}..." 177 ${ztCli} join "${network}" || true 178 fi 179 '') config.services.zerotierone.joinNetworks} 180 ''; 181 }) 182 ]; 183 }