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:
Claude
2026-08-13 02:12:32 +00:00
parent d835e0d734
commit ca239a3886
10 changed files with 242 additions and 367 deletions
+62 -53
View File
@@ -674,7 +674,7 @@ interpolates `${WEB_PORT}` directly. A quoted `<< 'MD'` heredoc doesn't,
and converting it means escaping *every* backtick used for inline-code and converting it means escaping *every* backtick used for inline-code
formatting (`` \`...\` ``) — miss one and bash tries to execute it as a formatting (`` \`...\` ``) — miss one and bash tries to execute it as a
command substitution the next time the heredoc is read, the same class of command substitution the next time the heredoc is read, the same class of
bug the coturn.sh backtick incident was (see `services/coturn.sh`'s bug the coturn.sh backtick incident was (see `attic/coturn.sh`'s
`write_readme` call). For a README with only one or two backticks, `write_readme` call). For a README with only one or two backticks,
escaping them is fine. For one with many (`services/iopaint.sh`'s model escaping them is fine. For one with many (`services/iopaint.sh`'s model
reference table), it's safer to leave the heredoc quoted and patch the reference table), it's safer to leave the heredoc quoted and patch the
@@ -778,63 +778,72 @@ so that hostname resolves; `configure_caddy_for_service`'s bare-port upstream
case already does this for you — don't hand-roll `localhost:PORT` in a case already does this for you — don't hand-roll `localhost:PORT` in a
Caddy site block. Caddy site block.
## Shared coturn (TURN/STUN) relay ## coturn (TURN/STUN) relay — dedicated per service, not shared
Any service that needs a TURN server for WebRTC/SIP NAT traversal shares Any service that needs a TURN server for WebRTC/SIP NAT traversal runs its
**one** coturn instance (`services/coturn.sh`) instead of running its own. **own dedicated** coturn container. There is no shared coturn service to
This exists because it didn't always: `asterisk` and `mattermost` used to install or point at — `services/coturn.sh` was tried and retired; it's
each embed a dedicated coturn container (`network_mode: host`, each with its parked at `attic/coturn.sh` (outside `services/*.sh`'s glob, so it never
own relay port range) — confirmed live, their default ranges overlapped by registers or appears in the menu — see `attic/README.md`). Sharing one
~100 UDP ports, so running both on one box meant a coin-flip over which instance saved a container per consumer (~40MB) but was a single point of
service's active call lost its media relay. One shared instance with one failure every consumer depended on, and needing a dedicated per-consumer
port range removes the collision instead of just moving it around. long-term-credential user added real setup complexity for a small RAM win.
Don't reintroduce it — give every new WebRTC/SIP-capable service its own
coturn, following the pattern below.
**Use `ensure_coturn_user` (`lib/common.sh`), not your own coturn container:** **The collision this pattern has to avoid:** `asterisk` and `mattermost`
each embed a dedicated coturn container (`network_mode: host`, each with
its own relay port range) — confirmed live, two independent coturns'
default ranges used to overlap by ~100 UDP ports, so running both on one
box meant a coin-flip over which service's active call lost its media
relay. Static default ranges alone don't solve this; something has to pick
non-overlapping ranges per box.
**Use `find_free_coturn_range` (`lib/common.sh`) to size the range, not a
hardcoded default:**
```bash ```bash
ensure_coturn_user "my-service" local MY_COTURN_MIN_PORT=49152 MY_COTURN_MAX_PORT=49252
if [ -n "$COTURN_HOST" ]; then find_free_coturn_range MY_COTURN_MIN_PORT MY_COTURN_MAX_PORT 100 49152
# Out-params (not `local` — read them after the call returns, same [[ "$MY_COTURN_MIN_PORT" != 49152 ]] && \
# convention as configure_caddy_for_service's CADDY_SERVICE_*): log_info "Dedicated coturn relay range shifted to ${MY_COTURN_MIN_PORT}-${MY_COTURN_MAX_PORT} to stay clear of another coturn already on this box."
# COTURN_HOST COTURN_PORT COTURN_USERNAME COTURN_PASSWORD
else
# coturn unavailable (not installed and services/coturn.sh isn't loaded
# to chain-install it — e.g. this file run fully standalone) — degrade
# gracefully. Don't block the rest of your install on this.
fi
``` ```
`ensure_coturn_user` chain-installs `services/coturn.sh` the first time Unlike a single fixed host port (`find_free_port`'s job — coturn's relay
*any* service needs one (guarded with `declare -F install_coturn`, same range isn't a statically bound listening socket you can detect with a live
pattern as the asterisk → security-dashboard chaining below), then `ss`/socket scan), `find_free_coturn_range` scans every `$DOCKER_DIR/*/.env`
registers a dedicated long-term-credential username/password for your for a `TURN_MAX_PORT=` line and starts the new range 50 ports past the
consumer name. The credential is cached in highest one found — so it works across every coturn-owning service on the
`~/docker/coturn/users/<consumer>.env`, so calling this again on a rerun box (Asterisk, each Mattermost instance, yours), regardless of install
reuses the same credential instead of minting a new one and silently order. Persist the chosen range as `TURN_MIN_PORT=`/`TURN_MAX_PORT=` in your
orphaning whatever client already has the old one configured. own `.env` so later installs' scans see it, and on an `update` rerun read
those same keys back from the existing `.env` instead of re-scanning — a
live coturn container must never silently move to a different port range
(breaks in-flight/repeat sessions on whatever client already has the old
range's ports allowed through its own firewall/NAT). See
`services/asterisk.sh`'s and `services/mattermost.sh`'s `EMBEDDED_COTURN_MIN_PORT`/
`MM_COTURN_MIN_PORT` handling for the reference pattern, including the
`MODE != "update"` gate that scans only on a fresh install.
**Why long-term credentials (`--lt-cred-mech`), not the REST-API/HMAC mode **Auth mode — long-term credentials (`--lt-cred-mech`) or HMAC
(`--use-auth-secret`) some WebRTC apps default to:** coturn does not support (`--use-auth-secret`), your choice per instance:** coturn doesn't support
running both auth mechanisms on one instance at once — enabling running both on one instance at once, but since each service now owns its
`--use-auth-secret` silently overrides `--lt-cred-mech` server-wide, which instance outright, this is a free per-service choice — no shared-instance
would break every static-credential consumer. `--lt-cred-mech` supports any constraint forcing one mode across every consumer. `services/asterisk.sh`
number of named users out of the box, which is the actual shape a uses `--lt-cred-mech` (fixed username/password, simplest to bake into a SIP
shared-multi-consumer coturn needs. If the service you're adding only device's config); `services/mattermost.sh` uses `--use-auth-secret` (HMAC),
exposes an HMAC-secret TURN setting in its own UI (no plain matching the Calls plugin's own "TURN Static Auth Secret" field. Check the
username/password option), check its docs for an alternative field first — consuming app's own TURN settings UI for which fields it actually exposes
Mattermost's Calls plugin looked HMAC-only at a glance but also accepts a before picking.
fixed username/credential pair via its "ICE Servers Configurations" JSON
field (see `services/mattermost.sh` for the exact format). Don't fall back
to a second coturn instance just because the first field you found expects
a shared secret.
**Migrating an existing service from its own embedded coturn:** don't do it **Legacy installs still on the old shared coturn:** an `update` rerun on an
silently. An `update` rerun must keep whatever coturn shape a service install that predates this repo's dedicated-coturn-only model (no `coturn:`
already has — detect the existing embedded container (e.g. `grep -q '^ block in its `docker-compose.yml`) must not try to silently migrate or
coturn:' docker-compose.yml` before regenerating it) and preserve it "heal" it — there's no shared coturn service left in this repo to heal it
exactly, the same non-destructive rule as every other `update` path in this against. Leave it running exactly as-is (an `update` never touches `.env`
file. Only switch to the shared coturn on an explicit `fresh` reinstall, and anyway) and point at a full/fresh reinstall as the migration path, which
warn before doing it — the TURN username/password changes, and any generates a new dedicated coturn container with fresh credentials. See the
already-configured client (a SIP phone, a browser session) keeps the old `_HAD_EMBEDDED_COTURN` handling in `services/asterisk.sh` and
credentials until it's reconfigured. See `services/asterisk.sh`'s `services/mattermost.sh` for the reference pattern — detect via `grep -q
`USE_EMBEDDED_COTURN` handling for the reference pattern. '^ coturn:' docker-compose.yml` before regenerating it, same as any other
non-destructive `update` path in this file.
+1 -2
View File
@@ -185,7 +185,7 @@ a ready-to-copy Caddy config snippet to `~/docker/caddy-snippets/`.
| Group | Services | | Group | Services |
|-------|---------| |-------|---------|
| `base` | `net-tools`, `ncdu`, `git`, `curl`, `wget`, `htop`, `tree`, `zip`/`unzip`, `ca-certificates`, `gnupg`, `jq`, `rsync`; `glow` (terminal markdown reader, Charm apt repo); Docker CE + Compose plugin; `openssh-server` with GitHub/Launchpad SSH key import, optional password-auth lockdown, and SSH Host aliases; optional NetBird overlay network | | `base` | `net-tools`, `ncdu`, `git`, `curl`, `wget`, `htop`, `tree`, `zip`/`unzip`, `ca-certificates`, `gnupg`, `jq`, `rsync`; `glow` (terminal markdown reader, Charm apt repo); Docker CE + Compose plugin; `openssh-server` with GitHub/Launchpad SSH key import, optional password-auth lockdown, and SSH Host aliases; optional NetBird overlay network |
| `homelab` | `caddy`, `crowdsec`, `authelia`, `coturn` (shared TURN/STUN relay — Asterisk, Mattermost Calls, and future WebRTC-capable services all register a dedicated credential against one instance instead of each running its own), `homeassistant`, `asterisk`, `pstn-trunk`, `sms-inbound`, `security-dashboard`, `sunshine`, `vpn-data-mount` (mount existing SMB shares from a NetBird-connected home box — SSH trust bootstrap, then read-only discovery of shares already configured there; never writes to the home box's Samba config; repeatable, pick from any number of a home box's shares in one pass; optional per-share [gocryptfs decrypt layer](#client-side-encryption-for-vpn-data-mount) so the VPS only ever handles ciphertext) | | `homelab` | `caddy`, `crowdsec`, `authelia`, `homeassistant`, `asterisk` (own dedicated coturn for TURN/STUN — see `mattermost` below for the other coturn-owning service), `pstn-trunk`, `sms-inbound`, `security-dashboard`, `sunshine`, `vpn-data-mount` (mount existing SMB shares from a NetBird-connected home box — SSH trust bootstrap, then read-only discovery of shares already configured there; never writes to the home box's Samba config; repeatable, pick from any number of a home box's shares in one pass; optional per-share [gocryptfs decrypt layer](#client-side-encryption-for-vpn-data-mount) so the VPS only ever handles ciphertext) |
| `utilities` | `actualbudget`, `ai-gpu`, `ai-stack`, `archivebox`, `beszel` (lightweight server + Docker monitoring — CPU/RAM/disk/network, auto-discovers running containers via the Docker socket; complements Gatus rather than replacing it — Gatus is a black-box HTTP check, Beszel is white-box host/process monitoring), `beszel-agent` (agent-only Beszel install for a remote/homelab box reporting to a hub elsewhere — connects outbound over HTTPS, no VPN/port-forwarding/FQDN needed on that box), `changedetection`, `ddclient`, `filebrowser`, `fmd`, `gatus`, `homebox`, `iopaint`, `joplin`, `koha`, `magicmirror`, `mail-archiver`, `mattermost`, `mealie`, `meshcentral`, `n8n`, `nextcloud`, `ntfy`, `onlyoffice`, `paintplus`, `portainer`, `rustdesk`, `stirling-pdf`, `syncthing`, `traccar`, `unifi`, `uptimekuma`, `vaultwarden`, `watchyourlan`, `watchtower`, `wg-easy`, `wordpress` (multi-site, dedicated MariaDB per site — blogs, business sites, e-commerce via WooCommerce) | | `utilities` | `actualbudget`, `ai-gpu`, `ai-stack`, `archivebox`, `beszel` (lightweight server + Docker monitoring — CPU/RAM/disk/network, auto-discovers running containers via the Docker socket; complements Gatus rather than replacing it — Gatus is a black-box HTTP check, Beszel is white-box host/process monitoring), `beszel-agent` (agent-only Beszel install for a remote/homelab box reporting to a hub elsewhere — connects outbound over HTTPS, no VPN/port-forwarding/FQDN needed on that box), `changedetection`, `ddclient`, `filebrowser`, `fmd`, `gatus`, `homebox`, `iopaint`, `joplin`, `koha`, `magicmirror`, `mail-archiver`, `mattermost`, `mealie`, `meshcentral`, `n8n`, `nextcloud`, `ntfy`, `onlyoffice`, `paintplus`, `portainer`, `rustdesk`, `stirling-pdf`, `syncthing`, `traccar`, `unifi`, `uptimekuma`, `vaultwarden`, `watchyourlan`, `watchtower`, `wg-easy`, `wordpress` (multi-site, dedicated MariaDB per site — blogs, business sites, e-commerce via WooCommerce) |
| `media` | `arm`, `audiobookshelf`, `calibre-web`, `emby`, `immich`, `jellyfin`, `lyrion` | | `media` | `arm`, `audiobookshelf`, `calibre-web`, `emby`, `immich`, `jellyfin`, `lyrion` |
| `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` | | `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` |
@@ -207,7 +207,6 @@ homelab
caddy caddy
crowdsec crowdsec
authelia authelia
coturn
homeassistant homeassistant
asterisk asterisk
pstn-trunk pstn-trunk
+30
View File
@@ -34,3 +34,33 @@ rather than `fresh` at the reinstall prompt, and having a snapshot.
Two copies of the same logic is the exact problem the merge existed to fix, Two copies of the same logic is the exact problem the merge existed to fix,
and this one will drift the moment `services/asterisk.sh` gets a fix that and this one will drift the moment `services/asterisk.sh` gets a fix that
isn't backported here — which it deliberately won't be. isn't backported here — which it deliberately won't be.
## `coturn.sh` / `coturn-test-check.sh`
The shared-coturn service (`services/coturn.sh`, moved here unchanged from
`services/`) and its standalone health-check tool (`tools/coturn-test-check.sh`,
moved from `tools/`). This model — every WebRTC/SIP-capable service
(`asterisk`, `mattermost`) sharing one coturn instance via `ensure_coturn_user`
— is no longer offered anywhere in this repo. Every service now runs its own
dedicated coturn instead, with `lib/common.sh`'s `find_free_coturn_range()`
avoiding the relay-port collisions a shared instance used to prevent by
scanning every coturn-owning service's own `.env` on the box. See
`CLAUDE.md`'s "coturn (TURN/STUN) relay" section for the current pattern.
Sharing one instance only ever saved ~40MB RAM per additional consumer
beyond the first — real, but small — against being a single point of
failure every consumer depended on. Parked here, not deleted, since the
code is still correct and someone could resurrect it if a future need for
it shows up. Both files still run standalone if invoked directly:
```bash
sudo bash attic/coturn.sh
sudo bash attic/coturn-test-check.sh
```
Nothing in this repo calls `ensure_coturn_user()` anymore (the function
itself was removed from `lib/common.sh`), so resurrecting this only makes
sense if you're deliberately reintroducing the shared-coturn pattern
yourself — a new consumer service would need its own call to whatever
takes `ensure_coturn_user`'s place, since that helper no longer exists to
call.
@@ -1,4 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# attic/coturn-test-check.sh — RETIRED along with attic/coturn.sh (formerly
# services/coturn.sh). This tool only makes sense against a shared coturn
# instance, which this repo no longer offers — see attic/coturn.sh's header
# for what replaced it (each service gets its own dedicated coturn now).
# Kept for reference alongside it, not actively maintained.
#
# tools/coturn-test-check.sh — Health-check for the shared coturn (TURN/STUN) # tools/coturn-test-check.sh — Health-check for the shared coturn (TURN/STUN)
# instance services/coturn.sh sets up, and every consumer registered against # instance services/coturn.sh sets up, and every consumer registered against
# it (Asterisk, one or more Mattermost instances, anything else added via # it (Asterisk, one or more Mattermost instances, anything else added via
+25 -5
View File
@@ -1,10 +1,30 @@
#!/bin/bash #!/bin/bash
# services/coturn.sh — Shared TURN/STUN relay (coturn) for WebRTC-capable services. # attic/coturn.sh — RETIRED. Formerly services/coturn.sh.
# Part of the modular post-install system (sourced by setup.sh).
# #
# Can also be run standalone on any machine: # The shared-coturn model this file implements is no longer offered by this
# sudo bash coturn.sh # repo at all: services/asterisk.sh and services/mattermost.sh each now run
# (Docker must already be installed when run standalone) # their own dedicated coturn unconditionally, with lib/common.sh's
# find_free_coturn_range() making that safe (it scans every coturn-owning
# service's own .env on the box for already-claimed relay ranges and picks
# a block that can't collide with any of them, dedicated or shared). Sharing
# one instance only ever saved ~40MB RAM per additional consumer beyond the
# first — real, but small — and it was a single point of failure every
# consumer depended on. Parked here, not deleted, since the code is still
# correct and someone could resurrect it if a future need for it shows up;
# living in attic/ (outside services/*.sh's glob) means it never
# self-registers, never appears in the menu, and `sudo ./setup.sh coturn`
# now correctly fails with "unknown service" instead of silently offering
# a coturn shape nothing else in this repo will register a user against.
#
# ── Everything below this point is the file exactly as it ran before
# retirement, kept for reference/rollback, not actively maintained. ────────
#
# Can still be run standalone on any machine, same as before:
# sudo bash attic/coturn.sh
# (Docker must already be installed when run standalone) — but nothing in
# this repo will call ensure_coturn_user() to register with it anymore, so
# doing this only makes sense if you're deliberately reintroducing the
# shared-coturn pattern yourself.
# #
# One coturn instance, shared by every service that needs TURN (Asterisk, # One coturn instance, shared by every service that needs TURN (Asterisk,
# Mattermost, and anything added later) instead of each service running its # Mattermost, and anything added later) instead of each service running its
+11 -5
View File
@@ -31,10 +31,16 @@ Rules of thumb:
`ensure_swapfile()` unconditionally, which offers a 2GB swapfile any time `ensure_swapfile()` unconditionally, which offers a 2GB swapfile any time
RAM is ≤4096MB and none exists yet (`services/asterisk.sh` also calls it RAM is ≤4096MB and none exists yet (`services/asterisk.sh` also calls it
directly for the standalone-run case, so it's covered either way). directly for the standalone-run case, so it's covered either way).
- Sharing one `coturn` instance (`services/coturn.sh`) instead of letting - **Historical note, no longer applicable:** this doc's Tier 2/3 plans below
each WebRTC-capable service (Asterisk, Mattermost) embed its own saves a were sized around one shared `coturn` instance instead of each WebRTC-
container per consumer and — more importantly — avoids relay-port capable service (Asterisk, Mattermost) embedding its own — it saved a
collisions between them. container per consumer and avoided relay-port collisions between them.
That shared-coturn service has since been retired from this repo (see
`attic/coturn.sh`); every service now runs its own dedicated coturn, and
`lib/common.sh`'s `find_free_coturn_range()` avoids the same relay-port
collisions by scanning each coturn-owning service's `.env` instead. Budget
a coturn container per WebRTC-capable service/instance, not one shared
~40MB line, when re-planning a box from scratch.
## Tier 1 — ~1 vCPU / 1GB RAM / 25GB SSD ## Tier 1 — ~1 vCPU / 1GB RAM / 25GB SSD
@@ -43,7 +49,7 @@ Example: DigitalOcean Basic, $6/mo.
This is tight enough that Docker's own daemon overhead is already a This is tight enough that Docker's own daemon overhead is already a
meaningful fraction of the box. **Pick one purpose, not a stack:** meaningful fraction of the box. **Pick one purpose, not a stack:**
- **Option A — Asterisk only.** Asterisk + the shared coturn service fits - **Option A — Asterisk only.** Asterisk + its own dedicated coturn fits
comfortably per this repo's own droplet-sizing notes (`services/asterisk.sh` comfortably per this repo's own droplet-sizing notes (`services/asterisk.sh`
README section) — a swapfile is added automatically (RAM ≤4GB, see above), README section) — a swapfile is added automatically (RAM ≤4GB, see above),
and this plan is "fine for a couple of extensions and light personal use." and this plan is "fine for a couple of extensions and light personal use."
-116
View File
@@ -1038,119 +1038,3 @@ CADDY_BLOCK
echo "" echo ""
} }
# ── Shared coturn (TURN/STUN) wiring ──────────────────────────────────────────
# Usage: ensure_coturn_user "<consumer-name>"
#
# Installs the shared coturn service (services/coturn.sh) if this is the
# first service on the box that needs TURN, then registers (or reuses) a
# dedicated long-term-credential user for the caller — one coturn instance,
# one relay port range, shared by every consumer instead of each service
# running its own and fighting over host ports (see services/coturn.sh's
# header for why that used to be a real, confirmed-live problem).
#
# Out-params (not `local` — read them after the call returns), same
# convention as configure_caddy_for_service's CADDY_SERVICE_* above:
# COTURN_HOST host/IP TURN clients should connect to
# COTURN_PORT coturn's listening port
# COTURN_USERNAME this consumer's long-term-credential username
# COTURN_PASSWORD this consumer's long-term-credential password
# COTURN_HOST is left empty if coturn couldn't be installed or reached —
# callers should treat that as "no TURN available" and degrade gracefully,
# same as checking CADDY_SERVICE_CONFIGURED after configure_caddy_for_service.
#
# Credentials are cached per-consumer in coturn's own users/<name>.env so a
# service re-running its own installer reuses the same one instead of
# minting a new credential and orphaning the old one (which would silently
# break already-configured clients still holding it).
ensure_coturn_user() {
local _consumer="$1"
COTURN_HOST="" COTURN_PORT="" COTURN_USERNAME="" COTURN_PASSWORD=""
if [ ! -d "$DOCKER_DIR/coturn" ]; then
if declare -F install_coturn >/dev/null 2>&1; then
log_info "No shared coturn (TURN/STUN) server yet — setting one up for $_consumer..."
# install_coturn cd's into $DOCKER_DIR/coturn and never cd's back —
# the caller (e.g. asterisk.sh, already cd'd into its own install
# directory) would otherwise return here with the wrong cwd and go
# on to write ITS docker-compose.yml/.env into coturn's directory
# instead of its own. Confirmed live: this clobbered coturn's
# compose file and left the consumer's own directory without one,
# so its later `docker compose up --build` failed with "Dockerfile:
# no such file or directory" (no Dockerfile in coturn's directory).
local _caller_pwd
_caller_pwd="$(pwd)"
install_coturn
local _coturn_rc=$?
cd "$_caller_pwd" || true
[ "$_coturn_rc" -ne 0 ] && { log_warning "coturn setup failed — $_consumer will run without TURN."; return 1; }
else
log_warning "services/coturn.sh not loaded — $_consumer will run without TURN."
log_warning "Run: sudo ./setup.sh coturn"
return 1
fi
fi
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would register coturn user '$_consumer'"
return 0
fi
local _env="$DOCKER_DIR/coturn/.env"
[ -f "$_env" ] || { log_warning "coturn installed but $_env missing — cannot register '$_consumer'."; return 1; }
local _realm _host _port
_realm="$(grep '^COTURN_REALM=' "$_env" | cut -d= -f2-)"
_host="$(grep '^COTURN_HOST=' "$_env" | cut -d= -f2-)"
_port="$(grep '^COTURN_PORT=' "$_env" | cut -d= -f2-)"; _port="${_port:-3478}"
local _userdir="$DOCKER_DIR/coturn/users"
local _userfile="$_userdir/${_consumer}.env"
mkdir -p "$_userdir"
if [ -f "$_userfile" ]; then
local _u _p
_u="$(grep '^COTURN_USER=' "$_userfile" | cut -d= -f2-)"
_p="$(grep '^COTURN_PASS=' "$_userfile" | cut -d= -f2-)"
COTURN_USERNAME="$_u" COTURN_PASSWORD="$_p"
# The cache file surviving doesn't mean the username still exists in
# coturn's own live database — confirmed live: a coturn
# container/volume recreated without preserving ./db wipes the
# database while this file (a separate directory) survives
# untouched, silently orphaning every consumer's credentials until
# something re-registers them. Without this check, re-running the
# consumer's installer (fresh or update) never re-registers anything
# since it only ever hits the else branch below on a MISSING cache
# file — a stale-but-present one looked identical to a healthy one.
# A real "user[realm]" line never contains a space; turnadmin -l's
# own startup log lines do (confirmed live, at least one coturn
# build writes them to stdout, not stderr), so filtering on that
# keeps this robust across builds without needing to match a
# specific log format.
local _db_users
_db_users="$(docker exec coturn turnadmin -l -b /var/lib/coturn/turndb 2>/dev/null | grep -v ' ' | sed -E 's/\[.*//' | awk 'NF')"
if ! grep -qx "$_u" <<< "$_db_users"; then
log_warning "coturn user '$_u' ($_consumer) has cached credentials but isn't in coturn's live database — re-registering with the same password."
if docker exec coturn turnadmin -a -u "$_u" -p "$_p" -r "$_realm" -b /var/lib/coturn/turndb >/dev/null 2>&1; then
log_success "Re-registered coturn user '$_u' for $_consumer"
else
log_warning "Could not re-register coturn user '$_u' for $_consumer — is the coturn container running?"
fi
fi
else
COTURN_USERNAME="$_consumer"
COTURN_PASSWORD="$(generate_password 24)"
if docker exec coturn turnadmin -a -u "$COTURN_USERNAME" -p "$COTURN_PASSWORD" \
-r "$_realm" -b /var/lib/coturn/turndb >/dev/null 2>&1; then
{ echo "COTURN_USER=$COTURN_USERNAME"; echo "COTURN_PASS=$COTURN_PASSWORD"; } > "$_userfile"
chmod 600 "$_userfile"
log_success "Registered coturn user '$COTURN_USERNAME' for $_consumer"
else
log_warning "Could not register a coturn user for $_consumer — is the coturn container running?"
COTURN_USERNAME="" COTURN_PASSWORD=""
return 1
fi
fi
COTURN_HOST="$_host"
COTURN_PORT="$_port"
}
+51 -111
View File
@@ -280,7 +280,7 @@ CBLOCK
fi fi
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
register_service asterisk homelab "Easy Asterisk PBX (intercom/VoIP; auto-tunes for a DigitalOcean droplet); TURN via the shared coturn service" 5061 register_service asterisk homelab "Easy Asterisk PBX (intercom/VoIP; auto-tunes for a DigitalOcean droplet); own dedicated coturn for TURN" 5061
# ── Install layout: directory + container names ──────────────────────────── # ── Install layout: directory + container names ────────────────────────────
# Sets ASTERISK_DIR / ASTERISK_CONTAINER / ASTERISK_COTURN / ASTERISK_PROJECT. # Sets ASTERISK_DIR / ASTERISK_CONTAINER / ASTERISK_COTURN / ASTERISK_PROJECT.
@@ -1024,14 +1024,14 @@ _asterisk_offer_dashboard_and_trunk() {
# and container names are therefore substituted afterwards, same placeholder # and container names are therefore substituted afterwards, same placeholder
# trick the Caddy volume line already uses below. # trick the Caddy volume line already uses below.
# #
# USE_EMBEDDED_COTURN controls whether this install runs its own dedicated # Every install now runs its own dedicated coturn — there's no shared coturn
# coturn container (legacy shape) or relies on the shared coturn service # service left in this repo to opt into (see attic/coturn.sh for why it was
# (services/coturn.sh) instead. This is NOT a free choice at every call site # retired). USE_EMBEDDED_COTURN still exists as a parameter purely for
# — an install that already has its own embedded coturn must keep getting # backward compatibility with pre-retirement installs that were pointed at
# one on every "update" regeneration of this file, or the next `docker # the old shared coturn service instead: an "update" on one of those must
# compose up` silently drops the container its own .env TURN_PASSWORD still # keep NOT writing a coturn: block (there's no .env TURN_PASSWORD for it to
# points at, breaking every already-configured phone with no warning. See # use), so it stays exactly as it was rather than silently gaining or losing
# the two call sites below for how each decides. # a container. See the two call sites below for how each decides.
_asterisk_write_compose() { _asterisk_write_compose() {
local PROJECT="$1" CONTAINER="$2" COTURN_CONTAINER="$3" USE_EMBEDDED_COTURN="${4:-true}" local PROJECT="$1" CONTAINER="$2" COTURN_CONTAINER="$3" USE_EMBEDDED_COTURN="${4:-true}"
local COTURN_MIN_PORT_VAL="${5:-49152}" COTURN_MAX_PORT_VAL="${6:-49252}" local COTURN_MIN_PORT_VAL="${5:-49152}" COTURN_MAX_PORT_VAL="${6:-49252}"
@@ -1106,9 +1106,9 @@ EOF
# #
# This is entirely about Asterisk's OWN SIP transport-tls cert (port # This is entirely about Asterisk's OWN SIP transport-tls cert (port
# 5061) -- it has nothing to do with coturn's separate, unrelated TURNS # 5061) -- it has nothing to do with coturn's separate, unrelated TURNS
# (TLS-wrapped TURN) capability, which the shared coturn service indeed # (TLS-wrapped TURN) capability, which the (since-retired) shared coturn
# doesn't support (see services/coturn.sh's README). A previous version # service indeed didn't support (see attic/coturn.sh's README). A
# of this check gated the mount on USE_EMBEDDED_COTURN == true, conflating # previous version of this check gated the mount on USE_EMBEDDED_COTURN == true, conflating
# the two. Confirmed live: on a shared-coturn install with a real Caddy # the two. Confirmed live: on a shared-coturn install with a real Caddy
# cert already sitting on disk for DOMAIN_NAME, Asterisk silently kept # cert already sitting on disk for DOMAIN_NAME, Asterisk silently kept
# generating (and re-generating) a self-signed cert forever, because # generating (and re-generating) a self-signed cert forever, because
@@ -1380,8 +1380,8 @@ _asterisk_configure_do_cloud_firewall() {
# silently dropped everything else before it ever reached the box. UFW being # silently dropped everything else before it ever reached the box. UFW being
# wide open proves nothing about a layer in front of it that UFW can't see. # wide open proves nothing about a layer in front of it that UFW can't see.
_asterisk_remind_non_do_firewall() { _asterisk_remind_non_do_firewall() {
local WEB_ADMIN_PORT_VAL="$1" WEB_ADMIN_PUBLIC_ACCESS_NEEDED="$2" USE_EMBEDDED_COTURN_VAL="${3:-true}" local WEB_ADMIN_PORT_VAL="$1" WEB_ADMIN_PUBLIC_ACCESS_NEEDED="$2"
local COTURN_MIN_PORT_VAL="${4:-49152}" COTURN_MAX_PORT_VAL="${5:-49252}" local COTURN_MIN_PORT_VAL="${3:-49152}" COTURN_MAX_PORT_VAL="${4:-49252}"
echo "" echo ""
log_warning "This box is reachable via FQDN but wasn't set up as a DigitalOcean droplet," log_warning "This box is reachable via FQDN but wasn't set up as a DigitalOcean droplet,"
log_warning "so no automatic network-edge firewall was configured (that step only exists" log_warning "so no automatic network-edge firewall was configured (that step only exists"
@@ -1397,15 +1397,8 @@ _asterisk_remind_non_do_firewall() {
[[ "$WEB_ADMIN_PUBLIC_ACCESS_NEEDED" == true ]] && echo " TCP ${WEB_ADMIN_PORT_VAL} (web admin)" [[ "$WEB_ADMIN_PUBLIC_ACCESS_NEEDED" == true ]] && echo " TCP ${WEB_ADMIN_PORT_VAL} (web admin)"
echo " TCP 8088, 8089 (Asterisk HTTP/HTTPS)" echo " TCP 8088, 8089 (Asterisk HTTP/HTTPS)"
echo " UDP 10000-20000 (RTP media)" echo " UDP 10000-20000 (RTP media)"
if [[ "$USE_EMBEDDED_COTURN_VAL" == true ]]; then
echo " UDP/TCP 3478 (TURN/STUN)" echo " UDP/TCP 3478 (TURN/STUN)"
echo " UDP ${COTURN_MIN_PORT_VAL}-${COTURN_MAX_PORT_VAL} (TURN relay)" echo " UDP ${COTURN_MIN_PORT_VAL}-${COTURN_MAX_PORT_VAL} (TURN relay)"
else
echo " UDP/TCP 3478 too, if the shared coturn instance (services/coturn.sh) lives"
echo " on this same box — its exact TURN relay range is in its own README"
echo " (~/docker/coturn/README.md), not repeated here since it's independently"
echo " configurable and this install doesn't own it."
fi
} }
# ── Shared: README ───────────────────────────────────────────────────────── # ── Shared: README ─────────────────────────────────────────────────────────
@@ -1461,9 +1454,7 @@ connecting a phone. The Security Dashboard's Extensions tab
| TURN username | ${TURN_USERNAME_VAL} | | TURN username | ${TURN_USERNAME_VAL} |
| TURN password | see \`.env\` → \`TURN_PASSWORD\` | | TURN password | see \`.env\` → \`TURN_PASSWORD\` |
$( [[ "$USE_EMBEDDED_COTURN" == true ]] \ This install runs its own dedicated coturn container (the \`coturn:\` service in docker-compose.yml).
&& echo "This install runs its own dedicated coturn container (the \`coturn:\` service in docker-compose.yml)." \
|| echo "TURN is served by the box's shared coturn service, not a container in this compose file — see \`~/docker/coturn/README.md\`. Every service on the box that needs TURN (Mattermost Calls, etc.) shares this same relay, each with its own dedicated username." )
Recommended softphones: Linphone, Zoiper, Bria, Grandstream Wave, and Recommended softphones: Linphone, Zoiper, Bria, Grandstream Wave, and
[Sipnetic](https://www.sipnetic.com/) on Android (free, TLS/SRTP + [Sipnetic](https://www.sipnetic.com/) on Android (free, TLS/SRTP +
@@ -1682,11 +1673,10 @@ install_asterisk() {
echo "[DRY-RUN] - offer local OR remote Authelia to protect the web admin" echo "[DRY-RUN] - offer local OR remote Authelia to protect the web admin"
echo "[DRY-RUN] - offer to create a DigitalOcean Cloud Firewall via doctl" echo "[DRY-RUN] - offer to create a DigitalOcean Cloud Firewall via doctl"
echo "[DRY-RUN] Would scan for a free web admin port starting at 8081 (avoids e.g. CrowdSec's 8080)" echo "[DRY-RUN] Would scan for a free web admin port starting at 8081 (avoids e.g. CrowdSec's 8080)"
echo "[DRY-RUN] Would register a TURN user with the shared coturn service (chain-installing it" echo "[DRY-RUN] Would run its own dedicated coturn container for TURN, with a relay port"
echo "[DRY-RUN] if this is the first service on the box that needs one), falling back to" echo "[DRY-RUN] range picked to avoid colliding with any other coturn already on the box"
echo "[DRY-RUN] Asterisk's own dedicated coturn if the shared service is unavailable"
echo "[DRY-RUN] Would open UFW ports: 5060, 5061, <web admin port>, 8088, 8089, 10000-20000," echo "[DRY-RUN] Would open UFW ports: 5060, 5061, <web admin port>, 8088, 8089, 10000-20000,"
echo "[DRY-RUN] plus 3478 + 49152-49252 only if falling back to a dedicated coturn" echo "[DRY-RUN] plus 3478 + the dedicated coturn's relay port range"
echo "[DRY-RUN] Would offer 'update in place' instead of a fresh install if $EA_DIR already exists" echo "[DRY-RUN] Would offer 'update in place' instead of a fresh install if $EA_DIR already exists"
echo "[DRY-RUN] Would patch vendor device-creation code + extensions.conf generator to route" echo "[DRY-RUN] Would patch vendor device-creation code + extensions.conf generator to route"
echo "[DRY-RUN] internal SIP MESSAGE through a dedicated [sip-messaging] dialplan context," echo "[DRY-RUN] internal SIP MESSAGE through a dedicated [sip-messaging] dialplan context,"
@@ -1754,19 +1744,16 @@ install_asterisk() {
log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs" log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs"
fi fi
# Self-heal a stale/orphaned shared-coturn registration on # A pre-existing install with no embedded coturn block predates
# every update, not just a full reinstall — the check inside # this repo's dedicated-coturn-only model — it's still pointed
# ensure_coturn_user() is what actually re-registers a # at a shared coturn container this repo no longer installs or
# missing user, this just needs to reach it. Gated on NOT # manages (attic/coturn.sh). Leave it running as-is; update
# having an embedded coturn: an install with its own # never touches .env or firewall rules anyway. Point at a
# dedicated coturn deliberately never touches the shared one # fresh reinstall as the migration path instead of silently
# on update (see the warning above and CLAUDE.md's coturn # trying to heal a registration against a service that no
# migration guidance) — calling this unconditionally would # longer exists here.
# silently chain-install services/coturn.sh for a box that
# was never using it, the exact "don't migrate silently on
# update" mistake that guidance warns against.
if [[ "$_HAD_EMBEDDED_COTURN" != true ]]; then if [[ "$_HAD_EMBEDDED_COTURN" != true ]]; then
ensure_coturn_user "asterisk" log_info "This install still points at a shared coturn service, which this repo no longer installs or manages. It will keep working as long as that coturn container keeps running. Run a full reinstall (not update) to migrate to a dedicated coturn."
fi fi
_asterisk_run_presence_step "$EA_DIR" "$CONTAINER" _asterisk_run_presence_step "$EA_DIR" "$CONTAINER"
@@ -1794,8 +1781,9 @@ install_asterisk() {
echo "" echo ""
log_warning "Full reinstall stops the existing containers and re-runs every" log_warning "Full reinstall stops the existing containers and re-runs every"
log_warning "prompt below from scratch (domain, networking, firewall, Caddy/" log_warning "prompt below from scratch (domain, networking, firewall, Caddy/"
log_warning "Authelia). The TURN credential registered with the shared coturn" log_warning "Authelia), including generating a fresh dedicated coturn container"
log_warning "service is reused as-is — no need to touch coturn for this." log_warning "with new TURN credentials — any already-configured phone's TURN"
log_warning "settings will need to be updated afterward (re-scan its QR code)."
local _WIPE_PBX_DATA="" local _WIPE_PBX_DATA=""
prompt_yn " Also delete stored PBX data (extensions, voicemail, recordings, spool)? (y/n):" "n" _WIPE_PBX_DATA prompt_yn " Also delete stored PBX data (extensions, voicemail, recordings, spool)? (y/n):" "n" _WIPE_PBX_DATA
@@ -1899,48 +1887,15 @@ install_asterisk() {
fi fi
# ── Secrets / TURN ─────────────────────────────────────────────────────── # ── Secrets / TURN ───────────────────────────────────────────────────────
# Prefer the shared coturn service (services/coturn.sh) — one TURN server # Asterisk always runs its own dedicated coturn — there is no shared
# for every service on the box instead of Asterisk running its own and # coturn service in this repo anymore (see attic/coturn.sh for why it
# fighting other consumers (Mattermost, etc.) over relay ports. Falls # was retired). find_free_coturn_range (below) is what makes running a
# back to Asterisk's own dedicated coturn if the shared service isn't # dedicated coturn per service safe: it checks every coturn-owning
# available (e.g. this file run standalone with no sibling services/*.sh # service's .env on the box and picks a relay range that can't collide
# sourced) or registration fails for any reason — Asterisk should never # with any of them.
# end up with no TURN at all just because the shared path had a problem.
local USE_EMBEDDED_COTURN=true local USE_EMBEDDED_COTURN=true
local TURN_USERNAME TURN_PASSWORD TURN_PORT_VAL TURN_SERVER_VAL local TURN_USERNAME TURN_PASSWORD TURN_PORT_VAL TURN_SERVER_VAL
local FORCE_EMBEDDED_COTURN=""
# Only reachable here via an explicit "fresh" choice above — "update"
# is handled separately and always preserves whatever coturn shape
# already exists, never silently switches it.
if [[ -f "$EA_DIR/docker-compose.yml" ]] && grep -q '^ coturn:' "$EA_DIR/docker-compose.yml" 2>/dev/null; then
echo ""
log_warning "This box's existing Asterisk install has its own dedicated coturn."
log_warning "Continuing may switch it to the new shared coturn service — any"
log_warning "phone/softphone configured with the OLD TURN username/password will"
log_warning "need updating once this completes."
fi
# Opt-out of the shared coturn preference below, for the rare case where
# you specifically want Asterisk isolated on its own TURN relay again
# (e.g. reproducing an older install's exact shape to rule out anything
# coturn-sharing-specific during troubleshooting). Only offered when a
# shared instance actually exists — no meaningful choice otherwise.
if [[ -d "$DOCKER_DIR/coturn" ]]; then
local _USE_SHARED_COTURN=""
prompt_yn "Use the shared coturn service for TURN? (n = run Asterisk's own dedicated coturn instead) (y/n):" "y" _USE_SHARED_COTURN
[[ "$_USE_SHARED_COTURN" =~ ^[Nn]$ ]] && FORCE_EMBEDDED_COTURN=true
fi
[[ "$FORCE_EMBEDDED_COTURN" != true ]] && ensure_coturn_user "asterisk"
if [[ "$FORCE_EMBEDDED_COTURN" != true && -n "${COTURN_HOST:-}" ]]; then
USE_EMBEDDED_COTURN=false
TURN_USERNAME="$COTURN_USERNAME"
TURN_PASSWORD="$COTURN_PASSWORD"
TURN_PORT_VAL="$COTURN_PORT"
TURN_SERVER_VAL="${COTURN_HOST}:${COTURN_PORT}"
log_success "Using the shared coturn service — TURN username '$COTURN_USERNAME'."
else
TURN_USERNAME="easyasterisk" TURN_USERNAME="easyasterisk"
TURN_PASSWORD="$(generate_password 24)" TURN_PASSWORD="$(generate_password 24)"
TURN_PORT_VAL="3478" TURN_PORT_VAL="3478"
@@ -1953,29 +1908,20 @@ install_asterisk() {
elif [[ -n "$DOMAIN_NAME" ]]; then elif [[ -n "$DOMAIN_NAME" ]]; then
TURN_SERVER_VAL="${DOMAIN_NAME}:3478" TURN_SERVER_VAL="${DOMAIN_NAME}:3478"
fi fi
if [[ "$FORCE_EMBEDDED_COTURN" == true ]]; then
log_info "Running Asterisk's own dedicated coturn, as requested."
else
log_info "Shared coturn unavailable — Asterisk will run its own dedicated coturn."
fi
fi
# A dedicated embedded coturn running ALONGSIDE any other coturn on the # A dedicated coturn here running alongside Asterisk's own on a prior
# same box (the shared instance, Asterisk's own on a prior install, # install, or any Mattermost instance's own, is exactly the pre-merge
# any Mattermost instance's own) is exactly the pre-merge collision bug # collision bug this repo's coturn history warns about if two of them
# this repo's coturn history warns about if two of them claim overlapping # claim overlapping relay ports — confirmed live, two independent
# relay ports — confirmed live, two independent coturns' default ranges # coturns' default ranges used to overlap by ~100 UDP ports.
# used to overlap by ~100 UDP ports. find_free_coturn_range (lib/common.sh) # find_free_coturn_range (lib/common.sh) checks every coturn-owning
# checks every coturn-owning service's .env on the box, not just the # service's .env on the box and picks a range starting safely past
# shared instance's, and picks a range starting safely past whatever's # whatever's already claimed. No other coturn on the box at all leaves
# already claimed. No other coturn on the box at all leaves it at the # it at the historical 49152-49252 default — nothing to collide with yet.
# historical 49152-49252 default — nothing to collide with yet.
local EMBEDDED_COTURN_MIN_PORT=49152 EMBEDDED_COTURN_MAX_PORT=49252 local EMBEDDED_COTURN_MIN_PORT=49152 EMBEDDED_COTURN_MAX_PORT=49252
if [[ "$USE_EMBEDDED_COTURN" == true ]]; then
find_free_coturn_range EMBEDDED_COTURN_MIN_PORT EMBEDDED_COTURN_MAX_PORT 100 49152 find_free_coturn_range EMBEDDED_COTURN_MIN_PORT EMBEDDED_COTURN_MAX_PORT 100 49152
[[ "$EMBEDDED_COTURN_MIN_PORT" != 49152 ]] && \ [[ "$EMBEDDED_COTURN_MIN_PORT" != 49152 ]] && \
log_info "Dedicated coturn relay range shifted to ${EMBEDDED_COTURN_MIN_PORT}-${EMBEDDED_COTURN_MAX_PORT} to stay clear of another coturn already on this box." log_info "Dedicated coturn relay range shifted to ${EMBEDDED_COTURN_MIN_PORT}-${EMBEDDED_COTURN_MAX_PORT} to stay clear of another coturn already on this box."
fi
_asterisk_write_compose "$ASTERISK_PROJECT" "$CONTAINER" "$ASTERISK_COTURN" "$USE_EMBEDDED_COTURN" \ _asterisk_write_compose "$ASTERISK_PROJECT" "$CONTAINER" "$ASTERISK_COTURN" "$USE_EMBEDDED_COTURN" \
"$EMBEDDED_COTURN_MIN_PORT" "$EMBEDDED_COTURN_MAX_PORT" "$EMBEDDED_COTURN_MIN_PORT" "$EMBEDDED_COTURN_MAX_PORT"
@@ -2017,18 +1963,16 @@ install_asterisk() {
DOMAIN_NAME=${DOMAIN_NAME} DOMAIN_NAME=${DOMAIN_NAME}
# ── TURN/STUN ───────────────────────────────────────────────── # ── TURN/STUN ─────────────────────────────────────────────────
# $( [[ "$USE_EMBEDDED_COTURN" == true ]] && echo "This install runs its own dedicated coturn (see the coturn: service in docker-compose.yml)." || echo "Using the shared coturn service — see ~/docker/coturn/README.md." ) # This install runs its own dedicated coturn (see the coturn: service in docker-compose.yml).
TURN_USERNAME=${TURN_USERNAME} TURN_USERNAME=${TURN_USERNAME}
TURN_PASSWORD=${TURN_PASSWORD} TURN_PASSWORD=${TURN_PASSWORD}
TURN_PORT=${TURN_PORT_VAL} TURN_PORT=${TURN_PORT_VAL}
# Empty when there's no publicly resolvable address (LAN-only, no FQDN). # Empty when there's no publicly resolvable address (LAN-only, no FQDN).
TURN_SERVER=${TURN_SERVER_VAL} TURN_SERVER=${TURN_SERVER_VAL}
# This install's OWN coturn relay range -- only set when USE_EMBEDDED_COTURN # This install's own coturn relay range — other services' find_free_coturn_range
# is true above. Left blank when using the shared coturn service, so other # (lib/common.sh) scans this file to avoid claiming an overlapping range.
# services' find_free_coturn_range (lib/common.sh) scan correctly skips this TURN_MIN_PORT=${EMBEDDED_COTURN_MIN_PORT}
# file instead of treating a range this install doesn't actually own as claimed. TURN_MAX_PORT=${EMBEDDED_COTURN_MAX_PORT}
TURN_MIN_PORT=$( [[ "$USE_EMBEDDED_COTURN" == true ]] && echo "$EMBEDDED_COTURN_MIN_PORT" )
TURN_MAX_PORT=$( [[ "$USE_EMBEDDED_COTURN" == true ]] && echo "$EMBEDDED_COTURN_MAX_PORT" )
# ── RTP port range ──────────────────────────────────────────── # ── RTP port range ────────────────────────────────────────────
RTP_START=10000 RTP_START=10000
@@ -2092,13 +2036,9 @@ ENV
ufw allow 8088/tcp ufw allow 8088/tcp
ufw allow 8089/tcp ufw allow 8089/tcp
ufw allow 10000:20000/udp ufw allow 10000:20000/udp
if [[ "$USE_EMBEDDED_COTURN" == true ]]; then
ufw allow 3478/udp ufw allow 3478/udp
ufw allow 3478/tcp ufw allow 3478/tcp
ufw allow "${EMBEDDED_COTURN_MIN_PORT}:${EMBEDDED_COTURN_MAX_PORT}/udp" ufw allow "${EMBEDDED_COTURN_MIN_PORT}:${EMBEDDED_COTURN_MAX_PORT}/udp"
fi
# Shared coturn opens its own ports once, at its own install time
# (services/coturn.sh) — nothing to open here when using it.
ensure_ufw_enabled ensure_ufw_enabled
log_success "UFW rules added." log_success "UFW rules added."
fi fi
@@ -2108,7 +2048,7 @@ ENV
_asterisk_configure_do_cloud_firewall "$DROPLET_ID" "$WEB_ADMIN_PORT_VAL" "$WEB_ADMIN_PUBLIC_ACCESS_NEEDED" \ _asterisk_configure_do_cloud_firewall "$DROPLET_ID" "$WEB_ADMIN_PORT_VAL" "$WEB_ADMIN_PUBLIC_ACCESS_NEEDED" \
"$EMBEDDED_COTURN_MIN_PORT" "$EMBEDDED_COTURN_MAX_PORT" "$EMBEDDED_COTURN_MIN_PORT" "$EMBEDDED_COTURN_MAX_PORT"
elif [[ -n "$DOMAIN_NAME" ]]; then elif [[ -n "$DOMAIN_NAME" ]]; then
_asterisk_remind_non_do_firewall "$WEB_ADMIN_PORT_VAL" "$WEB_ADMIN_PUBLIC_ACCESS_NEEDED" "$USE_EMBEDDED_COTURN" \ _asterisk_remind_non_do_firewall "$WEB_ADMIN_PORT_VAL" "$WEB_ADMIN_PUBLIC_ACCESS_NEEDED" \
"$EMBEDDED_COTURN_MIN_PORT" "$EMBEDDED_COTURN_MAX_PORT" "$EMBEDDED_COTURN_MIN_PORT" "$EMBEDDED_COTURN_MAX_PORT"
fi fi
+27 -48
View File
@@ -238,7 +238,7 @@ CBLOCK
fi fi
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
register_service mattermost utilities "Team messaging with voice/video calls (Mattermost; TURN via the shared coturn service); supports multiple isolated instances" 8065 register_service mattermost utilities "Team messaging with voice/video calls (Mattermost; own dedicated coturn for TURN); supports multiple isolated instances" 8065
install_mattermost() { install_mattermost() {
require_docker || return 1 require_docker || return 1
@@ -251,7 +251,6 @@ install_mattermost() {
local INSTANCE_SUFFIX="" PROJECT="mattermost" local INSTANCE_SUFFIX="" PROJECT="mattermost"
local MM_CONTAINER="mattermost" DB_CONTAINER="mattermost-db" local MM_CONTAINER="mattermost" DB_CONTAINER="mattermost-db"
local WEB_PORT="8065" CALLS_UDP_PORT="8443" local WEB_PORT="8065" CALLS_UDP_PORT="8443"
local COTURN_CONSUMER="mattermost"
if [ -d "$DIR" ]; then if [ -d "$DIR" ]; then
echo "" echo ""
@@ -280,7 +279,6 @@ install_mattermost() {
PROJECT="mattermost-$_suffix" PROJECT="mattermost-$_suffix"
MM_CONTAINER="mattermost-$_suffix" MM_CONTAINER="mattermost-$_suffix"
DB_CONTAINER="mattermost-$_suffix-db" DB_CONTAINER="mattermost-$_suffix-db"
COTURN_CONSUMER="mattermost-$_suffix"
log_info "New instance: $DIR" log_info "New instance: $DIR"
fi fi
fi fi
@@ -305,8 +303,8 @@ install_mattermost() {
echo "[DRY-RUN] Would create $DIR with docker-compose.yml" echo "[DRY-RUN] Would create $DIR with docker-compose.yml"
echo "[DRY-RUN] Would write .env with DB and Mattermost secrets" echo "[DRY-RUN] Would write .env with DB and Mattermost secrets"
echo "[DRY-RUN] Would create data/ logs/ config/ plugins/ db/ subdirectories" echo "[DRY-RUN] Would create data/ logs/ config/ plugins/ db/ subdirectories"
echo "[DRY-RUN] Would register a TURN user with the shared coturn service for '$COTURN_CONSUMER'" echo "[DRY-RUN] Would run this instance's own dedicated coturn container for TURN, with a relay"
echo "[DRY-RUN] (falling back to a dedicated coturn if the shared service is unavailable)" echo "[DRY-RUN] port range picked to avoid colliding with any other coturn already on the box"
echo "[DRY-RUN] Would open UFW ports ${WEB_PORT}/tcp, ${CALLS_UDP_PORT}/udp" echo "[DRY-RUN] Would open UFW ports ${WEB_PORT}/tcp, ${CALLS_UDP_PORT}/udp"
return 0 return 0
fi fi
@@ -323,16 +321,11 @@ install_mattermost() {
return 0 return 0
;; ;;
fresh) fresh)
if [ "$_HAD_EMBEDDED_COTURN" = true ]; then
echo ""
log_warning "This install has its own dedicated coturn. Continuing may switch it to"
log_warning "the shared coturn service — the Calls plugin's TURN config in System"
log_warning "Console will need updating to the new credentials afterward (see below)."
fi
echo "" echo ""
log_warning "Full reinstall stops the existing containers and re-runs every prompt" log_warning "Full reinstall stops the existing containers and re-runs every prompt"
log_warning "below from scratch. The TURN credential registered with the shared" log_warning "below from scratch, including generating a fresh dedicated coturn"
log_warning "coturn service is reused as-is — no need to touch coturn for this." log_warning "container with new TURN credentials — the Calls plugin's TURN config in"
log_warning "System Console will need updating afterward (see below)."
local _WIPE_MM_DATA="" local _WIPE_MM_DATA=""
prompt_yn " Also delete stored data (Postgres database, uploaded files, config, plugins)? (y/n):" "n" _WIPE_MM_DATA prompt_yn " Also delete stored data (Postgres database, uploaded files, config, plugins)? (y/n):" "n" _WIPE_MM_DATA
@@ -408,47 +401,31 @@ networks:
" "
fi fi
# ── TURN: shared coturn preferred, dedicated coturn as fallback ───────── # ── TURN: always this instance's own dedicated coturn ───────────────────
# See services/coturn.sh's header for why one shared TURN server beats # There is no shared coturn service in this repo anymore (see
# every service (Asterisk, each Mattermost instance, ...) running its # attic/coturn.sh for why it was retired) — every instance runs its own.
# own and fighting over host relay ports. # find_free_coturn_range (below) is what makes that safe: it checks
# every coturn-owning service's .env on the box and picks a relay range
# that can't collide with any of them.
local USE_EMBEDDED_COTURN=true local USE_EMBEDDED_COTURN=true
local TURN_HOST_VAL="" TURN_PORT_VAL="" TURN_USERNAME_VAL="" TURN_PASSWORD_VAL="" local TURN_HOST_VAL="" TURN_PORT_VAL="" TURN_USERNAME_VAL="" TURN_PASSWORD_VAL=""
local FORCE_EMBEDDED_COTURN=""
# Opt-out of the shared coturn preference, same as services/asterisk.sh — # A pre-existing instance with no embedded coturn block predates this
# only offered on a genuinely fresh install (never re-asked on update, # repo's dedicated-coturn-only model — it's still pointed at a shared
# matching every other coturn-shape decision in this file) and only when # coturn container this repo no longer installs or manages. Leave it
# a shared instance actually exists to opt out of. # running as-is (update never touches .env anyway) rather than trying
if [ "$MODE" = "fresh" ] && [ -d "$DOCKER_DIR/coturn" ]; then # to heal a registration against a service that no longer exists here.
local _USE_SHARED_COTURN="" if [ "$MODE" = "update" ] && [ "$_HAD_EMBEDDED_COTURN" != true ]; then
prompt_yn "Use the shared coturn service for TURN? (n = run this instance's own dedicated coturn instead) (y/n):" "y" _USE_SHARED_COTURN
[[ "$_USE_SHARED_COTURN" =~ ^[Nn]$ ]] && FORCE_EMBEDDED_COTURN=true
fi
if [ "$MODE" = "update" ] && [ "$_HAD_EMBEDDED_COTURN" = true ]; then
USE_EMBEDDED_COTURN=true # preserve exactly — never switch on update
elif [ "$FORCE_EMBEDDED_COTURN" = true ]; then
USE_EMBEDDED_COTURN=true
log_info "Running this instance's own dedicated coturn, as requested."
else
ensure_coturn_user "$COTURN_CONSUMER"
if [ -n "${COTURN_HOST:-}" ]; then
USE_EMBEDDED_COTURN=false USE_EMBEDDED_COTURN=false
TURN_HOST_VAL="$COTURN_HOST"; TURN_PORT_VAL="$COTURN_PORT" log_info "This instance still points at a shared coturn service, which this repo no longer installs or manages. It will keep working as long as that coturn container keeps running. Run a full reinstall (not update) to migrate to a dedicated coturn."
TURN_USERNAME_VAL="$COTURN_USERNAME"; TURN_PASSWORD_VAL="$COTURN_PASSWORD"
log_success "Using the shared coturn service — TURN username '$COTURN_USERNAME'."
else
log_info "Shared coturn unavailable — this instance will run its own dedicated coturn."
fi
fi fi
[ -n "$MM_SECRET" ] || MM_SECRET=$(generate_password 48) [ -n "$MM_SECRET" ] || MM_SECRET=$(generate_password 48)
# A dedicated coturn here running alongside the shared instance, Asterisk's # A dedicated coturn here running alongside Asterisk's own, a sibling
# own, or a sibling Mattermost instance's own is the same pre-merge relay- # Mattermost instance's own, or a legacy shared instance still running is
# port collision this repo's coturn history warns about (confirmed live: # the same pre-merge relay-port collision this repo's coturn history
# two independent coturns' default ranges used to overlap by ~100 UDP # warns about (confirmed live: two independent coturns' default ranges
# ports). find_free_coturn_range (lib/common.sh) checks every coturn- # used to overlap by ~100 UDP ports). find_free_coturn_range (lib/common.sh) checks every coturn-
# owning service's .env on the box and picks a range starting safely past # owning service's .env on the box and picks a range starting safely past
# whatever's already claimed; the historical 49153-49352 default only # whatever's already claimed; the historical 49153-49352 default only
# survives when nothing else on the box claims a range at all. # survives when nothing else on the box claims a range at all.
@@ -593,7 +570,9 @@ EOF
ufw allow 3479/udp; ufw allow 3479/tcp ufw allow 3479/udp; ufw allow 3479/tcp
ufw allow "${MM_COTURN_MIN_PORT}:${MM_COTURN_MAX_PORT}/udp" comment "Mattermost coturn relay" ufw allow "${MM_COTURN_MIN_PORT}:${MM_COTURN_MAX_PORT}/udp" comment "Mattermost coturn relay"
fi fi
# Shared coturn opens its own ports once, at its own install time. # A legacy instance still on a shared coturn (USE_EMBEDDED_COTURN=false
# above) has nothing to open here — that coturn's ports were opened
# once, at its own install time, whenever that was.
fi fi
echo "" echo ""
+9 -7
View File
@@ -172,8 +172,9 @@ fi
# Anveo-style ICE-enabled endpoints. Asterisk caches its OWN TURN_* values # Anveo-style ICE-enabled endpoints. Asterisk caches its OWN TURN_* values
# in its .env at the point it was configured — testing with those (not # in its .env at the point it was configured — testing with those (not
# re-deriving fresh credentials) proves what Asterisk is actually set up # re-deriving fresh credentials) proves what Asterisk is actually set up
# to use, not just that the shared coturn instance works in general (that # to use. Every current install runs its own dedicated coturn; a box that
# broader, multi-consumer check is tools/coturn-test-check.sh's job). ───────── # 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)" section "coturn (TURN relay for Asterisk)"
ASTERISK_ENV="$EA_DIR/.env" ASTERISK_ENV="$EA_DIR/.env"
@@ -197,11 +198,11 @@ else
if grep -q '^ coturn:' "$EA_DIR/docker-compose.yml" 2>/dev/null; then if grep -q '^ coturn:' "$EA_DIR/docker-compose.yml" 2>/dev/null; then
COTURN_CONTAINER="easy-asterisk-coturn" COTURN_CONTAINER="easy-asterisk-coturn"
[[ "$CONTAINER" == *-do ]] && COTURN_CONTAINER="easy-asterisk-do-coturn" [[ "$CONTAINER" == *-do ]] && COTURN_CONTAINER="easy-asterisk-do-coturn"
ok "Using an embedded, per-Asterisk coturn ($COTURN_CONTAINER) — not the shared" ok "Using an embedded, per-Asterisk coturn ($COTURN_CONTAINER); tested separately below."
ok "instance, so tools/coturn-test-check.sh won't see this one; tested separately below."
else else
COTURN_CONTAINER="coturn" 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 fi
if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$COTURN_CONTAINER"; then 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" warn "turnutils_uclient not found in $COTURN_CONTAINER — skipping live allocation test"
else else
# Plain UDP only — no -t/-T (TCP/TLS) flags. coturn is started with # 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 # encrypted/TCP transport here just fails the allocation outright
# against a server that never offered one, misreporting a config # against a server that never offered one, misreporting a config
# problem that doesn't exist. Confirmed live: this was the actual # 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 # turnutils_uclient also refuses to run at all without either -e
# <peer> or -y ("Either -e peer_address or -y must be specified", # <peer> or -y ("Either -e peer_address or -y must be specified",
# confirmed live). -e needs an actual reachable, non-loopback peer # 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 # --allow-loopback-peers, so -e 127.0.0.1 gets rejected with
# "channel bind: error 403 (Forbidden IP)" (also confirmed live, # "channel bind: error 403 (Forbidden IP)" (also confirmed live,
# against a real local coturn instance built to test this exact # against a real local coturn instance built to test this exact