前几天在某音符软件刷到了一个视频,是用Openspeedy的变速功能实现网盘下载加速。
在简单了解了一下这个加速软件后,我很好奇为何这个游戏变速器能实际影响网盘的下载速度。是否是百度网盘在用户端限制了用户一段时间内能获取的字节数?而时间加速骗过了这个计时器,让实际传输的数据变多了。
逆向这块我基础很弱,过程中大量使用GPT来协助整理,于是写了这个帖子记录一下这次的成果,希望能有所帮助。
以下均为个人观点,欢迎指正!
本次主要分析的是 Windows 客户端 kernel.dll 3.0.20.234。
先说一下我目前理解的限速框架
下载策略 //大概是拿到一些下载行为用到的控制参数
↓
sl = 120 //实际下载策略里发现的字段,猜测可能是Speed Limit?
↓
客户端换算
120 × 1024
↓
122880 B/s
↓
多个限速来源进行仲裁 //目前观察到4种,而限速逻辑存在一定的优先级区分
↓
得到最终生效的限速值
↓
Token Bucket(令牌桶) //类似一个额度池,定期补充,每次下载数据都会消耗
↓
按经过的时间补充下载额度
↓
下载数据消耗额度
↓
没有额度就等待
先说说这个下载策略,点击下载按钮以后,客户端不是随便找个服务器就开始传文件,而是需要拿到相应的信息,比如:
-去哪个地址下载
-与此次下载相关的控制参数
-速度相关参数
客户端拿到这些信息之后,建立下载任务,才真正开始传输数据。
而sl这个字段,就是其中控制速度的参数(暂且这么认为)
而进一步下追之后,这个值会进入限速的相关逻辑,被转化成
120 × 1024 = 122880 B/s也就是:120 KiB/s
于是这个值进入了客户端的限速系统。
注意,这个数值不是当前实际下载速度,而更类似一个基准,下载速度在其周围浮动。
而我们为什么要说下载策略,而不是直接说这个sl参数呢?
是因为我们发现客户端不只是听sl这一个值,还有其他来源。
例如(此处列举2种):locatedownload → 返回 sl 等下载相关参数
CMS配置 → 也可以提供整体速度限制相关参数
这些来源之间,通过一定的仲裁策略/优先级进行协调
可以理解成:
locatedownload : //此处抽象简单化了
“我这里限 120 KiB/s”
CMS :
“整体下载这里也有一个限制”
客户端比较这些来源决定最后哪个限制值生效
那么最后的实际限速值是怎么拿到的呢?
先解释一个关键点:
locatedownload、CMS、P2P SDK 等限速来源,其参数起作用,并不是按类似“谁最后到就听谁的”这种策略,客户端内部有一套set_sl的仲裁逻辑,会同时记录:
-限速值
-这个值是谁设置的
-当前有没有更高优先级的来源已经生效
我们用百度原始kernel.dll跑了一遍,可以抽象成这样的过程:
初始 / reset
↓
CDN = 524288 B/s
TOTAL = 524288 B/s
locatedownload 到来
sl = 120
↓
120 × 1024 = 122880 B/s
↓
CDN = 122880 B/s ← locatedownload
TOTAL = 122880 B/s ← locatedownload
随后 CMS 配置到来
↓
CDN = 122880 B/s ← locatedownload 保持
TOTAL = 122880 B/s ← CMS 接管
也就是说,同一个 122880 B/s,实际上可以有不同来源,在当时的一次真实长任务下载中, CDN 这一层的 120 KiB/s 来自 locatedownload,而 TOTAL(整体下载)这一层最终由 CMS 接管。
所以最后的实际限速值受多个来源影响。
另外一个支撑这套理论的证据是,我们在客户端里找到的CMS原始配置其实是
total_limit_speed = 81920 B/s也就是大约80 KiB/s
但最终的total却是122880 B/s
所以说,客户端内部其实做了一个兼容判断
CMS 原始候选 = 81920
当前 locatedownload CDN = 122880
locatedownload 当前有效
↓
CMS 候选比当前 CDN 更低
↓
把 CMS TOTAL 候选提高到 122880
后来在历史日志中,还看到了一次当前 CDN = 204800 B/s
CMS 原始值仍然为 81920 B/s
按照同一套逻辑,最后得出的total就是204800 B/s
所以说这里可以得到一个结论:
120 KiB/s 不是写死在这套仲裁逻辑里的。相同的 CMS 原始输入,在当前下载状态不同的时候,可以计算出 120 KiB/s,也可以计算出 200 KiB/s。
而有了这个120 KiB/s的限速值,就一定是它把下载速度卡住了吗?
暂时还不能
因为这里只是证明,客户端最终配置出了一个 122880 B/s 的限制。
但是客户端内部还有其他限制:
-CDN 限制 //内容分发网络的速度上限
-TOTAL 整体限制 //整个下载系统总体最多能放多少数据
-task 级限制 //针对某一个具体下载任务的速度限制
-NetGrid 等对象里的限制 //某个下载任务背后实际负责组织网络连接,数据传输的一层执行对象
一个限速值即使存在,它可能并未被真正使用
整个客户端
│
├─ TOTAL
│ 控制总体下载速度
│
├─ CDN
│ 控制 CDN 下载路径
│
└─ 某个具体任务
│
├─ task 级限制
│
└─ NetGrid / peer 执行层限制
其实类似不同的闸门,取了其中最紧的一个。最紧的一层决定瓶颈。
所以后面我没有继续只盯着配置值,而是启动真实下载任务,观察哪个限速器的“额度”真的在被不断消耗。也就是前面的Token Bucket(令牌桶)
对于令牌桶,我们主要关注三个东西
rate
每秒应该补多少下载额度
token
现在还剩多少下载额度
timestamp
上一次补额度时的时间
可以理解为
时间过去
↓
按照 rate 补 token
↓
下载数据
↓
消耗 token
↓
token 不够
↓
等待继续补
如果某一个限速器确实在卡我们的下载速度,那么他对应的令牌桶,应该出于一种不断被消耗,不断被补充,而消耗的速度大于被补充的速度,导致token长期接近耗尽的一种状态。
反之,如果一个限速器下载速度写的很小,但是他的token与timestamp实际并没有变化的话,至少这次下载没有从这里取额度。
在一次真实下载任务中,客户端显示的速度长期处于122764 \~ 126223 B/s范围。
与此同时,我们看到TOTAL:rate = 122880 B/s
更重要的是它的token,在连续采样之后,我们得到
480 / 482 次
token < 16 KiB
也就是在下载过程的绝大多数时间,它剩下的下载额度都很少,是一个十分典型的卡住吞吐的状态。
与此同时,我们也观察了其他限速器,此中的一个task 级限制,约为 500 MiB/s
额度一直很充足,显然不是它卡住了我们的下载速度。
还有一个task / NetGrid CDN = 16 KiB/s
那么为何下载速度不是16 KiB/s呢?
因为他的
token 没变
timestamp 没变
说明这次下载,并未消耗他的额度。
真正表现了瓶颈特征的,是122880 B/s
为了进一步证明,total限速器不是一个待在内存里的无关对象,我们还获取了暂停下载任务时的数据。当任务运行时
TOTAL token 在变化
TOTAL timestamp 在变化
文件进度在增长
暂停时
TOTAL rate 仍然 = 122880 B/s
但是:
token 冻结
timestamp 冻结
文件进度停止
网络读取也停止
甚至捕获到,冻结时token = 18 bytes。
所以说明,total限速器的活动和真实下载生命周期是同步的。
接下来就进入了Openspeedy的作用层了,也就是令牌桶是按什么时间补充额度的
核心就一个公式
新增 token ≈ rate × elapsed_time
比如
rate = 122880 B/s
程序认为过去了 1 秒
↓
大约补 122880 bytes
程序认为过去了 0.5 秒
↓
大约补 61440 bytes
而这个elapsed_time 是从哪里来的呢!
我们继续追百度原始 kernel.dll 的令牌桶补充逻辑,最后追到了 Windows 的 FILETIME 时间接口。也就是说,这个限速器不是单纯“每隔固定 1 秒发一次额度”,而是会读取时间,算出“距离上次过去了多久”,再据此补 token。
上一次时间
timestamp_old
↓
读取当前时间
timestamp_now
↓
elapsed = now - old
↓
新增 token ≈ rate × elapsed
↓
更新 timestamp
Openspeedy影响的,就是限速器对于时间的感知。
到这里,也只是在理论上说明了此套机制可行,我们还做了更进一步的实验。
首先,我们加载百度原来的签名 kernel.dll,让他自己的限速器工作,保持rate不变,只改变了它感知时间的速度,结果:
0.25x → 29.79 KiB/s
0.50x → 59.56 KiB/s
1.00x → 119.13 KiB/s
2.00x → 239.25 KiB/s
5.00x → 598.13 KiB/s
5个点做线性拟合,基本就是一条直线。
这里的“改变时间倍率”并不是在实验代码里直接修改 elapsed_time。我们在自己的测试程序中加载了 OpenSpeedy 官方的 speedpatch64.dll,通过它提供的倍率控制让整个进程看到的 FILETIME 时间变快或变慢。
与此同时,我们另外用一条不受本次 FILETIME 虚拟化影响的时间NtQuerySystemTime作为现实时间对照,因此可以确认百度限速器实际感知到的是 0.5x、2x、5x,而不是手工把计算结果乘了一个系数。
不过这还不够,完全可以质疑是在实验程序里调用限速函数,我们需要证明,这个数字能被传入实际的数据传输与文件写入。
之前的测试相当于拆一个水龙头阀门,而接下来,我们要把这个阀门装上真正的水管,看看水流是否真的经过这个阀门了。
我们接入了真实数据传输
本地 TCP sender
↓
百度原始 TOTAL 限速器
↓
recv() 接收数据
↓
写入文件
↓
FlushFileBuffers()
只有令牌桶允许,才能接受之后的数据
最终结果仍然是
无时间修改 → 119.14 KiB/s
0.5x → 59.57 KiB/s
1x → 119.13 KiB/s
2x → 239.20 KiB/s
5x → 599.53 KiB/s
到这里,时间倍率影响的已经不只是“令牌桶函数算出来的数字”,而是实际经过 socket 接收并写进文件的数据吞吐。
现在已经能证明:只要改变这个进程对时间的感知,百度原始限速器控制下的下载速度也会变化。
但前面的实验还有一个区别:speedpatch64.dll 是我们的测试程序自己加载进去的,而平时使用 OpenSpeedy 时,并不是目标程序主动加载它,而是 OpenSpeedy 从外部把时间补丁注入目标进程。
所以最后我们又补了一层验证:直接使用OpenSpeedy官方的Bridge流程,对我们自己的测试程序进行注入。 并没有修改真实运行中的百度网盘进程。
OpenSpeedy 官方 Bridge
↓
将 speedpatch64.dll 加载进测试程序
↓
改变测试程序看到的时间
↓
百度原始 kernel.dll
↓
TOTAL 令牌桶
↓
TCP recv()
↓
写入文件
得到的结果:
0.5x → 59.57 KiB/s
1x → 119.15 KiB/s
2x → 239.23 KiB/s
5x → 599.65 KiB/s
也就是说,从OpenSpeedy的实际外部注入链,到百度原始限速代码,再到真正的文件接收和写入,是能完整跑通的。
目前的证据支持:
sl=120会进入客户端限速逻辑,并换算成122880 B/s
真实普通长文件下载中,真正表现出瓶颈特征的是TOTAL = 122880 B/s
TOTAL 令牌桶按经过时间补充额度,改变进程感知的时间后,百度原始限速代码控制下的实际数据吞吐会近似按倍率变化
这里要声明一点,我们没有对真实在线的 baidunetdiskhost.exe 注入 OpenSpeedy ,所以不能真实完整证明,百度下载一定按某种倍率提升。真实百度进程部分始终只做了只读观察。
另外,这套结论主要对应我们观察到的普通长文件路径。客户端还存在小文件特殊路径,例如 fsl=0 时会跳过一个特定的全局 token consume,因此不能拿 120 KiB/s 这一条规则解释所有下载场景。
写完有点头昏脑胀的,如果有些地方描述不清或者有逻辑错误,欢迎各位佬指正,也请包涵。 ^-^