SIP and NAT explained: STUN, TURN, ICE, rport and SIP ALG
SIP NAT traversal explained: why NAT breaks SIP calls, how rport, STUN, TURN and ICE help, why SIP ALG hurts, and the Asterisk PJSIP settings.
Published 14 min read
SIP breaks behind NAT because it writes IP addresses and ports inside its messages, while the router rewrites only the packet headers. The usual symptoms are a phone that registers but never rings on incoming calls, or a call that connects with one-way audio. Four mechanisms cover most cases: rport and received send responses back to the address that sent the request, STUN tells an endpoint its public mapping, TURN relays media when no direct path works, and ICE tests the candidate paths and picks one. A SIP ALG in the router can rewrite those headers badly, so the usual fix is to turn it off and let the phone and the PBX handle NAT.
Why NAT breaks SIP
A NAT rewrites the source address and port of packets that leave your network. It does not read the payload. SIP does put addresses in the payload: the Via and Contact headers, and the connection address and media port in the SDP body. So a message can name one address while the packet comes from another.
Take a softphone on a home LAN at 192.168.1.50, behind a router whose public address is 203.0.113.20. The phone registers with a PBX at pbx.example.com. Its REGISTER leaves the phone like this, before the router touches it:
REGISTER sip:pbx.example.com SIP/2.0
Via: SIP/2.0/UDP 192.168.1.50:5060;branch=z9hG4bK776asdhds;rport
Max-Forwards: 70
From: <sip:1001@pbx.example.com>;tag=1928301774
To: <sip:1001@pbx.example.com>
Call-ID: a84b4c76e66710@192.168.1.50
CSeq: 1 REGISTER
Contact: <sip:1001@192.168.1.50:5060>
Expires: 300
Content-Length: 0
The router sends that packet out with source 203.0.113.20:49152, and the PBX sees that source. The Contact header still says 192.168.1.50. Here is what each part of the message does once NAT sits in the path:
| What the message carries | Does NAT rewrite it? | What goes wrong |
|---|---|---|
| Packet source address and port | Yes | Nothing, as long as replies go back to the packet source |
| Top Via sent-by | No | Replies go to the private address unless the server adds received and rport |
| Contact header | No | Requests the PBX starts later, such as an incoming INVITE, go to a private address |
| SDP connection line and media port | No | The far end sends RTP audio to a private address |
That table explains the three symptoms people report. Registration works, because the PBX answers the packet source. Incoming calls fail, because the PBX sends the INVITE to the Contact. Calls connect with one-way audio, because the SDP offers a private media address. For the audio case, the one-way audio guide walks through the checks in order.
rport: send the response back where it came from
RFC 3581 added the rport parameter for this. A client that wants symmetric response routing puts the bare parameter rport in the top Via header of its requests. The RFC says the client MAY include it and that it MUST have no value. The empty parameter is a flag that tells the server to use the extension.
When the server gets that request, it fills in two values. The RFC is direct about it: if the Via carries an empty rport, the server "MUST set the value of the parameter to the source port of the request", and it "MUST insert a 'received' parameter containing the source IP address that the request came from, even if it is identical to the value of the 'sent-by' component." RFC 3261 section 18.2.1 sets the baseline for received, which applies whenever the packet's source differs from the sent-by address. The Via the server returns looks like this:
Via: SIP/2.0/UDP 192.168.1.50:5060;branch=z9hG4bK776asdhds;received=203.0.113.20;rport=49152
The response now goes to 203.0.113.20 on port 49152, and it leaves from the same socket that received the request. That is symmetric response routing. Without rport and received, RFC 3261 section 18.2.2 sends UDP responses to the sent-by address, which here is the private 192.168.1.50.
rport only steers responses. Requests the server starts still use the Contact header, and that header still says 192.168.1.50. Asterisk handles this with an endpoint option that rewrites Contact, covered in the Asterisk section below.
Keepalives and binding lifetimes
Two timers are involved. The NAT drops a mapping when it sits idle, and the registrar drops a binding when its Expires runs out. Expires controls the registrar, not the NAT, because only traffic keeps a NAT mapping alive.
RFC 4787 sets the floor for UDP. A NAT UDP mapping timer "MUST NOT expire in less than two minutes", with exceptions for specific well-known ports, and it recommends a default of five minutes or more. Consumer routers do not always follow this. RFC 5626 notes that "many NATs have UDP timeouts as low as 30 seconds". That is why section 4.4.2 sets the STUN keepalive for UDP at a random interval between 24 and 29 seconds when the registrar does not send a Flow-Timer header. For TCP and TLS, section 4.4.1 uses CRLF keepalives at random intervals between 95 and 120 seconds where battery power is not a concern.
On the registrar side, RFC 3261 section 10.3 says the client only suggests a value, and "the registrar selects the actual time interval based on its local policy." In Asterisk, the AOR settings set those bounds. According to the res_pjsip reference, default_expiration is 3600 seconds, minimum_expiration is 60, and maximum_expiration is 7200.
A practical test: register the phone, leave it idle for ten minutes, then call it from outside. If inbound calls fail only after idle periods, the NAT mapping is expiring, and the keepalive interval is the setting to fix.
STUN: learn your public address
STUN (RFC 8489) lets an endpoint learn the public IP and port that a NAT assigned to it. The client sends a Binding request to a STUN server, the server reflects back the source address it saw, and the response carries that address in an XOR-MAPPED-ADDRESS attribute. The default port is 3478 over UDP and TCP, and 5349 for TLS.
The rule that matters most is to send the Binding request from the same socket that carries your media. A NAT mapping belongs to a socket. If the STUN check uses a different socket, you learn the mapping for the wrong flow. The output looks like this, with illustrative values:
Local socket: 192.168.1.50:40000 (the phone's RTP socket)
Binding request to: stun.example.org:3478
NAT mapping seen: 203.0.113.20:61234
XOR-MAPPED-ADDRESS: 203.0.113.20:61234
STUN has a limit. RFC 4787 defines several NAT mapping behaviours. Under address-and-port-dependent mapping, often called symmetric NAT, the NAT reuses an external port only for packets sent to the same destination. A peer at another address gets a different external port. The address STUN reports is then not the one your peer has to reach, so direct media fails even though STUN worked. That is the case TURN is for.
TURN: relay when there is no direct path
TURN (RFC 8656) is the relay. The client sends an Allocate request, authenticated with long-term credentials, and the server returns a relayed transport address in an XOR-RELAYED-ADDRESS attribute. Peers send their packets to that address, and the server forwards them to the client. An allocation has a default lifetime of 600 seconds, so the client keeps it alive with Refresh requests. TURN runs on the same ports as STUN, and it can run over TCP or TLS for networks that block UDP entirely.
The cost is that the relay carries the media. Every packet crosses the relay twice, once in from one side and once out to the other. Size a TURN server for the call's media bitrate in both directions, plus packet overhead. The same arithmetic applies to a PBX that stays in the media path, which is what Asterisk does when direct_media is off. The codec comparison has the per-call numbers.
ICE: let both sides test the paths
ICE (RFC 8445) turns discovery into a choice. Each agent gathers candidates of four types:
- host: an address on one of its own interfaces
- server-reflexive: a public mapping learned from a STUN server
- relayed: an address allocated on a TURN server
- peer-reflexive: an address learned during connectivity checks
Each side pairs its candidates with the remote candidates and sends STUN Binding requests across the pairs, which are the connectivity checks. The controlling agent nominates the pair to use. The RFC's suggested type preferences rank host candidates highest and relayed candidates lowest, so ICE prefers the direct path and uses the relay only when nothing else works.
In SIP, the candidates travel in the SDP as a=candidate lines (RFC 8839). RFC 8840 covers sending candidates incrementally, which helps when gathering takes time. An offer with candidates looks like this, with illustrative values:
m=audio 49170 RTP/AVP 111 0
a=ice-ufrag:4f2b
a=ice-pwd:8c1d9a7e3b5f2c4d6e8f0a1b
a=candidate:1 1 UDP 2130706431 192.168.1.50 49170 typ host
a=candidate:2 1 UDP 1694498815 203.0.113.20 61234 typ srflx raddr 192.168.1.50 rport 49170
ICE only helps when both ends run it. On Asterisk, the endpoint option ice_support defaults to no. Turn it on for an endpoint only after a test call across NAT behaves as you expect, and check which STUN and TURN settings your Asterisk version needs.
SIP ALG: the router feature that breaks SIP
RFC 2663 defines an application-level gateway as an application-specific translation agent that modifies the application's payload. A SIP ALG in a home or small-office router does exactly that. It reads SIP, rewrites Via, Contact and SDP, and tries to keep NAT state in step. RFC 3235 observes that ALGs typically run inside small routers next to the NAT component, and that they should stay simple. A simple rewrite that is only approximately right is the failure mode to expect.
Palo Alto Networks makes the same point in its PAN-OS documentation. Some applications, VoIP among them, have NAT handling built into the client, and the SIP ALG can interfere with signaling and stop the client working. Its advice is to disable the SIP ALG for that application rather than override the policy.
The symptoms vary. Registration seems to work, then the phone stops receiving calls. Calls connect but transfers fail. The SDP carries an address nobody expected. A quick test settles it: disable SIP ALG on the router, restart the phone's registration, and repeat the failed call. If it works, the ALG was the problem. To see what the PBX receives, capture on the PBX:
sudo tcpdump -i any -n -s 0 -A 'udp port 5060'
Look at the Via and Contact in the REGISTER and INVITE messages that arrive. If the Contact holds a private address and other headers have changed in odd ways, the ALG is the first suspect. The setting varies by vendor, so look under the router's NAT, firewall, or application settings.
Asterisk PJSIP: the server-side settings
Asterisk splits its NAT settings between the transport, which holds addresses, and the endpoint, which holds behaviour. These are the ones that matter, with the defaults from the res_pjsip reference:
| Option | Section | Default | Effect |
|---|---|---|---|
| external_signaling_address | transport | unset | Address used in SIP messages sent to outside destinations |
| external_media_address | transport | unset | Address used for RTP in SDP sent to outside destinations |
| local_net | transport | unset | Networks treated as local; addresses inside them get no NAT rewriting |
| direct_media | endpoint | yes | Lets endpoints send media to each other; no keeps Asterisk in the media path |
| rtp_symmetric | endpoint | no | Sends RTP back to the address and port the incoming RTP came from |
| force_rport | endpoint | yes | Uses RFC 3581 behaviour for responses |
| rewrite_contact | endpoint | no | Rewrites Contact to the packet's observed source |
| ice_support | endpoint | no | Enables ICE for the endpoint |
A typical configuration for phones on a private LAN, behind a router with public address 203.0.113.10:
[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0
local_net=192.168.1.0/24
external_signaling_address=203.0.113.10
external_media_address=203.0.113.10
[1001]
; the aor and auth sections are also required and are not shown
type=endpoint
context=internal
disallow=all
allow=opus
allow=g722
allow=ulaw
transport=transport-udp
aors=1001
auth=1001
direct_media=no
rtp_symmetric=yes
force_rport=yes
rewrite_contact=yes
A few notes on this configuration:
- The Asterisk NAT guide says the two external options should be the same address unless signaling and media sit on separate servers.
- Set local_net for every private network your phones use. Asterisk bug report ASTERISK-26879 describes external_media_address being ignored when no local_net is configured, so check the SDP in a trace after any change.
- external_signaling_address and external_signaling_hostname are mutually exclusive. Pick one.
- With direct_media=no the PBX stays in the media path. That costs PBX bandwidth, but it simplifies NAT, because each phone only has to reach the PBX.
- Open SIP on 5060 over UDP and TCP, and 5061 for TLS. Open the RTP range that rtpstart and rtpend define in rtp.conf. The RFC 3261 defaults are 5060 for UDP and TCP and 5061 for TLS.
For the PBX side of the setup, the FreePBX and Asterisk guide covers the account and transport steps. To check your work, the official PJSIP troubleshooting guide uses pjsip show endpoint 1001, which prints the Identify, InAuth and Aor lines, and pjsip set logger on for the SIP trace. Its first CLI steps are pjsip set logger on, core set verbose 4 and core set debug 4.
Symptoms and first checks
| Symptom | Likely cause | Check first |
|---|---|---|
| Phone registers, inbound calls never ring | Contact points to a private address | rewrite_contact, and whether the REGISTER carried rport |
| Call connects, audio is one-way | SDP advertises a private media address | external_media_address with local_net, and direct_media |
| Works after a call, fails after idle time | NAT mapping expired | Keepalive interval, using the RFC 5626 range for UDP |
| Works on the LAN, fails from outside | Firewall blocks the RTP range | rtp.conf range and matching firewall rules |
| Random 4xx errors or failed transfers | SIP ALG rewriting SIP | Disable the ALG and retest |
Testing from the phone side
Darwvin Softphone is in Early Access for Android and as a Linux desktop app. The feature list includes SIP over UDP, TCP and TLS. On desktop it adds a SIP message ladder and RTP stream analysis, and Call Doctor applies rules to explain why a call failed or had bad audio. Use those tools to confirm which symptom you have, then fix the server side. For account setup, see the setup guide.
FAQ
Do I need STUN or TURN for a SIP phone behind a home router?
Often not. With a public PBX address and the external addresses and local_net set, rport and symmetric RTP cover most cases. STUN only tells you your mapping. TURN is for when no direct path exists, such as address-and-port-dependent NAT on both sides. Fix the server settings first, and add TURN only if test calls still fail.
What is the difference between rport and symmetric response routing?
rport is the Via parameter. Symmetric response routing is the behaviour it enables, sending the response to the request's source IP and port from the socket that received it (RFC 3581).
Why does turning off SIP ALG fix calls?
An ALG rewrites SIP text without the full picture, so one wrong Via, Contact or SDP line can break the session. Once rport, received and the Asterisk NAT settings do the job, the ALG adds little and can break what works. Palo Alto's documentation gives the same advice for its firewalls.
What registration interval should I use behind NAT?
Pick the Expires value you want for the registrar, within Asterisk's AOR bounds of 60 to 7200 seconds, with a default of 3600. That value does not keep the NAT mapping open. For UDP, rely on keepalives in the range RFC 5626 gives, 24 to 29 seconds when no Flow-Timer is sent, and confirm with an idle test.
Which ports need to be open for SIP through a firewall?
SIP uses 5060 over UDP and TCP, and 5061 over TLS, according to RFC 3261. Open the RTP range that rtp.conf sets with rtpstart and rtpend, because media uses it. If you run STUN or TURN, open 3478, and 5349 for TLS.
References
- RFC 3261, SIP: https://www.rfc-editor.org/rfc/rfc3261
- RFC 3581, rport and symmetric response routing: https://www.rfc-editor.org/rfc/rfc3581
- RFC 2663, NAT terminology and the ALG definition: https://www.rfc-editor.org/rfc/rfc2663
- RFC 3235, NAT-friendly application design: https://www.rfc-editor.org/rfc/rfc3235
- RFC 4787, NAT behaviour for UDP: https://www.rfc-editor.org/rfc/rfc4787
- RFC 5626, keepalives for SIP connections: https://www.rfc-editor.org/rfc/rfc5626
- RFC 8489, STUN: https://www.rfc-editor.org/rfc/rfc8489
- RFC 8656, TURN: https://www.rfc-editor.org/rfc/rfc8656
- RFC 8445, ICE: https://www.rfc-editor.org/rfc/rfc8445
- RFC 8839, SDP offer/answer procedures for ICE: https://www.rfc-editor.org/rfc/rfc8839
- RFC 8840, trickle ICE for SIP: https://www.rfc-editor.org/rfc/rfc8840
- Asterisk, configuring res_pjsip to work through NAT: https://docs.asterisk.org/Configuration/Channel-Drivers/SIP/Configuring-res_pjsip/Configuring-res_pjsip-to-work-through-NAT
- Asterisk, res_pjsip reference: https://docs.asterisk.org/Latest_API/API_Documentation/Module_Configuration/res_pjsip/
- Asterisk, PJSIP troubleshooting guide: https://docs.asterisk.org/Configuration/Channel-Drivers/SIP/Configuring-res_pjsip/Asterisk-PJSIP-Troubleshooting-Guide/
- Palo Alto Networks, disable the SIP ALG (PAN-OS 9.1): https://docs.paloaltonetworks.com/content/techdocs/en_US/pan-os/9-1/pan-os-admin/app-id/disable-the-sip-application-level-gateway-alg.html
- ASTERISK-26879, external_media_address and local_net: https://issues-archive.asterisk.org/ASTERISK-26879