本文记录了 Tailscale 自建 DERP 中继服务器从不可用到修复的完整排查过程,涵盖 ACL 配置错误、TLS 证书验证失败、云安全组端口未开放等问题的诊断与解决。
问题现象
Windows 客户端(home-pc)无法通过 Tailscale IP(100.xxxx.xxx.xxx)SSH 连接到 Linux 服务器:
sql
ssh root@100.xxxx.xxx.xxx
ssh: connect to host 100.xxxx.xxx.xxx port 22: Connection timed out
同时健康检查报告 DERP 中继服务器无法连接。
排查过程
环境拓扑
自建 DERP 中继服务与目标 Linux 服务器运行在同一台机器上,使用公网 IP xxx.xxxx.xxx.xxx。
| 角色 | 信息 |
|---|---|
| Linux 服务器 / DERP 节点 | xxx.xxxx.xxx.xxx / 100.xxxx.xxx.xxx |
| Windows 客户端 | 100.xxxx.xxx.xxx |
| Tailscale 版本(服务端) | 1.98.2 |
| 云服务商 | 火山引擎(北京) |
问题一:ACL derpMap 中 HostName 为空
在目标机上执行 tailscale debug derp-map 发现自建 DERP 节点的 HostName 字段为空:
json
{
"Name": "1",
"RegionID": 900,
"HostName": "", // ← 致命错误
"IPv4": "xxx.xxxx.xxx.xxx",
"DERPPort": xxxxx,
"InsecureForTests": true
}
HostName 为空导致客户端无法确定连接目标,DERP 连接直接失败。
问题二:DERP 证书目录缺失
Docker 容器(yangchuansheng/derper)挂载的宿主机证书目录 /etc/derper/certs 不存在,导致容器内无有效 TLS 证书,derper 日志持续报错:
vbnet
http: TLS handshake error: cert mismatch with hostname: ""
问题三:云安全组未放行 TCP xxxxx
从 Windows 客户端测试 TCP 连通性发现 xxxxx 端口超时:
bash
Test-NetConnection xxx.xxxx.xxx.xxx -Port xxxxx
# TcpTestSucceeded : False
火山引擎安全组默认只放行了常用端口,未开放自定义的 DERP 端口。
问题四:derper 源码的 TLS SNI 检查
既有的 Docker 镜像(yangchuansheng/derper)虽然声称禁用了证书验证,但并未完全注释掉 derper 源码中对 TLS ClientHello SNI 的检查。客户端以 IP 直连时 SNI 字段为空,导致握手被拒绝。
解决方式:从 tailscale 源码编译,注释掉 cert.go 中的 hostname 检查:
go
// 修改前:
if hi.ServerName != m.hostname && !m.noHostname {
return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName)
}
// 修改后:
if false && hi.ServerName != m.hostname && !m.noHostname {
return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName)
}
问题五:健康检查误报(自签证书 + CGNAT 场景)
在 Docker 部署 + 自签证书 + CGNAT 网络环境下,Tailscale 客户端健康检查可能持续报告"无法连接到自建 DERP 中继服务器",但实际中继功能正常。这是由于 Tailscale 健康检查的 TLS 验证方式比 netcheck 更严格,自签证书不被健康检查信任所致。
关键结论: tailscale status 显示 active; relay "self-derp" 说明中继实际在工作。tailscale netcheck 也能正确测出延迟。健康检查警告在此场景下属于 cosmetic issue(表象问题) ,不影响功能。
消除方法(三选一):
- 换用真实域名 + Let's Encrypt(
DERP_CERT_MODE=letsencrypt),获得受信任的 TLS 证书 - 启用 Tailscale HTTPS Cert(需 tailnet 开启 HTTPS 支持),用
tailscale cert签发证书 - 忽略警告,功能不受影响
最终修复方案
| 问题 | 修复操作 | 命令 / 操作 | 状态 |
|---|---|---|---|
| ACL HostName 为空 | 补全 HostName = xxx.xxxx.xxx.xxx,删除冗余 CertName | Tailscale 管理后台 → Access Controls | ✅ |
| 证书缺失 | 手动生成自签证书 | openssl req -x509 ... | ✅ |
| SNI 检查 | 从源码编译 patched derper | sed 注释 cert.go 后 go build | ✅ |
| 安全组端口 | 火山引擎控制台添加规则 | TCP xxxxx 入方向放行 | ✅ |
| 服务持久化 | systemd 管理 derper | systemctl enable derper | ✅ |
最终 ACL derpMap 配置
json
{
"derpMap": {
"Regions": {
"900": {
"RegionID": 900,
"RegionCode": "self-derp",
"RegionName": "自建 DERP",
"Nodes": [
{
"Name": "1",
"RegionID": 900,
"HostName": "xxx.xxxx.xxx.xxx",
"IPv4": "xxx.xxxx.xxx.xxx",
"DERPPort": xxxxx,
"STUNPort": 3478,
"InsecureForTests": true
}
]
}
}
}
}
systemd 服务配置
ini
[Unit]
Description=Tailscale DERP Relay Server
After=network.target
[Service]
User=root
Restart=always
ExecStart=/usr/local/bin/derper --hostname=xxx.xxxx.xxx.xxx --certmode=manual --certdir=/etc/derper/certs -a=:xxxxx --stun=true --stun-port=3478 --http-port=-1
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
Docker 化部署方案(替代方案)
除源码编译外,也可使用 Docker 镜像 yangchuansheng/derper 快速部署,无需手动编译和 systemd 管理。
部署命令
ini
docker run -d \
--name derper \
--restart unless-stopped \
-p xxxxx:xxxxx/tcp \
-p 3478:3478/udp \
-v /etc/derper/certs:/app/certs:ro \
-v /var/run/tailscale:/var/run/tailscale:ro \
-e DERP_DOMAIN=derp.tailnet.local \
-e DERP_CERT_MODE=manual \
-e DERP_CERT_DIR=/app/certs \
-e DERP_ADDR=':xxxxx' \
-e DERP_STUN=true \
-e DERP_HTTP_PORT='-1' \
-e DERP_VERIFY_CLIENTS=true \
yangchuansheng/derper
关键差异
| 维度 | 源码编译方案 | Docker 方案 |
|---|---|---|
| 部署复杂度 | 需安装 Go、编译、systemd | 一行命令启动 |
| 证书管理 | 手动生成 + 挂载 | manual 模式自动生成自签证书;letsencrypt 模式自动申请 |
| 客户端验证 | 需编译时加 --verify-clients | 设置 DERP_VERIFY_CLIENTS=true,挂载 tailscale socket |
| SNI 检查 | 需手动注释 cert.go | 镜像已处理,配合 InsecureForTests 使用 |
| 代理配置 | 国内需配置 GOPROXY | Docker daemon 配置代理或 ~/.docker/config.json |
CGNAT 环境下的 ACL 配置
当服务器处于 CGNAT 之后无独立公网 IP 时,使用 Tailscale IP 作为节点地址,其他 Tailnet 成员通过 WireGuard 隧道直连 DERP:
json
{
"derpMap": {
"OmitDefaultRegions": false,
"Regions": {
"900": {
"RegionID": 900,
"RegionCode": "self-derp",
"RegionName": "自建 DERP",
"Nodes": [
{
"Name": "1",
"RegionID": 900,
"IPv4": "100.xxxx.xxx.xxx",
"DERPPort": xxxxx,
"STUNPort": 3478,
"InsecureForTests": true
}
]
}
}
}
}
注意: 使用 Tailscale IP 时无需 HostName 字段。所有 Tailnet 成员均可通过 WireGuard 隧道(100.x.x.x)到达 DERP 服务。但 tailscale cert 签发 HTTPS 证书需要 tailnet 开启 HTTPS 支持,CGNAT 环境下通常不可用。
验证结果
| 检查项 | 结果 |
|---|---|
| SSH via Tailscale IP | ✅ 正常 |
| tailscale netcheck(自建 DERP) | ✅ self-derp: 26.9ms |
| tailscale status | ✅ active; relay "self-derp" |
| 健康检查(DERP 告警) | ✅ 已消除 |
| derper 开机自启 | ✅ systemd enabled |
Docker 部署验证结果(本会话):
| 检查项 | 结果 |
|---|---|
| tailscale netcheck(Windows) | ✅ self-derp: 24.8ms |
| tailscale netcheck(Linux 服务器) | ✅ self-derp: 0.9ms |
| tailscale status(Windows → Linux) | ✅ active; relay "self-derp" |
| 健康检查(DERP 告警) | ⚠️ 自签证书场景下持续误报,实际中继正常 |
| SSH via Tailscale IP | ⚠️ DERP relay 建立后可达,tailscale down/up 后需等待连接稳定 |
| Docker 容器状态 | ✅ running,--restart unless-stopped |
关键经验
- ACL derpMap 中的 HostName 必须非空,纯 IP 部署时设为公网 IP 即可
InsecureForTests: true只跳过客户端验证服务端证书,不跳过 derper 对 SNI 的检查------需要从源码注释相关逻辑--verify-clients和CertName同时使用时可能出现冲突,建议二选一- 飞书文档创建 API 支持 XML 格式,可用 HTML 子集标签构建结构化内容
- 在国内云服务器编译 Tailscale 需配置 GOPROXY=goproxy.cn
- Docker 部署方案比源码编译更简洁,适合快速验证;国内环境需在 Docker daemon 或 ~/.docker/config.json 中配置 HTTP 代理拉取镜像
- CGNAT 环境下自建 DERP 可使用 Tailscale IP(100.x.x.x)作为节点地址,无需独立公网 IP
- CGNAT + 自签证书场景下健康检查误报是已知的 cosmetic issue,实际中继功能不受影响
- tailscale down/up 后 DERP relay 需要几秒钟重建,SSH 等连接需等状态变为 active 后再尝试
强制使用自建中继
在 ACL derpMap 中添加 OmitDefaultRegions: true 可完全禁用 Tailscale 官方 DERP 节点,所有中继流量强制走自建节点:
json
{
"derpMap": {
"OmitDefaultRegions": true,
"Regions": {
"900": {
"RegionID": 900,
"RegionCode": "self-derp",
"RegionName": "自建 DERP",
"Nodes": [
{
"Name": "1",
"RegionID": 900,
"HostName": "xxx.xxxx.xxx.xxx",
"IPv4": "xxx.xxxx.xxx.xxx",
"DERPPort": xxxxx,
"STUNPort": 3478,
"InsecureForTests": true
}
]
}
}
}
}
如果客户端分布在国外,设 true 后可能因无法连接自建中继导致组网失败。稳妥做法是保持 false(默认),让 Tailscale 自动选择延迟最低的节点。
暂时无法在飞书文档外展示此内容