Kamailio and RTPEngine, an Open Source SBC

A SIP SBC (Session Border Controller) is designed to isolate external VoIP traffic from the traffic generated within the private subnet, ensuring the correct signaling and media flows between the external and internal networks, and vice versa. The term “Border” refers to a point of demarcation, as an SBC is located at the edge of the network and represents the boundary between the internal network and Internet. For example, suppose we have a VoIP phone (using the SIP protocol) inside a LAN, and this phone needs to receive calls from a public VoIP provider, which is necessarily located on the Internet. To communicate directly, the phone would need a public IP, which would represent a security risk for our network. The solution is called SBC, which, deployed between the LAN and the Internet, decouples the public call leg - from the provider to SBC public interface - from the private call leg between its private interface and the internal LAN elements.

There are several SBC manufacturers, with different costs and feature sets, but the goal of this article is to use Kamailio with RTPEngine to build our own Open Source SBC. We previously discussed this topic in this post and covered the installation of RTPEngine here, using CentOS 7 as the operating system. Now, we will see how to install and configure everything on Ubuntu 24.04, and the process will be much simpler than before.

Before we begin, I will briefly describe my reference architecture, which is based on EC2 instances in AWS and is represented in the image below.

SBC Kamailio AWS

The public Network Load Balancer is the entry point for SIP traffic into the VPC, which consists of two Public subnets and two Private subnets. The Public subnets contain the SBCs, which have both a private IP and an Elastic Public IP, while the Private subnets contain the SIP Proxy and Media Servers, which only have private IPs. The Network Load Balancer operates at Layer 4 of the OSI stack and is responsible for routing incoming INVITE requests to one of the available SBCs. The data exchange sequence can be summarized as follows:

  1. The Network Load Balancer receives the INVITE on its Listener and forwards it to one of the SBCs selected within the Target Group.
  2. The SBC receives the INVITE on its public interface and is configured to accept SIP traffic over UDP, TCP, and TLS.
  3. After receiving the INVITE from the Internet, the SBC forwards it to one of the SIP Proxies through its private interface over UDP, adding the necessary Record-Route headers to remain in the call path and modifying the media section within the SDP, to separate audio streams between the public and private legs.
  4. The SIP Proxy forwards the INVITE to one of its Media Servers, which answers with a 200 OK containing its private IP address in the SDP.
  5. The SBC receives the 200 OK on its private interface, modifies the media section in the SDP by inserting its public IP address, and forwards it to the caller.
  6. The negotiation terminates with an ACK sent from the caller to the Media Server, following the call path defined through the Record-Route headers.
  7. The media communication is established between the caller and the SBC’s public interface, and between the SBC’s private interface and the Media Server. The two audio streams remain completely separate and distinct.

As a general consideration, we can observe that in this scenario, the AWS Network Load Balancer is only involved in the initial routing of the INVITE and is then excluded from the call path. Additionally, the presence of the SBC allows us to: Manage NAT handling; Perform TLS-to-SIP and SIP-to-TLS conversions; Handle RTP relay and SRTP-to-RTP conversion (and vice versa); Support optional RTP transcoding; Enhance architectural security through topology hiding.

Sample reference architecture

Let’s now take a detailed look at the configuration of Kamailio and RTPEngine. However, before proceeding, I have simplified the scenario by limiting the architecture to a single SBC. This will allow us to focus on a single component and better understand its configuration.

SBC Kamailio

As shown in the figure above, the SBC is installed on an AWS EC2 instance with an assigned Elastic Public IP, making the system accessible from the Internet on ports 5060 UDP/TCP and 5061 TLS. Internally, it operates on port 5065, allowing communication with other network components.

Setup

As you can see, the setup of Kamailio on Ubuntu 24.04 is very quick. After launching the instance and properly configuring EIP, Routing, and Security Groups, you can proceed with installing the necessary packages.

	apt-get update
	apt upgrade
	apt install kamailio kamailio-mysql-modules kamailio-tls-modules kamailio-utils-modules kamailio-phonenum-modules rtpengine mysql-server sngrep

Once this is done, and we have installed Kamailio, RTPEngine, and MySQL Server, we can proceed with the configuration.

Configuration

We will divide the configuration into three steps: rtpengine, database and finally kamailio.

RTPEngine

The RTPEngine configuration file is /etc/rtpengine/rtpengine.conf, the only parameter that needs to be changed is interface along with port-min e port-max if you want to customize the RTP port range.

	interface = internal/172.31.8.126;external/172.31.8.126!44.195.24.234

