WSL2ブリッジモード備忘録(おそらく誰の役にも立たないメモ)

Windows11にストアからインストールできるWSL2 0.51以降で非公式ながら提供されているネットワークブリッジモードを2ヶ月利用しての問題点についての調査記録です。

## ブリッジモードとは

WSL2はLAN上の別ノードから直接アクセスできません。これはユーザーから特に不満の多い部分でGitHubのissueも非常に長くなっています。ユーザーの工夫で様々な手法が提案されてきましたが、ブリッジモードはHyper-V仮想スイッチの接続を切り替えるWSL2からのアプローチです。実際にブリッジモードを試す方法については以下の記事をご参照ください。

[WSL2ネットワークを新機能でブリッジモードに変更する(IPv6も利用可)](https://qiita.com/shigeokamoto/items/d45d0a86aed7338a9dc9)

## Hyper-V外部仮想スイッチは環境により構成が異なる

外部仮想スイッチはWindowsの物理ネットワークアダプターを共有するのですが、この実現方法は有線ネットワークとWiFiの場合で異なるようです。

### 構成1 有線ネットワークアダプター

ネットワークアダプターの詳細では、`イーサネット`と`vEthernet(仮想スイッチ名)`の二つが存在し、WindowsはvEhternetに切り替えられますがMACアドレスも変わらず使用感としてはアダプタ名が変わっただけのように感じます。WSL2も当然仮想スイッチに接続され、 /etc/wsl.confで設定したMACアドレスを持つインタフェースが外部と直接通信を行います。LAN内にある他のノードでARPやNDPを確認するとそれぞれのMACアドレスがIP(IPv6)アドレスと紐づいていることが確認できます。まさに2台のPCがあるかのように扱えます。

### 構成2 WiFiネットワークアダプター

`WiFi`と`vEthernet(仮想スイッチ名)`が存在するのは同じですが、さらに`ネットワークブリッジ`が介在します。WiFiの接続先が変わった際にシームレスな切り替えができるように仕組みを変えているようです。

この構成の場合も、wsl.confで設定した新しいMACアドレスをWSL2やWindowsで確認できます。ところが、LAN内の他のノードからは物理ホストのMACアドレス(=Windowsと同じ)として見え。一つのMACアドレスに複数のIPアドレスが付与されているよに見えます。ただし、DHCPサーバーでは新たに付与したMACアドレスでIPアドレスの配布が可能なようです。これはDHCPメッセージに含まれるMACアドレスが使われるためでしょうか？

## 使用に際して違和感ある？

まず有線ネットワークアダプターですが特に違和感もなく、LAN上のDHCPでIPv4アドレスが割り当てられます。これはHCSとinitが連携して行うのでdhclinetやdhcpcdを使う必要はありません。/etc/resolv.confもDHCPサーバから受け取ったアドレスが設定されるので、Hyper-V(HCS)の簡易DNSサーバも使われなくなります。IPv6もRAを受けて自動構成が行われます。これらは**特別な設定なしに利用可能で、特に問題も発生していません**。


手のかかる子はWiFiの方です。
こちらも他のノードに物理アダプターのMACアドレスで見えるという違いはあるものの特別な設定なしに利用可能です。ただ、普通に使えていると思っていたのですが、IPv6が利用できなくなるケースがあることに気づきました。IPv4しか使わない場合には気づかない不具合です。

## WiFiで発生している問題はなに？

異常が発生したりしなかったり頻度もよくわからない状態だったので、すこし踏み込んで調査してみます。WSL2、対向ノードの双方でtcpdumpを使って現象を調査すると興味深いことが見えてきました。

問題が発生した際に利用できなくなるのは

〇 IPv4通信全般
〇 IPv6リンクローカルアドレス(LLA)
× IPv6グローバルユニキャストアドレス(GUA)

なんと、IPv6は全てダメだと思っていたのですが、LLAは通信できるようです。

次にどこで通信が止まっているのかpingを飛ばして確認してみます。WSL2から飛ばしたリクエストは対向ノードまで届き、さらにレスポンスを返しています。しかし、WSL2までは届いていません。

```
       〇 echo request送出   ---->  〇 echo request受取
WSL2   × echo response受取  <----  〇 echo response送出    対向ノード
       × echo request受取   <----  〇 echo request送出  
       -- echo response送出  ---->  -- echo response受取
```


ただ、レスポンスを返している時点で対向はWSL2への宛先を認識できていることがわかります。ダンプしている中にNS/NAのやり取りも出てきたのですが、こちらはお互いそれぞれが発行したNSに対してNAを受け取っています。

```
       〇 NS送出  ---->  〇 NS受取
WSL2   〇 NA受取  <----  〇 NA送出    対向ノード
       〇 NS受取  <----  〇 NS送出  
       〇 NA送出  ---->  〇 NA受取
```

WSL2も対向ノードもお互いにNeighbor Discoveryのやり取りが確立しているようなのでLLAでpingを投げてみると、お互いにレスポンスを受け取ることができます。ダメなのはWSL2側GUAでの受け取りだけです。

この状態に陥った時でもWindowsホストとのGUA同士での通信に問題はありません。そうなると、Linuxのネットワークドライバーも無罪が濃厚です。対向ノードの有線インタフェースからもパケットが送出されているので、問題に関係しそうなのはこの2つのWSL2向けの通信です。

```
 <---- Windowsネットワークブリッジ？ <---- WiFi AP <---- 
```

WiFiアクセスポイントのパケット中継に問題があるかもしれないと別の機材に置き換えて確認しても同じ現象が簡単に発生しました。ネットワークブリッジに問題がありそうです。

試行錯誤の過程で興味深い発見がありました。

neighbor cacheをフラッシュすると通信が復活するようです。

```
$ ip -6 neigh flush dev eth0
```

ただ、通信できない状態でもneighborに対してパケットを送出しているので、Linux管理下のneighbor cacheに異常があるというわけでもなさそうです。

そしてもう一つ、通信不能な現象が発生したりしなかったりしていたのは「**通信が継続しているか、停止しているか**」が問題だったようです。問題なく使えていた時は、SSHで他のノードに接続して作業していたなど常にパケットが流れていたからのようです。GUAでずっと通信していれば問題ないようです。

そこで、こんなスクリプトを書いてみました。**(現状ではこれが回避策となります)**

```shell
#! /bin/bash

ipv6keepalive () {
    while true
    do
        ping -6 -q -c2 -w4 <GUAでアクセス可能なノード>
# pingを飛ばす対象は、LAN内のルーターなど近隣ノードを指定してください(フレッツ光だと ntt.setup 等)
        if [ $? -ne 0 ];then
            sudo ip -6 neigh flush dev eth0
        fi
        sleep 50
    done
}

test $$ -ne $(pgrep -o "$(basename $0)") && exit 1
ipv6keepalive > /dev/null 2>&1
```

pingを飛ばす対象は、LAN内のルーターなど近隣のノードを指定してください(フレッツ光だと ntt.setup 等)
これを/etc/wsl.confに登録して起動時に実行しておくと、通信が途切れなくなります。たとえ途切れても自動復旧してくれます。50秒sleepしていますが、これが60秒だとまれに復旧プロセスに入ることがありました。

ここまで調査した現象から推定できる内容は、

- ネットワークブリッジは外部のノードに対して物理アダプターのMACアドレスで通信する
- 仮想スイッチ側ではそれぞれのMACアドレスで管理されているので、NATのような仕組みで変換情報をテーブルを管理している？
- 仮想スイッチ側のMACアドレスとIPアドレスさえあれば良いので変換情報はNATより単純？
- WSL2から外部方向への中継ではすべて送信元MACアドレスを物理アダプターのものに書きかえるので停滞しない
- 外部から飛び込んでくる宛先IPアドレスを見て適宜宛先MACアドレスを書きかえる
- 変換データがテーブル上に存続できる時間が限られている？(60秒？)
- このため変換データを失うとWSL2まで届けられなくなる
- 変換データが生成されるのは通信のきっかけになるような一部のトラフィックのみ？

この前提でtcpdumpのデータを見返すと、通信が復旧した際にはGUAを起点としてNSを発行し対向のMACアドレスを問い合わせています。通信不能な間にもNS/NAのやり取りは行われていますが、NSパケットはLLAから問い合わせていてGUAは出現しません。返答はGUAからNAを発行していますが通信は回復しません。となると、**条件のカギははNSを発したIPv6アドレス**(とMACアドレスのペア)というのが有力な気がします。そしてその**IPv6アドレスが使われなくなって60秒経過すると変換情報は削除されて通信できなくなる**ような気がします。試しに同一プレフィクスの存在しないアドレスにpingを投げると、通信が復旧しました。どうやらcacheされていないneighborへ通信しようとする場合には起点がGUAのNSが発行されるようです。neighbor cacheをクリアすると通信が回復するのはこういう理由だったのですね。

## 結論: WSL2のブリッジモードは有線LANだと快適、WiFiには少し課題があるものの回避は可能
検証のためにHyper-Vにubuntuをインストールして確認しましたが、挙動は同じでした。はやりHyper-Vの外部仮想スイッチをWiFiで利用する場合に発生する問題のようです。もっと言うと、Hyper-Vの障害ですらなくMicrosoft MAC bridgeドライバー？の問題のようです。

概ね回答にたどり着けたようです。ブリッジモードはWSL2のいくつかの不満を解消してくれ、有線LANでは非常に快適です。WiFi環境は多少難ありですが、回避はできそうです。
## IPv6アクセスをキープするためのsystemdスクリプト
2023/06/05追記)
ここに記したような問題に遭遇した場合、以下のファイルを用意してサービスを有効にすれば回避できるかもしれません。回避スクリプトのsystemd版です。

