自建 DERP 中继:让 Headscale 打洞失败也能稳连国内 20ms

J
Joy
2026年06月16日 · 3 分钟阅读

Tailscale/Headscale 靠 WireGuard P2P 直连,但很多 NAT 环境打洞会失败、流量被迫走官方境外 DERP,延迟动辄 100ms+。这篇记录我在自建 Headscale 上加一台国内自建 DERP 的全过程,含改源码、自签证书、防白嫖三处踩坑。

系列:Tailscale 折腾 3 / 4
  1. 1 Tailscale 是什么:一篇看懂这套 P2P 内网穿透神器(折腾系列·序章)
  2. 2 给 Headscale 配个 Web 界面:headscale-ui 安装折腾记
  3. 3 自建 DERP 中继:让 Headscale 打洞失败也能稳连国内 20ms 当前
  4. 4 把设备接进自建 Headscale:从 tailscale up 登录到 headscale-ui 管理

折腾背景:我用 Headscale 自建了一套私有组网,大部分设备能 WireGuard P2P 直连。但有几台在对称型 NAT / 运营商大内网后面的机器始终打洞失败,流量只能走中继——而 Tailscale 官方的 DERP 中继节点都在境外,延迟动辄 100ms 起步,看着就难受。于是自己在国内搭了一台 DERP,延迟直接压到 20ms 级。这篇把全过程连同三个坑一起记下来。

先搞清楚:DERP 到底在干嘛

WireGuard 组网的理想状态是两台设备点对点直连(P2P)。但现实里大量设备藏在 NAT 后面,打洞(NAT 穿透)并不总能成功。一旦直连建立不起来,流量就得找个公网中继转一道——这个中继就是 DERP(Designated Encrypted Relay for Packets)。

关键点:DERP 只在直连失败时兜底,直连成功时它不参与转发。所以自建 DERP 不是为了取代直连,而是为了让"兜底"那条路也别太慢。

