端口与 NAT 穿透

本页目录

A 从同一个 UDP socket 向 STUN 服务器发送请求,得到外部地址 203.0.113.10:62000,把它告诉 B。B 发来的包却没有到达 A:可能是 A 向 B 通信时使用了另一份映射,也可能映射相同但入站过滤不允许 B。NAT 穿透需要同时处理这两个问题,并通过实际连通性检查确认候选路径。

机制提供什么不保证什么
本地端口分配区分 socket/传输端点所有连接全局独占一个端口号
NAT 映射内外地址与端口的对应状态任意外部来源都可入站
STUN观察映射及构建连通性检查自动给所有对端打开路径
TURN分配并维护中继承载中继永远可达、无资源或认证失败
ICE收集候选、检查候选对并提名直接测量并选出全网最低 RTT 路径

端口容量:先说明哪些字段固定

TCP 连接通常由源/目的地址和端口在相应网络上下文中区分。同一本地端口可能用于连接不同远端,因此“有 28232 个临时端口,就只能总共建立 28232 条 TCP 连接”不成立。显式 bind、不同 socket 用法、目标集中程度与 NAT 分配策略会改变复用条件。Cloudflare:临时端口与连接四元组实验

若临时范围是 32768~60999,含两端共有 28232 个号。对固定源 IP、固定目的 IP:port,暂不考虑保留端口、并发存活和安全复用,若每次短连接让四元组占用约 60 s,则量级估算为 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 保留的映射与过滤状态仍可能占资源、超时或被回收,二者不能混为一个计时器。

映射与过滤:两条独立分类轴

映射与过滤分别回答两个问题 映射:出去时用哪个外部端点 同一本地 socket → 不同目的 过滤:哪些外部来源可以进来 外部端点已存在 → 是否准入 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依赖外部目的 IPS2/S3 可复用,S1 不属同组
Address and Port-Dependent Mapping,APDM依赖目的 IP 与端口三个目的分别处理

分类描述映射的依赖关系,不要求每次必然分配一个从未出现过的外部端口,更不表示 APDM 端口一定随机且不可预测。端口保留、冲突处理与映射分类是不同维度。RFC 4787:UDP NAT 映射

过滤行为已有映射时允许哪些外部来源
Endpoint-Independent Filtering不因此前联系的外部端点限制来源
Address-Dependent Filtering来源 IP 需符合此前出站通信建立的条件
Address and Port-Dependent Filtering来源 IP 和端口都需符合相应条件

EIM 可以和严格过滤同时存在;旧式 Cone/Symmetric 标签把多个行为压在一个名字里,不适合精确诊断。也不能仅凭“家用”或“移动网络”标签就断言行为和成功率。RFC 4787:过滤

相同公网端口,为什么回包仍可能被挡?

先只联系 A:3478,切换过滤轴观察另外两个来源的准入。再联系 B:3478,切换映射轴观察是否新建外部端点。每行的允许列表只对应那一行映射;不能拿另一映射上的通信历史替它放行。

正在呈现知识画面
相同公网端口,为什么回包仍可能被挡?

映射回答外部端点怎样复用,过滤回答哪些来源可以进入已有映射。它们不是同一条轴;STUN观察到映射也不证明任意对端能进来。

STUN:服务器告诉你它看到的来源

Binding Request 经过 NAT 后,STUN 服务器看到转换后的源端点,在响应的 XOR-MAPPED-ADDRESS 中返回。STUN 固定头为 20 B,含消息类型、属性区长度、magic cookie 和事务 ID;后续属性有类型、长度及对齐规则。XOR 是编码方式,不是对地址保密的加密。RFC 8489:STUN

对于 IPv4 XOR-MAPPED-ADDRESS,端口与 cookie 高 16 bit 异或,地址与完整 32 bit cookie 异或;IPv6 的掩码还结合事务 ID。末尾代码会验证本例 203.0.113.10:62000 的往返编解码。

得到的是服务器在这次事务中看到的地址⁠。若映射依赖目的,发往 B 的外部端点可能不同;若状态超时,旧地址也可能失效。测试必须使用实际通信所需的 socket 和上下文,不能随便开另一个端口探测后拿结果替代原 socket。

UDP 打洞为何要双方配合

A、B 通过应用自己的信令通道交换候选端点,并分别向对方候选发送检查包。出站包可能建立相应映射及过滤许可,后续对端包才有机会通过。首次检查丢弃并不必然失败,重试、时序和状态寿命都有关。

