Wi-Fiからセルラーへの切り替え後にキルスイッチで壊れるもの
キルスイッチは、ネットワークの変化が置き換えるインターフェース名に対して書かれる。信頼する前に外側の経路、アドレスファミリー、リゾルバーを確認すること。
ある瞬間ラップトップはオフィスのWi-Fiにあり、次の瞬間にはスマートフォンにつながれ、ウィンドウを切り替え終わる前にトンネルの表示はまた緑になっている。緑のランプが示すのはトンネルが再接続したことだけで、その隙間にルーティングとブロックのポリシーが何をしたかは別の問いだ。そのポリシーは最後にインストールされたときにコンパイルされた一致条件の集合で、引き継ぎがたった今削除したインターフェース、アドレス、アドレスファミリーを名指ししている。だから同じ切り替えが、見た目は同じ三つの結果を生む。ブロックが続くか、ポリシーが自分自身の復旧を妨げるか、何も覆っていない経路が開くかだ。
画面の上ではその三つの状態を区別できない。だからインジケーターが何色かを問うのではなく、ルールがどの識別子に固定されていたかを問うべきだ。Linuxではルールがインターフェース名やそこから埋めたipsetを指定できる。AndroidではVpnServiceに、setUnderlyingNetworksを通じてどの下位ネットワークに従うかを伝えられる。どの結び付きも、まさに置き換えられようとしているリンクを指しており、それぞれを後で評価し直さなければならない。
引き継ぎが実際に書き換えるもの
古いリンクが消えると、カーネルはそのアドレスを外し、そこを通っていた経路を引き揚げ、残ったものから送信元アドレスの選択を組み直す。デフォルト経路は新しいゲートウェイへ移り、古いルールが参照していたインターフェースインデックスは消えるか、別のデバイス用に再利用される。リンクごとのリゾルバーも更新されるが、経路と同じ瞬間ではない。これらはすべてVPNクライアントの下で起きるので、自分のトンネルの状態だけを見ているクライアントには何も見えない。
厄介なのは、ルールがその背後の意図ではなく特定の識別子を符号化していることだ。トンネルのインターフェースから出ないものすべてを落とすルールは、インターフェース名を符号化している。エンドポイントを許可するルールは、新しいネットワークが別の場所へ流すかもしれないアドレスを符号化している。識別子が消えると、ルールは目に見えて誤りになるわけではない。何とも一致しなくなるか、誤ったものと一致し始めるだけだ。
穴を開けずに外側のトンネルを組み直す
外側のトランスポートは古い送信元アドレスに束縛されていたので、新しいアドレスから新しいソケットを開く必要がある。エンドポイントが文字どおりのアドレスではなくホスト名なら、ブロックが効いている間に名前解決が成功しなければならない。これが順序の罠だ。エンドポイント以外のすべてを落とすほど厳しいポリシーは、それを見つけるのに必要な名前解決も落としてしまう。ブロックを上げる前に解決するか、まだ生きているトンネルを通すのでなければ。どちらの順序でも動きうるが、クライアントがどちらの順序にいるかを知っている場合に限る。
ここでクライアントの違いが診断では効いてくる。先に解決してからルールを入れるものは、引き継ぎのたびに目に見える断が入り、その後ひとりでに回復する。ブロックを立てたまま、生き残ったトンネルに名前解決を運ばせるものは、トンネルがリンクとともに死ぬと完全に失敗する。接続性を取り戻すために広い直接のデフォルト経路を足すと、テストは必ず通り、防いでいた漏れも必ず残る。
古いゲートウェイを指したままの経路も、別の原因で同じ行き詰まりの回復を生む。だからクライアントを責める前にテーブルを読むこと。下のコマンドはカーネルが実際に使うインターフェースと送信元アドレスを示すので、古い経路がまだ入っているかが一行で決まる。
ip route get 1.1.1.1
ip -6 route get 2606:4700:4700::1111新しいリンクがIPv6を運び、ルールが運ばないとき
セルラーやゲストのネットワークは、オフィスの有線LANが決して出さなかったグローバルなIPv6プレフィックスを配ることがよくある。IPv4のルールとしてだけ書かれたポリシー、つまり手作りのiptablesやnftablesのレシピの多くが取る既定の形は、それを何も覆っていない。トンネルはつながったままで、IPv6に到達できるアプリはその外へ出て行き、トンネルは本当に上がっているのでインジケーターは違いを示せない。裏返しの事例も同じくらいよくある。IPv6専用のルール一式は、v6を出さないネットワークでは何もしない。
引き継ぎの両側で、リンクが提供するものとポリシーが挙げているものを比べる。次のコマンドはクライアントが使っているのと同じ事実を出力する。
ip -4 route show default
ip -6 route show default
sudo nft list ruleset | grep -c 'ip6 '二番目のコマンドがデフォルト経路を出し、三番目がゼロを返すなら、新しいリンクはどんなルールにも見張られないままマシンをIPv6で外へ出せる。切り替えの後でもう一度三つとも実行すること。興味深い数字は二回の実行の差だからだ。二番目が何も出さなければ逆の事例が当てはまり、そこでのv6寄りのポリシーは飾りにすぎない。
リゾルバーはセッションの途中で持ち主を変える
現代のシステムではリゾルバーはリンクに結び付いているので、新しいリンクは新しいリゾルバーの組と、おそらく異なるsplit-DNSの見え方とともにやって来る。systemd-resolvedはリンクごとにスコープを持ち、経路に応じてそれらを選ぶ。macOSは動的ストアからスコープ付きのリゾルバーを読み、インターフェースの順序を守る。自前のリゾルバーを入れるクライアントはリンクのものと競合し、誰も見ていない優先順位のルールが勝者を決める。そのとき名前解決は壊れうるのに、パケットフィルタリングは書かれたとおりに動いている。
設定されているリゾルバーではなく、実際に有効なリゾルバーを読むこと。systemdのホストではstatusサブコマンドが、各スコープがどのリンクから来たか、private-DNSの名前が効いているかを示す。
resolvectl status --no-pager
resolvectl query intranet.example.com問い合わせがトンネルではなく物理リンクから出ていくなら、クライアントは優先順位の競争に負けており、ファイアウォールの調整では名前は直らない。引き継ぎの両側で問い合わせの経路を比べ、どちらが動いたかを見る。新しいネットワークが経路を持てないプライベートアドレスに解決する名前は第三の事例で、エラーではなくタイムアウトとして現れる。
引き継ぎを練習し、記録を残す
待つのではなく、意図的に変化を再現する。最初のリンクでトンネルを上げ、長く続く転送と繰り返しの名前解決を始め、それからリンクを切り替えてどちらが生き残るかを見る。古いリンクが消えてからトンネルが接続したと報告するまでの間隔を記録すること。その窓でポリシーが持ちこたえたかどうかが決まる。同じ手順を逆向きにも実行し、結果が一方向にならないようにする。
例外には独自の確認が要る。古いネットワークで正当だった迂回は、新しいネットワークで自動的に正当になるわけではない。自宅LANのプリンター、NAS、キャプティブポータルはセルラーには対応物がなく、そこへ通していた例外は対応物のないルールになる。
各方向について、インターフェース名、提供されるファミリー、有効なリゾルバー、断の長さを書き留める。再接続はトランスポートが回復したことの証明であって、隙間の間ブロックが持ちこたえたことの証明ではなく、切符に書くべき別々の主張だ。その記録があれば、次の引き継ぎは記憶ではなく既知の状態と比べられる。