Tools: Update: FreeSWITCH Fundamentals — Installation, Dialplan, SIP & IVR
Tutorial 41: FreeSWITCH Fundamentals — Installation, Dialplan, SIP & IVR
Table of Contents
1. Introduction
What Is FreeSWITCH?
FreeSWITCH vs Asterisk
When to Choose FreeSWITCH
2. Architecture Overview
Core Design Principles
Key Concepts
How a Call Flows
Directory Structure
Module System
FreeSWITCH vs Asterisk Concept Mapping
3. Installation
Option A: Package Install (Recommended)
Step 1: Get a SignalWire PAT
Step 2: Add the Repository
Step 3: Install FreeSWITCH
Step 4: Post-Install Configuration
Firewall Configuration
Verify Installation
Change Default Passwords
4. SIP Configuration (mod_sofia)
Understanding SIP Profiles
Internal Profile Configuration
External Profile Configuration
User Directory (SIP Extensions)
Complete 10-Extension Office Configuration
SIP Trunk (Gateway) Configuration
Inbound DID Routing
NAT Traversal
Registering SIP Phones
5. XML Dialplan
Dialplan Structure
Condition Matching
Regex Patterns
Channel Variables
Common Dialplan Actions
Anti-Actions
Complete Dialplan Examples
6. IVR Menus
IVR Concepts in FreeSWITCH
IVR Menu XML Configuration
IVR Menu Parameters Reference
IVR Entry Actions
Connecting the IVR to the Dialplan
Using play_and_get_digits
Text-to-Speech (TTS)
Complete 3-Level IVR Example
7. Voicemail
Voicemail Configuration
Dialplan Integration
Email Notification Setup
Greeting Management
Storage and Cleanup
8. Call Recording
Full-Call Recording with record_session
File Naming Best Practices
Stereo Recording
Start/Stop Recording Mid-Call
Post-Call Processing: Convert to MP3
Retention and Cleanup
9. Conference Bridge
Conference Profile Configuration
Dialplan for Conference Rooms
Dynamic PIN from Dialplan
Conference Management via fs_cli
Conference Events
10. Event Socket Layer (ESL)
What Is ESL?
ESL Configuration
fs_cli as ESL Client
Common ESL Commands
Python ESL: Inbound Mode
Python ESL: Call Control Application
Node.js ESL Example
ESL Outbound Mode
11. CDR & Logging
CDR with mod_cdr_csv
CDR with mod_cdr_sqlite
CDR with mod_cdr_pg (PostgreSQL)
CDR Fields Reference
Log Configuration
SIP Trace Logging
Log Rotation
12. Security Hardening
Change All Default Passwords
SIP Profile ACLs
fail2ban for FreeSWITCH
TLS for SIP (SIPS)
SRTP (Encrypted Media)
Rate Limiting
Firewall Best Practices Summary
13. Integration Patterns
FreeSWITCH + Kamailio (SBC / Load Balancer)
FreeSWITCH + WebRTC (mod_verto)
FreeSWITCH + Databases (mod_xml_curl)
FreeSWITCH + REST APIs
FreeSWITCH + Asterisk Interop
FreeSWITCH as Media Server Behind Kamailio
14. Troubleshooting
SIP Registration Failures
No Audio / One-Way Audio
Call Routing Problems
Performance Issues
Essential fs_cli Commands Reference
Quick Diagnostic Sequence A complete beginner-to-intermediate guide to FreeSWITCH — the high-performance open-source telephony platform. Learn installation, SIP endpoint configuration, XML dialplan, IVR menus, voicemail, recording, and integration patterns from scratch, with production-ready configurations throughout. Whether you are migrating from Asterisk or starting fresh, this tutorial gives you everything you need to deploy a fully functional FreeSWITCH system with SIP phones, trunks, call routing, conferencing, and external application control via the Event Socket Layer. FreeSWITCH is a scalable, open-source telephony platform designed for routing and interconnecting voice, video, and text communication protocols. Originally created by Anthony Minessale (a former Asterisk developer) in 2006, it was built from the ground up to address architectural limitations in existing PBX software. Unlike traditional PBX systems that evolved from hardware roots, FreeSWITCH was designed as a software media server — a communications engine that applications talk to, rather than a standalone phone system with a configuration UI. If you come from the Asterisk world, understanding the conceptual differences is critical before diving in. FreeSWITCH excels in these scenarios: Choose Asterisk instead if you need: a quick office PBX, FreePBX/GUI administration, massive community support, or ViciDial/call-center-specific features. FreeSWITCH is built on three fundamental design principles: Event-driven: Everything in FreeSWITCH generates events. A call arriving, a DTMF press, a conference join — all are events that can be captured, filtered, and acted upon by internal modules or external applications. Modular: The core is a lightweight engine. All functionality — SIP, dialplan, codecs, applications — comes from loadable modules. You enable only what you need. Session-based: Each call creates a session object that persists for the call's lifetime. Sessions hold all state (variables, media streams, applications) and are managed by a thread pool, not one-thread-per-call. Before configuring anything, understand these five concepts: Profiles — SIP listeners. Each profile binds to an IP:port and defines SIP behavior (codecs, NAT handling, authentication). The two default profiles are internal (port 5060, for registered phones) and external (port 5080, for trunks/carriers). Contexts — Dialplan routing containers. When a call arrives on a profile, it enters the context assigned to that profile. The internal profile routes to the default context. The external profile routes to the public context. Extensions — Named dialplan entries within a context. Each extension has conditions and actions. FreeSWITCH evaluates extensions top-to-bottom until one matches. Conditions — Pattern-matching rules within an extension. Match on destination_number, caller_id_number, time_of_day, channel variables, or any header field. Actions — What to do when conditions match. Actions include bridge (connect calls), playback (play audio), transfer (re-route), answer, hangup, and dozens more. FreeSWITCH loads modules at startup from autoload_configs/modules.conf.xml. Key modules: If you are coming from Asterisk, this mapping helps translate your knowledge: FreeSWITCH packages are distributed through SignalWire. You need a free Personal Access Token (PAT). Debian 12 (Bookworm): Ubuntu 24.04 (Noble): The freeswitch-meta-all package includes all modules, sounds, music-on-hold, codecs, and language packs. For production, start with freeswitch-meta-vanilla and add only the modules you need. Building from source gives you the latest code and full control over modules. If you compiled from source, config lives in /usr/local/freeswitch/conf/ instead of /etc/freeswitch/. Adjust paths accordingly. With iptables instead: This is critical. The default install ships with password 1234 for all extensions and ClueCon for ESL. Find and change these lines: Change the ESL password: Reload the configuration: mod_sofia is the SIP engine in FreeSWITCH. It manages all SIP communication through profiles. Each profile is an independent SIP listener with its own port, settings, and behavior. The default installation creates two profiles: Why two profiles? Security. Internal phones authenticate with username/password. External trunks authenticate by IP. Keeping them on separate ports with separate rules prevents unauthorized calls. The internal profile handles your SIP phones. Edit /etc/freeswitch/sip_profiles/internal.xml: The external profile handles SIP trunks and outside calls. Edit /etc/freeswitch/sip_profiles/external.xml: Each SIP phone/extension is defined in the directory. Files go in /etc/freeswitch/directory/default/. Here is a single extension definition. Create /etc/freeswitch/directory/default/1001.xml: Here is a production-ready configuration for a small office with 10 extensions and one SIP trunk. Create all 10 extension files — save this as a shell script: A gateway defines a connection to your SIP provider (ITSP). Create /etc/freeswitch/sip_profiles/external/my_provider.xml: For IP-based authentication (no username/password, provider allows your IP): After creating gateway files, reload: When a call arrives from a trunk, it enters the public context. You need to route it to the right extension. Edit /etc/freeswitch/dialplan/public.xml: If your FreeSWITCH server is behind NAT (e.g., in AWS/GCP/Azure with a private IP), configure these settings: In each SIP profile (internal.xml and external.xml): With extensions created, configure your SIP phone/softphone: The FreeSWITCH dialplan is a hierarchy: Context → Extension → Condition → Action. FreeSWITCH evaluates extensions top-to-bottom within a context. By default, it stops at the first matching extension (unless the extension is marked continue="true"). Conditions can match on any channel variable. Common fields: Multiple conditions in the same extension use AND logic — all must match. FreeSWITCH uses PCRE-compatible regular expressions: Captured groups are available as $1, $2, etc. Channel variables store per-call data. You can set them and use them in conditions/actions: Common built-in variables: Anti-actions execute when a condition does NOT match. They use the <anti-action> tag: Here is a production-ready default context. Save as /etc/freeswitch/dialplan/default.xml: FreeSWITCH provides two approaches to IVR: mod_ivr menus — XML-configured menu trees with automatic DTMF handling, timeouts, and invalid-input retries. Best for simple, static IVR flows. play_and_get_digits — A dialplan application that plays a prompt and collects DTMF digits. Best for dynamic IVRs with variable-based routing. You can combine both: use mod_ivr for the menu structure and play_and_get_digits for collecting account numbers or PINs. IVR menus are defined in /etc/freeswitch/autoload_configs/ivr.conf.xml: Add this to your default context in /etc/freeswitch/dialplan/default.xml: For dynamic DTMF collection (account numbers, PINs, etc.), use play_and_get_digits: play_and_get_digits parameter breakdown: Instead of pre-recorded prompts, you can use TTS: With mod_flite (built-in, free, basic quality): With mod_tts_commandline (use any external TTS engine): First configure in /etc/freeswitch/autoload_configs/tts_commandline.conf.xml: Then use in the dialplan: Putting it all together — here is the call flow: The XML for all three levels is already shown above in the ivr.conf.xml section. Just ensure the dialplan has transfer targets for each destination. FreeSWITCH voicemail is handled by mod_voicemail. Configure it in /etc/freeswitch/autoload_configs/voicemail.conf.xml: Add these extensions to your default context: For voicemail-to-email to work, you need a working mail system: Then set the notify-mailto in each user's directory XML to enable per-user email notification: Users manage their own greetings by calling *98 (voicemail check) and pressing: Greetings are stored in /var/lib/freeswitch/storage/voicemail/default/YOUR_DOMAIN/1001/. Voicemail files accumulate. Set up automated cleanup: The record_session application records both legs of a call (caller and callee) into a single file for the entire duration. Add it to any dialplan extension before the bridge: Use structured file naming for easy searching and archiving: This creates a path like: Stereo recording places each call leg on a separate audio channel (left = caller, right = callee). This is essential for speech analytics and quality review: You can control recording dynamically during a call using the API: In the dialplan, you can use DTMF-triggered recording: WAV files are large (~1 MB/minute for mono, ~2 MB/minute for stereo). Convert to MP3 for long-term storage: FreeSWITCH's mod_conference is carrier-grade — it can handle thousands of participants across hundreds of rooms. Configure profiles in /etc/freeswitch/autoload_configs/conference.conf.xml: Add to your default context: Instead of hardcoding a PIN in the conference profile, set it per-room: FreeSWITCH fires events for all conference activity. These are available via ESL: These events enable real-time dashboards, participant tracking, and integration with external management UIs. The Event Socket Layer is a TCP socket interface that allows external applications to monitor and control FreeSWITCH in real time. It is the primary integration mechanism — far more powerful than Asterisk's AMI because it is fully bidirectional and event-driven. ESL operates in two modes: The ESL listener is configured in /etc/freeswitch/autoload_configs/event_socket.conf.xml: fs_cli is itself an ESL client. Everything you type in fs_cli goes through ESL: The greenswitch library provides a clean Python ESL client. Install it: Basic monitoring application: A more advanced example — an automated call queue: For Node.js applications, use the modesl library: In outbound mode, FreeSWITCH connects to YOUR application for each call. This is ideal for complex call routing or IVR logic where you want full programmatic control. Dialplan configuration — route calls to your application: Python outbound ESL server: The simplest CDR method — writes call records to CSV files. Configure in /etc/freeswitch/autoload_configs/cdr_csv.conf.xml: CDR CSV files are written to /var/log/freeswitch/cdr-csv/. For queryable CDR data, use SQLite. Configure in /etc/freeswitch/autoload_configs/cdr_sqlite.conf.xml: The SQLite database is created at /var/lib/freeswitch/db/cdr.db. Query it: For production systems, PostgreSQL is the best CDR backend. Configure in /etc/freeswitch/autoload_configs/cdr_pg.conf.xml: Create the database and table: FreeSWITCH logging is configured in /etc/freeswitch/autoload_configs/logfile.conf.xml: For SIP debugging, enable trace on a specific profile: Set up logrotate to manage FreeSWITCH logs: This was covered in Section 3, but it bears repeating. The three most critical defaults: Restrict which IPs can register to each profile. Edit the profile XML: Define the ACL in /etc/freeswitch/autoload_configs/acl.conf.xml: Install and configure fail2ban to block brute-force SIP attacks: Create the FreeSWITCH filter at /etc/fail2ban/filter.d/freeswitch.conf: Create the jail at /etc/fail2ban/jail.d/freeswitch.conf: Encrypt SIP signaling with TLS: Enable TLS in the internal profile: After enabling TLS for signaling, enable SRTP for media encryption: Per-user SRTP enforcement in the directory: Protect against SIP floods by limiting registrations and call attempts: Set global limits in fs_cli: Kamailio handles SIP routing, load balancing, and security at the edge. FreeSWITCH handles media processing behind it. This is the standard carrier-grade architecture. Kamailio configuration snippet (routes SIP to FreeSWITCH): FreeSWITCH configuration — trust Kamailio's IP: mod_verto provides native WebRTC support without any external proxy. It uses WebSocket for signaling and handles SRTP/DTLS for media. Enable mod_verto in /etc/freeswitch/autoload_configs/modules.conf.xml: Configure mod_verto in /etc/freeswitch/autoload_configs/verto.conf.xml: Browser-side JavaScript (using verto.js): mod_xml_curl fetches configuration dynamically from an HTTP server. Instead of static XML files, FreeSWITCH asks your web app for user directory, dialplan, and configuration on every request. Enable in modules.conf.xml: Configure in /etc/freeswitch/autoload_configs/xml_curl.conf.xml: Flask backend example: Use Lua or Python scripts in the dialplan to call external REST APIs: Lua script (/etc/freeswitch/scripts/api_route.lua): You can interconnect FreeSWITCH and Asterisk via a SIP trunk between them: On FreeSWITCH — create a gateway to Asterisk. Save as /etc/freeswitch/sip_profiles/external/asterisk.xml: Route calls to Asterisk extensions (e.g., 2xxx range): On Asterisk — create the matching trunk in pjsip.conf: In this pattern, Kamailio handles all SIP routing and FreeSWITCH only processes media (IVR, conferencing, recording, transcoding): Kamailio selects the FreeSWITCH node using the dispatcher module: FreeSWITCH nodes are configured identically, with auth-calls set to false and Kamailio's IP in the ACL. Symptoms: Phone shows "Registration failed" or "403 Forbidden." Common causes and fixes: This is the most common VoIP problem. It is almost always a NAT/firewall issue. Common causes and fixes: Symptoms: Calls go to the wrong destination, get "UNALLOCATED_NUMBER," or drop. When something is not working, run through this sequence: You now have a working FreeSWITCH installation with SIP phones, trunks, IVR, voicemail, conferencing, call recording, external application control via ESL, and production security hardening. The configurations in this tutorial are production-tested patterns that you can adapt to your specific requirements. Next steps to explore: Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuseCode BlockCopyIncoming SIP INVITE │ ▼ mod_sofia (SIP stack) │ ▼ Profile (internal or external) │ ▼ Context (default or public) │ ▼ Extension matching (top-to-bottom) │ ▼ Condition evaluation (regex) │ ▼ Actions execute (answer, bridge, playback, etc.)
Incoming SIP INVITE │ ▼ mod_sofia (SIP stack) │ ▼ Profile (internal or external) │ ▼ Context (default or public) │ ▼ Extension matching (top-to-bottom) │ ▼ Condition evaluation (regex) │ ▼ Actions execute (answer, bridge, playback, etc.)
Incoming SIP INVITE │ ▼ mod_sofia (SIP stack) │ ▼ Profile (internal or external) │ ▼ Context (default or public) │ ▼ Extension matching (top-to-bottom) │ ▼ Condition evaluation (regex) │ ▼ Actions execute (answer, bridge, playback, etc.)
/etc/freeswitch/ # All configuration
├── freeswitch.xml # Master config (includes everything)
├── vars.xml # Global variables (domain, passwords, IPs)
├── autoload_configs/ # Module configurations
│ ├── modules.conf.xml # Which modules to load
│ ├── sofia.conf.xml # SIP module config (includes profiles)
│ ├── conference.conf.xml # Conference profiles
│ ├── voicemail.conf.xml # Voicemail settings
│ ├── ivr.conf.xml # IVR menu definitions
│ ├── cdr_csv.conf.xml # CDR output config
│ └── event_socket.conf.xml # ESL listener config
├── sip_profiles/ # SIP profile definitions
│ ├── internal.xml # Internal profile (phones, port 5060)
│ ├── external.xml # External profile (trunks, port 5080)
│ └── external/ # Gateway (trunk) definitions
│ └── my_provider.xml # One file per trunk
├── dialplan/ # Call routing rules
│ ├── default.xml # Internal context (phone-to-phone)
│ ├── public.xml # External context (inbound from trunks)
│ └── default/ # Additional dialplan fragments
├── directory/ # User/phone definitions
│ └── default/ # Domain directory
│ ├── 1000.xml # Extension 1000
│ ├── 1001.xml # Extension 1001
│ └── ...
└── lang/ # Language/sound file mappings /var/lib/freeswitch/ # Runtime data
├── db/ # SQLite databases (registrations, etc.)
├── recordings/ # Call recordings
├── storage/ # Voicemail, fax storage
└── sounds/ # Audio/prompt files /var/log/freeswitch/ # Logs
├── freeswitch.log # Main log (rotated)
└── cdr-csv/ # CDR files
/etc/freeswitch/ # All configuration
├── freeswitch.xml # Master config (includes everything)
├── vars.xml # Global variables (domain, passwords, IPs)
├── autoload_configs/ # Module configurations
│ ├── modules.conf.xml # Which modules to load
│ ├── sofia.conf.xml # SIP module config (includes profiles)
│ ├── conference.conf.xml # Conference profiles
│ ├── voicemail.conf.xml # Voicemail settings
│ ├── ivr.conf.xml # IVR menu definitions
│ ├── cdr_csv.conf.xml # CDR output config
│ └── event_socket.conf.xml # ESL listener config
├── sip_profiles/ # SIP profile definitions
│ ├── internal.xml # Internal profile (phones, port 5060)
│ ├── external.xml # External profile (trunks, port 5080)
│ └── external/ # Gateway (trunk) definitions
│ └── my_provider.xml # One file per trunk
├── dialplan/ # Call routing rules
│ ├── default.xml # Internal context (phone-to-phone)
│ ├── public.xml # External context (inbound from trunks)
│ └── default/ # Additional dialplan fragments
├── directory/ # User/phone definitions
│ └── default/ # Domain directory
│ ├── 1000.xml # Extension 1000
│ ├── 1001.xml # Extension 1001
│ └── ...
└── lang/ # Language/sound file mappings /var/lib/freeswitch/ # Runtime data
├── db/ # SQLite databases (registrations, etc.)
├── recordings/ # Call recordings
├── storage/ # Voicemail, fax storage
└── sounds/ # Audio/prompt files /var/log/freeswitch/ # Logs
├── freeswitch.log # Main log (rotated)
└── cdr-csv/ # CDR files
/etc/freeswitch/ # All configuration
├── freeswitch.xml # Master config (includes everything)
├── vars.xml # Global variables (domain, passwords, IPs)
├── autoload_configs/ # Module configurations
│ ├── modules.conf.xml # Which modules to load
│ ├── sofia.conf.xml # SIP module config (includes profiles)
│ ├── conference.conf.xml # Conference profiles
│ ├── voicemail.conf.xml # Voicemail settings
│ ├── ivr.conf.xml # IVR menu definitions
│ ├── cdr_csv.conf.xml # CDR output config
│ └── event_socket.conf.xml # ESL listener config
├── sip_profiles/ # SIP profile definitions
│ ├── internal.xml # Internal profile (phones, port 5060)
│ ├── external.xml # External profile (trunks, port 5080)
│ └── external/ # Gateway (trunk) definitions
│ └── my_provider.xml # One file per trunk
├── dialplan/ # Call routing rules
│ ├── default.xml # Internal context (phone-to-phone)
│ ├── public.xml # External context (inbound from trunks)
│ └── default/ # Additional dialplan fragments
├── directory/ # User/phone definitions
│ └── default/ # Domain directory
│ ├── 1000.xml # Extension 1000
│ ├── 1001.xml # Extension 1001
│ └── ...
└── lang/ # Language/sound file mappings /var/lib/freeswitch/ # Runtime data
├── db/ # SQLite databases (registrations, etc.)
├── recordings/ # Call recordings
├── storage/ # Voicemail, fax storage
└── sounds/ # Audio/prompt files /var/log/freeswitch/ # Logs
├── freeswitch.log # Main log (rotated)
└── cdr-csv/ # CDR files
# Install prerequisites
apt-get update && apt-get install -y gnupg2 wget lsb-release # Set your SignalWire PAT
TOKEN="YOUR_SIGNALWIRE_PAT_HERE" # Add SignalWire GPG key
wget --http-user=signalwire --http-password=$TOKEN \ -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \ https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg # Add repository
echo "machine freeswitch.signalwire.com login signalwire password $TOKEN" \ > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] \ https://freeswitch.signalwire.com/repo/deb/debian-release/ bookworm main" \ > /etc/apt/sources.list.d/freeswitch.list
# Install prerequisites
apt-get update && apt-get install -y gnupg2 wget lsb-release # Set your SignalWire PAT
TOKEN="YOUR_SIGNALWIRE_PAT_HERE" # Add SignalWire GPG key
wget --http-user=signalwire --http-password=$TOKEN \ -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \ https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg # Add repository
echo "machine freeswitch.signalwire.com login signalwire password $TOKEN" \ > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] \ https://freeswitch.signalwire.com/repo/deb/debian-release/ bookworm main" \ > /etc/apt/sources.list.d/freeswitch.list
# Install prerequisites
apt-get update && apt-get install -y gnupg2 wget lsb-release # Set your SignalWire PAT
TOKEN="YOUR_SIGNALWIRE_PAT_HERE" # Add SignalWire GPG key
wget --http-user=signalwire --http-password=$TOKEN \ -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \ https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg # Add repository
echo "machine freeswitch.signalwire.com login signalwire password $TOKEN" \ > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] \ https://freeswitch.signalwire.com/repo/deb/debian-release/ bookworm main" \ > /etc/apt/sources.list.d/freeswitch.list
# Install prerequisites
apt-get update && apt-get install -y gnupg2 wget lsb-release # Set your SignalWire PAT
TOKEN="YOUR_SIGNALWIRE_PAT_HERE" # Add SignalWire GPG key
wget --http-user=signalwire --http-password=$TOKEN \ -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \ https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg # Add repository
echo "machine freeswitch.signalwire.com login signalwire password $TOKEN" \ > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] \ https://freeswitch.signalwire.com/repo/deb/debian-release/ noble main" \ > /etc/apt/sources.list.d/freeswitch.list
# Install prerequisites
apt-get update && apt-get install -y gnupg2 wget lsb-release # Set your SignalWire PAT
TOKEN="YOUR_SIGNALWIRE_PAT_HERE" # Add SignalWire GPG key
wget --http-user=signalwire --http-password=$TOKEN \ -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \ https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg # Add repository
echo "machine freeswitch.signalwire.com login signalwire password $TOKEN" \ > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] \ https://freeswitch.signalwire.com/repo/deb/debian-release/ noble main" \ > /etc/apt/sources.list.d/freeswitch.list
# Install prerequisites
apt-get update && apt-get install -y gnupg2 wget lsb-release # Set your SignalWire PAT
TOKEN="YOUR_SIGNALWIRE_PAT_HERE" # Add SignalWire GPG key
wget --http-user=signalwire --http-password=$TOKEN \ -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \ https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg # Add repository
echo "machine freeswitch.signalwire.com login signalwire password $TOKEN" \ > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] \ https://freeswitch.signalwire.com/repo/deb/debian-release/ noble main" \ > /etc/apt/sources.list.d/freeswitch.list
apt-get update # Full install (recommended for learning — includes all modules)
apt-get install -y freeswitch-meta-all # OR minimal install (production — add modules as needed)
# apt-get install -y freeswitch-meta-vanilla
apt-get update # Full install (recommended for learning — includes all modules)
apt-get install -y freeswitch-meta-all # OR minimal install (production — add modules as needed)
# apt-get install -y freeswitch-meta-vanilla
apt-get update # Full install (recommended for learning — includes all modules)
apt-get install -y freeswitch-meta-all # OR minimal install (production — add modules as needed)
# apt-get install -y freeswitch-meta-vanilla
# Enable and start FreeSWITCH
systemctl enable freeswitch
systemctl start freeswitch # Verify it is running
systemctl status freeswitch # Check ownership (FreeSWITCH runs as freeswitch user)
ls -la /etc/freeswitch/
ls -la /var/lib/freeswitch/
ls -la /var/log/freeswitch/ # Fix ownership if needed
chown -R freeswitch:freeswitch /etc/freeswitch
chown -R freeswitch:freeswitch /var/lib/freeswitch
chown -R freeswitch:freeswitch /var/log/freeswitch
# Enable and start FreeSWITCH
systemctl enable freeswitch
systemctl start freeswitch # Verify it is running
systemctl status freeswitch # Check ownership (FreeSWITCH runs as freeswitch user)
ls -la /etc/freeswitch/
ls -la /var/lib/freeswitch/
ls -la /var/log/freeswitch/ # Fix ownership if needed
chown -R freeswitch:freeswitch /etc/freeswitch
chown -R freeswitch:freeswitch /var/lib/freeswitch
chown -R freeswitch:freeswitch /var/log/freeswitch
# Enable and start FreeSWITCH
systemctl enable freeswitch
systemctl start freeswitch # Verify it is running
systemctl status freeswitch # Check ownership (FreeSWITCH runs as freeswitch user)
ls -la /etc/freeswitch/
ls -la /var/lib/freeswitch/
ls -la /var/log/freeswitch/ # Fix ownership if needed
chown -R freeswitch:freeswitch /etc/freeswitch
chown -R freeswitch:freeswitch /var/lib/freeswitch
chown -R freeswitch:freeswitch /var/log/freeswitch
# Install build dependencies
apt-get update && apt-get install -y \ build-essential cmake automake autoconf libtool pkg-config \ libssl-dev zlib1g-dev libdb-dev libexpat1-dev libcurl4-openssl-dev \ libpcre3-dev libspeex-dev libspeexdsp-dev libsqlite3-dev \ libedit-dev libldns-dev libpq-dev libtiff-dev libjpeg-dev \ libavformat-dev libswscale-dev liblua5.3-dev \ libopus-dev libsndfile1-dev uuid-dev \ python3-dev erlang-dev yasm nasm \ git wget unzip # Clone the repository
cd /usr/local/src
git clone https://github.com/signalwire/freeswitch.git -b v1.10 freeswitch
cd freeswitch # Install libks and signalwire-c dependencies
git clone https://github.com/signalwire/libks.git
cd libks && cmake . -DCMAKE_INSTALL_PREFIX=/usr && make install && cd .. git clone https://github.com/signalwire/signalwire-c.git
cd signalwire-c && cmake . -DCMAKE_INSTALL_PREFIX=/usr && make install && cd .. # Bootstrap and configure
cd /usr/local/src/freeswitch
./bootstrap.sh -j # Edit modules.conf to enable/disable modules before building
# nano modules.conf ./configure --prefix=/usr/local/freeswitch # Build and install
make -j$(nproc)
make install # Install sounds and music on hold
make cd-sounds-install cd-moh-install # Create system user
useradd -r -s /bin/false freeswitch
chown -R freeswitch:freeswitch /usr/local/freeswitch
# Install build dependencies
apt-get update && apt-get install -y \ build-essential cmake automake autoconf libtool pkg-config \ libssl-dev zlib1g-dev libdb-dev libexpat1-dev libcurl4-openssl-dev \ libpcre3-dev libspeex-dev libspeexdsp-dev libsqlite3-dev \ libedit-dev libldns-dev libpq-dev libtiff-dev libjpeg-dev \ libavformat-dev libswscale-dev liblua5.3-dev \ libopus-dev libsndfile1-dev uuid-dev \ python3-dev erlang-dev yasm nasm \ git wget unzip # Clone the repository
cd /usr/local/src
git clone https://github.com/signalwire/freeswitch.git -b v1.10 freeswitch
cd freeswitch # Install libks and signalwire-c dependencies
git clone https://github.com/signalwire/libks.git
cd libks && cmake . -DCMAKE_INSTALL_PREFIX=/usr && make install && cd .. git clone https://github.com/signalwire/signalwire-c.git
cd signalwire-c && cmake . -DCMAKE_INSTALL_PREFIX=/usr && make install && cd .. # Bootstrap and configure
cd /usr/local/src/freeswitch
./bootstrap.sh -j # Edit modules.conf to enable/disable modules before building
# nano modules.conf ./configure --prefix=/usr/local/freeswitch # Build and install
make -j$(nproc)
make install # Install sounds and music on hold
make cd-sounds-install cd-moh-install # Create system user
useradd -r -s /bin/false freeswitch
chown -R freeswitch:freeswitch /usr/local/freeswitch
# Install build dependencies
apt-get update && apt-get install -y \ build-essential cmake automake autoconf libtool pkg-config \ libssl-dev zlib1g-dev libdb-dev libexpat1-dev libcurl4-openssl-dev \ libpcre3-dev libspeex-dev libspeexdsp-dev libsqlite3-dev \ libedit-dev libldns-dev libpq-dev libtiff-dev libjpeg-dev \ libavformat-dev libswscale-dev liblua5.3-dev \ libopus-dev libsndfile1-dev uuid-dev \ python3-dev erlang-dev yasm nasm \ git wget unzip # Clone the repository
cd /usr/local/src
git clone https://github.com/signalwire/freeswitch.git -b v1.10 freeswitch
cd freeswitch # Install libks and signalwire-c dependencies
git clone https://github.com/signalwire/libks.git
cd libks && cmake . -DCMAKE_INSTALL_PREFIX=/usr && make install && cd .. git clone https://github.com/signalwire/signalwire-c.git
cd signalwire-c && cmake . -DCMAKE_INSTALL_PREFIX=/usr && make install && cd .. # Bootstrap and configure
cd /usr/local/src/freeswitch
./bootstrap.sh -j # Edit modules.conf to enable/disable modules before building
# nano modules.conf ./configure --prefix=/usr/local/freeswitch # Build and install
make -j$(nproc)
make install # Install sounds and music on hold
make cd-sounds-install cd-moh-install # Create system user
useradd -r -s /bin/false freeswitch
chown -R freeswitch:freeswitch /usr/local/freeswitch
# SIP signaling (internal profile)
ufw allow 5060/tcp comment "FreeSWITCH SIP TCP internal"
ufw allow 5060/udp comment "FreeSWITCH SIP UDP internal" # SIP signaling (external profile)
ufw allow 5080/tcp comment "FreeSWITCH SIP TCP external"
ufw allow 5080/udp comment "FreeSWITCH SIP UDP external" # RTP media (voice/video)
ufw allow 16384:32768/udp comment "FreeSWITCH RTP media" # ESL (Event Socket Layer) - restrict to trusted IPs only
ufw allow from 10.0.0.0/8 to any port 8021 proto tcp comment "FreeSWITCH ESL" # WebRTC (if using mod_verto)
# ufw allow 8081/tcp comment "FreeSWITCH Verto WSS"
# ufw allow 8082/tcp comment "FreeSWITCH Verto WSS" ufw enable
# SIP signaling (internal profile)
ufw allow 5060/tcp comment "FreeSWITCH SIP TCP internal"
ufw allow 5060/udp comment "FreeSWITCH SIP UDP internal" # SIP signaling (external profile)
ufw allow 5080/tcp comment "FreeSWITCH SIP TCP external"
ufw allow 5080/udp comment "FreeSWITCH SIP UDP external" # RTP media (voice/video)
ufw allow 16384:32768/udp comment "FreeSWITCH RTP media" # ESL (Event Socket Layer) - restrict to trusted IPs only
ufw allow from 10.0.0.0/8 to any port 8021 proto tcp comment "FreeSWITCH ESL" # WebRTC (if using mod_verto)
# ufw allow 8081/tcp comment "FreeSWITCH Verto WSS"
# ufw allow 8082/tcp comment "FreeSWITCH Verto WSS" ufw enable
# SIP signaling (internal profile)
ufw allow 5060/tcp comment "FreeSWITCH SIP TCP internal"
ufw allow 5060/udp comment "FreeSWITCH SIP UDP internal" # SIP signaling (external profile)
ufw allow 5080/tcp comment "FreeSWITCH SIP TCP external"
ufw allow 5080/udp comment "FreeSWITCH SIP UDP external" # RTP media (voice/video)
ufw allow 16384:32768/udp comment "FreeSWITCH RTP media" # ESL (Event Socket Layer) - restrict to trusted IPs only
ufw allow from 10.0.0.0/8 to any port 8021 proto tcp comment "FreeSWITCH ESL" # WebRTC (if using mod_verto)
# ufw allow 8081/tcp comment "FreeSWITCH Verto WSS"
# ufw allow 8082/tcp comment "FreeSWITCH Verto WSS" ufw enable
# SIP
iptables -A INPUT -p udp --dport 5060 -j ACCEPT
iptables -A INPUT -p tcp --dport 5060 -j ACCEPT
iptables -A INPUT -p udp --dport 5080 -j ACCEPT
iptables -A INPUT -p tcp --dport 5080 -j ACCEPT # RTP
iptables -A INPUT -p udp --dport 16384:32768 -j ACCEPT # ESL (restrict source)
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 8021 -j ACCEPT
iptables -A INPUT -p tcp --dport 8021 -j DROP
# SIP
iptables -A INPUT -p udp --dport 5060 -j ACCEPT
iptables -A INPUT -p tcp --dport 5060 -j ACCEPT
iptables -A INPUT -p udp --dport 5080 -j ACCEPT
iptables -A INPUT -p tcp --dport 5080 -j ACCEPT # RTP
iptables -A INPUT -p udp --dport 16384:32768 -j ACCEPT # ESL (restrict source)
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 8021 -j ACCEPT
iptables -A INPUT -p tcp --dport 8021 -j DROP
# SIP
iptables -A INPUT -p udp --dport 5060 -j ACCEPT
iptables -A INPUT -p tcp --dport 5060 -j ACCEPT
iptables -A INPUT -p udp --dport 5080 -j ACCEPT
iptables -A INPUT -p tcp --dport 5080 -j ACCEPT # RTP
iptables -A INPUT -p udp --dport 16384:32768 -j ACCEPT # ESL (restrict source)
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 8021 -j ACCEPT
iptables -A INPUT -p tcp --dport 8021 -j DROP
# Connect to FreeSWITCH CLI
fs_cli # Inside fs_cli, check status
freeswitch@server> status # Expected output:
# UP 0 years, 0 days, 0 hours, 5 minutes, 23 seconds, 456 milliseconds, 789 microseconds
# FreeSWITCH (Version 1.10.x ...) is ready
# 0 session(s) since startup
# 0 session(s) - peak 0, last 5min 0
# 0 session(s) per Sec out of max 30, peak 0, last 5min 0
# 1000 session(s) max
# min idle cpu 0.00/99.67 # Check SIP profiles
freeswitch@server> sofia status # Expected output:
# Name Type Data State
# =================================================================================================
# internal profile sip:mod_sofia@YOUR_SERVER_IP:5060 RUNNING (0)
# external profile sip:mod_sofia@YOUR_SERVER_IP:5080 RUNNING (0)
# ... (other profiles) # Check loaded modules
freeswitch@server> module_exists mod_sofia
# true # Exit CLI
freeswitch@server> /exit
# Connect to FreeSWITCH CLI
fs_cli # Inside fs_cli, check status
freeswitch@server> status # Expected output:
# UP 0 years, 0 days, 0 hours, 5 minutes, 23 seconds, 456 milliseconds, 789 microseconds
# FreeSWITCH (Version 1.10.x ...) is ready
# 0 session(s) since startup
# 0 session(s) - peak 0, last 5min 0
# 0 session(s) per Sec out of max 30, peak 0, last 5min 0
# 1000 session(s) max
# min idle cpu 0.00/99.67 # Check SIP profiles
freeswitch@server> sofia status # Expected output:
# Name Type Data State
# =================================================================================================
# internal profile sip:mod_sofia@YOUR_SERVER_IP:5060 RUNNING (0)
# external profile sip:mod_sofia@YOUR_SERVER_IP:5080 RUNNING (0)
# ... (other profiles) # Check loaded modules
freeswitch@server> module_exists mod_sofia
# true # Exit CLI
freeswitch@server> /exit
# Connect to FreeSWITCH CLI
fs_cli # Inside fs_cli, check status
freeswitch@server> status # Expected output:
# UP 0 years, 0 days, 0 hours, 5 minutes, 23 seconds, 456 milliseconds, 789 microseconds
# FreeSWITCH (Version 1.10.x ...) is ready
# 0 session(s) since startup
# 0 session(s) - peak 0, last 5min 0
# 0 session(s) per Sec out of max 30, peak 0, last 5min 0
# 1000 session(s) max
# min idle cpu 0.00/99.67 # Check SIP profiles
freeswitch@server> sofia status # Expected output:
# Name Type Data State
# =================================================================================================
# internal profile sip:mod_sofia@YOUR_SERVER_IP:5060 RUNNING (0)
# external profile sip:mod_sofia@YOUR_SERVER_IP:5080 RUNNING (0)
# ... (other profiles) # Check loaded modules
freeswitch@server> module_exists mod_sofia
# true # Exit CLI
freeswitch@server> /exit
# Edit global variables
nano /etc/freeswitch/vars.xml
# Edit global variables
nano /etc/freeswitch/vars.xml
# Edit global variables
nano /etc/freeswitch/vars.xml
<!-- CHANGE THIS — default password for all extensions -->
<X-PRE-PROCESS cmd="set" data="default_password=PUT_A_STRONG_PASSWORD_HERE"/> <!-- Domain — set to your server IP or FQDN -->
<X-PRE-PROCESS cmd="set" data="domain=$${local_ip_v4}"/>
<!-- CHANGE THIS — default password for all extensions -->
<X-PRE-PROCESS cmd="set" data="default_password=PUT_A_STRONG_PASSWORD_HERE"/> <!-- Domain — set to your server IP or FQDN -->
<X-PRE-PROCESS cmd="set" data="domain=$${local_ip_v4}"/>
<!-- CHANGE THIS — default password for all extensions -->
<X-PRE-PROCESS cmd="set" data="default_password=PUT_A_STRONG_PASSWORD_HERE"/> <!-- Domain — set to your server IP or FQDN -->
<X-PRE-PROCESS cmd="set" data="domain=$${local_ip_v4}"/>
nano /etc/freeswitch/autoload_configs/event_socket.conf.xml
nano /etc/freeswitch/autoload_configs/event_socket.conf.xml
nano /etc/freeswitch/autoload_configs/event_socket.conf.xml
<configuration name="event_socket.conf" description="Socket Client"> <settings> <param name="nat-map" value="false"/> <param name="listen-ip" value="127.0.0.1"/> <param name="listen-port" value="8021"/> <!-- CHANGE THIS from ClueCon --> <param name="password" value="YOUR_SECURE_ESL_PASSWORD"/> <!--<param name="apply-inbound-acl" value="loopback.auto"/>--> </settings>
</configuration>
<configuration name="event_socket.conf" description="Socket Client"> <settings> <param name="nat-map" value="false"/> <param name="listen-ip" value="127.0.0.1"/> <param name="listen-port" value="8021"/> <!-- CHANGE THIS from ClueCon --> <param name="password" value="YOUR_SECURE_ESL_PASSWORD"/> <!--<param name="apply-inbound-acl" value="loopback.auto"/>--> </settings>
</configuration>
<configuration name="event_socket.conf" description="Socket Client"> <settings> <param name="nat-map" value="false"/> <param name="listen-ip" value="127.0.0.1"/> <param name="listen-port" value="8021"/> <!-- CHANGE THIS from ClueCon --> <param name="password" value="YOUR_SECURE_ESL_PASSWORD"/> <!--<param name="apply-inbound-acl" value="loopback.auto"/>--> </settings>
</configuration>
# From fs_cli
fs_cli -x "reloadxml" # Or restart the service
systemctl restart freeswitch
# From fs_cli
fs_cli -x "reloadxml" # Or restart the service
systemctl restart freeswitch
# From fs_cli
fs_cli -x "reloadxml" # Or restart the service
systemctl restart freeswitch
<profile name="internal"> <settings> <!-- Network --> <param name="sip-ip" value="$${local_ip_v4}"/> <param name="sip-port" value="5060"/> <param name="rtp-ip" value="$${local_ip_v4}"/> <!-- NAT: Set this to your public IP if behind NAT --> <!-- <param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/> --> <!-- <param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/> --> <!-- Dialplan context for calls arriving on this profile --> <param name="context" value="default"/> <!-- Codec preferences (in order) --> <param name="inbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <param name="outbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <!-- DTMF handling --> <param name="dtmf-type" value="rfc2833"/> <!-- Registration --> <param name="inbound-reg-force-matching-username" value="true"/> <param name="auth-calls" value="true"/> <param name="apply-nat-acl" value="nat.auto"/> <!-- Recording --> <param name="record-path" value="$${recordings_dir}"/> <param name="record-template" value="${caller_id_number}.${target_domain}.${strftime(%Y-%m-%d-%H-%M-%S)}.wav"/> <!-- Timers --> <param name="session-timeout" value="1800"/> <param name="rtp-timeout-sec" value="300"/> <param name="rtp-hold-timeout-sec" value="1800"/> <!-- Hold music --> <param name="hold-music" value="$${hold_music}"/> </settings>
</profile>
<profile name="internal"> <settings> <!-- Network --> <param name="sip-ip" value="$${local_ip_v4}"/> <param name="sip-port" value="5060"/> <param name="rtp-ip" value="$${local_ip_v4}"/> <!-- NAT: Set this to your public IP if behind NAT --> <!-- <param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/> --> <!-- <param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/> --> <!-- Dialplan context for calls arriving on this profile --> <param name="context" value="default"/> <!-- Codec preferences (in order) --> <param name="inbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <param name="outbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <!-- DTMF handling --> <param name="dtmf-type" value="rfc2833"/> <!-- Registration --> <param name="inbound-reg-force-matching-username" value="true"/> <param name="auth-calls" value="true"/> <param name="apply-nat-acl" value="nat.auto"/> <!-- Recording --> <param name="record-path" value="$${recordings_dir}"/> <param name="record-template" value="${caller_id_number}.${target_domain}.${strftime(%Y-%m-%d-%H-%M-%S)}.wav"/> <!-- Timers --> <param name="session-timeout" value="1800"/> <param name="rtp-timeout-sec" value="300"/> <param name="rtp-hold-timeout-sec" value="1800"/> <!-- Hold music --> <param name="hold-music" value="$${hold_music}"/> </settings>
</profile>
<profile name="internal"> <settings> <!-- Network --> <param name="sip-ip" value="$${local_ip_v4}"/> <param name="sip-port" value="5060"/> <param name="rtp-ip" value="$${local_ip_v4}"/> <!-- NAT: Set this to your public IP if behind NAT --> <!-- <param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/> --> <!-- <param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/> --> <!-- Dialplan context for calls arriving on this profile --> <param name="context" value="default"/> <!-- Codec preferences (in order) --> <param name="inbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <param name="outbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <!-- DTMF handling --> <param name="dtmf-type" value="rfc2833"/> <!-- Registration --> <param name="inbound-reg-force-matching-username" value="true"/> <param name="auth-calls" value="true"/> <param name="apply-nat-acl" value="nat.auto"/> <!-- Recording --> <param name="record-path" value="$${recordings_dir}"/> <param name="record-template" value="${caller_id_number}.${target_domain}.${strftime(%Y-%m-%d-%H-%M-%S)}.wav"/> <!-- Timers --> <param name="session-timeout" value="1800"/> <param name="rtp-timeout-sec" value="300"/> <param name="rtp-hold-timeout-sec" value="1800"/> <!-- Hold music --> <param name="hold-music" value="$${hold_music}"/> </settings>
</profile>
<profile name="external"> <settings> <param name="sip-ip" value="$${local_ip_v4}"/> <param name="sip-port" value="5080"/> <param name="rtp-ip" value="$${local_ip_v4}"/> <!-- NAT: uncomment and set if behind NAT --> <!-- <param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/> --> <!-- <param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/> --> <!-- Inbound calls from trunks go to 'public' context --> <param name="context" value="public"/> <!-- Codecs --> <param name="inbound-codec-string" value="PCMU,PCMA,G722"/> <param name="outbound-codec-string" value="PCMU,PCMA,G722"/> <!-- Do NOT require authentication (trunks use IP-based auth) --> <param name="auth-calls" value="false"/> <!-- DTMF --> <param name="dtmf-type" value="rfc2833"/> <!-- Aggressively reclaim failed channels --> <param name="rtp-timeout-sec" value="300"/> </settings> <!-- Gateway definitions are in sip_profiles/external/ directory --> <gateways> <X-PRE-PROCESS cmd="include" data="external/*.xml"/> </gateways>
</profile>
<profile name="external"> <settings> <param name="sip-ip" value="$${local_ip_v4}"/> <param name="sip-port" value="5080"/> <param name="rtp-ip" value="$${local_ip_v4}"/> <!-- NAT: uncomment and set if behind NAT --> <!-- <param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/> --> <!-- <param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/> --> <!-- Inbound calls from trunks go to 'public' context --> <param name="context" value="public"/> <!-- Codecs --> <param name="inbound-codec-string" value="PCMU,PCMA,G722"/> <param name="outbound-codec-string" value="PCMU,PCMA,G722"/> <!-- Do NOT require authentication (trunks use IP-based auth) --> <param name="auth-calls" value="false"/> <!-- DTMF --> <param name="dtmf-type" value="rfc2833"/> <!-- Aggressively reclaim failed channels --> <param name="rtp-timeout-sec" value="300"/> </settings> <!-- Gateway definitions are in sip_profiles/external/ directory --> <gateways> <X-PRE-PROCESS cmd="include" data="external/*.xml"/> </gateways>
</profile>
<profile name="external"> <settings> <param name="sip-ip" value="$${local_ip_v4}"/> <param name="sip-port" value="5080"/> <param name="rtp-ip" value="$${local_ip_v4}"/> <!-- NAT: uncomment and set if behind NAT --> <!-- <param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/> --> <!-- <param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/> --> <!-- Inbound calls from trunks go to 'public' context --> <param name="context" value="public"/> <!-- Codecs --> <param name="inbound-codec-string" value="PCMU,PCMA,G722"/> <param name="outbound-codec-string" value="PCMU,PCMA,G722"/> <!-- Do NOT require authentication (trunks use IP-based auth) --> <param name="auth-calls" value="false"/> <!-- DTMF --> <param name="dtmf-type" value="rfc2833"/> <!-- Aggressively reclaim failed channels --> <param name="rtp-timeout-sec" value="300"/> </settings> <!-- Gateway definitions are in sip_profiles/external/ directory --> <gateways> <X-PRE-PROCESS cmd="include" data="external/*.xml"/> </gateways>
</profile>
<include> <user id="1001"> <params> <param name="password" value="Str0ng_P@ss_1001!"/> <param name="vm-password" value="1001"/> </params> <variables> <variable name="toll_allow" value="domestic,international,local"/> <variable name="accountcode" value="1001"/> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="John Smith"/> <variable name="effective_caller_id_number" value="1001"/> <variable name="outbound_caller_id_name" value="My Company"/> <variable name="outbound_caller_id_number" value="15551234567"/> <variable name="callgroup" value="sales"/> </variables> </user>
</include>
<include> <user id="1001"> <params> <param name="password" value="Str0ng_P@ss_1001!"/> <param name="vm-password" value="1001"/> </params> <variables> <variable name="toll_allow" value="domestic,international,local"/> <variable name="accountcode" value="1001"/> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="John Smith"/> <variable name="effective_caller_id_number" value="1001"/> <variable name="outbound_caller_id_name" value="My Company"/> <variable name="outbound_caller_id_number" value="15551234567"/> <variable name="callgroup" value="sales"/> </variables> </user>
</include>
<include> <user id="1001"> <params> <param name="password" value="Str0ng_P@ss_1001!"/> <param name="vm-password" value="1001"/> </params> <variables> <variable name="toll_allow" value="domestic,international,local"/> <variable name="accountcode" value="1001"/> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="John Smith"/> <variable name="effective_caller_id_number" value="1001"/> <variable name="outbound_caller_id_name" value="My Company"/> <variable name="outbound_caller_id_number" value="15551234567"/> <variable name="callgroup" value="sales"/> </variables> </user>
</include>
#!/bin/bash
# create-extensions.sh
# Creates 10 SIP extensions (1001-1010) for FreeSWITCH DIRECTORY="/etc/freeswitch/directory/default" declare -A USERS=( [1001]="Alice Johnson:sales" [1002]="Bob Williams:sales" [1003]="Carol Davis:sales" [1004]="Dan Miller:support" [1005]="Eve Wilson:support" [1006]="Frank Brown:support" [1007]="Grace Taylor:billing" [1008]="Hank Anderson:billing" [1009]="Ivy Martinez:management" [1010]="Jack Thompson:management"
) for EXT in "${!USERS[@]}"; do IFS=':' read -r NAME GROUP <<< "${USERS[$EXT]}" # Generate a random password PASS=$(openssl rand -base64 12 | tr -dc 'A-Za-z0-9' | head -c 16) cat > "${DIRECTORY}/${EXT}.xml" << XMLEOF
<include> <user id="${EXT}"> <params> <param name="password" value="${PASS}"/> <param name="vm-password" value="${EXT}"/> </params> <variables> <variable name="toll_allow" value="domestic,local"/> <variable name="accountcode" value="${EXT}"/> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="${NAME}"/> <variable name="effective_caller_id_number" value="${EXT}"/> <variable name="outbound_caller_id_name" value="My Company"/> <variable name="outbound_caller_id_number" value="15551234567"/> <variable name="callgroup" value="${GROUP}"/> </variables> </user>
</include>
XMLEOF echo "Created ${EXT} — ${NAME} (${GROUP}) — Password: ${PASS}"
done chown -R freeswitch:freeswitch "${DIRECTORY}"
echo ""
echo "Done. Run 'fs_cli -x reloadxml' to apply."
echo "IMPORTANT: Save the passwords above — they will not be displayed again."
#!/bin/bash
# create-extensions.sh
# Creates 10 SIP extensions (1001-1010) for FreeSWITCH DIRECTORY="/etc/freeswitch/directory/default" declare -A USERS=( [1001]="Alice Johnson:sales" [1002]="Bob Williams:sales" [1003]="Carol Davis:sales" [1004]="Dan Miller:support" [1005]="Eve Wilson:support" [1006]="Frank Brown:support" [1007]="Grace Taylor:billing" [1008]="Hank Anderson:billing" [1009]="Ivy Martinez:management" [1010]="Jack Thompson:management"
) for EXT in "${!USERS[@]}"; do IFS=':' read -r NAME GROUP <<< "${USERS[$EXT]}" # Generate a random password PASS=$(openssl rand -base64 12 | tr -dc 'A-Za-z0-9' | head -c 16) cat > "${DIRECTORY}/${EXT}.xml" << XMLEOF
<include> <user id="${EXT}"> <params> <param name="password" value="${PASS}"/> <param name="vm-password" value="${EXT}"/> </params> <variables> <variable name="toll_allow" value="domestic,local"/> <variable name="accountcode" value="${EXT}"/> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="${NAME}"/> <variable name="effective_caller_id_number" value="${EXT}"/> <variable name="outbound_caller_id_name" value="My Company"/> <variable name="outbound_caller_id_number" value="15551234567"/> <variable name="callgroup" value="${GROUP}"/> </variables> </user>
</include>
XMLEOF echo "Created ${EXT} — ${NAME} (${GROUP}) — Password: ${PASS}"
done chown -R freeswitch:freeswitch "${DIRECTORY}"
echo ""
echo "Done. Run 'fs_cli -x reloadxml' to apply."
echo "IMPORTANT: Save the passwords above — they will not be displayed again."
#!/bin/bash
# create-extensions.sh
# Creates 10 SIP extensions (1001-1010) for FreeSWITCH DIRECTORY="/etc/freeswitch/directory/default" declare -A USERS=( [1001]="Alice Johnson:sales" [1002]="Bob Williams:sales" [1003]="Carol Davis:sales" [1004]="Dan Miller:support" [1005]="Eve Wilson:support" [1006]="Frank Brown:support" [1007]="Grace Taylor:billing" [1008]="Hank Anderson:billing" [1009]="Ivy Martinez:management" [1010]="Jack Thompson:management"
) for EXT in "${!USERS[@]}"; do IFS=':' read -r NAME GROUP <<< "${USERS[$EXT]}" # Generate a random password PASS=$(openssl rand -base64 12 | tr -dc 'A-Za-z0-9' | head -c 16) cat > "${DIRECTORY}/${EXT}.xml" << XMLEOF
<include> <user id="${EXT}"> <params> <param name="password" value="${PASS}"/> <param name="vm-password" value="${EXT}"/> </params> <variables> <variable name="toll_allow" value="domestic,local"/> <variable name="accountcode" value="${EXT}"/> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="${NAME}"/> <variable name="effective_caller_id_number" value="${EXT}"/> <variable name="outbound_caller_id_name" value="My Company"/> <variable name="outbound_caller_id_number" value="15551234567"/> <variable name="callgroup" value="${GROUP}"/> </variables> </user>
</include>
XMLEOF echo "Created ${EXT} — ${NAME} (${GROUP}) — Password: ${PASS}"
done chown -R freeswitch:freeswitch "${DIRECTORY}"
echo ""
echo "Done. Run 'fs_cli -x reloadxml' to apply."
echo "IMPORTANT: Save the passwords above — they will not be displayed again."
chmod +x create-extensions.sh
./create-extensions.sh
chmod +x create-extensions.sh
./create-extensions.sh
chmod +x create-extensions.sh
./create-extensions.sh
<include> <gateway name="my_provider"> <!-- Provider credentials --> <param name="username" value="your_sip_username"/> <param name="password" value="your_sip_password"/> <param name="realm" value="sip.provider.com"/> <param name="proxy" value="sip.provider.com"/> <!-- Registration (some providers require it, some use IP auth) --> <param name="register" value="true"/> <param name="register-transport" value="udp"/> <!-- Caller ID --> <param name="caller-id-in-from" value="true"/> <!-- Retry on failure --> <param name="retry-seconds" value="30"/> <!-- Codec preferences for this trunk --> <param name="codec-prefs" value="PCMU,PCMA,G722"/> <!-- Ping the provider to detect failures --> <param name="ping" value="25"/> <param name="ping-max" value="3"/> <param name="ping-min" value="1"/> </gateway>
</include>
<include> <gateway name="my_provider"> <!-- Provider credentials --> <param name="username" value="your_sip_username"/> <param name="password" value="your_sip_password"/> <param name="realm" value="sip.provider.com"/> <param name="proxy" value="sip.provider.com"/> <!-- Registration (some providers require it, some use IP auth) --> <param name="register" value="true"/> <param name="register-transport" value="udp"/> <!-- Caller ID --> <param name="caller-id-in-from" value="true"/> <!-- Retry on failure --> <param name="retry-seconds" value="30"/> <!-- Codec preferences for this trunk --> <param name="codec-prefs" value="PCMU,PCMA,G722"/> <!-- Ping the provider to detect failures --> <param name="ping" value="25"/> <param name="ping-max" value="3"/> <param name="ping-min" value="1"/> </gateway>
</include>
<include> <gateway name="my_provider"> <!-- Provider credentials --> <param name="username" value="your_sip_username"/> <param name="password" value="your_sip_password"/> <param name="realm" value="sip.provider.com"/> <param name="proxy" value="sip.provider.com"/> <!-- Registration (some providers require it, some use IP auth) --> <param name="register" value="true"/> <param name="register-transport" value="udp"/> <!-- Caller ID --> <param name="caller-id-in-from" value="true"/> <!-- Retry on failure --> <param name="retry-seconds" value="30"/> <!-- Codec preferences for this trunk --> <param name="codec-prefs" value="PCMU,PCMA,G722"/> <!-- Ping the provider to detect failures --> <param name="ping" value="25"/> <param name="ping-max" value="3"/> <param name="ping-min" value="1"/> </gateway>
</include>
<include> <gateway name="ip_auth_provider"> <param name="username" value="not_used"/> <param name="password" value="not_used"/> <param name="realm" value="sip.provider.com"/> <param name="proxy" value="sip.provider.com"/> <param name="register" value="false"/> <param name="caller-id-in-from" value="true"/> </gateway>
</include>
<include> <gateway name="ip_auth_provider"> <param name="username" value="not_used"/> <param name="password" value="not_used"/> <param name="realm" value="sip.provider.com"/> <param name="proxy" value="sip.provider.com"/> <param name="register" value="false"/> <param name="caller-id-in-from" value="true"/> </gateway>
</include>
<include> <gateway name="ip_auth_provider"> <param name="username" value="not_used"/> <param name="password" value="not_used"/> <param name="realm" value="sip.provider.com"/> <param name="proxy" value="sip.provider.com"/> <param name="register" value="false"/> <param name="caller-id-in-from" value="true"/> </gateway>
</include>
# From fs_cli
fs_cli -x "sofia profile external rescan" # Check gateway status
fs_cli -x "sofia status gateway my_provider" # Expected output:
# Name my_provider
# Profile external
# Scheme sip
# Realm sip.provider.com
# Username your_sip_username
# ...
# State REGED <-- Successfully registered
# Status UP
# From fs_cli
fs_cli -x "sofia profile external rescan" # Check gateway status
fs_cli -x "sofia status gateway my_provider" # Expected output:
# Name my_provider
# Profile external
# Scheme sip
# Realm sip.provider.com
# Username your_sip_username
# ...
# State REGED <-- Successfully registered
# Status UP
# From fs_cli
fs_cli -x "sofia profile external rescan" # Check gateway status
fs_cli -x "sofia status gateway my_provider" # Expected output:
# Name my_provider
# Profile external
# Scheme sip
# Realm sip.provider.com
# Username your_sip_username
# ...
# State REGED <-- Successfully registered
# Status UP
<include> <context name="public"> <!-- Route DID +15551234567 to extension 1001 --> <extension name="inbound_main"> <condition field="destination_number" expression="^(\+?1?5551234567)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="1001 XML default"/> </condition> </extension> <!-- Route DID +15559876543 to IVR --> <extension name="inbound_ivr"> <condition field="destination_number" expression="^(\+?1?5559876543)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="5000 XML default"/> </condition> </extension> <!-- Route DID +15555551234 to ring group (sales) --> <extension name="inbound_sales"> <condition field="destination_number" expression="^(\+?1?5555551234)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="9001 XML default"/> </condition> </extension> <!-- Catch-all: reject unknown DIDs --> <extension name="public_reject"> <condition field="destination_number" expression="^(.*)$"> <action application="log" data="WARNING Rejecting unknown inbound DID: ${destination_number} from ${sip_from_uri}"/> <action application="hangup" data="CALL_REJECTED"/> </condition> </extension> </context>
</include>
<include> <context name="public"> <!-- Route DID +15551234567 to extension 1001 --> <extension name="inbound_main"> <condition field="destination_number" expression="^(\+?1?5551234567)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="1001 XML default"/> </condition> </extension> <!-- Route DID +15559876543 to IVR --> <extension name="inbound_ivr"> <condition field="destination_number" expression="^(\+?1?5559876543)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="5000 XML default"/> </condition> </extension> <!-- Route DID +15555551234 to ring group (sales) --> <extension name="inbound_sales"> <condition field="destination_number" expression="^(\+?1?5555551234)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="9001 XML default"/> </condition> </extension> <!-- Catch-all: reject unknown DIDs --> <extension name="public_reject"> <condition field="destination_number" expression="^(.*)$"> <action application="log" data="WARNING Rejecting unknown inbound DID: ${destination_number} from ${sip_from_uri}"/> <action application="hangup" data="CALL_REJECTED"/> </condition> </extension> </context>
</include>
<include> <context name="public"> <!-- Route DID +15551234567 to extension 1001 --> <extension name="inbound_main"> <condition field="destination_number" expression="^(\+?1?5551234567)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="1001 XML default"/> </condition> </extension> <!-- Route DID +15559876543 to IVR --> <extension name="inbound_ivr"> <condition field="destination_number" expression="^(\+?1?5559876543)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="5000 XML default"/> </condition> </extension> <!-- Route DID +15555551234 to ring group (sales) --> <extension name="inbound_sales"> <condition field="destination_number" expression="^(\+?1?5555551234)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="9001 XML default"/> </condition> </extension> <!-- Catch-all: reject unknown DIDs --> <extension name="public_reject"> <condition field="destination_number" expression="^(.*)$"> <action application="log" data="WARNING Rejecting unknown inbound DID: ${destination_number} from ${sip_from_uri}"/> <action application="hangup" data="CALL_REJECTED"/> </condition> </extension> </context>
</include>
<X-PRE-PROCESS cmd="set" data="external_sip_ip=YOUR_PUBLIC_IP"/>
<X-PRE-PROCESS cmd="set" data="external_rtp_ip=YOUR_PUBLIC_IP"/>
<X-PRE-PROCESS cmd="set" data="external_sip_ip=YOUR_PUBLIC_IP"/>
<X-PRE-PROCESS cmd="set" data="external_rtp_ip=YOUR_PUBLIC_IP"/>
<X-PRE-PROCESS cmd="set" data="external_sip_ip=YOUR_PUBLIC_IP"/>
<X-PRE-PROCESS cmd="set" data="external_rtp_ip=YOUR_PUBLIC_IP"/>
<!-- Replace the sip-ip and rtp-ip lines with: -->
<param name="sip-ip" value="$${local_ip_v4}"/>
<param name="ext-sip-ip" value="$${external_sip_ip}"/>
<param name="rtp-ip" value="$${local_ip_v4}"/>
<param name="ext-rtp-ip" value="$${external_rtp_ip}"/> <!-- Enable NAT handling -->
<param name="apply-nat-acl" value="nat.auto"/>
<param name="aggressive-nat-detection" value="true"/>
<param name="local-network-acl" value="localnet.auto"/>
<!-- Replace the sip-ip and rtp-ip lines with: -->
<param name="sip-ip" value="$${local_ip_v4}"/>
<param name="ext-sip-ip" value="$${external_sip_ip}"/>
<param name="rtp-ip" value="$${local_ip_v4}"/>
<param name="ext-rtp-ip" value="$${external_rtp_ip}"/> <!-- Enable NAT handling -->
<param name="apply-nat-acl" value="nat.auto"/>
<param name="aggressive-nat-detection" value="true"/>
<param name="local-network-acl" value="localnet.auto"/>
<!-- Replace the sip-ip and rtp-ip lines with: -->
<param name="sip-ip" value="$${local_ip_v4}"/>
<param name="ext-sip-ip" value="$${external_sip_ip}"/>
<param name="rtp-ip" value="$${local_ip_v4}"/>
<param name="ext-rtp-ip" value="$${external_rtp_ip}"/> <!-- Enable NAT handling -->
<param name="apply-nat-acl" value="nat.auto"/>
<param name="aggressive-nat-detection" value="true"/>
<param name="local-network-acl" value="localnet.auto"/>
fs_cli -x "sofia status profile internal reg" # Output shows registered endpoints:
# Call-ID: xxxxx@yyyy
# User: 1001@YOUR_SERVER_IP
# Contact: "Alice Johnson" <sip:1001@phone_ip:port>
# Agent: Ooh, a softphone!
# Status: Registered(UDP)(unknown) EXP(2024-01-01 12:00:00)
# ...
fs_cli -x "sofia status profile internal reg" # Output shows registered endpoints:
# Call-ID: xxxxx@yyyy
# User: 1001@YOUR_SERVER_IP
# Contact: "Alice Johnson" <sip:1001@phone_ip:port>
# Agent: Ooh, a softphone!
# Status: Registered(UDP)(unknown) EXP(2024-01-01 12:00:00)
# ...
fs_cli -x "sofia status profile internal reg" # Output shows registered endpoints:
# Call-ID: xxxxx@yyyy
# User: 1001@YOUR_SERVER_IP
# Contact: "Alice Johnson" <sip:1001@phone_ip:port>
# Agent: Ooh, a softphone!
# Status: Registered(UDP)(unknown) EXP(2024-01-01 12:00:00)
# ...
<context name="default"> <!-- Container: who can use these rules --> <extension name="my_rule"> <!-- Named rule --> <condition field="X" expression="regex"> <!-- When to match --> <action application="Y" data="Z"/> <!-- What to do --> <action application="Y2" data="Z2"/> <!-- Actions run in sequence --> </condition> </extension>
</context>
<context name="default"> <!-- Container: who can use these rules --> <extension name="my_rule"> <!-- Named rule --> <condition field="X" expression="regex"> <!-- When to match --> <action application="Y" data="Z"/> <!-- What to do --> <action application="Y2" data="Z2"/> <!-- Actions run in sequence --> </condition> </extension>
</context>
<context name="default"> <!-- Container: who can use these rules --> <extension name="my_rule"> <!-- Named rule --> <condition field="X" expression="regex"> <!-- When to match --> <action application="Y" data="Z"/> <!-- What to do --> <action application="Y2" data="Z2"/> <!-- Actions run in sequence --> </condition> </extension>
</context>
^1001$ — Exact match for "1001"
^(10[0-1][0-9])$ — Match 1000-1019
^(\d{10})$ — Match any 10-digit number
^9(\d+)$ — Match 9 + any digits (capture digits after 9)
^(\+?1?\d{10})$ — Match with optional +1 prefix
^.*$ — Match anything (catch-all)
^1001$ — Exact match for "1001"
^(10[0-1][0-9])$ — Match 1000-1019
^(\d{10})$ — Match any 10-digit number
^9(\d+)$ — Match 9 + any digits (capture digits after 9)
^(\+?1?\d{10})$ — Match with optional +1 prefix
^.*$ — Match anything (catch-all)
^1001$ — Exact match for "1001"
^(10[0-1][0-9])$ — Match 1000-1019
^(\d{10})$ — Match any 10-digit number
^9(\d+)$ — Match 9 + any digits (capture digits after 9)
^(\+?1?\d{10})$ — Match with optional +1 prefix
^.*$ — Match anything (catch-all)
<!-- Set a variable -->
<action application="set" data="my_var=hello"/> <!-- Use a variable in an action -->
<action application="log" data="INFO The value is ${my_var}"/> <!-- Use a variable in a condition -->
<condition field="${my_var}" expression="^(hello)$">
<!-- Set a variable -->
<action application="set" data="my_var=hello"/> <!-- Use a variable in an action -->
<action application="log" data="INFO The value is ${my_var}"/> <!-- Use a variable in a condition -->
<condition field="${my_var}" expression="^(hello)$">
<!-- Set a variable -->
<action application="set" data="my_var=hello"/> <!-- Use a variable in an action -->
<action application="log" data="INFO The value is ${my_var}"/> <!-- Use a variable in a condition -->
<condition field="${my_var}" expression="^(hello)$">
<extension name="business_hours"> <condition field="time_of_day" expression="09:00-17:30"> <!-- This runs during business hours --> <action application="transfer" data="5000 XML default"/> <!-- This runs OUTSIDE business hours --> <anti-action application="playback" data="ivr/ivr-please_call_back_during_business_hours.wav"/> <anti-action application="voicemail" data="default $${domain} 1001"/> </condition>
</extension>
<extension name="business_hours"> <condition field="time_of_day" expression="09:00-17:30"> <!-- This runs during business hours --> <action application="transfer" data="5000 XML default"/> <!-- This runs OUTSIDE business hours --> <anti-action application="playback" data="ivr/ivr-please_call_back_during_business_hours.wav"/> <anti-action application="voicemail" data="default $${domain} 1001"/> </condition>
</extension>
<extension name="business_hours"> <condition field="time_of_day" expression="09:00-17:30"> <!-- This runs during business hours --> <action application="transfer" data="5000 XML default"/> <!-- This runs OUTSIDE business hours --> <anti-action application="playback" data="ivr/ivr-please_call_back_during_business_hours.wav"/> <anti-action application="voicemail" data="default $${domain} 1001"/> </condition>
</extension>
<include> <context name="default"> <!-- ============================================ --> <!-- INTERNAL EXTENSION CALLING (1001-1099) --> <!-- ============================================ --> <extension name="internal_extensions"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <action application="export" data="dialed_extension=$1"/> <!-- Set ring timeout to 30 seconds --> <action application="set" data="call_timeout=30"/> <!-- Set caller ID --> <action application="set" data="effective_caller_id_name=${outbound_caller_id_name}"/> <action application="set" data="effective_caller_id_number=${outbound_caller_id_number}"/> <!-- Ring the extension, then send to voicemail on no answer --> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="continue_on_fail=true"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> <!-- No answer — go to voicemail --> <action application="answer"/> <action application="sleep" data="1000"/> <action application="voicemail" data="default $${domain} ${dialed_extension}"/> </condition> </extension> <!-- ============================================ --> <!-- RING GROUPS --> <!-- ============================================ --> <!-- Sales ring group (9001) — ring all sales phones simultaneously --> <extension name="ring_group_sales"> <condition field="destination_number" expression="^(9001)$"> <action application="set" data="call_timeout=25"/> <action application="set" data="continue_on_fail=true"/> <action application="set" data="hangup_after_bridge=true"/> <action application="bridge" data="user/1001@$${domain},user/1002@$${domain},user/1003@$${domain}"/> <!-- All busy/unavailable — voicemail for sales --> <action application="voicemail" data="default $${domain} 1001"/> </condition> </extension> <!-- Support ring group (9002) — sequential ring (try each for 15s) --> <extension name="ring_group_support"> <condition field="destination_number" expression="^(9002)$"> <action application="set" data="continue_on_fail=true"/> <action application="set" data="hangup_after_bridge=true"/> <!-- Try first support agent --> <action application="set" data="call_timeout=15"/> <action application="bridge" data="user/1004@$${domain}"/> <!-- Try second --> <action application="bridge" data="user/1005@$${domain}"/> <!-- Try third --> <action application="bridge" data="user/1006@$${domain}"/> <!-- All unavailable --> <action application="voicemail" data="default $${domain} 1004"/> </condition> </extension> <!-- ============================================ --> <!-- OUTBOUND CALLS VIA TRUNK --> <!-- ============================================ --> <!-- Local/National calls: dial 9 + number --> <extension name="outbound_national"> <condition field="destination_number" expression="^9(\d{10,11})$"> <action application="set" data="effective_caller_id_number=15551234567"/> <action application="set" data="effective_caller_id_name=My Company"/> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="call_timeout=60"/> <action application="bridge" data="sofia/gateway/my_provider/$1"/> </condition> </extension> <!-- International calls: dial 00 + country code + number --> <extension name="outbound_international"> <condition field="destination_number" expression="^(00\d{7,15})$"> <!-- Check if user has international permission --> <action application="set" data="continue_on_fail=false"/> <action application="set" data="effective_caller_id_number=15551234567"/> <action application="bridge" data="sofia/gateway/my_provider/$1"/> </condition> </extension> <!-- ============================================ --> <!-- TIME-BASED ROUTING --> <!-- ============================================ --> <extension name="main_number_time_routing"> <condition field="destination_number" expression="^(5000)$"/> <!-- Check business hours: Mon-Fri 9:00-17:30 --> <condition field="time_of_day" expression="09:00-17:30"> <action application="transfer" data="5001 XML default"/> <anti-action application="transfer" data="5002 XML default"/> </condition> </extension> <!-- Business hours handler --> <extension name="business_hours_handler"> <condition field="destination_number" expression="^(5001)$"> <action application="answer"/> <action application="playback" data="ivr/ivr-welcome.wav"/> <action application="transfer" data="main_ivr XML default"/> </condition> </extension> <!-- After hours handler --> <extension name="after_hours_handler"> <condition field="destination_number" expression="^(5002)$"> <action application="answer"/> <action application="playback" data="ivr/ivr-after_hours_message.wav"/> <action application="voicemail" data="default $${domain} 1001"/> </condition> </extension> <!-- ============================================ --> <!-- UTILITIES --> <!-- ============================================ --> <!-- Echo test (dial 9196) --> <extension name="echo_test"> <condition field="destination_number" expression="^(9196)$"> <action application="answer"/> <action application="echo"/> </condition> </extension> <!-- Music on hold test (dial 9664) --> <extension name="moh_test"> <condition field="destination_number" expression="^(9664)$"> <action application="answer"/> <action application="playback" data="$${hold_music}"/> </condition> </extension> <!-- Voicemail access (dial *98) --> <extension name="voicemail_check"> <condition field="destination_number" expression="^\*98$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain}"/> </condition> </extension> <!-- Direct voicemail for extension (dial *99 + ext) --> <extension name="voicemail_direct"> <condition field="destination_number" expression="^\*99(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="default $${domain} $1"/> </condition> </extension> <!-- Call parking (dial 5900) --> <extension name="park_call"> <condition field="destination_number" expression="^(5900)$"> <action application="set" data="fifo_music=$${hold_music}"/> <action application="fifo" data="park@$${domain} in"/> </condition> </extension> <!-- Retrieve parked call (dial 5901) --> <extension name="retrieve_parked_call"> <condition field="destination_number" expression="^(5901)$"> <action application="answer"/> <action application="fifo" data="park@$${domain} out wait"/> </condition> </extension> </context>
</include>
<include> <context name="default"> <!-- ============================================ --> <!-- INTERNAL EXTENSION CALLING (1001-1099) --> <!-- ============================================ --> <extension name="internal_extensions"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <action application="export" data="dialed_extension=$1"/> <!-- Set ring timeout to 30 seconds --> <action application="set" data="call_timeout=30"/> <!-- Set caller ID --> <action application="set" data="effective_caller_id_name=${outbound_caller_id_name}"/> <action application="set" data="effective_caller_id_number=${outbound_caller_id_number}"/> <!-- Ring the extension, then send to voicemail on no answer --> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="continue_on_fail=true"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> <!-- No answer — go to voicemail --> <action application="answer"/> <action application="sleep" data="1000"/> <action application="voicemail" data="default $${domain} ${dialed_extension}"/> </condition> </extension> <!-- ============================================ --> <!-- RING GROUPS --> <!-- ============================================ --> <!-- Sales ring group (9001) — ring all sales phones simultaneously --> <extension name="ring_group_sales"> <condition field="destination_number" expression="^(9001)$"> <action application="set" data="call_timeout=25"/> <action application="set" data="continue_on_fail=true"/> <action application="set" data="hangup_after_bridge=true"/> <action application="bridge" data="user/1001@$${domain},user/1002@$${domain},user/1003@$${domain}"/> <!-- All busy/unavailable — voicemail for sales --> <action application="voicemail" data="default $${domain} 1001"/> </condition> </extension> <!-- Support ring group (9002) — sequential ring (try each for 15s) --> <extension name="ring_group_support"> <condition field="destination_number" expression="^(9002)$"> <action application="set" data="continue_on_fail=true"/> <action application="set" data="hangup_after_bridge=true"/> <!-- Try first support agent --> <action application="set" data="call_timeout=15"/> <action application="bridge" data="user/1004@$${domain}"/> <!-- Try second --> <action application="bridge" data="user/1005@$${domain}"/> <!-- Try third --> <action application="bridge" data="user/1006@$${domain}"/> <!-- All unavailable --> <action application="voicemail" data="default $${domain} 1004"/> </condition> </extension> <!-- ============================================ --> <!-- OUTBOUND CALLS VIA TRUNK --> <!-- ============================================ --> <!-- Local/National calls: dial 9 + number --> <extension name="outbound_national"> <condition field="destination_number" expression="^9(\d{10,11})$"> <action application="set" data="effective_caller_id_number=15551234567"/> <action application="set" data="effective_caller_id_name=My Company"/> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="call_timeout=60"/> <action application="bridge" data="sofia/gateway/my_provider/$1"/> </condition> </extension> <!-- International calls: dial 00 + country code + number --> <extension name="outbound_international"> <condition field="destination_number" expression="^(00\d{7,15})$"> <!-- Check if user has international permission --> <action application="set" data="continue_on_fail=false"/> <action application="set" data="effective_caller_id_number=15551234567"/> <action application="bridge" data="sofia/gateway/my_provider/$1"/> </condition> </extension> <!-- ============================================ --> <!-- TIME-BASED ROUTING --> <!-- ============================================ --> <extension name="main_number_time_routing"> <condition field="destination_number" expression="^(5000)$"/> <!-- Check business hours: Mon-Fri 9:00-17:30 --> <condition field="time_of_day" expression="09:00-17:30"> <action application="transfer" data="5001 XML default"/> <anti-action application="transfer" data="5002 XML default"/> </condition> </extension> <!-- Business hours handler --> <extension name="business_hours_handler"> <condition field="destination_number" expression="^(5001)$"> <action application="answer"/> <action application="playback" data="ivr/ivr-welcome.wav"/> <action application="transfer" data="main_ivr XML default"/> </condition> </extension> <!-- After hours handler --> <extension name="after_hours_handler"> <condition field="destination_number" expression="^(5002)$"> <action application="answer"/> <action application="playback" data="ivr/ivr-after_hours_message.wav"/> <action application="voicemail" data="default $${domain} 1001"/> </condition> </extension> <!-- ============================================ --> <!-- UTILITIES --> <!-- ============================================ --> <!-- Echo test (dial 9196) --> <extension name="echo_test"> <condition field="destination_number" expression="^(9196)$"> <action application="answer"/> <action application="echo"/> </condition> </extension> <!-- Music on hold test (dial 9664) --> <extension name="moh_test"> <condition field="destination_number" expression="^(9664)$"> <action application="answer"/> <action application="playback" data="$${hold_music}"/> </condition> </extension> <!-- Voicemail access (dial *98) --> <extension name="voicemail_check"> <condition field="destination_number" expression="^\*98$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain}"/> </condition> </extension> <!-- Direct voicemail for extension (dial *99 + ext) --> <extension name="voicemail_direct"> <condition field="destination_number" expression="^\*99(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="default $${domain} $1"/> </condition> </extension> <!-- Call parking (dial 5900) --> <extension name="park_call"> <condition field="destination_number" expression="^(5900)$"> <action application="set" data="fifo_music=$${hold_music}"/> <action application="fifo" data="park@$${domain} in"/> </condition> </extension> <!-- Retrieve parked call (dial 5901) --> <extension name="retrieve_parked_call"> <condition field="destination_number" expression="^(5901)$"> <action application="answer"/> <action application="fifo" data="park@$${domain} out wait"/> </condition> </extension> </context>
</include>
<include> <context name="default"> <!-- ============================================ --> <!-- INTERNAL EXTENSION CALLING (1001-1099) --> <!-- ============================================ --> <extension name="internal_extensions"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <action application="export" data="dialed_extension=$1"/> <!-- Set ring timeout to 30 seconds --> <action application="set" data="call_timeout=30"/> <!-- Set caller ID --> <action application="set" data="effective_caller_id_name=${outbound_caller_id_name}"/> <action application="set" data="effective_caller_id_number=${outbound_caller_id_number}"/> <!-- Ring the extension, then send to voicemail on no answer --> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="continue_on_fail=true"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> <!-- No answer — go to voicemail --> <action application="answer"/> <action application="sleep" data="1000"/> <action application="voicemail" data="default $${domain} ${dialed_extension}"/> </condition> </extension> <!-- ============================================ --> <!-- RING GROUPS --> <!-- ============================================ --> <!-- Sales ring group (9001) — ring all sales phones simultaneously --> <extension name="ring_group_sales"> <condition field="destination_number" expression="^(9001)$"> <action application="set" data="call_timeout=25"/> <action application="set" data="continue_on_fail=true"/> <action application="set" data="hangup_after_bridge=true"/> <action application="bridge" data="user/1001@$${domain},user/1002@$${domain},user/1003@$${domain}"/> <!-- All busy/unavailable — voicemail for sales --> <action application="voicemail" data="default $${domain} 1001"/> </condition> </extension> <!-- Support ring group (9002) — sequential ring (try each for 15s) --> <extension name="ring_group_support"> <condition field="destination_number" expression="^(9002)$"> <action application="set" data="continue_on_fail=true"/> <action application="set" data="hangup_after_bridge=true"/> <!-- Try first support agent --> <action application="set" data="call_timeout=15"/> <action application="bridge" data="user/1004@$${domain}"/> <!-- Try second --> <action application="bridge" data="user/1005@$${domain}"/> <!-- Try third --> <action application="bridge" data="user/1006@$${domain}"/> <!-- All unavailable --> <action application="voicemail" data="default $${domain} 1004"/> </condition> </extension> <!-- ============================================ --> <!-- OUTBOUND CALLS VIA TRUNK --> <!-- ============================================ --> <!-- Local/National calls: dial 9 + number --> <extension name="outbound_national"> <condition field="destination_number" expression="^9(\d{10,11})$"> <action application="set" data="effective_caller_id_number=15551234567"/> <action application="set" data="effective_caller_id_name=My Company"/> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="call_timeout=60"/> <action application="bridge" data="sofia/gateway/my_provider/$1"/> </condition> </extension> <!-- International calls: dial 00 + country code + number --> <extension name="outbound_international"> <condition field="destination_number" expression="^(00\d{7,15})$"> <!-- Check if user has international permission --> <action application="set" data="continue_on_fail=false"/> <action application="set" data="effective_caller_id_number=15551234567"/> <action application="bridge" data="sofia/gateway/my_provider/$1"/> </condition> </extension> <!-- ============================================ --> <!-- TIME-BASED ROUTING --> <!-- ============================================ --> <extension name="main_number_time_routing"> <condition field="destination_number" expression="^(5000)$"/> <!-- Check business hours: Mon-Fri 9:00-17:30 --> <condition field="time_of_day" expression="09:00-17:30"> <action application="transfer" data="5001 XML default"/> <anti-action application="transfer" data="5002 XML default"/> </condition> </extension> <!-- Business hours handler --> <extension name="business_hours_handler"> <condition field="destination_number" expression="^(5001)$"> <action application="answer"/> <action application="playback" data="ivr/ivr-welcome.wav"/> <action application="transfer" data="main_ivr XML default"/> </condition> </extension> <!-- After hours handler --> <extension name="after_hours_handler"> <condition field="destination_number" expression="^(5002)$"> <action application="answer"/> <action application="playback" data="ivr/ivr-after_hours_message.wav"/> <action application="voicemail" data="default $${domain} 1001"/> </condition> </extension> <!-- ============================================ --> <!-- UTILITIES --> <!-- ============================================ --> <!-- Echo test (dial 9196) --> <extension name="echo_test"> <condition field="destination_number" expression="^(9196)$"> <action application="answer"/> <action application="echo"/> </condition> </extension> <!-- Music on hold test (dial 9664) --> <extension name="moh_test"> <condition field="destination_number" expression="^(9664)$"> <action application="answer"/> <action application="playback" data="$${hold_music}"/> </condition> </extension> <!-- Voicemail access (dial *98) --> <extension name="voicemail_check"> <condition field="destination_number" expression="^\*98$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain}"/> </condition> </extension> <!-- Direct voicemail for extension (dial *99 + ext) --> <extension name="voicemail_direct"> <condition field="destination_number" expression="^\*99(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="default $${domain} $1"/> </condition> </extension> <!-- Call parking (dial 5900) --> <extension name="park_call"> <condition field="destination_number" expression="^(5900)$"> <action application="set" data="fifo_music=$${hold_music}"/> <action application="fifo" data="park@$${domain} in"/> </condition> </extension> <!-- Retrieve parked call (dial 5901) --> <extension name="retrieve_parked_call"> <condition field="destination_number" expression="^(5901)$"> <action application="answer"/> <action application="fifo" data="park@$${domain} out wait"/> </condition> </extension> </context>
</include>
<configuration name="ivr.conf" description="IVR Menus"> <menus> <!-- ============================================ --> <!-- MAIN MENU (Level 1) --> <!-- ============================================ --> <menu name="main_ivr" greet-long="ivr/ivr-welcome_to_our_company.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" confirm-macro="" confirm-key="" tts-engine="" tts-voice="" confirm-attempts="3" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Sales --> <entry action="menu-exec-app" digits="1" param="transfer 9001 XML default"/> <!-- Press 2 for Support --> <entry action="menu-exec-app" digits="2" param="transfer 9002 XML default"/> <!-- Press 3 for Billing --> <entry action="menu-sub" digits="3" param="billing_ivr"/> <!-- Press 4 for Company Directory --> <entry action="menu-exec-app" digits="4" param="transfer 411 XML default"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> <!-- Press * to repeat --> <entry action="menu-top" digits="*"/> </menu> <!-- ============================================ --> <!-- BILLING SUBMENU (Level 2) --> <!-- ============================================ --> <menu name="billing_ivr" greet-long="ivr/ivr-billing_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Account Balance --> <entry action="menu-exec-app" digits="1" param="transfer 8001 XML default"/> <!-- Press 2 for Payment --> <entry action="menu-exec-app" digits="2" param="transfer 8002 XML default"/> <!-- Press 3 for Billing Agent --> <entry action="menu-exec-app" digits="3" param="transfer 1007 XML default"/> <!-- Press 9 to go back to main menu --> <entry action="menu-back" digits="9"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> </menu> <!-- ============================================ --> <!-- SUPPORT SUBMENU (Level 2) --> <!-- ============================================ --> <menu name="support_ivr" greet-long="ivr/ivr-support_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Technical Support --> <entry action="menu-sub" digits="1" param="tech_support_ivr"/> <!-- Press 2 for General Inquiries --> <entry action="menu-exec-app" digits="2" param="transfer 1005 XML default"/> <!-- Press 9 to go back --> <entry action="menu-back" digits="9"/> </menu> <!-- ============================================ --> <!-- TECH SUPPORT (Level 3) --> <!-- ============================================ --> <menu name="tech_support_ivr" greet-long="ivr/ivr-tech_support_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Internet Issues --> <entry action="menu-exec-app" digits="1" param="transfer 1004 XML default"/> <!-- Press 2 for Phone/VoIP Issues --> <entry action="menu-exec-app" digits="2" param="transfer 1005 XML default"/> <!-- Press 3 for Email Issues --> <entry action="menu-exec-app" digits="3" param="transfer 1006 XML default"/> <!-- Press 9 to go back --> <entry action="menu-back" digits="9"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> </menu> </menus>
</configuration>
<configuration name="ivr.conf" description="IVR Menus"> <menus> <!-- ============================================ --> <!-- MAIN MENU (Level 1) --> <!-- ============================================ --> <menu name="main_ivr" greet-long="ivr/ivr-welcome_to_our_company.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" confirm-macro="" confirm-key="" tts-engine="" tts-voice="" confirm-attempts="3" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Sales --> <entry action="menu-exec-app" digits="1" param="transfer 9001 XML default"/> <!-- Press 2 for Support --> <entry action="menu-exec-app" digits="2" param="transfer 9002 XML default"/> <!-- Press 3 for Billing --> <entry action="menu-sub" digits="3" param="billing_ivr"/> <!-- Press 4 for Company Directory --> <entry action="menu-exec-app" digits="4" param="transfer 411 XML default"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> <!-- Press * to repeat --> <entry action="menu-top" digits="*"/> </menu> <!-- ============================================ --> <!-- BILLING SUBMENU (Level 2) --> <!-- ============================================ --> <menu name="billing_ivr" greet-long="ivr/ivr-billing_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Account Balance --> <entry action="menu-exec-app" digits="1" param="transfer 8001 XML default"/> <!-- Press 2 for Payment --> <entry action="menu-exec-app" digits="2" param="transfer 8002 XML default"/> <!-- Press 3 for Billing Agent --> <entry action="menu-exec-app" digits="3" param="transfer 1007 XML default"/> <!-- Press 9 to go back to main menu --> <entry action="menu-back" digits="9"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> </menu> <!-- ============================================ --> <!-- SUPPORT SUBMENU (Level 2) --> <!-- ============================================ --> <menu name="support_ivr" greet-long="ivr/ivr-support_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Technical Support --> <entry action="menu-sub" digits="1" param="tech_support_ivr"/> <!-- Press 2 for General Inquiries --> <entry action="menu-exec-app" digits="2" param="transfer 1005 XML default"/> <!-- Press 9 to go back --> <entry action="menu-back" digits="9"/> </menu> <!-- ============================================ --> <!-- TECH SUPPORT (Level 3) --> <!-- ============================================ --> <menu name="tech_support_ivr" greet-long="ivr/ivr-tech_support_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Internet Issues --> <entry action="menu-exec-app" digits="1" param="transfer 1004 XML default"/> <!-- Press 2 for Phone/VoIP Issues --> <entry action="menu-exec-app" digits="2" param="transfer 1005 XML default"/> <!-- Press 3 for Email Issues --> <entry action="menu-exec-app" digits="3" param="transfer 1006 XML default"/> <!-- Press 9 to go back --> <entry action="menu-back" digits="9"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> </menu> </menus>
</configuration>
<configuration name="ivr.conf" description="IVR Menus"> <menus> <!-- ============================================ --> <!-- MAIN MENU (Level 1) --> <!-- ============================================ --> <menu name="main_ivr" greet-long="ivr/ivr-welcome_to_our_company.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" confirm-macro="" confirm-key="" tts-engine="" tts-voice="" confirm-attempts="3" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Sales --> <entry action="menu-exec-app" digits="1" param="transfer 9001 XML default"/> <!-- Press 2 for Support --> <entry action="menu-exec-app" digits="2" param="transfer 9002 XML default"/> <!-- Press 3 for Billing --> <entry action="menu-sub" digits="3" param="billing_ivr"/> <!-- Press 4 for Company Directory --> <entry action="menu-exec-app" digits="4" param="transfer 411 XML default"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> <!-- Press * to repeat --> <entry action="menu-top" digits="*"/> </menu> <!-- ============================================ --> <!-- BILLING SUBMENU (Level 2) --> <!-- ============================================ --> <menu name="billing_ivr" greet-long="ivr/ivr-billing_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Account Balance --> <entry action="menu-exec-app" digits="1" param="transfer 8001 XML default"/> <!-- Press 2 for Payment --> <entry action="menu-exec-app" digits="2" param="transfer 8002 XML default"/> <!-- Press 3 for Billing Agent --> <entry action="menu-exec-app" digits="3" param="transfer 1007 XML default"/> <!-- Press 9 to go back to main menu --> <entry action="menu-back" digits="9"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> </menu> <!-- ============================================ --> <!-- SUPPORT SUBMENU (Level 2) --> <!-- ============================================ --> <menu name="support_ivr" greet-long="ivr/ivr-support_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Technical Support --> <entry action="menu-sub" digits="1" param="tech_support_ivr"/> <!-- Press 2 for General Inquiries --> <entry action="menu-exec-app" digits="2" param="transfer 1005 XML default"/> <!-- Press 9 to go back --> <entry action="menu-back" digits="9"/> </menu> <!-- ============================================ --> <!-- TECH SUPPORT (Level 3) --> <!-- ============================================ --> <menu name="tech_support_ivr" greet-long="ivr/ivr-tech_support_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Internet Issues --> <entry action="menu-exec-app" digits="1" param="transfer 1004 XML default"/> <!-- Press 2 for Phone/VoIP Issues --> <entry action="menu-exec-app" digits="2" param="transfer 1005 XML default"/> <!-- Press 3 for Email Issues --> <entry action="menu-exec-app" digits="3" param="transfer 1006 XML default"/> <!-- Press 9 to go back --> <entry action="menu-back" digits="9"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> </menu> </menus>
</configuration>
<!-- IVR entry point (dial 5000 or transfer from public context) -->
<extension name="main_ivr"> <condition field="destination_number" expression="^(main_ivr|5000)$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="ivr" data="main_ivr"/> </condition>
</extension>
<!-- IVR entry point (dial 5000 or transfer from public context) -->
<extension name="main_ivr"> <condition field="destination_number" expression="^(main_ivr|5000)$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="ivr" data="main_ivr"/> </condition>
</extension>
<!-- IVR entry point (dial 5000 or transfer from public context) -->
<extension name="main_ivr"> <condition field="destination_number" expression="^(main_ivr|5000)$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="ivr" data="main_ivr"/> </condition>
</extension>
<!-- Collect a 5-digit account number -->
<extension name="account_lookup"> <condition field="destination_number" expression="^(8001)$"> <action application="answer"/> <action application="sleep" data="500"/> <!-- play_and_get_digits parameters: min_digits max_digits max_tries timeout terminators audio_file bad_input_file variable_name regex_pattern digit_timeout transfer_on_failure --> <action application="play_and_get_digits" data="5 5 3 10000 # ivr/ivr-enter_account_number.wav ivr/ivr-that_was_an_invalid_entry.wav account_number \d{5} 15000"/> <!-- Now use the collected digits --> <action application="log" data="INFO Account number entered: ${account_number}"/> <!-- You could use mod_xml_curl to look up the account, or use Lua/Python to query a database --> <action application="playback" data="ivr/ivr-thank_you.wav"/> <action application="transfer" data="1007 XML default"/> </condition>
</extension>
<!-- Collect a 5-digit account number -->
<extension name="account_lookup"> <condition field="destination_number" expression="^(8001)$"> <action application="answer"/> <action application="sleep" data="500"/> <!-- play_and_get_digits parameters: min_digits max_digits max_tries timeout terminators audio_file bad_input_file variable_name regex_pattern digit_timeout transfer_on_failure --> <action application="play_and_get_digits" data="5 5 3 10000 # ivr/ivr-enter_account_number.wav ivr/ivr-that_was_an_invalid_entry.wav account_number \d{5} 15000"/> <!-- Now use the collected digits --> <action application="log" data="INFO Account number entered: ${account_number}"/> <!-- You could use mod_xml_curl to look up the account, or use Lua/Python to query a database --> <action application="playback" data="ivr/ivr-thank_you.wav"/> <action application="transfer" data="1007 XML default"/> </condition>
</extension>
<!-- Collect a 5-digit account number -->
<extension name="account_lookup"> <condition field="destination_number" expression="^(8001)$"> <action application="answer"/> <action application="sleep" data="500"/> <!-- play_and_get_digits parameters: min_digits max_digits max_tries timeout terminators audio_file bad_input_file variable_name regex_pattern digit_timeout transfer_on_failure --> <action application="play_and_get_digits" data="5 5 3 10000 # ivr/ivr-enter_account_number.wav ivr/ivr-that_was_an_invalid_entry.wav account_number \d{5} 15000"/> <!-- Now use the collected digits --> <action application="log" data="INFO Account number entered: ${account_number}"/> <!-- You could use mod_xml_curl to look up the account, or use Lua/Python to query a database --> <action application="playback" data="ivr/ivr-thank_you.wav"/> <action application="transfer" data="1007 XML default"/> </condition>
</extension>
<action application="speak" data="flite|kal|Welcome to our company. Press 1 for sales."/>
<action application="speak" data="flite|kal|Welcome to our company. Press 1 for sales."/>
<action application="speak" data="flite|kal|Welcome to our company. Press 1 for sales."/>
<configuration name="tts_commandline.conf" description="TTS Command"> <settings> <param name="command" value="echo '${text}' | /usr/bin/piper --model /opt/tts/en_US-lessac-medium.onnx --output_file ${file}"/> </settings>
</configuration>
<configuration name="tts_commandline.conf" description="TTS Command"> <settings> <param name="command" value="echo '${text}' | /usr/bin/piper --model /opt/tts/en_US-lessac-medium.onnx --output_file ${file}"/> </settings>
</configuration>
<configuration name="tts_commandline.conf" description="TTS Command"> <settings> <param name="command" value="echo '${text}' | /usr/bin/piper --model /opt/tts/en_US-lessac-medium.onnx --output_file ${file}"/> </settings>
</configuration>
<action application="speak" data="tts_commandline|default|Welcome to our company. Please hold while we connect you."/>
<action application="speak" data="tts_commandline|default|Welcome to our company. Please hold while we connect you."/>
<action application="speak" data="tts_commandline|default|Welcome to our company. Please hold while we connect you."/>
Caller dials main number │ ▼ Level 1: Main Menu 1 → Sales (ring group 9001) 2 → Support submenu 3 → Billing submenu 4 → Company directory 0 → Operator (ext 1009) │ ├── Level 2: Support │ 1 → Tech Support submenu │ 2 → General Inquiries (ext 1005) │ 9 → Back to Main │ │ │ └── Level 3: Tech Support │ 1 → Internet (ext 1004) │ 2 → VoIP (ext 1005) │ 3 → Email (ext 1006) │ 9 → Back to Support │ 0 → Operator │ └── Level 2: Billing 1 → Account Balance (collect acct#) 2 → Payment (ext 8002) 3 → Billing Agent (ext 1007) 9 → Back to Main 0 → Operator
Caller dials main number │ ▼ Level 1: Main Menu 1 → Sales (ring group 9001) 2 → Support submenu 3 → Billing submenu 4 → Company directory 0 → Operator (ext 1009) │ ├── Level 2: Support │ 1 → Tech Support submenu │ 2 → General Inquiries (ext 1005) │ 9 → Back to Main │ │ │ └── Level 3: Tech Support │ 1 → Internet (ext 1004) │ 2 → VoIP (ext 1005) │ 3 → Email (ext 1006) │ 9 → Back to Support │ 0 → Operator │ └── Level 2: Billing 1 → Account Balance (collect acct#) 2 → Payment (ext 8002) 3 → Billing Agent (ext 1007) 9 → Back to Main 0 → Operator
Caller dials main number │ ▼ Level 1: Main Menu 1 → Sales (ring group 9001) 2 → Support submenu 3 → Billing submenu 4 → Company directory 0 → Operator (ext 1009) │ ├── Level 2: Support │ 1 → Tech Support submenu │ 2 → General Inquiries (ext 1005) │ 9 → Back to Main │ │ │ └── Level 3: Tech Support │ 1 → Internet (ext 1004) │ 2 → VoIP (ext 1005) │ 3 → Email (ext 1006) │ 9 → Back to Support │ 0 → Operator │ └── Level 2: Billing 1 → Account Balance (collect acct#) 2 → Payment (ext 8002) 3 → Billing Agent (ext 1007) 9 → Back to Main 0 → Operator
<configuration name="voicemail.conf" description="Voicemail"> <settings> </settings> <profiles> <profile name="default"> <!-- Storage --> <param name="file-extension" value="wav"/> <param name="record-silence-threshold" value="200"/> <param name="record-silence-hits" value="5"/> <param name="max-record-len" value="300"/> <param name="max-retries" value="3"/> <!-- Greeting --> <param name="terminator-key" value="#"/> <param name="play-new-messages-key" value="1"/> <param name="play-saved-messages-key" value="2"/> <!-- Message controls (during playback) --> <param name="skip-greet-key" value="#"/> <param name="config-menu-key" value="5"/> <param name="record-greeting-key" value="1"/> <param name="choose-greeting-key" value="2"/> <param name="change-pass-key" value="6"/> <!-- Playback controls --> <param name="listen-key" value="1"/> <param name="save-key" value="2"/> <param name="delete-key" value="7"/> <param name="forward-key" value="8"/> <param name="repeat-key" value="0"/> <!-- Notification --> <param name="notify-mailto" value=""/> <param name="notify-email-body" value="You have a new voicemail from ${caller_id_number} (${caller_id_name}). The message is ${message_len} seconds long."/> <param name="notify-email-subject" value="New voicemail from ${caller_id_number}"/> <!-- Email with attachment --> <param name="email-from" value="voicemail@YOUR_DOMAIN"/> <param name="email-body" value="You have a new voicemail.\n\nFrom: ${caller_id_name} (${caller_id_number})\nDate: ${left_epoch}\nDuration: ${message_len} seconds\n\nThe recording is attached."/> <param name="email-subject" value="[Voicemail] New message from ${caller_id_number}"/> <!-- Attach the recording to the email --> <param name="vm-email-all-messages" value="true"/> <!-- Delete from server after emailing (set false to keep) --> <param name="vm-delete-file" value="false"/> <!-- Keep the message as new after emailing --> <param name="vm-keep-local-after-email" value="true"/> <!-- MWI (Message Waiting Indicator — lights up the phone's voicemail LED) --> <param name="vm-message-ext" value="wav"/> <!-- Storage location --> <param name="storage-dir" value="$${storage_dir}/voicemail/default"/> <!-- Operator extension (press 0 during greeting) --> <param name="operator-extension" value="operator XML default"/> <param name="operator-key" value="0"/> </profile> </profiles>
</configuration>
<configuration name="voicemail.conf" description="Voicemail"> <settings> </settings> <profiles> <profile name="default"> <!-- Storage --> <param name="file-extension" value="wav"/> <param name="record-silence-threshold" value="200"/> <param name="record-silence-hits" value="5"/> <param name="max-record-len" value="300"/> <param name="max-retries" value="3"/> <!-- Greeting --> <param name="terminator-key" value="#"/> <param name="play-new-messages-key" value="1"/> <param name="play-saved-messages-key" value="2"/> <!-- Message controls (during playback) --> <param name="skip-greet-key" value="#"/> <param name="config-menu-key" value="5"/> <param name="record-greeting-key" value="1"/> <param name="choose-greeting-key" value="2"/> <param name="change-pass-key" value="6"/> <!-- Playback controls --> <param name="listen-key" value="1"/> <param name="save-key" value="2"/> <param name="delete-key" value="7"/> <param name="forward-key" value="8"/> <param name="repeat-key" value="0"/> <!-- Notification --> <param name="notify-mailto" value=""/> <param name="notify-email-body" value="You have a new voicemail from ${caller_id_number} (${caller_id_name}). The message is ${message_len} seconds long."/> <param name="notify-email-subject" value="New voicemail from ${caller_id_number}"/> <!-- Email with attachment --> <param name="email-from" value="voicemail@YOUR_DOMAIN"/> <param name="email-body" value="You have a new voicemail.\n\nFrom: ${caller_id_name} (${caller_id_number})\nDate: ${left_epoch}\nDuration: ${message_len} seconds\n\nThe recording is attached."/> <param name="email-subject" value="[Voicemail] New message from ${caller_id_number}"/> <!-- Attach the recording to the email --> <param name="vm-email-all-messages" value="true"/> <!-- Delete from server after emailing (set false to keep) --> <param name="vm-delete-file" value="false"/> <!-- Keep the message as new after emailing --> <param name="vm-keep-local-after-email" value="true"/> <!-- MWI (Message Waiting Indicator — lights up the phone's voicemail LED) --> <param name="vm-message-ext" value="wav"/> <!-- Storage location --> <param name="storage-dir" value="$${storage_dir}/voicemail/default"/> <!-- Operator extension (press 0 during greeting) --> <param name="operator-extension" value="operator XML default"/> <param name="operator-key" value="0"/> </profile> </profiles>
</configuration>
<configuration name="voicemail.conf" description="Voicemail"> <settings> </settings> <profiles> <profile name="default"> <!-- Storage --> <param name="file-extension" value="wav"/> <param name="record-silence-threshold" value="200"/> <param name="record-silence-hits" value="5"/> <param name="max-record-len" value="300"/> <param name="max-retries" value="3"/> <!-- Greeting --> <param name="terminator-key" value="#"/> <param name="play-new-messages-key" value="1"/> <param name="play-saved-messages-key" value="2"/> <!-- Message controls (during playback) --> <param name="skip-greet-key" value="#"/> <param name="config-menu-key" value="5"/> <param name="record-greeting-key" value="1"/> <param name="choose-greeting-key" value="2"/> <param name="change-pass-key" value="6"/> <!-- Playback controls --> <param name="listen-key" value="1"/> <param name="save-key" value="2"/> <param name="delete-key" value="7"/> <param name="forward-key" value="8"/> <param name="repeat-key" value="0"/> <!-- Notification --> <param name="notify-mailto" value=""/> <param name="notify-email-body" value="You have a new voicemail from ${caller_id_number} (${caller_id_name}). The message is ${message_len} seconds long."/> <param name="notify-email-subject" value="New voicemail from ${caller_id_number}"/> <!-- Email with attachment --> <param name="email-from" value="voicemail@YOUR_DOMAIN"/> <param name="email-body" value="You have a new voicemail.\n\nFrom: ${caller_id_name} (${caller_id_number})\nDate: ${left_epoch}\nDuration: ${message_len} seconds\n\nThe recording is attached."/> <param name="email-subject" value="[Voicemail] New message from ${caller_id_number}"/> <!-- Attach the recording to the email --> <param name="vm-email-all-messages" value="true"/> <!-- Delete from server after emailing (set false to keep) --> <param name="vm-delete-file" value="false"/> <!-- Keep the message as new after emailing --> <param name="vm-keep-local-after-email" value="true"/> <!-- MWI (Message Waiting Indicator — lights up the phone's voicemail LED) --> <param name="vm-message-ext" value="wav"/> <!-- Storage location --> <param name="storage-dir" value="$${storage_dir}/voicemail/default"/> <!-- Operator extension (press 0 during greeting) --> <param name="operator-extension" value="operator XML default"/> <param name="operator-key" value="0"/> </profile> </profiles>
</configuration>
<!-- Send to voicemail (internal — after no answer on bridge) -->
<!-- This is already in the internal_extensions example above --> <!-- Check own voicemail: dial *98 -->
<extension name="voicemail_check"> <condition field="destination_number" expression="^\*98$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain}"/> </condition>
</extension> <!-- Leave voicemail directly for someone: dial *99 + extension -->
<extension name="voicemail_leave"> <condition field="destination_number" expression="^\*99(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="default $${domain} $1"/> </condition>
</extension> <!-- Check specific mailbox (for shared mailboxes): dial *97 + extension -->
<extension name="voicemail_check_specific"> <condition field="destination_number" expression="^\*97(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain} $1"/> </condition>
</extension>
<!-- Send to voicemail (internal — after no answer on bridge) -->
<!-- This is already in the internal_extensions example above --> <!-- Check own voicemail: dial *98 -->
<extension name="voicemail_check"> <condition field="destination_number" expression="^\*98$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain}"/> </condition>
</extension> <!-- Leave voicemail directly for someone: dial *99 + extension -->
<extension name="voicemail_leave"> <condition field="destination_number" expression="^\*99(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="default $${domain} $1"/> </condition>
</extension> <!-- Check specific mailbox (for shared mailboxes): dial *97 + extension -->
<extension name="voicemail_check_specific"> <condition field="destination_number" expression="^\*97(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain} $1"/> </condition>
</extension>
<!-- Send to voicemail (internal — after no answer on bridge) -->
<!-- This is already in the internal_extensions example above --> <!-- Check own voicemail: dial *98 -->
<extension name="voicemail_check"> <condition field="destination_number" expression="^\*98$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain}"/> </condition>
</extension> <!-- Leave voicemail directly for someone: dial *99 + extension -->
<extension name="voicemail_leave"> <condition field="destination_number" expression="^\*99(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="default $${domain} $1"/> </condition>
</extension> <!-- Check specific mailbox (for shared mailboxes): dial *97 + extension -->
<extension name="voicemail_check_specific"> <condition field="destination_number" expression="^\*97(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain} $1"/> </condition>
</extension>
# Install a lightweight MTA
apt-get install -y msmtp msmtp-mta # Configure SMTP relay
cat > /etc/msmtprc << 'EOF'
defaults
auth on
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile /var/log/msmtp.log account default
host smtp.gmail.com
port 587
from voicemail@YOUR_DOMAIN
user [email protected]
password your_app_password
EOF chmod 600 /etc/msmtprc
# Install a lightweight MTA
apt-get install -y msmtp msmtp-mta # Configure SMTP relay
cat > /etc/msmtprc << 'EOF'
defaults
auth on
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile /var/log/msmtp.log account default
host smtp.gmail.com
port 587
from voicemail@YOUR_DOMAIN
user [email protected]
password your_app_password
EOF chmod 600 /etc/msmtprc
# Install a lightweight MTA
apt-get install -y msmtp msmtp-mta # Configure SMTP relay
cat > /etc/msmtprc << 'EOF'
defaults
auth on
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile /var/log/msmtp.log account default
host smtp.gmail.com
port 587
from voicemail@YOUR_DOMAIN
user [email protected]
password your_app_password
EOF chmod 600 /etc/msmtprc
<!-- In /etc/freeswitch/directory/default/1001.xml -->
<include> <user id="1001"> <params> <param name="password" value="Str0ng_P@ss_1001!"/> <param name="vm-password" value="1001"/> <param name="vm-mailto" value="[email protected]"/> <param name="vm-email-all-messages" value="true"/> <param name="vm-attach-file" value="true"/> <param name="vm-keep-local-after-email" value="true"/> </params> <!-- ... variables ... --> </user>
</include>
<!-- In /etc/freeswitch/directory/default/1001.xml -->
<include> <user id="1001"> <params> <param name="password" value="Str0ng_P@ss_1001!"/> <param name="vm-password" value="1001"/> <param name="vm-mailto" value="[email protected]"/> <param name="vm-email-all-messages" value="true"/> <param name="vm-attach-file" value="true"/> <param name="vm-keep-local-after-email" value="true"/> </params> <!-- ... variables ... --> </user>
</include>
<!-- In /etc/freeswitch/directory/default/1001.xml -->
<include> <user id="1001"> <params> <param name="password" value="Str0ng_P@ss_1001!"/> <param name="vm-password" value="1001"/> <param name="vm-mailto" value="[email protected]"/> <param name="vm-email-all-messages" value="true"/> <param name="vm-attach-file" value="true"/> <param name="vm-keep-local-after-email" value="true"/> </params> <!-- ... variables ... --> </user>
</include>
#!/bin/bash
# /usr/local/bin/cleanup-voicemail.sh
# Delete voicemail messages older than 30 days STORAGE_DIR="/var/lib/freeswitch/storage/voicemail"
MAX_AGE=30 echo "$(date '+%Y-%m-%d %H:%M:%S') — Voicemail cleanup starting" # Find and delete old voicemail recordings
find "${STORAGE_DIR}" -name "*.wav" -type f -mtime +${MAX_AGE} -delete
find "${STORAGE_DIR}" -name "*.mp3" -type f -mtime +${MAX_AGE} -delete # Clean up empty directories
find "${STORAGE_DIR}" -type d -empty -delete echo "$(date '+%Y-%m-%d %H:%M:%S') — Voicemail cleanup complete"
#!/bin/bash
# /usr/local/bin/cleanup-voicemail.sh
# Delete voicemail messages older than 30 days STORAGE_DIR="/var/lib/freeswitch/storage/voicemail"
MAX_AGE=30 echo "$(date '+%Y-%m-%d %H:%M:%S') — Voicemail cleanup starting" # Find and delete old voicemail recordings
find "${STORAGE_DIR}" -name "*.wav" -type f -mtime +${MAX_AGE} -delete
find "${STORAGE_DIR}" -name "*.mp3" -type f -mtime +${MAX_AGE} -delete # Clean up empty directories
find "${STORAGE_DIR}" -type d -empty -delete echo "$(date '+%Y-%m-%d %H:%M:%S') — Voicemail cleanup complete"
#!/bin/bash
# /usr/local/bin/cleanup-voicemail.sh
# Delete voicemail messages older than 30 days STORAGE_DIR="/var/lib/freeswitch/storage/voicemail"
MAX_AGE=30 echo "$(date '+%Y-%m-%d %H:%M:%S') — Voicemail cleanup starting" # Find and delete old voicemail recordings
find "${STORAGE_DIR}" -name "*.wav" -type f -mtime +${MAX_AGE} -delete
find "${STORAGE_DIR}" -name "*.mp3" -type f -mtime +${MAX_AGE} -delete # Clean up empty directories
find "${STORAGE_DIR}" -type d -empty -delete echo "$(date '+%Y-%m-%d %H:%M:%S') — Voicemail cleanup complete"
chmod +x /usr/local/bin/cleanup-voicemail.sh # Run weekly
echo "0 3 * * 0 root /usr/local/bin/cleanup-voicemail.sh >> /var/log/voicemail-cleanup.log 2>&1" \ > /etc/cron.d/voicemail-cleanup
chmod +x /usr/local/bin/cleanup-voicemail.sh # Run weekly
echo "0 3 * * 0 root /usr/local/bin/cleanup-voicemail.sh >> /var/log/voicemail-cleanup.log 2>&1" \ > /etc/cron.d/voicemail-cleanup
chmod +x /usr/local/bin/cleanup-voicemail.sh # Run weekly
echo "0 3 * * 0 root /usr/local/bin/cleanup-voicemail.sh >> /var/log/voicemail-cleanup.log 2>&1" \ > /etc/cron.d/voicemail-cleanup
<extension name="recorded_internal_call"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <!-- Start recording BEFORE bridging --> <action application="set" data="RECORD_STEREO=true"/> <action application="set" data="media_bug_answer_req=true"/> <action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}_${caller_id_number}_to_${dialed_extension}.wav"/> <!-- Now bridge the call --> <action application="set" data="call_timeout=30"/> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="continue_on_fail=true"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> <!-- Voicemail on no answer (recording continues into VM) --> <action application="answer"/> <action application="voicemail" data="default $${domain} ${dialed_extension}"/> </condition>
</extension>
<extension name="recorded_internal_call"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <!-- Start recording BEFORE bridging --> <action application="set" data="RECORD_STEREO=true"/> <action application="set" data="media_bug_answer_req=true"/> <action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}_${caller_id_number}_to_${dialed_extension}.wav"/> <!-- Now bridge the call --> <action application="set" data="call_timeout=30"/> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="continue_on_fail=true"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> <!-- Voicemail on no answer (recording continues into VM) --> <action application="answer"/> <action application="voicemail" data="default $${domain} ${dialed_extension}"/> </condition>
</extension>
<extension name="recorded_internal_call"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <!-- Start recording BEFORE bridging --> <action application="set" data="RECORD_STEREO=true"/> <action application="set" data="media_bug_answer_req=true"/> <action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}_${caller_id_number}_to_${dialed_extension}.wav"/> <!-- Now bridge the call --> <action application="set" data="call_timeout=30"/> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="continue_on_fail=true"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> <!-- Voicemail on no answer (recording continues into VM) --> <action application="answer"/> <action application="voicemail" data="default $${domain} ${dialed_extension}"/> </condition>
</extension>
<!-- Date-based subdirectories + descriptive filenames -->
<action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}_${caller_id_number}_to_${destination_number}_${strftime(%Y%m%d-%H%M%S)}.wav"/>
<!-- Date-based subdirectories + descriptive filenames -->
<action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}_${caller_id_number}_to_${destination_number}_${strftime(%Y%m%d-%H%M%S)}.wav"/>
<!-- Date-based subdirectories + descriptive filenames -->
<action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}_${caller_id_number}_to_${destination_number}_${strftime(%Y%m%d-%H%M%S)}.wav"/>
/var/lib/freeswitch/recordings/2026/03/14/abc123_15551234567_to_1001_20260314-143022.wav
/var/lib/freeswitch/recordings/2026/03/14/abc123_15551234567_to_1001_20260314-143022.wav
/var/lib/freeswitch/recordings/2026/03/14/abc123_15551234567_to_1001_20260314-143022.wav
<!-- Enable stereo recording -->
<action application="set" data="RECORD_STEREO=true"/> <!-- Record in stereo WAV -->
<action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}.wav"/>
<!-- Enable stereo recording -->
<action application="set" data="RECORD_STEREO=true"/> <!-- Record in stereo WAV -->
<action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}.wav"/>
<!-- Enable stereo recording -->
<action application="set" data="RECORD_STEREO=true"/> <!-- Record in stereo WAV -->
<action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}.wav"/>
# Start recording on an active call (from fs_cli or ESL)
uuid_record <call-uuid> start /var/lib/freeswitch/recordings/mid_call_recording.wav # Stop recording
uuid_record <call-uuid> stop /var/lib/freeswitch/recordings/mid_call_recording.wav # Stop all recordings on a call
uuid_record <call-uuid> stop all
# Start recording on an active call (from fs_cli or ESL)
uuid_record <call-uuid> start /var/lib/freeswitch/recordings/mid_call_recording.wav # Stop recording
uuid_record <call-uuid> stop /var/lib/freeswitch/recordings/mid_call_recording.wav # Stop all recordings on a call
uuid_record <call-uuid> stop all
# Start recording on an active call (from fs_cli or ESL)
uuid_record <call-uuid> start /var/lib/freeswitch/recordings/mid_call_recording.wav # Stop recording
uuid_record <call-uuid> stop /var/lib/freeswitch/recordings/mid_call_recording.wav # Stop all recordings on a call
uuid_record <call-uuid> stop all
<!-- Agent presses *1 to start recording, *2 to stop -->
<extension name="record_on_demand"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <!-- Bind DTMF keys for recording control --> <action application="bind_digit_action" data="rec,*1,exec:record_session,/var/lib/freeswitch/recordings/${uuid}_on_demand.wav"/> <action application="bind_digit_action" data="rec,*2,exec:stop_record_session,/var/lib/freeswitch/recordings/${uuid}_on_demand.wav"/> <action application="digit_action_set_realm" data="rec"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> </condition>
</extension>
<!-- Agent presses *1 to start recording, *2 to stop -->
<extension name="record_on_demand"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <!-- Bind DTMF keys for recording control --> <action application="bind_digit_action" data="rec,*1,exec:record_session,/var/lib/freeswitch/recordings/${uuid}_on_demand.wav"/> <action application="bind_digit_action" data="rec,*2,exec:stop_record_session,/var/lib/freeswitch/recordings/${uuid}_on_demand.wav"/> <action application="digit_action_set_realm" data="rec"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> </condition>
</extension>
<!-- Agent presses *1 to start recording, *2 to stop -->
<extension name="record_on_demand"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <!-- Bind DTMF keys for recording control --> <action application="bind_digit_action" data="rec,*1,exec:record_session,/var/lib/freeswitch/recordings/${uuid}_on_demand.wav"/> <action application="bind_digit_action" data="rec,*2,exec:stop_record_session,/var/lib/freeswitch/recordings/${uuid}_on_demand.wav"/> <action application="digit_action_set_realm" data="rec"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> </condition>
</extension>
#!/bin/bash
# /usr/local/bin/convert-recordings.sh
# Convert WAV recordings to MP3 and archive RECORDINGS_DIR="/var/lib/freeswitch/recordings"
ARCHIVE_DIR="/var/lib/freeswitch/recordings-mp3"
LOG="/var/log/recording-convert.log" # Install lame if not present
which lame > /dev/null 2>&1 || apt-get install -y lame # Process WAV files older than 5 minutes (avoid in-progress recordings)
find "${RECORDINGS_DIR}" -name "*.wav" -type f -mmin +5 | while read WAV_FILE; do # Build MP3 path (mirror directory structure) REL_PATH="${WAV_FILE#${RECORDINGS_DIR}/}" MP3_FILE="${ARCHIVE_DIR}/${REL_PATH%.wav}.mp3" MP3_DIR=$(dirname "${MP3_FILE}") # Create directory mkdir -p "${MP3_DIR}" # Convert if lame --quiet -V2 "${WAV_FILE}" "${MP3_FILE}" 2>/dev/null; then echo "$(date '+%Y-%m-%d %H:%M:%S') Converted: ${REL_PATH}" >> "${LOG}" # Delete original WAV after successful conversion rm -f "${WAV_FILE}" else echo "$(date '+%Y-%m-%d %H:%M:%S') FAILED: ${REL_PATH}" >> "${LOG}" fi
done # Clean up empty directories
find "${RECORDINGS_DIR}" -type d -empty -delete 2>/dev/null
#!/bin/bash
# /usr/local/bin/convert-recordings.sh
# Convert WAV recordings to MP3 and archive RECORDINGS_DIR="/var/lib/freeswitch/recordings"
ARCHIVE_DIR="/var/lib/freeswitch/recordings-mp3"
LOG="/var/log/recording-convert.log" # Install lame if not present
which lame > /dev/null 2>&1 || apt-get install -y lame # Process WAV files older than 5 minutes (avoid in-progress recordings)
find "${RECORDINGS_DIR}" -name "*.wav" -type f -mmin +5 | while read WAV_FILE; do # Build MP3 path (mirror directory structure) REL_PATH="${WAV_FILE#${RECORDINGS_DIR}/}" MP3_FILE="${ARCHIVE_DIR}/${REL_PATH%.wav}.mp3" MP3_DIR=$(dirname "${MP3_FILE}") # Create directory mkdir -p "${MP3_DIR}" # Convert if lame --quiet -V2 "${WAV_FILE}" "${MP3_FILE}" 2>/dev/null; then echo "$(date '+%Y-%m-%d %H:%M:%S') Converted: ${REL_PATH}" >> "${LOG}" # Delete original WAV after successful conversion rm -f "${WAV_FILE}" else echo "$(date '+%Y-%m-%d %H:%M:%S') FAILED: ${REL_PATH}" >> "${LOG}" fi
done # Clean up empty directories
find "${RECORDINGS_DIR}" -type d -empty -delete 2>/dev/null
#!/bin/bash
# /usr/local/bin/convert-recordings.sh
# Convert WAV recordings to MP3 and archive RECORDINGS_DIR="/var/lib/freeswitch/recordings"
ARCHIVE_DIR="/var/lib/freeswitch/recordings-mp3"
LOG="/var/log/recording-convert.log" # Install lame if not present
which lame > /dev/null 2>&1 || apt-get install -y lame # Process WAV files older than 5 minutes (avoid in-progress recordings)
find "${RECORDINGS_DIR}" -name "*.wav" -type f -mmin +5 | while read WAV_FILE; do # Build MP3 path (mirror directory structure) REL_PATH="${WAV_FILE#${RECORDINGS_DIR}/}" MP3_FILE="${ARCHIVE_DIR}/${REL_PATH%.wav}.mp3" MP3_DIR=$(dirname "${MP3_FILE}") # Create directory mkdir -p "${MP3_DIR}" # Convert if lame --quiet -V2 "${WAV_FILE}" "${MP3_FILE}" 2>/dev/null; then echo "$(date '+%Y-%m-%d %H:%M:%S') Converted: ${REL_PATH}" >> "${LOG}" # Delete original WAV after successful conversion rm -f "${WAV_FILE}" else echo "$(date '+%Y-%m-%d %H:%M:%S') FAILED: ${REL_PATH}" >> "${LOG}" fi
done # Clean up empty directories
find "${RECORDINGS_DIR}" -type d -empty -delete 2>/dev/null
chmod +x /usr/local/bin/convert-recordings.sh # Run every 30 minutes
echo "*/30 * * * * root /usr/local/bin/convert-recordings.sh" \ > /etc/cron.d/recording-convert
chmod +x /usr/local/bin/convert-recordings.sh # Run every 30 minutes
echo "*/30 * * * * root /usr/local/bin/convert-recordings.sh" \ > /etc/cron.d/recording-convert
chmod +x /usr/local/bin/convert-recordings.sh # Run every 30 minutes
echo "*/30 * * * * root /usr/local/bin/convert-recordings.sh" \ > /etc/cron.d/recording-convert
#!/bin/bash
# /usr/local/bin/cleanup-recordings.sh
# Delete recordings older than retention period RECORDINGS_DIR="/var/lib/freeswitch/recordings"
MP3_DIR="/var/lib/freeswitch/recordings-mp3"
RETENTION_DAYS=90 echo "$(date '+%Y-%m-%d %H:%M:%S') — Recording cleanup starting"
echo "Retention: ${RETENTION_DAYS} days" # Delete old WAV files
WAV_COUNT=$(find "${RECORDINGS_DIR}" -name "*.wav" -type f -mtime +${RETENTION_DAYS} | wc -l)
find "${RECORDINGS_DIR}" -name "*.wav" -type f -mtime +${RETENTION_DAYS} -delete
echo "Deleted ${WAV_COUNT} WAV files older than ${RETENTION_DAYS} days" # Delete old MP3 files
MP3_COUNT=$(find "${MP3_DIR}" -name "*.mp3" -type f -mtime +${RETENTION_DAYS} | wc -l)
find "${MP3_DIR}" -name "*.mp3" -type f -mtime +${RETENTION_DAYS} -delete
echo "Deleted ${MP3_COUNT} MP3 files older than ${RETENTION_DAYS} days" # Clean empty directories
find "${RECORDINGS_DIR}" -type d -empty -delete 2>/dev/null
find "${MP3_DIR}" -type d -empty -delete 2>/dev/null # Report disk usage
echo "Current recording storage:"
du -sh "${RECORDINGS_DIR}" "${MP3_DIR}" 2>/dev/null echo "$(date '+%Y-%m-%d %H:%M:%S') — Cleanup complete"
#!/bin/bash
# /usr/local/bin/cleanup-recordings.sh
# Delete recordings older than retention period RECORDINGS_DIR="/var/lib/freeswitch/recordings"
MP3_DIR="/var/lib/freeswitch/recordings-mp3"
RETENTION_DAYS=90 echo "$(date '+%Y-%m-%d %H:%M:%S') — Recording cleanup starting"
echo "Retention: ${RETENTION_DAYS} days" # Delete old WAV files
WAV_COUNT=$(find "${RECORDINGS_DIR}" -name "*.wav" -type f -mtime +${RETENTION_DAYS} | wc -l)
find "${RECORDINGS_DIR}" -name "*.wav" -type f -mtime +${RETENTION_DAYS} -delete
echo "Deleted ${WAV_COUNT} WAV files older than ${RETENTION_DAYS} days" # Delete old MP3 files
MP3_COUNT=$(find "${MP3_DIR}" -name "*.mp3" -type f -mtime +${RETENTION_DAYS} | wc -l)
find "${MP3_DIR}" -name "*.mp3" -type f -mtime +${RETENTION_DAYS} -delete
echo "Deleted ${MP3_COUNT} MP3 files older than ${RETENTION_DAYS} days" # Clean empty directories
find "${RECORDINGS_DIR}" -type d -empty -delete 2>/dev/null
find "${MP3_DIR}" -type d -empty -delete 2>/dev/null # Report disk usage
echo "Current recording storage:"
du -sh "${RECORDINGS_DIR}" "${MP3_DIR}" 2>/dev/null echo "$(date '+%Y-%m-%d %H:%M:%S') — Cleanup complete"
#!/bin/bash
# /usr/local/bin/cleanup-recordings.sh
# Delete recordings older than retention period RECORDINGS_DIR="/var/lib/freeswitch/recordings"
MP3_DIR="/var/lib/freeswitch/recordings-mp3"
RETENTION_DAYS=90 echo "$(date '+%Y-%m-%d %H:%M:%S') — Recording cleanup starting"
echo "Retention: ${RETENTION_DAYS} days" # Delete old WAV files
WAV_COUNT=$(find "${RECORDINGS_DIR}" -name "*.wav" -type f -mtime +${RETENTION_DAYS} | wc -l)
find "${RECORDINGS_DIR}" -name "*.wav" -type f -mtime +${RETENTION_DAYS} -delete
echo "Deleted ${WAV_COUNT} WAV files older than ${RETENTION_DAYS} days" # Delete old MP3 files
MP3_COUNT=$(find "${MP3_DIR}" -name "*.mp3" -type f -mtime +${RETENTION_DAYS} | wc -l)
find "${MP3_DIR}" -name "*.mp3" -type f -mtime +${RETENTION_DAYS} -delete
echo "Deleted ${MP3_COUNT} MP3 files older than ${RETENTION_DAYS} days" # Clean empty directories
find "${RECORDINGS_DIR}" -type d -empty -delete 2>/dev/null
find "${MP3_DIR}" -type d -empty -delete 2>/dev/null # Report disk usage
echo "Current recording storage:"
du -sh "${RECORDINGS_DIR}" "${MP3_DIR}" 2>/dev/null echo "$(date '+%Y-%m-%d %H:%M:%S') — Cleanup complete"
chmod +x /usr/local/bin/cleanup-recordings.sh # Run daily at 4 AM
echo "0 4 * * * root /usr/local/bin/cleanup-recordings.sh >> /var/log/recording-cleanup.log 2>&1" \ > /etc/cron.d/recording-cleanup
chmod +x /usr/local/bin/cleanup-recordings.sh # Run daily at 4 AM
echo "0 4 * * * root /usr/local/bin/cleanup-recordings.sh >> /var/log/recording-cleanup.log 2>&1" \ > /etc/cron.d/recording-cleanup
chmod +x /usr/local/bin/cleanup-recordings.sh # Run daily at 4 AM
echo "0 4 * * * root /usr/local/bin/cleanup-recordings.sh >> /var/log/recording-cleanup.log 2>&1" \ > /etc/cron.d/recording-cleanup
<configuration name="conference.conf" description="Audio Conference"> <advertise> <!-- Advertise conference rooms via SIP SUBSCRIBE --> <room name="3001@$${domain}" status="FreeSWITCH"/> </advertise> <caller-controls> <!-- Default key bindings for participants --> <group name="default"> <control action="mute" digits="0"/> <control action="deaf mute" digits="*"/> <control action="energy up" digits="9"/> <control action="energy equ" digits="8"/> <control action="energy dn" digits="7"/> <control action="vol talk up" digits="3"/> <control action="vol talk zero" digits="2"/> <control action="vol talk dn" digits="1"/> <control action="vol listen up" digits="6"/> <control action="vol listen zero" digits="5"/> <control action="vol listen dn" digits="4"/> <control action="hangup" digits="#"/> </group> <!-- Moderator key bindings --> <group name="moderator"> <control action="mute" digits="0"/> <control action="deaf mute" digits="*"/> <control action="hangup" digits="#"/> <control action="lock" digits="*1"/> <control action="mute non_moderator" digits="*5"/> <control action="unmute non_moderator" digits="*6"/> <control action="kick last" digits="*7"/> <control action="transfer" digits="*9" data="1009 XML default"/> </group> </caller-controls> <profiles> <!-- Standard conference profile --> <profile name="default"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <!-- Comfort noise for silence --> <param name="comfort-noise" value="true"/> <!-- Sounds --> <param name="muted-sound" value="conference/conf-muted.wav"/> <param name="unmuted-sound" value="conference/conf-unmuted.wav"/> <param name="alone-sound" value="conference/conf-alone.wav"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="kicked-sound" value="conference/conf-kicked.wav"/> <param name="locked-sound" value="conference/conf-locked.wav"/> <param name="is-locked-sound" value="conference/conf-is-locked.wav"/> <param name="is-unlocked-sound" value="conference/conf-is-unlocked.wav"/> <param name="pin-sound" value="conference/conf-pin.wav"/> <param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/> <!-- Controls --> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <!-- Auto-record all conferences --> <!-- <param name="auto-record" value="/var/lib/freeswitch/recordings/conference/${conference_name}_${strftime(%Y%m%d-%H%M%S)}.wav"/> --> <!-- Max members (0 = unlimited) --> <param name="max-members" value="100"/> <!-- Announce count of members when joining --> <param name="announce-count" value="5"/> <!-- Codec preferences --> <param name="conference-flags" value="wait-mod|audio-always|waste-bandwidth"/> </profile> <!-- PIN-protected conference profile --> <profile name="secure"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <param name="comfort-noise" value="true"/> <!-- PIN required --> <param name="pin" value="12345"/> <param name="pin-retries" value="3"/> <param name="pin-sound" value="conference/conf-pin.wav"/> <param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <param name="max-members" value="50"/> <param name="conference-flags" value="wait-mod|audio-always"/> </profile> <!-- Webinar profile: listeners muted by default --> <profile name="webinar"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <param name="comfort-noise" value="true"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <param name="max-members" value="500"/> <!-- Members join muted --> <param name="member-flags" value="mute"/> <param name="conference-flags" value="wait-mod|audio-always"/> <!-- Auto-record webinars --> <param name="auto-record" value="/var/lib/freeswitch/recordings/webinar/${conference_name}_${strftime(%Y%m%d-%H%M%S)}.wav"/> </profile> </profiles>
</configuration>
<configuration name="conference.conf" description="Audio Conference"> <advertise> <!-- Advertise conference rooms via SIP SUBSCRIBE --> <room name="3001@$${domain}" status="FreeSWITCH"/> </advertise> <caller-controls> <!-- Default key bindings for participants --> <group name="default"> <control action="mute" digits="0"/> <control action="deaf mute" digits="*"/> <control action="energy up" digits="9"/> <control action="energy equ" digits="8"/> <control action="energy dn" digits="7"/> <control action="vol talk up" digits="3"/> <control action="vol talk zero" digits="2"/> <control action="vol talk dn" digits="1"/> <control action="vol listen up" digits="6"/> <control action="vol listen zero" digits="5"/> <control action="vol listen dn" digits="4"/> <control action="hangup" digits="#"/> </group> <!-- Moderator key bindings --> <group name="moderator"> <control action="mute" digits="0"/> <control action="deaf mute" digits="*"/> <control action="hangup" digits="#"/> <control action="lock" digits="*1"/> <control action="mute non_moderator" digits="*5"/> <control action="unmute non_moderator" digits="*6"/> <control action="kick last" digits="*7"/> <control action="transfer" digits="*9" data="1009 XML default"/> </group> </caller-controls> <profiles> <!-- Standard conference profile --> <profile name="default"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <!-- Comfort noise for silence --> <param name="comfort-noise" value="true"/> <!-- Sounds --> <param name="muted-sound" value="conference/conf-muted.wav"/> <param name="unmuted-sound" value="conference/conf-unmuted.wav"/> <param name="alone-sound" value="conference/conf-alone.wav"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="kicked-sound" value="conference/conf-kicked.wav"/> <param name="locked-sound" value="conference/conf-locked.wav"/> <param name="is-locked-sound" value="conference/conf-is-locked.wav"/> <param name="is-unlocked-sound" value="conference/conf-is-unlocked.wav"/> <param name="pin-sound" value="conference/conf-pin.wav"/> <param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/> <!-- Controls --> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <!-- Auto-record all conferences --> <!-- <param name="auto-record" value="/var/lib/freeswitch/recordings/conference/${conference_name}_${strftime(%Y%m%d-%H%M%S)}.wav"/> --> <!-- Max members (0 = unlimited) --> <param name="max-members" value="100"/> <!-- Announce count of members when joining --> <param name="announce-count" value="5"/> <!-- Codec preferences --> <param name="conference-flags" value="wait-mod|audio-always|waste-bandwidth"/> </profile> <!-- PIN-protected conference profile --> <profile name="secure"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <param name="comfort-noise" value="true"/> <!-- PIN required --> <param name="pin" value="12345"/> <param name="pin-retries" value="3"/> <param name="pin-sound" value="conference/conf-pin.wav"/> <param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <param name="max-members" value="50"/> <param name="conference-flags" value="wait-mod|audio-always"/> </profile> <!-- Webinar profile: listeners muted by default --> <profile name="webinar"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <param name="comfort-noise" value="true"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <param name="max-members" value="500"/> <!-- Members join muted --> <param name="member-flags" value="mute"/> <param name="conference-flags" value="wait-mod|audio-always"/> <!-- Auto-record webinars --> <param name="auto-record" value="/var/lib/freeswitch/recordings/webinar/${conference_name}_${strftime(%Y%m%d-%H%M%S)}.wav"/> </profile> </profiles>
</configuration>
<configuration name="conference.conf" description="Audio Conference"> <advertise> <!-- Advertise conference rooms via SIP SUBSCRIBE --> <room name="3001@$${domain}" status="FreeSWITCH"/> </advertise> <caller-controls> <!-- Default key bindings for participants --> <group name="default"> <control action="mute" digits="0"/> <control action="deaf mute" digits="*"/> <control action="energy up" digits="9"/> <control action="energy equ" digits="8"/> <control action="energy dn" digits="7"/> <control action="vol talk up" digits="3"/> <control action="vol talk zero" digits="2"/> <control action="vol talk dn" digits="1"/> <control action="vol listen up" digits="6"/> <control action="vol listen zero" digits="5"/> <control action="vol listen dn" digits="4"/> <control action="hangup" digits="#"/> </group> <!-- Moderator key bindings --> <group name="moderator"> <control action="mute" digits="0"/> <control action="deaf mute" digits="*"/> <control action="hangup" digits="#"/> <control action="lock" digits="*1"/> <control action="mute non_moderator" digits="*5"/> <control action="unmute non_moderator" digits="*6"/> <control action="kick last" digits="*7"/> <control action="transfer" digits="*9" data="1009 XML default"/> </group> </caller-controls> <profiles> <!-- Standard conference profile --> <profile name="default"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <!-- Comfort noise for silence --> <param name="comfort-noise" value="true"/> <!-- Sounds --> <param name="muted-sound" value="conference/conf-muted.wav"/> <param name="unmuted-sound" value="conference/conf-unmuted.wav"/> <param name="alone-sound" value="conference/conf-alone.wav"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="kicked-sound" value="conference/conf-kicked.wav"/> <param name="locked-sound" value="conference/conf-locked.wav"/> <param name="is-locked-sound" value="conference/conf-is-locked.wav"/> <param name="is-unlocked-sound" value="conference/conf-is-unlocked.wav"/> <param name="pin-sound" value="conference/conf-pin.wav"/> <param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/> <!-- Controls --> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <!-- Auto-record all conferences --> <!-- <param name="auto-record" value="/var/lib/freeswitch/recordings/conference/${conference_name}_${strftime(%Y%m%d-%H%M%S)}.wav"/> --> <!-- Max members (0 = unlimited) --> <param name="max-members" value="100"/> <!-- Announce count of members when joining --> <param name="announce-count" value="5"/> <!-- Codec preferences --> <param name="conference-flags" value="wait-mod|audio-always|waste-bandwidth"/> </profile> <!-- PIN-protected conference profile --> <profile name="secure"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <param name="comfort-noise" value="true"/> <!-- PIN required --> <param name="pin" value="12345"/> <param name="pin-retries" value="3"/> <param name="pin-sound" value="conference/conf-pin.wav"/> <param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <param name="max-members" value="50"/> <param name="conference-flags" value="wait-mod|audio-always"/> </profile> <!-- Webinar profile: listeners muted by default --> <profile name="webinar"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <param name="comfort-noise" value="true"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <param name="max-members" value="500"/> <!-- Members join muted --> <param name="member-flags" value="mute"/> <param name="conference-flags" value="wait-mod|audio-always"/> <!-- Auto-record webinars --> <param name="auto-record" value="/var/lib/freeswitch/recordings/webinar/${conference_name}_${strftime(%Y%m%d-%H%M%S)}.wav"/> </profile> </profiles>
</configuration>
<!-- ============================================ -->
<!-- CONFERENCE ROOMS -->
<!-- ============================================ --> <!-- Standard conference rooms: dial 3001-3099 -->
<extension name="conference_standard"> <condition field="destination_number" expression="^(30[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="room_$1@default"/> </condition>
</extension> <!-- PIN-protected conference rooms: dial 3100-3199 -->
<extension name="conference_secure"> <condition field="destination_number" expression="^(31[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="room_$1@secure"/> </condition>
</extension> <!-- Moderator entry: dial 3200 + room number (e.g., 32003001 for room 3001) -->
<extension name="conference_moderator"> <condition field="destination_number" expression="^3200(30[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="set" data="conference_member_flags=moderator"/> <action application="conference" data="room_$1@default+flags{moderator}"/> </condition>
</extension> <!-- Webinar rooms: dial 3300-3399 (listeners muted) -->
<extension name="conference_webinar"> <condition field="destination_number" expression="^(33[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="webinar_$1@webinar"/> </condition>
</extension> <!-- Webinar presenter: dial 3400 + room number -->
<extension name="conference_webinar_presenter"> <condition field="destination_number" expression="^3400(33[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="webinar_$1@webinar+flags{moderator}"/> </condition>
</extension>
<!-- ============================================ -->
<!-- CONFERENCE ROOMS -->
<!-- ============================================ --> <!-- Standard conference rooms: dial 3001-3099 -->
<extension name="conference_standard"> <condition field="destination_number" expression="^(30[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="room_$1@default"/> </condition>
</extension> <!-- PIN-protected conference rooms: dial 3100-3199 -->
<extension name="conference_secure"> <condition field="destination_number" expression="^(31[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="room_$1@secure"/> </condition>
</extension> <!-- Moderator entry: dial 3200 + room number (e.g., 32003001 for room 3001) -->
<extension name="conference_moderator"> <condition field="destination_number" expression="^3200(30[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="set" data="conference_member_flags=moderator"/> <action application="conference" data="room_$1@default+flags{moderator}"/> </condition>
</extension> <!-- Webinar rooms: dial 3300-3399 (listeners muted) -->
<extension name="conference_webinar"> <condition field="destination_number" expression="^(33[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="webinar_$1@webinar"/> </condition>
</extension> <!-- Webinar presenter: dial 3400 + room number -->
<extension name="conference_webinar_presenter"> <condition field="destination_number" expression="^3400(33[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="webinar_$1@webinar+flags{moderator}"/> </condition>
</extension>
<!-- ============================================ -->
<!-- CONFERENCE ROOMS -->
<!-- ============================================ --> <!-- Standard conference rooms: dial 3001-3099 -->
<extension name="conference_standard"> <condition field="destination_number" expression="^(30[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="room_$1@default"/> </condition>
</extension> <!-- PIN-protected conference rooms: dial 3100-3199 -->
<extension name="conference_secure"> <condition field="destination_number" expression="^(31[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="room_$1@secure"/> </condition>
</extension> <!-- Moderator entry: dial 3200 + room number (e.g., 32003001 for room 3001) -->
<extension name="conference_moderator"> <condition field="destination_number" expression="^3200(30[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="set" data="conference_member_flags=moderator"/> <action application="conference" data="room_$1@default+flags{moderator}"/> </condition>
</extension> <!-- Webinar rooms: dial 3300-3399 (listeners muted) -->
<extension name="conference_webinar"> <condition field="destination_number" expression="^(33[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="webinar_$1@webinar"/> </condition>
</extension> <!-- Webinar presenter: dial 3400 + room number -->
<extension name="conference_webinar_presenter"> <condition field="destination_number" expression="^3400(33[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="webinar_$1@webinar+flags{moderator}"/> </condition>
</extension>
<extension name="conference_dynamic_pin"> <condition field="destination_number" expression="^(35[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <!-- Collect PIN from caller --> <action application="play_and_get_digits" data="4 6 3 10000 # conference/conf-pin.wav conference/conf-bad-pin.wav entered_pin \d{4,6} 10000"/> <!-- Verify PIN (in production, check against a database) --> <action application="set" data="expected_pin=7890"/> <action application="execute_extension" data="check_pin_${entered_pin} XML features"/> <!-- If we get here, PIN was correct --> <action application="conference" data="room_$1@default"/> </condition>
</extension>
<extension name="conference_dynamic_pin"> <condition field="destination_number" expression="^(35[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <!-- Collect PIN from caller --> <action application="play_and_get_digits" data="4 6 3 10000 # conference/conf-pin.wav conference/conf-bad-pin.wav entered_pin \d{4,6} 10000"/> <!-- Verify PIN (in production, check against a database) --> <action application="set" data="expected_pin=7890"/> <action application="execute_extension" data="check_pin_${entered_pin} XML features"/> <!-- If we get here, PIN was correct --> <action application="conference" data="room_$1@default"/> </condition>
</extension>
<extension name="conference_dynamic_pin"> <condition field="destination_number" expression="^(35[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <!-- Collect PIN from caller --> <action application="play_and_get_digits" data="4 6 3 10000 # conference/conf-pin.wav conference/conf-bad-pin.wav entered_pin \d{4,6} 10000"/> <!-- Verify PIN (in production, check against a database) --> <action application="set" data="expected_pin=7890"/> <action application="execute_extension" data="check_pin_${entered_pin} XML features"/> <!-- If we get here, PIN was correct --> <action application="conference" data="room_$1@default"/> </condition>
</extension>
# List active conferences
fs_cli -x "conference list" # List members in a specific conference
fs_cli -x "conference room_3001 list" # Mute a participant (by member ID)
fs_cli -x "conference room_3001 mute 3" # Unmute a participant
fs_cli -x "conference room_3001 unmute 3" # Kick a participant
fs_cli -x "conference room_3001 kick 3" # Lock the conference (no new members)
fs_cli -x "conference room_3001 lock" # Unlock
fs_cli -x "conference room_3001 unlock" # Mute all non-moderators
fs_cli -x "conference room_3001 mute non_moderator" # Start recording a conference
fs_cli -x "conference room_3001 record /var/lib/freeswitch/recordings/conference/room_3001_$(date +%Y%m%d).wav" # Stop recording
fs_cli -x "conference room_3001 norecord all" # Play a file into the conference
fs_cli -x "conference room_3001 play /var/lib/freeswitch/sounds/announcement.wav" # Get conference count
fs_cli -x "conference room_3001 count"
# List active conferences
fs_cli -x "conference list" # List members in a specific conference
fs_cli -x "conference room_3001 list" # Mute a participant (by member ID)
fs_cli -x "conference room_3001 mute 3" # Unmute a participant
fs_cli -x "conference room_3001 unmute 3" # Kick a participant
fs_cli -x "conference room_3001 kick 3" # Lock the conference (no new members)
fs_cli -x "conference room_3001 lock" # Unlock
fs_cli -x "conference room_3001 unlock" # Mute all non-moderators
fs_cli -x "conference room_3001 mute non_moderator" # Start recording a conference
fs_cli -x "conference room_3001 record /var/lib/freeswitch/recordings/conference/room_3001_$(date +%Y%m%d).wav" # Stop recording
fs_cli -x "conference room_3001 norecord all" # Play a file into the conference
fs_cli -x "conference room_3001 play /var/lib/freeswitch/sounds/announcement.wav" # Get conference count
fs_cli -x "conference room_3001 count"
# List active conferences
fs_cli -x "conference list" # List members in a specific conference
fs_cli -x "conference room_3001 list" # Mute a participant (by member ID)
fs_cli -x "conference room_3001 mute 3" # Unmute a participant
fs_cli -x "conference room_3001 unmute 3" # Kick a participant
fs_cli -x "conference room_3001 kick 3" # Lock the conference (no new members)
fs_cli -x "conference room_3001 lock" # Unlock
fs_cli -x "conference room_3001 unlock" # Mute all non-moderators
fs_cli -x "conference room_3001 mute non_moderator" # Start recording a conference
fs_cli -x "conference room_3001 record /var/lib/freeswitch/recordings/conference/room_3001_$(date +%Y%m%d).wav" # Stop recording
fs_cli -x "conference room_3001 norecord all" # Play a file into the conference
fs_cli -x "conference room_3001 play /var/lib/freeswitch/sounds/announcement.wav" # Get conference count
fs_cli -x "conference room_3001 count"
<configuration name="event_socket.conf" description="Socket Client"> <settings> <!-- Listen on localhost only (secure) --> <param name="listen-ip" value="127.0.0.1"/> <param name="listen-port" value="8021"/> <param name="password" value="YOUR_SECURE_ESL_PASSWORD"/> <!-- Optional: allow connections from specific IPs --> <!--<param name="listen-ip" value="0.0.0.0"/>--> <!--<param name="apply-inbound-acl" value="loopback.auto"/>--> </settings>
</configuration>
<configuration name="event_socket.conf" description="Socket Client"> <settings> <!-- Listen on localhost only (secure) --> <param name="listen-ip" value="127.0.0.1"/> <param name="listen-port" value="8021"/> <param name="password" value="YOUR_SECURE_ESL_PASSWORD"/> <!-- Optional: allow connections from specific IPs --> <!--<param name="listen-ip" value="0.0.0.0"/>--> <!--<param name="apply-inbound-acl" value="loopback.auto"/>--> </settings>
</configuration>
<configuration name="event_socket.conf" description="Socket Client"> <settings> <!-- Listen on localhost only (secure) --> <param name="listen-ip" value="127.0.0.1"/> <param name="listen-port" value="8021"/> <param name="password" value="YOUR_SECURE_ESL_PASSWORD"/> <!-- Optional: allow connections from specific IPs --> <!--<param name="listen-ip" value="0.0.0.0"/>--> <!--<param name="apply-inbound-acl" value="loopback.auto"/>--> </settings>
</configuration>
# Connect to local FreeSWITCH
fs_cli # Connect to remote FreeSWITCH
fs_cli -H 10.0.0.5 -P 8021 -p YOUR_SECURE_ESL_PASSWORD # Execute a single command and exit
fs_cli -x "sofia status" # Execute API command
fs_cli -x "show channels" # Execute background API command
fs_cli -x "bgapi originate user/1001 &echo()"
# Connect to local FreeSWITCH
fs_cli # Connect to remote FreeSWITCH
fs_cli -H 10.0.0.5 -P 8021 -p YOUR_SECURE_ESL_PASSWORD # Execute a single command and exit
fs_cli -x "sofia status" # Execute API command
fs_cli -x "show channels" # Execute background API command
fs_cli -x "bgapi originate user/1001 &echo()"
# Connect to local FreeSWITCH
fs_cli # Connect to remote FreeSWITCH
fs_cli -H 10.0.0.5 -P 8021 -p YOUR_SECURE_ESL_PASSWORD # Execute a single command and exit
fs_cli -x "sofia status" # Execute API command
fs_cli -x "show channels" # Execute background API command
fs_cli -x "bgapi originate user/1001 &echo()"
pip3 install greenswitch
pip3 install greenswitch
pip3 install greenswitch
#!/usr/bin/env python3
"""
freeswitch_monitor.py
Monitor FreeSWITCH events via ESL (inbound mode).
Logs call activity and tracks concurrent calls.
""" import greenswitch
import json
from datetime import datetime class FreeSWITCHMonitor: def __init__(self, host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD'): self.host = host self.port = port self.password = password self.active_calls = {} self.conn = None def connect(self): """Establish ESL connection.""" self.conn = greenswitch.InboundESL( host=self.host, port=self.port, password=self.password ) self.conn.connect() print(f"[{self._now()}] Connected to FreeSWITCH at {self.host}:{self.port}") def subscribe_events(self): """Subscribe to call-related events.""" # Subscribe to specific events self.conn.send( 'event plain CHANNEL_CREATE CHANNEL_ANSWER ' 'CHANNEL_HANGUP_COMPLETE DTMF CODEC' ) # Register event handlers self.conn.register_handle('CHANNEL_CREATE', self._on_channel_create) self.conn.register_handle('CHANNEL_ANSWER', self._on_channel_answer) self.conn.register_handle('CHANNEL_HANGUP_COMPLETE', self._on_channel_hangup) self.conn.register_handle('DTMF', self._on_dtmf) def _on_channel_create(self, event): """Called when a new channel is created.""" uuid = event.headers.get('Unique-ID', 'unknown') caller = event.headers.get('Caller-Caller-ID-Number', 'unknown') dest = event.headers.get('Caller-Destination-Number', 'unknown') direction = event.headers.get('Call-Direction', 'unknown') self.active_calls[uuid] = { 'caller': caller, 'destination': dest, 'direction': direction, 'created': datetime.now(), 'state': 'ringing' } print(f"[{self._now()}] NEW CALL: {caller} -> {dest} ({direction}) UUID={uuid}") print(f" Active calls: {len(self.active_calls)}") def _on_channel_answer(self, event): """Called when a channel is answered.""" uuid = event.headers.get('Unique-ID', 'unknown') if uuid in self.active_calls: self.active_calls[uuid]['state'] = 'answered' caller = self.active_calls[uuid]['caller'] dest = self.active_calls[uuid]['destination'] print(f"[{self._now()}] ANSWERED: {caller} -> {dest} UUID={uuid}") def _on_channel_hangup(self, event): """Called when a channel hangs up.""" uuid = event.headers.get('Unique-ID', 'unknown') cause = event.headers.get('Hangup-Cause', 'unknown') duration = event.headers.get('variable_billsec', '0') if uuid in self.active_calls: call = self.active_calls.pop(uuid) print( f"[{self._now()}] HANGUP: {call['caller']} -> {call['destination']} " f"Duration={duration}s Cause={cause} UUID={uuid}" ) print(f" Active calls: {len(self.active_calls)}") def _on_dtmf(self, event): """Called when DTMF is received.""" uuid = event.headers.get('Unique-ID', 'unknown') digit = event.headers.get('DTMF-Digit', '?') print(f"[{self._now()}] DTMF: digit={digit} UUID={uuid}") def run_api(self, command): """Execute an API command and return the result.""" result = self.conn.send(f'api {command}') return result.data if result else None def originate_call(self, endpoint, app='&park()'): """Place a new call.""" cmd = f'api originate {endpoint} {app}' result = self.conn.send(cmd) print(f"[{self._now()}] Originate result: {result.data if result else 'failed'}") return result def _now(self): return datetime.now().strftime('%Y-%m-%d %H:%M:%S') def run(self): """Main event loop.""" self.connect() self.subscribe_events() print(f"[{self._now()}] Listening for events... (Ctrl+C to stop)") # Get initial status status = self.run_api('status') if status: print(f"\n--- FreeSWITCH Status ---\n{status}\n") try: # This blocks and processes events self.conn.process_events() except KeyboardInterrupt: print(f"\n[{self._now()}] Shutting down...") if __name__ == '__main__': monitor = FreeSWITCHMonitor( host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD' ) monitor.run()
#!/usr/bin/env python3
"""
freeswitch_monitor.py
Monitor FreeSWITCH events via ESL (inbound mode).
Logs call activity and tracks concurrent calls.
""" import greenswitch
import json
from datetime import datetime class FreeSWITCHMonitor: def __init__(self, host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD'): self.host = host self.port = port self.password = password self.active_calls = {} self.conn = None def connect(self): """Establish ESL connection.""" self.conn = greenswitch.InboundESL( host=self.host, port=self.port, password=self.password ) self.conn.connect() print(f"[{self._now()}] Connected to FreeSWITCH at {self.host}:{self.port}") def subscribe_events(self): """Subscribe to call-related events.""" # Subscribe to specific events self.conn.send( 'event plain CHANNEL_CREATE CHANNEL_ANSWER ' 'CHANNEL_HANGUP_COMPLETE DTMF CODEC' ) # Register event handlers self.conn.register_handle('CHANNEL_CREATE', self._on_channel_create) self.conn.register_handle('CHANNEL_ANSWER', self._on_channel_answer) self.conn.register_handle('CHANNEL_HANGUP_COMPLETE', self._on_channel_hangup) self.conn.register_handle('DTMF', self._on_dtmf) def _on_channel_create(self, event): """Called when a new channel is created.""" uuid = event.headers.get('Unique-ID', 'unknown') caller = event.headers.get('Caller-Caller-ID-Number', 'unknown') dest = event.headers.get('Caller-Destination-Number', 'unknown') direction = event.headers.get('Call-Direction', 'unknown') self.active_calls[uuid] = { 'caller': caller, 'destination': dest, 'direction': direction, 'created': datetime.now(), 'state': 'ringing' } print(f"[{self._now()}] NEW CALL: {caller} -> {dest} ({direction}) UUID={uuid}") print(f" Active calls: {len(self.active_calls)}") def _on_channel_answer(self, event): """Called when a channel is answered.""" uuid = event.headers.get('Unique-ID', 'unknown') if uuid in self.active_calls: self.active_calls[uuid]['state'] = 'answered' caller = self.active_calls[uuid]['caller'] dest = self.active_calls[uuid]['destination'] print(f"[{self._now()}] ANSWERED: {caller} -> {dest} UUID={uuid}") def _on_channel_hangup(self, event): """Called when a channel hangs up.""" uuid = event.headers.get('Unique-ID', 'unknown') cause = event.headers.get('Hangup-Cause', 'unknown') duration = event.headers.get('variable_billsec', '0') if uuid in self.active_calls: call = self.active_calls.pop(uuid) print( f"[{self._now()}] HANGUP: {call['caller']} -> {call['destination']} " f"Duration={duration}s Cause={cause} UUID={uuid}" ) print(f" Active calls: {len(self.active_calls)}") def _on_dtmf(self, event): """Called when DTMF is received.""" uuid = event.headers.get('Unique-ID', 'unknown') digit = event.headers.get('DTMF-Digit', '?') print(f"[{self._now()}] DTMF: digit={digit} UUID={uuid}") def run_api(self, command): """Execute an API command and return the result.""" result = self.conn.send(f'api {command}') return result.data if result else None def originate_call(self, endpoint, app='&park()'): """Place a new call.""" cmd = f'api originate {endpoint} {app}' result = self.conn.send(cmd) print(f"[{self._now()}] Originate result: {result.data if result else 'failed'}") return result def _now(self): return datetime.now().strftime('%Y-%m-%d %H:%M:%S') def run(self): """Main event loop.""" self.connect() self.subscribe_events() print(f"[{self._now()}] Listening for events... (Ctrl+C to stop)") # Get initial status status = self.run_api('status') if status: print(f"\n--- FreeSWITCH Status ---\n{status}\n") try: # This blocks and processes events self.conn.process_events() except KeyboardInterrupt: print(f"\n[{self._now()}] Shutting down...") if __name__ == '__main__': monitor = FreeSWITCHMonitor( host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD' ) monitor.run()
#!/usr/bin/env python3
"""
freeswitch_monitor.py
Monitor FreeSWITCH events via ESL (inbound mode).
Logs call activity and tracks concurrent calls.
""" import greenswitch
import json
from datetime import datetime class FreeSWITCHMonitor: def __init__(self, host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD'): self.host = host self.port = port self.password = password self.active_calls = {} self.conn = None def connect(self): """Establish ESL connection.""" self.conn = greenswitch.InboundESL( host=self.host, port=self.port, password=self.password ) self.conn.connect() print(f"[{self._now()}] Connected to FreeSWITCH at {self.host}:{self.port}") def subscribe_events(self): """Subscribe to call-related events.""" # Subscribe to specific events self.conn.send( 'event plain CHANNEL_CREATE CHANNEL_ANSWER ' 'CHANNEL_HANGUP_COMPLETE DTMF CODEC' ) # Register event handlers self.conn.register_handle('CHANNEL_CREATE', self._on_channel_create) self.conn.register_handle('CHANNEL_ANSWER', self._on_channel_answer) self.conn.register_handle('CHANNEL_HANGUP_COMPLETE', self._on_channel_hangup) self.conn.register_handle('DTMF', self._on_dtmf) def _on_channel_create(self, event): """Called when a new channel is created.""" uuid = event.headers.get('Unique-ID', 'unknown') caller = event.headers.get('Caller-Caller-ID-Number', 'unknown') dest = event.headers.get('Caller-Destination-Number', 'unknown') direction = event.headers.get('Call-Direction', 'unknown') self.active_calls[uuid] = { 'caller': caller, 'destination': dest, 'direction': direction, 'created': datetime.now(), 'state': 'ringing' } print(f"[{self._now()}] NEW CALL: {caller} -> {dest} ({direction}) UUID={uuid}") print(f" Active calls: {len(self.active_calls)}") def _on_channel_answer(self, event): """Called when a channel is answered.""" uuid = event.headers.get('Unique-ID', 'unknown') if uuid in self.active_calls: self.active_calls[uuid]['state'] = 'answered' caller = self.active_calls[uuid]['caller'] dest = self.active_calls[uuid]['destination'] print(f"[{self._now()}] ANSWERED: {caller} -> {dest} UUID={uuid}") def _on_channel_hangup(self, event): """Called when a channel hangs up.""" uuid = event.headers.get('Unique-ID', 'unknown') cause = event.headers.get('Hangup-Cause', 'unknown') duration = event.headers.get('variable_billsec', '0') if uuid in self.active_calls: call = self.active_calls.pop(uuid) print( f"[{self._now()}] HANGUP: {call['caller']} -> {call['destination']} " f"Duration={duration}s Cause={cause} UUID={uuid}" ) print(f" Active calls: {len(self.active_calls)}") def _on_dtmf(self, event): """Called when DTMF is received.""" uuid = event.headers.get('Unique-ID', 'unknown') digit = event.headers.get('DTMF-Digit', '?') print(f"[{self._now()}] DTMF: digit={digit} UUID={uuid}") def run_api(self, command): """Execute an API command and return the result.""" result = self.conn.send(f'api {command}') return result.data if result else None def originate_call(self, endpoint, app='&park()'): """Place a new call.""" cmd = f'api originate {endpoint} {app}' result = self.conn.send(cmd) print(f"[{self._now()}] Originate result: {result.data if result else 'failed'}") return result def _now(self): return datetime.now().strftime('%Y-%m-%d %H:%M:%S') def run(self): """Main event loop.""" self.connect() self.subscribe_events() print(f"[{self._now()}] Listening for events... (Ctrl+C to stop)") # Get initial status status = self.run_api('status') if status: print(f"\n--- FreeSWITCH Status ---\n{status}\n") try: # This blocks and processes events self.conn.process_events() except KeyboardInterrupt: print(f"\n[{self._now()}] Shutting down...") if __name__ == '__main__': monitor = FreeSWITCHMonitor( host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD' ) monitor.run()
python3 freeswitch_monitor.py
python3 freeswitch_monitor.py
python3 freeswitch_monitor.py
#!/usr/bin/env python3
"""
call_queue.py
Simple call queue: callers hear hold music, agents dial in to take the next call.
Uses ESL inbound mode.
""" import greenswitch
from collections import deque
from datetime import datetime
import threading class CallQueue: def __init__(self, host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD'): self.conn = greenswitch.InboundESL(host=host, port=port, password=password) self.waiting_callers = deque() # Queue of caller UUIDs self.available_agents = [] # List of agent UUIDs self.lock = threading.Lock() def connect(self): self.conn.connect() self.conn.send('event plain CHANNEL_ANSWER CHANNEL_HANGUP_COMPLETE') print(f"[{self._now()}] Queue system connected") def add_caller(self, uuid): """Add a caller to the queue and play hold music.""" with self.lock: self.waiting_callers.append(uuid) # Play music on hold to the caller self.conn.send( f'api uuid_broadcast {uuid} ' f'local_stream://moh both' ) print(f"[{self._now()}] Caller {uuid} added to queue. " f"Queue depth: {len(self.waiting_callers)}") self._try_connect() def add_agent(self, uuid): """Register an agent as available.""" with self.lock: self.available_agents.append(uuid) print(f"[{self._now()}] Agent {uuid} available. " f"Agents: {len(self.available_agents)}") self._try_connect() def _try_connect(self): """Try to bridge a waiting caller with an available agent.""" if self.waiting_callers and self.available_agents: caller_uuid = self.waiting_callers.popleft() agent_uuid = self.available_agents.pop(0) # Bridge the two calls self.conn.send( f'api uuid_bridge {caller_uuid} {agent_uuid}' ) print( f"[{self._now()}] CONNECTED: caller={caller_uuid} " f"agent={agent_uuid}" ) def get_stats(self): """Return current queue statistics.""" with self.lock: return { 'waiting_callers': len(self.waiting_callers), 'available_agents': len(self.available_agents), 'timestamp': self._now() } def _now(self): return datetime.now().strftime('%Y-%m-%d %H:%M:%S') if __name__ == '__main__': queue = CallQueue() queue.connect() # In production, callers and agents would be added via # dialplan outbound ESL or API calls print("Queue system running. Use ESL commands to add callers/agents.")
#!/usr/bin/env python3
"""
call_queue.py
Simple call queue: callers hear hold music, agents dial in to take the next call.
Uses ESL inbound mode.
""" import greenswitch
from collections import deque
from datetime import datetime
import threading class CallQueue: def __init__(self, host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD'): self.conn = greenswitch.InboundESL(host=host, port=port, password=password) self.waiting_callers = deque() # Queue of caller UUIDs self.available_agents = [] # List of agent UUIDs self.lock = threading.Lock() def connect(self): self.conn.connect() self.conn.send('event plain CHANNEL_ANSWER CHANNEL_HANGUP_COMPLETE') print(f"[{self._now()}] Queue system connected") def add_caller(self, uuid): """Add a caller to the queue and play hold music.""" with self.lock: self.waiting_callers.append(uuid) # Play music on hold to the caller self.conn.send( f'api uuid_broadcast {uuid} ' f'local_stream://moh both' ) print(f"[{self._now()}] Caller {uuid} added to queue. " f"Queue depth: {len(self.waiting_callers)}") self._try_connect() def add_agent(self, uuid): """Register an agent as available.""" with self.lock: self.available_agents.append(uuid) print(f"[{self._now()}] Agent {uuid} available. " f"Agents: {len(self.available_agents)}") self._try_connect() def _try_connect(self): """Try to bridge a waiting caller with an available agent.""" if self.waiting_callers and self.available_agents: caller_uuid = self.waiting_callers.popleft() agent_uuid = self.available_agents.pop(0) # Bridge the two calls self.conn.send( f'api uuid_bridge {caller_uuid} {agent_uuid}' ) print( f"[{self._now()}] CONNECTED: caller={caller_uuid} " f"agent={agent_uuid}" ) def get_stats(self): """Return current queue statistics.""" with self.lock: return { 'waiting_callers': len(self.waiting_callers), 'available_agents': len(self.available_agents), 'timestamp': self._now() } def _now(self): return datetime.now().strftime('%Y-%m-%d %H:%M:%S') if __name__ == '__main__': queue = CallQueue() queue.connect() # In production, callers and agents would be added via # dialplan outbound ESL or API calls print("Queue system running. Use ESL commands to add callers/agents.")
#!/usr/bin/env python3
"""
call_queue.py
Simple call queue: callers hear hold music, agents dial in to take the next call.
Uses ESL inbound mode.
""" import greenswitch
from collections import deque
from datetime import datetime
import threading class CallQueue: def __init__(self, host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD'): self.conn = greenswitch.InboundESL(host=host, port=port, password=password) self.waiting_callers = deque() # Queue of caller UUIDs self.available_agents = [] # List of agent UUIDs self.lock = threading.Lock() def connect(self): self.conn.connect() self.conn.send('event plain CHANNEL_ANSWER CHANNEL_HANGUP_COMPLETE') print(f"[{self._now()}] Queue system connected") def add_caller(self, uuid): """Add a caller to the queue and play hold music.""" with self.lock: self.waiting_callers.append(uuid) # Play music on hold to the caller self.conn.send( f'api uuid_broadcast {uuid} ' f'local_stream://moh both' ) print(f"[{self._now()}] Caller {uuid} added to queue. " f"Queue depth: {len(self.waiting_callers)}") self._try_connect() def add_agent(self, uuid): """Register an agent as available.""" with self.lock: self.available_agents.append(uuid) print(f"[{self._now()}] Agent {uuid} available. " f"Agents: {len(self.available_agents)}") self._try_connect() def _try_connect(self): """Try to bridge a waiting caller with an available agent.""" if self.waiting_callers and self.available_agents: caller_uuid = self.waiting_callers.popleft() agent_uuid = self.available_agents.pop(0) # Bridge the two calls self.conn.send( f'api uuid_bridge {caller_uuid} {agent_uuid}' ) print( f"[{self._now()}] CONNECTED: caller={caller_uuid} " f"agent={agent_uuid}" ) def get_stats(self): """Return current queue statistics.""" with self.lock: return { 'waiting_callers': len(self.waiting_callers), 'available_agents': len(self.available_agents), 'timestamp': self._now() } def _now(self): return datetime.now().strftime('%Y-%m-%d %H:%M:%S') if __name__ == '__main__': queue = CallQueue() queue.connect() # In production, callers and agents would be added via # dialplan outbound ESL or API calls print("Queue system running. Use ESL commands to add callers/agents.")
npm install modesl
npm install modesl
npm install modesl
// freeswitch_monitor.js
// FreeSWITCH event monitor using Node.js + modesl const esl = require('modesl'); const connection = new esl.Connection('127.0.0.1', 8021, 'YOUR_SECURE_ESL_PASSWORD', () => { console.log(`[${timestamp()}] Connected to FreeSWITCH via ESL`); // Get initial status connection.api('status', (result) => { console.log(`\n--- FreeSWITCH Status ---\n${result.getBody()}\n`); }); // Subscribe to events connection.subscribe([ 'CHANNEL_CREATE', 'CHANNEL_ANSWER', 'CHANNEL_HANGUP_COMPLETE' ], () => { console.log(`[${timestamp()}] Subscribed to call events`); });
}); // Track active calls
const activeCalls = new Map(); connection.on('esl::event::CHANNEL_CREATE::*', (event) => { const uuid = event.getHeader('Unique-ID'); const caller = event.getHeader('Caller-Caller-ID-Number') || 'unknown'; const dest = event.getHeader('Caller-Destination-Number') || 'unknown'; const direction = event.getHeader('Call-Direction') || 'unknown'; activeCalls.set(uuid, { caller, dest, direction, created: new Date() }); console.log(`[${timestamp()}] NEW: ${caller} -> ${dest} (${direction}) [${activeCalls.size} active]`);
}); connection.on('esl::event::CHANNEL_ANSWER::*', (event) => { const uuid = event.getHeader('Unique-ID'); const call = activeCalls.get(uuid); if (call) { console.log(`[${timestamp()}] ANSWER: ${call.caller} -> ${call.dest}`); }
}); connection.on('esl::event::CHANNEL_HANGUP_COMPLETE::*', (event) => { const uuid = event.getHeader('Unique-ID'); const cause = event.getHeader('Hangup-Cause') || 'unknown'; const duration = event.getHeader('variable_billsec') || '0'; const call = activeCalls.get(uuid); if (call) { activeCalls.delete(uuid); console.log( `[${timestamp()}] HANGUP: ${call.caller} -> ${call.dest} ` + `Duration=${duration}s Cause=${cause} [${activeCalls.size} active]` ); }
}); connection.on('error', (error) => { console.error(`[${timestamp()}] ESL Error:`, error);
}); function timestamp() { return new Date().toISOString().replace('T', ' ').substring(0, 19);
} // Originate a call example (uncomment to use):
// connection.api('originate user/1001 &echo()', (result) => {
// console.log('Originate result:', result.getBody());
// });
// freeswitch_monitor.js
// FreeSWITCH event monitor using Node.js + modesl const esl = require('modesl'); const connection = new esl.Connection('127.0.0.1', 8021, 'YOUR_SECURE_ESL_PASSWORD', () => { console.log(`[${timestamp()}] Connected to FreeSWITCH via ESL`); // Get initial status connection.api('status', (result) => { console.log(`\n--- FreeSWITCH Status ---\n${result.getBody()}\n`); }); // Subscribe to events connection.subscribe([ 'CHANNEL_CREATE', 'CHANNEL_ANSWER', 'CHANNEL_HANGUP_COMPLETE' ], () => { console.log(`[${timestamp()}] Subscribed to call events`); });
}); // Track active calls
const activeCalls = new Map(); connection.on('esl::event::CHANNEL_CREATE::*', (event) => { const uuid = event.getHeader('Unique-ID'); const caller = event.getHeader('Caller-Caller-ID-Number') || 'unknown'; const dest = event.getHeader('Caller-Destination-Number') || 'unknown'; const direction = event.getHeader('Call-Direction') || 'unknown'; activeCalls.set(uuid, { caller, dest, direction, created: new Date() }); console.log(`[${timestamp()}] NEW: ${caller} -> ${dest} (${direction}) [${activeCalls.size} active]`);
}); connection.on('esl::event::CHANNEL_ANSWER::*', (event) => { const uuid = event.getHeader('Unique-ID'); const call = activeCalls.get(uuid); if (call) { console.log(`[${timestamp()}] ANSWER: ${call.caller} -> ${call.dest}`); }
}); connection.on('esl::event::CHANNEL_HANGUP_COMPLETE::*', (event) => { const uuid = event.getHeader('Unique-ID'); const cause = event.getHeader('Hangup-Cause') || 'unknown'; const duration = event.getHeader('variable_billsec') || '0'; const call = activeCalls.get(uuid); if (call) { activeCalls.delete(uuid); console.log( `[${timestamp()}] HANGUP: ${call.caller} -> ${call.dest} ` + `Duration=${duration}s Cause=${cause} [${activeCalls.size} active]` ); }
}); connection.on('error', (error) => { console.error(`[${timestamp()}] ESL Error:`, error);
}); function timestamp() { return new Date().toISOString().replace('T', ' ').substring(0, 19);
} // Originate a call example (uncomment to use):
// connection.api('originate user/1001 &echo()', (result) => {
// console.log('Originate result:', result.getBody());
// });
// freeswitch_monitor.js
// FreeSWITCH event monitor using Node.js + modesl const esl = require('modesl'); const connection = new esl.Connection('127.0.0.1', 8021, 'YOUR_SECURE_ESL_PASSWORD', () => { console.log(`[${timestamp()}] Connected to FreeSWITCH via ESL`); // Get initial status connection.api('status', (result) => { console.log(`\n--- FreeSWITCH Status ---\n${result.getBody()}\n`); }); // Subscribe to events connection.subscribe([ 'CHANNEL_CREATE', 'CHANNEL_ANSWER', 'CHANNEL_HANGUP_COMPLETE' ], () => { console.log(`[${timestamp()}] Subscribed to call events`); });
}); // Track active calls
const activeCalls = new Map(); connection.on('esl::event::CHANNEL_CREATE::*', (event) => { const uuid = event.getHeader('Unique-ID'); const caller = event.getHeader('Caller-Caller-ID-Number') || 'unknown'; const dest = event.getHeader('Caller-Destination-Number') || 'unknown'; const direction = event.getHeader('Call-Direction') || 'unknown'; activeCalls.set(uuid, { caller, dest, direction, created: new Date() }); console.log(`[${timestamp()}] NEW: ${caller} -> ${dest} (${direction}) [${activeCalls.size} active]`);
}); connection.on('esl::event::CHANNEL_ANSWER::*', (event) => { const uuid = event.getHeader('Unique-ID'); const call = activeCalls.get(uuid); if (call) { console.log(`[${timestamp()}] ANSWER: ${call.caller} -> ${call.dest}`); }
}); connection.on('esl::event::CHANNEL_HANGUP_COMPLETE::*', (event) => { const uuid = event.getHeader('Unique-ID'); const cause = event.getHeader('Hangup-Cause') || 'unknown'; const duration = event.getHeader('variable_billsec') || '0'; const call = activeCalls.get(uuid); if (call) { activeCalls.delete(uuid); console.log( `[${timestamp()}] HANGUP: ${call.caller} -> ${call.dest} ` + `Duration=${duration}s Cause=${cause} [${activeCalls.size} active]` ); }
}); connection.on('error', (error) => { console.error(`[${timestamp()}] ESL Error:`, error);
}); function timestamp() { return new Date().toISOString().replace('T', ' ').substring(0, 19);
} // Originate a call example (uncomment to use):
// connection.api('originate user/1001 &echo()', (result) => {
// console.log('Originate result:', result.getBody());
// });
node freeswitch_monitor.js
node freeswitch_monitor.js
node freeswitch_monitor.js
<extension name="esl_controlled_ivr"> <condition field="destination_number" expression="^(7000)$"> <action application="socket" data="127.0.0.1:9090 async full"/> </condition>
</extension>
<extension name="esl_controlled_ivr"> <condition field="destination_number" expression="^(7000)$"> <action application="socket" data="127.0.0.1:9090 async full"/> </condition>
</extension>
<extension name="esl_controlled_ivr"> <condition field="destination_number" expression="^(7000)$"> <action application="socket" data="127.0.0.1:9090 async full"/> </condition>
</extension>
#!/usr/bin/env python3
"""
outbound_ivr.py
Simple outbound ESL IVR server.
FreeSWITCH connects to this app for call handling.
""" import socket
import threading def handle_call(client_socket, addr): """Handle one incoming ESL connection (one call).""" print(f"New call connection from {addr}") # Read the initial connect message data = client_socket.recv(65536).decode() # Send connect command client_socket.sendall(b'connect\n\n') data = client_socket.recv(65536).decode() # Parse caller info from headers headers = {} for line in data.split('\n'): if ':' in line: key, value = line.split(':', 1) headers[key.strip()] = value.strip() caller = headers.get('Caller-Caller-ID-Number', 'unknown') dest = headers.get('Caller-Destination-Number', 'unknown') uuid = headers.get('Unique-ID', 'unknown') print(f"Call from {caller} to {dest} (UUID: {uuid})") # Answer the call send_command(client_socket, 'answer') # Play a greeting send_command(client_socket, 'playback /var/lib/freeswitch/sounds/ivr/ivr-welcome.wav') # Collect digits send_command( client_socket, 'play_and_get_digits 1 1 3 10000 # ' '/var/lib/freeswitch/sounds/ivr/ivr-please_make_selection.wav ' '/var/lib/freeswitch/sounds/ivr/ivr-that_was_an_invalid_entry.wav ' 'selection \\d 10000' ) # Read the response to get the collected digit # (In production, parse CHANNEL_EXECUTE_COMPLETE events) # Transfer based on selection send_command(client_socket, 'transfer 1001 XML default') client_socket.close() def send_command(sock, command): """Send an ESL command.""" msg = f'sendmsg\ncall-command: execute\nexecute-app-name: {command}\n\n' sock.sendall(msg.encode()) # Read response try: return sock.recv(65536).decode() except Exception: return '' def main(): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(('127.0.0.1', 9090)) server.listen(50) print("Outbound ESL server listening on 127.0.0.1:9090") while True: client, addr = server.accept() thread = threading.Thread(target=handle_call, args=(client, addr)) thread.daemon = True thread.start() if __name__ == '__main__': main()
#!/usr/bin/env python3
"""
outbound_ivr.py
Simple outbound ESL IVR server.
FreeSWITCH connects to this app for call handling.
""" import socket
import threading def handle_call(client_socket, addr): """Handle one incoming ESL connection (one call).""" print(f"New call connection from {addr}") # Read the initial connect message data = client_socket.recv(65536).decode() # Send connect command client_socket.sendall(b'connect\n\n') data = client_socket.recv(65536).decode() # Parse caller info from headers headers = {} for line in data.split('\n'): if ':' in line: key, value = line.split(':', 1) headers[key.strip()] = value.strip() caller = headers.get('Caller-Caller-ID-Number', 'unknown') dest = headers.get('Caller-Destination-Number', 'unknown') uuid = headers.get('Unique-ID', 'unknown') print(f"Call from {caller} to {dest} (UUID: {uuid})") # Answer the call send_command(client_socket, 'answer') # Play a greeting send_command(client_socket, 'playback /var/lib/freeswitch/sounds/ivr/ivr-welcome.wav') # Collect digits send_command( client_socket, 'play_and_get_digits 1 1 3 10000 # ' '/var/lib/freeswitch/sounds/ivr/ivr-please_make_selection.wav ' '/var/lib/freeswitch/sounds/ivr/ivr-that_was_an_invalid_entry.wav ' 'selection \\d 10000' ) # Read the response to get the collected digit # (In production, parse CHANNEL_EXECUTE_COMPLETE events) # Transfer based on selection send_command(client_socket, 'transfer 1001 XML default') client_socket.close() def send_command(sock, command): """Send an ESL command.""" msg = f'sendmsg\ncall-command: execute\nexecute-app-name: {command}\n\n' sock.sendall(msg.encode()) # Read response try: return sock.recv(65536).decode() except Exception: return '' def main(): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(('127.0.0.1', 9090)) server.listen(50) print("Outbound ESL server listening on 127.0.0.1:9090") while True: client, addr = server.accept() thread = threading.Thread(target=handle_call, args=(client, addr)) thread.daemon = True thread.start() if __name__ == '__main__': main()
#!/usr/bin/env python3
"""
outbound_ivr.py
Simple outbound ESL IVR server.
FreeSWITCH connects to this app for call handling.
""" import socket
import threading def handle_call(client_socket, addr): """Handle one incoming ESL connection (one call).""" print(f"New call connection from {addr}") # Read the initial connect message data = client_socket.recv(65536).decode() # Send connect command client_socket.sendall(b'connect\n\n') data = client_socket.recv(65536).decode() # Parse caller info from headers headers = {} for line in data.split('\n'): if ':' in line: key, value = line.split(':', 1) headers[key.strip()] = value.strip() caller = headers.get('Caller-Caller-ID-Number', 'unknown') dest = headers.get('Caller-Destination-Number', 'unknown') uuid = headers.get('Unique-ID', 'unknown') print(f"Call from {caller} to {dest} (UUID: {uuid})") # Answer the call send_command(client_socket, 'answer') # Play a greeting send_command(client_socket, 'playback /var/lib/freeswitch/sounds/ivr/ivr-welcome.wav') # Collect digits send_command( client_socket, 'play_and_get_digits 1 1 3 10000 # ' '/var/lib/freeswitch/sounds/ivr/ivr-please_make_selection.wav ' '/var/lib/freeswitch/sounds/ivr/ivr-that_was_an_invalid_entry.wav ' 'selection \\d 10000' ) # Read the response to get the collected digit # (In production, parse CHANNEL_EXECUTE_COMPLETE events) # Transfer based on selection send_command(client_socket, 'transfer 1001 XML default') client_socket.close() def send_command(sock, command): """Send an ESL command.""" msg = f'sendmsg\ncall-command: execute\nexecute-app-name: {command}\n\n' sock.sendall(msg.encode()) # Read response try: return sock.recv(65536).decode() except Exception: return '' def main(): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(('127.0.0.1', 9090)) server.listen(50) print("Outbound ESL server listening on 127.0.0.1:9090") while True: client, addr = server.accept() thread = threading.Thread(target=handle_call, args=(client, addr)) thread.daemon = True thread.start() if __name__ == '__main__': main()
<configuration name="cdr_csv.conf" description="CDR CSV Format"> <settings> <!-- Master CSV file for all calls --> <param name="default-template" value="default"/> <!-- Rotate log file when it reaches this size (bytes) --> <param name="rotate-on-hup" value="true"/> <!-- Legs: a-leg only, b-leg only, or both --> <param name="legs" value="a"/> </settings> <templates> <!-- Default template — one line per call --> <template name="default">"${caller_id_name}","${caller_id_number}","${destination_number}","${context}","${start_stamp}","${answer_stamp}","${end_stamp}","${duration}","${billsec}","${hangup_cause}","${uuid}","${accountcode}","${read_codec}","${write_codec}","${sip_hangup_disposition}","${sip_from_uri}","${sip_to_uri}"</template> <!-- Detailed template with more fields --> <template name="detailed">"${caller_id_name}","${caller_id_number}","${destination_number}","${context}","${start_stamp}","${answer_stamp}","${end_stamp}","${progress_stamp}","${progress_media_stamp}","${duration}","${billsec}","${hangup_cause}","${uuid}","${bleg_uuid}","${accountcode}","${read_codec}","${write_codec}","${sip_hangup_disposition}","${endpoint_disposition}","${sip_from_uri}","${sip_to_uri}","${sip_call_id}","${network_addr}","${bridge_channel}","${last_app}","${last_arg}"</template> </templates>
</configuration>
<configuration name="cdr_csv.conf" description="CDR CSV Format"> <settings> <!-- Master CSV file for all calls --> <param name="default-template" value="default"/> <!-- Rotate log file when it reaches this size (bytes) --> <param name="rotate-on-hup" value="true"/> <!-- Legs: a-leg only, b-leg only, or both --> <param name="legs" value="a"/> </settings> <templates> <!-- Default template — one line per call --> <template name="default">"${caller_id_name}","${caller_id_number}","${destination_number}","${context}","${start_stamp}","${answer_stamp}","${end_stamp}","${duration}","${billsec}","${hangup_cause}","${uuid}","${accountcode}","${read_codec}","${write_codec}","${sip_hangup_disposition}","${sip_from_uri}","${sip_to_uri}"</template> <!-- Detailed template with more fields --> <template name="detailed">"${caller_id_name}","${caller_id_number}","${destination_number}","${context}","${start_stamp}","${answer_stamp}","${end_stamp}","${progress_stamp}","${progress_media_stamp}","${duration}","${billsec}","${hangup_cause}","${uuid}","${bleg_uuid}","${accountcode}","${read_codec}","${write_codec}","${sip_hangup_disposition}","${endpoint_disposition}","${sip_from_uri}","${sip_to_uri}","${sip_call_id}","${network_addr}","${bridge_channel}","${last_app}","${last_arg}"</template> </templates>
</configuration>
<configuration name="cdr_csv.conf" description="CDR CSV Format"> <settings> <!-- Master CSV file for all calls --> <param name="default-template" value="default"/> <!-- Rotate log file when it reaches this size (bytes) --> <param name="rotate-on-hup" value="true"/> <!-- Legs: a-leg only, b-leg only, or both --> <param name="legs" value="a"/> </settings> <templates> <!-- Default template — one line per call --> <template name="default">"${caller_id_name}","${caller_id_number}","${destination_number}","${context}","${start_stamp}","${answer_stamp}","${end_stamp}","${duration}","${billsec}","${hangup_cause}","${uuid}","${accountcode}","${read_codec}","${write_codec}","${sip_hangup_disposition}","${sip_from_uri}","${sip_to_uri}"</template> <!-- Detailed template with more fields --> <template name="detailed">"${caller_id_name}","${caller_id_number}","${destination_number}","${context}","${start_stamp}","${answer_stamp}","${end_stamp}","${progress_stamp}","${progress_media_stamp}","${duration}","${billsec}","${hangup_cause}","${uuid}","${bleg_uuid}","${accountcode}","${read_codec}","${write_codec}","${sip_hangup_disposition}","${endpoint_disposition}","${sip_from_uri}","${sip_to_uri}","${sip_call_id}","${network_addr}","${bridge_channel}","${last_app}","${last_arg}"</template> </templates>
</configuration>
<configuration name="cdr_sqlite.conf" description="CDR SQLite"> <settings> <param name="db-name" value="cdr"/> <param name="db-table" value="cdr"/> <param name="legs" value="a"/> <param name="default-template" value="default"/> </settings> <templates> <template name="default"> INSERT INTO cdr ( caller_id_name, caller_id_number, destination_number, context, start_stamp, answer_stamp, end_stamp, duration, billsec, hangup_cause, uuid, accountcode, read_codec, write_codec, sip_hangup_disposition, network_addr ) VALUES ( '${caller_id_name}', '${caller_id_number}', '${destination_number}', '${context}', '${start_stamp}', '${answer_stamp}', '${end_stamp}', '${duration}', '${billsec}', '${hangup_cause}', '${uuid}', '${accountcode}', '${read_codec}', '${write_codec}', '${sip_hangup_disposition}', '${network_addr}' ); </template> </templates>
</configuration>
<configuration name="cdr_sqlite.conf" description="CDR SQLite"> <settings> <param name="db-name" value="cdr"/> <param name="db-table" value="cdr"/> <param name="legs" value="a"/> <param name="default-template" value="default"/> </settings> <templates> <template name="default"> INSERT INTO cdr ( caller_id_name, caller_id_number, destination_number, context, start_stamp, answer_stamp, end_stamp, duration, billsec, hangup_cause, uuid, accountcode, read_codec, write_codec, sip_hangup_disposition, network_addr ) VALUES ( '${caller_id_name}', '${caller_id_number}', '${destination_number}', '${context}', '${start_stamp}', '${answer_stamp}', '${end_stamp}', '${duration}', '${billsec}', '${hangup_cause}', '${uuid}', '${accountcode}', '${read_codec}', '${write_codec}', '${sip_hangup_disposition}', '${network_addr}' ); </template> </templates>
</configuration>
<configuration name="cdr_sqlite.conf" description="CDR SQLite"> <settings> <param name="db-name" value="cdr"/> <param name="db-table" value="cdr"/> <param name="legs" value="a"/> <param name="default-template" value="default"/> </settings> <templates> <template name="default"> INSERT INTO cdr ( caller_id_name, caller_id_number, destination_number, context, start_stamp, answer_stamp, end_stamp, duration, billsec, hangup_cause, uuid, accountcode, read_codec, write_codec, sip_hangup_disposition, network_addr ) VALUES ( '${caller_id_name}', '${caller_id_number}', '${destination_number}', '${context}', '${start_stamp}', '${answer_stamp}', '${end_stamp}', '${duration}', '${billsec}', '${hangup_cause}', '${uuid}', '${accountcode}', '${read_codec}', '${write_codec}', '${sip_hangup_disposition}', '${network_addr}' ); </template> </templates>
</configuration>
sqlite3 /var/lib/freeswitch/db/cdr.db -- Show recent calls
SELECT caller_id_number, destination_number, duration, hangup_cause, start_stamp
FROM cdr
ORDER BY start_stamp DESC
LIMIT 20; -- Call volume by hour
SELECT strftime('%H', start_stamp) AS hour, COUNT(*) AS calls
FROM cdr
WHERE date(start_stamp) = date('now')
GROUP BY hour
ORDER BY hour; -- Average call duration by destination
SELECT destination_number, COUNT(*) AS calls, AVG(billsec) AS avg_duration, SUM(billsec) AS total_seconds
FROM cdr
WHERE billsec > 0
GROUP BY destination_number
ORDER BY calls DESC;
sqlite3 /var/lib/freeswitch/db/cdr.db -- Show recent calls
SELECT caller_id_number, destination_number, duration, hangup_cause, start_stamp
FROM cdr
ORDER BY start_stamp DESC
LIMIT 20; -- Call volume by hour
SELECT strftime('%H', start_stamp) AS hour, COUNT(*) AS calls
FROM cdr
WHERE date(start_stamp) = date('now')
GROUP BY hour
ORDER BY hour; -- Average call duration by destination
SELECT destination_number, COUNT(*) AS calls, AVG(billsec) AS avg_duration, SUM(billsec) AS total_seconds
FROM cdr
WHERE billsec > 0
GROUP BY destination_number
ORDER BY calls DESC;
sqlite3 /var/lib/freeswitch/db/cdr.db -- Show recent calls
SELECT caller_id_number, destination_number, duration, hangup_cause, start_stamp
FROM cdr
ORDER BY start_stamp DESC
LIMIT 20; -- Call volume by hour
SELECT strftime('%H', start_stamp) AS hour, COUNT(*) AS calls
FROM cdr
WHERE date(start_stamp) = date('now')
GROUP BY hour
ORDER BY hour; -- Average call duration by destination
SELECT destination_number, COUNT(*) AS calls, AVG(billsec) AS avg_duration, SUM(billsec) AS total_seconds
FROM cdr
WHERE billsec > 0
GROUP BY destination_number
ORDER BY calls DESC;
<configuration name="cdr_pg.conf" description="CDR PostgreSQL"> <settings> <param name="host" value="127.0.0.1"/> <param name="port" value="5432"/> <param name="dbname" value="freeswitch_cdr"/> <param name="user" value="freeswitch"/> <param name="password" value="YOUR_DB_PASSWORD"/> <param name="legs" value="a"/> <param name="default-template" value="default"/> <!-- Log errors to file if DB is unavailable --> <param name="spool-format" value="csv"/> <param name="log-dir" value="/var/log/freeswitch/cdr-pg-errors"/> </settings> <templates> <template name="default"> INSERT INTO cdr ( caller_id_name, caller_id_number, destination_number, context, start_stamp, answer_stamp, end_stamp, duration, billsec, hangup_cause, uuid, accountcode, read_codec, write_codec, sip_hangup_disposition, network_addr ) VALUES ( '${caller_id_name}', '${caller_id_number}', '${destination_number}', '${context}', to_timestamp(${start_epoch}), to_timestamp(${answer_epoch}), to_timestamp(${end_epoch}), ${duration}, ${billsec}, '${hangup_cause}', '${uuid}', '${accountcode}', '${read_codec}', '${write_codec}', '${sip_hangup_disposition}', '${network_addr}' ); </template> </templates>
</configuration>
<configuration name="cdr_pg.conf" description="CDR PostgreSQL"> <settings> <param name="host" value="127.0.0.1"/> <param name="port" value="5432"/> <param name="dbname" value="freeswitch_cdr"/> <param name="user" value="freeswitch"/> <param name="password" value="YOUR_DB_PASSWORD"/> <param name="legs" value="a"/> <param name="default-template" value="default"/> <!-- Log errors to file if DB is unavailable --> <param name="spool-format" value="csv"/> <param name="log-dir" value="/var/log/freeswitch/cdr-pg-errors"/> </settings> <templates> <template name="default"> INSERT INTO cdr ( caller_id_name, caller_id_number, destination_number, context, start_stamp, answer_stamp, end_stamp, duration, billsec, hangup_cause, uuid, accountcode, read_codec, write_codec, sip_hangup_disposition, network_addr ) VALUES ( '${caller_id_name}', '${caller_id_number}', '${destination_number}', '${context}', to_timestamp(${start_epoch}), to_timestamp(${answer_epoch}), to_timestamp(${end_epoch}), ${duration}, ${billsec}, '${hangup_cause}', '${uuid}', '${accountcode}', '${read_codec}', '${write_codec}', '${sip_hangup_disposition}', '${network_addr}' ); </template> </templates>
</configuration>
<configuration name="cdr_pg.conf" description="CDR PostgreSQL"> <settings> <param name="host" value="127.0.0.1"/> <param name="port" value="5432"/> <param name="dbname" value="freeswitch_cdr"/> <param name="user" value="freeswitch"/> <param name="password" value="YOUR_DB_PASSWORD"/> <param name="legs" value="a"/> <param name="default-template" value="default"/> <!-- Log errors to file if DB is unavailable --> <param name="spool-format" value="csv"/> <param name="log-dir" value="/var/log/freeswitch/cdr-pg-errors"/> </settings> <templates> <template name="default"> INSERT INTO cdr ( caller_id_name, caller_id_number, destination_number, context, start_stamp, answer_stamp, end_stamp, duration, billsec, hangup_cause, uuid, accountcode, read_codec, write_codec, sip_hangup_disposition, network_addr ) VALUES ( '${caller_id_name}', '${caller_id_number}', '${destination_number}', '${context}', to_timestamp(${start_epoch}), to_timestamp(${answer_epoch}), to_timestamp(${end_epoch}), ${duration}, ${billsec}, '${hangup_cause}', '${uuid}', '${accountcode}', '${read_codec}', '${write_codec}', '${sip_hangup_disposition}', '${network_addr}' ); </template> </templates>
</configuration>
-- Run as PostgreSQL superuser
CREATE DATABASE freeswitch_cdr;
CREATE USER freeswitch WITH PASSWORD 'YOUR_DB_PASSWORD';
GRANT ALL PRIVILEGES ON DATABASE freeswitch_cdr TO freeswitch; \c freeswitch_cdr CREATE TABLE cdr ( id SERIAL PRIMARY KEY, caller_id_name VARCHAR(128), caller_id_number VARCHAR(64), destination_number VARCHAR(64), context VARCHAR(64), start_stamp TIMESTAMP, answer_stamp TIMESTAMP, end_stamp TIMESTAMP, duration INTEGER, billsec INTEGER, hangup_cause VARCHAR(64), uuid VARCHAR(64) UNIQUE, accountcode VARCHAR(32), read_codec VARCHAR(32), write_codec VARCHAR(32), sip_hangup_disposition VARCHAR(32), network_addr VARCHAR(64), created_at TIMESTAMP DEFAULT NOW()
); -- Index for common queries
CREATE INDEX idx_cdr_start_stamp ON cdr (start_stamp);
CREATE INDEX idx_cdr_caller ON cdr (caller_id_number);
CREATE INDEX idx_cdr_destination ON cdr (destination_number);
CREATE INDEX idx_cdr_accountcode ON cdr (accountcode); GRANT ALL ON cdr TO freeswitch;
GRANT USAGE, SELECT ON SEQUENCE cdr_id_seq TO freeswitch;
-- Run as PostgreSQL superuser
CREATE DATABASE freeswitch_cdr;
CREATE USER freeswitch WITH PASSWORD 'YOUR_DB_PASSWORD';
GRANT ALL PRIVILEGES ON DATABASE freeswitch_cdr TO freeswitch; \c freeswitch_cdr CREATE TABLE cdr ( id SERIAL PRIMARY KEY, caller_id_name VARCHAR(128), caller_id_number VARCHAR(64), destination_number VARCHAR(64), context VARCHAR(64), start_stamp TIMESTAMP, answer_stamp TIMESTAMP, end_stamp TIMESTAMP, duration INTEGER, billsec INTEGER, hangup_cause VARCHAR(64), uuid VARCHAR(64) UNIQUE, accountcode VARCHAR(32), read_codec VARCHAR(32), write_codec VARCHAR(32), sip_hangup_disposition VARCHAR(32), network_addr VARCHAR(64), created_at TIMESTAMP DEFAULT NOW()
); -- Index for common queries
CREATE INDEX idx_cdr_start_stamp ON cdr (start_stamp);
CREATE INDEX idx_cdr_caller ON cdr (caller_id_number);
CREATE INDEX idx_cdr_destination ON cdr (destination_number);
CREATE INDEX idx_cdr_accountcode ON cdr (accountcode); GRANT ALL ON cdr TO freeswitch;
GRANT USAGE, SELECT ON SEQUENCE cdr_id_seq TO freeswitch;
-- Run as PostgreSQL superuser
CREATE DATABASE freeswitch_cdr;
CREATE USER freeswitch WITH PASSWORD 'YOUR_DB_PASSWORD';
GRANT ALL PRIVILEGES ON DATABASE freeswitch_cdr TO freeswitch; \c freeswitch_cdr CREATE TABLE cdr ( id SERIAL PRIMARY KEY, caller_id_name VARCHAR(128), caller_id_number VARCHAR(64), destination_number VARCHAR(64), context VARCHAR(64), start_stamp TIMESTAMP, answer_stamp TIMESTAMP, end_stamp TIMESTAMP, duration INTEGER, billsec INTEGER, hangup_cause VARCHAR(64), uuid VARCHAR(64) UNIQUE, accountcode VARCHAR(32), read_codec VARCHAR(32), write_codec VARCHAR(32), sip_hangup_disposition VARCHAR(32), network_addr VARCHAR(64), created_at TIMESTAMP DEFAULT NOW()
); -- Index for common queries
CREATE INDEX idx_cdr_start_stamp ON cdr (start_stamp);
CREATE INDEX idx_cdr_caller ON cdr (caller_id_number);
CREATE INDEX idx_cdr_destination ON cdr (destination_number);
CREATE INDEX idx_cdr_accountcode ON cdr (accountcode); GRANT ALL ON cdr TO freeswitch;
GRANT USAGE, SELECT ON SEQUENCE cdr_id_seq TO freeswitch;
<configuration name="logfile.conf" description="File Logging"> <settings> <param name="rotate-on-hup" value="true"/> </settings> <profiles> <profile name="default"> <settings> <param name="logfile" value="/var/log/freeswitch/freeswitch.log"/> <!-- Rotate every 10MB --> <param name="rollover" value="10485760"/> <!-- Log level: DEBUG, INFO, NOTICE, WARNING, ERR, CRIT, ALERT --> <param name="log-event" value="false"/> </settings> <mappings> <!-- What to log — set level per module --> <map name="all" value="info,warning,err,crit,alert"/> <!-- Enable debug for specific modules when troubleshooting --> <!-- <map name="mod_sofia" value="debug,info,warning,err"/> --> <!-- <map name="mod_dptools" value="debug,info,warning,err"/> --> </mappings> </profile> </profiles>
</configuration>
<configuration name="logfile.conf" description="File Logging"> <settings> <param name="rotate-on-hup" value="true"/> </settings> <profiles> <profile name="default"> <settings> <param name="logfile" value="/var/log/freeswitch/freeswitch.log"/> <!-- Rotate every 10MB --> <param name="rollover" value="10485760"/> <!-- Log level: DEBUG, INFO, NOTICE, WARNING, ERR, CRIT, ALERT --> <param name="log-event" value="false"/> </settings> <mappings> <!-- What to log — set level per module --> <map name="all" value="info,warning,err,crit,alert"/> <!-- Enable debug for specific modules when troubleshooting --> <!-- <map name="mod_sofia" value="debug,info,warning,err"/> --> <!-- <map name="mod_dptools" value="debug,info,warning,err"/> --> </mappings> </profile> </profiles>
</configuration>
<configuration name="logfile.conf" description="File Logging"> <settings> <param name="rotate-on-hup" value="true"/> </settings> <profiles> <profile name="default"> <settings> <param name="logfile" value="/var/log/freeswitch/freeswitch.log"/> <!-- Rotate every 10MB --> <param name="rollover" value="10485760"/> <!-- Log level: DEBUG, INFO, NOTICE, WARNING, ERR, CRIT, ALERT --> <param name="log-event" value="false"/> </settings> <mappings> <!-- What to log — set level per module --> <map name="all" value="info,warning,err,crit,alert"/> <!-- Enable debug for specific modules when troubleshooting --> <!-- <map name="mod_sofia" value="debug,info,warning,err"/> --> <!-- <map name="mod_dptools" value="debug,info,warning,err"/> --> </mappings> </profile> </profiles>
</configuration>
# Enable SIP trace (shows all SIP messages in fs_cli)
fs_cli -x "sofia profile internal siptrace on" # Disable
fs_cli -x "sofia profile internal siptrace off" # Enable on external profile
fs_cli -x "sofia profile external siptrace on" # Save SIP trace to a PCAP file
fs_cli -x "sofia profile internal capture on"
# File saved to /tmp/ by default
fs_cli -x "sofia profile internal capture off"
# Enable SIP trace (shows all SIP messages in fs_cli)
fs_cli -x "sofia profile internal siptrace on" # Disable
fs_cli -x "sofia profile internal siptrace off" # Enable on external profile
fs_cli -x "sofia profile external siptrace on" # Save SIP trace to a PCAP file
fs_cli -x "sofia profile internal capture on"
# File saved to /tmp/ by default
fs_cli -x "sofia profile internal capture off"
# Enable SIP trace (shows all SIP messages in fs_cli)
fs_cli -x "sofia profile internal siptrace on" # Disable
fs_cli -x "sofia profile internal siptrace off" # Enable on external profile
fs_cli -x "sofia profile external siptrace on" # Save SIP trace to a PCAP file
fs_cli -x "sofia profile internal capture on"
# File saved to /tmp/ by default
fs_cli -x "sofia profile internal capture off"
cat > /etc/logrotate.d/freeswitch << 'EOF'
/var/log/freeswitch/freeswitch.log { daily rotate 14 compress delaycompress missingok notifempty postrotate /usr/bin/fs_cli -x "fsctl send_sighup" > /dev/null 2>&1 || true endscript
} /var/log/freeswitch/cdr-csv/*.csv { daily rotate 30 compress delaycompress missingok notifempty create 640 freeswitch freeswitch
}
EOF
cat > /etc/logrotate.d/freeswitch << 'EOF'
/var/log/freeswitch/freeswitch.log { daily rotate 14 compress delaycompress missingok notifempty postrotate /usr/bin/fs_cli -x "fsctl send_sighup" > /dev/null 2>&1 || true endscript
} /var/log/freeswitch/cdr-csv/*.csv { daily rotate 30 compress delaycompress missingok notifempty create 640 freeswitch freeswitch
}
EOF
cat > /etc/logrotate.d/freeswitch << 'EOF'
/var/log/freeswitch/freeswitch.log { daily rotate 14 compress delaycompress missingok notifempty postrotate /usr/bin/fs_cli -x "fsctl send_sighup" > /dev/null 2>&1 || true endscript
} /var/log/freeswitch/cdr-csv/*.csv { daily rotate 30 compress delaycompress missingok notifempty create 640 freeswitch freeswitch
}
EOF
# 1. Extension passwords (vars.xml — default_password)
# Change from "1234" to something strong
# Then set individual passwords per extension in directory/*.xml # 2. ESL password (event_socket.conf.xml)
# Change from "ClueCon" # 3. Voicemail PINs (each user's vm-password)
# Change from extension number
# 1. Extension passwords (vars.xml — default_password)
# Change from "1234" to something strong
# Then set individual passwords per extension in directory/*.xml # 2. ESL password (event_socket.conf.xml)
# Change from "ClueCon" # 3. Voicemail PINs (each user's vm-password)
# Change from extension number
# 1. Extension passwords (vars.xml — default_password)
# Change from "1234" to something strong
# Then set individual passwords per extension in directory/*.xml # 2. ESL password (event_socket.conf.xml)
# Change from "ClueCon" # 3. Voicemail PINs (each user's vm-password)
# Change from extension number
<!-- In sip_profiles/internal.xml -->
<param name="apply-register-acl" value="trusted_networks"/>
<param name="apply-inbound-acl" value="trusted_networks"/>
<!-- In sip_profiles/internal.xml -->
<param name="apply-register-acl" value="trusted_networks"/>
<param name="apply-inbound-acl" value="trusted_networks"/>
<!-- In sip_profiles/internal.xml -->
<param name="apply-register-acl" value="trusted_networks"/>
<param name="apply-inbound-acl" value="trusted_networks"/>
<configuration name="acl.conf" description="Network Lists"> <network-lists> <!-- Trusted networks for SIP registration --> <list name="trusted_networks" default="deny"> <!-- Office network --> <node type="allow" cidr="10.0.0.0/8"/> <!-- VPN range --> <node type="allow" cidr="172.16.0.0/12"/> <!-- Specific remote office --> <node type="allow" cidr="203.0.113.50/32"/> <!-- Localhost --> <node type="allow" cidr="127.0.0.0/8"/> </list> <!-- SIP trunk provider IPs --> <list name="trunk_providers" default="deny"> <node type="allow" cidr="198.51.100.0/24"/> <node type="allow" cidr="203.0.113.100/32"/> </list> <!-- ESL access --> <list name="esl_access" default="deny"> <node type="allow" cidr="127.0.0.1/32"/> <node type="allow" cidr="10.0.0.0/8"/> </list> </network-lists>
</configuration>
<configuration name="acl.conf" description="Network Lists"> <network-lists> <!-- Trusted networks for SIP registration --> <list name="trusted_networks" default="deny"> <!-- Office network --> <node type="allow" cidr="10.0.0.0/8"/> <!-- VPN range --> <node type="allow" cidr="172.16.0.0/12"/> <!-- Specific remote office --> <node type="allow" cidr="203.0.113.50/32"/> <!-- Localhost --> <node type="allow" cidr="127.0.0.0/8"/> </list> <!-- SIP trunk provider IPs --> <list name="trunk_providers" default="deny"> <node type="allow" cidr="198.51.100.0/24"/> <node type="allow" cidr="203.0.113.100/32"/> </list> <!-- ESL access --> <list name="esl_access" default="deny"> <node type="allow" cidr="127.0.0.1/32"/> <node type="allow" cidr="10.0.0.0/8"/> </list> </network-lists>
</configuration>
<configuration name="acl.conf" description="Network Lists"> <network-lists> <!-- Trusted networks for SIP registration --> <list name="trusted_networks" default="deny"> <!-- Office network --> <node type="allow" cidr="10.0.0.0/8"/> <!-- VPN range --> <node type="allow" cidr="172.16.0.0/12"/> <!-- Specific remote office --> <node type="allow" cidr="203.0.113.50/32"/> <!-- Localhost --> <node type="allow" cidr="127.0.0.0/8"/> </list> <!-- SIP trunk provider IPs --> <list name="trunk_providers" default="deny"> <node type="allow" cidr="198.51.100.0/24"/> <node type="allow" cidr="203.0.113.100/32"/> </list> <!-- ESL access --> <list name="esl_access" default="deny"> <node type="allow" cidr="127.0.0.1/32"/> <node type="allow" cidr="10.0.0.0/8"/> </list> </network-lists>
</configuration>
apt-get install -y fail2ban
apt-get install -y fail2ban
apt-get install -y fail2ban
# /etc/fail2ban/filter.d/freeswitch.conf
[INCLUDES]
before = common.conf [Definition]
failregex = ^\s*\[WARNING\] sofia_reg\.c:\d+ SIP Registration Failed: IP=<HOST>.*$ ^\s*\[WARNING\] sofia_reg\.c:\d+.*auth challenge.*<HOST>.*$ ^\s*\[WARNING\].*REGISTER.*from.*<HOST>.*forbidden.*$ ^\s*\[WARNING\].*Authentication\s+Failed.*<HOST>.*$ ignoreregex =
# /etc/fail2ban/filter.d/freeswitch.conf
[INCLUDES]
before = common.conf [Definition]
failregex = ^\s*\[WARNING\] sofia_reg\.c:\d+ SIP Registration Failed: IP=<HOST>.*$ ^\s*\[WARNING\] sofia_reg\.c:\d+.*auth challenge.*<HOST>.*$ ^\s*\[WARNING\].*REGISTER.*from.*<HOST>.*forbidden.*$ ^\s*\[WARNING\].*Authentication\s+Failed.*<HOST>.*$ ignoreregex =
# /etc/fail2ban/filter.d/freeswitch.conf
[INCLUDES]
before = common.conf [Definition]
failregex = ^\s*\[WARNING\] sofia_reg\.c:\d+ SIP Registration Failed: IP=<HOST>.*$ ^\s*\[WARNING\] sofia_reg\.c:\d+.*auth challenge.*<HOST>.*$ ^\s*\[WARNING\].*REGISTER.*from.*<HOST>.*forbidden.*$ ^\s*\[WARNING\].*Authentication\s+Failed.*<HOST>.*$ ignoreregex =
# /etc/fail2ban/jail.d/freeswitch.conf
[freeswitch]
enabled = true
filter = freeswitch
logpath = /var/log/freeswitch/freeswitch.log
maxretry = 5
findtime = 300
bantime = 3600
action = iptables-allports[name=freeswitch, protocol=all]
# /etc/fail2ban/jail.d/freeswitch.conf
[freeswitch]
enabled = true
filter = freeswitch
logpath = /var/log/freeswitch/freeswitch.log
maxretry = 5
findtime = 300
bantime = 3600
action = iptables-allports[name=freeswitch, protocol=all]
# /etc/fail2ban/jail.d/freeswitch.conf
[freeswitch]
enabled = true
filter = freeswitch
logpath = /var/log/freeswitch/freeswitch.log
maxretry = 5
findtime = 300
bantime = 3600
action = iptables-allports[name=freeswitch, protocol=all]
systemctl enable fail2ban
systemctl restart fail2ban # Check status
fail2ban-client status freeswitch
systemctl enable fail2ban
systemctl restart fail2ban # Check status
fail2ban-client status freeswitch
systemctl enable fail2ban
systemctl restart fail2ban # Check status
fail2ban-client status freeswitch
# Generate a self-signed certificate (or use Let's Encrypt)
mkdir -p /etc/freeswitch/tls openssl req -x509 -nodes -days 3650 \ -newkey rsa:2048 \ -keyout /etc/freeswitch/tls/agent.pem \ -out /etc/freeswitch/tls/agent.pem \ -subj "/CN=YOUR_DOMAIN" # Combine into the format FreeSWITCH expects
cp /etc/freeswitch/tls/agent.pem /etc/freeswitch/tls/cafile.pem
chown freeswitch:freeswitch /etc/freeswitch/tls/*.pem
chmod 640 /etc/freeswitch/tls/*.pem
# Generate a self-signed certificate (or use Let's Encrypt)
mkdir -p /etc/freeswitch/tls openssl req -x509 -nodes -days 3650 \ -newkey rsa:2048 \ -keyout /etc/freeswitch/tls/agent.pem \ -out /etc/freeswitch/tls/agent.pem \ -subj "/CN=YOUR_DOMAIN" # Combine into the format FreeSWITCH expects
cp /etc/freeswitch/tls/agent.pem /etc/freeswitch/tls/cafile.pem
chown freeswitch:freeswitch /etc/freeswitch/tls/*.pem
chmod 640 /etc/freeswitch/tls/*.pem
# Generate a self-signed certificate (or use Let's Encrypt)
mkdir -p /etc/freeswitch/tls openssl req -x509 -nodes -days 3650 \ -newkey rsa:2048 \ -keyout /etc/freeswitch/tls/agent.pem \ -out /etc/freeswitch/tls/agent.pem \ -subj "/CN=YOUR_DOMAIN" # Combine into the format FreeSWITCH expects
cp /etc/freeswitch/tls/agent.pem /etc/freeswitch/tls/cafile.pem
chown freeswitch:freeswitch /etc/freeswitch/tls/*.pem
chmod 640 /etc/freeswitch/tls/*.pem
<!-- In sip_profiles/internal.xml, add these parameters -->
<param name="tls" value="true"/>
<param name="tls-bind-params" value="transport=tls"/>
<param name="tls-sip-port" value="5061"/>
<param name="tls-cert-dir" value="/etc/freeswitch/tls"/>
<param name="tls-version" value="tlsv1.2"/>
<!-- In sip_profiles/internal.xml, add these parameters -->
<param name="tls" value="true"/>
<param name="tls-bind-params" value="transport=tls"/>
<param name="tls-sip-port" value="5061"/>
<param name="tls-cert-dir" value="/etc/freeswitch/tls"/>
<param name="tls-version" value="tlsv1.2"/>
<!-- In sip_profiles/internal.xml, add these parameters -->
<param name="tls" value="true"/>
<param name="tls-bind-params" value="transport=tls"/>
<param name="tls-sip-port" value="5061"/>
<param name="tls-cert-dir" value="/etc/freeswitch/tls"/>
<param name="tls-version" value="tlsv1.2"/>
<!-- In sip_profiles/internal.xml -->
<param name="inbound-codec-string" value="OPUS,G722,PCMU,PCMA"/>
<param name="outbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <!-- Require SRTP -->
<!-- Options: mandatory, optional, forbidden -->
<param name="rtp_secure_media" value="optional"/>
<!-- In sip_profiles/internal.xml -->
<param name="inbound-codec-string" value="OPUS,G722,PCMU,PCMA"/>
<param name="outbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <!-- Require SRTP -->
<!-- Options: mandatory, optional, forbidden -->
<param name="rtp_secure_media" value="optional"/>
<!-- In sip_profiles/internal.xml -->
<param name="inbound-codec-string" value="OPUS,G722,PCMU,PCMA"/>
<param name="outbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <!-- Require SRTP -->
<!-- Options: mandatory, optional, forbidden -->
<param name="rtp_secure_media" value="optional"/>
<!-- In directory/default/1001.xml -->
<user id="1001"> <params> <param name="password" value="Str0ng_P@ss!"/> </params> <variables> <!-- Force SRTP for this user --> <variable name="rtp_secure_media" value="mandatory"/> <!-- ... other variables ... --> </variables>
</user>
<!-- In directory/default/1001.xml -->
<user id="1001"> <params> <param name="password" value="Str0ng_P@ss!"/> </params> <variables> <!-- Force SRTP for this user --> <variable name="rtp_secure_media" value="mandatory"/> <!-- ... other variables ... --> </variables>
</user>
<!-- In directory/default/1001.xml -->
<user id="1001"> <params> <param name="password" value="Str0ng_P@ss!"/> </params> <variables> <!-- Force SRTP for this user --> <variable name="rtp_secure_media" value="mandatory"/> <!-- ... other variables ... --> </variables>
</user>
<!-- In sip_profiles/internal.xml --> <!-- Max registrations per second -->
<param name="accept-blind-reg" value="false"/> <!-- Challenge all registrations (prevents spoofing) -->
<param name="auth-all-packets" value="true"/>
<param name="auth-calls" value="true"/> <!-- Limit concurrent calls -->
<!-- In vars.xml -->
<!-- <X-PRE-PROCESS cmd="set" data="max_sessions=500"/> -->
<!-- <X-PRE-PROCESS cmd="set" data="sessions_per_second=50"/> -->
<!-- In sip_profiles/internal.xml --> <!-- Max registrations per second -->
<param name="accept-blind-reg" value="false"/> <!-- Challenge all registrations (prevents spoofing) -->
<param name="auth-all-packets" value="true"/>
<param name="auth-calls" value="true"/> <!-- Limit concurrent calls -->
<!-- In vars.xml -->
<!-- <X-PRE-PROCESS cmd="set" data="max_sessions=500"/> -->
<!-- <X-PRE-PROCESS cmd="set" data="sessions_per_second=50"/> -->
<!-- In sip_profiles/internal.xml --> <!-- Max registrations per second -->
<param name="accept-blind-reg" value="false"/> <!-- Challenge all registrations (prevents spoofing) -->
<param name="auth-all-packets" value="true"/>
<param name="auth-calls" value="true"/> <!-- Limit concurrent calls -->
<!-- In vars.xml -->
<!-- <X-PRE-PROCESS cmd="set" data="max_sessions=500"/> -->
<!-- <X-PRE-PROCESS cmd="set" data="sessions_per_second=50"/> -->
# Set max concurrent sessions
fs_cli -x "fsctl max_sessions 500" # Set max sessions per second
fs_cli -x "fsctl sps 50"
# Set max concurrent sessions
fs_cli -x "fsctl max_sessions 500" # Set max sessions per second
fs_cli -x "fsctl sps 50"
# Set max concurrent sessions
fs_cli -x "fsctl max_sessions 500" # Set max sessions per second
fs_cli -x "fsctl sps 50"
# Minimal firewall rules for production FreeSWITCH # Flush existing rules (careful!)
# iptables -F # Default deny
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT # Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # Loopback
iptables -A INPUT -i lo -j ACCEPT # SSH (your management IP only)
iptables -A INPUT -p tcp -s YOUR_MGMT_IP --dport 22 -j ACCEPT # SIP from trusted networks only
iptables -A INPUT -p udp -s 10.0.0.0/8 --dport 5060 -j ACCEPT
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 5060 -j ACCEPT # SIP TLS
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 5061 -j ACCEPT # SIP from trunk providers (by IP)
iptables -A INPUT -p udp -s PROVIDER_IP_1 --dport 5080 -j ACCEPT
iptables -A INPUT -p udp -s PROVIDER_IP_2 --dport 5080 -j ACCEPT # RTP media (must be open — media comes from many IPs)
iptables -A INPUT -p udp --dport 16384:32768 -j ACCEPT # ESL (localhost only)
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 8021 -j ACCEPT # Save rules
iptables-save > /etc/iptables/rules.v4
# Minimal firewall rules for production FreeSWITCH # Flush existing rules (careful!)
# iptables -F # Default deny
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT # Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # Loopback
iptables -A INPUT -i lo -j ACCEPT # SSH (your management IP only)
iptables -A INPUT -p tcp -s YOUR_MGMT_IP --dport 22 -j ACCEPT # SIP from trusted networks only
iptables -A INPUT -p udp -s 10.0.0.0/8 --dport 5060 -j ACCEPT
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 5060 -j ACCEPT # SIP TLS
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 5061 -j ACCEPT # SIP from trunk providers (by IP)
iptables -A INPUT -p udp -s PROVIDER_IP_1 --dport 5080 -j ACCEPT
iptables -A INPUT -p udp -s PROVIDER_IP_2 --dport 5080 -j ACCEPT # RTP media (must be open — media comes from many IPs)
iptables -A INPUT -p udp --dport 16384:32768 -j ACCEPT # ESL (localhost only)
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 8021 -j ACCEPT # Save rules
iptables-save > /etc/iptables/rules.v4
# Minimal firewall rules for production FreeSWITCH # Flush existing rules (careful!)
# iptables -F # Default deny
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT # Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # Loopback
iptables -A INPUT -i lo -j ACCEPT # SSH (your management IP only)
iptables -A INPUT -p tcp -s YOUR_MGMT_IP --dport 22 -j ACCEPT # SIP from trusted networks only
iptables -A INPUT -p udp -s 10.0.0.0/8 --dport 5060 -j ACCEPT
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 5060 -j ACCEPT # SIP TLS
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 5061 -j ACCEPT # SIP from trunk providers (by IP)
iptables -A INPUT -p udp -s PROVIDER_IP_1 --dport 5080 -j ACCEPT
iptables -A INPUT -p udp -s PROVIDER_IP_2 --dport 5080 -j ACCEPT # RTP media (must be open — media comes from many IPs)
iptables -A INPUT -p udp --dport 16384:32768 -j ACCEPT # ESL (localhost only)
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 8021 -j ACCEPT # Save rules
iptables-save > /etc/iptables/rules.v4
Internet Kamailio (SBC) FreeSWITCH (Media) │ │ │ │ SIP INVITE │ │ ├───────────────────────►│ │ │ │ Rate limit, ACL check │ │ │ Route to FS │ │ ├────────────────────────►│ │ │ │ Answer, IVR, Bridge │ RTP (media) │ │ ├──────────────────────────────────────────────────►│ │ │ │
Internet Kamailio (SBC) FreeSWITCH (Media) │ │ │ │ SIP INVITE │ │ ├───────────────────────►│ │ │ │ Rate limit, ACL check │ │ │ Route to FS │ │ ├────────────────────────►│ │ │ │ Answer, IVR, Bridge │ RTP (media) │ │ ├──────────────────────────────────────────────────►│ │ │ │
Internet Kamailio (SBC) FreeSWITCH (Media) │ │ │ │ SIP INVITE │ │ ├───────────────────────►│ │ │ │ Rate limit, ACL check │ │ │ Route to FS │ │ ├────────────────────────►│ │ │ │ Answer, IVR, Bridge │ RTP (media) │ │ ├──────────────────────────────────────────────────►│ │ │ │
# kamailio.cfg (simplified)
# Route SIP to FreeSWITCH backend request_route { # Security checks if (!mf_process_maxfwd_header("10")) { sl_send_reply("483", "Too Many Hops"); exit; } # Rate limiting if (!pike_check_req()) { sl_send_reply("503", "Service Unavailable"); exit; } # Route INVITEs to FreeSWITCH if (is_method("INVITE")) { # Set destination to FreeSWITCH $du = "sip:FREESWITCH_IP:5060"; route(RELAY); } # Route REGISTERs to FreeSWITCH if (is_method("REGISTER")) { $du = "sip:FREESWITCH_IP:5060"; route(RELAY); }
} route[RELAY] { if (!t_relay()) { sl_reply_error(); }
}
# kamailio.cfg (simplified)
# Route SIP to FreeSWITCH backend request_route { # Security checks if (!mf_process_maxfwd_header("10")) { sl_send_reply("483", "Too Many Hops"); exit; } # Rate limiting if (!pike_check_req()) { sl_send_reply("503", "Service Unavailable"); exit; } # Route INVITEs to FreeSWITCH if (is_method("INVITE")) { # Set destination to FreeSWITCH $du = "sip:FREESWITCH_IP:5060"; route(RELAY); } # Route REGISTERs to FreeSWITCH if (is_method("REGISTER")) { $du = "sip:FREESWITCH_IP:5060"; route(RELAY); }
} route[RELAY] { if (!t_relay()) { sl_reply_error(); }
}
# kamailio.cfg (simplified)
# Route SIP to FreeSWITCH backend request_route { # Security checks if (!mf_process_maxfwd_header("10")) { sl_send_reply("483", "Too Many Hops"); exit; } # Rate limiting if (!pike_check_req()) { sl_send_reply("503", "Service Unavailable"); exit; } # Route INVITEs to FreeSWITCH if (is_method("INVITE")) { # Set destination to FreeSWITCH $du = "sip:FREESWITCH_IP:5060"; route(RELAY); } # Route REGISTERs to FreeSWITCH if (is_method("REGISTER")) { $du = "sip:FREESWITCH_IP:5060"; route(RELAY); }
} route[RELAY] { if (!t_relay()) { sl_reply_error(); }
}
<!-- In acl.conf.xml -->
<list name="kamailio" default="deny"> <node type="allow" cidr="KAMAILIO_IP/32"/>
</list> <!-- In sip_profiles/internal.xml -->
<param name="apply-inbound-acl" value="kamailio"/>
<param name="auth-calls" value="false"/>
<!-- Trust X-headers from Kamailio for routing decisions -->
<param name="apply-proxy-acl" value="kamailio"/>
<!-- In acl.conf.xml -->
<list name="kamailio" default="deny"> <node type="allow" cidr="KAMAILIO_IP/32"/>
</list> <!-- In sip_profiles/internal.xml -->
<param name="apply-inbound-acl" value="kamailio"/>
<param name="auth-calls" value="false"/>
<!-- Trust X-headers from Kamailio for routing decisions -->
<param name="apply-proxy-acl" value="kamailio"/>
<!-- In acl.conf.xml -->
<list name="kamailio" default="deny"> <node type="allow" cidr="KAMAILIO_IP/32"/>
</list> <!-- In sip_profiles/internal.xml -->
<param name="apply-inbound-acl" value="kamailio"/>
<param name="auth-calls" value="false"/>
<!-- Trust X-headers from Kamailio for routing decisions -->
<param name="apply-proxy-acl" value="kamailio"/>
<load module="mod_verto"/>
<load module="mod_verto"/>
<load module="mod_verto"/>
<configuration name="verto.conf" description="WebRTC Verto Endpoint"> <settings> <param name="debug" value="0"/> </settings> <profiles> <profile name="default-v4"> <param name="bind-local" value="YOUR_SERVER_IP:8081"/> <param name="bind-local" value="YOUR_SERVER_IP:8082" secure="true"/> <param name="force-register-domain" value="$${domain}"/> <param name="secure-combined" value="/etc/freeswitch/tls/agent.pem"/> <param name="secure-chain" value="/etc/freeswitch/tls/cafile.pem"/> <param name="userauth" value="true"/> <param name="context" value="default"/> <param name="dialplan" value="XML"/> <!-- STUN/TURN for NAT traversal --> <param name="rtp-ip" value="$${local_ip_v4}"/> <param name="ext-rtp-ip" value="$${external_rtp_ip}"/> <!-- Timer --> <param name="timer-name" value="soft"/> </profile> </profiles>
</configuration>
<configuration name="verto.conf" description="WebRTC Verto Endpoint"> <settings> <param name="debug" value="0"/> </settings> <profiles> <profile name="default-v4"> <param name="bind-local" value="YOUR_SERVER_IP:8081"/> <param name="bind-local" value="YOUR_SERVER_IP:8082" secure="true"/> <param name="force-register-domain" value="$${domain}"/> <param name="secure-combined" value="/etc/freeswitch/tls/agent.pem"/> <param name="secure-chain" value="/etc/freeswitch/tls/cafile.pem"/> <param name="userauth" value="true"/> <param name="context" value="default"/> <param name="dialplan" value="XML"/> <!-- STUN/TURN for NAT traversal --> <param name="rtp-ip" value="$${local_ip_v4}"/> <param name="ext-rtp-ip" value="$${external_rtp_ip}"/> <!-- Timer --> <param name="timer-name" value="soft"/> </profile> </profiles>
</configuration>
<configuration name="verto.conf" description="WebRTC Verto Endpoint"> <settings> <param name="debug" value="0"/> </settings> <profiles> <profile name="default-v4"> <param name="bind-local" value="YOUR_SERVER_IP:8081"/> <param name="bind-local" value="YOUR_SERVER_IP:8082" secure="true"/> <param name="force-register-domain" value="$${domain}"/> <param name="secure-combined" value="/etc/freeswitch/tls/agent.pem"/> <param name="secure-chain" value="/etc/freeswitch/tls/cafile.pem"/> <param name="userauth" value="true"/> <param name="context" value="default"/> <param name="dialplan" value="XML"/> <!-- STUN/TURN for NAT traversal --> <param name="rtp-ip" value="$${local_ip_v4}"/> <param name="ext-rtp-ip" value="$${external_rtp_ip}"/> <!-- Timer --> <param name="timer-name" value="soft"/> </profile> </profiles>
</configuration>
<!DOCTYPE html>
<html>
<head> <title>WebRTC Phone</title> <script src="https://cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js"></script> <script src="verto-min.js"></script>
</head>
<body> <h2>WebRTC Phone</h2> <div> <input type="text" id="number" placeholder="Enter number to call"/> <button onclick="makeCall()">Call</button> <button onclick="hangupCall()">Hangup</button> </div> <div id="status">Disconnected</div> <audio id="remoteAudio" autoplay></audio> <script> var verto; var currentCall = null; // Connect to FreeSWITCH via Verto $(document).ready(function() { verto = new $.verto({ login: '1001@YOUR_SERVER_IP', passwd: 'YOUR_EXTENSION_PASSWORD', socketUrl: 'wss://YOUR_SERVER_IP:8082', // ICE servers for NAT traversal iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ], deviceParams: { useMic: true, useSpeak: true }, audioParams: { googAutoGainControl: true, googNoiseSuppression: true, googEchoCancellation: true } }, { onWSLogin: function(v, success) { $('#status').text(success ? 'Connected' : 'Login Failed'); }, onDialogState: function(d) { switch (d.state.name) { case 'trying': $('#status').text('Calling...'); break; case 'ringing': $('#status').text('Ringing...'); break; case 'active': $('#status').text('In Call'); break; case 'hangup': case 'destroy': $('#status').text('Call Ended'); currentCall = null; break; } } }); }); function makeCall() { var number = $('#number').val(); if (!number) return; currentCall = verto.newCall({ destination_number: number, caller_id_name: 'WebRTC User', caller_id_number: '1001', useVideo: false, useStereo: false }); } function hangupCall() { if (currentCall) { currentCall.hangup(); } } </script>
</body>
</html>
<!DOCTYPE html>
<html>
<head> <title>WebRTC Phone</title> <script src="https://cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js"></script> <script src="verto-min.js"></script>
</head>
<body> <h2>WebRTC Phone</h2> <div> <input type="text" id="number" placeholder="Enter number to call"/> <button onclick="makeCall()">Call</button> <button onclick="hangupCall()">Hangup</button> </div> <div id="status">Disconnected</div> <audio id="remoteAudio" autoplay></audio> <script> var verto; var currentCall = null; // Connect to FreeSWITCH via Verto $(document).ready(function() { verto = new $.verto({ login: '1001@YOUR_SERVER_IP', passwd: 'YOUR_EXTENSION_PASSWORD', socketUrl: 'wss://YOUR_SERVER_IP:8082', // ICE servers for NAT traversal iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ], deviceParams: { useMic: true, useSpeak: true }, audioParams: { googAutoGainControl: true, googNoiseSuppression: true, googEchoCancellation: true } }, { onWSLogin: function(v, success) { $('#status').text(success ? 'Connected' : 'Login Failed'); }, onDialogState: function(d) { switch (d.state.name) { case 'trying': $('#status').text('Calling...'); break; case 'ringing': $('#status').text('Ringing...'); break; case 'active': $('#status').text('In Call'); break; case 'hangup': case 'destroy': $('#status').text('Call Ended'); currentCall = null; break; } } }); }); function makeCall() { var number = $('#number').val(); if (!number) return; currentCall = verto.newCall({ destination_number: number, caller_id_name: 'WebRTC User', caller_id_number: '1001', useVideo: false, useStereo: false }); } function hangupCall() { if (currentCall) { currentCall.hangup(); } } </script>
</body>
</html>
<!DOCTYPE html>
<html>
<head> <title>WebRTC Phone</title> <script src="https://cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js"></script> <script src="verto-min.js"></script>
</head>
<body> <h2>WebRTC Phone</h2> <div> <input type="text" id="number" placeholder="Enter number to call"/> <button onclick="makeCall()">Call</button> <button onclick="hangupCall()">Hangup</button> </div> <div id="status">Disconnected</div> <audio id="remoteAudio" autoplay></audio> <script> var verto; var currentCall = null; // Connect to FreeSWITCH via Verto $(document).ready(function() { verto = new $.verto({ login: '1001@YOUR_SERVER_IP', passwd: 'YOUR_EXTENSION_PASSWORD', socketUrl: 'wss://YOUR_SERVER_IP:8082', // ICE servers for NAT traversal iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ], deviceParams: { useMic: true, useSpeak: true }, audioParams: { googAutoGainControl: true, googNoiseSuppression: true, googEchoCancellation: true } }, { onWSLogin: function(v, success) { $('#status').text(success ? 'Connected' : 'Login Failed'); }, onDialogState: function(d) { switch (d.state.name) { case 'trying': $('#status').text('Calling...'); break; case 'ringing': $('#status').text('Ringing...'); break; case 'active': $('#status').text('In Call'); break; case 'hangup': case 'destroy': $('#status').text('Call Ended'); currentCall = null; break; } } }); }); function makeCall() { var number = $('#number').val(); if (!number) return; currentCall = verto.newCall({ destination_number: number, caller_id_name: 'WebRTC User', caller_id_number: '1001', useVideo: false, useStereo: false }); } function hangupCall() { if (currentCall) { currentCall.hangup(); } } </script>
</body>
</html>
<load module="mod_xml_curl"/>
<load module="mod_xml_curl"/>
<load module="mod_xml_curl"/>
<configuration name="xml_curl.conf" description="cURL XML Gateway"> <bindings> <!-- Dynamic user directory --> <binding name="directory"> <param name="gateway-url" value="http://127.0.0.1:8080/freeswitch/directory"/> <param name="gateway-credentials" value="fsapi:YOUR_API_KEY"/> <param name="auth-scheme" value="basic"/> <param name="timeout" value="5"/> <param name="enable-post" value="true"/> <param name="bindings" value="directory"/> </binding> <!-- Dynamic dialplan --> <binding name="dialplan"> <param name="gateway-url" value="http://127.0.0.1:8080/freeswitch/dialplan"/> <param name="gateway-credentials" value="fsapi:YOUR_API_KEY"/> <param name="auth-scheme" value="basic"/> <param name="timeout" value="5"/> <param name="enable-post" value="true"/> <param name="bindings" value="dialplan"/> </binding> </bindings>
</configuration>
<configuration name="xml_curl.conf" description="cURL XML Gateway"> <bindings> <!-- Dynamic user directory --> <binding name="directory"> <param name="gateway-url" value="http://127.0.0.1:8080/freeswitch/directory"/> <param name="gateway-credentials" value="fsapi:YOUR_API_KEY"/> <param name="auth-scheme" value="basic"/> <param name="timeout" value="5"/> <param name="enable-post" value="true"/> <param name="bindings" value="directory"/> </binding> <!-- Dynamic dialplan --> <binding name="dialplan"> <param name="gateway-url" value="http://127.0.0.1:8080/freeswitch/dialplan"/> <param name="gateway-credentials" value="fsapi:YOUR_API_KEY"/> <param name="auth-scheme" value="basic"/> <param name="timeout" value="5"/> <param name="enable-post" value="true"/> <param name="bindings" value="dialplan"/> </binding> </bindings>
</configuration>
<configuration name="xml_curl.conf" description="cURL XML Gateway"> <bindings> <!-- Dynamic user directory --> <binding name="directory"> <param name="gateway-url" value="http://127.0.0.1:8080/freeswitch/directory"/> <param name="gateway-credentials" value="fsapi:YOUR_API_KEY"/> <param name="auth-scheme" value="basic"/> <param name="timeout" value="5"/> <param name="enable-post" value="true"/> <param name="bindings" value="directory"/> </binding> <!-- Dynamic dialplan --> <binding name="dialplan"> <param name="gateway-url" value="http://127.0.0.1:8080/freeswitch/dialplan"/> <param name="gateway-credentials" value="fsapi:YOUR_API_KEY"/> <param name="auth-scheme" value="basic"/> <param name="timeout" value="5"/> <param name="enable-post" value="true"/> <param name="bindings" value="dialplan"/> </binding> </bindings>
</configuration>
#!/usr/bin/env python3
"""
freeswitch_api.py
Serve dynamic XML configuration to FreeSWITCH via mod_xml_curl.
""" from flask import Flask, request, Response
import sqlite3 app = Flask(__name__)
DB_PATH = '/var/lib/freeswitch/db/users.db' @app.route('/freeswitch/directory', methods=['POST'])
def directory(): """Return user XML based on registration request.""" user = request.form.get('user', '') domain = request.form.get('domain', '') action = request.form.get('action', '') if not user: return not_found() # Look up user in database conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute( 'SELECT password, name, caller_id FROM users WHERE extension = ?', (user,) ) row = cursor.fetchone() conn.close() if not row: return not_found() password, name, caller_id = row xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="directory"> <domain name="{domain}"> <user id="{user}"> <params> <param name="password" value="{password}"/> <param name="vm-password" value="{user}"/> </params> <variables> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="{name}"/> <variable name="effective_caller_id_number" value="{caller_id or user}"/> </variables> </user> </domain> </section>
</document>''' return Response(xml, mimetype='text/xml') @app.route('/freeswitch/dialplan', methods=['POST'])
def dialplan(): """Return dynamic dialplan XML.""" dest = request.form.get('Caller-Destination-Number', '') context = request.form.get('Caller-Context', 'default') caller = request.form.get('Caller-Caller-ID-Number', '') # Example: route based on database lookup conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute( 'SELECT action, target FROM routes WHERE pattern = ? AND context = ?', (dest, context) ) row = cursor.fetchone() conn.close() if row: action_type, target = row xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="dialplan"> <context name="{context}"> <extension name="dynamic_route"> <condition field="destination_number" expression="^{dest}$"> <action application="{action_type}" data="{target}"/> </condition> </extension> </context> </section>
</document>''' else: xml = not_found_xml() return Response(xml, mimetype='text/xml') def not_found(): return Response(not_found_xml(), mimetype='text/xml') def not_found_xml(): return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="result"> <result status="not found"/> </section>
</document>''' if __name__ == '__main__': app.run(host='127.0.0.1', port=8080)
#!/usr/bin/env python3
"""
freeswitch_api.py
Serve dynamic XML configuration to FreeSWITCH via mod_xml_curl.
""" from flask import Flask, request, Response
import sqlite3 app = Flask(__name__)
DB_PATH = '/var/lib/freeswitch/db/users.db' @app.route('/freeswitch/directory', methods=['POST'])
def directory(): """Return user XML based on registration request.""" user = request.form.get('user', '') domain = request.form.get('domain', '') action = request.form.get('action', '') if not user: return not_found() # Look up user in database conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute( 'SELECT password, name, caller_id FROM users WHERE extension = ?', (user,) ) row = cursor.fetchone() conn.close() if not row: return not_found() password, name, caller_id = row xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="directory"> <domain name="{domain}"> <user id="{user}"> <params> <param name="password" value="{password}"/> <param name="vm-password" value="{user}"/> </params> <variables> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="{name}"/> <variable name="effective_caller_id_number" value="{caller_id or user}"/> </variables> </user> </domain> </section>
</document>''' return Response(xml, mimetype='text/xml') @app.route('/freeswitch/dialplan', methods=['POST'])
def dialplan(): """Return dynamic dialplan XML.""" dest = request.form.get('Caller-Destination-Number', '') context = request.form.get('Caller-Context', 'default') caller = request.form.get('Caller-Caller-ID-Number', '') # Example: route based on database lookup conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute( 'SELECT action, target FROM routes WHERE pattern = ? AND context = ?', (dest, context) ) row = cursor.fetchone() conn.close() if row: action_type, target = row xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="dialplan"> <context name="{context}"> <extension name="dynamic_route"> <condition field="destination_number" expression="^{dest}$"> <action application="{action_type}" data="{target}"/> </condition> </extension> </context> </section>
</document>''' else: xml = not_found_xml() return Response(xml, mimetype='text/xml') def not_found(): return Response(not_found_xml(), mimetype='text/xml') def not_found_xml(): return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="result"> <result status="not found"/> </section>
</document>''' if __name__ == '__main__': app.run(host='127.0.0.1', port=8080)
#!/usr/bin/env python3
"""
freeswitch_api.py
Serve dynamic XML configuration to FreeSWITCH via mod_xml_curl.
""" from flask import Flask, request, Response
import sqlite3 app = Flask(__name__)
DB_PATH = '/var/lib/freeswitch/db/users.db' @app.route('/freeswitch/directory', methods=['POST'])
def directory(): """Return user XML based on registration request.""" user = request.form.get('user', '') domain = request.form.get('domain', '') action = request.form.get('action', '') if not user: return not_found() # Look up user in database conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute( 'SELECT password, name, caller_id FROM users WHERE extension = ?', (user,) ) row = cursor.fetchone() conn.close() if not row: return not_found() password, name, caller_id = row xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="directory"> <domain name="{domain}"> <user id="{user}"> <params> <param name="password" value="{password}"/> <param name="vm-password" value="{user}"/> </params> <variables> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="{name}"/> <variable name="effective_caller_id_number" value="{caller_id or user}"/> </variables> </user> </domain> </section>
</document>''' return Response(xml, mimetype='text/xml') @app.route('/freeswitch/dialplan', methods=['POST'])
def dialplan(): """Return dynamic dialplan XML.""" dest = request.form.get('Caller-Destination-Number', '') context = request.form.get('Caller-Context', 'default') caller = request.form.get('Caller-Caller-ID-Number', '') # Example: route based on database lookup conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute( 'SELECT action, target FROM routes WHERE pattern = ? AND context = ?', (dest, context) ) row = cursor.fetchone() conn.close() if row: action_type, target = row xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="dialplan"> <context name="{context}"> <extension name="dynamic_route"> <condition field="destination_number" expression="^{dest}$"> <action application="{action_type}" data="{target}"/> </condition> </extension> </context> </section>
</document>''' else: xml = not_found_xml() return Response(xml, mimetype='text/xml') def not_found(): return Response(not_found_xml(), mimetype='text/xml') def not_found_xml(): return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="result"> <result status="not found"/> </section>
</document>''' if __name__ == '__main__': app.run(host='127.0.0.1', port=8080)
<!-- In dialplan — call a Lua script for routing decisions -->
<extension name="api_routing"> <condition field="destination_number" expression="^(8[0-9]{3})$"> <action application="lua" data="api_route.lua"/> </condition>
</extension>
<!-- In dialplan — call a Lua script for routing decisions -->
<extension name="api_routing"> <condition field="destination_number" expression="^(8[0-9]{3})$"> <action application="lua" data="api_route.lua"/> </condition>
</extension>
<!-- In dialplan — call a Lua script for routing decisions -->
<extension name="api_routing"> <condition field="destination_number" expression="^(8[0-9]{3})$"> <action application="lua" data="api_route.lua"/> </condition>
</extension>
-- api_route.lua
-- Look up routing information from a REST API local api = require("socket.http")
local json = require("cjson") -- Get call info
local caller = session:getVariable("caller_id_number")
local dest = session:getVariable("destination_number") -- Call external API
local url = string.format( "http://127.0.0.1:8080/api/route?caller=%s&dest=%s", caller, dest
) local response, code = api.request(url) if code == 200 and response then local data = json.decode(response) if data.action == "bridge" then session:setVariable("effective_caller_id_number", data.caller_id or caller) session:execute("bridge", data.target) elseif data.action == "voicemail" then session:execute("voicemail", "default " .. data.domain .. " " .. data.mailbox) elseif data.action == "reject" then session:execute("respond", "403") end
else -- API unavailable — fallback to default routing session:execute("transfer", "1009 XML default")
end
-- api_route.lua
-- Look up routing information from a REST API local api = require("socket.http")
local json = require("cjson") -- Get call info
local caller = session:getVariable("caller_id_number")
local dest = session:getVariable("destination_number") -- Call external API
local url = string.format( "http://127.0.0.1:8080/api/route?caller=%s&dest=%s", caller, dest
) local response, code = api.request(url) if code == 200 and response then local data = json.decode(response) if data.action == "bridge" then session:setVariable("effective_caller_id_number", data.caller_id or caller) session:execute("bridge", data.target) elseif data.action == "voicemail" then session:execute("voicemail", "default " .. data.domain .. " " .. data.mailbox) elseif data.action == "reject" then session:execute("respond", "403") end
else -- API unavailable — fallback to default routing session:execute("transfer", "1009 XML default")
end
-- api_route.lua
-- Look up routing information from a REST API local api = require("socket.http")
local json = require("cjson") -- Get call info
local caller = session:getVariable("caller_id_number")
local dest = session:getVariable("destination_number") -- Call external API
local url = string.format( "http://127.0.0.1:8080/api/route?caller=%s&dest=%s", caller, dest
) local response, code = api.request(url) if code == 200 and response then local data = json.decode(response) if data.action == "bridge" then session:setVariable("effective_caller_id_number", data.caller_id or caller) session:execute("bridge", data.target) elseif data.action == "voicemail" then session:execute("voicemail", "default " .. data.domain .. " " .. data.mailbox) elseif data.action == "reject" then session:execute("respond", "403") end
else -- API unavailable — fallback to default routing session:execute("transfer", "1009 XML default")
end
<include> <gateway name="asterisk"> <param name="username" value="freeswitch_trunk"/> <param name="password" value="SHARED_TRUNK_PASSWORD"/> <param name="realm" value="ASTERISK_IP"/> <param name="proxy" value="ASTERISK_IP"/> <param name="register" value="true"/> <param name="caller-id-in-from" value="true"/> <param name="codec-prefs" value="PCMU,PCMA"/> </gateway>
</include>
<include> <gateway name="asterisk"> <param name="username" value="freeswitch_trunk"/> <param name="password" value="SHARED_TRUNK_PASSWORD"/> <param name="realm" value="ASTERISK_IP"/> <param name="proxy" value="ASTERISK_IP"/> <param name="register" value="true"/> <param name="caller-id-in-from" value="true"/> <param name="codec-prefs" value="PCMU,PCMA"/> </gateway>
</include>
<include> <gateway name="asterisk"> <param name="username" value="freeswitch_trunk"/> <param name="password" value="SHARED_TRUNK_PASSWORD"/> <param name="realm" value="ASTERISK_IP"/> <param name="proxy" value="ASTERISK_IP"/> <param name="register" value="true"/> <param name="caller-id-in-from" value="true"/> <param name="codec-prefs" value="PCMU,PCMA"/> </gateway>
</include>
<extension name="to_asterisk"> <condition field="destination_number" expression="^(2[0-9]{3})$"> <action application="bridge" data="sofia/gateway/asterisk/$1"/> </condition>
</extension>
<extension name="to_asterisk"> <condition field="destination_number" expression="^(2[0-9]{3})$"> <action application="bridge" data="sofia/gateway/asterisk/$1"/> </condition>
</extension>
<extension name="to_asterisk"> <condition field="destination_number" expression="^(2[0-9]{3})$"> <action application="bridge" data="sofia/gateway/asterisk/$1"/> </condition>
</extension>
; pjsip.conf — FreeSWITCH trunk
[freeswitch_trunk]
type=registration
outbound_auth=freeswitch_trunk_auth
server_uri=sip:FREESWITCH_IP:5080
client_uri=sip:freeswitch_trunk@FREESWITCH_IP:5080
retry_interval=60 [freeswitch_trunk_auth]
type=auth
auth_type=userpass
username=freeswitch_trunk
password=SHARED_TRUNK_PASSWORD [freeswitch_trunk_endpoint]
type=endpoint
context=from-freeswitch
disallow=all
allow=ulaw,alaw
outbound_auth=freeswitch_trunk_auth
aors=freeswitch_trunk_aor [freeswitch_trunk_aor]
type=aor
contact=sip:FREESWITCH_IP:5080 [freeswitch_trunk_identify]
type=identify
endpoint=freeswitch_trunk_endpoint
match=FREESWITCH_IP
; pjsip.conf — FreeSWITCH trunk
[freeswitch_trunk]
type=registration
outbound_auth=freeswitch_trunk_auth
server_uri=sip:FREESWITCH_IP:5080
client_uri=sip:freeswitch_trunk@FREESWITCH_IP:5080
retry_interval=60 [freeswitch_trunk_auth]
type=auth
auth_type=userpass
username=freeswitch_trunk
password=SHARED_TRUNK_PASSWORD [freeswitch_trunk_endpoint]
type=endpoint
context=from-freeswitch
disallow=all
allow=ulaw,alaw
outbound_auth=freeswitch_trunk_auth
aors=freeswitch_trunk_aor [freeswitch_trunk_aor]
type=aor
contact=sip:FREESWITCH_IP:5080 [freeswitch_trunk_identify]
type=identify
endpoint=freeswitch_trunk_endpoint
match=FREESWITCH_IP
; pjsip.conf — FreeSWITCH trunk
[freeswitch_trunk]
type=registration
outbound_auth=freeswitch_trunk_auth
server_uri=sip:FREESWITCH_IP:5080
client_uri=sip:freeswitch_trunk@FREESWITCH_IP:5080
retry_interval=60 [freeswitch_trunk_auth]
type=auth
auth_type=userpass
username=freeswitch_trunk
password=SHARED_TRUNK_PASSWORD [freeswitch_trunk_endpoint]
type=endpoint
context=from-freeswitch
disallow=all
allow=ulaw,alaw
outbound_auth=freeswitch_trunk_auth
aors=freeswitch_trunk_aor [freeswitch_trunk_aor]
type=aor
contact=sip:FREESWITCH_IP:5080 [freeswitch_trunk_identify]
type=identify
endpoint=freeswitch_trunk_endpoint
match=FREESWITCH_IP
┌──────────────┐ │ Kamailio │ SIP phones ──────► (Router) ◄────── SIP Trunks └──────┬───────┘ │ ┌────────────┼────────────┐ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ FS Node 1│ │ FS Node 2│ │ FS Node 3│ │ (Media) │ │ (Media) │ │ (Media) │ └──────────┘ └──────────┘ └──────────┘
┌──────────────┐ │ Kamailio │ SIP phones ──────► (Router) ◄────── SIP Trunks └──────┬───────┘ │ ┌────────────┼────────────┐ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ FS Node 1│ │ FS Node 2│ │ FS Node 3│ │ (Media) │ │ (Media) │ │ (Media) │ └──────────┘ └──────────┘ └──────────┘
┌──────────────┐ │ Kamailio │ SIP phones ──────► (Router) ◄────── SIP Trunks └──────┬───────┘ │ ┌────────────┼────────────┐ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ FS Node 1│ │ FS Node 2│ │ FS Node 3│ │ (Media) │ │ (Media) │ │ (Media) │ └──────────┘ └──────────┘ └──────────┘
# kamailio.cfg — load balance across FreeSWITCH nodes
modparam("dispatcher", "list_file", "/etc/kamailio/dispatcher.list") # dispatcher.list:
# setid destination flags priority
1 sip:FS_NODE_1:5060 0 50
1 sip:FS_NODE_2:5060 0 50
1 sip:FS_NODE_3:5060 0 50
# kamailio.cfg — load balance across FreeSWITCH nodes
modparam("dispatcher", "list_file", "/etc/kamailio/dispatcher.list") # dispatcher.list:
# setid destination flags priority
1 sip:FS_NODE_1:5060 0 50
1 sip:FS_NODE_2:5060 0 50
1 sip:FS_NODE_3:5060 0 50
# kamailio.cfg — load balance across FreeSWITCH nodes
modparam("dispatcher", "list_file", "/etc/kamailio/dispatcher.list") # dispatcher.list:
# setid destination flags priority
1 sip:FS_NODE_1:5060 0 50
1 sip:FS_NODE_2:5060 0 50
1 sip:FS_NODE_3:5060 0 50
# 1. Check if the profile is running
fs_cli -x "sofia status" # 2. Check registered users
fs_cli -x "sofia status profile internal reg" # 3. Enable SIP trace to see actual SIP messages
fs_cli -x "sofia profile internal siptrace on"
# Make a registration attempt, then:
fs_cli -x "sofia profile internal siptrace off" # 4. Check the log for auth failures
grep -i "auth" /var/log/freeswitch/freeswitch.log | tail -20 # 5. Verify the user exists in the directory
fs_cli -x "xml_locate directory default 1001" # 6. Test connectivity
# From the phone's network, check if SIP port is reachable:
# nmap -sU -p 5060 YOUR_SERVER_IP
# 1. Check if the profile is running
fs_cli -x "sofia status" # 2. Check registered users
fs_cli -x "sofia status profile internal reg" # 3. Enable SIP trace to see actual SIP messages
fs_cli -x "sofia profile internal siptrace on"
# Make a registration attempt, then:
fs_cli -x "sofia profile internal siptrace off" # 4. Check the log for auth failures
grep -i "auth" /var/log/freeswitch/freeswitch.log | tail -20 # 5. Verify the user exists in the directory
fs_cli -x "xml_locate directory default 1001" # 6. Test connectivity
# From the phone's network, check if SIP port is reachable:
# nmap -sU -p 5060 YOUR_SERVER_IP
# 1. Check if the profile is running
fs_cli -x "sofia status" # 2. Check registered users
fs_cli -x "sofia status profile internal reg" # 3. Enable SIP trace to see actual SIP messages
fs_cli -x "sofia profile internal siptrace on"
# Make a registration attempt, then:
fs_cli -x "sofia profile internal siptrace off" # 4. Check the log for auth failures
grep -i "auth" /var/log/freeswitch/freeswitch.log | tail -20 # 5. Verify the user exists in the directory
fs_cli -x "xml_locate directory default 1001" # 6. Test connectivity
# From the phone's network, check if SIP port is reachable:
# nmap -sU -p 5060 YOUR_SERVER_IP
# 1. Check RTP ports in firewall
# Ensure 16384-32768/udp is open # 2. Check NAT settings
fs_cli -x "sofia status profile internal"
# Look for ext-sip-ip and ext-rtp-ip — they should be your PUBLIC IP # 3. Check codec negotiation
fs_cli -x "show channels"
# Look at read_codec and write_codec — should match on both legs # 4. During a call, check RTP stats
fs_cli -x "uuid_debug_media <call-uuid> read on"
# This shows incoming RTP packets (or lack thereof) # 5. Check for RTP timeout
grep "rtp_timeout" /var/log/freeswitch/freeswitch.log | tail -10
# 1. Check RTP ports in firewall
# Ensure 16384-32768/udp is open # 2. Check NAT settings
fs_cli -x "sofia status profile internal"
# Look for ext-sip-ip and ext-rtp-ip — they should be your PUBLIC IP # 3. Check codec negotiation
fs_cli -x "show channels"
# Look at read_codec and write_codec — should match on both legs # 4. During a call, check RTP stats
fs_cli -x "uuid_debug_media <call-uuid> read on"
# This shows incoming RTP packets (or lack thereof) # 5. Check for RTP timeout
grep "rtp_timeout" /var/log/freeswitch/freeswitch.log | tail -10
# 1. Check RTP ports in firewall
# Ensure 16384-32768/udp is open # 2. Check NAT settings
fs_cli -x "sofia status profile internal"
# Look for ext-sip-ip and ext-rtp-ip — they should be your PUBLIC IP # 3. Check codec negotiation
fs_cli -x "show channels"
# Look at read_codec and write_codec — should match on both legs # 4. During a call, check RTP stats
fs_cli -x "uuid_debug_media <call-uuid> read on"
# This shows incoming RTP packets (or lack thereof) # 5. Check for RTP timeout
grep "rtp_timeout" /var/log/freeswitch/freeswitch.log | tail -10
<!-- Verify these are set correctly in your profile -->
<param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/>
<param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/>
<param name="apply-nat-acl" value="nat.auto"/>
<param name="aggressive-nat-detection" value="true"/>
<!-- Verify these are set correctly in your profile -->
<param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/>
<param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/>
<param name="apply-nat-acl" value="nat.auto"/>
<param name="aggressive-nat-detection" value="true"/>
<!-- Verify these are set correctly in your profile -->
<param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/>
<param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/>
<param name="apply-nat-acl" value="nat.auto"/>
<param name="aggressive-nat-detection" value="true"/>
# 1. Test dialplan matching without making a real call
fs_cli -x "expand xml_locate dialplan default 15551234567 as xml" # 2. Enable dialplan debug
fs_cli -x "sofia loglevel all 7" # 3. Check which context the call enters
grep "Processing" /var/log/freeswitch/freeswitch.log | tail -20 # 4. Manually trace a number through the dialplan
fs_cli
> reloadxml
> originate user/1001 &echo()
# Then check logs for the routing path # 5. Check for regex issues
# In fs_cli:
> regex 15551234567 ^(\+?1?\d{10})$
# Should return "true" if the pattern matches
# 1. Test dialplan matching without making a real call
fs_cli -x "expand xml_locate dialplan default 15551234567 as xml" # 2. Enable dialplan debug
fs_cli -x "sofia loglevel all 7" # 3. Check which context the call enters
grep "Processing" /var/log/freeswitch/freeswitch.log | tail -20 # 4. Manually trace a number through the dialplan
fs_cli
> reloadxml
> originate user/1001 &echo()
# Then check logs for the routing path # 5. Check for regex issues
# In fs_cli:
> regex 15551234567 ^(\+?1?\d{10})$
# Should return "true" if the pattern matches
# 1. Test dialplan matching without making a real call
fs_cli -x "expand xml_locate dialplan default 15551234567 as xml" # 2. Enable dialplan debug
fs_cli -x "sofia loglevel all 7" # 3. Check which context the call enters
grep "Processing" /var/log/freeswitch/freeswitch.log | tail -20 # 4. Manually trace a number through the dialplan
fs_cli
> reloadxml
> originate user/1001 &echo()
# Then check logs for the routing path # 5. Check for regex issues
# In fs_cli:
> regex 15551234567 ^(\+?1?\d{10})$
# Should return "true" if the pattern matches
# 1. Check system resources
fs_cli -x "status"
# Look at "min idle cpu" — should be above 50% # 2. Check session count
fs_cli -x "show channels count" # 3. Check for codec transcoding (CPU-intensive)
fs_cli -x "show channels"
# If read_codec != write_codec, transcoding is happening # 4. Check for memory leaks
fs_cli -x "status"
# Monitor session count over time — should not grow unbounded # 5. Profile-level stats
fs_cli -x "sofia status profile internal"
# Check CALLS-IN, CALLS-OUT, FAILED counts
# 1. Check system resources
fs_cli -x "status"
# Look at "min idle cpu" — should be above 50% # 2. Check session count
fs_cli -x "show channels count" # 3. Check for codec transcoding (CPU-intensive)
fs_cli -x "show channels"
# If read_codec != write_codec, transcoding is happening # 4. Check for memory leaks
fs_cli -x "status"
# Monitor session count over time — should not grow unbounded # 5. Profile-level stats
fs_cli -x "sofia status profile internal"
# Check CALLS-IN, CALLS-OUT, FAILED counts
# 1. Check system resources
fs_cli -x "status"
# Look at "min idle cpu" — should be above 50% # 2. Check session count
fs_cli -x "show channels count" # 3. Check for codec transcoding (CPU-intensive)
fs_cli -x "show channels"
# If read_codec != write_codec, transcoding is happening # 4. Check for memory leaks
fs_cli -x "status"
# Monitor session count over time — should not grow unbounded # 5. Profile-level stats
fs_cli -x "sofia status profile internal"
# Check CALLS-IN, CALLS-OUT, FAILED counts
# Increase file descriptor limits
cat >> /etc/security/limits.conf << 'EOF'
freeswitch soft nofile 65536
freeswitch hard nofile 65536
freeswitch soft nproc 65536
freeswitch hard nproc 65536
EOF # Increase max sessions (default 1000)
fs_cli -x "fsctl max_sessions 5000" # Increase sessions per second (default 30)
fs_cli -x "fsctl sps 200" # Disable unnecessary modules
# Edit modules.conf.xml and comment out unused modules
# Fewer modules = less memory + faster startup
# Increase file descriptor limits
cat >> /etc/security/limits.conf << 'EOF'
freeswitch soft nofile 65536
freeswitch hard nofile 65536
freeswitch soft nproc 65536
freeswitch hard nproc 65536
EOF # Increase max sessions (default 1000)
fs_cli -x "fsctl max_sessions 5000" # Increase sessions per second (default 30)
fs_cli -x "fsctl sps 200" # Disable unnecessary modules
# Edit modules.conf.xml and comment out unused modules
# Fewer modules = less memory + faster startup
# Increase file descriptor limits
cat >> /etc/security/limits.conf << 'EOF'
freeswitch soft nofile 65536
freeswitch hard nofile 65536
freeswitch soft nproc 65536
freeswitch hard nproc 65536
EOF # Increase max sessions (default 1000)
fs_cli -x "fsctl max_sessions 5000" # Increase sessions per second (default 30)
fs_cli -x "fsctl sps 200" # Disable unnecessary modules
# Edit modules.conf.xml and comment out unused modules
# Fewer modules = less memory + faster startup
# 1. Is FreeSWITCH running?
systemctl status freeswitch # 2. Can you connect via ESL?
fs_cli -x "status" # 3. Are SIP profiles up?
fs_cli -x "sofia status" # 4. Are phones registered?
fs_cli -x "sofia status profile internal reg" # 5. Are trunks connected?
fs_cli -x "sofia status gateway my_provider" # 6. Any errors in the log?
tail -100 /var/log/freeswitch/freeswitch.log | grep -i "error\|warning\|fail" # 7. Is it a firewall issue?
ss -ulnp | grep 5060 # SIP port listening?
ss -ulnp | grep 8021 # ESL port listening? # 8. Disk space?
df -h # 9. Memory?
free -h # 10. Active calls?
fs_cli -x "show channels count"
# 1. Is FreeSWITCH running?
systemctl status freeswitch # 2. Can you connect via ESL?
fs_cli -x "status" # 3. Are SIP profiles up?
fs_cli -x "sofia status" # 4. Are phones registered?
fs_cli -x "sofia status profile internal reg" # 5. Are trunks connected?
fs_cli -x "sofia status gateway my_provider" # 6. Any errors in the log?
tail -100 /var/log/freeswitch/freeswitch.log | grep -i "error\|warning\|fail" # 7. Is it a firewall issue?
ss -ulnp | grep 5060 # SIP port listening?
ss -ulnp | grep 8021 # ESL port listening? # 8. Disk space?
df -h # 9. Memory?
free -h # 10. Active calls?
fs_cli -x "show channels count"
# 1. Is FreeSWITCH running?
systemctl status freeswitch # 2. Can you connect via ESL?
fs_cli -x "status" # 3. Are SIP profiles up?
fs_cli -x "sofia status" # 4. Are phones registered?
fs_cli -x "sofia status profile internal reg" # 5. Are trunks connected?
fs_cli -x "sofia status gateway my_provider" # 6. Any errors in the log?
tail -100 /var/log/freeswitch/freeswitch.log | grep -i "error\|warning\|fail" # 7. Is it a firewall issue?
ss -ulnp | grep 5060 # SIP port listening?
ss -ulnp | grep 8021 # ESL port listening? # 8. Disk space?
df -h # 9. Memory?
free -h # 10. Active calls?
fs_cli -x "show channels count" - Introduction
- Architecture Overview
- Installation
- SIP Configuration (mod_sofia)
- XML Dialplan
- Call Recording
- Conference Bridge
- Event Socket Layer (ESL)
- CDR & Logging
- Security Hardening
- Integration Patterns
- Troubleshooting - Carrier-grade switching: High call volume (1,000+ concurrent), SIP-to-SIP routing, least-cost routing
- WebRTC gateway: Browser-based calling without external proxies
- IVR platform: Complex multi-level IVR systems with database lookups and REST API integration
- Conference server: Large-scale conferencing (hundreds of rooms, thousands of participants)
- Media server: Behind Kamailio/OpenSIPS as a B2BUA or media processing engine
- Telecom applications: When your app controls calls programmatically via ESL
- Multi-tenant PBX: Built-in domain-based multi-tenancy - Event-driven: Everything in FreeSWITCH generates events. A call arriving, a DTMF press, a conference join — all are events that can be captured, filtered, and acted upon by internal modules or external applications.
- Modular: The core is a lightweight engine. All functionality — SIP, dialplan, codecs, applications — comes from loadable modules. You enable only what you need.
- Session-based: Each call creates a session object that persists for the call's lifetime. Sessions hold all state (variables, media streams, applications) and are managed by a thread pool, not one-thread-per-call. - Go to https://id.signalwire.com/personal_access_tokens
- Create a free account (no credit card needed)
- Generate a Personal Access Token
- Save the token — you will need it below - mod_ivr menus — XML-configured menu trees with automatic DTMF handling, timeouts, and invalid-input retries. Best for simple, static IVR flows.
- play_and_get_digits — A dialplan application that plays a prompt and collects DTMF digits. Best for dynamic IVRs with variable-based routing. - 5 — Configuration menu
- 1 — Record a new greeting
- 2 — Choose between recorded greetings - mod_callcenter — Built-in ACD (Automatic Call Distribution) for contact centers
- mod_fifo — Call queues and parking
- mod_lcr — Least Cost Routing for multi-carrier environments
- mod_nibblebill — Real-time prepaid billing
- mod_skinny — Cisco SCCP phone support
- FusionPBX — Full web GUI for FreeSWITCH administration
Incoming SIP INVITE │ ▼ mod_sofia (SIP stack) │ ▼ Profile (internal or external) │ ▼ Context (default or public) │ ▼ Extension matching (top-to-bottom) │ ▼ Condition evaluation (regex) │ ▼ Actions execute (answer, bridge, playback, etc.)
Incoming SIP INVITE │ ▼ mod_sofia (SIP stack) │ ▼ Profile (internal or external) │ ▼ Context (default or public) │ ▼ Extension matching (top-to-bottom) │ ▼ Condition evaluation (regex) │ ▼ Actions execute (answer, bridge, playback, etc.)
Incoming SIP INVITE │ ▼ mod_sofia (SIP stack) │ ▼ Profile (internal or external) │ ▼ Context (default or public) │ ▼ Extension matching (top-to-bottom) │ ▼ Condition evaluation (regex) │ ▼ Actions execute (answer, bridge, playback, etc.)
/etc/freeswitch/ # All configuration
├── freeswitch.xml # Master config (includes everything)
├── vars.xml # Global variables (domain, passwords, IPs)
├── autoload_configs/ # Module configurations
│ ├── modules.conf.xml # Which modules to load
│ ├── sofia.conf.xml # SIP module config (includes profiles)
│ ├── conference.conf.xml # Conference profiles
│ ├── voicemail.conf.xml # Voicemail settings
│ ├── ivr.conf.xml # IVR menu definitions
│ ├── cdr_csv.conf.xml # CDR output config
│ └── event_socket.conf.xml # ESL listener config
├── sip_profiles/ # SIP profile definitions
│ ├── internal.xml # Internal profile (phones, port 5060)
│ ├── external.xml # External profile (trunks, port 5080)
│ └── external/ # Gateway (trunk) definitions
│ └── my_provider.xml # One file per trunk
├── dialplan/ # Call routing rules
│ ├── default.xml # Internal context (phone-to-phone)
│ ├── public.xml # External context (inbound from trunks)
│ └── default/ # Additional dialplan fragments
├── directory/ # User/phone definitions
│ └── default/ # Domain directory
│ ├── 1000.xml # Extension 1000
│ ├── 1001.xml # Extension 1001
│ └── ...
└── lang/ # Language/sound file mappings /var/lib/freeswitch/ # Runtime data
├── db/ # SQLite databases (registrations, etc.)
├── recordings/ # Call recordings
├── storage/ # Voicemail, fax storage
└── sounds/ # Audio/prompt files /var/log/freeswitch/ # Logs
├── freeswitch.log # Main log (rotated)
└── cdr-csv/ # CDR files
/etc/freeswitch/ # All configuration
├── freeswitch.xml # Master config (includes everything)
├── vars.xml # Global variables (domain, passwords, IPs)
├── autoload_configs/ # Module configurations
│ ├── modules.conf.xml # Which modules to load
│ ├── sofia.conf.xml # SIP module config (includes profiles)
│ ├── conference.conf.xml # Conference profiles
│ ├── voicemail.conf.xml # Voicemail settings
│ ├── ivr.conf.xml # IVR menu definitions
│ ├── cdr_csv.conf.xml # CDR output config
│ └── event_socket.conf.xml # ESL listener config
├── sip_profiles/ # SIP profile definitions
│ ├── internal.xml # Internal profile (phones, port 5060)
│ ├── external.xml # External profile (trunks, port 5080)
│ └── external/ # Gateway (trunk) definitions
│ └── my_provider.xml # One file per trunk
├── dialplan/ # Call routing rules
│ ├── default.xml # Internal context (phone-to-phone)
│ ├── public.xml # External context (inbound from trunks)
│ └── default/ # Additional dialplan fragments
├── directory/ # User/phone definitions
│ └── default/ # Domain directory
│ ├── 1000.xml # Extension 1000
│ ├── 1001.xml # Extension 1001
│ └── ...
└── lang/ # Language/sound file mappings /var/lib/freeswitch/ # Runtime data
├── db/ # SQLite databases (registrations, etc.)
├── recordings/ # Call recordings
├── storage/ # Voicemail, fax storage
└── sounds/ # Audio/prompt files /var/log/freeswitch/ # Logs
├── freeswitch.log # Main log (rotated)
└── cdr-csv/ # CDR files
/etc/freeswitch/ # All configuration
├── freeswitch.xml # Master config (includes everything)
├── vars.xml # Global variables (domain, passwords, IPs)
├── autoload_configs/ # Module configurations
│ ├── modules.conf.xml # Which modules to load
│ ├── sofia.conf.xml # SIP module config (includes profiles)
│ ├── conference.conf.xml # Conference profiles
│ ├── voicemail.conf.xml # Voicemail settings
│ ├── ivr.conf.xml # IVR menu definitions
│ ├── cdr_csv.conf.xml # CDR output config
│ └── event_socket.conf.xml # ESL listener config
├── sip_profiles/ # SIP profile definitions
│ ├── internal.xml # Internal profile (phones, port 5060)
│ ├── external.xml # External profile (trunks, port 5080)
│ └── external/ # Gateway (trunk) definitions
│ └── my_provider.xml # One file per trunk
├── dialplan/ # Call routing rules
│ ├── default.xml # Internal context (phone-to-phone)
│ ├── public.xml # External context (inbound from trunks)
│ └── default/ # Additional dialplan fragments
├── directory/ # User/phone definitions
│ └── default/ # Domain directory
│ ├── 1000.xml # Extension 1000
│ ├── 1001.xml # Extension 1001
│ └── ...
└── lang/ # Language/sound file mappings /var/lib/freeswitch/ # Runtime data
├── db/ # SQLite databases (registrations, etc.)
├── recordings/ # Call recordings
├── storage/ # Voicemail, fax storage
└── sounds/ # Audio/prompt files /var/log/freeswitch/ # Logs
├── freeswitch.log # Main log (rotated)
└── cdr-csv/ # CDR files
# Install prerequisites
apt-get update && apt-get install -y gnupg2 wget lsb-release # Set your SignalWire PAT
TOKEN="YOUR_SIGNALWIRE_PAT_HERE" # Add SignalWire GPG key
wget --http-user=signalwire --http-password=$TOKEN \ -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \ https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg # Add repository
echo "machine freeswitch.signalwire.com login signalwire password $TOKEN" \ > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] \ https://freeswitch.signalwire.com/repo/deb/debian-release/ bookworm main" \ > /etc/apt/sources.list.d/freeswitch.list
# Install prerequisites
apt-get update && apt-get install -y gnupg2 wget lsb-release # Set your SignalWire PAT
TOKEN="YOUR_SIGNALWIRE_PAT_HERE" # Add SignalWire GPG key
wget --http-user=signalwire --http-password=$TOKEN \ -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \ https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg # Add repository
echo "machine freeswitch.signalwire.com login signalwire password $TOKEN" \ > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] \ https://freeswitch.signalwire.com/repo/deb/debian-release/ bookworm main" \ > /etc/apt/sources.list.d/freeswitch.list
# Install prerequisites
apt-get update && apt-get install -y gnupg2 wget lsb-release # Set your SignalWire PAT
TOKEN="YOUR_SIGNALWIRE_PAT_HERE" # Add SignalWire GPG key
wget --http-user=signalwire --http-password=$TOKEN \ -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \ https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg # Add repository
echo "machine freeswitch.signalwire.com login signalwire password $TOKEN" \ > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] \ https://freeswitch.signalwire.com/repo/deb/debian-release/ bookworm main" \ > /etc/apt/sources.list.d/freeswitch.list
# Install prerequisites
apt-get update && apt-get install -y gnupg2 wget lsb-release # Set your SignalWire PAT
TOKEN="YOUR_SIGNALWIRE_PAT_HERE" # Add SignalWire GPG key
wget --http-user=signalwire --http-password=$TOKEN \ -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \ https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg # Add repository
echo "machine freeswitch.signalwire.com login signalwire password $TOKEN" \ > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] \ https://freeswitch.signalwire.com/repo/deb/debian-release/ noble main" \ > /etc/apt/sources.list.d/freeswitch.list
# Install prerequisites
apt-get update && apt-get install -y gnupg2 wget lsb-release # Set your SignalWire PAT
TOKEN="YOUR_SIGNALWIRE_PAT_HERE" # Add SignalWire GPG key
wget --http-user=signalwire --http-password=$TOKEN \ -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \ https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg # Add repository
echo "machine freeswitch.signalwire.com login signalwire password $TOKEN" \ > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] \ https://freeswitch.signalwire.com/repo/deb/debian-release/ noble main" \ > /etc/apt/sources.list.d/freeswitch.list
# Install prerequisites
apt-get update && apt-get install -y gnupg2 wget lsb-release # Set your SignalWire PAT
TOKEN="YOUR_SIGNALWIRE_PAT_HERE" # Add SignalWire GPG key
wget --http-user=signalwire --http-password=$TOKEN \ -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg \ https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg # Add repository
echo "machine freeswitch.signalwire.com login signalwire password $TOKEN" \ > /etc/apt/auth.conf.d/freeswitch.conf
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] \ https://freeswitch.signalwire.com/repo/deb/debian-release/ noble main" \ > /etc/apt/sources.list.d/freeswitch.list
apt-get update # Full install (recommended for learning — includes all modules)
apt-get install -y freeswitch-meta-all # OR minimal install (production — add modules as needed)
# apt-get install -y freeswitch-meta-vanilla
apt-get update # Full install (recommended for learning — includes all modules)
apt-get install -y freeswitch-meta-all # OR minimal install (production — add modules as needed)
# apt-get install -y freeswitch-meta-vanilla
apt-get update # Full install (recommended for learning — includes all modules)
apt-get install -y freeswitch-meta-all # OR minimal install (production — add modules as needed)
# apt-get install -y freeswitch-meta-vanilla
# Enable and start FreeSWITCH
systemctl enable freeswitch
systemctl start freeswitch # Verify it is running
systemctl status freeswitch # Check ownership (FreeSWITCH runs as freeswitch user)
ls -la /etc/freeswitch/
ls -la /var/lib/freeswitch/
ls -la /var/log/freeswitch/ # Fix ownership if needed
chown -R freeswitch:freeswitch /etc/freeswitch
chown -R freeswitch:freeswitch /var/lib/freeswitch
chown -R freeswitch:freeswitch /var/log/freeswitch
# Enable and start FreeSWITCH
systemctl enable freeswitch
systemctl start freeswitch # Verify it is running
systemctl status freeswitch # Check ownership (FreeSWITCH runs as freeswitch user)
ls -la /etc/freeswitch/
ls -la /var/lib/freeswitch/
ls -la /var/log/freeswitch/ # Fix ownership if needed
chown -R freeswitch:freeswitch /etc/freeswitch
chown -R freeswitch:freeswitch /var/lib/freeswitch
chown -R freeswitch:freeswitch /var/log/freeswitch
# Enable and start FreeSWITCH
systemctl enable freeswitch
systemctl start freeswitch # Verify it is running
systemctl status freeswitch # Check ownership (FreeSWITCH runs as freeswitch user)
ls -la /etc/freeswitch/
ls -la /var/lib/freeswitch/
ls -la /var/log/freeswitch/ # Fix ownership if needed
chown -R freeswitch:freeswitch /etc/freeswitch
chown -R freeswitch:freeswitch /var/lib/freeswitch
chown -R freeswitch:freeswitch /var/log/freeswitch
# Install build dependencies
apt-get update && apt-get install -y \ build-essential cmake automake autoconf libtool pkg-config \ libssl-dev zlib1g-dev libdb-dev libexpat1-dev libcurl4-openssl-dev \ libpcre3-dev libspeex-dev libspeexdsp-dev libsqlite3-dev \ libedit-dev libldns-dev libpq-dev libtiff-dev libjpeg-dev \ libavformat-dev libswscale-dev liblua5.3-dev \ libopus-dev libsndfile1-dev uuid-dev \ python3-dev erlang-dev yasm nasm \ git wget unzip # Clone the repository
cd /usr/local/src
git clone https://github.com/signalwire/freeswitch.git -b v1.10 freeswitch
cd freeswitch # Install libks and signalwire-c dependencies
git clone https://github.com/signalwire/libks.git
cd libks && cmake . -DCMAKE_INSTALL_PREFIX=/usr && make install && cd .. git clone https://github.com/signalwire/signalwire-c.git
cd signalwire-c && cmake . -DCMAKE_INSTALL_PREFIX=/usr && make install && cd .. # Bootstrap and configure
cd /usr/local/src/freeswitch
./bootstrap.sh -j # Edit modules.conf to enable/disable modules before building
# nano modules.conf ./configure --prefix=/usr/local/freeswitch # Build and install
make -j$(nproc)
make install # Install sounds and music on hold
make cd-sounds-install cd-moh-install # Create system user
useradd -r -s /bin/false freeswitch
chown -R freeswitch:freeswitch /usr/local/freeswitch
# Install build dependencies
apt-get update && apt-get install -y \ build-essential cmake automake autoconf libtool pkg-config \ libssl-dev zlib1g-dev libdb-dev libexpat1-dev libcurl4-openssl-dev \ libpcre3-dev libspeex-dev libspeexdsp-dev libsqlite3-dev \ libedit-dev libldns-dev libpq-dev libtiff-dev libjpeg-dev \ libavformat-dev libswscale-dev liblua5.3-dev \ libopus-dev libsndfile1-dev uuid-dev \ python3-dev erlang-dev yasm nasm \ git wget unzip # Clone the repository
cd /usr/local/src
git clone https://github.com/signalwire/freeswitch.git -b v1.10 freeswitch
cd freeswitch # Install libks and signalwire-c dependencies
git clone https://github.com/signalwire/libks.git
cd libks && cmake . -DCMAKE_INSTALL_PREFIX=/usr && make install && cd .. git clone https://github.com/signalwire/signalwire-c.git
cd signalwire-c && cmake . -DCMAKE_INSTALL_PREFIX=/usr && make install && cd .. # Bootstrap and configure
cd /usr/local/src/freeswitch
./bootstrap.sh -j # Edit modules.conf to enable/disable modules before building
# nano modules.conf ./configure --prefix=/usr/local/freeswitch # Build and install
make -j$(nproc)
make install # Install sounds and music on hold
make cd-sounds-install cd-moh-install # Create system user
useradd -r -s /bin/false freeswitch
chown -R freeswitch:freeswitch /usr/local/freeswitch
# Install build dependencies
apt-get update && apt-get install -y \ build-essential cmake automake autoconf libtool pkg-config \ libssl-dev zlib1g-dev libdb-dev libexpat1-dev libcurl4-openssl-dev \ libpcre3-dev libspeex-dev libspeexdsp-dev libsqlite3-dev \ libedit-dev libldns-dev libpq-dev libtiff-dev libjpeg-dev \ libavformat-dev libswscale-dev liblua5.3-dev \ libopus-dev libsndfile1-dev uuid-dev \ python3-dev erlang-dev yasm nasm \ git wget unzip # Clone the repository
cd /usr/local/src
git clone https://github.com/signalwire/freeswitch.git -b v1.10 freeswitch
cd freeswitch # Install libks and signalwire-c dependencies
git clone https://github.com/signalwire/libks.git
cd libks && cmake . -DCMAKE_INSTALL_PREFIX=/usr && make install && cd .. git clone https://github.com/signalwire/signalwire-c.git
cd signalwire-c && cmake . -DCMAKE_INSTALL_PREFIX=/usr && make install && cd .. # Bootstrap and configure
cd /usr/local/src/freeswitch
./bootstrap.sh -j # Edit modules.conf to enable/disable modules before building
# nano modules.conf ./configure --prefix=/usr/local/freeswitch # Build and install
make -j$(nproc)
make install # Install sounds and music on hold
make cd-sounds-install cd-moh-install # Create system user
useradd -r -s /bin/false freeswitch
chown -R freeswitch:freeswitch /usr/local/freeswitch
# SIP signaling (internal profile)
ufw allow 5060/tcp comment "FreeSWITCH SIP TCP internal"
ufw allow 5060/udp comment "FreeSWITCH SIP UDP internal" # SIP signaling (external profile)
ufw allow 5080/tcp comment "FreeSWITCH SIP TCP external"
ufw allow 5080/udp comment "FreeSWITCH SIP UDP external" # RTP media (voice/video)
ufw allow 16384:32768/udp comment "FreeSWITCH RTP media" # ESL (Event Socket Layer) - restrict to trusted IPs only
ufw allow from 10.0.0.0/8 to any port 8021 proto tcp comment "FreeSWITCH ESL" # WebRTC (if using mod_verto)
# ufw allow 8081/tcp comment "FreeSWITCH Verto WSS"
# ufw allow 8082/tcp comment "FreeSWITCH Verto WSS" ufw enable
# SIP signaling (internal profile)
ufw allow 5060/tcp comment "FreeSWITCH SIP TCP internal"
ufw allow 5060/udp comment "FreeSWITCH SIP UDP internal" # SIP signaling (external profile)
ufw allow 5080/tcp comment "FreeSWITCH SIP TCP external"
ufw allow 5080/udp comment "FreeSWITCH SIP UDP external" # RTP media (voice/video)
ufw allow 16384:32768/udp comment "FreeSWITCH RTP media" # ESL (Event Socket Layer) - restrict to trusted IPs only
ufw allow from 10.0.0.0/8 to any port 8021 proto tcp comment "FreeSWITCH ESL" # WebRTC (if using mod_verto)
# ufw allow 8081/tcp comment "FreeSWITCH Verto WSS"
# ufw allow 8082/tcp comment "FreeSWITCH Verto WSS" ufw enable
# SIP signaling (internal profile)
ufw allow 5060/tcp comment "FreeSWITCH SIP TCP internal"
ufw allow 5060/udp comment "FreeSWITCH SIP UDP internal" # SIP signaling (external profile)
ufw allow 5080/tcp comment "FreeSWITCH SIP TCP external"
ufw allow 5080/udp comment "FreeSWITCH SIP UDP external" # RTP media (voice/video)
ufw allow 16384:32768/udp comment "FreeSWITCH RTP media" # ESL (Event Socket Layer) - restrict to trusted IPs only
ufw allow from 10.0.0.0/8 to any port 8021 proto tcp comment "FreeSWITCH ESL" # WebRTC (if using mod_verto)
# ufw allow 8081/tcp comment "FreeSWITCH Verto WSS"
# ufw allow 8082/tcp comment "FreeSWITCH Verto WSS" ufw enable
# SIP
iptables -A INPUT -p udp --dport 5060 -j ACCEPT
iptables -A INPUT -p tcp --dport 5060 -j ACCEPT
iptables -A INPUT -p udp --dport 5080 -j ACCEPT
iptables -A INPUT -p tcp --dport 5080 -j ACCEPT # RTP
iptables -A INPUT -p udp --dport 16384:32768 -j ACCEPT # ESL (restrict source)
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 8021 -j ACCEPT
iptables -A INPUT -p tcp --dport 8021 -j DROP
# SIP
iptables -A INPUT -p udp --dport 5060 -j ACCEPT
iptables -A INPUT -p tcp --dport 5060 -j ACCEPT
iptables -A INPUT -p udp --dport 5080 -j ACCEPT
iptables -A INPUT -p tcp --dport 5080 -j ACCEPT # RTP
iptables -A INPUT -p udp --dport 16384:32768 -j ACCEPT # ESL (restrict source)
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 8021 -j ACCEPT
iptables -A INPUT -p tcp --dport 8021 -j DROP
# SIP
iptables -A INPUT -p udp --dport 5060 -j ACCEPT
iptables -A INPUT -p tcp --dport 5060 -j ACCEPT
iptables -A INPUT -p udp --dport 5080 -j ACCEPT
iptables -A INPUT -p tcp --dport 5080 -j ACCEPT # RTP
iptables -A INPUT -p udp --dport 16384:32768 -j ACCEPT # ESL (restrict source)
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 8021 -j ACCEPT
iptables -A INPUT -p tcp --dport 8021 -j DROP
# Connect to FreeSWITCH CLI
fs_cli # Inside fs_cli, check status
freeswitch@server> status # Expected output:
# UP 0 years, 0 days, 0 hours, 5 minutes, 23 seconds, 456 milliseconds, 789 microseconds
# FreeSWITCH (Version 1.10.x ...) is ready
# 0 session(s) since startup
# 0 session(s) - peak 0, last 5min 0
# 0 session(s) per Sec out of max 30, peak 0, last 5min 0
# 1000 session(s) max
# min idle cpu 0.00/99.67 # Check SIP profiles
freeswitch@server> sofia status # Expected output:
# Name Type Data State
# =================================================================================================
# internal profile sip:mod_sofia@YOUR_SERVER_IP:5060 RUNNING (0)
# external profile sip:mod_sofia@YOUR_SERVER_IP:5080 RUNNING (0)
# ... (other profiles) # Check loaded modules
freeswitch@server> module_exists mod_sofia
# true # Exit CLI
freeswitch@server> /exit
# Connect to FreeSWITCH CLI
fs_cli # Inside fs_cli, check status
freeswitch@server> status # Expected output:
# UP 0 years, 0 days, 0 hours, 5 minutes, 23 seconds, 456 milliseconds, 789 microseconds
# FreeSWITCH (Version 1.10.x ...) is ready
# 0 session(s) since startup
# 0 session(s) - peak 0, last 5min 0
# 0 session(s) per Sec out of max 30, peak 0, last 5min 0
# 1000 session(s) max
# min idle cpu 0.00/99.67 # Check SIP profiles
freeswitch@server> sofia status # Expected output:
# Name Type Data State
# =================================================================================================
# internal profile sip:mod_sofia@YOUR_SERVER_IP:5060 RUNNING (0)
# external profile sip:mod_sofia@YOUR_SERVER_IP:5080 RUNNING (0)
# ... (other profiles) # Check loaded modules
freeswitch@server> module_exists mod_sofia
# true # Exit CLI
freeswitch@server> /exit
# Connect to FreeSWITCH CLI
fs_cli # Inside fs_cli, check status
freeswitch@server> status # Expected output:
# UP 0 years, 0 days, 0 hours, 5 minutes, 23 seconds, 456 milliseconds, 789 microseconds
# FreeSWITCH (Version 1.10.x ...) is ready
# 0 session(s) since startup
# 0 session(s) - peak 0, last 5min 0
# 0 session(s) per Sec out of max 30, peak 0, last 5min 0
# 1000 session(s) max
# min idle cpu 0.00/99.67 # Check SIP profiles
freeswitch@server> sofia status # Expected output:
# Name Type Data State
# =================================================================================================
# internal profile sip:mod_sofia@YOUR_SERVER_IP:5060 RUNNING (0)
# external profile sip:mod_sofia@YOUR_SERVER_IP:5080 RUNNING (0)
# ... (other profiles) # Check loaded modules
freeswitch@server> module_exists mod_sofia
# true # Exit CLI
freeswitch@server> /exit
# Edit global variables
nano /etc/freeswitch/vars.xml
# Edit global variables
nano /etc/freeswitch/vars.xml
# Edit global variables
nano /etc/freeswitch/vars.xml
<!-- CHANGE THIS — default password for all extensions -->
<X-PRE-PROCESS cmd="set" data="default_password=PUT_A_STRONG_PASSWORD_HERE"/> <!-- Domain — set to your server IP or FQDN -->
<X-PRE-PROCESS cmd="set" data="domain=$${local_ip_v4}"/>
<!-- CHANGE THIS — default password for all extensions -->
<X-PRE-PROCESS cmd="set" data="default_password=PUT_A_STRONG_PASSWORD_HERE"/> <!-- Domain — set to your server IP or FQDN -->
<X-PRE-PROCESS cmd="set" data="domain=$${local_ip_v4}"/>
<!-- CHANGE THIS — default password for all extensions -->
<X-PRE-PROCESS cmd="set" data="default_password=PUT_A_STRONG_PASSWORD_HERE"/> <!-- Domain — set to your server IP or FQDN -->
<X-PRE-PROCESS cmd="set" data="domain=$${local_ip_v4}"/>
nano /etc/freeswitch/autoload_configs/event_socket.conf.xml
nano /etc/freeswitch/autoload_configs/event_socket.conf.xml
nano /etc/freeswitch/autoload_configs/event_socket.conf.xml
<configuration name="event_socket.conf" description="Socket Client"> <settings> <param name="nat-map" value="false"/> <param name="listen-ip" value="127.0.0.1"/> <param name="listen-port" value="8021"/> <!-- CHANGE THIS from ClueCon --> <param name="password" value="YOUR_SECURE_ESL_PASSWORD"/> <!--<param name="apply-inbound-acl" value="loopback.auto"/>--> </settings>
</configuration>
<configuration name="event_socket.conf" description="Socket Client"> <settings> <param name="nat-map" value="false"/> <param name="listen-ip" value="127.0.0.1"/> <param name="listen-port" value="8021"/> <!-- CHANGE THIS from ClueCon --> <param name="password" value="YOUR_SECURE_ESL_PASSWORD"/> <!--<param name="apply-inbound-acl" value="loopback.auto"/>--> </settings>
</configuration>
<configuration name="event_socket.conf" description="Socket Client"> <settings> <param name="nat-map" value="false"/> <param name="listen-ip" value="127.0.0.1"/> <param name="listen-port" value="8021"/> <!-- CHANGE THIS from ClueCon --> <param name="password" value="YOUR_SECURE_ESL_PASSWORD"/> <!--<param name="apply-inbound-acl" value="loopback.auto"/>--> </settings>
</configuration>
# From fs_cli
fs_cli -x "reloadxml" # Or restart the service
systemctl restart freeswitch
# From fs_cli
fs_cli -x "reloadxml" # Or restart the service
systemctl restart freeswitch
# From fs_cli
fs_cli -x "reloadxml" # Or restart the service
systemctl restart freeswitch
<profile name="internal"> <settings> <!-- Network --> <param name="sip-ip" value="$${local_ip_v4}"/> <param name="sip-port" value="5060"/> <param name="rtp-ip" value="$${local_ip_v4}"/> <!-- NAT: Set this to your public IP if behind NAT --> <!-- <param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/> --> <!-- <param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/> --> <!-- Dialplan context for calls arriving on this profile --> <param name="context" value="default"/> <!-- Codec preferences (in order) --> <param name="inbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <param name="outbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <!-- DTMF handling --> <param name="dtmf-type" value="rfc2833"/> <!-- Registration --> <param name="inbound-reg-force-matching-username" value="true"/> <param name="auth-calls" value="true"/> <param name="apply-nat-acl" value="nat.auto"/> <!-- Recording --> <param name="record-path" value="$${recordings_dir}"/> <param name="record-template" value="${caller_id_number}.${target_domain}.${strftime(%Y-%m-%d-%H-%M-%S)}.wav"/> <!-- Timers --> <param name="session-timeout" value="1800"/> <param name="rtp-timeout-sec" value="300"/> <param name="rtp-hold-timeout-sec" value="1800"/> <!-- Hold music --> <param name="hold-music" value="$${hold_music}"/> </settings>
</profile>
<profile name="internal"> <settings> <!-- Network --> <param name="sip-ip" value="$${local_ip_v4}"/> <param name="sip-port" value="5060"/> <param name="rtp-ip" value="$${local_ip_v4}"/> <!-- NAT: Set this to your public IP if behind NAT --> <!-- <param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/> --> <!-- <param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/> --> <!-- Dialplan context for calls arriving on this profile --> <param name="context" value="default"/> <!-- Codec preferences (in order) --> <param name="inbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <param name="outbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <!-- DTMF handling --> <param name="dtmf-type" value="rfc2833"/> <!-- Registration --> <param name="inbound-reg-force-matching-username" value="true"/> <param name="auth-calls" value="true"/> <param name="apply-nat-acl" value="nat.auto"/> <!-- Recording --> <param name="record-path" value="$${recordings_dir}"/> <param name="record-template" value="${caller_id_number}.${target_domain}.${strftime(%Y-%m-%d-%H-%M-%S)}.wav"/> <!-- Timers --> <param name="session-timeout" value="1800"/> <param name="rtp-timeout-sec" value="300"/> <param name="rtp-hold-timeout-sec" value="1800"/> <!-- Hold music --> <param name="hold-music" value="$${hold_music}"/> </settings>
</profile>
<profile name="internal"> <settings> <!-- Network --> <param name="sip-ip" value="$${local_ip_v4}"/> <param name="sip-port" value="5060"/> <param name="rtp-ip" value="$${local_ip_v4}"/> <!-- NAT: Set this to your public IP if behind NAT --> <!-- <param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/> --> <!-- <param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/> --> <!-- Dialplan context for calls arriving on this profile --> <param name="context" value="default"/> <!-- Codec preferences (in order) --> <param name="inbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <param name="outbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <!-- DTMF handling --> <param name="dtmf-type" value="rfc2833"/> <!-- Registration --> <param name="inbound-reg-force-matching-username" value="true"/> <param name="auth-calls" value="true"/> <param name="apply-nat-acl" value="nat.auto"/> <!-- Recording --> <param name="record-path" value="$${recordings_dir}"/> <param name="record-template" value="${caller_id_number}.${target_domain}.${strftime(%Y-%m-%d-%H-%M-%S)}.wav"/> <!-- Timers --> <param name="session-timeout" value="1800"/> <param name="rtp-timeout-sec" value="300"/> <param name="rtp-hold-timeout-sec" value="1800"/> <!-- Hold music --> <param name="hold-music" value="$${hold_music}"/> </settings>
</profile>
<profile name="external"> <settings> <param name="sip-ip" value="$${local_ip_v4}"/> <param name="sip-port" value="5080"/> <param name="rtp-ip" value="$${local_ip_v4}"/> <!-- NAT: uncomment and set if behind NAT --> <!-- <param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/> --> <!-- <param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/> --> <!-- Inbound calls from trunks go to 'public' context --> <param name="context" value="public"/> <!-- Codecs --> <param name="inbound-codec-string" value="PCMU,PCMA,G722"/> <param name="outbound-codec-string" value="PCMU,PCMA,G722"/> <!-- Do NOT require authentication (trunks use IP-based auth) --> <param name="auth-calls" value="false"/> <!-- DTMF --> <param name="dtmf-type" value="rfc2833"/> <!-- Aggressively reclaim failed channels --> <param name="rtp-timeout-sec" value="300"/> </settings> <!-- Gateway definitions are in sip_profiles/external/ directory --> <gateways> <X-PRE-PROCESS cmd="include" data="external/*.xml"/> </gateways>
</profile>
<profile name="external"> <settings> <param name="sip-ip" value="$${local_ip_v4}"/> <param name="sip-port" value="5080"/> <param name="rtp-ip" value="$${local_ip_v4}"/> <!-- NAT: uncomment and set if behind NAT --> <!-- <param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/> --> <!-- <param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/> --> <!-- Inbound calls from trunks go to 'public' context --> <param name="context" value="public"/> <!-- Codecs --> <param name="inbound-codec-string" value="PCMU,PCMA,G722"/> <param name="outbound-codec-string" value="PCMU,PCMA,G722"/> <!-- Do NOT require authentication (trunks use IP-based auth) --> <param name="auth-calls" value="false"/> <!-- DTMF --> <param name="dtmf-type" value="rfc2833"/> <!-- Aggressively reclaim failed channels --> <param name="rtp-timeout-sec" value="300"/> </settings> <!-- Gateway definitions are in sip_profiles/external/ directory --> <gateways> <X-PRE-PROCESS cmd="include" data="external/*.xml"/> </gateways>
</profile>
<profile name="external"> <settings> <param name="sip-ip" value="$${local_ip_v4}"/> <param name="sip-port" value="5080"/> <param name="rtp-ip" value="$${local_ip_v4}"/> <!-- NAT: uncomment and set if behind NAT --> <!-- <param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/> --> <!-- <param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/> --> <!-- Inbound calls from trunks go to 'public' context --> <param name="context" value="public"/> <!-- Codecs --> <param name="inbound-codec-string" value="PCMU,PCMA,G722"/> <param name="outbound-codec-string" value="PCMU,PCMA,G722"/> <!-- Do NOT require authentication (trunks use IP-based auth) --> <param name="auth-calls" value="false"/> <!-- DTMF --> <param name="dtmf-type" value="rfc2833"/> <!-- Aggressively reclaim failed channels --> <param name="rtp-timeout-sec" value="300"/> </settings> <!-- Gateway definitions are in sip_profiles/external/ directory --> <gateways> <X-PRE-PROCESS cmd="include" data="external/*.xml"/> </gateways>
</profile>
<include> <user id="1001"> <params> <param name="password" value="Str0ng_P@ss_1001!"/> <param name="vm-password" value="1001"/> </params> <variables> <variable name="toll_allow" value="domestic,international,local"/> <variable name="accountcode" value="1001"/> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="John Smith"/> <variable name="effective_caller_id_number" value="1001"/> <variable name="outbound_caller_id_name" value="My Company"/> <variable name="outbound_caller_id_number" value="15551234567"/> <variable name="callgroup" value="sales"/> </variables> </user>
</include>
<include> <user id="1001"> <params> <param name="password" value="Str0ng_P@ss_1001!"/> <param name="vm-password" value="1001"/> </params> <variables> <variable name="toll_allow" value="domestic,international,local"/> <variable name="accountcode" value="1001"/> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="John Smith"/> <variable name="effective_caller_id_number" value="1001"/> <variable name="outbound_caller_id_name" value="My Company"/> <variable name="outbound_caller_id_number" value="15551234567"/> <variable name="callgroup" value="sales"/> </variables> </user>
</include>
<include> <user id="1001"> <params> <param name="password" value="Str0ng_P@ss_1001!"/> <param name="vm-password" value="1001"/> </params> <variables> <variable name="toll_allow" value="domestic,international,local"/> <variable name="accountcode" value="1001"/> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="John Smith"/> <variable name="effective_caller_id_number" value="1001"/> <variable name="outbound_caller_id_name" value="My Company"/> <variable name="outbound_caller_id_number" value="15551234567"/> <variable name="callgroup" value="sales"/> </variables> </user>
</include>
#!/bin/bash
# create-extensions.sh
# Creates 10 SIP extensions (1001-1010) for FreeSWITCH DIRECTORY="/etc/freeswitch/directory/default" declare -A USERS=( [1001]="Alice Johnson:sales" [1002]="Bob Williams:sales" [1003]="Carol Davis:sales" [1004]="Dan Miller:support" [1005]="Eve Wilson:support" [1006]="Frank Brown:support" [1007]="Grace Taylor:billing" [1008]="Hank Anderson:billing" [1009]="Ivy Martinez:management" [1010]="Jack Thompson:management"
) for EXT in "${!USERS[@]}"; do IFS=':' read -r NAME GROUP <<< "${USERS[$EXT]}" # Generate a random password PASS=$(openssl rand -base64 12 | tr -dc 'A-Za-z0-9' | head -c 16) cat > "${DIRECTORY}/${EXT}.xml" << XMLEOF
<include> <user id="${EXT}"> <params> <param name="password" value="${PASS}"/> <param name="vm-password" value="${EXT}"/> </params> <variables> <variable name="toll_allow" value="domestic,local"/> <variable name="accountcode" value="${EXT}"/> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="${NAME}"/> <variable name="effective_caller_id_number" value="${EXT}"/> <variable name="outbound_caller_id_name" value="My Company"/> <variable name="outbound_caller_id_number" value="15551234567"/> <variable name="callgroup" value="${GROUP}"/> </variables> </user>
</include>
XMLEOF echo "Created ${EXT} — ${NAME} (${GROUP}) — Password: ${PASS}"
done chown -R freeswitch:freeswitch "${DIRECTORY}"
echo ""
echo "Done. Run 'fs_cli -x reloadxml' to apply."
echo "IMPORTANT: Save the passwords above — they will not be displayed again."
#!/bin/bash
# create-extensions.sh
# Creates 10 SIP extensions (1001-1010) for FreeSWITCH DIRECTORY="/etc/freeswitch/directory/default" declare -A USERS=( [1001]="Alice Johnson:sales" [1002]="Bob Williams:sales" [1003]="Carol Davis:sales" [1004]="Dan Miller:support" [1005]="Eve Wilson:support" [1006]="Frank Brown:support" [1007]="Grace Taylor:billing" [1008]="Hank Anderson:billing" [1009]="Ivy Martinez:management" [1010]="Jack Thompson:management"
) for EXT in "${!USERS[@]}"; do IFS=':' read -r NAME GROUP <<< "${USERS[$EXT]}" # Generate a random password PASS=$(openssl rand -base64 12 | tr -dc 'A-Za-z0-9' | head -c 16) cat > "${DIRECTORY}/${EXT}.xml" << XMLEOF
<include> <user id="${EXT}"> <params> <param name="password" value="${PASS}"/> <param name="vm-password" value="${EXT}"/> </params> <variables> <variable name="toll_allow" value="domestic,local"/> <variable name="accountcode" value="${EXT}"/> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="${NAME}"/> <variable name="effective_caller_id_number" value="${EXT}"/> <variable name="outbound_caller_id_name" value="My Company"/> <variable name="outbound_caller_id_number" value="15551234567"/> <variable name="callgroup" value="${GROUP}"/> </variables> </user>
</include>
XMLEOF echo "Created ${EXT} — ${NAME} (${GROUP}) — Password: ${PASS}"
done chown -R freeswitch:freeswitch "${DIRECTORY}"
echo ""
echo "Done. Run 'fs_cli -x reloadxml' to apply."
echo "IMPORTANT: Save the passwords above — they will not be displayed again."
#!/bin/bash
# create-extensions.sh
# Creates 10 SIP extensions (1001-1010) for FreeSWITCH DIRECTORY="/etc/freeswitch/directory/default" declare -A USERS=( [1001]="Alice Johnson:sales" [1002]="Bob Williams:sales" [1003]="Carol Davis:sales" [1004]="Dan Miller:support" [1005]="Eve Wilson:support" [1006]="Frank Brown:support" [1007]="Grace Taylor:billing" [1008]="Hank Anderson:billing" [1009]="Ivy Martinez:management" [1010]="Jack Thompson:management"
) for EXT in "${!USERS[@]}"; do IFS=':' read -r NAME GROUP <<< "${USERS[$EXT]}" # Generate a random password PASS=$(openssl rand -base64 12 | tr -dc 'A-Za-z0-9' | head -c 16) cat > "${DIRECTORY}/${EXT}.xml" << XMLEOF
<include> <user id="${EXT}"> <params> <param name="password" value="${PASS}"/> <param name="vm-password" value="${EXT}"/> </params> <variables> <variable name="toll_allow" value="domestic,local"/> <variable name="accountcode" value="${EXT}"/> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="${NAME}"/> <variable name="effective_caller_id_number" value="${EXT}"/> <variable name="outbound_caller_id_name" value="My Company"/> <variable name="outbound_caller_id_number" value="15551234567"/> <variable name="callgroup" value="${GROUP}"/> </variables> </user>
</include>
XMLEOF echo "Created ${EXT} — ${NAME} (${GROUP}) — Password: ${PASS}"
done chown -R freeswitch:freeswitch "${DIRECTORY}"
echo ""
echo "Done. Run 'fs_cli -x reloadxml' to apply."
echo "IMPORTANT: Save the passwords above — they will not be displayed again."
chmod +x create-extensions.sh
./create-extensions.sh
chmod +x create-extensions.sh
./create-extensions.sh
chmod +x create-extensions.sh
./create-extensions.sh
<include> <gateway name="my_provider"> <!-- Provider credentials --> <param name="username" value="your_sip_username"/> <param name="password" value="your_sip_password"/> <param name="realm" value="sip.provider.com"/> <param name="proxy" value="sip.provider.com"/> <!-- Registration (some providers require it, some use IP auth) --> <param name="register" value="true"/> <param name="register-transport" value="udp"/> <!-- Caller ID --> <param name="caller-id-in-from" value="true"/> <!-- Retry on failure --> <param name="retry-seconds" value="30"/> <!-- Codec preferences for this trunk --> <param name="codec-prefs" value="PCMU,PCMA,G722"/> <!-- Ping the provider to detect failures --> <param name="ping" value="25"/> <param name="ping-max" value="3"/> <param name="ping-min" value="1"/> </gateway>
</include>
<include> <gateway name="my_provider"> <!-- Provider credentials --> <param name="username" value="your_sip_username"/> <param name="password" value="your_sip_password"/> <param name="realm" value="sip.provider.com"/> <param name="proxy" value="sip.provider.com"/> <!-- Registration (some providers require it, some use IP auth) --> <param name="register" value="true"/> <param name="register-transport" value="udp"/> <!-- Caller ID --> <param name="caller-id-in-from" value="true"/> <!-- Retry on failure --> <param name="retry-seconds" value="30"/> <!-- Codec preferences for this trunk --> <param name="codec-prefs" value="PCMU,PCMA,G722"/> <!-- Ping the provider to detect failures --> <param name="ping" value="25"/> <param name="ping-max" value="3"/> <param name="ping-min" value="1"/> </gateway>
</include>
<include> <gateway name="my_provider"> <!-- Provider credentials --> <param name="username" value="your_sip_username"/> <param name="password" value="your_sip_password"/> <param name="realm" value="sip.provider.com"/> <param name="proxy" value="sip.provider.com"/> <!-- Registration (some providers require it, some use IP auth) --> <param name="register" value="true"/> <param name="register-transport" value="udp"/> <!-- Caller ID --> <param name="caller-id-in-from" value="true"/> <!-- Retry on failure --> <param name="retry-seconds" value="30"/> <!-- Codec preferences for this trunk --> <param name="codec-prefs" value="PCMU,PCMA,G722"/> <!-- Ping the provider to detect failures --> <param name="ping" value="25"/> <param name="ping-max" value="3"/> <param name="ping-min" value="1"/> </gateway>
</include>
<include> <gateway name="ip_auth_provider"> <param name="username" value="not_used"/> <param name="password" value="not_used"/> <param name="realm" value="sip.provider.com"/> <param name="proxy" value="sip.provider.com"/> <param name="register" value="false"/> <param name="caller-id-in-from" value="true"/> </gateway>
</include>
<include> <gateway name="ip_auth_provider"> <param name="username" value="not_used"/> <param name="password" value="not_used"/> <param name="realm" value="sip.provider.com"/> <param name="proxy" value="sip.provider.com"/> <param name="register" value="false"/> <param name="caller-id-in-from" value="true"/> </gateway>
</include>
<include> <gateway name="ip_auth_provider"> <param name="username" value="not_used"/> <param name="password" value="not_used"/> <param name="realm" value="sip.provider.com"/> <param name="proxy" value="sip.provider.com"/> <param name="register" value="false"/> <param name="caller-id-in-from" value="true"/> </gateway>
</include>
# From fs_cli
fs_cli -x "sofia profile external rescan" # Check gateway status
fs_cli -x "sofia status gateway my_provider" # Expected output:
# Name my_provider
# Profile external
# Scheme sip
# Realm sip.provider.com
# Username your_sip_username
# ...
# State REGED <-- Successfully registered
# Status UP
# From fs_cli
fs_cli -x "sofia profile external rescan" # Check gateway status
fs_cli -x "sofia status gateway my_provider" # Expected output:
# Name my_provider
# Profile external
# Scheme sip
# Realm sip.provider.com
# Username your_sip_username
# ...
# State REGED <-- Successfully registered
# Status UP
# From fs_cli
fs_cli -x "sofia profile external rescan" # Check gateway status
fs_cli -x "sofia status gateway my_provider" # Expected output:
# Name my_provider
# Profile external
# Scheme sip
# Realm sip.provider.com
# Username your_sip_username
# ...
# State REGED <-- Successfully registered
# Status UP
<include> <context name="public"> <!-- Route DID +15551234567 to extension 1001 --> <extension name="inbound_main"> <condition field="destination_number" expression="^(\+?1?5551234567)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="1001 XML default"/> </condition> </extension> <!-- Route DID +15559876543 to IVR --> <extension name="inbound_ivr"> <condition field="destination_number" expression="^(\+?1?5559876543)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="5000 XML default"/> </condition> </extension> <!-- Route DID +15555551234 to ring group (sales) --> <extension name="inbound_sales"> <condition field="destination_number" expression="^(\+?1?5555551234)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="9001 XML default"/> </condition> </extension> <!-- Catch-all: reject unknown DIDs --> <extension name="public_reject"> <condition field="destination_number" expression="^(.*)$"> <action application="log" data="WARNING Rejecting unknown inbound DID: ${destination_number} from ${sip_from_uri}"/> <action application="hangup" data="CALL_REJECTED"/> </condition> </extension> </context>
</include>
<include> <context name="public"> <!-- Route DID +15551234567 to extension 1001 --> <extension name="inbound_main"> <condition field="destination_number" expression="^(\+?1?5551234567)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="1001 XML default"/> </condition> </extension> <!-- Route DID +15559876543 to IVR --> <extension name="inbound_ivr"> <condition field="destination_number" expression="^(\+?1?5559876543)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="5000 XML default"/> </condition> </extension> <!-- Route DID +15555551234 to ring group (sales) --> <extension name="inbound_sales"> <condition field="destination_number" expression="^(\+?1?5555551234)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="9001 XML default"/> </condition> </extension> <!-- Catch-all: reject unknown DIDs --> <extension name="public_reject"> <condition field="destination_number" expression="^(.*)$"> <action application="log" data="WARNING Rejecting unknown inbound DID: ${destination_number} from ${sip_from_uri}"/> <action application="hangup" data="CALL_REJECTED"/> </condition> </extension> </context>
</include>
<include> <context name="public"> <!-- Route DID +15551234567 to extension 1001 --> <extension name="inbound_main"> <condition field="destination_number" expression="^(\+?1?5551234567)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="1001 XML default"/> </condition> </extension> <!-- Route DID +15559876543 to IVR --> <extension name="inbound_ivr"> <condition field="destination_number" expression="^(\+?1?5559876543)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="5000 XML default"/> </condition> </extension> <!-- Route DID +15555551234 to ring group (sales) --> <extension name="inbound_sales"> <condition field="destination_number" expression="^(\+?1?5555551234)$"> <action application="set" data="domain_name=$${domain}"/> <action application="transfer" data="9001 XML default"/> </condition> </extension> <!-- Catch-all: reject unknown DIDs --> <extension name="public_reject"> <condition field="destination_number" expression="^(.*)$"> <action application="log" data="WARNING Rejecting unknown inbound DID: ${destination_number} from ${sip_from_uri}"/> <action application="hangup" data="CALL_REJECTED"/> </condition> </extension> </context>
</include>
<X-PRE-PROCESS cmd="set" data="external_sip_ip=YOUR_PUBLIC_IP"/>
<X-PRE-PROCESS cmd="set" data="external_rtp_ip=YOUR_PUBLIC_IP"/>
<X-PRE-PROCESS cmd="set" data="external_sip_ip=YOUR_PUBLIC_IP"/>
<X-PRE-PROCESS cmd="set" data="external_rtp_ip=YOUR_PUBLIC_IP"/>
<X-PRE-PROCESS cmd="set" data="external_sip_ip=YOUR_PUBLIC_IP"/>
<X-PRE-PROCESS cmd="set" data="external_rtp_ip=YOUR_PUBLIC_IP"/>
<!-- Replace the sip-ip and rtp-ip lines with: -->
<param name="sip-ip" value="$${local_ip_v4}"/>
<param name="ext-sip-ip" value="$${external_sip_ip}"/>
<param name="rtp-ip" value="$${local_ip_v4}"/>
<param name="ext-rtp-ip" value="$${external_rtp_ip}"/> <!-- Enable NAT handling -->
<param name="apply-nat-acl" value="nat.auto"/>
<param name="aggressive-nat-detection" value="true"/>
<param name="local-network-acl" value="localnet.auto"/>
<!-- Replace the sip-ip and rtp-ip lines with: -->
<param name="sip-ip" value="$${local_ip_v4}"/>
<param name="ext-sip-ip" value="$${external_sip_ip}"/>
<param name="rtp-ip" value="$${local_ip_v4}"/>
<param name="ext-rtp-ip" value="$${external_rtp_ip}"/> <!-- Enable NAT handling -->
<param name="apply-nat-acl" value="nat.auto"/>
<param name="aggressive-nat-detection" value="true"/>
<param name="local-network-acl" value="localnet.auto"/>
<!-- Replace the sip-ip and rtp-ip lines with: -->
<param name="sip-ip" value="$${local_ip_v4}"/>
<param name="ext-sip-ip" value="$${external_sip_ip}"/>
<param name="rtp-ip" value="$${local_ip_v4}"/>
<param name="ext-rtp-ip" value="$${external_rtp_ip}"/> <!-- Enable NAT handling -->
<param name="apply-nat-acl" value="nat.auto"/>
<param name="aggressive-nat-detection" value="true"/>
<param name="local-network-acl" value="localnet.auto"/>
fs_cli -x "sofia status profile internal reg" # Output shows registered endpoints:
# Call-ID: xxxxx@yyyy
# User: 1001@YOUR_SERVER_IP
# Contact: "Alice Johnson" <sip:1001@phone_ip:port>
# Agent: Ooh, a softphone!
# Status: Registered(UDP)(unknown) EXP(2024-01-01 12:00:00)
# ...
fs_cli -x "sofia status profile internal reg" # Output shows registered endpoints:
# Call-ID: xxxxx@yyyy
# User: 1001@YOUR_SERVER_IP
# Contact: "Alice Johnson" <sip:1001@phone_ip:port>
# Agent: Ooh, a softphone!
# Status: Registered(UDP)(unknown) EXP(2024-01-01 12:00:00)
# ...
fs_cli -x "sofia status profile internal reg" # Output shows registered endpoints:
# Call-ID: xxxxx@yyyy
# User: 1001@YOUR_SERVER_IP
# Contact: "Alice Johnson" <sip:1001@phone_ip:port>
# Agent: Ooh, a softphone!
# Status: Registered(UDP)(unknown) EXP(2024-01-01 12:00:00)
# ...
<context name="default"> <!-- Container: who can use these rules --> <extension name="my_rule"> <!-- Named rule --> <condition field="X" expression="regex"> <!-- When to match --> <action application="Y" data="Z"/> <!-- What to do --> <action application="Y2" data="Z2"/> <!-- Actions run in sequence --> </condition> </extension>
</context>
<context name="default"> <!-- Container: who can use these rules --> <extension name="my_rule"> <!-- Named rule --> <condition field="X" expression="regex"> <!-- When to match --> <action application="Y" data="Z"/> <!-- What to do --> <action application="Y2" data="Z2"/> <!-- Actions run in sequence --> </condition> </extension>
</context>
<context name="default"> <!-- Container: who can use these rules --> <extension name="my_rule"> <!-- Named rule --> <condition field="X" expression="regex"> <!-- When to match --> <action application="Y" data="Z"/> <!-- What to do --> <action application="Y2" data="Z2"/> <!-- Actions run in sequence --> </condition> </extension>
</context>
^1001$ — Exact match for "1001"
^(10[0-1][0-9])$ — Match 1000-1019
^(\d{10})$ — Match any 10-digit number
^9(\d+)$ — Match 9 + any digits (capture digits after 9)
^(\+?1?\d{10})$ — Match with optional +1 prefix
^.*$ — Match anything (catch-all)
^1001$ — Exact match for "1001"
^(10[0-1][0-9])$ — Match 1000-1019
^(\d{10})$ — Match any 10-digit number
^9(\d+)$ — Match 9 + any digits (capture digits after 9)
^(\+?1?\d{10})$ — Match with optional +1 prefix
^.*$ — Match anything (catch-all)
^1001$ — Exact match for "1001"
^(10[0-1][0-9])$ — Match 1000-1019
^(\d{10})$ — Match any 10-digit number
^9(\d+)$ — Match 9 + any digits (capture digits after 9)
^(\+?1?\d{10})$ — Match with optional +1 prefix
^.*$ — Match anything (catch-all)
<!-- Set a variable -->
<action application="set" data="my_var=hello"/> <!-- Use a variable in an action -->
<action application="log" data="INFO The value is ${my_var}"/> <!-- Use a variable in a condition -->
<condition field="${my_var}" expression="^(hello)$">
<!-- Set a variable -->
<action application="set" data="my_var=hello"/> <!-- Use a variable in an action -->
<action application="log" data="INFO The value is ${my_var}"/> <!-- Use a variable in a condition -->
<condition field="${my_var}" expression="^(hello)$">
<!-- Set a variable -->
<action application="set" data="my_var=hello"/> <!-- Use a variable in an action -->
<action application="log" data="INFO The value is ${my_var}"/> <!-- Use a variable in a condition -->
<condition field="${my_var}" expression="^(hello)$">
<extension name="business_hours"> <condition field="time_of_day" expression="09:00-17:30"> <!-- This runs during business hours --> <action application="transfer" data="5000 XML default"/> <!-- This runs OUTSIDE business hours --> <anti-action application="playback" data="ivr/ivr-please_call_back_during_business_hours.wav"/> <anti-action application="voicemail" data="default $${domain} 1001"/> </condition>
</extension>
<extension name="business_hours"> <condition field="time_of_day" expression="09:00-17:30"> <!-- This runs during business hours --> <action application="transfer" data="5000 XML default"/> <!-- This runs OUTSIDE business hours --> <anti-action application="playback" data="ivr/ivr-please_call_back_during_business_hours.wav"/> <anti-action application="voicemail" data="default $${domain} 1001"/> </condition>
</extension>
<extension name="business_hours"> <condition field="time_of_day" expression="09:00-17:30"> <!-- This runs during business hours --> <action application="transfer" data="5000 XML default"/> <!-- This runs OUTSIDE business hours --> <anti-action application="playback" data="ivr/ivr-please_call_back_during_business_hours.wav"/> <anti-action application="voicemail" data="default $${domain} 1001"/> </condition>
</extension>
<include> <context name="default"> <!-- ============================================ --> <!-- INTERNAL EXTENSION CALLING (1001-1099) --> <!-- ============================================ --> <extension name="internal_extensions"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <action application="export" data="dialed_extension=$1"/> <!-- Set ring timeout to 30 seconds --> <action application="set" data="call_timeout=30"/> <!-- Set caller ID --> <action application="set" data="effective_caller_id_name=${outbound_caller_id_name}"/> <action application="set" data="effective_caller_id_number=${outbound_caller_id_number}"/> <!-- Ring the extension, then send to voicemail on no answer --> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="continue_on_fail=true"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> <!-- No answer — go to voicemail --> <action application="answer"/> <action application="sleep" data="1000"/> <action application="voicemail" data="default $${domain} ${dialed_extension}"/> </condition> </extension> <!-- ============================================ --> <!-- RING GROUPS --> <!-- ============================================ --> <!-- Sales ring group (9001) — ring all sales phones simultaneously --> <extension name="ring_group_sales"> <condition field="destination_number" expression="^(9001)$"> <action application="set" data="call_timeout=25"/> <action application="set" data="continue_on_fail=true"/> <action application="set" data="hangup_after_bridge=true"/> <action application="bridge" data="user/1001@$${domain},user/1002@$${domain},user/1003@$${domain}"/> <!-- All busy/unavailable — voicemail for sales --> <action application="voicemail" data="default $${domain} 1001"/> </condition> </extension> <!-- Support ring group (9002) — sequential ring (try each for 15s) --> <extension name="ring_group_support"> <condition field="destination_number" expression="^(9002)$"> <action application="set" data="continue_on_fail=true"/> <action application="set" data="hangup_after_bridge=true"/> <!-- Try first support agent --> <action application="set" data="call_timeout=15"/> <action application="bridge" data="user/1004@$${domain}"/> <!-- Try second --> <action application="bridge" data="user/1005@$${domain}"/> <!-- Try third --> <action application="bridge" data="user/1006@$${domain}"/> <!-- All unavailable --> <action application="voicemail" data="default $${domain} 1004"/> </condition> </extension> <!-- ============================================ --> <!-- OUTBOUND CALLS VIA TRUNK --> <!-- ============================================ --> <!-- Local/National calls: dial 9 + number --> <extension name="outbound_national"> <condition field="destination_number" expression="^9(\d{10,11})$"> <action application="set" data="effective_caller_id_number=15551234567"/> <action application="set" data="effective_caller_id_name=My Company"/> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="call_timeout=60"/> <action application="bridge" data="sofia/gateway/my_provider/$1"/> </condition> </extension> <!-- International calls: dial 00 + country code + number --> <extension name="outbound_international"> <condition field="destination_number" expression="^(00\d{7,15})$"> <!-- Check if user has international permission --> <action application="set" data="continue_on_fail=false"/> <action application="set" data="effective_caller_id_number=15551234567"/> <action application="bridge" data="sofia/gateway/my_provider/$1"/> </condition> </extension> <!-- ============================================ --> <!-- TIME-BASED ROUTING --> <!-- ============================================ --> <extension name="main_number_time_routing"> <condition field="destination_number" expression="^(5000)$"/> <!-- Check business hours: Mon-Fri 9:00-17:30 --> <condition field="time_of_day" expression="09:00-17:30"> <action application="transfer" data="5001 XML default"/> <anti-action application="transfer" data="5002 XML default"/> </condition> </extension> <!-- Business hours handler --> <extension name="business_hours_handler"> <condition field="destination_number" expression="^(5001)$"> <action application="answer"/> <action application="playback" data="ivr/ivr-welcome.wav"/> <action application="transfer" data="main_ivr XML default"/> </condition> </extension> <!-- After hours handler --> <extension name="after_hours_handler"> <condition field="destination_number" expression="^(5002)$"> <action application="answer"/> <action application="playback" data="ivr/ivr-after_hours_message.wav"/> <action application="voicemail" data="default $${domain} 1001"/> </condition> </extension> <!-- ============================================ --> <!-- UTILITIES --> <!-- ============================================ --> <!-- Echo test (dial 9196) --> <extension name="echo_test"> <condition field="destination_number" expression="^(9196)$"> <action application="answer"/> <action application="echo"/> </condition> </extension> <!-- Music on hold test (dial 9664) --> <extension name="moh_test"> <condition field="destination_number" expression="^(9664)$"> <action application="answer"/> <action application="playback" data="$${hold_music}"/> </condition> </extension> <!-- Voicemail access (dial *98) --> <extension name="voicemail_check"> <condition field="destination_number" expression="^\*98$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain}"/> </condition> </extension> <!-- Direct voicemail for extension (dial *99 + ext) --> <extension name="voicemail_direct"> <condition field="destination_number" expression="^\*99(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="default $${domain} $1"/> </condition> </extension> <!-- Call parking (dial 5900) --> <extension name="park_call"> <condition field="destination_number" expression="^(5900)$"> <action application="set" data="fifo_music=$${hold_music}"/> <action application="fifo" data="park@$${domain} in"/> </condition> </extension> <!-- Retrieve parked call (dial 5901) --> <extension name="retrieve_parked_call"> <condition field="destination_number" expression="^(5901)$"> <action application="answer"/> <action application="fifo" data="park@$${domain} out wait"/> </condition> </extension> </context>
</include>
<include> <context name="default"> <!-- ============================================ --> <!-- INTERNAL EXTENSION CALLING (1001-1099) --> <!-- ============================================ --> <extension name="internal_extensions"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <action application="export" data="dialed_extension=$1"/> <!-- Set ring timeout to 30 seconds --> <action application="set" data="call_timeout=30"/> <!-- Set caller ID --> <action application="set" data="effective_caller_id_name=${outbound_caller_id_name}"/> <action application="set" data="effective_caller_id_number=${outbound_caller_id_number}"/> <!-- Ring the extension, then send to voicemail on no answer --> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="continue_on_fail=true"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> <!-- No answer — go to voicemail --> <action application="answer"/> <action application="sleep" data="1000"/> <action application="voicemail" data="default $${domain} ${dialed_extension}"/> </condition> </extension> <!-- ============================================ --> <!-- RING GROUPS --> <!-- ============================================ --> <!-- Sales ring group (9001) — ring all sales phones simultaneously --> <extension name="ring_group_sales"> <condition field="destination_number" expression="^(9001)$"> <action application="set" data="call_timeout=25"/> <action application="set" data="continue_on_fail=true"/> <action application="set" data="hangup_after_bridge=true"/> <action application="bridge" data="user/1001@$${domain},user/1002@$${domain},user/1003@$${domain}"/> <!-- All busy/unavailable — voicemail for sales --> <action application="voicemail" data="default $${domain} 1001"/> </condition> </extension> <!-- Support ring group (9002) — sequential ring (try each for 15s) --> <extension name="ring_group_support"> <condition field="destination_number" expression="^(9002)$"> <action application="set" data="continue_on_fail=true"/> <action application="set" data="hangup_after_bridge=true"/> <!-- Try first support agent --> <action application="set" data="call_timeout=15"/> <action application="bridge" data="user/1004@$${domain}"/> <!-- Try second --> <action application="bridge" data="user/1005@$${domain}"/> <!-- Try third --> <action application="bridge" data="user/1006@$${domain}"/> <!-- All unavailable --> <action application="voicemail" data="default $${domain} 1004"/> </condition> </extension> <!-- ============================================ --> <!-- OUTBOUND CALLS VIA TRUNK --> <!-- ============================================ --> <!-- Local/National calls: dial 9 + number --> <extension name="outbound_national"> <condition field="destination_number" expression="^9(\d{10,11})$"> <action application="set" data="effective_caller_id_number=15551234567"/> <action application="set" data="effective_caller_id_name=My Company"/> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="call_timeout=60"/> <action application="bridge" data="sofia/gateway/my_provider/$1"/> </condition> </extension> <!-- International calls: dial 00 + country code + number --> <extension name="outbound_international"> <condition field="destination_number" expression="^(00\d{7,15})$"> <!-- Check if user has international permission --> <action application="set" data="continue_on_fail=false"/> <action application="set" data="effective_caller_id_number=15551234567"/> <action application="bridge" data="sofia/gateway/my_provider/$1"/> </condition> </extension> <!-- ============================================ --> <!-- TIME-BASED ROUTING --> <!-- ============================================ --> <extension name="main_number_time_routing"> <condition field="destination_number" expression="^(5000)$"/> <!-- Check business hours: Mon-Fri 9:00-17:30 --> <condition field="time_of_day" expression="09:00-17:30"> <action application="transfer" data="5001 XML default"/> <anti-action application="transfer" data="5002 XML default"/> </condition> </extension> <!-- Business hours handler --> <extension name="business_hours_handler"> <condition field="destination_number" expression="^(5001)$"> <action application="answer"/> <action application="playback" data="ivr/ivr-welcome.wav"/> <action application="transfer" data="main_ivr XML default"/> </condition> </extension> <!-- After hours handler --> <extension name="after_hours_handler"> <condition field="destination_number" expression="^(5002)$"> <action application="answer"/> <action application="playback" data="ivr/ivr-after_hours_message.wav"/> <action application="voicemail" data="default $${domain} 1001"/> </condition> </extension> <!-- ============================================ --> <!-- UTILITIES --> <!-- ============================================ --> <!-- Echo test (dial 9196) --> <extension name="echo_test"> <condition field="destination_number" expression="^(9196)$"> <action application="answer"/> <action application="echo"/> </condition> </extension> <!-- Music on hold test (dial 9664) --> <extension name="moh_test"> <condition field="destination_number" expression="^(9664)$"> <action application="answer"/> <action application="playback" data="$${hold_music}"/> </condition> </extension> <!-- Voicemail access (dial *98) --> <extension name="voicemail_check"> <condition field="destination_number" expression="^\*98$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain}"/> </condition> </extension> <!-- Direct voicemail for extension (dial *99 + ext) --> <extension name="voicemail_direct"> <condition field="destination_number" expression="^\*99(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="default $${domain} $1"/> </condition> </extension> <!-- Call parking (dial 5900) --> <extension name="park_call"> <condition field="destination_number" expression="^(5900)$"> <action application="set" data="fifo_music=$${hold_music}"/> <action application="fifo" data="park@$${domain} in"/> </condition> </extension> <!-- Retrieve parked call (dial 5901) --> <extension name="retrieve_parked_call"> <condition field="destination_number" expression="^(5901)$"> <action application="answer"/> <action application="fifo" data="park@$${domain} out wait"/> </condition> </extension> </context>
</include>
<include> <context name="default"> <!-- ============================================ --> <!-- INTERNAL EXTENSION CALLING (1001-1099) --> <!-- ============================================ --> <extension name="internal_extensions"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <action application="export" data="dialed_extension=$1"/> <!-- Set ring timeout to 30 seconds --> <action application="set" data="call_timeout=30"/> <!-- Set caller ID --> <action application="set" data="effective_caller_id_name=${outbound_caller_id_name}"/> <action application="set" data="effective_caller_id_number=${outbound_caller_id_number}"/> <!-- Ring the extension, then send to voicemail on no answer --> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="continue_on_fail=true"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> <!-- No answer — go to voicemail --> <action application="answer"/> <action application="sleep" data="1000"/> <action application="voicemail" data="default $${domain} ${dialed_extension}"/> </condition> </extension> <!-- ============================================ --> <!-- RING GROUPS --> <!-- ============================================ --> <!-- Sales ring group (9001) — ring all sales phones simultaneously --> <extension name="ring_group_sales"> <condition field="destination_number" expression="^(9001)$"> <action application="set" data="call_timeout=25"/> <action application="set" data="continue_on_fail=true"/> <action application="set" data="hangup_after_bridge=true"/> <action application="bridge" data="user/1001@$${domain},user/1002@$${domain},user/1003@$${domain}"/> <!-- All busy/unavailable — voicemail for sales --> <action application="voicemail" data="default $${domain} 1001"/> </condition> </extension> <!-- Support ring group (9002) — sequential ring (try each for 15s) --> <extension name="ring_group_support"> <condition field="destination_number" expression="^(9002)$"> <action application="set" data="continue_on_fail=true"/> <action application="set" data="hangup_after_bridge=true"/> <!-- Try first support agent --> <action application="set" data="call_timeout=15"/> <action application="bridge" data="user/1004@$${domain}"/> <!-- Try second --> <action application="bridge" data="user/1005@$${domain}"/> <!-- Try third --> <action application="bridge" data="user/1006@$${domain}"/> <!-- All unavailable --> <action application="voicemail" data="default $${domain} 1004"/> </condition> </extension> <!-- ============================================ --> <!-- OUTBOUND CALLS VIA TRUNK --> <!-- ============================================ --> <!-- Local/National calls: dial 9 + number --> <extension name="outbound_national"> <condition field="destination_number" expression="^9(\d{10,11})$"> <action application="set" data="effective_caller_id_number=15551234567"/> <action application="set" data="effective_caller_id_name=My Company"/> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="call_timeout=60"/> <action application="bridge" data="sofia/gateway/my_provider/$1"/> </condition> </extension> <!-- International calls: dial 00 + country code + number --> <extension name="outbound_international"> <condition field="destination_number" expression="^(00\d{7,15})$"> <!-- Check if user has international permission --> <action application="set" data="continue_on_fail=false"/> <action application="set" data="effective_caller_id_number=15551234567"/> <action application="bridge" data="sofia/gateway/my_provider/$1"/> </condition> </extension> <!-- ============================================ --> <!-- TIME-BASED ROUTING --> <!-- ============================================ --> <extension name="main_number_time_routing"> <condition field="destination_number" expression="^(5000)$"/> <!-- Check business hours: Mon-Fri 9:00-17:30 --> <condition field="time_of_day" expression="09:00-17:30"> <action application="transfer" data="5001 XML default"/> <anti-action application="transfer" data="5002 XML default"/> </condition> </extension> <!-- Business hours handler --> <extension name="business_hours_handler"> <condition field="destination_number" expression="^(5001)$"> <action application="answer"/> <action application="playback" data="ivr/ivr-welcome.wav"/> <action application="transfer" data="main_ivr XML default"/> </condition> </extension> <!-- After hours handler --> <extension name="after_hours_handler"> <condition field="destination_number" expression="^(5002)$"> <action application="answer"/> <action application="playback" data="ivr/ivr-after_hours_message.wav"/> <action application="voicemail" data="default $${domain} 1001"/> </condition> </extension> <!-- ============================================ --> <!-- UTILITIES --> <!-- ============================================ --> <!-- Echo test (dial 9196) --> <extension name="echo_test"> <condition field="destination_number" expression="^(9196)$"> <action application="answer"/> <action application="echo"/> </condition> </extension> <!-- Music on hold test (dial 9664) --> <extension name="moh_test"> <condition field="destination_number" expression="^(9664)$"> <action application="answer"/> <action application="playback" data="$${hold_music}"/> </condition> </extension> <!-- Voicemail access (dial *98) --> <extension name="voicemail_check"> <condition field="destination_number" expression="^\*98$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain}"/> </condition> </extension> <!-- Direct voicemail for extension (dial *99 + ext) --> <extension name="voicemail_direct"> <condition field="destination_number" expression="^\*99(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="default $${domain} $1"/> </condition> </extension> <!-- Call parking (dial 5900) --> <extension name="park_call"> <condition field="destination_number" expression="^(5900)$"> <action application="set" data="fifo_music=$${hold_music}"/> <action application="fifo" data="park@$${domain} in"/> </condition> </extension> <!-- Retrieve parked call (dial 5901) --> <extension name="retrieve_parked_call"> <condition field="destination_number" expression="^(5901)$"> <action application="answer"/> <action application="fifo" data="park@$${domain} out wait"/> </condition> </extension> </context>
</include>
<configuration name="ivr.conf" description="IVR Menus"> <menus> <!-- ============================================ --> <!-- MAIN MENU (Level 1) --> <!-- ============================================ --> <menu name="main_ivr" greet-long="ivr/ivr-welcome_to_our_company.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" confirm-macro="" confirm-key="" tts-engine="" tts-voice="" confirm-attempts="3" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Sales --> <entry action="menu-exec-app" digits="1" param="transfer 9001 XML default"/> <!-- Press 2 for Support --> <entry action="menu-exec-app" digits="2" param="transfer 9002 XML default"/> <!-- Press 3 for Billing --> <entry action="menu-sub" digits="3" param="billing_ivr"/> <!-- Press 4 for Company Directory --> <entry action="menu-exec-app" digits="4" param="transfer 411 XML default"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> <!-- Press * to repeat --> <entry action="menu-top" digits="*"/> </menu> <!-- ============================================ --> <!-- BILLING SUBMENU (Level 2) --> <!-- ============================================ --> <menu name="billing_ivr" greet-long="ivr/ivr-billing_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Account Balance --> <entry action="menu-exec-app" digits="1" param="transfer 8001 XML default"/> <!-- Press 2 for Payment --> <entry action="menu-exec-app" digits="2" param="transfer 8002 XML default"/> <!-- Press 3 for Billing Agent --> <entry action="menu-exec-app" digits="3" param="transfer 1007 XML default"/> <!-- Press 9 to go back to main menu --> <entry action="menu-back" digits="9"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> </menu> <!-- ============================================ --> <!-- SUPPORT SUBMENU (Level 2) --> <!-- ============================================ --> <menu name="support_ivr" greet-long="ivr/ivr-support_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Technical Support --> <entry action="menu-sub" digits="1" param="tech_support_ivr"/> <!-- Press 2 for General Inquiries --> <entry action="menu-exec-app" digits="2" param="transfer 1005 XML default"/> <!-- Press 9 to go back --> <entry action="menu-back" digits="9"/> </menu> <!-- ============================================ --> <!-- TECH SUPPORT (Level 3) --> <!-- ============================================ --> <menu name="tech_support_ivr" greet-long="ivr/ivr-tech_support_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Internet Issues --> <entry action="menu-exec-app" digits="1" param="transfer 1004 XML default"/> <!-- Press 2 for Phone/VoIP Issues --> <entry action="menu-exec-app" digits="2" param="transfer 1005 XML default"/> <!-- Press 3 for Email Issues --> <entry action="menu-exec-app" digits="3" param="transfer 1006 XML default"/> <!-- Press 9 to go back --> <entry action="menu-back" digits="9"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> </menu> </menus>
</configuration>
<configuration name="ivr.conf" description="IVR Menus"> <menus> <!-- ============================================ --> <!-- MAIN MENU (Level 1) --> <!-- ============================================ --> <menu name="main_ivr" greet-long="ivr/ivr-welcome_to_our_company.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" confirm-macro="" confirm-key="" tts-engine="" tts-voice="" confirm-attempts="3" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Sales --> <entry action="menu-exec-app" digits="1" param="transfer 9001 XML default"/> <!-- Press 2 for Support --> <entry action="menu-exec-app" digits="2" param="transfer 9002 XML default"/> <!-- Press 3 for Billing --> <entry action="menu-sub" digits="3" param="billing_ivr"/> <!-- Press 4 for Company Directory --> <entry action="menu-exec-app" digits="4" param="transfer 411 XML default"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> <!-- Press * to repeat --> <entry action="menu-top" digits="*"/> </menu> <!-- ============================================ --> <!-- BILLING SUBMENU (Level 2) --> <!-- ============================================ --> <menu name="billing_ivr" greet-long="ivr/ivr-billing_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Account Balance --> <entry action="menu-exec-app" digits="1" param="transfer 8001 XML default"/> <!-- Press 2 for Payment --> <entry action="menu-exec-app" digits="2" param="transfer 8002 XML default"/> <!-- Press 3 for Billing Agent --> <entry action="menu-exec-app" digits="3" param="transfer 1007 XML default"/> <!-- Press 9 to go back to main menu --> <entry action="menu-back" digits="9"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> </menu> <!-- ============================================ --> <!-- SUPPORT SUBMENU (Level 2) --> <!-- ============================================ --> <menu name="support_ivr" greet-long="ivr/ivr-support_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Technical Support --> <entry action="menu-sub" digits="1" param="tech_support_ivr"/> <!-- Press 2 for General Inquiries --> <entry action="menu-exec-app" digits="2" param="transfer 1005 XML default"/> <!-- Press 9 to go back --> <entry action="menu-back" digits="9"/> </menu> <!-- ============================================ --> <!-- TECH SUPPORT (Level 3) --> <!-- ============================================ --> <menu name="tech_support_ivr" greet-long="ivr/ivr-tech_support_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Internet Issues --> <entry action="menu-exec-app" digits="1" param="transfer 1004 XML default"/> <!-- Press 2 for Phone/VoIP Issues --> <entry action="menu-exec-app" digits="2" param="transfer 1005 XML default"/> <!-- Press 3 for Email Issues --> <entry action="menu-exec-app" digits="3" param="transfer 1006 XML default"/> <!-- Press 9 to go back --> <entry action="menu-back" digits="9"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> </menu> </menus>
</configuration>
<configuration name="ivr.conf" description="IVR Menus"> <menus> <!-- ============================================ --> <!-- MAIN MENU (Level 1) --> <!-- ============================================ --> <menu name="main_ivr" greet-long="ivr/ivr-welcome_to_our_company.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" confirm-macro="" confirm-key="" tts-engine="" tts-voice="" confirm-attempts="3" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Sales --> <entry action="menu-exec-app" digits="1" param="transfer 9001 XML default"/> <!-- Press 2 for Support --> <entry action="menu-exec-app" digits="2" param="transfer 9002 XML default"/> <!-- Press 3 for Billing --> <entry action="menu-sub" digits="3" param="billing_ivr"/> <!-- Press 4 for Company Directory --> <entry action="menu-exec-app" digits="4" param="transfer 411 XML default"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> <!-- Press * to repeat --> <entry action="menu-top" digits="*"/> </menu> <!-- ============================================ --> <!-- BILLING SUBMENU (Level 2) --> <!-- ============================================ --> <menu name="billing_ivr" greet-long="ivr/ivr-billing_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Account Balance --> <entry action="menu-exec-app" digits="1" param="transfer 8001 XML default"/> <!-- Press 2 for Payment --> <entry action="menu-exec-app" digits="2" param="transfer 8002 XML default"/> <!-- Press 3 for Billing Agent --> <entry action="menu-exec-app" digits="3" param="transfer 1007 XML default"/> <!-- Press 9 to go back to main menu --> <entry action="menu-back" digits="9"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> </menu> <!-- ============================================ --> <!-- SUPPORT SUBMENU (Level 2) --> <!-- ============================================ --> <menu name="support_ivr" greet-long="ivr/ivr-support_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Technical Support --> <entry action="menu-sub" digits="1" param="tech_support_ivr"/> <!-- Press 2 for General Inquiries --> <entry action="menu-exec-app" digits="2" param="transfer 1005 XML default"/> <!-- Press 9 to go back --> <entry action="menu-back" digits="9"/> </menu> <!-- ============================================ --> <!-- TECH SUPPORT (Level 3) --> <!-- ============================================ --> <menu name="tech_support_ivr" greet-long="ivr/ivr-tech_support_menu.wav" greet-short="ivr/ivr-please_make_selection.wav" invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav" timeout="10000" inter-digit-timeout="2000" max-failures="3" max-timeouts="3" digit-len="1"> <!-- Press 1 for Internet Issues --> <entry action="menu-exec-app" digits="1" param="transfer 1004 XML default"/> <!-- Press 2 for Phone/VoIP Issues --> <entry action="menu-exec-app" digits="2" param="transfer 1005 XML default"/> <!-- Press 3 for Email Issues --> <entry action="menu-exec-app" digits="3" param="transfer 1006 XML default"/> <!-- Press 9 to go back --> <entry action="menu-back" digits="9"/> <!-- Press 0 for Operator --> <entry action="menu-exec-app" digits="0" param="transfer 1009 XML default"/> </menu> </menus>
</configuration>
<!-- IVR entry point (dial 5000 or transfer from public context) -->
<extension name="main_ivr"> <condition field="destination_number" expression="^(main_ivr|5000)$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="ivr" data="main_ivr"/> </condition>
</extension>
<!-- IVR entry point (dial 5000 or transfer from public context) -->
<extension name="main_ivr"> <condition field="destination_number" expression="^(main_ivr|5000)$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="ivr" data="main_ivr"/> </condition>
</extension>
<!-- IVR entry point (dial 5000 or transfer from public context) -->
<extension name="main_ivr"> <condition field="destination_number" expression="^(main_ivr|5000)$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="ivr" data="main_ivr"/> </condition>
</extension>
<!-- Collect a 5-digit account number -->
<extension name="account_lookup"> <condition field="destination_number" expression="^(8001)$"> <action application="answer"/> <action application="sleep" data="500"/> <!-- play_and_get_digits parameters: min_digits max_digits max_tries timeout terminators audio_file bad_input_file variable_name regex_pattern digit_timeout transfer_on_failure --> <action application="play_and_get_digits" data="5 5 3 10000 # ivr/ivr-enter_account_number.wav ivr/ivr-that_was_an_invalid_entry.wav account_number \d{5} 15000"/> <!-- Now use the collected digits --> <action application="log" data="INFO Account number entered: ${account_number}"/> <!-- You could use mod_xml_curl to look up the account, or use Lua/Python to query a database --> <action application="playback" data="ivr/ivr-thank_you.wav"/> <action application="transfer" data="1007 XML default"/> </condition>
</extension>
<!-- Collect a 5-digit account number -->
<extension name="account_lookup"> <condition field="destination_number" expression="^(8001)$"> <action application="answer"/> <action application="sleep" data="500"/> <!-- play_and_get_digits parameters: min_digits max_digits max_tries timeout terminators audio_file bad_input_file variable_name regex_pattern digit_timeout transfer_on_failure --> <action application="play_and_get_digits" data="5 5 3 10000 # ivr/ivr-enter_account_number.wav ivr/ivr-that_was_an_invalid_entry.wav account_number \d{5} 15000"/> <!-- Now use the collected digits --> <action application="log" data="INFO Account number entered: ${account_number}"/> <!-- You could use mod_xml_curl to look up the account, or use Lua/Python to query a database --> <action application="playback" data="ivr/ivr-thank_you.wav"/> <action application="transfer" data="1007 XML default"/> </condition>
</extension>
<!-- Collect a 5-digit account number -->
<extension name="account_lookup"> <condition field="destination_number" expression="^(8001)$"> <action application="answer"/> <action application="sleep" data="500"/> <!-- play_and_get_digits parameters: min_digits max_digits max_tries timeout terminators audio_file bad_input_file variable_name regex_pattern digit_timeout transfer_on_failure --> <action application="play_and_get_digits" data="5 5 3 10000 # ivr/ivr-enter_account_number.wav ivr/ivr-that_was_an_invalid_entry.wav account_number \d{5} 15000"/> <!-- Now use the collected digits --> <action application="log" data="INFO Account number entered: ${account_number}"/> <!-- You could use mod_xml_curl to look up the account, or use Lua/Python to query a database --> <action application="playback" data="ivr/ivr-thank_you.wav"/> <action application="transfer" data="1007 XML default"/> </condition>
</extension>
<action application="speak" data="flite|kal|Welcome to our company. Press 1 for sales."/>
<action application="speak" data="flite|kal|Welcome to our company. Press 1 for sales."/>
<action application="speak" data="flite|kal|Welcome to our company. Press 1 for sales."/>
<configuration name="tts_commandline.conf" description="TTS Command"> <settings> <param name="command" value="echo '${text}' | /usr/bin/piper --model /opt/tts/en_US-lessac-medium.onnx --output_file ${file}"/> </settings>
</configuration>
<configuration name="tts_commandline.conf" description="TTS Command"> <settings> <param name="command" value="echo '${text}' | /usr/bin/piper --model /opt/tts/en_US-lessac-medium.onnx --output_file ${file}"/> </settings>
</configuration>
<configuration name="tts_commandline.conf" description="TTS Command"> <settings> <param name="command" value="echo '${text}' | /usr/bin/piper --model /opt/tts/en_US-lessac-medium.onnx --output_file ${file}"/> </settings>
</configuration>
<action application="speak" data="tts_commandline|default|Welcome to our company. Please hold while we connect you."/>
<action application="speak" data="tts_commandline|default|Welcome to our company. Please hold while we connect you."/>
<action application="speak" data="tts_commandline|default|Welcome to our company. Please hold while we connect you."/>
Caller dials main number │ ▼ Level 1: Main Menu 1 → Sales (ring group 9001) 2 → Support submenu 3 → Billing submenu 4 → Company directory 0 → Operator (ext 1009) │ ├── Level 2: Support │ 1 → Tech Support submenu │ 2 → General Inquiries (ext 1005) │ 9 → Back to Main │ │ │ └── Level 3: Tech Support │ 1 → Internet (ext 1004) │ 2 → VoIP (ext 1005) │ 3 → Email (ext 1006) │ 9 → Back to Support │ 0 → Operator │ └── Level 2: Billing 1 → Account Balance (collect acct#) 2 → Payment (ext 8002) 3 → Billing Agent (ext 1007) 9 → Back to Main 0 → Operator
Caller dials main number │ ▼ Level 1: Main Menu 1 → Sales (ring group 9001) 2 → Support submenu 3 → Billing submenu 4 → Company directory 0 → Operator (ext 1009) │ ├── Level 2: Support │ 1 → Tech Support submenu │ 2 → General Inquiries (ext 1005) │ 9 → Back to Main │ │ │ └── Level 3: Tech Support │ 1 → Internet (ext 1004) │ 2 → VoIP (ext 1005) │ 3 → Email (ext 1006) │ 9 → Back to Support │ 0 → Operator │ └── Level 2: Billing 1 → Account Balance (collect acct#) 2 → Payment (ext 8002) 3 → Billing Agent (ext 1007) 9 → Back to Main 0 → Operator
Caller dials main number │ ▼ Level 1: Main Menu 1 → Sales (ring group 9001) 2 → Support submenu 3 → Billing submenu 4 → Company directory 0 → Operator (ext 1009) │ ├── Level 2: Support │ 1 → Tech Support submenu │ 2 → General Inquiries (ext 1005) │ 9 → Back to Main │ │ │ └── Level 3: Tech Support │ 1 → Internet (ext 1004) │ 2 → VoIP (ext 1005) │ 3 → Email (ext 1006) │ 9 → Back to Support │ 0 → Operator │ └── Level 2: Billing 1 → Account Balance (collect acct#) 2 → Payment (ext 8002) 3 → Billing Agent (ext 1007) 9 → Back to Main 0 → Operator
<configuration name="voicemail.conf" description="Voicemail"> <settings> </settings> <profiles> <profile name="default"> <!-- Storage --> <param name="file-extension" value="wav"/> <param name="record-silence-threshold" value="200"/> <param name="record-silence-hits" value="5"/> <param name="max-record-len" value="300"/> <param name="max-retries" value="3"/> <!-- Greeting --> <param name="terminator-key" value="#"/> <param name="play-new-messages-key" value="1"/> <param name="play-saved-messages-key" value="2"/> <!-- Message controls (during playback) --> <param name="skip-greet-key" value="#"/> <param name="config-menu-key" value="5"/> <param name="record-greeting-key" value="1"/> <param name="choose-greeting-key" value="2"/> <param name="change-pass-key" value="6"/> <!-- Playback controls --> <param name="listen-key" value="1"/> <param name="save-key" value="2"/> <param name="delete-key" value="7"/> <param name="forward-key" value="8"/> <param name="repeat-key" value="0"/> <!-- Notification --> <param name="notify-mailto" value=""/> <param name="notify-email-body" value="You have a new voicemail from ${caller_id_number} (${caller_id_name}). The message is ${message_len} seconds long."/> <param name="notify-email-subject" value="New voicemail from ${caller_id_number}"/> <!-- Email with attachment --> <param name="email-from" value="voicemail@YOUR_DOMAIN"/> <param name="email-body" value="You have a new voicemail.\n\nFrom: ${caller_id_name} (${caller_id_number})\nDate: ${left_epoch}\nDuration: ${message_len} seconds\n\nThe recording is attached."/> <param name="email-subject" value="[Voicemail] New message from ${caller_id_number}"/> <!-- Attach the recording to the email --> <param name="vm-email-all-messages" value="true"/> <!-- Delete from server after emailing (set false to keep) --> <param name="vm-delete-file" value="false"/> <!-- Keep the message as new after emailing --> <param name="vm-keep-local-after-email" value="true"/> <!-- MWI (Message Waiting Indicator — lights up the phone's voicemail LED) --> <param name="vm-message-ext" value="wav"/> <!-- Storage location --> <param name="storage-dir" value="$${storage_dir}/voicemail/default"/> <!-- Operator extension (press 0 during greeting) --> <param name="operator-extension" value="operator XML default"/> <param name="operator-key" value="0"/> </profile> </profiles>
</configuration>
<configuration name="voicemail.conf" description="Voicemail"> <settings> </settings> <profiles> <profile name="default"> <!-- Storage --> <param name="file-extension" value="wav"/> <param name="record-silence-threshold" value="200"/> <param name="record-silence-hits" value="5"/> <param name="max-record-len" value="300"/> <param name="max-retries" value="3"/> <!-- Greeting --> <param name="terminator-key" value="#"/> <param name="play-new-messages-key" value="1"/> <param name="play-saved-messages-key" value="2"/> <!-- Message controls (during playback) --> <param name="skip-greet-key" value="#"/> <param name="config-menu-key" value="5"/> <param name="record-greeting-key" value="1"/> <param name="choose-greeting-key" value="2"/> <param name="change-pass-key" value="6"/> <!-- Playback controls --> <param name="listen-key" value="1"/> <param name="save-key" value="2"/> <param name="delete-key" value="7"/> <param name="forward-key" value="8"/> <param name="repeat-key" value="0"/> <!-- Notification --> <param name="notify-mailto" value=""/> <param name="notify-email-body" value="You have a new voicemail from ${caller_id_number} (${caller_id_name}). The message is ${message_len} seconds long."/> <param name="notify-email-subject" value="New voicemail from ${caller_id_number}"/> <!-- Email with attachment --> <param name="email-from" value="voicemail@YOUR_DOMAIN"/> <param name="email-body" value="You have a new voicemail.\n\nFrom: ${caller_id_name} (${caller_id_number})\nDate: ${left_epoch}\nDuration: ${message_len} seconds\n\nThe recording is attached."/> <param name="email-subject" value="[Voicemail] New message from ${caller_id_number}"/> <!-- Attach the recording to the email --> <param name="vm-email-all-messages" value="true"/> <!-- Delete from server after emailing (set false to keep) --> <param name="vm-delete-file" value="false"/> <!-- Keep the message as new after emailing --> <param name="vm-keep-local-after-email" value="true"/> <!-- MWI (Message Waiting Indicator — lights up the phone's voicemail LED) --> <param name="vm-message-ext" value="wav"/> <!-- Storage location --> <param name="storage-dir" value="$${storage_dir}/voicemail/default"/> <!-- Operator extension (press 0 during greeting) --> <param name="operator-extension" value="operator XML default"/> <param name="operator-key" value="0"/> </profile> </profiles>
</configuration>
<configuration name="voicemail.conf" description="Voicemail"> <settings> </settings> <profiles> <profile name="default"> <!-- Storage --> <param name="file-extension" value="wav"/> <param name="record-silence-threshold" value="200"/> <param name="record-silence-hits" value="5"/> <param name="max-record-len" value="300"/> <param name="max-retries" value="3"/> <!-- Greeting --> <param name="terminator-key" value="#"/> <param name="play-new-messages-key" value="1"/> <param name="play-saved-messages-key" value="2"/> <!-- Message controls (during playback) --> <param name="skip-greet-key" value="#"/> <param name="config-menu-key" value="5"/> <param name="record-greeting-key" value="1"/> <param name="choose-greeting-key" value="2"/> <param name="change-pass-key" value="6"/> <!-- Playback controls --> <param name="listen-key" value="1"/> <param name="save-key" value="2"/> <param name="delete-key" value="7"/> <param name="forward-key" value="8"/> <param name="repeat-key" value="0"/> <!-- Notification --> <param name="notify-mailto" value=""/> <param name="notify-email-body" value="You have a new voicemail from ${caller_id_number} (${caller_id_name}). The message is ${message_len} seconds long."/> <param name="notify-email-subject" value="New voicemail from ${caller_id_number}"/> <!-- Email with attachment --> <param name="email-from" value="voicemail@YOUR_DOMAIN"/> <param name="email-body" value="You have a new voicemail.\n\nFrom: ${caller_id_name} (${caller_id_number})\nDate: ${left_epoch}\nDuration: ${message_len} seconds\n\nThe recording is attached."/> <param name="email-subject" value="[Voicemail] New message from ${caller_id_number}"/> <!-- Attach the recording to the email --> <param name="vm-email-all-messages" value="true"/> <!-- Delete from server after emailing (set false to keep) --> <param name="vm-delete-file" value="false"/> <!-- Keep the message as new after emailing --> <param name="vm-keep-local-after-email" value="true"/> <!-- MWI (Message Waiting Indicator — lights up the phone's voicemail LED) --> <param name="vm-message-ext" value="wav"/> <!-- Storage location --> <param name="storage-dir" value="$${storage_dir}/voicemail/default"/> <!-- Operator extension (press 0 during greeting) --> <param name="operator-extension" value="operator XML default"/> <param name="operator-key" value="0"/> </profile> </profiles>
</configuration>
<!-- Send to voicemail (internal — after no answer on bridge) -->
<!-- This is already in the internal_extensions example above --> <!-- Check own voicemail: dial *98 -->
<extension name="voicemail_check"> <condition field="destination_number" expression="^\*98$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain}"/> </condition>
</extension> <!-- Leave voicemail directly for someone: dial *99 + extension -->
<extension name="voicemail_leave"> <condition field="destination_number" expression="^\*99(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="default $${domain} $1"/> </condition>
</extension> <!-- Check specific mailbox (for shared mailboxes): dial *97 + extension -->
<extension name="voicemail_check_specific"> <condition field="destination_number" expression="^\*97(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain} $1"/> </condition>
</extension>
<!-- Send to voicemail (internal — after no answer on bridge) -->
<!-- This is already in the internal_extensions example above --> <!-- Check own voicemail: dial *98 -->
<extension name="voicemail_check"> <condition field="destination_number" expression="^\*98$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain}"/> </condition>
</extension> <!-- Leave voicemail directly for someone: dial *99 + extension -->
<extension name="voicemail_leave"> <condition field="destination_number" expression="^\*99(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="default $${domain} $1"/> </condition>
</extension> <!-- Check specific mailbox (for shared mailboxes): dial *97 + extension -->
<extension name="voicemail_check_specific"> <condition field="destination_number" expression="^\*97(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain} $1"/> </condition>
</extension>
<!-- Send to voicemail (internal — after no answer on bridge) -->
<!-- This is already in the internal_extensions example above --> <!-- Check own voicemail: dial *98 -->
<extension name="voicemail_check"> <condition field="destination_number" expression="^\*98$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain}"/> </condition>
</extension> <!-- Leave voicemail directly for someone: dial *99 + extension -->
<extension name="voicemail_leave"> <condition field="destination_number" expression="^\*99(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="default $${domain} $1"/> </condition>
</extension> <!-- Check specific mailbox (for shared mailboxes): dial *97 + extension -->
<extension name="voicemail_check_specific"> <condition field="destination_number" expression="^\*97(\d{4})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="voicemail" data="check default $${domain} $1"/> </condition>
</extension>
# Install a lightweight MTA
apt-get install -y msmtp msmtp-mta # Configure SMTP relay
cat > /etc/msmtprc << 'EOF'
defaults
auth on
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile /var/log/msmtp.log account default
host smtp.gmail.com
port 587
from voicemail@YOUR_DOMAIN
user [email protected]
password your_app_password
EOF chmod 600 /etc/msmtprc
# Install a lightweight MTA
apt-get install -y msmtp msmtp-mta # Configure SMTP relay
cat > /etc/msmtprc << 'EOF'
defaults
auth on
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile /var/log/msmtp.log account default
host smtp.gmail.com
port 587
from voicemail@YOUR_DOMAIN
user [email protected]
password your_app_password
EOF chmod 600 /etc/msmtprc
# Install a lightweight MTA
apt-get install -y msmtp msmtp-mta # Configure SMTP relay
cat > /etc/msmtprc << 'EOF'
defaults
auth on
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile /var/log/msmtp.log account default
host smtp.gmail.com
port 587
from voicemail@YOUR_DOMAIN
user [email protected]
password your_app_password
EOF chmod 600 /etc/msmtprc
<!-- In /etc/freeswitch/directory/default/1001.xml -->
<include> <user id="1001"> <params> <param name="password" value="Str0ng_P@ss_1001!"/> <param name="vm-password" value="1001"/> <param name="vm-mailto" value="[email protected]"/> <param name="vm-email-all-messages" value="true"/> <param name="vm-attach-file" value="true"/> <param name="vm-keep-local-after-email" value="true"/> </params> <!-- ... variables ... --> </user>
</include>
<!-- In /etc/freeswitch/directory/default/1001.xml -->
<include> <user id="1001"> <params> <param name="password" value="Str0ng_P@ss_1001!"/> <param name="vm-password" value="1001"/> <param name="vm-mailto" value="[email protected]"/> <param name="vm-email-all-messages" value="true"/> <param name="vm-attach-file" value="true"/> <param name="vm-keep-local-after-email" value="true"/> </params> <!-- ... variables ... --> </user>
</include>
<!-- In /etc/freeswitch/directory/default/1001.xml -->
<include> <user id="1001"> <params> <param name="password" value="Str0ng_P@ss_1001!"/> <param name="vm-password" value="1001"/> <param name="vm-mailto" value="[email protected]"/> <param name="vm-email-all-messages" value="true"/> <param name="vm-attach-file" value="true"/> <param name="vm-keep-local-after-email" value="true"/> </params> <!-- ... variables ... --> </user>
</include>
#!/bin/bash
# /usr/local/bin/cleanup-voicemail.sh
# Delete voicemail messages older than 30 days STORAGE_DIR="/var/lib/freeswitch/storage/voicemail"
MAX_AGE=30 echo "$(date '+%Y-%m-%d %H:%M:%S') — Voicemail cleanup starting" # Find and delete old voicemail recordings
find "${STORAGE_DIR}" -name "*.wav" -type f -mtime +${MAX_AGE} -delete
find "${STORAGE_DIR}" -name "*.mp3" -type f -mtime +${MAX_AGE} -delete # Clean up empty directories
find "${STORAGE_DIR}" -type d -empty -delete echo "$(date '+%Y-%m-%d %H:%M:%S') — Voicemail cleanup complete"
#!/bin/bash
# /usr/local/bin/cleanup-voicemail.sh
# Delete voicemail messages older than 30 days STORAGE_DIR="/var/lib/freeswitch/storage/voicemail"
MAX_AGE=30 echo "$(date '+%Y-%m-%d %H:%M:%S') — Voicemail cleanup starting" # Find and delete old voicemail recordings
find "${STORAGE_DIR}" -name "*.wav" -type f -mtime +${MAX_AGE} -delete
find "${STORAGE_DIR}" -name "*.mp3" -type f -mtime +${MAX_AGE} -delete # Clean up empty directories
find "${STORAGE_DIR}" -type d -empty -delete echo "$(date '+%Y-%m-%d %H:%M:%S') — Voicemail cleanup complete"
#!/bin/bash
# /usr/local/bin/cleanup-voicemail.sh
# Delete voicemail messages older than 30 days STORAGE_DIR="/var/lib/freeswitch/storage/voicemail"
MAX_AGE=30 echo "$(date '+%Y-%m-%d %H:%M:%S') — Voicemail cleanup starting" # Find and delete old voicemail recordings
find "${STORAGE_DIR}" -name "*.wav" -type f -mtime +${MAX_AGE} -delete
find "${STORAGE_DIR}" -name "*.mp3" -type f -mtime +${MAX_AGE} -delete # Clean up empty directories
find "${STORAGE_DIR}" -type d -empty -delete echo "$(date '+%Y-%m-%d %H:%M:%S') — Voicemail cleanup complete"
chmod +x /usr/local/bin/cleanup-voicemail.sh # Run weekly
echo "0 3 * * 0 root /usr/local/bin/cleanup-voicemail.sh >> /var/log/voicemail-cleanup.log 2>&1" \ > /etc/cron.d/voicemail-cleanup
chmod +x /usr/local/bin/cleanup-voicemail.sh # Run weekly
echo "0 3 * * 0 root /usr/local/bin/cleanup-voicemail.sh >> /var/log/voicemail-cleanup.log 2>&1" \ > /etc/cron.d/voicemail-cleanup
chmod +x /usr/local/bin/cleanup-voicemail.sh # Run weekly
echo "0 3 * * 0 root /usr/local/bin/cleanup-voicemail.sh >> /var/log/voicemail-cleanup.log 2>&1" \ > /etc/cron.d/voicemail-cleanup
<extension name="recorded_internal_call"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <!-- Start recording BEFORE bridging --> <action application="set" data="RECORD_STEREO=true"/> <action application="set" data="media_bug_answer_req=true"/> <action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}_${caller_id_number}_to_${dialed_extension}.wav"/> <!-- Now bridge the call --> <action application="set" data="call_timeout=30"/> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="continue_on_fail=true"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> <!-- Voicemail on no answer (recording continues into VM) --> <action application="answer"/> <action application="voicemail" data="default $${domain} ${dialed_extension}"/> </condition>
</extension>
<extension name="recorded_internal_call"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <!-- Start recording BEFORE bridging --> <action application="set" data="RECORD_STEREO=true"/> <action application="set" data="media_bug_answer_req=true"/> <action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}_${caller_id_number}_to_${dialed_extension}.wav"/> <!-- Now bridge the call --> <action application="set" data="call_timeout=30"/> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="continue_on_fail=true"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> <!-- Voicemail on no answer (recording continues into VM) --> <action application="answer"/> <action application="voicemail" data="default $${domain} ${dialed_extension}"/> </condition>
</extension>
<extension name="recorded_internal_call"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <!-- Start recording BEFORE bridging --> <action application="set" data="RECORD_STEREO=true"/> <action application="set" data="media_bug_answer_req=true"/> <action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}_${caller_id_number}_to_${dialed_extension}.wav"/> <!-- Now bridge the call --> <action application="set" data="call_timeout=30"/> <action application="set" data="hangup_after_bridge=true"/> <action application="set" data="continue_on_fail=true"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> <!-- Voicemail on no answer (recording continues into VM) --> <action application="answer"/> <action application="voicemail" data="default $${domain} ${dialed_extension}"/> </condition>
</extension>
<!-- Date-based subdirectories + descriptive filenames -->
<action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}_${caller_id_number}_to_${destination_number}_${strftime(%Y%m%d-%H%M%S)}.wav"/>
<!-- Date-based subdirectories + descriptive filenames -->
<action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}_${caller_id_number}_to_${destination_number}_${strftime(%Y%m%d-%H%M%S)}.wav"/>
<!-- Date-based subdirectories + descriptive filenames -->
<action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}_${caller_id_number}_to_${destination_number}_${strftime(%Y%m%d-%H%M%S)}.wav"/>
/var/lib/freeswitch/recordings/2026/03/14/abc123_15551234567_to_1001_20260314-143022.wav
/var/lib/freeswitch/recordings/2026/03/14/abc123_15551234567_to_1001_20260314-143022.wav
/var/lib/freeswitch/recordings/2026/03/14/abc123_15551234567_to_1001_20260314-143022.wav
<!-- Enable stereo recording -->
<action application="set" data="RECORD_STEREO=true"/> <!-- Record in stereo WAV -->
<action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}.wav"/>
<!-- Enable stereo recording -->
<action application="set" data="RECORD_STEREO=true"/> <!-- Record in stereo WAV -->
<action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}.wav"/>
<!-- Enable stereo recording -->
<action application="set" data="RECORD_STEREO=true"/> <!-- Record in stereo WAV -->
<action application="record_session" data="/var/lib/freeswitch/recordings/${strftime(%Y/%m/%d)}/${uuid}.wav"/>
# Start recording on an active call (from fs_cli or ESL)
uuid_record <call-uuid> start /var/lib/freeswitch/recordings/mid_call_recording.wav # Stop recording
uuid_record <call-uuid> stop /var/lib/freeswitch/recordings/mid_call_recording.wav # Stop all recordings on a call
uuid_record <call-uuid> stop all
# Start recording on an active call (from fs_cli or ESL)
uuid_record <call-uuid> start /var/lib/freeswitch/recordings/mid_call_recording.wav # Stop recording
uuid_record <call-uuid> stop /var/lib/freeswitch/recordings/mid_call_recording.wav # Stop all recordings on a call
uuid_record <call-uuid> stop all
# Start recording on an active call (from fs_cli or ESL)
uuid_record <call-uuid> start /var/lib/freeswitch/recordings/mid_call_recording.wav # Stop recording
uuid_record <call-uuid> stop /var/lib/freeswitch/recordings/mid_call_recording.wav # Stop all recordings on a call
uuid_record <call-uuid> stop all
<!-- Agent presses *1 to start recording, *2 to stop -->
<extension name="record_on_demand"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <!-- Bind DTMF keys for recording control --> <action application="bind_digit_action" data="rec,*1,exec:record_session,/var/lib/freeswitch/recordings/${uuid}_on_demand.wav"/> <action application="bind_digit_action" data="rec,*2,exec:stop_record_session,/var/lib/freeswitch/recordings/${uuid}_on_demand.wav"/> <action application="digit_action_set_realm" data="rec"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> </condition>
</extension>
<!-- Agent presses *1 to start recording, *2 to stop -->
<extension name="record_on_demand"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <!-- Bind DTMF keys for recording control --> <action application="bind_digit_action" data="rec,*1,exec:record_session,/var/lib/freeswitch/recordings/${uuid}_on_demand.wav"/> <action application="bind_digit_action" data="rec,*2,exec:stop_record_session,/var/lib/freeswitch/recordings/${uuid}_on_demand.wav"/> <action application="digit_action_set_realm" data="rec"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> </condition>
</extension>
<!-- Agent presses *1 to start recording, *2 to stop -->
<extension name="record_on_demand"> <condition field="destination_number" expression="^(10[0-9]{2})$"> <action application="set" data="dialed_extension=$1"/> <!-- Bind DTMF keys for recording control --> <action application="bind_digit_action" data="rec,*1,exec:record_session,/var/lib/freeswitch/recordings/${uuid}_on_demand.wav"/> <action application="bind_digit_action" data="rec,*2,exec:stop_record_session,/var/lib/freeswitch/recordings/${uuid}_on_demand.wav"/> <action application="digit_action_set_realm" data="rec"/> <action application="bridge" data="user/${dialed_extension}@$${domain}"/> </condition>
</extension>
#!/bin/bash
# /usr/local/bin/convert-recordings.sh
# Convert WAV recordings to MP3 and archive RECORDINGS_DIR="/var/lib/freeswitch/recordings"
ARCHIVE_DIR="/var/lib/freeswitch/recordings-mp3"
LOG="/var/log/recording-convert.log" # Install lame if not present
which lame > /dev/null 2>&1 || apt-get install -y lame # Process WAV files older than 5 minutes (avoid in-progress recordings)
find "${RECORDINGS_DIR}" -name "*.wav" -type f -mmin +5 | while read WAV_FILE; do # Build MP3 path (mirror directory structure) REL_PATH="${WAV_FILE#${RECORDINGS_DIR}/}" MP3_FILE="${ARCHIVE_DIR}/${REL_PATH%.wav}.mp3" MP3_DIR=$(dirname "${MP3_FILE}") # Create directory mkdir -p "${MP3_DIR}" # Convert if lame --quiet -V2 "${WAV_FILE}" "${MP3_FILE}" 2>/dev/null; then echo "$(date '+%Y-%m-%d %H:%M:%S') Converted: ${REL_PATH}" >> "${LOG}" # Delete original WAV after successful conversion rm -f "${WAV_FILE}" else echo "$(date '+%Y-%m-%d %H:%M:%S') FAILED: ${REL_PATH}" >> "${LOG}" fi
done # Clean up empty directories
find "${RECORDINGS_DIR}" -type d -empty -delete 2>/dev/null
#!/bin/bash
# /usr/local/bin/convert-recordings.sh
# Convert WAV recordings to MP3 and archive RECORDINGS_DIR="/var/lib/freeswitch/recordings"
ARCHIVE_DIR="/var/lib/freeswitch/recordings-mp3"
LOG="/var/log/recording-convert.log" # Install lame if not present
which lame > /dev/null 2>&1 || apt-get install -y lame # Process WAV files older than 5 minutes (avoid in-progress recordings)
find "${RECORDINGS_DIR}" -name "*.wav" -type f -mmin +5 | while read WAV_FILE; do # Build MP3 path (mirror directory structure) REL_PATH="${WAV_FILE#${RECORDINGS_DIR}/}" MP3_FILE="${ARCHIVE_DIR}/${REL_PATH%.wav}.mp3" MP3_DIR=$(dirname "${MP3_FILE}") # Create directory mkdir -p "${MP3_DIR}" # Convert if lame --quiet -V2 "${WAV_FILE}" "${MP3_FILE}" 2>/dev/null; then echo "$(date '+%Y-%m-%d %H:%M:%S') Converted: ${REL_PATH}" >> "${LOG}" # Delete original WAV after successful conversion rm -f "${WAV_FILE}" else echo "$(date '+%Y-%m-%d %H:%M:%S') FAILED: ${REL_PATH}" >> "${LOG}" fi
done # Clean up empty directories
find "${RECORDINGS_DIR}" -type d -empty -delete 2>/dev/null
#!/bin/bash
# /usr/local/bin/convert-recordings.sh
# Convert WAV recordings to MP3 and archive RECORDINGS_DIR="/var/lib/freeswitch/recordings"
ARCHIVE_DIR="/var/lib/freeswitch/recordings-mp3"
LOG="/var/log/recording-convert.log" # Install lame if not present
which lame > /dev/null 2>&1 || apt-get install -y lame # Process WAV files older than 5 minutes (avoid in-progress recordings)
find "${RECORDINGS_DIR}" -name "*.wav" -type f -mmin +5 | while read WAV_FILE; do # Build MP3 path (mirror directory structure) REL_PATH="${WAV_FILE#${RECORDINGS_DIR}/}" MP3_FILE="${ARCHIVE_DIR}/${REL_PATH%.wav}.mp3" MP3_DIR=$(dirname "${MP3_FILE}") # Create directory mkdir -p "${MP3_DIR}" # Convert if lame --quiet -V2 "${WAV_FILE}" "${MP3_FILE}" 2>/dev/null; then echo "$(date '+%Y-%m-%d %H:%M:%S') Converted: ${REL_PATH}" >> "${LOG}" # Delete original WAV after successful conversion rm -f "${WAV_FILE}" else echo "$(date '+%Y-%m-%d %H:%M:%S') FAILED: ${REL_PATH}" >> "${LOG}" fi
done # Clean up empty directories
find "${RECORDINGS_DIR}" -type d -empty -delete 2>/dev/null
chmod +x /usr/local/bin/convert-recordings.sh # Run every 30 minutes
echo "*/30 * * * * root /usr/local/bin/convert-recordings.sh" \ > /etc/cron.d/recording-convert
chmod +x /usr/local/bin/convert-recordings.sh # Run every 30 minutes
echo "*/30 * * * * root /usr/local/bin/convert-recordings.sh" \ > /etc/cron.d/recording-convert
chmod +x /usr/local/bin/convert-recordings.sh # Run every 30 minutes
echo "*/30 * * * * root /usr/local/bin/convert-recordings.sh" \ > /etc/cron.d/recording-convert
#!/bin/bash
# /usr/local/bin/cleanup-recordings.sh
# Delete recordings older than retention period RECORDINGS_DIR="/var/lib/freeswitch/recordings"
MP3_DIR="/var/lib/freeswitch/recordings-mp3"
RETENTION_DAYS=90 echo "$(date '+%Y-%m-%d %H:%M:%S') — Recording cleanup starting"
echo "Retention: ${RETENTION_DAYS} days" # Delete old WAV files
WAV_COUNT=$(find "${RECORDINGS_DIR}" -name "*.wav" -type f -mtime +${RETENTION_DAYS} | wc -l)
find "${RECORDINGS_DIR}" -name "*.wav" -type f -mtime +${RETENTION_DAYS} -delete
echo "Deleted ${WAV_COUNT} WAV files older than ${RETENTION_DAYS} days" # Delete old MP3 files
MP3_COUNT=$(find "${MP3_DIR}" -name "*.mp3" -type f -mtime +${RETENTION_DAYS} | wc -l)
find "${MP3_DIR}" -name "*.mp3" -type f -mtime +${RETENTION_DAYS} -delete
echo "Deleted ${MP3_COUNT} MP3 files older than ${RETENTION_DAYS} days" # Clean empty directories
find "${RECORDINGS_DIR}" -type d -empty -delete 2>/dev/null
find "${MP3_DIR}" -type d -empty -delete 2>/dev/null # Report disk usage
echo "Current recording storage:"
du -sh "${RECORDINGS_DIR}" "${MP3_DIR}" 2>/dev/null echo "$(date '+%Y-%m-%d %H:%M:%S') — Cleanup complete"
#!/bin/bash
# /usr/local/bin/cleanup-recordings.sh
# Delete recordings older than retention period RECORDINGS_DIR="/var/lib/freeswitch/recordings"
MP3_DIR="/var/lib/freeswitch/recordings-mp3"
RETENTION_DAYS=90 echo "$(date '+%Y-%m-%d %H:%M:%S') — Recording cleanup starting"
echo "Retention: ${RETENTION_DAYS} days" # Delete old WAV files
WAV_COUNT=$(find "${RECORDINGS_DIR}" -name "*.wav" -type f -mtime +${RETENTION_DAYS} | wc -l)
find "${RECORDINGS_DIR}" -name "*.wav" -type f -mtime +${RETENTION_DAYS} -delete
echo "Deleted ${WAV_COUNT} WAV files older than ${RETENTION_DAYS} days" # Delete old MP3 files
MP3_COUNT=$(find "${MP3_DIR}" -name "*.mp3" -type f -mtime +${RETENTION_DAYS} | wc -l)
find "${MP3_DIR}" -name "*.mp3" -type f -mtime +${RETENTION_DAYS} -delete
echo "Deleted ${MP3_COUNT} MP3 files older than ${RETENTION_DAYS} days" # Clean empty directories
find "${RECORDINGS_DIR}" -type d -empty -delete 2>/dev/null
find "${MP3_DIR}" -type d -empty -delete 2>/dev/null # Report disk usage
echo "Current recording storage:"
du -sh "${RECORDINGS_DIR}" "${MP3_DIR}" 2>/dev/null echo "$(date '+%Y-%m-%d %H:%M:%S') — Cleanup complete"
#!/bin/bash
# /usr/local/bin/cleanup-recordings.sh
# Delete recordings older than retention period RECORDINGS_DIR="/var/lib/freeswitch/recordings"
MP3_DIR="/var/lib/freeswitch/recordings-mp3"
RETENTION_DAYS=90 echo "$(date '+%Y-%m-%d %H:%M:%S') — Recording cleanup starting"
echo "Retention: ${RETENTION_DAYS} days" # Delete old WAV files
WAV_COUNT=$(find "${RECORDINGS_DIR}" -name "*.wav" -type f -mtime +${RETENTION_DAYS} | wc -l)
find "${RECORDINGS_DIR}" -name "*.wav" -type f -mtime +${RETENTION_DAYS} -delete
echo "Deleted ${WAV_COUNT} WAV files older than ${RETENTION_DAYS} days" # Delete old MP3 files
MP3_COUNT=$(find "${MP3_DIR}" -name "*.mp3" -type f -mtime +${RETENTION_DAYS} | wc -l)
find "${MP3_DIR}" -name "*.mp3" -type f -mtime +${RETENTION_DAYS} -delete
echo "Deleted ${MP3_COUNT} MP3 files older than ${RETENTION_DAYS} days" # Clean empty directories
find "${RECORDINGS_DIR}" -type d -empty -delete 2>/dev/null
find "${MP3_DIR}" -type d -empty -delete 2>/dev/null # Report disk usage
echo "Current recording storage:"
du -sh "${RECORDINGS_DIR}" "${MP3_DIR}" 2>/dev/null echo "$(date '+%Y-%m-%d %H:%M:%S') — Cleanup complete"
chmod +x /usr/local/bin/cleanup-recordings.sh # Run daily at 4 AM
echo "0 4 * * * root /usr/local/bin/cleanup-recordings.sh >> /var/log/recording-cleanup.log 2>&1" \ > /etc/cron.d/recording-cleanup
chmod +x /usr/local/bin/cleanup-recordings.sh # Run daily at 4 AM
echo "0 4 * * * root /usr/local/bin/cleanup-recordings.sh >> /var/log/recording-cleanup.log 2>&1" \ > /etc/cron.d/recording-cleanup
chmod +x /usr/local/bin/cleanup-recordings.sh # Run daily at 4 AM
echo "0 4 * * * root /usr/local/bin/cleanup-recordings.sh >> /var/log/recording-cleanup.log 2>&1" \ > /etc/cron.d/recording-cleanup
<configuration name="conference.conf" description="Audio Conference"> <advertise> <!-- Advertise conference rooms via SIP SUBSCRIBE --> <room name="3001@$${domain}" status="FreeSWITCH"/> </advertise> <caller-controls> <!-- Default key bindings for participants --> <group name="default"> <control action="mute" digits="0"/> <control action="deaf mute" digits="*"/> <control action="energy up" digits="9"/> <control action="energy equ" digits="8"/> <control action="energy dn" digits="7"/> <control action="vol talk up" digits="3"/> <control action="vol talk zero" digits="2"/> <control action="vol talk dn" digits="1"/> <control action="vol listen up" digits="6"/> <control action="vol listen zero" digits="5"/> <control action="vol listen dn" digits="4"/> <control action="hangup" digits="#"/> </group> <!-- Moderator key bindings --> <group name="moderator"> <control action="mute" digits="0"/> <control action="deaf mute" digits="*"/> <control action="hangup" digits="#"/> <control action="lock" digits="*1"/> <control action="mute non_moderator" digits="*5"/> <control action="unmute non_moderator" digits="*6"/> <control action="kick last" digits="*7"/> <control action="transfer" digits="*9" data="1009 XML default"/> </group> </caller-controls> <profiles> <!-- Standard conference profile --> <profile name="default"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <!-- Comfort noise for silence --> <param name="comfort-noise" value="true"/> <!-- Sounds --> <param name="muted-sound" value="conference/conf-muted.wav"/> <param name="unmuted-sound" value="conference/conf-unmuted.wav"/> <param name="alone-sound" value="conference/conf-alone.wav"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="kicked-sound" value="conference/conf-kicked.wav"/> <param name="locked-sound" value="conference/conf-locked.wav"/> <param name="is-locked-sound" value="conference/conf-is-locked.wav"/> <param name="is-unlocked-sound" value="conference/conf-is-unlocked.wav"/> <param name="pin-sound" value="conference/conf-pin.wav"/> <param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/> <!-- Controls --> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <!-- Auto-record all conferences --> <!-- <param name="auto-record" value="/var/lib/freeswitch/recordings/conference/${conference_name}_${strftime(%Y%m%d-%H%M%S)}.wav"/> --> <!-- Max members (0 = unlimited) --> <param name="max-members" value="100"/> <!-- Announce count of members when joining --> <param name="announce-count" value="5"/> <!-- Codec preferences --> <param name="conference-flags" value="wait-mod|audio-always|waste-bandwidth"/> </profile> <!-- PIN-protected conference profile --> <profile name="secure"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <param name="comfort-noise" value="true"/> <!-- PIN required --> <param name="pin" value="12345"/> <param name="pin-retries" value="3"/> <param name="pin-sound" value="conference/conf-pin.wav"/> <param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <param name="max-members" value="50"/> <param name="conference-flags" value="wait-mod|audio-always"/> </profile> <!-- Webinar profile: listeners muted by default --> <profile name="webinar"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <param name="comfort-noise" value="true"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <param name="max-members" value="500"/> <!-- Members join muted --> <param name="member-flags" value="mute"/> <param name="conference-flags" value="wait-mod|audio-always"/> <!-- Auto-record webinars --> <param name="auto-record" value="/var/lib/freeswitch/recordings/webinar/${conference_name}_${strftime(%Y%m%d-%H%M%S)}.wav"/> </profile> </profiles>
</configuration>
<configuration name="conference.conf" description="Audio Conference"> <advertise> <!-- Advertise conference rooms via SIP SUBSCRIBE --> <room name="3001@$${domain}" status="FreeSWITCH"/> </advertise> <caller-controls> <!-- Default key bindings for participants --> <group name="default"> <control action="mute" digits="0"/> <control action="deaf mute" digits="*"/> <control action="energy up" digits="9"/> <control action="energy equ" digits="8"/> <control action="energy dn" digits="7"/> <control action="vol talk up" digits="3"/> <control action="vol talk zero" digits="2"/> <control action="vol talk dn" digits="1"/> <control action="vol listen up" digits="6"/> <control action="vol listen zero" digits="5"/> <control action="vol listen dn" digits="4"/> <control action="hangup" digits="#"/> </group> <!-- Moderator key bindings --> <group name="moderator"> <control action="mute" digits="0"/> <control action="deaf mute" digits="*"/> <control action="hangup" digits="#"/> <control action="lock" digits="*1"/> <control action="mute non_moderator" digits="*5"/> <control action="unmute non_moderator" digits="*6"/> <control action="kick last" digits="*7"/> <control action="transfer" digits="*9" data="1009 XML default"/> </group> </caller-controls> <profiles> <!-- Standard conference profile --> <profile name="default"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <!-- Comfort noise for silence --> <param name="comfort-noise" value="true"/> <!-- Sounds --> <param name="muted-sound" value="conference/conf-muted.wav"/> <param name="unmuted-sound" value="conference/conf-unmuted.wav"/> <param name="alone-sound" value="conference/conf-alone.wav"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="kicked-sound" value="conference/conf-kicked.wav"/> <param name="locked-sound" value="conference/conf-locked.wav"/> <param name="is-locked-sound" value="conference/conf-is-locked.wav"/> <param name="is-unlocked-sound" value="conference/conf-is-unlocked.wav"/> <param name="pin-sound" value="conference/conf-pin.wav"/> <param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/> <!-- Controls --> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <!-- Auto-record all conferences --> <!-- <param name="auto-record" value="/var/lib/freeswitch/recordings/conference/${conference_name}_${strftime(%Y%m%d-%H%M%S)}.wav"/> --> <!-- Max members (0 = unlimited) --> <param name="max-members" value="100"/> <!-- Announce count of members when joining --> <param name="announce-count" value="5"/> <!-- Codec preferences --> <param name="conference-flags" value="wait-mod|audio-always|waste-bandwidth"/> </profile> <!-- PIN-protected conference profile --> <profile name="secure"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <param name="comfort-noise" value="true"/> <!-- PIN required --> <param name="pin" value="12345"/> <param name="pin-retries" value="3"/> <param name="pin-sound" value="conference/conf-pin.wav"/> <param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <param name="max-members" value="50"/> <param name="conference-flags" value="wait-mod|audio-always"/> </profile> <!-- Webinar profile: listeners muted by default --> <profile name="webinar"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <param name="comfort-noise" value="true"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <param name="max-members" value="500"/> <!-- Members join muted --> <param name="member-flags" value="mute"/> <param name="conference-flags" value="wait-mod|audio-always"/> <!-- Auto-record webinars --> <param name="auto-record" value="/var/lib/freeswitch/recordings/webinar/${conference_name}_${strftime(%Y%m%d-%H%M%S)}.wav"/> </profile> </profiles>
</configuration>
<configuration name="conference.conf" description="Audio Conference"> <advertise> <!-- Advertise conference rooms via SIP SUBSCRIBE --> <room name="3001@$${domain}" status="FreeSWITCH"/> </advertise> <caller-controls> <!-- Default key bindings for participants --> <group name="default"> <control action="mute" digits="0"/> <control action="deaf mute" digits="*"/> <control action="energy up" digits="9"/> <control action="energy equ" digits="8"/> <control action="energy dn" digits="7"/> <control action="vol talk up" digits="3"/> <control action="vol talk zero" digits="2"/> <control action="vol talk dn" digits="1"/> <control action="vol listen up" digits="6"/> <control action="vol listen zero" digits="5"/> <control action="vol listen dn" digits="4"/> <control action="hangup" digits="#"/> </group> <!-- Moderator key bindings --> <group name="moderator"> <control action="mute" digits="0"/> <control action="deaf mute" digits="*"/> <control action="hangup" digits="#"/> <control action="lock" digits="*1"/> <control action="mute non_moderator" digits="*5"/> <control action="unmute non_moderator" digits="*6"/> <control action="kick last" digits="*7"/> <control action="transfer" digits="*9" data="1009 XML default"/> </group> </caller-controls> <profiles> <!-- Standard conference profile --> <profile name="default"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <!-- Comfort noise for silence --> <param name="comfort-noise" value="true"/> <!-- Sounds --> <param name="muted-sound" value="conference/conf-muted.wav"/> <param name="unmuted-sound" value="conference/conf-unmuted.wav"/> <param name="alone-sound" value="conference/conf-alone.wav"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="kicked-sound" value="conference/conf-kicked.wav"/> <param name="locked-sound" value="conference/conf-locked.wav"/> <param name="is-locked-sound" value="conference/conf-is-locked.wav"/> <param name="is-unlocked-sound" value="conference/conf-is-unlocked.wav"/> <param name="pin-sound" value="conference/conf-pin.wav"/> <param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/> <!-- Controls --> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <!-- Auto-record all conferences --> <!-- <param name="auto-record" value="/var/lib/freeswitch/recordings/conference/${conference_name}_${strftime(%Y%m%d-%H%M%S)}.wav"/> --> <!-- Max members (0 = unlimited) --> <param name="max-members" value="100"/> <!-- Announce count of members when joining --> <param name="announce-count" value="5"/> <!-- Codec preferences --> <param name="conference-flags" value="wait-mod|audio-always|waste-bandwidth"/> </profile> <!-- PIN-protected conference profile --> <profile name="secure"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <param name="comfort-noise" value="true"/> <!-- PIN required --> <param name="pin" value="12345"/> <param name="pin-retries" value="3"/> <param name="pin-sound" value="conference/conf-pin.wav"/> <param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <param name="max-members" value="50"/> <param name="conference-flags" value="wait-mod|audio-always"/> </profile> <!-- Webinar profile: listeners muted by default --> <profile name="webinar"> <param name="domain" value="$${domain}"/> <param name="rate" value="16000"/> <param name="interval" value="20"/> <param name="energy-level" value="100"/> <param name="comfort-noise" value="true"/> <param name="enter-sound" value="tone_stream://%(200,0,500,600,700)"/> <param name="exit-sound" value="tone_stream://%(500,0,300,200,100,50,25)"/> <param name="caller-controls" value="default"/> <param name="moderator-controls" value="moderator"/> <param name="max-members" value="500"/> <!-- Members join muted --> <param name="member-flags" value="mute"/> <param name="conference-flags" value="wait-mod|audio-always"/> <!-- Auto-record webinars --> <param name="auto-record" value="/var/lib/freeswitch/recordings/webinar/${conference_name}_${strftime(%Y%m%d-%H%M%S)}.wav"/> </profile> </profiles>
</configuration>
<!-- ============================================ -->
<!-- CONFERENCE ROOMS -->
<!-- ============================================ --> <!-- Standard conference rooms: dial 3001-3099 -->
<extension name="conference_standard"> <condition field="destination_number" expression="^(30[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="room_$1@default"/> </condition>
</extension> <!-- PIN-protected conference rooms: dial 3100-3199 -->
<extension name="conference_secure"> <condition field="destination_number" expression="^(31[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="room_$1@secure"/> </condition>
</extension> <!-- Moderator entry: dial 3200 + room number (e.g., 32003001 for room 3001) -->
<extension name="conference_moderator"> <condition field="destination_number" expression="^3200(30[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="set" data="conference_member_flags=moderator"/> <action application="conference" data="room_$1@default+flags{moderator}"/> </condition>
</extension> <!-- Webinar rooms: dial 3300-3399 (listeners muted) -->
<extension name="conference_webinar"> <condition field="destination_number" expression="^(33[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="webinar_$1@webinar"/> </condition>
</extension> <!-- Webinar presenter: dial 3400 + room number -->
<extension name="conference_webinar_presenter"> <condition field="destination_number" expression="^3400(33[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="webinar_$1@webinar+flags{moderator}"/> </condition>
</extension>
<!-- ============================================ -->
<!-- CONFERENCE ROOMS -->
<!-- ============================================ --> <!-- Standard conference rooms: dial 3001-3099 -->
<extension name="conference_standard"> <condition field="destination_number" expression="^(30[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="room_$1@default"/> </condition>
</extension> <!-- PIN-protected conference rooms: dial 3100-3199 -->
<extension name="conference_secure"> <condition field="destination_number" expression="^(31[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="room_$1@secure"/> </condition>
</extension> <!-- Moderator entry: dial 3200 + room number (e.g., 32003001 for room 3001) -->
<extension name="conference_moderator"> <condition field="destination_number" expression="^3200(30[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="set" data="conference_member_flags=moderator"/> <action application="conference" data="room_$1@default+flags{moderator}"/> </condition>
</extension> <!-- Webinar rooms: dial 3300-3399 (listeners muted) -->
<extension name="conference_webinar"> <condition field="destination_number" expression="^(33[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="webinar_$1@webinar"/> </condition>
</extension> <!-- Webinar presenter: dial 3400 + room number -->
<extension name="conference_webinar_presenter"> <condition field="destination_number" expression="^3400(33[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="webinar_$1@webinar+flags{moderator}"/> </condition>
</extension>
<!-- ============================================ -->
<!-- CONFERENCE ROOMS -->
<!-- ============================================ --> <!-- Standard conference rooms: dial 3001-3099 -->
<extension name="conference_standard"> <condition field="destination_number" expression="^(30[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="room_$1@default"/> </condition>
</extension> <!-- PIN-protected conference rooms: dial 3100-3199 -->
<extension name="conference_secure"> <condition field="destination_number" expression="^(31[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="room_$1@secure"/> </condition>
</extension> <!-- Moderator entry: dial 3200 + room number (e.g., 32003001 for room 3001) -->
<extension name="conference_moderator"> <condition field="destination_number" expression="^3200(30[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="set" data="conference_member_flags=moderator"/> <action application="conference" data="room_$1@default+flags{moderator}"/> </condition>
</extension> <!-- Webinar rooms: dial 3300-3399 (listeners muted) -->
<extension name="conference_webinar"> <condition field="destination_number" expression="^(33[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="webinar_$1@webinar"/> </condition>
</extension> <!-- Webinar presenter: dial 3400 + room number -->
<extension name="conference_webinar_presenter"> <condition field="destination_number" expression="^3400(33[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="conference" data="webinar_$1@webinar+flags{moderator}"/> </condition>
</extension>
<extension name="conference_dynamic_pin"> <condition field="destination_number" expression="^(35[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <!-- Collect PIN from caller --> <action application="play_and_get_digits" data="4 6 3 10000 # conference/conf-pin.wav conference/conf-bad-pin.wav entered_pin \d{4,6} 10000"/> <!-- Verify PIN (in production, check against a database) --> <action application="set" data="expected_pin=7890"/> <action application="execute_extension" data="check_pin_${entered_pin} XML features"/> <!-- If we get here, PIN was correct --> <action application="conference" data="room_$1@default"/> </condition>
</extension>
<extension name="conference_dynamic_pin"> <condition field="destination_number" expression="^(35[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <!-- Collect PIN from caller --> <action application="play_and_get_digits" data="4 6 3 10000 # conference/conf-pin.wav conference/conf-bad-pin.wav entered_pin \d{4,6} 10000"/> <!-- Verify PIN (in production, check against a database) --> <action application="set" data="expected_pin=7890"/> <action application="execute_extension" data="check_pin_${entered_pin} XML features"/> <!-- If we get here, PIN was correct --> <action application="conference" data="room_$1@default"/> </condition>
</extension>
<extension name="conference_dynamic_pin"> <condition field="destination_number" expression="^(35[0-9]{2})$"> <action application="answer"/> <action application="sleep" data="500"/> <!-- Collect PIN from caller --> <action application="play_and_get_digits" data="4 6 3 10000 # conference/conf-pin.wav conference/conf-bad-pin.wav entered_pin \d{4,6} 10000"/> <!-- Verify PIN (in production, check against a database) --> <action application="set" data="expected_pin=7890"/> <action application="execute_extension" data="check_pin_${entered_pin} XML features"/> <!-- If we get here, PIN was correct --> <action application="conference" data="room_$1@default"/> </condition>
</extension>
# List active conferences
fs_cli -x "conference list" # List members in a specific conference
fs_cli -x "conference room_3001 list" # Mute a participant (by member ID)
fs_cli -x "conference room_3001 mute 3" # Unmute a participant
fs_cli -x "conference room_3001 unmute 3" # Kick a participant
fs_cli -x "conference room_3001 kick 3" # Lock the conference (no new members)
fs_cli -x "conference room_3001 lock" # Unlock
fs_cli -x "conference room_3001 unlock" # Mute all non-moderators
fs_cli -x "conference room_3001 mute non_moderator" # Start recording a conference
fs_cli -x "conference room_3001 record /var/lib/freeswitch/recordings/conference/room_3001_$(date +%Y%m%d).wav" # Stop recording
fs_cli -x "conference room_3001 norecord all" # Play a file into the conference
fs_cli -x "conference room_3001 play /var/lib/freeswitch/sounds/announcement.wav" # Get conference count
fs_cli -x "conference room_3001 count"
# List active conferences
fs_cli -x "conference list" # List members in a specific conference
fs_cli -x "conference room_3001 list" # Mute a participant (by member ID)
fs_cli -x "conference room_3001 mute 3" # Unmute a participant
fs_cli -x "conference room_3001 unmute 3" # Kick a participant
fs_cli -x "conference room_3001 kick 3" # Lock the conference (no new members)
fs_cli -x "conference room_3001 lock" # Unlock
fs_cli -x "conference room_3001 unlock" # Mute all non-moderators
fs_cli -x "conference room_3001 mute non_moderator" # Start recording a conference
fs_cli -x "conference room_3001 record /var/lib/freeswitch/recordings/conference/room_3001_$(date +%Y%m%d).wav" # Stop recording
fs_cli -x "conference room_3001 norecord all" # Play a file into the conference
fs_cli -x "conference room_3001 play /var/lib/freeswitch/sounds/announcement.wav" # Get conference count
fs_cli -x "conference room_3001 count"
# List active conferences
fs_cli -x "conference list" # List members in a specific conference
fs_cli -x "conference room_3001 list" # Mute a participant (by member ID)
fs_cli -x "conference room_3001 mute 3" # Unmute a participant
fs_cli -x "conference room_3001 unmute 3" # Kick a participant
fs_cli -x "conference room_3001 kick 3" # Lock the conference (no new members)
fs_cli -x "conference room_3001 lock" # Unlock
fs_cli -x "conference room_3001 unlock" # Mute all non-moderators
fs_cli -x "conference room_3001 mute non_moderator" # Start recording a conference
fs_cli -x "conference room_3001 record /var/lib/freeswitch/recordings/conference/room_3001_$(date +%Y%m%d).wav" # Stop recording
fs_cli -x "conference room_3001 norecord all" # Play a file into the conference
fs_cli -x "conference room_3001 play /var/lib/freeswitch/sounds/announcement.wav" # Get conference count
fs_cli -x "conference room_3001 count"
<configuration name="event_socket.conf" description="Socket Client"> <settings> <!-- Listen on localhost only (secure) --> <param name="listen-ip" value="127.0.0.1"/> <param name="listen-port" value="8021"/> <param name="password" value="YOUR_SECURE_ESL_PASSWORD"/> <!-- Optional: allow connections from specific IPs --> <!--<param name="listen-ip" value="0.0.0.0"/>--> <!--<param name="apply-inbound-acl" value="loopback.auto"/>--> </settings>
</configuration>
<configuration name="event_socket.conf" description="Socket Client"> <settings> <!-- Listen on localhost only (secure) --> <param name="listen-ip" value="127.0.0.1"/> <param name="listen-port" value="8021"/> <param name="password" value="YOUR_SECURE_ESL_PASSWORD"/> <!-- Optional: allow connections from specific IPs --> <!--<param name="listen-ip" value="0.0.0.0"/>--> <!--<param name="apply-inbound-acl" value="loopback.auto"/>--> </settings>
</configuration>
<configuration name="event_socket.conf" description="Socket Client"> <settings> <!-- Listen on localhost only (secure) --> <param name="listen-ip" value="127.0.0.1"/> <param name="listen-port" value="8021"/> <param name="password" value="YOUR_SECURE_ESL_PASSWORD"/> <!-- Optional: allow connections from specific IPs --> <!--<param name="listen-ip" value="0.0.0.0"/>--> <!--<param name="apply-inbound-acl" value="loopback.auto"/>--> </settings>
</configuration>
# Connect to local FreeSWITCH
fs_cli # Connect to remote FreeSWITCH
fs_cli -H 10.0.0.5 -P 8021 -p YOUR_SECURE_ESL_PASSWORD # Execute a single command and exit
fs_cli -x "sofia status" # Execute API command
fs_cli -x "show channels" # Execute background API command
fs_cli -x "bgapi originate user/1001 &echo()"
# Connect to local FreeSWITCH
fs_cli # Connect to remote FreeSWITCH
fs_cli -H 10.0.0.5 -P 8021 -p YOUR_SECURE_ESL_PASSWORD # Execute a single command and exit
fs_cli -x "sofia status" # Execute API command
fs_cli -x "show channels" # Execute background API command
fs_cli -x "bgapi originate user/1001 &echo()"
# Connect to local FreeSWITCH
fs_cli # Connect to remote FreeSWITCH
fs_cli -H 10.0.0.5 -P 8021 -p YOUR_SECURE_ESL_PASSWORD # Execute a single command and exit
fs_cli -x "sofia status" # Execute API command
fs_cli -x "show channels" # Execute background API command
fs_cli -x "bgapi originate user/1001 &echo()"
pip3 install greenswitch
pip3 install greenswitch
pip3 install greenswitch
#!/usr/bin/env python3
"""
freeswitch_monitor.py
Monitor FreeSWITCH events via ESL (inbound mode).
Logs call activity and tracks concurrent calls.
""" import greenswitch
import json
from datetime import datetime class FreeSWITCHMonitor: def __init__(self, host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD'): self.host = host self.port = port self.password = password self.active_calls = {} self.conn = None def connect(self): """Establish ESL connection.""" self.conn = greenswitch.InboundESL( host=self.host, port=self.port, password=self.password ) self.conn.connect() print(f"[{self._now()}] Connected to FreeSWITCH at {self.host}:{self.port}") def subscribe_events(self): """Subscribe to call-related events.""" # Subscribe to specific events self.conn.send( 'event plain CHANNEL_CREATE CHANNEL_ANSWER ' 'CHANNEL_HANGUP_COMPLETE DTMF CODEC' ) # Register event handlers self.conn.register_handle('CHANNEL_CREATE', self._on_channel_create) self.conn.register_handle('CHANNEL_ANSWER', self._on_channel_answer) self.conn.register_handle('CHANNEL_HANGUP_COMPLETE', self._on_channel_hangup) self.conn.register_handle('DTMF', self._on_dtmf) def _on_channel_create(self, event): """Called when a new channel is created.""" uuid = event.headers.get('Unique-ID', 'unknown') caller = event.headers.get('Caller-Caller-ID-Number', 'unknown') dest = event.headers.get('Caller-Destination-Number', 'unknown') direction = event.headers.get('Call-Direction', 'unknown') self.active_calls[uuid] = { 'caller': caller, 'destination': dest, 'direction': direction, 'created': datetime.now(), 'state': 'ringing' } print(f"[{self._now()}] NEW CALL: {caller} -> {dest} ({direction}) UUID={uuid}") print(f" Active calls: {len(self.active_calls)}") def _on_channel_answer(self, event): """Called when a channel is answered.""" uuid = event.headers.get('Unique-ID', 'unknown') if uuid in self.active_calls: self.active_calls[uuid]['state'] = 'answered' caller = self.active_calls[uuid]['caller'] dest = self.active_calls[uuid]['destination'] print(f"[{self._now()}] ANSWERED: {caller} -> {dest} UUID={uuid}") def _on_channel_hangup(self, event): """Called when a channel hangs up.""" uuid = event.headers.get('Unique-ID', 'unknown') cause = event.headers.get('Hangup-Cause', 'unknown') duration = event.headers.get('variable_billsec', '0') if uuid in self.active_calls: call = self.active_calls.pop(uuid) print( f"[{self._now()}] HANGUP: {call['caller']} -> {call['destination']} " f"Duration={duration}s Cause={cause} UUID={uuid}" ) print(f" Active calls: {len(self.active_calls)}") def _on_dtmf(self, event): """Called when DTMF is received.""" uuid = event.headers.get('Unique-ID', 'unknown') digit = event.headers.get('DTMF-Digit', '?') print(f"[{self._now()}] DTMF: digit={digit} UUID={uuid}") def run_api(self, command): """Execute an API command and return the result.""" result = self.conn.send(f'api {command}') return result.data if result else None def originate_call(self, endpoint, app='&park()'): """Place a new call.""" cmd = f'api originate {endpoint} {app}' result = self.conn.send(cmd) print(f"[{self._now()}] Originate result: {result.data if result else 'failed'}") return result def _now(self): return datetime.now().strftime('%Y-%m-%d %H:%M:%S') def run(self): """Main event loop.""" self.connect() self.subscribe_events() print(f"[{self._now()}] Listening for events... (Ctrl+C to stop)") # Get initial status status = self.run_api('status') if status: print(f"\n--- FreeSWITCH Status ---\n{status}\n") try: # This blocks and processes events self.conn.process_events() except KeyboardInterrupt: print(f"\n[{self._now()}] Shutting down...") if __name__ == '__main__': monitor = FreeSWITCHMonitor( host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD' ) monitor.run()
#!/usr/bin/env python3
"""
freeswitch_monitor.py
Monitor FreeSWITCH events via ESL (inbound mode).
Logs call activity and tracks concurrent calls.
""" import greenswitch
import json
from datetime import datetime class FreeSWITCHMonitor: def __init__(self, host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD'): self.host = host self.port = port self.password = password self.active_calls = {} self.conn = None def connect(self): """Establish ESL connection.""" self.conn = greenswitch.InboundESL( host=self.host, port=self.port, password=self.password ) self.conn.connect() print(f"[{self._now()}] Connected to FreeSWITCH at {self.host}:{self.port}") def subscribe_events(self): """Subscribe to call-related events.""" # Subscribe to specific events self.conn.send( 'event plain CHANNEL_CREATE CHANNEL_ANSWER ' 'CHANNEL_HANGUP_COMPLETE DTMF CODEC' ) # Register event handlers self.conn.register_handle('CHANNEL_CREATE', self._on_channel_create) self.conn.register_handle('CHANNEL_ANSWER', self._on_channel_answer) self.conn.register_handle('CHANNEL_HANGUP_COMPLETE', self._on_channel_hangup) self.conn.register_handle('DTMF', self._on_dtmf) def _on_channel_create(self, event): """Called when a new channel is created.""" uuid = event.headers.get('Unique-ID', 'unknown') caller = event.headers.get('Caller-Caller-ID-Number', 'unknown') dest = event.headers.get('Caller-Destination-Number', 'unknown') direction = event.headers.get('Call-Direction', 'unknown') self.active_calls[uuid] = { 'caller': caller, 'destination': dest, 'direction': direction, 'created': datetime.now(), 'state': 'ringing' } print(f"[{self._now()}] NEW CALL: {caller} -> {dest} ({direction}) UUID={uuid}") print(f" Active calls: {len(self.active_calls)}") def _on_channel_answer(self, event): """Called when a channel is answered.""" uuid = event.headers.get('Unique-ID', 'unknown') if uuid in self.active_calls: self.active_calls[uuid]['state'] = 'answered' caller = self.active_calls[uuid]['caller'] dest = self.active_calls[uuid]['destination'] print(f"[{self._now()}] ANSWERED: {caller} -> {dest} UUID={uuid}") def _on_channel_hangup(self, event): """Called when a channel hangs up.""" uuid = event.headers.get('Unique-ID', 'unknown') cause = event.headers.get('Hangup-Cause', 'unknown') duration = event.headers.get('variable_billsec', '0') if uuid in self.active_calls: call = self.active_calls.pop(uuid) print( f"[{self._now()}] HANGUP: {call['caller']} -> {call['destination']} " f"Duration={duration}s Cause={cause} UUID={uuid}" ) print(f" Active calls: {len(self.active_calls)}") def _on_dtmf(self, event): """Called when DTMF is received.""" uuid = event.headers.get('Unique-ID', 'unknown') digit = event.headers.get('DTMF-Digit', '?') print(f"[{self._now()}] DTMF: digit={digit} UUID={uuid}") def run_api(self, command): """Execute an API command and return the result.""" result = self.conn.send(f'api {command}') return result.data if result else None def originate_call(self, endpoint, app='&park()'): """Place a new call.""" cmd = f'api originate {endpoint} {app}' result = self.conn.send(cmd) print(f"[{self._now()}] Originate result: {result.data if result else 'failed'}") return result def _now(self): return datetime.now().strftime('%Y-%m-%d %H:%M:%S') def run(self): """Main event loop.""" self.connect() self.subscribe_events() print(f"[{self._now()}] Listening for events... (Ctrl+C to stop)") # Get initial status status = self.run_api('status') if status: print(f"\n--- FreeSWITCH Status ---\n{status}\n") try: # This blocks and processes events self.conn.process_events() except KeyboardInterrupt: print(f"\n[{self._now()}] Shutting down...") if __name__ == '__main__': monitor = FreeSWITCHMonitor( host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD' ) monitor.run()
#!/usr/bin/env python3
"""
freeswitch_monitor.py
Monitor FreeSWITCH events via ESL (inbound mode).
Logs call activity and tracks concurrent calls.
""" import greenswitch
import json
from datetime import datetime class FreeSWITCHMonitor: def __init__(self, host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD'): self.host = host self.port = port self.password = password self.active_calls = {} self.conn = None def connect(self): """Establish ESL connection.""" self.conn = greenswitch.InboundESL( host=self.host, port=self.port, password=self.password ) self.conn.connect() print(f"[{self._now()}] Connected to FreeSWITCH at {self.host}:{self.port}") def subscribe_events(self): """Subscribe to call-related events.""" # Subscribe to specific events self.conn.send( 'event plain CHANNEL_CREATE CHANNEL_ANSWER ' 'CHANNEL_HANGUP_COMPLETE DTMF CODEC' ) # Register event handlers self.conn.register_handle('CHANNEL_CREATE', self._on_channel_create) self.conn.register_handle('CHANNEL_ANSWER', self._on_channel_answer) self.conn.register_handle('CHANNEL_HANGUP_COMPLETE', self._on_channel_hangup) self.conn.register_handle('DTMF', self._on_dtmf) def _on_channel_create(self, event): """Called when a new channel is created.""" uuid = event.headers.get('Unique-ID', 'unknown') caller = event.headers.get('Caller-Caller-ID-Number', 'unknown') dest = event.headers.get('Caller-Destination-Number', 'unknown') direction = event.headers.get('Call-Direction', 'unknown') self.active_calls[uuid] = { 'caller': caller, 'destination': dest, 'direction': direction, 'created': datetime.now(), 'state': 'ringing' } print(f"[{self._now()}] NEW CALL: {caller} -> {dest} ({direction}) UUID={uuid}") print(f" Active calls: {len(self.active_calls)}") def _on_channel_answer(self, event): """Called when a channel is answered.""" uuid = event.headers.get('Unique-ID', 'unknown') if uuid in self.active_calls: self.active_calls[uuid]['state'] = 'answered' caller = self.active_calls[uuid]['caller'] dest = self.active_calls[uuid]['destination'] print(f"[{self._now()}] ANSWERED: {caller} -> {dest} UUID={uuid}") def _on_channel_hangup(self, event): """Called when a channel hangs up.""" uuid = event.headers.get('Unique-ID', 'unknown') cause = event.headers.get('Hangup-Cause', 'unknown') duration = event.headers.get('variable_billsec', '0') if uuid in self.active_calls: call = self.active_calls.pop(uuid) print( f"[{self._now()}] HANGUP: {call['caller']} -> {call['destination']} " f"Duration={duration}s Cause={cause} UUID={uuid}" ) print(f" Active calls: {len(self.active_calls)}") def _on_dtmf(self, event): """Called when DTMF is received.""" uuid = event.headers.get('Unique-ID', 'unknown') digit = event.headers.get('DTMF-Digit', '?') print(f"[{self._now()}] DTMF: digit={digit} UUID={uuid}") def run_api(self, command): """Execute an API command and return the result.""" result = self.conn.send(f'api {command}') return result.data if result else None def originate_call(self, endpoint, app='&park()'): """Place a new call.""" cmd = f'api originate {endpoint} {app}' result = self.conn.send(cmd) print(f"[{self._now()}] Originate result: {result.data if result else 'failed'}") return result def _now(self): return datetime.now().strftime('%Y-%m-%d %H:%M:%S') def run(self): """Main event loop.""" self.connect() self.subscribe_events() print(f"[{self._now()}] Listening for events... (Ctrl+C to stop)") # Get initial status status = self.run_api('status') if status: print(f"\n--- FreeSWITCH Status ---\n{status}\n") try: # This blocks and processes events self.conn.process_events() except KeyboardInterrupt: print(f"\n[{self._now()}] Shutting down...") if __name__ == '__main__': monitor = FreeSWITCHMonitor( host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD' ) monitor.run()
python3 freeswitch_monitor.py
python3 freeswitch_monitor.py
python3 freeswitch_monitor.py
#!/usr/bin/env python3
"""
call_queue.py
Simple call queue: callers hear hold music, agents dial in to take the next call.
Uses ESL inbound mode.
""" import greenswitch
from collections import deque
from datetime import datetime
import threading class CallQueue: def __init__(self, host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD'): self.conn = greenswitch.InboundESL(host=host, port=port, password=password) self.waiting_callers = deque() # Queue of caller UUIDs self.available_agents = [] # List of agent UUIDs self.lock = threading.Lock() def connect(self): self.conn.connect() self.conn.send('event plain CHANNEL_ANSWER CHANNEL_HANGUP_COMPLETE') print(f"[{self._now()}] Queue system connected") def add_caller(self, uuid): """Add a caller to the queue and play hold music.""" with self.lock: self.waiting_callers.append(uuid) # Play music on hold to the caller self.conn.send( f'api uuid_broadcast {uuid} ' f'local_stream://moh both' ) print(f"[{self._now()}] Caller {uuid} added to queue. " f"Queue depth: {len(self.waiting_callers)}") self._try_connect() def add_agent(self, uuid): """Register an agent as available.""" with self.lock: self.available_agents.append(uuid) print(f"[{self._now()}] Agent {uuid} available. " f"Agents: {len(self.available_agents)}") self._try_connect() def _try_connect(self): """Try to bridge a waiting caller with an available agent.""" if self.waiting_callers and self.available_agents: caller_uuid = self.waiting_callers.popleft() agent_uuid = self.available_agents.pop(0) # Bridge the two calls self.conn.send( f'api uuid_bridge {caller_uuid} {agent_uuid}' ) print( f"[{self._now()}] CONNECTED: caller={caller_uuid} " f"agent={agent_uuid}" ) def get_stats(self): """Return current queue statistics.""" with self.lock: return { 'waiting_callers': len(self.waiting_callers), 'available_agents': len(self.available_agents), 'timestamp': self._now() } def _now(self): return datetime.now().strftime('%Y-%m-%d %H:%M:%S') if __name__ == '__main__': queue = CallQueue() queue.connect() # In production, callers and agents would be added via # dialplan outbound ESL or API calls print("Queue system running. Use ESL commands to add callers/agents.")
#!/usr/bin/env python3
"""
call_queue.py
Simple call queue: callers hear hold music, agents dial in to take the next call.
Uses ESL inbound mode.
""" import greenswitch
from collections import deque
from datetime import datetime
import threading class CallQueue: def __init__(self, host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD'): self.conn = greenswitch.InboundESL(host=host, port=port, password=password) self.waiting_callers = deque() # Queue of caller UUIDs self.available_agents = [] # List of agent UUIDs self.lock = threading.Lock() def connect(self): self.conn.connect() self.conn.send('event plain CHANNEL_ANSWER CHANNEL_HANGUP_COMPLETE') print(f"[{self._now()}] Queue system connected") def add_caller(self, uuid): """Add a caller to the queue and play hold music.""" with self.lock: self.waiting_callers.append(uuid) # Play music on hold to the caller self.conn.send( f'api uuid_broadcast {uuid} ' f'local_stream://moh both' ) print(f"[{self._now()}] Caller {uuid} added to queue. " f"Queue depth: {len(self.waiting_callers)}") self._try_connect() def add_agent(self, uuid): """Register an agent as available.""" with self.lock: self.available_agents.append(uuid) print(f"[{self._now()}] Agent {uuid} available. " f"Agents: {len(self.available_agents)}") self._try_connect() def _try_connect(self): """Try to bridge a waiting caller with an available agent.""" if self.waiting_callers and self.available_agents: caller_uuid = self.waiting_callers.popleft() agent_uuid = self.available_agents.pop(0) # Bridge the two calls self.conn.send( f'api uuid_bridge {caller_uuid} {agent_uuid}' ) print( f"[{self._now()}] CONNECTED: caller={caller_uuid} " f"agent={agent_uuid}" ) def get_stats(self): """Return current queue statistics.""" with self.lock: return { 'waiting_callers': len(self.waiting_callers), 'available_agents': len(self.available_agents), 'timestamp': self._now() } def _now(self): return datetime.now().strftime('%Y-%m-%d %H:%M:%S') if __name__ == '__main__': queue = CallQueue() queue.connect() # In production, callers and agents would be added via # dialplan outbound ESL or API calls print("Queue system running. Use ESL commands to add callers/agents.")
#!/usr/bin/env python3
"""
call_queue.py
Simple call queue: callers hear hold music, agents dial in to take the next call.
Uses ESL inbound mode.
""" import greenswitch
from collections import deque
from datetime import datetime
import threading class CallQueue: def __init__(self, host='127.0.0.1', port=8021, password='YOUR_SECURE_ESL_PASSWORD'): self.conn = greenswitch.InboundESL(host=host, port=port, password=password) self.waiting_callers = deque() # Queue of caller UUIDs self.available_agents = [] # List of agent UUIDs self.lock = threading.Lock() def connect(self): self.conn.connect() self.conn.send('event plain CHANNEL_ANSWER CHANNEL_HANGUP_COMPLETE') print(f"[{self._now()}] Queue system connected") def add_caller(self, uuid): """Add a caller to the queue and play hold music.""" with self.lock: self.waiting_callers.append(uuid) # Play music on hold to the caller self.conn.send( f'api uuid_broadcast {uuid} ' f'local_stream://moh both' ) print(f"[{self._now()}] Caller {uuid} added to queue. " f"Queue depth: {len(self.waiting_callers)}") self._try_connect() def add_agent(self, uuid): """Register an agent as available.""" with self.lock: self.available_agents.append(uuid) print(f"[{self._now()}] Agent {uuid} available. " f"Agents: {len(self.available_agents)}") self._try_connect() def _try_connect(self): """Try to bridge a waiting caller with an available agent.""" if self.waiting_callers and self.available_agents: caller_uuid = self.waiting_callers.popleft() agent_uuid = self.available_agents.pop(0) # Bridge the two calls self.conn.send( f'api uuid_bridge {caller_uuid} {agent_uuid}' ) print( f"[{self._now()}] CONNECTED: caller={caller_uuid} " f"agent={agent_uuid}" ) def get_stats(self): """Return current queue statistics.""" with self.lock: return { 'waiting_callers': len(self.waiting_callers), 'available_agents': len(self.available_agents), 'timestamp': self._now() } def _now(self): return datetime.now().strftime('%Y-%m-%d %H:%M:%S') if __name__ == '__main__': queue = CallQueue() queue.connect() # In production, callers and agents would be added via # dialplan outbound ESL or API calls print("Queue system running. Use ESL commands to add callers/agents.")
npm install modesl
npm install modesl
npm install modesl
// freeswitch_monitor.js
// FreeSWITCH event monitor using Node.js + modesl const esl = require('modesl'); const connection = new esl.Connection('127.0.0.1', 8021, 'YOUR_SECURE_ESL_PASSWORD', () => { console.log(`[${timestamp()}] Connected to FreeSWITCH via ESL`); // Get initial status connection.api('status', (result) => { console.log(`\n--- FreeSWITCH Status ---\n${result.getBody()}\n`); }); // Subscribe to events connection.subscribe([ 'CHANNEL_CREATE', 'CHANNEL_ANSWER', 'CHANNEL_HANGUP_COMPLETE' ], () => { console.log(`[${timestamp()}] Subscribed to call events`); });
}); // Track active calls
const activeCalls = new Map(); connection.on('esl::event::CHANNEL_CREATE::*', (event) => { const uuid = event.getHeader('Unique-ID'); const caller = event.getHeader('Caller-Caller-ID-Number') || 'unknown'; const dest = event.getHeader('Caller-Destination-Number') || 'unknown'; const direction = event.getHeader('Call-Direction') || 'unknown'; activeCalls.set(uuid, { caller, dest, direction, created: new Date() }); console.log(`[${timestamp()}] NEW: ${caller} -> ${dest} (${direction}) [${activeCalls.size} active]`);
}); connection.on('esl::event::CHANNEL_ANSWER::*', (event) => { const uuid = event.getHeader('Unique-ID'); const call = activeCalls.get(uuid); if (call) { console.log(`[${timestamp()}] ANSWER: ${call.caller} -> ${call.dest}`); }
}); connection.on('esl::event::CHANNEL_HANGUP_COMPLETE::*', (event) => { const uuid = event.getHeader('Unique-ID'); const cause = event.getHeader('Hangup-Cause') || 'unknown'; const duration = event.getHeader('variable_billsec') || '0'; const call = activeCalls.get(uuid); if (call) { activeCalls.delete(uuid); console.log( `[${timestamp()}] HANGUP: ${call.caller} -> ${call.dest} ` + `Duration=${duration}s Cause=${cause} [${activeCalls.size} active]` ); }
}); connection.on('error', (error) => { console.error(`[${timestamp()}] ESL Error:`, error);
}); function timestamp() { return new Date().toISOString().replace('T', ' ').substring(0, 19);
} // Originate a call example (uncomment to use):
// connection.api('originate user/1001 &echo()', (result) => {
// console.log('Originate result:', result.getBody());
// });
// freeswitch_monitor.js
// FreeSWITCH event monitor using Node.js + modesl const esl = require('modesl'); const connection = new esl.Connection('127.0.0.1', 8021, 'YOUR_SECURE_ESL_PASSWORD', () => { console.log(`[${timestamp()}] Connected to FreeSWITCH via ESL`); // Get initial status connection.api('status', (result) => { console.log(`\n--- FreeSWITCH Status ---\n${result.getBody()}\n`); }); // Subscribe to events connection.subscribe([ 'CHANNEL_CREATE', 'CHANNEL_ANSWER', 'CHANNEL_HANGUP_COMPLETE' ], () => { console.log(`[${timestamp()}] Subscribed to call events`); });
}); // Track active calls
const activeCalls = new Map(); connection.on('esl::event::CHANNEL_CREATE::*', (event) => { const uuid = event.getHeader('Unique-ID'); const caller = event.getHeader('Caller-Caller-ID-Number') || 'unknown'; const dest = event.getHeader('Caller-Destination-Number') || 'unknown'; const direction = event.getHeader('Call-Direction') || 'unknown'; activeCalls.set(uuid, { caller, dest, direction, created: new Date() }); console.log(`[${timestamp()}] NEW: ${caller} -> ${dest} (${direction}) [${activeCalls.size} active]`);
}); connection.on('esl::event::CHANNEL_ANSWER::*', (event) => { const uuid = event.getHeader('Unique-ID'); const call = activeCalls.get(uuid); if (call) { console.log(`[${timestamp()}] ANSWER: ${call.caller} -> ${call.dest}`); }
}); connection.on('esl::event::CHANNEL_HANGUP_COMPLETE::*', (event) => { const uuid = event.getHeader('Unique-ID'); const cause = event.getHeader('Hangup-Cause') || 'unknown'; const duration = event.getHeader('variable_billsec') || '0'; const call = activeCalls.get(uuid); if (call) { activeCalls.delete(uuid); console.log( `[${timestamp()}] HANGUP: ${call.caller} -> ${call.dest} ` + `Duration=${duration}s Cause=${cause} [${activeCalls.size} active]` ); }
}); connection.on('error', (error) => { console.error(`[${timestamp()}] ESL Error:`, error);
}); function timestamp() { return new Date().toISOString().replace('T', ' ').substring(0, 19);
} // Originate a call example (uncomment to use):
// connection.api('originate user/1001 &echo()', (result) => {
// console.log('Originate result:', result.getBody());
// });
// freeswitch_monitor.js
// FreeSWITCH event monitor using Node.js + modesl const esl = require('modesl'); const connection = new esl.Connection('127.0.0.1', 8021, 'YOUR_SECURE_ESL_PASSWORD', () => { console.log(`[${timestamp()}] Connected to FreeSWITCH via ESL`); // Get initial status connection.api('status', (result) => { console.log(`\n--- FreeSWITCH Status ---\n${result.getBody()}\n`); }); // Subscribe to events connection.subscribe([ 'CHANNEL_CREATE', 'CHANNEL_ANSWER', 'CHANNEL_HANGUP_COMPLETE' ], () => { console.log(`[${timestamp()}] Subscribed to call events`); });
}); // Track active calls
const activeCalls = new Map(); connection.on('esl::event::CHANNEL_CREATE::*', (event) => { const uuid = event.getHeader('Unique-ID'); const caller = event.getHeader('Caller-Caller-ID-Number') || 'unknown'; const dest = event.getHeader('Caller-Destination-Number') || 'unknown'; const direction = event.getHeader('Call-Direction') || 'unknown'; activeCalls.set(uuid, { caller, dest, direction, created: new Date() }); console.log(`[${timestamp()}] NEW: ${caller} -> ${dest} (${direction}) [${activeCalls.size} active]`);
}); connection.on('esl::event::CHANNEL_ANSWER::*', (event) => { const uuid = event.getHeader('Unique-ID'); const call = activeCalls.get(uuid); if (call) { console.log(`[${timestamp()}] ANSWER: ${call.caller} -> ${call.dest}`); }
}); connection.on('esl::event::CHANNEL_HANGUP_COMPLETE::*', (event) => { const uuid = event.getHeader('Unique-ID'); const cause = event.getHeader('Hangup-Cause') || 'unknown'; const duration = event.getHeader('variable_billsec') || '0'; const call = activeCalls.get(uuid); if (call) { activeCalls.delete(uuid); console.log( `[${timestamp()}] HANGUP: ${call.caller} -> ${call.dest} ` + `Duration=${duration}s Cause=${cause} [${activeCalls.size} active]` ); }
}); connection.on('error', (error) => { console.error(`[${timestamp()}] ESL Error:`, error);
}); function timestamp() { return new Date().toISOString().replace('T', ' ').substring(0, 19);
} // Originate a call example (uncomment to use):
// connection.api('originate user/1001 &echo()', (result) => {
// console.log('Originate result:', result.getBody());
// });
node freeswitch_monitor.js
node freeswitch_monitor.js
node freeswitch_monitor.js
<extension name="esl_controlled_ivr"> <condition field="destination_number" expression="^(7000)$"> <action application="socket" data="127.0.0.1:9090 async full"/> </condition>
</extension>
<extension name="esl_controlled_ivr"> <condition field="destination_number" expression="^(7000)$"> <action application="socket" data="127.0.0.1:9090 async full"/> </condition>
</extension>
<extension name="esl_controlled_ivr"> <condition field="destination_number" expression="^(7000)$"> <action application="socket" data="127.0.0.1:9090 async full"/> </condition>
</extension>
#!/usr/bin/env python3
"""
outbound_ivr.py
Simple outbound ESL IVR server.
FreeSWITCH connects to this app for call handling.
""" import socket
import threading def handle_call(client_socket, addr): """Handle one incoming ESL connection (one call).""" print(f"New call connection from {addr}") # Read the initial connect message data = client_socket.recv(65536).decode() # Send connect command client_socket.sendall(b'connect\n\n') data = client_socket.recv(65536).decode() # Parse caller info from headers headers = {} for line in data.split('\n'): if ':' in line: key, value = line.split(':', 1) headers[key.strip()] = value.strip() caller = headers.get('Caller-Caller-ID-Number', 'unknown') dest = headers.get('Caller-Destination-Number', 'unknown') uuid = headers.get('Unique-ID', 'unknown') print(f"Call from {caller} to {dest} (UUID: {uuid})") # Answer the call send_command(client_socket, 'answer') # Play a greeting send_command(client_socket, 'playback /var/lib/freeswitch/sounds/ivr/ivr-welcome.wav') # Collect digits send_command( client_socket, 'play_and_get_digits 1 1 3 10000 # ' '/var/lib/freeswitch/sounds/ivr/ivr-please_make_selection.wav ' '/var/lib/freeswitch/sounds/ivr/ivr-that_was_an_invalid_entry.wav ' 'selection \\d 10000' ) # Read the response to get the collected digit # (In production, parse CHANNEL_EXECUTE_COMPLETE events) # Transfer based on selection send_command(client_socket, 'transfer 1001 XML default') client_socket.close() def send_command(sock, command): """Send an ESL command.""" msg = f'sendmsg\ncall-command: execute\nexecute-app-name: {command}\n\n' sock.sendall(msg.encode()) # Read response try: return sock.recv(65536).decode() except Exception: return '' def main(): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(('127.0.0.1', 9090)) server.listen(50) print("Outbound ESL server listening on 127.0.0.1:9090") while True: client, addr = server.accept() thread = threading.Thread(target=handle_call, args=(client, addr)) thread.daemon = True thread.start() if __name__ == '__main__': main()
#!/usr/bin/env python3
"""
outbound_ivr.py
Simple outbound ESL IVR server.
FreeSWITCH connects to this app for call handling.
""" import socket
import threading def handle_call(client_socket, addr): """Handle one incoming ESL connection (one call).""" print(f"New call connection from {addr}") # Read the initial connect message data = client_socket.recv(65536).decode() # Send connect command client_socket.sendall(b'connect\n\n') data = client_socket.recv(65536).decode() # Parse caller info from headers headers = {} for line in data.split('\n'): if ':' in line: key, value = line.split(':', 1) headers[key.strip()] = value.strip() caller = headers.get('Caller-Caller-ID-Number', 'unknown') dest = headers.get('Caller-Destination-Number', 'unknown') uuid = headers.get('Unique-ID', 'unknown') print(f"Call from {caller} to {dest} (UUID: {uuid})") # Answer the call send_command(client_socket, 'answer') # Play a greeting send_command(client_socket, 'playback /var/lib/freeswitch/sounds/ivr/ivr-welcome.wav') # Collect digits send_command( client_socket, 'play_and_get_digits 1 1 3 10000 # ' '/var/lib/freeswitch/sounds/ivr/ivr-please_make_selection.wav ' '/var/lib/freeswitch/sounds/ivr/ivr-that_was_an_invalid_entry.wav ' 'selection \\d 10000' ) # Read the response to get the collected digit # (In production, parse CHANNEL_EXECUTE_COMPLETE events) # Transfer based on selection send_command(client_socket, 'transfer 1001 XML default') client_socket.close() def send_command(sock, command): """Send an ESL command.""" msg = f'sendmsg\ncall-command: execute\nexecute-app-name: {command}\n\n' sock.sendall(msg.encode()) # Read response try: return sock.recv(65536).decode() except Exception: return '' def main(): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(('127.0.0.1', 9090)) server.listen(50) print("Outbound ESL server listening on 127.0.0.1:9090") while True: client, addr = server.accept() thread = threading.Thread(target=handle_call, args=(client, addr)) thread.daemon = True thread.start() if __name__ == '__main__': main()
#!/usr/bin/env python3
"""
outbound_ivr.py
Simple outbound ESL IVR server.
FreeSWITCH connects to this app for call handling.
""" import socket
import threading def handle_call(client_socket, addr): """Handle one incoming ESL connection (one call).""" print(f"New call connection from {addr}") # Read the initial connect message data = client_socket.recv(65536).decode() # Send connect command client_socket.sendall(b'connect\n\n') data = client_socket.recv(65536).decode() # Parse caller info from headers headers = {} for line in data.split('\n'): if ':' in line: key, value = line.split(':', 1) headers[key.strip()] = value.strip() caller = headers.get('Caller-Caller-ID-Number', 'unknown') dest = headers.get('Caller-Destination-Number', 'unknown') uuid = headers.get('Unique-ID', 'unknown') print(f"Call from {caller} to {dest} (UUID: {uuid})") # Answer the call send_command(client_socket, 'answer') # Play a greeting send_command(client_socket, 'playback /var/lib/freeswitch/sounds/ivr/ivr-welcome.wav') # Collect digits send_command( client_socket, 'play_and_get_digits 1 1 3 10000 # ' '/var/lib/freeswitch/sounds/ivr/ivr-please_make_selection.wav ' '/var/lib/freeswitch/sounds/ivr/ivr-that_was_an_invalid_entry.wav ' 'selection \\d 10000' ) # Read the response to get the collected digit # (In production, parse CHANNEL_EXECUTE_COMPLETE events) # Transfer based on selection send_command(client_socket, 'transfer 1001 XML default') client_socket.close() def send_command(sock, command): """Send an ESL command.""" msg = f'sendmsg\ncall-command: execute\nexecute-app-name: {command}\n\n' sock.sendall(msg.encode()) # Read response try: return sock.recv(65536).decode() except Exception: return '' def main(): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(('127.0.0.1', 9090)) server.listen(50) print("Outbound ESL server listening on 127.0.0.1:9090") while True: client, addr = server.accept() thread = threading.Thread(target=handle_call, args=(client, addr)) thread.daemon = True thread.start() if __name__ == '__main__': main()
<configuration name="cdr_csv.conf" description="CDR CSV Format"> <settings> <!-- Master CSV file for all calls --> <param name="default-template" value="default"/> <!-- Rotate log file when it reaches this size (bytes) --> <param name="rotate-on-hup" value="true"/> <!-- Legs: a-leg only, b-leg only, or both --> <param name="legs" value="a"/> </settings> <templates> <!-- Default template — one line per call --> <template name="default">"${caller_id_name}","${caller_id_number}","${destination_number}","${context}","${start_stamp}","${answer_stamp}","${end_stamp}","${duration}","${billsec}","${hangup_cause}","${uuid}","${accountcode}","${read_codec}","${write_codec}","${sip_hangup_disposition}","${sip_from_uri}","${sip_to_uri}"</template> <!-- Detailed template with more fields --> <template name="detailed">"${caller_id_name}","${caller_id_number}","${destination_number}","${context}","${start_stamp}","${answer_stamp}","${end_stamp}","${progress_stamp}","${progress_media_stamp}","${duration}","${billsec}","${hangup_cause}","${uuid}","${bleg_uuid}","${accountcode}","${read_codec}","${write_codec}","${sip_hangup_disposition}","${endpoint_disposition}","${sip_from_uri}","${sip_to_uri}","${sip_call_id}","${network_addr}","${bridge_channel}","${last_app}","${last_arg}"</template> </templates>
</configuration>
<configuration name="cdr_csv.conf" description="CDR CSV Format"> <settings> <!-- Master CSV file for all calls --> <param name="default-template" value="default"/> <!-- Rotate log file when it reaches this size (bytes) --> <param name="rotate-on-hup" value="true"/> <!-- Legs: a-leg only, b-leg only, or both --> <param name="legs" value="a"/> </settings> <templates> <!-- Default template — one line per call --> <template name="default">"${caller_id_name}","${caller_id_number}","${destination_number}","${context}","${start_stamp}","${answer_stamp}","${end_stamp}","${duration}","${billsec}","${hangup_cause}","${uuid}","${accountcode}","${read_codec}","${write_codec}","${sip_hangup_disposition}","${sip_from_uri}","${sip_to_uri}"</template> <!-- Detailed template with more fields --> <template name="detailed">"${caller_id_name}","${caller_id_number}","${destination_number}","${context}","${start_stamp}","${answer_stamp}","${end_stamp}","${progress_stamp}","${progress_media_stamp}","${duration}","${billsec}","${hangup_cause}","${uuid}","${bleg_uuid}","${accountcode}","${read_codec}","${write_codec}","${sip_hangup_disposition}","${endpoint_disposition}","${sip_from_uri}","${sip_to_uri}","${sip_call_id}","${network_addr}","${bridge_channel}","${last_app}","${last_arg}"</template> </templates>
</configuration>
<configuration name="cdr_csv.conf" description="CDR CSV Format"> <settings> <!-- Master CSV file for all calls --> <param name="default-template" value="default"/> <!-- Rotate log file when it reaches this size (bytes) --> <param name="rotate-on-hup" value="true"/> <!-- Legs: a-leg only, b-leg only, or both --> <param name="legs" value="a"/> </settings> <templates> <!-- Default template — one line per call --> <template name="default">"${caller_id_name}","${caller_id_number}","${destination_number}","${context}","${start_stamp}","${answer_stamp}","${end_stamp}","${duration}","${billsec}","${hangup_cause}","${uuid}","${accountcode}","${read_codec}","${write_codec}","${sip_hangup_disposition}","${sip_from_uri}","${sip_to_uri}"</template> <!-- Detailed template with more fields --> <template name="detailed">"${caller_id_name}","${caller_id_number}","${destination_number}","${context}","${start_stamp}","${answer_stamp}","${end_stamp}","${progress_stamp}","${progress_media_stamp}","${duration}","${billsec}","${hangup_cause}","${uuid}","${bleg_uuid}","${accountcode}","${read_codec}","${write_codec}","${sip_hangup_disposition}","${endpoint_disposition}","${sip_from_uri}","${sip_to_uri}","${sip_call_id}","${network_addr}","${bridge_channel}","${last_app}","${last_arg}"</template> </templates>
</configuration>
<configuration name="cdr_sqlite.conf" description="CDR SQLite"> <settings> <param name="db-name" value="cdr"/> <param name="db-table" value="cdr"/> <param name="legs" value="a"/> <param name="default-template" value="default"/> </settings> <templates> <template name="default"> INSERT INTO cdr ( caller_id_name, caller_id_number, destination_number, context, start_stamp, answer_stamp, end_stamp, duration, billsec, hangup_cause, uuid, accountcode, read_codec, write_codec, sip_hangup_disposition, network_addr ) VALUES ( '${caller_id_name}', '${caller_id_number}', '${destination_number}', '${context}', '${start_stamp}', '${answer_stamp}', '${end_stamp}', '${duration}', '${billsec}', '${hangup_cause}', '${uuid}', '${accountcode}', '${read_codec}', '${write_codec}', '${sip_hangup_disposition}', '${network_addr}' ); </template> </templates>
</configuration>
<configuration name="cdr_sqlite.conf" description="CDR SQLite"> <settings> <param name="db-name" value="cdr"/> <param name="db-table" value="cdr"/> <param name="legs" value="a"/> <param name="default-template" value="default"/> </settings> <templates> <template name="default"> INSERT INTO cdr ( caller_id_name, caller_id_number, destination_number, context, start_stamp, answer_stamp, end_stamp, duration, billsec, hangup_cause, uuid, accountcode, read_codec, write_codec, sip_hangup_disposition, network_addr ) VALUES ( '${caller_id_name}', '${caller_id_number}', '${destination_number}', '${context}', '${start_stamp}', '${answer_stamp}', '${end_stamp}', '${duration}', '${billsec}', '${hangup_cause}', '${uuid}', '${accountcode}', '${read_codec}', '${write_codec}', '${sip_hangup_disposition}', '${network_addr}' ); </template> </templates>
</configuration>
<configuration name="cdr_sqlite.conf" description="CDR SQLite"> <settings> <param name="db-name" value="cdr"/> <param name="db-table" value="cdr"/> <param name="legs" value="a"/> <param name="default-template" value="default"/> </settings> <templates> <template name="default"> INSERT INTO cdr ( caller_id_name, caller_id_number, destination_number, context, start_stamp, answer_stamp, end_stamp, duration, billsec, hangup_cause, uuid, accountcode, read_codec, write_codec, sip_hangup_disposition, network_addr ) VALUES ( '${caller_id_name}', '${caller_id_number}', '${destination_number}', '${context}', '${start_stamp}', '${answer_stamp}', '${end_stamp}', '${duration}', '${billsec}', '${hangup_cause}', '${uuid}', '${accountcode}', '${read_codec}', '${write_codec}', '${sip_hangup_disposition}', '${network_addr}' ); </template> </templates>
</configuration>
sqlite3 /var/lib/freeswitch/db/cdr.db -- Show recent calls
SELECT caller_id_number, destination_number, duration, hangup_cause, start_stamp
FROM cdr
ORDER BY start_stamp DESC
LIMIT 20; -- Call volume by hour
SELECT strftime('%H', start_stamp) AS hour, COUNT(*) AS calls
FROM cdr
WHERE date(start_stamp) = date('now')
GROUP BY hour
ORDER BY hour; -- Average call duration by destination
SELECT destination_number, COUNT(*) AS calls, AVG(billsec) AS avg_duration, SUM(billsec) AS total_seconds
FROM cdr
WHERE billsec > 0
GROUP BY destination_number
ORDER BY calls DESC;
sqlite3 /var/lib/freeswitch/db/cdr.db -- Show recent calls
SELECT caller_id_number, destination_number, duration, hangup_cause, start_stamp
FROM cdr
ORDER BY start_stamp DESC
LIMIT 20; -- Call volume by hour
SELECT strftime('%H', start_stamp) AS hour, COUNT(*) AS calls
FROM cdr
WHERE date(start_stamp) = date('now')
GROUP BY hour
ORDER BY hour; -- Average call duration by destination
SELECT destination_number, COUNT(*) AS calls, AVG(billsec) AS avg_duration, SUM(billsec) AS total_seconds
FROM cdr
WHERE billsec > 0
GROUP BY destination_number
ORDER BY calls DESC;
sqlite3 /var/lib/freeswitch/db/cdr.db -- Show recent calls
SELECT caller_id_number, destination_number, duration, hangup_cause, start_stamp
FROM cdr
ORDER BY start_stamp DESC
LIMIT 20; -- Call volume by hour
SELECT strftime('%H', start_stamp) AS hour, COUNT(*) AS calls
FROM cdr
WHERE date(start_stamp) = date('now')
GROUP BY hour
ORDER BY hour; -- Average call duration by destination
SELECT destination_number, COUNT(*) AS calls, AVG(billsec) AS avg_duration, SUM(billsec) AS total_seconds
FROM cdr
WHERE billsec > 0
GROUP BY destination_number
ORDER BY calls DESC;
<configuration name="cdr_pg.conf" description="CDR PostgreSQL"> <settings> <param name="host" value="127.0.0.1"/> <param name="port" value="5432"/> <param name="dbname" value="freeswitch_cdr"/> <param name="user" value="freeswitch"/> <param name="password" value="YOUR_DB_PASSWORD"/> <param name="legs" value="a"/> <param name="default-template" value="default"/> <!-- Log errors to file if DB is unavailable --> <param name="spool-format" value="csv"/> <param name="log-dir" value="/var/log/freeswitch/cdr-pg-errors"/> </settings> <templates> <template name="default"> INSERT INTO cdr ( caller_id_name, caller_id_number, destination_number, context, start_stamp, answer_stamp, end_stamp, duration, billsec, hangup_cause, uuid, accountcode, read_codec, write_codec, sip_hangup_disposition, network_addr ) VALUES ( '${caller_id_name}', '${caller_id_number}', '${destination_number}', '${context}', to_timestamp(${start_epoch}), to_timestamp(${answer_epoch}), to_timestamp(${end_epoch}), ${duration}, ${billsec}, '${hangup_cause}', '${uuid}', '${accountcode}', '${read_codec}', '${write_codec}', '${sip_hangup_disposition}', '${network_addr}' ); </template> </templates>
</configuration>
<configuration name="cdr_pg.conf" description="CDR PostgreSQL"> <settings> <param name="host" value="127.0.0.1"/> <param name="port" value="5432"/> <param name="dbname" value="freeswitch_cdr"/> <param name="user" value="freeswitch"/> <param name="password" value="YOUR_DB_PASSWORD"/> <param name="legs" value="a"/> <param name="default-template" value="default"/> <!-- Log errors to file if DB is unavailable --> <param name="spool-format" value="csv"/> <param name="log-dir" value="/var/log/freeswitch/cdr-pg-errors"/> </settings> <templates> <template name="default"> INSERT INTO cdr ( caller_id_name, caller_id_number, destination_number, context, start_stamp, answer_stamp, end_stamp, duration, billsec, hangup_cause, uuid, accountcode, read_codec, write_codec, sip_hangup_disposition, network_addr ) VALUES ( '${caller_id_name}', '${caller_id_number}', '${destination_number}', '${context}', to_timestamp(${start_epoch}), to_timestamp(${answer_epoch}), to_timestamp(${end_epoch}), ${duration}, ${billsec}, '${hangup_cause}', '${uuid}', '${accountcode}', '${read_codec}', '${write_codec}', '${sip_hangup_disposition}', '${network_addr}' ); </template> </templates>
</configuration>
<configuration name="cdr_pg.conf" description="CDR PostgreSQL"> <settings> <param name="host" value="127.0.0.1"/> <param name="port" value="5432"/> <param name="dbname" value="freeswitch_cdr"/> <param name="user" value="freeswitch"/> <param name="password" value="YOUR_DB_PASSWORD"/> <param name="legs" value="a"/> <param name="default-template" value="default"/> <!-- Log errors to file if DB is unavailable --> <param name="spool-format" value="csv"/> <param name="log-dir" value="/var/log/freeswitch/cdr-pg-errors"/> </settings> <templates> <template name="default"> INSERT INTO cdr ( caller_id_name, caller_id_number, destination_number, context, start_stamp, answer_stamp, end_stamp, duration, billsec, hangup_cause, uuid, accountcode, read_codec, write_codec, sip_hangup_disposition, network_addr ) VALUES ( '${caller_id_name}', '${caller_id_number}', '${destination_number}', '${context}', to_timestamp(${start_epoch}), to_timestamp(${answer_epoch}), to_timestamp(${end_epoch}), ${duration}, ${billsec}, '${hangup_cause}', '${uuid}', '${accountcode}', '${read_codec}', '${write_codec}', '${sip_hangup_disposition}', '${network_addr}' ); </template> </templates>
</configuration>
-- Run as PostgreSQL superuser
CREATE DATABASE freeswitch_cdr;
CREATE USER freeswitch WITH PASSWORD 'YOUR_DB_PASSWORD';
GRANT ALL PRIVILEGES ON DATABASE freeswitch_cdr TO freeswitch; \c freeswitch_cdr CREATE TABLE cdr ( id SERIAL PRIMARY KEY, caller_id_name VARCHAR(128), caller_id_number VARCHAR(64), destination_number VARCHAR(64), context VARCHAR(64), start_stamp TIMESTAMP, answer_stamp TIMESTAMP, end_stamp TIMESTAMP, duration INTEGER, billsec INTEGER, hangup_cause VARCHAR(64), uuid VARCHAR(64) UNIQUE, accountcode VARCHAR(32), read_codec VARCHAR(32), write_codec VARCHAR(32), sip_hangup_disposition VARCHAR(32), network_addr VARCHAR(64), created_at TIMESTAMP DEFAULT NOW()
); -- Index for common queries
CREATE INDEX idx_cdr_start_stamp ON cdr (start_stamp);
CREATE INDEX idx_cdr_caller ON cdr (caller_id_number);
CREATE INDEX idx_cdr_destination ON cdr (destination_number);
CREATE INDEX idx_cdr_accountcode ON cdr (accountcode); GRANT ALL ON cdr TO freeswitch;
GRANT USAGE, SELECT ON SEQUENCE cdr_id_seq TO freeswitch;
-- Run as PostgreSQL superuser
CREATE DATABASE freeswitch_cdr;
CREATE USER freeswitch WITH PASSWORD 'YOUR_DB_PASSWORD';
GRANT ALL PRIVILEGES ON DATABASE freeswitch_cdr TO freeswitch; \c freeswitch_cdr CREATE TABLE cdr ( id SERIAL PRIMARY KEY, caller_id_name VARCHAR(128), caller_id_number VARCHAR(64), destination_number VARCHAR(64), context VARCHAR(64), start_stamp TIMESTAMP, answer_stamp TIMESTAMP, end_stamp TIMESTAMP, duration INTEGER, billsec INTEGER, hangup_cause VARCHAR(64), uuid VARCHAR(64) UNIQUE, accountcode VARCHAR(32), read_codec VARCHAR(32), write_codec VARCHAR(32), sip_hangup_disposition VARCHAR(32), network_addr VARCHAR(64), created_at TIMESTAMP DEFAULT NOW()
); -- Index for common queries
CREATE INDEX idx_cdr_start_stamp ON cdr (start_stamp);
CREATE INDEX idx_cdr_caller ON cdr (caller_id_number);
CREATE INDEX idx_cdr_destination ON cdr (destination_number);
CREATE INDEX idx_cdr_accountcode ON cdr (accountcode); GRANT ALL ON cdr TO freeswitch;
GRANT USAGE, SELECT ON SEQUENCE cdr_id_seq TO freeswitch;
-- Run as PostgreSQL superuser
CREATE DATABASE freeswitch_cdr;
CREATE USER freeswitch WITH PASSWORD 'YOUR_DB_PASSWORD';
GRANT ALL PRIVILEGES ON DATABASE freeswitch_cdr TO freeswitch; \c freeswitch_cdr CREATE TABLE cdr ( id SERIAL PRIMARY KEY, caller_id_name VARCHAR(128), caller_id_number VARCHAR(64), destination_number VARCHAR(64), context VARCHAR(64), start_stamp TIMESTAMP, answer_stamp TIMESTAMP, end_stamp TIMESTAMP, duration INTEGER, billsec INTEGER, hangup_cause VARCHAR(64), uuid VARCHAR(64) UNIQUE, accountcode VARCHAR(32), read_codec VARCHAR(32), write_codec VARCHAR(32), sip_hangup_disposition VARCHAR(32), network_addr VARCHAR(64), created_at TIMESTAMP DEFAULT NOW()
); -- Index for common queries
CREATE INDEX idx_cdr_start_stamp ON cdr (start_stamp);
CREATE INDEX idx_cdr_caller ON cdr (caller_id_number);
CREATE INDEX idx_cdr_destination ON cdr (destination_number);
CREATE INDEX idx_cdr_accountcode ON cdr (accountcode); GRANT ALL ON cdr TO freeswitch;
GRANT USAGE, SELECT ON SEQUENCE cdr_id_seq TO freeswitch;
<configuration name="logfile.conf" description="File Logging"> <settings> <param name="rotate-on-hup" value="true"/> </settings> <profiles> <profile name="default"> <settings> <param name="logfile" value="/var/log/freeswitch/freeswitch.log"/> <!-- Rotate every 10MB --> <param name="rollover" value="10485760"/> <!-- Log level: DEBUG, INFO, NOTICE, WARNING, ERR, CRIT, ALERT --> <param name="log-event" value="false"/> </settings> <mappings> <!-- What to log — set level per module --> <map name="all" value="info,warning,err,crit,alert"/> <!-- Enable debug for specific modules when troubleshooting --> <!-- <map name="mod_sofia" value="debug,info,warning,err"/> --> <!-- <map name="mod_dptools" value="debug,info,warning,err"/> --> </mappings> </profile> </profiles>
</configuration>
<configuration name="logfile.conf" description="File Logging"> <settings> <param name="rotate-on-hup" value="true"/> </settings> <profiles> <profile name="default"> <settings> <param name="logfile" value="/var/log/freeswitch/freeswitch.log"/> <!-- Rotate every 10MB --> <param name="rollover" value="10485760"/> <!-- Log level: DEBUG, INFO, NOTICE, WARNING, ERR, CRIT, ALERT --> <param name="log-event" value="false"/> </settings> <mappings> <!-- What to log — set level per module --> <map name="all" value="info,warning,err,crit,alert"/> <!-- Enable debug for specific modules when troubleshooting --> <!-- <map name="mod_sofia" value="debug,info,warning,err"/> --> <!-- <map name="mod_dptools" value="debug,info,warning,err"/> --> </mappings> </profile> </profiles>
</configuration>
<configuration name="logfile.conf" description="File Logging"> <settings> <param name="rotate-on-hup" value="true"/> </settings> <profiles> <profile name="default"> <settings> <param name="logfile" value="/var/log/freeswitch/freeswitch.log"/> <!-- Rotate every 10MB --> <param name="rollover" value="10485760"/> <!-- Log level: DEBUG, INFO, NOTICE, WARNING, ERR, CRIT, ALERT --> <param name="log-event" value="false"/> </settings> <mappings> <!-- What to log — set level per module --> <map name="all" value="info,warning,err,crit,alert"/> <!-- Enable debug for specific modules when troubleshooting --> <!-- <map name="mod_sofia" value="debug,info,warning,err"/> --> <!-- <map name="mod_dptools" value="debug,info,warning,err"/> --> </mappings> </profile> </profiles>
</configuration>
# Enable SIP trace (shows all SIP messages in fs_cli)
fs_cli -x "sofia profile internal siptrace on" # Disable
fs_cli -x "sofia profile internal siptrace off" # Enable on external profile
fs_cli -x "sofia profile external siptrace on" # Save SIP trace to a PCAP file
fs_cli -x "sofia profile internal capture on"
# File saved to /tmp/ by default
fs_cli -x "sofia profile internal capture off"
# Enable SIP trace (shows all SIP messages in fs_cli)
fs_cli -x "sofia profile internal siptrace on" # Disable
fs_cli -x "sofia profile internal siptrace off" # Enable on external profile
fs_cli -x "sofia profile external siptrace on" # Save SIP trace to a PCAP file
fs_cli -x "sofia profile internal capture on"
# File saved to /tmp/ by default
fs_cli -x "sofia profile internal capture off"
# Enable SIP trace (shows all SIP messages in fs_cli)
fs_cli -x "sofia profile internal siptrace on" # Disable
fs_cli -x "sofia profile internal siptrace off" # Enable on external profile
fs_cli -x "sofia profile external siptrace on" # Save SIP trace to a PCAP file
fs_cli -x "sofia profile internal capture on"
# File saved to /tmp/ by default
fs_cli -x "sofia profile internal capture off"
cat > /etc/logrotate.d/freeswitch << 'EOF'
/var/log/freeswitch/freeswitch.log { daily rotate 14 compress delaycompress missingok notifempty postrotate /usr/bin/fs_cli -x "fsctl send_sighup" > /dev/null 2>&1 || true endscript
} /var/log/freeswitch/cdr-csv/*.csv { daily rotate 30 compress delaycompress missingok notifempty create 640 freeswitch freeswitch
}
EOF
cat > /etc/logrotate.d/freeswitch << 'EOF'
/var/log/freeswitch/freeswitch.log { daily rotate 14 compress delaycompress missingok notifempty postrotate /usr/bin/fs_cli -x "fsctl send_sighup" > /dev/null 2>&1 || true endscript
} /var/log/freeswitch/cdr-csv/*.csv { daily rotate 30 compress delaycompress missingok notifempty create 640 freeswitch freeswitch
}
EOF
cat > /etc/logrotate.d/freeswitch << 'EOF'
/var/log/freeswitch/freeswitch.log { daily rotate 14 compress delaycompress missingok notifempty postrotate /usr/bin/fs_cli -x "fsctl send_sighup" > /dev/null 2>&1 || true endscript
} /var/log/freeswitch/cdr-csv/*.csv { daily rotate 30 compress delaycompress missingok notifempty create 640 freeswitch freeswitch
}
EOF
# 1. Extension passwords (vars.xml — default_password)
# Change from "1234" to something strong
# Then set individual passwords per extension in directory/*.xml # 2. ESL password (event_socket.conf.xml)
# Change from "ClueCon" # 3. Voicemail PINs (each user's vm-password)
# Change from extension number
# 1. Extension passwords (vars.xml — default_password)
# Change from "1234" to something strong
# Then set individual passwords per extension in directory/*.xml # 2. ESL password (event_socket.conf.xml)
# Change from "ClueCon" # 3. Voicemail PINs (each user's vm-password)
# Change from extension number
# 1. Extension passwords (vars.xml — default_password)
# Change from "1234" to something strong
# Then set individual passwords per extension in directory/*.xml # 2. ESL password (event_socket.conf.xml)
# Change from "ClueCon" # 3. Voicemail PINs (each user's vm-password)
# Change from extension number
<!-- In sip_profiles/internal.xml -->
<param name="apply-register-acl" value="trusted_networks"/>
<param name="apply-inbound-acl" value="trusted_networks"/>
<!-- In sip_profiles/internal.xml -->
<param name="apply-register-acl" value="trusted_networks"/>
<param name="apply-inbound-acl" value="trusted_networks"/>
<!-- In sip_profiles/internal.xml -->
<param name="apply-register-acl" value="trusted_networks"/>
<param name="apply-inbound-acl" value="trusted_networks"/>
<configuration name="acl.conf" description="Network Lists"> <network-lists> <!-- Trusted networks for SIP registration --> <list name="trusted_networks" default="deny"> <!-- Office network --> <node type="allow" cidr="10.0.0.0/8"/> <!-- VPN range --> <node type="allow" cidr="172.16.0.0/12"/> <!-- Specific remote office --> <node type="allow" cidr="203.0.113.50/32"/> <!-- Localhost --> <node type="allow" cidr="127.0.0.0/8"/> </list> <!-- SIP trunk provider IPs --> <list name="trunk_providers" default="deny"> <node type="allow" cidr="198.51.100.0/24"/> <node type="allow" cidr="203.0.113.100/32"/> </list> <!-- ESL access --> <list name="esl_access" default="deny"> <node type="allow" cidr="127.0.0.1/32"/> <node type="allow" cidr="10.0.0.0/8"/> </list> </network-lists>
</configuration>
<configuration name="acl.conf" description="Network Lists"> <network-lists> <!-- Trusted networks for SIP registration --> <list name="trusted_networks" default="deny"> <!-- Office network --> <node type="allow" cidr="10.0.0.0/8"/> <!-- VPN range --> <node type="allow" cidr="172.16.0.0/12"/> <!-- Specific remote office --> <node type="allow" cidr="203.0.113.50/32"/> <!-- Localhost --> <node type="allow" cidr="127.0.0.0/8"/> </list> <!-- SIP trunk provider IPs --> <list name="trunk_providers" default="deny"> <node type="allow" cidr="198.51.100.0/24"/> <node type="allow" cidr="203.0.113.100/32"/> </list> <!-- ESL access --> <list name="esl_access" default="deny"> <node type="allow" cidr="127.0.0.1/32"/> <node type="allow" cidr="10.0.0.0/8"/> </list> </network-lists>
</configuration>
<configuration name="acl.conf" description="Network Lists"> <network-lists> <!-- Trusted networks for SIP registration --> <list name="trusted_networks" default="deny"> <!-- Office network --> <node type="allow" cidr="10.0.0.0/8"/> <!-- VPN range --> <node type="allow" cidr="172.16.0.0/12"/> <!-- Specific remote office --> <node type="allow" cidr="203.0.113.50/32"/> <!-- Localhost --> <node type="allow" cidr="127.0.0.0/8"/> </list> <!-- SIP trunk provider IPs --> <list name="trunk_providers" default="deny"> <node type="allow" cidr="198.51.100.0/24"/> <node type="allow" cidr="203.0.113.100/32"/> </list> <!-- ESL access --> <list name="esl_access" default="deny"> <node type="allow" cidr="127.0.0.1/32"/> <node type="allow" cidr="10.0.0.0/8"/> </list> </network-lists>
</configuration>
apt-get install -y fail2ban
apt-get install -y fail2ban
apt-get install -y fail2ban
# /etc/fail2ban/filter.d/freeswitch.conf
[INCLUDES]
before = common.conf [Definition]
failregex = ^\s*\[WARNING\] sofia_reg\.c:\d+ SIP Registration Failed: IP=<HOST>.*$ ^\s*\[WARNING\] sofia_reg\.c:\d+.*auth challenge.*<HOST>.*$ ^\s*\[WARNING\].*REGISTER.*from.*<HOST>.*forbidden.*$ ^\s*\[WARNING\].*Authentication\s+Failed.*<HOST>.*$ ignoreregex =
# /etc/fail2ban/filter.d/freeswitch.conf
[INCLUDES]
before = common.conf [Definition]
failregex = ^\s*\[WARNING\] sofia_reg\.c:\d+ SIP Registration Failed: IP=<HOST>.*$ ^\s*\[WARNING\] sofia_reg\.c:\d+.*auth challenge.*<HOST>.*$ ^\s*\[WARNING\].*REGISTER.*from.*<HOST>.*forbidden.*$ ^\s*\[WARNING\].*Authentication\s+Failed.*<HOST>.*$ ignoreregex =
# /etc/fail2ban/filter.d/freeswitch.conf
[INCLUDES]
before = common.conf [Definition]
failregex = ^\s*\[WARNING\] sofia_reg\.c:\d+ SIP Registration Failed: IP=<HOST>.*$ ^\s*\[WARNING\] sofia_reg\.c:\d+.*auth challenge.*<HOST>.*$ ^\s*\[WARNING\].*REGISTER.*from.*<HOST>.*forbidden.*$ ^\s*\[WARNING\].*Authentication\s+Failed.*<HOST>.*$ ignoreregex =
# /etc/fail2ban/jail.d/freeswitch.conf
[freeswitch]
enabled = true
filter = freeswitch
logpath = /var/log/freeswitch/freeswitch.log
maxretry = 5
findtime = 300
bantime = 3600
action = iptables-allports[name=freeswitch, protocol=all]
# /etc/fail2ban/jail.d/freeswitch.conf
[freeswitch]
enabled = true
filter = freeswitch
logpath = /var/log/freeswitch/freeswitch.log
maxretry = 5
findtime = 300
bantime = 3600
action = iptables-allports[name=freeswitch, protocol=all]
# /etc/fail2ban/jail.d/freeswitch.conf
[freeswitch]
enabled = true
filter = freeswitch
logpath = /var/log/freeswitch/freeswitch.log
maxretry = 5
findtime = 300
bantime = 3600
action = iptables-allports[name=freeswitch, protocol=all]
systemctl enable fail2ban
systemctl restart fail2ban # Check status
fail2ban-client status freeswitch
systemctl enable fail2ban
systemctl restart fail2ban # Check status
fail2ban-client status freeswitch
systemctl enable fail2ban
systemctl restart fail2ban # Check status
fail2ban-client status freeswitch
# Generate a self-signed certificate (or use Let's Encrypt)
mkdir -p /etc/freeswitch/tls openssl req -x509 -nodes -days 3650 \ -newkey rsa:2048 \ -keyout /etc/freeswitch/tls/agent.pem \ -out /etc/freeswitch/tls/agent.pem \ -subj "/CN=YOUR_DOMAIN" # Combine into the format FreeSWITCH expects
cp /etc/freeswitch/tls/agent.pem /etc/freeswitch/tls/cafile.pem
chown freeswitch:freeswitch /etc/freeswitch/tls/*.pem
chmod 640 /etc/freeswitch/tls/*.pem
# Generate a self-signed certificate (or use Let's Encrypt)
mkdir -p /etc/freeswitch/tls openssl req -x509 -nodes -days 3650 \ -newkey rsa:2048 \ -keyout /etc/freeswitch/tls/agent.pem \ -out /etc/freeswitch/tls/agent.pem \ -subj "/CN=YOUR_DOMAIN" # Combine into the format FreeSWITCH expects
cp /etc/freeswitch/tls/agent.pem /etc/freeswitch/tls/cafile.pem
chown freeswitch:freeswitch /etc/freeswitch/tls/*.pem
chmod 640 /etc/freeswitch/tls/*.pem
# Generate a self-signed certificate (or use Let's Encrypt)
mkdir -p /etc/freeswitch/tls openssl req -x509 -nodes -days 3650 \ -newkey rsa:2048 \ -keyout /etc/freeswitch/tls/agent.pem \ -out /etc/freeswitch/tls/agent.pem \ -subj "/CN=YOUR_DOMAIN" # Combine into the format FreeSWITCH expects
cp /etc/freeswitch/tls/agent.pem /etc/freeswitch/tls/cafile.pem
chown freeswitch:freeswitch /etc/freeswitch/tls/*.pem
chmod 640 /etc/freeswitch/tls/*.pem
<!-- In sip_profiles/internal.xml, add these parameters -->
<param name="tls" value="true"/>
<param name="tls-bind-params" value="transport=tls"/>
<param name="tls-sip-port" value="5061"/>
<param name="tls-cert-dir" value="/etc/freeswitch/tls"/>
<param name="tls-version" value="tlsv1.2"/>
<!-- In sip_profiles/internal.xml, add these parameters -->
<param name="tls" value="true"/>
<param name="tls-bind-params" value="transport=tls"/>
<param name="tls-sip-port" value="5061"/>
<param name="tls-cert-dir" value="/etc/freeswitch/tls"/>
<param name="tls-version" value="tlsv1.2"/>
<!-- In sip_profiles/internal.xml, add these parameters -->
<param name="tls" value="true"/>
<param name="tls-bind-params" value="transport=tls"/>
<param name="tls-sip-port" value="5061"/>
<param name="tls-cert-dir" value="/etc/freeswitch/tls"/>
<param name="tls-version" value="tlsv1.2"/>
<!-- In sip_profiles/internal.xml -->
<param name="inbound-codec-string" value="OPUS,G722,PCMU,PCMA"/>
<param name="outbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <!-- Require SRTP -->
<!-- Options: mandatory, optional, forbidden -->
<param name="rtp_secure_media" value="optional"/>
<!-- In sip_profiles/internal.xml -->
<param name="inbound-codec-string" value="OPUS,G722,PCMU,PCMA"/>
<param name="outbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <!-- Require SRTP -->
<!-- Options: mandatory, optional, forbidden -->
<param name="rtp_secure_media" value="optional"/>
<!-- In sip_profiles/internal.xml -->
<param name="inbound-codec-string" value="OPUS,G722,PCMU,PCMA"/>
<param name="outbound-codec-string" value="OPUS,G722,PCMU,PCMA"/> <!-- Require SRTP -->
<!-- Options: mandatory, optional, forbidden -->
<param name="rtp_secure_media" value="optional"/>
<!-- In directory/default/1001.xml -->
<user id="1001"> <params> <param name="password" value="Str0ng_P@ss!"/> </params> <variables> <!-- Force SRTP for this user --> <variable name="rtp_secure_media" value="mandatory"/> <!-- ... other variables ... --> </variables>
</user>
<!-- In directory/default/1001.xml -->
<user id="1001"> <params> <param name="password" value="Str0ng_P@ss!"/> </params> <variables> <!-- Force SRTP for this user --> <variable name="rtp_secure_media" value="mandatory"/> <!-- ... other variables ... --> </variables>
</user>
<!-- In directory/default/1001.xml -->
<user id="1001"> <params> <param name="password" value="Str0ng_P@ss!"/> </params> <variables> <!-- Force SRTP for this user --> <variable name="rtp_secure_media" value="mandatory"/> <!-- ... other variables ... --> </variables>
</user>
<!-- In sip_profiles/internal.xml --> <!-- Max registrations per second -->
<param name="accept-blind-reg" value="false"/> <!-- Challenge all registrations (prevents spoofing) -->
<param name="auth-all-packets" value="true"/>
<param name="auth-calls" value="true"/> <!-- Limit concurrent calls -->
<!-- In vars.xml -->
<!-- <X-PRE-PROCESS cmd="set" data="max_sessions=500"/> -->
<!-- <X-PRE-PROCESS cmd="set" data="sessions_per_second=50"/> -->
<!-- In sip_profiles/internal.xml --> <!-- Max registrations per second -->
<param name="accept-blind-reg" value="false"/> <!-- Challenge all registrations (prevents spoofing) -->
<param name="auth-all-packets" value="true"/>
<param name="auth-calls" value="true"/> <!-- Limit concurrent calls -->
<!-- In vars.xml -->
<!-- <X-PRE-PROCESS cmd="set" data="max_sessions=500"/> -->
<!-- <X-PRE-PROCESS cmd="set" data="sessions_per_second=50"/> -->
<!-- In sip_profiles/internal.xml --> <!-- Max registrations per second -->
<param name="accept-blind-reg" value="false"/> <!-- Challenge all registrations (prevents spoofing) -->
<param name="auth-all-packets" value="true"/>
<param name="auth-calls" value="true"/> <!-- Limit concurrent calls -->
<!-- In vars.xml -->
<!-- <X-PRE-PROCESS cmd="set" data="max_sessions=500"/> -->
<!-- <X-PRE-PROCESS cmd="set" data="sessions_per_second=50"/> -->
# Set max concurrent sessions
fs_cli -x "fsctl max_sessions 500" # Set max sessions per second
fs_cli -x "fsctl sps 50"
# Set max concurrent sessions
fs_cli -x "fsctl max_sessions 500" # Set max sessions per second
fs_cli -x "fsctl sps 50"
# Set max concurrent sessions
fs_cli -x "fsctl max_sessions 500" # Set max sessions per second
fs_cli -x "fsctl sps 50"
# Minimal firewall rules for production FreeSWITCH # Flush existing rules (careful!)
# iptables -F # Default deny
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT # Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # Loopback
iptables -A INPUT -i lo -j ACCEPT # SSH (your management IP only)
iptables -A INPUT -p tcp -s YOUR_MGMT_IP --dport 22 -j ACCEPT # SIP from trusted networks only
iptables -A INPUT -p udp -s 10.0.0.0/8 --dport 5060 -j ACCEPT
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 5060 -j ACCEPT # SIP TLS
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 5061 -j ACCEPT # SIP from trunk providers (by IP)
iptables -A INPUT -p udp -s PROVIDER_IP_1 --dport 5080 -j ACCEPT
iptables -A INPUT -p udp -s PROVIDER_IP_2 --dport 5080 -j ACCEPT # RTP media (must be open — media comes from many IPs)
iptables -A INPUT -p udp --dport 16384:32768 -j ACCEPT # ESL (localhost only)
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 8021 -j ACCEPT # Save rules
iptables-save > /etc/iptables/rules.v4
# Minimal firewall rules for production FreeSWITCH # Flush existing rules (careful!)
# iptables -F # Default deny
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT # Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # Loopback
iptables -A INPUT -i lo -j ACCEPT # SSH (your management IP only)
iptables -A INPUT -p tcp -s YOUR_MGMT_IP --dport 22 -j ACCEPT # SIP from trusted networks only
iptables -A INPUT -p udp -s 10.0.0.0/8 --dport 5060 -j ACCEPT
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 5060 -j ACCEPT # SIP TLS
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 5061 -j ACCEPT # SIP from trunk providers (by IP)
iptables -A INPUT -p udp -s PROVIDER_IP_1 --dport 5080 -j ACCEPT
iptables -A INPUT -p udp -s PROVIDER_IP_2 --dport 5080 -j ACCEPT # RTP media (must be open — media comes from many IPs)
iptables -A INPUT -p udp --dport 16384:32768 -j ACCEPT # ESL (localhost only)
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 8021 -j ACCEPT # Save rules
iptables-save > /etc/iptables/rules.v4
# Minimal firewall rules for production FreeSWITCH # Flush existing rules (careful!)
# iptables -F # Default deny
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT # Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # Loopback
iptables -A INPUT -i lo -j ACCEPT # SSH (your management IP only)
iptables -A INPUT -p tcp -s YOUR_MGMT_IP --dport 22 -j ACCEPT # SIP from trusted networks only
iptables -A INPUT -p udp -s 10.0.0.0/8 --dport 5060 -j ACCEPT
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 5060 -j ACCEPT # SIP TLS
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 5061 -j ACCEPT # SIP from trunk providers (by IP)
iptables -A INPUT -p udp -s PROVIDER_IP_1 --dport 5080 -j ACCEPT
iptables -A INPUT -p udp -s PROVIDER_IP_2 --dport 5080 -j ACCEPT # RTP media (must be open — media comes from many IPs)
iptables -A INPUT -p udp --dport 16384:32768 -j ACCEPT # ESL (localhost only)
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 8021 -j ACCEPT # Save rules
iptables-save > /etc/iptables/rules.v4
Internet Kamailio (SBC) FreeSWITCH (Media) │ │ │ │ SIP INVITE │ │ ├───────────────────────►│ │ │ │ Rate limit, ACL check │ │ │ Route to FS │ │ ├────────────────────────►│ │ │ │ Answer, IVR, Bridge │ RTP (media) │ │ ├──────────────────────────────────────────────────►│ │ │ │
Internet Kamailio (SBC) FreeSWITCH (Media) │ │ │ │ SIP INVITE │ │ ├───────────────────────►│ │ │ │ Rate limit, ACL check │ │ │ Route to FS │ │ ├────────────────────────►│ │ │ │ Answer, IVR, Bridge │ RTP (media) │ │ ├──────────────────────────────────────────────────►│ │ │ │
Internet Kamailio (SBC) FreeSWITCH (Media) │ │ │ │ SIP INVITE │ │ ├───────────────────────►│ │ │ │ Rate limit, ACL check │ │ │ Route to FS │ │ ├────────────────────────►│ │ │ │ Answer, IVR, Bridge │ RTP (media) │ │ ├──────────────────────────────────────────────────►│ │ │ │
# kamailio.cfg (simplified)
# Route SIP to FreeSWITCH backend request_route { # Security checks if (!mf_process_maxfwd_header("10")) { sl_send_reply("483", "Too Many Hops"); exit; } # Rate limiting if (!pike_check_req()) { sl_send_reply("503", "Service Unavailable"); exit; } # Route INVITEs to FreeSWITCH if (is_method("INVITE")) { # Set destination to FreeSWITCH $du = "sip:FREESWITCH_IP:5060"; route(RELAY); } # Route REGISTERs to FreeSWITCH if (is_method("REGISTER")) { $du = "sip:FREESWITCH_IP:5060"; route(RELAY); }
} route[RELAY] { if (!t_relay()) { sl_reply_error(); }
}
# kamailio.cfg (simplified)
# Route SIP to FreeSWITCH backend request_route { # Security checks if (!mf_process_maxfwd_header("10")) { sl_send_reply("483", "Too Many Hops"); exit; } # Rate limiting if (!pike_check_req()) { sl_send_reply("503", "Service Unavailable"); exit; } # Route INVITEs to FreeSWITCH if (is_method("INVITE")) { # Set destination to FreeSWITCH $du = "sip:FREESWITCH_IP:5060"; route(RELAY); } # Route REGISTERs to FreeSWITCH if (is_method("REGISTER")) { $du = "sip:FREESWITCH_IP:5060"; route(RELAY); }
} route[RELAY] { if (!t_relay()) { sl_reply_error(); }
}
# kamailio.cfg (simplified)
# Route SIP to FreeSWITCH backend request_route { # Security checks if (!mf_process_maxfwd_header("10")) { sl_send_reply("483", "Too Many Hops"); exit; } # Rate limiting if (!pike_check_req()) { sl_send_reply("503", "Service Unavailable"); exit; } # Route INVITEs to FreeSWITCH if (is_method("INVITE")) { # Set destination to FreeSWITCH $du = "sip:FREESWITCH_IP:5060"; route(RELAY); } # Route REGISTERs to FreeSWITCH if (is_method("REGISTER")) { $du = "sip:FREESWITCH_IP:5060"; route(RELAY); }
} route[RELAY] { if (!t_relay()) { sl_reply_error(); }
}
<!-- In acl.conf.xml -->
<list name="kamailio" default="deny"> <node type="allow" cidr="KAMAILIO_IP/32"/>
</list> <!-- In sip_profiles/internal.xml -->
<param name="apply-inbound-acl" value="kamailio"/>
<param name="auth-calls" value="false"/>
<!-- Trust X-headers from Kamailio for routing decisions -->
<param name="apply-proxy-acl" value="kamailio"/>
<!-- In acl.conf.xml -->
<list name="kamailio" default="deny"> <node type="allow" cidr="KAMAILIO_IP/32"/>
</list> <!-- In sip_profiles/internal.xml -->
<param name="apply-inbound-acl" value="kamailio"/>
<param name="auth-calls" value="false"/>
<!-- Trust X-headers from Kamailio for routing decisions -->
<param name="apply-proxy-acl" value="kamailio"/>
<!-- In acl.conf.xml -->
<list name="kamailio" default="deny"> <node type="allow" cidr="KAMAILIO_IP/32"/>
</list> <!-- In sip_profiles/internal.xml -->
<param name="apply-inbound-acl" value="kamailio"/>
<param name="auth-calls" value="false"/>
<!-- Trust X-headers from Kamailio for routing decisions -->
<param name="apply-proxy-acl" value="kamailio"/>
<load module="mod_verto"/>
<load module="mod_verto"/>
<load module="mod_verto"/>
<configuration name="verto.conf" description="WebRTC Verto Endpoint"> <settings> <param name="debug" value="0"/> </settings> <profiles> <profile name="default-v4"> <param name="bind-local" value="YOUR_SERVER_IP:8081"/> <param name="bind-local" value="YOUR_SERVER_IP:8082" secure="true"/> <param name="force-register-domain" value="$${domain}"/> <param name="secure-combined" value="/etc/freeswitch/tls/agent.pem"/> <param name="secure-chain" value="/etc/freeswitch/tls/cafile.pem"/> <param name="userauth" value="true"/> <param name="context" value="default"/> <param name="dialplan" value="XML"/> <!-- STUN/TURN for NAT traversal --> <param name="rtp-ip" value="$${local_ip_v4}"/> <param name="ext-rtp-ip" value="$${external_rtp_ip}"/> <!-- Timer --> <param name="timer-name" value="soft"/> </profile> </profiles>
</configuration>
<configuration name="verto.conf" description="WebRTC Verto Endpoint"> <settings> <param name="debug" value="0"/> </settings> <profiles> <profile name="default-v4"> <param name="bind-local" value="YOUR_SERVER_IP:8081"/> <param name="bind-local" value="YOUR_SERVER_IP:8082" secure="true"/> <param name="force-register-domain" value="$${domain}"/> <param name="secure-combined" value="/etc/freeswitch/tls/agent.pem"/> <param name="secure-chain" value="/etc/freeswitch/tls/cafile.pem"/> <param name="userauth" value="true"/> <param name="context" value="default"/> <param name="dialplan" value="XML"/> <!-- STUN/TURN for NAT traversal --> <param name="rtp-ip" value="$${local_ip_v4}"/> <param name="ext-rtp-ip" value="$${external_rtp_ip}"/> <!-- Timer --> <param name="timer-name" value="soft"/> </profile> </profiles>
</configuration>
<configuration name="verto.conf" description="WebRTC Verto Endpoint"> <settings> <param name="debug" value="0"/> </settings> <profiles> <profile name="default-v4"> <param name="bind-local" value="YOUR_SERVER_IP:8081"/> <param name="bind-local" value="YOUR_SERVER_IP:8082" secure="true"/> <param name="force-register-domain" value="$${domain}"/> <param name="secure-combined" value="/etc/freeswitch/tls/agent.pem"/> <param name="secure-chain" value="/etc/freeswitch/tls/cafile.pem"/> <param name="userauth" value="true"/> <param name="context" value="default"/> <param name="dialplan" value="XML"/> <!-- STUN/TURN for NAT traversal --> <param name="rtp-ip" value="$${local_ip_v4}"/> <param name="ext-rtp-ip" value="$${external_rtp_ip}"/> <!-- Timer --> <param name="timer-name" value="soft"/> </profile> </profiles>
</configuration>
<!DOCTYPE html>
<html>
<head> <title>WebRTC Phone</title> <script src="https://cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js"></script> <script src="verto-min.js"></script>
</head>
<body> <h2>WebRTC Phone</h2> <div> <input type="text" id="number" placeholder="Enter number to call"/> <button onclick="makeCall()">Call</button> <button onclick="hangupCall()">Hangup</button> </div> <div id="status">Disconnected</div> <audio id="remoteAudio" autoplay></audio> <script> var verto; var currentCall = null; // Connect to FreeSWITCH via Verto $(document).ready(function() { verto = new $.verto({ login: '1001@YOUR_SERVER_IP', passwd: 'YOUR_EXTENSION_PASSWORD', socketUrl: 'wss://YOUR_SERVER_IP:8082', // ICE servers for NAT traversal iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ], deviceParams: { useMic: true, useSpeak: true }, audioParams: { googAutoGainControl: true, googNoiseSuppression: true, googEchoCancellation: true } }, { onWSLogin: function(v, success) { $('#status').text(success ? 'Connected' : 'Login Failed'); }, onDialogState: function(d) { switch (d.state.name) { case 'trying': $('#status').text('Calling...'); break; case 'ringing': $('#status').text('Ringing...'); break; case 'active': $('#status').text('In Call'); break; case 'hangup': case 'destroy': $('#status').text('Call Ended'); currentCall = null; break; } } }); }); function makeCall() { var number = $('#number').val(); if (!number) return; currentCall = verto.newCall({ destination_number: number, caller_id_name: 'WebRTC User', caller_id_number: '1001', useVideo: false, useStereo: false }); } function hangupCall() { if (currentCall) { currentCall.hangup(); } } </script>
</body>
</html>
<!DOCTYPE html>
<html>
<head> <title>WebRTC Phone</title> <script src="https://cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js"></script> <script src="verto-min.js"></script>
</head>
<body> <h2>WebRTC Phone</h2> <div> <input type="text" id="number" placeholder="Enter number to call"/> <button onclick="makeCall()">Call</button> <button onclick="hangupCall()">Hangup</button> </div> <div id="status">Disconnected</div> <audio id="remoteAudio" autoplay></audio> <script> var verto; var currentCall = null; // Connect to FreeSWITCH via Verto $(document).ready(function() { verto = new $.verto({ login: '1001@YOUR_SERVER_IP', passwd: 'YOUR_EXTENSION_PASSWORD', socketUrl: 'wss://YOUR_SERVER_IP:8082', // ICE servers for NAT traversal iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ], deviceParams: { useMic: true, useSpeak: true }, audioParams: { googAutoGainControl: true, googNoiseSuppression: true, googEchoCancellation: true } }, { onWSLogin: function(v, success) { $('#status').text(success ? 'Connected' : 'Login Failed'); }, onDialogState: function(d) { switch (d.state.name) { case 'trying': $('#status').text('Calling...'); break; case 'ringing': $('#status').text('Ringing...'); break; case 'active': $('#status').text('In Call'); break; case 'hangup': case 'destroy': $('#status').text('Call Ended'); currentCall = null; break; } } }); }); function makeCall() { var number = $('#number').val(); if (!number) return; currentCall = verto.newCall({ destination_number: number, caller_id_name: 'WebRTC User', caller_id_number: '1001', useVideo: false, useStereo: false }); } function hangupCall() { if (currentCall) { currentCall.hangup(); } } </script>
</body>
</html>
<!DOCTYPE html>
<html>
<head> <title>WebRTC Phone</title> <script src="https://cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js"></script> <script src="verto-min.js"></script>
</head>
<body> <h2>WebRTC Phone</h2> <div> <input type="text" id="number" placeholder="Enter number to call"/> <button onclick="makeCall()">Call</button> <button onclick="hangupCall()">Hangup</button> </div> <div id="status">Disconnected</div> <audio id="remoteAudio" autoplay></audio> <script> var verto; var currentCall = null; // Connect to FreeSWITCH via Verto $(document).ready(function() { verto = new $.verto({ login: '1001@YOUR_SERVER_IP', passwd: 'YOUR_EXTENSION_PASSWORD', socketUrl: 'wss://YOUR_SERVER_IP:8082', // ICE servers for NAT traversal iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ], deviceParams: { useMic: true, useSpeak: true }, audioParams: { googAutoGainControl: true, googNoiseSuppression: true, googEchoCancellation: true } }, { onWSLogin: function(v, success) { $('#status').text(success ? 'Connected' : 'Login Failed'); }, onDialogState: function(d) { switch (d.state.name) { case 'trying': $('#status').text('Calling...'); break; case 'ringing': $('#status').text('Ringing...'); break; case 'active': $('#status').text('In Call'); break; case 'hangup': case 'destroy': $('#status').text('Call Ended'); currentCall = null; break; } } }); }); function makeCall() { var number = $('#number').val(); if (!number) return; currentCall = verto.newCall({ destination_number: number, caller_id_name: 'WebRTC User', caller_id_number: '1001', useVideo: false, useStereo: false }); } function hangupCall() { if (currentCall) { currentCall.hangup(); } } </script>
</body>
</html>
<load module="mod_xml_curl"/>
<load module="mod_xml_curl"/>
<load module="mod_xml_curl"/>
<configuration name="xml_curl.conf" description="cURL XML Gateway"> <bindings> <!-- Dynamic user directory --> <binding name="directory"> <param name="gateway-url" value="http://127.0.0.1:8080/freeswitch/directory"/> <param name="gateway-credentials" value="fsapi:YOUR_API_KEY"/> <param name="auth-scheme" value="basic"/> <param name="timeout" value="5"/> <param name="enable-post" value="true"/> <param name="bindings" value="directory"/> </binding> <!-- Dynamic dialplan --> <binding name="dialplan"> <param name="gateway-url" value="http://127.0.0.1:8080/freeswitch/dialplan"/> <param name="gateway-credentials" value="fsapi:YOUR_API_KEY"/> <param name="auth-scheme" value="basic"/> <param name="timeout" value="5"/> <param name="enable-post" value="true"/> <param name="bindings" value="dialplan"/> </binding> </bindings>
</configuration>
<configuration name="xml_curl.conf" description="cURL XML Gateway"> <bindings> <!-- Dynamic user directory --> <binding name="directory"> <param name="gateway-url" value="http://127.0.0.1:8080/freeswitch/directory"/> <param name="gateway-credentials" value="fsapi:YOUR_API_KEY"/> <param name="auth-scheme" value="basic"/> <param name="timeout" value="5"/> <param name="enable-post" value="true"/> <param name="bindings" value="directory"/> </binding> <!-- Dynamic dialplan --> <binding name="dialplan"> <param name="gateway-url" value="http://127.0.0.1:8080/freeswitch/dialplan"/> <param name="gateway-credentials" value="fsapi:YOUR_API_KEY"/> <param name="auth-scheme" value="basic"/> <param name="timeout" value="5"/> <param name="enable-post" value="true"/> <param name="bindings" value="dialplan"/> </binding> </bindings>
</configuration>
<configuration name="xml_curl.conf" description="cURL XML Gateway"> <bindings> <!-- Dynamic user directory --> <binding name="directory"> <param name="gateway-url" value="http://127.0.0.1:8080/freeswitch/directory"/> <param name="gateway-credentials" value="fsapi:YOUR_API_KEY"/> <param name="auth-scheme" value="basic"/> <param name="timeout" value="5"/> <param name="enable-post" value="true"/> <param name="bindings" value="directory"/> </binding> <!-- Dynamic dialplan --> <binding name="dialplan"> <param name="gateway-url" value="http://127.0.0.1:8080/freeswitch/dialplan"/> <param name="gateway-credentials" value="fsapi:YOUR_API_KEY"/> <param name="auth-scheme" value="basic"/> <param name="timeout" value="5"/> <param name="enable-post" value="true"/> <param name="bindings" value="dialplan"/> </binding> </bindings>
</configuration>
#!/usr/bin/env python3
"""
freeswitch_api.py
Serve dynamic XML configuration to FreeSWITCH via mod_xml_curl.
""" from flask import Flask, request, Response
import sqlite3 app = Flask(__name__)
DB_PATH = '/var/lib/freeswitch/db/users.db' @app.route('/freeswitch/directory', methods=['POST'])
def directory(): """Return user XML based on registration request.""" user = request.form.get('user', '') domain = request.form.get('domain', '') action = request.form.get('action', '') if not user: return not_found() # Look up user in database conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute( 'SELECT password, name, caller_id FROM users WHERE extension = ?', (user,) ) row = cursor.fetchone() conn.close() if not row: return not_found() password, name, caller_id = row xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="directory"> <domain name="{domain}"> <user id="{user}"> <params> <param name="password" value="{password}"/> <param name="vm-password" value="{user}"/> </params> <variables> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="{name}"/> <variable name="effective_caller_id_number" value="{caller_id or user}"/> </variables> </user> </domain> </section>
</document>''' return Response(xml, mimetype='text/xml') @app.route('/freeswitch/dialplan', methods=['POST'])
def dialplan(): """Return dynamic dialplan XML.""" dest = request.form.get('Caller-Destination-Number', '') context = request.form.get('Caller-Context', 'default') caller = request.form.get('Caller-Caller-ID-Number', '') # Example: route based on database lookup conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute( 'SELECT action, target FROM routes WHERE pattern = ? AND context = ?', (dest, context) ) row = cursor.fetchone() conn.close() if row: action_type, target = row xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="dialplan"> <context name="{context}"> <extension name="dynamic_route"> <condition field="destination_number" expression="^{dest}$"> <action application="{action_type}" data="{target}"/> </condition> </extension> </context> </section>
</document>''' else: xml = not_found_xml() return Response(xml, mimetype='text/xml') def not_found(): return Response(not_found_xml(), mimetype='text/xml') def not_found_xml(): return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="result"> <result status="not found"/> </section>
</document>''' if __name__ == '__main__': app.run(host='127.0.0.1', port=8080)
#!/usr/bin/env python3
"""
freeswitch_api.py
Serve dynamic XML configuration to FreeSWITCH via mod_xml_curl.
""" from flask import Flask, request, Response
import sqlite3 app = Flask(__name__)
DB_PATH = '/var/lib/freeswitch/db/users.db' @app.route('/freeswitch/directory', methods=['POST'])
def directory(): """Return user XML based on registration request.""" user = request.form.get('user', '') domain = request.form.get('domain', '') action = request.form.get('action', '') if not user: return not_found() # Look up user in database conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute( 'SELECT password, name, caller_id FROM users WHERE extension = ?', (user,) ) row = cursor.fetchone() conn.close() if not row: return not_found() password, name, caller_id = row xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="directory"> <domain name="{domain}"> <user id="{user}"> <params> <param name="password" value="{password}"/> <param name="vm-password" value="{user}"/> </params> <variables> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="{name}"/> <variable name="effective_caller_id_number" value="{caller_id or user}"/> </variables> </user> </domain> </section>
</document>''' return Response(xml, mimetype='text/xml') @app.route('/freeswitch/dialplan', methods=['POST'])
def dialplan(): """Return dynamic dialplan XML.""" dest = request.form.get('Caller-Destination-Number', '') context = request.form.get('Caller-Context', 'default') caller = request.form.get('Caller-Caller-ID-Number', '') # Example: route based on database lookup conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute( 'SELECT action, target FROM routes WHERE pattern = ? AND context = ?', (dest, context) ) row = cursor.fetchone() conn.close() if row: action_type, target = row xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="dialplan"> <context name="{context}"> <extension name="dynamic_route"> <condition field="destination_number" expression="^{dest}$"> <action application="{action_type}" data="{target}"/> </condition> </extension> </context> </section>
</document>''' else: xml = not_found_xml() return Response(xml, mimetype='text/xml') def not_found(): return Response(not_found_xml(), mimetype='text/xml') def not_found_xml(): return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="result"> <result status="not found"/> </section>
</document>''' if __name__ == '__main__': app.run(host='127.0.0.1', port=8080)
#!/usr/bin/env python3
"""
freeswitch_api.py
Serve dynamic XML configuration to FreeSWITCH via mod_xml_curl.
""" from flask import Flask, request, Response
import sqlite3 app = Flask(__name__)
DB_PATH = '/var/lib/freeswitch/db/users.db' @app.route('/freeswitch/directory', methods=['POST'])
def directory(): """Return user XML based on registration request.""" user = request.form.get('user', '') domain = request.form.get('domain', '') action = request.form.get('action', '') if not user: return not_found() # Look up user in database conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute( 'SELECT password, name, caller_id FROM users WHERE extension = ?', (user,) ) row = cursor.fetchone() conn.close() if not row: return not_found() password, name, caller_id = row xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="directory"> <domain name="{domain}"> <user id="{user}"> <params> <param name="password" value="{password}"/> <param name="vm-password" value="{user}"/> </params> <variables> <variable name="user_context" value="default"/> <variable name="effective_caller_id_name" value="{name}"/> <variable name="effective_caller_id_number" value="{caller_id or user}"/> </variables> </user> </domain> </section>
</document>''' return Response(xml, mimetype='text/xml') @app.route('/freeswitch/dialplan', methods=['POST'])
def dialplan(): """Return dynamic dialplan XML.""" dest = request.form.get('Caller-Destination-Number', '') context = request.form.get('Caller-Context', 'default') caller = request.form.get('Caller-Caller-ID-Number', '') # Example: route based on database lookup conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute( 'SELECT action, target FROM routes WHERE pattern = ? AND context = ?', (dest, context) ) row = cursor.fetchone() conn.close() if row: action_type, target = row xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="dialplan"> <context name="{context}"> <extension name="dynamic_route"> <condition field="destination_number" expression="^{dest}$"> <action application="{action_type}" data="{target}"/> </condition> </extension> </context> </section>
</document>''' else: xml = not_found_xml() return Response(xml, mimetype='text/xml') def not_found(): return Response(not_found_xml(), mimetype='text/xml') def not_found_xml(): return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml"> <section name="result"> <result status="not found"/> </section>
</document>''' if __name__ == '__main__': app.run(host='127.0.0.1', port=8080)
<!-- In dialplan — call a Lua script for routing decisions -->
<extension name="api_routing"> <condition field="destination_number" expression="^(8[0-9]{3})$"> <action application="lua" data="api_route.lua"/> </condition>
</extension>
<!-- In dialplan — call a Lua script for routing decisions -->
<extension name="api_routing"> <condition field="destination_number" expression="^(8[0-9]{3})$"> <action application="lua" data="api_route.lua"/> </condition>
</extension>
<!-- In dialplan — call a Lua script for routing decisions -->
<extension name="api_routing"> <condition field="destination_number" expression="^(8[0-9]{3})$"> <action application="lua" data="api_route.lua"/> </condition>
</extension>
-- api_route.lua
-- Look up routing information from a REST API local api = require("socket.http")
local json = require("cjson") -- Get call info
local caller = session:getVariable("caller_id_number")
local dest = session:getVariable("destination_number") -- Call external API
local url = string.format( "http://127.0.0.1:8080/api/route?caller=%s&dest=%s", caller, dest
) local response, code = api.request(url) if code == 200 and response then local data = json.decode(response) if data.action == "bridge" then session:setVariable("effective_caller_id_number", data.caller_id or caller) session:execute("bridge", data.target) elseif data.action == "voicemail" then session:execute("voicemail", "default " .. data.domain .. " " .. data.mailbox) elseif data.action == "reject" then session:execute("respond", "403") end
else -- API unavailable — fallback to default routing session:execute("transfer", "1009 XML default")
end
-- api_route.lua
-- Look up routing information from a REST API local api = require("socket.http")
local json = require("cjson") -- Get call info
local caller = session:getVariable("caller_id_number")
local dest = session:getVariable("destination_number") -- Call external API
local url = string.format( "http://127.0.0.1:8080/api/route?caller=%s&dest=%s", caller, dest
) local response, code = api.request(url) if code == 200 and response then local data = json.decode(response) if data.action == "bridge" then session:setVariable("effective_caller_id_number", data.caller_id or caller) session:execute("bridge", data.target) elseif data.action == "voicemail" then session:execute("voicemail", "default " .. data.domain .. " " .. data.mailbox) elseif data.action == "reject" then session:execute("respond", "403") end
else -- API unavailable — fallback to default routing session:execute("transfer", "1009 XML default")
end
-- api_route.lua
-- Look up routing information from a REST API local api = require("socket.http")
local json = require("cjson") -- Get call info
local caller = session:getVariable("caller_id_number")
local dest = session:getVariable("destination_number") -- Call external API
local url = string.format( "http://127.0.0.1:8080/api/route?caller=%s&dest=%s", caller, dest
) local response, code = api.request(url) if code == 200 and response then local data = json.decode(response) if data.action == "bridge" then session:setVariable("effective_caller_id_number", data.caller_id or caller) session:execute("bridge", data.target) elseif data.action == "voicemail" then session:execute("voicemail", "default " .. data.domain .. " " .. data.mailbox) elseif data.action == "reject" then session:execute("respond", "403") end
else -- API unavailable — fallback to default routing session:execute("transfer", "1009 XML default")
end
<include> <gateway name="asterisk"> <param name="username" value="freeswitch_trunk"/> <param name="password" value="SHARED_TRUNK_PASSWORD"/> <param name="realm" value="ASTERISK_IP"/> <param name="proxy" value="ASTERISK_IP"/> <param name="register" value="true"/> <param name="caller-id-in-from" value="true"/> <param name="codec-prefs" value="PCMU,PCMA"/> </gateway>
</include>
<include> <gateway name="asterisk"> <param name="username" value="freeswitch_trunk"/> <param name="password" value="SHARED_TRUNK_PASSWORD"/> <param name="realm" value="ASTERISK_IP"/> <param name="proxy" value="ASTERISK_IP"/> <param name="register" value="true"/> <param name="caller-id-in-from" value="true"/> <param name="codec-prefs" value="PCMU,PCMA"/> </gateway>
</include>
<include> <gateway name="asterisk"> <param name="username" value="freeswitch_trunk"/> <param name="password" value="SHARED_TRUNK_PASSWORD"/> <param name="realm" value="ASTERISK_IP"/> <param name="proxy" value="ASTERISK_IP"/> <param name="register" value="true"/> <param name="caller-id-in-from" value="true"/> <param name="codec-prefs" value="PCMU,PCMA"/> </gateway>
</include>
<extension name="to_asterisk"> <condition field="destination_number" expression="^(2[0-9]{3})$"> <action application="bridge" data="sofia/gateway/asterisk/$1"/> </condition>
</extension>
<extension name="to_asterisk"> <condition field="destination_number" expression="^(2[0-9]{3})$"> <action application="bridge" data="sofia/gateway/asterisk/$1"/> </condition>
</extension>
<extension name="to_asterisk"> <condition field="destination_number" expression="^(2[0-9]{3})$"> <action application="bridge" data="sofia/gateway/asterisk/$1"/> </condition>
</extension>
; pjsip.conf — FreeSWITCH trunk
[freeswitch_trunk]
type=registration
outbound_auth=freeswitch_trunk_auth
server_uri=sip:FREESWITCH_IP:5080
client_uri=sip:freeswitch_trunk@FREESWITCH_IP:5080
retry_interval=60 [freeswitch_trunk_auth]
type=auth
auth_type=userpass
username=freeswitch_trunk
password=SHARED_TRUNK_PASSWORD [freeswitch_trunk_endpoint]
type=endpoint
context=from-freeswitch
disallow=all
allow=ulaw,alaw
outbound_auth=freeswitch_trunk_auth
aors=freeswitch_trunk_aor [freeswitch_trunk_aor]
type=aor
contact=sip:FREESWITCH_IP:5080 [freeswitch_trunk_identify]
type=identify
endpoint=freeswitch_trunk_endpoint
match=FREESWITCH_IP
; pjsip.conf — FreeSWITCH trunk
[freeswitch_trunk]
type=registration
outbound_auth=freeswitch_trunk_auth
server_uri=sip:FREESWITCH_IP:5080
client_uri=sip:freeswitch_trunk@FREESWITCH_IP:5080
retry_interval=60 [freeswitch_trunk_auth]
type=auth
auth_type=userpass
username=freeswitch_trunk
password=SHARED_TRUNK_PASSWORD [freeswitch_trunk_endpoint]
type=endpoint
context=from-freeswitch
disallow=all
allow=ulaw,alaw
outbound_auth=freeswitch_trunk_auth
aors=freeswitch_trunk_aor [freeswitch_trunk_aor]
type=aor
contact=sip:FREESWITCH_IP:5080 [freeswitch_trunk_identify]
type=identify
endpoint=freeswitch_trunk_endpoint
match=FREESWITCH_IP
; pjsip.conf — FreeSWITCH trunk
[freeswitch_trunk]
type=registration
outbound_auth=freeswitch_trunk_auth
server_uri=sip:FREESWITCH_IP:5080
client_uri=sip:freeswitch_trunk@FREESWITCH_IP:5080
retry_interval=60 [freeswitch_trunk_auth]
type=auth
auth_type=userpass
username=freeswitch_trunk
password=SHARED_TRUNK_PASSWORD [freeswitch_trunk_endpoint]
type=endpoint
context=from-freeswitch
disallow=all
allow=ulaw,alaw
outbound_auth=freeswitch_trunk_auth
aors=freeswitch_trunk_aor [freeswitch_trunk_aor]
type=aor
contact=sip:FREESWITCH_IP:5080 [freeswitch_trunk_identify]
type=identify
endpoint=freeswitch_trunk_endpoint
match=FREESWITCH_IP
┌──────────────┐ │ Kamailio │ SIP phones ──────► (Router) ◄────── SIP Trunks └──────┬───────┘ │ ┌────────────┼────────────┐ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ FS Node 1│ │ FS Node 2│ │ FS Node 3│ │ (Media) │ │ (Media) │ │ (Media) │ └──────────┘ └──────────┘ └──────────┘
┌──────────────┐ │ Kamailio │ SIP phones ──────► (Router) ◄────── SIP Trunks └──────┬───────┘ │ ┌────────────┼────────────┐ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ FS Node 1│ │ FS Node 2│ │ FS Node 3│ │ (Media) │ │ (Media) │ │ (Media) │ └──────────┘ └──────────┘ └──────────┘
┌──────────────┐ │ Kamailio │ SIP phones ──────► (Router) ◄────── SIP Trunks └──────┬───────┘ │ ┌────────────┼────────────┐ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ FS Node 1│ │ FS Node 2│ │ FS Node 3│ │ (Media) │ │ (Media) │ │ (Media) │ └──────────┘ └──────────┘ └──────────┘
# kamailio.cfg — load balance across FreeSWITCH nodes
modparam("dispatcher", "list_file", "/etc/kamailio/dispatcher.list") # dispatcher.list:
# setid destination flags priority
1 sip:FS_NODE_1:5060 0 50
1 sip:FS_NODE_2:5060 0 50
1 sip:FS_NODE_3:5060 0 50
# kamailio.cfg — load balance across FreeSWITCH nodes
modparam("dispatcher", "list_file", "/etc/kamailio/dispatcher.list") # dispatcher.list:
# setid destination flags priority
1 sip:FS_NODE_1:5060 0 50
1 sip:FS_NODE_2:5060 0 50
1 sip:FS_NODE_3:5060 0 50
# kamailio.cfg — load balance across FreeSWITCH nodes
modparam("dispatcher", "list_file", "/etc/kamailio/dispatcher.list") # dispatcher.list:
# setid destination flags priority
1 sip:FS_NODE_1:5060 0 50
1 sip:FS_NODE_2:5060 0 50
1 sip:FS_NODE_3:5060 0 50
# 1. Check if the profile is running
fs_cli -x "sofia status" # 2. Check registered users
fs_cli -x "sofia status profile internal reg" # 3. Enable SIP trace to see actual SIP messages
fs_cli -x "sofia profile internal siptrace on"
# Make a registration attempt, then:
fs_cli -x "sofia profile internal siptrace off" # 4. Check the log for auth failures
grep -i "auth" /var/log/freeswitch/freeswitch.log | tail -20 # 5. Verify the user exists in the directory
fs_cli -x "xml_locate directory default 1001" # 6. Test connectivity
# From the phone's network, check if SIP port is reachable:
# nmap -sU -p 5060 YOUR_SERVER_IP
# 1. Check if the profile is running
fs_cli -x "sofia status" # 2. Check registered users
fs_cli -x "sofia status profile internal reg" # 3. Enable SIP trace to see actual SIP messages
fs_cli -x "sofia profile internal siptrace on"
# Make a registration attempt, then:
fs_cli -x "sofia profile internal siptrace off" # 4. Check the log for auth failures
grep -i "auth" /var/log/freeswitch/freeswitch.log | tail -20 # 5. Verify the user exists in the directory
fs_cli -x "xml_locate directory default 1001" # 6. Test connectivity
# From the phone's network, check if SIP port is reachable:
# nmap -sU -p 5060 YOUR_SERVER_IP
# 1. Check if the profile is running
fs_cli -x "sofia status" # 2. Check registered users
fs_cli -x "sofia status profile internal reg" # 3. Enable SIP trace to see actual SIP messages
fs_cli -x "sofia profile internal siptrace on"
# Make a registration attempt, then:
fs_cli -x "sofia profile internal siptrace off" # 4. Check the log for auth failures
grep -i "auth" /var/log/freeswitch/freeswitch.log | tail -20 # 5. Verify the user exists in the directory
fs_cli -x "xml_locate directory default 1001" # 6. Test connectivity
# From the phone's network, check if SIP port is reachable:
# nmap -sU -p 5060 YOUR_SERVER_IP
# 1. Check RTP ports in firewall
# Ensure 16384-32768/udp is open # 2. Check NAT settings
fs_cli -x "sofia status profile internal"
# Look for ext-sip-ip and ext-rtp-ip — they should be your PUBLIC IP # 3. Check codec negotiation
fs_cli -x "show channels"
# Look at read_codec and write_codec — should match on both legs # 4. During a call, check RTP stats
fs_cli -x "uuid_debug_media <call-uuid> read on"
# This shows incoming RTP packets (or lack thereof) # 5. Check for RTP timeout
grep "rtp_timeout" /var/log/freeswitch/freeswitch.log | tail -10
# 1. Check RTP ports in firewall
# Ensure 16384-32768/udp is open # 2. Check NAT settings
fs_cli -x "sofia status profile internal"
# Look for ext-sip-ip and ext-rtp-ip — they should be your PUBLIC IP # 3. Check codec negotiation
fs_cli -x "show channels"
# Look at read_codec and write_codec — should match on both legs # 4. During a call, check RTP stats
fs_cli -x "uuid_debug_media <call-uuid> read on"
# This shows incoming RTP packets (or lack thereof) # 5. Check for RTP timeout
grep "rtp_timeout" /var/log/freeswitch/freeswitch.log | tail -10
# 1. Check RTP ports in firewall
# Ensure 16384-32768/udp is open # 2. Check NAT settings
fs_cli -x "sofia status profile internal"
# Look for ext-sip-ip and ext-rtp-ip — they should be your PUBLIC IP # 3. Check codec negotiation
fs_cli -x "show channels"
# Look at read_codec and write_codec — should match on both legs # 4. During a call, check RTP stats
fs_cli -x "uuid_debug_media <call-uuid> read on"
# This shows incoming RTP packets (or lack thereof) # 5. Check for RTP timeout
grep "rtp_timeout" /var/log/freeswitch/freeswitch.log | tail -10
<!-- Verify these are set correctly in your profile -->
<param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/>
<param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/>
<param name="apply-nat-acl" value="nat.auto"/>
<param name="aggressive-nat-detection" value="true"/>
<!-- Verify these are set correctly in your profile -->
<param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/>
<param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/>
<param name="apply-nat-acl" value="nat.auto"/>
<param name="aggressive-nat-detection" value="true"/>
<!-- Verify these are set correctly in your profile -->
<param name="ext-sip-ip" value="YOUR_PUBLIC_IP"/>
<param name="ext-rtp-ip" value="YOUR_PUBLIC_IP"/>
<param name="apply-nat-acl" value="nat.auto"/>
<param name="aggressive-nat-detection" value="true"/>
# 1. Test dialplan matching without making a real call
fs_cli -x "expand xml_locate dialplan default 15551234567 as xml" # 2. Enable dialplan debug
fs_cli -x "sofia loglevel all 7" # 3. Check which context the call enters
grep "Processing" /var/log/freeswitch/freeswitch.log | tail -20 # 4. Manually trace a number through the dialplan
fs_cli
> reloadxml
> originate user/1001 &echo()
# Then check logs for the routing path # 5. Check for regex issues
# In fs_cli:
> regex 15551234567 ^(\+?1?\d{10})$
# Should return "true" if the pattern matches
# 1. Test dialplan matching without making a real call
fs_cli -x "expand xml_locate dialplan default 15551234567 as xml" # 2. Enable dialplan debug
fs_cli -x "sofia loglevel all 7" # 3. Check which context the call enters
grep "Processing" /var/log/freeswitch/freeswitch.log | tail -20 # 4. Manually trace a number through the dialplan
fs_cli
> reloadxml
> originate user/1001 &echo()
# Then check logs for the routing path # 5. Check for regex issues
# In fs_cli:
> regex 15551234567 ^(\+?1?\d{10})$
# Should return "true" if the pattern matches
# 1. Test dialplan matching without making a real call
fs_cli -x "expand xml_locate dialplan default 15551234567 as xml" # 2. Enable dialplan debug
fs_cli -x "sofia loglevel all 7" # 3. Check which context the call enters
grep "Processing" /var/log/freeswitch/freeswitch.log | tail -20 # 4. Manually trace a number through the dialplan
fs_cli
> reloadxml
> originate user/1001 &echo()
# Then check logs for the routing path # 5. Check for regex issues
# In fs_cli:
> regex 15551234567 ^(\+?1?\d{10})$
# Should return "true" if the pattern matches
# 1. Check system resources
fs_cli -x "status"
# Look at "min idle cpu" — should be above 50% # 2. Check session count
fs_cli -x "show channels count" # 3. Check for codec transcoding (CPU-intensive)
fs_cli -x "show channels"
# If read_codec != write_codec, transcoding is happening # 4. Check for memory leaks
fs_cli -x "status"
# Monitor session count over time — should not grow unbounded # 5. Profile-level stats
fs_cli -x "sofia status profile internal"
# Check CALLS-IN, CALLS-OUT, FAILED counts
# 1. Check system resources
fs_cli -x "status"
# Look at "min idle cpu" — should be above 50% # 2. Check session count
fs_cli -x "show channels count" # 3. Check for codec transcoding (CPU-intensive)
fs_cli -x "show channels"
# If read_codec != write_codec, transcoding is happening # 4. Check for memory leaks
fs_cli -x "status"
# Monitor session count over time — should not grow unbounded # 5. Profile-level stats
fs_cli -x "sofia status profile internal"
# Check CALLS-IN, CALLS-OUT, FAILED counts
# 1. Check system resources
fs_cli -x "status"
# Look at "min idle cpu" — should be above 50% # 2. Check session count
fs_cli -x "show channels count" # 3. Check for codec transcoding (CPU-intensive)
fs_cli -x "show channels"
# If read_codec != write_codec, transcoding is happening # 4. Check for memory leaks
fs_cli -x "status"
# Monitor session count over time — should not grow unbounded # 5. Profile-level stats
fs_cli -x "sofia status profile internal"
# Check CALLS-IN, CALLS-OUT, FAILED counts
# Increase file descriptor limits
cat >> /etc/security/limits.conf << 'EOF'
freeswitch soft nofile 65536
freeswitch hard nofile 65536
freeswitch soft nproc 65536
freeswitch hard nproc 65536
EOF # Increase max sessions (default 1000)
fs_cli -x "fsctl max_sessions 5000" # Increase sessions per second (default 30)
fs_cli -x "fsctl sps 200" # Disable unnecessary modules
# Edit modules.conf.xml and comment out unused modules
# Fewer modules = less memory + faster startup
# Increase file descriptor limits
cat >> /etc/security/limits.conf << 'EOF'
freeswitch soft nofile 65536
freeswitch hard nofile 65536
freeswitch soft nproc 65536
freeswitch hard nproc 65536
EOF # Increase max sessions (default 1000)
fs_cli -x "fsctl max_sessions 5000" # Increase sessions per second (default 30)
fs_cli -x "fsctl sps 200" # Disable unnecessary modules
# Edit modules.conf.xml and comment out unused modules
# Fewer modules = less memory + faster startup
# Increase file descriptor limits
cat >> /etc/security/limits.conf << 'EOF'
freeswitch soft nofile 65536
freeswitch hard nofile 65536
freeswitch soft nproc 65536
freeswitch hard nproc 65536
EOF # Increase max sessions (default 1000)
fs_cli -x "fsctl max_sessions 5000" # Increase sessions per second (default 30)
fs_cli -x "fsctl sps 200" # Disable unnecessary modules
# Edit modules.conf.xml and comment out unused modules
# Fewer modules = less memory + faster startup
# 1. Is FreeSWITCH running?
systemctl status freeswitch # 2. Can you connect via ESL?
fs_cli -x "status" # 3. Are SIP profiles up?
fs_cli -x "sofia status" # 4. Are phones registered?
fs_cli -x "sofia status profile internal reg" # 5. Are trunks connected?
fs_cli -x "sofia status gateway my_provider" # 6. Any errors in the log?
tail -100 /var/log/freeswitch/freeswitch.log | grep -i "error\|warning\|fail" # 7. Is it a firewall issue?
ss -ulnp | grep 5060 # SIP port listening?
ss -ulnp | grep 8021 # ESL port listening? # 8. Disk space?
df -h # 9. Memory?
free -h # 10. Active calls?
fs_cli -x "show channels count"
# 1. Is FreeSWITCH running?
systemctl status freeswitch # 2. Can you connect via ESL?
fs_cli -x "status" # 3. Are SIP profiles up?
fs_cli -x "sofia status" # 4. Are phones registered?
fs_cli -x "sofia status profile internal reg" # 5. Are trunks connected?
fs_cli -x "sofia status gateway my_provider" # 6. Any errors in the log?
tail -100 /var/log/freeswitch/freeswitch.log | grep -i "error\|warning\|fail" # 7. Is it a firewall issue?
ss -ulnp | grep 5060 # SIP port listening?
ss -ulnp | grep 8021 # ESL port listening? # 8. Disk space?
df -h # 9. Memory?
free -h # 10. Active calls?
fs_cli -x "show channels count"
# 1. Is FreeSWITCH running?
systemctl status freeswitch # 2. Can you connect via ESL?
fs_cli -x "status" # 3. Are SIP profiles up?
fs_cli -x "sofia status" # 4. Are phones registered?
fs_cli -x "sofia status profile internal reg" # 5. Are trunks connected?
fs_cli -x "sofia status gateway my_provider" # 6. Any errors in the log?
tail -100 /var/log/freeswitch/freeswitch.log | grep -i "error\|warning\|fail" # 7. Is it a firewall issue?
ss -ulnp | grep 5060 # SIP port listening?
ss -ulnp | grep 8021 # ESL port listening? # 8. Disk space?
df -h # 9. Memory?
free -h # 10. Active calls?
fs_cli -x "show channels count" - Introduction
- Architecture Overview
- Installation
- SIP Configuration (mod_sofia)
- XML Dialplan
- Call Recording
- Conference Bridge
- Event Socket Layer (ESL)
- CDR & Logging
- Security Hardening
- Integration Patterns
- Troubleshooting - Carrier-grade switching: High call volume (1,000+ concurrent), SIP-to-SIP routing, least-cost routing
- WebRTC gateway: Browser-based calling without external proxies
- IVR platform: Complex multi-level IVR systems with database lookups and REST API integration
- Conference server: Large-scale conferencing (hundreds of rooms, thousands of participants)
- Media server: Behind Kamailio/OpenSIPS as a B2BUA or media processing engine
- Telecom applications: When your app controls calls programmatically via ESL
- Multi-tenant PBX: Built-in domain-based multi-tenancy - Event-driven: Everything in FreeSWITCH generates events. A call arriving, a DTMF press, a conference join — all are events that can be captured, filtered, and acted upon by internal modules or external applications.
- Modular: The core is a lightweight engine. All functionality — SIP, dialplan, codecs, applications — comes from loadable modules. You enable only what you need.
- Session-based: Each call creates a session object that persists for the call's lifetime. Sessions hold all state (variables, media streams, applications) and are managed by a thread pool, not one-thread-per-call. - Go to https://id.signalwire.com/personal_access_tokens
- Create a free account (no credit card needed)
- Generate a Personal Access Token
- Save the token — you will need it below - mod_ivr menus — XML-configured menu trees with automatic DTMF handling, timeouts, and invalid-input retries. Best for simple, static IVR flows.
- play_and_get_digits — A dialplan application that plays a prompt and collects DTMF digits. Best for dynamic IVRs with variable-based routing. - 5 — Configuration menu
- 1 — Record a new greeting
- 2 — Choose between recorded greetings - mod_callcenter — Built-in ACD (Automatic Call Distribution) for contact centers
- mod_fifo — Call queues and parking
- mod_lcr — Least Cost Routing for multi-carrier environments
- mod_nibblebill — Real-time prepaid billing
- mod_skinny — Cisco SCCP phone support
- FusionPBX — Full web GUI for FreeSWITCH administration