上一篇博客中,我介绍了两种方法来将磁力链接转换为一个种子文件:一种方法是直接从种子站点下载,另一种方法通过libtorrent库实现。第二种方法通过libtorrent库的强大功能,只有寥寥几行代码就实现了种子文件的转换。正如前文所说,简单的背后往往蕴含着复杂的逻辑设计,这篇博客通过对WireShark截获下的所有网络包的分析,深入学习BitTorrent协议,包括:DHT网络,KRPC协议,种子文件格式,B编码,BitTorrent扩展,uTP协议等。

一、加入DHT网络

磁力链接的发明使得P2P客户端直接从DHT网络中寻找资源,而不是传统的依赖于Tracker服务器,这样就避免了Tracker服务器的单点故障问题,所以从DHT中获取的种子有时候也叫做Trackerless torrent。DHT网络是一种分布式的去中心化网络,每个加入DHT网络的节点都要负责存储这个网络中的资源和其他成员的联系信息。小虾在他的一篇文章中做了一个非常形象的比喻:

举个形象点的例子,把DHT网络比作一个朋友圈子,你想进入这个圈子必须要有一个人带领你进去,通常会有一些特定人负责介绍你进入这个圈子。当你被A带进这个朋友圈,此刻你就只认识A而已。但是你的目的是想找奥巴马总统,所以你会问A要奥巴马的联系方式,但是A没有奥巴马的联系方式,他会介绍一个美国朋友B给你认识。于是你去问B要奥巴马的联系方式,B其实也没有奥巴马的联系方式,但是B认识一个州长C。于是你又得到了C的联系方式,C把奥巴马的联系方式告诉你之后,你就可以写信或者致电给奥巴马了。

可以看到如果一个新节点要加入到DHT网络中,它必须要先认识一个人带你进去。这样的人我们把他叫做bootstrap node,常见的bootstrap node有:router.bittorrent.comrouter.utorrent.comrouter.bitcomet.comdht.transmissionbt.com等等。所以首先我们通过DNS将其转换为IP:

dns

这里以dht.transmissionbt.com为例进行说明,从DNS查询结果得到了两个IP地址:212.129.33.50 和 91.121.59.153。

二、了解KRPC协议,获取Peers

DHT协议早在2005年就已经成为了官方BitTorrent协议的一部份,官方文档参考BEP-005,这里有一份中文翻译。DHT建立在UDP之上,想要获取需要的Peers信息,首先要了解下KademliaKRPC协议。 关于路由表和Kademlia的介绍,可以参照官方文档。我们这里的重点在于如何根据磁力链接获取拥有该磁力链接对应的种子文件信息的Peers,所以只需要了解KRPC协议。 KRPC协议是由B编码组成的一个简单的RPC结构,有4种请求:pingfind_nodeget_peersannounce_peer

  • ping: 检测节点是否可达,请求包含一个参数id,代表该节点的nodeID。对应的回复也应该包含回复者的nodeID。
  • find_node: 该请求包含两个参数idtargetid为该节点的nodeID,target为要查询的nodeID。回复中应该包含被请求节点的路由表中距离target最接近的8个nodeID。
  • get_peers: 该请求包含两个参数idinfohashid为该节点的nodeID,infohash为种子文件的SHA1哈希值,也就是磁力链接的btih值。如果被请求的节点有对应info_hash的peers,他将返回一个关键字values,这是一个列表类型的字符串。每一个字符串包含了CompactIP-address/portinfo格式的peers信息。如果被请求的节点没有这个infohash的peers,那么他将返回关键字nodes,这个关键字包含了被请求节点的路由表中离info_hash最近的K个nodes,使用Compactnodeinfo格式回复。
  • announce_peer: 如果节点正在下载torrent文件,则需要通知其他人你正在哪个端口进行下载,这样就可以分享给其他人,让其他人连接你进行下载。

