app/matrix: add coturn deployment

TURN server is required for proper cross-NAT voice/video calls via
Matrix.

Change-Id: I8182292dd8ef30690ae4b9487c22aedcff098710
Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1387
Reviewed-by: informatic <informatic@hackerspace.pl>
master
informatic 2022-05-07 11:27:24 +02:00 committed by informatic
parent b39edc3256
commit 690ed45f66
4 changed files with 224 additions and 0 deletions

View File

@ -0,0 +1,177 @@
local kube = import "../../../kube/kube.libsonnet";
{
local app = self,
local cfg = app.cfg,
cfg:: {
image: error "cfg.image must be set",
realm: error "cfg.realm must be set",
authSecret: error "cfg.authSecret must be set",
storageClassName: error "cfg.storageClassName must be set",
portStart: 49152,
portEnd: 49172,
loadBalancerIP: null,
},
ns:: error "ns needs to be provided",
configMap: app.ns.Contain(kube.ConfigMap("coturn")) {
data: {
"coturn.conf": |||
# VoIP traffic is all UDP. There is no reason to let users connect to arbitrary TCP endpoints via the relay.
no-tcp-relay
no-tls
no-dtls
# don't let the relay ever try to connect to private IP address ranges within your network (if any)
# given the turn server is likely behind your firewall, remember to include any privileged public IPs too.
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=192.168.0.0-192.168.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
# recommended additional local peers to block, to mitigate external access to internal services.
# https://www.rtcsec.com/article/slack-webrtc-turn-compromise-and-bug-bounty/#how-to-fix-an-open-turn-relay-to-address-this-vulnerability
no-multicast-peers
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=100.64.0.0-100.127.255.255
denied-peer-ip=127.0.0.0-127.255.255.255
denied-peer-ip=169.254.0.0-169.254.255.255
denied-peer-ip=192.0.0.0-192.0.0.255
denied-peer-ip=192.0.2.0-192.0.2.255
denied-peer-ip=192.88.99.0-192.88.99.255
denied-peer-ip=198.18.0.0-198.19.255.255
denied-peer-ip=198.51.100.0-198.51.100.255
denied-peer-ip=203.0.113.0-203.0.113.255
denied-peer-ip=240.0.0.0-255.255.255.255
# special case the turn server itself so that client->TURN->TURN->client flows work
# this should be one of the turn server's listening IPs
# FIXME allowed-peer-ip=10.0.0.1
# consider whether you want to limit the quota of relayed streams per user (or total) to avoid risk of DoS.
user-quota=12 # 4 streams per video call, so 12 streams = 3 simultaneous relayed calls per user.
total-quota=1200
use-auth-secret
|||,
},
},
dataVolume: app.ns.Contain(kube.PersistentVolumeClaim("coturn-data")) {
spec+: {
storageClassName: cfg.storageClassName,
resources: {
requests: {
storage: "10Gi",
},
},
},
},
deployment: app.ns.Contain(kube.Deployment("coturn")) {
spec+: {
replicas: 1,
template+: {
spec+: {
volumes_: {
config: kube.ConfigMapVolume(app.configMap),
data: kube.PersistentVolumeClaimVolume(app.dataVolume),
},
containers_: {
coturn: kube.Container("coturn") {
image: cfg.image,
ports_: {
turn: { containerPort: 3478 },
} + {
["fwd-%d" % [n]]: { containerPort: n }
for n in std.range(cfg.portStart, cfg.portEnd)
},
command: [
# This disgusting hack comes from the fact that
# official coturn containers have turnserver
# binary set up with CAP_NET_BIND_SERVICE=+ep,
# while there's really no use that in our case.
#
# Due to our PSP we can't exec said binary, nor
# can we chmod/chown/setcap on it, as we are
# running as an unprivileged user.
#
# Copying it over is the easiest method of
# stripping said spurious cap.
"/bin/sh", "-c",
"cp /usr/bin/turnserver /tmp/turnserver && \\
exec /tmp/turnserver \\
-c /config/coturn.conf \\
--log-binding \\
--realm=$(COTURN_REALM) \\
--static-auth-secret=$(COTRN_STATIC_AUTH_SECRET) \\
--min-port $(COTURN_MIN_PORT) \\
--max-port $(COTURN_MAX_PORT) \\
" + if cfg.loadBalancerIP != null then "-X $(COTURN_EXTERNAL_IP)" else "",
],
volumeMounts_: {
config: { mountPath: "/config" },
data: { mountPath: "/var/lib/coturn" },
},
env_: {
COTURN_REALM: cfg.realm,
COTURN_STATIC_AUTH_SECRET: cfg.authSecret,
COTURN_EXTERNAL_IP: cfg.loadBalancerIP,
COTURN_MIN_PORT: cfg.portStart,
COTURN_MAX_PORT: cfg.portEnd,
},
},
},
securityContext: {
runAsUser: 1000,
runAsGroup: 1000,
fsGroup: 2000,
},
},
},
},
},
svcTCP: app.ns.Contain(kube.Service("coturn-tcp")) {
target_pod:: app.deployment.spec.template,
metadata+: {
annotations+: {
"metallb.universe.tf/allow-shared-ip": "coturn",
},
},
spec+: {
type: "LoadBalancer",
loadBalancerIP: cfg.loadBalancerIP,
externalTrafficPolicy: "Local",
ports: [
{ name: "turn", port: 3478, protocol: "TCP" },
] + [
{ name: "fwd-%d" % [n], port: n, protocol: "TCP" }
for n in std.range(cfg.portStart, cfg.portEnd)
],
},
},
svcUDP: app.ns.Contain(kube.Service("coturn-udp")) {
target_pod:: app.deployment.spec.template,
metadata+: {
annotations+: {
"metallb.universe.tf/allow-shared-ip": "coturn",
},
},
spec+: {
type: "LoadBalancer",
loadBalancerIP: cfg.loadBalancerIP,
externalTrafficPolicy: "Local",
ports: [
{ name: "turn", port: 3478, protocol: "UDP" },
] + [
{ name: "fwd-%d" % [n], port: n, protocol: "UDP" }
for n in std.range(cfg.portStart, cfg.portEnd)
],
},
},
}