We have defined two logical interfaces, one called internal that uses the private interface and a second called external that always uses the private interface (being the only one actually allocated on the EC2 instance), but announces the public IP associated with the interface. In this way we can use the direction option to tell rtpengine that traffic arriving from the Internet and directed to the Media Server must follow the direction external -> internal, while traffic in the opposite direction must follow internal -> external. We are saying that for SIP messages arriving from the Internet and directed towards the inside of the VPC, rtpengine will overwrite the SDP with the local address, while for messages directed towards the Internet rtpengine will replace the local address with the public one.

MySQL server

Kamailio provides the command kamdbctl create to create the database kamailio with all the tables, the only thing to do before running the command is to configure the necessary parameters in the file /etc/kamailio/kamctlrc. Let me say that in this scenario kamailio does not use its tables, but we need a table where to insert the data to characterize external destinations. So let’s create the table routing (you can choose the name you prefer, just align the configuration).

	create table routing (sipdomain varchar(512) NOT NULL UNIQUE, name varchar(255) NOT NULL, location varchar(16) NOT NULL, sipproto varchar(16) NOT NULL, rtpproto varchar(16) NOT NULL, PRIMARY KEY (sipdomain));

Kamailio

For better management, I have split the Kamailio configuration into multiple files, all located in /etc/kamailio.

  • definitions.cfg: contains static definitions, such as enabled modules, binding ports, and database credentials
  • aliases.cfg: holds any aliases that Kamailio should handle as its own URIs
  • tls.cfg: stores certificates and the private key for TLS management
  • dispatcher.list: lists destinations managed by the dispatcher module.
  • kamailio.cfg: the main configuration file containing the core logic and processing rules

Below are the details of each configuration file, highlighting the key sections for each.

definitions.cfg

	#
	# Global define to activate specific features
	#

	// Enable debug
	#!define WITH_DEBUG
	// Enable TLS
	#!define WITH_TLS
	// Enable NAT
	##!define WITH_NAT
	// Enable RTPENGINE
	#!define WITH_RTPENGINE
	// Enable call dispatching
	#!define WITH_DISPATCHER
	// Enable XHTTP module
	#!define WITH_XHTTP
	// Enable Antiflood
	##!define WITH_ANTIFLOOD
	// Enable Authentication
	##!define WITH_AUTH
	// Enable SQLOPS
	#!define WITH_SQLOPS
	// Enable MySQL
	#!define WITH_MYSQL

	# Custom parameters definitions
	#
	#!substdef "!SERVER_HEADER!imsip/2.0.0!g"
	#!substdef "!UA_HEADER!imsip/2.0.0!g"
	#
	#!substdef "!SIP_PORT!5060!g"
	#!substdef "!SIPS_PORT!5061!g"
	#!substdef "!SIP_INTERNAL_PORT!5065!g"
	#!substdef "!SIPS_INTERNAL_PORT!5062!g"
	#!substdef "!HTTP_PORT!8090!g"
	#!substdef "!PRIVATE_IP4_ADDR!172.31.8.126!g"
	#!substdef "!PUBLICIP4_ADDR!44.195.24.234!g"
	#
	#!substdef "!DB_USER!kamailio!g"
	#!substdef "!DB_PASS!kama1l1@SBC!g"
	#!substdef "!DB_HOST!localhost!g"
	#!substdef "!DB_PORT!3306!g"
	#
	#!substdef "!DISPATCHER_LIST!/etc/kamailio/dispatcher.list!g"
	#!substdef "!TLS_CONFIG!/etc/kamailio/tls.cfg!g"

Among the definitions are the ports on which kamailio will listen to manage traffic in the different directions, divided between internal interface and external interface. The parameter PUBLICIP4_ADDR corresponds to the public EIP address configured as advertise IP.

aliases.cfg

	#
	# Custom server alias
	#

	alias="LB-Net-54rte5r15a3ff7bf.elb.us-east-1.amazonaws.com"
	alias="172.31.8.126"
	alias="44.195.24.234"

Here I have entered the IP addresses and DNS record name of the Network Load Balancer. You can enter additional aliases if you have specific domains to manage.

tls.cfg

	#
	# Kamailio TLS Configuration File
	#

	[server:default]
	method = TLSv1.2+
	verify_certificate = no
	require_certificate = no
	private_key = /etc/kamailio/cert/private.key
	certificate = /etc/kamailio/cert/mydomaincertificate.crt
	ca_list = /etc/kamailio/cert/SectigoRSADomainValidationSecureServerCA.crt

	[client:default]
	verify_certificate = no
	require_certificate = no
	ca_list = /etc/kamailio/cert/SectigoRSADomainValidationSecureServerCA.crt

The tls.cfg file contains references to the certificate, private key and rootCA to be able to manage the TLS handshake both as a server and as a client.

