§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返回给客户端 | 更改设置、切换锁定、隔离端口、弹出设备。默认不打开套接字,也不通过网络发送任何内容(例外仅有默认关闭的 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)。也就是说,登录用户的应用,以及沙盒内的 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,并且帮助程序将每个条目固定为带有arp -s/ndp -s的permanent条目(首次使用时信任 — 观察到的第一个 MAC 是可信的)。然后,这些 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 时 | 许可证密钥、设备哈希、设备名称(系统设置中的“名称”)、应用版本。解除时仅发送密钥和设备哈希。个人信息由 Stripe 在购买时处理,应用不处理 |
| 更新检查 | 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 服务器打开的套接字仅限本机回环;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}'