在上一篇文章里,我们已经了解了种子文件里的info
包含了资源的元数据(metadata)
。元数据
包括资源的文件名
,文件大小
,文件块的sha1值
等等。
我们之前通过DHT协议
收集到的info-hash
,其实是元数据
的经过sha1算法计算后的值。sha1算法是不可逆的算法,是没有办法从info-hash
逆向得到元数据
。你肯定会问,那收集这些info-hash
有什么用,看着其中的某一串BCQQ5HYSXW4MCNCX3EVFNESOHPXS7BZL
,都不知道它是小姐姐还是葫芦娃。
不要急...
平时使用bt工具下载电影,除了从网上下载后缀是.torrent
的种子文件,还有一种方式,你应该也都很熟悉,那就是磁力链(Magnet)
。
magnet:?xt=urn:btih:BCQQ5HYSXW4MCNCX3EVFNESOHPXS7BZL
磁力链的格式
magnet:?xt=urn:btih:<info-hash>
磁力链里的BCQQ5HYSXW4MCNCX3EVFNESOHPXS7BZL
其实就是资源元数据
对应的info-hash
。
将上面的磁力链接粘贴到迅雷里,会出现
BCQQ5HYSXW4MCNCX3EVFNESOHPXS7BZL
原来是经典美剧《越狱》。
我以前也很好奇迅雷是怎样实现上面的功能的,当我还是小白的时候,我能想到的是,当我们用种子去迅雷下载资源时,迅雷就计算一下资源元数据
对应的info-hash
,然后把元数据
和info-hash
记录到自己的数据库里,这样当用户使用磁力链
去下载资源时,就可以从磁力链
里提取出info-hash
,从数据库里找出info-hash
对应的元数据
返回给用户。
但后面我想到一种情况,如果磁力链
对应的种子文件,之前没有用户下载过,迅雷要怎样处理这种情况?其他非商业开源的bt下载工具,也没有服务器来存储元数据
和info-hash
。
BEP9-Extension for Peers to Send Metadata Files其实是解决这种情况的方案,我们可以向其他的peer客户端发起请求,来下载资源的元数据
。
在BEP9
之前,我们要先了解BEP3
,BEP29
,BEP10
。协议的内容有点多,我刚开始看也是一脸懵逼,但慢慢来,找相关的内容看,还是可以看懂的。
前文
peer间传递数据使用的BEP3
里定义的peer protocol,peer protocol是一个应用层协议,基于tcp
或udp
协议。
早期的peer protocol是基于tcp
的,但是会有一些问题,主要是tcp
协议会将我们的带宽均匀地分配给每一个tcp
连接,基于tcp
的bt下载会和其他客户端建立大量的tcp
连接,这样bt程序就会占满我们的带宽,直接导致其他网络应用程序的网络延迟。
utp
的提出就是为了解决上面的问题,utp
基于udp
,和tcp
的主要不同是实现了自己的拥塞控制算法(具体可以看BEP29
)。
使用utp
协议的bt客户端,可以在没有其他程序运行时,占满所有带宽。在有其他网络程序运行时,可以保证其他程序的网络仍保持流畅。
utp协议
协议头部格式
ver
- 协议版本
- 目前版本是 1
connection_id
- 连接id
- 一个随机,唯一的数字,用来确认哪些数据包属于同一个连接。
wnd_size
- 窗口大小
timestamp_microseconds
- 包发送时的时间戳
timestamp_difference_microseconds
- 包发送时的时间戳减去上次收到的包的时间戳
seq_nr
- 数据包的序号
- 和
tcp
不同,utp
的序号根据数据包的个数,不是根据bytes计算。
ack_nr
- 对端确认最后收到的数据包序号。
type
- 数据包的类型
ST_SYN
= 4- 和
tcp
的SYN
相似,用来开始连接。SYN
包的connection_id
被初始化为一个随机的数字。本机客户端后续发送的包connection_id
=SYN
包的connection_id
+ 1。 - 对端接收到
SYN
包后,响应一个ST_STATE
包,让本机确认,对端已经收到自己发送的SYN
包。对端后续的发送的包的connection_id
=SYN
包的connection_id
。 - 这样双方就成功地建立了连接。
- 和
ST_STATE
= 2- 状态包,用来发送ack,告诉对端自己成功收到数据包。
ST_FIN
= 1- 和
tcp
的FIN
相似,用来终止连接。这是整个连接的最后一个包,连接中不会有包的seq_nr
比FIN
包的seq_nr
大。
- 和
ST_RESET
= 3- 和
tcp
的RST
相似,用来强行终止连接。当收到这个ST_RESET
包时,说明对端没有连接的状态信息,此时本机应该终止连接,关闭socket。
- 和
ST_DATA
= 0- 成功建立连接后,用来发送的数据包。
ST_DATA
数据包总会有payload
。
- 成功建立连接后,用来发送的数据包。
上面也可以看到ST_SYN
的 connection_id
= 39889, seq_nr
= 28852, ST_STATE
的 connection_id
= 39889, ack_nr
= 28852。
成功建立连接后,就可以开始发送数据。
peer wire 协议
peer wire协议是peer间的通信协议,基于tcp
或utp
,定义了peer可以发送的消息。
handshake message
成功建立utp
的连接后,本机会发送一个握手消息。握手消息是必须要发送,且是第一个发送的消息。
ps: wireshark不能解析 utp data里的消息,如果有人知道怎样设置可以解析,请告诉我。google了很久都没找到办法。how-to-identify-torrent-file-pieces-in-µtp
握手消息的格式
handshake: <pstrlen><pstr><reserved><info_hash><peer_id>
pstrlen: pstr字符串的长度,单位字节。
pstr: 协议的字符串名称。(BT协议v1.0定义,pstrlen = 19, and pstr = "BitTorrent protocol")
reserved: 保留的8个字节。每个bit可以用来扩展协议。(后面的extended message会用到)。
info_hash: 20字节长的元数据sha1对应值。磁力链里btih对应的值。
peer_id: 20字节长的客户端id。
对端接收到handshake
消息后,会对比info_hash
,如果不同,对端会关闭连接。如果相同,就会马上回复一个handshake message
。
上面的图里,对端除了回复一个handshake message
,后面还附带着一个extended handshake message
(这个是下载种子文件相关的消息,下面会继续讲),一个bitfield message
和多个having message
。
peer message
除了handshark message
,其他的消息格式都是<length prefix><message ID><payload>
。<length prefix>
决定了消息的<message ID><payload>
的所占字节长度。
peer wire
协议定义了下面几种消息,这几种消息和下载文件相关。(下面的几个消息,和下载种子文件关系不大,简单介绍下)
keep-alive: <len=0000>
没有message ID和payload, peer可能会在长时间接收不到消息后,关闭连接,keep-alive
消息是用来维持连接。
choke: <len=0001><id=0>
unchoke: <len=0001><id=1>
interested: <len=0001><id=2>
not interested: <len=0001><id=3>
上面四个消息,用来改变连接的状态。本机的客户端,对每一个与其他客户端的连接,都维护这一个四元组(am_choking
, am_interested
, peer_choking
, peer_interested
)的状态信息。
只有在 peer_choking
=0 (对端不阻塞),am_interested
=1(本机感兴趣),本机才会从对端下载文件。
只有在 am_choking
=0 (本机不阻塞),peer_choking
=1(对端感兴趣),对端才能从本机下载文件。
have: <len=0005><id=4><piece index>
通知其他客户端,自己成功地下载了piece index对应的文件块,并通过了hash对比检查。其他客户端可以请求我,下载这块文件。
bitfield: <len=0001+X><id=5><bitfield>
通知其他客户端,自己有那些piece index对应的文件块,<bitfield>
里的每个bit的index对应文件块的index。例如,<bitfield>
里的第一个bit的值=1,表示自己有piece index=0对应的文件块。
request: <len=0013><id=6><index><begin><length>
用来请求下载指定index的文件块。
extended message(扩展消息)
千山万水,铺垫了这么久,终于要进入主题了。通过磁力链下载种子种子文件,使用的是extended message
。
BEP10
,bt客户端可以在bt协议上,增加新的扩展功能,而不会影响其他不支持新功能的客户端。
为了通知其他客户端,自己支持extended message
,handshake message
里的reserved
的从右边起的第20个bit(0开始数起)要置为1。所以可以通过表达式reserved_byte[5] & 0x10
来检查客户端是否支持extended message
。
extended message
和上面其他的peer message
格式类似。<length prefix><message ID><extended message ID><payload>
。这里message ID
= 20。
extended message ID
=0时,是extended handshake message(注意:extended handshake message和我们之前的handshake message
是不同的两类消息,要区分开)。
上面的图是双方成功建立utp
连接后,对端的回复。紧跟在handshake message
后面的就是extended message
。
extended message handshake
的payload
是一个经过bencoded
过的字典。
m
一个字典,字典内包含键值对<extended name
(扩展名),extended message ID
(扩展消息ID)>。
{
"upload_only": 3
"lt_donthave": 7,
"ut_holepunch" 4m
"ut_metadata": 2,
"ut_pex": 10,
"ut_comment": 6,
"ut_bi": 9,
...
}
在m
里,我们可以看到ut_metadata
,说明对端支持从它那里下载种子文件元数据。
payload
字典里的其他键还有p
,v
,yourip
, ipv6
, ipv4
, reqq
, 还可以有和特定扩展相关的键。例如metadata_size
。(种子文件元数据的大小,单位字节)
一个支持下载种子文件元数据的extended message payload
的例子
{
"m": {
"ut_pex": 10,
"ut_metadata": 2
}
"metadata_size": 23164,
"p": 6881,
"v": "µTorrent 1.2"
}
下载种子文件元数据
对端已经告诉我们,支持下载种子文件的元数据metadata
了。
元数据按照每块16k的大小,分成多个文件块。上面我们得到的metadata_size
= 23164,23164 / 16 / 1024 = 1.49,所以我们请求的元数据被分成了2个文件块。
BEP9
定义了三种用于下载元数据的扩展消息
这三种消息的格式都是<length prefix><message ID><extended message ID><payload>
,其中extended message ID
= ut_metadata
的值,在我们的例子里就是extended handshake message
里ut_metadata
的值2。payload
使用bencode
编码。
request
msg_type
= 0- 请求下载某块文件
payload = {'msg_type': 0, 'piece': 0}
data
msg_type
= 1- 响应
request
, 返回某块元数据文件 payload = {'msg_type': 1, 'piece': 0, 'total_size': 3425}eexxxxxxxxxxxxxxxxxxxxxxx....
(eexxxxxxxxx...是文件块的内容)
reject
msg_type
= 2- 响应
request
, 没有对应的元数据文件块 payload = {'msg_type': 2, 'piece': 0}
上图可以看到本机发送了{'msg_type': 0, 'piece': 0}
和'msg_type': 0, 'piece': 1}
两条request
消息。
对端响应了{'msg_type': 1, 'piece': 0, 'total_size': 23164}
的data
消息。(data
响应因为包含了元数据,比较大,分成了多个包发送,后面也有{'msg_type': 1, 'piece': 1, 'total_size': 23164}
,上面没有截图出来)
认真观察data
响应的数据
可以看到我们要下载的电影名称1917.2019.DVDSCR.x264-TOPKEK.mp4e
。
本机通过request
消息获取到第一块和第二块元数据后,就可以将两块数据合并,使用sha1对最终的得到的元数据进行检查,如果和info_hash
对比成功,那元数据就下载成功了。
终于,我们通过收集的种子文件元数据对应的info_hash
,可以逆向得到元数据了。看着一串串的info_hash
,我们终于知道它们是小姐姐还是葫芦娃了。
总结
本来是打算,自己造轮子来实现上面的过程,越深入看资料,发现要处理的细节太多了,比如utp
协议的实现就是个难题。
于是我决定站在巨人的肩膀上😁,通过从已有的种子站点提供的转换接口,和通过libtorrent
库来下载。libtorrent
库的功能很强大,只要几行代码就能实现种子文件的转换。后面讲解下libtorrent
的使用。
参考资料
BEP3 - The BitTorrent Protocol Specification
BEP29 - uTorrent transport protocol
Bittorrent Protocol Specification v1.0
BEP10 - Extension Protocol
µTP 协议 —— 对 BEP29 的简要理解
使用WireShark进行磁力链接协议分析