# Temporary reverse SSH tunnel for Windows PowerShell 5.1 / PowerShell 7. [CmdletBinding()] param( [ValidateRange(1,65535)][int]$LocalPort = 22, [switch]$Check, [switch]$Install, [switch]$InstallOnly, [switch]$NoInstall ) $ErrorActionPreference = 'Stop' if ($InstallOnly) { $Install = $true } if ($Check -and $Install) { throw '-Check cannot be combined with installation.' } if ($NoInstall -and $Install) { throw '-NoInstall cannot be combined with installation.' } if ($Install -and $LocalPort -ne 22) { throw 'Installation uses the existing SSH configuration and requires -LocalPort 22. Configure custom ports manually.' } if (-not $Check -and -not $NoInstall -and $LocalPort -eq 22) { $Install = $true } function Find-OpenSshTool([string]$Name) { $command = Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($command) { return $command.Source } if ($env:WINDIR) { foreach ($folder in @('Sysnative', 'System32')) { $candidate = Join-Path $env:WINDIR "$folder\OpenSSH\$Name" if (Test-Path -LiteralPath $candidate) { return $candidate } } } return $null } function Test-LocalSsh { $scanner = Find-OpenSshTool 'ssh-keyscan.exe' if (-not $scanner) { return $false } # Windows PowerShell 5.1 represents native stderr as error records. # keyscan writes ordinary banner diagnostics there; suppress them locally. $ErrorActionPreference = 'Continue' $keys = @(& $scanner '-4' '-T' '3' '-p' "$LocalPort" '127.0.0.1' 2>$null) if ($LASTEXITCODE -ne 0) { return $false } return [bool]($keys | Where-Object { $_ -match '^\S+\s+(ssh-|ecdsa-)\S+\s+\S+' } | Select-Object -First 1) } function Install-LocalSsh { if ($env:OS -ne 'Windows_NT') { throw 'Use connect.sh to install SSH on Linux/macOS.' } if (-not (Get-Command Add-WindowsCapability -ErrorAction SilentlyContinue)) { throw 'This Windows version does not support the built-in OpenSSH installer. Install OpenSSH manually.' } $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($identity) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Installation must run as Administrator.' } Write-Host 'Installing/enabling OpenSSH server. This service persists after the tunnel exits.' if (-not (Find-OpenSshTool 'ssh.exe') -or -not (Find-OpenSshTool 'ssh-keyscan.exe')) { $result = Add-WindowsCapability -Online -Name 'OpenSSH.Client~~~~0.0.1.0' if ($result.RestartNeeded) { throw 'OpenSSH Client requires a restart. Restart Windows manually, then rerun.' } } $service = Get-Service -Name sshd -ErrorAction SilentlyContinue if (-not $service) { $result = Add-WindowsCapability -Online -Name 'OpenSSH.Server~~~~0.0.1.0' if ($result.RestartNeeded) { throw 'OpenSSH Server requires a restart. Restart Windows manually, then rerun.' } } Set-Service -Name sshd -StartupType Automatic Start-Service -Name sshd # Use the OS installer defaults. Do not rewrite authentication or add firewall rules. } $ssh = Find-OpenSshTool 'ssh.exe' $ready = Test-LocalSsh if ($Install -and (-not $ready -or -not $ssh)) { if ($env:OS -ne 'Windows_NT') { throw 'Use connect.sh to install SSH on Linux/macOS.' } $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($identity) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { if (-not $PSCommandPath) { throw 'Download this script to a file and rerun with -Install, or use an Administrator terminal.' } Write-Host 'Approve the Windows UAC prompt to install and enable OpenSSH.' $elevatedArgs = '-NoProfile -ExecutionPolicy Bypass -File "' + $PSCommandPath + '" -LocalPort 22 -Install' if ($InstallOnly) { $elevatedArgs += ' -InstallOnly' } $process = Start-Process -FilePath (Join-Path $PSHOME $(if ($PSVersionTable.PSEdition -eq 'Core') { 'pwsh.exe' } else { 'powershell.exe' })) -Verb RunAs -ArgumentList $elevatedArgs -Wait -PassThru exit $process.ExitCode } Install-LocalSsh $ssh = Find-OpenSshTool 'ssh.exe' for ($attempt = 0; $attempt -lt 5; $attempt++) { if (Test-LocalSsh) { $ready = $true; break } Start-Sleep -Seconds 1 } } if (-not $ssh -or -not $ready) { throw "SSH check failed on 127.0.0.1:$LocalPort. Rerun the downloaded script with -Install to install/start SSH on port 22. Existing custom ports and authentication settings are preserved." } Write-Host "SSH service ready: handshake verified on 127.0.0.1:$LocalPort (login credentials not tested)." if ($InstallOnly) { return } $work = Join-Path ([IO.Path]::GetTempPath()) ('quick-tunnel-' + [Guid]::NewGuid().ToString('N')) $null = New-Item -ItemType Directory -Path $work $tunnelExit = 0 try { $knownHosts = Join-Path $work 'known_hosts' [IO.File]::WriteAllText($knownHosts, "[6f3907bcb084.sn.mynetname.net]:22022 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKZqy8G5eBCTsLi/tRhKCcktl/SvL01l8r1fCJF4EeYZ`n", [Text.Encoding]::ASCII) # Use a separate config file so Windows argument quoting cannot strip path quotes. $clientConfig = Join-Path $work 'ssh_config' $configText = 'UserKnownHostsFile "' + $knownHosts.Replace('\', '/') + '"' + "`nGlobalKnownHostsFile NUL`n" [IO.File]::WriteAllText($clientConfig, $configText, (New-Object Text.UTF8Encoding($false))) $sshArgs = @( '-F', $clientConfig, '-4', '-NT', '-o', 'PreferredAuthentications=password', '-o', 'PubkeyAuthentication=no', '-o', 'KbdInteractiveAuthentication=no', '-o', 'PasswordAuthentication=yes', '-o', 'BatchMode=no', '-o', 'NumberOfPasswordPrompts=3', '-o', 'ExitOnForwardFailure=yes', '-o', 'ServerAliveInterval=30', '-o', 'ServerAliveCountMax=3', '-o', 'ConnectTimeout=15', '-o', 'StrictHostKeyChecking=yes', '-o', 'ControlMaster=no', '-o', 'ControlPath=none', '-o', 'ControlPersist=no', '-p', '22022', '-R', "0.0.0.0:10020:127.0.0.1:$LocalPort", 'temp-tunnel@6f3907bcb084.sn.mynetname.net' ) if ($Check) { & $ssh '-G' @sshArgs | Out-Null if ($LASTEXITCODE -ne 0) { throw 'OpenSSH configuration check failed.' } Write-Host "Service and configuration OK: receiver 10020 -> this machine 127.0.0.1:$LocalPort. No connection opened." } else { Write-Host "Forward: receiver 10020 -> this machine 127.0.0.1:$LocalPort" Write-Host 'Enter the tunnel password. Keep this terminal open; Ctrl-C disconnects.' & $ssh @sshArgs $tunnelExit = $LASTEXITCODE } } finally { Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue } if ($tunnelExit -ne 0) { throw "SSH exited with code $tunnelExit. Check the SSH message above." }