开端
家里那台小米 AC2100,刷了 ImmortalWrt,安安稳稳当了好几年上网工具。最初我想让它跑 xray,因为服务器上的代理用的是 VLESS 协议,xray 算是当下最主流的方案了。可装上之后,路由器直接卡死。一开始我还怀疑是不是 xray 本身有问题,又试了一圈其他支持 VLESS 的程序,结果无一例外全部卡死,而且卡死后路由器必须重启才能恢复。
查了一圈资料,发现当时资源占用最低的代理方案是 trojan 协议,于是把 go-trojan 装上去,果然能正常运行了。我以为这就是完美方案,没想到这只是踩坑的序幕。接下来几天,时不时就上不了网。一开始还以为是运营商的问题,没太在意。后来问题越来越频繁,实在忍不了了,开始把怀疑对象转到路由器本身。
先 ping 了一下路由器网关,果然延迟不对劲,看来是负载过高。但要定位问题,必须找到是哪个进程在搞事情。于是 SSH 进路由器后台,用 top 命令死死盯着看。那会儿 AI 还没这么智能,只能靠肉眼观察。
经过一番不懈努力,终于抓到了 CPU 飙升的瞬间。

从截图看,貌似是系统网络栈卡住了,CPU 的 io 和 sys 都比较高。trojan-go 虽然也在占 CPU,但不像是主因。这下我彻底摸不着头脑了。又过了几天,实在忍无可忍,想着这些天也就多装了一个 trojan-go,干脆死马当活马医,先把它下掉。没想到拿掉之后,路由器不卡了!
又过了些天,我还是不死心,一定要把花 40 块大洋买的路由器利用起来。上网又查了一轮,找到一篇关于 Golang 在 MIPS 设备上兼容性的 GitHub issue——Abnormal memory usage on MIPS platform with GOMIPS=softfloat · Issue #39174 · golang/go。原来 Golang GC 本身也会消耗资源,而 MIPS 硬件资源紧张,就会导致系统 CPU 使用量飙升。看描述跟我的症状相当吻合,于是按照 issue 里的解决思路,用 ulimit -v 32768 限制 Golang 的虚拟内存上限,不让它去 GC 大块内存。同时设置环境变量 GOMAXPROCS=2,约束 CPU 核心数上限。
经过这一通操作,之前路由器 CPU 飙升的毛病居然就好了!
升级
虽然路由器看起来已经"完美"了,但它毕竟跑的是 trojan 协议,手里 VLESS 协议的服务还是用不上。现在已经进入氛围编程时代,以前要花大量时间才能搞定的事情,现在一两天也许就能完成。何不借助氛围编程复刻一个轻量的 VLESS 客户端呢?说干就干——反正不是我干。
我想要的其实很窄:协议只要 REALITY 加 VLESS 加 Vision,服务端已经在用的就是这套。入口只要 SOCKS5 和透明 REDIRECT。内存要在启动时就定死,之后绝不增长,宁可拒绝连接也不申请新内存。
单线程,一个 epoll 循环就够,反正只有一个核。
用 Zig 写
Zig 对嵌入式来说是一门近乎完美的语言:没有 runtime、没有 GC,编译出来的包个头小,启动后内存占用低。而且它对低端 MIPS 设备的支持很完整,语言层面的安全性也比 C 强不少。总之,是当前场景下的最优解。
我给它起名 zray,然后开工。
因为是氛围编程,具体技术细节这里就不展开了。不过有一点需要提一句:由于涉及网络加密相关技术,如果你直接用 Claude Code 来做工具化开发,它会拒绝写代码,认为你在做有风险的事情。单靠修改提示词也糊弄不过去,所以这种事情还是得交给第三方 Agent 来干。写完后交叉编译出目标版本,让 Agent 直接 SSH 到路由器上测试就行。
最终的结果相当喜人:编译出来的二进制只有 800 KB,是 trojan-go(13 MB)的十六分之一,比起 34 MB 的 xray 更是小了将近两个数量级。运行时虚拟内存只有 7 MB,而 trojan-go 要 18 MB,连它的一半都不到。关键是连接池和上游通信的内存都做了控制,总体内存使用是稳定的。


测一下速
速度也不拖后腿。在路由器上经隧道拉 50 MB 文件,HTTPS 流量能跑到 9 到 10 MB/s,zray 只占一个核的 17% 左右;而同一个文件不走隧道直接从 WAN 下载,反而只有 1.4 MB/s——慢的其实是运营商的国际出口。HTTP 流量就差远了,被 CPU 卡在 1.5 MB/s。同一条隧道,为什么差这么多?这得从 Vision 的设计说起。
zray 和服务器之间是一条 TLS 1.3 连接,叫它"外层 TLS"就行。默认情况下,客户端发出的每个字节都要经过这层 AES-GCM 加密后才进隧道,服务器解密后再转发。桌面和手机的 CPU 都有专门的 AES 指令,一条指令就能完成一轮运算,加密 1 GB 数据不费什么力气。可 MT7621 这颗 MIPS 核没有这种指令,只能用普通整数运算一步步"软算"。实测这颗核的极限就在 1.5 MB/s 左右,此时 zray 一个线程已经被占到了 98%。
Vision 的思路是避开二次加密。当你访问的是 HTTPS 网站时,数据本身已经是浏览器和网站之间的 TLS 加密流了,再套一层外层 TLS 等于在加密密文,纯属浪费。所以 Vision 只让 TLS 握手那几个包走外层 TLS,等它识别到内层握手完成、后面全是加密后的应用数据,就通知服务器切到"直拷",之后的数据原样复制进隧道,不再经过 AES-GCM。这时 zray 只是在两个 socket 之间搬运字节,CPU 占用 17%,速度完全由链路决定。
明文 HTTP 就没这个待遇了。它本身没有加密,要是也直拷进隧道,等于把明文直接暴露在公网上,协议不允许。所以 HTTP 流量必须一直留在外层 TLS 里,每个字节都要软算 AES-GCM,速度自然被 CPU 钉死。好在实际使用中绝大多数流量都是 HTTPS,影响不大。不过这也解释了为什么 Vision 这种"握手后直拷"的设计,在低端设备上格外有意义。
这里还踩了一个坑:刚部署完实测只有 1 MB/s,查了半天 zray 的代码一无所获,抓包才发现是路由器内核给 TCP 接收缓冲 设的默认上限只有 930 KB(128 MB 内存的机器就是这么算的),再乘上 190 ms 的 RTT,单条流的理论上限就在 1 MB/s 左右。在 /etc/sysctl.d/ 里把 net.ipv4.tcp_rmem 的上限调到 4 MB,速度直接翻了几倍。跟程序本身没关系,但这种坑不踩一次是真不会知道。

代码放在 GitHub,yunnysunny/zray,MIT 协议。README 有中文版。预编译的二进制覆盖 mipsel、mips、aarch64、armv7、x86_64,都是静态 musl,scp 到路由器就能跑。
如果你家里也有一台老路由器,CPU 也跟我的小米 AC2100 一样是联发科 7621,先别急着扔,它还能发挥余热。
像小米路由器 3G / 4A 千兆版、红米 AC2100、新路由 3(Newifi D2)、斐讯 K2P、网件 R6220 都是联发科 7621 的。
折腾不动了,去 云栈社区 翻翻帖子也能找到不少同好。