WSL2をIPv6ネットワークに接続する(番外:改良版をタスクに登録する)

## はじめに
:::note warn
2023.11.12追記
現在の最新WSLではブリッジモードやミラーモードというネットワークモードが追加されているのでIPv6を使いたい場合にはそちらをご利用ください。
この手法は旧来のネットワークモードやWindowsサンドボックスでの活用方法もありますが、基本的にはもはや必要のない方法です。
:::

前中後編で、現状のWindowsのネットワーク機能を検証し、dhcpcdの機能を一部だけ拝借してWSL2をIPv6ネットワークに接続しました。実際にはHyper-V仮想ネットワークに対してコンフィグレーション情報を広告するように設定するものなので、WSL2の為の設定をvEhternet (Default Switch)にも広げるだけで、クライアントHyper-VやSandboxで起動したインスタンスもIPv6ネットワークに参加できるようになりました。

前編 [*WSL2がIPv6通信できない理由*](https://qiita.com/shigeokamoto/items/ba89516d1448b13f7251)
中編 [*現時点のWindows標準機能だけでRAを広告する*](https://qiita.com/shigeokamoto/items/9c5a631fe3099ed346c2)
後編 [*DHCPv6-PDでプレフィクス委譲を受ける*](https://qiita.com/shigeokamoto/items/a13f5fea5085f541a0b5)
番外 *WSL1主体のスクリプトに書き換えてタスクスケジューラに登録する* <-- イマココ

ここまでうまくいったという方であれば、いちいちコマンドを起動するのも面倒になってきていると思います。そして常用してみると細かな粗が見えてくると思います。

そこで次のステップとしては、タスクスケジューラでWindowsログイン時に自動設定を行い少し調整を加えたいと思います。

## タスクスケジューラを使って起動時に自動設定する
基本の生活がWSL2+Ubuntuだったので、そこからコマンドを起動するような形で作ったのですが、タスクスケジューラからの起動であればWSL2は役割も薄いのでここを主体にする意味はあまりなさそうです。

そこで、WSL1を主体に動かすスクリプトに作り替えてから登録することにします。スクリプトの内容的には後編のものと同様なのですが、いくつか構成を組み替えます。

1. Windowsから wsl1 でスクリプトを起動できるようにする
2. wsl2インスタンスを起こす
3. vEhternetインタフェースの設定は dhcpcdがプレフィクスの割り当てを受けた際にスクリプトを呼び出す。
4. 気になった点を改善

## 今回の改善点

とりあえずつないでみようというコンセプトとはいえ、常用してみるといろいろ気にかかります。
- ノートPCにてスリープ時のバッテリー消費が激しい
- スリープ復帰時に経路を見失うことがある
- プレフィクスが変更になった際に古いアドレスが残る

dhcpcdを起動したままモダンスタンバイに入ると、通常の5倍のスピードでバッテリーを消費していました。RAを受け取るたびに起こされてしまうようです。そこで、dhcpcdはネットワークの設定を終えると停止するようにしました。上流ルータに対してリース要求を継続する為だけに常駐させていましたが、タスクスケジューラから時々起動させれば十分でしょう。

次に経路の問題ですが、Windowsが配布する経路情報の有効期限は1800秒がデフォルトなので有効期限を過ぎる経路を喪失してしまいます。通常は常にリフレッシュされるので途切れることはないのですが、スリープ中などネットワークインタフェースが停止している間に期限を迎えると喪失することがあります。この問題に関しては復帰後にタスクスケジューラでスクリプトが走るようにしておけばリカバーできそうですが、`Set-NetRoute -Publish yes`を呼び出してもすぐにRAが広告されるというわけではないようです。一旦広告をやめて再開させると強制的にRAの広告が行われるようです。

更に、WSL2側で経路の有効期限が切れただけであればこれで対処できるのですがスリープ中にリース期限を過ぎてしまい、ルーターが別のプレフィクスを割り当てなおしてきた場合に問題が発生することがあります。新しいプレフィクスを設定する前に仮想インタフェースがRAを出してしまったケースです。この場合には配布した経路情報をキャンセルするために有効期限を0秒に縮めたRAを送出します。[Set-NetIPInterface -AdvertisedRouterLifetime](https://docs.microsoft.com/ja-jp/powershell/module/nettcpip/Set-NetIPInterface?view=windowsserver2019-ps#:~:text=False-,-AdvertisedRouterLifetime,-Specifies%20a%20lifetime)を0秒にします。

これで経路はキャンセルできたのですが、`AdvertisedRouterLifetime`は経路の有効期限というだけで配布したプレフィクスの使用を終了させる効力はないようです。プレフィクスを抹消するには別途 [SetNetRoute -ValidLifetime](https://docs.microsoft.com/ja-jp/powershell/module/nettcpip/set-netroute?view=windowsserver2019-ps#:~:text=False-,-ValidLifetime,-Specifies%20a%20valid)を0にして広告するようです。

## ブラッシュアップした設定スクリプト

前項のような改善を加えたスクリプトをdhcpcdから外部スクリプトとして呼び出します。スクリプトは1本にまとめたいので、base64エンコードしてメインのスクリプトの中に埋めてしまいます。

具体的にはこのようなファイルを作成し、`tar`でアーカイブしたものを`base64`コマンドでテキスト化します。対応するケースが増えてしまったので少し長くなってしまいました。

```shell:.dhcpcd.script
#! /bin/sh

if [ ! "$reason" = "BOUND6" -a ! "$reason" = "REBIND6" ]; then
    exit
fi

callPowerShell () {
    case $1 in
        super )
            powershell.exe start-process -windowstyle hidden powershell -Verb runas $2 | tr -d '\r';;
        * )
            powershell.exe $2 | tr -d '\r';;
    esac
}

getOldPrefix () {
    _ps=$(cat <<- EOF
        Get-NetRoute -AddressFamily IPv6 |
        where {\$_.InterfaceAlias -match 'vEther.*' -and \$_.DestinationPrefix -match '${prefix56}.0::/64'} |
        %{\$_.DestinationPrefix};
EOF
    )
    callPowerShell normal "$_ps"
}

getInterfaces () {
    _ps=$(cat <<- EOF
        Get-NetRoute -DestinationPrefix '0.0.0.0/0' | %{'I:' + \$_.ifIndex + ':A:' + \$_.InterfaceAlias};
        Get-NetAdapter -Name 'vEther*' | %{'N:' + \$_.Name};
EOF
    )
    _d=$(callPowerShell normal "$_ps")
    defaultindex=$(echo "$_d" | sed -nr 's/^I:([0-9]+):.*$/\1/p')
    defaultname=$( echo "$_d" | sed -nr 's/^I:.*:A:(.*)$/\1/p')
    defaultswitch=$(echo "$_d"| sed -nr 's/^N:(vEther.*Default.*)$/\1/p')
    wslswitch=$(echo "$_d"| sed -nr 's/^N:(vEther.*WSL.*)$/\1/p')
}

stateDefault () {
    cat <<- EOF
        Set-NetIPInterface -Forwarding Enabled -InterfaceIndex ${defaultindex};
        Set-NetIPInterface -InterfaceAlias 'vEthernet (${defaultname})'
         -AddressFamily IPv6 -Dhcp Disabled -RouterDiscovery Disabled;
EOF
}

stateRemoveOldPrefix () {
    for remove in  $oldprefix
    do
        remove=$(echo $remove| sed "s|.::/64\$|$i::/64|")
        cat <<- EOF
                Set-NetIPInterface -InterfaceAlias '$interface' -AddressFamily IPv6
                        -AdvertisedRouterLifetime (New-TimeSpan -Minutes 0) -PassThru;
                Set-NetRoute -InterfaceAlias '$interface' -AddressFamily IPv6
                        -DestinationPrefix '$remove' -Publish no  -PassThru;
                Set-NetRoute -InterfaceAlias '$interface' -AddressFamily IPv6
                        -DestinationPrefix '$remove' -ValidLifetime 0 -PreferredLifetime 0 -Publish yes -PassThru;
EOF
    done
}

stateRemoveAddress () {
    cat <<- EOF
        Get-NetIPAddress -InterfaceAlias '$interface' -AddressFamily IPv6 |
                where {\`\$_.IPAddress.StartsWith('$prefix56')} |
                Remove-NetIPAddress -Confirm:\`\$False -PassThru;
EOF
}

stateNewIF () {
    cat <<- EOF
        New-NetIPAddress -IPAddress $prefix:$address -PrefixLength 64
                -InterfaceAlias '$interface' -SkipAsSource \`\$True -PolicyStore ActiveStore;
EOF
}

stateRoute () {
    cat <<- EOF
        Set-NetIPInterface -InterfaceAlias '$interface' -AddressFamily IPv6 -Advertising Enabled
                -AdvertiseDefaultRoute Enabled -Forwarding Enabled
                -AdvertisedRouterLifetime (New-TimeSpan -Minutes 30) -PassThru;
        Set-NetRoute -InterfaceAlias '$interface' -AddressFamily IPv6 -DestinationPrefix '$prefix::/64' -Publish no -PassThru;
        Set-NetRoute -InterfaceAlias '$interface' -AddressFamily IPv6 -DestinationPrefix '$prefix::/64' -Publish yes;
EOF
}

prefix=${new_dhcp6_ia_pd1_prefix1%::*}
prefix56=$(echo $prefix | sed -r 's/^(.*)..$/\1/')
address=$(od -An -tx8 -N8 /dev/random | sed -r 's/^ *(.{4})(.{4})(.{4})(.{4})/\1:\2:\3:\4/')

getInterfaces
default=$(stateDefault)
oldprefix=$(getOldPrefix)

if [ "${oldprefix}" = "${prefix}::/64" ]; then
    default=
fi

i=0
for interface in "$wslswitch" "$defaultswitch"
do
    if [ -n "$interface" ]; then
        removera=$removera$(stateRemoveOldPrefix "$removera")
        removeaddr=$removeaddr$(stateRemoveAddress "$removeaddr")
        newif=$newif$(stateNewIF "$newif")
        route=$route$(stateRoute "$route")
        i=`expr $i + 1`
        prefix=$(echo $prefix| sed "s/.\$/$i/")
    fi
done

if [ -n "$default" ]; then
    if [ -n "$removera" ]; then
        removera="${removera}sleep 6;"
    fi
    route="sleep 2;$route"
    c="$default$removera$removeaddr$newif$route"
else
    c="$route"
fi
callPowerShell super "\"${c}\""
```

このファイルを以下の一文でエンコードします。

```shell
tar czf - .dhcpcd.script | base64
```

生成されたテキストを本体のスクリプト内に埋めておいて作動する時に /tmp/.dhcpcd.script が生成されます。多少コードに変更は入ってますが、後編の内容と同じです。


```shell:ipv6.sh
#! /bin/sh

installPackage () {
    for package in $*
    do
        which $package > /dev/null 2>&1
        if [ $? -ne 0 ]; then
            apk --no-cache add $package
        fi
    done
}

startWSL2 () {
    wsl2list=$(wsl.exe -l -v | iconv -futf-16le | sed -nr -e 's/\r//' -e 's/^[* ]+([^ ]+) *([^ ]+) *2$/\1 \2/p')
    if [ -n "$wsl2list" -a "$wsl2list" = "${wsl2list%% Running*}" ]; then
       (cd /mnt/c;wsl.exe -d ${wsl2list%% *} echo)
#        wsl.exe -d ${wsl2list%% *} --cd / echo
    fi
}

createDhcpcdConf () {
    dhcpcdconf=/tmp/.dhcpcd.conf
    dhcpcdscript=/tmp/.dhcpcd.script
    defaultindex=$(powershell.exe "Get-NetRoute -DestinationPrefix '0.0.0.0/0'| %{\$_.ifIndex}" | tr -d '\r')
    wsl1if=$(ip -4 addr 2> /dev/null)
    devs=$(cat /proc/net/route | sed -nr 's/^([a-z]+[0-9]+)[^0-9a-z]*.*$/\1/p' | sort | uniq)
    primary=$(echo "$wsl1if" | sed -nr "s/^${defaultindex}: *([^:]*):.*\$/\\1/p")
    devs=$(echo "${devs}" | grep -v ${primary})
    devs=$(echo $devs)
    c="denyinterfaces ${devs}\ninterface ${primary}\nscript ${dhcpcdscript}\nnogateway\nnoipv6rs\nipv6only\nnodhcp\nnoipv4\nnoipv4ll\ndhcp6\nia_pd 1"
    echo -e $c > $dhcpcdconf
}

runDhcpcd () {
    dhcpcd -f $dhcpcdconf -1 2>/dev/null;
}

cd /tmp
base64 -d <<EOF | tar xzf -
H4sIAAAAAAAAA+0XXW/aSDCv+FdMfY4MVDaQBNpzygNtkgopl0al1z6UHnHsJaxqbGvXhESE/36z
3rVjE9JreupVJ3kixfbO98zOB7Y/82LPt7nHaJzs/BRoI7zodsWz86LbLj5TVLu7v9M56Ox18d/B
/v5Ou7Pf3evsQPvnmFOGBU9cBrATfXXnURI9SvdP+P8p/PYMWpc0bPGZptEpfIZnoBuMuDwKdeiD
/vrdn2dHPR0sdxPz/vj1MEV9OYRkRkINEMgNTbQp1TTPDYLzaEnYaEaCAOoNWKUEnssJGB2goVbj
i5gwaIBWE5hYUHNBbZMbAiItiRWzyCOcg7WkoR8teXIbEJhR3ydhgQGsj4RdAluELgdjD+4gYWD5
YI6ZeXio1ZrQ2KrjIWnqA3c9ba1pVyR5F/jnjEzpzb0Dk5j3jbrnJvDqlQXH70602luSWGckeR8t
EgLWwPcZmnzizmlwC8Pz6x7cabXljDACq7ExsYdhQtjU9cggoGivNXcTbwbm9TFGkdlNE2Md+iAo
jwhPaOgmNAqVGRmxsYrTg25vbbcdp9U7MNdCze5qK98agyBMFQ40VB5K+QkjNncDTDC6pyvnczv5
E7x/aLLZttO/VtvEaO+uzKFjwvPUPzodhj65wS/TGeSn5fgI05WKge/GiALrzJ2TLGBNJfUs5xfY
hw5P/NTwx72WZD6ZuosAXUC7kIF4s0jgfR21cOKDFTIweeuvoVP/3LZ+//K84dhNozXutGKzJCFE
K1AAfEOC3USv63azsY2fLylmumRCmf/MqWdX5kiybEpa8uApUj6NTosS8BJgDSZECS+WcCH7I5ma
4XmeNbBOIrZ0mU/DKzgO3ctAqMvRMuPGqhhoka1tgjYqRWU8JGhMLkDEed0wsb63VZ51hOMNjihX
ZqS3lOG3F10Tdptj1HXJfH5P5ojfUv3TiAFLkdjBAIwo8GUhytRFWk1is4gb8lPGXOd3dlqrY+PO
oOnbHV67Wimg3xUIg2YH5ja3UUoNj9HDhKJi6fQpnZKEYuHUz8jS+oBvo9gNwfqDhojm0G6Ade5y
/mHGFof3dqi6/hELtvQCFQ/kOV9cBpTPsAbhP9X70Q2on8eijbqRhDBGyofKulsMTMm6rKn4UUg2
rouy5pFCeZulNSN7qmuivedj5CJtlJkseySGJf9Ek1ndNLLJYDbSkVCT1m0ofxOFU8rmjpB04gY4
lItuFmsBb8vw5BGnxE3acCp/VXY4hpvhZDJOSXiVzKB3gKZ9OwajrzQe8FG0YFgDwtAPbCHsjALq
3Y6SCCMx8BJ6TdL3zRJO78/3N62nZiOvr0Kb04plp/qmtCPvgw97o/YDtbq/Uaz/qma2Du0se+lq
UarWX6IX61DlVyRY0vSNVUiWE/EDpjeh7iT2OxOJ6ew6TnOtZYWQN2N5kM1hOQDF/LXtdOrh0FN3
FTkipBhgyJObl7hwvISWT65bDDezaF4WAM26vTpYNx7+R5nOeM8Z7zvjAyG8vFRpaoChruKcbWj5
TEFMcQdtqA1dN1Y5yTpdxbNtcJ2GrbyRZ1rSrZz225oYYXlixBTTjXxR0PGjtH/oGs40ISbVbAni
nPdejxp6zO0b2ZtyanOQ6jmBGHzyXcQ8YxTvJdasm+gFAsGKmafTvpE+FINsVLo8S8WLa4mSxSMT
mt5UXZ4JGtq/IDcxA4Pi9ti50Gp56ItXJpvfLXtstAzaUssihjSdA9p9dFT0yjm4R+feb4kdpjF7
X/OAkBh6h3qmRzykP7rE7R0qJ2R76+eq71NQCKmMk2Ig2O5zLnWGKjaWY/nrTB+jWd56rOvar/6p
WkEFFVRQQQUVVFBBBRVUUEEFFVRQQQUVVFBBBRV8J/wN5aYq9wAoAAA=
EOF

installPackage dhcpcd
startWSL2
createDhcpcdConf
runDhcpcd
```

全容はこのような形です。前回と異なり、スクリプトを実行した後で停止しています。



## タスクスケジューラへ登録

改善の話はここまでにしていよいよ本題のタスクスケジューラへの登録です。

シェルスクリプトの実行をそのままタスクへ登録したのでは、実行時にコンソールウィンドウがちらちらと出てきてしまします。さすがにそれは頂けないので、wscriptを使って起動することにします。

起動スクリプトとしてはこんな感じです。

```:ipv6.vbs
Set ws = CreateObject("WScript.Shell")
ws.run "wsl -d Alpine -u root /mnt/c/Users/...../AppData/Local/Tasks/ipv6.sh", 0, true
```

筆者はこの`ipv6.vbs`と先ほどのシェルスクリプトを %USERPROFILE%AppData の下に一緒に置いてます。`ipv6.sh`は wsl1の中に置いても構わないのですが`ipv6.vbs`はWindows側に置かないといけないので同じ場所に置く方が管理しやすいと思います。

次にタスクスケジューラに登録します。登録内容はこのような感じです。

用意するもの
ipv6.vbs: Wscriptスクリプト
ipv6.sh:  ipv6.vbsから呼び出すシェルスクリプト


登録内容
[全般]
◎ユーザーがログオンしている時のみ実行する
■最上位の特権で実行する

[トリガー]
タスクの開始: ログオン時
◎任意のユーザー
■繰り返し時間 30分  継続時間: 無制限　

[操作]
プログラムの開始
プログラム: wscript.exe
引数: c:\\Users\\....\\ipv6.vbs


- ネットワークの設定を操作するのに管理者権限が必要ですから最上位の特権で実行してください。
- トリガーはログオン時に起動して、その後繰り返し動作させてください。
- プログラムは wscript.exe に先ほどの起動スクリプトを引数として与えてください。
- 繰り返し間隔はRA有効期限の1800秒を根拠にすると30分ですが、1時間でも特に不都合は感じません。


これを仕込んでおくことで、ログオンすると自動でプリフィクスをリースして設定してくれます。Hyper-Vマネージャがインストールされていれば vEthernet (Default Switch)は起動時に生成されるようなので、クライアントHyper-VやWindows Sandbox も普通にIPv6通信ができるようになると思います。もう一つ副作用として2021年9月28日時点のWindows11 + Windows Terminal だとWSLの初回起動に失敗するのですが、一度インスタンスを起動させているのでこの問題に遭遇せずに済みます。

## おわりに
本題のタスクへの登録よりも改善点の解説が多くなりましたが、そこそこ使えるレベルにはなったと思います。利用に際して条件を選ぶところはありますが、つながってさえしまえば快適です。Windowsホストについてはパブリックネットワークのファイヤーウオールルールを書かないといけませんが、それ以外に特に制限もありませんから自由に楽しめると思います。逆にいうと管理外のネットワークで接続する際には丸裸の状態で外部ネットワークに接続してしまうので、各仮想インスタンスそれぞれにパケットフィルタ等の対応が必要ですから注意してください。