Kamailio and FreeSWITCH on the same server with NSQ and JANSSON-RPC

This post will demonstrate how to run FreeSWITCH and Kamailio on a single server.
FreeSWITCH will handle authentication and act as registrar while Kamailio will handle presence updates using the NSQ module. You might be wondering why this setup would be useful. The reason we found, is that FreeSWITCH is not so great at handling presence updates. It relies heavily on SQL queries, and when you are dealing with 800+ user-agents all doing presence, FreeSWITCH can’t really keep up. You also may be wondering why not have Kamailio act as registrar. The reason FreeSWITCH will be registrar is we had a system in place already (single FreeSWITCH instance) and it would require too large of a change to move authentication to Kamailio. So this exact setup may not be so useful for many, it does contain helpful hints on how to run Kamailio and FreeSWITCH together on a single server, it also demonstrates use of NSQ and the JANSSON-RPC module.

In order for FreeSWITCH and Kamailio to run on a single server, both services must bind to different ports on a single interface or on separate interfaces altogether. In this setup, I have FreeSWITCH setup to bind SIP on the loopback interface (127.0.0.1), RTP on the public facing interface and Kamailio binding to the public facing interface (4.5.6.7:5060). I used the dispatcher module to detect if FreeSWITCH is up or down.
Here is my dispatcher.list:

#setid sipdest flags priority attrs
1 sip:127.0.0.1:5060 2 0 duid=1

Next you’ll have to setup a FreeSWITCH sofia profile to bind to loopback for SIP and public interface for RTP:

<configuration name="sofia.conf" description="Sofia">
  <profiles>
    <profile name="local">
      <settings>
        ...
        <param name="sip-ip" value="127.0.0.1"/>
        <param name="sip-port" value="5060"/>
        <param name="rtp-ip" value="4.5.6.7"/>
        ...
      </settings>
    </profile>
  </profiles>
</configuration>

I named the profile local but you can obviously name it whatever you like. That’s it for the FreeSWITCH side of things, make sure your profile is loaded by doing:
fs_cli -x "sofia status profile local"

You should see in output:

RTP-IP                 4.5.6.7
SIP-IP                 127.0.0.1
URL                     sip:mod_sofia@127.0.0.1:5060
BIND-URL                sip:mod_sofia@127.0.0.1:5060;transport=udp,tcp

Now it’s time to setup Kamailio, I’ll highlight some important sections and attach the entire kamailio.cfg at the end of post.
First thing I like to do in my kamailio.cfg is setup the global definitions that will be used later (that way the script is more portable):

# - defines
#!define DBURL "postgres://kamailio:kamailio@localhost/kamailio"
#!define JANSSON_RPC "conn=presence;addr=localhost;port=8080;priority=10;weight=10"
#!define LISTEN 4.5.6.7:5060

The DBURL is required for presence and NSQ modules, so you will need to provision your database with the appropriate tables. Now define the module parameters, the ones I will highlight are related to NSQ:

modparam("nsq", "db_url", DBURL)
modparam("nsq", "consumer_workers", 1)
modparam("nsq", "topic_channel", "presence:kamailio")
modparam("nsq", "max_in_flight", 200)
modparam("nsq", "lookupd_address", "nsqlookup01")
modparam("nsq", "pua_mode", 1)

