Repository Wiki
jason5ng32/MyIP

WebRTC 地址泄露与浏览器隐私检测

本页说明 MyIP 如何通过浏览器 WebRTC 的 ICE 候选收集,展示不同 STUN 服务观测到的地址,以及如何处理浏览器禁用 WebRTC、检测失败和结果汇总。该功能提供地址暴露的诊断证据,而不是对浏览器全部隐私能力的综合认证。

目的与范围

本文覆盖 WebRtcTest 的检测入口、STUN 连接、候选地址筛选、归属信息补全、状态语义、命令与事件、超时和资源清理。IP 定位服务内部实现、DNS 泄露检测、全局刷新编排和报告生成规则属于相邻能力,不在本页展开。

**证据范围:**本页依据 WebRtcTest.vue 的脚本实现。未进一步读取模板、Store、useMaxmind、事件总线或专门的 WebRTC 测试实现,因此不会推断它们的持久化策略、缓存、网络端点及测试覆盖率。

概述:这项检测能说明什么

组件为四个 STUN 服务分别创建 RTCPeerConnection,创建数据通道和本地 Offer,监听 ICE 候选。只有类型为 srflx 或 prflx 且能提取出 IP 的候选才构成成功结果;浏览器自身产生的 host 候选不证明 STUN 请求成功,不能作为“STUN IP”展示。

结果需要按以下方式解读:

结果实现中的含义不能直接推出的结论
得到 IP接受了一个反射候选,并从候选字符串提取地址该地址一定绕过了代理,或一定不同于 HTTP 出口
StatusUnavailableWebRTC API 缺失,或运行时呈现明确的阻止/替换行为浏览器所有隐私风险都已消除
StatusError没有及时得到合格候选,或出现其他异常一定启用了 mDNS,或一定不存在泄露
IP 成功、归属字段失败地址已得到,但归属查询没有返回数据STUN 本身失败

已读取的检测代码没有将 STUN IP 与 HTTP 出口 IP 进行比较,也没有生成独立的“泄露/不泄露”布尔判定。natType 的实现是对 ICE 候选类型的翻译映射,不是完整的 NAT 映射/过滤行为探测。

来源:候选成功条件、类型映射。

架构与依赖

Loading diagram...

Source: WebRtcTest.vue

这一结构把“是否拿到地址”和“地址归属查询”分为两个阶段:IP 可以先成为有效结果,归属信息随后补齐。报告快照则由整轮检测统一发出,不由每个候选事件分别发布。

WebRtcTest 还依赖 useMainStore 记录挂载/完成状态,依赖 vue-i18n 生成显示状态,使用 trackEvent 记录刷新操作。隐私显示层初始化了 createMaskGate(t),但本页未读取模板,不能据此断言所有字段或 SDP 日志均已脱敏。

检测生命周期与真实控制流

1. 挂载不等于执行检测

onMounted 只调用 store.setMountingStatus('WebRTC', true)。脚本中的命令入口为 webrtc:run,其处理器先等待下一次 webrtc:finished,再启动检测;这种顺序可以覆盖“API 不可用时立即完成”的同步事件路径。

javascript
1useAppCommand('webrtc:run', ({ isRefresh = false } = {}) => { 2 const finished = waitForAppEvent('webrtc:finished'); 3 checkAllWebRTC(isRefresh); 4 return finished; 5});

Source: WebRtcTest.vue

isRefresh 默认为 false,为真时记录 trackEvent('Section', 'RefreshClick', 'WebRTC')。它不改变 STUN 超时,也不切换服务器列表。自动运行偏好的具体调度位置未在本页核验。

2. API 缺失时直接形成结果

组件初始化时通过 typeof RTCPeerConnection === 'function' 保存 isWebRtcAvailable。每轮开始设置 isStarted = true;若 API 缺失,则四张服务器记录统一写入 StatusUnavailable,并设置 natTypeCode = 'unavailable',随后更新 Store 并发布完成事件,不创建连接。

这是一种明确的产品语义:对于地址泄露检测,禁用 WebRTC 是保护性状态。toneOf 将该状态映射为 ok-fast,而不是普通错误;isFieldPending 同时把不可用状态列为无有效详情值的情况。

来源:可用性与色调、快速完成分支。

3. 四个服务器并行探测

正常路径遍历 stunServers,将地址、类型、国家和组织重置为 StatusTesting,清空 country_code,将 natTypeCode 设为 undefined,然后立即调用各自的 checkSTUNServer(server)。

单服务器探测会:

  1. 重建 sdpLog 数组并关闭日志展开状态。
  2. 记录以 performance.now() 为基准的毫秒偏移日志。
  3. 用单个 STUN URL 构造 RTCPeerConnection。
  4. 检查 createDataChannel、createOffer 是否存在,识别隐私扩展提供的空壳对象。
  5. 将真实连接加入 activeConnections。
  6. 注册 ICE 收集状态、候选错误与候选回调。
  7. 创建空标签数据通道,创建 Offer 并设置本地描述。
  8. 设置 5000 毫秒兜底计时器。

已读取路径没有调用 getUserMedia,也没有建立远端会话或调用 setRemoteDescription;其目的在于触发候选收集,而非发起音视频通话。

4. 严格筛选候选,避免把本地候选当作 STUN 结果

候选处理先写日志,再检查是否已经完成,然后从空格分隔字符串的第八项读取类型。

javascript
1 const candidate = event.candidate.candidate; 2 log(`candidate: ${candidate}`); 3 if (settled) return; 4 const type = candidate.split(' ')[7]; 5 6 // Only server-reflexive / peer-reflexive candidates represent a 7 // STUN answer. Host candidates (with or without mDNS) don't prove 8 // STUN worked and we must not display them as the "STUN IP". 9 if (type !== 'srflx' && type !== 'prflx') return; 10 11 const ipMatch = CANDIDATE_IP_RE.exec(candidate); 12 if (!ipMatch) return; 13 succeedWith(ipMatch[0], candidate);

Source: WebRtcTest.vue

host、relay 和未知类型都不会使本轮成功。空候选只记录 end-of-candidates,不会立即宣告失败;后续由超时结束。地址提取使用覆盖常见 IPv4/IPv6 形式的正则,而非完整 SDP 解析器,因此不应将其视为独立的严格 IP 校验 API。

5. 地址成功后再补齐归属

succeedWith 在等待归属查询前就设置 settled = true 并清除 5 秒计时器,防止后续候选重复覆盖结果。随后写入 IP、翻译后的候选类型和无语言依赖的类型码。

第一次更新 IPArray 只带 { ip, country: '' };lookupMaxmind(ip) 返回数据后,再追加包含国家代码、位置、ASN、组织的记录。代码采用追加而不是原位替换,供 watch(IPArray) 调用 store.updateAllIPs。Store 是否合并或去重,不能从组件实现确认。

若归属查询返回空值,只将国家与组织置为 StatusError,保留已经成功的 IP 和候选类型。成功路径最后关闭连接、从集合移除并完成 Promise。

来源:成功与归属补全、Store 同步。

6. 整轮完成是一个有上限的快照

javascript
1 const allSettledPromise = Promise.allSettled(promises); 2 const timeoutPromise = new Promise((resolve) => setTimeout(resolve, 6000)); 3 return Promise.race([allSettledPromise, timeoutPromise]).then(() => { 4 store.setLoadingStatus('WebRTC', true); 5 emitWebrtcFinished(); 6 });

Source: WebRtcTest.vue

这里有两层不同的时间边界:5 秒用于单连接等待有效候选,6 秒用于整轮等待各服务器流程。由于成功候选会取消其 5 秒计时器并等待定位查询,6 秒快照可能在归属补齐之前发布。

Promise.race 不取消落后的任务。因此,“已发出完成事件”不代表全部底层异步任务一定已结束;定位结果仍可能在事件之后写回卡片与 IPArray。代码在完成处调用 setLoadingStatus('WebRTC', true),其布尔值不能脱离 Store 实现按名称猜测为“仍在加载”。

Loading diagram...

Source: WebRtcTest.vue

图中的归属查询没有由组件设定独立超时;整轮 6 秒边界可先于查询返回。网络错误事件以及 Offer 失败的细分行为见下文。

状态模型、结果事件与内部接口

服务器记录和类型码

每项 stunServers 记录包含 id、url、ip、natType、country、country_code、org、sdpLog、sdpOpen;执行过程中再写入 natTypeCode。显示字段使用当前语言的状态文本,类型码用于下游消费时避免依赖翻译字符串。

字段或状态用途与生命周期
ip等待/检测/失败/不可用标签,或成功提取的地址
natType候选类型的翻译结果,或对应状态标签
natTypeCode检测开始为 undefined;成功为 srflx/prflx;失败为 error;不可用为 unavailable
country_code初始及重测时为空;归属成功后填充
sdpLog每个服务器的相对时间日志;每次实际 STUN 检测重建
IPArray组件内累积的地址记录,监听后推送到 Store;整轮重测没有清空它
hasEverSettled发布快照时设为 true,表示至少完成过一轮,不在重测时重置
activeConnections尚需关闭的连接集合,不是结果存储

determineNATType 和 natTypeCodeOf 都支持 host、srflx、prflx、relay,但正常成功路径此前已过滤为两种反射候选。因此不能因为映射函数包含四种类型,就认为界面会接受四种类型作为 STUN 成功结果。

来源:初始状态、类型与事件、重测重置。

报告快照的真实结构

