Lafine系统设计·技术文件

RoamSwitch 架构和安全白皮书

本文档解释了 RoamSwitch 使用哪些权限运行以及它在该边界上执行的操作。 它不包含任何营销语言;此处所述的所有内容都可以根据运输应用程序进行验证 二进制及其实际行为。

版本 v1.5 适用对象 RoamSwitch 1.9.47 (build 104) 要求 macOS 13.0+ / Apple Silicon 发布日期 2026-09-15 Team ID GV76B6G4YU

§1

RoamSwitch 是一款适用于 Mac 的菜单栏应用程序。取决于您对网络的信任程度 当前连接到时,它会自动切换 macOS 防火墙、共享服务、AirDrop、 和 DNS。它还监视 ARP 欺骗、向外部开放的端口、USB 存储和 类似勒索软件的加密活动,当它检测到危险的东西时,它会尽可能地 使用数据包过滤器 (pf) 切断流量——紧急气隙。

换句话说,RoamSwitch 安装了一个具有 root 权限的守护进程,并且如果它愿意的话,可以停止所有 Mac 上的网络流量。它是一个人建造的,“请相信我”是不够的 证明这种特权是合理的。因此,本文档以您可以验证的形式解释了设计。

这是给谁的

  • 工程师决定是否安装
  • 想要在评论或文章之前了解内部结构的安全研究人员和记者
  • 合作伙伴公司的安全人员评估内部部署或 OEM 捆绑

本文档不涵盖的内容

它不涉及检测阈值调整、误报统计或 UI 演练。 它解释的是四件事:特权、进程边界、数据流和 密码学。功能规范本身位于捆绑的 MCP 资源中 roamswitch://docs/features 和应用程序内帮助。

披露内容以及披露程度(披露政策)

本文档是在攻击者已经拥有分布式二进制文件的假设下编写的。 此处显示的每个端点 URL、标识符、文件路径、XPC 协议和嵌入式公钥 可以在几分钟内从运输 RoamSwitch.app 中取出 stringscodesign -d 或流量代理。因此将它们写在这里 给攻击者带来任何新东西。它唯一进步的是审稿人的理解。

另一方面,从二进制文件中看不到的服务器端实现——rate-limit 阈值、密钥、管理端点、数据库架构、Firebase 项目布局 - 不包括在内。 OEM 和合作伙伴集成设计也超出了本文的范围,并在单独的章节中介绍。 内部文件。 “通过观察客户你可以学到什么”是这条信息的披露线 文档。

补充

本文档对应于顶部注明的版本的来源。行为发生变化的地方 在更高的版本中,该文档被修改,版本号和目标版本也被更新。 如果您发现文本和代码之间存在差异,请通过以下方式告知我们 lafine.net/contact.html

§2

RoamSwitch.app 由三个可执行文件组成。其中只有一个人享有特权;的 另外两个以登录用户权限运行。所有三款产品均启用了强化运行时、开发人员 ID 签字并经过公证。

RoamSwitch 组件与信任边界示意图 MCP 客户端 Claude Desktop / Code MCPServer 用户权限 仅只读 RoamSwitch.app 用户权限 菜单栏 / 监控 / UI Helper root / LaunchDaemon SMAppService.daemon 系统二进制文件 pfctl / socketfilterfw … stdio (JSON-RPC) 仅只读 * XPC mach service 通过代码签名授权 进程执行
* MCPServer不连接到应用程序本身;它直接读取共享首选项域和监控模块(§8)。
每个组件的权限和角色
可执行文件特权可以做不能做
漫游交换机
.app
登录用户 监控网络状态、运行诊断、绘制 UI、通过 XPC 调用帮助程序、通过 defaults 更改 AirDrop、启动 ClamAV(可选) 直接操作防火墙、pf或系统守护进程(所有这些都通过帮助程序)
漫游开关
助手
HelperProtocol(第 3 表)中列出的操作:防火墙/隐形、共享守护程序的加载/卸载、应用 pf 规则集、更改 DNS、向进程发送信号 还有什么。没有用于运行任意命令的界面。它也没有网络发送权限
漫游交换机
MCPServer
登录用户 读取并格式化诊断值,搜索本地知识库;结果通过stdio返回给客户端 更改设置、切换锁定、隔离端口、弹出设备。它不打开套接字。它不通过网络发送任何内容

