Commit 8640088

HPCesia <me@hpcesia.com>
2026-09-12 15:55:06
tribios: pin ap and mobius's ips
Assisted-by: opencode:deepseek-v4-flash
1 parent b89a5ae
Changed files (4)
modules
hosts
services
modules/hosts/mobius/networking.nix
@@ -7,6 +7,11 @@
   };
 
   den.aspects.mobius = {
+    dhcpReservation = {
+      ip = "192.168.52.3";
+      mac = "00:e0:1a:00:1d:7b";
+    };
+
     nixos = {
       networking.networkmanager.enable = true;
     };
modules/hosts/tribios/networking.nix
@@ -12,10 +12,24 @@
         lan = ["br-lan"];
       };
     };
+    settings.services.kea = {
+      interfaces = ["br-lan"];
+      router = "192.168.52.1";
+      dnsServers = ["223.5.5.5" "119.29.29.29"];
+      extraReservations = [
+        {
+          # AP.
+          hostname = "ap";
+          mac = "f4:f6:47:33:56:98";
+          ip = "192.168.52.2";
+        }
+      ];
+    };
   };
 
   den.aspects.tribios = {
     includes = [
+      den.aspects.services.kea
       den.aspects.services.mihomo
       den.aspects.services.mihomo.tproxy
     ];
@@ -76,36 +90,6 @@
       };
 
       systemd.services.miniupnpd.after = ["systemd-networkd.service"];
-
-      services.kea.dhcp4 = {
-        enable = true;
-        settings = {
-          interfaces-config.interfaces = ["br-lan"];
-          lease-database = {
-            name = "/var/lib/kea/dhcp4.leases";
-            persist = true;
-            type = "memfile";
-          };
-          valid-lifetime = 4000;
-          subnet4 = [
-            {
-              id = 1;
-              subnet = "192.168.52.0/24";
-              pools = [{pool = "192.168.52.2 - 192.168.52.200";}];
-              option-data = [
-                {
-                  name = "routers";
-                  data = "192.168.52.1";
-                }
-                {
-                  name = "domain-name-servers";
-                  data = "223.5.5.5, 119.29.29.29";
-                }
-              ];
-            }
-          ];
-        };
-      };
     };
   };
 }
