我的问题是由 tailscale nat traversal 这篇文章引起的,文章比较长。
问题是在 The benefits of birthdays 这一章节:
Rather than open 1 port on the hard side and have the easy side try 65,535 possibilities, let’s open, say, 256 ports on the hard side (by having 256 sockets sending to the easy side’s ip:port), and have the easy side probe target ports at random.
两台机器在不同的地方使用 tailscale 组网,tailscale 是基于 WireGuard 的。
机器 A 的内网 ip 192.168.1.2, 公网 ip 2.2.2.2
机器 B 的内网 ip 192.168.2.2, 公网 ip 3.3.3.3
假设 A 是 Hard NAT(Symmetric NAT ), 而 B 是 Easy NAT ( Fullcone NAT )。
对于 Hard NAT 和 Fullcone NAT 的一点解释:
A 的某个程序使用 192.168.1.2:1234 向 4.4.4.4:5678 发出一个请求, 那么在 NAT 映射后,是 2.2.2.2:4321 发往 4.4.4.4:5678 ,这样 4.4.4.4:5678 返回的请求,到达 NAT 也会被正常映射到设备 A 的 1234 端口。这是由 iptables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT
这种防火墙规则决定的。
但是对于 Hard NAT 的 A 来讲,如果是 5.5.5.5:5678 向 A 的 2.2.2.2:4321 发起一条请求,NAT 会检查源地址不是 4.4.4.4:5678 ,所以会 drop 掉这一条请求。
但对于 B 来讲,192.168.2.2:1234->3.3.3.3:4321 -> 4.4.4.4:5678, 建立了一个这样的 NAT 表,当其他的设备比如 5.5.5.5:9876 主动请求 3.3.3.3:4321 端口的时候,也会被 NAT 转发到 192.168.2.2:1234 ,这就是 Fullcone NAT ,表现为很容易 p2p 连接成功。
假设 B 机器想要连接 A 机器,由于 A 是 Hard NAT ,无法直接选择一个合适的端口进行连接,但反过来是可以的,因为每一台运行 tailscale 程序的设备来讲,都会和 DERP 服务器进行着连接。
不管是谁连接谁,都先走一下 DERP 中继,这样起码就能获取到每一方的外部端口。理想情况下,两端都是 Easy NAT ,那它们使用连接 DERP 服务器的端口进行直连,基本上就可以 p2p 连接成功。
但 A 是 Hard NAT ,于是(下面是我的猜测) DERP 服务器将 B 的 ip 和端口告诉了 A ,虽然是 B 想主动连接 A ,但 tailscale 程序的实现是两端都进行尝试连接,这样 A 直连成功了 B ,192.168.1.2:1234->2.2.2.2:5678->3.3.3.3:8765->192.168.2.2:4321 ,这样它俩就建立了正常的直连。
文章中的举例是需要让 A 开 256 个端口连接 B ,B 通过遍历端口进行和 A 连接,当尝试了 1024 次时候,能和 A 成功连接的概率是 98% (生日悖论)。
我的疑问是,为什么需要 A 开很多端口进行尝试探测?我的猜测那种方案有什么不能实现的地方吗?
我尝试过很多次,对于复杂的网络环境,tailscale 有时候能打洞成功,有时候会失败。而且两端都是 Fullcone NAT, 并且双方都有 ipv6 地址。
1
swananan 9 天前
你是在问为什么 tailscale 可以打通 Hard NAT 吗?
Hard NAT 蛋疼的地方在于,它是 Address and Port-Dependent Mapping 和 Address and Port-Dependent Filtering ,Address and Port-Dependent Mapping 意味着,A 和 DERP 中继构建的 mapping ,不会复用给 A 和 B 之间。导致 B 想和 A 打通,只能猜测 A 和 B mapping 中,新映射的 port 是多少。 所以 如果 A 能够开很多端口去尝试(即使用很多 socket ,确保有很多 local ip 和 local port 组合),B 就很有希望能连上 A 。 |
2
swananan 9 天前
另外关于存在成功率的问题,我觉得原因可能是很多 NAT 实现本质上是有状态的,会随着负载的情况,NAT 行为甚至有可能改变(取决于 NAT 实现)。另外,在绝大部分场景,对于开发人员来说,它还是一个完全黑盒。所以,有成功率波动我觉得也挺正常的。
|
3
lovemaostar 9 天前
你可以尝试映射任意一边的 udp 41641
|
4
wheat0r 9 天前
最后的实践令我疑惑。原则上来说两个节点都有 ipv6 ,就不用考虑 NAT 的问题,只需要解决防火墙的问题就可以。但是 DERP 本身可能需要 ipv6 地址,并且 ipv6 上有正确的监听。
|
6
Ploter 9 天前
确实有遇到明明双端都有 IPv6 却无法直连的情况,不过一端重新连接 WIFI 就解决了,我以为是校园网的问题
|
7
mcluyu 9 天前
家里是移动宽带有 v6 ,v6 只开放给了路由器没有继续向下分配,ts 装在路由器上,v4 是大内网, 公司只有 v4 公网但是经过 NAT 到我的设备。
实际就是: 偶尔能打洞成功, 且每次升级后成功率会很低。 全靠 upnp 但是只要我使用手机热点,两边都有移动 V6 时, 几乎百分百成功,延迟速度都很好。 |
9
Judoon 9 天前
基于近期观察,好像从某个版本之后,长时间没有流量或者请求就会断掉连接。
具体现象是 tailscale status 看到的状态是 relay 或者直接没有,而 ping 一下之后(不管是 tailscale ping 还是直接 ping ),看状态就是 direct 了 题主的问题应该是理论上 A 直接去连 B 就能打通,为什么需要 A 开很多端口让 B 来尝试吧? |
10
Actrace 9 天前
tailscale 现在是靠定时提交接口 IP 信息来维持联系,如果提交不上去,或者这个过程出现了什么问题,那么可能会导致一些问题。
|
12
Kinnikuman OP @swananan 我在附言中 append 了一条我的疑惑,原来写的可能表达不是很清楚。请再帮看下。
|
13
Chalice 8 天前
easy nat 不等于 Nat2 ( fullcone nat ),easy nat 本质上是 Endpoint-Independent NAT mapping ,这里面包含了 Nat1 - Nat3 ( Port-Restricted Cone NAT ),而 Nat 3 还会校验 ip:port 的一致性( Nat2 只校验 IP )。
|
14
swananan 8 天前
机器 A 的内网 ip 192.168.1.2, 公网 ip 2.2.2.2
机器 B 的内网 ip 192.168.2.2, 公网 ip 3.3.3.3 假设 A 是 Hard NAT(Symmetric NAT ), 而 B 是 Easy NAT ( Fullcone NAT ) DERP 中继,我理解是类似 stun server ,公网 ip 4.4.4.4:5678 。 A 第一次和 DERP 发起 ip 和 port 探测,A 的 local socket port 假设是 1234 ,那么 Hard NAT 会生成 : origin tuple: 192.168.1.2:1234-4.4.4.4:5678 以及 reply tuple: 4.4.4.4:5678-2.2.2.2:public_port1 A 再次使用相同 socket 和 B 发起请求(这个时候通过信道,A 已经拿到了 B 的 public ip 和 public port ),那么 Hard NAT 会生成: origin tuple: 192.168.1.2:1234-3.3.3.3: B-public-port 以及 reply tuple: 3.3.3.3: B public-port-2.2.2.2:public_port2 对于 Hard NAT 而言,因为它是 Address and Port-Dependent Mapping ,这意味着 reply tuple 中生成的 public-port 极大概率是不一样的,不会因为 origin tuple 中的 sip sport 一致,就能保证 public-port 一致。而 easy NAT 是可以保证 public-port 一致的。 这意味着 B 想和 A 搭上话,必须要疯狂猜测 A 的 public port2 是多少。因为 Hard NAT 有 Address and Port-Dependent Filtering ,所以必须要求 A 给 B 发过数据的四元组才行,也就是 public port2. 以上是为什么一方有了 Hard NAT 之后,NAT 打洞困难重重的原因。 我理解所有提升这种场景打洞成功率的方案,都是为了让 B 能够快速的猜到 A 的 public port2 ,所以 A 发出多个基于不同 socket 的 UDP 报文,创建多个 public port2 ,然后让 B 提升猜测 public port2 的概率,是一个比较可行的方案。 |
15
Kinnikuman OP |
16
swananan 8 天前
@Kinnikuman 你画的图有一处不对,就是对于 A 再次尝试向 B 发送 UDP 报文的时候,B 是没有办法感知到 A 的新的 2.2.2.2:public_port2 的详细值的。因为 Easy NAT 一般也会具有 Address and Port-Dependent Filtering ,所以 A 向 B 发送的报文会被 Easy NAT 丢弃。所以,B 只能傻傻的去猜,B 没办法轻松获取到 4321 这个 public port2.
|
17
Kinnikuman OP @swananan 我做了一下试验,我的电脑处于 Fullcone NAT 后面 (STUN client version 0.97 Primary: Independent Mapping, Independent Filter, preserves ports, no hairpin),运行了下面代码中的脚本,然后用两个服务器做测试,服务器 A 监听 8081 端口,然后电脑执行这个这个程序向服务器 A 发送一条请求,拿到了电脑的 IP 和端口: Received from: Your IP: 123.xx.xx.138, Your Port: 57851 ,然后用服务器 C 向电脑的公共 ip 123.xx.xx.138:57851 尝试连接,但是 connection refused 了。这也许能证明,我的 stun 检测的结果 (Independent Mapping, Independent Filter)是错误的。
试验代码: https://gist.github.com/FaiChou/7291580826d83d9340f9fe6c8cd8a79b 我的问题应该没有了。 |
18
Kinnikuman OP @Kinnikuman 补充一下这个测试。
~ ./socket_program 8081 45.xx.xx.13 8081 Server listening on port 8081 Received from server B: Your IP: 123.xx.xx.138, Your Port: 61288 Received connection from 45.xx.xx.13:51662 Received connection from 103.xx.xx.120:39458 只有当我在 OpenWrt 防火墙指定端口转发才可以正常收到任意 ip 发到 61288 端口的请求 uci add firewall redirect # =cfg133837 uci set firewall.@redirect[-1].dest='lan' uci set firewall.@redirect[-1].target='DNAT' uci set firewall.@redirect[-1].name='test' uci set firewall.@redirect[-1].src='wan' uci set firewall.@redirect[-1].src_dport='61288' uci set firewall.@redirect[-1].dest_ip='192.168.11.112' uci set firewall.@redirect[-1].dest_port='8081' |
19
Chalice 8 天前
@Kinnikuman 你这样配置就等于已经手动做了端口映射了。。。哪里还需要打洞穿透呢。
|
20
Kinnikuman OP @Chalice 只是证明一下是防火墙 NAT 的规则导致它不能完全表现为 Endpoint-Independent Filtering ,是一个假的 Fullcone NAT ,经过测试,我的 NAT 是 Port-Restricted Cone NAT 。
电脑上连接着服务器 A, 然后服务器 A 再用不同端口访问电脑的 PublicIP:Port 也是被 denied. 所以是 Port-Restricted Cone NAT 。 |