在我们的实验中,因为并没有真正的下载torrent文件,所以并不需要announce_peer请求。ping非常简单,我们忽略之,只看find_nodeget_peers这两个请求。形象点说find_node请求就好比我们交朋友,如果运气好的话每次发送find_node都可以得到8个新node,再对这8个新node发送find_node,就可以得到8*8=64个新node(当然这是理想情况,我们没考虑丢包,节点不在线,返回节点数没8个这些情况)。一般情况下find_nodetarget参数就是我们自己的nodeID,所以得到的8个node都是离自己最近的(Kademlia距离,并非物理距离)。这样我们很快就得到了非常多的node信息,路由表就是这样建立起来的。 但是实际上find_node请求在这里对我们用处并不大,要想根据磁力链接获取种子信息,最重要的请求还是get_peers,它可以根据infohash来找到拥有该种子信息的peers。 下图是find_node请求,idtarget参数都是当前节点的nodeID:909f9cbdedf4f7e29e820e3fd5e00a2965450b8a

find_node

下图是对find_node请求的回复。回复报文中可以看到返回了8个node,每个node都是紧凑的nodeID IP/Port格式:

find_node_resp

下图是get_peers请求,infohash = 1619ecc9373c3639f4ee3e261638f29b33a6cbd6,正是磁力链接magnet:?xt=urn:btih:1619ecc9373c3639f4ee3e261638f29b33a6cbd6&dn;=ubuntu-14.10-desktop-i386.iso中的btih值。

get_peers

下图是对get_peers请求的回复。回复报文中包含了8个node,以及100个peer。可见包含该种子文件的peer非常多。

get_peers_resp

三、Bencoding编码和种子文件格式

Bencoding编码(简称B编码)在BitTorrent协议中非常常见,从上面的四张图中就可以看到KRPC协议采用了B编码来发送消息,不仅如此,在后面介绍的PEX和BitTorrent扩展中我们也会看到B编码的影子。而且更重要的是,BT种子文件本身就是一个B编码的字典,所以要想学习BitTorrent,首先得学习B编码。B编码是一种非常简洁的数据格式,共支持4种不同的类型:字节串、整数、列表和字典。

  • string: 格式为 <字符串长度>:<字符串>。如 hell: 4:hell
  • integer: 格式为 i<整数>e。如 i1999e 表示数字1999
  • list: 格式为 l[数据1][数据2][数据3][…]e。如 l5:hello5:worldi101ee 表示列表[hello, world, 101]
  • dictionary: 格式为 d[key1][value1][key2][value2][…]e,其中 key 必须是 string 而且按照字母顺序排序。如 d2:aai100e2:bb2:bb2:cci200ee 表示字典 {aa:100, bb:bb, cc:200}

BT种子文件整个是一个dictionary格式,比较重要的key有announce( tracker 服务器的地址)、announce-list(可选的 tracker 服务器地址)、creation date(文件创建时间)、created by(文件创建者)、info(该bt种子文件的文件信息)等。其中info对应的value根据种子包含的是单文件还是多文件有所区别,其中piece length(每一数据块的长度)和pieces(所有数据块的 SHA1 校验值)是公共部分,如果是单文件的话,则包含name(文件名称)和length(文件的长度)两个key,如果是多文件的话,则包含name(文件夹名称)和files(文件列表,每个文件列表下面是包括每一个文件的信息,文件信息是个字典)两个key。 关于Bencoding和种子文件格式更详细的信息可以参考这里这里这里,另外这里有一份BitTorrent的非官方规范,其中也有关于Bencoding的介绍。 我们这里以ubuntu-14.10-desktop-i386.iso.torrent文件为例进行介绍,使用WinHex打开该文件如下图所示:

torrent

图的右边我将其转换成容易阅读的格式,可以看到上面介绍的announceannounce-listinfo等字段。其中info字段对于一个种子文件来说最为重要,根据磁力链接从DHT网络中下载种子文件也是下载info字段的内容,其他内容包括announceannounce-listcreated by等在DHT网络中并没有保留。磁力链接的infohash也是根据info字段来计算的,info字段的pieces为每个数据块的校验值,其作用是验证下载下来的文件是否正确,如果下载下来的文件块计算出来的SHA1值和pieces中的SHA1校验值不一致,该数据块要重新下载。 所以,我们可以看出根据磁力链接下载文件是分成两个步骤的:先根据infohash下载种子文件的info字段,种子文件并不是必须的,但是info字段却必不可少;然后根据infohash下载源文件,将下载的每一个数据块和info中的对应的SHA1校验码进行比较,不一致重新下载该数据块。我们这里研究的其实就是第一步,要注意的是info字段的下载也是分块的,下面将进行介绍,下载完成后使用infohash进行校验。 需要注意的是,一般的种子文件会包含announce,也就是tracker服务器的地址,如果没有tracker服务器,文件中可能会包含nodesnodes是存有种子信息的peer节点,这样的种子文件就是trackerless torrent。如果有nodes客户端直接从nodes获取种子信息,而从DHT网络中下载下来的种子文件既没有annouce也没有nodes,客户端只能通过info字段计算出hashinfo,再从bootstrap node节点开始在DHT网络中寻找种子信息。

四、uTP协议

在介绍info字段的下载之前,我们还要了解下uTP协议,它是一个基于UDP的开放的BT点对点文件共享协议。在uTP协议出现之前,BT下载会占用网络中大量的链接,直接导致其它网络应用服务质量下载和网络的拥堵,因此有很多ISP都开始限制BT的下载。uTP减轻了网络延迟并解决了传统的基于TCP的BT协议所遇到的拥塞控制问题,提供可靠的有序的传送。 一个有效的uTP数据包包含下面格式的报头:

uTP-header

其中,我们最为关心的是typeconnection idseq_nrack_nr这几个值。type字段表示包类型,uTP的包类型有下面5种:

  • ST_DATA = 0: 最重要的数据包,uTP就是使用该类型的包传送数据
  • ST_FIN = 1: 关闭连接,这是uTP连接的最后一个包,类似于TCP中的FIN
  • ST_STATE = 2: 简单的应答包,表明已从对方收到了数据包,该包不包含任何数据,seq_nr值不变
  • ST_RESET = 3: 终止连接,类似于TCP中的RST
  • ST_SYN = 4: 初始化连接,类似于TCP中的SYN,这是uTP连接的第一个包

关于uTP协议的内容参考官方文档BEP-029uTP的一个很重要的特点是使用connection id来标识一次连接,而不是每个包算一次连接。所以在分析ST_DATA时,需要注意找所有connection id相同的数据包,然后按seq_nr排序,seq_nr应该是依次递增的(注意ST_STATE包不会增加seq_nr值),如果发现两个ST_DATAseq_nr值相同则说明后面那个报文是重复报文需要忽略掉,如果发现两个ST_DATAseq_nr值不是连续的,中间差了一个或多个,则可能是由于网络原因发生了丢包现象,数据包将不可用。 下面是一个简单的ST_SYN报文,从192.168.18.33发送到112.208.162.161,seq_nr = 31445,ack_nr = 0, connection id = 14487

uTP-syn

112.208.162.161收到ST_SYN报文后,会向192.168.18.33发送一个ST_STATE报文,表示已收到。如果没有回复,则连接不能建立。如下图所示,seq_nr = 9690,ack_nr = 31445,connection id = 14487

uTP-state

uTP连接建立之后,就开始传送需要的数据了。peer和peer之间传送数据也是遵循着一定的规范,这就是下面要讲的Peer Wire协议。

五、Peer Wire协议

Peer Wire协议是Peer之间的通信协议,通常由一个握手消息开始。握手消息的格式是这样的:<pstrlen><pstr><reserved><info_hash><peer_id> 下面是一个握手报文的示例:

