Žilinská univerzita > Fakulta riadenia a informatiky > Katedra informačných sietí

Kamailio Call establishment permission rules

This article talks about deploying permission control mechanism for call establishment in Kamailio SIP Proxy.

In many VoIP solutions, it is crutial to deploy numbering scheme and write down rules where users are/aren't allowed to call.
On top of that, a company can allow the people to call outside, for example to PSTN. The rules can change over time as well as the numbering scheme itself.

This increases the demand to maintain the numbering system and permission rules for system administrators in a easy-to-use way. It is not feasible to rewrite the configuration files all over again and again.

 

There is a module for Kamailio SIP server, called permissions, which enables us to tell Kamailio to check every caller and callee info. Based on this info, Kamailio can decide if the call can be routed or not. The configuration is pretty easy and works for versions 3.3.x and 4.0.

Changes for kamailio.cfg file:

First, we define a directive in the "defined values" section that we can use later to easily turn on/off the permissions checking:

#!define WITH_PERMISSIONS

Next, in the modules section, we actually have to load the module (We don't need to install it. It is a part of default installation of Kamailio):

#!ifdef WITH_PERMISSIONS
loadmodule "permissions.so"
#!endif

In the module parameters section we can define where the files that store the rules are:

#!ifdef WITH_PERMISSIONS
modparam("permissions","default_allow_file","/etc/kamailio/rules.allow")
modparam("permissions","default_deny_file","/etc/kamailio/rules.deny")
#!endif
note: these are not the default names of the files, so they have to be specified. See the documentation of the permissions module for more information.

The last step is to incorporate the module functionality to the routing logic. In the main routing block "request_route" you can add

#!ifdef
route(PERMISSIONS_AUTHORIZE);
#!endif

right before route(SIPOUT); if you didn't alter the routing logic before. The new route itself looks like:

route[PERMISSIONS_AUTHORIZE] {
  #!ifdef WITH_PERMISSIONS
  if(allow_routing())
  {
    xlog("L_NOTICE","Routing allowed for $fu calling $ru");
  }
  else
  {
    xlog("L_NOTICE","Routing denied for $fu calling $ru");
    sl_send_reply("403","Routing Not Allowed");
    exit;
  }
  #!endif
}

And that's it! Now we have to create the files that will contain the permission rules. As we specified in the routing logic, we need files /etc/kamailio/rules.allow and /etc/kamailio/rules.deny.

This is an example of the allow file that comes with the Kamailio source code:

# SIP Express Router permissions module config file
# Syntax:
# from_list [EXCEPT from_list] : req_uri_list [EXCEPT req_uri_list]
#
# from_list and req_uri_list are comma separated expressions
# Expressions are treated as case insensitive POSIX Extended Regular Expressions
# Keyword ALL matches any expression.
#
# Examples:
# ALL : "^sip:361[0-9]*@abc.com$" EXCEPT "^sip:361[0-9]*3@abc.com$", "^sip:361[0-9]*4@abc.com$"
#
# "^sip:3677[0-9]*@abc.com$" : "^sip:361[0-9]*@abc.com$"
#
# ALL : ALL

 

As you can see, single-line comments begin with "#" sign and the syntax is explained. Let's have a look at our example rule:

 

"^sip:1[0-9]{3}@awesome_domain.com$" EXCEPT "^sip:1111@awesome_domain.com$", "^sip:1112@awesome_domain.com$" : "^sip:2[0-9]{3}@awesome_domain.com"

This rule tells us that users from our awesome domain with numbers from <1000-1999> range (except numbers 1111 and 1112) are allowed to call to numbers <2000-2999>.

Now examine this example:

"^sip:hr-*@awesome_domain.com$" : ALL EXCEPT "^sip:the-boss@awesome_domain.com"

This states that HR department (with SIP URI hr-........@awesome_domain.com) is not limited in making calls, except they can't call the boss to fire him :)

 

These two examples were from the allow file! The same syntax applies for the deny rules as well.

A single rule

ALL : ALL

in the deny file will deny all routing and you can only allow call you explicitly specify in the allow file.

For more information how the rules are treated and files evaluated, you should have a look at the documentation of permissions module, this section in particular.

 

Kamailio needs to be restarted in order for the new configuration to take effect. Restart is also necessary everytime the rules files change.

shell# /etc/init.d/kamailio restart

 

That concludes the configuration of Kamailio SIP server.

 

 

 

What comes next is a consideration how to maintain the permission rules in the files. When Kamailio is running on a single node, it is not a problem to manage the rules simply by editing the files and restarting Kamailio. But what if many Kamailio instances run on many different nodes of a computer cluster to provide High Availability or Load Balancing? Or what if a computer breaks down and you lose the files? There is a need to keep the rules on some external storage (i.e. database) and configure cluster nodes to download them.

The next paragraph talks about ways how to do this. In the end the option I consider the best is explained more in depth as well as the configuration itself.

 

One option is to store the contents of the allow and deny files in the database. All that is required is a single table that would hold two rows (one for allow file and one for deny file). There are ways how to store text files in different database systems, like for example CLOB data type in Oracle. Mysql offers ways like using the LOAD DATA INFILE. Once the file contents are in database, all that remains is to write a script that will pull the data and save them in the local files on HDD. This can be done in many ways. Kamailio is capable of calling Lua scripting language. This language have resources of handling DB results and writing to local files (using modules SQLops, App_Lua and Rtimer). Using Lua would mean, that Kamailio itself would be responsible for downloading the files periodically. If Kamailio fails, files are not refreshed. (note: Keep in mind that if you are calling Lua script from Kamailio, the file which contains the script must belong to user running Kamailio). If you would like to keep it out of Kamailio, there is a possibility to call perl or python scripts directly from Linux, for example.

Drawback of this solution is the fact that the whole content of the file is stored in one row od DB table. Once it is edited, the row has to be replaced by a new one.

From the structure of the files we can see that it would be better to store the rules in database as 1 row = 1 rule. This would enable the administrator to add, remove or edit a single rule. Database can be accessed from many different systems, so creation of various interfaces for editing the rules is and easy task.

Let's have a look how to do this. The following example works with Oracle DB which is accessed using UnixODBC, but changing the syntax to your DB system should not be a problem.

We can create a table permissions_rules that will look like this:

CREATE TABLE permissions_rules (
  type VARCHAR2(1) default '' CONSTRAINT check_type CHECK (type IN ('A','D')) NOT NULL,
  rule VARCHAR2(300) NOT NULL,
  last_change DATE NOT NULL
);
CREATE OR REPLACE TRIGGER permissions_rules_tr
before insert on permissions_rules FOR EACH ROW
BEGIN
  :NEW.last_change:=sysdate;
END permissions_rules_tr;
/

 

The column rule stores the specific rule and the column type holds the information whether the rule is of allow or deny type. Type can only have value of "A" or "D". Trigger before insert on this table only inputs the current sysdate to the last column.

 

Following python script is used to download the rules and construct the files. Python is a part of many Linux distributions. You need to install package python-pyodbc (for Debian/Ubuntu) or pyodbc (for Fedora/RedHat).

note: The installation and configuration of UnixODBC is not a part of this example. Many good tutorials can be found online and ODBC can be configured to access all DB systems.

Install pyodbc:

(for Debian/Ubuntu) shell# apt-get install python-pyodbc
(for Fedora/RedHat) shell# yum install pyodbc

Create a file named /etc/kamailio/permissions_python.py:

import pyodbc
cnxn=pyodbc.connect('DSN=OracleSIPGateway;UID=sipgateway_cs_ro;PWD=1%2%3%DB')
cursor1=cnxn.cursor()
cursor2=cnxn.cursor()

###first allow file rules
#open file for writing
f=open('/etc/kamailio/rules.allow','w')

#write some header stuff to the file.. like syntax etc.
f.write("# SIP Express Router permissions module config file\n# allowfile\n# Syntax:\n")
f.write("# from_list [EXCEPT from_list] : req_uri_list [EXCEPT req_uri_list]\n#\n")
f.write("# from_list and req_uri_list are comma separated expressions\n")
f.write("# Expressions are treated as case insensitive POSIX Extended Regular Expressions\n")
f.write("# Keyword ALL matches any expression.\n#\n")
f.write("# Examples:\n# ALL : \"^sip:361[0-9]*@abc.com$\" EXCEPT \"^sip:361[0-9]*3@abc.com$\", \"^sip:361[0-9]*4@abc.com$\"\n")
f.write("#\n# \"^sip:3677[0-9]*@abc.com$\" : \"^sip:361[0-9]*@abc.com$\"\n#\n# All : ALL\n")

#fetch rows and write them to file
for row in cursor1.execute("select rule from permissions_rules where type like 'A'"):
    #you can do a lot of things with the rule before writing it to file.. for example syntaxt checking
    f.write(row[0])
    f.write('\n')

f.close()

###the same for deny rules
f=open('/etc/kamailio/rules.deny','w')

#write some header stuff to the file.. like syntax etc.
f.write("# SIP Express Router permissions module config file\n# denyfile\n# Syntax:\n")
f.write("# from_list [EXCEPT from_list] : req_uri_list [EXCEPT req_uri_list]\n#\n")
f.write("# from_list and req_uri_list are comma separated expressions\n")
f.write("# Expressions are treated as case insensitive POSIX Extended Regular Expressions\n")
f.write("# Keyword ALL matches any expression.\n#\n")
f.write("# Examples:\n# ALL : \"^sip:361[0-9]*@abc.com$\" EXCEPT \"^sip:361[0-9]*3@abc.com$\", \"^sip:361[0-9]*4@abc.com$\"\n")
f.write("#\n# \"^sip:3677[0-9]*@abc.com$\" : \"^sip:361[0-9]*@abc.com$\"\n#\n# All : ALL\n")

for row in cursor2.execute("select rule from permissions_rules where type like 'D'"):
    #again, syntax verification etc. is possible here
    f.write(row[0])
    f.write('\n')

f.close()

cnxn.close()

 

Next thing to do is to create a bash script that will call this python code and in the end it will restart Kamailio.

Create file /etc/kamailio/permissions_script.sh:

#!/bin/bash

echo "Downloading Permissions rules and constructing files for Kamailio!" >> /var/log/syslog #use /var/log/messages on Fedora/RedHat

#fire python script
python /etc/kamailio/permissions_python.py

#if only one Kamailio instance is running in a cluster, check if it is on this node:
str=`/etc/init.d/kamailio status`
if [[ "$str" == *pid* ]]
then
  #if this node is running Kamailio, restart it
  echo "Restarting Kamailio!" >> /var/log/syslog #use /var/log/messages on Fedora/RedHat
  /etc/init.d/kamailio restart;
fi
 And don't forget to add execute rights
shell# chmod +x /etc/kamailio/permissions_script.sh

Now use cron to call this script when you desire. For example, in some setups it is sufficient for permissions rules to be refreshed every 12 hours.

shell# crontab -e
note: you can change the cron default editor to your favorite one by shell command "export EDITOR=your_editor" :)

