Tailscale/Headscale 靠 WireGuard P2P 直连,但很多 NAT 环境打洞会失败、流量被迫走官方境外 DERP,延迟动辄 100ms+。这篇记录我在自建 Headscale 上加一台国内自建 DERP 的全过程,含改源码、自签证书、防白嫖三处踩坑。
系列:Tailscale 折腾 3 / 4
- 1 Tailscale 是什么:一篇看懂这套 P2P 内网穿透神器(折腾系列·序章)
- 2 给 Headscale 配个 Web 界面:headscale-ui 安装折腾记
- 3 自建 DERP 中继:让 Headscale 打洞失败也能稳连国内 20ms 当前
- 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 version2. 安装 derper
go install tailscale.com/cmd/derper@latest装完后源码会落在 ~/go/pkg/mod/tailscale.com@<版本号>/ 里,版本号每次都不一样,下一步要进这个目录,记得先 ls 看看自己实际是哪个版本。
3. 改源码 cert.go —— 第一个坑
默认的 derper 在 manual 证书模式下,会强制校验 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.targetsudo 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.yamlregions:
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.yaml里derp.paths引用了这个derp.yaml,然后重启:
systemctl restart headscale
systemctl status headscale7. 验证:延迟到底压下来没
在任意一台已入网的客户端跑 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-clientssudo 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 登录 + 管设备。