Newer
Older
skyworks-Nix-infra / hosts / xlab-gateway / dhcp.nix
# xlab-gateway DHCP + DDNS + IPv6 SLAAC
# Kea DHCPv4 on bond.lan254, DDNS forwarding to BIND9 at 10.0.0.1:5353
{ config, pkgs, ... }:

let
  lanDeviceUnit = "sys-subsystem-net-devices-bond.lan254.device";
  waitForLanAddresses = pkgs.writeShellScript "wait-for-xlab-lan-addresses" ''
    set -eu

    for _ in $(${pkgs.coreutils}/bin/seq 1 30); do
      if ${pkgs.iproute2}/bin/ip -4 -brief address show dev bond.lan254 \
          | ${pkgs.gnugrep}/bin/grep -Fq "10.253.254.1/24" \
        && ${pkgs.iproute2}/bin/ip -6 -brief address show dev bond.lan254 \
          | ${pkgs.gnugrep}/bin/grep -Fq "fd99:23eb:1682:1::1/64"; then
        exit 0
      fi
      ${pkgs.coreutils}/bin/sleep 1
    done

    echo "bond.lan254 did not acquire its configured addresses within 30 seconds" >&2
    exit 1
  '';
in
{
  age.secrets.xlab-ddns-tsig = {
    file = ../../secrets/xlab-ddns-tsig.age;
    owner = "kea";
    group = "kea";
    mode = "0400";
  };

  # ===========================================================================
  # Kea DHCPv4
  # ===========================================================================
  services.kea.dhcp4 = {
    enable = true;
    settings = {
      interfaces-config.interfaces = [ "bond.lan254" ];

      lease-database = {
        type = "memfile";
        name = "/var/lib/kea/kea-leases4.csv";
        persist = true;
        lfc-interval = 3600;
      };

      expired-leases-processing = {
        reclaim-timer-wait-time = 10;
        flush-reclaimed-timer-wait-time = 25;
        hold-reclaimed-time = 3600;
        max-reclaim-leases = 100;
        max-reclaim-time = 250;
      };

      valid-lifetime = 86400;
      renew-timer = 21600;
      rebind-timer = 43200;

      option-data = [
        { name = "routers"; data = "10.253.254.1"; }
        { name = "domain-name-servers"; data = "10.253.254.1"; }
        { name = "domain-name"; data = "dev.skyw.top"; }
        { name = "domain-search"; data = "dev.skyw.top"; }
      ];

      # Cisco CAPWAP APs -> Catalyst 9800-CL WMI 10.0.10.10 via DHCP option 43
      # sub-option 241 (0xf1). Scoped to APs (option-60 "Cisco AP*") so other
      # clients never receive it. Mirrors door1's kea-dhcp4 cisco-ap class.
      client-classes = [
        {
          name = "cisco-ap";
          test = "substring(option[60].hex,0,8) == 'Cisco AP'";
          option-data = [
            { name = "vendor-encapsulated-options"; csv-format = false; data = "F1:04:0A:00:0A:0A"; always-send = true; }
            # CAPWAP over the 1420-MTU wg-to-wgnet tunnel: the AP sends 1441B DF
            # DTLS packets and ignores PMTUD frag-needed, so the join blackholes.
            # Pin the AP's wired MTU to 1420 so it never exceeds the path.
            { name = "interface-mtu"; data = "1420"; always-send = true; }
          ];
        }
      ];

      subnet4 = [
        {
          id = 1;
          subnet = "10.253.254.0/24";
          pools = [
            { pool = "10.253.254.100 - 10.253.254.240"; }
          ];
          option-data = [
            { name = "subnet-mask"; data = "255.255.255.0"; }
            { name = "routers"; data = "10.253.254.1"; }
            { name = "domain-name-servers"; data = "10.253.254.1"; }
            # Classless static routes: 10.0.0.0/16 via 10.253.254.1, default via 10.253.254.1
            { code = 121; csv-format = false; data = "100A000AFDFE01000AFDFE01"; }
            # MS classless static routes (same)
            { code = 249; csv-format = false; data = "100A000AFDFE01000AFDFE01"; }
          ];
          reservations = [];
        }
      ];

      ddns-send-updates = true;
      ddns-qualifying-suffix = "dev.skyw.top.";

      dhcp-ddns = {
        enable-updates = true;
        max-queue-size = 1024;
        ncr-protocol = "UDP";
        ncr-format = "JSON";
        sender-ip = "127.0.0.1";
        sender-port = 0;
        server-ip = "127.0.0.1";
        server-port = 53001;
      };
    };
    
  };
  
  # The global network-online target is intentionally disabled on this router,
  # so wait for the actual LAN device and addresses before Kea opens sockets.
  systemd.services.kea-dhcp4-server = {
    requires = [ lanDeviceUnit ];
    wants = [ "kea-dhcp-ddns-server.service" ];
    after = [
      lanDeviceUnit
      "systemd-networkd.service"
      "kea-dhcp-ddns-server.service"
    ];
    serviceConfig = {
      ExecStartPre = [ waitForLanAddresses ];
      RestartSec = "2s";
    };
  };


  # ===========================================================================
  # Kea DHCP-DDNS (D2) - Forwards DNS updates to BIND9
  # ===========================================================================
  services.kea.dhcp-ddns = {
    enable = true;
    settings = {
      ip-address = "127.0.0.1";
      port = 53001;

      tsig-keys = [
        {
          # The main gateway temporarily accepts both names during rotation;
          # xlab uses only the replacement key.
          name = "edge-ddns-key-next";
          algorithm = "HMAC-SHA256";
          secret-file = config.age.secrets.xlab-ddns-tsig.path;
        }
      ];

      forward-ddns.ddns-domains = [
        {
          name = "dev.skyw.top.";
          key-name = "edge-ddns-key-next";
          dns-servers = [{ ip-address = "10.0.0.1"; port = 5353; }];
        }
      ];

      reverse-ddns.ddns-domains = [
        {
          name = "10.in-addr.arpa.";
          key-name = "edge-ddns-key-next";
          dns-servers = [{ ip-address = "10.0.0.1"; port = 5353; }];
        }
        {
          name = "2.8.6.1.b.e.3.2.9.9.d.f.ip6.arpa.";
          key-name = "edge-ddns-key-next";
          dns-servers = [{ ip-address = "10.0.0.1"; port = 5353; }];
        }
      ];
    };
  };

  systemd.services.kea-dhcp-ddns-server.restartTriggers = [
    config.age.secrets.xlab-ddns-tsig.file
  ];

  # ===========================================================================
  # radvd - IPv6 SLAAC, DNS, and search-domain advertisements
  # ===========================================================================
  services.radvd = {
    enable = true;
    config = ''
      interface bond.lan254 {
        AdvSendAdvert on;
        AdvManagedFlag off;
        AdvOtherConfigFlag off;
        MinRtrAdvInterval 30;
        MaxRtrAdvInterval 100;
        prefix fd99:23eb:1682:1::/64 {
          AdvOnLink on;
          AdvAutonomous on;
          AdvRouterAddr on;
        };
        RDNSS fd99:23eb:1682:1::1 {
          AdvRDNSSLifetime 3600;
        };
        DNSSL dev.skyw.top {
          AdvDNSSLLifetime 3600;
        };
      };
    '';
  };

  systemd.services.radvd = {
    requires = [ lanDeviceUnit ];
    after = [ lanDeviceUnit "systemd-networkd.service" ];
    serviceConfig.ExecStartPre = [ waitForLanAddresses ];
  };
}