权利和签署

  • 所有三个目标都有 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) 仅声明 LabelBundleProgram、单个 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。

HelperProtocol — 以 root 身份运行的所有内容
方法它的作用二进制/API 调用
设置块全部(_:) 打开或关闭应用程序防火墙和隐身模式 /usr/libexec/ApplicationFirewall/socketfilterfw
--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/downwg showwireguard-tools;如果未安装则禁用)
终止进程(pid:forceKill:) 挂起 (SIGSTOP) 或强制退出 (SIGKILL) 进程。用于包含类似勒索软件的进程。仅 pid > 1 Kill(2) 系统调用(不是子进程)
获取助手版本(...) 返回助手的版本字符串(用于应用程序兼容性检查)
设计权衡

terminateProcess 可以将 SIGKILL 发送到任何进程,只要 pid > 1setSecureDNSServers 接受任何 DNS 服务器字符串。那就是 特征所需的宽度,但并不窄。判断它的前提是 它前面的代码签名检查 (ClientValidator) 是唯一的门。

助手内部的状态

  • HelperTool.shared 是跨连接共享的单个实例。它曾经是每个连接的单独实例,因此打开新连接的紧急遏制可能会引发一场失去“要带回哪些服务”的竞赛。
  • 共享服务和 DNS 备份仅在串行队列 (stateQueue) 上发生变化。

§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 连接、帮助程序的启动还是故障安全计时器,更改都会按顺序处理。
  • 优先级顺序如下(排在上面的优先)。
    1. 紧急气隙 → set skip on lo0block drop all(不考虑其他)
    2. VPN 终止开关 → block drop allpass quick 仅适用于:lo、隧道接口 (utunN)、与固定端点 IP 的 UDP 握手、DHCP 和 ICMP
    3. 开发服务器守卫 → block drop in quick proto tcp ... port { … }
    4. None → 重新加载 /etc/pf.conf 并将 pf 返回到原始状态
  • 应用后读回。 pfctl -sr读回规则以确认block drop all或每个端口的规则确实已加载。 pfctl -f 被默默忽略的情况不被视为成功。
  • 临时文件被写入包含 UUID 的路径,并在应用后删除(v1.4.5 放弃了固定路径,取而代之的是难以猜测的路径)。状态目录是/Library/Application Support/RoamSwitch
API 行为

来自 enableNetworkAirGapsetGuardedDevServerPorts 的 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 加入不受信任的网络时(专业版,默认关闭)。重新固定每次网络变化 不受影响

如何防止气隙挥之不去

  • 最多在 10 分钟后解除。助手本身仍会在启动后立即以及每 60 秒的计时器上运行 releaseAirGapIfExpired(),但作为与之完全独立的安全网,一个完全不带 KeepAlive 的专用 LaunchDaemon(AirGapFailsafe)会每 3 分钟单独唤醒一次,执行相同的判定与解除。即使助手本身崩溃、连同其自身的自动解除计时器一起失效,这个安全网也不受影响(1.8.6+)。
  • 这个专用守护进程完全不会触碰交互式助手本身的启动配置。此前(1.8.5)曾使用助手自身的 KeepAlive.PathState 来实现这一恢复机制,但它偶尔会与正常的应用退出操作发生冲突,导致意外重启,因此该方案已被撤回,替换为当前完全独立的守护进程方式。
  • 时间戳的判定采用单调递增时间(ProcessInfo.systemUptime)与挂钟时间相结合的混合方式。通常优先使用单调递增时间,因此不受 NTP 校正或手动修改系统时间的影响。仅在单调递增时间不可信时(即重启后:当前值小于已保存的值,判定为发生了重启)才会回退到挂钟时间。
  • 在重新启动或守护进程重生后重新应用。 在启动时,帮助程序使用 reapplyFromDisk() 读取磁盘上的状态,并按气隙、端口防护、系统默认的顺序自行恢复(10 分钟规则也适用于此)。
  • 失败的提升被视为失败。 如果 block drop all 实际上无法删除,则时间戳会被写回,以便故障安全计时器和重试有一些收敛。屏幕绝不会错误地显示“抬起”。
  • 您可以随时收回控制权 - 使用模式中的提升按钮,或者只需关闭 Wi-Fi。

