feat(nginx): stream options

This commit is contained in:
arcnmx 2024-03-30 18:49:54 -07:00
parent 071b4aa9ca
commit ad185929c2
3 changed files with 362 additions and 54 deletions

View file

@ -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;
});
};
};
}

View file

@ -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);
}

View file

@ -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
);
};
}