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.