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