这是一个用于学习UDP内网穿透原理的教学项目,实现了简化版的STUN和TURN协议,以及NAT类型探测功能。通过这个项目,你可以深入理解NAT穿透的底层机制。
NAT是一种在IPv4地址短缺背景下广泛使用的技术,它允许多个内网设备共享一个公网IP地址。但NAT也带来了P2P通信的挑战,因为外部无法直接访问NAT后的设备。
graph TD
A[NAT类型] --> B[开放互联网<br/>Open Internet]
A --> C[完全锥形<br/>Full Cone]
A --> D[限制锥形<br/>Restricted Cone]
A --> E[端口限制锥形<br/>Port Restricted]
A --> F[对称型<br/>Symmetric]
B --> B1[无NAT<br/>直接P2P]
C --> C1[最容易穿透<br/>映射固定]
D --> D1[需要IP白名单<br/>端口任意]
E --> E1[需要IP:Port白名单<br/>最常见]
F --> F1[最难穿透<br/>映射动态变化]
graph TB
subgraph "内网A"
ClientA[客户端A<br/>192.168.1.100:5000]
RouterA[NAT路由器A<br/>内: 192.168.1.1<br/>外: 203.0.113.1]
end
subgraph "公网"
STUN[STUN服务器<br/>198.51.100.1:3478<br/>198.51.100.1:3479]
TURN[TURN服务器<br/>198.51.100.1:3480]
Signal[信令服务器<br/>可选]
end
subgraph "内网B"
ClientB[客户端B<br/>10.0.0.50:6000]
RouterB[NAT路由器B<br/>内: 10.0.0.1<br/>外: 203.0.113.2]
end
ClientA -.->|1. STUN Binding| STUN
ClientB -.->|1. STUN Binding| STUN
ClientA -->|2. 获取公网地址| RouterA
ClientB -->|2. 获取公网地址| RouterB
ClientA -.->|3. 交换地址信息| Signal
ClientB -.->|3. 交换地址信息| Signal
ClientA ===>|4. UDP打洞| RouterA
ClientB ===>|4. UDP打洞| RouterB
RouterA <==>|5. P2P连接| RouterB
ClientA -.->|6. TURN中继<br/>如果P2P失败| TURN
ClientB -.->|6. TURN中继<br/>如果P2P失败| TURN
sequenceDiagram
participant C as 客户端
participant S1 as STUN主服务器<br/>(IP1:Port1)
participant S2 as STUN备服务器<br/>(IP2:Port2)
Note over C,S2: Test 1: 基础连通性测试
C->>S1: BINDING_REQUEST
S1->>C: BINDING_RESPONSE<br/>(返回客户端公网地址)
alt 本地地址 == 映射地址
Note over C: 开放互联网(无NAT)
else 地址不同
Note over C: 存在NAT,继续测试
end
Note over C,S2: Test 2: Full Cone测试
C->>S1: TEST_REQUEST<br/>(change IP & Port)
S2-->>C: TEST_RESPONSE<br/>(从不同IP和端口响应)
alt 收到响应
Note over C: Full Cone NAT
else 未收到响应
Note over C,S2: Test 3: Restricted Cone测试
C->>S1: TEST_REQUEST<br/>(change IP only)
S2-->>C: TEST_RESPONSE<br/>(从不同IP响应)
alt 收到响应
Note over C: Restricted Cone NAT
else 未收到响应
Note over C,S2: Test 4: 对称型测试
C->>S2: BINDING_REQUEST<br/>(连接备用服务器)
S2->>C: BINDING_RESPONSE<br/>(返回新映射)
alt 映射端口改变
Note over C: Symmetric NAT
else 映射端口不变
Note over C: Port Restricted NAT
end
end
end
sequenceDiagram
participant A as Client A<br/>NAT-A后
participant NA as NAT-A<br/>203.0.113.1:45678
participant NB as NAT-B<br/>203.0.113.2:56789
participant B as Client B<br/>NAT-B后
participant S as STUN/Signal
Note over A,B: 阶段1: 地址发现
A->>S: 获取公网地址
S->>A: 203.0.113.1:45678
B->>S: 获取公网地址
S->>B: 203.0.113.2:56789
Note over A,B: 阶段2: 地址交换
A->>S: 我的地址是 203.0.113.1:45678
B->>S: 我的地址是 203.0.113.2:56789
S->>A: B的地址是 203.0.113.2:56789
S->>B: A的地址是 203.0.113.1:45678
Note over A,B: 阶段3: 打洞
A->>NA: 发送到 203.0.113.2:56789
NA->>NB: UDP包(可能被丢弃)
Note over NA: NAT-A创建映射规则:<br/>允许从203.0.113.2:56789接收
B->>NB: 发送到 203.0.113.1:45678
NB->>NA: UDP包(可能被丢弃)
Note over NB: NAT-B创建映射规则:<br/>允许从203.0.113.1:45678接收
Note over A,B: 阶段4: 建立连接
A->>NA: 再次发送
NA->>NB: UDP包
NB->>B: 转发(规则已存在)
B->>NB: 回复
NB->>NA: UDP包
NA->>A: 转发(规则已存在)
Note over A,B: P2P连接建立成功!
udp_nat_traversal.cpp
├── 协议定义
│ ├── MessageType # STUN/TURN消息类型
│ ├── NATType # NAT类型枚举
│ ├── AttributeType # 属性类型
│ └── 数据结构 # 消息头、属性等
│
├── STUN服务器 (STUNServer)
│ ├── 双套接字监听 # 主地址 + 备用地址
│ ├── BINDING请求处理 # 返回客户端公网地址
│ └── NAT类型测试支持 # CHANGE-REQUEST处理
│
├── TURN服务器 (TURNServer)
│ ├── 分配管理 # ALLOCATE请求处理
│ ├── 中继转发 # SEND/DATA指示处理
│ └── 权限控制 # 简化的权限管理
│
└── NAT检测客户端 (NATTypeDetector)
├── 连通性测试 # 基础STUN绑定
├── NAT类型判断 # 4步测试流程
└── 穿透建议 # 根据类型给出建议
g++ -std=c++11 udp_nat_traversal.cpp -lpthread -o nat_traversal./nat_traversal
选择: 1启动STUN服务器(端口3478, 3479)和TURN服务器(端口3480)
./nat_traversal
选择: 2
输入STUN服务器IP和端口./nat_traversal
选择: 3- 在本机启动服务器模式
- 开新终端运行NAT检测,连接127.0.0.1:3478
- 观察输出的NAT类型(应该是Open Internet)
- 在一台机器上启动服务器
- 在另一台机器运行NAT检测
- 观察不同网络环境下的NAT类型
- 在公网VPS上部署服务器
- 在家庭/办公网络运行检测
- 测试不同运营商的NAT类型
当内网客户端创建UDP socket并发送数据包到公网时:
// 客户端绑定本地地址 192.168.1.100:5000
bind(sock, local_addr);
// 发送到公网服务器
sendto(sock, data, server_addr);
// NAT设备创建映射表项:
// 内网 192.168.1.100:5000 <-> 公网 203.0.113.1:45678不同NAT类型的映射行为:
- Full Cone: 一对一固定映射,任何外部主机都可以通过公网地址访问
- Restricted Cone: 需要内网先发送过数据包到外部IP
- Port Restricted: 需要内网先发送过数据包到外部IP:Port
- Symmetric: 每个目标地址使用不同的映射
成功的打洞需要精确的时序控制:
// 双方几乎同时发送
TimeA: ClientA -> NAT_A -> [创建规则] -> NAT_B (可能被丢弃)
TimeB: ClientB -> NAT_B -> [创建规则] -> NAT_A (可能被丢弃)
// 规则创建后,后续包可以通过
TimeC: ClientA -> NAT_A -> NAT_B -> ClientB ✓
TimeD: ClientB -> NAT_B -> NAT_A -> ClientA ✓当P2P打洞失败时,使用TURN服务器中继:
// 客户端A分配中继地址
TURN_Server.allocate() -> RelayAddress_A
// 客户端B分配中继地址
TURN_Server.allocate() -> RelayAddress_B
// A发送数据到B的中继地址
A -> TURN -> RelayAddress_B -> B
// B发送数据到A的中继地址
B -> TURN -> RelayAddress_A -> A// 定期发送心跳包维持NAT映射
while (connected) {
sendto(sock, "HEARTBEAT", peer_addr);
sleep(30); // 30秒心跳
}- 同时尝试多个端口
- 使用端口预测算法
- 实施快速重试机制
尝试P2P -> 失败 -> 尝试打洞 -> 失败 -> 使用TURN中继
- 视频会议: WebRTC使用ICE框架(包含STUN/TURN)
- 在线游戏: 低延迟P2P连接
- 文件传输: P2P文件共享
- IoT设备: 远程访问内网设备
- VoIP电话: SIP协议配合STUN/TURN
A: 用于完整的NAT类型检测。通过从不同IP和端口发送响应,可以准确判断NAT的过滤行为。
A: 取决于NAT类型组合:
- Cone to Cone: >90%
- Cone to Symmetric: ~50%
- Symmetric to Symmetric: <10%
A:
- 使用多个端口同时尝试
- 实现端口预测算法
- 优化打洞时序
- 准备TURN服务器作为后备
A: 理论上不需要(地址充足),但实际中仍可能存在防火墙需要穿透。
- ICE (Interactive Connectivity Establishment): 完整的NAT穿透框架
- WebRTC: 浏览器中的P2P通信标准
- QUIC协议: Google的基于UDP的传输协议
- WireGuard: 现代VPN协议的NAT穿透实现
MIT License - 仅供学习使用