Kamailio WebRTC SIP Server


The purpose of this article is to demo the process of using Kamailio + RTP Engine to enable SIP-based WebRTC call to a traditional SIP UA like Xlite. Kamailio Will thus provide not only call routing but also NATing, TLS and WebSocket support for webrtc endpoints. For this bridging of SRTP from WebRTC endpoint like JSSIP to RTP for SIP UA like Xlite, we will use the RTP engine.

Kamailio

Kamailio’s modules on WebSocket, TLS, NATHelper help it to support WebSocket based SIP which the default Kamailio configuration doesn’t. Snippets from Kamailio config to support webrtc endpoints is below. A full kamailio config is present here https://github.com/altanai/kamailioexamples/blob/master/webrtc_to_webrtc_ws/websocket_tls_webrtc_kamailio.cfg

loadmodule "tm.so"
loadmodule "sl.so"
loadmodule "rr.so"
loadmodule "pv.so"
loadmodule "maxfwd.so"
loadmodule "usrloc.so"
loadmodule "textops.so"
loadmodule "siputils.so"
loadmodule "xlog.so"
loadmodule "sanity.so"
loadmodule "ctl.so"
loadmodule "kex.so"
loadmodule "corex.so"
loadmodule "tls.so"
loadmodule "xhttp.so"
loadmodule "websocket.so"
loadmodule "nathelper.so" 

Configuration of the important modules

TLS module

To provide the SSL support which webrtc endpoints require Certificate Authority CA and provate certs signed by it

creating file paths

mkdir certs
mkdir certs/private
mkdir certs/newcerts
touch certs/index.txt
echo 01 >certs/serial
echo 01 >certs/crlnumber

list the files

/home/ubuntu/certs# ls
crlnumber  index.txt  newcerts  private  serial

Create CA private key

openssl genrsa -out certs/private/cakey.pem 2048
chmod 600 certs/private/cakey.pem

Create CA self signed certificate

openssl req -out certs/cacert.pem -x509 -new -key certs/private/cakey.pem

Create server / client certificate, a private key (by name privkey.pem)

openssl req -out kamailio1_cert_req.pem -new -nodes
openssl ca -in kamailio1_cert_req.pem -out kamailio1_cert.pem

Output should be like

Certificate is to be certified until Jun 25 11:02:41 2020 GMT (365 days)
Sign the certificate? [y/n]:y
1 out of 1 certificate requests certified, commit? [y/n]y
Write out database with 1 new entries
Data Base Updated

and files genrated shoudl look like

/home/ubuntu# ls 
 certs  kamailio1_cert.pem kamailio1_cert_req.pem privkey.pem

Copy the newly created certs to their respective paths

mkdir /etc/pki/CA/
cp kamailio1_cert.pem /etc/pki/CA/
cp privkey.pem /etc/pki/CA/

Make list of ca certs by finidng all cacerts accross root firectory and appending them to a catlist pem

find / -name cacert.pem
cat /usr/share/doc/libssl-doc/demos/cms/cacert.pem >> /home/ubuntu/catlist.pem
cat /usr/share/doc/libssl-doc/demos/smime/cacert.pem >> /home/ubuntu/catlist.pem
cat /home/ubuntu/kamailio_source_code/misc/tls-ca/rootCA/cacert.pem >> /home/ubuntu/catlist.pem
...
cp /home/ubuntu/catlist.pem /etc/pki/CA/

update kamailio.cfg

#!ifdef WITH_TLS
enable_tls=1
#!endif
...
modparam("tls", "tls_method", "SSLv23")
modparam("tls", "certificate", "/etc/pki/CA/kamailio1_cert.pem")
modparam("tls", "private_key", "/etc/pki/CA/privkey.pem")
modparam("tls", "ca_list", "/etc/pki/CA/calist.pem")

Websocket module

Websocket is considered a transport option just as TCP or UDP in kamailio config , hence just as one defines IP addr and ports for TCP, UDP protocol , we need to define the same for WS or WSS

#!substdef "!MY_WS_ADDR!tcp:MY_IP_ADDR:MY_WS_PORT!g"
#!substdef "!MY_WSS_ADDR!tls:MY_IP_ADDR:MY_WSS_PORT!g"
...
listen=MY_IP_ADDR
#!ifdef WITH_WEBSOCKETS
listen=MY_WS_ADDR
#!ifdef WITH_TLS
listen=MY_WSS_ADDR
#!endif
#!endif

check if port in R-URI meant for ws or wss, did not receive websocket or secure websocket

