Sirius
Sirius

目录

BT握手与TCP的连接与释放

系列 - P2P下载解析

定位了DHT爬虫的一个问题,在这梳理一下BT握手流程

BitTorrent 客户端(假设为 A)与另一个 Peer(假设为 B)从建立 TCP 连接开始,经过标准握手、扩展握手,最终到通过 ut_metadata 扩展获取种子元数据的完整流程

  • 客户端 A 知道 Peer B 的 IP 地址和监听端口 (IP_B, Port_B)。
  • 客户端 A 知道它感兴趣的种子的 info_hash
  • 客户端 A 没有这个种子的元数据通过磁力连接向B请求这个种子的信息
  • 我们假设 Peer B 拥有这个种子的元数据,并且支持 BEP-10 扩展协议和 ut_metadata 扩展。
  • 环境是我本地的环境这里A的地址为192.168.30.125:59806, B的地址是192.168.30.230:63219

整体的流程如图,除了最后一次挥手没等停止抓包了,

图中长度66的都是没有其他信息的, dMAC(6)+sMAC(6)+type(2)(ipv4 0x0800) = 14 IP 头 5 * 4 = 20 TCP 头 5 * 4 = 20 + 可选 12 字节(主要是默认开的时间戳10B) 共66B

  • 客户端 A 的操作系统向 Peer B 的 (IP_B, Port_B) 发送一个 TCP SYN 包
  • Peer B 的操作系统接收到 SYN 包,如果端口正在监听且允许连接,则发送一个 TCP SYN-ACK 包 回给 A。
  • 客户端 A 的操作系统接收到 SYN-ACK 包,发送一个 TCP ACK 包 给 B。
  • 结果: TCP 三次握手完成。A 和 B 之间建立了一条可靠的、面向连接的 TCP 通道。现在可以互相发送应用层数据了。 Pasted image 20250415204420.png
  • TCP 连接建立后,客户端 A 立即 构造标准的 68 字节 BitTorrent 握手消息: <"BitTorrent protocol"><8 reserved bytes><info_hash><peer_id_A>
  • 其中 reserved 字节的第 5 字节的位 4 设置为 1,表示 A 支持 BEP-10 扩展协议如果双方都在这个位上设置为 1,它们就可以在标准握手之后立即交换扩展握手消息,从而协商使用如 ut_metadata (元数据交换), ut_pex (Peer Exchange) 等多种现代 BitTorrent 扩展功能。
  • 字节 7 (索引为 7), 位 3 (第 3 位)。掩码 (Mask) 0x04 (十六进制) 或 00000100 (二进制)
  • 设置此位置表明该 Peer 支持 BEP-4 DHT Protocol (DHT 协议)。表明客户端理解并能够参与 DHT 网络。如果设置了此位,意味着该 Peer 可能运行一个 DHT 节点,并且(根据 BEP-4)理论上可以在与 BitTorrent P2P 流量相同的端口上响应 DHT 查询,不过我们KRPC监听端口和tcp端口不一样所以没有设置,设置此位是加入 Mainline DHT 网络的前提。
  • A 将这 68 字节数据交给其 TCP 协议栈,发送 TCP 数据包 给 B。 Pasted image 20250415205800.png
  • Peer B 的 TCP 协议栈 接收 TCP 数据包,并将数据递交给 BitTorrent 应用层。
  • B 检查协议字符串是否为 “BitTorrent protocol”,检查 info_hash 是否是它关心的种子。假设匹配。
  • B 记录下 A 的 peer_id_A
  • B 检查 A 发来的 reserved 字节,发现 A 支持 BEP-10。
  • Peer B 构造自己的 68 字节标准握手消息: <"BitTorrent protocol"><8 reserved bytes><info_hash><peer_id_B> (B 也在 reserved 字节中表明自己支持 BEP-10)。
  • B 发送 TCP 数据包 给 A。 Pasted image 20250415210136.png
  • 客户端 A 的 TCP 协议栈 接收 TCP 数据包
  • A 验证协议字符串和 info_hash
  • A 记录下 B 的 peer_id_B
  • A 检查 B 发来的 reserved 字节,确认 B 也支持 BEP-10。
  • 结果: 标准握手完成。双方确认了对方身份和意图(针对同一个 info_hash),并且都知晓对方支持扩展协议。
  • 这里有点不一样的是,BT客户端的实现互不相同,有的会等A发送扩展握手后在回应,有的直接在握手包后面回复扩展握手了,这里注意一下,前面读取握手包只读取68字节,剩下的,扩展握手时候在处理,qbt就是这种。
  • 因为 A 和 B 都支持 BEP-10,所以此阶段会进行。_
  • 扩展握手很简单,不过要注意ID, 双方后续通信需要使用对方握手时候使用的ut_metadata ID 而且不能取0
  • 客户端 A 构造 BEP-10 扩展握手消息 (消息 ID 固定为 0): <长度前缀><Bencoded 载荷> 载荷是一个 Bencode 字典,至少包含 m 字典,例如 {"m": {"ut_metadata": 1}} (表示 A 希望 B 在发送 ut_metadata 消息给 A 时使用 ID 1)。可能还包含 v (版本), metadata_size (如果 A 碰巧有元数据的话,但在此场景下 A 没有,所以不含或为 0) 等。
  • A 发送 TCP 数据包 给 B。 Pasted image 20250415213230.png
  • Peer B 接收 TCP 数据包
  • B 解析载荷,记录下 A 要求使用的扩展 ID 映射 (例如,知道发送 ut_metadata 给 A 要用 ID 1)。记录 A 的版本等信息。
  • Peer B 构造自己的 BEP-10 扩展握手消息 (消息 ID 固定为 0): <长度前缀><Bencoded 载荷> 载荷包含 B 的 m 字典,例如 {"m": {"ut_metadata": 3}, "metadata_size": 9308} (表示 B 希望 A 在发送 ut_metadata 给 B 时使用 ID 3,并且 B 宣告它拥有的元数据大小为 12345 字节)。
  • B 发送 TCP 数据包 给 A。
  1. A 接收并处理扩展握手(108后半部分): Pasted image 20250415210405.png
    • 客户端 A 接收 TCP 数据包
    • A 解析载荷,记录下 B 要求使用的扩展 ID 映射 (知道发送 ut_metadata 给 B 要用 ID 2)。
    • A 从 B 的握手消息中获知了元数据的确切大小 (metadata_size: 9308)。
    • 扩展协议握手完成。双方知道了如何互相发送扩展消息(特别是 ut_metadata),并且 A 知道了元数据的总大小。
    • 解释一下其余字段,ut_pex 是节点交换协议标识位,reqq是说明piece分片请求中,由于可以流水线处理,最大支持的处理数,v 可选是版本, your ip 是对方看到的IP
  • 因为 A 需要元数据,且 B 支持 ut_metadata 并拥有元数据,所以此阶段会进行。A 知道需要向 B 发送 ID 为 3 的消息来请求元数据。_
  • 客户端 A 根据获知的 metadata_size (9308 字节) 和标准块大小 (16 KB = 16384 字节),计算出需要请求多少个元数据块 (我们这个就1快儿)。假设元数据大小是 30000 字节,则需要请求块 0 和块 1。

  • A 开始请求第一个元数据块 (索引 0):

    • 构造 ut_metadata 请求载荷 (Bencode 字典): {"msg_type": 0, "piece": 0}

    • 构造 BEP-10 消息: <长度前缀><上述 Bencoded 载荷> (注意消息 ID 是 3)

    • A 发送 TCP 数据包 给 B。

    • (如果需要更多块,A 可以连续发送对块 1, 2… 的请求,不必等待响应,这称为流水线 Pipelining)。 Pasted image 20250415213501.png

  • Peer B 接收 TCP 数据包
  • B 解析消息,识别出是 ID 为 2 (对应 ut_metadata) 的消息,再解析内部载荷,看到是 msg_type: 0 (请求),请求的是 piece: 0
  • B 获取元数据的第 0 块数据(从字节 0 到 16383,但在此例中只有 9308 字节)。
  • B 构造 ut_metadata 数据载荷 (Bencode 字典 + 原始数据): {"msg_type": 1, "piece": 0, "total_size": 9308} 后紧跟 9308 字节的元数据块。
  • B 构造 BEP-10 消息: <长度前缀><上述 Bencode 载荷 + 原始数据块> (注意消息 ID 是 1,这是 A 在握手中指定的 B->A 的 ut_metadata ID)。
  • B 发送 TCP 数据包 给 A。
  • 如下图显然我们这里tcp分包了,然后在119重组 Pasted image 20250415213718.png
  • 客户端 A 接收 TCP 数据包
  • A 解析消息,识别出是 ID 为 1 (对应 ut_metadata),解析内部载荷看到是 msg_type: 1 (数据),对应 piece: 0,总大小 total_size: 9308
  • A 读取随后的 9308 字节原始数据。
  • A 将这块数据存储在内存中,作为元数据的第 0 块。
  • 如果元数据大于 16 KiB,A 会继续发送对后续块 (piece: 1, piece: 2…) 的请求,B 也会类似地响应数据块。A 持续接收并组装。
  • 当 A 接收完所有预期的块(根据 total_size 判断)后,它将所有块按顺序拼接起来,得到完整的 Bencode 编码的 info 字典数据。

  • A 计算这段完整数据的 SHA-1 哈希值。

  • A 将计算出的哈希与它最初从 Magnet 链接得到的 info_hash 进行比较。

  • 结果:

  • 匹配: A 成功获取并验证了元数据!现在它可以解析这个 info 字典,了解文件结构、大小、分块哈希等,并开始请求实际的文件数据块。

  • 不匹配: 获取元数据失败,A 可能会丢弃数据并尝试从其他 Peer 获取。

    Pasted image 20250415213910.png 大功告成了

实际连接可能不会这么快关闭,毕竟还要传输数据 Pasted image 20250415214713.png

实际下载中,会有些其他和拓展协议并列的消息,需要注意过滤掉,比如MsgTypeExt: 5的bitfield,用于表示B已经下载的分块。 Pasted image 20250415215132.png Pasted image 20250415215009.png 还有就是,可见我们连续发送了多个分片元数据请求,这个请求后B开始发送元数据到A,注意超时时间不要设置的太短,如果网络情况差,可能不会很快。

Pasted image 20250415220312.png

相关内容

KRPC请求格式
KRPC
Kademlia 算法(二)
Kademlia 算法(一)
BitTorrent 的 DHT协议