1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11 cfg = config.services.murmur;
12 forking = cfg.logFile != null;
13 configFile = pkgs.writeText "murmurd.ini" ''
14 database=${cfg.stateDir}/murmur.sqlite
15 dbDriver=QSQLITE
16
17 autobanAttempts=${toString cfg.autobanAttempts}
18 autobanTimeframe=${toString cfg.autobanTimeframe}
19 autobanTime=${toString cfg.autobanTime}
20
21 logfile=${optionalString (cfg.logFile != null) cfg.logFile}
22 ${optionalString forking "pidfile=/run/murmur/murmurd.pid"}
23
24 welcometext="${cfg.welcometext}"
25 port=${toString cfg.port}
26
27 ${optionalString (cfg.hostName != "") "host=${cfg.hostName}"}
28 ${optionalString (cfg.password != "") "serverpassword=${cfg.password}"}
29
30 bandwidth=${toString cfg.bandwidth}
31 users=${toString cfg.users}
32
33 textmessagelength=${toString cfg.textMsgLength}
34 imagemessagelength=${toString cfg.imgMsgLength}
35 allowhtml=${boolToString cfg.allowHtml}
36 logdays=${toString cfg.logDays}
37 bonjour=${boolToString cfg.bonjour}
38 sendversion=${boolToString cfg.sendVersion}
39
40 ${optionalString (cfg.registerName != "") "registerName=${cfg.registerName}"}
41 ${optionalString (cfg.registerPassword != "") "registerPassword=${cfg.registerPassword}"}
42 ${optionalString (cfg.registerUrl != "") "registerUrl=${cfg.registerUrl}"}
43 ${optionalString (cfg.registerHostname != "") "registerHostname=${cfg.registerHostname}"}
44
45 certrequired=${boolToString cfg.clientCertRequired}
46 ${optionalString (cfg.sslCert != "") "sslCert=${cfg.sslCert}"}
47 ${optionalString (cfg.sslKey != "") "sslKey=${cfg.sslKey}"}
48 ${optionalString (cfg.sslCa != "") "sslCA=${cfg.sslCa}"}
49
50 ${optionalString (cfg.dbus != null) "dbus=${cfg.dbus}"}
51
52 ${cfg.extraConfig}
53 '';
54in
55{
56 imports = [
57 (mkRenamedOptionModule [ "services" "murmur" "welcome" ] [ "services" "murmur" "welcometext" ])
58 (mkRemovedOptionModule [ "services" "murmur" "pidfile" ] "Hardcoded to /run/murmur/murmurd.pid now")
59 ];
60
61 options = {
62 services.murmur = {
63 enable = mkOption {
64 type = types.bool;
65 default = false;
66 description = "If enabled, start the Murmur Mumble server.";
67 };
68
69 openFirewall = mkOption {
70 type = types.bool;
71 default = false;
72 description = ''
73 Open ports in the firewall for the Murmur Mumble server.
74 '';
75 };
76
77 user = mkOption {
78 type = types.str;
79 default = "murmur";
80 description = ''
81 The name of an existing user to use to run the service.
82 If not specified, the default user will be created.
83 '';
84 };
85
86 group = mkOption {
87 type = types.str;
88 default = "murmur";
89 description = ''
90 The name of an existing group to use to run the service.
91 If not specified, the default group will be created.
92 '';
93 };
94
95 stateDir = mkOption {
96 type = types.path;
97 default = "/var/lib/murmur";
98 description = ''
99 Directory to store data for the server.
100 '';
101 };
102
103 autobanAttempts = mkOption {
104 type = types.int;
105 default = 10;
106 description = ''
107 Number of attempts a client is allowed to make in
108 `autobanTimeframe` seconds, before being
109 banned for `autobanTime`.
110 '';
111 };
112
113 autobanTimeframe = mkOption {
114 type = types.int;
115 default = 120;
116 description = ''
117 Timeframe in which a client can connect without being banned
118 for repeated attempts (in seconds).
119 '';
120 };
121
122 autobanTime = mkOption {
123 type = types.int;
124 default = 300;
125 description = "The amount of time an IP ban lasts (in seconds).";
126 };
127
128 logFile = mkOption {
129 type = types.nullOr types.path;
130 default = null;
131 example = "/var/log/murmur/murmurd.log";
132 description = "Path to the log file for Murmur daemon. Empty means log to journald.";
133 };
134
135 welcometext = mkOption {
136 type = types.str;
137 default = "";
138 description = "Welcome message for connected clients.";
139 };
140
141 port = mkOption {
142 type = types.port;
143 default = 64738;
144 description = "Ports to bind to (UDP and TCP).";
145 };
146
147 hostName = mkOption {
148 type = types.str;
149 default = "";
150 description = "Host to bind to. Defaults binding on all addresses.";
151 };
152
153 package = mkPackageOption pkgs "murmur" { };
154
155 password = mkOption {
156 type = types.str;
157 default = "";
158 description = "Required password to join server, if specified.";
159 };
160
161 bandwidth = mkOption {
162 type = types.int;
163 default = 72000;
164 description = ''
165 Maximum bandwidth (in bits per second) that clients may send
166 speech at.
167 '';
168 };
169
170 users = mkOption {
171 type = types.int;
172 default = 100;
173 description = "Maximum number of concurrent clients allowed.";
174 };
175
176 textMsgLength = mkOption {
177 type = types.int;
178 default = 5000;
179 description = "Max length of text messages. Set 0 for no limit.";
180 };
181
182 imgMsgLength = mkOption {
183 type = types.int;
184 default = 131072;
185 description = "Max length of image messages. Set 0 for no limit.";
186 };
187
188 allowHtml = mkOption {
189 type = types.bool;
190 default = true;
191 description = ''
192 Allow HTML in client messages, comments, and channel
193 descriptions.
194 '';
195 };
196
197 logDays = mkOption {
198 type = types.int;
199 default = 31;
200 description = ''
201 How long to store RPC logs for in the database. Set 0 to
202 keep logs forever, or -1 to disable DB logging.
203 '';
204 };
205
206 bonjour = mkOption {
207 type = types.bool;
208 default = false;
209 description = ''
210 Enable Bonjour auto-discovery, which allows clients over
211 your LAN to automatically discover Murmur servers.
212 '';
213 };
214
215 sendVersion = mkOption {
216 type = types.bool;
217 default = true;
218 description = "Send Murmur version in UDP response.";
219 };
220
221 registerName = mkOption {
222 type = types.str;
223 default = "";
224 description = ''
225 Public server registration name, and also the name of the
226 Root channel. Even if you don't publicly register your
227 server, you probably still want to set this.
228 '';
229 };
230
231 registerPassword = mkOption {
232 type = types.str;
233 default = "";
234 description = ''
235 Public server registry password, used authenticate your
236 server to the registry to prevent impersonation; required for
237 subsequent registry updates.
238 '';
239 };
240
241 registerUrl = mkOption {
242 type = types.str;
243 default = "";
244 description = "URL website for your server.";
245 };
246
247 registerHostname = mkOption {
248 type = types.str;
249 default = "";
250 description = ''
251 DNS hostname where your server can be reached. This is only
252 needed if you want your server to be accessed by its
253 hostname and not IP - but the name *must* resolve on the
254 internet properly.
255 '';
256 };
257
258 clientCertRequired = mkOption {
259 type = types.bool;
260 default = false;
261 description = "Require clients to authenticate via certificates.";
262 };
263
264 sslCert = mkOption {
265 type = types.str;
266 default = "";
267 description = "Path to your SSL certificate.";
268 };
269
270 sslKey = mkOption {
271 type = types.str;
272 default = "";
273 description = "Path to your SSL key.";
274 };
275
276 sslCa = mkOption {
277 type = types.str;
278 default = "";
279 description = "Path to your SSL CA certificate.";
280 };
281
282 extraConfig = mkOption {
283 type = types.lines;
284 default = "";
285 description = "Extra configuration to put into murmur.ini.";
286 };
287
288 environmentFile = mkOption {
289 type = types.nullOr types.path;
290 default = null;
291 example = literalExpression ''"''${config.services.murmur.stateDir}/murmurd.env"'';
292 description = ''
293 Environment file as defined in {manpage}`systemd.exec(5)`.
294
295 Secrets may be passed to the service without adding them to the world-readable
296 Nix store, by specifying placeholder variables as the option value in Nix and
297 setting these variables accordingly in the environment file.
298
299 ```
300 # snippet of murmur-related config
301 services.murmur.password = "$MURMURD_PASSWORD";
302 ```
303
304 ```
305 # content of the environment file
306 MURMURD_PASSWORD=verysecretpassword
307 ```
308
309 Note that this file needs to be available on the host on which
310 `murmur` is running.
311 '';
312 };
313
314 dbus = mkOption {
315 type = types.enum [
316 null
317 "session"
318 "system"
319 ];
320 default = null;
321 description = "Enable D-Bus remote control. Set to the bus you want Murmur to connect to.";
322 };
323 };
324 };
325
326 config = mkIf cfg.enable {
327 users.users.murmur = mkIf (cfg.user == "murmur") {
328 description = "Murmur Service user";
329 home = cfg.stateDir;
330 createHome = true;
331 uid = config.ids.uids.murmur;
332 group = cfg.group;
333 };
334 users.groups.murmur = mkIf (cfg.group == "murmur") {
335 gid = config.ids.gids.murmur;
336 };
337
338 networking.firewall = mkIf cfg.openFirewall {
339 allowedTCPPorts = [ cfg.port ];
340 allowedUDPPorts = [ cfg.port ];
341 };
342
343 systemd.services.murmur = {
344 description = "Murmur Chat Service";
345 wantedBy = [ "multi-user.target" ];
346 after = [ "network.target" ];
347 preStart = ''
348 ${pkgs.envsubst}/bin/envsubst \
349 -o /run/murmur/murmurd.ini \
350 -i ${configFile}
351 '';
352
353 serviceConfig = {
354 # murmurd doesn't fork when logging to the console.
355 Type = if forking then "forking" else "simple";
356 PIDFile = mkIf forking "/run/murmur/murmurd.pid";
357 EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
358 ExecStart = "${cfg.package}/bin/mumble-server -ini /run/murmur/murmurd.ini";
359 Restart = "always";
360 RuntimeDirectory = "murmur";
361 RuntimeDirectoryMode = "0700";
362 User = cfg.user;
363 Group = cfg.group;
364
365 # service hardening
366 AmbientCapabilities = "CAP_NET_BIND_SERVICE";
367 CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
368 LockPersonality = true;
369 MemoryDenyWriteExecute = true;
370 NoNewPrivileges = true;
371 PrivateDevices = true;
372 PrivateTmp = true;
373 ProtectClock = true;
374 ProtectControlGroups = true;
375 ProtectHome = true;
376 ProtectHostname = true;
377 ProtectKernelLogs = true;
378 ProtectKernelModules = true;
379 ProtectKernelTunables = true;
380 ProtectSystem = "full";
381 RestrictAddressFamilies = "~AF_PACKET AF_NETLINK";
382 RestrictNamespaces = true;
383 RestrictSUIDSGID = true;
384 RestrictRealtime = true;
385 SystemCallArchitectures = "native";
386 SystemCallFilter = "@system-service";
387 UMask = 27;
388 };
389 };
390
391 # currently not included in upstream package, addition requested at
392 # https://github.com/mumble-voip/mumble/issues/6078
393 services.dbus.packages = mkIf (cfg.dbus == "system") [
394 (pkgs.writeTextFile {
395 name = "murmur-dbus-policy";
396 text = ''
397 <!DOCTYPE busconfig PUBLIC
398 "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
399 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
400 <busconfig>
401 <policy user="${cfg.user}">
402 <allow own="net.sourceforge.mumble.murmur"/>
403 </policy>
404
405 <policy context="default">
406 <allow send_destination="net.sourceforge.mumble.murmur"/>
407 <allow receive_sender="net.sourceforge.mumble.murmur"/>
408 </policy>
409 </busconfig>
410 '';
411 destination = "/share/dbus-1/system.d/murmur.conf";
412 })
413 ];
414
415 security.apparmor.policies."bin.mumble-server".profile =
416 ''
417 include <tunables/global>
418
419 ${cfg.package}/bin/{mumble-server,.mumble-server-wrapped} {
420 include <abstractions/base>
421 include <abstractions/nameservice>
422 include <abstractions/ssl_certs>
423 include "${pkgs.apparmorRulesFromClosure { name = "mumble-server"; } cfg.package}"
424 pix ${cfg.package}/bin/.mumble-server-wrapped,
425
426 r ${config.environment.etc."os-release".source},
427 r ${config.environment.etc."lsb-release".source},
428 owner rwk ${cfg.stateDir}/murmur.sqlite,
429 owner rw ${cfg.stateDir}/murmur.sqlite-journal,
430 owner r ${cfg.stateDir}/,
431 r /run/murmur/murmurd.pid,
432 r /run/murmur/murmurd.ini,
433 r ${configFile},
434 ''
435 + optionalString (cfg.logFile != null) ''
436 rw ${cfg.logFile},
437 ''
438 + optionalString (cfg.sslCert != "") ''
439 r ${cfg.sslCert},
440 ''
441 + optionalString (cfg.sslKey != "") ''
442 r ${cfg.sslKey},
443 ''
444 + optionalString (cfg.sslCa != "") ''
445 r ${cfg.sslCa},
446 ''
447 + optionalString (cfg.dbus != null) ''
448 dbus bus=${cfg.dbus}
449 ''
450 + ''
451 }
452 '';
453 };
454
455 meta.maintainers = with lib.maintainers; [ felixsinger ];
456}