The topic_channel tells Kamailio to listen for messages on the presence topic and uses the kamailio channel. The lookupd_address tells kamailio to how to find the producers. If you are unfamiliar with NSQ, I suggest you check out the documentation (http://nsq.io/)

I setup a JANSSON-RPC server running on the localhost (written in golang) which checks for active calls when a user-agent subscribes and sends an NSQ message to Kamailio in the event the To-User is actually on a call. Here we define the JANSSON-RPC server:

modparam("janssonrpc-c", "result_pv", "$var(jsrpc_result)")
modparam("janssonrpc-c", "server", JANSSON_RPC)

The result_pv is used to receive results from the RPC request. In our case, we don’t care about the result since our NSQ module will receive a message on result.

Now let’s check out the route section of our config. You’ll notice one of the first thing that happens in our config is CHECK_SOURCE_IP

route[CHECK_SOURCE_IP] {
    if ($si == "127.0.0.1") {
        setflag(FLAG_FROM_FREESWITCH);
    } else {
        route(NAT_TEST_AND_CORRECT);
    }
}

Since we should only be receiving requests from FreeSWITCH on the loopback, we can easily determine “who” is sending our SIP packet. I set a flag here, FLAG_FROM_FREESWITCH, which is very important later on. If the packet is not from FreeSWITCH, we do some NAT handling:

route[NAT_TEST_AND_CORRECT] {
    if (is_method("REGISTER")) {
        if (nat_uac_test("19")) {
            fix_nated_contact();
        }
    } else {
        if (nat_uac_test("3")) {
            fix_nated_contact();
            force_rport();
        }
        if (has_body("application/sdp") && nat_uac_test("8")) {
            fix_nated_sdp("10");
        }
    }
}

We handle NAT for REGISTERs in a specific way here because we must send the correct contact header to FreeSWITCH, otherwise FreeSWITCH will get contact header containing a private IP and will not know how to route INVITEs to the user-agent.

Next we handle the SIP SUBSCRIBE:

route[SUBSCRIBE] {
    if (is_method("SUBSCRIBE")) {
 
        if (!t_newtran()) {
            sl_reply_error();
            exit;
        }

        if ($tU == $null) {
            xlog("L_INFO", "$ci|stop|ignoring subscribe with empty TO username from a $ua");
            sl_send_reply(400, "Missing TO username");
            t_release();
            exit;
        }

        if ($fU == $null) {
            xlog("L_INFO", "$ci|stop|ignoring subscribe with empty FROM username from a $ua");
            sl_send_reply(400, "Missing FROM username");
            t_release();
            exit;
        }

        # do an RPC request to check for Active calls
        if ($rU != $null) {
            # forward message-summary SUBSCRIBEs to FreeSWITCH
            if ($hdr(Event) == "message-summary") {
                xlog("L_INFO", "$ci|log|CHECKING MESSAGE SUMMARY");
                record_route();
                route(DISPATCH);
                route(RELAY);
            } else {
                xlog("L_INFO", "$ci|log|CHECKING PRESENCE");
                janssonrpc_notification("presence", "Server.QueryPresence", '[{"CallId":"' + $ci + '","FromUser":"' + $fU + '","FromDomain":"' + $fd + '","ToUser":"' + $rU + '","ToDomain":"' + $rd + '"}]');
            }
        }

        if (!handle_subscribe()) {
            xlog("L_INFO", "$ci|stop|unsupported subsribe");
            t_release();
            exit;
        }

        t_release();
        exit;
    }
}

We are validating the SUBSCRIBE, then checking the event type. If it’s a message-summary event, we add a record-route header and forward it to FreeSWITCH. (We could handle message-summary using NSQ but right now it’s easier for FreeSWITCH to handle it and doesn’t put a strain on FreeSWITCH)
If it’s not a message-summary event, we send an RPC request to our service which should know if the To-User is on a call or not, if they are on a call, our RPC server sends an NSQ message to the presence topic.
You’ll notice the next section is for handling NSQ messages:

# receive presence updates from NSQ and update watchers
event_route[nsq:consumer-event-presence-update] {
    $var(call-id) = $(nsqE{nsq.json,Call-ID});
    xlog("L_INFO", "$var(call-id)|log|payload $nsqE");
    if ($(nsqE{nsq.json,Event-Package}) == "dialog") {   
        if($sht(p=>$var(call-id)) != $(nsqE{nsq.json,State}) || $(nsqE{nsq.json,Flush-Level}) != $null) {
             xlog("L_INFO", "$(nsqE{nsq.json,Call-ID})|log|received $(nsqE{nsq.json,Event-Package}) update for $(nsqE{nsq.json,From}) state $(nsqE{nsq.json,State})");
             $sht(p=>$(nsqE{nsq.json,Call-ID})) = $(nsqE{nsq.json,State});
             nsq_pua_publish($nsqE);
             pres_refresh_watchers("$(nsqE{nsq.json,From})", "$(nsqE{nsq.json,Event-Package})", 1);
        } else {
            xlog("L_INFO", "$var(call-id)|log|received duplicate $(nsqE{nsq.json,Event-Package}) update for $(nsqE{nsq.json,From}) state $(nsqE{nsq.json,State})");
            xlog("L_INFO", "$var(call-id)|log|payload $nsqE");
        } 
    } else {
       xlog("L_INFO", "$var(call-id)|log|received $(nsqE{nsq.json,Event-Package}) update for $(nsqE{nsq.json,From}) $nsqE");
       nsq_pua_publish($nsqE);
       pres_refresh_watchers("$(nsqE{nsq.json,From})", "$(nsqE{nsq.json,Event-Package})", 1);
    }
}

The NSQ message must be json format and contain the required fields for the presence module to send an update. I’ll save those details for another post.

Let’s go back up to the main routing block of our config now.

    # handle SUBSCRIBE requests
    route(SUBSCRIBE);

    # handle requests within SIP dialogs
    route(WITHINDLG);

    ###############################
    ### HANDLE INITIAL REQUESTS ###
    # handle retransmissions
    if(t_precheck_trans()) {
        t_check_trans();
        exit;
    }
    t_check_trans();

    if (is_method("INVITE|REFER")) {
        record_route();
    }

    if (is_method("NOTIFY") && $hdr(event) == "check-sync" && isflagset(FLAG_FROM_FREESWITCH)) {
        record_route();
        xlog("L_INFO", "$ci|log|Rebooting phone [$ru]\n");
        t_on_reply("REPLY_FROM_DEVICE"); # handle NAT
        route(RELAY);
    }

After handling SUBSCRIBEs, we check for in-dialog request and route them. We add a record_route header if INVITE or REFER. Then we handle NOTIFYs with check-sync events specially. This allows us to remotely reboot phones which support the feature.

The next portion is important when forwarding a REGISTER to another registrar:

    if (!isflagset(FLAG_FROM_FREESWITCH) && is_method("REGISTER")) {
        add_path();
    }

If we do not add a path header to the REGISTER before forwarding to FreeSWITCH, FreeSWITCH will not know to send INVITEs to user-agents through Kamailio.

The last part of our main routing block does some final checks to decide how/where to send the packet.

    route(DISPATCH);
   
    route(RELAY);

Our dispatch route checks if the SIP signal is from FreeSWITCH, if so, fix NAT on replies to it. If not from FreeSWITCH (e.g. from outside user-agent), send to FreeSWITCH:

route[DISPATCH] {
    if (isflagset(FLAG_FROM_FREESWITCH)) {
        t_on_reply("REPLY_FROM_DEVICE"); # handle NAT
    } else if (!ds_select_dst("1", "0")) {
        #if we are here that means no destination is available. We notify the user by 404 and exit the script.
        xlog("L_NOTICE", "No destination available!");
        send_reply("404", "No destination");
        exit;
    }
}

And here is the relay route:

route[RELAY] {
    if (is_method("INVITE")) {
        if(!t_is_set("failure_route")) t_on_failure("MANAGE_FAILURE");
    }

    if (!t_relay()) {
       sl_reply_error();
    }
    exit;
}

And that’s it!
Here is the full kamailio.cfg

 

Setup an SMTP server with user authentication using postgres, postfix, and dovecot on Debian 8

Please note: this tutorial assumes postgres is already setup and configured on the machine.
Install & configure the dovecot and postfix:

apt-get install dovecot-core dovecot-pgsql postfix -y

Provision the smtp database in postgres:

su -c "psql -c \"CREATE USER smtp WITH PASSWORD 'smtp';\"" postgres
su -c "psql -c \"CREATE DATABASE smtp WITH OWNER=smtp;\"" postgres

Encrypt you password using doveadm tool and SHA512-CRYPT:

doveadm pw -s SHA512-CRYPT -p test1234 -r 100000
************************

Create the SQL file with your one test user:

cat > smtp.sql<<EOT
CREATE TABLE users (
    id SERIAL,
    username VARCHAR(128) NOT NULL,
    password VARCHAR(512) NOT NULL,
    constraint username_key unique (username)
);
insert into users (username, password) values ('emailuser01', '************************');

EOT

Import your SQL table and test user:

psql -h localhost -d 'smtp' -U smtp -W < smtp.sql

Configure dovecot
Open /etc/dovecot/conf.d/10-master.conf in your favorite text editor and uncomment the following:

  # Postfix smtp-auth
  unix_listener /var/spool/postfix/private/auth {
    mode = 0666
  }

Enable SQL and login auth:

sed -i "s/auth_mechanisms = plain/auth_mechanisms = plain login/g" /etc/dovecot/conf.d/10-auth.conf
sed -i "s/\!include auth-system.conf.ext/#\!include auth-system.conf.ext/g" /etc/dovecot/conf.d/10-auth.conf
sed -i "s/#\!include auth-sql.conf.ext/\!include auth-sql.conf.ext/g" /etc/dovecot/conf.d/10-auth.conf

Disable the default password scheme

sed -i "s/default_pass_scheme/#default_pass_scheme/g" /etc/dovecot/dovecot-sql.conf.ext

Enable SHA512-CRYPT and set database parameters

cat >>/etc/dovecot/dovecot-sql.conf.ext<<EOT
default_pass_scheme = SHA512-CRYPT
driver = pgsql
connect = host=localhost dbname=smtp user=smtp password='smtp'
password_query = select username as user, password from users where username = '%n';

EOT

Restart dovecot

service dovecot restart

Configure postfix to use dovecot for sasl

postconf -e 'smtpd_sasl_type = dovecot'
postconf -e 'smtpd_sasl_auth_enable = yes'
postconf -e 'smtpd_recipient_restrictions = permit_sasl_authenticated,permit_mynetworks,reject_unauth_destination'
postconf -e 'smtpd_sasl_path = private/auth'

Enable submission for clients on port tcp/587

sed -i "s/\#submission/submission/g" /etc/postfix/master.cf

Restart postfix

service postfix restart

Email client setup example

Now configure your mail client to use the SMTP server. For this example we will say the mail client is postfix and the smtp server we just setup has an IP of 192.168.0.186

192.168.0.186:587 emailuser01:test1234 > /etc/postfix/sasl_passwd
postmap hash:/etc/postfix/sasl_passwd
cat >>/etc/postfix/main.cf <<EOT

smtp_sasl_auth_enable = yes
smtp_sasl_mechanism_filter = plain, login
smtp_sasl_security_options = noanonymous
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
relayhost = 192.168.0.186:587

EOT

Restart postfix

service postfix restart

Send test email

echo "TEST" > testemail
mail -s "TEST EMAIL" me@myemaildomain.com < testemail

RTPEngine with Kamailio as Load-balancer and IP Gateway

This post explains how to setup Kamailio as an SBC and IP Gateway. We are using Debian 8 in this example. It uses Kamailio’s dispatcher module to distribute calls to Asterisk. It uses RTPEngine to proxy media to & from the public internet across the LAN to Asterisk.
This is a powerful setup as you can easily scale out using a single public IP address.
Here is the IP layout we will be implementing:

Kamailio Public (eth0) 7.6.5.4
Kamailio Private (eth0) 10.10.10.254
Asterisk machine 1 - 10.10.10.3
Asterisk machine 2 - 10.10.10.4

Setup Debian and iptables to act as IP Gateway:

# edit /etc/sysctl.conf
enabled net.ipv4.ip_forward=1

# add iptables rules
iptables -F
iptables -t nat -F

iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT
iptables -P FORWARD ACCEPT

iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i eth1 -s 10.10.10.0/255.255.255.0 -j ACCEPT
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

Install RTPEngine + Kamailio with RTPEngine Module.

 

Download kamailio.cfg

Download sip.conf

Download /etc/default/ngcp-rtpengine-daemon

Kamailio: High-Availability/Failover with Corosync and Pacemaker on Debian 7

In this setup, we will have 2 Kamailio servers, referred to as ‘nodes’. One will be active and one will standby. There is a 3rd ‘floating’ IP that is moved to which ever node is active. Kamailio should be configured to use the floating IP. In this example, the nodes are:

kam01: 10.10.10.18
kam02: 10.10.10.19

Floating IP: 10.10.10.200

First, add both nodes to /etc/hosts

10.10.10.18    kam01
10.10.10.19    kam02

Install corosync and pacemaker:

apt-get install ntp corosync pacemaker -y

Generate /etc/corosync/authkey:

corosync-keygen

Copy corosync-key  to kam02:

scp -P 22 /etc/corosync/authkey root@kam02:/etc/corosync

Enable corosync on kam01 & kam02:

sed -i "s/START=no/START=yes/g" /etc/default/corosync

Enable pacemaker service in corosync:

cat > /etc/corosync/service.d/pcmk << EOF
service {
  name: pacemaker
  ver: 1
}
EOF

Add corosync config:

cat >/etc/corosync/corosync.conf<<EOF
#
totem {
        version: 2
        transport: udpu
        interface {
                member {
                        memberaddr: 10.10.10.18
                }
                member {
                        memberaddr: 10.10.10.19
                }
                ringnumber: 0
                bindnetaddr: 10.10.10.0
                mcastport: 5405
        }
}

logging {
        to_logfile: yes
        logfile: /var/log/corosync/corosync.log
        debug: off
        timestamp: on
        logger_subsys {
                subsys: AMF
                debug: off
        }
}
EOF

Start corosync and pacemaker:

service corosync start
service pacemaker start

Configure corosync, changes will propagate to other node. Make sure to disable stonith:

crm configure property stonith-enabled=false
crm configure primitive FAILOVER-IP ocf:heartbeat:IPaddr2 params ip="10.10.10.200" nic="eth0" cidr_netmask="255.255.255.0" op monitor interval="10s"
crm configure primitive KAM-HA lsb:kamailio op monitor interval="30s"
crm configure group KAM-HA-GROUP FAILOVER-IP KAM-HA
crm configure colocation KAM-HA-GROUP-COLO inf: FAILOVER-IP KAM-HA
crm configure order KAM-HA-ORDER inf: FAILOVER-IP KAM-HA
crm configure property no-quorum-policy=ignore

You need no-quorum-policy=ignore for a 2 node cluster. If you messed up during the crm configure part, you can start over using these commands:

# TO CLEAR CONFIG AND START OVER
crm configure property stop-all-resources=true
crm configure erase

It’s sometimes useful to see what resource agents are available, you can check with these commands:

# TO LIST RESOURCE AGENTS
crm ra list lsb
crm ra list systemd # (if using systemd init system)
crm ra list ocf heartbeat
crm ra list ocf pacemaker

If you need to migrate services and floating IP to other node you can run:

crm resource migrate KAM-HA-GROUP kam02

If you need edit specific parameters in the config, you can export it to xml, make the changes and re-import them:

# ADVANCED EDITING
cibadmin --query > tmp.xml
vi tmp.xml
cibadmin --replace --xml-file tmp.xml

 

RTPProxy: Compiling & Installing on Debian 8

cd /usr/src/
git clone https://github.com/sippy/rtpproxy.git
cd rtpproxy
./configure
make
make install

cp scripts/rtpproxy.init.debian /etc/init.d/rtpproxy
chmod +x /etc/init.d/rtpproxy
sed -i "s/DAEMON=\/usr\/bin\/rtpproxy/DAEMON=\/usr\/local\/bin\/rtpproxy/g" /etc/init.d/rtpproxy

cat > /etc/default/rtpproxy <<EOT
# Defaults for rtpproxy
# I like to use same user as Kamailio when running on Kamailio box
USER="kamailio"
GROUP="kamailio"
PIDFILE="/var/run/rtpproxy.pid"
# replace with your network interface IP address
LISTEN_ADDR=9.8.7.6

# The control socket.
CONTROL_SOCK="unix:/var/run/rtpproxy.sock"
# To listen on an UDP socket, uncomment this line:
#CONTROL_SOCK=udp:127.0.0.1:7722

# Additional options that are passed to the daemon.
EXTRA_OPTS="-l \$LISTEN_ADDR"

DAEMON_OPTS="-s \$CONTROL_SOCK -u \$USER:\$GROUP -p \$PIDFILE \$EXTRA_OPTS"

EOT

/etc/init.d/rtpproxy start

FreeSWITCH: High call volume alert script

I was tasked to come up with a way to monitor for high call volume in FreeSWITCH. I came up with this here is a simple script that will check the channel count and email me if it’s greater than $MAX_CALLS

 

#!/bin/bash

MAX_CALLS=100
CHANNEL_COUNT=$(/usr/bin/fs_cli -x "show channels count" | awk '/total/ {print $1}')

if [ $CHANNEL_COUNT -gt $MAX_CALLS ]; then
        # From
        FROM="alert@voipxswitch.com"
        # Subject
        SUBJECT="High Call Volume"
        # To
        TOEMAIL="admin@voipxswitch.com"

        /usr/bin/mailx "-aFrom:$FROM" -s "$SUBJECT" "$TOEMAIL"<<END
This is an email alert to notify you that the server has reached $CHANNEL_COUNT calls.
END

fi

Kamailio: Compiling & Installing on Debian 8

Here we will compile and install Kamailio from GIT master branch and create our own systemd service configuration file.

# install dependencies
apt-get install libpq-dev pkg-config build-essential bison make libperl-dev git linux-headers-$(uname -r) libunistring-dev flex libjson-c-dev libevent-dev 

# download & compile
cd /usr/src
git clone git://git.sip-router.org/kamailio kamailio-git
cd kamailio-git

# include your own modules, these are what i compile
make include_modules="db_postgres debugger usrloc dispatcher registrar auth auth_db avp avpops tm rr pv sl maxfwd nathelper textops siputils uac uac_redirect db_text xlog sanity htable app_perl path ctrl tls ctl mi_fifo kex permissions dmq dialog websocket xhttp textopsx sdpops kazoo tmx uuid presence presence_xml presence_dialoginfo presence_mwi outbound" cfg

make all

make install

# add kam group/user
groupadd -g 6001 kamailio
useradd -u 6001 -g 6001 -d /usr/local/kamailio -M -s /bin/false kamailio

# create custom directories for kamailio
mkdir -p /usr/local/kamailio/{run,etc,tmp}

# create kamailio default file
cat >/etc/default/kamailio <<EOT
RUN_KAMAILIO=yes
USER=kamailio
GROUP=kamailio
SHM_MEMORY=64
PKG_MEMORY=8
PIDFILE=/usr/local/kamailio/run/kamailio.pid
CFGFILE=/usr/local/kamailio/etc/kamailio.cfg
#DUMP_CORE=yes
EOT

# create systemd file
cat >/etc/systemd/system/kamailio.service<<EOT
[Unit]
Description=Kamailio (OpenSER) - the Open Source SIP Server
After=syslog.target network.target

[Service]
Type=forking
EnvironmentFile=-/etc/default/kamailio
PIDFile=\$PIDFILE
# ExecStart requires a full absolute path
ExecStart=/usr/local/sbin/kamailio -P \$PIDFILE -f \$CFGFILE -m \$SHM_MEMORY -M \$PKG_MEMORY -u \$USER -g \$GROUP
ExecStopPost=/bin/rm -f \$PIDFILE
Restart=on-abort

[Install]
WantedBy=multi-user.target

EOT

# enable kamailio service
systemctl enable kamailio

# if you change the systemd file you can reload changes with:
systemctl daemon-reload

# set permissions to /usr/local/kamailio
chown kamailio:kamailio -R /usr/local/kamailio

Please note, by default, Kamailio will install it’s configuration files in /usr/local/etc/kamailio, modules in /usr/local/lib64/kamailio, and binaries in /usr/local/sbin

Kamailio: Basic SIP Proxy (all requests) Setup

In this example, I will share how to setup Kamailio to proxy SIP requests to a SIP switch (such as FreeSWITCH or Asterisk).
192.168.1.101 is the IP of Kamailio
192.168.1.102 is the IP of FreeSWITCH or Asterisk
Here are snippets from the main config script, kamailio.cfg:

...
 

#!define IPADDRESS "192.168.1.101"

#!define SWITCH_IPADDRESS "192.168.1.102"


#!define FLAG_FROM_SWITCH 1
#!define FLAG_FROM_USER 2

# ------------------ module loading ----------------------------------
loadmodule "tm.so"
loadmodule "rr.so"
loadmodule "pv.so"
loadmodule "sl.so"
loadmodule "maxfwd.so"
loadmodule "nathelper.so"
loadmodule "textops.so"
loadmodule "siputils.so"
loadmodule "xlog.so"
loadmodule "sanity.so"
loadmodule "path.so"

# ----------------- setting module-specific parameters ---------------
modparam("nathelper|registrar", "received_avp", "$avp(s:rcv)")
# -------------------------  request routing logic -------------------
# main routing logic

route {

        # per request initial checks
        route(SANITY_CHECK);

        # CANCEL processing
        if (is_method("CANCEL")) {
                if (t_check_trans()) {
                        t_relay();
                }
                exit;
        }

        route(CHECK_SOURCE_IP);

        ##################################
        ### HANDLE SEQUENTIAL REQUESTS ###
        route(WITHINDLG);

        ###############################
        ### HANDLE INITIAL REQUESTS ###
        t_check_trans();

        if (is_method("INVITE|REFER")) {
                record_route();
        }
       
        if (is_method("REGISTER")) {
            add_path();
        }

        if (isflagset(FLAG_FROM_SWITCH)) {
                # don't send INVITE from SWITCH back to SWITCH, set reply route to handle NAT and forward them along
                t_on_reply("EXTERNAL_REPLY");
        } else {
                # set destination to your SWITCH
                $du = "sip:192.168.1.102:5060";
        }

        route(RELAY);


}


route[SANITY_CHECK]
{
        if (!sanity_check()) {
                #xlog("L_WARN", "$ci|end|message is insane");
                exit;
        }

        if (!mf_process_maxfwd_header("10")) {
                #xlog("L_WARN", "$ci|end|too much hops, not enough barley");
                send_reply("483", "Too Many Hops");
                exit;
        }

        if ($ua == "friendly-scanner" ||
                $ua == "sundayddr" ||
                $ua =~ "sipcli" ) {
                #xlog("L_WARN", "$ci|end|dropping message with user-agent $ua");
                exit;
        }

        if ($si == IPADDRESS) {
                #xlog("L_WARN", "$ci|end|dropping message");
                exit;
        }

}


route[CHECK_SOURCE_IP]
{
        if ($si == SWITCH_IPADDRESS) {
                setflag(FLAG_FROM_SWITCH);
        } else {
                setflag(FLAG_FROM_USER);
        }
}

# Handle requests within SIP dialogs
route[WITHINDLG]
{
        if (has_totag()) {
                # sequential request withing a dialog should
                # take the path determined by record-routing
                if (loose_route()) {
                        route(RELAY);
                } else {
                        if (is_method("NOTIFY")) {
                                route(RELAY);
                        }
                        if (is_method("SUBSCRIBE") && uri == myself) {
                                # in-dialog subscribe requests
                                exit;
                        }
                        if (is_method("ACK")) {
                                if (t_check_trans()) {
                                        # no loose-route, but stateful ACK;
                                        # must be an ACK after a 487
                                        # or e.g. 404 from upstream server
                                        t_relay();
                                        exit;
                                } else {
                                        # ACK without matching transaction ... ignore and discard
                                        #xlog("ACK without matching transaction ... ignore and discard");
                                        exit;
                                }
                        }
                        sl_send_reply("404","Not here");
                }
                exit;
        }
}

onreply_route[EXTERNAL_REPLY]
{
        route(NAT_TEST_AND_CORRECT);
}


route[NAT_TEST_AND_CORRECT]
{
        if (nat_uac_test("3")) {
                if (is_method("REGISTER")) {
                        fix_nated_register();
                } else {
                        fix_nated_contact();
                }
                force_rport();
        }
        if (has_body("application/sdp") && nat_uac_test("8")) {
                fix_nated_sdp("10");
        }
}

route[RELAY]
{
        if (!t_relay()) {
                sl_reply_error();
        }
        exit;
}

This example will proxy all SIP requests to the switch. Typically, this is not recommended as Kamailio is able to handle the SIP REGISTERs but I’ll save that for another tutorial.

Syncing DNS Zones from Windows DNS Server to Linux Bind9 DNS Server

Windows Setup

First step is to dump the DNS zones from Windows into a file. Then generate an FTP command file which will upload the DNS zone file dump to your bind server. I’ve created a batch script to handle all of this:

@echo off
dnscmd /enumzones > dns.zones.txt
echo user ftp_bind_user> ftpcmd.dat
echo ftp_bind_password>> ftpcmd.dat
echo bin>> ftpcmd.dat
echo put dns.zones.txt>> ftpcmd.dat
echo quit>> ftpcmd.dat
ftp -n -s:ftpcmd.dat 192.168.1.101
del ftpcmd.dat
del dns.zones.txt

Some notes about the above script:

192.168.1.101 is the bind9 server’s IP address.

ftp_bind_user is the ftp user name setup on the bind9 server

ftp_bind_password is the ftp password setup on the bind9 server

I have this batch script run every hour, on the hour, using the Window task scheduler. This script will most likely need to run as an administrator in order to dump the DNS zones to a file.

Linux Setup

Now we setup the bash script. This script will first parse and format dns.zones.txt into a usable bind format, then it will scan all the current zone files and remove any that are not listed in the updated zone file. I set this script to run every hour, ten minutes after each hour. This script assumes you have bind running as user “bind” in group “bind”, please change the chown line accordingly.

#!/bin/bash

ZONE_PATH="/home/bind"
BIND_PATH="/var/lib/bind"
TMP=$(mktemp)
FC="dns.zones.txt"

for ZONE in $(awk '$2=="Primary" {print $1}' "${ZONE_PATH}/${FC}") do
   printf "zone ${ZONE} {\n\ttype slave;\n\tmasters { 192.168.1.100; };\n\tfile \"${BIND_PATH}/${ZONE}.zone\";\n};\n"
done > ${TMP}

for ZONE in "$BIND_PATH"/*.zone do
   grep -q "${ZONE}" "${TMP}" || rm -rf "${ZONE}"
done

mv ${TMP} /etc/bind/named.conf.slave-zones
chown bind:bind /etc/bind/named.conf.slave-zones

rndc reload

Some notes about the above script:

ZONE_PATH is where the dns.zones.txt file is uploaded to, this is of course determined by your FTP server setup (I setup proftpd with SQL backend, but you could easily setup vsftpd)

BIND_PATH is where we are telling bind to store the zone files.

192.168.1.100 is the IP Address of the Windows DNS Server

/etc/bind/named.conf.slave-zones is setup to be included in the “named.conf” file with the line:

include “/etc/bind/named.conf.slave-zones”;

Compiling bind9 on linux with Response Rate Limiting (to prevent DDoS DNS attacks)

This tutorial can easily be applied to most any linux system. I went through these steps on Debian 7 server.

First let’s setup the environment, this tutorial assumes you have no previous install of bind on the server.

mkdir -p /var/local/cache/bind
mkdir -p /usr/local/etc/bind

We are assuming group id and user id 5005 are free, you may need to substitute ids

groupadd -g 5005 bind
useradd -u 5005 -g 5005 -d /var/local/cache/bind -M -s /bin/false bind


Now let’s download the bind9 source code. This tutorial assumes you have the required dependencies installed. The only one I found tricky to locate was libkrb5-dev (on Debian you can install it with apt-get install libkrb5-dev)

cd /usr/src
wget http://ftp.isc.org/isc/bind9/cur/9.9/bind-9.9.5-P1.tar.gz
tar zxvf bind-9.9.5-P1.tar.gz
cd bind-9.9.5-P1
./configure '--enable-threads' '--enable-largefile' '--with-libtool' '--enable-shared' '--enable-static' '--with-openssl=/usr' '--with-gssapi=/usr' '--with-gnu-ld' '--with-geoip=/usr' '--enable-ipv6' '--enable-rrl'
make
make install
wget --user=ftp --password=ftp ftp://ftp.rs.internic.net/domain/db.cache -O /usr/local/etc/bind/db.root


Last step is to install the configuration files and startup scripts.

rndc-confgen -a -c /usr/local/etc/bind/rndc.key
cat > /etc/named.conf <<EOT
 include "/usr/local/etc/bind/rndc.key";
 include "/usr/local/etc/bind/named.conf";
EOT
cat > /usr/local/etc/named.conf <<EOT
#
controls {
 inet 127.0.0.1 port 953
 allow { 127.0.0.1; 192.168.1.100; } keys { "rndc-key"; };
 };
options {
 directory "/var/local/cache/bind";
 allow-new-zones yes;
 transfers-in 500;
 empty-zones-enable yes;
 //forwarders { 8.8.8.8; 8.8.4.4; };
 recursion no;
 //allow-transfer {"none";};
 allow-query { any; };
 dnssec-validation auto;
 auth-nxdomain no;    # conform to RFC1035
 listen-on-v6 { any; };
 rate-limit {
  responses-per-second 5;
  #window 5;
  #log-only yes;
  };
};
zone "." {
 type hint;
 file "/usr/local/etc/bind/db.root";
};
EOT
chown bind:bind -R /var/local/cache/bind
chown bind:bind -R /usr/local/etc/bind

Please note the init.d scripts only work on Debian based systems. I do not have init.d scripts for any other distribution.

Download the init.d script here

Download the init.d default file here


Copy the init.d script to /etc/init.d/bind9

Copy the init.d default file to /etc/default/bind9

chmod +x /etc/init.d/bind9
/etc/init.d/bind9 start