Commit 1a71345

HPCesia <me@hpcesia.com>
2026-08-31 15:44:20
tailscale derp: init
1 parent 09b78e7
Changed files (4)
.secrets/cache/cyrene/01d5bc63ddba392c9fa55ccb759d10c5ed6d354b55e2ff614d1b68e811b72614
@@ -0,0 +1,8 @@
+age-encryption.org/v1
+-> ssh-ed25519 7JvPpg eFoDP61kD600XcJ9JFHqDE6lx8Pa4jLOdQ9j80clr1o
+qMKUfnXpIL1160EkoQl9AZZakKtTlqBGax/2VuJ4YPQ
+-> cO8()M-grease L,mZiP_ $
+VEGF63WU+3T/WvYPBKR0moovG/dYCdmkHNYMuD0Jg776sfqWNx4ncozyZDPEvlqP
+H+RBNA
+--- IhimD/hkZqs1CYHB8e8vKkn+6KBl9y0RlBAJhgXjsiI
+`/��h��sto�o(��|���(:Bw�:ӘQ^�@�e���-�pY
\ No newline at end of file
modules/hosts/hyacine/services/default.nix
@@ -0,0 +1,16 @@
+{den, ...}: {
+  den.hosts.hyacine = {
+    settings = {
+      services.tailscale-derp = {
+        domain = "derp-hyacine.trin.one";
+        port = 8010;
+      };
+    };
+  };
+
+  den.aspects.hyacine.includes = with den.aspects; [
+    services.caddy
+
+    services.tailscale-derp
+  ];
+}
modules/services/headscale.nix
@@ -63,6 +63,102 @@
 
       tailnetDomain = den.aspects.services.caddy.tailnetDomain;
 
+      derpHosts = let
+        allHosts = builtins.concatMap builtins.attrValues (builtins.attrValues den.hosts);
+      in
+        builtins.filter (h: h.hasAspect den.aspects.services.tailscale-derp) allHosts;
+
+      hexDigitValue = c:
+        lib.lists.findFirstIndex (d: d == c) 0 (lib.stringToCharacters "0123456789abcdef");
+
+      mkDerpRegionId = domain:
+        900
+        + (
+          builtins.foldl' (acc: c: lib.mod (acc * 16 + hexDigitValue c) 99) 0
+          (lib.stringToCharacters (builtins.hashString "sha256" domain))
+        );
+
+      derpRegions =
+        map (h: let
+          dcfg = h.settings.services.tailscale-derp;
+          v4 = h.address.ipv4;
+          v6 = h.address.ipv6;
+        in {
+          inherit h dcfg;
+          regionId = mkDerpRegionId dcfg.domain;
+          v4 =
+            if v4.clearText != null
+            then v4.clearText
+            else if v4.secret.name != null && v4.secret.file != null
+            then config.vaultix.placeholder.${v4.secret.name}
+            else null;
+          v6 =
+            if v6.clearText != null
+            then v6.clearText
+            else if v6.secret.name != null && v6.secret.file != null
+            then config.vaultix.placeholder.${v6.secret.name}
+            else null;
+        })
+        derpHosts;
+
+      duplicateDerpDomains = let
+        domains = map (r: r.dcfg.domain) derpRegions;
+      in
+        lib.filter (d: builtins.length (lib.filter (r: r.dcfg.domain == d) derpRegions) > 1) (lib.unique domains);
+
+      collidingDerpRegionIds = lib.filter (r: builtins.length (lib.filter (r': r'.regionId == r.regionId) derpRegions) > 1) derpRegions;
+
+      derpersWithoutAddress = lib.filter (r: r.v4 == null && r.v6 == null) derpRegions;
+
+      assertUniqueDerpDomains =
+        lib.assertMsg (duplicateDerpDomains == [])
+        "Multiple DERP hosts declare the same domain: ${lib.concatStringsSep ", " duplicateDerpDomains}";
+
+      assertUniqueDerpRegionIds =
+        lib.assertMsg (collidingDerpRegionIds == [])
+        ("DERP region ID collision between hosts: "
+          + lib.concatStringsSep ", " (lib.unique (map (r: r.h.name) collidingDerpRegionIds)));
+
+      assertDerpersHaveAddress =
+        lib.assertMsg (derpersWithoutAddress == [])
+        ("DERP hosts have no public IPv4 or IPv6 (clearText or secret): "
+          + lib.concatStringsSep ", " (map (r: r.h.name) derpersWithoutAddress));
+
+      mkDerpNodeLines = r:
+        lib.concatStringsSep "\n" (
+          ["      - name: ${r.h.name}"]
+          ++ ["        regionid: ${toString r.regionId}"]
+          ++ ["        hostname: ${r.dcfg.domain}"]
+          ++ lib.optionals (r.v4 != null) ["        ipv4: \"${r.v4}\""]
+          ++ lib.optionals (r.v6 != null) ["        ipv6: \"${r.v6}\""]
+          ++ ["        stunport: 3478"]
+          ++ ["        derpport: 443"]
+          ++ ["        stunonly: false"]
+        );
+
+      mkDerpRegionBlock = r:
+        lib.concatStringsSep "\n" (
+          ["  ${toString r.regionId}:"]
+          ++ ["    regionid: ${toString r.regionId}"]
+          ++ ["    regioncode: ${r.h.name}"]
+          ++ ["    regionname: ${r.h.name}"]
+          ++ ["    nodes:"]
+          ++ [(mkDerpNodeLines r)]
+        );
+
+      derpMapYaml = assert assertUniqueDerpDomains;
+      assert assertUniqueDerpRegionIds;
+      assert assertDerpersHaveAddress; (
+        if derpRegions == []
+        then "regions: {}\n"
+        else "regions:\n" + lib.concatStringsSep "\n" (map mkDerpRegionBlock derpRegions) + "\n"
+      );
+
+      hostsWithIpv4Secret =
+        builtins.filter (h: h.address.ipv4.secret.name != null && h.address.ipv4.secret.file != null) derpHosts;
+      hostsWithIpv6Secret =
+        builtins.filter (h: h.address.ipv6.secret.name != null && h.address.ipv6.secret.file != null) derpHosts;
+
       # Tailnet entries (from all hosts): emit A/AAAA records pointing at the
       # owning host's tailscale addresses so MagicDNS resolves them.
       tailnetEntries =
@@ -130,15 +226,18 @@
             v6 = "fd7a:115c:a1e0::/48";
             allocation = "random";
           };
-          derp.server = lib.optionalAttrs (cfg.derp.enable) {
-            enabled = true;
-            stun_listen_addr = "0.0.0.0:${toString cfg.derp.port}";
-            verify_clients = true;
-            region_id = 999;
-            region_code = "headscale";
-            region_name = "Headscale Embedded DERP";
-            ipv4 = lib.mkIf (hostAddr.ipv4.clearText != null) hostAddr.ipv4.clearText;
-            ipv6 = lib.mkIf (hostAddr.ipv6.clearText != null) hostAddr.ipv6.clearText;
+          derp = {
+            server = lib.optionalAttrs (cfg.derp.enable) {
+              enabled = true;
+              stun_listen_addr = "0.0.0.0:${toString cfg.derp.port}";
+              verify_clients = true;
+              region_id = 999;
+              region_code = "headscale";
+              region_name = "Headscale Embedded DERP";
+              ipv4 = lib.mkIf (hostAddr.ipv4.clearText != null) hostAddr.ipv4.clearText;
+              ipv6 = lib.mkIf (hostAddr.ipv6.clearText != null) hostAddr.ipv6.clearText;
+            };
+            paths = [config.vaultix.templates.headscale-derp-map.path];
           };
           dns = {
             magic_dns = cfg.dns.enable;
@@ -159,6 +258,17 @@
 
       networking.firewall.allowedUDPPorts = lib.optional (cfg.derp.enable) cfg.derp.port;
 
+      vaultix.secrets = lib.mkMerge (
+        (map (h: {${h.address.ipv4.secret.name}.file = h.address.ipv4.secret.file;}) hostsWithIpv4Secret)
+        ++ (map (h: {${h.address.ipv6.secret.name}.file = h.address.ipv6.secret.file;}) hostsWithIpv6Secret)
+      );
+      vaultix.templates.headscale-derp-map = {
+        content = derpMapYaml;
+        owner = config.services.headscale.user;
+        group = config.services.headscale.group;
+        mode = "0400";
+      };
+
       systemd.services.headscale = let
         nixConfig = config.services.headscale.configFile;
         hsCfg = config.services.headscale;
@@ -222,7 +332,7 @@
             ++ (
               lib.optional
               (hostAddr.ipv6.secret.name != null)
-              "HEADSCALE_DERP_SERVER_IPV4=${config.vaultix.placeholder.${hostAddr.ipv6.secret.name}}"
+              "HEADSCALE_DERP_SERVER_IPV6=${config.vaultix.placeholder.${hostAddr.ipv6.secret.name}}"
             )
           );
           owner = config.services.headscale.user;
modules/services/tailscale-derp.nix
@@ -0,0 +1,35 @@
+{lib, ...}: {
+  den.aspects.services.tailscale-derp = {
+    settings.host = {
+      domain = lib.mkOption {
+        type = lib.types.str;
+      };
+      port = lib.mkOption {
+        type = lib.types.port;
+        default = 8010;
+      };
+    };
+
+    reverseProxy = {host, ...}: let
+      cfg = host.settings.services.tailscale-derp;
+    in {
+      ${cfg.domain} = {
+        port = cfg.port;
+      };
+    };
+
+    nixos = {host, ...}: let
+      cfg = host.settings.services.tailscale-derp;
+    in {
+      services.tailscale.derper = {
+        enable = true;
+
+        inherit (cfg) domain port;
+
+        configureNginx = false;
+        openFirewall = true;
+        verifyClients = true;
+      };
+    };
+  };
+}