Shared coturn (services/coturn.sh, ensure_coturn_user in lib/common.sh) is no
longer an installable or usable option anywhere in this repo. It's moved to
attic/coturn.sh (with tools/coturn-test-check.sh alongside it), which is
outside setup.sh's services/*.sh glob, so it never registers, never appears
in the menu, and `sudo ./setup.sh coturn` now fails with "unknown service".
Asterisk and Mattermost each already had an opt-out to run their own
dedicated coturn instead of the shared one; that opt-out is now the only
behavior — the shared-coturn preference, the opt-out prompt, and every
ensure_coturn_user() call site are gone. find_free_coturn_range()
(lib/common.sh) is what makes unconditional dedicated coturn safe: it scans
every coturn-owning service's own .env on the box for already-claimed relay
ranges and picks one that can't collide, so Asterisk + any number of
Mattermost instances can each run their own coturn on one box without the
relay-port collisions this repo's coturn history warns about.
Existing installs still pointed at a shared coturn container are left
running as-is on `update` (no silent migration attempt against a service
that no longer exists to heal against) — a full/fresh reinstall is the
migration path, which generates a new dedicated coturn with fresh
credentials and says so.
Also updates CLAUDE.md's coturn guidance for future service authors,
attic/README.md with the retirement rationale, and stale
services/coturn.sh path references in services/asterisk.sh,
tools/pstn-test-check.sh, README.md, and docs/vps-sizing-recommendations.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crt4ymNEHEbWqscB1qvZgC
Latest live run showed the test getting killed by its own `timeout 10`
before turnutils_uclient printed any result — just two startup INFO
lines, no error. That's the coturn/coturn Docker image's turnutils_uclient
(apparently a newer build with structured "LEVEL component: message"
logging, different from the older packaged version available for local
testing) taking longer than 10s to complete, not a real failure.
Bumped both scripts' timeout to 20s, and now check for timeout(1)'s own
exit code (124) separately from a real reported error — reported as WARN
with a suggested manual command to re-run with more time and see the full
result, instead of lumping "still running" in with "actually failed."
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
Not another guess: installed coturn locally (apt-get install coturn) and
ran the actual server + turnutils_uclient against it to verify this
before shipping, since the last two rounds shipped based on reading the
usage text alone and both turned out incomplete.
-e 127.0.0.1 satisfies turnutils_uclient's "-e or -y required" check, but
then fails allocation with "channel bind: error 403 (Forbidden IP)" —
services/coturn.sh never sets --allow-loopback-peers, so loopback as a
peer address is correctly rejected by a real coturn instance, and the
previous fix's own comment about "loopback is always reachable" missed
that reachable and permitted aren't the same thing.
-y ("client-to-client") sidesteps this: it negotiates both ends of a real
relay through the server itself, needs no separate peer address, and
works fine over loopback. Verified directly against a real local
instance: exits 0 with real packet-loss/RTT stats on valid credentials,
and correctly fails ("Cannot complete Allocation", exit 255) on a wrong
password — so it's still a meaningful pass/fail, not just "didn't crash."
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
Another real failure from a live run: turnutils_uclient refuses to run at
all without either -e <peer> or -y — a bare auth-only invocation isn't
enough for it to actually attempt anything. Add -e 127.0.0.1 to both
tools/pstn-test-check.sh's and tools/coturn-test-check.sh's invocations;
loopback is always reachable since the test already runs via `docker exec`
inside the coturn container itself, and it lets the test actually prove
data relays through the allocation, not just that auth succeeded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
Real bug caught from a live run: the coturn allocation test passed -t -T
(TCP/TLS) to turnutils_uclient, but services/coturn.sh always starts
coturn with --no-tls --no-dtls — requesting an encrypted/TCP transport
against a server that never offered one fails the allocation outright
("Cannot complete Allocation"), misreporting a config problem that didn't
exist. Dropped both flags in both tools/pstn-test-check.sh and
tools/coturn-test-check.sh so the test matches what the server actually
supports (plain UDP).
Also, from user feedback on the same run:
- warn()/fail() now collect their messages into arrays; the Summary
section prints a "Needs attention" recap of every FAIL/WARN together
at the end, instead of leaving the user to scroll back through a long
run to find what needs fixing.
- The softphone-setup block now offers to reprint itself one extension
at a time (paced with a keypress between each) after the main run, so
a long device list isn't lost in the scrollback either. Factored the
per-extension print into print_ext_info() so the full run and this
reprint can't drift apart. Guarded with `[ -t 0 ]` so it's skipped
automatically when the script isn't run interactively.
Verified via a fuller mock harness (fake docker/curl/systemctl/getent,
non-TTY stdin) that: the corrected turnutils_uclient invocation reports
success, the recap correctly lists FAIL before WARN, and the interactive
reprint prompt is skipped without hanging when stdin isn't a terminal.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
Two follow-ups on the PSTN health check:
- New "coturn (TURN relay for Asterisk)" section reads Asterisk's own
TURN_* values from its .env (not re-derived) and runs a live TURN
allocation against whichever coturn Asterisk is actually configured to
use — the shared instance, or its own embedded per-Asterisk coturn if
that's what this box has (detected via the same "grep -q '^ coturn:'
docker-compose.yml" check CLAUDE.md's migration guidance describes).
Proves what Asterisk itself would use at call time, complementing
tools/coturn-test-check.sh's broader multi-consumer check.
- New "Softphone setup" section parses pjsip.conf directly and prints
per-extension SIP server/username/password/port/transport, plus TURN
credentials for any extension with ice_support=yes — the same values
Sipnetic's "Add Account" screen needs, computed here so a client isn't
installed just to read them out of the Security Dashboard.
Also fixed a bug caught while building a mock test harness to verify both
additions: the extension-registration parser grabbed state via a fixed
field position ($3), silently truncating multi-word states like "Not in
use" down to "Not". Replaced with a regex that captures everything
between the extension and the trailing "N of inf" — verified against both
single- and multi-word states.
And a real syntax bug caught by bash -n before this ever shipped: an
apostrophe inside a ${VAR:-default} expansion ("this box's IP") opens an
unterminated single-quote context even inside double quotes — reworded
to avoid the apostrophe entirely rather than fight bash's parser.
Full mock run (fake docker/curl/systemctl/getent, real pjsip.conf/.env
fixtures matching the actual generated format) confirmed both new
sections and the registration fix all produce correct output end to end.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
Three follow-ups from live testing on this session's actual VPS:
- tools/pstn-test-check.sh's SMS section printed the Forward-to-URL value
to configure but never said what to do next — add the "text this DID,
then watch journalctl -u sms-inbound -f" step right after it.
- docs/pstn-sms-test-checklist.md: the "which box actually handled this"
question has a simple answer (a DID's inbound routing targets exactly
one IP:port, so there's no ambiguity to resolve, only a portal setting
to confirm) — written up so it doesn't need re-deriving. Also fixed the
--list example to cd into the repo first; ./setup.sh is a relative path
and silently fails with "command not found" from any other directory,
confirmed live in this session.
- New tools/coturn-test-check.sh: health-checks the shared coturn instance
(services/coturn.sh) and every consumer registered against it (Asterisk,
any number of Mattermost instances) — container/identity, each cached
consumer credential cross-checked against coturn's own live user
database (catches the container/volume-recreated-without-db drift case),
UFW rules for both the TURN port and the relay range, a capacity
explanation reasoned from the actual port-range math instead of a guess,
and a real TURN allocation test per consumer via turnutils_uclient —
the only way to prove credentials + port range + firewall all actually
work together, not just that each looks right in isolation. Deliberately
does not attempt a concurrent load test, since that would consume real
relay ports other services may be actively using.
Verified the turnadmin -l output parsing, UFW rule matching, and the
empty-array-under-set–u loop pattern against mock data before shipping.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
Server-side config was fully verifiable already; what wasn't is the
provider-account side (Anveo's authorized-IP list, DID routing, SMS
forward-URL) since that lives entirely outside this box. Rather than
leave "go check the portal" as a vague pointer, compute and print the
exact values each portal field needs to match: this box's public IP,
the trunk DID (from .pstn-trunk.env), and the SMS forward URL read
straight from /opt/sms-inbound/settings.env (SMS_FORWARD_URL) instead of
making the user reconstruct or hunt for a value the installer already
generated and stored. Anveo-specific field-by-field checklist when
PROVIDER_NAME matches; generic fallback otherwise.
Verified the .pstn-trunk.env / settings.env sourcing against mock files
matching the real generated format, including the literal $[from]$-style
Anveo placeholders in SMS_FORWARD_URL, which must survive `source` under
`set -u` without triggering bash's legacy $[...] arithmetic expansion —
same guard pattern services/pstn-trunk.sh's own update path already uses.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
docs/pstn-sms-test-checklist.md's manual steps (registration, trunk
reachability, dialplan contexts, kill-switch state, usage-alert timer
health, recent call/message activity) are all things a script can check
directly instead of re-typed by hand each time — and re-typing them is
exactly what led to the container-name mistake in the prior commit.
tools/pstn-test-check.sh auto-detects the container/directory the same
way the checklist doc now does, runs every automatable check, and prints
PASS/WARN/FAIL per item plus a summary. What it can't cover — actually
placing a call or sending a text — still needs the checklist doc.
Caught during testing against real command output pasted in this
session: the endpoint-parsing loop matched pjsip's own column-header
line ("<Endpoint/CID...> <State...>") as if it were a real endpoint row,
producing a bogus result. Fixed by skipping any row whose parsed
extension starts with "<".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn