Kamailio route testing

Abstract

This document describes the testing of a single route in kamailio using specific headers sent by sipp and custom testing routes in kamailio. We will cover an example route that handles multiple conditions and replies to our call with a positive (200 OK) or negative (500 Server Internal Error) response.

Architecture overview

We are developing a highly scalable cloud SIP infrastructure with docker and kubernetes with microservices in kamailio, asterisk, rtpengine and cgrates. Our infrastructure can scale on premise, in the cloud with geographical regions request routing. It also has the possibility of having RTP nodes near the client installed on premise and it is surely a boost for the quality of experience and simplicity of deployment.

We have two distinct layers of kamailio nodes: the proxy and the router layer. Only the proxy layer has a public IP and is connected to the outside world on the Internet besides the media layer.

The different layers are interconnected and it’s impossible to test the single layer using sipp (http://sipp.sourceforge.net/). It’s even harder to test a single route in kamailio.

CD/CI

In our development pipeline we’re using Jenkins to run tests on every commit/merge in our testing branch with different tools. The behaviour tests on the whole infrastructure are run using sipp with different scenarios.

This is an example bash script for running a test:

#!/usr/bin/env bash
 set -o pipefail
 set -o nounset
 NAME="ut_kama_proxy-should_dispatcher_reload"
 DATE_FILE=$(date '+%Y-%m-%d_%H-%M')
 NODE_OWNER=$(echo "${POD_NODE}" | cut -d '.' -f 2)
 NODE_DOMAIN=$(echo "${POD_NODE}" | cut -d '.' -f3-)
 TARGET="proxy.${NODE_OWNER}.${NODE_DOMAIN}"
 # ---------------------------------------------------------------- #
 echo "${NAME} :: starting script"
 # ---------------------------------------------------------------- #
 # ---------------------------------------------------------------- #
 echo "${NAME} :: creating csv file with credentials"
 # ---------------------------------------------------------------- #
 mkdir -p /tmp/inf_files
 cat << EOF > /tmp/inf_files/"${NAME}".csv
 SEQUENTIAL
 test;$TARGET;[authentication username=test password=xxx];0039040123123;
 EOF
 SIPP=/usr/local/bin/sipp
 SCENARIOS=/sipp/scenarios/"${NAME}".xml
 CREDENTIALS=/tmp/inf_files/"${NAME}".csv
 "${SIPP}" "${TARGET}" -sf "${SCENARIOS}" -l 1 -m 1 -r 1 -max_non_invite_retrans 3 -rp 1000 -inf "${CREDENTIALS}"
 EXIT_CODE="${?}"
 # ---------------------------------------------------------------- #
 echo "${NAME} :: remove file and folder with credentials"
 # ---------------------------------------------------------------- #
 rm -rf /tmp/inf_files
 exit "${EXIT_CODE}";

As you can see we’re using scenarios in .xml and for the example above the scenario file is very simple and sends a certain header that specifies the test to be run.

<?xml version="1.0" encoding="iso-8859-2" ?>

<!DOCTYPE scenario SYSTEM "sipp.dtd">


<scenario name="[router] [unit test] SHOULD_DISPATCHER_RELOAD - OK">


 <send retrans="10">

   <![CDATA[


     INVITE sip:[field3]@[remote_ip]:[remote_port] SIP/2.0

     Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]

     From: sipp <sip:[field0]@[field1]>;tag=[call_number]

     To: <sip:[field3]@[field1]:[remote_port]>

     Call-ID: [call_id]

     CSeq: [cseq] INVITE

     Contact: sip:[field0]@[local_ip]:[local_port]

     Max-Forwards: 70

     Content-Type: application/sdp

     Content-Length: [len]

     X-evosip-Test: TEST_SHOULD_DISPATCHER_RELOAD

     v=0

     o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]

     s=-

     c=IN IP[media_ip_type] [media_ip]

     t=0 0

     m=audio [media_port] RTP/AVP 8

     a=rtpmap:8 PCMA/8000


   ]]>

 </send>


 <recv response="200" rrs="true" optional="false"></recv>


 <!-- definition of the response time repartition table (unit is ms)   -->

 <ResponseTimeRepartition value="10, 20, 30, 40, 50, 100, 150, 200"/>

</scenario>

So we can automate calls with different types of requests, headers and options and expect a certain response for a test to be defined as passed or failed.

Kamailio configuration structure

Our kamailio nodes have the standard kamailio.cfg file that includes the testing routes, that I will describe later, only if the global variable TESTING is defined. We pass that variable on the creation of the container only upon the testing process in Jenkins.

#!ifdef TESTING

include_file "kamailio-test.cfg"

#!endif

