Block non-US NANP area codes - NANP != US, a real gap the design carried

Anveo Direct's "Call Security" rate-cap feature (found while evaluating
their account portal) prompted a closer look at our own NANP dialplan
pattern, which turned out to have a real gap: NANP covers Caribbean/
Atlantic nations and several US territories, not just the US - these dial
exactly like a normal 10-digit US number but bill at international/
premium rates, a well-known toll-fraud/"one-ring scam" vector precisely
because the format looks domestic. The existing _1NXXNXXXXX/_NXXNXXXXX
patterns matched all of them equally, meaning any extension (including
"full" tier) could reach premium Caribbean/territory numbers despite the
whole point of the design being "US only, no premium-rate destinations."

Verified the area code list against multiple sources rather than relying
on training-data recall alone, then added an explicit block on all 27
(Bahamas, Barbados, Anguilla, Antigua & Barbuda, British Virgin Islands,
US Virgin Islands, Cayman Islands, Bermuda, Grenada, Turks & Caicos,
Jamaica, Montserrat, Northern Mariana Islands, Guam, American Samoa, Sint
Maarten, Saint Lucia, Dominica, Saint Vincent, Puerto Rico, Dominican
Republic, Trinidad & Tobago, Saint Kitts & Nevis) - checked via REGEX()
against the extracted area code, before permission-tier checks, so it
applies regardless of tier. Tested the extraction/matching logic against
both blocked and legitimate US area codes.