dispatcher.list

	# group sip addresses of your * units
	# setid(int) destination(sip uri) flags(int,opt) priority(int,opt) attrs(str,opt)
	1 sip:172.31.6.60:5065 0 0 duid=mediaserver1
	1 sip:172.31.6.61:5065 0 0 duid=mediaserver2

The dispatcher.list file contains the destinations to which kamailio will forward calls based on their availability.

kamailio.cfg

In the file kamailio.cfg there is the execution logic of kamailio, here I show you the main parts for the operation in SBC mode, but you can find the complete file at the link above by clicking on the name.

Let’s start from the “Global Parameters” configuration section where I configured the IPs and ports on which kamailio must listen, separating the internal and external interfaces. The internal interfaces report only the private IP and the port, while the interfaces dedicated to external communication on the Internet also report the public IP as advertise address. In this way all SIP traffic to and from the Internet will use the external interface (depending on the transport protocol indicated by the caller), while traffic on the internal subnet will use the internal interface and kamailio will add two Record-Routes in the message header, one for each interface crossed by the message.

	## Listen on UDP
	listen=udp:PRIVATE_IP4_ADDR:SIP_PORT advertise PUBLICIP4_ADDR:SIP_PORT name "extifudp"
	listen=udp:PRIVATE_IP4_ADDR:SIP_INTERNAL_PORT name "intifudp"

	## Listen on TCP
	listen=tcp:PRIVATE_IP4_ADDR:SIP_PORT advertise PUBLICIP4_ADDR:SIP_PORT name "extiftcp"
	listen=tcp:PRIVATE_IP4_ADDR:SIP_INTERNAL_PORT name "intiftcp"

	#!ifdef WITH_TLS
	enable_tls=yes

	## Listen on TLS Port
	listen=tls:PRIVATE_IP4_ADDR:SIPS_PORT advertise PUBLICIP4_ADDR:SIPS_PORT name "extiftls"
	listen=tls:PRIVATE_IP4_ADDR:SIPS_INTERNAL_PORT name "intiftls"

	/* upper limit for TLS connections */
	tls_max_connections=2048
	#!endif

	/* life time of TCP connection when there is no traffic
	 * - a bit higher than registration expires to cope with UA behind NAT */
	tcp_connection_lifetime=3605

	/* upper limit for TCP connections (it includes the TLS connections) */
	tcp_max_connections=2048

	#!ifdef WITH_XHTTP
	tcp_accept_no_cl=yes
	## Listen on HTTP Port
	listen=tcp:PRIVATE_IP4_ADDR:HTTP_PORT
	#!endif

With the current configuration kamailio is listening on the following ports:

  • extifudp port 5060 UDP advertise 44.195.24.234
  • extiftcp port 5060 TCP advertise 44.195.24.234
  • extiftls port 5061 TCP/TLS advertise 44.195.24.234
  • intifudp port 5065 UDP
  • intiftcp port 5065 TCP
  • intiftls port 5062 TCP/TLS

Additionally, it is important to note the parameters configured for the dispatcher module, where ds_default_sockname is configured as intifudp and represents the interface used by the dispatcher module to forward traffic to the final destinations, which are on the internal subnet.

	#!ifdef WITH_DISPATCHER
	modparam("dispatcher", "list_file", "DISPATCHER_LIST")
	modparam("dispatcher", "ds_default_sockname", "intifudp")
	modparam("dispatcher", "ds_ping_method", "OPTIONS")
	modparam("dispatcher", "ds_ping_interval", 30)
	modparam("dispatcher", "ds_ping_from", "sip:PRIVATE_IP4_ADDR")
	modparam("dispatcher", "ds_ping_reply_codes", "code=200;code=484")
	modparam("dispatcher", "ds_probing_mode", 1)
	modparam("dispatcher", "ds_probing_threshold", 1)
	modparam("dispatcher", "flags", 2)
	modparam("dispatcher", "xavp_dst", "_dsdst_")
	modparam("dispatcher", "xavp_ctx", "_dsctx_")
	#!endif

Now let’s get to the routing logic. First we check if the INVITE is directed to the system itself using the is_myself("$ru") function and then we check if the INVITE comes from a local IP or from the Internet using the is_ip_rfc1918("$si") function which evaluates if the source address of the message is a private IP. If the INVITE is not directed to the system itself, it means that we are in the presence of an outgoing call and the query checks if there is a specific configuration in the database with the protocol to be used in outgoing calls. After these initial evaluations, control then passes to the RTPENGINE block, which is finally also called on the onreply_route.

	#!ifdef WITH_SQLOPS
	  if (is_method("INVITE")) {
		if (is_myself("$ru")) { 
		  if (is_ip_rfc1918("$si")) {
			$dlg_var(location) = "int";
		  }
		}
		else {
		  if (sql_xquery("cb", "select location, sipproto, rtpproto from routing where sipdomain = '$rd'", "ra") == 1) {
			$dlg_var(location) = $xavp(ra=>location);
			$dlg_var(sipproto) = $xavp(ra=>sipproto);
			$dlg_var(rtpproto) = $xavp(ra=>rtpproto);
			sql_result_free("ra");
		  }
		}
	  }
	#!endif

	#!ifdef WITH_RTPENGINE
	  route(RTPENGINE);
	#!endif

