这个项目解决什么问题

在已有 Clash / Clash Verge 机场订阅的前提下,有时会希望:

  • 少数站点(例如部分国内娱乐站)出口走自己的 SSH 服务器
  • 其它流量继续按订阅原有规则走机场节点或直连

手动做法通常是:终端里开 ssh -D,再改 Clash YAML。订阅一更新,手改内容容易被冲掉;隧道断了还要自己重连。

SSH Proxy Tool(仓库目录名可能是 wifi-helper)把这两件事收进 macOS 菜单栏:

  1. 管理本机 ssh -D SOCKS5 隧道(连接 / 断开 / 健康检查 / 指数退避重连)
  2. 生成 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-server127.0.0.1:<port>
编辑代理组增加可选分组(如「国内直连站点」),内含 my-server / DIRECT
编辑规则prepend 一批 DOMAIN-SUFFIX,<domain>,<分组名>

每段形态大致是:

prepend:
  - ...
append: []
delete: []

工具侧由 build_verge_enhance_patches() 根据域名列表与本地端口生成三段文本,用户粘贴保存即可。订阅刷新时,Verge 会把这些增强重新叠回去。

有一个容易踩坑的点:不要把 `prepend-proxies` 之类写进「扩展覆写配置」。新版本里扩展覆写更适合顶层字段 deep-merge;节点 / 分组 / 规则列表注入应走上面三个专用编辑器,否则代理页可能根本看不到新分组。

使用时的推荐顺序

  1. 本工具填好 SSH,连接成功,状态变为已连接
  2. 生成增强配置,分别贴到「编辑节点 / 代理组 / 规则」(GUI 里先点「高级」再贴 YAML)
  3. 订阅点「使用」,代理页确认出现 my-server 与对应分组
  4. 将该分组选成 `my-server`(不要误选成 DIRECT
  5. 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 动态转发在高并发下的性能边界——这些在联调日志里会暴露得很清楚。