GeoIP Filtering for Asterisk et al

  • Posted on: 11 January 2011
  • By: netservers

This article is the first of a planned series of articles concerning open source security. In these articles we'll share information and Howtos concerning the installation and management of open source software.

The problem

In recent weeks we've had a number of attempts to abuse our voice over IP system coming from China, Russia and various Eastern European countries. These attackers have use SIP to attempt calls to both national and premium rate numbers. Thankfully our access control and SIP password policies have prevented these attacks being successful. However, we have taken the view that further steps should be taken to minimise our exposure to such attacks.

Risk Mitigation

When considering this attack, it's worth considering why we permit direct SIP connections to our servers in the first place and whether or not we should simply block all incoming SIP. The reasons why we cannot block all SIP are:

  1. We use several SIP trunk providers, some of whom operate in multiple Western European and US states.
  2. Several of our staff use SIP softphones on their Android/iPhones and occasionally travel around Europe.
  3. We advertise our SIP URI on our web site and invite international customers to use it.

The most notable thing about the attacks we've seen is that they are coming from countries where we don't currently do business. It occurred to us that we should filter incoming SIP calls from countries where we do not do business.

Some might question why we don't simply rely on keeping our servers up to date and cross our fingers that they'll be ok. Our answer is that keeping your server up to date does not protect you from zero-day attacks. Based on our observations, we can eliminate the vast majority of  attackers by blocking incoming SIP calls from sources that have invariably sent only attack traffic.

GeoIP Filtering on CentOS 5.5

Our Asterisk server is a CentOS 5.5 system with a 2.6.18 kernel. Unfortunately the GeoIP netfilter (iptables) module is not part of the CentOS kernel. Although we could build this module, this creates an administrative overhead that prevents us from upgrading to the latest kernels with Yum. For that reason, we decided to use a netfilter module that does come with CentOS to achieve the same result. That is the nfnetlink_queue module.

nfnetlink_queue sends packets to a userspace queue. We use this module to send initial incoming SIP packets to a perl script for evaluation. You could apply this technique to any packets, so this Howto is applicable to GeoIP based filtering under CentOS 5.

A userspace perl script is less robust than a kernel module. On its own, it is more susceptible to DoS attacks that a kernel-based solution. We have mitigated this risk, and improved our resilience to DoS attacks by combining our use of nfnetlink_queue with both the state and hashlimit modules, both of which are part of the CentOS kernel.

Step-by-Step

Install necessary RPMs:

# yum install cmake swig python-devel libnfnetlink-devel perl-NetPacket perl-Geo-IPfree

Download and install libnetfilter_queue:

# wget http://www.netfilter.org/projects/libnetfilter_queue/files/libnetfilter_queue-0.0.16.tar.bz2
# tar -jxvf libnetfilter_queue-0.0.16.tar.bz2 
# cd libnetfilter_queue-0.0.16
# ./configure --prefix=/usr
# make
# make install
# nano /usr/lib/pkgconfig/libnetfilter_queue.pc <-- Change prefix to /usr

Install perl nfqueue bindings library:

# wget http://www.nufw.org/attachments/download/32/nfqueue-bindings-0.3.tar.gz
# tar -zxvf nfqueue-bindings-0.3.tar.gz 
# cd nfqueue-bindings-0.3
# export PKG_CONFIG_PATH=/usr/lib/pkgconfig/libnetfilter_queue.pc

Apply this patch:

