WebRTC 的 STUN/TURN/ICE 真实 IP 泄漏机制与客户端分流拦截取证
一、问题边界:代理开了,UDP 还是从物理网卡出去了
先把结论摆在前面,因为它决定了后面所有取证动作的方向。系统级代理(HTTP CONNECT、SOCKS5、甚至 PAC 分流)作用在应用层的 socket 语义上,浏览器主进程发起 HTTPS 请求时会尊重 --proxy-server 或系统代理设置。WebRTC 的 ICE 代理(ICE agent)走的是另一条路径:它由渲染进程通过 PeerConnectionFactory 创建,底层直接调用操作系统的 UDP socket 工厂去绑定本地网卡,向 STUN 服务器发送 Binding Request。这一层不经过 HTTP 代理栈,除非浏览器显式配置了 iceTransportPolicy 或 webrtc.ip_handling_policy 之类的策略,或者系统层面用 TUN 设备把 UDP 流量整体接管。
所以你会看到一种非常典型的现象:访问 https://ifconfig.me 显示的是代理出口 IP,但打开任意 WebRTC 检测页,srflx 候选里赫然是本地宽带的公网地址。这不是浏览器 bug,是 ICE 协议设计使然。RFC 8445 定义的 ICE agent 需要收集尽可能多的候选地址对来提升连通性,host 候选来自本地接口,srflx 候选来自 STUN 反射,relay 候选来自 TURN。收集阶段默认并发发起,不做代理感知。
这条链路上还有一类容易忽略的关联故障。用户报告“浏览器能打开应用连不上”,排查下来往往是代理只覆盖了 TCP,UDP 被丢弃或者走了错误路由,STUN 超时导致 ICE 收集阶段拖到 8 秒以上,媒体协商失败。这个现象在 浏览器能打开应用连不上 里有完整的分层排查路径,本文聚焦的是它的反面:UDP 没被拦住,反而泄漏了真实地址。
二、NAT 穿透流程的报文级拆解
2.1 STUN Binding Request 的字段结构
STUN 报文头固定 20 字节。前两位是 message type,Binding Request 为 0x0001,Binding Success Response 为 0x0101。接着是 2 字节 length、4 字节 magic cookie 0x2112A442、12 字节 transaction ID。属性区按 TLV 排列,常见的有 XOR-MAPPED-ADDRESS(0x0020)、MAPPED-ADDRESS(0x0001)、SOFTWARE(0x8022)、FINGERPRINT(0x8028)、ICE-CONTROLLED(0x8029)。
关键在 XOR-MAPPED-ADDRESS。服务端把观察到的源地址与 magic cookie 做异或:IPv4 情况下端口异或高 16 位 cookie,地址异或整个 cookie。客户端解出来就是自己的公网映射。这个值一旦被浏览器写进 SDP 的 a=candidate 行,对端和任何能读到 SDP 的中间人都能看到。
Wireshark 抓这个最直接:
stun && stun.type == 0x0001
stun.att.type == 0x00202
过滤出 Binding Request 后看 response 里的 XOR-MAPPED-ADDRESS,那就是泄漏源。实际排障时我习惯同时开 udp.port == 3478 || udp.port == 19302,Google 的公共 STUN 走 19302,很多客户端硬编码这个端口。
2.2 候选类型与它们的泄漏语义
ICE 候选分四类,泄漏风险各不相同。
| 候选类型 | 来源 | 泄漏内容 | 代理环境下的行为 |
|---|---|---|---|
| host | 本地网卡枚举 | 内网地址、链路本地地址 | 通常不构成公网泄漏,但暴露内网拓扑 |
| srflx | STUN 反射 | 本地出口公网 IP 与端口 | 核心泄漏源,绕过代理直连 |
| prflx | 对端反射 | 对端观察到的地址 | 被动产生,难以本地拦截 |
| relay | TURN 分配 | TURN 服务器地址 | 走代理时通常安全,但配置错误会直连 |
host 候选在双栈环境里会同时出现 IPv4 与 IPv6。IPv6 的全局地址往往是直接可路由的,没有 NAT 遮挡,泄漏后果比 IPv4 更严重。这类问题在 IPv4 与 IPv6 双栈排障 中有系统性的分析,WebRTC 场景下尤其要注意 ice_candidate_pool_size 与 IPv6 候选的交互。
2.3 完整时序
Browser(ICE Agent) STUN Server TURN Server Peer
| | | |
|-- Binding Request ------>| | |
| (src=物理网卡IP:随机端口) | |
|<-- Binding Success ------| | |
| XOR-MAPPED-ADDRESS=公网IP:port | |
| | | |
|-- Allocate Request ------------------------------>| |
| (REQUESTED-TRANSPORT=UDP) | |
|<-- Allocate Success ------------------------------| |
| RELAYED-ADDRESS=TURN出口IP:port | |
| | | |
|-- CreatePermission ------------------------------>| |
|<-- CreatePermission Success ----------------------| |
| | | |
|========== 候选交换(经信令通道,SDP) =================================>|
| | | |
|<================= 媒体流(SRTP over DTLS) ==========================>|2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
注意第一段:Binding Request 的源地址是物理网卡,不是代理的虚拟接口。这一步发生在任何应用层代理决策之前,因为 ICE agent 用的是 raw UDP socket。
三、为什么系统代理拦不住它
3.1 代理的作用域
HTTP 代理与 SOCKS5 代理都是“应用主动连接代理,再由代理转发”的模型。浏览器知道自己在用代理,所以 HTTP 栈会把请求发给代理。但 WebRTC 的 socket 创建路径是 rtc::AsyncSocket 与平台相关的 PhysicalSocketServer,它直接调 socket(AF_INET, SOCK_DGRAM, 0) 然后 bind 到 INADDR_ANY,再 sendto 目标地址。这个调用序列里没有任何代理介入点。
Chrome 有个 --force-webrtc-ip-handling-policy 启动参数,值可以是 default、public-interface-only、disable-non-proxied-udp。最后一个才是真正能拦住 srflx 泄漏的开关,它让 ICE agent 只使用代理提供的 UDP relay,不直接发 STUN。但这个参数默认不开,普通用户不会去改。
3.2 TUN 设备为什么有效
TUN 是三层虚拟网卡,内核把匹配路由的 IP 包交给用户态程序处理。VPN 客户端(WireGuard、OpenVPN、sing-box、Clash 的 TUN 模式)会把自己的路由表项插到默认路由前面,0.0.0.0/1 与 128.0.0.0/1 是最常见的两条,覆盖整个 IPv4 空间且优先级高于 0.0.0.0/0。这样任何 UDP 包,包括 STUN 的,都会进入 TUN,被用户态程序接管。
问题出在路由表的细节上。如果 TUN 程序只接管了 TCP,或者路由表里有更具体的 /32 条目指向物理网关,STUN 的 UDP 包就会从物理网卡漏出去。这类残留配置非常常见,排查思路参考 系统代理残留故障排查,核心是看 ip rule 与 ip route show table all 里有没有绕过 TUN 的例外。
3.3 内核参数与 socket 绑定
net.ipv4.conf.all.rp_filter 与 net.ipv4.conf.default.rp_filter 影响反向路径校验。TUN 场景下如果 rp_filter 设为严格模式(1),来自 TUN 的包可能因为源地址不在 TUN 网段而被丢弃。多数 VPN 客户端会把它设为 2(宽松模式)。这不是泄漏的直接原因,但会影响 TUN 拦截的完整性。
另一个参数是 net.ipv4.ip_local_port_range。STUN 客户端从临时端口范围选源端口,默认 32768 60999。取证时如果发现某个端口在短时间内反复出现 Binding Request,可以反推是哪个进程。
四、客户端拦截的三种实现路径
4.1 内核 TUN 层拦截
这是最彻底的一层。以 sing-box 的 TUN 入站为例,配置里 auto_route 打开后会自动插入路由规则,strict_route 会额外处理 DNS 与 ICMP。关键配置项:
{
"type": "tun",
"inet4_address": "172.19.0.1/30",
"auto_route": true,
"strict_route": true,
"stack": "system"
}2
3
4
5
6
7
stack 选 system 走内核协议栈,gvisor 走用户态。前者性能好,后者对异常包的处理更可控。拦截 STUN 的验证方法是抓 TUN 接口:
tcpdump -i tun0 -n 'udp port 3478 or udp port 19302'如果能看到 Binding Request 从 172.19.0.x 发出,说明拦截生效。如果物理网卡上也有同样的包,说明路由有例外。
4.2 浏览器策略层
Chromium 系浏览器支持 WebRtcIPHandlingPolicy 策略,通过企业策略或 chrome://flags 设置。取值与行为:
default:所有接口都收集候选,泄漏风险最高default_public_and_private_interfaces:排除 loopbackdefault_public_interface_only:只用默认路由接口disable_non_proxied_udp:禁用非代理 UDP,强制走 TURN over TCP 或代理 relay
Firefox 的对应开关是 media.peerconnection.ice.proxy_only 与 media.peerconnection.ice.relay_only。前者让 ICE 只使用代理,后者强制 relay 候选。about:config 里还有 media.peerconnection.ice.obfuscate_host_addresses,用于混淆 host 候选中的 mDNS 名称。
策略层的局限在于它依赖浏览器实现正确。历史上多个版本出现过策略未覆盖某些候选类型的问题,所以策略要配合网络层拦截做纵深防御。
4.3 反欺骗规则
在网关或本机防火墙上直接丢弃 STUN 出站包,是最粗暴也最可靠的方式。nftables 规则:
nft add rule inet filter output udp dport 3478 drop
nft add rule inet filter output udp dport 19302 drop2
但这会误伤合法的 WebRTC 应用。更精细的做法是只允许发往指定 TURN 服务器的 UDP,其余 STUN 目标一律丢弃。iptables 的 owner 模块可以按进程过滤,但浏览器是多进程架构,渲染进程的 socket 归属需要看 cgroup。
五、ConnProof 取证手法
ConnProof 的 CLI 提供 diagnose、tcp、tls、dns、websocket、doctor 六个子命令。WebRTC 泄漏取证主要用 diagnose 与 doctor,配合 --verbose 看接口与路由细节。
先看整体网络状态:
connproof diagnose --verbose输出里会列出活动接口、默认路由、DNS 配置。重点看默认路由指向的是物理网卡还是 TUN。如果默认路由是物理网卡而 TUN 只挂了部分网段,STUN 就会漏。
再看 UDP 可达性。ConnProof 没有专门的 UDP 探测子命令,但 diagnose 会报告 UDP 出站是否被拦截。配合 --format json 便于脚本化比对:
connproof diagnose --format json --timeout 5doctor 子命令做的是配置一致性检查,会标记“代理已启用但存在非代理 UDP 路径”这类状态:
connproof doctor --verboseDNS 泄漏常与 WebRTC 泄漏同时出现,因为两者都涉及“哪些流量走了代理”的判断。用 dns 子命令确认解析路径:
connproof dns example.com --verbose如果 DNS 查询发往本地 ISP 的 resolver 而不是代理隧道内的 resolver,说明分流规则有洞,STUN 大概率也漏。TLS 层用 tls 子命令确认出口证书链,间接验证出口 IP:
connproof tls ifconfig.me --port 443 --verbose--ipv4 与 --ipv6 参数可以强制指定协议族,用于分别验证双栈下的泄漏情况。IPv6 场景下 STUN 泄漏更隐蔽,因为很多 TUN 配置只接管了 IPv4。
六、失败边界与权衡
TUN 拦截不是没有代价。全量接管 UDP 会增加用户态转发开销,高码率视频会议场景下 CPU 占用明显上升。stack: gvisor 比 system 更耗 CPU,但隔离性更好。这是性能与可控性的权衡。
策略层拦截的代价是兼容性。disable_non_proxied_udp 会让部分依赖 P2P 直连的应用退化到 TURN 中继,延迟上升 30 到 80 毫秒,带宽成本转移到 TURN 服务器。企业环境里这个代价通常可接受,消费级场景用户会抱怨卡顿。
反欺骗规则的代价是误伤。硬丢 3478 端口会让所有自建 STUN 失效,包括内网合法的媒体服务器。规则要按目标地址白名单收敛,不能一刀切。
还有一个边界是 prflx 候选。它由对端观察产生,本地无法拦截。即使你封了所有出站 STUN,对端仍可能通过信令或媒体路径观察到你的地址。这类泄漏只能靠信令层加密与 TURN 强制中继来缓解。
七、取证清单
按顺序执行,每步都有明确的判定标准。
connproof diagnose --verbose确认默认路由归属,TUN 必须在物理网卡之前。tcpdump -i <物理网卡> -n 'udp port 3478 or udp port 19302'抓 30 秒,有 Binding Request 即为泄漏。connproof doctor --verbose看配置一致性告警。- 浏览器开
chrome://webrtc-internals,看 ICE candidate pair 里有没有 srflx 类型的本地公网地址。 connproof diagnose --ipv6 --verbose单独验证 IPv6 路径,双栈泄漏常被忽略。- 修复后重复步骤 2,物理网卡上应无 STUN 流量,TUN 接口上应有。
这套流程在多次现场排障里验证过,能把“代理开了但 IP 还是漏”的问题定位到具体的路由表项或浏览器策略项。WebRTC 的泄漏不是玄学,是 socket 创建路径与路由决策的确定性结果,抓住这两点就能闭环。