什么是 WebRTC 泄露?IP 泄露防护与浏览器隐私
什么是 WebRTC IP 泄漏?了解 ICE 候选如何暴露网络身份,以及如何控制浏览器信号并验证代理一致性。
BotBrowser Team
WebRTC 让页面获得网络的第二个视角。在任何通话建立之前,浏览器就能报告存在哪些本地网络接口、STUN 服务器观察到哪个公网地址,以及由哪个中继应答。BotBrowser 配置 ICE 服务器列表和页面能拿到的候选内容,并让这些观察结果与配置文件和已批准的路线保持一致,范围限于浏览器经该路线发出的路径;但它不能保证中继、对端或源站网络不会通过其他路径暴露地址。
TL;DR
- 代理改写的是 HTTP 请求,而候选收集可能走另一条路径,因此页面加载检查通过并不能说明 WebRTC 的情况。
- 每种类型只说明一件事:
host是本地接口,srflx是 STUN 服务器看到的地址,relay是 TURN 端点,prflx是对端在检查中发现的映射。 - BotBrowser 通过
--bot-webrtc-ice选择 ICE 服务器列表和对外呈现的候选信息,而--bot-webrtc=profile与--bot-webrtc=disabled决定 WebRTC 跟随配置文件还是被移除。 - 发布证据包括处于
complete的iceGatheringState、与批准路线一致的chrome://webrtc-internals网格,以及报告同一个本地候选的getStats()。
目录
观看视频
这段英文视频展示页面在任何通话开始之前,能从 RTCPeerConnection 候选收集中得知什么。本地实验页面声明三组候选,用同一个脚本和常规候选回调逐一读取,再把每个声明的 server-reflexive 地址与声明的 HTTP 可见地址做比较。随后在 ICE 服务器列表为空的官方 Google Chrome 中运行同类调用:Windows 机器返回两行 host 候选,Ubuntu 机器返回一行,且都带 mDNS 名称。它只说明这两台机器的返回结果,候选列表一致代表一致性,而不是匿名。测试不涉及 BotBrowser 配置文件。音轨为英文。
一次候选收集会暴露什么
候选(candidate)是媒体的一条可能路径,其 type 字段记录浏览器如何得知这条路径:host、srflx、prflx 或 relay。Chrome 在通话建立过程中通常会产生全部四种,每一种回答的是关于会话背后网络的不同问题。值得审查的不是候选是否存在,而是这条候选让页面能推断出关于机器、路线和应答中继的什么信息。
| 类型 | 页面得知什么 | chrome://webrtc-internals 中的字段 | 受控上下文的预期 |
|---|---|---|---|
host | 本地接口值,或被隐藏为 mDNS 名称 | 本地描述中的 candidate 行 | 一个 mDNS 名称,或配置文件定义的值 |
srflx | STUN 服务器看到的公网地址 | candidate 行与 relatedAddress | 已批准的出口,而不是主机上行链路 |
relay | 将承载媒体的 TURN 端点 | 带 typ relay 的 candidate 行 | 配置了可访问的 TURN 时出现 |
prflx | 检查过程中由对端发现的映射 | 统计视图中的 candidate-pair 行 | 不出现,或与批准路线一致 |
这张表之所以重要,是因为一套部署可能通过其中一行而失败于另一行。隐藏了私有接口值的配置文件,仍然会留下描述 STUN 请求实际出口的 srflx 行;而 TURN 中继又增加了对端在媒体阶段会看到的第二个公网值。逐项审查这四类候选,也能让失败报告更具体:缺少 relay 行指向 TURN 条目或 UDP 路径,而多出意外的 prflx 行则指向连接的对端一侧。
主机候选与 mDNS 名称
host 候选描述运行浏览器的机器上的一个网络接口。现代 Chrome 会把私有 IPv4 与 IPv6 值隐藏在 4f2a9c1e-7b3d-4a52-9e88-1c6d0b8f2e11.local 这样的 mDNS 名称之后,于是页面知道接口存在,却拿不到对应的值。这是有意义的限制,但它并不完整:该 mDNS 名称在配置文件生命周期内保持稳定,它会出现在本地描述里,也会再次出现在统计视图的 local-candidate 记录中。页面比较两次访问,就能判断两者是否使用了同一个接口,因此 WebRTC mDNS 候选与本地网络隐私把这个名称本身当成需要核查的一项,而不是已经解决的问题。
机器位于代理之后时,主机候选收集依然发生。代理改变的是页面请求的出口,它不消除本地接口,因此除非配置文件策略另有规定,浏览器会继续枚举它们。受控上下文应当显示一个 mDNS 名称或配置文件定义的值,而部署记录应写明配置文件产生的是哪一种。
服务器反射候选与观察到的值
srflx 候选存在,是因为浏览器向某个服务器发送了 STUN 绑定请求,而回复指明了该服务器看到的源地址。常见路径是经 udp:3478 使用 stun:stun.l.google.com:19302。回复携带映射后的公网值,relatedAddress 字段保留产生它的本地值,因此一行里可以同时包含同一次映射的两端。
srflx 行是泄漏报告最常引用的一行,因为它描述的不是主机自己选择的地址。当请求经过已批准的路线时,该值是路线出口;当请求直接发出时,该值是主机上行链路。两种结果在列表里看起来一样,这也是审查必须记录路线而不仅是记录值的原因,所以 代理一致性:DNS 与 WebRTC把 HTTP 出口、解析结果和候选值放在同一轮里比对。
中继候选与对端反射候选
配置且可访问 TURN 服务器时会出现 relay 候选。浏览器在服务器上分配一个值,媒体随后经 turn:relay.example.com:3478?transport=udp 流动。该行指明的是 TURN 端点而不是主机,因此它正是显示部署拥有一条不依赖本地上行链路的媒体路径的那一行。
当对端检查从浏览器未预测到的值抵达时,会出现 prflx 候选。该行由这次检查生成,而不是来自本地枚举,因此它记录的是另一方看到的结果。受控上下文出现 prflx 行,就意味着某条路径绕开了计划中的映射,审查应把它当作关于对端与路径的问题,而不是关于配置文件的问题。
浏览器层控制的边界
知道这四类候选之后,边界就更容易描述。下面的每种做法都改变页面或浏览器 API 报告的内容,却没有一种能改变媒体开始流动后对端或 TURN 服务器观察到的东西。
浏览器设置与扩展
通过浏览器设置关闭 WebRTC,例如 Firefox 中的 media.peerconnection.enabled,会完全移除候选收集。这种缺失是可观察的:页面可以测试 RTCPeerConnection 是否存在、createOffer() 是否返回结果,缺少该 API 的浏览器在页面所预期的人群中会显得突出。
扩展可以拦截 onicecandidate、改写 RTCIceServer 列表或替换构造函数。它们在原生收集代码产生值之后才运行,因此页面经原型路径取到的值,仍可能与网络层实际使用的值不同。打补丁还会在对象形状上留下痕迹:不同的原型链、返回被修改值的 getter,或比页面预期更晚触发的回调。
VPN 与中继的限制
VPN 把所有操作系统连接移入隧道,它改变 STUN 服务器看到的内容,却不改变机器枚举到的接口。主机行仍然描述本地接口,媒体也仍然直接与对端协商,除非要求使用 TURN 中继。隧道还增加了部署未必批准的第二个出口,只检查代理的审查会漏掉它。
中继服务也有各自的限制。TURN 端点用自身地址池中的值应答,通常位于开通服务时选择的区域,该值会与代理出口不同。这种差异是预期内的,应当记录下来,因为把每个新值都当作泄漏的审查会不断重复同一个问题。
BotBrowser 如何处理 ICE 服务器与候选
BotBrowser 结合已加载的配置文件与启动标志,改变 ICE 服务器列表以及浏览器对外呈现的候选信息。这些控制位于 JavaScript 层之下,因此读取 pc.localDescription 或 pc.getStats() 的页面拿到的就是配置文件与标志产生的结果。
ICE 配置的三个开关
| 开关 | 选择什么 | 什么不在其范围内 |
|---|---|---|
--bot-webrtc-ice="google" | 服务器列表 stun:stun.l.google.com:19302 | udp:3478 能否到达该服务器 |
--bot-webrtc-ice="custom:..." | 自定义 STUN 或 TURN 列表 | 该中继的凭据 |
--bot-webrtc=profile | 配置文件中存储的 WebRTC 设置 | 代理路线 |
--bot-webrtc=disabled | 移除 WebRTC 能力 | 无:该缺失对页面可见 |
区分这两组开关很有必要。--bot-webrtc-ice 选择服务器,--bot-webrtc 选择姿态。只设置姿态的启动会保留配置文件中的服务器列表,而只设置服务器列表的启动会保留配置文件已记录的姿态。只读其中一个就假设另一个,正是部署出现“配置文件看似正确、会话却经另一条路径收集”的原因。
标志与代理路径
常见的启动方式把预设与代理、本地 DNS 策略组合在一起:
chrome --bot-profile="path/to/profile.enc" \
--proxy-server="socks5://user:[email protected]:1080" \
--bot-webrtc-ice="google" \
--bot-local-dns \
--bot-timezone="America/New_York" \
--bot-locale="en-US"
自定义列表直接指定 STUN 与 TURN 服务器:
chrome --bot-profile="path/to/profile.enc" \
--proxy-server="socks5://user:[email protected]:1080" \
--bot-webrtc-ice="custom:stun:stun.example.com:3478,turn:turn.example.com:3478?transport=udp"
自动化通过启动参数传入相同的值:
const { chromium } = require('playwright-core');
(async () => {
const browser = await chromium.launch({
executablePath: 'path/to/botbrowser/chrome',
args: [
'--bot-profile=path/to/profile.enc',
'--proxy-server=socks5://user:[email protected]:1080',
'--bot-webrtc-ice=google',
'--bot-local-dns',
],
headless: true,
});
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com');
await browser.close();
})();
经过中继的路径
已批准的媒体路径必须能从浏览器到达 TURN 端点,而传输方式决定它能否到达。代理服务承载 UDP 时,socks5://user:[email protected]:1080 也能承载 STUN 与 TURN 流量,页面请求与媒体共用同一条路线。服务只承载 TCP 时,turn:turn.example.com:3478?transport=udp 无法经它应答,?transport=tcp 的中继需要作为独立路径另行批准。SOCKS5 上的 UDP:私有 QUIC 与 WebRTC 路由介绍了按上下文确定传输方式的决定及其验证方法。
在发布前核对候选集合
核对分三部分:收集状态、列出全部候选的网格,以及承载媒体的候选对的统计记录。三者合起来只需几分钟,留下的证据可供后续审查与部署记录对照。
在 chrome://webrtc-internals 中读取收集状态
打开 chrome://webrtc-internals,启动测试页面,观察 iceGatheringState。它从 new 变为 gathering,最终停在 complete。页面显示的网格每行一个候选,包含 type 列、address、port、protocol 与 relatedAddress 值。网格始终不到 complete,说明收集仍在进行或已经停滞;到了 complete 却只有 host 行,说明没有 STUN 回复到达。
把网格与 pc.getStats() 对照
pc.getStats() 返回的 local-candidate 记录包含 candidateType、address、port、protocol 与 relatedAddress,candidate-pair 记录包含 state、nominated、localCandidateId 与 remoteCandidateId。state: succeeded 且被提名的候选对就是承载媒体的路径。查出它的 localCandidateId,再确认对应的 local-candidate 行报告的类型与值和网格一致。两个视图相互矛盾时,例如网格里是 mDNS 名称而统计行里是私有 IPv4 值,说明页面与媒体路径使用了不同接口。
中继候选缺失时
到了 complete 却没有 relay 行是最常见的失败,按下面的顺序排查最快:
- 服务器列表里没有 TURN 条目。网格只会显示
host与srflx行,使用--bot-webrtc-ice="google"启动时本来就是这样。 - TURN URL 缺少
?transport=udp。没有传输参数的 relay URL 会退回 TCP,在只承载 UDP 的路径上不会有任何应答。 - 出口过滤了
udp:3478。TURN 分配始终无法完成,不会出现relay行,chrome://webrtc-internals显示这次尝试在没有候选的情况下结束。 --bot-webrtc-ice的取值与配置文件不匹配。自定义列表指向配置文件不允许的主机,或使用部署未获授权的预设,都会让本次启动使用一个无人选择的服务器列表,而网格显示的恰好就是那个列表。
每一步都只是一次检查,网格通常无需翻日志就能给出答案:TURN 端点那一行要么存在,要么不存在。
实践决策指南
把姿态与服务器列表一起决定,再用部署批准的路线核对结果。
- 将
--bot-webrtc-ice与--proxy-server搭配使用。没有代理时出口本来就是主机上行链路,标志只改变由哪个服务器报告它。 - 除非部署指定了自有基础设施,否则使用
google预设。stun:stun.l.google.com:19302是绝大多数流量使用的配置,省略它的会话反而会因为别的原因显得异常。 - 加上
--bot-local-dns。域名解析与媒体是最常绕过代理的两条路径,同时处理二者能让审查过程更短。 - 只在应用确实没有媒体功能时使用
--bot-webrtc=disabled。缺失的 API 可见,页面可以记录这一差异。 - 每次更换代理端点、TURN 服务商或配置文件后,重新执行三步核对。候选列表是这三项输入的共同结果。
BotBrowser 会破坏通话吗? 不会,RTCPeerConnection 始终可用,媒体照常流动。这些标志改变的是由哪些服务器收集、页面收到哪些值。
两个标志有什么区别? --bot-webrtc-ice 选择服务器,--bot-webrtc 选择 WebRTC 跟随配置文件还是被移除。
VPN 足够吗? VPN 改变所有连接的出口,但保留主机行,并增加一个部署未必批准的出口。
IPv6 呢? 同样适用这四种类型;只覆盖 IPv4 而不覆盖 IPv6 的配置文件值,会让两类地址之间出现可见差异。
实践结论
只看页面流量的泄漏审查会漏掉媒体经过的路径。记录姿态、服务器列表与已批准路线;收集到 iceGatheringState 报告 complete;把网格与 getStats() 对照;再把结果与部署记录放在一起。后续会话出现不同的行时,这种对照能直接指出变化所在,而不必由审查者去猜。
周边网络控制见代理配置、DNS 泄漏防护和多账号浏览器隔离。功能页面列出了相关产品能力,WebRTC mDNS 候选文章说明了 mDNS 名称给主机行增加了什么。