Available Now

FreePBX Custom Admin Module

Six independently-toggleable, system-level fixes for FreePBX 16/17 — the kind that normally require editing config files by hand and remembering to re-apply them after every Asterisk upgrade. One admin page, each utility opt-in, each with its own on/off switch, each self-healing on every fwconsole reload so the fix survives upgrades.

Why This Exists

Every production FreePBX fleet ends up with a shared folder of operator scripts and fwconsole snippets that fix the same handful of recurring problems: trunks going unreachable after NAT rebind, mobile endpoints getting fail2banned on their roaming IPs, stuck TCP sockets after a WAN blackout, Asterisk suddenly dropping every SIP registration under heavy call volume. Each one has a known fix, documented across a dozen forum threads, that nobody wants to maintain by hand across 20 PBXes.

Custom Admin bundles those fixes into one FreePBX admin page. Each is an independent on/off toggle, each is self-healing (survives Asterisk and FreePBX upgrades that would otherwise overwrite the hand-edits), and each is cleanly reversible: turning it off strips the change without touching anything the operator added themselves.

Six Utilities, One Admin Page

  • Stasis Overload Mitigation — stops SIP mass-dereg under heavy call volume
  • Restart Asterisk when idle — one-click restart with zero dropped calls
  • Dynamic Endpoint Trust — auto-whitelists registered SIP endpoints
  • Stale TCP Socket Watchdog — kills ghost SIP-TCP sockets
  • PJSIP Receive-Trunk AOR Fix — survives NAT rebind on inbound trunks
  • Read Caller ID Number — diagnostic feature code + Custom Destination

Compatibility: FreePBX 16 and 17 with PJSIP. MIT licensed, no license keys, no phone-home.

1. Stasis Overload Mitigation

Fixes a real failure mode where every peer's SIP registration drops under heavy call volume.

What breaks without the fix

Asterisk's PJSIP channel driver uses a shared "taskprocessor overload" signal to tell SIP intake to pause when the system is under pressure. The default trigger (taskprocessor_overload_trigger=global) means any overloaded taskprocessor in Asterisk — Stasis, bridges, ARI, CDR — pauses PJSIP's REGISTER handling. During a call-volume spike, Stasis thread churn can trip the global signal and knock SIP intake offline for long enough that every peer's REGISTER expires and the whole fleet marks itself unreachable.

Separately, the stock Stasis threadpool ships with initial_size=0, which means the pool grows and shrinks on every call. Under volume this floods the stasis/pool-control taskprocessor — the very thing that then trips the global overload trigger. The two problems reinforce each other.

What the fix does

Writes two sentinel-wrapped config blocks:

  • /etc/asterisk/pjsip_custom.conf → adds [system] taskprocessor_overload_trigger=pjsip_only — narrows the trigger to PJSIP's own taskprocessors, so a Stasis spike no longer knocks SIP off the air
  • /etc/asterisk/stasis.conf → adds [threadpool] initial_size=10, idle_timeout_sec=60, max_size=100 — pre-warms the pool so pool-control doesn't flood during normal operation

One deployment saw the Stasis queue depth drop from ∼1,700 to ∼16 after this was enabled.

Survives upgrades

Both files are already-standard override paths. pjsip_custom.conf is FreePBX's documented #include target for pjsip overrides; stasis.conf is a stock Asterisk file FreePBX does not regenerate. Self-heals from doDialplanHook on every reload — if an Asterisk upgrade rewrites stasis.conf, the sentinel block is re-injected on the next Apply Config.

Fully reversible

The two blocks are sentinel-wrapped. Disabling the toggle strips them cleanly, leaving the rest of each file untouched. Any operator content you added elsewhere in pjsip_custom.conf or stasis.conf stays exactly where it was.

Note: Asterisk reads [system] and [threadpool] once, at startup — a plain fwconsole reload does not pick them up. One fwconsole restart is required to activate; the panel's companion utility (next section) makes that painless.

2. Restart Asterisk When Idle

