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 技能连载 - 网络诊断工具

适用于 PowerShell 5.1 及以上版本

在日常运维中,网络问题往往是最难定位的一类故障。接口掉线、带宽异常、HTTP 响应变慢——这些问题如果不能快速发现并定位,就会影响业务连续性。传统的图形化网络工具虽然直观,但难以批量执行和自动化巡检。

PowerShell 提供了全面的网络诊断能力:从网卡状态检测、HTTP 请求质量测量,到延迟丢包统计和实时流量监控,都可以通过脚本完成。更重要的是,这些脚本可以集成到定时任务或 CI/CD 流水线中,实现网络健康状态的持续观测。

本文将围绕四个实用场景,介绍如何用 PowerShell 构建一套轻量但实用的网络诊断工具集。

网络接口健康检查

首先,了解本机网卡的基本状态是排查网络问题的第一步。下面的脚本汇总所有活跃网卡的 IP 地址、链路状态、速率和已发送/接收的字节数,帮助你快速判断网络接口层是否正常。

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
function Get-NetworkInterfaceHealth {
<#
.SYNOPSIS
获取本机所有活跃网络接口的健康状态
#>

$adapters = Get-NetAdapter |
Where-Object { $_.Status -eq 'Up' -and $_.InterfaceType -notmatch 'Loopback' }

$results = foreach ($adapter in $adapters) {
$ipConfig = Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv2 -ErrorAction SilentlyContinue |
Select-Object -First 1

$stats = Get-NetAdapterStatistics -Name $adapter.Name -ErrorAction SilentlyContinue

[PSCustomObject]@{
名称 = $adapter.Name
接口描述 = $adapter.InterfaceDescription
链路速率 = "$($adapter.LinkSpeed)"
IPv4地址 = if ($ipConfig) { $ipConfig.IPAddress } else { '未分配' }
已接收字节 = if ($stats) { '{0:N2} GB' -f ($stats.ReceivedBytes / 1GB) } else { 'N/A' }
已发送字节 = if ($stats) { '{0:N2} GB' -f ($stats.SentBytes / 1GB) } else { 'N/A' }
MAC地址 = $adapter.MacAddress
}
}

$results | Format-Table -AutoSize
$results
}

Get-NetworkInterfaceHealth

执行结果示例:

1
2
3
4
名称     接口描述                            链路速率   IPv4地址       已接收字节  已发送字节  MAC地址
---- ---------- -------- -------- ---------- ---------- --------
以太网 Intel(R) Ethernet Connection I219-V 1 Gbps 192.168.1.100 12.35 GB 3.28 GB AA:BB:CC:DD:EE:FF
Wi-Fi Intel(R) Wi-Fi 6 AX200 866.7 Mbps 192.168.1.101 5.71 GB 1.04 GB 11:22:33:44:55:66

HTTP 响应质量检测

