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