RTPengine for NAT Traversal and Transcoding
Introduction¶
A SIP proxy like Kamailio moves signalling but never touches RTP. When endpoints are behind NAT, or two legs use incompatible codecs or encryption, you need a media proxy. RTPengine (from Sipwise) is the standard one: it rewrites the SDP so both sides send media to it, then relays (and optionally transcodes) between them.
What it does¶
- NAT traversal — replaces the
c=/m=address in the SDP offer/answer with its own public IP:port, so endpoints send RTP to RTPengine regardless of what private address they advertised. RTPengine learns each side's real source address from the first packets ("latching") and relays. - Bridging — the two legs never talk directly; useful for topology hiding, recording, lawful intercept, and forcing media through a controlled path.
- Transcoding — G.711 ↔ Opus ↔ G.729 ↔ AMR when the two legs can't agree. CPU-intensive; a licensed codec (G.729, AMR) may need a plugin.
- SRTP/DTLS-SRTP ↔ RTP — terminate encryption on one leg, plain on the other (e.g. WebRTC client ↔ legacy SIP trunk).
- RTP ↔ WebRTC — ICE, DTLS, rtcp-mux, bundle for browser endpoints.
The kernel module¶
Userspace relaying every RTP packet is expensive. RTPengine's xt_RTPENGINE
iptables/nftables kernel module forwards established, non-transcoded streams
in the kernel — the userspace daemon only handles setup and control. This is
what lets one box relay tens of thousands of calls.
modprobe xt_RTPENGINE
iptables -I INPUT -p udp --dport 30000:40000 -j RTPENGINE --id 0
Transcoded, SRTP-terminated, or recorded streams stay in userspace.
Driving it from Kamailio¶
RTPengine listens on an ng control socket (UDP, default 2223); the SIP proxy
sends it commands (offer/answer/delete) as bencoded messages via the
rtpengine module:
loadmodule "rtpengine.so"
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")
route[RELAY] {
if (is_method("INVITE")) {
if (nat_uac_test("8") || $sel(cfg_get.always_relay)) {
rtpengine_offer("replace-origin replace-session-connection ICE=remove");
}
}
...
}
onreply_route[NAT_REPLY] {
if (has_body("application/sdp")) {
rtpengine_answer("replace-origin replace-session-connection ICE=remove");
}
}
failure_route / on BYE:
rtpengine_delete();
rtpengine_manage() is the do-the-right-thing wrapper for offer/answer/delete
based on the message. Flags control transcoding (transcode-PCMU), SRTP
(RTP/SAVP), recording (record-call), etc.
SDP rewriting — what actually changes¶
Offer in:
c=IN IP4 192.168.1.50
m=audio 40000 RTP/AVP 0 8
c=IN IP4 203.0.113.10 <- RTPengine's public IP
m=audio 30112 RTP/AVP 0 8 <- a port RTPengine allocated for this call/leg
Same transformation on the answer. Now both endpoints send to RTPengine.
Verification and troubleshooting¶
rtpengine-ctl list # active sessions
rtpengine-ctl list sessions | head
rtpengine-ctl list totals
iptables -L INPUT -n | grep RTPENGINE # kernel forwarding rule present?
tcpdump -i any -n 'udp portrange 30000-40000'
- No audio / one-way audio — the classic. Check: did the proxy actually call
rtpengine_offer/answer(not just on some paths)? Is the SDP being rewritten (capture the INVITE/200 leaving the proxy)? Is RTPengine'sinterfaceconfig set to the right public IP (behind its own NAT, setinterface=priv/pubadvertised addresses)? Is the RTP port range open in the firewall? - Audio works then cuts out after ~30s — RTP timeout / the proxy didn't send
deleteon a re-INVITE, or one side stopped sending and latching timed out. rtpengine-ctl listshows the session but no packets flow — endpoint sent RTP to the private address in the original SDP (proxy didn't rewrite that leg), or asymmetric RTP +strict-sourcerejecting the real source.- High CPU — transcoding.
rtpengine-ctl listshows which sessions transcode. Align codecs end to end so the kernel path is used; only transcode where genuinely required. - Kernel module not forwarding —
xt_RTPENGINEnot loaded, the iptablesRTPENGINE --id 0rule missing, or the daemon's--table 0doesn't match. Everything falls back to (slow) userspace. - WebRTC calls fail — need
ICE=force/ICE=removeappropriately,RTP/SAVPF,rtcp-mux-offer, DTLS; the flag set differs from plain SIP. Check the browser console +rtpenginelog. - Recording produces silence —
record-callflag set but the recording daemon (rtpengine-recording) isn't running or its spool dir isn't writable.
Related tools and reading¶
- On-site: Port Checker, Subnet Calculator.
- Related posts: Kamailio vs Asterisk: when to use each, RTP and RTCP basics for troubleshooting voice.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Telecom Systems
SMSC Basics for Sysadmins
The SMSC is the store-and-forward hub for text messages. Here's the message paths (MO, MT, AO, AT), how retries work, and a Kannel config to get started.
October 24, 2026eSIM and Remote SIM Provisioning
An eSIM is a reprogrammable chip (eUICC) that holds downloadable operator profiles. Here's the GSMA RSP architecture — SM-DP+, SM-DS, LPA — and the profile lifecycle.
October 24, 2026SIP Trunk Basics for Sysadmins
A sysadmin's view of SIP trunking — signalling vs media, the ports involved, and why one-way audio is almost always NAT.
October 24, 2026