1903 lines
119 KiB
PowerShell
1903 lines
119 KiB
PowerShell
|
|
# Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2)
|
|||
|
|
param(
|
|||
|
|
[string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap",
|
|||
|
|
[string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap"
|
|||
|
|
)
|
|||
|
|
$ErrorActionPreference = 'Stop'
|
|||
|
|
$script:pass = 0; $script:fail = 0
|
|||
|
|
function Check($name, $cond, $detail) {
|
|||
|
|
if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) }
|
|||
|
|
else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) }
|
|||
|
|
}
|
|||
|
|
function Read-Pcap($path) {
|
|||
|
|
$b = [IO.File]::ReadAllBytes($path)
|
|||
|
|
function U32([byte[]]$x, [int]$o, [bool]$sw) {
|
|||
|
|
if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] }
|
|||
|
|
return [BitConverter]::ToUInt32($x, $o)
|
|||
|
|
}
|
|||
|
|
$magic = U32 $b 0 $false
|
|||
|
|
$swap = ($magic -eq 0xa1b2c3d4)
|
|||
|
|
$pkts = New-Object System.Collections.Generic.List[object]
|
|||
|
|
$o = 24
|
|||
|
|
while ($o + 16 -le $b.Length) {
|
|||
|
|
$incl = U32 $b ($o+8) $swap
|
|||
|
|
if ($o + 16 + $incl -gt $b.Length) { break }
|
|||
|
|
$data = New-Object byte[] $incl
|
|||
|
|
[Array]::Copy($b, $o+16, $data, 0, $incl)
|
|||
|
|
$pkts.Add([pscustomobject]@{ Len = $incl; Data = $data })
|
|||
|
|
$o += 16 + $incl
|
|||
|
|
}
|
|||
|
|
return $pkts
|
|||
|
|
}
|
|||
|
|
function UdpPayload($pkt) {
|
|||
|
|
$d = $pkt.Data
|
|||
|
|
if ($d.Length -lt 42) { return $null }
|
|||
|
|
$ethType = ([uint32]$d[12] -shl 8) -bor $d[13]
|
|||
|
|
$off = 14
|
|||
|
|
if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 }
|
|||
|
|
if ($ethType -ne 0x0800) { return $null }
|
|||
|
|
$ihl = ($d[$off] -band 0x0F) * 4
|
|||
|
|
if ($d[$off + 9] -ne 17) { return $null }
|
|||
|
|
$ipStart = $off; $udp = $off + $ihl
|
|||
|
|
$src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15]
|
|||
|
|
$dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19]
|
|||
|
|
$sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1]
|
|||
|
|
$dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3]
|
|||
|
|
$pl = New-Object byte[] ($d.Length - $udp - 8)
|
|||
|
|
[Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length)
|
|||
|
|
return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl }
|
|||
|
|
}
|
|||
|
|
function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) }
|
|||
|
|
function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) }
|
|||
|
|
function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' }
|
|||
|
|
|
|||
|
|
Write-Host "=== 载入 $Pcap ==="
|
|||
|
|
$pkts = Read-Pcap $Pcap
|
|||
|
|
Write-Host ("总包数: {0}" -f $pkts.Count)
|
|||
|
|
$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } }
|
|||
|
|
|
|||
|
|
# ---- A1: 31772 帧魔数只允许 ENet / EUCE ----
|
|||
|
|
$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 })
|
|||
|
|
$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 }
|
|||
|
|
$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr"
|
|||
|
|
|
|||
|
|
# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ----
|
|||
|
|
$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' })
|
|||
|
|
$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count)
|
|||
|
|
|
|||
|
|
# ---- A3: ENet 帧类型仅 1/2/4 ----
|
|||
|
|
$types = $enet | Group-Object { LE32 $_.Payload 4 }
|
|||
|
|
$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr"
|
|||
|
|
|
|||
|
|
# ---- A4: 形态A DMX 帧 ----
|
|||
|
|
$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 })
|
|||
|
|
Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count)
|
|||
|
|
Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024"
|
|||
|
|
$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) })
|
|||
|
|
$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } }
|
|||
|
|
Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count)
|
|||
|
|
$tsSame = 0
|
|||
|
|
for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } }
|
|||
|
|
Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0"
|
|||
|
|
|
|||
|
|
# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ----
|
|||
|
|
$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' })
|
|||
|
|
$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' })
|
|||
|
|
Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count)
|
|||
|
|
$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) })
|
|||
|
|
$hbMono = $true; $swaps = 0
|
|||
|
|
for ($i=1; $i -lt $hostSeq.Count; $i++) {
|
|||
|
|
if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序)
|
|||
|
|
elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败
|
|||
|
|
}
|
|||
|
|
Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps)
|
|||
|
|
$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) })
|
|||
|
|
$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count
|
|||
|
|
Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched)
|
|||
|
|
$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' })
|
|||
|
|
Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count)
|
|||
|
|
|
|||
|
|
# ---- A6: 1947 密钥报文 ----
|
|||
|
|
$k = @($msgs | Where-Object { $_.Dport -eq 1947 })
|
|||
|
|
if ($k.Count -gt 0) {
|
|||
|
|
$h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload)
|
|||
|
|
$allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0
|
|||
|
|
Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0)
|
|||
|
|
Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌"
|
|||
|
|
} else { Check "A6 1947负载恒定" $false "| 无样本" }
|
|||
|
|
|
|||
|
|
# ---- A7: A·B·B 节奏 ----
|
|||
|
|
$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21)
|
|||
|
|
$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join ''
|
|||
|
|
Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat"
|
|||
|
|
|
|||
|
|
# ---- C1: type=02 主机标识心跳 ----
|
|||
|
|
$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 })
|
|||
|
|
$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' })
|
|||
|
|
Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count)
|
|||
|
|
|
|||
|
|
# ---- C2: EUCE 帧 ----
|
|||
|
|
$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' })
|
|||
|
|
$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 })
|
|||
|
|
Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count)
|
|||
|
|
|
|||
|
|
# ---- 发现阶段 ----
|
|||
|
|
Write-Host "`n=== 载入 $PcapDiscovery ==="
|
|||
|
|
$pkts2 = Read-Pcap $PcapDiscovery
|
|||
|
|
$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } }
|
|||
|
|
$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 })
|
|||
|
|
$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 })
|
|||
|
|
Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count)
|
|||
|
|
$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 })
|
|||
|
|
$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' })
|
|||
|
|
Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count)
|
|||
|
|
$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致"
|
|||
|
|
$hasHost = $false
|
|||
|
|
foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } }
|
|||
|
|
Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---- R1-R3: RDM 帧 (rdm3.pcap) ----
|
|||
|
|
Write-Host "`n=== 载入 RDM 抓包 ==="
|
|||
|
|
$rdmPcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\rdm3.pcap"
|
|||
|
|
if (Test-Path $rdmPcap) {
|
|||
|
|
$pkts3 = Read-Pcap $rdmPcap
|
|||
|
|
$msgs3 = @(); foreach ($p in $pkts3) { $u = UdpPayload $p; if ($u) { $msgs3 += $u } }
|
|||
|
|
$rdm = @($msgs3 | Where-Object { # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2)
|
|||
|
|
param(
|
|||
|
|
[string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap",
|
|||
|
|
[string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap"
|
|||
|
|
)
|
|||
|
|
$ErrorActionPreference = 'Stop'
|
|||
|
|
$script:pass = 0; $script:fail = 0
|
|||
|
|
function Check($name, $cond, $detail) {
|
|||
|
|
if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) }
|
|||
|
|
else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) }
|
|||
|
|
}
|
|||
|
|
function Read-Pcap($path) {
|
|||
|
|
$b = [IO.File]::ReadAllBytes($path)
|
|||
|
|
function U32([byte[]]$x, [int]$o, [bool]$sw) {
|
|||
|
|
if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] }
|
|||
|
|
return [BitConverter]::ToUInt32($x, $o)
|
|||
|
|
}
|
|||
|
|
$magic = U32 $b 0 $false
|
|||
|
|
$swap = ($magic -eq 0xa1b2c3d4)
|
|||
|
|
$pkts = New-Object System.Collections.Generic.List[object]
|
|||
|
|
$o = 24
|
|||
|
|
while ($o + 16 -le $b.Length) {
|
|||
|
|
$incl = U32 $b ($o+8) $swap
|
|||
|
|
if ($o + 16 + $incl -gt $b.Length) { break }
|
|||
|
|
$data = New-Object byte[] $incl
|
|||
|
|
[Array]::Copy($b, $o+16, $data, 0, $incl)
|
|||
|
|
$pkts.Add([pscustomobject]@{ Len = $incl; Data = $data })
|
|||
|
|
$o += 16 + $incl
|
|||
|
|
}
|
|||
|
|
return $pkts
|
|||
|
|
}
|
|||
|
|
function UdpPayload($pkt) {
|
|||
|
|
$d = $pkt.Data
|
|||
|
|
if ($d.Length -lt 42) { return $null }
|
|||
|
|
$ethType = ([uint32]$d[12] -shl 8) -bor $d[13]
|
|||
|
|
$off = 14
|
|||
|
|
if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 }
|
|||
|
|
if ($ethType -ne 0x0800) { return $null }
|
|||
|
|
$ihl = ($d[$off] -band 0x0F) * 4
|
|||
|
|
if ($d[$off + 9] -ne 17) { return $null }
|
|||
|
|
$ipStart = $off; $udp = $off + $ihl
|
|||
|
|
$src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15]
|
|||
|
|
$dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19]
|
|||
|
|
$sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1]
|
|||
|
|
$dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3]
|
|||
|
|
$pl = New-Object byte[] ($d.Length - $udp - 8)
|
|||
|
|
[Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length)
|
|||
|
|
return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl }
|
|||
|
|
}
|
|||
|
|
function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) }
|
|||
|
|
function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) }
|
|||
|
|
function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' }
|
|||
|
|
|
|||
|
|
Write-Host "=== 载入 $Pcap ==="
|
|||
|
|
$pkts = Read-Pcap $Pcap
|
|||
|
|
Write-Host ("总包数: {0}" -f $pkts.Count)
|
|||
|
|
$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } }
|
|||
|
|
|
|||
|
|
# ---- A1: 31772 帧魔数只允许 ENet / EUCE ----
|
|||
|
|
$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 })
|
|||
|
|
$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 }
|
|||
|
|
$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr"
|
|||
|
|
|
|||
|
|
# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ----
|
|||
|
|
$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' })
|
|||
|
|
$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count)
|
|||
|
|
|
|||
|
|
# ---- A3: ENet 帧类型仅 1/2/4 ----
|
|||
|
|
$types = $enet | Group-Object { LE32 $_.Payload 4 }
|
|||
|
|
$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr"
|
|||
|
|
|
|||
|
|
# ---- A4: 形态A DMX 帧 ----
|
|||
|
|
$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 })
|
|||
|
|
Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count)
|
|||
|
|
Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024"
|
|||
|
|
$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) })
|
|||
|
|
$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } }
|
|||
|
|
Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count)
|
|||
|
|
$tsSame = 0
|
|||
|
|
for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } }
|
|||
|
|
Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0"
|
|||
|
|
|
|||
|
|
# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ----
|
|||
|
|
$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' })
|
|||
|
|
$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' })
|
|||
|
|
Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count)
|
|||
|
|
$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) })
|
|||
|
|
$hbMono = $true; $swaps = 0
|
|||
|
|
for ($i=1; $i -lt $hostSeq.Count; $i++) {
|
|||
|
|
if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序)
|
|||
|
|
elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败
|
|||
|
|
}
|
|||
|
|
Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps)
|
|||
|
|
$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) })
|
|||
|
|
$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count
|
|||
|
|
Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched)
|
|||
|
|
$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' })
|
|||
|
|
Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count)
|
|||
|
|
|
|||
|
|
# ---- A6: 1947 密钥报文 ----
|
|||
|
|
$k = @($msgs | Where-Object { $_.Dport -eq 1947 })
|
|||
|
|
if ($k.Count -gt 0) {
|
|||
|
|
$h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload)
|
|||
|
|
$allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0
|
|||
|
|
Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0)
|
|||
|
|
Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌"
|
|||
|
|
} else { Check "A6 1947负载恒定" $false "| 无样本" }
|
|||
|
|
|
|||
|
|
# ---- A7: A·B·B 节奏 ----
|
|||
|
|
$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21)
|
|||
|
|
$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join ''
|
|||
|
|
Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat"
|
|||
|
|
|
|||
|
|
# ---- C1: type=02 主机标识心跳 ----
|
|||
|
|
$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 })
|
|||
|
|
$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' })
|
|||
|
|
Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count)
|
|||
|
|
|
|||
|
|
# ---- C2: EUCE 帧 ----
|
|||
|
|
$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' })
|
|||
|
|
$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 })
|
|||
|
|
Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count)
|
|||
|
|
|
|||
|
|
# ---- 发现阶段 ----
|
|||
|
|
Write-Host "`n=== 载入 $PcapDiscovery ==="
|
|||
|
|
$pkts2 = Read-Pcap $PcapDiscovery
|
|||
|
|
$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } }
|
|||
|
|
$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 })
|
|||
|
|
$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 })
|
|||
|
|
Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count)
|
|||
|
|
$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 })
|
|||
|
|
$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' })
|
|||
|
|
Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count)
|
|||
|
|
$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致"
|
|||
|
|
$hasHost = $false
|
|||
|
|
foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } }
|
|||
|
|
Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在"
|
|||
|
|
|
|||
|
|
Write-Host "`n========================================"
|
|||
|
|
Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail)
|
|||
|
|
if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" }
|
|||
|
|
.Src -eq '192.168.123.1' -and (LE32 # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2)
|
|||
|
|
param(
|
|||
|
|
[string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap",
|
|||
|
|
[string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap"
|
|||
|
|
)
|
|||
|
|
$ErrorActionPreference = 'Stop'
|
|||
|
|
$script:pass = 0; $script:fail = 0
|
|||
|
|
function Check($name, $cond, $detail) {
|
|||
|
|
if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) }
|
|||
|
|
else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) }
|
|||
|
|
}
|
|||
|
|
function Read-Pcap($path) {
|
|||
|
|
$b = [IO.File]::ReadAllBytes($path)
|
|||
|
|
function U32([byte[]]$x, [int]$o, [bool]$sw) {
|
|||
|
|
if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] }
|
|||
|
|
return [BitConverter]::ToUInt32($x, $o)
|
|||
|
|
}
|
|||
|
|
$magic = U32 $b 0 $false
|
|||
|
|
$swap = ($magic -eq 0xa1b2c3d4)
|
|||
|
|
$pkts = New-Object System.Collections.Generic.List[object]
|
|||
|
|
$o = 24
|
|||
|
|
while ($o + 16 -le $b.Length) {
|
|||
|
|
$incl = U32 $b ($o+8) $swap
|
|||
|
|
if ($o + 16 + $incl -gt $b.Length) { break }
|
|||
|
|
$data = New-Object byte[] $incl
|
|||
|
|
[Array]::Copy($b, $o+16, $data, 0, $incl)
|
|||
|
|
$pkts.Add([pscustomobject]@{ Len = $incl; Data = $data })
|
|||
|
|
$o += 16 + $incl
|
|||
|
|
}
|
|||
|
|
return $pkts
|
|||
|
|
}
|
|||
|
|
function UdpPayload($pkt) {
|
|||
|
|
$d = $pkt.Data
|
|||
|
|
if ($d.Length -lt 42) { return $null }
|
|||
|
|
$ethType = ([uint32]$d[12] -shl 8) -bor $d[13]
|
|||
|
|
$off = 14
|
|||
|
|
if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 }
|
|||
|
|
if ($ethType -ne 0x0800) { return $null }
|
|||
|
|
$ihl = ($d[$off] -band 0x0F) * 4
|
|||
|
|
if ($d[$off + 9] -ne 17) { return $null }
|
|||
|
|
$ipStart = $off; $udp = $off + $ihl
|
|||
|
|
$src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15]
|
|||
|
|
$dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19]
|
|||
|
|
$sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1]
|
|||
|
|
$dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3]
|
|||
|
|
$pl = New-Object byte[] ($d.Length - $udp - 8)
|
|||
|
|
[Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length)
|
|||
|
|
return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl }
|
|||
|
|
}
|
|||
|
|
function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) }
|
|||
|
|
function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) }
|
|||
|
|
function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' }
|
|||
|
|
|
|||
|
|
Write-Host "=== 载入 $Pcap ==="
|
|||
|
|
$pkts = Read-Pcap $Pcap
|
|||
|
|
Write-Host ("总包数: {0}" -f $pkts.Count)
|
|||
|
|
$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } }
|
|||
|
|
|
|||
|
|
# ---- A1: 31772 帧魔数只允许 ENet / EUCE ----
|
|||
|
|
$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 })
|
|||
|
|
$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 }
|
|||
|
|
$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr"
|
|||
|
|
|
|||
|
|
# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ----
|
|||
|
|
$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' })
|
|||
|
|
$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count)
|
|||
|
|
|
|||
|
|
# ---- A3: ENet 帧类型仅 1/2/4 ----
|
|||
|
|
$types = $enet | Group-Object { LE32 $_.Payload 4 }
|
|||
|
|
$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr"
|
|||
|
|
|
|||
|
|
# ---- A4: 形态A DMX 帧 ----
|
|||
|
|
$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 })
|
|||
|
|
Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count)
|
|||
|
|
Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024"
|
|||
|
|
$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) })
|
|||
|
|
$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } }
|
|||
|
|
Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count)
|
|||
|
|
$tsSame = 0
|
|||
|
|
for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } }
|
|||
|
|
Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0"
|
|||
|
|
|
|||
|
|
# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ----
|
|||
|
|
$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' })
|
|||
|
|
$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' })
|
|||
|
|
Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count)
|
|||
|
|
$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) })
|
|||
|
|
$hbMono = $true; $swaps = 0
|
|||
|
|
for ($i=1; $i -lt $hostSeq.Count; $i++) {
|
|||
|
|
if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序)
|
|||
|
|
elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败
|
|||
|
|
}
|
|||
|
|
Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps)
|
|||
|
|
$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) })
|
|||
|
|
$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count
|
|||
|
|
Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched)
|
|||
|
|
$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' })
|
|||
|
|
Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count)
|
|||
|
|
|
|||
|
|
# ---- A6: 1947 密钥报文 ----
|
|||
|
|
$k = @($msgs | Where-Object { $_.Dport -eq 1947 })
|
|||
|
|
if ($k.Count -gt 0) {
|
|||
|
|
$h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload)
|
|||
|
|
$allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0
|
|||
|
|
Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0)
|
|||
|
|
Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌"
|
|||
|
|
} else { Check "A6 1947负载恒定" $false "| 无样本" }
|
|||
|
|
|
|||
|
|
# ---- A7: A·B·B 节奏 ----
|
|||
|
|
$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21)
|
|||
|
|
$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join ''
|
|||
|
|
Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat"
|
|||
|
|
|
|||
|
|
# ---- C1: type=02 主机标识心跳 ----
|
|||
|
|
$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 })
|
|||
|
|
$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' })
|
|||
|
|
Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count)
|
|||
|
|
|
|||
|
|
# ---- C2: EUCE 帧 ----
|
|||
|
|
$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' })
|
|||
|
|
$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 })
|
|||
|
|
Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count)
|
|||
|
|
|
|||
|
|
# ---- 发现阶段 ----
|
|||
|
|
Write-Host "`n=== 载入 $PcapDiscovery ==="
|
|||
|
|
$pkts2 = Read-Pcap $PcapDiscovery
|
|||
|
|
$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } }
|
|||
|
|
$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 })
|
|||
|
|
$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 })
|
|||
|
|
Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count)
|
|||
|
|
$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 })
|
|||
|
|
$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' })
|
|||
|
|
Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count)
|
|||
|
|
$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致"
|
|||
|
|
$hasHost = $false
|
|||
|
|
foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } }
|
|||
|
|
Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在"
|
|||
|
|
|
|||
|
|
Write-Host "`n========================================"
|
|||
|
|
Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail)
|
|||
|
|
if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" }
|
|||
|
|
.Payload 4) -eq 4 -and (Hex # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2)
|
|||
|
|
param(
|
|||
|
|
[string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap",
|
|||
|
|
[string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap"
|
|||
|
|
)
|
|||
|
|
$ErrorActionPreference = 'Stop'
|
|||
|
|
$script:pass = 0; $script:fail = 0
|
|||
|
|
function Check($name, $cond, $detail) {
|
|||
|
|
if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) }
|
|||
|
|
else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) }
|
|||
|
|
}
|
|||
|
|
function Read-Pcap($path) {
|
|||
|
|
$b = [IO.File]::ReadAllBytes($path)
|
|||
|
|
function U32([byte[]]$x, [int]$o, [bool]$sw) {
|
|||
|
|
if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] }
|
|||
|
|
return [BitConverter]::ToUInt32($x, $o)
|
|||
|
|
}
|
|||
|
|
$magic = U32 $b 0 $false
|
|||
|
|
$swap = ($magic -eq 0xa1b2c3d4)
|
|||
|
|
$pkts = New-Object System.Collections.Generic.List[object]
|
|||
|
|
$o = 24
|
|||
|
|
while ($o + 16 -le $b.Length) {
|
|||
|
|
$incl = U32 $b ($o+8) $swap
|
|||
|
|
if ($o + 16 + $incl -gt $b.Length) { break }
|
|||
|
|
$data = New-Object byte[] $incl
|
|||
|
|
[Array]::Copy($b, $o+16, $data, 0, $incl)
|
|||
|
|
$pkts.Add([pscustomobject]@{ Len = $incl; Data = $data })
|
|||
|
|
$o += 16 + $incl
|
|||
|
|
}
|
|||
|
|
return $pkts
|
|||
|
|
}
|
|||
|
|
function UdpPayload($pkt) {
|
|||
|
|
$d = $pkt.Data
|
|||
|
|
if ($d.Length -lt 42) { return $null }
|
|||
|
|
$ethType = ([uint32]$d[12] -shl 8) -bor $d[13]
|
|||
|
|
$off = 14
|
|||
|
|
if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 }
|
|||
|
|
if ($ethType -ne 0x0800) { return $null }
|
|||
|
|
$ihl = ($d[$off] -band 0x0F) * 4
|
|||
|
|
if ($d[$off + 9] -ne 17) { return $null }
|
|||
|
|
$ipStart = $off; $udp = $off + $ihl
|
|||
|
|
$src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15]
|
|||
|
|
$dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19]
|
|||
|
|
$sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1]
|
|||
|
|
$dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3]
|
|||
|
|
$pl = New-Object byte[] ($d.Length - $udp - 8)
|
|||
|
|
[Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length)
|
|||
|
|
return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl }
|
|||
|
|
}
|
|||
|
|
function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) }
|
|||
|
|
function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) }
|
|||
|
|
function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' }
|
|||
|
|
|
|||
|
|
Write-Host "=== 载入 $Pcap ==="
|
|||
|
|
$pkts = Read-Pcap $Pcap
|
|||
|
|
Write-Host ("总包数: {0}" -f $pkts.Count)
|
|||
|
|
$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } }
|
|||
|
|
|
|||
|
|
# ---- A1: 31772 帧魔数只允许 ENet / EUCE ----
|
|||
|
|
$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 })
|
|||
|
|
$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 }
|
|||
|
|
$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr"
|
|||
|
|
|
|||
|
|
# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ----
|
|||
|
|
$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' })
|
|||
|
|
$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count)
|
|||
|
|
|
|||
|
|
# ---- A3: ENet 帧类型仅 1/2/4 ----
|
|||
|
|
$types = $enet | Group-Object { LE32 $_.Payload 4 }
|
|||
|
|
$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr"
|
|||
|
|
|
|||
|
|
# ---- A4: 形态A DMX 帧 ----
|
|||
|
|
$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 })
|
|||
|
|
Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count)
|
|||
|
|
Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024"
|
|||
|
|
$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) })
|
|||
|
|
$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } }
|
|||
|
|
Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count)
|
|||
|
|
$tsSame = 0
|
|||
|
|
for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } }
|
|||
|
|
Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0"
|
|||
|
|
|
|||
|
|
# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ----
|
|||
|
|
$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' })
|
|||
|
|
$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' })
|
|||
|
|
Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count)
|
|||
|
|
$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) })
|
|||
|
|
$hbMono = $true; $swaps = 0
|
|||
|
|
for ($i=1; $i -lt $hostSeq.Count; $i++) {
|
|||
|
|
if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序)
|
|||
|
|
elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败
|
|||
|
|
}
|
|||
|
|
Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps)
|
|||
|
|
$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) })
|
|||
|
|
$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count
|
|||
|
|
Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched)
|
|||
|
|
$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' })
|
|||
|
|
Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count)
|
|||
|
|
|
|||
|
|
# ---- A6: 1947 密钥报文 ----
|
|||
|
|
$k = @($msgs | Where-Object { $_.Dport -eq 1947 })
|
|||
|
|
if ($k.Count -gt 0) {
|
|||
|
|
$h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload)
|
|||
|
|
$allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0
|
|||
|
|
Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0)
|
|||
|
|
Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌"
|
|||
|
|
} else { Check "A6 1947负载恒定" $false "| 无样本" }
|
|||
|
|
|
|||
|
|
# ---- A7: A·B·B 节奏 ----
|
|||
|
|
$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21)
|
|||
|
|
$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join ''
|
|||
|
|
Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat"
|
|||
|
|
|
|||
|
|
# ---- C1: type=02 主机标识心跳 ----
|
|||
|
|
$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 })
|
|||
|
|
$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' })
|
|||
|
|
Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count)
|
|||
|
|
|
|||
|
|
# ---- C2: EUCE 帧 ----
|
|||
|
|
$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' })
|
|||
|
|
$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 })
|
|||
|
|
Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count)
|
|||
|
|
|
|||
|
|
# ---- 发现阶段 ----
|
|||
|
|
Write-Host "`n=== 载入 $PcapDiscovery ==="
|
|||
|
|
$pkts2 = Read-Pcap $PcapDiscovery
|
|||
|
|
$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } }
|
|||
|
|
$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 })
|
|||
|
|
$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 })
|
|||
|
|
Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count)
|
|||
|
|
$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 })
|
|||
|
|
$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' })
|
|||
|
|
Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count)
|
|||
|
|
$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致"
|
|||
|
|
$hasHost = $false
|
|||
|
|
foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } }
|
|||
|
|
Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在"
|
|||
|
|
|
|||
|
|
Write-Host "`n========================================"
|
|||
|
|
Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail)
|
|||
|
|
if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" }
|
|||
|
|
.Payload 0x1A 4) -eq '4275746c' -and (Hex # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2)
|
|||
|
|
param(
|
|||
|
|
[string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap",
|
|||
|
|
[string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap"
|
|||
|
|
)
|
|||
|
|
$ErrorActionPreference = 'Stop'
|
|||
|
|
$script:pass = 0; $script:fail = 0
|
|||
|
|
function Check($name, $cond, $detail) {
|
|||
|
|
if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) }
|
|||
|
|
else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) }
|
|||
|
|
}
|
|||
|
|
function Read-Pcap($path) {
|
|||
|
|
$b = [IO.File]::ReadAllBytes($path)
|
|||
|
|
function U32([byte[]]$x, [int]$o, [bool]$sw) {
|
|||
|
|
if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] }
|
|||
|
|
return [BitConverter]::ToUInt32($x, $o)
|
|||
|
|
}
|
|||
|
|
$magic = U32 $b 0 $false
|
|||
|
|
$swap = ($magic -eq 0xa1b2c3d4)
|
|||
|
|
$pkts = New-Object System.Collections.Generic.List[object]
|
|||
|
|
$o = 24
|
|||
|
|
while ($o + 16 -le $b.Length) {
|
|||
|
|
$incl = U32 $b ($o+8) $swap
|
|||
|
|
if ($o + 16 + $incl -gt $b.Length) { break }
|
|||
|
|
$data = New-Object byte[] $incl
|
|||
|
|
[Array]::Copy($b, $o+16, $data, 0, $incl)
|
|||
|
|
$pkts.Add([pscustomobject]@{ Len = $incl; Data = $data })
|
|||
|
|
$o += 16 + $incl
|
|||
|
|
}
|
|||
|
|
return $pkts
|
|||
|
|
}
|
|||
|
|
function UdpPayload($pkt) {
|
|||
|
|
$d = $pkt.Data
|
|||
|
|
if ($d.Length -lt 42) { return $null }
|
|||
|
|
$ethType = ([uint32]$d[12] -shl 8) -bor $d[13]
|
|||
|
|
$off = 14
|
|||
|
|
if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 }
|
|||
|
|
if ($ethType -ne 0x0800) { return $null }
|
|||
|
|
$ihl = ($d[$off] -band 0x0F) * 4
|
|||
|
|
if ($d[$off + 9] -ne 17) { return $null }
|
|||
|
|
$ipStart = $off; $udp = $off + $ihl
|
|||
|
|
$src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15]
|
|||
|
|
$dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19]
|
|||
|
|
$sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1]
|
|||
|
|
$dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3]
|
|||
|
|
$pl = New-Object byte[] ($d.Length - $udp - 8)
|
|||
|
|
[Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length)
|
|||
|
|
return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl }
|
|||
|
|
}
|
|||
|
|
function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) }
|
|||
|
|
function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) }
|
|||
|
|
function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' }
|
|||
|
|
|
|||
|
|
Write-Host "=== 载入 $Pcap ==="
|
|||
|
|
$pkts = Read-Pcap $Pcap
|
|||
|
|
Write-Host ("总包数: {0}" -f $pkts.Count)
|
|||
|
|
$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } }
|
|||
|
|
|
|||
|
|
# ---- A1: 31772 帧魔数只允许 ENet / EUCE ----
|
|||
|
|
$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 })
|
|||
|
|
$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 }
|
|||
|
|
$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr"
|
|||
|
|
|
|||
|
|
# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ----
|
|||
|
|
$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' })
|
|||
|
|
$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count)
|
|||
|
|
|
|||
|
|
# ---- A3: ENet 帧类型仅 1/2/4 ----
|
|||
|
|
$types = $enet | Group-Object { LE32 $_.Payload 4 }
|
|||
|
|
$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr"
|
|||
|
|
|
|||
|
|
# ---- A4: 形态A DMX 帧 ----
|
|||
|
|
$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 })
|
|||
|
|
Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count)
|
|||
|
|
Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024"
|
|||
|
|
$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) })
|
|||
|
|
$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } }
|
|||
|
|
Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count)
|
|||
|
|
$tsSame = 0
|
|||
|
|
for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } }
|
|||
|
|
Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0"
|
|||
|
|
|
|||
|
|
# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ----
|
|||
|
|
$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' })
|
|||
|
|
$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' })
|
|||
|
|
Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count)
|
|||
|
|
$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) })
|
|||
|
|
$hbMono = $true; $swaps = 0
|
|||
|
|
for ($i=1; $i -lt $hostSeq.Count; $i++) {
|
|||
|
|
if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序)
|
|||
|
|
elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败
|
|||
|
|
}
|
|||
|
|
Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps)
|
|||
|
|
$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) })
|
|||
|
|
$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count
|
|||
|
|
Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched)
|
|||
|
|
$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' })
|
|||
|
|
Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count)
|
|||
|
|
|
|||
|
|
# ---- A6: 1947 密钥报文 ----
|
|||
|
|
$k = @($msgs | Where-Object { $_.Dport -eq 1947 })
|
|||
|
|
if ($k.Count -gt 0) {
|
|||
|
|
$h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload)
|
|||
|
|
$allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0
|
|||
|
|
Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0)
|
|||
|
|
Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌"
|
|||
|
|
} else { Check "A6 1947负载恒定" $false "| 无样本" }
|
|||
|
|
|
|||
|
|
# ---- A7: A·B·B 节奏 ----
|
|||
|
|
$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21)
|
|||
|
|
$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join ''
|
|||
|
|
Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat"
|
|||
|
|
|
|||
|
|
# ---- C1: type=02 主机标识心跳 ----
|
|||
|
|
$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 })
|
|||
|
|
$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' })
|
|||
|
|
Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count)
|
|||
|
|
|
|||
|
|
# ---- C2: EUCE 帧 ----
|
|||
|
|
$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' })
|
|||
|
|
$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 })
|
|||
|
|
Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count)
|
|||
|
|
|
|||
|
|
# ---- 发现阶段 ----
|
|||
|
|
Write-Host "`n=== 载入 $PcapDiscovery ==="
|
|||
|
|
$pkts2 = Read-Pcap $PcapDiscovery
|
|||
|
|
$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } }
|
|||
|
|
$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 })
|
|||
|
|
$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 })
|
|||
|
|
Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count)
|
|||
|
|
$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 })
|
|||
|
|
$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' })
|
|||
|
|
Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count)
|
|||
|
|
$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致"
|
|||
|
|
$hasHost = $false
|
|||
|
|
foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } }
|
|||
|
|
Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在"
|
|||
|
|
|
|||
|
|
Write-Host "`n========================================"
|
|||
|
|
Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail)
|
|||
|
|
if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" }
|
|||
|
|
.Payload 0x2A 2) -eq 'cc01' })
|
|||
|
|
Check "R1 RDM帧=Butler→主机+cc01" ($rdm.Count -gt 0) ("| n={0} 长度分布={1}" -f $rdm.Count, (($rdm | Group-Object { # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2)
|
|||
|
|
param(
|
|||
|
|
[string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap",
|
|||
|
|
[string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap"
|
|||
|
|
)
|
|||
|
|
$ErrorActionPreference = 'Stop'
|
|||
|
|
$script:pass = 0; $script:fail = 0
|
|||
|
|
function Check($name, $cond, $detail) {
|
|||
|
|
if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) }
|
|||
|
|
else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) }
|
|||
|
|
}
|
|||
|
|
function Read-Pcap($path) {
|
|||
|
|
$b = [IO.File]::ReadAllBytes($path)
|
|||
|
|
function U32([byte[]]$x, [int]$o, [bool]$sw) {
|
|||
|
|
if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] }
|
|||
|
|
return [BitConverter]::ToUInt32($x, $o)
|
|||
|
|
}
|
|||
|
|
$magic = U32 $b 0 $false
|
|||
|
|
$swap = ($magic -eq 0xa1b2c3d4)
|
|||
|
|
$pkts = New-Object System.Collections.Generic.List[object]
|
|||
|
|
$o = 24
|
|||
|
|
while ($o + 16 -le $b.Length) {
|
|||
|
|
$incl = U32 $b ($o+8) $swap
|
|||
|
|
if ($o + 16 + $incl -gt $b.Length) { break }
|
|||
|
|
$data = New-Object byte[] $incl
|
|||
|
|
[Array]::Copy($b, $o+16, $data, 0, $incl)
|
|||
|
|
$pkts.Add([pscustomobject]@{ Len = $incl; Data = $data })
|
|||
|
|
$o += 16 + $incl
|
|||
|
|
}
|
|||
|
|
return $pkts
|
|||
|
|
}
|
|||
|
|
function UdpPayload($pkt) {
|
|||
|
|
$d = $pkt.Data
|
|||
|
|
if ($d.Length -lt 42) { return $null }
|
|||
|
|
$ethType = ([uint32]$d[12] -shl 8) -bor $d[13]
|
|||
|
|
$off = 14
|
|||
|
|
if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 }
|
|||
|
|
if ($ethType -ne 0x0800) { return $null }
|
|||
|
|
$ihl = ($d[$off] -band 0x0F) * 4
|
|||
|
|
if ($d[$off + 9] -ne 17) { return $null }
|
|||
|
|
$ipStart = $off; $udp = $off + $ihl
|
|||
|
|
$src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15]
|
|||
|
|
$dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19]
|
|||
|
|
$sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1]
|
|||
|
|
$dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3]
|
|||
|
|
$pl = New-Object byte[] ($d.Length - $udp - 8)
|
|||
|
|
[Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length)
|
|||
|
|
return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl }
|
|||
|
|
}
|
|||
|
|
function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) }
|
|||
|
|
function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) }
|
|||
|
|
function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' }
|
|||
|
|
|
|||
|
|
Write-Host "=== 载入 $Pcap ==="
|
|||
|
|
$pkts = Read-Pcap $Pcap
|
|||
|
|
Write-Host ("总包数: {0}" -f $pkts.Count)
|
|||
|
|
$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } }
|
|||
|
|
|
|||
|
|
# ---- A1: 31772 帧魔数只允许 ENet / EUCE ----
|
|||
|
|
$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 })
|
|||
|
|
$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 }
|
|||
|
|
$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr"
|
|||
|
|
|
|||
|
|
# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ----
|
|||
|
|
$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' })
|
|||
|
|
$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count)
|
|||
|
|
|
|||
|
|
# ---- A3: ENet 帧类型仅 1/2/4 ----
|
|||
|
|
$types = $enet | Group-Object { LE32 $_.Payload 4 }
|
|||
|
|
$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr"
|
|||
|
|
|
|||
|
|
# ---- A4: 形态A DMX 帧 ----
|
|||
|
|
$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 })
|
|||
|
|
Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count)
|
|||
|
|
Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024"
|
|||
|
|
$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) })
|
|||
|
|
$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } }
|
|||
|
|
Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count)
|
|||
|
|
$tsSame = 0
|
|||
|
|
for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } }
|
|||
|
|
Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0"
|
|||
|
|
|
|||
|
|
# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ----
|
|||
|
|
$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' })
|
|||
|
|
$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' })
|
|||
|
|
Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count)
|
|||
|
|
$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) })
|
|||
|
|
$hbMono = $true; $swaps = 0
|
|||
|
|
for ($i=1; $i -lt $hostSeq.Count; $i++) {
|
|||
|
|
if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序)
|
|||
|
|
elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败
|
|||
|
|
}
|
|||
|
|
Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps)
|
|||
|
|
$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) })
|
|||
|
|
$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count
|
|||
|
|
Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched)
|
|||
|
|
$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' })
|
|||
|
|
Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count)
|
|||
|
|
|
|||
|
|
# ---- A6: 1947 密钥报文 ----
|
|||
|
|
$k = @($msgs | Where-Object { $_.Dport -eq 1947 })
|
|||
|
|
if ($k.Count -gt 0) {
|
|||
|
|
$h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload)
|
|||
|
|
$allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0
|
|||
|
|
Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0)
|
|||
|
|
Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌"
|
|||
|
|
} else { Check "A6 1947负载恒定" $false "| 无样本" }
|
|||
|
|
|
|||
|
|
# ---- A7: A·B·B 节奏 ----
|
|||
|
|
$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21)
|
|||
|
|
$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join ''
|
|||
|
|
Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat"
|
|||
|
|
|
|||
|
|
# ---- C1: type=02 主机标识心跳 ----
|
|||
|
|
$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 })
|
|||
|
|
$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' })
|
|||
|
|
Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count)
|
|||
|
|
|
|||
|
|
# ---- C2: EUCE 帧 ----
|
|||
|
|
$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' })
|
|||
|
|
$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 })
|
|||
|
|
Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count)
|
|||
|
|
|
|||
|
|
# ---- 发现阶段 ----
|
|||
|
|
Write-Host "`n=== 载入 $PcapDiscovery ==="
|
|||
|
|
$pkts2 = Read-Pcap $PcapDiscovery
|
|||
|
|
$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } }
|
|||
|
|
$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 })
|
|||
|
|
$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 })
|
|||
|
|
Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count)
|
|||
|
|
$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 })
|
|||
|
|
$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' })
|
|||
|
|
Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count)
|
|||
|
|
$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致"
|
|||
|
|
$hasHost = $false
|
|||
|
|
foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } }
|
|||
|
|
Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在"
|
|||
|
|
|
|||
|
|
Write-Host "`n========================================"
|
|||
|
|
Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail)
|
|||
|
|
if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" }
|
|||
|
|
.Payload.Length } | ForEach-Object { "$(# Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2)
|
|||
|
|
param(
|
|||
|
|
[string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap",
|
|||
|
|
[string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap"
|
|||
|
|
)
|
|||
|
|
$ErrorActionPreference = 'Stop'
|
|||
|
|
$script:pass = 0; $script:fail = 0
|
|||
|
|
function Check($name, $cond, $detail) {
|
|||
|
|
if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) }
|
|||
|
|
else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) }
|
|||
|
|
}
|
|||
|
|
function Read-Pcap($path) {
|
|||
|
|
$b = [IO.File]::ReadAllBytes($path)
|
|||
|
|
function U32([byte[]]$x, [int]$o, [bool]$sw) {
|
|||
|
|
if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] }
|
|||
|
|
return [BitConverter]::ToUInt32($x, $o)
|
|||
|
|
}
|
|||
|
|
$magic = U32 $b 0 $false
|
|||
|
|
$swap = ($magic -eq 0xa1b2c3d4)
|
|||
|
|
$pkts = New-Object System.Collections.Generic.List[object]
|
|||
|
|
$o = 24
|
|||
|
|
while ($o + 16 -le $b.Length) {
|
|||
|
|
$incl = U32 $b ($o+8) $swap
|
|||
|
|
if ($o + 16 + $incl -gt $b.Length) { break }
|
|||
|
|
$data = New-Object byte[] $incl
|
|||
|
|
[Array]::Copy($b, $o+16, $data, 0, $incl)
|
|||
|
|
$pkts.Add([pscustomobject]@{ Len = $incl; Data = $data })
|
|||
|
|
$o += 16 + $incl
|
|||
|
|
}
|
|||
|
|
return $pkts
|
|||
|
|
}
|
|||
|
|
function UdpPayload($pkt) {
|
|||
|
|
$d = $pkt.Data
|
|||
|
|
if ($d.Length -lt 42) { return $null }
|
|||
|
|
$ethType = ([uint32]$d[12] -shl 8) -bor $d[13]
|
|||
|
|
$off = 14
|
|||
|
|
if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 }
|
|||
|
|
if ($ethType -ne 0x0800) { return $null }
|
|||
|
|
$ihl = ($d[$off] -band 0x0F) * 4
|
|||
|
|
if ($d[$off + 9] -ne 17) { return $null }
|
|||
|
|
$ipStart = $off; $udp = $off + $ihl
|
|||
|
|
$src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15]
|
|||
|
|
$dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19]
|
|||
|
|
$sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1]
|
|||
|
|
$dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3]
|
|||
|
|
$pl = New-Object byte[] ($d.Length - $udp - 8)
|
|||
|
|
[Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length)
|
|||
|
|
return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl }
|
|||
|
|
}
|
|||
|
|
function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) }
|
|||
|
|
function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) }
|
|||
|
|
function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' }
|
|||
|
|
|
|||
|
|
Write-Host "=== 载入 $Pcap ==="
|
|||
|
|
$pkts = Read-Pcap $Pcap
|
|||
|
|
Write-Host ("总包数: {0}" -f $pkts.Count)
|
|||
|
|
$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } }
|
|||
|
|
|
|||
|
|
# ---- A1: 31772 帧魔数只允许 ENet / EUCE ----
|
|||
|
|
$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 })
|
|||
|
|
$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 }
|
|||
|
|
$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr"
|
|||
|
|
|
|||
|
|
# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ----
|
|||
|
|
$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' })
|
|||
|
|
$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count)
|
|||
|
|
|
|||
|
|
# ---- A3: ENet 帧类型仅 1/2/4 ----
|
|||
|
|
$types = $enet | Group-Object { LE32 $_.Payload 4 }
|
|||
|
|
$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr"
|
|||
|
|
|
|||
|
|
# ---- A4: 形态A DMX 帧 ----
|
|||
|
|
$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 })
|
|||
|
|
Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count)
|
|||
|
|
Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024"
|
|||
|
|
$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) })
|
|||
|
|
$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } }
|
|||
|
|
Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count)
|
|||
|
|
$tsSame = 0
|
|||
|
|
for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } }
|
|||
|
|
Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0"
|
|||
|
|
|
|||
|
|
# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ----
|
|||
|
|
$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' })
|
|||
|
|
$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' })
|
|||
|
|
Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count)
|
|||
|
|
$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) })
|
|||
|
|
$hbMono = $true; $swaps = 0
|
|||
|
|
for ($i=1; $i -lt $hostSeq.Count; $i++) {
|
|||
|
|
if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序)
|
|||
|
|
elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败
|
|||
|
|
}
|
|||
|
|
Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps)
|
|||
|
|
$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) })
|
|||
|
|
$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count
|
|||
|
|
Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched)
|
|||
|
|
$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' })
|
|||
|
|
Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count)
|
|||
|
|
|
|||
|
|
# ---- A6: 1947 密钥报文 ----
|
|||
|
|
$k = @($msgs | Where-Object { $_.Dport -eq 1947 })
|
|||
|
|
if ($k.Count -gt 0) {
|
|||
|
|
$h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload)
|
|||
|
|
$allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0
|
|||
|
|
Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0)
|
|||
|
|
Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌"
|
|||
|
|
} else { Check "A6 1947负载恒定" $false "| 无样本" }
|
|||
|
|
|
|||
|
|
# ---- A7: A·B·B 节奏 ----
|
|||
|
|
$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21)
|
|||
|
|
$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join ''
|
|||
|
|
Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat"
|
|||
|
|
|
|||
|
|
# ---- C1: type=02 主机标识心跳 ----
|
|||
|
|
$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 })
|
|||
|
|
$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' })
|
|||
|
|
Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count)
|
|||
|
|
|
|||
|
|
# ---- C2: EUCE 帧 ----
|
|||
|
|
$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' })
|
|||
|
|
$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 })
|
|||
|
|
Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count)
|
|||
|
|
|
|||
|
|
# ---- 发现阶段 ----
|
|||
|
|
Write-Host "`n=== 载入 $PcapDiscovery ==="
|
|||
|
|
$pkts2 = Read-Pcap $PcapDiscovery
|
|||
|
|
$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } }
|
|||
|
|
$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 })
|
|||
|
|
$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 })
|
|||
|
|
Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count)
|
|||
|
|
$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 })
|
|||
|
|
$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' })
|
|||
|
|
Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count)
|
|||
|
|
$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致"
|
|||
|
|
$hasHost = $false
|
|||
|
|
foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } }
|
|||
|
|
Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在"
|
|||
|
|
|
|||
|
|
Write-Host "`n========================================"
|
|||
|
|
Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail)
|
|||
|
|
if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" }
|
|||
|
|
.Name)x$(# Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2)
|
|||
|
|
param(
|
|||
|
|
[string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap",
|
|||
|
|
[string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap"
|
|||
|
|
)
|
|||
|
|
$ErrorActionPreference = 'Stop'
|
|||
|
|
$script:pass = 0; $script:fail = 0
|
|||
|
|
function Check($name, $cond, $detail) {
|
|||
|
|
if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) }
|
|||
|
|
else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) }
|
|||
|
|
}
|
|||
|
|
function Read-Pcap($path) {
|
|||
|
|
$b = [IO.File]::ReadAllBytes($path)
|
|||
|
|
function U32([byte[]]$x, [int]$o, [bool]$sw) {
|
|||
|
|
if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] }
|
|||
|
|
return [BitConverter]::ToUInt32($x, $o)
|
|||
|
|
}
|
|||
|
|
$magic = U32 $b 0 $false
|
|||
|
|
$swap = ($magic -eq 0xa1b2c3d4)
|
|||
|
|
$pkts = New-Object System.Collections.Generic.List[object]
|
|||
|
|
$o = 24
|
|||
|
|
while ($o + 16 -le $b.Length) {
|
|||
|
|
$incl = U32 $b ($o+8) $swap
|
|||
|
|
if ($o + 16 + $incl -gt $b.Length) { break }
|
|||
|
|
$data = New-Object byte[] $incl
|
|||
|
|
[Array]::Copy($b, $o+16, $data, 0, $incl)
|
|||
|
|
$pkts.Add([pscustomobject]@{ Len = $incl; Data = $data })
|
|||
|
|
$o += 16 + $incl
|
|||
|
|
}
|
|||
|
|
return $pkts
|
|||
|
|
}
|
|||
|
|
function UdpPayload($pkt) {
|
|||
|
|
$d = $pkt.Data
|
|||
|
|
if ($d.Length -lt 42) { return $null }
|
|||
|
|
$ethType = ([uint32]$d[12] -shl 8) -bor $d[13]
|
|||
|
|
$off = 14
|
|||
|
|
if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 }
|
|||
|
|
if ($ethType -ne 0x0800) { return $null }
|
|||
|
|
$ihl = ($d[$off] -band 0x0F) * 4
|
|||
|
|
if ($d[$off + 9] -ne 17) { return $null }
|
|||
|
|
$ipStart = $off; $udp = $off + $ihl
|
|||
|
|
$src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15]
|
|||
|
|
$dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19]
|
|||
|
|
$sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1]
|
|||
|
|
$dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3]
|
|||
|
|
$pl = New-Object byte[] ($d.Length - $udp - 8)
|
|||
|
|
[Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length)
|
|||
|
|
return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl }
|
|||
|
|
}
|
|||
|
|
function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) }
|
|||
|
|
function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) }
|
|||
|
|
function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' }
|
|||
|
|
|
|||
|
|
Write-Host "=== 载入 $Pcap ==="
|
|||
|
|
$pkts = Read-Pcap $Pcap
|
|||
|
|
Write-Host ("总包数: {0}" -f $pkts.Count)
|
|||
|
|
$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } }
|
|||
|
|
|
|||
|
|
# ---- A1: 31772 帧魔数只允许 ENet / EUCE ----
|
|||
|
|
$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 })
|
|||
|
|
$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 }
|
|||
|
|
$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr"
|
|||
|
|
|
|||
|
|
# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ----
|
|||
|
|
$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' })
|
|||
|
|
$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count)
|
|||
|
|
|
|||
|
|
# ---- A3: ENet 帧类型仅 1/2/4 ----
|
|||
|
|
$types = $enet | Group-Object { LE32 $_.Payload 4 }
|
|||
|
|
$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr"
|
|||
|
|
|
|||
|
|
# ---- A4: 形态A DMX 帧 ----
|
|||
|
|
$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 })
|
|||
|
|
Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count)
|
|||
|
|
Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024"
|
|||
|
|
$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) })
|
|||
|
|
$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } }
|
|||
|
|
Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count)
|
|||
|
|
$tsSame = 0
|
|||
|
|
for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } }
|
|||
|
|
Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0"
|
|||
|
|
|
|||
|
|
# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ----
|
|||
|
|
$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' })
|
|||
|
|
$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' })
|
|||
|
|
Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count)
|
|||
|
|
$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) })
|
|||
|
|
$hbMono = $true; $swaps = 0
|
|||
|
|
for ($i=1; $i -lt $hostSeq.Count; $i++) {
|
|||
|
|
if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序)
|
|||
|
|
elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败
|
|||
|
|
}
|
|||
|
|
Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps)
|
|||
|
|
$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) })
|
|||
|
|
$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count
|
|||
|
|
Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched)
|
|||
|
|
$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' })
|
|||
|
|
Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count)
|
|||
|
|
|
|||
|
|
# ---- A6: 1947 密钥报文 ----
|
|||
|
|
$k = @($msgs | Where-Object { $_.Dport -eq 1947 })
|
|||
|
|
if ($k.Count -gt 0) {
|
|||
|
|
$h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload)
|
|||
|
|
$allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0
|
|||
|
|
Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0)
|
|||
|
|
Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌"
|
|||
|
|
} else { Check "A6 1947负载恒定" $false "| 无样本" }
|
|||
|
|
|
|||
|
|
# ---- A7: A·B·B 节奏 ----
|
|||
|
|
$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21)
|
|||
|
|
$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join ''
|
|||
|
|
Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat"
|
|||
|
|
|
|||
|
|
# ---- C1: type=02 主机标识心跳 ----
|
|||
|
|
$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 })
|
|||
|
|
$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' })
|
|||
|
|
Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count)
|
|||
|
|
|
|||
|
|
# ---- C2: EUCE 帧 ----
|
|||
|
|
$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' })
|
|||
|
|
$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 })
|
|||
|
|
Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count)
|
|||
|
|
|
|||
|
|
# ---- 发现阶段 ----
|
|||
|
|
Write-Host "`n=== 载入 $PcapDiscovery ==="
|
|||
|
|
$pkts2 = Read-Pcap $PcapDiscovery
|
|||
|
|
$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } }
|
|||
|
|
$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 })
|
|||
|
|
$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 })
|
|||
|
|
Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count)
|
|||
|
|
$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 })
|
|||
|
|
$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' })
|
|||
|
|
Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count)
|
|||
|
|
$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致"
|
|||
|
|
$hasHost = $false
|
|||
|
|
foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } }
|
|||
|
|
Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在"
|
|||
|
|
|
|||
|
|
Write-Host "`n========================================"
|
|||
|
|
Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail)
|
|||
|
|
if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" }
|
|||
|
|
.Count)" }) -join ','))
|
|||
|
|
$badRdmLen = @($rdm | Where-Object { [int](LE16 # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2)
|
|||
|
|
param(
|
|||
|
|
[string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap",
|
|||
|
|
[string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap"
|
|||
|
|
)
|
|||
|
|
$ErrorActionPreference = 'Stop'
|
|||
|
|
$script:pass = 0; $script:fail = 0
|
|||
|
|
function Check($name, $cond, $detail) {
|
|||
|
|
if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) }
|
|||
|
|
else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) }
|
|||
|
|
}
|
|||
|
|
function Read-Pcap($path) {
|
|||
|
|
$b = [IO.File]::ReadAllBytes($path)
|
|||
|
|
function U32([byte[]]$x, [int]$o, [bool]$sw) {
|
|||
|
|
if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] }
|
|||
|
|
return [BitConverter]::ToUInt32($x, $o)
|
|||
|
|
}
|
|||
|
|
$magic = U32 $b 0 $false
|
|||
|
|
$swap = ($magic -eq 0xa1b2c3d4)
|
|||
|
|
$pkts = New-Object System.Collections.Generic.List[object]
|
|||
|
|
$o = 24
|
|||
|
|
while ($o + 16 -le $b.Length) {
|
|||
|
|
$incl = U32 $b ($o+8) $swap
|
|||
|
|
if ($o + 16 + $incl -gt $b.Length) { break }
|
|||
|
|
$data = New-Object byte[] $incl
|
|||
|
|
[Array]::Copy($b, $o+16, $data, 0, $incl)
|
|||
|
|
$pkts.Add([pscustomobject]@{ Len = $incl; Data = $data })
|
|||
|
|
$o += 16 + $incl
|
|||
|
|
}
|
|||
|
|
return $pkts
|
|||
|
|
}
|
|||
|
|
function UdpPayload($pkt) {
|
|||
|
|
$d = $pkt.Data
|
|||
|
|
if ($d.Length -lt 42) { return $null }
|
|||
|
|
$ethType = ([uint32]$d[12] -shl 8) -bor $d[13]
|
|||
|
|
$off = 14
|
|||
|
|
if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 }
|
|||
|
|
if ($ethType -ne 0x0800) { return $null }
|
|||
|
|
$ihl = ($d[$off] -band 0x0F) * 4
|
|||
|
|
if ($d[$off + 9] -ne 17) { return $null }
|
|||
|
|
$ipStart = $off; $udp = $off + $ihl
|
|||
|
|
$src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15]
|
|||
|
|
$dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19]
|
|||
|
|
$sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1]
|
|||
|
|
$dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3]
|
|||
|
|
$pl = New-Object byte[] ($d.Length - $udp - 8)
|
|||
|
|
[Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length)
|
|||
|
|
return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl }
|
|||
|
|
}
|
|||
|
|
function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) }
|
|||
|
|
function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) }
|
|||
|
|
function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' }
|
|||
|
|
|
|||
|
|
Write-Host "=== 载入 $Pcap ==="
|
|||
|
|
$pkts = Read-Pcap $Pcap
|
|||
|
|
Write-Host ("总包数: {0}" -f $pkts.Count)
|
|||
|
|
$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } }
|
|||
|
|
|
|||
|
|
# ---- A1: 31772 帧魔数只允许 ENet / EUCE ----
|
|||
|
|
$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 })
|
|||
|
|
$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 }
|
|||
|
|
$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr"
|
|||
|
|
|
|||
|
|
# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ----
|
|||
|
|
$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' })
|
|||
|
|
$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count)
|
|||
|
|
|
|||
|
|
# ---- A3: ENet 帧类型仅 1/2/4 ----
|
|||
|
|
$types = $enet | Group-Object { LE32 $_.Payload 4 }
|
|||
|
|
$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr"
|
|||
|
|
|
|||
|
|
# ---- A4: 形态A DMX 帧 ----
|
|||
|
|
$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 })
|
|||
|
|
Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count)
|
|||
|
|
Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024"
|
|||
|
|
$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) })
|
|||
|
|
$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } }
|
|||
|
|
Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count)
|
|||
|
|
$tsSame = 0
|
|||
|
|
for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } }
|
|||
|
|
Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0"
|
|||
|
|
|
|||
|
|
# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ----
|
|||
|
|
$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' })
|
|||
|
|
$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' })
|
|||
|
|
Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count)
|
|||
|
|
$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) })
|
|||
|
|
$hbMono = $true; $swaps = 0
|
|||
|
|
for ($i=1; $i -lt $hostSeq.Count; $i++) {
|
|||
|
|
if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序)
|
|||
|
|
elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败
|
|||
|
|
}
|
|||
|
|
Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps)
|
|||
|
|
$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) })
|
|||
|
|
$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count
|
|||
|
|
Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched)
|
|||
|
|
$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' })
|
|||
|
|
Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count)
|
|||
|
|
|
|||
|
|
# ---- A6: 1947 密钥报文 ----
|
|||
|
|
$k = @($msgs | Where-Object { $_.Dport -eq 1947 })
|
|||
|
|
if ($k.Count -gt 0) {
|
|||
|
|
$h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload)
|
|||
|
|
$allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0
|
|||
|
|
Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0)
|
|||
|
|
Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌"
|
|||
|
|
} else { Check "A6 1947负载恒定" $false "| 无样本" }
|
|||
|
|
|
|||
|
|
# ---- A7: A·B·B 节奏 ----
|
|||
|
|
$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21)
|
|||
|
|
$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join ''
|
|||
|
|
Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat"
|
|||
|
|
|
|||
|
|
# ---- C1: type=02 主机标识心跳 ----
|
|||
|
|
$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 })
|
|||
|
|
$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' })
|
|||
|
|
Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count)
|
|||
|
|
|
|||
|
|
# ---- C2: EUCE 帧 ----
|
|||
|
|
$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' })
|
|||
|
|
$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 })
|
|||
|
|
Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count)
|
|||
|
|
|
|||
|
|
# ---- 发现阶段 ----
|
|||
|
|
Write-Host "`n=== 载入 $PcapDiscovery ==="
|
|||
|
|
$pkts2 = Read-Pcap $PcapDiscovery
|
|||
|
|
$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } }
|
|||
|
|
$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 })
|
|||
|
|
$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 })
|
|||
|
|
Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count)
|
|||
|
|
$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 })
|
|||
|
|
$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' })
|
|||
|
|
Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count)
|
|||
|
|
$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致"
|
|||
|
|
$hasHost = $false
|
|||
|
|
foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } }
|
|||
|
|
Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在"
|
|||
|
|
|
|||
|
|
Write-Host "`n========================================"
|
|||
|
|
Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail)
|
|||
|
|
if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" }
|
|||
|
|
.Payload 0x28) -ne (# Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2)
|
|||
|
|
param(
|
|||
|
|
[string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap",
|
|||
|
|
[string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap"
|
|||
|
|
)
|
|||
|
|
$ErrorActionPreference = 'Stop'
|
|||
|
|
$script:pass = 0; $script:fail = 0
|
|||
|
|
function Check($name, $cond, $detail) {
|
|||
|
|
if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) }
|
|||
|
|
else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) }
|
|||
|
|
}
|
|||
|
|
function Read-Pcap($path) {
|
|||
|
|
$b = [IO.File]::ReadAllBytes($path)
|
|||
|
|
function U32([byte[]]$x, [int]$o, [bool]$sw) {
|
|||
|
|
if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] }
|
|||
|
|
return [BitConverter]::ToUInt32($x, $o)
|
|||
|
|
}
|
|||
|
|
$magic = U32 $b 0 $false
|
|||
|
|
$swap = ($magic -eq 0xa1b2c3d4)
|
|||
|
|
$pkts = New-Object System.Collections.Generic.List[object]
|
|||
|
|
$o = 24
|
|||
|
|
while ($o + 16 -le $b.Length) {
|
|||
|
|
$incl = U32 $b ($o+8) $swap
|
|||
|
|
if ($o + 16 + $incl -gt $b.Length) { break }
|
|||
|
|
$data = New-Object byte[] $incl
|
|||
|
|
[Array]::Copy($b, $o+16, $data, 0, $incl)
|
|||
|
|
$pkts.Add([pscustomobject]@{ Len = $incl; Data = $data })
|
|||
|
|
$o += 16 + $incl
|
|||
|
|
}
|
|||
|
|
return $pkts
|
|||
|
|
}
|
|||
|
|
function UdpPayload($pkt) {
|
|||
|
|
$d = $pkt.Data
|
|||
|
|
if ($d.Length -lt 42) { return $null }
|
|||
|
|
$ethType = ([uint32]$d[12] -shl 8) -bor $d[13]
|
|||
|
|
$off = 14
|
|||
|
|
if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 }
|
|||
|
|
if ($ethType -ne 0x0800) { return $null }
|
|||
|
|
$ihl = ($d[$off] -band 0x0F) * 4
|
|||
|
|
if ($d[$off + 9] -ne 17) { return $null }
|
|||
|
|
$ipStart = $off; $udp = $off + $ihl
|
|||
|
|
$src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15]
|
|||
|
|
$dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19]
|
|||
|
|
$sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1]
|
|||
|
|
$dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3]
|
|||
|
|
$pl = New-Object byte[] ($d.Length - $udp - 8)
|
|||
|
|
[Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length)
|
|||
|
|
return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl }
|
|||
|
|
}
|
|||
|
|
function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) }
|
|||
|
|
function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) }
|
|||
|
|
function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' }
|
|||
|
|
|
|||
|
|
Write-Host "=== 载入 $Pcap ==="
|
|||
|
|
$pkts = Read-Pcap $Pcap
|
|||
|
|
Write-Host ("总包数: {0}" -f $pkts.Count)
|
|||
|
|
$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } }
|
|||
|
|
|
|||
|
|
# ---- A1: 31772 帧魔数只允许 ENet / EUCE ----
|
|||
|
|
$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 })
|
|||
|
|
$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 }
|
|||
|
|
$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr"
|
|||
|
|
|
|||
|
|
# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ----
|
|||
|
|
$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' })
|
|||
|
|
$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count)
|
|||
|
|
|
|||
|
|
# ---- A3: ENet 帧类型仅 1/2/4 ----
|
|||
|
|
$types = $enet | Group-Object { LE32 $_.Payload 4 }
|
|||
|
|
$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr"
|
|||
|
|
|
|||
|
|
# ---- A4: 形态A DMX 帧 ----
|
|||
|
|
$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 })
|
|||
|
|
Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count)
|
|||
|
|
Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024"
|
|||
|
|
$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) })
|
|||
|
|
$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } }
|
|||
|
|
Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count)
|
|||
|
|
$tsSame = 0
|
|||
|
|
for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } }
|
|||
|
|
Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0"
|
|||
|
|
|
|||
|
|
# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ----
|
|||
|
|
$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' })
|
|||
|
|
$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' })
|
|||
|
|
Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count)
|
|||
|
|
$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) })
|
|||
|
|
$hbMono = $true; $swaps = 0
|
|||
|
|
for ($i=1; $i -lt $hostSeq.Count; $i++) {
|
|||
|
|
if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序)
|
|||
|
|
elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败
|
|||
|
|
}
|
|||
|
|
Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps)
|
|||
|
|
$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) })
|
|||
|
|
$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count
|
|||
|
|
Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched)
|
|||
|
|
$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' })
|
|||
|
|
Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count)
|
|||
|
|
|
|||
|
|
# ---- A6: 1947 密钥报文 ----
|
|||
|
|
$k = @($msgs | Where-Object { $_.Dport -eq 1947 })
|
|||
|
|
if ($k.Count -gt 0) {
|
|||
|
|
$h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload)
|
|||
|
|
$allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0
|
|||
|
|
Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0)
|
|||
|
|
Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌"
|
|||
|
|
} else { Check "A6 1947负载恒定" $false "| 无样本" }
|
|||
|
|
|
|||
|
|
# ---- A7: A·B·B 节奏 ----
|
|||
|
|
$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21)
|
|||
|
|
$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join ''
|
|||
|
|
Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat"
|
|||
|
|
|
|||
|
|
# ---- C1: type=02 主机标识心跳 ----
|
|||
|
|
$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 })
|
|||
|
|
$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' })
|
|||
|
|
Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count)
|
|||
|
|
|
|||
|
|
# ---- C2: EUCE 帧 ----
|
|||
|
|
$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' })
|
|||
|
|
$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 })
|
|||
|
|
Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count)
|
|||
|
|
|
|||
|
|
# ---- 发现阶段 ----
|
|||
|
|
Write-Host "`n=== 载入 $PcapDiscovery ==="
|
|||
|
|
$pkts2 = Read-Pcap $PcapDiscovery
|
|||
|
|
$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } }
|
|||
|
|
$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 })
|
|||
|
|
$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 })
|
|||
|
|
Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count)
|
|||
|
|
$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 })
|
|||
|
|
$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' })
|
|||
|
|
Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count)
|
|||
|
|
$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致"
|
|||
|
|
$hasHost = $false
|
|||
|
|
foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } }
|
|||
|
|
Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在"
|
|||
|
|
|
|||
|
|
Write-Host "`n========================================"
|
|||
|
|
Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail)
|
|||
|
|
if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" }
|
|||
|
|
.Payload.Length - 0x2A) })
|
|||
|
|
Check "R2 RDM长度字段0x28" ($badRdmLen.Count -eq 0) ("| {0}/{1} 一致" -f ($rdm.Count - $badRdmLen.Count), $rdm.Count)
|
|||
|
|
$uids = @($rdm | ForEach-Object { (Hex # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2)
|
|||
|
|
param(
|
|||
|
|
[string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap",
|
|||
|
|
[string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap"
|
|||
|
|
)
|
|||
|
|
$ErrorActionPreference = 'Stop'
|
|||
|
|
$script:pass = 0; $script:fail = 0
|
|||
|
|
function Check($name, $cond, $detail) {
|
|||
|
|
if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) }
|
|||
|
|
else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) }
|
|||
|
|
}
|
|||
|
|
function Read-Pcap($path) {
|
|||
|
|
$b = [IO.File]::ReadAllBytes($path)
|
|||
|
|
function U32([byte[]]$x, [int]$o, [bool]$sw) {
|
|||
|
|
if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] }
|
|||
|
|
return [BitConverter]::ToUInt32($x, $o)
|
|||
|
|
}
|
|||
|
|
$magic = U32 $b 0 $false
|
|||
|
|
$swap = ($magic -eq 0xa1b2c3d4)
|
|||
|
|
$pkts = New-Object System.Collections.Generic.List[object]
|
|||
|
|
$o = 24
|
|||
|
|
while ($o + 16 -le $b.Length) {
|
|||
|
|
$incl = U32 $b ($o+8) $swap
|
|||
|
|
if ($o + 16 + $incl -gt $b.Length) { break }
|
|||
|
|
$data = New-Object byte[] $incl
|
|||
|
|
[Array]::Copy($b, $o+16, $data, 0, $incl)
|
|||
|
|
$pkts.Add([pscustomobject]@{ Len = $incl; Data = $data })
|
|||
|
|
$o += 16 + $incl
|
|||
|
|
}
|
|||
|
|
return $pkts
|
|||
|
|
}
|
|||
|
|
function UdpPayload($pkt) {
|
|||
|
|
$d = $pkt.Data
|
|||
|
|
if ($d.Length -lt 42) { return $null }
|
|||
|
|
$ethType = ([uint32]$d[12] -shl 8) -bor $d[13]
|
|||
|
|
$off = 14
|
|||
|
|
if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 }
|
|||
|
|
if ($ethType -ne 0x0800) { return $null }
|
|||
|
|
$ihl = ($d[$off] -band 0x0F) * 4
|
|||
|
|
if ($d[$off + 9] -ne 17) { return $null }
|
|||
|
|
$ipStart = $off; $udp = $off + $ihl
|
|||
|
|
$src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15]
|
|||
|
|
$dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19]
|
|||
|
|
$sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1]
|
|||
|
|
$dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3]
|
|||
|
|
$pl = New-Object byte[] ($d.Length - $udp - 8)
|
|||
|
|
[Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length)
|
|||
|
|
return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl }
|
|||
|
|
}
|
|||
|
|
function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) }
|
|||
|
|
function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) }
|
|||
|
|
function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' }
|
|||
|
|
|
|||
|
|
Write-Host "=== 载入 $Pcap ==="
|
|||
|
|
$pkts = Read-Pcap $Pcap
|
|||
|
|
Write-Host ("总包数: {0}" -f $pkts.Count)
|
|||
|
|
$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } }
|
|||
|
|
|
|||
|
|
# ---- A1: 31772 帧魔数只允许 ENet / EUCE ----
|
|||
|
|
$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 })
|
|||
|
|
$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 }
|
|||
|
|
$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr"
|
|||
|
|
|
|||
|
|
# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ----
|
|||
|
|
$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' })
|
|||
|
|
$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count)
|
|||
|
|
|
|||
|
|
# ---- A3: ENet 帧类型仅 1/2/4 ----
|
|||
|
|
$types = $enet | Group-Object { LE32 $_.Payload 4 }
|
|||
|
|
$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr"
|
|||
|
|
|
|||
|
|
# ---- A4: 形态A DMX 帧 ----
|
|||
|
|
$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 })
|
|||
|
|
Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count)
|
|||
|
|
Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024"
|
|||
|
|
$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) })
|
|||
|
|
$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } }
|
|||
|
|
Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count)
|
|||
|
|
$tsSame = 0
|
|||
|
|
for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } }
|
|||
|
|
Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0"
|
|||
|
|
|
|||
|
|
# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ----
|
|||
|
|
$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' })
|
|||
|
|
$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' })
|
|||
|
|
Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count)
|
|||
|
|
$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) })
|
|||
|
|
$hbMono = $true; $swaps = 0
|
|||
|
|
for ($i=1; $i -lt $hostSeq.Count; $i++) {
|
|||
|
|
if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序)
|
|||
|
|
elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败
|
|||
|
|
}
|
|||
|
|
Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps)
|
|||
|
|
$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) })
|
|||
|
|
$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count
|
|||
|
|
Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched)
|
|||
|
|
$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' })
|
|||
|
|
Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count)
|
|||
|
|
|
|||
|
|
# ---- A6: 1947 密钥报文 ----
|
|||
|
|
$k = @($msgs | Where-Object { $_.Dport -eq 1947 })
|
|||
|
|
if ($k.Count -gt 0) {
|
|||
|
|
$h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload)
|
|||
|
|
$allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0
|
|||
|
|
Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0)
|
|||
|
|
Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌"
|
|||
|
|
} else { Check "A6 1947负载恒定" $false "| 无样本" }
|
|||
|
|
|
|||
|
|
# ---- A7: A·B·B 节奏 ----
|
|||
|
|
$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21)
|
|||
|
|
$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join ''
|
|||
|
|
Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat"
|
|||
|
|
|
|||
|
|
# ---- C1: type=02 主机标识心跳 ----
|
|||
|
|
$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 })
|
|||
|
|
$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' })
|
|||
|
|
Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count)
|
|||
|
|
|
|||
|
|
# ---- C2: EUCE 帧 ----
|
|||
|
|
$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' })
|
|||
|
|
$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 })
|
|||
|
|
Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count)
|
|||
|
|
|
|||
|
|
# ---- 发现阶段 ----
|
|||
|
|
Write-Host "`n=== 载入 $PcapDiscovery ==="
|
|||
|
|
$pkts2 = Read-Pcap $PcapDiscovery
|
|||
|
|
$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } }
|
|||
|
|
$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 })
|
|||
|
|
$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 })
|
|||
|
|
Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count)
|
|||
|
|
$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 })
|
|||
|
|
$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' })
|
|||
|
|
Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count)
|
|||
|
|
$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致"
|
|||
|
|
$hasHost = $false
|
|||
|
|
foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } }
|
|||
|
|
Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在"
|
|||
|
|
|
|||
|
|
Write-Host "`n========================================"
|
|||
|
|
Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail)
|
|||
|
|
if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" }
|
|||
|
|
.Payload 0x32 6) } | Sort-Object -Unique)
|
|||
|
|
Check "R3 灯具UID(Src@0x32)" ($uids.Count -ge 1) ("| UID: {0}" -f ($uids -join ' '))
|
|||
|
|
$disc = @($msgs3 | Where-Object { # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2)
|
|||
|
|
param(
|
|||
|
|
[string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap",
|
|||
|
|
[string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap"
|
|||
|
|
)
|
|||
|
|
$ErrorActionPreference = 'Stop'
|
|||
|
|
$script:pass = 0; $script:fail = 0
|
|||
|
|
function Check($name, $cond, $detail) {
|
|||
|
|
if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) }
|
|||
|
|
else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) }
|
|||
|
|
}
|
|||
|
|
function Read-Pcap($path) {
|
|||
|
|
$b = [IO.File]::ReadAllBytes($path)
|
|||
|
|
function U32([byte[]]$x, [int]$o, [bool]$sw) {
|
|||
|
|
if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] }
|
|||
|
|
return [BitConverter]::ToUInt32($x, $o)
|
|||
|
|
}
|
|||
|
|
$magic = U32 $b 0 $false
|
|||
|
|
$swap = ($magic -eq 0xa1b2c3d4)
|
|||
|
|
$pkts = New-Object System.Collections.Generic.List[object]
|
|||
|
|
$o = 24
|
|||
|
|
while ($o + 16 -le $b.Length) {
|
|||
|
|
$incl = U32 $b ($o+8) $swap
|
|||
|
|
if ($o + 16 + $incl -gt $b.Length) { break }
|
|||
|
|
$data = New-Object byte[] $incl
|
|||
|
|
[Array]::Copy($b, $o+16, $data, 0, $incl)
|
|||
|
|
$pkts.Add([pscustomobject]@{ Len = $incl; Data = $data })
|
|||
|
|
$o += 16 + $incl
|
|||
|
|
}
|
|||
|
|
return $pkts
|
|||
|
|
}
|
|||
|
|
function UdpPayload($pkt) {
|
|||
|
|
$d = $pkt.Data
|
|||
|
|
if ($d.Length -lt 42) { return $null }
|
|||
|
|
$ethType = ([uint32]$d[12] -shl 8) -bor $d[13]
|
|||
|
|
$off = 14
|
|||
|
|
if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 }
|
|||
|
|
if ($ethType -ne 0x0800) { return $null }
|
|||
|
|
$ihl = ($d[$off] -band 0x0F) * 4
|
|||
|
|
if ($d[$off + 9] -ne 17) { return $null }
|
|||
|
|
$ipStart = $off; $udp = $off + $ihl
|
|||
|
|
$src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15]
|
|||
|
|
$dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19]
|
|||
|
|
$sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1]
|
|||
|
|
$dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3]
|
|||
|
|
$pl = New-Object byte[] ($d.Length - $udp - 8)
|
|||
|
|
[Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length)
|
|||
|
|
return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl }
|
|||
|
|
}
|
|||
|
|
function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) }
|
|||
|
|
function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) }
|
|||
|
|
function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' }
|
|||
|
|
|
|||
|
|
Write-Host "=== 载入 $Pcap ==="
|
|||
|
|
$pkts = Read-Pcap $Pcap
|
|||
|
|
Write-Host ("总包数: {0}" -f $pkts.Count)
|
|||
|
|
$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } }
|
|||
|
|
|
|||
|
|
# ---- A1: 31772 帧魔数只允许 ENet / EUCE ----
|
|||
|
|
$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 })
|
|||
|
|
$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 }
|
|||
|
|
$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr"
|
|||
|
|
|
|||
|
|
# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ----
|
|||
|
|
$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' })
|
|||
|
|
$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count)
|
|||
|
|
|
|||
|
|
# ---- A3: ENet 帧类型仅 1/2/4 ----
|
|||
|
|
$types = $enet | Group-Object { LE32 $_.Payload 4 }
|
|||
|
|
$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr"
|
|||
|
|
|
|||
|
|
# ---- A4: 形态A DMX 帧 ----
|
|||
|
|
$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 })
|
|||
|
|
Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count)
|
|||
|
|
Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024"
|
|||
|
|
$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) })
|
|||
|
|
$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } }
|
|||
|
|
Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count)
|
|||
|
|
$tsSame = 0
|
|||
|
|
for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } }
|
|||
|
|
Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0"
|
|||
|
|
|
|||
|
|
# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ----
|
|||
|
|
$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' })
|
|||
|
|
$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' })
|
|||
|
|
Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count)
|
|||
|
|
$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) })
|
|||
|
|
$hbMono = $true; $swaps = 0
|
|||
|
|
for ($i=1; $i -lt $hostSeq.Count; $i++) {
|
|||
|
|
if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序)
|
|||
|
|
elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败
|
|||
|
|
}
|
|||
|
|
Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps)
|
|||
|
|
$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) })
|
|||
|
|
$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count
|
|||
|
|
Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched)
|
|||
|
|
$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' })
|
|||
|
|
Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count)
|
|||
|
|
|
|||
|
|
# ---- A6: 1947 密钥报文 ----
|
|||
|
|
$k = @($msgs | Where-Object { $_.Dport -eq 1947 })
|
|||
|
|
if ($k.Count -gt 0) {
|
|||
|
|
$h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload)
|
|||
|
|
$allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0
|
|||
|
|
Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0)
|
|||
|
|
Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌"
|
|||
|
|
} else { Check "A6 1947负载恒定" $false "| 无样本" }
|
|||
|
|
|
|||
|
|
# ---- A7: A·B·B 节奏 ----
|
|||
|
|
$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21)
|
|||
|
|
$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join ''
|
|||
|
|
Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat"
|
|||
|
|
|
|||
|
|
# ---- C1: type=02 主机标识心跳 ----
|
|||
|
|
$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 })
|
|||
|
|
$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' })
|
|||
|
|
Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count)
|
|||
|
|
|
|||
|
|
# ---- C2: EUCE 帧 ----
|
|||
|
|
$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' })
|
|||
|
|
$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 })
|
|||
|
|
Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count)
|
|||
|
|
|
|||
|
|
# ---- 发现阶段 ----
|
|||
|
|
Write-Host "`n=== 载入 $PcapDiscovery ==="
|
|||
|
|
$pkts2 = Read-Pcap $PcapDiscovery
|
|||
|
|
$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } }
|
|||
|
|
$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 })
|
|||
|
|
$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 })
|
|||
|
|
Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count)
|
|||
|
|
$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 })
|
|||
|
|
$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' })
|
|||
|
|
Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count)
|
|||
|
|
$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致"
|
|||
|
|
$hasHost = $false
|
|||
|
|
foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } }
|
|||
|
|
Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在"
|
|||
|
|
|
|||
|
|
Write-Host "`n========================================"
|
|||
|
|
Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail)
|
|||
|
|
if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" }
|
|||
|
|
.Src -eq '192.168.123.1' -and (Hex # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2)
|
|||
|
|
param(
|
|||
|
|
[string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap",
|
|||
|
|
[string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap"
|
|||
|
|
)
|
|||
|
|
$ErrorActionPreference = 'Stop'
|
|||
|
|
$script:pass = 0; $script:fail = 0
|
|||
|
|
function Check($name, $cond, $detail) {
|
|||
|
|
if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) }
|
|||
|
|
else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) }
|
|||
|
|
}
|
|||
|
|
function Read-Pcap($path) {
|
|||
|
|
$b = [IO.File]::ReadAllBytes($path)
|
|||
|
|
function U32([byte[]]$x, [int]$o, [bool]$sw) {
|
|||
|
|
if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] }
|
|||
|
|
return [BitConverter]::ToUInt32($x, $o)
|
|||
|
|
}
|
|||
|
|
$magic = U32 $b 0 $false
|
|||
|
|
$swap = ($magic -eq 0xa1b2c3d4)
|
|||
|
|
$pkts = New-Object System.Collections.Generic.List[object]
|
|||
|
|
$o = 24
|
|||
|
|
while ($o + 16 -le $b.Length) {
|
|||
|
|
$incl = U32 $b ($o+8) $swap
|
|||
|
|
if ($o + 16 + $incl -gt $b.Length) { break }
|
|||
|
|
$data = New-Object byte[] $incl
|
|||
|
|
[Array]::Copy($b, $o+16, $data, 0, $incl)
|
|||
|
|
$pkts.Add([pscustomobject]@{ Len = $incl; Data = $data })
|
|||
|
|
$o += 16 + $incl
|
|||
|
|
}
|
|||
|
|
return $pkts
|
|||
|
|
}
|
|||
|
|
function UdpPayload($pkt) {
|
|||
|
|
$d = $pkt.Data
|
|||
|
|
if ($d.Length -lt 42) { return $null }
|
|||
|
|
$ethType = ([uint32]$d[12] -shl 8) -bor $d[13]
|
|||
|
|
$off = 14
|
|||
|
|
if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 }
|
|||
|
|
if ($ethType -ne 0x0800) { return $null }
|
|||
|
|
$ihl = ($d[$off] -band 0x0F) * 4
|
|||
|
|
if ($d[$off + 9] -ne 17) { return $null }
|
|||
|
|
$ipStart = $off; $udp = $off + $ihl
|
|||
|
|
$src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15]
|
|||
|
|
$dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19]
|
|||
|
|
$sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1]
|
|||
|
|
$dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3]
|
|||
|
|
$pl = New-Object byte[] ($d.Length - $udp - 8)
|
|||
|
|
[Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length)
|
|||
|
|
return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl }
|
|||
|
|
}
|
|||
|
|
function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) }
|
|||
|
|
function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) }
|
|||
|
|
function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' }
|
|||
|
|
|
|||
|
|
Write-Host "=== 载入 $Pcap ==="
|
|||
|
|
$pkts = Read-Pcap $Pcap
|
|||
|
|
Write-Host ("总包数: {0}" -f $pkts.Count)
|
|||
|
|
$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } }
|
|||
|
|
|
|||
|
|
# ---- A1: 31772 帧魔数只允许 ENet / EUCE ----
|
|||
|
|
$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 })
|
|||
|
|
$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 }
|
|||
|
|
$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr"
|
|||
|
|
|
|||
|
|
# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ----
|
|||
|
|
$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' })
|
|||
|
|
$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count)
|
|||
|
|
|
|||
|
|
# ---- A3: ENet 帧类型仅 1/2/4 ----
|
|||
|
|
$types = $enet | Group-Object { LE32 $_.Payload 4 }
|
|||
|
|
$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' '
|
|||
|
|
Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr"
|
|||
|
|
|
|||
|
|
# ---- A4: 形态A DMX 帧 ----
|
|||
|
|
$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 })
|
|||
|
|
Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count)
|
|||
|
|
Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024"
|
|||
|
|
$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) })
|
|||
|
|
$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } }
|
|||
|
|
Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count)
|
|||
|
|
$tsSame = 0
|
|||
|
|
for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } }
|
|||
|
|
Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0"
|
|||
|
|
|
|||
|
|
# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ----
|
|||
|
|
$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' })
|
|||
|
|
$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' })
|
|||
|
|
Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count)
|
|||
|
|
$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) })
|
|||
|
|
$hbMono = $true; $swaps = 0
|
|||
|
|
for ($i=1; $i -lt $hostSeq.Count; $i++) {
|
|||
|
|
if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序)
|
|||
|
|
elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败
|
|||
|
|
}
|
|||
|
|
Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps)
|
|||
|
|
$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) })
|
|||
|
|
$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count
|
|||
|
|
Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched)
|
|||
|
|
$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' })
|
|||
|
|
Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count)
|
|||
|
|
|
|||
|
|
# ---- A6: 1947 密钥报文 ----
|
|||
|
|
$k = @($msgs | Where-Object { $_.Dport -eq 1947 })
|
|||
|
|
if ($k.Count -gt 0) {
|
|||
|
|
$h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload)
|
|||
|
|
$allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0
|
|||
|
|
Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0)
|
|||
|
|
Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌"
|
|||
|
|
} else { Check "A6 1947负载恒定" $false "| 无样本" }
|
|||
|
|
|
|||
|
|
# ---- A7: A·B·B 节奏 ----
|
|||
|
|
$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21)
|
|||
|
|
$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join ''
|
|||
|
|
Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat"
|
|||
|
|
|
|||
|
|
# ---- C1: type=02 主机标识心跳 ----
|
|||
|
|
$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 })
|
|||
|
|
$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' })
|
|||
|
|
Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count)
|
|||
|
|
|
|||
|
|
# ---- C2: EUCE 帧 ----
|
|||
|
|
$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' })
|
|||
|
|
$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 })
|
|||
|
|
Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count)
|
|||
|
|
|
|||
|
|
# ---- 发现阶段 ----
|
|||
|
|
Write-Host "`n=== 载入 $PcapDiscovery ==="
|
|||
|
|
$pkts2 = Read-Pcap $PcapDiscovery
|
|||
|
|
$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } }
|
|||
|
|
$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 })
|
|||
|
|
$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 })
|
|||
|
|
Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count)
|
|||
|
|
$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 })
|
|||
|
|
$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' })
|
|||
|
|
Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count)
|
|||
|
|
$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length })
|
|||
|
|
Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致"
|
|||
|
|
$hasHost = $false
|
|||
|
|
foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } }
|
|||
|
|
Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在"
|
|||
|
|
|
|||
|
|
Write-Host "`n========================================"
|
|||
|
|
Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail)
|
|||
|
|
if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" }
|
|||
|
|
.Payload 0x2A 4) -eq 'aaaa5dbb' })
|
|||
|
|
Check "R4 DISC发现响应脉冲" ($disc.Count -gt 0) ("| n={0}" -f $disc.Count)
|
|||
|
|
} else { Check "R1 RDM帧" $false "| rdm3.pcap 不存在" }
|
|||
|
|
Write-Host "
|
|||
|
|
========================================"
|
|||
|
|
Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail)
|
|||
|
|
if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" }
|