Once you are at the cron edit screen, create your times:

0 7,19 * * * /etc/kamailio/permissions_script.sh

This will download permissions rules every day at 7:00 and 19:00. Good idea is to customize cron to call the script when the server load is lowest within a single day.

After editing crontab, restart crond:

shell# /etc/init.d/crond restart

And that's it. This will download the rules from the database and save them in your local files. The python script can be extended to verify the syntax of the rule and output some debug messages to syslog when opening the files or database connection.

Another great idea is to call this script before Kamailio starts. That can be achieved by modifying the /etc/init.d/kamailio script, by adding line "python /etc/kamailio/permissions_python.py" right in the beggining of the start() function.

 

 

As said before, there are many different ways how to interact with the database table and it's contents.

Let's say that the administrator changes the content of a file locally and wants to populate the changes over all the nodes. Here is a small bash script that will allow him to upload new rules to database (while erasing all the old ones). This script also enables him to insert or remove a single rule from database (following the correct syntax) and check current contents of the database table.

We will name the script file "SIPuser":

 

#!/bin/bash

domain="my_domain.com"
dbdsn="myDSNfromODBCconfig"
dbuser="Chewbacca"
dbpassword="port123"

case "$1" in
  permissions)
    case "$2" in
      add)
        case "$3" in
          allow)
            type="A"
            ;;
          deny)
            type="D"
            ;;
          *)
            echo "Incorrect rule specification"
            exit 1;
            ;;
        esac
        #there can be rule validation here.. TODO
        echo "insert into permissions_rules(type,rule) values ('$type','$4');" | isql $dbdsn $dbuser $dbpassword > /dev/null
        echo "Rule added to database!"
        ;;
      rm)
        case "$3" in
          allow)
            type="A"
            ;;
          deny)
            type="D"
            ;;
          *)
            echo "Incorrect rule specification"
            exit 1;
            ;;
        esac
        #there can be rule validation here.. TODO
        echo "delete from permissions_rules where type like '$type' and rule like '$4';" | isql $dbdsn $dbuser $dbpassword > /dev/null
        echo "Rule removed from database!"
        ;;
      show)
        echo "rules.allow rules in database:"
        echo "select rule from permissions_rules where type like 'A';" | isql $dbdsn $dbuser $dbpassword -du -b
        echo
        echo "rules.deny rules in database:"
        echo "select rule from permissions_rules where type like 'D';" | isql $dbdsn $dbuser $dbpassword -du -b
        ;;
      upload)
        echo "caution: use this only on machine, where Kamailio is running in order to maintain file consistency!"
        read -p "Are you sure you want to continue? [y/N] " decision
        decision=`echo $decision | tr '[A-Z]' '[a-z]'`
        if [ "$decision" == "y" ]; then
          echo "Removing old rules from DB."
          echo "delete from permissions_rules;" | isql $dbdsn $dbuser $dbpassword > /dev/null
          echo "Done."
          #allow rules:
          echo -n "Parsing and uploading rules.allow file rules..."
          cat /etc/kamailio/rules.allow | while read line
          do
            if [ "${line:0:1}" != "#" ]; then
              echo "insert into permissions_rules(type,rule) values('A','$line');" | isql $dbdsn $dbuser $dbpassword > /dev/null
            fi
          done
          echo "Allow rules uploaded!"
          #deny rules:
          echo -n "Parsing and uploading rules.deny file rules..."
          cat /etc/kamailio/rules.deny | while read line
          do
            if [ "${line:0:1}" != "#" ]; then
              echo "insert into permissions_rules(type,rule) values('D','$line');" | isql $dbdsn $dbuser $dbpassword > /dev/null
            fi
          done
          echo "Deny rules uploaded!"
        else
          echo
        fi
        ;;
      *)
       echo "Incorect input."
       exit 1;
       ;;
    esac
    exit 0;
    ;;
  *)
    echo "Syntax:"
    echo "  permissions:"
    echo "    SIPuser permissions add <allow|deny> '<rule>'   ...  adds new rule <rule> of type allow or deny to the database"
    echo "                                                         (don't forget to enclose the <rule> in \"'\" signs)"
    echo "    SIPuser permissions rm <allow|deny> '<rule>'    ...  removes rules <rule> of type allow or deny to the database"
    echo "                                                         (don't forget to enclose the <rule> in \"'\" signs)"
    echo "    SIPuser permissions show            ...  shows the database stored rules"
    echo "    SIPuser permissions upload          ...  uploads the rules stored in local rules.allow|deny files to the database"
    echo "                                             (caution: use this only on machine, where Kamailio is running in order to maintain"
    echo "                                              file consistency!)"
    echo
    exit 2
    ;;
esac

This script utilizes isql command, which is part of the ODBC package. This script is only a simple example of interaction with the database, but can be used for quick tasks. Surely other, for example web-based applications can be developed to edit contents of the database.

 

In this article I tried to talk about permission checking for call establishment in Kamailio SIP server and synchronization of permissions rules across multiple cluster nodes.

Bear in mind that this is NOT the only way of achieving the goal :). Any comments are welcome.

Skupiny: