Saturday, February 29, 2020

SIP Monitoring based on SIP3 and Heplify

Few words upfronts.

 - What is SIP3?

Follow the link :) If you don't know why you need it (or similar system) - you don't.

 - Why SIP3 and not Homer
Homer 3 was really good. It actually covers all my needs in SIP traffic monitoring. Than Homer 5 with new GUI was introduced. And things slowly start getting worse. Mostly cause of really strange changes in UI, which can stop working at any moment. No, it will respond, show something, but sometimes not the real picture. The usual answer - log out and back in. With Homer 7 things went even worse. Recommendation from developers is using the latest alpha version. If set up the system with a recommended setup (set of docker containers), it's not a "set and forget" solution. No. It will stop processing traffic at some time without any warning. For monitoring system which I need not every day (actually I need mostly post-mortem traces), it always ends up with log into the system and finds out it stops working like a week ago. Not the best option.
But it's really my experience. Maybe I'm not that lucky at all.

 - Why heplify and not captain (which is a part of SIP3)?
The answer is quite simple. Heplify - single binary, captain (at the moment) - a full zoo of tools, like docker, ansible, python, etc. Yes, I'm aware, that captain also can provide RTCP stats, but overhead is too big.

So, getting to the good part.

Installation and setting up

Central server

I'm gonna use CentOS 8 (minimal). Not like I'm a big fan of it, but developers like it. So, most tested one

1. Install docker

# yum install -y yum-utils   device-mapper-persistent-data  lvm2
# yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# containerdSource=https://download.docker.com/linux/centos/7/x86_64/stable/Packages/containerd.io-1.2.6-3.3.el7.x86_64.rpm
# dnf install $containerdSource -y
# dnf install docker-ce docker-ce-cli -y
# systemctl disable firewalld
# systemctl start docker
# systemctl enable --now docker

 

2. Install ansible, pip, git


# yum install -y epel-release
# yum install -y ansible git python3-pip
# pip3 install docker-py


3. Install SIP3 backend


# cd /usr/src/
# git clone https://github.com/sip3io/sip3-ansible.git


Note on taking metrics from public network services.

/usr/src/sip3-ansible/roles/sip3-salto/templates/application.yml.j2
#! Server
server:
  uri: udp://0.0.0.0:15060


#! Management socket
management:
  uri: udp://0.0.0.0:15090

#! Metrics
metrics:
  logging:
    step: 1000
  influxdb:
    uri: http://sip3-influxdb:8086
    db: sip3
    step: 1000

#! MongoDB
mongo:
  uri: mongodb://sip3-mongodb:27017
  db: sip3
  bulk-size: 1
  collections:
    - prefix: attributes
      indexes:
        ascending: [name]
      max-collections: 7
    - prefix: sip_register_index
      indexes:
        ascending: [created_at, terminated_at, src_addr, src_host, dst_addr, dst_host, caller, callee, state]
        hashed: [call_id, x_call_id]
      max-collections: 7
    - prefix: sip_register_raw
      indexes:
        ascending: [created_at]
        hashed: [call_id]
      max-collections: 7
    - prefix: sip_call_index
      indexes:
        ascending: [created_at, terminated_at, src_addr, src_host, dst_addr, dst_host, caller, callee, state, error_code, error_type, duration, setup_time, establish_time]
        hashed: [call_id, x_call_id]
      max-collections: 7
    - prefix: sip_call_raw
      indexes:
        ascending: [created_at]
        hashed: [call_id]
      max-collections: 7
    - prefix: sip_message_index
      indexes:
        ascending: [created_at, terminated_at, src_addr, src_host, dst_addr, dst_host, caller, callee, state]
        hashed: [call_id, x_call_id]
      max-collections: 7
    - prefix: sip_message_raw
      indexes:
        ascending: [created_at]
        hashed: [call_id]
      max-collections: 7
    - prefix: sip_options_index
      indexes:
        ascending: [created_at, terminated_at, src_addr, src_host, dst_addr, dst_host, caller, callee, state]
        hashed: [call_id, x_call_id]
      max-collections: 7
    - prefix: sip_options_raw
      indexes:
        ascending: [created_at]
        hashed: [call_id]
      max-collections: 7

