diff --git a/nixos/modules/services/mail/rspamd.nix b/nixos/modules/services/mail/rspamd.nix index b80aa48f2c8..09fb587e74b 100644 --- a/nixos/modules/services/mail/rspamd.nix +++ b/nixos/modules/services/mail/rspamd.nix @@ -1,14 +1,152 @@ -{ config, lib, pkgs, ... }: +{ config, options, pkgs, lib, ... }: with lib; let cfg = config.services.rspamd; + opts = options.services.rspamd; - mkBindSockets = socks: concatStringsSep "\n" (map (each: " bind_socket = \"${each}\"") socks); + bindSocketOpts = {options, config, ... }: { + options = { + socket = mkOption { + type = types.str; + example = "localhost:11333"; + description = '' + Socket for this worker to listen on in a format acceptable by rspamd. + ''; + }; + mode = mkOption { + type = types.str; + default = "0644"; + description = "Mode to set on unix socket"; + }; + owner = mkOption { + type = types.str; + default = "${cfg.user}"; + description = "Owner to set on unix socket"; + }; + group = mkOption { + type = types.str; + default = "${cfg.group}"; + description = "Group to set on unix socket"; + }; + rawEntry = mkOption { + type = types.str; + internal = true; + }; + }; + config.rawEntry = let + maybeOption = option: + optionalString options.${option}.isDefined " ${option}=${config.${option}}"; + in + if (!(hasPrefix "/" config.socket)) then "${config.socket}" + else "${config.socket}${maybeOption "mode"}${maybeOption "owner"}${maybeOption "group"}"; + }; - rspamdConfFile = pkgs.writeText "rspamd.conf" + workerOpts = { name, ... }: { + options = { + enable = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Whether to run the rspamd worker."; + }; + name = mkOption { + type = types.nullOr types.str; + default = name; + description = "Name of the worker"; + }; + type = mkOption { + type = types.nullOr (types.enum [ + "normal" "controller" "fuzzy_storage" "proxy" "lua" + ]); + description = "The type of this worker"; + }; + bindSockets = mkOption { + type = types.listOf (types.either types.str (types.submodule bindSocketOpts)); + default = []; + description = '' + List of sockets to listen, in format acceptable by rspamd + ''; + example = [{ + socket = "/run/rspamd.sock"; + mode = "0666"; + owner = "rspamd"; + } "*:11333"]; + apply = value: map (each: if (isString each) + then if (isUnixSocket each) + then {socket = each; owner = cfg.user; group = cfg.group; mode = "0644"; rawEntry = "${each}";} + else {socket = each; rawEntry = "${each}";} + else each) value; + }; + count = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + Number of worker instances to run + ''; + }; + includes = mkOption { + type = types.listOf types.str; + default = []; + description = '' + List of files to include in configuration + ''; + }; + extraConfig = mkOption { + type = types.lines; + default = ""; + description = "Additional entries to put verbatim into worker section of rspamd config file."; + }; + }; + config = mkIf (name == "normal" || name == "controller" || name == "fuzzy") { + type = mkDefault name; + includes = mkDefault [ "$CONFDIR/worker-${name}.inc" ]; + bindSockets = mkDefault (if name == "normal" + then [{ + socket = "/run/rspamd/rspamd.sock"; + mode = "0660"; + owner = cfg.user; + group = cfg.group; + }] + else if name == "controller" + then [ "localhost:11334" ] + else [] ); + }; + }; + + indexOf = default: start: list: e: + if list == [] + then default + else if (head list) == e then start + else (indexOf default (start + (length (listenStreams (head list).socket))) (tail list) e); + + systemdSocket = indexOf (abort "Socket not found") 0 allSockets; + + isUnixSocket = socket: hasPrefix "/" (if (isString socket) then socket else socket.socket); + isPort = hasPrefix "*:"; + isIPv4Socket = hasPrefix "*v4:"; + isIPv6Socket = hasPrefix "*v6:"; + isLocalHost = hasPrefix "localhost:"; + listenStreams = socket: + if (isLocalHost socket) then + let port = (removePrefix "localhost:" socket); + in [ "127.0.0.1:${port}" ] ++ (if config.networking.enableIPv6 then ["[::1]:${port}"] else []) + else if (isIPv6Socket socket) then [removePrefix "*v6:" socket] + else if (isPort socket) then [removePrefix "*:" socket] + else if (isIPv4Socket socket) then + throw "error: IPv4 only socket not supported in rspamd with socket activation" + else if (length (splitString " " socket)) != 1 then + throw "error: string options not supported in rspamd with socket activation" + else [socket]; + + mkBindSockets = enabled: socks: concatStringsSep "\n " (flatten (map (each: + if cfg.socketActivation && enabled != false then + let systemd = (systemdSocket each); + in (imap (idx: e: "bind_socket = \"systemd:${toString (systemd + idx - 1)}\";") (listenStreams each.socket)) + else "bind_socket = \"${each.rawEntry}\";") socks)); + + rspamdConfFile = pkgs.writeText "rspamd.conf" '' .include "$CONFDIR/common.conf" @@ -22,19 +160,33 @@ let .include "$CONFDIR/logging.inc" } - worker { - ${mkBindSockets cfg.bindSocket} - .include "$CONFDIR/worker-normal.inc" - } - - worker { - ${mkBindSockets cfg.bindUISocket} - .include "$CONFDIR/worker-controller.inc" - } + ${concatStringsSep "\n" (mapAttrsToList (name: value: '' + worker ${optionalString (value.name != "normal" && value.name != "controller") "${value.name}"} { + type = "${value.type}"; + ${optionalString (value.enable != null) + "enabled = ${if value.enable != false then "yes" else "no"};"} + ${mkBindSockets value.enable value.bindSockets} + ${optionalString (value.count != null) "count = ${toString value.count};"} + ${concatStringsSep "\n " (map (each: ".include \"${each}\"") value.includes)} + ${value.extraConfig} + } + '') cfg.workers)} ${cfg.extraConfig} ''; + allMappedSockets = flatten (mapAttrsToList (name: value: + if value.enable != false + then imap (idx: each: { + name = "${name}"; + index = idx; + value = each; + }) value.bindSockets + else []) cfg.workers); + allSockets = map (e: e.value) allMappedSockets; + + allSocketNames = map (each: "rspamd-${each.name}-${toString each.index}.socket") allMappedSockets; + in { @@ -48,36 +200,43 @@ in enable = mkEnableOption "Whether to run the rspamd daemon."; debug = mkOption { + type = types.bool; default = false; description = "Whether to run the rspamd daemon in debug mode."; }; - bindSocket = mkOption { - type = types.listOf types.str; - default = [ - "/run/rspamd/rspamd.sock mode=0660 owner=${cfg.user} group=${cfg.group}" - ]; - defaultText = ''[ - "/run/rspamd/rspamd.sock mode=0660 owner=${cfg.user} group=${cfg.group}" - ]''; + socketActivation = mkOption { + type = types.bool; description = '' - List of sockets to listen, in format acceptable by rspamd - ''; - example = '' - bindSocket = [ - "/run/rspamd.sock mode=0666 owner=rspamd" - "*:11333" - ]; + Enable systemd socket activation for rspamd. ''; }; - bindUISocket = mkOption { - type = types.listOf types.str; - default = [ - "localhost:11334" - ]; + workers = mkOption { + type = with types; attrsOf (submodule workerOpts); description = '' - List of sockets for web interface, in format acceptable by rspamd + Attribute set of workers to start. + ''; + default = { + normal = {}; + controller = {}; + }; + example = literalExample '' + { + normal = { + includes = [ "$CONFDIR/worker-normal.inc" ]; + bindSockets = [{ + socket = "/run/rspamd/rspamd.sock"; + mode = "0660"; + owner = "${cfg.user}"; + group = "${cfg.group}"; + }]; + }; + controller = { + includes = [ "$CONFDIR/worker-controller.inc" ]; + bindSockets = [ "[::1]:11334" ]; + }; + } ''; }; @@ -113,6 +272,13 @@ in config = mkIf cfg.enable { + services.rspamd.socketActivation = mkDefault (!opts.bindSocket.isDefined && !opts.bindUISocket.isDefined); + + assertions = [ { + assertion = !cfg.socketActivation || !(opts.bindSocket.isDefined || opts.bindUISocket.isDefined); + message = "Can't use socketActivation for rspamd when using renamed bind socket options"; + } ]; + # Allow users to run 'rspamc' and 'rspamadm'. environment.systemPackages = [ pkgs.rspamd ]; @@ -128,17 +294,22 @@ in gid = config.ids.gids.rspamd; }; + environment.etc."rspamd.conf".source = rspamdConfFile; + systemd.services.rspamd = { description = "Rspamd Service"; - wantedBy = [ "multi-user.target" ]; - after = [ "network.target" ]; + wantedBy = mkIf (!cfg.socketActivation) [ "multi-user.target" ]; + after = [ "network.target" ] ++ + (if cfg.socketActivation then allSocketNames else []); + requires = mkIf cfg.socketActivation allSocketNames; serviceConfig = { ExecStart = "${pkgs.rspamd}/bin/rspamd ${optionalString cfg.debug "-d"} --user=${cfg.user} --group=${cfg.group} --pid=/run/rspamd.pid -c ${rspamdConfFile} -f"; Restart = "always"; RuntimeDirectory = "rspamd"; PrivateTmp = true; + Sockets = mkIf cfg.socketActivation (concatStringsSep " " allSocketNames); }; preStart = '' @@ -146,5 +317,25 @@ in ${pkgs.coreutils}/bin/chown ${cfg.user}:${cfg.group} /var/lib/rspamd ''; }; + systemd.sockets = mkIf cfg.socketActivation + (listToAttrs (map (each: { + name = "rspamd-${each.name}-${toString each.index}"; + value = { + description = "Rspamd socket ${toString each.index} for worker ${each.name}"; + wantedBy = [ "sockets.target" ]; + listenStreams = (listenStreams each.value.socket); + socketConfig = { + BindIPv6Only = mkIf (isIPv6Socket each.value.socket) "ipv6-only"; + Service = "rspamd.service"; + SocketUser = mkIf (isUnixSocket each.value.socket) each.value.owner; + SocketGroup = mkIf (isUnixSocket each.value.socket) each.value.group; + SocketMode = mkIf (isUnixSocket each.value.socket) each.value.mode; + }; + }; + }) allMappedSockets)); }; + imports = [ + (mkRenamedOptionModule [ "services" "rspamd" "bindSocket" ] [ "services" "rspamd" "workers" "normal" "bindSockets" ]) + (mkRenamedOptionModule [ "services" "rspamd" "bindUISocket" ] [ "services" "rspamd" "workers" "controller" "bindSockets" ]) + ]; } diff --git a/nixos/tests/rspamd.nix b/nixos/tests/rspamd.nix index 35e534246b6..6b2e2dd3a53 100644 --- a/nixos/tests/rspamd.nix +++ b/nixos/tests/rspamd.nix @@ -13,11 +13,12 @@ let $machine->succeed("[[ \"\$(stat -c %G ${socket})\" == \"${group}\" ]]"); $machine->succeed("[[ \"\$(stat -c %a ${socket})\" == \"${mode}\" ]]"); ''; - simple = name: enableIPv6: makeTest { + simple = name: socketActivation: enableIPv6: makeTest { name = "rspamd-${name}"; machine = { services.rspamd = { enable = true; + socketActivation = socketActivation; }; networking.enableIPv6 = enableIPv6; }; @@ -29,7 +30,15 @@ let $machine->succeed("id \"rspamd\" >/dev/null"); ${checkSocket "/run/rspamd/rspamd.sock" "rspamd" "rspamd" "660" } sleep 10; + $machine->log($machine->succeed("cat /etc/rspamd.conf")); $machine->log($machine->succeed("systemctl cat rspamd.service")); + ${if socketActivation then '' + $machine->log($machine->succeed("systemctl cat rspamd-controller-1.socket")); + $machine->log($machine->succeed("systemctl cat rspamd-normal-1.socket")); + '' else '' + $machine->fail("systemctl cat rspamd-controller-1.socket"); + $machine->fail("systemctl cat rspamd-normal-1.socket"); + ''} $machine->log($machine->succeed("curl http://localhost:11334/auth")); $machine->log($machine->succeed("curl http://127.0.0.1:11334/auth")); ${optionalString enableIPv6 '' @@ -39,10 +48,12 @@ let }; in { - simple = simple "simple" true; - ipv4only = simple "ipv4only" false; - bindports = makeTest { - name = "rspamd-bindports"; + simple = simple "simple" false true; + ipv4only = simple "ipv4only" false false; + simple-socketActivated = simple "simple-socketActivated" true true; + ipv4only-socketActivated = simple "ipv4only-socketActivated" true false; + deprecated = makeTest { + name = "rspamd-deprecated"; machine = { services.rspamd = { enable = true; @@ -56,6 +67,72 @@ in $machine->waitForFile("/run/rspamd.sock"); ${checkSocket "/run/rspamd.sock" "root" "root" "600" } ${checkSocket "/run/rspamd-worker.sock" "root" "root" "666" } + $machine->log($machine->succeed("cat /etc/rspamd.conf")); + $machine->fail("systemctl cat rspamd-normal-1.socket"); + $machine->log($machine->succeed("rspamc -h /run/rspamd-worker.sock stat")); + $machine->log($machine->succeed("curl --unix-socket /run/rspamd-worker.sock http://localhost/ping")); + ''; + }; + + bindports = makeTest { + name = "rspamd-bindports"; + machine = { + services.rspamd = { + enable = true; + socketActivation = false; + workers.normal.bindSockets = [{ + socket = "/run/rspamd.sock"; + mode = "0600"; + owner = "root"; + group = "root"; + }]; + workers.controller.bindSockets = [{ + socket = "/run/rspamd-worker.sock"; + mode = "0666"; + owner = "root"; + group = "root"; + }]; + }; + }; + + testScript = '' + ${initMachine} + $machine->waitForFile("/run/rspamd.sock"); + ${checkSocket "/run/rspamd.sock" "root" "root" "600" } + ${checkSocket "/run/rspamd-worker.sock" "root" "root" "666" } + $machine->log($machine->succeed("cat /etc/rspamd.conf")); + $machine->fail("systemctl cat rspamd-normal-1.socket"); + $machine->log($machine->succeed("rspamc -h /run/rspamd-worker.sock stat")); + $machine->log($machine->succeed("curl --unix-socket /run/rspamd-worker.sock http://localhost/ping")); + ''; + }; + socketActivated = makeTest { + name = "rspamd-socketActivated"; + machine = { + services.rspamd = { + enable = true; + workers.normal.bindSockets = [{ + socket = "/run/rspamd.sock"; + mode = "0600"; + owner = "root"; + group = "root"; + }]; + workers.controller.bindSockets = [{ + socket = "/run/rspamd-worker.sock"; + mode = "0666"; + owner = "root"; + group = "root"; + }]; + }; + }; + + testScript = '' + startAll + $machine->waitForFile("/run/rspamd.sock"); + ${checkSocket "/run/rspamd.sock" "root" "root" "600" } + ${checkSocket "/run/rspamd-worker.sock" "root" "root" "666" } + $machine->log($machine->succeed("cat /etc/rspamd.conf")); + $machine->log($machine->succeed("systemctl cat rspamd-normal-1.socket")); $machine->log($machine->succeed("rspamc -h /run/rspamd-worker.sock stat")); $machine->log($machine->succeed("curl --unix-socket /run/rspamd-worker.sock http://localhost/ping")); '';