Docs Add rtpengine to your voice stack

Add rtpengine to your voice stack

Build rtpengine, have OpenSIPS or Kamailio anchor every call's media on the relay, prove it with a test call, and operate it. No sipnab involved.

On this page

A SIP proxy such as OpenSIPS carries the signaling of a call, and by default the audio flows directly between the two phones. A media relay puts itself in that path: the proxy rewrites each call’s SDP so that both phones send their audio to the relay, and the relay forwards it. That fixes one-way audio behind NAT, hides your customers’ addresses from each other, and gives you one place where you can measure or record every call’s media.

rtpengine is the media relay most OpenSIPS and Kamailio deployments use. This guide builds it, has OpenSIPS anchor every call’s media on it, and proves that with a test call. With Kamailio instead, one section replaces the OpenSIPS steps. This guide does not use sipnab. When you have this working, Let sipnab name rtpengine’s media adds sipnab beside it.

The parts, and what each one does:

PartRole
OpenSIPSYour SIP proxy. Its rtpengine module asks rtpengine for a relay port for each call and rewrites the SDP to point at it.
rtpengineRelays each call’s media. OpenSIPS controls it over rtpengine’s ng protocol, on a UDP port that only OpenSIPS should reach.
SIPpPlays a caller and a callee, for the test call.

Everything below runs on one machine, which keeps the example short. The last section says what changes when rtpengine has a machine to itself.

Tested on

Every command on this page ran as written, in order, on x86_64 virtual machines with 2 cores and 3 or 4 GB of memory:

  • With OpenSIPS built in step 3: on a clean Debian 13 (kernel 6.12.63) on 2026-09-27, and on a clean Ubuntu 24.04.5 (kernel 6.8.0) on 2026-09-26, with an earlier step 4 that named the source build’s paths.
  • With the OpenSIPS 4.0 packages: on Ubuntu 24.04.5, on 2026-09-27.
  • With Kamailio: on Debian 13, and beside OpenSIPS on Ubuntu 24.04.5, on 2026-09-27.

The commands pin the components to the versions below. Newer commits may behave differently, and pinning them keeps the guide describing what you get.

SoftwareVersion or commit
OpenSIPS, built heref46ef9337b, master, 4.1.0-dev
OpenSIPS, from packages4.0.2, installed as the OpenSIPS guide installs it
Kamailio6.1.4, installed as the Kamailio guide installs it
rtpengine8da4be3355, master, packaged as 26.3.0.0
SIPp (for the test call)the distribution’s sip-tester: 3.7.3 on Debian, 3.7.2 on Ubuntu

The examples use 192.0.2.10 as the machine’s address. Replace it with yours everywhere it appears.

Before you start: your SIP server

Find your case, and follow the steps it names:

  • No SIP server yet. Follow every step. Step 3 builds OpenSIPS for you. If you would rather run the OpenSIPS 4.0 packages, install them with step 1 of the OpenSIPS guide and skip step 3 here.
  • OpenSIPS already runs, from the packages or built from source. Skip step 3. The rtpengine, rtp_relay and dialog modules come with the opensips package and with the default build, so there is nothing to add. Step 4 finds your configuration file and replaces it. On a machine whose script you want to keep, add the lines step 4 marks to your own script instead.
  • Kamailio already runs, or you want Kamailio. Follow steps 1 and 2, then With Kamailio in place of steps 3 and 4, then steps 5 and 6.
  • Both OpenSIPS and Kamailio on one machine. Set up OpenSIPS as above, then see With Kamailio, which covers both.

1. Build rtpengine

rtpengine’s own documentation recommends building Debian packages from the source tree, which gives you a systemd unit, a configuration file and a clean uninstall. The build installs its dependencies from the tree’s debian/control, then runs rtpengine’s test suite, which takes a while:

# Run all of these, in order.
sudo apt-get update
sudo apt-get install -y --no-install-recommends git ca-certificates build-essential devscripts equivs fakeroot
sudo mkdir -p /usr/local/src/voice && sudo chown "$USER": /usr/local/src/voice
cd /usr/local/src/voice
git clone https://github.com/sipwise/rtpengine.git
cd rtpengine
git checkout 8da4be3355
sudo mk-build-deps -i -r -t "apt-get -y --no-install-recommends" debian/control
dpkg-buildpackage -us -uc -b -j2
cd ..
sudo apt-get install -y ./ngcp-rtpengine-daemon_26.3.0.0+0~mr26.3.0.0_amd64.deb \
  ./ngcp-rtpengine-utils_26.3.0.0+0~mr26.3.0.0_all.deb

