{ config, lib, pkgs, ... }: with lib; let cfg = config.services.jitsi-videobridge; attrsToArgs = a: concatStringsSep " " (mapAttrsToList (k: v: "${k}=${toString v}") a); # HOCON is a JSON superset that videobridge2 uses for configuration. # It can substitute environment variables which we use for passwords here. # https://github.com/lightbend/config/blob/master/README.md # # Substitution for environment variable FOO is represented as attribute set # { __hocon_envvar = "FOO"; } toHOCON = x: if isAttrs x && x ? __hocon_envvar then ("\${" + x.__hocon_envvar + "}") else if isAttrs x then "{${ concatStringsSep "," (mapAttrsToList (k: v: ''"${k}":${toHOCON v}'') x) }}" else if isList x then "[${ concatMapStringsSep "," toHOCON x }]" else builtins.toJSON x; # We're passing passwords in environment variables that have names generated # from an attribute name, which may not be a valid bash identifier. toVarName = s: "XMPP_PASSWORD_" + stringAsChars (c: if builtins.match "[A-Za-z0-9]" c != null then c else "_") s; defaultJvbConfig = { videobridge = { ice = { tcp = { enabled = true; port = 4443; }; udp.port = 10000; }; stats = { enabled = true; transports = [ { type = "muc"; } ]; }; apis.xmpp-client.configs = flip mapAttrs cfg.xmppConfigs (name: xmppConfig: { hostname = xmppConfig.hostName; domain = xmppConfig.domain; username = xmppConfig.userName; password = { __hocon_envvar = toVarName name; }; muc_jids = xmppConfig.mucJids; muc_nickname = xmppConfig.mucNickname; disable_certificate_verification = xmppConfig.disableCertificateVerification; }); }; }; # Allow overriding leaves of the default config despite types.attrs not doing any merging. jvbConfig = recursiveUpdate defaultJvbConfig cfg.config; in { options.services.jitsi-videobridge = with types; { enable = mkEnableOption (lib.mdDoc "Jitsi Videobridge, a WebRTC compatible video router"); config = mkOption { type = attrs; default = { }; example = literalExpression '' { videobridge = { ice.udp.port = 5000; websockets = { enabled = true; server-id = "jvb1"; }; }; } ''; description = lib.mdDoc '' Videobridge configuration. See for default configuration with comments. ''; }; xmppConfigs = mkOption { description = lib.mdDoc '' XMPP servers to connect to. See for more information. ''; default = { }; example = literalExpression '' { "localhost" = { hostName = "localhost"; userName = "jvb"; domain = "auth.xmpp.example.org"; passwordFile = "/var/lib/jitsi-meet/videobridge-secret"; mucJids = "jvbbrewery@internal.xmpp.example.org"; }; } ''; type = attrsOf (submodule ({ name, ... }: { options = { hostName = mkOption { type = str; example = "xmpp.example.org"; description = lib.mdDoc '' Hostname of the XMPP server to connect to. Name of the attribute set is used by default. ''; }; domain = mkOption { type = nullOr str; default = null; example = "auth.xmpp.example.org"; description = lib.mdDoc '' Domain part of JID of the XMPP user, if it is different from hostName. ''; }; userName = mkOption { type = str; default = "jvb"; description = lib.mdDoc '' User part of the JID. ''; }; passwordFile = mkOption { type = str; example = "/run/keys/jitsi-videobridge-xmpp1"; description = lib.mdDoc '' File containing the password for the user. ''; }; mucJids = mkOption { type = str; example = "jvbbrewery@internal.xmpp.example.org"; description = lib.mdDoc '' JID of the MUC to join. JiCoFo needs to be configured to join the same MUC. ''; }; mucNickname = mkOption { # Upstream DEBs use UUID, let's use hostname instead. type = str; description = lib.mdDoc '' Videobridges use the same XMPP account and need to be distinguished by the nickname (aka resource part of the JID). By default, system hostname is used. ''; }; disableCertificateVerification = mkOption { type = bool; default = false; description = lib.mdDoc '' Whether to skip validation of the server's certificate. ''; }; }; config = { hostName = mkDefault name; mucNickname = mkDefault (builtins.replaceStrings [ "." ] [ "-" ] ( config.networking.hostName + optionalString (config.networking.domain != null) ".${config.networking.domain}" )); }; })); }; nat = { localAddress = mkOption { type = nullOr str; default = null; example = "192.168.1.42"; description = lib.mdDoc '' Local address when running behind NAT. ''; }; publicAddress = mkOption { type = nullOr str; default = null; example = "1.2.3.4"; description = lib.mdDoc '' Public address when running behind NAT. ''; }; }; extraProperties = mkOption { type = attrsOf str; default = { }; description = lib.mdDoc '' Additional Java properties passed to jitsi-videobridge. ''; }; openFirewall = mkOption { type = bool; default = false; description = lib.mdDoc '' Whether to open ports in the firewall for the videobridge. ''; }; apis = mkOption { type = with types; listOf str; description = lib.mdDoc '' What is passed as --apis= parameter. If this is empty, "none" is passed. Needed for monitoring jitsi. ''; default = []; example = literalExpression "[ \"colibri\" \"rest\" ]"; }; }; config = mkIf cfg.enable { users.groups.jitsi-meet = {}; services.jitsi-videobridge.extraProperties = optionalAttrs (cfg.nat.localAddress != null) { "org.ice4j.ice.harvest.NAT_HARVESTER_LOCAL_ADDRESS" = cfg.nat.localAddress; "org.ice4j.ice.harvest.NAT_HARVESTER_PUBLIC_ADDRESS" = cfg.nat.publicAddress; }; systemd.services.jitsi-videobridge2 = let jvbProps = { "-Dnet.java.sip.communicator.SC_HOME_DIR_LOCATION" = "/etc/jitsi"; "-Dnet.java.sip.communicator.SC_HOME_DIR_NAME" = "videobridge"; "-Djava.util.logging.config.file" = "/etc/jitsi/videobridge/logging.properties"; "-Dconfig.file" = pkgs.writeText "jvb.conf" (toHOCON jvbConfig); # Mitigate CVE-2021-44228 "-Dlog4j2.formatMsgNoLookups" = true; } // (mapAttrs' (k: v: nameValuePair "-D${k}" v) cfg.extraProperties); in { aliases = [ "jitsi-videobridge.service" ]; description = "Jitsi Videobridge"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; environment.JAVA_SYS_PROPS = attrsToArgs jvbProps; script = (concatStrings (mapAttrsToList (name: xmppConfig: "export ${toVarName name}=$(cat ${xmppConfig.passwordFile})\n" ) cfg.xmppConfigs)) + '' ${pkgs.jitsi-videobridge}/bin/jitsi-videobridge --apis=${if (cfg.apis == []) then "none" else concatStringsSep "," cfg.apis} ''; serviceConfig = { Type = "exec"; DynamicUser = true; User = "jitsi-videobridge"; Group = "jitsi-meet"; CapabilityBoundingSet = ""; NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; PrivateDevices = true; ProtectHostname = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; RestrictNamespaces = true; LockPersonality = true; RestrictRealtime = true; RestrictSUIDSGID = true; TasksMax = 65000; LimitNPROC = 65000; LimitNOFILE = 65000; }; }; environment.etc."jitsi/videobridge/logging.properties".source = mkDefault "${pkgs.jitsi-videobridge}/etc/jitsi/videobridge/logging.properties-journal"; # (from videobridge2 .deb) # this sets the max, so that we can bump the JVB UDP single port buffer size. boot.kernel.sysctl."net.core.rmem_max" = mkDefault 10485760; boot.kernel.sysctl."net.core.netdev_max_backlog" = mkDefault 100000; networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ jvbConfig.videobridge.ice.tcp.port ]; networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [ jvbConfig.videobridge.ice.udp.port ]; assertions = [{ message = "publicAddress must be set if and only if localAddress is set"; assertion = (cfg.nat.publicAddress == null) == (cfg.nat.localAddress == null); }]; }; meta.maintainers = lib.teams.jitsi.members; }