iOS VPNアプリがトンネル稼働中なのに切断と表示する理由
iOSアプリはトンネルを制御しますが所有はしておらず、マネージャの状態はキャッシュされています。まず環境設定を読み込み、その後に状態とインターフェースのカウンタを読んでください。
アプリは切断と言い、メニューバーは接続と言い、トラフィックは明らかに流れている。このずれは再試行で直せる競合状態ではない。アプリとトンネルは別プロセスであり、コードが読むオブジェクトは、システムが保存する構成のキャッシュされたビューにすぎない。Apple の Personal VPN の表面は制御 API であり、データ経路ではない。奇妙な挙動の多くは、この一点から説明できる。各呼び出しで実際に何が起きるかを見ていこう。
マネージャオブジェクトはキャッシュされたビューであり、トンネルではない
NETunnelProviderManager はシステムが所有する環境設定ストアから埋められるため、loadFromPreferences が完了するまでそのプロパティに意味はない。その前に connection.status を読むと既定値が返る。だから再起動後の最初の状態更新は、トンネルが動いていても切断と報告する。まず読み込み、その同じ完了ハンドラで状態とプロトコル構成を読むこと。以降の変化はポーリングではなく NEVPNStatusDidChange で監視し、通知は On Demand が開始した接続でも発火する点に注意しよう。その場合、何かを返すアプリの呼び出しは存在しない。プロトコル設定も同じで、読み込んだ構成から読まれるのであって、アプリが渡した値からではない。
トンネル自体は Network Extension のプロバイダプロセスに住んでいるため、アプリは接続を失わずに自分の状態を失うことができる。UI が示すものはすべてマネージャから再構築すべきで、前回のセッションから覚えていてはいけない。
起動順序は保存、読み込み、その後に起動
プロバイダベースの接続には順序どおりの三つの手順が要る。プロバイダ構成を書いて saveToPreferences を呼び、保存された構成が手元のものになるようマネージャを読み込み、その後に NETunnelProviderSession.startVPNTunnel() で起動する。保存を省けば起動には起動するものがなく、保存の前に読み込めばマネージャはまだ古い構成を映している。
startVPNTunnel が返すブール値は、要求が受け付けられたことだけを意味する。プロバイダ層の失敗は後から切断への状態変化として届き、それは NEVPNStatusDidChange 通知で観測する。通知ハンドラは loadFromPreferences の後に connection.status を読み直す必要がある。キャッシュされた値がまだ古い可能性があるからだ。要求層の失敗は、例外を投げる形式の呼び出しから、あるいは startVPNTunnelAndReturnError: のエラー出力パラメータから即座に返る。戻り値を結果とみなすコードは、決して確立しなかった接続を報告することになる。もう一つの静かな失敗は、インストール済みの拡張機能と一致しないプロバイダのバンドル識別子だ。要求は拒否され、メッセージのどこにも識別子を指す記述はない。識別子が誤っていれば、プロバイダのログ行が一行も出る前に、要求はたいてい即座に失敗する。
起動後に環境設定を読み直すことが、状態画面を正しくする。システムが構成を変えても、マネージャが新しい構成を押し付けてくることはない。
信頼の素材はプロファイルで届き、アプリのコードから来ることはない
アプリにはルート証明書を iOS の信頼ストアに追加する支持された方法がない。したがって Personal VPN 構成は、端末がすでに信頼している素材しか参照できない。インストール済みプロファイル、デバイス管理、あるいはシステムが別の理由で信頼する CA が築いた連鎖だ。初回起動時に自前の CA を作り、システムがそれを受け入れると期待するアプリは、プロバイダ内部の検証で失敗する。そこではアプリの UI から理由が見えない。
その境界を、あるがままの設計制約として扱い、信頼のアンカーを登録プロファイルとともに配布すること。VPN 認証は信頼の判断から切り離しておくべきだ。失敗する場所が違い、調べる人も違うからだ。
カウンタはインターフェースから来るのであり、VPN API からではない
マネージャが公開するのは状態と構成だけで、トンネルごとのバイト数を数えるものはない。使用量の数値が要るなら、getifaddrs() を呼び、該当するインターフェースの if_data を読む。Wi-Fi なら en0、セルラーなら pdp_ip インターフェース、トンネル自体なら utun デバイスだ。utun のカウンタは内側のトラフィックを表し、プロトコルのオーバーヘッドを含む。
これらの数値は推定であり、アプリごとの会計ではない。インターフェースが作り直されるとリセットされる。UI でもそのように表示すること。さもないと、誰かが顧客への請求に使ってしまう。アプリごとの数値が必要なら、まったく別の仕組みが要る。これらのフィールドを上手に読むことではない。
後片付けは stopVPNTunnel の担当であり、終了処理ではない
applicationWillTerminate が与えるのは、システムがプロセスを殺すまでの数秒であり、接続を畳むのに頼れる場所ではない。停止は stopVPNTunnel() に送るか、プロバイダの停止コールバックで処理し、起動に使ったマネージャまたは構成の識別子を保持しておくこと。そうすれば停止は、端末上のすべての構成ではなく、その接続だけを対象にする。
その識別子は、属するプロファイルのそばに保存すること。マネージャオブジェクトは、ユーザーがどの構成を選んだかを記録しない。そして、On Demand が立ち上げた接続にはアプリの呼び出しが一切ないことを忘れないように。だから状態画面は、何かを描く前に環境設定を読み込まねばならない。