From 3abc46d8a96f2b0166c9ba17c7811952420280ea Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 00:13:09 +0000 Subject: [PATCH 1/3] asterisk: add opt-out for shared coturn on fresh reinstall ensure_coturn_user always preferred the shared coturn service with no override once it was reachable -- there was no way to deliberately run Asterisk's own dedicated coturn again short of stopping the shared service outright (which would also break every other consumer, e.g. Mattermost Calls). Useful for reproducing an older install's exact shape when troubleshooting anything that might be specific to the coturn-sharing path. Only offered when a shared instance actually exists, and only reachable via an explicit fresh reinstall, matching this repo's existing rule that coturn shape never changes silently on an update. --- services/asterisk.sh | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/services/asterisk.sh b/services/asterisk.sh index f22749c..04c8eaa 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -1885,6 +1885,7 @@ install_asterisk() { # end up with no TURN at all just because the shared path had a problem. local USE_EMBEDDED_COTURN=true 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 @@ -1897,8 +1898,19 @@ install_asterisk() { log_warning "need updating once this completes." fi - ensure_coturn_user "asterisk" - if [[ -n "${COTURN_HOST:-}" ]]; then + # 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" @@ -1918,7 +1930,11 @@ install_asterisk() { elif [[ -n "$DOMAIN_NAME" ]]; then TURN_SERVER_VAL="${DOMAIN_NAME}:3478" fi - log_info "Shared coturn unavailable — Asterisk will run its own dedicated coturn." + 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 _asterisk_write_compose "$ASTERISK_PROJECT" "$CONTAINER" "$ASTERISK_COTURN" "$USE_EMBEDDED_COTURN" From 85fc17237732f3e869bf95fe4cdbbf7d91f01b96 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 00:27:33 +0000 Subject: [PATCH 2/3] asterisk: avoid relay-port collision between dedicated and shared coturn Answers "can Asterisk run its own coturn while Mattermost keeps using the shared one" -- yes, but not safely until now: Asterisk's dedicated coturn hardcoded relay ports 49152-49252, entirely inside the shared instance's own default range (49152-49452). Running both on the same box (now possible via the previous commit's opt-out prompt) recreated the exact pre-merge collision this repo's coturn history warns about. When a shared instance is present, read its actual configured COTURN_MAX_PORT from ~/docker/coturn/.env and pick a dedicated range starting safely past it, so the two can never overlap regardless of what the shared instance was configured with. No shared instance on the box means no collision risk, so the historical 49152-49252 default is left untouched in that case. Threaded the computed range through every place it was previously hardcoded: the coturn container's own --min-port/--max-port, the UFW rule, the DigitalOcean Cloud Firewall rule list, the non-DO firewall reminder, and the generated README's port table. Verified the range-shift arithmetic directly: a shared instance configured up to 49452 shifts the dedicated range to 49502-49602 (clear); no shared instance leaves it at the original default. --- services/asterisk.sh | 56 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/services/asterisk.sh b/services/asterisk.sh index 04c8eaa..232246a 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -1017,6 +1017,7 @@ _asterisk_offer_dashboard_and_trunk() { # the two call sites below for how each decides. _asterisk_write_compose() { 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_DEPENDS=" depends_on: coturn: @@ -1040,8 +1041,8 @@ _asterisk_write_compose() { - --lt-cred-mech - --user=\${TURN_USERNAME:-easyasterisk}:\${TURN_PASSWORD} - --realm=\${DOMAIN_NAME:-localhost} - - --min-port=49152 - - --max-port=49252 + - --min-port=${COTURN_MIN_PORT_VAL} + - --max-port=${COTURN_MAX_PORT_VAL} - --no-tls - --no-dtls - --no-cli @@ -1296,6 +1297,7 @@ CADDY_BLOCK # ── DigitalOcean Cloud Firewall (network edge, in front of the droplet) ──── _asterisk_configure_do_cloud_firewall() { local DROPLET_ID="$1" WEB_ADMIN_PORT_VAL="$2" WEB_ADMIN_PUBLIC="$3" + local COTURN_MIN_PORT_VAL="${4:-49152}" COTURN_MAX_PORT_VAL="${5:-49252}" local DO_FW_RULES=( "protocol:tcp,ports:22,address:0.0.0.0/0,address:::/0" @@ -1311,7 +1313,7 @@ _asterisk_configure_do_cloud_firewall() { "protocol:tcp,ports:3478,address:0.0.0.0/0,address:::/0" "protocol:udp,ports:3478,address:0.0.0.0/0,address:::/0" "protocol:udp,ports:10000-20000,address:0.0.0.0/0,address:::/0" - "protocol:udp,ports:49152-49252,address:0.0.0.0/0,address:::/0" + "protocol:udp,ports:${COTURN_MIN_PORT_VAL}-${COTURN_MAX_PORT_VAL},address:0.0.0.0/0,address:::/0" ) echo "" @@ -1362,6 +1364,7 @@ _asterisk_configure_do_cloud_firewall() { # wide open proves nothing about a layer in front of it that UFW can't see. _asterisk_remind_non_do_firewall() { local WEB_ADMIN_PORT_VAL="$1" WEB_ADMIN_PUBLIC_ACCESS_NEEDED="$2" USE_EMBEDDED_COTURN_VAL="${3:-true}" + local COTURN_MIN_PORT_VAL="${4:-49152}" COTURN_MAX_PORT_VAL="${5:-49252}" echo "" 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" @@ -1379,10 +1382,12 @@ _asterisk_remind_non_do_firewall() { echo " UDP 10000-20000 (RTP media)" if [[ "$USE_EMBEDDED_COTURN_VAL" == true ]]; then echo " UDP/TCP 3478 (TURN/STUN)" - echo " UDP 49152-49252 (TURN relay)" + echo " UDP ${COTURN_MIN_PORT_VAL}-${COTURN_MAX_PORT_VAL} (TURN relay)" else - echo " UDP/TCP 3478 and UDP 49152-49252 too, if the shared coturn instance" - echo " (services/coturn.sh) lives on this same box." + 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 } @@ -1392,6 +1397,7 @@ _asterisk_remind_non_do_firewall() { _asterisk_write_readme() { local EA_DIR="$1" CONTAINER="$2" IS_DO="$3" DOMAIN_NAME="$4" PUBLIC_IP="$5" WEB_ADMIN_PORT_VAL="$6" local USE_EMBEDDED_COTURN="${7:-true}" TURN_USERNAME_VAL="${8:-easyasterisk}" TURN_SERVER_DISPLAY="${9:-}" + local COTURN_MIN_PORT_VAL="${10:-49152}" COTURN_MAX_PORT_VAL="${11:-49252}" local _host="${DOMAIN_NAME:-${PUBLIC_IP:-}}" [ -z "$TURN_SERVER_DISPLAY" ] && TURN_SERVER_DISPLAY="${_host}:3478" @@ -1525,7 +1531,7 @@ docker exec -it ${CONTAINER} easy-asterisk | 8088/8089 | TCP | Asterisk HTTP/WS (ARI/AMI) | | 3478 | UDP/TCP | TURN/STUN (coturn) | | 10000–20000 | UDP | RTP media streams | -| 49152–49252 | UDP | TURN relay media ports | +| ${COTURN_MIN_PORT_VAL}–${COTURN_MAX_PORT_VAL} | UDP | TURN relay media ports (only if this install runs its own dedicated coturn — see below) | ## Data directories (all inside ${EA_DIR}/, included in backup) @@ -1937,7 +1943,30 @@ install_asterisk() { fi fi - _asterisk_write_compose "$ASTERISK_PROJECT" "$CONTAINER" "$ASTERISK_COTURN" "$USE_EMBEDDED_COTURN" + # A dedicated embedded coturn running ALONGSIDE the shared instance on the + # same box (this install's own choice above, or Mattermost/anything else + # still using the shared one) is exactly the pre-merge collision bug this + # repo's coturn history warns about if both claim the same relay ports — + # confirmed live, their default ranges used to overlap by ~100 UDP ports. + # Read the shared instance's actual configured range (not just its + # install-time default, since coturn.sh lets that be customized) and pick + # a range that starts safely past its end, so the two can never collide + # regardless of what the shared instance was configured with. No shared + # instance on this box at all means no collision is possible, so the + # historical default is left alone in that case. + local EMBEDDED_COTURN_MIN_PORT=49152 EMBEDDED_COTURN_MAX_PORT=49252 + if [[ "$USE_EMBEDDED_COTURN" == true && -f "$DOCKER_DIR/coturn/.env" ]]; then + local _shared_coturn_max_port="" + _shared_coturn_max_port="$(grep -E '^COTURN_MAX_PORT=' "$DOCKER_DIR/coturn/.env" 2>/dev/null | cut -d= -f2-)" + if [[ "$_shared_coturn_max_port" =~ ^[0-9]+$ ]]; then + EMBEDDED_COTURN_MIN_PORT=$((_shared_coturn_max_port + 50)) + EMBEDDED_COTURN_MAX_PORT=$((EMBEDDED_COTURN_MIN_PORT + 100)) + log_info "Dedicated coturn relay range shifted to ${EMBEDDED_COTURN_MIN_PORT}-${EMBEDDED_COTURN_MAX_PORT} to stay clear of the shared instance's ${_shared_coturn_max_port}-port ceiling." + fi + fi + + _asterisk_write_compose "$ASTERISK_PROJECT" "$CONTAINER" "$ASTERISK_COTURN" "$USE_EMBEDDED_COTURN" \ + "$EMBEDDED_COTURN_MIN_PORT" "$EMBEDDED_COTURN_MAX_PORT" # ── Pick a free port for the web admin ───────────────────────────────────── # Hardcoding a single number gets fragile fast once several services share @@ -2048,7 +2077,7 @@ ENV if [[ "$USE_EMBEDDED_COTURN" == true ]]; then ufw allow 3478/udp ufw allow 3478/tcp - ufw allow 49152:49252/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. @@ -2058,9 +2087,11 @@ ENV # ── Network-edge firewall (in front of the box, not UFW) ────────────────── if [[ "$IS_DO" == true ]]; then - _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" 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" "$USE_EMBEDDED_COTURN" \ + "$EMBEDDED_COTURN_MIN_PORT" "$EMBEDDED_COTURN_MAX_PORT" fi # ── CrowdSec note ────────────────────────────────────────────────────────── @@ -2086,7 +2117,8 @@ ENV # ── README ──────────────────────────────────────────────────────────────── _asterisk_write_readme "$EA_DIR" "$CONTAINER" "$IS_DO" "$DOMAIN_NAME" "$PUBLIC_IP" "$WEB_ADMIN_PORT_VAL" \ - "$USE_EMBEDDED_COTURN" "$TURN_USERNAME" "$TURN_SERVER_VAL" + "$USE_EMBEDDED_COTURN" "$TURN_USERNAME" "$TURN_SERVER_VAL" \ + "$EMBEDDED_COTURN_MIN_PORT" "$EMBEDDED_COTURN_MAX_PORT" # ── Start ───────────────────────────────────────────────────────────────── echo "" From d835e0d7349878cc9fab993a3e2ee90180ccb852 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 00:55:31 +0000 Subject: [PATCH 3/3] mattermost, asterisk: dynamic, collision-safe dedicated coturn ranges Mattermost's own embedded-coturn fallback hardcoded the same relay range (49153-49352) for every instance, with no per-instance offset -- running two Mattermost instances without the shared coturn service (or one alongside Asterisk's own dedicated coturn, now possible via the prior commit) would silently reproduce the exact pre-merge collision bug this repo's coturn history warns about, just among Mattermost instances instead of Asterisk/Mattermost. Adds find_free_coturn_range() (lib/common.sh, standalone-mode-stubbed in both services matching the existing port_in_use/find_free_port convention): a coturn relay range can't be collision-checked with live socket scanning the way a single port can -- coturn only opens ports inside its configured range on demand, so an idle range looks the same as an unclaimed one to ss/netstat. The only reliable check is reading what every other coturn-owning service's .env on the box actually claims (COTURN_MAX_PORT for the shared instance, TURN_MAX_PORT for each dedicated one) and picking a range starting safely past the highest. Also adds Mattermost's own opt-out prompt for the shared coturn preference, matching the one just added to Asterisk (fresh-install-only, never re-asked on update, same as every other coturn-shape decision in that file). An update now explicitly preserves its existing dedicated range from .env rather than silently recomputing a new one. Verified end-to-end: a shared instance + Asterisk's dedicated coturn + two independent Mattermost instances, each discovering and avoiding every range already claimed by the others, land on entirely non-overlapping port blocks. --- lib/common.sh | 35 +++++++++++++++++++++++ services/asterisk.sh | 56 ++++++++++++++++++++++++------------ services/mattermost.sh | 65 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 134 insertions(+), 22 deletions(-) diff --git a/lib/common.sh b/lib/common.sh index 016aaf5..bd9c4ff 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -805,6 +805,41 @@ find_free_port() { eval "$_varname='$_port'" } +# find_free_coturn_range MIN_VARNAME MAX_VARNAME RANGE_SIZE [START_PORT] +# A coturn relay port range can't be collision-checked with port_in_use / +# find_free_port the way a single fixed port can: coturn only opens ports +# inside min-port..max-port on demand, per active TURN allocation, so an +# idle range shows up as nothing listening either way — a live socket scan +# can't tell two coturn CONFIGS apart. The only reliable check is reading +# what range every other coturn-owning service on the box actually claims, +# from its own .env (COTURN_MAX_PORT for the shared instance in +# ~/docker/coturn/.env, TURN_MAX_PORT for every dedicated per-service coturn +# — Asterisk's own, each Mattermost instance's, etc., each in that service's +# own .env). Every service directory keeps its .env at the same top-level +# path, so one glob covers all of them without needing to know which +# services exist ahead of time. +# +# Writes a RANGE_SIZE-wide block starting safely past the highest claimed +# max-port back into MIN_VARNAME/MAX_VARNAME. No other coturn on the box at +# all (fresh install, nothing else uses TURN) leaves it at START_PORT — no +# collision is possible yet, so there's nothing to shift away from. +find_free_coturn_range() { + local _min_varname="$1" _max_varname="$2" _range_size="${3:-200}" _start="${4:-49152}" + local _highest_max=$((_start - 1)) _f _found + for _f in "$DOCKER_DIR"/*/.env; do + [ -f "$_f" ] || continue + _found="$(grep -E '^(COTURN|TURN)_MAX_PORT=' "$_f" 2>/dev/null | tail -1 | cut -d= -f2-)" + [[ "$_found" =~ ^[0-9]+$ ]] || continue + [ "$_found" -gt "$_highest_max" ] && _highest_max=$_found + done + local _min=$_start + if [ "$_highest_max" -ge "$_start" ]; then + _min=$((_highest_max + 50)) + fi + eval "$_min_varname='$_min'" + eval "$_max_varname='$((_min + _range_size))'" +} + # ── Caddy reverse-proxy wiring (shared by every web service) ───────────────── # Usage: configure_caddy_for_service "Name" "UPSTREAM" "default-subdomain" ["extra"] # UPSTREAM: container:port for caddy_net routing (e.g. "filebrowser:80"), diff --git a/services/asterisk.sh b/services/asterisk.sh index 232246a..69747c4 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -123,6 +123,23 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then log_success "Swapfile enabled (${SWAP_MB}MB, swappiness=10, persists across reboots)." } + # Standalone-mode copy of lib/common.sh's find_free_coturn_range() — + # kept in sync by hand, same as every other helper stubbed in this block. + find_free_coturn_range() { + local _min_varname="$1" _max_varname="$2" _range_size="${3:-200}" _start="${4:-49152}" + local _highest_max=$((_start - 1)) _f _found + for _f in "$DOCKER_DIR"/*/.env; do + [ -f "$_f" ] || continue + _found="$(grep -E '^(COTURN|TURN)_MAX_PORT=' "$_f" 2>/dev/null | tail -1 | cut -d= -f2-)" + [[ "$_found" =~ ^[0-9]+$ ]] || continue + [ "$_found" -gt "$_highest_max" ] && _highest_max=$_found + done + local _min=$_start + [ "$_highest_max" -ge "$_start" ] && _min=$((_highest_max + 50)) + eval "$_min_varname='$_min'" + eval "$_max_varname='$((_min + _range_size))'" + } + configure_caddy_for_service() { local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}" local _caddy_dir="$DOCKER_DIR/caddy" @@ -1943,26 +1960,21 @@ install_asterisk() { fi fi - # A dedicated embedded coturn running ALONGSIDE the shared instance on the - # same box (this install's own choice above, or Mattermost/anything else - # still using the shared one) is exactly the pre-merge collision bug this - # repo's coturn history warns about if both claim the same relay ports — - # confirmed live, their default ranges used to overlap by ~100 UDP ports. - # Read the shared instance's actual configured range (not just its - # install-time default, since coturn.sh lets that be customized) and pick - # a range that starts safely past its end, so the two can never collide - # regardless of what the shared instance was configured with. No shared - # instance on this box at all means no collision is possible, so the - # historical default is left alone in that case. + # A dedicated embedded coturn running ALONGSIDE any other coturn on the + # same box (the shared instance, Asterisk's own on a prior install, + # any Mattermost instance's own) is exactly the pre-merge collision bug + # this repo's coturn history warns about if two of them claim overlapping + # relay ports — confirmed live, two independent coturns' default ranges + # used to overlap by ~100 UDP ports. find_free_coturn_range (lib/common.sh) + # checks every coturn-owning service's .env on the box, not just the + # shared instance's, and picks a range starting safely past whatever's + # already claimed. No other coturn on the box at all leaves it at the + # historical 49152-49252 default — nothing to collide with yet. local EMBEDDED_COTURN_MIN_PORT=49152 EMBEDDED_COTURN_MAX_PORT=49252 - if [[ "$USE_EMBEDDED_COTURN" == true && -f "$DOCKER_DIR/coturn/.env" ]]; then - local _shared_coturn_max_port="" - _shared_coturn_max_port="$(grep -E '^COTURN_MAX_PORT=' "$DOCKER_DIR/coturn/.env" 2>/dev/null | cut -d= -f2-)" - if [[ "$_shared_coturn_max_port" =~ ^[0-9]+$ ]]; then - EMBEDDED_COTURN_MIN_PORT=$((_shared_coturn_max_port + 50)) - EMBEDDED_COTURN_MAX_PORT=$((EMBEDDED_COTURN_MIN_PORT + 100)) - log_info "Dedicated coturn relay range shifted to ${EMBEDDED_COTURN_MIN_PORT}-${EMBEDDED_COTURN_MAX_PORT} to stay clear of the shared instance's ${_shared_coturn_max_port}-port ceiling." - fi + if [[ "$USE_EMBEDDED_COTURN" == true ]]; then + find_free_coturn_range EMBEDDED_COTURN_MIN_PORT EMBEDDED_COTURN_MAX_PORT 100 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." fi _asterisk_write_compose "$ASTERISK_PROJECT" "$CONTAINER" "$ASTERISK_COTURN" "$USE_EMBEDDED_COTURN" \ @@ -2011,6 +2023,12 @@ TURN_PASSWORD=${TURN_PASSWORD} TURN_PORT=${TURN_PORT_VAL} # Empty when there's no publicly resolvable address (LAN-only, no FQDN). TURN_SERVER=${TURN_SERVER_VAL} +# This install's OWN coturn relay range -- only set when USE_EMBEDDED_COTURN +# is true above. Left blank when using the shared coturn service, so other +# services' find_free_coturn_range (lib/common.sh) scan correctly skips this +# file instead of treating a range this install doesn't actually own as claimed. +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_START=10000 diff --git a/services/mattermost.sh b/services/mattermost.sh index 7ccb3a6..c59a896 100644 --- a/services/mattermost.sh +++ b/services/mattermost.sh @@ -59,6 +59,21 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then eval "$_varname='$_port'" } + find_free_coturn_range() { + local _min_varname="$1" _max_varname="$2" _range_size="${3:-200}" _start="${4:-49152}" + local _highest_max=$((_start - 1)) _f _found + for _f in "$DOCKER_DIR"/*/.env; do + [ -f "$_f" ] || continue + _found="$(grep -E '^(COTURN|TURN)_MAX_PORT=' "$_f" 2>/dev/null | tail -1 | cut -d= -f2-)" + [[ "$_found" =~ ^[0-9]+$ ]] || continue + [ "$_found" -gt "$_highest_max" ] && _highest_max=$_found + done + local _min=$_start + [ "$_highest_max" -ge "$_start" ] && _min=$((_highest_max + 50)) + eval "$_min_varname='$_min'" + eval "$_max_varname='$((_min + _range_size))'" + } + # Match common.sh's eval-based pattern so local vars in install_* are set correctly prompt_text() { local _q="$1" _def="$2" _var="$3" _r @@ -399,9 +414,23 @@ networks: # own and fighting over host relay ports. local USE_EMBEDDED_COTURN=true 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 — + # only offered on a genuinely fresh install (never re-asked on update, + # matching every other coturn-shape decision in this file) and only when + # a shared instance actually exists to opt out of. + if [ "$MODE" = "fresh" ] && [ -d "$DOCKER_DIR/coturn" ]; then + local _USE_SHARED_COTURN="" + 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 @@ -415,6 +444,30 @@ networks: fi [ -n "$MM_SECRET" ] || MM_SECRET=$(generate_password 48) + # A dedicated coturn here running alongside the shared instance, Asterisk's + # own, or a sibling Mattermost instance's own is the same pre-merge relay- + # port collision this repo's coturn history warns about (confirmed live: + # two independent coturns' default ranges 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 + # whatever's already claimed; the historical 49153-49352 default only + # survives when nothing else on the box claims a range at all. + local MM_COTURN_MIN_PORT=49153 MM_COTURN_MAX_PORT=49352 + if [ "$USE_EMBEDDED_COTURN" = true ] && [ "$MODE" != "update" ]; then + find_free_coturn_range MM_COTURN_MIN_PORT MM_COTURN_MAX_PORT 200 49153 + [[ "$MM_COTURN_MIN_PORT" != 49153 ]] && \ + log_info "Dedicated coturn relay range shifted to ${MM_COTURN_MIN_PORT}-${MM_COTURN_MAX_PORT} to stay clear of another coturn already on this box." + elif [ "$MODE" = "update" ] && [ -f "$DIR/.env" ]; then + # Preserve whatever range this instance was already using — an update + # must never silently move it (a live coturn container restarting on + # a different port range would break in-flight/repeat Calls sessions). + local _existing_min _existing_max + _existing_min="$(grep -E '^TURN_MIN_PORT=' "$DIR/.env" 2>/dev/null | cut -d= -f2-)" + _existing_max="$(grep -E '^TURN_MAX_PORT=' "$DIR/.env" 2>/dev/null | cut -d= -f2-)" + [[ "$_existing_min" =~ ^[0-9]+$ ]] && MM_COTURN_MIN_PORT="$_existing_min" + [[ "$_existing_max" =~ ^[0-9]+$ ]] && MM_COTURN_MAX_PORT="$_existing_max" + fi + local _COTURN_SERVICE="" if [ "$USE_EMBEDDED_COTURN" = true ]; then _COTURN_SERVICE=" @@ -431,8 +484,8 @@ networks: - --use-auth-secret - --static-auth-secret=\${COTURN_SECRET} - --realm=\${MM_REALM:-localhost} - - --min-port=49153 - - --max-port=49352 + - --min-port=${MM_COTURN_MIN_PORT} + - --max-port=${MM_COTURN_MAX_PORT} - --no-tls - --no-dtls - --no-cli @@ -509,6 +562,12 @@ TURN_HOST=$TURN_HOST_VAL TURN_PORT=$TURN_PORT_VAL TURN_USERNAME=$TURN_USERNAME_VAL TURN_PASSWORD=$TURN_PASSWORD_VAL +# This instance's OWN coturn relay range -- only set when it runs a dedicated +# coturn above. Left blank when using the shared coturn service, so other +# services' find_free_coturn_range (lib/common.sh) scan correctly skips this +# file instead of treating a range this instance doesn't actually own as claimed. +TURN_MIN_PORT=$( [ "$USE_EMBEDDED_COTURN" = true ] && echo "$MM_COTURN_MIN_PORT" ) +TURN_MAX_PORT=$( [ "$USE_EMBEDDED_COTURN" = true ] && echo "$MM_COTURN_MAX_PORT" ) EOF chmod 600 .env @@ -532,7 +591,7 @@ EOF ufw allow "${CALLS_UDP_PORT}/udp" comment "Mattermost Calls RTC${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}" if [ "$USE_EMBEDDED_COTURN" = true ]; then ufw allow 3479/udp; ufw allow 3479/tcp - ufw allow 49153:49352/udp comment "Mattermost coturn relay" + ufw allow "${MM_COTURN_MIN_PORT}:${MM_COTURN_MAX_PORT}/udp" comment "Mattermost coturn relay" fi # Shared coturn opens its own ports once, at its own install time. fi