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/镜像源无关。
1network:2 version: 23 ethernets:4 eth0:5 dhcp4: true6 dhcp4-overrides: { use-dns: false }7 nameservers: { addresses: [223.5.5.5, 119.29.29.29] }8
9# /etc/systemd/resolved.conf10[Resolve]11DNS=223.5.5.5 119.29.29.2912FallbackDNS=223.6.6.6 180.76.76.7613
14# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg —— 阻止 cloud-init 回滚1 collapsed line
15network: {config: disabled}1sudo netplan generate && sudo netplan apply2sudo systemctl restart systemd-resolved⚠️ 注意
registry-1.docker.io在国内被投毒到2a03:2880:...:face:b00c::(Facebook 段),Docker Hub 必须走镜像源。
2. 服务端:Docker 镜像源
1{ "registry-mirrors": [2 "https://docker.m.daocloud.io",3 "https://docker.1ms.run",4 "https://docker.xuanyuan.me" ] }1sudo systemctl restart docker3. 服务端:启动 derper
ghcr.io 在国内直连不稳,用南大镜像站拉取后重打 tag:
1docker pull ghcr.nju.edu.cn/yangchuansheng/ip_derper:latest2docker tag ghcr.nju.edu.cn/yangchuansheng/ip_derper:latest \3 ghcr.io/yangchuansheng/ip_derper:latest~/services/tailscale/docker-compose.yml
1services:2 derper:3 image: ghcr.io/yangchuansheng/ip_derper4 container_name: derper5 restart: always6 environment:7 - DERP_ADDR=:134778 - DERP_VERIFY_CLIENTS=true # 走宿主 tailscaled 校验客户端身份9 ports:10 - "13477:13477"11 - "13478:3478/udp"12 volumes:13 - /var/run/tailscale:/var/run/tailscale # 挂目录,不是单个 socket 文件1cd ~/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。
1sudo mkdir -p /etc/systemd/system/tailscaled.service.d2[Service]3RuntimeDirectoryPreserve=yes # systemd 永不删除该目录,容器无需重启即可看到新 socket1sudo systemctl daemon-reload && sudo systemctl restart tailscaled5. 服务端:安全组
阿里云 ECS → 安全组 → 入方向,两条规则(源 0.0.0.0/0):
| 协议 | 端口 |
|---|---|
| 自定义 TCP | 13477/13477 |
| 自定义 UDP | 13478/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": true17 }]18 }19 }20}各字段的必要性:
| 字段 | 为什么 |
|---|---|
STUNPort | 不能省。省略时 netcheck 默认探 3478(未开放)→ 测不到延迟 → 区域永不被选中 |
InsecureForTests | 容器自签证书 CN/SAN 是 127.0.0.1,靠它跳过域名校验 |
IPv4 | 让客户端直接连 IPv4,避免 DNS 解析 |
保存后客户端需重新拉取 netmap(退出重开 Tailscale.app)。
7. 验证
1# 服务端2sudo tailscale debug derp gz # 应显示 Region 900 == "gz"3tailscale netcheck # 出现 gz 及低延迟4curl -k -i -N -H "Connection: Upgrade" -H "Upgrade: DERP" \5 https://47.103.61.134:13477/derp # 期望 HTTP/1.1 101 Switching Protocols6
7# 客户端8tailscale ping <peer> # 期望 pong ... via DERP(gz)9tailscale status # peer 后显示 relay "gz"8. 三个「假警报」,看到不用管
-
tailscale debug derp报x509: certificate signed by unknown authority该命令自身的 bug:ipn/localapi/debugderp.go的tlsConfigForNode()漏了InsecureForTests(只处理CertName)。 真正建连的derphttp.tlsClient()是认这个字段的,中继不受影响。 -
is port 80 blocked?CanPort80: false且安全组本来就没开 80,正常。 -
标准 STUN 探包无响应 Tailscale 的 derper STUN 用私有方言:只回应带
SOFTWARE="tailnode"且以合法FINGERPRINT(crc32 ^ 0x5354554e)结尾的请求,其余静默丢弃(ErrWrongSoftware)。 用stun.miwifi.com之类公网 STUN 能通、用你的 derper 不通,不代表 derper 坏了。1import struct, os, zlib2software = b"tailnode"3tid = os.urandom(12)4attr = struct.pack('>HH', 0x8022, len(software)) + software5msg = struct.pack('>HH', 0x0001, len(attr) + 8) + b'\x21\x12\xa4\x42' + tid + attr6msg += 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. 日常运维
1cd ~/services/tailscale2docker compose ps / logs -f derper / restart / up -d| 项 | 值 |
|---|---|
| 容器 | restart: always |
| docker | systemctl enable 已开 |
| tailscaled 重启 | derper 无需重启(RuntimeDirectoryPreserve=yes) |