Theo Howe WebRTC 与 ICE:候选地址、会话协商和 IP 泄漏检测
网络通信与隐私安全 · 第 1 篇

WebRTC 与 ICE:候选地址、会话协商和 IP 泄漏检测

WebRTC & ICE: Candidates, Offers, and IP Leak Detection
阅读约 14 分钟 · 10 节 更新于 2026-11-04 整理自与 ChatGPT 的对话,已对照以下资料核实:RFC 8445(ICE)、RFC 8489(STUN)、RFC 8656(TURN)、RFC 8838(Trickle ICE)、RFC 8828(WebRTC IP 地址处理)、RFC 8841(数据通道的 SDP)、IETF 草案 draft-ietf-mmusic-mdns-ice-candidates、W3C《WebRTC: Real-Time Communication in Browsers》、MDN(RTCIceCandidate.type、icegatheringstatechange),拓展部分参考 Tailscale 文档 DERP servers(2026-11-04 查阅)

打开一个“WebRTC 泄漏检测”网站,它几秒钟就能列出你的内网地址和公网地址。这件事背后不是网站在“扫描”你的电脑,而是浏览器在为一次可能的点对点连接做准备:收集候选地址,再通过 JavaScript 交给网页。这篇从 WebRTC 的组成讲起,依次讲清 ICE 候选地址、连接建立流程、Offer 和本地描述,最后落到泄漏检测的原理,以及开了代理 TUN 模式后该怎么判断。原对话里说错或说得不全的地方都单独标了出来。

读完这一篇,你应该能够:
  • 说出 Host、srflx、prflx、relay 四种候选地址分别从哪里来
  • 讲清从收集候选、交换、连通性检查到选定路径的完整流程
  • 解释 createOffer 和 setLocalDescription 各做什么,为什么检测网站不用真的建立连接
  • 看懂泄漏检测的结果,分清“地址暴露”和“流量绕过代理”

WebRTC 是什么

WebRTC(Web Real-Time Communication)让浏览器(或其他应用)之间可以尽量直接地传输音视频和任意数据,常见于网页视频会议、语音通话、浏览器间传文件、云游戏和远程桌面。

它不是一个协议,而是一组协议和 API 的组合:

组成负责什么
信令(Signaling)双方交换“我想怎么连、我在哪里”的信息。WebRTC 不规定信令怎么做,由应用自己用 HTTP、WebSocket 等实现
SDP描述会话的文本格式:有哪些媒体、用什么编码、ICE 和加密参数
ICE收集可能的通信地址,逐一测试,选出能用的那条路径
STUN让设备得知自己在 NAT 外面“看起来”是什么地址和端口;也被 ICE 用来做连通性检查
TURN直连走不通时,提供一个中继服务器转发数据
DTLS / SRTP / SCTP加密和传输:音视频走 SRTP(密钥由 DTLS 协商),数据通道走 SCTP over DTLS
一句话记住

WebRTC 的“点对点”指的是数据尽量直连,不是“完全不需要服务器”:交换连接信息总要靠信令服务器,直连失败时还要靠 TURN 中继。

打个比方

两个人想当面交接一个包裹,但都住在有门卫的小区里(NAT)。

  • 先打电话约好时间和交接方式:这是信令和 SDP。
  • 各自去问门卫“外面的人看我是从哪个门出来的”:这是 STUN。
  • 把能想到的碰头地点都列出来,挨个试哪儿能碰上:这是 ICE。
  • 实在碰不上,就寄放到双方都能去的快递柜:这是 TURN。
别踩坑原对话里的偏差
  • “WebRTC 让两个设备能够在不经过传统服务器转发数据的情况下直接通信”。只说了理想情况:信令必须经过服务器;直连不成功时数据要经 TURN 转发。
  • 把流程写成“信令 → STUN → ICE → P2P → 最后才用 TURN”。实际上配置了 TURN 时,中继候选和其他候选是同时收集的,不是直连失败之后才去申请;TURN 只是在其他路径都不通时才被选中(RFC 8445 第 2.1 节)。

ICE 候选地址

ICE 候选地址(ICE candidate)不是单纯的 IP,而是一条“你可以试着从这里找到我”的线索。一台设备通常会收集到好几个。

候选地址里有什么

网页拿到的每个候选地址都是一行文本,格式来自 SDP 的 a=candidate 行:

candidate:842163049 1 udp 1677729535 203.0.113.10 62000 typ srflx raddr 0.0.0.0 rport 9
          └基础标识┘ │  │   └─优先级─┘ └──地址──┘ └端口┘ └─类型─┘ └──对应的本地地址──┘
                  组件 协议

typ 后面就是类型;raddr/rport 是它所对应的本机地址。浏览器启用 mDNS 隐藏时,这里会填成 0.0.0.0 和 9,免得从旁边漏出内网 IP。

候选地址的优先级按 RFC 8445 的公式计算:

公式候选地址优先级(RFC 8445 第 5.1.2.1 节)
$$ \text{priority} = 2^{24}\times \text{类型偏好} + 2^{8}\times \text{本地偏好} + (256 - \text{组件 ID}) $$
  • 类型偏好:0 到 126,推荐值 host 126、prflx 110、srflx 100、relay 0。
  • 本地偏好:0 到 65535,同一类型里区分多块网卡或 IPv4/IPv6。
  • 组件 ID:RTP 为 1,RTCP 为 2;现在多数连接复用同一端口,基本都是 1。
算一遍算一个 host 候选的优先级

一个 host 候选,类型偏好取推荐值 126,本地偏好取最大值 65535,组件 ID 为 1。它的 priority 是多少?

答:126 × 16777216 + 65535 × 256 + 255 = 2,130,706,431。这个数在浏览器里很常见,看到它基本就是本地偏好拉满的 host 候选。

四种候选类型

类型从哪里来典型内容
host
本机候选
本机网卡上的地址,不需要任何服务器192.168.1.10,或被 mDNS 替换成 xxxx.local
srflx
服务器反射候选
向 STUN 服务器(或 TURN 服务器)发请求,对方告诉你“我看到你来自哪里”NAT 映射后的公网地址和端口,如 203.0.113.10:62000
prflx
对端反射候选
连通性检查时,从对方回过来的 STUN 消息里才发现的新地址收集阶段没发现、检查时冒出来的映射地址
relay
中继候选
向 TURN 服务器申请(Allocate)到的转发地址TURN 服务器上的 IP 和端口

srflx 不是你的电脑真正拥有的地址,而是 NAT 设备为这次通信临时开的映射;relay 则不只是一个地址,TURN 服务器会真的替你转发数据。

别踩坑原对话里的遗漏
  • 只讲了 host、srflx、relay 三种。ICE 标准(RFC 8445)和浏览器 API(MDN RTCIceCandidate.type)都定义了四种,漏了 prflx:它在连通性检查阶段才产生,泄漏检测页面上一般看不到,但读 chrome://webrtc-internals 时会遇到。
  • “srflx 通过 STUN 获取”只说了一种来源。TURN 服务器在 Allocate 响应里也会带上你的反射地址(XOR-MAPPED-ADDRESS),所以只配了 TURN 时同样会得到 srflx(RFC 8656 第 7.3 节、RFC 8445 第 2.1 节)。

从收集到连通:ICE 的完整流程

一句话记住

收集候选 ≠ 建立连接。 收集只是列出“可能的地址”;要配对、检查、提名之后,才知道哪条路真的能走。

交换候选与 Trickle ICE

双方各自收集好候选后,要通过信令把候选发给对方(写在 SDP 里,或单独一条条发)。信令服务器只负责转交这些信息,一般不参与之后的数据传输。

传统做法是等全部收集完再一次性发送。Trickle ICE(Trickle ICE)则是收集到一个就发一个,双方能更早开始检查,连接建立更快。浏览器的 icecandidate 事件就是按这种方式一条条吐出候选的;最后会再触发一次 candidate 为 null 的事件,表示收集结束。

候选对、连通性检查和提名

  1. 配对:把自己的每个候选和对方的每个候选组合成候选对(candidate pair),按两边优先级排序。
  2. 连通性检查:在每个候选对上发 STUN Binding 请求,收到对应的响应,说明这条路径双向可达。检查过程中如果发现了新的映射地址,就生成 prflx 候选。
  3. 提名:ICE 给双方分配角色,控制方(controlling agent)从检查通过的候选对里选一条,再发一个带“提名”标记的 STUN 请求告诉对方(RFC 8445 第 2.3 节)。
  4. 传输:之后的音视频和数据都走被提名的那一对。如果选中的是 host 或 srflx 组合,TURN 就完全不参与。
概念回答的问题
Candidate我可能在哪里被联系到?
Candidate Pair我们试试用哪两个地址通信?
Connectivity Check这条路通不通?
Nominated Pair最终走哪条路?
先想一想

Mac 和 Windows 不在同一个局域网,各自都收集到了 host、srflx、relay 三种候选。如果两边的 NAT 都允许打洞,最终最可能选中哪种组合?TURN 服务器还会转发数据吗?

想好了,看答案

最可能选中 srflx ↔ srflx(双方都用 NAT 映射后的公网地址直连)。host ↔ host 因为不在同一局域网而检查失败;relay 优先级最低,只有前面都不通时才会被选中。既然选中的是直连路径,TURN 不转发任何数据。

Offer 与本地描述

检测网站的脚本里总有 createOffer() 和 setLocalDescription() 两步。它们本身不负责“获取 IP”,而是 WebRTC 会话协商的起点。

createOffer:写一份会话提议

Offer(SDP offer)只是一份文本,生成它不会向任何人发送数据。只建了一个数据通道时,Offer 大致长这样:

v=0
o=- 4611731400430051336 2 IN IP4 127.0.0.1
s=-
t=0 0
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 0.0.0.0
a=ice-ufrag:EsAw
a=ice-pwd:bP+XJMM09aR8AiX1jdukzR6Y
a=fingerprint:sha-256 0F:74:31:25:...
a=setup:actpass
a=mid:0
a=sctp-port:5000
字段含义
m=application … UDP/DTLS/SCTP webrtc-datachannel声明一个数据通道,跑在 DTLS 之上的 SCTP 上(RFC 8841)
a=ice-ufrag / a=ice-pwdICE 连通性检查用的用户名片段和密码
a=fingerprintDTLS 证书指纹,用来确认对方身份
a=setup:actpassDTLS 角色协商:发起方表示“主动被动都可以”
a=sctp-portSCTP 使用的端口号

setLocalDescription:把提议正式生效

createOffer() 只是写好了草稿;setLocalDescription(offer) 才把它设为这条连接的本地描述,更新协商状态。W3C 规范注明,这一步可能触发 ICE 候选收集(依据 JSEP,RFC 9429 第 5.9 节),于是浏览器开始收集候选并逐个触发 icecandidate 事件。

正常的双端通信还差一半:对方要回一份 Answer,发起方用 setRemoteDescription() 设为远端描述,ICE 检查才有对象可测。

打个比方

createOffer() 是写好一封合作邀请函;setLocalDescription() 是盖章确认“我就按这份来”,盖完章秘书(浏览器)就开始去准备各种联系方式(候选地址)。对方回函(Answer)之前,合作其实还没开始。

别踩坑原对话里的偏差
  • “真正让浏览器进入 ICE 收集的就是 setLocalDescription()”。大多数情况如此,但不绝对:如果配置了 iceCandidatePoolSize,浏览器可以在调用它之前就预先收集一批候选(W3C 规范 RTCConfiguration)。
  • 原对话有一段示例把 pc.onicecandidate = … 写在 await pc.setLocalDescription(offer) 之后。规范的写法是先注册监听再开始协商,避免漏掉早到的事件。
  • “Offer 不等于网络连接,也不等于已经向对方发送了数据”,这一点是对的。
  • 必须先 createDataChannel()(或加一条音视频轨道):没有任何媒体或数据通道时,Offer 里没有 m= 段,也就不会收集候选。
概念解决的问题
Offer / Answer双方想建立什么样的会话?
Local / Remote Description本端和对端正式采用的会话描述
ICE Candidate可以通过哪些地址找到我?
一句话记住

Offer / Answer 负责会话协商,ICE 负责找路。两套机制配合,才是一次完整的 WebRTC 连接。

STUN 到底参与了哪些环节

原对话里你问:“STUN 只在获取 srflx 时有作用,host 和 TURN 都与 STUN 无关,对吧?”原回答说“基本正确”。更准确的说法是:收集 host 候选确实不需要 STUN 服务器,但 STUN 这套消息格式贯穿了 ICE 的好几个环节。

环节用到的 STUN 消息和哪类候选有关
收集 host不需要host
收集 srflx向 STUN 服务器发 Binding 请求srflx
收集 relayTURN 的 Allocate、CreatePermission、ChannelBind 都是 STUN 格式的消息;Allocate 响应还会顺带返回 srflxrelay、srflx
连通性检查与提名在每个候选对上发 STUN Binding 请求所有类型,包括 host 和 prflx
一句话记住

STUN 服务器只在收集 srflx 时用到;但 STUN 协议是 ICE 的“通用语言”,连通性检查、TURN 申请都在用它。

泄漏检测网站做了什么

一段脚本就能读出候选地址

检测网站不需要扫描你的网卡,也不需要向你的路由器发请求。它做的只是让浏览器自己准备一次连接:

const pc = new RTCPeerConnection({
  iceServers: [{ urls: "stun:stun.l.google.com:19302" }]  // 指定一个 STUN 服务器
});
pc.onicecandidate = (e) => {
  if (e.candidate) console.log(e.candidate.candidate);    // 每收集到一个就打印
  else console.log("收集结束");
};
pc.createDataChannel("probe");                            // 让 Offer 里有 m= 段
await pc.setLocalDescription(await pc.createOffer());     // 触发候选收集

它不需要把 Offer 发给任何人、不需要对方回 Answer、也不需要真的建立数据通道。只要本地收集到候选,网页前端就已经读到了。

公式检测网站的判断逻辑
网页普通请求的来源 IP(HTTP 出口)   ── 网站后台直接看到
WebRTC 候选里的 srflx / host / IPv6   ── 前端脚本读到后上报
                    │
            两者比对:候选里有没有出现 HTTP 出口以外的公网地址?
                    │
        有 → 报告“可能泄漏”;  没有 → 至少这一次没发现

两个 IP 不一致不一定是泄漏,比如 WebRTC 走了另一个同样属于代理的出口;也要结合地址归属判断。

谁能看到什么

角色能看到什么
网页 JavaScript浏览器通过 icecandidate 事件交出来的全部候选
STUN 服务器STUN 请求到达时的来源 IP 和端口
网站后台普通 HTTP 请求的来源 IP,以及脚本主动上报的候选
别踩坑原对话里的偏差
  • “STUN 服务器得到的公网 IP 不会自动传给网站后台”。只有当 STUN 服务器是第三方的(比如上面例子里的 Google)才成立。STUN 服务器的地址是网站自己填的:如果填的是网站自己运营的 STUN 服务器,那么来源 IP 一到就被网站看到了,根本不需要脚本上报。
  • “是浏览器主动把收集到的候选通过 JavaScript API 提供给网页”,这一点是对的,也是整个检测的关键。

开了 TUN + Global 还会泄漏吗

浏览器默认只用默认路由那块网卡

原对话举例说,Windows 有以太网、Wi-Fi、Clash TUN 三块网卡时,“浏览器可能收集这些网络接口对应的候选地址”。这不是浏览器的默认行为。RFC 8828 定义了四种地址处理模式:

模式收集哪些地址
模式 1:枚举全部所有网卡的地址都用
模式 2:默认路由 + 对应的本地地址按系统路由表选出默认路由那块网卡,只给出这块网卡的私有地址作为 host 候选
模式 3:只用默认路由同模式 2,但不给私有地址,只有经 STUN/TURN 得到的地址
模式 4:强制走代理同模式 3,且浏览器配置了代理时,WebRTC 流量也必须走这个代理;代理不支持 UDP 就改用 TCP

标准要求:没有得到用户同意时不得使用模式 1,应使用模式 2;摄像头、麦克风授权可以算作一种同意。在此之上,浏览器还会用 mDNS 候选(mDNS ICE candidate)隐藏私有地址。

所以开了 TUN 且接管了系统默认路由之后,按模式 2,浏览器选中的“默认路由网卡”就是 TUN 虚拟网卡本身。

别踩坑
  • 以为普通网页能拿到你所有网卡的地址。按 RFC 8828,那需要你先授权(比如允许了摄像头),否则只用默认路由。
  • 以为 mDNS 能藏住一切。它只替换私有地址;公网 IPv6 这类本来就公开的地址不一定会被隐藏,所以测试时 IPv6 要单独看。
  • 各浏览器的具体默认策略和开启版本没有逐一对照厂商文档核实,以你自己的测试结果为准。

STUN 请求经过 VPS 的三种结果

假设环境如下(地址都是示例):

项目地址
Windows 内网 IP192.168.1.100
TUN 虚拟网卡198.18.0.1
家庭公网 IP203.0.113.10
VPS 出口 IP198.51.100.50
STUN 请求的去向STUN 服务器看到srflx 候选结论
被 TUN 接管,由 VPS 转发出去198.51.100.50VPS 地址公网地址没有经 srflx 暴露
绕过了 TUN,直接从家里路由器出去203.0.113.10家庭公网 IP泄漏
进了 TUN,但代理链路不支持 UDP收不到没有 srflx不代表安全,还要看 host 和 IPv6

“STUN 经过 VPS 就不会泄漏”有三个前提:

  1. 是 VPS 替你发出去的:请求从 VPS 出口到达 STUN 服务器,而不是只在本机绕了一圈又从原网卡出去。
  2. UDP 全程有人接:STUN 通常走 UDP,代理客户端、代理协议和服务端都要支持 UDP 转发;不支持时结果是失败,而不是自动变成 VPS 地址。
  3. 其他候选也没漏:srflx 已经是 VPS 地址,host 候选、IPv6 地址、TURN 候选仍要单独看。
