Skip to content

Tailscale 自建 DERP 服务器(Ubuntu)

校对日期:2026-09-04。本文基于 Tailscale 当前官方文档、v1.102.3 稳定版源码和最新策略字段整理。

DERP 是 Tailscale 的加密中继服务。节点无法建立直连或 Peer Relay 时,才会通过 DERP 转发已加密的 WireGuard 数据。Tailscale 目前仍将自建 DERP 标记为 Alpha;如果问题是频繁无法直连,仍应优先排查 NAT 和防火墙。

方案选择

方案对外地址证书续期适用场景
A:内置 Let's Encrypt域名derper 自动申请自动默认推荐,最简单
B:外部证书域名外部 ACME、公信或私有 CA外部程序负责已有统一证书管理,或需要 DNS-01
C:纯 IP 公信证书公网 IPLet's Encrypt 短期 IP 证书自动无域名时的纯 IP 优选方案
D:纯 IP 自签公网 IPderper 自动生成,CertName 指纹锁定手动无法使用 ACME IP 证书时的备选

方案 D 同时属于“自签”和“纯 IP”。derper 只会在 --hostname 是 IP 且相应证书文件不存在时自动生成自签证书;域名场景没有必要使用自签证书。

统一约定

文中使用以下示例值,部署时必须全部替换:

配置示例值说明
DERP 域名derp.example.com方案 A/B 使用
公网 IPv4203.0.113.10文档专用示例地址,不可直接使用
公网 IPv62001:db8::10可选;没有可用 IPv6 时显式填 none
自定义区域 ID900Tailscale 为用户保留 900999
DERP HTTPS 端口443/tcpDERPPort 的默认值
STUN 端口3478/udpSTUNPort 的默认值
持久化目录/var/lib/derper存放 derper 私钥和证书

1. 准备网络

  1. VPS 应有固定、公网可路由的 IP,且客户端能直接访问。
  2. 方案 A/B 需要创建 derp.example.com 的 A 记录;只在 IPv6 确实可用时创建 AAAA 记录。
  3. 不要在 DERP 前放 CDN、HTTP 反向代理或全局负载均衡器。使用 Cloudflare DNS 时,应设为“仅 DNS”。
  4. 在云平台安全组和主机防火墙中放行入站 TCP 443、UDP 3478 以及入站/出站 ICMP。方案 A/C 还必须放行 TCP 80。
  5. 不要对 UDP 3478 做限速,也不要使用会吞掉 TCP RST 的防火墙策略。

如果主机已使用 UFW:

bash
sudo ufw allow 443/tcp
sudo ufw allow 3478/udp
# 仅方案 A/C 必须;方案 B/D 可选
sudo ufw allow 80/tcp
sudo ufw status

TCP 80 对方案 A/C 的 ACME HTTP-01 验证是必需的,不能关闭。方案 B/D 不依赖 HTTP-01,可用 --http-port=-1 禁用 HTTP 监听;代价是该节点不再提供 /generate_204 和捕获门户检测。

2. 安装 Go 和 derper

derper 没有官方二进制包,需用最新 Go 从源码构建。Ubuntu 可使用 Snap:

bash
sudo apt update
sudo apt install -y snapd
sudo snap install go --classic
go version

在 Go Wiki 列出的 Ubuntu 18.04、20.04、22.04 和 24.04(amd64arm64armhf)上,也可选用 longsleep/golang-backports PPA:

bash
sudo apt update
sudo apt install -y software-properties-common
sudo add-apt-repository -y ppa:longsleep/golang-backports
sudo apt update
sudo apt install -y golang-go
go version

该 PPA 是社区维护包,不由 Go 项目创建或提供支持,发布节奏也可能落后。如果编译 derper 时提示 Go 版本过低,应改用 Snap 或 Go 官方安装包,不要强行使用旧编译器。本文不写死易过期的 Go 版本号。

构建并安装 derper:

bash
go install tailscale.com/cmd/derper@latest
sudo install -m 0755 "$(go env GOPATH)/bin/derper" /usr/local/bin/derper
/usr/local/bin/derper --version

纯 IP 公信证书方案还应确认当前二进制已支持 --acme-ip-certs

bash
/usr/local/bin/derper --help 2>&1 | grep -- '--acme-ip-certs'

如果下载 Go 模块超时,再根据所在网络配置可信的 GOPROXY,不要在文档中写死第三方代理。

3. 创建运行用户

bash
sudo useradd --system --home-dir /var/lib/derper --shell /usr/sbin/nologin derper
sudo install -d -o derper -g derper -m 0750 /var/lib/derper /var/lib/derper/certs

如果 derper 用户已存在,只需执行第二条命令。

4. 准备证书

四种方案只选一种。

方案 A:域名 + 内置 Let's Encrypt

无需手工准备证书。derper 通过 TCP 80 完成 HTTP-01 验证,并把证书缓存到 /var/lib/derper/certs

确保:

  • derp.example.com 已解析到该 VPS。
  • TCP 80/443 可从公网直接访问。
  • 域名没有经过 CDN 代理。

方案 B:域名 + 外部证书

外部证书必须满足:

  • SAN 包含 derp.example.com
  • 由客户端系统信任的 CA 签发。私有 CA 需要在所有客户端安装根证书。
  • .crt 中包含服务器证书和中间证书,即 full chain,不能只复制 leaf certificate。
  • 私钥与证书匹配。

将外部证书复制为 derper 要求的固定文件名:

bash
sudo install -o root -g derper -m 0644 \
  /path/to/fullchain.pem \
  /var/lib/derper/certs/derp.example.com.crt

sudo install -o root -g derper -m 0640 \
  /path/to/privkey.pem \
  /var/lib/derper/certs/derp.example.com.key

--certmode=manual 会读取 <certdir>/<hostname>.crt<certdir>/<hostname>.key。证书续期后必须重新复制文件并重启 derper;只有 derper 内置 Let's Encrypt 模式会自动申请和轮换证书。

Cloudflare Origin CA 证书不适用于这个直连方案:它通常不被终端系统直接信任,而 DERP 也不应放在 Cloudflare HTTP 代理后。

方案 C:纯 IP + Let's Encrypt IP 证书

新版 derper 支持 --acme-ip-certs,可按需为 IPv4 或 IPv6 申请约 6 天有效期的 Let's Encrypt IP 证书并自动续期。该方案不需要 DNS。

要求:

  • 公网 IP 直接配置在 derper 主机网卡上。
  • Let's Encrypt 能直接访问该 IP 的 TCP 80 完成 HTTP-01。
  • 不经过 NAT、反向代理或负载均衡。

截至本文校对日期,derper 在“网卡只有私网 IP、公网 IP 通过 NAT 映射”的环境中,可能会错误地为私网 IP 申请证书。这种 VPS 应改用方案 A、B 或 D,进展见 Tailscale #20660

方案 D:纯 IP + 自签证书

无需手工运行 OpenSSL。在 --hostname 填公网 IP、--certmode=manual 且相应证书文件不存在时,derper 会自动:

  1. 生成 ECDSA P-256 私钥。
  2. 生成 SAN 包含该 IP、有效期 1 年的自签证书。
  3. 在日志中输出 sha256-raw:<fingerprint> 形式的 CertName

客户端会同时校验 IP SAN、证书有效期和 SHA-256 指纹,不需要 InsecureForTests。证书指纹锁定在 Tailscale v1.82 引入,但早期实现曾有问题;服务端和客户端都应使用当前稳定版。

5. 配置 systemd

5.1 选择证书模式

创建配置文件:

bash
sudoedit /etc/default/derper

根据所选方案,只写入下面四组中的一组。

方案 A

ini
DERPER_HOSTNAME=derp.example.com
DERPER_CERTMODE=letsencrypt
DERPER_HTTP_PORT=80
DERPER_EXTRA_ARGS=