javascript
1const emitWebrtcFinished = () => { 2 hasEverSettled.value = true; 3 emitAppEvent('webrtc:finished', { 4 servers: stunServers.map((server) => ({ 5 id: server.id, 6 url: server.url, 7 ip: server.ip, 8 natTypeCode: server.natTypeCode, 9 country_code: server.country_code, 10 org: server.org, 11 })), 12 }); 13};

Source: WebRtcTest.vue

快照不包含原始 SDP 日志、翻译后的 natType、国家显示名称或 ASN。源码注释说明下游报告构建器会丢弃没有 natTypeCode 的等待项;本页没有读取该构建器,因此这属于组件声明的下游约定,而不是对报告实现的独立验证。

内部接口参考

这些函数位于 Vue <script setup> 中,是组件内部接口,不是 HTTP API。源码为 JavaScript,以下返回说明依据函数体,不代表仓库声明了 TypeScript 类型。

接口参数返回与副作用
checkSTUNServer(stun)一条可变服务器记录返回 Promise;通过修改该记录表达结果,正常结束不返回结果对象
checkAllWebRTC(isRefresh)刷新标志,真值时记录分析事件异步执行整轮;API 不可用时直接发布结果,否则在全 settled 或 6 秒时发布
determineNATType(candidate)ICE 候选字符串按第八项返回翻译后的类型;其他值为 unknown 对应翻译
natTypeCodeOf(candidate)ICE 候选字符串返回四种已知类型之一或 unknown
emitWebrtcFinished()无设置粘性完成标志,发布 servers 快照
webrtc:run 命令处理器可省略的对象,isRefresh = false返回 waitForAppEvent('webrtc:finished') 的等待结果

异常不是这些接口的结构化返回值:常规失败写入字段并完成 Promise。lookupMaxmind 的错误吞并契约由调用点注释说明,本页未核验其实现;不应把组件视为能吸收任何第三方实现异常的通用安全包装器。

来源:单服务器入口、整轮入口与命令。

配置与使用示例

固定参数

已读取实现中的参数是代码常量或内联值,没有发现该组件读取 STUN 环境变量或接受服务器配置属性。

选项类型当前值作用
Google STUN字符串stun.l.google.com:19302id = google
BlackBerry STUN字符串stun.voip.blackberry.com:3478id = blackberry
Twilio STUN字符串global.stun.twilio.comid = twilio,代码未显式写端口
Cloudflare STUN字符串stun.cloudflare.comid = cloudflare,代码未显式写端口
单连接候选超时毫秒数值5000没有有效反射候选时进入错误状态
整轮等待上限毫秒数值6000限制统一完成事件的等待时间,不取消工作
数据通道标签字符串''触发数据通道相关协商
isRefresh命令输入标志false控制刷新点击的分析事件
simpleModeStore 偏好本页未核验默认值被读取为 isSimpleMode;模板行为未核验

来源:服务器列表、连接与超时、整轮参数。

源码示例:触发候选收集并保留失败日志

