Monday, January 26, 2026

Compiling rtpengine 13.5.x on RHEL/Alma/Rocky 9

rtpengine having Debian as a target platform. But sometimes you need Red Hat-based distros.

So, voici how to compile and install rtpengine version mr13.5.1.3 (and I guess all 13.5.x branch) on a plain RHEL 9 system:

Enable Required Repositories

sudo dnf -y install epel-release
sudo dnf -y update


Install Development Tools

sudo dnf -y groupinstall "Development Tools"
sudo dnf -y groupinstall "RPM Development Tools"


Install Build Dependencies

sudo dnf -y install \
    gcc \
    make \
    pkgconfig \
    redhat-rpm-config \
    glib2-devel \
    libcurl-devel \
    openssl-devel \
    pcre-devel \
    zlib-devel \
    hiredis-devel \
    systemd-devel \
    libpcap-devel \
    libevent-devel \
    json-glib-devel \
    gperf \
    perl-IPC-Cmd \
    perl-podlators \
    libatomic \
    ncurses-devel \
    xmlrpc-c-devel \
    iptables-devel \
    iptables-nft \
    libmnl-devel \
    libnftnl-devel \
    pandoc \
    wget \
    git \
    libtiff-devel \
    libjpeg-turbo-devel \
    libsndfile-devel \
    libxml2-devel \
    autoconf \
    automake \
    libtool \
    mosquitto-devel \
    libwebsockets-devel \
    opus-devel \
    libjwt-devel

Install FFmpeg

sudo dnf -y install ffmpeg-free-devel

Note: you can use ffmpeg-devel from RPM-Fusion repo

Install MariaDB Development Files

sudo dnf -y install mariadb-devel mariadb-connector-c-devel

Create mysqlclient.pc symlink (rtpengine looks for this)


sudo ln -sf /usr/lib64/pkgconfig/mariadb.pc /usr/lib64/pkgconfig/mysqlclient.pc

Install Kernel Development Packages (for kernel module)

sudo dnf -y install kernel-devel kernel-headers elfutils-libelf-devel

Create Stub libiptc.pc File

RHEL 9 doesn't provide libiptc via pkg-config. Create a stub file:

sudo tee /usr/lib64/pkgconfig/libiptc.pc << 'EOF'
prefix=/usr
exec_prefix=${prefix}
libdir=${exec_prefix}/lib64
includedir=${prefix}/include

Name: libiptc
Description: Stub libiptc for RHEL 9 (iptables chain management disabled)
Version: 1.8.9
Cflags:
Libs:
EOF

Verify it works

pkg-config --exists libiptc && echo "libiptc.pc created successfully"

Build and Install spandsp from FreeSWITCH Fork

The standard spandsp package doesn't work correctly on RHEL 9. Use the FreeSWITCH fork:

cd /usr/src
sudo git clone https://github.com/freeswitch/spandsp.git
cd spandsp
sudo ./bootstrap.sh
sudo ./configure --prefix=/usr
sudo make -j$(nproc)
sudo make install
sudo ldconfig

Build rtpengine and it's components

cd /usr/src
sudo wget -O rtpengine-mr13.5.1.3.tar.gz \
    https://github.com/sipwise/rtpengine/archive/refs/tags/mr13.5.1.3.tar.gz
sudo tar xzf rtpengine-mr13.5.1.3.tar.gz
sudo mv rtpengine-mr13.5.1.3 rtpengine
cd rtpengine


Build rtpengine Daemon

cd /usr/src/rtpengine/daemon
sudo make RTPENGINE_VERSION=mr13.5.1.3 -j$(nproc) with_iptables_option=no


Build Recording Daemon

cd /usr/src/rtpengine/recording-daemon
sudo make RTPENGINE_VERSION=mr13.5.1.3 -j$(nproc) with_iptables_option=no


Build Kernel Module (Optional)

cd /usr/src/rtpengine/kernel-module
KERNEL_VERSION=$(uname -r)
sudo make RTPENGINE_VERSION=mr13.5.1.3 KSRC=/usr/src/kernels/$KERNEL_VERSION
sudo make install
sudo depmod -a


Install rtpengine

cd /usr/src/rtpengine/daemon
sudo make install with_iptables_option=no

cd /usr/src/rtpengine/recording-daemon
sudo make install with_iptables_option=no


Create rtpengine User and Directories

