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

Pocket

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 使うことが増えてきて、いい加減鬱陶しくなってきたので、なんとかしようと思う。

解決方法

とりあえず、まずは 2種類 の解決方法から。

1) VPN へのメトリックを延ばす方法

今のところ、この (1) の方法で回避の確認が取れているのは、少し古いバージョンの GlobalProtect 5.x 系 のみだ。
以前は 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回の実行で成功しない場合、 上記コードの最後の行 を、ルートメトリックが書き換わるまで何度もしつこく実行する。

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

        $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       
      • ルートメトリックの書き換えに成功した例:

        $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 を接続しなおす度に、毎回実行する。

2) Hyper-V へのメトリックを短くする方法

(1) がダメな場合のワークアラウンド。
途中の注釈は (1) と同じなので省いている

  1. WSL2 の場合は何らかの WSL2 を使ったディストリビューションを立ち上げる。
  2. VPN を接続状態にする。
  3. 管理者権限で Windows PowerShell を立ち上げ、以下のコードを実行する。

    # 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;
            };
        }
    }
    # shorten vm metric
    $hvIfs = Get-NetAdapter -IncludeHidden | Where-Object InterfaceDescription -Match 'Hyper-V Virtual Ethernet Adapter';
    $hvAddresses = $hvIfs | Get-NetIPAddress -AddressFamily IPv4 | Get-NetworkAddress;
    $hvIfs | Set-NetIPInterface -InterfaceMetric 1;
    $hvIfs | Get-NetRoute -AddressFamily IPv4 | Select-Object -PipelineVariable rt | Where-Object { $hvAddresses | Where-Object { $_.NwAddr -eq (Get-NetworkAddress $rt.DestinationPrefix.Split('/')[0] $_.Prfx).NwAddr } } | Set-NetRoute -RouteMetric 0 -PassThru;
    • 期待通りルートメトリックが書き換わると、以下のようになる

      $hvIfs | Get-NetRoute -AddressFamily IPv4 | Select-Object -PipelineVariable rt | Where-Object { $hvAddresses | Where-Object { $_.NwAddr -eq (Get-NetworkAddress $rt.DestinationPrefix.Split('/')[0] $_.Prfx).NwAddr } };
      
      ifIndex DestinationPrefix  NextHop RouteMetric ifMetric PolicyStore
      ------- -----------------  ------- ----------- -------- -----------
      48      172.30.175.255/32  0.0.0.0           0 1        ActiveStore
      48      172.30.160.1/32    0.0.0.0           0 1        ActiveStore
      48      172.30.160.0/20    0.0.0.0           0 1        ActiveStore
      21      172.18.31.255/32   0.0.0.0           0 1        ActiveStore
      21      172.18.16.1/32     0.0.0.0           0 1        ActiveStore
      21      172.18.16.0/20     0.0.0.0           0 1        ActiveStore
  4. 上記を、 PC を再起動したり、 WSL を起動しなおす度に、毎回実行する。

解説

上記のコードは、 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外 と通信する際に、 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 の問い合わせ (クエリ) を受けると、 そのレスポンスが VPN を抜けて明後日の方向にルーティングされてしまう。
これが、 ゲスト側で DNS による名前解決ができなくなる原因だ。

(1) では VPN へのルーティングメトリックを長くする

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

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

ただ、 VPN ソリューションとしては VPN を通らなくなるような抜け穴は極力塞いでおきたい。
このため、 VPN ツールの更新とともにだんだん (1) の手口が通用しなくなってきた。

(2) では Hyper-V へのルーティングメトリックを短くする

(2) の方法の場合、 もっと単純に WSL2 等 Hyper-V に関するルートへのメトリックを最短の 1 にする手段をとっている。

この場合、 VPN プロバイダによっては最短で同じメトリックとなってしまう。
基本的に、ルーティングでメトリックもアドレスプレフィックスも同じ場合、あとから登録されたルートが優先されるようなので、多くの場合は意図したとおりに動く。
しかし、状況によっては期待しない動きとなる場合は、もう (1) の方法でなんとかして伸ばすしか方法がなさそうだ。。。

また、 (2) の方法の場合 Hyper-V が偶々イントラネットに実在するアドレスを割り当ててしまった際に、それを常に優先するルーティングを設定してしまう上に、 GlobalProtect を OFF にしても PC の電源を落とすまでその設定が残り続けてしまう問題もある。
(2) のコマンドでは基本的に同一リンク上のアドレス宛のルーティングしか変更していないので、このコマンドの実行ならではな問題は生じにくいはずではある。(そもそもアドレスが衝突すると、上記コマンドの実施にかかわらず「ロンゲストマッチ」の仕組みで Hyper-V 側にルーティングされて、イントラネットに繋がらなくなるため)
とはいえ、規模の大きいイントラネットを使っているネットワークの場合は注意が必要だろう。

Windows 11 の場合、 WSL や Default Switch 用に自動作成された仮想 NIC がネットワーク接続一覧に出てこないために、アドレスが衝突していることに気づきにくいので尚更だ。

別解

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

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

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

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

改訂履歴

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

VPN に繋ぐと WSL2 や Hyper-V VM でネットワークに繋がらなくなる問題を解消する」への5件のフィードバック

  1. GlobalProtectでVPN接続中にローカル接続するために便利に使用させていただきましたが、最近急にローカル接続できなくなりました。

    • 私も最近つながらなくなったのですが、アダプタ名が”PANGP Virtual Ethernet Adapter Secure”に変わっていて、これを反映したらつながるようになりました。

      • すみません。確認不足で、実際はつながってませんでした。
        私も内容をよく分かってないので、作者の改定待ちます。。

    • 少し古いバージョンの GlobalProtect 5.x 系では引き続き使えていますが、最新の 6系 は環境が無いので試せていませんね…

      とりあえず、ワークアラウンドとして「2) Hyper-V へのメトリックを短くする方法」を追加してみたので、もしこれで上手く行ったよという方はコメントいただけると助かります。

  2. ピンバック: プロキシ環境下で Docker Desktop から Rancher Desktop への移行する | Aqua Ware つぶやきブログ

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください