Tutorial 42: Kamailio Fundamentals — Installation, Routing, Authentication & TLS
A complete beginner-to-intermediate guide to Kamailio — the high-performance open-source SIP proxy/router. Learn installation, SIP routing logic, user authentication, NAT traversal, TLS encryption, and load balancing from scratch, with production-ready configurations for use as a Session Border Controller (SBC) in front of Asterisk or FreeSWITCH. Whether you need to scale a single-server PBX to handle thousands of concurrent registrations, add a security layer in front of your media servers, or build a multi-tenant SIP platform, Kamailio is the tool that makes it possible.
Technologies: Kamailio, SIP, RTPEngine, TLS, WebRTC, MariaDB, Dispatcher, NAT, Prometheus Difficulty: Beginner–Intermediate Reading time: ~70 minutes
Table of Contents
- Introduction
- Architecture Overview
- Installation — Debian 12 / Ubuntu 24.04
- Configuration Language
- SIP Routing
- User Authentication
- NAT Traversal
- TLS & Security
- Load Balancing with Dispatcher
- Dialog & Accounting
- WebRTC Gateway
- Integration with Media Servers
- Monitoring & Management
- Troubleshooting
1. Introduction
What Is Kamailio?
Kamailio is a high-performance, open-source SIP (Session Initiation Protocol) server written in C. It functions as a SIP proxy, SIP registrar, SIP router, and application server. It does not handle media (audio/video) — it exclusively processes SIP signaling messages.
Think of Kamailio as a traffic controller for phone calls. It decides where calls should go, authenticates callers, and enforces policies — but it never touches the actual voice data. That job belongs to media servers like Asterisk, FreeSWITCH, or RTPEngine.
Kamailio Is NOT a PBX
This is the single most important concept to internalize:
| Aspect | Kamailio | Asterisk / FreeSWITCH |
|---|---|---|
| Role | SIP Proxy / Router | PBX / Media Server (B2BUA) |
| Media handling | None — signaling only | Full media processing |
| Call capacity | 10,000–50,000+ CPS | 200–2,000 concurrent calls |
| Voicemail, IVR | No | Yes |
| Conference bridges | No | Yes |
| Call recording | No | Yes |
| SIP message processing | Extremely fast (microseconds) | Slower (milliseconds) |
| Configuration | Custom routing language | dialplan (Asterisk) / XML (FS) |
| Architecture | Stateless or stateful proxy | Back-to-Back User Agent |
B2BUA vs Proxy: Asterisk and FreeSWITCH act as a B2BUA (Back-to-Back User Agent) — they terminate one SIP session and create a new one, sitting in the middle of both the signaling and media paths. Kamailio acts as a proxy — it forwards SIP messages without terminating the session, and media flows directly between endpoints (or through a separate media relay like RTPEngine).
When You Need Kamailio
You need Kamailio when:
- Scaling: Your Asterisk server handles 200 registrations fine, but you need to support 10,000+ SIP devices
- Security (SBC): You want a hardened front-end that absorbs SIP attacks before they reach your PBX
- Load balancing: You have 3+ Asterisk servers and need to distribute calls intelligently
- Multi-tenant routing: You operate a SIP platform serving multiple customers/domains
- Geographic routing: Route calls to the nearest media server based on caller location
- WebRTC gateway: Bridge browser-based SIP.js/JsSIP clients to your traditional SIP infrastructure
- Registration proxy: Offload thousands of SIP registrations from your media server
- Topology hiding: Hide your internal network structure from external SIP peers
- Regulatory compliance: Apply call routing rules, rate limiting, and CDR collection at the edge
Use Cases
| Use Case | Description |
|---|---|
| Session Border Controller (SBC) | Front-end proxy protecting Asterisk/FreeSWITCH from the internet |
| SIP Load Balancer | Distribute calls across multiple media servers with failover |
| SIP Registrar | Handle 50,000+ device registrations efficiently |
| SIP Router | Route calls between carriers, PBXes, and customers |
| WebRTC Gateway | Convert WSS/SIP.js traffic to standard SIP+RTP |
| Fraud Prevention | Rate limiting, geo-blocking, call pattern analysis |
| Number Portability | LNP database lookups for call routing |
2. Architecture Overview
Core Engine
Kamailio's core is a SIP message processing engine. When a SIP message arrives, the core:
- Parses the SIP message
- Executes the routing logic defined in
kamailio.cfg - Forwards, replies, or drops the message based on your rules
The core itself is minimal — most functionality comes from modules.
Module System
Kamailio has 200+ modules. Here are the essential ones:
| Module | Purpose |
|---|---|
| sl | Stateless replies (400, 500 errors) |
| tm | Transaction management (stateful SIP processing) |
| rr | Record-Route header handling (stay in the signaling path) |
| maxfwd | Max-Forwards header check (loop prevention) |
| usrloc | User location database (where devices are registered) |
| registrar | REGISTER request processing |
| auth | SIP authentication framework |
| auth_db | Database-backed authentication (subscriber table) |
| dialog | Call dialog tracking (active call count, duration) |
| dispatcher | Load balancing / failover to backend servers |
| nathelper | NAT detection and signaling fix-up |
| rtpengine | RTPEngine media relay integration |
| tls | TLS/SSL transport for SIP |
| websocket | SIP over WebSocket (for WebRTC) |
| pike | Flood detection and blocking |
| htable | In-memory hash tables (blacklists, caches) |
| acc | Call accounting (CDRs) |
| permissions | IP-based access control |
| topoh | Topology hiding |
| jsonrpcs | JSON-RPC management interface |
| xlog | Extended logging with pseudo-variables |
Configuration Language
Kamailio uses its own configuration language in kamailio.cfg. The config consists of:
- Global parameters — server settings (listen address, debug level, etc.)
- Module loading — which modules to activate
- Module parameters — settings for each module
- Route blocks — the actual SIP processing logic
Request Processing Flow
Every SIP request follows this processing chain:
SIP Request arrives
│
▼
┌──────────────┐
│ request_route│ ── Main routing logic (MANDATORY)
└──────┬───────┘
│ t_relay() / forward()
▼
┌──────────────┐
│ branch_route │ ── Per-branch processing (before forwarding)
└──────┬───────┘
│ SIP request sent to destination
│
│ Response comes back
▼
┌──────────────┐
│ onreply_route│ ── Process responses (180, 200, etc.)
└──────┬───────┘
│ If negative response (4xx, 5xx, 6xx)
▼
┌──────────────┐
│ failure_route│ ── Handle failures (try alternate route)
└──────────────┘
Additional route types:
- reply_route — global reply processing (all responses)
- onsend_route — executed just before sending a message out
- event_route — triggered by internal events (e.g.,
event_route[tm:local-request])
Media Path — Kamailio Does NOT Touch RTP
SIP Signaling
Phone A ◄─────────────────────────────────► Phone B
│ via Kamailio proxy │
│ │
│ RTP Media (Voice) │
└──────────────────────────────────────────┘
Direct (or via RTPEngine)
Kamailio only processes the SIP signaling path. For media (RTP), you either:
- Let endpoints send media directly to each other (simple, but breaks with NAT)
- Use RTPEngine as a media relay (required for NAT, WebRTC, SRTP)
- Route through a B2BUA like Asterisk (which handles both signaling and media)
Comparison: Kamailio vs OpenSIPS vs Asterisk
| Feature | Kamailio | OpenSIPS | Asterisk |
|---|---|---|---|
| Type | SIP Proxy | SIP Proxy | B2BUA / PBX |
| Origin | Fork of SER (2005) | Fork of OpenSER/Kamailio (2008) | Digium (1999) |
| Language | C | C | C |
| Config | kamailio.cfg (scripting) | opensips.cfg (scripting) | extensions.conf / ARI |
| Performance | ~50,000 CPS | ~50,000 CPS | ~200-500 CPS |
| Media | No (use RTPEngine) | No (use RTPEngine) | Yes (built-in) |
| Registrations | 1M+ | 1M+ | ~10,000 practical |
| IVR/Voicemail | No | No | Yes |
| Learning curve | Steep | Steep | Moderate |
| Community | Large, active | Large, active | Very large |
| License | GPLv2 | GPLv2 | GPLv2 (dual) |
Kamailio and OpenSIPS share the same heritage (both forked from SIP Express Router). They have similar config syntax but have diverged significantly. Choose either based on your team's familiarity — both are production-proven at massive scale.
3. Installation
Prerequisites
- OS: Debian 12 (Bookworm) or Ubuntu 24.04 (Noble) — 64-bit
- RAM: 1 GB minimum, 4 GB+ recommended for production
- CPU: 1 core minimum, 4+ cores for production
- Disk: 10 GB minimum
- Network: Static public IP, ports 5060/UDP+TCP, 5061/TCP (TLS), 8443/TCP (WSS)
Step 1: Add the Official Kamailio Repository
Always use the official Kamailio repository for the latest stable release:
# Install prerequisites
apt update
apt install -y gnupg2 curl apt-transport-https
# --- For Debian 12 (Bookworm) ---
curl -fsSL https://deb.kamailio.org/kamailiodebkey.gpg | \
gpg --dearmor -o /usr/share/keyrings/kamailio-archive-keyring.gpg
cat > /etc/apt/sources.list.d/kamailio.list << 'EOF'
deb [signed-by=/usr/share/keyrings/kamailio-archive-keyring.gpg] https://deb.kamailio.org/kamailio60 bookworm main
EOF
# --- For Ubuntu 24.04 (Noble) ---
curl -fsSL https://deb.kamailio.org/kamailiodebkey.gpg | \
gpg --dearmor -o /usr/share/keyrings/kamailio-archive-keyring.gpg
cat > /etc/apt/sources.list.d/kamailio.list << 'EOF'
deb [signed-by=/usr/share/keyrings/kamailio-archive-keyring.gpg] https://deb.kamailio.org/kamailio60 noble main
EOF
apt update
Step 2: Install Kamailio and Essential Modules
apt install -y \
kamailio \
kamailio-mysql-modules \
kamailio-tls-modules \
kamailio-websocket-modules \
kamailio-json-modules \
kamailio-extra-modules \
kamailio-xml-modules \
kamailio-presence-modules
# Verify installation
kamailio -v
# Output: version: kamailio 6.0.x ...
Package breakdown:
| Package | Modules Included |
|---|---|
kamailio |
Core + sl, tm, rr, maxfwd, textops, siputils, xlog, sanity, pv, corex, kex |
kamailio-mysql-modules |
db_mysql (MySQL/MariaDB database connector) |
kamailio-tls-modules |
tls (SIP over TLS) |
kamailio-websocket-modules |
websocket (SIP over WebSocket for WebRTC) |
kamailio-json-modules |
json, jsonrpcs (JSON parsing + management interface) |
kamailio-extra-modules |
htable, pike, rtpengine, pipelimit, and more |
kamailio-xml-modules |
XML parsing support |
kamailio-presence-modules |
presence, presence_xml (BLF/presence) |
Step 3: Install and Configure the Database
Kamailio needs a database for user authentication, location storage, and call accounting.
# Install MariaDB
apt install -y mariadb-server mariadb-client
# Secure the installation
mysql_secure_installation
# Set root password, remove anonymous users, disable remote root, remove test DB
# Start and enable MariaDB
systemctl enable --now mariadb
Configure the Kamailio database control tool:
# Edit kamctlrc — the database configuration file
cat > /etc/kamailio/kamctlrc << 'CONF'
## Database type: MYSQL, PGSQL, ORACLE, DB_BERKELEY, DBTEXT
DBENGINE=MYSQL
## Database host
DBHOST=localhost
## Database name
DBNAME=kamailio
## Database admin user (for creating the DB)
DBRWUSER="kamailio"
DBRWPW="YOUR_KAMAILIO_DB_PASSWORD"
## Read-only user (optional, for failover)
DBROUSER="kamailioro"
DBROPW="YOUR_KAMAILIO_RO_PASSWORD"
## Database port
DBPORT=3306
## Path to mysql client
INSTALL_EXTRA_TABLES=yes
INSTALL_PRESENCE_TABLES=yes
INSTALL_DBUID_TABLES=yes
CONF
Create the database and tables:
# Create the Kamailio database (answer 'y' to all prompts)
kamdbctl create
# This creates:
# - kamailio database
# - Core tables: location, subscriber, version, etc.
# - Extra tables: dialog, acc, dispatcher, etc.
# - Presence tables: presentity, watchers, etc.
# Verify
mysql -u kamailio -p'YOUR_KAMAILIO_DB_PASSWORD' -e "SHOW TABLES;" kamailio
Expected tables include:
+--------------------+
| Tables_in_kamailio |
+--------------------+
| acc |
| address |
| aliases |
| dialog |
| dispatcher |
| domain |
| location |
| missed_calls |
| subscriber |
| trusted |
| version |
| ... |
+--------------------+
Step 4: Configure Kamailio Defaults
# Enable Kamailio to start via systemd
sed -i 's/^RUN_KAMAILIO=no/RUN_KAMAILIO=yes/' /etc/default/kamailio
# Set memory allocation (adjust based on your server)
sed -i 's/^# SHM_MEMORY=.*/SHM_MEMORY=128/' /etc/default/kamailio
sed -i 's/^# PKG_MEMORY=.*/PKG_MEMORY=16/' /etc/default/kamailio
# Review the defaults file
cat /etc/default/kamailio
The /etc/default/kamailio file should contain:
# Set to "yes" to enable kamailio
RUN_KAMAILIO=yes
# User and group to run as
USER=kamailio
GROUP=kamailio
# Shared memory (MB) — total pool for all Kamailio processes
SHM_MEMORY=128
# Private memory (MB) — per-process memory
PKG_MEMORY=16
# Config file location
CFGFILE=/etc/kamailio/kamailio.cfg
# PID file
PIDFILE=/run/kamailio/kamailio.pid
Step 5: Firewall Configuration
# Using ufw
ufw allow 5060/udp comment "SIP UDP"
ufw allow 5060/tcp comment "SIP TCP"
ufw allow 5061/tcp comment "SIP TLS"
ufw allow 8443/tcp comment "SIP WSS (WebRTC)"
ufw allow 10000:20000/udp comment "RTP Media (RTPEngine)"
# Or using iptables directly
iptables -A INPUT -p udp --dport 5060 -j ACCEPT
iptables -A INPUT -p tcp --dport 5060 -j ACCEPT
iptables -A INPUT -p tcp --dport 5061 -j ACCEPT
iptables -A INPUT -p tcp --dport 8443 -j ACCEPT
iptables -A INPUT -p udp --dport 10000:20000 -j ACCEPT
Step 6: Start and Verify
# Check config syntax before starting
kamailio -c -f /etc/kamailio/kamailio.cfg
# Start Kamailio
systemctl start kamailio
systemctl enable kamailio
# Check status
systemctl status kamailio
# Verify it's listening
ss -ulnp | grep kamailio
ss -tlnp | grep kamailio
# Expected output:
# udp UNCONN 0 0 0.0.0.0:5060 0.0.0.0:* users:(("kamailio",pid=...))
# tcp LISTEN 0 0 0.0.0.0:5060 0.0.0.0:* users:(("kamailio",pid=...))
Test with a SIP OPTIONS ping:
# Install sipsak (SIP Swiss Army Knife)
apt install -y sipsak
# Send OPTIONS to Kamailio
sipsak -s sip:YOUR_SERVER_IP:5060
# Expected: SIP/2.0 200 OK
# Using kamctl
kamctl stats
kamctl fifo which
# Using kamcmd
kamcmd core.version
kamcmd core.uptime
kamcmd ul.dump
4. Configuration Language
Config File Structure
The Kamailio configuration file (/etc/kamailio/kamailio.cfg) has four distinct sections, and they must appear in this order:
┌─────────────────────────────┐
│ 1. Global Parameters │ ← Server-wide settings
├─────────────────────────────┤
│ 2. Module Loading │ ← loadmodule statements
├─────────────────────────────┤
│ 3. Module Parameters │ ← modparam() calls
├─────────────────────────────┤
│ 4. Route Blocks │ ← Routing logic
└─────────────────────────────┘
Global Parameters
#!KAMAILIO
/* ===== Global Parameters ===== */
/* Listen addresses — where Kamailio accepts SIP */
listen=udp:YOUR_SERVER_IP:5060
listen=tcp:YOUR_SERVER_IP:5060
/* Server aliases — domains this server responds to */
alias="YOUR_DOMAIN"
alias="YOUR_SERVER_IP"
/* Logging */
debug=2 # 0=emergency ... 4=debug (use 2 for production)
log_stderror=no # Log to syslog, not stderr
log_facility=LOG_LOCAL0 # Syslog facility
/* Process configuration */
children=8 # Number of SIP worker processes (per interface)
tcp_children=4 # Number of TCP worker processes
tcp_max_connections=4096 # Max simultaneous TCP connections
tcp_connection_lifetime=3600 # TCP keepalive (seconds)
/* Memory */
# Shared memory and private memory set in /etc/default/kamailio
/* DNS */
dns=no # Disable internal DNS (use system resolver)
rev_dns=no # Disable reverse DNS lookups
dns_try_ipv6=no # Disable IPv6 DNS queries
use_dns_cache=on # Cache DNS results
dns_cache_flags=1 # DNS cache settings
/* SIP-specific */
mhomed=0 # Multi-homed (0=disabled, 1=choose best source IP)
auto_aliases=no # Don't auto-detect aliases from DNS
server_signature=no # Hide Kamailio version in Server header
server_header="Server: SIP Proxy" # Custom Server header
user_agent_header="User-Agent: SIP Proxy" # Custom UA header
/* Reply to OPTIONS with 200 OK */
#!define WITH_AUTH
#!define WITH_MYSQL
#!define WITH_NAT
Preprocessor directives (#!define) let you enable/disable features at compile time, similar to #ifdef in C.
Module Loading
Modules are loaded with loadmodule. The order can matter — some modules depend on others:
/* ===== Module Loading ===== */
/* Core SIP modules */
loadmodule "kex.so" # Kamailio core extensions
loadmodule "corex.so" # Core extra functions
loadmodule "sl.so" # Stateless replies
loadmodule "tm.so" # Transaction management (stateful SIP)
loadmodule "tmx.so" # Transaction extensions
loadmodule "rr.so" # Record-Route
loadmodule "maxfwd.so" # Max-Forwards check
loadmodule "pv.so" # Pseudo-variables
loadmodule "textops.so" # Text operations on SIP messages
loadmodule "textopsx.so" # Extended text operations
loadmodule "siputils.so" # SIP utility functions
loadmodule "xlog.so" # Extended logging
loadmodule "sanity.so" # SIP message sanity checks
/* Database */
loadmodule "db_mysql.so" # MySQL/MariaDB connector
/* User management */
loadmodule "usrloc.so" # User location (registration database)
loadmodule "registrar.so" # REGISTER processing
/* Authentication */
loadmodule "auth.so" # Auth framework
loadmodule "auth_db.so" # Database-backed auth
/* NAT traversal */
loadmodule "nathelper.so" # NAT detection + signaling fix
loadmodule "rtpengine.so" # RTPEngine media relay
/* Dialog and accounting */
loadmodule "dialog.so" # Call dialog tracking
loadmodule "acc.so" # Accounting / CDR
/* Load balancing */
loadmodule "dispatcher.so" # Dispatcher (load balancer)
/* Security */
loadmodule "pike.so" # Flood detection
loadmodule "htable.so" # Hash tables
/* Management */
loadmodule "jsonrpcs.so" # JSON-RPC control interface
loadmodule "ctl.so" # Control interface (kamcmd)
loadmodule "cfg_rpc.so" # RPC for config
Module Parameters
Each module is configured with modparam("module_name", "parameter", value):
/* ===== Module Parameters ===== */
/* ----- tm ----- */
modparam("tm", "failure_reply_mode", 3)
modparam("tm", "fr_timer", 30000) # Final response timeout (ms)
modparam("tm", "fr_inv_timer", 120000) # INVITE final response timeout (ms)
modparam("tm", "restart_fr_on_each_reply", 1)
/* ----- rr ----- */
modparam("rr", "enable_full_lr", 1) # Full loose-routing
modparam("rr", "append_fromtag", 1) # Append From-tag to Record-Route
/* ----- usrloc ----- */
modparam("usrloc", "db_mode", 2) # 0=cache, 1=write-through, 2=write-back, 3=DB only
modparam("usrloc", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
modparam("usrloc", "timer_interval", 60) # Cleanup interval (seconds)
modparam("usrloc", "use_domain", 0) # 0=ignore domain, 1=use domain in AOR
/* ----- registrar ----- */
modparam("registrar", "method_filtering", 1)
modparam("registrar", "max_expires", 3600) # Max registration lifetime
modparam("registrar", "min_expires", 60) # Min registration lifetime
modparam("registrar", "default_expires", 600) # Default if not specified
modparam("registrar", "gruu_enabled", 0)
/* ----- auth_db ----- */
modparam("auth_db", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
modparam("auth_db", "calculate_ha1", 1) # Calculate HA1 from plaintext password
modparam("auth_db", "password_column", "password")
modparam("auth_db", "load_credentials", "$avp(s:caller_uuid)=uuid")
modparam("auth_db", "use_domain", 0)
/* ----- nathelper ----- */
modparam("nathelper", "natping_interval", 30) # NAT keepalive interval
modparam("nathelper", "ping_nated_only", 1) # Only ping NATed contacts
modparam("nathelper", "sipping", 1) # Use SIP OPTIONS as keepalive
/* ----- rtpengine ----- */
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")
/* ----- dialog ----- */
modparam("dialog", "db_mode", 1)
modparam("dialog", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
modparam("dialog", "dlg_flag", 4)
modparam("dialog", "profiles_with_value", "caller")
modparam("dialog", "profiles_no_value", "active_calls")
/* ----- dispatcher ----- */
modparam("dispatcher", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
modparam("dispatcher", "ds_ping_interval", 10) # Health check interval (seconds)
modparam("dispatcher", "ds_ping_method", "OPTIONS")
modparam("dispatcher", "ds_probing_threshold", 3) # Failures before marking down
modparam("dispatcher", "ds_inactive_threshold", 3) # Successes before marking up
modparam("dispatcher", "ds_probing_mode", 1) # Probe all destinations
/* ----- pike ----- */
modparam("pike", "sampling_time_unit", 2) # Sampling window (seconds)
modparam("pike", "reqs_density_per_unit", 30) # Max requests per window
modparam("pike", "remove_latency", 4) # Ban duration (sampling units)
/* ----- jsonrpcs ----- */
modparam("jsonrpcs", "pretty_format", 1)
modparam("jsonrpcs", "transport", 1) # 0=FIFO, 1=datagram, 2=both
Route Blocks
Route blocks contain the actual SIP processing logic:
/* ===== Route Blocks ===== */
/* Main request routing */
request_route {
# This is where every incoming SIP request is processed
xlog("L_INFO", "Received $rm from $si:$sp — R-URI: $ru\n");
# ... routing logic ...
}
/* Named sub-routes (called with route(NAME)) */
route[AUTH] {
# Authentication logic
}
route[REGISTRAR] {
# Registration handling
}
route[RELAY] {
# Forward the request
}
/* Reply processing */
reply_route {
# Process all SIP responses
}
onreply_route[MANAGE_REPLY] {
# Per-transaction reply handling
}
/* Failure handling */
failure_route[MANAGE_FAILURE] {
# Called when a transaction receives a negative final response
}
/* Branch processing */
branch_route[MANAGE_BRANCH] {
# Called for each branch before forwarding
}
Pseudo-Variables
Pseudo-variables provide access to SIP message fields and internal state. They are the workhorses of Kamailio configuration:
| Variable | Description | Example Value |
|---|---|---|
$si |
Source IP address | 192.168.1.100 |
$sp |
Source port | 5060 |
$ru |
Request URI | sip:[email protected] |
$rU |
Request URI username | 1001 |
$rd |
Request URI domain | example.com |
$fu |
From URI | sip:[email protected] |
$fU |
From URI username | 1000 |
$fd |
From URI domain | example.com |
$tu |
To URI | sip:[email protected] |
$tU |
To URI username | 1001 |
$rm |
Request method | INVITE |
$ci |
Call-ID header | abc123@host |
$cs |
CSeq number | 1 |
$ua |
User-Agent header | Zoiper/5.6 |
$Ri |
Received IP (local) | 10.0.0.1 |
$Rp |
Received port (local) | 5060 |
$proto |
Transport protocol | UDP |
$hdr(Name) |
Any SIP header by name | $hdr(Contact) |
$var(x) |
Script variable (per-message) | $var(result) |
$avp(x) |
AVP variable (per-transaction) | $avp(auth_result) |
$xavp(x) |
Extended AVP (structured data) | $xavp(caller=>name) |
$sht(table=>key) |
Hash table value | $sht(blacklist=>$si) |
$dlg(...) |
Dialog attributes | $dlg(count) |
$T_reply_code |
Last reply code in failure_route | 486 |
$retcode / $rc |
Return code of last function | 1 |
Conditional Logic
# If/else
if ($rm == "INVITE") {
xlog("L_INFO", "Processing INVITE\n");
} else if ($rm == "REGISTER") {
xlog("L_INFO", "Processing REGISTER\n");
} else {
xlog("L_INFO", "Processing $rm\n");
}
# Regex matching
if ($rU =~ "^1[0-9]{3}$") {
# Request URI username matches 4-digit extension starting with 1
xlog("L_INFO", "Internal extension: $rU\n");
}
# String matching
if ($ua =~ "friendly-scanner") {
# Block known SIP scanners
sl_send_reply(403, "Forbidden");
exit;
}
# IP matching
if ($si == "10.0.0.1") {
# Request from trusted IP
}
# Check if header exists
if (is_present_hf("X-Custom-Header")) {
xlog("L_INFO", "Custom header value: $hdr(X-Custom-Header)\n");
}
# Logical operators
if ($rm == "INVITE" && !has_totag()) {
# New INVITE (not a re-INVITE)
}
Core Functions
# Forward request statelessly (fast, no failure handling)
forward();
# Forward request statefully (enables failure_route)
t_relay();
# Send a stateless reply
sl_send_reply(200, "OK");
sl_send_reply(403, "Forbidden");
sl_send_reply(503, "Service Unavailable");
# Send a stateful reply
t_reply(200, "OK");
# Rewrite the Request-URI
$ru = "sip:[email protected]";
# Or using rewriteuri:
rewriteuri("sip:[email protected]");
# Rewrite just the host portion
$rd = "10.0.0.5";
# Rewrite just the username
$rU = "2001";
# Add a SIP header
append_hf("X-Forwarded-For: $si\r\n");
# Remove a SIP header
remove_hf("User-Agent");
# Replace a SIP header
subst_hf("User-Agent", "/.*$/SIP Proxy/", "a");
# Stop processing
exit;
# Drop the message silently
drop();
Complete Minimal Configuration
Here is a complete, working minimal kamailio.cfg that functions as a basic SIP proxy:
#!KAMAILIO
/* ===== Global Parameters ===== */
listen=udp:YOUR_SERVER_IP:5060
listen=tcp:YOUR_SERVER_IP:5060
alias="YOUR_DOMAIN"
debug=2
log_stderror=no
log_facility=LOG_LOCAL0
children=4
auto_aliases=no
server_signature=no
/* ===== Module Loading ===== */
loadmodule "kex.so"
loadmodule "sl.so"
loadmodule "tm.so"
loadmodule "rr.so"
loadmodule "maxfwd.so"
loadmodule "pv.so"
loadmodule "textops.so"
loadmodule "siputils.so"
loadmodule "xlog.so"
loadmodule "sanity.so"
/* ===== Module Parameters ===== */
modparam("tm", "fr_timer", 30000)
modparam("tm", "fr_inv_timer", 120000)
modparam("rr", "enable_full_lr", 1)
modparam("rr", "append_fromtag", 1)
/* ===== Routing Logic ===== */
request_route {
/* --- 1. Sanity checks --- */
if (!mf_process_maxfwd_header(10)) {
sl_send_reply(483, "Too Many Hops");
exit;
}
if (!sanity_check("17895", "7")) {
xlog("L_WARN", "Malformed SIP from $si:$sp\n");
exit;
}
/* --- 2. Record-Route for in-dialog requests --- */
if (is_method("INVITE|SUBSCRIBE")) {
record_route();
}
/* --- 3. Handle sequential requests (in-dialog) --- */
if (has_totag()) {
if (loose_route()) {
if (is_method("BYE")) {
xlog("L_INFO", "BYE from $fU to $tU\n");
}
t_relay();
exit;
} else {
/* Sequential request but no Route header — reject */
sl_send_reply(404, "Not Found");
exit;
}
}
/* --- 4. Handle CANCEL --- */
if (is_method("CANCEL")) {
if (t_check_trans()) {
t_relay();
}
exit;
}
/* --- 5. Absorb retransmissions --- */
t_check_trans();
/* --- 6. Handle OPTIONS (keepalive) --- */
if (is_method("OPTIONS") && ($rU == $null || $rU == "")) {
sl_send_reply(200, "OK");
exit;
}
/* --- 7. Route the request --- */
xlog("L_INFO", "$rm from $fU@$si to $rU — forwarding\n");
t_relay();
exit;
}
5. SIP Routing
REGISTER Handling
Registration is how SIP phones tell Kamailio where they can be reached. The usrloc module stores contact-to-AOR (Address of Record) mappings, and the registrar module processes REGISTER requests.
/* Add to module loading section */
loadmodule "usrloc.so"
loadmodule "registrar.so"
/* Module parameters */
modparam("usrloc", "db_mode", 2)
modparam("usrloc", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
modparam("registrar", "max_expires", 3600)
modparam("registrar", "default_expires", 600)
Registration route:
route[REGISTRAR] {
/* Only handle REGISTER */
if (!is_method("REGISTER")) return;
/* Check for too many contacts */
if (is_present_hf("Contact")) {
if ($hdr(Contact) =~ "\*") {
/* Unregister all — Contact: * */
}
}
/* Save the registration */
if (!save("location")) {
sl_send_reply(500, "Server Error - Registration Failed");
exit;
}
/* save() automatically sends 200 OK with Contact + Expires */
exit;
}
In request_route, add before the forwarding logic:
/* Handle REGISTER */
if (is_method("REGISTER")) {
route(REGISTRAR);
exit;
}
Stateless vs Stateful Forwarding
Stateless (sl module): Kamailio forwards the request and immediately forgets about it. Fast, but no failure handling.
/* Stateless forward — fire and forget */
forward();
Stateful (tm module): Kamailio tracks the transaction, enabling failure routes, timeout handling, and proper retransmission management. This is what you want for INVITE processing.
/* Stateful forward — enables failure_route and onreply_route */
t_on_failure("MANAGE_FAILURE");
t_on_reply("MANAGE_REPLY");
t_on_branch("MANAGE_BRANCH");
t_relay();
Rule of thumb: Always use t_relay() for INVITE transactions. Use forward() only for simple pass-through scenarios where you don't need failure handling.
Record-Route
When Kamailio proxies an INVITE, it must add a Record-Route header to stay in the signaling path for subsequent requests (BYE, re-INVITE, etc.). Without Record-Route, the BYE goes directly between endpoints — Kamailio never sees it and cannot track call state or generate CDRs.
/* Add Record-Route for dialog-initiating requests */
if (is_method("INVITE|SUBSCRIBE")) {
record_route();
}
What this does:
INVITE from Phone A → Kamailio → Phone B
With record_route(), Kamailio adds:
Record-Route: <sip:YOUR_SERVER_IP;lr>
Phone B's 200 OK includes this in the Route set.
Now BYE from Phone A → Kamailio → Phone B
(Kamailio stays in the signaling path)
Without record_route():
BYE from Phone A → Phone B (directly — Kamailio is bypassed)
Sequential Request Routing (In-Dialog)
After the initial INVITE/200 OK/ACK, subsequent requests (BYE, re-INVITE, UPDATE) are "in-dialog" — they have a To tag. These must be routed using loose_route():
/* Handle sequential (in-dialog) requests */
if (has_totag()) {
/* Validate Route headers */
if (loose_route()) {
/* In-dialog request with valid Route — forward it */
if (is_method("BYE")) {
xlog("L_INFO", "Call ended: $fU → $tU (Call-ID: $ci)\n");
/* Optionally: track in CDR */
}
if (is_method("ACK")) {
/* ACK for 2xx — route it */
}
t_relay();
exit;
} else {
/* In-dialog request but no valid Route header */
if (is_method("ACK")) {
/* ACK without Route — might be for a local 4xx reply */
if (t_check_trans()) {
exit;
}
exit;
}
/* All other in-dialog without Route — error */
sl_send_reply(404, "Not Found");
exit;
}
}
Routing to External Destinations (SIP Trunks)
To route calls to an external SIP trunk or carrier:
route[TO_TRUNK] {
/* Route to SIP trunk */
$ru = "sip:" + $rU + "@TRUNK_IP:5060";
/* Add custom headers if the trunk requires them */
append_hf("P-Asserted-Identity: <sip:$fU@YOUR_DOMAIN>\r\n");
/* Remove internal headers */
remove_hf("X-Internal-Route");
/* Forward statefully with failure handling */
t_on_failure("TRUNK_FAILURE");
t_relay();
exit;
}
failure_route[TRUNK_FAILURE] {
/* If the primary trunk fails, try backup */
if (t_is_canceled()) exit;
xlog("L_WARN", "Trunk failed with $T_reply_code for $rU\n");
if ($T_reply_code >= 500) {
/* 5xx error — try backup trunk */
$ru = "sip:" + $rU + "@BACKUP_TRUNK_IP:5060";
t_relay();
exit;
}
}
Complete Routing Configuration
Here is a complete SIP proxy configuration with REGISTER handling, INVITE routing, and in-dialog request management:
request_route {
/* ---- Sanity ---- */
if (!mf_process_maxfwd_header(10)) {
sl_send_reply(483, "Too Many Hops");
exit;
}
if (!sanity_check("17895", "7")) {
xlog("L_WARN", "Malformed SIP from $si:$sp\n");
exit;
}
/* ---- Anti-flood ---- */
if (src_ip != myself) {
if (!pike_check_req()) {
xlog("L_ALERT", "PIKE BLOCK: $si flooding\n");
exit;
}
}
/* ---- Record-Route ---- */
if (is_method("INVITE|SUBSCRIBE")) {
record_route();
}
/* ---- In-dialog requests ---- */
if (has_totag()) {
if (loose_route()) {
if (is_method("BYE")) {
xlog("L_INFO", "BYE: $fU → $tU ($ci)\n");
}
route(RELAY);
exit;
}
if (is_method("ACK") && t_check_trans()) {
exit;
}
sl_send_reply(404, "Not Found");
exit;
}
/* ---- CANCEL ---- */
if (is_method("CANCEL")) {
if (t_check_trans()) t_relay();
exit;
}
/* ---- Retransmission absorption ---- */
t_check_trans();
/* ---- REGISTER ---- */
if (is_method("REGISTER")) {
route(REGISTRAR);
exit;
}
/* ---- OPTIONS keepalive ---- */
if (is_method("OPTIONS") && ($rU == $null || $rU == "")) {
sl_send_reply(200, "OK");
exit;
}
/* ---- INVITE routing ---- */
if (is_method("INVITE")) {
/* Look up the destination in the location table */
if (!lookup("location")) {
/* Not found in registrations — try external routing */
xlog("L_INFO", "User $rU not registered, sending 404\n");
sl_send_reply(404, "User Not Found");
exit;
}
/* Found — relay to registered contact */
route(RELAY);
exit;
}
/* ---- Other requests ---- */
if (is_method("MESSAGE|NOTIFY|INFO|UPDATE|PRACK|REFER")) {
if (lookup("location")) {
route(RELAY);
exit;
}
sl_send_reply(404, "User Not Found");
exit;
}
/* Default — reject */
sl_send_reply(405, "Method Not Allowed");
exit;
}
route[RELAY] {
t_on_failure("MANAGE_FAILURE");
t_on_reply("MANAGE_REPLY");
if (!t_relay()) {
sl_send_reply(500, "Server Error");
}
exit;
}
route[REGISTRAR] {
if (!save("location")) {
sl_send_reply(500, "Registration Failed");
}
exit;
}
onreply_route[MANAGE_REPLY] {
xlog("L_INFO", "Reply $T_reply_code for $rm ($ci)\n");
}
failure_route[MANAGE_FAILURE] {
if (t_is_canceled()) exit;
xlog("L_WARN", "Failure $T_reply_code for $rU ($ci)\n");
}
6. User Authentication
Overview
Without authentication, anyone can register phones and make calls through your proxy. Kamailio uses SIP Digest Authentication (RFC 2617) — the same challenge-response mechanism used by HTTP Basic Auth but adapted for SIP.
Authentication flow:
Phone Kamailio Database
│ │ │
│─── REGISTER (no auth) ───────►│ │
│ │ │
│◄── 401 Unauthorized ──────────│ (with WWW-Authenticate │
│ (nonce + realm) │ challenge) │
│ │ │
│─── REGISTER (with auth) ─────►│ │
│ (username + response hash) │── lookup password ────────►│
│ │◄── password ───────────────│
│ │ │
│ │ verify hash(password + │
│ │ nonce + method + uri) │
│ │ │
│◄── 200 OK ────────────────────│ │
Module Setup
/* Module loading */
loadmodule "auth.so"
loadmodule "auth_db.so"
/* Module parameters */
modparam("auth_db", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
modparam("auth_db", "calculate_ha1", 1)
modparam("auth_db", "password_column", "password")
modparam("auth_db", "use_domain", 0)
Parameters explained:
calculate_ha1=1: Passwords are stored in plaintext; Kamailio calculates the HA1 hash. Set to0if you store pre-computed HA1 hashes (more secure).password_column: Which column in thesubscribertable contains the password.use_domain=0: Ignore the domain part — authenticate by username only. Set to1for multi-domain setups.
Managing Users
# Add a user
kamctl add 1001 MySecurePass123
# This inserts into the 'subscriber' table
# List users
kamctl showdb subscriber
# Change password
kamctl passwd 1001 NewPassword456
# Delete a user
kamctl rm 1001
# Add with domain (if use_domain=1)
kamctl add [email protected] MySecurePass123
Alternatively, insert directly into the database:
INSERT INTO subscriber (username, domain, password, ha1, ha1b)
VALUES (
'1001',
'YOUR_DOMAIN',
'MySecurePass123',
MD5('1001:YOUR_DOMAIN:MySecurePass123'),
MD5('1001@YOUR_DOMAIN:YOUR_DOMAIN:MySecurePass123')
);
Authentication Route
route[AUTH] {
/* Skip authentication for requests from trusted IPs */
if (src_ip == myself) return;
/* REGISTER requires WWW-Authenticate (401) */
if (is_method("REGISTER")) {
if (!www_authorize("YOUR_DOMAIN", "subscriber")) {
www_challenge("YOUR_DOMAIN", 1);
exit;
}
/* Verify the From URI matches the authenticated user */
if ($au != $fU) {
xlog("L_WARN", "Auth mismatch: auth=$au from=$fU ($si)\n");
sl_send_reply(403, "Forbidden — User Mismatch");
exit;
}
return; /* Auth passed */
}
/* All other requests use Proxy-Authenticate (407) */
if (!proxy_authorize("YOUR_DOMAIN", "subscriber")) {
proxy_challenge("YOUR_DOMAIN", 1);
exit;
}
/* Verify authenticated user matches From */
if ($au != $fU) {
xlog("L_WARN", "Auth mismatch: auth=$au from=$fU ($si)\n");
sl_send_reply(403, "Forbidden — User Mismatch");
exit;
}
/* Remove the Proxy-Authorization header before forwarding */
consume_credentials();
return;
}
Key functions:
| Function | Purpose |
|---|---|
www_authorize(realm, table) |
Check WWW credentials (for REGISTER) |
www_challenge(realm, qop) |
Send 401 with WWW-Authenticate header |
proxy_authorize(realm, table) |
Check Proxy credentials (for INVITE, etc.) |
proxy_challenge(realm, qop) |
Send 407 with Proxy-Authenticate header |
consume_credentials() |
Remove Proxy-Authorization header (don't leak to next hop) |
IP-Based Authentication for Trunks
SIP trunks typically authenticate by IP address rather than username/password. Use the permissions module:
loadmodule "permissions.so"
modparam("permissions", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
Add trusted IPs to the address table:
# Add a trusted trunk IP (group 1 = trunks)
kamctl address add 1 TRUNK_IP 32 5060 "Primary SIP Trunk"
# Reload the address table
kamctl address reload
# Show trusted addresses
kamctl address show
Or directly in the database:
INSERT INTO address (grp, ip_addr, mask, port, tag)
VALUES (1, 'TRUNK_IP', 32, 5060, 'Primary SIP Trunk');
INSERT INTO address (grp, ip_addr, mask, port, tag)
VALUES (1, 'BACKUP_TRUNK_IP', 32, 5060, 'Backup SIP Trunk');
Use in routing:
route[AUTH] {
/* Check if source IP is a trusted trunk (group 1) */
if (allow_source_address(1)) {
xlog("L_INFO", "Trusted trunk: $si ($hdr(P-Asserted-Identity))\n");
return; /* Skip digest auth */
}
/* Not a trunk — require digest authentication */
if (is_method("REGISTER")) {
if (!www_authorize("YOUR_DOMAIN", "subscriber")) {
www_challenge("YOUR_DOMAIN", 1);
exit;
}
} else {
if (!proxy_authorize("YOUR_DOMAIN", "subscriber")) {
proxy_challenge("YOUR_DOMAIN", 1);
exit;
}
consume_credentials();
}
/* Verify From matches authenticated user */
if ($au != $null && $au != $fU) {
sl_send_reply(403, "Forbidden");
exit;
}
return;
}
Complete Authenticated Proxy Configuration
request_route {
/* Sanity */
if (!mf_process_maxfwd_header(10)) {
sl_send_reply(483, "Too Many Hops");
exit;
}
if (!sanity_check("17895", "7")) {
exit;
}
/* Record-Route */
if (is_method("INVITE|SUBSCRIBE")) {
record_route();
}
/* In-dialog requests */
if (has_totag()) {
if (loose_route()) {
route(RELAY);
exit;
}
if (is_method("ACK") && t_check_trans()) exit;
sl_send_reply(404, "Not Found");
exit;
}
/* CANCEL */
if (is_method("CANCEL")) {
if (t_check_trans()) t_relay();
exit;
}
t_check_trans();
/* ---- AUTHENTICATE ---- */
route(AUTH);
/* REGISTER */
if (is_method("REGISTER")) {
if (!save("location")) {
sl_send_reply(500, "Registration Failed");
}
exit;
}
/* OPTIONS */
if (is_method("OPTIONS") && ($rU == $null || $rU == "")) {
sl_send_reply(200, "OK");
exit;
}
/* Route to registered user or reject */
if (!lookup("location")) {
sl_send_reply(404, "User Not Found");
exit;
}
route(RELAY);
exit;
}
route[AUTH] {
/* Trusted trunk IPs — no auth needed */
if (allow_source_address(1)) return;
/* REGISTER → 401 challenge */
if (is_method("REGISTER")) {
if (!www_authorize("YOUR_DOMAIN", "subscriber")) {
www_challenge("YOUR_DOMAIN", 1);
exit;
}
if ($au != $fU) {
sl_send_reply(403, "Forbidden");
exit;
}
return;
}
/* All other methods → 407 challenge */
if (is_method("INVITE|MESSAGE|SUBSCRIBE|NOTIFY|REFER")) {
if (!proxy_authorize("YOUR_DOMAIN", "subscriber")) {
proxy_challenge("YOUR_DOMAIN", 1);
exit;
}
if ($au != $fU) {
sl_send_reply(403, "Forbidden");
exit;
}
consume_credentials();
}
return;
}
route[RELAY] {
t_on_failure("MANAGE_FAILURE");
if (!t_relay()) {
sl_send_reply(500, "Server Error");
}
exit;
}
failure_route[MANAGE_FAILURE] {
if (t_is_canceled()) exit;
xlog("L_WARN", "Call to $rU failed: $T_reply_code\n");
}
7. NAT Traversal
The NAT Problem
NAT (Network Address Translation) is the biggest source of SIP headaches. When a phone behind NAT sends a SIP message, the headers contain the phone's private IP (e.g., 192.168.1.100), but the actual packets arrive from the router's public IP (e.g., 203.0.113.50).
This causes two problems:
- Signaling: Kamailio tries to send SIP replies to 192.168.1.100 — which is unreachable from the internet
- Media: The SDP body says "send RTP to 192.168.1.100:10000" — the other party cannot reach this address
┌──────────────┐ ┌──────────┐ ┌──────────────┐
│ Phone A │ │ NAT │ │ Kamailio │
│ 192.168.1.100│────────►│ Router │────────►│ Public IP │
│ SIP Contact: │ │203.0.113 │ │ │
│ 192.168.1.100│ │ .50 │ │ │
│ SDP c= line: │ │ │ │ │
│ 192.168.1.100│ │ │ │ │
└──────────────┘ └──────────┘ └──────────────┘
Problem: SIP headers say 192.168.1.100
Packets actually came from 203.0.113.50
Kamailio can't reply to 192.168.1.100!
NAT Detection with nathelper
The nathelper module provides functions to detect and fix NAT:
/* Module loading */
loadmodule "nathelper.so"
/* Module parameters */
modparam("nathelper", "natping_interval", 30) # Send keepalive every 30s
modparam("nathelper", "ping_nated_only", 1) # Only ping NATed clients
modparam("nathelper", "sipping", 1) # Use SIP OPTIONS as keepalive
modparam("nathelper", "received_avp", "$avp(RECEIVED)")
NAT detection function — nat_uac_test(flags):
| Flag | Test |
|---|---|
| 1 | Contact header contains RFC1918 private IP |
| 2 | Source IP differs from Via "sent-by" address |
| 4 | Contact header contains RFC1918 but is different from source IP |
| 8 | SDP body contains RFC1918 private IP |
| 16 | Source port differs from Via "sent-by" port |
| 32 | Contact header port differs from source port |
Common usage: nat_uac_test(63) — tests all flags combined (1+2+4+8+16+32 = 63).
Fixing NAT in SIP Signaling
route[NATDETECT] {
/* Detect NAT */
if (nat_uac_test(63)) {
/* Mark this transaction as NATed */
setflag(5); /* Flag 5 = NATed */
/* Fix the Contact header to use the real source IP:port */
fix_nated_contact();
/* Force adding rport to Via (so replies go to real source port) */
force_rport();
xlog("L_INFO", "NAT detected for $fU from $si:$sp\n");
}
return;
}
route[NATMANAGE] {
/* Fix Contact in replies too */
if (is_reply()) {
if (nat_uac_test(63)) {
fix_nated_contact();
}
}
return;
}
For REGISTER requests, store the real source IP so Kamailio can reach the phone later:
route[REGISTRAR] {
/* NAT fix for REGISTER */
if (nat_uac_test(63)) {
setflag(5);
fix_nated_register(); /* Store real IP:port in location DB */
force_rport();
}
if (!save("location")) {
sl_send_reply(500, "Registration Failed");
}
exit;
}
RTPEngine — Media NAT Fix
Fixing SIP signaling is only half the battle. The RTP media streams also need NAT handling. RTPEngine is a high-performance media relay that sits alongside Kamailio.
Installing RTPEngine
# Debian 12 / Ubuntu 24.04
apt install -y rtpengine
# Or from the official repository for the latest version:
# Add the RTPEngine repo (check https://dfx.at/rtpengine/ for current URLs)
apt install -y \
ngcp-rtpengine-daemon \
ngcp-rtpengine-iptables \
ngcp-rtpengine-recording-daemon # Optional: for call recording
Configuring RTPEngine
cat > /etc/rtpengine/rtpengine.conf << 'EOF'
[rtpengine]
# Control socket — Kamailio connects here
listen-ng=127.0.0.1:2223
# Network interfaces
interface=public/YOUR_SERVER_IP
# Port range for RTP
port-min=10000
port-max=20000
# Timeouts
timeout=60
silent-timeout=600
final-timeout=7200
# Logging
log-level=5
log-facility=daemon
# Recording (optional)
# recording-dir=/var/spool/rtpengine
# recording-method=pcap
# recording-format=eth
# TOS/DSCP marking for QoS
tos=184
# Pidfile
pidfile=/run/rtpengine/rtpengine.pid
# Number of worker threads
num-threads=4
EOF
# Enable and start RTPEngine
systemctl enable --now rtpengine
systemctl status rtpengine
# Verify it's listening
ss -ulnp | grep rtpengine
Kamailio RTPEngine Integration
/* Module loading */
loadmodule "rtpengine.so"
/* Module parameters */
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")
Usage in routing logic:
route[NATMANAGE] {
/* Determine RTPEngine flags based on NAT status and direction */
if (is_request()) {
if (has_body("application/sdp")) {
if (isflagset(5)) {
/* Caller is NATed — replace private IPs, force relay */
rtpengine_manage("replace-origin replace-session-connection ICE=remove");
} else {
/* Caller is not NATed — just relay for consistency */
rtpengine_manage("ICE=remove");
}
}
}
if (is_reply()) {
if (has_body("application/sdp")) {
if (isflagset(5)) {
rtpengine_manage("replace-origin replace-session-connection ICE=remove");
} else {
rtpengine_manage("ICE=remove");
}
}
}
return;
}
RTPEngine flags explained:
| Flag | Purpose |
|---|---|
replace-origin |
Replace the o= line IP in SDP |
replace-session-connection |
Replace the c= line IP in SDP |
ICE=remove |
Strip ICE candidates (for non-WebRTC endpoints) |
ICE=force |
Force ICE (for WebRTC endpoints) |
RTP/SAVPF |
Use SRTP with RTCP feedback (WebRTC) |
RTP/AVP |
Use plain RTP (traditional SIP) |
DTLS=passive |
DTLS-SRTP passive mode (WebRTC) |
rtcp-mux-demux |
Handle RTCP multiplexed on same port |
media-address=IP |
Force specific media address |
STUN/TURN Overview
While RTPEngine handles media relay server-side, clients behind NAT may also need STUN/TURN:
- STUN: Helps clients discover their public IP. Lightweight but fails with symmetric NAT.
- TURN: Full relay server. Works with all NAT types but adds latency and bandwidth cost.
- ICE: Framework that uses both STUN and TURN to find the best media path.
For most Kamailio deployments, RTPEngine eliminates the need for client-side STUN/TURN — the proxy handles everything. STUN/TURN becomes important primarily for WebRTC deployments.
If you need a TURN server, coturn is the standard choice:
apt install -y coturn
# Configuration in /etc/turnserver.conf
Complete NAT-Aware Configuration
#!KAMAILIO
/* ===== Global Parameters ===== */
listen=udp:YOUR_SERVER_IP:5060 advertise YOUR_SERVER_IP:5060
listen=tcp:YOUR_SERVER_IP:5060 advertise YOUR_SERVER_IP:5060
debug=2
log_stderror=no
log_facility=LOG_LOCAL0
children=8
auto_aliases=no
server_signature=no
force_rport=yes /* Always add rport */
/* ===== Module Loading ===== */
loadmodule "kex.so"
loadmodule "sl.so"
loadmodule "tm.so"
loadmodule "tmx.so"
loadmodule "rr.so"
loadmodule "maxfwd.so"
loadmodule "pv.so"
loadmodule "textops.so"
loadmodule "siputils.so"
loadmodule "xlog.so"
loadmodule "sanity.so"
loadmodule "db_mysql.so"
loadmodule "usrloc.so"
loadmodule "registrar.so"
loadmodule "auth.so"
loadmodule "auth_db.so"
loadmodule "nathelper.so"
loadmodule "rtpengine.so"
/* ===== Module Parameters ===== */
modparam("tm", "fr_timer", 30000)
modparam("tm", "fr_inv_timer", 120000)
modparam("rr", "enable_full_lr", 1)
modparam("rr", "append_fromtag", 1)
modparam("usrloc", "db_mode", 2)
modparam("usrloc", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
modparam("registrar", "max_expires", 3600)
modparam("registrar", "default_expires", 600)
modparam("auth_db", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
modparam("auth_db", "calculate_ha1", 1)
modparam("auth_db", "password_column", "password")
modparam("auth_db", "use_domain", 0)
modparam("nathelper", "natping_interval", 30)
modparam("nathelper", "ping_nated_only", 1)
modparam("nathelper", "sipping", 1)
modparam("nathelper", "received_avp", "$avp(RECEIVED)")
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")
/* ===== Routing Logic ===== */
request_route {
/* Sanity checks */
if (!mf_process_maxfwd_header(10)) {
sl_send_reply(483, "Too Many Hops");
exit;
}
if (!sanity_check("17895", "7")) {
exit;
}
/* NAT detection */
route(NATDETECT);
/* Record-Route */
if (is_method("INVITE|SUBSCRIBE")) {
record_route();
}
/* In-dialog requests */
if (has_totag()) {
if (loose_route()) {
if (is_method("BYE")) {
xlog("L_INFO", "BYE: $fU → $tU\n");
/* Release RTPEngine session */
rtpengine_delete();
}
if (is_method("INVITE|UPDATE|ACK")) {
route(NATMANAGE);
}
route(RELAY);
exit;
}
if (is_method("ACK") && t_check_trans()) exit;
sl_send_reply(404, "Not Found");
exit;
}
/* CANCEL */
if (is_method("CANCEL")) {
if (t_check_trans()) t_relay();
exit;
}
t_check_trans();
/* Authenticate */
route(AUTH);
/* REGISTER */
if (is_method("REGISTER")) {
route(REGISTRAR);
exit;
}
/* OPTIONS */
if (is_method("OPTIONS") && ($rU == $null || $rU == "")) {
sl_send_reply(200, "OK");
exit;
}
/* INVITE — lookup and route */
if (!lookup("location")) {
sl_send_reply(404, "User Not Found");
exit;
}
/* NAT manage for initial INVITE */
route(NATMANAGE);
route(RELAY);
exit;
}
route[RELAY] {
t_on_reply("MANAGE_REPLY");
t_on_failure("MANAGE_FAILURE");
if (!t_relay()) {
sl_send_reply(500, "Server Error");
}
exit;
}
route[AUTH] {
if (is_method("REGISTER")) {
if (!www_authorize("YOUR_DOMAIN", "subscriber")) {
www_challenge("YOUR_DOMAIN", 1);
exit;
}
if ($au != $fU) {
sl_send_reply(403, "Forbidden");
exit;
}
return;
}
if (is_method("INVITE|MESSAGE|SUBSCRIBE")) {
if (!proxy_authorize("YOUR_DOMAIN", "subscriber")) {
proxy_challenge("YOUR_DOMAIN", 1);
exit;
}
if ($au != $fU) {
sl_send_reply(403, "Forbidden");
exit;
}
consume_credentials();
}
return;
}
route[REGISTRAR] {
if (nat_uac_test(63)) {
setflag(5);
fix_nated_register();
force_rport();
}
if (!save("location")) {
sl_send_reply(500, "Registration Failed");
}
exit;
}
route[NATDETECT] {
if (nat_uac_test(63)) {
setflag(5);
fix_nated_contact();
force_rport();
}
return;
}
route[NATMANAGE] {
if (is_request()) {
if (has_body("application/sdp")) {
if (isflagset(5)) {
rtpengine_manage("replace-origin replace-session-connection ICE=remove");
} else {
rtpengine_manage("ICE=remove");
}
}
}
if (is_reply()) {
if (has_body("application/sdp")) {
rtpengine_manage("replace-origin replace-session-connection ICE=remove");
}
}
return;
}
onreply_route[MANAGE_REPLY] {
if (status =~ "[12][0-9][0-9]") {
route(NATMANAGE);
}
}
failure_route[MANAGE_FAILURE] {
if (t_is_canceled()) exit;
xlog("L_WARN", "Call to $rU failed: $T_reply_code\n");
if ($T_reply_code == 486 || $T_reply_code == 408) {
xlog("L_INFO", "User $rU busy or timeout\n");
}
}
This configuration handles:
- NAT detection on all incoming requests
- SIP signaling fix-up (Contact, Via, rport)
- RTPEngine integration for media relay
- NAT keepalive pings to registered clients
- Proper RTPEngine session cleanup on BYE
- In-dialog NAT management for re-INVITEs and replies
8. TLS & Security
TLS Module Configuration
TLS encrypts SIP signaling, preventing eavesdropping and tampering. SIP over TLS runs on port 5061 by default.
/* Module loading */
loadmodule "tls.so"
/* Global: add TLS listen address */
listen=tls:YOUR_SERVER_IP:5061
/* TLS module parameters */
modparam("tls", "config", "/etc/kamailio/tls.cfg")
modparam("tls", "tls_force_run", 1) /* Start even if cert is missing */
/* Alternative: inline TLS config (instead of separate tls.cfg) */
modparam("tls", "private_key", "/etc/kamailio/certs/privkey.pem")
modparam("tls", "certificate", "/etc/kamailio/certs/fullchain.pem")
modparam("tls", "ca_list", "/etc/kamailio/certs/chain.pem")
modparam("tls", "tls_method", "TLSv1.2+") /* Minimum TLS 1.2 */
modparam("tls", "verify_certificate", 0) /* 0=don't require client cert */
modparam("tls", "require_certificate", 0) /* 0=allow non-TLS clients */
modparam("tls", "connection_timeout", 60)
Separate TLS Config File
For more granular control, use a dedicated TLS config file (/etc/kamailio/tls.cfg):
# /etc/kamailio/tls.cfg
[server:default]
method = TLSv1.2+
verify_certificate = no
require_certificate = no
private_key = /etc/kamailio/certs/privkey.pem
certificate = /etc/kamailio/certs/fullchain.pem
ca_list = /etc/kamailio/certs/chain.pem
# Specific config for a trusted peer
[server:TRUNK_IP:5061]
method = TLSv1.2+
verify_certificate = yes
require_certificate = yes
private_key = /etc/kamailio/certs/privkey.pem
certificate = /etc/kamailio/certs/fullchain.pem
ca_list = /etc/kamailio/certs/trunk-ca.pem
[client:default]
method = TLSv1.2+
verify_certificate = yes
private_key = /etc/kamailio/certs/privkey.pem
certificate = /etc/kamailio/certs/fullchain.pem
ca_list = /etc/kamailio/certs/chain.pem
Certificate Generation
Option A: Let's Encrypt (Production)
# Install certbot
apt install -y certbot
# Obtain certificate (standalone mode — stop Kamailio briefly)
systemctl stop kamailio
certbot certonly --standalone -d YOUR_DOMAIN
# Certificate files are at:
# /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem
# /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem
# /etc/letsencrypt/live/YOUR_DOMAIN/chain.pem
# Create symlinks for Kamailio
mkdir -p /etc/kamailio/certs
ln -sf /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem /etc/kamailio/certs/fullchain.pem
ln -sf /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem /etc/kamailio/certs/privkey.pem
ln -sf /etc/letsencrypt/live/YOUR_DOMAIN/chain.pem /etc/kamailio/certs/chain.pem
# Set permissions
chown -R kamailio:kamailio /etc/kamailio/certs/
# Auto-renewal with Kamailio reload
cat > /etc/letsencrypt/renewal-hooks/post/kamailio.sh << 'HOOK'
#!/bin/bash
systemctl reload kamailio
HOOK
chmod +x /etc/letsencrypt/renewal-hooks/post/kamailio.sh
systemctl start kamailio
Option B: Self-Signed (Testing)
mkdir -p /etc/kamailio/certs
cd /etc/kamailio/certs
# Generate CA
openssl genrsa -out ca-key.pem 4096
openssl req -x509 -new -nodes -key ca-key.pem -sha256 -days 3650 \
-out ca-cert.pem \
-subj "/C=US/ST=State/L=City/O=MyOrg/CN=SIP-CA"
# Generate server certificate
openssl genrsa -out privkey.pem 2048
openssl req -new -key privkey.pem -out server.csr \
-subj "/C=US/ST=State/L=City/O=MyOrg/CN=YOUR_DOMAIN"
# Sign with CA
openssl x509 -req -in server.csr -CA ca-cert.pem -CAkey ca-key.pem \
-CAcreateserial -out fullchain.pem -days 365 -sha256
# Copy CA cert as chain
cp ca-cert.pem chain.pem
# Set ownership
chown -R kamailio:kamailio /etc/kamailio/certs/
chmod 600 /etc/kamailio/certs/privkey.pem
Client Certificate Verification
For mutual TLS (mTLS), require clients to present a valid certificate:
/* In tls.cfg — require client certificates */
[server:default]
method = TLSv1.2+
verify_certificate = yes
require_certificate = yes
private_key = /etc/kamailio/certs/privkey.pem
certificate = /etc/kamailio/certs/fullchain.pem
ca_list = /etc/kamailio/certs/ca-cert.pem
In routing logic, you can check TLS peer information:
if ($proto == "tls") {
xlog("L_INFO", "TLS peer: $tls_peer_subject\n");
xlog("L_INFO", "TLS peer CN: $tls_peer_subject_cn\n");
xlog("L_INFO", "TLS peer verified: $tls_peer_verified\n");
if ($tls_peer_verified != 1) {
sl_send_reply(403, "Certificate Verification Failed");
exit;
}
}
SRTP with RTPEngine
TLS only encrypts SIP signaling. To encrypt the media (RTP), you need SRTP. RTPEngine handles this transparently:
/* In NATMANAGE route — encrypt media */
route[NATMANAGE] {
if (is_request()) {
if (has_body("application/sdp")) {
if ($proto == "tls" || $proto == "wss") {
/* TLS/WSS client — use SRTP on client side, plain RTP to backend */
rtpengine_manage("replace-origin replace-session-connection ICE=remove SDES-off RTP/SAVP");
} else {
/* Plain SIP — no SRTP */
rtpengine_manage("replace-origin replace-session-connection ICE=remove");
}
}
}
if (is_reply()) {
if (has_body("application/sdp")) {
rtpengine_manage("replace-origin replace-session-connection ICE=remove");
}
}
return;
}
Pike Module — Flood Protection
The pike module detects and blocks IP addresses that send too many SIP requests (brute-force attacks, SIP scanners):
loadmodule "pike.so"
modparam("pike", "sampling_time_unit", 2) /* 2-second window */
modparam("pike", "reqs_density_per_unit", 30) /* Max 30 requests per window */
modparam("pike", "remove_latency", 4) /* Block for 4 windows (8 seconds) */
Usage in request_route:
request_route {
/* Anti-flood — check before anything else */
if (src_ip != myself) {
if (!pike_check_req()) {
xlog("L_ALERT", "PIKE: Blocking flood from $si\n");
/* Silently drop — don't send reply (wastes resources) */
exit;
}
}
/* ... rest of routing ... */
}
Htable — IP Blacklisting
The htable module provides in-memory hash tables for fast lookups. Use it for blacklists, whitelists, and rate counters:
loadmodule "htable.so"
/* Define a blacklist table */
modparam("htable", "htable", "ipban=>size=8;autoexpire=3600;")
/* size=8: 2^8 = 256 slots, autoexpire=3600: entries expire after 1 hour */
/* Define a counter table for rate limiting per user */
modparam("htable", "htable", "ratelimit=>size=10;autoexpire=60;")
Usage:
request_route {
/* Check blacklist */
if ($sht(ipban=>$si) != $null) {
xlog("L_WARN", "Blocked blacklisted IP: $si\n");
exit; /* Silently drop */
}
/* After pike blocks, add to blacklist */
if (src_ip != myself) {
if (!pike_check_req()) {
$sht(ipban=>$si) = 1;
xlog("L_ALERT", "BLACKLISTED: $si (pike trigger)\n");
exit;
}
}
/* Per-user rate limiting (max 10 INVITE per minute) */
if (is_method("INVITE") && !has_totag()) {
$sht(ratelimit=>$fU) = $sht(ratelimit=>$fU) + 1;
if ($sht(ratelimit=>$fU) > 10) {
sl_send_reply(429, "Too Many Requests");
exit;
}
}
}
fail2ban Integration
Kamailio can log authentication failures in a format that fail2ban can parse:
/* In the AUTH route — log failures */
route[AUTH] {
if (is_method("REGISTER")) {
if (!www_authorize("YOUR_DOMAIN", "subscriber")) {
/* Log for fail2ban — note the specific format */
xlog("L_WARN", "AUTH_FAILED: method=REGISTER user=$fU from=$si\n");
www_challenge("YOUR_DOMAIN", 1);
exit;
}
}
}
Create a fail2ban filter for Kamailio:
cat > /etc/fail2ban/filter.d/kamailio.conf << 'EOF'
[Definition]
failregex = AUTH_FAILED:.*from=<HOST>
ignoreregex =
EOF
Create a fail2ban jail:
cat > /etc/fail2ban/jail.d/kamailio.conf << 'EOF'
[kamailio]
enabled = true
filter = kamailio
logpath = /var/log/kamailio/kamailio.log
maxretry = 5
findtime = 300
bantime = 3600
action = iptables-allports[name=kamailio, protocol=all]
EOF
# Make sure Kamailio logs to a file (rsyslog)
cat > /etc/rsyslog.d/kamailio.conf << 'EOF'
local0.* /var/log/kamailio/kamailio.log
EOF
mkdir -p /var/log/kamailio
systemctl restart rsyslog
systemctl restart fail2ban
Security Best Practices
| Practice | Implementation |
|---|---|
| Hide server version | server_signature=no in kamailio.cfg |
| Block scanners | Check User-Agent for friendly-scanner, sipcli, sipvicious |
| Require auth | Always authenticate REGISTER and INVITE from external sources |
| Use TLS | Enable port 5061, redirect SIP phones to use TLS |
| Rate limit | Pike + htable for per-IP and per-user rate limiting |
| IP whitelist | Use permissions module for trusted trunk IPs |
| Geo-blocking | Use GeoIP module to block countries you don't serve |
| Topology hiding | Use topoh module to strip internal IPs from SIP headers |
| Log auth failures | Enable fail2ban integration |
| Firewall | Only open ports 5060/5061 to the internet, restrict RTP range |
| Regular updates | Keep Kamailio and RTPEngine updated |
9. Load Balancing with Dispatcher
What Is the Dispatcher Module?
The dispatcher module distributes SIP traffic across multiple backend servers (Asterisk, FreeSWITCH, etc.) with health checking and automatic failover. This is the most common reason to deploy Kamailio in front of a PBX cluster.
┌──────────────┐
│ Kamailio │
│ (Dispatcher)│
└──────┬───────┘
│
┌────────────┼────────────┐
│ │ │
┌────▼───┐ ┌────▼───┐ ┌────▼───┐
│Asterisk│ │Asterisk│ │Asterisk│
│ #1 │ │ #2 │ │ #3 │
└────────┘ └────────┘ └────────┘
Module Setup
loadmodule "dispatcher.so"
modparam("dispatcher", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
modparam("dispatcher", "table_name", "dispatcher")
modparam("dispatcher", "flags", 2) /* Failover support */
modparam("dispatcher", "ds_ping_interval", 10) /* Health check every 10s */
modparam("dispatcher", "ds_ping_method", "OPTIONS") /* Use SIP OPTIONS */
modparam("dispatcher", "ds_probing_threshold", 3) /* 3 failures = down */
modparam("dispatcher", "ds_inactive_threshold", 3) /* 3 successes = up */
modparam("dispatcher", "ds_probing_mode", 1) /* Probe all destinations */
modparam("dispatcher", "ds_ping_from", "sip:kamailio@YOUR_DOMAIN")
modparam("dispatcher", "ds_ping_reply_codes", "class2;class3;class4")
Destination Sets and Algorithms
Destinations are organized into sets (groups). Each set has an algorithm that determines how traffic is distributed:
| Algorithm | ID | Description |
|---|---|---|
| Round-Robin | 0 | Rotate through backends sequentially |
| Weight-Based | 1 | Distribute by weight (e.g., 70/30 split) |
| Call-ID Hash | 2 | Same Call-ID always goes to same backend |
| Random | 3 | Random selection |
| Priority | 4 | Always use first available (active-standby) |
| Hash over From URI | 5 | Same caller always reaches same backend |
| Hash over To URI | 6 | Same destination always reaches same backend |
| Hash over Request URI | 7 | Hash on R-URI |
| Hash over source IP | 8 | Same source IP always reaches same backend |
| Weight-based random | 9 | Random with weight probability |
| Call-load distribution | 10 | Least-loaded backend (requires dialog module) |
| Relative weight | 11 | Weight relative to total |
| Random (no reuse) | 12 | Random without selecting the same destination twice |
Adding Backend Servers
Via Database
/* dispatcher table: setid, destination, flags, priority, attrs, description */
-- Set 1: Asterisk backends (Round-Robin)
INSERT INTO dispatcher (setid, destination, flags, priority, attrs, description)
VALUES (1, 'sip:10.0.0.10:5060', 0, 0, 'weight=50;duid=ast1', 'Asterisk 1');
INSERT INTO dispatcher (setid, destination, flags, priority, attrs, description)
VALUES (1, 'sip:10.0.0.11:5060', 0, 0, 'weight=30;duid=ast2', 'Asterisk 2');
INSERT INTO dispatcher (setid, destination, flags, priority, attrs, description)
VALUES (1, 'sip:10.0.0.12:5060', 0, 0, 'weight=20;duid=ast3', 'Asterisk 3');
-- Set 2: Media servers (Priority-based failover)
INSERT INTO dispatcher (setid, destination, flags, priority, attrs, description)
VALUES (2, 'sip:10.0.0.20:5060', 0, 0, 'duid=media1', 'Primary Media');
INSERT INTO dispatcher (setid, destination, flags, priority, attrs, description)
VALUES (2, 'sip:10.0.0.21:5060', 0, 1, 'duid=media2', 'Backup Media');
# Reload dispatcher data
kamcmd dispatcher.reload
Via File (Alternative to Database)
If you prefer not to use a database, configure dispatcher to use a flat file:
modparam("dispatcher", "list_file", "/etc/kamailio/dispatcher.list")
modparam("dispatcher", "ds_probing_mode", 1)
Create /etc/kamailio/dispatcher.list:
# Format: setid destination flags priority attrs
# Set 1: Asterisk backends
1 sip:10.0.0.10:5060 0 0 weight=50;duid=ast1
1 sip:10.0.0.11:5060 0 0 weight=30;duid=ast2
1 sip:10.0.0.12:5060 0 0 weight=20;duid=ast3
# Set 2: Media servers (priority failover)
2 sip:10.0.0.20:5060 0 0 duid=media1
2 sip:10.0.0.21:5060 0 1 duid=media2
Dispatcher Routing Logic
route[DISPATCH] {
/* Select a backend from set 1 using round-robin (algorithm 0) */
if (!ds_select_dst(1, 0)) {
/* No backends available */
xlog("L_ERR", "DISPATCHER: No backends available in set 1\n");
sl_send_reply(503, "Service Unavailable");
exit;
}
xlog("L_INFO", "DISPATCH: Routing $rU to $du (set 1)\n");
/* Enable failover — if this backend fails, try the next */
t_on_failure("DISPATCH_FAILURE");
route(RELAY);
exit;
}
failure_route[DISPATCH_FAILURE] {
if (t_is_canceled()) exit;
/* Mark the failed destination as inactive */
if (t_check_status("5[0-9][0-9]") || t_check_status("408")) {
xlog("L_WARN", "DISPATCH: Backend $du failed ($T_reply_code), trying next\n");
ds_mark_dst("ip"); /* Mark as inactive (probing) */
/* Try the next backend in the set */
if (ds_next_dst()) {
xlog("L_INFO", "DISPATCH: Failover to $du\n");
t_on_failure("DISPATCH_FAILURE");
route(RELAY);
exit;
}
/* All backends down */
xlog("L_ERR", "DISPATCH: All backends in set 1 are down\n");
t_reply(503, "All Backends Unavailable");
exit;
}
}
Health Checking
Kamailio automatically sends SIP OPTIONS pings to all dispatcher destinations at the configured interval. The health check behavior:
Backend UP:
─── OPTIONS ──► Backend
◄── 200 OK ──── Backend
(Counter: successes = 0 → stays UP)
Backend goes DOWN:
─── OPTIONS ──► Backend
◄── [timeout] ── Backend
(Counter: failures = 1, 2, 3 → marked INACTIVE when threshold reached)
Backend recovers:
─── OPTIONS ──► Backend
◄── 200 OK ──── Backend
(Counter: successes = 1, 2, 3 → marked ACTIVE when threshold reached)
Monitor dispatcher status:
# Show all destinations and their status
kamcmd dispatcher.list
# Output format:
# SET DEST FLAGS PRIORITY ATTRS
# 1 sip:10.0.0.10:5060 AP 0 weight=50
# 1 sip:10.0.0.11:5060 AP 0 weight=30
# 1 sip:10.0.0.12:5060 IP 0 weight=20
# Flags: A=Active, I=Inactive, P=Probing, D=Disabled, X=Deleted
# Manually set destination state
kamcmd dispatcher.set_state ip 1 sip:10.0.0.12:5060 # Set to Inactive/Probing
kamcmd dispatcher.set_state ap 1 sip:10.0.0.12:5060 # Set to Active/Probing
Complete Dispatcher Configuration for 3 Asterisk Backends
#!KAMAILIO
#!define WITH_DISPATCHER
listen=udp:YOUR_SERVER_IP:5060
listen=tcp:YOUR_SERVER_IP:5060
debug=2
log_stderror=no
log_facility=LOG_LOCAL0
children=8
auto_aliases=no
server_signature=no
/* Modules */
loadmodule "kex.so"
loadmodule "sl.so"
loadmodule "tm.so"
loadmodule "tmx.so"
loadmodule "rr.so"
loadmodule "maxfwd.so"
loadmodule "pv.so"
loadmodule "textops.so"
loadmodule "siputils.so"
loadmodule "xlog.so"
loadmodule "sanity.so"
loadmodule "db_mysql.so"
loadmodule "usrloc.so"
loadmodule "registrar.so"
loadmodule "auth.so"
loadmodule "auth_db.so"
loadmodule "dispatcher.so"
loadmodule "nathelper.so"
loadmodule "rtpengine.so"
loadmodule "dialog.so"
/* Module parameters */
modparam("tm", "fr_timer", 30000)
modparam("tm", "fr_inv_timer", 120000)
modparam("rr", "enable_full_lr", 1)
modparam("rr", "append_fromtag", 1)
modparam("usrloc", "db_mode", 2)
modparam("usrloc", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
modparam("registrar", "max_expires", 3600)
modparam("registrar", "default_expires", 600)
modparam("auth_db", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
modparam("auth_db", "calculate_ha1", 1)
modparam("auth_db", "password_column", "password")
modparam("auth_db", "use_domain", 0)
/* Dispatcher — load balancer */
modparam("dispatcher", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
modparam("dispatcher", "flags", 2)
modparam("dispatcher", "ds_ping_interval", 10)
modparam("dispatcher", "ds_ping_method", "OPTIONS")
modparam("dispatcher", "ds_probing_threshold", 3)
modparam("dispatcher", "ds_inactive_threshold", 3)
modparam("dispatcher", "ds_probing_mode", 1)
modparam("dispatcher", "ds_ping_from", "sip:kamailio@YOUR_DOMAIN")
modparam("dispatcher", "ds_ping_reply_codes", "class2;class3;class4")
/* NAT */
modparam("nathelper", "natping_interval", 30)
modparam("nathelper", "ping_nated_only", 1)
modparam("nathelper", "sipping", 1)
modparam("nathelper", "received_avp", "$avp(RECEIVED)")
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")
/* Dialog — for call-load balancing */
modparam("dialog", "db_mode", 1)
modparam("dialog", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
modparam("dialog", "dlg_flag", 4)
/* ===== Routing ===== */
request_route {
if (!mf_process_maxfwd_header(10)) {
sl_send_reply(483, "Too Many Hops");
exit;
}
if (!sanity_check("17895", "7")) exit;
/* NAT detection */
if (nat_uac_test(63)) {
setflag(5);
fix_nated_contact();
force_rport();
}
/* Record-Route */
if (is_method("INVITE|SUBSCRIBE")) {
record_route();
}
/* In-dialog requests */
if (has_totag()) {
if (loose_route()) {
if (is_method("BYE")) {
rtpengine_delete();
}
if (has_body("application/sdp")) {
route(NATMANAGE);
}
route(RELAY);
exit;
}
if (is_method("ACK") && t_check_trans()) exit;
sl_send_reply(404, "Not Found");
exit;
}
/* CANCEL */
if (is_method("CANCEL")) {
if (t_check_trans()) t_relay();
exit;
}
t_check_trans();
/* Authenticate */
route(AUTH);
/* REGISTER — save locally, don't forward to backends */
if (is_method("REGISTER")) {
if (nat_uac_test(63)) {
fix_nated_register();
}
if (!save("location")) {
sl_send_reply(500, "Registration Failed");
}
exit;
}
/* OPTIONS */
if (is_method("OPTIONS") && ($rU == $null || $rU == "")) {
sl_send_reply(200, "OK");
exit;
}
/* INVITE — dispatch to backend Asterisk servers */
if (is_method("INVITE")) {
/* Track the dialog for call-load balancing */
setflag(4);
dlg_manage();
/* NAT manage */
route(NATMANAGE);
/* Dispatch to backend set 1 */
route(DISPATCH);
exit;
}
/* Other requests — try location lookup first */
if (lookup("location")) {
route(RELAY);
exit;
}
sl_send_reply(404, "Not Found");
exit;
}
route[AUTH] {
if (is_method("REGISTER")) {
if (!www_authorize("YOUR_DOMAIN", "subscriber")) {
www_challenge("YOUR_DOMAIN", 1);
exit;
}
if ($au != $fU) {
sl_send_reply(403, "Forbidden");
exit;
}
return;
}
if (is_method("INVITE|MESSAGE")) {
if (!proxy_authorize("YOUR_DOMAIN", "subscriber")) {
proxy_challenge("YOUR_DOMAIN", 1);
exit;
}
if ($au != $fU) {
sl_send_reply(403, "Forbidden");
exit;
}
consume_credentials();
}
return;
}
route[DISPATCH] {
/* Round-robin (algorithm 0) across set 1 */
if (!ds_select_dst(1, 0)) {
xlog("L_ERR", "DISPATCH: All backends down for $rU\n");
sl_send_reply(503, "Service Unavailable");
exit;
}
xlog("L_INFO", "DISPATCH: $rU → $du\n");
t_on_failure("DISPATCH_FAILURE");
route(RELAY);
exit;
}
route[RELAY] {
t_on_reply("MANAGE_REPLY");
if (!t_relay()) {
sl_send_reply(500, "Server Error");
}
exit;
}
route[NATMANAGE] {
if (is_request()) {
if (has_body("application/sdp")) {
rtpengine_manage("replace-origin replace-session-connection ICE=remove");
}
}
if (is_reply()) {
if (has_body("application/sdp")) {
rtpengine_manage("replace-origin replace-session-connection ICE=remove");
}
}
return;
}
onreply_route[MANAGE_REPLY] {
if (status =~ "[12][0-9][0-9]") {
route(NATMANAGE);
}
}
failure_route[DISPATCH_FAILURE] {
if (t_is_canceled()) exit;
if (t_check_status("5[0-9][0-9]") || t_check_status("408")) {
xlog("L_WARN", "DISPATCH: $du failed ($T_reply_code), failover\n");
ds_mark_dst("ip");
if (ds_next_dst()) {
xlog("L_INFO", "DISPATCH: Failover → $du\n");
t_on_failure("DISPATCH_FAILURE");
route(RELAY);
exit;
}
xlog("L_ERR", "DISPATCH: All backends exhausted\n");
t_reply(503, "All Backends Unavailable");
exit;
}
}
10. Dialog & Accounting
Dialog Module — Track Active Calls
The dialog module tracks SIP call dialogs from the initial INVITE to the final BYE. It provides call counting, duration tracking, and per-user call limits.
loadmodule "dialog.so"
modparam("dialog", "db_mode", 1) /* 1 = write to DB on changes */
modparam("dialog", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
modparam("dialog", "dlg_flag", 4) /* Flag to enable dialog tracking */
modparam("dialog", "timeout_avp", "$avp(dlg_timeout)")
modparam("dialog", "default_timeout", 43200) /* 12-hour max call duration */
/* Dialog profiles — for per-user call limits */
modparam("dialog", "profiles_with_value", "caller;callee")
modparam("dialog", "profiles_no_value", "active_calls")
Enable dialog tracking in the routing logic:
request_route {
/* ... (sanity, NAT, in-dialog handling) ... */
if (is_method("INVITE") && !has_totag()) {
/* New call — enable dialog tracking */
setflag(4);
dlg_manage();
/* Set dialog timeout (optional) */
$avp(dlg_timeout) = 7200; /* 2-hour limit for this call */
/* Set dialog profile for the caller */
set_dlg_profile("caller", "$fU");
set_dlg_profile("callee", "$rU");
set_dlg_profile("active_calls");
/* ... route the call ... */
}
}
Per-User Call Limits
Prevent a single user from making too many simultaneous calls:
route[CHECK_CALL_LIMITS] {
/* Max 5 simultaneous calls per caller */
if (get_profile_size("caller", "$fU", "$avp(caller_calls)")) {
if ($avp(caller_calls) >= 5) {
xlog("L_WARN", "Call limit reached for $fU ($avp(caller_calls) active)\n");
sl_send_reply(486, "Busy — Call Limit Reached");
exit;
}
}
/* Max 2 simultaneous calls per callee */
if (get_profile_size("callee", "$rU", "$avp(callee_calls)")) {
if ($avp(callee_calls) >= 2) {
xlog("L_WARN", "Callee $rU busy ($avp(callee_calls) active)\n");
sl_send_reply(486, "Busy Here");
exit;
}
}
return;
}
Dialog Statistics
# View active calls
kamcmd dlg.list
# Count active dialogs
kamcmd dlg.stats_active
# Get dialog by Call-ID
kamcmd dlg.dlg_list_ctx
# View profile sizes (active calls per user)
kamcmd dlg.profile_list caller
kamcmd dlg.profile_list active_calls
# Terminate a dialog
kamcmd dlg.end_dlg <h_entry> <h_id>
Accounting Module — CDR Generation
The acc module generates Call Detail Records (CDRs) and logs them to the database, syslog, or both.
loadmodule "acc.so"
loadmodule "acc_db.so" /* Database CDR backend (included in kamailio-mysql-modules) */
/* Accounting to database */
modparam("acc", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
modparam("acc", "db_flag", 1) /* Flag to trigger DB accounting */
modparam("acc", "db_missed_flag", 2) /* Flag for missed call logging */
modparam("acc", "failed_transaction_flag", 3) /* Flag for failed transactions */
/* What to log */
modparam("acc", "log_level", 1)
modparam("acc", "log_flag", 1)
/* CDR generation — log duration, start/end time */
modparam("acc", "cdr_enable", 1)
modparam("acc", "cdr_start_on_confirmed", 1) /* Start CDR timer on 200 OK */
modparam("acc", "cdr_log_enable", 1)
/* Extra fields to log */
modparam("acc", "db_extra",
"src_user=$fU;src_domain=$fd;src_ip=$si;"
"dst_user=$rU;dst_domain=$rd;"
"callid=$ci;ua=$ua")
/* CDR extra fields */
modparam("acc", "cdr_extra",
"src_user=$fU;dst_user=$rU;src_ip=$si;callid=$ci")
Enable accounting in routing:
request_route {
/* ... */
if (is_method("INVITE") && !has_totag()) {
setflag(1); /* Enable DB accounting */
setflag(2); /* Log missed calls */
setflag(3); /* Log failed transactions */
setflag(4); /* Enable dialog tracking (needed for CDR duration) */
dlg_manage();
/* ... route the call ... */
}
}
CDR Database Schema
The acc table stores accounting records:
-- View CDRs
SELECT id, method, from_tag, to_tag, callid, sip_code, sip_reason,
src_user, dst_user, src_ip, time
FROM acc
ORDER BY time DESC
LIMIT 20;
-- CDR table (if cdr_enable=1)
SELECT id, start_time, end_time, duration,
src_user, dst_user, src_ip, callid,
sip_code, sip_reason
FROM cdrs
ORDER BY start_time DESC
LIMIT 20;
Custom CDR Fields
Add custom data to your CDRs using AVPs:
/* In modparam section */
modparam("acc", "db_extra",
"src_user=$fU;src_domain=$fd;src_ip=$si;"
"dst_user=$rU;dst_domain=$rd;"
"callid=$ci;ua=$ua;"
"trunk=$avp(trunk_name);"
"campaign=$avp(campaign)")
/* In routing logic — set custom fields before acc triggers */
if (is_method("INVITE") && !has_totag()) {
$avp(trunk_name) = "protech";
$avp(campaign) = "uk_sales";
setflag(1);
/* ... */
}
You need to add corresponding columns to the acc table:
ALTER TABLE acc ADD COLUMN trunk VARCHAR(64) DEFAULT '';
ALTER TABLE acc ADD COLUMN campaign VARCHAR(64) DEFAULT '';
11. WebRTC Gateway
Overview
WebRTC allows browser-based voice/video calls using JavaScript. Browsers use SIP over WebSocket (WSS) for signaling and DTLS-SRTP for media. Kamailio acts as a gateway between WebRTC and traditional SIP endpoints.
┌─────────┐ WSS/SRTP ┌──────────┐ SIP/RTP ┌──────────┐
│ Browser │ ◄────────────► │ Kamailio │ ◄──────────► │ Asterisk │
│ SIP.js │ │+RTPEngine│ │ (PBX) │
└─────────┘ └──────────┘ └──────────┘
Browser → Kamailio: SIP over WSS (port 8443), DTLS-SRTP media
Kamailio → Asterisk: SIP over UDP (port 5060), plain RTP media
RTPEngine: Converts DTLS-SRTP ↔ plain RTP
WebSocket Module Setup
/* Module loading */
loadmodule "websocket.so"
loadmodule "xhttp.so" /* HTTP handling for WebSocket upgrade */
loadmodule "nathelper.so"
loadmodule "rtpengine.so"
loadmodule "tls.so"
/* Listen on WSS (WebSocket Secure) */
listen=tls:YOUR_SERVER_IP:8443
/* WebSocket parameters */
modparam("websocket", "keepalive_mechanism", 1) /* 1 = PING/PONG */
modparam("websocket", "keepalive_timeout", 30) /* Seconds */
modparam("websocket", "keepalive_processes", 1)
/* TLS for WSS */
modparam("tls", "private_key", "/etc/kamailio/certs/privkey.pem")
modparam("tls", "certificate", "/etc/kamailio/certs/fullchain.pem")
modparam("tls", "tls_method", "TLSv1.2+")
HTTP/WebSocket Upgrade Handling
WebSocket connections start as HTTP requests with an Upgrade: websocket header:
/* Handle HTTP requests (WebSocket upgrade) */
event_route[xhttp:request] {
set_reply_close();
set_reply_no_connect();
if ($hdr(Upgrade) =~ "websocket" &&
$hdr(Connection) =~ "Upgrade" &&
$rm == "GET") {
/* Validate Origin header (optional but recommended) */
# if ($hdr(Origin) != "https://YOUR_DOMAIN") {
# xhttp_reply(403, "Forbidden", "text/plain", "Bad Origin");
# exit;
# }
/* Accept WebSocket upgrade */
if (ws_handle_handshake()) {
exit;
}
}
/* Non-WebSocket HTTP request — reject */
xhttp_reply(404, "Not Found", "text/plain", "WebSocket Only");
exit;
}
WebRTC Routing Logic
WebRTC requires special handling for media negotiation. The key is using RTPEngine to bridge between DTLS-SRTP (WebRTC) and plain RTP (traditional SIP):
route[NATMANAGE_WEBRTC] {
if (is_request()) {
if (has_body("application/sdp")) {
if ($proto == "ws" || $proto == "wss") {
/* WebRTC → SIP: convert DTLS-SRTP to plain RTP */
rtpengine_manage(
"replace-origin replace-session-connection "
"rtcp-mux-demux ICE=remove "
"RTP/AVP SDES-off");
} else if (ds_is_from_list(1)) {
/* Reply from backend (SIP) → WebRTC: convert RTP to DTLS-SRTP */
rtpengine_manage(
"replace-origin replace-session-connection "
"rtcp-mux-offer ICE=force "
"RTP/SAVPF SDES-off "
"DTLS=passive");
} else {
/* SIP to SIP — standard relay */
rtpengine_manage(
"replace-origin replace-session-connection ICE=remove");
}
}
}
if (is_reply()) {
if (has_body("application/sdp")) {
if ($proto == "ws" || $proto == "wss") {
/* Reply going to WebRTC client */
rtpengine_manage(
"replace-origin replace-session-connection "
"rtcp-mux-offer ICE=force "
"RTP/SAVPF SDES-off "
"DTLS=passive");
} else {
/* Reply going to SIP endpoint */
rtpengine_manage(
"replace-origin replace-session-connection "
"rtcp-mux-demux ICE=remove "
"RTP/AVP SDES-off");
}
}
}
return;
}
Nginx Reverse Proxy for WSS
In production, you typically put Nginx in front of Kamailio for WSS, handling TLS termination:
# /etc/nginx/sites-available/kamailio-wss
upstream kamailio_wss {
server 127.0.0.1:8080; # Kamailio WS (non-TLS) listener
}
server {
listen 443 ssl;
server_name YOUR_DOMAIN;
ssl_certificate /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location /ws {
proxy_pass http://kamailio_wss;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
When using Nginx for TLS termination, Kamailio listens on plain WS:
/* Plain WebSocket behind Nginx */
listen=tcp:127.0.0.1:8080
JsSIP/SIP.js Browser Client Example
Here is a minimal browser client using JsSIP:
<!DOCTYPE html>
<html>
<head>
<title>WebRTC Phone</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jssip/3.10.0/jssip.min.js"></script>
</head>
<body>
<h2>WebRTC SIP Phone</h2>
<div>
<input type="text" id="target" placeholder="Extension (e.g., 1002)" />
<button onclick="makeCall()">Call</button>
<button onclick="hangUp()">Hang Up</button>
</div>
<div id="status">Disconnected</div>
<audio id="remoteAudio" autoplay></audio>
<script>
// Configuration
const socket = new JsSIP.WebSocketInterface('wss://YOUR_DOMAIN:8443');
const configuration = {
sockets: [socket],
uri: 'sip:1001@YOUR_DOMAIN',
password: 'MySecurePass123',
display_name: 'Web User 1001',
register: true,
session_timers: false
};
// Create User Agent
const ua = new JsSIP.UA(configuration);
let currentSession = null;
// Event handlers
ua.on('connected', () => {
document.getElementById('status').textContent = 'Connected';
});
ua.on('registered', () => {
document.getElementById('status').textContent = 'Registered';
});
ua.on('registrationFailed', (e) => {
document.getElementById('status').textContent = 'Registration failed: ' + e.cause;
});
ua.on('newRTCSession', (data) => {
const session = data.session;
currentSession = session;
if (session.direction === 'incoming') {
document.getElementById('status').textContent = 'Incoming call from ' + session.remote_identity.uri;
// Auto-answer (or show accept/reject buttons)
session.answer({
mediaConstraints: { audio: true, video: false }
});
}
session.on('confirmed', () => {
document.getElementById('status').textContent = 'Call active';
});
session.on('ended', () => {
document.getElementById('status').textContent = 'Call ended';
currentSession = null;
});
session.on('failed', (e) => {
document.getElementById('status').textContent = 'Call failed: ' + e.cause;
currentSession = null;
});
session.on('peerconnection', (e) => {
e.peerconnection.ontrack = (event) => {
document.getElementById('remoteAudio').srcObject = event.streams[0];
};
});
});
// Start the User Agent
ua.start();
// Make a call
function makeCall() {
const target = document.getElementById('target').value;
if (!target) return;
const options = {
mediaConstraints: { audio: true, video: false },
pcConfig: {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
}
};
currentSession = ua.call('sip:' + target + '@YOUR_DOMAIN', options);
}
// Hang up
function hangUp() {
if (currentSession) {
currentSession.terminate();
}
}
</script>
</body>
</html>
Complete WebRTC Gateway Configuration
#!KAMAILIO
listen=udp:YOUR_SERVER_IP:5060
listen=tcp:YOUR_SERVER_IP:5060
listen=tls:YOUR_SERVER_IP:5061
listen=tls:YOUR_SERVER_IP:8443
debug=2
log_stderror=no
log_facility=LOG_LOCAL0
children=8
tcp_children=8
auto_aliases=no
server_signature=no
force_rport=yes
/* Modules */
loadmodule "kex.so"
loadmodule "sl.so"
loadmodule "tm.so"
loadmodule "tmx.so"
loadmodule "rr.so"
loadmodule "maxfwd.so"
loadmodule "pv.so"
loadmodule "textops.so"
loadmodule "siputils.so"
loadmodule "xlog.so"
loadmodule "sanity.so"
loadmodule "xhttp.so"
loadmodule "websocket.so"
loadmodule "tls.so"
loadmodule "db_mysql.so"
loadmodule "usrloc.so"
loadmodule "registrar.so"
loadmodule "auth.so"
loadmodule "auth_db.so"
loadmodule "nathelper.so"
loadmodule "rtpengine.so"
/* TLS */
modparam("tls", "private_key", "/etc/kamailio/certs/privkey.pem")
modparam("tls", "certificate", "/etc/kamailio/certs/fullchain.pem")
modparam("tls", "tls_method", "TLSv1.2+")
/* WebSocket */
modparam("websocket", "keepalive_mechanism", 1)
modparam("websocket", "keepalive_timeout", 30)
/* Transaction */
modparam("tm", "fr_timer", 30000)
modparam("tm", "fr_inv_timer", 120000)
modparam("rr", "enable_full_lr", 1)
modparam("rr", "append_fromtag", 1)
/* User location */
modparam("usrloc", "db_mode", 2)
modparam("usrloc", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
modparam("registrar", "max_expires", 3600)
modparam("registrar", "default_expires", 300)
/* Auth */
modparam("auth_db", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
modparam("auth_db", "calculate_ha1", 1)
modparam("auth_db", "password_column", "password")
modparam("auth_db", "use_domain", 0)
/* NAT + RTPEngine */
modparam("nathelper", "natping_interval", 30)
modparam("nathelper", "ping_nated_only", 1)
modparam("nathelper", "sipping", 1)
modparam("nathelper", "received_avp", "$avp(RECEIVED)")
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")
/* WebSocket upgrade handler */
event_route[xhttp:request] {
set_reply_close();
set_reply_no_connect();
if ($hdr(Upgrade) =~ "websocket" && $hdr(Connection) =~ "Upgrade" && $rm == "GET") {
if (ws_handle_handshake()) exit;
}
xhttp_reply(404, "Not Found", "text/plain", "WebSocket Only");
exit;
}
/* Main routing */
request_route {
if (!mf_process_maxfwd_header(10)) {
sl_send_reply(483, "Too Many Hops");
exit;
}
if (!sanity_check("17895", "7")) exit;
/* NAT */
if (nat_uac_test(63)) {
setflag(5);
fix_nated_contact();
force_rport();
}
/* Tag WebSocket requests */
if ($proto == "ws" || $proto == "wss") {
setflag(6); /* Flag 6 = WebRTC client */
}
if (is_method("INVITE|SUBSCRIBE")) {
record_route();
}
/* In-dialog */
if (has_totag()) {
if (loose_route()) {
if (is_method("BYE")) rtpengine_delete();
if (has_body("application/sdp")) route(NATMANAGE_WS);
route(RELAY);
exit;
}
if (is_method("ACK") && t_check_trans()) exit;
sl_send_reply(404, "Not Found");
exit;
}
if (is_method("CANCEL")) {
if (t_check_trans()) t_relay();
exit;
}
t_check_trans();
/* Auth */
route(AUTH);
/* REGISTER */
if (is_method("REGISTER")) {
if (nat_uac_test(63)) fix_nated_register();
if (!save("location")) sl_send_reply(500, "Failed");
exit;
}
/* OPTIONS */
if (is_method("OPTIONS") && ($rU == $null || $rU == "")) {
sl_send_reply(200, "OK");
exit;
}
/* INVITE */
if (!lookup("location")) {
sl_send_reply(404, "User Not Found");
exit;
}
route(NATMANAGE_WS);
route(RELAY);
exit;
}
route[AUTH] {
if (is_method("REGISTER")) {
if (!www_authorize("YOUR_DOMAIN", "subscriber")) {
www_challenge("YOUR_DOMAIN", 1);
exit;
}
return;
}
if (is_method("INVITE|MESSAGE")) {
if (!proxy_authorize("YOUR_DOMAIN", "subscriber")) {
proxy_challenge("YOUR_DOMAIN", 1);
exit;
}
consume_credentials();
}
return;
}
route[RELAY] {
t_on_reply("MANAGE_REPLY_WS");
t_on_failure("MANAGE_FAILURE");
if (!t_relay()) sl_send_reply(500, "Relay Error");
exit;
}
route[NATMANAGE_WS] {
if (is_request()) {
if (has_body("application/sdp")) {
if (isflagset(6)) {
/* FROM WebRTC → TO SIP: DTLS-SRTP → plain RTP */
rtpengine_manage(
"replace-origin replace-session-connection "
"rtcp-mux-demux ICE=remove RTP/AVP SDES-off");
} else {
/* FROM SIP → TO WebRTC or SIP */
rtpengine_manage(
"replace-origin replace-session-connection ICE=remove");
}
}
}
if (is_reply()) {
if (has_body("application/sdp")) {
if (isflagset(6)) {
/* Reply TO WebRTC client: plain RTP → DTLS-SRTP */
rtpengine_manage(
"replace-origin replace-session-connection "
"rtcp-mux-offer ICE=force RTP/SAVPF SDES-off DTLS=passive");
} else {
rtpengine_manage(
"replace-origin replace-session-connection ICE=remove");
}
}
}
return;
}
onreply_route[MANAGE_REPLY_WS] {
if (status =~ "[12][0-9][0-9]") {
route(NATMANAGE_WS);
}
}
failure_route[MANAGE_FAILURE] {
if (t_is_canceled()) exit;
xlog("L_WARN", "Call to $rU failed: $T_reply_code\n");
}
12. Integration with Media Servers
Kamailio + Asterisk: SBC / Load Balancer Pattern
The most common deployment: Kamailio sits in front of one or more Asterisk servers, handling registration, authentication, NAT, and load balancing. Asterisk handles media processing (IVR, voicemail, conferencing, recording).
Internet DMZ LAN
─────────────────────────────────────────────────────────────────
┌──────────┐ ┌──────────┐
SIP Phones ────────────►│ Kamailio │──────────────►│ Asterisk │
SIP Trunks ────────────►│ (SBC) │ │ (PBX) │
WebRTC ────────────►│ │──────────────►│ Asterisk │
│+RTPEngine│ │ (PBX) │
└──────────┘ └──────────┘
Port 5060/5061 │ Port 5060
Port 8443 (WSS) │ (Internal)
Port 10000-20000
(RTP via RTPEngine)
Asterisk configuration for Kamailio integration:
On each Asterisk backend, configure it to trust the Kamailio server:
; /etc/asterisk/pjsip.conf (Asterisk 16+)
[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0
; Trust Kamailio as a SIP peer
[kamailio]
type=identify
endpoint=kamailio
match=YOUR_SERVER_IP
[kamailio]
type=endpoint
context=from-kamailio
disallow=all
allow=alaw
allow=ulaw
allow=opus
direct_media=no
trust_id_inbound=yes
trust_id_outbound=yes
send_pai=yes
[kamailio]
type=aor
max_contacts=1
; /etc/asterisk/extensions.conf
[from-kamailio]
; Calls from Kamailio arrive here
exten => _X.,1,NoOp(Call from Kamailio: ${CALLERID(num)} → ${EXTEN})
same => n,Dial(PJSIP/${EXTEN},30)
same => n,Hangup()
Kamailio + FreeSWITCH
The same pattern works with FreeSWITCH as the media server:
<!-- FreeSWITCH: /etc/freeswitch/sip_profiles/internal.xml -->
<!-- Add Kamailio as an ACL -->
<param name="apply-inbound-acl" value="kamailio"/>
<!-- /etc/freeswitch/autoload_configs/acl.conf.xml -->
<list name="kamailio" default="deny">
<node type="allow" cidr="YOUR_SERVER_IP/32"/>
</list>
Topology Hiding (topoh Module)
The topoh module hides your internal network topology from external SIP peers. It replaces internal IP addresses in SIP headers with the Kamailio server's public address:
loadmodule "topoh.so"
modparam("topoh", "mask_ip", "YOUR_SERVER_IP")
modparam("topoh", "mask_callid", 1) /* Mask Call-ID */
modparam("topoh", "sanity_checks", 1) /* Validate before unmasking */
What topoh does:
BEFORE (external peer sees internal IPs):
Via: SIP/2.0/UDP 10.0.0.10:5060 ← Internal Asterisk IP exposed!
Contact: <sip:[email protected]> ← Internal IP exposed!
Record-Route: <sip:10.0.0.10;lr> ← Internal IP exposed!
AFTER (topoh masks everything):
Via: SIP/2.0/UDP YOUR_SERVER_IP:5060
Contact: <sip:1001@YOUR_SERVER_IP>
Record-Route: <sip:YOUR_SERVER_IP;lr>
Header Manipulation for Integration
When routing between Kamailio and backend servers, you often need to add, remove, or modify SIP headers:
route[TO_ASTERISK] {
/* Add custom headers for Asterisk to use */
append_hf("X-Caller-IP: $si\r\n");
append_hf("X-Original-URI: $ou\r\n");
append_hf("X-Auth-User: $au\r\n");
/* Set P-Asserted-Identity for the backend */
remove_hf("P-Asserted-Identity");
append_hf("P-Asserted-Identity: <sip:$fU@YOUR_DOMAIN>\r\n");
/* Remove headers that shouldn't reach the backend */
remove_hf("Proxy-Authorization");
/* Route to backend */
$du = "sip:10.0.0.10:5060";
route(RELAY);
}
Domain-Based Routing (Multi-Tenant)
Route calls to different backends based on the domain in the request:
route[DOMAIN_ROUTE] {
/* Route based on destination domain */
if ($rd == "customer-a.com") {
$du = "sip:10.0.0.10:5060"; /* Customer A's Asterisk */
xlog("L_INFO", "Routing to Customer A backend\n");
} else if ($rd == "customer-b.com") {
$du = "sip:10.0.0.11:5060"; /* Customer B's Asterisk */
xlog("L_INFO", "Routing to Customer B backend\n");
} else if ($rd == "customer-c.com") {
/* Customer C uses dispatcher set 3 */
if (!ds_select_dst(3, 0)) {
sl_send_reply(503, "Service Unavailable");
exit;
}
} else {
/* Unknown domain */
sl_send_reply(404, "Domain Not Served");
exit;
}
route(RELAY);
exit;
}
For database-driven domain routing, use the domain module:
loadmodule "domain.so"
modparam("domain", "db_url",
"mysql://kamailio:YOUR_KAMAILIO_DB_PASSWORD@localhost/kamailio")
-- Add served domains
INSERT INTO domain (domain) VALUES ('customer-a.com');
INSERT INTO domain (domain) VALUES ('customer-b.com');
/* Check if we serve this domain */
if (!is_domain_local("$rd")) {
sl_send_reply(403, "Domain Not Served");
exit;
}
Complete Integration Config Example
/* Integrated SBC config: Kamailio in front of 2 Asterisk servers */
route[ROUTE_TO_BACKEND] {
/* Determine which backend based on called number pattern */
if ($rU =~ "^1[0-9]{3}$") {
/* 4-digit extensions starting with 1 → Asterisk 1 */
$du = "sip:10.0.0.10:5060";
xlog("L_INFO", "Routing ext $rU → Asterisk 1\n");
} else if ($rU =~ "^2[0-9]{3}$") {
/* 4-digit extensions starting with 2 → Asterisk 2 */
$du = "sip:10.0.0.11:5060";
xlog("L_INFO", "Routing ext $rU → Asterisk 2\n");
} else if ($rU =~ "^[0-9]{10,}$") {
/* External number (10+ digits) → dispatcher set for trunks */
if (!ds_select_dst(2, 4)) { /* Set 2, priority algorithm */
sl_send_reply(503, "No Trunks Available");
exit;
}
xlog("L_INFO", "Routing PSTN $rU → trunk $du\n");
} else {
sl_send_reply(404, "Number Not Routable");
exit;
}
/* Add integration headers */
append_hf("P-Asserted-Identity: <sip:$fU@YOUR_DOMAIN>\r\n");
append_hf("X-Kamailio-Server: sbc01\r\n");
/* Enable dialog tracking */
setflag(4);
dlg_manage();
/* Manage media */
route(NATMANAGE);
/* Forward with failover */
t_on_failure("BACKEND_FAILURE");
route(RELAY);
exit;
}
failure_route[BACKEND_FAILURE] {
if (t_is_canceled()) exit;
if (t_check_status("408|5[0-9][0-9]")) {
xlog("L_WARN", "Backend $du failed ($T_reply_code)\n");
/* Try alternate backend */
if ($rU =~ "^1[0-9]{3}$") {
/* Asterisk 1 failed — try Asterisk 2 */
$du = "sip:10.0.0.11:5060";
} else if ($rU =~ "^2[0-9]{3}$") {
/* Asterisk 2 failed — try Asterisk 1 */
$du = "sip:10.0.0.10:5060";
} else {
/* Trunk failover — use dispatcher */
if (ds_next_dst()) {
t_on_failure("BACKEND_FAILURE");
route(RELAY);
exit;
}
t_reply(503, "All Backends Down");
exit;
}
t_on_failure("FINAL_FAILURE");
route(RELAY);
exit;
}
}
failure_route[FINAL_FAILURE] {
if (t_is_canceled()) exit;
xlog("L_ERR", "All backends failed for $rU\n");
}
13. Monitoring & Management
kamcmd — Command-Line Management
kamcmd is the primary tool for interacting with a running Kamailio instance:
# Server information
kamcmd core.version
kamcmd core.uptime
kamcmd core.info
# Process management
kamcmd core.ps # List processes
kamcmd core.psx # Extended process list
# Statistics
kamcmd stats.get_statistics all # All statistics
kamcmd stats.get_statistics core: # Core statistics
kamcmd stats.get_statistics tmx: # Transaction statistics
kamcmd stats.get_statistics dialog: # Dialog (call) statistics
kamcmd stats.get_statistics usrloc: # Registration statistics
kamcmd stats.get_statistics dispatcher: # Dispatcher statistics
# User location (registrations)
kamcmd ul.dump # Dump all registrations
kamcmd ul.lookup location 1001 # Look up specific user
kamcmd ul.rm location 1001 # Remove a registration
kamcmd ul.flush # Flush expired contacts
# Dispatcher
kamcmd dispatcher.list # List all destinations with status
kamcmd dispatcher.reload # Reload from database
kamcmd dispatcher.set_state ap 1 sip:10.0.0.10:5060 # Set Active
kamcmd dispatcher.set_state ip 1 sip:10.0.0.10:5060 # Set Inactive
# Dialog
kamcmd dlg.list # List active calls
kamcmd dlg.stats_active # Active call count
kamcmd dlg.end_dlg <h_entry> <h_id> # Terminate a call
# TLS
kamcmd tls.list # List TLS connections
kamcmd tls.info # TLS module info
# Memory
kamcmd pkg.stats # Per-process memory
kamcmd mod.stats # Per-module memory
# Configuration reload
kamcmd cfg.sets # List config variables
kamctl — User and Database Management
kamctl is the higher-level management tool for user/database operations:
# User management
kamctl add 1001 password123 # Add user
kamctl rm 1001 # Remove user
kamctl passwd 1001 newpass # Change password
kamctl showdb subscriber # List all users
# Address/ACL management
kamctl address add 1 10.0.0.5 32 5060 "Asterisk Backend"
kamctl address show
kamctl address reload
# Dispatcher management
kamctl dispatcher add 1 sip:10.0.0.10:5060 0 0 "" "Backend 1"
kamctl dispatcher show
kamctl dispatcher reload
# Database operations
kamctl db show version # Show table versions
kamctl db exec "SELECT * FROM location" # Raw SQL query
# Online monitoring
kamctl monitor # Real-time stats display
kamctl stats # One-shot statistics
# Fifo commands
kamctl fifo which # List available FIFO commands
JSONRPC Interface for Remote Management
The jsonrpcs module exposes a JSON-RPC interface for programmatic management:
loadmodule "jsonrpcs.so"
modparam("jsonrpcs", "pretty_format", 1)
modparam("jsonrpcs", "transport", 1)
/* Optional: HTTP-based JSONRPC */
loadmodule "xhttp.so"
/* Add to event_route[xhttp:request] or use separate port */
Example JSONRPC calls:
# Using kamcmd (which uses JSONRPC internally)
kamcmd -s tcp:localhost:2049 core.version
# Using curl (if HTTP transport is enabled)
curl -s -X POST http://localhost:5060/jsonrpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"core.version","id":1}'
# Get active calls count
curl -s -X POST http://localhost:5060/jsonrpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"stats.get_statistics","params":[["dialog:"]],"id":1}'
Statistics Overview
Key statistics to monitor:
| Category | Statistic | Description |
|---|---|---|
| Core | core:rcv_requests |
Total received SIP requests |
| Core | core:rcv_replies |
Total received SIP replies |
| Core | core:fwd_requests |
Total forwarded requests |
| Core | core:fwd_replies |
Total forwarded replies |
| Core | core:drop_requests |
Dropped requests |
| Core | core:err_requests |
Error requests |
| Transaction | tmx:UAS_transactions |
Server transactions |
| Transaction | tmx:UAC_transactions |
Client transactions |
| Transaction | tmx:inuse_transactions |
Currently active transactions |
| Transaction | tmx:2xx_transactions |
Successful transactions |
| Transaction | tmx:4xx_transactions |
Client error transactions |
| Transaction | tmx:5xx_transactions |
Server error transactions |
| Dialog | dialog:active_dialogs |
Currently active calls |
| Dialog | dialog:processed_dialogs |
Total processed dialogs |
| Dialog | dialog:expired_dialogs |
Timed-out dialogs |
| Dialog | dialog:failed_dialogs |
Failed dialogs |
| Registrar | usrloc:registered_users |
Currently registered users |
| Registrar | usrloc:location-contacts |
Total contacts in location table |
| Registrar | usrloc:location-expires |
Expired contacts |
| Dispatcher | Check via kamcmd dispatcher.list |
Per-destination status |
Prometheus Exporter
Use kamailio_exporter to expose Kamailio statistics as Prometheus metrics:
# Install the Kamailio Prometheus exporter
# Option 1: Go binary
wget https://github.com/florentchauveau/kamailio_exporter/releases/latest/download/kamailio_exporter_linux_amd64
chmod +x kamailio_exporter_linux_amd64
mv kamailio_exporter_linux_amd64 /usr/local/bin/kamailio_exporter
# Create systemd service
cat > /etc/systemd/system/kamailio-exporter.service << 'EOF'
[Unit]
Description=Kamailio Prometheus Exporter
After=kamailio.service
[Service]
ExecStart=/usr/local/bin/kamailio_exporter \
--kamailio.address=unix:/var/run/kamailio/kamailio_ctl \
--web.listen-address=:9494
Restart=always
User=kamailio
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now kamailio-exporter
Prometheus scrape config:
# /etc/prometheus/prometheus.yml
scrape_configs:
- job_name: 'kamailio'
static_configs:
- targets: ['localhost:9494']
scrape_interval: 15s
Grafana Dashboard
Key panels for a Kamailio Grafana dashboard:
| Panel | PromQL Query | Type |
|---|---|---|
| Active Calls | kamailio_dialog_active_dialogs |
Gauge |
| Calls/sec | rate(kamailio_dialog_processed_dialogs[5m]) |
Graph |
| SIP Requests/sec | rate(kamailio_core_rcv_requests[5m]) |
Graph |
| Failed Calls | rate(kamailio_tmx_5xx_transactions[5m]) |
Graph |
| Registered Users | kamailio_usrloc_registered_users |
Gauge |
| Memory Usage | kamailio_core_shm_used_size |
Gauge |
| Error Rate | rate(kamailio_core_err_requests[5m]) |
Graph |
| Dispatcher Status | kamailio_dispatcher_target_up |
Status Map |
Log Analysis Patterns
Configure rsyslog to write Kamailio logs to a dedicated file:
# /etc/rsyslog.d/kamailio.conf
local0.* /var/log/kamailio/kamailio.log
Key log patterns to monitor:
# Authentication failures (potential brute force)
grep "AUTH_FAILED" /var/log/kamailio/kamailio.log | tail -20
# Pike blocks (flood detection)
grep "PIKE" /var/log/kamailio/kamailio.log | tail -20
# Dispatcher failures (backend down)
grep "DISPATCH.*failed" /var/log/kamailio/kamailio.log | tail -20
# Registration events
grep "REGISTER" /var/log/kamailio/kamailio.log | tail -20
# Call failures
grep "failed.*T_reply_code" /var/log/kamailio/kamailio.log | tail -20
Logrotate configuration:
cat > /etc/logrotate.d/kamailio << 'EOF'
/var/log/kamailio/kamailio.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
postrotate
/usr/bin/systemctl reload rsyslog > /dev/null 2>&1 || true
endscript
}
EOF
sipcapture / Homer Integration
Homer is a SIP capture and analysis tool. Kamailio can send SIP message copies to Homer for analysis:
loadmodule "siptrace.so"
modparam("siptrace", "duplicate_uri", "sip:HOMER_IP:9060")
modparam("siptrace", "hep_mode_on", 1)
modparam("siptrace", "hep_version", 3)
modparam("siptrace", "hep_capture_id", 100)
modparam("siptrace", "trace_on", 1)
modparam("siptrace", "trace_flag", 22)
/* In request_route — enable tracing for all traffic */
request_route {
setflag(22); /* Enable SIP trace */
sip_trace();
/* ... */
}
14. Troubleshooting
SIP Tracing in Kamailio
Debug Logging Levels
| Level | Name | Use Case |
|---|---|---|
| 0 | L_ALERT | Critical errors only |
| 1 | L_BUG | Software bugs |
| 2 | L_CRIT / L_ERR | Errors (production default) |
| 3 | L_WARN | Warnings + errors |
| 4 | L_NOTICE | Important operational info |
| 5 | L_INFO | General operational info |
| 6 | L_DBG | Full debug output (development only) |
Change debug level at runtime (no restart needed):
# Get current debug level
kamcmd cfg.get core debug
# Set debug to maximum (temporary — resets on restart)
kamcmd cfg.seti core debug 6
# Set back to production level
kamcmd cfg.seti core debug 2
Using xlog for Targeted Tracing
/* Add strategic xlog statements */
request_route {
xlog("L_INFO", ">>> $rm from $fU@$si:$sp to $rU R-URI=$ru\n");
/* Trace specific call by Call-ID */
if ($ci =~ "abc123") {
xlog("L_DBG", "[TRACE] Headers:\n$mb\n");
}
}
route[RELAY] {
xlog("L_INFO", "RELAY: $rm → $du (branch=$T_branch_idx)\n");
}
onreply_route[MANAGE_REPLY] {
xlog("L_INFO", "<<< Reply $T_reply_code $T_reply_reason for $rm ($ci)\n");
}
failure_route[MANAGE_FAILURE] {
xlog("L_WARN", "!!! FAILURE: $T_reply_code for $rU from $du ($ci)\n");
}
Config Trace Mode
Enable route execution tracing (very verbose — development only):
/* In kamailio.cfg — global parameter */
cfgtrace=1 /* Trace all route block execution */
Or per-message in routing logic:
if ($ci =~ "debug-this-call") {
setflag(25);
/* Use cfgtrace for this specific request */
}
Common Routing Errors
481 Call Leg Does Not Exist
Cause: Kamailio receives an in-dialog request (BYE, re-INVITE) but cannot find the matching transaction or dialog.
Common reasons:
- Kamailio restarted during an active call
- Record-Route was not added to the initial INVITE
- Dialog module lost state
Fix:
/* Ensure Record-Route is always added */
if (is_method("INVITE|SUBSCRIBE")) {
record_route();
}
/* Handle stale in-dialog requests gracefully */
if (has_totag()) {
if (!loose_route()) {
if (is_method("ACK")) {
if (t_check_trans()) exit;
exit; /* Silently absorb orphan ACKs */
}
if (is_method("BYE")) {
/* Reply 481 but don't log as error — normal after restart */
sl_send_reply(481, "Call Leg Does Not Exist");
exit;
}
}
}
407 Authentication Loops
Cause: Kamailio keeps challenging with 407, and the client keeps retrying with credentials, but they never match.
Diagnosis:
# Check if the user exists in subscriber table
kamctl showdb subscriber | grep 1001
# Check realm mismatch
# The realm in www_authorize/proxy_authorize MUST match
# what the SIP phone uses
Fix:
/* Ensure realm matches your domain/IP */
if (!proxy_authorize("YOUR_DOMAIN", "subscriber")) {
/* Try with IP as realm if domain fails */
if (!proxy_authorize("YOUR_SERVER_IP", "subscriber")) {
proxy_challenge("YOUR_DOMAIN", 1);
exit;
}
}
Also verify calculate_ha1 is set correctly:
calculate_ha1=1— passwords stored in plaintext, Kamailio hashes themcalculate_ha1=0— passwords stored as HA1 hashes already
483 Too Many Hops
Cause: SIP message is looping between servers, incrementing Max-Forwards until it reaches 0.
Diagnosis:
# Watch for looping messages
grep "483" /var/log/kamailio/kamailio.log
# Check Via headers in the SIP trace — if you see
# the same server IP appearing multiple times, it's a loop
Fix:
/* Ensure mf_process_maxfwd_header is at the TOP of request_route */
request_route {
if (!mf_process_maxfwd_header(10)) {
sl_send_reply(483, "Too Many Hops");
exit;
}
/* Check for loop: same message arriving twice */
if (is_method("INVITE") && !has_totag()) {
if (t_lookup_request()) {
/* Already processing this transaction — retransmission */
exit;
}
}
}
Registration Failures
Symptom: SIP phones cannot register. Check:
# 1. Is Kamailio listening?
ss -ulnp | grep 5060
# 2. Check for registration attempts in logs
grep "REGISTER" /var/log/kamailio/kamailio.log | tail -20
# 3. Verify user exists
kamctl showdb subscriber
# 4. Check current registrations
kamcmd ul.dump
# 5. Test registration manually
sipsak -U -C sip:test@YOUR_SERVER_IP \
-s sip:1001@YOUR_SERVER_IP \
-u 1001 -a password123
Common issues:
| Issue | Solution |
|---|---|
| User not in subscriber table | kamctl add 1001 password |
| Wrong realm | Match realm in kamailio.cfg to what phone sends |
| TLS required but phone uses UDP | Add listen=udp:... |
| Firewall blocking | Open port 5060 UDP+TCP |
| Registration expires too fast | Increase max_expires |
NAT Problems (One-Way Audio)
Symptom: Call connects but one or both sides cannot hear audio.
Diagnosis checklist:
# 1. Is RTPEngine running?
systemctl status rtpengine
ss -ulnp | grep rtpengine
# 2. Check RTPEngine statistics
echo "list totals" | nc -u 127.0.0.1 2223
# 3. Are RTP ports open in the firewall?
# Port range 10000-20000 UDP must be open
ss -ulnp | grep -E "1[0-9]{4}|20000"
# 4. Check if NAT is being detected
grep "NAT detected" /var/log/kamailio/kamailio.log | tail -10
# 5. Capture SIP to check SDP
ngrep -W byline -d any port 5060 | grep -A20 "INVITE"
# Look at c= and m= lines — do they contain private IPs?
Fix checklist:
- Verify
nathelpermodule is loaded andnat_uac_test()is called - Verify
rtpenginemodule is loaded andrtpengine_manage()is called for both requests AND replies - Verify
Record-Routeis added so in-dialog requests go through Kamailio - Verify
force_rportis applied for NATed clients - Verify
fix_nated_contact()is called to rewrite Contact headers - Verify RTPEngine firewall ports are open (10000-20000 UDP)
Dispatcher Failover Not Working
Symptom: Calls fail when a backend goes down instead of failing over.
Diagnosis:
# Check dispatcher status
kamcmd dispatcher.list
# Are backends marked as Active (AP) or Inactive (IP)?
# Check if health checking is working
grep "OPTIONS" /var/log/kamailio/kamailio.log | tail -10
Common issues:
| Issue | Solution |
|---|---|
flags not set to 2 |
modparam("dispatcher", "flags", 2) enables failover |
No failure_route |
Must define failure_route with ds_mark_dst + ds_next_dst |
| Backend rejects OPTIONS | Set ds_ping_reply_codes to accept 4xx replies |
| Probing not enabled | modparam("dispatcher", "ds_probing_mode", 1) |
t_on_failure not set |
Must call t_on_failure("DISPATCH_FAILURE") before t_relay() |
Performance Tuning
| Parameter | Default | Production Recommendation | Notes |
|---|---|---|---|
children |
4 | 8–16 per interface | SIP worker processes |
tcp_children |
4 | 8–16 | TCP/TLS worker processes |
tcp_max_connections |
2048 | 4096–16384 | For large deployments |
SHM_MEMORY |
64 MB | 128–512 MB | Shared memory pool |
PKG_MEMORY |
8 MB | 16–32 MB | Per-process memory |
usrloc db_mode |
0 | 2 (write-back) | Balance speed vs persistence |
tm fr_timer |
30000 | 5000–10000 | Reduce for faster failover |
tm fr_inv_timer |
120000 | 30000–60000 | INVITE timeout |
maxfwd |
10 | 10 | Rarely needs changing |
dns_cache |
on | on | Cache DNS lookups |
tcp_connection_lifetime |
600 | 120–300 | Close idle TCP sooner |
OS-level tuning:
# /etc/sysctl.conf — network tuning for SIP proxy
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.rmem_default = 1048576
net.core.wmem_default = 1048576
net.core.netdev_max_backlog = 5000
net.ipv4.udp_mem = 8388608 12582912 16777216
net.ipv4.ip_local_port_range = 10000 65535
net.core.somaxconn = 4096
fs.file-max = 1000000
# Apply
sysctl -p
# Increase open file limit for Kamailio
cat >> /etc/security/limits.d/kamailio.conf << 'EOF'
kamailio soft nofile 65536
kamailio hard nofile 65536
EOF
Essential kamcmd Commands Reference
| Command | Purpose |
|---|---|
kamcmd core.version |
Show Kamailio version |
kamcmd core.uptime |
Show uptime |
kamcmd core.ps |
List all processes |
kamcmd cfg.seti core debug 6 |
Set debug level (runtime) |
kamcmd cfg.get core debug |
Get current debug level |
kamcmd stats.get_statistics all |
All statistics |
kamcmd stats.reset_statistics all |
Reset all counters |
kamcmd ul.dump |
Dump all registrations |
kamcmd ul.lookup location <user> |
Find a registered user |
kamcmd ul.rm location <user> |
Remove a registration |
kamcmd ul.flush |
Remove expired contacts |
kamcmd dlg.list |
List active call dialogs |
kamcmd dlg.stats_active |
Count active dialogs |
kamcmd dlg.end_dlg <h> <id> |
Terminate a dialog |
kamcmd dispatcher.list |
List dispatcher destinations |
kamcmd dispatcher.reload |
Reload dispatcher from DB |
kamcmd dispatcher.set_state <f> <s> <d> |
Set destination state |
kamcmd tls.list |
List TLS connections |
kamcmd pkg.stats |
Per-process memory usage |
kamcmd mod.stats |
Per-module memory usage |
kamcmd tm.list |
List active transactions |
kamcmd htable.dump ipban |
Dump blacklist table |
kamcmd htable.delete ipban <key> |
Remove from blacklist |
kamcmd htable.reload ipban |
Reload hash table |
kamcmd cfg.sets |
List all config variables |
Quick Diagnostic Script
Save this as /usr/local/bin/kamailio-diag.sh for quick health checks:
#!/bin/bash
# Kamailio Quick Diagnostics
echo "=== Kamailio Diagnostics ==="
echo ""
echo "--- Version & Uptime ---"
kamcmd core.version 2>/dev/null || echo "Kamailio not running!"
kamcmd core.uptime 2>/dev/null
echo ""
echo "--- Listening Sockets ---"
ss -ulnp 2>/dev/null | grep kamailio
ss -tlnp 2>/dev/null | grep kamailio
echo ""
echo "--- Active Registrations ---"
REGS=$(kamcmd ul.dump 2>/dev/null | grep -c "AOR::" || echo "0")
echo "Registered users: $REGS"
echo ""
echo "--- Active Calls ---"
kamcmd stats.get_statistics dialog: 2>/dev/null | grep active || echo "No dialog module"
echo ""
echo "--- Dispatcher Status ---"
kamcmd dispatcher.list 2>/dev/null | grep -E "URI|FLAGS" || echo "No dispatcher"
echo ""
echo "--- Key Statistics ---"
kamcmd stats.get_statistics core:rcv_requests 2>/dev/null
kamcmd stats.get_statistics core:err_requests 2>/dev/null
kamcmd stats.get_statistics tmx:5xx_transactions 2>/dev/null
echo ""
echo "--- Memory Usage ---"
kamcmd stats.get_statistics shmem: 2>/dev/null | head -5
echo ""
echo "--- Recent Errors (last 10) ---"
grep -i "error\|critical\|alert" /var/log/kamailio/kamailio.log 2>/dev/null | tail -10
echo ""
echo "--- RTPEngine Status ---"
systemctl is-active rtpengine 2>/dev/null || echo "RTPEngine not installed"
echo ""
echo "=== Done ==="
chmod +x /usr/local/bin/kamailio-diag.sh
End of Tutorial 42. You now have the foundation to deploy Kamailio as a SIP proxy, SBC, load balancer, or WebRTC gateway in front of your Asterisk or FreeSWITCH infrastructure. Start with a basic proxy config (Section 4), add authentication (Section 6), layer on NAT handling (Section 7) and TLS (Section 8), then scale with dispatcher (Section 9) as your platform grows.