sudo useradd -r -s /sbin/nologin rtpengine

sudo mkdir -p /var/run/rtpengine
sudo mkdir -p /var/spool/rtpengine
sudo mkdir -p /etc/rtpengine

sudo chown rtpengine:rtpengine /var/run/rtpengine /var/spool/rtpengine


Systemd integration

sudo tee /etc/rtpengine/rtpengine.conf << 'EOF'
[rtpengine]
table = -1
interface = eth0
listen-ng = 127.0.0.1:2223
port-min = 10000
port-max = 20000
log-level = 6
log-facility = daemon
foreground = false
pidfile = /var/run/rtpengine/rtpengine.pid
EOF

sudo tee /etc/systemd/system/rtpengine.service << 'EOF'
[Unit]
Description=RTPEngine Media Proxy
After=network.target

[Service]
Type=forking
PIDFile=/var/run/rtpengine/rtpengine.pid
ExecStart=/usr/bin/rtpengine --config-file=/etc/rtpengine/rtpengine.conf --pidfile=/var/run/rtpengine/rtpengine.pid
ExecStop=/bin/kill -TERM $MAINPID
Restart=on-failure
User=root
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload


Configure Firewall (if applicable)

sudo firewall-cmd --permanent --add-port=10000-20000/udp
sudo firewall-cmd --permanent --add-port=2223/udp
sudo firewall-cmd --reload


Load Kernel Module (if built)

sudo modprobe xt_RTPENGINE

Verify it's loaded
lsmod | grep xt_RTPENGINE

To load on boot, create a modules-load.d file
echo "xt_RTPENGINE" | sudo tee /etc/modules-load.d/rtpengine.conf

Start rtpengine

sudo systemctl enable rtpengine
sudo systemctl start rtpengine
sudo systemctl status rtpengine
rtpengine --version


Verification

ss -ulnp | grep 2223

Test with rtpengine-ctl (if available)
echo "list totals" | nc -u 127.0.0.1 222

Thursday, June 5, 2025

Granulating hangup causes on Asterisk with Kamailio. Or getting one.

 I'm not touching Asterisk SIP-related things a lot, but when I do, it can be frustrating from time to time. Or maybe I don't know the "correct way" of doing things.

Task is simple - after invoking Dial() get something more granular than ${DIALSTATUS}. Especially in a case of failed call. 

But we also have a ${HANGUPCAUSE} variable here. And turns out, it integer value is coming directly from SIP responce, Reason header. Which we can modify on Kamailio.

So, if we have an answer something like  

SIP/2.0 604 Does not exist anywhere
...

from upstream, we can add/modify custom reason with something like:

kamailio.conf

onreply_route{

...

   if ($rs == 604) {

      remove_hf("Reason");

      append_hf("Reason: Q.850;cause=3;text=\"No route to destination\"\n");

   }

}

And in Asterisk we will have 3 in a ${HANGUPCAUSE} after Dial(). Point, there is a limitation on possible numbers here.

Other option to get it purely on Asterisk could be using  ${HANGUPCAUSE(chan,tech)} function, but this required knowing of an outgoing channel name. Which we can also get in a parent channel via Pre-Dial Handler (see example here) and passing this name via AstDB for example using parent ${CHANNEL} as a key, as I'm not sure we can propagate variable up to stack.

As an example:

extensions.conf

 ...

    same => n,Set(__PARENT_CHANNEL=${CHANNEL})

    same => n,Dial(PJSIP/${EXTEN}@${TRUNK},,b(save_child_channel^s^1)) 

    same => n,Set(CHILD_CHANNEL=${DB_DELETE(chan_save/${CHANNEL})})

    same => n,NoOp(SIP Reply: ${HANGUPCAUSE(${CHILD_CHANNEL},tech)}

[save_child_channel]

exten => s,1,Set(DB(chan_save/${PARENT_CHANNEL})=${CHANNEL})

    same => n,Return()

 A bit overcomplicated as for me, but maybe I miss some "built-in function".

Friday, May 16, 2025

Kamailio TLS connection lifetime

 TL;DR;

Pay attention to 

modparam("tls", "connection_timeout", 600) # Default

 A bit longer.

Usually, you don't need to use SIP OPTIONS ping over TCP connections. Because you have TCP keepalives built-in. And the default registration time is 1h, which is also generous in the case of TCP (TLS). But the default connection timeout, found in mod_tls parameters is only 10 minutes. So, if your registration time is lower than 10 minutes and there are no calls on this phone (which is ok as well), you'll have a TLS connection drop every 10 minutes with a corresponding indication on the phone itself.

 Again, defaults might not be 100% sane.

Wednesday, March 5, 2025

Kamailio - change script behavoir at a realtime

For the moment, it's impossible to apply changes to Kamailio config file (not using KEMI, for sure) without restarting Kamailio.

Here is a small trick that you can use to enable/disable some parts of a script in a realtime, that is very useful for debugging. Not the whole script, but something.

kamailio.cfg

#!KAMAILIO

# REALTIME changable parameters
...
# Enable or disable test numbers function. Done as realtime not to use group (SQL) functions every call
realtime.test_destination=0 

...

 route[TEST_DESTINATION_DETECT] {
    if ($(sel(cfg_get.realtime.test_destination){s.int}) != 1) {
        return;
    }

    if (is_user_in("To", "test-dst-to")) {
        xlog("$var(debug_level)", "[TEST_DESTINATION_DETECT] $rU as a test destination TO number\n");

        setflag(FLT_TESTNUMBER);
        return;
    }

    if (is_user_in("From", "test-dst-from")) {
        xlog("$var(debug_level)", "[TEST_DESTINATION_DETECT] $fU as a test destination FROM number\n");

        setflag(FLT_TESTNUMBER);
    }
}

In this example the whole route execution is based on a value of realtime.test_destination parameter. By default it's 0 (means false), but can be easily changed with 

# kamcmd cfg.set realtime test_destination 1

And no need to restart script. Small, but useful trick for me. Only thing to keep in mind, that unlike KEMI this trick is not taking into accout transaction states, etc. Just change and that's it.


P.S:  Got some feedback, that $(set(cfg_get...)) might lead to a crashes on a high load. Htable might be a good alternative for this as well.

Tuesday, January 7, 2025

Asterisk PJSIP endpoint identification by To number

Recently got an interesting task.

For calls, that are made to a specific number, apply DTMF transcoding towards caller side. Means when someone (an automated system, actually) answers on this specific number, it starts send DTMF codes towards caller. But the main issue as, that callers, that are calling to this number accept DTMF only in inband mode, despite that looking on SIP traces they should accept usual RFC4733.

DTMF transcoding (in PJSIP) is handled by Asterisk via endpoint parameters. So, all calls ONLY to this number need to be put into a specific endpoint. Gladly, it's not so hard to achieve, but the documentation is lacking examples. So, here is the one:

pjsip.conf

[global]
...
# Depending on your configuration you might need to adjust the order
endpoint_identifier_order=ip,username,header,anonymous

# Actual endpoint we need call to be placed FROM
[test_endpoint]
type=endpoint
...
dtmf_mode=inband
identify_by=header

[test_endpoint]
type=identify
endpoint=test_endpoint
match_header=To:/123456789/

Now all calls made to a number, that consists sequence 123456789 will be made from test_endpoint and all DTMF towards this endpoint will be transcoded to inband.

Thursday, October 10, 2024

Collecting MOS stats from rtpengine with Kamailio

 Not a secret, that rtpengine can collect MOS stats during a call. So, this small note is just filling some gaps in an official documentation:

kamailio.cfg

# rtpengine cumulative MOS
modparam("rtpengine", "mos_min_pv", "$avp(re_mos_min)")
modparam("rtpengine", "mos_min_at_pv", "$avp(re_mos_min_at)")
...
# rtpengine A-Leg MOS
modparam("rtpengine", "mos_A_label_pv", "$avp(re_mos_A_label)")
modparam("rtpengine", "mos_min_A_pv", "$avp(re_mos_min_A)")
modparam("rtpengine", "mos_min_packetloss_A_pv", "$avp(re_mos_min_packetloss_A)")
...
# rtpengine B-Leg MOS
modparam("rtpengine", "mos_B_label_pv", "$avp(re_mos_B_label)")
modparam("rtpengine", "mos_min_B_pv", "$avp(re_mos_min_B)")
modparam("rtpengine", "mos_min_packetloss_B_pv", "$avp(re_mos_min_packetloss_B)")
...

# route is called on both request and responce.
route[RTPENGINE_MANAGE]
# Here we're adding labels to A and B channels.
  ...
  # Values would be used later in rtpengine_delete() call
  if (is_request()) {
    $var(rtpengine_params) = $var(rtpengine_params) + " label=leg_A";
  } else {
    $var(rtpengine_params) = $var(rtpengine_params) + " label=leg_B";
  }

  ...
  rtpengine_manage("$var(rtpengine_params)");

}

...

# This route is called on freeing resources on rtpengine
route[RTPENGINE_DELETE] {

  # Here we're initializing variables that were mentioned in modparam.
  $avp(re_mos_A_label) = "leg_A";
  $avp(re_mos_B_label) = "leg_B";
 
  rtpengine_delete();

  # Print quality stats for the call
  if (!is_method("BYE")) {
        return;
  }

  if ($avp(re_mos_average) != $null) {
        xlog("L_NOTICE", "[CALL_STATS][MIN] MOS:$avp(re_mos_min) T:$avp(re_mos_min_at) L:$avp(re_mos_min_packetloss) J:$avp(re_mos_min_jitter) RTT:$avp(re_mos_min_roundtrip) [MAX] MOS:$avp(re_mos_max) T:$avp(re_mos_max_at) L:$avp(re_mos_max_packetloss) J:$avp(re_mos_max_jitter) RTT:$avp(re_mos_max_roundtrip) [AVG] MOS:$avp(re_mos_average) L:$avp(re_mos_average_packetloss) J:$avp(re_mos_average_jitter) RTT:$avp(re_mos_average_roundtrip)\n");
  }
  if ($avp(re_mos_average_A) != $null) {
        xlog("L_NOTICE", "[CALL_STATS][A][MIN] MOS:$avp(re_mos_min_A) T:$avp(re_mos_min_at_A) L:$avp(re_mos_min_packetloss_A) J:$avp(re_mos_min_jitter_A) RTT: $avp(re_mos_min_roundtrip_A) [MAX] MOS:$avp(re_mos_max_A) T:$avp(re_mos_max_at_A) L:$avp(re_mos_max_packetloss_A) J:$avp(re_mos_max_jitter_A) RTT:$avp(re_mos_max_roundtrip_A) [AVG] MOS:$avp(re_mos_average_A) L:$avp(re_mos_average_packetloss_A) J:$avp(re_mos_average_jitter_A) RTT:$avp(re_mos_average_roundtrip_A)\n");
  }
  if ($avp(re_mos_average_B) != $null) {
        xlog("L_NOTICE", "[CALL_STATS][B][MIN] MOS:$avp(re_mos_min_B) T:$avp(re_mos_min_at_B) L:$avp(re_mos_min_packetloss_B) J:$avp(re_mos_min_jitter_B) RTT:$avp(re_mos_min_roundtrip_B) [MAX] MOS:$avp(re_mos_max_B) T:$avp(re_mos_max_at_B) L:$avp(re_mos_max_packetloss_B) J:$avp(re_mos_max_jitter_B) RTT:$avp(re_mos_max_roundtrip_B) [AVG] MOS:$avp(re_mos_average_B) L:$avp(re_mos_average_packetloss_B) J:$avp(re_mos_average_jitter_B) RTT:$avp(re_mos_average_roundtrip_B)\n");
  }
}

Example is not far from the documentation, but just to point how initialize the corresponding variables correctly.

Monday, July 29, 2024

Sipsak → sipexer for small SIP tests

I think a lot of peoples who are working with SIP knows and use sipsak. It's a Swiss-army knife for SIP, as it states. But not anymore. In a way migrating to OpenSSL 3.0 and newer distributions proper TLS transport support was lost. Or, in another words, I cannot make it work on Alma 9.

 

So, meet a replacement - sipexer from Kamailio author. Doing mainly the same (for me) and supports TLS and WebSocket out of the box. As a bonus for simple testing, you can get SIP reply code as a return code of the program itself.

So, instead of checks like

 

sipsak -s sip:check_server_health@localhost:5061 --transport=tls

if [ $? != 0 ]; then

  restart_kamailio

fi

 

you can do 

 

sipexer -timeout-write 500 -timeout 500 -vl 0 -mt options -ruser check_server_health tls:localhost:5061 >> /dev/null

if [ $? != 200 ]; then

  restart_kamailio

fi

 

Quite a simple replacement and due to sipexer being written on Go, you can just get the binary and use it right away.

I like it when things become simpler.