168 lines
5.4 KiB
PowerShell
168 lines
5.4 KiB
PowerShell
#Requires -Version 5.1
|
|
|
|
[CmdletBinding(DefaultParameterSetName = 'Explicit')]
|
|
param(
|
|
[Parameter(Mandatory = $true, ParameterSetName = 'Config')]
|
|
[string] $ConfigPath,
|
|
|
|
[Parameter(Mandatory = $true, ParameterSetName = 'Explicit')]
|
|
[ValidatePattern('^[A-Za-z0-9._:-]+$')]
|
|
[string] $ServerHost,
|
|
|
|
[ValidateRange(1, 65535)]
|
|
[int] $ServerPort = 22,
|
|
|
|
[ValidatePattern('^[A-Za-z0-9._-]+$')]
|
|
[string] $RemoteUser = 'caiwu-tunnel',
|
|
|
|
[Parameter(Mandatory = $true, ParameterSetName = 'Explicit')]
|
|
[string] $PrivateKeyPath,
|
|
|
|
[Parameter(Mandatory = $true, ParameterSetName = 'Explicit')]
|
|
[ValidatePattern('^[A-Za-z0-9._:-]+$')]
|
|
[string] $RemoteBindAddress,
|
|
|
|
[Parameter(Mandatory = $true, ParameterSetName = 'Explicit')]
|
|
[ValidateRange(1, 65535)]
|
|
[int] $RemoteBindPort,
|
|
|
|
[Parameter(Mandatory = $true, ParameterSetName = 'Explicit')]
|
|
[ValidatePattern('^[A-Za-z0-9._:-]+$')]
|
|
[string] $UpstreamHost,
|
|
|
|
[ValidateRange(1, 65535)]
|
|
[int] $UpstreamPort = 1433,
|
|
|
|
[ValidateRange(10, 300)]
|
|
[int] $RetrySeconds = 30,
|
|
|
|
[switch] $Once
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
$statePath = $null
|
|
|
|
if ($PSCmdlet.ParameterSetName -eq 'Config') {
|
|
$resolvedConfigPath = (Resolve-Path -LiteralPath $ConfigPath -ErrorAction Stop).Path
|
|
$statePath = $resolvedConfigPath + '.pid'
|
|
$settings = Get-Content -LiteralPath $resolvedConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
|
|
$ServerHost = [string] $settings.ServerHost
|
|
$ServerPort = [int] $settings.ServerPort
|
|
$RemoteUser = [string] $settings.RemoteUser
|
|
$PrivateKeyPath = [string] $settings.PrivateKeyPath
|
|
$RemoteBindAddress = [string] $settings.RemoteBindAddress
|
|
$RemoteBindPort = [int] $settings.RemoteBindPort
|
|
$UpstreamHost = [string] $settings.UpstreamHost
|
|
$UpstreamPort = [int] $settings.UpstreamPort
|
|
$RetrySeconds = [int] $settings.RetrySeconds
|
|
}
|
|
|
|
$endpointPattern = '^[A-Za-z0-9._:-]+$'
|
|
foreach ($endpoint in @($ServerHost, $RemoteBindAddress, $UpstreamHost)) {
|
|
if ([string]::IsNullOrWhiteSpace($endpoint) -or $endpoint -notmatch $endpointPattern) {
|
|
throw 'The tunnel configuration contains an invalid endpoint.'
|
|
}
|
|
}
|
|
|
|
if ($RemoteUser -notmatch '^[A-Za-z0-9._-]+$') {
|
|
throw 'The tunnel configuration contains an invalid SSH user.'
|
|
}
|
|
|
|
foreach ($port in @($ServerPort, $RemoteBindPort, $UpstreamPort)) {
|
|
if ($port -lt 1 -or $port -gt 65535) {
|
|
throw 'The tunnel configuration contains an invalid port.'
|
|
}
|
|
}
|
|
|
|
if ($RetrySeconds -lt 10 -or $RetrySeconds -gt 300) {
|
|
throw 'The tunnel retry interval must be between 10 and 300 seconds.'
|
|
}
|
|
|
|
$sshCommand = Get-Command 'ssh.exe' -ErrorAction Stop
|
|
$resolvedKeyPath = (Resolve-Path -LiteralPath $PrivateKeyPath -ErrorAction Stop).Path
|
|
|
|
$identity = '{0}|{1}|{2}|{3}' -f $ServerHost, $ServerPort, $RemoteBindAddress, $RemoteBindPort
|
|
$sha256 = [System.Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$identityBytes = [System.Text.Encoding]::UTF8.GetBytes($identity)
|
|
$identityHash = [System.BitConverter]::ToString($sha256.ComputeHash($identityBytes)).Replace('-', '')
|
|
}
|
|
finally {
|
|
$sha256.Dispose()
|
|
}
|
|
|
|
$mutex = New-Object System.Threading.Mutex($false, ('Local\CaiWuXiTong-UpstreamTunnel-{0}' -f $identityHash.Substring(0, 20)))
|
|
$ownsMutex = $false
|
|
|
|
try {
|
|
try {
|
|
$ownsMutex = $mutex.WaitOne(0)
|
|
}
|
|
catch [System.Threading.AbandonedMutexException] {
|
|
$ownsMutex = $true
|
|
}
|
|
|
|
if (-not $ownsMutex) {
|
|
Write-Output 'The read-only upstream tunnel is already managed by another local process.'
|
|
exit 0
|
|
}
|
|
|
|
if ($null -ne $statePath) {
|
|
[System.IO.File]::WriteAllText($statePath, [string] $PID, [System.Text.Encoding]::ASCII)
|
|
$currentSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
& icacls.exe $statePath '/inheritance:r' '/grant:r' "*$currentSid`:(F)" '*S-1-5-18:(F)' | Out-Null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw 'The tunnel PID file permissions could not be restricted.'
|
|
}
|
|
}
|
|
|
|
$forward = '{0}:{1}:{2}:{3}' -f $RemoteBindAddress, $RemoteBindPort, $UpstreamHost, $UpstreamPort
|
|
$destination = '{0}@{1}' -f $RemoteUser, $ServerHost
|
|
$sshArguments = @(
|
|
'-N',
|
|
'-T',
|
|
'-i', $resolvedKeyPath,
|
|
'-p', $ServerPort,
|
|
'-o', 'BatchMode=yes',
|
|
'-o', 'ConnectTimeout=10',
|
|
'-o', 'ExitOnForwardFailure=yes',
|
|
'-o', 'ServerAliveInterval=20',
|
|
'-o', 'ServerAliveCountMax=3',
|
|
'-o', 'StrictHostKeyChecking=accept-new',
|
|
'-o', 'TCPKeepAlive=yes',
|
|
'-o', 'LogLevel=ERROR',
|
|
'-R', $forward,
|
|
$destination
|
|
)
|
|
|
|
do {
|
|
& $sshCommand.Source @sshArguments
|
|
$exitCode = $LASTEXITCODE
|
|
|
|
if ($Once) {
|
|
exit $exitCode
|
|
}
|
|
|
|
Write-Warning ('The read-only upstream tunnel disconnected (SSH exit code {0}); retrying shortly.' -f $exitCode)
|
|
$retryDelay = $RetrySeconds + (Get-Random -Minimum 0 -Maximum 6)
|
|
Start-Sleep -Seconds $retryDelay
|
|
}
|
|
while ($true)
|
|
}
|
|
finally {
|
|
if ($null -ne $statePath -and (Test-Path -LiteralPath $statePath -PathType Leaf)) {
|
|
$recordedPid = Get-Content -LiteralPath $statePath -Raw -ErrorAction SilentlyContinue
|
|
if ([string] $recordedPid -eq [string] $PID) {
|
|
Remove-Item -LiteralPath $statePath -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
if ($ownsMutex) {
|
|
$mutex.ReleaseMutex()
|
|
}
|
|
|
|
$mutex.Dispose()
|
|
}
|