上篇介绍了Headscale+Tailscale的部署过程,也提到了DERP是中转流量的,Headscale默认使用的是Tailscale公司提供的公共DERP服务器组,这些服务器都在大陆地区以外,离大陆最近的是香港、东京两个节点,延迟和带宽都不太理想,在Tailscale打洞失败的情况下连接体验非常差,要改善连接体验就需要自建DERP服务。本篇就着重介绍自建DERP的过程和其中要踩的坑。
DERP工作方式
在开始搭建之前,需要了解一下DERP的工作方式,为后面的流程避坑。
DERP服务器提供两种服务:
- STUN:以udp方式工作,告知节点通过层层nat后最终对应的公网ip和端口号
- DERP:以http+websocket方式工作,为节点提供流量转发服务
Tailscale的工作逻辑是先走DERP中转尽快建立连接,再尝试打通NAT走直连。
Tailscale会先尝试与DERP建立http连接,并升级协议到websocket,打开ws连接后,再尝试通过STUN服务提供的ip端口来打通NAT直连。
操作环境
- 云服务器一台,有nginx做反向代理,配置好https证书
- 如果域名不需要备案,服务器80、443端口没有其他服务占用,也可以让DERP服务端自己申请证书完成https加密
DERP部署
Tailscale官方并没有提供DERP的镜像,所以这里选用的是第三方的DERP镜像。
docker run -d \
--name derp \
--restart unless-stopped \
-p 8800:80 \
-p 3478:3478/udp \
--log-opt max-size=10M \
-v /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock \
sparanoid/derp:edge \
derper \
-a :80 \
-verify-clients true
- derper是DERP服务的二进制文件
- 上面打开的
80
和3478
分别是DERP
和STUN
的端口。 - 指定的
-v
选项和下面给derper传递的-verify-clients
选项是用于验证连接DERP的节点身份的,-verify-clients
参数是默认不开启的,谁都可以通过你自建的DERP中转流量,对于流量有限的服务器来说难以接受,这就需要在derper所在的服务器上安装Tailscale,并加入到同一个虚拟网络中,打开derper的-verify-clients
选项,同时设置-v
参数,将Tailscale的socket套接字映射给derper,这样derper就会通过宿主机上的Tailscale来验证节点的身份,只有和derper在同一个虚拟网络中的节点才有权通过本DERP转发流量。 - 指定log的max-size是因为在打开
-verify-clients
选项时,每一条认证失败的信息都会被打印到日志里,在一些未授权节点只设置了一个DERP服务器的情况下,未授权节点会不停尝试连接DERP服务器,然后被DERP服务器拒绝连接,时间长了DERP服务器写的日志可能会占用太多的存储空间。 -a
参数指定非443端口时,内置的证书申请服务就会被禁用,derp会以http方式提供服务,https加密就需要前端的nginx来实现。
❗这里有一个坑,-a
参数必须在-verify-clients
前面,不然指定端口不生效,derper会以443端口https模式提供服务
其他derper的参数可以参考本文末尾。
在部署完成后,访问8800端口,会看到以下内容:
DERP
This is a Tailscale DERP server.
Nginx反向代理配置
这一步给DERP配置反代,Nginx已经做好了https,直接将http请求转发给DERP
server {
listen 443;
server_name derp.example.domain;
location / {
proxy_pass http://127.0.0.1:8800;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# websocket握手
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection Upgrade;
}
}
❗这里又是一个坑,DERP服务是通过http+websocket的方式工作的,如果不把ws的请求头转发,虽然能访问到DERP的页面,但Tailscale无法通过DERP转发流量。网上很多教程都说看见页面就可以了,那只是http可以了,websocket还不行,需要在nginx的反代配置里加上相应的参数,让Tailscale的ws握手请求能转发给DERP,Tailscale才能通过DERP转发流量。
Headscale设置DERP列表
在Headscale的设置文件夹中新建一个derp.yaml
文件,写入DERP的配置信息
regions:
901:
regionid: 901
regioncode: DERP
regionname: Custom DERP
nodes:
- name: DERP_SERVER_1
regionid: 901
hostname: derp.example.domain
stunport: 3478
stunonly: false
derpport: 443
regionid
在900
-999
之间任选,这一段是Tailscale官方为自建DERP保留的,这里选择901。regioncode
、regionname
、nodes.name
都可以自己取,然后根据自建DERP的信息修改配置文件的内容。
然后修改Headscale的主配置文件,添加自建DERP:
---
server_url: http://*.*.*.*:*
listen_addr: 0.0.0.0:*
metrics_listen_addr: 0.0.0.0:9090
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: false
ip_prefixes:
- 100.64.0.0/10
derp:
urls: [] # 如果不希望使用Tailscale提供的公共DERP组,可以像我一样注释掉
# - https://controlplane.tailscale.com/derpmap/default
paths:
- /etc/headscale/derp.yaml # 上面添加的配置文件
在任意节点上执行命令,检查DERP配置下发状态
root@centos:~# tailscale netcheck
Report:
* UDP: true
* IPv4: yes, *.*.*.*:*
* IPv6: no, but OS has support
* MappingVariesByDestIP:
* HairPinning: false
* PortMapping: UPnP, NAT-PMP, PCP
* Nearest DERP: Custom DERP
* DERP latency:
- DERP: 29.3ms (Custom DERP)
测试DERP转发
首先选任意两节点,在一节点上ping另一节点,等待NAT穿透后,通过tailscale命令获取直连ip:
root@ubuntu:~# tailscale status
100.64.0.1 ubuntu root linux -
100.64.0.3 centos root linux active; direct 1.1.1.1:11111, tx 2120992 rx 453744
在iptables中将直连ip的数据包drop掉:
iptables -A OUTPUT -d 1.1.1.1 -j DROP
有时候这一步drop掉还能直连,是因为直连可以是双向的,drop掉一个节点的ip,还有另一个节点的ip可以直连,再ping一次拿到新的直连ip设置drop即可。
当drop掉直连ip时,再次ping另一节点,如果延迟稳定在几十ms(取决于节点到DERP服务器的延迟),则说明当前连接走DERP中继,服务搭建成功,如果出现网络不可达等,则说明DERP的部署有问题。
也可以通过tailscale命令来确定流量是否走转发:
root@ubuntu:~# tailscale status
100.64.0.1 ubuntu root linux -
100.64.0.3 centos root linux active; relay "DERP", tx 2122484 rx 455116
如上,显示relay "DERP"
,则说明当前流量走自建DERP。
测试完成后,删除iptables的drop规则:
iptables -D OUTPUT -d 1.1.1.1 -j drop
克隆机器时避免重复加入
克隆虚拟机时会把ts的配置一起克隆,两个机器同时加入网络会冲突,需要在新的机器上面删除ts的配置后再重新加入网络
rm -rf /var/lib/tailscale/*
参考链接
@米开朗基杨 的博客: 链接
Tailscale的自建DERP教程:链接
derper的参数说明
/app # derper -h
Usage of derper:
-a string
server HTTP/HTTPS listen address, in form ":port", "ip:port", or for IPv6 "[ip]:port". If the IP is omitted, it defaults to all interfaces. Serves HTTPS if the port is 443 and/or -certmode is manual, otherwise HTTP. (default ":443")
-accept-connection-burst int
burst limit for accepting new connection (default 9223372036854775807)
-accept-connection-limit float
rate limit for accepting new connection (default +Inf)
-bootstrap-dns-names string
optional comma-separated list of hostnames to make available at /bootstrap-dns
-c string
config file path
-certdir string
directory to store LetsEncrypt certs, if addr's port is :443 (default "/root/.cache/tailscale/derper-certs")
-certmode string
mode for getting a cert. possible options: manual, letsencrypt (default "letsencrypt")
-derp
whether to run a DERP server. The only reason to set this false is if you're decommissioning a server but want to keep its bootstrap DNS functionality still running. (default true)
-dev
run in localhost development mode (overrides -a)
-hostname string
LetsEncrypt host name, if addr's port is :443 (default "derp.tailscale.com")
-http-port int
The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag. (default 80)
-mesh-psk-file string
if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.
-mesh-with string
optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list
-stun
whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value. (default true)
-stun-port int
The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag. (default 3478)
-unpublished-bootstrap-dns-names string
optional comma-separated list of hostnames to make available at /bootstrap-dns and not publish in the list
-verify-clients
verify clients to this DERP server through a local tailscaled instance.