The package’s configuration asks for rtpengine’s kernel forwarding module, which this guide does not install. Tell it to forward in userspace, and restart it:

# Run all of these, in order.
sudo sed -i '0,/^table = 0/s//table = -1/' /etc/rtpengine/rtpengine.conf
sudo systemctl restart ngcp-rtpengine-daemon
systemctl is-active ngcp-rtpengine-daemon

Userspace forwarding copies every packet through the rtpengine process. The kernel module, which this guide does not install, moves that work into the kernel.

2. Look at what rtpengine listens on

/etc/rtpengine/rtpengine.conf has several sections. The service reads the [rtpengine] section, and the [interface-...] sections it points to. The file also carries example sections, such as [rtpengine-testing], that the service does not read, so read the two that matter by name:

# Run all of these, in order.
sed -n '/^\[rtpengine\]/,/^\[/p' /etc/rtpengine/rtpengine.conf | grep -E '^(listen-ng|interfaces-config|port-min|port-max) '
sed -n '/^\[interface-default\]/,/^\[/p' /etc/rtpengine/rtpengine.conf | grep -E '^address '
  • listen-ng = localhost:2223 is the control port, where OpenSIPS sends its requests. It listens on the loopback address, so nothing outside the machine can reach it. Keep it that way: the ng protocol has no authentication, and anyone who can reach the port can create and tear down relay sessions.
  • interfaces-config = interface tells rtpengine to take its media addresses from the sections named [interface-...]. The one section, [interface-default], says address = any: every address the machine has at startup. rtpengine writes one of them into the SDP and receives the media there.
  • port-min and port-max bound the media ports it allocates, 30000-39999.

3. Build OpenSIPS

Skip this step if OpenSIPS is already installed, from the packages or from source.

OpenSIPS master builds with compiler optimizations turned off, which is right for OpenSIPS’s own developers and wrong for a proxy carrying calls. Turn them back on before you build. The rtpengine module is part of the default build:

# Run all of these, in order.
sudo apt-get install -y --no-install-recommends bison flex uuid-dev pkg-config libncurses-dev
cd /usr/local/src/voice
git clone https://github.com/OpenSIPS/opensips.git
cd opensips
git checkout f46ef9337b
make Makefile.conf
sed -i 's/^DEFS+= -DCC_O0/#DEFS+= -DCC_O0/' Makefile.conf
make -j2 all
sudo make install
/usr/local/sbin/opensips -V | head -2

The second line of opensips -V lists the build flags. CC_O0 is no longer among them.

Leave DBG_MALLOC as it is. With both it and CC_O0 switched off, this commit of master does not compile: net/tcp_conn_defs.h calls get_ticks() without including the header that declares it, and only DBG_MALLOC’s headers happen to supply it.

4. Configure OpenSIPS to anchor every call on rtpengine

This configuration is a minimal proxy with the relay added. Your own script does much more (registration, authentication, routing to carriers). The relay part is the block marked below, and the loadmodule lines it needs.

Write the configuration

The packages and the source build keep their configuration and their modules in different places. The first two lines find them: C is your configuration file, and M the directory your install loads modules from, which the script’s mpath names.

