feat: coturn TURN-relay + bandwidth defaults for mobile
All checks were successful
deploy / deploy (push) Successful in 42s
All checks were successful
deploy / deploy (push) Successful in 42s
Adds a coturn pod that gives clients a relay path when direct UDP to JVB:10001 doesn't make it through carrier NAT (the typical mobile-data failure mode the user hit). Same domain as the rest — meet.it.financeflow.de — because TURN ports (3478/5349) don't collide with the Ingress on 443. - 80-coturn.yaml: hostNetwork Deployment binding STUN+TURN on 3478 (UDP/TCP) and TURNS on 5349 (UDP/TCP), inline-templates turnserver.conf with PUBLIC_IP + TURN_CREDENTIALS_SECRET. TLS cert mounted from the same jitsi-tls Secret cert-manager already manages for the web Ingress. CronJob restarts coturn weekly so cert renewals propagate. - 10-config.yaml: STUN now points at our own coturn; TURN_HOST/TURNS_HOST set so Prosody mod_external_services hands TURN endpoints to clients during XMPP session init. RESOLUTION capped at 480p, START_VIDEO_MUTED=5 keeps large rooms light on bandwidth. - generate-secrets.sh + 20-secrets.yaml.example: TURN_CREDENTIALS_SECRET added so Prosody and coturn share the HMAC key (already pre-synced out-of-band into the cluster). - deploy.yml: sed __PUBLIC_IP__ in coturn manifest, rollout-status coturn. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cb4b2ddaba
commit
cebcf4f567
@ -25,13 +25,13 @@ jobs:
|
||||
chmod 600 "$HOME/.kube/config"
|
||||
kubectl config current-context
|
||||
|
||||
# __PUBLIC_IP__ placeholder in jvb manifest needs the actual public
|
||||
# IP of darkember. Inject from a repo secret so the manifest stays
|
||||
# generic in git.
|
||||
- name: Patch JVB public IP
|
||||
# __PUBLIC_IP__ placeholder lives in JVB + coturn manifests — both
|
||||
# advertise their public address so clients can reach them. Single
|
||||
# sed pass over the directory keeps the secret out of git.
|
||||
- name: Patch __PUBLIC_IP__ in manifests
|
||||
run: |
|
||||
test -n "${{ secrets.DARKEMBER_PUBLIC_IP }}" || (echo "secret DARKEMBER_PUBLIC_IP missing" && exit 1)
|
||||
sed -i "s|__PUBLIC_IP__|${{ secrets.DARKEMBER_PUBLIC_IP }}|g" infra/k3s/60-jvb.yaml
|
||||
sed -i "s|__PUBLIC_IP__|${{ secrets.DARKEMBER_PUBLIC_IP }}|g" infra/k3s/60-jvb.yaml infra/k3s/80-coturn.yaml
|
||||
|
||||
- name: Apply manifests
|
||||
# 20-secrets.yaml is intentionally NOT applied — secret must be
|
||||
@ -45,6 +45,7 @@ jobs:
|
||||
kubectl apply -f infra/k3s/50-web.yaml
|
||||
kubectl apply -f infra/k3s/60-jvb.yaml
|
||||
kubectl apply -f infra/k3s/70-ingress.yaml
|
||||
kubectl apply -f infra/k3s/80-coturn.yaml
|
||||
|
||||
# ConfigMap-only changes don't restart pods on their own, so a
|
||||
# deploy that just edits 10-config.yaml would otherwise leave the
|
||||
@ -59,6 +60,7 @@ jobs:
|
||||
kubectl -n jitsi rollout status deployment/jicofo --timeout=3m
|
||||
kubectl -n jitsi rollout status deployment/jitsi-web --timeout=3m
|
||||
kubectl -n jitsi rollout status deployment/jvb --timeout=3m
|
||||
kubectl -n jitsi rollout status deployment/coturn --timeout=3m
|
||||
|
||||
- name: Smoke-check
|
||||
run: |
|
||||
|
||||
@ -43,8 +43,17 @@ data:
|
||||
# === Videobridge brewery (where jicofo finds JVBs over XMPP) ===
|
||||
JVB_BREWERY_MUC: "jvbbrewery"
|
||||
|
||||
# === STUN — default Jitsi-hosted STUN servers; ok for getting started ===
|
||||
JVB_STUN_SERVERS: "meet-jit-si-turnrelay.jitsi.net:443"
|
||||
# === STUN/TURN — our own coturn (deploy 80-coturn.yaml). JVB itself
|
||||
# uses STUN to discover its public-side mapping; clients additionally
|
||||
# learn the TURN endpoints from Prosody via mod_external_services and
|
||||
# fall back to relay when direct UDP doesn't reach JVB:10001 (typical
|
||||
# for mobile-carrier NATs). ===
|
||||
JVB_STUN_SERVERS: "meet.it.financeflow.de:3478"
|
||||
TURN_HOST: "meet.it.financeflow.de"
|
||||
TURNS_HOST: "meet.it.financeflow.de"
|
||||
TURN_PORT: "3478"
|
||||
TURNS_PORT: "5349"
|
||||
TURN_TRANSPORT: "udp,tcp"
|
||||
|
||||
# === UX / lockdown ===
|
||||
# Pre-join page on — gives joiners a chance to set audio/video before
|
||||
@ -58,3 +67,12 @@ data:
|
||||
ENABLE_CLOSE_PAGE: "0"
|
||||
ENABLE_TRANSCRIPTIONS: "0"
|
||||
ENABLE_RECORDING: "0"
|
||||
|
||||
# === Bandwidth defaults — keep things sane on mobile ===
|
||||
# Cap outgoing video at 480p so even slow connections can stream.
|
||||
# Users on fat pipes can still manually bump it via the toolbar.
|
||||
RESOLUTION: "480"
|
||||
RESOLUTION_MIN: "180"
|
||||
# In rooms with >5 people, new joiners start with video muted —
|
||||
# saves bandwidth in larger team meetings, easy 1-click to enable.
|
||||
START_VIDEO_MUTED: "5"
|
||||
|
||||
@ -25,3 +25,7 @@ stringData:
|
||||
# kubectl -n embertime exec -it deploy/embertime-postgres -- \
|
||||
# psql -U embertime -t -c "select meeting_jwt_secret from app_settings"
|
||||
JWT_APP_SECRET: "REPLACE_WITH_VALUE_FROM_EMBERTIME"
|
||||
# HMAC secret shared between coturn and Prosody. Prosody mints
|
||||
# time-limited TURN credentials; coturn validates with the same key.
|
||||
# Generate fresh via generate-secrets.sh.
|
||||
TURN_CREDENTIALS_SECRET: "REPLACE_WITH_32_RANDOM_CHARS"
|
||||
|
||||
162
infra/k3s/80-coturn.yaml
Normal file
162
infra/k3s/80-coturn.yaml
Normal file
@ -0,0 +1,162 @@
|
||||
# coturn — STUN+TURN relay for clients whose network can't reach JVB
|
||||
# directly. Hosted on the same node as JVB; hostNetwork: true so the
|
||||
# TURN listening ports bind on the public NIC. The TLS cert is the
|
||||
# same one cert-manager already issues for the jitsi-web Ingress —
|
||||
# we mount the jitsi-tls Secret as a volume. Watch out: when the cert
|
||||
# renews, this pod must be restarted to pick up the new file (the
|
||||
# weekly CronJob below handles that).
|
||||
#
|
||||
# Auth model: HMAC time-limited credentials. coturn validates with
|
||||
# `use-auth-secret` + `static-auth-secret=<TURN_CREDENTIALS_SECRET>`;
|
||||
# Prosody hands out matching credentials per session via
|
||||
# mod_external_services. Both read the secret from the same k8s
|
||||
# Secret entry so they stay in lockstep.
|
||||
#
|
||||
# Port plan (mirrors required FritzBox forwards):
|
||||
# UDP 3478 STUN + TURN (cleartext)
|
||||
# TCP 3478 TURN over TCP — first cleartext fallback
|
||||
# UDP 5349 TURN over DTLS
|
||||
# TCP 5349 TURN over TLS — works through most firewalls
|
||||
# UDP 50000-50100 relay range — actual media flows on these
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: coturn
|
||||
namespace: jitsi
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels: { app: coturn }
|
||||
template:
|
||||
metadata:
|
||||
labels: { app: coturn }
|
||||
spec:
|
||||
hostNetwork: true
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
containers:
|
||||
- name: coturn
|
||||
image: coturn/coturn:4.7-alpine
|
||||
# Inline-template the config so we can interpolate the env-var
|
||||
# secret without an extra ConfigMap-then-envsubst dance.
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
cat > /tmp/turnserver.conf <<EOF
|
||||
listening-port=3478
|
||||
tls-listening-port=5349
|
||||
listening-ip=0.0.0.0
|
||||
external-ip=${PUBLIC_IP}
|
||||
relay-ip=${PUBLIC_IP}
|
||||
cert=/certs/tls.crt
|
||||
pkey=/certs/tls.key
|
||||
# Jitsi/Prosody hands out HMAC-signed time-limited creds
|
||||
# using this same secret — coturn validates them.
|
||||
use-auth-secret
|
||||
static-auth-secret=${TURN_CREDENTIALS_SECRET}
|
||||
realm=meet.it.financeflow.de
|
||||
# Relay port range. Forward these on the router.
|
||||
min-port=50000
|
||||
max-port=50100
|
||||
log-file=stdout
|
||||
simple-log
|
||||
no-loopback-peers
|
||||
no-multicast-peers
|
||||
no-tlsv1
|
||||
no-tlsv1_1
|
||||
fingerprint
|
||||
# Don't allow relaying to internal addresses — TURN should
|
||||
# only relay to the public internet.
|
||||
denied-peer-ip=10.0.0.0-10.255.255.255
|
||||
denied-peer-ip=172.16.0.0-172.31.255.255
|
||||
denied-peer-ip=192.168.0.0-192.168.255.255
|
||||
EOF
|
||||
exec turnserver -c /tmp/turnserver.conf
|
||||
env:
|
||||
- name: PUBLIC_IP
|
||||
value: "__PUBLIC_IP__"
|
||||
- name: TURN_CREDENTIALS_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: jitsi-secrets
|
||||
key: TURN_CREDENTIALS_SECRET
|
||||
ports:
|
||||
- { name: stun-udp, containerPort: 3478, hostPort: 3478, protocol: UDP }
|
||||
- { name: stun-tcp, containerPort: 3478, hostPort: 3478, protocol: TCP }
|
||||
- { name: turns-udp, containerPort: 5349, hostPort: 5349, protocol: UDP }
|
||||
- { name: turns-tcp, containerPort: 5349, hostPort: 5349, protocol: TCP }
|
||||
volumeMounts:
|
||||
- { name: tls, mountPath: /certs, readOnly: true }
|
||||
resources:
|
||||
requests: { cpu: 100m, memory: 128Mi }
|
||||
limits: { cpu: 1, memory: 512Mi }
|
||||
volumes:
|
||||
- name: tls
|
||||
secret:
|
||||
secretName: jitsi-tls
|
||||
items:
|
||||
- { key: tls.crt, path: tls.crt }
|
||||
- { key: tls.key, path: tls.key }
|
||||
---
|
||||
# Cert-manager renews the Let's-Encrypt cert ~30 days before expiry.
|
||||
# The Secret content updates automatically, but coturn keeps the old
|
||||
# in-memory cert until it's restarted. Restart once a week — Sundays
|
||||
# 03:00 — gives us plenty of slack vs. the 90-day cert lifetime and
|
||||
# 30-day renewal window.
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: coturn-cert-refresh
|
||||
namespace: jitsi
|
||||
spec:
|
||||
schedule: "0 3 * * 0"
|
||||
concurrencyPolicy: Forbid
|
||||
successfulJobsHistoryLimit: 1
|
||||
failedJobsHistoryLimit: 2
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
serviceAccountName: coturn-restarter
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: kubectl
|
||||
image: bitnami/kubectl:1.30
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- kubectl -n jitsi rollout restart deployment/coturn
|
||||
---
|
||||
# Minimal RBAC for the CronJob — only allows rolling-restart of the
|
||||
# coturn Deployment, nothing else. Scoped to the jitsi namespace.
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: coturn-restarter
|
||||
namespace: jitsi
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: coturn-restarter
|
||||
namespace: jitsi
|
||||
rules:
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments"]
|
||||
resourceNames: ["coturn"]
|
||||
verbs: ["get", "patch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: coturn-restarter
|
||||
namespace: jitsi
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: coturn-restarter
|
||||
namespace: jitsi
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: coturn-restarter
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
@ -17,6 +17,7 @@ rand() { openssl rand -hex 16; } # 32 hex chars = 16 bytes entropy, plenty for
|
||||
JICOFO_COMPONENT_SECRET=$(rand)
|
||||
JICOFO_AUTH_PASSWORD=$(rand)
|
||||
JVB_AUTH_PASSWORD=$(rand)
|
||||
TURN_CREDENTIALS_SECRET=$(rand)
|
||||
|
||||
cat <<EOF
|
||||
apiVersion: v1
|
||||
@ -31,4 +32,5 @@ stringData:
|
||||
JICOFO_AUTH_PASSWORD: "${JICOFO_AUTH_PASSWORD}"
|
||||
JVB_AUTH_USER: "jvb"
|
||||
JVB_AUTH_PASSWORD: "${JVB_AUTH_PASSWORD}"
|
||||
TURN_CREDENTIALS_SECRET: "${TURN_CREDENTIALS_SECRET}"
|
||||
EOF
|
||||
|
||||
Loading…
Reference in New Issue
Block a user