Updated docs/pstn-calling-voipms-plan.md's toll-fraud reasoning, which had
previously (incorrectly) claimed NANP-only restriction alone fully closed
the cost-per-minute risk - noted as a real, live gap rather than glossing
over it. Also noted Anveo Direct's provider-side Call Security setting
(rate cap + max call duration) as a complementary backstop worth
configuring on their account portal directly, independent of this repo's
code.
This commit is contained in:
Claude
2026-07-22 13:22:46 +00:00
parent c8cc2271a7
commit 26f3f08304
2 changed files with 73 additions and 7 deletions
+29 -5
View File
@@ -130,9 +130,20 @@ anyone notices. Two independent layers matter more than either alone:
trunk's own capabilities.
**Important nuance: these two layers bound different things, and neither
alone bounds both.** NANP-only restriction bounds *cost-per-minute* (a
compromised box can only ever reach $0.01/min US numbers, never $25/min
international/premium destinations) — that risk is fully closed. It does
alone bounds both.** NANP-only restriction bounds *cost-per-minute* — but
**NANP-only alone was not actually sufficient**, and this design carried
that gap for a while before it was caught. NANP includes Caribbean/Atlantic
nations and several US territories (Bahamas, Puerto Rico, Dominican
Republic, and 24 others) that dial exactly like a normal 10-digit US number
but bill at international/premium rates — a well-known toll-fraud/
"one-ring scam" vector specifically because the format looks domestic. The
NANP pattern match alone would have let a compromised extension reach every
one of these. **Fixed**: `services/pstn-trunk.sh` now blocks those 27 area
codes explicitly, checked before permission tier, for every extension
regardless of tier — see the dialplan file's own comment and
"Non-US NANP area codes are blocked" in its generated README for the full
list. With that fix in place, cost-per-minute risk actually is closed (a
compromised box can only ever reach genuine US-rate destinations). It does
**not** bound *how fast* the prepaid balance gets burned: nothing stops a
compromised box from opening many concurrent US-destination calls in
parallel and draining the whole balance (e.g. $15 balance ÷ $0.01/min =
@@ -165,7 +176,11 @@ separately from that hourly check.
- 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 trunk. No catch-all `_X.`
pattern.
pattern. **Also implemented**: an explicit block on the 27 NANP area
codes that aren't actually US (Caribbean/Atlantic nations + US
territories — see the toll-fraud nuance above for why this matters),
checked via `REGEX()` against the extracted area code before permission
tier, ahead of every other check.
- **Three-tier permission model — implemented**, superseding an earlier flat
allow-list design. `internal` / `restricted` / `full` per extension, read
live from `pstn-permissions.conf` via `AST_CONFIG()` rather than baked
@@ -309,7 +324,10 @@ generator output. Fixed by quoting every value in that heredoc.
1. ~~Decide: new `services/pstn-trunk.sh`...~~ Done — separate service file,
generalized to any IP-auth SIP provider (VoIP.ms is just the default).
2. ~~IP auth vs. registration~~ Done — IP authentication, no password stored.
3. ~~Exact NANP dial pattern(s)~~ Done — `_1NXXNXXXXX` / `_NXXNXXXXX`.
3. ~~Exact NANP dial pattern(s)~~ Done — `_1NXXNXXXXX` / `_NXXNXXXXX`. ~~NANP
≠ US gap~~ Done — 27 Caribbean/territory area codes explicitly blocked
(see toll-fraud nuance above); this was a real, live gap in the design
for a while, not a hypothetical.
4. ~~Inbound~~ Done — rings a configurable list of extensions (ring-group
supported), each checked live per-call against its own tier. ~~Permission
model~~ Done — superseded the original flat allow-list with a 3-tier
@@ -338,3 +356,9 @@ generator output. Fixed by quoting every value in that heredoc.
(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).
7. Anveo Direct specifically has a provider-side "Call Security" setting
(account portal, not exposed through SIP config) — a per-minute rate
cap ("block calls with rate more than $X/min, 0 = block all") and a
maximum call duration. Worth setting on any Anveo Direct account as a
provider-side backstop independent of this repo's own dialplan code —
not automatable from here since it's their web UI, not a config file.
+44 -2
View File
@@ -221,8 +221,19 @@ _pstn_write_dialplan_include() {
; to the trunk, so an unauthorized or compromised extension can't reach
; anything else even if the trunk itself would technically allow more. See
; docs/pstn-calling-voipms-plan.md for the toll-fraud reasoning.
;
; NANP is NOT the same thing as "US" — it also covers Caribbean/Atlantic
; nations and several US territories, all of which dial exactly like a
; normal 10-digit US number but get billed by most providers at
; international/premium rates (a well-known toll-fraud/"one-ring scam"
; vector precisely because the number format looks domestic). Blocked by
; area code below, checked before tier permission — this is a hard "never
; reachable" rule, not something even a "full" tier extension can override,
; since "full" means "any US number," not "any NANP-shaped number."
exten => _1NXXNXXXXX,1,NoOp(PSTN outbound call attempt from ${CHANNEL(peername)} to ${EXTEN})
same => n,Set(PSTN_AREA_CODE=${EXTEN:1:3})
same => n,GotoIf($[${REGEX("^(242|246|264|268|284|340|345|441|473|649|658|664|670|671|684|721|758|767|784|787|809|829|849|868|869|876|939)$" ${PSTN_AREA_CODE})} = 1]?pstn_intl_blocked,1)
same => n,Set(PSTN_CALLER=${CHANNEL(peername)})
same => n,Set(PSTN_TIER=${AST_CONFIG(pstn-permissions.conf,${PSTN_CALLER},tier)})
same => n,GotoIf($["${PSTN_TIER}" = "full"]?pstn_check_busy,1)
@@ -235,6 +246,11 @@ __ALERT_DENY_TIER_LINE__
exten => _NXXNXXXXX,1,NoOp(Assuming NANP - adding leading 1)
same => n,Goto(1${EXTEN},1)
exten => pstn_intl_blocked,1,NoOp(PSTN outbound call to ${EXTEN} blocked - non-US/premium NANP area code ${PSTN_AREA_CODE})
__ALERT_DENY_INTL_LINE__
same => n,Busy(15)
same => n,Hangup()
exten => pstn_check_allow_out,1,Set(PSTN_ALLOWED=${AST_CONFIG(pstn-permissions.conf,${PSTN_CALLER},allowed_numbers)})
same => n,GotoIf($[${REGEX("^(${PSTN_ALLOWED})$" ${EXTEN})} = 1]?pstn_check_busy,1)
same => n,NoOp(Denied - ${EXTEN} not on ${PSTN_CALLER}'s approved number list)
@@ -263,10 +279,11 @@ EOF
if [[ -n "$NTFY_URL" ]]; then
local _esc_url="${NTFY_URL//&/\\&}"
sed -i "s#__ALERT_DENY_TIER_LINE__# same => n,System(curl -m 5 -s -d 'PSTN trunk: outbound call denied - no PSTN permission.' '${_esc_url}' >/dev/null 2>\\&1 \\&)#" "$FILE"
sed -i "s#__ALERT_DENY_INTL_LINE__# same => n,System(curl -m 5 -s -d 'PSTN trunk: outbound call blocked - non-US/premium NANP area code.' '${_esc_url}' >/dev/null 2>\\&1 \\&)#" "$FILE"
sed -i "s#__ALERT_DENY_NUMBER_LINE__# same => n,System(curl -m 5 -s -d 'PSTN trunk: outbound call denied - number not pre-approved.' '${_esc_url}' >/dev/null 2>\\&1 \\&)#" "$FILE"
sed -i "s#__ALERT_BUSY_LINE__# same => n,System(curl -m 5 -s -d 'PSTN trunk: outbound concurrent-call cap reached - a call was rejected.' '${_esc_url}' >/dev/null 2>\\&1 \\&)#" "$FILE"
else
sed -i "/__ALERT_DENY_TIER_LINE__/d; /__ALERT_DENY_NUMBER_LINE__/d; /__ALERT_BUSY_LINE__/d" "$FILE"
sed -i "/__ALERT_DENY_TIER_LINE__/d; /__ALERT_DENY_INTL_LINE__/d; /__ALERT_DENY_NUMBER_LINE__/d; /__ALERT_BUSY_LINE__/d" "$FILE"
fi
# ── Inbound: [from-pstn-trunk], one unrolled block per ring-group member.
@@ -761,7 +778,7 @@ background, cost estimate, and toll-fraud reasoning.
| Provider | ${PROVIDER_NAME} |
| Server/POP | ${TRUNK_SERVER} (inbound match IPs: ${TRUNK_SERVER_IPS}) |
| DID | ${TRUNK_DID} |
| Outbound scope | US/NANP only — \`_1NXXNXXXXX\` / \`_NXXNXXXXX\` patterns, no catch-all |
| Outbound scope | US/NANP only — \`_1NXXNXXXXX\` / \`_NXXNXXXXX\` patterns, no catch-all, minus 27 non-US/premium NANP area codes (see below) |
| Full-PSTN extensions | ${FULL_EXTS:-none} |
| Restricted-PSTN extensions | ${RESTRICTED_EXTS:-none} |
| Concurrency caps | ${MAX_OUTBOUND} outbound / ${MAX_INBOUND} inbound simultaneous calls (live — see \`pstn-limits.conf\` below) |
@@ -771,6 +788,31 @@ background, cost estimate, and toll-fraud reasoning.
| Monthly spend alert threshold | \$${MONTH_THRESHOLD} |
| Hourly burst alert threshold | ${BURST_THRESHOLD} calls/hour |
## Non-US NANP area codes are blocked, not just "anything outside NANP"
NANP (the North American Numbering Plan) isn't the same thing as "US" — it
also covers several Caribbean/Atlantic nations and US territories, all of
which dial exactly like a normal 10-digit US number but get billed by most
providers at international/premium rates. This is a well-known toll-fraud/
"one-ring scam" vector specifically because the number *looks* domestic.
27 area codes are blocked explicitly, checked before permission tier — this
applies to **every** extension regardless of tier, since "full" means "any
US number," not "any NANP-shaped number":
Bahamas (242), Barbados (246), Anguilla (264), Antigua & Barbuda (268),
British Virgin Islands (284), US Virgin Islands (340), Cayman Islands
(345), Bermuda (441), Grenada (473), Turks & Caicos (649), Jamaica
(658/876), Montserrat (664), Northern Mariana Islands (670), Guam (671),
American Samoa (684), Sint Maarten (721), Saint Lucia (758), Dominica
(767), Saint Vincent (784), Puerto Rico (787/939), Dominican Republic
(809/829/849), Trinidad & Tobago (868), Saint Kitts & Nevis (869).
If you have a legitimate reason to call one of these (e.g. family in Puerto
Rico), remove that entry from the `REGEX()` pattern in
`pstn-trunk-dialplan.conf`'s `_1NXXNXXXXX` extension — it'll be
regenerated exactly the same way on the next reinstall/update, so note the
change somewhere you'll remember it, or keep a local diff.
## Permission tiers
Every extension can always call and receive calls from other Asterisk