if (($Rp == MY_WS_PORT || $Rp == MY_WSS_PORT) && !(proto == WS || proto == WSS)) {
    xlog("L_WARN", "SIP request received on $Rp\n");
    sl_send_reply("403", "Forbidden");
    exit;
}

request_route for websocket , included checking is client is behind NAT using nat_uac_test methods from NAThelper. If it is then for REGISTER methods do fix_nated_register and for other add_contact_alias

#!ifdef WITH_WEBSOCKETS
if (nat_uac_test(64)) {
	# NAT traversal  WebSocket
	force_rport();
	if (is_method("REGISTER")) {
		fix_nated_register();
	} else {
		if (!add_contact_alias()) {
		xlog("L_ERR", "Error aliasing contact <$ct>\n");
		sl_send_reply("400", "Bad Request");
		exit;
		}
	}
}
#!endif

RTPengine

RTP relay and NAT helps with RTP packets

For detailed steps goto https://telecom.altanai.com/2018/04/03/rtp-engine-on-kamailio-sip-server/

Dependencies

apt-get remove rtpproxy
sudo apt install debhelper iptables-dev libcurl4-openssl-dev libglib2.0-dev libxmlrpc-core-c3-dev libhiredis-dev markdown build-essential:native

Source

git clone https://github.com/sipwise/rtpengine.git
cd rtpengine
 ./debian/flavors/no_ngcp

Run

rtpengine --interface=54.86.35.95 --listen-ng=25061 --listen-cli=25062 --foreground --log-stderr --listen-udp=2222 --listen-tcp=25060

Integrate with kamailio using

loadmodule "rtpengine.so"
...
modparam("rtpengine", "rtpengine_sock", "udp:localhost:7722")
modparam("nathelper", "received_avp", "$avp(s:rcv)")
...
tbd

JSSIP

Like most other WebRTC libraries , JSSIP is event driven and provides provide core WEBRTC API like getUserMedia and RTP PeerConnection providing STUN,ICE,DTLS, SRTP features. It also integrated with rtcninja to provide cross browser accessibility. The differentiators with JSSIP lies in the fact that it supports SIP stack over websockets

JSSIP WebRTC client for kamailio

For tghe JSSIP default cllient UI and library one can use CDN based https://cdnjs.cloudflare.com/ajax/libs/jssip/3.1.2/jssip.min.js or can take a pull from JSSIP repo and build urself using gulp https://github.com/versatica/JsSIP.

Instantiate JSSIP websocket interface with kamailio IP

var socket = new JsSIP.WebSocketInterface('wss://<kamailio_ip>:443');

Add configuration for registeration . Note if not using kamailio as proxy to SBC, it is recommended to add regiseteration features to provide user reachability for incoming calls and NAT pings

 var configuration = {
   sockets  : socket,
   uri      : 'sip:username@example.com',
   password : 'password'
 };

create UA and start

var ua = new JsSIP.UA(configuration);
ua.start();

SIP over WEBSOCKET messages and kamailio processing

REGISTER sip JSSIP UA with user altanai, domina voiptelcom.com

REGISTER sip:voiptelco.com SIP/2.0
Via: SIP/2.0/WSS 830p2l39g8bg.invalid;branch=z9hG4bK242397
Max-Forwards: 69
To: 
From: ;tag=3jaad0q8l8
Call-ID: fvn2cd1b9gqh6kd7nqdpj5
CSeq: 1 REGISTER
Contact: ;+sip.ice;reg-id=1;+sip.instance="";expires=600
Expires: 600
Allow: INVITE,ACK,CANCEL,BYE,UPDATE,MESSAGE,OPTIONS,REFER,INFO
Supported: path,gruu,outbound
User-Agent: JsSIP 3.1.2
Content-Length: 0

Processed by kamailio using REGISTRAR route block

route(REGISTRAR);
..
route[REGISTRAR] {
	if (is_method("REGISTER")) {
		if (!save("location")) {
			sl_reply_error();
		}
		exit;
	}
}
SIP/2.0 200 OK
Via: SIP/2.0/WSS 830p2l39g8bg.invalid;branch=z9hG4bK242397;rport=19035;received=x.x.x.x
To: ;tag=4dad943d40a0a309c33d64467664aa30.f6d3
From: ;tag=3jaad0q8l8
Call-ID: fvn2cd1b9gqh6kd7nqdpj5
CSeq: 1 REGISTER
Contact: ;expires=600;received="sip:x.x.x.x:19035;transport=ws";pub-gruu="sip:altanai@voiptelco.com;gr=urn:uuid:80fa65e7-1cd7-4e40-bbee-c07f7a1ae9a5";temp-gruu="sip:uloc-5d260578-4b58-c-eeac6bd6@voiptelco.com;gr";+sip.instance="";reg-id=1
Server: kamailio (5.2.3 (x86_64/linux))
Content-Length: 0