One-click restart that waits for zero active calls — no dropped conversations.

The problem

Several of the fixes bundled in this module (the Stasis Mitigation above, plus any ad-hoc Asterisk config change) only take effect after a full Asterisk restart — fwconsole reload is not enough. Done at the wrong moment, fwconsole restart drops every active call. Done manually with asterisk -rx "core show channels count" in a terminal, it means someone has to sit there and watch channel count until it's safe.

The restart-when-idle button queues the restart and returns you to work. A tiny background watcher polls channel count every 10 seconds and runs fwconsole restart as soon as it sees two consecutive zero readings — or gives up after a configurable max-wait window.

How it works

  • Spawns one detached bash watcher process. No crontab, no systemd timer
  • Polls asterisk -rx "core show channels count" every 10 s
  • Two consecutive zero readings before firing — guards against the one-sample race where a call lands during the gap
  • Fires sudo -n /usr/sbin/fwconsole restart via a narrow sudoers rule
  • Max-wait options: 5 / 15 / 30 / 60 / 120 min — if nothing idles out, the watcher logs timed_out and exits
  • Cancel path: writes a sentinel file; watcher respects on next tick
  • One watcher at a time — double-click guard built in

Smart UI

The restart controls only appear when a change is actually pending. The panel compares the two mitigation files' mtimes against the running Asterisk's start time (read from /proc/<pid>/stat); once Asterisk is in sync with the on-disk state, the whole restart section collapses out of view. No "restart now?" nag on a system that's already live.

Now or when idle

One form with a Now | When idle radio. "Now" fires immediately via a detached wrapper (with a short pre-fire sleep so the HTTP response closes before Asterisk goes down). "When idle" queues the watcher. Both routes write to the same audit log so the panel always shows the last outcome.

3. Dynamic Endpoint Trust

Keeps real roaming SIP endpoints out of your firewall's crosshairs, automatically.

The problem

Mobile softphones, home-office extensions on dynamic IPs, and road warriors on cellular all hit the FreePBX Firewall and fail2ban as if they were attackers — getting throttled, banned, or filtered out of the "trusted" zone whenever their public IP changes. The usual fix is to add each endpoint's current IP to the Firewall's Trusted zone and the fail2ban ignoreip list, by hand, repeatedly.

Dynamic Endpoint Trust automates it. An every-minute cron script reads currently-registered SIP endpoint IPs from asterisk -rx "database show registrar" and syncs them into the FreePBX Firewall Trusted zone and fail2ban's ignore list. A 20-minute grace window gives endpoints time to re-register before their IPs are removed.

Safety guarantees

  • Only touches IPs it added itself — never removes operator-added trusted CIDRs
  • Ledger at /var/lib/dynamic-endpoint-trust/managed.list tracks every add/remove
  • Log at /var/log/dynamic-endpoint-trust.log for forensic review
  • Dry-run from CLI with --dry-run — see what would change without changing anything
  • Reads per-jail configuration (not just jails[0]) so drifted jails don't go unnoticed

4. Stale TCP Socket Watchdog

Kills ghost SIP-TCP sockets left behind by WAN blackouts, surgically.

The problem

When a TCP-registered phone's WAN link disappears mid-session (router reboot, ISP blip, cellular handoff), Asterisk is left holding a TCP socket whose Send-Q never drains. The phone comes back on a new port and tries to re-register, but the stale socket is still "live" from the kernel's perspective, blocking the new connection. Classic workaround is to restart Asterisk, which drops every call. Not acceptable.

The watchdog does the surgical thing: it uses ss -K to kill only the asterisk-owned SIP-TCP sockets whose Send-Q won't drain on a two-sample probe, and only when the peer isn't currently registered from a working connection at the same address. No restart, no unregister storm.

How it stays safe

  • Two-sample Send-Q probe — only kills sockets that are genuinely stuck
  • Peer IP must be unregistered or registered from a different working socket — never kills a working connection
  • Dry-run subtoggle: install the cron, log what would be killed, don't actually kill anything
  • Log at /var/log/stale-tcp-socket-watchdog.log — silent in steady state; logs only when it acts
  • UDP-only sites see this as a silent no-op — only meaningful where at least one PJSIP transport is -tcp or -tls

