Add voipms-trunk service: US-only outbound PSTN, max 3 concurrent calls

Adds a VoIP.ms SIP trunk on top of asterisk-digital-ocean: IP-authenticated
trunk (no password stored), NANP-only outbound dialplan, a global 3-call
concurrent cap via GROUP()/GROUP_COUNT(), and inbound routing to one
extension. Config lives in its own include files rather than being
appended directly to pjsip.conf/extensions.conf, since Easy Asterisk fully
regenerates both from its own internal state — the includes are patched
into the vendor's generator functions so they survive that regeneration.

Wires the new service into setup.sh's is_installed() and README's services
table, and updates docs/pstn-calling-voipms-plan.md to reflect what's now
implemented vs. still open (spend/volume alerting, live-account
verification).
This commit is contained in:
Claude
2026-07-21 23:13:59 +00:00
parent 719f9117be
commit 2ac2982e38
4 changed files with 445 additions and 38 deletions
+53 -37
View File
@@ -1,9 +1,15 @@
# PSTN Calling via VoIP.ms — Planning Notes
Research and decisions from a design discussion, saved here so the work can
be picked up in a fresh chat without re-deriving the background. **Nothing
has been implemented yet** — this is prep for a future `services/*.sh`
addition on top of `asterisk-digital-ocean`.
be picked up in a fresh chat without re-deriving the background.
**Implemented** — see `services/voipms-trunk.sh` (run `sudo ./setup.sh
voipms-trunk` after `asterisk-digital-ocean` is installed). IP-authenticated
trunk, US/NANP-only outbound dialplan, 3-concurrent-call cap, inbound to one
extension. That file's own header comment explains how it survives Easy
Asterisk's config regeneration (an architectural wrinkle discovered while
implementing this — worth reading before touching either file). The
outbound spend/volume alert mentioned below is still not implemented.
## Decision so far
- **Provider: VoIP.ms.** Chosen for its prepaid-balance model: turn off
@@ -67,33 +73,38 @@ parallel and draining the whole balance (e.g. $15 balance ÷ $0.01/min =
1,500 minutes total, which 20 concurrent legs could burn through in under
an hour). The prepaid-balance-off-auto-recharge layer bounds the *dollar*
ceiling; only a concurrent-call cap bounds the *speed* of a breach. Treat
the per-extension concurrent-call cap and spend/volume alert below as
required before funding a live trunk, not optional hardening.
the concurrent-call cap and spend/volume alert below as required before
funding a live trunk, not optional hardening.
**Implemented:** the concurrent-call cap in `services/voipms-trunk.sh` is a
*global* cap (max 3 outbound legs total via the trunk, via
`GROUP()`/`GROUP_COUNT()` in the dialplan, shared across all extensions) —
not per-extension. That was the explicit ask when this got built; a
per-extension cap layered on top is still a possible future refinement, not
done. The spend/volume alert is still not implemented.
## What it takes technically (asterisk-digital-ocean)
- A PJSIP trunk to VoIP.ms: `endpoint` / `aor` / `auth` / `identify`
sections in the pjsip config, using either IP authentication or SIP
registration — VoIP.ms supports both. IP auth is simpler for a droplet
(it has a static IP already) and avoids storing a SIP password in the
config at all — worth confirming with VoIP.ms which they actually
recommend before choosing.
- An outbound dialplan route matching US numbers only, e.g. `_1NXXNXXXXX`
(11-digit NANP with leading 1) or `_NXXNXXXXX`, depending on how numbers
get dialed from the existing extensions, routed to the VoIP.ms trunk. No
catch-all `_X.` pattern — an explicit NANP pattern is itself a hard block
on non-US destinations at the dialplan level.
- A PJSIP trunk to VoIP.ms: `endpoint` / `aor` / `identify` sections in the
pjsip config. **Implemented with IP authentication** (no `auth` section,
no SIP password stored anywhere) — see `services/voipms-trunk.sh`.
- An outbound dialplan route matching US numbers only — **implemented**:
`_1NXXNXXXXX` (11-digit NANP with leading 1) and `_NXXNXXXXX` (10-digit,
auto-prefixed with 1), both routed to the VoIP.ms trunk. No catch-all
`_X.` pattern.
- VoIP.ms-specific setup that isn't scriptable (user does this manually):
create the account, order a DID (inbound is wanted — see above), decide
pay-per-minute vs. unlimited DID plan and whether to add E911, pick a
VoIP.ms POP/server (affects the trunk hostname), fund the prepaid balance
($15 minimum), turn off auto-recharge.
- Defense-in-depth to design alongside the trunk (not yet designed):
- Per-extension concurrent-call cap in the dialplan (`GROUP()` /
`GROUP_COUNT()`) so one compromised extension can't open dozens of
simultaneous outbound legs at once.
- A simple outbound call-count/spend alert — could live in the existing
`security-dashboard` service (see `services/security-dashboard.sh`) or
as a separate CDR-based check. Not designed yet.
($15 minimum), turn off auto-recharge. `services/voipms-trunk.sh` prompts
for the POP hostname, DID, and a ring extension for inbound at install time.
- Defense-in-depth alongside the trunk:
- **Implemented:** a global concurrent-call cap in the dialplan
(`GROUP()`/`GROUP_COUNT()`, max 3 outbound legs via the trunk at once)
so a compromised extension can't open dozens of simultaneous outbound
legs. Global, not per-extension — see the note above.
- **Not implemented:** a simple outbound call-count/spend alert — could
live in the existing `security-dashboard` service (see
`services/security-dashboard.sh`) or as a separate CDR-based check.
- Worth being explicit that CrowdSec's existing `asterisk_bf` /
`asterisk_user_enum` scenarios (see `services/crowdsec.sh`) cover
registration brute-force, which is a *different* threat model from a
@@ -111,16 +122,21 @@ required before funding a live trunk, not optional hardening.
across the space, VoIP.ms included.
## Open items for whoever picks this up next
1. Decide: new `services/voipms-trunk.sh`, or an optional trunk section
added directly to `services/asterisk-digital-ocean.sh`? Leaning toward a
separate service file so trunk config isn't forced on installs that
don't want PSTN calling, matching this repo's one-feature-per-file
convention (see CLAUDE.md).
2. IP auth vs. registration — confirm which VoIP.ms recommends for a single
fixed-IP droplet.
3. Exact NANP dial pattern(s) and any prefix-stripping VoIP.ms requires.
4. Inbound is decided (wanted) — still need to pick pay-per-minute vs.
unlimited DID plan based on real expected volume, and decide on E911.
5. Design the concurrent-call cap and any spend/volume alerting mentioned
above — treat as required before funding a live trunk, not optional
(see toll-fraud nuance above: NANP-only bounds cost/min, not burn speed).
1. ~~Decide: new `services/voipms-trunk.sh`...~~ Done — separate service file.
2. ~~IP auth vs. registration~~ Done — IP authentication, no password stored.
3. ~~Exact NANP dial pattern(s)~~ Done — `_1NXXNXXXXX` / `_NXXNXXXXX`.
4. ~~Inbound~~ Done — rings one extension, prompted at install time. Still
unresolved: pick pay-per-minute vs. unlimited DID plan on VoIP.ms's side
based on real expected volume, and decide on E911 (see cost estimate).
5. ~~Concurrent-call cap~~ Done — global 3-call cap. Still not implemented:
the spend/volume alert (security-dashboard integration or CDR-based
check) — treat as still-required before fully trusting this against a
sustained breach, not optional (see toll-fraud nuance above: the NANP
restriction + call cap bound cost/min and burn speed, but nothing here
yet notices a breach in progress or alerts on unusual volume).
6. Verify against a live VoIP.ms account: auto-recharge-off behavior at
sign-up, and that the chosen POP server's actual source IP for inbound
calls matches what `services/voipms-trunk.sh` resolved via DNS at install
time (VoIP.ms's docs mention some redundancy/failover between servers —
if inbound calls ever stop matching the `identify` section, this is the
first thing to check).