Lafine Systems Design · 技術文書

RoamSwitch アーキテクチャ/セキュリティホワイトペーパー

RoamSwitch がどのような権限で動作し、その境界で何を行っているのかを説明する技術文書です。 宣伝的な表現は使わず、記載した内容はすべて、配布中のアプリのバイナリと実際の動作から確認できるようにしています。

版 v1.6 対象 RoamSwitch 1.11.14 (build 161) 要件 macOS 13.0+ / Apple Silicon 発行 2026-09-23 Team ID GV76B6G4YU

§1この文書について

RoamSwitch は、メニューバーに常駐する Mac 向けのアプリです。いま接続しているネットワークをどの程度 信頼しているかに応じて、macOS のファイアウォール、共有サービス、AirDrop、DNS を自動的に切り替えます。 あわせて、ARP スプーフィング、外部に開いたポート、USB ストレージ、ランサムウェアのような暗号化の動きを 監視しており、危険を検知した場合には、パケットフィルタ(pf)で通信を遮断する緊急隔離 (Air-Gap)まで行います。

つまり RoamSwitch は、root 権限のデーモンを常駐させ、その気になれば Mac の通信をすべて止められる、 ということになります。開発元は個人であり、「信頼してください」という言葉だけでこの権限を正当化することは できません。そこで本書では、その代わりに、設計の内容を検証できる形で説明していきます。

対象となる読者

  • 導入するかどうかを技術的に判断したいエンジニアの方
  • レビューや記事化の前に、内部の作りを確認しておきたいセキュリティ研究者・ジャーナリストの方
  • 自社導入や OEM 同梱を検討している、パートナー企業のセキュリティ担当の方

この文書が扱わないこと

検知ロジックのしきい値調整や誤検知の統計、画面の操作手順については触れません。本書で説明するのは、 権限、プロセスの境界、データの流れ、暗号の 4 つです。機能そのものの仕様は、同梱の MCP リソース roamswitch://docs/features と、アプリ内のヘルプに記載しています。

何をどこまで書くか(開示方針)

本書は、攻撃者がすでに配布バイナリを入手している、という前提で書いています。ここに登場するエンドポイント の URL、識別子、ファイルパス、XPC プロトコル、埋め込みの公開鍵は、いずれも出荷済みの RoamSwitch.app から strings や codesign -d、通信プロキシを使えば、 数分で取り出せるものです。ですから、これらを記載しても、攻撃者にとっての手がかりが増えることはありません。 増えるのは、レビューする側の理解だけです。

一方で、バイナリからは分からないサーバー側の実装 ── レート制限のしきい値、鍵、管理用のエンドポイント、 DB スキーマ、Firebase プロジェクトの構成 ── は記載しません。OEM・パートナー連携の設計も本書の範囲外とし、 別の内部文書で扱います。「クライアントを観察すれば分かる範囲」が、本書の開示の線引きです。

補足

本書の内容は、冒頭に記載したバージョンのソースに対応しています。以降のバージョンで挙動が変わった箇所に ついては、改版のうえ版番号と対象ビルドを更新します。記述とコードの食い違いに気づかれた場合は、 lafine.net/contact.html からお知らせください。

§2コンポーネントと信頼境界

RoamSwitch.app は、3 つの実行体で構成されています。このうち特権を持つのは 1 つだけで、 残りの 2 つはログインユーザーの権限で動作します。3 つとも Hardened Runtime を有効にし、Developer ID で 署名して公証を受けた状態で配布しています。

RoamSwitch のコンポーネントと信頼境界の図 MCP クライアント Claude Desktop / Code MCPServer ユーザー権限 読み取り専用 RoamSwitch.app ユーザー権限 メニューバー / 監視 / UI Helper root / LaunchDaemon SMAppService.daemon システムバイナリ pfctl / socketfilterfw … stdio (JSON-RPC) 読み取りのみ * XPC mach service コード署名で認可 Process 実行
* MCPServer はアプリ本体には接続せず、共有の設定ドメインと監視モジュールを直接読み取ります(§8)。
コンポーネント別の権限と役割
実行体権限できることできないこと
RoamSwitch
.app
ログインユーザー ネットワーク状態の監視、各種診断、UI 表示、ヘルパーへの XPC 呼び出し、AirDrop の defaults 変更、ClamAV(任意)の起動 ファイアウォール・pf・システムデーモンの直接操作(すべてヘルパー経由で行います)
RoamSwitch
Helper
root HelperProtocol に列挙された操作だけ(§3 の表)。ファイアウォール/ステルス、共有デーモンの load / unload、pf ルールセットの適用、DNS の変更、プロセスへのシグナル送出 それ以外。任意コマンドを実行するためのインターフェースはありません。ネットワーク送信のエンタイトルメントは宣言していませんが、サンドボックス外のため、これ自体は通信を禁止するものではありません(外向き通信の実測は §7)
RoamSwitch
MCPServer
ログインユーザー 診断値の読み取りと整形、ローカルのナレッジベース検索。結果は stdio 経由でクライアントに返します 設定変更、ロックダウン切替、ポート隔離、デバイス取り出し。既定ではソケットを開かず、ネットワーク送信もしません(例外は、既定で無効の npm 署名検証と、この Mac 自身(127.0.0.1)宛ての脆弱性診断だけです。§8)。ネットワーク送信のエンタイトルメントは宣言していませんが、サンドボックス外のため、これ自体は通信を禁止するものではありません

Entitlements と署名

  • 3 つのターゲットは、いずれも ENABLE_HARDENED_RUNTIME = true です。
  • App Sandbox は無効にしています(com.apple.security.app-sandbox = false)。ヘルパーと MCPServer の entitlements は、空の辞書です。
  • 配布用のビルドには、Developer ID Application 署名、Apple の公証、staple を行っています(§10)。
設計上のトレードオフ

App Sandbox は使っていません。RoamSwitch は、IOKit からのハードウェア UUID の取得、CoreWLAN、 DiskArbitration、他プロセスの待ち受けソケットの列挙(lsof)、LaunchDaemon への XPC 接続、 システムバイナリの起動を必要とします。これらはいずれもサンドボックスの中では実現できないため、 有効にしていません。

その代わりに、次の 4 つで補っています。1 つ目は Hardened Runtime、2 つ目は Developer ID 署名と公証です。 3 つ目は、root で動く実行体をヘルパー 1 つに限定し、そのヘルパーが実行できる操作を固定して一覧化して いること(§3 の表)。4 つ目は、ヘルパーへの接続をコード署名で制限していること(§3)です。

§3特権ヘルパーの設計

登録のされ方

ヘルパーは、SMAppService.daemon(plistName:) を使って LaunchDaemon として登録します。 アプリに埋め込んだ plist(Contents/Library/LaunchDaemons/com.tetsuharu.RoamSwitch.Helper.plist) で宣言しているのは、Label、BundleProgram、MachServices の 1 エントリ、 AssociatedBundleIdentifiers だけです。SMAppService の仕様上、アプリが /Applications に置かれていないと、そもそも登録できません。初回の登録時には、ユーザーが システム設定でヘルパーを手動で承認しないと有効になりません。

誰の接続を受けるか(ClientValidator)

ヘルパーは、NSXPCListener の shouldAcceptNewConnection で接続元を検査し、これを 通過したものにだけ HelperProtocol を渡します。検査には、PID ではなく audit_token を使っています。PID の使い回しや TOCTOU を避けるためです。

# Release ビルドで接続元に要求するコード署名要件
identifier "com.tetsuharu.RoamSwitch"
  and anchor apple generic
  and certificate leaf[subject.OU] = "GV76B6G4YU"

この要件を SecCodeCopyGuestWithAttributes と SecStaticCodeCheckValidity で 照合し、通らなければ接続を切ります。チーム ID の固定を外しているのは DEBUG ビルドだけで、これは開発中の 都合によるものです。実際に配布されるのは、常に Release ビルドです。

境界の要点

ヘルパーの安全性は、このコード署名要件 1 つに懸かっています。要件を満たす相手(正規に署名された RoamSwitch.app)であれば、下の表にある操作をすべて呼び出せます。任意のコマンドを流し込む 口はありませんが、表に並んでいる操作そのものは、決して弱いものではありません。RoamSwitch.app 本体が乗っ取られた場合、これらの操作は攻撃者の手に渡ります。

ヘルパーができること(全リスト)

Shared/HelperProtocol.swift が定義している特権操作は、以下がすべてです。ここに載っていない 特権 API はありません。

