Sukka's Blog

童话只美在真实却从不续写

KoolClash - 在 Koolshare LEDE 上使用 Clash

Sukka's Avatar 2019-03-19 创作集

  1. 1. 思路
  2. 2. 有用的参考资料
  3. 3. Koolshare 插件目录结构
  4. 4. Koolshare 脚本编写
  5. 5. Koolshare 插件主页面(插件入口)
  6. 6. 脚本执行
  7. 7. 配置文件修改逻辑
  8. 8. 日志获取
  9. 9. 守护进程
  10. 10. Clash 进程启动问题与修复记录
  11. 11. Clash 透明代理问题

Clash 是一个使用 Go 开发的、基于规则的多平台代理客户端,支持诸多协议,拥有像 Surge 一样强大的代理规则,现在已经有了在 Windows、macOS、Android 上的客户端。KoolClash 是运行在 Koolshare OpenWrt/LEDE 的 Clash 客户端。

如果要了解 KoolClash 的工作机制,请参考我写的 这篇文章

Clash for Windows 的开发者 Fndroid 之前发布过在 Windows 上使用 Hyper-V 虚拟 Koolshare OpenWrt 实现通过 Clash 透明代理的 教程视频,但是操作很繁琐。因为我也碰巧在用 Hyper-V 虚拟 Koolshare OpenWrt,所以我就决定尝试为 Koolshare 的软件中心开发一个插件来简化 Clash 安装和配置,就这样开了坑。
KoolClash 的开发过程可谓是举步维艰,我没有 Koolshare 插件开发基础、Koolshare 也没有提供任何文档,我甚至一度以为可能会最后弃坑,但是我最终抽出了三天时间里做出了 KoolClash 第一个 Beta 版本。在接下来的两周时间里不断打磨、终于释出了可以日常使用的版本、并在三周后推出了第一个稳定版。

关于 KoolClash 项目的介绍在 GitHub使用文档 中都有,所以本文就不再长篇大论安利 KoolClash 了,主要重点讲述 KoolClash 的开发思路,遇到的困难和解决方案,还会提及到一些 KoolClash 的操作逻辑。如果你有更好的想法,欢迎评论或者 在 GitHub 上开 issue

注意,请不要在本文的评论中反馈任何 Bug,任何使用问题请在 GitHub 开 issue。

思路

Clash 本质是一个网络处理核心、提供可运行的二进制文件;开放外部控制 API,GUI 只需要通过 API 控制 Clash 即可。所以为 Clash 开发 Koolshare OpenWrt 客户端其实并不难,只需要将 Clash 的二进制扔进去,写个界面启动、关闭 Clash 和上传配置文件的功能即可 跟把大象放进冰箱里一样简单

有用的参考资料

Koolshare OpenWrt/LEDE x64 版的软件中心几乎没有开发文档。我找到的最有用的文档资源莫过于这个 梅林 AM380 软件中心插件开发教程详解。虽然 Koolshare OpenWrt/LEDE 和 Koolshare Merlin 之间天差地别,但是这篇文档对于零基础 Koolshare 插件开发提供了 Koolshare 插件开发的基本思路。

Koolshare OpenWrt/LEDE 的插件和路由器中的脚本通信使用的 HTTP API 是 koolshare-httpdb。之前有开源在 GitHub 上、但后来删库了,不过 DevHub 缓存了 README,通过 README 可以很好了解 httpdb 的通信机制和如何构造 POST 请求。

最重要的参考资料无疑是 Koolshare OpenWrt/LEDE x64 插件 的 Repo,存放了所有插件的原始代码。虽然很多插件的脚本都经过压缩加密了,但是在参考插件目录结构、启动脚本和命令时是非常有用的。

Koolshare 插件目录结构

$ tree koolclash
koolclash
├── bin # 可执行二进制的目录,安装时需要将里面的文件复制到 /koolshare/bin/ 中
├── init.d # 开机时自动执行的脚本,安装时需要将里面的文件复制到 /koolshare/init.d/ 中
├── install.sh # 安装脚本,涉及到文件复制、增加执行权限等操作
├── koolclash # KoolClash 自有目录,复制到 /koolshare/ 中
├── scripts # 插件的运行脚本,只有这里面的脚本可以被 HTTP API 调用,安装时需要将里面的文件复制到 /koolshare/scripts/ 中
├── uninstall.sh # 卸载脚本,涉及到文件删除等操作
└── webs # Koolshare 软件中心用到的网页资源,安装时需要将里面的文件复制到 /koolshare/webs/ 中
    ├── koolclash # KoolClash 自有目录,放置了 clash-dashboard
    ├── Module_koolclash.asp # 插件界面、也是插件入口
    └── res
        ├── icon-koolclash-bg.png # 插件在软件中心列表中的背景
        └── icon-koolclash.png # 插件的图标

安装时安装包中所有内容会被解压到 /tmp/[插件名称] 目录下面并会开始执行 /tmp/[插件名称]/install.sh,也就是安装脚本。安装脚本包括的操作有删除旧文件、从 tmp 中复制目录和文件到路由器中、赋予 755 权限、设置插件信息等操作;卸载脚本主要是删除文件和目录、删除插件信息的操作。这些都可以参看别的插件的 install.shuninstall.sh

Koolshare 脚本编写

Koolshare 插件中所有脚本都强烈建议带上 source $KSROOT/scripts/base.sh,Koolshare 的很多功能如返回 HTTP 响应功能(http_response)、日志功能(logger)等都依赖这个。一般脚本开头都如下所示:

#!/bin/sh
export KSROOT=/koolshare
source $KSROOT/scripts/base.sh
alias echo_date='echo 【$(date +%Y年%m月%d日\ %X)】:'

Koolshare 还提供了一个 /koolshare/scripts/base.sh,应该可以用来简化一些操作,不过并没有看见多少 Koolshare 插件有在使用。

Koolshare 插件主页面(插件入口)

由于 /koolshare/webs 目录下面的内容都可以通过 http://[路由器 IP]/[相对 webs 目录的路径] 访问(不过需要注意的是 Luci 的 Webserver 不支持不带 . 的文件名路径的访问,访问文件名不带 . 的文件会报 500 错误),所以你可以自行引入外部 UI 库和框架。KoolClash 内置的 clash-dashboard 就是放在 /koolshare/webs/koolclash 目录下面,可以通过 http://[Router IP]/koolclash/index.html 访问。

Koolshare 插件的界面也依赖 advanced-tomato,css 类都很简单,看现有的代码就可以直接学会。不过 KoolClash 界面实现的 nav tab 组切换和其它 Koolshare 插件不同,KoolClash 的 nav tab 不依赖 JavaScript、而是使用 HTML5 特性和一些 CSS 黑魔法实现了 nav tab 组件,感兴趣的可以看看 实现思路KoolClash 的具体代码

Koolshare LEDE/OpenWrt 插件与脚本通信是是通过 POST /_api 调用 HTTP API httpdb,具体使用方法可以参考上文给出的文档,通过 HTTP API 可以修改/存储 dbus 数据、运行脚本并传递参数给脚本等。

比较小的数据可以 base64 后通过参数传递给脚本,这样可以保留空白和换行;如果是比较大的数据或者上传文件,需要通过 /_upload 上传文件接口,上传以后文件会存在 /tmp/upload 目录中(这个目录中的文件同时还可以被 http://[Router IP]/temp/ 访问到,还可以用来从路由器中下载文件或获取日志)。KoolProxy 和 WireGuard 插件都有调用这个接口,KoolClash 在写 Clash 配置文件上传时就参考了它们的代码。

Koolshare 软件中心是 PJAX 的,在旧版的 Koolshare LEDE 的软件中心上甚至只要页面的 location hash 不改变就不会刷新页面。KoolClash 之前使用 let 声明变量,由于 let 禁止重复声明变量特性、在 Koolshare 的 PJAX 页面上会引起问题。KoolClash 的修复方案是只在有明确块级定义域内使用 let,在声明页面全局变量时使用不严格模式的 var

脚本执行

上文提到了需要通过 HTTP API 调用执行的脚本都必须放在 /scripts/ 目录下面,并且需要引用 /koolshare/base.sh 来实现一些功能,比如通过 HTTP API 给页面返回数据(脚本中使用 http_response 返回 HTTP 响应数据、脚本里只允许执行一次 http_reponse、返回的数据在 AJAX 请求响应的 result 字段中)等等。

需要注意的是,根据之前 httpdb 的 README,通过 Web 添加的参数是从 $2 开始排起的(第一个是 Web 传进来的请求 id,用来区分不同的脚本),而且如果脚本没有及时通过 http_response 返回响应,那么前端的 AJAX 请求就会一直等待直到超时。

Koolshare 的开机自启脚本的逻辑我并不清楚,看到 koolss 插件代码中包括了 init.d 脚本、rc.d 和添加防火墙的方法。KoolClash 直接照抄了 koolss 的逻辑、同时添加了这三种方法,但是在实际中并没有能判断出是通过哪种方式启动的。

配置文件修改逻辑

KoolClash 为了简化 iptables 操作和 dnsmasq 配置文件的操作(其实是因为我懒),需要让 Clash 将 redir 监听在 23456 端口上、将 DNS 监听在 23453 端口上,所以 KoolClash 在保存 Clash 配置文件的时候会自动覆盖 external-controllerdns.listen 字段的设置。

Koolshare 内置了 jq 可以操作 JSON,但是 Clash 使用 YAML 作为配置文件格式,Koolshare OpenWrt 中没有内置操作 YAML 的工具;所以 KoolClash 一开始使用 sedgrep 来检查和操作配置文件。后来 KoolClash 引入了 yq 来操作 YAML,强大的 yq 让 KoolClash 迅速推出了 Clash 配置文件合法性检查功能。

一些公共代理服务提供商会把托管配置中的 external-controller 设置为监听在 127.0.0.1 上,毕竟软路由最后会运行在哪个网段上是不确定的,监听在 0.0.0.0 上又不安全;但是在软路由或者路由器上运行,Clash 的控制肯定是由局域网内的设备而不是路由器自己控制。所以 KoolClash 会修改配置文件、默认让 Clash 的外部控制监听在路由器的 LAN IP 上。在 OpenWrt 上获取 LAN IP 的方法是 uci get network.lan.ipaddr。当然用户也可以自己决定 Clash 的外部控制监听的 IP。

日志获取

Koolshare 的很多插件都有日志获取功能。但是 httpdb 只能获得单次 HTTP 请求返回结果。研究 Koolshare 其他插件时发现 Koolshare WebServer 会获取 /tmp/upload/ 目录下的内容、正如前文提及的、可以通过 http://[Router IP]/temp/ 获取到。所以可以通过将脚本的逐步运行结果输出到 /tmp/upload/ 目录下的一个文本文件、并通过插件页面不断 AJAX 这个文件来实现展示操作日志的功能。

在分析其他 Koolshare 插件时发现,判断日志结束、停止 AJAX 操作是通过在日志里输出 XU6J03M6。搜索这个字符串发现这个是 Merlin 开发时留下来的,当 Merlin 的软件中心和插件被移植到 Koolshare 平台时,这个判断日志停止的被跟着移植了过来。不过 Koolshare 的判断是在前端实现的,所以在编写 Koolshare 插件时并不一定需要使用这个特定的字符串。

守护进程

KoolClash 之前的进程启动方式是通过 HTTP API 调用脚本,脚本先提前用 http_reponse 向前端页面返回数据结束连接,然后脚本直接启动 Clash 进程。由于 Clash 不是以守护进程启动,脚本就会 stuck 在启动 Clash 这一步,于是这个会话就会一直停滞在这里、Clash 就会常驻在后台。这意味着启动 Clash 进程必须是脚本的最后一步、通过 iptables 将所有流量都转发到 Clash 上的操作都必须在启动 Clash 进程之前执行。如果 Clash 进程最终没有能启动起来,那么就相当于所有流量都被转发到黑洞里面去了,用户就会被锁在路由器外面。

现在 KoolClash 使用 start-stop-daemon 启动 Clash 并使其作为守护进程在后台常驻,因此不需要把启动 Clash 进程放在脚本最后一步了,在启动 Clash 进程后(sleep 2)就可以用 pidof clash 检查 Clash 进程是否启动成功。如果 Clash 进程没有启动成功,KoolClash 就会立刻中断启动、回滚操作,以避免用户再被锁在路由器外面。

Clash 进程启动问题与修复记录

既然提到了用户因为 KoolClash 被锁在路由器外面,我就不得不好好谈一谈这个问题。正如我之前所说,在 KoolClash 没有使用守护进程时,操作 iptables 必须在启动 Clash 进程之前。KoolClash 早期版本导致用户被锁在路由器外面的原因,就是因为 Clash 进程没有正常启动。

在 KoolClash 推出第一个版本的时候就有很多用户被锁在路由器、软路由外面,最终得出的结论是 KoolClash 第一个版本并没有把操作结果及时反馈给用户,所以用户误以为插件没有响应,导致重复操作 iptables。不过好在 KoolClash 那时候并没有实现开机自动启动,所以只需要重启就可以清空 iptables,让大部分用户得以重新进入路由器。KoolClash 在第二个版本中立刻实现了把界面上所有 <button> 元素全部 disabled 来阻止用户重复操作。除此以外,KoolClash 在那个版本中开始在启动 Clash 之前检查 Clash 配置文件的合法性,虽然很简陋。

但是 0.4.0 Beta 版本之后最早一批 KoolClash 用户已经不会被锁在路由器外面,但是 KoolClash 的新用户被锁在路由器外面。在创建了一个全新的 Hyper-V 虚拟机测试以后我才意识到,Clash 启动时需要 Country.mmdb 来解析规则,如果 Clash 没有找到 Country.mmdb,就会自行下载。但是由于 iptables 操作在 Clash 进程启动之前,导致 IP 库无法下载。KoolClash 在接下来的版本内置了 IP 解析库,解决了这个问题。

需要注意,根据 MaxMind GeoLite2 的使用协议,任何参与 直接分发 GeoLite2 的项目必须在项目介绍中提及 GeoLite2 和 MaxMind。

当然困扰了 KoolClash 的这个问题将会得到一劳永逸的解决。除了前文所说的回滚机制,在本文写就时,KoolClash 将会实现不再把发往局域网的流量转发到 Clash,用户将再也不会因为 KoolClash 被锁在路由器外面。

Clash 透明代理问题

这个问题我一直在翻来覆去地强调。这个问题并不影响你通过 KoolClash 或其它 Clash 透明代理来看 Netflix 或者浏览网页,不过如果你想要了解技术细节的话可以继续看。

Clash 的 DNS 需要、且只用来解析域名分流规则,所以要求客户端所有的 DNS 请求必须经过一次 Clash;并且无论 Clash DNS 解析到的 IP 是不是被污染的(很多人遇到的 243 开头的污染 IP 除外)、只要不被 GeoIP 解析成 CN,就不会影响 Clash 将流量发往代理服务器。
Clash 会把 Host 发给代理服务器、在代理服务器上再进行一次解析。和那些将 Clash 作为系统代理的客户端不同(如 Clash for Windows 和 clashX,应用程序最终都是连接 127.0.0.1 上客户端监听的端口、并不需要关心 DNS 解析),Clash 的透明代理方案同时会将 Clash 的 DNS 的解析结果返回给下游的客户端。这就会出现客户端获得的 IP 是 Clash 的 DNS 返回的、但代理服务器实际连接的 IP 是代理服务器自己解析、两个 IP 不同的情况。这会引起一些网络调试的不便,一些特殊设计(比如内部实现了自己的 DNS 解析机制)的应用程序也可能无法正常运行。

本文作者 : Sukka
本文采用 CC BY-NC-SA 4.0 许可协议。转载和引用时请注意遵守协议!
本文链接 : https://blog.skk.moe/post/how-koolclash-developed/

本文最后更新于 天前,文中所描述的信息可能已发生改变