View File

@ -95,6 +95,7 @@ local cas = import "./cas.libsonnet";
local wellKnown = import "./wellknown.libsonnet";
local synapse = import "./synapse.libsonnet";
local mediaRepo = import "./media-repo.libsonnet";
local coturn = import "./coturn.libsonnet";
{
local app = self,
@ -115,6 +116,7 @@ local mediaRepo = import "./media-repo.libsonnet";
appserviceTelegram: "dock.mau.dev/tulir/mautrix-telegram@sha256:c6e25cb57e1b67027069e8dc2627338df35d156315c004a6f2b34b6aeaa79f77",
wellKnown: "registry.k0.hswaw.net/q3k/wellknown:1611960794-adbf560851a46ad0e58b42f0daad7ef19535687c",
mediaRepo: "turt2live/matrix-media-repo:v1.2.8",
coturn: "coturn/coturn:4.5.2-r11-alpine",
},
# OpenID Connect provider configuration.
@ -193,6 +195,23 @@ local mediaRepo = import "./media-repo.libsonnet";
# List of administrative users MXIDs (used in matrix-media-repo only)
admins: [],
# Deploy coturn STUN/TURN server
coturn: {
enable: false,
config: {
domain: error "coturn.config.domain must be set",
# Default to public domain - this may be adjusted when multiple
# turn servers are deployed.
realm: self.domain,
# Set this to assigned LoadBalacer IP for correct NAT resolution
loadBalancerIP: null,
authSecret: { secretKeyRef: { name: "coturn", key: "auth_secret" } },
},
},
},
# DEPRECATED: this needs to be removed in favor of namespace.Contain() in
@ -279,6 +298,17 @@ local mediaRepo = import "./media-repo.libsonnet";
},
} else {},
coturn: if cfg.coturn.enable then coturn {
ns: app.namespace,
cfg+: {
storageClassName: cfg.storageClassName,
image: cfg.images.coturn,
realm: cfg.coturn.config.realm,
loadBalancerIP: cfg.coturn.config.loadBalancerIP,
authSecret: cfg.coturn.config.authSecret,
},
} else null,
synapse: synapse {
ns: app.namespace,
postgres: app.postgres3,

View File

@ -69,6 +69,13 @@ local kube = import "../../../kube/kube.libsonnet";
server_url: "https://%s/_cas" % [cfg.webDomain],
service_url: "https://%s" % [cfg.webDomain],
},
} else {}) + (if cfg.coturn.enable then {
turn_uris: [ "turn:%s?transport=udp" % cfg.coturn.config.domain, "turn:%s?transport=tcp" % cfg.coturn.config.domain ],
# Lifetime of single TURN user credentials - 1 day, recommended by TURN REST
# spec, see https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00#section-2.2
turn_user_lifetime: 24 * 60 * 60 * 1000,
turn_allow_guests: true,
} else {}),
configMap: app.ns.Contain(kube.ConfigMap("synapse")) {
@ -87,6 +94,8 @@ local kube = import "../../../kube/kube.libsonnet";
enabled: true,
client_secret: "$(OIDC_CLIENT_SECRET)",
},
} else {}) + (if cfg.coturn.enable then {
turn_shared_secret: "$(TURN_SHARED_SECRET)",
} else {}),
# Synapse process Deployment/StatefulSet base resource.
@ -151,6 +160,7 @@ local kube = import "../../../kube/kube.libsonnet";
REDIS_PASSWORD: app.redis.cfg.password,
POD_NAME: { fieldRef: { fieldPath: "metadata.name" } },
OIDC_CLIENT_SECRET: if cfg.oidc.enable then cfg.oidc.config.client_secret else "",
TURN_SHARED_SECRET: if cfg.coturn.enable then cfg.coturn.config.authSecret else "",
X_SECRETS_CONFIG: std.manifestYamlDoc(app.secretsConfig),
X_LOCAL_CONFIG: std.manifestYamlDoc(worker.cfg.localConfig),

View File

@ -36,6 +36,13 @@ matrix {
password: std.strReplace(importstr "secrets/plain/media-repo-matrix-postgres", "\n", ""),
},
},
coturn+: {
enable: true,
config+: {
domain: "turn.hackerspace.pl",
loadBalancerIP: "185.236.240.59",
},
},
},
riot+: {