跳到主要内容

NAT 网络地址转换

网络地址转换(Network Address Translation,简称 NAT)是现代互联网基础设施中不可或缺的技术。它解决了 IPv4 地址短缺的问题,同时也为网络提供了一层安全保护。理解 NAT 的工作原理对于网络编程、P2P 应用开发、游戏联机等场景至关重要。

为什么需要 NAT

IPv4 地址空间只有约 43 亿个地址,远不足以满足全球互联网设备的需求。NAT 的核心思想是:让多台设备共享一个或少数几个公网 IP 地址。

私有地址与公网地址

RFC 1918 定义了私有 IP 地址范围,这些地址只能在局域网内部使用,不能直接在公网上路由:

类别地址范围可用地址数
A 类10.0.0.0 ~ 10.255.255.255约 1677 万
B 类172.16.0.0 ~ 172.31.255.255约 104 万
C 类192.168.0.0 ~ 192.168.255.255约 6.5 万

NAT 设备(通常是路由器)位于私有网络和公网的边界,负责在两者之间转换 IP 地址。

NAT 的核心工作原理

NAT 的基本操作是修改数据包的 IP 地址和端口号。当一个来自内网的数据包经过 NAT 设备时:

  1. NAT 将源 IP 地址从私有地址替换为公网地址
  2. NAT 将源端口替换为一个新的端口(如果需要)
  3. NAT 在转换表中记录这个映射关系
  4. 当响应数据包返回时,NAT 根据转换表反向转换

NAT 的类型

根据 RFC 3022 的定义,NAT 主要分为两大类:Basic NAT 和 NAPT。在实际应用中,NAPT 又根据映射规则的不同分为四种行为类型。

Basic NAT(基础地址转换)

Basic NAT 只转换 IP 地址,不转换端口号。它需要一个公网 IP 地址池,每个内网 IP 映射到一个公网 IP。这种方式节省地址的效果有限,现在已较少使用。

工作方式

  • 内网主机 192.168.1.10 发出数据包
  • NAT 将源 IP 替换为公网 IP 203.0.113.10
  • 端口号保持不变
  • 响应数据包返回时,根据 IP 映射表反向转换

NAPT(网络地址端口转换)

NAPT(Network Address Port Translation)同时转换 IP 地址和端口号,是现在最常用的 NAT 类型。它可以让成千上万的内网设备共享一个公网 IP 地址。

NAPT 也称为 PAT(Port Address Translation)或 IP 伪装(IP Masquerade)。

四种 NAT 行为类型

根据 RFC 3489 的定义,NAPT 根据映射规则的不同,可以分为四种行为类型。这些类型决定了外部主机能否主动向内网主机发送数据,直接影响 P2P 应用的穿透难度。

1. Full Cone NAT(全锥形 NAT)

全锥形 NAT 是最宽松的类型。一旦内网主机创建了映射,任何外部主机都可以通过这个映射向内网主机发送数据。

规则

  • 同一个内网 IP:端口 总是映射到同一个公网 IP:端口
  • 任何外部主机只要知道这个公网映射,就可以发送数据进来
内网主机 (192.168.1.10:5000) 发送数据到任意外部服务器后:
→ 映射为 (203.0.113.1:20001)
→ 任何外部主机都可以向 203.0.113.1:20001 发送数据
→ 数据会被转发给 192.168.1.10:5000

这是最理想的 NAT 类型,对 P2P 应用最友好,但也意味着安全性相对较低。

2. Address Restricted Cone NAT(地址受限锥形 NAT)

地址受限锥形 NAT 增加了一个限制:只有内网主机曾经发送过数据的外部 IP 地址,才能向这个映射发送数据。

规则

  • 同一个内网 IP:端口 总是映射到同一个公网 IP:端口
  • 只有内网主机曾经向其发送过数据的外部 IP,才能向这个映射发送数据
  • 外部主机的端口号不限
内网主机 (192.168.1.10:5000) 发送数据到服务器 A (93.184.216.34):
→ 映射为 (203.0.113.1:20001)
→ 服务器 A 的任何端口都可以向这个映射发送数据
→ 其他 IP 地址的服务器发送的数据会被丢弃

3. Port Restricted Cone NAT(端口受限锥形 NAT)

