1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.jitsi-videobridge;
9 attrsToArgs = a: lib.concatStringsSep " " (lib.mapAttrsToList (k: v: "${k}=${toString v}") a);
10
11 format = pkgs.formats.hocon { };
12
13 # We're passing passwords in environment variables that have names generated
14 # from an attribute name, which may not be a valid bash identifier.
15 toVarName =
16 s:
17 "XMPP_PASSWORD_"
18 + lib.stringAsChars (c: if builtins.match "[A-Za-z0-9]" c != null then c else "_") s;
19
20 defaultJvbConfig = {
21 videobridge = {
22 ice = {
23 tcp = {
24 enabled = true;
25 port = 4443;
26 };
27 udp.port = 10000;
28 };
29 stats = {
30 enabled = true;
31 transports = [ { type = "muc"; } ];
32 };
33 apis.xmpp-client.configs = lib.flip lib.mapAttrs cfg.xmppConfigs (
34 name: xmppConfig: {
35 hostname = xmppConfig.hostName;
36 domain = xmppConfig.domain;
37 username = xmppConfig.userName;
38 password = format.lib.mkSubstitution (toVarName name);
39 muc_jids = xmppConfig.mucJids;
40 muc_nickname = xmppConfig.mucNickname;
41 disable_certificate_verification = xmppConfig.disableCertificateVerification;
42 }
43 );
44 apis.rest.enabled = cfg.colibriRestApi;
45 };
46 };
47
48 # Allow overriding leaves of the default config despite types.attrs not doing any merging.
49 jvbConfig = lib.recursiveUpdate defaultJvbConfig cfg.config;
50in
51{
52 imports = [
53 (lib.mkRemovedOptionModule [ "services" "jitsi-videobridge" "apis" ]
54 "services.jitsi-videobridge.apis was broken and has been migrated into the boolean option services.jitsi-videobridge.colibriRestApi. It is set to false by default, setting it to true will correctly enable the private /colibri rest API."
55 )
56 ];
57 options.services.jitsi-videobridge = with lib.types; {
58 enable = lib.mkEnableOption "Jitsi Videobridge, a WebRTC compatible video router";
59
60 config = lib.mkOption {
61 type = attrs;
62 default = { };
63 example = lib.literalExpression ''
64 {
65 videobridge = {
66 ice.udp.port = 5000;
67 websockets = {
68 enabled = true;
69 server-id = "jvb1";
70 };
71 };
72 }
73 '';
74 description = ''
75 Videobridge configuration.
76
77 See <https://github.com/jitsi/jitsi-videobridge/blob/master/jvb/src/main/resources/reference.conf>
78 for default configuration with comments.
79 '';
80 };
81
82 xmppConfigs = lib.mkOption {
83 description = ''
84 XMPP servers to connect to.
85
86 See <https://github.com/jitsi/jitsi-videobridge/blob/master/doc/muc.md> for more information.
87 '';
88 default = { };
89 example = lib.literalExpression ''
90 {
91 "localhost" = {
92 hostName = "localhost";
93 userName = "jvb";
94 domain = "auth.xmpp.example.org";
95 passwordFile = "/var/lib/jitsi-meet/videobridge-secret";
96 mucJids = "jvbbrewery@internal.xmpp.example.org";
97 };
98 }
99 '';
100 type = attrsOf (
101 submodule (
102 { name, ... }:
103 {
104 options = {
105 hostName = lib.mkOption {
106 type = str;
107 example = "xmpp.example.org";
108 description = ''
109 Hostname of the XMPP server to connect to. Name of the attribute set is used by default.
110 '';
111 };
112 domain = lib.mkOption {
113 type = nullOr str;
114 default = null;
115 example = "auth.xmpp.example.org";
116 description = ''
117 Domain part of JID of the XMPP user, if it is different from hostName.
118 '';
119 };
120 userName = lib.mkOption {
121 type = str;
122 default = "jvb";
123 description = ''
124 User part of the JID.
125 '';
126 };
127 passwordFile = lib.mkOption {
128 type = str;
129 example = "/run/keys/jitsi-videobridge-xmpp1";
130 description = ''
131 File containing the password for the user.
132 '';
133 };
134 mucJids = lib.mkOption {
135 type = str;
136 example = "jvbbrewery@internal.xmpp.example.org";
137 description = ''
138 JID of the MUC to join. JiCoFo needs to be configured to join the same MUC.
139 '';
140 };
141 mucNickname = lib.mkOption {
142 # Upstream DEBs use UUID, let's use hostname instead.
143 type = str;
144 description = ''
145 Videobridges use the same XMPP account and need to be distinguished by the
146 nickname (aka resource part of the JID). By default, system hostname is used.
147 '';
148 };
149 disableCertificateVerification = lib.mkOption {
150 type = bool;
151 default = false;
152 description = ''
153 Whether to skip validation of the server's certificate.
154 '';
155 };
156 };
157 config = {
158 hostName = lib.mkDefault name;
159 mucNickname = lib.mkDefault (
160 builtins.replaceStrings [ "." ] [ "-" ] (config.networking.fqdnOrHostName)
161 );
162 };
163 }
164 )
165 );
166 };
167
168 nat = {
169 localAddress = lib.mkOption {
170 type = nullOr str;
171 default = null;
172 example = "192.168.1.42";
173 description = ''
174 Local address to assume when running behind NAT.
175 '';
176 };
177
178 publicAddress = lib.mkOption {
179 type = nullOr str;
180 default = null;
181 example = "1.2.3.4";
182 description = ''
183 Public address to assume when running behind NAT.
184 '';
185 };
186
187 harvesterAddresses = lib.mkOption {
188 type = listOf str;
189 default = [
190 "stunserver.stunprotocol.org:3478"
191 "stun.framasoft.org:3478"
192 "meet-jit-si-turnrelay.jitsi.net:443"
193 ];
194 example = [ ];
195 description = ''
196 Addresses of public STUN services to use to automatically find
197 the public and local addresses of this Jitsi-Videobridge instance
198 without the need for manual configuration.
199
200 This option is ignored if {option}`services.jitsi-videobridge.nat.localAddress`
201 and {option}`services.jitsi-videobridge.nat.publicAddress` are set.
202 '';
203 };
204 };
205
206 extraProperties = lib.mkOption {
207 type = attrsOf str;
208 default = { };
209 description = ''
210 Additional Java properties passed to jitsi-videobridge.
211 '';
212 };
213
214 openFirewall = lib.mkOption {
215 type = bool;
216 default = false;
217 description = ''
218 Whether to open ports in the firewall for the videobridge.
219 '';
220 };
221
222 colibriRestApi = lib.mkOption {
223 type = bool;
224 description = ''
225 Whether to enable the private rest API for the COLIBRI control interface.
226 Needed for monitoring jitsi, enabling scraping of the /colibri/stats endpoint.
227 '';
228 default = false;
229 };
230 };
231
232 config = lib.mkIf cfg.enable {
233 users.groups.jitsi-meet = { };
234
235 services.jitsi-videobridge.extraProperties =
236 if (cfg.nat.localAddress != null) then
237 {
238 "org.ice4j.ice.harvest.NAT_HARVESTER_LOCAL_ADDRESS" = cfg.nat.localAddress;
239 "org.ice4j.ice.harvest.NAT_HARVESTER_PUBLIC_ADDRESS" = cfg.nat.publicAddress;
240 }
241 else
242 {
243 "org.ice4j.ice.harvest.STUN_MAPPING_HARVESTER_ADDRESSES" =
244 lib.concatStringsSep "," cfg.nat.harvesterAddresses;
245 };
246
247 systemd.services.jitsi-videobridge2 =
248 let
249 jvbProps = {
250 "-Dnet.java.sip.communicator.SC_HOME_DIR_LOCATION" = "/etc/jitsi";
251 "-Dnet.java.sip.communicator.SC_HOME_DIR_NAME" = "videobridge";
252 "-Djava.util.logging.config.file" = "/etc/jitsi/videobridge/logging.properties";
253 "-Dconfig.file" = format.generate "jvb.conf" jvbConfig;
254 # Mitigate CVE-2021-44228
255 "-Dlog4j2.formatMsgNoLookups" = true;
256 } // (lib.mapAttrs' (k: v: lib.nameValuePair "-D${k}" v) cfg.extraProperties);
257 in
258 {
259 aliases = [ "jitsi-videobridge.service" ];
260 description = "Jitsi Videobridge";
261 after = [ "network.target" ];
262 wantedBy = [ "multi-user.target" ];
263
264 environment.JAVA_SYS_PROPS = attrsToArgs jvbProps;
265
266 script =
267 (lib.concatStrings (
268 lib.mapAttrsToList (
269 name: xmppConfig: "export ${toVarName name}=$(cat ${xmppConfig.passwordFile})\n"
270 ) cfg.xmppConfigs
271 ))
272 + ''
273 ${pkgs.jitsi-videobridge}/bin/jitsi-videobridge
274 '';
275
276 serviceConfig = {
277 Type = "exec";
278
279 DynamicUser = true;
280 User = "jitsi-videobridge";
281 Group = "jitsi-meet";
282
283 AmbientCapabilities = "CAP_NET_BIND_SERVICE";
284 CapabilityBoundingSet = "";
285 NoNewPrivileges = true;
286 ProtectSystem = "strict";
287 ProtectHome = true;
288 PrivateTmp = true;
289 PrivateDevices = true;
290 ProtectHostname = true;
291 ProtectKernelTunables = true;
292 ProtectKernelModules = true;
293 ProtectControlGroups = true;
294 RestrictAddressFamilies = [
295 "AF_INET"
296 "AF_INET6"
297 "AF_UNIX"
298 ];
299 RestrictNamespaces = true;
300 LockPersonality = true;
301 RestrictRealtime = true;
302 RestrictSUIDSGID = true;
303
304 TasksMax = 65000;
305 LimitNPROC = 65000;
306 LimitNOFILE = 65000;
307 };
308 };
309
310 environment.etc."jitsi/videobridge/logging.properties".source =
311 lib.mkDefault "${pkgs.jitsi-videobridge}/etc/jitsi/videobridge/logging.properties-journal";
312
313 # (from videobridge2 .deb)
314 # this sets the max, so that we can bump the JVB UDP single port buffer size.
315 boot.kernel.sysctl."net.core.rmem_max" = lib.mkDefault 10485760;
316 boot.kernel.sysctl."net.core.netdev_max_backlog" = lib.mkDefault 100000;
317
318 networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [
319 jvbConfig.videobridge.ice.tcp.port
320 ];
321 networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall [
322 jvbConfig.videobridge.ice.udp.port
323 ];
324
325 assertions = [
326 {
327 message = "publicAddress must be set if and only if localAddress is set";
328 assertion = (cfg.nat.publicAddress == null) == (cfg.nat.localAddress == null);
329 }
330 ];
331 };
332
333 meta.maintainers = lib.teams.jitsi.members;
334}