自建 Tailscale DERP 中继服务器
by Gralliry
Tailscale 基于 WireGuard,理想情况下两个节点之间会建立 直接的 P2P 连接(称为打洞 / NAT traversal)。
但总有打洞失败的时候:
- 两边都是 对称 NAT(Symmetric NAT)
- 其中一台在 CGNAT(运营商级 NAT) 后面,比如手机热点、某些 4G/5G 网络
- 企业防火墙严格限制 UDP
这时 Tailscale 会通过 DERP 服务器 中继流量。DERP 全称 Detoured Encrypted Routing Protocol,本质就是一个加密中继——你的数据经过它转发,但在 Tailscale 端到端加密之下,DERP 节点看不到你的流量内容。
Tailscale 默认提供全球多个 DERP 节点,但延迟不一定理想(尤其在国内)。自建一个靠近自己的 DERP 节点,能显著改善打洞失败时的连接体验。
准备工作
你需要的就三样东西:
- 一台有公网 IP 的 VPS
- 一个域名(或者泛域名),解析到这台 VPS
- 域名的 TLS 证书
域名与 DNS
DERP 节点要求走 HTTPS,所以必须有一个域名和对应的 TLS 证书。
我的域名是 forye.top,给 DERP 分配的子域是 derp.forye.top:
derp.forye.top A 你的服务器 IP
证书
DERP_CERT_MODE=manual 表示自己管理证书。只需要把证书和私钥放到挂载的 ./certs 目录下:
certs/
├── derp.forye.top.crt # 证书(或 fullchain)
└── derp.forye.top.key # 私钥
注意文件名必须严格匹配:公钥必须命名为
{域名}.crt,私钥必须命名为{域名}.key。比如域名为derp.forye.top,文件名就必须是derp.forye.top.crt和derp.forye.top.key,否则 derper 无法识别加载。
如果
DERP_CERT_MODE设为letsencrypt,derper 会自动申请证书。但在国内网络环境下自动续签可能不稳定,所以这里用manual手动管理。
如果你已经有泛域名证书,也可以直接用——域名匹配即可。
Docker Compose 配置
直接贴完整的 docker-compose.yml:
services:
derper:
image: fredliang/derper
container_name: derper
hostname: derper
environment:
- DERP_CERT_MODE=manual
- DERP_ADDR=:13477
- DERP_HTTP_PORT=13476
- DERP_STUN_PORT=13478
- DERP_DOMAIN=derp.forye.top
- DERP_VERIFY_CLIENTS=true
volumes:
- /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock
- ./certs:/app/certs
ports:
- "13477:13477"
- "13478:13478/udp"
network_mode: bridge
端口说明
| 端口 | 协议 | 用途 |
|---|---|---|
| 13476 | TCP | HTTP 服务(用于 ACME 验证,manual 模式下其实用不到) |
| 13477 | TCP | DERP 主服务——中继流量入口,必须开放 |
| 13478 | UDP | STUN——协助 NAT 打洞,必须开放 |
为避免与服务器上已有服务冲突,把端口都放在了
1347x段。域名正常解析到 443 也没关系——Tailscale 客户端通过/derp/map接口拿到的是实际端口。
环境变量详解
| 变量 | 值 | 说明 |
|---|---|---|
DERP_CERT_MODE |
manual |
手动管理证书,从 /app/certs 目录读取 |
DERP_ADDR |
:13477 |
DERP 中继服务监听端口 |
DERP_HTTP_PORT |
13476 |
HTTP 服务端口(健康检查等) |
DERP_STUN_PORT |
13478 |
STUN 端口,用于 NAT 类型探测 |
DERP_DOMAIN |
derp.forye.top |
DERP 节点的域名 |
DERP_VERIFY_CLIENTS |
true |
验证来访客户端是否属于你的 Tailnet |
DERP_VERIFY_CLIENTS=true 的作用
设为 true 后,derper 会通过挂载的 tailscaled.sock 与本地 Tailscale 守护进程通信,验证请求的节点是否在你的 Tailnet 中。这是重要的安全措施——否则任何人都可以用你的 DERP 节点中继流量(他们依然解不了密,但你的带宽和 CPU 会被消耗)。
前提是你的 VPS 也加入了同一个 Tailnet:
# 在 VPS 上登录到你的 Tailnet
tailscale up --accept-routes=false --advertise-tags=tag:derper
tag:derper需要在 Tailscale 控制台创建 ACL tag(Settings→Keys→Tags),并授权给相应的用户或密钥。
如果 VPS 不方便跑 Tailscale,也可以把 DERP_VERIFY_CLIENTS 设为 false,但建议配合防火墙限制来源 IP。
启动
docker compose up -d
查看日志确认启动正常:
docker compose logs -f
看到类似这样的输出就说明服务跑起来了:
DERP server started on :13477
STUN server started on :13478
注册到 Tailnet
DERP 节点启动后,还需要告诉 Tailscale 你的自定义 DERP 服务器在哪。配置方式有两种:
- 在线 URL 形式:Tailscale/Headscale 从指定 URL 拉取 JSON 格式的 DERP 映射
- 本地文件形式:在客户端本地写 YAML 格式的配置文件
这里以在 Tailscale 中配置 DERP 节点为例。
ACL 配置
进入 Tailscale Admin Console → ACLs,在现有规则下方增加 "derpMap" 字段并保存:
{
"derpMap": {
// 设为 true 则不下发官方的 DERP 节点
// 测试或想完全自控时可打开
"OmitDefaultRegions": false,
"Regions": {
"900": {
// Tailscale 的 900–999 是保留给自定义 DERP 的
"RegionID": 900,
"RegionCode": "self-derper",
"RegionName": "自建 HongKong DERP",
"Nodes": [
{
"Name": "self_derper_1",
"RegionID": 900,
"HostName": "derp.forye.top",
"DERPPort": 13477,
"STUNPort": 13478,
// 如果使用自签名证书,客户端不做证书校验
// (用了受信任证书(如 Let's Encrypt)则删掉或设为 false)
"InsecureForTests": true
}
]
}
}
}
}
OmitDefaultRegions 说明
| 值 | 行为 |
|---|---|
false(默认) |
保留 Tailscale 官方所有 DERP 节点,自定义节点追加到列表中 |
true |
移除全部官方 DERP 节点,只使用自定义节点 |
测试阶段建议设为 false,这样即使自定义节点有问题也不影响现有连接。
移除单个官方节点
如果只想移除某个特定官方节点(比如延迟太高的),把它的 RegionID 设为 null 即可:
{
"derpMap": {
"Regions": {
"1": null
}
}
}
把 1 换成其它官方 RegionID 可以逐个移除。官方 RegionID 可以通过 tailscale netcheck 查看。
配置字段说明
| 字段 | 说明 |
|---|---|
RegionID |
900–999,自定义 DERP 的保留范围 |
RegionCode |
短标识符,如 self-derper |
RegionName |
人类可读的名称,会在客户端日志中显示 |
HostName |
DERP 服务器的域名 |
DERPPort |
DERP 中继端口(对应 DERP_ADDR) |
STUNPort |
STUN 端口(对应 DERP_STUN_PORT) |
InsecureForTests |
自签名证书时设为 true 跳过客户端证书校验 |
IPv4 / IPv6 |
可选,显式指定 IP 地址。"IPv6": "none" 表示无 IPv6 |
STUNOnly |
true 表示该节点只做 STUN,不中继流量 |
客户端 Side-load(测试用)
如果不方便改 ACL,也可以在单台客户端上临时指定 DERP 映射:
tailscale up --derp-map=/path/to/derp-map.json
保存为 /path/to/derp-map.json:
{
"Regions": {
"901": {
"RegionID": 901,
"RegionCode": "self-derper",
"RegionName": "自建 HongKong DERP",
"Nodes": [
{
"Name": "self_derper_1",
"RegionID": 901,
"HostName": "derp.forye.top",
"DERPPort": 13477,
"STUNPort": 13478,
"STUNOnly": false
}
]
}
}
}
验证是否生效
浏览器直接访问
tailscale netcheck 实际上只检测 3478/udp 端口(STUN),就算 netcheck 显示能连通,也不代表 DERP 的 443(或自定义端口)可以转发流量。
最简单的办法是直接打开 DERP 服务器的 HTTPS 页面:
https://derp.forye.top:13477
页面能正常显示且地址栏的 SSL 证书标签显示正常可用,才是真的没问题了。如果浏览器访问都报证书错误或连接超时,客户端也肯定无法通过它中继流量。
命令行验证
确认页面可访问后,在任意 Tailscale 客户端上运行:
tailscale netcheck
输出中应该能看到你的自定义节点:
Report:
DERP region: self-derper (900) ...
STUN server: derp.forye.top:13478 ...
更直接的方式是强制走中继来测试连接:
tailscale ping --derp 目标节点的IP
如果输出显示 via DERP(self-derper) 就是成功。
也可以查看当前所有连接的中继路径:
tailscale status --json | jq '.Peer[] | select(.ID != 0) | {HostName, Relay}'
常见问题
连接显示 still processing / timeout
检查防火墙,确保 13477 (TCP) 和 13478 (UDP) 已放行:
# Ubuntu/Debian
ufw allow 13477/tcp
ufw allow 13478/udp
# CentOS/RHEL
firewall-cmd --add-port=13477/tcp --permanent
firewall-cmd --add-port=13478/udp --permanent
firewall-cmd --reload
如果用的是云服务商(阿里云、腾讯云、AWS 等),也要在安全组/防火墙规则里放行这两个端口。
STUN 无法工作
STUN 是 UDP 协议,容易被 NAT 或防火墙拦截。可以用 stunclient 测试:
# 安装 stunclient
apt install stun-client # Debian/Ubuntu
yum install stun # CentOS/RHEL
# 测试
stunclient derp.forye.top 13478
证书过期
手动更新证书后,重启容器即可:
docker compose restart
如果发现 DERP_CERT_MODE=letsencrypt 自动续签失败,切回 manual 并手动更新证书是最稳妥的。
总结
自建 DERP 服务器的步骤就这些:
- 准备域名和证书
- 用 Docker Compose 启动 derper
- 在 Tailscale ACL 中注册你的 DERP 节点
- 验证连接
搭建完成后,你的 Tailnet 多了一个低延迟的中继节点。虽然 P2P 直连永远是首选,但在打洞失败时,一个靠近自己的 DERP 节点能让连接体验好很多。
tags: tailscale - derp - vpn - 自建