一、STUN协议概述

STUN,全称是Session Traversal Utilities for NAT。说白了,它就是NAT穿透的侦察兵。

我刚开始做WebRTC的时候,以为只要两端都连上互联网,就能直接建立P2P连接。结果第一次联调就翻车了——两边都在公司内网,谁也连不上谁。后来我才明白,没有STUN,你连自己在NAT后面的位置都搞不清楚。

STUN的核心作用就三个:

  • 发现公网IP和端口——告诉你在NAT外面长什么样
  • 检测NAT类型——判断你处在哪种NAT后面
  • 保持NAT绑定——通过周期性心跳维持映射关系

STUN是一个轻量级的客户端-服务器协议。它基于UDP,设计上尽量简单。为什么用UDP?因为TCP在NAT穿透场景下太笨重了,握手延迟高,而且很多NAT对TCP的处理方式不一样。

关键认知:STUN不负责穿透,它只负责探测。真正的穿透工作是由ICE(Interactive Connectivity Establishment)来协调完成的。STUN只是ICE手中的一把尺子。

二、STUN消息结构

STUN消息结构非常简洁。我当年第一次看RFC 5389的时候,觉得这协议设计得真干净——没有复杂的握手,没有状态机,就是一个请求一个响应。

2.1 消息头格式

所有STUN消息都共享同一个20字节的头部:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0 0|     STUN Message Type     |         Message Length        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Magic Cookie                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                     Transaction ID (96 bits)                  |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

头部字段说明:

字段 长度 说明
Message Type 14位 高两位固定为0。区分请求、响应、指示等
Message Length 16位 不包括头部的消息体长度
Magic Cookie 32位 固定值0x2112A442,用于校验
Transaction ID 96位 唯一标识一次请求-响应交互

这里有个细节我想强调一下:Magic Cookie的值0x2112A442不是随便选的。它是RFC 5389引入的,用来区分老版STUN(RFC 3489)和新版STUN。我在一次排查问题时,发现某个老设备发的STUN消息没有这个Magic Cookie,结果解析全乱了。嗯,兼容性问题总是藏在最不起眼的地方。

2.2 属性结构

消息头后面跟着一系列TLV(Type-Length-Value)格式的属性:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Type                  |            Length             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Value (variable)                     ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

常见的STUN属性:

  • MAPPED-ADDRESS (0x0001)——服务器看到的客户端公网地址
  • XOR-MAPPED-ADDRESS (0x0020)——经过异或处理的映射地址,更安全
  • USERNAME (0x0006)——用于消息完整性校验的用户名
  • MESSAGE-INTEGRITY (0x0008)——HMAC-SHA1摘要,防止篡改
  • FINGERPRINT (0x8028)——CRC32校验,可选但推荐

避坑指南:我曾经在生产环境中发现,某些NAT设备会修改UDP负载中的IP地址。STUN的XOR-MAPPED-ADDRESS就是为了应对这种情况——通过异或操作让IP地址在传输中不可读,防止NAT误修改。

三、STUN请求/响应流程

STUN的交互流程非常简单。我画了一张图,你可以直观地看到整个过程:

STUN请求/响应流程 WebRTC客户端 NAT设备 STUN服务器 ① Binding Request (源IP: 192.168.1.10:54321) ② 经过NAT转换 (公网IP: 203.0.113.5:12345) ③ Binding Response (XOR-MAPPED-ADDRESS) ④ 响应返回客户端 (客户端得知公网地址) 结果 客户端公网地址: 203.0.113.5:12345 NAT类型: 锥形NAT / 对称NAT 图:STUN Binding请求/响应流程

流程其实就四步:

  1. 客户端发送Binding Request——客户端向STUN服务器发送一个UDP请求,里面包含自己的内网地址信息
  2. NAT进行地址转换——请求经过NAT时,源IP和端口被替换成公网地址
  3. 服务器返回Binding Response——STUN服务器收到请求后,把看到的公网地址(源IP+端口)封装在XOR-MAPPED-ADDRESS属性中返回
  4. 客户端解析响应——客户端拿到自己的公网地址,就知道自己在NAT外面的样子了

你可能会问:为什么客户端不直接查自己的网卡信息?因为内网地址在公网上不可路由。你想想看,如果两端都在192.168.1.x网段,它们怎么知道对方在公网上的真实地址?

核心要点:STUN服务器看到的源IP和端口,就是客户端在公网上的"身份"。这个信息会被ICE用来尝试建立P2P连接。

四、STUN的局限性

STUN不是万能的。我在项目中踩过不少坑,这里总结一下它的主要局限:

4.1 对对称NAT无能为力

这是STUN最大的痛点。对称NAT(Symmetric NAT)每次建立新连接时都会分配不同的公网端口。STUN探测到的地址只对当前连接有效,换一个目标地址就变了。

我曾经遇到一个客户,他们的网络环境就是对称NAT。STUN返回的地址看起来没问题,但ICE尝试连接时就是不通。折腾了两天,最后只能用TURN中继方案。

4.2 无法穿透防火墙

STUN只处理NAT,不处理防火墙。很多企业防火墙会深度检查UDP流量,发现不是预期的协议就直接丢弃。STUN包可能连服务器都到不了。

4.3 依赖公共STUN服务器

公共STUN服务器可能不稳定、有延迟,或者被运营商屏蔽。我建议在生产环境中自建STUN服务器集群,或者使用云服务商提供的STUN服务。

4.4 安全性问题

STUN本身没有加密机制。虽然MESSAGE-INTEGRITY提供了完整性校验,但无法防止中间人攻击。如果你的应用对安全性要求高,建议在DTLS之上运行STUN。

重要提醒:不要指望STUN解决所有NAT穿透问题。它只是一个探测工具。真正的穿透策略需要结合ICE、TURN、甚至UPnP等多种技术。我见过太多团队把STUN当成万能药,结果上线后问题频发。

4.5 性能开销

每次STUN交互都会增加连接建立的延迟。在移动网络环境下,UDP包可能被重传多次,导致延迟飙升。我记得有一次在3G网络下测试,STUN请求平均耗时超过2秒,用户体验非常差。

总结一下STUN的适用场景:

场景 STUN效果 建议
锥形NAT(Full Cone / Restricted Cone) ✅ 有效 直接使用STUN
端口限制锥形NAT ⚠️ 部分有效 需要配合ICE
对称NAT ❌ 无效 必须使用TURN
防火墙后 ❌ 可能无效 尝试TURN或HTTP代理

嗯,STUN就讲到这里。它虽然简单,但却是WebRTC网络穿透的基石。理解STUN的工作原理,你就能更好地理解ICE为什么需要收集那么多候选地址——因为不是所有NAT都那么好说话。


公众号:蓝海资料掘金营,微信deep3321