Merge main into ionos-script-integration-x32ofw, resolve coturn conflicts

Both branches independently solved the same Mattermost/Asterisk coturn
relay-port collision problem. Kept main's find_free_coturn_range-based
approach (documented in CLAUDE.md as the canonical pattern, and shared
across every coturn-owning service) over this branch's earlier
Mattermost-only port-slot scheme, and cleaned up the now-unused
_MM_COTURN_PORT/_MM_COTURN_MIN/_MM_COTURN_MAX/EMBEDDED_COTURN_SLOT
references that had auto-merged without conflict markers.
This commit is contained in:
Claude
2026-08-14 01:26:28 +00:00
12 changed files with 565 additions and 410 deletions
+73 -98
View File
@@ -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
@@ -223,7 +238,7 @@ CBLOCK
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() {
require_docker || return 1
@@ -236,7 +251,6 @@ install_mattermost() {
local INSTANCE_SUFFIX="" PROJECT="mattermost"
local MM_CONTAINER="mattermost" DB_CONTAINER="mattermost-db"
local WEB_PORT="8065" CALLS_UDP_PORT="8443"
local COTURN_CONSUMER="mattermost"
if [ -d "$DIR" ]; then
echo ""
@@ -265,7 +279,6 @@ install_mattermost() {
PROJECT="mattermost-$_suffix"
MM_CONTAINER="mattermost-$_suffix"
DB_CONTAINER="mattermost-$_suffix-db"
COTURN_CONSUMER="mattermost-$_suffix"
log_info "New instance: $DIR"
fi
fi
@@ -283,8 +296,8 @@ install_mattermost() {
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 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] (falling back to a dedicated coturn if the shared service is unavailable)"
echo "[DRY-RUN] Would run this instance's own dedicated coturn container for TURN, with a relay"
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"
return 0
fi
@@ -301,16 +314,11 @@ install_mattermost() {
return 0
;;
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 ""
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 "coturn service is reused as-is — no need to touch coturn for this."
log_warning "below from scratch, including generating a fresh dedicated coturn"
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=""
prompt_yn " Also delete stored data (Postgres database, uploaded files, config, plugins)? (y/n):" "n" _WIPE_MM_DATA
@@ -421,84 +429,49 @@ networks:
"
fi
# ── TURN: shared coturn preferred, dedicated coturn as fallback ─────────
# See services/coturn.sh's header for why one shared TURN server beats
# every service (Asterisk, each Mattermost instance, ...) running its
# own and fighting over host relay ports.
# ── TURN: always this instance's own dedicated coturn ───────────────────
# There is no shared coturn service in this repo anymore (see
# attic/coturn.sh for why it was retired) — every instance runs its own.
# 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 TURN_HOST_VAL="" TURN_PORT_VAL="" TURN_USERNAME_VAL="" TURN_PASSWORD_VAL=""
if [ "$MODE" = "update" ] && [ "$_HAD_EMBEDDED_COTURN" = true ]; then
USE_EMBEDDED_COTURN=true # preserve exactly — never switch on update
else
ensure_coturn_user "$COTURN_CONSUMER"
if [ -n "${COTURN_HOST:-}" ]; then
USE_EMBEDDED_COTURN=false
TURN_HOST_VAL="$COTURN_HOST"; TURN_PORT_VAL="$COTURN_PORT"
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
# A pre-existing instance with no embedded coturn block predates this
# repo's dedicated-coturn-only model — it's still pointed at a shared
# coturn container this repo no longer installs or manages. Leave it
# running as-is (update never touches .env anyway) rather than trying
# to heal a registration against a service that no longer exists here.
if [ "$MODE" = "update" ] && [ "$_HAD_EMBEDDED_COTURN" != true ]; then
USE_EMBEDDED_COTURN=false
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."
fi
[ -n "$MM_SECRET" ] || MM_SECRET=$(generate_password 48)
# Each embedded-coturn instance (this Mattermost falls back to its own
# dedicated coturn when the shared one isn't available) needs its own
# listening port and relay range, or two instances both on embedded
# coturn collide on identical fixed numbers — confirmed live for the
# Asterisk-vs-Mattermost case this same offset scheme now also fixes
# (see the git history on this block). A relay range can't be found by
# scanning port-by-port like find_free_port does for a single port
# (CLAUDE.md's port-collision-avoidance section is explicit about this
# for large ranges) — so instead each instance gets an integer "slot"
# or the next one already used elsewhere, and a wide-enough width
# (200, matching this range's existing size) keeps slots from
# overlapping each other. base 3479/49253 is right after Asterisk's own
# embedded-coturn numbers (3478/49152-49252) so slot 0 doesn't collide
# with Asterisk either.
#
# The slot is assigned once (the smallest integer not already claimed
# by another mattermost*/.env on this box) and cached in THIS
# instance's own .env as EMBEDDED_COTURN_SLOT, so re-running this same
# instance's installer (update or full reinstall) always reads the
# same slot back instead of potentially reassigning it — reassignment
# would silently move an already-configured instance's TURN port out
# from under it.
local EMBEDDED_COTURN_SLOT=""
[ -f "$DIR/.env" ] && EMBEDDED_COTURN_SLOT="$(grep '^EMBEDDED_COTURN_SLOT=' "$DIR/.env" 2>/dev/null | cut -d= -f2-)"
if [ -z "$EMBEDDED_COTURN_SLOT" ]; then
# Assigning a NEW slot — also live-verify the candidate control port
# and relay-range boundaries aren't already bound by something this
# box's own .env files don't know about (a manually-run process, an
# unrelated service). An already-cached slot (the branch above) is
# trusted as-is and never re-verified — that's what "stable across
# re-runs" means; a live process squatting on an already-assigned
# slot's port is a conflict to report, not silently route around by
# moving an already-configured instance. Can't scan the full
# 200-port relay range port-by-port (CLAUDE.md's
# port-collision-avoidance section covers why large ranges use an
# offset instead of scanning) — checking the control port plus the
# relay range's own two boundary ports is the practical middle
# ground between "no live check at all" and a full range scan.
local _used_slots _cand _p _min _max
_used_slots="$(grep -h '^EMBEDDED_COTURN_SLOT=' "$DOCKER_DIR"/mattermost*/.env 2>/dev/null | cut -d= -f2-)"
_cand=0
while true; do
_p=$((3479 + _cand)); _min=$((49253 + _cand * 200)); _max=$((_min + 199))
if echo "$_used_slots" | grep -qx "$_cand" \
|| port_in_use "$_p" || port_in_use "$_p" udp \
|| port_in_use "$_min" udp || port_in_use "$_max" udp; then
_cand=$((_cand + 1))
continue
fi
break
done
EMBEDDED_COTURN_SLOT="$_cand"
# A dedicated coturn here running alongside Asterisk's own, a sibling
# Mattermost instance's own, or a legacy shared instance still running 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 _MM_COTURN_PORT=$((3479 + EMBEDDED_COTURN_SLOT))
local _MM_COTURN_MIN=$((49253 + EMBEDDED_COTURN_SLOT * 200))
local _MM_COTURN_MAX=$((_MM_COTURN_MIN + 199))
local _COTURN_SERVICE=""
if [ "$USE_EMBEDDED_COTURN" = true ]; then
@@ -510,14 +483,14 @@ networks:
user: root
command:
- -n
- --listening-port=${_MM_COTURN_PORT}
- --listening-port=3479
- --listening-ip=0.0.0.0
- --fingerprint
- --use-auth-secret
- --static-auth-secret=\${COTURN_SECRET}
- --realm=\${MM_REALM:-localhost}
- --min-port=${_MM_COTURN_MIN}
- --max-port=${_MM_COTURN_MAX}
- --min-port=${MM_COTURN_MIN_PORT}
- --max-port=${MM_COTURN_MAX_PORT}
- --no-tls
- --no-dtls
- --no-cli
@@ -601,12 +574,12 @@ TURN_HOST=$TURN_HOST_VAL
TURN_PORT=$TURN_PORT_VAL
TURN_USERNAME=$TURN_USERNAME_VAL
TURN_PASSWORD=$TURN_PASSWORD_VAL
# This instance's embedded-coturn port slot (see the comment above the
# EMBEDDED_COTURN_SLOT assignment in mattermost.sh) — read back on every
# re-run so it never gets reassigned out from under an already-running
# instance. Reserved even when USE_EMBEDDED_COTURN is currently false, in
# case this instance ever falls back to its own coturn later.
EMBEDDED_COTURN_SLOT=$EMBEDDED_COTURN_SLOT
# 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
@@ -629,10 +602,12 @@ EOF
ufw allow "${WEB_PORT}/tcp" comment "Mattermost${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}"
ufw allow "${CALLS_UDP_PORT}/udp" comment "Mattermost Calls RTC${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}"
if [ "$USE_EMBEDDED_COTURN" = true ]; then
ufw allow "${_MM_COTURN_PORT}/udp"; ufw allow "${_MM_COTURN_PORT}/tcp"
ufw allow "${_MM_COTURN_MIN}:${_MM_COTURN_MAX}/udp" comment "Mattermost coturn relay${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}"
ufw allow 3479/udp; ufw allow 3479/tcp
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.
# 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
echo ""
@@ -648,9 +623,9 @@ EOF
# cannot run at the same time as --lt-cred-mech on one instance).
local _ICE_JSON _turn_config_md
if [ "$USE_EMBEDDED_COTURN" = true ]; then
_ICE_JSON="[{\"urls\":[\"turn:${SITE_DOMAIN:-YOUR_IP}:${_MM_COTURN_PORT}?transport=udp\"],\"username\":\"static\",\"credential\":\"see COTURN_SECRET below — this dedicated coturn uses use-auth-secret/HMAC, not a fixed credential\"}]"
_ICE_JSON="[{\"urls\":[\"turn:${SITE_DOMAIN:-YOUR_IP}:3479?transport=udp\"],\"username\":\"static\",\"credential\":\"see COTURN_SECRET below — this dedicated coturn uses use-auth-secret/HMAC, not a fixed credential\"}]"
_turn_config_md="This instance runs its own dedicated coturn (HMAC/REST-API auth):
- TURN Server URI: \`turn:${SITE_DOMAIN:-YOUR_IP}:${_MM_COTURN_PORT}?transport=udp\`
- TURN Server URI: \`turn:${SITE_DOMAIN:-YOUR_IP}:3479?transport=udp\`
- System Console → Plugins → Calls → **TURN Static Auth Secret**: value of \`COTURN_SECRET\` in \`.env\`"
else
_ICE_JSON="[{\"urls\":[\"turn:${TURN_HOST_VAL}:${TURN_PORT_VAL}?transport=udp\"],\"username\":\"${TURN_USERNAME_VAL}\",\"credential\":\"${TURN_PASSWORD_VAL}\"}]"