The route[RTPENGINE] builds the list of parameters to pass to the rtpengine_manage() function based on the direction of the message and the protocol in use. Rules are applied in presence of SDP in the message, i.e. on the INVITE method and related replay and on the ACK method. So if we have an incoming INVITE with SDP, the logic executes the rtpengine_manage() function on the INVITE and on the related 200 OK (which will contain the SDP to negotiate the codecs), if instead we are in the presence of an INVITE without SDP, the logic executes the rtpengine_manage() function on the 200 OK (which must contain the SDP) and on the ACK (which will in turn contain the SDP to negotiate the codecs).
The list of parameters passed to the rtpengine_manage() function is built as $var(rtp_param) = “replace-origin replace-session-connection " + $dlg_var(rtp_direction) + " " + $dlg_var(rtp_media); according to the direction of the message and the type of media you want to handle. In short, on a TLS INVITE with SDP/SRTP coming from the Internet and directed to an internal MediaServer, the list of parameters is “replace-origin replace-session-connection direction=external direction=internal RTP”, while on 200 OK the parameters are “replace-origin replace-session-connection direction=internal direction=external”.

	#!ifdef WITH_RTPENGINE
	route[SET_RTP_DIRECTION] {
	  #  If the Source IP is an RFC1918 address, it's coming from an internal endpoint, so the call is going out
	  if (is_ip_rfc1918("$si")) {
		if ($dlg_var(location) == "int") {
		  $dlg_var(rtp_direction) = "direction=internal direction=internal";
		} else {
		  $dlg_var(rtp_direction) = "direction=internal direction=external";
		}
	  } else {
		$dlg_var(rtp_direction) = "direction=external direction=internal";
	  }
	}

	route[SET_RTP_MEDIA] {
	  $dlg_var(rtp_media) = "RTP";
	  if (is_ip_rfc1918("$si")) {
		if ($dlg_var(rtpproto) != "") {
		  $dlg_var(rtp_media) = $(dlg_var(rtpproto){s.toupper});
		} else {
		  $dlg_var(rtp_media) = "";
		}
	  }
	}

	route[BUILD_RTPENGINE_PARAMETER] {
	  route(SET_RTP_DIRECTION);   # returns $dlg_var(rtp_direction)
	  route(SET_RTP_MEDIA);   # returns $dlg_var(rtp_Media)
	  
	  $var(rtp_param) = "replace-origin replace-session-connection " + $dlg_var(rtp_direction) + " " + $dlg_var(rtp_media);
	  xlog("L_INFO", "[RTPENGINE]: Param: [$var(rtp_param)]\n");
	  rtpengine_manage($var(rtp_param));
	}

	route[RTPENGINE] {
		if ( is_method("INVITE") ) {
			if ( sdp_content() ) {
				xlog("L_INFO", "[RTPENGINE] Message has SDP\n");
				route(BUILD_RTPENGINE_PARAMETER);
			}
		}

		if ( is_method("ACK") ) {
			if ( sdp_content() ) {
				xlog("L_INFO", "[RTPENGINE] Message has SDP\n");
				route(BUILD_RTPENGINE_PARAMETER);
			}
		}
	}
	#!endif

Once the rtpengine features have been activated, the call management proceeds following the route[RELAY], in particular if the call is directed to the system itself it is routed to the MediaServer via the dispatcher module, if instead it is a call to external destinations it will be forwarded outside via the interface corresponding to the transport protocol that has been chosen.

Kamailio SBC Open Source at work

Flow of an incoming call from the Internet and forwarded to the Media Server.

kamailio-sbc

Flow of an outgoing call, generated by the MediaServer and directed to the Internet.

kamailio-sbc

As you can see from the traces, data flows are decupled, both SIP and RTP, between the external and internal interfaces of Kamailio.

Here also a flow of a call coming from the Internet and that originally used TLS, as you can see from the original Request-Uri, and that was converted to UDP by Kamailio adding the appropriate Record-Route for the two distinct legs. The call flow shows only the dialogue between the private interface of Kamailio and the Media Server, since the external route is encrypted.

kamailio-sbc