建议

先看完所有配置阶段后熟悉所有的环节和概念(这个过程使用的是域名方案,需要备案),然后跳转到 不备案纯 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)

你需要手动添加以下三条规则,缺一不可:

协议类型端口范围授权对象描述
TCP334450.0.0.0/0DERP HTTPS 端口。避开 443 减少被扫描,用于 TLS 握手和数据中转。
UDP34780.0.0.0/0STUN 端口。这是 Tailscale 进行 NAT 穿透探测的核心,不通则无法实现 P2P。
TCP22你的办公/家IPSSH 远程管理。建议限制来源 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.timerSSD 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(方便后续如果更换服务器能快速生效)。
  • 关键:关闭代理模式 (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 验证)。

  • 步骤:

    1. 登录 CF 控制台,点击右上角 My Profile → API Tokens。
    2. 点击 Create Token。
    3. 选择 Edit Cloudflare DNS 模板。
    4. Permissions:确保包含 Zone - DNS - Edit 和 Zone - Zone - Read。
    5. Zone Resources:选择 Include - Specific zone - xxx.xxx。
    6. 记录下生成的 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,但使用域名有三个不可替代的优势:

  1. SSL 证书绑定:DERP 必须在 TLS (HTTPS) 下运行,证书是绑定域名的,IP 无法直接申请常规免费证书。
  2. 平滑迁移:如果当前云节点故障,你可以在其他区域再起一台,只需在 DNS 里改一下 IP,所有客户端会自动重连,无需修改 ACL。
  3. 安全性:配合证书,可以防止中间人攻击(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/certs

3. 执行容器启动命令(深度解析)

这是整个项目的核心命令,每一行环境变量都对应一个关键的工程逻辑:

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.sock

5. 身份校验逻辑的“直观体验”

一旦这个阶段部署完成,任何尝试连接该服务器的外部设备,如果不在你的 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 ~/.bashrc

2. 配置 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 的所有路由逻辑都由一个中心化的控制面板管理。

  1. 打开 Tailscale Admin Console - Access Control。
  2. 在编辑器中,找到 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 里的所有设备就会开始白嫖你的带宽。

模拟步骤

  1. 注册一个第二个 Tailscale 账号(免费即可)。
  2. 在任意一台机器上用这个新账号 tailscale up 加入新 tailnet。
  3. 在新 tailnet 的 ACL 控制台 中添加上面的 derpMap,将 IP 改为你真实的服务器地址,保存。
  4. 在该机器上执行:
# 等待 ACL 下发后,尝试连接你的 DERP
tailscale netcheck
  1. 回到你的 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,不能仅凭标签判断源码版本。本教程采用宿主机 Tailscale 1.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 RETURN

RETURN 表示回到调用它的防火墙链继续处理,并非无条件放行所有流量。保留这个节点属性,Tailscale 重启后仍会按控制台策略生成规则。

3. 保证宿主机重启服务后,容器仍能完成认证

DERP_VERIFY_CLIENTS=true 依赖宿主机的 tailscaled.sock。不要单独 bind mount 这个 socket 文件:tailscaled 重启时会重建 socket,容器可能仍绑定旧文件,导致认证失效。

应同时做两项配置:

  1. 容器只读挂载 /var/run/tailscale 整个运行目录。
  2. 让 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 tailscaled

Ubuntu 的服务单元使用 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 配置。

随后在云安全组开放:

协议公网端口用途
TCP55443DERP TLS 连接与转发
UDP53478STUN 路径探测
TCP22SSH 管理,按需要限制来源

使用高位端口能减少部分默认端口扫描,身份校验仍由证书指纹和 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.conf

Swap 为短时内存尖峰提供缓冲,不会限制进程增长,也不能保证不再 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 节点属性和运行目录设置,并重复认证与转发验证。

参考资料