From 85fc17237732f3e869bf95fe4cdbbf7d91f01b96 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 00:27:33 +0000 Subject: [PATCH] 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 ""