13K Star 的 fancyss,一个 Shell 脚本如何在路由器上编排整个翻墙栈
- 采用“编排者不实现”模式,调用上游二进制,用 Shell 脚本编排透明代理全流程
- 配置存储使用华硕固件 dbus 键值存储,简单但缺乏事务和 schema 管理
- DNS 分流通过 chinadns-ng 和 smartdns 实现,但零点定时任务常导致 DNS 进程挂掉
- 节点分流支持最多 16 条规则和 8 个目标,用 TSV 文件存储状态实现热重载
- 项目由 hq450 一人维护 8 年,依赖机场广告盈利,bus factor 为 1
前阵子有个朋友搬家,新居的宽带运营商把全局代理封得死死的,他之前在软路由上跑的 OpenClash 直接失效。折腾了三天没搞定,跑来问我有没有什么不依赖 OpenWrt 的方案。
我问他家里什么路由器,他说华硕 RT-AX86U。我说那你试试 fancyss,装个梅林固件,直接在路由器上跑,不用额外搞软路由。
他半信半疑地用了,然后跑来跟我说,这东西界面看着挺简陋的,但居然稳得离谱,半个月没掉过线。他问我,这玩意儿到底怎么做到的?
我去翻了源码。结论是,fancyss 用的技术栈简陋到让你意外,全是 Shell 脚本,但它解决问题的思路非常聪明。13,653 个 Star,3,241 个 Fork,一个纯 Shell 项目,能做到这个规模,背后的工程逻辑值得拆一拆。
fancyss 到底是什么
一句话定位,fancyss 是一个跑在华硕 Merlin/官改固件路由器上的透明代理插件。
关键词是「透明代理」。你设备上不需要装任何客户端,连上路由器的 Wi-Fi,所有流量自动按规则走代理。手机、电视、IoT 设备,只要连上这个 Wi-Fi,就等于挂了全局代理。
这件事的难点不在于代理协议本身,而在于如何在路由器这个极度受限的环境里,把多种代理协议、DNS 分流、规则匹配、节点切换全部编排起来,还要让普通用户能通过 Web 界面操作。
fancyss 的选择是,自己不实现任何代理协议,全部调用上游二进制。它做的事情是「编排」,不是「实现」。
七平台二进制分发的工程挑战
翻文件树第一眼看到的就是 binaries/ 目录,里面按平台分了七个子目录,bin-arm、bin-hnd、bin-hnd_v8、bin-qca、bin-mtk、bin-ipq32、bin-ipq64。每个目录里放着完全相同的一套二进制,只是编译目标不同。
这套二进制清单包括 xray(代理核心)、chinadns-ng(DNS 分流)、smartdns(DNS 缓存)、naive(NaïveProxy)、tuic-client(TUIC 协议)、ipt2socks(透明代理桥接)、geotool(geoip 查询)、sub-tool(订阅解析)、node-tool(节点测速)、dnsclient(DNS 探测),加上 haveged(熵补充)、jq(JSON 解析)、websocketd(WebSocket 守护)等辅助工具。
你想想看这意味着什么。华硕路由器的 CPU 架构横跨 BCM4708 的 armv7 到 BCM4916 的 armv8,还有高通 IPQ53xx 和联发科 MT798X,内核版本从 2.6.36 到 5.4.281。fancyss 要保证每个平台都有对应编译的二进制,而且这些二进制要能在路由器的嵌入式 Linux 上跑, musl libc、busybox 环境、有限的 JFFS 空间。
在 ss_xray.sh 里有一个平台到架构的映射逻辑,
case $pkg_arch in
arm) ARCH=armv5 ;;
hnd) ARCH=armv7 ;;
hnd_v8) ARCH=arm64 ;;
qca) ARCH=armv7 ;;
mtk) ARCH=arm64 ;;
esac
这段代码决定了从 GitHub 下载哪个架构的 Xray 二进制。build.sh 里的 sync_binary 函数负责把 binaries/ 目录里的对应版本拷贝到各平台的 bin-* 目录。打包时 build.sh 生成 7 个平台各 2 个包(full/lite),共 14 个离线安装包,再加 7 个 debug 包。
full 和 lite 的区别在于是否包含 NaïveProxy 和 TUIC 的二进制。这两个协议体积大,大部分用户用不到,lite 版裁掉它们来省 JFFS 空间。这是个很务实的工程决策,路由器的 JFFS 分区通常只有几十 MB,每个二进制都是 UPX 压缩过的,省一个 NaïveProxy 能省好几 MB。
dbus 键值存储,路由器上的「数据库」
fancyss 最有意思的设计决策是配置存储。它不用文件,不用 SQLite,用的是华硕固件自带的 dbus 键值存储。
在 ss_base.sh 里,配置加载就一行,
eval $(dbus export ss | sed 's/export //' | sed 's/;export /\n/g;' | sed '/ssconf_.*$/d'|sed 's/^/export /' | tr '\n' ';')
这行代码把 dbus 里所有 ss 开头的键值对导出为 shell 变量。比如 ss_basic_mode 存代理模式,ss_basic_type 存节点类型,ss_basic_server 存服务器地址。整个插件的配置状态都存在 dbus 里,脚本之间通过 source ss_base.sh 共享。
说实话,第一次看到这个设计我是有点震惊的。dbus 键值存储没有事务,没有 schema,没有查询语言,就是一个扁平的 key-value。但它的优势也很明显,路由器重启后数据不丢(存在 JFFS 里),不需要额外的数据库进程,shell 脚本可以直接读写。
代价是配置多了之后管理很粗糙。ss_base.sh 里有一堆 unset 清理无用变量,就是在处理键值存储积累的垃圾。ss_node_common.sh 里引入了 fss_detect_storage_schema 和 fss_get_node_field_plain 这些函数,说明项目后期不得不在键值存储之上加了一层「schema」抽象,来管理越来越复杂的节点数据。
DNS 分流,两个引擎的精巧配合
DNS 分流是科学上网最核心也最容易出问题的环节。你想想看,如果 google.com 的 DNS 解析走了国内 DNS,返回的就是被污染的 IP,代理连接的就是一个假服务器。如果所有 DNS 都走代理 DNS,国内网站的解析就慢得要命。
fancyss 提供了两套 DNS 方案,chinadns-ng 和 smartdns,可以二选一或配合使用。
chinadns-ng 做的是「国内外 DNS 分流」。它的原理是同时向国内 DNS 和国外 DNS 发查询,根据域名匹配规则决定用哪个结果。国内域名走国内 DNS 拿到真实 IP,国外域名走可信 DNS(比如 DoH)拿到未污染的 IP。
smartdns 做的是「DNS 缓存和优选」。在 ss_base_dns.sh 里有一个端口池设计让我眼前一亮,
SMARTDNS_RELAY_PORT_BASE=1055
SMARTDNS_RELAY_PORT_MAX=1070
fancyss 为每个「机场」(订阅源)分配一个独立的 smartdns relay 端口,从 1055 到 1070,最多支持 16 个机场的 DNS 中继。每个机场可以有自己的 DNS 配置,互不干扰。这种设计解决了多机场场景下 DNS 策略冲突的问题,比如机场 A 的节点需要用机场 A 提供的 DNS 才能解锁 Netflix,机场 B 则不需要。
但这套 DNS 系统也是最脆弱的部分。我翻了 issue 区,DNS 相关的 bug 占了高赞 issue 的一大半。issue #33700 标题是「0 点刚过,DNS 就挂了」,16 个赞。issue #33638 是「升级后 chinadns-ng 一直是未启动导致节点不可用」,issue #33659 是「chinadns-ng 还是不能正常运行,依然是双 x」。这些 issue 说明 DNS 分流在真实环境中的稳定性远不如理想,尤其是零点这个时间点,很可能是定时任务(订阅更新、规则更新)触发了 DNS 重启,但重启逻辑有竞态条件导致 DNS 进程没正确拉起来。
节点分流,16 条规则 8 个目标的硬约束
fancyss 3.0 引入了一个「节点分流」功能,这是我读源码时觉得设计最精巧的部分。
传统的透明代理只有一个出口节点,所有走代理的流量都从同一个节点出去。节点分流允许你按规则把不同流量路由到不同节点。比如 AI 网站走节点 A(日本,解锁 ChatGPT),流媒体走节点 B(美国,解锁 Netflix),其他走节点 C(默认)。
在 ss_node_shunt.sh 里,这个功能的实现有一组硬约束,
FSS_SHUNT_MAX_RULES=16
FSS_SHUNT_MAX_TARGETS=8
最多 16 条规则,最多 8 个目标节点。规则匹配用的是 rules_ng2 目录下的域名和 IP 列表,包括 site/ai.txt、site/netflix.txt、site/disney.txt、site/google.txt 等分类。运行时通过 geotool 二进制查 geosite.dat 和 geoip.dat 做匹配。
运行时状态存在 /tmp/fancyss_shunt/ 目录下,用 TSV 文件而不是 JSON。active_rules.tsv 存当前生效的规则,target_nodes.txt 存目标节点列表,runtime.meta 存运行时元数据。选择 TSV 而不是 JSON 的原因很实际,路由器上 jq 解析大 JSON 很慢,而 shell 的 awk/sed 处理 TSV 几乎零开销。
ss_node_shunt.sh 里还有一个 fss_shunt_state_write 函数,用 .tmp.$$ 临时文件加原子 rename 来写状态文件,避免并发读写时的数据损坏。在路由器的 busybox 环境里,这种文件锁的写法是最可靠的并发控制手段。
还有一个热重载机制。ss_shunt_hot_reload.sh 允许在不中断代理连接的情况下更新分流规则。HOT_STATE_FILE 记录热重载状态,切换节点时通过 ss_node_identity_reconcile.sh 做节点身份对账,确保旧连接不会被错误地路由到新节点。
透明代理的 iptables 编排
透明代理的核心是 iptables 规则。fancyss 不自己写代理协议,但它要负责把流量正确地导向代理二进制。
ssconfig.sh 是整个插件的入口脚本,它做的事情是按顺序编排,探测运行时环境(CPU 核数、ISP DNS、LAN IP)、生成代理配置(Xray JSON 或其他协议配置)、启动代理进程、配置 DNS 分流、写入 iptables 规则、启动状态监控。
并发控制用的是 flock 文件锁,LOCK_FILE=/var/lock/koolss.lock。set_lock() 和 unset_lock() 保证同一时间只有一个 ssconfig.sh 实例在跑。这在路由器环境里非常重要,因为用户可能在 Web 界面频繁切换节点,每次切换都会触发一次完整的重配流程。
ssconfig.sh 里的 refresh_runtime_context 函数用 nvram get wan0_dns 拿 ISP 分配的 DNS,用 nvram get lan_ipaddr 拿 LAN 地址,用 grep -c '^processor' /proc/cpuinfo 数 CPU 核数。这些都是华硕固件 nvram 接口的直接调用,跑在路由器固件提供的运行时环境里。
透明代理的具体实现是 ipt2socks 二进制配合 iptables REDIRECT 规则。TCP 流量通过 iptables 的 REDIRECT target 重定向到 ipt2socks 监听的本地端口,ipt2socks 再把流量转成 SOCKS5 协议发给 Xray。UDP 流量用 TPROXY 模式处理。这套方案不是 fancyss 发明的,是科学上网社区的标准实践,但 fancyss 把它包装成了可配置、可切换的模块化系统。
一个人的八年
fancyss 的维护现状有点让人唏嘘。
GitHub 显示 1 个贡献者。bus factor 等于 1。整个项目从 2018 年到现在,8 年时间,核心维护者就是 hq450 一个人。没有 GitHub Releases,版本管理靠 Changelog.txt 手写,离线包直接放在 packages/ 目录里通过 raw.githubusercontent.com 分发。
但这个项目的活跃度其实不低。Changelog 最新版本是 3.5.30(2026 年 6 月 1 日),半个月前还在更新。Xray-core 跟进了 v26.6.1,NaïveProxy 跟进了 v148.0.7778.96-2,anytls-zig 跟到了 0.1.6。这些上游二进制的版本追踪和重新编译,全是手工完成的。
协议是 GPL-3.0,对商业使用有约束。README 里挂了三个机场广告(Nexitally、ssLinks、nf.video),这几乎是个人维护的科学上网项目标准商业模式,靠机场推广佣金覆盖服务器和开发成本。
issue 区的生态很活跃。33700 多个 issue(包含 closed),高赞 issue 集中在 DNS 故障、订阅更新失败、协议兼容性三类。issue #33870 提到 anytls 协议的兼容问题,说明社区在积极跟进新协议,但新协议的适配速度取决于维护者一个人的精力。issue #33861「很多代理商疑似都有不兼容问题」则暴露了一个结构性矛盾,fancyss 依赖上游二进制(主要是 Xray)处理协议,如果机场用了 Xray 不支持的协议变体,fancyss 无能为力。
「编排者不实现」模式
拆完 fancyss 的源码,我一直在想一个问题,为什么一个纯 Shell 脚本项目能做到 13K Star,而很多功能更完整的代理工具反而默默无闻。
答案在于一个设计决策,编排者不实现。
fancyss 自己不写任何代理协议,不写 DNS 解析器,不写 geoip 查询。它做的事情是把上游最好的工具(Xray、chinadns-ng、smartdns、NaïveProxy)编排到一个统一的框架里,用 Web 界面和 Shell 脚本把它们串起来,让普通用户不需要懂命令行就能用。
这个模式我称之为「编排者模式」。它的核心是,在一个生态碎片化、工具链复杂的领域,不做新的轮子,而是做最好的胶水。fancyss 的 1275 个文件里,核心价值不在任何单个脚本,而在于那套「探测环境 → 选二进制 → 生成配置 → 编排 iptables → 管理 DNS → 监控状态」的编排流水线。
这个模式的可迁移性在哪?你想想看,Kubernetes 做的也是类似的事,它不实现容器运行时(用 containerd),不实现网络(用 CNI 插件),不实现存储(用 CSI 插件),它做的是编排。Terraform 也一样,不实现任何云 API,做的是多云资源的编排。区别只是规模和领域。
fancyss 证明了,即使在路由器这个极度受限的环境里,即使只用 Shell 脚本,只要编排逻辑做得足够清晰、模块化做得足够好,就能撑起一个 13K Star 的生态。技术栈的选择不重要,解决问题的思路才重要。
什么场景该用 fancyss?你家里有华硕路由器,不想搞软路由,想全屋透明代理,fancyss 是唯一的选择。什么场景不该用?如果你用的是 OpenWrt,Passwall 和 OpenClash 是更好的选择,生态更活跃,社区贡献者更多,bus factor 不止 1。
评论互动