V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
Kinnikuman
V2EX  ›  程序员

关于 tailscale NAT 打洞问题的理解

  •  
  •   Kinnikuman · 3 天前 · 1654 次点击

    我的问题是由 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 连接成功。

    回到 The benefits of birthdays

    假设 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 地址。

    tailscale ping

    第 1 条附言  ·  3 天前
    对于 derp 服务器来讲,它可能不知道 A 和 B 处于哪种 NAT 下面,但只要 A 想连接 B ,或者 B 想连接 A ,先经过 derp ,然后 derp 服务器将节点的 ip:port 通知对方,对方进行**两次**访问尝试,但凡有一方是 Fullcone NAT ,那就能保证 p2p 连接成功。

    但 tailscale 那篇文章中,需要让 Hard NAT 一方发出 256 个 socket ,然后另一方进行尝试扫描碰撞。这完全没必要呀?

    我不知道是我理解的有问题,还是什么原因。我的这个方式可行吗?
    20 条回复    2024-12-19 17:53:25 +08:00
    swananan
        1
    swananan  
       3 天前
    你是在问为什么 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 。
    swananan
        2
    swananan  
       3 天前
    另外关于存在成功率的问题,我觉得原因可能是很多 NAT 实现本质上是有状态的,会随着负载的情况,NAT 行为甚至有可能改变(取决于 NAT 实现)。另外,在绝大部分场景,对于开发人员来说,它还是一个完全黑盒。所以,有成功率波动我觉得也挺正常的。
    lovemaostar
        3
    lovemaostar  
       3 天前
    你可以尝试映射任意一边的 udp 41641
    wheat0r
        4
    wheat0r  
       3 天前
    最后的实践令我疑惑。原则上来说两个节点都有 ipv6 ,就不用考虑 NAT 的问题,只需要解决防火墙的问题就可以。但是 DERP 本身可能需要 ipv6 地址,并且 ipv6 上有正确的监听。
    FaiChou
        5
    FaiChou  
       3 天前 via iPhone
    @wheat0r v6 确实没 nat ,但有时候 ts 就是不能用 v6 直连,即使 v6 可用也不行。现实网络环境太复杂了。
    Ploter
        6
    Ploter  
       3 天前
    确实有遇到明明双端都有 IPv6 却无法直连的情况,不过一端重新连接 WIFI 就解决了,我以为是校园网的问题
    mcluyu
        7
    mcluyu  
       3 天前
    家里是移动宽带有 v6 ,v6 只开放给了路由器没有继续向下分配,ts 装在路由器上,v4 是大内网, 公司只有 v4 公网但是经过 NAT 到我的设备。
    实际就是: 偶尔能打洞成功, 且每次升级后成功率会很低。 全靠 upnp
    但是只要我使用手机热点,两边都有移动 V6 时, 几乎百分百成功,延迟速度都很好。
    badgv
        8
    badgv  
       3 天前 via Android
    @wheat0r derp 不需要有 v6 ,如果节点都有 v6 ,直接就会走 v6 直连,每个节点会提交自己这边的接口 ip 信息
    Judoon
        9
    Judoon  
       3 天前
    基于近期观察,好像从某个版本之后,长时间没有流量或者请求就会断掉连接。
    具体现象是 tailscale status 看到的状态是 relay 或者直接没有,而 ping 一下之后(不管是 tailscale ping 还是直接 ping ),看状态就是 direct 了


    题主的问题应该是理论上 A 直接去连 B 就能打通,为什么需要 A 开很多端口让 B 来尝试吧?
    Actrace
        10
    Actrace  
       3 天前
    tailscale 现在是靠定时提交接口 IP 信息来维持联系,如果提交不上去,或者这个过程出现了什么问题,那么可能会导致一些问题。
    wheat0r
        11
    wheat0r  
       3 天前
    @badgv 如果节点前面有 ingress 方向的防火墙,是不是需要 derp 来打洞?
    Kinnikuman
        12
    Kinnikuman  
    OP
       3 天前
    @swananan 我在附言中 append 了一条我的疑惑,原来写的可能表达不是很清楚。请再帮看下。
    Chalice
        13
    Chalice  
       3 天前
    easy nat 不等于 Nat2 ( fullcone nat ),easy nat 本质上是 Endpoint-Independent NAT mapping ,这里面包含了 Nat1 - Nat3 ( Port-Restricted Cone NAT ),而 Nat 3 还会校验 ip:port 的一致性( Nat2 只校验 IP )。
    swananan
        14
    swananan  
       2 天前
    机器 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 的概率,是一个比较可行的方案。
    Kinnikuman
        15
    Kinnikuman  
    OP
       2 天前
    @swananan 我做了个图比较形象,感觉你说的例子有一点问题。我一会写一个 example 来测试下。

    swananan
        16
    swananan  
       2 天前
    @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.
    Kinnikuman
        17
    Kinnikuman  
    OP
       2 天前
    @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



    我的问题应该没有了。
    Kinnikuman
        18
    Kinnikuman  
    OP
       2 天前
    @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'
    Chalice
        19
    Chalice  
       2 天前
    @Kinnikuman 你这样配置就等于已经手动做了端口映射了。。。哪里还需要打洞穿透呢。
    Kinnikuman
        20
    Kinnikuman  
    OP
       2 天前
    @Chalice 只是证明一下是防火墙 NAT 的规则导致它不能完全表现为 Endpoint-Independent Filtering ,是一个假的 Fullcone NAT ,经过测试,我的 NAT 是 Port-Restricted Cone NAT 。

    电脑上连接着服务器 A, 然后服务器 A 再用不同端口访问电脑的 PublicIP:Port 也是被 denied. 所以是 Port-Restricted Cone NAT 。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2007 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 19ms · UTC 16:14 · PVG 00:14 · LAX 08:14 · JFK 11:14
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.