libtorrent打洞分析

相关变量

  • torrent_peer::supports_holepunch:torrent_peer类是每个peer的特征集合,其中supports_holepunch代表是否支持打洞,默认值是false
  • bt_peer_connection::m_holepunch_id: 对方peer申明的,它所支持的打洞消息的消息id。

种子来源

添加peer的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
torrent::add_peer
--->
peer_list::add_i2p_peer或peer_list::add_peer
--->
peer_list::insert_peer或peer_list::update_peer

```

如果flags参数包含`flag_holepunch`时,才设置`torrent_peer::supports_holepunch`为true。<!--more-->查找所有调用add_peer的地方,约有以下来源:

* 从resume文件里获得peer(上次开启任务保存的)
* dht方式从互联网中得到种子
* tracker从互联网中拿到种子
* lsd方式从本地网络得到种子
* pex方式从互联网中拿到种子

其中只有从pex方式得到互联网种子的调用语句使用flags参数,

# 判断是否支持打洞消息
在和另一个peer建立连接时,使用bt_peer_connection类,q原来成员变量bt_peer_connection::m_supports_extensions默认值为false,表示不支持bt扩展协议。

在bt_peer_connection的握手时,会发送是否支持扩展协议的标志,以及支持哪些扩展。
当确定对面也支持扩展时,进行扩展的handshake操作,其中发送出去的becode结构中,有几以下个整形变量upload_only、ut_holepunch、lt_donthave,他们就对应着本peer接收这3种通知时使用的消息id类型。

当从握手拿到了对方设置的ut_holepunch时,赋值给成员变量m_holepunch_id,此时此连接的函数supports_holepunch就返回了true值,表示支持打洞。

bool bt_peer_connection::supports_holepunch() const { return m_holepunch_id != 0; }

1
2
3
4
5
6
7
8
9
10
11
12

但bt_peer_connection的supports_holepunch()返回true只能说明此peer支持打洞,打洞是需要第三方服务器介入的,如何得到中间介绍人呢,见下节。

# 中间介绍人
上文提到,种子来源有pex方式。

当session开启了ut_pex插件时(`session_handle::add_extension(&libtorrent::create_ut_pex_plugin)`),在ut_pex_peer_plugin::tick()中会使用send_ut_peer_list和send_ut_peer_diff函数来向其它peer发送自己拥有的种子。其中`bt_peer_connection* p = static_cast<bt_peer_connection*>(peer); ... flags |= p->supports_holepunch() ? 8 : 0;`将peer信息发送给另外一个peer时,会带上了支持打洞的flags,然后接收方拿到peer后,向torrent里添加peer时,`torrent_peer::supports_holepunch`的值就会是true了。

假设自己是peer A,和peer B握手后知道B是支持打洞消息的,在和peer C连接时,使用pex扩展协议和peer C交换peer时,会把peer B的ip、端口、标志传递peer C,peer C拿到B后就满足了打洞的条件,因为A和B能正常通信,A和C也能正常通信,B和C就可以使用中间人A来进行打洞。

# 什么时候尝试打洞
在连接某个peer失败时,并且满足条件时,尝试进入打洞模式。见`void peer_connection::connect_failed(error_code const& e)`中,

if ((!is_utp(m_socket)
|| !m_settings.get_bool(settings_pack::enable_outgoing_tcp))
&& m_peer_info
&& m_peer_info->supports_holepunch
&& !m_holepunch_mode)
{
// see if we can try a holepunch
bt_peer_connection
p = t->find_introducer(remote());
if (p)
p->write_holepunch_msg(bt_peer_connection::hp_rendezvous, remote(), 0);
}
```
假设这段代码是peer C的机器上运行,也就是C从A拿到B的信息后,直接连接B失败了,就判断现在不是utp或者禁止tcp连接、并且支持打洞消息、当前不是打洞模式并且找到了中间介绍人(上文中提到的A),则向A发送消息开启打洞流程。

参数中bt_peer_connection::hp_rendezvous这个变量很有意思,rendezvous是约会地点的意思,当然在这里只是一个消息类型,remote()是B的ip地址信息。

A接收到C发送的请求打洞(介绍约会)消息后,在自己的peer里找到和B的连接,并且向B发送个打洞消息p->write_holepunch_msg(hp_connect, remote(), 0);,同时向C也回复一个打洞消息write_holepunch_msg(hp_connect, ep, 0);,这里就将自己和B、C的连接时使用的ip地址和端口通知给C、B。

B和C接收到类型为hp_connect的消息后,将torrent_peer::supports_utp置为true,将bt_peer_connection的m_holepunch_mode设置为true。然后同时开始尝试连接对方,这时,就完成了一个打洞流程,但是由于NAT的特性,不一定成功,具体参考NAT和打洞原理。

总结

  1. A从trcker拿到peer B和peer C,和B和C进行连接时,知道了B和C支持打洞消息。
  2. A和C进行pex种子交换,将B传递给C,此时B由pex传递的flags知道C能够通过A打洞。
  3. B如果连接C正常(不需要打洞)则正常连接。
  4. B如果连接C失败,就尝试通过A打洞。于是向A发送了打洞请求。
  5. A接收到B要向C连接的打洞请求,就给B和C发送了互相连接的打洞消息。
  6. B和C接收请求后,相互发送消息,可能会打通NAT。
net, P2P
p2p