nixos/rspamd: options for worker configuration and socket activation

+308 -40
+226 -35
nixos/modules/services/mail/rspamd.nix
··· 1 - { config, lib, pkgs, ... }: 1 + { config, options, pkgs, lib, ... }: 2 2 3 3 with lib; 4 4 5 5 let 6 6 7 7 cfg = config.services.rspamd; 8 + opts = options.services.rspamd; 8 9 9 - mkBindSockets = socks: concatStringsSep "\n" (map (each: " bind_socket = \"${each}\"") socks); 10 + bindSocketOpts = {options, config, ... }: { 11 + options = { 12 + socket = mkOption { 13 + type = types.str; 14 + example = "localhost:11333"; 15 + description = '' 16 + Socket for this worker to listen on in a format acceptable by rspamd. 17 + ''; 18 + }; 19 + mode = mkOption { 20 + type = types.str; 21 + default = "0644"; 22 + description = "Mode to set on unix socket"; 23 + }; 24 + owner = mkOption { 25 + type = types.str; 26 + default = "${cfg.user}"; 27 + description = "Owner to set on unix socket"; 28 + }; 29 + group = mkOption { 30 + type = types.str; 31 + default = "${cfg.group}"; 32 + description = "Group to set on unix socket"; 33 + }; 34 + rawEntry = mkOption { 35 + type = types.str; 36 + internal = true; 37 + }; 38 + }; 39 + config.rawEntry = let 40 + maybeOption = option: 41 + optionalString options.${option}.isDefined " ${option}=${config.${option}}"; 42 + in 43 + if (!(hasPrefix "/" config.socket)) then "${config.socket}" 44 + else "${config.socket}${maybeOption "mode"}${maybeOption "owner"}${maybeOption "group"}"; 45 + }; 46 + 47 + workerOpts = { name, ... }: { 48 + options = { 49 + enable = mkOption { 50 + type = types.nullOr types.bool; 51 + default = null; 52 + description = "Whether to run the rspamd worker."; 53 + }; 54 + name = mkOption { 55 + type = types.nullOr types.str; 56 + default = name; 57 + description = "Name of the worker"; 58 + }; 59 + type = mkOption { 60 + type = types.nullOr (types.enum [ 61 + "normal" "controller" "fuzzy_storage" "proxy" "lua" 62 + ]); 63 + description = "The type of this worker"; 64 + }; 65 + bindSockets = mkOption { 66 + type = types.listOf (types.either types.str (types.submodule bindSocketOpts)); 67 + default = []; 68 + description = '' 69 + List of sockets to listen, in format acceptable by rspamd 70 + ''; 71 + example = [{ 72 + socket = "/run/rspamd.sock"; 73 + mode = "0666"; 74 + owner = "rspamd"; 75 + } "*:11333"]; 76 + apply = value: map (each: if (isString each) 77 + then if (isUnixSocket each) 78 + then {socket = each; owner = cfg.user; group = cfg.group; mode = "0644"; rawEntry = "${each}";} 79 + else {socket = each; rawEntry = "${each}";} 80 + else each) value; 81 + }; 82 + count = mkOption { 83 + type = types.nullOr types.int; 84 + default = null; 85 + description = '' 86 + Number of worker instances to run 87 + ''; 88 + }; 89 + includes = mkOption { 90 + type = types.listOf types.str; 91 + default = []; 92 + description = '' 93 + List of files to include in configuration 94 + ''; 95 + }; 96 + extraConfig = mkOption { 97 + type = types.lines; 98 + default = ""; 99 + description = "Additional entries to put verbatim into worker section of rspamd config file."; 100 + }; 101 + }; 102 + config = mkIf (name == "normal" || name == "controller" || name == "fuzzy") { 103 + type = mkDefault name; 104 + includes = mkDefault [ "$CONFDIR/worker-${name}.inc" ]; 105 + bindSockets = mkDefault (if name == "normal" 106 + then [{ 107 + socket = "/run/rspamd/rspamd.sock"; 108 + mode = "0660"; 109 + owner = cfg.user; 110 + group = cfg.group; 111 + }] 112 + else if name == "controller" 113 + then [ "localhost:11334" ] 114 + else [] ); 115 + }; 116 + }; 117 + 118 + indexOf = default: start: list: e: 119 + if list == [] 120 + then default 121 + else if (head list) == e then start 122 + else (indexOf default (start + (length (listenStreams (head list).socket))) (tail list) e); 123 + 124 + systemdSocket = indexOf (abort "Socket not found") 0 allSockets; 125 + 126 + isUnixSocket = socket: hasPrefix "/" (if (isString socket) then socket else socket.socket); 127 + isPort = hasPrefix "*:"; 128 + isIPv4Socket = hasPrefix "*v4:"; 129 + isIPv6Socket = hasPrefix "*v6:"; 130 + isLocalHost = hasPrefix "localhost:"; 131 + listenStreams = socket: 132 + if (isLocalHost socket) then 133 + let port = (removePrefix "localhost:" socket); 134 + in [ "127.0.0.1:${port}" ] ++ (if config.networking.enableIPv6 then ["[::1]:${port}"] else []) 135 + else if (isIPv6Socket socket) then [removePrefix "*v6:" socket] 136 + else if (isPort socket) then [removePrefix "*:" socket] 137 + else if (isIPv4Socket socket) then 138 + throw "error: IPv4 only socket not supported in rspamd with socket activation" 139 + else if (length (splitString " " socket)) != 1 then 140 + throw "error: string options not supported in rspamd with socket activation" 141 + else [socket]; 142 + 143 + mkBindSockets = enabled: socks: concatStringsSep "\n " (flatten (map (each: 144 + if cfg.socketActivation && enabled != false then 145 + let systemd = (systemdSocket each); 146 + in (imap (idx: e: "bind_socket = \"systemd:${toString (systemd + idx - 1)}\";") (listenStreams each.socket)) 147 + else "bind_socket = \"${each.rawEntry}\";") socks)); 10 148 11 - rspamdConfFile = pkgs.writeText "rspamd.conf" 149 + rspamdConfFile = pkgs.writeText "rspamd.conf" 12 150 '' 13 151 .include "$CONFDIR/common.conf" 14 152 ··· 22 160 .include "$CONFDIR/logging.inc" 23 161 } 24 162 25 - worker { 26 - ${mkBindSockets cfg.bindSocket} 27 - .include "$CONFDIR/worker-normal.inc" 28 - } 29 - 30 - worker { 31 - ${mkBindSockets cfg.bindUISocket} 32 - .include "$CONFDIR/worker-controller.inc" 33 - } 163 + ${concatStringsSep "\n" (mapAttrsToList (name: value: '' 164 + worker ${optionalString (value.name != "normal" && value.name != "controller") "${value.name}"} { 165 + type = "${value.type}"; 166 + ${optionalString (value.enable != null) 167 + "enabled = ${if value.enable != false then "yes" else "no"};"} 168 + ${mkBindSockets value.enable value.bindSockets} 169 + ${optionalString (value.count != null) "count = ${toString value.count};"} 170 + ${concatStringsSep "\n " (map (each: ".include \"${each}\"") value.includes)} 171 + ${value.extraConfig} 172 + } 173 + '') cfg.workers)} 34 174 35 175 ${cfg.extraConfig} 36 176 ''; 37 177 178 + allMappedSockets = flatten (mapAttrsToList (name: value: 179 + if value.enable != false 180 + then imap (idx: each: { 181 + name = "${name}"; 182 + index = idx; 183 + value = each; 184 + }) value.bindSockets 185 + else []) cfg.workers); 186 + allSockets = map (e: e.value) allMappedSockets; 187 + 188 + allSocketNames = map (each: "rspamd-${each.name}-${toString each.index}.socket") allMappedSockets; 189 + 38 190 in 39 191 40 192 { ··· 48 200 enable = mkEnableOption "Whether to run the rspamd daemon."; 49 201 50 202 debug = mkOption { 203 + type = types.bool; 51 204 default = false; 52 205 description = "Whether to run the rspamd daemon in debug mode."; 53 206 }; 54 207 55 - bindSocket = mkOption { 56 - type = types.listOf types.str; 57 - default = [ 58 - "/run/rspamd/rspamd.sock mode=0660 owner=${cfg.user} group=${cfg.group}" 59 - ]; 60 - defaultText = ''[ 61 - "/run/rspamd/rspamd.sock mode=0660 owner=${cfg.user} group=${cfg.group}" 62 - ]''; 208 + socketActivation = mkOption { 209 + type = types.bool; 63 210 description = '' 64 - List of sockets to listen, in format acceptable by rspamd 65 - ''; 66 - example = '' 67 - bindSocket = [ 68 - "/run/rspamd.sock mode=0666 owner=rspamd" 69 - "*:11333" 70 - ]; 211 + Enable systemd socket activation for rspamd. 71 212 ''; 72 213 }; 73 214 74 - bindUISocket = mkOption { 75 - type = types.listOf types.str; 76 - default = [ 77 - "localhost:11334" 78 - ]; 215 + workers = mkOption { 216 + type = with types; attrsOf (submodule workerOpts); 79 217 description = '' 80 - List of sockets for web interface, in format acceptable by rspamd 218 + Attribute set of workers to start. 219 + ''; 220 + default = { 221 + normal = {}; 222 + controller = {}; 223 + }; 224 + example = literalExample '' 225 + { 226 + normal = { 227 + includes = [ "$CONFDIR/worker-normal.inc" ]; 228 + bindSockets = [{ 229 + socket = "/run/rspamd/rspamd.sock"; 230 + mode = "0660"; 231 + owner = "${cfg.user}"; 232 + group = "${cfg.group}"; 233 + }]; 234 + }; 235 + controller = { 236 + includes = [ "$CONFDIR/worker-controller.inc" ]; 237 + bindSockets = [ "[::1]:11334" ]; 238 + }; 239 + } 81 240 ''; 82 241 }; 83 242 ··· 113 272 114 273 config = mkIf cfg.enable { 115 274 275 + services.rspamd.socketActivation = mkDefault (!opts.bindSocket.isDefined && !opts.bindUISocket.isDefined); 276 + 277 + assertions = [ { 278 + assertion = !cfg.socketActivation || !(opts.bindSocket.isDefined || opts.bindUISocket.isDefined); 279 + message = "Can't use socketActivation for rspamd when using renamed bind socket options"; 280 + } ]; 281 + 116 282 # Allow users to run 'rspamc' and 'rspamadm'. 117 283 environment.systemPackages = [ pkgs.rspamd ]; 118 284 ··· 128 294 gid = config.ids.gids.rspamd; 129 295 }; 130 296 297 + environment.etc."rspamd.conf".source = rspamdConfFile; 298 + 131 299 systemd.services.rspamd = { 132 300 description = "Rspamd Service"; 133 301 134 - wantedBy = [ "multi-user.target" ]; 135 - after = [ "network.target" ]; 302 + wantedBy = mkIf (!cfg.socketActivation) [ "multi-user.target" ]; 303 + after = [ "network.target" ] ++ 304 + (if cfg.socketActivation then allSocketNames else []); 305 + requires = mkIf cfg.socketActivation allSocketNames; 136 306 137 307 serviceConfig = { 138 308 ExecStart = "${pkgs.rspamd}/bin/rspamd ${optionalString cfg.debug "-d"} --user=${cfg.user} --group=${cfg.group} --pid=/run/rspamd.pid -c ${rspamdConfFile} -f"; 139 309 Restart = "always"; 140 310 RuntimeDirectory = "rspamd"; 141 311 PrivateTmp = true; 312 + Sockets = mkIf cfg.socketActivation (concatStringsSep " " allSocketNames); 142 313 }; 143 314 144 315 preStart = '' ··· 146 317 ${pkgs.coreutils}/bin/chown ${cfg.user}:${cfg.group} /var/lib/rspamd 147 318 ''; 148 319 }; 320 + systemd.sockets = mkIf cfg.socketActivation 321 + (listToAttrs (map (each: { 322 + name = "rspamd-${each.name}-${toString each.index}"; 323 + value = { 324 + description = "Rspamd socket ${toString each.index} for worker ${each.name}"; 325 + wantedBy = [ "sockets.target" ]; 326 + listenStreams = (listenStreams each.value.socket); 327 + socketConfig = { 328 + BindIPv6Only = mkIf (isIPv6Socket each.value.socket) "ipv6-only"; 329 + Service = "rspamd.service"; 330 + SocketUser = mkIf (isUnixSocket each.value.socket) each.value.owner; 331 + SocketGroup = mkIf (isUnixSocket each.value.socket) each.value.group; 332 + SocketMode = mkIf (isUnixSocket each.value.socket) each.value.mode; 333 + }; 334 + }; 335 + }) allMappedSockets)); 149 336 }; 337 + imports = [ 338 + (mkRenamedOptionModule [ "services" "rspamd" "bindSocket" ] [ "services" "rspamd" "workers" "normal" "bindSockets" ]) 339 + (mkRenamedOptionModule [ "services" "rspamd" "bindUISocket" ] [ "services" "rspamd" "workers" "controller" "bindSockets" ]) 340 + ]; 150 341 }
+82 -5
nixos/tests/rspamd.nix
··· 13 13 $machine->succeed("[[ \"\$(stat -c %G ${socket})\" == \"${group}\" ]]"); 14 14 $machine->succeed("[[ \"\$(stat -c %a ${socket})\" == \"${mode}\" ]]"); 15 15 ''; 16 - simple = name: enableIPv6: makeTest { 16 + simple = name: socketActivation: enableIPv6: makeTest { 17 17 name = "rspamd-${name}"; 18 18 machine = { 19 19 services.rspamd = { 20 20 enable = true; 21 + socketActivation = socketActivation; 21 22 }; 22 23 networking.enableIPv6 = enableIPv6; 23 24 }; ··· 29 30 $machine->succeed("id \"rspamd\" >/dev/null"); 30 31 ${checkSocket "/run/rspamd/rspamd.sock" "rspamd" "rspamd" "660" } 31 32 sleep 10; 33 + $machine->log($machine->succeed("cat /etc/rspamd.conf")); 32 34 $machine->log($machine->succeed("systemctl cat rspamd.service")); 35 + ${if socketActivation then '' 36 + $machine->log($machine->succeed("systemctl cat rspamd-controller-1.socket")); 37 + $machine->log($machine->succeed("systemctl cat rspamd-normal-1.socket")); 38 + '' else '' 39 + $machine->fail("systemctl cat rspamd-controller-1.socket"); 40 + $machine->fail("systemctl cat rspamd-normal-1.socket"); 41 + ''} 33 42 $machine->log($machine->succeed("curl http://localhost:11334/auth")); 34 43 $machine->log($machine->succeed("curl http://127.0.0.1:11334/auth")); 35 44 ${optionalString enableIPv6 '' ··· 39 48 }; 40 49 in 41 50 { 42 - simple = simple "simple" true; 43 - ipv4only = simple "ipv4only" false; 51 + simple = simple "simple" false true; 52 + ipv4only = simple "ipv4only" false false; 53 + simple-socketActivated = simple "simple-socketActivated" true true; 54 + ipv4only-socketActivated = simple "ipv4only-socketActivated" true false; 55 + deprecated = makeTest { 56 + name = "rspamd-deprecated"; 57 + machine = { 58 + services.rspamd = { 59 + enable = true; 60 + bindSocket = [ "/run/rspamd.sock mode=0600 user=root group=root" ]; 61 + bindUISocket = [ "/run/rspamd-worker.sock mode=0666 user=root group=root" ]; 62 + }; 63 + }; 64 + 65 + testScript = '' 66 + ${initMachine} 67 + $machine->waitForFile("/run/rspamd.sock"); 68 + ${checkSocket "/run/rspamd.sock" "root" "root" "600" } 69 + ${checkSocket "/run/rspamd-worker.sock" "root" "root" "666" } 70 + $machine->log($machine->succeed("cat /etc/rspamd.conf")); 71 + $machine->fail("systemctl cat rspamd-normal-1.socket"); 72 + $machine->log($machine->succeed("rspamc -h /run/rspamd-worker.sock stat")); 73 + $machine->log($machine->succeed("curl --unix-socket /run/rspamd-worker.sock http://localhost/ping")); 74 + ''; 75 + }; 76 + 44 77 bindports = makeTest { 45 78 name = "rspamd-bindports"; 46 79 machine = { 47 80 services.rspamd = { 48 81 enable = true; 49 - bindSocket = [ "/run/rspamd.sock mode=0600 user=root group=root" ]; 50 - bindUISocket = [ "/run/rspamd-worker.sock mode=0666 user=root group=root" ]; 82 + socketActivation = false; 83 + workers.normal.bindSockets = [{ 84 + socket = "/run/rspamd.sock"; 85 + mode = "0600"; 86 + owner = "root"; 87 + group = "root"; 88 + }]; 89 + workers.controller.bindSockets = [{ 90 + socket = "/run/rspamd-worker.sock"; 91 + mode = "0666"; 92 + owner = "root"; 93 + group = "root"; 94 + }]; 51 95 }; 52 96 }; 53 97 ··· 56 100 $machine->waitForFile("/run/rspamd.sock"); 57 101 ${checkSocket "/run/rspamd.sock" "root" "root" "600" } 58 102 ${checkSocket "/run/rspamd-worker.sock" "root" "root" "666" } 103 + $machine->log($machine->succeed("cat /etc/rspamd.conf")); 104 + $machine->fail("systemctl cat rspamd-normal-1.socket"); 105 + $machine->log($machine->succeed("rspamc -h /run/rspamd-worker.sock stat")); 106 + $machine->log($machine->succeed("curl --unix-socket /run/rspamd-worker.sock http://localhost/ping")); 107 + ''; 108 + }; 109 + socketActivated = makeTest { 110 + name = "rspamd-socketActivated"; 111 + machine = { 112 + services.rspamd = { 113 + enable = true; 114 + workers.normal.bindSockets = [{ 115 + socket = "/run/rspamd.sock"; 116 + mode = "0600"; 117 + owner = "root"; 118 + group = "root"; 119 + }]; 120 + workers.controller.bindSockets = [{ 121 + socket = "/run/rspamd-worker.sock"; 122 + mode = "0666"; 123 + owner = "root"; 124 + group = "root"; 125 + }]; 126 + }; 127 + }; 128 + 129 + testScript = '' 130 + startAll 131 + $machine->waitForFile("/run/rspamd.sock"); 132 + ${checkSocket "/run/rspamd.sock" "root" "root" "600" } 133 + ${checkSocket "/run/rspamd-worker.sock" "root" "root" "666" } 134 + $machine->log($machine->succeed("cat /etc/rspamd.conf")); 135 + $machine->log($machine->succeed("systemctl cat rspamd-normal-1.socket")); 59 136 $machine->log($machine->succeed("rspamc -h /run/rspamd-worker.sock stat")); 60 137 $machine->log($machine->succeed("curl --unix-socket /run/rspamd-worker.sock http://localhost/ping")); 61 138 '';