# ============================================================================ # 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) }