Dynamic dispatchers in Kamailio with jsonrpc and graphql with external Orchestrator and API

Abstract

This document describes the usage of kamailio in a dynamic, multi layer and containerized environment with and external orchestrator that is able to force a custom dynamic list of dispatchers to a running kamailio node.

Architecture overview

Our highly scalable cloud SIP infrastructure is using docker and kubernetes with microservices in kamailio, asterisk, rtpengine and cgrates. This kind of 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. Under the router layer we have a TPS (Transcoding and Playback Service) Asterisk layer and obviously external RTP nodes layer.

The API

We’re using a JavaScript API running on Node.JS and using GraphQL as a query language. This API is tracking the creation of every node in our infrastructure and is aware of the infrastructure architecture being able to serve only relevant data to a node. In our multi layer infrastructure the proxy layer should be able to have a list of routers under itself. The router layer should be able to get a list of proxies over and TPS under itself.

A sample router GraphQL API request in our case will be structured like this;

{ 

  kamailio { 

    dispatcher {

      list { 

        kamailioConf

      } 

    }

  }

}

The response for the kamailioConf key will hold a value like this:

2 sip:172.22.2.6:5060

1 sip:172.22.2.95:5060

1 sip:172.22.2.94:5060

As you can see this router has a proxy on top with setid = 2 and two TPS nodes beneath with setid = 1.

Kamailio configuration

In our pursuit of having a stateless kamailio instance we define a file on our container that will contain the dispatcher list that can then be updated. So our configuration should be like this:

loadmodule "dispatcher.so"

modparam("dispatcher", "list_file", "/tmp/dispatcher.list")

We’re then using the xhttp module to be able to trigger the dispatcher list reload route from the orchestrator.

Some kamailio pod in our infrastructure have two network ports, one local and one public.
For security reasons we’re listening to the port 80 only on the local network interface. Thus filtering the request by that port gives us the security that the orchestrator is making the call.

event_route[xhttp:request] {

  # Check if the call is from the local network

  if!(dst_port==80) {

    xlog("L_NOTICE", "[XHTTP:REQUEST] $si FORBIDDEN! ***\n");

    exit;

  }




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

    xlog("L_NOTICE", "[XHTTP:REQUEST] $si ACCEPTED ***\n");

    jansson_get("method", "$rb", "$var(rpcMethod)");

    xlog("L_NOTICE", "[XHTTP:REQUEST] RPC METHOD: $var(rpcMethod) ***\n");




    if($var(rpcMethod) == "dispatcher.reload") {

      xlog("L_NOTICE", "Reloading dispatcher list\n");

      route(DISPATCHER_LIST);

    }

    jsonrpc_dispatch();

    exit;

}

Our DISPATHER_LIST method will then handle the API query for the new list of dispatchers and reload the list via rpc.

route[DISPATCHER_LIST] {

  xlog("L_INFO", "route[DISPATCHER_LIST]: 

Fetching dispacther_list! \n");

  if($sht(api=>token)==$null){

    route(GET_API_TOKEN);

  }

  $http_req(all) = $null;

  $http_req(suspend) = 1;

  $http_req(timeout) = 500;

  $http_req(method) = "POST";

  $http_req(hdr) = "Content-Type: application/json";

  $http_req(hdr) = "Authorization: Bearer " + $sht(api=>token);

  $var(graphql_query) = 

"{\"query\": \"{kamailio {dispatcher {list {kamailioConf }}}}\"}";

  $http_req(body) = $var(graphql_query);

  http_async_query(API_QUERY_URL, "DISPATCHER_SET");

}

The response of the async http query is then handled by DISPATCHER_SET route:

route[DISPATCHER_SET] {

  if ($http_ok && $http_rs == 200) {

    xlog("L_INFO", "route[DISPATCHER_SET]: response $http_rb)\n");

    jansson_get("data.kamailio.dispatcher.list.kamailioConf", $http_rb, "$var(conf)");

    if($var(conf)!="0") {

      exec_msg("printf \"$var(conf)\" > /tmp/dispatcher.list");

      jsonrpc_exec('{"jsonrpc": "2.0", "method":

"dispatcher.reload", "id": "1"}');

      xlog("L_INFO", "route[DISPATCHER_SET]: 

Dispatchers reloaded! \n");

    }

  }

}

The API Call

So at this point we have Kamailio ready for handling the dispatcher reload RPC call that first refreshes the dispatcher list got from the API.

Now let’s take a look at how we managed to send the request via our API written in JS.

We have a function called makeRPCSignal  which handles different rpc calls (rpcMethod) beside the dispatcher.reload. Here’s a simplified example:

function makeRPCSignal (rpcMethod) {

  const options = {

    url: `http://${pod.ipAddress}/rpc`,

    body: JSON.stringify({'jsonrpc': '2.0', 'method': rpcMethod,

                          'id': '1'}),

  }

  request.post(options, (error, res, body) => {

    if (!error && res.statusCode === 200) {

      log.debug(`${JSON.stringify(res)}\n`, moduleInfo)

      resolve(new RPCSuccess(pod, res.statusCode, rpcMethod))

    } else {

      log.debug(`${JSON.stringify(error)}\n`, moduleInfo)

      if (res) {

        if (res.statusCode) {

          log.debug(`rpcMethod: Status Code: ${res.statusCode}\n`,

                     moduleInfo)

          reject(new RPCException(pod, res.statusCode, '', rpcMethod))

        } else {

           reject(new RPCException(pod, -1, '', rpcMethod))

        }

      } else {

        reject(new RPCException(pod, -1, '', rpcMethod))

      }

    }

  }).on('error', (e) => {

    reject(new RPCException(pod, -1, e.message, rpcMethod))

  })

}

As you can see there’s a also a simple function RPCException that formats the logs with additional information ready to be sent to Elasticsearch.

Conclusions

So at this point we have a port 80 exposed for the orchestrator to make a RPC call via HTTP that triggers a kamailio route which enquiries the API for the actual dispatcher list. If there is any error in the process we will handle it on the orchestrator side (retry, deletion, creation of new instances, etc.).

This kind of approach is just a step towards a stateless kamailio instance that gets configured on runtime by the needs of the infrastructure as a whole. Creating multiple layers and handling the mutability of the whole by an orchestrator gives the ability to have a highly scalable and cloud oriented architecture.

Leave a Reply

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