diff -uNr nfqueue-bindings-0.3.orig/FindPerlLibs2.cmake nfqueue-bindings-0.3/FindPerlLibs2.cmake
--- nfqueue-bindings-0.3.orig/FindPerlLibs2.cmake	2009-10-18 16:37:28.000000000 +0100
+++ nfqueue-bindings-0.3/FindPerlLibs2.cmake	2011-01-08 16:28:11.000000000 +0000
@@ -8,6 +8,7 @@
 #
 
 SET(PERL_POSSIBLE_INCLUDE_PATHS
+  /usr/lib/perl5/5.8.8/i386-linux-thread-multi/CORE
   /usr/lib/perl/5.8.3/CORE
   /usr/lib/perl/5.8.2/CORE
   /usr/lib/perl/5.8.1/CORE
@@ -18,6 +19,7 @@
 
 SET(PERL_POSSIBLE_LIB_PATHS
   /usr/lib
+  /usr/lib/perl5/5.8.8/i386-linux-thread-multi/CORE
   )
 
 FIND_PATH(PERL_INCLUDE_PATH perl.h

Build it:

# make
# make install

Perl script to process the packets (/usr/local/bin/geo_filter.pl):

#!/usr/bin/perl -w
#
# Copyright Netservers Limited (C) 2011
#
# Licensed under the GPL ver 2.0
# http://www.gnu.org/licenses/gpl-2.0.html
#

use strict;
use lib '/usr/local/lib/perl5';
use Geo::IPfree;
use Sys::Syslog qw(:DEFAULT setlogsock);

my $geo = Geo::IPfree->new;
my %ok = map { $_ => 1 } qw / GB US AC AT AU BE CA CH DE DK ES FI FR GG
                              IE IM IS JE LU NL NO PL SE ZA /; # Countries we allow
BEGIN {
  push @INC,"perl";
  push @INC,"build/perl";
  push @INC,"NetPacket-0.04";
};

use nfqueue;
use NetPacket::IP qw(IP_PROTO_TCP);
use NetPacket::TCP;
use Socket qw(AF_INET AF_INET6);

my $q;

setlogsock('unix');
openlog("geo_filter", "ndelay,pid", 'LOG_SECURITY');

sub cleanup() {
  $q->unbind(AF_INET);
  $q->close();
  syslog("info","Stopping");
  closelog;
}

sub cb() {
  my ($dummy,$payload) = @_;
  print "dummy is $dummy\n" if $dummy;
  if ($payload) {
        my $ip_obj = NetPacket::IP->decode($payload->get_data());
        if($ip_obj->{src_ip} =~ /^(192.168|10|172\.(1[6-9]|2[0-9]|3[0-1]))\./) {
                syslog("info","Allowing packet from RFC1918 $ip_obj->{src_ip}");
                $payload->set_verdict($nfqueue::NF_ACCEPT);
                return;
        }
        my( $code, $name ) = $geo->LookUp($ip_obj->{src_ip});
        if($ok{$code}) {
                syslog("info","Allowing packet from [$code,$name] $ip_obj->{src_ip}");
                $payload->set_verdict($nfqueue::NF_ACCEPT);
                return;
        }
	syslog("info","Dropped packet from [$code,$name] $ip_obj->{src_ip}");
        $payload->set_verdict($nfqueue::NF_DROP); # Default is to drop
  }
}

$q = new nfqueue::queue();
$q->open();
$q->bind(AF_INET);
$SIG{INT} = "cleanup";
$q->set_callback(\&cb);
syslog("info","Starting up");
$q->create_queue(0);
$q->set_queue_maxlen(5000);
$q->try_run();

This script is set to run from inittab and will respawn if ever it dies:

geo:345:respawn:/usr/local/bin/geo_filter.pl

After adding the above line to /etc/inittab, remember to run 'telinit q'.

Next we need to send the appropriate packets into NFQUEUE, and drop over-rate packets:

# iptables -I INPUT -p udp --dport 5060 -m state --state NEW -j DROP
# iptables -I INPUT -p udp --dport 5060 -m state --state NEW -m hashlimit \
 --hashlimit 2 --hashlimit-mode srcip --hashlimit-name sip -j NFQUEUE

- Result -

[root@voip2 ~]# iptables -L INPUT -n -v
Chain INPUT (policy ACCEPT 46M packets, 13G bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 NFQUEUE    udp  --  *      *       0.0.0.0/0            0.0.0.0/0           udp dpt:5060 state NEW limit: avg 2/sec burst 5 mode srcip NFQUEUE num 0
    0     0 DROP       udp  --  *      *       0.0.0.0/0            0.0.0.0/0           udp dpt:5060 state NEW 
    0     0 ACCEPT     udp  --  virbr0 *       0.0.0.0/0            0.0.0.0/0           udp dpt:53 
    0     0 ACCEPT     tcp  --  virbr0 *       0.0.0.0/0            0.0.0.0/0           tcp dpt:53 
    0     0 ACCEPT     udp  --  virbr0 *       0.0.0.0/0            0.0.0.0/0           udp dpt:67 
    0     0 ACCEPT     tcp  --  virbr0 *       0.0.0.0/0            0.0.0.0/0           tcp dpt:67 

Finally, save your iptables configuration

iptables-save

Now, keep an eye on /var/log/messages for log entries concerning packets that are being accepted or dropped.