/ 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  }