ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

GOOSE报文逐字节拆解:ASN.1 TLV解析与排错指南

GOOSE报文逐字节拆解:ASN.1 TLV解析与排错指南 简介这是一份聚焦GOOSE报文解析的PDF技术资料面向电力系统通信、智能变电站及IEC 61850相关开发与调试人员。内容围绕ISO/IEC 8802-3帧格式展开系统讲解普通报文与广播报文的结构组成覆盖目的MAC、源MAC、TPID、以太网类型0x88B8、APPID、APDU长度等关键字段并结合ASN.1的BER编码形式详细说明Tag、Length、Value的解析方法同时给出BOOL型、BIT-String型、UTC型、INT型、Unsigned型等多种数据类型的标记与解码示例。资料中还配有具体报文十六进制逐字节分析帮助读者直观理解APDU Head结构、allData数据集合及各字段含义便于实际抓包与报文排查。资源为单份PDF文档压缩包大小约121KB内容精炼、结构清晰适合有一定GOOSE基础或正在学习IEC 61850报文细节的工程师查阅。该资源已有1034人学习可作为日常开发、测试及故障分析时的快速参考手册。1. 为什么抓 GOOSE 包比抓普通 TCP 包更容易懵在变电站自动化项目的联调现场最常见的场景是用 Wireshark 或 tcpdump 抓到一帧目的 MAC 是01 0C CD 01 xx xx、以太网类型是0x88B8的报文数据区看起来是一堆十六进制字节但按普通协议“字节偏移 固定长度”去切很快就会错位。这不是你抓包姿势不对而是 GOOSE 报文本身就是用 ASN.1 BER 编码的 TLV 流字段不是按位置排的而是按 Tag 识别的。真正调过规约转换网关或者写过继保测试仪的人都有体会GOOSE 的解析难点不在抓包而在把61 81 xx这段 APDU Head 拆开之后怎么把stNum、sqNum、allData这类字段从 TLV 里准确提出来。本文用三份不同智能站的实际报文从帧头拆到数据集合给出可以直接落地的解析方法和参数判断经验。适合做 IEC 61850 二次设备调试、电网监控后台、规约网关开发的技术人员。2. 802.3 帧上的 GOOSE 封包结构与 VLAN 细节2.1 先看基础普通报文与广播报文的差异GOOSE 报文基于 ISO/IEC 8802-3 的以太网帧格式不经过 IP 层直接以二层组播或广播方式传输。普通报文的结构依次是目的 MAC、源 MAC、可选 TPID/TCI、以太网类型、APPID、APDU 长度、保留位、APDU。其中括号内的 TPID/TCI 不是必需的但强烈建议在以太网传输时保留否则经过带 VLAN 的交换机后优先级和 VID 信息会丢失。广播报文略微不同目的 MAC 固定为FF FF FF FF FF FF且不携带 TPID/TCI 字段也就是没有 VLAN 标签。这种格式多用于测试工具或老式装置的点对点广播输出。实际智能站里绝大多数 GOOSE 都是组播形式目的 MAC 落在01 0C CD 01这个 OUI 段内后面两位由 SCD 文件里的 GOOSE 控制块配置决定。解析时不能只认0x88B8还要结合 APPID 做双重过滤因为一个变电站内可能同时存在几十条 GOOSE 控制块。字段长度字节说明目的 MAC6组播地址通常以 01 0C CD 01 开头源 MAC6发送装置的物理地址TPID20x8100VLAN 标签标识TCI2优先级 CFI VID以太网类型2GOOSE 固定为 0x88B8APPID2应用标识用于过滤报文长度2从 APPID 字段开始到报文结尾的字节数保留位4通常为 00 00 00 002.2 TPID 0x8100 与 TCI 的优先级陷阱抓包里最常见的一个坑是报文头出现81 00后面紧跟40 03或80 00很多人会把它当成普通数据直接跳过。实际上81 00就是 TPID 0x8100后面的 TCI 两个字节里高 3 位是用户优先级User Priority第 4 位是 CFI 标志低 12 位是 VID。以示例报文里的40 03为例二进制是0100 0000 0000 0011解析出来优先级为 2VID 为 3。如果装置配置了 VLAN抓包工具没有设置对应的 VLAN 过滤规则Wireshark 默认会按无 VLAN 的方式解析导致后续字段全部错位。我一般会在抓包时就加上vlan eth.type 0x88b8的显示过滤或者在抓包分析工具里把 VLAN 标签识别打开再对照 SCD 文件里的 VLAN ID 确认报文走的是哪条虚拟网络。组播地址相同但 VID 不同的两条 GOOSE在交换机里是分成两条流量转发的调试时要特别注意。2.3 APPID 与长度字段的计算边界以太网类型之后是 APPID长度为 2 字节。APPID 由 SCD 文件中的APPID属性下发范围在 0x0000 到 0x3FFF 之间高低字节组合后构成唯一标识。示例报文里88 B8 00 07 00 90这一段00 07就是 APPID00 90就是长度字段。这个长度是 0x0090也就是 144 字节它表示从 APPID 字段的第一个字节开始到整个 APDU 结束的总字节数。长度字段的值等于 APPID 2 字节 长度字段本身 2 字节 保留位 4 字节 APDU 长度即m 8其中 m 是 APDU 的字节数。解析时不要把长度字段当成 APDU 长度直接用否则会把保留位也纳入 APDU 解析范围导致最后一个字段的边界判断错误。我在做报文比对工具时就是用这个公式反推 APDU 长度先校验报文长度是否对得上再往下拆 TLV能提前拦截一半以上的截断报文。3. ASN.1 BER 的 TLV 解码规则与 GOOSE 数据类型映射3.1 Tag 字节的位域拆解GOOSE APDU 采用 ASN.1 的 BERBasic Encoding Rules编码基本单位是 TLV 三元组也就是 Tag Length Value。Tag 字节不是随便写的它的位域含义是Bit 7 和 Bit 6 表示 Tag 类型类别00是通用类型01是应用类型10是上下文相关类型11是私有类型Bit 5 表示 Primitive0还是 Constructed1Bit 4 到 Bit 0 才是真正的标签值。以 APDU Head 的61为例二进制是0110 0001Bit 7-6 为01说明是应用类型Bit 5 为1说明是 Constructed 构造类型Bit 4-0 为00001标签值为 1。所以61表示 APDU是一个应用类型的构造节点后面跟的长度覆盖全部子字段。同理80的二进制是1000 0000属于上下文相关类型、Primitive 模式、标签值 0表示 gocbRef83的二进制是1000 0011表示布尔型数据。这些 Tag 值在 GOOSE 的 ASN.1 定义里是固定的记熟可以省去反复查表的功夫。3.2 GOOSE 常见数据类型的 Tag 映射表在 GOOSE 报文和 allData 集合里字段的数据类型决定了 Tag 的高位组合。下表是实际解析中最常碰到的数据类型映射关系Tag 值数据类型长度字节解析方式0x80gocbRefVisibleString变长按 ASCII 码直接读字符串0x81timeAllowedtoLiveINT2整型单位 ms0x82datSetVisibleString变长按 ASCII 码直接读字符串0x83goID / boolean变长 / 1字符串或 0x00、0x01 布尔值0x84tUTC 时间88 字节时间戳UTC 格式0x85stNumINT2 或 3状态序号整型0x86sqNumINT2 或 3序列序号整型0x87testBOOLEAN10x00 为 FALSE0x01 为 TRUE0x88confRevINT2 或 3配置版本号0x89ndsComBOOLEAN10x00 表示 FALSE0x8AnumDatSetEntriesINT1allData 中的数据集条目数量0xABallData构造类型变长内部嵌套多个 TLV 数据注意0x84的 UTC 时间戳是 8 字节编码为秒数和纳秒数组合前 4 字节是秒后 4 字节是纳秒基准时间是 1970 年 1 月 1 日。示例报文84 08 00 00 00 00 00 00 00 00解析出来就是01/01/1970_00:00:00.000000表示装置没有校准系统时间这在现场抓包里很常见不必当成解析错误。3.3 Length 字段的短格式与长格式BER 编码的 Length 字段有两种写法。当长度小于 128 时用短格式Length 字段本身就是字节数比如80 25里的25表示 37 字节。当长度大于等于 128 时用长格式Length 字段的最高位为 1低 7 位表示后续用几个字节来编码长度值。APDU Head 里的61 81 85就是长格式的例子81表示后跟 1 个长度字节85表示 APDU 的长度是 133 字节。61 82 01 6D同理表示长度占用 2 字节值为 0x016D也就是 365 字节。解析函数里必须同时处理这两种情况否则遇到长格式时会把长度字节本身当成值的一部分导致整个 TLV 错位。我在写解析器时固定先读一个字节判断最高位再决定要不要继续读长度字节。4. 三份真实报文逐字节拆解从守护报文到嵌套数据集4.1 报文一目的 MAC 为01 00 00 00 00 07的守护报文先看一段带 VLAN 标签的完整报文头0000: 01 00 00 00 00 07 08 00 06 86 48 42 81 00 40 03 0010: 88 B8 00 07 00 90 00 00 00 00 61 81 85 80 25 50前 6 字节01 00 00 00 00 07是目的 MAC这里用了组播地址但未用标准 IEC 组播段接着 6 字节08 00 06 86 48 42是源 MAC。81 00是 TPID40 03是 TCI解析出优先级 2、VID 3。以太网类型88 B8确认是 GOOSEAPPID 是00 07长度00 90为 144 字节。四个保留字节00 00 00 00之后61 81 85是 APDU 头。80 25表示 gocbRef长度 37。从偏移地址 0x0010 的0x50开始是 ASCII 字符0x50是P往后连续的字符串是P2A1J1Q6Protection/LLN0$GSEprotection接着81 02 05 0081是 timeAllowedtoLive02是长度05 00就是 0x0500十进制 1280单位毫秒。再往后82 25是 datSet长度也是 37后面的字符串同样是P2A1J1Q6Protection/LLN0$GSEprotection。83 01 37中的83是 goID长度 10x37是 ASCII 码的7。84 08后面跟 8 字节时间戳全 0 表示基准时间。85 01 01是 stNum 值为 186 03 02 70 A1是 sqNum0x0270A1 等于 159905。87 01 00是 test 为 FALSE88 01 01是 confRev 为 189 01 00是 ndsCom 为 FALSE8A 01 04是 numDatSetEntries 为 4。最后AB 10是 allData长度 16 字节里面包含 4 个数据条目。仔细看内部分解83 01 00 -- boolean FALSE 84 03 02 00 00 -- bit-string长度 3 83 01 00 -- boolean FALSE 84 03 02 00 00 -- bit-string长度 3这里有个容易误判的点allData 里的84不是时间戳 Tag而是 BIT-STRING 类型。两个 Tag 值相同但上下文不同解析逻辑要按 allData 内部的 ASN.1 类型定义走不能一概而论按 GoID 或时间处理。4.2 报文二标准组播地址01 0C CD 01的 Comgoose 样例0000 01 0c cd 01 00 04 01 0c cd 01 10 10 88 b8 00 04 0010 00 94 00 00 00 00 61 81 89 80 1c 58 37 32 31 32目的 MAC01 0C CD 01 00 04这是标准 IEC 61850 GOOSE 组播段。源 MAC01 0C CD 01 10 10看起来像组播地址实际是装置配置的特殊 MAC不需要按常规单播源 MAC 去校验。没有 TPID/TCI直接是88 B8以太网类型APPID 是00 04长度00 94为 148 字节保留位 4 字节全 0。APDU 头61 81 89长度 137。80 1C是 gocbRef28 字节0x58是X字符串为X7212_2HBPROT/LLN0$GO$gocbTx81 02 27 10是 timeAllowedtoLive0x2710 就是 10000 毫秒。82 1C是 datSet长度 28内容为X7212_2HBPROT/LLN0$dsGooseTx。83 11是 goID长度 17查 ASCII 码表可得X7212_GOOSE_TX_ID。84 08 47 42 d2 8a c8 31 26 ea是 8 字节时间戳。85 01 01是 stNum 为 186 01 0D是 sqNum 为 1387 01 00是 test 为 FALSE88 01 01是 confRev 为 189 01 00是 ndsCom 为 FALSE8A 01 08是 numDatSetEntries 为 8。allData 的AB 18长度 24 字节注意里面出现了 8 个数据链但报文里只展开了 4 个布尔型和 4 个 bit-string每个长度很短83 01 00 84 01 00 83 01 00 84 01 00 83 01 00 84 01 00 83 01 00 84 01 00这里的84 01 00是 BIT-STRING长度 1 字节值 0x00。上一份报文里84 03 02 00 00的 BIT-STRING 长度为 3是因为带了填充位和多余字节。解析 BIT-STRING 时第一个字节是未使用比特数后面才是实际数据这个细节直接影响数值还原。4.3 报文三Goose3 的嵌套 allData 超长 APDU 主要涉及构造节点第三份报文的长格式非常有代表性0000 01 0c cd 01 01 ff 00 0d 60 9f 07 a6 81 00 80 00 0010 88 b8 00 00 01 79 00 00 00 00 61 82 01 6d 80 10目的 MAC01 0C CD 01 01 FF源 MAC00 0D 60 9F 07 A6TPID81 00TCI80 00优先级 4VID 0。以太网类型88 B8APPID00 00长度01 79即 377 字节。APDU 头是61 82 01 6D长格式长度 365 字节。80 10是 gocbRef长度 16ASCII 内容为EDP01LD0/gooseST。81 01 0A是 timeAllowedtoLive 为 10 毫秒。82 18是 datSet长度 24内容为EDP01LD0/LLN0$All_ST_Pos。83 0C是 goID长度 12内容为LD0_Goose_ST。84 08时间戳全 085 01 01是 stNum 为 186 01 00是 sqNum 为 087 01 00是 test 为 FALSE88 01 20是 confRev 为 3289 01 00是 ndsCom 为 FALSE8A 01 08是 numDatSetEntries 为 8。这份报文最值得研究的是 allData。AB 82 01 10表示 allData 本身是构造类型长度 0x0110 即 272 字节里面嵌套了 8 个A2开头的数据结构。A2是上下文相关类、构造类型、标签值 2对应 IEC 61850 里的 Position 类型每个 Position 内部又由85整数、89布尔、86整数、84bit-string、91UTC 时间等标签组成。例如a2 20 a2 05 85 01 00 89 00 86 01 00 84 02 06 40 84 03 03 00 00 91 08 45 65 09 c2 7f ff ff 18A2 20表示一个长度为 32 的 Position 数据节点内部第一个A2 05嵌套了 5 字节数据85 01 00是整型值 089 00是布尔 FALSE86 01 00是另一个整型 084 02 06 40是 bit-string84 03 03 00 00是另一个 bit-string91 08是 8 字节时间戳。嵌套解析时要把每个 A2 当成独立 TLV 子树递归处理而不是平铺推进否则位置值序列会对不上号。5. 手写最小 GOOSE 解析器的关键点与排错技巧5.1 一个 50 行内的 TLV 解析循环def read_length(buf, i): b buf[i] if b 0x80: n b 0x7F return int.from_bytes(buf[i1:i1n], big), i 1 n return b, i 1 def parse_tlv(buf, i): tag buf[i] length, next_i read_length(buf, i 1) value buf[next_i:next_i length] return tag, length, value, next_i length def walk_apdu(buf): fields {} tag, length, value, i parse_tlv(buf, 0) if tag ! 0x61: raise ValueError(fexpected APDU 0x61, got 0x{tag:02x}) while i len(buf): tag, length, value, i parse_tlv(buf, i) name { 0x80: gocbRef, 0x81: timeAllowedToLive, 0x82: datSet, 0x83: goID, 0x84: t, 0x85: stNum, 0x86: sqNum, 0x87: test, 0x88: confRev, 0x89: ndsCom, 0x8A: numDatSetEntries, 0xAB: allData, }.get(tag, ftag_0x{tag:02x}) fields[name] value print(f{name:20s} tag0x{tag:02x} len{length}) return fields这段代码只做两层解析walk_apdu先读 APDU 头确认 Tag 是0x61后循环解析顶层字段。read_length处理短格式和长格式两种长度编码返回长度值和下一个字段的起始偏移。parse_tlv返回 tag、length、value 和下一个位置value 是原始字节切片具体字段的二次解析由调用方按 Tag 决定。allData 的0xAB在这里只是被整体取出如果需要递归展开就把 value 再传给walk_apdu或者单独写一个针对数据集类型的解析逻辑。对于0x84时间戳用struct.unpack(II, value)分别得到秒和纳秒对于 VisibleString直接value.decode(ascii)即可。参数说明buf是 802.3 帧里去掉 MAC、VLAN、以太网类型、APPID、长度和保留位之后的 APDU 完整字节序列起始位置就是 APDU Head 的0x61字节。5.2 时间戳和 stNum/sqNum 的组合判断GOOSE 的可靠性机制依赖 stNum 和 sqNum 的配合stNum 是状态序号变位时加一sqNum 是序列序号状态没变化时每帧加一stNum 变化后 sqNum 从 1 重新开始。现场判定丢帧时计算相邻两帧的时间差和 sqNum 差值就能定位是发送侧抖动还是网络丢包。例如 sqNum 从 159904 跳到 159907中间少了两帧说明有丢包如果帧率低于 timeAllowedtoLive 的设定值说明装置发送周期配置异常。时间戳不能直接作为判断依据很多装置不校时84 08后全是零只有 sqNum 才是可靠的连续性参考。5.3 实际排错时我最先看的三处第一看 APPID 是否在 SCD 文件里有对应的 GOOSE 控制块不在表里就直接丢弃过滤掉遗留装置的旧报文。第二看 allData 的 numDatSetEntries 和实际 TLV 数量是否一致不一致时多半是 SCD 数据集定义和装置实际下发的数据类型不匹配常见于通信重启后装置加载了旧配置。第三看嵌套结构里的A2节点长度是否异常比如位置量 Position 正常是 30 字节左右如果出现 10 字节以内或连续多个零长度优先怀疑交换机裁剪了长帧。还有一点VLAN 标签存在时用 Wireshark 默认模板解析会漏掉 TPID记得手工打开802.1Q解析然后按vlan.id过滤再去核对 TCI 里的优先级和 VID这也是我前面强调先验证 TPID 再往下拆帧的原因。本文还有配套的精品资源点击获取
返回列表