Monday, October 28, 2019

One more SIP firewall based on Kamailio

Sometimes it's impossible to put Kamailio in a front of third-party SIP software and it's nice to have sort of SIP firewall upfront.

One of solutions - use iptables (like I did a long ago here), but overall, IPTables is not a SIP processing engine. But Kamailio is.

So, idea is quite simple. We have our main PBX software sitting on ethX (or ensXX), port 5060. We're mirroring SIP traffic on this interface:port to localhost, where Kamailio is listens to them in promiscuous mode. And writing a log file, Fail2Ban can analyze. 
Target here - is to protect main PBX software from consuming a lot of resources on fake INVITE's and REGISTER's from not-too-friendly-scanners.

First - install Kamailio.
I prefer to put syslog messages to separate file.

Configure Kamailio with file









loadmodule ""
loadmodule ""
loadmodule ""
loadmodule ""
loadmodule ""
loadmodule ""
loadmodule ""
loadmodule ""
loadmodule ""
loadmodule ""
# ----------------- setting module-specific parameters ---------------
modparam("sipcapture", "db_url", "text:///tmp/")
modparam("sipcapture", "raw_socket_listen", "")
modparam("sipcapture", "raw_moni_capture_on", 1)
modparam("sipcapture", "raw_interface", "lo")
modparam("sipcapture", "promiscious_on", 1)

loadmodule ""
# ----- pike params -----
modparam("pike", "sampling_time_unit", 2)
modparam("pike", "reqs_density_per_unit", 16)
modparam("pike", "remove_latency", 4)

loadmodule ""
# ----- htable params -----
/* ip ban htable with autoexpire after 5 minutes */
modparam("htable", "htable", "ipban=>size=8;autoexpire=300;")

####### Routing Logic ########

