From 802b51b34d8ed782c8c8c037fc0a142b54378d05 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 17:47:52 +0000 Subject: [PATCH] pstn-trunk: strip a leading + before normalizing Caller-ID/whitelist numbers Live trace confirmed: once trust_id_inbound=yes started surfacing real caller identity (previous commit), Anveo delivers it "+E.164" style (e.g. "+15551234567") instead of the bare digits the old callerid-fallback path produced. PSTN_CALLERID_NORM's existing "add a leading 1 if length is 10" check never fires for a 12-character "+"-prefixed value and never strips the "+" either, so it can never match an 11-digit, digits-only allowed_numbers entry no matter how correctly the number is whitelisted - a correctly-configured restricted-tier extension or group member would still always get busy. Fixed in three places: the inbound dialplan's PSTN_CALLERID_NORM computation (new PSTN_CID_RAW step strips a leading "+" first), the group-ring shell script's own defensive re-normalization, and the dashboard's admin-input-side normalizers (_normalize_nanp_number, _normalize_personal_did_input) so pasting a number straight out of a phone's call log (which naturally includes the "+") works too instead of being silently dropped. --- services/pstn-trunk.sh | 41 ++++++++++++++++++++++++---------- services/security-dashboard.sh | 28 ++++++++++++++--------- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/services/pstn-trunk.sh b/services/pstn-trunk.sh index 57e58f3..1e6c735 100644 --- a/services/pstn-trunk.sh +++ b/services/pstn-trunk.sh @@ -478,12 +478,23 @@ EOF # PSTN_CALLERID_NORM below mirrors what outbound's _NXXNXXXXXX pattern # already does (Goto 1${EXTEN} to add a leading "1" before any tier/ # allowed_numbers check) but for the inbound direction: allowed_numbers -# is always stored 11-digit (dashboard's NUMBER_RE requires exactly 11 -# digits), but a provider's inbound Caller-ID isn't guaranteed to include -# the leading "1" — confirmed live: Anveo delivered a bare 10-digit -# CALLERID(num), so every restricted-tier check against it failed on -# a plain digit-count mismatch (11-digit pattern vs. 10-digit string), -# regardless of whether the number itself was genuinely on the list. +# is always stored 11-digit, digits only (dashboard's NUMBER_RE requires +# exactly 11 digits, no other characters), but a provider's inbound +# Caller-ID isn't guaranteed to arrive in that exact shape. Two confirmed- +# live gaps here, not just one: +# - Anveo delivered a bare 10-digit CALLERID(num) with no leading "1", so +# every restricted-tier check against it failed on a plain digit-count +# mismatch (11-digit pattern vs. 10-digit string), regardless of whether +# the number itself was genuinely on the list. +# - Separately, once trust_id_inbound=yes (see _pstn_write_pjsip_include) +# started surfacing the real caller identity instead of the endpoint's +# static callerid fallback, that real identity arrived "+E.164" style +# (a leading "+", e.g. "+15551234567") — 12 characters, so the LEN=10 +# check never fires and the "+" is never stripped, leaving a value that +# can never match an 11-digit, digits-only allowed_numbers pattern no +# matter how correctly the number is whitelisted. PSTN_CID_RAW below +# strips a leading "+" first, THEN the existing 10-digit check runs +# against that. # Every restricted-tier comparison below (ring-group members, personal- # DID owner, group-owned personal DID) uses this normalized value # instead of raw ${CALLERID(num)}. @@ -517,7 +528,8 @@ exten => _X.,1,NoOp(Inbound PSTN call from ${CALLERID(num)} to ${EXTEN}) same => n,Set(PSTN_KILLED=${AST_CONFIG(pstn-trunk-killswitch.conf,state,tripped)}) same => n,GotoIf($["${PSTN_KILLED}" = "1"]?pstn_in_killed,1) same => n,Set(PSTN_DID_10=${IF($[${LEN(${EXTEN})} = 11]?${EXTEN:1}:${EXTEN})}) - same => n,Set(PSTN_CALLERID_NORM=${IF($[${LEN(${CALLERID(num)})} = 10]?1${CALLERID(num)}:${CALLERID(num)})}) + same => n,Set(PSTN_CID_RAW=${IF($["${CALLERID(num):0:1}" = "+"]?${CALLERID(num):1}:${CALLERID(num)})}) + same => n,Set(PSTN_CALLERID_NORM=${IF($[${LEN(${PSTN_CID_RAW})} = 10]?1${PSTN_CID_RAW}:${PSTN_CID_RAW})}) same => n,Set(PSTN_PERSONAL_OWNER=${AST_CONFIG(pstn-personal-dids.conf,${PSTN_DID_10},owner)}) same => n,GotoIf($["${PSTN_PERSONAL_OWNER}" != ""]?pstn_personal_inbound,1) same => n,Set(PSTN_RING_LIST=) @@ -673,11 +685,16 @@ CALLER="$1" GROUP="$2" CONF_DIR="/etc/asterisk" -# allowed_numbers is always stored 11-digit (dashboard's NUMBER_RE requires -# it); the dialplan already passes a normalized caller ID in here -# (PSTN_CALLERID_NORM), but normalize again defensively — this script is -# also useful to invoke by hand for testing, and a bare 10-digit caller ID -# would otherwise never match any 11-digit allowed_numbers entry. +# allowed_numbers is always stored 11-digit, digits only (dashboard's +# NUMBER_RE requires it); the dialplan already passes a normalized caller ID +# in here (PSTN_CALLERID_NORM), but normalize again defensively — this +# script is also useful to invoke by hand for testing. Strip a leading "+" +# first (real caller identity via trust_id_inbound can arrive "+E.164" +# style, e.g. "+15551234567" — 12 characters, so it'd otherwise skip the +# 10-digit check below and never match an 11-digit allowed_numbers entry +# no matter how correctly the number is whitelisted), then a bare 10-digit +# caller ID would otherwise never match any 11-digit allowed_numbers entry. +CALLER="${CALLER#+}" [[ ${#CALLER} -eq 10 ]] && CALLER="1${CALLER}" _ini_get() { diff --git a/services/security-dashboard.sh b/services/security-dashboard.sh index c2a57a1..186219d 100644 --- a/services/security-dashboard.sh +++ b/services/security-dashboard.sh @@ -786,14 +786,18 @@ NUMBER_RE_10 = re.compile(r"^\d{10}$") def _normalize_nanp_number(token): - """Accepts either a bare 10-digit NANP number (the natural way to type - a US number) or an already-11-digit one (leading "1" country code) and - returns the canonical 11-digit form allowed_numbers is always stored - in - REGEX() comparisons against CALLERID(num) require an exact - digit-count match, and this used to silently DROP a plain 10-digit - entry instead of normalizing it, the admin-input-side twin of the bug - that was also failing inbound calls whose Caller-ID itself arrived - without a leading "1" (see PSTN_CALLERID_NORM in pstn-trunk.sh).""" + """Accepts a bare 10-digit NANP number (the natural way to type a US + number), an already-11-digit one (leading "1" country code), or either + of those with a leading "+" (the natural way to paste a number straight + out of a phone's call log) and returns the canonical 11-digit, + digits-only form allowed_numbers is always stored in - REGEX() + comparisons against CALLERID(num) require an exact digit-count match, + and this used to silently DROP a plain 10-digit entry instead of + normalizing it, the admin-input-side twin of the bug that was also + failing inbound calls whose Caller-ID itself arrived without a leading + "1", or arrived "+E.164" style with a leading "+" Asterisk never + stripped (see PSTN_CALLERID_NORM / PSTN_CID_RAW in pstn-trunk.sh).""" + token = token.lstrip("+") if NUMBER_RE_10.match(token): return "1" + token if NUMBER_RE.match(token): @@ -1429,9 +1433,11 @@ PERSONAL_DID_RE_11 = re.compile(r"^1\d{10}$") def _normalize_personal_did_input(did): """write_personal_did()'s DID field is hand-typed, same footgun as - allowed_numbers - accept either the canonical bare 10-digit form or an - 11-digit one with the NANP "1" prefix, returning the canonical 10-digit - form either way instead of rejecting a plainly-valid 11-digit entry.""" + allowed_numbers - accept the canonical bare 10-digit form, an 11-digit + one with the NANP "1" prefix, or either with a leading "+" (pasted + straight from a call log), returning the canonical 10-digit form either + way instead of rejecting a plainly-valid entry.""" + did = did.lstrip("+") if PERSONAL_DID_RE.match(did): return did if PERSONAL_DID_RE_11.match(did):