INVITE from user1 altanai to john, notice that “To” header doesnt have tag. This will be handy for recognizing whether it is first message of dialog offer and in-dialog message such as ACK , RE-INVITE , BYE etc

INVITE sip:john@voiptelco.com SIP/2.0
Via: SIP/2.0/WSS ipoct61ao12v.invalid;branch=z9hG4bK4220209
Max-Forwards: 69
To: sip:john@voiptelco.com
From: sip:altanai@voiptelco.com;tag=2q0lecmbsn
Call-ID: s8bnv5869fp68d1ju8c1
CSeq: 1799 INVITE
Contact: 
Content-Type: application/sdp
Session-Expires: 90
Allow: INVITE,ACK,CANCEL,BYE,UPDATE,MESSAGE,OPTIONS,REFER,INFO
Supported: timer,gruu,ice,replaces,outbound
User-Agent: JsSIP 3.1.2
Content-Length: 1823

with SDP containing codecs and ICE details. Supporting audio over UDP/ TLS/RTL /SAVPF . Codecs beings

  • 111 OPUS
  • 103 ISAC/16000
  • 104 ISAC/32000
  • 9 G722
  • 0 PCMU / G.711u narrowband
  • 8 PCMA / G.711
  • 106 , 105 , 13 – CN / comfort noise
  • 110 , 112 , 113 , 126 – telephone-event / DTMF
v=0
o=- 4779000713447952953 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0
a=msid-semantic: WMS SFIXFrpsOUskJ5JQhp1mIARlDk6S3hVFTOBb
m=audio 55839 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
c=IN IP4 192.168.0.3
a=rtcp:9 IN IP4 0.0.0.0
a=candidate:3802297132 1 udp 2122260223 192.168.0.3 55839 typ host generation 0 network-id 1 network-cost 10
a=candidate:2887880668 1 tcp 1518280447 192.168.0.3 9 typ host tcptype active generation 0 network-id 1 network-cost 10
a=ice-ufrag:0PIq
a=ice-pwd:i7ccvGPXLDO5JqMwbCUqMcyN
a=ice-options:trickle
a=fingerprint:sha-256 AB:50:70:E3:57:E3:0C:7B:61:3B:03:5B:0F:54:14:14:9C:49:50:16:07:DC:E7:09:3E:4D:B5:A0:2B:EC:84:A1
a=setup:actpass
a=mid:0
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=sendrecv
a=msid:SFIXFrpsOUskJ5JQhp1mIARlDk6S3hVFTOBb e803836d-249a-4b81-b73f-17e0f08dde5a
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:106 CN/32000
a=rtpmap:105 CN/16000
a=rtpmap:13 CN/8000
a=rtpmap:110 telephone-event/48000
a=rtpmap:112 telephone-event/32000
a=rtpmap:113 telephone-event/16000
a=rtpmap:126 telephone-event/8000
a=ssrc:2800821831 cname:kLWViBLDLVfCXY8x
a=ssrc:2800821831 msid:SFIXFrpsOUskJ5JQhp1mIARlDk6S3hVFTOBb e803836d-249a-4b81-b73f-17e0f08dde5a
a=ssrc:2800821831 mslabel:SFIXFrpsOUskJ5JQhp1mIARlDk6S3hVFTOBb
a=ssrc:2800821831 label:e803836d-249a-4b81-b73f-17e0f08dde5a

100 trying from callee, note the to and from headers remain same for request or responses. This is send automatically by kamailio for INVITE.

SIP/2.0 100 trying -- your call is important to us
Via: SIP/2.0/WSS ipoct61ao12v.invalid;branch=z9hG4bK4220209;rport=17502;received=x.x.x.x
To: sip:john@voiptelco.com
From: sip:altanai@voiptelco.com;tag=2q0lecmbsn
Call-ID: s8bnv5869fp68d1ju8c1
CSeq: 1799 INVITE
Server: kamailio (5.2.3 (x86_64/linux))
Content-Length: 0

180 ringing from Callee, note the addition of contact header

SIP/2.0 180 Ringing
Record-Route: 
Via: SIP/2.0/WSS ipoct61ao12v.invalid;rport=17502;received=x.x.x.x;branch=z9hG4bK4220209
To: sip:john@voiptelco.com;tag=pvm73e3t89
From: sip:altanai@voiptelco.com;tag=2q0lecmbsn
Call-ID: s8bnv5869fp68d1ju8c1
CSeq: 1799 INVITE
Contact: sip:john@voiptelco.com;alias=x.x.x.x~17510~6;gr=urn:uuid:2e560b36-3ea8-41fd-80e3-ede66babb8a7
Supported: timer,gruu,ice,replaces,outbound
Content-Length: 0

