1{ config, options, pkgs, lib, ... }:
2
3with lib;
4
5let
6
7 cfg = config.services.rspamd;
8 opts = options.services.rspamd;
9
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));
148
149 rspamdConfFile = pkgs.writeText "rspamd.conf"
150 ''
151 .include "$CONFDIR/common.conf"
152
153 options {
154 pidfile = "$RUNDIR/rspamd.pid";
155 .include "$CONFDIR/options.inc"
156 }
157
158 logging {
159 type = "syslog";
160 .include "$CONFDIR/logging.inc"
161 }
162
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)}
174
175 ${cfg.extraConfig}
176 '';
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
190in
191
192{
193
194 ###### interface
195
196 options = {
197
198 services.rspamd = {
199
200 enable = mkEnableOption "Whether to run the rspamd daemon.";
201
202 debug = mkOption {
203 type = types.bool;
204 default = false;
205 description = "Whether to run the rspamd daemon in debug mode.";
206 };
207
208 socketActivation = mkOption {
209 type = types.bool;
210 description = ''
211 Enable systemd socket activation for rspamd.
212 '';
213 };
214
215 workers = mkOption {
216 type = with types; attrsOf (submodule workerOpts);
217 description = ''
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 }
240 '';
241 };
242
243 extraConfig = mkOption {
244 type = types.lines;
245 default = "";
246 description = ''
247 Extra configuration to add at the end of the rspamd configuration
248 file.
249 '';
250 };
251
252 user = mkOption {
253 type = types.string;
254 default = "rspamd";
255 description = ''
256 User to use when no root privileges are required.
257 '';
258 };
259
260 group = mkOption {
261 type = types.string;
262 default = "rspamd";
263 description = ''
264 Group to use when no root privileges are required.
265 '';
266 };
267 };
268 };
269
270
271 ###### implementation
272
273 config = mkIf cfg.enable {
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
282 # Allow users to run 'rspamc' and 'rspamadm'.
283 environment.systemPackages = [ pkgs.rspamd ];
284
285 users.users = singleton {
286 name = cfg.user;
287 description = "rspamd daemon";
288 uid = config.ids.uids.rspamd;
289 group = cfg.group;
290 };
291
292 users.groups = singleton {
293 name = cfg.group;
294 gid = config.ids.gids.rspamd;
295 };
296
297 environment.etc."rspamd.conf".source = rspamdConfFile;
298
299 systemd.services.rspamd = {
300 description = "Rspamd Service";
301
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;
306
307 serviceConfig = {
308 ExecStart = "${pkgs.rspamd}/bin/rspamd ${optionalString cfg.debug "-d"} --user=${cfg.user} --group=${cfg.group} --pid=/run/rspamd.pid -c ${rspamdConfFile} -f";
309 Restart = "always";
310 RuntimeDirectory = "rspamd";
311 PrivateTmp = true;
312 Sockets = mkIf cfg.socketActivation (concatStringsSep " " allSocketNames);
313 };
314
315 preStart = ''
316 ${pkgs.coreutils}/bin/mkdir -p /var/lib/rspamd
317 ${pkgs.coreutils}/bin/chown ${cfg.user}:${cfg.group} /var/lib/rspamd
318 '';
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));
336 };
337 imports = [
338 (mkRenamedOptionModule [ "services" "rspamd" "bindSocket" ] [ "services" "rspamd" "workers" "normal" "bindSockets" ])
339 (mkRenamedOptionModule [ "services" "rspamd" "bindUISocket" ] [ "services" "rspamd" "workers" "controller" "bindSockets" ])
340 ];
341}