peer-wire-handshake

在BitTorrent协议的v1.0版本, pstrlen = 19, pstr = "BitTorrent protocol",info_hash是上文中提到的磁力链接中的btihpeer_id每个客户端都不一样,但是有着一定的规则,根据前面几个字符可以推断出客户端的类型,譬如这里的peer_id-LT开头可以大致推断出客户端为libtorrent这里有一份非常全面的列表,这里也有一个列表。 收到握手消息后,对方也会回复一个握手消息,并且开始协商一些基本的信息。如下图是握手报文的回复:

peer-wire-handshake-resp

从上图中可以看到,对方除了回复了一个握手消息外,在握手消息的后面还附带了一个扩展消息,一个bitfield消息和多个have消息。这些都是Peer Wire协议基本的通信消息,拥有统一的格式:<length prefix><message ID><payload>,可以从message ID看出是哪种类型的消息。Peer Wire消息有:keep-alive、choke(0)、unchoke(1)、interested(2)、not interested(3)、have(4)、bitfield(5)、request(6)、piece(7)、cancel(8)、port(9),另外还有下面要介绍的扩展协议extend(20)。 关于Peer Wire协议的详细内容,请参考BitTorrent的规范,这里是中文翻译这里也有一点资料。 在下载文件时,这些消息扮演着非常重要的角色,而在这里,我们只关注message ID为20的extend扩展协议。

六、BitTorrent协议扩展与ut_metadata和ut_pex

经过了上面层层的铺垫,终于到了最后一步下载我们的种子文件了。获取种子文件的metadata使用的是BitTorrent的协议扩展,BEP-010BEP-009是关于这个的权威规范,这里有一份BEP-009的中文翻译。 根据BEP-010我们知道,扩展消息一般在Peer Wire握手之后立即发出,是一个B编码的字典,其中有一个key很重要,那便是mm也是一个字典,表示客户端支持的所有扩展以及每个扩展的编号。我们将上图中握手消息后的扩展消息写成下面的格式:

{
    e: 0,
    ipv4: xxx,
    ipv6: xxx,
    complete_ago: 1,
    m:
    {
        upload_only: 3,
        lt_donthave: 7,
        ut_holepunch: 4,
        ut_metadata: 2,
        ut_pex: 1,
        ut_comment: 6
    },
    matadata_size: 45377,
    p: 33733,
    reqq: 255,
    v: BitTorrent 7.9.3
    yp: 19616,
    yourip: xxx
}

其中ut_pex表示该客户端支持PEX(Peer Exchange)ut_metadata表示支持BEP-009(也就是交换种子文件的metadata)。PEX在这里派不上用场,我们主要关心metadata的交换。接着上面的握手消息,我们在完成双方握手之后,并且得到了对方支持的扩展信息。接着我们发出下面的请求:

metadata_request

该请求也通知对方我们支持的扩展情况,然后后面接着一个扩展消息。如何识别一个扩展消息是什么消息呢?我们从上面的m字典可以看到可能会有多种不同的扩展消息,那么我们这个消息是哪个扩展呢?答案是message ID后面那个数字,这个数字对应着m字典中的编号。譬如我们这里的消息是:00 00 00 1b 14 02 ... 00 00 00 1b 表示消息长度为 0x1b (27 bytes) 14 表示是 扩展消息(0x14 = 20) 02 对应上面m字典中的 ut_metadata 所以我们这个消息是ut_metadata消息,下面我们再来研究下BEP-009BEP-009定义了三种不同的消息类型:requestdatareject。我们这里的图显示的是[msg_type: 0, piece: 2]正是request消息,意思是向对象请求第二个piece的数据。而piece又是什么意思呢?根据BEP-009我们知道,种子文件的metadata(也就是info部分)会按16KB分成若干块,除最后一块每一块的大小都是16KB,每一块从0开始按顺序进行编号。所以这个请求的意思就是向对象请求第三块的metadata。我们再来看一下torrent文件,如下图:

