トンネル経由のデフォルトルートを追加すると、トンネル自身のパケットもそれに従います。するとテーブルには、自らを打ち消す規則が含まれることになります。サーバーへの暗号化接続が、これから確立しようとしている接続の内側を通ろうとします。

このループを防ぐ仕組みは二つあります。一つはルーティングテーブルに見え、もう一つはアプリケーションの内側に住んでいます。

内部トラフィックと外部トラフィックは別の判断です

アプリケーションのパケットは仮想インターフェースに入り、カプセル化されます。結果は、サーバーの公開アドレス宛ての新しいパケットです。そのパケットには、そのどの部分もトンネルに入ってはならないため、物理ネットワーク上のルートが必要です。

どちらもルーティングの判断であり、トンネルに属するのは最初の一つだけです。クライアントがサーバーも含むデフォルトルートをインストールすると、外側のパケットも捕捉されます。

症状は即座かつ完全です。ハンドシェイクが完了した後に何も流れないか、ルートがインストールされた瞬間に接続が切れます。どちらの場合も keepalive はもう外に出られません。

選択肢その一:エンドポイントへのホストルート

直接的な方法は、元のゲートウェイ経由でサーバーのアドレスへのルートを追加します。たった一行です。テーブルを読む人なら誰でも見えます。変更記録でも説明しやすいです。

その弱点は、下位の経路が変わったときに現れます。保存されたルートは、古いネットワークのゲートウェイとインターフェースを名指しします。Wi-Fi を切り替えた後、そのゲートウェイが存在しないことがあり、ルートは取り残されて、正しく見えながら何も行いません。

# before the tunnel: which path reaches the endpoint?
ip route get 198.51.100.7

# pin it through the current gateway
ip route add 198.51.100.7/32 via 192.168.1.1 dev wlan0

# after the tunnel is up, confirm the pin survived
ip route get 198.51.100.7

ホスト名はこれを難しくします。エンドポイントが複数のアドレスに解決される名前なら、除外は実際に使われているアドレスを覆う必要があります。後からの名前解決は、ルートが決して覆わなかったものを返すことがあります。

選択肢その二:ソケットを保護する

もう一つの方法はルーティングテーブルに一切触れません。クライアントはトンネルを運ぶソケットをマークまたは保護するので、オペレーティングシステムはポリシーに従ってそのソケットのトラフィックをトンネルの外へ送ります。Android はこの仕組みを VPN アプリケーションに直接公開しています。デスクトップクライアントはファイアウォールマークやデバイスへのバインド呼び出しで同等のものを実装しています。

利点は、例外が保存されたゲートウェイではなく接続に従うことです。ネットワークが変わると下位の経路が書き換えられ、保護されたソケットはアドレスに固定されていなかったため、それとともに移動します。ルートテーブルはきれいなままです。

代償は可視性です。ルーティングテーブルには例外を示すものが何もないため、ホストルートを探すレビュアーは何も見つけられず、設計が安全でないと結論づけるかもしれません。保護はクライアントのコードと、そのコードがインストールするポリシールールの中に住んでいます。

どちらを選ぶか

クライアントが対応しているなら、固定されたルートを壊すネットワーク変更に耐えるので、ソケットレベルの仕組みを選んでください。例外を他のツールから見えるようにする必要がある場合、またはクライアントがより良いものを提供しない場合は、ホストルートを選んでください。

どちらの仕組みも、サーバーのアドレスがそもそも到達可能でなければならないという必要性を取り除きません。プライベートゲートウェイには公開の待ち受けアドレスがあり、トンネルの外に残らなければならないのはそのアドレスです。その背後の内部ネットワークは、トンネルが存在して初めて到達可能になります。

どちらを選んでも、トンネルを所有するコンポーネントのそばに置いてください。手で追加した場当たり的なホストルートは次のネットワークまで機能し、その後は明日の謎の障害になります。

関連記事