A sends a request to the STUN server from the same UDP socket and obtains the external address 203.0.113.10:62000, which it tells B. However, packets sent by B do not reach A: this may be because A used a different mapping when communicating with B, or because the mapping is the same but inbound filtering does not allow B. NAT traversal requires handling both issues simultaneously and confirming candidate paths through actual connectivity checks.
Mechanism
Provides
Does Not Guarantee
Local Port Allocation
Distinguishes sockets/transport endpoints
Every connection globally exclusively uses one port number
NAT Mapping
State correspondence between internal/external addresses and ports
Any external source can enter inbound
STUN
Observe mappings and build connectivity checks
Automatically open paths to all peers
TURN
Allocate and maintain relay carriers
Relay is always reachable, without resource or authentication failures
ICE
Collect candidates, check candidate pairs, and nominate
Directly measure and select the globally lowest RTT path
Port Capacity: Clarify Which Fields Are Fixed
TCP connections are typically distinguished by source/destination addresses and ports within the corresponding network context. The same local port may be used to connect to different remote endpoints, so the statement "with 28,232 ephemeral ports, you can only establish a total of 28,232 TCP connections" is incorrect. Explicit binds, different socket usage, target concentration, and NAT allocation policies alter multiplexing conditions. Cloudflare: Ephemeral Ports and Connection Quadruplet Experiments
If the ephemeral range is 32768–60999, there are 28,232 numbers including both ends. For a fixed source IP and fixed destination IP:port, temporarily ignoring reserved ports, concurrent survival, and secure multiplexing, if each short connection occupies the quadruplet for about 60 seconds, the magnitude estimate is 28,232/60 ≈ 470 new connections per second. This is a capacity budget under restricted conditions, not the global performance upper limit of the machine.
Expanding source addresses, multiplexing connections, and reducing invalid short connections may change the budget; increasing the port range does not fix limitations on file descriptors, memory, conntrack, or upstream NAT port quotas. Identify the failure point first, then explain errors like EADDRNOTAVAIL; do not unconditionally modify TIME_WAIT parameters just because an error occurs.
# Read-only verification of local configuration and connection status; this article does not modify these parameters.cat /proc/sys/net/ipv4/ip_local_port_rangecat /proc/sys/net/ipv4/ip_local_reserved_portsss -sss -tan state time-wait
UDP does not have the TCP TIME_WAIT state, but the mapping and filtering states reserved by NAT/conntrack for UDP may still occupy resources, time out, or be reclaimed; the two cannot be treated as a single timer.
Mapping and Filtering: Two Independent Classification Axes
Assume the internal endpoint is fixed at I=192.168.1.10:50000, visiting S1=198.51.100.1:3478, S2=198.51.100.2:3478, and S3=198.51.100.2:9999 in sequence.
Mapping Behavior
Does it include destination in mapping reuse conditions?
Understanding in this example
Endpoint-Independent Mapping (EIM)
Does not depend on external destination endpoint
I can reuse the same mapping for all three
Address-Dependent Mapping (ADM)
Depends on external destination IP
S2/S3 can reuse, S1 is not in the same group
Address and Port-Dependent Mapping (APDM)
Depends on destination IP and port
Three destinations are handled separately
Classifications describe the dependencies of mappings; they do not require that a never-before-seen external port be allocated every time, nor do they imply that APDM ports are necessarily random and unpredictable. Port reservation, conflict handling, and mapping classification are different dimensions. RFC 4787: UDP NAT Mappings
Filtering Behavior
Which external sources are allowed when a mapping already exists?
Endpoint-Independent Filtering
Does not restrict sources based on previously contacted external endpoints
Address-Dependent Filtering
Source IP must meet conditions established by previous outbound communication
Address and Port-Dependent Filtering
Both source IP and port must meet corresponding conditions
EIM can coexist with strict filtering; legacy Cone/Symmetric labels compress multiple behaviors into one name, which is unsuitable for precise diagnosis. You cannot assert behavior and success rates solely based on labels like "home" or "mobile network". RFC 4787: Filtering
Same Public Port, Why Are Return Packets Still Blocked?
First, contact only A:3478, then switch the filtering axis to observe the admission of the other two sources. Then contact B:3478, and switch the mapping axis to observe whether a new external endpoint is created. The allowed list for each row corresponds only to that row's mapping; you cannot use communication history from another mapping to allow it through.
Preparing the visual
Same Public Port, Why Are Return Packets Still Blocked?
Mapping answers how external endpoints are reused; filtering answers which sources can enter an existing mapping. They are not on the same axis; observing a mapping via STUN does not prove that any peer can enter.
{"id":"nat-mapping-filtering","title":"Same Public Port, Why Are Return Packets Still Blocked?","summary":"Mapping answers how external endpoints are reused; filtering answers which sources can enter an existing mapping. They are not on the same axis; observing a mapping via STUN does not prove that any peer can enter.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"form-field\"><span data-i18n=\"mapping\"></span><select id=\"mapping\"><option value=\"ei\">Endpoint-independent</option><option value=\"ad\">Address-dependent</option><option value=\"ap\">Address+port-dependent</option></select></label><label class=\"form-field\"><span data-i18n=\"filter\"></span><select id=\"filter\"><option value=\"ei\">Endpoint-independent</option><option value=\"ad\">Address-dependent</option><option value=\"ap\">Address+port-dependent</option></select></label><label class=\"check\"><input type=\"checkbox\" id=\"a\" checked><span data-i18n=\"a\"></span></label><label class=\"check\"><input type=\"checkbox\" id=\"b\"><span data-i18n=\"b\"></span></label><label class=\"check\"><input type=\"checkbox\" id=\"c\"><span data-i18n=\"c\"></span></label><section class=\"panel\"><h3 data-i18n=\"records\"></h3><div id=\"records\"></div></section><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#records .tr{grid-template-columns:1fr 1.4fr 1.8fr}#records .tr>*{min-height:8em;white-space:pre-line}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}const remote=[['A',3478],['B',3478],['B',9999]];function draw(){const map=$('#mapping').value,filter=$('#filter').value,groups=new Map;['a','b','c'].forEach((id,i)=>{if($('#'+id).checked){const key=map==='ei'?'I':map==='ad'?remote[i][0]:remote[i].join(':');if(!groups.has(key))groups.set(key,[]);groups.get(key).push(i)}});const rows=[['public','contacts','inbound']],result=[];[...groups].forEach(([key,seen],j)=>{const allowed=remote.map((r,i)=>filter==='ei'||seen.some(n=>remote[n][0]===r[0]&&(filter==='ad'||remote[n][1]===r[1])));rows.push([62000+j,seen.map(i=>remote[i].join(':')).join('\\n'),remote.map((r,i)=>r.join(':')+' '+(allowed[i]?'✓':'✗')).join('\\n')]);result.push({port:62000+j,seen,allowed})});while(rows.length<4)rows.push(['—','—','—']);table('records',rows);$('#records').dataset.snapshot=JSON.stringify(result);}document.querySelectorAll('input,select').forEach(e=>e.onchange=draw);draw();","audio":false,"strings":{"a":"A:3478","b":"B:3478","c":"B:9999","contacts":"Sent to these endpoints","filter":"Filtering Behavior","heading":"Same Public Port, Why Are Return Packets Still Blocked?","intro":"Select mapping and filtering rules separately, then select the remote endpoints contacted by the internal host.","mapping":"Mapping Behavior","records":"Mappings and reachable remote endpoints","reset":"Reset","scope":"Fix one internal UDP endpoint, three remote endpoints A:3478, B:3478, B:9999; assign different example ports for different mappings. Filtering calculates actual contact records for each mapping, ignoring timeouts and port reuse.","scopeLabel":"Model Scope and Conventions"}}
STUN: The Server Tells You the Source It Sees
After the Binding Request passes through the NAT, the STUN server sees the translated source endpoint and returns it in the XOR-MAPPED-ADDRESS of the response. The STUN fixed header is 20 bytes, containing message type, attribute length, magic cookie, and transaction ID; subsequent attributes have type, length, and alignment rules. XOR is an encoding method, not encryption for address confidentiality. RFC 8489: STUN
For IPv4 XOR-MAPPED-ADDRESS, the port is XORed with the high 16 bits of the cookie, and the address is XORed with the full 32-bit cookie; IPv6 masks also incorporate the transaction ID. The code at the end verifies the round-trip encoding/decoding of the example 203.0.113.10:62000.
What you get is the address seen by the server during this transaction. If the mapping depends on the destination, the external endpoint for B may be different; if the state times out, the old address may also become invalid. Tests must use the sockets and context required for actual communication; do not randomly open another port to probe and use the result to replace the original socket.
Why Do UDP Hole Punching Require Both Parties to Cooperate?
A and B exchange candidate endpoints via their own application signaling channels and send check packets to each other's candidates respectively. Outbound packets may establish the corresponding mapping and filtering permissions, allowing subsequent packets from the peer to pass through. Discarding the first check does not necessarily mean failure; retries, timing, and state lifespan are all factors.
STUN is not responsible for implementing this signaling channel for the application. Hole punching is not "the STUN server forwards all subsequent data"; once a direct connection is established, application data can flow directly between the two ends. Multiple layers of NAT, different mapping behaviors, hairpin support, and outbound UDP restrictions all affect the result.
TURN: Clearly Distinguish Protocols on Both Sides of the Relay
The client first obtains a relay address and maintains an allocation via Allocate, then establishes corresponding permissions for the peer. Sending can use Send Indication; after establishing channel binding, more compact ChannelData can also be used; these are not application payloads forwarded by the server to ordinary peers as-is. RFC 8656: TURN
B sends ordinary UDP to the relay address; the server receives it according to permissions and other conditions, then forwards it to A via Data Indication or ChannelData. Data Indication belongs to the TURN server-to-client side and cannot be placed on the ordinary UDP peer side. The client-to-TURN connection can choose supported carriers such as UDP, TCP, or TLS, but this does not mean that ordinary relay peers automatically switch to the same protocol.
Allocations, permissions, and channel bindings have their own lifespans and refresh rules. Credentials, quotas, server resources, and network policies can all cause relay failures; therefore, TURN provides a backup carrier rather than a "always working" solution. TLS to the relay only protects the corresponding link; for end-to-end confidentiality, business logic still relies on corresponding end-to-end encryption.
How to Budget for Latency and Bandwidth
Assume one-way A→R is 15 ms and R→B is 20 ms; ignoring queuing and processing, the relay path for this direction is about 35 ms; if the reverse is exactly symmetric, the RTT is about 70 ms. Do not add two RTTs and multiply by an unknown N as a universal formula, nor guarantee that the relay is always slower than all direct connections.
If A→B application payload is 2 Mbit/s and B→A is 1 Mbit/s, the relay needs to receive 3 Mbit/s and send 3 Mbit/s, totaling 6 Mbit/s for RX+TX, plus encapsulation and protocol overhead. Network cards, service provider billing, and capacity limits have different metrics; clarify whether the measurement is for ingress, egress, or both.
ICE: Candidate Pairs Must Be Checked and Nominated
Candidates include host (local endpoint), server-reflexive (observed from STUN), and relayed (allocated by TURN); peer-reflexive candidates may also be discovered during checks. Both parties exchange candidates and ICE credentials via signaling, forming comparable candidate pairs and arranging STUN connectivity checks. RFC 8445: ICE
Candidate priority is often constructed as 2²⁴×type_pref + 2⁸×local_pref + (256−component_id). Type preference is a policy preference, not a measured RTT. Successful checks produce usable candidate pairs; the controlling role determines the pair to use via nomination, the controlled role cooperates, and there are rules for handling role conflicts.
Candidate pairs do not only exist in the neat combinations of host↔host, srflx↔srflx, and relay↔relay. Sorting, check scheduling, triggered checks, and nomination form a complete process; do not draw it as a fixed serial ladder where "any relay is considered only after all direct connections completely fail." Applications can also restrict candidate types for privacy or network policy reasons.
Trickle ICE allows incremental delivery of candidates, allowing collection and checking to overlap; it does not exempt authentication, checks, or nomination. When the network changes, corresponding restart or persistence mechanisms are still needed; initial success does not guarantee permanent availability of candidates. RFC 8838: Trickle ICE
Tailscale as an Implementation Case
Tailscale has direct and relay paths; official documentation also distinguishes between DERP and peer relay. The system can first use available relays to maintain connections while attempting more suitable direct connections; relay communication and direct discovery can overlap.
DERP is not another name for TURN; specific path establishment, WireGuard encrypted carriers, and relay services should be understood according to product documentation. Random port allocation increases the difficulty of probing, but "random means it is mathematically impossible to hit" is also incorrect; do not rely on infinite port probing to replace reliable relay design. Tailscale Connection Types
Diagnostic Order: Locate Where State is Lost
Known Phenomenon
Priority Check
Local socket establishment failure
Ports, binding method, FD/memory, and error codes
STUN no response
Server reachability, transaction matching, protocol, and filtering
Has srflx but direct connection fails
Same socket, destination-dependent mapping, bidirectional filtering, state lifespan
TURN allocation successful but no data
Permission, channel, relay address, and peer path
ICE checks successful but no application data
Nomination result, application mapping, encryption state, and subsequent path changes
IPv6 can reduce the need for IPv4 address translation, but having a global address does not mean the firewall allows inbound traffic; ICE can still be used to select and verify paths. Port mapping tools explicitly request mappings on manageable gateways, which is a different approach from observing NAT behavior with STUN.
Local Example: XOR Address and Capacity
fromipaddressimportIPv4Addressimportstructcookie=0x2112A442port=62000address=int(IPv4Address('203.0.113.10'))value=struct.pack('!BBHI',0,1,port^(cookie>>16),address^cookie)reserved,family,xport,xaddress=struct.unpack('!BBHI',value)assertreserved==0andfamily==1assertxport^(cookie>>16)==portassertstr(IPv4Address(xaddress^cookie))=='203.0.113.10'assertvalue.hex()=='0001d322ea12d548'assert60999-32768+1==28232assert470<28232/60<471defpriority(type_pref,local_pref,component):return(1<<24)*type_pref+(1<<8)*local_pref+256-componentassertpriority(126,65535,1)==2130706431assertpriority(126,65535,1)>priority(100,65535,1)>priority(0,65535,1)assert2*(2+1)==6print('STUN IPv4 XOR attribute, port budget, ICE priority, and relay traffic check passed')
The example does not include complete STUN transactions, authentication, or ICE state machines; no hole punching, port scanning, relay allocation, or gateway configuration is initiated.
Continue Reading
TCP: Quadruplets, TIME_WAIT, and connection lifecycle.
UDP and QUIC: Datagrams, migration, and path verification.