diff --git a/GateWay-KeepAlive.ps1 b/GateWay-KeepAlive.ps1 index f547839..9ad5626 100644 --- a/GateWay-KeepAlive.ps1 +++ b/GateWay-KeepAlive.ps1 @@ -1,188 +1,263 @@ -# $Global:TestDomain = "www.google.com" #测试代理是否可用 -# $Global:TestIP = "223.5.5.5" #测试是否连接网络 -# $Global:originalDns = "223.5.5.5" #默认DNS -# $Global:originalGateway = "192.168.6.1" #默认网关 -# $Global:CustomGateway = "192.168.6.2" #代理网关 -# $Global:CustomIP = "192.168.6.4" #默认ip -# $Global:TargetNetworkAdapter = {} #网络适配器实例 -# $Global:TargetNetworkAdapterKeyword = "Ethernet" #网络适配器名字通配符 -# $Global:isCustomGateway = $false #是否为自定义网关 +. (Join-Path $PSScriptRoot 'config.ps1') +. (Join-Path $PSScriptRoot 'logger.ps1') +. (Join-Path $PSScriptRoot 'proxy-test.ps1') -. $(-Join($(Get-Location).Path, '\config.ps1')) -. $(-Join($(Get-Location).Path, '\proxy-test.ps1')) -. $(-Join($(Get-Location).Path, '\logger.ps1')) +$Script:ProxySuccessCount = 0 +$Script:ProxyFailureCount = 0 +$Script:LastSwitchTime = (Get-Date).AddSeconds(-1 * $Global:SwitchCooldownSeconds) - -function isDhcpEnable(){ - return Get-NetIPConfiguration | select-object InterfaceAlias, IPv4Address, @{n="dhcp";e={$_.NetIPv4Interface.DHCP}} | Select-Object -ExpandProperty dhcp +function Test-IsAdministrator { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]$identity + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } -function isForeignConnected { - # check foreign network availability - $result = Test-NetConnection -ComputerName $Global:TestDomain -Port 443 - - if ($result.TcpTestSucceeded) { - Write-Log -Message "Successfully connected to $($Global:TestDomain) via HTTPS. isCustomGateway:$($Global:isCustomGateway)" - return $true - } else { - Write-Log -Message "Failed to connect to $($Global:TestDomain) via HTTPS. isCustomGateway:$($Global:isCustomGateway)" - return $false - } +function Request-Administrator { + Write-Log -Message "Requesting Administrator privileges..." + Start-Process pwsh -ArgumentList "-NoExit", "-File", $PSCommandPath -Verb RunAs + exit } -function isNetworkConnected { - # check network availability - $result = $(Test-Connection -ComputerName $Global:TestIP -Count 1).Status -eq 'Success' - if ($result) { - Write-Log -Message "Successfully connected to $($Global:TestIP) via ICMP. isCustomGateway:$($Global:isCustomGateway)" - return $true - } else { - Write-Log -Message "Failed to connect to $($Global:TestIP) via ICMP. isCustomGateway:$($Global:isCustomGateway)" - return $false - } -} +function Get-TargetAdapter { + $adapter = Get-NetAdapter | + Where-Object { $_.Name.Contains($Global:TargetNetworkAdapterKeyword) -and $_.Status -eq "Up" } | + Select-Object -First 1 -function setDhcp($enable){ - if($enable){ - Set-NetIPInterface -InterfaceAlias $Global:TargetNetworkAdapter.name -Dhcp Enabled - Write-Log -Message "enable dhcp" - } else { - Set-NetIPInterface -InterfaceAlias $Global:TargetNetworkAdapter.name -Dhcp Disabled - Write-Log -Message "disable dhcp" + if (-not $adapter) { + throw "No active network adapter found by keyword '$($Global:TargetNetworkAdapterKeyword)'." } + + $Global:TargetNetworkAdapter = $adapter + Write-Log -Message "Target adapter: $($adapter.Name), InterfaceIndex: $($adapter.ifIndex)" + return $adapter } -function changeGateway{ +function Get-CurrentDefaultGateway { param( - $adapter,$isCustomGateway + [Parameter(Mandatory)] + [int]$InterfaceIndex ) - Write-Log -Message "Changing gateway for adapter: $($adapter.Name) isCustomGateway $($isCustomGateway)" - - - # check gateway availability - if($isCustomGateway){ - $currentGateway = $(Get-NetIPConfiguration).IPv4DefaultGateway.NextHop - if($currentGateway -eq $Global:CustomGateway){ - Write-Log -Message "current gateway is $currentGateway same as targetGateway $Global:TargetGateway, change gateway canceled" - $Global:isCustomGateway = $true - return - } - $result = $(Test-Connection $Global:CustomGateway -Count 1).Status - if (! ($result -eq 'Success')){ - Write-Log -Message "custom gateway $Global:CustomGateway is $result, refuse to change" - return - } + + $route = Get-NetRoute -InterfaceIndex $InterfaceIndex -DestinationPrefix "0.0.0.0/0" -ErrorAction SilentlyContinue | + Sort-Object RouteMetric, ifMetric | + Select-Object -First 1 + + if ($route) { + return $route.NextHop } - # check current ip configuration - $existingIP = Get-NetIPAddress -InterfaceAlias $adapter.Name -AddressFamily IPv4 + return $null +} - # if have ip configuration, delete it - if ($existingIP) { - Write-Log -Message "Removing existing IP address: $($existingIP.IPAddress)" - Remove-NetIPAddress -InterfaceAlias $adapter.Name -Confirm:$false +function Get-CurrentMode { + param( + [Parameter(Mandatory)] + [int]$InterfaceIndex + ) + + $gateway = Get-CurrentDefaultGateway -InterfaceIndex $InterfaceIndex + if ($gateway -eq $Global:CustomGateway) { + return "PROXY" } - # delete current route(gateway) - $existingRoute = Get-NetRoute -InterfaceAlias $adapter.Name -DestinationPrefix "0.0.0.0/0" - if ($existingRoute) { - Write-Log -Message "Removing existing default route..." - Remove-NetRoute -InterfaceAlias $adapter.Name -Confirm:$false + return "DIRECT" +} + +function Test-CooldownExpired { + $elapsed = ((Get-Date) - $Script:LastSwitchTime).TotalSeconds + return $elapsed -ge $Global:SwitchCooldownSeconds +} + +function Test-HostReachable { + param( + [Parameter(Mandatory)] + [string]$Host + ) + + try { + return [bool](Test-Connection -ComputerName $Host -Count 1 -Quiet -ErrorAction Stop) + } catch { + Write-Log -Message "Host check failed for ${Host}: $_" + return $false + } +} + +function Test-ProxyDns { + try { + $result = Resolve-DnsName -Name $Global:TestDomain -Server $Global:CustomGateway -ErrorAction Stop + return [bool]$result + } catch { + Write-Log -Message "Proxy DNS check failed via $($Global:CustomGateway): $_" + return $false + } +} + +function Test-InternetTcp { + try { + return [bool](Test-NetConnection -ComputerName $Global:TestDomain -Port 443 -InformationLevel Quiet) + } catch { + Write-Log -Message "Internet TCP check failed for $($Global:TestDomain):443: $_" + return $false + } +} + +function Test-ProxyHealthy { + $gatewayReachable = Test-HostReachable -Host $Global:CustomGateway + if (-not $gatewayReachable) { + Write-Log -Message "Proxy gateway $($Global:CustomGateway) is not reachable." + return $false } - if ($isCustomGateway){ #setting gateway + $dnsOk = Test-ProxyDns + if (-not $dnsOk) { + return $false + } - # config customGateway - $Global:isCustomGateway = $true + if ($Global:UseSocksProxyHealthCheck) { + return Test-SocksProxy -ProxyHost $Global:CustomGateway -ProxyPort $Global:ProxyPort -TestHost $Global:TestDomain -TestPort 443 + } - # setting dns server - Set-DnsClientServerAddress -InterfaceAlias $adapter.name -ServerAddresses ($Global:CustomGateway, $Global:CustomGateway) - # disable dhcp - setDhcp($false) - # setting gateway to customGateway - New-NetIPAddress -InterfaceAlias $adapter.Name -AddressFamily IPv4 -IPAddress $Global:CustomIP -PrefixLength 24 -DefaultGateway $Global:CustomGateway + return $true +} + +function Set-DefaultGateway { + param( + [Parameter(Mandatory)] + [int]$InterfaceIndex, + + [Parameter(Mandatory)] + [string]$Gateway + ) + + $currentGateway = Get-CurrentDefaultGateway -InterfaceIndex $InterfaceIndex + if ($currentGateway -eq $Gateway) { + Write-Log -Message "Default gateway is already $Gateway." + return + } + + $existingRoutes = Get-NetRoute -InterfaceIndex $InterfaceIndex -DestinationPrefix "0.0.0.0/0" -ErrorAction SilentlyContinue + foreach ($route in $existingRoutes) { + Write-Log -Message "Removing default route $($route.NextHop) from interface $InterfaceIndex." + Remove-NetRoute -InterfaceIndex $InterfaceIndex -DestinationPrefix "0.0.0.0/0" -NextHop $route.NextHop -Confirm:$false + } + + Write-Log -Message "Adding default route $Gateway to interface $InterfaceIndex." + New-NetRoute -InterfaceIndex $InterfaceIndex -DestinationPrefix "0.0.0.0/0" -NextHop $Gateway -RouteMetric 1 | Out-Null +} + +function Set-ProxyDns { + param( + [Parameter(Mandatory)] + [int]$InterfaceIndex + ) + + Write-Log -Message "Setting DNS to proxy gateway $($Global:CustomGateway)." + Set-DnsClientServerAddress -InterfaceIndex $InterfaceIndex -ServerAddresses $Global:CustomGateway + Clear-DnsClientCache +} + +function Set-DirectDns { + param( + [Parameter(Mandatory)] + [int]$InterfaceIndex + ) + + if ($Global:DirectDnsServers -and $Global:DirectDnsServers.Count -gt 0) { + Write-Log -Message "Setting DNS to $($Global:DirectDnsServers -join ', ')." + Set-DnsClientServerAddress -InterfaceIndex $InterfaceIndex -ServerAddresses $Global:DirectDnsServers } else { - # config originalGateway - $Global:isCustomGateway = $false - - # setting dns server - Set-DnsClientServerAddress -InterfaceAlias $adapter.name -ServerAddresses ($Global:originalDns, $Global:originalDns) - # disable dhcp - setDhcp($false) - # setting gateway to originalGateway - New-NetIPAddress -InterfaceAlias $adapter.Name -AddressFamily IPv4 -IPAddress $Global:CustomIP -PrefixLength 24 -DefaultGateway $Global:originalGateway + Write-Log -Message "Resetting DNS to DHCP provided servers." + Set-DnsClientServerAddress -InterfaceIndex $InterfaceIndex -ResetServerAddresses } - Start-Sleep 3 + + Clear-DnsClientCache } +function Switch-ToProxy { + param( + [Parameter(Mandatory)] + [int]$InterfaceIndex + ) -function getIPAddress(){ - $IP = Get-NetIPConfiguration | Select-Object -ExpandProperty IPV4Address | Select-Object -ExpandProperty IPAddress - return $IP + Write-Log -Message "Switching to proxy gateway $($Global:CustomGateway)." -ToEventLog + Set-ProxyDns -InterfaceIndex $InterfaceIndex + Set-DefaultGateway -InterfaceIndex $InterfaceIndex -Gateway $Global:CustomGateway + $Global:isCustomGateway = $true + $Script:LastSwitchTime = Get-Date } -function getNetworkAdapter(){ - $networkAdapters = Get-NetAdapter | ForEach-Object { - [PSCustomObject]@{ - Name = $_.Name - Interface = $_.InterfaceDescription - Status = $_.Status - LinkSpeed = $_.LinkSpeed - MacAddress = $_.MacAddress - } - } - - foreach ($networkAdapter in $networkAdapters){ - if($networkAdapter.name.Contains($TargetNetworkAdapterKeyword)){ - $Global:TargetNetworkAdapter = $networkAdapter - break; - } - } - Write-Log -Message "getNetworkAdapter: $Global:TargetNetworkAdapter" +function Switch-ToDirect { + param( + [Parameter(Mandatory)] + [int]$InterfaceIndex + ) + + Write-Log -Message "Switching to direct gateway $($Global:originalGateway)." -ToEventLog + Set-DefaultGateway -InterfaceIndex $InterfaceIndex -Gateway $Global:originalGateway + Set-DirectDns -InterfaceIndex $InterfaceIndex + $Global:isCustomGateway = $false + $Script:LastSwitchTime = Get-Date } -function init { +function Initialize-GatewayKeepAlive { + if (-not (Test-IsAdministrator)) { + Request-Administrator + } + Write-Log -Message "GateWay-KeepAliveForPowershell is start" -ToEventLog - # check permission - $IsAdmin = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() - if (-not $IsAdmin.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - # if not admin permission, restart it with admin permission - Write-Log -Message "Requesting Administrator privileges..." - Start-Process pwsh -ArgumentList "-NoExit", "-File", $PSCommandPath -Verb RunAs - exit - } + Write-Log -Message "Running with Administrator privileges." - Write-Log -Message "Running with Administrator privileges!" + $adapter = Get-TargetAdapter + $mode = Get-CurrentMode -InterfaceIndex $adapter.ifIndex + $Global:isCustomGateway = $mode -eq "PROXY" + Write-Log -Message "Initial mode: $mode" - # get netAdapter instance - getNetworkAdapter - - Write-Log -Message "current adapter: $($Global:TargetNetworkAdapter.name)" - - # try to change to customGateway at start - changeGateway $Global:TargetNetworkAdapter $true + return $adapter } -function loop(){ - while($true){ - # if network offline and using customGateway, try to restore to originalGateway - if((!$(isNetworkConnected)) -and $Global:isCustomGateway){ - Write-Log -Message "Network is down, change to original gateway" -ToEventLog - changeGateway $Global:TargetNetworkAdapter $false - } +function Start-GatewayKeepAliveLoop { + param( + [Parameter(Mandatory)] + $Adapter + ) - # if customGateway proxy is working and using original gateway, try to change to customGateway - if($(Test-SocksProxy -ProxyHost $Global:CustomGateway -ProxyPort 1070) -and (!$Global:isCustomGateway)){ - Write-Log -Message "isCustomGateway $Global:isCustomGateway" - Write-Log -Message "proxy host is up, change to custom gateway" -ToEventLog - changeGateway $Global:TargetNetworkAdapter $true + while ($true) { + $mode = Get-CurrentMode -InterfaceIndex $Adapter.ifIndex + $Global:isCustomGateway = $mode -eq "PROXY" + $proxyHealthy = Test-ProxyHealthy + + if ($mode -eq "PROXY") { + $internetOk = Test-InternetTcp + if ($proxyHealthy -and $internetOk) { + $Script:ProxyFailureCount = 0 + } else { + $Script:ProxyFailureCount++ + Write-Log -Message "Proxy mode health failed $($Script:ProxyFailureCount)/$($Global:ProxyFailureThreshold)." + } + + if ($Script:ProxyFailureCount -ge $Global:ProxyFailureThreshold -and (Test-CooldownExpired)) { + Switch-ToDirect -InterfaceIndex $Adapter.ifIndex + $Script:ProxyFailureCount = 0 + $Script:ProxySuccessCount = 0 + } + } else { + if ($proxyHealthy) { + $Script:ProxySuccessCount++ + Write-Log -Message "Proxy mode candidate succeeded $($Script:ProxySuccessCount)/$($Global:ProxySuccessThreshold)." + } else { + $Script:ProxySuccessCount = 0 + } + + if ($Script:ProxySuccessCount -ge $Global:ProxySuccessThreshold -and (Test-CooldownExpired)) { + Switch-ToProxy -InterfaceIndex $Adapter.ifIndex + $Script:ProxySuccessCount = 0 + $Script:ProxyFailureCount = 0 + } } - # if customGateway proxy is not working but network still online, do nothing - - Start-Sleep 1 + Start-Sleep -Seconds $Global:HealthCheckIntervalSeconds } } -init -loop +$adapter = Initialize-GatewayKeepAlive +Start-GatewayKeepAliveLoop -Adapter $adapter diff --git a/config.ps1 b/config.ps1 index 2f1a47a..dfa272d 100644 --- a/config.ps1 +++ b/config.ps1 @@ -3,8 +3,15 @@ $Global:TestIP = "223.5.5.5" #测试是否连接网络 $Global:originalDns = "223.5.5.5" #默认DNS $Global:originalGateway = "192.168.6.1" #默认网关 $Global:CustomGateway = "192.168.6.2" #代理网关 -$Global:CustomIP = "192.168.6.4" #默认ip $Global:TargetNetworkAdapter = {} #网络适配器实例 $Global:TargetNetworkAdapterKeyword = "Ethernet" #网络适配器名字通配符 $Global:isCustomGateway = $false #是否为自定义网关 -$Global:isDebugging = $false #是否为调试模式(日志输出到控制台) \ No newline at end of file +$Global:isDebugging = $false #是否为调试模式(日志输出到控制台) + +$Global:ProxyPort = 1070 #代理网关SOCKS5检测端口 +$Global:UseSocksProxyHealthCheck = $true #是否检测SOCKS5代理端口 +$Global:DirectDnsServers = @() #直连模式DNS,留空表示恢复DHCP下发的DNS +$Global:ProxySuccessThreshold = 3 #连续成功多少次后切换到代理网关 +$Global:ProxyFailureThreshold = 3 #连续失败多少次后切回默认网关 +$Global:HealthCheckIntervalSeconds = 5 #检测间隔 +$Global:SwitchCooldownSeconds = 30 #切换后的冷却时间 diff --git a/logger.ps1 b/logger.ps1 index 666e175..763e42c 100644 --- a/logger.ps1 +++ b/logger.ps1 @@ -1,4 +1,4 @@ -. $(-Join($(Get-Location).Path, '\config.ps1')) +. (Join-Path $PSScriptRoot 'config.ps1') function Write-Log { param ( diff --git a/proxy-test.ps1 b/proxy-test.ps1 index c853bc3..a4fe40a 100644 --- a/proxy-test.ps1 +++ b/proxy-test.ps1 @@ -1,12 +1,13 @@ -. $(-Join($(Get-Location).Path, '\logger.ps1')) -. $(-Join($(Get-Location).Path, '\config.ps1')) +. (Join-Path $PSScriptRoot 'config.ps1') +. (Join-Path $PSScriptRoot 'logger.ps1') function Test-SocksProxy { param ( [string]$ProxyHost, [int]$ProxyPort, [string]$TestHost = "google.com", - [int]$TestPort = 443 + [int]$TestPort = 443, + [int]$TimeoutMilliseconds = 1500 ) $result = $false @@ -22,7 +23,10 @@ function Test-SocksProxy { $tcpClient = New-Object System.Net.Sockets.TcpClient $tcpClient.SendTimeout = 1000 $tcpClient.ReceiveTimeout = 1000 - $tcpClient.Connect($ProxyHost, $ProxyPort) + $connectTask = $tcpClient.ConnectAsync($ProxyHost, $ProxyPort) + if (-not $connectTask.Wait($TimeoutMilliseconds)) { + throw "Timed out connecting to ${ProxyHost}:$ProxyPort." + } # 代理成功连接,开始验证目标地址是否可达 $stream = $tcpClient.GetStream() @@ -33,7 +37,10 @@ function Test-SocksProxy { # 接收服务器响应 $response = New-Object byte[] 2 - $stream.Read($response, 0, $response.Length) + $bytesRead = $stream.Read($response, 0, $response.Length) + if ($bytesRead -lt $response.Length) { + throw "SOCKS handshake response is incomplete." + } if ($response[1] -ne 0x00) { throw "SOCKS handshake failed. Proxy does not support no-authentication." @@ -55,7 +62,10 @@ function Test-SocksProxy { # 接收连接响应 $connectResponse = New-Object byte[] 10 - $stream.Read($connectResponse, 0, $connectResponse.Length) + $bytesRead = $stream.Read($connectResponse, 0, $connectResponse.Length) + if ($bytesRead -lt 2) { + throw "SOCKS connect response is incomplete." + } if ($connectResponse[1] -eq 0x00) { Write-Log -Message "Proxy is valid and can connect to ${TestHost}:$TestPort."