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"
|
chmod 600 "$HOME/.kube/config"
|
||||||
kubectl config current-context
|
kubectl config current-context
|
||||||
|
|
||||||
# __PUBLIC_IP__ placeholder in jvb manifest needs the actual public
|
# __PUBLIC_IP__ placeholder lives in JVB + coturn manifests — both
|
||||||
# IP of darkember. Inject from a repo secret so the manifest stays
|
# advertise their public address so clients can reach them. Single
|
||||||
# generic in git.
|
# sed pass over the directory keeps the secret out of git.
|
||||||
- name: Patch JVB public IP
|
- name: Patch __PUBLIC_IP__ in manifests
|
||||||
run: |
|
run: |
|
||||||
test -n "${{ secrets.DARKEMBER_PUBLIC_IP }}" || (echo "secret DARKEMBER_PUBLIC_IP missing" && exit 1)
|
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
|
- name: Apply manifests
|
||||||
# 20-secrets.yaml is intentionally NOT applied — secret must be
|
# 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/50-web.yaml
|
||||||
kubectl apply -f infra/k3s/60-jvb.yaml
|
kubectl apply -f infra/k3s/60-jvb.yaml
|
||||||
kubectl apply -f infra/k3s/70-ingress.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
|
# ConfigMap-only changes don't restart pods on their own, so a
|
||||||
# deploy that just edits 10-config.yaml would otherwise leave the
|
# 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/jicofo --timeout=3m
|
||||||
kubectl -n jitsi rollout status deployment/jitsi-web --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/jvb --timeout=3m
|
||||||
|
kubectl -n jitsi rollout status deployment/coturn --timeout=3m
|
||||||
|
|
||||||
- name: Smoke-check
|
- name: Smoke-check
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@ -43,8 +43,17 @@ data:
|
|||||||
# === Videobridge brewery (where jicofo finds JVBs over XMPP) ===
|
# === Videobridge brewery (where jicofo finds JVBs over XMPP) ===
|
||||||
JVB_BREWERY_MUC: "jvbbrewery"
|
JVB_BREWERY_MUC: "jvbbrewery"
|
||||||
|
|
||||||
# === STUN — default Jitsi-hosted STUN servers; ok for getting started ===
|
# === STUN/TURN — our own coturn (deploy 80-coturn.yaml). JVB itself
|
||||||
JVB_STUN_SERVERS: "meet-jit-si-turnrelay.jitsi.net:443"
|
# 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 ===
|
# === UX / lockdown ===
|
||||||
# Pre-join page on — gives joiners a chance to set audio/video before
|
# Pre-join page on — gives joiners a chance to set audio/video before
|
||||||
@ -58,3 +67,12 @@ data:
|
|||||||
ENABLE_CLOSE_PAGE: "0"
|
ENABLE_CLOSE_PAGE: "0"
|
||||||
ENABLE_TRANSCRIPTIONS: "0"
|
ENABLE_TRANSCRIPTIONS: "0"
|
||||||
ENABLE_RECORDING: "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 -- \
|
# kubectl -n embertime exec -it deploy/embertime-postgres -- \
|
||||||
# psql -U embertime -t -c "select meeting_jwt_secret from app_settings"
|
# psql -U embertime -t -c "select meeting_jwt_secret from app_settings"
|
||||||
JWT_APP_SECRET: "REPLACE_WITH_VALUE_FROM_EMBERTIME"
|
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_COMPONENT_SECRET=$(rand)
|
||||||
JICOFO_AUTH_PASSWORD=$(rand)
|
JICOFO_AUTH_PASSWORD=$(rand)
|
||||||
JVB_AUTH_PASSWORD=$(rand)
|
JVB_AUTH_PASSWORD=$(rand)
|
||||||
|
TURN_CREDENTIALS_SECRET=$(rand)
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
@ -31,4 +32,5 @@ stringData:
|
|||||||
JICOFO_AUTH_PASSWORD: "${JICOFO_AUTH_PASSWORD}"
|
JICOFO_AUTH_PASSWORD: "${JICOFO_AUTH_PASSWORD}"
|
||||||
JVB_AUTH_USER: "jvb"
|
JVB_AUTH_USER: "jvb"
|
||||||
JVB_AUTH_PASSWORD: "${JVB_AUTH_PASSWORD}"
|
JVB_AUTH_PASSWORD: "${JVB_AUTH_PASSWORD}"
|
||||||
|
TURN_CREDENTIALS_SECRET: "${TURN_CREDENTIALS_SECRET}"
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user