From ad185929c2e9e3c013fea3b90da3b474170c1f2a Mon Sep 17 00:00:00 2001 From: arcnmx Date: Sat, 30 Mar 2024 18:49:54 -0700 Subject: [PATCH] feat(nginx): stream options --- modules/nixos/nginx/listen.nix | 113 ++++++++++++++++++++--- modules/nixos/nginx/ssl.nix | 139 +++++++++++++++++++--------- modules/nixos/nginx/stream.nix | 164 +++++++++++++++++++++++++++++++++ 3 files changed, 362 insertions(+), 54 deletions(-) create mode 100644 modules/nixos/nginx/stream.nix diff --git a/modules/nixos/nginx/listen.nix b/modules/nixos/nginx/listen.nix index e8b4098e..a04da268 100644 --- a/modules/nixos/nginx/listen.nix +++ b/modules/nixos/nginx/listen.nix @@ -6,11 +6,12 @@ }: let inherit (inputs.self.lib.lib) mkAlmostOptionDefault; inherit (lib.options) mkOption mkEnableOption; - inherit (lib.modules) mkIf mkMerge mkOptionDefault mkForce; + inherit (lib.modules) mkIf mkMerge mkBefore mkOptionDefault mkForce; inherit (lib.attrsets) attrValues mapAttrs; - inherit (lib.lists) filter concatMap; + inherit (lib.lists) optional filter concatMap; + inherit (lib.strings) hasPrefix hasInfix; inherit (config.services) nginx; - listenModule = { config, virtualHost, ... }: { + listenModule = { config, virtualHost, listenKind, ... }: { options = with lib.types; { enable = mkEnableOption "this port" // { default = true; @@ -40,19 +41,61 @@ type = bool; default = false; }; + listenParameters = mkOption { + type = listOf str; + internal = true; + }; + listenConfigs = mkOption { + type = listOf (separatedString " "); + internal = true; + }; + listenDirectives = mkOption { + type = lines; + internal = true; + }; }; config = { - enable = mkIf (config.ssl && !virtualHost.ssl.enable) (mkForce false); - port = mkOptionDefault ( + enable = mkMerge [ + (mkIf (config.ssl && !virtualHost.ssl.enable) (mkForce false)) + (mkIf (listenKind == "streamServer" && !config.ssl && virtualHost.ssl.enable && virtualHost.ssl.force != false) (mkForce false)) + ]; + port = mkIf (listenKind == "virtualHost") (mkOptionDefault ( if config.ssl then nginx.defaultSSLListenPort else nginx.defaultHTTPListenPort - ); + )); addresses = mkMerge [ (mkOptionDefault virtualHost.listenAddresses') (mkIf (config.addr != null) (mkAlmostOptionDefault [ config.addr ])) ]; + listenParameters = mkOptionDefault ( + optional config.ssl "ssl" + ++ optional virtualHost.default or false "default_server" + ++ optional virtualHost.reuseport or false "reuseport" + ++ optional config.proxyProtocol or false "proxy_protocol" + ++ config.extraParameters + ); + listenConfigs = let + # TODO: handle quic listener..? + mkListenHost = { addr, port }: let + addr' = if hasInfix ":" addr && !hasPrefix "[" addr then "[${addr}]" else addr; + host = + if addr != null then "${addr'}:${toString port}" + else toString port; + in assert port != null; host; + mkDirective = addr: let + host = mkListenHost { inherit addr; inherit (config) port; }; + in mkMerge ( + [ (mkBefore host) ] + ++ config.listenParameters + ); + in mkOptionDefault (map (mkDirective) config.addresses); + listenDirectives = mkMerge (map (conf: mkOptionDefault "listen ${conf};") config.listenConfigs); }; }; - hostModule = { config, ... }: let + listenType = { specialArgs, modules ? [ ] }: lib.types.submoduleWith { + inherit specialArgs; + modules = [ listenModule ] ++ modules; + }; + hostModule = { nixosConfig, config, ... }: let cfg = attrValues config.listen'; enabledCfg = filter (port: port.enable) cfg; mkListen = listen: addr: let @@ -65,10 +108,11 @@ in { options = with lib.types; { listen' = mkOption { - type = attrsOf (submoduleWith { - modules = [ listenModule ]; + type = attrsOf (listenType { specialArgs = { + inherit nixosConfig; virtualHost = config; + listenKind = "virtualHost"; }; }); default = { }; @@ -89,13 +133,60 @@ )); }; }; + streamServerModule = { nixosConfig, config, ... }: let + enabledListen = filter (port: port.enable) (attrValues config.listen); + in { + options = with lib.types; { + listen = mkOption { + type = attrsOf (listenType { + specialArgs = { + inherit nixosConfig; + virtualHost = config; + streamServer = config; + listenKind = "streamServer"; + }; + }); + default = { }; + }; + listenAddresses = mkOption { + type = nullOr (listOf str); + default = null; + }; + listenAddresses' = mkOption { + type = listOf str; + internal = true; + description = "listenAddresses or defaultListenAddresses if empty"; + }; + reuseport = mkOption { + type = types.bool; + default = false; + description = "only required on one host"; + }; + }; + + config = { + enable = mkIf (config.listen != { } && enabledListen == [ ]) (mkForce false); + listenAddresses' = mkOptionDefault ( + if config.listenAddresses != null then config.listenAddresses else nginx.defaultListenAddresses + ); + streamConfig = mkIf (config.listen != { }) (mkMerge ( + map (listen: mkBefore listen.listenDirectives) enabledListen + )); + }; + }; in { - options = with lib.types; { - services.nginx.virtualHosts = mkOption { + options.services.nginx = with lib.types; { + virtualHosts = mkOption { type = attrsOf (submoduleWith { modules = [ hostModule ]; shorthandOnlyDefinesConfig = true; }); }; + stream.servers = mkOption { + type = attrsOf (submoduleWith { + modules = [ streamServerModule ]; + shorthandOnlyDefinesConfig = false; + }); + }; }; } diff --git a/modules/nixos/nginx/ssl.nix b/modules/nixos/nginx/ssl.nix index dd99f272..6bcbd70e 100644 --- a/modules/nixos/nginx/ssl.nix +++ b/modules/nixos/nginx/ssl.nix @@ -7,8 +7,9 @@ inherit (inputs.self.lib.lib) mkAlmostOptionDefault; inherit (lib.options) mkOption mkEnableOption; inherit (lib.modules) mkIf mkMerge mkDefault mkOptionDefault; + inherit (lib.attrsets) mapAttrsToList; inherit (lib.trivial) warnIf; - inherit (config.services.nginx) virtualHosts; + inherit (config.services) nginx; forceRedirectConfig = virtualHost: '' if ($x_scheme = http) { return ${toString virtualHost.redirectCode} https://$x_forwarded_host$request_uri; @@ -26,42 +27,73 @@ extraConfig = mkIf emitForce (forceRedirectConfig virtualHost); }; }; + sslModule = { config, name, ... }: let + cfg = config.ssl; + in { + options.ssl = with lib.types; { + enable = mkOption { + type = bool; + }; + force = mkOption { + # TODO: "force-nonlocal"? exceptions for tailscale? + type = enum [ false true "only" "reject" ]; + default = false; + }; + forced = mkOption { + type = bool; + readOnly = true; + }; + cert = { + name = mkOption { + type = nullOr str; + default = null; + }; + keyPath = mkOption { + type = nullOr path; + default = null; + }; + path = mkOption { + type = nullOr path; + default = null; + }; + copyFromVhost = mkOption { + type = nullOr str; + default = null; + }; + copyFromStreamServer = mkOption { + type = nullOr str; + default = null; + }; + }; + }; + config = { + ssl = { + enable = mkOptionDefault (cfg.cert.name != null || cfg.cert.keyPath != null); + forced = mkOptionDefault (cfg.force != false && cfg.force != "reject"); + cert = let + mkCopyCert = copyCert: { + name = mkDefault copyCert.name; + keyPath = mkAlmostOptionDefault copyCert.keyPath; + path = mkAlmostOptionDefault copyCert.path; + }; + copyCertVhost = mkCopyCert nginx.virtualHosts.${cfg.cert.copyFromVhost}.ssl.cert; + copyCertStreamServer = mkCopyCert nginx.stream.servers.${cfg.cert.copyFromStreamServer}.ssl.cert; + in mkMerge [ + (mkIf (cfg.cert.copyFromVhost != null) copyCertVhost) + (mkIf (cfg.cert.copyFromStreamServer != null) copyCertStreamServer) + ]; + }; + }; + }; hostModule = { config, name, ... }: let cfg = config.ssl; emitForce = cfg.forced && config.proxied.enabled; in { + imports = [ sslModule ]; options = with lib.types; { ssl = { - enable = mkOption { - type = bool; - }; - force = mkOption { - # TODO: "force-nonlocal"? exceptions for tailscale? - type = enum [ false true "only" "reject" ]; - default = false; - }; - forced = mkOption { - type = bool; - readOnly = true; - }; cert = { enable = mkEnableOption "ssl cert via name.shortServer"; - name = mkOption { - type = nullOr str; - default = null; - }; - keyPath = mkOption { - type = nullOr path; - default = null; - }; - path = mkOption { - type = nullOr path; - default = null; - }; - copyFromVhost = mkOption { - type = nullOr str; - default = null; - }; }; }; locations = mkOption { @@ -73,22 +105,11 @@ }; config = { ssl = { - enable = mkOptionDefault (cfg.cert.name != null || cfg.cert.keyPath != null); - forced = mkOptionDefault (cfg.force != false && cfg.force != "reject"); cert = let certConfig.name = mkIf cfg.cert.enable (warnIf (config.name.shortServer == null) "ssl.cert.enable set but name.shortServer is null" ( mkAlmostOptionDefault config.name.shortServer )); - copyCert = virtualHosts.${cfg.cert.copyFromVhost}.ssl.cert; - otherCertConfig = mkIf (cfg.cert.copyFromVhost != null) { - name = mkDefault copyCert.name; - keyPath = mkAlmostOptionDefault copyCert.keyPath; - path = mkAlmostOptionDefault copyCert.path; - }; - in mkMerge [ - certConfig - otherCertConfig - ]; + in certConfig; }; addSSL = mkIf (cfg.enable && (cfg.force == false || emitForce)) (mkDefault true); forceSSL = mkIf (cfg.enable && cfg.force == true && !emitForce) (mkDefault true); @@ -102,13 +123,45 @@ extraConfig = mkIf emitForce (forceRedirectConfig config); }; }; + upstreamServerModule = { config, nixosConfig, ... }: let + cfg = config.ssl; + in { + imports = [ sslModule ]; + config = { + ssl.cert = let + cert = nixosConfig.security.acme.certs.${cfg.cert.name}; + in { + path = mkIf (cfg.cert.name != null) (mkAlmostOptionDefault "${cert.directory}/fullchain.pem"); + keyPath = mkIf (cfg.cert.name != null) (mkAlmostOptionDefault "${cert.directory}/key.pem"); + }; + #listen.ssl = mkIf cfg.enable { ssl = true; }; + extraConfig = mkMerge [ + (mkIf (cfg.cert.path != null) "ssl_certificate ${cfg.cert.path};") + (mkIf (cfg.cert.keyPath != null) "ssl_certificate_key ${cfg.cert.keyPath};") + ]; + }; + }; in { - options = with lib.types; { - services.nginx.virtualHosts = mkOption { + options.services.nginx = with lib.types; { + virtualHosts = mkOption { type = attrsOf (submoduleWith { modules = [ hostModule ]; shorthandOnlyDefinesConfig = true; }); }; + stream.servers = mkOption { + type = attrsOf (submoduleWith { + modules = [ upstreamServerModule ]; + shorthandOnlyDefinesConfig = false; + }); + }; }; + config.systemd.services.nginx = let + mapStreamServer = server: mkIf (server.enable && server.ssl.enable && server.ssl.cert.name != null) { + wants = [ "acme-finished-${server.ssl.cert.name}.target" ]; + after = [ "acme-selfsigned-${server.ssl.cert.name}.service" ]; + before = [ "acme-${server.ssl.cert.name}.service" ]; + }; + streamServerCerts = mapAttrsToList (_: mapStreamServer) nginx.stream.servers; + in mkIf nginx.enable (mkMerge streamServerCerts); } diff --git a/modules/nixos/nginx/stream.nix b/modules/nixos/nginx/stream.nix new file mode 100644 index 00000000..575c5d5a --- /dev/null +++ b/modules/nixos/nginx/stream.nix @@ -0,0 +1,164 @@ +{ + config, + lib, + ... +}: let + inherit (lib.options) mkOption mkEnableOption; + inherit (lib.modules) mkIf mkMerge mkBefore mkOptionDefault; + inherit (lib.attrsets) mapAttrsToList; + inherit (lib.lists) optional; + inherit (lib.strings) hasPrefix hasInfix; + cfg = config.services.nginx.stream; + upstreamServerModule = {config, name, ...}: { + options = with lib.types; { + enable = mkEnableOption "upstream server" // { + default = true; + }; + addr = mkOption { + type = str; + default = name; + }; + port = mkOption { + type = port; + }; + server = mkOption { + type = str; + example = "unix:/tmp/backend3"; + }; + settings = mkOption { + type = attrsOf (oneOf [ int str ]); + default = { }; + }; + extraConfig = mkOption { + type = str; + default = ""; + }; + serverConfig = mkOption { + type = separatedString " "; + internal = true; + }; + serverDirective = mkOption { + type = str; + internal = true; + }; + }; + config = let + settings = mapAttrsToList (key: value: "${key}=${toString value}") config.settings; + in { + server = let + addr = if hasInfix ":" config.addr && ! hasPrefix "[" config.addr then "[${config.addr}]" else config.addr; + in mkOptionDefault "${addr}:${toString config.port}"; + serverConfig = mkMerge ( + [ (mkBefore config.server) ] + ++ settings + ++ optional (config.extraConfig != "") config.extraConfig + ); + serverDirective = mkOptionDefault "server ${config.serverConfig};"; + }; + }; + upstreamModule = {config, name, nixosConfig, ...}: { + options = with lib.types; let + upstreamServer = submoduleWith { + modules = [ upstreamServerModule ]; + specialArgs = { + inherit nixosConfig; + upstream = config; + }; + }; + in { + enable = mkEnableOption "upstream block" // { + default = true; + }; + name = mkOption { + type = str; + default = name; + }; + servers = mkOption { + type = attrsOf upstreamServer; + }; + extraConfig = mkOption { + type = lines; + default = ""; + }; + streamConfig = mkOption { + type = lines; + internal = true; + }; + upstreamBlock = mkOption { + type = lines; + internal = true; + }; + }; + + config = { + streamConfig = mkMerge ( + mapAttrsToList (_: server: mkIf server.enable server.serverDirective) config.servers + ++ [ config.extraConfig ] + ); + upstreamBlock = mkOptionDefault '' + upstream ${config.name} { + ${config.streamConfig} + } + ''; + }; + }; + serverModule = {config, ...}: { + options = with lib.types; { + enable = mkEnableOption "stream server block" // { + default = true; + }; + extraConfig = mkOption { + type = lines; + default = ""; + }; + streamConfig = mkOption { + type = lines; + internal = true; + }; + serverBlock = mkOption { + type = lines; + internal = true; + }; + }; + + config = { + streamConfig = mkMerge [ + config.extraConfig + ]; + serverBlock = mkOptionDefault '' + server { + ${config.streamConfig} + } + ''; + }; + }; +in { + options.services.nginx.stream = with lib.types; { + servers = mkOption { + type = attrsOf (submoduleWith { + modules = [serverModule]; + shorthandOnlyDefinesConfig = false; + specialArgs = { + nixosConfig = config; + }; + }); + default = { }; + }; + upstreams = mkOption { + type = attrsOf (submoduleWith { + modules = [upstreamModule]; + shorthandOnlyDefinesConfig = false; + specialArgs = { + nixosConfig = config; + }; + }); + default = { }; + }; + }; + config.services.nginx = { + streamConfig = mkMerge ( + mapAttrsToList (_: upstream: mkIf upstream.enable upstream.upstreamBlock) cfg.upstreams + ++ mapAttrsToList (_: server: mkIf server.enable server.serverBlock) cfg.servers + ); + }; +}