# ============================================================ # 公司电脑网络与设备自测脚本 # 用途:验证公司电脑能否走通「远程串流家里的电脑」所需条件 # 用法:先在开始菜单打开 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)