2026-04-24 03:15:24
最近断断续续 Vibe Coding 写了一个小工具,叫 Runnel。因为我之前用的两个机场都跪了 (虽然后来又逐渐恢复了),于是萌生了自己写个工具来翻墙的想法,顺便把一些网络相关的东西重新摸一遍。
Runnel 有“小河流”的意思,我觉得还挺贴切:不是要做一个很宏大的网络基础设施,就是在本机和远端之间挖一条能用、能看、能调试的小通道。
先让 AI 快速撸了一个车 native-http 版本的,然后又加了多路复用的 native-mux 模式。后来又想到一个同事说他一直用自己写的代理工具叫做 daze,所以立马让 AI 用 Rust 重写了一份跑了起来。
于是它现在支持几种模式:native-http、native-mux、几个 daze 风格的模式,以及后来主要投入精力做的 wg 模式。
前面这些更像传统 SOCKS 代理,应用明确连到本地 SOCKS 端口,然后 Runnel 把流量转到远端。WG 模式不一样,它走 TUN 设备和 WireGuard 风格的 UDP 包,更像日常会用的全局 VPN。
真正让我把重心转到 WG 模式的原因也很普通:SOCKS 在浏览器里挺好用,但到系统级流量、命令行工具、各种后台服务时就没那么舒服了。macOS 下总会碰到有些软件不吃系统代理,有些软件自己做 DNS,有些东西干脆只看路由表。最后还是得上 TUN。
WG 模式底层用的是 Cloudflare 的 boringtun,目标很简单:
听起来像配置一遍 WireGuard 而已,但自己写以后就会发现,大部分时间并不在加密和解密上,而是在处理操作系统。
Linux 上需要 /dev/net/tun、ip、sysctl、iptables。macOS 上是 ifconfig、route、networksetup。命令不复杂,但出错方式很多。例如服务端没停干净,再起一次就会卡在:
ip address add 10.8.0.1 peer 10.8.0.2 dev runnel-wg0
因为设备还在,地址也还在。再比如 macOS 上如果之前的 tunnel 没清理干净,WG endpoint 的路由可能会被系统解析到某个 utun 上。这个时候 client 还没真正起来,但发往 server endpoint 的 UDP 包已经准备钻进旧 tunnel,最后当然连不上。
这类问题很烦,但它们也逼着我把启动阶段做得更啰嗦一点。现在 Runnel 会做 preflight,会打印 hook,会在 client 装路由之前先做一次短 handshake probe。key、endpoint 或两端配置有问题时,最好在启动阶段直接报错,而不是把系统路由改完,然后留给用户一个没有响应的黑盒。
我写这类工具最大的感受是:网络工具的“好用”,很多时候不是吞吐量高一点,而是坏的时候知道坏在哪里。
WG 的配置很容易写错,尤其是 private key、peer public key、tunnel IP 这种互相交叉的字段。手写一次就够让人烦了,所以后来加了:
runnel wg-config --server-endpoint SERVER-IP:1443 > runnel.yaml
这个命令会一次生成 client 和 server 两边配置。两台机器用同一个 YAML,只是启动时分别执行:
sudo runnel --config runnel.yaml server
sudo runnel --config runnel.yaml client --tui
它减少了很多“我复制错了吗”的问题,也方便把一些默认值放进去。比如现在生成配置默认带 adblock,默认启用 DNS capture,也会默认带上 noise engine 和 mask obfs:
client:
wg:
engine: noise
obfs: mask
obfs_padding_min: 8
obfs_padding_max: 96
obfs_handshake_padding: 256
obfs_response_padding: 192
这些东西不一定适合所有人,但它们很适合我自己的使用场景:默认配置应该尽量接近日常可用,而不是只给一个裸 tunnel。
我原来只想做 IP 规则,例如:
client:
ip_rules:
direct:
- "10.*"
- "192.168.*"
这个在 WG 模式下比较直观,direct 就是给这些 IP 加本地直连 route,不让它们进 tunnel。后来很快又想要域名规则:
client:
domain_rules:
direct:
- "*.baidu.com"
- "*.cn"
block:
- "*.xxx.com"
问题是 WG 看到的是 IP 包,不知道这个连接原本访问的是 github.com 还是别的什么域名。能抓到域名的地方只有 DNS。
所以现在 WG 的 domain rules 是 DNS 驱动的。client 会把本机 DNS 指到 127.0.0.1:53,Runnel 在本地做一个很小的 DNS forwarder。它看到查询 www.xxx.com,发现命中 direct 规则,就等上游 DNS 返回 IP,然后动态加一条 host route,让这个 IP 走本地网关。
这个方案很实用,但并不完美。第一次访问一个域名时,如果系统还没有拿到解析结果,它可能已经先走了 tunnel。DoH、DoT、浏览器缓存、直接访问 IP,也都绕过这条逻辑。CDN 共享 IP 还会带来另一个问题:你为了某个域名加的直连 route,可能影响另一个解析到同一 IP 的域名。
这些限制没法假装不存在,所以文档里也写清楚了。能解决 80% 的日常问题就很好,剩下 20% 要靠更底层的 packet inspection 或系统扩展,那又是另外一个坑。
后来我把 adblock-rust 集成进来了。原因很简单:既然 WG 模式已经有 DNS capture,那 block 域名就可以顺手做掉。
这里我比较在意优先级。最后定下来是:
domain_rules.block
用户手写规则永远应该赢过订阅规则。否则哪天 EasyPrivacy 把一个你需要的域名拦了,就会很难受。
性能上我一开始也有点担心。毕竟 tunnel 里每个包都可能很密集,如果每个请求、每个 packet 都去跑 adblock,那肯定不行。最后实现是 DNS 级别的:只有新的域名查询会进入规则匹配,订阅会缓存在本地,域名决策也有内存缓存。实际看下来,这个成本可以接受。
有次 TUI 里看到 p.data.cctv.com 被 block,我还专门去查了一下。它不是用户规则拦的,是订阅规则命中的。
Runnel 有一个 TUI,不是为了好看,主要是为了少开几个终端。
WG 模式刚开始出问题时,我经常要同时看:
这些信息散在 log、netstat、route、tcpdump 里会很痛苦。放到一个 TUI 里之后,很多问题一眼能看出方向。
比如看到 HANDSHAKE(REKEY_TIMEOUT) 连续刷,基本就是两边没有成功握手,先别怀疑浏览器。看到 www.qq.com pending,就知道 DNS query 被捕获了,但还没拿到可用于直连的 IP。
我给所有模式都加了一个 mode_perf benchmark。小请求跑 1000 次,大响应跑 8 个 1MiB 下载。非 WG 模式走 SOCKS path,WG 模式会真正拉起 child process,创建 TUN 设备,然后从 tunnel IP 发 HTTP 请求。
本机测试的结果挺有意思:
native-mux 小请求大约 3300 req/s
daze-czar 小请求大约 3400 req/s
wg 小请求大约 2500 req/s
但大响应吞吐 WG 看起来很低,只有几十 MiB/s。刚看到这个数字我也有点怀疑,毕竟实际用起来 WG 并不觉得卡。后来想想也正常:这个 benchmark 是 localhost 上的端到端测试,WG 走真实 TUN 设备,会经过内核路由、用户态加解密、UDP socket、NAT 这些路径;而很多 SOCKS 模式本质上就是本机 TCP 流转发,少了不少系统边界。
日常使用的“流畅”也不完全由大文件吞吐决定。连接建立、DNS、浏览器并发、小请求延迟,这些东西更容易影响体感。WG 在小请求上的数据并不差,而且系统级接管以后,很多应用不用再单独配置代理,反而省心。
所以 benchmark 还是要有,但不能只看一个数。尤其是代理/VPN 这种东西,不同 workload 差别太大。
标准的 WireGuard 长度非常固定,所以基本上会容易检测出来。我在最初使用单纯的 WG 用一段时间开始丢包,于是我开始琢磨如何加噪音来抗审查。
于是给 WG mode 加了 noise engine。默认的 device engine 是让 boringtun 自己管理 WireGuard 设备和 UAPI socket,比较标准。noise engine 则是 Runnel 自己跑 TUN/UDP loop,直接用 boringtun::noise::Tunn 做加解密。
这样做的好处是 transport 变得更可控。比如可以在 WireGuard UDP 包外面包一层 mask,把包头和长度藏一下,再加一些 padding。现在的 obfs: mask 做的就是这个方向。
我不想把它说成什么“抗审查神器”,但从我平稳使用的一周结果看这样的混淆对于个人使用已经足够。现实网络环境要麻烦得多,包长、时序、UDP 行为、重传模式,都可能暴露特征。
我也看了一些类似工具的方向,比如 AmneziaVPN、Nym 把 QUIC 和 WG 组合起来的思路,Runnel 现在还只是很早期的版本,但 noise engine 让后面继续试 QUIC、padding profile、甚至更麻烦的 packet shaping 都方便了一些。
Runnel 还没有到我觉得很完整的阶段,还有些细节可以继续打磨,比如 IPv6 和双栈还需要更好的 schema,macOS 上更细粒度的 app split tunneling 也不是简单路由表能解决的。
但目前这个状态我已经挺满意了,它不是一个周末玩具了,至少已经变成了我自己会认真拿来使用、测试、折腾网络问题的工具。
这大概就是个人项目最舒服的地方:先让自己用得爽一点,遇到问题再修复。我的日常使用场景是 macOS(client) + Linux(server),其他路径目前只是在单元测试中有覆盖,也许其他实际场景会碰到一些配置问题,欢迎 PR。
2026-02-13 00:55:52
今天跑 Rust 编译器测试的时候又发现非常地慢,CPU 资源根本无法利用起来,我记得几年前碰到过这个问题,当时我写了篇文章分享出来,并发现很多人都有同样的困扰。
而我今天碰到的这个问题虽然现象一样,但解决方法又不同了。我不确定是 macOS 系统更新,亦或是我更新了 VS Code 造成的。
复现脚本很简单,循环创建随机命名的 shell 脚本,然后对比首次和再次执行的耗时:
#!/bin/bash
rm -rf /tmp/speed_test
mkdir -p /tmp/speed_test
for i in {1..10}; do
FILENAME=$(openssl rand -hex 10)
echo $'#!/bin/sh\necho Hello' > "/tmp/speed_test/$FILENAME.sh"
chmod a+x "/tmp/speed_test/$FILENAME.sh"
FILE="/tmp/speed_test/$FILENAME.sh"
first=$(TIMEFORMAT="%R"; (time $FILE > /dev/null) 2>&1)
second=$(TIMEFORMAT="%R"; (time $FILE > /dev/null) 2>&1)
echo "第一次: $first 第二次: $second"
done
在 VS Code 终端的输出:
第一次: 0.525 第二次: 0.007
第一次: 0.290 第二次: 0.009
第一次: 0.280 第二次: 0.007
第一次: 0.272 第二次: 0.008
第一次: 0.307 第二次: 0.008
...
差距大概 30-50 倍。换到 Warp 终端跑同一个脚本,两次都在 0.006s 左右。
应该不是我上篇文章提到的 SIP 问题,我确定 System Settings → Privacy & Security → Developer Tools 中已经加入了 VS Code。
看起来也不像是文件系统缓存的原因,因为 0.2-0.5 秒远超磁盘缓存的量级。用 log show 看了下系统日志:
log show --predicate 'subsystem == "com.apple.syspolicy.exec"' --last 2m --style compact
输出大量这样的记录:
GK performScan: PST: (path: 8d0e4c2de41c3e77), (team: (null)), (id: (null)), (bundle_id: (null))
Error Domain=NSOSStatusErrorDomain Code=-67062
GK evaluateScanResult: 2, PST: (path: 8d0e4c2de41c3e77), ... (bundle_id: NOT_A_BUNDLE), 0, 0, 1, 0, 7, 7, 0
从日志上看每次执行新文件 syspolicyd 都会做一次 GK performScan。这就是 macOS 的 Gatekeeper 安全扫描——对首次执行的新可执行文件做代码签名验证和恶意软件检查。扫描结果会被缓存,所以同一个文件第二次执行就快了。
进一步验证:我们把测试脚本里改成 (time /bin/sh $FILE > /dev/null) 2>&1,这样就是直接通过 sh 来执行:
直接执行 ./script.sh → 0.248s (触发 execve → Gatekeeper 扫描)
/bin/sh ./script.sh → 0.006s (只是让 /bin/sh 读取文件,不触发安全扫描)
原因确认了。当用 ./script.sh 执行时,内核的 execve 系统调用会触发 AppleSystemPolicy.kext 中的 MACF hook (mpo_proc_notify_exec_complete),通知 syspolicyd 进行评估。而 /bin/sh script.sh 只是让已受信的 /bin/sh 进程读取文件内容来解释执行,不触发 execve 的安全检查路径。
接着试了 System Settings → Privacy & Security → Full Disk Access,给 VS Code 完全磁盘访问权限。重启 VS Code,再跑脚本:
第一次: 0.005 第二次: 0.005
第一次: 0.005 第二次: 0.005
第一次: 0.006 第二次: 0.006
...
问题消失了。syspolicyd 日志中的 performScan 也不再出现。
Full Disk Access (FDA) 在 macOS 的 TCC (Transparency, Consent, and Control) 框架中对应的是 kTCCServiceSystemPolicyAllFiles 权限。这个权限的含义远超“磁盘访问“——它实际上是 TCC 框架中最高级别的信任授权。
macOS 会追踪每个进程的 responsible process(负责进程)。在 VS Code 终端中敲的命令,它的 responsible process 是 VS Code 本身。当 AppleSystemPolicy.kext 的 MACF hook 拦截到 execve 后,会检查 responsible process 的信任级别。拥有 FDA 授权的进程被识别为高信任来源,syspolicyd 会走快速路径,跳过完整的 Gatekeeper 扫描。
而 Warp 这些原生终端,因为我已经加入系统默认信任的开发工具列表,所以它们派生的子进程一开始就不会触发完整扫描。
需要说明:Apple 没有公开文档化这个具体流程。上面的描述来自实验推断和社区逆向分析,不是官方说法。
发现 FDA 有效之后,我尝试反向验证:把 VS Code 从 FDA 列表中移除,重启 VS Code,再跑脚本。
结果:仍然很快。问题没有复现。
syspolicyd 的扫描评估结果存储在 /var/db/SystemPolicyConfiguration/ExecPolicy 这个 SQLite 数据库中(35MB),同时 AppleSystemPolicy.kext 在内核中维护了一个运行时缓存:
$ sysctl security.mac.asp.stats.cache_entry_count
security.mac.asp.stats.cache_entry_count: 4700
也就是说,当 VS Code 拥有 FDA 时,它被评估为可信 responsible process,这个信任结果被持久化了。移除 FDA 后,历史记录并不会被清除。macOS 的安全评估系统是“学习型“的——它记住过去的信任决策。
要彻底重现原来的问题,可能需要重启 Mac 清除内核缓存,或者更极端地清理 ExecPolicy 数据库。
如果你也遇到类似的问题——新编译的程序、新创建的脚本首次执行莫名其妙地慢,可以检查一下是不是 Gatekeeper 的锅:
# 查看最近的 syspolicyd 扫描记录
log show --predicate 'subsystem == "com.apple.syspolicy.exec"' --last 5m --style compact | grep performScan
解决方案按排序:
2026-01-31 07:00:00
月底升级了 Copilot Pro+,月初额度重置,这几天可以放开用,想到什么就 vibe 一把。
我的博客跑在 Hexo 上很多年了。其实没什么大问题,就是每次看到那几百 MB 的 node_modules,心里总有点膈应——生成几百个静态 HTML,真的需要这么多依赖吗?但迁移到别的博客系统又懒得折腾,所以一直拖着。
这次干脆试试:能不能用 AI 一个下午撸一个 Rust 版的 Hexo?我的目标比较简单:生成跟原来一样的静态文件,兼容我现在用的主题就行。
我用的是 OpenCode + Opus 4.5。陆陆续续聊了一下午,产出了 hexo-rs。能用,但还有些边边角角的问题。
Vibe Coding 的工具和体会以后再写,这篇主要聊 hexo-rs 的实现和踩过的坑。
Hexo 主题基本都用 EJS 模板——就是把 JavaScript 嵌到 HTML 里,跟 PHP 差不多。
用 QuickJS 跑 JS,通过 quick-js crate 调用。好处是不用依赖 Node.js,坏处是 Windows 上编不过(libquickjs-sys 挂了),所以暂时只支持 Linux 和 macOS。
Markdown 用 pulldown-cmark,代码高亮用 syntect,本地服务器用 axum。都是常规选择,没什么特别的。
这个 bug 藏得很深。生成 tag 和 category 页面时,一开始用 HashMap 存文章分组:
let mut tags: HashMap<String, Vec<&Post>> = HashMap::new();
HashMap 迭代顺序不确定,每次生成的 HTML 可能不一样。页面看着没问题,但 diff 一下就发现乱了。改成 BTreeMap 就好了:
let mut tags: BTreeMap<String, Vec<&Post>> = BTreeMap::new();
Hexo 有一堆 helper 函数:url_for、css、js、date 之类的。都得在 Rust 里实现一遍,然后塞进 QuickJS。
最烦的是 date。Hexo 用 Moment.js 的格式(YYYY-MM-DD),Rust 的 chrono 用 strftime(%Y-%m-%d)。得写个转换函数,挺无聊的活。
EJS 的 partial 可以套娃,A 引用 B,B 又引用 C,变量还得一层层传下去。搞了个作用域栈,进 partial 压栈,出来弹栈。不难,但容易写错。
代码 100% 是 AI 写的。我干的事:描述需求、review 代码、把报错贴给它让它改、偶尔拍板选方案。
像 EJS 模板引擎这种东西,自己从头写估计得半天,AI 几分钟就吐出来了。
但 AI 也挺蠢的:
但 AI 又确实非常强,我想到应该使用现在线上的 catcoding.me 来和新生成的内容一一对比,然后它就呼啦啦地一通操作把问题都找出来了,自己修改完。
cargo binstall hexo-rs # 或 cargo install hexo-rs
hexo-rs generate # 生成静态文件
hexo-rs server # 本地预览
hexo-rs clean # 清理
hexo-rs new "标题"
不支持 Hexo 插件,不支持 Stylus 编译(.styl 文件得先用 Node 编译好),Windows 也不行。
简单的博客应该够用。复杂主题可能会有兼容问题。
代码在这:github.com/chenyukang/hexo-rs
用 Hexo 的可以试试。有问题提 issue,我让 AI 来修 :)
这篇文章到底是人写的,还是 AI 写的?
Update 2026-02-13: 鉴于使用 quickjs 对于我来说还是太重了,我后续又做了一些改动,把 ejs template 换成了 tare template,这样就把 vexo 模板直接放在了 Rust 项目里,所以目前 hexo-rs 就是这个博客的 generator 了。
2025-12-10 01:05:59
I just got back from CKCon in beautiful Chiang Mai 🌴, where I gave a talk on the Fiber Network. To help everyone wrap their heads around how Fiber (CKB’s Lightning Network) actually moves assets, I hacked a visual simulation with AI.
To my surprise, people didn’t just understand it—they loved it! 🎉
Here is the “too long; didn’t read” version. But first, go ahead and play with the dots yourself, 👉 Play the Simulation: fiber-simulation

