295 lines
13 KiB
PowerShell
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)
|