1{
2 config,
3 lib,
4 pkgs,
5 utils,
6 ...
7}:
8let
9 cfg = config.services.coturn;
10 pidfile = "/run/turnserver/turnserver.pid";
11 configFile = pkgs.writeText "turnserver.conf" ''
12 listening-port=${toString cfg.listening-port}
13 tls-listening-port=${toString cfg.tls-listening-port}
14 alt-listening-port=${toString cfg.alt-listening-port}
15 alt-tls-listening-port=${toString cfg.alt-tls-listening-port}
16 ${lib.concatStringsSep "\n" (map (x: "listening-ip=${x}") cfg.listening-ips)}
17 ${lib.concatStringsSep "\n" (map (x: "relay-ip=${x}") cfg.relay-ips)}
18 min-port=${toString cfg.min-port}
19 max-port=${toString cfg.max-port}
20 ${lib.optionalString cfg.lt-cred-mech "lt-cred-mech"}
21 ${lib.optionalString cfg.no-auth "no-auth"}
22 ${lib.optionalString cfg.use-auth-secret "use-auth-secret"}
23 ${lib.optionalString (
24 cfg.static-auth-secret != null
25 ) "static-auth-secret=${cfg.static-auth-secret}"}
26 ${lib.optionalString (
27 cfg.static-auth-secret-file != null
28 ) "static-auth-secret=#static-auth-secret#"}
29 realm=${cfg.realm}
30 ${lib.optionalString cfg.no-udp "no-udp"}
31 ${lib.optionalString cfg.no-tcp "no-tcp"}
32 ${lib.optionalString cfg.no-tls "no-tls"}
33 ${lib.optionalString cfg.no-dtls "no-dtls"}
34 ${lib.optionalString cfg.no-udp-relay "no-udp-relay"}
35 ${lib.optionalString cfg.no-tcp-relay "no-tcp-relay"}
36 ${lib.optionalString (cfg.cert != null) "cert=${cfg.cert}"}
37 ${lib.optionalString (cfg.pkey != null) "pkey=${cfg.pkey}"}
38 ${lib.optionalString (cfg.dh-file != null) "dh-file=${cfg.dh-file}"}
39 pidfile=${pidfile}
40 ${lib.optionalString cfg.secure-stun "secure-stun"}
41 ${lib.optionalString cfg.no-cli "no-cli"}
42 cli-ip=${cfg.cli-ip}
43 cli-port=${toString cfg.cli-port}
44 ${lib.optionalString (cfg.cli-password != null) "cli-password=${cfg.cli-password}"}
45 ${cfg.extraConfig}
46 '';
47in
48{
49 options = {
50 services.coturn = {
51 enable = lib.mkEnableOption "coturn TURN server";
52 listening-port = lib.mkOption {
53 type = lib.types.int;
54 default = 3478;
55 description = ''
56 TURN listener port for UDP and TCP.
57 Note: actually, TLS and DTLS sessions can connect to the
58 "plain" TCP and UDP port(s), too - if allowed by configuration.
59 '';
60 };
61 tls-listening-port = lib.mkOption {
62 type = lib.types.int;
63 default = 5349;
64 description = ''
65 TURN listener port for TLS.
66 Note: actually, "plain" TCP and UDP sessions can connect to the TLS and
67 DTLS port(s), too - if allowed by configuration. The TURN server
68 "automatically" recognizes the type of traffic. Actually, two listening
69 endpoints (the "plain" one and the "tls" one) are equivalent in terms of
70 functionality; but we keep both endpoints to satisfy the RFC 5766 specs.
71 For secure TCP connections, we currently support SSL version 3 and
72 TLS version 1.0, 1.1 and 1.2.
73 For secure UDP connections, we support DTLS version 1.
74 '';
75 };
76 alt-listening-port = lib.mkOption {
77 type = lib.types.int;
78 default = cfg.listening-port + 1;
79 defaultText = lib.literalExpression "listening-port + 1";
80 description = ''
81 Alternative listening port for UDP and TCP listeners;
82 default (or zero) value means "listening port plus one".
83 This is needed for RFC 5780 support
84 (STUN extension specs, NAT behavior discovery). The TURN Server
85 supports RFC 5780 only if it is started with more than one
86 listening IP address of the same family (IPv4 or IPv6).
87 RFC 5780 is supported only by UDP protocol, other protocols
88 are listening to that endpoint only for "symmetry".
89 '';
90 };
91 alt-tls-listening-port = lib.mkOption {
92 type = lib.types.int;
93 default = cfg.tls-listening-port + 1;
94 defaultText = lib.literalExpression "tls-listening-port + 1";
95 description = ''
96 Alternative listening port for TLS and DTLS protocols.
97 '';
98 };
99 listening-ips = lib.mkOption {
100 type = lib.types.listOf lib.types.str;
101 default = [ ];
102 example = [
103 "203.0.113.42"
104 "2001:DB8::42"
105 ];
106 description = ''
107 Listener IP addresses of relay server.
108 If no IP(s) specified in the config file or in the command line options,
109 then all IPv4 and IPv6 system IPs will be used for listening.
110 '';
111 };
112 relay-ips = lib.mkOption {
113 type = lib.types.listOf lib.types.str;
114 default = [ ];
115 example = [
116 "203.0.113.42"
117 "2001:DB8::42"
118 ];
119 description = ''
120 Relay address (the local IP address that will be used to relay the
121 packets to the peer).
122 Multiple relay addresses may be used.
123 The same IP(s) can be used as both listening IP(s) and relay IP(s).
124
125 If no relay IP(s) specified, then the turnserver will apply the default
126 policy: it will decide itself which relay addresses to be used, and it
127 will always be using the client socket IP address as the relay IP address
128 of the TURN session (if the requested relay address family is the same
129 as the family of the client socket).
130 '';
131 };
132 min-port = lib.mkOption {
133 type = lib.types.int;
134 default = 49152;
135 description = ''
136 Lower bound of UDP relay endpoints
137 '';
138 };
139 max-port = lib.mkOption {
140 type = lib.types.int;
141 default = 65535;
142 description = ''
143 Upper bound of UDP relay endpoints
144 '';
145 };
146 lt-cred-mech = lib.mkOption {
147 type = lib.types.bool;
148 default = false;
149 description = ''
150 Use long-term credential mechanism.
151 '';
152 };
153 no-auth = lib.mkOption {
154 type = lib.types.bool;
155 default = false;
156 description = ''
157 This option is opposite to lt-cred-mech.
158 (TURN Server with no-auth option allows anonymous access).
159 If neither option is defined, and no users are defined,
160 then no-auth is default. If at least one user is defined,
161 in this file or in command line or in usersdb file, then
162 lt-cred-mech is default.
163 '';
164 };
165 use-auth-secret = lib.mkOption {
166 type = lib.types.bool;
167 default = false;
168 description = ''
169 TURN REST API flag.
170 Flag that sets a special authorization option that is based upon authentication secret.
171 This feature can be used with the long-term authentication mechanism, only.
172 This feature purpose is to support "TURN Server REST API", see
173 "TURN REST API" link in the project's page
174 <https://github.com/coturn/coturn/>
175
176 This option is used with timestamp:
177
178 usercombo -> "timestamp:userid"
179 turn user -> usercombo
180 turn password -> base64(hmac(secret key, usercombo))
181
182 This allows TURN credentials to be accounted for a specific user id.
183 If you don't have a suitable id, the timestamp alone can be used.
184 This option is just turning on secret-based authentication.
185 The actual value of the secret is defined either by option static-auth-secret,
186 or can be found in the turn_secret table in the database.
187 '';
188 };
189 static-auth-secret = lib.mkOption {
190 type = lib.types.nullOr lib.types.str;
191 default = null;
192 description = ''
193 'Static' authentication secret value (a string) for TURN REST API only.
194 If not set, then the turn server
195 will try to use the 'dynamic' value in turn_secret table
196 in user database (if present). The database-stored value can be changed on-the-fly
197 by a separate program, so this is why that other mode is 'dynamic'.
198 '';
199 };
200 static-auth-secret-file = lib.mkOption {
201 type = lib.types.nullOr lib.types.str;
202 default = null;
203 description = ''
204 Path to the file containing the static authentication secret.
205 '';
206 };
207 realm = lib.mkOption {
208 type = lib.types.str;
209 default = config.networking.hostName;
210 defaultText = lib.literalExpression "config.networking.hostName";
211 example = "example.com";
212 description = ''
213 The default realm to be used for the users when no explicit
214 origin/realm relationship was found in the database, or if the TURN
215 server is not using any database (just the commands-line settings
216 and the userdb file). Must be used with long-term credentials
217 mechanism or with TURN REST API.
218 '';
219 };
220 cert = lib.mkOption {
221 type = lib.types.nullOr lib.types.str;
222 default = null;
223 example = "/var/lib/acme/example.com/fullchain.pem";
224 description = ''
225 Certificate file in PEM format.
226 '';
227 };
228 pkey = lib.mkOption {
229 type = lib.types.nullOr lib.types.str;
230 default = null;
231 example = "/var/lib/acme/example.com/key.pem";
232 description = ''
233 Private key file in PEM format.
234 '';
235 };
236 dh-file = lib.mkOption {
237 type = lib.types.nullOr lib.types.str;
238 default = null;
239 description = ''
240 Use custom DH TLS key, stored in PEM format in the file.
241 '';
242 };
243 secure-stun = lib.mkOption {
244 type = lib.types.bool;
245 default = false;
246 description = ''
247 Require authentication of the STUN Binding request.
248 By default, the clients are allowed anonymous access to the STUN Binding functionality.
249 '';
250 };
251 no-cli = lib.mkOption {
252 type = lib.types.bool;
253 default = false;
254 description = ''
255 Turn OFF the CLI support.
256 '';
257 };
258 cli-ip = lib.mkOption {
259 type = lib.types.str;
260 default = "127.0.0.1";
261 description = ''
262 Local system IP address to be used for CLI server endpoint.
263 '';
264 };
265 cli-port = lib.mkOption {
266 type = lib.types.int;
267 default = 5766;
268 description = ''
269 CLI server port.
270 '';
271 };
272 cli-password = lib.mkOption {
273 type = lib.types.nullOr lib.types.str;
274 default = null;
275 description = ''
276 CLI access password.
277 For the security reasons, it is recommended to use the encrypted
278 for of the password (see the -P command in the turnadmin utility).
279 '';
280 };
281 no-udp = lib.mkOption {
282 type = lib.types.bool;
283 default = false;
284 description = "Disable UDP client listener";
285 };
286 no-tcp = lib.mkOption {
287 type = lib.types.bool;
288 default = false;
289 description = "Disable TCP client listener";
290 };
291 no-tls = lib.mkOption {
292 type = lib.types.bool;
293 default = false;
294 description = "Disable TLS client listener";
295 };
296 no-dtls = lib.mkOption {
297 type = lib.types.bool;
298 default = false;
299 description = "Disable DTLS client listener";
300 };
301 no-udp-relay = lib.mkOption {
302 type = lib.types.bool;
303 default = false;
304 description = "Disable UDP relay endpoints";
305 };
306 no-tcp-relay = lib.mkOption {
307 type = lib.types.bool;
308 default = false;
309 description = "Disable TCP relay endpoints";
310 };
311 extraConfig = lib.mkOption {
312 type = lib.types.lines;
313 default = "";
314 description = "Additional configuration options";
315 };
316 };
317 };
318
319 config = lib.mkIf cfg.enable (
320 lib.mkMerge [
321 {
322 assertions = [
323 {
324 assertion = cfg.static-auth-secret != null -> cfg.static-auth-secret-file == null;
325 message = "static-auth-secret and static-auth-secret-file cannot be set at the same time";
326 }
327 ];
328 }
329
330 {
331 users.users.turnserver = {
332 uid = config.ids.uids.turnserver;
333 group = "turnserver";
334 description = "coturn TURN server user";
335 };
336 users.groups.turnserver = {
337 gid = config.ids.gids.turnserver;
338 members = [ "turnserver" ];
339 };
340
341 systemd.services.coturn =
342 let
343 runConfig = "/run/coturn/turnserver.cfg";
344 in
345 {
346 description = "coturn TURN server";
347 after = [ "network-online.target" ];
348 wants = [ "network-online.target" ];
349 wantedBy = [ "multi-user.target" ];
350
351 unitConfig = {
352 Documentation = "man:coturn(1) man:turnadmin(1) man:turnserver(1)";
353 };
354
355 preStart = ''
356 cat ${configFile} > ${runConfig}
357 ${lib.optionalString (cfg.static-auth-secret-file != null) ''
358 ${pkgs.replace-secret}/bin/replace-secret \
359 "#static-auth-secret#" \
360 ${cfg.static-auth-secret-file} \
361 ${runConfig}
362 ''}
363 chmod 640 ${runConfig}
364 '';
365 serviceConfig = rec {
366 Type = "notify";
367 ExecStart = utils.escapeSystemdExecArgs [
368 (lib.getExe' pkgs.coturn "turnserver")
369 "-c"
370 runConfig
371 ];
372 User = "turnserver";
373 Group = "turnserver";
374 RuntimeDirectory = [
375 "coturn"
376 "turnserver"
377 ];
378 RuntimeDirectoryMode = "0700";
379 Restart = "on-abort";
380
381 # Hardening
382 AmbientCapabilities =
383 if
384 cfg.listening-port < 1024
385 || cfg.alt-listening-port < 1024
386 || cfg.tls-listening-port < 1024
387 || cfg.alt-tls-listening-port < 1024
388 || cfg.min-port < 1024
389 then
390 [ "CAP_NET_BIND_SERVICE" ]
391 else
392 [ "" ];
393 CapabilityBoundingSet = AmbientCapabilities;
394 DevicePolicy = "closed";
395 LockPersonality = true;
396 MemoryDenyWriteExecute = true;
397 NoNewPrivileges = true;
398 PrivateDevices = true;
399 PrivateTmp = true;
400 PrivateUsers = true;
401 ProcSubset = "pid";
402 ProtectClock = true;
403 ProtectControlGroups = true;
404 ProtectHome = true;
405 ProtectHostname = true;
406 ProtectKernelLogs = true;
407 ProtectKernelModules = true;
408 ProtectKernelTunables = true;
409 ProtectProc = "invisible";
410 ProtectSystem = "strict";
411 RemoveIPC = true;
412 RestrictAddressFamilies =
413 [
414 "AF_INET"
415 "AF_INET6"
416 "AF_UNIX"
417 ]
418 ++ lib.optionals (cfg.listening-ips == [ ]) [
419 # only used for interface discovery when no listening ips are configured
420 "AF_NETLINK"
421 ];
422 RestrictNamespaces = true;
423 RestrictRealtime = true;
424 RestrictSUIDSGID = true;
425 SystemCallArchitectures = "native";
426 SystemCallFilter = [
427 "@system-service"
428 "~@privileged @resources"
429 ];
430 UMask = "0077";
431 };
432 };
433 }
434 ]
435 );
436}