PowerShell 技能连载 - 网络故障排查工具集

适用于 PowerShell 5.1 及以上版本

网络故障是运维工作中最常见也最令人头疼的问题类型。当用户反馈”系统连不上”时,问题可能出在 DNS 解析、防火墙规则、TCP 端口不通、SSL 证书过期、路由环路等任何一个环节。传统做法是依次打开命令行窗口,手动执行 ping、tracert、nslookup、telnet 等工具,逐一排除可能的原因——这个过程既繁琐又容易遗漏关键检查项。

更糟糕的是,不同工具的输出格式各异,很难快速汇总为一份完整的诊断结论。当你在深夜被叫起来处理紧急故障时,最需要的是一个能一键完成所有网络层检测、并直接给出问题定位的工具,而不是在多个黑窗口之间来回切换、靠经验猜测瓶颈在哪一跳。

PowerShell 提供了 Test-NetConnectionResolve-DnsNameTest-Connection 等原生网络 cmdlet,配合 .NET 的 System.Net.SocketsSystem.Net.Security 类,完全可以构建一套功能完备的网络诊断工具集。本文将从连接性测试、DNS 诊断、综合诊断报告三个层面,展示如何用 PowerShell 打造一键式网络故障排查方案。

连接性测试:TCP 端口扫描与延迟测量

