Kamailio® (successor of former OpenSER and SER) is a mature, open-source SIP proxy server released under GPLv2+, written in ANSI C and capable of handling thousands of call setups per second. It has proven itself as a key component of carrier-grade SIP service delivery platforms, whether used as a Session Border Controller (SBC), core SIP server with media relay capabilities, or load-balanced SIP proxy.
Key Characteristics
- Real-time Configuration: Runtime parameter updates without server restart
Kamailio routing is based on a stateless-to-stateful spectrum: - RFC 3261 Compliant: Full compliance with the SIP protocol specification
- High Performance: Can handle up to 5000+ call setups per second in stateless mode
- Scalability: Serves populations of 300,000+ online subscribers on 4GB memory systems
- Modular Architecture: 150+ modules available, easily extensible with third-party addons
- Multi-OS Support: Alpine, CentOS, Debian, Fedora, FreeBSD, NetBSD, OpenBSD, OpenSUSE, Oracle, RHEL, Solaris, …
- Scripting Support: Native support for Lua, Python, Perl, and Java SIP Servlets (via KEMI framework)
- Media Handling: Integration with RTPEngine for media processing and relay
Stateless Routing (Load Distribution)
↓
Stateful Routing (Call State Management)
↓
Application-level Routing (Lua/Python Scripts)
Stateless Routing
[SIP Message] → [Basic Validation] → [Forward to Destination]
- No transaction state kept
- Suitable for load balancers and simple proxies like simple forwarding
- Uses
sl(stateless replies) module
Stateful Routing
[SIP Message] → [Create Transaction] → [State Machine] → [Manage Responses/Retransmissions]
- Transaction state tracked in memory
- Suitable for call control logic
- Uses
tm(transaction management) module - Examples include voicemail fallback, complex B2BUA call flows
Application-level Routing (Lua Scripts)
[SIP Event] → [Lua Script Execution] → [KSR API Calls] → [Routing Decision]
- Custom business logic in Lua/Python
- Access to call context via pseudo-variables
- Enables AI integration and dynamic decision-making
KEMI (Kamailio EMbedded Interface)
Kamailio’s native configuration language (kamailio.cfg) provides powerful SIP routing capabilities, but it has limitations for complex business logic. Traditional kamailio.cfg has limitations:
- Limited customization capabilities compared to full programming languages
- Pre-compiled at startup, preventing runtime reload without restart
- Server restarts disrupt all ongoing calls and development workflow
The KEMI framework solves this by allowing external scripting languages to execute SIP routing logic at runtime without server restarts.
✅ Runtime Reload – Update scripts without restarting the server (via RPC commands)
✅ Full Programming Language – Leverage loops, conditions, functions, libraries
✅ Hot Testing – Test routing logic without disrupting active calls
✅ Better Debugging – Language-specific debuggers and test frameworks
✅ Faster Execution – Interpreted at runtime, optimized for SIP processing
KEMI Supported Languages
| Language | Module | Best For |
|---|---|---|
| Lua | app_lua | Recommended – Fast, lightweight, production SIP routing |
| JavaScript | app_jsdt | Node.js compatibility, microservices |
| Python | app_python | Rich ecosystem, ML integration |
| Python3 | app_python3 | Modern Python, AI/ML libraries |
| Squirrel | app_sqlang | C-like syntax, lightweight alternative |
Legacy Modules (non-KEMI) – Not recommended:
- .NET/C# (
app_mono) - Perl (
app_perl) - Java (
app_java)
As Kamailio becomes the interpreter for these languages , it executes logic faster at runtime . KEMI also extends the modules with extension to kamailio c functions
Lua Language Characteristics
Lua is a lightweight, embeddable scripting language designed for speed and simplicity:
- MIT Licensed – Free for commercial and personal use
- Tiny Footprint – 297 KB compressed, 1.2 MB uncompressed
- Fast Execution – Register-based VM with optional JIT via LuaJIT
- Multi-paradigm – Imperative, functional, and object-oriented programming
- Automatic Memory Management – Incremental garbage collection
- Extensible – Meta-mechanisms for implementing classes and inheritance
Platform Support and Usage
Lua runs on virtually all platforms:
- Operating Systems: Unix, Linux, Windows, macOS
- Embedded Systems: Android, iOS, Symbian, BREW, Windows Phone
- Microcontrollers: ARM, Rabbit, Lego MindStorms
- Enterprise: IBM mainframes, cloud platforms
Lua is trusted in production by major companies:
- Adobe Lightroom – Photo editing and batch processing
- World of Warcraft – Game scripting framework
- Angry Birds – Mobile game engine
- NVIDIA DLSS – AI rendering technology
- Roblox – Game development platform
- Tarantool – In-memory database scripting
Key Advantages of employing Lua for SIP Routing
| Advantage | Impact |
|---|---|
| Speed | Comparable to C/C++, 10-100x faster than Python |
| Low Memory | 600KB total runtime, ideal for high-load scenarios |
| Easy Embedding | Designed to be embedded in C applications |
| Standard Library | All essential functions included by default |
| Concurrency – | Coroutines for non-blocking I/O operations |
Lua is the recommended choice for KEMI-based routing due to its lightweight nature, fast execution, and deep Kamailio integration via the app_lua module.
app_lua Module
The app_lua module (Kamailio 5.0+) integrates Lua as a first-class KEMI engine. It exports Kamailio functions to Lua scripts via the KSR API (Kamailio Scripting Runtime). It includes files to
- global parameters
- loading modules
- module parameters
- routing blocks like request_route {…}, reply_route {…}, branch_route {…} etc
These parameters including initialization event routes , are interpreted and loaded at Kamailio startup.
# In kamailio.cfg:
# 1. Load the app_lua module
loadmodule "app_lua.so"
# 2. Specify the Lua script path (required for runtime execution)
modparam("app_lua", "load", "/etc/kamailio/lua/routing.lua")
# 3. Set Lua as the configuration engine (optional, use if all routing in Lua)
cfgengine "lua"
Lua KEMI Interpreter
LuaJIT, an independent implementation of Lua using a just-in-time compiler. Lua interpreter is linked from liblua library in app_lua module.
Predefined functions in app_lua module
- ksr_request_route() : executed by Kamailio core every time a SIP request is received, equivalent of request_route {} from kamailio.cfg
- ksr_reply_route() : executed by Kamailio core every time a SIP Response (reply) is received, equivalent of reply_route {} from kamailio.cfg
- ksr_onsend_route() : executed when a SIP request (and optionally for a response) is sent out, equivalent of onsend_route {}
- branch route callback : name of the Lua function to be executed instead of a branch route has to be provided as parameter to KSR.tm.t_on_branch(…)
- onreply route callback : name of the Lua function to be executed instead of an onreply route has to be provided as parameter to KSR.tm.t_on_reply(…)
- failure route callback : name of the Lua function to be executed instead of a failure route has to be provided as parameter to KSR.tm.t_on_failure(…)
- branch failure route callback : name of the Lua function to be executed instead of an event route for branch failure has to be provided as parameter to KSR.tm.t_on_branch_failure(…)
- event route callback : name of the Lua function to be exectued instead of module specific event_route blocks is provided via event_callback parameter of that module
KSR is the new dynamic object exporting Kamailio functions. sr is the old static object exporting Kamailio functions
route.lua for routing logic
Main Routing Functions
Kamailio automatically calls these Lua functions if defined:
ksr_request_route() Every time a SIP request is received
function ksr_request_route()
KSR.info("===== Incoming SIP Request =====\n")
-- Log request details
KSR.info("From: " .. KSR.pv.get("$fu") .. "\n")
KSR.info("To: " .. KSR.pv.get("$tu") .. "\n")
KSR.info("Method: " .. KSR.pv.get("$rm") .. "\n")
-- Check max forwards
if KSR.maxfwd.process_maxfwd(10) < 0 then
KSR.sl.send_reply(483, "Too Many Hops")
return
end
-- Set routes
KSR.tm.t_on_branch("ksr_branch_route")
KSR.tm.t_on_reply("ksr_reply_route")
KSR.tm.t_on_failure("ksr_failure_route")
-- Forward request
if KSR.tm.t_relay() < 0 then
KSR.sl.send_reply(500, "Server error")
end
end
function ksr_reply_route()
KSR.info("===== response - from kamailio lua script\n");
end
function ksr_branch_route_one()
KSR.info("===== branch route - from kamailio lua script\n");
end
function ksr_onreply_route_one()
KSR.info("===== onreply route - from kamailio lua script\n");
end
function ksr_failure_route_one()
KSR.info("===== failure route - from kamailio lua script\n");
end
Core Kemi functions
KSR.add_local_rport() : Set the internal flag to add rport parameter to local generated Via header.
KSR.add_tcp_alias() : Adds a tcp port alias for the current connection (if tcp). Can help in firewall or nat traversal
KSR.add_tcp_alias_via() : Adds the port from the message via as an alias to TCP connection.
void KSR.dbg(…) : debug log
KSR.dbg("debug log message from embedded interpreter\n");
void KSR.err(…) : error logging
KSR.err("error log message from embedded interpreter\n");
void KSR.info(…) : info log
KSR.info("info log message from embedded interpreter\n");
KSR.force_rport() : Add rport parameter to the top Via of the incoming request and sent the SIP response to source port.
KSR.is_method() : Return true if the value of the parameter matches the method type of the SIP message.
if(KSR.is_method("INVITE")) {
...
}
KSR.is_method_in() ; Return true if SIP method of the currently processed message is matching one of the corresponding characters given as parameter. Matching the method is done based on corresponding characters:
I – INVITE
A – ACK
B – BYE
C – CANCEL
R – REGISTER
M – MESSAGE
O – OPTIONS
S – SUBSCRIBE
P – PUBLISH
N – NOTIFY
U – UPDATE
K – KDMQ
G – GET
T – POST
V – PUT
D – DELETE
if KSR.is_method_in("IABC") then
-- the method is INVITE, ACK, BYE or CANCEL
...
end
KSR.is_INVITE() : Return true if the method type of the SIP message is INVITE.
KSR.is_ACK() : true if the method type is ACK.
KSR.is_BYE() : true if the method type is BYE.
KSR.is_CANCEL() : true if CANCEL.
KSR.is_REGISTER() : true if REGISTER.
KSR.is_MESSAGE() : true if SIP message is MESSAGE.
KSR.is_SUBSCRIBE() : true if SIP message is SUBSCRIBE.
KSR.is_PUBLISH() : true if PUBLISH.
KSR.is_NOTIFY() : true if NOTIFY.
KSR.is_OPTIONS() : true if OPTIONS.
KSR.is_INFO() : true if INFO.
KSR.is_UPDATE() : true if UPDATE.
KSR.is_PRACK() : true if PRACK.
KSR.is_myself(…) : Return true of the URI address provided as parameter matches a local socket (IP) or local domain.
if KSR.is_myself("sip:127.0.0.1:5060") then
...
end
KSR.is_myself_furi() : Return true if the URI in From header matches a local socket (IP) or local domain.
KSR.is_myself_ruri() : Return true if the R-URI matches a local socket (IP) or local domain.
KSR.is_myself_turi() : Return true if the URI in To header matches a local socket (IP) or local domain.
KSR.is_myself_suri() : true if the URI built from source IP, source port and protocol matches a local socket (IP).
KSR.is_myself_suri() : true if the source IP matches a local socket (IP).
void KSR.log(…) : Write a log message specifying the level value. The level parameter can be: “dbg” , “info” , “warn” , “crit” , “err”
KSR.setflag(…) : Set the SIP message/transaction flag ( int from 0 to 31 ) at the index provided by the parameter.
KSR.resetflag(…) : Reset the SIP message/transaction flag ( int from 0 to 31 ) at the index provided by the parameter.
KSR.isflagset(…) :Return true if the message/transaction flag at the index provided by the parameter is set (the bit has value 1).
KSR.setbflag(…) : Set the branch flag(0-31) at the index provided by the parameter.
KSR.resetbflag(…) : Reset the branch flag(0-31) at the index provided by the parameter.
KSR.isbflagset(…) : Return true if the branch flag at the index provided by the parameter is set (the bit has value 1).
KSR.setbiflag(…) : Set the flag at the index provided by the first parameter to the branch number specified by the second parameter. The flag parameter has to be a number from 0 to 31. The branch parameter should be between 0 and 12 (a matter of max_branches global parameter).
KSR.resetbiflag(…) : Reset a branch flag by position and branch index.
KSR.isbiflagset(…) : Test if a branch flag is set by position and branch index.
KSR.setsflag(…) : Set a script flag.
KSR.resetsflag(…) : Reset a script flag.
KSR.issflagset(…) : Test if a script flag is set.
KSR.seturi(…) : Set the request URI (R-URI).
KSR.seturi("sip:alice@voip.com");
KSR.setuser(…)
KSR.setuser("alice");
KSR.sethost(…)
KSR.sethost("voip.com");
KSR.setdsturi(…)
KSR.setdsturi("sip:voip.com:5061;transport=tls");
KSR.resetdsturi(…) : Reset the destination URI (aka: outbound proxy address, dst_uri, $du).
KSR.isdsturiset(…) : Test if destination URI is set.
KSR.force_rport(…) : Set the flag for “rport” handling (send the reply based on source address instead of Via header).
KSR.set_drop(…) : Set the DROP flag, so at the end of KEMI script execution, the SIP request branch or the SIP response is not forwarded.
KSR.set_advertised_address() : Set the address (host or ip) to be advertised in Via header.
KSR.set_advertised_port() : Set the port (in string format) to be advertised in Via header.
KSR.set_forward_close(…) : Set the flag to close the connection after forwarding the message.
KSR.set_forward_no_connect(…) : Set the flag to not open a connection if the connection to the target does not exist when attempting to forward a message.
KSR.set_reply_close(…) : Set the flag to close the connection after sending a response.
KSR.set_reply_no_connect(…) : Set the flag to not open a connection if the connection for sending the response does not exist.
KSR.forward(…) : Forward the SIP request in stateless mode to the address set in destination URI ($du), or, if this is not set, to the address in request URI ($ru).
KSR.forward_uri(…) : Forward the SIP request in stateless mode to the address provided in the SIP URI parameter.
KSR.forward_uri("sip:127.0.0.1:5080;transport=tcp");
KSR.hdr.append(…) : Append header to current SIP message (request or reply).
KSR.hdr.append("X-My-Hdr: " + KSR.pv.getw("$si") + "\r\n");
KSR.hdr.append_after(…) : Append header to current SIP message (request or reply). It will be added after the first header matching the name hdrname.
KSR.hdr.append_after("X-My-Hdr: " + KSR.pv.getw("$si") + "\r\n", "Call-Id");
KSR.hdr.insert(…) : Insert header to current SIP message (request or reply). It will be added before the first header.
KSR.hdr.insert("X-My-Hdr: " + KSR.pv.getw("$si") + "\r\n");
KSR.hdr.insert_before(…) : Insert header to current SIP message (request or reply). It will be added before the header matching the name hdrname.
KSR.hdr.insert_before("X-My-Hdr: " + KSR.pv.getw("$si") + "\r\n", "Call-Id");
KSR.hdr.remove(…) : Remove all the headers with the name hdrval.
KSR.hdr.remove("X-My-Hdr");
KSR.hdr.is_present(…) : Return greater than 0 if a header with the name hdrval exists and less than 0 if there is no such header.
if(KSR.hdr.is_present("X-My-Hdr") > 0) {
...
}
KSR.hdr.append_to_reply(…) : Add a header to the SIP response to be generated by Kamailio for the current SIP request.
KSR.hdr.append_to_reply("X-My-Hdr: " + KSR.pv.getw("$si") + "\r\n");
PV And Special KEMI Functions
PV And Special KEMI Functions
KSR.pv submodule provides the functions to get, set and test the values of pseduo-variables.
The pvname that appears in the next sections in the function prototypes has to be a valid pseudo-variable name for Kamailio native configuration file (for example $ru, $var(x), $shv(z), …).
KSR.pv.get(…) : Return the value of pseudo-variable pvname. The returned value can be string or integer.
KSR.dbg("ruri is: " + KSR.pv.get("$ru") + "\n");
KSR.pv.gete(…) : Return the value of pseudo-variable pvname if it is different than $null or the empty string (“”) if the variable is having the $null value.
KSR.dbg("avp is: " + KSR.pv.gete("$avp(x)") + "\n");
KSR.pv.getvn(…) : Return the value of pseudo-variable pvname if it is different than $null or the parameter vn if the variable is having the $null value.
KSR.dbg("avp is: " + KSR.pv.getvn("$avp(x)", 0) + "\n");
KSR.pv.getvs(…) : Return the value of pseudo-variable pvname if it is different than $null or the parameter vs if the variable is having the $null value.
KSR.dbg("avp is: " + KSR.pv.getvs("$avp(x)", "foo") + "\n");
KSR.pv.getw(…) : Return the value of pseudo-variable pvname if it is different than $null or the string <> if the variable is having the $null value. This should be used instead of KSR.pv.get(…) in the scripting languages that throw and error when attempting to print a NULL (or NIL) value.
KSR.dbg("avp is: " + KSR.pv.getw("$avp(x)") + "\n");
KSR.pv.seti(…) : set the value of pseudo-variable pvname to integer value provided by parameter val.
KSR.pv.seti("$var(x)", 10);
KSR.pv.sets(…) : set the value of pseudo-variable pvname to string value provided by parameter val.
KSR.pv.sets("$var(x)", "kamailio");
KSR.pv.unset(…) : Set the value of pseudo-variable pvname to $null.
KSR.pv.unset("$avp(x)");
KSR.pv.is_null(…) : Return true if pseudo-variable pvname is $null.
if(KSR.pv.is_null("$avp(x)")) {
...
}
KSR.x.modf(…) : Execute a function (specified by fname) exported by a Kamailio module.
KSR.x.modf("sl_send_reply", "200", "OK");
KSR.x.exit(…) : stop the execution of the SIP routing script.
KSR.x.drop(…) : stop the execution of the SIP routing script and drop routing further the SIP request branch or response.
References:
