问题
我平时带着 MacBook 在公司和家之间来回跑。
公司得靠 ClashMeta X 跑在本机做代理才能访问外网。回到家之后,OpenWrt 路由器上跑着 Mihomo,在网关层做了透明代理,整个局域网的流量都被接管,连上 Wi-Fi 就直接能用,本机什么都不用开。
两套环境,两种状态,每天切换两次。
如果回家忘了关 ClashMeta X,本机代理和路由器透明代理会同时生效,两层 NAT 叠在一起,轻则延迟翻倍,重则连接直接超时。最初的解法是手动切:回家关,出门开。坚持了大概三天。
识别「我在哪」
要自动化,首先要解决的问题是:当前连的是哪个网络?
方案一:Wi-Fi SSID
读当前连接的 SSID,如果是家里的名字就判定在家。
问题在于 SSID 不唯一。咖啡馆、同事家、邻居,都可能有一样的名字。而且路由器换了 SSID 改了,脚本就失效了。
方案二:网关 IP
家里路由器的 LAN IP 通常固定,比如 192.168.10.1。
但 192.168.1.1 是全球用量最高的路由器默认地址,几乎每个网络都可能用这个。单凭 IP 判断,误判的概率不低。
最终方案:网关 IP + MAC 地址双重验证
MAC 地址烧录在网卡硬件里,理论上全球唯一,不会随网络环境变化。只要当前网关的 IP 和 MAC 同时匹配,就可以高置信度地确认这是家里的网络。
MAC 地址怎么拿
网关 IP 好拿,读系统路由表就行。MAC 地址需要走 ARP。
ARP 是局域网里把 IP 映射到 MAC 的协议,查询结果会缓存在本地 ARP 表里。arp -n <网关IP> 就能取到对应的 MAC。
但这里有个坑:ARP 缓存可能是过期的。
刚切换网络时,ARP 表里可能还留着上一个网络的记录。直接读,拿到的是旧的 MAC,判断就错了。
正确的做法是:先 arp -d 清掉这个 IP 的缓存,再发几个 ping 强制触发新的 ARP 请求,然后再读。多一步,但拿到的是真实结果。
什么时候触发
触发时机比逻辑本身更值得想清楚。
最朴素的方案是定时轮询,每几秒检查一次。能跑,但会持续占用资源,对 Mac 是不必要的消耗。
更好的时机是:网络刚发生变化的那一刻。
macOS 切换网络时,系统会更新 DNS 配置文件 /var/run/resolv.conf。这个文件变化,意味着网络环境切换了。
macOS 的 LaunchAgent 提供了 WatchPaths 配置项,可以监听指定路径的文件变动,文件一旦被修改,系统自动拉起对应的程序。用它监听 /var/run/resolv.conf,空闲时零消耗,切换时精准触发。
怎么关应用
确认在家之后,需要把 ClashMeta X 关掉。
直接 kill 进程最省事,但粗暴。某些代理软件被强杀后,下次启动可能状态异常。
更好的方式是模拟正常退出。macOS 上可以用 AppleScript 向应用发退出指令:
tell application "ClashMetaX" to quit
效果等同于用户点菜单里的「退出」。如果这一步失败(应用无响应),备用方案是用 System Events 模拟 Command + Q。两层降级,覆盖绝大多数情况。
flowchart TD
A[需要退出应用] --> B[AppleScript\ntell app to quit]
B --> C{成功?}
C -->|是| D[完成]
C -->|否| E[System Events\n模拟 Command+Q]
E --> F{成功?}
F -->|是| D
F -->|否| G[记录失败\n放弃]
启动方向也有对称的检查:先 pgrep -x 确认应用没有在跑,没有才 open -a 启动,避免同一个应用开多个实例。
收尾:DNS 刷新
切换完成后还有一步容易漏掉:刷新 DNS 缓存。
系统 DNS 缓存里可能还留着旧网络解析出来的 IP。带着旧缓存进新网络,某些域名会解析到错误的地址。主动清掉:
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder
不论切到家里还是切出去,这一步都执行。
完整流程
flowchart TD
A[网络切换\n/var/run/resolv.conf 变化] --> B[LaunchAgent 唤起脚本]
B --> C[刷新 ARP 缓存\n触发新的 ARP 请求]
C --> D[读取当前网关\nIP + MAC]
D --> E{与家庭网关匹配?\nIP + MAC 双重验证}
E -->|匹配 - 在家| F[关闭 ClashMeta X\n关闭 Tailscale]
E -->|不匹配 - 其他地方| G[确保 ClashMeta X\n正在运行]
F --> H[刷新 DNS 缓存]
G --> H
H --> I[完成]
整个过程全自动,网络切换后几秒内完成。
最后
这个脚本解决的问题很小,但每天都会遇到。整个实现过程里细节不少:ARP 缓存的时序问题、应用退出的兼容性、网关信息获取的容错处理……都是跑起来发现问题再修的。
到目前为止没再因为忘记切换代理导致网络出问题。