端口受限锥形 NAT 进一步收紧限制:只有内网主机曾经发送过数据的外部 IP:端口,才能向这个映射发送数据。

规则

  • 同一个内网 IP:端口 总是映射到同一个公网 IP:端口
  • 只有内网主机曾经向其发送过数据的外部 IP:端口,才能向这个映射发送数据
内网主机 (192.168.1.10:5000) 发送数据到服务器 A (93.184.216.34:80):
→ 映射为 (203.0.113.1:20001)
→ 只有 93.184.216.34:80 可以向这个映射发送数据
→ 即使是同一服务器的其他端口发送的数据也会被丢弃

4. Symmetric NAT(对称型 NAT)

对称型 NAT 是最严格的类型。内网主机向不同的目的地发送数据时,会创建不同的映射。

规则

  • 内网主机向不同的目标 IP:端口 发送数据时,会得到不同的公网映射
  • 只有数据包的目标 IP:端口 才能向对应的映射发送数据
内网主机 (192.168.1.10:5000):
→ 发送到服务器 A (93.184.216.34:80):映射为 (203.0.113.1:20001)
→ 发送到服务器 B (151.101.1.69:443):映射为 (203.0.113.1:20002)
→ 两个映射完全独立,互不相通

四种类型对比

类型映射规则外部入站限制穿透难度安全性
Full Cone固定映射无限制容易
Address Restricted固定映射IP 白名单较容易
Port Restricted固定映射IP:端口 白名单中等较高
Symmetric随目标变化严格匹配困难

从 Full Cone 到 Symmetric,限制逐渐增加,安全性提高,但 P2P 穿透的难度也随之增加。

NAT 穿透技术

当两台位于不同 NAT 后的主机需要直接通信时(如视频通话、P2P 文件传输、在线游戏),就需要进行 NAT 穿透。主流的穿透技术包括 STUN、TURN 和 ICE。

STUN(Session Traversal Utilities for NAT)

STUN 是一种轻量级协议,定义在 RFC 5389 中。它的核心功能是帮助客户端发现自己的公网 IP 地址和端口。

工作原理

  1. 客户端向 STUN 服务器发送请求
  2. STUN 服务器收到请求后,将客户端的公网 IP:端口 放入响应中返回
  3. 客户端从而得知自己的公网映射

STUN 不仅可以检测公网地址,还可以用来判断 NAT 的类型。通过向不同的 STUN 服务器发送请求并观察返回的映射是否相同,可以判断是否为 Symmetric NAT。

代码示例:使用 Python 进行 STUN 检测

import socket
import struct

def stun_request(stun_server, stun_port=3478):
"""
向 STUN 服务器发送请求并获取公网地址

RFC 5389 定义的 STUN Binding Request
"""
# 创建 UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(5)

# 构造 STUN Binding Request
# STUN 头部: 20 字节
# 类型 (2 字节) + 长度 (2 字节) + Magic Cookie (4 字节) + 事务 ID (12 字节)

msg_type = 0x0001 # Binding Request
msg_length = 0 # 无属性
magic_cookie = 0x2112A442 # RFC 5389 定义的魔数

# 生成随机事务 ID (12 字节)
import os
transaction_id = os.urandom(12)

# 打包头部
header = struct.pack('!HHI', msg_type, msg_length, magic_cookie) + transaction_id

try:
# 发送请求
sock.sendto(header, (stun_server, stun_port))

# 接收响应
data, addr = sock.recvfrom(1024)

# 解析响应
if len(data) < 20:
return None

# 解析头部
resp_type, resp_length, resp_cookie = struct.unpack('!HHI', data[:8])
resp_transaction = data[8:20]

# 验证事务 ID
if resp_transaction != transaction_id:
return None

# 解析 XOR-MAPPED-ADDRESS 属性 (类型 0x0020)
offset = 20
while offset < len(data):
attr_type, attr_length = struct.unpack('!HH', data[offset:offset+4])

if attr_type == 0x0020: # XOR-MAPPED-ADDRESS
# 解析地址
addr_data = data[offset+4:offset+4+attr_length]
# 第一个字节保留,第二个字节是地址族 (1=IPv4, 2=IPv6)
family = addr_data[1]

