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