← 返回文章

从自建 DERP 到 Peer Relay:一次用第一性原理破局的网络排障

为了加速国内 Tailscale 中继并防止被白嫖流量,我自建 DERP 却意外撞上阿里云 CIDR 冲突。在补丁越打越复杂的路上停下来,用第一性原理重新审视真实需求,才发现问题根本不需要被解决。

NetworkingOSCloud Computing
从自建 DERP 到 Peer Relay:一次用第一性原理破局的网络排障

最近在折腾各种 AI agent,家里的工作站、公司的开发机、云上的几台机器散落各处。不管是随时连上去看实时日志、调试环境,还是让机器上的 agent 之间直接打通 RPC 通信,Tailscale 早就成了我的刚需:不需要自己去配复杂的 WireGuard 拓扑或者维护一堆端口映射,自动打洞、身份明确的 mesh 网络用起来确实省心。

但在国内用 Tailscale,大家都有一个共同的痛点:官方的 DERP 中继节点基本都在境外(香港、东京、新加坡等)。只要两端设备的 NAT 稍微复杂一点(比如一端是公司严苛的对称型 NAT,另一端是移动 5G),打洞直连失败就只能退化到中继模式。这时候走官方 DERP,延迟瞬间飙到上百毫秒,日常传点大文件或者交互式敲命令时,卡顿感相当明显。

手头正好有一台闲置的阿里云 ECS,我很自然地动了心思:自己搭一个国内的 DERP 节点,给中继链路提速。

本以为这就是个把容器拉起来、签个证书半小时就能收工的常规操作,没想到后面一脚踩进了一个层层套娃的泥潭。

前提:DERP 绝不能被白嫖

自建 DERP 本身并不复杂,官方仓库就开源了 derper 工具。但我心里很清楚,国内云主机的公网带宽可都是真金白银,防蹭用是第一原则。

DERP 转发的是端到端加密的数据包,服务器本身虽然解不开内容,但只要中继端口完全暴露在公网,全网扫描的爬虫和白嫖党分分钟能把你的出口带宽吃满,月底账单会非常感人。

怎么鉴权?

  • 靠 IP 白名单?移动设备的公网 IP 天天变,总不能随身带个脚本天天去云控制台改安全组;
  • 官方最正统的方案是加上启动参数:derper --verify-clients。

这个参数的实现逻辑很优雅:derper 通过本机的 Unix Domain Socket 直接与宿主机上运行的 tailscaled 交互。当有客户端发起中继握手时,derper 让 tailscaled 去控制面校验对方的公钥身份——只有真正属于自己 Tailnet 的设备,才允许使用中继。

这个方案安全、省心,不需要自己手工分发任何静态密码或 token。但它带来了一个前提条件:这台阿里云 VM 本身,也必须安装并运行一个 tailscaled 守护进程。

我把容器镜像拉下来、配好域名、用 ACME 自动签下证书,把防火墙端口放行。一切顺风顺水,我甚至觉得马上就能连上去测速了。

然后,机器的网络瞬间“暴毙”。

意料之外的 CIDR 冲突

tailscaled 按默认配置装好,一执行 tailscale up,VM 的网络瞬间瘫痪: 终端里随便敲个 curl 或者跑 dnf makecache,全部卡死在域名解析上,报 DNS 超时。

我停掉 tailscaled 服务,网络立刻秒恢复;一拉起服务,系统立刻又变成“半残”。

稍微看一眼路由表和策略规则,根因就浮出水面了——CIDR 撞车了。

Tailscale 默认给所有设备分配的内网虚拟 IP,落在 RFC 6598 规定的运营商级 CGNAT 保留网段:100.64.0.0/10(范围是 100.64.0.0 到 100.127.255.255)。

巧就巧在,阿里云 VPC 内部的基础设施也是用这个保留段! 比如:

  • 阿里云的核心内网 DNS 是 100.100.2.136 和 100.100.2.138;
  • 云服务器的元数据服务(Metadata)是 100.100.100.200。

在 Linux 默认模式下,tailscaled 启动时会在内核创建一张虚拟网卡(tailscale0),并下发策略路由(ip rule 里的 table 52),把所有目标地址为 100.64.0.0/10 的流量通通往 tailscale0 里拽。

这下直接形成了路由黑洞:ECS 内部发出的所有 DNS 解析请求,全部被截胡进了 Tailscale 虚拟网卡,由于 Tailnet 里根本没有这台 DNS,数据包直接丢入虚空。DNS 断了,证书自动续期、包管理器更新、云监控全线罢工。

我本可以死磕到底

看清了是路由冲突,当时我脑子里冒出的第一个念头,就是典型的工程师本能:“兵来将挡,我还能被几条路由规则难住?”