STUN 不负责替应用实现这条信令通道。打洞也不是“STUN 服务器转发后续全部数据”;直连建立后,应用数据可直接在两端之间流动。多层 NAT、不同映射行为、hairpin 支持和出站 UDP 限制都会影响结果。

TURN:明确区分中继两侧的协议

客户端先通过 Allocate 获取中继地址并维护 allocation,再为对端建立相应 permission。发送可用 Send Indication,建立 channel binding 后也可使用更紧凑的 ChannelData;它们不是服务器向普通对端原样转发的应用载荷。RFC 8656:TURN

TURN:客户端侧是 TURN 封装,对端侧是中继 UDP 客户端 A 已建立 allocation TURN 服务器 R relay 地址 R:60000 对端 B 普通 UDP 对端 Send Indication / ChannelData UDP:源 R:60000,目的 B UDP:源 B,目的 R:60000 Data Indication / ChannelData 需有相应 permission;ChannelData 还需 channel binding。TURN 不保证目标、认证与网络永远可用。

B 向 relay 地址发普通 UDP,服务器按 permission 等条件接收,再通过 Data Indication 或 ChannelData 给 A。Data Indication 属于 TURN 服务器到客户端的一侧,不能放到普通 UDP 对端的一侧。客户端到 TURN 可以选择 UDP、TCP、TLS 等支持的承载,但不意味着普通 relay 对端也自动改用相同协议。

allocation、permission、channel binding 有各自寿命与刷新规则。凭据、配额、服务器资源和网络策略都会导致中继失败,因此 TURN 提供备用承载而非“永远能通”。TLS 到中继只保护对应链路;业务需要端到端保密时仍依赖相应端到端加密。

延迟与带宽怎样预算

假设单向 A→R=15 ms、R→B=20 ms,忽略排队和处理,则该方向中继路径约 35 ms;若反向恰好对称,RTT 约 70 ms。不能拿两个 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 角色通过 nomination 确定使用的对,controlled 角色配合,角色冲突也有处理规则。

候选对不只存在 host↔host、srflx↔srflx、relay↔relay 三种整齐组合。排序、检查调度、触发检查和提名构成完整流程,不能画成“所有直连完全失败之后才开始考虑任何中继”的固定串行阶梯。应用也可出于隐私或网络策略限制候选类型。

Trickle ICE 允许增量传递候选,让收集与检查重叠进行;不会免去认证、检查或提名。网络改变后还需相应重启或持续性机制,初次成功不保证候选永久可用。RFC 8838:Trickle ICE

Tailscale 作为实现案例

Tailscale 有直连与中继路径,官方文档还区分 DERP 和 peer relay。系统可先使用可用中继维持连接,同时尝试更合适的直连;中继通信和直连发现可以重叠进行。

DERP 也不是 TURN 的另一个名字;具体路径建立、WireGuard 加密承载和中继服务应按产品文档理解。随机端口分配会增加探测难度,但“随机就数学上绝不可能命中”同样不成立;不应靠无限端口试探代替可靠的中继设计。Tailscale 连接类型

诊断顺序:定位状态在哪一层丢失

已知现象优先核对
本地 socket 建立失败端口、绑定方式、FD/内存及错误码
STUN 无响应服务器可达、事务匹配、协议与过滤
有 srflx 但直连失败相同 socket、目的相关映射、双向过滤、状态寿命
TURN allocation 成功但无数据permission、channel、relay 地址和对端路径
ICE checks 成功但应用无数据提名结果、应用映射、加密状态和后续路径变化

IPv6 可减少 IPv4 地址转换需求,但有全局地址不代表防火墙允许入站;ICE 仍可用于选择和验证路径。端口映射工具则是在可管理的网关上明确请求映射,和观察 NAT 行为的 STUN 是不同做法。

本地算例:XOR 地址与容量

from ipaddress import IPv4Address
import struct

cookie = 0x2112A442
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 状态机;未发起打洞、端口扫描、中继分配或网关配置。

继续查阅

  • TCP:四元组、TIME_WAIT 与连接生命周期。
  • UDP 与 QUIC:数据报、迁移与路径验证。
  • WebRTC:ICE 与媒体协商的结合。
  • Tailscale 架构:控制、直连和中继的分工。