在上一篇文章里,我们已经了解了种子文件里的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之前,我们要先了解BEP3BEP29BEP10。协议的内容有点多,我刚开始看也是一脸懵逼,但慢慢来,找相关的内容看,还是可以看懂的。

前文

peer间传递数据使用的BEP3里定义的peer protocol,peer protocol是一个应用层协议,基于tcpudp协议。

早期的peer protocol是基于tcp的,但是会有一些问题,主要是tcp协议会将我们的带宽均匀地分配给每一个tcp连接,基于tcp的bt下载会和其他客户端建立大量的tcp连接,这样bt程序就会占满我们的带宽,直接导致其他网络应用程序的网络延迟。

utp的提出就是为了解决上面的问题,utp基于udp,和tcp的主要不同是实现了自己的拥塞控制算法(具体可以看BEP29)。

使用utp协议的bt客户端,可以在没有其他程序运行时,占满所有带宽。在有其他网络程序运行时,可以保证其他程序的网络仍保持流畅。

utp协议

协议头部格式
8GAcFO.md.jpg

ver - 协议版本

  • 目前版本是 1

connection_id - 连接id

  • 一个随机,唯一的数字,用来确认哪些数据包属于同一个连接。

wnd_size - 窗口大小

timestamp_microseconds - 包发送时的时间戳

timestamp_difference_microseconds - 包发送时的时间戳减去上次收到的包的时间戳

seq_nr - 数据包的序号

  • tcp不同,utp的序号根据数据包的个数,不是根据bytes计算。

ack_nr - 对端确认最后收到的数据包序号。

type - 数据包的类型

  • ST_SYN = 4

    • tcpSYN相似,用来开始连接。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

    • tcpFIN相似,用来终止连接。这是整个连接的最后一个包,连接中不会有包的seq_nrFIN包的seq_nr大。
  • ST_RESET = 3

    • tcpRST相似,用来强行终止连接。当收到这个ST_RESET包时,说明对端没有连接的状态信息,此时本机应该终止连接,关闭socket。
  • ST_DATA = 0

    • 成功建立连接后,用来发送的数据包。ST_DATA数据包总会有payload

8GqmsH.md.jpg
8GqeQe.md.jpg

上面也可以看到ST_SYNconnection_id = 39889, seq_nr = 28852, ST_STATEconnection_id = 39889, ack_nr = 28852。

成功建立连接后,就可以开始发送数据。

peer wire 协议

peer wire协议是peer间的通信协议,基于tcputp,定义了peer可以发送的消息。

handshake message

成功建立utp的连接后,本机会发送一个握手消息。握手消息是必须要发送,且是第一个发送的消息。
8tLIFs.md.jpg
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
8tjzkQ.md.jpg

上面的图里,对端除了回复一个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 messagehandshake 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是不同的两类消息,要区分开)。

8tjzkQ.md.jpg

上面的图是双方成功建立utp连接后,对端的回复。紧跟在handshake message后面的就是extended message

extended message handshakepayload是一个经过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字典里的其他键还有pvyourip, 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 messageut_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}

8dfjHA.md.jpg

上图可以看到本机发送了{'msg_type': 0, 'piece': 0}'msg_type': 0, 'piece': 1}两条request消息。

8dhC38.md.jpg

对端响应了{'msg_type': 1, 'piece': 0, 'total_size': 23164}data消息。(data响应因为包含了元数据,比较大,分成了多个包发送,后面也有{'msg_type': 1, 'piece': 1, 'total_size': 23164},上面没有截图出来)

认真观察data响应的数据
8dTSl6.jpg

可以看到我们要下载的电影名称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进行磁力链接协议分析

Last modification:March 26th, 2020 at 11:42 am
如果觉得我的文章对你有用,请尽情赞赏 🐶