2021-01-28に更新

VPN に繋ぐと WSL2 や Hyper-V VM でネットワークに繋がらなくなる問題を解消する

OpenVPN や Cisco AnyConnect といった VPN に接続した際、 Hyper-V 仮想マシン内からや、 WSL2 のディストリビューション内、 WSL2 ベースの Docker コンテナ内から、 PC 外のネットワークにアクセスしようとすると、 以下のようなエラーが発生して失敗する。

$ # curl 利用時の例
curl: (6) Could not resolve host: example.com
curl: (5) Could not resolve proxy: proxy.example.com

$ # apt で更新しようとした場合の例
W: Failed to fetch http://archive.ubuntu.com/ubuntu/dists/focal/InRelease  Temporary failure resolving 'archive.ubuntu.com'
W: Failed to fetch http://archive.ubuntu.com/ubuntu/dists/focal/InRelease  Temporary failure resolving 'proxy.example.com'

エラーの内容からわかるとおり、アクセス先やプロキシーのドメイン名を DNS で解決できなくなっている。

このような問題が発生することは以前から知っていたのだが、このご時世で VPN 使うことが増えてきて、いい加減鬱陶しくなってきたので、なんとかしようと思う。

解決方法

とりあえず、まずは解決方法から。

  1. WSL2 の場合は何らかの WSL2 を使ったディストリビューションを立ち上げる。
    • WSL 2 based engine が有効になった Docker Desktop を起動するだけでも OK。
    • WSL2 を使っていないなら、何もする必要はない。
  2. VPN を接続状態にする。
  3. 管理者権限で PowerShell を立ち上げ、以下のコードを実行する。 (Cisco AnyConnect の例)

    # define function
    function Get-NetworkAddress ([Parameter(Mandatory, ValueFromPipelineByPropertyName)][string]$IPAddress, [Parameter(Mandatory, ValueFromPipelineByPropertyName)][int]$PrefixLength) {
        process {
            [pscustomobject]@{
                Addr = $IPAddress;
                Prfx = $PrefixLength;
                NwAddr = [ipaddress]::Parse($IPAddress).Address -band [uint64][BitConverter]::ToUInt32([System.Linq.Enumerable]::Reverse([BitConverter]::GetBytes([uint32](0xFFFFFFFFL -shl (32 - $PrefixLength) -band 0xFFFFFFFFL))), 0);
            };
        }
    }
    # extend route metric
    $targets = Get-NetAdapter | Where-Object InterfaceDescription -Match 'Hyper-V Virtual Ethernet Adapter' | Get-NetIPAddress -AddressFamily IPv4 | Get-NetworkAddress;
    Get-NetAdapter | Where-Object InterfaceDescription -Match 'Cisco AnyConnect' | Get-NetRoute -AddressFamily IPv4 | Select-Object -PipelineVariable rt | Where-Object { $targets | Where-Object { $_.NwAddr -eq (Get-NetworkAddress $rt.DestinationPrefix.Split('/')[0] $_.Prfx).NwAddr } } | Set-NetRoute -RouteMetric 60000;
    
    • AnyConnect 以外の場合は、 上記の 'Cisco AnyConnect' のところを VPN ソリューションの ネットワーク接続 アダプタ名に書き換える。
      具体的には、 Win+Rncpa.cpl を実行して「ネットワーク接続」を開き、該当する VPN の接続のプロパティを開いて、 "接続の方法" のところに書かれた名前 (の一部) を指定する。
  4. 上記を、 PC を再起動したり、 VPN を接続しなおす度に、毎回実行する。

解説

上記のコードは、 VPN プロバイダが ホスト PC に作成しているルーティングの設定のうち、 Hyper-V や WSL に関係する宛先ものだけ、ルーティングのメトリックをめっちゃ長くしている。

Hyper-V や WSL のネットワークの仕組みに簡単に触れながら、もう少し細かく説明していこう。

Hyper-V と WSL2 の NAT

まずそもそもの前提として、 ホストPC と WSL2 の仮想環境で異なる IPアドレス を持っている
利便性のため、 WSL2 の localhost のリッスンが、ホスト PC の localhost へ転送されているなど、 IPアドレス が異なることをあまり意識せずすむような仕組みにはなっているが。

そして その WSL2 や、 Hyper-V の VM では、 PC の外と通信する際に NAT を経由して通信を行う。
(Hyper-V の場合は、上記以外にも 仮想NIC をブリッジする方法もあるのだが、今回の問題から外れるので、除外して考える。)

このとき、 Hyper-V や WSL2 のバックエンドとなる Hyper-V ハイパーバイザーは、 ホスト PC 上に 仮想 NIC を作成する。
そしてその仮想 NIC に、 WinNAT と呼ばれる 機能を割り当て、 更に WSL2 向けには DNS の昨日も割り当てて、 ホスト PC の外との通信の中継を担わせるようになっている。

これら、 仮想 NIC や、 WSL2 に自動で割り当てられる IP アドレスは、 ホスト PC と被らない適当なプライベート IP アドレスが割り当てられる。

なお、 やっかいなことに、この割り当てられた IP アドレスは、起動する度に毎回異なる。

VPN 側の動作

一方の ソフトウェア VPN では、 VPN の有効化と同時に、 ホスト PC のルーティングを片っ端から書き換えている。

具体的には、 VPN の 仮想 NIC に対して短いメトリックとなるよう、 ルーティングを書き加える。
それによって、全ての通信が VPN トンネルを通るようになっているのだ。

そして悲劇が起こる

その結果、どうなるか。

ホスト PC から ゲスト VM (あるいは WSL ディストロ) のプライベート IPアドレス へ通信しようとすると、 VPN のトンネリング側にルーティングされてしまう

例えば、 WSL2 から ホストPC へ DNS の問い合わせ (クエリ) を受けると、 そのレスポンスが明後日の方向にルーティングされてしまう。
これが、 ゲスト側で DNS による名前解決ができなくなる原因だ。

この問題を回避するため、 前述のスクリプトでは、 ルーティングテーブルを書き換えて、 ゲスト VM のアドレスに対して、 VPN の 仮想NIC のメトリックを長くすることで、 VPN にルーティングされないようにしているわけだ。

本当は、 VPN へのルーティングを消してしまいたいところなのだが、 ルーティングを消しても、 VPN クライアントが即座にルーティングを復活させてしまうため、 メトリックを長くするだけにとどめている。

WSL2 , problem with network connection when VPN used (PulseSecure) · Issue #5068 · microsoft/WSL | GitHub
上記の Issue ではもっと単純に、 WSL2 へのメトリックは 1 にして、 全ての VPN 関係のルーティングのメトリックを 4000 くらいに延ばすワークアラウンドが紹介されている。
しかしそれだと、本来 VPN 経由にしなくてはならない通信までもが VPN 通らなくなったりする場合があって、 VPN を使わせるポリシー上マズい。
このため、上述のように、 必要な ネットワークアドレス だけに絞ってメトリックを書き換えている。

改訂履歴

  • 2020-01-28: 仮想ネットワーク上に DHCP は存在しないのに、 DHCP の存在がすると誤記があったので修正。 スクリプトで Hyper-V の仮想スイッチをさかすときに、 NIC 名ではなくて、ネットワークアダプタの接続名で探すように更新。
Originally published at aquasoftware.net
ツイッターでシェア
みんなに共有、忘れないようにメモ

advanceboy

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント