A は同じ UDP ソケットから STUN サーバーへリクエストを送信し、外部アドレス 203.0.113.10:62000 を取得して B に伝えます。しかし、B から送られてきたパケットは A に到達しませんでした。これは、A が B と通信する際に別のマッピングを使用したか、またはマッピングは同じだが着信フィルタが B を許可していない可能性があります。NAT 穴あけでは、これら2つの問題の両方を同時に処理し、実際の接続性チェックを通じて候補パスを確認する必要があります。
メカニズム 提供するもの 保証しないもの
ローカルポート割り当て ソケット/トランスポートエンドポイントの区別 すべての接続がグローバルに排他的に1つのポート番号を使用するわけではない
NAT マッピング 内部と外部のアドレスおよびポートの対応関係 任意の外部発信元からの着信が許可されるわけではない
STUN マッピングの観察および接続性チェックの構築 自動的にすべてのピアへのパスを開くわけではない
TURN リレーの割り当ておよび維持 リレーが常に到達可能である、またはリソースや認証に失敗しないわけではない
ICE 候補の収集、候補ペアのチェックおよびノミネーション 直接測定してネットワーク全体の最低 RTT パスを選択するわけではない
ポート容量:まず固定されているフィールドを明確にする
TCP 接続は通常、ソース/宛先アドレスとポートによって、対応するネットワークコンテキスト内で区別されます。同じローカルポートが異なるリモートピアへの接続に使用される可能性があるため、「一時ポートが 28232 個しかない場合、合計で 28232 本の TCP 接続しか確立できない」という主張は成立しません。明示的な bind、異なるソケットの使用方法、ターゲットの集中度、および NAT 割り当てポリシーは、再利用条件を変更します。Cloudflare:一時ポートと接続の4元組に関する実験
一時ポートの範囲が 32768~60999 の場合、両端を含めて 28232 個の番号があります。固定されたソース IP、固定された宛先 IP:port に対して、保留ポート、並行存続、および安全な再利用を考慮せず、短い接続ごとに4 タプルが約 60 秒占有すると仮定すると、規模の推定は 28232/60≈470 回の新しい接続/秒となります。これは制限条件下での容量予算であり、マシン全体の性能上限ではありません。
ソースアドレスの拡張、接続の再利用、無効な短い接続の削減は予算を変更する可能性があります。ポート範囲を拡大しても、ファイルディスクリプタ、メモリ、conntrack、またはアップストリーム NAT のポート割当制限は解消されません。失敗点を特定し、EADDRNOTAVAIL などのエラーを説明してから、エラーが発生したからといって無条件に TIME_WAIT パラメータを変更してはいけません。
cat /proc/sys/net/ipv4/ip_local_port_range
cat /proc/sys/net/ipv4/ip_local_reserved_ports
ss - s
ss - tan state time-wait
UDP には TCP の TIME_WAIT 状態はありませんが、NAT/conntrack が UDP 用に保持するマッピングおよびフィルタリング状態は、リソースを占有したり、タイムアウトしたり、回収されたりする可能性があります。これらを1つのタイマーで混同して扱うことはできません。
マッピングとフィルタリング:2つの独立した分類軸
マッピングとフィルタリングはそれぞれ2つの質問に答える
マッピング:送信時にどの外部エンドポイントを使用するか
同じローカルソケット → 異なる宛先
フィルタリング:どの外部発信元が着信できるか
外部エンドポイントが既に存在する → 許可されるか
EIM:同じマッピングを再利用可能
依然として連絡済みの IP:port からのみ受信可能
Endpoint-Independent Mapping は、誰でも着信できることを意味しません。Full Cone と EIM を単純に等号で結んではいけません。
内部エンドポイントを I=192.168.1.10:50000 として固定し、S1=198.51.100.1:3478、S2=198.51.100.2:3478、S3=198.51.100.2:9999 に順にアクセスします。
マッピング動作 宛先をマッピング再利用条件に含めるか この例での解釈
Endpoint-Independent Mapping (EIM) 外部宛先エンドポイントに依存しない I は三者に対して同じマッピングを再利用可能
Address-Dependent Mapping (ADM) 外部宛先 IP に依存する S2/S3 は再利用可能だが、S1 は同一グループに属さない
Address and Port-Dependent Mapping (APDM) 宛先 IP とポートの両方に依存する 3つの宛先はそれぞれ個別に処理される
分類はマッピングの依存関係を記述するものであり、毎回必ず未使用の外部ポートを割り当てることを要求するものではなく、APDM のポートが必ずランダムで予測不可能であることを示すものでもありません。ポートの予約、競合処理、およびマッピング分類は異なる次元です。RFC 4787:UDP NAT マッピング
フィルタリング動作 既存のマッピングがある場合に許可される外部発信元
Endpoint-Independent Filtering 以前に連絡した外部エンドポイントによって発信元が制限されない
Address-Dependent Filtering 発信元 IP は、以前の出信通信で確立された条件に合致する必要がある
Address and Port-Dependent Filtering 発信元 IP とポートの両方が、対応する条件に合致する必要がある
EIM は厳格なフィルタリングと共存できます。旧式の Cone/Symmetric ラベルは複数の動作を1つの名前に圧縮しており、正確な診断には適していません。「家庭用」や「モバイルネットワーク」というラベルだけで、動作と成功率を断定することもできません。RFC 4787:フィルタリング
同じパブリックポートなのに、なぜ返信パケットがブロックされるのか?
まず A:3478 への連絡のみを行い、フィルタリング軸を切り替えて他の2つの発信元の許可状況を観察します。次に B:3478 への連絡を行い、マッピング軸を切り替えて新しい外部エンドポイントが作成されるかどうかを観察します。各行の許可リストは、その行のマッピングのみに対応します。別のマッピング上の通信履歴を使って、それを通すことはできません。
図を準備しています 再読み込み
同じパブリックポートなのに、なぜ返信パケットがブロックされるのか? マッピングは外部エンドポイントの再利用方法を答え、フィルタリングは既存のマッピングへの着信を許可する発信元を答えます。これらは同じ軸ではありません。STUN がマッピングを観察しても、任意のピアが着信できることを証明するものではありません。
{"id":"nat-mapping-filtering","title":"同じパブリックポートなのに、なぜ返信パケットがブロックされるのか?","summary":"マッピングは外部エンドポイントの再利用方法を答え、フィルタリングは既存のマッピングへの着信を許可する発信元を答えます。これらは同じ軸ではありません。STUN がマッピングを観察しても、任意のピアが着信できることを証明するものではありません。","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":"これらのエンドポイントに送信済み","filter":"フィルタリング動作","heading":"同じパブリックポートなのに、なぜ返信パケットがブロックされるのか?","intro":"マッピングルールとフィルタリングルールをそれぞれ選択し、内蔵ホストが連絡したピアを選択します。","mapping":"マッピング動作","records":"マッピングおよび着信可能なピア","reset":"リセット","scope":"固定された内蔵UDPエンドポイント、3つのピア A:3478、B:3478、B:9999;異なるマッピングに対して異なるサンプルポートを割り当てます。フィルタリングは、各マッピングの実際の連絡記録に基づいて計算され、タイムアウトとポート再利用は無視されます。","scopeLabel":"モデル範囲と規約"}}
STUN:サーバーが観察した発信元を教える
Binding Request は NAT を通過した後、STUN サーバーは変換されたソースエンドポイントを観察し、レスポンスの XOR-MAPPED-ADDRESS フィールドに返します。STUN の固定ヘッダーは 20 バイトで、メッセージタイプ、属性領域の長さ、マジッククッキー、およびトランザクション ID を含みます。続く属性にはタイプ、長さ、およびアライメントルールがあります。XOR はエンコーディング方式であり、アドレスを秘匿するための暗号化ではありません。RFC 8489:STUN
IPv4 の XOR-MAPPED-ADDRESS については、ポートとクッキーの上位 16 ビットが XOR され、アドレスは完全な 32 ビットクッキーと XOR されます。IPv6 のマスクにはトランザクション ID も組み合わされます。末尾のコードは、本例の 203.0.113.10:62000 の往復エンコード/デコードを検証します。
得られるのはサーバーがこのトランザクションで観察したアドレス です。マッピングが宛先に依存する場合、B 宛ての外部エンドポイントは異なる可能性があります。状態がタイムアウトした場合、古いアドレスも無効になる可能性があります。テストには、実際の通信に必要なソケットとコンテキストを使用する必要があり、他のポートを適当に開いて結果を取得し、元のソケットの結果と置き換えてはいけません。
UDP 穴あけでなぜ双方の協調が必要なのか
A と B は、アプリケーション独自のシグナリングチャネルを介して候補エンドポイントを交換し、それぞれ相手の候補に対してチェックパケットを送信します。送信パケットは対応するマッピングおよびフィルタ許可を確立する可能性があり、その後で相手のパケットが通過する機会を得ます。最初のチェックで破棄されたからといって、必ずしも失敗するわけではありません。リトライ、タイミング、および状態の寿命が関係しています。
STUN は、アプリケーションがこのシグナリングチャネルを実装することを担当していません。穴あけは「STUN サーバーが後続の全データを転送する」ものではありません。ピアツーピア接続が確立されると、アプリケーションデータは両端の間で直接フローします。多重 NAT、異なるマッピング動作、ヘアピンサポート、および送信 UDP の制限は結果に影響を与えます。
TURN:リレー両側のプロトコルを明確に区別する
クライアントはまず Allocate を通じてリレーアドレスを取得し、割り当てを維持してから、相手側に適切なパーミッションを確立します。送信には Send Indication を使用でき、Channel Binding が確立された後は、よりコンパクトな ChannelData も使用できます。これらは、サーバーが通常のピアに対してアプリケーションペイロードをそのまま転送するものではありません。RFC 8656:TURN
TURN:クライアント側は TURN 封入、対向側はリレー UDP
クライアント A
割り当てが確立済み
TURN サーバー R
リレーアドレス R:60000
対向側 B
通常の UDP ピア
Send Indication / ChannelData
UDP:ソース R:60000、宛先 B
UDP:ソース B、宛先 R:60000
Data Indication / ChannelData
対応するパーミッションが必要です。ChannelData には Channel Binding も必要です。TURN は、ターゲット、認証、およびネットワークが常に利用可能であることを保証しません。
B はリレーアドレスに対して通常の UDP を送信し、サーバーはパーミッションなどの条件に基づいて受信し、Data Indication または ChannelData を通じて A に転送します。Data Indication は TURN サーバーからクライアントへの片方向のものであり、通常の UDP ピア側に配置することはできません。クライアントから TURN への接続には、UDP、TCP、TLS など、サポートされている伝送路を選択できますが、通常のリレーピアが自動的に同じプロトコルに切り替わることを意味するものではありません。
割り当て、パーミッション、Channel Binding には、それぞれ寿命と更新ルールがあります。資格情報、クォータ、サーバーリソース、およびネットワークポリシーにより、リレーが失敗する可能性があります。そのため、TURN は「常に接続可能」なものではなく、代替伝送路を提供するものです。リレーへの TLS は対応するリンクのみを保護します。エンドツーエンドの機密性が必要な業務では、依然として対応するエンドツーエンド暗号化に依存します。
レイテンシと帯域幅の予算はどのように計算するか
片方向 A→R=15 ms、R→B=20 ms と仮定し、キューイングと処理を無視すると、その方向のリレーパスは約 35 ms になります。逆方向がちょうど対称である場合、RTT は約 70 ms になります。2つの RTT を足して不明な N を掛けることを一般的な公式として使用することはできず、リレーがすべてのピアツーピア接続よりも必ずしも遅くなるとは限りません。
A→B のアプリケーションペイロードが 2 Mbit/s、B→A が 1 Mbit/s の場合、リレーは 3 Mbit/s を受信し、3 Mbit/s を送信する必要があり、RX+TX の合計は 6 Mbit/s になります。さらに、エンコーディングとプロトコルのオーバーヘッドがかかります。ネットワークカード、プロバイダーの課金、および容量制限の口径は異なるため、測定対象が入口、出口、またはその合計のどちらであるかを明確にする必要があります。
ICE:候補ペアはチェックされ、ノミネートされる必要がある
候補には、host(ローカルエンドポイント)、server-reflexive(STUN で観察されたもの)、relayed(TURN で割り当てられたもの)が含まれ、チェック時には peer-reflexive 候補が発見されることもあります。双方はシグナリングを介して候補と ICE 資格情報を交換し、比較可能な候補ペアを形成し、STUN 接続性チェックを手配します。RFC 8445:ICE
候補の優先度は、通常 2²⁴×type_pref + 2⁸×local_pref + (256−component_id) によって構築されます。type preference はポリシーに基づく優先度であり、実測された RTT ではありません。チェックが成功すると、使用可能な候補ペアが生成されます。controlling 役割はノミネーションを通じて使用するペアを決定し、controlled 役割はこれに協力します。役割の衝突にも処理ルールがあります。
候補ペアは、host↔host、srflx↔srflx、relay↔relay の3つの整然とした組み合わせだけではありません。ソート、チェックスケジューリング、トリガーチェック、およびノミネーションが一連のフローを構成しており、「すべてのピアツーピア接続が完全に失敗してから、リレーを考慮し始める」という固定された直列階段として描くことはできません。アプリケーションは、プライバシーやネットワークポリシーの理由で、候補タイプを制限することもできます。
Trickle ICE は、候補を増分的に伝送することを許可し、収集とチェックを重複して行います。認証、チェック、またはノミネーションを免除するものではありません。ネットワークが変更された後には、対応する再起動または永続化メカニズムが必要であり、最初の成功が候補の永久利用可能性を保証するものではありません。RFC 8838:Trickle ICE
Tailscale を実装例として
Tailscale にはピアツーピア接続とリレーパスがあり、公式ドキュメントでは DERP と peer relay を区別しています。システムは、利用可能なリレーを使用して接続を維持しながら、より適切なピアツーピア接続を試すことができます。リレー通信とピアツーピア接続の発見は重複して行うことができます。
DERP は TURN の別名ではありません。具体的なパスの確立、WireGuard 暗号化伝送、およびリレーサービスは、製品ドキュメントに基づいて理解する必要があります。ランダムなポート割り当ては探難を困難にしますが、「ランダムであれば数学的に絶対にヒットしない」という主張も成立しません。無限のポートを試すことで、信頼性の高いリレー設計を代替してはいけません。Tailscale 接続タイプ
診断順序:どのレイヤーで状態が失われたかを特定する
既知の現象 優先して確認すべき項目
ローカルソケットの確立失敗 ポート、バインド方法、FD/メモリ、およびエラーコード
STUN の応答なし サーバーの到達可能性、トランザクションの一致、プロトコルおよびフィルタ
srflx はあるがピアツーピア接続が失敗 同じソケット、宛先依存マッピング、双方向フィルタ、状態の寿命
TURN 割り当ては成功したがデータなし パーミッション、チャンネル、リレーアドレス、および対向パス
ICE チェックは成功したがアプリケーションにデータなし ノミネーション結果、アプリケーションマッピング、暗号化状態、および後続のパス変更
IPv6 は IPv4 のアドレス変換ニーズを減らしますが、グローバルアドレスがあってもファイアウォールが入信を許可するとは限りません。ICE は依然としてパスの選択と検証に使用できます。ポートマッピングツールは、管理可能なゲートウェイ上で明示的にマッピングを要求するものであり、NAT の動作を観察する STUN とは異なるアプローチです。
ローカル計算例:XOR アドレスと容量
from ipaddress import IPv4Address
import struct
cookie = 0x 2112A442
port = 62000
address = 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 )
assert reserved == 0 and family == 1
assert xport ^ ( cookie > > 16 ) == port
assert str ( IPv4Address ( xaddress ^ cookie ) ) == ' 203.0.113.10'
assert value . hex ( ) == ' 0001d322ea12d548'
assert 60999 - 32768 + 1 == 28232
assert 470 < 28232 / 60 < 471
def priority ( type_pref , local_pref , component ) :
return ( 1 < < 24 ) * type_pref + ( 1 < < 8 ) * local_pref + 256 - component
assert priority ( 126 , 65535 , 1 ) == 2130706431
assert priority ( 126 , 65535 , 1 ) > priority ( 100 , 65535 , 1 ) > priority ( 0 , 65535 , 1 )
assert 2 * ( 2 + 1 ) == 6
print ( ' STUN IPv4 XOR 属性、ポート予算、ICE 優先度、およびリレートラフィックチェックが通過しました' )
この例には、完全な STUN トランザクション、認証、または ICE 状態マシンは含まれていません。穴あけ、ポートスキャン、リレー割り当て、またはゲートウェイ設定の試行は行っていません。
継続的な参照