We all love Layer 1 blockchains like Bitcoin or CKB for their security, but let’s be honest: they aren’t exactly built for speed.
Every transaction has to be shouted out to the entire world and written down by thousands of nodes. On CKB, you’re waiting about 8 seconds for a block; on Bitcoin, it’s 10 minutes! Plus, the fees can get nasty if you’re just trying to buy a coffee. ☕️
So, how do we fix this?
The Lightning Network is a scalable, low-fee, and instant micro-payment solution for P2P payments.
The secret sauce isn’t actually new. Even Satoshi Nakamoto hinted at this “high-frequency” magic in an early email:
Intermediate transactions do not need to be broadcast. Only the final outcome gets recorded by the network.
A Lightning Network consists of Peers and Channels. A peer can send, receive, or forward a payment. A Channel is used for communication between two Peers.

Imagine you and a friend want to trade money back and forth quickly:
Everything in the middle? That’s off-chain magic. ✨
Now, if Fiber was just about paying your direct neighbor, it would be boring. The real power comes from the Network.

This means Alice can pay Bob even if they don’t have a direct channel between them. The payment can travel through one or more intermediate nodes. As long as there is a path with enough liquidity, the payment will reach its destination instantly.
All data is wrapped in Onion Packets (yes, like layers of an onion). The nodes in the middle serve as couriers, but they are blindfolded:
They simply follow a basic rule: they forward the Hash Time Lock, and if the payment succeeds, they earn a tiny fee for their trouble. Easy peasy.
The “Not So Easy” Part 😅
While the idea is simple, building it is… well, an engineering adventure. We’re dealing with cryptography, heavy concurrency, routing algorithms, and a whole jungle of edge cases. But hey, that’s what makes it fun!
We’ve poured the last two years into building Fiber, and I’m proud to say it’s finally GA ready.
If you want to geek out on the details, check these out:
Here is the full presentation from my talk: CKB Fiber Network Engineering Updates
2025-11-26 18:15:38
越来越多的开发者开始使用 LLM 等 AI 工具,过去半年我看到不少相关讨论:有人非常反感使用 LLM 工具,有人保持中立,但确实有相当数量的 AI 生成 PR 给开源项目维护者带来了负担和困扰。
现在出现了一些新苗头,比如 GitHub 账号也开始“养号”。我推测大概有以下几个原因:
2023–2024 年类似的情况比较多,有些加密货币项目会根据开发者的 GitHub 公开提交记录进行空投。如果一个账号给项目方关注的项目提过 PR,就更容易获得。例如 Rust 在区块链领域用得比较多,所以 Rust compiler 项目是比较容易获得空投的。我自己也因为一些开源记录拿到过空投,当时兑换了 1 万多人民币。我看到不少 Rust 社区维护者也得到了空投,不过他们对加密货币普遍不感兴趣;也有个别人因此换到了不少钱,觉得很惊讶。有同事给以太坊提过几个 PR,他的空投价值大约 15 万人民币,因此还出现了有人收购 GitHub 空投资格的情况。
但我认为这只是短期现象。现在再为了空投去养号是否还有机会?我不敢确定。因为“养号”的特征很明显,其实很好自动识别。而且到了 2025 年类似空投已经很少了,即使有,也会要求复杂的钱包交互,不是币圈的人通常不会折腾这些。
很多开发人员都知道,一个拿得出手的 GitHub 账号应该会对找工作有帮助。但我对此保持怀疑,因为养出来的 GitHub 账号一眼就能看出,从面试官的角度,我认为加上一个这样的 GitHub 账号到简历里是减分项。
总之,如果只是为了以上两种目的去“养号”,我都建议停手,因为这通常是费时费力但得不偿失的事情。
另外一部分人是真的想参与开源项目,他们可能认为使用 LLM 能降低难度。
现在的 AI 工具确实比以前更强大了。你可以把一个 GitHub issue 给它,稍微写点 prompt,AI 就能自动生成 PR,甚至自动发 PR。但这种方式通常会忽略一些开源项目本身的贡献约定,从而导致 PR 一发出来维护者就知道这人肯定连 contribution guide 都没看过,这样就会直接关闭掉这个 PR,这有个典型的例子。
AI 的确比我原本想象的好用很多,我在日常开发中也会使用,但主要把它当成增强版搜索引擎或自动化工具。例如我会让 AI 帮我做一些自动化流程:我有一个 prompt 模板,只需要给一个 issue 号,LLM 就能帮我解析问题,把相关的 bug 重现代码放到测试目录,创建对应的 Git branch,尝试在本地重现问题,然后从 backtrace 定位可疑代码。这确实省了我不少时间。但这建立在我自己按这个流程做过很多遍,能找出一套比较稳定的方法。
正如我之前说的,如果你想用好 AI,你必须具备项目的 domain knowledge,才能判断 AI 有没有“骗你”。
在 Rust compiler 项目里,目前 LLM 生成的 PR 基本只有 typo fix 之类的会偶尔被接受。只要涉及稍微复杂一些的代码修改,一眼就能看出不是人写的。
如果真的想参与开源项目,最好的方式还是从项目中简单的 issues 开始。如果不懂就多问,多看文档和代码。每个人都是从新手阶段慢慢走过来的,维护者一般对真心想参与的贡献者会更有耐心。
即使用 LLM 生成代码,我们依然要逐行 review,确保正确、可维护、简洁。如果你丢一堆机器生成的代码,让 maintainer 帮你审核,这会引起极大的反感。
比如这位开发者,在 maintainer 审核后对代码发出质疑的时候也承认是 AI 写的代码:

建议大家可以去看看上面那个 PR 里的讨论,我觉得有些评论挺有价值。OCaml 的维护者 gasche 表达的观点很明确:
The fact that you were able to generate large amount of code that passes test is interesting, but that’s only 20% of the work, the other 80% are to get the feature discussed, reviewed and integrated, and this work will be paid by you and others. But you only focus on the initial writing phase and you personal success, over-communicate on this, and do not appear to realize that this has very real costs on others.
在多人协作的开源项目中,稍微复杂一点的功能,写代码其实只占很小一部分,更多的是协作与讨论。一个 PR 是否能 merge,还要考虑长期维护成本。
另外,LLM 生成的代码其实是非常容易检测的,比如现在就有类似的工具可以以比较高的准确度判断代码是否是 AI 写的: AI Code Detector by Span
还有一些开发者 (尤其是非英语母语者),他们可能对自己的英语不够自信,所以使用 LLM 来帮忙写 PR description 和 comments。有的开发者就是偷懒,认为 LLM 总结的即全面又好。但从维护者的角度来说,这是不友好的,因为 LLM 生成的内容过于冗长:
The comments left by you are significantly too verbose. While being detailed is good, please be respectful of reviewer time and avoid verbose text that mostly doesn’t convey any useful content.
在 Rust maintainer channel 里也讨论过这点,看起来很多人是反感读 LLM 生成的东西的,大家期待的鲜活的人类讨论,而不是机器生成的文字。
其实英语稍微差点的开发者,只要写的内容不是过于离谱,其他开发者也能理解,不用太在意 typo 之类的错误,因为人的大脑纠正的功能过于强大。后来我在 rustc-dev-guide 上加了这么一段:
If you’re not a native English speaker and feel unsure about writing, try using a translator to help. But avoid using LLM tools that generate long, complex words. In daily teamwork, simple and clear words are best for easy understanding. Even small typos or grammar mistakes can make you seem more human, and people connect better with humans.
AI 工具在开源项目中的过度尝试,只会让更多人反感,比如 zig 项目明确表明:
No LLMs for issues. No LLMs for patches / pull requests. No LLMs for comments on the bug tracker, including translation.
我不知道未来会怎样。也许 AI 工具最终会更智能。但至少现在,它还处于一个尴尬的中间地带:用得好能帮你节省时间,用不好反而不如不用。
2025-11-20 15:07:23
这两天都在讨论 Cloudflare 的安全事故 Cloudflare outage on November 18, 2025,我也写点自己的想法。
这个事故当然引起的范围特别广,我当时正在用 ChatGPT,突然再打开总是提示正在加载,我还以为是自己的 VPN 出了问题,第二天起来才知道 Cloudflare 跪了好久。
没多久 Cloudflare 就发出来了一个非常详细的事故说明。我对里面的场景非常熟悉,因为我之前因为类似的原因把大疆的大部分流量都给搞挂了,具体请看谈谈工作中的犯错中的配置错误。
这次事故里 Cloudflare 给出了一段 Rust 代码,所以讨论自然会集中在 Rust 上。但把事故归咎于 Rust 本身就不太合理。从他们的场景来看和我之前在 Kong 上做流量分发是非常类似的,无非是这里他们使用了机器学习的技术来判断一个流量是否为恶意请求,而文中所说的 features 文件是训练好的模型数据。
根本原因是数据库的权限更改,导致查询出来的 features 是有重复的,size 变成期望的两倍。而这个错误的配置通过自动同步机制会同步到全球各个节点。每个节点会有一个 bot 模块,根据 features 去计算是否拦截请求,可以想象这是个典型的机器学习分类问题,比如带有什么特征的 HTTP agent、或者是请求的 payload 之类的这些特征综合考虑来计算。这个 Bot Management具体内容可以参考其产品说明。
那么如果 features 坏了,这个机器学习模块 bot 能否正常工作?答案是不行的,这点文章已经说明:
Both versions were affected by the issue, although the impact observed was different.
Customers deployed on the new FL2 proxy engine, observed HTTP 5xx errors. Customers on our old proxy engine, known as FL, did not see errors, but bot scores were not generated correctly, resulting in all traffic receiving a bot score of zero. Customers that had rules deployed to block bots would have seen large numbers of false positives.
事故发生的时候新老组件都有同时在运行,两个组件在这种场景下都无法正常工作,只是错误呈现方式不同。这也解释了我当时用 ChatGPT 给出的浏览器错误是一个拦截错误。
所以这里,unwrap 其实已经算是整个错误的最后一环了。试想一下如果不 unwrap 无非是这几种场景:
可以看到这两种情形都差不多,甚至如果按照 fail fast 的策略,日志中会有明显的 500 错误,我不知道 Cloudflare 是否做了错误监控,因为按理来说这种级别的错误是非常明显的,需要立即报警。
很多人都集中讨论在这里的 unwrap:

当然这不是最佳实践,但这时候即使使用 .expect("invalid bots input") 这样的写法也好不到哪里去,同样会 500 错误,只是日志里面多留一条错误信息。因为如果不监控错误码,是没人立即发现问题所在的。
更好的做法是对输入进行严格校验,例如检查特征数量和大小。如果不符合预期,应保留旧配置并拒绝加载新数据,而不是加载到一半才发现尺寸异常,更不应该没有 fallback 机制。
当然这里代码没有完全开源,我们从短短的代码片段无法了解整个项目的场景。
从这个经典的错误我们应该发现的是更高维度的警戒,开发管理和运维上有这些问题:
Rust 过去天天宣传“一旦学会 rust,即便是新手也能写出健壮安全的代码”,而真的出现问题了,又开始指责写代码的人是菜鸟。
Cloudflare Rewrote Their Core in Rust, Then Half of the Internet Went Down
这里有点混淆视听,因为 Rust 所说的要解决的安全问题是内存问题,不是逻辑问题。另外,也不是因为重写导致的问题发生。
为什么 Cloudflare 要用 Rust 重写一些关键组件,可以看看他们之前的文章 Incident report on memory leak caused by Cloudflare parser bug
当然我承认在有的公司,可能有的团队完全是为了绩效或者纯个人偏好而发起重写老组件的项目。而更多公司确实是被内存安全问题折磨得怀疑人生才会去重写,像上面文中所说的安全事故是底裤被人扒了,自己还不知道,得让旁观者告诉你才发现。和这次事故的因为工程管理上所做成的安全事故有明显的分别。所以 Rust 所说的安全,是如何避免内存安全。
甚至即使是用了 Rust,一些内存上的问题还是可能因为逻辑上的错误而出现,比如我这个工作中的 PR Avoid duplicated retryable tasks就是避免往队列里加了重复的 task 而造成内存用得越来越多。
这次 Cloudflare 的事故就比如一个司机驾驶沃尔沃,结果碰上了山体滑坡被压死了,这种场景下就是换成任意其他品牌的车都会是一个结果。但如果你跑来说,看吧,沃尔沃号称安全,结果还不是一样死,这叫做虚假宣传。
这不叫虚假宣传,而是你对车有了不切实际的幻想。沃尔沃确实不完美,但每个人都会有不同的选择偏好。正常人理解沃尔沃说的安全是大部分场景下、对比其他车会安全一点,而不是说买了沃尔沃就会长生不老了。
永远记住:No Silver Bullet。
总之,这次 Cloudflare 的事故虽然造成的影响挺大,但这个公司也确实足够公开透明,事故分析写得非常清晰,值得大家学习并反思自己组织上有没有类似的工程问题。