{ lib, config, pkgs, ... }: let cfg = config.services.every-channel.netboot; scriptsRoot = ../..; boolString = v: if v then "true" else "false"; mkExport = name: value: "export ${name}=${lib.escapeShellArg value}"; optionalExport = name: value: if value == null then "" else mkExport name value; optionalFileExport = name: path: if path == null then "" else '' if [[ ! -r ${lib.escapeShellArg path} ]]; then echo "error: required file is not readable: ${path}" >&2 exit 2 fi export ${name}="$(tr -d '\\r\\n' < ${lib.escapeShellArg path})" ''; stageToolchain = with pkgs; [ bash coreutils curl gawk gnugrep gnused gnutar gzip python3 ]; ipxeToolchain = with pkgs; [ git gnumake gcc binutils perl mtools docker-client ]; in { options.services.every-channel.netboot = { enable = lib.mkEnableOption "every.channel persistent netboot stage/serve services"; listenIP = lib.mkOption { type = lib.types.str; example = "10.20.30.2"; description = "IP address bound for TFTP/HTTP on the provisioning VLAN."; }; interface = lib.mkOption { type = lib.types.str; example = "enp3s0"; description = "Network interface name on the provisioning VLAN."; }; hostname = lib.mkOption { type = lib.types.str; default = "boot.every.channel"; description = "Boot server hostname advertised to DHCP/iPXE clients."; }; rootDir = lib.mkOption { type = lib.types.str; default = "/var/lib/every-channel-netboot"; description = "Persistent root directory containing staged HTTP and TFTP artifacts."; }; httpPort = lib.mkOption { type = lib.types.port; default = 8080; description = "HTTP port used for kernel/initrd/netboot script serving."; }; tftpBootFilename = lib.mkOption { type = lib.types.str; default = "ec-ipxe.efi"; description = "Filename served via TFTP (DHCP option 67 in UniFi-only mode)."; }; chainTokenFile = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "/run/agenix/every-channel-netboot-chain-token"; description = "Optional file containing netboot chain token passed to stage/serve scripts."; }; httpAllowedCIDRs = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; example = [ "10.20.30.0/24" ]; description = "Optional CIDR allowlist for HTTP artifact serving."; }; openFirewall = lib.mkOption { type = lib.types.bool; default = true; description = "Open firewall ports for TFTP/HTTP (and ProxyDHCP ports when enabled)."; }; stageOnBoot = lib.mkOption { type = lib.types.bool; default = true; description = "Start the stage oneshot service during boot before serving."; }; release = { host = lib.mkOption { type = lib.types.str; default = "https://git.every.channel"; description = "Forge host used to fetch netboot release assets."; }; repo = lib.mkOption { type = lib.types.str; default = "every-channel/every.channel"; description = "Forge repository containing netboot release assets."; }; tag = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Optional release tag to stage; defaults to latest release."; }; localTarball = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Optional local netboot tarball path; bypasses release API download when set."; }; tokenFile = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Optional file containing Forge API token for private release access."; }; verifyChecksums = lib.mkOption { type = lib.types.bool; default = true; description = "Verify staged release tarball using SHA256SUMS.txt when available."; }; }; ipxe = { buildEmbedded = lib.mkOption { type = lib.types.bool; default = true; description = "Build embedded iPXE (ec-ipxe.efi) before staging artifacts."; }; path = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Optional prebuilt iPXE EFI binary path; used when buildEmbedded is false."; }; allowRemoteDownload = lib.mkOption { type = lib.types.bool; default = false; description = "Allow remote iPXE URL download during staging. Disabled by default."; }; sha256 = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Optional SHA256 digest expected for selected iPXE EFI binary."; }; repo = lib.mkOption { type = lib.types.str; default = "https://github.com/ipxe/ipxe.git"; description = "iPXE source repository used for embedded EFI builds."; }; ref = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Optional git ref/commit for iPXE build reproducibility."; }; useDocker = lib.mkOption { type = lib.types.enum [ "auto" "true" "false" ]; default = "auto"; description = "Container fallback mode for iPXE builds (auto/true/false)."; }; dockerImage = lib.mkOption { type = lib.types.str; default = "ubuntu:24.04"; description = "Docker image used when iPXE build runs in container mode."; }; dockerPlatform = lib.mkOption { type = lib.types.str; default = "linux/amd64"; description = "Docker platform used for containerized iPXE build."; }; }; proxyDhcp = { enable = lib.mkOption { type = lib.types.bool; default = false; description = "Enable dnsmasq ProxyDHCP chainloading mode."; }; subnet = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "10.20.30.0/24"; description = "ProxyDHCP subnet (required when proxyDhcp.enable is true)."; }; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = cfg.proxyDhcp.enable -> cfg.proxyDhcp.subnet != null; message = "services.every-channel.netboot.proxyDhcp.subnet must be set when proxyDhcp.enable is true"; } { assertion = cfg.ipxe.buildEmbedded || cfg.ipxe.path != null || cfg.ipxe.allowRemoteDownload; message = "Set ipxe.buildEmbedded=true, or provide ipxe.path, or allow ipxe.allowRemoteDownload=true"; } ]; systemd.tmpfiles.rules = [ "d ${cfg.rootDir} 0750 root root -" "d ${cfg.rootDir}/http 0750 root root -" "d ${cfg.rootDir}/tftp 0750 root root -" ]; systemd.services.every-channel-netboot-ipxe = lib.mkIf cfg.ipxe.buildEmbedded { description = "every.channel netboot embedded iPXE build"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = lib.mkIf cfg.stageOnBoot [ "multi-user.target" ]; path = stageToolchain ++ ipxeToolchain; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' set -euo pipefail ${mkExport "EVERY_CHANNEL_NETBOOT_ROOT" cfg.rootDir} ${mkExport "EVERY_CHANNEL_NETBOOT_HOSTNAME" cfg.hostname} ${mkExport "EVERY_CHANNEL_NETBOOT_HTTP_PORT" (toString cfg.httpPort)} ${mkExport "EVERY_CHANNEL_NETBOOT_IPXE_FILENAME" cfg.tftpBootFilename} ${mkExport "EVERY_CHANNEL_NETBOOT_IPXE_REPO" cfg.ipxe.repo} ${optionalExport "EVERY_CHANNEL_NETBOOT_IPXE_REF" cfg.ipxe.ref} ${mkExport "EVERY_CHANNEL_NETBOOT_IPXE_USE_DOCKER" cfg.ipxe.useDocker} ${mkExport "EVERY_CHANNEL_NETBOOT_IPXE_DOCKER_IMAGE" cfg.ipxe.dockerImage} ${mkExport "EVERY_CHANNEL_NETBOOT_IPXE_DOCKER_PLATFORM" cfg.ipxe.dockerPlatform} ${optionalFileExport "EVERY_CHANNEL_NETBOOT_CHAIN_TOKEN" cfg.chainTokenFile} ${scriptsRoot}/scripts/netboot-build-ipxe.sh ''; }; systemd.services.every-channel-netboot-stage = { description = "every.channel netboot artifact stage"; requires = lib.optionals cfg.ipxe.buildEmbedded [ "every-channel-netboot-ipxe.service" ]; after = [ "network-online.target" ] ++ lib.optionals cfg.ipxe.buildEmbedded [ "every-channel-netboot-ipxe.service" ]; wants = [ "network-online.target" ] ++ lib.optionals cfg.ipxe.buildEmbedded [ "every-channel-netboot-ipxe.service" ]; wantedBy = lib.mkIf cfg.stageOnBoot [ "multi-user.target" ]; path = stageToolchain; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' set -euo pipefail ${mkExport "EVERY_CHANNEL_NETBOOT_ROOT" cfg.rootDir} ${mkExport "EVERY_CHANNEL_NETBOOT_HOSTNAME" cfg.hostname} ${mkExport "EVERY_CHANNEL_NETBOOT_HTTP_PORT" (toString cfg.httpPort)} ${mkExport "EVERY_CHANNEL_FORGE_HOST" cfg.release.host} ${mkExport "EVERY_CHANNEL_FORGE_REPO" cfg.release.repo} ${optionalExport "EVERY_CHANNEL_NETBOOT_RELEASE_TAG" cfg.release.tag} ${optionalExport "EVERY_CHANNEL_NETBOOT_TARBALL" cfg.release.localTarball} ${mkExport "EVERY_CHANNEL_NETBOOT_VERIFY_RELEASE_CHECKSUMS" (boolString cfg.release.verifyChecksums)} ${mkExport "EVERY_CHANNEL_NETBOOT_ALLOW_REMOTE_IPXE" (boolString cfg.ipxe.allowRemoteDownload)} ${mkExport "EVERY_CHANNEL_IPXE_EFI_FILENAME" cfg.tftpBootFilename} ${optionalExport "EVERY_CHANNEL_IPXE_EFI_SHA256" cfg.ipxe.sha256} ${optionalFileExport "EVERY_CHANNEL_FORGE_TOKEN" cfg.release.tokenFile} ${optionalFileExport "EVERY_CHANNEL_NETBOOT_CHAIN_TOKEN" cfg.chainTokenFile} ${lib.optionalString cfg.ipxe.buildEmbedded (mkExport "EVERY_CHANNEL_IPXE_EFI_PATH" "${cfg.rootDir}/tftp/${cfg.tftpBootFilename}")} ${lib.optionalString (!cfg.ipxe.buildEmbedded && cfg.ipxe.path != null) (mkExport "EVERY_CHANNEL_IPXE_EFI_PATH" cfg.ipxe.path)} ${scriptsRoot}/scripts/netboot-stage.sh ''; }; systemd.services.every-channel-netboot = { description = "every.channel netboot HTTP + TFTP service"; requires = [ "every-channel-netboot-stage.service" ]; after = [ "network-online.target" "every-channel-netboot-stage.service" ]; wants = [ "network-online.target" "every-channel-netboot-stage.service" ]; wantedBy = lib.mkIf cfg.stageOnBoot [ "multi-user.target" ]; path = stageToolchain ++ [ pkgs.dnsmasq ]; serviceConfig = { Type = "simple"; Restart = "always"; RestartSec = "5s"; }; script = '' set -euo pipefail ${mkExport "EVERY_CHANNEL_NETBOOT_ROOT" cfg.rootDir} ${mkExport "EVERY_CHANNEL_NETBOOT_LISTEN_IP" cfg.listenIP} ${mkExport "EVERY_CHANNEL_NETBOOT_INTERFACE" cfg.interface} ${mkExport "EVERY_CHANNEL_NETBOOT_HOSTNAME" cfg.hostname} ${mkExport "EVERY_CHANNEL_NETBOOT_HTTP_PORT" (toString cfg.httpPort)} ${mkExport "EVERY_CHANNEL_NETBOOT_PROXY_DHCP" (boolString cfg.proxyDhcp.enable)} ${optionalExport "EVERY_CHANNEL_NETBOOT_PROXY_SUBNET" cfg.proxyDhcp.subnet} ${mkExport "EVERY_CHANNEL_NETBOOT_TFTP_BOOT_FILENAME" cfg.tftpBootFilename} ${mkExport "EVERY_CHANNEL_NETBOOT_HTTP_ALLOWED_CIDRS" (lib.concatStringsSep "," cfg.httpAllowedCIDRs)} ${optionalFileExport "EVERY_CHANNEL_NETBOOT_CHAIN_TOKEN" cfg.chainTokenFile} ${scriptsRoot}/scripts/netboot-serve.sh ''; }; networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.httpPort ]; networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall ( [ 69 ] ++ lib.optionals cfg.proxyDhcp.enable [ 67 4011 ] ); }; }