Tailscale 自建 DERP 中继服务器故障排查与修复记录

本文记录了 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(表象问题) ,不影响功能。

消除方法(三选一):

  1. 换用真实域名 + Let's Encrypt(DERP_CERT_MODE=letsencrypt),获得受信任的 TLS 证书
  2. 启用 Tailscale HTTPS Cert(需 tailnet 开启 HTTPS 支持),用 tailscale cert 签发证书
  3. 忽略警告,功能不受影响

最终修复方案

问题 修复操作 命令 / 操作 状态
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-clientsCertName 同时使用时可能出现冲突,建议二选一
  • 飞书文档创建 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 自动选择延迟最低的节点。

暂时无法在飞书文档外展示此内容

相关推荐
用户6757049885021 小时前
Redis有1亿个Key,如何优雅地找出特定前缀的那10万条?
后端
用户6757049885022 小时前
程序员常犯的坑:别再用 VARCHAR 存 IP 了!用对方式,性能何止提升10倍!
后端
老马95272 小时前
opencode8-桌面应用实战 3
前端·人工智能·后端
用户298698530142 小时前
Java 中的 Word 变量管理:添加、统计、获取与删除
java·后端
神奇小汤圆2 小时前
互联网大厂精选面试八股文(附2026最新Java+AI高频题)
后端
EMA2 小时前
智旅云图(一个智能旅游规划项目)学习指南
人工智能·后端
传说之后2 小时前
Go 网络编程:从 TCP 字节流到自定义协议设计
后端·架构
Rust研习社2 小时前
手把手带你使用 Bacon 高效开发应用
后端·rust·编程语言
Nturmoils2 小时前
书签真正难的不是收藏,而是找回来:我是怎么做这个 Chrome 插件的
javascript·后端·浏览器