使用WireShark进行磁力链接协议分析
在上一篇博客中,我介绍了两种方法来将磁力链接转换为一个种子文件:一种方法是直接从种子站点下载,另一种方法通过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.com
、router.utorrent.com
、router.bitcomet.com
、dht.transmissionbt.com
等等。所以首先我们通过DNS将其转换为IP:
这里以dht.transmissionbt.com
为例进行说明,从DNS查询结果得到了两个IP地址:212.129.33.50 和 91.121.59.153。
二、了解KRPC协议,获取Peers
DHT协议早在2005年就已经成为了官方BitTorrent协议的一部份,官方文档参考BEP-005,这里有一份中文翻译。DHT建立在UDP之上,想要获取需要的Peers信息,首先要了解下Kademlia
的KRPC
协议。 关于路由表和Kademlia的介绍,可以参照官方文档。我们这里的重点在于如何根据磁力链接获取拥有该磁力链接对应的种子文件信息的Peers,所以只需要了解KRPC
协议。 KRPC
协议是由B编码组成的一个简单的RPC结构,有4种请求:ping
、find_node
、get_peers
和 announce_peer
。
ping
: 检测节点是否可达,请求包含一个参数id
,代表该节点的nodeID。对应的回复也应该包含回复者的nodeID。find_node
: 该请求包含两个参数id
和target
,id
为该节点的nodeID,target
为要查询的nodeID。回复中应该包含被请求节点的路由表中距离target
最接近的8个nodeID。get_peers
: 该请求包含两个参数id
和infohash
,id
为该节点的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_node
和get_peers
这两个请求。形象点说find_node
请求就好比我们交朋友,如果运气好的话每次发送find_node
都可以得到8个新node,再对这8个新node发送find_node
,就可以得到8*8=64个新node(当然这是理想情况,我们没考虑丢包,节点不在线,返回节点数没8个这些情况)。一般情况下find_node
的target
参数就是我们自己的nodeID,所以得到的8个node都是离自己最近的(Kademlia距离,并非物理距离)。这样我们很快就得到了非常多的node信息,路由表就是这样建立起来的。 但是实际上find_node
请求在这里对我们用处并不大,要想根据磁力链接获取种子信息,最重要的请求还是get_peers
,它可以根据infohash
来找到拥有该种子信息的peers。 下图是find_node
请求,id
和target
参数都是当前节点的nodeID:909f9cbdedf4f7e29e820e3fd5e00a2965450b8a
下图是对find_node
请求的回复。回复报文中可以看到返回了8个node,每个node都是紧凑的nodeID IP/Port
格式:
下图是get_peers
请求,infohash = 1619ecc9373c3639f4ee3e261638f29b33a6cbd6
,正是磁力链接magnet:?xt=urn:btih:1619ecc9373c3639f4ee3e261638f29b33a6cbd6&dn;=ubuntu-14.10-desktop-i386.iso
中的btih
值。
下图是对get_peers
请求的回复。回复报文中包含了8个node,以及100个peer。可见包含该种子文件的peer非常多。
三、Bencoding编码和种子文件格式
Bencoding
编码(简称B编码)在BitTorrent协议中非常常见,从上面的四张图中就可以看到KRPC
协议采用了B编码来发送消息,不仅如此,在后面介绍的PEX
和BitTorrent扩展中我们也会看到B编码的影子。而且更重要的是,BT种子文件本身就是一个B编码的字典,所以要想学习BitTorrent,首先得学习B编码。B编码是一种非常简洁的数据格式,共支持4种不同的类型:字节串、整数、列表和字典。
string
: 格式为<字符串长度>:<字符串>
。如hell: 4:hell
integer
: 格式为i<整数>e
。如i1999e
表示数字1999list
: 格式为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打开该文件如下图所示:
图的右边我将其转换成容易阅读的格式,可以看到上面介绍的announce
、announce-list
、info
等字段。其中info
字段对于一个种子文件来说最为重要,根据磁力链接从DHT网络中下载种子文件也是下载info
字段的内容,其他内容包括announce
、announce-list
和created by
等在DHT网络中并没有保留。磁力链接的infohash
也是根据info
字段来计算的,info
字段的pieces
为每个数据块的校验值,其作用是验证下载下来的文件是否正确,如果下载下来的文件块计算出来的SHA1值和pieces
中的SHA1校验值不一致,该数据块要重新下载。 所以,我们可以看出根据磁力链接下载文件是分成两个步骤的:先根据infohash
下载种子文件的info
字段,种子文件并不是必须的,但是info
字段却必不可少;然后根据infohash
下载源文件,将下载的每一个数据块和info
中的对应的SHA1校验码进行比较,不一致重新下载该数据块。我们这里研究的其实就是第一步,要注意的是info
字段的下载也是分块的,下面将进行介绍,下载完成后使用infohash
进行校验。 需要注意的是,一般的种子文件会包含announce
,也就是tracker服务器的地址,如果没有tracker服务器,文件中可能会包含nodes
,nodes
是存有种子信息的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
数据包包含下面格式的报头:
其中,我们最为关心的是type
、connection id
、seq_nr
和ack_nr
这几个值。type
字段表示包类型,uTP的包类型有下面5种:
ST_DATA
= 0: 最重要的数据包,uTP就是使用该类型的包传送数据ST_FIN
= 1: 关闭连接,这是uTP连接的最后一个包,类似于TCP中的FINST_STATE
= 2: 简单的应答包,表明已从对方收到了数据包,该包不包含任何数据,seq_nr值不变ST_RESET
= 3: 终止连接,类似于TCP中的RSTST_SYN
= 4: 初始化连接,类似于TCP中的SYN,这是uTP连接的第一个包
关于uTP
协议的内容参考官方文档BEP-029。uTP
的一个很重要的特点是使用connection id
来标识一次连接,而不是每个包算一次连接。所以在分析ST_DATA
时,需要注意找所有connection id
相同的数据包,然后按seq_nr
排序,seq_nr
应该是依次递增的(注意ST_STATE
包不会增加seq_nr
值),如果发现两个ST_DATA
的seq_nr
值相同则说明后面那个报文是重复报文需要忽略掉,如果发现两个ST_DATA
的seq_nr
值不是连续的,中间差了一个或多个,则可能是由于网络原因发生了丢包现象,数据包将不可用。 下面是一个简单的ST_SYN
报文,从192.168.18.33发送到112.208.162.161,seq_nr = 31445,ack_nr = 0, connection id = 14487
112.208.162.161收到ST_SYN
报文后,会向192.168.18.33发送一个ST_STATE
报文,表示已收到。如果没有回复,则连接不能建立。如下图所示,seq_nr = 9690,ack_nr = 31445,connection id = 14487
在uTP
连接建立之后,就开始传送需要的数据了。peer和peer之间传送数据也是遵循着一定的规范,这就是下面要讲的Peer Wire
协议。
五、Peer Wire协议
Peer Wire
协议是Peer之间的通信协议,通常由一个握手消息开始。握手消息的格式是这样的:<pstrlen><pstr><reserved><info_hash><peer_id>
下面是一个握手报文的示例:
在BitTorrent协议的v1.0版本, pstrlen
= 19, pstr
= "BitTorrent protocol",info_hash
是上文中提到的磁力链接中的btih
,peer_id
每个客户端都不一样,但是有着一定的规则,根据前面几个字符可以推断出客户端的类型,譬如这里的peer_id
以-LT
开头可以大致推断出客户端为libtorrent
。这里有一份非常全面的列表,这里也有一个列表。 收到握手消息后,对方也会回复一个握手消息,并且开始协商一些基本的信息。如下图是握手报文的回复:
从上图中可以看到,对方除了回复了一个握手消息外,在握手消息的后面还附带了一个扩展消息,一个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-010
和BEP-009
是关于这个的权威规范,这里有一份BEP-009的中文翻译。 根据BEP-010
我们知道,扩展消息一般在Peer Wire
握手之后立即发出,是一个B编码的字典,其中有一个key很重要,那便是m
。m
也是一个字典,表示客户端支持的所有扩展以及每个扩展的编号。我们将上图中握手消息后的扩展消息写成下面的格式:
{
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的交换。接着上面的握手消息,我们在完成双方握手之后,并且得到了对方支持的扩展信息。接着我们发出下面的请求:
该请求也通知对方我们支持的扩展情况,然后后面接着一个扩展消息。如何识别一个扩展消息是什么消息呢?我们从上面的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-009
。BEP-009
定义了三种不同的消息类型:request
、data
和 reject
。我们这里的图显示的是[msg_type: 0, piece: 2]
正是request
消息,意思是向对象请求第二个piece的数据。而piece
又是什么意思呢?根据BEP-009
我们知道,种子文件的metadata(也就是info部分)会按16KB分成若干块,除最后一块每一块的大小都是16KB,每一块从0开始按顺序进行编号。所以这个请求的意思就是向对象请求第三块的metadata。我们再来看一下torrent文件,如下图:
从图中形象的表示可以看到torrent文件整个info的长度为45377,这个值正是上面握手报文后的扩展消息中的metadata_size
的值。在发送request
消息之后,接下来对方应该回复data
消息(如果对方有数据)或reject
消息(如果对方没有数据)。下图是针对上面的request
消息的回复:
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消息,编号03
为upload_only
,表示设置upload_only
= 1 00 00 31 70 14 02 xx
--> message ID
为20(0x14),extend消息,编号02
为ut_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 date
、create by
或comment
等信息,种子文件就算完成下载了。
七、校验info_hash
我们将从DHT网络中下载的种子文件和原始的种子文件进行比较,可以看到annouce
和annouce-list
字段都丢掉了,create date
发生了变化,info
字段不变:
另外,我们也可以使用这篇文章中介绍的方法将种子文件转换为磁力链接,检查infohash
的值是否一致。从下面的代码也可以看出infohash
的计算只和info
字段有关。
import bencode, hashlib, base64, urllib
torrent = open('ubuntu.torrent', 'rb').read()
metadata = bencode.bdecode(torrent)
hashcontents = bencode.bencode(metadata)
参考
- 写了个磁力搜索的网页 - 收录最近热门分享的资源
- BEP-005 DHT Protocol
- 【P2P网络】BitTorrent的DHT协议(译自官方版本)
- BitTorrentDraftDHTProtocol
- torrent文件分析
- bttorrent 种子文件结构解析
- BEncode编码方式以及torrent文件的一些内容
- Bittorrent Protocol Specification v1.0
- BitTorrent协议规范
- BEP-020 Peer ID Conventions
- BEP-029 uTorrent transport protocol
- peer之间的通信协议
- BEP-010 Extension Protocol
- extension protocol for bittorrent
- BEP-009 Extension for Peers to Send Metadata Files
- 【P2P网络】Extension for Peers to Send Metadata Files翻译稿
- Python将BT种子文件转换为磁力链的两种方法
- 常见P2P协议之BitTorrent 分析
- BT(带中心Tracker)通信协议的分析
- BT协议具体分析
- BitTorrent 协议分析与实现
- How PEX protocol (Magnetic links) finds it first IP?
- BT协议库libtorrent的种子文件解析方法探究