javascript
1 pc.createDataChannel(''); 2 pc.createOffer().then((offer) => { 3 // Offer SDP is multi-line; record it as a single block so the 4 // Collapsible <pre> renders it verbatim. 5 log(`createOffer ok\n--- Offer SDP ---\n${offer.sdp}--- end SDP ---`); 6 return pc.setLocalDescription(offer); 7 }).then(() => { 8 log('setLocalDescription ok'); 9 }).catch((error) => { 10 log(`offer/setLocalDescription failed: ${error?.message || error}`); 11 });

Source: WebRtcTest.vue

该片段展示了诊断工具的关键取舍:保留 Offer SDP 和具体错误,但不在 Promise 拒绝时直接断言隐私策略或网络根因;通常仍由候选等待超时统一收敛为失败。

源码示例:区分明确阻止与普通异常

javascript
1 const constructionBlocked = error?.name === 'NotAllowedError' 2 || error?.name === 'NotSupportedError' 3 || (error instanceof TypeError && /not a constructor/i.test(error?.message || '')); 4 if (constructionBlocked) { 5 log(`construction blocked: ${error?.message || error}`); 6 failWith('StatusUnavailable'); 7 return; 8 }

Source: WebRtcTest.vue

这是包围连接建立流程的同步 catch 判定。不要把它推广为“所有异步 NotAllowedError 都显示不可用”:Offer 链中的拒绝由上一个示例的 .catch 只记录日志。

故障模式、边界与并发

故障如何呈现

情况当前处理排查含义
RTCPeerConnection 全局 API 缺失所有服务器立即不可用不等待网络超时
对象缺少 createDataChannel 或 createOffer丢弃对象引用,标记不可用不调用可能不存在的 close()
同步异常满足阻止条件标记不可用作为保护性状态呈现
其他同步异常console.error、写日志、标记错误不应解读为明确的隐私保护
onicecandidateerror记录错误码、文本和 URL不直接终止检测,仍可能得到有效候选
Offer 或本地描述 Promise 拒绝仅写入日志通常等待 5 秒兜底
候选收集结束但无合格 IP记录结束标记不提前失败,仍等超时
只有 host/mDNS 或 relay 候选忽略为成功结果不能证明 STUN 成功,也不能确定失败根因
归属查询返回空值国家和组织失败,IP 保留应区分地址获取与定位服务两个层面

源码刻意不区分“超时”和“mDNS 隐私”,因为两者不是互斥原因。把无有效候选直接解释为 mDNS 保护会造成误判。

来源:失败收敛、事件与异常。

单连接去重不等于整轮互斥

单连接的 settled 保证第一个成功候选之后的候选不再改写结果,也使超时失败不覆盖已成功的 IP。但 checkAllWebRTC 本身没有执行中锁、代次编号或旧任务取消逻辑。

因此,如果外部允许重叠调用,会有以下风险:

  • 多轮检测操作同一个服务器对象,旧轮定位返回后可能覆盖新轮字段。
  • 日志辅助函数始终向当前 stun.sdpLog 写入;新轮替换数组后,旧轮仍可能向新数组追加日志。
  • 命令等待的是下一次同名完成事件,组件没有携带请求 ID;外部事件工具是否进一步隔离并发,本页未核验。
  • IPArray 在刷新时不清空,重复地址可能持续进入 Store 更新路径。

这些是调用点可见的边界,不代表已经验证了全局界面会允许用户重复并发触发。

卸载时关闭连接,但不等于取消所有异步任务

onBeforeUnmount 遍历 activeConnections 调用 close(),然后清空集合,避免 ICE 收集继续占用资源。正常成功和失败路径也会关闭并移除连接。

不过,卸载钩子没有清除各闭包内的计时器,没有取消定位请求,也没有终止整轮 Promise.race。所以不能将资源关闭描述为完整的请求取消机制。需要强化这一行为时,应同时考虑计时器、定位查询、状态写回和完成事件的生命周期,而不只是 RTCPeerConnection.close()。

来源:连接关闭、成功关闭、卸载清理。

性能、运维与隐私注意事项

  1. 网络并行度。 每轮最多创建四个独立连接;组件没有重试循环。所有服务不可达时,常规路径约在单服务器兜底时间后收敛,而不是顺序等待四次超时。
  2. 定位查询成本。 每个成功服务器都会调用一次 lookupMaxmind(ip);组件没有按 IP 去重或缓存。相同出口地址是否共享缓存取决于未核验的定位模块。
  3. 日志排查顺序。 先查看构造/空壳识别,再查看 Offer、本地描述、ICE 错误和候选类型,最后确认是否出现 resolved IP。日志使用每台服务器各自的毫秒偏移,不能把它当作全局绝对时间。
  4. 外部交互。 测试主动使用四个外部 STUN 地址,并把得到的 IP 传给定位模块。归属查询的实际网络目的地和数据保留策略需要另查其实现。
  5. 诊断日志可能敏感。 日志保留完整 Offer SDP、候选和解析 IP。截图、复制日志或提交问题前应检查其中地址信息;仅知道初始化了 createMaskGate,不足以保证日志被遮罩。
  6. 没有独立持久化实现。 已读取组件使用响应式内存状态和 Store 更新,没有直接数据库或本地存储操作。全局 IP 历史是否持久化不属于本页已验证范围。

来源:日志与时间戳、定位与候选日志、并行及 Store 同步。

扩展与验证建议

以下是基于现有结构的维护建议,不是已实现功能或已验证测试:

  • **增减 STUN 服务:**修改 stunServers,保留稳定 id 与完整显示状态字段,因为报告快照直接使用它们。
  • **修改候选策略:**同时检查候选过滤、类型映射、结果说明和报告语义;单独放开 host 会破坏“STUN 地址确实来自反射候选”的现有约束。
  • **增加可取消运行:**应使用每轮标识隔离状态写回,并协调命令等待、事件发布、定位请求和卸载清理。
  • **调整超时:**分别评估单连接候选期限和整轮快照期限;提高整轮上限不会自动为定位查询加入取消机制。

本次取证未读取 WebRTC 专项测试,无法确认上述边界的自动化覆盖。建议回归验证 API 缺失、构造阻止、空壳 API、只有 host 候选、有效 IPv4/IPv6、重复候选、ICE 错误后成功、Offer 拒绝、定位为空或延迟、重叠运行以及运行中卸载。并应验证“归属失败不覆盖成功 IP”“6 秒事件不被误认作底层工作全部结束”两项核心语义。

相关链接

关于 DNS 泄露、HTTP 出口地址、定位服务和报告生成,应分别参考对应专题;本次运行上下文未提供这些目录项的实际路径,因此不构造未经验证的 Wiki 链接。

Sources

(1 files)