Scripts/Test-FortiClientIKEv2Readiness.ps1

<#
.SYNOPSIS
    Pre-connect / post-failure diagnostic for FortiClient IKEv2 on Windows ARM.
 
.DESCRIPTION
    Captures the local-stack state that isn't visible in FortiClient's own logs:
      - CPU architecture / OS build / FortiClient version
      - IKEEXT (Windows built-in IKE/AuthIP) service state - known port 500/4500 conflict source
      - UDP 500 / 4500 binding ownership
      - Fortinet / WAN Miniport virtual adapter presence and status
      - Recent System + Application event log entries tied to RRAS, IKEEXT, or FortiClient crashes
      - FortiClient VPN-related service states
 
    Run this AFTER a failed IKEv2 attempt (before rebooting/retrying) so the event log
    window still contains the relevant entries. Pass -SinceMinutes to widen/narrow the
    event log lookback relative to now.
 
    Part of the FortiClient Connection Troubleshooting Toolkit (see FCT-Toolkit.ps1 in this same
    folder for the menu-driven entry point) - also fully usable standalone.
 
.PARAMETER SinceMinutes
    How far back to search event logs, in minutes. Default 30.
 
.PARAMETER OutputPath
    Where to write the report. Defaults under the toolkit's shared output folder
    (C:\Admin\FCTToolkit\Output\Reports).
 
.EXAMPLE
    .\Test-FortiClientIKEv2Readiness.ps1 -SinceMinutes 60
#>


[CmdletBinding()]
param(
    [int]$SinceMinutes = 30,
    [string]$OutputPath = "C:\Admin\FCTToolkit\Output\Reports\FortiClient_IKEv2_Readiness_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt"
)

$ErrorActionPreference = 'SilentlyContinue'
$OutputDir = Split-Path -Path $OutputPath -Parent
if ($OutputDir -and -not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null }

$sb = New-Object System.Text.StringBuilder
function Write-Section {
    param([string]$Title)
    $null = $sb.AppendLine("`n===== $Title =====")
    Write-Host "`n===== $Title =====" -ForegroundColor Cyan
}
function Write-Line {
    param([string]$Text)
    $null = $sb.AppendLine($Text)
    Write-Host $Text
}

$since = (Get-Date).AddMinutes(-$SinceMinutes)

# ---------------------------------------------------------------------------
Write-Section "System / Architecture"
$cs = Get-CimInstance Win32_ComputerSystem
$os = Get-CimInstance Win32_OperatingSystem
Write-Line "Computer: $($cs.Name)"
Write-Line "Manufacturer: $($cs.Manufacturer) $($cs.Model)"
Write-Line "Processor Arch: $($env:PROCESSOR_ARCHITECTURE)"
Write-Line "OS: $($os.Caption) Build $($os.BuildNumber)"
Write-Line "OS Architecture: $($os.OSArchitecture)"

# ---------------------------------------------------------------------------
Write-Section "FortiClient Install / Version"
$fcReg = Get-ItemProperty "HKLM:\SOFTWARE\Fortinet\FortiClient*" -ErrorAction SilentlyContinue
$fcUninstall = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" |
    Where-Object { $_.DisplayName -like "*FortiClient*" }
if ($fcUninstall) {
    foreach ($f in $fcUninstall) {
        Write-Line "Product: $($f.DisplayName)"
        Write-Line "Version: $($f.DisplayVersion)"
        Write-Line "InstallLocation: $($f.InstallLocation)"
    }
} else {
    Write-Line "Could not find FortiClient in Uninstall registry keys."
}
$exePath = Get-ChildItem "C:\Program Files\Fortinet\FortiClient\fortivpn.exe" -ErrorAction SilentlyContinue
if ($exePath) {
    $ver = (Get-Item $exePath.FullName).VersionInfo
    Write-Line "fortivpn.exe: $($ver.FileVersion) (arch check below)"
    $peHeader = [System.Reflection.AssemblyName]::GetAssemblyName($exePath.FullName) 2>$null
}

