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