PowerShellを使ってVPNの接続・切断を効率的に行う方法という問いには、保存済みVPN名を確認してから接続し、ConnectionStatusだけでなくルートと名前解決も検証するという方法で答えます。Get-VpnConnectionは保存済みプロファイルを読み、実接続はrasdial等の接続経路を使う。ユーザー接続と全ユーザー接続では参照範囲が異なる。この記事ではGet-VpnConnectionのName、ServerAddress、TunnelType、AllUserConnectionの範囲で接続先を一意にするを判断軸にし、実行前の確認、記事固有のコード、合否判定、戻し方を一続きで示します。
VPNプロファイルの新規作成ではなく、既に承認された接続の安全な利用に絞る。完了は「指定プロファイルがConnectedとなり、意図した内部宛先への経路とDNS応答が得られる」と定義します。対象が取れない場合は「プロファイル0件ならユーザー範囲、-AllUserConnection、端末管理配布、名前の全角半角を確認する」として切り分け、推測で成功扱いにしません。
VPN名とサーバーを接続前に固定
VPN名とサーバーを接続前に固定では、Windows VPNの接続と切断の対象を「Get-VpnConnectionのName、ServerAddress、TunnelType、AllUserConnectionの範囲で接続先を一意にする」という単位で扱います。Get-VpnConnectionは保存済みプロファイルを読み、実接続はrasdial等の接続経路を使う。ユーザー接続と全ユーザー接続では参照範囲が異なる。対象が複数なら表示名の部分一致で先頭を採らず、一意になる条件を追加します。
Windows VPNの接続と切断を始める前に、PowerShellの版、コマンドの提供元、必要権限、管理ポリシーの有無を確認します。権限不足と対象なしは意味が異なるため、例外を0件へ置き換えません。
保存済みプロファイルを安全に使う
保存済みVPN名を確認してから接続し、ConnectionStatusだけでなくルートと名前解決も検証する。Windows VPNの接続と切断ではこの進め方により、操作したという事実ではなく、期待する状態へ到達したかでタイトルの問いへ答えられます。VPNプロファイルの新規作成ではなく、既に承認された接続の安全な利用に絞る。
保存済みプロファイルを安全に使うの合格条件は、指定プロファイルがConnectedとなり、意図した内部宛先への経路とDNS応答が得られることです。作業時刻、実行ユーザー、端末名を添え、判断に使った値が後から追える形にします。
ConnectionStatusを開始点として採取
ConnectionStatusを開始点として採取は変更前の基準点です。Get-VpnConnectionのName、ServerAddress、TunnelType、AllUserConnectionの範囲で接続先を一意にするを出力に含め、取得時刻と一緒に保存します。値だけを切り取ると別対象との比較になるため、識別列を省きません。
$vpnName = 'Office VPN'
$allUserConnection = $false
$internalFqdn = 'intranet.contoso.example'
$internalPort = 443
$expectedVpnInterfaceAlias = 'Office VPN'
$ordinaryFqdn = 'www.microsoft.com'
$ordinaryPort = 443
$getParams = @{ Name=$vpnName; ErrorAction='Stop' }
if ($allUserConnection) { $getParams.AllUserConnection = $true }
$profiles = @(Get-VpnConnection @getParams)
if ($profiles.Count -ne 1) { throw "VPNプロファイルを一意にできません: $($profiles.Count)件" }
$profile = $profiles[0]
$profileBaseline = [pscustomobject]@{
Name=[string]$profile.Name; ServerAddress=[string]$profile.ServerAddress; TunnelType=[string]$profile.TunnelType
AllUserConnection=[bool]$profile.AllUserConnection; SplitTunneling=[bool]$profile.SplitTunneling
AuthenticationMethod=@($profile.AuthenticationMethod); EncryptionLevel=[string]$profile.EncryptionLevel
ConnectionStatus=[string]$profile.ConnectionStatus
}
$preVpnRouteKeys = @(Get-NetRoute -InterfaceAlias $expectedVpnInterfaceAlias -ErrorAction SilentlyContinue | ForEach-Object { "$($_.DestinationPrefix)|$($_.NextHop)|$($_.InterfaceIndex)" } | Sort-Object)
[pscustomobject]@{ Profile=$profileBaseline; RouteBaseline=($preVpnRouteKeys -join ';'); CapturedAt=Get-Date }
if ($profileBaseline.ConnectionStatus -eq 'Connected') { Write-Warning '既に接続中です。接続操作を重ねず、現在の利用者と経路を確認してください。' }
Get-VpnConnectionは保存済みプロファイルを読み、実接続はrasdial等の接続経路を使う。ユーザー接続と全ユーザー接続では参照範囲が異なる。出力が多い場合も最初から無理に一件へ絞らず、候補数と除外理由を残してから対象を決めます。
接続後に経路とDNSを照合
接続後に経路とDNSを照合では同じ対象を別経路でもう一度読みます。判定したいのは「コマンドが終了したか」ではなく、指定プロファイルがConnectedとなり、意図した内部宛先への経路とDNS応答が得られるかどうかです。
if ($profileBaseline.ConnectionStatus -eq 'Connected') { throw '既存接続を上書きしないため停止します。' }
$approvalToken = "OPEN-VPN-SETTINGS $($profileBaseline.Name) SERVER=$($profileBaseline.ServerAddress) TYPE=$($profileBaseline.TunnelType)"
if ((Read-Host "Windows VPN画面を開く前に $approvalToken を入力") -ne $approvalToken) { throw '中止しました。' }
$profileRebindRows = @(Get-VpnConnection @getParams)
if ($profileRebindRows.Count -ne 1) { throw '承認後にVPN profileを一意にできません。' }
$profileRebind = $profileRebindRows[0]
$profileRebindIdentity = [pscustomobject]@{
Name=[string]$profileRebind.Name; ServerAddress=[string]$profileRebind.ServerAddress; TunnelType=[string]$profileRebind.TunnelType
AllUserConnection=[bool]$profileRebind.AllUserConnection; SplitTunneling=[bool]$profileRebind.SplitTunneling
AuthenticationMethod=@($profileRebind.AuthenticationMethod); EncryptionLevel=[string]$profileRebind.EncryptionLevel
ConnectionStatus=[string]$profileRebind.ConnectionStatus
}
if ((ConvertTo-Json $profileRebindIdentity -Depth 5 -Compress) -ne (ConvertTo-Json $profileBaseline -Depth 5 -Compress)) {
throw '接続直前のName/Server/Tunnel/Auth/Split/Status identityが変わりました。'
}
[pscustomobject]@{ Name=$vpnName; Server=$profileBaseline.ServerAddress; ExecuteAutomatically=$false; ExpectedInterface=$expectedVpnInterfaceAlias }
Write-Host 'Windowsの「設定 > ネットワークとインターネット > VPN」で同じ名前とサーバーのprofileを選び、「接続」を1回だけ押します。'
Write-Warning '予期しない証明書警告、資格情報要求、別サーバー名が表示されたら中止します。遠隔管理中で代替経路がない場合も実行しません。'
プロファイル0件ならユーザー範囲、-AllUserConnection、端末管理配布、名前の全角半角を確認する。Windows VPNの接続と切断の期待値と実測値が一致しないときは追加変更を重ねず、対象識別、権限、ポリシー、時間差の順で原因を分けます。
rasdialの終了表示を読み違えない
rasdialの終了表示を読み違えないでは、保存済みVPN名を確認してから接続し、ConnectionStatusだけでなくルートと名前解決も検証する。Windows VPNの接続と切断の例中にある名前、パス、ID、時刻はサンプルなので、そのまま本番へ貼らず、直前の読み取り結果から承認値を入れます。
$expectedOrdinaryInterfaceAlias='Ethernet'
function Get-IttripVpnIdentity($Row){[pscustomobject]@{Name=[string]$Row.Name;ServerAddress=[string]$Row.ServerAddress;TunnelType=[string]$Row.TunnelType;AllUserConnection=[bool]$Row.AllUserConnection;SplitTunneling=[bool]$Row.SplitTunneling;AuthenticationMethod=@($Row.AuthenticationMethod);EncryptionLevel=[string]$Row.EncryptionLevel}}
Read-Host 'Windows VPN画面で手動接続後、Enterでconnect acceptanceとmandatory cleanupを開始します'
$acceptanceErrors=[Collections.Generic.List[string]]::new();$connectedRows=@();$accepted=$false
try{$connectedRows=@(Get-VpnConnection @getParams);if($connectedRows.Count -ne 1 -or [string]$connectedRows[0].ConnectionStatus -ne 'Connected'){throw 'Connectedを一意に確認できません。'};if((ConvertTo-Json (Get-IttripVpnIdentity $connectedRows[0]) -Depth 5 -Compress) -ne (ConvertTo-Json (Get-IttripVpnIdentity $profile) -Depth 5 -Compress)){throw '接続後profile identityがbaselineと一致しません。'};$dnsAnswers=@(Resolve-DnsName -Name $internalFqdn -Type A -ErrorAction Stop|Where-Object IPAddress);$probe=Test-NetConnection -ComputerName $internalFqdn -Port $internalPort -InformationLevel Detailed -WarningAction SilentlyContinue;$accepted=$dnsAnswers.Count -gt 0 -and $probe.TcpTestSucceeded -and $probe.InterfaceAlias -eq $expectedVpnInterfaceAlias;if(-not $accepted){throw 'DNS/TCP/VPN interface acceptanceが不一致です。'}}catch{$acceptanceErrors.Add($_.Exception.Message)}
Write-Host 'connect acceptanceの成否や直前readの成否にかかわらず、Windows VPN画面で同じprofileの「切断」を1回押します。profileは削除しません。';Read-Host '手動切断後にEnterでbounded exact cleanupを開始します'
$readErrors=[Collections.Generic.List[string]]::new();$disconnectedExact=$false;$lastStatus='<not-read>';$lastIdentityExact=$false
for($attempt=1;$attempt -le 8;$attempt++){
try{$rows=@(Get-VpnConnection @getParams);if($rows.Count -ne 1){throw "profile count=$($rows.Count)"};$lastIdentityExact=(ConvertTo-Json (Get-IttripVpnIdentity $rows[0]) -Depth 5 -Compress) -eq (ConvertTo-Json (Get-IttripVpnIdentity $profile) -Depth 5 -Compress);$lastStatus=[string]$rows[0].ConnectionStatus;$disconnectedExact=$lastIdentityExact -and $lastStatus -eq 'Disconnected'}catch{$readErrors.Add("attempt=$($attempt):$($_.Exception.Message)");$disconnectedExact=$false}
if($disconnectedExact){break};Start-Sleep -Seconds 2
}
$afterRouteKeys=@(Get-NetRoute -InterfaceAlias $expectedVpnInterfaceAlias -ErrorAction SilentlyContinue|ForEach-Object{"$($_.DestinationPrefix)|$($_.NextHop)|$($_.InterfaceIndex)"}|Sort-Object);$routesExact=($afterRouteKeys-join '|') -eq (($preVpnRouteKeys|Sort-Object)-join '|')
$ordinary=Test-NetConnection -ComputerName $ordinaryFqdn -Port $ordinaryPort -InformationLevel Detailed -WarningAction SilentlyContinue;$ordinaryExact=$ordinary.TcpTestSucceeded -and $ordinary.InterfaceAlias -eq $expectedOrdinaryInterfaceAlias -and $ordinary.InterfaceAlias -ne $expectedVpnInterfaceAlias
$cleanupExact=$disconnectedExact -and $routesExact -and $ordinaryExact
[pscustomobject]@{WorkflowPhase='ConnectThenMandatoryDisconnect';ConnectAccepted=$accepted;DisconnectStatus=$lastStatus;ProfileIdentityExact=$lastIdentityExact;RoutesRestoredExact=$routesExact;OrdinaryInterface=$ordinary.InterfaceAlias;OrdinaryPathExact=$ordinaryExact;ReadErrors=@($readErrors);CleanupExact=$cleanupExact}
if(-not $cleanupExact){throw 'successful exact Disconnected read、full profile identity、baseline routes、approved non-VPN ordinary pathが揃いません。'}
if(-not $accepted){throw "connect acceptance失敗後のmandatory cleanupはexact確認済みです: $($acceptanceErrors -join '|')"}
パスワードをスクリプトやコマンド履歴へ直接書かない。接続失敗を無限再試行してアカウントをロックしない。Windows VPNの接続と切断でプレビュー対応コマンドを使える場合はWhatIfを先に実行し、非対応の操作は対象一覧と引数を画面へ出して人が承認してから一度だけ実行します。
切断は同じ接続名へ限定
同じVPN名を指定して切断し、追加された経路が消え、通常回線が戻ったことを確認する。復旧操作にも同じ識別条件を使い、名前が似た別対象へ戻し処理を適用しません。
- Windows VPNの接続と切断の変更前値と取得時刻
- 復旧対象: Get-VpnConnectionのName、ServerAddress、TunnelType、AllUserConnectionの範囲で接続先を一意にする
- 復旧後の判定: 指定プロファイルがConnectedとなり、意図した内部宛先への経路とDNS応答が得られる
- 再実行を止める条件: パスワードをスクリプトやコマンド履歴へ直接書かない。接続失敗を無限再試行してアカウントをロックしない
資格情報をコードへ書かない
パスワードをスクリプトやコマンド履歴へ直接書かない。接続失敗を無限再試行してアカウントをロックしない。資格情報をコードへ書かないに該当したら、警告を消して継続するのではなく、どの条件で止まったかを記録します。
プロファイル0件ならユーザー範囲、-AllUserConnection、端末管理配布、名前の全角半角を確認する。Windows VPNの接続と切断ではエラー本文、FullyQualifiedErrorId、対象ID、直前に成功した段階を残すと、別担当者が安全な地点から調査できます。
運用監視で再接続ループを防ぐ
接続時刻、VPN名、終了コード、割当IP、検証した内部FQDNを記録し、機密の資格情報は記録しない。Windows VPNの接続と切断を繰り返す場合は、正常、対象なし、要承認、失敗を異なる終了状態として記録し、前回値との比較だけで異常を決めません。
| 運用監視で再接続ループを防ぐの識別軸 | Get-VpnConnectionのName、ServerAddress、TunnelType、AllUserConnectionの範囲で接続先を一意にする |
| 採用する実測 | 指定プロファイルがConnectedとなり、意図した内部宛先への経路とDNS応答が得られる |
| 0件時の扱い | プロファイル0件ならユーザー範囲、-AllUserConnection、端末管理配布、名前の全角半角を確認する |
| 保留にする兆候 | パスワードをスクリプトやコマンド履歴へ直接書かない。接続失敗を無限再試行してアカウントをロックしない |
Windows VPNの接続と切断の実行記録には、開始前の対象候補、採用した識別値、実行したコード、終了後の実測、除外した候補と理由を同じ作業番号で残します。特に「Get-VpnConnectionのName、ServerAddress、TunnelType、AllUserConnectionの範囲で接続先を一意にする」を省くと、後日の再確認で別対象の値を比較するおそれがあります。画面コピーだけでなく、日時と端末名を含む構造化した出力も保存します。
PowerShellを使ってVPNの接続・切断を効率的に行う方法を定期手順へ組み込む場合も、初回は対話的に候補を確認します。正常時は「指定プロファイルがConnectedとなり、意図した内部宛先への経路とDNS応答が得られる」、判定不能時は「プロファイル0件ならユーザー範囲、-AllUserConnection、端末管理配布、名前の全角半角を確認する」、中止時は「パスワードをスクリプトやコマンド履歴へ直接書かない。接続失敗を無限再試行してアカウントをロックしない」をそれぞれ別の結果として扱います。これにより、0件や例外を都合よく成功へ丸めず、次の担当者が同じ対象と条件で追試できます。
修正後コードの合格条件:VPN名のConnectedだけでは完了にせず、承認済み内部FQDNが解決でき、指定TCPポートへ到達し、Test-NetConnectionが期待VPNインターフェイスを選んだことを同時に確認します。どれか一つでも不一致なら接続名を限定して切断します。
再修正後は接続確認だけで終わりません。正常系でも同じVPN名を明示承認して切断し、rasdial終了値、Disconnected、追加経路消失、通常回線のTCP到達をそろえて初めて接続・切断完了とします。
安全版 r4a-takeover-20260719:rasdialによる自動接続・自動切断を廃止しました。VPN profileのName/Server/Tunnel/Auth/Statusを直前に再確認し、WindowsのVPN画面で一回だけ接続・切断します。DNS、TCP、InterfaceAlias、残留routeはPowerShellで読み取り検証します。
独立監査追補 r4a-r6-20260719:connect acceptance失敗時もGUI disconnectをmandatoryにします。読取errorやConnecting等でloopを抜けず、full profile identity、Disconnected、baseline route、approved non-VPN ordinary interfaceをすべて必須にします。
公式情報・参考資料
Windows VPNの接続と切断で使うコマンド名、引数、対応環境は次のMicrosoft一次資料で確認しました。記事の確認日は2026年7月17日です。OSやモジュール更新後は、実行端末のGet-Helpと併せて再確認してください。

コメント