这个项目解决什么问题
在已有 Clash / Clash Verge 机场订阅的前提下,有时会希望:
- 少数站点(例如部分国内娱乐站)出口走自己的 SSH 服务器
- 其它流量继续按订阅原有规则走机场节点或直连
手动做法通常是:终端里开 ssh -D,再改 Clash YAML。订阅一更新,手改内容容易被冲掉;隧道断了还要自己重连。
SSH Proxy Tool(仓库目录名可能是 wifi-helper)把这两件事收进 macOS 菜单栏:
- 管理本机
ssh -DSOCKS5 隧道(连接 / 断开 / 健康检查 / 指数退避重连) - 生成 Clash Verge Rev 的「编辑节点 / 编辑代理组 / 编辑规则」三段增强片段,让域名规则叠在订阅之上,而不是改订阅正文
它做的事情和你在终端、编辑器里手动操作等价,只是把重复劳动自动化。
核心思路:两段式,而不是整机 VPN
整条链路可以概括为:
指定域名的 App 流量
→ Clash(规则命中 DOMAIN-SUFFIX)
→ 本机 SOCKS5 127.0.0.1:<port>
→ ssh -D 隧道
→ 你的服务器出口
未命中的流量
→ 仍按原订阅规则(机场 / DIRECT / REJECT 等)
要点:
- 分流靠 Clash,不是靠本工具劫持系统所有流量
- 转发靠 SSH 动态端口转发(
-D),本工具只负责把进程拉起来并维持 - 机场节点与
my-server并行存在,互不取代
因此目标效果是「域名级分流」,不是「一键接管整机」。
架构与模块划分
项目是 Python 3.9+ 的 macOS 菜单栏应用,主要依赖 rumps、PyYAML、requests、PySocks,可用 py2app 打成 .app。
wifi-helper/
├── run.py / python -m ssh_proxy_tool # 入口
├── default_domains.json # 内置域名列表
└── ssh_proxy_tool/
├── menu_app.py # rumps 菜单 UI 与交互
├── settings_dialog.py
├── config.py # 配置读写 + Keychain 存密码
├── ssh_tunnel.py # ssh -D 进程、健康检查、重连、测速
├── domains.py # 默认域名 + 自定义域名
└── clash_merger.py # 生成 Verge 增强片段 / 全量 YAML 合并
职责边界比较干净:
| 模块 | 职责 |
|---|---|
menu_app | 状态字形、连接/断开、生成配置预览、自定义站点、设置 |
ssh_tunnel | 子进程生命周期,不关心 Clash |
clash_merger | 纯函数生成 YAML 片段,可单测 |
config | 非敏感字段落盘;密码只进钥匙串 |
配置落在用户目录,例如:
~/.config/ssh-proxy-tool/config.json(不含密码)~/.config/ssh-proxy-tool/custom_domains.txt- 密码:macOS Keychain,服务名
ssh-proxy-tool
实现要点一:SSH 隧道管理
隧道本质就是系统自带的:
ssh -D <local_port> -N -p <port> user@host
实现上额外做了这些工程细节:
- 密码认证走
sshpass;密钥认证更推荐,不依赖 sshpass ServerAliveInterval/ExitOnForwardFailure等选项,避免假死连接占着端口- 后台 monitor:端口是否在听、进程是否还在;掉线后指数退避重连
- UI 线程不阻塞:握手放后台线程,
rumps菜单只收状态回调 - 「测试代理」:经本地
socks5h://127.0.0.1:<port>请求探测 URL(例如 B 站 API),验证隧道是否真通
状态用简单字形反馈:○ 未连接 / … 连接中 / ● 已连接 / ! 异常。
实现要点二:Clash Verge 增强,而不是改订阅正文
直接改订阅 YAML 的问题是:远程订阅一更新,本地改动就没了。
Clash Verge Rev(较新版本)对「列表类」增强提供了订阅级三个入口:
| 菜单 | 写入内容 |
|---|---|
| 编辑节点 | 增加本地 SOCKS 节点 my-server → 127.0.0.1:<port> |
| 编辑代理组 | 增加可选分组(如「国内直连站点」),内含 my-server / DIRECT |
| 编辑规则 | prepend 一批 DOMAIN-SUFFIX,<domain>,<分组名> |
每段形态大致是:
prepend:
- ...
append: []
delete: []
工具侧由 build_verge_enhance_patches() 根据域名列表与本地端口生成三段文本,用户粘贴保存即可。订阅刷新时,Verge 会把这些增强重新叠回去。
有一个容易踩坑的点:不要把 `prepend-proxies` 之类写进「扩展覆写配置」。新版本里扩展覆写更适合顶层字段 deep-merge;节点 / 分组 / 规则列表注入应走上面三个专用编辑器,否则代理页可能根本看不到新分组。
使用时的推荐顺序
- 本工具填好 SSH,连接成功,状态变为已连接
- 生成增强配置,分别贴到「编辑节点 / 代理组 / 规则」(GUI 里先点「高级」再贴 YAML)
- 订阅点「使用」,代理页确认出现
my-server与对应分组 - 将该分组选成 `my-server`(不要误选成
DIRECT) - Clash 保持规则模式;系统代理或 TUN 至少开一种,让浏览器流量进入 Clash
联调时踩过的坑(比写代码更重要)
1. 「测试代理 200」≠ 浏览器一定通
本工具测的是「直连本地 SOCKS」是否通;浏览器还要先被 Clash 规则指到 my-server。两边成功条件不同。
2. 域名规则在列表里,日志却是 GeoIP(cn) using DIRECT
典型日志:
match GeoIP(cn) using DIRECT
说明连接匹配时 只有 IP、没有域名,DOMAIN-SUFFIX 被跳过,掉进订阅自带的「国内 IP 直连」。若当前网络直连这些国内 CDN IP 不通,页面就会大量 ERR_CONNECTION_CLOSED,表现为半残/白屏。
应对思路:
- 确认运行中的「规则」页里,目标
DOMAIN-SUFFIX排在GEOIP,CN之前 - 删掉冲突规则(例如另有一条
bilibili.com → DIRECT) - 打开 流量嗅探(Sniffer),或在扩展配置里启用 sniffer /
force-domain,让 TLS SNI 等能关联回域名 - 以日志是否出现
using 国内直连站点(或你的分组名)为准,而不是只看编辑窗口里有没有规则
3. 空闲延迟 200ms,一开网页就飙到秒级
ssh -D 单通道承载浏览器海量并发时,很容易带宽打满或 TCP-over-TCP 拥塞:空闲测速仍好看,真实开页却卡。缓解方向包括:
- 收窄走隧道的域名,避免把整个
GEOIP,CN都塞进 SSH - 换线路/带宽更好的服务器
- 长期重度访问可考虑在服务器上跑专用代理协议,而不是长期依赖动态转发看视频
项目边界
刻意不做的事情:
- 不实现系统级透明代理 / VPN
- 不破解、不攻击第三方服务;只自动化「连自己的 SSH」和「改本地 Clash 增强配置」
- 不替代 Clash:没有 Clash(或等价规则引擎),就只剩「本机 SOCKS」,做不到精细域名分流
可以继续改进的方向
- 自动写入 / 更新 Verge 增强文件(减少手动粘贴)
- 在扩展配置中一键给出 sniffer 片段,降低「规则在、匹配不到」的概率
- 隧道侧并发与带宽监控,菜单上提示「空闲正常 / 负载拥塞」
- 更细的域名集维护(按站点分组开关)
小结
SSH Proxy Tool 的设计很克制:隧道一层 + Clash 规则一层。用 ssh -D 提供可控出口,用 Verge 的订阅增强把域名规则持久叠在机场配置上,从而得到「特定域名走自己的服务器,其余照旧」的分流效果。真正难的往往不在菜单栏 UI,而在 Clash 规则命中顺序、域名嗅探,以及 SSH 动态转发在高并发下的性能边界——这些在联调日志里会暴露得很清楚。