request_route {

    # flood detection from same IP and traffic ban for a while
    # be sure you exclude checking trusted peers, such as pstn gateways
    # - local host excluded (e.g., loop to self)
    if(src_ip!=myself) {
        if($sht(ipban=>$si)!=$null) {
            # ip is already blocked
            xlog("L_NOTICE", "[SIP-FIREWALL][ANTIFLOOD-BANNED] [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]\n");
        if (!pike_check_req()) {
            $sht(ipban=>$si) = 1;
            xlog("L_NOTICE", "[SIP-FIREWALL][ANTIFLOOD-ADD] [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]\n");
            xlog("L_ALERT", "[SIP-FIREWALL][FAIL2BAN] $si\n");

    if (search("friendly-scanner|sipvicious|sipcli*|vaxasip|sip-scan|iWar|sipsak")) {
        xlog("L_NOTICE", "[SIP-FIREWALL][FRIENDLYSCANNER_MESSAGE]: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
        xlog("L_ALERT", "[SIP-FIREWALL][FAIL2BAN] $si\n");
    if (search_body("friendly-scanner|sipvicious|sipcli*|vaxasip|sip-scan|iWar|sipsak")) {
        xlog("L_NOTICE", "[SIP-FIREWALL][FRIENDLYSCANNER_BODY]: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
        xlog("L_ALERT", "[FAIL2BAN] $si\n");
    if(!sanity_check("17895", "7")) {
        xlog("L_NOTICE", "[SIP-FIREWALL][MALFORMED] [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]\n");
        xlog("L_ALERT", "[SIP-FIREWALL][FAIL2BAN] $si\n");
    if (!is_method("INVITE|REGISTER")) {

    if (pcre_match("$rd", "^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$$")) {
        if (pcre_match("$fd", "^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$$")) {
            xlog("L_NOTICE", "[SIP-FIREWALL][NOT_DOMAIN_BASED_URI]: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
            xlog("L_ALERT", "[SIP-FIREWALL][FAIL2BAN] $si\n");

It's actually my file, but you can adopt rules to yours.

Next - set up Fail2Ban


# filter for kamailio messages

enabled  = true
filter   = kamailio
action   = iptables-allports[name=KAMAILIO, protocol=all]
logpath  = /var/log/kamailio.log
maxretry = 5
bantime  = 1800
findtime = 60

Next - make sure you don't have net.ipv4.ip_forward set to 1. You will run into infinite loops in this case

# sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0

And last - use iptables TEE target to mirror traffic to localhost

# iptables -A PREROUTING -t mangle -i eth0 -p udp --dport 5060 -j TEE --gateway

That's it.

Wednesday, October 23, 2019

Building RTPEngine with Docker

Idea is quite simple.
To build rtpengine packet on Debian system, but not having all build dependencies on target machine.
So, we'll use Docker to generate deb files for target system and than - just remove image. This way system will held only minimum needed packets.

As a target, Debian 10 (buster) will be used, as I succeed to install rtpengine mr8.0 (with G.729 support) on it and can't get it working on Debian 9 (maybe use lower version?)

So. All is done on Debian 10 netinst.
  • First step - install Docker

    apt update
    apt install -y apt-transport-https ca-certificates curl gnupg2 software-properties-common
    curl -fsSL | apt-key add -
    add-apt-repository "deb [arch=amd64] $(lsb_release -cs) stable"
    apt update
    apt install -y docker-ce
  • Prepare Dockerfile

    FROM debian:buster
    MAINTAINER Igor Olhovskiy <>

    ENV DEBIAN_FRONTEND noninteractive
    ENV BCG729_VERSION 1.0.4

    RUN apt-get update && \
        apt-get upgrade -y && \
        apt-get install -y git \
                    dpkg-dev \
                    cmake \
                    unzip \
                    wget \
                    debhelper-compat \
                    default-libmysqlclient-dev \
                    gperf \
                    iptables-dev \
                    libavcodec-dev \
                    libavfilter-dev \
                    libavformat-dev \
                    libavutil-dev \
                    libbencode-perl \
                    libcrypt-openssl-rsa-perl \
                    libcrypt-rijndael-perl \
                    libcurl4-openssl-dev \
                    libdigest-crc-perl \
                    libdigest-hmac-perl \
                    libevent-dev libglib2.0-dev \
                    libhiredis-dev libio-multiplex-perl \
                    libio-socket-inet6-perl libiptc-dev \
                    libjson-glib-dev libnet-interface-perl \
                    libpcap0.8-dev \
                    libpcre3-dev \
                    libsocket6-perl \
                    libspandsp-dev \
                    libssl-dev \
                    libswresample-dev \
                    libsystemd-dev \
                    libxmlrpc-core-c3-dev \
                    markdown \
                    curl \
                    wget \
                    zlib1g-dev && \
        cd /usr/src && \
        curl$BCG729_VERSION > bcg729_$BCG729_VERSION.orig.tar.gz && \
        tar zxf bcg729_$BCG729_VERSION.orig.tar.gz && \
        cd bcg729-$BCG729_VERSION && \
        git clone debian && \
        dpkg-buildpackage -us -uc -sa && \
        cd /usr/src && \
        dpkg -i *.deb && \
        cd /usr/src && \
        git clone -b $RTPENGINE_VERSION && \
        cd rtpengine && \
        dpkg-buildpackage && \
        apt-get clean && \
        rm -rf /var/lib/apt/lists/*

    RUN mkdir -p /opt/deb && \
        mv /usr/src/*.deb /opt/deb

    VOLUME ["/opt/deb"]
  • Build and get deb files

    docker build -t rtpengine:build .
    docker create --name rtpengine-build rtpengine:build
    docker cp rtpengine-build:/opt/deb .
  • Install rtpengine

    cd deb
    dpkg -i libbcg729-0*.deb
    dpkg -i ngcp-rtpengine-daemon_*.deb
    apt-get install -f
    dpkg -i ngcp-rtpengine-recording-daemon_*.deb

    apt-get install -f
    dpkg -i ngcp-rtpengine-utils_*.deb

    apt-get install -f
    dpkg -i ngcp-rtpengine-iptables_*.deb

    apt-get install -f
    dpkg -i ngcp-rtpengine-kernel-dkms_*.deb
    dpkg -i ngcp-rtpengine_*.deb
    Idea of repeating apt-get install -f is to apt to add missing packets.
    Next - configure rtpengine as usual

Thursday, April 18, 2019

Simple Albert laucher plugin for managing VPN's

For some reasons now switched to Linux laptop from Macbook. And as an amazing replacement for Alfred launcher found Albert.
It also have a powerful plugin system and simple workflows could be created really fast.
One of workflows I used to in Alfred is switch VPN's on/off directly from Alfred's interface. As it's all opensource, answer is quite simple - if you need something, write it.
So, as a small task during sickness wrote a plugin for toggling VPN's state, that are managed through NetworkManager.

Looks like this:

Code is simple and published here

Thursday, March 14, 2019

Simple Call ACL App for FusionPBX

Made a simple Call Access Control List application for FusionPBX

Idea is to have a set of rules, that will be match on Caller and Callee number to allow or reject this call.
As an example - simple limit certain extension to call only certain numbers. Or block some callers to call exact this extension.

All is done on regex-like simple patterns.

As an example in screen below, extension 902 (or any number contains 902) can't dial any number, unless it contains 901 (extension 901 as well)

Rules are applied in order.

This app now is a part of my fork of FusionPBX, but fully compatible with vanilla Fusion version 4.4

To install it

# cd /usr/src
# git clone -b 4.4 fusionpbx-samael
# cp -r fusionpbx-samael/app/call_acl /var/www/fusionpbx/app/
# mkdir -p /var/www/fusionpbx/resources/install/scripts/app/custom
# cp -r fusionpbx-samael/resources/install/scripts/app/custom/call_acl /var/www/fusionpbx/resources/install/scripts/app/custom
# cp -r fusionpbx-samael/resources/install/scripts/app/app_custom.lua /var/www/fusionpbx/resources/install/scripts/app/
# cp -r fusionpbx-samael/app/dialplans/resources/switch/conf/dialplan/041_call_acl.xml /var/www/fusionpbx/app/dialplans/resources/switch/conf/dialplan

# chown -R www-data. /var/www/fusionpbx

FusionPBX Menu -> Advanced -> Upgrade -> Schema + App Defaults + Menu Defaults + Permission Defaults

Note, by default in Dialplan call_acl by default is disabled. Done this is mainly cause you don't want to enable it  on all domains. So, enable it per domain.

For cons - it's really heavy under high load and with big number of rules, cause heavily using regular expressions which are not super fast.

Wednesday, March 6, 2019

SIP/Kamaiio not-reachable endpoints and timers.

According to RFC3261 we have following mechanism of reliability:
(3 next blocks are taken from this book)

For non-INVITE transactions, a SIP timer, T1, is started by a UAC or a stateful proxy server when a new request is generated or sent. If no response to the request (as identified by a response containing the identical local tag, remote tag, Call-ID, and CSeq) is received when T1 expires, the request is resent. After a request is retransmitted, the next timer period is doubled until T2 is reached. 
If a provisional (informational class 1xx) response is received, the UAC or stateful proxy server immediately switches to timer T2. After that, the remaining retrans- missions occur at T2 intervals. This capped exponential backoff process is continued until a 64*T1, after which the request is declared dead. 

For an INVITE transaction, the retransmission scheme is slightly different. INVITEs are retransmitted starting at T1, and then the timer is doubled after each retransmission. The INVITE is retransmitted until 64*T1 after which the request is declared dead. After a provisional (1xx) response is received, the INVITE is never retransmitted. A stateful proxy must store a forwarded request or generated response message for 32 seconds. 

Suggested default values for T1 and T2 are 500 ms and 4 seconds, respectively. Timer T1 is supposed to be an estimate of the roundtrip time (RTT) in the network.

Main problem here is if remote side is dead, that we can get it after only 32 seconds. Which is too big, especially when we have some conditions like forward on not reachable. So, normally, decision is taken after not receiving provisional (1xx) response after 5 (or less) seconds. 

Kamailio has a fr_timer to control these type of behavior. By default, it's fully compatible with RFC. But it's not what we need. So, idea is to have fr_timer small initially and then - restore it on receiving provisional reply.

    t_set_fr(INVITE_TIMEOUT, 2000);
reply_route {
    if(status =~ "1[0-9][0-9]") {
        # Not set INVITE TIMEOUT here to prevent possible custom values
        t_set_fr(0, 30000);

failure_route {
    # Restore timers if was reset.
    t_set_fr(INVITE_TIMEOUT, 2000);