flowchart LR
    A["设备 A
NAT 后"] B["设备 B
NAT 后"] D["自建 DERP
公网中继"] A -. "① 优先尝试 P2P 直连" .-> B A == "② 打洞失败 → 走中继" ==> D D == "② 加密转发" ==> B style D fill:#dbe9ff,stroke:#2563eb,stroke-width:2px

全流程总览

整套折腾就八步,先看地图再动手:

flowchart TD
    S1["① 装 Go 环境"] --> S2["② go install derper"]
    S2 --> S3["③ 改源码 cert.go
关闭 hostname 校验"] S3 --> S4["④ 编译 + 自签 SSL 证书"] S4 --> S5["⑤ 写 systemd 服务
开机自启"] S5 --> S6["⑥ Headscale 注册
DERP region"] S6 --> S7["⑦ netcheck 验证
延迟 & 命中"] S7 --> S8["⑧ 安全加固
--verify-clients 防白嫖"] style S3 fill:#ffe9d5,stroke:#b71d18 style S8 fill:#ffe9d5,stroke:#b71d18

标橙的两步(③改源码、⑧防白嫖)是最容易翻车 / 最容易被忽略的,下面会重点说。

1. 装 Go 环境

DERP 服务(derper)得自己用 Go 编译,先把 Go 装好:

# 下载(按需换最新版本号)
wget https://golang.google.cn/dl/go1.23.2.linux-amd64.tar.gz
# 安装
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.23.2.linux-amd64.tar.gz
# 配环境变量(建议同时写进 /etc/profile 持久化)
export PATH=$PATH:/usr/local/go/bin
echo 'export PATH=$PATH:/usr/local/go/bin' >> /etc/profile && source /etc/profile
# 开启 module + 国内代理(不然 go install 慢到怀疑人生)
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
# 验证
go version

2. 安装 derper

go install tailscale.com/cmd/derper@latest

装完后源码会落在 ~/go/pkg/mod/tailscale.com@<版本号>/ 里,版本号每次都不一样,下一步要进这个目录,记得先 ls 看看自己实际是哪个版本。

3. 改源码 cert.go —— 第一个坑

默认的 derpermanual 证书模式下,会强制校验 TLS 握手里的 ServerName 必须等于启动时指定的 hostname。我们用的是自签证书,这个校验会让客户端连不上。解决办法是把校验那段注释掉。

# 进目录(版本号换成你 ls 看到的真实值)
cd ~/go/pkg/mod/tailscale.com@v1.76.3/cmd/derper/
sudo chmod 777 ./cert.go
sudo vim cert.go

找到 getCertificate 方法,把开头的 hostname 校验注释掉:

func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
	// ↓↓↓ 注释掉这三行,放行自签证书 ↓↓↓
	// if hi.ServerName != m.hostname {
	// 	return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName)
	// }

	certCopy := new(tls.Certificate)
	*certCopy = *m.cert
	certCopy.Certificate = certCopy.Certificate[:len(certCopy.Certificate):len(certCopy.Certificate)]
	return certCopy, nil
}

保存退出。

踩坑提示:这是改 Go module 缓存里的源码,所以一旦 go install 升级了版本,这个改动会丢,得重新改一遍。升级 derper 后连不上,先回来看这里。

4. 编译 + 自签证书

# 编译到固定目录
go build -o /etc/derp/derper

# 自签 SSL 证书(derp.e7coding.com 换成你自己的域名/标识)
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
  -keyout /etc/derp/derp.e7coding.com.key \
  -out /etc/derp/derp.e7coding.com.crt \
  -subj "/CN=derp.e7coding.com" \
  -addext "subjectAltName=DNS:derp.e7coding.com"

5. 写 systemd 服务

让 derper 开机自启、崩了自动拉起:

sudo vim /etc/systemd/system/derp.service

内容(hostname 和证书名要对得上):

[Unit]
Description=HW Derper
After=network.target
Wants=network.target

[Service]
User=root
Restart=always
ExecStart=/etc/derp/derper -hostname derp.e7coding.com -a :33445 -http-port 33446 -certmode manual -certdir /etc/derp
RestartPreventExitStatus=1

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl start derp
sudo systemctl status derp

参数速记:-a :33445 是 DERP 的 HTTPS 监听端口,-http-port 33446 是健康检查用的 HTTP 端口,-certmode manual -certdir /etc/derp 指向刚才的自签证书。

6. 在 Headscale 注册这台 DERP

回到 Headscale 服务器,编辑(或新建)DERP 配置:

vim /etc/headscale/derp.yaml
regions:
  900:
    regionid: 900
    regioncode: custom
    regionname: custom shanghai
    nodes:
      - name: 900a
        regionid: 900
        hostname: <DERP 服务器公网 IP>
        ipv4: <DERP 服务器公网 IP>
        ipv6: <DERP 服务器 IPv6,没有可删掉这行>
        derpport: 33445
        insecurefortests: true

insecurefortests: true 是因为我们用的是自签证书,得让 Headscale 别去做严格证书校验。确认 headscale config.yamlderp.paths 引用了这个 derp.yaml,然后重启:

systemctl restart headscale
systemctl status headscale

7. 验证:延迟到底压下来没

在任意一台已入网的客户端跑 netcheck(这是 Mac 的路径,Linux 直接 tailscale netcheck):

/Applications/Tailscale.app/Contents/MacOS/Tailscale netcheck

看到自建节点出现在列表、且延迟明显低于境外节点,就成了:

Report:
	* UDP: true
	* Nearest DERP: HW shanghai
	* DERP latency:
		-  hw: 23.8ms  (custom shanghai)   ← 自建,20ms 级
		-  hb: 43.3ms  (Huawei Beijing)
		-  hk: 46.7ms  (Hang kong)

到这里,自建 DERP 已经能用了。但先别急着收工——默认状态下它是裸奔的。

8. 安全加固:–verify-clients 防白嫖

默认配置下,任何人只要拿到你 DERP 的地址和端口,就能把它当中继免费用。地址一旦泄露,等于给别人当免费流量跳板。

解法:让 DERP 只服务"自己组网内的节点"。做法是给 DERP 服务器也装一个 Tailscale 客户端并登录进自己的 Headscale,再给 derper 加 --verify-clients 参数——这样 derper 会通过本机的 tailscaled 校验来访者是不是自己人。

flowchart TD
    X["陌生人拿到
DERP 地址+端口"] -->|"--verify-clients 开启"| Y{"是本组网节点?"} Y -->|"否"| Z["拒绝中继"] Y -->|"是"| W["正常转发"] style Z fill:#ffe9d5,stroke:#b71d18 style W fill:#dbe9ff,stroke:#2563eb

第一步,DERP 服务器上装 Tailscale 并登录自己的 Headscale:

# 安装
curl -fsSL https://tailscale.com/install.sh | sh
# 登录到自建 Headscale
tailscale up --login-server=<你的 Headscale 地址>
# 终端会输出一个 register 链接,复制到浏览器(或在 headscale-ui 里)把这台节点加入组网

把这台 DERP 节点在 Headscale / headscale-ui 里批准入网后,第二步给 service 加上 --verify-clients

ExecStart=/etc/derp/derper -hostname derp.e7coding.com -a :33445 -http-port 33446 -certmode manual -certdir /etc/derp --verify-clients
sudo systemctl daemon-reload
sudo systemctl restart derp
sudo systemctl status derp

现在这台 DERP 只给自己人用了,白嫖者会被直接拒掉。完毕!

踩坑小结

  • 改源码会随版本丢失cert.go 的改动在 Go module 缓存里,go install 升级版本后要重改。升级后连不上先查这里。
  • 目录版本号别照抄:源码路径里的 tailscale.com@v1.76.3 每次安装可能不同,进目录前先 ls 确认。
  • --verify-clients 依赖本机 tailscaled:DERP 机器自己得先成功 tailscale up 入网,否则加了这个参数 derper 会校验失败、谁都连不上。
  • insecurefortests: true 别漏:自签证书场景下 Headscale 侧不加这行会校验失败。
  • 端口放行:云服务器安全组 / 防火墙记得放行 33445(和你用的健康检查端口)。

控制面、Web 界面、国内中继都齐了,最后一篇把日常那一环补上 👉 把设备接入自建 Headscale:tailscale up 登录 + 管设备

分享

评论

相关文章

2 分钟阅读
给 Headscale 配个 Web 界面:headscale-ui 安装折腾记

Headscale 原生只有 CLI,每次加节点、批用户、看路由都得 SSH 敲命令。headscale-ui 是个纯静态 Web 前端,调 Headscale API 把这些操作搬到浏览器里。这篇记录它的安装、反代配置和登录踩坑。

文章 折腾