方案 B

ini
DERPER_HOSTNAME=derp.example.com
DERPER_CERTMODE=manual
DERPER_HTTP_PORT=-1
DERPER_EXTRA_ARGS=

方案 C

ini
DERPER_HOSTNAME=203.0.113.10
DERPER_CERTMODE=letsencrypt
DERPER_HTTP_PORT=80
DERPER_EXTRA_ARGS=--acme-ip-certs

方案 D

ini
DERPER_HOSTNAME=203.0.113.10
DERPER_CERTMODE=manual
DERPER_HTTP_PORT=-1
DERPER_EXTRA_ARGS=

5.2 创建统一的 systemd 服务

bash
sudoedit /etc/systemd/system/derper.service

写入:

ini
[Unit]
Description=Tailscale DERP relay
Documentation=https://tailscale.com/docs/reference/derp-servers/custom-derp-servers
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=derper
Group=derper
EnvironmentFile=/etc/default/derper
ExecStart=/usr/local/bin/derper \
  --hostname=${DERPER_HOSTNAME} \
  --a=:443 \
  --http-port=${DERPER_HTTP_PORT} \
  --stun=true \
  --stun-port=3478 \
  --certmode=${DERPER_CERTMODE} \
  $DERPER_EXTRA_ARGS \
  --certdir=/var/lib/derper/certs \
  --c=/var/lib/derper/derper.key
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/derper
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

--hostname 是 TLS 证书的域名或 IP,不是监听地址。--a=:443 表示在所有本机地址的 TCP 443 上监听;--http-port=-1 则会完全禁用额外的 HTTP 监听。

启用服务:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now derper
sudo systemctl status derper --no-pager
sudo journalctl -u derper -n 100 --no-pager

首次启动失败时,优先检查 /etc/default/derper、DNS、端口和证书文件名,不要通过 InsecureForTests 规避 TLS 错误。

5.3 获取自签证书指纹

仅方案 D 需要此步。启动成功后,可直接从证书计算与 derper 日志相同的 CertName

bash
sudo openssl x509 \
  -in /var/lib/derper/certs/203.0.113.10.crt \
  -outform DER |
  sha256sum |
  awk '{print "sha256-raw:" $1}'

保存完整输出,稍后填入 derpMapCertName。也可从日志中查找 derper 自动输出的指纹。

6. 配置 Tailnet 策略

Tailscale 策略文件使用 HuJSON,支持注释和尾随逗号。将以下字段合并到现有顶层对象中,不要覆盖已有的 grants、ACL 或其他策略。

6.1 域名方案完整示例

方案 A/B 使用以下配置。注释中同时列出了当前 DERPMapDERPRegionDERPNode 的其他字段:

jsonc
// 国内运营商网络、多层 NAT 或对固定 UDP 41641 不友好时建议开启。
// 它改变的是客户端 WireGuard UDP 端口,不是 DERP 端口。
"randomizeClientPort": true,

"derpMap": {
  // HomeParams 是首选 DERP 选择参数。普通部署建议省略。
  // RegionScore 是区域 ID 到延迟系数的映射:
  // 0 < score < 1 会提高该区域优先级,score > 1 会降低优先级;
  // 未写时按 1.0 处理,小于等于 0 的值会被忽略。
  // 它只影响首选区域计算,不会篡改 netcheck 显示的实测 RTT。
  // "HomeParams": {
  //   "RegionScore": {
  //     "900": 0.5,
  //   },
  // },

  // false:保留 Tailscale 官方 DERP 作为备选;
  // true:只使用本 derpMap 中的自定义区域。一般不建议设为 true。
  "OmitDefaultRegions": false,

  // DERP 区域表,key 是字符串形式的 RegionID。
  // 也可将已有官方区域的值设为 null,单独禁用该区域。
  "Regions": {
    "900": {
      // 区域唯一 ID,必须与外层 key 和节点 RegionID 一致。
      // 900–999 专供用户自建 DERP。
      "RegionID": 900,

      // 区域短代码,通常使用城市、机场或自定义缩写。
      "RegionCode": "myderp",

      // 人类可读的英文区域名称。
      "RegionName": "My DERP",

      // 可选:区域所在城市的纬度和经度。
      // "Latitude": 31.2304,
      // "Longitude": 121.4737,

      // Avoid 已废弃,不要再使用。需要相应行为时使用 NoMeasureNoHome。
      // "Avoid": true,

      // true 时不测量该区域,也不将它选为 home DERP;
      // 仅在某个 peer 已声明它是 home DERP 时才允许连接。普通部署省略。
      // "NoMeasureNoHome": true,

      // 区域内节点按优先级排列。
      // 自建部署按官方要求每个区域只放一个 DERP 节点;
      // 需要高可用时,建立多个单节点区域。
      "Nodes": [
        {
          // 全部区域范围内唯一的节点名,不是域名。
          "Name": "900a",

          // 节点所属区域,必须与父级 RegionID 一致。
          "RegionID": 900,

          // 必填的节点域名或 IP。域名场景用于 TLS SNI;
          // 未指定 IPv4/IPv6 时,还用于 DNS 解析和 TCP 拨号。
          "HostName": "derp.example.com",

          // 可选的证书校验名。省略时使用 HostName。
          // 普通公信证书无需填;自签证书使用 sha256-raw:<64位指纹>。
          // "CertName": "cert.example.com",

          // 可选的固定公网 IPv4。填写后优先直接拨号,避免 DNS 故障。
          // 必须是公网可路由地址;填 "none" 可禁用 IPv4。
          "IPv4": "203.0.113.10",

          // 可选的固定公网 IPv6。省略时通过 HostName 查询 AAAA;填 "none" 禁用 IPv6。
          "IPv6": "none",

          // STUN UDP 端口。0 或省略表示 3478,-1 表示关闭该节点的 STUN。
          "STUNPort": 3478,

          // true 表示该节点只提供 STUN,不提供 DERP 中继。普通 DERP 节点省略。
          // "STUNOnly": true,

          // DERP HTTPS 端口。0 或省略表示 443。
          "DERPPort": 443,

          // 仅供单元测试:跳过 TLS 校验。用户绝对不应设置。
          // "InsecureForTests": true,

          // 仅供测试:覆盖 STUN 服务器 IP。用户不应设置。
          // "STUNTestIP": "192.0.2.1",

          // 表示该节点的 HTTP 80 可访问,供捕获门户检测使用。
          // 仅当 DERPER_HTTP_PORT=80 时设为 true;禁用 HTTP 时必须删除或设为 false。
          "CanPort80": true,
        },
      ],
    },
  },
},

方案 A 保留 CanPort80: true。方案 B 按本文默认的 DERPER_HTTP_PORT=-1 部署时,必须删除 CanPort80;如果希望保留捕获门户检测,可将 B 的端口改回 80 并放行 TCP 80。

6.2 纯 IP 公信证书的节点配置

方案 C 保留上面的 derpMap 外层结构,将 Nodes 中的节点替换为:

jsonc
{
  "Name": "900a",
  "RegionID": 900,
  "HostName": "203.0.113.10",
  "IPv4": "203.0.113.10",
  "IPv6": "none",
  "STUNPort": 3478,
  "DERPPort": 443,
  "CanPort80": true,
}

Let's Encrypt IP 证书由系统公信根验证,因此不要填 CertName

6.3 纯 IP 自签证书的节点配置

方案 D 的节点配置为:

jsonc
{
  "Name": "900a",
  "RegionID": 900,
  "HostName": "203.0.113.10",
  "CertName": "sha256-raw:REPLACE_WITH_THE_64_HEX_FINGERPRINT",
  "IPv4": "203.0.113.10",
  "IPv6": "none",
  "STUNPort": 3478,
  "DERPPort": 443,
}

必须将 CertName 替换为第 5.3 节实际计算出的完整值。证书更换后指纹也会改变,必须同步更新 Tailnet 策略。方案 D 默认禁用 HTTP,因此不填 CanPort80

7. randomizeClientPort 的国内网络建议

在国内运营商网络、CGNAT 或多层 NAT 下,如果疑似光猫/防火墙对固定 UDP 41641 处理异常,可将它作为优先尝试的兼容性选项:

jsonc
"randomizeClientPort": true,

它使客户端用随机 UDP 端口承载 WireGuard 流量,而不是固定使用 41641。这是针对特定防火墙/NAT 的 workaround,不是通用加速开关;官方也建议只在排障确认需要时使用。它不会改变 DERP 的 TCP 443 或 UDP 3478,也不能保证一定打洞成功。

不要在以下场景全局开启:

  • 客户端依赖 tailscaled --portPORT= 固定端口。
  • 路由器为某个固定 UDP 端口做了静态端口映射。
  • 现网已经可靠直连,且需要可预测的本地监听端口。

如果只想对特定设备开启,可用当前更精细的 nodeAttrs 方式,并删除全局 randomizeClientPort

jsonc
"nodeAttrs": [
  {
    // 替换为现有的 group、tag 或其他可用目标。
    "target": ["group:cn-network"],
    "attr": ["randomize-client-port"],
  },
],

8. 验证

8.1 服务和证书

方案 A/B 从 VPS 外部执行:

bash
curl --fail --show-error --silent \
  --output /dev/null --write-out '%{http_code}\n' \
  https://derp.example.com/

方案 C 使用公网 IP:

bash
curl --fail --show-error --silent \
  --output /dev/null --write-out '%{http_code}\n' \
  https://203.0.113.10/

应输出 200。方案 D 的证书不在系统信任链中,普通 curl 报“unknown authority”是预期行为;不要用 curl -k 作为配置正确的证明。

检查服务端监听和日志:

bash
sudo ss -lntup
sudo journalctl -u derper -n 100 --no-pager

8.2 Tailscale 客户端

在已登录该 tailnet 的最新版客户端执行:

bash
tailscale version
tailscale netcheck
tailscale debug derp 900

结果中应出现自定义区域,HTTPS、DERP 和 STUN 测试应成功。方案 D 应由 tailscale debug derp 900CertName 完成指纹验证。

即使 DERP 可用,节点之间仍可能升级为直连;这是预期行为,不代表自建 DERP 失效。

9. 访问限制、续期与更新

限制其他 tailnet 使用

默认情况下,知道服务器地址的其他人也可以将它加入自己的 DERP Map。如果需要只允许本 tailnet,服务器必须同时运行已加入目标 tailnet 的 tailscaled,且 derper 与 tailscaled 应由同一 Tailscale 源码版本构建,然后才能在 DERPER_EXTRA_ARGS 中增加 --verify-clients

方案 C 同时启用时,写为:

ini
DERPER_EXTRA_ARGS="--acme-ip-certs --verify-clients"

未满足本地 tailscaled 和版本一致性要求时,不要启用 --verify-clients

证书续期

  • 方案 A/C:derper 自动续期。
  • 方案 B:由外部证书工具续期;续期后更新 .crt/.key 并执行 sudo systemctl restart derper
  • 方案 D:自签证书有效期 1 年,需手动轮换并同步更新 CertName 指纹。不要直接覆盖老证书而不更新策略。

更新 derper

derper 应与 Tailscale 节点以相近频率更新:

bash
go install tailscale.com/cmd/derper@latest
sudo systemctl stop derper
sudo install -m 0755 "$(go env GOPATH)/bin/derper" /usr/local/bin/derper
sudo systemctl start derper
sudo systemctl status derper --no-pager

systemd 的 Restart=on-failure 已负责异常退出后重启,无需再配置 cron 定时重启。如已启用 --verify-clients,更新时必须保持 derper 与 tailscaled 的源码版本一致。

参考资料