Tuesday, December 15, 2020

Loop protection on Kamailio/Asterisk

 Sometimes, due to incorrect configuration, even not on the system we control, we can have call loops. Imagine a situation with loop call forward with one of the endpoints is a cell phone and forward is made via another operator. Usually, you can't get all these loops relying only on the internal pre-save mechanism.

 

But there is a simple and efficient method to avoid such loops. Idea is to limit calls from A to B, if there are more than X such calls per second.

 

Proposed solutions are Kamailio and Asterisk's implementation of these scenarios.

Kamailio method is based on htable

...

# Not more than FROM_TO_PER_SECOND_RATIO calls per second from X number to Y number. If it is set to 3,  4th call would be blocked
#!define FROM_TO_PER_SECOND_RATIO 3

...

loadmodule "htable.so"

modparam("htable", "htable", "fu_tu=>size=5;autoexpire=5;")

...

request_route{

...

     # Process only new calls
    if (is_method("INVITE") && !has_totag()) {

        $var(from_to_hash) = $fU + '_' + $tU + '_' + $timef(%H_%M_%S);

        if ($sht(fu_tu=>$var(from_to_hash)) != $null) {
            $sht(fu_tu=>$var(from_to_hash)) = $sht(fu_tu=>$var(from_to_hash)) + 1;
            xlog("L_NOTICE", "Possible loop detected on call $fu -> $tu ($sht(fu_tu=>$var(from_to_hash)) total calls during last second)\n");
        } else {
            xlog("L_INFO", "Loop protection enabled on call $fu -> $tu with $var(from_to_hash) hash)\n");
            $sht(fu_tu=>$var(from_to_hash)) = 1;
        }

        if ($sht(fu_tu=>$var(from_to_hash)) > FROM_TO_PER_SECOND_RATIO) {
            xlog("L_WARN", "Loop detected on call $fu -> $tu (IP:$si:$sp)\n");
            send_reply("482", "Loop detected");
            exit;
        }
    }

...

}


Asterisk method is based on GROUP()

[dial_all]

exten => _X.,1,NoOp(Main call context)

  same => n,GoSub(loop_protection, ${EXTEN}, 1)

...

 

[loop_protection]
; Provide loop protection based on calls per second on same from/to numbers.
exten => _X.,1,NoOp(Loop protection...)
  same => n,Set(GROUP_HASH=${CALLERID(num)}_${EXTEN}_${STRFTIME(${EPOCH},,%Y_%m_%d_%H_%M_%S)})
  same => n,Set(GROUP()=${GROUP_HASH})
  same => n,GoToIf($[${GROUP_COUNT(${GROUP_HASH})}<=3]?limit_ok)
  same => n,NoOp(Total Calls From ${CALLERID(num)} to ${EXTEN} during this second exceeded limit of <%= @loop_cps_ratio %>)
  same => n,Hangup(25)
  same => n(limit_ok),NoOp(Total Calls From ${CALLERID(num)} to ${EXTEN} during this second is: ${GROUP_COUNT(${GROUP_HASH})})
  same => n,Return()


The mechanism is fairly simple on both. Have a hash with the following name - <from_number>_<to_number>_<date_with_second>, calculate it for every new call, and if his hash name already exists - increase it by 1 till limit is reached. After this - drop the call, considering this is a loop.

 

Maybe not the best method, but reliable. Only thing I did not investigate much - possible memory leak on Asterisk with group names. I consider this is something to be tested.

Thursday, October 29, 2020

Kamailio and mobile TCP endpoints. Unregistering

The modern world is mobile. You may like it or not. But it is. So, with all these technologies that are about saving your battery life, all application reachability goes to vendor-lock push servers.
 
But the problem emerges in the following. SIP client registers on registrar (Kamailio) with TCP. Yes, you can do SIP keepalives here, but why to have em if we have a built-in mechanism of keepalive in TCP itself? Plus, additional packets over the network will drain the battery faster. And iOS, when putting the app to the background, just cut the network of app off. Literally killing it.
 
But we somehow need to know which actual state of the endpoint is. And here Kamailio is to help us with tcpops module.
 
