From 4aff4ba8c072fe955930f3840deed46781b2f7e2 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Mon, 12 Jan 2026 20:20:48 -0700 Subject: [PATCH] Continued WireGuard troubleshooting --- Borealis.ps1 | 741 +++++++++++++++++++---- Data/Agent/Roles/role_WireGuardTunnel.py | 250 +++++++- Docs/Codex/WireGuard_Troubleshooting.md | 106 ++++ 3 files changed, 936 insertions(+), 161 deletions(-) create mode 100644 Docs/Codex/WireGuard_Troubleshooting.md diff --git a/Borealis.ps1 b/Borealis.ps1 index 9afa6adc..5f91bae7 100644 --- a/Borealis.ps1 +++ b/Borealis.ps1 @@ -523,6 +523,10 @@ $wireGuardInstallerDir = Join-Path $depsRoot 'VPN_Tunnel_Adapter' $wireGuardBootstrapperName = 'wireguard-installer.exe' $wireGuardBootstrapperPath = Join-Path $wireGuardInstallerDir $wireGuardBootstrapperName $wireGuardMsiVersion = '0.5.3' +$wireGuardTunnelLegacyName = 'BorealisWireGuardTunnel' +$wireGuardTunnelNameInternal = 'Borealis' +$wireGuardTunnelNameFriendly = 'Borealis' +$wireGuardTunnelBootstrapAddress = '169.254.255.254/32' $wireGuardMsiFiles = @{ 'X64' = "wireguard-amd64-$wireGuardMsiVersion.msi" 'AMD64' = "wireguard-amd64-$wireGuardMsiVersion.msi" @@ -702,6 +706,10 @@ function Install_Agent_Dependencies { ServicePresent = $false DriverPresent = $false DriverPaths = @() + AdapterPresent = $false + AdapterNames = @() + DedicatedAdapterPresent = $false + DedicatedAdapterNames = @() } $exeCandidates = @( @@ -748,6 +756,29 @@ function Install_Agent_Dependencies { $state.DriverPaths = $driverHits | Select-Object -Unique } + try { + $adapters = Get-WireGuardAdapters + if ($adapters) { + $state.AdapterPresent = $true + $state.AdapterNames = $adapters | Select-Object -ExpandProperty Name -Unique + $state.DriverPresent = $true + $state.Installed = $true + } + + $dedicated = @() + foreach ($adapter in ($adapters | Where-Object { $_ })) { + if (Test-WireGuardAdapterName -Adapter $adapter -ExpectedName $wireGuardTunnelNameFriendly) { + $dedicated += $adapter + } + } + if ($dedicated.Count -gt 0) { + $state.DedicatedAdapterPresent = $true + $state.DedicatedAdapterNames = $dedicated | Select-Object -ExpandProperty Name -Unique + $state.DriverPresent = $true + $state.Installed = $true + } + } catch {} + $uninstallRoots = @( 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall' @@ -836,29 +867,51 @@ function Install_Agent_Dependencies { ) $pair = @{ PrivateKey = $null; PublicKey = $null; Source = $null } - $cliCandidates = @() - if ($WgExe) { $cliCandidates += $WgExe } - if ($WireGuardExe) { $cliCandidates += $WireGuardExe } + $wgCli = $null + if ($WgExe -and (Test-Path $WgExe -PathType Leaf)) { $wgCli = $WgExe } - foreach ($cli in $cliCandidates) { - if (-not $cli -or -not (Test-Path $cli -PathType Leaf)) { continue } + $priv = $null + if ($wgCli) { + try { $priv = (& $wgCli genkey) } catch {} + if ($priv) { + $priv = $priv.Trim() + $pair.PrivateKey = $priv + $pair.Source = $wgCli + try { + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $wgCli + $psi.Arguments = 'pubkey' + $psi.RedirectStandardInput = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + if ($psi.PSObject.Properties.Name -contains 'StandardInputEncoding') { + $psi.StandardInputEncoding = [System.Text.Encoding]::ASCII + } + if ($psi.PSObject.Properties.Name -contains 'StandardOutputEncoding') { + $psi.StandardOutputEncoding = [System.Text.Encoding]::ASCII + } + $proc = New-Object System.Diagnostics.Process + $proc.StartInfo = $psi + $proc.Start() | Out-Null + $proc.StandardInput.WriteLine($priv) + $proc.StandardInput.Close() + $pub = $proc.StandardOutput.ReadToEnd() + $proc.WaitForExit() + if ($proc.ExitCode -eq 0 -and $pub) { + $pair.PublicKey = $pub.Trim() + } + } catch {} + } + } + + if (-not $pair.PrivateKey -and $WireGuardExe -and (Test-Path $WireGuardExe -PathType Leaf)) { try { - # Only wg.exe supports genkey; wireguard.exe likely won't, but attempt anyway - $priv = (& $cli genkey) + $priv = (& $WireGuardExe genkey) if ($priv) { - $priv = $priv.Trim() - $pair.PrivateKey = $priv - $pair.Source = $cli - try { - $padLen = 4 - ($priv.Length % 4) - $privForPub = $priv - if ($padLen -lt 4) { - $privForPub = $priv + ('=' * $padLen) - } - $pub = ($privForPub | & $cli pubkey) - if ($pub) { $pair.PublicKey = $pub.Trim() } - } catch {} - break + $pair.PrivateKey = $priv.Trim() + $pair.Source = $WireGuardExe } } catch {} } @@ -878,6 +931,504 @@ function Install_Agent_Dependencies { return $pair } + function Find-WireGuardDriverInf { + param( + [Parameter()][string]$LogName = 'Install.log' + ) + + $logPrefix = '[WireGuard]' + $output = $null + try { + $output = & pnputil.exe /enum-drivers + } catch { + Write-AgentLog -FileName $LogName -Message ("$logPrefix pnputil /enum-drivers failed: {0}" -f $_.Exception.Message) + return $null + } + + if (-not $output) { return $null } + $published = $null + $original = $null + $provider = $null + foreach ($line in ($output -split "`r?`n")) { + if (-not $line.Trim()) { + if ($published -and ($original -match '^(wireguard|wintun)\.inf$' -or $provider -like '*WireGuard*')) { + return $published + } + $published = $null + $original = $null + $provider = $null + continue + } + if ($line -match 'Published Name\s*:\s*(\S+)') { + $published = $Matches[1].Trim() + continue + } + if ($line -match 'Original Name\s*:\s*(\S+)') { + $original = $Matches[1].Trim() + continue + } + if ($line -match 'Provider Name\s*:\s*(.+)') { + $provider = $Matches[1].Trim() + continue + } + } + + if ($published -and ($original -match '^(wireguard|wintun)\.inf$' -or $provider -like '*WireGuard*')) { + return $published + } + return $null + } + + function Get-WireGuardAdapters { + param([string]$NameFilter) + + $args = @{ + Namespace = 'root/StandardCimv2' + ClassName = 'MSFT_NetAdapter' + ErrorAction = 'SilentlyContinue' + } + if ($NameFilter) { + $args.Filter = "Name='$NameFilter'" + } else { + $args.Filter = "InterfaceDescription LIKE '%WireGuard%'" + } + try { + $opTimeout = (Get-Command Get-CimInstance).Parameters.ContainsKey('OperationTimeoutSec') + if ($opTimeout) { $args.OperationTimeoutSec = 10 } + } catch {} + + try { + return Get-CimInstance @args + } catch { + return $null + } + } + + function Test-WireGuardAdapterName { + param( + [Parameter()][object]$Adapter, + [Parameter(Mandatory = $true)][string]$ExpectedName + ) + + if (-not $Adapter -or -not $ExpectedName) { return $false } + $adapterName = $Adapter.Name + if (-not $adapterName) { return $false } + if ($adapterName.ToString().Trim().ToLowerInvariant() -ne $ExpectedName.ToLowerInvariant()) { + return $false + } + $desc = $Adapter.InterfaceDescription + if (-not $desc) { + return $false + } + if ($desc.ToString() -notlike '*WireGuard*') { + return $false + } + return $true + } + + function Get-WireGuardAdapterByName { + param([string]$AdapterName) + + if (-not $AdapterName) { return $null } + try { + $adapters = Get-WireGuardAdapters -NameFilter $AdapterName + if ($adapters) { + foreach ($adapter in @($adapters)) { + if (Test-WireGuardAdapterName -Adapter $adapter -ExpectedName $AdapterName) { + return $adapter + } + } + } + } catch {} + return $null + } + + function Rename-WireGuardAdapterName { + param( + [Parameter(Mandatory = $true)][string]$OldName, + [Parameter(Mandatory = $true)][string]$NewName, + [Parameter()][string]$LogName = 'Install.log' + ) + + $logPrefix = '[WireGuard]' + if ($OldName -eq $NewName) { return $true } + $args = "interface set interface name=`"$OldName`" newname=`"$NewName`"" + $proc = Start-Process -FilePath 'netsh.exe' -ArgumentList $args -PassThru -WindowStyle Hidden -ErrorAction SilentlyContinue + if (-not $proc) { + Write-AgentLog -FileName $LogName -Message "$logPrefix netsh rename failed to start." + return $false + } + if (-not $proc.WaitForExit(10000)) { + try { $proc.Kill() } catch {} + Write-AgentLog -FileName $LogName -Message "$logPrefix netsh rename timed out." + return $false + } + if ($proc.ExitCode -ne 0) { + Write-AgentLog -FileName $LogName -Message ("$logPrefix netsh rename failed with exit code {0}." -f $proc.ExitCode) + return $false + } + Write-AgentLog -FileName $LogName -Message ("$logPrefix Renamed adapter {0} -> {1}" -f $OldName, $NewName) + return $true + } + + function Remove-WireGuardTunnelService { + param( + [Parameter(Mandatory = $true)][string]$TunnelName, + [Parameter(Mandatory = $true)][string]$WireGuardExe, + [Parameter()][string]$LogName = 'Install.log' + ) + + if (-not $TunnelName) { return } + $logPrefix = '[WireGuard]' + $serviceName = "WireGuardTunnel$TunnelName" + Write-AgentLog -FileName $LogName -Message ("$logPrefix Cleaning tunnel service {0}" -f $serviceName) + $serviceExists = $false + try { + $svc = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + if ($svc) { $serviceExists = $true } + } catch {} + + if ($serviceExists) { + try { + $proc = Start-Process -FilePath 'sc.exe' -ArgumentList 'stop', $serviceName -PassThru -WindowStyle Hidden -ErrorAction SilentlyContinue + if ($proc) { + if (-not $proc.WaitForExit(10000)) { + try { $proc.Kill() } catch {} + } + } + } catch {} + } else { + Write-AgentLog -FileName $LogName -Message ("$logPrefix Tunnel service {0} not present; skipping stop/uninstall." -f $serviceName) + } + + if ($serviceExists -and $WireGuardExe -and (Test-Path $WireGuardExe -PathType Leaf)) { + try { + $proc = Start-Process -FilePath $WireGuardExe -ArgumentList @('/uninstalltunnelservice', $TunnelName) -PassThru -WindowStyle Hidden -ErrorAction SilentlyContinue + if ($proc) { + if (-not $proc.WaitForExit(15000)) { + try { $proc.Kill() } catch {} + Write-AgentLog -FileName $LogName -Message ("$logPrefix /uninstalltunnelservice timed out for {0}" -f $TunnelName) + } else { + Write-AgentLog -FileName $LogName -Message ("$logPrefix /uninstalltunnelservice exit code for {0}: {1}" -f $TunnelName, $proc.ExitCode) + } + } + } catch {} + } + + if ($serviceExists) { + try { + $proc = Start-Process -FilePath 'sc.exe' -ArgumentList 'delete', $serviceName -PassThru -WindowStyle Hidden -ErrorAction SilentlyContinue + if ($proc) { + if (-not $proc.WaitForExit(10000)) { + try { $proc.Kill() } catch {} + } else { + Write-AgentLog -FileName $LogName -Message ("$logPrefix sc delete exit code for {0}: {1}" -f $serviceName, $proc.ExitCode) + } + } + } catch {} + } + + try { + $confPaths = Get-WireGuardConfigPaths -TunnelName $TunnelName + foreach ($confPath in $confPaths) { + if ($confPath -and (Test-Path $confPath -PathType Leaf)) { + Remove-Item -Path $confPath -Force -ErrorAction SilentlyContinue + Write-AgentLog -FileName $LogName -Message ("$logPrefix Removed tunnel config {0}" -f $confPath) + } + } + } catch {} + } + + function Get-WireGuardConfigPaths { + param([string]$TunnelName) + + if (-not $TunnelName) { return @() } + $paths = @() + $borealisConfigDir = $null + try { + if ($scriptDir) { + $borealisConfigDir = Join-Path $scriptDir 'Agent\Borealis\Settings\WireGuard' + } + } catch {} + if ($borealisConfigDir) { + $paths += $borealisConfigDir + } + if ($env:ProgramFiles) { + $paths += (Join-Path $env:ProgramFiles 'WireGuard\Data\Configurations') + } + $programDataRoot = if ($env:ProgramData) { $env:ProgramData } else { 'C:\ProgramData' } + if ($programDataRoot) { + $paths += (Join-Path $programDataRoot 'Borealis\WireGuard\Configurations') + } + if ($wireGuardInstallerDir) { + $paths += $wireGuardInstallerDir + } + $paths = $paths | Where-Object { $_ } + $confPaths = @() + foreach ($dir in $paths) { + $confPaths += (Join-Path $dir "$TunnelName.conf") + } + return $confPaths | Select-Object -Unique + } + + function Get-WireGuardConfigPath { + param([string]$TunnelName) + + if (-not $TunnelName) { return $null } + $confPaths = Get-WireGuardConfigPaths -TunnelName $TunnelName + foreach ($confPath in $confPaths) { + if (-not $confPath) { continue } + $dir = Split-Path $confPath -Parent + try { + if ($dir -and -not (Test-Path $dir)) { + New-Item -ItemType Directory -Path $dir -Force | Out-Null + } + } catch {} + if ($dir -and (Test-Path $dir)) { + return $confPath + } + } + return $confPaths | Select-Object -First 1 + } + + function Get-WireGuardTunnelServiceConfigPath { + param([string]$ServiceName) + + if (-not $ServiceName) { return $null } + $regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\$ServiceName" + try { + $imagePath = (Get-ItemProperty -Path $regPath -Name ImagePath -ErrorAction Stop).ImagePath + } catch { + return $null + } + if (-not $imagePath) { return $null } + $text = $imagePath.ToString() + if ($text -match '(?i)/tunnelservice\s+"([^"]+)"') { + return $Matches[1] + } + if ($text -match '(?i)/tunnelservice\s+(\S+)') { + return $Matches[1] + } + return $null + } + + function Ensure-WireGuardTunnelAdapter { + param( + [Parameter(Mandatory = $true)][string]$WireGuardExe, + [Parameter()][string]$LogName = 'Install.log' + ) + + $logPrefix = '[WireGuard]' + $friendlyName = $wireGuardTunnelNameFriendly + $internalName = $wireGuardTunnelNameInternal + $serviceName = "WireGuardTunnel$internalName" + + Write-AgentLog -FileName $LogName -Message ("$logPrefix Ensuring tunnel adapter: {0}" -f $friendlyName) + $existing = Get-WireGuardAdapterByName -AdapterName $friendlyName + if ($existing) { + Write-AgentLog -FileName $LogName -Message ("$logPrefix Adapter already present: {0}" -f $friendlyName) + return $true + } + + $internalAdapter = Get-WireGuardAdapterByName -AdapterName $internalName + if ($internalAdapter) { + if ($friendlyName -and $friendlyName -ne $internalName) { + $renamed = Rename-WireGuardAdapterName -OldName $internalName -NewName $friendlyName -LogName $LogName + if ($renamed) { return $true } + return $false + } + return $true + } + + try { + $wgCliExe = $null + try { + $wgCliCandidate = Join-Path (Split-Path $WireGuardExe -Parent) 'wg.exe' + if (Test-Path $wgCliCandidate -PathType Leaf) { $wgCliExe = $wgCliCandidate } + } catch {} + + $serviceConfigPath = Get-WireGuardTunnelServiceConfigPath -ServiceName $serviceName + $existingService = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + $servicePresent = $false + if ($existingService -or $serviceConfigPath) { $servicePresent = $true } + if ($servicePresent) { + $serviceConfigPath = Get-WireGuardTunnelServiceConfigPath -ServiceName $serviceName + if ($serviceConfigPath -and -not (Test-Path $serviceConfigPath -PathType Leaf)) { + $keyPair = New-WireGuardKeyPair -WireGuardExe $WireGuardExe -WgExe $wgCliExe + if ($keyPair.PrivateKey) { + $conf = @" +[Interface] +PrivateKey = $($keyPair.PrivateKey.Trim()) +Address = $wireGuardTunnelBootstrapAddress +ListenPort = 0 +"@ + try { + $dir = Split-Path $serviceConfigPath -Parent + if ($dir -and -not (Test-Path $dir)) { + New-Item -ItemType Directory -Path $dir -Force | Out-Null + } + Set-Content -Path $serviceConfigPath -Value $conf -Encoding ASCII -Force + if (Test-Path $serviceConfigPath -PathType Leaf) { + Write-AgentLog -FileName $LogName -Message ("$logPrefix Created adapter config at {0}" -f $serviceConfigPath) + } + } catch { + Write-AgentLog -FileName $LogName -Message ("$logPrefix Failed to write adapter config at {0}: {1}" -f $serviceConfigPath, $_.Exception.Message) + } + } + } + + Write-AgentLog -FileName $LogName -Message "$logPrefix Tunnel service already present; restarting to provision adapter." + try { Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue } catch {} + try { Start-Service -Name $serviceName -ErrorAction SilentlyContinue } catch {} + for ($i = 0; $i -lt 10; $i++) { + Start-Sleep -Milliseconds 500 + if (Get-WireGuardAdapterByName -AdapterName $friendlyName) { return $true } + } + Write-AgentLog -FileName $LogName -Message "$logPrefix Tunnel service present but adapter missing; uninstalling before reinstall." + } + + Remove-WireGuardTunnelService -TunnelName $internalName -WireGuardExe $WireGuardExe -LogName $LogName + if ($wireGuardTunnelLegacyName -and $wireGuardTunnelLegacyName -ne $internalName) { + Remove-WireGuardTunnelService -TunnelName $wireGuardTunnelLegacyName -WireGuardExe $WireGuardExe -LogName $LogName + } + for ($i = 0; $i -lt 10; $i++) { + Start-Sleep -Milliseconds 500 + $stillPresent = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + $stillConfig = Get-WireGuardTunnelServiceConfigPath -ServiceName $serviceName + if (-not $stillPresent -and -not $stillConfig) { break } + } + $serviceStillPresent = $false + if (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) { $serviceStillPresent = $true } + if (Get-WireGuardTunnelServiceConfigPath -ServiceName $serviceName) { $serviceStillPresent = $true } + if ($serviceStillPresent) { + Write-AgentLog -FileName $LogName -Message "$logPrefix Tunnel service still present after uninstall; skipping reinstall to avoid WireGuard dialog." + return $false + } + + try { + if (-not (Test-Path $wireGuardInstallerDir)) { + New-Item -ItemType Directory -Path $wireGuardInstallerDir -Force | Out-Null + } + } catch {} + + $keyPair = New-WireGuardKeyPair -WireGuardExe $WireGuardExe -WgExe $wgCliExe + if (-not $keyPair.PrivateKey) { + Write-AgentLog -FileName $LogName -Message "$logPrefix Failed to generate WireGuard keypair for adapter provision." + return $false + } + + $conf = @" +[Interface] +PrivateKey = $($keyPair.PrivateKey.Trim()) +Address = $wireGuardTunnelBootstrapAddress +ListenPort = 0 +"@ + $confPath = $null + $confPaths = Get-WireGuardConfigPaths -TunnelName $internalName + if ($serviceConfigPath) { + $confPaths = @($serviceConfigPath) + ($confPaths | Where-Object { $_ -ne $serviceConfigPath }) + } + foreach ($candidate in $confPaths) { + if (-not $candidate) { continue } + $dir = Split-Path $candidate -Parent + try { + if ($dir -and -not (Test-Path $dir)) { + New-Item -ItemType Directory -Path $dir -Force | Out-Null + } + } catch {} + try { + Set-Content -Path $candidate -Value $conf -Encoding ASCII -Force + if (Test-Path $candidate -PathType Leaf) { + $confPath = $candidate + Write-AgentLog -FileName $LogName -Message ("$logPrefix Created adapter config at {0}" -f $confPath) + $borealisConfigDir = $null + try { + if ($scriptDir) { + $borealisConfigDir = Join-Path $scriptDir 'Agent\Borealis\Settings\WireGuard' + } + } catch {} + if ($borealisConfigDir) { + try { + $confFull = [System.IO.Path]::GetFullPath($confPath) + $borealisFull = [System.IO.Path]::GetFullPath($borealisConfigDir) + if ($confFull.ToLowerInvariant().StartsWith($borealisFull.ToLowerInvariant()) -eq $false) { + Write-AgentLog -FileName $LogName -Message ("$logPrefix Adapter config stored outside Borealis settings: {0}" -f $confPath) + } + } catch {} + } + break + } + Write-AgentLog -FileName $LogName -Message ("$logPrefix Adapter config not found after write: {0}" -f $candidate) + } catch { + Write-AgentLog -FileName $LogName -Message ("$logPrefix Failed to write adapter config at {0}: {1}" -f $candidate, $_.Exception.Message) + } + } + + if (-not $confPath) { + Write-AgentLog -FileName $LogName -Message "$logPrefix Failed to write adapter config to any candidate path." + return $false + } + + $svcPreInstall = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + $svcConfig = Get-WireGuardTunnelServiceConfigPath -ServiceName $serviceName + if (-not $svcPreInstall -and -not $svcConfig) { + Write-AgentLog -FileName $LogName -Message ("$logPrefix Installing tunnel service {0} for adapter provisioning with config {1}." -f $serviceName, $confPath) + $installArgs = "/installtunnelservice `"$confPath`"" + $proc = Start-Process -FilePath $WireGuardExe -ArgumentList $installArgs -PassThru -WindowStyle Hidden -ErrorAction SilentlyContinue + if (-not $proc) { + Write-AgentLog -FileName $LogName -Message "$logPrefix /installtunnelservice failed to start." + return $false + } + $installTimeoutMs = 60000 + if (-not $proc.WaitForExit($installTimeoutMs)) { + try { $proc.Kill() } catch {} + Write-AgentLog -FileName $LogName -Message "$logPrefix /installtunnelservice timed out." + $svcAfterTimeout = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + if (-not $svcAfterTimeout) { + return $false + } + Write-AgentLog -FileName $LogName -Message "$logPrefix Tunnel service present after /installtunnelservice timeout." + } else { + $installExit = $proc.ExitCode + Write-AgentLog -FileName $LogName -Message ("$logPrefix /installtunnelservice exit code: {0}" -f $installExit) + if ($installExit -ne 0) { + $svcAfterExit = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + if (-not $svcAfterExit) { + return $false + } + Write-AgentLog -FileName $LogName -Message "$logPrefix Tunnel service present despite non-zero /installtunnelservice exit code." + } + } + } else { + Write-AgentLog -FileName $LogName -Message "$logPrefix Tunnel service already present; skipping /installtunnelservice." + } + try { Start-Service -Name $serviceName -ErrorAction SilentlyContinue } catch {} + } catch { + Write-AgentLog -FileName $LogName -Message ("$logPrefix Adapter provisioning failed: {0}" -f $_.Exception.Message) + return $false + } + + $finalAdapter = $null + $internalAdapter = $null + $attempts = 60 + for ($i = 0; $i -lt $attempts; $i++) { + Start-Sleep -Milliseconds 500 + $finalAdapter = Get-WireGuardAdapterByName -AdapterName $friendlyName + if ($finalAdapter) { return $true } + $internalAdapter = Get-WireGuardAdapterByName -AdapterName $internalName + if ($internalAdapter -and $friendlyName -and $friendlyName -ne $internalName) { + Rename-WireGuardAdapterName -OldName $internalName -NewName $friendlyName -LogName $LogName | Out-Null + $finalAdapter = Get-WireGuardAdapterByName -AdapterName $friendlyName + if ($finalAdapter) { return $true } + } + } + + if ($internalAdapter -and -not $finalAdapter) { + Write-AgentLog -FileName $LogName -Message ("$logPrefix Tunnel adapter present as {0}, but rename to {1} did not complete." -f $internalName, $friendlyName) + } + return $false + } + function Ensure-WireGuardDriver { param( [Parameter(Mandatory = $true)][string]$WireGuardExe, @@ -914,125 +1465,40 @@ function Install_Agent_Dependencies { } $state = $stateAfterManager - $wgCliExe = $null try { - $wgCliCandidate = Join-Path (Split-Path $WireGuardExe -Parent) 'wg.exe' - if (Test-Path $wgCliCandidate -PathType Leaf) { $wgCliExe = $wgCliCandidate } - } catch {} - - $bootstrapName = 'BorealisBootstrap' - $bootstrapDir = Join-Path $env:TEMP 'Borealis-WireGuard-Bootstrap' - $bootstrapConf = Join-Path $bootstrapDir "$bootstrapName.conf" - try { - if (-not (Test-Path $bootstrapDir)) { - New-Item -ItemType Directory -Path $bootstrapDir -Force | Out-Null - } - } catch {} - - $keyPair = New-WireGuardKeyPair -WireGuardExe $WireGuardExe -WgExe $wgCliExe - if (-not $keyPair.PrivateKey) { - $msg = "$logPrefix Failed to generate WireGuard keypair for driver bootstrap." - Write-AgentLog -FileName $LogName -Message $msg - throw $msg - } - - $peerKey = if ($keyPair.PublicKey) { $keyPair.PublicKey } else { $keyPair.PrivateKey } - $conf = @" -[Interface] -PrivateKey = $($keyPair.PrivateKey) -Address = 10.255.255.2/32 -ListenPort = 0 -"@ - if ($keyPair.PublicKey) { - $conf += @" - -[Peer] -PublicKey = $peerKey -AllowedIPs = 10.255.255.1/32 -"@ - } - try { - Set-Content -Path $bootstrapConf -Value $conf -Encoding ASCII -Force - Write-AgentLog -FileName $LogName -Message ("$logPrefix Created driver bootstrap config at {0}" -f $bootstrapConf) - } catch { - $msg = "$logPrefix Failed to write bootstrap config: $($_.Exception.Message)" - Write-AgentLog -FileName $LogName -Message $msg - throw $msg - } - - $serviceName = "WireGuardTunnel$bootstrapName" - try { - Write-AgentLog -FileName $LogName -Message ("$logPrefix Installing bootstrap tunnel service to seed driver ({0})" -f $serviceName) - & $WireGuardExe /installtunnelservice $bootstrapConf | Out-Null - $installExit = $LASTEXITCODE - Write-AgentLog -FileName $LogName -Message ("$logPrefix /installtunnelservice exit code: {0}" -f $installExit) - if ($installExit -ne 0) { - throw "wireguard.exe /installtunnelservice failed with exit code $installExit" + $adapterProvisioned = Ensure-WireGuardTunnelAdapter -WireGuardExe $WireGuardExe -LogName $LogName + if (-not $adapterProvisioned) { + Write-AgentLog -FileName $LogName -Message "$logPrefix Adapter provisioning did not complete." } } catch { - $msg = "$logPrefix Failed to install bootstrap tunnel service: $($_.Exception.Message)" - Write-AgentLog -FileName $LogName -Message $msg - throw $msg + Write-AgentLog -FileName $LogName -Message ("$logPrefix Adapter provisioning failed: {0}" -f $_.Exception.Message) } - Start-Sleep -Seconds 2 - try { - & $WireGuardExe /uninstalltunnelservice $bootstrapName | Out-Null - $uninstallExit = $LASTEXITCODE - Write-AgentLog -FileName $LogName -Message ("$logPrefix /uninstalltunnelservice exit code: {0}" -f $uninstallExit) - Write-AgentLog -FileName $LogName -Message ("$logPrefix Removed bootstrap tunnel service {0}" -f $serviceName) - } catch { - Write-AgentLog -FileName $LogName -Message ("$logPrefix Cleanup of bootstrap tunnel service failed: {0}" -f $_.Exception.Message) - } - - try { Remove-Item -Path $bootstrapConf -Force -ErrorAction SilentlyContinue } catch {} - try { Remove-Item -Path $bootstrapDir -Recurse -Force -ErrorAction SilentlyContinue } catch {} - $post = Get-WireGuardInstallState $postDriver = $post.DriverPresent - $postMsg = "[WireGuard] Driver presence after bootstrap: $postDriver (Exe: $WireGuardExe)" + $postMsg = "[WireGuard] Driver presence after adapter provisioning: $postDriver (Exe: $WireGuardExe)" Write-AgentLog -FileName $LogName -Message $postMsg if ($postDriver) { return } - # Fallback: install Wintun driver directly via pnputil using the official Wintun package - try { - Ensure-WireGuardInstallerFile -Url $wintunDownloadUrl -DestinationPath $wintunZipPath -LogName $LogName - } catch { - Write-AgentLog -FileName $LogName -Message ("$logPrefix Failed to cache Wintun zip: {0}" -f $_.Exception.Message) - } - - if (Test-Path $wintunZipPath -PathType Leaf) { - $extractRoot = Join-Path $env:TEMP 'Borealis-Wintun' - try { Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $extractRoot } catch {} - try { New-Item -ItemType Directory -Force -Path $extractRoot | Out-Null } catch {} - try { - Expand-Archive -Path $wintunZipPath -DestinationPath $extractRoot -Force - Write-AgentLog -FileName $LogName -Message ("$logPrefix Expanded Wintun zip to {0}" -f $extractRoot) - } catch { - Write-AgentLog -FileName $LogName -Message ("$logPrefix Failed to expand Wintun zip: {0}" -f $_.Exception.Message) - } - - $infPath = Get-ChildItem -Path $extractRoot -Recurse -Filter '*.inf' -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($infPath -and (Test-Path $infPath.FullName -PathType Leaf)) { + $publishedInf = Find-WireGuardDriverInf -LogName $LogName + if ($publishedInf) { + $infPath = Join-Path $env:WINDIR "INF\$publishedInf" + if (Test-Path $infPath -PathType Leaf) { try { - Write-AgentLog -FileName $LogName -Message ("$logPrefix Installing Wintun driver via pnputil: {0}" -f $infPath.FullName) - pnputil.exe /add-driver "`"$($infPath.FullName)`"" /install | Out-Null - $postPnP = Get-WireGuardInstallState - $postPnPDriver = $postPnP.DriverPresent - Write-AgentLog -FileName $LogName -Message ("$logPrefix Driver presence after pnputil install: $postPnPDriver") - if (-not $postPnPDriver) { - throw "$logPrefix pnputil install completed but driver still missing." - } - return + Write-AgentLog -FileName $LogName -Message ("$logPrefix Installing WireGuard driver via pnputil: {0}" -f $infPath) + pnputil.exe /add-driver "`"$infPath`"" /install | Out-Null } catch { Write-AgentLog -FileName $LogName -Message ("$logPrefix pnputil driver install failed: {0}" -f $_.Exception.Message) } - } else { - Write-AgentLog -FileName $LogName -Message "$logPrefix No Wintun INF found in extracted package." } } - throw "$logPrefix Driver still missing after bootstrap and pnputil fallback." + $postPnP = Get-WireGuardInstallState + $postPnPDriver = $postPnP.DriverPresent + Write-AgentLog -FileName $LogName -Message ("$logPrefix Driver presence after pnputil install: {0}" -f $postPnPDriver) + if ($postPnPDriver) { return } + + throw "$logPrefix Driver still missing after adapter provisioning and pnputil fallback." } # AutoHotKey portable @@ -1113,12 +1579,18 @@ AllowedIPs = 10.255.255.1/32 $state = Get-WireGuardInstallState $stateVersion = if ($state.Version) { $state.Version } else { 'unknown' } $stateExe = if ($state.ExePath) { $state.ExePath } else { 'n/a' } - $stateSummary = "[WireGuard] Detected install state: Installed={0}; Version={1}; Service={2}; Driver={3}; Exe={4}" -f ` - $state.Installed, $stateVersion, $state.ServicePresent, $state.DriverPresent, $stateExe + $stateSummary = "[WireGuard] Detected install state: Installed={0}; Version={1}; Service={2}; Driver={3}; Adapter={4}; BorealisAdapter={5}; Exe={6}" -f ` + $state.Installed, $stateVersion, $state.ServicePresent, $state.DriverPresent, $state.AdapterPresent, $state.DedicatedAdapterPresent, $stateExe Write-AgentLog -FileName $logName -Message $stateSummary if ($state.DriverPaths -and $state.DriverPaths.Count -gt 0) { Write-AgentLog -FileName $logName -Message ("[WireGuard] Driver paths: {0}" -f ($state.DriverPaths -join '; ')) } + if ($state.AdapterNames -and $state.AdapterNames.Count -gt 0) { + Write-AgentLog -FileName $logName -Message ("[WireGuard] Adapter names: {0}" -f ($state.AdapterNames -join '; ')) + } + if ($state.DedicatedAdapterNames -and $state.DedicatedAdapterNames.Count -gt 0) { + Write-AgentLog -FileName $logName -Message ("[WireGuard] Borealis adapter names: {0}" -f ($state.DedicatedAdapterNames -join '; ')) + } if (-not ($state.Installed -and $state.DriverPresent -and $state.ServicePresent)) { $installerCandidate = $null @@ -1154,8 +1626,8 @@ AllowedIPs = 10.255.255.1/32 $state = Get-WireGuardInstallState $postVersion = if ($state.Version) { $state.Version } else { 'unknown' } $postExe = if ($state.ExePath) { $state.ExePath } else { 'n/a' } - $postSummary = "[WireGuard] Post-install state: Installed={0}; Version={1}; Service={2}; Driver={3}; Exe={4}" -f ` - $state.Installed, $postVersion, $state.ServicePresent, $state.DriverPresent, $postExe + $postSummary = "[WireGuard] Post-install state: Installed={0}; Version={1}; Service={2}; Driver={3}; Adapter={4}; BorealisAdapter={5}; Exe={6}" -f ` + $state.Installed, $postVersion, $state.ServicePresent, $state.DriverPresent, $state.AdapterPresent, $state.DedicatedAdapterPresent, $postExe Write-AgentLog -FileName $logName -Message $postSummary if ($state.Installed -and $state.DriverPresent) { Write-Host "WireGuard installed and verified (version: $($state.Version))." -ForegroundColor Green @@ -1163,10 +1635,14 @@ AllowedIPs = 10.255.255.1/32 Write-Host "WireGuard installed (driver pending bootstrap)." -ForegroundColor Yellow } } else { - Write-Host "WireGuard already installed (version: $($state.Version))." -ForegroundColor Green + if ($state.DedicatedAdapterPresent) { + Write-Host "WireGuard already installed (version: $($state.Version))." -ForegroundColor Green + } else { + Write-Host "WireGuard installed (version: $($state.Version)); provisioning Borealis adapter." -ForegroundColor Yellow + } } - # Ensure wintun driver is seeded even before first tunnel is created + # Ensure WireGuard driver and Borealis tunnel adapter are provisioned $wgExe = $state.ExePath if (-not $wgExe -or -not (Test-Path $wgExe -PathType Leaf)) { # try default install path @@ -1175,9 +1651,16 @@ AllowedIPs = 10.255.255.1/32 if ($wgExe -and (Test-Path $wgExe -PathType Leaf)) { try { Ensure-WireGuardDriver -WireGuardExe $wgExe -LogName $logName + $adapterOk = Ensure-WireGuardTunnelAdapter -WireGuardExe $wgExe -LogName $logName $finalState = Get-WireGuardInstallState if (-not ($finalState.Installed -and $finalState.DriverPresent)) { - throw "WireGuard driver still missing after bootstrap attempts." + throw "WireGuard driver still missing after provisioning attempts." + } + if (-not $finalState.DedicatedAdapterPresent) { + throw "Borealis tunnel adapter still missing after provisioning attempts." + } + if (-not $adapterOk) { + Write-AgentLog -FileName $logName -Message "[WireGuard] Borealis tunnel adapter provisioning returned false." } Write-Host "WireGuard driver verified." -ForegroundColor Green } catch { diff --git a/Data/Agent/Roles/role_WireGuardTunnel.py b/Data/Agent/Roles/role_WireGuardTunnel.py index 0e356e3d..c2616409 100644 --- a/Data/Agent/Roles/role_WireGuardTunnel.py +++ b/Data/Agent/Roles/role_WireGuardTunnel.py @@ -22,9 +22,14 @@ import os import subprocess import threading import time +import re from pathlib import Path from typing import Any, Dict, Optional +try: + import winreg # type: ignore +except Exception: # pragma: no cover - non-Windows guard + winreg = None from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import x25519 @@ -41,6 +46,9 @@ except Exception: # pragma: no cover - fallback for runtime path issues ROLE_NAME = "WireGuardTunnel" ROLE_CONTEXTS = ["system"] +TUNNEL_NAME = "Borealis" +TUNNEL_DISPLAY_NAME = "Borealis" +TUNNEL_IDLE_ADDRESS = "169.254.255.254/32" def _log_path() -> Path: @@ -130,14 +138,21 @@ class WireGuardClient: self.cert_root = base / "Borealis" / "Certificates" / "VPN_Client" self.temp_root = base / "Borealis" / "Temp" self.temp_root.mkdir(parents=True, exist_ok=True) - self.conf_path = self.temp_root / "borealis-wg-client.conf" - self.service_name = "borealis-wg-client" + self.service_name = TUNNEL_NAME + self.display_name = TUNNEL_DISPLAY_NAME + self.conf_path = self._wireguard_config_path() self.session: Optional[SessionConfig] = None self.idle_deadline: Optional[float] = None self._idle_thread: Optional[threading.Thread] = None self._stop_event = threading.Event() + self._session_lock = threading.Lock() self._client_keys = _generate_client_keys(self.cert_root) self._wg_exe = self._resolve_wireguard_exe() + self._last_install_already_present = False + try: + self._ensure_idle_service() + except Exception: + pass def _resolve_wireguard_exe(self) -> str: candidates = [ @@ -149,6 +164,148 @@ class WireGuardClient: return candidate return "wireguard.exe" + def _service_id(self) -> str: + return f"WireGuardTunnel${self.service_name}" + + def _service_reg_path(self) -> str: + return f"SYSTEM\\CurrentControlSet\\Services\\{self._service_id()}" + + def _service_reg_exists(self) -> bool: + if winreg is None: + return False + try: + winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, self._service_reg_path()) + return True + except FileNotFoundError: + return False + except PermissionError: + _write_log("WireGuard service registry check denied; treating as present.") + return True + except Exception as exc: + _write_log(f"WireGuard service registry check failed: {exc}") + return False + + def _service_image_path(self) -> Optional[str]: + if winreg is None: + return None + try: + key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, self._service_reg_path()) + value, _ = winreg.QueryValueEx(key, "ImagePath") + return str(value) if value else None + except Exception: + return None + + def _service_config_path(self) -> Optional[Path]: + image_path = self._service_image_path() + if not image_path: + return None + match = re.search(r'(?i)/tunnelservice\s+"([^"]+)"', image_path) + if match: + return Path(match.group(1)) + match = re.search(r"(?i)/tunnelservice\s+(\S+)", image_path) + if match: + return Path(match.group(1)) + return None + + def _wireguard_config_path(self) -> Path: + settings_dir = self.temp_root.parent / "Settings" / "WireGuard" + candidates = [ + settings_dir, + Path(os.environ.get("ProgramFiles", "C:\\Program Files")) / "WireGuard" / "Data" / "Configurations", + Path(os.environ.get("ProgramData", "C:\\ProgramData")) / "Borealis" / "WireGuard" / "Configurations", + self.temp_root, + ] + for config_dir in candidates: + candidate = config_dir / f"{self.service_name}.conf" + if candidate.is_file(): + return candidate + for config_dir in candidates: + try: + config_dir.mkdir(parents=True, exist_ok=True) + return config_dir / f"{self.service_name}.conf" + except Exception: + continue + return self.temp_root / f"{self.service_name}.conf" + + def _write_config(self, text: str) -> bool: + return self._write_config_to(self.conf_path, text) + + def _write_config_to(self, path: Path, text: str) -> bool: + try: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="ascii") + return True + except Exception as exc: + _write_log(f"Failed to write WireGuard config at {path}: {exc}") + return False + + def _render_idle_config(self) -> str: + private_key = self._client_keys["private"] + return "\n".join( + [ + "[Interface]", + f"PrivateKey = {private_key}", + f"Address = {TUNNEL_IDLE_ADDRESS}", + "ListenPort = 0", + ] + ) + + def _service_exists(self) -> bool: + code, _, _ = self._run(["sc.exe", "query", self._service_id()]) + if code == 0: + return True + return self._service_reg_exists() + + def _install_service(self) -> bool: + code, out, err = self._run([self._wg_exe, "/installtunnelservice", str(self.conf_path)]) + self._last_install_already_present = False + if code != 0: + if "already installed and running" in err.lower(): + self._last_install_already_present = True + _write_log("WireGuard tunnel service already installed; skipping install.") + return True + if "access is denied" in err.lower(): + _write_log("Failed to install WireGuard tunnel service: access denied; ensure agent runs elevated.") + return False + _write_log(f"Failed to install WireGuard tunnel service: code={code} err={err}") + return False + return True + + def _restart_service(self) -> bool: + service_id = self._service_id() + stop_code, _, stop_err = self._run(["sc.exe", "stop", service_id]) + if stop_code != 0 and stop_err: + _write_log(f"WireGuard stop service returned code={stop_code} err={stop_err}") + time.sleep(1) + start_code, _, start_err = self._run(["sc.exe", "start", service_id]) + if start_code != 0 and start_err: + _write_log(f"WireGuard start service returned code={start_code} err={start_err}") + return start_code == 0 + + def _ensure_adapter_name(self) -> None: + if self.service_name == self.display_name: + return + args = [ + "netsh.exe", + "interface", + "set", + "interface", + f'name="{self.service_name}"', + f'newname="{self.display_name}"', + ] + self._run(args) + + def _ensure_idle_service(self) -> None: + if self._service_exists(): + return + if not Path(self._wg_exe).is_file(): + return + idle_config = self._render_idle_config() + if not self._write_config(idle_config): + return + if self._install_service(): + self._ensure_adapter_name() + def _validate_token(self, token: Dict[str, Any], *, signing_client: Optional[Any] = None) -> None: payload = dict(token or {}) signature = payload.pop("signature", None) @@ -232,43 +389,72 @@ class WireGuardClient: self._idle_thread = t def start_session(self, session: SessionConfig, *, signing_client: Optional[Any] = None) -> None: - if self.session: - _write_log("Rejecting start_session: existing session already active.") - return + with self._session_lock: + if self.session: + _write_log("Rejecting start_session: existing session already active.") + return - try: - self._validate_token(session.token, signing_client=signing_client) - except Exception as exc: - _write_log(f"Refusing to start WireGuard session: {exc}") - return + try: + self._validate_token(session.token, signing_client=signing_client) + except Exception as exc: + _write_log(f"Refusing to start WireGuard session: {exc}") + return - rendered = self._render_config(session) - self.conf_path.write_text(rendered, encoding="utf-8") - _write_log(f"Rendered WireGuard client config to {self.conf_path}") + rendered = self._render_config(session) + if not self._write_config(rendered): + _write_log("Failed to write WireGuard client config.") + return + _write_log(f"Rendered WireGuard client config to {self.conf_path}") - # Pre-stop any orphaned tunnel service using the same name - self.stop_session(reason="preflight", ignore_missing=True) + service_config_path = self._service_config_path() + if service_config_path and service_config_path != self.conf_path: + if self._write_config_to(service_config_path, rendered): + _write_log(f"Rendered WireGuard client config to service path {service_config_path}") - code, out, err = self._run([self._wg_exe, "/installtunnelservice", str(self.conf_path)]) - if code != 0: - _write_log(f"Failed to install WireGuard client tunnel: code={code} err={err}") - return + if not self._service_exists(): + if not self._install_service(): + return - self.session = session - self.idle_deadline = time.time() + max(60, session.idle_seconds) - _write_log("WireGuard client session started; idle timer armed.") - self._start_idle_monitor() + service_present = self._service_exists() + if not service_present and self._last_install_already_present: + _write_log("WireGuard tunnel service presence inferred from install response.") + service_present = True + if not service_present: + _write_log("WireGuard tunnel service still missing after install attempt.") + return + + self._restart_service() + self._ensure_adapter_name() + + self.session = session + self.idle_deadline = time.time() + max(60, session.idle_seconds) + _write_log("WireGuard client session started; idle timer armed.") + self._start_idle_monitor() def stop_session(self, reason: str = "stop", ignore_missing: bool = False) -> None: - code, out, err = self._run([self._wg_exe, "/uninstalltunnelservice", self.service_name]) - if code != 0: - if not ignore_missing: - _write_log(f"Failed to uninstall WireGuard client tunnel: code={code} err={err}") - else: - _write_log(f"WireGuard client session stopped (reason={reason}).") - self.session = None - self.idle_deadline = None - self._stop_event.set() + with self._session_lock: + if not self._service_exists(): + if not ignore_missing: + _write_log("WireGuard tunnel service not found when stopping session.") + self.session = None + self.idle_deadline = None + self._stop_event.set() + return + + idle_config = self._render_idle_config() + wrote_idle = self._write_config(idle_config) + service_config_path = self._service_config_path() + if service_config_path and service_config_path != self.conf_path: + wrote_idle = self._write_config_to(service_config_path, idle_config) or wrote_idle + if wrote_idle: + self._restart_service() + self._ensure_adapter_name() + _write_log(f"WireGuard client session stopped (reason={reason}).") + elif not ignore_missing: + _write_log("Failed to write idle WireGuard config.") + self.session = None + self.idle_deadline = None + self._stop_event.set() def bump_activity(self) -> None: if self.session and self.idle_deadline: diff --git a/Docs/Codex/WireGuard_Troubleshooting.md b/Docs/Codex/WireGuard_Troubleshooting.md new file mode 100644 index 00000000..6545ea8e --- /dev/null +++ b/Docs/Codex/WireGuard_Troubleshooting.md @@ -0,0 +1,106 @@ +# Borealis WireGuard Troubleshooting Handoff + +This file is a self-contained handoff prompt + context for a new Codex agent to resume WireGuard tunnel troubleshooting. + +## Prompt to Use in a New Codex Session + +Copy/paste the prompt below into a new Codex chat: + +""" +You are a new Codex agent working in d:\Github\Borealis. Please do the following: + +1) Read AGENTS.md, Docs/Codex/BOREALIS_AGENT.md, Docs/Codex/BOREALIS_ENGINE.md, Docs/Codex/REVERSE_TUNNELS.md, then Docs/Codex/WireGuard_Troubleshooting.md. +2) Investigate why the WireGuard tunnel does not come up (remote shell timeouts) even though the Engine emits vpn_tunnel_start. +3) Focus on the WireGuard client lifecycle in Data/Agent/Roles/role_WireGuardTunnel.py and the bootstrap logic in Borealis.ps1 (WireGuard adapter provisioning). +4) Use Data/Agent for edits (runtime under Agent/ is ephemeral). Keep the adapter name "Borealis" and ensure idempotent behavior. Do not rely on the PIA adapter. +5) Provide concrete fixes + verification steps. Be careful with Windows services and avoid GUI popup dialogs when possible. +""" + +## Environment / Scope + +- Workspace: d:\Github\Borealis +- Host OS: Windows 10/11 (build 26200). Current tests run on the Windows 11 machine that also runs Engine + Agent. +- Agent/Engine launch: via Borealis.ps1, always elevated as admin. +- Network: Engine + Agent run on the same host during testing (Engine endpoint is "localhost:30000"). +- WireGuard version: wireguard.exe 0.5.3, wg.exe 1.0.20210914. +- PIA (Private Internet Access) is installed and supplies a wintun driver (pia-wintun.sys). Do NOT treat the PIA adapter as the Borealis adapter. + +## Desired Behavior + +- Agent has a dedicated WireGuard adapter named "Borealis" (Description shows "WireGuard Tunnel"). +- Adapter provisioning is idempotent: if "Borealis" exists, do not recreate it. +- WireGuard config should be stored under Agent\Borealis\Settings\WireGuard\Borealis.conf (preferred) and not only in Program Files. +- Agent should bring up the WireGuard tunnel on vpn_tunnel_start, then remote shell / RDP / VNC / SSH should flow through it. +- On stop/idle, the tunnel should be torn down and firewall rules removed. + +## Recent Changes (Current Repo State) + +- Data/Agent/Roles/role_WireGuardTunnel.py + - Service name fix: WireGuard tunnel service is "WireGuardTunnel$Borealis". + - Config path preference: Agent\Borealis\Settings\WireGuard. + - Uses registry ImagePath to locate the actual service config when needed. + - Adds a session lock to prevent concurrent start/stop. +- Borealis.ps1 + - WireGuard config search order includes Agent\Borealis\Settings\WireGuard. + - Adapter provisioning reads the service ImagePath to write config when service exists. + - Avoids /installtunnelservice if service still present to prevent GUI error dialogs. + - Adapter name is "Borealis". + +Note: Data/Agent changes only apply to runtime after Borealis.ps1 re-stages the agent under Agent/. + +## Symptoms from Fresh Logs (2026-01-12 19:29) + +Agent (Agent/Logs/VPN_Tunnel/tunnel.log): +- "WireGuard tunnel service already installed; skipping install." +- "WireGuard tunnel service still missing after install attempt." + +Engine (Engine/Logs/VPN_Tunnel/tunnel.log): +- vpn_tunnel_session_create for agent LAB-OPERATOR-01_..._SYSTEM +- WireGuard listener installed (service=borealis-wg) +- vpn_api_status_response status=up + +Engine (Engine/Logs/VPN_Tunnel/remote_shell.log): +- repeated vpn_shell_connect_attempt to 10.255.0.2:47002 +- timeouts + +Agent (Agent/Logs/VPN_Tunnel/remote_shell.log): +- VPN shell server listening on 0.0.0.0:47002 + +Net effect: engine believes tunnel is "up", but remote shell cannot reach 10.255.0.2. This implies the WireGuard client tunnel is not actually up on the agent. + +## Key Paths + +- Agent WireGuard role: Data/Agent/Roles/role_WireGuardTunnel.py +- Agent VPN shell role: Data/Agent/Roles/role_VpnShell.py +- Engine WireGuard manager: Data/Engine/services/VPN/wireguard_server.py +- Engine tunnel service: Data/Engine/services/VPN/vpn_tunnel_service.py +- Agent tunnel logs: Agent/Logs/VPN_Tunnel/tunnel.log +- Agent shell logs: Agent/Logs/VPN_Tunnel/remote_shell.log +- Engine tunnel logs: Engine/Logs/VPN_Tunnel/tunnel.log +- Engine shell logs: Engine/Logs/VPN_Tunnel/remote_shell.log +- Agent WireGuard config: Agent/Borealis/Settings/WireGuard/Borealis.conf + +## Known WireGuard Services / Names + +- Engine listener service name: "borealis-wg" +- Agent tunnel service name: "WireGuardTunnel$Borealis" +- Adapter name in Control Panel: "Borealis" + +## Suggested Verification Commands + +- Check agent service: + - Get-Service -Name "WireGuardTunnel$Borealis" + - Get-ItemProperty "HKLM:\\SYSTEM\\CurrentControlSet\\Services\\WireGuardTunnel$Borealis" | Select-Object ImagePath +- Confirm adapter exists: + - Get-NetAdapter -IncludeHidden | Where-Object { $_.InterfaceDescription -like "*WireGuard*" } | Select-Object Name, Status, InterfaceDescription, ifIndex +- Check WireGuard state: + - "C:\\Program Files\\WireGuard\\wg.exe" show + +## Troubleshooting Focus Areas + +- Ensure runtime is up-to-date (Borealis.ps1 re-staging Data/Agent -> Agent/). +- Validate service detection vs. WireGuard install output (sc.exe vs registry). +- Confirm the config file used by the service matches Agent/Borealis/Settings/WireGuard/Borealis.conf. +- Confirm /installtunnelservice is not invoked when service already exists (avoid WireGuard GUI errors). +- Confirm the WireGuard tunnel actually connects (wg.exe show handshake) before attempting remote shell. +