如何恢复端口防护误报

未知端口自动阻止(Pro,激活 Pro 后默认开启)可以阻止合法端口 LAN 接收器 — LocalSend、Syncthing、启用防护后启动的任何内容。当那个 发生时,请通过通知横幅上的“允许”按钮或中的匹配行来允许它 “暴露端口”屏幕。一次允许的可执行文件被记录为已知并且不会被阻止 再次(PortAnomalyGuard.allowPort(_:))。请注意,通用脚本解释器 (Python、Node.js、Netcat 等)严格受 path:port 限制,而不是二进制 独自一人以防止离地生活的袭击。 Apple系统守护进程满足 anchor applerapportd,支持切换等)不是 首先观看。

自愈勒索软件金丝雀诱饵和遏制抑制

如果金丝雀诱饵文件被篡改或重命名并触发紧急气隙,则释放 验证安全性后的遏制会自动重新生成丢失或损坏的金丝雀文件 返回到从嵌入模板生成的真实基线哈希值,立即恢复不间断的 kqueue 监视(防止对手基线污染)。 此外,在主动遏制期间(显示紧急模式时),会自动抑制冗余通知警报和定期后台完整性轮询的重新触发事件,以避免在事件响应期间分散用户的注意力。

下载文件的多层检测(静态签名 + 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)中已存在同名的历史隔离文件,新的威胁也会以带时间戳的唯一文件名可靠地移入隔离文件夹,彻底避免因命名冲突导致威胁残留在原始位置的漏检情况。

