User-Defined Functions

Overview

VoIP systems are very generic. Their basic call flow is strictly defined in various RFCs, however there are still a plenty of ways to customize the business logic on behalf of the protocol itself.

Kamailio SIP proxy is one example of such system. It strictly follows RFC 3261. At the same time it gives you a card blanche for modifying each SIP message accordingly to your business logic. That is what makes every Kamailio configuration specific and harder for engineers to properly set granular VoIP monitoring.

We designed the SIP3 platform with generic VoIP systems in mind. User-Defined Functions (UDFs) let engineers get granular monitoring data, based on various user-defined and service attributes.

At the moment the SIP3 platform lets you write UDFs using Groovy.

This document will walk you though the process of writing UDFs. For more information about deployment, please read the SIP3 Installation Guide.

User-Defined Function Base Structure

To better understand SIP3 UDF structure let’s analyze following sip_message_udf example:

Groovy
def eventBus = vertx.eventBus()                                 (1)
eventBus.localConsumer("sip_message_udf", { event ->            (2)
    // `event.body` is a Map<String, Object>:
    // {
    //  "src_addr" : String,
    //  "src_host" : String (Optional),
    //  "src_port" : Integer,
    //  "dst_addr" : String,
    //  "dst_host" : String (Optional),
    //  "dst_port" : Integer,
    //  "payload" : Map<String, String>,
    //  "attributes" : Map<String, String|Boolean>
    // }
    def packet = event.body()

    def sip_message = packet['payload']                         (3)
    if (sip_message['from'].matches('<sip:100@.*')) {
        packet['attributes']['robocall'] = true                 (4)
    }

    event.reply(true)                                           (5)
})
1 Under the hood SIP3 uses Vert.x framework which makes it very simple to write code extensions in different languages. You can find more about that in the Vert.x Documentation. Alternatively you can copy the code snippet above and modify it according to your needs.
2 "sip_message_udf" is a name of the SIP3 UDF endpoint. As an input it receives a SIP3 packet with generic payload represented as a Map<String, Object>.
3 "sip_message_udf" endpoint receives as a payload a SIP message represented as a Map<String, String> where keys are SIP header names and values are corresponding SIP header values.
4 In our example robocall is an User-Defined Attribute which is assigned to a SIP3 packet. There is no limit of the amount of user defined attributes that can be assigned to a packet. Every assigned attribute is automatically populated as a search criteria in the Advanced Search and all related Service Metrics.
5 Each UDF has to return boolean result which may be used by SIP3 Salto to define further message flow.

User-Defined Attributes

As mentioned in the previous section, the main purpose of any SIP3 UDF is assigning String or Boolean attribute to a SIP3 packet according to its business logic.

We call such attributes User-Defined Attributes (UDAs). Each UDA by default will be provisioned as an Advanced Search expression and as a Metrics tag.

However, in some cases you may want to have more control over your UDAs provisioning by using a special SIP3 UDAs syntax in attributes names.

  • Database (d) - in this mode your attribute will be saved to a database and provisioned as an Advanced Search expression.

  • Options (o) - enables autocompletion for Advanced Search expressions.

  • Metrics (m) - in this mode your attribute will be provisioned as a Metrics tag.

Let’s take a closer look at provisioning modes:

user_agent                  (1)
do:operation_system         (2)
d:x_session_id              (3)
m:robo_call                 (4)
:wangiri_candidate          (5)
1 user_agent - The UDA with default behaviour: provisioning as the Advanced Search expression sip.user_agent and as the Metrics tag user_agent.
2 do:operation_system - The UDA operation_system will be provisioned to a database and available in the Advanced Search expression sip.operation_system with options for autocompletion.
3 d:x_session_id - x_session_id provisioned to a database and available in the Advanced Search expression sip.x_session_id without options.
4 m:robo_call - will be provisioned as Metrics tag robo_call.
5 :wangiri_candidate - wangiri_candidate will be available only in User-Defined Functions. Neither the Advanced Search expression nor the Metrics tag will be provisioned.

UDAs can bring your SIP3 experience to a next level if you follow the following rule:

Always choose String attributes with a limited amount of possible values when you use Options write mode for UDA. Otherwise, it may affect your database performance and can even bring it down. For instance, if you have a mobile application: operation_system or application_version are good choice of UDAs with Options.

Service Attributes

Most attributes assigned within a UDF are UDAs. Along with those each UDF endpoint carries out its own set of attributes called Service Attributes. Service Attributes help resolving various call aggregation and correlation problems.

SIP3 correlates same call legs by strictly matching From and To users. Now, let’s imagine that you have a SIP node which receives all SIP messages containing From headers with US numbers in international format (From: <sip:13027030026@sip3.io>). The SIP node handles SIP messages and routes them further while modifying the From header according to the US national number format (From: <sip:3027030026@sip3.io>). Such configuration breaks the SIP3 call correlation. Fortunately, there is a solution to that problem. We can simply do a pattern matching and assign the results to the caller and callee Service Attributes defined for the "sip_message_udf" endpoint, as shown below.

Groovy
def eventBus = vertx.eventBus()
eventBus.localConsumer("sip_message_udf", { event ->
    def packet = event.body()

    def from_header = packet['payload']['from']
    def matcher = (from_header =~ /1(\d*)/)
    if (matcher) {
        packet['attributes']['caller'] = matcher[0][1]
    }

    event.reply(true)
})

The example above shows just a simple customization which can be done by analyzing SIP message content. However, you can build a very complex business logic using vertx object. Read in the Vert.x Documentation and this tutorial how to send HTTP requests or query a remote database just in a few lines of code.

Endpoints

Below you can find a list of all avaialble service UDF endpoints.

"packet_udf"

1. Description

"packet_udf" UDF is called for every packet received by SIP3 Salto. The main purpose of this UDF is to filter duplicates (e.g. traffic sent from SIP3 Captain and Heplify can be filtered only by this function).

2. Payload

"packet_udf" UDF receives as a payload a Map<String, Object> of src_addr, src_port, src_host, dst_addr, dst_port and dst_host.

3. User-Defined and Service Attributes

"packet_udf" UDF doesn’t support User-Defined and Service Attributes.

4. Usage Example

Following example shows how to filter traffic passing between SBC and SSW hosts:

Groovy
def eventBus = vertx.eventBus()
eventBus.localConsumer("packet_udf", { event ->
    // `event.body` is a Map<String, Object>:
    // {
    //  "sender_addr" : String,
    //  "sender_host" : String (Optional),
    //  "sender_port" : Integer,
    //  "payload" : Map<String, Object>,
    //      "src_addr" : String,
    //      "src_host" : String (Optional),
    //      "src_port" : Integer,
    //      "dst_addr" : String,
    //      "dst_host" : String (Optional),
    //      "dst_port" : Integer,
    // }
    def packet = event.body()

    if (packet['sender_host'] == 'SBC'
            && (packet['payload']['src_host'] == 'SSW' || packet['payload']['dst_host'] == 'SSW')) {
        event.reply(false)
    } else {
        event.reply(true)
    }
})

"sip_message_udf"

1. Description

"sip_message_udf" UDF is called for every SIP message received by SIP3 Salto. UDAs and Service Attributes assigned within the UDF are used for further registration, call aggregation and search.

2. Payload

"sip_message_udf" UDF receives as a payload a Map<String, String> where keys are SIP header names and values are corresponding SIP header values.

3. User-Defined and Service Attributes

"sip_message_udf" UDF has no restrictions on assigning User-Defined Attributes. However, it considers caller, callee and x_call_id as a Service Attributes used to resolve various call correlation problems.

4. Usage Example

Following example shows how to define and assign robocall attribute:

Groovy
def eventBus = vertx.eventBus()
eventBus.localConsumer("sip_message_udf", { event ->
    // `event.body` is a Map<String, Object>:
    // {
    //  "src_addr" : String,
    //  "src_host" : String (Optional),
    //  "src_port" : Integer,
    //  "dst_addr" : String,
    //  "dst_host" : String (Optional),
    //  "dst_port" : Integer,
    //  "payload" : Map<String, String>,
    //  "attributes" : Map<String, String|Boolean>
    // }
    def packet = event.body()

    def sip_message = packet['payload']
    if (sip_message['from'].matches('<sip:100@.*')) {
        packet['attributes']['robocall'] = true
    }

    event.reply(true)
})

"sip_call_udf"

1. Description

"sip_call_udf" UDF is called for every SIP call session aggregated by SIP3 Salto. UDAs and Service Attributes assigned within the UDF are used for further registration, call aggregation and search. Also, this UDF is a perfect source of real-time CDRs.

2. Payload

"sip_call_udf" UDF receives as a payload a Map<String, Any> of session attributes.

3. User-Defined and Service Attributes

"sip_call_udf" UDF has no restrictions on assigning User-Defined Attributes. However, it considers caller, callee and x_call_id as a Service Attributes used to resolve various call correlation problems.

4. Usage Example

Following example shows how to define and assign problematic attribute to a call with setup_time greater than 5 seconds:

Groovy
def eventBus = vertx.eventBus()
eventBus.localConsumer("sip_call_udf", { event ->
    // `event.body` is a Map<String, Object>:
    // {
    //  "src_addr" : String,
    //  "src_host" : String (Optional),
    //  "src_port" : Integer,
    //  "dst_addr" : String,
    //  "dst_host" : String (Optional),
    //  "dst_port" : Integer,
    //  "payload" : Map<String, Object>,
    //      "created_at" : Long,
    //      "terminated_at" : Long,
    //      "state" : String,
    //      "caller" : String,
    //      "callee" : String,
    //      "call_id" : String,
    //      "duration" : Long (Optional),
    //      "setup_time" : Long (Optional),
    //      "cancel_time" : Long (Optional),
    //      "establish_time" : Long (Optional),
    //      "terminated_by" : String (Optional)
    //   "attributes" : Map<String, String|Boolean>
    // }
    def session = event.body()

    def setup_time = session['payload']['setup_time']
    if (setup_time != null && setup_time > 5000) {
        session['attributes']['problematic'] = true
    }

    event.reply(true)
})