本文发自 http://www.binss.me/blog/use-zerotier-to-connect-remote-synology/,转载请注明出处。

最近过年回家,轻装上阵,只带了一台 Macbook 和 iPads 回去,没想到刚过几天,就对群晖里的大姐姐们甚是想念,于是开始了远程访问群晖的折腾。

我的环境如下:

  • 群晖所处网络:深圳电信 100M ,没有公网 ipv4,有公网 ipv6
  • 当前所处网络:广州联通 100M,没有公网 ipv4 和 ipv6

由于群晖所处网络没有公网 ipv4 、而当前所处网络又不支持 ipv6 ,断绝了我直接通过 ip 进行连接的想法。无奈之下,只能尝试一些内网穿透的方案。

QuickConnect

群晖的 QuickConnect 服务是我抛弃黑群晖,转向白群晖的的根本原因。

使用很简单,只需在群晖的 控制面板 - QuickConnect 中启用,设置 QuickConnect id 后,即可随时随地浏览器打开 https://quickconnect.to 登录群晖的 DSM 。

实现上,QuickConnect 并未具备太强的内网穿透能力,在我这种情况下基本退化到通过群晖中转服务器进行中转的模式,实测下载速度大概在 500 KB/s 左右。

优点:使用简单、稳定

缺点:

  1. 数据经过群晖服务器中转,有隐私泄漏风险
  2. 只支持群晖官方支持的服务,如 DSM、Moments、Drives ,而无法访问其他自搭建的服务
  3. 速度受到群晖中转服务器的限制

frp

https://github.com/fatedier/frp

曾经折腾过的一个方案,实现上是在一台具有公网 ip 的机器上部署 frps ,在内网机器上部署 frpc ,其他机器通过访问公网 ip 机器相应的端口即可访问到内网机器上的服务:

实现上,内网机器主动连接公网 ip 机器并保持长连接,并根据配置为内网机器相应服务监听相应的端口。当公网 ip 机器的 frps 收到相应端口的请求时,会通过长连接转发给内网的机器。因此 frp 方案实际上也是流量转发来实现内网穿透的。

这套方案我之前用了几个月,最大的缺点在于中转服务器的成本。国内最菜的上行 1M 带宽 VPS ,官方价也要 80 块左右一个月,而 1M 带宽的下载速度只有约 128 KB/s 。偶尔会出现抽风导致连不上,还不如使用群晖的 QuickConnect 。

ZeroTier

这是我几经尝试,最后选择的方案。

首先登录官网 https://www.zerotier.com ,注册账号,点击 Create a Network 创建一个网络,会给你分配一个 Network ID ,进入管理页面:

Access control 选择 private ,这样任何申请加入该子网的设备需要经过你的允许才能成功加入。

随后划分一个子网段,我这里划分的子网为 172.23.0.0/16 :

随后就可以静候设备加入了。

群晖

首先 https://www.synology.com/zh-tw/knowledgebase/DSM/tutorial/Compatibility_Peripherals/What_kind_of_CPU_does_my_NAS_have 查询 CPU 对应的架构,然后从官网 - Download - Synology NAS 下载相应的 spk 包。我的群晖是 DS918+ ,因此下载的是 zerotier_apollolake-6.1_1.4.0-0.spk 。

但当我打开 ZeroTier 应用,输入 network id 点击 Join 后发现没反应,只能尝试通过命令来加入网络。然而此时我位于外网,唯一能访问群晖的就只有 QuickConnect ,怎么办呢?

经过一番思考,采用以下方法来曲线救国:

  1. 群晖的 docker 安装 ubuntu 容器并启动
  2. 双击容器,进入终端机页面,安装 ssh 工具 apt update && apt install ssh
  3. 使用群晖的局域网 ip 进行 ssh 登录

这样我们就登录到群晖上,可以通过命令来加入网络了:

sudo zerotier-cli join <network-id>

然后还要到官网的管理页面,会发现在 Members 区域新出现了一台设备,将最前面的勾勾上表示允许其加入网络:

如果已经按照上文说的设置好了子网,那么勾上后会为新设备分配子网段内的一个 ip 。我们可以在设备通过 sudo zerotier-cli info 查看其是否已经成功加入子网:

200 info xxxxxxxxc1 1.4.0 ONLINE

Mac

直接通过 brew 进行安装,安装期间需要 root 权限:

brew cask install zerotier-one

运行发现 GUI 还是无法加入 (Join 按钮不亮),无奈之下还是通过命令来加入网络,后续流程同上述的群晖。

IOS

登录 非国区 商店,App store 搜索 zerotier 下载即可。

运行 App ,点右上角的加号添加子网,输入 network id 即可加入,会要求在设置允许其 VPN 配置,允许即可。

添加成功后,会在主界面多出子网相应的选项,点击开关开始连接:

在经过若干秒后,状态栏显示 VPN 标志,表示连接成功。

后续

被加入到子网的设备此时位于同一个子网内,可通过命令查看子网下的在线设备:

$ zerotier-cli peers
200 peers
<ztaddr>   <ver>  <role> <lat> <link> <lastTX> <lastRX> <path>
xxxxxxc2c1 1.4.0  LEAF      26 DIRECT 1408     1408     1.1.1.1/7565
xxxxxxe174 -      PLANET    85 DIRECT 16739    3246     1.1.1.1/9993
xxxxxxbf30 -      PLANET   388 DIRECT 14180    8389     1.1.1.1/9993
xxxxxx387a 1.4.1  LEAF     224 DIRECT 16739    16471    2.2.2.2/62191
xxxxxx7190 -      PLANET   251 DIRECT 14180    13921    1.1.1.1/9993
xxxxxx1db7 -      PLANET   263 DIRECT 14180    3067     1.1.1.1/9993

设备之间可通过 ip 相互访问,比如我的 NAS 分配的 IP 为 172.23.0.1 ,可通过 ping 测试下是否可达:

$ ping 172.23.0.1
PING 172.23.0.1 (172.23.0.1): 56 data bytes
64 bytes from 172.23.0.1: icmp_seq=0 ttl=64 time=47.454 ms
64 bytes from 172.23.0.1: icmp_seq=1 ttl=64 time=17.453 ms
64 bytes from 172.23.0.1: icmp_seq=2 ttl=64 time=43.546 ms
64 bytes from 172.23.0.1: icmp_seq=3 ttl=64 time=28.613 ms
64 bytes from 172.23.0.1: icmp_seq=4 ttl=64 time=28.880 ms
64 bytes from 172.23.0.1: icmp_seq=5 ttl=64 time=18.904 ms
64 bytes from 172.23.0.1: icmp_seq=6 ttl=64 time=19.538 ms
64 bytes from 172.23.0.1: icmp_seq=7 ttl=64 time=26.655 ms
64 bytes from 172.23.0.1: icmp_seq=8 ttl=64 time=20.685 ms
64 bytes from 172.23.0.1: icmp_seq=9 ttl=64 time=22.113 ms
^C
--- 172.23.0.1 ping statistics ---
10 packets transmitted, 10 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 17.453/27.384/47.454/9.864 ms

延迟很低,看来是打洞成功,两台机器直连了。确认其是否直连可通过以方式:

  1. zerotier-cli info -j 查看 tcpFallbackActive 是否为 true ,如是,则说明当前走的是最慢的 relay 模式,必然不是直连
  2. zerotier-cli peers 查看相应 LEAF 的 path 栏是否有 ip 地址,如果没有,表示是 relay,也不是直连
  3. 实测。通过 ping 查看延时,传输大文件查看速率,如果延时较低、传输速度接近上下行带宽,基本可以确定就是直连了

在 zerotier 的帮助下,我们当前机器和 NAS 位于同一个子网内,于是 Finder - Network 就探测到了 NAS ,输入账号密码登录,随便下载一个电影下来测速:

大概能维持在 3MB/s 左右,基本满足需求。

同时由于位于同一个内网,infuse 能够直接连上 NAS 的 Plex ,美滋滋:

分析

根据官方文档,ZeroTier 是一套 SDN (Software-Defined Network) 方案,能够在不同网络的机器之间建立起子网。

