Newer
Older
skyworks-Nix-infra / hosts / xlab-gateway / default.nix
# xlab-gateway - Lab Gateway / Router
# Current services: Kea DHCP4/DDNS, radvd, WireGuard, NAT, policy routing
{ config, pkgs, lib, ... }:

{
  imports = [
    ./hardware-configuration.nix
    ./networking.nix
    ./dhcp.nix
  ];

  age.identityPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];

  networking.hostName = "xlab-gateway";

  nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";

  # This router has no interactive virtual console. On this hardware,
  # systemd-vconsole-setup can block indefinitely writing its UTF-8 escape to
  # /dev/tty0, which also stalls switch-to-configuration. Disable the real
  # setup (including in initrd), but retain no-op units so a live switch never
  # hits a remove-then-reload ordering failure.
  console.enable = false;
  systemd.services.systemd-vconsole-setup = {
    description = "No-op virtual console setup on headless xlab gateway";
    serviceConfig = {
      Type = "oneshot";
      ExecStart = "${pkgs.coreutils}/bin/true";
      RemainAfterExit = true;
    };
  };
  systemd.services.reload-systemd-vconsole-setup = {
    description = "No-op virtual console reload on headless xlab gateway";
    wantedBy = [ "multi-user.target" ];
    reloadIfChanged = true;
    serviceConfig = {
      Type = "oneshot";
      ExecStart = "${pkgs.coreutils}/bin/true";
      ExecReload = "${pkgs.coreutils}/bin/true";
      RemainAfterExit = true;
    };
  };

  boot = {
    loader = {
      systemd-boot.enable = true;
      efi.canTouchEfiVariables = true;
    };
    kernel.sysctl = {
      "net.ipv4.ip_forward" = 1;
      "net.ipv6.conf.all.forwarding" = 1;

      # BBR + fq: better throughput for xlab-ORIGINATED TCP over the high-RTT
      # abroad WG paths (forwarded transit keeps the endpoints' own CC).
      "net.core.default_qdisc" = "fq";
      "net.ipv4.tcp_congestion_control" = "bbr";

      # Bigger socket buffers / autotune ceiling for high-BDP abroad links.
      "net.core.rmem_max" = 67108864;
      "net.core.wmem_max" = 67108864;
      "net.core.rmem_default" = 1048576;
      "net.core.wmem_default" = 1048576;
      "net.ipv4.tcp_rmem" = "4096 1048576 67108864";
      "net.ipv4.tcp_wmem" = "4096 1048576 67108864";
      "net.core.netdev_max_backlog" = 16384;
    };
    # tcp_bbr is a module on this kernel (reno cubic only built-in) — load it
    # so tcp_congestion_control = bbr above takes effect.
    kernelModules = [ "tcp_bbr" "wireguard" ];
  };

  # WireGuard-crypto + routing appliance: keep cores pinned so WG throughput
  # doesn't sag while frequencies ramp on a burst, and to cut forwarding jitter.
  powerManagement.cpuFreqGovernor = "performance";

  # Gateway doesn't need to block boot waiting for all interfaces
  systemd.network.wait-online.enable = false;

  users.users.ldx = {
    extraGroups = [ "networkmanager" ];
    hashedPassword = "$y$j9T$R0XBoDSGk700h7UdglfaJ1$mjRwpJir/Tno1.fCbjet0cp/JPb1DW.JILvmE5.NJuD";
  };
  
  users.users."ye-lw21" = {
    extraGroups = [ "networkmanager" ];
    hashedPassword = "$y$j9T$jiLKGLB/gJKEYYn2zaoUw/$9mfwEUo5z2sH9OXwioLnbAVpCMOg2lUpA3ph9Vqx228";
  };

  environment.systemPackages = with pkgs; [
    wireguard-tools
    iperf3
    ethtool
    tcpdump
    nftables
    iproute2
    glances
    smartmontools
  ];

  services.smartd = {
    enable = true;
    autodetect = true;
  };

  # CAPWAP over the 1420-MTU wg-to-wgnet tunnel: the lab Cisco APs (e.g. 3802
  # b08b.cfa8.4fa8) send ~1469B DON'T-FRAGMENT DTLS packets to the C9800 WLC at
  # 10.0.10.10 during join, and ignore both PMTUD and DHCP interface-mtu, so the
  # CAPWAP/DTLS handshake blackholes on the tunnel and the AP never joins. Clear
  # the DF bit on UDP->WLC at ingress so those packets fragment through the tunnel
  # (the WLC reassembles). Confirmed: AP joins with this in place. See memory
  # cisco-wlc-discovery.
  systemd.services.capwap-df-clear = {
    description = "Clear DF on lab AP CAPWAP->WLC (10.0.10.10) so DTLS fits the 1420 wg tunnel";
    after = [ "network.target" "systemd-networkd.service" ];
    wantedBy = [ "multi-user.target" ];
    serviceConfig = {
      Type = "oneshot";
      RemainAfterExit = true;
      ExecStart = pkgs.writeShellScript "capwap-df-clear-up" ''
        set -eu
        for _ in $(${pkgs.coreutils}/bin/seq 1 30); do
          ${pkgs.iproute2}/bin/ip link show bond.lan254 >/dev/null 2>&1 && break
          ${pkgs.coreutils}/bin/sleep 1
        done
        ${pkgs.iproute2}/bin/ip link show bond.lan254 >/dev/null

        # clsact is shared infrastructure. Never replace or remove it, and own
        # only the two CAPWAP priorities below.
        if ! ${pkgs.iproute2}/bin/tc qdisc show dev bond.lan254 | ${pkgs.gnugrep}/bin/grep -q '^qdisc clsact '; then
          ${pkgs.iproute2}/bin/tc qdisc add dev bond.lan254 clsact
        fi
        for port in 5246 5247; do
          # `replace` without an explicit flower handle can still return
          # EEXIST. Delete only our owned preference, then recreate it.
          ${pkgs.iproute2}/bin/tc filter del dev bond.lan254 ingress \
            protocol ip pref "$port" 2>/dev/null || true
          ${pkgs.iproute2}/bin/tc filter add dev bond.lan254 ingress \
            protocol ip pref "$port" flower \
            ip_proto udp dst_ip 10.0.10.10 dst_port "$port" \
            action pedit ex munge ip df set 0 pipe action csum ip
        done
      '';
      ExecStop = pkgs.writeShellScript "capwap-df-clear-down" ''
        for port in 5246 5247; do
          ${pkgs.iproute2}/bin/tc filter del dev bond.lan254 ingress \
            protocol ip pref "$port" 2>/dev/null || true
        done
      '';
    };
  };

  system.stateVersion = "25.11";
}