Skip to content

Add rtpengine

github-actions[bot] edited this page Oct 17, 2026 · 1 revision

A SIP proxy such as OpenSIPS carries the signaling of a call. 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.
  • The relay forwards it.

That does three things:

  • It fixes one-way audio behind NAT.
  • It hides your customers' addresses from each other.
  • It 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,
  • 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:

Part Role
OpenSIPS Your SIP proxy. Its rtpengine module asks rtpengine for a relay port for each call and rewrites the SDP to point at it.
rtpengine Relays each call's media. OpenSIPS controls it over rtpengine's ng protocol, on a UDP port that only OpenSIPS should reach.
SIPp Plays 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 block on this page ran as written, in order, on 2026-10-01.

  • The machines were clean x86_64 virtual machines with 2 cores and 3 GB of memory:
    • Debian 13 (kernel 6.12.63),
    • Ubuntu 24.04.5 (kernel 6.8.0).
  • Each test call played SIPp's G.711 sample.
  • A capture on the machine counted the audio going through rtpengine both ways.

On each machine, it ran:

  • With OpenSIPS built from source on a machine with nothing installed.
  • With Kamailio, and beside the OpenSIPS 4.0 packages on one machine.
  • On its own machine:
    • on 2026-10-01 with the relay's control address moved to the machine's own address,
    • on 2026-09-28 on two machines.

On Debian 13, on 2026-09-28, causing each fault under When something does not work produced what it describes.

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

Software Version or commit
OpenSIPS, built here f46ef9337b, master, 4.1.0-dev
OpenSIPS, from packages 4.0.2, installed as the OpenSIPS guide installs it
Kamailio 6.1.4, installed as the Kamailio guide installs it
rtpengine 8da4be3355, 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.
  • OpenSIPS already runs, from the packages or built from source. Skip the Build OpenSIPS step.
    • The rtpengine, rtp_relay and dialog modules come with the opensips package and with the default build. So there is nothing to add.
    • The Configure OpenSIPS to anchor every call on rtpengine step finds your configuration file and replaces it.
    • On a machine whose script you want to keep, add the lines that step marks to your own script instead.
  • Kamailio already runs, or you want Kamailio. Follow these, in order:
    • the Build rtpengine and Look at what rtpengine listens on steps,
    • With Kamailio, in place of the Build OpenSIPS and Configure OpenSIPS to anchor every call on rtpengine steps,
    • the Place a test call and Operate it steps.
  • Both OpenSIPS and Kamailio on one machine. Set up OpenSIPS as above. Then see With Kamailio, which covers both.

Build rtpengine

rtpengine's own documentation recommends building Debian packages from the source tree. That gives you a systemd unit, a configuration file and a clean uninstall.

The build installs its dependencies from the tree's debian/control. Then it 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. This guide does not install that module. Tell rtpengine 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.

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]. The service does not read them.

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. 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. That means 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.

Build OpenSIPS

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

OpenSIPS master builds with compiler optimizations turned off.

  • That is right for OpenSIPS's own developers.
  • It is 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.
  • Only DBG_MALLOC's headers happen to supply it.

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,
  • 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.
  • M is the directory your install loads modules from. The script's mpath names it.
# 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 OpenSIPS 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,
    • sends delete when the call ends.

    You do not call rtpengine_offer() and rtpengine_answer() yourself.

If you built OpenSIPS from source: give it a user and a unit

The packages come with an opensips user and a systemd unit. Skip to Check the configuration and start OpenSIPS in either of these cases:

  • You installed the packages.
  • Your source build already runs as a service.

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.

Place a test call

SIPp plays both ends:

  • a callee that echoes audio back,
  • 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 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. That is 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.

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 it 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,
    • Total managed sessions 1,
    • Average call duration about 9 seconds.
  • Total number of 1-way streams counts streams whose audio went only one way. This is 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 line names 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 Configure Kamailio as a proxy step.
  • Then follow the Build rtpengine and Look at what rtpengine listens on steps here for rtpengine.
  • In place of the Build OpenSIPS and Configure OpenSIPS to anchor every call on rtpengine steps, 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),
  • 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

The Place a test call step's test call then works unchanged. So does the Operate it step, 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 the Place a test call step's caller at:
    • 192.0.2.10:5062 for a call through Kamailio,
    • 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. The SIP proxy stays where it is. The examples use 192.0.2.20 for the relay's machine. Set it up there with the Build rtpengine and Look at what rtpengine listens on steps. Then make two changes.

On the relay's machine, have rtpengine take control requests on an address the proxy can reach. Then restart it:

# Run all of these, in order.
sudo sed -i '0,/^listen-ng = localhost:2223$/s//listen-ng = 192.0.2.20:2223/' /etc/rtpengine/rtpengine.conf
sed -n '/^\[rtpengine\]/,/^\[/p' /etc/rtpengine/rtpengine.conf | grep '^listen-ng'
sudo systemctl restart ngcp-rtpengine-daemon
systemctl is-active ngcp-rtpengine-daemon

In the relay's firewall:

  • The ng protocol has no authentication. So allow UDP 2223 from your SIP proxies only.
  • Open UDP 30000-39999 to the phones and carriers that send it audio.

On the proxy's machine, point the proxy at it. The first line finds the proxy's configuration: the OpenSIPS packages', a source build's, or Kamailio's:

# Run all of these, in order.
for f in /etc/opensips/opensips.cfg /usr/local/etc/opensips/opensips.cfg /etc/kamailio/kamailio.cfg; do sudo test -f "$f" && C=$f && break; done
sudo sed -i 's|"udp:127.0.0.1:2223"|"udp:192.0.2.20:2223"|' "$C"
sudo grep -n 'rtpengine_sock' "$C"
case "$C" in */kamailio/*) sudo systemctl restart kamailio;; *) sudo systemctl restart opensips;; esac

Several relays can share the load. List them all in one rtpengine_sock value, separated by spaces.

rtpengine writes one of its machine's addresses into the SDP.

  • It is the one in [interface-default]'s address.

  • That is any in the package's configuration.

  • The phones may reach the relay through NAT. Then:

    • set address to its private address,
    • add advertised with the public one.

    rtpengine then writes the public one into the SDP.

The Place a test call step's callee listens on 127.0.0.1. A relay on another machine cannot reach that. Let sipnab name rtpengine's media starts the callee on the machine's address instead. It places the test call through the relay on its own machine.

When something does not work

  • rtpengine logs FAILED TO OPEN KERNEL TABLE 0. One of these happened:

    • table = -1 was not set.
    • The service was not restarted after setting it.
  • OpenSIPS logs can't send ... command to a RTP proxy (111:Connection refused) or timeout waiting reply from a RTP proxy. One of these is true:

    • rtpengine is not running.
    • 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 two things:

    • The INVITE reached that line of the route.
    • 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. One of these is true:

    • The phones cannot reach a port in 30000-39999 on the relay.
    • interface names an address they cannot reach.

    Open the range, and check which address rtpengine writes into the SDP.

Home

Getting started

Using the TUI

CLI & automation

Configuration

Integrations (API & MCP)

Your SIP server

Add to your voice stack

Development & internals

Clone this wiki locally