torrent-info

从图中形象的表示可以看到torrent文件整个info的长度为45377,这个值正是上面握手报文后的扩展消息中的metadata_size的值。在发送request消息之后,接下来对方应该回复data消息(如果对方有数据)或reject消息(如果对方没有数据)。下图是针对上面的request消息的回复:

metadata_resp

msg_type为1表示是回复就是我所需要的数据,但是注意这里的数据并没完,由于uTP协议的缘故,我们可以根据connection id找到这个连接后续的所有数据。 这里其实一共收到了三个消息,我们分别来看一下:

00 00 00 03 09 83 c5 --> message ID为9,port消息,表示端口号为0x83c5 = 33733
00 00 00 03 14 03 01 --> message ID为20(0x14),extend消息,编号03upload_only,表示设置upload_only = 1
00 00 31 70 14 02 xx --> message ID为20(0x14),extend消息,编号02ut_metadata,后面的xx表示[msg_type: 1, piece: 2, total_size: 45377]和相应块的metadata数据

看第三个消息可以知道消息长度为0x3170,这个长度包括了[msg_type...]这一串字符串的长度,共0x2f个字节,我们将其减去就得到了piece2的长度:0x3170 - 0x2f = 0x3141 我们上面说过每个块的大小应该是16KB,也就是0x4000,这里的大小为0x3141,只可能是最后一块。我们稍微计算验证下,将整个info的长度45377(0xb141)按16KB分块:

piece 0: 0x0001 ~ 0x4000 长度0x4000
piece 1: 0x4001 ~ 0x8000 长度0x4000
piece 2: 0x8001 ~ 0xb141 长度0x3141

可以看到piece2正是最后一块,大小为0x3141。至此我们得到了第二块的metadata,然后通过request消息获取piece0和piece1获取第一和第二块的metadata,将三块的消息合并成torrent文件info字段,然后再加上create datecreate bycomment等信息,种子文件就算完成下载了。

七、校验info_hash

我们将从DHT网络中下载的种子文件和原始的种子文件进行比较,可以看到annouceannouce-list字段都丢掉了,create date发生了变化,info字段不变:

torrent-comp

另外,我们也可以使用这篇文章中介绍的方法将种子文件转换为磁力链接,检查infohash的值是否一致。从下面的代码也可以看出infohash的计算只和info字段有关。

import bencode, hashlib, base64, urllib

torrent = open('ubuntu.torrent', 'rb').read()
metadata = bencode.bdecode(torrent)
hashcontents = bencode.bencode(metadata)

参考

  1. 写了个磁力搜索的网页 - 收录最近热门分享的资源
  2. BEP-005 DHT Protocol
  3. 【P2P网络】BitTorrent的DHT协议(译自官方版本)
  4. BitTorrentDraftDHTProtocol
  5. torrent文件分析
  6. bttorrent 种子文件结构解析
  7. BEncode编码方式以及torrent文件的一些内容
  8. Bittorrent Protocol Specification v1.0
  9. BitTorrent协议规范
  10. BEP-020 Peer ID Conventions
  11. BEP-029 uTorrent transport protocol
  12. peer之间的通信协议
  13. BEP-010 Extension Protocol
  14. extension protocol for bittorrent
  15. BEP-009 Extension for Peers to Send Metadata Files
  16. 【P2P网络】Extension for Peers to Send Metadata Files翻译稿
  17. Python将BT种子文件转换为磁力链的两种方法
  18. 常见P2P协议之BitTorrent 分析
  19. BT(带中心Tracker)通信协议的分析
  20. BT协议具体分析
  21. BitTorrent 协议分析与实现
  22. How PEX protocol (Magnetic links) finds it first IP?
  23. BT协议库libtorrent的种子文件解析方法探究
扫描二维码,在手机上阅读!