260 lines
13 KiB
PowerShell
260 lines
13 KiB
PowerShell
# ============================================================
|
||
# 公司电脑网络与设备自测脚本
|
||
# 用途:验证公司电脑能否走通「远程串流家里的电脑」所需条件
|
||
# 用法:先在开始菜单打开 Windows PowerShell,再执行:
|
||
# powershell -ExecutionPolicy Bypass -File "公司电脑网络自测.ps1"
|
||
# 结果:屏幕输出 + 同目录下「网络自测结果_时间戳.txt」
|
||
# ============================================================
|
||
|
||
$ErrorActionPreference = 'Continue'
|
||
$resultFile = Join-Path $PSScriptRoot ('网络自测结果_' + (Get-Date -Format 'yyyyMMdd_HHmmss') + '.txt')
|
||
# 注意:PowerShell 变量名不区分大小写,这里刻意避开 $r / $R 这类易冲突的名字
|
||
$Lines = New-Object System.Collections.ArrayList
|
||
|
||
function Add-Line {
|
||
param([string]$Text)
|
||
[void]$Lines.Add($Text)
|
||
Write-Host $Text
|
||
}
|
||
function Add-Section {
|
||
param([string]$Title)
|
||
Add-Line ''
|
||
Add-Line ('===== ' + $Title + ' =====')
|
||
}
|
||
|
||
# 用绝对路径调用系统命令:某些受管控环境的 PATH 会被裁剪,直接写 ping / netsh 会找不到
|
||
$Sys32 = Join-Path $env:SystemRoot 'System32'
|
||
$pingExe = Join-Path $Sys32 'ping.exe'
|
||
$netshExe = Join-Path $Sys32 'netsh.exe'
|
||
|
||
# 强制走 IPv6 的 TCP 连通性测试(ICMPv6 常被系统/网关默认拦截,所以必须用 TCP 判断)
|
||
function Test-TcpIPv6 {
|
||
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() } }
|
||
}
|
||
|
||
# 把外部命令输出安全地拼成一行文本(命令无输出时返回空串,避免 Join 抛异常)
|
||
function Get-NativeText {
|
||
param($Output)
|
||
return (@($Output) -join ' ')
|
||
}
|
||
|
||
Add-Line ('公司电脑网络自测 ' + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'))
|
||
|
||
# ---------- 1. 设备与显示(决定解码能力与串流分辨率) ----------
|
||
Add-Section '1. 设备与显示'
|
||
try {
|
||
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
|
||
Add-Line ('CPU : ' + $cpu.Name.Trim())
|
||
$cs = Get-CimInstance Win32_ComputerSystem
|
||
Add-Line ('内存 : ' + [math]::Round($cs.TotalPhysicalMemory / 1GB, 1) + ' GB')
|
||
$os = Get-CimInstance Win32_OperatingSystem
|
||
Add-Line ('系统 : ' + $os.Caption + ' build ' + $os.BuildNumber)
|
||
foreach ($g in (Get-CimInstance Win32_VideoController)) {
|
||
Add-Line ('显卡 : ' + $g.Name + ' 驱动 ' + $g.DriverVersion +
|
||
' 当前 ' + $g.CurrentHorizontalResolution + 'x' + $g.CurrentVerticalResolution + '@' + $g.CurrentRefreshRate + 'Hz')
|
||
}
|
||
Add-Line '提示 : 若屏幕是 120/144Hz,请到「设置-系统-屏幕-高级显示」把刷新率手动调到最高'
|
||
}
|
||
catch { Add-Line ('读取设备信息失败: ' + $_.Exception.Message) }
|
||
|
||
# ---------- 2. IPv6 能力(最关键的一项:家里只有公网 IPv6 可直连) ----------
|
||
Add-Section '2. IPv6 能力(关键)'
|
||
try {
|
||
$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) { Add-Line ('本机 IPv6 : ' + $a.IPAddress + ' (' + $a.InterfaceAlias + ')') }
|
||
} else {
|
||
Add-Line '本机 IPv6 : 无 -> 公司是纯 IPv4 网络,家里 IPv6 直连走不通,只能靠打洞或中转'
|
||
}
|
||
# 用 TCP 而不是 ping 来判断:ICMPv6 默认会被 Windows 防火墙拦掉,ping 不通不代表 IPv6 不可用
|
||
$v6Targets = @(
|
||
@{ Name = '阿里公共DNS v6 (2400:3200::1:53)'; Addr = '2400:3200::1'; Port = 53 },
|
||
@{ Name = '电信DNS v6 (240e:4c:4008::1:53)'; Addr = '240e:4c:4008::1'; Port = 53 }
|
||
)
|
||
foreach ($t in $v6Targets) {
|
||
if (Test-TcpIPv6 -Address $t.Addr -Port $t.Port) {
|
||
Add-Line ('IPv6 出网 : ' + $t.Name + ' -> 通(公司有可用的 IPv6)')
|
||
} else {
|
||
Add-Line ('IPv6 出网 : ' + $t.Name + ' -> 不通')
|
||
}
|
||
}
|
||
Add-Line '说明 : 只要有任意一条通,说明公司网络能跑 IPv6,家里的 IPv6 直连方案就有戏'
|
||
}
|
||
catch { Add-Line ('IPv6 检测失败: ' + $_.Exception.Message) }
|
||
|
||
# ---------- 3. 代理与审计软件初筛(决定会不会被拦) ----------
|
||
Add-Section '3. 代理与审计软件初筛'
|
||
try {
|
||
$wOut = & $netshExe winhttp show proxy
|
||
$w = (Get-NativeText $wOut) -replace '\s+', ' '
|
||
Add-Line ('WinHTTP 代理 : ' + $w.Trim())
|
||
$envProxy = @()
|
||
if ($env:HTTP_PROXY) { $envProxy += ('HTTP_PROXY=' + $env:HTTP_PROXY) }
|
||
if ($env:HTTPS_PROXY) { $envProxy += ('HTTPS_PROXY=' + $env:HTTPS_PROXY) }
|
||
if ($envProxy.Count -gt 0) { Add-Line ('环境变量代理 : ' + ($envProxy -join '; ')) } else { Add-Line '环境变量代理 : 无' }
|
||
$ieProxy = Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -ErrorAction SilentlyContinue
|
||
if ($ieProxy) {
|
||
Add-Line ('系统代理 : 开关=' + $ieProxy.ProxyEnable + ' 服务器=' + $ieProxy.ProxyServer + ' PAC=' + $ieProxy.AutoConfigURL)
|
||
}
|
||
$keywords = 'Sangfor|深信服|QiAnXin|奇安信|360|TianQing|天擎|NSFOCUS|绿盟|EDR|Sentinel|CrowdStrike|Symantec|McAfee|Sophos|ESET|Kaspersky|卡巴|Huorong|火绒|IP-guard|亿赛通|天锐|网神|亚信|安恒|Forcepoint|Zscaler|Netskope|Umbrella|AnyConnect'
|
||
$hit = @()
|
||
$hit += Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.Name -match $keywords }
|
||
$hit += Get-Service -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -match $keywords }
|
||
if ($hit.Count -gt 0) {
|
||
Add-Line '命中管控/安全类软件(可能拦截远控或加密流量):'
|
||
$hit | Select-Object -First 12 | ForEach-Object { Add-Line (' - ' + $_.Name) }
|
||
} else {
|
||
Add-Line '未发现明显的安全审计类软件'
|
||
}
|
||
}
|
||
catch { Add-Line ('代理/审计检测失败: ' + $_.Exception.Message) }
|
||
|
||
# ---------- 4. 出站 TCP 端口自由度(是否只放行 80/443) ----------
|
||
Add-Section '4. 出站 TCP 端口自由度'
|
||
$tcpTargets = @(
|
||
@{ Name = 'github.com:22 (SSH)'; Host = 'github.com'; Port = 22 },
|
||
@{ Name = 'github.com:443 (HTTPS)'; Host = 'github.com'; Port = 443 },
|
||
@{ Name = '114.114.114.114:53 (TCP DNS)'; Host = '114.114.114.114'; Port = 53 },
|
||
@{ Name = '114.114.114.114:8080'; Host = '114.114.114.114'; Port = 8080 }
|
||
)
|
||
foreach ($t in $tcpTargets) {
|
||
try {
|
||
$c = Test-NetConnection -ComputerName $t.Host -Port $t.Port -InformationLevel Quiet -WarningAction SilentlyContinue
|
||
if ($c) { Add-Line ($t.Name.PadRight(28) + ' -> 通') } else { Add-Line ($t.Name.PadRight(28) + ' -> 不通或被拦') }
|
||
} catch { Add-Line ($t.Name.PadRight(28) + ' -> 检测异常') }
|
||
}
|
||
Add-Line '说明 : 若只有 443 通、非标端口不通,说明公司做了端口白名单,串流要依赖打洞或中转'
|
||
|
||
# ---------- 5. UDP 出站能力(串流视频走 UDP,最关键的一测) ----------
|
||
Add-Section '5. UDP 出站能力(关键)'
|
||
|
||
# 5.1 DNS over UDP 53:最基础的 UDP 通断验证
|
||
try {
|
||
$udp = New-Object System.Net.Sockets.UdpClient
|
||
$udp.Client.ReceiveTimeout = 3000
|
||
$srv = New-Object System.Net.IPEndPoint ([System.Net.IPAddress]::Parse('114.114.114.114'), 53)
|
||
$q = [byte[]](0xAB,0xCD, 0x01,0x00, 0x00,0x01, 0x00,0x00, 0x00,0x00, 0x00,0x00,
|
||
0x02,0x71,0x71, 0x03,0x63,0x6F,0x6D, 0x00, 0x00,0x01, 0x00,0x01)
|
||
[void]$udp.Send($q, $q.Length, $srv)
|
||
$remote = New-Object System.Net.IPEndPoint ([System.Net.IPAddress]::Any, 0)
|
||
$resp = $udp.Receive([ref]$remote)
|
||
$udp.Close()
|
||
Add-Line ('UDP/53 (114.114.114.114) -> 通,收到 ' + $resp.Length + ' 字节')
|
||
}
|
||
catch { Add-Line ('UDP/53 -> 不通或被拦 : ' + $_.Exception.Message) }
|
||
|
||
# 5.2 STUN:验证任意高位 UDP 端口能否出去,并取出 NAT 映射出的公网地址
|
||
function Invoke-StunProbe {
|
||
param([string]$Server, [int]$Port = 3478, [int]$TimeoutMs = 3000)
|
||
$res = New-Object psobject -Property @{ Server = ($Server + ':' + $Port); Ok = $false; Mapped = ''; LocalPort = 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 解析失败' }
|
||
$udp = New-Object System.Net.Sockets.UdpClient 0
|
||
$udp.Client.ReceiveTimeout = $TimeoutMs
|
||
$res.LocalPort = $udp.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]$udp.Send($pkt, 20, $ep)
|
||
$remote = New-Object System.Net.IPEndPoint ([System.Net.IPAddress]::Any, 0)
|
||
$resp = $udp.Receive([ref]$remote)
|
||
$udp.Close()
|
||
$pos = 20
|
||
while ($pos + 4 -le $resp.Length) {
|
||
# 注意:必须先把 byte 转成 int 再位移,否则 [byte] -shl 8 会溢出归零
|
||
$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)
|
||
$res.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])
|
||
$res.Mapped = (New-Object System.Net.IPAddress (, $b)).ToString() + ':' + $mp
|
||
break
|
||
}
|
||
$pos += 4 + $alen + ((4 - ($alen % 4)) % 4)
|
||
}
|
||
$res.Ok = $true
|
||
} catch { $res.Err = $_.Exception.Message }
|
||
return $res
|
||
}
|
||
|
||
$stunServers = @('stun.miwifi.com', 'stun.qq.com', 'stun.hitv.com')
|
||
$mappedPorts = @()
|
||
foreach ($s in $stunServers) {
|
||
$r = Invoke-StunProbe -Server $s
|
||
if ($r.Ok) {
|
||
Add-Line ('STUN ' + $r.Server.PadRight(24) + ' -> 通 本地端口 ' + $r.LocalPort + ' 映射为 ' + $r.Mapped)
|
||
if ($r.Mapped) { $mappedPorts += ($r.Mapped -split ':')[-1] }
|
||
} else {
|
||
Add-Line ('STUN ' + $r.Server.PadRight(24) + ' -> 不通 : ' + $r.Err)
|
||
}
|
||
}
|
||
if ($mappedPorts.Count -ge 2) {
|
||
$uniq = ($mappedPorts | Sort-Object -Unique).Count
|
||
if ($uniq -eq 1) {
|
||
Add-Line 'NAT 判断 : 多次映射端口一致 -> 锥形 NAT,P2P 打洞成功率高(好消息)'
|
||
} else {
|
||
Add-Line 'NAT 判断 : 每次映射端口都不同 -> 对称 NAT,P2P 打洞较难,需要中继兜底'
|
||
}
|
||
}
|
||
Add-Line '说明 : STUN 能通 = 高位 UDP 端口可出网,这是 Sunshine/Moonlight 直连的前提'
|
||
|
||
# ---------- 6. 到家里的链路(核心结论所在) ----------
|
||
Add-Section '6. 到家里的链路(核心)'
|
||
# 家里主机的公网 IPv6(若网关重新拨号 / 前缀变化,需要重新获取后替换)
|
||
$homeV6 = '240e:338:263:3600:e591:cb62:3503:8a23'
|
||
# 家里的运营商出口 IPv4(100.64 开头 = 运营商级 NAT,无法直连,这是正常现象)
|
||
$homeV4 = ''
|
||
|
||
if ($homeV6 -ne '') {
|
||
# 先 TCP 再 ping:TCP 通即可确认入站放行;ping 不通多半只是 ICMPv6 被拦,不必惊慌
|
||
# 端口 38443 对应家里临时监听;正式启用 Sunshine 后应改成 47989 / 47990
|
||
if (Test-TcpIPv6 -Address $homeV6 -Port 38443) {
|
||
Add-Line '家里 IPv6:38443 -> 通 ★ IPv6 入站放行,可以走「最优路径:IPv6 直连」'
|
||
} else {
|
||
Add-Line '家里 IPv6:38443 -> 不通(可能网关 IPv6 防火墙拦了入站,或家里测试监听没开)'
|
||
}
|
||
$pOut = & $pingExe -6 -n 2 -w 2000 $homeV6
|
||
$p = Get-NativeText $pOut
|
||
if ($p -match 'Reply from|来自') { Add-Line 'ping6 家里 -> 通' } else { Add-Line 'ping6 家里 -> 不通(ICMPv6 常被默认拦截,仅供参考)' }
|
||
} else { Add-Line '未填写家里 IPv6,跳过' }
|
||
|
||
if ($homeV4 -ne '') {
|
||
$pOut = & $pingExe -n 2 -w 2000 $homeV4
|
||
$p = Get-NativeText $pOut
|
||
if ($p -match 'Reply from|来自') { Add-Line 'ping4 家里出口 -> 通' } else { Add-Line 'ping4 家里出口 -> 不通(被运营商 NAT 拦,属正常)' }
|
||
} else { Add-Line '未填写家里 IPv4,跳过' }
|
||
|
||
# ---------- 收尾 ----------
|
||
Add-Section '自测完成'
|
||
Add-Line '把结果文件发给 AI 助手,据此决定走 IPv6 直连 / 打洞 / 中转。'
|
||
Add-Line ('结果已保存: ' + $resultFile)
|
||
try { $Lines | Out-File -FilePath $resultFile -Encoding UTF8 } catch { Write-Host ('写文件失败: ' + $_.Exception.Message) }
|
||
Write-Host ''
|
||
Write-Host ('结果文件:' + $resultFile)
|