仅仅知道端口是否可达是不够的,很多时候我们需要深入了解 HTTP 服务的响应质量,包括首字节时间(TTFB)、总耗时、状态码以及重定向链。这些指标对 Web 服务运维至关重要。

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
function Test-HttpQuality {
<#
.SYNOPSIS
测试 HTTP/HTTPS 服务的响应质量
#>
param(
[Parameter(Mandatory)]
[string[]]$Urls,

[int]$TimeoutSec = 10
)

$results = foreach ($url in $Urls) {
$timer = [System.Diagnostics.Stopwatch]::StartNew()

try {
$response = Invoke-WebRequest -Uri $url -TimeoutSec $TimeoutSec `
-UseBasicParsing -MaximumRedirection 0 -ErrorAction Stop

$timer.Stop()

[PSCustomObject]@{
URL = $url
状态码 = [int]$response.StatusCode
状态描述 = $response.StatusDescription
总耗时ms = $timer.ElapsedMilliseconds
内容长度 = if ($response.Headers['Content-Length']) {
'{0:N0} bytes' -f [int]$response.Headers['Content-Length']
} else {
'N/A'
}
服务器 = if ($response.Headers['Server']) {
$response.Headers['Server']
} else {
'-'
}
结果 = '成功'
}
} catch {
$timer.Stop()
$statusCode = $null

if ($_.Exception.Response) {
$statusCode = [int]$_.Exception.Response.StatusCode
}

[PSCustomObject]@{
URL = $url
状态码 = $statusCode
状态描述 = if ($statusCode) { $_.Exception.Message.Substring(0, 60) } else { '-' }
总耗时ms = $timer.ElapsedMilliseconds
内容长度 = 'N/A'
服务器 = '-'
结果 = '失败'
}
}
}

$results | Format-Table -AutoSize
}

$targets = @(
'https://blog.vichamp.com',
'https://github.com',
'https://nonexistent-test-12345.example.com'
)

Test-HttpQuality -Urls $targets

执行结果示例:

1
2
3
4
5
URL                                               状态码 状态描述                   总耗时ms 内容长度       服务器        结果
--- ------ -------- -------- ---------- ------ ----
https://blog.vichamp.com 200 OK 312 15,280 bytes GitHub.com 成功
https://github.com 200 OK 287 82,451 bytes GitHub.com 成功
https://nonexistent-test-12345.example.com - - 5012 N/A - 失败

延迟与丢包统计分析

网络延迟和丢包率是衡量链路质量的核心指标。下面的脚本通过多次 ICMP 测试,统计平均延迟、最小/最大延迟、抖动(jitter)以及丢包率,并给出一个综合评级。

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
function Test-NetworkLatency {
<#
.SYNOPSIS
测试到目标主机的延迟和丢包率
#>
param(
[Parameter(Mandatory)]
[string[]]$Targets,

[int]$Count = 20,

[int]$DelayMs = 500
)

$results = foreach ($target in $Targets) {
Write-Host "正在测试 $target ..." -ForegroundColor Gray

$latencies = [System.Collections.Generic.List[double]]::new()
$lostCount = 0

for ($i = 0; $i -lt $Count; $i++) {
$reply = Test-Connection -ComputerName $target -Count 1 -Quiet -ErrorAction SilentlyContinue

if ($reply) {
$detail = Test-Connection -ComputerName $target -Count 1 -ErrorAction SilentlyContinue
if ($detail) {
$latencies.Add($detail.ResponseTime)
}
} else {
$lostCount++
}

if ($i -lt $Count - 1) {
Start-Sleep -Milliseconds $DelayMs
}
}

if ($latencies.Count -gt 0) {
$avg = [math]::Round(($latencies | Measure-Object -Average).Average, 1)
$min = [math]::Round(($latencies | Measure-Object -Minimum).Minimum, 1)
$max = [math]::Round(($latencies | Measure-Object -Maximum).Maximum, 1)

# 计算抖动(相邻延迟差的平均值)
$jitterValues = [System.Collections.Generic.List[double]]::new()
for ($j = 1; $j -lt $latencies.Count; $j++) {
$jitterValues.Add([math]::Abs($latencies[$j] - $latencies[$j - 1]))
}
$jitter = if ($jitterValues.Count -gt 0) {
[math]::Round(($jitterValues | Measure-Object -Average).Average, 1)
} else {
0
}

$lossRate = [math]::Round(($lostCount / $Count) * 100, 1)

# 综合评级
$grade = if ($avg -lt 10 -and $lossRate -eq 0 -and $jitter -lt 5) {
'优秀'
} elseif ($avg -lt 50 -and $lossRate -lt 1) {
'良好'
} elseif ($avg -lt 100 -and $lossRate -lt 5) {
'一般'
} else {
'较差'
}
} else {
$avg = $min = $max = $jitter = 0
$lossRate = 100
$grade = '不可达'
}

[PSCustomObject]@{
目标 = $target
平均ms = $avg
最小ms = $min
最大ms = $max
抖动ms = $jitter
丢包率 = "$lossRate%"
评级 = $grade
}
}

$results | Format-Table -AutoSize
}

# 批量测试多个目标的网络质量
Test-NetworkLatency -Targets @(
'baidu.com',
'google.com',
'github.com'
) -Count 15

执行结果示例:

1
2
3
4
5
目标        平均ms 最小ms 最大ms 抖动ms 丢包率  评级
---- ------ ------ ------ ------ ------ ----
baidu.com 8.3 5.2 15.1 1.8 0.0% 优秀
google.com 42.7 38.1 55.3 3.2 0.0% 良好
github.com 85.1 72.4 140.6 8.5 6.7% 一般

实时网络流量监控

最后一个工具用于监控指定时间窗口内的网络流量变化趋势。它每隔一定时间采样一次网卡统计数据,计算每秒的吞吐量,帮助你发现突发的流量异常(例如某个进程突然大量上传数据)。

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
function Watch-NetworkTraffic {
<#
.SYNOPSIS
实时监控网络接口流量
#>
param(
[string]$InterfaceName = (Get-NetAdapter |
Where-Object { $_.Status -eq 'Up' -and $_.InterfaceType -notmatch 'Loopback' } |
Select-Object -First 1).Name,

[int]$DurationSeconds = 30,

[int]$IntervalSeconds = 2
)

$adapter = Get-NetAdapter -Name $InterfaceName -ErrorAction SilentlyContinue

if (-not $adapter) {
Write-Host "未找到网络接口:$InterfaceName" -ForegroundColor Red
return
}

Write-Host "监控接口:$InterfaceName ($($adapter.InterfaceDescription))" -ForegroundColor Cyan
Write-Host "持续时间:${DurationSeconds}秒,采样间隔:${IntervalSeconds}秒" -ForegroundColor Cyan
Write-Host ('-' * 70)

$samples = [System.Collections.Generic.List[PSCustomObject]]::new()
$startTime = Get-Date

$prevStats = Get-NetAdapterStatistics -Name $InterfaceName
$prevTime = Get-Date

while (((Get-Date) - $startTime).TotalSeconds -lt $DurationSeconds) {
Start-Sleep -Seconds $IntervalSeconds

$currentStats = Get-NetAdapterStatistics -Name $InterfaceName
$currentTime = Get-Date

$elapsed = ($currentTime - $prevTime).TotalSeconds
if ($elapsed -le 0) { $elapsed = 1 }

$rxRate = ($currentStats.ReceivedBytes - $prevStats.ReceivedBytes) / $elapsed
$txRate = ($currentStats.SentBytes - $prevStats.SentBytes) / $elapsed

function Format-Bytes {
param([double]$Bytes)
if ($Bytes -ge 1MB) { '{0,8:N2} MB/s' -f ($Bytes / 1MB) }
elseif ($Bytes -ge 1KB) { '{0,8:N2} KB/s' -f ($Bytes / 1KB) }
else { '{0,8:N2} B/s' -f $Bytes }
}

$sample = [PSCustomObject]@{
时间 = $currentTime.ToString('HH:mm:ss')
接收速率 = Format-Bytes $rxRate
发送速率 = Format-Bytes $txRate
累计接收 = '{0:N2} MB' -f ($currentStats.ReceivedBytes / 1MB)
累计发送 = '{0:N2} MB' -f ($currentStats.SentBytes / 1MB)
}

$samples.Add($sample)
Write-Host (" {0} | 接收: {1} | 发送: {2}" -f `
$sample.时间, $sample.接收速率, $sample.发送速率)

$prevStats = $currentStats
$prevTime = $currentTime
}

Write-Host ('-' * 70)
Write-Host "采样完成,共 $($samples.Count) 个数据点" -ForegroundColor Green

$samples | Format-Table -AutoSize
}

# 监控 30 秒内的网络流量变化
Watch-NetworkTraffic -DurationSeconds 30 -IntervalSeconds 3

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
监控接口:以太网 (Intel(R) Ethernet Connection I219-V)
持续时间:30秒,采样间隔:3秒
----------------------------------------------------------------------
14:20:03 | 接收: 2.35 MB/s | 发送: 0.42 MB/s
14:20:06 | 接收: 5.18 MB/s | 发送: 0.38 MB/s
14:20:09 | 接收: 1.72 MB/s | 发送: 0.25 MB/s
14:20:12 | 接收: 8.91 MB/s | 发送: 1.23 MB/s
14:20:15 | 接收: 3.44 MB/s | 发送: 0.55 MB/s
14:20:18 | 接收: 0.85 MB/s | 发送: 0.12 MB/s
14:20:21 | 接收: 1.06 MB/s | 发送: 0.18 MB/s
14:20:24 | 接收: 4.67 MB/s | 发送: 0.89 MB/s
14:20:27 | 接收: 2.13 MB/s | 发送: 0.33 MB/s
14:20:30 | 接收: 1.88 MB/s | 发送: 0.29 MB/s
----------------------------------------------------------------------
采样完成,共 10 个数据点

时间 接收速率 发送速率 累计接收 累计发送
---- -------- -------- -------- --------
14:20:03 2.35 MB/s 0.42 MB/s 12,350.22 MB 3,281.45 MB
14:20:06 5.18 MB/s 0.38 MB/s 12,365.76 MB 3,282.59 MB
14:20:09 1.72 MB/s 0.25 MB/s 12,370.92 MB 3,283.34 MB

注意事项

  1. 管理员权限Get-NetAdapterStatistics 等部分 cmdlet 在某些 Windows 版本上需要以管理员身份运行 PowerShell,否则可能返回不完整的数据
  2. 采样精度:流量监控的精度取决于采样间隔,间隔越短结果越精确,但也会增加系统开销;建议生产环境使用 3-5 秒的间隔
  3. IPv4/IPv6 兼容:延迟测试中 Test-Connection 默认可能优先使用 IPv6,如果目标仅支持 IPv4,可通过 -TargetName 参数配合 [System.Net.Dns]::GetHostAddresses 显式指定地址族
  4. HTTPS 证书警告:HTTP 质量检测中遇到自签名证书或过期证书时,Invoke-WebRequest 会抛出异常,可根据实际需求决定是否跳过证书验证(添加 -SkipCertificateCheck 参数,仅限 PowerShell 7+)
  5. 并发性能:批量测试多个目标时,如果目标数量较多,可以考虑使用 Runspaces 或 PowerShell 7 的 ForEach-Object -Parallel 来并发执行,显著缩短总耗时
  6. 防火墙与 ICMP:部分云服务器和网络安全设备默认屏蔽 ICMP 协议,此时 ping 测试会失败但 HTTP 服务可能正常,诊断时应结合多种协议综合判断

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 技能连载 - SSH 隧道与端口转发

适用于 PowerShell 5.1 及以上版本

在混合环境的运维工作中,并非所有服务都暴露在公网上。数据库、内部 API、管理后台通常隐藏在跳板机或防火墙后面,直接访问需要 VPN 或复杂的网络配置。SSH 隧道(SSH Tunneling)提供了一种轻量、安全的方案,通过加密通道将远程服务映射到本地端口,无需额外的网络基础设施。

Windows 自 OpenSSH 客户端内置以来(Windows 10 1809+、Windows Server 2019+),PowerShell 脚本可以直接调用 ssh 命令建立隧道。结合 PowerShell 的进程管理能力,可以动态创建、监控和清理 SSH 隧道,实现自动化的端口转发管理。

本文将讲解 PowerShell 中 SSH 隧道的创建、管理和自动化实践。

基础本地端口转发

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
# 本地端口转发:将远程数据库映射到本地端口
# 场景:远程 MySQL 在 10.0.1.50:3306,通过跳板机 bastion.example.com 访问

function New-SSHTunnel {
param(
[Parameter(Mandatory)]
[string]$JumpHost,

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

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

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

[string]$SshUser = $env:USERNAME,

[string]$KeyPath
)

$sshArgs = @(
"-N" # 不执行远程命令
"-L" # 本地端口转发
"${LocalPort}:${RemoteHost}:${RemotePort}"
)

if ($KeyPath) {
$sshArgs += "-i", $KeyPath
}

$sshArgs += "${SshUser}@${JumpHost}"

# 启动 SSH 进程(后台运行)
$process = Start-Process -FilePath "ssh" `
-ArgumentList $sshArgs `
-WindowStyle Hidden `
-PassThru

# 等待端口就绪
$ready = $false
for ($i = 0; $i -lt 10; $i++) {
Start-Sleep -Milliseconds 500
try {
$tcp = New-Object System.Net.Sockets.TcpClient("127.0.0.1", $LocalPort)
$tcp.Close()
$ready = $true
break
} catch {
# 端口尚未就绪,继续等待
}
}

if ($ready) {
Write-Host "SSH 隧道已建立:localhost:${LocalPort} -> ${RemoteHost}:${RemotePort}" `
-ForegroundColor Green
Write-Host "进程 ID:$($process.Id)"
} else {
Write-Warning "隧道建立超时,请检查连接参数"
}