modules/services/kea/default.nix
@@ -0,0 +1,156 @@
+{lib, ...}: let
+  octets = ip: lib.splitString "." ip;
+  lastOctet = ip: lib.toInt (builtins.elemAt (octets ip) 3);
+  prefix = ip: lib.concatStringsSep "." (lib.take 3 (octets ip));
+  joinIP = prefix: last: "${prefix}.${toString last}";
+in {
+  den.aspects.services.kea = {
+    settings.host = {
+      interfaces = lib.mkOption {
+        description = "Interfaces the DHCP server listens on.";
+        type = lib.types.listOf lib.types.str;
+      };
+      staticRange = {
+        start = lib.mkOption {
+          description = "Start of the static IP range. Reservations must fall within it.";
+          type = lib.types.str;
+          default = "192.168.52.2";
+        };
+        end = lib.mkOption {
+          description = "End of the static IP range. Reservations must fall within it.";
+          type = lib.types.str;
+          default = "192.168.52.50";
+        };
+      };
+      poolEnd = lib.mkOption {
+        description = "End of the dynamic pool. The pool starts right after the static range.";
+        type = lib.types.str;
+        default = "192.168.52.200";
+      };
+      router = lib.mkOption {
+        description = "Default gateway advertised to clients.";
+        type = lib.types.nullOr lib.types.str;
+        default = null;
+      };
+      dnsServers = lib.mkOption {
+        description = "DNS servers advertised to clients.";
+        type = lib.types.listOf lib.types.str;
+        default = [];
+      };
+      extraReservations = lib.mkOption {
+        description = "Reservations for devices not managed by Nix, e.g. an AP.";
+        type = lib.types.listOf (lib.types.submodule {
+          options = {
+            hostname = lib.mkOption {
+              description = "Hostname for the device.";
+              type = lib.types.nullOr lib.types.str;
+              default = null;
+            };
+            mac = lib.mkOption {
+              description = "MAC address to reserve.";
+              type = lib.types.str;
+            };
+            ip = lib.mkOption {
+              description = "Fixed IP to assign.";
+              type = lib.types.str;
+            };
+          };
+        });
+        default = [];
+      };
+    };
+
+    nixos = {
+      host,
+      dhcpReservation,
+      ...
+    }: let
+      cfg = host.settings.services.kea;
+
+      fromHosts = lib.unique (
+        map (e: {
+          hostname = e.source.host.name;
+          mac = e.value.mac;
+          ip = e.value.ip;
+        })
+        dhcpReservation
+      );
+
+      reservations = cfg.extraReservations ++ fromHosts;
+
+      staticPrefix = prefix cfg.staticRange.start;
+      staticStart = lastOctet cfg.staticRange.start;
+      staticEnd = lastOctet cfg.staticRange.end;
+      poolStart = joinIP staticPrefix (staticEnd + 1);
+
+      inStaticRange = ip:
+        prefix ip
+        == staticPrefix
+        && lastOctet ip >= staticStart
+        && lastOctet ip <= staticEnd;
+
+      nameOf = r:
+        if r.hostname != null
+        then r.hostname
+        else r.ip;
+
+      macs = map (r: r.mac) reservations;
+      ips = map (r: r.ip) reservations;
+    in {
+      assertions =
+        (map (r: {
+            assertion = inStaticRange r.ip;
+            message =
+              "Kea reservation '${nameOf r}' has ip '${r.ip}' outside the static "
+              + "range '${cfg.staticRange.start} - ${cfg.staticRange.end}'";
+          })
+          reservations)
+        ++ [
+          {
+            assertion = lib.unique macs == macs;
+            message = "Kea reservations contain duplicate MAC addresses";
+          }
+          {
+            assertion = lib.unique ips == ips;
+            message = "Kea reservations contain duplicate IP addresses";
+          }
+        ];
+
+      services.kea.dhcp4 = {
+        enable = true;
+        settings = {
+          interfaces-config.interfaces = cfg.interfaces;
+          lease-database = {
+            name = "/var/lib/kea/dhcp4.leases";
+            persist = true;
+            type = "memfile";
+          };
+          valid-lifetime = 4000;
+          subnet4 = [
+            {
+              id = 1;
+              subnet = "${staticPrefix}.0/24";
+              pools = [{pool = "${poolStart} - ${cfg.poolEnd}";}];
+              reservations = map (r:
+                {
+                  hw-address = r.mac;
+                  ip-address = r.ip;
+                }
+                // lib.optionalAttrs (r.hostname != null) {hostname = r.hostname;})
+              reservations;
+              option-data =
+                lib.optional (cfg.router != null) {
+                  name = "routers";
+                  data = cfg.router;
+                }
+                ++ lib.optional (cfg.dnsServers != []) {
+                  name = "domain-name-servers";
+                  data = lib.concatStringsSep ", " cfg.dnsServers;
+                };
+            }
+          ];
+        };
+      };
+    };
+  };
+}
modules/services/kea/quirks.nix
@@ -0,0 +1,22 @@
+{den, ...}: {
+  den.quirks.dhcpReservation.description = ''
+    DHCP reservations self-declared by hosts, collected fleet-wide.
+
+    Example:
+      {
+        mac = "00:11:22:33:44:55"; # MAC address to reserve
+        ip = "192.168.52.10";      # Fixed IP to assign
+      }
+  '';
+
+  den.policies.collect-dhcp-reservations = _: let
+    inherit (den.lib.policy) pipe;
+  in [
+    (pipe.from "dhcpReservation" [
+      (pipe.collectAll ({host, ...}: true))
+      pipe.withProvenance
+    ])
+  ];
+
+  den.schema.host.includes = [den.policies.collect-dhcp-reservations];
+}