Client-side Kamailio

At Sipfront, we implement a product to provide SIP performance testing as a service. The core element of every performance benchmarking is an agent on the client side generating the traffic, which in our case is sipp, while at the same time, Kamailio is a well known SIP proxy typically used on the server side. In this post, I’ll show how and why the two are nonetheless fitting together in tandem.

sipp UAS Scenarios

sipp is a high performance SIP testing tool, driven by XML scenarios. A typical scenario for a client accepting incoming calls is to to wait for incoming messages and respond to them:

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="uas-answer">
    <recv request="INVITE"></recv>
    <send>
        <![CDATA[  
            SIP/2.0 180 Ringing
            [last_Via:]
            [last_From:]
            [last_To:];tag=[call_number]
            ...
        ]]>
    </send>
    ...

Now if you want to receive incoming calls from a SIP system, you usually have to register first. This is achieved by a scenario like the following

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="uas-register">
    <send retrans="500">
        <![CDATA[
            REGISTER sip:[field1 file="callee.csv"] SIP/2.0
            Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
            From: "[field2 file="callee.csv"]" <sip:[field0 file="callee.csv"]@[field1 file="callee.csv"]>;tag=[call_number]
            To: "[field2 file="callee.csv"]" <sip:[field0 file="callee.csv"]@[field1 file="callee.csv"]>
            Contact: <sip:[field0 file="callee.csv"]@[local_ip]:[local_port]>
            ...
        ]]>
    </send>
    ...

That way, the SIP server knows to send inbound calls for the user defined in the To header to the address defined in the Contact header.

The initial approach I took with Sipfront was to split the test state of acting as a called party into two actions:

  1. launch sipp with a scenario file to register the list of called party users as shown in above’s uas-register scenario.
  2. quit sipp and launch it again with a different scenario file to wait for inbound calls and answer them as shown in the uas-answer scenario shown above.

So as long as you make sure to launch the call-answering sipp instance on the same ip/port that you put into the Contact header of your registration, all is fine in SIP over UDP land.

Entering TCP and TLS

The inherent problem of above’s approach is that as of today, there is no possibility (that I’m aware of) to reset a sipp instance and replace its underlying scenario file without stopping the process and starting it again with a different scenario file. If you happen to run your benchmark over TCP or TLS, this will close your TCP socket towards the System under Test (SuT), and on receiving inbound calls for the called parties you just registered, the SuT has no way to deliver the calls to your sipp instance (there are edge cases where the SuT would try to reconnect by itself to the address given in the Contact header, but that only works under very specific conditions).

There are three approaches I can see to fix this problem:

  1. Implement a feature in sipp to reset an instance and replace the scenario file registering the called parties with a scenario file waiting for inbound calls. This seems like a fundamental change in sipp with unforseeable technical challenges, so a no-go for me short- to mid-term.
  2. Linux supports socket hot-transfer between processes via Ancillary Messages over unix domain sockets using the system calls sendmsg() and recvmsg(). Since Linux 5.6, there is another system call pidfd_getfd() supposed to take out most of the complexity of the former approach. NodeJS and HAProxy are projects making use of these approaches to achieve uninterrupted switch-overs between processes. Neither have I worked with any of those two approaches, nor do I feel comfortable to implement such a mechanism in sipp in any reasonable time. However, it’s probably the most elegant way to solve this problem.
  3. Stick with UDP on sipp and put an element between sipp and the SuT to convert between UDP and the target protocol we’d like to talk over with the SuT. Kamailio is a perfect fit for such a task, as it’s fully scriptable, highly performant and supports UDP, TCP and TLS, among others.

Kamailio to the rescue

The overall idea is simple: let sipp communicate via UDP to Kamailio as next hop, which acts as stateless proxy towards the System under Test.

This can be achieved by starting sipp with the following options:

sipp -p "${sipp_port}"" -rsa "${kamailio_ip}:${kamailio-port}" -t u1 ...

That options tell sipp to bind to the port defined in sipp_port and use one single UDP socket for all communication, and send all traffic to the ip and port defined in kamailio_ip and kamailio_port, respectively.

In Kamailio, I could now just forward the request to the request URI defined in the SIP message, e.g.:

INVITE sip:bob@example.com;transport=tls SIP/2.0

The drawback of that approach would be that we’d lose the possibility to define an outbound proxy. So, instead of routing based on the request URI, sipp sends an additional X-header like X-Outbound-Proxy to Kamailio containing the actual destination URI.

INVITE sip:bob@example.com SIP/2.0
X-Outbound-Proxy: sip:1.2.3.4:5060;transport=tls

Kamailio can pick this header for all requests coming from sipp and statelessly forward the traffic. Since we record-route outbound traffic, we can be sure that in-dialog requests will reach Kamailio and will be loose-routed to sipp. For initial inbound requests creating a SIP dialog, like in the uas-answer scenario we saw at the very top of this post, we know that sipp is listening on a fixed sipp_port as described earlier, so we can forward that inbound traffic to this address.

if (loose_route()) {
    xlog("L_INFO", "Request is loose-routed\n");
} else if (is_ip_rfc1918("$si")) {
    xlog("L_INFO", "Request from internal to external\n");
    $du = $hdr(X-Outbound-Proxy);
    remove_hf("X-Outbound-Proxy");
} else {
    xlog("L_INFO", "Request from external to internal\n");
    $du = "sip:[% local_ip %]:[% sipp_port %];transport=udp";
}
...
xlog("L_INFO", "Forwarding request to $du\n");
forward();

Conclusion

The approach taken here to leverage Kamailio as protocol converter on the client side has some draw-backs in the very basic configuration, mainly being intrusive in respect to the test scenario by adding another Via and Record-Route header, which you might want to avoid.

However, there is no reason to not take this approach further and for example apply the topos module of Kamailio to hide the topology behind our Kamailio instance.

On the other hand, adding Kamailio into the signaling path opens up a huge amount of flexibility. No matter which traffic generation agent you use (be it sipp or baresip or anything else), you can easily modify the SIP traffic directly in the Kamailio scripting language or its extensions without altering the agent itself.

And finally, with rtpengine as companion application to Kamailio, you can also start tampering with the media path of your test calls, like converting your media to DTLS-SRTP, in case your client does not support it natively.

Feedback

If you have any comments, suggestions or remarks to this article, please engage in a discussion over at Twitter @sipfrontcom or send me an Email.