5. PJSIP Receive-Trunk AOR Fix

Fixes receive-registration trunks going unreachable after a NAT rebind.

The problem

FreePBX generates pjsip.aor.conf with max_contacts=1 on receive-registration trunk AORs, but without remove_existing=yes (core forces that flag on extension AORs; it doesn't force it on trunk AORs). The consequence shows up right after a NAT rebind — your upstream carrier's router reboots, their WAN reconnects, their new REGISTER arrives from a different source port, and PJSIP rejects it with "will exceed max contacts" instead of replacing the stale contact. The trunk stays unreachable until Asterisk is manually reloaded or the stale contact's TTL expires naturally.

The fix injects remove_existing=yes onto every receive-registration PJSIP trunk's AOR at config-generation time via core FreePBX's own PJSip->addAor() hook — so the value lands in pjsip.aor.conf when it's first written. No post-hoc file patching, no res_pjsip reload dance, no /usr/local/sbin/ helper script.

Why the hook, not the file

Earlier versions of this fix tried to patch pjsip.aor.conf on every reload. The problem: doDialplanHook runs during config generation, and FreePBX's retrieve_conf then overwrites pjsip.aor.conf with a fresh generation on every single reload — silently clobbering the patch. Using addAor() instead injects the value before the file is written, so it lands in the source of truth from the start. Only receive-registration trunks are touched: send-registration (outbound) trunks don't accept REGISTERs, so remove_existing is moot on them.

6. Read Caller ID Number

Diagnostic Custom Destination + Feature Code that speaks the calling number digit-by-digit.

What it does

Answers the call, waits half a second, speaks the calling number digit-by-digit via SayDigits(${CALLERID(num)}), pauses, speaks it again, hangs up. Handy for verifying CID rewrites on trunks and inbound routes — especially when a carrier claims they're sending the right number but you suspect NAT, PAI munging, or an outbound route rewrite is clipping it.

Two ways to reach it. First, as a Custom Destination in the Inbound Route → Destination dropdown under "Custom Admin" — drop it on a test DID and the carrier's own CID comes back verbatim over the audio. Second, as a Feature Code dialable from any internal extension (default *78) — useful for internal CID-rewrite verification without burning a DID.

Config surface

  • Destination label — whatever shows up in Inbound Route / IVR / Time Condition dropdowns
  • Dialplan extens name — defaults to s; change for cosmetic distinction
  • Feature code — managed in Admin → Feature Code Admin under Custom Admin; default *78

Under the Hood

A single admin page, a single config table, and a shared self-heal path.

Config Table

One k/v table — customadmin_config — carries every toggle state. The admin page reads it on load, each save handler writes to it, and applyToggles() re-composes the cron lines in the asterisk user's crontab from current state on every save.

Cron Model

Watchdog cron lines live in the asterisk user's own crontab (/var/spool/cron/asterisk) — writable by the FreePBX PHP-FPM worker without any privilege escalation. Runtime root privilege for the two scripts that need it is granted via a narrow /etc/sudoers.d/customadmin file.

Self-Heal

Every fwconsole reload fires doDialplanHook, which runs at root context. There it verifies the helper scripts and sudoers rules are in place and refreshes them if missing or outdated. A manifest-based safe-overwrite policy lets upgrades refresh shipped script copies while preserving any operator hand-edits.

Sentinel-Wrapped Edits

Every file this module touches — pjsip_custom.conf, stasis.conf, extensions_custom.conf — carries sentinel comments around our injected block. Disabling the toggle strips the sentinel-wrapped section cleanly; operator content elsewhere in the file is left exactly where it was.

Dialplan integration: uses FreePBX's own doDialplanHook and myDialplanHooks BMO contract, so there's no race with retrieve_conf rewriting generated files. The Read-CID extension is injected into extensions_custom.conf with sentinel comments; the AOR fix uses core's documented PJSip->addAor() hook API.

Installation

Install as a standard FreePBX module. Toggles are all off by default — opt in to the ones you want.

From Module Admin

Download the module

Download the tarball:

Upload in Module Admin

Go to Admin → Module Admin → Upload modules and upload the tarball. Or use the Download (From Web) option to pull it directly from the repo.

Install and Apply Config

Find "Custom Admin" in the module list, click Install, then Apply Config. The module self-signs on install and registers a voip-stuff.net repository address for future updates directly through FreePBX Module Admin.

Command Line

fwconsole ma downloadinstall customadmin
fwconsole reload

After installation, navigate to the Custom Admin admin page to toggle each utility. The admin page shows each utility's current status (disabled / active / pending restart) and offers immediate controls to apply, disable, or reset.

Uninstall removes every cron entry, helper script, sudoers rule, sentinel-wrapped config block, the AstDB tree, and the module's database tables — no manual cleanup needed.

Frequently Asked Questions

Does enabling everything carry a performance cost?

No. The two cron scripts (Dynamic Endpoint Trust, Stale TCP Socket Watchdog) run once per minute and finish in well under a second on a typical fleet. The Stasis mitigation is pure config — Asterisk reads it once at startup. The AOR fix is a config-generation hook with negligible overhead. The Read-CID extension is a dialplan addition that executes only when actually dialed.

Will the Stasis Overload Mitigation break anything else?

No reports so far across the deployments running it. The pjsip_only setting narrows which taskprocessors can trigger PJSIP intake pausing — it doesn't disable protection, just makes it more surgical. The threadpool pre-warm is a tuning parameter, not a behavior change. Both are documented Asterisk settings; the module just makes them easy to apply + survive upgrades.

Why does the Stasis mitigation need an Asterisk restart, not just a reload?

Asterisk reads [system] and [threadpool] sections once, at startup — a fwconsole reload doesn't re-read them. That's a core Asterisk design, not a module quirk. The restart-when-idle utility in this same module exists specifically to make that restart painless (zero dropped calls, one click).

Does the restart-when-idle use crontab?

No. One detached bash process per restart request, spawned via setsid nohup. The watcher's whole lifecycle is bounded by its own polling loop; it exits when it fires, is cancelled, or times out. No persistent scheduler, no systemd timer, no state to clean up afterward.

Does Dynamic Endpoint Trust open my PBX to abuse?

It auto-trusts the IPs of already-registered SIP endpoints — endpoints that have proven they know a valid extension's credentials. If an attacker has valid credentials they can get through regardless of whether their IP is "trusted" or not; the trust-zone addition just stops the firewall from throttling legitimate (if roaming) users. Pair it with the Device Lock module (also on this site) for an additional defense layer that blocks even credential-correct REGISTERs from unauthorized devices.

Will the Stale TCP Socket Watchdog ever kill a working phone?

The design works hard to prevent that. It requires two consecutive Send-Q non-drain samples (a working connection drains its queue between samples), joins live registrations against the socket table so a working peer at the same IP is never killed, and ships with a dry-run mode so you can watch what it would kill for a day before enabling act mode. On UDP-only sites it's a silent no-op.

Can I enable one utility and not the others?

Yes. Each utility has its own independent on/off toggle. The config table carries one row per toggle; the admin page renders each as a separate section. Enable only what you need.

Does it work with chan_sip?

The PJSIP-specific utilities (Stasis mitigation, Receive-Trunk AOR fix, Stale TCP Socket Watchdog on TCP transports) are PJSIP-only. The others (Dynamic Endpoint Trust, Read-CID) are protocol-agnostic. chan_sip is deprecated upstream in Asterisk and FreePBX, and we don't test against it.

Is the module free?

Yes. Released under the MIT license — free for commercial and personal use. No license keys, no subscriptions, no phone-home.

Get Custom Admin

Download the module and get six independent system-level FreePBX fixes behind one admin page.