跳到主要内容

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 与 ICMP 和 IP 协议详解。