VPNでプライベートサブネットを一つだけ通す方法
クライアント側のルート、サーバーがプッシュするルート、ネゴシエーションされるセレクターのどれを選ぶかを決め、フルトンネルにせずプライベートサブネットを一つだけ通します。
一つのプライベートサブネットだけをトンネルに通し、それ以外は通常の経路を使うのは小さな要望だが、間違え方は非常に多い。結果を決めるのは split tunneling というラベルのチェックボックスではなく、判断をどこで強制するかだ。クライアント自身のルートテーブルか、サーバーがプッシュするルートか、両端がネゴシエーションするポリシーか、階層を選び間違えると、望まないフルトンネルになるか、相手側が受け入れないトンネルへ向かうルートになる。私は設定を開く前に三つの条件で判定し、そのうえで以下の仕組みのどれか一つを選ぶ。
設計を決める三つの条件
まず、ネットワークの管理者から正確なプレフィックスとマスクをもらう必要がある。10.x ネットワークはプレフィックスではなく、10.20.0.0/16 がプレフィックスだ。自宅の LAN、Docker のブリッジ、あるいは自分が動かしている別の VPN と重複しないことを確認してほしい、重複するとルートが曖昧になり、カーネルは意図しないエントリを選ぶからだ。
次に、戻りトラフィックに誰が答えるか。パケットをトンネルに向けても、それは VPN サーバーまで届くだけだ。サブネットが別のルーターの背後にあり、そのルーターがクライアントプールへの戻りルートを持っていないなら、サーバーが NAT で隠すか、そのルーターがプールを学習する必要がある。クライアント側のルートでは戻り経路の欠落は直せない。
三つ目は、強制する場所だ。ルートを入れて OS に任せるクライアントもあれば、ポリシーをネゴシエーションするクライアントもある。ゲートウェイが受け入れないポリシーではトンネル自体が張られず、パケットは明確なエラーもなく捨てられる。何かを書く前に、自分がどちらなのかを確定させる。
WireGuard: AllowedIPs がルートテーブル
クライアント側では、AllowedIPs がトンネルに入ってよい宛先を列挙し、wg-quick が各エントリを通常のルートに変換する。つまりこの作業のクライアント側は一行で済む。
[Peer]
PublicKey = ...
Endpoint = vpn.example.net:51820
AllowedIPs = 10.20.0.0/16次に、同じクライアントに対するサーバー側のエントリと比べる。これはサーバーがクライアントへ送るものを表し、通常はクライアントのトンネルアドレスであって、プライベートサブネットではない。
[Peer]
PublicKey = ...
AllowedIPs = 10.99.0.2/32この二つを取り違えるのが最もよく見る間違いだ。10.20.0.0/16 をサーバーの peer セクションに置くと、サブネット全体がそのクライアントの背後にあると宣言することになり、これは拠点間の主張であって許可ではない。どの peer の AllowedIPs にも一致しないパケットは黙って捨てられるので、誤ったエントリはファイアウォールの問題とまったく同じに見える。
ルートを自分で管理するなら、[Interface] の下に Table = off を設定し、必要なルートを追加する。すると自動では何も追加されないので、ルートはインターフェースを立ち上げる側に属する。
OpenVPN: ルートをプッシュし、それが届くことを確認する
通常のサーバーでは、変更はサーバー設定のクライアントプールの隣にある一行のディレクティブだ。
push "route 10.20.0.0 255.255.0.0"クライアントは接続時にこれを追加する。失敗の形が現れるのは、同じプロファイルが redirect-gateway もプッシュする場合、あるいは誰かがすでに route-nopull を追加している場合であり、前者はセッションをフルトンネルに変え、後者は自分用のルートを含むプッシュされたルートをすべて捨てる。
フルトンネルのサーバーに対して狭い動作が必要なクライアントは、不要な部分だけを無視すべきだ。OpenVPN 2.4 以降には、まさにそのための pull-filter がある。
pull-filter ignore "redirect-gateway"
route 10.20.0.0 255.255.0.0アドレスファミリーは分けて扱うべきで、プッシュされた IPv4 ルートは IPv6 に解決される内部サービスには何の効果もなく、そのサービスだけがトンネル経由で失敗し、他は問題なく見える。
プライベートサブネットが VPN サーバー上ではなく別のクライアントの背後にある場合、サーバーに伝える必要がある。client-config-dir でクライアントごとのファイルを作り、iroute 10.20.0.0 255.255.0.0 を含め、さらに対応する route も加えて他のクライアントが経路を学習できるようにする。iroute がなければ、サーバーがそのプレフィックスをその特定のトンネルへ送る理由はない。
IPsec: トラフィックセレクターが一致する必要がある
IPsec にはプッシュするルートがない。両端がトンネル内に置くアドレスをネゴシエーションし、合意したセレクターの外のトラフィックは暗号化もされず、通しもしない。strongSwan のクライアントではセレクターを明示する。
leftsubnet=192.168.1.0/24
rightsubnet=10.20.0.0/16両側とも 0.0.0.0/0 でゲートウェイを設定している場合、IKEv2 の絞り込みによって子 SA は通常二つの提案の共通部分まで縮小され、これが望ましい結果だ。一方で、設定したセレクターに合わない提案を拒否するゲートウェイもあり、その場合はログにルーティングの不満ではなく失敗した子 SA が出る。そうなったときの変更はゲートウェイ側の話で、クライアントのルートでは相手が受け入れないセレクターのトンネルは作れない。
Windows では内蔵クライアントが同じ判断を二つの設定の背後に隠している。トンネルの分割を有効にし、プレフィックスをプロファイルに登録する必要がある。
Set-VpnConnection -Name Office -SplitTunneling $true
Add-VpnConnectionRoute -ConnectionName Office -DestinationPrefix 10.20.0.0/16プレフィックスを登録することが、再接続後もそれを残すのであり、手で追加したルートはそうならない。
エンドポイントと DNS は判断の外に置く
VPN のエンドポイントはトンネルなしでも到達できなければならず、そうでなければトンネルは自分自身に崩れ落ちる。10.20.0.0/16 だけをルーティングする場合は、エンドポイントがパブリックアドレスなので自動的にそうなる。フルトンネルのプロファイルではそうならず、クライアントにはトンネルが上がる前に エンドポイントへの明示的なホストルートが必要だ。
DNS はルートのプッシュが代わりに決めてくれない二つ目の判断だ。内部名は通常、プライベートネットワークからしか届かないリゾルバーで解決される。サーバーが DNS サーバーをプッシュすればすべての名前がそこを通り、プッシュしなければ内部名は失敗するか、意図しないものへ公開で解決される。分割 DNS は内部リゾルバーに検索ドメインを一つ残すが、クライアントの対応に依存するので、ルーティングの副作用ではなく独立した作業として扱う。
確認し、再起動して、もう一度確認する
三つの確認でルーティングの疑問はほぼ覆える。カーネルに遠隔アドレスがどこへ行くかを尋ね、サブネットの内側から何かを取得し、ふつうの公開リクエストが今も通常の接続から出ていくことを確かめる。
ip route get 10.20.5.7
curl -sS http://10.20.5.7/health
curl -sS https://ifconfig.me一行目はインターフェース名を示す。物理インターフェースを示すなら、ルートは入っていない。Windows では Find-NetRoute -RemoteIPAddress 10.20.5.7 が同じ問いに答え、最後の確認では VPN サーバーではなく、いつもの自分のアドレスが出るはずだ。
そのうえで再起動する。手で追加したルートは消え、プロファイルやプッシュされた設定、あるいは AllowedIPs から来るルートは戻ってくる。再起動の前に狭いアクセスが動き、後で失敗するなら、原因はルートそのものではなくトンネルの起動の仕方にある。
完了と言う前に四点を確認する。プレフィックスがネットワーク管理者から来ていてローカルの何とも重複しないこと、戻り経路があるか NAT のルールが代わっていること、設定した仕組みが自分の環境でルーティングを強制するものであること、きれいに再起動した後も同じ確認が通ることの四つであり、戻り経路が繰り返し失敗する部分なら、strongSwan のフォワーディングに関する注記も読む価値がある。