---
title: 端口与 NAT 穿透
url: https://doc.liz6.com/networking/03-l4-transport-layer/03-ports-and-nat-traversal
locale: zh
area: networking
tags:
- networking
- L4传输层
date: 2026-06-30
modified: 2026-07-11
description: 端口是传输层的"多路复用器",NAT 把它变成了互联网最大的 hack。STUN/TURN/ICE 这一套 NAT 穿透工具的本质是:在外网映射和内网地址之间建立一条可预测的路径。
---

# 端口与 NAT 穿透

> 端口是传输层的"多路复用器",NAT 把它变成了互联网最大的 hack。STUN/TURN/ICE 这一套 NAT 穿透工具的本质是:在外网映射和内网地址之间建立一条可预测的路径。

## 概述

NAT 穿透是当前互联网架构中最大的妥协——IPv4 地址耗尽迫使数十亿设备共享公网 IP，导致"直接连接"变成了例外。在 NAT 之后，两个设备无法直接通信（除非通过中继或打洞）。STUN 帮助设备发现自己的公网映射、TURN 提供不可穿透时的中继、ICE 自动选择最优路径（直连 > 打洞 > 中继）。Tailscale 将这些技术封装成了用户无感的 mesh VPN。

## Ephemeral Ports

TCP/UDP 连接中 client 端自动选的 port, 范围由内核管理:

```bash
cat /proc/sys/net/ipv4/ip_local_port_range  # 默认: 32768 60999
```

耗尽风险: 高并发短连接 (每个连接 consuming 一个 ephemeral port + TIME_WAIT 期间的 60s) → 端口用光 → `connect()` 返回 EADDRNOTAVAIL

缓解: 增大范围 + tw_reuse

## NAT 分类 (RFC 4787)

NAT 的映射行为决定了穿透难度:

<svg viewBox="0 0 720 400" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="NAT 映射行为分类:从 Full Cone 到 Symmetric,穿透难度递增">
  <rect width="720" height="400" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">NAT 映射行为分类:从 Full Cone 到 Symmetric,穿透难度递增</text>

  <rect x="40" y="48" width="640" height="84" rx="8" fill="#dcfce7" stroke="#4ade80"/>
  <text x="56" y="72" font-size="13.5" font-weight="700" fill="#166534">Full Cone(Endpoint-Independent Mapping, EIM)</text>
  <text x="56" y="92" font-size="11" fill="#15803d">内 (IP₁,Port₁) → 外 (IP_ext,Port_ext),固定映射</text>
  <text x="56" y="108" font-size="11" fill="#15803d">任何外部主机只要知道 Port_ext,就能发包进来</text>
  <text x="664" y="122" text-anchor="end" font-size="12.5" font-weight="700" fill="#166534">最容易穿透</text>

  <rect x="40" y="144" width="640" height="84" rx="8" fill="#ffedd5" stroke="#f97316"/>
  <text x="56" y="168" font-size="13.5" font-weight="700" fill="#9a3412">Address-Dependent Mapping</text>
  <text x="56" y="188" font-size="11" fill="#c2410c">内 (IP₁,Port₁) → 外 (IP_ext,Port_ext_X) 对 外部 IP_X</text>
  <text x="56" y="204" font-size="11" fill="#c2410c">同一内部 endpoint 对不同目标 IP → 不同外部 port</text>
  <text x="664" y="218" text-anchor="end" font-size="12.5" font-weight="700" fill="#9a3412">仅通信过的主机能回包</text>

  <rect x="40" y="240" width="640" height="84" rx="8" fill="#ffffff" stroke="#ef4444" stroke-width="2"/>
  <text x="56" y="264" font-size="13.5" font-weight="700" fill="#dc2626">Symmetric(Address and Port-Dependent Mapping, APDM)</text>
  <text x="56" y="284" font-size="11" fill="#334155">内 (IP₁,Port₁) → 外 (IP_ext,Port_ext_X) 对 (外部 IP_X,Port_X)</text>
  <text x="56" y="300" font-size="11" fill="#334155">外部端口不可预测,每个目标 port 都不同</text>
  <text x="664" y="314" text-anchor="end" font-size="12.5" font-weight="700" fill="#dc2626">最难穿透</text>

  <rect x="40" y="336" width="640" height="50" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="56" y="356" font-size="12.5" fill="#3730a3">由上到下,外部映射的可预测性依次降低 —— 这正是 STUN 打洞从</text>
  <text x="56" y="374" font-size="12.5" fill="#3730a3">"总能成功"到"几乎不可能"的分界线。</text>