以 Linux 网络栈的灵活性,要想把这套冲突按下去,多的是折腾的手段:

  1. 先改 DNS:把 /etc/resolv.conf 里的内网 DNS 换成公共 DNS(如 223.5.5.5)。外网是能解析了,但阿里云很多内网镜像源和服务域名,通过公网 DNS 解析出来的还是内网 CGNAT 地址,治标不治本;
  2. 手搓明细 Host Route:既然 100.64.0.0/10 被拦截,那我就在系统路由表里加静态明细路由,把 100.100.2.136/32 的优先级提到 tailscale0 之前,强行绑定到物理网卡 eth0;
  3. 折腾 Netfilter 与策略路由:加了明细路由后,发现某些特定协议的包依然被 Tailscale 的 iptables 规则拦截,我又去翻 tailscale set --netfilter-mode=nodivert,甚至考虑用 iptables 给阿里云内部流量打 fwmark,再搞一套独立的多路由表配合策略路由分流。

讲实话,真要顺着这条路硬刚,搞 Network Namespace 隔离也好,把 iptables/nftables 的规则链翻个底朝天也罢,凭着以往积累的网络排障经验,我绝对能把它强行调通。

但改到第三层补丁、正在写自动化维护路由脚本的那一刻,我突然停住了。

看着屏幕上一地鸡毛的路由规则,我心里升起一股强烈的荒谬感:这不就是教科书级的 XY Problem 吗?

  • 原始需求(X):安全、可控地为我的国内设备提供中继加速;
  • 我脑补的手段(Y):为了给 DERP 验身份,必须把这台云主机作为一个常规节点加入 Tailnet;
  • 深陷的泥潭:为了解决手段 Y 带来的副作用(阿里云 CIDR 冲突),我一头扎进系统路由表和 iptables 里,花了整整大半天去修补那些本不该存在的漏洞。

我手里真正忙活得满头大汗的事情,早就和最开始的目标毫无关系了。

这套靠人工搓出来的路由例外表极其脆弱。万一哪天阿里云内部基础设施新增了一个 IP 节点?万一哪天系统升级重置了网卡配置?只要有一处对不上,机器就会莫名其妙断网。

我们很多时候太容易被自己的技术能力绑架了——因为自己有能力死磕解决复杂的问题,就误以为这个问题是必须被解决的,结果在错误的道路上一路狂奔,亲手造出了一个日后连自己都不敢碰的复杂度怪兽。

意料之外的收获:Peer Relay

在反思这套方案时,其实还有个看似省事的退路:放弃官方的 --verify-clients,去用社区魔改的带 token 鉴权的 derper 分支。没有本地校验,机器自然不用跑 tailscaled,冲突立消。

但我很快打消了这个念头。除了不想长期肉身维护第三方分支以外,还有一个更关键的原因:在翻看 Tailscale 最近的连接演进时,我注意到了 Peer Relay。

在 Tailscale 的数据链路中,连接优先级的顺序其实是:

Direct(打洞直连) → Peer Relay(节点中继) → DERP(服务器中继)

很多人只知道 DERP,但实际上 Tailscale 早就支持了 Peer Relay:它允许我们在 Tailnet 里把某台网络条件好的设备(比如这台公网云主机)声明为一个 UDP 中继节点。

相比于 DERP,Peer Relay 的优势实在太大了:

  1. 性能远胜 DERP:DERP 是基于 TCP/TLS(WebSockets)封装的中继,哪怕延迟降下来了,握手和吞吐开销依然很大;而 Peer Relay 是纯 WireGuard UDP 中继,延迟更低、吞吐更接近原生物理网速;
  2. 原生的安全机制:它直接依托于 Tailnet 的 ACL Capability Grant(tailscale.com/cap/relay)授权,只有被授权的设备才能走它中继,根本不需要对外暴露容易被扫描的公共端口。

无论从延迟体验还是架构干净程度来看,Peer Relay 才是国内中继真正的主力解法。

但要用 Peer Relay,这台云主机就必须作为一个合法的节点加入 Tailnet。换句话说,tailscaled 必须跑起来,绕是绕不过去的。

转折:用第一性原理回归底层事实

既然必须跑 tailscaled,但我又不想继续在路由表里死磕,那破局点在哪?

既然意识到了自己掉进了 XY Problem 的陷阱,那就必须把所有预设全给拆了。我逼自己停下来,用所谓**“第一性原理”**把这层层外壳剥掉,退回到最底层的网络事实去推导:

这台云主机上的操作系统,真的需要“进入”Tailscale 网络吗?

我们一条条看底层事实:

  • 我需要在这台机器上通过 Tailscale 的虚拟 IP 去 SSH 它吗?不需要,它本来就有公网 IP 和密钥,正常的运维走公网足够;
  • 这台主机上的应用需要主动去访问我家里的 NAS 或者内网机器吗?完全不需要;
  • 它是作为 Subnet Router(子网路由)或者 Exit Node(出口节点)存在吗?也不是。

抽丝剥茧之后,本质一下子浮出水面:

  • 中继的本质是什么? 是一个暴露在公网上的后台进程,负责接收两端发来的加密数据包,并原封不动地转发出去;
  • 鉴权的本质是什么? 是这个进程能拿到本地的身份令牌,证明自己是可信的,并帮控制面做一下校验。

