资讯详情

资讯详情

STUN协议实战:从NAT穿透原理到UDP打洞的完整解析

遇到两个明明都在公网环境下、却怎么都连不上的设备第一反应多半是防火墙、是配置、是网络延迟。但真正搞过P2P联机、搞过音视频通话的人都知道十有八九卡在NAT上——设备本身不在公网而是在某个路由器后面的一层“内网假象”里。STUN协议就是这个场景下最先会用到的工具它做的事情说起来很简单帮你问一台公网服务器“从外面看我的地址和端口长什么样”。但就这么一个简单的“照镜子”机制背后牵扯到NAT类型判断、UDP端口映射存活性、ICE候选收集等一系列问题。这篇文章把我实际调试STUN协议时摸过的底、踩过的坑、翻过的RFC都整理出来适合正在做WebRTC通话、P2P文件传输、局域网穿透联机或者单纯想搞懂“打洞到底怎么打”的人。1. STUN想要解决的根本问题两个设备为何互相“看不见”先从一个最常见的联机失败场景说起。A同学在家里电脑上开了一个监听端口比如UDP 5000B同学在网络另一头直接往A的“公网IP:5000”发数据。结果自然是石沉大海。A查了一下自己的IP发现是192.168.1.8这显然是个内网地址那A在公网上的真实地址是多少没人知道。这就是整个P2P通信最核心的困局设备知道自己内网地址却不知道自己在公网上的映射地址。1.1 一个典型的P2P失败场景拆解家里的路由器通常做的是NAPTNetwork Address Port Translation把内网设备的内网IP内网端口映射成公网侧的一个公网IP公网端口。A发起一条从192.168.1.8:5000到外部某个地址的UDP流量时路由器会在自己的NAT表里创建一条映射记录把源地址改写成例如221.12.3.4:30000。关键就在这里这个30000是路由器随机分配的A自己并不知道。B如果直接往221.12.3.4:30000发包理论上能到A的路由器但路由器一看NAT表发现没有对应的“出站会话”记录就直接把包丢弃了。因为NAT的工作逻辑本质上是以“出站方向建立的映射”为前提的只有内网设备先往外发包路由器才允许外网回来的包进来。B的探测包属于“无中生有的入站流量”自然会被拒之门外。1.2 NAT映射是“假身份证”但又是唯一能用的身份有人会问既然路由器会动态分配端口那能不能让A直接问路由器要一个固定端口家用路由器基本不会给你这种操作权限UPnP通用即插即用协议可以尝试自动做端口映射但很多路由器默认关闭而且UPnP在安全性上一直有争议。比UPnP更普适、更轻量的办法就是让设备自己去发现“映射后的地址”也就是STUN做的事情。STUN的思路很朴素公网上部署一台STUN服务器内网设备向它发一个UDP请求服务器收到后能看到这个请求的“源IP和源端口”——这个源地址恰好就是路由器做了NAPT之后的外网映射地址。服务器把这个观察到的地址放在响应里返回给客户端。客户端一看响应里的MAPPED-ADDRESS就知道自己在别人眼里长什么样了。1.3 STUN在协议栈中的位置STUN全称Session Traversal Utilities for NAT注意它名字里的是“Utilities工具”不是“Solution解决方案”。它本身不做数据转发只负责“探测”和“发现”。在实际的P2P通信架构里STUN通常只是第一步——拿到映射地址之后还需要ICEInteractive Connectivity Establishment去尝试连通性检测遇到STUN搞不定的NAT类型时还需要TURN做中继兜底。所以理解STUN要把它放在“打洞链路的前置探测环节”这个位置上不要指望它解决所有NAT问题。2. STUN的核心机制从外部视角看自己的映射关系STUN最核心的交互就是一对消息Binding Request和Binding Response。无论客户端处于什么网络环境只要它能路由到公网它就可以向STUN服务器发一个Binding Request服务器收到后把“我看到你的地址”封装在响应里返回。整个过程一般一次UDP往返就完成了开销极小。2.1 请求响应中的关键字段RFC 5389STUN的新版标准规定服务器返回的映射地址放在MAPPED-ADDRESS属性里但实际传输时用的多是XOR-MAPPED-ADDRESS。之所以要做XOR异或是为了防止某些中间设备把这些字段误当作DNS查询去处理。早些时候有设备看到UDP负载里的IP地址明文会自作主张去改写导致返回的地址错乱。异或之后看起来是乱码反而能“免疫”这种画蛇添足。具体异或算法不复杂把IP地址和端口跟一个固定的Magic Cookie0x2112A442做异或然后存到属性里。客户端拿到后用同样的Cookie异或回去就能还原出真实地址。2.2 为什么STUN选择UDP而不是TCPSTUN支持UDP、TCP、TLS三种传输方式但绝大多数NAT穿透场景下用的是UDP。原因有两方面。第一P2P音视频和游戏联机本身就在UDP上跑STUN用UDP可以保持和业务流量一致的路径特征NAT映射不会被别的一次TCP连接“带偏”。第二UDP是面向无连接的一个Binding Request发出后如果网络路径上有某个NAT设备做了奇怪的策略UDP的失败特征与真实业务流量一致更容易暴露问题。我在实际调试中发现有些商用网络会对TCP的某些端口做精细管控但UDP通常会宽松一些。STUN探测用UDP某种意义上也是在试探“UDP打洞这条路到底通不通”。如果UDP的STUN请求能正常收到响应说明这条路大概率可以走如果连STUN都失败后面做UDP穿透基本没戏。2.3 一个请求拿到两个地址的情况请求到了STUN服务器处理时还会有一个细节服务器从哪个本地地址收到请求响应的源地址也来自哪个本地地址。如果一台STUN服务器有多块网卡、多个公网IP它收到请求的网卡对应的地址和响应的源地址必须保持一致。有些客户端会同时把“响应的源地址”和“MAPPED-ADDRESS”拿出来做对比——前者代表“我发出去后在网络的哪个点被服务器接收”后者代表“我在服务器视角下显示的映射地址”。正常情况两者应该一致如果不一致说明中间还有一层额外的NAT或负载均衡设备信息量很大。3. 穿透的分水岭NAT类型与STUN的边界如果说STUN只是“照镜子”那NAT类型就决定了这面镜子照出来的是真相还是假象。业内通常把NAT分为四种类型按从宽松到严格的顺序排列Full Cone全锥型、Restricted Cone地址限制锥型、Port Restricted Cone端口限制锥型、Symmetric对称型。3.1 四种NAT行为模式详解Full Cone内网设备只要建立了映射外网任何主机都可以通过这个映射地址向它发包。这种最宽松打洞最容易但家用路由器默认一般不这么配。Restricted Cone外网主机想要向内网设备发包前提是这个内网设备之前已经向该外网IP发过包。也就是说NAT只限制“目标IP”不限制“目标端口”。Port Restricted Cone更进一步外网主机向内网设备发包不仅要求内网设备曾向该IP发过包还要求曾向该IP的特定端口发过包。这是绝大多数家用路由器的默认行为也是STUN打洞成功率较高的类型。Symmetric最严格的一类。内网设备每次向不同的目标地址发包路由器都会分配一个新的映射端口。哪怕同一个内网设备往两个不同的公网IP发UDP包源端口都会不同——映射关系跟“目标地址目标端口”强绑定。STUN在这种NAT下的表现很有意思A问STUN服务器B自己映射成了221.12.3.4:30000但A去连另一台设备C时路由器又会给A分配一个完全不同的端口比如221.12.3.4:30001。A拿着30000这个地址去告诉C“来连我”实际上C往30000发包根本到不了A。这就是Symmetric NAT下STUN失效的根本原因。3.2 不同类型下STUN的表现差异用表格展示更直观NAT类型STUN探测结果两设备直连可能性说明Full Cone有效映射地址高只要知道地址就能连Restricted Cone有效映射地址较高需要预先“打招呼”Port Restricted Cone有效映射地址中等需要双方互发探测包Symmetric只能拿到一个“临时的”映射极低地址不固定无法被“告知后连接”需要补充的是现实中同一台设备在不同网络路径上可能表现出不同的NAT行为。有些运营商大网内部还有一层CGN运营商级NAT设备先经过家庭路由器映射一次再经过运营商NAT映射一次此时STUN只能看到运营商NAT映射后的地址这层映射的行为如果是对称型的那基本就告别UDP直连了。3.3 如何主动探测NAT类型RFC 5780扩展RFC 5780NAT行为发现扩展给STUN增加了一些辅助机制用来判断NAT属于哪种类型。核心思路是通过CHANGE-REQUEST等属性让服务器从另一个IP或另一个端口来响应客户端从而观察NAT对不同源地址/源端口请求的反应方式。实际操作中一个功能完整的STUN客户端至少要做两次探测第一次向服务器A的IP:Port发请求观察响应的MAPPED-ADDRESS记录源地址和端口。第二次请求服务器用另一个IP或另一个端口来响应通过CHANGE-REQUEST控制看NAT是否允许这个“陌生源”的响应进来。如果第二次响应能收到说明NAT对源IP和源端口都比较宽松大概率是Full Cone或Restricted Cone如果收不到就需要对照测试结果细分。不过说句实在话这种探测在工程上更多是用于摸底和调优真正的连通性判断还是要靠后续的ICE链路逐一验证不能把“NAT类型预测”当成最终结论。4. 报文协议细节STUN头部与关键属性全解想要在抓包时看懂STUN在干什么就得熟悉它的报文结构。STUN的消息格式不算复杂但细节比较多尤其要区分老版RFC 3489和新版RFC 5389的差异否则按旧习惯去看字段很容易对不上。4.1 STUN报文头部结构一个STUN消息头固定是20字节包含以下部分字段长度说明Message Type2字节高4位是Method低2位是Class整体区分请求/响应/指示Message Length2字节不包括20字节头部本身的属性部分长度Magic Cookie4字节固定值0x2112A442Transaction ID12字节随机生成用于关联请求和响应Message Type字段一开始容易让人迷惑。它由Method和Class两部分组成以Binding请求为例BINDING方法的编号是0b000000000001Class是Request0b00时组合后整个Message Type值为0x0001如果是Response0b01则值是0x0101。所以看到0x0001是Binding Request看到0x0101是Binding Response这两个值在抓包里非常常见。4.2 常见Attribute解析属性部分是一连串的TLVType-Length-Value下面几个是STUN中最常被关注到的MAPPED-ADDRESS0x0001早期版本里用来返回映射地址。RFC 5389保留了这个属性但新实现更推荐用XOR-MAPPED-ADDRESS。XOR-MAPPED-ADDRESS0x0020返回的IP和端口经过Magic Cookie异或防止中间设备误判为DNS。这是实际抓包时最常看到的地址属性。CHANGE-REQUEST0x0003RFC 5780中用来请求服务器更换IP/端口回包做NAT类型测试用的。RESPONSE-ORIGIN0x802b表示服务器实际从哪个地址发出的响应可用于检测服务器是否有多地址出口。SOURCE-ADDRESS0x802c表示服务器接收请求的那个本地地址。SOFTWARE0x8022描述软件版本信息没有协议作用主要用于排查和兼容性确认。4.3 FINGERPRINT与消息完整性FINGERPRINT0x8028是一个可选属性放在消息末尾。它是对整个消息包括头部和所有属性做CRC-32计算后得到一个固定值再加一个固定的异或掩码0x5354554e。作用主要是给中间设备一个判断依据——看到这个指纹就知道这是一个STUN消息可以避免跟其他协议混淆。做NAT穿透调试时如果发现某些设备把UDP包当普通业务流量做了改写指纹机制能帮你确认问题是否出在“协议识别”上。另一个跟安全相关的属性是MESSAGE-INTEGRITY它通过用户名、密码等凭据生成HMAC-SHA1摘要防止消息被篡改。在生产环境部署STUN服务时如果服务对内网开放建议至少加上完整性校验否则别人可以伪造FINGERPRINT或篡改CHANGE-REQUEST属性来干扰打洞流程。4.4 从RFC 3489到RFC 5389字段差异带来的兼容坑老版本RFC 3489把STUN归类为“NAT会话穿越工具”它的头部没有Magic Cookie只有16字节的Transaction ID而且它用的是MAPPED-ADDRESS明文返回地址。新协议RFC 5389把Magic Cookie引入头部并新增了XOR-MAPPED-ADDRESS。这个变化带来一个非常实际的兼容问题很多现网里的STUN服务器是老版本或者客户端库用的是旧协议偏方两边字段对不上就会出现“请求发了、响应收到了、但地址解析出来是错的”这种诡异现象。排查时最直接的方法就是抓包看响应里的Transaction ID对应关系和XOR-MAPPED-ADDRESS的值如果解析出来是个奇怪的内网IP先检查是不是客户端把RFC 3489的字段解析逻辑套在了RFC 5389的头上了。5. 一次实际的STUN交互候选收集、连通性与抓包验证光看协议定义不够真正用过一次才能理解STUN在一条完整P2P链路上是怎么衔接的。我拿一个实际联机场景来说明A和B都在各自家庭网络后面它们要通过一个信令服务器交换信息然后建立UDP直连。5.1 完整的交互流程A启动客户端先向STUN服务器发送Binding Request。STUN服务器返回XOR-MAPPED-ADDRESS假设是221.12.3.4:30000。A通过信令服务器把这个候选地址CANDIDATE告诉BB也做同样的操作把自己的STUN探测结果告诉A。A开始向B的候选地址发UDP探测包B也同时向A的候选地址发探测包。由于双方都在发各自家里的NAT都建立了“内网 - 对方公网地址”的映射此时NAT才允许入站的对应流量双方的UDP包就能跨越NAT互通了。这也就是常说的“UDP打洞”。整个过程里STUN只负责第1-2步的地址发现真正的“打通”是靠第4步双方互发探测包完成的。这也是为什么很多人把STUN、NAT穿透、P2P打洞混为一谈——其实STUN只是前置条件打洞这个动作发生在其后。5.2 抓包看到的关键特征用抓包工具过滤UDP 3478端口STUN默认端口时能清晰看到来去两条消息客户端发出的Binding Request以及服务器返回的Binding Response。重点看响应消息20字节头部里的Magic Cookie应该是0x2112A442Message Type是0x0101Transaction ID与请求里的完全一致XORMAPPEDADDRESS属性里有经过异或的地址用工具解出来就是映射后的公网IP和端口。在Wireshark网络抓包工具里STUN解析器已经内置了展开XOR-MAPPED-ADDRESS后会直接显示解码后的地址非常方便。如果你发现Wireshark没有自动识别成STUN而是显示为“UDP payload”大概率是客户端发送时使用了非标准端口或者FINGERPRINT指纹缺失导致解析器没认出来。5.3 一个实际观察到的案例有一次我调试两个设备A上报的STUN映射地址是221.12.3.4:30000B上报的是118.99.2.5:40000。两边拿着对方的地址互相发包前几秒什么都没收到。检查发现两边的NAT都是Port Restricted Cone按道理互发就能通但我的探测包间隔太大导致NAT映射表里对应的“目标IP:目标端口”记录已经老化消失了。后来我把探测包的发送间隔从5秒改成500毫秒并且两边同时开启“连续探测模式”大约1秒内就收到对端的数据。这背后是NAT映射老化的工程问题跟STUN协议本身无关但实际联调中非常常见。6. 兜底方案Symmetric NAT下STUN失效后的处理前面提到Symmetric NAT是STUN穿透的“天敌”。我第一次碰到这种场景时一度以为是STUN服务器返回的地址不对后来反复抓包才发现映射地址每次都在变——只要内网设备向新的目标地址发包端口就会换。这种情况下就算把当前映射地址告诉对端对端发来的包也不会被路由器放行因为NAT表里根本没有对应记录。6.1 Symmetric NAT为何无法通过互相探测打通拿具体流程说。A向STUN服务器发请求路由器给A分配了映射端口30000A把这个地址发给B。B向30000发包但A的NAT表里没有“A - B”的会话记录包被丢掉。此时A也向B的候选地址发探测包A的NAT以为A要访问B于是新建了一条映射可能给A重新分配端口30001。B收到A从30001发来的包B往30001回包但A的NAT表里“A - B”的映射确实是30001这条反而通了。看似能通问题在于当A再向STUN服务器发一次请求时会重新分配一个端口A与B的这条映射也可能因为长时间没有新流量而老化。而且在很多Symmetric NAT实现里映射关系是“目标IP目标端口”级别的A访问B和访问STUN服务器用的是完全不同的端口也就是说A根本无法告诉B“一个稳定的、能持续使用的端口”。所以在Symmetric NAT下直连基本不可行只能走中继。6.2 用TURN做中继兜底TURNTraversal Using Relays around NAT协议就是给这种场景准备的。TURN服务器在公网有一个稳定的中继地址客户端通过TURN协议在服务器上分配一个中继端口所有发给这个中继端口的数据TURN服务器会原封不动转发给客户端。A和B各 自 在TURN服务器上分配一个中继地址通过信令互换后双方的数据都发到TURN服务器由服务器转发这样即使双方的NAT再严格也能通信。代价是所有流量都经过TURN服务器带宽成本和延迟都会增加而且服务器压力大。所以工程上通常采用ICE的分级策略——先尝试STUN直连host candidate和srflx candidate失败了再升级到TURN中继relay candidate。STUN在这里依然有价值它负责快速判断“有没有直连的可能性”如果连STUN探测都拿不到可靠的映射地址那大概率是Symmetric NAT直接上中继就好不用在白费力气打洞。6.3 STUN、TURN、ICE三者的分工定位如果非要用一段话总结三者的分工STUN负责“发现自己的外网映射地址”TURN负责“提供一个所有人都能访问的中继地址”ICE负责“把所有候选地址收集起来逐一尝试连通性选出最优路径”。实际工程实践中STUN服务器和TURN服务器经常部署在同一台机器上、共用同一个端口——客户端先尝试STUN拿到映射地址后做连通性检测不行再自动升级到TURN功能。抓包时看到同一个IP和端口上既跑STUN Binding又跑TURN Allocate不用惊讶这是很常见的融合部署方式。7. 工程落地时的实践细节与常见坑最后这部分我结合自己做过的实际项目把STUN从“能跑通”到“跑得稳”之间踩过的坑汇总一下。7.1 NAT映射老化时间比你想象的短很多家用NAT设备对UDP映射的老化时间设置为30秒到2分钟不等。如果你的应用是实时音视频流量持续在跑问题不大但如果是消息类应用比如P2P传输文件、消息推送数据是突发性的很可能你STUN探测到的映射地址在下次发包前就已经失效了。解决方案不是在STUN层面反复请求而是在应用层面维持心跳——按一定周期向对端发送保活包让NAT表里的会话不老化。保活包间隔需要根据网络类型动态调整我见过有人固定4秒发一次在移动网络下浪费了不少流量后来改成“收到对端数据就重置计时器超过10秒没收发才发心跳”的策略效果好了很多。7.2 并发Binding请求问题STUN客户端在ICE候选收集中可能会同时发起多个Binding探测A向STUN_1发、向STUN_2发、向STUN_3发每个请求都携带不同的Transaction ID。这是正常的但要注意如果只用同一个socket发送所有请求收到的响应可能乱序一个响应回来时你无法确定它对应的是哪个请求的result。解决方法是在客户端代码里维护一个Transaction ID到回调的映射表响应到达时用Transaction ID去匹配而不是按到达顺序处理。有些实现为了省事一个请求一个socket这样做也行但会占用大量临时端口NAT映射表也会变得混乱不推荐。7.3 防火墙与运营商对UDP的特殊策略STUN探测获取到的映射地址正确但这不代表对端就一定能把业务包发进来。有些企业防火墙具备应用层识别能力发现UDP负载不具备业务特征时会直接丢弃还有些运营商网络对UDP流量做了限速或高丢包策略。我在调试中发现某个跨运营商场景下STUN响应始终超时后来在STUN服务器侧抓包发现请求到了服务器但响应回不去——运营商把UDP 3478的入站端口封堵了。解决方式是改用TCP 443或者TLS 443端口跑STUNRFC 5389支持STUN over TLS借HTTPS的“通行证”混过去。7.4 多IP设备与候选地址优先级一台设备可能同时具备多个候选地址WiFi下的内网地址、4G/5G蜂窝网络地址、蓝牙或虚拟网卡地址、STUN探测到的公网映射地址。ICE会把它们全部收集起来但连通的本质是“两个候选地址之间确实能收发数据”。实际工程中我试过在一台开着虚拟网卡的笔记本上做STUN探测虚拟网卡抢占了默认路由的源地址导致STUN请求走了非预期的路径映射回来的地址不是真实出口IP。排查时用了route和tcpdump逐条路径比对才定位到。所以部署时最好明确指定STUN探测用哪块物理网卡并关闭不必要的虚拟网卡干扰。7.5 时间同步问题STUN响应本身不依赖时间戳但FINGERPRINT计算、日志排查、多服务器日志关联都需要准确时间。我遇到过NAT类型测试结果非常诡异的情况后来发现是服务器和客户端时间偏差太大导致日志里请求和响应时间顺序都对不上定位问题浪费了不少时间。做分布式STUN服务时NTP网络时间协议同步是基础中的基础不要忽视。7.6 不要把STUN当安全边界STUN服务器本身是一个UDP服务如果对外开放会被扫描和滥用。我在生产环境部署STUN服务时做了三件事一是限制单IP的请求速率防止被刷流量二是对需要凭据的TURN功能严格鉴权避免被当作公共代理三是把STUN和TURN服务放在独立的机器或容器里与其他业务隔离避免被当跳板。7.7 一个可以复用的调试流程如果你正在做STUN相关开发遇到问题不知道从哪里查起我建议按这个顺序排查先确认客户端能收到Binding Response抓包看有没有响应。确认Transaction ID匹配排除丢包和乱序。用Wireshark展开XOR-MAPPED-ADDRESS确认映射地址是否合理非内网、非保留IP段。在STUN服务器侧抓包看同样的请求到达时的源IP/源端口和客户端解析到的是否一致。如果不一致说明中间还有一层NAT或代理。再用RFC 5780的CHANGE-REQUEST做一轮NAT类型探测判断直连有没有戏。如果直连概率低尽早切换到TURN中继路径。写在最后STUN协议本身不复杂但它是理解NAT穿透、P2P通信、ICE链路的一把钥匙。很多人一开始觉得这协议很简单不就是一问一答嘛但真去调一次跨运营商、双NAT、防火墙严格的场景就会发现每一个字段、每一个超时、每一条NAT表项背后都有讲究。我个人实际使用中的体会是不要迷信STUN一次探测的结果网络环境会变NAT行为在不同时间、不同路径上也会变所有结论都应该通过ICE的连通性检测去验证。真正上了生产环境一定要给STUN配好重试策略、超时策略和降级到TURN的路径这样用户才能在任何网络下都保持连接稳定。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →