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