In order to keep radio resources free, if a UE doesn’t send or receive data for a predefined threshold, it’ll detach from the network and call back to Idle mode.
If the UE has data to send to the network, the UE will re-attach to the network, whereas if the network has data to send to the UE, it’ll Page the UE in the tracking area it’s currently in, the UE is always listening for it’s identifier (s-TMSI) on the paging channel, and if it hears it’s identifier called, the UE will re-attach.
I’ve also attached a PCAP file of the packet flow between the eNB and the MME.
The next packet is sent from the MME back to the eNB confirming UE is releasing from the network.
UEContextReleaseComplete
Finally after the UE has released it’s radio resources the eNB sends a UEContextReleaseComplete so the MME knows the UE is now in Idle state and will need to be paged.
Recently we saw Open5Gs’s Update Location Answer response putting the Subscribed-Periodic-RAU-TAU-Timer AVP in the top level and not in the AVP Code 1400 (APN Configuration) Diameter payload from the HSS to the MME.
But what exactly does the Subscribed-Periodic-RAU-TAU-Timer AVP in the Update Location Answer response do?
Folks familiar with EUTRAN might recognise TAU as Tracking Area Update, while RAU is Routing Area Update in GERAN/UTRAN (UMTS).
Periodic tracking area updating is used to periodically notify the availability of the UE to the network. The procedure is controlled in the UE by the periodic tracking area update timer (timer T3412). The value of timer T3412 is sent by the network to the UE in the ATTACH ACCEPT message and can be sent in the TRACKING AREA UPDATE ACCEPT message. The UE shall apply this value in all tracking areas of the list of tracking areas assigned to the UE, until a new value is received.
Section 5.3.5 of 24301-9b0 (3GPP TS 24.301 V9.11.0)
So the Periodic Tracking Area Update timer simply defines how often the UE should send a Tracking Area Update when stationary (not moving between cells / tracking area lists).
On a PCM (G.711) RTP packet the payload is typically 160 bytes per packet.
But the total size of the frame on the wire is typically ~214 bytes, to carry a 160 byte payload that means 25% of the data being carried is headers.
This is fine for VoIP services operating over fixed lines, but when we’re talking about VoLTE / IMS and the traffic is being transferred over Radio Access Networks with limited bandwidth / resources, it’s important to minimize this as much as possible.
IMS uses the AMR codec, where the RTP payload for each packet is around 90 bytes, meaning up to two thirds of the packet on the wire (Or in this case the air / Uu interface) is headers.
Using ROHC the size of the headers are cut down to only 4-5 bytes, this is because the IPv4 headers, UDP headers and RTP headers are typically the same in each packet – with only the RTP Sequence number, RTP timestamp IPv4 & UDP checksum and changing between frames.
I modified the Kamailio config allow Transcoding, as I talked about in the post on setting up Transcoding in RTPengine with Kamailio.
Now I had a working Kamailio instance with RTPengine that was transcoding.
So the next step becomes testing the transcoding is working, for this I had two SIPp instances, one to make the calls and once to answer them.
Instance 1
Makes calls to the IP of the Kamailio / RTPengine instance, for this I modified the uac_pcap scenario to playback an RTP stream of a PCMA (G.711 a-law) call to the called party (stored in a pcap file), and made it call the Kamailio instance multiple times based on how many concurrent transcoding sessions I wanted:
Note: NextEPC the Open Source project rebranded as Open5Gs in 2019 due to a naming issue. The remaining software called NextEPC is a branch of an old version of Open5Gs. This post was written before the rebranding.
I’ve been working for some time on Private LTE networks, the packet core I’m using is NextEPC, it’s well written, flexible and well supported.
I joined the Open5Gs group and I’ve contributed a few bits and pieces to the project, including a Python wrapper for adding / managing subscribers in the built in Home Subscriber Server (HSS).
Basic Python library to interface with MongoDB subscriber DB in NextEPC HSS / PCRF. Requires Python 3+, mongo, pymongo and bson. (All available through PIP)
If you are planning to run this on a different machine other than localhost (the machine hosting the MongoDB service) you will need to enable remote access to MongoDB by binding it’s IP to 0.0.0.0:
This is done by editing /etc/mongodb.conf and changing the bind IP to: bind_ip = 0.0.0.0
Starting from today, Australia’s largest carrier will begin disconnecting ISDN services, and all will be shut off entirely by 2022.
When I started working in the industry the question wasn’t how many SIP Channels you wanted, but rather PRI or BRI when it came to trunk lines.
We’d use ISDN dial up to remotely administer systems, and had a bulky ISDN PRI test set for testing lines and BERT.
ISDN Crossovers, QSIG, gateways, ISUP, Bit Error Rate Testing, loopbacks, clock slips, cause code 16, enblock dialing – all things I’m not likely to need to know anymore.
Since ISDN was first rolled out in Australia just over 3 decades ago, to it being switched off from today onward, ISDN has been crazily reliable, and as much as it pains me to admit it, it has availability stats any SIP provider would be envious of.
As the market evolved over the years the smaller telcos realized using ISDN-SIP gateways made by AudioCodes, Epigy, Sangoma & Quintum was a cheap way to provide a drop in replacement for those expensive Telstra/Optus ISDN services. This generally was a stop gap measure as companies then made the move to straight SIP and stopped paying for the gateway device each month.
And just like that, ISDN was no longer relevant.
So today SIP based VoIP is the clear standard, ISDN fades away, but strangely those POTS lines carry on. Sure they are just VoIP delivered over a carrier grade ATA, but that 48vDC loop of copper with AC ring current, standardized over 120 years ago somehow lives on…
Here’s an article from The Age dated 11 August 1989:
Number may be up for the Telephone
Alexander Graham Bell’s system of sending voices over wires with a varying electrical current started a revolution, then weathered it for 123 years.
In Australia, that unchallenged reign officially ended yesterday with the official beginning of another revolution called integrated services digital networking — ISDN. The telephone has married the computer and gone digital.
The revolution really began late last year when Telecom invited several big companies to try out the communications system that will soon extend its tendrils into our offices and eventually, our homes.
In Canberra’s new National Convention Centre yesterday, the Minister for Telecommunications and Aviation Support, Mrs Kelly, communicated simultaneously via voice, fax and photovideotex on a single telephone line with two senior officers of the Defence Department in an office on the other side of Canberra.
Mr Bell’s telephone could never do that — it does only one job at a time.
With ISDN, any device that speaks or responds to the simple on-off language of computer digital code, or anything that can be rendered in the same code, including the human voice, video images and computer images, can be sent simultaneously on a single ISDN connection.
Each type of information is coded and -addressed in such a way that, at the other end, fax messages go to faxes, voices come out of the telephone and computer images arrive onscreen or on a printer.
The system inaugurated by Mrs Kelly is called ISDN Macro-link. It allows large companies with high-volume communications needs to design their own communications networks by selecting from a range of services available through the network, and plugging in the devices they need to do their business.
Like Lego, ISDN can be configured in myriad ways. It permits permanent, high-volume links to be established between offices in widely separated locations so that big computers can talk to each other.
Charges for all ISDN use are based on the amount of information sent.
The blazingly fast G4 fax can transmit a full A4 page in a few seconds, and the image emerges perfect and with the tiniest detail still visible, as if fresh from a laser printer.
The same link allows an executive in Melbourne to call a colleague in Sydney as easily as dialing an extension in the same building.
One can sketch a diagram on his computer terminal, and it will appear at the other end as it is being drawn.
Photographs can be digitized on a scanner and sent to a computer or printer as they speak, and if they wish, the colleagues can even see each other’s faces on slow scan video.
ISDN’s availability will lead to a change in the design of items like telephones, personal computers, printers and faxes — already companies are making devices that integrate all four.
As Australian and overseas networks develop, and ISDN communication between continents increases, the global village will shrink even further.
Number may be up for the telephone – Grahame O’Neill, Science and Technology Reporter ‘The AGE’ Melbourne Australia – Friday 11 August 1989
Note: NextEPC the Open Source project rebranded as Open5Gs in 2019 due to a naming issue. The remaining software called NextEPC is a branch of an old version of Open5Gs. This post was written before the rebranding.
In a production network network elements would typically not all be on the same machine, as is the default example that ships with NextEPC.
NextEPC is designed to be standards compliant, so in theory you can connect any core network element (MME, PGW, SGW, PCRF, HSS) from NextEPC or any other vendor to form a functioning network, so long as they are 3GPP compliant.
To demonstrate this we will cover isolating each network element onto it’s on machine and connect each network element to the other. For some interfaces specifying multiple interfaces is supported to allow connection to multiple
In these examples we’ll be connecting NextEPC elements together, but it could just as easily be EPC elements from a different vendor in the place of any NextEPC network element.
Service
IP
Identity
P-GW
10.0.1.121
pgw.localdomain
S-GW
10.0.1.122
PCRF
10.0.1.123
pcrf.localdomain
MME
10.0.1.124
mme.localdomain
HSS
10.0.1.118
hss.localdomain
External P-GW
In it’s simplest from the P-GW has 3 interfaces:
S5 – Connection to home network S-GW (GTP-C)
Gx – Connection to PCRF (Diameter)
Sgi – Connection to external network (Generally the Internet via standard TCP/IP)
S5 Interface Configuration
Edit /etc/nextepc/pgw.confand change the address to IP of the server running the P-GW for the listener on GTP-C and GTP-U interfaces.
If you are using NextEPC’s HSS you may need to enable MongoDB access from the PCRF. This is done by editing ‘‘/etc/mongodb.conf’’ and changing the bind IP to: bind_ip = 0.0.0.0
Restart MongoDB for changes to take effect.
$ /etc/init.d/mongodb restart
External MME
In it’s simplest form the MME has 3 interfaces:
S1AP – Connections from eNodeBs
S6a – Connection to HSS (Diameter)
S11 – Connection to S-GW (GTP-C)
S11 Interface Configuration
Edit /etc/nextepc/mme.conf, filling the IP address of the S-GW and P-GW servers.
We’ve talked about using a few different modules, like a SIP Registrar and Htable, that rely on data stored in Kamailio’s memory, the same is true for all the Stateful proxy discussion last week.
But what if you want to share this data between multiple Kamailio instances? This allows distributing workload and every server having the same information and therefore any server is able to process any request.
This allows memory data to be shared between multiple Kamailio instances (aka “Nodes”), so for example if you are storing data in Htable on one Kamailio box, all the other boxes/nodes in the DMQ pool will have the same HTable data.
Kamailio uses SIP to transfer DMQ messages between DMQ nodes, and DNS to discover DMQ nodes.
For this example we’ll share user location data (usrloc) between Kamailio instances, so we’ll create a very simple setup to store location data:
####### Routing Logic ########
/* Main SIP request routing logic
* - processing of any incoming SIP request starts with this route
* - note: this is the same as route { ... } */
request_route {
#Enable record_routing so we see the BYE / Re-INVITE etc
if(is_method("REGISTER")){
save("location");
}else{
sl_reply("500", "Nope");
}
}
Now if we register a SIP endpoint we should be able to view it using Kamcmd’s ul.dump call, as we talked about in the Kamailio SIP Registrar tutorial.
Next we’ll setup DMQ to allow this data to be shared to other nodes, so they also have the same userloc data available,
First we’ll begin by binding to an extra port for the DMQ messages to go to, to make it a bit clearer what is normal SIP and what’s DMQ,
So for this we’ll add a new line in the config to listen on port 5090:
/* uncomment and configure the following line if you want Kamailio to
* bind on a specific interface/port/proto (default bind on all available) */
listen=udp:0.0.0.0:5060
listen=tcp:0.0.0.0:5060
listen=udp:0.0.0.0:5090
The server_address means we’re listening on any IP on port 5090. In production you may have an IP set here of a private NIC or something non public facing.
The notification address resolves to 2x A records, one is the IP of this Kamailio instance / node, the other is the IP of the other Kamailio instance / node, I’ve just done this in /etc/hosts
Finally we’ll add some routing logic to handle the DMQ messages coming in on port 5090:
####### Routing Logic ########
/* Main SIP request routing logic
* - processing of any incoming SIP request starts with this route
* - note: this is the same as route { ... } */
request_route {
if (is_method("KDMQ") && $Rp == 5090)
{
dmq_handle_message();
}
#Enable record_routing so we see the BYE / Re-INVITE etc
if(is_method("REGISTER")){
save("location");
}else{
sl_reply("500", "Nope");
}
}
We’ll put the same config on the other Kamailio instance and restart Kamailio on both.
We can now check the DMQ node status to confirm they’re talking to each other.
We talked a little about the Transaction module and using it for Transaction Stateful SIP Proxy, but it’s worth knowing a bit more about the Transaction Module and the powerful functions it offers.
So today I’ll cover some cool functionality TM offers!
Different Reply Routes
By calling the t_on_reply(); we can specify the reply route to be used for replies in this transaction.
route[RELAY]{
#Use reply route "OurReplyRoute" for responses for this transaction
t_on_reply("OurReplyRoute");
#Relay (aka Forward) the request
t_relay_to_udp("192.168.3.118", "5060");
}
onreply_route[OurReplyRoute] {
#On replies from route[RELAY]
#Check our AVP we set in the initial request
xlog("for $rs response the value of AVP \"state_test_var\" is $avp(state_test_var) ");
#Append a header so we can see this was proxied in the SIP capture
append_hf("X-Proxied: For the reply\r\n");
}
Any responses from the route[RELAY] routing block will go to onreply_route[OurReplyRoute], the beauty of this is it allows you to have multiple reply routes each with their own logic. For example for a call leg to a carrier you may want to preserve CLI, but for a call leg to a customer you may wish to restrict it if that’s the option the user has selected, and you can make these changes / modifications in the reply messages.
Failure Routes
Failure routes allow the transaction module to know to try again if a call fails, for example if no response is received from the destination, send it to a different destination, like a backup.
route[RELAY]{
#Use reply route "OurReplyRoute" for responses for this transaction
t_on_reply("OurReplyRoute");
t_on_failure("OurFailureRoute");
#Relay (aka Forward) the request
t_relay_to_udp("192.168.1.118", "5060");
}
failure_route[OurFailureRoute]{
xlog("At failure route");
t_reply("500", "Remote end never got back to us");
exit;
}
We can build upon this, and try a different destination if the first one fails:
request_route {
#Enable record_routing so we see the BYE / Re-INVITE etc
record_route();
#Handle Registrations in a dumb way so they don't messy our tests
if(is_method("REGISTER")){
sl_reply("200", "ok");
exit;
}
#Append a header so we can see this was proxied in the SIP capture
append_hf("X-Proxied: You betcha\r\n");
if(is_method("INVITE")){
#Createa new AVP called "state_test_var" and set the value to "I remember"
$avp(state_test_var) = "I remember";
}
#Let syslog know we've set the value and check it
xlog("for $rm the value of AVP \"state_test_var\" is $avp(state_test_var) ");
#Send to route[RELAY] routing block
rewritehostport("nonexistentdomain.com");
route(RELAY);
}
route[RELAY]{
#Use reply route "OurReplyRoute" for responses for this transaction
t_on_reply("OurReplyRoute");
t_on_failure("OurFailureRoute");
#Relay (aka Forward) the request
t_relay();
}
failure_route[OurFailureRoute]{
xlog("At failure route");
#t_reply("500", "Remote end never got back to us");
rewritehostport("192.168.3.118");
append_branch();
t_relay();
}
onreply_route[OurReplyRoute] {
#On replies from route[RELAY]
#Check our AVP we set in the initial request
xlog("for $rs response the value of AVP \"state_test_var\" is $avp(state_test_var) ");
#Append a header so we can see this was proxied in the SIP capture
append_hf("X-Proxied: For the reply\r\n");
}
One thing to keep in mind is that there’s lots of definitions of failure, for example if you are sending a call to a carrier and get a 404 response back, you probably want to relay that through to the end user, because that destination isn’t there.
But if you get back a 5xx series response you may consider that to be a failure and select the next carrier for example.
Different conditions / requirements have different definitions of “failures” and so there’s a lot to think about when implementing this, along with timeouts for no replies, TCP session management, etc.
Parallel Forking the Call to Multiple Destinations
Parallel Forking is a fancy way of saying ring multiple destinations at the same time.
/* Main SIP request routing logic
* - processing of any incoming SIP request starts with this route
* - note: this is the same as route { ... } */
request_route {
#Enable record_routing so we see the BYE / Re-INVITE etc
record_route();
#Handle Registrations in a dumb way so they don't messy our tests
if(is_method("REGISTER")){
sl_reply("200", "ok");
exit;
}
#Append a header so we can see this was proxied in the SIP capture
append_hf("X-Proxied: You betcha\r\n");
if(is_method("INVITE")){
#Createa new AVP called "state_test_var" and set the value to "I remember"
$avp(state_test_var) = "I remember";
}
#Let syslog know we've set the value and check it
xlog("for $rm the value of AVP \"state_test_var\" is $avp(state_test_var) ");
#Send to route[RELAY] routing block
route(RELAY);
}
route[RELAY]{
#Use reply route "OurReplyRoute" for responses for this transaction
t_on_reply("OurReplyRoute");
#Append branches for each destination we want to forward to
append_branch("sip:[email protected]");
append_branch("sip:[email protected]");
append_branch("sip:[email protected]");
t_on_failure("OurFailureRoute");
#Relay (aka Forward) the request
t_relay();
}
failure_route[OurFailureRoute]{
xlog("At failure route");
t_reply("500", "All those destinations failed us");
}
onreply_route[OurReplyRoute] {
#On replies from route[RELAY]
#Check our AVP we set in the initial request
xlog("for $rs response the value of AVP \"state_test_var\" is $avp(state_test_var) ");
#Append a header so we can see this was proxied in the SIP capture
append_hf("X-Proxied: For the reply\r\n");
}
Serial Forking / Sequential Forking the calls to Multiple Destinations one after the Other
This could be used to try a series of weighted destinations and only try the next if the preceding one fails:
/* Main SIP request routing logic
* - processing of any incoming SIP request starts with this route
* - note: this is the same as route { ... } */
request_route {
#Enable record_routing so we see the BYE / Re-INVITE etc
record_route();
#Send to route[RELAY] routing block
route(RELAY);
}
route[RELAY]{
#Use reply route "OurReplyRoute" for responses for this transaction
t_on_reply("OurReplyRoute");
append_branch("sip:[email protected]", "0.3");
append_branch("sip:[email protected]", "0.2");
append_branch("sip:[email protected]", "0.1");
t_load_contacts();
t_next_contacts();
t_on_failure("OurFailureRoute");
#Relay (aka Forward) the request
t_relay();
break;
}
failure_route[OurFailureRoute]{
xlog("At failure route - Trying next destination");
t_on_failure("OurFailureRoute");
t_relay();
}
onreply_route[OurReplyRoute] {
#On replies from route[RELAY]
#Append a header so we can see this was proxied in the SIP capture
append_hf("X-Proxied: For the reply\r\n");
}
Again this will try each destination, but one after the other based on the weight we added to each destination in the append_branch()
The 3 different proxies all do the same thing, they all relay SIP messages, so we need a way to determine what state has been saved.
To do this we’ll create a variable (actually an AVP) in the initial request (in our example it’ll be an INVITE), and we’ll reference it when handling a response to make sure we’ve got transactional state.
We’ll also try and reference it in the BYE message, which will fail, as we’re only creating a Transaction Stateful proxy, and the BYE isn’t part of the transaction, but in order to see the BYE we’ll need to enable Record Routing.
Stateless Proof
Before we add any state, let’s create a working stateless proxy, and add see how it doesn’t remember:
####### Routing Logic ########
/* Main SIP request routing logic
* - processing of any incoming SIP request starts with this route
* - note: this is the same as route { ... } */
request_route {
#Enable record_routing so we see the BYE / Re-INVITE etc
record_route();
#Handle Registrations in a dumb way so they don't messy our tests
if(is_method("REGISTER")){
sl_reply("200", "ok");
exit;
}
#Append a header so we can see this was proxied in the SIP capture
append_hf("X-Proxied: You betcha\r\n");
if(is_method("INVITE")){
#Createa new AVP called "state_test_var" and set the value to "I remember"
$avp(state_test_var) = "I remember";
}
#Let syslog know we've set the value and check it
xlog("for $rm the value of AVP \"state_test_var\" is $avp(state_test_var) ");
#Forard to new IP
forward("192.168.3.118");
}
onreply_route{
#Check our AVP we set in the initial request
xlog("for $rs response the value of AVP \"state_test_var\" is $avp(state_test_var) ");
#Append a header so we can see this was proxied in the SIP capture
append_hf("X-Proxied: For the reply\r\n");
}
Now when we run this and call from any phone other than 192.168.3.118, the SIP INVITE will hit the Proxy, and be forwarded to 192.168.3.118.
Syslog will show the INVITE and us setting the var, but for the replies, the value of AVP $avp(state_test_var) won’t be set, as it’s stateless.
Let’s take a look:
kamailio[2577]: {1 1 INVITE [email protected]} ERROR: : for INVITE the value of AVP "state_test_var" is I remember
kamailio[2575]: {2 1 INVITE [email protected]} ERROR: <script>: for 100 response the value of AVP "state_test_var" is <null>
kamailio[2576]: {2 1 INVITE [email protected]} ERROR: <script>: for 180 response the value of AVP "state_test_var" is <null>
kamailio[2579]: {2 1 INVITE [email protected]} ERROR: <script>: for 200 response the value of AVP "state_test_var" is <null>
kamailio[2580]: {1 1 ACK [email protected]} ERROR: <script>: for ACK the value of AVP "state_test_var" is <null>
kamailio[2581]: {1 2 BYE [email protected]} ERROR: <script>: for BYE the value of AVP "state_test_var" is <null>
We can see after the initial INVITE none of the subsequent replies knew the value of our $avp(state_test_var), so we know the proxy is at this stage – Stateless.
Doing the heavy lifting of our state management is the Transaction Module (aka TM). The Transaction Module deserves a post of it’s own (and it’ll get one).
We’ll load the TM module (loadmodule “tm.so”) and use thet_relay() function instead of the forward() function.
But we’ll need to do a bit of setup around this, we’ll need to create a new route block to call t_relay() from (It’s finicky as to where it can be called from), and we’ll need to create a new reply_route{} to manage the response for this particular dialog.
Let’s take a look at the code:
####### Routing Logic ########
/* Main SIP request routing logic
* - processing of any incoming SIP request starts with this route
* - note: this is the same as route { ... } */
request_route {
#Enable record_routing so we see the BYE / Re-INVITE etc
record_route();
#Handle Registrations in a dumb way so they don't messy our tests
if(is_method("REGISTER")){
sl_reply("200", "ok");
exit;
}
#Append a header so we can see this was proxied in the SIP capture
append_hf("X-Proxied: You betcha\r\n");
if(is_method("INVITE")){
#Createa new AVP called "state_test_var" and set the value to "I remember"
$avp(state_test_var) = "I remember";
}
#Let syslog know we've set the value and check it
xlog("for $rm the value of AVP \"state_test_var\" is $avp(state_test_var) ");
#Send to route[RELAY] routing block
route(RELAY);
}
route[RELAY]{
#Use reply route "OurReplyRoute" for responses for this transaction
t_on_reply("OurReplyRoute");
#Relay (aka Forward) the request
t_relay_to_udp("192.168.3.118", "5060");
}
onreply_route[OurReplyRoute] {
#On replies from route[RELAY]
#Check our AVP we set in the initial request
xlog("for $rs response the value of AVP \"state_test_var\" is $avp(state_test_var) ");
#Append a header so we can see this was proxied in the SIP capture
append_hf("X-Proxied: For the reply\r\n");
}
So unlike before where we just called forward(); to forward the traffic we’re now calling in a routing block called RELAY.
Inside route[RELAY] we set the routing block that will be used to manage replies for this particular transaction, and then call t_relay_to_udp() to relay the request.
We renamed our onreply_route to onreply_route[OurReplyRoute], as specified in the route[RELAY].
So now let’s make a call (INVITE) and see how it looks in the log:
kamailio[5008]: {1 1 INVITE [email protected]} ERROR: : for INVITE the value of AVP "state_test_var" is I remember
kamailio[5005]: {2 1 INVITE [email protected]} ERROR: <script>: for 100 response the value of AVP "state_test_var" is I remember
kamailio[5009]: {2 1 INVITE [email protected]} ERROR: <script>: for 180 response the value of AVP "state_test_var" is I remember
kamailio[5011]: {2 1 INVITE [email protected]} ERROR: <script>: for 200 response the value of AVP "state_test_var" is I remember
kamailio[5010]: {1 1 ACK [email protected]} ERROR: <script>: for ACK the value of AVP "state_test_var" is <null>
kamailio[5004]: {1 2 BYE [email protected]} ERROR: <script>: for BYE the value of AVP "state_test_var" is <null>
kamailio[5007]: {2 2 BYE [email protected]} ERROR: <script>: for 200 response the value of AVP "state_test_var" is <null>
Here we can see for the INVITE, the 100 TRYING, 180 RINGING and 200 OK responses, state was maintained as the variable we set in the INVITE we were able to reference again.
(The subsequent BYE didn’t get state maintained because it’s not part of the transaction.)
ptime is the packetization timer in VoIP, it’s set in the SDP message and defines the length of each RTP packet that’s sent;
This gives the length of time in milliseconds represented by the media in a packet. This is probably only meaningful for audio data, but may be used with other media types if it makes sense. It should not be necessary to know ptime to decode RTP or vat audio, and it is intended as a recommendation for the encoding/packetisation of audio. It is a media-level attribute, and it is not dependent on charset.
A lower ptime value leads to more packet per second, while longer ptime leads to fewer packets per second.
In a Toll Quality (TDM) network 8000 samples per second are taken, this is reflected in PCM (Pulse Code Modulation) encoding of the data, see in PCMA / G.711 a-law for example.
But if each of these 8,000 samples per second were sent on an individual packet, we’d be seeing a huge number of tiny RTP packets where the header is a lot larger than the payload.
Instead endpoints generally wait until they’ve got a certain number of theses samples and then send them at once, every X milliseconds as defined by the ptime value.
A ptime of 1000ms would mean 1 packet per second.
A ptime of 20ms would mean 50 packets per second.
A ptime of 50ms would mean 20 packets per second.
ptime headaches
Some VoIP endpoints have issues with varied ptime (*cough Cisco SPA series cough*), and if you’re interconnecting with other carrier networks you have no real control as to what ptime endpoints use (except if you have a B2Bua that can resample / restuff the packets, or you use maxptime which really just limits more than fixes) so it’s worth understanding well.
International carrier trunks often have higher ptime values as they're often dealing with lower quality links, so they want to cut down the packets per second and often have jitter buffers in place to compensate for poor quality links.
RFC4566 (the second version of SDP) introduced the maxptime value.
This optional header in the SDP body allows an endpoint to specify the maximum ptime value it supports.
Older endpoints often don’t have much memory or processing power, so have very small buffers to store the received audio in before playing it to the user, and store the audio to be transmitted before sending it down the wire.
Mismatched ptime or a ptime that’s out of bounds for one endpoint can lead to some strange issues. Often an endpoint will ring, answer the call and even get a 200 OK, but immediately followed by a BYE from the incompatible end instead of an ACK.
In the initial INVITE ptime is not mandatory, meaning you may not know the caller has limits to the ptime values they can support, and the endpoint hangs up the calls straight after the 200 OK.
Identifying these issues may take some time, but here’s some good places to look:
SDP ptime value on INVITE and 200 OK
Time between RTP packets
Timestamp difference between RTP packets
Although it seems pretty self evident, if your endpoint only supports up to 20ms ptime, set the maxptime header to 20ms. You’d be surprised how often this isn’t the case.
One of the most searched keywords that leads to this site is Kamailio vs Asterisk, so I thought I’d expand upon this a bit more as I’m a big fan of both, and it’s somewhat confusing.
(Almost everything in this post I talk about on Asterisk is roughly true for FreeSWITCH as well, although FS is generally more stable and scalable than Asterisk. )
Asterisk
Asterisk is a collection of PBX / softswitch components that you can configure and put together to create a large number of different products with the use of config files and modules.
Asterisk can read and write the RTP media stream, allowing it to offer services like Voicemail, B2B-UA, Conferencing, Playing back audio, call recording, etc.
It’s easy to learn and clear to understand how it handles “calls”.
Kamailio
Kamailio is a SIP proxy, from which you can modify SIP headers and then forward them on or process them and generate a response.
Kamailio is unable to do manipulate the RTP media stream. It can’t listen to, modify or add to the call audio, it only cares about SIP and not the media stream. This means it can’t playback an audio file, record a call or serve voicemail.
Kamailio has a bit of a steep learning curve, which I’ve tried to cover in my Kamailio 101 series, but even so, Kamailio doesn’t understand the concept of a “call”, it deals in Sessions, as in SIP, and everything you want to do, you have to write into Kamailio’s logic. Awesome power but a lot to take in.
Note – RTPengine is growing in capabilities and integrates beautifully into Kamailio, so for some applications you may be able to use RTPengine for media handling.
Scale
Speed
Stability
Media Functions
Ease
Asterisk
X
X
Kamailio
X
X
X
Working Together
Asterisk has always had issues at scale. This is for a variety of reasons, but the most simplistic explanation is that Asterisk is fairly hefty software, and that each subscriber you add to the system consumes resources at a rate where once your system reaches a few hundred users you start to see issues with stability.
Kamailio works amazingly at scale, it’s architecture was designed with running at scale in mind, and it’s super lightweight footprint means the load on the box between handling 1,000 sessions and handling 100,000 sessions isn’t that much.
Because Asterisk has the feature set, and Kamailio has the scalability, so the the two can be used together really effectively. Let’s look at some examples of Asterisk and Kamailio working together:
Asterisk Clustering
You have a cluster of Asterisk based Voicemail servers, serving your softswitch environment. You can use a Kamailio instance to sit in front of them and route INVITEs evenly throughout the cluster of Asterisk instances.
You’d be using Asterisk’s VM functions (because Asterisk can do media functions) and Kamailio’s SIP routing functions.
You have a Kamailio based Softswitch that routes SIP traffic from customers to carriers, customers want a hosted Conference Bridge. You offer this by routing any SIP INVITES to the address of the conference bridge to an Asterisk server that serves as the conference bridge.
You’d be using Kamialio to route the SIP traffic and using Asterisk’s ability to be aware of the media stream and join several sources to offer the conference bridge.
Which should I use?
It all depends on what you need to do.
If you need to do anything with the audio stream you probably need to use something like Asterisk, FreeSwitch, YaTE, etc, as Kamailio can’t do anything with the audio stream.*
If it’s just signalling, both would generally be able to work, Asterisk would be easier to setup but Kamailio would be more scaleable / stable.
Asterisk is amazingly quick and versatile when it comes to solving problems, I can whip something together with Asterisk that’ll fix an immediate need in a faction of the time I can do the same thing in Kamailio.
On the other hand I can fix a problem with Kamailio that’ll scale to hundreds of thousands of users without an issue, and be lightning fast and rock solid.
Summary
Kamailio only deals with SIP signalling. It’s very fast, very solid, but if you need to do anything with the media stream like mixing, muxing or transcoding (RTP / audio) itself, Kamailio can’t help you.*
Asterisk is able to deal with the media stream, and offer a variety of services through it’s rich module ecosystem, but the trade-off is less stability and more resource intensive.
If you do require Asterisk functionality it’s worth looking into FreeSWITCH, although slightly harder to learn it’s generally regarded as superior in a lot of ways to Asterisk.
I don’t write much about Asterisk these days – the rest of the internet has that pretty well covered, but I regularly post about Kamailio and other facets of SIP.
I’m not a fan of Transcoding. It costs resources, often leads to reduced quality and adds latency.
Through some fancy SDP manipulating footwork we can often rejig the SDP order or limit the codecs we don’t support to cut down, or even remove entirely, the need for transcoding in the network.
There are no module parameters for SDP ops, we’ve just got to load the module with loadmodule “sdpops.so”
Use in Routing Logic
We’ll pickup where we left off on the Basic Stateless SIP Proxy use case (You can grab the basic code from that post), but this time we’ll remove PCMU (Aka G.711 μ-law) from the SDP body:
loadmodule "sdpops.so"
####### Routing Logic ########
/* Main SIP request routing logic
* - processing of any incoming SIP request starts with this route
* - note: this is the same as route { ... } */
request_route {
if(is_method("REGISTER")){
sl_reply("200", "Ok");
}
xlog("Received $rm to $ru - Forwarding");
append_hf("X-Proxied: You betcha\r\n");
#Remove PCMU (G.711 u-law) by it's SDP Payload ID
sdp_remove_codecs_by_id("0");
#Remove PCMU by name
sdp_remove_codecs_by_name("PCMU");
#Forard to new IP
forward("192.168.3.110");
}
onreply_route{
xlog("Got a reply $rs");
append_hf("X-Proxied: For the reply\r\n");
}
We can remove the codec either by it’s name (PCMU) or by it’s payload ID.
For removing it by name we just specify the name:
#Remove PCMU by name
sdp_remove_codecs_by_name("PCMU");
And by payload ID:
#Remove PCMU (G.711 u-law) by it's SDP Payload ID
sdp_remove_codecs_by_id("0");
We may want to remove all but one codec, again super simple:
That’s obviously a bit of a problem, as we build out our network we might have a series of load balancers that send traffic to a pool of Registrars, but according to RFC3261 this can’t be done, the SIP REGISTER request would need to go direct to one of these Registrars.
To get around this the SIP Path extensions, officially called “Session Initiation Protocol (SIP) Extension Header Field for Registering Non-Adjacent Contacts” (catchy title) was defined under RFC 3327.
An additional header is introduced called “Path:” for each proxy between the UA and the Registrar,
As the SIP REGISTER request passes through each proxy, each proxy appends the Path header with the value of it’s own SIP URI.
Let’s take a look at an example call flow from [email protected] who sends his REGISTER to atlanta.com, which is proxied by atlanta.com to registrar1.atlanta.com:
Bob to atlanta.com:
[email protected] > atlanta.com
REGISTER sip:atlanta.com SIP/2.0
Via: SIP/2.0/UDP 192.0.2.4:5060;branch=z9hG4bKnashds7
To: Bob <sip:[email protected]>
From: Bob <sip:[email protected]>;tag=456248
Call-ID: 843817637684230@998sdasdh09
CSeq: 1826 REGISTER
Contact: <Bob <sip:[email protected]>>
Supported: path
The REGISTER request is received by atlanta.com, which forwards it to registrar1.atlanta.com after adding it’s own URI as a Path header.
A seemingly simple question is how many concurrent calls can a system handle.
Sadly the answer to that question is seldom simple and easy to say, even more so when we talk about transcoding.
Transcoding is the process of taking a media stream encoded in one codec (format) and transferring it to a different codec (hence trans-coding).
This can be a very resource intensive process, so there’s a large number of hardware based solutions (PCI cards / network devices) that use FGPAs and clever processor arrangements to handle the transcoding. These products are made by a multitude of different vendors but are generally called hardware transcoders.
Today we’ll talk a bit about software based transcoding, and how many concurrent calls you can transcode on common VM configurations.
These stats will translate fairly well to their dedicated hardware counterparts, but a VM provides us with a consistent hardware environment so makes it a bit easier.
For these tests I created the baseline VM to run in VMWare Workstation with the below settings:
We’ll be transcoding using RTPengine, which recently added transcoding capabilities, so I set that up as per my post on setting up RTPengine for Transcoding.
Next I setup some SIPp scenarios to simulate call loads, from G.711 a-law to G.711 u-law (the simplest of transcoding (well re-compounding)) and used glances to get the max CPU usage and logged the results.
PCMA to PCMU (Re-companding)
RTPengine fared significantly better than I expected, I stopped at 150 concurrent transcoding sessions as that’s when call quality was really starting to degrade, but I was still achieving MOS of 4.3+ up to 130 concurrent sessions.
For what I needed to do, running this in a virtualised environment allowed 150 transcoding sessions before the MOS started to drop and call quality was adversely affected. Either way I was pretty amazed at how efficiently RTPengine managed to handle this.
Transcoding from one codec to a different codec was a different matter, and I’ll post the results from that another day.
We’ve talked a bit in the Kamailio Bytes series about different modules we can use, but I thought it’d be useful to talk about setting up a SIP Proxy using Kamailio, and what’s involved in routing a message from Host A to Host B.
When we talk about proxying for the most part we’re talking about forwarding, let’s look at the steps involved:
Our Kamailio instance receives a SIP request (for simplicity we’ll assume an INVITE).
Kamailio looks at it’s routing logic to lookup where to forward the request to. You could find out where to send the request to from a lot of different sources, you could consult the Dialplan Module or Dispatcher Module, perform an SQL lookup, consult UsrLoc table to find a AoR, etc.
Add it’s own Via header (more on that later)
Forward the Request (aka Proxy it) to the destination selected
Let’s take a look at a very simple way we can do this with two lines in Kamailio to forward any requests to 192.168.1.110:
####### Routing Logic ########
/* Main SIP request routing logic
* - processing of any incoming SIP request starts with this route
* - note: this is the same as route { ... } */
request_route {
xlog("Received $rm to $ru - Forwarding");
#Forard to new IP
forward("192.168.1.110");
}
After we restart Kamailio and send a call (INVITE) to it let’s see how it handles it:
Let’s make a small modification, we’ll add a header called “X-Proxied” to the request before we forward it.
####### Routing Logic ########
/* Main SIP request routing logic
* - processing of any incoming SIP request starts with this route
* - note: this is the same as route { ... } */
request_route {
xlog("Received $rm to $ru - Forwarding");
append_hf("X-Proxied: You betcha\r\n");
#Forard to new IP
forward("192.168.1.110");
}
On the wire the packets still come from the requester, to the Proxy (Kamailio) before being forwarded to the forward destination (192.168.1.110):
We’ve now got a basic proxy that takes all requests to the proxy address and forwards it to an IP Address.
If you’re very perceptive you might have picked up the fact that the in-dialog responses, like the 100 Trying, the 180 Ringing and the 200 Ok also all went through the proxy, but if you look at syslog you’ll only see the initial request.
/usr/sbin/kamailio: Received INVITE to sip:[email protected]:5060 - Forwarding
So why didn’t we hit that xlog() route and generate a log entry for the replies?
But before we can talk too much about managing replies, let’s talk about Via…
It’s all about the Via
Before we can answer that question let’s take a look at Via headers.
The SIP Via header is added by a proxy when it forwards a SIP message onto another destination,
When a response is sent the reverse is done, each SIP proxy removes their details from the Via header and forwards to the next Via header along.
As we can see in the example above, each proxy adds it’s own address as a Via header, before it uses it’s internal logic to work out where to forward it to, and then forward on the INVITE.
Now because all our routing information is stored in Via headers when we need to route a Response back, each proxy doesn’t need to consult it’s internal logic to work out where to route to, but can instead just strip it’s own address out of the Via header, and then forward it to the next Via header IP Address down in the list.
Via headers are also used to detect looping, a proxy can check when it receives a SIP message if it’s own IP address is already in a Via header, if it is, there’s a loop there.
Managing Responses in Kamailio
By default Kamailio manages responses by looking at the Via header, if the top Via header is its own IP address, it strips it’s own Via header and forwards it onto the next destination in the Via header.
We can add our own logic into this by adding a new route called onreply_route{}
onreply_route{
xlog("Got a reply $rs");
append_hf("X-Proxied: For the reply\r\n");
}
Now we’ll create a log entry with the response code in syslog for each response we receive, and we’ll add a header on the replies too:
Recap
A simple proxy to forward INVITEs is easy to implement in Kamailio, the real tricky question is what’s the logic involved to make the decision,
Now we’ll put both together to create something functional you could use in your own deployments. (You’d often find it’s faster to use HTable to store and retrieve data like this, but that’s a conversation for another day)
The Project
We’ll build a SIP honeypot using Kamailio. It’ll listen on a Public IP address for SIP connections from people scanning the internet with malicious intent and log their IPs, so our real SIP softswitches know to ignore them.
We’ll use GeoIP2 to lookup the location of the IP and then store that data into a MySQL database.
Lastly we’ll create a routing block we can use on another Kamailio instance to verify if that the IP address of the received SIP message is not in our blacklist by searching the MySQL database for the source IP.
The Database
In this example I’m going to create a database called “blacklist” with one table called “baddies”, in MySQL I’ll run:
CREATE database blacklist;
CREATE TABLE `baddies` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`ip_address` INT unsigned UNIQUE,
`hits` INT,
`last_seen` DATETIME,
`ua` TEXT,
`country` TEXT,
`city` TEXT
);
I’ll setup a MySQL user to INSERT/UPDATE/SELECT data from the MySQL database.
For storing IP addresses in the database we’ll store them as unsigned integers, and then use the INET_ATON('127.0.0.1') MySQL command to encode them from dotted-decimal format, and the INET_NTOA('2130706433') to put them back into dotted decimal.
Modparams
Now we’ll need to configure Kamailio, I’ll continue on from where we left off in the last post on GeoIP2 as we’ll use that to put Geographic data about the IP before adding the MySQL and SQLOps modules:
# ----- SQL params -----
loadmodule "db_mysql.so"
loadmodule "sqlops.so"
#Create a new MySQL database connection called blacklist_db
modparam("sqlops","sqlcon","blacklist_db=>mysql://root:yourpassword@localhost/blacklist")
#Set timeouts for MySQL Connections
modparam("db_mysql", "ping_interval", 60)
modparam("db_mysql", "auto_reconnect", 1)
modparam("db_mysql", "timeout_interval", 2)
After loading db_mysql and sqlops we create a new object / connection called blacklist_db with our MySQL Database parameters.
Now after a restart we’ll be connected to our MySQL database.
Honeypot Routing Logic
Now we’ll create a route to log the traffic:
####### Routing Logic ########
/* Main SIP request routing logic
* - processing of any incoming SIP request starts with this route
* - note: this is the same as route { ... } */
request_route {
route(AddToBlacklist);
sl_reply('200', 'Sure thing boss!');
}
route[AddToBlacklist]{
xlog("Packet received from IP $si");
sql_xquery("blacklist_db", "insert into baddies (ip_address, hits, last_seen, ua, country, city) values (2130706433, 10, NOW(), 'testua2', 'Australia', 'Hobart');");
}
Now for each SIP message received a new record will be inserted into the database:
root@ip-172-31-8-156:/etc/kamailio# mysql -u root -p blacklist -e "select * from baddies;"
Enter password:
+----+------------+------+---------------------+---------+-----------+--------+
| id | ip_address | hits | last_seen | ua | country | city |
+----+------------+------+---------------------+---------+-----------+--------+
| 1 | 2130706433 | 10 | 2019-08-13 02:52:57 | testua2 | Australia | Hobart |
| 2 | 2130706433 | 10 | 2019-08-13 02:53:01 | testua2 | Australia | Hobart |
| 3 | 2130706433 | 10 | 2019-08-13 02:53:05 | testua2 | Australia | Hobart |
+----+------------+------+---------------------+---------+-----------+--------+
This is great but we’re not actually putting the call variables in here, and we’ve got a lot of duplicates, let’s modify our sql_xquery() to include the call variables:
Now we’re setting the IP Address value to the Source IP psedovariable ($si) and formatting it using the INET_ATON function in MySQL, setting the last_seen to the current timestamp and setting the user agent to the User Agent psedovariable ($ua).
Let’s restart Kamailio, truncate the data that’s currently in the DB, send some SIP traffic to it and then check the contents:
mysql -u root -p blacklist -e "select *, INET_NTOA(ip_address) from baddies;"
Here you can see we’re starting to get somewhere, the IP, UA and last_seen values are all now correct.
We’re getting multiple entries from the same IP though, instead we just want to increment the hits counter and set the last_seen to the current time, for that we’ll just update the SQL query to set the time to be NOW() and if that IP is already in the database to update the last_seen value and incriment the hits counter:
route[AddToBlacklist]{
xlog("Packet received from IP $si");
geoip2_match("$si", "src"))
sql_xquery("blacklist_db", "insert into baddies (ip_address, hits, last_seen, ua, country, city) values (INET_ATON('$si'), 1, NOW(), '$ua', '$gip2(src=>cc)', '$gip2(src=>city)') ON DUPLICATE KEY UPDATE last_seen = NOW(), hits = hits + 1;", "r_sql");
}
The only issue with this is if GeoIP2 doesn’t have a match, no record will be added in the database, so we’ll add a handler for that:
route[AddToBlacklist]{
xlog("Packet received from IP $si");
if(geoip2_match("$si", "src")){
sql_xquery("blacklist_db", "insert into baddies (ip_address, hits, last_seen, ua, country, city) values (INET_ATON('$si'), 1, NOW(), '$ua', '$gip2(src=>cc)', '$gip2(src=>city)') ON DUPLICATE KEY UPDATE last_seen = NOW(), hits = hits + 1;", "r_sql");
}else{ ##If no match in GeoIP2 leave Country & City fields blank
sql_xquery("blacklist_db", "insert into baddies (ip_address, hits, last_seen, ua, country, city) values (INET_ATON('$si'), 1, NOW(), '$ua', '', '') ON DUPLICATE KEY UPDATE last_seen = NOW(), hits = hits + 1;", "r_sql");
}
}
Now let’s check our database again and see how the data looks:
mysql -u root -p blacklist -e "select *, INET_NTOA(ip_address) from baddies;"
Perfect! Now we’re inserting data into our blacklist from our honeypot. Now we’ll configure a new routing block we can use on another Kamailio instance to see if an IP is in the blacklist.
I left this running on my AWS box for a few hours, and lots of dodgy UAs dropped in to say hello, one of which was very insistent on making calls to Poland…
Querying the Data
Now we’ve got a blacklist it’s only useful if we block the traffic from our malicous actors who we’ve profiled in the database.
You could feed this into BGP to null route the traffic, or hook this into your firewall’s API, but we’re going to do this in Kamailio, so we’ll create a new routing block we can use on a different Kamailio instance – Like a production one – to see if the IP it just received traffic from is in the blacklist.
We’ve already spoken about querying databases in the SQLops Kamailio bytes, but this routing block will query the blacklist database, and if the sender is in the database, one or more records will be returned, so we know they’re bad and will drop their traffic:
route[CheckBlacklist]{
xlog("Checking blacklist for ip $si");
#Define a variable containing the SQL query we'll run
$var(sql) = "select INET_NTOA(ip_address) as ip_address from baddies;";
#Log the SQL query we're going to run to syslog for easy debugging
xlog("Query to run is $var(sql)");
#Query blacklist_db running the query stored in $var(sql) and store the result of the query to result_sql
sql_query("blacklist_db", "$var(sql)", "result_sql");
#If more than 0 records were returned from the database, drop the traffic
if($dbr(result_sql=>rows)>0){
xlog("This guy is bad news. Dropping traffic from $si");
exit;
}else{
xlog("No criminal record for $si - Allowing to progress");
}
}
This Honeypot use case just put those elements together.
In reality a far better implementation of this would use HTable to store this data, but hopefully this gives you a better understanding of how to actually work with data.
Final Note
I wrote this post about a week ago, and left the config running on an AWS box. I was getting hits to it within the hour, and in the past week I’ve had 172 IPs come and say hello, and some like the FriendlyScanner instance at 159.65.220.215 has sent over 93,000 requests:
RTPengine has an API / control protocol, which is what Kamailio / OpenSER uses to interact with RTPengine, called the ng Control Protocol.
Connection is based on Bencode encoded data and communicates via a UDP socket.
I wrote a simple Python script to pull active calls from RTPengine, code below:
#Quick Python library for interfacing with Sipwise's fantastic rtpengine - https://github.com/sipwise/rtpengine
#Bencode library from https://pypi.org/project/bencode.py/ (Had to download files from webpage (PIP was out of date))
import bencode
import socket
import sys
import random
import string
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = ('188.0.169.13', 2224) #Your server address
cookie = "0_2393_6"
data = bencode.encode({'command': 'list'})
message = str(cookie) + " " + str(data)
print(message)
sent = sock.sendto(message, server_address)
print('waiting to receive')
data, server = sock.recvfrom(4096)
print('received "%s"' % data)
data = data.split(" ", 1) #Only split on first space
print("Cookie is: " + str(data[0]))
print("Data is: " + str(bencode.decode(data[1])))
print("There are " + str(len(bencode.decode(data[1])['calls'])) + " calls up on RTPengine at " + str(server_address[0]))
for calls in bencode.decode(data[1])['calls']:
print(calls)
cookie = "1_2393_6"
data = bencode.encode({'command': 'query', 'call-id': str(calls)})
message = str(cookie).encode('utf-8') + " ".encode('utf-8') + str(data).encode('utf-8')
sent = sock.sendto(message, server_address)
print('\n\nwaiting to receive')
data, server = sock.recvfrom(8192)
data = data.split(" ", 1) #Only split on first space
bencoded_data = bencode.decode(data[1])
for keys in bencoded_data:
print(keys)
print("\t" + str(bencoded_data[keys]))
sock.close()
Want more telecom goodness?
I have a good old fashioned RSS feed you can subscribe to.