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