Kamailio-test.cfg file contains the routes that are triggered by this if condition at the top of the request route:

request_route {

#!ifdef TESTING

 if($hdr(X-evosip-Test) =~ "^TEST_") {

   route($(hdr(X-evosip-Test){s.rm,"})); # "

   exit;

 }

#!endif

...

The name of the route as from the example before is TEST_SHOULD_DISPATCHER_RELOAD and so that route is triggered when the header contains a record for X-evosip-Test.

 Kamailio route to be tested

We use the xhttp module for accepting rpc calls to reload the dispatchers that are requested to our central API that orchestrates the SIP infrastructure.

This route uses a shared table dispatcher that has the variable list set to 1 if the dispatchers have been correctly updated upon the xhttp request. Otherwise it checks if the dispatchers are being updated for longer than 60 seconds before returning a true value.

# ---------------------------------------------------------------------------------

# route SHOULD_DISPATCHER_RELOAD

# returns true / false depens on where the dispatchers are currently being reloaded

# ---------------------------------------------------------------------------------

route[SHOULD_DISPATCHER_RELOAD] {

  if($sht(dispatcher=>list) == 1) {

    return 0;

  } else {

    $var(todate) = $(sht(dispatcher=>list){s.int}) + 60;

    if ($var(todate) < $TV(sn)) {

      return 1;

    } else {

      return 0;

    }

  }

}#end route[SHOULD_DISPATCHER_RELOAD]

Dispatcher list reloading via API

The xhttp:request route just calls the DISPATCHER_LIST route that handles the API calls and updates the dispatcher list.

event_route[xhttp:request] {

  …

  if ($hu =~ "^/rpc") {

    $var(command) = $(hu{s.select,2,/});

    if($var(command) == "reload.dispatchers") {

      route(DISPATCHER_LIST);

      xhttp_reply("200", "OK", "text/html", "Dispatchers Set for RELOAD");

      exit;

    }

...

So the first thing the route DISPATCHER_LIST does is to set the shared table variable to the current timestamp until the dispatcher list is not correctly updated. This allows the route to not be called again and avoid concurrent calls towards the API. When the dispatcher list is correctly updated the shared table variable dispatcher=>list is set to 1.

route[DISPATCHER_LIST] {

  $sht(dispatcher=>list) = $TV(sn);

  …

  jsonrpc_exec('{"jsonrpc": "2.0", "method": "dispatcher.reload", "id": "1"}');

  xlog("L_INFO", "route[DISPATCHER_SET]: Dispatchers reloaded! \n");

  $sht(dispatcher=>list) = 1;

  …

Kamailio testing routes

Our kamailio testing routes are auxiliary routes defined to call specific functions in our kamailio.cfg, functions that return a specific value or a boolean one. We tend to write simple routes for specific functions that are then called inside a routing logic.

The example route from the previous section is this one:

route[TEST_SHOULD_DISPATCHER_RELOAD] {

  $var(TestsPassed) = 0;

  $var(TestsNum) = 3;

  # Dispatcher list has been correctly reloaded

  # and should not be reloaded again

  $sht(dispatcher=>list) = 1;

  if(!route(SHOULD_DISPATCHER_RELOAD)) {

    $var(TestsPassed) = $var(TestsPassed) + 1;

  } 

  # The dispatcher list route has been just triggered

  # and should not be called again for 60 seconds

  $sht(dispatcher=>list) = $TV(sn);

  if(!route(SHOULD_DISPATCHER_RELOAD)) {

    $var(TestsPassed) = $var(TestsPassed) + 1;

  }

  # The dispatcher list route has been called

  # more than 60 seconds ago and should reload

  $sht(dispatcher=>list) = $(sht(dispatcher=>list){s.int}) - 65;

  if(route(SHOULD_DISPATCHER_RELOAD)) {

    $var(TestsPassed) = $var(TestsPassed) + 1;

  }

  # Tests concluded count test number and respond via sl_send_reply

  if($var(TestsPassed) >= $var(TestsNum)) {

    sl_send_reply("200", "DISPATCHER RELOAD TRIGGER WORKING");

  } else {

    sl_send_reply("500", "DISPATCHER RELOAD TRIGGER NOT WORKING");

  }

}

This test route consists on 3 tests to check the correct functionality of the route SHOULD_DISPATCHER_RELOAD works correctly. If all three cases return the expected boolean value the router returns a 200 reply to sipp and passes the tests, otherwise it returns a 500 reply that fails the test.

Conclusions

Using this methodology we’re able to test specific kamailio routes in our infrastructure checking the correct functionality in a production like environment.

Leave a Reply

Your email address will not be published. Required fields are marked *