The idea is quite simple. On each query to location table, we will clean-up it from "dead" TCP connections. So, after save(). And the only actual state of endpoints would be taken in the branch and calling process. Yes, it will give extra initial INVITE on already 'dead' connection, but appears to be, it sometimes cleaning just arrived REGISTER's so it's acceptable.


# Save info for TCP connections for unregister on close/timeout/error
loadmodule "htable.so"

modparam("htable", "htable", "tcpconn=>size=15;autoexpire=7200;")

...

# Handle SIP registrations
route[REGISTRAR] {

   ...

    save("location");
    route(TRACK_TCP_STATE);

    $var(processed_subscriber) = $fu;

    route(TCP_REGISISTER_CLEANUP);

   ...

}

 

route[TRACK_TCP_STATE] {
    if (proto == UDP) {
        return;
    }
    xlog("[TRACK_TCP_STATE] Saving state for future disconect track of $fu\n");

    $sht(tcpconn=>$conid) = $fu;


    tcp_enable_closed_event();
}


# Make sure you set up $var(processed_subscriber) before calling this route

route[TCP_REGISISTER_CLEANUP] {

    if ($var(processed_subscriber) == $null) {
        return;
    }

    xlog("[TCP_REGISISTER_CLEANUP] Processing subscriber $var(processed_subscriber)\n");

    # Getting registered endpoints for this AoR
    if (!reg_fetch_contacts("location", "$var(processed_subscriber)", "subscriber")) {
        $var(processed_subscriber) = $null;
        xlog("[TCP_REGISISTER_CLEANUP] No registered contacts for $var(processed_subscriber)\n");
        return;
    }

    $var(i) = 0;

    # Loop through registered endpoints
    while ($var(i) < $(ulc(subscriber=>count))) {

        $var(stored_subscriber_conid) = $(ulc(subscriber=>conid)[$var(i)]);

        # Make sure proto is TCP
        if ($var(stored_subscriber_conid) != $null) {

            # Check if entry is still active TCP connection. Unregister otherwise.
            if ($var(stored_subscriber_conid) == -1 || !tcp_conid_alive("$var(stored_subscriber_conid)")) {

                $var(stored_subscriber_ruid) = $(ulc(subscriber=>ruid)[$var(i)]);
                $var(stored_subscriber_address) = $(ulc(subscriber=>addr)[$var(i)]);

                xlog("[TCP_REGISISTER_CLEANUP]: Unregistering entry $var(i)/$var(stored_subscriber_conid) -> $var(stored_subscriber_address)\n");

                if (!unregister("location", "$var(processed_subscriber)", "$var(stored_subscriber_ruid)")) {
                    xlog("[TCP_REGISISTER_CLEANUP]: Unregistering entry $var(i)/$var(stored_subscriber_conid) -> $var(stored_subscriber_address) FAILED!\n");
                }
            }
        }
        $var(i) = $var(i) + 1;
    }
    $var(processed_subscriber) = $null;
}


event_route[tcp:closed] {
    xlog("[TCP:CLOSED] $proto connection closed conid=$conid\n");

    $var(processed_subscriber) = $sht(tcpconn=>$conid);

    if ($var(processed_subscriber) != $null) {
        route(TCP_REGISISTER_CLEANUP);
        $sht(tcpconn=>$conid) = $null;
    }
}

event_route[tcp:timeout] {
    xlog("[TCP:TIMEOUT] $proto connection timeout conid=$conid\n");

    $var(processed_subscriber) = $sht(tcpconn=>$conid);

    if ($var(processed_subscriber) != $null) {
        route(TCP_REGISISTER_CLEANUP);
        $sht(tcpconn=>$conid) = $null;
    }
}

event_route[tcp:reset] {
    xlog("[TCP:RESET] $proto reset closed conid=$conid\n");

    $var(processed_subscriber) = $sht(tcpconn=>$conid);

    if ($var(processed_subscriber) != $null) {
        route(TCP_REGISISTER_CLEANUP);
        $sht(tcpconn=>$conid) = $null;
    }
}


