Retire the shared coturn service — every WebRTC/SIP service now runs its own
Shared coturn (services/coturn.sh, ensure_coturn_user in lib/common.sh) is no longer an installable or usable option anywhere in this repo. It's moved to attic/coturn.sh (with tools/coturn-test-check.sh alongside it), which is outside setup.sh's services/*.sh glob, so it never registers, never appears in the menu, and `sudo ./setup.sh coturn` now fails with "unknown service". Asterisk and Mattermost each already had an opt-out to run their own dedicated coturn instead of the shared one; that opt-out is now the only behavior — the shared-coturn preference, the opt-out prompt, and every ensure_coturn_user() call site are gone. find_free_coturn_range() (lib/common.sh) is what makes unconditional dedicated coturn safe: it scans every coturn-owning service's own .env on the box for already-claimed relay ranges and picks one that can't collide, so Asterisk + any number of Mattermost instances can each run their own coturn on one box without the relay-port collisions this repo's coturn history warns about. Existing installs still pointed at a shared coturn container are left running as-is on `update` (no silent migration attempt against a service that no longer exists to heal against) — a full/fresh reinstall is the migration path, which generates a new dedicated coturn with fresh credentials and says so. Also updates CLAUDE.md's coturn guidance for future service authors, attic/README.md with the retirement rationale, and stale services/coturn.sh path references in services/asterisk.sh, tools/pstn-test-check.sh, README.md, and docs/vps-sizing-recommendations.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crt4ymNEHEbWqscB1qvZgC
This commit is contained in:
@@ -1,218 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# tools/coturn-test-check.sh — Health-check for the shared coturn (TURN/STUN)
|
||||
# instance services/coturn.sh sets up, and every consumer registered against
|
||||
# it (Asterisk, one or more Mattermost instances, anything else added via
|
||||
# ensure_coturn_user() in lib/common.sh).
|
||||
#
|
||||
# Checks: container up, identity/.env readable, every registered consumer
|
||||
# actually exists in coturn's own user database (not just a cached
|
||||
# users/<name>.env file — the two can drift, e.g. a container recreated from
|
||||
# an older image/db), UFW has the TURN port + relay range open, and — the
|
||||
# part nothing else in this repo does — a REAL TURN allocation test per
|
||||
# consumer via turnutils_uclient (bundled in the coturn/coturn image), which
|
||||
# is the only way to prove credentials + port range + firewall all actually
|
||||
# work together end to end, not just that each piece looks right in isolation.
|
||||
#
|
||||
# Does NOT attempt a concurrent load test (e.g. opening dozens of allocations
|
||||
# at once) — that would consume real relay ports on a server other services
|
||||
# may be actively using. See "Capacity" in the output for how the port range
|
||||
# bounds concurrent capacity, reasoned from the numbers instead of guessed at.
|
||||
#
|
||||
# Usage:
|
||||
# sudo bash tools/coturn-test-check.sh
|
||||
#
|
||||
# Safe to run any time — the one allocation test per consumer opens and
|
||||
# immediately releases a single relay port, the same as a single real call
|
||||
# briefly would.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
PASS=0
|
||||
WARN=0
|
||||
FAIL=0
|
||||
|
||||
ok() { printf ' [OK] %s\n' "$1"; PASS=$((PASS + 1)); }
|
||||
warn() { printf ' [WARN] %s\n' "$1"; WARN=$((WARN + 1)); }
|
||||
fail() { printf ' [FAIL] %s\n' "$1"; FAIL=$((FAIL + 1)); }
|
||||
section() { printf '\n== %s ==\n' "$1"; }
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Run with sudo — needs docker exec." >&2
|
||||
exec sudo bash "$0" "$@"
|
||||
fi
|
||||
|
||||
ACTUAL_USER="${SUDO_USER:-${USER:-root}}"
|
||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "/root")"
|
||||
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
||||
COTURN_DIR="$DOCKER_DIR/coturn"
|
||||
|
||||
# ── Container + identity ──────────────────────────────────────────────────────
|
||||
section "Detecting install"
|
||||
|
||||
if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -qx coturn; then
|
||||
fail "No running 'coturn' container found — is services/coturn.sh installed and started?"
|
||||
echo ""
|
||||
echo " $PASS passed, $WARN warnings, $FAIL failed. Stopping."
|
||||
exit 1
|
||||
fi
|
||||
ok "Container running: coturn"
|
||||
|
||||
if [ ! -f "$COTURN_DIR/.env" ]; then
|
||||
fail "$COTURN_DIR/.env not found — can't read realm/host/port range."
|
||||
exit 1
|
||||
fi
|
||||
set +u
|
||||
# shellcheck disable=SC1090
|
||||
source "$COTURN_DIR/.env"
|
||||
set -u
|
||||
COTURN_REALM="${COTURN_REALM:-}"
|
||||
COTURN_HOST="${COTURN_HOST:-}"
|
||||
COTURN_PORT="${COTURN_PORT:-3478}"
|
||||
COTURN_MIN_PORT="${COTURN_MIN_PORT:-49152}"
|
||||
COTURN_MAX_PORT="${COTURN_MAX_PORT:-49452}"
|
||||
ok "Realm: ${COTURN_REALM:-<unset>} Host: ${COTURN_HOST:-<unset>} Port: $COTURN_PORT"
|
||||
ok "Relay port range: ${COTURN_MIN_PORT}-${COTURN_MAX_PORT}"
|
||||
|
||||
# ── Registered consumers ──────────────────────────────────────────────────────
|
||||
section "Registered consumers"
|
||||
|
||||
# turnadmin -l writes its own startup log lines ("INFO SQLite connection
|
||||
# was closed.", "INFO log file opened: ...") to STDOUT on at least some
|
||||
# coturn builds, not stderr — confirmed live, `2>/dev/null` alone let them
|
||||
# through and got misparsed as usernames. A real "user[realm]" line never
|
||||
# contains a space; every log line here does, so filtering those out is a
|
||||
# safe, simple way to keep only genuine entries regardless of which coturn
|
||||
# build's log-noise happens to leak onto stdout.
|
||||
DB_USERS="$(docker exec coturn turnadmin -l -b /var/lib/coturn/turndb 2>/dev/null | grep -v ' ' | sed -E 's/\[.*//' | awk 'NF' | sort -u)"
|
||||
if [ -z "$DB_USERS" ]; then
|
||||
warn "No users found in coturn's own database — nothing has actually registered yet, or turnadmin -l's output format changed. Raw:"
|
||||
docker exec coturn turnadmin -l -b /var/lib/coturn/turndb 2>&1 | sed 's/^/ /'
|
||||
fi
|
||||
|
||||
CACHED_CONSUMERS=()
|
||||
if [ -d "$COTURN_DIR/users" ]; then
|
||||
while IFS= read -r f; do
|
||||
CACHED_CONSUMERS+=("$(basename "$f" .env)")
|
||||
done < <(find "$COTURN_DIR/users" -maxdepth 1 -name '*.env' -type f 2>/dev/null | sort)
|
||||
fi
|
||||
|
||||
if [ "${#CACHED_CONSUMERS[@]}" -eq 0 ]; then
|
||||
warn "No cached consumer credentials in $COTURN_DIR/users — nothing has registered via ensure_coturn_user() yet."
|
||||
else
|
||||
for c in "${CACHED_CONSUMERS[@]}"; do
|
||||
if grep -qx "$c" <<< "$DB_USERS"; then
|
||||
ok "Consumer '$c' — cached credentials present AND found in coturn's live database"
|
||||
else
|
||||
fail "Consumer '$c' has a cached users/${c}.env but is NOT in coturn's database — its calls will fail 401 Unauthorized. Likely cause: the coturn container/volume was recreated without preserving ./db. Fix: sudo docker exec coturn turnadmin -a -u $c -p <password from users/${c}.env> -r $COTURN_REALM -b /var/lib/coturn/turndb"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Flag anything in the live DB with no cached file too — orphaned/manually
|
||||
# added users aren't wrong, just worth knowing about.
|
||||
while IFS= read -r u; do
|
||||
[ -z "$u" ] && continue
|
||||
found=false
|
||||
for c in "${CACHED_CONSUMERS[@]:-}"; do [ "$c" = "$u" ] && found=true && break; done
|
||||
[ "$found" = false ] && warn "Database has user '$u' with no matching users/${u}.env — added manually, or a leftover from a removed service."
|
||||
done <<< "$DB_USERS"
|
||||
|
||||
# ── Firewall ───────────────────────────────────────────────────────────────────
|
||||
section "Firewall (UFW)"
|
||||
if command -v ufw &>/dev/null; then
|
||||
UFW_STATUS="$(ufw status 2>/dev/null)"
|
||||
if grep -qE "^${COTURN_PORT}(/udp|/tcp)?\b.*ALLOW" <<< "$UFW_STATUS"; then
|
||||
ok "TURN listening port ${COTURN_PORT} allowed"
|
||||
else
|
||||
fail "TURN listening port ${COTURN_PORT} not found in 'ufw status' — clients may not reach it"
|
||||
fi
|
||||
if grep -qE "^${COTURN_MIN_PORT}:${COTURN_MAX_PORT}/udp\b.*ALLOW" <<< "$UFW_STATUS"; then
|
||||
ok "Relay port range ${COTURN_MIN_PORT}-${COTURN_MAX_PORT}/udp allowed"
|
||||
else
|
||||
fail "Relay port range ${COTURN_MIN_PORT}-${COTURN_MAX_PORT}/udp not found in 'ufw status' — allocated relay ports would be unreachable, breaking media even after a successful TURN allocation"
|
||||
fi
|
||||
else
|
||||
warn "ufw not installed — can't confirm the relay range is actually open (may be fine if this box has no firewall, or one outside UFW)"
|
||||
fi
|
||||
|
||||
# ── Capacity ───────────────────────────────────────────────────────────────────
|
||||
section "Capacity"
|
||||
RANGE_SIZE=$((COTURN_MAX_PORT - COTURN_MIN_PORT + 1))
|
||||
CONSUMER_COUNT="${#CACHED_CONSUMERS[@]}"
|
||||
echo " Relay range holds ${RANGE_SIZE} ports. Each concurrent relayed call/leg typically"
|
||||
echo " uses one allocation (roughly one port) for its lifetime — released when the call"
|
||||
echo " ends, not held permanently. With ${CONSUMER_COUNT} registered consumer(s), the range"
|
||||
echo " would need all of them to have ~$((RANGE_SIZE / (CONSUMER_COUNT > 0 ? CONSUMER_COUNT : 1))) simultaneous relayed calls each, at the same"
|
||||
echo " moment, before it runs out — for Asterisk + a handful of Mattermost instances at"
|
||||
echo " personal/small-team scale, that ceiling is not realistically reachable in normal"
|
||||
echo " use. If you ever DO expect that much simultaneous WebRTC/SIP relay traffic, raise"
|
||||
echo " COTURN_MIN_PORT/COTURN_MAX_PORT in $COTURN_DIR/.env, update the matching UFW rule,"
|
||||
echo " and restart coturn — no consumer reconfiguration needed, they don't cache the range."
|
||||
echo ""
|
||||
echo " Note: not every call needs a TURN relay at all — TURN is the FALLBACK when two"
|
||||
echo " peers can't reach each other directly (STUN/ICE finds a direct path first when"
|
||||
echo " possible). Real relay usage is usually well below \"every concurrent call.\""
|
||||
|
||||
# ── Real allocation test per consumer ─────────────────────────────────────────
|
||||
section "Live allocation test (one real TURN allocation per registered consumer)"
|
||||
if ! docker exec coturn which turnutils_uclient &>/dev/null; then
|
||||
warn "turnutils_uclient not found in the coturn image — skipping live allocation tests."
|
||||
else
|
||||
TEST_HOST="${COTURN_HOST:-127.0.0.1}"
|
||||
for c in "${CACHED_CONSUMERS[@]:-}"; do
|
||||
[ -z "$c" ] && continue
|
||||
_u="$(grep '^COTURN_USER=' "$COTURN_DIR/users/${c}.env" 2>/dev/null | cut -d= -f2-)"
|
||||
_p="$(grep '^COTURN_PASS=' "$COTURN_DIR/users/${c}.env" 2>/dev/null | cut -d= -f2-)"
|
||||
if [ -z "$_u" ] || [ -z "$_p" ]; then
|
||||
warn "$c: couldn't read cached credentials, skipping live test"
|
||||
continue
|
||||
fi
|
||||
# Plain UDP only — no -t/-T (TCP/TLS) flags; coturn runs with
|
||||
# --no-tls --no-dtls (services/coturn.sh), so requesting an
|
||||
# encrypted/TCP transport here fails the allocation against a
|
||||
# server that never offered one. See tools/pstn-test-check.sh's
|
||||
# matching comment — confirmed live this was the actual cause of a
|
||||
# "Cannot complete Allocation" failure, not a real coturn problem.
|
||||
#
|
||||
# -y ("client-to-client"), not -e <peer>: turnutils_uclient refuses
|
||||
# to run at all without one of the two ("Either -e peer_address or
|
||||
# -y must be specified", confirmed live), but -e needs an actual
|
||||
# reachable, non-loopback peer — services/coturn.sh never sets
|
||||
# --allow-loopback-peers, so -e 127.0.0.1 gets rejected with
|
||||
# "channel bind: error 403 (Forbidden IP)" (confirmed live against
|
||||
# a real local coturn instance built specifically to test this).
|
||||
# -y negotiates both ends of a real relay through the server
|
||||
# itself, no separate peer needed, and works over loopback —
|
||||
# confirmed correctly reporting success (exit 0, real packet-loss
|
||||
# stats) with valid credentials and failure ("Cannot complete
|
||||
# Allocation", exit 255) with a wrong password.
|
||||
OUT="$(docker exec coturn timeout 20 turnutils_uclient -u "$_u" -w "$_p" -y "$TEST_HOST" -p "$COTURN_PORT" 2>&1)"
|
||||
RC=$?
|
||||
if [ "$RC" -eq 0 ]; then
|
||||
ok "$c: TURN allocation succeeded (credentials + relay range + reachability all confirmed working)"
|
||||
elif [ "$RC" -eq 124 ]; then
|
||||
# timeout(1)'s own exit code — no error was printed yet when the
|
||||
# clock ran out, so this isn't a reported failure like "Cannot
|
||||
# complete Allocation" would be. Worth a look, not a hard FAIL.
|
||||
warn "$c: TURN test didn't finish within 20s (no error printed — likely still negotiating). Raw output so far:"
|
||||
echo "$OUT" | tail -n 15 | sed 's/^/ /'
|
||||
else
|
||||
fail "$c: TURN allocation failed (exit $RC) — raw output:"
|
||||
echo "$OUT" | tail -n 15 | sed 's/^/ /'
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
section "Summary"
|
||||
echo " $PASS passed, $WARN warnings, $FAIL failed."
|
||||
echo ""
|
||||
echo " A passing allocation test here proves TURN works end to end for that consumer."
|
||||
echo " It does NOT by itself prove Asterisk or Mattermost are actually configured to USE"
|
||||
echo " it — check each service's own .env for TURN_HOST/TURN_USERNAME (asterisk.sh) or"
|
||||
echo " the Calls plugin's ICE Servers Configurations (mattermost.sh) matches what's"
|
||||
echo " printed above, then place a real call from outside the LAN (the case TURN"
|
||||
echo " actually exists for — two peers on the same LAN usually connect directly and never"
|
||||
echo " touch the relay at all, so a same-LAN test call proves nothing about TURN)."
|
||||
|
||||
[ "$FAIL" -eq 0 ]
|
||||
@@ -172,8 +172,9 @@ fi
|
||||
# Anveo-style ICE-enabled endpoints. Asterisk caches its OWN TURN_* values
|
||||
# in its .env at the point it was configured — testing with those (not
|
||||
# re-deriving fresh credentials) proves what Asterisk is actually set up
|
||||
# to use, not just that the shared coturn instance works in general (that
|
||||
# broader, multi-consumer check is tools/coturn-test-check.sh's job). ─────────
|
||||
# to use. Every current install runs its own dedicated coturn; a box that
|
||||
# still points at a legacy shared coturn instance predates that (see
|
||||
# attic/coturn.sh) and can be spot-checked with attic/coturn-test-check.sh. ─────────
|
||||
section "coturn (TURN relay for Asterisk)"
|
||||
|
||||
ASTERISK_ENV="$EA_DIR/.env"
|
||||
@@ -197,11 +198,11 @@ else
|
||||
if grep -q '^ coturn:' "$EA_DIR/docker-compose.yml" 2>/dev/null; then
|
||||
COTURN_CONTAINER="easy-asterisk-coturn"
|
||||
[[ "$CONTAINER" == *-do ]] && COTURN_CONTAINER="easy-asterisk-do-coturn"
|
||||
ok "Using an embedded, per-Asterisk coturn ($COTURN_CONTAINER) — not the shared"
|
||||
ok "instance, so tools/coturn-test-check.sh won't see this one; tested separately below."
|
||||
ok "Using an embedded, per-Asterisk coturn ($COTURN_CONTAINER); tested separately below."
|
||||
else
|
||||
COTURN_CONTAINER="coturn"
|
||||
ok "Using the shared coturn instance (also covered by tools/coturn-test-check.sh)"
|
||||
ok "Using a legacy shared coturn instance (this repo no longer installs this shape —"
|
||||
ok "see attic/coturn.sh; also covered by attic/coturn-test-check.sh)"
|
||||
fi
|
||||
|
||||
if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$COTURN_CONTAINER"; then
|
||||
@@ -210,7 +211,8 @@ else
|
||||
warn "turnutils_uclient not found in $COTURN_CONTAINER — skipping live allocation test"
|
||||
else
|
||||
# Plain UDP only — no -t/-T (TCP/TLS) flags. coturn is started with
|
||||
# --no-tls --no-dtls (services/coturn.sh), so requesting an
|
||||
# --no-tls --no-dtls (services/asterisk.sh's embedded coturn, and
|
||||
# attic/coturn.sh's legacy shared one — same flags either way), so requesting an
|
||||
# encrypted/TCP transport here just fails the allocation outright
|
||||
# against a server that never offered one, misreporting a config
|
||||
# problem that doesn't exist. Confirmed live: this was the actual
|
||||
@@ -220,7 +222,7 @@ else
|
||||
# turnutils_uclient also refuses to run at all without either -e
|
||||
# <peer> or -y ("Either -e peer_address or -y must be specified",
|
||||
# confirmed live). -e needs an actual reachable, non-loopback peer
|
||||
# to relay through — services/coturn.sh never sets
|
||||
# to relay through — this repo's coturn containers never set
|
||||
# --allow-loopback-peers, so -e 127.0.0.1 gets rejected with
|
||||
# "channel bind: error 403 (Forbidden IP)" (also confirmed live,
|
||||
# against a real local coturn instance built to test this exact
|
||||
|
||||
Reference in New Issue
Block a user