使用 Frpc 进行内网穿透构建 ZeroTier Moon 记录

一、简介

Zerotier 是一个专用于异地组网的工具,它方便将多台异地机器以 P2P 或者 中转 Relay 的方式实现宛如局域网般的流畅体验。

Zerotier 组网中节点分为三个部分,分别是位于国外的中央服务器 Planet,用户自建节点 Moon,以及用户其他节点 Leaf。

由于 Planet 位于国外,当两台机器地理位置相隔甚远时,无论是 UDP 打洞还是 Relay 中继,速度都非常慢,因此尝试自建一台国内Zerotier Moon 来提高打洞概率 + 中继速度。

网上搭建 Zerotier Moon 的教程都需要购买一台服务器,但本人不想这么折腾,因此尝试探索 FRPC 内网穿透的搭建方式。

二、Zerotier 打洞/中继

在做内网穿透/搭建 Moon 之前,我们得先理解 Zerotier 的打洞和中继原理。

本节参考:ZeroTierOne/service/OneService.cpp - github,以及自己花费大量时间调试 + wireshark 抓包的痛苦经验。

1. 监听状态

Zerotier 会在本地同时使用 3 个端口,其中每个端口都会分别监听 TCP 和 UDP 连接。以下是 Zerotier 在我本机上的监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜  zerotier-one sudo lsof -i -P -n | grep zerotier
zerotier- 2091716 zerotier-one 6u IPv4 868501026 0t0 TCP 127.0.0.1:9993 (LISTEN)
zerotier- 2091716 zerotier-one 7u IPv6 868501027 0t0 TCP [::1]:9993 (LISTEN)

zerotier- 2091716 zerotier-one 16u IPv4 868501047 0t0 UDP 192.168.51.236:9993
zerotier- 2091716 zerotier-one 17u IPv4 868501048 0t0 TCP 192.168.51.236:9993 (LISTEN)

zerotier- 2091716 zerotier-one 14u IPv4 868501045 0t0 UDP 192.168.51.236:30978
zerotier- 2091716 zerotier-one 15u IPv4 868501046 0t0 TCP 192.168.51.236:30978 (LISTEN)

zerotier- 2091716 zerotier-one 18u IPv4 868501049 0t0 UDP 192.168.51.236:42276
zerotier- 2091716 zerotier-one 19u IPv4 868501050 0t0 TCP 192.168.51.236:42276 (LISTEN)

# ... 略去剩余 IPv6 监听信息

2. 三个端口

先讲端口,这三个端口分别为首选端口、次选端口和末选端口,这三个端口的定义如注释所描述的那样:

1
2
3
4
5
6
7
8
9
10
11
12
13
// ref: https://github.com/zerotier/ZeroTierOne/blob/adfbbc3/service/OneService.cpp#L802

/*
* To attempt to handle NAT/gateway craziness we use three local UDP ports:
*
* [0] is the normal/default port, usually 9993
* [1] is a port derived from our ZeroTier address
* [2] is a port computed from the normal/default for use with uPnP/NAT-PMP mappings
*
* [2] exists because on some gateways trying to do regular NAT-t interferes
* destructively with uPnP port mapping behavior in very weird buggy ways.
* It's only used if uPnP/NAT-PMP is enabled in this build.
*/

其中首选端口默认固定为 9993(默认端口可被修改,参阅ZeroTier One Network Virtualization Service Documentation)。

我在网上看搭建 Moon 的教程中有看到过设置 9995 端口的,不看源码是真容易搞不清楚哪个端口更重要。

现在明确一点,Zerotier 默认情况下不涉及 9995 端口,只涉及到 9993 端口

当 host 尝试连接 peer 时,这三个端口会同时发送 UDP 数据至 peer。

上下文中 host 指代本机,尽管 p2p 是去中心化的,但是为了便于说明还是要区分本机和远程对等机。

peer 在接收到数据后,对应端口会立即朝着源地址返回一个 UDP 包打洞。倘若 host 接收到 peer 返回的三个 UDP 包的任意一个,则视为可被 DIRECT ACCESS,即 P2P 打洞成功。host 和 peer 会定期发送心跳包维护 p2p 洞,此时数据传递所使用的端口即 host 成功接收到的 peer 包的那个端口