HelperProtocol — root で実行される操作の全て
メソッド行うこと起動されるバイナリ / API
setBlockAll(_:) アプリケーションファイアウォールとステルスモードの ON / OFF /usr/libexec/ApplicationFirewall/socketfilterfw
--setblockall / --setstealthmode
getBlockAllStatus(...) 上記の現在値の読み取り socketfilterfw --getblockall
setSharingServicesEnabled(_:) SSH / SMB / 画面共有デーモンの unload / load。停止時に「元々起動していたもの」だけを記録し、復帰時にそれだけを戻します /bin/launchctl list / unload -w / load -w
(対象は ssh.plist / com.apple.smbd.plist / com.apple.screensharing.plist の 3 つに固定)
enableNetworkAirGap(...)
disableNetworkAirGap(...)
緊急時の全遮断(block drop all)の適用・解除。PFRulesetCoordinator を経由します(§4) /sbin/pfctl -f / -e / -sr
setGuardedDevServerPorts(_:) 指定した開発サーバーポートへの外部からの接続だけを pf で遮断します(localhost は素通し)。空配列を渡すと全解除します /sbin/pfctl(同じく Coordinator 経由)
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 や不正な文字列は捨て、上限 6 万件でキャップ。一時ファイル + アトミック置換で書きます /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 の形式を検証し、要求に無いピンは解除する reconcile 方式。ピン集合は gateway_arp_lock.json に永続化 /usr/sbin/arp -s / -d
/usr/sbin/ndp -s / -d
wireGuardImport(_:)
wireGuardForget(...)
VPN トンネル(§5)。WireGuard の .conf をヘルパーが 0600 で保存 / 削除します ファイル書き込みのみ
wireGuardUp(endpointIPv4:endpointIPv6:port:)
wireGuardDown(...)
wireGuardStatus(...)
トンネルの起動 / 停止 / 状態取得。エンドポイントのホスト名はアプリ側で解決して IP をヘルパーに渡します(キルスイッチ中はヘルパーの DNS が通らないため) Homebrew の wg-quick up/down / wg show(wireguard-tools。未導入時は無効)
terminateProcess(pid:forceKill:) プロセスの一時停止(SIGSTOP)または強制終了(SIGKILL)。ランサムウェア様プロセスの封じ込めに使います。pid > 1 のみ kill(2) システムコール(サブプロセスではありません)
getHelperVersion(...) ヘルパーのバージョン文字列を返します(アプリとの互換性確認に使います) —
設計上のトレードオフ

terminateProcess は、pid > 1 でさえあれば、どのプロセスにも SIGKILL を送れます。setSecureDNSServers には、任意の DNS サーバー文字列を 渡せます。機能を成立させるうえで必要な広さではありますが、狭いとは言えません。手前にあるコード署名 チェック(ClientValidator)が唯一の関門です。この点を踏まえて判断してください。

ヘルパー内部の状態

  • HelperTool.shared は、接続をまたいで共有する 1 つのインスタンスにしています。以前は接続ごとに別インスタンスを渡していたため、緊急封じ込めが新しい接続を開くと、「どのサービスを元に戻せばよいか」という記録を取りこぼすレースが発生していました。
  • 共有サービスと DNS のバックアップは、直列キュー(stateQueue)の上でのみ書き換わります。

ヘルパー内部の権限分離と、SSH のログを読むための例外

ヘルパーは起動の直後に、自分自身へカーネルが強制する上限(実行時のサンドボックス。上の App Sandbox のエンタイトルメントとは別のものです)を掛け、ヘルパーが起動する子プロセスもこれを引き継ぎます。システムのプログラムの置き換え、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パケットフィルタ(pf)の扱い

pf を操作する機能は 4 つあります。緊急 Air-Gap、開発サーバーのポートガード、VPN トンネルの WireGuard キルスイッチ(§5、1.7.6 以降)、Tailscale キルスイッチ(§5、1.8.0 以降。選択したバックエンドの分だけ)です。いずれも PFRulesetCoordinator という 1 つの窓口を必ず経由し、自分で pfctl -f を実行することはありません。

なぜ窓口を 1 つにしたか

以前は、2 つの機能がそれぞれ独自に pfctl -f でルールを読み込んでおり、pf に 1 つしかない メインルールセットを奪い合っていました。ポートガードの狭いルール block ... port {…} が、 Air-Gap の block drop all よりあとに読み込まれると、画面には「隔離中」と表示されているのに Mac には到達できてしまう、という状態が発生していました。この不具合は、別のマシンから実際に攻撃を試して 発見し、1.4.3 で修正しています(経緯は docs/marketing/zenn/03_lan_side_attack_test.md に まとめています)。

いまの作り

  • 毎回、丸ごと組み直します。現在の状態から、必要なルールセット全体を作り直し、一括で適用します。差分での適用は行いません。
  • 直列キューは 1 本です。pf を変更する処理はすべて同じ DispatchQueue に乗るため、どの XPC 接続から来ても、ヘルパーの起動処理でも、フェイルセーフタイマーでも、順番に処理されます。
  • 優先順位は次のとおりです(上にあるものが優先されます)。
    1. 緊急 Air-Gap → set skip on lo0 と block drop all(ほかは考慮しません)
    2. VPN キルスイッチ → block drop all +「lo / トンネル インターフェース(utunN)/ 固定したエンドポイント IP への UDP ハンドシェイク / DHCP / ICMP」だけを pass quick
    3. 開発サーバーガード → block drop in quick proto tcp ... port { … }
    4. どれもない場合 → /etc/pf.conf を読み直し、pf を元の状態に戻します
  • 適用したあとに読み返します。pfctl -sr でルールを読み戻し、block drop all や各ポートのルールが実際に載っているかを確認します。pfctl -f が黙って無視された場合を「成功」とは扱いません。
  • 一時ファイルは UUID を含むパスに書き込み、適用が終わったら削除します(v1.4.5 で固定パスをやめ、推測しにくいパスに変更しました)。状態を保存するディレクトリは /Library/Application Support/RoamSwitch です。
API の挙動

enableNetworkAirGap と setGuardedDevServerPorts の XPC 応答 (Bool, String?) は、読み返しまで通過したかどうかを返します。呼び出し側 (ARPSpoofContainmentManager など)は、失敗した場合はやり直し、それでも駄目な場合は 「まだ通信は止まっていません。すぐに Wi-Fi を切ってください」という内容を、そのまま画面に表示します。

§5遮断のかかり方と、戻り方

遮断は、それぞれ別物です

遮断の種類
種類範囲発動条件loopback
緊急 Air-Gap 入る通信も出る通信も、すべて止めます ランサムウェアのような暗号化の動きを検知したとき。ARP スプーフィング検知時はロックダウン中のみ即時発動し、バランス/信頼済みネットワークでは通知のみ(ユーザーが手動で発動)。curl 等で取得した内容を Base64 デコードしてシェル/AppleScript に直接渡すなど「ClickFix」型の既知の不審コマンドパターンをシェル履歴で検知したとき(1.8.7〜、Pro・既定オフ)。ふだんの外出先保護では使いません set skip on lo0 で通します
VPN キルスイッチ トンネルとハンドシェイク・DHCP・ICMP 以外の全通信 VPN トンネル(§5、Pro・既定オフ)が有効で、未信頼ネットワークに接続したとき。トンネル確立まで(および切断時)継続 set skip on lo0 で通します
開発サーバーポートガード 指定ポートへの、外からの TCP 接続だけ ボタン 1 つでの手動隔離、または、見慣れない待ち受けポートを検知したときの自動遮断(Pro) localhost からは、今までどおりです
ふだんの未信頼ネットワーク保護 ファイアウォールとステルス、共有の停止(§6)。pf は使いません 登録していないネットワークに接続したとき —
リンク保護 フィッシング/詐欺ドメインへの名前解決だけ(/etc/hosts で 0.0.0.0)。pf は使いません 脅威フィード掲載またはブランド ホモグラフ偽装に該当する接続先。既定でオン(Pro) 影響しません
予防的 ARP/NDP 固定 ゲートウェイ・IPv6 ルーター・同一リンク上 DNS の MAC のみ(近隣キャッシュ)。pf は使いません 未信頼ネットワークに接続したとき(Pro・既定オフ)。ネットワーク変更ごとに再固定 影響しません

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 の項目が近隣テーブルから消え(実機で確認)、確認できないことがあります。その場合は、手動の解除まで、縮退モードのまま残ります。
  • ヘルパーが落ちても、解除の判定は止まりません。KeepAlive を一切持たない専用の LaunchDaemon(AirGapFailsafe)が 3 分ごとに単体で起動し、ヘルパーと同じ判定(低確度は 10 分で解除、高確度は上限での原因の確認と縮退)を行います。ヘルパー自身がクラッシュして自己解除のタイマーごと失われても、このセーフティネットには影響しません(1.8.6〜。確度による判定は 1.9.52〜)。
  • この専用デーモンはヘルパー本体の常駐設定には一切手を加えません。以前(1.8.5)はヘルパー自身の KeepAlive.PathState でこの復旧を実現していましたが、通常のアプリ終了操作と稀に干渉して意図せず再起動してしまう不具合があったため撤回し、現在の完全独立デーモン方式に置き換えました。
  • タイムスタンプの判定は、単調増加時間(ProcessInfo.systemUptime)と壁時計を組み合わせたハイブリッド方式です。通常は単調増加時間を優先するため NTP 補正や手動での時刻変更の影響を受けません。reboot 直後で単調増加時間が信用できない場合(保存値より現在値が小さい=再起動を検知)にのみ、壁時計にフォールバックします。
  • 再起動やデーモンの再生成のあとに、当て直します。ヘルパーは起動時に reapplyFromDisk() でディスク上の状態を読み込み、Air-Gap、ポートガード、システム既定の順に、自分で復元します。隔離の状態(確度、全面か縮退か)と経過時間も引き継ぎ、時間の計測は、最初の発動時刻から続きます(実機で、ヘルパーを再起動して確認)。
  • 解除に失敗した場合は、失敗として扱います。block drop all を実際に外せなかったときは、タイムスタンプを書き戻し、フェイルセーフタイマーと再試行が向かう先を残します。画面が「解除済み」と誤って表示することはありません。
  • ユーザーは、メニューバーの「Air-Gap隔離を解除する」、モーダルの解除ボタン、Wi-Fi を切ることのいずれでも、いつでも操作の主導権を取り戻せます。解除の操作は、Pro 版でなくても使えます(Pro が隔離の途中で失効しても、解除できます)。

ポートガードの誤検知からの戻し方