```systemd:/etc/systemd/system/bridge-keepalive.service
[Unit]
After=network.target network-online.target multi-user.target
[Service]
User=root
Type=simple
Restart=on-failure
StandardOutput=journal
ExecStart=sh -c '\
check () {\
    [ -z "$1" ] && return;\
    for v in flush refresh;\
    do\
        ping -q -c1 -s16 -w4 $1 > /dev/null && return;\
        ip neigh flush dev eth0;\
        if [ $v = "refresh" ];then ip link set eth0 down;ip link set eth0 up;prefix=;fi;\
        echo $v;\
    done;\
};\
count=0;\
getgateway () {\
    [ -n "$prefix" ] && return;\
    for i in 0 1 2;\
    do\
        prefix=$(ip -6 r | sed -nE "0,/^2[^:]{3}:.* eth0 / s|^(2[^:]{3}:)([^/]*)/.*$|\\1\\2|p");\
        if [ -n "$prefix" ];then count=0;return;fi;\
        sleep 2;\
    done;\
    count=$(($count + 1));\
    echo "cannot get ipv6 prefix."$count;\
    if [ $count -gt 10 ];then sleep 1800;fi;\
};\
ip neigh flush dev eth0;\
sleep 2;\
while true;\
do\
    getgateway;check $prefix;sleep 55;\
done\
'
[Install]
WantedBy=multi-user.target
```
```shell
$ systemctl --now enable bridge-keepalive
```