Commit Graph
638 Commits
Author SHA1 Message Date
Outis 73b6a7ab20 Merge pull request #211 from outis1one/claude/sip-voip-integration-atsins
Claude/sip voip integration atsins
2026-07-23 11:05:17 -04:00
Claude 3661c8fbca Wire up internal SIP MESSAGE enforcement using a dedicated dialplan context
Confirmed against a live install's pjsip.conf/extensions.conf that every
endpoint falls back to context=intercom for messaging (message_context
blank), and that [intercom] owns one exact-match per-device dial pattern
regenerated on every dialplan rebuild. Rather than risk racing that, every
endpoint now gets message_context=sip-messaging (patched into both of Easy
Asterisk's device-creation code paths, plus a one-time migration for
existing devices), routing messages to their own [sip-messaging] context
gated on the existing pstn-permissions.conf messaging flag.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
2026-07-23 15:03:03 +00:00
Claude b10a626b22 Fix ntfy deny-all default, add crowdsec update mode, presence alerts, embedded Asterisk admin tab
- ntfy.sh: auth-default-access was deny-all, silently blocking both publish
  and anonymous subscribe on every topic once an auth-file is present.
  Default to read-write (same model as public ntfy.sh) so alerts from
  crowdsec.sh/pstn-trunk.sh actually get delivered.
- crowdsec.sh: add update/fresh/cancel reinstall gating — reruns previously
  re-asked every optional prompt (ASN exempt, geo-allowlist, ntfy, remote
  LAPI) unconditionally with no way to just refresh in place.
- asterisk-digital-ocean.sh: add optional ntfy alerts on extension
  registration going offline/online, checked every 2 minutes via systemd
  timer (cron.d fallback), offered on both fresh install and update.
- security-dashboard.sh: replace the outbound-only Asterisk Web Admin link
  with an embedded, lazy-loaded iframe tab, with a best-effort Caddy
  frame-ancestors patch and an always-available "open in new tab" fallback.
2026-07-23 14:15:17 +00:00
Outis 542b793b9a Merge pull request #210 from outis1one/claude/sip-voip-integration-atsins
Add named extension groups and Security Log column filtering
2026-07-23 08:33:35 -04:00
Claude 3359898b4e Add named extension groups and Security Log column filtering
Groups: a new always-available "Groups" card in the PSTN tab (same place
as Internal SIP messaging, no dependency on a PSTN trunk being installed)
lets you name a set of extensions and bulk-enable/disable messaging for
all of them at once. Purely a management-layer convenience - pstn-groups.conf
is never read by the dialplan, which only ever looks at per-extension keys
in pstn-permissions.conf. Applying a group action just calls the same
write_messaging() each individual checkbox uses, once per current member.
Editing membership never retroactively changes anything already applied,
and deleting a group never touches members' own settings - confirmed with
tests covering create/apply/edit-membership/re-apply/delete.

Security Log: added a per-column filter row (Time/Event/Account/Remote/
Severity), live as you type, filtering client-side against the
already-fetched events rather than re-querying - persists correctly across
the existing 30-second auto-refresh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
2026-07-23 06:05:37 +00:00
Outis e6bc51061d Merge pull request #209 from outis1one/claude/sip-voip-integration-atsins
Claude/sip voip integration atsins
2026-07-23 01:49:41 -04: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
Outis a82f450c0a Merge pull request #208 from outis1one/claude/sip-voip-integration-atsins
Claude/sip voip integration atsins
2026-07-23 00:46:01 -04:00
Claude 616dcdcfbb Fix Security Log tab loading the entire multi-GB log into memory
parse_security_log() did f.readlines() on ASTERISK_LOG before slicing the
last 5000 lines - that reads the ENTIRE file into memory first. That log
is Asterisk's unrotated console/security output, and this tab polls it
every 30 seconds from the browser. Confirmed live: on a 1GB-RAM droplet
with a 1.4GB log file, this ballooned the dashboard (explicitly meant to
be a lightweight stdlib-only process) to 677MB RSS / 1.8GB peak swap,
which left CrowdSec unable to even start (boot timeout) and directly
contributed to the droplet becoming unresponsive.

Fixed by reading only a bounded ~2MB tail from the end of the file
(seek + fixed-size read) instead of the whole thing - memory use is now
constant regardless of how large the log grows. Tested against a 180MB
synthetic log: memory delta dropped from being proportional to file size
to ~7MB, in 0.05s.

Also added log rotation (services/asterisk-digital-ocean.sh) for that same
file, which had no rotation at all and reached 1.4GB in about 3 days -
copytruncate avoids needing to signal the containerized Asterisk process
to reopen its log handle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
2026-07-23 04:44:19 +00:00
Claude 78456e6a3a Fix ASN-exempt fork never actually disabling the hub-original scenarios
"cscli scenarios remove crowdsecurity/asterisk_bf crowdsecurity/asterisk_user_enum"
was failing silently (stderr suppressed, "|| true" swallowed the non-zero
exit) because these scenarios are members of the crowdsecurity/asterisk
collection, and cscli refuses to remove/disable a collection member
without --force. The un-exempted hub originals kept running side-by-side
with the ASN-exempt local forks the entire time, independently banning the
same traffic with no ASN awareness at all - confirmed live by a ban
showing scenario "crowdsecurity/asterisk_bf" (the hub original) rather
than "local/asterisk_bf" (the fork) for an ASN that had just been exempted.

Fixed in two places:
- crowdsec.sh's initial setup now passes --force and surfaces a clear
  warning (with the manual fix command) if it still fails, instead of
  silently continuing.
- security-dashboard's set-asn-exempt.sh helper now re-asserts the forced
  disable on every ASN-list save, so an install already affected by this
  (from before this fix, or where the step failed for any other reason)
  self-heals the next time the ASN list is edited via the dashboard,
  without needing to re-run the full CrowdSec installer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
2026-07-23 03:44:19 +00:00
Outis 0464a98ad5 Merge pull request #207 from outis1one/claude/sip-voip-integration-atsins
Fix ASN-exempt whitelist silently failing with Permission denied
2026-07-22 23:32:55 -04:00
Claude 1f7f89b984 Fix ASN-exempt whitelist silently failing with Permission denied
set_asn_exempt() was writing /etc/crowdsec/scenarios/local-asterisk_*.yaml
directly from the dashboard's own process, but those files are root:root
mode 644 and the secdash service user (--shell /usr/sbin/nologin) has no
write grant to them - confirmed by the reported "[Errno 13] Permission
denied" error. Every whitelist/unban attempt through the dashboard was
silently a no-op as far as CrowdSec was concerned, which is why a
carrier's IP kept getting re-banned even after repeatedly trying to
exempt its ASN.

Fixed by routing the edit through a dedicated root-owned helper script
(set-asn-exempt.sh, installed alongside app.py, root:root mode 700) that's
the one thing added to the sudoers whitelist - same pattern every other
CrowdSec-touching action here already uses (cscli via sudo), rather than
loosening the scenario files' own permissions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
2026-07-23 03:29:51 +00:00
Outis c3b4bd2990 Merge pull request #206 from outis1one/claude/sip-voip-integration-atsins
Add per-country international rates and active-hangup on kill-switch …
2026-07-22 12:42:12 -04: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
Outis 3d7ba6bf74 Merge pull request #205 from outis1one/claude/sip-voip-integration-atsins
Claude/sip voip integration atsins
2026-07-22 11:35:01 -04:00
Claude f9113b275b docs: confirm Anveo Direct real-time balance blocking and signaling IPs
Support reply (MFonk, 7/22/2026) confirms both open questions: all calls
(incoming and outgoing) are blocked in real time at $0 balance, not just
via the recurring-fee grace-period mechanism, and the 4 signaling IPs
already hardcoded into this doc's research notes are still current.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
2026-07-22 15:26:10 +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 8e2552ae73 Reconcile VoIP.ms ToS negative-balance language with the toll-fraud design
The user found ToS language stating the account "may run on a negative
balance" and describing a 30-day-notice-then-suspend / 30-consecutive-
days-then-close process - worth reading against the wiki's "balance must
be over $0 to call" claim the whole toll-fraud design leans on.

Reconciled: these describe two different things, not a contradiction.
New call attempts should still be blocked in real time at $0 (the wiki's
claim, and the core assumption this design needs). The ToS's negative-
balance language most plausibly covers recurring fees (DID/E911) landing
when balance is already near zero, and in-progress-call settlement edge
cases - not a window where fraud keeps dialing while negative. Still not
verified against a live account either way.
2026-07-22 02:27:26 +00:00
Outis d0c0fc38cd Merge pull request #204 from outis1one/claude/sip-voip-integration-atsins
Claude/sip voip integration atsins
2026-07-21 21:51:33 -04: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
Claude 2ac2982e38 Add voipms-trunk service: US-only outbound PSTN, max 3 concurrent calls
Adds a VoIP.ms SIP trunk on top of asterisk-digital-ocean: IP-authenticated
trunk (no password stored), NANP-only outbound dialplan, a global 3-call
concurrent cap via GROUP()/GROUP_COUNT(), and inbound routing to one
extension. Config lives in its own include files rather than being
appended directly to pjsip.conf/extensions.conf, since Easy Asterisk fully
regenerates both from its own internal state — the includes are patched
into the vendor's generator functions so they survive that regeneration.

Wires the new service into setup.sh's is_installed() and README's services
table, and updates docs/pstn-calling-voipms-plan.md to reflect what's now
implemented vs. still open (spend/volume alerting, live-account
verification).
2026-07-21 23:13:59 +00:00
Claude 719f9117be Record inbound decision and cost estimate in VoIP.ms planning doc
Inbound (DID) is now decided as wanted, not outbound-only. Adds a cost
estimate table for 100 min/month each direction, and clarifies that
NANP-only restriction bounds cost-per-minute but not burn speed, so the
concurrent-call cap and spend alert are required before funding a live
trunk, not optional hardening.
2026-07-21 22:41:22 +00:00
Outis 5f60cc1e14 Merge pull request #203 from outis1one/claude/asterisk-digital-ocean-w22kk8
docs: add PSTN calling (VoIP.ms) planning notes
2026-07-21 18:07:31 -04:00
Claude ed5b2b8daa docs: add PSTN calling (VoIP.ms) planning notes
Captures the toll-fraud / prepaid-cap research and decisions from a design
discussion (provider: VoIP.ms, US-only calling for now) so implementation
can be picked up in a future session without re-deriving the background.
Nothing implemented yet.
2026-07-21 22:04:15 +00:00
Outis e995ec58b3 Merge pull request #202 from outis1one/claude/asterisk-digital-ocean-w22kk8
Claude/asterisk digital ocean w22kk8
2026-07-21 17:54:29 -04:00
Claude 78f4202386 security-dashboard: unwhitelist/ban ASNs, independent Basic Auth layer
- CrowdSec tab: per-ASN "Unwhitelist" (drop from the exempt filter, future
  traffic evaluated normally) and "Unwhitelist + Ban" (also immediately
  bans every IP CrowdSec has on record for that ASN, for accidental-
  whitelist cases) buttons. set_asn_exempt now allows clearing the list
  down to zero ASNs, needed to unwhitelist the last remaining entry.
- New sudoers permission (cscli decisions add --ip * --duration * --type
  ban --reason *) scoped narrowly, list-form subprocess args only.
- Caddy/Authelia config factored into _secdash_configure_caddy() and a new
  _secdash_remove_caddy_block() (whole-block delete-and-regenerate, not
  in-place patching) so "update" mode can now offer to reconfigure it.
- Installer offers an independent HTTP Basic Auth layer in front of
  Authelia (Caddy basicauth, generated via `caddy hash-password`) so a
  future Authelia bug/misconfig alone isn't enough to expose a page that
  can delete active security bans.
2026-07-21 21:29:32 +00:00
Claude 9770232227 security-dashboard: label ASN exemptions from alert history too
Currently-exempt ASNs with no active ban (e.g. T-Mobile once its bans
stop firing) had no carrier name to show, since the name lookup only
looked at cscli decisions list (active bans only). Add a
cscli alerts list-based fallback (includes expired/resolved alerts)
and merge it into the name lookup used by /api/asn-exempt.
2026-07-21 21:19:45 +00:00
Outis 796fb99824 Merge pull request #201 from outis1one/claude/asterisk-digital-ocean-w22kk8
Show carrier/ASN and country in the CrowdSec bans table
2026-07-21 17:13:17 -04:00
Claude 9e98c90a57 Show carrier/ASN and country in the CrowdSec bans table
Confirmed the real cscli decisions list -o json structure live rather
than guessing again: AS number/name and country live on each alert's
"source" object (source.as_number, source.as_name, source.cn), not on
the individual decision. Surfaces this as a "Network / Carrier" and
"Country" column in the bans table, adds a per-row "Exempt ASN" button
that appends straight to the Asterisk brute-force ASN exemption list,
and labels the exempt list's own entries with carrier names (pulled
from current ban data where available) instead of showing bare numbers.
2026-07-21 21:07:41 +00:00
Outis bdcb32dffb Merge pull request #200 from outis1one/claude/asterisk-digital-ocean-w22kk8
Fix security-dashboard binding to 127.0.0.1 instead of 0.0.0.0
2026-07-21 16:59:14 -04:00
Claude 880bba86ed Fix security-dashboard binding to 127.0.0.1 instead of 0.0.0.0
Confirmed live: Caddy (in a container) reaches this via
host.docker.internal, a Docker bridge gateway IP, not localhost — a
loopback-only bind refuses that connection outright ("dial tcp
172.17.0.1:8092: connect: connection refused"), even though curl from
the host itself worked fine on 127.0.0.1. Bind to 0.0.0.0 and rely on
UFW for the actual access scoping instead, matching every other
host-network service in this repo (e.g. the Asterisk web admin, which
already binds this way successfully with the same
ufw_allow_from_caddy_net pattern).
2026-07-21 20:34:06 +00:00
Outis b75241fda4 Merge pull request #199 from outis1one/claude/asterisk-digital-ocean-w22kk8
Fix install function name mismatch for security-dashboard
2026-07-21 16:21:35 -04:00
Claude f45fdc1c96 Fix install function name mismatch for security-dashboard
Confirmed live: setup.sh's run_service() calls install_${name} with
no hyphen-to-underscore conversion, so a hyphenated service name needs
a literally-hyphenated function name (install_security-dashboard, not
install_security_dashboard) to be found at all — got this wrong on
first pass by following CLAUDE.md's own (incorrect) guidance, which
said to convert hyphens to underscores. Every other hyphenated service
in the repo (asterisk-digital-ocean, wolf-pair, mail-archiver,
drum-rhythm-game) already keeps hyphens literal; corrected CLAUDE.md
to match actual practice instead of the other way around.
2026-07-21 20:19:58 +00:00
Outis 4d4b775e2a Merge pull request #198 from outis1one/claude/asterisk-digital-ocean-w22kk8
Add security-dashboard: Asterisk failed-connections + CrowdSec bans, …
2026-07-21 16:16:43 -04:00
Claude 009aaa017b Add security-dashboard: Asterisk failed-connections + CrowdSec bans, one page
New service, native on the host (not Docker) so it can call cscli and
read Asterisk's security log directly without bridging the
container/host boundary or exposing CrowdSec LAPI credentials to a
containerized frontend.

- Security Log tab: parses ~/docker/asterisk-digital-ocean/logs/full
  for SIP auth failures (wrong password, unknown extension, etc.) with
  timestamp/account/remote IP, classified by severity.
- CrowdSec tab: current bans via cscli, a delete/unban button per
  entry, and ASN-exempt management for the Asterisk brute-force
  scenarios (services/crowdsec.sh) without SSHing in.
- Link out to the existing Asterisk web admin (reads its domain from
  asterisk-digital-ocean's own .env, doesn't hardcode or embed it).

Runs as a dedicated unprivileged system user (secdash), with sudo
scoped to exactly three commands via /etc/sudoers.d/security-dashboard
(cscli decisions delete --id <digits>, cscli decisions list -o json,
systemctl restart crowdsec) — validated with visudo -c. Listens on
127.0.0.1 only, reachable through Caddy, and refuses to proceed without
explicit confirmation if no Authelia (local or remote) is configured,
since this page can delete active security bans.

Stdlib-only Python (no framework), matching the RAM-conscious pattern
already used for Easy Asterisk's own web admin. All embedded code
(bash, Python, JS) syntax-checked; the generated sudoers rule
validated with visudo -c -f.
2026-07-21 13:10:04 +00:00
Outis 72c75f5577 Merge pull request #197 from outis1one/claude/asterisk-digital-ocean-w22kk8
Fix fatal CrowdSec crash-loop in ASN-exempt Asterisk scenarios
2026-07-21 08:58:01 -04:00
Claude c7dac67234 Fix fatal CrowdSec crash-loop in ASN-exempt Asterisk scenarios
Confirmed live: unquoted integer literals in the ASN exclusion filter
(evt.Enriched.ASNNumber in [21928, 14593]) made CrowdSec fatal-crash-loop
at startup with "cannot use string as type int in array" — ASNNumber is
a string field internally despite printing as a bare number in cscli
output, same as IsoCode in the geo-allowlist scenario. Quote each ASN
as a string to match, exactly like the working geo-allowlist pattern.
2026-07-21 12:57:00 +00:00
Outis ddd49585b3 Merge pull request #196 from outis1one/claude/asterisk-digital-ocean-w22kk8
Claude/asterisk digital ocean w22kk8
2026-07-21 08:48:50 -04:00
Claude c2d74f9520 Add optional ASN-exempt Asterisk brute-force scenarios to crowdsec.sh
Confirmed live: a phone roaming WiFi<->mobile on a CGNAT carrier
(Starlink, T-Mobile home internet) got banned by crowdsecurity/asterisk_bf,
either from its own re-registration burst or collaterally from another
customer sharing the same rotating public IP. Forks asterisk_bf and
asterisk_user_enum locally with an ASN exclusion added to their filter,
disabling the hub originals so events aren't double-processed. Scoped
narrowly to Asterisk auth-failure detection only — SSH, web scanning,
and the geo-allowlist scenario are all unaffected, so this doesn't
broadly exempt the carrier from every protection on the box.
2026-07-21 12:47:48 +00:00
Claude 6dd5bbc2ce Include the check/unban commands directly in CrowdSec ntfy alerts
Confirmed live: a legitimate SIP device on a CGNAT ISP (Starlink,
T-Mobile home internet) got collaterally banned by
crowdsecurity/asterisk_bf alongside actual bad actors sharing the same
carrier IP. The alert now includes the exact commands (with the banned
IP substituted in) instead of just naming the ban, so recovering from
this doesn't require remembering or looking up cscli syntax.
2026-07-21 12:36:02 +00:00
Outis 597ac9827a Merge pull request #195 from outis1one/claude/asterisk-digital-ocean-w22kk8
Claude/asterisk digital ocean w22kk8
2026-07-20 23:12:44 -04:00
Claude f7db82333a Add authelia.sh support for protecting multiple apex domains
New "Add another protected domain to this instance" option on re-run,
via add_authelia_domain(): appends a session.cookies entry and an
access_control.rules entry (both YAML lists Authelia natively supports)
plus a Caddy auth.<domain> portal block for the new domain, all on the
same Authelia + Redis container instead of standing up a second full
stack. Each domain gets its own login/session, sharing one user
database — the right fit when a single (possibly upsized) droplet ends
up fronting more than one domain, without doubling the RAM cost of a
second Authelia+Redis instance. Documents both this and the
already-working separate-instance path in CLAUDE.md, with the
per-approach tradeoffs.
2026-07-21 03:03:33 +00:00