#! Application
sip:
  message:
    x-correlation-header: X-Call-ID
    exclusions: [MESSAGE, SUBSCRIBE, NOTIFY, OPTIONS, REGISTER]
  calls:
    aggregation-timeout: 120000

attributes:
  record-ip-addresses: false
  record-call-users: false


It's not to index in the database all IP addresses and usernames are seen on the system, cause the production system usually resides on public networks, where are tons of spam/attack calls and packets. Also retention period for database is set to 7 days, which is ok for me.

# ansible-playbook /usr/src/sip3-ansible/playbooks/trial/sip3-backend.yml

Relax and wait for system installs.

4. Add autostart


 /etc/systemd/system/sip3-backend.service

[Unit]
Description=Start Ansible Playbook SIP3
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/ansible-playbook /usr/src/sip3-ansible/playbooks/trial/sip3-backend.yml
TimeoutStartSec=10

[Install]
WantedBy=default.target




# systemctl daemon-reload
# systemctl enable sip3-backend


5. (Optional )Add hosts aliases to db

To get more friendly trances to read. Point, no spaces in names are allowed.


# docker exec -it sip3-mongodb mongo
# use sip3
# db.hosts.insert({"name" : "my_softswitch", "sip": ["X.X.X.X", "10.10.0.1"]})

Installing heplify (capture agent)

Log in to the server you want to collect SIP traffic from

# cd /usr/local/bin/
# wget https://github.com/sipcapture/heplify/releases/download/1.56/heplify
# chmod +x heplify


/etc/systemd/system/heplify.service
[Unit]
Description=Captures packets from wire and sends them to SIP3
After=network.target

[Service]
WorkingDirectory=/usr/local/bin
ExecStart=/usr/local/bin/heplify -i any -m SIP -hs SIP3_SERVER_ADDRESS:15060
ExecStop=/bin/kill ${MAINPID}
Restart=on-failure
RestartSec=10s
Type=simple

[Install]
WantedBy=multi-user.target




# systemctl daemon-reload
# systemctl enable heplify
# systemctl start heplify


System using

Log into your newly installed system on
http://SIP3_SEVER_ADDRESS with admin:admin credentials.

Sometimes, just after login, you will need to refresh your page. Actually, make sure Go! button is active
It's your search string. Simple, but yet powerful. It really looks like Wireshark one.
You can add search critera there based on sip.
or ip.
criteria
ip.src_host and ip.dst_host are hosts we added at step 5 installing Central Server paragraph.

Changing password is quite tricky, cause SIP3 relies on Grafana auth (Grafana is shipped as well) and to change password, head to

 http://SIP3_SEVER_ADDRESS/grafana

and use it interface to change password. As a bonus, you have Grafana dashboard with some useful metrics. And for sure you can add one you like. How - read Grafana manual :)

Adding batteries

One is a key feature of SIP3 is the ability to glue call legs in a 1 call flow. Usual algorithm is based on a strict match From and To numbers, but it's really rare case (at least, in my scenarios). There is a possibility to write user-defined functions, but I'll leave it to official manual. Another way to match call is to use custom SIP headers to match call legs. The default SIP3 header is X-Call-ID. So, if one leg holds Call-ID standart header matching other call X-Call-ID header, they would consider as one call and glue together.

In my case, I'm using FusionPBX, the solution is really simple. Add dialplan entry:
It will add X-Call-ID header to call equal to current Call-ID, if it's not present. Also, FreeSwitch preserves X-Headers over call legs. which is perfect for us.

So, we will have a picture like this

Upgrading

# cd /usr/src
# git pull
# ansible-playbook /usr/src/sip3-ansible/playbooks/trial/sip3-backend.yml


Happy using!

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

/etc/kamailio/kamailio.cfg
 
#!KAMAILIO

#!define WITH_ANTIFLOOD
#!define WITH_DOMAIN_NAMES
#!define WITH_SANITY_CHECK
#!define WITH_SCANNER_BODY
#!define WITH_SCANNER_MESSAGE

debug=2
log_stderror=no

memdbg=5
memlog=5

log_facility=LOG_LOCAL7

children=6

auto_aliases=no

listen=udp:127.0.0.1:5060


