在 bittorrent.org 开了个提案,想在邀请主要 BT 客户端开发者审阅前,在 CometBBS 先征求一些意见

提案地址是: [Proposal] DHT Hole Punching Extension Protocol · Issue #178 · bittorrent/bittorrent.org · GitHub

虽然 bittorrent.org 已经很多年没有新的 BEP 能进入了,但是至少试过了 :slight_smile:
任何建议都不胜感激!

下面是中文版本,由于原本使用的是英语编写的提案内容,所以用了 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 数据包。



  1. 10 个对等体是一个相对平衡的数值,因为很少有 Torrent 在没有任何可用于 PeX 打孔中继的对等体的情况下,还拥有超过 10 个对等体。此外,节点可以定期轮换自己的 want_rendezvous 列表。 ↩︎

  2. 使用 seq 的原因是两台计算机上的时钟可能并不同步。基于时间戳的窗口协商可能会产生显著的漂移,严重到足以导致打孔完全失败。 ↩︎

  3. 要求连续三次增加的原因是,如果任何一方经历网络连接不稳定,更新可能会延迟。如果我们直接进行打孔,可能会因为不稳定的网络延迟而失败。如果没有观察到连续三次增加,节点必须继续等待直到满足条件;如果超过 5 分钟仍未满足,则视为失败。 ↩︎

2個讚

虽然我还没有阅读文章,先回复一下
是的,目前只有比特彗星能够支持PEX与DHT的NAT1打洞,其它BT客户端一概不支持,libtorrent也只支持最基础的bep55,然而实际应用上bep55基本没有作用,真正实现NAT1打洞应当是走pex和dht
如图所示,比特彗星能在DHT网络中传播打洞后的端口

之前提交过给libtorrent等其它客户端让支持NAT1打洞,与那些开发者聊天过程得知他们根本不知道什么是内网ip不懂NAT1,因为他们每个人都有公网地址,所以到了现在,依旧还是只有比特彗星实现了NAT1打洞

1個讚

直接 NAT1 打洞需要持续向一个地址发送数据包,刷新自己端口在 NAT 跟踪连接表上的活动。一旦老化被淘汰,洞就失效了。
BitComet 自己有服务器,我想这不是问题。但是 libtorrent 等等可能不愿意出这个基础设施。

当然也可以直接把数据包指向藤子的服务器,但用户量上来之后还是要换掉的。


基于 DHT 似乎是最优解,Kad 网络路由完可以自然获得路径上的愿意提供打洞的节点,而且相同 infohash 下一定是双方都能选取同一批节点,这非常有助于交换信令。

此外就是,通过这种方式加 uTP 打洞,是可以同时打穿 NAT 1 2 3 的,只要双方别都是 4 就能通。

正确的nat1 PEX打洞操作是
与另一个peer建立连接后,双方都会把NAT1正确的端口通过PEX传递给对方与其它所有人,实现打洞
也就是做了一个端口号覆盖的操作,我不明白libtorrent开发者为什么看不明白

比如彗星的PEX支持A为公网,B为NAT3,两者打洞成功,网络异常断开后支持反向回连,不需要作为C的第三人

DHT打洞就和上面一样了
直接把端口通过implied_port发出去到DHT网络中,其它客户端依旧也不具备这一点,libtorrent 发送的永远是监听端口

你写的这篇文章看起来就是让其它DHT用户充当ip和端口的stun检测服务器,用来获取nat1背后的真实端口,我觉得难点是,libtorrent 开发者应该要把我上面提到的两点做上先实现
翻了下是2021年提交过的issues

bep55最大的作用还是私有种子下,因为私有种子会强制禁用PEX开关,只能通过bep55协议去获取端口号,就算是公开的BT种子也基本上很难拿到任何BEP55的打洞信息(虽然原理其实也是pex的一种,但是该协议对于私有种子不会被禁用)
image

2個讚

你写的这篇文章看起来就是让其它DHT用户充当ip和端口的stun检测服务器,用来获取nat1背后的真实端口,我觉得难点是,libtorrent 开发者应该要把我上面提到的两点做上先实现

事情其实是完全相反的。其它 DHT 用户(这里我称其为中继节点) 其实毫不关心双方的 IP 和端口。而且也不需要是 NAT 1,它可以是 NAT 1、2、3、4,只要不是双方都是 NAT4 就都可以通。

中继节点的任务其实是充当一个记事本,双方会在上面写下 DHT 节点 ID。

我是 AABBCC,我想要连接 DDEEFF,我要给它的数字是 X。
我是 DDEEFF,我想要链接 AABBCC,我要给它的数字是 X。

回应:

你好 AABBCC,当前 DDEEFF 想要连接你,它最后给你的数字是 X。
你好 DDEEFF,当前 AABBCC 想要连接你,它最后给你的数字是 X。


通过这样的轮询/更新,观察给出的数字有没有增加,就可以得知在没有直接通信的情况下得知双方是否互相注意到对方。
通过更改数字的频率,就知道对方是否进入了打洞状态。

只要互相都进入打洞状态,就会同时发起 uTP 连接。根据 NAT 的特性,双方同时连接可以打通 NAT 1 2 3 4(只要不都是4)。


而 IP 和端口——则是由 find_node 查询负责的,把节点 ID 查询为实际的 IP 和端口号。而这两个值是 announce_peer 查询创建的。

至于 implied_port 的问题,我无能为力,它属于另一个规范里的东西。

