
之前写过一篇如何通过网络层面阻断B站直播P2P上传流量的专栏文章,大致思路是通过浏览器F12开发人员工具分析P2P相关请求域名domain和url,然后通过Adblock自定义规则的方式进行阻断,这个方法我自己用得一直也还不错。
后来有一段时间没怎么看直播,也就没有再对这个方法进行更新维护,最近收到网友反馈,说这个方法现在会把弹幕也一起拦截掉,我半信半疑地打开了几个直播,发现确实如此,这的确不是个小事情。无奈只能再次磨刀霍霍向P2P,哈哈。
首先,随机到b站直播分区打开一个直播间,开启F12开发工具,转到网络选项卡,依据以往的经验,我们直接选择WS筛选器,可以清楚发现b站建立了若干条WebSocket连接,而名称为sub、域为*.网页链接的WS则基本可以确定是b站直播的弹幕推送通道。
可能有的读者对WebSocket不太了解,这里可以简单理解为,网页通过一种特殊的方式建立了与服务器的网络连接,并且通过这条连接双向传送数据。与xhr和fetch相比,WS可以双向自由收发数据,任何一端都可以是发送端或接收端,从而摆脱传统HTTP请求只能由客户端发起、服务器被动发送响应的限制。对于b站直播客户端来说,WS既可以实现加密传输(WSS,WebSocket Secure),还可以实现服务端向客户端实时推送弹幕,从而降低负载和延迟。

F12开发人员工具
从图中可以看出,网页与服务器尝试建立了很多连接,但都处于挂起状态,也就是尚未连接成功。点击任意一条名称为sub的记录,可以看出请求的URL。

名为sub的WS所请求的URL
还记得我们之前的两条策略吗?分别是
@@*.chat.bilibili.com/sub *.chat.bilibili.com 这条WS恰好命中了我们的黑名单策略,还恰好绕过了白名单策略。怪不得它会一直挂起。
解决方法当然也很简单,比如,添加一条如下所示的白名单规则即可。
@@*.chat.bilibili.com:2245/sub 也许你要问:为什么不直接修改之前的白名单规则?emm,我发现有的直播间仍然使用旧的URL,而有的直播间使用新的URL,我也不知道这是为什么。而且我对正则表达式也不太了解(主要是懒),如果有更好的正则表达写法,欢迎留言提出。
修改Adblock之后刷新直播间页面,再次观察WS连接记录,如图

可以发现,WS连接的数量减少了很多,这符合我们的思维:WS可以自由双向发送数据,因此显然没有必要为同类型的数据建立多条WS连接。同时WS连接请求得到了服务器的响应,并且已经成功建立连接。此时弹幕也恢复了正常。Yes!
问题得到了解决,但我自己却不是特别满意,因为我的余光看到了控制台输出的一大堆错误和警告信息。

糟糕的控制台报错信息
仔细分析这些报错信息,容易猜测这是因为我们将P2P相关的网络请求阻断了,从而导致P2P连接建立失败,触发网页的重试重连机制,导致无限循环。这当然令人不爽!
那有没有更好的解决办法呢?之前的文章中有读者提到,可以”直接禁用webRTC“来阻断P2P上传。这的确也是不错的思路。那么如何进行具体分析呢?我们不妨从这些报错信息入手。

切换到控制台视图,将滑块拉到最上方从头看起,可以猜测实现P2P功能的组件名为Misaka Network,该组件首先进行Network Check网络检查,并给出support的判断,进一步输出网络NAT类型,最后进行P2P连接的创建。P2P组件的取名确实很有趣,很容易让人联想到《某科学的超电磁炮》番剧中的御坂网络和Last Order,两者确实有一定的相同之处,巧妙而风趣。
分析其连接思路之后,我们可以开始确定入手点。要想阻止P2P上传,要从哪里下手?每个人给出的答案可能不尽相同,但我盯上的是Network Check: Supported的一行。如果有办法修改这个判断结果,或者干扰Network Check的判断,使其误以为当前的网络不支持P2P,网页自然不会再继续进行P2P连接,也就更不会无限重试。
说做就做。点击对应消息右侧的蓝色链接,开发人员工具自动定位到了源代码标签页中的对应的发起代码所在行。该行代码处在key: "checkP2PSupport" 下方的value: function() 中,key名则印证了我们的想法。

为了便于阅读代码,我将该段function代码拷贝到了VSCode中,同时也复制在下文中,以便于读者追踪。
function a() {
var t, e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : window, n = !!e.WebSocket, r = !(!e.MediaSource && !e.WebKitMediaSource), i = !!(e.RTCPeerConnection || e.mozRTCPeerConnection || e.webkitRTCPeerConnection), o = e.RTCDataChannel || e.DataChannel, s = !!o, a = !!s && (null == o || null === (t = o.prototype) || void 0 === t ? void 0 : t.hasOwnProperty("onbufferedamountlow")), c = !!e.ReadableStream, u = n && r && i && s && a && c, l = "Misaka Network Check: ".concat(u ? "Supported" : "Unsupported");
if ("development" == pt.V.curNodeEnv) {
var h = "[SistersPlayer] ".concat(l, "\n\t") + "WebSocket: ".concat(n, "\n\t") + "MediaSource: ".concat(r, "\n\t") + "RTCPeerConnection: ".concat(i, "\n\t") + "RTCDataChannel: ".concat(s, "\n\t") + "RTCDataChannel onbufferedamountlow: ".concat(a, "\n\t") + "Fetch API ReadableStream: ".concat(c);
console.log(h)
} else
console.log("[SistersPlayer] ".concat(l));
return u
} 从 ”Supported“ 结果的打印开始分析代码,我大致将该函数内的主要变量及其作用梳理为下图。其中左侧为变量名,右侧为对应的判断条件或作用描述,中间用一个制表符分开。
n websocket support
r MediaSource || WebKitMediaSource
i RTCPeerConnection || mozRTCPeerConnection || webkitRTCPeerConnection
s o
a s && (null == o || null === (t = o.prototype) || void 0 === t ? void 0 : t.hasOwnProperty("onbufferedamountlow"))
c ReadableStream
o RTCDataChannel || DataChannel
u n && r && i && s && a && c 至此,该段代码的逻辑基本清楚。控制打印 "Supported" 的u变量取决于n, r, i, s, a, c这几个变量,而这些变量分别用于检测P2P所需的各项功能(是否受支持)。
由于u取各个变量的交集,我们只需让其中一个变量为假,即可让代码认为P2P不受支持,从而放弃建立P2P连接。在这里我自己选择了i变量,虽然使该变量为假需要同时使三个功能为假(关闭三个功能),但这三个功能明显与P2P关联较为紧密,若关闭了作用较为重要的功能,很可能会误伤其他操作的使用。读者若有兴趣,可以尝试对其他功能进行研究,最终效果应该一致。
通过网上资料查阅和实践,想要彻底关闭这些功能可能需要修改浏览器的flag设置,这显然较为繁琐。于是我们采取较为简单的措施,即通过tampermonkey执行JS delete代码来删除对应的功能,示例如下。
delete window.RTCPeerConnection;
delete window.mozRTCPeerConnection;
delete window.webkitRTCPeerConnection; 添加脚本后刷新页面,观察控制台输出的信息,可以看到Network Check 一行已经给出了Unsupported结果,至此目的达成。

喜欢这篇文章的话,不妨点个赞再走呗~
要是能投个币就更好啦~