排查网络故障的第一步是确认目标主机各端口的可达性。下面的脚本封装了 TCP 连接测试、HTTP/HTTPS 探测和延迟测量功能,支持批量检测多个目标的多个端口,并以结构化对象返回结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
function Test-NetworkConnectivity {
<#
.SYNOPSIS
测试网络连接性:TCP 端口扫描、HTTP 探测、延迟测量
.PARAMETER ComputerName
目标主机名或 IP 地址,支持数组
.PARAMETER Ports
要测试的 TCP 端口列表
.PARAMETER TimeoutMs
连接超时时间(毫秒),默认 3000
.PARAMETER TestHttp
是否对 80/443 端口执行 HTTP/HTTPS 探测
#>
param(
[Parameter(Mandatory)]
[string[]]$ComputerName,
[int[]]$Ports = @(22, 80, 443, 3389, 8080),
[int]$TimeoutMs = 3000,
[switch]$TestHttp
)

$results = @()

foreach ($target in $ComputerName) {
Write-Host "`n[*] 正在测试目标: $target" -ForegroundColor Cyan

# ICMP 延迟测量
$ping = Test-Connection -ComputerName $target -Count 4 -ErrorAction SilentlyContinue
if ($ping) {
$avgLatency = ($ping | Measure-Object -Property ResponseTime -Average).Average
$jitter = [math]::Round(($ping | Measure-Object -Property ResponseTime -StandardDeviation).StandardDeviation, 2)
$packetLoss = [math]::Round((1 - ($ping.Count / 4)) * 100, 1)
Write-Host " ICMP: 平均延迟 ${avgLatency}ms, 抖动 ${jitter}ms, 丢包率 ${packetLoss}%" -ForegroundColor Green
} else {
$avgLatency = $null
$jitter = $null
$packetLoss = 100
Write-Host " ICMP: 目标不可达" -ForegroundColor Red
}

# TCP 端口扫描
foreach ($port in $Ports) {
$tcpClient = New-Object System.Net.Sockets.TcpClient
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()

try {
$asyncResult = $tcpClient.BeginConnect($target, $port, $null, $null)
$waited = $asyncResult.AsyncWaitHandle.WaitOne($TimeoutMs, $false)

if ($waited) {
$tcpClient.EndConnect($asyncResult)
$stopwatch.Stop()
$status = "Open"
$latencyMs = $stopwatch.ElapsedMilliseconds
$color = "Green"
} else {
$stopwatch.Stop()
$status = "Timeout"
$latencyMs = $TimeoutMs
$color = "Yellow"
}
} catch {
$stopwatch.Stop()
$status = "Closed"
$latencyMs = $stopwatch.ElapsedMilliseconds
$color = "Red"
} finally {
$tcpClient.Close()
}

$httpInfo = $null
if ($TestHttp -and $status -eq "Open" -and $port -in @(80, 443)) {
$scheme = if ($port -eq 443) { "https" } else { "http" }
try {
$response = Invoke-WebRequest -Uri "${scheme}://${target}" `
-UseBasicParsing -TimeoutSec 5 -ErrorAction Stop
$httpInfo = @{
StatusCode = $response.StatusCode
Server = $response.Headers["Server"]
}
} catch {
$httpInfo = @{ StatusCode = $_.Exception.Response.StatusCode.Value__ }
}
}

$result = [PSCustomObject]@{
Target = $target
Port = $port
Status = $status
LatencyMs = $latencyMs
ICMPaAvgMs = $avgLatency
ICMPJitter = $jitter
ICMPLossPct = $packetLoss
HttpInfo = $httpInfo
Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
}
$results += $result

$httpStr = if ($httpInfo) { " | HTTP $($httpInfo.StatusCode)" } else { "" }
Write-Host " 端口 ${port}: ${status} (${latencyMs}ms)${httpStr}" -ForegroundColor $color
}
}

return $results
}

# 执行示例:检测 Web 服务器的常见端口
Test-NetworkConnectivity -ComputerName "example.com" -Ports @(22, 80, 443, 8080) -TestHttp
1
2
3
4
5
6
7
8
9
10
11
12
13
[*] 正在测试目标: example.com
ICMP: 平均延迟 42.25ms, 抖动 3.17ms, 丢包率 0%
端口 22: Timeout (3000ms)
端口 80: Open (38ms) | HTTP 200
端口 443: Open (45ms) | HTTP 200
端口 8080: Closed (1ms)

Target Port Status LatencyMs ICMPaAvgMs ICMPJitter ICMPLossPct HttpInfo Timestamp
------ ---- ------ --------- ---------- ---------- ----------- -------- ---------
example.com 22 Timeout 3000 42.25 3.17 0 2026-02-10 08:15:23
example.com 80 Open 38 42.25 3.17 0 {StatusCode, Server} 2026-02-10 08:15:23
example.com 443 Open 45 42.25 3.17 0 {StatusCode, Server} 2026-02-10 08:15:24
example.com 8080 Closed 1 42.25 3.17 0 2026-02-10 08:15:24

从输出结果可以看到,脚本一次性完成了 ICMP 延迟测试和 4 个端口的 TCP 连接检测,并对 80 和 443 端口额外执行了 HTTP 探测,返回了状态码。这种结构化的输出结果可以直接导出为 CSV 或 HTML 报告,方便归档和对比。

DNS 诊断:解析链路与缓存检查

DNS 解析故障常常伪装成网络不通——域名解析不到、解析到了错误地址、或者解析延迟过高都会导致业务异常。下面的脚本实现了完整的 DNS 诊断链路:递归解析追踪、本地缓存检查、SOA 记录验证和区域传输测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
function Test-DnsResolution {
<#
.SYNOPSIS
执行全面的 DNS 诊断:解析追踪、缓存检查、区域传输测试
.PARAMETER DomainName
要诊断的域名
.PARAMETER DnsServer
指定 DNS 服务器(默认使用系统配置)
.PARAMETER RecordTypes
要查询的记录类型列表
#>
param(
[Parameter(Mandatory)]
[string]$DomainName,
[string]$DnsServer,
[string[]]$RecordTypes = @("A", "AAAA", "CNAME", "MX", "NS", "TXT", "SOA")
)

Write-Host "`n========== DNS 诊断报告 ==========" -ForegroundColor Cyan
Write-Host "目标域名: $DomainName"
Write-Host "诊断时间: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host "===================================`n"

# 1. 解析各类型记录
Write-Host "[1] DNS 记录查询" -ForegroundColor Yellow
foreach ($rtype in $RecordTypes) {
try {
$params = @{ Name = $DomainName; Type = $rtype; ErrorAction = "Stop" }
if ($DnsServer) { $params["Server"] = $DnsServer }

$records = Resolve-DnsName @params
foreach ($rec in $records) {
$section = $rec.Section
$ttl = if ($rec.TTL) { "$($rec.TTL)s" } else { "N/A" }
$value = switch ($rtype) {
"A" { $rec.IPAddress }
"AAAA" { $rec.IPAddress }
"MX" { "$($rec.Preference) $($rec.NameExchange)" }
"NS" { $rec.NameHost }
"TXT" { $rec.Strings -join " " }
"SOA" { "$($rec.PrimaryServer) Serial=$($rec.Serial)" }
"CNAME"{ $rec.NameHost }
default { $rec.IPAddress }
}
Write-Host " ${rtype}: $value (TTL: $ttl, Section: $section)" -ForegroundColor Green
}
} catch {
Write-Host " ${rtype}: 未找到记录 ($($_.Exception.Message))" -ForegroundColor DarkGray
}
}

# 2. DNS 解析计时
Write-Host "`n[2] 解析性能测试" -ForegroundColor Yellow
$timings = @()
for ($i = 1; $i -le 5; $i++) {
$sw = [System.Diagnostics.Stopwatch]::StartNew()
try {
$null = Resolve-DnsName -Name $DomainName -Type A -ErrorAction Stop
$sw.Stop()
$timings += $sw.ElapsedMilliseconds
Write-Host " 第 ${i} 次: $($sw.ElapsedMilliseconds)ms" -ForegroundColor Green
} catch {
$sw.Stop()
$timings += $sw.ElapsedMilliseconds
Write-Host " 第 ${i} 次: 失败 ($($sw.ElapsedMilliseconds)ms)" -ForegroundColor Red
}
}
$avgTime = [math]::Round(($timings | Measure-Object -Average).Average, 2)
$maxTime = ($timings | Measure-Object -Maximum).Maximum
Write-Host " 平均: ${avgTime}ms, 最大: ${maxTime}ms" -ForegroundColor Cyan

# 3. NS 服务器可达性检查
Write-Host "`n[3] NS 服务器可达性" -ForegroundColor Yellow
try {
$nsRecords = Resolve-DnsName -Name $DomainName -Type NS -ErrorAction Stop
foreach ($ns in $nsRecords) {
$nsHost = $ns.NameHost
if ($nsHost) {
$ping = Test-Connection -ComputerName $nsHost -Count 2 -ErrorAction SilentlyContinue
if ($ping) {
$latency = [math]::Round(($ping | Measure-Object -Property ResponseTime -Average).Average, 2)
Write-Host " ${nsHost}: 在线 (${latency}ms)" -ForegroundColor Green
} else {
Write-Host " ${nsHost}: 不可达" -ForegroundColor Red
}
}
}
} catch {
Write-Host " 无法获取 NS 记录" -ForegroundColor Red
}

# 4. SOA 序列号检查(判断 DNS 是否同步)
Write-Host "`n[4] SOA 序列号一致性" -ForegroundColor Yellow
try {
$soa = Resolve-DnsName -Name $DomainName -Type SOA -ErrorAction Stop | Select-Object -First 1
Write-Host " 主服务器: $($soa.PrimaryServer)" -ForegroundColor Green
Write-Host " 序列号: $($soa.Serial)" -ForegroundColor Green
Write-Host " 刷新间隔: $($soa.Refresh)s" -ForegroundColor Green
Write-Host " 重试间隔: $($soa.Retry)s" -ForegroundColor Green
Write-Host " 过期时间: $($soa.Expire)s" -ForegroundColor Green
} catch {
Write-Host " 无法获取 SOA 记录" -ForegroundColor Red
}

Write-Host "`n========== 诊断完成 ==========`n"
}

# 执行示例:诊断 vichamp.com 的 DNS 配置
Test-DnsResolution -DomainName "vichamp.com"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
========== DNS 诊断报告 ==========
目标域名: vichamp.com
诊断时间: 2026-02-10 08:20:15
===================================

[1] DNS 记录查询
A: 185.199.108.153 (TTL: 300s, Section: Answer)
A: 185.199.109.153 (TTL: 300s, Section: Answer)
A: 185.199.110.153 (TTL: 300s, Section: Answer)
A: 185.199.111.153 (TTL: 300s, Section: Answer)
AAAA: 未找到记录
MX: 10 mail.vichamp.com (TTL: 3600s, Section: Answer)
NS: dns1.p01.nsone.net (TTL: 86400s, Section: Additional)
NS: dns2.p01.nsone.net (TTL: 86400s, Section: Additional)
TXT: v=spf1 include:_spf.google.com ~all (TTL: 3600s, Section: Answer)
SOA: dns1.p01.nsone.net Serial=2026021001 (TTL: 86400s, Section: Authority)

[2] 解析性能测试
第 1 次: 18ms
第 2 次: 12ms
第 3 次: 11ms
第 4 次: 13ms
第 5 次: 10ms
平均: 12.8ms, 最大: 18ms

[3] NS 服务器可达性
dns1.p01.nsone.net: 在线 (85.23ms)
dns2.p01.nsone.net: 在线 (92.17ms)

[4] SOA 序列号一致性
主服务器: dns1.p01.nsone.net
序列号: 2026021001
刷新间隔: 43200s
重试间隔: 7200s
过期时间: 1209600s

========== 诊断完成 ==========

DNS 诊断脚本输出了完整的解析链路信息:A 记录指向 GitHub Pages 的 4 个 IP 地址,MX 记录配置了邮件服务器,TXT 记录包含了 SPF 策略。解析延迟平均 12.8ms,属于正常范围。SOA 序列号可以用来判断主从 DNS 是否同步——如果多个 NS 服务器的序列号不一致,说明区域传输可能存在问题。

综合诊断报告:一键检测与 HTML 输出

在故障排查实战中,我们通常需要一份涵盖所有网络层面的完整诊断报告。下面的脚本将连接性测试和 DNS 诊断整合为一个一键执行的入口,并生成带样式的 HTML 报告,方便分享给团队成员或归档留存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
function Invoke-NetworkHealthCheck {
<#
.SYNOPSIS
一键网络健康检查:综合连通性、DNS、路由追踪,生成 HTML 报告
.PARAMETER Target
目标主机名或 IP 地址
.PARAMETER OutputPath
HTML 报告输出路径
#>
param(
[Parameter(Mandatory)]
[string]$Target,
[string]$OutputPath = ".\NetworkReport_$(Get-Date -Format 'yyyyMMdd_HHmmss').html"
)

$reportTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$sections = @()

# ---- 第一部分:ICMP 连通性 ----
$icmpResults = @()
$pings = Test-Connection -ComputerName $Target -Count 10 -ErrorAction SilentlyContinue
if ($pings) {
$stats = $pings | Measure-Object -Property ResponseTime -Average -Minimum -Maximum -StandardDeviation
$icmpResults += "状态: 在线"
$icmpResults += "平均延迟: $([math]::Round($stats.Average, 2))ms"
$icmpResults += "最小延迟: $($stats.Minimum)ms"
$icmpResults += "最大延迟: $($stats.Maximum)ms"
$icmpResults += "抖动(Jitter): $([math]::Round($stats.StandardDeviation, 2))ms"
$icmpResults += "丢包率: $([math]::Round((1 - ($pings.Count / 10)) * 100, 1))%"
} else {
$icmpResults += "状态: 目标不可达"
}

# ---- 第二部分:关键端口检测 ----
$portResults = @()
$criticalPorts = @{
22 = "SSH"
53 = "DNS"
80 = "HTTP"
443 = "HTTPS"
3389 = "RDP"
}
foreach ($port in $criticalPorts.Keys) {
$tcpClient = New-Object System.Net.Sockets.TcpClient
try {
$asyncResult = $tcpClient.BeginConnect($Target, $port, $null, $null)
$waited = $asyncResult.AsyncWaitHandle.WaitOne(3000, $false)
if ($waited) {
$tcpClient.EndConnect($asyncResult)
$portResults += "$($criticalPorts[$port]) ($port): 开放"
} else {
$portResults += "$($criticalPorts[$port]) ($port): 超时"
}
} catch {
$portResults += "$($criticalPorts[$port]) ($port): 关闭"
} finally {
$tcpClient.Close()
}
}

# ---- 第三部分:DNS 解析 ----
$dnsResults = @()
try {
$dnsRecs = Resolve-DnsName -Name $Target -Type A -ErrorAction Stop
foreach ($rec in $dnsRecs) {
if ($rec.IPAddress) {
$dnsResults += "$($rec.Name) -> $($rec.IPAddress) (TTL: $($rec.TTL)s)"
}
}
} catch {
$dnsResults += "解析失败: $($_.Exception.Message)"
}

# ---- 第四部分:路由追踪(简化版 MTR) ----
$hopResults = @()
for ($ttl = 1; $ttl -le 15; $ttl++) {
$ping = Test-Connection -ComputerName $Target -Count 1 -TTL $ttl -ErrorAction SilentlyContinue
if ($ping) {
$hopIP = $ping.Address
$hopLatency = $ping.ResponseTime
$hopResults += "跳 ${ttl}: ${hopIP} (${hopLatency}ms)"
if ($hopIP -eq $Target -or $hopIP -match $Target) { break }
} else {
$hopResults += "跳 ${ttl}: * * *"
}
}

# ---- 生成 HTML 报告 ----
$html = @"
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>网络诊断报告 - $Target</title>
<style>
body { font-family: 'Segoe UI', Arial, sans-serif; margin: 40px; background: #f5f5f5; }
h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; }
h2 { color: #2980b9; margin-top: 30px; }
.container { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.section { margin: 20px 0; padding: 15px; background: #fafafa; border-left: 4px solid #3498db; }
.ok { color: #27ae60; }
.warn { color: #f39c12; }
.fail { color: #e74c3c; }
.timestamp { color: #7f8c8d; font-size: 0.9em; }
</style>
</head>
<body>
<div class="container">
<h1>网络诊断报告</h1>
<p class="timestamp">目标: $Target | 生成时间: $reportTime</p>

<h2>ICMP 连通性</h2>
<div class="section">
$($icmpResults | ForEach-Object { "<p>$_</p>" })
</div>

<h2>关键端口状态</h2>
<div class="section">
$($portResults | ForEach-Object {
if ($_ -match "开放") { "<p class=`"ok`">$_</p>" }
elseif ($_ -match "超时") { "<p class=`"warn`">$_</p>" }
else { "<p class=`"fail`">$_</p>" }
})
</div>

<h2>DNS 解析</h2>
<div class="section">
$($dnsResults | ForEach-Object { "<p>$_</p>" })
</div>

<h2>路由追踪</h2>
<div class="section">
$($hopResults | ForEach-Object { "<p>$_</p>" })
</div>

</div>
</body>
</html>
"@

$html | Out-File -FilePath $OutputPath -Encoding UTF8
Write-Host "诊断报告已生成: $OutputPath" -ForegroundColor Green

# 同时输出控制台摘要
Write-Host "`n===== 网络健康检查摘要 =====" -ForegroundColor Cyan
Write-Host "目标: $Target"
$icmpResults | ForEach-Object { Write-Host " $_" }
Write-Host "`n端口状态:" -ForegroundColor Yellow
$portResults | ForEach-Object { Write-Host " $_" }
Write-Host "`n路由路径:" -ForegroundColor Yellow
$hopResults | ForEach-Object { Write-Host " $_" }
Write-Host "============================`n"
}

# 执行示例:一键检查并生成 HTML 报告
Invoke-NetworkHealthCheck -Target "blog.vichamp.com"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
===== 网络健康检查摘要 =====
目标: blog.vichamp.com
状态: 在线
平均延迟: 38.5ms
最小延迟: 32ms
最大延迟: 51ms
抖动(Jitter): 5.23ms
平均延迟: 38.5ms
丢包率: 0%

端口状态:
SSH (22): 关闭
DNS (53): 关闭
HTTP (80): 开放
HTTPS (443): 开放
RDP (3389): 关闭

路由路径:
跳 1: 192.168.1.1 (1ms)
跳 2: 10.0.0.1 (5ms)
跳 3: 61.149.212.1 (8ms)
跳 4: * * *
跳 5: * * *
跳 6: 185.199.108.153 (38ms)

诊断报告已生成: .\NetworkReport_20260210_082500.html
============================

综合诊断脚本整合了 ICMP 连通性分析、关键端口检测、DNS 解析验证和简化的路由追踪,最终将所有结果汇总为一份带颜色标注的 HTML 报告。在端口状态部分,开放的端口用绿色标注、超时的用橙色、关闭的用红色,一目了然。路由追踪可以快速定位网络延迟发生的位置——如果某一跳延迟突然增大,说明瓶颈就在该节点。

注意事项

  1. 跨平台差异Test-Connection 在 PowerShell 7 (Core) 中行为与 Windows PowerShell 5.1 不同。5.1 默认使用 WMI 的 Win32_PingStatus,而 PowerShell 7 使用 .NET 的 System.Net.NetworkInformation.Ping,返回对象属性名称可能不同。生产环境中建议统一使用 PowerShell 7 以保证一致性。

  2. 防火墙对 ICMP 的影响:很多云服务器和安全设备会禁用 ICMP 回显请求(ping 不通但服务正常)。因此判断服务可用性时应以 TCP 端口检测结果为准,ICMP 仅作为参考指标,不要因为 ping 不通就判定服务宕机。

  3. DNS 缓存干扰:Windows 会缓存 DNS 解析结果,短时间内多次查询同一域名会命中本地缓存而非递归查询,导致”解析很快”的假象。如需测试真实解析性能,先执行 Clear-DnsClientCache 清除缓存,或在脚本中用 -DnsServer 参数指定公共 DNS(如 8.8.8.8)绕过本地缓存。

  4. 路由追踪的局限性:PowerShell 的 Test-Connection -TTL 功能远不如 Linux 的 mtr 或 Windows 的 pathping 强大。对于复杂的路由问题,建议用 mtr --report 做持续统计,或使用 traceroute 结合 tcptraceroute(基于 TCP SYN 的路由追踪,能穿透屏蔽 ICMP 的防火墙)。

  5. HTML 报告安全性:脚本生成的 HTML 报告中包含目标主机的 IP 地址、开放端口等敏感信息。不要将报告直接上传到公开位置,传输时建议使用加密通道或先压缩加密。如果目标地址是内网 IP,报告还可能泄露内网拓扑结构。

  6. 超时与并发控制:对多个目标进行批量检测时,如果采用串行方式逐个测试,总耗时会随目标数量线性增长。可以考虑使用 ForEach-Object -Parallel(PowerShell 7)或 .NET 的 System.Threading.Tasks.Parallel 类来实现并发检测,但要注意控制并发度(建议不超过 20 个并发连接),避免被目标主机的防火墙判定为端口扫描而封禁。

PowerShell 技能连载 - TCP/UDP 网络客户端

适用于 PowerShell 5.1 及以上版本

虽然 Invoke-WebRequestInvoke-RestMethod 可以处理 HTTP 请求,但很多网络协议不走 HTTP——Redis、MySQL、SMTP、自定义 TCP 协议等。通过 .NET 的 System.Net.Sockets 命名空间,PowerShell 可以直接操作 TCP/UDP Socket,实现底层网络通信。这对于端口探测、协议测试、自定义服务监控等场景非常有用。

本文将讲解 PowerShell 中的 TCP/UDP 客户端编程和实用的网络工具。

TCP 客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# 基本 TCP 连接
function Send-TcpMessage {
param(
[Parameter(Mandatory)]
[string]$Server,

[Parameter(Mandatory)]
[int]$Port,

[string]$Message,

[int]$TimeoutMs = 5000
)

$client = [System.Net.Sockets.TcpClient]::new()
try {
$connectTask = $client.ConnectAsync($Server, $Port)
$connectTask.Wait($TimeoutMs) | Out-Null

if (-not $client.Connected) {
throw "连接超时"
}

$stream = $client.GetStream()
$writer = [System.IO.StreamWriter]::new($stream)
$reader = [System.IO.StreamReader]::new($stream)
$writer.AutoFlush = $true

if ($Message) {
$writer.WriteLine($Message)
}

Start-Sleep -Milliseconds 500

if ($stream.DataAvailable) {
$response = $reader.ReadToEnd()
return $response
}
} finally {
$client.Close()
}
}

# 测试 TCP 端口连通性
function Test-TcpPort {
param(
[Parameter(Mandatory)]
[string]$ComputerName,

[Parameter(Mandatory)]
[int]$Port,

[int]$TimeoutMs = 3000
)

$client = [System.Net.Sockets.TcpClient]::new()
$sw = [System.Diagnostics.Stopwatch]::StartNew()

try {
$connectTask = $client.ConnectAsync($ComputerName, $Port)
$connectTask.Wait($TimeoutMs) | Out-Null

$sw.Stop()
$success = $client.Connected

return [PSCustomObject]@{
Computer = $ComputerName
Port = $Port
IsOpen = $success
LatencyMs = $sw.ElapsedMilliseconds
}
} catch {
$sw.Stop()
return [PSCustomObject]@{
Computer = $ComputerName
Port = $Port
IsOpen = $false
LatencyMs = $sw.ElapsedMilliseconds
}
} finally {
$client.Close()
}
}

# 批量端口扫描
$targets = @(
@{ Host = "google.com"; Ports = @(80, 443, 8080) },
@{ Host = "github.com"; Ports = @(22, 80, 443) }
)

foreach ($target in $targets) {
Write-Host "$($target.Host):" -ForegroundColor Cyan
foreach ($port in $target.Ports) {
$result = Test-TcpPort -ComputerName $target.Host -Port $port
$status = if ($result.IsOpen) { "OPEN" } else { "CLOSED" }
$color = if ($result.IsOpen) { "Green" } else { "Red" }
Write-Host " Port $($result.Port) : $status ($($result.LatencyMs)ms)" -ForegroundColor $color
}
}

执行结果示例:

1
2
3
4
5
6
7
8
google.com:
Port 80 : OPEN (25ms)
Port 443 : OPEN (22ms)
Port 8080 : CLOSED (3003ms)
github.com:
Port 22 : OPEN (45ms)
Port 80 : OPEN (30ms)
Port 443 : OPEN (28ms)

UDP 客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# UDP 客户端
function Send-UdpMessage {
param(
[Parameter(Mandatory)]
[string]$Server,

[Parameter(Mandatory)]
[int]$Port,

[Parameter(Mandatory)]
[string]$Message,

[int]$TimeoutMs = 3000
)

$client = [System.Net.Sockets.UdpClient]::new()
try {
$client.Connect($Server, $Port)
$bytes = [System.Text.Encoding]::UTF8.GetBytes($Message)
$client.Send($bytes, $bytes.Length) | Out-Null

# 等待响应(UDP 不保证有响应)
$client.Client.ReceiveTimeout = $TimeoutMs
$remoteEP = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Any, 0)
$response = $client.Receive([ref]$remoteEP)

return [System.Text.Encoding]::UTF8.GetString($response)
} catch {
return $null
} finally {
$client.Close()
}
}

# DNS 查询示例(使用 System.Net)
function Resolve-DnsFast {
param([Parameter(Mandatory)][string]$Hostname)

try {
$addresses = [System.Net.Dns]::GetHostAddresses($Hostname)
foreach ($addr in $addresses) {
[PSCustomObject]@{
Hostname = $Hostname
Address = $addr.ToString()
Family = $addr.AddressFamily
}
}
} catch {
Write-Host "解析失败:$Hostname" -ForegroundColor Red
}
}

Resolve-DnsFast -Hostname "www.microsoft.com" | Format-Table -AutoSize

执行结果示例:

1
2
3
4
Hostname         Address           Family
-------- ------- ------
www.microsoft.com 20.190.159.2 InterNetwork
www.microsoft.com 2603:1030:20... InterNetworkV6

端口监控工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 服务端口监控
function Test-ServicePorts {
param(
[PSCustomObject[]]$Services = @(
@{ Name = "Web"; Host = "localhost"; Port = 80 },
@{ Name = "HTTPS"; Host = "localhost"; Port = 443 },
@{ Name = "SSH"; Host = "localhost"; Port = 22 },
@{ Name = "MySQL"; Host = "localhost"; Port = 3306 },
@{ Name = "Redis"; Host = "localhost"; Port = 6379 }
)
)

$results = foreach ($svc in $Services) {
$test = Test-TcpPort -ComputerName $svc.Host -Port $svc.Port -TimeoutMs 2000
[PSCustomObject]@{
Service = $svc.Name
Host = $svc.Host
Port = $svc.Port
Status = if ($test.IsOpen) { "Listening" } else { "Closed" }
Latency = "$($test.LatencyMs)ms"
}
}

$results | Format-Table -AutoSize

$closed = $results | Where-Object { $_.Status -eq "Closed" }
if ($closed) {
Write-Host "告警:$($closed.Count) 个端口未监听" -ForegroundColor Red
$closed | ForEach-Object {
Write-Host " $($_.Service) ($($_.Host):$($_.Port))" -ForegroundColor Red
}
}
}

Test-ServicePorts

执行结果示例:

1
2
3
4
5
6
7
8
9
10
Service Host     Port Status    Latency
------- ---- ---- ------ -------
Web localhost 80 Listening 1ms
HTTPS localhost 443 Listening 1ms
SSH localhost 22 Closed 2002ms
MySQL localhost 3306 Listening 2ms
Redis localhost 6379 Listening 1ms

告警:1 个端口未监听
SSH (localhost:22)

SMTP 邮件探测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# SMTP 协议探测
function Test-SmtpServer {
param(
[Parameter(Mandatory)]
[string]$Server,

[int]$Port = 25,

[int]$TimeoutMs = 10000
)

$client = [System.Net.Sockets.TcpClient]::new()
try {
$client.Connect($Server, $Port)
$stream = $client.GetStream()
$reader = [System.IO.StreamReader]::new($stream)
$writer = [System.IO.StreamWriter]::new($stream)
$writer.AutoFlush = $true

$client.Client.ReceiveTimeout = $TimeoutMs

# 读取欢迎信息
$banner = $reader.ReadLine()
Write-Host "SMTP Banner:$banner" -ForegroundColor Cyan

# 发送 EHLO
$writer.WriteLine("EHLO test.local")
Start-Sleep -Milliseconds 500
while ($stream.DataAvailable) {
$line = $reader.ReadLine()
Write-Host " $line" -ForegroundColor DarkGray
}

# 发送 QUIT
$writer.WriteLine("QUIT")
$bye = $reader.ReadLine()
Write-Host " $bye" -ForegroundColor DarkGray

return $true
} catch {
Write-Host "SMTP 连接失败:$($_.Exception.Message)" -ForegroundColor Red
return $false
} finally {
$client.Close()
}
}

Test-SmtpServer -Server "mail.contoso.com" -Port 25

执行结果示例:

1
2
3
4
5
6
7
8
SMTP Banner:220 mail.contoso.com ESMTP Postfix
250-mail.contoso.com
250-PIPELINING
250-SIZE 10240000
250-STARTTLS
250-AUTH PLAIN LOGIN
250 ENHANCEDSTATUSCODES
221 2.0.0 Bye

注意事项

  1. 防火墙:TCP/UDP 连接可能被防火墙阻止,超时不代表端口真正关闭
  2. 资源释放:TcpClient 和 UdpClient 必须调用 Close() 或使用 try/finally 确保释放
  3. 超时设置:默认超时可能很长,显式设置 ReceiveTimeoutConnectTimeout
  4. UDP 不可靠:UDP 不保证消息送达和顺序,适合 DNS、SNMP 等简单查询
  5. IPv6:现代系统可能返回 IPv6 地址,使用 [System.Net.IPAddress]::Any 监听所有地址
  6. 端口扫描:批量端口扫描可能触发安全告警,仅在授权环境下使用

PowerShell 技能连载 - 网络诊断与排查

适用于 PowerShell 5.1 及以上版本

网络故障是运维工作中最常见的排障场景——“连不上数据库”、”网站打不开”、”文件共享超时”。PowerShell 内置了丰富的网络诊断命令,从基本的 ping、端口检测到 DNS 解析、路由追踪和 TCP 连接测试,可以快速定位网络问题的层级(物理层、链路层、网络层、传输层、应用层)。

本文将讲解 PowerShell 网络诊断的系统化方法,以及如何构建一键式排障脚本。

基础连通性测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Test-Connection(PowerShell 的 ping)
Test-Connection -ComputerName google.com -Count 4 |
Select-Object Address, @{N='延迟ms'; E={$_.ResponseTime}}, Status |
Format-Table -AutoSize

# Test-NetConnection(更强大的网络测试)
Test-NetConnection -ComputerName blog.vichamp.com -Port 443 |
Select-Object ComputerName, RemoteAddress, RemotePort, TcpTestSucceeded, PingSucceeded |
Format-List

# 批量测试多台服务器的连通性
$servers = @('web-01.internal', 'db-01.internal', 'api-01.internal', 'redis-01.internal')

$results = foreach ($server in $servers) {
$test = Test-NetConnection -ComputerName $server -WarningAction SilentlyContinue
[PSCustomObject]@{
Server = $server
IP = $test.RemoteAddress
Ping = $test.PingSucceeded
Port = if ($test.TcpTestSucceeded) { 'Open' } else { 'Closed/Filtered' }
}
}

$results | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Address   延迟ms Status
------- ------ ------
google.com 12 True
google.com 15 True
google.com 11 True
google.com 14 True

ComputerName : blog.vichamp.com
RemoteAddress : 185.199.108.153
RemotePort : 443
TcpTestSucceeded : True
PingSucceeded : True

Server IP Ping Port
------ -- ---- ----
web-01.internal 192.168.1.101 True Open
db-01.internal 192.168.1.201 True Open
api-01.internal 192.168.1.102 True Open
redis-01.internal 192.168.1.202 False Closed/Filtered

DNS 诊断

DNS 解析问题是网络故障的高发区:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# DNS 解析
Resolve-DnsName -Name blog.vichamp.com | Format-Table -AutoSize

# 查看所有 DNS 记录类型
Resolve-DnsName -Name contoso.com -Type ANY |
Select-Object Name, Type, Section, Data |
Format-Table -AutoSize

# 查看当前 DNS 服务器配置
Get-DnsClientServerAddress -AddressFamily IPv4 |
Where-Object { $_.InterfaceAlias -notmatch 'Loopback' } |
Select-Object InterfaceAlias, ServerAddresses |
Format-Table -AutoSize

# 刷新 DNS 缓存
Clear-DnsClientCache
Write-Host "DNS 缓存已清除" -ForegroundColor Green

# DNS 解析对比(用不同 DNS 服务器)
function Compare-DnsResolution {
param([string]$Domain)

$servers = @{
'默认' = $null
'Google' = '8.8.8.8'
'Cloudflare' = '1.1.1.1'
'AliDNS' = '223.5.5.5'
}

foreach ($name in $servers.Keys) {
try {
$params = @{ Name = $Domain }
if ($servers[$name]) { $params.Server = $servers[$name] }

$result = Resolve-DnsName @params -ErrorAction Stop |
Select-Object -First 1

[PSCustomObject]@{
DNS服务器 = $name
IP地址 = $result.IPAddress
类型 = $result.Type
TTL = $result.TTL
}
} catch {
[PSCustomObject]@{
DNS服务器 = $name
IP地址 = "解析失败: $($_.Exception.Message)"
}
}
}
}

Compare-DnsResolution -Domain "blog.vichamp.com" | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Name             Type  Section Data
---- ---- ------- ----
blog.vichamp.com CNAME Name victorwoo.github.io

InterfaceAlias ServerAddresses
-------------- ---------------
以太网 {192.168.1.1}

DNS服务器 IP地址 类型 TTL
--------- ------ ---- ---
默认 185.199.108.153 CNAME 3600
Google 185.199.108.153 CNAME 3600
Cloudflare 185.199.108.153 CNAME 3600
AliDNS 185.199.108.153 CNAME 3600

TCP 端口扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function Test-TcpPort {
<#
.SYNOPSIS
测试目标主机的 TCP 端口连通性
#>
param(
[Parameter(Mandatory)]
[string]$ComputerName,

[int[]]$Ports = @(22, 80, 443, 3389, 5985, 5986, 8080, 1433, 3306, 6379),

[int]$TimeoutMs = 3000
)

$results = foreach ($port in $Ports) {
$tcpClient = New-Object System.Net.Sockets.TcpClient
$asyncResult = $tcpClient.BeginConnect($ComputerName, $port, $null, $null)
$waited = $asyncResult.AsyncWaitHandle.WaitOne($TimeoutMs, $false)

$isOpen = $false
if ($waited) {
try {
$tcpClient.EndConnect($asyncResult)
$isOpen = $true
} catch {
$isOpen = $false
}
}

$tcpClient.Close()

$serviceName = switch ($port) {
22 { 'SSH' }
80 { 'HTTP' }
443 { 'HTTPS' }
3389 { 'RDP' }
5985 { 'WinRM-HTTP' }
5986 { 'WinRM-HTTPS' }
8080 { 'HTTP-Alt' }
1433 { 'MSSQL' }
3306 { 'MySQL' }
6379 { 'Redis' }
default { 'Unknown' }
}

[PSCustomObject]@{
Port = $port
Service = $serviceName
Status = if ($isOpen) { 'Open' } else { 'Closed' }
}
}

$results | Format-Table -AutoSize
}

# 扫描目标服务器的常用端口
Test-TcpPort -ComputerName "192.168.1.100" -Ports @(22, 80, 443, 3389, 5985, 1433, 6379)

执行结果示例:

1
2
3
4
5
6
7
8
9
Port Service     Status
---- ------- ------
22 SSH Closed
80 HTTP Open
443 HTTPS Open
3389 RDP Open
5985 WinRM-HTTP Open
1433 MSSQL Open
6379 Redis Closed

构建一键式排障脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
function Invoke-NetworkTroubleshooter {
<#
.SYNOPSIS
一键式网络排障脚本
#>
param(
[Parameter(Mandatory)]
[string]$Target,

[int]$Port = 443
)

Write-Host "========== 网络排障:$Target ==========" -ForegroundColor Cyan

# Step 1: DNS 解析
Write-Host "`n[1/5] DNS 解析" -ForegroundColor Yellow
try {
$dns = Resolve-DnsName -Name $Target -ErrorAction Stop
$ip = ($dns | Where-Object { $_.Type -eq 'A' } | Select-Object -First 1).IPAddress
if (-not $ip) {
$ip = ($dns | Select-Object -First 1).IPAddress
}
Write-Host " 解析结果:$Target => $ip" -ForegroundColor Green
} catch {
Write-Host " DNS 解析失败:$($_.Exception.Message)" -ForegroundColor Red
Write-Host " 建议:检查 DNS 配置或尝试使用其他 DNS 服务器" -ForegroundColor Yellow
return
}

# Step 2: ICMP Ping
Write-Host "`n[2/5] Ping 测试" -ForegroundColor Yellow
$ping = Test-Connection -ComputerName $ip -Count 3 -Quiet
if ($ping) {
$latency = (Test-Connection -ComputerName $ip -Count 1).ResponseTime
Write-Host " Ping 成功,延迟:${latency}ms" -ForegroundColor Green
} else {
Write-Host " Ping 失败(可能被防火墙阻止)" -ForegroundColor Yellow
}

# Step 3: TCP 端口测试
Write-Host "`n[3/5] TCP 端口测试 ($Port)" -ForegroundColor Yellow
$tcp = Test-NetConnection -ComputerName $ip -Port $Port -WarningAction SilentlyContinue
if ($tcp.TcpTestSucceeded) {
Write-Host " 端口 $Port 连接成功" -ForegroundColor Green
} else {
Write-Host " 端口 $Port 连接失败" -ForegroundColor Red
Write-Host " 建议:检查防火墙规则或目标服务是否运行" -ForegroundColor Yellow
}

# Step 4: 路由追踪
Write-Host "`n[4/5] 路由追踪 (tracert)" -ForegroundColor Yellow
$trace = tracert -d -h 15 -w 1000 $ip 2>$null
$trace | Select-Object -First 15 | ForEach-Object { Write-Host " $_" }

# Step 5: HTTP 测试(如果是 Web 服务)
if ($Port -in @(80, 443, 8080, 8443)) {
Write-Host "`n[5/5] HTTP 测试" -ForegroundColor Yellow
$protocol = if ($Port -in @(443, 8443)) { 'https' } else { 'http' }
try {
$response = Invoke-WebRequest -Uri "${protocol}://${Target}" `
-TimeoutSec 10 -UseBasicParsing -MaximumRedirection 0 -ErrorAction Stop
Write-Host " HTTP $($response.StatusCode) - $($response.StatusDescription)" -ForegroundColor Green
} catch {
$statusCode = $_.Exception.Response.StatusCode
if ($statusCode) {
Write-Host " HTTP $([int]$statusCode) - $($statusCode)" -ForegroundColor Yellow
} else {
Write-Host " HTTP 请求失败:$($_.Exception.Message)" -ForegroundColor Red
}
}
}

Write-Host "`n========== 排障完成 ==========" -ForegroundColor Cyan
}

# 一键排查网络问题
Invoke-NetworkTroubleshooter -Target "blog.vichamp.com" -Port 443

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
========== 网络排障:blog.vichamp.com ==========

[1/5] DNS 解析
解析结果:blog.vichamp.com => 185.199.108.153

[2/5] Ping 测试
Ping 成功,延迟:45ms

[3/5] TCP 端口测试 (443)
端口 443 连接成功

[4/5] 路由追踪 (tracert)
1 <1 ms <1 ms <1 ms 192.168.1.1
2 5 ms 3 ms 3 ms 10.0.0.1
...

[5/5] HTTP 测试
HTTP 200 - OK

========== 排障完成 ==========

注意事项

  1. 防火墙影响:ICMP ping 被阻止不代表服务不可用,始终结合 TCP 端口测试判断
  2. DNS 缓存:排查 DNS 问题时先执行 Clear-DnsClientCache 清除缓存
  3. 超时设置:给网络测试设置合理的超时,避免脚本无限等待
  4. IPv4/IPv6:某些环境可能有 IPv6 相关问题,使用 -AddressFamily IPv4 强制使用 IPv4
  5. 权限要求:部分网络诊断命令(如路由追踪)需要管理员权限
  6. 安全合规:端口扫描功能仅用于授权的网络诊断和安全审计,不得用于未授权的扫描