# Run all of these, in order.
for f in /etc/opensips/opensips.cfg /usr/local/etc/opensips/opensips.cfg; do sudo test -f "$f" && C=$f && break; done
for d in /usr/lib/*/opensips/modules /usr/local/lib64/opensips/modules; do [ -f "$d/tm.so" ] && M=$d && break; done
echo "configuration: $C   modules: $M"
sudo tee "$C" >/dev/null <<'EOF'
# OpenSIPS as a SIP proxy that anchors every call's media on rtpengine.
log_level=3
stderror_enabled=no
syslog_enabled=yes
syslog_facility=LOG_LOCAL0
udp_workers=2
open_files_limit=4096

socket=udp:192.0.2.10:5060   # the address your phones and carriers reach

mpath="MODULES/"

loadmodule "proto_udp.so"   # built into the core, but still loaded by name
loadmodule "signaling.so"
loadmodule "sl.so"
loadmodule "tm.so"
loadmodule "rr.so"
loadmodule "maxfwd.so"
loadmodule "sipmsgops.so"

loadmodule "mi_fifo.so"
modparam("mi_fifo", "fifo_name", "/run/opensips/opensips_fifo")

# The relay: rtp_relay drives rtpengine for the whole dialog.
loadmodule "dialog.so"
loadmodule "rtp_relay.so"
loadmodule "rtpengine.so"
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")

route {
	if (!mf_process_maxfwd_header(10)) {
		send_reply(483, "Too Many Hops");
		exit;
	}

	if (has_totag()) {
		if (is_method("ACK") && t_check_trans()) {
			t_relay();
			exit;
		}
		if (!loose_route()) {
			send_reply(404, "Not here");
			exit;
		}
		t_relay();
		exit;
	}

	if (is_method("CANCEL")) {
		if (t_check_trans())
			t_relay();
		exit;
	}
	t_check_trans();

	if (!is_method("INVITE")) {
		send_reply(405, "Method Not Allowed");
		exit;
	}

	record_route();

	# --- the relay starts here ---
	create_dialog();
	rtp_relay_engage("rtpengine");
	# --- the relay ends here ---

	# Where the call goes. Here, a test callee on this machine; in your
	# stack, lookup("location"), dispatcher or a carrier.
	$du = "sip:127.0.0.1:5070";
	t_relay();
}
EOF
sudo sed -i "s|^mpath=\"MODULES/\"|mpath=\"$M/\"|" "$C"
sudo grep '^mpath=' "$C"

What the relay block does:

  • create_dialog() makes OpenSIPS track the call, so that it knows when the call ends and can release the relay ports with it.
  • rtp_relay_engage("rtpengine") hands the call to rtpengine for its whole life. OpenSIPS sends rtpengine the caller’s SDP (an offer) and forwards the rewritten SDP to the callee, does the same for the callee’s answer, and sends delete when the call ends. You do not call rtpengine_offer() and rtpengine_answer() yourself.

If you built OpenSIPS in step 3: give it a user and a unit

The packages come with an opensips user and a systemd unit. If you installed them, or if your source build already runs as a service, skip to Check the configuration and start OpenSIPS. A fresh build has neither. Run OpenSIPS as its own user, under systemd:

# Run all of these, in order.
sudo useradd --system --home-dir /run/opensips --shell /usr/sbin/nologin opensips
sudo chown root:opensips /usr/local/etc/opensips
sudo chmod 750 /usr/local/etc/opensips
sudo tee /etc/systemd/system/opensips.service >/dev/null <<'EOF'
[Unit]
Description=OpenSIPS SIP server
After=network.target ngcp-rtpengine-daemon.service

[Service]
Type=forking
User=opensips
Group=opensips
RuntimeDirectory=opensips
RuntimeDirectoryMode=775
PIDFile=/run/opensips/opensips.pid
ExecStart=/usr/local/sbin/opensips -P /run/opensips/opensips.pid -f /usr/local/etc/opensips/opensips.cfg -m 64 -M 8
Restart=always
TimeoutStopSec=30s
LimitNOFILE=262144

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload

Check the configuration and start OpenSIPS

# Run all of these, in order.
for f in /etc/opensips/opensips.cfg /usr/local/etc/opensips/opensips.cfg; do sudo test -f "$f" && C=$f && break; done
sudo chown root:opensips "$C"
sudo chmod 640 "$C"
sudo opensips -C -f "$C"
sudo systemctl enable opensips
sudo systemctl restart opensips
systemctl is-active opensips

opensips -C checks the configuration and prints config file ok before you start anything. sudo opensips finds the packaged binary in /usr/sbin and the built one in /usr/local/sbin.

5. Place a test call

SIPp plays both ends: a callee that echoes audio back, and a caller that dials through OpenSIPS and plays a recorded G.711 sample. SIPp’s built-in caller ignores the Record-Route header OpenSIPS adds, so its BYE would miss the proxy and draw 404 Not here. The two route sed lines make it honor the route set, the way a real phone does. The callee writes every message it receives to uas.msg, so that you can read the SDP OpenSIPS handed it:

# Run all of these, in order.
sudo apt-get install -y sip-tester
mkdir -p ~/sipp/pcap && cd ~/sipp
ln -sf /usr/share/sip-tester/*.pcap pcap/
sipp -sd uac_pcap > uac_rr.xml
sed -i 's|<recv response="200" rtd="true" crlf="true">|<recv response="200" rtd="true" crlf="true" rrs="true">|' uac_rr.xml
sed -i -E 's#^( *)(ACK|BYE) sip:\[service\]@\[remote_ip\]:\[remote_port\] SIP/2.0#\1\2 [next_url] SIP/2.0\n\1[routes]#' uac_rr.xml
sipp -sn uas -i 127.0.0.1 -p 5070 -rtp_echo -m 1 -trace_msg -message_file uas.msg -bg
sudo sipp -sf uac_rr.xml 192.0.2.10:5060 -i 192.0.2.10 -p 5080 -s echo -m 1 -timeout 90s

The caller needs sudo because it plays the audio sample through a raw socket. At the end SIPp’s statistics screen shows Successful call at 1.

Now compare the SDP the caller sent with the SDP the callee received:

# Run all of these, in order.
cd ~/sipp
awk '/INVITE sip:/{f=1} f' uas.msg | grep -m2 -E '^(c=IN IP4|m=audio)'

SIPp’s caller offered its media on port 6000, SIPp’s default. The callee received an m=audio port between 30000 and 39999 instead: a port rtpengine allocated for this call. Both ends sent their audio to rtpengine, and rtpengine forwarded it. On one machine the caller and rtpengine share the address 192.0.2.10, so the port is what shows the relay. With phones on other machines, the c= address changes too, from the caller’s to rtpengine’s.

6. Operate it

See the calls rtpengine is relaying. During a call, list sessions all names each one by Call-ID. After the call ends, rtpengine keeps it for a short while before dropping it (the delete-delay setting), so the list can still show a call that just ended. list totals starts with the calls up now, then counts every call since rtpengine started. These lines are the ones to read:

# Run all of these, in order.
sudo rtpengine-ctl list sessions all
sudo rtpengine-ctl list totals | grep -E 'Owned sessions|Total managed sessions|Total relayed packets +:|Total number of 1-way streams|Average call duration'

Right after the test call, Owned sessions is still 1 and Total managed sessions is 0: rtpengine still holds the call. Run the same command a minute later and the two have swapped, Owned sessions 0 and Total managed sessions 1, with Average call duration about 9 seconds. Total number of 1-way streams counts streams whose audio went only one way, the relay’s own view of one-way audio.

Check health and read the logs.

# Run all of these, in order.
systemctl is-active opensips ngcp-rtpengine-daemon
sudo journalctl -u ngcp-rtpengine-daemon -n 50
sudo journalctl -u opensips -n 50

rtpengine logs one line per offer, answer and delete, each naming the Call-ID, so the journal is the first place to look when a call has no audio.

Restart after a configuration change. A restart of rtpengine drops the media of every call it is relaying. Those calls stay up in OpenSIPS and go silent. Restart it when no calls are up, or accept that the calls in progress lose their audio:

# Run all of these, in order.
sudo systemctl restart ngcp-rtpengine-daemon
sudo systemctl restart opensips

Uninstall. The rtpengine build installed its build dependencies through one package, ngcp-rtpengine-build-deps. Purging it and running autoremove removes them. autoremove also removes any other package that nothing depends on any more, so on a machine that runs other software, drop -y and read its list first.

# Run all of these, in order.
sudo systemctl disable --now opensips ngcp-rtpengine-daemon
sudo apt-get purge -y ngcp-rtpengine-daemon ngcp-rtpengine-utils ngcp-rtpengine-build-deps
sudo apt-get autoremove -y

With Kamailio

Kamailio drives rtpengine with its own rtpengine module, which comes with the kamailio package. Set Kamailio up as the Kamailio guide does, through its step 2, then follow steps 1 and 2 here for rtpengine. In place of steps 3 and 4, add the relay to Kamailio’s configuration.

Kamailio has no rtp_relay_engage() that follows the whole dialog. Instead the script calls rtpengine_manage() at each point where an SDP or the end of the call passes: on the INVITE (the offer), on the reply that carries the callee’s SDP (the answer), and on the BYE (the delete). The lines marked the relay are the ones to add to your own script:

# Run all of these, in order.
sudo tee /etc/kamailio/kamailio.cfg >/dev/null <<'EOF'
#!KAMAILIO
# Kamailio as a SIP proxy that anchors every call's media on rtpengine.
debug=2
log_stderror=no
log_facility=LOG_LOCAL0
children=2

listen=udp:192.0.2.10:5060   # the address your phones and carriers reach

loadmodule "tm.so"
loadmodule "sl.so"
loadmodule "rr.so"
loadmodule "maxfwd.so"
loadmodule "siputils.so"
loadmodule "textops.so"
loadmodule "pv.so"
loadmodule "kex.so"
loadmodule "corex.so"
loadmodule "ctl.so"

# the relay
loadmodule "rtpengine.so"
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")

request_route {
	if (!mf_process_maxfwd_header("10")) {
		sl_send_reply("483", "Too Many Hops");
		exit;
	}

	if (has_totag()) {
		if (loose_route()) {
			if (is_method("BYE")) {
				rtpengine_manage();   # the relay: release the call's ports
			}
			t_relay();
			exit;
		}
		if (is_method("ACK") && t_check_trans()) {
			t_relay();
		}
		exit;
	}

	if (is_method("CANCEL")) {
		if (t_check_trans()) {
			t_relay();
		}
		exit;
	}
	t_check_trans();

	if (!is_method("INVITE")) {
		sl_send_reply("405", "Method Not Allowed");
		exit;
	}

	record_route();

	rtpengine_manage();              # the relay: rewrite the offer
	t_on_reply("MANAGE_REPLY");      # the relay: and, in the reply, the answer

	# Where the call goes. Here, a test callee on this machine; in your
	# stack, lookup("location"), dispatcher or a carrier.
	$du = "sip:127.0.0.1:5070";
	t_relay();
}

# the relay: the reply that carries the callee's SDP
onreply_route[MANAGE_REPLY] {
	if (has_body("application/sdp")) {
		rtpengine_manage();
	}
}
EOF
sudo kamailio -c -f /etc/kamailio/kamailio.cfg
sudo systemctl enable kamailio
sudo systemctl restart kamailio
systemctl is-active kamailio

Step 5’s test call then works unchanged, and so does step 6, with kamailio in place of opensips in the systemctl and journalctl commands.

With OpenSIPS on the same machine, Kamailio listens on 5062, as OpenSIPS and Kamailio on one machine sets it up: change 5060 to 5062 in the listen line above. One rtpengine serves both proxies. Each call is its own session in rtpengine, named by its Call-ID, so the two proxies’ calls do not collide. Point step 5’s caller at 192.0.2.10:5062 for a call through Kamailio, and at 192.0.2.10:5060 for one through OpenSIPS.

Put rtpengine on its own machine

A busy relay usually gets a machine to itself, often with a public address, while OpenSIPS stays where it is. Three things change:

  • The control port. On the relay, set listen-ng to an address OpenSIPS can reach, such as listen-ng = 192.0.2.20:2223, and restart rtpengine. The ng protocol has no authentication, so allow that port only from your OpenSIPS machines, in the relay’s firewall.
  • OpenSIPS’s socket. Point rtpengine_sock at it: modparam("rtpengine", "rtpengine_sock", "udp:192.0.2.20:2223"), then restart OpenSIPS. Several relays can share the load: list them all in one rtpengine_sock value, separated by spaces.
  • The media ports. Open UDP 30000-39999 on the relay to the phones and carriers that send it audio. In [interface-default], set address to the address they reach it on. If that address is a public one mapped by NAT to a private one, set address to the private address and add advertised with the public one, which is what rtpengine then writes into the SDP.
  • The test callee. Step 5’s callee listens on 127.0.0.1, which only a relay on the same machine can reach. With the relay elsewhere, start the callee on the machine’s address and point $du at it, as Let sipnab name rtpengine’s media does.

When something does not work

  • rtpengine logs FAILED TO OPEN KERNEL TABLE 0. table = -1 was not set, or the service was not restarted after setting it.
  • OpenSIPS logs no available proxies or can't send command to rtpengine. rtpengine is not running, or rtpengine_sock names an address it does not listen on. Compare the socket with listen-ng.
  • The callee’s SDP still carries the caller’s address. The call did not pass through rtp_relay_engage(). Check that the INVITE reached that line of the route, and that rtpengine logged an offer for its Call-ID.
  • The test call’s BYE gets 404 Not here. The caller ignored the route set. Use the edited uac_rr.xml, not SIPp’s built-in uac_pcap.
  • One-way audio through a relay on its own machine. The phones cannot reach a port in 30000-39999 on the relay, or interface names an address they cannot reach. Open the range, and check which address rtpengine writes into the SDP.