未知ポートの自動遮断(Pro、Pro 有効化時に既定でオン)が、正規の LAN 受信アプリ (LocalSend や Syncthing など、ガードを有効にしたあとに起動したもの)を止めてしまうことがあります。 そのときは、通知バナーの「許可する」ボタン、または「外部公開ポート」画面の該当項目から解除できます。 一度許可した実行体は既知として記録され、以降は再び遮断されません (PortAnomalyGuard.allowPort(_:))。なお、Python や Node.js、Netcat などの汎用インタプリタは、 Living-off-the-land 攻撃を防止するためバイナリ単体ではなく「実行パス+ポート番号」で厳格に識別されます。 anchor apple を満たす macOS 標準のシステムデーモン(Handoff の rapportd など)は、 最初から監視対象外として安全に保護されます。

ランサムウェアおとり(カナリア)の自律復元と隔離中制御

カナリアファイル(おとり)が改ざん・リネームされて緊急 Air-Gap が発動した場合、安全を確認して隔離を解除すると、 破損・消失したカナリアファイルは自動的にアプリ内の正規テンプレートから再生成・ハッシュ修復され、 kqueue リアルタイム監視が途切れることなく即座に再開されます(攻撃者による改変データが新ベースラインとして誤認されることはありません)。 また、緊急隔離発動中(モーダル表示中)は定期整合性ポーリングからの多重発火および重複通知を自動抑止し、ユーザーの確認・対処を妨げない設計としています。

汎用エントロピー型ランサムウェア検知 — 1.10.0、Pro・既定オン

Documents・Desktop・Downloads・Pictures フォルダを FSEvents で監視し、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版の機能です(事前スナップショットの取得自体は無料版でも動きます)。

フォレンジック証跡バンドル — 1.10.0

カナリア検知やランサムウェアのエントロピー検知などで緊急 Air-Gap が始まると、隔離処理をブロックしないバックグラウンドキューで、その時点のプロセス一覧(ps)・ネットワーク接続(lsof -i)・直近5分間の変更ファイル・疑わしいプロセスが開いているファイルの一覧を、~/Library/Application Support/RoamSwitch/incident-evidence/<タイムスタンプ>/manifest.json に保存します。各証跡の SHA-256 を manifest に記録し、事後の改ざん検知に使えます。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・メールアプリが、直接シェルを起動/一時フォルダや隔離属性つきの場所から、署名なしのバイナリを実行/ダウンロードしてシェルへ渡す一行コマンド(生 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〜。機密漏洩を避けるため、環境変数はこの1つの値だけを読み取ります。Apple 署名のプロセスは対象外)/コマンドラインが、RoamSwitch の設置した偽の認証情報ファイル(~/.aws/credentials や ~/.env.backup など、中身に目印があるもの)を直接指定する実行(cat、tar、curl -F など。本物のファイルは対象外。ライブラリ経由の読み取りはここでは検知せず、ハニートークンの監視が担当します)。各検知には、MITRE ATT&CK の技法 ID が付きます。

通知は、重大度が高いものだけをバナーにし、同じ種類は 10 分に 1 回にまとめます(記録には全件が残ります)。Claude Code や GitHub CLI など、開発ツールが、自分の認証情報を読む動作は、既定で通知しません。ほかのプログラムが読んだ場合は、通知の「このプログラムを許可」で、その項目だけを許可できます(署名がある場合は Team ID、ない場合は実行ファイル名と、項目名の組)。

記録は、root だけが読める形(ディレクトリ 0700、ファイル 0600)で保存し、既定で 200 MB・14 日を保持します。環境変数は保存しませんが、コマンドライン引数には、機密が含まれることがあります。各セグメントは、前のセグメントのハッシュにつなぐハッシュチェーンで封印し、書き換え、セグメントの欠落、封印の削除を、「チェーンを検証」で検出します(実機で、それぞれ確認)。改ざんの検知であり、防止ではありません。最新のセグメントの削除・巻き戻しは、検出できません。読み取りは、ヘルパーの XPC を通り、MCP は、アプリ経由で(Pro の再確認つき)です。

限界: ヘルパーの起動前や、記録を有効にする前の実行は、記録されません。eslogger は事後の観測で、ブロックはできません。1 秒に数千回のプロセス起動が続くような負荷では、取りこぼしが出ます(件数を画面に表示し、動作は続けます)。この記録は、EDR(エンドポイントの検知と対応)の代わりではありません。

認証情報ハニートークン — 1.10.0、Pro・既定オン

~/.aws/credentials・~/.ssh/id_rsa_backup・~/.docker/config.json の3か所に、目印を埋め込んだ偽の認証情報ファイルを設置します。設置先に本物のファイルが既にある場合は、絶対に上書きしません。アクセスの検知は、カナリアガードと同じ kqueue(DispatchSource.makeFileSystemObjectSource)に加え、EndpointSecurity 無しでは open() イベントを確実に拾えないため、30秒間隔のアクセス日時ポーリングを併用します。検知は通知のみで、遮断や隔離は行いません。ssh が既定で探す名前(id_rsa)とは別の名前にしてあり、通常の ssh の実行を壊したり、誤検知を出したりしないようにしています。AWS の偽装は [default] ではなく別のプロファイル名、Docker の偽装は存在しない社内レジストリ名を使い、いずれも 0600 で設置します(1.10.3 以降)。3か所に加えて、~/.pypirc と ~/.env.backup にも設置します(PyPI のトークンや、.env・AI の API キーを狙う探索向け)。~/.npmrc は npm が平常時に毎回読むため、GCP の認証情報は偽のファイルがあると正規の認証が壊れるため、囮にしません。囮に実際のアクセスがあると、関連する囮(~/.aws/config・~/.ssh/config・~/.kube/config)を1件だけ追加で設置し、その追加分も監視します(連鎖は1段で止まり、既存のファイルは上書きしません)。

ブラウザ認証情報アクセス監視 — 1.10.0、Pro内でもオプトイン・既定オフ

Chrome・Brave・Edge・Vivaldi・Chromium・Firefox の保存済みパスワード/Cookie データベースへの書き込みを FSEvents で監視します。バースト発生時に lsof でプロセスを特定し、ブラウザ自身・Spotlight 等のインデクサ・バックアップデーモン以外を検知対象とします。他の新規ガードと異なり、Pro を有効にしても自動ではオンになりません。ブラウザ拡張機能やパスワードマネージャーのインポート機能などが引っかかる誤検知のリスクが本質的に残るためです。

Sensor との通信(TLS と署名)

同一 LAN の RoamSwitch Sensor(別製品)と、証明書のフィンガープリント(SHA-256)を固定した TLS 1.3 で、ペアリング・監査の要求・結果の取得を行います(Sensor 0.3.9 以降)。フィンガープリントは、ネットワーク経由では取得せず、Sensor の画面に表示された値を、利用者が入力します。認証局・ホスト名・有効期限は検証せず、固定した値だけで信頼します。TLS が失敗しても、平文には切り替えません(切り替えると、中間者に、ダウングレードされるため)。要求の署名は、時刻と nonce を含む形(v2)で、再送を防ぎます。フィンガープリントのない、旧形式のペアリングだけは、従来どおり平文で接続し、その旨を警告します。

証明書が変わると、接続を拒否します。固定の更新は、自動では行わず、利用者が、新しい値を入力して確認したときに、実際に TLS で往復して、署名つきの探査に成功した場合だけ、保存します。限界: この探査は、その Sensor が、以前ペアリングした Sensor と同じであることを、鍵で照合するものではありません。利用者が、Sensor の画面で確認した値を、入力する前提です。

ダウンロードファイルの多層検知(静的シグネチャ + ClamAV) — 1.8.7 で静的シグネチャ層を追加

