Merge pull request #310 from outis1one/claude/ionos-script-integration-x32ofw

Add provider-portal checklist with real values to the PSTN health check
This commit is contained in:
Outis
2026-08-11 16:45:24 -04:00
committed by GitHub
3 changed files with 331 additions and 5 deletions
+23 -2
View File
@@ -12,6 +12,24 @@ Asterisk or PSTN trunk reinstall, or just periodically to catch drift (a
provider-side change, an expired international allow-list, a forgotten
kill-switch trip).
## How do I know a test call/text actually used THIS box?
Short answer: there's no ambiguity to resolve — a DID can only point at one
place. In the Anveo (or any IP-auth) portal, the DID's inbound routing
targets one specific IP:port (`$[E164]$@<this box's public IP>:5060`), and
the Outbound Trunk's Authorized IP Addresses list is what lets *this* box's
outbound calls out. If you have multiple boxes, only the one whose IP is
actually configured in the portal can send or receive on that DID at all —
there's nothing to "make sure" beyond confirming the portal points at this
box's current IP (§ Provider portal checklist output from
`tools/pstn-test-check.sh` prints it directly).
The practical way to *watch* it happen on this box specifically, live,
while you place the test: run `docker exec -it $CONTAINER asterisk -rvvv`
or `journalctl -u sms-inbound -f` in one terminal, then place the call/text
from another phone in real time. If it shows up here as it happens, this
box handled it — no separate confirmation needed.
## 0. Before you start
`$CONTAINER`/`$EA_DIR` only live in the shell session where you set them —
@@ -34,10 +52,13 @@ If `$CONTAINER` prints empty, the container isn't running at all — check
Re-run this block at the start of every new terminal session, not just
once — it's cheap and removes the whole class of failure above.
Check the three services this checklist covers are actually installed:
Check the three services this checklist covers are actually installed
run this from the repo directory itself (`~/ubuntu-post-install`, not
`~/docker/asterisk` or wherever you happen to be — `./setup.sh` is a
relative path and fails with "command not found" from anywhere else):
```bash
sudo ./setup.sh --list | grep -E "asterisk|pstn-trunk|sms-inbound|security-dashboard"
cd ~/ubuntu-post-install && sudo ./setup.sh --list | grep -E "asterisk|pstn-trunk|sms-inbound|security-dashboard"
```
Read your box's own current settings before testing — this file has your
+186
View File
@@ -0,0 +1,186 @@
#!/usr/bin/env bash
# tools/coturn-test-check.sh — Health-check for the shared coturn (TURN/STUN)
# instance services/coturn.sh sets up, and every consumer registered against
# it (Asterisk, one or more Mattermost instances, anything else added via
# ensure_coturn_user() in lib/common.sh).
#
# Checks: container up, identity/.env readable, every registered consumer
# actually exists in coturn's own user database (not just a cached
# users/<name>.env file — the two can drift, e.g. a container recreated from
# an older image/db), UFW has the TURN port + relay range open, and — the
# part nothing else in this repo does — a REAL TURN allocation test per
# consumer via turnutils_uclient (bundled in the coturn/coturn image), which
# is the only way to prove credentials + port range + firewall all actually
# work together end to end, not just that each piece looks right in isolation.
#
# Does NOT attempt a concurrent load test (e.g. opening dozens of allocations
# at once) — that would consume real relay ports on a server other services
# may be actively using. See "Capacity" in the output for how the port range
# bounds concurrent capacity, reasoned from the numbers instead of guessed at.
#
# Usage:
# sudo bash tools/coturn-test-check.sh
#
# Safe to run any time — the one allocation test per consumer opens and
# immediately releases a single relay port, the same as a single real call
# briefly would.
set -uo pipefail
PASS=0
WARN=0
FAIL=0
ok() { printf ' [OK] %s\n' "$1"; PASS=$((PASS + 1)); }
warn() { printf ' [WARN] %s\n' "$1"; WARN=$((WARN + 1)); }
fail() { printf ' [FAIL] %s\n' "$1"; FAIL=$((FAIL + 1)); }
section() { printf '\n== %s ==\n' "$1"; }
if [ "$(id -u)" -ne 0 ]; then
echo "Run with sudo — needs docker exec." >&2
exec sudo bash "$0" "$@"
fi
ACTUAL_USER="${SUDO_USER:-${USER:-root}}"
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "/root")"
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
COTURN_DIR="$DOCKER_DIR/coturn"
# ── Container + identity ──────────────────────────────────────────────────────
section "Detecting install"
if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -qx coturn; then
fail "No running 'coturn' container found — is services/coturn.sh installed and started?"
echo ""
echo " $PASS passed, $WARN warnings, $FAIL failed. Stopping."
exit 1
fi
ok "Container running: coturn"
if [ ! -f "$COTURN_DIR/.env" ]; then
fail "$COTURN_DIR/.env not found — can't read realm/host/port range."
exit 1
fi
set +u
# shellcheck disable=SC1090
source "$COTURN_DIR/.env"
set -u
COTURN_REALM="${COTURN_REALM:-}"
COTURN_HOST="${COTURN_HOST:-}"
COTURN_PORT="${COTURN_PORT:-3478}"
COTURN_MIN_PORT="${COTURN_MIN_PORT:-49152}"
COTURN_MAX_PORT="${COTURN_MAX_PORT:-49452}"
ok "Realm: ${COTURN_REALM:-<unset>} Host: ${COTURN_HOST:-<unset>} Port: $COTURN_PORT"
ok "Relay port range: ${COTURN_MIN_PORT}-${COTURN_MAX_PORT}"
# ── Registered consumers ──────────────────────────────────────────────────────
section "Registered consumers"
DB_USERS="$(docker exec coturn turnadmin -l -b /var/lib/coturn/turndb 2>/dev/null | sed -E 's/\[.*//' | awk 'NF' | sort -u)"
if [ -z "$DB_USERS" ]; then
warn "No users found in coturn's own database — nothing has actually registered yet, or turnadmin -l's output format changed. Raw:"
docker exec coturn turnadmin -l -b /var/lib/coturn/turndb 2>&1 | sed 's/^/ /'
fi
CACHED_CONSUMERS=()
if [ -d "$COTURN_DIR/users" ]; then
while IFS= read -r f; do
CACHED_CONSUMERS+=("$(basename "$f" .env)")
done < <(find "$COTURN_DIR/users" -maxdepth 1 -name '*.env' -type f 2>/dev/null | sort)
fi
if [ "${#CACHED_CONSUMERS[@]}" -eq 0 ]; then
warn "No cached consumer credentials in $COTURN_DIR/users — nothing has registered via ensure_coturn_user() yet."
else
for c in "${CACHED_CONSUMERS[@]}"; do
if grep -qx "$c" <<< "$DB_USERS"; then
ok "Consumer '$c' — cached credentials present AND found in coturn's live database"
else
fail "Consumer '$c' has a cached users/${c}.env but is NOT in coturn's database — its calls will fail 401 Unauthorized. Likely cause: the coturn container/volume was recreated without preserving ./db. Fix: sudo docker exec coturn turnadmin -a -u $c -p <password from users/${c}.env> -r $COTURN_REALM -b /var/lib/coturn/turndb"
fi
done
fi
# Flag anything in the live DB with no cached file too — orphaned/manually
# added users aren't wrong, just worth knowing about.
while IFS= read -r u; do
[ -z "$u" ] && continue
found=false
for c in "${CACHED_CONSUMERS[@]:-}"; do [ "$c" = "$u" ] && found=true && break; done
[ "$found" = false ] && warn "Database has user '$u' with no matching users/${u}.env — added manually, or a leftover from a removed service."
done <<< "$DB_USERS"
# ── Firewall ───────────────────────────────────────────────────────────────────
section "Firewall (UFW)"
if command -v ufw &>/dev/null; then
UFW_STATUS="$(ufw status 2>/dev/null)"
if grep -qE "^${COTURN_PORT}(/udp|/tcp)?\b.*ALLOW" <<< "$UFW_STATUS"; then
ok "TURN listening port ${COTURN_PORT} allowed"
else
fail "TURN listening port ${COTURN_PORT} not found in 'ufw status' — clients may not reach it"
fi
if grep -qE "^${COTURN_MIN_PORT}:${COTURN_MAX_PORT}/udp\b.*ALLOW" <<< "$UFW_STATUS"; then
ok "Relay port range ${COTURN_MIN_PORT}-${COTURN_MAX_PORT}/udp allowed"
else
fail "Relay port range ${COTURN_MIN_PORT}-${COTURN_MAX_PORT}/udp not found in 'ufw status' — allocated relay ports would be unreachable, breaking media even after a successful TURN allocation"
fi
else
warn "ufw not installed — can't confirm the relay range is actually open (may be fine if this box has no firewall, or one outside UFW)"
fi
# ── Capacity ───────────────────────────────────────────────────────────────────
section "Capacity"
RANGE_SIZE=$((COTURN_MAX_PORT - COTURN_MIN_PORT + 1))
CONSUMER_COUNT="${#CACHED_CONSUMERS[@]}"
echo " Relay range holds ${RANGE_SIZE} ports. Each concurrent relayed call/leg typically"
echo " uses one allocation (roughly one port) for its lifetime — released when the call"
echo " ends, not held permanently. With ${CONSUMER_COUNT} registered consumer(s), the range"
echo " would need all of them to have ~$((RANGE_SIZE / (CONSUMER_COUNT > 0 ? CONSUMER_COUNT : 1))) simultaneous relayed calls each, at the same"
echo " moment, before it runs out — for Asterisk + a handful of Mattermost instances at"
echo " personal/small-team scale, that ceiling is not realistically reachable in normal"
echo " use. If you ever DO expect that much simultaneous WebRTC/SIP relay traffic, raise"
echo " COTURN_MIN_PORT/COTURN_MAX_PORT in $COTURN_DIR/.env, update the matching UFW rule,"
echo " and restart coturn — no consumer reconfiguration needed, they don't cache the range."
echo ""
echo " Note: not every call needs a TURN relay at all — TURN is the FALLBACK when two"
echo " peers can't reach each other directly (STUN/ICE finds a direct path first when"
echo " possible). Real relay usage is usually well below \"every concurrent call.\""
# ── Real allocation test per consumer ─────────────────────────────────────────
section "Live allocation test (one real TURN allocation per registered consumer)"
if ! docker exec coturn which turnutils_uclient &>/dev/null; then
warn "turnutils_uclient not found in the coturn image — skipping live allocation tests."
else
TEST_HOST="${COTURN_HOST:-127.0.0.1}"
for c in "${CACHED_CONSUMERS[@]:-}"; do
[ -z "$c" ] && continue
_u="$(grep '^COTURN_USER=' "$COTURN_DIR/users/${c}.env" 2>/dev/null | cut -d= -f2-)"
_p="$(grep '^COTURN_PASS=' "$COTURN_DIR/users/${c}.env" 2>/dev/null | cut -d= -f2-)"
if [ -z "$_u" ] || [ -z "$_p" ]; then
warn "$c: couldn't read cached credentials, skipping live test"
continue
fi
OUT="$(docker exec coturn timeout 10 turnutils_uclient -t -T -u "$_u" -w "$_p" "$TEST_HOST" -p "$COTURN_PORT" 2>&1)"
RC=$?
if [ "$RC" -eq 0 ]; then
ok "$c: TURN allocation succeeded (credentials + relay range + reachability all confirmed working)"
else
fail "$c: TURN allocation failed (exit $RC) — raw output:"
echo "$OUT" | tail -n 15 | sed 's/^/ /'
fi
done
fi
# ── Summary ───────────────────────────────────────────────────────────────────
section "Summary"
echo " $PASS passed, $WARN warnings, $FAIL failed."
echo ""
echo " A passing allocation test here proves TURN works end to end for that consumer."
echo " It does NOT by itself prove Asterisk or Mattermost are actually configured to USE"
echo " it — check each service's own .env for TURN_HOST/TURN_USERNAME (asterisk.sh) or"
echo " the Calls plugin's ICE Servers Configurations (mattermost.sh) matches what's"
echo " printed above, then place a real call from outside the LAN (the case TURN"
echo " actually exists for — two peers on the same LAN usually connect directly and never"
echo " touch the relay at all, so a same-LAN test call proves nothing about TURN)."
[ "$FAIL" -eq 0 ]
+122 -3
View File
@@ -60,6 +60,10 @@ ok "Directory: $EA_DIR"
ASTERISK_DIR="$EA_DIR/config/asterisk"
LOGS_DIR="$EA_DIR/logs"
# Fetched once, reused by both the softphone-setup and provider-checklist
# sections below.
PUBLIC_IP="$(curl -4 -s --max-time 5 ifconfig.me 2>/dev/null || true)"
# ── Registration ─────────────────────────────────────────────────────────────
section "Extension registration"
@@ -69,9 +73,12 @@ if [ -z "$ENDPOINTS_OUT" ]; then
else
# Endpoint lines look like " Endpoint: 101/101 Unavailable 0 of inf" —
# skip the trunk itself (checked separately below) and the header/legend.
# State is captured with a regex, not a fixed field number: it's one or
# more words ("Unavailable", but also "Not in use" — a single $3 field
# grab truncated that to just "Not").
while IFS= read -r line; do
ext="$(awk '{print $2}' <<< "$line" | cut -d/ -f1)"
state="$(awk '{print $3}' <<< "$line")"
state="$(sed -E 's/^ Endpoint:[[:space:]]+[^[:space:]]+[[:space:]]+(.*[^[:space:]])[[:space:]]+[0-9]+ of inf[[:space:]]*$/\1/' <<< "$line")"
# Skip the column-header/legend line ("<Endpoint/CID...> <State...>")
# printed once at the top of real output — it matches the same
# "^ Endpoint:" grep as an actual endpoint row.
@@ -139,6 +146,57 @@ else
fi
fi
# ── coturn (TURN) — used for remote/NAT'd extensions' media relay, and by
# Anveo-style ICE-enabled endpoints. Asterisk caches its OWN TURN_* values
# in its .env at the point it was configured — testing with those (not
# re-deriving fresh credentials) proves what Asterisk is actually set up
# to use, not just that the shared coturn instance works in general (that
# broader, multi-consumer check is tools/coturn-test-check.sh's job). ─────────
section "coturn (TURN relay for Asterisk)"
ASTERISK_ENV="$EA_DIR/.env"
TURN_SERVER="" TURN_USERNAME="" TURN_PASSWORD="" TURN_PORT=""
if [ -f "$ASTERISK_ENV" ]; then
set +u
# shellcheck disable=SC1090
source "$ASTERISK_ENV"
set -u
TURN_SERVER="${TURN_SERVER:-}"
TURN_USERNAME="${TURN_USERNAME:-}"
TURN_PASSWORD="${TURN_PASSWORD:-}"
TURN_PORT="${TURN_PORT:-}"
fi
if [ -z "$TURN_SERVER" ]; then
warn "Asterisk has no TURN configured — fine for LAN-only extensions, but a phone on"
warn "mobile data or behind restrictive NAT may get one-way or no audio without it."
warn "Add it via: sudo ./setup.sh asterisk (update mode)"
else
if grep -q '^ coturn:' "$EA_DIR/docker-compose.yml" 2>/dev/null; then
COTURN_CONTAINER="easy-asterisk-coturn"
[[ "$CONTAINER" == *-do ]] && COTURN_CONTAINER="easy-asterisk-do-coturn"
ok "Using an embedded, per-Asterisk coturn ($COTURN_CONTAINER) — not the shared"
ok "instance, so tools/coturn-test-check.sh won't see this one; tested separately below."
else
COTURN_CONTAINER="coturn"
ok "Using the shared coturn instance (also covered by tools/coturn-test-check.sh)"
fi
if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$COTURN_CONTAINER"; then
fail "Container '$COTURN_CONTAINER' not running — Asterisk's TURN config points at it but it's down"
elif ! docker exec "$COTURN_CONTAINER" which turnutils_uclient &>/dev/null; then
warn "turnutils_uclient not found in $COTURN_CONTAINER — skipping live allocation test"
else
OUT="$(docker exec "$COTURN_CONTAINER" timeout 10 turnutils_uclient -t -T -u "$TURN_USERNAME" -w "$TURN_PASSWORD" 127.0.0.1 -p "${TURN_PORT:-3478}" 2>&1)"
if [ $? -eq 0 ]; then
ok "Live TURN allocation succeeded with Asterisk's own configured credentials (user '$TURN_USERNAME')"
else
fail "Live TURN allocation FAILED with Asterisk's configured credentials — raw output:"
echo "$OUT" | tail -n 15 | sed 's/^/ /'
fi
fi
fi
# ── SMS inbound ───────────────────────────────────────────────────────────────
section "SMS inbound"
if systemctl list-unit-files sms-inbound.service &>/dev/null; then
@@ -163,6 +221,63 @@ for log in pstn-trunk-calls.log sip-messages.log; do
fi
done
# ── Softphone setup (Sipnetic or any SIP client) ──────────────────────────────
# Same data the Security Dashboard's per-extension "info" panel shows
# (showEaDeviceDetails in services/security-dashboard.sh), read directly from
# pjsip.conf here so this is useful even without the dashboard installed.
# Passwords are read from the live config on this box, not regenerated —
# printing them is exactly as sensitive as the dashboard's own info panel.
section "Softphone setup — one block per extension (password shown, handle accordingly)"
DOMAIN_NAME=""
[ -f "$EA_DIR/.env" ] && DOMAIN_NAME="$(grep -E '^DOMAIN_NAME=' "$EA_DIR/.env" | cut -d= -f2-)"
SIP_SERVER="${DOMAIN_NAME:-${PUBLIC_IP:-<could not auto-detect this box IP>}}"
PJSIP_CONF="$ASTERISK_DIR/pjsip.conf"
if [ ! -f "$PJSIP_CONF" ]; then
warn "pjsip.conf not found at $PJSIP_CONF — can't print softphone settings"
else
DEVICE_INFO="$(awk '
/^\[[0-9]+\]$/ { ext = substr($0, 2, length($0)-2); cur_type=""; next }
/^type=endpoint/ { cur_type="endpoint"; next }
/^type=auth/ { cur_type="auth"; next }
/^type=aor/ { cur_type="aor"; next }
cur_type=="endpoint" && /^transport=/ { split($0,a,"="); transport[ext]=a[2] }
cur_type=="endpoint" && /^ice_support=yes/ { ice[ext]="yes" }
cur_type=="auth" && /^password=/ { split($0,a,"="); pass[ext]=a[2] }
END {
for (e in pass) printf "%s|%s|%s|%s\n", e, pass[e], transport[e], (ice[e] ? ice[e] : "no")
}
' "$PJSIP_CONF" | sort)"
if [ -z "$DEVICE_INFO" ]; then
warn "No devices found in pjsip.conf"
else
while IFS='|' read -r ext pass transport ice; do
[ -z "$ext" ] && continue
if [ "$transport" = "transport-tls" ]; then
port=5061; proto="tls"
else
port=5060; proto="udp"
fi
echo " Extension $ext:"
echo " SIP server: $SIP_SERVER"
echo " Username: $ext"
echo " Password: $pass"
echo " Port: $port"
echo " Transport: $proto"
if [ "$ice" = "yes" ] && [ -n "$TURN_SERVER" ]; then
echo " TURN server: $TURN_SERVER"
echo " TURN user: $TURN_USERNAME"
echo " TURN pass: $TURN_PASSWORD"
fi
echo ""
done <<< "$DEVICE_INFO"
ok "Printed setup info for $(wc -l <<< "$DEVICE_INFO") extension(s) — same values Sipnetic's"
ok "'Add Account' screen (or the dashboard's QR code / Download settings) needs"
fi
fi
# ── Provider portal checklist ─────────────────────────────────────────────────
# Everything above is server-side and this script's own checks; the provider
# account/portal side (authorized IPs, DID routing, the SMS forward URL) is
@@ -183,8 +298,6 @@ if [ -f "$PSTN_ENV" ]; then
PROVIDER_NAME="${PROVIDER_NAME:-}"
fi
PUBLIC_IP="$(curl -4 -s --max-time 5 ifconfig.me 2>/dev/null || true)"
if [ -n "$TRUNK_DID" ]; then
echo " This box's DID: $TRUNK_DID"
else
@@ -232,6 +345,12 @@ if [ -f "$SMS_SETTINGS" ]; then
echo " ${SMS_FORWARD_URL}"
echo " (paste exactly as shown — press SAVE not RETURN on Anveo's SMS tab, then"
echo " reopen it to confirm the whole string came back, it's long)"
echo ""
echo " Once that's saved: text ${TRUNK_DID:-this DID} from any OTHER phone (not a"
echo " softphone registered to this Asterisk — an outside cell number), then watch:"
echo " journalctl -u sms-inbound -f"
echo " It should land in Sipnetic (or whichever softphone owns that DID/extension)"
echo " within a few seconds. See docs/pstn-sms-test-checklist.md §10 if it doesn't."
else
echo " sms-inbound is installed but no SMS_FORWARD_URL found in $SMS_SETTINGS"
echo " — re-run: sudo ./setup.sh sms-inbound"