前言

自从进入大学以来,作为一个工科学生,笔记本电脑一向是“告别 mac,平板本性能太拉”。平时背个游戏本怎么着也得是负重 5kg,所以日常的搭配都是 iPad mini + 键盘鼠标远程连到放在宿舍的游戏本。Tailscale 组网在校园网比较正常的时候延迟可以稳定在 3ms 左右,但是一旦出现网络波动或者在外访问,Tailscale 自带的 DERP Server 延迟又太高,于是就有了自建的想法。

Tailscale 的组网原理

Tailscale 的终极目标是让两台处于网络上的任何位置的机器建立点对点连接(直连),但现实世界是复杂的,大部份情况下机器都位于 NAT 和防火墙后面,这时候就需要通过打洞来实现直连,也就是 NAT 穿透。

NAT 按照 NAT 映射行为有状态防火墙行为可以分为多种类型,但对于 NAT 穿透来说根本不需要关心这么多类型,只需要看 NAT 或者有状态防火墙是否会严格检查目标 Endpoint,根据这个因素,可以将 NAT 分为 Easy NAT 和 Hard NAT

  • Easy NAT 及其变种称为 “Endpoint-Independent Mapping” (EIM,终点无关的映射)
    这里的 Endpoint 指的是目标 Endpoint,也就是说,有状态防火墙只要看到有客户端自己发起的出向包,就会允许相应的入向包进入,不管这个入向包是谁发进来的都可以
  • hard NAT 以及变种称为 “Endpoint-Dependent Mapping”(EDM,终点相关的映射
    这种 NAT 会针对每个目标 Endpoint 来生成一条相应的映射关系。 在这样的设备上,如果客户端向某个目标 Endpoint 发起了出向包,假设客户端的公网 IP 是 2.2.2.2,那么有状态防火墙就会打开一个端口,假设是 4242。那么只有来自该目标 Endpoint 的入向包才允许通过 2.2.2.2:4242,其他客户端一律不允许。这种 NAT 更加严格,所以叫 Hard NAT。

对于 Easy NAT,我们只需要提供一个第三方的服务,它能够告诉客户端“它看到的客户端的公网 ip:port 是什么”,然后将这个信息以某种方式告诉通信对端(peer),后者就知道该和哪个地址建连了!这种服务就叫 STUN (Session Traversal Utilities for NAT,NAT会话穿越应用程序)。它的工作流程如下图所示:

  • 笔记本向 STUN 服务器发送一个请求:“从你的角度看,我的地址什么?”
  • STUN 服务器返回一个响应:“我看到你的 UDP 包是从这个地址来的:ip:port”。

中继是什么

对于 Hard NAT 来说,STUN 就不好使了,即使 STUN 拿到了客户端的公网 ip:port 告诉通信对端也于事无补,因为防火墙是和 STUN 通信才打开的缺口,这个缺口只允许 STUN 的入向包进入,其他通信对端知道了这个缺口也进不来。通常企业级 NAT 都属于 Hard NAT。

这种情况下打洞是不可能了,但也不能就此放弃,可以选择一种折衷的方式:创建一个中继服务器(relay server),客户端与中继服务器进行通信,中继服务器再将包中继(relay)给通信对端。

至于中继的性能,那就要看具体情况了:

  • 如果能直连,那显然没必要用中继方式;
  • 如果无法直连,而中继路径又非常接近双方直连的真实路径,并且带宽足够大,那中继方式并不会明显降低通信质量。延迟肯定会增加一点,带宽会占用一些,但相比完全连接不上,还是可以接受的。

事实上对于大部分网络而言,Tailscale 都可以通过各种黑科技打洞成功,只有极少数情况下才会选择中继,中继只是一种 fallback 机制。

DERP 又是什么

DERP 即 Detoured Encrypted Routing Protocol,这是 Tailscale 自研的一个协议:

  • 它是一个通用目的包中继协议,运行在 HTTP 之上,而大部分网络都是允许 HTTP 通信的。
  • 它根据目的公钥(destination’s public key)来中继加密的流量(encrypted payloads)。

Tailscale 使用的算法很有趣,所有客户端之间的连接都是先选择 DERP 模式(中继模式),这意味着连接立即就能建立(优先级最低但 100% 能成功的模式),用户不用任何等待。然后开始并行地进行路径发现,通常几秒钟之后,我们就能发现一条更优路径,然后将现有连接透明升级(upgrade)过去,变成点对点连接(直连)。

因此,DERP 既是 Tailscale 在 NAT 穿透失败时的保底通信方式(此时的角色与 TURN 类似),也是在其他一些场景下帮助我们完成 NAT 穿透的旁路信道。 换句话说,它既是我们的保底方式,也是有更好的穿透链路时,帮助我们进行连接升级(upgrade to a peer-to-peer connection)的基础设施。

自建私有 DERP server

Tailscale 的私钥只会保存在当前节点,因此 DERP server 无法解密流量,它只能和互联网上的其他路由器一样,呆呆地将加密的流量从一个节点转发到另一个节点,只不过 DERP 使用了一个稍微高级一点的协议来防止滥用。

Tailscale 开源了 DERP 服务器的代码,如果你感兴趣,可以阅读 DERP 的源代码

Tailscale 官方内置了很多 DERP 服务器,分步在全球各地,惟独不包含中国大陆,原因你懂得。这就导致了一旦流量通过 DERP 服务器进行中继,延时就会非常高。而且官方提供的 DERP 服务器是万人骑,存在安全隐患。

为了实现低延迟、高安全性,我们可以参考 Tailscale 官方文档自建私有的 DERP 服务器。

使用自有域名部署需要满足以下几个条件:

  • 要有自己的域名,并且申请了 SSL 证书
  • 需要准备一台或多台云主机
  • 如果服务器在国内,域名需要备案
  • 如果服务器在国外,则不需要备案

如果以上条件都俱备,就可以按照下面的步骤开始部署了。

安装 golang 并设置 GOPROXY

由于众所周知的网络原因,在安装 golang 时需要使用镜像源,我这里使用的是阿里云的镜像

golang 官网找到 golang 的最新版本并记录

随后登录云服务器,输入

$ wget https://mirrors.aliyun.com/golang/go1.xx.x.linux-amd64.tar.gz # x 换成刚刚记下的版本号	
$ sudo rm -rf /usr/local/go && tar -C /usr/local -xzf go1.21.4.linux-amd64.tar.gz # 安装 golang
$ echo "export PATH=$PATH:/usr/local/go/bin" >> ~/.bashrc && source ~/.bashrc # 添加 golang 到环境变量
$ go env -w GO111MODULE=on
$ go env -w GOPROXY=https://goproxy.cn,direct # 设置 GOPROXY

安装 derper 并配置访问权限

在云服务器中输入

$ go install tailscale.com/cmd/derper@main # 安装官方 derper
$ mv $HOME/go/bin/derper /usr/bin # 移动至 /usr/bin,便于访问
$ sudo curl -fsSL https://tailscale.com/install.sh | sudo sh # (可选)安装 Tailscale,用于防止被白嫖 DERP Server
$ sudo tailscale up # 此处按提示登录即可

配置 systemctl 服务

先创建 service 文件:

$ sudo touch /etc/systemd/system/tailscale-derper.service

然后在其中输入:

[Unit]
Description=Tailscale DERP Server
After=network.target

[Service]
ExecStart=derper --hostname=derp.yourdomain.com --verify-clients 
# 将 derp.yourdomain.com 替换为你的域名,若不需要防止白嫖则不需要使用 --verify-clients 参数
Restart=always
User=root

[Install]
WantedBy=multi-user.target

接着,重新加载systemctl配置:

$ sudo systemctl daemon-reload

启用开机自启并启动 DERP Server:!别忘了打开服务器的安全组哦!

$ sudo systemctl enable tailscale-derper.service
$ sudo systemctl start tailscale-derper.service

若需要检查服务状态或查看日志,则请输入:

sudo systemctl status tailscale-derper.service # 查看服务状态
sudo journalctl -u tailscale-derper.service # 查看日志

配置 derper 自动更新

在云服务器中输入 crontab -e (可能会需要选择编辑器)并在文件最后加入

0 0 * * * go install tailscale.com/cmd/derper@main && mv $HOME/go/bin/derper /usr/bin -f && systemctl restart tailscale-derper.service

保存退出即可

在 Tailscale 面板中配置 DERP Server

打开 Tailscale - Access Controls,在其中加入

{
    // 前略
    "derpMap": {
        // 如果想要所有节点只使用自建中继的话,就启用这条配置
        // "OmitDefaultRegions": true,
		"Regions": {
			"900": {
				"RegionID":   900,
				"RegionCode": "tsh",
				"RegionName": "Shanghai",
				"Nodes": [
					{
						    "Name":     "Shanghai",
						    "RegionID": 900,
						    "HostName": "derp.mydomain.com",
					},
					// 如果有多个区域、多个节点,或者使用了自定义端口,那么可以参考这部分
			"901": {
				"RegionID": 901,
				"RegionCode": "Oracle-OSAKA",
				"Nodes": [
					    {
                        "Name": "Oracle-OSAKA-1",
                        "RegionID": 901,
                        "HostName": "osaka1.derp.mydomain.com",
                        "DERPPort": 4443,
					    },
					    {
                        "Name": "Oracle-OSAKA-1",
                        "RegionID": 901,
                        "HostName": "osaka2.derp.mydomain.com",
                        "DERPPort": 4443,
					    },
					]
				],
			},
		},
	},
}


参考文献

  1. Tailscale 基础教程:部署私有 DERP 中继服务器
  2. Custom DERP Servers · Tailscale Docs
  3. How Tailscale works