设计上分为两层:VL1 负责实现底层的 p2p 传输,建立虚拟的 “网线” ,对应 OSI 中的链路层。 VL2 负责模拟 Ethernet ,对应 OSI 中的网络层。

VL1 有类似于 DNS 的拓扑,全球共有 12 台根服务器,由 ZeroTier 公司维护,称为 earth 。同时用户也可以部署公网节点加入到集群中、专为自身服务,从而避免对 earth 的依赖,称为 moon 。earth 和 moon 统称为 planet 。而普通的 client 节点称为 leaf 。在整个拓扑中,每个节点都会分配一个长度为 40-bit 的地址,但区别于 ip,其不包含任何含义。

当节点 A 想要联系 B 时,如果发现无法直接联系上 B,就会创建一个包发送给上层节点 R 。如果 R 发现能够联系上 B,则会将该包转发给 B,否则会继续将包继续往上层传递。一旦中间人 R 发现能够联系上 B,会给 A 发送一个 rendezvous 包来告知 A 如何连接上 B ,同时 R 会也会发送 rendezvous 包告知 B 如何连接上 A 。如此一来,A 和 B 握上了手,尝试通过打洞来建立直接连接。如果 A 和 B 之间无法建立连接,则会通过中继。

节点之间传输的包都通过密钥加密,链路、中转节点无法获得其明文内容。

VL2 实现了 VLAN 、多播、流量控制、权限控制等功能。在创建一个子网(具有唯一 network ID)后,每当新节点加入到该子网中时,都会向该网络的 controller 发送查询信息以获得该网络的配置信息。随后会在节点上建立虚拟 Ethernet 设备 (位于 OSI layer 2) 来实现功能。

Linux 实现

对于 Linux ,ZeroTier 创建了一块网卡,并赋予其相应的 ip :

eth50     Link encap:Ethernet  HWaddr AE:7E:0E:5D:FA:A4
          inet addr:172.23.0.1  Bcast:172.23.255.255  Mask:255.255.0.0
          inet6 addr: fc10:1a29:9632:a127:c2c1::1/40 Scope:Global
          inet6 addr: fe80::ac7e:eff:fe5d:faa4/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:545294 errors:0 dropped:0 overruns:0 frame:0
          TX packets:1624304 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:47563661 (45.3 MiB)  TX bytes:2045555333 (1.9 GiB)

其既拥有 ip 地址,又拥有 mac 地址,说明是一个 layer 2 设备,一般来说是 tap 设备,通过 ethtool 可以验证:

binss@binss-NAS:/proc/5401$ ethtool -i eth50
driver: tun
version: 1.6
firmware-version:
bus-info: tap
supports-statistics: no
supports-test: no
supports-eeprom-access: no
supports-register-dump: no
supports-priv-flags: no

同时我们发现,zerotier-one 进程的 pid 为 5401 ,让我们查看其打开的 fd :

sh-4.3# ls -al /proc/5401/fd
lrwx------ 1 root root 64 Jan 29 12:01 28 -> /dev/net/tun

sh-4.3# cat /proc/5401/fdinfo/28
pos:    102
flags:  02100002
mnt_id: 15
iff:    eth50

因此在 Linux 下 ZeroTier 的数据流为:

             socket API               fd
eth0 (物理网卡) <-----> zerotier-one <-----> tap (eth50) <----> other processes

zerotier-one 进程在通过 socket 收到远端发来的数据包后,将其进行解密,随后通过打开的 tap 设备 (eth50) 注入到网络协议栈中,于是其他进程就能够接收到发送给 172.23.0.1 的数据包。同理,其他进程发往 172.23.0.1/16 的数据包会被网络协议栈捕获并通过 eth50 转发给 zerotier-one 进程,由其进行加密后,根据目标在 172.23.0.1/16 网段中的 ip 找到相应的长连接 (socket),通过物理网卡 eth0 发送出去。

Mac 实现

对于 Mac 来说,利用了 10.13 引入的 feth :

feth7783: flags=8943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST> mtu 16370
    ether 66:65:74:68:1e:67
    peer: feth2783
    media: autoselect
    status: active
