Kamailio e RTPEngine, un SBC Open Source
Un SBC (Session Border Controller) SIP permette di separare il traffico VoIP esterno alla nostra subnet dal traffico generato all’interno della subnet stessa, assicurando i corretti flussi di segnalazione e media tra esterno ed interno e viceversa. Il termine “Border” infatti, sta proprio ad indicare questa funzione, dato che un SBC si trova ai bordi della rete e rappresenta il confine tra una network interna ed Internet. Supponiamo ad esempio di avere un telefono VoIP (che usa il protocollo SIP) all’interno di una LAN e che tale telefono debba ricevere chiamate da un VoIP Provider pubblico, che necessariamente si troverà su Internet. Per poter comunicare direttamente, il telefono dovrebbe avere un IP pubblico e questo rappresenterebbe un problema dal punto di vista della sicurezza della nostra rete. L’alternativa si chiama SBC, che posto tra la LAN e Internet disaccoppia il leg di chiamata pubblico dal Provider alla sua interfaccia pubblica, dal leg di chiamata privato tra la sua interfaccia privata e gli elementi interni alla LAN.
Esistono diversi produttori di SBC, più o meno costosi e più o meno ricchi di funzionalità, ma lo scopo di questo articolo è quello di utilizzare Kamailio con rtpengine per costruire il nostro SBC Open Source. Avevamo già parlato dell’argomento in questo post ed avevamo visto come installare rtpengine qui, usando come sistema operativo CentOS 7. Ora invece vedremo come installare e configurate il tutto su Ubuntu 24.04 e la cosa risulterà molto più semplice di prima.
Prima di iniziare vi descrivo in breve la mia architettura di riferimento, basata su istanze EC2 in AWS e rappresentata nell’immagine qui sotto.
Il Network Load Balancer pubblico è il punto di ingresso del traffico SIP all’interno della VPC, dove sono presenti 2 Public subnet e 2 Private subnet. Le Public subnet ospitano gli SBC che hanno sia IP privato sia Elastic Public IP, mentre le Private subnet ospitano il SIP Proxy ed i Media Server che hanno solo IP privato. Il Network Load Balancer lavora a livello 4 dello stack OSI e si occupa solo ed esclusivamente di ruotare l’INVITE ricevuto verso uno degli SBC disponibili. La sequenza dello scambio dati può essere riassunta così:
- Il Network Load Balancer riceve l’INVITE sul suo Listener e lo inoltra verso uno degli SBC selezionato all’interno del Target Group.
- L’SBC riceve l’INVITE sulla sua interfaccia pubblica ed è configurato per poter accettare traffico SIP su UDP, TCP e TLS.
- Ricevuto l’INVITE da Internet, l’SBC lo inoltra verso uno dei SIP Proxy attraverso la sua interfaccia privata in UDP, aggiungendo i Record-Route necessari a rimanere nel path di chiamata e modificando opportunamente la parte media all’interno del SDP in modo da poter separare i flussi audio tra leg pubblico e leg privato.
- Il SIP Proxy inoltra l’INVITE verso uno dei suoi Media Server, il quale risponde alla chiamata con il 200 OK contenente nel SDP il suo indirizzo IP privato.
- L’SBC riceve il 200 OK sull’interfaccia privata, ne modifica la parte media all’interno del SDP inserendo il suo indirizzo IP pubblico, e lo inoltra all’esterno verso il chiamante.
- La negoziazione si chiude con l’ACK che dal chiamante arriva al Media Server, ripercorrendo il path di chiamata dichiarato attraverso le varie Record-Route.
- La comunicazione media si stabilisce tra chiamante ed interfaccia pubblica del SBC e tra interfaccia privata del SBC e Media Server, i due flussi audio sono a tutti gli effetti separati e distinti.
Come considerazione generale, possiamo notare che in uno scenario di questo tipo il Network Load Balancer di AWS interviene sono sul routing iniziale dell’INVITE essendo poi escluso dal path di chiamata; inoltre la presenza del SBC ci permette di gestire il NAT, effettuare conversioni TLS -> SIP e viceversa, gestire RTP relay e conversione SRTP -> RTP e viceversa, supportare eventuale RTP transcoding, aumentare la sicurezza architetturale tramite il Topology hiding.
Scenario di riferimento
Andiamo a vedere ora in dettaglio la configurazione di Kamailio e rtpengine. Prima però ho voluto semplificare lo scenario, limitando l’architettura ad un solo SBC, in modo da concentrare l’attenzione su un singolo elemento.
Come indicato nella figura qui sopra, l’SBC è installato su un’istanza EC2 di AWS con un Elstic IP pubblico assegnato, in modo che il sistema sia visibile su Internet sulle porte 5060 UDP/TCP e 5061 TLS, mentre verso l’interno è attivo sulla porta 5065.
Installazione
Il setup di Kamailio su Ubuntu 24.04 è molto rapido; infatti dopo aver avviato l’istanza e configurato opportunamente EIP, Routing e Security Group si può procedere con l’installazione dei pacchetti.
apt-get update
apt upgrade
apt install kamailio kamailio-mysql-modules kamailio-tls-modules kamailio-utils-modules kamailio-phonenum-modules rtpengine mysql-server sngrep
Fatto ciò, abbiamo installato kamailio, rtpengine e mysql server, possiamo quindi procedere con la configurazione.
Configurazione
Dividiamo la configurazione in tre steps: rtpengine, database e kamailio.
RTPEngine
Il file di configurazione di rtpengine è /etc/rtpengine/rtpengine.conf, l’unico parametro da cambiare è interface ed eventualmente port-min e port-max in base al range di porte RTP che si decide di voler usare.
interface = internal/172.31.8.126;external/172.31.8.126!44.195.24.234
Abbiamo definito due interfacce logiche, una denominata internal che usa l’interfaccia privata ed una seconda denominata external che usa sempre l’interfaccia privata (essendo l’unica allocata realmente sull’istanza EC2), ma annuncia l’IP pubblico associato all’interfaccia. In questo modo possiamo usare l’opzione direction per dire ad rtpengine che il traffico in arrivo da Internet e diretto verso il Media Server deve seguire la direzione external -> internal, mentre quello nella direzione opposta deve seguire internal -> external. Stiamo dicendo che per i messaggi SIP in arrivo da Internet e diretti verso l’interno della VPC rtpengine andrà a sovrascrivere l’SDP con l’indirizzo locale, mentre per i messaggi diretti verso Internet rtpengine andrà a sostituire l’indirizzo locale con quello pubblico.
MySQL server
Kamailio mette a disposizione il comando kamdbctl create per creare il database kamailio con tutte le tabelle già predisposte, l’unica cosa da fare prima di eseguire il comando è configurare i parametri necessari nel file /etc/kamailio/kamctlrc. In realtà in questo scenario kamailio non fa uso delle sue tabelle, ma a noi serve una tabella dove inserire i dati per caratterizzare le eventuali destinazioni esterne da chiamare. Andiamo quindi a creare la tabella routing (potete scegliere il nome che preferite, basta poi allineare la configurazione).
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
Per una migliore gestione ho diviso la configurazione di kamailio su più files tutti in /etc/kamailio.
- definitions.cfg: contiene le definizioni statiche come ad esempio moduli da attivare, porte su cui fare il binding, user e password per il database
- aliases.cfg: contiene gli eventuali alias che kamailio deve gestire come proprie uri
- tls.cfg: file con certificati e chiave privata per la gestione di TLS
- dispatcher.list: lista delle destinazioni gestite dal modulo dispatcher
- kamailio.cfg: file di configurazione principale con i vari blocchi di logica
Vi riporto di seguito i dettagli indicando per ciascun file le parti principali della configurazione.
#
# 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"
Tra le definizioni ci sono le porte su cui kamailio si metterà in ascolto per gestire il traffico nelle varie direzioni, suddivise tra interfaccia interna ed interfaccia esterna. Il parametro PUBLICIP4_ADDR corrisponde all’indirizzo EIP pubblico configurato come advertise IP.
#
# Custom server alias
#
alias="LB-Net-54rte5r15a3ff7bf.elb.us-east-1.amazonaws.com"
alias="172.31.8.126"
alias="44.195.24.234"
Qui ho inserito gli indirizzi IP e il DNS record name del Network Load Balancer. Potete inserire ulteriori alias se avete domini specifici da gestire.
#
# 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
Il file tls.cfg contiene i riferimenti a certificato, private key e rootCA per poter gestire l’handshake TLS sia come server che come client.
# 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
Il file dispatcher.list contiene le destinazioni verso cui kamailio inoltrerà le chiamate sulla base della loro disponibilità.
Nel file kamailio.cfg c’è la logica di esecuzione di kamailio, qui vi riporto le parti principali per il funzionamento in modalità SBC, ma trovate il file completo al link sopra facendo click sul nome.
Iniziamo dalla sezione di configurazione dei “Global Parameters” in cui ho configurato IP e porte su cui kamailio deve mettersi in listening, separando interfaccia interna ed esterna. Le interfacce interne riportano il solo IP privato e la porta, mentre le interfacce dedicate alla comunicazione esterna su Internet riportano anche l’IP pubblico come advertise address. In questo modo tutto il traffico SIP da e verso Internet utilizzerà l’interfaccia esterna (a seconda del protocollo di trasporto indicato dal chiamante), mentre il traffico sulla subnet interna utilizzerà l’interfaccia interna e kamailio aggiungerà due Record-Route nel message header, una per ciascuna interfaccia attraversata dal messaggio.
## 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
Con la configurazione indicata qui sopra kamailio è in listening sulle seguenti porte:
- extifudp porta 5060 UDP advertise 44.195.24.234
- extiftcp porta 5060 TCP advertise 44.195.24.234
- extiftls porta 5061 TCP/TLS advertise 44.195.24.234
- intifudp porta 5065 UDP
- intiftcp porta 5065 TCP
- intiftls porta 5062 TCP/TLS
Inoltre, è importante notare i parametri configurati per il dispatcher module, dove ds_default_sockname è configurato come intifudp e rappresenta l’interfaccia usata dal modulo dispatcher per inoltrare traffico alle destinazioni finali, che si trovano sulla subnet interna.
#!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
Veniamo ora alla logica di routing vera e propria. Per prima cosa controlliamo se l’INVITE è diretto al sistema stesso tramite la funzione is_myself("$ru") e poi controlliamo se l’INVITE proviene da un IP locale o da Internet usando la funzione is_ip_rfc1918("$si") che valuta se l’indirizzo sorgente del messaggio è un IP privato. Nel caso in cui l’INVITE non è diretto al sistema stesso, significa che siamo in presenza di una chiamata uscente e la query controlla se esiste una configurazione specifica nel database relativamente al protocollo da usare in uscita. Fatte queste prime valutazioni il controllo passa poi al blocco RTPENGINE, che viene poi chiamato anche sulla 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
La route[RTPENGINE] costruisce la lista dei parametri da passare alla funzione rtpengine_manage() sulla base della direzione del messaggio e del protocollo in uso. L’operazione viene effettuata in presenza di SDP nel messaggio, cioè sul metodo INVITE e relative replay e sul metodo ACK. Quindi se abbiamo un INVITE con SDP in ingresso, la logica esegue la funzione rtpengine_manage() sull’INVITE e sul relativo 200 OK (che conterrà l’SDP per negoziare i codec), se invece siamo in presenza di un INVITE senza SDP la logica esegue la funzione rtpengine_manage() sul 200 OK (che dovrà contenere l’SDP) e sul ACK (che conterrà a sua volta l’SDP per negoziare i codec).
La lista dei parametri passati alla funzione rtpengine_manage() è cotruita come $var(rtp_param) = “replace-origin replace-session-connection " + $dlg_var(rtp_direction) + " " + $dlg_var(rtp_media); sulla base della direzione del messaggio e del tipo di media che si vuole gestire. In poche parole, su un INVITE TLS con SDP/SRTP in arrivo da Internet e diretto verso un MediaServer interno, l’elenco dei parametri è “replace-origin replace-session-connection direction=external direction=internal RTP”, mentre sul 200 OK i parametri sono “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
Dopo che le funzionalità di rtpengine sono state attivate, la gestione della chiamata procede sulla base di quanto stabilito sulla route[RELAY], in particolare se la chiamata è diretta al sistema stesso viene ruotata al MediaServer tramite il modulo dispatcher, se invece è una chiamata verso destinazioni esterne verrà inoltrata fuori tramite l’interfaccia corrispondente al protocollo di trasporto che si è scelto.
Kamailio SBC Open Source all’opera
Flusso di una chiamata in arrivo da Internet e inoltrata al Media Server.
Flusso di una chiamata uscente, generata dal MediaServer e diretta verso Internet.
Come potete vedere dai traces è evidente la separazione dei flussi, sia SIP che RTP, tra le interfacce esterne e interne di Kamailio.
Concludo con un flusso di una chiamata proveniente da Internet e che all’origine ha usato TLS, come è evidente dalla Request-Uri originale e che è stata convertita in UDP da Kamailio che ha inserito le opportune Record-Route per i due leg distinti. Il call flow mostra solo il dialogo tra interfaccia privata di Kamailio e Media Server, dato che la tratta esterna è criptata.