{ config, lib, pkgs, ... }: let cfg = config.kyouma.ooklaserver; in { options = { kyouma.ooklaserver = let inherit (lib) mkOption types; in { enable = lib.mkEnableOption "ookla speedtest server"; package = lib.mkPackageOption pkgs "ooklaserver" {}; domain = mkOption { description = "Domain to use."; default = null; type = with types; nullOr nonEmptyStr; }; openFirewall = mkOption { description = "Whether to open the firewall for the specified ports."; default = false; type = types.bool; }; tcpPorts = mkOption { description = '' The server listens on TCP port 5060 and 8080 by default. These ports are required for speedtest.net servers, although more can be added. ''; default = [ 5060 8080 ]; type = with types; listOf port; }; udpPorts = mkOption { description = '' The server listens on UDP port 5060 and 8080 by default. These ports are required for speedtest.net servers, although more can be added. ''; default = [ 5060 8080 ]; type = with types; listOf port; }; settings = mkOption { description = '' OoklaServer configuration written as Nix expression. Comma seperated values should be written as list. ''; default = {}; type = with lib.types; let valueType = nullOr (oneOf [ bool int str (attrsOf valueType) (listOf (oneOf [ port str ])) ]); in valueType; }; }; }; config = lib.mkIf cfg.enable { security.acme.certs.${cfg.domain} = { reloadServices = [ "ooklaserver.service" ]; webroot = "/var/lib/acme/acme-challenge"; }; networking.firewall = lib.mkIf cfg.openFirewall { allowedUDPPorts = cfg.udpPorts; allowedTCPPorts = cfg.tcpPorts; }; kyouma.ooklaserver.settings = let inherit (lib) mkDefault; in { OoklaServer = { inherit (cfg) tcpPorts udpPorts; enableAutoUpdate = false; ssl.useLetsEncrypt = false; useIPv6 = mkDefault true; allowedDomains = mkDefault [ "*.ookla.com" "*.speedtest.net" ]; userAgentFilterEnabled = mkDefault true; workerThreadPool = { capacity = mkDefault 30000; stackSizeBytes = mkDefault 102400; }; ipTracking = { gcIntervalMinutes = mkDefault 5; maxIdleAgeMinutes = mkDefault 35; slidingWindowBucketLengthMinutes = mkDefault 5; metricTopIpCount = mkDefault 5; maxConnPerIp = mkDefault 500; maxConnPerBucketPerIp = mkDefault 20000; }; clientAuthToken.denyInvalid = mkDefault true; websocket.frameSizeLimitBytes = mkDefault 5242880; http.maxHeadersSize = mkDefault 65536; }; openSSL.server = { certificateFile = "/run/credentials/${config.systemd.services.ooklaserver.name}/cert.pem"; privateKeyFile = "/run/credentials/${config.systemd.services.ooklaserver.name}/key.pem"; minimumTLSProtocol = mkDefault "1.2"; }; logging.loggers.app = { name = mkDefault "Application"; channel = { class = mkDefault "ConsoleChannel"; pattern = mkDefault "[%p] %t"; }; level = mkDefault "information"; }; }; systemd.services.ooklaserver = let configFile = let anyToString = arg: if (lib.isBool arg) then lib.boolToString arg else if (lib.isList arg) then lib.concatStringsSep "," (map (val: toString val) arg) else toString arg; in with lib; lib.pipe cfg.settings [ (mapAttrsRecursive (path: val: "${concatStringsSep "." path} = ${anyToString val}")) (collect isString) (concatLines) (pkgs.writeTextDir "bin/OoklaServer.properties") ]; packageWithCfg = pkgs.symlinkJoin { name = "${cfg.package.name}-with-config"; paths = [ cfg.package configFile ]; }; in { description = "Ookla speedtest server daemon"; wantedBy = [ "multi-user.target" ]; wants = [ "network-online.target" ]; serviceConfig = { Type = "simple"; Restart = "always"; User = "ooklaserver"; Group = "ooklaserver"; DynamicUser = true; LoadCredential = [ "cert.pem:${config.security.acme.certs.${cfg.domain}.directory}/cert.pem" "key.pem:${config.security.acme.certs.${cfg.domain}.directory}/key.pem" ]; ExecStart = "${packageWithCfg}/bin/OoklaServer"; WorkingDirectory = packageWithCfg; SyslogIdentifier = "ooklaserver"; ReadOnlyPaths = [ packageWithCfg ]; RestrictSUIDSGID = true; RestrictNamespaces = true; PrivateTmp = true; PrivateDevices = true; PrivateUsers = true; ProtectHostname = true; ProtectClock = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectKernelLogs = true; ProtectControlGroups = true; ProtectSystem = "strict"; ProtectHome = true; ProtectProc = "invisible"; SystemCallArchitectures = "native"; SystemCallFilter = "@system-service"; SystemCallErrorNumber = "EPERM"; LockPersonality = true; NoNewPrivileges = true; }; }; }; }