</svg>

NAT filtering 行为 (防火墙侧): 类似 mapping, Endpoint-Independent → 最松; Address+Port-Dependent → 最严。

如果 NAT 是 Endpoint-Independent Mapping + Address-Dependent Filtering (最常见 home router), 则 STUN 有效。如果 Symmetric (大多数移动网络), 则需要 TURN 或端口预测。

## STUN (RFC 8489)

<svg viewBox="0 0 720 300" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="STUN 流程:先探测公网映射,再让 Peer 直连">
  <defs><marker id="stunAh" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="300" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">STUN:先探测公网映射,再让 Peer 直连</text>

  <rect x="40" y="50" width="300" height="28" rx="6" fill="#4f46e5"/>
  <text x="190" y="69" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">① 探测公网映射(Binding Request)</text>
  <rect x="52" y="90" width="276" height="44" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="66" y="109" font-size="12" font-weight="700" fill="#3730a3">Client → STUN Server</text>
  <text x="66" y="126" font-size="11" fill="#4f46e5">Binding Request · 端口 3478/5349(TLS)</text>
  <rect x="52" y="140" width="276" height="44" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="66" y="159" font-size="12" font-weight="700" fill="#3730a3">Server 记录来源</text>
  <text x="66" y="176" font-size="11" fill="#4f46e5">src_ip:port(NAT 外部地址)</text>
  <rect x="52" y="196" width="276" height="38" rx="6" fill="#e0e7ff"/>
  <text x="190" y="220" text-anchor="middle" font-size="11.5" font-weight="600" fill="#3730a3">Response: XOR-MAPPED-ADDRESS=1.2.3.4:56789</text>

  <rect x="380" y="50" width="300" height="28" rx="6" fill="#0d9488"/>
  <text x="530" y="69" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">② 告知 Peer,尝试直连</text>
  <rect x="392" y="90" width="276" height="44" rx="6" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="406" y="109" font-size="12" font-weight="700" fill="#115e59">客户端获得公网映射</text>
  <text x="406" y="126" font-size="11" fill="#0f766e">"从公网看,我是 1.2.3.4:56789"</text>
  <rect x="392" y="140" width="276" height="44" rx="6" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="406" y="159" font-size="12" font-weight="700" fill="#115e59">告诉 Peer 该地址</text>
  <text x="406" y="176" font-size="11" fill="#0f766e">Peer 直接发 UDP 到 1.2.3.4:56789</text>
  <rect x="392" y="196" width="276" height="38" rx="6" fill="#ccfbf1"/>
  <text x="530" y="213" text-anchor="middle" font-size="11" font-weight="600" fill="#115e59">Full Cone / 已通信过的 Address-Dependent</text>
  <text x="530" y="227" text-anchor="middle" font-size="11" font-weight="600" fill="#115e59">→ 包能进来</text>

  <line x1="330" y1="215" x2="388" y2="215" stroke="#475569" stroke-width="1.8" marker-end="url(#stunAh)"/>
  <text x="360" y="262" text-anchor="middle" font-size="12" fill="#64748b">STUN 只负责发现地址;能不能真正穿透,取决于对端 NAT 的过滤策略。</text>
</svg>

## TURN (RFC 8656)

STUN 对 Symmetric NAT 无效 — 外部 port 不可预测。TURN 做中继:

