nixpkgs/nixos/modules/services/networking/jitsi-videobridge.nix
Erin Yuki Schlarb 61cf88212d jitsi-videobridge: Set configurable default NAT harvester address to make the
service work behind NAT without additional manual configuration
2024-09-30 18:19:33 +02:00

303 lines
9.9 KiB
Nix

{ config, lib, pkgs, ... }:
let
cfg = config.services.jitsi-videobridge;
attrsToArgs = a: lib.concatStringsSep " " (lib.mapAttrsToList (k: v: "${k}=${toString v}") a);
format = pkgs.formats.hocon { };
# 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_" + lib.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 = lib.flip lib.mapAttrs cfg.xmppConfigs (name: xmppConfig: {
hostname = xmppConfig.hostName;
domain = xmppConfig.domain;
username = xmppConfig.userName;
password = format.lib.mkSubstitution (toVarName name);
muc_jids = xmppConfig.mucJids;
muc_nickname = xmppConfig.mucNickname;
disable_certificate_verification = xmppConfig.disableCertificateVerification;
});
apis.rest.enabled = cfg.colibriRestApi;
};
};
# Allow overriding leaves of the default config despite types.attrs not doing any merging.
jvbConfig = lib.recursiveUpdate defaultJvbConfig cfg.config;
in
{
imports = [
(lib.mkRemovedOptionModule [ "services" "jitsi-videobridge" "apis" ]
"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."
)
];
options.services.jitsi-videobridge = with lib.types; {
enable = lib.mkEnableOption "Jitsi Videobridge, a WebRTC compatible video router";
config = lib.mkOption {
type = attrs;
default = { };
example = lib.literalExpression ''
{
videobridge = {
ice.udp.port = 5000;
websockets = {
enabled = true;
server-id = "jvb1";
};
};
}
'';
description = ''
Videobridge configuration.
See <https://github.com/jitsi/jitsi-videobridge/blob/master/jvb/src/main/resources/reference.conf>
for default configuration with comments.
'';
};
xmppConfigs = lib.mkOption {
description = ''
XMPP servers to connect to.
See <https://github.com/jitsi/jitsi-videobridge/blob/master/doc/muc.md> for more information.
'';
default = { };
example = lib.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 = lib.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 = lib.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 = lib.mkOption {
type = str;
default = "jvb";
description = ''
User part of the JID.
'';
};
passwordFile = lib.mkOption {
type = str;
example = "/run/keys/jitsi-videobridge-xmpp1";
description = ''
File containing the password for the user.
'';
};
mucJids = lib.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 = lib.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 = lib.mkOption {
type = bool;
default = false;
description = ''
Whether to skip validation of the server's certificate.
'';
};
};
config = {
hostName = lib.mkDefault name;
mucNickname = lib.mkDefault (builtins.replaceStrings [ "." ] [ "-" ] (
config.networking.fqdnOrHostName
));
};
}));
};
nat = {
localAddress = lib.mkOption {
type = nullOr str;
default = null;
example = "192.168.1.42";
description = ''
Local address to assume when running behind NAT.
'';
};
publicAddress = lib.mkOption {
type = nullOr str;
default = null;
example = "1.2.3.4";
description = ''
Public address to assume when running behind NAT.
'';
};
harvesterAddresses = lib.mkOption {
type = listOf str;
default = [
"stunserver.stunprotocol.org:3478"
"stun.framasoft.org:3478"
"meet-jit-si-turnrelay.jitsi.net:443"
];
example = [];
description = ''
Addresses of public STUN services to use to automatically find
the public and local addresses of this Jitsi-Videobridge instance
without the need for manual configuration.
This option is ignored if {option}`services.jitsi-videobridge.nat.localAddress`
and {option}`services.jitsi-videobridge.nat.publicAddress` are set.
'';
};
};
extraProperties = lib.mkOption {
type = attrsOf str;
default = { };
description = ''
Additional Java properties passed to jitsi-videobridge.
'';
};
openFirewall = lib.mkOption {
type = bool;
default = false;
description = ''
Whether to open ports in the firewall for the videobridge.
'';
};
colibriRestApi = lib.mkOption {
type = bool;
description = ''
Whether to enable the private rest API for the COLIBRI control interface.
Needed for monitoring jitsi, enabling scraping of the /colibri/stats endpoint.
'';
default = false;
};
};
config = lib.mkIf cfg.enable {
users.groups.jitsi-meet = {};
services.jitsi-videobridge.extraProperties =
if (cfg.nat.localAddress != null) then {
"org.ice4j.ice.harvest.NAT_HARVESTER_LOCAL_ADDRESS" = cfg.nat.localAddress;
"org.ice4j.ice.harvest.NAT_HARVESTER_PUBLIC_ADDRESS" = cfg.nat.publicAddress;
} else {
"org.ice4j.ice.harvest.STUN_MAPPING_HARVESTER_ADDRESSES" = lib.concatStringsSep "," cfg.nat.harvesterAddresses;
};
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" = format.generate "jvb.conf" jvbConfig;
# Mitigate CVE-2021-44228
"-Dlog4j2.formatMsgNoLookups" = true;
} // (lib.mapAttrs' (k: v: lib.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 = (lib.concatStrings (lib.mapAttrsToList (name: xmppConfig:
"export ${toVarName name}=$(cat ${xmppConfig.passwordFile})\n"
) cfg.xmppConfigs))
+ ''
${pkgs.jitsi-videobridge}/bin/jitsi-videobridge
'';
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 =
lib.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" = lib.mkDefault 10485760;
boot.kernel.sysctl."net.core.netdev_max_backlog" = lib.mkDefault 100000;
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall
[ jvbConfig.videobridge.ice.tcp.port ];
networking.firewall.allowedUDPPorts = lib.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;
}