← 全部文章

自建 DERP 中继:Tailscale 与 Headscale 教程

在外面用 Roamtty 连家里或公司的服务器时,Tailscale 会先尝试让手机和服务器直接连通;直连失败,流量就改由 DERP 中继转发。官方 DERP 节点都在海外,在中国大陆延迟常达 200–500 ms,打一个字要等半秒才回显。在离你近的服务器上自建 DERP,中继延迟可以降到几十毫秒。

本文先部署官方的 derper,再分别接入 Tailscale 官方服务和自建 Headscale。命令按写作时的最新版本核对:Tailscale 1.104、Headscale 0.29.5。

DERP 是什么

  • Tailscale 设备之间优先用 UDP 直连(NAT 打洞)。两端都在运营商级 NAT、对称 NAT 或严格的防火墙后面时,打洞会失败,手机用蜂窝网络时很常见。
  • 这时流量经 DERP 中继转发。DERP 走 HTTPS,只转发 WireGuard 加密后的数据包,中继看不到内容,也无法冒充你的设备。
  • 同一个 derper 进程还提供 STUN(UDP 3478),帮设备发现自己的公网地址,近处的 STUN 也有助于直连成功。

判断当前是否走中继:在服务器上运行 tailscale status,对端那一行显示 relay "tok" 表示经东京中继,显示 direct 198.51.100.7:41641 则是直连。

选择方案

你在用做法
Tailscale 官方账号部署 derper,再在策略文件中添加 derpMap
自建 Headscale,中继放在同一台 VPS开启 Headscale 内置 DERP,不用另装 derper
自建 Headscale,中继放在另一台服务器部署 derper,再写入 derp.yaml

准备工作

  • 一台有公网 IP 的服务器,网络上离你常去的地方近(国内云厂商或香港)。1 核 1 GB 足够,SSH 和 mosh 的流量很小。不要放在负载均衡、反向代理或 CDN 后面。
  • 一个域名,A 记录指向这台服务器,下文用 derp.example.com、公网 IP 用 203.0.113.10 代替。DNS 托管在 Cloudflare 时设为“仅 DNS”(灰色云),DERP 经 CDN 代理会连不上。没有域名见直接用 IP。
  • 放行端口:TCP 443(DERP)、TCP 80(申请证书和网络检测)、UDP 3478(STUN),并允许 ICMP。云厂商的安全组和系统防火墙都要放行。
# Ubuntu / Debian 用 ufw 时
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 3478/udp

中国大陆的服务器:未备案的域名访问 80 / 443 端口会被云厂商拦截,请先看国内服务器一节,改用高位端口。

部署 derper

接入 Tailscale 官方服务和 Headscale 外置中继都用这一节,以 Ubuntu / Debian 为例。

1. 编译安装

derper 没有预编译包,用 Go 编译。ARM 服务器把下面的 amd64 换成 arm64;在国内把 go.dev 换成 golang.google.cn,并先设置 export GOPROXY=https://goproxy.cn,direct。

curl -fsSL https://go.dev/dl/go1.27.2.linux-amd64.tar.gz | sudo tar -C /usr/local -xz
export PATH=$PATH:/usr/local/go/bin
go install tailscale.com/cmd/derper@latest
sudo install -m 755 ~/go/bin/derper /usr/local/bin/derper

Tailscale 客户端升级后偶尔需要新版 derper,每隔几个月重新运行后两条命令并重启服务即可。

2. 用 systemd 运行

sudo tee /etc/systemd/system/derper.service > /dev/null <<'EOF'
[Unit]
Description=Tailscale DERP server
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/derper -hostname derp.example.com -a :443 -http-port 80 -stun-port 3478 -certdir /var/lib/derper/certs
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now derper

默认的证书模式会在第一次 HTTPS 访问时向 Let's Encrypt 申请证书并自动续期,所以域名解析要已生效,80、443 端口要能从公网访问。验证:

curl https://derp.example.com

返回的 HTML 里有 This is a Tailscale DERP server 即部署成功。出错时用 journalctl -u derper -f 查看日志。

3. 只允许自己的设备使用

默认任何人知道你的域名,都能把它加进自己的 DERP 列表,占用你的带宽。按接入方式二选一,在 ExecStart 那一行末尾追加参数:

  • Tailscale 官方:先在这台服务器上安装 Tailscale 并登录同一个账号(curl -fsSL https://tailscale.com/install.sh | sh,再 sudo tailscale up),然后追加 -verify-clients。derper 通过本机的 tailscaled 判断连接者是否属于你的 tailnet。在管理后台为这台机器关闭密钥过期(Disable key expiry),否则过期后所有中继请求都会被拒绝。
  • Headscale:追加 -verify-client-url=https://hs.example.com/verify -verify-client-url-fail-open=false。有设备接入时 derper 询问 Headscale,只放行在 Headscale 登记过的设备;第二个参数让 Headscale 暂时不可达时拒绝连接,默认是放行。
sudo systemctl daemon-reload && sudo systemctl restart derper

接入 Tailscale 官方服务

打开 Tailscale 管理后台的 Access controls,切换到 JSON 编辑器,在策略文件最外层加入 derpMap:

{
  // 原有的 grants、acls、tagOwners 等保持不变
  "derpMap": {
    "Regions": {
      "900": {
        "RegionID": 900,
        "RegionCode": "myderp",
        "RegionName": "My DERP",
        "Nodes": [
          {
            "Name": "900a",
            "RegionID": 900,
            "HostName": "derp.example.com",
            "IPv4": "203.0.113.10"
          }
        ]
      }
    }
  }
}
  • 区域 ID 900–999 留给自建节点。每个区域放一台服务器,有多台就分别建 900、901……
  • IPv4 可选,建议填写,DNS 出问题时设备仍能连上中继。
  • 端口不是 443 时在节点里加 "DERPPort": 33445,STUN 端口不是 3478 时加 "STUNPort"。
  • 官方节点会保留作为后备,设备自动选延迟最低的区域。只想用自己的节点时,在 derpMap 中加 "OmitDefaultRegions": true;但这台服务器一旦故障,无法直连的设备就会断开。

保存后新的 DERP 列表会立即下发到所有设备,不用重启客户端。

Headscale:开启内置 DERP

Headscale 自带 DERP 服务器,使用 server_url 的域名、端口和证书,不用另装 derper。编辑 /etc/headscale/config.yaml:

derp:
  server:
    enabled: true
    region_id: 999
    region_code: "headscale"
    region_name: "Headscale Embedded DERP"
    verify_clients: true              # 只允许本 Headscale 的设备使用
    stun_listen_addr: "0.0.0.0:3478"
    private_key_path: /var/lib/headscale/derp_server_private.key
    automatically_add_embedded_derp_region: true
    ipv4: 203.0.113.10                # VPS 的公网 IP
  urls:
    - https://controlplane.tailscale.com/derpmap/default   # 保留官方节点作后备
  paths: []
  auto_update_enabled: true
  update_frequency: 3h
sudo ufw allow 3478/udp
sudo systemctl restart headscale
  • server_url 必须是 https。用 Caddy 反向代理时不用改;用 nginx 时要转发 Upgrade 和 Connection 请求头。
  • VPS 访问不了 controlplane.tailscale.com 时,Headscale 会因下载不到官方 DERP 列表而启动失败,此时把 urls 改成 [],只用自建节点。
  • 内置 DERP 没有做吞吐优化,自用足够;中继流量大时改用独立的 derper。

Headscale:使用独立 derper

中继放在另一台离你更近的服务器上时,先按部署 derper装好(客户端验证用 -verify-client-url),再在 Headscale 所在的 VPS 上新建 /etc/headscale/derp.yaml,键名全部小写:

regions:
  900:
    regionid: 900
    regioncode: myderp
    regionname: My DERP
    nodes:
      - name: 900a
        regionid: 900
        hostname: derp.example.com
        ipv4: 203.0.113.10
        stunport: 3478
        derpport: 443

在 config.yaml 中引用它:

derp:
  server:
    enabled: false
  urls:
    - https://controlplane.tailscale.com/derpmap/default
  paths:
    - /etc/headscale/derp.yaml
  auto_update_enabled: true
  update_frequency: 3h

运行 sudo systemctl restart headscale 后生效。内置 DERP 和独立 derper 也可以同时用:保持 server.enabled: true,再在 paths 中加上 derp.yaml。

验证

在任意一台已加入网络的电脑或服务器上运行:

tailscale netcheck

DERP 延迟列表里应出现你的节点,并且延迟最低,输出类似:

* Nearest DERP: My DERP
* DERP latency:
    - myderp: 12.3ms  (My DERP)
    - hkg: 48.9ms  (Hong Kong)
    - tok: 71.4ms  (Tokyo)

Headscale 内置 DERP 的区域代码是 headscale。还可以检查连通性,并看到对端的实际路径:

tailscale debug derp myderp      # 检查与该区域的 TLS 和 STUN
tailscale ping 100.64.0.2        # 对端的 Tailscale IP

tailscale ping 先显示 via DERP(myderp),打洞成功后变成 via 198.51.100.7:41641 直连。Roamtty 里不用做任何设置,照常用服务器的 100.x.y.z 地址连接。

国内服务器

中国大陆的云服务器上,未备案域名访问 80 / 443 会被拦截,Let's Encrypt 的验证也随之失败。做法是换一个高位端口(下例 33445),并用 DNS 验证申请证书。

1. 用 DNS 验证申请证书

以 DNS 托管在 Cloudflare 为例,用 acme.sh 申请(API 令牌需要该域名的 DNS 编辑权限;其他 DNS 服务商见 acme.sh 文档),在 root 下运行:

curl https://get.acme.sh | sh -s email=you@example.com
export CF_Token=<Cloudflare API 令牌>
~/.acme.sh/acme.sh --issue --server letsencrypt --dns dns_cf -d derp.example.com
mkdir -p /etc/derper/certs
~/.acme.sh/acme.sh --install-cert -d derp.example.com \
  --key-file /etc/derper/certs/derp.example.com.key \
  --fullchain-file /etc/derper/certs/derp.example.com.crt \
  --reloadcmd "systemctl restart derper"

证书文件名必须是 <域名>.crt 和 <域名>.key。acme.sh 会自动续期,并在续期后重启 derper 加载新证书。

2. 改用高位端口

把 derper.service 的 ExecStart 改为下面这行,关闭 80 端口,从目录读取证书;安全组放行 TCP 33445 和 UDP 3478:

ExecStart=/usr/local/bin/derper -hostname derp.example.com -a :33445 -http-port -1 -stun-port 3478 -certmode manual -certdir /etc/derper/certs

DERP 列表里同步改端口:Tailscale 策略文件的节点加 "DERPPort": 33445,Headscale 的 derp.yaml 写 derpport: 33445。

没有域名:直接用 IP

-hostname 填公网 IP 并使用 -certmode manual 时,derper 会自动生成自签名证书,并在日志里打印一段带 CertName 的节点配置:

ExecStart=/usr/local/bin/derper -hostname 203.0.113.10 -a :33445 -http-port -1 -stun-port 3478 -certmode manual -certdir /etc/derper/certs
sudo journalctl -u derper | grep CertName

把日志中的 "CertName": "sha256-raw:…" 加到节点配置里(derp.yaml 中写作 certname),HostName 填 IP。客户端按证书指纹校验,不需要域名和 CA 证书,但所有设备的 Tailscale 都要是 1.82 或更新的版本。

常见问题

现象处理
netcheck 里没有自己的节点Tailscale:确认策略文件已保存。Headscale:确认已重启,运行 tailscale debug derp-map 查看是否有该区域,derp.yaml 键名是否小写
有节点但没有延迟数值DERP 端口不通或证书无效:在另一台机器上运行 curl -v https://derp.example.com(自定义端口加上 :33445),检查安全组
Let's Encrypt 证书申请失败域名未解析到本机、80 / 443 不通,或开了 Cloudflare 代理(橙色云);国内服务器见上一节
加了 -verify-clients 后都连不上这台服务器上的 Tailscale 未登录、已过期或不在同一个 tailnet,运行 tailscale status 检查
Headscale 启动失败,日志提到 DERPMapVPS 访问不了 controlplane.tailscale.com,把 urls 设为 []
走自建中继后延迟仍高中继延迟等于手机到中继再到服务器的往返,选两边都近的机房;给服务器放行 UDP 41641 有助于直连

Tailscale 与 Headscale 的基本配置见使用文档的从外网连接一节。