<svg viewBox="0 0 720 300" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="TURN 中继流程:穿不透就中继">
  <defs><marker id="turnAh" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="300" fill="#ffffff"/>
  <text x="360" y="30" text-anchor="middle" font-size="18" font-weight="700" fill="#1f2933">TURN:穿不透就中继,永远能通但要付代价</text>

  <rect x="40" y="90" width="150" height="56" rx="6" fill="#e2e8f0"/>
  <text x="115" y="113" text-anchor="middle" font-size="13" font-weight="700" fill="#334155">Peer A</text>
  <text x="115" y="131" text-anchor="middle" font-size="11" fill="#64748b">发起端</text>

  <rect x="230" y="90" width="220" height="56" rx="6" fill="#eef2ff" stroke="#818cf8"/>
  <text x="340" y="113" text-anchor="middle" font-size="13" font-weight="700" fill="#3730a3">TURN Server</text>
  <text x="340" y="131" text-anchor="middle" font-size="11" fill="#4338ca">分配 port 50000(Allocate)</text>

  <rect x="490" y="90" width="190" height="56" rx="6" fill="#e2e8f0"/>
  <text x="585" y="113" text-anchor="middle" font-size="13" font-weight="700" fill="#334155">Peer B</text>
  <text x="585" y="131" text-anchor="middle" font-size="11" fill="#64748b">接收端</text>

  <line x1="190" y1="118" x2="226" y2="118" stroke="#475569" stroke-width="1.6" marker-end="url(#turnAh)"/>
  <text x="208" y="86" text-anchor="middle" font-size="9.5" fill="#475569">① Allocate/Send</text>
  <line x1="450" y1="118" x2="486" y2="118" stroke="#475569" stroke-width="1.6" marker-end="url(#turnAh)"/>
  <text x="468" y="86" text-anchor="middle" font-size="9.5" fill="#475569">② Data Indication</text>

  <text x="360" y="176" text-anchor="middle" font-size="11" fill="#64748b">③ 反向同理:Peer B → TURN Server → Peer A</text>

  <rect x="60" y="200" width="600" height="68" rx="8" fill="#ffedd5" stroke="#f97316"/>
  <text x="76" y="226" font-size="12.5" fill="#9a3412">中继延迟 = N × (A↔TURN 延迟 + TURN↔B 延迟)</text>
  <text x="76" y="248" font-size="12.5" fill="#9a3412">带宽:TURN server 承担两倍(A→TURN + TURN→B)—— 永远能通,但要付延迟与带宽代价。</text>
</svg>

## ICE (RFC 8445): 候选收集与连接检查

<svg viewBox="0 0 720 410" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="ICE:候选收集定优先级,连通性检查选路径">
  <defs><marker id="iceAh" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="410" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">ICE:候选收集定优先级,连通性检查选路径</text>

  <text x="40" y="50" font-size="12.5" font-weight="700" fill="#1f2933">候选收集 Candidate Gathering(按 type_pref 定优先级)</text>
  <text x="40" y="64" font-size="10.5" fill="#94a3b8">priority = 2²⁴×type_pref + 2⁸×local_pref + (256−component_id)</text>

  <text x="40" y="94" font-size="12" font-weight="700" fill="#166534">host</text>
  <rect x="150" y="76" width="300" height="26" rx="3" fill="#22c55e"/>
  <text x="160" y="94" font-size="11" fill="#ffffff">192.168.1.10:50000 · 本地局域网 · type_pref=126(最高)</text>

  <text x="40" y="128" font-size="12" font-weight="700" fill="#3730a3">srflx</text>
  <rect x="150" y="110" width="230" height="26" rx="3" fill="#4f46e5"/>
  <text x="160" y="128" font-size="11" fill="#ffffff">1.2.3.4:56789 · STUN 映射 · type_pref=100</text>

  <text x="40" y="162" font-size="12" font-weight="700" fill="#9a3412">relay</text>
  <rect x="150" y="144" width="70" height="26" rx="3" fill="#f97316"/>
  <text x="230" y="162" font-size="11" fill="#c2410c">5.5.5.5:60000 · TURN 中继 · type_pref=0(最低,仅保底)</text>

  <line x1="40" y1="190" x2="680" y2="190" stroke="#e2e8f0" stroke-width="1"/>

  <text x="40" y="210" font-size="12.5" font-weight="700" fill="#1f2933">连通性检查:对每个 candidate pair 发 STUN Binding Request</text>

  <rect x="260" y="222" width="200" height="40" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="360" y="246" text-anchor="middle" font-size="11.5" font-weight="700" fill="#3730a3">逐对发送,选可用的最高优先级 pair</text>

  <line x1="360" y1="262" x2="140" y2="278" stroke="#475569" stroke-width="1.6" marker-end="url(#iceAh)"/>
  <line x1="360" y1="262" x2="360" y2="278" stroke="#475569" stroke-width="1.6" marker-end="url(#iceAh)"/>
  <line x1="360" y1="262" x2="580" y2="278" stroke="#475569" stroke-width="1.6" marker-end="url(#iceAh)"/>

  <rect x="40" y="280" width="200" height="56" rx="8" fill="#dcfce7" stroke="#4ade80"/>
  <text x="140" y="302" text-anchor="middle" font-size="12" font-weight="700" fill="#166534">host ↔ host 直连</text>
  <text x="140" y="322" text-anchor="middle" font-size="11" fill="#15803d">不经过 NAT</text>

  <rect x="260" y="280" width="200" height="56" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="360" y="302" text-anchor="middle" font-size="12" font-weight="700" fill="#115e59">仅 srflx ↔ srflx 通</text>
  <text x="360" y="322" text-anchor="middle" font-size="11" fill="#0f766e">公网 STUN 路径</text>

  <rect x="480" y="280" width="200" height="56" rx="8" fill="#ffedd5" stroke="#f97316"/>
  <text x="580" y="302" text-anchor="middle" font-size="12" font-weight="700" fill="#9a3412">否则</text>
  <text x="580" y="322" text-anchor="middle" font-size="11" fill="#c2410c">TURN 中继(保底)</text>

  <rect x="40" y="356" width="640" height="40" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="56" y="380" font-size="12.5" fill="#3730a3">Trickle ICE:不等收集完所有候选,边收集边做连通性检查,减少建立时间。</text>
</svg>

## Tailscale 的 NAT 穿透策略

<svg viewBox="0 0 720 420" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="Tailscale NAT 穿透:按优先级逐级尝试,失败才降级">
  <defs><marker id="tsAh" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="420" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">Tailscale NAT 穿透:按优先级逐级尝试,失败才降级</text>

  <rect x="40" y="48" width="640" height="40" rx="6" fill="#dcfce7" stroke="#4ade80"/>
  <text x="56" y="73" font-size="12.5" font-weight="700" fill="#166534">① 同一局域网 → host-to-host 直连(UDP 或 TCP via tailscaled)</text>

  <line x1="60" y1="90" x2="60" y2="104" stroke="#475569" stroke-width="1.6" marker-end="url(#tsAh)"/>
  <text x="76" y="101" font-size="10" fill="#64748b">不同局域网,继续下一级</text>

  <rect x="40" y="106" width="640" height="40" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="56" y="131" font-size="12.5" font-weight="700" fill="#3730a3">② 一方公网 IP + Full Cone NAT → STUN → 直接 UDP</text>

  <line x1="60" y1="148" x2="60" y2="162" stroke="#475569" stroke-width="1.6" marker-end="url(#tsAh)"/>
  <text x="76" y="159" font-size="10" fill="#64748b">双方 Symmetric NAT,继续下一级</text>

  <rect x="40" y="164" width="640" height="40" rx="6" fill="#ffedd5" stroke="#f97316"/>
  <text x="56" y="189" font-size="12.5" font-weight="700" fill="#9a3412">③ 双方 Symmetric NAT → port prediction(生日攻击)</text>

  <line x1="60" y1="206" x2="60" y2="220" stroke="#475569" stroke-width="1.6" marker-end="url(#tsAh)"/>
  <text x="76" y="217" font-size="10" fill="#64748b">预测失败,继续下一级</text>

  <rect x="40" y="222" width="640" height="40" rx="6" fill="#e2e8f0" stroke="#cbd5e1"/>
  <text x="56" y="247" font-size="12.5" font-weight="700" fill="#334155">④ 以上都失败 → DERP relay(自建中继)</text>

  <text x="40" y="282" font-size="12.5" font-weight="700" fill="#1f2933">③ 细节:端口分配模式决定打洞成功率</text>

  <rect x="40" y="294" width="300" height="56" rx="8" fill="#dcfce7" stroke="#4ade80"/>
  <text x="190" y="316" text-anchor="middle" font-size="12" font-weight="700" fill="#166534">递增分配(N, N+1, N+2…)</text>
  <text x="190" y="336" text-anchor="middle" font-size="11" fill="#15803d">可预测 → 可打洞</text>

  <rect x="380" y="294" width="300" height="56" rx="8" fill="#ffffff" stroke="#ef4444" stroke-width="2"/>
  <text x="530" y="316" text-anchor="middle" font-size="12" font-weight="700" fill="#dc2626">随机分配</text>
  <text x="530" y="336" text-anchor="middle" font-size="11" fill="#334155">不可预测 → 打洞不可能</text>

  <line x1="220" y1="350" x2="330" y2="368" stroke="#475569" stroke-width="1.6" marker-end="url(#tsAh)"/>
  <line x1="500" y1="350" x2="390" y2="368" stroke="#475569" stroke-width="1.6" marker-end="url(#tsAh)"/>

  <rect x="200" y="372" width="320" height="40" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="360" y="396" text-anchor="middle" font-size="11" font-weight="700" fill="#3730a3">双方同时向预测 port 发 UDP → 命中的 pair 建立通道</text>
</svg>

## 参考

- **RFC**: 4787, 8489, 8656, 8445, 6887
- **tailscale blog**: "How NAT traversal works"

*Keywords: ephemeral ports, NAT, Full Cone, Symmetric NAT, STUN, TURN, ICE, hole punching, DERP*
