workbuddy.xpcool.com/net-test-v2.ps1

295 lines
13 KiB
PowerShell

# =====================================================================
# net-test-v2.ps1 - Company-side network readiness probe (v2)
# ---------------------------------------------------------------------
# WHY v2: the v1 test reported "symmetric NAT" and "port whitelist",
# but BOTH conclusions were wrong, because:
# * NAT test used a NEW socket (new local port) per STUN
# server -> mapped ports differ for ANY nat type.
# * port-whitelist test used targets where the port is
# simply not listening (114.114.114.114:8080,
# github.com:443 is DNS-poisoned in CN).
# This version uses the CORRECT methods.
# Output is 100% ASCII and carries NO BOM -> immune to copy/paste.
# =====================================================================
$ErrorActionPreference = 'Continue'
$Out = New-Object System.Collections.ArrayList
function W {
param([string]$T)
[void]$Out.Add($T)
Write-Host $T
}
function Sec {
param([string]$T)
W ''
W ('===== ' + $T + ' =====')
}
$stamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$resultFile = Join-Path (Get-Location) ('net-test-v2_' + $stamp + '.txt')
# ---------------------------------------------------------------------
# Chinese vendor names, built from Unicode code points so that this file
# stays PURE ASCII (copy/paste-safe) while still matching Chinese names.
$cnSun = [char]0x5411 + [char]0x65E5 + [char]0x8475 # "sunflower" remote control
$cnHr = [char]0x706B + [char]0x7ED2 # Huorong
$cnSang = [char]0x6DF1 + [char]0x4FE1 + [char]0x670D # Sangfor
$cnQax = [char]0x5947 + [char]0x5B89 + [char]0x4FE1 # QiAnXin
$cnTq = [char]0x5929 + [char]0x64CE # TianQing
$cnAsi = [char]0x4E9A + [char]0x4FE1 # AsiaInfo
$cnLr = [char]0x8054 + [char]0x8F6F # Leagsoft
$cnUu = 'UU' + [char]0x8FDC + [char]0x7A0B # NetEase UU Remote
W ('net-test-v2 company-side network probe ' + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'))
# ---------------------------------------------------------------------
Sec '0. self check'
$selfBytes = [System.IO.File]::ReadAllBytes($PSCommandPath)
$hasBom = ($selfBytes.Length -ge 3 -and $selfBytes[0] -eq 0xEF -and $selfBytes[1] -eq 0xBB -and $selfBytes[2] -eq 0xBF)
W ('script file : ' + $PSCommandPath)
W ('script has BOM : ' + $hasBom + ' (pure ASCII script -> BOM irrelevant)')
# ---------------------------------------------------------------------
Sec '1. privileges (need admin to install a VPN driver)'
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
W ('running as Administrator : ' + $isAdmin)
try {
$grp = (net localgroup Administrators) 2>$null
W ('local Administrators group members:')
foreach ($g in $grp) { if ($g.Trim() -ne '') { W (' ' + $g.Trim()) } }
} catch { W ' (could not query localgroup)' }
W 'note: if you can install software normally, you usually have admin'
# ---------------------------------------------------------------------
Sec '2. host info'
try {
$os = Get-CimInstance Win32_OperatingSystem
W ('OS : ' + $os.Caption + ' build ' + $os.BuildNumber)
W ('RAM : ' + [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 1) + ' GB')
} catch { W 'OS info failed' }
try {
Get-CimInstance Win32_VideoController | ForEach-Object {
W ('GPU : ' + $_.Name + ' | drv ' + $_.DriverVersion)
}
} catch { W 'GPU info failed' }
try {
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue
$scr = [System.Windows.Forms.Screen]::AllScreens
foreach ($s in $scr) {
W ('SCREEN : ' + $s.Bounds.Width + 'x' + $s.Bounds.Height + ' primary=' + $s.Primary)
}
} catch { W 'screen info failed' }
# ---------------------------------------------------------------------
Sec '3. IPv6 capability (re-check)'
$v6 = Get-NetIPAddress -AddressFamily IPv6 -ErrorAction SilentlyContinue |
Where-Object { $_.IPAddress -notlike 'fe80*' -and $_.IPAddress -ne '::1' }
if ($v6) {
foreach ($a in $v6) { W ('IPv6 local : ' + $a.IPAddress + ' (' + $a.AddressState + ')') }
} else {
W 'IPv6 local : NONE -> company network is IPv4 only (IPv6 direct path is dead)'
}
# ---------------------------------------------------------------------
Sec '4. TCP egress freedom (targets that REALLY listen)'
function Test-Tcp {
param([string]$H, [int]$P, [int]$TimeoutMs = 4000)
$res = 'UNKNOWN'
try {
$ips = [System.Net.Dns]::GetHostAddresses($H) | Where-Object { $_.AddressFamily.ToString() -eq 'InterNetwork' }
if (-not $ips) { return 'DNS_FAIL' }
$sock = New-Object System.Net.Sockets.Socket('InterNetwork', 'Stream', 'Tcp')
$ar = $sock.BeginConnect((New-Object System.Net.IPEndPoint($ips[0], $P)), $null, $null)
if ($ar.AsyncWaitHandle.WaitOne($TimeoutMs, $false)) {
try { $sock.EndConnect($ar); $res = 'OPEN' }
catch { $res = 'REFUSED (path OK, port closed)' }
} else {
$res = 'TIMEOUT (blocked / dropped)'
}
$sock.Close()
} catch { $res = ('ERR ' + $_.Exception.Message) }
return $res
}
$tcpTargets = @(
@('www.baidu.com', 443, 'domestic HTTPS - baseline'),
@('www.qq.com', 443, 'domestic HTTPS - baseline'),
@('www.aliyun.com', 80, 'domestic HTTP - baseline'),
@('193.112.118.168', 80, 'your own server :80'),
@('193.112.118.168', 443, 'your own server :443'),
@('193.112.118.168', 22025, 'your own server :22025 (non-standard)'),
@('portquiz.net', 443, 'portquiz :443 (all-port listener)'),
@('portquiz.net', 8080, 'portquiz :8080 (all-port listener)'),
@('portquiz.net', 8443, 'portquiz :8443 (all-port listener)')
)
foreach ($t in $tcpTargets) {
$r = Test-Tcp -H $t[0] -P $t[1]
W ('TCP {0,-22} {1,-6} -> {2} [{3}]' -f ($t[0] + ':' + $t[1]), $t[1], $r, $t[2])
}
W 'read: if baidu:443 is OPEN then 443 is NOT whitelisted-blocked.'
W 'read: portquiz.net listens on EVERY port -> OPEN there proves arbitrary ports allowed.'
# ---------------------------------------------------------------------
Sec '5. UDP egress + CORRECT NAT mapping test'
function New-StunReq {
$b = New-Object byte[] 20
$b[0] = 0; $b[1] = 1; $b[2] = 0; $b[3] = 0
$b[4] = 0x21; $b[5] = 0x12; $b[6] = 0xA4; $b[7] = 0x42
$r = New-Object byte[] 12
(New-Object System.Random).NextBytes($r)
[Array]::Copy($r, 0, $b, 8, 12)
return , $b
}
function Invoke-Stun {
param($Udp, [string]$H, [int]$P, [int]$TimeoutMs = 3000)
try {
$ips = [System.Net.Dns]::GetHostAddresses($H) | Where-Object { $_.AddressFamily.ToString() -eq 'InterNetwork' }
if (-not $ips) { return @{ S = 'DNS_FAIL' } }
$req = New-StunReq
[void]$Udp.Send($req, $req.Length, $ips[0].ToString(), $P)
$Udp.Client.ReceiveTimeout = $TimeoutMs
$remote = New-Object System.Net.IPEndPoint([System.Net.IPAddress]'0.0.0.0', 0)
try { $resp = $Udp.Receive([ref]$remote) } catch { return @{ S = 'TIMEOUT' } }
if ($resp.Length -lt 20) { return @{ S = 'SHORT' } }
for ($i = 0; $i -lt 12; $i++) {
if ($resp[8 + $i] -ne $req[8 + $i]) { return @{ S = 'TXID_MISMATCH' } }
}
$mtype = ([int]$resp[0] * 256) + [int]$resp[1]
if ($mtype -ne 0x0101) { return @{ S = ('TYPE_' + $mtype) } }
$mlen = ([int]$resp[2] * 256) + [int]$resp[3]
$pos = 20
$end = 20 + $mlen
while ($pos + 4 -le $end) {
$atype = ([int]$resp[$pos] * 256) + [int]$resp[$pos + 1]
$alen = ([int]$resp[$pos + 2] * 256) + [int]$resp[$pos + 3]
if ($atype -eq 0x0020 -and $alen -ge 8 -and $resp[$pos + 5] -eq 1) {
$p = (([int]$resp[$pos + 6] * 256) + [int]$resp[$pos + 7]) -bxor 0x2112
$b0 = [int]$resp[$pos + 8] -bxor 0x21
$b1 = [int]$resp[$pos + 9] -bxor 0x12
$b2 = [int]$resp[$pos + 10] -bxor 0xA4
$b3 = [int]$resp[$pos + 11] -bxor 0x42
return @{ S = 'OK'; IP = ('{0}.{1}.{2}.{3}' -f $b0, $b1, $b2, $b3); Port = $p }
}
if ($atype -eq 0x0001 -and $alen -ge 8 -and $resp[$pos + 5] -eq 1) {
$p = ([int]$resp[$pos + 6] * 256) + [int]$resp[$pos + 7]
return @{ S = 'OK'; IP = ('{0}.{1}.{2}.{3}' -f $resp[$pos + 8], $resp[$pos + 9], $resp[$pos + 10], $resp[$pos + 11]); Port = $p }
}
$pos += 4 + $alen + ((4 - ($alen % 4)) % 4)
}
return @{ S = 'NO_ATTR' }
} catch { return @{ S = ('ERR ' + $_.Exception.Message) } }
}
$stunList = @(
@('stun.miwifi.com', 3478),
@('stun.hitv.com', 3478),
@('stun.qq.com', 3478),
@('stun.l.google.com', 19302),
@('stun.nextcloud.com', 443)
)
$rounds = @()
foreach ($lp in @(46001, 46002)) {
W ('--- round : fixed local UDP port ' + $lp + ' ---')
try {
$udp = New-Object System.Net.Sockets.UdpClient($lp)
} catch {
W (' bind local port failed: ' + $_.Exception.Message)
continue
}
$maps = @()
foreach ($sv in $stunList) {
$r = Invoke-Stun -Udp $udp -H $sv[0] -P $sv[1]
if ($r.S -eq 'OK') {
W (' {0,-24} -> mapped {1}:{2}' -f ($sv[0] + ':' + $sv[1]), $r.IP, $r.Port)
$maps += $r
} else {
W (' {0,-24} -> {1}' -f ($sv[0] + ':' + $sv[1]), $r.S)
}
}
$udp.Close()
$rounds += , @{ LP = $lp; Maps = $maps }
W ' => STUN replies received means UDP egress AND UDP return path work.'
}
W ''
W '--- NAT verdict ---'
$verdict = 'UNKNOWN (not enough STUN samples)'
foreach ($rd in $rounds) {
$ports = ($rd.Maps | ForEach-Object { $_.Port } | Sort-Object -Unique)
if ($ports.Count -eq 0) { continue }
W ('local port ' + $rd.LP + ' -> public ip ' + (($rd.Maps | ForEach-Object { $_.IP } | Sort-Object -Unique) -join ',') + ' mapped ports: ' + ($ports -join ','))
if ($ports.Count -eq 1 -and $rd.Maps.Count -ge 2) {
$verdict = 'ENDPOINT-INDEPENDENT MAPPING (cone NAT) -> UDP hole punching is FEASIBLE'
} elseif ($ports.Count -gt 1) {
$verdict = 'SYMMETRIC NAT (mapping changes per destination) -> hole punching unreliable, relay needed'
}
}
W ('NAT verdict : ' + $verdict)
# ---------------------------------------------------------------------
Sec '6. local proxy / tunnel clients already present'
W 'listening local TCP ports (common proxy ports):'
$common = @(1080, 1081, 1087, 2080, 2081, 7890, 7891, 7892, 7893, 7897, 8080, 8889, 10808, 10809, 10810, 20171)
$listening = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue
foreach ($p in $common) {
$hit = $listening | Where-Object { $_.LocalPort -eq $p }
if ($hit) {
$procName = ''
try { $procName = (Get-Process -Id $hit[0].OwningProcess -ErrorAction SilentlyContinue).ProcessName } catch { }
W (' LISTEN ' + $p + ' pid=' + $hit[0].OwningProcess + ' proc=' + $procName)
}
}
W '(if 7890/7897/7892 etc are LISTENING, a Clash-style proxy client is running)'
W ''
W 'installed software matching VPN / proxy / remote-control / streaming keywords:'
$keys = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
$pat = 'tailscale|zerotier|wireguard|netbird|clash|v2ray|xray|shadowsocks|trojan|sing-box|mihomo|todesk|sunlogin|anydesk|teamviewer|uu|gameviewer|parsec|moonlight|sunshine|radmin|splashtop|rustdesk|netease'
$pat = $pat + '|' + $cnSun + '|' + $cnHr + '|' + $cnSang + '|' + $cnQax + '|' + $cnTq + '|' + $cnAsi + '|' + $cnLr + '|' + $cnUu
$found = @()
foreach ($k in $keys) {
try {
Get-ItemProperty $k -ErrorAction SilentlyContinue | ForEach-Object {
$n = $_.DisplayName
if ($n -and ($n -match $pat)) { $found += $n }
}
} catch { }
}
if ($found.Count -gt 0) {
$found | Sort-Object -Unique | ForEach-Object { W (' ' + $_) }
} else { W ' (none matched)' }
# ---------------------------------------------------------------------
Sec '7. security / endpoint-management agents (may block drivers or audit traffic)'
$secPat = 'hipsdaemon|hrwsc|edr|savsvc|sangfor|huorong|qianxin|tianqing|asiainfo|symantec|mcafee|sophos|kaspersky|trend|eset|bitdefender|crowdstrike|sentinel|defender|acmp|leagsoft|atrust|anysec|zhudongfangyu|sysdiag|360'
$secPat = $secPat + '|' + $cnHr + '|' + $cnSang + '|' + $cnQax + '|' + $cnTq + '|' + $cnAsi + '|' + $cnLr
$svcs = Get-Service -ErrorAction SilentlyContinue
foreach ($s in $svcs) {
$text = ($s.Name + ' ' + $s.DisplayName)
if ($text -match $secPat) {
W (' ' + $s.Name + ' | ' + $s.DisplayName + ' | ' + $s.Status)
}
}
# ---------------------------------------------------------------------
Sec '8. summary'
W ''
W ('IPv6 available : ' + ($(if ($v6) { 'YES' } else { 'NO' })))
W ('Admin rights : ' + $isAdmin)
W ('NAT verdict : ' + $verdict)
W ''
W 'How to read the TCP section:'
W ' * www.baidu.com:443 OPEN -> 443 egress is fine (v1 result was false)'
W ' * portquiz.net:* OPEN -> arbitrary TCP ports allowed'
W ' * everything TIMEOUT -> real port whitelist, relay over 443 only'
W ''
$Out | Out-File -FilePath $resultFile -Encoding UTF8
W ('result saved to: ' + $resultFile)