diff --git a/docs/anveo-direct-setup-guide.md b/docs/anveo-direct-setup-guide.md new file mode 100644 index 0000000..8b75bd9 --- /dev/null +++ b/docs/anveo-direct-setup-guide.md @@ -0,0 +1,220 @@ +# Anveo Direct + Easy Asterisk — confirmed working setup guide + +This is the exact sequence that got a real Anveo Direct DID working end to +end (both outbound and inbound) with `asterisk-digital-ocean.sh` + +`pstn-trunk.sh`, confirmed live on a real droplet. Follow it in order for +each additional number — steps 1–2 are one-time account setup; steps +3–7 repeat per DID. + +## 0. Prerequisites + +- `asterisk-digital-ocean.sh` (or `asterisk.sh` for a LAN box) already + installed and running, with at least one extension configured. +- This box's public IP address (`curl -4 ifconfig.me`). + +## 1. Anveo Direct account (one-time) + +1. Create an account at `https://www.anveo.com/account.asp?account_type=direct` + — this is what makes it a **Direct** account (trunk/DID product), not + the hosted-PBX product. +2. Fund the account balance directly (not a subscription plan). Bank + transfers can take several hours to clear. +3. **Known account-level caps as of this writing**, both separate from + anything this installer enforces: + - **Trial account limits**: 2 concurrent outbound calls (CPS: 2), + $2/30-day spend limit, $2 minimum balance — shown on the Outbound + Trunks page under "Account Limits." A "request higher limits" link + sits next to the CallerID Policies section for asking Anveo to lift + these once funded. + - **Phone Numbers cap**: "You can manage up to 2 phone numbers. Please + contact customer support to increase the limit" — a **separate** cap + from the concurrent-call limit above, shown on the Phone Numbers + summary page. Anveo indicated (not independently confirmed) that + removing this requires providing a tax ID (EIN for a business, SSN + for an individual) — decide for yourself whether that trade-off is + worth it before planning on more than 2 DIDs. +4. CallerID policy: USA/Canada/international destinations all require + CallerID **from a verified/Anveo-owned number** — any DID ordered + through Anveo satisfies this automatically. + +## 2. Order a DID + +1. Go to the DID ordering tool ("Phone Numbers around the World" / + similar), pick a number, and select the **Per Minute** rate plan — not + Prime. + - **Per Minute**: $0.004/min incoming, 10 dedicated incoming channels + bundled in, no separate trunk product needed for inbound. + - **Prime**: free incoming, but requires a separately-purchased "Anveo + Trunk" (a *different* product — flat monthly fee, $6.50–$17/mo by + zone, confirmed via the "Anveo Direct Trunk Price List" page). Per + Minute avoids this fee entirely, which is why it's the one to use. +2. Confirm: $0.25 one-time setup, $0.15/month recurring (billed the 15th), + 3-month minimum term, pre-paid from account balance. + +## 3. Create the Outbound Service (Call Termination) Trunk + +This only needs to be done **once** — the same trunk carries outbound +calls for every DID/extension. Skip this step for DIDs 2+. + +1. Outbound Trunks → **Add a new Call Termination Trunk**. +2. Fill in: + - **Title**: any label (e.g. `asterisk-do`) + - **Dialing Prefix**: leave **blank** — confirmed not required (no red + "required" asterisk on this field, unlike Authorized IP Addresses and + Call Routing Method). The dialplan here dials the bare number, no + prefix. + - **Authorized IP Addresses**: this box's public IP, one per line. This + is the IP-authentication Anveo uses instead of a SIP password. + - **Rate Cap**: optional safety ceiling — e.g. `Yes, $1/min` filters out + any route pricier than that. Domestic US termination runs far below + this, so it doesn't interfere with normal calls, just blocks getting + routed onto an absurdly expensive carrier by accident. + - **Concurrent Calls Limit**: whatever you want as an Anveo-side cap + (e.g. `Yes, 6`) — note the account's Trial "Outbound Channels: 2" + limit (see step 1) overrides this until lifted; whichever number is + lower wins. + - **Call Routing Method**: leave the default **Custom Least Cost + Routing Model** and all of its sub-fields (All Prime Routes / cost + lowest-to-highest / 5 routes / cost-ordered failover / 10 sec + timeout) exactly as shown — sensible defaults, nothing to tune here. +3. Save. + +### Finding the real outbound rate + +The trunk's LCR pulls from **"All Prime Routes"** by default — meaning +your real per-minute cost is whatever's in the **Anveo Direct Prime** rate +card (Outbound Services/Call Termination page → "Download rates here" +under the **Prime** column, not Value/Standard/All). Confirmed from that +CSV: standard US-to-US domestic is a flat **`$0.00388/min`**, billed +per-second (not rounded to the minute) — this is the generic `USA.` +catch-all entry (prefix `1`); a few specific area codes have their own +slightly different override rates, and Virgin Islands is billed +separately/higher (~$0.02/min) despite looking like a normal US number. + +## 4. Create the SIP Trunk (inbound forwarding) — once per DID + +Anveo has **two unrelated "trunk" concepts** — don't confuse them: +- The **Outbound Service Trunk** from step 3 handles calls *out*. +- A separate **"SIP Trunk"** object (Account Options → wherever inbound + routing objects live) handles calls *in* — this is what actually + populates a DID's "Destination SIP Trunk" dropdown. + +For **each DID**, create one of these: + +1. **Trunk Name**: any label (e.g. `asterisk-do-inbound`) — can reuse the + same one for multiple DIDs if you want them all forwarding the same + way, or make one per DID for clarity. +2. **Primary**: type **SIP URI**, value: + ``` + $[E164]$@:5060 + ``` + (port 5060 — Easy Asterisk's default SIP port, confirmed from its own + vendor source `DEFAULT_SIP_PORT="5060"`). The `$[E164]$` placeholder is + replaced by Anveo with the DID's own number, digits only, country code + included, no `+` — e.g. DID `+15551234567` becomes `15551234567@IP:5060` + in the actual INVITE Anveo sends. +3. **Failover**: leave blank. +4. Save. +5. Go to the DID's own **Call Options** tab → set **Destination SIP + Trunk** to this new SIP Trunk object (not the Outbound Service Trunk) + → Save. +6. **Optional, recommended once you're adding more DIDs**: Account + Options → Service Defaults → set **Default Destination Trunk** to this + SIP Trunk, so future DIDs auto-route here without repeating step 5. + +## 5. Configure the droplet + +``` +sudo ./setup.sh pstn-trunk +``` + +- Existing install → choose **update** (`r`) if you're just changing the + DID/server, or the CLI will walk fresh prompts if none exists yet. +- Provider quick-pick: **1) Anveo Direct** — pre-fills `sbc.anveo.com` and + Anveo's 4 published signaling IPs (only one of which the hostname + itself resolves to at any given moment; the others are entered as + "additional known source IPs" so inbound is recognized regardless of + which one a call actually arrives from). +- **DID**: 10 digits, no leading `1` (e.g. `5551234567`). +- **Outbound per-minute rate**: `0.00388` (see step 3's rate-lookup note) + — pre-filled automatically when Anveo Direct is the selected provider. +- Concurrent-call caps, ring-group extensions, ntfy topic, spend-cap + kill-switch thresholds: set to taste (all live-editable later without + reinstalling). +- Who can call/be called (permission tiers, approved numbers), internal + SIP messaging, and personal-number assignment are **not** prompted here + — set them all in the Security Dashboard's **PSTN Trunk** tab instead + (install it first if you haven't: `sudo ./setup.sh security-dashboard`). + Every extension starts at `internal` (no PSTN access) until granted + there. + +## 6. Grant permissions and (optionally) a personal number + +In the Security Dashboard's PSTN Trunk tab: +1. Set the tier for each extension that should get PSTN access (`full` or + `restricted` — `restricted` also needs at least one approved number). +2. **Personal numbers** card: assign this DID to a specific extension if + you want inbound calls to it to ring *only* that extension (instead of + the whole shared ring-group) and its outbound Caller-ID to show this + DID instead of the shared trunk DID. Enter the DID as **10 digits, no + leading 1** (matches the same convention as the trunk DID above) — + the dashboard's own input validates against exactly that format. + +## 7. Test + +- **Outbound**: from a full/restricted-tier extension in Sipnetic, dial + `1` + area code + number (11 digits total — the dialplan matches the + full NANP pattern, not a bare 10-digit number without the leading `1`). +- **Inbound**: from any outside phone, dial the DID's real number. If a + personal number was assigned, only that extension should ring; if not, + the whole configured ring-group rings simultaneously until someone + answers or 20 seconds pass. +- Watch the live console while testing either direction: + ``` + docker exec -it easy-asterisk-do asterisk -rvvv + ``` + +## Bugs hit and fixed along the way (informational — already fixed) + +These were all real, confirmed-live bugs in earlier versions of this +installer, not configuration mistakes — listed here for context on what +"just works" now that didn't before: + +- **NANP dial pattern was one digit short** (`_1NXXNXXXXX` / `_NXXNXXXXX`, + 10/9 characters instead of the correct 11/10) — Asterisk's exact-length + pattern matching never matched a real number at all. +- **`${CHANNEL(peername)}`** isn't a valid channel-function item on this + Asterisk build — broke the caller-extension lookup used for every + permission check. +- **`${EXTEN}` corruption after `Goto` to a named extension** — every + `Goto(somelabel,1)` to a *different* extension (not a same-extension + priority label) resets `${EXTEN}` to that label's own name, silently + breaking the approved-numbers check, the international country check, + and the final `Dial()` itself. Fixed by capturing the real dialed + number into its own variable before any such jump. +- **Duplicate ring-group extensions** (the same extension listed twice in + `RING_EXTS`) generated duplicate dialplan priority labels, causing an + infinite loop that never reached the ring step at all. Now deduped + automatically. +- **Personal-DID lookup format mismatch** — DIDs are stored 10-digit, but + Anveo delivers the called number as 11-digit E.164 in `${EXTEN}`; the + lookup now normalizes to 10 digits first. +- **Easy Asterisk only regenerates `pjsip.conf`/`extensions.conf` if they + don't already exist** — a box that already has devices configured (the + normal case) never picks up a vendor-script `#include` patch on a plain + restart. Both the PSTN trunk and internal-messaging installers now also + patch the live config files directly, not just the generator source. +- **A stale/malformed `.pstn-trunk.env`** could crash the entire installer + under `setup.sh`'s `set -u` on `source`. Now guarded and falls back to + safe defaults, erroring clearly only if genuinely required fields are + missing. + +## Still open + +- The 2-phone-number account cap (see step 1) blocks getting all 4 + planned numbers until either Anveo lifts it or a tax ID is provided — + tabled for now. +- The interactive CLI walkthrough (`pstn-trunk.sh` actually prompting + through account setup step by step, not just this static doc) hasn't + been built yet — this guide is the reference for building that once + there's appetite for it. diff --git a/docs/pstn-calling-voipms-plan.md b/docs/pstn-calling-voipms-plan.md index 2cf04fd..63f1107 100644 --- a/docs/pstn-calling-voipms-plan.md +++ b/docs/pstn-calling-voipms-plan.md @@ -3,6 +3,12 @@ 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. +**For the confirmed-working, step-by-step Anveo Direct account + droplet +setup (real values, real bugs hit and fixed), see +`docs/anveo-direct-setup-guide.md` instead of re-deriving it from this +file** — this doc is the design/decision log; that one is the clean +how-to. + **Implemented** — see `services/pstn-trunk.sh` (run `sudo ./setup.sh pstn-trunk` after `asterisk-digital-ocean` **or** `asterisk` (home/LAN) is installed — both are supported, see the file for the static-IP caveat on the diff --git a/services/pstn-trunk.sh b/services/pstn-trunk.sh index 23cf40a..48f6bbd 100644 --- a/services/pstn-trunk.sh +++ b/services/pstn-trunk.sh @@ -1211,6 +1211,101 @@ ENV _pstn_install_periodic_timer "$EA_DIR" } +# Simple "press Enter to continue" pause — the portal steps below happen in +# a browser, not this terminal, so there's nothing to validate; this just +# gates pacing. Auto-skips under UNATTENDED (prompt_text's own behavior). +_pstn_wait_continue() { + local _msg="$1" _ignored="" + prompt_text " $_msg" "" _ignored +} + +# ── Anveo Direct walkthrough ──────────────────────────────────────────────── +# Every value/field below is confirmed working end-to-end against a real +# account and a real call (both directions) — see +# docs/anveo-direct-setup-guide.md for the full narrative this was built +# from, including every bug that was hit and fixed along the way to get +# here. Nothing here is automatable (it's a real account behind a browser), +# so this walks the account-side steps with a pause between each, then lets +# the rest of this installer's existing prompts (DID, ring group, etc.) +# handle the Asterisk side as normal. +_pstn_anveo_walkthrough() { + local _pub_ip="" + _pub_ip="$(curl -fsS -4 --max-time 3 ifconfig.me 2>/dev/null || true)" + [[ -z "$_pub_ip" ]] && _pub_ip="" + + echo "" + log_info "Anveo Direct walkthrough — each step below happens in Anveo's own portal," + log_info "not this terminal. Press Enter after each one to move to the next. Every" + log_info "value given here is confirmed to work, not a guess." + + echo "" + echo " Step 1/5 — Account" + echo " https://www.anveo.com/account.asp?account_type=direct" + echo " Create/verify your account, then fund the balance directly (NOT a" + echo " subscription plan) — \$25 is plenty to start. Bank transfers can take a" + echo " few hours to clear. Two account-level caps to know about, both separate" + echo " from anything this installer enforces:" + echo " - Trial limits: 2 concurrent outbound calls, \$2/30-day spend cap, \$2" + echo " minimum balance — a 'request higher limits' link sits next to" + echo " CallerID Policies once you're funded." + echo " - Phone-numbers cap: most new accounts can manage up to 2 DIDs;" + echo " raising it means contacting Anveo support (they may ask for a tax" + echo " ID — your call whether that trade-off is worth it)." + echo " CallerID policy: outbound Caller-ID must be a verified/Anveo-owned" + echo " number — any DID you order through them satisfies this automatically." + _pstn_wait_continue "Press Enter once your account is created and funded:" + + echo "" + echo " Step 2/5 — Order a DID" + echo " Order a phone number choosing the 'Per Minute' rate plan — NOT Prime." + echo " Per Minute bundles 10 dedicated incoming channels at \$0.004/min with no" + echo " separate trunk fee; Prime is free incoming but requires a separate," + echo " flat monthly-fee trunk product you don't need." + _pstn_wait_continue "Press Enter once you've ordered a DID:" + + echo "" + echo " Step 3/5 — Outbound Service (Call Termination) Trunk" + echo " Skip this step if you already have one from a previous number — one" + echo " trunk serves every DID on the account, this only needs doing once." + echo " Outbound Trunks -> Add a new Call Termination Trunk:" + echo " Title: any label" + echo " Dialing Prefix: leave BLANK (confirmed not required)" + echo " Authorized IP Addresses: ${_pub_ip}" + echo " Rate Cap: optional, e.g. \$1/min (a safety ceiling only —" + echo " domestic calls run far below this)" + echo " Concurrent Calls Limit: your choice (the Trial account's own 2-call" + echo " cap from Step 1 overrides this until lifted)" + echo " Call Routing Method: leave 'Custom Least Cost Routing Model' and" + echo " all its sub-fields exactly as defaulted" + _pstn_wait_continue "Press Enter once the trunk is created:" + + echo "" + echo " Step 4/5 — SIP Trunk (inbound forwarding) — one per DID" + echo " A SEPARATE object from the Outbound trunk above — don't confuse them;" + echo " this is what actually makes inbound calls to THIS DID ring anywhere." + echo " Trunk Name: any label" + echo " Primary: SIP URI -> \$[E164]\$@${_pub_ip}:5060" + echo " Failover: leave blank" + echo " Then open this DID's own Call Options tab -> Destination SIP Trunk ->" + echo " select the SIP Trunk you just created -> Save." + _pstn_wait_continue "Press Enter once done:" + + echo "" + echo " Step 5/5 — Confirmed rate" + echo " Standard US-to-US domestic on the Prime rate card (the route set this" + echo " trunk's Custom LCR pulls from by default) is \$0.00388/min, billed" + echo " per-second — already the default a few prompts from now, no need to" + echo " look it up yourself unless you want to double-check it." + _pstn_wait_continue "Press Enter to continue:" + + echo "" + log_success "Portal setup done. The rest of this installer configures the Asterisk side." + log_info "One more thing for after this finishes: grant PSTN access (and, if you" + log_info "want, this DID as a personal number) to the right extension via the" + log_info "Security Dashboard's PSTN Trunk tab — every extension starts at 'internal'" + log_info "(no PSTN access) until you do." +} + install_pstn-trunk() { require_docker || return 1 @@ -1236,7 +1331,9 @@ install_pstn-trunk() { if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would require an existing asterisk-digital-ocean OR asterisk (LAN) install" - echo "[DRY-RUN] Would prompt for: known-provider quick-pick (Anveo Direct/VoIP.ms pre-fill known" + echo "[DRY-RUN] Would prompt for: known-provider quick-pick (Anveo Direct runs a full 5-step" + echo "[DRY-RUN] interactive portal walkthrough — account/funding, DID ordering, both trunk" + echo "[DRY-RUN] objects, confirmed rate — pausing for Enter between each; VoIP.ms pre-fills known" echo "[DRY-RUN] server/signaling-IP values; still editable) or manual entry, SIP provider name, DID," echo "[DRY-RUN] max concurrent outbound/inbound calls (default 10/10), inbound ring-group extensions," echo "[DRY-RUN] ntfy alert topic (optional), international-calling allow-list (CLI-only," @@ -1279,7 +1376,8 @@ install_pstn-trunk() { fi log_info "Configuring a SIP PSTN trunk for $ASTERISK_KIND (any IP-authenticated provider —" - log_info "VoIP.ms and Anveo Direct are both confirmed working; see docs/pstn-calling-voipms-plan.md)." + log_info "VoIP.ms and Anveo Direct are both confirmed working; see docs/pstn-calling-voipms-plan.md" + log_info "and, for Anveo Direct specifically, docs/anveo-direct-setup-guide.md)." log_info "US-only outbound (NANP dialplan), a concurrent-call cap, per-extension permission" log_info "tiers, an inbound ring-group, and ntfy alerts on denied/rejected calls plus" log_info "spend/volume checks." @@ -1380,27 +1478,7 @@ install_pstn-trunk() { _default_provider_name="Anveo Direct" _default_server="sbc.anveo.com" _default_extra_ips="169.48.232.158 204.216.109.55 176.9.39.206 72.9.149.25" - echo "" - log_info "Anveo Direct known values pre-filled below (hostname, 4 published signaling IPs)." - log_warning "Before continuing: create/verify your account at" - log_warning " https://www.anveo.com/account.asp?account_type=direct" - log_warning "fund it, order a DID, and configure at least one outbound Call" - log_warning "Termination trunk in Anveo's own portal (Outbound Trunks page). A Trial" - log_warning "account shows its own \$2/30-day spend limit and \$2 minimum balance —" - log_warning "separate from anything this installer enforces; confirm with Anveo" - log_warning "support whether those change once verified/funded. Their CallerID policy" - log_warning "requires a verified/Anveo-owned number as Caller-ID — any DID you order" - log_warning "through them satisfies that automatically (see this service's personal-" - log_warning "number feature for assigning a specific one per extension)." - log_info "Confirmed live (2026-07-23): the trunk's own Dialing Prefix field has no" - log_info "required-field marker on Anveo's Edit Call Termination Trunk screen — it's" - log_info "optional. Leave it blank; this dialplan dials the bare number, no prefix." - log_warning "Anveo Direct also separates two different 'trunk' concepts — don't confuse them:" - log_warning "an Outbound Service (Call Termination) Trunk handles calls OUT (IP-authenticated," - log_warning "what this installer configures), while inbound routing for a DID is a SEPARATE" - log_warning "'SIP Trunk' object (Account -> the DID's Call Options tab -> Destination SIP" - log_warning "Trunk) that forwards to a SIP URI — e.g. \$[E164]\$@:5060." - log_warning "Create both in Anveo's portal; this installer only handles the Asterisk side." + _pstn_anveo_walkthrough ;; 2) _default_provider_name="VoIP.ms"