Advanced ViciDial Inbound Call Flow Customization
Blacklists, CLI Filtering, Ring-Back Delay, Agent Ranking & Recording Security
Difficulty: Advanced | Time to implement: 4-8 hours | Asterisk version: 16+ recommended (tested on 18.x)
Table of Contents
- Introduction: Why Default ViciDial Inbound Handling Falls Short
- Complete Call Flow Architecture
- Call Filtering Pipeline
- Ring-Back Delay (Anti-Call-Center Detection)
- Agent Ranking System
- Recording Security & HTML5 Player
- Codec & Audio Quality Tuning
- Testing, Rollout & Monitoring
- Complete Reference Configuration
- Troubleshooting
1. Introduction
Out of the box, ViciDial handles inbound calls with a straightforward pipeline: a DID receives a call, it routes to an inbound group, and the next available agent answers. This works for basic operations, but professional call centers --- especially those handling emergency services, home trade inquiries, or high-value leads --- need significantly more control.
Problems with default ViciDial inbound handling:
- No call filtering. Every call reaches an agent, including spam, robocalls, anonymous callers, and recruiting companies farming your DIDs. Each wasted call costs 30-90 seconds of agent time and displaces a real customer.
- Instant answer reveals you are a call center. When a customer dials a local plumber's number, they expect a normal business phone to ring for 3-5 seconds before someone answers. ViciDial routes calls in under 1 second --- callers immediately sense something is off.
- Flat agent distribution. ViciDial's default
longest_wait_allrouting treats every agent equally. In reality, your best agents should handle calls first, and trainees should only receive overflow. - Recordings are publicly accessible. Default ViciDial serves recording files as direct downloads to anyone who knows (or guesses) the URL. For GDPR compliance and client confidentiality, you need access controls.
- Suboptimal codec negotiation. ViciDial's default codec order often forces unnecessary transcoding, adding latency and degrading audio quality.
This tutorial walks through every layer of a production-hardened inbound call flow, built from real-world configurations running thousands of calls per day.
What you will build:
Incoming call
|
v
[5-stage filtering pipeline] --> Reject spam, anonymous, non-mobile
|
v
[Ring-back delay 2-5s] --> Caller hears normal ringing
|
v
[ViciDial inbound group] --> Ranked agent routing
|
v
[Best available agent] --> Rank 5 first, rank 9 last
|
v
[Secure recording] --> IP-restricted, HTML5 player
Prerequisites:
- ViciDial installation with Asterisk 16+ (18.x recommended)
- Root SSH access to the ViciDial server
- MySQL/MariaDB admin access
- Basic familiarity with Asterisk dialplan syntax
- Apache with
mod_rewriteenabled
2. Complete Call Flow Architecture
PSTN / SIP Trunk
|
v
+-------------------+
| [trunkinbound] |
| context |
+-------------------+
|
+---------+---------+
| FILTER PIPELINE |
+---------+---------+
|
+---------------+---------------+
| | |
[BLACKLIST] [BLOCKED DID] [ANONYMOUS]
DB lookup Regex match No CLI check
| | |
+-------+-------+-------+-------+
| |
[UK MOBILE] [SPAM BLOCK]
07/+447 only Known recruiters
| |
+-------+-------+
|
PASS ALL FILTERS
|
v
+-------------------+
| [trunkinbound- |
| route] |
+-------------------+
|
+---------+---------+
| RING-BACK DELAY |
| RAND(2,5) seconds |
| Ringing() + Wait()|
+---------+---------+
|
v
+-------------------+
| agi-DID_route.agi |
| (ViciDial native) |
+-------------------+
|
did_route=EXTEN
extension=XXXXXX
|
v
+-------------------+
| [default] ctx |
| exten XXXXXX |
+-------------------+
|
v
+-------------------+
| Custom AGI |
| (optional: |
| repeat caller |
| detection, CRM |
| lookup, etc.) |
+-------------------+
|
+---------+---------+
| INBOUND GROUP |
| (ranked routing) |
+---------+---------+
|
+---------+---------+
| next_agent_call= |
| inbound_group_rank|
+---------+---------+
|
+---+-----------+-----------+---+
| | | | |
Rank 5 Rank 6 Rank 7 Rank 8 Rank 9
(best) (trainee)
| | | | |
v v v v v
[Agent] [Agent] [Agent] [Agent] [Agent]
|
v
+------------+
| MixMonitor |
| Recording |
+------------+
|
v
+-------------------+
| Apache protected |
| IP whitelist |
| HTML5 player |
+-------------------+
3. Call Filtering Pipeline
All filters live in the [trunkinbound] context of extensions.conf. This is the context that SIP trunk peers route inbound calls to (configured in the trunk's context= setting in sip.conf or sip-vicidial.conf).
Important: Backup First
Before editing any Asterisk configuration:
# Always create a timestamped backup
cp /etc/asterisk/extensions.conf /etc/asterisk/extensions.conf.bak.$(date +%Y%m%d)
3.1 Database-Driven Blacklist
Asterisk has a built-in blacklist function that checks the AstDB (internal Berkeley DB). This is the fastest check --- a simple key lookup with no external database query.
Adding numbers to the blacklist:
# Add a number to the Asterisk blacklist
asterisk -rx 'database put blacklist "441234567890" "1"'
# Verify it was added
asterisk -rx 'database show blacklist'
# Remove a number
asterisk -rx 'database del blacklist "441234567890"'
Dialplan implementation:
[trunkinbound]
; ============================================================
; STAGE 1: Database blacklist check
; Checks AstDB family "blacklist" for the caller's number.
; If found, play congestion tone and hang up.
; ============================================================
exten => _X.,1,Set(CALLERID(num)=${FILTER(+0123456789,${CALLERID(num)})})
exten => _X.,n,GotoIf(${BLACKLIST()}?blacklisted,s,1)
Blacklisted calls land here:
[blacklisted]
exten => s,1,NoOp(BLACKLISTED: ${CALLERID(num)} calling ${EXTEN})
exten => s,n,Log(WARNING,BLACKLISTED call from ${CALLERID(num)} to ${EXTEN} - rejected)
exten => s,n,Congestion(3)
exten => s,n,Hangup()
Why use AstDB instead of MySQL? Speed. AstDB is an in-process key-value store --- lookup takes microseconds. MySQL queries, even fast ones, require a network round-trip and connection overhead. For a per-call check, AstDB is the right choice.
Bulk-loading a blacklist from a file:
#!/bin/bash
# /usr/local/bin/load-blacklist.sh
# One number per line in blacklist.txt
while IFS= read -r number; do
number=$(echo "$number" | tr -d '[:space:]')
[ -z "$number" ] && continue
asterisk -rx "database put blacklist \"$number\" \"1\""
echo "Blacklisted: $number"
done < /path/to/blacklist.txt
3.2 Regex-Based Blocked DIDs
Some numbers need to be hard-blocked directly in the dialplan. This is useful for numbers that keep calling from slightly different CLIs (e.g., a recruiter rotating through a number block) or for blocking entire number ranges.
; ============================================================
; STAGE 2: Regex-based hard blocks
; Block specific known nuisance numbers.
; Uses pattern matching on CALLERID(num).
; ============================================================
; Block specific numbers (add as many as needed)
exten => _X.,n,GotoIf($["${CALLERID(num)}" = "441234000001"]?blocked_did,s,1)
exten => _X.,n,GotoIf($["${CALLERID(num)}" = "441234000002"]?blocked_did,s,1)
exten => _X.,n,GotoIf($["${CALLERID(num)}" = "441234000003"]?blocked_did,s,1)
; Block an entire number range (e.g., a known spam prefix)
exten => _X.,n,Set(BLOCKED_RANGE=${REGEX("^44120[0-9]{7}$" ${CALLERID(num)})})
exten => _X.,n,GotoIf($["${BLOCKED_RANGE}" = "1"]?blocked_did,s,1)
Blocked DID handler:
[blocked_did]
exten => s,1,NoOp(BLOCKED DID: ${CALLERID(num)} calling ${EXTEN})
exten => s,n,Log(WARNING,BLOCKED-DID call from ${CALLERID(num)} to ${EXTEN} - hard blocked)
exten => s,n,Congestion(3)
exten => s,n,Hangup()
When to use regex blocks vs. the AstDB blacklist:
| Scenario | Use |
|---|---|
| Individual numbers reported as spam | AstDB blacklist (easy to add/remove) |
| Entire number ranges or prefixes | Regex block in dialplan |
| Numbers that change frequently | AstDB (scriptable, no reload needed) |
| Permanent blocks | Regex (survives AstDB wipes) |
3.3 Anonymous Call Rejection
Calls with no caller ID, or with "anonymous" / "unknown" as the CLI, are almost never legitimate in a B2B or emergency services context. Block them.
; ============================================================
; STAGE 3: Anonymous / withheld CLI rejection
; Blocks calls where CALLERID(num) is empty, "anonymous",
; "unknown", "restricted", "unavailable", or "private".
; ============================================================
exten => _X.,n,Set(CLI_LOWER=${TOLOWER(${CALLERID(num)})})
exten => _X.,n,GotoIf($["${CALLERID(num)}" = ""]?anon_reject,s,1)
exten => _X.,n,GotoIf($["${CLI_LOWER}" = "anonymous"]?anon_reject,s,1)
exten => _X.,n,GotoIf($["${CLI_LOWER}" = "unknown"]?anon_reject,s,1)
exten => _X.,n,GotoIf($["${CLI_LOWER}" = "restricted"]?anon_reject,s,1)
exten => _X.,n,GotoIf($["${CLI_LOWER}" = "unavailable"]?anon_reject,s,1)
exten => _X.,n,GotoIf($["${CLI_LOWER}" = "private"]?anon_reject,s,1)
Anonymous rejection handler:
[anon_reject]
exten => s,1,NoOp(ANONYMOUS CALL REJECTED: CLI="${CALLERID(num)}" to ${EXTEN})
exten => s,n,Log(WARNING,ANONYMOUS call rejected - CLI="${CALLERID(num)}" to DID ${EXTEN})
exten => s,n,Congestion(3)
exten => s,n,Hangup()
Important considerations:
- Some legitimate callers (hospitals, government offices) may send "restricted" CLI. If you serve those sectors, consider routing anonymous calls to a voicemail or secondary queue instead of rejecting outright.
- The
TOLOWER()function handles case variations ("Anonymous", "ANONYMOUS", etc.). - Always log rejected calls so you can audit and adjust.
3.4 UK Mobile-Only Filter
If your operation only accepts calls from UK mobile numbers (common for plumbing/electrical emergency services where callbacks go to mobiles), you can filter by number pattern. UK mobiles start with 07 (national) or +447 / 447 (international format).
; ============================================================
; STAGE 4: UK mobile number validation
; Only allows calls from UK mobile numbers.
; Pattern: 07XXX XXXXXX (national) or 447XXX XXXXXX (international)
;
; UK mobile prefixes: 071, 072, 073, 074, 075, 076, 077, 078, 079
; Excludes 070 (personal numbers) and 076 (pagers, except 07624)
; ============================================================
exten => _X.,n,Set(IS_UK_MOBILE=${REGEX("^(0?447[1-9][0-9]{8}|07[1-9][0-9]{8})$" ${CALLERID(num)})})
exten => _X.,n,GotoIf($["${IS_UK_MOBILE}" = "0"]?not_uk_mobile,s,1)
Non-mobile rejection handler:
[not_uk_mobile]
exten => s,1,NoOp(NON-UK-MOBILE REJECTED: ${CALLERID(num)} calling ${EXTEN})
exten => s,n,Log(NOTICE,NON-UK-MOBILE call rejected - CLI=${CALLERID(num)} to DID ${EXTEN})
exten => s,n,Congestion(3)
exten => s,n,Hangup()
Regex breakdown:
^(0?447[1-9][0-9]{8}|07[1-9][0-9]{8})$
^ Start of string
( Group 1 (international format):
0? Optional leading zero (some trunks send 0447...)
447 UK country code + mobile indicator
[1-9] First digit after 7 (excludes 070x)
[0-9]{8} Remaining 8 digits
| OR
07 National format prefix
[1-9] First digit after 7 (excludes 070x)
[0-9]{8} Remaining 8 digits
)
$ End of string
Adapting for other countries:
| Country | Mobile Regex Pattern |
|---|---|
| UK | ^(0?447[1-9][0-9]{8}|07[1-9][0-9]{8})$ |
| Italy | ^(0?393[0-9]{8,9}|3[0-9]{8,9})$ |
| France | ^(0?336[0-9]{8}|06[0-9]{8})$ |
| Germany | ^(0?491[5-7][0-9]{7,10}|01[5-7][0-9]{7,10})$ |
| Romania | ^(0?407[0-9]{8}|07[0-9]{8})$ |
3.5 Recruiting/Spam Company Block
Some companies systematically call through acquired DID lists --- recruitment agencies, SEO spammers, "Google listing" scams. These typically rotate CLIs but within predictable ranges. Block them by prefix.
; ============================================================
; STAGE 5: Known spam / recruiter company blocks
; Block known nuisance callers by CLI prefix or exact match.
; These are companies that repeatedly call our DIDs.
; ============================================================
; Recruiter company A - rotates through 44203100xxxx range
exten => _X.,n,Set(IS_RECRUITER_A=${REGEX("^44203100[0-9]{4}$" ${CALLERID(num)})})
exten => _X.,n,GotoIf($["${IS_RECRUITER_A}" = "1"]?spam_blocked,s,1)
; SEO spam company B - uses 44800155xxxx range
exten => _X.,n,Set(IS_SPAM_B=${REGEX("^44800155[0-9]{4}$" ${CALLERID(num)})})
exten => _X.,n,GotoIf($["${IS_SPAM_B}" = "1"]?spam_blocked,s,1)
; Robo-dialer C - specific numbers
exten => _X.,n,GotoIf($["${CALLERID(num)}" = "441onal098765"]?spam_blocked,s,1)
Spam handler (with logging for analysis):
[spam_blocked]
exten => s,1,NoOp(SPAM/RECRUITER BLOCKED: ${CALLERID(num)} calling ${EXTEN})
exten => s,n,Log(WARNING,SPAM-BLOCKED call from ${CALLERID(num)} to DID ${EXTEN})
exten => s,n,Congestion(3)
exten => s,n,Hangup()
Pro tip: Create a script that parses Asterisk logs for rejected calls and generates a weekly report:
#!/bin/bash
# /usr/local/bin/weekly-filter-report.sh
# Run via cron: 0 8 * * 1 /usr/local/bin/weekly-filter-report.sh
echo "=== Call Filter Report (past 7 days) ==="
echo ""
echo "--- Blacklisted ---"
grep "BLACKLISTED" /var/log/asterisk/messages | tail -7d | wc -l
echo "--- Blocked DIDs ---"
grep "BLOCKED-DID" /var/log/asterisk/messages | tail -7d | wc -l
echo "--- Anonymous Rejected ---"
grep "ANONYMOUS" /var/log/asterisk/messages | tail -7d | wc -l
echo "--- Non-UK-Mobile ---"
grep "NON-UK-MOBILE" /var/log/asterisk/messages | tail -7d | wc -l
echo "--- Spam/Recruiter ---"
grep "SPAM-BLOCKED" /var/log/asterisk/messages | tail -7d | wc -l
Completing the Filter Pipeline
After all five filters pass, route the call to the next stage:
; ============================================================
; ALL FILTERS PASSED - Route to delay + DID processing
; ============================================================
exten => _X.,n,NoOp(FILTERS PASSED: ${CALLERID(num)} -> ${EXTEN})
exten => _X.,n,Goto(trunkinbound-route,${EXTEN},1)
4. Ring-Back Delay
This is a critical technique for operations where callers should not realize they are calling a call center. When a customer dials their local plumber's number, they expect to hear the phone ring for a few seconds. ViciDial routes calls to agents in under a second, which instantly signals "this is a call center" to the caller.
The solution: introduce a random 2-5 second delay during which the caller hears a standard ring-back tone.
Why Random Delay?
A fixed delay (e.g., always 3 seconds) creates a recognizable pattern. Callers who phone multiple times notice the phone always rings exactly three times. A random delay between 2-5 seconds mimics the natural variability of a real phone.
Implementation
[trunkinbound-route]
; ============================================================
; RING-BACK DELAY
; Generates a random delay between 2-5 seconds.
; Caller hears standard carrier ring-back tone (180 Ringing).
; This makes the call feel like a normal business phone.
; ============================================================
exten => _X.,1,NoOp(Inbound call to DID ${EXTEN} from ${CALLERID(num)})
; Generate random delay: 2, 3, 4, or 5 seconds
exten => _X.,n,Set(RBT_DELAY=${RAND(2,5)})
exten => _X.,n,NoOp(Ring-back delay: ${RBT_DELAY} seconds)
; Send 180 Ringing to the caller's carrier
; This causes the caller to hear their carrier's ring-back tone
exten => _X.,n,Ringing()
; Wait for the random delay period
exten => _X.,n,Wait(${RBT_DELAY})
; Now route through ViciDial's DID handler
exten => _X.,n,AGI(agi://127.0.0.1:4577/agi-DID_route.agi)
; After DID_route.agi processes, ViciDial sets variables and
; routes based on the DID configuration (did_route, extension, etc.)
exten => _X.,n,Goto(${did_route},1)
How It Works Technically
Ringing()sends a SIP180 Ringingresponse back to the trunk/carrier. The carrier then plays its standard ring-back tone to the caller. No audio is generated by Asterisk --- the caller hears their own carrier's tone.Wait(${RBT_DELAY})pauses dialplan execution for the random number of seconds. During this time, the caller simply hears ringing.RAND(2,5)generates a random integer: 2, 3, 4, or 5. Each value is equally likely (25% chance each).
Alternative: Using Playback for Controlled Ringing
If your SIP trunk does not send proper 180 Ringing, or if you want precise control over what the caller hears, you can play the ring tone from Asterisk directly:
; Alternative: Asterisk-generated ring tone
exten => _X.,n,Set(RBT_DELAY=${RAND(2,5)})
exten => _X.,n,Answer()
exten => _X.,n,Playback(ring)
exten => _X.,n,Wait(${RBT_DELAY})
Warning: Using Answer() + Playback(ring) means the call is answered from a billing perspective. The Ringing() + Wait() approach keeps the call in a pre-answer state, which is preferable.
Tuning the Delay
| Scenario | Recommended Delay |
|---|---|
| Emergency services (plumber, electrician) | RAND(2,4) --- callers expect fast answer |
| General business inquiries | RAND(3,5) --- normal business pace |
| Premium/sales lines | RAND(2,3) --- faster answer = higher conversion |
| After-hours overflow | RAND(4,7) --- simulate phone ringing longer |
5. Agent Ranking System
ViciDial's agent ranking system lets you control which agents receive calls first within an inbound group. This is invaluable for routing calls to your most experienced agents while keeping trainees as overflow.
How Ranking Works
- Each agent is assigned a rank (0-9) per inbound group
- Lower rank = higher priority (rank 5 agents receive calls before rank 9)
- An agent can have different ranks in different inbound groups
- The inbound group must use
next_agent_call = inbound_group_rankfor rankings to take effect
ViciDial Admin Configuration
Step 1: Set the Inbound Group Routing Method
Navigate to: Admin > Inbound Groups > [your group] > Detail View
Set these fields:
| Setting | Value | Purpose |
|---|---|---|
| Next Agent Call | inbound_group_rank |
Routes based on agent rank |
| No Delay Call Route | Y |
Skip the 15-second default delay |
| In-Group Rank | (see agent assignments) | Per-agent priority |
Step 2: Assign Agent Ranks
Navigate to: Admin > Inbound Groups > [your group] > In-Group Agents
Or use the Closer In-Group Ranking section to assign ranks:
| Agent ID | Agent Name | Rank | Role |
|---|---|---|---|
| AGENT_A | Top Performer 1 | 5 | Senior --- gets calls first |
| AGENT_B | Top Performer 2 | 5 | Senior --- gets calls first |
| AGENT_C | Experienced 1 | 6 | Experienced --- second tier |
| AGENT_D | Experienced 2 | 6 | Experienced --- second tier |
| AGENT_E | Mid-Level 1 | 7 | Mid-level --- third tier |
| AGENT_F | Standard 1 | 8 | Standard --- fourth tier |
| AGENT_G | Trainee 1 | 9 | Trainee --- overflow only |
| AGENT_H | Trainee 2 | 9 | Trainee --- overflow only |
Step 3: Different Ranks Per Inbound Group
An agent can excel at one type of call but be less suited for another. Assign different ranks per group:
| Agent | "primary" Group Rank | "overflow" Group Rank | "specialty" Group Rank |
|---|---|---|---|
| AGENT_A | 5 (first) | 7 (backup) | 5 (first) |
| AGENT_B | 5 (first) | 5 (first) | 9 (last) |
| AGENT_C | 7 (backup) | 5 (first) | 6 (second) |
This means AGENT_A is one of the first agents to receive "primary" calls and "specialty" calls, but only gets "overflow" calls when senior agents are busy.
Database-Level Configuration
All rank assignments are stored in the vicidial_inbound_group_agents table. You can view and modify them directly:
View current rankings:
SELECT group_id, user, group_rank, group_weight, group_grade
FROM vicidial_inbound_group_agents
WHERE group_id = 'your_ingroup'
ORDER BY group_rank ASC, user ASC;
Bulk-update agent ranks:
-- Promote AGENT_A to rank 5 in the "primary" inbound group
UPDATE vicidial_inbound_group_agents
SET group_rank = 5
WHERE group_id = 'primary' AND user = 'AGENT_A';
-- Set all trainees to rank 9
UPDATE vicidial_inbound_group_agents
SET group_rank = 9
WHERE group_id = 'primary' AND user IN ('AGENT_G', 'AGENT_H', 'AGENT_I');
Add a new agent to an inbound group with a specific rank:
INSERT INTO vicidial_inbound_group_agents
(group_id, user, group_rank, group_weight, group_grade, group_web_vars)
VALUES
('primary', 'NEW_AGENT', 7, 0, 1, '');
Verify the inbound group uses ranked routing:
SELECT group_id, group_name, next_agent_call, no_delay_call_route
FROM vicidial_inbound_groups
WHERE group_id = 'your_ingroup';
The next_agent_call field must be inbound_group_rank for rankings to have any effect.
Confirming Rankings Are Active
After configuration, verify rankings work correctly:
-- See which agents are logged into the inbound group and their ranks
SELECT
vla.user,
vla.status,
vla.campaign_id,
viga.group_rank,
vla.closer_campaigns
FROM vicidial_live_agents vla
JOIN vicidial_inbound_group_agents viga
ON viga.user = vla.user
WHERE viga.group_id = 'your_ingroup'
AND vla.closer_campaigns LIKE '%your_ingroup%'
ORDER BY viga.group_rank ASC;
Agent Login Requirements
Agents must select the correct inbound groups when logging in. If you have multiple ranked inbound groups (e.g., "primary" and "overflow"), agents need to select all groups they should receive calls from in the Closer Inbound Group Selection screen.
If an agent is rank 5 in "primary" but does not select "primary" at login, they will not receive any calls from that group regardless of rank.
6. Recording Security & HTML5 Player
By default, ViciDial recording URLs are publicly accessible. Anyone who knows the file naming convention can download recordings directly. For operations handling sensitive calls (financial, medical, emergency), this is unacceptable.
Apache mod_rewrite Access Control
This configuration uses Apache mod_rewrite to check the caller's IP address. Whitelisted office IPs get direct MP3 access; everyone else is redirected to a custom HTML5 player that streams the audio without allowing easy download.
Create the Apache configuration file:
File: /etc/apache2/conf.d/recording-security.conf (openSUSE/ViciBox)
or: /etc/httpd/conf.d/recording-security.conf (CentOS/RHEL)
# =============================================================
# Recording Access Security
# Whitelisted IPs: direct MP3 download
# All others: redirected to HTML5 streaming player
# =============================================================
<Directory "/var/spool/asterisk/monitorDONE">
Options -Indexes
AllowOverride None
Require all granted
RewriteEngine On
# -------------------------------------------------------
# WHITELIST: Office IPs that get direct MP3 access
# Add your office, VPN, and management IPs here
# -------------------------------------------------------
RewriteCond %{REMOTE_ADDR} !^10\.20\.30\.40$
RewriteCond %{REMOTE_ADDR} !^10\.20\.30\.41$
RewriteCond %{REMOTE_ADDR} !^192\.168\.1\.0/24$
# -------------------------------------------------------
# REDIRECT: Non-whitelisted IPs go to HTML5 player
# Only applies to .mp3 and .wav files
# -------------------------------------------------------
RewriteRule ^(.+)\.(mp3|wav)$ /recording-player.php?file=$1.$2 [R=302,L]
</Directory>
# Alias for recording URL path
Alias /RECORDINGS /var/spool/asterisk/monitorDONE
Enable the configuration:
# Verify mod_rewrite is loaded
apache2ctl -M | grep rewrite
# If not loaded (openSUSE):
a2enmod rewrite
# Test configuration
apache2ctl configtest
# Reload Apache
systemctl reload apache2
HTML5 Recording Player
Create a dark-themed HTML5 player that streams recordings with byte-range support (seeking works) but makes casual downloading harder.
File: /srv/www/htdocs/recording-player.php (adjust path for your OS)
<?php
/**
* HTML5 Recording Player
* Streams recordings with byte-range support (seeking).
* Dark theme, no direct download link.
*
* Security: validates filename, prevents directory traversal.
*/
// Configuration
$recording_base = '/var/spool/asterisk/monitorDONE';
$allowed_extensions = ['mp3', 'wav'];
// Get and sanitize the requested file
$file = isset($_GET['file']) ? $_GET['file'] : '';
$file = basename($file); // Strip directory components (prevent traversal)
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
// Validate
if (empty($file) || !in_array($ext, $allowed_extensions)) {
http_response_code(400);
die('Invalid request');
}
$filepath = $recording_base . '/' . $file;
if (!file_exists($filepath)) {
http_response_code(404);
die('Recording not found');
}
// If this is a streaming request (from the audio element), serve the file
if (isset($_GET['stream'])) {
$size = filesize($filepath);
$mime = ($ext === 'mp3') ? 'audio/mpeg' : 'audio/wav';
// Handle byte-range requests (required for seeking)
if (isset($_SERVER['HTTP_RANGE'])) {
$range = $_SERVER['HTTP_RANGE'];
preg_match('/bytes=(\d+)-(\d*)/', $range, $matches);
$start = intval($matches[1]);
$end = isset($matches[2]) && $matches[2] !== '' ? intval($matches[2]) : $size - 1;
$length = $end - $start + 1;
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $start-$end/$size");
header("Content-Length: $length");
header("Content-Type: $mime");
header('Accept-Ranges: bytes');
header('Cache-Control: no-cache, no-store');
$fp = fopen($filepath, 'rb');
fseek($fp, $start);
echo fread($fp, $length);
fclose($fp);
exit;
}
// Full file request
header("Content-Type: $mime");
header("Content-Length: $size");
header('Accept-Ranges: bytes');
header('Cache-Control: no-cache, no-store');
readfile($filepath);
exit;
}
// Render the HTML5 player page
$stream_url = '?file=' . urlencode($file) . '&stream=1';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Recording Player</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1a1a2e;
color: #e0e0e0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.player-container {
background: #16213e;
border-radius: 12px;
padding: 40px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
max-width: 600px;
width: 90%;
}
.player-header {
text-align: center;
margin-bottom: 30px;
}
.player-header h1 {
font-size: 18px;
font-weight: 500;
color: #a0a0c0;
margin-bottom: 8px;
}
.player-header .filename {
font-size: 13px;
color: #6a6a8a;
word-break: break-all;
}
audio {
width: 100%;
margin: 20px 0;
outline: none;
border-radius: 8px;
}
/* Style the audio element for dark theme (WebKit browsers) */
audio::-webkit-media-controls-panel {
background: #0f3460;
}
audio::-webkit-media-controls-current-time-display,
audio::-webkit-media-controls-time-remaining-display {
color: #e0e0e0;
}
.player-footer {
text-align: center;
margin-top: 20px;
font-size: 12px;
color: #4a4a6a;
}
/* Disable right-click context menu on audio */
audio { -webkit-user-select: none; user-select: none; }
</style>
</head>
<body>
<div class="player-container">
<div class="player-header">
<h1>Call Recording</h1>
<div class="filename"><?php echo htmlspecialchars($file); ?></div>
</div>
<audio controls controlsList="nodownload" preload="metadata">
<source src="<?php echo htmlspecialchars($stream_url); ?>"
type="<?php echo ($ext === 'mp3') ? 'audio/mpeg' : 'audio/wav'; ?>">
Your browser does not support audio playback.
</audio>
<div class="player-footer">
Playback only — recording cannot be downloaded.
</div>
</div>
<script>
// Disable right-click on audio element
document.querySelector('audio').addEventListener('contextmenu', function(e) {
e.preventDefault();
});
</script>
</body>
</html>
Security Notes
controlsList="nodownload"removes the download button from Chrome/Edge audio controls.basename()prevents directory traversal attacks (../../etc/passwd).- Byte-range support is essential --- without it, seeking in the audio player will not work.
Cache-Control: no-cacheprevents browsers from caching recordings locally.- This is not a bulletproof DRM solution. A determined user can still capture the audio stream. The goal is to prevent casual downloading by non-technical users (clients, auditors).
Testing Recording Security
# From a whitelisted IP - should get the MP3 file directly
curl -I http://YOUR_SERVER/RECORDINGS/MP3/test-recording.mp3
# Expected: Content-Type: audio/mpeg
# From a non-whitelisted IP - should get a 302 redirect
curl -I http://YOUR_SERVER/RECORDINGS/MP3/test-recording.mp3
# Expected: HTTP/1.1 302 Found, Location: /recording-player.php?file=...
7. Codec & Audio Quality Tuning
Default ViciDial codec settings often cause unnecessary transcoding, which adds latency and can introduce audio artifacts. For UK and European operations, alaw (G.711a) should always be the first codec offered.
SIP Peer Codec Configuration
In sip.conf or sip-vicidial.conf, set the codec order for your trunk peers:
; =============================================================
; Codec configuration for UK/European SIP trunks
; alaw FIRST - native codec for UK/EU, no transcoding needed
; =============================================================
[trunk_peer_template](!)
type=peer
disallow=all
allow=alaw ; G.711a - UK/EU native, 64kbps, no transcoding
allow=ulaw ; G.711u - US fallback, minimal transcoding
allow=g722 ; Wideband - better quality if trunk supports it
allow=g729 ; Low-bandwidth fallback
allow=gsm ; Last resort
Why alaw first?
| Codec | Bandwidth | Region | Transcoding from alaw |
|---|---|---|---|
| alaw (G.711a) | 64 kbps | UK/EU native | None (passthrough) |
| ulaw (G.711u) | 64 kbps | US/Canada native | Minimal (mu-law to a-law) |
| g722 | 64 kbps | Any | Required (CPU-intensive) |
| g729 | 8 kbps | Any | Required (license needed) |
| gsm | 13 kbps | Any | Required |
When your trunk and your agents both use alaw, Asterisk can pass the audio through without touching it (native bridging). This eliminates transcoding latency entirely.
ConfBridge Audio Quality Settings
ViciDial uses ConfBridge (Asterisk 16+) for call bridging. Key settings for audio quality:
Modify the ConfBridge profile via ViciDial's ADMIN_keepalive_ALL.pl:
The dsp_drop_silence setting is critical. When enabled, ConfBridge drops silence frames to save bandwidth. This causes clipping at the start of speech --- the first syllable gets cut off because Asterisk thinks it is still silence.
; In confbridge.conf or applied via keepalive script:
[default_bridge]
type=bridge
; CRITICAL: Do NOT drop silence frames
; Dropping silence causes speech clipping at utterance boundaries
dsp_drop_silence=no
; Use software mixing (not native RTP bridging)
; This ensures consistent audio processing
mixing_interval=20
[default_user]
type=user
; Standard user profile for agents
announce_user_count=no
quiet=yes
Disable native RTP bridging:
Native RTP bridging bypasses Asterisk's audio processing entirely, sending RTP directly between endpoints. While this reduces server load, it breaks features like recording, DTMF detection, and quality monitoring. For call centers, always use software mixing.
Add to modules.conf:
; Disable native RTP bridging to ensure recordings and
; DTMF detection work reliably through ConfBridge
noload => bridge_native_rtp.so
Jitter Buffer Configuration
Network jitter causes audio crackling, gaps, and robotic speech. An adaptive jitter buffer smooths out timing variations.
In sip.conf (general section):
[general]
; =============================================================
; Jitter buffer - adaptive mode
; Compensates for network timing variations
; =============================================================
jbEnabled = yes
jbForce = no ; Do not force JB on all channels
jbImpl = adaptive ; Adaptive algorithm (adjusts to conditions)
jbMaxSize = 200 ; Maximum buffer size in ms
jbResyncThreshold = 1000 ; Resync if drift exceeds 1000ms
jbLog = no ; Disable JB logging (noisy in production)
RTP and Session Timer Settings
[general]
; =============================================================
; RTP keepalive and session timers
; Prevents NAT timeouts, detects dead calls
; =============================================================
; RTP keepalive: send comfort noise every 15 seconds
; Prevents NAT gateways from closing the RTP pinhole
rtpkeepalive=15
; RTP hold timeout: hang up if no RTP received for 300 seconds
; Catches "zombie" calls where one side disconnected silently
rtpholdtimeout=300
; RTP timeout: hang up if no RTP at all for 60 seconds
rtptimeout=60
; Session timers: re-INVITE every 1800 seconds (30 minutes)
; Catches calls stuck in "connected" state after network failure
session-timers=originate
session-expires=1800
session-minse=90
session-refresher=uas
; SIP qualify: check trunk health every 15 seconds
qualifyfreq=15
QoS (Quality of Service) Marking
If your network supports QoS (DSCP marking), set proper values so routers prioritize voice traffic:
[general]
; QoS DSCP markings
tos_sip=cs3 ; SIP signaling: CS3 (DSCP 24)
tos_audio=ef ; RTP audio: Expedited Forwarding (DSCP 46)
cos_sip=3 ; 802.1p CoS for SIP
cos_audio=5 ; 802.1p CoS for RTP
RTP Port Range
Tighten the RTP port range to reduce firewall exposure:
In rtp.conf:
[general]
; Tight RTP port range
; 500 ports supports ~250 simultaneous calls
rtpstart=10000
rtpend=10500
Your firewall should only allow UDP traffic on this range from your SIP trunk provider IPs.
8. Testing, Rollout & Monitoring
Testing Filters Without Breaking Live Calls
Never deploy filters directly to production. Use this graduated approach:
Phase 1: Log-Only Mode
Instead of rejecting calls, log what would be rejected:
; LOG-ONLY MODE: Replace actual rejection with logging
; Use this to validate filter accuracy before going live
; Instead of:
; exten => _X.,n,GotoIf(${BLACKLIST()}?blacklisted,s,1)
; Use:
exten => _X.,n,ExecIf(${BLACKLIST()}?NoOp(WOULD-BLOCK-BLACKLIST: ${CALLERID(num)} to ${EXTEN}))
exten => _X.,n,ExecIf(${BLACKLIST()}?Log(NOTICE,FILTER-TEST: Blacklist would block ${CALLERID(num)}))
Run in log-only mode for at least one business day. Then analyze:
# Count how many calls each filter would have blocked
grep "FILTER-TEST" /var/log/asterisk/messages | \
awk -F'FILTER-TEST: ' '{print $2}' | \
sort | uniq -c | sort -rn
Phase 2: Selective Enforcement
Enable filters one at a time, starting with the lowest-risk:
- Blacklist (first) --- you manually control this list, lowest risk
- Spam/recruiter block (second) --- specific known numbers
- Anonymous rejection (third) --- review log-only data first
- UK mobile filter (last, highest risk) --- most aggressive, blocks all landlines
Wait 24-48 hours between enabling each filter. Monitor agent feedback.
Phase 3: Full Enforcement
Once all filters are validated, switch to the production dialplan shown in Section 3.
Monitoring Rejected Calls
Real-time monitoring via Asterisk CLI:
# Watch filter activity in real-time
asterisk -rx "core set verbose 3"
# Then in another terminal:
tail -f /var/log/asterisk/messages | grep -E "(BLACKLISTED|BLOCKED-DID|ANONYMOUS|NON-UK-MOBILE|SPAM-BLOCKED)"
Daily rejection summary (add to crontab):
#!/bin/bash
# /usr/local/bin/daily-filter-stats.sh
# Cron: 0 23 * * * /usr/local/bin/daily-filter-stats.sh >> /var/log/filter-stats.log
DATE=$(date +%Y-%m-%d)
LOG="/var/log/asterisk/messages"
echo "=== Filter Stats for $DATE ==="
echo "Blacklisted: $(grep "$DATE" $LOG | grep -c 'BLACKLISTED')"
echo "Blocked DIDs: $(grep "$DATE" $LOG | grep -c 'BLOCKED-DID')"
echo "Anonymous: $(grep "$DATE" $LOG | grep -c 'ANONYMOUS')"
echo "Non-UK-Mobile: $(grep "$DATE" $LOG | grep -c 'NON-UK-MOBILE')"
echo "Spam/Recruiter: $(grep "$DATE" $LOG | grep -c 'SPAM-BLOCKED')"
echo "Total rejected: $(grep "$DATE" $LOG | grep -cE '(BLACKLISTED|BLOCKED-DID|ANONYMOUS|NON-UK-MOBILE|SPAM-BLOCKED)')"
echo "Total inbound: $(grep "$DATE" $LOG | grep -c 'FILTERS PASSED')"
echo "---"
Grafana dashboard query (if using Loki for log aggregation):
{job="asterisk"} |= "BLACKLISTED" or "BLOCKED-DID" or "ANONYMOUS" or "NON-UK-MOBILE" or "SPAM-BLOCKED"
| pattern `<_> <level> <_> <filter_type> call from <caller> to DID <did>`
| count_over_time({job="asterisk"} |= "BLOCKED" [1h]) by (filter_type)
Gradual Rollout Checklist
Before enabling each filter in production:
- Backup
extensions.conf:cp extensions.conf extensions.conf.bak.$(date +%Y%m%d) - Test in log-only mode for at least 24 hours
- Review log-only results: no false positives on legitimate callers
- Enable the filter in the dialplan
- Reload Asterisk:
asterisk -rx "dialplan reload" - Monitor the first 30 minutes of live calls
- Check agent feedback: are they missing any calls they expected?
- Run the daily stats script to verify rejection counts are reasonable
- Document the change in your change log
Emergency Rollback
If a filter is blocking legitimate calls:
# Immediate: restore backup and reload
cp /etc/asterisk/extensions.conf.bak.YYYYMMDD /etc/asterisk/extensions.conf
asterisk -rx "dialplan reload"
# Verify the rollback took effect
asterisk -rx "dialplan show trunkinbound" | head -20
The dialplan reload command is non-disruptive --- it does not drop active calls. Only new calls will use the updated dialplan.
9. Complete Reference Configuration
Here is the full [trunkinbound] context with all filters, ring-back delay, and routing assembled into a single block. Copy this as a starting point and customize for your environment.
; =============================================================
; ADVANCED INBOUND CALL FLOW
; File: /etc/asterisk/extensions.conf (add to existing file)
;
; Pipeline: Blacklist -> Blocked DIDs -> Anonymous -> UK Mobile
; -> Spam Block -> Ring-Back Delay -> DID Route
; =============================================================
[trunkinbound]
; --- STAGE 1: Sanitize CLI and check blacklist ---
exten => _X.,1,Set(CALLERID(num)=${FILTER(+0123456789,${CALLERID(num)})})
exten => _X.,n,NoOp(INBOUND: CLI=${CALLERID(num)} DID=${EXTEN})
exten => _X.,n,GotoIf(${BLACKLIST()}?blacklisted,s,1)
; --- STAGE 2: Hard-blocked numbers ---
exten => _X.,n,GotoIf($["${CALLERID(num)}" = "441234000001"]?blocked_did,s,1)
exten => _X.,n,GotoIf($["${CALLERID(num)}" = "441234000002"]?blocked_did,s,1)
; Add more hard blocks as needed...
exten => _X.,n,Set(BLOCKED_RANGE=${REGEX("^44120[0-9]{7}$" ${CALLERID(num)})})
exten => _X.,n,GotoIf($["${BLOCKED_RANGE}" = "1"]?blocked_did,s,1)
; --- STAGE 3: Anonymous / withheld CLI ---
exten => _X.,n,Set(CLI_LOWER=${TOLOWER(${CALLERID(num)})})
exten => _X.,n,GotoIf($["${CALLERID(num)}" = ""]?anon_reject,s,1)
exten => _X.,n,GotoIf($["${CLI_LOWER}" = "anonymous"]?anon_reject,s,1)
exten => _X.,n,GotoIf($["${CLI_LOWER}" = "unknown"]?anon_reject,s,1)
exten => _X.,n,GotoIf($["${CLI_LOWER}" = "restricted"]?anon_reject,s,1)
exten => _X.,n,GotoIf($["${CLI_LOWER}" = "unavailable"]?anon_reject,s,1)
exten => _X.,n,GotoIf($["${CLI_LOWER}" = "private"]?anon_reject,s,1)
; --- STAGE 4: UK mobile-only filter ---
exten => _X.,n,Set(IS_UK_MOBILE=${REGEX("^(0?447[1-9][0-9]{8}|07[1-9][0-9]{8})$" ${CALLERID(num)})})
exten => _X.,n,GotoIf($["${IS_UK_MOBILE}" = "0"]?not_uk_mobile,s,1)
; --- STAGE 5: Known spam / recruiter blocks ---
exten => _X.,n,Set(IS_RECRUITER_A=${REGEX("^44203100[0-9]{4}$" ${CALLERID(num)})})
exten => _X.,n,GotoIf($["${IS_RECRUITER_A}" = "1"]?spam_blocked,s,1)
; Add more spam patterns as needed...
; --- ALL FILTERS PASSED ---
exten => _X.,n,NoOp(FILTERS PASSED: ${CALLERID(num)} -> ${EXTEN})
exten => _X.,n,Goto(trunkinbound-route,${EXTEN},1)
[trunkinbound-route]
; --- RING-BACK DELAY ---
exten => _X.,1,NoOp(Routing DID ${EXTEN} from ${CALLERID(num)})
exten => _X.,n,Set(RBT_DELAY=${RAND(2,5)})
exten => _X.,n,NoOp(Ring-back delay: ${RBT_DELAY}s)
exten => _X.,n,Ringing()
exten => _X.,n,Wait(${RBT_DELAY})
; --- ViciDial DID routing ---
exten => _X.,n,AGI(agi://127.0.0.1:4577/agi-DID_route.agi)
exten => _X.,n,Goto(${did_route},1)
; =============================================================
; REJECTION HANDLERS
; =============================================================
[blacklisted]
exten => s,1,NoOp(BLACKLISTED: ${CALLERID(num)} calling ${EXTEN})
exten => s,n,Log(WARNING,BLACKLISTED call from ${CALLERID(num)} to ${EXTEN} - rejected)
exten => s,n,Congestion(3)
exten => s,n,Hangup()
[blocked_did]
exten => s,1,NoOp(BLOCKED DID: ${CALLERID(num)} calling ${EXTEN})
exten => s,n,Log(WARNING,BLOCKED-DID call from ${CALLERID(num)} to ${EXTEN} - hard blocked)
exten => s,n,Congestion(3)
exten => s,n,Hangup()
[anon_reject]
exten => s,1,NoOp(ANONYMOUS CALL REJECTED: CLI="${CALLERID(num)}")
exten => s,n,Log(WARNING,ANONYMOUS call rejected - CLI="${CALLERID(num)}" to DID ${EXTEN})
exten => s,n,Congestion(3)
exten => s,n,Hangup()
[not_uk_mobile]
exten => s,1,NoOp(NON-UK-MOBILE REJECTED: ${CALLERID(num)})
exten => s,n,Log(NOTICE,NON-UK-MOBILE call rejected - CLI=${CALLERID(num)} to DID ${EXTEN})
exten => s,n,Congestion(3)
exten => s,n,Hangup()
[spam_blocked]
exten => s,1,NoOp(SPAM/RECRUITER BLOCKED: ${CALLERID(num)})
exten => s,n,Log(WARNING,SPAM-BLOCKED call from ${CALLERID(num)} to DID ${EXTEN})
exten => s,n,Congestion(3)
exten => s,n,Hangup()
After adding this configuration:
# Verify syntax
asterisk -rx "dialplan reload"
asterisk -rx "dialplan show trunkinbound"
asterisk -rx "dialplan show trunkinbound-route"
# If there are errors, check the CLI output for line numbers
10. Troubleshooting
Filter is blocking legitimate calls
Symptom: Agents report missing calls from known customers.
Diagnosis:
# Check if the caller was filtered
grep "441234567890" /var/log/asterisk/messages | tail -5
Look for any of the filter log lines (BLACKLISTED, BLOCKED-DID, ANONYMOUS, NON-UK-MOBILE, SPAM-BLOCKED).
Fix: If the UK mobile filter is too aggressive, consider allowing specific landline prefixes:
; Allow UK landlines as well as mobiles
; Matches: 44XXXXXXXXX (any UK number, 10+ digits)
exten => _X.,n,Set(IS_UK_NUMBER=${REGEX("^(0?44[0-9]{9,10}|0[1-9][0-9]{8,9})$" ${CALLERID(num)})})
exten => _X.,n,GotoIf($["${IS_UK_NUMBER}" = "0"]?not_uk_number,s,1)
Ring-back delay causes timeout
Symptom: Calls drop before reaching an agent, especially with long delays.
Diagnosis: Some SIP trunks have a short ring timeout (e.g., 15 seconds). If the ring-back delay is 5 seconds and ViciDial takes 2-3 seconds to find an agent, you are already at 7-8 seconds before the agent phone starts ringing.
Fix: Reduce the maximum delay:
; Reduce maximum delay from 5 to 3 seconds
exten => _X.,n,Set(RBT_DELAY=${RAND(2,3)})
Agent rankings not working
Symptom: Calls are distributed evenly regardless of rank assignments.
Diagnosis:
-- Check the inbound group routing method
SELECT group_id, next_agent_call FROM vicidial_inbound_groups
WHERE group_id = 'your_ingroup';
If next_agent_call is not inbound_group_rank, rankings are ignored.
Fix:
UPDATE vicidial_inbound_groups
SET next_agent_call = 'inbound_group_rank'
WHERE group_id = 'your_ingroup';
Recording player shows "Recording not found"
Symptom: The HTML5 player loads but cannot find the file.
Diagnosis: Check the recording path. ViciDial may store recordings in subdirectories:
# Find where recordings actually live
find /var/spool/asterisk/monitorDONE -name "*.mp3" | head -5
If recordings are in subdirectories (e.g., MP3/), update $recording_base in the player:
$recording_base = '/var/spool/asterisk/monitorDONE/MP3';
Codec negotiation not using alaw
Symptom: sip show channelstats shows ulaw or g729 instead of alaw.
Diagnosis:
asterisk -rx "sip show channelstats"
# Check the "Codec" column for active calls
Fix: Verify the trunk peer has disallow=all before allow=alaw. Without disallow=all, Asterisk appends your allowed codecs to the defaults instead of replacing them.
Also check that the remote trunk supports alaw. Some US-based trunks only support ulaw.
Dialplan reload fails
Symptom: dialplan reload shows errors.
Diagnosis:
# Check for syntax errors
asterisk -rx "dialplan reload" 2>&1 | grep -i error
# Common issues:
# - Missing closing bracket in GotoIf
# - Unmatched quotes in REGEX
# - Missing exten => prefix
Fix: Use the backup you created before editing:
cp /etc/asterisk/extensions.conf.bak.YYYYMMDD /etc/asterisk/extensions.conf
asterisk -rx "dialplan reload"
Further Reading
- Asterisk documentation:
core show function BLACKLIST,core show function REGEX,core show application GotoIf - ViciDial admin manual: Inbound Groups configuration, Closer campaigns, Agent ranking
- ConfBridge tuning:
core show application ConfBridge,confbridge show profile bridge default_bridge - Apache mod_rewrite: RewriteCond and RewriteRule directive reference
This tutorial is based on production configurations handling thousands of daily calls across multiple ViciDial servers. All IP addresses, phone numbers, and company names have been replaced with placeholders.