return $process
}

# 示例:通过跳板机连接远程 MySQL
$tunnel = New-SSHTunnel -JumpHost "bastion.example.com" `
-LocalPort 13306 `
-RemoteHost "10.0.1.50" `
-RemotePort 3306 `
-SshUser "admin" `
-KeyPath "$env:USERPROFILE\.ssh\id_rsa"

执行结果示例:

1
2
SSH 隧道已建立:localhost:13306 -> 10.0.1.50:3306
进程 ID:24516

远程端口转发与动态转发

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
# 远程端口转发:将本地服务暴露给远程网络
# 场景:让远程服务器访问本地开发中的 Web API

function New-SSHRemoteTunnel {
param(
[Parameter(Mandatory)]
[string]$RemoteServer,

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

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

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

[string]$SshUser = $env:USERNAME
)

$sshArgs = @(
"-N"
"-R"
"${RemotePort}:${LocalHost}:${LocalPort}"
"${SshUser}@${RemoteServer}"
)

$process = Start-Process -FilePath "ssh" `
-ArgumentList $sshArgs `
-WindowStyle Hidden `
-PassThru

Write-Host "远程隧道已建立:${RemoteServer}:${RemotePort} -> ${LocalHost}:${LocalPort}" `
-ForegroundColor Green
return $process
}

# 动态端口转发(SOCKS 代理)
function New-SSHSocksProxy {
param(
[Parameter(Mandatory)]
[string]$Server,

[int]$LocalPort = 1080,

[string]$SshUser = $env:USERNAME,

[string]$KeyPath
)

$sshArgs = @(
"-N"
"-D" # 动态转发,创建 SOCKS5 代理
"$LocalPort"
)

if ($KeyPath) {
$sshArgs += "-i", $KeyPath
}

$sshArgs += "${SshUser}@${Server}"

$process = Start-Process -FilePath "ssh" `
-ArgumentList $sshArgs `
-WindowStyle Hidden `
-PassThru

Write-Host "SOCKS5 代理已启动:127.0.0.1:${LocalPort}" -ForegroundColor Green
Write-Host "使用方式:配置浏览器或工具使用 SOCKS5 代理 127.0.0.1:${LocalPort}"
return $process
}

# 示例:创建 SOCKS5 代理
$socks = New-SSHSocksProxy -Server "bastion.example.com" `
-LocalPort 1080 `
-SshUser "admin"

执行结果示例:

1
2
SOCKS5 代理已启动:127.0.0.1:1080
使用方式:配置浏览器或工具使用 SOCKS5 代理 127.0.0.1:1080

隧道生命周期管理

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
# 隧道管理器:统一管理多个 SSH 隧道
$script:TunnelRegistry = @{}

function Register-Tunnel {
param(
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][System.Diagnostics.Process]$Process,
[int]$LocalPort,
[string]$Target
)

$script:TunnelRegistry[$Name] = @{
Process = $Process
LocalPort = $LocalPort
Target = $Target
CreatedAt = Get-Date
Status = "Running"
}
}

function Get-SSHTunnel {
param([string]$Name)

if ($Name) {
$entry = $script:TunnelRegistry[$Name]
if ($entry) {
# 检查进程是否仍在运行
$alive = Get-Process -Id $entry.Process.Id -ErrorAction SilentlyContinue
$status = if ($alive) { "Running" } else { "Stopped" }
$entry.Status = $status

[PSCustomObject]@{
Name = $Name
PID = $entry.Process.Id
LocalPort = $entry.LocalPort
Target = $entry.Target
Status = $status
Uptime = (Get-Date) - $entry.CreatedAt
}
}
} else {
foreach ($key in $script:TunnelRegistry.Keys) {
Get-SSHTunnel -Name $key
}
}
}

function Stop-SSHTunnel {
param([Parameter(Mandatory)][string]$Name)

$entry = $script:TunnelRegistry[$Name]
if (-not $entry) {
Write-Warning "未找到隧道:$Name"
return
}

$process = $entry.Process
if ($process -and -not $process.HasExited) {
Stop-Process -Id $process.Id -Force
Write-Host "隧道 '$Name' 已停止(PID: $($process.Id))" -ForegroundColor Yellow
}

$script:TunnelRegistry.Remove($Name)
}

# 使用示例
$tunnel1 = New-SSHTunnel -JumpHost "bastion.example.com" `
-LocalPort 13306 -RemoteHost "10.0.1.50" -RemotePort 3306 -SshUser "admin"
Register-Tunnel -Name "mysql-prod" -Process $tunnel1 -LocalPort 13306 -Target "10.0.1.50:3306"

$tunnel2 = New-SSHTunnel -JumpHost "bastion.example.com" `
-LocalPort 15432 -RemoteHost "10.0.1.60" -RemotePort 5432 -SshUser "admin"
Register-Tunnel -Name "pg-prod" -Process $tunnel2 -LocalPort 15432 -Target "10.0.1.60:5432"

