← All Tutorials

Kamailio Fundamentals — Installation, Routing, Authentication & TLS

Infrastructure & DevOps Intermediate 75 min read #42

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

  1. Introduction
  2. Architecture Overview
  3. Installation — Debian 12 / Ubuntu 24.04
  4. Configuration Language
  5. SIP Routing
  6. User Authentication
  7. NAT Traversal
  8. TLS & Security
  9. Load Balancing with Dispatcher
  10. Dialog & Accounting
  11. WebRTC Gateway
  12. Integration with Media Servers
  13. Monitoring & Management
  14. 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:

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:

  1. Parses the SIP message
  2. Executes the routing logic defined in kamailio.cfg
  3. 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:

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:

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:

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

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:

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:

  1. Signaling: Kamailio tries to send SIP replies to 192.168.1.100 — which is unreachable from the internet
  2. 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:

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:


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:

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:

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:

  1. Verify nathelper module is loaded and nat_uac_test() is called
  2. Verify rtpengine module is loaded and rtpengine_manage() is called for both requests AND replies
  3. Verify Record-Route is added so in-dialog requests go through Kamailio
  4. Verify force_rport is applied for NATed clients
  5. Verify fix_nated_contact() is called to rewrite Contact headers
  6. 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.

Need expert help with your setup?

VoIP infrastructure consulting, AI voice agent integration, monitoring stacks, scaling — I've done it all in production.

Get a Free Consultation