13K Star 的 fancyss,一个 Shell 脚本如何在路由器上编排整个翻墙栈

发布于 2026年07月11日 18:13 #DevOps#Github 解读 原文链接

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-armbin-hndbin-hnd_v8bin-qcabin-mtkbin-ipq32bin-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_schemafss_get_node_field_plain 这些函数,说明项目后期不得不在键值存储之上加了一层「schema」抽象,来管理越来越复杂的节点数据。

DNS 分流,两个引擎的精巧配合

DNS 分流是科学上网最核心也最容易出问题的环节。你想想看,如果 google.com 的 DNS 解析走了国内 DNS,返回的就是被污染的 IP,代理连接的就是一个假服务器。如果所有 DNS 都走代理 DNS,国内网站的解析就慢得要命。

fancyss 提供了两套 DNS 方案,chinadns-ngsmartdns,可以二选一或配合使用。

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.txtsite/netflix.txtsite/disney.txtsite/google.txt 等分类。运行时通过 geotool 二进制查 geosite.datgeoip.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.lockset_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 流量通过 iptablesREDIRECT 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。

评论互动

© 2026 王若风的技术博客 · Powered by Astro