# ---------------------------------------------------------------------------
Write-Section "IKEEXT / RasMan Service State (known port 500/4500 conflict source)"
foreach ($svc in @('IKEEXT','RemoteAccess','RasMan','PolicyAgent')) {
    $s = Get-Service -Name $svc -ErrorAction SilentlyContinue
    if ($s) {
        Write-Line ("{0,-14} Status={1,-8} StartType={2}" -f $s.Name, $s.Status, $s.StartType)
    } else {
        Write-Line "$svc`: service not found"
    }
}

# ---------------------------------------------------------------------------
Write-Section "FortiClient Service State"
Get-Service | Where-Object { $_.DisplayName -like "*Forti*" } | ForEach-Object {
    Write-Line ("{0,-30} Status={1,-8} StartType={2}" -f $_.Name, $_.Status, $_.StartType)
}

# ---------------------------------------------------------------------------
Write-Section "UDP 500 / 4500 Port Ownership"
foreach ($port in 500,4500) {
    $eps = Get-NetUDPEndpoint -LocalPort $port -ErrorAction SilentlyContinue
    if ($eps) {
        foreach ($ep in $eps) {
            $procName = (Get-Process -Id $ep.OwningProcess -ErrorAction SilentlyContinue).ProcessName
            Write-Line "UDP $port -> PID $($ep.OwningProcess) ($procName) local=$($ep.LocalAddress)"
        }
    } else {
        Write-Line "UDP $port -> no listener bound"
    }
}

# ---------------------------------------------------------------------------
Write-Section "Virtual / VPN Adapters"
Get-NetAdapter | Where-Object {
    $_.InterfaceDescription -match 'Fortinet|WAN Miniport|IKEv2|IPsec|Forti'
} | Select-Object Name, InterfaceDescription, Status, MacAddress | ForEach-Object {
    Write-Line ("{0,-20} {1,-40} Status={2}" -f $_.Name, $_.InterfaceDescription, $_.Status)
}
Write-Line "`n(If nothing listed above, the IPsec virtual adapter never came up for this attempt.)"

# ---------------------------------------------------------------------------
Write-Section "System Event Log - RRAS / IKEEXT / RasClient (last $SinceMinutes min)"
Get-WinEvent -FilterHashtable @{
    LogName   = 'System'
    StartTime = $since
} -ErrorAction SilentlyContinue |
    Where-Object { $_.ProviderName -match 'RasClient|RemoteAccess|IKEEXT|Rasman' -or $_.Id -in 20227,13801,13868 } |
    Select-Object TimeCreated, Id, ProviderName, LevelDisplayName, Message |
    ForEach-Object {
        Write-Line "`n[$($_.TimeCreated)] Id=$($_.Id) [$($_.ProviderName)] $($_.LevelDisplayName)"
        Write-Line "$($_.Message)"
    }

# ---------------------------------------------------------------------------
Write-Section "Application Event Log - FortiClient (last $SinceMinutes min)"
Get-WinEvent -FilterHashtable @{
    LogName   = 'Application'
    StartTime = $since
} -ErrorAction SilentlyContinue |
    Where-Object { $_.ProviderName -match 'Forti' -or $_.Message -match 'fortivpn|FortiClient' } |
    Select-Object TimeCreated, Id, ProviderName, LevelDisplayName, Message |
    ForEach-Object {
        Write-Line "`n[$($_.TimeCreated)] Id=$($_.Id) [$($_.ProviderName)] $($_.LevelDisplayName)"
        Write-Line "$($_.Message)"
    }

# ---------------------------------------------------------------------------
Write-Section "Driver Signing / Faulting Module Check (Application log, last $SinceMinutes min)"
Get-WinEvent -FilterHashtable @{
    LogName   = 'Application'
    StartTime = $since
} -ErrorAction SilentlyContinue |
    Where-Object { $_.Id -in 1000,1001 -and $_.Message -match 'forti' } |
    Select-Object TimeCreated, Id, Message |
    ForEach-Object {
        Write-Line "`n[$($_.TimeCreated)] Id=$($_.Id)"
        Write-Line "$($_.Message)"
    }

# ---------------------------------------------------------------------------
$sb.ToString() | Out-File -FilePath $OutputPath -Encoding UTF8
Write-Host "`nReport saved to: $OutputPath" -ForegroundColor Green