EasyTier icon indicating copy to clipboard operation
EasyTier copied to clipboard

feat: add upnp support

Open debuggerchen opened this issue 4 months ago • 24 comments

添加Upnp支持 #420 #815 #1271 在STUN测试和UDP Hole Punching时将自动尝试映射EasyTier的相关端口 对于WAN/ISP支持Full Cone NAT,LAN不支持Full Cone NAT但支持Upnp的情形,本PR有助于实现P2P直连 谢谢 顺祝中秋快乐

debuggerchen avatar Oct 06 '25 10:10 debuggerchen

引入了一个igd-next依赖用于处理upnp。其仓库位于rust-igd。目前其官方最新版本尚不支持ipv6环境,因而此处使用了我修改后的支持ipv6的branch。目前已经向igd-next提交了PR,合并后可以换到官方源。

debuggerchen avatar Oct 06 '25 12:10 debuggerchen

我之前一直以为 EasyTier 支持 UPnP(

btw,如果方便的话,有必要往 cli 的 STUN 测试里加上 UPnP 测试吗...?

Pigeon0v0 avatar Oct 06 '25 13:10 Pigeon0v0

cli和core两边都要从bind_request做stun测试。cli加上upnp能确保得到core一样的探测结果,或许这样更好一些不容易出现误解?现在只加了一个60s的upnp端口映射,大概影响不大?

debuggerchen avatar Oct 06 '25 13:10 debuggerchen

我感觉 upnp 应该映射 -l 的那个监听器吧,这样还可以复用 mapped-listener 功能

KKRainbow avatar Oct 06 '25 14:10 KKRainbow

另外这个 upnp 库是同步库吧,在 tokio 里用会阻塞线程吧

KKRainbow avatar Oct 06 '25 14:10 KKRainbow

我早前尝试过在尽可能少引入新依赖的前提下实现 upnp-igd 协议,commit 在这里,不过还没有完全调通,我的 openwrt 会报奇怪的错误。

https://github.com/EasyTier/EasyTier/commit/f272a1775ec75ce162b6d25db3355f128624a914#diff-fd68a3890144ee6ac17a4d82457cc42460e79b8d9fa195156aa48fe32c85ce20

KKRainbow avatar Oct 06 '25 14:10 KKRainbow

另外这个 upnp 库是同步库吧,在 tokio 里用会阻塞线程吧

Done

我感觉 upnp 应该映射 -l 的那个监听器吧,这样还可以复用 mapped-listener 功能

现在为了实现快一点,正好着急用。确实用mapped-listener更好一些

debuggerchen avatar Oct 06 '25 15:10 debuggerchen

我感觉 upnp 应该映射 -l 的那个监听器吧,这样还可以复用 mapped-listener 功能

我感觉用mapped listener的问题在于,如果ISP不是NAT1而是NAT2/NAT3,那么不光要对listener做端口映射,还要用listener socket/port向对方发包,感觉就有点奇怪了

另外,如果是开了mapped-listener,对于ISP NAT1场景来说,路由器后的用户就直接暴露在公网下,可能安全性上有些微影响。如果只是打洞的短暂时间内使用端口转发可能更安全一些

debuggerchen avatar Oct 06 '25 18:10 debuggerchen

换到异步upnp实现

debuggerchen avatar Oct 08 '25 06:10 debuggerchen

另外这个 upnp 库是同步库吧,在 tokio 里用会阻塞线程吧

Done

我感觉 upnp 应该映射 -l 的那个监听器吧,这样还可以复用 mapped-listener 功能

现在为了实现快一点,正好着急用。确实用mapped-listener更好一些

如果WAN直接是公网IP这样应该蛮好。如果WAN还走NAT,那还是有打洞需要,还要维持NAT Mapping状态,放在listener可能需要蛮大改动。我感觉可以考虑如果检测是公网IP就复用Mapped Listener来,如果是NAT就还是在打洞时处理。说的不对请赐教🙂

debuggerchen avatar Oct 08 '25 14:10 debuggerchen

如果WAN还走NAT,那还是有打洞需要,还要维持NAT Mapping状态

这里我没有很理解,假设网络拓扑是 A(EasyTier) -> B (Router) --- NAT1 --> C,A 在 B 上建立 upnp 映射,那么打洞实际是由 B 完成的吧。

我理解的过程是:A 发送 upnp 请求将 B 上的 54321 端口映射到 A 的 10010 端口,B 上建立映射后会通过 stun 获取 54321 端口的外网地址,后面 C 发往 B 公网地址的流量会被 B 转发到 A 的 10010。

这个过程中,A 的端口可以是固定的,所以没有必要放打洞逻辑里吧

KKRainbow avatar Oct 08 '25 14:10 KKRainbow

这里我没有很理解,假设网络拓扑是 A(EasyTier) -> B (Router) --- NAT1 --> C,A 在 B 上建立 upnp 映射,那么打洞实际是由 B 完成的吧。

NAT1的话这样可以,但是也需要复用Listener的端口/socket定时发送一些包保活NAT。我不确定现在是否已经有类似机制 另外UPnP应该也能解决WAN是NAT2/3的问题,但打洞时也要复用Listener的端口/socket去发送。 可能会涉及对现在stun/udplistener的修改?

debuggerchen avatar Oct 08 '25 14:10 debuggerchen

NAT1的话这样可以,但是也需要复用Listener的端口/socket定时发送一些包保活NAT。我不确定现在是否已经有类似机制

UDP 端口是多路复用的,假设还是刚才的例子,C 在连接到 A 后,更上层会发心跳包保活的。保活包 src port 就是 10010,dst 是 C 的地址。

另外UPnP应该也能解决WAN是NAT2/3的问题,但打洞时也要复用Listener的端口/socket去发送。

NAT2/3 的话 UPnP 应该是无能为力的吧,UPnP 应该是只映射了入站,不会映射出站的。也就是说 A 节点发出的打洞包,不一定会走 B 上的对应端口。

KKRainbow avatar Oct 08 '25 14:10 KKRainbow

UDP 端口是多路复用的,假设还是刚才的例子,C 在连接到 A 后,更上层会发心跳包保活的。保活包 src port 就是 10010,dst 是 C 的地址。

如果是中途一段时间内暂时没有udp peer,是否会有问题呢?

NAT2/3 的话 UPnP 应该是无能为力的吧,UPnP 应该是只映射了入站,不会映射出站的。也就是说 A 节点发出的打洞包,不一定会走 B 上的对应端口。

确实UPnP主要入站。不过我这边的路由器上,看起来在SNAT时会更倾向尽量local port == external port,这样upnp内部端口跟外部端口设为一样的话,还是能测出来endpointindependent

debuggerchen avatar Oct 08 '25 14:10 debuggerchen

UDP 端口是多路复用的,假设还是刚才的例子,C 在连接到 A 后,更上层会发心跳包保活的。保活包 src port 就是 10010,dst 是 C 的地址。

如果是中途一段时间内暂时没有udp peer,是否会有问题呢?

NAT2/3 的话 UPnP 应该是无能为力的吧,UPnP 应该是只映射了入站,不会映射出站的。也就是说 A 节点发出的打洞包,不一定会走 B 上的对应端口。

确实UPnP主要入站。不过我这边的路由器上,看起来在SNAT时会更倾向尽量local port == external port,这样upnp内部端口跟外部端口设为一样的话,还是能测出来endpointindependent

现在 udp listener 有个 v6 hole punch 的特殊功能,作用是主动从这个 listener 发送一个 v6 数据包到指定的外部地址。这个功能如果扩展到 v4,是否可以解决你提到的两个问题呢

KKRainbow avatar Oct 08 '25 14:10 KKRainbow

现在 udp listener 有个 v6 hole punch 的特殊功能,作用是主动从这个 listener 发送一个 v6 数据包到指定的外部地址。这个功能如果扩展到 v4,是否可以解决你提到的两个问题呢

我看一下,谢大佬指路🌹

debuggerchen avatar Oct 08 '25 14:10 debuggerchen

最近有点忙手头也没合适的设备和网络环境,有点耽搁

个人还是感觉就目前的架构来看,放在打洞时upnp可能更优雅一点,不需要添加过多的复杂功能和耦合,能解决的情形也多一点

debuggerchen avatar Oct 24 '25 06:10 debuggerchen

我考虑一下。这个 pr 有测试过不,路由器上可以正确增加新的 upnp entry 不

KKRainbow avatar Oct 24 '25 07:10 KKRainbow

我用两台xiaomi路由器测试过了没有问题,如果有其他设备也可以试一试。 最近赶论文暂时顾不上了o(╥﹏╥)o

debuggerchen avatar Oct 24 '25 07:10 debuggerchen

我早前尝试过在尽可能少引入新依赖的前提下实现 upnp-igd 协议,commit 在这里,不过还没有完全调通,我的 openwrt 会报奇怪的错误。

f272a17#diff-fd68a3890144ee6ac17a4d82457cc42460e79b8d9fa195156aa48fe32c85ce20

不知道这个问题跟我在igd-next那边遇到的问题是否一致。

我用那个库的时候也遇到了报错。调试后发现是路由器上的miniupnpd会在upnp client search_gateway时同时以IPv4地址和IPv6地址响应,而且IPv6地址响应更快一些,导致会优先解析到IPv6地址。但是client代码在构造upnp url时,对IPv6地址url的构造是错误的,导致会报错。

此外,IPv6的upnp映射协议和IPv4的映射协议不同,加上miniupnpd貌似会要求client source ip和映射请求中的internal ip一致,导致向IPv6 upnp server请求映射IPv4地址下的某个端口就会报错。

我在igd-next那边提的PR就是加了一下对IPv6 upnp地址的过滤。大佬如果想自己实现的话,可以看看是不是上述问题导致的。

debuggerchen avatar Oct 24 '25 08:10 debuggerchen

大佬如果想自己实现的话,可以看看是不是上述问题导致的。

按照你的方法来实现吧。我就不做重复工作了

但是client代码在构造upnp url时,对IPv6地址url的构造是错误的,导致会报错。 我在igd-next那边提的PR就是加了一下对IPv6 upnp地址的过滤。

为啥这里的修复不是让它构造正确的 url 呢

KKRainbow avatar Oct 24 '25 08:10 KKRainbow

为啥这里的修复不是让它构造正确的 url 呢

也构造了正确的url,但是构造正确也没啥实际用途,因为↓

此外,IPv6的upnp映射协议和IPv4的映射协议不同,加上miniupnpd貌似会要求client source ip和映射请求中的internal ip一致,导致向IPv6 upnp server请求映射IPv4地址下的某个端口就会报错。

想用IPv6 upnp应该要实现IGDv2协议,但是懒了就没搞😂

debuggerchen avatar Oct 24 '25 08:10 debuggerchen

我希望还是做IPv6的功能,完整一些比较好

purifierprobe avatar Oct 30 '25 14:10 purifierprobe

是不是用portmapper库合适 这个upnp调用的upnp-igd , NAT-PMP, PCP手搓的

ririyeye avatar Nov 20 '25 13:11 ririyeye