别踩坑
  • “开了 Global 就等于所有流量都走 VPS”。Global 是代理规则层面的选择,UDP 能不能被 TUN 接管、代理协议支不支持 UDP、系统有没有 IPv6 旁路,都会影响结果。

地址暴露 ≠ 流量泄漏

你想检查的实际在问
候选地址暴露网页能不能知道我的真实 IP?
STUN 流量STUN 服务器看到的是家庭 IP 还是 VPS IP?
后续 P2P 流量真正连上之后,UDP 数据有没有绕过代理?

ICE 会根据双方候选的配对和检查结果选路,所以 STUN 请求经过 VPS,不代表之后的连通性检查和数据都经过 VPS。反过来,host 候选暴露了 192.168.1.100,也不代表互联网上的网站能连进你的内网。

先想一想

开了 TUN + Global,检测网站显示:HTTP 出口 198.51.100.50(VPS),host 候选 3f2a….local,srflx 198.51.100.50。能不能下结论说“WebRTC 完全不泄漏”?

想好了,看答案

不能。 这次只说明:内网 IPv4 被 mDNS 藏住了,srflx 也是 VPS 地址,家庭公网 IPv4 没有通过候选暴露。还没覆盖的有:IPv6 地址、真正建立 P2P 连接后的数据走向(检测网站通常根本不建立连接),以及换一个 STUN 服务器或浏览器后的情况。

动手测一遍

实操要点对照测试的步骤
  1. 关掉代理,打开 BrowserLeaks WebRTC 或 IPLeak.net,记下真实的公网 IPv4、IPv6 和 WebRTC 候选,作为基线。
  2. 开启 TUN + Global,刷新同一页面,记下普通 HTTP 检测到的出口 IP。
  3. 逐条看候选:typ host(是不是 .local)、typ srflx(是 VPS 还是家庭 IP)、有没有出现 IPv6 地址。
  4. 想看原始过程:在浏览器开发者工具的 Console 里粘贴上一节那段脚本,亲眼看候选一条条出来;把 iceServers 改成空数组再跑一次,就只剩 host 候选。
HTTP 出口srflx说明
VPSVPSSTUN 出口符合预期
VPS家庭公网 IPSTUN 流量绕过了代理,需要排查
VPS没有STUN 被阻断、没配置或没收集成功,不等于安全
VPSVPS,但实际连接直连候选没暴露,后续通信路径仍可能泄漏

本篇小结

  • WebRTC 的点对点是“数据尽量直连”,信令总要服务器,直连失败还要 TURN 中继。
  • 候选地址有四种:host(本机网卡)、srflx(STUN/TURN 看到的 NAT 映射)、prflx(检查时发现)、relay(TURN 分配);优先级 host 最高、relay 最低。
  • ICE 流程:收集 → 交换(可以 Trickle)→ 配对 → STUN 连通性检查 → 控制方提名 → 传输。
  • createOffer() 写会话提议,setLocalDescription() 让它生效并通常触发收集;检测网站只走到这一步就够了。
  • STUN 服务器只管 srflx,但 STUN 协议贯穿连通性检查和 TURN。
  • 浏览器默认只用默认路由那块网卡并用 mDNS 藏私有地址;开 TUN 后重点看 srflx 是不是 VPS、IPv6 有没有漏,并分清“地址暴露”和“流量泄漏”。

拓展

以下内容来自 ChatGPT 结合我之前的提问(Tailscale、UDP 打洞)做的联想,不属于本篇正文,留作后续学习线索。

WebRTC 和 Tailscale 的对照。 两者都要解决“两台在 NAT 后面的设备怎么直连”,思路很像,但定位不同:WebRTC 是一套给应用用的实时通信能力,Tailscale 是把设备组成同一个私有网络的组网产品。

对比WebRTCTailscale
定位实时音视频和数据通信设备组网(虚拟局域网)
找路ICE + STUN自己的 NAT 穿透机制(DISCO 消息)
直连失败时TURN 中继先找同网内的 Peer Relay,再退到 DERP 中继
加密DTLS、SRTPWireGuard(DERP 只转发已加密的包,看不到内容)

Tailscale 一侧依据官方文档 DERP servers 核实;原对话把 Tailscale 的穿透写成“DERP、STUN、WireGuard 等机制”,文档里 DERP 同时负责协商连接和兜底中继,具体是否使用标准 STUN 未在本次查阅的文档中核实。

下一步可以学:NAT 类型与 UDP 打洞。 现在已经知道 ICE 怎么收集、配对、检查地址;接下来要弄清为什么有些 NAT 环境能打洞成功,有些却只能走 TURN 中继。这和端点无关映射(Endpoint-Independent Mapping)等 NAT 行为有关,可以从 RFC 4787 入手。