Server

RTPengine for NAT Traversal and Transcoding

By Prabath Thalangama· October 24, 2026· 4 min read
#rtpengine#rtp#nat

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
Offer out (from RTPengine):
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's interface config set to the right public IP (behind its own NAT, set interface=priv/pub advertised addresses)? Is the RTP port range open in the firewall?
  • Audio works then cuts out after ~30s — RTP timeout / the proxy didn't send delete on a re-INVITE, or one side stopped sending and latching timed out.
  • rtpengine-ctl list shows 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-source rejecting the real source.
  • High CPU — transcoding. rtpengine-ctl list shows which sessions transcode. Align codecs end to end so the kernel path is used; only transcode where genuinely required.
  • Kernel module not forwarding — xt_RTPENGINE not loaded, the iptables RTPENGINE --id 0 rule missing, or the daemon's --table 0 doesn't match. Everything falls back to (slow) userspace.
  • WebRTC calls fail — need ICE=force/ICE=remove appropriately, RTP/SAVPF, rtcp-mux-offer, DTLS; the flag set differs from plain SIP. Check the browser console + rtpengine log.
  • Recording produces silence — record-call flag set but the recording daemon (rtpengine-recording) isn't running or its spool dir isn't writable.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.