サンドボックス化された Mac アプリでアクティブな VPN を検出する
この問いに公開 API はなく、インターフェース名を使う技はヒューリスティックにすぎません。アプリが本当に何に依存すべきかを決める 6 つのチェック。
トンネルが有効なときに別の振る舞いをする画面が一つ必要だが、それを教えてくれる呼び出しは存在しない。これがリリース上の制約だ。macOS では任意の VPN が接続されているかを報告する公開フレームワークはなく、iOS では状態は構成をインストールしたアプリに属する。以下のチェックはその限界から出荷できるものまでをたどるもので、どれか一つで探索を早く終えられる。
チェック 1: そもそもこの問いに答えられるか
サンドボックスはどの API よりも先に問いを決着させる。第三者のプロセスは他社のエクステンションを列挙できず、そのエクステンションの構成も読めない。したがって問い合わせられる対応オブジェクトは存在しない。
どの回避策も同じ壁に突き当たる。インターフェース一覧や scutil --nc list、システム構成の直接読み取りは、サンドボックスが保留する権限を必要とするか、Apple が決して約束していない挙動に依存する。計画がそのどれかを必要とするなら、問題は計画のほうだ。文書化されていない細部に乗った機能は、納品日付きのサポートチケットである。
チェック 2: アプリ自身がトンネルを管理しているか
管理しているなら、自分の構成については対応する答えがある。設定を読み込み、エラーを処理し、そのうえで状態を読み、ポーリングではなく変更を購読する。
vpnManager.loadFromPreferences { error in
// handle a load failure before reading status
}
NotificationCenter.default.addObserver(
forName: .NEVPNStatusDidChange,
object: nil,
queue: nil
) { note in
let status = (note.object as? NEVPNConnection)?.status
}これが返すものを正確に捉えよう。インストールしたトンネルの状態だ。別のクライアントを使うユーザーは切断済みとして表示されるが、それはコードの不具合ではない。答えの上に築く前に、機能が本当に問うているのは 二つのうちどちらかを決めること。
チェック 3: ヒューリスティックは実際に何を教えるか
広くコピーされている手法は、システムのプロキシ辞書にスコープ付きエントリを問い合わせ、そのキーを読む。キーはたまたまインターフェース名になっている。
let settings = CFNetworkCopySystemProxySettings()?.takeRetainedValue() as NSDictionary?
let scoped = settings?["__SCOPED__"] as? [String: Any]
let names = scoped?.keys ?? []tap、tun、ppp、ipsec または utun で始まるキーは、トンネルにスコープされたインターフェースが存在することを示す。名前全体を照合すると即座に失敗する。iOS 12 には utun1 と utun2 が搭載され、一部のプロファイルは ipsec4、ppp0、utun0 として現れるからだ。代わりに接頭辞を比較しよう。
それでも、名前は所有者を特定しない。システム機能や他社のエクステンションも utun インターフェースを作るし、あなたのトラフィックを一切運ばずに存在するものもある。周囲にトンネルらしきものがあるというヒントとして読むべきで、それ以上のものではない。ここでの誤検知は 一つの機能だけを制御するなら安く、ログイン画面を制御するなら高くつく。
チェック 4: 代わりに宛先を試せるか
アプリが内部サービスに到達できるかだけを知りたいなら、そのサービスを試せばよい。タイムアウト付きの短いプローブは、機能が本当に依存する問いに答えるし、ユーザーがクライアントを変えてもトンネルの形が変わっても機能し続ける。
到達性は自分の経路についての観測だ。インターフェース名は経路に隣接する何かについての推測であり、だからヒューリスティックはバグ報告で議論を生み、プローブは証拠を生む。ユーザーが VPN は有効だと言い張るのにプローブが失敗するなら、デバッグすべき具体的な経路が手に入る。名前解決、ルート、サービスの権限だ。
チェック 5: プラットフォームに強制させられるか
要件が「トンネルから何も漏れない」という保証なら、その置き場所はプロファイルであり、検出器ではない。iOS 14 以降のフルトンネル構成で includeAllNetworks を有効にすると、アプリに推測させるのではなくプラットフォームの挙動が変わる。このフラグは NEVPNProtocol のリファレンス に文書化されている。
これは別種の答えだ。観測ではなく強制である。しかも自分が管理するプロファイルにしか適用できないので、他人のトンネルを眺める消費者向けアプリの役には立たない。
チェック 6: 失敗を安全かつ観測可能にする
トンネルを必要とする機能だけを切り、アプリの残りは使えるままにする。不確かなときにインターフェース全体を隠す検出器は、想定外の名前を使うトンネルの利用者に障害を作り出す。代わりに保守的な挙動へ戻そう。ローカル機能は動き続け、本当にトンネルを必要とするリモート呼び出しだけが無効になる。この選択はプロダクトの判断であり、ヒューリスティックの隣に書き留める価値がある。
観測した内容を記録する。インターフェース名、プラットフォームのバージョン、プローブ一回の結果だ。それが次の報告を議論からチェックに変える。そのうえで、対応する宣言があるならそちらを優先しよう。管理された構成は意図を明示的に運べるし、名前を調べる理由の大半を取り除ける。
チェック 1 から 5 で対応する答えが得られなかったなら、プローブを出荷し、結果を正直にラベル付けしよう。文書化されたベストエフォートのシグナルは、次のリリースで壊れる非公開 API より良いプロダクト判断である。