200 OK with SDP

SIP/2.0 200 OK
Record-Route: 
Via: SIP/2.0/WSS ipoct61ao12v.invalid;rport=17502;received=x.x.x.x;branch=z9hG4bK4220209
To: sip:altanai@voiptelco.com;tag=pvm73e3t89
From: sip:john@voiptelco.com ;tag=2q0lecmbsn
Call-ID: s8bnv5869fp68d1ju8c1
CSeq: 1799 INVITE
Contact: sip:john@voiptelco.com;alias=x.x.x.x~17510~6;gr=urn:uuid:2e560b36-3ea8-41fd-80e3-ede66babb8a7
Session-Expires: 90;refresher=uas
Supported: timer,gruu,ice,replaces,outbound
Content-Type: application/sdp
Content-Length: 1477
v=0
o=- 4562215268128860297 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0
a=msid-semantic: WMS oxU77z9z9RfNL4CayvM1cMJKI0r7u6ZdqLBd
m=audio 55380 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
c=IN IP4 192.168.0.3
a=rtcp:9 IN IP4 0.0.0.0
a=candidate:3802297132 1 udp 2122260223 192.168.0.3 55380 typ host generation 0 network-id 1 network-cost 10
a=ice-ufrag:40h+
a=ice-pwd:6zJo50N7Bb2mqnrHq+jniukk
a=ice-options:trickle
a=fingerprint:sha-256 8A:F9:BE:8D:8A:80:FF:8C:89:3D:3A:D2:A1:36:B2:EC:11:53:81:7E:F4:53:E7:40:1E:B9:1E:A2:0F:D4:EA:2E
a=setup:active
a=mid:0
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=sendrecv
a=msid:oxU77z9z9RfNL4CayvM1cMJKI0r7u6ZdqLBd cb6da6b5-d5b8-460e-88bc-1458ebc718e6
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:106 CN/32000
a=rtpmap:105 CN/16000
a=rtpmap:13 CN/8000
a=rtpmap:110 telephone-event/48000
a=rtpmap:112 telephone-event/32000
a=rtpmap:113 telephone-event/16000
a=rtpmap:126 telephone-event/8000
a=ssrc:3489110087 cname:oZ2LjwJD385qPHHH

Kamailio handling replies using reply_route

onreply_route {
     if (nat_uac_test(64)) {
         add_contact_alias();
     }
 }

Sending ACK.

ACK sip:john@voiptelco.com;alias=x.x.x.x~17510~6;gr=urn:uuid:2e560b36-3ea8-41fd-80e3-ede66babb8a7 SIP/2.0

Route: 
Via: SIP/2.0/WSS ipoct61ao12v.invalid;branch=z9hG4bK1876245
Max-Forwards: 69
To: sip:altanai@voiptelco.com ;tag=pvm73e3t89
From: sip:john@voiptelco.com;tag=2q0lecmbsn
Call-ID: s8bnv5869fp68d1ju8c1
CSeq: 1799 ACK
Allow: INVITE,ACK,CANCEL,BYE,UPDATE,MESSAGE,OPTIONS,REFER,INFO
Supported: outbound
User-Agent: JsSIP 3.1.2
Content-Length: 0

since ACK is a Within dialog message and sequential request withing a dialog should take the path determined by record-routing, we first check if it has to tag. Having a to tag validates that it is a in-dialog request .

After this validate if is loose_route() and has no destination URI $du , then try to add rui alias using handle_ruri_alias( ), if that fails, reject the request.

If it is not loose_route() and method is ACK then check if the ACK matches a transaction t_check_trans() ie is stateful. If it is then relay otherwise reject.

route(WITHINDLG);
...
route[WITHINDLG] {
	if (has_totag()) {
		if (loose_route()) {
			if ($du == "") {
				if (!handle_ruri_alias()) {
				xlog("L_ERR", "Bad alias <$ru>\n");
				sl_send_reply("400", "Bad Request");
				exit;
				}
			}
			route(RELAY);
		} else {
			if ( is_method("ACK") ) {
				if ( t_check_trans() ) {
				t_relay();
				exit;
				} else {
				exit;
				}
			}
			sl_send_reply("404", "Not Found");
		}
		exit;
	}
}

Also required to convert ICE packet fromWebRTC to non ICE for Xlite.

Resources :

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.