在抓包时,经常看见 host 发三个 udp 给 Peer(注意一共有三个端口,一个端口发一个),而最后只能从 peer 那边接收到一个 UDP 包。

使用 zerotier-cli peers 命令可以查看本机与其他 peer 的连接是 DIRECT(p2p) 还是 relay(中继),只有这两种连接状态。

该命令需要 sudo/管理员权限。

我们同时还可以看到 Zerotier 会监听这三个端口的 TCP 协议数据。这里的 TCP 协议数据与打洞/peers沟通无关,它实际上使用的是 Http 协议,主要用来与本地的 zerotier-cli 进行交互,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  zerotier-one echo "GET /info HTTP/1.1\r\nX-ZT1-Auth: $(sudo cat /var/lib/zerotier-one/authtoken.secret)\r\n\r\n" | nc 127.0.0.1 9993 -v
Connection to 127.0.0.1 9993 port [tcp/*] succeeded!
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json
Content-Length: 91
Connection: close

{
"controller": true,
"apiVersion": 4,
"clock": 1684295560845,
"databaseReady": true
}

这里我只测试成功过 127.0.0.1:9993 的 TCP 连接,其他监听端口/监听地址的组合我都无法用 nc 测试成功过,暂不了解具体原因。

具体其他的 HTTP 请求选项可以参考 Network Virtualization Service API 来理解,这里不再赘述。

3. 中继

当 host 和 peer 没法 p2p 直连时,Zerotier 会尝试使用中继手段,相关逻辑位于 nodeWirePacketSendFunction 函数 中。

中继也分为两种,一种是 TCP 中继,一种是 UDP 中继:

  • UDP 中继。UDP中继是 Zerotier 的主流中继实现方式,它会寻找 Moon/Planet 并要求他们来为待发送的数据包进行 UDP 中继,因此无论是 host 还是 peer,中继发送/接收的数据全部都是 UDP 数据,逻辑比较简单。

  • TCP 中继。 Zerotier 认为 TCP 中继开销太大,因此只在极端恶劣的情况下(例如UDP中继完全失败,即所有UDP数据包全被网关过滤或者超时非常严重等情况,)才会使用 TCP 中继,但事实上这种恶劣情况概率极小,所以可以等同于 Zerotier 基本上不使用 TCP 中继。

    Zerotier 只有在 60s 没有接收到任何数据时才能进行 TCP中继,这个时间相当的长,相信大多情况下应该都不会触发这个条件。

用户可以根据 ZeroTier TCP Proxy Server Documentation 配置 local.conf 来指定是否强制使用 TCP 中继。在启用强制TCP中继后,UDP中继功能将不再启用。虽然 Zerotier 认为 TCP 中继会比 UDP 中继慢,但事实上我用 ping 测试发现 TCP 中继节点比 UDP 中继节点距离我更近一点,延迟更小,因此 Zerotier 的这个说法仁者见仁智者见智,需要理论联系实际。

三、Frpc 内网穿透

1. 做法

Frpc 用来穿透 Moon 服务器的 9993 UDP 端口。

这里本人用的是 NatFrp,这个真的相当良心,免费版每月 5Gb/10Mbps/2tunnel,基本满足绝大多数的需求。

选一个距离 peers 比较近一点的机房,然后选多线机房(个人理解是同时接入多个运营商网络的机房),这样本机在任何运营商网络下都能有比较高的 p2p 打洞成功概率,这是我的隧道配置:

image-20230517132531412

注意这里指定本机 IP 时一定要指定为局域网IP(即 192.168.0.0/16 等),而非回环IP(即127.0.0.1),符合条件的局域网 IP 范围如下图所示:

image-20230517120903864

代码位置位于 InetAddress::ipScope 函数

可能有人看到 172.16.0.0/12 也可以,因此就在 Zerotier 控制面板上给 moon 服务器/被穿透的服务额外增添了一个 172.16 打头的虚拟网 IP,之后把 Frpc 绑定到这样新添加的 172.16 打头IP上,以为也能达到要求。但经过本人实验是不行的,原因是 Zerotier 服务不会监听 Zerotier 自己虚拟网段下的 IP

这里填写的本机IP,一定要是既符合上图网段要求,同时还被 Zerotier 监听 UDP 协议的 IP。

2. 原理

如果兴趣不大则可以跳过本节内容。

这是因为 isAddressValidForPath 函数 只把四种类型的 IP 视为有效地址:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* Check whether this address is valid for a ZeroTier path
*
* This checks the address type and scope against address types and scopes
* that we currently support for ZeroTier communication.
*
* @param a Address to check
* @return True if address is good for ZeroTier path use
*/
static inline bool isAddressValidForPath(const InetAddress &a)
{
if ((a.ss_family == AF_INET)||(a.ss_family == AF_INET6)) {
switch(a.ipScope()) {
/* Note: we don't do link-local at the moment. Unfortunately these
* cause several issues. The first is that they usually require a
* device qualifier, which we don't handle yet and can't portably
* push in PUSH_DIRECT_PATHS. The second is that some OSes assign
* these very ephemerally or otherwise strangely. So we'll use
* private, pseudo-private, shared (e.g. carrier grade NAT), or
* global IP addresses. */
case InetAddress::IP_SCOPE_PRIVATE:
case InetAddress::IP_SCOPE_PSEUDOPRIVATE:
case InetAddress::IP_SCOPE_SHARED:
case InetAddress::IP_SCOPE_GLOBAL:
if (a.ss_family == AF_INET6) {
// TEMPORARY HACK: for now, we are going to blacklist he.net IPv6
// tunnels due to very spotty performance and low MTU issues over
// these IPv6 tunnel links.
const uint8_t *ipd = reinterpret_cast<const uint8_t *>(reinterpret_cast<const struct sockaddr_in6 *>(&a)->sin6_addr.s6_addr);
if ((ipd[0] == 0x20)&&(ipd[1] == 0x01)&&(ipd[2] == 0x04)&&(ipd[3] == 0x70)) {
return false;
}
}
return true;
default:
return false;
}
}
return false;
}

这其中包括了 IP_SCOPE_PRIVATE 局域网地址和 IP_SCOPE_GLOBAL 公网地址,但并不包括 IP_SCOPE_LOOPBACK 回环地址。

Zerotier 在接收到 UDP 数据包后会获取包中的目的 IP,进而判断该数据包是否合法。这个逻辑比较容易理解,只要知道对方朝的是自己哪个 IP 地址发包,就能得知哪个网卡可以 p2p 打洞。

但倘若 FRP 绑定的是本机的 127.0.0.1,那么即便其他 peer 能通过 FRP 发包到 udp://127.0.0.1:9993,Zerotier 也会丢弃接收到的 UDP 数据,造成 p2p 失败。

三、Frpc 测试

在创建好隧道并且也在远程 moon 节点所在机器上也连接好 Frpc 隧道后,接下来需要测试一下 host 和 moon 之间的 UDP 收发能力

这一步非常重要,因为 UDP 协议的特殊性,很多网络都会对 UDP 数据包有着严苛的过滤条件。

例如本人在学校校园网中就无法成功收发 UDP 数据包。

测试步骤很简单:

  1. 修改 moon 机器上 frpc 待转发的端口,从 9993 修改为 9992,之后重新启动 frpc,此时穿透的 UDP 数据应该会发送至本机 9992 端口处。

    这一步可以通过直接修改 frpc.ini 或者在网页面板上修改并重新拉取配置文件来完成。

    9992端口没有什么特殊性,可以随便改成一个自己记得住的端口;这里修改端口是因为 9993 端口已经被 Zerotier 服务占用了,一个端口无法同时被多个 UDP 监听。

  2. 在 moon 机器上启动 UDP-EchoServer 服务,以下是我用来测试的 python 代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # python3 /tmp/udp-echoserver.py 192.168.XX.XX 9992

    import sys
    import socket

    def udp_echo_server(host, port):
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    server_socket.bind((host, port))
    print(f"UDP Echo server started on {host}:{port}")
    while True:
    data, addr = server_socket.recvfrom(1024)
    print(f"Received data from {addr}: {len(data)}")
    server_socket.sendto(b"server: " + data, addr)

    if __name__ == "__main__":
    if len(sys.argv) < 3:
    print("Usage: python udp_echo_server.py <host> <port>")
    sys.exit(1)
    host = sys.argv[1]
    port = int(sys.argv[2])
    udp_echo_server(host, port)
  3. host 上运行 nc -u <ip> <port> 向 Frpc 中转服务器发送 UDP 数据包,查看发送的数据包能否被转发回来。

测试效果如下,图中上面两个窗口是 moon 服务器的 shell,最下方窗口是 host 的 shell。host 使用 nc -u <ip> <port> 并在交互式界面中输入数据并按下 enter 键发送。该 UDP 数据包将被发送至 Frpc 中转服务器并穿透至 moon 的 udp://192.168.x.x:9992,随后 9992 端口上的 echo server 就会把该数据包原样返回。只要 host 能在发送 UDP 数据包后原封不动的接收到 UDP 数据,即可证明双方 UDP 收发功能正常。

image-20230517135624795

这一步可能会有一定概率失败,失败的原因主要有两个(都是本人遇到过的):

  1. Frpc 公网中转服务器所分配的端口号过大,例如分配了 50000+ 的端口号。过大的 UDP 端口号可能会被路由策略过滤,只能重新申请分配新的 UDP 隧道或者更换中转服务器节点,来降低所分配的 UDP 端口号

    本人测试 UDP 端口号 < 30000 基本上没有出现过问题。

  2. 复杂或受限网络可能会限制 UDP 数据包的收发,例如校园网。本人连接校园网后实测无法收发 UDP 数据包,但切换为手机热点就可以通过 UDP 测试。

如果想测试 9993 端口的收信功能则可以使用命令:sudo tshark -i any udp port 9993 and src host 192.168.x.x

UDP测试完成后记得把隧道端口号改回 9993

四、Zerotier Moon 搭建

关于 Zerotier Moon 搭建网上教程是非常多的,基本上都是大同小异。可以参考这个 搭建ZeroTier的Moon服务器小记 - dengzile

  • Moon 服务器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # 0. 切换工作目录
    cd /var/lib/zerotier-one

    # 1. 创建基础 moon 文件
    sudo zerotier-idtool initmoon identity.public > moon.json

    # 2. 此处需要修改 moon.json 中 stableEndpoints 为 Frpc 分配的公网IP和端口
    # (注意该隧道需要映射至 moon 的 9993 端口)

    # 3. 给 moon.json 文件签名,生成 moon 文件
    sudo zerotier-idtool genmoon moon.json

    # 4. 将签名好的 moon 文件移动至 moons.d 文件夹下
    mkdir moons.d
    mv 000000*.moon moons.d

    # 5. 重启 zerotier-one 服务
    sudo service zerotier-one restart

    # 6. 此时可以罗列出当前的 moons 信息
    sudo zerotier-cli listmoons
  • windows (本机),使用管理员权限打开 cmd:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 0. 切换工作目录
    C:\Users\Kiprey>cd C:\ProgramData\ZeroTier\One

    # 1. 创建 moons.d 文件夹并切换
    C:\ProgramData\ZeroTier\One>mkdir moons.d
    C:\ProgramData\ZeroTier\One>cd moons.d

    # 2. 拷贝远程 moon 节点上的 moon 文件,由于此时 moon 还没配置好,因此这种数据下载实际上是通过 UDP 中继完成。
    C:\ProgramData\ZeroTier\One\moons.d>scp kiprey@172.24.0.133:/var/lib/zerotier-one/moons.d/000000xxxxxxxxxx.moon .
    000000xxxxxxxxxx.moon 100% 259 0.5KB/s 00:00

    # 3. 重启服务
    # 键入 win + R 启动 "运行" 窗口 -> services.msc -> 找到 Zerotier-One 服务并重启

这种下发 moon 文件的操作应该是可以通过 zerotier-cli orbit 命令来实现,但本人在实际测试的是否发现 orbit 可能会失败,即没能成功下发 moon 文件,不太清楚是哪里有问题,因此最终还是手动下载了一下。

不过这个问题并不重要,只是随口提起。

重启本机 Zerotier 服务后再运行 zerotier-cli peers,可以发现 Moon 节点以及和 Moon 相近的节点全部从 RELAY 中继变成了 DIRECT 直连:

  • 配置 moon 前:

    image-20230517142839955

    sshping 延迟平均高达 300ms,操作 ssh 一卡一卡的。

  • 配置 moon 后:

    image-20230517142520240

    sshping 的延迟降低到了 100ms 左右,ssh 操作明显的流畅起来了。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2024 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~