Hole-Punching Rust库
5 min read

Hole-Punching Rust库

上篇说libp2p终于支持hole punching了,可以支持很多应用场景了。为此,我还写了一个简单的应用p2p-tun。不仅实现了端口转发,还实现tun2socks了。但也发现了一些局限,比如不支持udp,比如relay不会自动重连等等。还有,libp2p是基于stream的。虽然也可以在tcp基础上传输udp,但是效率太低,现实意义不大。另外也发现libp2p委实太过臃肿,占用资源太多,Bug坑也不少。libp2p想要承担基础网络的功能,复杂是必然的。但是,对于一个普通的应用来说,带上这么一个臃肿的包袱是否合适呢?平台的事情就应该由平台来做,应用本身就可以就很简单。比如TCP/IP的协议很简单,但TCP/IP的支撑网络很复杂。socket应用接口很简单,internet的路由很复杂。因为libp2p的DHT实际上承载了internet路由的功能,所以很耗费资源。还有,libp2p的打孔功能,号称是分布式的。实际上呢,是先借助公网的Peer实现中转连接,然后才完成打孔(这不禁让我想起“国产凌凌漆”里那个有光才会亮而没有光绝对不会亮的手电筒)。现在是不依赖于特定的中转服务器了,但是寻找中转的效率太低,连接速度太慢。如果不非得追求分布式,直接依赖一台中心服务器来帮忙,那效率就高多了,也简单得多。

于是就想,有没有简单的打孔库呢?不需要复杂功能,不需要中转数据,只需要能完成打孔即可,其它的交由上层应用处理。事实上,我手头就刚刚完成几个vpn和proxy的程序,由于没有打孔功能,只能在公网环境下使用。如果能有一个模块,能够代为完成打孔,那岂不是很好。实际上打孔的原理很简单,就是要通讯的双方同时(几十秒之内)都给对方发消息,就可以打开NAT映射和防火墙规则了。只需要在通讯建立之前,由一台服务器代为中转消息即可。还有一个关键点是,与服务器通讯的port和Peer通讯的port必须相同。正好最近看了cloudflare的几篇blog,里面很详尽讲述了他们是如何利用SO_REUSEADDRSO_REUSEPORT搞定海量连接问题的。看起来也就是几行代码的事情,说干就干,于是就有了rndz库(rendezvous缩写)。rndz完成的事情很简单,对于客户端,给一个目标peer的id,帮你完成打孔动作,然后把原始的socket(没有额外的封装)交给你,直接用就行--就是简单把connect函数替换一下。对于服务端,则是把listen替换一下。

TCP

use rndz::tcp::Client;

let c1 = Client::new(rndz_server_addr, "c1", None)?;
c1.listen()?;
while let Ok(stream) = c1.accept()?{
//...
}
use rndz::tcp::Client;
let c2 = Client::new(rndz_server_addr, "c2", None)?;
let stream = c.connect("c1")?;

UDP

use rndz::udp::Client;

let c1 = Client::new(rndz_server_addr, "c1", None)?;
c1.listen()?;
c1.as_socket().recv_from(...)?;
use rndz::udp::Client;
let c2 = Client::new(rndz_server_addr, "c2", None)?;
c.connect("c1")?;
c.as_socket().send(b'hello')?;

对于TCP,还提供基于tokio的异步版本AsyncClient

接着是对之前的程序进行改造,以支持打孔。比如 minivtun-rs是一个基于udp的vpn,还有quic-tun是基于quic的tcp端口转发。因为rndz提供了原生的socket,所以改造成本很低!以上都是基于udp,我还需要改造一个基于tcp的实用应用。正在不久前,我发现过一个基于libp2p实现的文件传输程序pcp,理念挺不错,缺点就是libp2p建立连接的过程太漫长。现在要找一个rust写的(因为rndz是rust写的,因为之前的minivtun-rs也是rust),简单实用的,最好还有续传的。于是发现了teleport,这个没有对网络传输进行epoll优化什么的,而直接用了rust原生TcpStream。这样把TcpStreamTcpListener简单替换一下,搞定!有了这个支持打孔的文件传输工具,就可以轻松在各虚拟机之间轻松传递文件,而不必依赖ssh,也不必考虑子网互通的问题了!

之前以为ipv6时代不需要打孔的,现实情况是,还得要!所以rndz库的存在,是有着现实意义的。实际上,目前的rndz client会自动根据rndz服务器的解析地址来选择本地绑定是使用ipv4还是ipv6地址。我目前临时提供两个测试server(ipv4: rndz.optman.net:8888 ipv6: rndz6.optman.net:8888),可以实测一下打孔效果。有一个好的测试工具很重要,我之前说过电信同城ipv4不互通,是不对的,其实是可以通的。

peer1

$ rndz client --id c1 --server-addr rndz.optman.net:8888 

peer2

$ rndz client --id c2 --server-addr rndz.optman.net:8888 --remote-peer c1

rndz目前只是简单实现了功能,有很多细节并没有考虑。比如id的生成和校验问题,通讯是否要加密等等。我认为呢,id生成交由上层应用或者用户根据业务生成更方便灵活。而加密问题更应该是上层tls等考虑的,底层尽量简单,毕竟这做的是连接建立之前的工作,原本就是不安全的飞地。