# 查看所有隧道状态
Get-SSHTunnel | Format-Table -AutoSize

执行结果示例:

1
2
3
4
Name       PID   LocalPort Target         Status  Uptime
---- --- --------- ------ ------ ------
mysql-prod 24516 13306 10.0.1.50:3306 Running 00:02:15
pg-prod 24788 15432 10.0.1.60:5432 Running 00:01:03

隧道健康检查与自动重连

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
# 隧道守护进程:自动检测断线并重连
function Start-TunnelDaemon {
param(
[int]$CheckIntervalSeconds = 30
)

Write-Host "隧道守护进程已启动,检查间隔:${CheckIntervalSeconds}秒" -ForegroundColor Cyan

while ($true) {
$tunnels = Get-SSHTunnel
foreach ($t in $tunnels) {
if ($t.Status -ne "Running") {
Write-Warning "检测到隧道 '$($t.Name)' 已断开,尝试重连..."

# 尝试检测端口是否仍被占用(残留进程)
$portInUse = Get-NetTCPConnection -LocalPort $t.LocalPort `
-ErrorAction SilentlyContinue

if ($portInUse) {
Write-Host "端口 $($t.LocalPort) 仍被占用,等待释放..." -ForegroundColor Yellow
continue
}

# 解析目标地址
$parts = $t.Target -split ":"
$remoteHost = $parts[0]
$remotePort = [int]$parts[1]

# 重新建立隧道
$newTunnel = New-SSHTunnel -JumpHost "bastion.example.com" `
-LocalPort $t.LocalPort `
-RemoteHost $remoteHost `
-RemotePort $remotePort `
-SshUser "admin"

if ($newTunnel) {
Register-Tunnel -Name $t.Name -Process $newTunnel `
-LocalPort $t.LocalPort -Target $t.Target
Write-Host "隧道 '$($t.Name)' 重连成功" -ForegroundColor Green
}
}
}

Start-Sleep -Seconds $CheckIntervalSeconds
}
}

# 端口连通性测试工具
function Test-TunnelConnectivity {
param(
[Parameter(Mandatory)]
[string]$HostName,

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

[int]$TimeoutMs = 3000
)

$tcp = New-Object System.Net.Sockets.TcpClient
$asyncResult = $tcp.BeginConnect($HostName, $Port, $null, $null)
$waited = $asyncResult.AsyncWaitHandle.WaitOne($TimeoutMs, $false)

$result = if ($waited -and $tcp.Connected) {
$tcp.EndConnect($asyncResult)
[PSCustomObject]@{
Host = $HostName
Port = $Port
Status = "Open"
Latency = "N/A"
}
} else {
[PSCustomObject]@{
Host = $HostName
Port = $Port
Status = "Closed"
Latency = "N/A"
}
}

$tcp.Close()
return $result
}

# 批量检查所有隧道
Get-SSHTunnel | ForEach-Object {
Test-TunnelConnectivity -HostName "127.0.0.1" -Port $_.LocalPort
} | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
隧道守护进程已启动,检查间隔:30

Host Port Status
---- ---- ------
127.0.0.1 13336 Open
127.0.0.1 15432 Open

配置文件驱动的隧道管理

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
# 从配置文件批量创建隧道
$tunnelConfigJson = @"
{
"jumpHost": "bastion.example.com",
"sshUser": "admin",
"tunnels": [
{
"name": "mysql-prod",
"localPort": 13306,
"remoteHost": "10.0.1.50",
"remotePort": 3306
},
{
"name": "redis-prod",
"localPort": 16379,
"remoteHost": "10.0.1.70",
"remotePort": 6379
},
{
"name": "pg-staging",
"localPort": 25432,
"remoteHost": "10.0.2.60",
"remotePort": 5432
}
]
}
"@

