2023-02-22に更新

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

OpenVPN や Cisco AnyConnect, GlobalProtect 等といった VPN に接続した際、 Hyper-V 仮想マシン内からや、 WSL2 のディストリビューション内、 Windows Sandbox 内、 WSL2 ベースの Docker コンテナ内 等々、 Hyper-V 系の技術を使った仮想環境から、 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 使うことが増えてきて、いい加減鬱陶しくなってきたので、なんとかしようと思う。

解決方法

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

以前は AnyConnect でもこの方法が使えたはずなのだが、どうやら対策されて打つ手なしになってしまった模様。
今のところ、以下の方法で回避の確認が取れているのは、 GlobalProtect のみだ。

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

    $targetIfName = 'PANGP Virtual Ethernet Adapter';
    # define function
    function Get-NetworkAddress {
        param([Parameter(Mandatory, ValueFromPipelineByPropertyName)][string]$IPAddress, [Parameter(Mandatory, ValueFromPipelineByPropertyName)][int]$PrefixLength);
        process {
            $bitNwAddr = [ipaddress]::Parse($IPAddress).Address -band [uint64][BitConverter]::ToUInt32([System.Linq.Enumerable]::Reverse([BitConverter]::GetBytes([uint32](0xFFFFFFFFL -shl (32 - $PrefixLength) -band 0xFFFFFFFFL))), 0);
            [pscustomobject]@{
                Addr = $IPAddress;
                Prfx = $PrefixLength;
                NwAddr = [ipaddress]::new($bitNwAddr).IPAddressToString + '/' + $PrefixLength;
            };
        }
    }
    # extend route metric
    $targetAddresses = Get-NetAdapter -IncludeHidden | Where-Object InterfaceDescription -Match 'Hyper-V Virtual Ethernet Adapter' | Get-NetIPAddress -AddressFamily IPv4 | Get-NetworkAddress;
    $targetIfs = Get-NetAdapter -IncludeHidden | Where-Object InterfaceDescription -Match $targetIfName;
    $targetIfs | Set-NetIPInterface -InterfaceMetric 2;
    $targetIfs | Get-NetRoute -AddressFamily IPv4 | Select-Object -PipelineVariable rt | Where-Object { $targetAddresses | Where-Object { $_.NwAddr -eq (Get-NetworkAddress $rt.DestinationPrefix.Split('/')[0] $_.Prfx).NwAddr } } | Set-NetRoute -RouteMetric 6000;
    
    • GlobalProtect 以外の場合は、 上記コード一行目の 'PANGP Virtual Ethernet Adapter' のところを VPN ソリューションの ネットワーク接続 アダプタ名に書き換える。
      具体的には、 Win+Rncpa.cpl を実行して「ネットワーク接続」を開き、該当する VPN の接続のプロパティを開いて、 "接続の方法" のところに書かれた名前 (の一部) を指定する。
      • OpenVPN なら TAP-Windows Adapter とか、 AnyConnect なら Cisco AnyConnect とか。 環境によっても違うかも。
    • 管理者権限の PowerShell は、 スタート ボタンを右クリックで簡単に立ち上げられる。
    • 1回の実行で成功しない場合、 上記コードの最後の行 を、ルートメトリックが書き換わるまで何度もしつこく実行する。

      • ルートメトリックが書き換わっていない状態の例:

        ```text/plain
        $targetIfs | Get-NetRoute -AddressFamily IPv4 | Select-Object -PipelineVariable rt | Where-Object { $targetAddresses | Where-Object { $.NwAddr -eq (Get-NetworkAddress $rt.DestinationPrefix.Split('/')[0] $.Prfx).NwAddr } }

        ifIndex DestinationPrefix NextHop RouteMetric ifMetric
        ------- ----------------- ------- ----------- --------
        36 172.30.175.255/32 0.0.0.0 0 0
        36 172.30.160.1/32 0.0.0.0 0 0
        36 172.30.160.0/20 0.0.0.0 0 0
        20 172.18.31.255/32 0.0.0.0 0 0
        20 172.18.16.1/32 0.0.0.0 0 0
        20 172.18.16.0/20 0.0.0.0 0 0
        ```

      • ルートメトリックの書き換えに成功した例:

        ```text/plain
        $targetIfs | Get-NetRoute -AddressFamily IPv4 | Select-Object -PipelineVariable rt | Where-Object { $targetAddresses | Where-Object { $.NwAddr -eq (Get-NetworkAddress $rt.DestinationPrefix.Split('/')[0] $.Prfx).NwAddr } }

        ifIndex DestinationPrefix NextHop RouteMetric ifMetric
        ------- ----------------- ------- ----------- --------
        36 172.30.175.255/32 0.0.0.0 6000 0
        36 172.30.160.1/32 0.0.0.0 6000 0
        36 172.30.160.0/20 0.0.0.0 6000 0
        20 172.18.31.255/32 0.0.0.0 6000 0
        20 172.18.16.1/32 0.0.0.0 6000 0
        20 172.18.16.0/20 0.0.0.0 6000 0
        ```

  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 や 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 クライアントが即座にルーティングを復活させてしまうため、 メトリックを長くするだけにとどめている。

なお、 GlobalProtect でも、書き換えたメトリックを戻される対策がされたようだが、 インターフェースメトリックを書き換えたり、 短期間に何度も書き換え直したりすると、メトリックを戻すことを諦めてくれる。
このため、上記のようなコードになっている。

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

別解

Microsoft Docs 上の WSL のトラブルシューティング では、 WSL の /etc/resolv.conf の DNS を、 ホストPC の WinNAT ではなく VPN のトンネリング先のネットワークの DNS に書き換える方法も掲載されている。

……が、 これを行うと、ゲスト側の DNS の名前解決こそできるようになるものの、 依然 ホストPC から ゲストOS へのパケットが届かない問題は解消されない。
このため、 ゲストで立てたサーバーに、 ブラウザでアクセスしたり、 ssh 等でログインしたりと言ったことはできないままだ。

更に、イントラネット内で別途 DNS を運用していた場合、イントラネット内の名前解決ができなくなる問題もある。

このため、この DNS を書き換える方法は、望ましい解決方法とは言えない。

改訂履歴

  • 2023-02-22:
    • コードの一部でエラーが発生していた部分を修正。
  • 2023-02-14:
    • 各社のメトリック書き換え対策に対し、 GlobalProtect 向けにコードを変更し、 AnyConnect 向けには打つ手なしとなったことを追記。
  • 2021-04-04:
    • Windows Sandbox などでも問題となる旨追記
    • いくつかの VPN ソフトについての設定例を追記
    • Microsoft Docs 記載の改善方法について追記
  • 2021-01-28:
    • 仮想ネットワーク上に DHCP は存在しないのに、 DHCP の存在がすると誤記があったので修正。
    • スクリプトで Hyper-V の仮想スイッチをさかす際に、 NIC 名ではなくて、ネットワークアダプタの接続名で探すように変更。
Originally published at aquasoftware.net
ツイッターでシェア
みんなに共有、忘れないようにメモ

advanceboy

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

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

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

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

コメント