Hardening Your Kamailio Configuration
5 min read

Hardening Your Kamailio Configuration

In this article, we implement user-agent blocking, geographic-based IP blocking, and IP address allow listing for Kamailio.

Given the current landscape of DDOS (Distributed Denial of Service) attacks against VoIP service providers, it has never been more important to harden your telephony infrastructure against attacks. In this article, we are going to implement user-agent blocking, geographic-based IP blocking, and IP address allow listing. In this article, we will be utilizing Kamailio's antiflood, auth, geoip2, htable, permissions, pike, registrar, secfilter, and usrloc module. In a later article, we will explore utilizing iptables to block malicious SIP messages.

Prerequisites

  • Your Kamailio instance is connected to a database backend.

  • Your Kamailio database has the secfilter table

  • You have kamailio-geoip2-modules installed

  • You have downloaded the GeoLite2 database

    • Place the GeoLite2-Country.mmdb file in the following directory:
      • /etc/kamailio
  • You have the kamcmd utility properly configured

    • You can signup for and download the database by visiting this link.

Step 1 — Add directives to your kamailio.cfg file

At the top of your kamailio.cfg file, below #!KAMAILIO we will add five new directives (if they do not already exist):

  1. #!define WITH_GEOIP2
  2. #!define WITH_SECFILTER
  3. #!define WITH_ANTIFLOOD
  4. #!define WITH_AUTH
  5. #!define WITH_IPAUTH

Your Kamailio header should look something like the following:

#!KAMAILIO
#!define WITH_MYSQL
#!define WITH_GEOIP2
#!define WITH_SECFILTER
#!define WITH_ANTIFLOOD
#!define WITH_AUTH
#!define WITH_IPAUTH

Step 2 — Update your Modules Section

Update your #!ifdef WITH_MYSQL section to the following:

#!ifdef WITH_MYSQL
loadmodule "db_mysql.so"
loadmodule "sqlops.so"
#!endif

Above the #!ifdef WITH_DEBUG add the following:

#!ifdef WITH_GEOIP2
loadmodule "geoip2.so"
#!endif

#!ifdef WITH_SECFILTER
loadmodule "secfilter.so"
#!endif

Step 3 — Update your module-specific parameters

Add the following line to your #!ifdef WITH_ANTIFLOOD section:

modparam("htable", "htable", "auth_try=>size=12;initval=0;autoexpire=86400")

Above the #!ifdef WITH_DEBUG section add the following (update the DBURL with your database connection string otherwise it will not work):

#!ifdef WITH_GEOIP2
modparam("geoip2", "path", "/etc/kamailio/GeoLite2-Country.mmdb")
#!endif

#!ifdef WITH_SECFILTER
modparam("secfilter", "db_url", DBURL)
#!endif

# ---— sqlops params -----
modparam("sqlops","sqlcon", "cb=>DBURL")

Step 4 — Update route[REQINIT] section

Update your route[REQINIT] section to the following:

