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:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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."
|
||||||
|
|||||||
+35
-116
@@ -805,6 +805,41 @@ find_free_port() {
|
|||||||
eval "$_varname='$_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) ─────────────────
|
# ── Caddy reverse-proxy wiring (shared by every web service) ─────────────────
|
||||||
# Usage: configure_caddy_for_service "Name" "UPSTREAM" "default-subdomain" ["extra"]
|
# Usage: configure_caddy_for_service "Name" "UPSTREAM" "default-subdomain" ["extra"]
|
||||||
# UPSTREAM: container:port for caddy_net routing (e.g. "filebrowser:80"),
|
# UPSTREAM: container:port for caddy_net routing (e.g. "filebrowser:80"),
|
||||||
@@ -1003,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"
|
|
||||||
}
|
|
||||||
|
|||||||
+164
-81
@@ -123,6 +123,23 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|||||||
log_success "Swapfile enabled (${SWAP_MB}MB, swappiness=10, persists across reboots)."
|
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() {
|
configure_caddy_for_service() {
|
||||||
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
|
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
|
||||||
local _caddy_dir="$DOCKER_DIR/caddy"
|
local _caddy_dir="$DOCKER_DIR/caddy"
|
||||||
@@ -263,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.
|
||||||
@@ -385,6 +402,30 @@ _asterisk_refresh_vendor_files() {
|
|||||||
else
|
else
|
||||||
log_warning "entrypoint.sh logger.conf template changed upstream — security events won't be logged to a file. Update the sed patch in this installer."
|
log_warning "entrypoint.sh logger.conf template changed upstream — security events won't be logged to a file. Update the sed patch in this installer."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Regenerate the self-signed TLS cert when it doesn't match the current
|
||||||
|
# DOMAIN_NAME. Vendor's own check only asks "does the file exist" and
|
||||||
|
# "does it have a SAN extension" -- never "does the SAN match the domain
|
||||||
|
# actually configured now" -- so a domain entered once (even a
|
||||||
|
# placeholder, or one later changed) sticks in the cert FOREVER: it
|
||||||
|
# survives every subsequent update *and* full reinstall, because
|
||||||
|
# /etc/asterisk/certs is a bind-mounted host directory neither install
|
||||||
|
# mode ever wipes (the same reason pjsip.conf/devices survive reinstalls
|
||||||
|
# too). Confirmed live: a box's TLS transport kept presenting a cert for
|
||||||
|
# a stale, originally-entered domain long after DOMAIN_NAME had changed
|
||||||
|
# and a full reinstall had been run in between -- most SIP/TLS clients
|
||||||
|
# refuse a cert like that outright with no clear error, and this was the
|
||||||
|
# actual cause of a "port's open but registration still fails" case that
|
||||||
|
# every other check (firewall, coturn, DNS) had already come back clean.
|
||||||
|
if grep -q '^if \$regen_cert; then$' ./docker/entrypoint.sh; then
|
||||||
|
sed -i '/^if \$regen_cert; then$/i\
|
||||||
|
if [[ "$regen_cert" != true && -n "${DOMAIN_NAME:-}" ]] && ! openssl x509 -in /etc/asterisk/certs/server.crt -noout -ext subjectAltName 2>/dev/null | grep -q "DNS:${DOMAIN_NAME}"; then\
|
||||||
|
log_info "Existing TLS cert does not match current DOMAIN_NAME (${DOMAIN_NAME}) -- regenerating"\
|
||||||
|
regen_cert=true\
|
||||||
|
fi' ./docker/entrypoint.sh
|
||||||
|
else
|
||||||
|
log_warning "entrypoint.sh cert-regen check changed upstream — a stale-domain cert won't auto-regenerate. Update the sed patch in this installer."
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Shared: log rotation for logs/full (unbounded otherwise) ──────────────
|
# ── Shared: log rotation for logs/full (unbounded otherwise) ──────────────
|
||||||
@@ -983,16 +1024,17 @@ _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_DEPENDS=" depends_on:
|
local _COTURN_DEPENDS=" depends_on:
|
||||||
coturn:
|
coturn:
|
||||||
@@ -1016,8 +1058,8 @@ _asterisk_write_compose() {
|
|||||||
- --lt-cred-mech
|
- --lt-cred-mech
|
||||||
- --user=\${TURN_USERNAME:-easyasterisk}:\${TURN_PASSWORD}
|
- --user=\${TURN_USERNAME:-easyasterisk}:\${TURN_PASSWORD}
|
||||||
- --realm=\${DOMAIN_NAME:-localhost}
|
- --realm=\${DOMAIN_NAME:-localhost}
|
||||||
- --min-port=49152
|
- --min-port=${COTURN_MIN_PORT_VAL}
|
||||||
- --max-port=49252
|
- --max-port=${COTURN_MAX_PORT_VAL}
|
||||||
- --no-tls
|
- --no-tls
|
||||||
- --no-dtls
|
- --no-dtls
|
||||||
- --no-cli
|
- --no-cli
|
||||||
@@ -1060,10 +1102,24 @@ EOF
|
|||||||
|
|
||||||
# Share Caddy's cert store (read-only) so the entrypoint can auto-sync a
|
# Share Caddy's cert store (read-only) so the entrypoint can auto-sync a
|
||||||
# real Let's Encrypt cert for DOMAIN_NAME instead of falling back to
|
# real Let's Encrypt cert for DOMAIN_NAME instead of falling back to
|
||||||
# self-signed. No-op if Caddy isn't installed on this box. Only relevant
|
# self-signed. No-op if Caddy isn't installed on this box.
|
||||||
# to the embedded coturn — the shared coturn service doesn't do TLS/TURNS
|
#
|
||||||
# at all (see services/coturn.sh's README for that tradeoff).
|
# This is entirely about Asterisk's OWN SIP transport-tls cert (port
|
||||||
if [[ "$USE_EMBEDDED_COTURN" == true && -d "$DOCKER_DIR/caddy/data" ]]; then
|
# 5061) -- it has nothing to do with coturn's separate, unrelated TURNS
|
||||||
|
# (TLS-wrapped TURN) capability, which the (since-retired) shared coturn
|
||||||
|
# service indeed didn't support (see attic/coturn.sh's README). A
|
||||||
|
# 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
|
||||||
|
# cert already sitting on disk for DOMAIN_NAME, Asterisk silently kept
|
||||||
|
# generating (and re-generating) a self-signed cert forever, because
|
||||||
|
# /caddy-data was never mounted into the container at all -- sync_caddy_
|
||||||
|
# cert() couldn't see a cert store that, from its own vantage point,
|
||||||
|
# simply didn't exist. Most SIP/TLS clients refuse a self-signed cert
|
||||||
|
# outright with no clear error, which was the actual cause of a
|
||||||
|
# "port's open, cert domain matches, registration still silently fails"
|
||||||
|
# case that every other layer (firewall, coturn reachability, DNS, cert
|
||||||
|
# CN/SAN) had already checked out clean on.
|
||||||
|
if [[ -d "$DOCKER_DIR/caddy/data" ]]; then
|
||||||
sed -i "s#CADDY_VOLUME_PLACEHOLDER# - ${DOCKER_DIR}/caddy/data:/caddy-data:ro#" docker-compose.yml
|
sed -i "s#CADDY_VOLUME_PLACEHOLDER# - ${DOCKER_DIR}/caddy/data:/caddy-data:ro#" docker-compose.yml
|
||||||
else
|
else
|
||||||
sed -i "/CADDY_VOLUME_PLACEHOLDER/d" docker-compose.yml
|
sed -i "/CADDY_VOLUME_PLACEHOLDER/d" docker-compose.yml
|
||||||
@@ -1258,6 +1314,7 @@ CADDY_BLOCK
|
|||||||
# ── DigitalOcean Cloud Firewall (network edge, in front of the droplet) ────
|
# ── DigitalOcean Cloud Firewall (network edge, in front of the droplet) ────
|
||||||
_asterisk_configure_do_cloud_firewall() {
|
_asterisk_configure_do_cloud_firewall() {
|
||||||
local DROPLET_ID="$1" WEB_ADMIN_PORT_VAL="$2" WEB_ADMIN_PUBLIC="$3"
|
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=(
|
local DO_FW_RULES=(
|
||||||
"protocol:tcp,ports:22,address:0.0.0.0/0,address:::/0"
|
"protocol:tcp,ports:22,address:0.0.0.0/0,address:::/0"
|
||||||
@@ -1273,7 +1330,7 @@ _asterisk_configure_do_cloud_firewall() {
|
|||||||
"protocol:tcp,ports:3478,address:0.0.0.0/0,address:::/0"
|
"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: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: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 ""
|
echo ""
|
||||||
@@ -1310,12 +1367,47 @@ _asterisk_configure_do_cloud_firewall() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Non-DO public VPS: no automated network-edge firewall step exists for
|
||||||
|
# arbitrary providers the way _asterisk_configure_do_cloud_firewall automates
|
||||||
|
# DigitalOcean via doctl -- there's no universal API to drive. But a box set
|
||||||
|
# up with a public FQDN is, in practice, almost always sitting behind some
|
||||||
|
# provider-managed firewall anyway, and skipping this reminder left it
|
||||||
|
# entirely unmentioned. Confirmed live on an IONOS VPS: UFW showed every SIP/
|
||||||
|
# TURN/RTP port as ALLOW, Asterisk's own PJSIP logger showed zero incoming
|
||||||
|
# packets of any kind, and nothing in this installer's own output pointed at
|
||||||
|
# the actual cause -- IONOS's separate network-level firewall (Cloud Panel ->
|
||||||
|
# Networking -> Firewall Policies) only allowed 22/80/443/8443/8447 and
|
||||||
|
# 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.
|
||||||
|
_asterisk_remind_non_do_firewall() {
|
||||||
|
local WEB_ADMIN_PORT_VAL="$1" WEB_ADMIN_PUBLIC_ACCESS_NEEDED="$2"
|
||||||
|
local COTURN_MIN_PORT_VAL="${3:-49152}" COTURN_MAX_PORT_VAL="${4:-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"
|
||||||
|
log_warning "for DO, via doctl). Most VPS/cloud providers run their OWN network-level"
|
||||||
|
log_warning "firewall in front of the box, separate from UFW and invisible to it — UFW can"
|
||||||
|
log_warning "show every port as ALLOW while traffic still gets silently dropped before it"
|
||||||
|
log_warning "ever reaches this box. Check your provider's console for it (e.g. IONOS: Cloud"
|
||||||
|
log_warning "Panel -> Networking -> Firewall Policies) and allow inbound, matching what UFW"
|
||||||
|
log_warning "just opened on this box:"
|
||||||
|
echo " TCP 22 (SSH)"
|
||||||
|
echo " UDP/TCP 5060 (SIP)"
|
||||||
|
echo " TCP 5061 (SIP TLS)"
|
||||||
|
[[ "$WEB_ADMIN_PUBLIC_ACCESS_NEEDED" == true ]] && echo " TCP ${WEB_ADMIN_PORT_VAL} (web admin)"
|
||||||
|
echo " TCP 8088, 8089 (Asterisk HTTP/HTTPS)"
|
||||||
|
echo " UDP 10000-20000 (RTP media)"
|
||||||
|
echo " UDP/TCP 3478 (TURN/STUN)"
|
||||||
|
echo " UDP ${COTURN_MIN_PORT_VAL}-${COTURN_MAX_PORT_VAL} (TURN relay)"
|
||||||
|
}
|
||||||
|
|
||||||
# ── Shared: README ─────────────────────────────────────────────────────────
|
# ── Shared: README ─────────────────────────────────────────────────────────
|
||||||
# One document with a droplet-only section appended in public-cloud mode, so
|
# One document with a droplet-only section appended in public-cloud mode, so
|
||||||
# the two deployment shapes can't document themselves differently by accident.
|
# the two deployment shapes can't document themselves differently by accident.
|
||||||
_asterisk_write_readme() {
|
_asterisk_write_readme() {
|
||||||
local EA_DIR="$1" CONTAINER="$2" IS_DO="$3" DOMAIN_NAME="$4" PUBLIC_IP="$5" WEB_ADMIN_PORT_VAL="$6"
|
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 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:-<host-ip>}}"
|
local _host="${DOMAIN_NAME:-${PUBLIC_IP:-<host-ip>}}"
|
||||||
[ -z "$TURN_SERVER_DISPLAY" ] && TURN_SERVER_DISPLAY="${_host}:3478"
|
[ -z "$TURN_SERVER_DISPLAY" ] && TURN_SERVER_DISPLAY="${_host}:3478"
|
||||||
|
|
||||||
@@ -1362,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 +
|
||||||
@@ -1447,11 +1537,9 @@ docker exec -it ${CONTAINER} easy-asterisk
|
|||||||
| 5061 | TCP | SIP over TLS |
|
| 5061 | TCP | SIP over TLS |
|
||||||
| ${WEB_ADMIN_PORT_VAL} | TCP | Easy Asterisk web admin (auto-picked — see \`.env\`) |
|
| ${WEB_ADMIN_PORT_VAL} | TCP | Easy Asterisk web admin (auto-picked — see \`.env\`) |
|
||||||
| 8088/8089 | TCP | Asterisk HTTP/WS (ARI/AMI) |
|
| 8088/8089 | TCP | Asterisk HTTP/WS (ARI/AMI) |
|
||||||
|
| 3478 | UDP/TCP | TURN/STUN (coturn) |
|
||||||
| 10000–20000 | UDP | RTP media streams |
|
| 10000–20000 | UDP | RTP media streams |
|
||||||
$( [[ "$USE_EMBEDDED_COTURN" == true ]] \
|
| ${COTURN_MIN_PORT_VAL}–${COTURN_MAX_PORT_VAL} | UDP | TURN relay media ports (only if this install runs its own dedicated coturn — see below) |
|
||||||
&& echo "| 3478 | UDP/TCP | TURN/STUN (this install's own dedicated coturn) |
|
|
||||||
| 49152–49252 | UDP | TURN relay media ports (dedicated coturn) |" \
|
|
||||||
|| echo "| See \`~/docker/coturn/.env\` | UDP/TCP | TURN/STUN — shared coturn service, not opened by this install |" )
|
|
||||||
|
|
||||||
## Data directories (all inside ${EA_DIR}/, included in backup)
|
## Data directories (all inside ${EA_DIR}/, included in backup)
|
||||||
|
|
||||||
@@ -1585,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,"
|
||||||
@@ -1657,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"
|
||||||
@@ -1697,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
|
||||||
|
|
||||||
@@ -1802,36 +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
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
ensure_coturn_user "asterisk"
|
|
||||||
if [[ -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"
|
||||||
@@ -1844,10 +1908,23 @@ 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
|
||||||
log_info "Shared coturn unavailable — Asterisk will run its own dedicated coturn."
|
|
||||||
fi
|
|
||||||
|
|
||||||
_asterisk_write_compose "$ASTERISK_PROJECT" "$CONTAINER" "$ASTERISK_COTURN" "$USE_EMBEDDED_COTURN"
|
# A dedicated coturn here running alongside Asterisk's own on a prior
|
||||||
|
# install, or 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 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
|
||||||
|
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."
|
||||||
|
|
||||||
|
_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 ─────────────────────────────────────
|
# ── Pick a free port for the web admin ─────────────────────────────────────
|
||||||
# Hardcoding a single number gets fragile fast once several services share
|
# Hardcoding a single number gets fragile fast once several services share
|
||||||
@@ -1886,12 +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 — other services' find_free_coturn_range
|
||||||
|
# (lib/common.sh) scans this file to avoid claiming an overlapping range.
|
||||||
|
TURN_MIN_PORT=${EMBEDDED_COTURN_MIN_PORT}
|
||||||
|
TURN_MAX_PORT=${EMBEDDED_COTURN_MAX_PORT}
|
||||||
|
|
||||||
# ── RTP port range ────────────────────────────────────────────
|
# ── RTP port range ────────────────────────────────────────────
|
||||||
RTP_START=10000
|
RTP_START=10000
|
||||||
@@ -1955,20 +2036,21 @@ 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 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.
|
|
||||||
ensure_ufw_enabled
|
ensure_ufw_enabled
|
||||||
log_success "UFW rules added."
|
log_success "UFW rules added."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── DigitalOcean Cloud Firewall (network edge) ────────────────────────────
|
# ── Network-edge firewall (in front of the box, not UFW) ──────────────────
|
||||||
[[ "$IS_DO" == true ]] && \
|
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" \
|
||||||
|
"$EMBEDDED_COTURN_MIN_PORT" "$EMBEDDED_COTURN_MAX_PORT"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── CrowdSec note ──────────────────────────────────────────────────────────
|
# ── CrowdSec note ──────────────────────────────────────────────────────────
|
||||||
# Not installed here — select it separately from the whiptail menu, or
|
# Not installed here — select it separately from the whiptail menu, or
|
||||||
@@ -1993,7 +2075,8 @@ ENV
|
|||||||
|
|
||||||
# ── README ────────────────────────────────────────────────────────────────
|
# ── README ────────────────────────────────────────────────────────────────
|
||||||
_asterisk_write_readme "$EA_DIR" "$CONTAINER" "$IS_DO" "$DOMAIN_NAME" "$PUBLIC_IP" "$WEB_ADMIN_PORT_VAL" \
|
_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 ─────────────────────────────────────────────────────────────────
|
# ── Start ─────────────────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
+72
-97
@@ -59,6 +59,21 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|||||||
eval "$_varname='$_port'"
|
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
|
# Match common.sh's eval-based pattern so local vars in install_* are set correctly
|
||||||
prompt_text() {
|
prompt_text() {
|
||||||
local _q="$1" _def="$2" _var="$3" _r
|
local _q="$1" _def="$2" _var="$3" _r
|
||||||
@@ -223,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
|
||||||
@@ -236,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 ""
|
||||||
@@ -265,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
|
||||||
@@ -283,8 +296,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
|
||||||
@@ -301,16 +314,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
|
||||||
|
|
||||||
@@ -421,84 +429,49 @@ 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=""
|
||||||
|
|
||||||
if [ "$MODE" = "update" ] && [ "$_HAD_EMBEDDED_COTURN" = true ]; then
|
# A pre-existing instance with no embedded coturn block predates this
|
||||||
USE_EMBEDDED_COTURN=true # preserve exactly — never switch on update
|
# repo's dedicated-coturn-only model — it's still pointed at a shared
|
||||||
else
|
# coturn container this repo no longer installs or manages. Leave it
|
||||||
ensure_coturn_user "$COTURN_CONSUMER"
|
# running as-is (update never touches .env anyway) rather than trying
|
||||||
if [ -n "${COTURN_HOST:-}" ]; then
|
# to heal a registration against a service that no longer exists here.
|
||||||
|
if [ "$MODE" = "update" ] && [ "$_HAD_EMBEDDED_COTURN" != true ]; 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)
|
||||||
|
|
||||||
# Each embedded-coturn instance (this Mattermost falls back to its own
|
# A dedicated coturn here running alongside Asterisk's own, a sibling
|
||||||
# dedicated coturn when the shared one isn't available) needs its own
|
# Mattermost instance's own, or a legacy shared instance still running is
|
||||||
# listening port and relay range, or two instances both on embedded
|
# the same pre-merge relay-port collision this repo's coturn history
|
||||||
# coturn collide on identical fixed numbers — confirmed live for the
|
# warns about (confirmed live: two independent coturns' default ranges
|
||||||
# Asterisk-vs-Mattermost case this same offset scheme now also fixes
|
# used to overlap by ~100 UDP ports). find_free_coturn_range (lib/common.sh) checks every coturn-
|
||||||
# (see the git history on this block). A relay range can't be found by
|
# owning service's .env on the box and picks a range starting safely past
|
||||||
# scanning port-by-port like find_free_port does for a single port
|
# whatever's already claimed; the historical 49153-49352 default only
|
||||||
# (CLAUDE.md's port-collision-avoidance section is explicit about this
|
# survives when nothing else on the box claims a range at all.
|
||||||
# for large ranges) — so instead each instance gets an integer "slot"
|
local MM_COTURN_MIN_PORT=49153 MM_COTURN_MAX_PORT=49352
|
||||||
# or the next one already used elsewhere, and a wide-enough width
|
if [ "$USE_EMBEDDED_COTURN" = true ] && [ "$MODE" != "update" ]; then
|
||||||
# (200, matching this range's existing size) keeps slots from
|
find_free_coturn_range MM_COTURN_MIN_PORT MM_COTURN_MAX_PORT 200 49153
|
||||||
# overlapping each other. base 3479/49253 is right after Asterisk's own
|
[[ "$MM_COTURN_MIN_PORT" != 49153 ]] && \
|
||||||
# embedded-coturn numbers (3478/49152-49252) so slot 0 doesn't collide
|
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."
|
||||||
# with Asterisk either.
|
elif [ "$MODE" = "update" ] && [ -f "$DIR/.env" ]; then
|
||||||
#
|
# Preserve whatever range this instance was already using — an update
|
||||||
# The slot is assigned once (the smallest integer not already claimed
|
# must never silently move it (a live coturn container restarting on
|
||||||
# by another mattermost*/.env on this box) and cached in THIS
|
# a different port range would break in-flight/repeat Calls sessions).
|
||||||
# instance's own .env as EMBEDDED_COTURN_SLOT, so re-running this same
|
local _existing_min _existing_max
|
||||||
# instance's installer (update or full reinstall) always reads the
|
_existing_min="$(grep -E '^TURN_MIN_PORT=' "$DIR/.env" 2>/dev/null | cut -d= -f2-)"
|
||||||
# same slot back instead of potentially reassigning it — reassignment
|
_existing_max="$(grep -E '^TURN_MAX_PORT=' "$DIR/.env" 2>/dev/null | cut -d= -f2-)"
|
||||||
# would silently move an already-configured instance's TURN port out
|
[[ "$_existing_min" =~ ^[0-9]+$ ]] && MM_COTURN_MIN_PORT="$_existing_min"
|
||||||
# from under it.
|
[[ "$_existing_max" =~ ^[0-9]+$ ]] && MM_COTURN_MAX_PORT="$_existing_max"
|
||||||
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
|
fi
|
||||||
break
|
|
||||||
done
|
|
||||||
EMBEDDED_COTURN_SLOT="$_cand"
|
|
||||||
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=""
|
local _COTURN_SERVICE=""
|
||||||
if [ "$USE_EMBEDDED_COTURN" = true ]; then
|
if [ "$USE_EMBEDDED_COTURN" = true ]; then
|
||||||
@@ -510,14 +483,14 @@ networks:
|
|||||||
user: root
|
user: root
|
||||||
command:
|
command:
|
||||||
- -n
|
- -n
|
||||||
- --listening-port=${_MM_COTURN_PORT}
|
- --listening-port=3479
|
||||||
- --listening-ip=0.0.0.0
|
- --listening-ip=0.0.0.0
|
||||||
- --fingerprint
|
- --fingerprint
|
||||||
- --use-auth-secret
|
- --use-auth-secret
|
||||||
- --static-auth-secret=\${COTURN_SECRET}
|
- --static-auth-secret=\${COTURN_SECRET}
|
||||||
- --realm=\${MM_REALM:-localhost}
|
- --realm=\${MM_REALM:-localhost}
|
||||||
- --min-port=${_MM_COTURN_MIN}
|
- --min-port=${MM_COTURN_MIN_PORT}
|
||||||
- --max-port=${_MM_COTURN_MAX}
|
- --max-port=${MM_COTURN_MAX_PORT}
|
||||||
- --no-tls
|
- --no-tls
|
||||||
- --no-dtls
|
- --no-dtls
|
||||||
- --no-cli
|
- --no-cli
|
||||||
@@ -601,12 +574,12 @@ TURN_HOST=$TURN_HOST_VAL
|
|||||||
TURN_PORT=$TURN_PORT_VAL
|
TURN_PORT=$TURN_PORT_VAL
|
||||||
TURN_USERNAME=$TURN_USERNAME_VAL
|
TURN_USERNAME=$TURN_USERNAME_VAL
|
||||||
TURN_PASSWORD=$TURN_PASSWORD_VAL
|
TURN_PASSWORD=$TURN_PASSWORD_VAL
|
||||||
# This instance's embedded-coturn port slot (see the comment above the
|
# This instance's OWN coturn relay range -- only set when it runs a dedicated
|
||||||
# EMBEDDED_COTURN_SLOT assignment in mattermost.sh) — read back on every
|
# coturn above. Left blank when using the shared coturn service, so other
|
||||||
# re-run so it never gets reassigned out from under an already-running
|
# services' find_free_coturn_range (lib/common.sh) scan correctly skips this
|
||||||
# instance. Reserved even when USE_EMBEDDED_COTURN is currently false, in
|
# file instead of treating a range this instance doesn't actually own as claimed.
|
||||||
# case this instance ever falls back to its own coturn later.
|
TURN_MIN_PORT=$( [ "$USE_EMBEDDED_COTURN" = true ] && echo "$MM_COTURN_MIN_PORT" )
|
||||||
EMBEDDED_COTURN_SLOT=$EMBEDDED_COTURN_SLOT
|
TURN_MAX_PORT=$( [ "$USE_EMBEDDED_COTURN" = true ] && echo "$MM_COTURN_MAX_PORT" )
|
||||||
EOF
|
EOF
|
||||||
chmod 600 .env
|
chmod 600 .env
|
||||||
|
|
||||||
@@ -629,10 +602,12 @@ EOF
|
|||||||
ufw allow "${WEB_PORT}/tcp" comment "Mattermost${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}"
|
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)}"
|
ufw allow "${CALLS_UDP_PORT}/udp" comment "Mattermost Calls RTC${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}"
|
||||||
if [ "$USE_EMBEDDED_COTURN" = true ]; then
|
if [ "$USE_EMBEDDED_COTURN" = true ]; then
|
||||||
ufw allow "${_MM_COTURN_PORT}/udp"; ufw allow "${_MM_COTURN_PORT}/tcp"
|
ufw allow 3479/udp; ufw allow 3479/tcp
|
||||||
ufw allow "${_MM_COTURN_MIN}:${_MM_COTURN_MAX}/udp" comment "Mattermost coturn relay${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}"
|
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 ""
|
||||||
@@ -648,9 +623,9 @@ EOF
|
|||||||
# cannot run at the same time as --lt-cred-mech on one instance).
|
# cannot run at the same time as --lt-cred-mech on one instance).
|
||||||
local _ICE_JSON _turn_config_md
|
local _ICE_JSON _turn_config_md
|
||||||
if [ "$USE_EMBEDDED_COTURN" = true ]; then
|
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_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\`"
|
- System Console → Plugins → Calls → **TURN Static Auth Secret**: value of \`COTURN_SECRET\` in \`.env\`"
|
||||||
else
|
else
|
||||||
_ICE_JSON="[{\"urls\":[\"turn:${TURN_HOST_VAL}:${TURN_PORT_VAL}?transport=udp\"],\"username\":\"${TURN_USERNAME_VAL}\",\"credential\":\"${TURN_PASSWORD_VAL}\"}]"
|
_ICE_JSON="[{\"urls\":[\"turn:${TURN_HOST_VAL}:${TURN_PORT_VAL}?transport=udp\"],\"username\":\"${TURN_USERNAME_VAL}\",\"credential\":\"${TURN_PASSWORD_VAL}\"}]"
|
||||||
|
|||||||
+128
-31
@@ -3036,11 +3036,31 @@ def ea_device_sipnetic_string(extension):
|
|||||||
"""Sipnetic's own documented "account string" QR-scan format
|
"""Sipnetic's own documented "account string" QR-scan format
|
||||||
(https://www.sipnetic.com/qr-codes): semicolon-separated key=value pairs,
|
(https://www.sipnetic.com/qr-codes): semicolon-separated key=value pairs,
|
||||||
n=display name, u=username, d=domain/IP (no port), p=password,
|
n=display name, u=username, d=domain/IP (no port), p=password,
|
||||||
dt=default transport (0=UDP, 1=TCP, 2=TLS). Unlike
|
dt=default transport (0=UDP, 1=TCP, 2=TLS), st=STUN/TURN server. Unlike
|
||||||
ea_device_provisioning()'s deliberately generic plain-text file, this one
|
ea_device_provisioning()'s deliberately generic plain-text file, this one
|
||||||
IS a verified, documented format for one specific app, built from the
|
IS a verified, documented format for one specific app, built from the
|
||||||
exact same ea_device_details() data.
|
exact same ea_device_details() data.
|
||||||
|
|
||||||
|
st is intentionally set to an explicit turn:user:pass@host URI whenever
|
||||||
|
coturn is configured, rather than left unset -- leaving it out doesn't
|
||||||
|
mean "no TURN", it means Sipnetic falls back to its own default/built-in
|
||||||
|
STUN server instead of the coturn instance Asterisk itself is actually
|
||||||
|
using, which is silently wrong rather than absent. This is the same
|
||||||
|
TURN_SERVER/TURN_USERNAME/TURN_PASSWORD ea_device_details() already
|
||||||
|
reads from the Asterisk .env (see ea_connection_defaults()) -- whichever
|
||||||
|
coturn Asterisk is actually configured against.
|
||||||
|
|
||||||
|
The host is deliberately stripped of its port before going into st.
|
||||||
|
Sipnetic's own doc for this field is explicit that the value is a
|
||||||
|
"hostname or IP address without port", and its own worked example is
|
||||||
|
`st=turn:user:password@turn.mydomain.com;` -- no port anywhere, even
|
||||||
|
in the URI form. Confirmed live: appending :3478 (matching the doc's
|
||||||
|
generic URI-with-credentials description, which doesn't actually show a
|
||||||
|
port example) gets silently truncated by the app -- the FQDN came
|
||||||
|
through, the port after it did not. coturn's listening port in this
|
||||||
|
repo is always the STUN/TURN-conventional 3478, which is what a
|
||||||
|
portless address implies anyway, so dropping it costs nothing.
|
||||||
|
|
||||||
A literal ';' in any field must be doubled per that same doc page --
|
A literal ';' in any field must be doubled per that same doc page --
|
||||||
generated passwords are alnum-only (_ea_generate_password) so this only
|
generated passwords are alnum-only (_ea_generate_password) so this only
|
||||||
matters for a hand-typed device name, but escaping costs nothing."""
|
matters for a hand-typed device name, but escaping costs nothing."""
|
||||||
@@ -3053,10 +3073,19 @@ def ea_device_sipnetic_string(extension):
|
|||||||
|
|
||||||
host = d["server"] or ""
|
host = d["server"] or ""
|
||||||
dt = "2" if d["transport"] == "TLS" else "0"
|
dt = "2" if d["transport"] == "TLS" else "0"
|
||||||
return "n=%s;u=%s;d=%s;p=%s;dt=%s;" % (
|
fields = [
|
||||||
esc_field(d["name"] or d["extension"]), esc_field(d["extension"]),
|
"n=%s" % esc_field(d["name"] or d["extension"]),
|
||||||
esc_field(host), esc_field(d["password"]), dt,
|
"u=%s" % esc_field(d["extension"]),
|
||||||
)
|
"d=%s" % esc_field(host),
|
||||||
|
"p=%s" % esc_field(d["password"]),
|
||||||
|
"dt=%s" % dt,
|
||||||
|
]
|
||||||
|
if d.get("turn_server") and d.get("turn_username") and d.get("turn_password"):
|
||||||
|
turn_host = d["turn_server"].rsplit(":", 1)[0]
|
||||||
|
fields.append("st=%s" % esc_field(
|
||||||
|
"turn:%s:%s@%s" % (d["turn_username"], d["turn_password"], turn_host)
|
||||||
|
))
|
||||||
|
return ";".join(fields) + ";"
|
||||||
|
|
||||||
|
|
||||||
def _ea_edit_device_block(extension, mutate):
|
def _ea_edit_device_block(extension, mutate):
|
||||||
@@ -3612,7 +3641,14 @@ INDEX_HTML = """<!doctype html>
|
|||||||
}
|
}
|
||||||
nav button:hover { color: var(--text); }
|
nav button:hover { color: var(--text); }
|
||||||
nav button.active { color: var(--text); border-bottom-color: var(--accent); }
|
nav button.active { color: var(--text); border-bottom-color: var(--accent); }
|
||||||
main { padding: var(--sp-6); max-width: 1180px; margin: 0 auto; }
|
/* 1180 was too narrow for the Extensions table specifically (Ext, Name,
|
||||||
|
Mobile, Status, Transport, PSTN, Whitelist, Messaging, Voicemail, plus
|
||||||
|
the row-action column) -- ten columns including a dropdown and a free-text
|
||||||
|
whitelist field forced .table-wrap's horizontal scrollbar even on a normal
|
||||||
|
desktop viewport. 1600 gives every tab's tables room without it; narrow
|
||||||
|
viewports still fall back to that same scrollbar (.table-wrap already
|
||||||
|
handles it), this only raises the ceiling for wide ones. */
|
||||||
|
main { padding: var(--sp-6); max-width: 1600px; margin: 0 auto; }
|
||||||
|
|
||||||
/* ── Cards ────────────────────────────────────────────────────────────── */
|
/* ── Cards ────────────────────────────────────────────────────────────── */
|
||||||
.card {
|
.card {
|
||||||
@@ -3804,6 +3840,48 @@ INDEX_HTML = """<!doctype html>
|
|||||||
.toast.ok { border-left-color: var(--ok); }
|
.toast.ok { border-left-color: var(--ok); }
|
||||||
@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
|
@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
|
||||||
|
|
||||||
|
/* ── QR modal ─────────────────────────────────────────────────────────── */
|
||||||
|
/* Above #toasts (z-index 60) since a toast firing while the modal is open
|
||||||
|
(e.g. a save from another tab action) should still be visible on top. */
|
||||||
|
#qr-modal-overlay {
|
||||||
|
position: fixed; inset: 0; z-index: 70;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
display: none; align-items: center; justify-content: center;
|
||||||
|
padding: var(--sp-4);
|
||||||
|
}
|
||||||
|
#qr-modal-overlay.show { display: flex; }
|
||||||
|
/* Sized to the QR frame's own 192px, not this -- the caption text below it
|
||||||
|
(TURN credentials warning included) wrapped down to a couple of
|
||||||
|
characters per line at that width. 320px gives it room to breathe while
|
||||||
|
staying well short of the surrounding card. */
|
||||||
|
.qr-modal {
|
||||||
|
position: relative; width: 320px; max-width: calc(100vw - 2 * var(--sp-4));
|
||||||
|
background: var(--surface); border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius); padding: var(--sp-4);
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,0.45);
|
||||||
|
display: flex; flex-direction: column; align-items: center; gap: var(--sp-2);
|
||||||
|
}
|
||||||
|
.qr-modal-close {
|
||||||
|
position: absolute; top: var(--sp-2); right: var(--sp-2);
|
||||||
|
background: none; border: none; color: var(--text-faint); cursor: pointer;
|
||||||
|
font-size: 1.3rem; line-height: 1; padding: 0.2rem 0.4rem; border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
.qr-modal-close:hover { color: var(--danger); background: rgba(255,107,107,0.1); }
|
||||||
|
/* qrcode.js draws modules edge-to-edge with no margin of its own -- the
|
||||||
|
rendered image's own content ran right to its edge (verified against
|
||||||
|
the raw generated PNG: the code region covered all but ~1px of it).
|
||||||
|
Real camera scanners need an actual light quiet zone around the code
|
||||||
|
per the QR spec, not the modal's dark theme background touching the
|
||||||
|
modules directly -- this was reported as unreadable by both Sipnetic
|
||||||
|
and Linphone before this fix. 192px/20px = 2in outer frame, ~4-module
|
||||||
|
white border on each side, sized generously since exact module count
|
||||||
|
varies with the encoded string length. */
|
||||||
|
#qr-modal-canvas {
|
||||||
|
width: 192px; height: 192px; box-sizing: border-box;
|
||||||
|
background: #fff; padding: 20px; border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
#qr-modal-canvas img, #qr-modal-canvas canvas { width: 100%; height: 100%; display: block; }
|
||||||
|
|
||||||
/* The one-time device password must not auto-dismiss like a toast does. */
|
/* The one-time device password must not auto-dismiss like a toast does. */
|
||||||
.callout {
|
.callout {
|
||||||
display: none; align-items: flex-start; gap: var(--sp-3);
|
display: none; align-items: flex-start; gap: var(--sp-3);
|
||||||
@@ -4033,7 +4111,7 @@ INDEX_HTML = """<!doctype html>
|
|||||||
<p class="muted"><b>Ring</b> dials every member at once — first to answer gets the call, everyone else stops ringing. <b>Page</b> tells Asterisk to signal auto-answer to every member via SIP headers, for devices that honor it, turning the same simultaneous dial into a one-way intercom-style broadcast instead.</p>
|
<p class="muted"><b>Ring</b> dials every member at once — first to answer gets the call, everyone else stops ringing. <b>Page</b> tells Asterisk to signal auto-answer to every member via SIP headers, for devices that honor it, turning the same simultaneous dial into a one-way intercom-style broadcast instead.</p>
|
||||||
<p class="muted">You don't need <b>Page</b> just to mix an auto-answering device with normally-ringing phones in the same group, though — auto-answer is really a property of the device's own SIP client configuration, not something Asterisk enforces per member. Confirmed against baresip's own source: it decides purely from its account's local <code>answermode</code> setting and never looks at any auto-answer signal on the incoming call, so a device configured to auto-answer (e.g. a dedicated intercom/kiosk running <code>baresip</code> in Answer Mode: Auto) picks up <i>everything</i> routed to it instantly and unconditionally, while ordinary phones in the same plain <b>Ring</b> group just keep ringing until a person answers — no extra setting needed here for that mix.</p>
|
<p class="muted">You don't need <b>Page</b> just to mix an auto-answering device with normally-ringing phones in the same group, though — auto-answer is really a property of the device's own SIP client configuration, not something Asterisk enforces per member. Confirmed against baresip's own source: it decides purely from its account's local <code>answermode</code> setting and never looks at any auto-answer signal on the incoming call, so a device configured to auto-answer (e.g. a dedicated intercom/kiosk running <code>baresip</code> in Answer Mode: Auto) picks up <i>everything</i> routed to it instantly and unconditionally, while ordinary phones in the same plain <b>Ring</b> group just keep ringing until a person answers — no extra setting needed here for that mix.</p>
|
||||||
<p class="muted">For a dedicated always-on auto-answer device (a wall-mounted intercom, a paging station), Easy Asterisk — the vendor project this installer builds on — has a built-in <code>baresip</code>-based kiosk client for exactly that. It installs on a separate small Linux machine (an old PC, a Raspberry Pi), not this Asterisk server itself: <a href="/download/kiosk-client-installer.sh" download>download the installer script</a>, then see <code>docs/kiosk-paging-setup.md</code> in this repo for the full walkthrough.</p>
|
<p class="muted">For a dedicated always-on auto-answer device (a wall-mounted intercom, a paging station), Easy Asterisk — the vendor project this installer builds on — has a built-in <code>baresip</code>-based kiosk client for exactly that. It installs on a separate small Linux machine (an old PC, a Raspberry Pi), not this Asterisk server itself: <a href="/download/kiosk-client-installer.sh" download>download the installer script</a>, then see <code>docs/kiosk-paging-setup.md</code> in this repo for the full walkthrough.</p>
|
||||||
<p class="muted">For a phone or tablet running Sipnetic instead, each extension's own detail panel below (Extensions tab → click a row → "Sipnetic QR code") can generate a scan-to-configure code — no dedicated kiosk hardware needed for that one.</p>
|
<p class="muted">For a phone or tablet running Sipnetic instead, each extension's own detail panel below (Extensions tab → click a row → "Click here for a QR code") can generate a scan-to-configure code — no dedicated kiosk hardware needed for that one.</p>
|
||||||
</details>
|
</details>
|
||||||
<div class="row" style="margin-bottom:var(--sp-2)">
|
<div class="row" style="margin-bottom:var(--sp-2)">
|
||||||
<input type="text" id="ea-room-ext" placeholder="Extension, e.g. 500" style="width:9rem">
|
<input type="text" id="ea-room-ext" placeholder="Extension, e.g. 500" style="width:9rem">
|
||||||
@@ -4106,6 +4184,14 @@ INDEX_HTML = """<!doctype html>
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<div id="toasts"></div>
|
<div id="toasts"></div>
|
||||||
|
<div id="qr-modal-overlay" onclick="if (event.target === this) closeSipneticQr()">
|
||||||
|
<div class="qr-modal">
|
||||||
|
<button class="qr-modal-close" onclick="closeSipneticQr()" aria-label="Close">×</button>
|
||||||
|
<div id="qr-modal-canvas"></div>
|
||||||
|
<p class="muted" style="margin:var(--sp-2) 0 0; font-size:0.8rem; width:100%">Scan with Sipnetic (Add Account → Scan QR Code) to auto-fill this extension's SIP and TURN settings.</p>
|
||||||
|
<p class="muted" style="margin:0; font-size:0.75rem; width:100%">Contains the extension's password and TURN credentials in plain text — treat the image like the password itself.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
// Embedded verbatim (license header preserved below) for the Sipnetic
|
// Embedded verbatim (license header preserved below) for the Sipnetic
|
||||||
// QR-provisioning feature -- self-contained, no CDN dependency, same
|
// QR-provisioning feature -- self-contained, no CDN dependency, same
|
||||||
@@ -5525,10 +5611,9 @@ async function showEaDeviceDetails(ext) {
|
|||||||
</button>
|
</button>
|
||||||
<button class="action" onclick="resetEaPassword('${esc(d.extension)}')">Reset password</button>
|
<button class="action" onclick="resetEaPassword('${esc(d.extension)}')">Reset password</button>
|
||||||
<a class="action" style="text-decoration:none" href="/api/ea-device-provisioning?ext=${encodeURIComponent(d.extension)}" download>Download settings</a>
|
<a class="action" style="text-decoration:none" href="/api/ea-device-provisioning?ext=${encodeURIComponent(d.extension)}" download>Download settings</a>
|
||||||
<button class="action" onclick="toggleSipneticQr('${esc(d.extension)}')">Sipnetic QR code</button>
|
<button class="action" onclick="showSipneticQr('${esc(d.extension)}')">Click here for a QR code</button>
|
||||||
<button class="action" onclick="closeEaDeviceDetails()">Close</button>
|
<button class="action" onclick="closeEaDeviceDetails()">Close</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="sipnetic-qr-box" style="display:none"></div>
|
|
||||||
${d.env_error ? `<p class="muted" style="margin:var(--sp-2) 0 0; color:var(--warn)">${esc(d.env_error)}</p>` : ""}
|
${d.env_error ? `<p class="muted" style="margin:var(--sp-2) 0 0; color:var(--warn)">${esc(d.env_error)}</p>` : ""}
|
||||||
<p class="muted" style="margin:var(--sp-2) 0 0">A phone that never registers is most often the transport above: an extension
|
<p class="muted" style="margin:var(--sp-2) 0 0">A phone that never registers is most often the transport above: an extension
|
||||||
written LAN-only while the phone dials in over TLS from outside. If the Security Log shows nothing at all for it, the traffic
|
written LAN-only while the phone dials in over TLS from outside. If the Security Log shows nothing at all for it, the traffic
|
||||||
@@ -5543,33 +5628,45 @@ async function showEaDeviceDetails(ext) {
|
|||||||
// qr-codes) -- scan-to-configure, built server-side from the same data the
|
// qr-codes) -- scan-to-configure, built server-side from the same data the
|
||||||
// details panel already shows. Rendered client-side with the embedded
|
// details panel already shows. Rendered client-side with the embedded
|
||||||
// qrcode.js at the top of this script block so nothing here needs a CDN or
|
// qrcode.js at the top of this script block so nothing here needs a CDN or
|
||||||
// a new Python dependency.
|
// a new Python dependency. Shown in a small popup (#qr-modal-overlay, ~2in
|
||||||
async function toggleSipneticQr(ext) {
|
// square) rather than inline -- it contains the extension's password in
|
||||||
const box = document.getElementById("sipnetic-qr-box");
|
// plain text, so it should be glanced at and dismissed, not left sitting
|
||||||
if (!box) return;
|
// open in the page.
|
||||||
if (box.style.display !== "none" && box.dataset.ext === ext) {
|
async function showSipneticQr(ext) {
|
||||||
box.style.display = "none";
|
const overlay = document.getElementById("qr-modal-overlay");
|
||||||
box.innerHTML = "";
|
const canvas = document.getElementById("qr-modal-canvas");
|
||||||
return;
|
if (!overlay || !canvas) return;
|
||||||
}
|
|
||||||
const res = await fetch("/api/ea-device-qr?ext=" + encodeURIComponent(ext));
|
const res = await fetch("/api/ea-device-qr?ext=" + encodeURIComponent(ext));
|
||||||
if (!res.ok) { toast("No QR data for extension " + ext, "err"); return; }
|
if (!res.ok) { toast("No QR data for extension " + ext, "err"); return; }
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
box.dataset.ext = ext;
|
canvas.innerHTML = "";
|
||||||
box.innerHTML = `
|
new QRCode(canvas, {
|
||||||
<div class="row" style="align-items:flex-start; gap:var(--sp-3); margin-top:var(--sp-3)">
|
// Rendered at 3x the on-screen size (456, not 152) then scaled back down
|
||||||
<div id="sipnetic-qr-canvas"></div>
|
// by the #qr-modal-canvas img/canvas{width:100%} CSS rule -- the physical
|
||||||
<div style="flex:1">
|
// footprint doesn't change, but the extra resolution gives the browser's
|
||||||
<p class="muted" style="margin-top:0">Scan with Sipnetic (Add Account → Scan QR Code) to auto-fill this extension's SIP settings.</p>
|
// downscaling real anti-aliasing to work with instead of the blocky
|
||||||
<p class="muted">Contains this extension's password in plain text — treat the image like the password itself.</p>
|
// 1px-per-module edges qrcode.js draws natively. Verified with a headless
|
||||||
<code style="word-break:break-all; display:block; margin-top:var(--sp-1)">${esc(data.account_string)}</code>
|
// render + OpenCV decode: since the st= TURN field was added, the account
|
||||||
</div>
|
// string is long enough to need a denser QR (more modules in the same
|
||||||
</div>`;
|
// frame) that a real camera reads far more reliably at this resolution
|
||||||
box.style.display = "";
|
// than at 1:1. 152 = the 192px frame's content box after its 20px white
|
||||||
new QRCode(document.getElementById("sipnetic-qr-canvas"), {
|
// quiet-zone padding on each side (192 - 2*20) -- keep the *displayed*
|
||||||
text: data.account_string, width: 180, height: 180,
|
// size (via CSS, not this) in sync with #qr-modal-canvas if that changes.
|
||||||
|
text: data.account_string, width: 456, height: 456,
|
||||||
correctLevel: QRCode.CorrectLevel.M,
|
correctLevel: QRCode.CorrectLevel.M,
|
||||||
});
|
});
|
||||||
|
overlay.classList.add("show");
|
||||||
|
document.addEventListener("keydown", qrModalEscHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSipneticQr() {
|
||||||
|
const overlay = document.getElementById("qr-modal-overlay");
|
||||||
|
if (overlay) overlay.classList.remove("show");
|
||||||
|
document.removeEventListener("keydown", qrModalEscHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
function qrModalEscHandler(e) {
|
||||||
|
if (e.key === "Escape") closeSipneticQr();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setEaTransport(ext, connType) {
|
async function setEaTransport(ext, connType) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+9
@@ -134,6 +134,15 @@ mkdir -p "$CONFIG_DIR"
|
|||||||
|
|
||||||
# Determine TURN/STUN server address
|
# Determine TURN/STUN server address
|
||||||
turn_server="${TURN_SERVER:-${DOMAIN_NAME:-$local_ip}:${TURN_PORT:-3478}}"
|
turn_server="${TURN_SERVER:-${DOMAIN_NAME:-$local_ip}:${TURN_PORT:-3478}}"
|
||||||
|
# TURN_SERVER may be set (e.g. carried over in .env from an older install)
|
||||||
|
# to a bare host with no port — the fallback above only appends one when
|
||||||
|
# TURN_SERVER is unset/empty entirely. Append it here too so every client
|
||||||
|
# that reads this config (Sipnetic QR export, the web admin's own display)
|
||||||
|
# always gets a host:port it can actually dial, not a host that silently
|
||||||
|
# depends on the client guessing the default port.
|
||||||
|
if [[ -n "$TURN_SERVER" && "$TURN_SERVER" != *:* ]]; then
|
||||||
|
turn_server="${TURN_SERVER}:${TURN_PORT:-3478}"
|
||||||
|
fi
|
||||||
|
|
||||||
cat > "$CONFIG_FILE" << EOF
|
cat > "$CONFIG_FILE" << EOF
|
||||||
# Easy Asterisk Configuration (Docker) - $(date)
|
# Easy Asterisk Configuration (Docker) - $(date)
|
||||||
|
|||||||
Reference in New Issue
Block a user