function Start-TunnelsFromConfig {
param(
[Parameter(Mandatory)]
[string]$ConfigPath,

[string]$KeyPath
)

$config = Get-Content $ConfigPath -Raw | ConvertFrom-Json

$results = @()
foreach ($t in $config.tunnels) {
Write-Host "正在建立隧道:$($t.name)..." -ForegroundColor Cyan

$tunnel = New-SSHTunnel -JumpHost $config.jumpHost `
-LocalPort $t.localPort `
-RemoteHost $t.remoteHost `
-RemotePort $t.remotePort `
-SshUser $config.sshUser `
-KeyPath $KeyPath

Register-Tunnel -Name $t.name -Process $tunnel `
-LocalPort $t.localPort `
-Target "$($t.remoteHost):$($t.remotePort)"

$results += [PSCustomObject]@{
Name = $t.name
LocalPort = $t.localPort
Target = "$($t.remoteHost):$($t.remotePort)"
PID = $tunnel.Id
}
}

return $results
}

# 保存配置并启动
$tunnelConfigJson | Set-Content "$env:TEMP\tunnels.json" -Encoding UTF8
Start-TunnelsFromConfig -ConfigPath "$env:TEMP\tunnels.json" | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
正在建立隧道:mysql-prod...
SSH 隧道已建立:localhost:13306 -> 10.0.1.50:3306
正在建立隧道:redis-prod...
SSH 隧道已建立:localhost:16379 -> 10.0.1.70:6379
正在建立隧道:pg-staging...
SSH 隧道已建立:localhost:25432 -> 10.0.2.60:5432

Name LocalPort Target PID
---- --------- ------ ---
mysql-prod 13306 10.0.1.50:3306 24516
redis-prod 16379 10.0.1.70:6379 24788
pg-staging 25432 10.0.2.60:5432 25012

注意事项

  1. 端口冲突检查:创建隧道前务必检查本地端口是否已被占用,避免端口冲突导致隧道建立失败
  2. SSH 密钥管理:优先使用密钥认证而非密码,密钥文件权限应设置为仅当前用户可读(Windows 上移除继承权限)
  3. 连接超时配置:SSH 默认不发送 keepalive 包,建议在 ~/.ssh/config 中配置 ServerAliveInterval 60 防止空闲断开
  4. 进程清理:脚本异常退出时 SSH 进程可能残留为孤儿进程,应使用 try/finally 或注册退出事件确保清理
  5. 安全审计:SSH 隧道可绕过防火墙策略,生产环境中应记录所有隧道的创建和销毁操作,纳入审计日志
  6. 网络跳跃限制:复杂场景需要多级跳板时,使用 SSH ProxyJump(-J 参数)比嵌套隧道更可靠且更易维护

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. 安全合规:端口扫描功能仅用于授权的网络诊断和安全审计,不得用于未授权的扫描

PowerShell 技能连载 - Windows 防火墙规则管理

适用于 PowerShell 5.1 及以上版本(Windows 内置模块)

Windows 防火墙是服务器和终端安全的第一道防线。无论是部署新服务、排查网络故障还是进行安全加固,都离不开防火墙规则的配置与管理。传统的图形界面操作虽然直观,但在批量管理和自动化场景下效率低下,且容易遗漏。

PowerShell 内置的 NetSecurity 模块提供了完整的防火墙管理能力,支持查看、创建、修改和删除规则,所有操作都可以脚本化、可重复执行。对于运维工程师来说,掌握这套命令不仅能提高日常工作效率,更是实现基础设施即代码(IaC)的重要基础。

查看防火墙规则

日常运维中最常见的操作就是查看当前生效的防火墙规则。Get-NetFirewallRule 是核心命令,结合 Get-NetFirewallPortFilterGet-NetFirewallAddressFilter 可以获取完整的规则详情。

以下函数封装了常用的查询逻辑,支持按名称、方向、启用状态等条件过滤,并输出格式化的规则摘要:

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
function Get-FirewallRuleSummary {
param(
[string]$Name = "*",
[ValidateSet("Inbound", "Outbound", "Both")]
[string]$Direction = "Both",
[switch]$EnabledOnly
)

$filter = @{ DisplayName = $Name }
if ($Direction -ne "Both") {
$filter["Direction"] = $Direction
}
if ($EnabledOnly) {
$filter["Enabled"] = "True"
}

$rules = Get-NetFirewallRule @filter

$results = foreach ($rule in $rules) {
$portFilter = $rule | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue
$addrFilter = $rule | Get-NetFirewallAddressFilter -ErrorAction SilentlyContinue

[PSCustomObject]@{
Name = $rule.DisplayName
Direction = $rule.Direction
Action = $rule.Action
Enabled = $rule.Enabled
Protocol = if ($portFilter) { $portFilter.Protocol } else { "Any" }
LocalPort = if ($portFilter) { $portFilter.LocalPort } else { "Any" }
RemoteAddr = if ($addrFilter) { $addrFilter.RemoteAddress } else { "Any" }
Profile = $rule.Profile
}
}

return $results | Sort-Object Enabled, Direction, Action
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PS> Get-FirewallRuleSummary -Name "*SSH*" -EnabledOnly

Name : OpenSSH Server (sshd)
Direction : Inbound
Action : Allow
Enabled : True
Protocol : TCP
LocalPort : 22
RemoteAddr : Any
Profile : Any

Name : OpenSSH Client
Direction : Outbound
Action : Allow
Enabled : True
Protocol : TCP
LocalPort : Any
RemoteAddr : Any
Profile : Any

创建防火墙规则

创建规则是防火墙管理中最关键的操作。New-NetFirewallRule 命令支持丰富的参数,可以精确控制协议、端口、地址范围和配置文件。

以下函数将常见的规则创建流程封装为可复用的命令,内置了参数校验和冲突检测逻辑,避免重复创建同名规则:

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
function New-FirewallAllowRule {
param(
[Parameter(Mandatory)]
[string]$Name,

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

[ValidateSet("TCP", "UDP")]
[string]$Protocol = "TCP",

[ValidateSet("Inbound", "Outbound")]
[string]$Direction = "Inbound",

[string[]]$RemoteAddress = "Any",

[ValidateSet("Domain", "Private", "Public", "Any")]
[string]$Profile = "Any"
)

# 检查同名规则是否已存在
$existing = Get-NetFirewallRule -DisplayName $Name -ErrorAction SilentlyContinue
if ($existing) {
Write-Warning "规则 '$Name' 已存在,跳过创建"
return $existing
}

$params = @{
DisplayName = $Name
Direction = $Direction
Action = "Allow"
Protocol = $Protocol
LocalPort = $Port
RemoteAddress = $RemoteAddress
Profile = $Profile
Enabled = "True"
}

$rule = New-NetFirewallRule @params
Write-Host "已创建规则: $Name ($Protocol/$Port, $Direction)"
return $rule
}

执行结果示例:

1
2
3
4
5
PS> New-FirewallAllowRule -Name "Web Server HTTP" -Port 80 -Protocol TCP
已创建规则: Web Server HTTP (TCP/80, Inbound)

PS> New-FirewallAllowRule -Name "Web Server HTTP" -Port 80
WARNING: 规则 'Web Server HTTP' 已存在,跳过创建

批量管理规则

在服务器初始化或安全加固场景中,往往需要一次性配置大量规则。手动逐条创建既繁琐又容易出错,批量管理脚本能显著提升效率和一致性。

以下函数接收规则定义数组,自动创建并输出执行报告,支持回滚操作:

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
function Import-FirewallRuleSet {
param(
[Parameter(Mandatory)]
[string]$RuleSetName,

[Parameter(Mandatory)]
[array]$Rules
)

$report = @()
$created = @()

foreach ($rule in $Rules) {
$qualifiedName = "$RuleSetName - $($rule.Name)"
try {
$result = New-FirewallAllowRule `
-Name $qualifiedName `
-Port $rule.Port `
-Protocol $rule.Protocol `
-Direction $rule.Direction `
-RemoteAddress $rule.RemoteAddress `
-Profile $rule.Profile

if ($result) {
$created += $qualifiedName
$status = "Created"
}
else {
$status = "Skipped"
}
}
catch {
$status = "Error: $($_.Exception.Message)"
}

$report += [PSCustomObject]@{
Rule = $qualifiedName
Port = "$($rule.Protocol)/$($rule.Port)"
Status = $status
}
}

Write-Host "`n规则集 '$RuleSetName' 导入完成: $($created.Count)/$($Rules.Count) 条已创建"
return $report | Format-Table -AutoSize
}

使用示例——批量导入 Web 服务器所需规则:

1
2
3
4
5
6
7
$webRules = @(
@{ Name = "HTTP"; Port = 80; Protocol = "TCP"; Direction = "Inbound"; RemoteAddress = "Any"; Profile = "Any" }
@{ Name = "HTTPS"; Port = 443; Protocol = "TCP"; Direction = "Inbound"; RemoteAddress = "Any"; Profile = "Any" }
@{ Name = "SSH"; Port = 22; Protocol = "TCP"; Direction = "Inbound"; RemoteAddress = "10.0.0.0/8"; Profile = "Domain" }
)

Import-FirewallRuleSet -RuleSetName "Web Server" -Rules $webRules

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
已创建规则: Web Server - HTTP (TCP/80, Inbound)
已创建规则: Web Server - HTTPS (TCP/443, Inbound)
已创建规则: Web Server - SSH (TCP/22, Inbound)

规则集 'Web Server' 导入完成: 3/3 条已创建

Rule Port Status
---- ---- ------
Web Server - HTTP TCP/80 Created
Web Server - HTTPS TCP/443 Created
Web Server - SSH TCP/22 Created

导出与导入规则配置

在多台服务器之间同步防火墙配置,或者在变更前做备份,都需要导出导入功能。以下函数将规则集导出为 JSON 文件,便于版本控制和跨机器部署。

注意这里使用数组拼接 -join 来构建多行文本,避免在代码块中嵌入三反引号导致 Markdown 解析错误:

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
function Export-FirewallRules {
param(
[Parameter(Mandatory)]
[string]$NamePattern,

[Parameter(Mandatory)]
[string]$OutputPath
)

$rules = Get-NetFirewallRule -DisplayName $NamePattern -ErrorAction SilentlyContinue

$export = foreach ($rule in $rules) {
$portFilter = $rule | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue
$addrFilter = $rule | Get-NetFirewallAddressFilter -ErrorAction SilentlyContinue

@{
Name = $rule.DisplayName
Direction = [string]$rule.Direction
Action = [string]$rule.Action
Enabled = [string]$rule.Enabled
Protocol = if ($portFilter) { [string]$portFilter.Protocol } else { "" }
LocalPort = if ($portFilter) { [string]$portFilter.LocalPort } else { "" }
RemoteAddress = if ($addrFilter) { [string]$addrFilter.RemoteAddress } else { "" }
Profile = [string]$rule.Profile
}
}

$jsonLines = @(
"{"
' "ExportDate": "' + (Get-Date -Format "yyyy-MM-dd HH:mm:ss") + '",'
' "RuleCount": ' + $export.Count + ','
' "Rules": ['
)

for ($i = 0; $i -lt $export.Count; $i++) {
$comma = if ($i -lt $export.Count - 1) { "," } else { "" }
$ruleJson = $export[$i] | ConvertTo-Json -Compress
$jsonLines += " $ruleJson$comma"
}

$jsonLines += @(
" ]"
"}"
)

($jsonLines -join "`n") | Out-File -FilePath $OutputPath -Encoding UTF8
Write-Host "已导出 $($export.Count) 条规则到: $OutputPath"
}

执行结果示例:

1
2
PS> Export-FirewallRules -NamePattern "Web Server*" -OutputPath "C:\Backup\firewall-web.json"
已导出 3 条规则到: C:\Backup\firewall-web.json

对应的导入函数从 JSON 文件读取规则定义并批量创建:

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
function Import-FirewallRulesFromFile {
param(
[Parameter(Mandatory)]
[string]$FilePath
)

$content = Get-Content -Path $FilePath -Raw | ConvertFrom-Json
Write-Host "文件包含 $($content.RuleCount) 条规则,导出时间: $($content.ExportDate)"

$results = @()
foreach ($r in $content.Rules) {
try {
$existing = Get-NetFirewallRule -DisplayName $r.Name -ErrorAction SilentlyContinue
if ($existing) {
$results += [PSCustomObject]@{ Name = $r.Name; Status = "AlreadyExists" }
continue
}

New-NetFirewallRule `
-DisplayName $r.Name `
-Direction $r.Direction `
-Action $r.Action `
-Protocol $r.Protocol `
-LocalPort $r.LocalPort `
-RemoteAddress $r.RemoteAddress `
-Profile $r.Profile `
-Enabled $r.Enabled | Out-Null

$results += [PSCustomObject]@{ Name = $r.Name; Status = "Created" }
}
catch {
$results += [PSCustomObject]@{ Name = $r.Name; Status = "Error: $($_.Exception.Message)" }
}
}

$created = ($results | Where-Object Status -eq "Created").Count
Write-Host "`n导入完成: $created 条新建, $($results.Count - $created) 条跳过"
return $results | Format-Table -AutoSize
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
PS> Import-FirewallRulesFromFile -FilePath "C:\Backup\firewall-web.json"
文件包含 3 条规则,导出时间: 2025-04-16 10:30:00

导入完成: 3 条新建, 0 条跳过

Name Status
---- ------
Web Server - HTTP Created
Web Server - HTTPS Created
Web Server - SSH Created

服务器安全加固

生产服务器的防火墙配置直接决定攻击面大小。以下加固脚本针对常见的风险点实施最小权限原则:禁用所有入站流量后仅开放必要端口,限制管理端口的访问来源,并关闭不必要的出站流量。

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
function Initialize-ServerFirewallHardening {
param(
[int[]]$AllowedInboundPorts = @(80, 443),
[int]$SshPort = 22,
[string[]]$AdminNetworks = @("10.0.0.0/8", "172.16.0.0/12"),
[switch]$WhatIf
)

$log = @()

# 第一步:阻止所有默认入站流量
$step1 = "Step 1: 设置默认入站策略为阻止"
$log += $step1
Write-Host $step1

if (-not $WhatIf) {
Set-NetFirewallProfile -Profile Domain,Public,Private `
-DefaultInboundAction Block `
-DefaultOutboundAction Allow `
-Enabled True
}

# 第二步:开放业务端口
$step2 = "Step 2: 开放业务端口 ($($AllowedInboundPorts -join ', '))"
$log += $step2
Write-Host $step2

foreach ($port in $AllowedInboundPorts) {
$ruleName = "Hardened - HTTP/S Port $port"
if (-not $WhatIf) {
New-FirewallAllowRule -Name $ruleName -Port $port -Protocol TCP | Out-Null
}
$log += " -> 已开放端口: $port/TCP"
}

# 第三步:限制管理端口访问来源
$step3 = "Step 3: 限制管理端口 ($SshPort) 仅允许管理网段访问"
$log += $step3
Write-Host $step3

foreach ($network in $AdminNetworks) {
$ruleName = "Hardened - SSH from $network"
if (-not $WhatIf) {
New-FirewallAllowRule -Name $ruleName -Port $SshPort `
-Protocol TCP -RemoteAddress $network | Out-Null
}
$log += " -> 已允许: $network -> $SshPort/TCP"
}

# 第四步:禁用不必要的规则
$step4 = "Step 4: 禁用非必要预置规则"
$log += $step4
Write-Host $step4

$builtinDisabled = @()
$noisyRules = @(
"File and Printer Sharing (Echo Request - ICMPv4-In)"
"Network Discovery (NB-Datagram-In)"
"Network Discovery (NB-Name-In)"
)

foreach ($ruleName in $noisyRules) {
$rule = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
if ($rule -and $rule.Enabled -eq "True") {
if (-not $WhatIf) {
$rule | Disable-NetFirewallRule | Out-Null
}
$builtinDisabled += $ruleName
}
}

$log += " -> 已禁用 $($builtinDisabled.Count) 条预置规则"

# 输出加固报告
$summary = @(
""
"========== 加固报告 =========="
"默认入站策略: Block"
"业务端口: $($AllowedInboundPorts -join ', ')"
"管理端口: $SshPort/TCP (限 $($AdminNetworks -join ', '))"
"禁用预置规则: $($builtinDisabled.Count) 条"
"WhatIf 模式: $WhatIf"
"================================"
)
$summary | ForEach-Object { Write-Host $_ }

return $log
}

执行结果示例(预览模式):

1
2
3
4
5
6
7
8
9
10
11
12
13
PS> Initialize-ServerFirewallHardening -WhatIf
Step 1: 设置默认入站策略为阻止
Step 2: 开放业务端口 (80, 443)
Step 3: 限制管理端口 (22) 仅允许管理网段访问
Step 4: 禁用非必要预置规则

========== 加固报告 ==========
默认入站策略: Block
业务端口: 80, 443
管理端口: 22/TCP (限 10.0.0.0/8, 172.16.0.0/12)
禁用预置规则: 3 条
WhatIf 模式: True
================================

小结

PowerShell 的 NetSecurity 模块让 Windows 防火墙管理变得完全可脚本化。核心命令只有四个——Get-NetFirewallRuleNew-NetFirewallRuleSet-NetFirewallRuleRemove-NetFirewallRule,但配合过滤器和管道可以覆盖几乎所有管理场景。实际运维中建议:始终使用 -WhatIf 预览变更、变更前导出备份、管理端口严格限制来源地址。将这些函数纳入 DSC 或 Ansible Playbook 中,就能实现防火墙策略的版本化管理和自动部署。