在多账号防关联、跨境业务环境隔离以及高匿名网络数据采集工程中,许多开发者常遇到一个极其隐蔽的问题:
明明本地已经全局配置了 S5 协议或 HTTP 代理网关,访问
ipinfo.io显示的出口 IP 也是目标地域节点,但目标风控系统(如 Cloudflare、TikTok、Facebook 等)却依然能精准识别出本机的真实内网/公网 IP 地址。
导致这种"隐形破防"的核心元凶,通常就是浏览器底层自带的实时音视频通信协议------WebRTC(Web Real-Time Communication)。
本文将深入解析 WebRTC 穿透 NAT 的底层网络通信协议(STUN/TURN/ICE),剖析其如何绕过普通会话代理窃取真实 IP,并使用原生 Python 构建一套针对 WebRTC 泄漏风险的自动化检测系统。
一、 WebRTC 为什么能"穿透代理"探测真实 IP?
1. P2P 穿透与 ICE 框架机制
WebRTC 最初的设计目的是为了让两个浏览器节点之间实现低延迟的点对点(P2P)音视频通话,尽量避免经由中间服务器转发流量。
为了在复杂的 NAT(网络地址转换)与防火墙环境下建立直连通道,WebRTC 引入了 ICE(Interactive Connectivity Establishment,交互式连通建立) 框架。
text
复制编辑 & 运行
[浏览器客户端] │ ├─── 1. 普通 HTTP/HTTPS 业务流量 ──────> [走配置的本地 S5 / HTTP 代理] │ └─── 2. WebRTC 发起 UDP STUN 探测 ───> [直接经本地物理网卡旁路直出!] │ ▼ [公网 STUN 服务器] │ (返回真实出网 IP) ▼ [JavaScript 拿到真实宿主机 IP]
2. STUN 协议(RFC 5389)的旁路直连
在发起 P2P 会话前,浏览器内部的 WebRTC 引擎会自动向公网公共 STUN(Session Traversal Utilities for NAT)服务器 发送 UDP 数据报文(默认端口 3478)。
- 浏览器的大多数会话代理插件(或系统常规代理)默认仅劫持 TCP 流量(如 HTTP/HTTPS);
- WebRTC 发出的 STUN 探测包通常走 UDP 报文,且由浏览器内核直接调用系统物理网卡接口发出;
- STUN 服务器收到 UDP 包后,会在响应报文(Binding Response)的属性字段
XOR-MAPPED-ADDRESS中填入其看到的客户端真实物理 IP; - 浏览器网页中的 JavaScript 脚本通过调用
RTCPeerConnection提供的事件监听接口,可以直接读取到这个原生公网 IP,进而上报给风控服务端。
这导致客户端配置的代理通道被完全"旁路绕过",造成事实上的真实物理环境泄漏。
二、 Python 原生实现:STUN 协议交互与真实 IP 探查
以下代码不依赖任何第三方封装,完全基于 Python 标准库 socket 与 struct 实现标准 RFC 5389 STUN 客户端,模拟探测本机对外暴露的真实映射地址:
python
复制编辑 & 运行
import socket import struct import random import logging logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") # RFC 5389 STUN 报文规范常量 STUN_MAGIC_COOKIE = 0x2112A442 BINDING_REQUEST = 0x0001 BINDING_RESPONSE = 0x0101 XOR_MAPPED_ADDRESS = 0x0020 MAPPED_ADDRESS = 0x0001 class STUNProbe: """基于 RFC 5389 的原生 STUN 探测器""" @staticmethod def generate_transaction_id() -> bytes: """生成 12 字节的随机事务 ID""" return bytes([random.randint(0, 255) for _ in range(12)]) @classmethod def get_public_ip_via_stun(cls, stun_host: str = "stun.l.google.com", stun_port: int = 19302, timeout: float = 3.0): """向目标 STUN 服务器发送 Binding Request 并解析返回的反射真实 IP""" sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(timeout) trans_id = cls.generate_transaction_id() # 组装 STUN 头部 (20 字节): Type (2B) + Length (2B) + Magic Cookie (4B) + Transaction ID (12B) msg_type = BINDING_REQUEST msg_len = 0 header = struct.pack("!HHI", msg_type, msg_len, STUN_MAGIC_COOKIE) + trans_id logging.info(f"[*] 正在向 STUN 服务器 [{stun_host}:{stun_port}] 发送 UDP 穿透探测...") try: sock.sendto(header, (stun_host, stun_port)) data, addr = sock.recvfrom(2048) if len(data) < 20: raise ValueError("STUN 响应报文长度不足") resp_type, resp_len, magic = struct.unpack("!HHI", data[:8]) resp_trans_id = data[8:20] if resp_type != BINDING_RESPONSE or resp_trans_id != trans_id: raise ValueError("非法的 STUN 响应或事务 ID 不匹配") # 遍历解析返回属性 offset = 20 while offset < 20 + resp_len: attr_type, attr_len = struct.unpack("!HH", data[offset:offset+4]) attr_data = data[offset+4 : offset+4+attr_len] offset += 4 + attr_len # 兼容 4 字节边界对齐 (RFC 5389) if attr_len % 4 != 0: offset += 4 - (attr_len % 4) # 解析 XOR-MAPPED-ADDRESS if attr_type == XOR_MAPPED_ADDRESS: _, family, xor_port = struct.unpack("!BBH", attr_data[:4]) # 异或解码端口 port = xor_port ^ (STUN_MAGIC_COOKIE >> 16) if family == 0x01: # IPv4 xor_ip = struct.unpack("!I", attr_data[4:8])[0] ip_int = xor_ip ^ STUN_MAGIC_COOKIE ip = socket.inet_ntoa(struct.pack("!I", ip_int)) return ip, port # 解析 MAPPED-ADDRESS (传统格式兼容) elif attr_type == MAPPED_ADDRESS: _, family, port = struct.unpack("!BBH", attr_data[:4]) if family == 0x01: ip = socket.inet_ntoa(attr_data[4:8]) return ip, port return None, None finally: sock.close() if __name__ == '__main__': # 公共测试 STUN 节点 (包含 Google / 知名 STUN 服务) test_servers = [ ("stun.l.google.com", 19302), ("stun1.l.google.com", 19302), ("stun.cloudflare.com", 3478) ] for host, port in test_servers: try: detected_ip, detected_port = STUNProbe.get_public_ip_via_stun(host, port) if detected_ip: print("\n================== 穿透检测结果 ==================") print(f"[!] 物理物理出网真实 IP: {detected_ip}") print(f"[!] 映射外部临时端口: {detected_port}") print("==================================================\n") break except Exception as e: logging.warning(f"节点 {host}:{port} 探测失败: {e}")
三、 工程防御与彻底杜绝泄漏的最佳策略
面对 WebRTC 的旁路刺探,工业级采集与多账号隔离系统通常采用以下多维防御方案:
- 禁用或替换浏览器 WebRTC 行为(指纹浏览器标准配置):
- 彻底关闭模式(Disabled): 在 Chromium 内核启动参数中加入
--disable-webrtc,彻底阻止任何 UDP 探针发出; - 伪装替换模式(Altered / Replace): 专业指纹浏览器(如 AdsPower、Hubstudio)会在内核层 Hook
RTCPeerConnection接口,强制将其返回的候选地址伪造为您指定的 S5 出口代理 IP,做到天衣无缝。
- 彻底关闭模式(Disabled): 在 Chromium 内核启动参数中加入
- 全协议会话代理(S5 UDP Associate):
如果业务必须使用音视频交互,应确保使用的底层中继协议完整支持 RFC 1928 S5 UDP 转发机制(CMD 0x03 - UDP ASSOCIATE),将所有 UDP 数据报文全部强行导入加密中继隧道中,严防物理网卡旁路直通。
【技术参考】:本文基于 RFC 5389 与 WebRTC 核心通信架构设计撰写,由博主 @jxys5 原创实战总结。