§1
RoamSwitch 是一款適用於 Mac 的選單列應用程式。取決於您對網路的信任程度
目前連接到時,它會自動切換 macOS 防火牆、共享服務、AirDrop、
和 DNS。它還監視 ARP 欺騙、向外部開放的連接埠、USB 儲存和
類似勒索軟體的加密活動,當它檢測到危險的東西時,它會盡可能地
使用資料包過濾器 (pf) 切斷流量-緊急氣隙。
換句話說,RoamSwitch 安裝了一個具有 root 權限的守護進程,如果它願意的話,可以停止所有 Mac 上的網路流量。它是一個人建造的,「請相信我」是不夠的 證明這種特權是合理的。因此,本文檔以您可以驗證的形式解釋了設計。
這是給誰的
- 工程師決定是否安裝
- 想要在評論或文章之前了解內部結構的安全研究人員和記者
- 合作夥伴公司的安全人員評估內部部署或 OEM 捆綁
本文檔不涵蓋的內容
它不涉及檢測閾值調整、誤報統計或 UI 演練。
它解釋的是四件事:特權、進程邊界、資料流和
密碼學。功能規格本身位於捆綁的 MCP 資源中
roamswitch://docs/features 和應用程式內幫助。
揭露內容以及揭露程度(揭露政策)
本文檔是在攻擊者已經擁有分散式二進位檔案的假設下編寫的。
此處顯示的每個端點 URL、識別碼、檔案路徑、XPC 協定和嵌入式公鑰
可以在幾分鐘內從運輸 RoamSwitch.app 中取出
strings、codesign -d 或流量代理。因此將它們寫在這裡
給攻擊者帶來任何新東西。它唯一進步的是審稿人的理解。
另一方面,從二進位檔案看不到的伺服器端實作-rate-limit 閾值、金鑰、管理端點、資料庫架構、Firebase 專案佈局 - 不包括在內。 OEM 和合作夥伴整合設計也超出了本文的範圍,並在單獨的章節中介紹。 內部文件。 「透過觀察客戶你可以學到什麼」是這則訊息的揭露線 文檔。
本文檔對應於頂部註明的版本的來源。行為改變的地方
在較高的版本中,該文件被修改,版本號和目標版本也被更新。
如果您發現文字和程式碼之間存在差異,請透過以下方式告知我們
lafine.net/contact.html。
§2
RoamSwitch.app 由三個可執行檔組成。其中只有一個人享有特權;的
另外兩個以登入使用者權限運行。所有三款產品均啟用了強化運行時、開發人員 ID
簽名並經過公證。
| 執行檔 | 特權 | 可以做 | 不能做 |
|---|---|---|---|
| 漫遊交換器 .app |
登入用戶 | 監控網路狀態、執行診斷、繪製 UI、透過 XPC 呼叫幫助程式、透過 defaults 更改 AirDrop、啟動 ClamAV(可選) |
直接操作防火牆、pf或系統守護程序(所有這些都透過幫助程序) |
| 漫遊開關 助手 |
根 | 僅 HelperProtocol(第 3 表)中所列的操作:防火牆/隱形、共用守護程式的載入/卸載、套用 pf 規則集、變更 DNS、傳送訊號 |
其他一切操作。沒有用於執行任意命令的介面。未宣告網路傳送 entitlement,但由於不在沙盒內,這本身並不禁止通訊(對外通訊的實測見 §7) |
| 漫遊交換器 MCPServer |
登入用戶 | 讀取並格式化診斷值,搜尋本機知識庫;結果透過stdio傳回給客戶端 | 變更設定、切換鎖定、隔離連接埠、彈出裝置。預設不開啟 socket,也不透過網路傳送任何內容(例外僅有預設關閉的 npm 簽章驗證,以及針對本機自身 127.0.0.1 的弱點診斷,見 §8)。未宣告網路傳送 entitlement,但由於不在沙盒內,這本身並不禁止通訊 |
權利和簽署
- 三個目標都有
ENABLE_HARDENED_RUNTIME = true。 - 應用程式沙箱已停用 (
com.apple.security.app-sandbox = false)。幫助程序和 MCPServer 的權利是空字典。 - 分發版本經過開發者 ID 應用程式簽署、Apple 公證並裝訂 (§10)。
未使用應用程式沙箱。 RoamSwitch需要從IOKit讀取硬體UUID,使用CoreWLAN並
DiskArbitration,枚舉其他程序的監聽套接字(lsof),開啟一個XPC
連接到 LaunchDaemon,並產生系統二進位。這一切在內部都是不可能的
沙箱,因此它被禁用。
有四件事可以彌補這一點。首先,強化運行時。二、開發者ID簽章和 公證。第三,只有一個可執行檔以 root 身分運行——助手——以及該助手可以做什麼 是固定和枚舉的(§3表)。四、與helper的連接受代碼限制 簽名(§3)。
§3
如何註冊
此幫助程式使用 SMAppService.daemon(plistName:) 註冊為 LaunchDaemon。的
嵌入到應用程式中的 plist
(Contents/Library/LaunchDaemons/com.tetsuharu.RoamSwitch.Helper.plist) 僅聲明
Label、BundleProgram、單一 MachServices 條目,以及
AssociatedBundleIdentifiers。由於 SMAppService 的工作原理,
除非應用程式位於 /Applications 中,否則甚至無法註冊。在第一
註冊後,只有用戶在系統中手動批准後,助手才會激活
設定。
它接受誰的連結 (ClientValidator)
助手檢查 NSXPCListener 的連接過程
shouldAcceptNewConnection 並且只將 HelperProtocol 分發給那些
通過。檢查使用 audit_token 而不是 PID,
避免PID重複使用和TOCTOU。
# Release 版本對連線方要求的程式碼簽署條件
identifier "com.tetsuharu.RoamSwitch"
and anchor apple generic
and certificate leaf[subject.OU] = "GV76B6G4YU"
此要求透過 SecCodeCopyGuestWithAttributes 檢查並
SecStaticCodeCheckValidity,不通過則斷開連接。僅
為了開發方便,DEBUG 版本刪除了 team-ID 引腳。實際運送的始終是
發佈建置。
助手的安全取決於這個單一的程式碼簽章要求。任何滿足它的東西(a
正確簽署的 RoamSwitch.app) 可以呼叫下表中的每個操作。有
沒有通道可以向其提供任意命令,但該表中的操作在功能上並不弱
他們自己。如果 RoamSwitch.app 本身被接管,這些操作將會傳遞給
攻擊者。
助手可以做什麼(完整清單)
Shared/HelperProtocol.swift 中定義的特權操作都是這些。
沒有此處未列出的特權 API。
| 方法 | 它的作用 | 二進位/API 呼叫 |
|---|---|---|
| 設定區塊全部(_:) | 開啟或關閉應用程式防火牆和隱身模式 | /usr/libexec/ApplicationFirewall/ --setblockall / --setstealthmode |
| 取得區塊所有狀態(...) | 讀取上面的當前值 | socketfilterfw --getblockall |
| 設定共享服務啟用(_:) | 卸載/載入 SSH/SMB/螢幕共享守護程式。停止時,它僅記錄“正在運行的”並僅恢復那些 | /bin/launchctl list / unload -w / load -w (固定為三個: ssh.plist / com.apple.smbd.plist / com.apple.screensharing.plist) |
| 啟用NetworkAirGap(...) disableNetworkAirGap(...) |
應用並解除緊急完全阻擋 (block drop all)。經歷 PFRulesetCoordinator (§4) |
/sbin/pfctl -f / -e / -sr |
| setGuardedDevServerPorts(_:) | 使用 pf 僅阻止 external 連接到給定的開發伺服器連接埠(本機主機通過)。傳遞一個空數組會提升所有這些 | /sbin/pfctl(也透過協調器) |
| setSecureDNSServers(_:) restoreOriginalDNSServers(...) getCurrentDNSServers(...) |
將活動網路服務的 DNS 切換為阻止惡意軟體的 DNS (Quad9 9.9.9.9 / Cloudflare 1.1.1.2),備份原始設定並還原它們 |
/usr/sbin/networksetup -listallnetworkservices / -getdnsservers / -setdnsservers |
| setLinkGuardSinkhole(_:) | 連結防護(§5)。將給定的網路釣魚/詐騙網域寫入 /etc/hosts 的分隔託管部分作為 0.0.0.0,然後刷新 DNS 快取。空數組會刪除該部分。域被標準化和重複資料刪除; IP 和垃圾被丟棄;上限為 60,000;透過臨時檔案+原子替換寫入 |
重寫 /etc/hosts (FileManager.replaceItemAt)/usr/bin/dscacheutil -flushcache /usr/bin/killall -HUP mDNSResponder |
| lockGatewayARP(_:) unlockGatewayARP(...) getGatewayARPLockStatus(...) |
預防性閘道 ARP/NDP 鎖定 (§5)。將給定的 IP → MAC 映射(IPv4 網關、IPv6 預設路由器、連結 DNS 解析器)固定為 permanent 鄰居快取項目。 IP 和 MAC 格式經過驗證;協調模型(不在請求中的引腳將被刪除)。引腳集保留為 gateway_arp_lock.json |
/usr/sbin/arp -s / -d /usr/sbin/ndp -s / -d |
| wireGuardImport(_:) wireGuardForget(...) |
VPN 隧道 (§5)。幫助程式儲存/刪除 0600 處的 WireGuard .conf 文字 |
文件只寫 |
| wireGuardUp(端點IPv4:端點IPv6:連接埠:) wireGuardDown(...) wireGuardStatus(...) |
將隧道開啟/關閉/讀取狀態。端點主機名稱是 由應用程式 解析,並且 IP 被傳遞給助手(助手的 DNS 可以被終止開關切斷) | 自製程式 wg-quick up/down、wg show(wireguard-tools;若未安裝則停用) |
| 終止進程(pid:forceKill:) | 掛起 (SIGSTOP) 或強制退出 (SIGKILL) 流程。用於包含類似勒索軟體的進程。僅 pid > 1 |
Kill(2) 系統呼叫(不是子程序) |
| 取得助手版本(...) | 傳回助手的版本字串(用於應用程式相容性檢查) | — |
terminateProcess 可以將 SIGKILL 傳送到任何進程,只要
pid > 1。 setSecureDNSServers 接受任何 DNS 伺服器字串。那就是
特徵所需的寬度,但並不窄。判斷它的前提是
它前面的代碼簽名檢查 (ClientValidator) 是唯一的門。
助手內部的狀態
HelperTool.shared是跨連線共享的單一實例。它曾經是每個連線的單獨實例,因此打開新連線的緊急遏制可能會引發一場失去「要帶回哪些服務」的競賽。- 共享服務和 DNS 備份僅在序列佇列 (
stateQueue) 上變更。
輔助程式內部的權限分離,以及為讀取 SSH 日誌而設的例外
輔助程式在啟動後立即替自己加上一道由核心強制執行的上限(執行階段沙盒,與上文 App Sandbox 的 entitlement 是兩回事),它啟動的子行程也會繼承。這道上限會阻止替換系統程式、註冊 LaunchDaemon 或 LaunchAgent、改寫 sudoers、PAM、sshd 的設定、直接寫入本機目錄儲存區,以及執行位於使用者目錄、外接磁碟區、暫存資料夾等可寫位置的程式。它不會阻止透過 Open Directory(dscl)進行的帳號變更,也不限制網路收發(那不是這道上限的對象)。
在這道上限之下,/usr/bin/log 無法執行。在 macOS 26 上,log 不能在沙盒內執行(log: Cannot run while sandboxed),以降權後的 nobody 身分也不能執行(Must be admin)。也就是說,登入使用者的 App,以及沙盒內的 root 輔助程式,都讀不了 sshd 的日誌(統一日誌)。SSH 暴力破解偵測必須讀取這份日誌,所以在權限分離上設了一個例外。
這個例外,是在輔助程式進入沙盒之前,先以 --ssh-log-reader 啟動同一個二進位檔而得到的一個小型 root 行程。該行程只接受 1 位元組的指令(開始與停止),只會以固定的引數(log stream --style ndjson --level info,以及只保留 sshd 行的固定條件)啟動 /usr/bin/log。它不傳遞環境變數,呼叫端也沒有任何途徑指定引數或指令。它不讀取、也不解譯日誌內容,輸出會流入輔助程式交給它的管線。
日誌行中含有使用者名稱等攻擊者可以寫入的字串,所以 root 輔助程式不解譯這些行。它把每一行交給以 nobody 身分執行、在禁止網路、寫檔案、啟動與複製行程的沙盒中的解析工作行程去解譯,來源位址則取自不受使用者名稱影響的行尾(IP port N)。在關閉了遠端登入的 Mac 上,這個行程不會啟動 log,什麼也不讀。可以透過輔助程式的 XPC 呼叫的 SSH 相關操作共有 5 個:套用設定、取得狀態、紀錄與登入,以及解除封鎖,沒有傳入任意指令的途徑。
§4
touch pf 的四個功能:緊急氣隙、開發伺服器連接埠防護、VPN 隧道
WireGuard 終止開關(§5,1.7.6+)及其 Tailscale 終止開關(§5,1.8.0+ - 僅選定的後端)。他們所有人總是會走
透過單一入口點 PFRulesetCoordinator,並且從不執行
pfctl -f 他們自己。
為什麼只有一個入口點
先前,這兩個功能各自獨立加載帶有 pfctl -f 的規則,相互競爭
超過 pf 的單一主要規則集。如果港口警衛的狹義規則 block ... port {…} 是
在氣隙的 block drop all 之後加載,您最終可能會處於螢幕
說“隔離”,但 Mac 仍然可以訪問。這個bug是透過實際攻擊發現的
來自另一台主機的機器,並在 1.4.3 中修復(故事寫在
docs/marketing/zenn/03_lan_side_attack_test.md)。
現在如何運作
- 每次都完全重建。 整個所需的規則集從當前狀態重建並一次應用。它永遠不會作為 diff 來應用。
- 一個串列佇列。 每個 pf 變更都在同一個
DispatchQueue上執行,因此無論它來自 XPC 連接、幫助程式的啟動還是故障安全計時器,變更都會按順序處理。 - 優先順序如下(排在上面的優先)。
- 緊急氣隙 →
set skip on lo0和block drop all(不考慮其他) - VPN 終止開關 →
block drop all加pass quick僅適用於:lo、隧道介面 (utunN)、與固定端點 IP 的 UDP 握手、DHCP 和 ICMP - 開發伺服器守衛 →
block drop in quick proto tcp ... port { … } - None → 重新載入
/etc/pf.conf並將 pf 返回原始狀態
- 緊急氣隙 →
- 應用後讀回。
pfctl -sr讀回規則以確認block drop all或每個連接埠的規則確實已載入。pfctl -f被默默忽略的情況不被視為成功。 - 臨時檔案被寫入包含
UUID的路徑,並在應用後刪除(v1.4.5 放棄了固定路徑,取而代之的是難以猜測的路徑)。狀態目錄是/Library/Application Support/RoamSwitch。
來自 enableNetworkAirGap 和 setGuardedDevServerPorts 的 XPC 回應,
(Bool, String?),報告操作是否一路通過
回讀。呼叫者(例如 ARPSpoofContainmentManager)在失敗時重試,並且如果
它仍然失敗,它將訊息直接顯示在螢幕上:「交通尚未停止。請關閉
現在有無線網路。 」
§5
塊的種類各不相同
| 種類 | 適用範圍 | 觸發 | 環回 |
|---|---|---|---|
| 緊急氣隙 | 停止所有傳入和傳出的流量 | 當偵測到類似勒索軟體的加密活動時。在偵測到 ARP 欺騙時,它僅在鎖定狀態下立即觸發;在平衡/可信任網路上,它會通知(您手動觸發它)。當在 shell 歷史記錄中偵測到「ClickFix」型的已知可疑指令模式時,例如將以 curl 等取得的內容進行 Base64 解碼後直接傳給 shell/AppleScript(1.8.7+,Pro,預設關閉)。 不用於日常外出防護 |
透過 set skip on lo0 |
| VPN 終止開關 | 除了隧道、握手、DHCP 和 ICMP 之外的所有內容 | 當 VPN 隧道(§5,Pro,預設關閉)開啟並且您加入不受信任的網路。一直保持到隧道建立(以及隧道關閉時) | 透過 set skip on lo0 |
| 開發伺服器連接埠防護 | 僅 external TCP 連接到給定連接埠 | 一鍵手動隔離,或偵測到陌生監聽連接埠時自動阻斷(Pro) | 來自本機主機,未更改 |
| 日常不可信網路保護 | 防火牆和隱身,共享停止(§6)。未使用 pf | 當您連接到尚未註冊的網路時 | — |
| 連結衛士 | 僅網路釣魚/詐騙網域的名稱解析(0.0.0.0 透過 /etc/hosts)。未使用 pf |
威脅來源上的目的地,或品牌同形異義詞。預設開啟(專業版) | 不受影響 |
| 預防性 ARP/NDP 鎖定 | 僅網關的 MAC、IPv6 路由器和連結 DNS(鄰居快取)。未使用 pf | 加入不受信任的網路時(專業版,預設為關閉)。重新固定每次網路變化 | 不受影響 |
讓 Air-Gap 既不滯留、也不會被等出來的機制
- 結束方式取決於觸發的可信度(1.9.52 起)。勒索軟體誘餌檔案遭竄改、XProtect 的確定偵測,以及回應 ARP 警告的手動隔離屬於「高可信度」;ClickFix、連接埠異常等屬於「低可信度」。低可信度的隔離仍然最長 10 分鐘自動解除。高可信度的隔離在 10 分鐘時不會解除:僅憑時間就完全解除,攻擊者只要等待就能脫離隔離。
- 高可信度的隔離會維持完全隔離直到上限(預設 1 小時,可在設定中改為 5 分鐘到 24 小時),並在此確認原因。對勒索軟體而言,只有在可疑程序已被識別且已結束、並且誘餌檔案自偵測時起未再變動時,才認定原因已消除並解除。無法確認時轉入「降級模式」。降級模式持續阻擋新連線(迴路與 DHCP 除外),只恢復 Wi-Fi 無線電,讓使用者能確認狀況。它不會因為時間過去而開啟。只有使用者解除,或確認原因已消除時才會開啟。
- 開始隔離時,也會切斷先前已經開啟的對外連線。清空 pf 的狀態表(
pfctl -F states),停止隔離之前就已持續的通訊。轉入降級模式時同樣會切斷。 - 由 ARP 詐騙引起的隔離,只有在無需任何通訊即可確認原因已消除時,才會自動解除。確認只讀取 ARP 快取(不傳送封包),當受信任的閘道 MAC 連續 3 次、跨度至少 60 秒都被看到時,判定原因已消除。但隔離期間,閘道的 ARP 項目可能會從鄰居表中消失(已在實際 Mac 上確認),此時無法確認。這種情況下,它會一直維持降級模式,直到手動解除。
- 即使輔助程式當機,解除判定也不會停止。完全沒有
KeepAlive的專用 LaunchDaemon(AirGapFailsafe)每 3 分鐘獨立啟動一次,做出與輔助程式相同的判定(低可信度:10 分鐘解除;高可信度:在上限時確認原因後降級)。即使輔助程式本身當機、連同自我解除的計時器一併遺失,這道安全網也不受影響(1.8.6 起;依可信度判定自 1.9.52 起)。 - 這個專用守護程式完全不會觸碰互動式助手本身的啟動設定。先前(1.8.5)曾使用助手自身的
KeepAlive.PathState來實現這一恢復機制,但它偶爾會與正常的應用程式結束操作發生衝突,導致非預期的重新啟動,因此該方案已被撤回,改用目前完全獨立的守護程式方式。 - 時間戳的判定採用單調遞增時間(
ProcessInfo.systemUptime)與掛鐘時間相結合的混合方式。通常優先使用單調遞增時間,因此不受 NTP 校正或手動修改系統時間的影響。僅在單調遞增時間不可信時(即重新啟動後:目前值小於已儲存的值,判定為發生了重新啟動)才會回退到掛鐘時間。 - 重新啟動或常駐程式重建之後會重新套用。輔助程式啟動時以
reapplyFromDisk()讀取磁碟上的狀態,依 Air-Gap、連接埠防護、系統預設的順序自行還原。隔離的狀態(可信度、完全或降級)與已經過的時間也會沿用,計時從最初觸發的時刻繼續(已在實際 Mac 上重新啟動輔助程式確認)。 - 失敗的提升被視為失敗。 如果
block drop all實際上無法刪除,則時間戳會被寫回,以便故障安全計時器和重試有一些收斂。螢幕絕不會錯誤地顯示「抬起」。 - 使用者可以隨時奪回主導權:透過選單列的「解除 Air-Gap 隔離」、彈出視窗中的解除按鈕,或直接關閉 Wi-Fi。解除操作不需要 Pro 版(即使隔離過程中 Pro 到期也可以解除)。
如何恢復端口防護誤報
未知端口自動阻止(Pro,啟動 Pro 後預設開啟)可以阻止合法端口
LAN 接收器 — LocalSend、Syncthing、啟用防護後啟動的任何內容。當那個
發生時,請透過通知橫幅上的「允許」按鈕或中的匹配行來允許它
“暴露連接埠”螢幕。一次允許的可執行檔被記錄為已知且不會被阻止
再一次(PortAnomalyGuard.allowPort(_:))。請注意,通用腳本解釋器
(Python、Node.js、Netcat 等)嚴格受 path:port 限制,而非二進位
獨自一人以防止離地生活的攻擊。 Apple系統守護程式滿足
anchor apple(rapportd,支援切換等)不是
首先觀看。
自癒勒索軟體金絲雀誘餌和遏制抑制
如果金絲雀誘餌檔案被篡改或重新命名並觸發緊急氣隙,則釋放 驗證安全性後的遏制會自動重新產生遺失或損壞的金絲雀文件 返回從嵌入模板產生的真實基線雜湊值,立即恢復不間斷的 kqueue 監視(防止對手基線污染)。 此外,在主動遏制期間(顯示緊急模式時),會自動抑制冗餘通知警報和定期後台完整性輪詢的重新觸發事件,以避免在事件回應期間分散用戶的注意力。
通用熵值型勒索軟體偵測 — 1.10.0,Pro,預設開啟
透過 FSEvents 監控 Documents・Desktop・Downloads・Pictures 資料夾,當 Shannon 熵超過閾值(7.92)的寫入在 5 秒內以 20 個以上檔案的突發形式出現時進行偵測。與金絲雀(誘餌檔案)方式不同,本功能不依賴於對誘餌檔案的操作,因此也能偵測到沒有誘餌檔案位置發生的加密。僅在達到閾值時才使用 lsof 確定當時的處理程序,並與允許清單(cp・mv・ditto・tar・ffmpeg・Xcode・Time Machine・主流瀏覽器等)進行核對。偵測到後,會透過與金絲雀防護相同的 RansomwareContainmentManager 自動封鎖網路。
Linux 版透過 fanotify 為寫入事件附加 PID,因此可以按處理程序追蹤突發寫入。由於 macOS 既沒有 fanotify,(基於方針)也沒有 EndpointSecurity,本實作僅透過 FSEvents 取得路徑,並不依賴處理程序而是全域計算突發寫入。侷限:當多個合法處理程序同時寫入大量高熵檔案時(例如多個壓縮・加密工作重疊的情況),本功能可能比 Linux 版更容易出現誤判。
用於勒索軟體復原的事前快照(Mac)
偵測時拍攝的快照已包含偵測之前被加密的檔案,因此不能作為回復點。所以無論是否偵測到,都會定期建立 APFS 本機快照(預設每 6 小時,可選關閉、1、3、6、12、24 小時)。一旦發生偵測,新的事前快照與舊快照清理會暫停最多 7 天(保留模式),避免加密後的狀態擠掉最後一份加密之前的快照。復原一律由人手動進行,只把選定的檔案取出到 ~/RoamSwitch-Recovered/ 下,不會覆寫目前的檔案,也不提供整個磁碟區的還原。取出檔案需要掛載快照,因此特權輔助程式需要完整磁碟取用權限(沒有該權限時仍會建立快照)。macOS 可能在可用空間不足等情況下自行刪除本機快照,無法保證保留超過約 24 小時。復原視窗屬於 Pro 功能(快照的建立本身在非 Pro 下也會執行)。
鑑識證據包 — 1.10.0
當金絲雀偵測或勒索軟體熵值偵測等觸發緊急 Air-Gap 時,會在不阻擋隔離處理本身的背景佇列中,將當時的處理程序清單(ps)・網路連線(lsof -i)・最近 5 分鐘內變更的檔案・可疑處理程序已開啟的檔案清單,儲存至 ~/Library/Application Support/RoamSwitch/incident-evidence/<時間戳記>/manifest.json。manifest 中記錄了每項證據的 SHA-256,可用於事後竄改偵測。可透過 MCP 的 get_forensic_evidence_bundles 工具取得清單。侷限:由於設計上不使用 EndpointSecurity 等權限,本功能不會擷取封包或處理程序的記憶體・檔案描述符快照。
程序執行記錄(Mac,Pro,預設關閉)
特權輔助程式把 macOS 內建的 /usr/bin/eslogger(exec、fork、exit)當作子程序啟動,只在這台 Mac 內記錄程序的啟動。不申請 EndpointSecurity 權限(不呼叫 es_new_client),需要在系統設定的「完整磁碟取用權限」中允許 RoamSwitch(清單中沒有時,用「+」加入 /Applications/RoamSwitch.app)。沒有授權期間,狀態顯示為「無法使用」並等待,每 60 秒重新確認一次(不會切換為高負載的輪詢)。只做通知。不會阻止執行、切斷通訊或隔離。
對以下 10 種行為發出通知。瀏覽器・Office・郵件應用程式直接啟動 shell/從暫存資料夾或帶隔離屬性的位置執行未簽章的二進位檔案/下載後傳給 shell 的單行指令(伴隨原始 IP 的 URL、base64、eval、停用 TLS 驗證等情況)/使用 base64 或 eval 的 osascript/剛移除隔離屬性後立即執行該檔案/從剛寫入的自動啟動註冊(LaunchAgent・LaunchDaemon)啟動未簽章的二進位檔案/非 Apple 簽章的父處理程序取得鑰匙圈中的機密資訊(如 security find-generic-password -w)/base64・eval 解碼與 curl・wget・exec・system 呼叫同時出現在同一行的 python3・perl・ruby・php・pwsh 的 -c/-e/-Command 單行執行(1.10.0 起。僅使用 base64 或僅使用 eval 不會觸發)/設定 DYLD_INSERT_LIBRARIES 環境變數後啟動處理程序(1.10.0 起。為避免洩漏機密,僅讀取這一個環境變數的值,且不針對 Apple 簽章的處理程序)/指令列直接指定 RoamSwitch 放置的誘餌憑證檔案(如 ~/.aws/credentials、~/.env.backup,內容中帶有標記)的執行(如 cat、tar、curl -F;真實檔案不會符合,透過函式庫讀取的情況由蜜罐權杖監視負責)。每項偵測都會附帶 MITRE ATT&CK 技術 ID。
只有嚴重程度高的偵測才會以橫幅顯示,同一種類每 10 分鐘合併為一次(記錄中保留全部)。Claude Code、GitHub CLI 等開發工具讀取自己憑證的行為預設不通知。若其他程式讀取,可用通知中的「允許此程式」只放行該項目(有簽章時依 Team ID,沒有時依執行檔名稱,並與項目名稱配對)。
記錄以僅 root 可讀的形式儲存(目錄 0700、檔案 0600),預設保留 200 MB、14 天。不儲存環境變數,但命令列引數中可能含有機密。每個區段以連接到前一區段雜湊的雜湊鏈封存,「驗證鏈」可偵測竄改、區段缺漏與封存被刪除(均已在實際 Mac 上確認)。這是對竄改的偵測,而非防止。最新區段的刪除或回溯無法偵測。讀取經由輔助程式的 XPC,MCP 則經由應用程式(含 Pro 再確認)。
限制:輔助程式啟動前,或啟用記錄之前的執行不會被記錄。eslogger 是事後觀察,無法阻止。在每秒數千次程序啟動的負載下會有遺漏(會在畫面顯示數量,並持續運作)。這項記錄不能取代 EDR(端點偵測與回應)。
憑證蜜罐權杖 — 1.10.0,Pro,預設開啟
在 ~/.aws/credentials・~/.ssh/id_rsa_backup・~/.docker/config.json 三處放置帶有標記的偽造憑證檔案。如果該位置已存在真實檔案,絕不會覆寫。存取偵測除了使用與金絲雀防護相同的 kqueue(DispatchSource.makeFileSystemObjectSource)之外,由於沒有 EndpointSecurity 無法可靠捕捉 open() 事件,還並行採用每 30 秒一次的存取時間輪詢。偵測僅發出通知,不會封鎖或隔離。ssh 誘餌使用 ssh 預設不會尋找的名稱(而非 id_rsa),因此不會破壞正常的 ssh 執行,也不會產生誤報。AWS 誘餌使用獨立的設定檔名稱而不是 [default],Docker 誘餌指向不存在的內部映像倉庫,並且都以 0600 權限放置(自 1.10.3 起)。除這三處外,還會在 ~/.pypirc 與 ~/.env.backup 放置誘餌(針對搜尋 PyPI 權杖、.env 與 AI API 金鑰的行為)。不使用 ~/.npmrc,因為 npm 平時每次都會讀取;也不使用 GCP 憑證,因為偽造的檔案會破壞正常驗證。誘餌一旦被真實存取,就會額外放置一個相關誘餌(~/.aws/config、~/.ssh/config 或 ~/.kube/config)並一併監視(連鎖只延伸一步,不會覆寫既有檔案)。
瀏覽器憑證存取監控 — 1.10.0,Pro 內也需主動啟用,預設關閉
透過 FSEvents 監控 Chrome・Brave・Edge・Vivaldi・Chromium・Firefox 已儲存密碼/Cookie 資料庫的寫入。發生突發寫入時,使用 lsof 確定處理程序,將瀏覽器本身・Spotlight 等索引程式・備份守護程序以外的情況作為偵測對象。與其他新增防護不同,即使啟用 Pro 也不會自動開啟。這是因為瀏覽器擴充功能或密碼管理器的匯入功能等仍存在固有的誤判風險。
與 Sensor 的通訊(TLS 與簽章)
與同一區域網路內的 RoamSwitch Sensor(另一產品)進行配對、稽核請求與結果取得時,使用固定憑證指紋(SHA-256)的 TLS 1.3(Sensor 0.3.9 及以上)。指紋不透過網路取得,而由使用者輸入 Sensor 畫面上顯示的值。不驗證憑證授權單位、主機名稱與有效期限,只信任固定的值。TLS 失敗也不會切換為明文(切換會讓中間人得以降級)。請求簽章包含時間戳記與 nonce(v2),以防重送。只有沒有指紋的舊格式配對仍照舊以明文連線,並給出警告。
憑證變更時會拒絕連線。固定值的更新不會自動進行,只有當使用者輸入並確認新值、且實際透過 TLS 往返並成功完成帶簽章的探測時才會儲存。限制:這項探測並不能用金鑰核對該 Sensor 與先前配對的是否為同一台。它以使用者輸入在 Sensor 畫面上確認過的值為前提。
下載檔案的多層偵測(靜態簽章 + ClamAV)— 1.8.7 新增靜態簽章層
新出現在下載、桌面或文件資料夾中的檔案,會先經過 StaticSignatureScanner 的輕量級靜態簽章比對(僅檢查前 4MB,只做簡單的 AND/OR 位元組樣式比對,不進行正規表示式或熵值計算)。它不需要 EndpointSecurity entitlement,即使未安裝 ClamAV 也能持續運作。偵測範圍被刻意限定在無需真實惡意軟體樣本也能確保準確性的部分——即業界標準的 EICAR 測試簽章,以及公開的攻擊手法文件(如 PayloadsAllTheThings)中記載的 5 種經典反向 shell 單行指令(bash/sh 的 /dev/tcp/、nc -e、Python 的 pty.spawn、Perl 的 Socket、PHP 的 fsockopen)。我們不會在沒有真實樣本的情況下,為特定惡意軟體家族撰寫位元組層級的「簽章」——那只會帶來一種無法驗證的虛假安全感。這些樣式本身以 XOR 0x5A 混淆後儲存,以避免 RoamSwitch 自身的執行檔因明文包含 EICAR 字串等內容而被 ClamAV 誤判。
只有被靜態簽章層判定為「乾淨」的檔案,才會繼續進入 ClamAV 掃描。新出現在下載、桌面或文件資料夾中的檔案,無論是否帶有 .tmp 副檔名或 com.apple.quarantine 屬性(例如透過終端機複製而來),都會立即被 ClamAV 掃描。EICAR 等業界標準測試簽章會被視為「無害」,不發出通知,僅記錄至通知歷史,也不會被隔離或攔截(只有真正的惡意軟體樣本才會被隔離)。此外,即使隔離資料夾(Quarantine)中已存在同名的歷史隔離檔案,新的威脅也會以帶時間戳記的唯一檔名可靠地移入隔離資料夾,徹底避免因命名衝突導致威脅殘留在原始位置的漏判情況。
開發用 Web 資料夾中的 Web Shell 偵測(Web Rules)
對本機 Web 開發(~/Sites、MAMP、Herd、Local、/Library/WebServer/Documents,以及登記用於套件 CVE 比對的專案資料夾)中的 PHP、JavaScript、JSP、ASP 檔案,使用與 Linux 版相同的 18 條偵測規則進行檢查。規則只描述公開記載的手法,不含從真實樣本擷取的簽章。規則分三層:內建於應用程式、每天一次帶簽章的僅接收更新(併入現有的同一次取得),以及 ~/Library/Application Support/RoamSwitch/rules.d 中的自訂規則(只讀取擁有者為 root 或本人、且群組與其他使用者不可寫的檔案)。下載等已在監視的資料夾在寫入後立即檢查,開發用 Web 資料夾每天一次並可手動檢查(每天一次的檢查不會進入 node_modules 與 .git;指定資料夾的檢查會進入 node_modules)。由於不使用 EndpointSecurity,無法在執行前阻止,全部為事後偵測。
預設只自動隔離 critical。high 不會觸碰檔案,只會出現在通知與設定畫面的清單中,並可選擇「今後不再偵測」(隔離層級可在設定中變更)。偵測後掛鉤(~/Library/Application Support/RoamSwitch/hooks/post-detection)僅在擁有者為 root 或本人且他人不可寫時執行,30 秒後終止。帳號處置(預設關閉,開啟需要管理員密碼)會停用被隔離檔案擁有者的登入 shell,但不會處理 root、UID 低於 501 的帳號以及已登入的使用者(包括正在使用這台 Mac 的您)。已在 macOS 26 虛擬機中,以 root 身分在與輔助程式相同的沙盒下,驗證了真實的 dscl 變更並還原 shell(沙盒會阻止對 dslocal 的直接寫入,但不會阻止透過 dscl 進行的變更)。管理員密碼確認以及從應用程式到輔助程式的呼叫路徑尚未在真機上驗證。在開發資料夾的 114,501 個檔案中偵測到 0 筆。
監控新的自動啟動註冊(LaunchAgent/LaunchDaemon)— 1.8.7
透過 FSEvents 即時偵測新放置到 ~/Library/LaunchAgents、/Library/LaunchAgents、/Library/LaunchDaemons 中的 .plist 檔案(PersistenceMonitorGuard)。由於不使用 EndpointSecurity 的 ES_EVENT_TYPE_AUTH_CREATE,因此無法攔截寫入行為本身,能做到的是在寫入後的數秒內察覺並判定其內容。
判定的關鍵不在於「執行檔本身的簽章」,而在於「它直接要啟動的是什麼」。2026 年報告的實際手法——例如偽裝成 Google Update 的 LaunchAgent 中隱藏經 Base64 編碼的 bash 指令碼,或 root 權限的 LaunchDaemon 在每次啟動時重新執行經 Base64 編碼的 AppleScript 酬載——都是把惡意程式碼藏在指令碼本身裡,同時直接呼叫簽章合法的 /bin/bash 或 /usr/bin/osascript。只檢查執行檔本身簽章的驗證方式會被這類手法完全繞過。因此,任何新註冊的 LaunchAgent/LaunchDaemon,只要直接啟動裸腳本直譯器(bash、sh、zsh、osascript、python3、perl、ruby、php 等),即使該直譯器本身的簽章合法有效,也會被無條件發出警告。傳遞給它的引數還會進一步交給 StaticSignatureScanner 判定,如果包含已知的可疑樣式,會一併寫入通知。透過 BundleProgram(指向應用程式套件內已簽章的編譯執行檔的形式,包括 RoamSwitch 自身的 Helper)註冊的項目,則照常僅透過 codesign --verify --strict 進行簽章驗證。
偵測 ClickFix 並觸發緊急遮斷 — 1.8.7,預設關閉
「ClickFix」是一種社交工程攻擊手法:偽造的 CAPTCHA 或錯誤提示畫面會指示您「開啟終端機並貼上以下指令以完成驗證」,誘使使用者親自執行該指令。偵測到的數量在 2024 至 2025 年間激增超過 500%,截至 2026 年,它已被視為 macOS 上最猖獗的攻擊途徑之一。由於實際執行的是使用者自己合法的 shell 操作,因此會完全繞過 Gatekeeper 的簽章驗證。
選單列 →「惡意軟體防護」→ ClickFix 防護(Pro,預設關閉)。透過 FSEvents 監控對 ~/.zsh_history、~/.bash_history 的附加寫入(僅針對該防護啟動後新增的行,不會回溯既有歷史記錄),只偵測與 StaticSignatureScanner 共用的反向 shell 單行指令,以及將 Base64 解碼內容直接透過管線傳入 shell 或 osascript 的特定組合樣式。正規安裝程式中極為常見的單獨 curl | bash,由於與 Homebrew、rustup、nvm 等官方安裝步驟難以區分,被刻意排除在偵測範圍之外。一旦偵測到符合項,就會觸發前文所述的緊急氣隙。由於指令已經執行完畢,這屬於事後應對,但仍有可能攔截正在進行中的多階段酬載下載或憑證外洩。與其他氣隙觸發條件一樣,它最多會在 10 分鐘後自動解除(見本節開頭)。
已知的限制。 由於偵測的前提是寫入 shell 歷史記錄,因此無法應對繞過終端機、直接透過 applescript:// URL scheme 呼叫 Script Editor、且完全不向磁碟寫入酬載的無檔案(fileless)變種。針對此類變種,Apple 自身也導入了一定的緩解措施(對未識別指令碼彈出確認對話框),但也有報告指出存在繞過該措施的變種。我們在真機上使用 macOS 統一日誌(log show/log stream)對這條路徑進行了驗證,但未能找到可用於判定的特有日誌訊號。與其把無法涵蓋的部分標榜為已涵蓋,我們選擇如實說明這項限制。
Docker 風險偵測防護 — 1.8.9,Pro・預設關閉
選單列 →「惡意軟體防護」→「偵測 Docker 特權容器・docker.sock 掛載」(DockerEventGuard)。由於該程式碼庫中沒有像 docker events 那樣建立持久串流連線的先例,因此採用與 PortAnomalyGuard 相同的定時輪詢方式。每 20 秒透過輕量的 docker ps -q 取得容器 ID 集合,僅對與上次的差異(新啟動的容器)透過 docker inspect --format 進行詳細確認。用於判定的格式字串特意與 Linux 版的 roamswitch_core::health::LinuxHealthChecker::DOCKER_INSPECT_RISK_FORMAT 保持一致,因此兩個平台會偵測完全相同的條件(--privileged 啟動,或 /var/run/docker.sock 綁定掛載)。
即使偵測到也不會自動封鎖。特權容器或 docker.sock 掛載是可能導致容器逃逸的高風險「設定」,但並非已確認的入侵(也存在正當用途,例如刻意以 privileged 模式執行監控代理)。由於大多數使用者並不使用 Docker,因此即使在 Pro 授權下也預設保持關閉。不使用 EndpointSecurity entitlement;docker CLI 依 Docker Desktop 預設安裝路徑・Homebrew・which 的順序尋找。已在真實 Docker Desktop(29.7.2)環境中驗證 privileged 容器・docker.sock 掛載容器・一般容器三種情境,確認無誤判也無漏判。
機密資訊・API 金鑰外洩稽核 — 1.8.4 新增,1.8.9 支援資料夾掃描
選單列 →「惡意軟體防護」→「機密資訊・API 金鑰外洩稽核」。貼上文字後會立即偵測 API 金鑰與權杖的外洩模式,顯示行號、已遮罩的字串以及依類型給出的建議處理方式(SecretLeakAuditor,完全本機處理)。1.8.9 新增「選擇資料夾掃描」功能,使用相同的偵測引擎對目錄(如原始碼簽出目錄)進行遞迴稽核(auditDirectory(at:))。會自動排除 .git・node_modules・target・vendor・dist・build・__pycache__・venv,並跳過超過 2MB 的檔案或判定為二進位的檔案。處理在獨立執行緒中進行以避免阻塞主執行緒,且絕不會向外部傳送任何內容。
Link Guard(阻止網路釣魚連線)— 1.7.2 及更高版本,在 1.8.0 中得到強化
選單列→「惡意軟體防護」→「連結防護」(專業版)。阻止裝置上所有瀏覽器和應用程式與網路釣魚/詐騙網站的連線。 有兩個執行點,依優先權順序一起使用。
- ① 內容過濾系統擴充(
RoamSwitchLinkFilter,1.8.0+,一旦獲得批准即首選)。NEFilterDataProvider系統擴充 — 未經 Apple 審查(內容過濾器提供者為自助服務,無核准佇列)。它檢查 名稱解析之後的實際出站 TCP 流 。目標名稱來自作業系統解析的主機名,或未能從流的第一個位元組解析 TLS SNI — 因此瀏覽器執行自己的 DoH/DoT 並連接到裸 IP 仍會被阻止。它從不重寫/etc/hosts並將每個流歸因於一個進程。 QUIC (UDP/443) 沒有可讀的 SNI,因此在block模式下它會被丟棄,迫使瀏覽器回退到 TCP 上的 HTTP/2。需要在系統設定中一次性批准。 - ②
/etc/hosts水槽(回退,當擴充未核准/拒絕時)。 特權幫助程式將目標網域寫入/etc/hosts的分隔託管部分作為0.0.0.0並刷新快取。上限為 60,000。一旦擴充功能處於活動狀態,此部分就會被刪除。 - 三種模式。 「關閉」停用它。 「僅警告」 暫停 匹配的連線並詢問使用者(請參閱下方的 Real
warn)。 「自動阻止明顯的詐騙網站(建議)」立即下降。 從 1.7.2. 開始預設為區塊 - Real
warn(1.8.0+,僅限擴充)。 在warn上點選系統擴充功能 holds 流(作業系統命名的路徑和 SNI 路徑)與.pause()且應用程式會引發「允許/阻止」通知。當使用者的點擊達到allowlist.txt/extra.txt時(擴充功能每 1.5 秒輪詢一次小狀態檔案),保留的流將恢復(允許)或刪除(封鎖)。 25 秒內沒有答案將失敗 open(警告是建議性的)。此決策會按主機快取(允許 5 分鐘/阻止 1 小時),因此頁面的其他流程和後續存取都是即時且強制執行的。block通知未更改:「已封鎖…」和「允許一次(5 分鐘)」按鈕。 - 只有明確的案例才會被阻止。 威脅來源上的清單或品牌名稱 Unicode 同形異義詞 - 其他所有內容(高風險 TLD、子網域模仿等)都是警告。判決引擎與 Linux 版本共享,並且不會向任何地方發送 URL。
- 從錯誤的區塊中恢復。 從通知或選單允許網域(5分鐘或永久)。重新產生該部分時,將減去允許清單。
- Pro-gate. 強制執行 (
applyMode()) 僅發生在有效的 Pro 許可證上。如果沒有 Pro,模式會被存儲,但/etc/hosts永遠不會被觸及。許可證的啟動或失效將在會話期間生效。 - 源和捆綁種子。 阻止清單來自簽署的威脅來源(第 7 節,使用來源專用金鑰進行驗證)。即使在第一次提取之前,它也適用於應用程式的捆綁種子(約 60,000 個條目),並且關閉「自動更新」意味著沒有出站流量。
預防性閘道 ARP/NDP 鎖定 — 1.7.5 及更高版本
選單列→「連接埠和裝置監視器」→「將網關的 ARP/NDP 固定在不受信任的網路上(預防性)」(專業版,預設為關閉)。
- 工作原理 加入不受信任的網路時,會從
route/scutil --dns/arp -n/ndp -an收集 IPv4 閘道、IPv6 預設路由器和連結 DNS 解析器的目前 MAC,並且說明程式將每個項目固定為具有 __X65 /arp -n是可信的)。然後,這些 IP 的欺騙性 ARP/NDP 回復將被忽略,因此無法設定中間人攻擊。 - Scope. 僅固定這三種條目。可信(開放)網路永遠不會被固定。每次網路更改時,它都會解鎖一次並重新固定。離線公共解析器(8.8.8.8,...)沒有線上 ARP 條目,並且自動排除。
arp -s是預防性的;氣隙是事後發生的。 此引腳存在,因此欺騙無法成功; ARP 欺騙偵測 (ARPSpoofContainmentManager) 和緊急氣隙一旦被發現,就會「比人類更快地切斷」。他們獨立運作。- Persistence. 引腳集儲存到
/Library/Application Support/RoamSwitch/gateway_arp_lock.json,因此可以透過 XPC 重新連接或幫助程式重新啟動來解鎖。
VPN 隧道 (WireGuard / Tailscale) 及其終止開關 — 1.7.6 及更高版本,1.8.0 中可選擇後端
功能表列→「連接埠和裝置監視器」→「VPN 隧道」(Pro,預設為關閉)。這是 primary 反 MITM 防禦 - 它不依賴 L2 (ARP/NDP) 的完整性。 後端可以在「WireGuard(設定檔)」和「Tailscale(退出節點)」之間選擇(子選單→「後端」)。 RoamSwitch 本身不實現加密;只有選定的後端是武裝的。
- (A) WireGuard 後端。 驅動 Homebrew 的
wireguard-tools(無 Apple 網路擴充權利)。導入你自己的.conf;助手將其保存在0600處。 Kill-switch:pfblock drop all加上pass quick僅用於lo、隧道介面、與固定端點 IP、DHCP 和 ICMP 的 UDP 握手。非全隧道AllowedIPs會引發分割隧道警告。 - (B) Tailscale 後端 (1.8.0+). 對於已經執行 Tailscale 的使用者。 RoamSwitch not 執行
tailscale up/登入/安裝它 — 它讀取tailscale status --json並執行tailscale set --exit-node=<node>。 建議使用 Standalone 版本(從 tailscale.com 直接下載)並啟用 CLI 整合(設定 →「CLI integration」→「Install Now」,會加入/usr/local/bin/tailscale) — App Store (GUI) 建置無法從應用程式外部(沙箱)驅動;使用它,您可以在 Tailscale 應用程式中選擇退出節點,而 RoamSwitch 僅顯示狀態。 需要出口節點(透過隧道路由所有流量);如果它離線或無法訪問,它會自動撤防。 - ⚠️ 不建議使用 Homebrew 版本 (
brew install tailscale)。Homebrew 發行的 tailscaled 存在已知的 macOS 缺陷,可能無法正確改寫路由表(tailscale/tailscale#21395、#21216)——Exit Node 可能顯示為「已連線」,但流量實際仍在外洩,或者取消設定時會導致 Mac 自身的網路連線完全中斷。RoamSwitch 會驗證實際的作業系統路由表並在發生此類情況時發出警告,但使用上述 Standalone 版本可從根本上避免該問題。 - 斷開連線時的網路重新配置。 當退出節點被清除時,獨立的 macOS
tailscaled不會完全恢復路由 + DNS,因此 RoamSwitch 會反彈活動的網路服務(與手動切換 Wi-Fi 相同;僅具有 IPv4 位址的服務,對於多 NIC 和靜態 IP 是安全的)。流量下降約 5-10 秒,並且在此視窗期間保護等級不會降級。 - Tailscale 終止開關預設為關閉(選擇加入)。 出口節點已隧道化所有內容; pf
block drop all(僅允許 CGNAT/MagicDNS/STUN/DERP/DHCP/ICMP/DNS)適用於高級用戶,並且「防洩漏,而不是防洩漏」(允許使用 DNS,因此tailscaled仍然存在)。 - 自動/許可證遺失。 在不受信任的網路上出現,在受信任的網路上出現;當專業許可證失效時解除。
§6
當您連接到未註冊的網路時,「保護等級」開關不使用 pf。它只是以稍後可以逆轉的方式更改標準作業系統設定。
| 操作 | 實施 | 特權 | 是如何恢復的 |
|---|---|---|---|
| 防火牆+隱身模式開啟 | socketfilterfw --setblockall on / --setstealthmode on | 根(幫助者) | off 當您返回安全網路時 |
| 停止 SSH/SMB/螢幕分享 | launchctl卸載-w | 根(幫助者) | 僅記錄 停止時正在運行的 和返回時的 load -w (SSH 也耦合到 /usr/sbin/systemsetup -setremotelogin on 以確保現代 macOS 恢復) |
| 停用空投 | 預設寫入 com.apple.sharingd DiscoverableMode | 用戶(應用程式本身) | 保存先前的值並在返回時將其寫回 |
這些都不是 RoamSwitch 新增的新阻止機制 - 它只是切換作業系統設定。如果 你刪除應用程序,唯一停止的是依賴網路的切換;最後一個作業系統 已套用的設定保持原樣。沒有任何東西被鎖定,但如果你想犯錯 安全起見,在卸載之前,在受信任的網路上將其設定回「開啟」。
§7
Mac 上保留的內容
| 數據 | 地點 | 內容 |
|---|---|---|
| 許可證令牌 | 鑰匙圈 com.tetsuharu.RoamSwitch.license | Ed25519 簽署的令牌。 kSecAttrAccessibleAfterFirstUnlock |
| 應用程式設定/防護開關 | 用戶預設套件 com.tetsuharu.RoamSwitch | 可信任網路註冊、保護策略、排除清單等 |
| PF狀態 | /庫/應用程式支援/ | 氣隙時間戳記、受保護連接埠的 JSON、VPN 終止開關狀態、所套用的規則集的暫存文件 |
| Link Guard 威脅來源 | 〜/庫/應用程式支援/ | 下載的網路釣魚/詐騙網域清單(如果尚未獲取,則為應用程式的捆綁種子)。 Feed 版本位於 UserDefaults 中 |
| Link Guard 託管部分 | /etc/主機 | 由 # BEGIN RoamSwitch link guard ... # END 界定的分隔部分,將阻止的域清空為 0.0.0.0。當模式為「關閉」時刪除 (§5) |
| ARP/NDP 鎖銷 | /庫/應用程式支援/ | IP → MAC 裝置由預防鎖 (§5) 製成 permanent。解鎖後刪除 |
| WireGuard 配置 | /Library/Application Support/0600) | 使用者導入的 .conf。端點主機名稱也儲存在 UserDefaults 中(應用程式解析它) |
| 設備後備 UUID | 使用者預設值 | 僅當 IOKit 不傳回 UUID 時產生的隨機值 (§9) |
| 紀錄 | os.Logger/NSLog | 統一日誌記錄。沒有任何內容被傳送到外部 |
離開機器的流量(完整清單)
任何地方都沒有收集並傳送診斷結果、連接埠資訊、URL 或日誌的程式碼。既沒有整合分析 SDK,也沒有整合當機回報 SDK。唯一的外部函式庫是 Sparkle(更新)。出現在網路上的通訊,就是下面這 8 項,再加上表格下方所述由使用者操作觸發的功能、預設關閉的功能以及 DNS 威脅防護,僅此而已(第 6 項僅在使用者設定 VPN 時發生,第 7 項僅在啟用 RoamSwitch Sensor 配對時發生)。
| 連接方式 | 目的地 | 當它發生時 | 發送了什麼 |
|---|---|---|---|
| 許可證啟用/停用 | lafine.net /api/v1/license/* |
僅當使用者輸入許可證密鑰或停用 Pro 時 | 授權金鑰、裝置雜湊、裝置名稱(系統設定中的「名稱」)、App 版本。解除時僅傳送金鑰與裝置雜湊。個人資訊由 Stripe 在購買時處理,App 不處理 |
| 更新檢查 | lafine.net /updates/appcast.xml |
Sparkle 每 24 小時檢查一次(距上次檢查已超過 24 小時時,啟動時也會檢查)。預設一律啟用,不傳送系統資訊 | HTTP 請求(標準 UA 和版本)。下載的項目由 EdDSA 簽章驗證 (§10) |
| Link Guard 威脅來源 | lafine.net /updates/v1/{manifest, feed/<版本>.txt} |
在 Pro 版中,連結保護的「自動更新」開啟(預設)時,每 24 小時一次(以及啟動時)。關閉「自動更新」後,此路徑即消失 | 僅使用 GET。沒有查詢字串、Cookie,也沒有識別裝置的資訊(標準 User-Agent 中包含應用程式名稱、版本與 macOS 版本)。這是僅接收的已簽署靜態檔案,清單與來源本文均以 Ed25519 驗證。簽章金鑰專用於該來源,與應用程式更新的 SUPublicEDKey 是不同的金鑰(即使外洩,影響也僅限於「錯誤的封鎖清單」) |
| 套件 CVE 比對 / 實證型弱點診斷的更新資料 | lafine.net /updates/v1/manifest |
應用程式啟動時(距上次取得已超過 30 天時)。每 24 小時檢查一次,實際取得每 30 天一次。始終進行(套件 CVE 比對的更新資料沒有關閉開關,因為它是僅用於本機清單比對的僅接收資料,包括 Free 版在內的所有使用者均適用。實證型弱點診斷的 CVE 對應表僅在啟用「實證型弱點診斷(主動可達性驗證)」時才會取得) | 僅 GET。沒有查詢字串、沒有 cookie,也沒有任何可識別裝置的資訊(標準 User-Agent 中包含應用程式名稱、版本與 macOS 版本)。這是僅接收的已簽章靜態檔案;清單與來源資料本體均透過 Ed25519 驗證。與 Link Guard 威脅來源重複使用相同的金鑰與相同的清單,但實際取得的來源檔案(Homebrew/npm/PyPI/crates.io/RubyGems/Packagist/Go/Maven 的已知 CVE 對應表、實證型弱點診斷的 CVE 對應表,以及用於打字仿冒偵測的熱門 npm 套件名稱清單)是各自獨立的檔案 |
| ClamAV 病毒定義更新 | ClamAV 官方鏡像 | 使用者安裝了 ClamAV 時。Pro 版會在啟動時、每 24 小時以及自主巡檢時自動啟動 freshclam;Free 版僅在點選選單中的「更新病毒庫」時啟動 |
ClamAV 的標準病毒庫取得。不包含來自 RoamSwitch 的資訊(可能包含 ClamAV 自身的標準識別資訊) |
| VPN 隧道(§5,WireGuard / Tailscale) | 使用者配置的 WireGuard 端點,或 Tailscale 控制平面(Tailscale, Inc.) | 僅當使用者已設定 VPN 隧道(Pro,預設為關閉)並加入不受信任的網路時才會發生。選擇 WireGuard 時,在隧道建立之前端點主機名稱會透過 DNS 解析一次。選擇 Tailscale 時,這是由使用者自行安裝並登入的 tailscaled 處理的控制平面通訊,RoamSwitch 本身僅讀取 tailscale status --json 並執行 tailscale set --exit-node= | 選擇 WireGuard 時:WireGuard 握手(UDP)和隧道內的流量。目的地是使用者自己的 VPN 伺服器,內容是使用者自己的流量。選擇 Tailscale 時:使用者自己的 Tailscale 帳戶與 Tailscale, Inc. 控制平面之間的通訊(裝置註冊、DERP 中繼等,遵循 Tailscale 自身的隱私權政策)。無論哪種情況,RoamSwitch 都不會加入任何識別碼或診斷資料 |
| RoamSwitch Sensor 配對與稽核連動 | 同一區域網路上的 RoamSwitch Sensor(使用者自行部署、以固定 IP 執行的裝置) | 僅在啟用「RoamSwitch Sensor 配對」(Pro,預設關閉) 並輸入 Sensor 的 IP 位址與配對碼完成配對時(一次性通訊)。配對後,僅在您點擊「向 Sensor 請求稽核」,以及自動取得結果期間(請求發出 5 分鐘後開始,每隔 5 分鐘一次,最多 5 次),才會再次連線到同一個 Sensor 固定 IP | 配對時會傳送本端點的主機名稱和 Ed25519 公開金鑰,並接收 Sensor 發放的公開金鑰。稽核請求與結果取得均透過 Ed25519 簽章驗證,傳送的僅是帶簽章的請求。診斷結果或連接埠資訊只會作為 Sensor 的回應被接收,絕不會傳送檔案內容。通訊目的地僅為使用者自行部署的 Sensor 的固定 IP 位址,絕不會到達包括 lafine.net 在內的任何外部伺服器。相互信任僅透過配對碼這項明確操作建立 |
| 結帳頁面 | 條紋結帳 | 僅當用戶按下購買按鈕時(它在瀏覽器中打開) | —(瀏覽器導航) |
這裡所說的「Zero Telemetry」,是指不蒐集並傳送使用情況或診斷結果的遙測,並不代表完全沒有通訊。上表的 8 條路徑以及表格下方所述的功能確實存在。但它們要麼由使用者觸發,要麼是附簽章驗證的僅接收取得,要麼是與使用者所選外部服務(ClamAV 散布來源、防護 DNS 服務供應商、被檢查的 URL 等)之間的通訊,Mac 上的診斷結果、連接埠與檔案內容不會被傳送給開發商。
Link Guard 威脅來源(第 3 行)在“它不發送任何內容”之上添加了“它確實提取更新” 防禦。兩者保持分開; Linux 白皮書 §1.1 同樣拆分了“零” 「遙測」來自「僅接收更新」。關閉“自動更新”,Link Guard 在捆綁包上運行 資料(約 60,000 個網路釣魚/詐騙網域)加上離線同形異義字偵測,且不會出現此路徑。
VPN 隧道(第 6 行)僅在使用者設定自己的 WireGuard 伺服器,或選擇 Tailscale 後端並已使用自己的 Tailscale 帳戶登入 tailscaled 時才會出現;目的地和內容都在使用者的控制之下。 RoamSwitch 僅負責建立隧道、維持斷網開關,或讀取 Tailscale 狀態以切換出口節點—— 不加入任何識別碼或使用資料。在未設定 VPN 的情況下,該路徑不存在。
RoamSwitch Sensor 配對(第 7 行)是僅限 Pro 且預設關閉的功能,除非您啟用它,否則該通訊路徑不存在。配對本身僅透過明確操作建立——輸入 Sensor 操作者發放的配對碼,以及 Sensor 的固定 IP 位址。配對完成後的所有通訊(稽核請求、結果取得)也都送往使用者自己擁有的這同一個固定 IP,並透過 Ed25519 簽章驗證。絕不會到達包括 lafine.net 在內的任何外部伺服器。診斷結果或檔案內容除了作為 Sensor 自身的回應外,絕不會被傳送。
表格中未列出的通訊如下。它們都不會把診斷結果或檔案內容傳送給開發商。(1)Pro 版的 DNS 威脅防護會把系統 DNS 切換為 Quad9、Cloudflare、AdGuard、CleanBrowsing 之一(預設適用範圍為「外出時、不受信任的 Wi-Fi」)。生效期間,網域名稱解析查詢會送往該服務供應商。(2)Pro 版的 npm 簽章驗證每次執行都會先請求確認,然後查詢 npm 登錄庫。(3)實證型弱點診斷(主動可達性驗證)僅限於這台 Mac 本機(127.0.0.1)。(4)「連結安全性診斷」視窗直接向目標 URL 傳送的 HEAD 請求,見下一段。
應用程式內「連結安全性檢查」表格向目標 URL 發送 HEAD 請求以查看
縮短的 URL 到達的地方(以下重定向到私人或本地地址被
v1.4.5 SSRF 緩解)。相較之下,MCP audit_url_safety 是離線分析
當場完成並將 URL 發送到任何地方(§8)。
這不只是一種說法。2026-08-29,我們在運作中的 1.4.7 上,使用 tcpdump + 程序歸屬(nettop / lsof / 經過篩選的 pktap 擷取)+ LuLu,進行了約 2 小時的稽核,期間安全等級固定為最大鎖定,並強制觸發 appcast 檢查。結果(1.4.7 時點):歸屬於 RoamSwitch / RoamSwitchHelper / RoamSwitchMCPServer 的對外通訊,除對 lafine.net 的 appcast 檢查外沒有其他;MCP 伺服器開啟的 socket 僅限本機迴路;entitlements 傾印為空。此次實測針對的是 1.4.7。此後新增的路徑已列在上表中,並預計對目前版本重新實測。
完整的文章和任何人都可以運行來重現它的腳本: 審計/RESULTS-2026-08-29.md
此次實測針對的是 1.4.7。此後新增了連結保護的威脅情報(1.7.2,上表第 3 行)、VPN 通道(1.7.6,第 6 行)、套件 CVE 比對的更新資料(第 4 行)、Sensor 配對(第 7 行)等路徑。在目前版本上稽核,會觀察到送往 lafine.net 的僅接收 GET(appcast、威脅情報、更新資料);若已設定 VPN 通道,還會觀察到送往使用者自己 WireGuard 伺服器的 UDP(目的地與內容均由使用者掌控)。威脅情報在「自動更新:關閉」時消失,VPN 通訊在「未設定 VPN」時消失(appcast 和套件 CVE 比對的更新資料沒有關閉開關)。
§8
RoamSwitchMCPServer 是一個獨立的命令列工具,捆綁在
RoamSwitch.app/Contents/MacOS/。 MCP 用戶端,例如 Claude Desktop 或 Claude Code
將其作為子進程啟動並透過 stdio (換行符分隔的 JSON-RPC
2.0)。官方 SDK 不會針對本機的 macOS SDK 進行構建,因此
在基金會的 JSONSerialization 之上手動實現。
設計約束什麼
- 它是唯讀的。 根本沒有用於更改安全性等級、隔離連接埠或彈出設備的 API。這並不是 v1 中被遺忘的東西——它是被故意遺漏的。讓外部程式碼(此處為 LLM)重寫安全工具的保護狀態會破壞每個使用者的信任。
- 不會開啟監聽通訊端。既不註冊服務,也不監聽連接埠。它從 stdin 讀取一行、向 stdout 回傳一行,之後由用戶端終止處理程序。
- 不傳送任何遙測資料。與外部主機通訊的只有需主動啟用(Mac 上僅限 Pro)的 run_npm_audit_signatures(npm 登錄庫)。run_active_vuln_scan(預設關閉)只向 127.0.0.1 傳送探測,Mac 上的 get_exposed_ports 只會對每個暴露的連接埠向 127.0.0.1 傳送 1 次 HTTP GET 以檢查標頭。診斷類工具為了查詢預設閘道的 MAC 位址,可能會向區域網路內的閘道傳送 ICMP ping,通常 1 次(查不到 MAC 時最多數次)。
- 它從不同的域讀取設定。
UserDefaults(suiteName: "com.tetsuharu.RoamSwitch")明確讀取應用程式的域(其自己的捆綁包 ID 域為空)。它只會讀取;它不寫。
它公開的工具
| 工具 | 它返回什麼 | 交通 |
|---|---|---|
| 取得安全報告 | 18 項診斷(FileVault / SIP / Gatekeeper / 自動更新 / XProtect / 防火牆 / Wi-Fi 加密 / ARP / 閘道器 ARP 固定 / SSH 設定稽核 / Sudo NOPASSWD 稽核 / 公開連接埠 / 防護設定等)的評分與各項目改善建議。每個項目還會附帶對應的 NIST CSF 2.0・CIS Controls v8 控制項 ID(1.10.0 起)。 | 限本地 |
| verify_security_findings | 依目前狀態重新評估 get_security_report 中的各項目(checkId),並為每個項目回傳 stillPresent(仍然存在)、resolved(已解決,或依設計不適用)、inconclusive(無法判定)三者之一,以及與語言無關的原因代碼。用於在修復之後確認問題是否真的消除;因權限不足、特權輔助程式未連線、無法存取 Docker 等原因而未能量測的項目,不會被回報為安全,而是回傳 inconclusive。省略 checkIds 時檢查所有項目(最多可指定 Mac 50 項、Linux 64 項;Linux 還可指定 isServer)。Mac 上的 host_firewall、network_stealth_mode、gateway_arp_lock、malware_scanning、dns_threat_guard、usb_zero_trust 由 RoamSwitch 應用程式內設定的值決定是否合格,因此這些項目反映的是應用程式內設定,而不是對作業系統實際狀態的重新量測。唯讀,不會連線外部主機,也不會進行修復或變更任何設定。在 Linux 上也不會向區域網路傳送任何內容,只會從本機快取讀取閘道。在 Mac 上,為查詢閘道的 MAC 位址,可能會向區域網路內的閘道傳送 ICMP ping,通常 1 次(查不到 MAC 時最多數次)。 | 限本地 |
| audit_mcp_configs | 讀取 AI 用戶端的 MCP 設定檔(Claude Desktop、Claude Code、Cursor、Windsurf、VS Code、Cline、Gemini CLI,以及已登錄專案資料夾中的 .mcp.json 等),指出有風險的設定。會偵測不需逐次確認就自動核准的工具(auto_approve)、非 loopback 主機上的明文 http:// 伺服器(不僅是 url 欄位,以 --server http://… 這類啟動參數傳入的連線目的地也包括在內。remote_plain_http)、以 shell 或直譯器單行指令啟動(shell_wrapper)、取得未固定版本的套件或映像檔的 npx / uvx / docker run 等啟動方式(unpinned_package_launcher)、帶有 --privileged 或掛載了 Docker 套接字、根檔案系統的 docker run(container_host_access)、讓行程在啟動時載入額外程式碼的環境變數(如 LD_PRELOAD、NODE_OPTIONS 中的 --require;名稱不區分大小寫,不回傳值;code_loading_env)、指向無認證攻擊工具後端 HexStrike AI 的設定(hexstrike)、群組或其他使用者可寫入的設定檔(world_writable_config),Claude Code 的 enableAllProjectMcpServers(auto_trust_project_servers),以及同一設定檔中同時存在讀取他人撰寫文字(網頁、郵件、聊天、Issue)的伺服器和可執行指令或修改檔案的伺服器的組合(間接提示注入的前提條件;依知名套件名稱判斷,不檢視伺服器工具的內容;injection_exposure)。無參數、唯讀,不會向網路傳送任何內容。不會回傳環境變數或標頭的值、參數與任何機密,只回傳伺服器名稱、指令名稱、URL 主機,以及(為登錄庫名稱時的)套件或映像檔名稱。無法解析的檔案會列在 unreadableFiles 中,表示未被稽核。 | 無 |
| audit_agent_setup | 讀取提供給 AI 代理的指示檔與技能(~/.claude 下的 CLAUDE.md、commands、agents、skills,以及 ~/.codex、~/.cursor/rules、~/.gemini、~/.agents、~/.openclaw,和已登錄專案資料夾中的 CLAUDE.md、AGENTS.md、.cursorrules 等)和 OpenClaw 的 openclaw.json,指出危險的內容與設定。對於指示與技能檔,會偵測隱藏在不可見 Unicode 標籤字元中的句子(hidden_tag_characters)、零寬字元和改變文字方向的控制字元(invisible_characters)、要求忽略先前指示或向使用者隱瞞的措辭(override_instruction)、curl … | sh 這類下載後直接執行的寫法(remote_script_exec)、解碼後執行編碼字串(encoded_payload_exec)、對金鑰與憑證存放位置的引用(credential_access)及其附近的外送或資料接收服務位址(credential_exfil_shape、exfil_endpoint)、設定重新啟動後仍保留的機制(persistence_instruction)、技能中的已編譯程式碼(skill_precompiled_code)和他人可寫入的檔案(agent_file_writable)。HTML 註解中的內容在渲染後會被隱藏,因此以較高風險處理。對於 OpenClaw,會偵測驗證被關閉(openclaw_auth_disabled)、可從網路存取的監聽方式(openclaw_wide_bind)、不足 24 個字元的權杖(openclaw_weak_token)、允許所有來源(openclaw_origin_wildcard)、已開啟的 dangerously… 開關(openclaw_dangerous_flag)、信任所有對象的代理設定(openclaw_broad_trusted_proxy、openclaw_proxy_trust_loosened)、對任何人開放的私訊或群組策略(openclaw_open_dm)、mDNS 的 full(openclaw_mdns_full)、被隱藏的自我檢查結果(openclaw_audit_suppressed)以及寬鬆的權限(openclaw_loose_permissions)。所有規則都是確定性的,符合只表示「需要人來讀一讀」,並不斷定為惡意。無參數、唯讀,不會向網路傳送任何內容。不會回傳檔案本文或機密(權杖只回報長度),只回傳規則名稱、行號、URL 主機和設定鍵路徑。無法讀取的檔案會列在 unreadableFiles 中,表示未被稽核。 | |
| 取得暴露端口 | 偵聽 TCP 連接埠的清單。對於在本機之外暴露的連接埠,會與已知危險服務資料庫比對,並對每個暴露的連接埠向 127.0.0.1:port 傳送 1 次 HTTP GET(本機封閉)以檢查 CORS / 標頭 | 只探測到127.0.0.1 |
| 獲取守衛狀態 | Pro自動回應防護的開/關狀態(連接埠異常/ARP/USB/藍牙/Web+郵件下載/DNS威脅防護)、目前防護等級和可信任網路狀態 | 限本地 |
| 審核網址安全 | 判斷 URL 是否為網路釣魚/同形異義字(Unicode 欺騙)/品牌子網域欺騙/高風險 TLD/明文 HTTP。它是同步且完全離線(analyzeURL;它不遵循重定向) | 無 |
| 取得應用程式協助 | 捆綁知識庫的全文搜尋(功能規格/設定/故障排除/通知訊息解釋) | 無 |
| audit_secrets | 從文字、檔案或目錄中偵測外洩的 API Key 與私密金鑰(偵測值於輸出時已遮蔽)。 | 無 |
| audit_security_logs | 彙總近期安全記錄(Mac:Sudo 驗證失敗、SSH 暴力破解嘗試、Gatekeeper 攔截、XProtect 偵測等 / Linux:Sudo 驗證失敗、SSH 暴力破解、防火牆攔截、AppArmor 拒絕、ClamAV 偵測等),自動遮蔽 API 金鑰、權杖等機密資訊,並回傳以記錄範本化的新模式偵測與頻率異常(統計突增)偵測結果。在 Linux 上,如果無法讀取 journal(例如逾時),統計中的 0 筆並不表示什麼都沒發生,此時 journalReadComplete 為 false,並在 journalError 中給出原因。 | 限本地 |
| run_active_vuln_scan | 僅限 127.0.0.1 的非破壞性實證型漏洞驗證。不會連線其他主機,但屬於會發送網路請求的工具,因此預設關閉,需在設定中主動啟用。 | 僅 127.0.0.1(預設關閉) |
| run_package_cve_scan | 將已安裝的套件(Mac:Homebrew / Linux:dpkg・dnf・zypper・pacman)與本機 CVE 對照表比對。完全不產生任何網路通訊。在 Linux 上,如果無法取得套件清單或逾時,不會回報「無符合項目」,而是 complete 為 false,並在 incompleteReason 中給出原因(未完成的結果不等於沒有問題)。 在 Ubuntu(20.04、22.04、24.04、26.04)上,使用 Ubuntu 專用資料,依來源套件名稱與來源版本比對,只檢查 Ubuntu 已發布修復、CVSS 7.0 以上的 CVE,因此「無符合項目」不代表沒有已知 CVE。結果中增加 ecosystem 與 release,每筆偵測帶有 binary(已安裝的二進位套件名稱);只有 Ubuntu Pro 才能修復的漏洞帶有 proOnly。Ubuntu 衍生版(Linux Mint、Pop!_OS 等)與不支援的版本會回傳 complete 為 false,並在 incompleteReason 中給出原因,不會改用 Debian 的資料。 | 無 |
| run_package_cve_scan_languages | 將 npm / PyPI / crates.io / RubyGems / Packagist / Go / Maven 等相依性鎖定檔與同一本機 CVE 對照表比對。完全不產生任何網路通訊。 | 無 |
| get_quarantine_status | 回傳惡意軟體隔離 Vault 的內容(原始路徑、偵測到的威脅名稱、隔離時間與大小)。 | 限本地 |
| get_canary_status | 回傳勒索軟體誘餌檔案(Canary)的佈署狀態,以及最近 50 筆偵測事件。 | 限本地 |
| get_ransomware_recovery_snapshots | 回傳用於勒索軟體復原的 APFS 快照(事前、偵測時、手動)清單、建議的復原來源、保留模式狀態以及快照間隔設定。偵測時的快照可能已包含被加密的檔案。僅限 Pro。 | 限本地 |
| search_exec_events | 依路徑、參數或簽章 ID 檢索程序執行記錄(eslogger),並回傳記錄器狀態及記錄是否可讀。僅限 Pro。 | 限本地 |
| get_process_tree | 根據執行記錄還原指定程序的祖先與後代。僅限 Pro。 | 限本地 |
| get_notification_history | 回傳 RoamSwitch 傳送的通知(安全記錄稽核異常、ClickFix 偵測等)歷史記錄,最近1週、最新在前。 | 限本地 |
| get_port_anomaly_incidents | 回傳連接埠異常防護的基準狀態、目前自動阻斷的連接埠,以及最近 50 筆事件。回應中明確註明:目前自動阻斷的連接埠是不含時間戳記的當前狀態快照,與含時間戳記的事件歷史是兩回事。 | 限本地 |
| get_ssh_guard_incidents | 傳回 SSH 暴力破解防護的狀態與紀錄。包括:是否執行封鎖還是只做偵測(例如另一個防禦工具已在保護 sshd)、目前被封鎖的來源位址與剩餘時間,以及最近的紀錄(封鎖、僅偵測、失敗累積的來源位址登入成功,三者之一,最多 200 筆)。嘗試過的使用者名稱本身在 Linux 上不儲存(只記數量),在 Mac 上經過無害化處理後最多傳回 5 個。Mac 還會傳回最近的 SSH 登入(來源位址、使用者、驗證方式、金鑰指紋)。唯讀,沒有解除封鎖的 MCP 工具。Linux 上封鎖清單只有 root 能讀取,因此以一般使用者執行時只傳回數量。Linux 可用於 Server Edition,以及 sshd 正在執行的 Client Edition;Mac 僅限 Pro 版。 | 限本地 |
| get_runtime_threat_status | 回傳是否因 XProtect 偵測到惡意軟體而進入 Air-Gap 隔離,以及觸發該隔離的事件。排查 Air-Gap 原因時應最先檢視此工具。 | 限本地 |
| get_ransomware_entropy_guard_status | 回傳通用熵值型勒索軟體偵測的啟用狀態,以及最近的熵值突發偵測事件。 | 限本地 |
| get_honeytoken_status | 回傳憑證蜜罐權杖的啟用狀態,以及最近的存取偵測事件。 | 限本地 |
| get_browser_credential_watch_status | 回傳瀏覽器憑證存取監控的啟用狀態,以及最近的存取偵測事件。 | 限本地 |
| get_forensic_evidence_bundles | 回傳緊急 Air-Gap 觸發時儲存的鑑識證據包(處理程序清單、網路連線、變更檔案等)清單。 | 限本地 |
| get_vulnerability_scan_history | 回傳實證型漏洞診斷(run_active_vuln_scan)所記錄的各項目最新結果及經過天數(pass_age)。 | 限本地 |
initialize 回應中包含的 instructions 也明確寫明「Cannot change security level,
isolate ports, or eject devices」,向用戶端的 LLM 傳達能力邊界。MCP 資源
(roamswitch://docs/*)同樣是唯讀的 Markdown 文件。1.10.0 還新增了
agentskills.io 格式的分診流程(roamswitch://skills/*,共 4 份,固定為英文)。
2026 年 9 月,我們使用本 MCP 伺服器讓本機 LLM(Qwen 3.8 27B)進行一次事件調查實戰演練(fire drill)時,發現模型誤讀了 get_port_anomaly_incidents 回傳的資料,把一個與本次事件無關的舊連接埠阻斷紀錄,錯誤地編入了目前正在發生的事件敘述中。原因在於:不含時間戳記的「目前阻斷連接埠」快照,與含時間戳記的真實事件歷史,在同一個回應中未加區分地一併回傳。修正方案是新增說明欄位,明確標註哪一部分是時間軸資料、哪一部分只是快照。修正後,用同一情境對同一模型重新執行,模型引用了說明欄位中的文字,明確拒絕將舊連接埠歸因於目前事件。我們透過自動迴歸測試與真實本機 LLM 重現兩種方式,驗證了修正的有效性。
此伺服器及其使用的偵測邏輯(ARP 監控、連接埠掃描、
連接埠稽核、18 項健康檢查、URL 安全分析)的原始碼,已於
github.com/lafine1211/roamswitch-mcp 公開(MIT 授權,
為出貨程式碼的鏡像,按每次發布加上標籤)。您可以直接在程式碼中確認它是否為唯讀、它傳遞給 LLM 的內容,以及
它沒有發送任何內容出去。其中不包含特權助手、pf 控制、實際執行攔截的各項防護,
以及授權機制——這些仍保留在應用程式儲存庫中。
測試也隨之而來——鏡像單元測試、對抗性輸入測試和突變模糊測試,
由 swift test 運行並在 CI 中驗證。模糊測試發現了一次無人防範的崩潰
(深度嵌套的 JSON 物件上的 JSONSerialization 堆疊溢位);它是固定的
在解析器之前進行嵌套深度檢查,並記錄在 SECURITY_TESTING.md 中。
§9
代幣
- 它使用Ed25519(Curve25519簽名)。 公鑰嵌入在應用程式中(
LicenseVerifier.embeddedPublicKeyBase64)。對應的私鑰僅存在於許可證後端(Firebase Functions 環境變數)中,而不存在於儲存庫中。 - 簽章資料是規範的 JSON。 簽章是透過使用
JSONEncoder的.sortedKeys和.withoutEscapingSlashes編碼LicensePayload(許可證金鑰、層、裝置雜湊、頒發時間、到期日、席位數)產生的確切位元組來建立和驗證的。 - 它被設計為關閉失敗。 如果嵌入的金鑰遺失或格式錯誤,或無法產生簽章或規範 JSON,則結果不會「驗證」 - 它傳回
invalidSignature。
裝置綁定
device_hash = SHA-256( "RoamSwitch-LifetimeSalt-v1" : lowercase(IOPlatformUUID) )
原始硬體 UUID 不會傳送到伺服器。在極少數情況下 IOKit 不返回
UUID,它會回退到保存在 UserDefaults 中的隨機 UUID。在驗證時,如果
令牌的 device_hash 與目前設備雜湊不匹配,結果為
deviceMismatch。
它可以離線工作
validateSavedLicense() 在啟動時所做的就是從鑰匙圈中讀取令牌並
使用嵌入的公鑰在本地驗證它。它不會連接到網路。如果
許可證伺服器關閉後,Pro 功能仍可在已啟動的 Mac 上繼續運作。
僅在新啟用和明確停用時才聯繫伺服器。的
盡最大努力向伺服器發出停用通知 - 即使失敗,本地停用也會
總是完成。
預設為一次性(終身)購買;僅當以下情況時才檢查 expires_at
is_lifetime 是假的。座位數依等級表示 — 2 表示個人 Pro,5 表示個人 Pro
團隊。
§10
簽名及公證(scripts/release.sh)
- 運行
xcodebuild archive(發布、手動簽名、開發者 ID 應用程式)。 - 將
-exportArchive匯出為method: developer-id。 - 在
notarytool submit --wait之後,在 .app 上執行stapler staple。 - 使用
spctl -a -t exec -vv進行驗證。 - 裝訂後重新壓縮 以產生 Sparkle 更新工件(因此包含公證票並且它可以離線運行而不會出現 Gatekeeper 警告)。
- 建造 DMG,公證並裝訂 DMG,並使用
stapler validate進行驗證。
更新(Sparkle 2.9.6)
| 鑰匙 | 價值 |
|---|---|
| SUFeedURL | https://lafine.net/updates/appcast.xml |
| SU公有金鑰 | CNxzwijMzMCJzliId76Yl88S/9np6t/xg/zQ9YbYzHs= |
| SU啟用自動檢查 | 真實 |
| SUScheduledCheckInterval | 86400 |
在套用更新之前,會先驗證 appcast 中所列的 EdDSA 簽章
針對應用程式中嵌入的 SUPublicEDKey 。私有簽署金鑰僅存在於
建構環境。 appcast 透過 HTTPS 提供服務。 Delta 更新經過簽名驗證
同樣的方式。
appcast.xml URL 是公開的這一事實本身並不是一個弱點。其內容
僅包含版本號、發行說明、下載 URL、檔案大小和 EdDSA 簽名
每個構建——沒有什麼秘密。信任的錨不是“appcast 在傳輸過程中是否真實”,而是
使用應用程式中內建的公鑰驗證工件的簽名。攻擊者可以
完全替換appcast(MITM、DNS劫持、入侵web主機)仍無法推送
沒有簽署金鑰的惡意更新。 Gatekeeper(開發者 ID 和公證)是
第二個門。
仍然存在兩個風險。一種是更新未到達,因為主機已關閉或 appcast 已損壞 (不會發生錯誤的安裝-你只是沒有得到更新)。另一種是凍結攻擊 故意隱瞞安全更新。 Sparkle 2.x 透過檢查拒絕降級和重播 版本排序,但完整的防禦凍結需要一個專用的更新伺服器 到期。這是我們要解決的問題。
§11
RoamSwitch 旨在處理什麼
- 來自同一 LAN 上的攻擊者或來自受感染的 IoT 設備的探測和攻擊。它透過隱密、暴露連接埠審核和與外部隔離來回應。
- 暴露在您不信任的網路上。它會自動停止共享服務和 AirDrop。
- 中間人攻擊(ARP/NDP 欺騙)。自 1.7.6 起採用多層防禦:(1) VPN 隧道 + 終止開關(§5,核心防禦,不依賴 L2 的完整性),(2) 在不受信任的網路上對閘道做 ARP/NDP 預防性固定(§5),(3) 欺騙偵測與緊急氣隙(事後)。VPN 隧道與閘道固定為 Pro 功能,預設關閉;欺騙偵測預設開啟,緊急氣隙在啟用 Pro 後預設開啟。
- 尋找在未經身份驗證的情況下暴露在
0.0.0.0上的開發伺服器和資料庫(Redis、MongoDB、Elasticsearch 等),並從外部阻止它們。 - 及早捕獲類似勒索軟體的未經授權的加密活動並停止所有流量(它不依賴簽名)。
- BadUSB 和實體鍵盤核准防護(
USBKeyboardGuard、CGEventTap + IOKit)可攔截並丟棄來自未經批准的 USB 鍵盤/電纜(Rubber Ducky、O.MG 電纜等)的擊鍵,以防止自動命令注入攻擊。 - 對未知 USB 儲存的核准提示(無法辨識的磁碟機保持唯讀狀態,而不是立即彈出),以及對連接的儲存進行自動 ClamAV 掃描(選購)。
- 完全在本機偵測剪貼簿中的敏感 API 金鑰(OpenAI、Anthropic、GitHub、AWS 等),防止誤貼上至網頁表單或 AI 聊天中。
- 偵測並警告從 Hugging Face 或網路下載的危險 Pickle 格式 AI 模型檔案(存在任意程式碼執行風險)。
- 下載檔案中業界標準的 EICAR 測試簽章,以及公開文件記載的經典反向 shell 單行指令(
StaticSignatureScanner,1.8.7+,即使未安裝 ClamAV 也能運作)。 - 直接呼叫裸腳本直譯器的新增 LaunchAgent/LaunchDaemon 自動啟動註冊(
PersistenceMonitorGuard,1.8.7+)。 - 「ClickFix」型攻擊——由偽造的警告畫面誘導使用者在終端機中親自執行指令——透過監控 shell 歷史記錄中的已知可疑指令樣式來偵測,並觸發緊急氣隙(
ClickFixGuard,1.8.7+,Pro,預設關閉)。 - 在偵測到
--privileged啟動或/var/run/docker.sock綁定掛載等具有容器逃逸風險的 Docker 設定的瞬間發出通知(DockerEventGuard,1.8.9 起,Pro・預設關閉・僅通知)。 - 不僅支援貼上文字,還支援按資料夾進行遞迴掃描的機密資訊・API 金鑰外洩稽核(
SecretLeakAuditor,1.8.9 起支援資料夾掃描,完全本機處理)。
我們決定不做什麼
- 它不是防毒軟體的替代品。 ClamAV 和 XProtect 用作輔助工具; RoamSwitch 本身並不是通用惡意軟體偵測器。
- 我們決定不申請 EndpointSecurity entitlement。個人開發者帳號(Apple Developer Program Individual)的核准率偏低,因此我們在此預先說明:只要這項方針不變,任何真正需要 pre-exec 攔截(例如
ES_EVENT_TYPE_AUTH_EXEC)的功能,都會被視為已關閉的非目標(non-goal)。作為無法阻止執行本身的替代方案,我們在不申請 entitlement 的前提下,實作了在實務上已足夠充分的事後(post-hoc)偵測——靜態簽章偵測、自動啟動監控,以及 ClickFix 防護。 - 這不是一個保證。 它是深度防禦堆疊中的一層,而不是「完全防止勒索軟體」。行銷文案也是在這個前提下進行審查的。
- 它無法保護已經受損的 root 或核心。 如果攻擊者已經擁有 root,他們也可以刪除 helper 的 pf 規則。
- 它本身不會恢復 L2 完整性。 預防性 ARP/NDP pin 是首次使用時信任的 — 如果攻擊者在您連線之前就已經就位,它可以固定一個欺騙性的 MAC。當您不想做出這樣的假設時,VPN 隧道 (§5) 就是答案:即使 L2 中毒,內容也會被加密,終止開關會阻止明文洩漏。它不能取代企業 DHCP 監聽或動態 ARP 檢查。
- 它不提供 VPN 伺服器。 隧道功能使用使用者提供的 WireGuard 設定; RoamSwitch 不會成為 VPN 提供者。
安裝 RoamSwitch 增加的攻擊面
| 攻擊面 | 它是如何包含的 |
|---|---|
以 root 身分執行的 LaunchDaemon 及其 mach 服務 (com.tetsuharu.RoamSwitch.Helper) |
操作面固定為HelperProtocol(§3表)。沒有任意指令通道。連接由代碼簽署要求使用 audit_token 進行授權。 |
如果 RoamSwitch.app 本身受到損害,則所有幫助程式的操作都會傳遞給攻擊者 |
啟用 Hardened Runtime,不賦予應用程式多餘的權限。網路傳送僅限於 §7 表格中的路徑(以及表格下方所述由使用者操作觸發與預設關閉的功能)。雖然未宣告網路傳送 entitlement,但由於應用程式不在沙盒內執行,這本身並不禁止通訊(實測見 §7 末尾)。今後計畫接受第三方審查。 |
| 助手產生的系統二進位路徑 | 像 /sbin/pfctl 這樣的絕對路徑是直接指定的,不依賴 PATH。參數也是硬編碼的(除了連接埠號碼、DNS 字串、ARP IP/MAC 對和 VPN 端點 IP — 所有這些都經過格式驗證)。 |
連結防護重寫 /etc/hosts (setLinkGuardSinkhole) |
寫入僅限於分隔的託管部分;它外面的行被逐字保留。域被規範化和驗證,IP 和垃圾被刪除,列表上限為 60,000,並且檔案透過臨時檔案 + 原子替換寫入。它只處理明確的案例(威脅來源清單或品牌同形異義詞),由僅限本地的判決引擎決定。阻止清單本身來自經過簽署驗證的威脅來源(來源專用金鑰)。 |
Link Guard 的威脅來源取得(每日僅接收 GET) |
沒有查詢字串和裝置專屬識別碼的靜態文件取得(User-Agent 僅為標準值)。清單和提要正文均經過 Ed25519 驗證,並且驗證失敗的提取將被丟棄(不會回退到未簽署的資料)。 「自動更新:關閉」會完全刪除路徑。 |
預防性 ARP/NDP 接腳 (lockGatewayARP) |
僅固定「網關、IPv6 路由器、連結 DNS」的鄰居快取項目。傳入的 IP/MAC 對經過格式驗證並根據請求集進行協調(它從不自行添加條目)。它僅在不受信任的網路上運行;專業版,預設為關閉。首次使用信任限制在第 11 節「我們決定不做的事情」中說明。 |
VPN 隧道(wireGuardImport/Up/…、自製程式 wireguard-tools) |
.conf 由助手存放在 0600 處。端點 IP 由應用程式解析並傳遞給幫助程式(幫助程式永遠不會解析任意主機名稱)。隧道的目的地和內容由使用者控制。如果未安裝 wireguard-tools,則該功能將被停用。專業版,預設為關閉。終止開關經過 PFRulesetCoordinator (§4)。 |
| MCP 伺服器將系統狀態傳遞給 LLM(困惑的副手) | 它是唯讀的,沒有實作寫入 API。 URL 檢查處於離線狀態。設定域以唯讀方式引用。 |
| 劫持更新路徑 | EdDSA 簽章驗證 (SUPublicEDKey),以及捆綁的公證票。 appcast 透過 HTTPS 進行。 |
附錄 A
本文檔中所述的所有內容都可以使用以下命令針對分散式工件進行驗證。
簽署及公證
# Developer ID 簽署與團隊 ID
codesign -dvvv /Applications/RoamSwitch.app 2>&1 | grep -E 'Authority|TeamIdentifier|flags'
# 公證票證是否已 staple
stapler validate /Applications/RoamSwitch.app
spctl -a -t exec -vvv /Applications/RoamSwitch.app
# 內建 Helper / MCP 伺服器的簽署
codesign -dvvv /Applications/RoamSwitch.app/Contents/MacOS/RoamSwitchHelper
codesign -dvvv /Applications/RoamSwitch.app/Contents/MacOS/RoamSwitchMCPServer
Entitlements(確認未宣告網路傳送 entitlement;由於不在沙盒內,這本身並不禁止通訊)
codesign -d --entitlements :- /Applications/RoamSwitch.app/Contents/MacOS/RoamSwitchHelper
codesign -d --entitlements :- /Applications/RoamSwitch.app/Contents/MacOS/RoamSwitchMCPServer
# → 空的 entitlements 字典。沒有 app-sandbox / network client 鍵。
測量流量
# 同時執行 tcpdump,確認正常使用中不會產生表格所列路徑以外的通訊
sudo tcpdump -i any -n 'host not 127.0.0.1' and 'not port 53'
# 除表格所列路徑(授權啟用、更新檢查、已簽署的更新資料、ClamAV 更新等)之外沒有其他流量
若要對每個進程的歸因進行更嚴格的自動檢查,請參閱
roamswitch-support/audit/ 中的 rs-zerotel-audit.sh
防禦架構和滲透測試(5 個防禦邊界)
# 一次性自動驗證 XPC 授權邊界、pf Air-Gap 優先權、連接埠暴露偵測、MCP 唯讀、ARP 監控
git clone https://github.com/lafine1211/roamswitch-support
cd roamswitch-support/audit
./rs-defense-audit.sh all
來自 macOS VM 的即時滲透測試和多層防禦審核:RESULTS-DEFENSE-2026-08-30
DNS 威脅防護(Quad9 惡意軟體和 C2 封鎖)驗證
# 查詢 Quad9 官方用於驗證威脅攔截的測試網域
nslookup test.dns9.quad9.net
# → 應回傳 ** server can't find test.dns9.quad9.net: NXDOMAIN,即名稱解析被攔截
Web & Mail Download Guard(ClamAV 即時檢測)驗證
# 在 Downloads 中建立無害的業界標準防毒測試字串(EICAR)
echo 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > ~/Downloads/eicar_test.com
# → FSEvents 偵測觸發 ClamAV 立即掃描,不顯示通知,「偵測到 EICAR 測試簽章(無害)」會
# 記錄至通知歷史。由於 EICAR 是業界標準測試檔案,並非真實威脅,
# 不會被隔離或攔截,檔案將原樣保留(僅在放置真實惡意軟體樣本時才會移入隔離管理員)。
靜態簽章偵測(無需 EndpointSecurity)驗證
## 一條來自公開攻擊手法說明的經典反向 shell 單行指令(無害,不會執行)
echo 'bash -i >& /dev/tcp/127.0.0.1/4444 0>&1' > ~/Downloads/rs_test.sh
## → 即使未安裝 ClamAV,也會出現「已隔離危險的下載檔案」通知,且該檔案會被
## 立即移動到 Quarantine 資料夾(不依賴 ClamAV 的簽章資料庫)
新增自動啟動監控(PersistenceMonitorGuard)驗證
# 建立一個直接呼叫裸腳本直譯器的無害 LaunchAgent(不會真正執行)
cat > ~/Library/LaunchAgents/com.example.selfcheck.plist <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>com.example.selfcheck</string>
<key>ProgramArguments</key><array><string>/bin/bash</string><string>-c</string><string>echo hi</string></array>
</dict></plist>
EOF
# → 數秒內應出現「偵測到新的自動啟動註冊」通知。確認後請將其移除:
rm ~/Library/LaunchAgents/com.example.selfcheck.plist
Link Guard(封鎖網路釣魚連線)
# 是否寫入了受管理區塊(Pro + 預設模式下為 BEGIN/END 兩行)
sudo grep -c 'RoamSwitch link guard' /etc/hosts
# 區塊內容與 sinkhole 數量
sudo sed -n '/BEGIN RoamSwitch link guard/,/END RoamSwitch link guard/p' /etc/hosts | head -4
sudo sed -n '/BEGIN RoamSwitch link guard/,/END RoamSwitch link guard/p' /etc/hosts | grep -c '^0\.0\.0\.0'
# 是否真的被攔截(用區塊中的一筆記錄確認,無害)
D=$(sudo sed -n '/BEGIN RoamSwitch/,/END RoamSwitch/p' /etc/hosts | awk '/^0\.0\.0\.0/{print $2; exit}')
dscacheutil -q host -a name "$D" # → ip_address: 0.0.0.0(名稱解析已被攔截)
# 也可透過選單 →「連結保護」→「關閉」確認上述區塊消失
只接收威脅來源(經過簽名驗證)
# 公開的資訊來源與清單(任何人都可取得並驗證)
curl -s https://lafine.net/updates/v1/manifest # version/generated/threatfeed{...}
curl -sI https://lafine.net/updates/v1/manifest.sig # → text/plain
# 傳送的僅是不帶查詢字串、Cookie、裝置專屬識別碼的 GET 請求(User-Agent 為標準值)。可搭配執行 tcpdump 加以確認
預防性 ARP/NDP 鎖定(§5、1.7.5+)
# 在預防性鎖定已啟用的狀態下連線不受信任網路 → 檢查 permanent 項目
arp -an | grep -i permanent # 閘道器等 IP 會以 (permanent) 列出
ndp -an | grep -i 'P ' # IPv6 端(P = permanent)
sudo cat "/Library/Application Support/RoamSwitch/gateway_arp_lock.json" # 目前固定的 IP→MAC 集合
# 在選單中解除固定 → 上述內容應消失
VPN 隧道 + 終止開關(§5、1.7.6+)
# 前提是已安裝 wireguard-tools(透過 Homebrew)
brew list wireguard-tools >/dev/null && echo "wireguard-tools: OK"
# 在不受信任網路上啟用 VPN → 通道建立前 pf 處於網路阻斷開關狀態
sudo pfctl -sr | grep -E 'block drop all|pass .*(utun|udp)' # block drop all + 有限的 pass quick 規則
# 通道建立後(已執行 wg-quick up)
wg show # handshake / transfer 正常運作中
route -n get default | grep interface # → utunN(預設路由為通道)
# 停用 VPN → 網路阻斷開關解除,pf 恢復原狀
特權幫手本身
# 已註冊的 LaunchDaemon
sudo launchctl print system/com.tetsuharu.RoamSwitch.Helper
# 目前載入的 pf 規則(Air-Gap / 連接埠防護的實際狀態)
sudo pfctl -sr
# Helper 的狀態目錄
ls -la "/Library/Application Support/RoamSwitch/"
MCP伺服器的回應(離線檢查)
BIN=/Applications/RoamSwitch.app/Contents/MacOS/RoamSwitchMCPServer
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | "$BIN"
# 會回傳 serverInfo 與工具定義。不會產生任何網路連線
MCP伺服器的來源碼和測試
git clone https://github.com/lafine1211/roamswitch-mcp
cd roamswitch-mcp
swift build -c release # 與發行二進位檔相同的原始碼
swift test # 單元測試、對抗性輸入、stdio、突變模糊測試
# 測試內容與已發現的問題記錄在 SECURITY_TESTING.md 中
裝置識別符
# 綁定到權杖的原始值(傳送的僅為加鹽 SHA-256)
ioreg -d2 -c IOPlatformExpertDevice | awk -F'"' '/IOPlatformUUID/{print $4}'