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:
@@ -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 $2–5/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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user