其实我没看懂,双方能够建立DHT通讯收到对方的数据包,那么代表已经打洞成功了,何必要多此一举记录这个数字
然后是数字高的告诉整个DHT网络我可以充当中续节点?至少我看起来并不能解决nat1打洞问题,可能我看不懂的原因
要想成功打洞,那么需要对方的真实端口,文章中我没看到如何获取到
比如B向作为A的我,对我的监听端口发送一个DHT数据包,那么我肯定是收不到的

总之要尽快被注意到的话,圈一下负责人 arvidn 来看issues吧,或者发送到 developer mailing list

1個讚

在打洞成功前,双方无法直接通信(因为NAT/防火墙阻止入站数据包)。
它们需要做的是在一个第三方的记事本上写东西,充当沟通作用。

那么小樱可能会问,诶诶这双方都能和中继节点通信为啥不直接一起通信完了算了。让它中继流量不就好了。
这还得回到 NAT 的问题,假设双方都在 NAT 后,你联系中继节点是你发送 UDP 包给中继节点,中继节点回包。这是出站连接,防火墙NAT给你准了。
但如果连接表老化后,防火墙把你的出站端口给关了,中继节点再主动给你发包,这就算入站连接,就收不到包了。

seq 是一个很巧妙的设计。虽然你看它仅仅是一个递增的数字,但它充当了很多的作用:

  • 它的存在,代表了对方注意到了你,想和你连接
  • 它的变化,代表了对方仍然在线,正在做准备工作,观察和你的通信
  • 它的连续变化,代表了网络是否通畅、另一方是否在忙其它事情、准备工作是否就绪,是否可以开始打洞

至于选择中继节点的问题,Kad 网络会自然选出来,就像你的磁力链接能够自然选出来元数据存在了哪些节点上一样(实际上哪些节点负责存储这个 infohash 的元数据文件,在这个提案里,哪些节点也就负责提供信令中继)。
只需决定查询哪个中继节点,和哪个中继节点通信,做法是直接广播,我全都要。这样另一方查哪个都能查得到。就算有节点中间跑路掉线了,DHT 的原理也保证消息肯定丢不了。

Kad 的工作原理就像菜鸟驿站。info_hash 不但是种子标识符,在 kad 的世界里同时也是地理坐标。
每个 Peer 就是菜鸟驿站,当有人要获取 info_hash 对应的种子时,就会直接开车去 kad 里的那个坐标位置。
然后这个离这个坐标位置最近的 K 个(一般是20)节点就会负责存储你想要的种子元数据。

就是这样 :wink:

看起来这个新方案按解决的是 之前UDP打孔中 需要同任务下的公网用户充当stun服务器功能的问题
这个可以允许DHT网络中的其他节点承当stun服务器

这个提议也许可以发到qb(libtorrent)那边去 也许可以成为一种非标准方案

这个客户端发现也许可以优先选择已经连接的客户端
特别是哪些发起为本地的 这说明对面的端口是打开的
此外断头档也可以持续的保持一些用户在线

2個讚

我看不懂但是大为震撼

现在都有ipv6公网了。再打通v4可以获取其他优势吗,比如上传更快?查找用户更快等等?

因为IP6永远不成主流了 就是这么简单,甚至运营商大量收回IPV6了(某些地区),而且IP6自身问题更多,更复杂

1個讚

只开ipv6绿灯↓

v4v6双绿灯↓

对诶,我也知道区别,但是没有人科普过。更别说开发者了。

1为什么v4可以获得更多用户,这里面当然有v4追踪器的缘故,但是其他人不会添加v6追踪器吗。并不清楚其他国家的网络组成,美国人均v4,巴西也可以人均v4吗?

2这图里面给巴西用户的上传速度4m,中国用户200k​:sweat_smile:

3连个手机流量都有v6了,这些国外用户居然没有v6?!

外国没人用IPV6,那些有IP6的都是附送的 连主流网游都不支持IP6就知道怎么回事了。 禁用IP4就无法玩网游啰

不是有地址就行的,要开绿灯。注意【发起】那一列,基本都是“远程”的。如果没有绿灯,这一列就会只有“本地”,代表你只能主动发起连接,获取不到别人的连接请求。由于ipv4地址已经分配完毕,大多数用户都得不到公网(或者说”独立的“)ipv4地址,那他们就都只能主动发起连接,两个这样的用户之间就无法直连。而开绿灯的用户不受此限制,这才是上图连接数差距的真正原因

如果你的光猫是路由模式,那么默认情况下 IPV6 防火墙会被运营商启用。你的 IPV6 实际上无法接受任何传入连接,除非打洞。

如果要竞选谁的nat最差的,我应该是最差的一档吧。如图nat4,不知道为什么有时候是nat3,有时候在绿灯的情况下显示udp被拦截,即使我经常刷下行流量,上传流量经常在100k左右,有时候又正常了,比如现在的5m/s。实际上我打联机游戏也会影响,问宽带师傅为什么打游戏卡,他说附近有什么基站信号干扰什么的,一直没解决,我注意到的说比特彗星检查的ipv4地址,是当地人民政府公共地址,不知道有多少人会和我一样。

还有我这一层nat用luck什么的检查公共v4端口改变打洞是没戏的。

实际上默认密码光猫可以关防火墙,其他地方可能不一样,我关了防火墙还是怪怪的吧,我的nat4网络连接俄罗斯网友就是提不起来,偶尔一些国际用户不受影响,不知道他们是怎么做到的,我有用反吸血php程序,应该不会遇到恶意的