ダウンロードフォルダやデスクトップ、書類フォルダに新規配置されたファイルは、まず StaticSignatureScanner による軽量な静的シグネチャ照合を通過します(先頭 4MB を対象、AND/OR パターンの単純なバイト列一致のみで、正規表現やエントロピー計算は行いません)。EndpointSecurity entitlement を使わず、ClamAV が未導入の環境でも動作します。検知対象は、実際のマルウェア検体を持たずに正確さを保証できる範囲——業界標準の EICAR テスト署名と、公開の攻撃手法解説(PayloadsAllTheThings 等)に掲載されている定番のリバースシェル一行コマンド 5 種(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 シェル検知(Web Rules)

ローカルの Web 開発(~/Sites、MAMP、Herd、Local、/Library/WebServer/Documents、パッケージ CVE 照合に登録したプロジェクトフォルダー)にある PHP・JavaScript・JSP・ASP のファイルを、Linux 版と共通の 18 本の検知ルールで検査します。ルールは公開されている手口だけを記述したもので、実際の検体から抜き出した署名は含みません。アプリに同梱、毎日 1 回の署名付き更新(受信専用。既存の更新と同じ 1 回の取得にまとまる)、~/Library/Application Support/RoamSwitch/rules.d の独自ルール(所有者が root か自分自身で、グループ・他人が書き込めないファイルだけを読む)の 3 層です。ダウンロードなど既に監視しているフォルダーでは書き込みの直後に、開発用の Web フォルダーは 1 日 1 回と手動で検査します(1 日 1 回の検査は node_modules と .git に入りません。フォルダーを指定した検査は node_modules にも入ります)。EndpointSecurity を使わないため、実行前に止めることはできず、すべて事後の検知です。

自動で隔離するのは既定で critical だけです。high は、ファイルに触れず、通知と設定画面の一覧に出し、「今後は検知しない」を選べます(隔離の水準は設定で変えられます)。検知後フック(~/Library/Application Support/RoamSwitch/hooks/post-detection)は、所有者が root か自分自身で、書き込み不可のものだけを実行し、30 秒で打ち切ります。アカウント対処(既定オフ、有効化に管理者パスワードが必要)は、隔離したファイルの持ち主のログインシェルを無効にしますが、root、UID 501 未満、ログイン中のユーザー(この Mac を使っているあなたを含む)には何もしません。root 権限で、ヘルパーと同じサンドボックスの下で動かし、macOS 26 の仮想マシンで、実際の dscl によるシェルの変更と復元を確かめました(サンドボックスは dslocal への直接の書き込みは止めますが、dscl 経由の変更は止めません)。管理者パスワードの確認と、アプリからヘルパーへの呼び出しの経路は、まだ実機で確かめていません。手元の開発フォルダー 114,501 ファイルでの検知は 0 件でした。

新しい自動起動登録(LaunchAgent/LaunchDaemon)の監視 — 1.8.7

~/Library/LaunchAgents・/Library/LaunchAgents・/Library/LaunchDaemons への新規 .plist 設置を FSEvents でリアルタイム検知します(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 やエラー画面が「確認のため Terminal を開いてこのコマンドを貼り付けてください」と指示し、ユーザー自身にコマンドを実行させる社会工学的な攻撃手法です。2024〜2025 年にかけて検知数が 500% 以上増加し、2026 年時点で macOS における最も蔓延した攻撃経路の一つとされています。実行するのはユーザー自身の正規のシェル操作であるため、Gatekeeper の署名検証を完全にすり抜けます。

メニューバー →「マルウェア対策」→ ClickFix 対策(Pro・既定オフ)。~/.zsh_history・~/.bash_history への追記を FSEvents で監視し(起動後に新規追加された行のみが対象で、既存の履歴はさかのぼりません)、StaticSignatureScanner と共有するリバースシェル一行コマンド、および Base64 デコードした内容をシェルまたは osascript に直接パイプする複合パターンのみを検知します。正規のインストーラーで極めて頻繁に使われる curl | bash 単体は、Homebrew・rustup・nvm 等の公式インストール手順と区別が付かないため、意図的に検知対象から外しています。検知した瞬間、既存の緊急 Air-Gap(本節)を発動します。コマンドはすでに実行済みのため事後対応にはなりますが、多段ペイロードの追加ダウンロードや、認証情報の外部送信が進行中であれば、それを遮断できる可能性があります。他の Air-Gap 発動要因と同様、最長 10 分で自律的に解除されます(本節冒頭)。

既知の限界。 検知はシェル履歴への書き込みが前提のため、Terminal を経由せず applescript:// URL スキームで Script Editor を直接呼び出し、ペイロードをディスクに一切書き込まないファイルレス型の亜種には対応できません。この種の亜種は、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 ライセンスでも既定は OFF のままです。EndpointSecurity entitlement は使用せず、docker CLI は Docker Desktop の既定インストール先・Homebrew・which の順で探索します。実機の Docker Desktop(29.7.2)で privileged コンテナ・docker.sock マウントコンテナ・通常コンテナの 3 パターンを検証し、誤検知・検知漏れが無いことを確認済みです。

機密情報・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 を超えるファイルやバイナリと判定されたファイルもスキップします。処理はメインスレッドをブロックしないよう別スレッドで実行され、外部への送信は一切発生しません。

リンク保護(フィッシング接続の遮断) — 1.7.2 以降、1.8.0 で強化

メニューバー →「マルウェア対策」→「リンク保護」(Pro)。フィッシング/詐欺サイトへの接続を、ブラウザやアプリを問わず端末側で遮断します。強制ポイントは 2 段で、優先順に併用します。

  • ① コンテンツフィルタ システム拡張(RoamSwitchLinkFilter、1.8.0〜、承認後に優先)。NEFilterDataProvider の system extension です。Apple の申請は不要(コンテンツフィルタは 2016 年からセルフサービス、審査キューなし)。名前解決の後に実際の外向き TCP フローを見ます。宛先の名前は OS 解決名、無ければ TLS ClientHello の SNI をパースして得るため、ブラウザーが自前で DoH/DoT 解決して生 IP に繋ぐケースも遮断できます。/etc/hosts は書き換えず、プロセス単位の帰属が取れます。QUIC(UDP/443)は SNI が読めないため block モードでは drop し、ブラウザーを HTTP/2(TCP)にフォールバックさせます。初回のみシステム設定での承認が要ります。
  • ② /etc/hosts シンクホール(フォールバック、拡張が未承認・拒否のとき)。特権ヘルパーが /etc/hosts の区切り付き管理セクションに対象ドメインを 0.0.0.0 で書き込み、dscacheutil -flushcache と killall -HUP mDNSResponder でキャッシュを流します。上限 6 万件。拡張が有効になるとこのセクションは撤去されます。
  • 3 モード。「オフ」=無効。「警告のみ」=該当接続を一時停止してユーザーに諮ります(下記の実 warn)。「明らかな詐欺サイトは自動でブロック(推奨)」=即遮断。1.7.2 以降の既定はブロック。
  • 実 warn(1.8.0〜、拡張のみ)。warn ヒット時、システム拡張は該当フロー(OS 解決名・SNI の両経路)を .pause() で保留し、アプリが「許可/ブロック」通知を出します。ユーザーのタップが allowlist.txt/extra.txt に届いた瞬間(拡張が小さな状態ファイルを 1.5 秒間隔でポーリング)、保留フローを resume(許可)または drop(ブロック)します。25 秒無応答なら fail-open(warn は助言的なので通す)。判断はホスト単位でキャッシュ(許可 5 分/ブロック 1 時間)し、同じページの他フローや再訪は即時・強制。block 通知は従来どおり「〜をブロックしました」+「今回は許可(5 分)」。
  • 遮断は明確なケースだけ。「脅威フィード掲載」または「ブランド名の Unicode ホモグラフ偽装」に該当するときだけブロックし、それ以外(高リスク TLD、サブドメイン偽装など)は警告に留めます。判定エンジンは Linux 版と共通で、URL をどこにも送りません。
  • 誤遮断からの戻し方。通知またはメニューからドメインを「許可」(5 分間 or 恒久)。許可リストはセクション再生成時に差し引かれます。
  • Pro ゲート。遮断の適用(applyMode())は Pro ライセンスが有効なときだけ行われます。非 Pro ではモード設定は保存されますが /etc/hosts は一切変更されません。ライセンスの有効化・失効はセッション中でも反映されます。
  • フィードと同梱シード。ブロック対象の一覧は署名付きの脅威フィード(§7、フィード専用鍵で検証)由来です。未取得でもアプリ同梱のシード(約 6 万件)で動作し、「自動更新」をオフにすると外部通信は発生しません。

予防的ゲートウェイ ARP/NDP 固定 — 1.7.5 以降

メニューバー →「ポート・デバイス監視」→「未信頼ネットワークでゲートウェイの ARP/NDP を固定(予防)」(Pro・既定オフ)。

  • 仕組み。未信頼ネットワークに接続した時点で、route / scutil --dns / arp -n / ndp -an から IPv4 ゲートウェイ・IPv6 デフォルトルーター・同一リンク上の DNS リゾルバの現在の MAC を収集し、ヘルパーが arp -s / ndp -s で permanent エントリとして固定します(TOFU=最初に観測した MAC を信頼)。以降その IP に対する ARP/NDP スプーフィング応答は無視され、中間者攻撃を未然に防ぎます。
  • 範囲。固定するのは上記 3 種のエントリだけです。信頼済み(オープン)ネットワークでは固定しません。ネットワークが変わるたびに一度解除して再固定します。off-link のパブリック リゾルバ(8.8.8.8 等)は同一リンクに ARP エントリが無いため自動的に対象外になります。
  • arp -s は予防、Air-Gap は事後。この固定は「なりすましを成立させない」ための予防策で、ARP スプーフィング検知(ARPSpoofContainmentManager)と緊急 Air-Gap は「検知したら人間より速く切る」ための事後策です。両者は独立して動きます。
  • 永続化。ピン集合は /Library/Application Support/RoamSwitch/gateway_arp_lock.json に保存され、XPC 再接続やヘルパー再起動をまたいで解除できます。

VPN トンネル(WireGuard/Tailscale)とキルスイッチ — 1.7.6 以降、1.8.0 でバックエンド選択

メニューバー →「ポート・デバイス監視」→「VPN トンネル」(Pro・既定オフ)。L2(ARP/NDP)の完全性に依存しない、中間者攻撃対策の本命です。バックエンドは「WireGuard(設定ファイル)」または「Tailscale(Exit Node)」から選べます(サブメニュー →「バックエンド」)。RoamSwitch 自身は暗号処理を実装せず、既存の VPN/tailnet に相乗りします。選ばれた方だけを arm します。

  • (A) WireGuard バックエンド。Apple の Network Extension エンタイトルメントは使わず、Homebrew の wireguard-tools(wg-quick + wireguard-go)を呼び出します。ユーザーが自分の WireGuard 設定(.conf)を読み込むと、ヘルパーが 0600 で保存します。キルスイッチは pf の block drop all +「lo / トンネル IF / 固定エンドポイント IP への UDP ハンドシェイク / DHCP / ICMP」だけの pass quick。AllowedIPs がフルトンネルでない設定にはスプリット トンネル警告を出します。
  • (B) Tailscale バックエンド(1.8.0〜)。既に Tailscale を使っているユーザー向け。RoamSwitch は tailscale up/ログイン/導入は行わず、tailscale status --json を読み tailscale set --exit-node=<ノード> を実行するだけです。CLI 版(brew install tailscale)推奨——App Store 版(GUI)は sandbox のためアプリ外から制御できず、その場合は Tailscale アプリで Exit Node を選び RoamSwitch は状態表示のみを担当します。Exit Node 必須(全通信をそこ経由に)。オフライン/到達不能なら自動解除。
  • ⚠️ Homebrew 版(brew install tailscale)は非推奨。Homebrew が配布する tailscaled には、macOS でルーティングテーブルを正しく書き換えられない既知の不具合があります(tailscale/tailscale#21395・#21216)。Exit Node への接続が「成功」と表示されても実際には通信が漏れる、または解除時にインターネット接続そのものが失われる場合があります。RoamSwitch は実際の OS ルーティングテーブルを検証してこの状態を検知・警告しますが、そもそも上記の Standalone 版を使うことで回避できます。
  • 切断時のネットワーク再構成。standalone の macOS tailscaled は Exit Node 解除時に routing/DNS を正しく戻さない既知の症状があるため、RoamSwitch はアクティブなネットワークサービスを bounce します(「Wi-Fi 手動トグル」と同じ。IPv4 アドレスを持つサービスのみ、複数 NIC・static IP でも安全)。この間 5〜10 秒ほど通信が途切れ、その間 RoamSwitch は保護レベルを降格しません。
  • Tailscale キルスイッチは既定オフ・オプトイン。Exit Node 自体がトンネルを提供するので、pf block drop all(CGNAT/MagicDNS/STUN/DERP/DHCP/ICMP/DNS のみ許可)は上級者向けです。DNS を許可するぶん「漏れにくい」止まりです。
  • 自動適用/ライセンス失効。未信頼ネットワークでトンネル/Exit Node を起動、信頼済みで停止。Pro ライセンス失効で解除。

§6ふだんの未信頼ネットワーク保護

登録していないネットワークに接続したときの「保護レベル」の切り替えは、pf ではなく、OS 標準の設定を、 あとで元に戻せる形で変更しているだけです。

保護レベルごとの操作
操作実装権限復元方法
ファイアウォール + ステルスモード ON socketfilterfw --setblockall on / --setstealthmode on root(ヘルパー) 安全なネットワークに復帰したときに off
SSH / SMB / 画面共有の停止 launchctl unload -w root(ヘルパー) 停止時に起動していたものだけを記録し、復帰時に load -w(SSH は /usr/sbin/systemsetup -setremotelogin on も連携実行して確実に復元)
AirDrop を無効化 defaults write com.apple.sharingd DiscoverableMode ユーザー(アプリ本体) 直前の値を保存しておき、復帰時に書き戻す

いずれも、RoamSwitch が新しく追加した遮断のしくみではなく、OS の設定を切り替えているだけです。 アプリを削除すると、ネットワークに応じた切り替えが行われなくなるだけで、最後に適用された OS 設定は そのまま残ります。締まったまま放置されることはありませんが、安全側に倒したい場合は、アンインストールの 前に、信頼できるネットワーク上で「オープン」に戻しておくことをおすすめします。

§7データの扱いと Zero Telemetry

Mac の中に残るもの

ディスクに保存されるデータ
データ場所内容
ライセンストークンKeychain
com.tetsuharu.RoamSwitch.license
Ed25519 署名付きトークン。kSecAttrAccessibleAfterFirstUnlock
アプリ設定 / ガードの ON-OFFUserDefaults suite
com.tetsuharu.RoamSwitch
信頼ネットワークの登録、保護ポリシー、除外リストなど
pf 状態/Library/Application Support/RoamSwitch/Air-Gap のタイムスタンプ、ガード対象ポートの JSON、VPN キルスイッチ状態、適用中ルールの一時ファイル
リンク保護の脅威フィード~/Library/Application Support/RoamSwitch/threatfeed/feed.txtダウンロード済みのフィッシング/詐欺ドメイン一覧(未取得ならアプリ同梱のシード)。フィード版数は UserDefaults
リンク保護の管理セクション/etc/hosts# BEGIN RoamSwitch link guard 〜 # END で囲まれた区切り付きセクション。ブロック対象を 0.0.0.0 で無効化。モード「オフ」で削除されます(§5)
ARP/NDP 固定ピン/Library/Application Support/RoamSwitch/gateway_arp_lock.json予防ロック(§5)で permanent 化した IP → MAC の集合。固定解除時に削除
WireGuard 設定/Library/Application Support/RoamSwitch/(ヘルパー領域、0600)ユーザーが読み込んだ .conf の内容。エンドポイントのホスト名は UserDefaults にも退避(アプリ側で解決するため)
デバイスフォールバック UUIDUserDefaultsIOKit が UUID を返さないときにのみ生成される乱数(§9)
ログos.Logger / NSLog統合ログ。外部への送信はありません

外に出る通信(全リスト)

診断結果、ポート情報、URL、ログを収集して送信するコードは、どこにもありません。解析 SDK も クラッシュレポーター SDK も入れていません。外部ライブラリは Sparkle(アップデート)だけです。 ネットワークに出るのは、次の 8 つに、表の下に記した、ユーザー操作で動く機能・既定オフの機能・DNS 脅威保護を加えたものがすべてです(6 つ目はユーザーが VPN を設定した場合のみ、7 つ目は RoamSwitch Sensor ペアリングを有効にした場合のみ)。

RoamSwitch が行う外部通信
通信宛先発生条件送信内容
ライセンス認証 / 解除 lafine.net
/api/v1/license/*
ユーザーがライセンスキーを入力したとき、または Pro を解除したときだけ ライセンスキー、デバイスハッシュ、端末名(システム設定の「名前」)、アプリバージョン。解除時はキーとデバイスハッシュのみ。個人情報は購入時に Stripe が扱い、アプリは扱いません
アップデート確認 lafine.net
/updates/appcast.xml
Sparkle が 24 時間ごとに実施(前回の確認から 24 時間以上空いていれば、起動時にも実施)。標準では常に有効で、システム情報は送りません HTTP リクエスト(標準的な UA・バージョン)。取得物は EdDSA 署名で検証します(§10)
リンク保護の脅威フィード 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 日に 1 回です。常時(パッケージCVE照合の更新データはオフにする設定がなく、ローカルの一覧照合にのみ使う受信専用データのため、Free 版を含む全ユーザーが対象です。実証型脆弱性診断のCVEマップは「実証型脆弱性診断(能動的到達確認)」を有効にした場合のみ取得します) GET のみ。クエリ文字列・Cookie・端末を識別する情報はありません(標準の User-Agent として、アプリ名・バージョンと macOS の版が含まれます)。受信専用の署名付き静的ファイルで、マニフェストとフィード本体を Ed25519 で検証します。リンク保護の脅威フィードと同じ鍵・同じマニフェストを使い回しますが、取得するフィード自体(Homebrew/npm/PyPI/crates.io/RubyGems/Packagist/Go/Maven の既知CVEマップ、実証型脆弱性診断のCVEマップ、タイポスクワッティング検知用の人気npmパッケージ名リスト)は別ファイルです
ClamAV ウイルス定義更新 ClamAV 公式ミラー ユーザーが ClamAV を導入した場合。Pro 版では起動時・24 時間ごと・自律巡回時に自動で、Free 版ではメニューの「ウイルス定義を更新」を押したときだけ、freshclam を起動します ClamAV 標準の定義取得。RoamSwitch 由来の情報は含みません(ClamAV 自身の標準的な識別情報は含まれることがあります)
VPN トンネル(§5、WireGuard/Tailscale) ユーザーが設定した WireGuard エンドポイント、または Tailscale のコントロールプレーン(Tailscale, Inc.) VPN トンネル(Pro・既定オフ)を設定し、未信頼ネットワークに接続したときだけ。WireGuard 選択時はトンネル起動前にエンドポイントのホスト名を 1 回 DNS 解決します。Tailscale 選択時は、ユーザー自身が導入・ログイン済みの tailscaled が行うコントロールプレーン通信で、RoamSwitch 自身は tailscale status --json の読み取りと tailscale set --exit-node= の実行のみを行います WireGuard 選択時は WireGuard のハンドシェイク(UDP)とトンネル内の通信。宛先はユーザー自身の VPN サーバーで、内容はユーザーの通信そのものです。Tailscale 選択時は、ユーザー自身の Tailscale アカウントと Tailscale, Inc. のコントロールプレーン間の通信(デバイス登録・DERP リレー等、Tailscale 社のプライバシーポリシーに準拠)。いずれの場合も RoamSwitch は識別子・診断結果を一切足しません
RoamSwitch Sensor ペアリング・監査連携 同一LAN上のRoamSwitch Sensor(ユーザー自身が設置し、固定IPで運用する機器) 「RoamSwitch Sensor ペアリング」(Pro・既定オフ)を有効にし、SensorのIPアドレスとペアリングコードを入力してペアリングしたとき(一度きりの通信)。ペアリング後は、「Sensorへ監査をリクエスト」を押したとき、および結果の自動取得(リクエストの5分後から5分おきに最大5回)のときに、同じSensorの固定IPアドレスへ接続します ペアリング時は自端末のホスト名とEd25519公開鍵を送信し、Sensorから発行された公開鍵を受け取ります。監査リクエスト・結果取得はEd25519署名で認証された通信で、送信するのは署名付きのリクエストのみです。診断結果やポート情報自体はSensorからの応答としてのみ受信し、ファイルの中身が送信されることはありません。通信先はユーザー自身が設置したSensorの固定IPアドレスのみで、lafine.netを含む外部サーバーには一切届きません。相互信頼はペアリングコードによる明示的な操作でのみ成立します
決済ページ Stripe Checkout ユーザーが購入ボタンを押したときだけ(ブラウザで開きます) —(ブラウザ側の遷移です)
「Zero Telemetry」の範囲

ここで言う「Zero Telemetry」は、利用状況や診断結果を収集して送信するテレメトリを行わない、という 意味です。通信を一切しない、という意味ではありません。上の表の 8 経路と、表の下に記した機能は、実際に存在します。ただし、 いずれもユーザーがきっかけをつくるもの、署名検証つきの受信専用の取得、またはユーザーが選んだ外部サービス(ClamAV の配布元、保護 DNS プロバイダー、確認対象の URL など)との通信であり、Mac 上の診断結果、 ポート、ファイルの中身を開発元へ送ることはありません。

リンク保護の脅威フィード(3 行目)は「送らない」防御に「受け取るだけの更新はする」を足したものです。両者は別の話として切り分けており、Linux 版ホワイトペーパー §1.1 でも「Zero Telemetry」と「受信専用の更新」を分けて記述しています。「自動更新」をオフにすれば、リンク保護は同梱データ(約 6 万件のフィッシング/詐欺ドメイン)とオフラインのホモグラフ検知だけで動作し、この経路は発生しません。

VPN トンネル(6 行目)は、ユーザーが自分の WireGuard サーバーを設定した場合、または Tailscale バックエンドを選び自分の Tailscale アカウントで tailscaled にログイン済みの場合にのみ発生し、宛先も内容もユーザーの管理下にあります。RoamSwitch はトンネルを張る・キルスイッチを掛ける・Tailscale の状態を読んで Exit Node を切り替えるだけで、識別子や利用状況を足すことはありません。VPN を設定しなければこの経路は存在しません。

RoamSwitch Sensor ペアリング(7 行目)は、Pro 限定・既定オフの機能で、有効にしない限りこの経路は存在しません。ペアリングは、Sensorオペレーターが発行したペアリングコードとSensorの固定IPアドレスを入力する明示的な操作でのみ成立します。ペアリング後の通信(監査リクエスト・結果取得)もすべてユーザー自身が設置したSensorの固定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 の飛び先を確認するために、対象の URL へ HEAD リクエストを送信します(プライベートアドレスやローカルアドレスへの追跡は、v1.4.5 の SSRF 対策で停止しています)。一方、MCP の audit_url_safety は、その場で完結するオフライン 解析であり、URL をどこにも送信しません(§8)。

実測(2026-08-29) — 判定 PASS

主張だけではありません。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 のものです。それ以降に加わった経路は上の表に挙げており、現行版での再実測は予定しています。

詳細と、誰でも自分のマシンで再現できるスクリプト: audit/RESULTS-2026-08-29.ja.md / roamswitch-support/audit/。 第三者の追試を歓迎します。

この実測は 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照合の更新データには、オフにする設定がありません)。

§8MCP サーバーのセキュリティモデル

RoamSwitchMCPServer は、RoamSwitch.app/Contents/MacOS/ に同梱している単体の コマンドラインツールです。Claude Desktop や Claude Code などの MCP クライアントが、これをサブプロセスと して起動し、stdio(改行区切りの JSON-RPC 2.0)でやり取りします。公式 SDK が本機の macOS SDK ではビルドできなかったため、Foundation の JSONSerialization の上に手で実装して います。

設計で縛っていること

  • 読み取り専用です。セキュリティレベルの変更、ポートの隔離、デバイスの取り出しといった API は、そもそも用意していません。v1 で作り忘れたわけではなく、意図的に外しています。外部のコード(ここでは LLM)に、セキュリティツールの防御状態を書き換えさせてしまうと、すべてのユーザーの信頼が崩れるためです。
  • 待ち受けソケットを開きません。サービス登録もリッスンもしません。stdin から 1 行読み、stdout に 1 行返し、あとはクライアントがプロセスを終了させます。
  • テレメトリは送信しません。外部のホストへ通信するのは、オプトイン(Mac では Pro 限定)の run_npm_audit_signatures(npm レジストリ)だけです。run_active_vuln_scan(既定で無効)は 127.0.0.1 へのプローブ、Mac の get_exposed_ports は公開ポートごとに 127.0.0.1 へ HTTP GET を1回送ってヘッダを確認するだけです。診断系のツールは、デフォルトゲートウェイの MAC アドレスを調べるため、LAN 内のゲートウェイに ICMP ping を通常1発(MAC を引けないときは数発まで)送ることがあります。
  • 設定は別のドメインから読み取ります。UserDefaults(suiteName: "com.tetsuharu.RoamSwitch") で、アプリ側のドメインを明示的に指定して読み取ります(自身のバンドル ID のドメインは空です)。読み取るだけで、書き込みは行いません。

公開しているツール

tools/list が返す主なツールと、返すデータ(全ツールの一覧は MCP連携設定 を参照)
ツール返すもの通信
get_security_report18 項目診断(FileVault / SIP / Gatekeeper / 自動更新 / XProtect / ファイアウォール / Wi-Fi 暗号化 / ARP / ゲートウェイ ARP 固定 / SSH 設定監査 / Sudo NOPASSWD 監査 / 公開ポート / ガード構成 等)のスコアと、項目別の改善アドバイス。各項目には NIST CSF 2.0・CIS Controls v8 の対応する管理策 ID も付きます(1.10.0〜)。ローカルのみ
verify_security_findingsget_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 のアプリ内設定の値で合否が決まるため、これらはアプリ内設定の反映であって、OS の実状態の再測定ではありません。読み取り専用で、外部のホストには接続せず、修復も設定変更もしません。Linux では LAN にも何も送らず、ゲートウェイはキャッシュを参照するだけです。Mac ではゲートウェイの MAC アドレスを調べるため、LAN 内のゲートウェイに ICMP ping を通常1発(MAC を引けないときは数発まで)送ることがあります。ローカルのみ
audit_mcp_configsAI クライアントの MCP 設定ファイル(Claude Desktop・Claude Code・Cursor・Windsurf・VS Code・Cline・Gemini CLI と、登録済みのプロジェクトフォルダにある .mcp.json など)を読み、危うい設定を指摘します。毎回の確認なしで自動承認されるツール(auto_approve)、loopback 以外への平文 http:// のサーバー(url の欄だけでなく、--server http://… のように起動引数で渡した接続先も。remote_plain_http)、シェルやインタプリタの1行コマンドでの起動(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)、Web ページ・メール・チャット・Issue など第三者の文章を読むサーバーと、コマンド実行やファイル変更ができるサーバーが同じ設定ファイルにある構成(間接プロンプトインジェクションの前提になる組み合わせ。既知のパッケージ名で判定し、サーバーのツールの中身は見ません。injection_exposure)を検出します。引数はなく、読み取り専用で、ネットワークには何も送りません。環境変数やヘッダーの値・引数・秘密は返さず、サーバー名・コマンド名・URL のホスト・(レジストリ名のときの)パッケージ名やイメージ名だけを返します。解析できなかったファイルは unreadableFiles に並び、監査していません。なし
audit_agent_setupAI エージェントに読ませる指示ファイルとスキル(~/.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)、誰からでも受け付ける DM・グループ設定(openclaw_open_dm)、mDNS の full(openclaw_mdns_full)、非表示にした自己診断の指摘(openclaw_audit_suppressed)、緩い権限(openclaw_loose_permissions)を検出します。規則はすべて決め打ちで、一致は「人が読んで確認する箇所」であり、悪意の断定ではありません。引数はなく、読み取り専用で、ネットワークには何も送りません。ファイルの本文や秘密は返さず(トークンは長さだけ)、規則名・行番号・URL のホスト・設定のキー名だけを返します。読めなかったファイルは unreadableFiles に並び、監査していません。
get_exposed_ports待ち受け中の TCP ポート一覧。localhost を越えて公開されているものは、既知危険サービス DB と照合し、公開ポートごとに 127.0.0.1:port へ HTTP GET を 1 回送って(ローカル閉域)CORS / ヘッダを確認します127.0.0.1 へのプローブのみ
get_guard_statusPro の自動対応ガード(ポート異常 / ARP / USB / Bluetooth / Web・Mail ダウンロード / DNS 脅威保護)の ON / OFF、現在の保護レベル、信頼ネットワーク状態ローカルのみ
audit_url_safetyURL のフィッシング / ホモグラフ(Unicode 偽装)/ ブランドサブドメイン偽装 / 高リスク TLD / 平文 HTTP の判定。同期・完全オフラインです(analyzeURL。リダイレクト追跡は行いません)なし
get_app_help同梱ナレッジベースの全文検索(機能仕様 / 設定 / トラブルシュート / 通知メッセージ解説)なし
audit_secretsテキスト・ファイル・ディレクトリからAPIキーや秘密鍵の漏洩を検出します(検出値はマスクして出力)。なし
audit_security_logs直近のセキュリティログ(Sudo認証失敗・SSH総当たり試行・Gatekeeper遮断・XProtect検知など)を集計し、APIキーやトークンなどの機密情報を自動マスキングした上で、ログパターンをテンプレート化した新規パターン検知・頻度異常(統計的スパイク)検知の結果も返します。Linux では、journal を読めなかったとき(タイムアウトなど)は、集計の 0 件を「何も起きていない」とせず、journalReadComplete が false になり、journalError に理由が入ります。ローカルのみ
run_active_vuln_scan127.0.0.1 限定・非破壊の実証型脆弱性検証。外部ホストには接続しませんが、ネットワークリクエストを送るツールのため、既定は無効・設定でのオプトインが必要です。127.0.0.1 のみ(既定 OFF)
run_package_cve_scanインストール済みパッケージ(Mac: Homebrew / Linux: dpkg・dnf・zypper・pacman)をローカルのCVEマップと照合します。通信は一切行いません。Linux では、パッケージの一覧を取得できなかった・タイムアウトしたときは「該当なし」とせず、complete が false になり、incompleteReason に理由が入ります(完了できなかった結果は問題なしではありません)。 Linux の 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_languagesnpm / PyPI / crates.io などの依存ロックファイルを、同じローカルCVEマップと照合します。通信は一切行いません。なし
get_quarantine_statusマルウェア隔離Vaultの中身(元のパス・検出された脅威名・隔離日時・サイズ)を返します。ローカルのみ
get_canary_statusランサムウェア・カナリア(おとりファイル)の設置状況と、直近50件までの検知インシデントを返します。ローカルのみ
get_ransomware_recovery_snapshotsランサムウェア復旧用のAPFSスナップショット(事前・検知時・手動)の一覧、推奨する復旧元、保持モードの状態、取得間隔の設定を返します。検知時のスナップショットには暗号化済みのファイルが含まれ得ます。Pro版限定です。ローカルのみ
search_exec_eventsプロセス実行の記録(eslogger)を検索します。パス・引数・署名IDで絞り込め、記録の状態と、記録を読めるかも返します。Pro版限定です。ローカルのみ
get_process_tree指定したプロセスの祖先と子孫を、実行の記録から組み立てて返します。Pro版限定です。ローカルのみ
get_notification_historyRoamSwitchが送信した通知(セキュリティログ監査の異常、ClickFix検知など)の履歴を、直近1週間ぶん・最新順で返します。ローカルのみ
get_port_anomaly_incidentsポート異常ガードのベースライン取得状況・自動遮断中のポート・直近50件までのインシデントを返します。ローカルのみ
get_ssh_guard_incidentsSSH 総当たりガードの状態と記録を返します。禁止まで行っているか、検知だけか(別の防御ツールが sshd を守っているときなど)、いま禁止している接続元と残り時間、直近の記録(禁止した、検知だけだった、失敗を重ねた接続元からログインに成功した、のいずれか。最大 200 件)を含みます。試されたユーザー名そのものは、Linux では保存せず(数だけ)、Mac では無害化して最大 5 件まで返します。Mac は、最近の SSH ログイン(接続元・ユーザー・認証方法・鍵の指紋)も返します。読み取り専用で、禁止を解除する MCP ツールはありません。Linux では禁止の一覧を root だけが読めるため、一般ユーザーで動かすと件数だけが返ります。Linux は Server Edition と、sshd が動いている Client Edition で使えます。Mac は Pro 版限定です。ローカルのみ
get_runtime_threat_statusXProtectのマルウェア検知による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本、英語固定)も追加しました。

ローカルLLMによる実地検証で見つかった誤帰属と、その修正

2026年9月、本MCPサーバーを使ってローカルLLM(Qwen 3.8 27B)にインシデント調査をさせる実地演習(fire drill)を行った際、モデルが get_port_anomaly_incidents の返すデータを誤読し、無関係な過去のポートブロックを、現在進行中のインシデントの物語に取り込んでしまう事例を発見した。原因は、タイムスタンプを持たない「現在ブロック中のポート」のスナップショットと、タイムスタンプ付きの実際のインシデント履歴が、同じレスポンスの中で区別なく返されていたこと。修正として、どちらが時系列データでどちらがスナップショットかを明示する注釈フィールドを追加した。修正後、同じシナリオを実際に同モデルで再実行したところ、モデルは注釈の文言を引用しながら、過去のポートを現在のインシデントに帰属させることを明示的に拒否した。回帰テストと実機LLMでの再現確認、両方で修正の有効性を確認済み。

要点:ソースを公開

このサーバーと、そこから使う検知ロジック(ARP 監視・ポートスキャン・ポート診断・18 項目ヘルスチェック・ リンク安全性診断)のソースは、github.com/lafine1211/roamswitch-mcp で公開しています(MIT、 出荷コードのミラー、リリースごとにタグ)。「読み取り専用であること」「LLM に何を渡しているか」 「外部送信がないこと」をコードで直接確認できます。特権ヘルパー・pf 制御・実際に遮断する各ガード・ ライセンスは含みません(本体リポジトリのまま)。

テストも同梱しています(本体スイートのミラー+敵対的入力テスト+ミューテーションファジング、 swift test で実行、CI で検証)。ファジングで JSONSerialization の未対策の クラッシュ(深くネストした JSON オブジェクトでのスタックオーバーフロー)を 1 件発見し、 パーサ前段のネスト深度チェックで修正しました(SECURITY_TESTING.md に記録)。

§9ライセンス認証の暗号設計

トークン

  • Ed25519(Curve25519 署名)を使っています。公開鍵はアプリに埋め込んでいます(LicenseVerifier.embeddedPublicKeyBase64)。対応する秘密鍵は、ライセンスバックエンド(Firebase Functions の環境変数)にのみ存在し、リポジトリには含まれていません。
  • 署名の対象は正準 JSON です。JSONEncoder の .sortedKeys と .withoutEscapingSlashes で LicensePayload(ライセンスキー、tier、デバイスハッシュ、発行時刻、有効期限、台数)をエンコードした、正確なバイト列に対して署名と検証を行います。
  • fail-closed で設計しています。埋め込み鍵が欠落・不正であったり、署名や正準 JSON の生成に失敗したりした場合は、「検証成功」にはならず、invalidSignature を返します。

デバイスバインド

device_hash = SHA-256( "RoamSwitch-LifetimeSalt-v1" : lowercase(IOPlatformUUID) )

生のハードウェア UUID は、サーバーに送信しません。IOKit が UUID を返さないまれなケースでは、 UserDefaults に保持する乱数 UUID にフォールバックします。検証時、トークン内の device_hash と現在のデバイスハッシュが一致しない場合は、deviceMismatch と なります。

オフラインで動く

要点:サーバーが無くても動く

起動時の validateSavedLicense() が行うのは、Keychain のトークンを読み込み、埋め込みの 公開鍵で手元で検証することだけです。ネットワークには接続しません。ライセンスサーバーが将来的に停止した 場合でも、すでに認証済みの Mac では、Pro 機能はそのまま使えます。サーバーと通信するのは、新規の アクティベーションと、明示的な解除のときだけです。解除のサーバーへの通知はベストエフォートで、失敗した 場合でも、手元の解除は必ず完了します。

既定は買い切り(Lifetime)で、is_lifetime でない場合にのみ expires_at を 確認します。台数は tier で表現し、個人 Pro が 2 台、Team が 5 台です。

§10配布とアップデート

署名・公証(scripts/release.sh)

  1. xcodebuild archive を実行します(Release、手動署名、Developer ID Application)。
  2. -exportArchive で method: developer-id としてエクスポートします。
  3. notarytool submit --wait のあと、.app に stapler staple します。
  4. spctl -a -t exec -vv で検証します。
  5. staple のあとに再度 zip し、Sparkle 用の配信物を作ります(公証チケットを同梱し、オフラインでも Gatekeeper の警告なしで動くようにするためです)。
  6. DMG を作成し、DMG も公証・staple したうえで、stapler validate で検証します。

アップデート(Sparkle 2.9.6)

Info.plist のアップデート設定
キー値
SUFeedURLhttps://lafine.net/updates/appcast.xml
SUPublicEDKeyCNxzwijMzMCJzliId76Yl88S/9np6t/xg/zQ9YbYzHs=
SUEnableAutomaticCheckstrue
SUScheduledCheckInterval86400

配信物は、appcast に記載された EdDSA 署名を、アプリに埋め込んだ SUPublicEDKey で検証してから適用します。署名用の秘密鍵は、ビルド環境にしかありません。appcast は HTTPS で配信します。 差分(delta)アップデートも、同じように署名を検証します。

要点:appcast は公開前提

appcast.xml の URL が公開されていること自体は、弱点ではありません。中身は、バージョン 番号、リリースノート、ダウンロード URL、ファイルサイズ、各ビルドの EdDSA 署名だけで、秘密は含まれて いません。信頼のよりどころは、「通信の途中で appcast が本物かどうか」ではなく、アプリに焼き込まれた 公開鍵で配信物の署名を検証すること、にあります。appcast を完全に差し替えられる攻撃者(MITM、DNS 乗っ取り、Web ホストの侵害)であっても、署名鍵がないかぎり、不正なアップデートを通すことはできません。 Gatekeeper(Developer ID と公証)が、2 つ目の関門になります。

残るリスクは 2 つあります。1 つは、ホストの停止や壊れた appcast による更新の不達です(不正な インストールは起きず、単に更新されないだけです)。もう 1 つは、セキュリティ更新を意図的に握りつぶす freeze 攻撃です。Sparkle 2 系は、バージョンの順序を確認してダウングレードやリプレイを弾きますが、 freeze への完全な対策には、有効期限つきの専用アップデートサーバーが必要です。ここは今後の検討課題と しています。

§11脅威モデルと、やらないこと

RoamSwitch が相手にするもの

  • 同じ LAN にいる攻撃者や、乗っ取られた IoT 機器からの探査・攻撃。ステルス化、公開ポートの監査、外部からの隔離で対応します(実測検証: RESULTS-DEFENSE-2026-08-30 および包括的ペネトレーションテストレポート RESULTS-PENTEST-2026-08-31 にて、ARPスプーフィング・未知ポート・ランサムウェア暗号化・ダウンロード検疫の全シナリオに対する自律遮断を実証済み)。
  • 信頼していないネットワークでの露出。共有サービスと AirDrop を自動的に停止します。
  • 中間者攻撃(ARP/NDP スプーフィング)。1.7.6 以降は多層で対応します:(1)VPN トンネル + キルスイッチ(§5、本命。L2 の完全性に依存しない)、(2)未信頼ネットワークでのゲートウェイ ARP/NDP 予防固定(§5)、(3)スプーフィング検知と緊急 Air-Gap(事後)。VPN トンネルとゲートウェイの固定は Pro・既定オフです。スプーフィング検知は既定オンで、緊急 Air-Gap は Pro の有効化時に既定でオンです。
  • 認証なしで 0.0.0.0 に晒された開発サーバーやデータベース(Redis、MongoDB、Elasticsearch など)、およびローカルAI/LLM推論サーバー(Ollama、LM Studio、vLLM、Gradio など)を見つけ、外部から遮断します。
  • ランサムウェアのような不正な暗号化の動きを早い段階で捉え、通信をすべて止めます(シグネチャには依存しません)。
  • 未承認のキーボードや改造USBケーブル(Rubber Ducky / O.MG Cable等)からの自動コマンド注入を水際で遮断するBadUSB物理キーボード承認ガード(USBKeyboardGuard、CGEventTap + IOKit)。
  • 許可していない USB ストレージの承認プロンプト(未知のドライブは即取り出しではなく読み取り専用で保留)と、接続されたストレージの自動 ClamAV スキャン(任意)。
  • クリップボード内の機密 API キー(OpenAI、Anthropic、GitHub、AWS 等)を完全ローカルで検知し、Web フォームや AI チャットへの誤貼り付けを未然に防止。
  • HuggingFace や Web からダウンロードされた危険な Pickle 形式 AI モデルファイル(任意のコード実行リスク)の検知・警告。
  • ダウンロードファイル内の業界標準 EICAR テスト署名と、公開されている定番リバースシェル一行コマンド(StaticSignatureScanner、1.8.7〜、ClamAV 未導入でも動作)。
  • 生のスクリプトインタプリタを直接呼び出す、新規の LaunchAgent/LaunchDaemon 自動起動登録(PersistenceMonitorGuard、1.8.7〜)。
  • 偽の警告画面がユーザー自身に Terminal でコマンドを実行させる「ClickFix」型攻撃を、既知の不審コマンドパターンのシェル履歴監視で検知し、緊急 Air-Gap を発動(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 として扱うことを、あらかじめ明記します。実行そのものを止められない代わりに、post-hoc(事後)の検知で実用上十分な範囲(静的シグネチャ検知、自動起動監視、ClickFix 対策)を entitlement なしで実装しています。
  • 保証ではありません。多層防御のうちの 1 層であって、「ランサムウェアを完全に防ぐ」ものではありません。マーケティングの文言も、この前提でレビューしています。
  • すでに乗っ取られた root やカーネルは守れません。攻撃者がすでに root を取得している場合、ヘルパーの pf ルールも外せてしまいます。
  • L2 の完全性そのものを回復することはしません。ARP/NDP 予防固定は「最初に観測した MAC を信頼する(TOFU)」方式で、接続前から攻撃者が居座っている場合は偽の MAC を固定し得ます。この前提を置きたくない場合の答えが VPN トンネル(§5)で、こちらは L2 が汚染されていても中身は暗号化されキルスイッチで平文が漏れません。企業ネットワークの DHCP スヌーピングや Dynamic ARP Inspection の代わりにはなりません。
  • VPN サーバーは提供しません。トンネル機能はユーザーが用意した WireGuard 設定を使うもので、RoamSwitch が VPN 事業者になることはありません。

RoamSwitch を入れることで増える攻撃面

導入で増える攻撃面と、その抑え方
攻撃面抑え方
root で動く LaunchDaemon と、その mach service(com.tetsuharu.RoamSwitch.Helper) 操作面を HelperProtocol に固定しています(§3 の表)。任意コマンドを実行する口はありません。接続はコード署名要件で認可し、audit_token を使います。
RoamSwitch.app 本体が侵害された場合、ヘルパーの全操作が攻撃者に渡ります Hardened Runtime を有効にし、アプリ側に不要な権限を持たせません。ネットワーク送信は、§7 の表の経路(と表の下に記したユーザー操作・既定オフの機能)に限定しています。ネットワーク送信のエンタイトルメントは宣言していませんが、サンドボックス外のため、これ自体は通信を禁止するものではありません(実測は §7 の末尾)。今後、第三者レビューの対象にする予定です。
ヘルパーが起動するシステムバイナリのパス /sbin/pfctl などを絶対パスで直接指定し、PATH には依存しません。引数もハードコードしています(ポート番号・DNS 文字列・ARP の IP/MAC・VPN エンドポイント IP を除く。いずれも形式を検証します)。
リンク保護が /etc/hosts を書き換える(setLinkGuardSinkhole) 書き込みは区切り付きの管理セクション内に限定し、セクション外の行はそのまま保持します。ドメインは正規化・検証し、IP や不正文字列は捨て、6 万件でキャップ、一時ファイル + アトミック置換で書きます。適用するのは「脅威フィード掲載」または「ブランド名のホモグラフ偽装」の明確なケースだけで、判定エンジンはローカル完結です。ブロックリスト自体は署名検証済みのフィード(フィード専用鍵)に由来します。
リンク保護の脅威フィード取得(受信専用の日次 GET) クエリ文字列と端末ごとの識別子がない静的ファイル取得のみ(User-Agent は標準のもの)。マニフェスト・フィード本体とも Ed25519 で検証し、検証に失敗した取得物は破棄します(無署名データにフォールバックしません)。「自動更新: オフ」でこの経路自体が消えます。
ARP/NDP 予防固定(lockGatewayARP) 固定するのは「ゲートウェイ・IPv6 ルーター・同一リンク上 DNS」の近隣キャッシュ エントリだけ。渡された IP・MAC は形式を検証し、要求集合との reconcile 方式(勝手にエントリを増やさない)。未信頼ネットワークでのみ動作し、Pro・既定オフ。TOFU の限界は「やらないと決めていること」に明記。
VPN トンネル(wireGuardImport/Up/…、Homebrew wireguard-tools) .conf はヘルパーが 0600 で保存。エンドポイント IP はアプリ側で解決してヘルパーに渡す(ヘルパーに任意ホスト名の名前解決をさせない)。トンネルの宛先・内容はユーザー管理下。wireguard-tools 未導入なら機能は無効。Pro・既定オフ。キルスイッチは PFRulesetCoordinator(§4)を経由。
MCP サーバーが LLM にシステム状態を渡すこと(confused deputy) 読み取り専用で、書き込み API は実装していません。URL 診断はオフラインです。設定ドメインは read-only で参照します。
アップデート経路の乗っ取り 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

# 同梱ヘルパー / MCP サーバーの署名
codesign -dvvv /Applications/RoamSwitch.app/Contents/MacOS/RoamSwitchHelper
codesign -dvvv /Applications/RoamSwitch.app/Contents/MacOS/RoamSwitchMCPServer

Entitlements(ネットワーク送信のエンタイトルメントを宣言していないことの確認。サンドボックス外のため、これ自体は通信を禁止するものではありません)

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。tcpdump + nettop/lsof/pktap + LuLu で外向き通信を実測し、 PASS/FAIL を判定します。実施例:RESULTS-2026-08-29(PASS)。

防御機構・ペネトレーション実測検証(5つの防御境界)

# XPC 認可境界、pf Air-Gap 優先度、ポート露出検知、MCP Read-Only、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、および最新検証レポート RESULTS-PENTEST-2026-08-31(全項目 PASS / 脆弱性修正完了)。

DNS 脅威保護(Quad9 悪質ドメイン・C2 遮断)の動作確認

# Quad9 公式の脅威ブロック検証用テストドメインを問い合わせ
nslookup test.dns9.quad9.net
# → ** server can't find test.dns9.quad9.net: NXDOMAIN となり名前解決が遮断されること

ダウンロード保護(ClamAV リアルタイム検知)の動作確認

# 無害なアンチウイルス標準テスト文字列(EICAR)を Downloads に作成
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 不要)の動作確認

## 公開の攻撃手法解説に載っている定番のリバースシェル一行コマンド(無害、実行はしない)
echo 'bash -i >& /dev/tcp/127.0.0.1/4444 0>&1' > ~/Downloads/rs_test.sh
## → ClamAV 未導入でも「危険なダウンロードファイルを隔離しました」の通知とともに
##   Quarantine フォルダへ即時隔離されること(ClamAV のシグネチャDBに依存しない)

新規自動起動監視(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

リンク保護(フィッシング接続の遮断)の動作確認

# 管理セクションが書かれているか(Pro + 既定モードなら BEGIN/END の 2 行)
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'

# 実際に落ちているか(セクション内の 1 件で確認。無害)
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

# ヘルパーの状態ディレクトリ
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 にテスト内容と発見済みの問題

デバイス識別子

# トークンにバインドされる素の値(送信されるのは salted SHA-256 のみ)
ioreg -d2 -c IOPlatformExpertDevice | awk -F'"' '/IOPlatformUUID/{print $4}'
RoamSwitch アーキテクチャ/セキュリティホワイトペーパー v1.6 · 対象 1.11.14 (build 161) · Lafine Systems Design · 2026-09-23
誤り・不足の指摘は lafine.net/contact.html へお願いします。