前序文章:
前文内容回顾:
BitTorrent 是一种用于分发文件的协议,元数据文件采用 bencode 编码,分片进行 SHA-1 哈希计算比对,并介绍元数据文件数据结构,通过 HTTP 请求由 Trakcer 交换节点信息,节点直接直接进行通讯。
分布式哈希表(DHT)中,每个节点有自己的 ID 和路由表,通过 KRPC 在 DHT 中可以获取指定信息哈希对应的下载者信息。
在第四部分文章中阐述了分布式哈希的工作方法,在这篇文章中,要分别分析拓展协议盒元数据传输拓展,在开始之前,有一些细节上的理解可以在此进行讨论,这并没有被具体说明,仅根据个人理解做出的判断,如果有不同的看法意见,欢迎提出并共同讨论。
这部分主要针对我在理解 BEP 过程中产生的疑问和分析的结果,如下:
一个下载器的节点 ID(peer id)和它正在下载的内容的元数据的信息哈希(infohash)有什么联系?
并没有特别强的联系,下载内容的信息哈希是根据元数据文件内容确定的,节点 ID 是随机选定的,它们仅仅是形式上相近,都是 20 字节类似 SHA1 结果的字符串,但并没有内在的联系。
既然节点ID和信息哈希没有内在联系,那又是如何通过信息哈希找到节点的呢?
通过信息哈希可以找到对应节点,可以将每一个 DHT 节点看作是一个增强版的 Tracker,通过 Tracker 可以获取到下载该数据的 IP 或域名地址及对应的端口,并无法获取这个节点的 DHT ID,这是并不冲突的。
DHT 是否可以获取元数据文件?
很显然不可以,在文中也反复进行了说明。
既然无法获取元数据文件,那 DHT 节点的作用是什么?
正如前述,每一个节点口可以看作是一个 Tracker,当通过信息哈希进行请求时,如果知道对应的节点,将返回对应的节点,否则将会尝试寻找更近的节点进行响应。
作为一个下载器,annouce_peer 应该向哪些节点发送?
当要宣布正在下载特定信息哈希的种子时,通常需要向与该infohash最接近的一组节点发出announce_peer,这些节点的数量通常为该信息哈希对应的桶内的所有节点。
作为 DHT,明知道有更多更近的节点,但还是收到了 announce_peer,应该如何处理?
即使有其他当前节点 ID 更接近目标信息哈希的节点,仍然应该处理 announce_peer 请求,存储并为 DHT 网络的其他用户提供帮助。 DHT协议的目标是分散和共享信息,因此即使不是最接近的节点,仍然可以为整个网络做出贡献。
如果某个节点将自己的 annouce_peer 发送给了它认为最近的几个节点,但由于它未发现更近的很多节点,是否会导致请求过程无法找到该节点?
这是可能的。但通常不会导致无法找到该节点,因为DHT网络是动态的,信息会不断传播和更新。在很多 DHT 实现过程中,节点根据网络和优化策略,有选择地将消息发送给最接近的节点,这是一种优化措施,不是协议的强制要求。需要注意的是,这可能会增加网络流量和负载。
如前所述,DHT 提供的仅仅是节点信息,并不能传输和交换元数据信息,更不是用来传输文件的,想要通过信息哈希获取到元数据信息保存为元数据文件,需要 BEP 0009 中的节点发送元数据文件拓展实现,在这一章节中,将要讨论的是 BEP 0010 规定实现的拓展协议,元数据文件交换的内容将在下一章节进行讨论。
BEP 10 的目的是在不干扰 BitTorrent 协议的情况下,为 BitTorrent 提供一个简单的传输功能。
为了标识次扩展,需要在握手信息中将预留位从右向左计算第 20 位置位进行标识。
当握手双方均支持该协议,其通讯内容新增
标记 | 说明 |
---|---|
0x14 | extended |
通过 extended 来实现拓展协议的功能,extended 消息包括:
握手信息的负载是一个字典,字典中所有的内容都是可选的,区分大小写,对未知的键值都可以进行忽略,这一部分如果较难理解,可以结合下一章节的实例进行分析,字典中通常包括:
例如,这是一个拓展握手信息:
d1:md11:LT_metadatai1e6:µT_PEXi2ee1:pi6881e1:v13:QCloud_rand 1e
其对应的握手信息:
{
"m":{
"LT_metadata":1,
"ut_pex":2,
},
"p":6881,
"v":QCloud_rand 1
}
在 BEP10 中,并没有定义任何一个拓展协议,具体需要由其他拓展进行支持,在下一章中将以 元数据文件交换拓展 作为目标进行分析,此外,在 BEP 0010 中,做出了如下一些说明,具体解释了不使用全局信息 ID,规范名称前缀,使用字典而不是数组,使用单字节作为标识符和使用常量的原因,感兴趣可以自行翻译并查看。
元数据传输拓展基于上述拓展协议,允许客户端从对等点下载元数据,使得磁力链接成为可能。
在此部分处理过程中,元数据以 16KiB(16384 字节)的块进行处理。元数据块的索引从 0 开始。除了最后一个块可能更小之外,所有块都是 16KiB。
拓展头:
元数据传输拓展将 ut_metadata 加到扩展标头握手消息中的“m”字典中。还将 metadata_size 添加到握手消息中来,指定元数据字节数的整数值,例如这是一个握手信息:
{
"m": {
"ut_metadata", 3
},
"metadata_size": 31235
}
该拓展消息有三种类型:
请求
通过请求消息来请求元数据的分片
数据
数据消息在字典中添加了另一个键 total_size。
元数据片段跟在字典后,它是消息的一部分(长度计算包括它)。
拒绝
出于安全考虑,下载器可拒绝其他连接的传输请求。
在这里再次提醒,请注意:频繁请求 DHT 但不为 DHT 提供数据是自私且不被社区推荐的行为。
DHT 的实现并不是本文的重点,在此,直接使用现有的库进行处理,本次使用了基于 Go 语言的 DHT(https://github.com/nictuku/dht) 库来进行 peer 获取。
为了方便演示,选择了 ubuntu-22.04.3-live-server-amd64.iso 的官方发布作为实例,其信息哈希为:
da1a0defb35d43a218fc7eb0fc8d4c6c44a3ed2d
通过 DHT 获取节点使用如下程序:
package main
import (
"fmt"
"github.com/nictuku/dht"
"os"
"time"
)
func main() {
ih, _ := dht.DecodeInfoHash("da1a0defb35d43a218fc7eb0fc8d4c6c44a3ed2d")
d, _ := dht.New(nil)
_ = d.Start()
go drainresults(d)
d.AddNode("router.bittorrent.com:6881")
d.AddNode("dht.transmissionbt.com:6881")
d.AddNode("router.utorrent.com:6881")
for {
d.PeersRequest(string(ih), false)
time.Sleep(5 * time.Second)
}
}
func drainresults(n *dht.DHT) {
count := 0
for r := range n.PeersRequestResults {
for _, peers := range r {
for _, x := range peers {
fmt.Printf("%d: %v\n", count, dht.DecodePeerAddress(x))
count++
if count >= 10 {
os.Exit(0)
}
}
}
}
}
改程序启动了一个 DHT 监听器,并循环发送 request peers 查询,来获取前 10 节点,运行结果如下:
需要注意,DHT 是一个匿名的公共维护的环境,其中包含很多恶意存在的数据,可能 DHT 所提供的节点是错误的,需要进行逐一检查节点连接性,对连接良好的节点使用前文所述 Sockit 工具进行连接,若这些节点都无法连接或无法响应请求,则尝试通过 DHT 获取更多节点,使用信息哈希进行握手,随后进行扩展握手,发送元数据传输请求:
此处握手信息和前文所述内容一致,握手信息:
在收到对方回复的握手信息后,判断是否启用了拓展,若启用,则发送拓展握手包,由于手动进行发包测试,几乎在发包之前就收到了对端发送的拓展握手包,通过分析其 ut_metadata 对应的信息 ID,构造拓展握手包,如:
{
"m": {
"ut_metadata", 3,
},
"p": 6881,
"v": "QCloud_rand 1",
"metadata_size": 31235
}
对其编码后计算消息长度,根据上文内容构造数据包:
[00 00 00 4D 14 00 64 31 3A 6D 64 31 31 3A 75 74 5F 6D 65 74 61 64 61 74 61 69 33 65 65 31 33 3A 6D 65 74 61 64 61 74 61 5F 73 69 7A 65 69 33 31 32 33 35 65 31 3A 70 69 36 38 38 31 65 31 3A 76 31 33 3A 51 43 6C 6F 75 64 5F 72 61 6E 64 20 31 65 ]
在完成拓展握手后,即可发送元数据请求,如:
{
"msg_type": 0,
"piece": 0,
}
消息类型 0 对应请求,当前没有任何数据,请求第 0 片数据,下面是拒绝的例子:
可以清楚看出,回复的消息类型为 2 ,即拒绝。
更换目标节点后重新进行发送,有如下成功请求:
其中红框标注的两个请求分别是请求第 2 片 和 第 0 片数据的响应,蓝框标记的是获取的目标元数据文件的开头。
以此循环,最终获取整个元数据文件,同时,需要在获取完成后校验 SHA 1 值以避免传输错误或恶意节点提供的错误数据。
Magnet URI(磁铁链接,磁力链接)格式为:
v1:magnet:?xt=urn:btih:<信息哈希>&dn=<名称>&tr=<Tracker地址>&x.pe=<节点地址>
v2:magnet:?xt=urn:btmh:<目标信息哈希>&dn=<名称>&tr=<Tracker地址>&x.
pe=<节点地址>
目前我们仅分析了 BEP 3 提出的 BitTorrent 协议,还尚未分析新的 BitTorrent 协议,所以暂时忽略 v2 版本的地址,只看 v1 版本,阅读到这里的你,应该已经没有不理解的内容了,仅仅需要注意,出于兼容性设计客户端还应该支持 32 个字符的 base32 编码的信息哈希。
对于一个链接,只有信息哈希是必须的,其他都是可选参数。
本部分内容首先对前述内容容易产生的问题进行了进一步分析和讨论,分析了 BEP 10 和 BEP 9 两个元数据,并根据次进行了 DHT 查询,获取元数据的实例。
在实践过程中,DHT 经常会返回非常多错误的信息,有的信息可以很明显看出是虚假的信息,有趣的一点是在完成文章过程中,通过中文互联网查询到了一些内容相近的文章,它们加入 DHT 后面对请求在处理 DHT 的时候有一个相同的逻辑:伪造随机的数据进行返回,并经常随机更新自己的节点 IP 以避免被其他节点拉黑。频繁请求查询,特别是向同一个节点频繁进行查询都可以认为是不礼貌的行为,仅仅查询不为其他节点提供服务是不值得提倡的环境下,随机返回错误的数据是对社区的公然破坏,在此对这样的行为表示愤怒和反对。(又:如果出于测试完全可以标记只读来告知其他节点不向自己发送请求,而不需要进行伪造响应)
这篇文章的内容到这里就结束了,截止目前,已经完成了 BEP 中 3 5 9 10 项规范的分析,后续的文章将会分析更多内容,如果有的话,链接会放在这里,还请多多关注:
最后,本文参加的征文活动广告:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。