# Per SIP request initial checks
route[REQINIT] {
    # No connect for sending replies
	set_reply_no_connect();

	# Enforce symmetric signaling
	# Send back replies to the source address of request
	force_rport();

#!ifdef WITH_SECFILTER
    # Check for SQL Injection attacks
    # Search for illegal characters in several headers (user-agent, from, to and contact).
    # If illegal characters are found the message will be dropped.
    secf_check_sqli_all();
    xinfo("[REQINIT]: $rm from $si allowed for call-id: $(ci), passed sql injection attack test\n");

#!ifdef WITH_GEOIP2
    # Identify the source of the messsage via IP address identification
    # If the IP Address is not from the US, but is contained in the "address" table then the message will be accepted.
    if (geoip2_match("$si", "src")) {
        secf_check_country($gip2(src=>cc));
        # return values ...
        #  2 = allowlisted
        #  1 = not found
        # -1 = error
        # -2 = blacklisted
        if ($? != 2 && !allow_source_address_group()) {
            xinfo("[REQINIT]: $rm from $si blocked for call-id: $(ci), source IP country '$gip2(src=>cc)' is not allowlisted\n");
            exit;
        }
        xinfo("[REQINIT]: $rm from $si allowed for call-id: $(ci), source IP country '$gip2(src=>cc)' is allowlisted or is from an approved IP\n");
    }
#!endif

    # Identify the user agent of the message via user-agent header
    # If the user agent is on the block list, it will be dropped.
    secf_check_ua();
    if ($? == -2) {
        # return values ...
        #  2 = allowlisted
        #  1 = not found
        # -1 = error
        # -2 = blacklisted
        xinfo("[REQINIT]: $rm from $si blocked for call-id: $(ci), useragent '$ua' is blocklisted\n");
        exit;
    }
    xinfo("[REQINIT]: $rm from $si allowed for call-id: $(ci), useragent '$ua' is not blocklisted\n");
#!endif

#!ifdef WITH_ANTIFLOOD
	# Flood detection from same IP and traffic ban for a while
	if(src_ip!=myself && !allow_source_address_group()) {
		if($sht(ipban=>$si)!=$null) {
			# ip is already blocked
			xinfo("[REQINIT]: $rm from $si blocked for call-id: $(ci), request from blocked IP\n");
            exit;
		}
        xinfo("[REQINIT]: $rm from $si allowed for call-id: $(ci), request from a unblocked IP\n");
		if (!pike_check_req()) {
			xinfo("[REQINIT]: $rm from $si blocked for call-id: $(ci), pike blocking\n");
			$sht(ipban=>$si) = 1;
			exit;
		}
        xinfo("[REQINIT]: $rm from $si allowed for call-id: $(ci), pike not blocking\n");
	}

    if($sht(auth_try=>$si) > 20){
        xinfo("[REQINIT]: $rm from $si blocked for call-id: $(ci), exceeds register attempts\n");
        exit;
	}
    xinfo("[REQINIT]: $rm from $si allowed for call-id: $(ci), current register attempts = $sht(auth_try=>$si) for $si\n");
#!endif

	if (!sanity_check()) {
		xinfo("[REQINIT]: $rm from $si blocked for call-id: $(ci), malformed SIP request\n");
		exit;
	}
    xinfo("[REQINIT]: $rm from $si allowed for call-id: $(ci), request is sane\n");

    if (!mf_process_maxfwd_header("10")) {
        xinfo("[REQINIT]: $rm from $si blocked for call-id: $(ci), too many hops\n");
		sl_send_reply("483","Too Many Hops");
		exit;
	}
    xinfo("[REQINIT]: $rm from $si allowed for call-id: $(ci), passed hop test\n");

    # Hostname in contact
    if($sel(contact.uri.host) =~ "^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$") {
        xinfo("[REQINIT]: $rm from $si blocked for call-id: $(ci), using IP instead of dns for hostname\n");
        exit;
    }
    xinfo("[REQINIT]: $rm from $si allowed for call-id: $(ci), hostname is not IP\n");

    # Check supported methos
    if (!is_method("REGISTER|INVITE|ACK|BYE|CANCEL|INFO|UPDATE|OPTIONS")) {
        sl_send_reply("405", "Method not allowed");
        exit;
    }
    xinfo("[REQINIT]: $rm from $si allowed for call-id: $(ci), method is allowed\n");

    if(is_method("OPTIONS")) {

	    # Only reply to option messages if the endpoint or the carrier is defined
	    if (allow_source_address_group()) {
            xinfo("[REQINIT]: $rm from $si allowed for call-id: $(ci), sent from allowed address group, responding with OPTIONS\n");
		    sl_send_reply("200","Keepalive");
		    exit;
        }
#!ifdef WITH_USRLOCDB
        # We need to find out if the location of the Options request is from a registered Endpoint
        if (sql_xquery("cb", "select * from location where contact like '%$si%'", "ra")) {
            if ($dbr(ra=>rows) == 0){
                xinfo("[REQINIT]: $rm from $si blocked for call-id: $(ci), is not from a registered endpoint\n");
                sql_result_free("ra");
                exit;
            }
            xinfo("[REQINIT]: $rm from $si allowed for call-id: $(ci), is from a registered endpoint\n");
            sl_send_reply("200","Keepalive");
            sql_result_free("ra");
            exit;
        }
#!endif
    }

}

Step 5 — Add user agents to the block-list

Let's add some of my favorite User-Agents to immediately block. At the command line (BASH), execute the following commands:

kamcmd secfilter.add_bl ua friendly-scanner
kamcmd secfilter.add_bl ua sipcli/
kamcmd secfilter.add_bl ua VaxSIPUserAgent/
kamcmd secfilter.add_bl ua pplsip
kamcmd secfilter.add_bl ua system 
kamcmd secfilter.add_bl ua exec.
kamcmd secfilter.add_bl ua sipsak
kamcmd secfilter.add_bl ua sipvicious
kamcmd secfilter.add_bl ua siparmyknife
kamcmd secfilter.add_bl ua VaxIPUserAgent
kamcmd secfilter.add_bl ua SIParator/
kamcmd secfilter.add_bl ua eyebeam

Step 6 — Add Countries to your allow-list

Add the two letter country code for countries you expect to receive traffic from:

kamcmd secfilter.add_wl country US

Step 7 — Add IP Addresses to your address table

This is where you want to add your upstream carrier's static IPs and your customer's static IPs.

Assuming Group 1 for Carriers and Group 2 for Customers (Groups are arbitrary and can be whatever you like to differentiate them), add your carriers and customers to the address table similar to the following:

insert into address (id, grp, ip_addr, mask, port, tag) values (1, 1, '1.1.1.1', 32, 0, 'Carrier');
insert into address (id, grp, ip_addr, mask, port, tag) values (2, 2, '2.2.2.2', 32, 0, 'Customer');

Step 8 — Restart Kamailio

Go ahead and restart your Kamailio instance

systemctl restart kamailio

Code Review

If you would like to review a complete example of this, please view my GitHub Gist.