我从头到尾需要的,只是让 tailscaled 作为一个后台进程存在,负责连上控制面获取身份、帮其他设备转发 UDP 报文(Peer Relay),并在本地暴露一个 socket 供 derper 校验身份。

这台机器本身的 Linux 操作系统、它的业务进程、它的 DNS 和系统路由表,压根就不需要知道 Tailscale 的存在!

之前的冲突之所以发生,纯粹是因为我们在认知上把它当成了一台普通的“办公终端”,理所当然地觉得“装了 Tailscale 就必须建网卡、改路由、接管流量”。

一旦用第一性原理把“操作系统必须进网”这个伪前提剥离掉,真正的解法就自己跳出来了:只要让 tailscaled 纯在用户态运行,完全不碰内核网络栈就行了。

一行配置破局:Userspace Networking

Tailscale 官方其实早就为无 root 权限或容器环境提供了现成的原生参数:--tun=userspace-networking。

在 Linux 上,配置起来极其简单。如果是用 systemd 管理 tailscaled,编辑 /etc/default/tailscaled(Debian/Ubuntu)或对应的配置文件,加上启动参数:

FLAGS="--tun=userspace-networking"

或者手动启动时直接带上:

tailscaled --tun=userspace-networking

重启服务的一瞬间,困扰了我大半天的所有问题全部迎刃而解:

  • 不建虚拟网卡:内核里完全没有 tailscale0;
  • 不碰路由表与 iptables:宿主机的系统路由原封不动,没有多余的策略路由拦截;
  • 阿里云内部网络畅通无阻:系统 DNS(100.100.2.136)、Metadata、内网软件源继续走原生物理通道,CIDR 冲突直接物理蒸发;
  • 核心功能毫发无损:tailscaled 依然在用户态保持控制面长连接,Peer Relay 的 UDP 转发正常工作,本机的 /var/run/tailscale/tailscaled.sock 也依然稳稳为 derper 提供鉴权。

大半天写下的那些花里胡哨的路由补丁,在一行启动参数面前全成了多余的笑话。

最终落地与架构

理顺了网络边界之后,整台机器的职责变得异常清爽。

既然 Userspace 模式下的 tailscaled 既稳定又不扰民,我索性把 Peer Relay 和自建 DERP 全部配齐,打造“双保险”:

  • 日常主力(Peer Relay):设备之间打不通直连时,优先通过云主机的 Peer Relay 进行高速 WireGuard UDP 中继;
  • 兜底方案(DERP):给个别尚不支持 Peer Relay 的客户端(比如手机上的某些第三方工具如 Surge)做保底,或者在极端苛刻的网络下走 TCP/TLS 兜底。

在 Tailscale 控制台的 ACL 策略中,为中继节点添加能力授权:

{
  "tagOwners": {
    "tag:relay": ["autogroup:admin"]
  },
  "grants": [
    {
      "src": ["autogroup:member"],
      "dst": ["tag:relay"],
      "app": {
        "tailscale.com/cap/relay": [{}]
      }
    }
  ]
}

在云主机端为节点打标启动:

tailscale up --advertise-tags=tag:relay --accept-routes=false

同时把带 --verify-clients 的 derper 拉起来:

derper \
  -hostname derp.yourdomain.com \
  -certmode manual \
  -certdir /etc/derper/certs \
  -verify-clients

整台机器的分工极度清晰:

云服务器 (ECS)
├── tailscaled (Userspace 模式)  → 维护 Tailnet 身份 + 提供 Peer Relay (UDP)
│                                → 暴露本地 socket 供客户端鉴权
└── derper (Container)           → 承接传统 DERP (TCP/TLS) + STUN (UDP)

在客户端执行 tailscale ping <peer>,终端里清晰地显示两端在穿透失败后无缝走上了本地云服务器的 Peer Relay,延迟直接从境外官方 DERP 的 150ms 压到了 20ms 以内,日常操作顺滑如内网。

写在最后

这次排障折腾下来,最大的感悟倒不是学会了 --tun=userspace-networking 这个参数,而是让我重新审视了工程师面对问题时的“思维惯性”。

在遇到 CIDR 冲突、路由拦截的那一刻,凭着以往积累的网络底层经验,我完全有能力写出一套极为精妙的 iptables 规则和多路由表分流策略把它搞定。我们往往会下意识地享受这种“通过硬刚复杂机制把问题强行摆平”的成就感。

但我差点就陷了进去——为了炫技式地解决一个由“错误前提”引发的次生问题,去亲手造出一个日后连自己都懒得维护的复杂度怪兽。

死磕技术细节固然是工程师的基本功,但更重要、也往往更难的一步,是在投入大量精力之前先踩一脚刹车:我们手里正在死磕的这个难题,真的是必须被解决的吗?

当你发现这台机器打从一开始就不需要进入内网的三层网络栈时,那个看起来极其棘手的 CIDR 冲突,其实连一行路由规则都不需要写。

参考链接