loadmodule "sl.so"
loadmodule "pv.so"
loadmodule "maxfwd.so"
loadmodule "textops.so"
loadmodule "siputils.so"
loadmodule "xlog.so"
loadmodule "sanity.so"
loadmodule "regex.so"
loadmodule "sipcapture.so"
loadmodule "db_text.so"
# ----------------- setting module-specific parameters ---------------
modparam("sipcapture", "db_url", "text:///tmp/")
modparam("sipcapture", "raw_socket_listen", "127.0.0.1:5060")
modparam("sipcapture", "raw_moni_capture_on", 1)
modparam("sipcapture", "raw_interface", "lo")
modparam("sipcapture", "promiscious_on", 1)

#!ifdef WITH_ANTIFLOOD
loadmodule "pike.so"
# ----- pike params -----
modparam("pike", "sampling_time_unit", 2)
modparam("pike", "reqs_density_per_unit", 16)
modparam("pike", "remove_latency", 4)

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

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

request_route {


#!ifdef WITH_ANTIFLOOD
    # 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");
            exit;
        }
        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");
            exit;
        }
    }
#!endif

#!ifdef WITH_SCANNER_MESSAGE
    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");
        exit;
    }
#!endif
#!ifdef WITH_SCANNER_BODY
    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");
        exit;
    }
#!endif
#!ifdef WITH_SANITY_CHECK
    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");
        exit;
    }
#!endif
#!ifdef WITH_DOMAIN_NAMES
    if (!is_method("INVITE|REGISTER")) {
        exit;
    }

    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");
            exit;
        }
    }
#!endif
}



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

Next - set up Fail2Ban

/etc/fail2ban/filter.d/kamailio.conf

[Definition]
# filter for kamailio messages
failregex = [SIP-FIREWALL][FAIL2BAN] <HOST>


/etc/fail2ban/jail.conf
...
[kamailio-iptables]
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 127.0.0.1

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 https://download.docker.com/linux/debian/gpg | apt-key add -
    add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"
    apt update
    apt install -y docker-ce
  • Prepare Dockerfile

    FROM debian:buster
    MAINTAINER Igor Olhovskiy <igorolhovskiy@gmail.com>

    ENV DEBIAN_FRONTEND noninteractive
    ENV RTPENGINE_VERSION mr8.0
    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 https://codeload.github.com/BelledonneCommunications/bcg729/tar.gz/$BCG729_VERSION > bcg729_$BCG729_VERSION.orig.tar.gz && \
        tar zxf bcg729_$BCG729_VERSION.orig.tar.gz && \
        cd bcg729-$BCG729_VERSION && \
        git clone https://github.com/ossobv/bcg729-deb.git debian && \
        dpkg-buildpackage -us -uc -sa && \
        cd /usr/src && \
        dpkg -i *.deb && \
        cd /usr/src && \
        git clone -b $RTPENGINE_VERSION https://github.com/sipwise/rtpengine.git && \
        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 https://github.com/samael33/fusionpbx.git 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

(optional)
# 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.

...
route[ON_START_TRANSACTION] {
    t_set_fr(INVITE_TIMEOUT, 2000);
    route(REAY);
}
...
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);
    ....
    route(TRY_NEXT_DESTINATION);
}
...

Friday, August 3, 2018

Bulk Import Extensions/Devices to FusionPBX

FusionPBX is a really great software. So, community is constantly expanding it possibilities and adding new features.
On of these expands - app for bulk import extensions/devices/voicemails from CSV file.


Idea is quite simple - you upload CSV file, it's being processed and shows you a preview where you can select which field corresponding to which row (or column) of the file.

Installation is quite simple. I've made it in my fork of FusionPBX, but it's fully compatible with original one.

On your server

cd /usr/src

# If you're using 4.2 of FusionPBX - replace 4.4 with 4.2 below, 
#  but point, only 4.4 version is supported now
git clone -b 4.4 https://github.com/samael33/fusionpbx fusionpbx-samael 

mv fusionpbx-samael/app/bulk_import_extensions /var/www/fusionpbx/app
chown -R www-data:www-data /var/www/fusionpbx/app/bulk_import_extensions
rm -r fusionpbx-samael

::
Log into the FusionPBX webpage
Advanced -> Upgrade
Menu Defaults and Permission Defaults.
Log out and back in.