
人工補充幾句: 以下正文內容都是AI寫的 這個工具的主要用途是帶Macbook出門避免連AIGC的api直接走國內ip被封號 前提是surge要配置好aigc走指定鏈路(如vps->US家寬) 這樣基本能確保100%不會誤走國內外網直連AIGC API
家裡有一套基於 Surge + PF 防火牆的 fail-closed 網路保護方案:Surge 負責代理分流,PF 做最後一道 killswitch——Surge 程序異常退出時,境外流量自動被 PF 攔截,不會直連洩漏。
但有一個問題:怎麼知道現在的保護狀態是好的?
靠肉眼盯著 Surge 圖示顯然不現實。於是我寫了 NetworkGuard,一個住在狀態列裡的小 App,每 5 秒跑一輪探測,有問題就發系統通知。
它長什麼樣

點開選單,能看到完整的系統快照:
- PF 防火牆:當前是 guarded / open / lockdown 哪種模式
- Surge:程序狀態 + 增強模式 + 代理模式(rule/proxy/direct)
- Xray 橋接:備用代理埠是否在 LISTEN
- 在家檢測:ARP 和哨兵雙重驗證,確認是不是在自己家的網路裡
- 快捷操作:臨時放行 30 分鐘、手動 Lockdown、恢復自動監控
狀態列圖示用 SF Symbols,一眼就能看出保護狀態:
| 圖示 | 含義 |
|---|---|
checkmark.shield | 最佳狀態:guarded + Surge 健康 |
exclamationmark.shield | Surge 異常,需要關注 |
shield.slash | 自動 Lockdown(Surge 掛了且不在家) |
lock.shield | 手動鎖定中 |
在家判定:為什麼需要這個
在外網,Surge 掛掉就必須 Lockdown——因為沒有代理鏈路,境外直連就會洩漏。但在家網裡,即使 Surge 暫時掛掉,我的路由器和 mihomo 還在,流量有其他出路,不必立刻封鎖。
所以 surge-guard(控制 PF 的守護程序)會判斷是否"在家網",邏輯是:
在家 = (ARP 內網 MAC 匹配) AND (哨兵 HTTP 探測透過)
具體說:
- ARP 組:檢測區域網兩臺裝置的 MAC 地址是否匹配,互為 fallback
- 哨兵組:向區域網內兩臺伺服器發 HTTP 請求(帶 token 鑑權),任一透過即可
- 兩類證據都要有:ARP 只能證明你在這個子網,哨兵帶 token 鑑權才能排除偽造
任何一組失敗,就不算"在家",Surge 異常時會進入 Lockdown。
Web 監控面板

App 啟動時會在本機 9800 埠起一個 HTTP 服務,提供三個介面:
localhost:9800/— HTML 儀表盤,每 5 秒自動重新整理/api/status— 當前探測狀態 JSON/api/events— 最近 50 條狀態變化記錄
面板裡能看到所有探測點的實時狀態,以及歷史切換記錄——每次 Surge 重啟、切換模式、ARP 狀態變化都會記錄下來,方便排查"剛才為什麼圖示閃了一下"這類問題。
事件記錄的格式是 JSONL,存在本地,按型別區分:
| 型別 | 觸發條件 | 是否發通知 |
|---|---|---|
| pf_mode | PF 模式切換 | ✅ |
| surge | Surge 程序/模式變化 | ✅(程序變化) |
| sentinel | 哨兵可達性變化 | ❌ |
| home | 在家/外網切換 | ❌ |
| xray | Xray 埠變化 | ❌ |
| override | 臨時放行開始/結束 | ✅ |
通知只發"真正重要"的事件,哨兵瞬間抖動之類的不會騷擾到你。
去抖動:解決誤報問題
早期版本有一個煩人的問題:Surge 重啟的那幾秒,App 會誤報"Surge 程序退出",圖示閃歎號,還發通知,但實際上 Surge 只是在重啟。
根本原因是所有探測都是"即時判斷"——一次檢測失敗就切狀態。
修復方案是給每個訊號加去抖動:連續 3 次檢測失敗(約 15 秒)才真正切換狀態,但恢復立即生效。用一個 failStreak 字典記錄各訊號的連續失敗次數:
func debounce(_ key: String, rawOk: Bool, stableOk: Bool) -> Bool {
if rawOk {
failStreak[key] = 0
return true // 恢復立即生效
}
let streak = (failStreak[key] ?? 0) + 1
failStreak[key] = streak
return streak < 3 ? stableOk : false // 連續 3 次才切換
}
對 Surge 還有一個額外保護:程序存在但 API 暫時無響應時,保留上次已知的 mode 和 enhanced 狀態,不會因為一次 API 超時就顯示歎號。
另一個坑是 curl 探測哨兵時會走 Surge 的 TUN 介面——Surge 重啟期間 TUN 不通,哨兵探測就失敗了。解決方法是加 --noproxy '*' 強制直連區域網,繞過 TUN:
curl -fsS --max-time 2 --noproxy '*' -H "X-Guard-Token: ..." http://內網IP:18888/healthz
同理,ARP 重新整理快取也從 ping(經過 TUN)改成 TCP 連線 80 埠(走本地子網路由,不經過 Surge)。
當 Surge 掛掉,那 5 秒網路是什麼狀態
這是我做這個系統時仔細推演過的一個問題。
在外網,Surge 掛掉後到 Lockdown 之間大約有 5 秒視窗。這 5 秒裡網路狀態是:
- 境外非 CN 新連線:已被 PF 攔截(guarded 模式的 allow_wan 表本來就是空的,PF 規則在 Surge 掛之前就封著這些流量)
- CN IP 新連線:直連放行,但沒有代理,走裸連
- 已建立的 TCP 連線:PF 狀態表還在,可能再傳幾秒資料
5 秒後切入 Lockdown,強制 kill 所有現有連線狀態。
所以真正的風險視窗不是"Surge 掛了的那 5 秒",而是 Surge 掛之前就有 DIRECT 規則的境外連線正在傳輸——PF 狀態表會讓這條連線多活幾秒。
在家網,Surge 掛了不會 Lockdown,因為 home_trusted 成立——路由器 + mihomo 還在,流量有保障。這是設計行為。
整體架構
NetworkGuard 本身不控制任何防火牆規則,它只讀取狀態檔案和發系統通知。真正控制 PF 的是 surge-guard.sh(root 常駐程序),兩者透過狀態檔案通訊:
surge-guard.sh(root)
├── 每 5 秒跑 guard_once()
├── 控制 PF anchor 規則
└── 寫入 ~/.config/surge-guard/state
NetworkGuard.app(使用者態)
├── 每 5 秒讀 state 檔案
├── 探測 Surge API / ARP / 哨兵 / Xray
└── 發系統通知 + 更新選單 + 寫事件日誌
還有一個設計:NetworkGuard 程序是整個保護系統的"主開關"。surge-guard.sh 每輪會檢測 NetworkGuard 程序是否在執行,如果不在,自動切換到 open(全放行)模式——讓電腦恢復正常上網。重新開啟 App,保護自動恢復。
整個 App 是單檔案 Swift,用 AppKit + UserNotifications + Network Framework,編譯後約 300KB。如果你也有類似的 Surge + PF 方案想要視覺化監控,這個思路應該值得參考。
📷 封面圖 · Flux Schnell AI 生成 · panshi.io 品牌風格