Files
ubuntu-post-install/docs/pstn-sms-test-checklist.md
Claude 5c18cfa364 Add SMS test reminder, "which box handled it" note, and a coturn health check
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
2026-08-11 20:32:02 +00:00

12 KiB

PSTN + DID + SMS — end-to-end test checklist

For verifying an already-installed Asterisk + PSTN trunk + SMS setup (droplet or home/LAN) actually works, top to bottom. Not a setup guide — see docs/anveo-direct-setup-guide.md for account/droplet setup from scratch, and docs/pstn-calling-voipms-plan.md for the design/architecture background. This doc assumes both are already done and just walks through proving it all still works, in order, with what to check when a step fails.

Run through this after any of: first go-live, a provider/DID change, an 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 — a new terminal, a reboot, or just enough time passing between test steps leaves them empty again, and an empty $CONTAINER silently turns docker exec -it $CONTAINER asterisk -rx ... into docker exec -it asterisk -rx ..., which fails with "No such container: asterisk" (there's no container literally named that — it's easy-asterisk or easy-asterisk-do). Auto-detect instead of typing it by hand, so a stale or forgotten variable can't silently break every command below:

CONTAINER="$(docker ps --format '{{.Names}}' | grep -m1 -E '^easy-asterisk(-do)?$')"
EA_DIR=~/docker/asterisk; [ -d ~/docker/asterisk-digital-ocean ] && EA_DIR=~/docker/asterisk-digital-ocean
echo "Container: $CONTAINER   Dir: $EA_DIR"

If $CONTAINER prints empty, the container isn't running at all — check docker compose -f $EA_DIR/docker-compose.yml ps before anything else. 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 — 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):

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 real DID, tiers reference, ring-group, and rate, regenerated on every pstn-trunk install/update:

cat $EA_DIR/README-pstn-trunk.md

1. Registration sanity check

Every extension you're about to test with needs to actually be registered before anything else will work.

docker exec -it $CONTAINER asterisk -rx "pjsip show endpoints"

Each extension you plan to test should show Avail (registered), not Unavail. If a softphone shows registered in its own UI but Asterisk disagrees, the extension's transport (UDP vs TLS) is the usual culprit — check it via the Security Dashboard's Extensions tab button on that row before going further, not by re-entering credentials.

2. Trunk itself

docker exec -it $CONTAINER asterisk -rx "pjsip show endpoint pstn-trunk"

Look for Contact state, not just that the endpoint exists. An IP-authenticated trunk has no registration to show — what matters here is that Asterisk considers the endpoint reachable, and that the provider's own portal doesn't report the account balance at $0 or the DID unassigned.

3. Permission tiers — set before testing calls

Every extension starts at internal (no PSTN at all) until granted a tier. Before testing outbound/inbound, set at least one extension to full via the Security Dashboard's Extensions tab (or restricted with an approved number, if that's what you want to test). Press "Commit Changes (Restart Asterisk)" after — tier/permission edits are written to disk immediately but the dialplan has been observed not picking them up until a full container restart, not just a reload (see "Dashboard changes may need Commit Changes" in docs/anveo-direct-setup-guide.md). Skipping this step is the single most common reason a "correctly configured" test call still fails.

4. Outbound call test

From a full-tier extension's softphone, dial a real US number as 1 + area code + number (11 digits — the dialplan matches the full NANP pattern, not a bare 10-digit number).

Watch the live console while dialing:

docker exec -it $CONTAINER asterisk -rvvv

Expected: the call connects, and a line appears in the call log:

tail -f $EA_DIR/logs/pstn-trunk-calls.log

If it fails, check in this order (matches the actual bugs this system has hit — see the "Bugs hit and fixed" section of docs/anveo-direct-setup-guide.md):

  • restricted/internal tier dialing a number not on its approved list — expected to fail with a busy signal, not a bug.
  • Concurrency cap already hit — check pstn-limits.conf or the dashboard's displayed caps.
  • Kill-switch tripped (see §7 below) — every PSTN call, in or out, blocked regardless of tier.
  • dialplan show intercom and count digits by hand against what you dialed — a pattern-length mismatch has bitten this exact system before.

5. Inbound call test — shared DID / ring-group

From any outside phone, dial the trunk's main DID (shown in README-pstn-trunk.md). Every full/restricted-tier member of the configured ring-group should ring simultaneously for ~20 seconds unless a personal DID routes it elsewhere (see §6).

docker exec -it $CONTAINER asterisk -rx "dialplan show from-pstn-trunk"

If nothing rings: confirm the calling number's actual Caller-ID is reaching Asterisk correctly — a bare Hangup()/no ring with no ntfy denial alert either usually means the inbound context isn't loading at all (check asterisk -rx "dialplan show from-pstn-trunk" returns content, not "No such context"). A Busy(15) + ntfy denial alert means it loaded fine and the caller's number just isn't on a restricted-tier member's approved list — expected behavior, not a bug.

6. Inbound call test — personal DID

If you've assigned a personal DID to a specific extension (Security Dashboard → PSTN Trunk tab → Personal numbers), dial that number instead of the shared trunk DID. Only the owning extension (or every current member, if assigned to a Group) should ring — no ring-group fallback. Confirm the owner's outbound Caller-ID also shows this DID instead of the shared trunk DID when they place a call out.

7. Spend-cap kill-switch (only if enabled)

Don't trigger this for real — it force-hangs-up active calls and blocks all PSTN traffic until manually cleared. To confirm it's wired up without tripping it:

cat $EA_DIR/config/asterisk/pstn-trunk-killswitch.conf
systemctl status pstn-trunk-usage.timer 2>/dev/null || crontab -l | grep pstn-trunk-usage

The timer/cron entry should be active and recent (journalctl -u pstn-trunk-usage.service -n 20 shows it actually ran within the last minute). If you do want to confirm the block itself works, lower MAX_MONTHLY_SPEND temporarily via sudo ./setup.sh pstn-trunk (update mode) to something you've already exceeded, confirm calling is blocked with the priority=urgent ntfy alert, then re-run and clear it (this is CLI-only by design — not on the dashboard).

8. International calling (only if you've allowed any countries)

cat $EA_DIR/config/asterisk/pstn-intl-allowed.conf

Only full-tier extensions can dial these regardless of the allow-list. Dial 011 + country code + number. If an expiry was set, confirm the allow-list actually clears itself after that date (the periodic usage-alert script does this, not a separate cron job) — worth checking once near the expiry date rather than assuming.

9. Internal SIP messaging (no PSTN/SMS involved)

From one extension enabled for messaging=yes (Security Dashboard → Extensions tab → Messaging column) to another, send a native SIP message in Sipnetic (or whichever softphone). Should land instantly, no carrier involved. Check the log if it doesn't arrive:

tail -f $EA_DIR/logs/sip-messages.log

An extension with messaging=no should have its message silently denied (fails closed) — confirm this too, not just the success path.

10. SMS inbound test

Prerequisite: sudo ./setup.sh sms-inbound already run, and the Anveo portal's DID → SMS tab → "Forward to URL" configured with the exact string the installer printed (see docs/anveo-direct-setup-guide.md §8 if this hasn't been done yet).

Text the DID's number from any outside phone. Expected: it lands in Sipnetic's message thread for whichever extension/group owns that DID within a few seconds — not a push notification, a real message in the softphone's own thread.

journalctl -u sms-inbound -f

Shows sender, recipient, message length, and the AMI exchange — never the message body itself. If nothing arrives:

  • Confirm the DID is a mobile-class number if you're testing a 2FA/verification code specifically — geographic DIDs get silently rejected by many services before a message is ever sent (see the setup guide's "Pick the right kind of number" section).
  • Confirm short-code SMS is actually enabled on this specific DID (not every number in the pool has it by default).
  • Check pstn-personal-dids.conf/pstn-groups.conf actually has an entry for the DID you texted — an unowned DID has nowhere to route to.

Outbound SMS from this box isn't a working path yet (see the setup guide's "What about sending?" section) — don't test for it.

11. Dashboard cross-check

Open the Security Dashboard's Calls & Texts tab and confirm the calls and messages from the tests above actually show up there — this is reading the same log files tested above, so an empty tab after a successful test call usually means the dashboard's ASTERISK_LOG_DIR/log paths have drifted from where Asterisk is actually writing, not that logging is broken. Also check the Extensions tab shows correct live status (online/offline) for whichever device you tested with.

Quick reference — verification commands

docker exec -it $CONTAINER asterisk -rx "pjsip show endpoints"
docker exec -it $CONTAINER asterisk -rx "pjsip show endpoint pstn-trunk"
docker exec -it $CONTAINER asterisk -rx "dialplan show intercom"
docker exec -it $CONTAINER asterisk -rx "dialplan show from-pstn-trunk"
tail -f $EA_DIR/logs/pstn-trunk-calls.log
tail -f $EA_DIR/logs/sip-messages.log
journalctl -u sms-inbound -f          # SMS via Anveo's HTTP webhook — the path §10 actually tests
tail -f $EA_DIR/logs/pstn-sms.log     # SMS-over-SIP dialplan path — confirmed NOT offered by Anveo;
                                       # only relevant if a future provider delivers texts over SIP
journalctl -u pstn-trunk-usage.service -n 20