diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index f361163ca631..3b374a34ac91 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -642,6 +642,7 @@ ./services/networking/iperf3.nix ./services/networking/ircd-hybrid/default.nix ./services/networking/iwd.nix + ./services/networking/jitsi-videobridge.nix ./services/networking/keepalived/default.nix ./services/networking/keybase.nix ./services/networking/kippo.nix diff --git a/nixos/modules/services/networking/jitsi-videobridge.nix b/nixos/modules/services/networking/jitsi-videobridge.nix new file mode 100644 index 000000000000..b368ee14903d --- /dev/null +++ b/nixos/modules/services/networking/jitsi-videobridge.nix @@ -0,0 +1,276 @@ +{ 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 "Jitsi Videobridge, a WebRTC compatible video router"; + + config = mkOption { + type = attrs; + default = { }; + example = literalExample '' + { + videobridge = { + ice.udp.port = 5000; + websockets = { + enabled = true; + server-id = "jvb1"; + }; + }; + } + ''; + description = '' + Videobridge configuration. + + See + for default configuration with comments. + ''; + }; + + xmppConfigs = mkOption { + description = '' + XMPP servers to connect to. + + See for more information. + ''; + default = { }; + example = literalExample '' + { + "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 = '' + 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 = '' + Domain part of JID of the XMPP user, if it is different from hostName. + ''; + }; + userName = mkOption { + type = str; + default = "jvb"; + description = '' + User part of the JID. + ''; + }; + passwordFile = mkOption { + type = str; + example = "/run/keys/jitsi-videobridge-xmpp1"; + description = '' + File containing the password for the user. + ''; + }; + mucJids = mkOption { + type = str; + example = "jvbbrewery@internal.xmpp.example.org"; + description = '' + 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 = '' + 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 = '' + 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 = '' + Local address when running behind NAT. + ''; + }; + + publicAddress = mkOption { + type = nullOr str; + default = null; + example = "1.2.3.4"; + description = '' + Public address when running behind NAT. + ''; + }; + }; + + extraProperties = mkOption { + type = attrsOf str; + default = { }; + description = '' + Additional Java properties passed to jitsi-videobridge. + ''; + }; + + openFirewall = mkOption { + type = bool; + default = false; + description = '' + Whether to open ports in the firewall for the videobridge. + ''; + }; + }; + + 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); + } // (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=none + ''; + + 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 = with lib.maintainers; [ ]; +}