feth2783: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 5000 mtu 2800
    ether ae:60:1c:a9:2b:b9
    inet 172.23.0.179 netmask 0xffff0000 broadcast 172.23.255.255
    inet6 fe80::ac60:1cff:fea9:2bb9%feth2783 prefixlen 64 scopeid 0xa
    inet6 fc10:1a29:962c:b3d3:13dc::1 prefixlen 40
    peer: feth7783
    nd6 options=201<PERFORMNUD,DAD>
    media: autoselect
    status: active

根据官方文档:

Starting in 10.13 Darwin contains something called a “fake Ethernet” device. They start with “feth,” can be created with the “ifconfig” command, and appear to behave very much like “veth pairs” on Linux. Create two “feth” interfaces, peer them, and now packets injected in one come out of the other.

The method we found is pure black magic. A BPF (Berkeley Packet Filter) socket seems to be needed to receive packets from the pair, while an AF_NDRV socket (yet another bit of almost undocumented internal magic) seems to be the best way to inject them. Injection can also be done via BPF but the AF_NDRV method seemed to yield superior performance.

可知 Mac 下 ZeroTier 创建了一对 feth ,数据流为:

             socket API           BPF socket
en0 (物理网卡) <-----> zerotier-one <-----> feth(feth7783) <---> feth(feth2783) <----->  other processes

zerotier-one 进程在通过 socket 收到远端发来的数据包后,将其进行解密,随后通过 BPF socket 将数据写入的 feth 设备的一端 feth7783 中,根据 veth 设备的特性,数据会从另一端 feth2783 中出来,并注入到网络协议栈中,于是其他进程就能够接收到发送给 172.23.0.179 的数据包。同理,其他进程发往 172.23.0.1/16 的数据包会被网络协议栈捕获并通过 feth 转发给 zerotier-one 进程,由其进行加密后,根据目标在 172.23.0.1/16 网段中的 ip 找到相应的长连接 (socket),通过物理网卡 en0 发送出去。

为什么不直接用 tap 呢?文档中也提到了:

People might ask why we don’t use the NetworkExtension framework or the “utun” device type. It’s because NetworkExtension and “utun” only support layer 3 “tun” type interfaces, not layer 2 “tap” interfaces.

IOS 实现

由于 IOS 的封闭性,没法用工具进行 debug ,只能从使用过程中观察到的一些现象进行推测。

在 zerotier-one app 中打开相应子网的开关后,IOS 设备显示连接到 VPN 。根据我对 IOS 代理机制的了解,此时 app 能够通过 tun 设备接管流量。但 tun 只有 layer 3 设备,而我们需要 layer 2 的设备,怎么办?这点 zerotier 的文档中也提到:

We do have code to glue a layer 3 tunnel to a layer 2 virtual network by implementing our own IPv4 ARP and IPv6 NDP. This is how we work on phones (iOS and Android). It’s not ideal though. Many desktop users want real “tap” devices for various reasons including bridging to VMs, doing real multicast, and running exotic protocols.

说明在 IOS 上使用了自制 "layer 3 tunnel to a layer 2 virtual network" 技术,能够利用 tun 来实现 layer 2 的功能。

总结

早在一年多前,我就曾经尝试过使用 ZeroTier 来做内网穿透,还在 VPS 上部署了 moon 来加速,但最后实测下来发现性能不佳,因此放弃。但不曾想过士别三日,另当刮目相看。

总体来说,ZeroTier 配置简单,使用方便,性能优秀,从官方提供的一些手册和文档来看,ZeroTier Inc. 这个团队很 nb 。但经过两天的使用,还是发现有一些缺点:

  1. 在某些时间段会出现速度下降、ping 值飙高、丢包的情况,推测可能是 ISP 做了 UDP QoS
  2. 需要安装专门的 app ,虽然在 Mac 上不会和原有代理软件(Surge)产生冲突,但在 IOS 上会顶掉代理软件的 VPN
  3. GUI 上有蜜汁问题,我在群晖和 Mac 上都没能通过 GUI 加入到自己的子网,需要手输命令

但总体来说,瑕不掩瑜,在此推荐给大家。

参考

https://www.zerotier.com/manual/

https://www.zerotier.com/how-zerotier-eliminated-kernel-extensions-on-macos/