WireGuardは専用ハードウェアが無くても十分高速なトンネルを構築できる素晴らしいインフラプロトコルです。昨今では常時VPN使用を提案するようなケースもあるようですが、やはり必要な時に繋がり、必要ない時には遠慮するような仕組みが効率的です。そこでWindowsのタスクスケジューラとPowerShellを組み合わせて接続切断の自動化を実現してみます。
WireGuardのサービス登録
Windows用WireGuardクライアントはGUIで設定するのが基本だと思いますが、サービスとしても登録可能です。そしてサービスであればスクリプトから簡単に接続切断のコントロールができます。これは標準のVPNも同様ですね。WireGuardを中心に話を進めますが、標準のL2TP/IPSecやIKEv2なども同様にコントロールできると思います。ただ、WireGuardはL2TP/IPSecと異なり回線品質によって切断されたりしないのでより利便性が高いと考えています。
では、さっそく登録してみましょう。
wireguard /installtunnelservice C:\WireGuard\PEER.conf
これでWireGuardTunnel$PEER
というサービスが生成されます。PEER.confの形式については https://www.wireguard.com/ 等を参照して作ってください。本稿ではWireGuardのクライアント・サーバー接続までは完了していることを前提としています。
登録が終われば、
Start-Service WireGuardTunnel$PEER
Stop-Service WireGuardTunnel$PEER
等のコマンドで接続・切断は自由自在です。
簡単に接続・切断が行えるもののできれば自動が良いですよね?
接続・切断の手順
自動接続の手順をPowerShellを使って書いていきます。
具体的な処理としては以下のようなルールを想定します。
- 一般的なサイトへ疎通しているか?(=ネットワークは接続されている)
- ローカルでしか接続しえないサーバーへ接続できるか?
- 接続できればローカルなので切断・接続できなければVPNを繋ぐ
このような処理を組み立てます。
インターネット疎通確認
8.8.8.8つまりGoogleのDNS over HTTPSサーバーへの疎通を確認します。Public DNSなのでかなりの耐性があるはずということで例として使わせていただきます。通常は自身が所有している外部からアクセス可能なサーバーを指定するのが良いでしょう。
for($i=0; $i -lt 10; $i++){curl.exe -s --connect-timeout 2 https://8.8.8.8;if ($?) {_connect;break;};sleep $($i*3)};
8.8.8.8へアクセスできるまでループを繰り返します。アクセスできれば_connectに定義された関数を呼び出して終了です。
GoogleのDNSサーバーへアクセスできた(=インターネットには接続されている)状況が確認できれば次のステップです。
function _connect() {
$STATUS=(get-service 'WireGuardTunnel$PEER').Status;
if ('Running' -eq $STATUS) {
$route=Get-NetRoute -DestinationPrefix '0.0.0.0/0';
$next=([String](find-netroute -RemoteIPAddress 192.168.19.42 -InterfaceIndex ($route.ifindex)).NextHop).Trim();
$route|New-NetRoute -DestinationPrefix 192.168.19.42/32 -PolicyStore ActiveStore -NextHop $next;
}
curl.exe -s --connect-timeout 2 192.168.19.42;$CONNECT=$?;
Get-NetRoute|?{$_.DestinationPrefix -eq '192.168.19.42/32'}|Remove-NetRoute -confirm:$false;
if ($CONNECT -and 'Running' -eq $STATUS) {
stop-service 'WireGuardTunnel$PEER';toast('VPN DISCONNECTED');
}
elseif (!$CONNECT -and 'Running' -ne $STATUS) {
start-service 'WireGuardTunnel$PEER';toast('VPN conneted');
}
}
順番に見ていきます。
内部サーバー疎通確認
$STATUS=(get-service 'WireGuardTunnel$PEER').Status;
WireGuardTunnel$PEER
サービスの稼働状況を調べます。稼働していればRunning
となるはずです。Running状態であれば、サービスが稼働している=VPN接続中であるということです。
この状態に応じて制御するのですが、そもそもVPNが必要なのかどうかを調べます。つまり、ローカル接続でのサーバーへの疎通を調べます。
$route=Get-NetRoute -DestinationPrefix '0.0.0.0/0';
まずデフォルトゲートウェイの経路を確認しておきます。
$next=([String](find-netroute -RemoteIPAddress 192.168.19.42 -InterfaceIndex ($route.ifindex)).NextHop).Trim();
ここでは仮に192.168.19.42(閉じたローカル環境でのみアクセス可能なWEBサーバーを指定してください)への経路を調べます。但し、VPN接続中だからアクセス可能という条件は排除したいので、デフォルトゲートへの接続のあるインタフェース(=物理インタフェース)上でのゲートウェイを探します。多くの場合デフォルトゲートウェイそのもので良いでしょうが、個別の経路が付けられているケースも考慮しておきます。
$route|New-NetRoute -DestinationPrefix 192.168.19.42/32 -PolicyStore ActiveStore -NextHop $next;
そして、物理インタフェース上でのゲートとなるアドレス(=VPNを繋いでなければ通過するであろうゲートウェイ)を必ず通過するように経路を付けます。
curl.exe -s --connect-timeout 2 192.168.19.42;
$CONNECT=$?;
この状態でcurl.exeを使って接続を試みます。
curl.exeのexitコードを$CONNECT変数へ控えます。つまり接続できたのかできなかったのかを控えます。
接続できたということは、VPNが無くても接続できる状態。
接続できなかったということはVPNが必要な状態だとわかります。
Get-NetRoute|?{$_.DestinationPrefix -eq '192.168.19.42/32'}|Remove-NetRoute -confirm:$false;
確認が終わったので、余計な経路は削除しておきます。
接続・切断
調査した現在のVPN接続状況、そもそも必要かどうかという二つの情報に基づいて接続、あるいは切断を行います。
if ($CONNECT -and 'Running' -eq $STATUS) {
stop-service 'WireGuardTunnel$PEER';toast('VPN DISCONNECTED');
}
確認サーバーへ接続可能かつWireGuard接続中の場合、切断します。
elseif (!$CONNECT -and 'Running' -ne $STATUS) {
start-service 'WireGuardTunnel$PEER';toast('VPN conneted');
}
逆に確認サーバーへはアクセスできず、WireGuardが切断されている場合には接続します。
それ以外の場合には現状維持です。
通知
接続出来たら(切断されたら)出来たでそれは知りたいですよね。なのでトーストに通知します。
ただし、当該サーバーへ接続できない場所が普段の生活拠点の場合(=VPNを繋ぐのが当たり前の生活)、PCを起動すると必ずVPNに接続することになります。PCを起動するたびにVPN接続したという通知を受け取るのも結構煩雑です。そこで起動してから10分間は通知を行わないようにします。TicCountはWindowsが起動してからの経過時間をミリ秒で示していますので1,000倍して秒になおし、さらに60倍して分、10倍して10分を確認します。
if ([Environment]::TickCount -lt 10 * 60 * 1000) {return;};
そこから後は接続したのか切断したのかをトーストで通知します。わかりやすいように画像で通知するのも良いでしょう
タスクスケジューラーへの登録
全体像としてはこのような処理の流れをタスクスケジューラーから実行します。
この時のトリガーは以下のものを設定しています。
Microsoft-Windows-NetworkProfile ID: 100000
Microsoft-Windows-Power-Troubleshooter ID:1
Microsoft-Windows-Power-Troubleshooter ID:507
ネットワークイベント(=ネットワークが接続された)だけでも大丈夫かもしれませんが、Powe-TroubleshooterのID:1は高速シャットダウンからの復帰、ID:507はconnected standbyからの復帰イベントです。
あとは、先ほどまで説明したPowerShellスクリプトを実行すれば良いのですが、タスクスケジューラーからPowerShellスクリプトを実行するとどうしてもウィンドウが見えてしまいます。これを手軽に回避するにはVBScriptやJScriptでラッピングするしか方法は無さそうです。このため実行もwscript c:\WireGuard\connect.js
という形でwscriptコマンドを使って実行する必要があります。
参考)接続・切断スクリプト
script="function toast($msg) {"
script=script + "if ([Environment]::TickCount -lt 10 * 60 * 1000) {return;};"
script=script + "$template=[Windows.UI.Notifications.ToastNotificationManager,Windows.UI.Notifications, ContentType = WindowsRuntime]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType, Windows.UI.Notifications, ContentType = WindowsRuntime]::ToastText01);"
script=script + "$template.GetElementsByTagName('text').Item(0).InnerText=$msg;"
script=script + "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('{6D809377-6AF0-444B-8957-A3773F02200E}\\WireGuard\\wireguard.exe').Show($template);"
script=script + "};"
script=script + "function _connect() {"
script=script + "$STATUS=(get-service 'WireGuardTunnel$PEER').Status;"
script=script + "if ('Running' -eq $STATUS) {$route=Get-NetRoute -DestinationPrefix '0.0.0.0/0';$next=([String](find-netroute -RemoteIPAddress 192.168.19.42 -InterfaceIndex ($route.ifindex)).NextHop).Trim();"
script=script + "$route|New-NetRoute -DestinationPrefix 192.168.19.42/32 -PolicyStore ActiveStore -NextHop $next;};"
script=script + "curl.exe -s --connect-timeout 2 192.168.19.42;$CONNECT=$?;"
script=script + "Get-NetRoute|?{$_.DestinationPrefix -eq '192.168.19.42/32'}|Remove-NetRoute -confirm:$false;"
script=script + "if ($CONNECT -and 'Running' -eq $STATUS) {stop-service 'WireGuardTunnel$PEER';toast('VPN DISCONNECTED');}"
script=script + "elseif (!$CONNECT -and 'Running' -ne $STATUS) {start-service 'WireGuardTunnel$PEER';toast('VPN conneted');}"
script=script + "};"
script=script + "for($i=0; $i -lt 10; $i++){curl.exe --connect-timeout 2 https://8.8.8.8;if ($?) {_connect;break;};sleep $($i*3)};"
ws = WScript.CreateObject("WScript.Shell");
ws.run("powershell -command \"" + script + "\"", 0, true);