本期我们就来讲解NAT_代理
目录
[为什么需要 NAT?](#为什么需要 NAT?)
[NAT 映射表的生命周期管理](#NAT 映射表的生命周期管理)
[为什么需要 NAPT?](#为什么需要 NAPT?)
[NAPT 核心内容](#NAPT 核心内容)
[工作流程详解(以外出 TCP 为例)](#工作流程详解(以外出 TCP 为例))
[端口分配策略与 NAT 分类](#端口分配策略与 NAT 分类)
[NAPT 对传输层和应用层的冲击](#NAPT 对传输层和应用层的冲击)
[TCP 和 UDP 的连接跟踪难题](#TCP 和 UDP 的连接跟踪难题)
[IP 分片与校验和重算](#IP 分片与校验和重算)
[UDP 穿洞的详细原理与流程](#UDP 穿洞的详细原理与流程)
[为什么穿洞对对称型 NAT 无效?](#为什么穿洞对对称型 NAT 无效?)
[TCP 穿洞的可行性](#TCP 穿洞的可行性)
NAT技术介绍
为什么需要 NAT?
1. IPv4 地址空间枯竭
IPv4 仅提供 32 位地址,理论最大 43 亿个。早在 1990 年代初,互联网增长就显示出地址短缺危机。虽然 CIDR(无类别域间路由)显著延缓了消耗速度,但无法从根本上增加地址数目。
2. 临时解决思路:私有地址
RFC 1918 划出三段地址供内部网络私用,这些地址在公网上不可路由:
-
10.0.0.0/8 (约 1677 万个地址)
-
172.16.0.0/12 (约 104 万个地址)
-
192.168.0.0/16 (约 6.5 万个地址)
任何组织都可以在内部自由使用这些地址,无需向注册机构申请。但这也带来问题:私有地址的数据包无法直接穿越公网路由器。
3. NAT 的出现
NAT 在边界路由器(或防火墙)上,将内部私有地址在数据包通过时转换为合法的公网地址,从而使内部主机能够访问公网。随着时间推移,NAT 从一种临时过渡方案,演变为一种安全边界(尽管并非真正安全),被大规模部署。
NAT与IP的转换
NAT 技术的核心在于在网络边界处对 IP 数据包的网络层及传输层头进行动态改写,从而将私有地址空间与公网地址空间连接起来,是一个基于状态的、精确的多字段修改与逆向还原的流水线。
转换的本质:基于"映射表"的有状态改写
NAT 设备(路由器、防火墙)维护一张网络地址转换表,每条表项记录了内部私有地址和外部公网地址的绑定关系。根据转换的粒度不同,可分为三种模式:
| 模式 | 转换维度 | 表项内容示例 |
|---|---|---|
| 静态 NAT | 私有 IP ↔ 公网 IP(一对一) | 192.168.1.10 ↔ 203.0.113.10 |
| 动态 NAT | 私有 IP ↔ 公网 IP 池(动态占用) | 192.168.1.10 ↔ 203.0.113.5 |
| NAPT (PAT) | 私有 IP:Port ↔ 公网 IP:Port(多对一复用) | 192.168.1.10:5000 ↔ 203.0.113.1:32001 |
最普适的 NAPT 转换表项实际为五元组绑定 :
{协议, 内网源IP, 内网源端口, 转换后的公网端口, 外网目的IP, 外网目的端口}
转化流程:出站与入站的双向修改
以最常见的 NAPT 为例,假设内网主机 192.168.1.2:5000 访问公网服务器 8.8.8.8:80(协议 TCP),NAT 公网 IP 为 203.0.113.1。
1. 出站包转换(私有 → 公网)
步骤 1:包到达 NAT 内部接口
源IP:192.168.1.2 \| 源端口:5000 \| 目的IP:8.8.8.8 \| 目的端口:80
步骤 2:查映射表
NAT 设备使用 {协议, 源IP, 源端口} 作为主键查询已存在的映射。
-
若有匹配条目(同一主机同一端口之前已建立映射),则直接使用对应的公网端口。
-
若首次出现,则从端口池中分配一个未使用的公网端口(例如 32001),并创建新表项。
步骤 3:重写 IP 头
-
源 IP 地址字段:
192.168.1.2→203.0.113.1 -
IP 头校验和:必须重新计算,因为源 IP 改变破坏了原来的校验和。
步骤 4:重写 TCP 头(传输层)
-
源端口:
5000→32001 -
TCP 校验和:也必须重新计算,因为源端口改变了,并且由于 IP 层的源地址变化,伪头部(包括源/目的 IP)也跟着变化。
步骤 5:转发
包变为:
源IP:203.0.113.1 \| 源端口:32001 \| 目的IP:8.8.8.8 \| 目的端口:80
然后从外部接口发出。
2. 入站包转换(公网 → 私有)
当服务器回包时,包的目的地址为 203.0.113.1:32001,到达 NAT 外部接口。
步骤 1:查映射表(反向)
NAT 使用 {协议, 目的IP, 目的端口} 作为查询键,在映射表中寻找对应条目。
- 找到:
203.0.113.1:32001→192.168.1.2:5000(同时对目的 IP 8.8.8.8:80 进行匹配,取决于 NAT 类型)。
步骤 2:重写 IP 和 TCP 头
-
目的 IP:
203.0.113.1→192.168.1.2 -
目的端口:
32001→5000 -
重新计算 IP 头校验和与 TCP 校验和(伪头部中目的 IP 发生变化)。
步骤 3:向内网转发
包变为:
源IP:8.8.8.8 \| 源端口:80 \| 目的IP:192.168.1.2 \| 目的端口:5000
主机 192.168.1.2 完全无感知地址曾被转换过。
校验和重算的原理
这是 NAT 在修改包包头时必须完成的关键操作,也是实现中最容易出错的性能瓶颈。
-
IP 头校验和 :只校验 IP 头部,算法是简单的 16 位反码求和。由于只修改了源 IP,设备会执行增量更新 :
新校验和 = 旧校验和 - 旧源IP(每16位) + 新源IP(每16位),比完全重算快得多。
-
TCP/UDP 校验和 :覆盖伪头部 + 整个传输层段。伪头部包含源/目的 IP、协议号、传输层长度。只要源 IP 或端口变化,校验和就必须重构。高性能 NAT 用硬件卸载或高度优化的软件算法完成。
NAT 映射表的生命周期管理
映射条目的创建与删除直接影响通信的成败:
-
创建触发:对于 TCP,收到 SYN 包且无匹配条目时创建;对于 UDP,收到第一个出站包时创建。
-
老化与删除:
-
TCP:收到 FIN 或 RST 后进入 TIME_WAIT 状态,短时间后删除;若异常断连,依赖超时(通常 30 分钟)清除。
-
UDP:无连接,完全依赖空闲超时(典型 30-120 秒)。这也是 UDP 穿越 NAT 需要心跳保活的根本原因。
-
NAT 设备的端口分配策略和对映射条目的管理行为,直接决定了它是全圆锥、端口受限圆锥还是对称型 NAT。
NAT的缺点
-
破坏端到端原则
在网络中间引入状态,不再是由终端单独负责通信,导致网络透明性丧失。
-
公网无法主动访问内网(单向可达)
任何希望被外网访问的服务都必须依赖显式端口映射或额外协议(如 UPnP),极大增加了双向通信(P2P)的复杂度。
-
协议内嵌地址失效
应用层载荷中携带的内网 IP/端口 无法被 NAT 自动改写,迫使使用 ALG 强行修复或依赖 STUN/TURN/ICE 等穿越方案,引入额外开销。
-
连接状态脆弱,映射易老化
NAT 映射受超时器控制,UDP 无连接、TCP 长连接空闲时都可能被清除,应用必须保持心跳,增加实现复杂度。
-
P2P 穿透困难,对称型 NAT 需中继
不同类型的 NAT 行为(尤其对称型)使打洞不可靠,必须引入 TURN 中继,导致带宽成本升高、延迟增大。
-
安全是错觉
NAT 丢弃入向包的行为常被误作安全屏障,但实际并无策略管控,易被穿透,且多用户共用一个公网 IP 使审计、溯源困难。
-
开发与调试负担重
本地绑定地址不可对外通信,端口映射不稳定,排查连接问题须深刻理解 NAT 行为,徒增编码与运维成本。
NTPT
为什么需要 NAPT?
从静态 NAT 到动态 NAT 的局限
-
静态 NAT:一对一固定映射,完全无法节省公网地址。
-
动态 NAT:N 个私有地址共享 M 个公网地址(N > M),但同一时刻,一个公网 IP 只能分配给一个私有地址。当 M 不够时,后来的内网主机就无法上网。
这种"公网 IP 独占"的模型,仍旧要求一个组织拥有相当数量的公网地址,只适用于小型网络,解决不了家庭用户或多租户数据中心海量终端的并发需求。
地址复用需求的质变
随着家庭宽带、移动设备的普及,一个家庭网络可能同时有数十台设备上网,ISP 却只能提供一个公网 IP。此时需要一种技术能在传输层层面实现多路复用,让多个私有地址的不同连接可以共享同一个公网 IP,并且不会混淆彼此的数据流。
NAPT 应运而生
NAPT 利用传输层协议(TCP/UDP)的端口号 作为二级标识符,将映射从 (私有IP) 维度扩展为 (私有IP, 私有端口) ↔ (公网IP, 公网端口) 对。这样,一个公网 IP 理论上可容纳 65535 个 TCP 和 65535 个 UDP 并发连接,一举将地址复用效率提升到单台主机级别。
从此,一个公网 IP 能够带整个局域网的时代到来,NAPT 成为 IPv4 互联网的"默认中间盒"。
NAPT 核心内容
转换表结构:五元组绑定
NAPT 设备(通常是路由器/网关)维护一张扩展的转换表,每一条映射至少包含:
{ 协议, 内网源IP, 内网源端口, 公网替换端口, 外网目的IP, 外网目的端口 }
当收到从内到外的数据包时:
-
提取源 IP、源端口、目的 IP、目的端口、协议。
-
查表找相同的私有三元组(源IP、源端口、协议)及目的信息。
-
若不存在,分配一个新的公网源端口,创建条目。
-
将包的源 IP 改为网关的公网 IP,源端口改为分配的端口,修正 TCP/UDP 校验和,转发。
回包到达时,根据目的端口反向查找表项,还原为内网私有 IP:端口,向内网转发。
工作流程详解(以外出 TCP 为例)
内网主机 192.168.1.10:50000 请求 93.184.216.34:80(example.com):
-
包到达网关,内网源:
192.168.1.10:50000,目的93.184.216.34:80,协议 TCP。 -
网关查表,未命中,从空闲端口池中分配,假设公网端口
32001。 -
添加条目:
-
协议 TCP
-
内网源 192.168.1.10:50000
-
公网源 203.0.113.1:32001
-
目的 93.184.216.34:80
-
-
重写包:源 IP → 203.0.113.1,源端口 → 32001,重新计算 IP 和 TCP 校验和。
-
服务器回复的目的地址为
203.0.113.1:32001。网关收到后,匹配转换条目,回复包的目的地址重写为192.168.1.10:50000,向内网发出。
对于 UDP,过程相同,但 UDP 是无连接协议,路由器还要管理超时老化以回收端口。
端口分配策略与 NAT 分类
这是 NAPT 最影响 P2P 通信安全的方面。根据映射行为(端点依赖),NAPT 可分为:
-
全圆锥型 (Full Cone NAT)
映射
(内网IP:port)固定对应一个(公网IP:port)。任何外部主机 只要知道该公网 IP:port 就可以向内网主机发送数据包。穿越最容易,但安全性最低。
-
受限圆锥型 (Restricted Cone NAT)
仅允许"内部曾发过包的"外部 IP 发送回包。即内部主机必须先向对方 IP 发过一个包,之后对方来自该 IP 的任何源端口包才能通过。
-
端口受限圆锥型 (Port Restricted Cone NAT)
要求外部 IP 且源端口必须是内部曾发送的目的端口。映射更严格。
-
对称型 NAT (Symmetric NAT)
同一个内网 (IP:port),针对不同的外部 (目的IP:port) 会映射到不同的公网端口。内部主机每次和新端点通信,都会得到一个新的公网映射。这种特性使得 P2P 打洞几乎不可能(除非端口可预测),需要 TURN 中继。
NAT 的这四种行为,就是 NAPT 端口分配策略的直接体现。在编写 C++ P2P 库时,你需要通过连接 STUN 服务器来检测当前 NAPT 的类型,并据此选择连接策略。
NAPT 对传输层和应用层的冲击
TCP 和 UDP 的连接跟踪难题
-
UDP :无连接,NAPT 必须靠超时定时器 来管理映射。如果应用长时间不发包,映射可能老化删除,导致后续包丢失或重新映射成新端口。所以在 NAPT 后写 UDP 应用必须实现心跳保活,间隔必须小于映射超时(通常 30-120 秒)。
-
TCP :有连接建立和拆除的标志位(SYN, FIN, RST),NAPT 可据此敏捷地创建和删除映射。但如果 TCP 连接因异常关闭未发 FIN,或静默空闲太久,同样可能被中间 NAPT 清除。因此长连接应用仍需要 TCP keep-alive 或应用层心跳。
应用层协议的"内网地址泄漏"
NAPT 只改 IP 头和传输层头。许多应用协议(FTP, SIP, RTSP, P2P 协议)会在载荷里写上自己的内网 IP:port 用于后续数据连接。NAPT 看不到这些内容,导致对端收到不可达的内网地址。解决方案要么是 ALG(应用层网关) 在中间解析重写,要么引入 STUN/TURN/ICE 这类 NAT 穿越机制,由终端自行发现并补偿地址差异。对于加密流量,ALG 彻底失效,NAT 穿越成为唯一出路。
IP 分片与校验和重算
NAPT 修改了 IP 头(源 IP)和传输层伪头部(源端口),必须重新计算传输层校验和(TCP/UDP 包括伪头部)。如果数据包在中间分片,NAPT 设备还需处理分片重组或修改分片的端口信息,这是高性能网关的实现难点。
代理服务器
总体
| 属性 | 正向代理 (Forward Proxy) | 反向代理 (Reverse Proxy) |
|---|---|---|
| 服务对象 | 客户端(代表客户端发起请求) | 服务端(代表服务器接收请求) |
| 客户端感知 | 客户端明确配置代理地址,已知代理存在 | 客户端通常认为代理就是真实服务器,对代理无感知 |
| 典型部署位置 | 靠近客户端一侧(企业出口、用户PC) | 靠近服务器一侧(数据中心入口、云服务前) |
| 主要作用 | 隐藏客户端身份、突破访问限制、缓存与过滤 | 隐藏服务器拓扑、负载均衡、SSL 卸载、应用层防护 |
| IP 层视角 | 源 IP 是客户端 → 代理发出时源 IP 变为代理的 IP | 客户端看到的目的 IP 是代理 IP → 代理向后端发起时源 IP 为代理 IP |
正向代理
1. 典型工作流
-
客户端(如浏览器)配置正向代理的地址和端口。
-
客户端将 HTTP 请求(或通过 CONNECT 建立隧道)发往代理。
-
代理解析请求,根据自身规则(如 ACL、缓存)决定是否转发。
-
代理以自己的身份向目标服务器发起连接,源 IP 被替换为代理 IP。
-
代理收到响应后,将其返回给原客户端。
2. 关键技术细节
-
HTTP 请求格式变化 :普通请求中,目标 URI 是绝对路径;但发往正向代理时,客户端必须发送绝对 URI (如
GET http://example.com/index.html HTTP/1.1),以便代理知道目标服务器的地址。 -
CONNECT 隧道 :针对 HTTPS,客户端通过
CONNECT example.com:443建立 TCP 隧道;代理仅做流量透传,无法检查内容。这是正向代理实现 HTTPS 访问的基础。 -
身份隐藏:目标服务器日志记录的是正向代理的 IP,客户端真实 IP 被隐藏。
-
访问控制与内容缓存:企业常用正向代理过滤恶意网站或缓存常用资源。
3. C++ 编程中的体现
开发一个简单的正向代理,你需要:
cpp
// 侦听客户端连接
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
// ... bind, listen
// 接受后解析 HTTP 请求行,提取绝对 URI
std::string host, port, path;
parse_absolute_uri(request_line, host, port, path);
// 连接远程服务器
int remote_fd = connect_to(host, port);
// 如果方法为 CONNECT,则直接双向透传数据
if (method == "CONNECT") {
relay_data(client_fd, remote_fd); // 全双工转发
}
// 否则重新构造请求(将路径改回相对路径),发送并中继响应
反向代理
1. 典型工作流
-
客户端向 www.example.com 发起请求,DNS 解析到反向代理的 IP。
-
客户端认为自己直接与目标服务器通信,实际上目标服务器是代理。
-
反向代理根据请求的主机名(Host 头)、URL 路径等规则,选择一台后端服务器。
-
代理以后端服务器的 IP 发起新请求(源 IP 为代理的内网 IP)。
-
代理收到后端响应后,可能进行缓存、压缩或修改,然后返回客户端。
2. 关键技术细节
-
无客户端配置:客户端不需要任何设置,完全透明。
-
负载均衡:支持轮询、最少连接、哈希一致性等多种算法。
-
SSL 卸载:反向代理负责处理 HTTPS 加密和解密,后端服务器只需明文 HTTP,降低后端负载。
-
安全防护:隐藏在公网后,抵御 DDoS、WAF 规则过滤恶意请求。
-
请求改写 :添加
X-Forwarded-For头,将原始客户端 IP 传递给后端。
3. C++ 编程中的体现
实现一个简单反向代理(如基于 Nginx 模型):
cpp
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// 监听 80/443 端口
while (true) {
int client_fd = accept(server_fd, ...);
// 解析 HTTP 请求,获取 Host 和 URI
http_request req = parse_http(client_fd);
// 根据配置选择后端服务器(如轮询)
backend_server backend = select_backend(req.host, req.uri);
int backend_fd = connect_to(backend.ip, backend.port);
// 改写请求头:添加 X-Forwarded-For,修改 Host 等
req.headers["X-Forwarded-For"] = get_peer_ip(client_fd);
// 向后端发送改写后的请求
send_http_request(backend_fd, req);
// 接收后端响应,可选地修改响应头或体,然后中继给客户端
http_response resp = recv_http_response(backend_fd);
relay_response(client_fd, resp);
}
关键差异的深层理解
1. 透明性与方向
-
正向代理是客户端的代表,对于服务器透明;客户端必须配置代理。
-
反向代理是服务器的代表,对于客户端透明;客户端无需配置。
2. 连接建立目标
-
正向代理:客户端向代理发起连接,目标是外部任意服务器。
-
反向代理:客户端直接向代理发起连接,但实际上客户端期望访问的是代理背后的特定服务。
3. 地址管理
-
正向代理:替换源 IP,但目标 IP 保持原目标服务器。
-
反向代理:替换目的 IP(内部服务器 IP),但源 IP 可能被新的连接覆盖,并需要通过特殊头(如
X-Forwarded-For)传递原始客户端 IP。
4. 典型协议行为
-
正向代理 HTTP/1.1 要求绝对 URI;反向代理接受普通的绝对路径。
-
正向代理使用 CONNECT 隧道进行 HTTPS 透传;反向代理则直接终结 HTTPS,使用自己的证书。
常见应用场景对比
| 场景 | 正向代理 | 反向代理 |
|---|---|---|
| 访问外部资源 | 企业员工上网、爬虫代理池 | --- |
| 突破地理限制 | 用户通过代理访问封锁内容 | --- |
| 匿名访问 | 隐藏用户真实 IP | --- |
| 网站高可用 | --- | 负载均衡、HA |
| CDN 分发 | --- | 静态资源缓存、静态化加速 |
| 微服务网关 | --- | 统一入口,路由、认证、限流 |
| 应用安全 | --- | WAF、DDoS 防护、隐藏后端细节 |
内网穿透
内网穿透广义上指任何使得位于 NAT/防火墙后的主机能够被公网访问的技术总和。手段包括端口映射、反向代理、中继转发、隧道协议、专有穿透服务等。穿透本身不保证对等直连,往往需要公网中转服务器作为入口或中继。
核心原理:借助公网中间节点建立"回路"
所有内网穿透方案的核心思路都是:内网主机主动向外建立一个可达的公网锚点,然后将外部请求通过该锚点"反射"或"中继"到内网。 这个锚点可能是一个拥有公网 IP 的服务器,也可能是一个通过 NAT 穿越打通的直连通道。
根据锚点的角色,内网穿透大致可分为三类:
隧道代理模式(反向代理穿透)
这是最常见、最通用的模式,代表软件如 frp、ngrok、Cloudflare Tunnel。
原理:
-
内网 A 启动一个客户端代理程序(Agent),向公网上的穿透服务器(Broker) 发起一个 TCP 长连接(或 UDP + QUIC 等),并声明要暴露的本地服务(如
127.0.0.1:80)。 -
穿透服务器在公网监听一个端口(如
203.0.113.5:8080),当 B 请求该端口时,服务器并不直接处理,而是通过已经建立的隧道连接将请求数据转发给内网 Agent。 -
Agent 收到转发请求后,在本机向实际服务(
127.0.0.1:80)发起连接,然后将响应数据通过隧道回传给穿透服务器,最终送达 B。
本质 :
整个架构是一个反向代理,但后端的真实服务器在私有网络中,通过由内向外的长连接与代理保持联系。从 B 的视角看,似乎只是访问了一个公网 Web 服务,完全感知不到后端的真实位置。
关键技术点:
-
维持映射:Agent 的长连接充当了 NAT 映射的保持者。只要连接不断,NAPT 网关上的映射就存活,服务器就能随时向 Agent 推送请求。
-
多路复用:一个隧道连接通常承载多个并发请求流,需要类似 HTTP/2 或自定义流复用协议(如 yamux)来避免建立过多连接。
-
端口分配与虚拟主机:穿透服务器可根据客户端标识或请求的 Host 头将不同内网客户端的不同服务映射到不同的公网端口或同一个端口的虚拟主机。
中继转发模式(TURN 风格穿透)
适用于两个内网主机之间需要通信且无法直连的情况。
原理:
-
公网中继服务器(TURN 服务器)为双方转发所有数据。A 和 B 都主动连接到中继服务器,之后所有流量通过中继拷贝。
-
中继服务器分配中转地址,并负责将从一个内网节点收到的数据复制转发给另一个节点。
本质 :
纯粹的中继,无技巧打洞。缺点是中继带宽压力大、延迟增加,但可以保证任何 NAT 环境下的连通,是对称型 NAT 下最后的保底方案。
协议层网关(ALG / UPnP)自动映射
这不是通用方案,但在特定网络环境中可用:
-
UPnP / NAT-PMP:内网程序向支持该协议的路由器请求一个公网端口映射,路由器自动配置 NAT 转发规则。这相当于程序自己告诉路由器:"把公网端口 12345 转发到我的 192.168.1.10:80"。
-
ALG:路由器内置应用层网关,能识别并动态修改特定协议(如 FTP、SIP)的控制消息,自动打开数据通道。但这种方法受限于协议和路由器支持。
以上三种中,隧道代理和 UPnP 用于暴露服务 ,中继和穿洞则更侧重于对等通信(后文会区分穿洞与穿透)。
内网穿洞
内网穿洞是内网穿透的子集,是一种具体的 NAT 穿越技术
内网穿洞的原理就是:利用公网信令服务交换 NAT 后的公网地址,然后双方同时向对方的地址发送数据包,在各自的 NAT 设备上制造一个"我主动联系过对方"的假象,让 NAT 将随后对方发来的包误认为是回复,从而放行,建立直连通道。
穿洞是去中继化的关键步骤,尤其适用于 P2P 应用。典型流程以 UDP 穿洞为例:
穿洞要解决的根本矛盾
回顾 NAPT 的行为:当内部主机向外发送数据包时,NAT 设备会临时创建 (内网 IP:端口) ↔ (公网 IP:新端口) 的映射,并允许回包进入;但从外部主动发来的包,若没有对应映射,一律丢弃。
两个内网主机 A 和 B 无法直接通信,因为:
-
A 不知道 B 的 NAT 公网映射地址。
-
即使知道,B 的 NAT 也因为没有 B 主动发起的出站包而拒绝 A 的入站包。
穿洞的核心思路是利用一个公网信令服务器,交换双方的公网映射地址,然后双方同时向对方地址发送数据包,欺骗各自的 NAT 认为对方包是己方请求的响应,从而打开防火墙。
UDP 穿洞的详细原理与流程
UDP 是穿洞的最常用协议,因为它的无连接特性允许随意发送数据报而无需握手。
- 前提条件
-
NAT 类型必须支持打洞:全圆锥 、受限圆锥 、端口受限圆锥 均可打洞;对称型 NAT 几乎无法打洞(除非端口预测成功)。
-
需要一个公网可访问的信令服务器(通常使用 TCP 或 UDP 维持与客户端的通信,用来交换地址信息)。
-
双方都能主动向对方发送 UDP 包。
- 完整步骤
Step 1:收集映射地址
-
A 向公网 STUN 服务器发送绑定请求,STUN 服务器返回 A 在 NAT 上映射的公网 IP 和端口,记为
PubA:PortA。 -
B 同理,获得
PubB:PortB。
Step 2:信令交换
-
A 通过信令服务器将自己的公网地址
PubA:PortA告诉 B,并告知自己希望连接。 -
信令服务器把 B 的公网地址
PubB:PortB发给 A。 -
此时双方都已知道对方的"门牌号"。
Step 3:同步打洞
-
A 向
PubB:PortB发送一个 UDP 包(通常包含少量特征数据)。 -
几乎是同时,B 向
PubA:PortA发送一个 UDP 包。
包在 NAT 上的行为分析:
-
A 发出的包经过 A 的 NAT,NAT 为这条流记录:
(A私网IP:端口) → PubA:PortA → PubB:PortB的映射,并允许来自PubB:PortB的回包通过。 -
该包到达 B 的 NAT。如果 B 还未向 A 发出任何包,B 的 NAT 中不存在对应映射,会丢弃这个包(端口受限圆锥和受限圆锥会丢弃,全圆锥可能接收,但后续仍需双向确认)。
-
几乎同一时刻,B 发出的包到达 A 的 NAT。由于 A 此前已经向 B 发出过包,A 的 NAT 中存在从 B 方向回来的映射预期,因此这个包被允许通过,送达 A。
Step 4:连通确认
- A 收到 B 的打洞包后,就知道洞已打通。它再次向 B 发送一个确认包,这个包到达 B 的 NAT 时,恰好 B 也已经向 A 发过包,B 的 NAT 此时也认可 A 的入站包。双方正式连通,之后可以双向任意发送数据。
整个过程相当于两人同时走向对方的大楼,先各自朝对面楼丢一个沙包,沙包砸中对面楼的窗户时,保安看到是自己人丢回来的东西,便开门放行。一旦门开了,两边的人就可以自由互传。
为什么穿洞对对称型 NAT 无效?
对称型 NAT 为每一个不同的外部目的地 分配一个新的公网端口。当 A 用相同的内网端口向 STUN 服务器发包时,得到映射 PubA:PortA_stun;当 A 向 B 的公网地址 PubB:PortB 发打洞包时,A 的 NAT 会分配一个全新的 公网端口 PubA:PortA_hole。
关键问题是:信令服务器告诉 B 的 A 地址是 PubA:PortA_stun,B 按照这个地址打洞,打向 PubA:PortA_stun 的包。但 A 的 NAT 只为 PubA:PortA_stun 映射了与 STUN 服务器之间的流,只有当来自 STUN 服务器的包进来时才放行。B 的包到达 A 的 NAT 时,NAT 一看目标是 PortA_stun,但该端口对应的预期源地址是 STUN 服务器,而不是 B,因此直接丢弃。
所以,除非能通过某种方式预测对称型 NAT 的端口分配规律,否则穿洞必然失败,必须退回到中继(TURN)。
TCP 穿洞的可行性
TCP 的设计不直接支持打洞,但可以利用"TCP 同时打开"(Simultaneous Open)机制:
-
双方使用相同的本地端口调用
connect(),且彼此都已经知道对方的对公地址和端口。 -
操作系统发送 SYN 包,如果双方的 SYN 在对方的 SYN 到达之前发出,内核将进入
SYN_SENT状态;收到对方的 SYN 时,响应 SYN-ACK,从而完成握手。 -
这个流程高度依赖操作系统 TCP 栈的实现,且需要精确控制时序,且一旦一端 NAT 映射已分配,另一端的 SYN 可能触发 RST。工业级 P2P 系统很少依赖 TCP 打洞,普遍采用UDP 打洞 + 上层可靠传输(如 QUIC、µTP、KCP)的方案。
本期内容到这里就结束了,喜欢请点个赞谢谢
封面图自取:
