1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 inherit (lib)
10 getExe
11 literalExpression
12 mkAfter
13 mkEnableOption
14 mkIf
15 mkMerge
16 mkOption
17 optionalAttrs
18 optionalString
19 ;
20
21 inherit (lib.types)
22 bool
23 listOf
24 nullOr
25 path
26 port
27 str
28 ;
29
30 cfg = config.services.netbird.server.coturn;
31in
32
33{
34 options.services.netbird.server.coturn = {
35 enable = mkEnableOption "a Coturn server for Netbird, will also open the firewall on the configured range";
36
37 useAcmeCertificates = mkOption {
38 type = bool;
39 default = false;
40 description = ''
41 Whether to use ACME certificates corresponding to the given domain for the server.
42 '';
43 };
44
45 domain = mkOption {
46 type = str;
47 description = "The domain under which the coturn server runs.";
48 };
49
50 user = mkOption {
51 type = str;
52 default = "netbird";
53 description = ''
54 The username used by netbird to connect to the coturn server.
55 '';
56 };
57
58 password = mkOption {
59 type = nullOr str;
60 default = null;
61 description = ''
62 The password of the user used by netbird to connect to the coturn server.
63 Be advised this will be world readable in the nix store.
64 '';
65 };
66
67 passwordFile = mkOption {
68 type = nullOr path;
69 default = null;
70 description = ''
71 The path to a file containing the password of the user used by netbird to connect to the coturn server.
72 '';
73 };
74
75 openPorts = mkOption {
76 type = listOf port;
77 default = with config.services.coturn; [
78 listening-port
79 alt-listening-port
80 tls-listening-port
81 alt-tls-listening-port
82 ];
83 defaultText = literalExpression ''
84 with config.services.coturn; [
85 listening-port
86 alt-listening-port
87 tls-listening-port
88 alt-tls-listening-port
89 ];
90 '';
91
92 description = ''
93 The list of ports used by coturn for listening to open in the firewall.
94 '';
95 };
96 };
97
98 config = mkIf cfg.enable (mkMerge [
99 {
100 assertions = [
101 {
102 assertion = (cfg.password == null) != (cfg.passwordFile == null);
103 message = "Exactly one of `password` or `passwordFile` must be given for the coturn setup.";
104 }
105 ];
106
107 services.coturn =
108 {
109 enable = true;
110
111 realm = cfg.domain;
112 lt-cred-mech = true;
113 no-cli = true;
114
115 extraConfig = ''
116 fingerprint
117 user=${cfg.user}:${if cfg.password != null then cfg.password else "@password@"}
118 no-software-attribute
119 '';
120 }
121 // (optionalAttrs cfg.useAcmeCertificates {
122 cert = "@cert@";
123 pkey = "@pkey@";
124 });
125
126 systemd.services.coturn =
127 let
128 dir = config.security.acme.certs.${cfg.domain}.directory;
129 preStart' =
130 (optionalString (cfg.passwordFile != null) ''
131 ${getExe pkgs.replace-secret} @password@ ${cfg.passwordFile} /run/coturn/turnserver.cfg
132 '')
133 + (optionalString cfg.useAcmeCertificates ''
134 ${getExe pkgs.replace-secret} @cert@ <(echo -n "$CREDENTIALS_DIRECTORY/cert.pem") /run/coturn/turnserver.cfg
135 ${getExe pkgs.replace-secret} @pkey@ <(echo -n "$CREDENTIALS_DIRECTORY/pkey.pem") /run/coturn/turnserver.cfg
136 '');
137 in
138 (optionalAttrs (preStart' != "") { preStart = mkAfter preStart'; })
139 // (optionalAttrs cfg.useAcmeCertificates {
140 serviceConfig.LoadCredential = [
141 "cert.pem:${dir}/fullchain.pem"
142 "pkey.pem:${dir}/key.pem"
143 ];
144 });
145
146 security.acme.certs = mkIf cfg.useAcmeCertificates {
147 ${cfg.domain}.postRun = ''
148 systemctl restart coturn.service
149 '';
150 };
151
152 networking.firewall = {
153 allowedUDPPorts = cfg.openPorts;
154 allowedTCPPorts = cfg.openPorts;
155
156 allowedUDPPortRanges = with config.services.coturn; [
157 {
158 from = min-port;
159 to = max-port;
160 }
161 ];
162 };
163 }
164 ]);
165}