if family == 1: # IPv4
# 端口和 IP 地址需要与 Magic Cookie 和事务 ID 进行 XOR
xport = struct.unpack('!H', addr_data[2:4])[0]
port = xport ^ (magic_cookie >> 16)

xip = struct.unpack('!I', addr_data[4:8])[0]
ip_int = xip ^ magic_cookie
ip = socket.inet_ntoa(struct.pack('!I', ip_int))

return (ip, port)

# 对齐到 4 字节边界
offset += 4 + ((attr_length + 3) // 4) * 4

except socket.timeout:
print("STUN 请求超时")
finally:
sock.close()

return None

# 使用示例
# 注意:需要使用真实的 STUN 服务器
# result = stun_request('stun.l.google.com', 19302)
# if result:
# print(f"公网地址: {result[0]}:{result[1]}")

TURN(Traversal Using Relays around NAT)

当 NAT 穿透失败时(例如双方都是 Symmetric NAT),TURN 服务器可以作为中继,转发双方的数据。TURN 定义在 RFC 5766 中。

工作原理

  1. 客户端向 TURN 服务器申请一个中继地址
  2. 其他客户端向这个中继地址发送数据
  3. TURN 服务器将数据转发给目标客户端

TURN 的缺点是所有数据都经过服务器中继,增加了延迟和服务器带宽成本。

ICE(Interactive Connectivity Establishment)

ICE 是一个综合框架,定义在 RFC 8445 中。它系统地尝试所有可能的连接方式,选择最优的路径。

ICE 的候选地址类型

类型说明优先级
Host本地网络接口的地址最高
Server Reflexive通过 STUN 获得的公网地址
Peer Reflexive连接检查中发现的公网地址
RelayedTURN 服务器的中继地址最低

ICE 工作流程

ICE 的核心优势在于它会自动尝试所有可能的连接组合,从最优的直连到最差的中继,确保总能找到一条可用的路径。

WebRTC 中的 ICE 实现

WebRTC 是 ICE 协议最典型的应用场景。以下是一个简化的示例:

// WebRTC 中的 ICE 配置
const iceConfig = {
iceServers: [
// STUN 服务器
{ urls: 'stun:stun.l.google.com:19302' },
// TURN 服务器
{
urls: 'turn:turn.example.com:3478',
username: 'user',
credential: 'pass'
}
]
};

// 创建 RTCPeerConnection
const pc = new RTCPeerConnection(iceConfig);

// 监听 ICE 候选
pc.onicecandidate = (event) => {
if (event.candidate) {
// 将候选发送给对端(通过信令服务器)
console.log('ICE 候选:', event.candidate);

// 候选类型
// host: 本地地址
// srflx: 服务器反射地址(通过 STUN 获得)
// relay: 中继地址(通过 TURN 获得)
}
};

// 监听 ICE 连接状态
pc.oniceconnectionstatechange = () => {
console.log('ICE 状态:', pc.iceConnectionState);
// 可能的状态: checking, connected, completed, failed, disconnected
};

NAT 的实际应用场景

家庭网络

典型的家庭网络拓扑:

互联网 (公网 IP: 203.0.113.1)

│ (NAT)

┌───┴───┐
│ 路由器 │
└───┬───┘
│ 192.168.1.1

┌───┴────────────────┐
│ 交换机 │
└───┬───┬───┬────────┘
│ │ │
┌───┘ ┌─┘ ┌─┘
│ │ │
手机 电脑 智能电视
192.168.1.x

家庭路由器通常使用 NAPT,让多台设备共享一个公网 IP。

企业网络

企业网络可能存在多层 NAT:

互联网

│ 企业 NAT (NAPT)

内部网络 (10.x.x.x)

│ 部门 NAT

办公网络 (192.168.x.x)

多层 NAT 增加了网络复杂性,但也提供了更好的安全隔离。

云服务

云服务器通常直接分配公网 IP,但容器服务(如 Kubernetes)内部大量使用 NAT:

  • Pod 网络使用私有地址
  • 通过 NAT 访问外部服务
  • Service 通过 DNAT 将流量分发到 Pod

NAT 带来的问题与解决方案

问题 1:P2P 通信困难

NAT 阻止了外部主机主动向内网发起连接,导致 P2P 应用(如视频通话、P2P 下载)难以建立直连。

解决方案:使用 ICE 框架,结合 STUN 和 TURN 进行穿透。

问题 2:服务器无法主动推送

位于 NAT 后的服务器无法被外部客户端直接访问。

解决方案

  • 端口映射(Port Forwarding):在 NAT 设备上配置静态映射
  • UPnP:应用自动请求 NAT 设备开放端口
  • 反向代理:通过公网代理服务器转发请求

问题 3:IP 地址追踪困难

由于 NAT 的存在,服务器只能看到公网 IP,无法识别具体的内网主机。

解决方案

  • 应用层记录用户标识
  • 使用 X-Forwarded-For 等头部传递原始 IP

问题 4:某些协议不兼容

FTP 等协议在数据连接中传递 IP 地址,会被 NAT 破坏。

解决方案

  • ALG(Application Layer Gateway):NAT 设备识别并修改应用层数据
  • 使用被动模式(PASV)等协议扩展

NAT 配置示例

Linux iptables NAT 配置

# 启用 IP 转发
echo 1 > /proc/sys/net/ipv4/ip_forward

# SNAT (源地址转换):让内网主机访问外网
# 将来自 192.168.1.0/24 网段的数据包源地址改为公网 IP
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j SNAT --to-source 203.0.113.1

# MASQUERADE:动态获取公网 IP(适用于拨号网络)
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o ppp0 -j MASQUERADE

# DNAT (目的地址转换):端口映射,让外网访问内网服务
# 将访问公网 IP 80 端口的流量转发到内网服务器
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to-destination 192.168.1.100:80

# 查看 NAT 规则
iptables -t nat -L -n -v

Nginx 反向代理与 NAT

当内网服务需要对外提供访问时,可以使用 Nginx 反向代理:

# 反向代理配置
server {
listen 80;
server_name api.example.com;

location / {
proxy_pass http://192.168.1.100:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

NAT 穿透检测工具

测试 NAT 类型

# 使用 stuntman-client 测试 NAT 类型
apt install stuntman-client
stunclient stun.l.google.com 19302

# 输出示例:
# NAT behavior: Port Restricted Cone
# External IP: 203.0.113.1
# External Port: 54321

Python NAT 类型检测

import socket
import stun

def detect_nat_type():
"""检测 NAT 类型"""
# 使用 pystun 库
nat_type, external_ip, external_port = stun.get_ip_info(
stun_host='stun.l.google.com',
stun_port=19302
)

print(f"NAT 类型: {nat_type}")
print(f"公网 IP: {external_ip}")
print(f"公网端口: {external_port}")

return nat_type

# NAT 类型枚举
# stun.Blocked: 无法访问 STUN 服务器
# stun.OpenInternet: 公网 IP,无 NAT
# stun.FullCone: 全锥形 NAT
# stun.RestrictedCone: 地址受限锥形 NAT
# stun.PortRestrictedCone: 端口受限锥形 NAT
# stun.Symmetric: 对称型 NAT
# stun.SymmetricUDPFirewall: 对称型 UDP 防火墙

NAT 与 IPv6

IPv6 提供了海量的地址空间,理论上不再需要 NAT。然而,NAT 在 IPv6 网络中仍然存在:

NAT66:IPv6 到 IPv6 的地址转换,主要用于:

  • 地址隐藏和安全隔离
  • 多宿主(Multi-homing)场景
  • 网络迁移和重组

NAT64:IPv6 到 IPv4 的转换,用于 IPv6 网络访问 IPv4 资源:

  • DNS64 将 IPv4 地址映射为 IPv6 地址
  • NAT64 网关进行协议转换

总结

NAT 是解决 IPv4 地址短缺的关键技术,在现代网络中无处不在。理解 NAT 的类型和工作原理对于网络编程和分布式系统开发至关重要:

  • Full Cone NAT 最宽松,穿透最容易,适合 P2P 应用
  • Symmetric NAT 最严格,穿透最困难,可能需要 TURN 中继
  • STUN 用于检测公网地址和 NAT 类型
  • TURN 作为中继,在直连失败时保证通信
  • ICE 综合框架,自动选择最优连接方式

在实际开发中,应优先使用 ICE 框架处理 NAT 穿透,确保应用在各种网络环境下都能正常工作。

[!TIP] 想要了解更多网络层协议?请看 ARP 与 ICMPIP 协议详解