Gralliry's Blog

记录学习与生活

2026/07/26

自建 Tailscale DERP 中继服务器

by Gralliry

Tailscale 基于 WireGuard,理想情况下两个节点之间会建立 直接的 P2P 连接(称为打洞 / NAT traversal)。

但总有打洞失败的时候:

这时 Tailscale 会通过 DERP 服务器 中继流量。DERP 全称 Detoured Encrypted Routing Protocol,本质就是一个加密中继——你的数据经过它转发,但在 Tailscale 端到端加密之下,DERP 节点看不到你的流量内容。

Tailscale 默认提供全球多个 DERP 节点,但延迟不一定理想(尤其在国内)。自建一个靠近自己的 DERP 节点,能显著改善打洞失败时的连接体验。


准备工作

你需要的就三样东西:

  1. 一台有公网 IP 的 VPS
  2. 一个域名(或者泛域名),解析到这台 VPS
  3. 域名的 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.crtderp.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(SettingsKeysTags),并授权给相应的用户或密钥。

如果 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 服务器在哪。配置方式有两种:

这里以在 Tailscale 中配置 DERP 节点为例。

ACL 配置

进入 Tailscale Admin Console → ACLs,在现有规则下方增加 "derpMap" 字段并保存:

{
  "derpMap": {
    // 设为 true 则不下发官方的 DERP 节点
    // 测试或想完全自控时可打开
    "OmitDefaultRegions": false,
    "Regions": {
      "900": {
        // Tailscale  900999 是保留给自定义 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 服务器的步骤就这些:

  1. 准备域名和证书
  2. 用 Docker Compose 启动 derper
  3. 在 Tailscale ACL 中注册你的 DERP 节点
  4. 验证连接

搭建完成后,你的 Tailnet 多了一个低延迟的中继节点。虽然 P2P 直连永远是首选,但在打洞失败时,一个靠近自己的 DERP 节点能让连接体验好很多。

tags: tailscale - derp - vpn - 自建