How to?

tailscale 自建 derp server 中继

Sep 28, 2026
2609
6 Minutes
1038 Words

by: deepseek 41 flash. verified ok.

see: https://ameow.xyz/archives/tailscale-derp-server-deployment

阿里云自建 Tailscale DERP 中继

服务器 (Ubuntu 22.04) · 区域 900 / gz / Guangzhou · 端口 TCP 13477(DERP)、UDP 13478(STUN)

最终效果:tailscale netcheck → Nearest DERP: Guangzhou (17.7ms),全 tailnet 客户端自动选它作为 home DERP。


1. 服务端:修复 DNS(前置条件)

阿里云这台机器 DNS 完全不可用:127.0.0.53 存根本身超时,DHCP 下发的 100.100.2.136/138 也不通 —— 表现为任何域名都解析不了,与 Docker/镜像源无关。

/etc/netplan/50-cloud-init.yaml
1
network:
2
version: 2
3
ethernets:
4
eth0:
5
dhcp4: true
6
dhcp4-overrides: { use-dns: false }
7
nameservers: { addresses: [223.5.5.5, 119.29.29.29] }
8
9
# /etc/systemd/resolved.conf
10
[Resolve]
11
DNS=223.5.5.5 119.29.29.29
12
FallbackDNS=223.6.6.6 180.76.76.76
13
14
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg —— 阻止 cloud-init 回滚
1 collapsed line
15
network: {config: disabled}
Terminal window
1
sudo netplan generate && sudo netplan apply
2
sudo systemctl restart systemd-resolved

⚠️ 注意 registry-1.docker.io 在国内被投毒到 2a03:2880:...:face:b00c::(Facebook 段),Docker Hub 必须走镜像源。

2. 服务端:Docker 镜像源

/etc/docker/daemon.json
1
{ "registry-mirrors": [
2
"https://docker.m.daocloud.io",
3
"https://docker.1ms.run",
4
"https://docker.xuanyuan.me" ] }
Terminal window
1
sudo systemctl restart docker

3. 服务端:启动 derper

ghcr.io 在国内直连不稳,用南大镜像站拉取后重打 tag:

Terminal window
1
docker pull ghcr.nju.edu.cn/yangchuansheng/ip_derper:latest
2
docker tag ghcr.nju.edu.cn/yangchuansheng/ip_derper:latest \
3
ghcr.io/yangchuansheng/ip_derper:latest

~/services/tailscale/docker-compose.yml

1
services:
2
derper:
3
image: ghcr.io/yangchuansheng/ip_derper
4
container_name: derper
5
restart: always
6
environment:
7
- DERP_ADDR=:13477
8
- DERP_VERIFY_CLIENTS=true # 走宿主 tailscaled 校验客户端身份
9
ports:
10
- "13477:13477"
11
- "13478:3478/udp"
12
volumes:
13
- /var/run/tailscale:/var/run/tailscale # 挂目录,不是单个 socket 文件
Terminal window
1
cd ~/services/tailscale && docker compose up -d

本机是 tailnet 节点(DERP 校验身份需要宿主跑着 tailscaled 并已登录)。

4. 服务端:修 tailscaled 重启导致容器失联的坑

根因:systemd 的 RuntimeDirectory=tailscale 会在 tailscaled 每次重启时删除并重建整个 /run/tailscale 目录。容器里无论挂单文件还是挂目录,bind mount 都会指向被删除的旧 inode → dial unix ...: connection refused → derper 把所有客户端 rejected → DERP 连上就 EOF。

/etc/systemd/system/tailscaled.service.d/derper.conf
1
sudo mkdir -p /etc/systemd/system/tailscaled.service.d
2
[Service]
3
RuntimeDirectoryPreserve=yes # systemd 永不删除该目录,容器无需重启即可看到新 socket
Terminal window
1
sudo systemctl daemon-reload && sudo systemctl restart tailscaled

5. 服务端:安全组

阿里云 ECS → 安全组 → 入方向,两条规则(源 0.0.0.0/0):

协议端口
自定义 TCP13477/13477
自定义 UDP13478/13478

主机侧 ufw/firewalld 均 inactive,无需额外配置。

服务器上 curl 自己的公网 IP 会失败,是阿里云 1:1 NAT 不支持回环,正常现象。


6. Tailscale ACL

https://login.tailscale.com/admin/acls/file(可视化 Policies 页不显示 derpMap),加一个与 grants/acls 同级的顶层键:

1
"derpMap": {
2
"OmitDefaultRegions": false,
3
"Regions": {
4
"900": {
5
"RegionID": 900,
6
"RegionCode": "gz",
7
"RegionName": "Guangzhou",
8
"Nodes": [{
9
"Name": "aliyun-derper",
10
"RegionID": 900,
11
"HostName": "47.103.61.134",
12
"IPv4": "47.103.61.134",
13
"DERPPort": 13477,
14
"STUNPort": 13478,
15
"CanPort80": false,
5 collapsed lines
16
"InsecureForTests": true
17
}]
18
}
19
}
20
}

各字段的必要性:

字段为什么
STUNPort不能省。省略时 netcheck 默认探 3478(未开放)→ 测不到延迟 → 区域永不被选中
InsecureForTests容器自签证书 CN/SAN 是 127.0.0.1,靠它跳过域名校验
IPv4让客户端直接连 IPv4,避免 DNS 解析

保存后客户端需重新拉取 netmap(退出重开 Tailscale.app)。


7. 验证

Terminal window
1
# 服务端
2
sudo tailscale debug derp gz # 应显示 Region 900 == "gz"
3
tailscale netcheck # 出现 gz 及低延迟
4
curl -k -i -N -H "Connection: Upgrade" -H "Upgrade: DERP" \
5
https://47.103.61.134:13477/derp # 期望 HTTP/1.1 101 Switching Protocols
6
7
# 客户端
8
tailscale ping <peer> # 期望 pong ... via DERP(gz)
9
tailscale status # peer 后显示 relay "gz"

8. 三个「假警报」,看到不用管

  1. tailscale debug derp 报 x509: certificate signed by unknown authority 该命令自身的 bug:ipn/localapi/debugderp.go 的 tlsConfigForNode() 漏了 InsecureForTests(只处理 CertName)。 真正建连的 derphttp.tlsClient() 是认这个字段的,中继不受影响。

  2. is port 80 blocked? CanPort80: false 且安全组本来就没开 80,正常。

  3. 标准 STUN 探包无响应 Tailscale 的 derper STUN 用私有方言:只回应带 SOFTWARE="tailnode" 且以合法 FINGERPRINT(crc32 ^ 0x5354554e)结尾的请求,其余静默丢弃(ErrWrongSoftware)。 用 stun.miwifi.com 之类公网 STUN 能通、用你的 derper 不通,不代表 derper 坏了。

    1
    import struct, os, zlib
    2
    software = b"tailnode"
    3
    tid = os.urandom(12)
    4
    attr = struct.pack('>HH', 0x8022, len(software)) + software
    5
    msg = struct.pack('>HH', 0x0001, len(attr) + 8) + b'\x21\x12\xa4\x42' + tid + attr
    6
    msg += struct.pack('>HHI', 0x8028, 4, zlib.crc32(msg) ^ 0x5354554e)

9. 已知限制

  • 无法强制「只走 DERP」:Tailscale 始终优先点对点直连,DERP 只是兜底。tailscale set/up 没有相关开关。
  • DERP 区域选取是自动的:由 netcheck 挑延迟最低的区域,无需手动指定。
  • STUN 是区域被选中的必要条件(不是可选项):UDP 可用时,netcheck 只用 STUN 测延迟;HTTPS/ICMP 兜底仅在所有 STUN 探测都失败时才触发。

10. 日常运维

Terminal window
1
cd ~/services/tailscale
2
docker compose ps / logs -f derper / restart / up -d
项值
容器restart: always
dockersystemctl enable 已开
tailscaled 重启derper 无需重启(RuntimeDirectoryPreserve=yes)
Article title:tailscale 自建 derp server 中继
Article author:Julyfun
Release time:Sep 28, 2026
Copyright 2026
Sitemap