WebRTC 指纹与隐私
概述
WebRTC (Web Real-Time Communication) 是一种支持网页浏览器进行实时语音、视频通信和数据共享的技术。然而,WebRTC 也可能泄露用户的真实 IP 地址和其他设备信息,成为指纹识别和隐私泄露的重要途径。
📚 前置知识
在开始本配方之前,建议先掌握以下内容:
💡 提示: WebRTC 是最容易泄露真实 IP 的途径之一。即使使用 VPN 或代理,WebRTC 也可能暴露你的本地 IP。了解这一机制对于隐私保护和自动化爬虫至关重要。
基础概念
定义
WebRTC 是一组标准化的 API,允许浏览器和移动应用程序进行点对点的实时通信,无需安装插件或第三方软件。
主要组件:
- getUserMedia: 访问摄像头和麦克风
- RTCPeerConnection: 建立点对点音视频通信
- RTCDataChannel: 点对点数据传输
隐私风险:
- 泄露真实 IP 地址(绕过 VPN/代理)
- 收集设备指纹信息
- 获取本地网络拓扑
- 暴露媒体设备信息
核心原理
WebRTC 连接建立过程
1
2
3
4
5
6
7
8
9
10
11
12
| 客户端 A 客户端 B
| |
|-- 1. 创建 RTCPeerConnection ----------->|
| |
|-- 2. 收集 ICE Candidates --------------->|
| (包含本地/公网 IP) |
| |
|<- 3. 交换 SDP Offer/Answer --------------|
| |
|-- 4. 建立点对点连接 -------------------->|
| |
|<======= 5. 实时通信 =====================>|
|
ICE (Interactive Connectivity Establishment)
WebRTC 使用 ICE 协议收集候选连接路径:
- Host Candidate: 本地 IP 地址(局域网 IP)
- Server Reflexive Candidate: 公网 IP(通过 STUN 服务器获取)
- Relay Candidate: 中继 IP(通过 TURN 服务器)
这些信息可以被 JavaScript 访问,导致 IP 泄露。
详细内容
IP 地址泄露
1. 本地 IP 泄露
即使用户使用 VPN,WebRTC 仍可能泄露真实的本地 IP。
泄露机制:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // 创建 RTCPeerConnection
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});
// 创建虚拟数据通道
pc.createDataChannel("");
// 创建 Offer
pc.createOffer().then((offer) => pc.setLocalDescription(offer));
// 监听 ICE 候选
pc.onicecandidate = (ice) => {
if (ice && ice.candidate && ice.candidate.candidate) {
const candidate = ice.candidate.candidate;
console.log("ICE Candidate:", candidate);
tags: ["Web", "浏览器指纹", "Canvas指纹", "WebAssembly", "代理池", "视频App"]
// 解析 IP 地址
const ipRegex =
/([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/;
const match = candidate.match(ipRegex);
if (match) {
console.log("Leaked IP:", match[1]);
}
}
};
|
泄露的信息:
- 内网 IPv4 地址 (192.168.x.x, 10.x.x.x)
- 内网 IPv6 地址
- 公网 IPv4/IPv6 地址
- mDNS 候选(某些浏览器)
2. 公网 IP 泄露
通过 STUN 服务器获取用户的真实公网 IP,绕过 VPN/代理。
完整示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| function getIPs(callback) {
const ips = [];
const pc = new RTCPeerConnection({
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
{ urls: "stun:stun1.l.google.com:19302" },
],
});
pc.createDataChannel("");
pc.createOffer().then((offer) => pc.setLocalDescription(offer));
pc.onicecandidate = (ice) => {
if (!ice || !ice.candidate || !ice.candidate.candidate) {
return;
}
const candidate = ice.candidate.candidate;
const ipRegex =
/([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/;
const match = candidate.match(ipRegex);
if (match && !ips.includes(match[1])) {
ips.push(match[1]);
callback(match[1], candidate);
}
};
}
// 使用
getIPs((ip, candidate) => {
console.log("IP:", ip);
console.log(
"Type:",
candidate.includes("typ host")
? "Local"
: candidate.includes("typ srflx")
? "Public"
: "Relay"
);
});
|
设备指纹信息
1. 媒体设备枚举
WebRTC 可以枚举用户的音视频设备:
1
2
3
4
5
6
7
8
9
10
| navigator.mediaDevices.enumerateDevices().then((devices) => {
devices.forEach((device) => {
console.log({
kind: device.kind, // "audioinput", "videoinput", "audiooutput"
label: device.label, // 设备名称
deviceId: device.deviceId, // 唯一标识符
groupId: device.groupId, // 设备组
});
});
});
|
指纹用途:
deviceId 可作为持久化标识符- 设备数量和类型组合形成独特指纹
- 设备名称可能暴露硬件信息
2. 编解码器支持
不同设备支持不同的音视频编解码器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| const pc = new RTCPeerConnection();
pc.createOffer().then((offer) => {
const codecs = [];
// 解析 SDP 获取支持的编解码器
offer.sdp.split("\r\n").forEach((line) => {
if (line.startsWith("a=rtpmap:")) {
codecs.push(line);
}
});
console.log("Supported codecs:", codecs);
});
|
3. RTC 统计信息
1
2
3
4
5
6
7
8
9
10
11
12
| const pc = new RTCPeerConnection();
pc.getStats().then((stats) => {
stats.forEach((report) => {
console.log({
type: report.type,
id: report.id,
timestamp: report.timestamp,
...report,
});
});
});
|
可获取的信息:
隐私保护检测
检测 WebRTC 泄露
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| async function detectWebRTCLeak() {
const results = {
localIPs: [],
publicIPs: [],
devices: [],
leakDetected: false,
};
// 1. 检测 IP 泄露
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});
pc.createDataChannel("");
pc.createOffer().then((offer) => pc.setLocalDescription(offer));
await new Promise((resolve) => {
pc.onicecandidate = (ice) => {
if (!ice || !ice.candidate) {
resolve();
return;
}
const candidate = ice.candidate.candidate;
const ipMatch = candidate.match(/([0-9]{1,3}\.){3}[0-9]{1,3}/);
if (ipMatch) {
const ip = ipMatch[0];
if (
ip.startsWith("192.168.") ||
ip.startsWith("10.") ||
ip.startsWith("172.")
) {
results.localIPs.push(ip);
} else {
results.publicIPs.push(ip);
results.leakDetected = true;
}
}
};
setTimeout(resolve, 2000);
});
// 2. 检测设备枚举
try {
const devices = await navigator.mediaDevices.enumerateDevices();
results.devices = devices.map((d) => ({
kind: d.kind,
label: d.label,
hasId: !!d.deviceId,
}));
} catch (e) {
console.log("Device enumeration blocked");
}
return results;
}
// 使用
detectWebRTCLeak().then((results) => {
console.log("WebRTC Leak Detection Results:", results);
if (results.leakDetected) {
console.warn("⚠️ WebRTC IP leak detected!");
}
});
|
实战示例
示例 1: 完整的 IP 泄露检测工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
| class WebRTCLeakDetector {
constructor() {
this.ips = new Set();
this.candidates = [];
}
async detect() {
return new Promise((resolve) => {
const pc = new RTCPeerConnection({
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
{ urls: "stun:stun1.l.google.com:19302" },
{ urls: "stun:stun.services.mozilla.com" },
],
});
pc.createDataChannel("leak-test");
pc.createOffer()
.then((offer) => pc.setLocalDescription(offer))
.catch((err) => console.error(err));
pc.onicecandidate = (event) => {
if (!event || !event.candidate) {
pc.close();
resolve(this.generateReport());
return;
}
this.processCandidate(event.candidate);
};
// 超时保护
setTimeout(() => {
pc.close();
resolve(this.generateReport());
}, 5000);
});
}
processCandidate(candidate) {
this.candidates.push(candidate.candidate);
const parts = candidate.candidate.split(" ");
const ip = parts[4];
const type = parts[7];
if (ip && this.isValidIP(ip)) {
this.ips.add(ip);
console.log(`Found ${type} IP: ${ip}`);
}
}
isValidIP(str) {
// IPv4
const ipv4 = /^(\d{1,3}\.){3}\d{1,3}$/;
// IPv6
const ipv6 = /^([\da-f]{1,4}:){7}[\da-f]{1,4}$/i;
return ipv4.test(str) || ipv6.test(str);
}
generateReport() {
const report = {
ips: Array.from(this.ips),
local: [],
public: [],
ipv6: [],
candidates: this.candidates,
};
report.ips.forEach((ip) => {
if (ip.includes(":")) {
report.ipv6.push(ip);
} else if (this.isLocalIP(ip)) {
report.local.push(ip);
} else {
report.public.push(ip);
}
});
return report;
}
isLocalIP(ip) {
return (
ip.startsWith("192.168.") ||
ip.startsWith("10.") ||
ip.match(/^172\.(1[6-9]|2\d|3[01])\./)
);
}
}
// 使用
const detector = new WebRTCLeakDetector();
detector.detect().then((report) => {
console.log("=== WebRTC Leak Report ===");
console.log("Local IPs:", report.local);
console.log("Public IPs:", report.public);
console.log("IPv6:", report.ipv6);
if (report.public.length > 0) {
console.warn("⚠️ Public IP leak detected!");
}
});
|
示例 2: 禁用 WebRTC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // 方法1: 覆盖 RTCPeerConnection (不推荐,可能破坏功能)
(function () {
"use strict";
const noop = function () {};
const noopPromise = function () {
return Promise.resolve();
};
window.RTCPeerConnection = function () {
return {
createOffer: noopPromise,
createAnswer: noopPromise,
setLocalDescription: noopPromise,
setRemoteDescription: noopPromise,
close: noop,
addEventListener: noop,
};
};
console.log("WebRTC has been disabled");
})();
// 方法2: 浏览器设置
// Chrome: chrome://flags/#enable-webrtc-hide-local-ips-with-mdns
// Firefox: media.peerconnection.enabled = false
// 方法3: 浏览器扩展
// - WebRTC Leak Prevent (Chrome)
// - Disable WebRTC (Firefox)
|
示例 3: 设备指纹采集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
| async function collectWebRTCFingerprint() {
const fingerprint = {};
// 1. 媒体设备
try {
const devices = await navigator.mediaDevices.enumerateDevices();
fingerprint.devices = devices.map((d) => ({
kind: d.kind,
id: d.deviceId,
groupId: d.groupId,
}));
fingerprint.deviceCount = {
audioinput: devices.filter((d) => d.kind === "audioinput").length,
videoinput: devices.filter((d) => d.kind === "videoinput").length,
audiooutput: devices.filter((d) => d.kind === "audiooutput").length,
};
} catch (e) {
fingerprint.devices = "blocked";
}
// 2. 支持的编解码器
const pc = new RTCPeerConnection();
const offer = await pc.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});
fingerprint.codecs = {
audio: [],
video: [],
};
offer.sdp.split("\r\n").forEach((line) => {
if (line.startsWith("a=rtpmap:")) {
const codec = line.split(" ")[1];
if (line.includes("audio")) {
fingerprint.codecs.audio.push(codec);
} else if (line.includes("video")) {
fingerprint.codecs.video.push(codec);
}
}
});
pc.close();
// 3. RTC 能力
fingerprint.capabilities = {
audio: RTCRtpSender.getCapabilities
? RTCRtpSender.getCapabilities("audio")
: null,
video: RTCRtpSender.getCapabilities
? RTCRtpSender.getCapabilities("video")
: null,
};
return fingerprint;
}
// 使用
collectWebRTCFingerprint().then((fp) => {
console.log("WebRTC Fingerprint:", fp);
// 计算指纹哈希
const fpString = JSON.stringify(fp);
console.log("Fingerprint hash:", hashCode(fpString));
});
function hashCode(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return hash.toString(16);
}
|
最佳实践
用户隐私保护
- 使用浏览器扩展
- WebRTC Leak Prevent
- uBlock Origin (启用隐私过滤)
- Privacy Badger
- 浏览器设置
Firefox:
about:config → media.peerconnection.enabled = falsemedia.peerconnection.ice.default_address_only = truemedia.peerconnection.ice.no_host = true
Chrome:
chrome://flags/#enable-webrtc-hide-local-ips-with-mdns 启用- 使用扩展程序
- VPN 配置
- 确保 VPN 支持 WebRTC 保护
- 启用 VPN 的 IPv6 泄露防护
- 测试 VPN 是否真正阻止 WebRTC 泄露
- 定期检测
网站开发者
- 最小权限原则
- 仅在必要时请求媒体权限
- 明确告知用户为何需要 WebRTC
- 隐私声明
- 提供控制选项
常见问题
Q: VPN 能防止 WebRTC 泄露吗?
A: 不一定。WebRTC 可以绕过 VPN 直接泄露本地和公网 IP:
- 本地 IP: VPN 无法隐藏(192.168.x.x)
- 公网 IP: 可能绕过 VPN 隧道泄露真实 IP
- 解决方案: 禁用 WebRTC 或使用支持 WebRTC 保护的 VPN
Q: 为什么浏览器允许 WebRTC 泄露 IP?
A:
- WebRTC 设计用于点对点通信,需要真实 IP 建立连接
- 这是功能需求,不是安全漏洞
- 浏览器逐步增强隐私保护(如 mDNS)
Q: mDNS 是什么?如何保护隐私?
A:
mDNS (Multicast DNS) 是一种隐私保护机制:
- 用随机
.local 地址替代真实本地 IP - Chrome 和 Safari 默认启用
- Firefox 需要手动配置
示例:a1b2c3d4.local 而不是 192.168.1.100
Q: 如何检测网站是否在收集 WebRTC 指纹?
A:
- 打开浏览器开发者工具
- 在 Console 中运行:
1
2
3
4
5
| RTCPeerConnection.prototype._createOffer = RTCPeerConnection.prototype.createOffer;
RTCPeerConnection.prototype.createOffer = function () {
console.trace("WebRTC createOffer called");
return this._createOffer.apply(this, arguments);
};
|
- 查看是否有 WebRTC 调用的堆栈追踪
进阶阅读
官方文档
隐私研究
检测工具
相关章节