监控新的自动启动注册(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,只要直接启动裸脚本解释器(bashshzshosascriptpython3perlrubyphp 等),即使该解释器本身的签名合法有效,也会被无条件发出警告。传递给它的参数还会进一步交给 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 showlog 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:))。会自动排除 .gitnode_modulestargetvendordistbuild__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 -spermanent 条目(首次使用时信任 — 观察到的第一个 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:pf block 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>建议使用独立的 CLI (brew install tailscale) — App Store (GUI) 构建无法从应用程序外部(沙箱)驱动;使用它,您可以在 Tailscale 应用程序中选择退出节点,而 RoamSwitch 仅显示状态。 需要出口节点(通过隧道路由所有流量);如果它离线或无法访问,它会自动撤防。
  • 断开连接时的网络重新配置。 当退出节点被清除时,独立的 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状态/库/应用程序支持/RoamSwitch/气隙时间戳、受保护端口的 JSON、VPN 终止开关状态、所应用的规则集的临时文件
Link Guard 威胁源〜/库/应用程序支持/RoamSwitch/threatfeed/feed.txt下载的网络钓鱼/诈骗域列表(如果尚未获取,则为应用程序的捆绑种子)。 Feed 版本位于 UserDefaults 中
Link Guard 托管部分/etc/主机# BEGIN RoamSwitch link guard ... # END 界定的分隔部分,将阻止的域清空为 0.0.0.0。当模式为“关闭”时删除 (§5)
ARP/NDP 锁销/库/应用程序支持/RoamSwitch/gateway_arp_lock.jsonIP → MAC 装置由预防锁 (§5) 制成 permanent。解锁后删除
WireGuard 配置/Library/Application Support/RoamSwitch/(帮助区域,0600用户导入的 .conf。端点主机名也存储在 UserDefaults 中(应用程序解析它)
设备后备 UUID用户默认值仅当 IOKit 不返回 UUID 时生成的随机值 (§9)
日志os.Logger/NSLog统一日志记录。没有任何内容被发送到外部

离开机器的流量(完整列表)

任何地方都没有代码可以收集和发送诊断结果、端口信息、URL 或 日志。不包含分析 SDK 和崩溃报告器 SDK。唯一的外部库是 Sparkle (更新)。进入网络的就是这八个,仅此而已(仅当用户配置 VPN 时才是第六个,仅当启用 RoamSwitch Sensor 配对时才是第七个)。

RoamSwitch 建立的出站连接
连接方式目的地当它发生时发送了什么
许可证激活/停用 lafine.net
/api/v1/license/*
仅当用户输入许可证密钥或停用 Pro 时 许可证密钥、设备哈希、主机名、应用程序版本。个人信息由 Stripe 在购买时处理;该应用程序不处理它
更新检查 lafine.net
/updates/appcast.xml
Sparkle,每 24 小时一次以及发布时 HTTP 请求(标准 UA 和版本)。下载的项目由 EdDSA 签名验证 (§10)
Link Guard 威胁源 lafine.net
/updates/v1/{manifest, feed/<版本>.txt}
当 Link Guard (§5) 打开并启用“自动更新”时,每 24 小时(以及启动时)。关闭“自动更新”会删除此路径 GET。没有查询字符串,没有cookies,没有任何可以识别机器的东西。 receive-only 签名的静态文件;清单和 Feed 正文均经过 Ed25519 验证。签名密钥是 专用于 feed — 来自应用程序更新 SUPublicEDKey 独立密钥 (因此泄漏仅限于“坏块列表”)
软件包 CVE 比对 / 实证型漏洞诊断的更新数据 lafine.net
/updates/v1/manifest
自应用启动起每 24 小时一次(始终进行。软件包 CVE 比对的更新数据没有关闭开关,因为它纯粹是用于本地清单比对的仅接收数据;实证型漏洞诊断的 CVE 映射仅在启用“实证型漏洞诊断(主动可达性验证)”时才会获取) 仅 GET。没有查询字符串、没有 cookie,也没有任何可识别设备的信息。这是仅接收的已签名静态文件;清单与源数据本体均通过 Ed25519 验证。与 Link Guard 威胁源复用相同的密钥与相同的清单,但实际获取的源文件(Homebrew/npm/PyPI/crates.io/RubyGems/Packagist/Go/Maven 的已知 CVE 映射、实证型漏洞诊断的 CVE 映射,以及用于打字仿冒检测的热门 npm 软件包名称列表)是各自独立的文件
ClamAV 病毒定义更新 ClamAV 官方镜像 仅当用户安装了 ClamAV 并使用扫描功能时。它启动 freshclam 标准 ClamAV 定义获取。它不包含 RoamSwitch 派生的信息
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」的范围

这里的“零遥测”意味着没有遥测收集和发送使用数据或 诊断结果。这并不意味着根本没有网络流量。中的八条路 上表确实存在。但它们中的每一个要么是用户发起的,要么是 签名验证、仅接收 获取以及诊断结果、端口、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 自身的响应外,绝不会被发送。

应用内“链接安全检查”表向目标 URL 发送 HEAD 请求以查看 缩短的 URL 到达的地方(以下重定向到私人或本地地址被 v1.4.5 SSRF 缓解)。相比之下,MCP audit_url_safety 是离线分析 当场完成并将 URL 发送到任何地方(§8)。

实测(2026-08-29)— 判定 PASS

这不仅仅是断言。 2026 年 8 月 29 日,正在运行的 1.4.7 安装经过了审核 tcpdump + 每个进程归因(nettop / lsof / 已过滤 pktap capture) + LuLu,大约 2 小时的窗口,安全级别固定为 最大锁定和强制 appcast 检查。 结果:没有出站流量归因于 RoamSwitchRoamSwitchHelper、 或 RoamSwitchMCPServer,除了对 lafine.net 进行 appcast 检查; MCP 服务器唯一的套接字是本地主机;权利转储是空的.

完整的文章和任何人都可以运行来重现它的脚本: 审计/RESULTS-2026-08-29.md

此测量来自 1.4.7,在 Link Guard 威胁源(1.7.2,第 3 行)和 VPN 之前 隧道(1.7.6,第 5 行)。在 1.7.2 或更高版本上审核,您将看到 two 仅接收 GETs 到 lafine.net(appcast 和威胁源);如果有VPN 配置完成后,您还将看到到用户自己的 WireGuard 服务器的 UDP(目的地和内容 在用户的控制下)。所有这些都会随着“自动更新:关闭”而消失,并且没有配置 VPN。

§8

RoamSwitchMCPServer 是一个独立的命令行工具,捆绑在 RoamSwitch.app/Contents/MacOS/。 MCP 客户端,例如 Claude Desktop 或 Claude Code 将其作为子进程启动并通过 stdio (换行符分隔的 JSON-RPC 2.0)。官方 SDK 不会针对本机的 macOS SDK 进行构建,因此 在基金会的 JSONSerialization 之上手动实现。

设计约束什么

  • 它是只读的。 根本没有用于更改安全级别、隔离端口或弹出设备的 API。这并不是 v1 中被遗忘的东西——它是被故意遗漏的。让外部代码(此处为 LLM)重写安全工具的保护状态会破坏每个用户的信任。
  • 它不打开套接字。它既不注册服务也不监听。它从标准输入读取一行,在标准输出上返回一行,然后客户端结束进程。
  • 它没有发送任何信息。 所有诊断均在 Mac 内部完成。
  • 它从不同的域读取设置。 UserDefaults(suiteName: "com.tetsuharu.RoamSwitch") 显式读取应用程序的域(其自己的捆绑包 ID 域为空)。它只会读取;它不写。

它公开的工具

工具tools/list返回,以及它们返回的数据
工具它返回什么交通
获取安全报告18 项检查(FileVault / SIP / Gatekeeper / 自动更新 / XProtect / 防火墙 / Wi-Fi 加密 / ARP / 网关 ARP 锁定 / SSH 配置审计 / Sudo NOPASSWD 审计 / 暴露端口 / 防护配置 等)的评分,以及各项目的改进建议仅限本地
获取暴露端口侦听 TCP 端口的列表。对于任何在本地主机之外暴露的内容,它会交叉引用已知危险的服务数据库,并使用 HTTP 探针检查 CORS / 标头到 127.0.0.1:port (本地,已关闭)只探测到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 密钥、令牌等敏感信息,并返回基于日志模板化的新模式检测与频率异常(统计学突增)检测结果。仅限本地
run_active_vuln_scan仅限 127.0.0.1 的非破坏性实证漏洞验证。唯一会使用网络的工具,默认关闭,需在设置中主动启用。仅 127.0.0.1(默认关闭)
run_package_cve_scan将已安装的软件包(Mac:Homebrew / Linux:dpkg・dnf・zypper・pacman)与本地 CVE 映射进行比对。完全不产生任何网络通信。
run_package_cve_scan_languages将 npm / PyPI / crates.io / RubyGems / Packagist / Go / Maven 等依赖锁定文件与同一本地 CVE 映射进行比对。完全不产生任何网络通信。
get_quarantine_status返回恶意软件隔离 Vault 的内容(原始路径、检出的威胁名称、隔离时间与大小)。仅限本地
get_canary_status返回勒索软件诱饵文件(Canary)的部署状态,以及最近 50 条检测事件。仅限本地
get_notification_history返回 RoamSwitch 发送的通知(安全日志审计异常、ClickFix 检测等)历史记录,最近1周、最新在前。仅限本地
get_port_anomaly_incidents返回端口异常防护的基线状态、当前自动阻断的端口,以及最近 50 条事件。响应中明确注明:当前自动阻断的端口是不带时间戳的当前状态快照,与带时间戳的事件历史是两回事。仅限本地
get_runtime_threat_status返回是否因 XProtect 检出恶意软件而进入 Air-Gap 隔离,以及触发该隔离的事件。排查 Air-Gap 原因时应最先查看此工具。仅限本地

initialize 响应中的 instructions 字段也明确指出, “无法更改安全级别、隔离端口或弹出设备”传达该功能 客户端LLM的边界。 MCP 资源 (roamswitch://docs/*) 是只读的 Markdown 文档也是如此。

在本地 LLM 实战演练中发现并修复的错误归因问题

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

  1. 运行 xcodebuild archive (发布、手动签名、开发者 ID 应用程序)。
  2. -exportArchive 导出为 method: developer-id
  3. notarytool submit --wait 之后,在 .app 上运行 stapler staple
  4. 使用 spctl -a -t exec -vv 进行验证。
  5. 装订后重新压缩 以生成 Sparkle 更新工件(因此包含公证票并且它可以离线运行而不会出现 Gatekeeper 警告)。
  6. 构建 DMG,公证并装订 DMG,并使用 stapler validate 进行验证。

更新(Sparkle 2.9.6)

更新 Info.plist 中的设置
钥匙价值
SUFeedURLhttps://lafine.net/updates/appcast.xml
SU公共密钥CNxzwijMzMCJzliId76Yl88S/9np6t/xg/zQ9YbYzHs=
SU启用自动检查真实
SUScheduledCheckInterval86400

在应用更新之前,会验证 appcast 中列出的 EdDSA 签名 针对应用程序中嵌入的 SUPublicEDKey 。私有签名密钥仅存在于 构建环境。 appcast 通过 HTTPS 提供服务。 Delta 更新经过签名验证 同样的方式。

要点:appcast 以公开为前提

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 pin(§5),以及 (3) 具有紧急气隙的欺骗检测(事后)。所有 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 本身受到损害,则所有帮助程序的操作都会传递给攻击者 已启用强化运行时,并且应用程序不会被授予不必要的权限。出站流量仅限于上述八个路径(第六个仅在用户配置 VPN 时,第七个仅在启用 RoamSwitch Sensor 配对时)。我们计划让第三方对此进行审查。
助手生成的系统二进制路径 /sbin/pfctl 这样的绝对路径是直接指定的,不依赖于 PATH。参数也是硬编码的(除了端口号、DNS 字符串、ARP IP/MAC 对和 VPN 端点 IP — 所有这些都经过格式验证)。
链接防护重写 /etc/hosts (setLinkGuardSinkhole) 写入仅限于分隔的托管部分;它外面的行被逐字保留。域被规范化和验证,IP 和垃圾被删除,列表上限为 60,000,并且文件通过临时文件 + 原子替换写入。它只处理明确的案例(威胁源列表或品牌同形异义词),由仅限本地的判决引擎决定。阻止列表本身来自经过签名验证的威胁源(源专用密钥)。
Link Guard 的威胁源获取(每日仅接收 GET 没有查询字符串和标识符的静态文件获取。清单和提要正文均经过 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

权利(无网络发送权限)

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 请求。可配合运行 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 与 5 个工具的定义。不会产生任何网络连接

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}'
RoamSwitch 架构/安全白皮书 v1.5 · 适用对象 1.9.28 (build 85) · Lafine Systems Design · 2026-09-15
如发现错误或遗漏,请通过 lafine.net/contact.html 反馈。