上篇文章介绍了如何使用 Headscale
替代 Tailscale 官方的控制服务器,并接入各个平台的客户端。本文将会介绍如何让 Tailscale 使用自定义的 DERP Servers。可能很多人都不知道 DERP
是个啥玩意儿,没关系,我先从中继服务器开始讲起。
STUN 是什么
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 机制。
中继协议简介
中继协议有多种实现方式。
TURN
TURN 即 Traversal Using Relays around NAT,这是一种经典的中继实现方式,核心理念是:
- 用户(人)先去公网上的 TURN 服务器认证,成功后后者会告诉你:“我已经为你分配了 ip:port,接下来将为你中继流量”,
- 然后将这个 ip:port 地址告诉对方,让它去连接这个地址,接下去就是非常简单的客户端/服务器通信模型了。
与 STUN 不同,这种协议没有真正的交互性,不是很好用,因此 Tailscale 并没有采用 TURN 作为中继协议。
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 服务器。有两种部署模式,一种是基于域名,另外一种不需要域名,可以直接使用 IP,不过需要一点黑科技。我们先来看最简单的使用域名的方案。
使用域名
这种方案需要满足以下几个条件:
- 要有自己的域名,并且申请了 SSL 证书
- 需要准备一台或多台云主机
- 如果服务器在国内,域名需要备案
- 如果服务器在国外,则不需要备案
如果以上条件都俱备,就可以按照下面的步骤开始部署了。
推荐直接使用 Docker 来部署,我已经构建好了 Docker 镜像,直接部署就可以了:
? → docker run --restart always \
--name derper -p 12345:12345 -p 3478:3478/udp \
-v /root/.acme.sh/xxxx/:/app/certs \
-e DERP_CERT_MODE=manual \
-e DERP_ADDR=:12345 \
-e DERP_DOMAIN=xxxx \
-d ghcr.io/yangchuansheng/derper:latest
有几点需要注意:
- 能用 443 端口尽量用 443 端口,实在不行再用别的端口;
- 默认情况下也会开启 STUN 服务,UDP 端口是
3478
; - 防火墙需要放行端口 12345 和 3478;
- 准备好 SSL 证书;
- 域名部分我打了码,请换成你自己的域名。
关于证书部分需要重点说明:假设你的域名是 xxx.com
,那么证书的名称必须是 xxx.com.crt
,一个字符都不能错!同理,私钥名称必须是 xxx.com.key
,一个字符都不能错!
查看容器日志:
? → docker logs -f derper
2022/03/26 11:36:28 no config path specified; using /var/lib/derper/derper.key
2022/03/26 11:36:28 derper: serving on :12345 with TLS
2022/03/26 11:36:28 running STUN server on [::]:3478
目前 derper 运行一段时间就会崩溃,暂时还没有更好的解决方案,只能通过定时重启来解决,比如通过 crontab 来设置每两小时重启一次容器:
0 */2 * * * docker restart derper &> /dev/null
具体可参考这个 issue: Derper TLS handshake error: remote error: tls: internal error
部署好 derper 之后,就可以修改 Headscale 的配置来使用自定义的 DERP 服务器了。Headscale 可以通过两种形式的配置来使用自定义 DERP:
- 一种是在线 URL,格式是
JSON
,与 Tailscale 官方控制服务器使用的格式和语法相同。 - 另一种是本地文件,格式是
YAML
。
我们可以直接使用本地的 YAML 配置文件,内容如下:
# /etc/headscale/derp.yaml
regions:
900:
regionid: 900
regioncode: thk
regionname: Tencent Hongkong
nodes:
- name: 900a
regionid: 900
hostname: xxxx
ipv4: xxxx
stunport: 3478
stunonly: false
derpport: 12345
- name: 900b
regionid: 900
hostname: xxxx
ipv4: xxxx
stunport: 3478
stunonly: false
derpport: 12345
901:
regionid: 901
regioncode: hs
regionname: Huawei Shanghai
nodes:
- name: 901a
regionid: 901
hostname: xxxx
ipv4: xxxx
stunport: 3478
stunonly: false
derpport: