提案地址是: [Proposal] DHT Hole Punching Extension Protocol · Issue #178 · bittorrent/bittorrent.org · GitHub
虽然 bittorrent.org 已经很多年没有新的 BEP 能进入了,但是至少试过了 ![]()
任何建议都不胜感激!
下面是中文版本,由于原本使用的是英语编写的提案内容,所以用了 Gemini 做了翻译:
BEP 草案:DHT 打孔扩展协议
概述
此文档介绍了一个扩展,允许对等体通过 DHT 上的信令中继节点完成打孔操作。该扩展无需依赖 PeX 或者 BitTorrent 连接,并完全通过 DHT 完成交换,与现有主线网络兼容。除此以外,对于不同的 Info Hash 将自然选择不同的中继节点,这还可以避免对单个中继节点产生过大的负载,并将流量自然平衡到不同空间的节点中。
理由
BEP-55 介绍了一种通过 PeX 协议进行 NAT 打孔的标准。尽管 IPv6 日益普及,但大量运营商仍然决定禁止用户的 IPv6 入站连接,因此打孔仍然必要。然而 BEP-55 存在一个固有缺陷——即需要可以连接到双方的第三者来中继打孔信令。如果一个 Torrent 没有满足 PeX Holepunch 条件的对等体 —— 或者所有的对等体都位于 NAT 之后,则打孔将无法成功,因为这依赖于 PeX 消息,但没有任何可连接的 Peer 可以打破这个局面。
消息
拟议的消息包含对现有消息的向前兼容的扩展,以及一个新的消息,用于更改中继节点上的状态。
扩展 announce_peer
此文档为现有的 BEP-0005 中描述的 announce_peer 消息扩展了 support_holepunch 字段,其值固定为 1,以向 DHT 空间的其它潜在信令中继节点指示自己支持 DHT 打孔扩展协议。如果支持此协议但不想接受打孔请求,则不应添加此字段。
参与者通过这种方式无需进行 get_peers 节点发现查询,就可以获取初始节点列表。
arguments: {"id" : "<querying nodes id>", "info_hash" : "<20-byte infohash of target torrent>", "support_holepunch": 1}
其它支持此扩展协议的节点在回复此消息时,也需要添加一个额外的字段指示自己愿意成为此 Info Hash 的 DHT 信令中继节点。如果支持此协议但不想成为信令中继节点,则不应添加此字段。
response: {"id" : "<queried nodes id>", "token" :"<opaque write token>", "values" : ["<peer 1 info string>", "<peer 2 info string>"], "accept_rendezvous": 1}
or: {"id" : "<queried nodes id>", "token" :"<opaque write token>", "nodes" : "<compact node info>", "accept_rendezvous": 1}
扩展 get_peers
通过扩展 get_peers KRPC 方法,我们可以在查询指定 Peers 的连接信息的过程中,同时找到愿意提供 DHT 打孔消息中继的节点。这是通过添加一个值固定为 1 的额外字段 support_holepunch 来完成的。
支持此扩展并愿意成为对应 Info Hash 的节点应该在响应中包含 accept_rendezvous 字段,其值固定为 1,否则不应包含此字段。
这提供了一种向前兼容发现支持 DHT 打孔的客户端的方式。通过完成 get_peers 查询,客户端最终能够得到一份查询路径上愿意中继打孔信令的节点列表 $L$。
这是因为在 Kademlia 算法中,由于 info_hash 的距离是确定的,双方一定会向距离 info_hash 最近的 $K$ 个节点查询,查询范围逐渐收敛。
我们截取 $L$ 的最后(也就是距离 info_hash 最近)的 $K$ 个节点,组成列表 $R$。列表 $R$ 将自然包含双方都共享的服务节点。该列表应定期刷新。
arguments: {"id" : "<querying nodes id>", "info_hash" : "<20-byte infohash of target torrent>", "support_holepunch": 1}
response: {"id" : "<queried nodes id>", "token" :"<opaque write token>", "values" : ["<peer 1 info string>", "<peer 2 info string>"], "accept_rendezvous": 1}
or: {"id" : "<queried nodes id>", "token" :"<opaque write token>", "nodes" : "<compact node info>", "accept_rendezvous": 1}
refresh_rendezvous
新增了一个新的 refresh_rendezvous KRPC 方法,用于刷新信令中继节点上的当前状态。对于同一目标节点,其状态应在所有 $R$ 中继节点上保持同步,不应维护独立的状态机。
arguments: {"id" : "<querying nodes id>", "token" :"<opaque write token>", "pkt_seq": 1, "info_hash" : "<20-byte infohash of target torrent>", "want_rendezvous": [{"id": "peer 1 id", "seq": 1}, {"id": "peer 2 id", "seq": 1}]}
response: {"id" : "<queried nodes id>", "token" :"<opaque write token>", "pkg_seq": 1, "others" [{"id": "peer want rendezvous you 1 id", "seq": 1}, {"id": "peer want rendezvous you 2 id", "seq": 3}]: }
如果一个节点支持此协议但不想接受 refresh_rendezvous 请求(例如由于资源不足),它应该静默忽略这些请求。
参数
请求参数中的 want_rendezvous 字段包含了当前客户端在此 Info Hash 上想要会合的最多 10 个对等体的节点 ID。如果一个查询里包含超过 10 个对等体[1],则必须返回 KRPC 标准错误响应 203 Protocol Error。为了防止单个节点消耗过多资源,中继节点应仅存储来自同一节点的最新的 10 条记录,当节点发送新的列表时,使用 LRU 算法剔除旧记录。
pkt_seq 字段是由请求节点提供的 uint_32 递增字段。其主要目的是防止链路上的 UDP 延迟导致 seq 字段值发生回退。如果从具有相同 Info Hash 的同一节点收到的查询中,其 pkt_seq 值低于上一次查询的值,则应静默忽略。
刷新
类似 ping 方法一样,客户端应该每 5 分钟[^periodicallybroadcast]向 $R$ 中的所有中继节点广播此查询,以便刷新自己的状态,并从中继节点上获取任何希望与自己会合的对等体信息。如果一个节点超过三个轮询周期未发送新的 refresh_rendezvous,它将被中继节点剔除。
不透明写入令牌 (opaque write token) 可能会在请求之间过期。如果发生这种情况,中继节点应在响应中返回一个新的令牌。如果节点的令牌过期,它必须通过执行 get_peers 查询来刷新令牌。
会合
要启动会合流程,需要双方同意并协商会合窗口。同意的方式是将目标节点 ID 加入到下一次广播的 want_rendezvous 参数中,并将 seq 置为 1。如果目标已经在自己的 want_rendezvous 列表中,则将 seq 值加 1[2]。随后,将下一次 refresh_rendezvous 请求的间隔缩短为 15 秒,直到会合成功或失败。此外,在启动会合流程前,应先通过 find_node 获取目标对等体的连接信息,以为发起 uTP 连接做准备。
当 $R$ 中的中继节点的响应中包含目标节点不同的 seq 值时,以最大的为准。这是为了防止链路上的 UDP 包延迟导致 seq 值回退。
若连续 3 次[3]查询的 seq 值都比上一次请求的更大,则应立即向目标发起 uTP 连接,并持续尝试最多 2 分钟。这足以让双方同时发送 UDP 包并穿透 NAT,建立 uTP 连接。连接建立期间,refresh_rendezvous 消息应继续发送以保持更新。
当所有连接尝试全部失败后,则应将目标节点 ID 忽略 1 小时,并将其从 want_rendezvous 中移除,以避免过多的失败尝试。下次重新尝试时,seq 必须重置。
refresh_rendezvous 基于轮询机制运行,这是出于 DHT NAT 穿透的考虑。由于 NAT 的特性,如果你能连接到一个中继节点,通常你就能再次连接。然而,相反的情况也可能发生,即 UDP 追踪表项过期,导致无法接收传入的 DHT 数据包。




