246 lines
11 KiB
PowerShell
246 lines
11 KiB
PowerShell
# ============================================================================
|
|
# Network readiness self-test (run on the CLIENT / office PC)
|
|
#
|
|
# Purpose: decide whether this machine can act as a Sunshine + Moonlight
|
|
# streaming client back to the home PC over public IPv6.
|
|
#
|
|
# This file is intentionally PURE ASCII on purpose, so that it survives being
|
|
# copy-pasted between machines. It does NOT require a UTF-8 BOM.
|
|
# All output is plain English.
|
|
#
|
|
# Run:
|
|
# powershell -ExecutionPolicy Bypass -File net-test.ps1
|
|
#
|
|
# A result file is written next to the script:
|
|
# net-test-result_YYYYMMDD_HHmmss.txt
|
|
# ============================================================================
|
|
|
|
$ErrorActionPreference = 'SilentlyContinue'
|
|
$Sys32 = Join-Path $env:SystemRoot 'System32'
|
|
$netshExe = Join-Path $Sys32 'netsh.exe'
|
|
|
|
$Report = New-Object System.Collections.ArrayList
|
|
|
|
function W([string]$Text) {
|
|
[void]$Report.Add($Text)
|
|
Write-Host $Text
|
|
}
|
|
function SEC([string]$Title) {
|
|
W ''
|
|
W ('===== ' + $Title + ' =====')
|
|
}
|
|
|
|
# ---- EDIT ME if the home address changes ----------------------------------
|
|
$HomeIPv6 = '240e:338:263:3600:e591:cb62:3503:8a23'
|
|
$HomePort = 38443
|
|
# ---------------------------------------------------------------------------
|
|
|
|
W '=========================================================='
|
|
W ' Network self-test for remote game streaming (client side)'
|
|
W (' Time: ' + (Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))
|
|
W '=========================================================='
|
|
|
|
# Self encoding check: proves that a missing BOM cannot break this file.
|
|
try {
|
|
$sb = [System.IO.File]::ReadAllBytes($MyInvocation.MyCommand.Path)
|
|
$hasBom = ($sb.Length -ge 3 -and $sb[0] -eq 0xEF -and $sb[1] -eq 0xBB -and $sb[2] -eq 0xBF)
|
|
if ($hasBom) { W 'Script encoding : UTF-8 with BOM' }
|
|
else { W 'Script encoding : no BOM (file is pure ASCII - harmless)' }
|
|
} catch { }
|
|
|
|
# ===================== 1. DEVICE & DISPLAY =====================
|
|
SEC '1. DEVICE & DISPLAY'
|
|
$os = Get-CimInstance Win32_OperatingSystem
|
|
if ($os) { W ('OS : ' + $os.Caption + ' build ' + $os.BuildNumber + ' ' + $os.OSArchitecture) }
|
|
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
|
|
if ($cpu) { W ('CPU : ' + ([string]$cpu.Name).Trim() + ' (' + $cpu.NumberOfCores + 'C/' + $cpu.NumberOfLogicalProcessors + 'T)') }
|
|
$cs = Get-CimInstance Win32_ComputerSystem
|
|
if ($cs) { W ('RAM : ' + [math]::Round($cs.TotalPhysicalMemory / 1GB, 1) + ' GB') }
|
|
|
|
foreach ($g in (Get-CimInstance Win32_VideoController)) {
|
|
$hz = $g.CurrentRefreshRate
|
|
if (-not $hz -or $hz -le 0) { $hz = 'unknown' } else { $hz = ($hz.ToString() + 'Hz') }
|
|
W ('GPU : ' + $g.Name + ' drv ' + $g.DriverVersion + ' ' + $hz)
|
|
}
|
|
|
|
try {
|
|
Add-Type -AssemblyName System.Windows.Forms
|
|
foreach ($s in [System.Windows.Forms.Screen]::AllScreens) {
|
|
$prim = ''
|
|
if ($s.Primary) { $prim = ' (primary)' }
|
|
W ('Display : ' + $s.DeviceName + ' ' + $s.Bounds.Width + 'x' + $s.Bounds.Height + $prim)
|
|
}
|
|
} catch { }
|
|
|
|
W 'HINT : a 60Hz panel costs ~17ms per frame. If your panel is 120/144Hz,'
|
|
W ' go to Settings > System > Display > Advanced and set it to max.'
|
|
|
|
# ===================== 2. IPv6 =====================
|
|
SEC '2. IPv6'
|
|
$v6 = Get-NetIPAddress -AddressFamily IPv6 -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.IPAddress -notlike 'fe80*' -and $_.IPAddress -ne '::1' -and $_.PrefixOrigin -ne 'WellKnown' }
|
|
if ($v6) {
|
|
foreach ($a in $v6) { W ('Local IPv6: ' + $a.IPAddress + ' via ' + $a.InterfaceAlias) }
|
|
} else {
|
|
W 'Local IPv6: NONE -> IPv6 direct connection is NOT possible from this PC'
|
|
}
|
|
|
|
$egress = ''
|
|
foreach ($u in @('https://api6.ipify.org', 'https://ipv6.icanhazip.com')) {
|
|
try {
|
|
$r = Invoke-WebRequest -Uri $u -TimeoutSec 10 -UseBasicParsing
|
|
if ($r.Content) { $egress = ([string]$r.Content).Trim(); break }
|
|
} catch { }
|
|
}
|
|
if ($egress) { W ('IPv6 egress: OK your public IPv6 = ' + $egress) }
|
|
else { W 'IPv6 egress: FAIL (no IPv6 route to the internet, or blocked)' }
|
|
|
|
# ===================== 3. PROXY & SECURITY SOFTWARE =====================
|
|
SEC '3. PROXY & SECURITY SOFTWARE'
|
|
$wOut = & $netshExe winhttp show proxy
|
|
W ('WinHTTP : ' + ((@($wOut) -join ' ') -replace '\s+', ' ').Trim())
|
|
|
|
$ie = Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -ErrorAction SilentlyContinue
|
|
if ($ie) { W ('IE proxy : enable=' + $ie.ProxyEnable + ' server=' + $ie.ProxyServer + ' pac=' + $ie.AutoConfigURL) }
|
|
|
|
$watch = 'zhudongfangyu|HipsDaemon|HipsTray|usysdiag|360|qqpcmgr|QQPCTray|Symantec|McAfee|CrowdStrike|SentinelAgent|Sophos|ekrn|avp|Kaspersky|TmListen|SangFor|EDRAgent|AnyDesk|ToDesk|sunlogin|oray'
|
|
$hits = Get-Process -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.ProcessName -match $watch } |
|
|
Select-Object -ExpandProperty ProcessName -Unique
|
|
if ($hits) { foreach ($h in $hits) { W ('Running : ' + $h) } }
|
|
else { W 'Running : no known security / audit / remote-control tool detected' }
|
|
|
|
# ===================== 4. UDP & STUN (NAT type) =====================
|
|
SEC '4. UDP & STUN (NAT type)'
|
|
try {
|
|
$udp = New-Object System.Net.Sockets.UdpClient 0
|
|
$udp.Client.ReceiveTimeout = 3000
|
|
$dns = [System.Net.IPAddress]::Parse('114.114.114.114')
|
|
$q = [byte[]](0x12,0x34,0x01,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,
|
|
0x03,0x77,0x77,0x77,0x03,0x63,0x6f,0x6d,0x00,0x00,0x01,0x00,0x01)
|
|
$ep = New-Object System.Net.IPEndPoint ($dns, 53)
|
|
[void]$udp.Send($q, $q.Length, $ep)
|
|
$rm = New-Object System.Net.IPEndPoint ([System.Net.IPAddress]::Any, 0)
|
|
$resp = $udp.Receive([ref]$rm)
|
|
$udp.Close()
|
|
W ('UDP/53 : OK (' + $resp.Length + ' bytes from 114.114.114.114)')
|
|
} catch {
|
|
W ('UDP/53 : FAILED - high-port UDP may be blocked (' + $_.Exception.Message + ')')
|
|
}
|
|
|
|
function Invoke-Stun {
|
|
param([string]$Server, [int]$Port = 3478, [int]$TimeoutMs = 3000)
|
|
$o = New-Object psobject -Property @{ Server = ($Server + ':' + $Port); Ok = $false; Mapped = ''; Local = 0; Err = '' }
|
|
try {
|
|
$ip = ([System.Net.Dns]::GetHostAddresses($Server) |
|
|
Where-Object { $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork } |
|
|
Select-Object -First 1)
|
|
if (-not $ip) { throw 'dns resolution failed' }
|
|
$u = New-Object System.Net.Sockets.UdpClient 0
|
|
$u.Client.ReceiveTimeout = $TimeoutMs
|
|
$o.Local = $u.Client.LocalEndPoint.Port
|
|
$ep = New-Object System.Net.IPEndPoint ($ip, $Port)
|
|
$pkt = New-Object byte[] 20
|
|
$pkt[0] = 0x00; $pkt[1] = 0x01
|
|
$pkt[4] = 0x21; $pkt[5] = 0x12; $pkt[6] = 0xA4; $pkt[7] = 0x42
|
|
$rnd = New-Object System.Random
|
|
for ($i = 8; $i -lt 20; $i++) { $pkt[$i] = [byte]$rnd.Next(0, 256) }
|
|
[void]$u.Send($pkt, 20, $ep)
|
|
$rm2 = New-Object System.Net.IPEndPoint ([System.Net.IPAddress]::Any, 0)
|
|
$resp = $u.Receive([ref]$rm2)
|
|
$u.Close()
|
|
$pos = 20
|
|
while ($pos + 4 -le $resp.Length) {
|
|
# NOTE: cast to [int] before shifting - [byte] -shl 8 overflows to 0
|
|
$atype = ([int]$resp[$pos] -shl 8) -bor [int]$resp[$pos + 1]
|
|
$alen = ([int]$resp[$pos + 2] -shl 8) -bor [int]$resp[$pos + 3]
|
|
if ($atype -eq 0x0020 -and $alen -ge 8) {
|
|
$b = [byte[]]$resp[($pos + 8)..($pos + 11)]
|
|
$b[0] = [byte]($b[0] -bxor 0x21); $b[1] = [byte]($b[1] -bxor 0x12)
|
|
$b[2] = [byte]($b[2] -bxor 0xA4); $b[3] = [byte]($b[3] -bxor 0x42)
|
|
$xp = ((([int]$resp[$pos + 6] -shl 8) -bor [int]$resp[$pos + 7]) -bxor 0x2112)
|
|
$o.Mapped = (New-Object System.Net.IPAddress (, $b)).ToString() + ':' + $xp
|
|
break
|
|
}
|
|
elseif ($atype -eq 0x0001 -and $alen -ge 8) {
|
|
$b = [byte[]]$resp[($pos + 8)..($pos + 11)]
|
|
$mp = (([int]$resp[$pos + 6] -shl 8) -bor [int]$resp[$pos + 7])
|
|
$o.Mapped = (New-Object System.Net.IPAddress (, $b)).ToString() + ':' + $mp
|
|
break
|
|
}
|
|
$pos += 4 + $alen + ((4 - ($alen % 4)) % 4)
|
|
}
|
|
$o.Ok = $true
|
|
} catch { $o.Err = $_.Exception.Message }
|
|
return $o
|
|
}
|
|
|
|
$mappedPorts = @()
|
|
foreach ($srv in @('stun.miwifi.com', 'stun.qq.com', 'stun.hitv.com')) {
|
|
$r = Invoke-Stun -Server $srv
|
|
if ($r.Ok -and $r.Mapped -ne '') {
|
|
W ('STUN ' + $r.Server + ' -> mapped ' + $r.Mapped + ' (local port ' + $r.Local + ')')
|
|
$mappedPorts += ($r.Mapped.Split(':')[-1])
|
|
} elseif ($r.Err -ne '') {
|
|
W ('STUN ' + $r.Server + ' -> failed: ' + $r.Err)
|
|
} else {
|
|
W ('STUN ' + $r.Server + ' -> no response')
|
|
}
|
|
}
|
|
if ($mappedPorts.Count -ge 2) {
|
|
$uniq = ($mappedPorts | Sort-Object -Unique).Count
|
|
if ($uniq -eq 1) { W ('NAT type : CONE (identical mapped port) -> UDP hole punching likely OK') }
|
|
else { W ('NAT type : SYMMETRIC (mapped ports ' + ($mappedPorts -join ', ') + ') -> UDP hole punching difficult') }
|
|
} else {
|
|
W 'NAT type : UNKNOWN (not enough STUN replies)'
|
|
}
|
|
|
|
# ===================== 5. HOME REACHABILITY (TCP) =====================
|
|
SEC '5. HOME REACHABILITY (TCP)'
|
|
function Test-Tcp6 {
|
|
param([string]$Address, [int]$Port, [int]$TimeoutMs = 5000)
|
|
$c = $null
|
|
try {
|
|
$c = New-Object System.Net.Sockets.TcpClient ([System.Net.Sockets.AddressFamily]::InterNetworkV6)
|
|
$iar = $c.BeginConnect($Address, $Port, $null, $null)
|
|
$ok = $iar.AsyncWaitHandle.WaitOne($TimeoutMs, $false)
|
|
if ($ok -and $c.Connected) { return $true }
|
|
return $false
|
|
} catch { return $false }
|
|
finally { if ($c) { $c.Close() } }
|
|
}
|
|
|
|
if ($HomeIPv6 -ne '') {
|
|
if (Test-Tcp6 -Address $HomeIPv6 -Port $HomePort) {
|
|
W ('TCP [' + $HomeIPv6 + ']:' + $HomePort + ' -> OK *** IPv6 INBOUND WORKS ***')
|
|
W ' => Path 1 (Sunshine + Moonlight over IPv6 direct) is VIABLE.'
|
|
} else {
|
|
W ('TCP [' + $HomeIPv6 + ']:' + $HomePort + ' -> FAILED')
|
|
W ' => Either the home listener is down, or the home gateway blocks IPv6 inbound.'
|
|
W ' => Fall back to hole punching (Tailscale) or a relay.'
|
|
}
|
|
} else { W 'Home IPv6 not configured - skipped.' }
|
|
|
|
# ===================== 6. SUMMARY =====================
|
|
SEC 'SUMMARY'
|
|
$v6ok = ($v6 -ne $null)
|
|
$inOk = $false
|
|
if ($HomeIPv6 -ne '') { $inOk = Test-Tcp6 -Address $HomeIPv6 -Port $HomePort }
|
|
W ('IPv6 address present : ' + $v6ok)
|
|
W ('IPv6 internet egress : ' + $(if ($egress) { 'OK' } else { 'FAIL' }))
|
|
W ('Home IPv6 reachable : ' + $inOk)
|
|
if ($v6ok -and $egress -and $inOk) { W 'VERDICT: good to go - use Moonlight over IPv6 direct.' }
|
|
elseif ($v6ok -and $egress) { W 'VERDICT: IPv6 works but home is not reachable - check home listener/gateway.' }
|
|
else { W 'VERDICT: no usable IPv6 here - the office network likely blocks it.' }
|
|
|
|
# ===================== WRITE RESULT FILE =====================
|
|
$outFile = Join-Path (Get-Location).Path ('net-test-result_' + (Get-Date).ToString('yyyyMMdd_HHmmss') + '.txt')
|
|
try {
|
|
$Report | Out-File -FilePath $outFile -Encoding UTF8
|
|
Write-Host ''
|
|
Write-Host ('Result saved to: ' + $outFile)
|
|
} catch {
|
|
Write-Host ''
|
|
Write-Host ('Could not write result file: ' + $_.Exception.Message)
|
|
}
|