Simple and efficient solution. Yes, some of "dead" endpoints would be present in table during expiration time, but I'm ok with that.

Monday, July 6, 2020

FusionPBX - VTiger 7 Integration

Every telecom engineer should build a PBX, CallCenter, and CRM Integration.

So, my time for CRM integration came. The idea is quite simple.
  1. Calls are registering in CRM with call recording
  2. Calls are being notified with popup while using CRM
  3. Calls from CRM directly. Click 2 Call actually
Integration is done for popular opensource CRM VTiger 7 and FusionPBX. Have to say, not plain, reworked, my fork actually.

This project consists of 3 parts.
  1. PBXManager  - module for VTiger CRM, built on top of the original PBXManager module made for Asterisk.
  2. VFusion Daemon - Docker Image that is running with FusionPBX (FreeSwitch) and listening to FreeSwitch events and making requests to VTiger CRM. Made on NodeJS
  3. FusionPBX - Actually my fork of it. Version 4.4

I actually tried to avoid daemons or so, resides on FusionPBX server, but really can't avoid it totally. So, architecture is not perfect. Some of the requests are made from FreeSwitch via mod_curl, some - in v_xml_cdr app by Fusion, some - in VFusion daemon. Not clean, but as of now it's working.
So, how it looks like?

1. Call from unknown contact


2. Call from a known contact

 
3. Call details records

yes, you can listen to the recordings directly from CRM.


Click 2 Call is made quite simple. Just click on a phone number inside CRM and you will have a callback to your extension. After picking up, a call to that number would be established.

As it's intended to be a commercial product, code is open. But documentation is still not. Maybe in the future documentation also would be open.

If you have any interest in deploying this product - pls contact me at support <at> consertis.at. Or here.

Tuesday, June 9, 2020

Janus Audio/Video plays around

WebRTC got to me. I can't say it was the first time, but most deep at the moment.

Small disclaimer. I'm not a web developer and only now got, that browser is OS by itself.

And the way to get media from the browser is WebRTC. So, let's make one more bicycle to have WebRTC - VoIP bridge.

There are many projects around to fill this gap. FreeSwitch (mode_verto or mod_sofia), Kamailio + RTPEngine are just to mention some.

And Janus was chosen. To try something new and get some skills in bright'n'shiny world of web development. And I like, that Janus has already built-in SIP plugin.

The idea of this small proof of concept - get audio and video streams separated and treated separately. Why - to have audio part handled by SIP (passed to Asterisk, more precisely) and video (or screen sharing part in the future) - by Janus SFU unit (VideoRoom plugin)

Project is just a set of 3 docker containers running in network host mode (just don't want to play around with NAT and all this stuff) and glued together with docker-compose. Nginx as a web server, Janus as Janus WebRTC server, and Asterisk as SIP application server.


Web part is just reworked and gloriously copy-pasted from original Janus demos.


This project is intended to be used as a playground for future experiments and donor of code for other projects.


Repo with project on GitHub.

Some of the pictures not to be so boring:


Audio is passed to Asterisk via SIP plugin, and Video is handled by EchoTest plugin.




Audio is mixing by Asterisk (SIP plugin as well), Video is handled by VideoRoom plugin.


Point, this one is tested on a VPS with a Let'sEncrypt certs to test it mostly "in the wild" and also there are many restrictions in modern browsers about WebRTC and security, which means even over LAN you need to have pure HTTPS.



Friday, March 13, 2020

Periodic signals with FusionPBX (School Bells)

A small app designed actually for schools. When you have PBX connected to your announcement system, it would be logical if PBX can handle periodic signals as well. So, the name is also obvious in this case - School Bells

Now it's a part of my FusionPBX fork and fusionpbx-apps repo as well.

The idea is quite simple - to manage signals from GUI in cron-like style. Not super friendly, but most of us know what cron is.


Limitation - it will allow you to choose only from Recordings, not like tone_stream or so.

Installation:


# cd /usr/src/
# git clone https://github.com/fusionpbx/fusionpbx-apps
# cd fusionpbx-apps/
# cp -R school_bells /var/www/fusionpbx/app/


Add

* * * * * php /var/www/fusionpbx/app/school_bells/school_bells_cron.php 

to your crontab

Go to GUI

Upgrades -> Schema; App Defaults; Menu Defaults; Permission Defaults

Log out and back in

Apps -> School Bells

Enjoy!

Saturday, February 29, 2020

SIP Monitoring based on SIP3 and Heplify

Few words upfronts.

 - What is SIP3?

Follow the link :) If you don't know why you need it (or similar system) - you don't.

 - Why SIP3 and not Homer
