建议
先看完所有配置阶段后熟悉所有的环节和概念(这个过程使用的是域名方案,需要备案),然后跳转到 不备案纯 IP 方案 去部署。免去备案的烦恼。
注意点
- 阿里云服务器启动 Tailscale 后,如果 DNS、APT 或监控失联,需要检查 Linux 的 CGNAT 丢弃规则。
--accept-dns=false本身不能解除防火墙误拦,完整配置见 不备案纯 IP 方案。
第一阶段:基础设施准备(云服务器与网络安全)
1. 云服务器 (ECS) 的选型逻辑
对于 DERP 这种纯流量转发型服务,资源消耗极其不均衡:CPU/内存需求极低,但对带宽延迟极度敏感。
- 地域选择:必须选择离你主要使用设备最近的节点。
- 计费模式:强烈建议选择 “按使用流量计费”。
- 将带宽上限直接拉到 100Mbps(甚至更高)。
- DERP 平时负载极低,只有在传输大文件或远程桌面时才会有峰值。按量付费能保证你在需要时有足够的带宽吞吐,而平时只需支付极少的存储费。
- 镜像版本:推荐 Ubuntu 22.04 或 24.04 LTS,内核较新,对 Docker 和网络栈的优化更好。
2. 配置“防火墙模板”(安全组控制台)
在云控制台,不要直接在实例里乱改,建议先创建一个安全组模板,方便后续扩容或管理。
核心入方向规则 (Inbound)
你需要手动添加以下三条规则,缺一不可:
| 协议类型 | 端口范围 | 授权对象 | 描述 |
|---|---|---|---|
| TCP | 33445 | 0.0.0.0/0 | DERP HTTPS 端口。避开 443 减少被扫描,用于 TLS 握手和数据中转。 |
| UDP | 3478 | 0.0.0.0/0 | STUN 端口。这是 Tailscale 进行 NAT 穿透探测的核心,不通则无法实现 P2P。 |
| TCP | 22 | 你的办公/家IP | SSH 远程管理。建议限制来源 IP 以保安全。 |
3. 云服务器的网络检查(重要)
在云控制台完成配置后,务必确认以下两点,否则容易出现“内通外不通”:
- 关联实例:确保刚才创建的安全组已经正确关联到了你的 ECS 实例。
- 出方向规则 (Outbound):默认通常是“允许所有”,请保持现状,确保 DERP 容器能访问公网去校验
tailscaled.sock和回传数据。
4. 系统资源保护(2C2G 小规格必做)
2 核 2GB 的轻量服务器跑 Docker + Tailscale + 系统服务,内存余量极小。不做以下两步,系统在负载突增时(如自动更新触发大规模磁盘 I/O)极易被 OOM Killer 杀进程甚至整机锁死。
真实故障案例
在一台 2C2G 阿里云轻量服务器上,
unattended-upgrades(自动更新)在凌晨触发apt-daily.service,单个unattended-upgr进程吃掉了 1.2GB 内存,几乎占满物理内存。由于没有配置 Swap(Free swap = 0kB),内核无路可走,触发了 OOM Killer 强制杀进程:Out of memory: Killed process 11275 (unattended-upgr) total-vm:1536592kB, anon-rss:1216480kB apt-daily.service: Failed with result 'oom-kill'.这次系统 ” 自愈 ” 了——OOM Killer 杀掉了罪魁祸首后内存释放。但更早一次同样的场景中,系统直接锁死,SSH 无响应,只能通过云控制台强制重启。
排查命令:
# 查看内核 OOM Killer 记录 dmesg -T | grep -iE 'oom|killed|out of memory' # 查看指定时间段的系统日志(按需调整时间范围) journalctl --since '2026-05-03 22:30' --until '2026-05-03 23:10' -p warning --no-pager
A. 强制添加 Swap
在 2GB 内存的机器上不设置 Swap 是非常危险的。Swap 相当于在硬盘上开辟一块空间充当 ” 临时内存 “,当内存快满时,系统会把不常用的数据移到硬盘,防止内核因为 ” 无路可走 ” 而触发 OOM 锁死 CPU。
# 创建 2G 的虚拟内存
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# 写入 fstab 防止重启失效
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
# ESSD 云盘读写性能好,但 Swap 毕竟是磁盘 I/O,
# 将 swappiness 调低,让系统尽量用完物理内存后再动用 Swap
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p- 验证:执行
free -h,Swap 一行应显示2.0Gi。
B. 关闭高资源占用的定时任务
Ubuntu 默认启用了一系列 systemd 定时任务(timer)。在大内存机器上无感,但在 2GB 机器上,部分任务会吃掉大量内存甚至触发 OOM。以下是系统默认定时任务的资源评估:
| Timer | 功能 | 资源占用 | 建议 |
|---|---|---|---|
apt-daily.timer | 自动下载软件包更新 | 极高(可吃 1.3GB+) | 必须关闭 |
apt-daily-upgrade.timer | 自动安装更新 | 极高 | 必须关闭 |
man-db.timer | 重建 man 手册索引 | 中等(内存尖峰) | 建议关闭 |
logrotate.timer | 日志轮转 | 极低 | 保留,防止日志撑爆磁盘 |
fstrim.timer | SSD TRIM 回收空间 | 低 | 保留,ESSD 云盘需要 |
sysstat-collect.timer | 每 10 分钟采集系统统计 | 极低 | 保留,排查问题有用 |
dpkg-db-backup.timer | 备份 dpkg 数据库 | 极低 | 保留 |
systemd-tmpfiles-clean.timer | 清理临时文件 | 极低 | 保留 |
e2scrub_all.timer | 文件系统检查 | 低 | 保留 |
# 关闭自动更新服务
sudo systemctl stop unattended-upgrades
sudo systemctl disable unattended-upgrades
# 关闭 apt 定时任务(光关服务不够,timer 会定期拉起 apt-daily.service,
# 内部仍然调用 unattended-upgr 进程,照样吃满内存)
sudo systemctl stop apt-daily.timer apt-daily-upgrade.timer
sudo systemctl disable apt-daily.timer apt-daily-upgrade.timer
# 关闭 man 手册索引重建(小内存机器上偶发内存尖峰)
sudo systemctl stop man-db.timer
sudo systemctl disable man-db.timer
# 验证:确认已关闭的 timer 不再出现
systemctl list-timers --all | grep -iE 'apt|man-db|unattended'代价
关闭后你将失去自动安全补丁。对于一台只暴露两个高位端口的 DERP 中转服务器,攻击面极小,风险可控。但建议每月手动执行一次
apt update && apt upgrade保持系统更新。注意:手动
apt upgrade时,如果unattended-upgrades包本身被更新,其安装脚本可能会重新启用 timer。更新完后务必检查:systemctl list-timers | grep -iE 'apt|man-db|unattended'如果有输出,说明 timer 被 ” 复活 ” 了,需要重新关闭。
5. 安装本地 Tailscale (宿主机节点)
为了让 DERP 具备“查票”验证能力,宿主机本身必须也加入你的 Tailscale 网络:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --accept-dns=false- 检查 Sock 文件:安装后确认
/var/run/tailscale/tailscaled.sock存在,它是 DERP 容器识别“自己人”的唯一信物。
为什么要加 --accept-dns=false?
这个参数是网络稳定性的救命稻草。
A. 防止内网“失联” (The Aliyun Conflict)
阿里云的 apt 源和内部服务依赖其内网 DNS(通常是 100.100.2.136)。
- 默认 (True):Tailscale 会接管
/etc/resolv.conf,将所有 DNS 请求导向自己的100.100.100.100。 - 结果:Tailscale 有时无法正确转发阿里云的内网域名请求,导致
apt update拿不到 IP,或者像刚才那样虽然拿到了 IP 但路由优先级混乱。 - 关闭后:系统将继续使用阿里云原生的 DNS,互不干扰。
B. 解决“127.0.0.53”死循环
Ubuntu 24.04 使用 systemd-resolved。
- 默认 (True):Tailscale 会试图通过
resolvectl强行插入解析链路。 - 结果:在某些复杂的网络环境下(尤其是国内云厂商),这会导致 DNS 环路或超时,表现为你刚才遇到的
Connecting to mirrors...卡死。
第一阶段完成后,你的服务器就是一个在公网上“待命”的专业网关了。下一步就是通过 DNS 把你的域名和这台机器的 IP 绑定,并把灰云朵点亮。
第二阶段:DNS 解析与流量路由配置
1. 域名解析策略(Cloudflare 侧)
由于你的 DERP 服务器没有备案,选择 Cloudflare (CF) 作为 DNS 解析商是最灵活的做法。
- 添加 A 记录:
- Type:
A - Name:
derp(对应完整域名derp.xxx.xxx) - IPv4 address: 填入你在第一阶段获得的阿里云 公网 IP。
- TTL: 设置为
Auto或1 min(方便后续如果更换服务器能快速生效)。
- Type:
- 关键:关闭代理模式 (Proxy Status):
- 操作:点击那朵云,使其变为 灰色 (DNS Only)。
- 为什么要这样做?:Cloudflare 的“橙色云朵”是为 Web 流量(HTTP/HTTPS)设计的 CDN 代理。而 Tailscale 的 DERP 协议涉及非标端口的 TLS 握手和复杂的 UDP 穿透协助。如果开启代理,CF 节点会尝试接管 TLS,导致 Tailscale 客户端无法识别真实的服务器指纹,直接导致连接失败。
2. 申请 Cloudflare API 令牌 (Token)
为了在第四阶段实现“无人值守”的证书更新,我们需要让宿主机的 acme.sh 有权限修改 DNS 记录来证明域名的所有权(DNS-01 验证)。
-
步骤:
- 登录 CF 控制台,点击右上角 My Profile → API Tokens。
- 点击 Create Token。
- 选择 Edit Cloudflare DNS 模板。
- Permissions:确保包含
Zone - DNS - Edit和Zone - Zone - Read。 - Zone Resources:选择
Include - Specific zone - xxx.xxx。 - 记录下生成的 API Token(它只会出现一次)。
3. 连通性预检
在进入容器部署阶段之前,必须确保解析已经生效。在你的 本地电脑 (macOS) 终端运行:
# 验证解析是否指向云服务器 IP
nslookup derp.xxx.xxx
# 验证端口是否已在公网开放(第一阶段防火墙生效检查)
nc -vz derp.xxx.xxx 33445- 预期结果:
nc命令返回succeeded。如果显示Connection refused或Timeout,请回过头检查云安全组是否有误,或者你的 ISP 是否拦截了该非标端口。
4. 为什么不直接用公网 IP?
虽然 Tailscale ACL 支持填 IP,但使用域名有三个不可替代的优势:
- SSL 证书绑定:DERP 必须在 TLS (HTTPS) 下运行,证书是绑定域名的,IP 无法直接申请常规免费证书。
- 平滑迁移:如果当前云节点故障,你可以在其他区域再起一台,只需在 DNS 里改一下 IP,所有客户端会自动重连,无需修改 ACL。
- 安全性:配合证书,可以防止中间人攻击(MITM),确保你的中转数据只有你自己能解密。
第二阶段完成后,你的域名已经像导航一样指向了那台待命的服务器。接下来,我们将进入第三阶段,在那台服务器上拉起具有“查票功能”的 Docker 容器。
第三阶段:Docker 容器部署与身份验证闭环
1. 镜像选择与环境适配
我们使用的是 fredliang/derper,这个镜像对原生 Tailscale DERP 进行了封装,支持通过环境变量快速配置。
- 获取镜像:考虑到国内云服务器拉取 Docker Hub 可能存在网络抖动,建议配置加速器或通过
docker pull预先缓存。 - 核心依赖:确保宿主机的
/var/run/tailscale/tailscaled.sock存在。这个文件是容器内 DERP 进程向宿主机 Tailscale 询问“这个客户端是不是咱家人”的通信渠道。
2. 建立证书挂载目录
在宿主机创建持久化目录,用于存放第四阶段将生成的 SSL 证书。
sudo mkdir -p /opt/derp/certs3. 执行容器启动命令(深度解析)
这是整个项目的核心命令,每一行环境变量都对应一个关键的工程逻辑:
docker run -d \
--name derper \
--restart always \
-p 33445:443 \
-p 3478:3478/udp \
-v /opt/derp/certs:/app/certs \
-v /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock \
-e DERP_DOMAIN=derp.xxx.xxx \
-e DERP_CERT_MODE=manual \
-e DERP_VERIFY_CLIENTS=true \
fredliang/derper参数详解:
-p 33445:443:将容器内的 HTTPS 标准端口映射到宿主机的非标端口 33445。这既能通过防火墙,又能有效避开针对 443 端口的扫描。-v ...tailscaled.sock:最关键的挂载。通过这个 Socket,DERP 获得了“查票”的能力。DERP_CERT_MODE=manual:禁止镜像自带的自动申请逻辑。因为我们要用acme.sh手动管理证书,这样更灵活且利于多机同步。DERP_VERIFY_CLIENTS=true:开启防白嫖开关。没有这个参数,你的服务器就是公共中转站,带宽会被瞬间耗尽。
4. 直观验证:服务是否真的启动?
容器启动后,不要只看 docker ps,要通过以下三步确认服务质量:
A. 检查容器内连通性
docker logs -f derper- 预期输出:看到
derper: serving on :443 with TLS。
B. 验证证书加载(此时证书尚未申请,服务可能会报警告,这是正常的)
由于我们选了 manual 模式且还没放证书,此时服务会启动但 TLS 可能握手失败。这正是我们进入第四阶段的触发条件。
C. 验证 Socket 权限
如果日志报 permission denied 访问 .sock 失败,需要给宿主机的 Socket 提权:
sudo chmod 666 /var/run/tailscale/tailscaled.sock5. 身份校验逻辑的“直观体验”
一旦这个阶段部署完成,任何尝试连接该服务器的外部设备,如果不在你的 Tailscale 网络中,DERP 会在日志中直接打印 rejected: peer nodekey:... not found in local tailscaled。
第三阶段完成后,你的 DERP 就像一个装好了防盗门但还没拿到钥匙(证书)的房间。接下来的第四阶段,我们将通过 acme.sh 打造这把“全自动钥匙”。
第四阶段:自动化证书管理与全自动钩子 (Hook)
1. 安装 acme.sh 脚本
acme.sh 是目前最轻量、功能最全的自动化证书管理工具,它支持全自动续期并触发后续动作。
# 安装 acme.sh,替换为你的真实邮箱用于接收通知
curl https://get.acme.sh | sh -s email=your@email.com
# 使环境变量生效
source ~/.bashrc2. 配置 Cloudflare API 凭证
为了让脚本能自动完成 DNS-01 挑战(无需手动改解析记录),我们需要将第二阶段申请的 Token 导入环境变量:
# 这里的 Token 来自第二阶段的记录
export CF_Token="xxxxxxxxxxxxxxxxxxxxxxxxxxxx"3. 申请证书 (ECC 算法)
现在的 acme.sh 默认使用 ZeroSSL,而 ZeroSSL 的 API 在国内部分网络环境下极其不稳定,且需要绑定邮箱,经常会导致申请失败。为了保证成功率,切换到 Let’s Encrypt 是标准动作。
acme.sh --set-default-ca --server letsencrypt我们推荐使用 ECC (椭圆曲线) 证书,它比传统的 RSA 证书更小、更快,更适合 DERP 这种高并发中转场景。
acme.sh --issue --dns dns_cf -d derp.xxx.xxx --ecc- 工作原理:
acme.sh会调用 CF API 自动生成一个随机的 TXT 记录,Let’s Encrypt 验证通过后,证书会自动下载到~/.acme.sh/目录下。 - 注意:这里的
derp.xxx.xxx要和 3. 执行容器启动命令(深度解析) 中配置DERP_DOMAIN的值一致。
4. 部署证书并挂载“自动重启钩子”
这是实现无人值守的关键。不要手动拷贝证书,使用 --install-cert 命令,它会记住路径,并在以后每次更新证书时自动执行拷贝和重启操作。
acme.sh --install-cert -d derp.xxx.xxx --ecc \
--key-file /opt/derp/certs/derp.xxx.xxx.key \
--fullchain-file /opt/derp/certs/derp.xxx.xxx.crt \
--reloadcmd "docker restart derper"逻辑拆解:
--key-file/--fullchain-file:将证书分发到你在第三阶段为 Docker 准备的挂载目录中。--reloadcmd:这是灵魂参数。acme.sh每 60 天会自动续期证书。一旦续期成功,它会立刻执行docker restart derper,让容器重新加载那份有效期又是 90 天的新证书。
5. 验证全自动链路
配置完成后,你可以通过查看 acme.sh 的定时任务和日志来确认一切正常:
-
检查 Crontab:输入
crontab -l,你应该能看到一行acme.sh --cron,它每天凌晨会定时巡检证书。 -
检查 Hook:
cat ~/.acme.sh/derp.xxx.xxx_ecc/derp.xxx.xxx.conf | grep Le_ReloadCmd如果输出包含
docker restart derper,恭喜你,这台服务器已经进化为自愈系统。
6. 最终连通性大检查
现在,你的 DERP 已经有了合法的证书。在你的设备上执行最后的“大考”:
# 1. 验证 TLS 是否被认可(应该返回 200 OK,且没有证书报错)
curl -v https://derp.xxx.xxx:33445如果返回 Successfully established a DERP connection,则说明:
- 域名解析对了。
- 防火墙端口通了。
- Docker 身份验证生效了。
- SSL 证书握手成功了。
至此,你的私有 DERP 中转网络已经彻底竣工。你现在拥有了一个比官方节点延迟更低,且绝对私密的专属通道。
第五阶段:Tailnet 集成与全局路由生效
1. 登录并修改 Tailscale 控制台
Tailscale 的所有路由逻辑都由一个中心化的控制面板管理。
- 打开 Tailscale Admin Console - Access Control。
- 在编辑器中,找到
derpMap结构(如果不存在,通常加在groups或hosts之后,但在最外层大括号内)。
2. 注入 derpMap 配置
将以下 JSON 代码块填入。这里的 RegionID 建议从 901 开始,以避免与官方 RegionID(1-30)冲突。
"derpMap": {
"OmitDefaultRegions": false, // 设为 true 可测试强制走自建,生产环境建议 false
"Regions": {
"901": {
"RegionID": 901,
"RegionCode": "my-cloud", // 自建节点标识
"RegionName": "My Cloud",
"Nodes": [
{
"Name": "1",
"RegionID": 901,
"HostName": "derp.xxx.xxx",
"DERPPort": 33445, // 对应你在第三阶段映射的端口
"STUNPort": 3478,
"InsecureForTests": false,
},
],
},
},
},3. 触发配置下发
点击 Save。一旦点击保存,Tailscale 的控制平面(Coordination Server)会在几秒钟内将这个新地图推送到你全球所有的设备上(macOS, Ubuntu, iOS, Android 等)。
4. 深度验证:从“感知”到“使用”
A. 探测感知(Netcheck)
在你的 macOS 上执行,观察你的 DERP 是否出现在列表中:
tailscale netcheck- 检查点:在输出的
DERP Latency列表中,你应该能看到my-cloud: 20-40ms。这证明客户端已经收到了地图,并且通过 STUN 成功探测到了自建节点的延迟。
B. 协议握手(Debug)
tailscale debug derp 901- 预期结果:返回
Successfully established a DERP connection。这说明你的客户端已经完成了 TLS 握手,并且 DERP 服务器通过宿主机 .sock 文件验证了你的身份。
C. 流量实战(Ping)
针对你的远程设备进行测试:
tailscale ping <目标设备的 Tailscale IP>- 核心观察:输出应包含
via DERP(my-cloud)且延迟相比官方节点大幅下降。
5. 常见故障排除 (Infra Checklist)
- 配置不生效?:如果在
netcheck中完全看不到901,请检查 JSON 语法(逗号是否漏写)或RegionID是否重复。 - 延迟依然很高?:确认
derpMap里的HostName拼写是否与证书一致。如果不一致,TLS 校验失败后客户端会默默回退到官方节点。 - P2P 依然不通?:DERP 的作用是 ” 保底 “。如果有了低延迟 DERP 后仍然觉得卡顿,请检查远程设备所在的内网是否完全封死了 UDP 流量(如果 UDP 3478 被封,STUN 探测不到,连接会变得非常脆弱)。
总结:第五阶段完成后,你的专属 DERP 中转网络正式闭环。你构建了一个 ” 按需触发、低延迟、防白嫖、自续期 ” 的企业级 VPN 中转方案。现在,你可以随时随地以 ” 本地内网 ” 的体验去调试远程设备了。
验证防白嫖(VERIFY_CLIENTS)是否生效
适用范围
无论你使用的是域名方案还是纯 IP 方案,验证步骤都一样。
开启 DERP_VERIFY_CLIENTS=true 后,需要确认两件事:自己人能连、陌生人被拒。
1. 确认授权设备正常
在你的客户端(如 Mac)执行:
tailscale netcheck在输出的 DERP latency 列表中应能看到你的自建节点及其延迟(如 my-derp: 14.7ms)。有延迟数据说明客户端已经通过身份验证并成功与 DERP 通信。
2. 模拟陌生访客(curl 快速验证)
在服务器上用 curl 模拟一个没有 Tailscale 身份的客户端,尝试发起 DERP 协议升级:
curl -sk https://127.0.0.1:55443/derp \
-H 'Upgrade: derp' \
-H 'Connection: Upgrade' \
-o /dev/null -w '%{http_code}\n'返回 101(协议升级)是正常的——DERP 会先接受 TCP 连接,但在后续的密钥交换阶段因为拿不到合法的 node key 而超时断开。等待约 10 秒后查看日志:
docker logs --tail 5 derper预期输出:
derp: <容器网关IP>:xxxxx: receive client key: read tcp ... i/o timeout
这说明陌生连接在密钥交换阶段被卡死并超时,无法完成握手,不会被中转任何流量。
3. 实打实看到 rejected(模拟白嫖)
curl 只能证明 ” 没有密钥的连接会超时 “。如果你想亲眼看到 DERP 主动拒绝一个真实的 Tailscale 客户端,最直观的方式是模拟一次白嫖——用另一个 tailnet 的设备尝试蹭你的 DERP。
白嫖原理
如果你没有开启 VERIFY_CLIENTS,任何人只要知道你的 IP 和端口,在他自己的 Tailscale ACL 里加一条 derpMap 就能把你的服务器当成免费中转:
"derpMap": {
"Regions": {
"901": {
"RegionID": 901,
"RegionCode": "free-ride",
"RegionName": "Free Ride",
"Nodes": [{
"Name": "1",
"RegionID": 901,
"HostName": "<你的服务器IP>",
"IPv4": "<你的服务器IP>",
"DERPPort": 55443,
"STUNPort": 53478,
"InsecureForTests": true
}]
}
}
}InsecureForTests: true 会跳过证书校验,连指纹都不需要知道。保存后他 tailnet 里的所有设备就会开始白嫖你的带宽。
模拟步骤
- 注册一个第二个 Tailscale 账号(免费即可)。
- 在任意一台机器上用这个新账号
tailscale up加入新 tailnet。 - 在新 tailnet 的 ACL 控制台 中添加上面的
derpMap,将 IP 改为你真实的服务器地址,保存。 - 在该机器上执行:
# 等待 ACL 下发后,尝试连接你的 DERP
tailscale netcheck- 回到你的 DERP 服务器查看日志:
docker logs --tail 10 derper预期结果
- 开启了
VERIFY_CLIENTS=true:日志中出现:
rejected: peer nodekey:xxxxx not found in local tailscaled
DERP 通过 tailscaled.sock 查询了宿主机的 tailscaled,确认这个 node key 不属于你的 tailnet,直接拒绝。白嫖失败。
- 如果没开
VERIFY_CLIENTS:对方的netcheck会正常显示你的 DERP 延迟,白嫖成功,你的带宽就是别人的了。
为什么 curl 是 " 超时 " 而真实客户端是 " 拒绝 "?
curl 不会发送 DERP 协议的客户端密钥,连接卡在等待密钥阶段直到超时。而真正的 Tailscale 客户端会正常发送 node key,DERP 拿着这个 key 去问 tailscaled” 这人是咱家的吗?“,得到否定答案后主动断开并记录
rejected。
不备案纯 IP 方案
这套方案使用 公网 IP + 高位端口 + 自签名证书指纹 部署 DERP,不依赖域名解析或 ACME 签发。客户端通过 CertName 固定服务器证书,服务端通过宿主机 Tailscale 校验客户端身份。
早期参考:V2EX 讨论、自建 Tailscale Derper 中继服务。
以下统一使用文档示例地址:公网 IP 为 203.0.113.10,宿主机 Tailscale IP 为 100.64.0.10,另一台设备为 100.64.0.20。部署时将它们替换为自己的地址,并填写自己生成的证书指纹。
| 项目 | 配置 |
|---|---|
| 宿主机系统 | Ubuntu 24.04,已安装 Docker Engine 和 Compose 插件 |
| 宿主机 Tailscale | 参考版本 1.104.2 |
| DERP 镜像 | 华为镜像站的 yangchuansheng/derper:v1.102.5,固定 digest |
| DERP 端口 | 公网 55443/TCP → 容器 33445/TCP |
| STUN 端口 | 公网 53478/UDP → 容器 3478/UDP |
| 容器资源上限 | 0.5 个逻辑核、256MiB 内存 |
版本选择
此镜像标签虽然叫
v1.102.5,其中derper --version实际返回1.103.0-ERR-BuildInfo,不能仅凭标签判断源码版本。本教程采用宿主机 Tailscale1.104.2作为参考组合。Tailscale 官方 DERP 说明将同一 Git revision 作为
--verify-clients的受测组合。使用第三方镜像时,应固定镜像 digest,并在升级后重新验证认证与转发,跨版本兼容需要逐次确认。
1. 安装宿主机 Tailscale
宿主机需要先加入自己的 tailnet,容器再通过 LocalAPI 查询客户端是否为可见节点。
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --accept-dns=false
# 检查版本、节点状态和本地 API socket
tailscale version
tailscale status
ls -l /var/run/tailscale/tailscaled.sock如果宿主机已经加入 tailnet,只需要调整 DNS 接收选项:
sudo tailscale set --accept-dns=false--accept-dns=false 让这台服务器保留原有 DNS 配置。它不会解除防火墙对 DNS 回包的丢弃;阿里云还需要完成下面的 CGNAT 兼容配置。
2. 解决阿里云 CGNAT 网段冲突
为什么启动 Tailscale 后 DNS、APT 和监控会失联?
CGNAT 是运营商级 NAT。它使用的共享地址段为 100.64.0.0/10,范围是 100.64.0.0 到 100.127.255.255,并非公网可路由地址,也不同于常见的 192.168.x.x 私网段。见 RFC 6598。
Tailscale 从这段地址中分配节点 IP。Linux 客户端默认会丢弃从 tailscale0 以外接口进入、源地址属于该段的流量,防止其他网络伪装成 Tailscale 节点。Tailscale 官方说明解释了这一行为及其兼容配置。
阿里云也把这段地址用于内网服务,例如:
- 内网 DNS:
100.100.2.136、100.100.2.138。 - Ubuntu 内网镜像:可能解析到
100.100.2.148、100.100.2.158。
这些服务的回包从云网卡 eth0 进入,也会命中默认丢弃规则。结果就是 DNS 超时、apt update 卡住、依赖解析的监控上报失败。因此,即使保留了阿里云 DNS,单独设置 --accept-dns=false 仍可能无效。
为这台服务器关闭 CGNAT 丢弃规则
打开 Tailscale 策略编辑器,在最外层的 nodeAttrs 数组中追加:
{
"nodeAttrs": [
{
"target": ["100.64.0.10"],
"attr": ["disable-linux-cgnat-drop-rule"]
}
]
}如果已有 nodeAttrs,只追加其中的对象,保留其他条目。target 填的是服务器的 Tailscale IP,不是公网 IP。
这个属性由 Tailscale 官方 Linux 客户端实现。它只对目标节点取消整段 CGNAT 的默认丢弃规则,不是仅放行两个 DNS 地址;不需要同时切换 nodivert 或手工删除 Tailscale 的防火墙规则。
配套启用 eth0 的反向路径校验
反向路径校验(RPF)根据路由表检查数据包的来源:如果一个包从 eth0 进入,但回到其源地址的最佳路线应走另一块接口,严格模式会丢弃它。阿里云 DNS 正常经 eth0 往返,可继续通信;预期经 tailscale0 到达的节点地址从 eth0 冒入时,则会被检查。
本方案适用于单云网卡、只承担普通 Tailscale 节点和 DERP 的服务器。先确认云网卡名称和路由:
ip route get 100.100.2.136
ip route get 100.100.2.138下面以两者均走 eth0 为例,持久化该接口的严格校验:
sudo tee /etc/sysctl.d/90-aliyun-derp-rpf.conf >/dev/null <<'EOF'
net.ipv4.conf.eth0.rp_filter = 1
EOF
sudo sysctl -p /etc/sysctl.d/90-aliyun-derp-rpf.conf
sysctl net.ipv4.conf.all.rp_filter \
net.ipv4.conf.eth0.rp_filter \
net.ipv4.conf.tailscale0.rp_filter这里采用 all=0、eth0=1、tailscale0=0,只在云网卡启用严格模式。Linux 实际取 all 与接口值中的较大值,因此其他系统若已有 all=2,仅设 eth0=1 并不会变成严格模式。多网卡、非对称路由、出口节点或子网路由场景需要按实际路由重新评估,不能直接照搬。原理见 Linux 内核 RPF 文档。
RPF 是来源检查,不能代替 Tailscale 身份认证或 DERP 的 VERIFY_CLIENTS。
检查配置是否生效
sudo iptables -S ts-input应看到对应 CGNAT 规则变为 RETURN,而不是 DROP:
-A ts-input -s 100.64.0.0/10 ! -i tailscale0 -j RETURNRETURN 表示回到调用它的防火墙链继续处理,并非无条件放行所有流量。保留这个节点属性,Tailscale 重启后仍会按控制台策略生成规则。
3. 保证宿主机重启服务后,容器仍能完成认证
DERP_VERIFY_CLIENTS=true 依赖宿主机的 tailscaled.sock。不要单独 bind mount 这个 socket 文件:tailscaled 重启时会重建 socket,容器可能仍绑定旧文件,导致认证失效。
应同时做两项配置:
- 容器只读挂载
/var/run/tailscale整个运行目录。 - 让 systemd 在 tailscaled 停止或重启时保留这个目录。
sudo mkdir -p /etc/systemd/system/tailscaled.service.d
sudo tee /etc/systemd/system/tailscaled.service.d/20-derper-runtime.conf >/dev/null <<'EOF'
[Service]
RuntimeDirectoryPreserve=yes
EOF
sudo systemctl daemon-reload
sudo systemctl restart tailscaledUbuntu 的服务单元使用 RuntimeDirectory=tailscale,默认会在停止服务时删除运行目录。因此,只改目录挂载还不够;还需要 RuntimeDirectoryPreserve=yes。这项设置保留目录,允许容器看到新生成的 socket;/run 在整机重启时仍会正常重建。见 systemd 官方说明。
只读挂载限制的是目录文件修改,容器仍能连接其中的 Unix socket。它不改变 LocalAPI 本身的权限。
4. 使用 Docker Compose 部署纯 IP DERP
创建证书持久化目录:
sudo mkdir -p /opt/derp/certs
cd /opt/derp将以下内容保存为 /opt/derp/docker-compose.yml,把 DERP_DOMAIN 替换为自己的公网 IP:
services:
derper:
container_name: derper
image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/ghcr.io/yangchuansheng/derper:v1.102.5@sha256:4cb56cf122b2135bdbba401b854a90b0805a28b86a68167078894bb30a9780eb
restart: unless-stopped
ports:
- "55443:33445"
- "53478:3478/udp"
environment:
DERP_DOMAIN: "203.0.113.10"
DERP_CERT_MODE: "manual"
DERP_CERT_DIR: "/cert"
DERP_ADDR: ":33445"
DERP_STUN: "true"
DERP_STUN_PORT: "3478"
DERP_HTTP_PORT: "-1"
DERP_VERIFY_CLIENTS: "true"
volumes:
- "/etc/localtime:/etc/localtime:ro"
- "/etc/timezone:/etc/timezone:ro"
- "./certs:/cert"
- "/var/run/tailscale:/var/run/tailscale:ro"
cpus: "0.50"
mem_limit: 256M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"关键参数:
DERP_DOMAIN虽然名字含 Domain,这个镜像也支持填纯 IP,并在缺少对应证书时生成自签名 IP 证书。DERP_CERT_MODE=manual配合持久化的/cert使用,不走域名证书申请流程。DERP_HTTP_PORT=-1关闭额外的 HTTP 服务,本方案只使用上面的 TLS 和 STUN 端口。DERP_VERIFY_CLIENTS=true保留客户端校验;需要使用 DERP 的客户端必须对宿主机 Tailscale 可见。mem_limit是单机 Compose 的内存限制字段,不能写成服务级的memory: 256M。见 Docker Compose 文档。
校验配置并启动:
sudo docker compose config --quiet
sudo docker compose pull
sudo docker compose up -d
sudo docker logs --tail 30 derper如果是在已有容器上修改挂载或资源限制,使用 sudo docker compose up -d --force-recreate 应用新配置;单独 docker restart 不会读取新的 Compose 配置。
随后在云安全组开放:
| 协议 | 公网端口 | 用途 |
|---|---|---|
| TCP | 55443 | DERP TLS 连接与转发 |
| UDP | 53478 | STUN 路径探测 |
| TCP | 22 | SSH 管理,按需要限制来源 |
使用高位端口能减少部分默认端口扫描,身份校验仍由证书指纹和 VERIFY_CLIENTS 完成。
5. 配置证书指纹与 DERP Map
获取并保留证书指纹
容器首次启动后,从日志获取 sha256-raw: 指纹:
sudo docker logs derper 2>&1 | grep 'sha256-raw:'也可以对持久化证书的 DER 编码计算 SHA-256,得到 sha256-raw: 后面的十六进制值:
openssl x509 -in /opt/derp/certs/203.0.113.10.crt -outform DER | sha256sum
openssl x509 -in /opt/derp/certs/203.0.113.10.crt -noout -dates保留 certs 目录,升级镜像时继续使用同一张证书。自签名证书也有有效期;删除后重建或到期换证,都需要同步更新控制台中的指纹。
将自建节点加入 tailnet
在 Tailscale 策略编辑器最外层合并下面的 derpMap。保留已有规则及前面添加的 nodeAttrs,并将 IP 和指纹换成自己的值:
{
"derpMap": {
"OmitDefaultRegions": false,
"Regions": {
"901": {
"RegionID": 901,
"RegionCode": "my-derp",
"RegionName": "My DERP",
"Nodes": [
{
"Name": "1",
"RegionID": 901,
"HostName": "203.0.113.10",
"IPv4": "203.0.113.10",
"DERPPort": 55443,
"STUNPort": 53478,
"STUNOnly": false,
"InsecureForTests": true,
"CertName": "sha256-raw:REPLACE_WITH_YOUR_CERT_SHA256",
"CanPort80": false
}
]
}
}
}
}OmitDefaultRegions=false保留官方 DERP 作为候选和兜底,不强迫所有流量走自建节点。HostName、IPv4填公网 IP;DERPPort、STUNPort填宿主机映射后的端口。CertName绑定完整证书的 SHA-256 指纹,负责客户端对服务器身份的校验。InsecureForTests=true沿用此纯 IP 配置的兼容设置。现代客户端的sha256-raw:处理会单独安装指纹校验,不要求这一项必须为 true;关键是保留正确的CertName,不能只开启跳过校验却省略指纹。相关逻辑见 DERP TLS 配置和指纹校验实现。
保存后,控制平面会把地图下发到客户端,通常不需要 tailscale down/up --reset。重置可能改变已有 DNS、路由等偏好,不能作为常规刷新步骤。
6. 控制内存占用,避免后台任务触发 OOM
对于少量设备、以 SSH 为主、偶尔使用远程桌面的场景,1GB 内存可作为起步规格。需要给操作系统、tailscaled、Docker 和云监控预留空间,并控制后台任务峰值;256MiB 是 DERP 容器的上限,不是预留内存,也不是它会持续占用这么多。
限制容器资源并保留系统余量
前面的 Compose 已包含:
cpus: "0.50":限制为半个逻辑核的 CPU 配额。mem_limit: 256M:限制容器 RAM,避免异常增长拖累宿主机;超过限制仍可能发生容器 OOM。- 日志
10m × 3轮转:控制日志磁盘占用,这不是内存限制。
检查实际配置:
sudo docker inspect --format \
'NanoCpus={{.HostConfig.NanoCpus}} Memory={{.HostConfig.Memory}}' derper
sudo docker stats --no-stream derper
free -h预期 NanoCpus=500000000、Memory=268435456。评估主机余量时主要看 MemAvailable / available,不能把 Linux 文件缓存全部算成不可用内存。
保留 Swap,降低换页倾向
建议准备 2GiB Swap,并持久化 vm.swappiness=10。已有 Swap 时不要重新创建或格式化;首次配置可按前文 A. 强制添加 Swap 操作。
swapon --show
sysctl vm.swappiness如需单独设置换页倾向:
sudo tee /etc/sysctl.d/90-derp-memory.conf >/dev/null <<'EOF'
vm.swappiness = 10
EOF
sudo sysctl -p /etc/sysctl.d/90-derp-memory.confSwap 为短时内存尖峰提供缓冲,不会限制进程增长,也不能保证不再 OOM。自动更新任务仍可能耗尽 RAM 和 Swap,因此还需要控制下面的后台任务。
关闭自动更新与手册索引任务,改为人工维护
小内存机器上的主要峰值来源之一是 unattended-upgrades 及 APT 定时任务;man-db 重建索引也可能带来额外负载。将这些任务改为人工维护:
sudo systemctl disable --now \
unattended-upgrades.service \
apt-daily.timer \
apt-daily-upgrade.timer \
man-db.timer
systemctl is-enabled \
unattended-upgrades.service \
apt-daily.timer \
apt-daily-upgrade.timer \
man-db.timer四项应显示 disabled。只关闭 unattended-upgrades.service 不够,APT timer 仍可能拉起更新任务。保留日志轮转、磁盘 TRIM、系统统计和云监控等有用的基础服务。
关闭自动任务后,需要安排人工审查并安装安全更新。手动更新后再次检查这些 timer,避免软件包安装脚本重新启用它们。
需要刷新软件包索引时,可以给这一次命令加临时资源限制:
sudo systemd-run --scope \
-p MemoryMax=512M \
-p CPUQuota=50% \
apt-get update它只限制这一次索引更新,不会为 APT 服务安装永久限制,也不等于已执行系统升级。安装或升级软件包时,应另外评估所需内存和维护窗口。
7. 按层验证部署结果
宿主机 DNS 与内网源
dig +time=2 +tries=1 @100.100.2.136 mirrors.cloud.aliyuncs.com
getent ahostsv4 mirrors.cloud.aliyuncs.com
curl -I --max-time 10 http://mirrors.cloud.aliyuncs.com/ubuntu/dists/noble/InRelease应能收到 DNS 答复并访问镜像源。再按前面的受限命令运行 apt-get update,确认真实更新路径可用。云监控应恢复成功上报;控制台图表需要等待新的采样数据。
容器服务与本地认证接口
sudo docker exec derper curl -k -fsS --max-time 5 \
https://127.0.0.1:33445/debug/check预期返回 derp.Server ConsistencyCheck okay。通过公网再检查 TLS 服务可达性:
curl -k -I --max-time 10 https://203.0.113.10:55443/curl -k 只用于检查服务响应,它跳过证书验证,不能证明证书指纹或客户端身份校验正确。
客户端收到地图并完成真实通信
在客户端执行,最后一条命令中的 100.64.0.20 应替换为另一台设备的 Tailscale IP:
tailscale debug derp-map
tailscale netcheck
tailscale ping --until-direct=false -c 5 100.64.0.20- 地图应包含 region
901;netcheck可看到my-derp的探测延迟。 via DERP(my-derp)表示这次通信实际使用了自建中继。direct表示已经直连,属于正常且通常更好的结果。- 保留官方节点时,客户端会按路径和延迟选择,不保证每次都使用自建 DERP。
netcheck 有延迟只说明探测可用,不能单独证明 VERIFY_CLIENTS 生效。完整验收还应确认:合法节点能转发数据,宿主机不认识的 node key 被拒绝。HTTP 101、无密钥连接超时,以及 nc -zuv 显示成功,都不能代替这些验证。
确认 tailscaled 重启后认证仍正常
配置完成后,通过公网 SSH 或云控制台执行一次受控验证:
sudo systemctl restart tailscaled
systemctl show tailscaled -p RuntimeDirectoryPreserve
sudo docker exec derper curl -k -fsS --max-time 5 \
https://127.0.0.1:33445/debug/check应看到 RuntimeDirectoryPreserve=yes,等待 tailscaled 恢复运行后,一致性检查仍通过,客户端也能继续通信。无需为这次宿主机服务重启重新创建 DERP 容器。
后续升级时,保留证书目录、Compose 资源限制、CGNAT 节点属性和运行目录设置,并重复认证与转发验证。