Commit Graph
18 Commits
Author SHA1 Message Date
Claude a33b8e7b23 Simplify pstn-trunk.sh: point to the dashboard instead of CLI permission prompts
Removed the full/restricted-extension, approved-numbers pool/whiptail,
messaging-extensions, and personal-DID-assignment prompts from the
installer. All four are already live-editable, no-restart-needed settings
the Security Dashboard's PSTN Trunk tab manages end to end — the CLI wall
of prompts (that then needed a full reinstall to change) just duplicated
that with more friction. Every extension now starts at internal tier (no
PSTN, no messaging) until granted via the dashboard. Kept: provider/DID
setup, concurrency caps, and the inbound ring-group (basic trunk wiring,
not a permission). Also updated the generated README and CLI summary to
match, and refreshed the stale "known gap" messaging section that predated
the dialplan enforcement built earlier this session.
2026-07-23 17:16:42 +00:00
Claude 3f033da635 Fix trunk/messaging #include lines never reaching an existing install's live config
Confirmed live via a real failed test call ("extension not found in context
'intercom'"): Easy Asterisk's entrypoint only regenerates pjsip.conf/
extensions.conf if they don't already exist. Patching the vendor generator
functions (as both pstn-trunk.sh and the earlier SIP MESSAGE work did) only
takes effect on a future full regeneration — a box that already has devices
configured, the normal case, never triggers one on a plain restart, so the
#include lines never actually reached the live files despite the generator
patch succeeding.

Both installers now also patch the live pjsip.conf/extensions.conf directly
(same anchors, idempotent) and force a reload, so the trunk/messaging
dialplan actually takes effect immediately regardless of whether Easy
Asterisk ever regenerates its config on its own.
2026-07-23 17:10:13 +00:00
Claude 67ded2eb38 Use the confirmed Anveo Direct Prime rate as the RATE_PER_MIN default
0.00388/min for standard US-to-US domestic, confirmed against Anveo's own
Prime rate card CSV (the route set the outbound trunk's LCR config actually
pulls from) rather than the earlier ~0.001 ballpark guess. Defaults to it
automatically when the Anveo Direct quick-pick was chosen.
2026-07-23 16:55:22 +00:00
Claude fdd2aaa1d3 Add shared approved-numbers pool + whiptail multi-select per extension
Restricted extensions previously required retyping the full 11-digit
number list from scratch at each extension's own prompt. Now offers
entering a pool once, then a whiptail checklist per extension to pick a
subset (falls back to typing numbers directly, or 'all' for the whole
pool, when whiptail isn't available or the run is unattended).
2026-07-23 16:49:19 +00:00
Claude 16da6c1372 Clarify PSTN tier prompts — enter extension numbers, not tier names
User feedback: reading the tier list (internal/restricted/full) right before
"Full-PSTN extensions:" read as ambiguous about whether to type the tier
word or extension numbers. Reworded both prompts to say "extension NUMBERS"
explicitly and added inline examples (e.g. '999 213').
2026-07-23 16:44:35 +00:00
Claude 4c0326ca5f Update Anveo Direct dial-prefix warning with live-confirmed findings
The dialing-prefix ambiguity is resolved: Anveo's own trunk edit screen has
no required-field marker on Dialing Prefix, confirming it's optional. Also
documents the separate Outbound Service Trunk vs. inbound SIP Trunk
distinction discovered while walking through a real account setup.
2026-07-23 16:05:52 +00:00
Claude 7851e2befd Decouple internal SIP messaging management from PSTN trunk installation
Messaging has no dependency on a PSTN trunk existing (no cost, no
carrier, no DID), but the whole PSTN Trunk tab - including the messaging
checkbox added last commit - was hidden behind pstn_installed(), which
only becomes true once services/pstn-trunk.sh's dialplan is actually
wired in. That meant enabling messaging required going through full SIP
trunk/provider setup first for no real reason.

Added a standalone "Internal SIP messaging" card that's always visible in
the tab regardless of trunk status, backed by a new write_messaging()
that only touches the messaging key (leaving tier/allowed_numbers/
personal_did untouched) and creates pstn-permissions.conf from scratch if
it doesn't exist yet. Confirmed the systemd unit's ReadWritePaths and the
group/chmod grants already covered this - both are set up whenever a base
Asterisk install is detected, independent of pstn-trunk - so no
permission-layer changes were needed, only the dashboard's own artificial
UI gate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
2026-07-23 05:38:36 +00:00
Claude 8291eba55e Make the messaging permission flag live-editable via the dashboard
get_all_permissions()/write_permission() now handle messaging alongside
tier/allowed_numbers as one save action, and the Security Dashboard's PSTN
Trunk table gets a Messaging checkbox column - no more needing to re-run
pstn-trunk.sh's CLI just to change who can use internal SIP texting,
matching how tier/allowed_numbers/personal_did already worked.

Tested that messaging correctly survives tier changes and personal-DID
assignment/removal on the same extension (independent axes, as intended).

Still explicitly not done, and said so in both READMEs rather than
implying otherwise now that there's a nice UI for it: the actual SIP
MESSAGE dialplan wiring that would make Asterisk enforce this flag. That
gap hasn't changed - only the permission storage/UI layer around it has.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
2026-07-23 05:27:58 +00:00
Claude 3705fb5fcc Add Anveo Direct/VoIP.ms known-provider quick-pick to pstn-trunk.sh
Prompts now start with a 1/2/3 choice (Anveo Direct / VoIP.ms / manual)
that pre-fills already-known values - Anveo Direct's sbc.anveo.com
hostname and its 4 published signaling IPs, so they don't need retyping
from memory - plus provider-specific account-setup reminders. Every
value stays editable at each subsequent prompt; this only changes
defaults, not behavior, so existing manual-entry users see no change.

Flagged rather than silently trusted: Anveo Direct's own Outbound Trunks
page documents dialing as [PREFIX]PHONENUMBER@sbc.anveo.com, which
contradicts an earlier no-prefix-needed finding from their FAQ. The
dialplan still dials the bare number (matching the older finding) - the
quick-pick prints a loud warning to verify the Prefix field can be left
blank before relying on this, rather than guessing and hardcoding prefix
handling without live confirmation either way.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
2026-07-23 05:08:30 +00:00
Claude fcb01a4d93 Add personal/per-extension DID assignment ("their own numbers")
Multiple DIDs can now share one trunk/account, each assigned to a specific
extension - additive to the existing shared trunk DID/ring-group, which
keeps working unchanged for everyone regardless of what's assigned here.

- New pstn-personal-dids.conf (DID -> owner extension), read live by the
  dialplan for inbound routing: a call to a personal DID routes straight
  to its owner, checked against the owner's own tier/approved-numbers, no
  ring-group fallback since it's that extension's own line.
- New personal_did= field per extension in pstn-permissions.conf: the
  outbound Caller-ID override, used by pstn_check_busy (the shared exit
  point for both domestic and international dialing) instead of the
  shared trunk DID when the calling extension has one assigned.
- Both files kept in sync automatically by one write path - CLI prompt at
  install/update, or a new "Personal numbers" card in the Security
  Dashboard's PSTN Trunk tab - rather than requiring hand-editing both
  consistently. Reassigning a DID or giving an extension a new one cleanly
  drops the stale side of the old mapping.
- Assigning a DID to an internal-tier extension is accepted but warned
  about (won't ring anyone until also granted full/restricted tier),
  matching this repo's permissive-with-warnings style rather than
  blocking the action outright.

Also fixed a real bug found while building this: security-dashboard.sh's
write_permission() did cp.remove_section(ext) whenever tier was set to
"internal", silently discarding any messaging=yes or personal_did=
already set on that extension. Now removes only the tier/allowed_numbers
keys, dropping the section only once nothing else remains in it.

Separately, de-anchored pstn-trunk.sh's install prompts from VoIP.ms as
the implicit default (provider name default, server-hostname example
wording, rate-prompt wording) now that Anveo Direct is an equally
confirmed, tested provider option.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
2026-07-23 05:03:13 +00:00
Claude 2d9dd80271 Add per-country international rates and active-hangup on kill-switch trip
- CLI country menu now asks for a per-minute rate when a country is added,
  stored alongside its code/name in pstn-intl-allowed.conf. The usage-alert
  script buckets each month's outbound calls into domestic vs. per-country
  international (longest-code-prefix match on the dialed digits) and prices
  each bucket at its own rate instead of one blended flat rate, which badly
  under/over-estimated international cost.
- While the kill-switch is tripped, the periodic script now also force-hangs
  up any PSTN call already in progress (docker exec + Asterisk's "channel
  request hangup" against the trunk's active channels), not just new calls
  - closing the gap where an in-progress call just kept running until it
  ended naturally. Runs every check while tripped, not only the run that
  trips it, to close a start-of-call race.
- Fixed a bug caught by testing: the hangup sweep was nested inside the
  "does the call log exist" guard, so it silently never fired if the log
  was missing/rotated while tripped. Moved it outside that guard.
- Documented an honest "how bulletproof is this?" breakdown in the
  generated README: the provider's own real-time $0-balance block is the
  actual hard ceiling (confirmed for Anveo Direct), the kill-switch here is
  an estimate-based second layer, not a replacement for funding the prepaid
  account at your actual risk tolerance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
2026-07-22 15:52:08 +00:00
Claude 9b950808cc Add spend-cap kill-switch, international-calling toggle, per-minute checks
- Replace the hourly cron.d spend/volume check with a per-minute systemd
  timer (cron.d fallback), since it's now also the kill-switch's
  enforcement point and the interval is the exposure window.
- Add a hard monthly spend-cap kill-switch: trips pstn-trunk-killswitch.conf
  (read live by the dialplan, both call directions, internal calling
  unaffected) once estimated spend reaches an admin-set cap, plus a loud
  ntfy warning at 80% of the cap before it trips. Does not auto-reset;
  clearing it is a CLI-only action (update mode) so a compromised/careless
  web session can't quietly re-enable spend after a trip.
- Add a CLI-only (never web-dashboard) international-calling allow-list:
  continent/country menu, always asked every run with the resulting
  allow-list printed exactly once right after, optional auto-expiry with
  day-of and at-expiry ntfy notices, and active re-blocking once expired.
  Dialplan gates the US "011" prefix pattern on it for full-tier extensions.
- Add an independent messaging_enabled flag per extension in
  pstn-permissions.conf for Asterisk's native SIP MESSAGE texting, since
  the risk profile (no cost) differs from PSTN calling permissions. The
  dialplan wiring to enforce it is flagged as a known gap pending live
  verification against Easy Asterisk's message routing, rather than
  shipping a guessed pattern.
- security-dashboard.sh: detect whether pstn-trunk is actually installed
  (not just base Asterisk) and show a clear "not installed" state instead
  of misleading default caps/extensions; add clickable column-header
  sorting to the CrowdSec active-bans table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
2026-07-22 14:51:19 +00:00
Claude 26f3f08304 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.
2026-07-22 13:22:46 +00:00
Claude c8cc2271a7 Support providers with multiple inbound signaling IPs (e.g. Anveo Direct)
VoIP.ms's one-POP-one-hostname model meant a single resolved IP was enough
for the trunk's identify match=, but Anveo Direct (researched as an
alternative after finding VoIP.ms's ToS selectively requires government ID
at signup) sends inbound signaling from a fixed set of published IPs
instead. Generalizes the pjsip include to accept multiple match= lines in
one identify object (PJSIP allows repeating it to build one match set - no
separate config objects needed per IP), with a new install prompt for any
additional known source IPs beyond the auto-resolved one.

No dial-prefix support was added despite earlier research suggesting Anveo
Direct needed one - verified against their current official FAQ (fetched
directly rather than trusting a stale search-result claim) that plain
number dialing works with no prefix.

Fixed a real bug caught while testing the update-mode round trip:
.pstn-trunk.env was written with unquoted values, which breaks `source`
entirely once any value contains a space - a multi-IP list or a
multi-word provider name (e.g. "Anveo Direct") would make bash try to run
the second word as a command. Predates this change (a multi-extension
ring group has the same shape); only surfaced by actually re-sourcing the
file and reapplying, not by inspecting generator output. Fixed by quoting
every value.

Updated docs/pstn-calling-voipms-plan.md with the Anveo Direct/DIDLogic/
SIP.US research findings and what's now implemented vs. still open
(real-time balance enforcement at Anveo specifically is unconfirmed).
2026-07-22 03:58:41 +00:00
Claude 876fd6553b Add inbound concurrency cap, bump defaults to 10/10, make caps live-editable
Adds an inbound concurrent-call cap mirroring the existing outbound one -
outbound alone didn't protect against an inbound call-flood, which also
costs money per-minute on VoIP.ms. Both defaults bumped from 3 to 10.

Moves the cap numbers themselves out of static dialplan text and into a new
pstn-limits.conf, read live via AST_CONFIG() the same way permission tiers
already are - changing either cap takes effect on the next call, no
Asterisk restart, no reinstall. "update in place" never touches this file,
matching the existing protection for pstn-permissions.conf/.env/firewall/
Caddy config.

Adds a concurrency-caps card to security-dashboard.sh's "PSTN Trunk" tab,
above the existing permissions table, so both caps are visible and editable
from the same web page. Tested against a real running instance of the
Python app: default fallback when the file doesn't exist yet, save/persist,
invalid-input rejection, and a bash-to-Python round trip on the generated
file format.

Inbound dialplan ordering mirrors outbound's existing pattern: permission
check (is any ring-group member authorized for this caller) before the
concurrency check, consistent with outbound's tier-check-then-busy-check
order.
2026-07-22 00:49:41 +00:00
Claude 3bd952e55d PSTN trunk: 3-tier live permissions + Security Dashboard web UI + dual target
Reworks the outbound permission model from a flat allow-list into three
per-extension tiers (internal / restricted / full), addressing the ask for
extensions that can only reach pre-approved numbers plus extensions with
full US calling, while internal extension-to-extension dialing and ring
groups stay ungated for everyone regardless of tier.

Permissions now live in pstn-permissions.conf, read by the dialplan via
Asterisk's AST_CONFIG() on every call instead of being baked into static
dialplan text - editing that file takes effect on the next call, no
Asterisk restart and no re-running the installer. "update in place" mode
never touches this file (same protection this repo's update-mode
convention already gives .env/firewall/Caddy config); only a "fresh"
reinstall (with confirmation) or the web UI change it.

Adds a "PSTN Trunk" tab to services/security-dashboard.sh: lists every
extension (parsed from pjsip.conf) with its live tier and approved numbers,
editable with no restart - this is what makes the tier model actually
manageable day to day. Extracted the dashboard's systemd-unit writing into
its own function so "update" mode refreshes it too (previously only fresh
installs did), and generalized both the dashboard and the trunk service to
detect either asterisk-digital-ocean or the home/LAN asterisk install.

Inbound ring-group membership now checks each member's tier live per call
via an unrolled per-member dialplan block (full always rings, restricted
only if the caller's number is approved, internal never rings) rather than
a single static Dial() string.

Caught and fixed two real bugs during testing against a sandboxed vendor
copy and a live instance of the (stdlib-only) Python dashboard app:
- Asterisk Goto/GotoIf argument parsing: ring<ext>/skip<ext> are named
  priorities within the same extension (declared via "same => n(label),..."),
  not separate exten => entries, so jumping to them needs the single-argument
  Goto(label) form - the two-argument Goto(label,1) form used initially
  addresses a different, nonexistent extension named "label" instead.
- A security-relevant REGEX() direction issue: the inbound Caller-ID check
  initially interpolated attacker-influenced call data into the PATTERN side
  of a REGEX() match rather than the tested-string side, which would let a
  crafted Caller-ID forge a match against an unrelated approved-numbers
  entry. Fixed by keeping the admin-controlled approved-list as the pattern
  and the live call data as the string being tested, consistently on both
  the outbound and inbound checks.

Verified end-to-end: dialplan/pjsip generation and vendor-file patching
(idempotent, syntax-checked) as before, plus the new permission-file
round-trip between bash and Python, and the dashboard's new API endpoints
exercised against a real running Python server (extension parsing, tier
changes, number normalization, invalid-input rejection, atomic file writes).
2026-07-21 23:59:50 +00:00
Claude 1e2a3743ab Rework PSTN trunk: role-based permissions, ring-groups, ntfy spend alerts
Generalizes services/pstn-trunk.sh (renamed from voipms-trunk.sh in the
prior commit) away from VoIP.ms specifics - any IP-authenticated SIP
provider works, VoIP.ms is just the suggested default. Adds:

- Role-based outbound permission: a configurable allow-list of extensions
  that may dial PSTN numbers (regex-gated on CHANNEL(peername)), separate
  from internal extension-to-extension dialing which stays open to everyone
  regardless. Blank list preserves the original "everyone can dial out"
  behavior.
- Inbound ring-group: rings a configurable list of extensions instead of a
  single hardcoded one.
- ntfy alerts: immediate on denied (unauthorized extension) or rejected
  (concurrency cap hit) calls, plus an hourly cron-driven check that alerts
  once per month when estimated spend crosses a threshold and every hour
  call volume looks like a burst. Uses a self-contained pipe-delimited call
  log rather than Asterisk's CDR, to avoid depending on CDR module
  availability and CSV comma-quoting.
- Settings persisted to .pstn-trunk.env so "update in place" reapplies
  everything from that file instead of fragile re-parsing out of generated
  Asterisk config (which had a real bug: update mode was extracting the
  wrong Dial(PJSIP/...) line).

Tested end-to-end against a sandboxed copy of the real vendor files:
permission-gate regex, ring-group dial-string construction, ntfy line
injection/removal, and the usage-alert script's threshold/burst/monthly-
dedup logic all verified with synthetic data. Caught and fixed a sed `&`
escaping bug in the ring-group substitution before it shipped (RING_DIAL
contains literal `&` join characters, which sed's replacement syntax
otherwise treats as "insert the match").
2026-07-21 23:38:08 +00:00
Claude 6b0e378123 Rework PSTN trunk: role-based permissions, ring-groups, ntfy spend alerts
Renames services/voipms-trunk.sh to services/pstn-trunk.sh and generalizes
it away from VoIP.ms specifics - any IP-authenticated SIP provider works,
VoIP.ms is just the suggested default. Adds:

- Role-based outbound permission: a configurable allow-list of extensions
  that may dial PSTN numbers (regex-gated on CHANNEL(peername)), separate
  from internal extension-to-extension dialing which stays open to everyone
  regardless. Blank list preserves the original "everyone can dial out"
  behavior.
- Inbound ring-group: rings a configurable list of extensions instead of a
  single hardcoded one.
- ntfy alerts: immediate on denied (unauthorized extension) or rejected
  (concurrency cap hit) calls, plus an hourly cron-driven check that alerts
  once per month when estimated spend crosses a threshold and every hour
  call volume looks like a burst. Uses a self-contained pipe-delimited call
  log rather than Asterisk's CDR, to avoid depending on CDR module
  availability and CSV comma-quoting.
- Settings persisted to .pstn-trunk.env so "update in place" reapplies
  everything from that file instead of fragile re-parsing out of generated
  Asterisk config (which had a real bug: update mode was extracting the
  wrong Dial(PJSIP/...) line).

Tested end-to-end against a sandboxed copy of the real vendor files:
permission-gate regex, ring-group dial-string construction, ntfy line
injection/removal, and the usage-alert script's threshold/burst/monthly-
dedup logic all verified with synthetic data. Caught and fixed a sed `&`
escaping bug in the ring-group substitution before it shipped (RING_DIAL
contains literal `&` join characters, which sed's replacement syntax
otherwise treats as "insert the match").
2026-07-21 23:37:27 +00:00