Homer 3 was really good. It actually covers all my needs in SIP traffic monitoring. Than Homer 5 with new GUI was introduced. And things slowly start getting worse. Mostly cause of really strange changes in UI, which can stop working at any moment. No, it will respond, show something, but sometimes not the real picture. The usual answer - log out and back in. With Homer 7 things went even worse. Recommendation from developers is using the latest alpha version. If set up the system with a recommended setup (set of docker containers), it's not a "set and forget" solution. No. It will stop processing traffic at some time without any warning. For monitoring system which I need not every day (actually I need mostly post-mortem traces), it always ends up with log into the system and finds out it stops working like a week ago. Not the best option.
But it's really my experience. Maybe I'm not that lucky at all.

 - Why heplify and not captain (which is a part of SIP3)?
The answer is quite simple. Heplify - single binary, captain (at the moment) - a full zoo of tools, like docker, ansible, python, etc. Yes, I'm aware, that captain also can provide RTCP stats, but overhead is too big.

So, getting to the good part.

Installation and setting up

Central server

I'm gonna use CentOS 8 (minimal). Not like I'm a big fan of it, but developers like it. So, most tested one

1. Install docker

# yum install -y yum-utils   device-mapper-persistent-data  lvm2
# yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# containerdSource=https://download.docker.com/linux/centos/7/x86_64/stable/Packages/containerd.io-1.2.6-3.3.el7.x86_64.rpm
# dnf install $containerdSource -y
# dnf install docker-ce docker-ce-cli -y
# systemctl disable firewalld
# systemctl start docker
# systemctl enable --now docker

 

2. Install ansible, pip, git


# yum install -y epel-release
# yum install -y ansible git python3-pip
# pip3 install docker-py


3. Install SIP3 backend


# cd /usr/src/
# git clone https://github.com/sip3io/sip3-ansible.git


Note on taking metrics from public network services.

/usr/src/sip3-ansible/roles/sip3-salto/templates/application.yml.j2
#! Server
server:
  uri: udp://0.0.0.0:15060


#! Management socket
management:
  uri: udp://0.0.0.0:15090

#! Metrics
metrics:
  logging:
    step: 1000
  influxdb:
    uri: http://sip3-influxdb:8086
    db: sip3
    step: 1000

#! MongoDB
mongo:
  uri: mongodb://sip3-mongodb:27017
  db: sip3
  bulk-size: 1
  collections:
    - prefix: attributes
      indexes:
        ascending: [name]
      max-collections: 7
    - prefix: sip_register_index
      indexes:
        ascending: [created_at, terminated_at, src_addr, src_host, dst_addr, dst_host, caller, callee, state]
        hashed: [call_id, x_call_id]
      max-collections: 7
    - prefix: sip_register_raw
      indexes:
        ascending: [created_at]
        hashed: [call_id]
      max-collections: 7
    - prefix: sip_call_index
      indexes:
        ascending: [created_at, terminated_at, src_addr, src_host, dst_addr, dst_host, caller, callee, state, error_code, error_type, duration, setup_time, establish_time]
        hashed: [call_id, x_call_id]
      max-collections: 7
    - prefix: sip_call_raw
      indexes:
        ascending: [created_at]
        hashed: [call_id]
      max-collections: 7
    - prefix: sip_message_index
      indexes:
        ascending: [created_at, terminated_at, src_addr, src_host, dst_addr, dst_host, caller, callee, state]
        hashed: [call_id, x_call_id]
      max-collections: 7
    - prefix: sip_message_raw
      indexes:
        ascending: [created_at]
        hashed: [call_id]
      max-collections: 7
    - prefix: sip_options_index
      indexes:
        ascending: [created_at, terminated_at, src_addr, src_host, dst_addr, dst_host, caller, callee, state]
        hashed: [call_id, x_call_id]
      max-collections: 7
    - prefix: sip_options_raw
      indexes:
        ascending: [created_at]
        hashed: [call_id]
      max-collections: 7

#! Application
sip:
  message:
    x-correlation-header: X-Call-ID
    exclusions: [MESSAGE, SUBSCRIBE, NOTIFY, OPTIONS, REGISTER]
  calls:
    aggregation-timeout: 120000

attributes:
  record-ip-addresses: false
  record-call-users: false


It's not to index in the database all IP addresses and usernames are seen on the system, cause the production system usually resides on public networks, where are tons of spam/attack calls and packets. Also retention period for database is set to 7 days, which is ok for me.

# ansible-playbook /usr/src/sip3-ansible/playbooks/trial/sip3-backend.yml

Relax and wait for system installs.

4. Add autostart


 /etc/systemd/system/sip3-backend.service

[Unit]
Description=Start Ansible Playbook SIP3
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/ansible-playbook /usr/src/sip3-ansible/playbooks/trial/sip3-backend.yml
TimeoutStartSec=10

[Install]
WantedBy=default.target




# systemctl daemon-reload
# systemctl enable sip3-backend


5. (Optional )Add hosts aliases to db

To get more friendly trances to read. Point, no spaces in names are allowed.


# docker exec -it sip3-mongodb mongo
# use sip3
# db.hosts.insert({"name" : "my_softswitch", "sip": ["X.X.X.X", "10.10.0.1"]})

Installing heplify (capture agent)

Log in to the server you want to collect SIP traffic from

# cd /usr/local/bin/
# wget https://github.com/sipcapture/heplify/releases/download/1.56/heplify
# chmod +x heplify


/etc/systemd/system/heplify.service
[Unit]
Description=Captures packets from wire and sends them to SIP3
After=network.target

[Service]
WorkingDirectory=/usr/local/bin
ExecStart=/usr/local/bin/heplify -i any -m SIP -hs SIP3_SERVER_ADDRESS:15060
ExecStop=/bin/kill ${MAINPID}
Restart=on-failure
RestartSec=10s
Type=simple

[Install]
WantedBy=multi-user.target




# systemctl daemon-reload
# systemctl enable heplify
# systemctl start heplify


System using

Log into your newly installed system on
http://SIP3_SEVER_ADDRESS with admin:admin credentials.

Sometimes, just after login, you will need to refresh your page. Actually, make sure Go! button is active
It's your search string. Simple, but yet powerful. It really looks like Wireshark one.
You can add search critera there based on sip.
or ip.
criteria
ip.src_host and ip.dst_host are hosts we added at step 5 installing Central Server paragraph.

Changing password is quite tricky, cause SIP3 relies on Grafana auth (Grafana is shipped as well) and to change password, head to

 http://SIP3_SEVER_ADDRESS/grafana

and use it interface to change password. As a bonus, you have Grafana dashboard with some useful metrics. And for sure you can add one you like. How - read Grafana manual :)

Adding batteries

One is a key feature of SIP3 is the ability to glue call legs in a 1 call flow. Usual algorithm is based on a strict match From and To numbers, but it's really rare case (at least, in my scenarios). There is a possibility to write user-defined functions, but I'll leave it to official manual. Another way to match call is to use custom SIP headers to match call legs. The default SIP3 header is X-Call-ID. So, if one leg holds Call-ID standart header matching other call X-Call-ID header, they would consider as one call and glue together.

In my case, I'm using FusionPBX, the solution is really simple. Add dialplan entry:
It will add X-Call-ID header to call equal to current Call-ID, if it's not present. Also, FreeSwitch preserves X-Headers over call legs. which is perfect for us.

So, we will have a picture like this

Upgrading

# cd /usr/src
# git pull
# ansible-playbook /usr/src/sip3-ansible/playbooks/trial/sip3-backend.yml


Happy using!