简介这是一份面向电力自动化、变电站通信协议开发与测试人员的GOOSE报文解析技术文档适合具备一定网络与ASN.1编码基础、需要深入理解IEC 61850 GOOSE帧结构的工程师学习参考。资源包共1个PDF文件大小约121KB内容围绕基于ISO/IEC 8802-3的帧格式展开系统梳理普通报文与广播报文的组成差异涵盖目的MAC、源MAC、TPID、TCI、以太网类型0x88B8、APPID及APDU数据长度等字段含义。文档重点讲解APDU Head的61 81长度标识规则以及ASN.1 BER编码中TLV结构与Tag位域解析方法并逐一说明BOOL、BIT-String、UTC时间、INT、Unsigned、Visible-String等数据类型的标记与解码方式。文中还给出多组十六进制报文实例逐字节标注gocbRef、timeAllowedtoLive、datSet、goID、stNum、sqNum、test、confRev、ndsCom、numDatSetEntries及allData等字段的解析结果便于读者对照报文快速定位与排错。目前已有1038人学习下载适合作为协议调试与报文分析的案头参考。1. 抓包抓到一串十六进制GOOSE 报文到底怎么读变电站过程层抓包Wireshark 里跳出一串61 81 85 80 25 ...很多人第一反应是“这啥”。这不是普通以太网帧是 GOOSEGeneric Object Oriented Substation EventIEC 61850 里负责跳闸、联闭锁这类毫秒级信号传输的报文。它跑在二层不走 TCP/IP所以用普通协议解析器根本拆不开。这份《GOOSE报文解析.pdf》的价值就在这它把 GOOSE 的帧结构、ASN.1 BER 编码规则、以及三组真实抓包普通报文、Comgoose、Goose3的逐字节拆解摆在一起让你能对着十六进制一个字节一个字节地读。适合刚接触智能变电站调试的运维、做保护装置测试的工程师以及需要自己写解析脚本的人。读完你至少能做到拿到一帧 GOOSE知道从哪个字节开始是 APDU83 01 00到底代表什么。2. GOOSE 帧结构拆解从以太网头到 APDU 的字节地图2.1 普通报文与广播报文的帧格式差异GOOSE 直接封装在 ISO/IEC 8802-3 以太网帧里没有 IP 头也没有 TCP 头。普通报文的结构是目的 MAC 源 MAC (TPID TCI) 以太网类型 APPID APDU 长度 保留位 APDU。括号里的 TPID 和 TCI 是可选的 VLAN 标签但文档里明确写了“强烈建议加入”实际工程中只要经过交换机基本都会带 VLAN。TPID 固定为0x8100TCI 里包含用户优先级、CFI 和 VID。以太网类型对 GOOSE 来说是0x88B8这是识别 GOOSE 帧的第一道标志。APPID 是应用标识用来区分同一网段里不同的 GOOSE 控制块。APDU 长度字段的值是 m8m 是 APDU 本身长度加 8 是因为后面还跟着 8 字节的保留位和 APDU 头。广播报文和普通报文的区别只在目的 MAC广播用FF FF FF FF FF FF普通报文用组播地址。结构上广播报文没有 TPID 和 TCI直接是目的 MAC 源 MAC 以太网类型 APPID 长度 保留位 APDU。这个差异在写解析器时如果不注意遇到广播帧就会把以太网类型字段读错位。提示抓包时先看目的 MAC 第一个字节的最低位如果是 1 就是组播全 F 就是广播这决定你后面按哪种结构去偏移。2.2 APDU 头与 ASN.1 BER 编码规则APDU 头格式是61 81 GOOSEPDU 长度。61是 ASN.1 里的 Tag表示这是一个 constructed 类型81是长度编码方式表示后面跟一个字节的长度值。文档里写“从 80 开始算起”意思是长度字段的起始偏移要从 APDU 头之后开始数。GOOSE 的 APDU 用 ASN.1 BER 编码核心就是 TLVTag Length Value。Tag 一个字节高两位Bit 7,6表示类型类别Bit 5 表示是原始类型还是构造类型低五位Bit 4-0是 Tag 值。文档里列出的数据类型映射很关键Tag 值数据类型说明83BOOL布尔型值 00 为 FALSE01 为 TRUE84BIT-String位串长度字段后跟实际位数和填充85Int整型86Unsigned无符号整型87BOOLtest 标志常用88UnsignedconfRev 常用89BOOLndsCom 常用8AUnsignednumDatSetEntries91UTC时间类型AB构造类型allData 数据集Length 字段的编码有短形式和长形式。短形式就是一个字节值小于 128长形式第一个字节最高位为 1低七位表示后面跟几个字节的长度值。比如81 85表示后面一个字节85是长度即 133 字节。这个规则在解析 gocbRef 这种长字符串时一定会遇到。Value 部分对字符串类型直接用 ASCII 编码对数值类型按 BER 规则编码。比如85 01 01表示 Int 类型、长度 1、值 1对应 stNum1。2.3 用 Python 写一个最小解析器验证字段光看文档不够我一般会写个最小解析器把关键字段抽出来验证自己有没有读错偏移。下面这段代码只处理普通报文带 VLAN 标签输入是十六进制字符串输出 gocbRef、stNum、sqNum 和 allData 的原始字节。import binascii def parse_goose(hex_str): data binascii.unhexlify(hex_str.replace( , )) # 以太网头目的MAC(6) 源MAC(6) TPID(2) TCI(2) 以太网类型(2) offset 6 6 2 2 2 eth_type data[offset-2:offset] if eth_type ! b\x88\xb8: raise ValueError(不是 GOOSE 报文) appid data[offset:offset2] offset 2 length int.from_bytes(data[offset:offset2], big) offset 2 # 保留位 2 字节 offset 2 # APDU 头61 81 长度 assert data[offset] 0x61 offset 1 if data[offset] 0x80: len_bytes data[offset] 0x7f offset 1 apdu_len int.from_bytes(data[offset:offsetlen_bytes], big) offset len_bytes else: apdu_len data[offset] offset 1 # 开始解析 TLV result {} while offset len(data): tag data[offset] offset 1 if offset len(data): break l data[offset] offset 1 if l 0x80: n l 0x7f l int.from_bytes(data[offset:offsetn], big) offset n value data[offset:offsetl] offset l if tag 0x80: result[gocbRef] value.decode(ascii, errorsignore) elif tag 0x85: result[stNum] int.from_bytes(value, big) elif tag 0x86: result[sqNum] int.from_bytes(value, big) elif tag 0xab: result[allData_raw] value.hex() return result # 用文档里第一组报文的前半段测试 hex_frame 0100000000070800068648428100400388B800070090000000006181858025503241314A31513650726F74656374696F6E2F4C4C4E302447534570726F74656374696F6E print(parse_goose(hex_frame))这段代码的逻辑说明先跳过以太网头固定 18 字节含 VLAN校验以太网类型必须是88B8。然后读 APPID 和长度跳过 2 字节保留位。APDU 头里61后面跟长度如果长度字节最高位是 1说明是长形式低七位表示后续几个字节组成长度值。之后进入 TLV 循环遇到80就取 gocbRef 的 ASCII 值遇到85和86就转成整数。参数说明hex_str是 Wireshark 里复制的十六进制流去掉空格offset的初始值 18 是普通报文的固定头长度如果抓的是广播报文要去掉 TPID 和 TCI 的 4 字节。跑通这个脚本你就能确认文档里说的“从 80 开始算起”到底对应哪个偏移。实际调试中我见过有人把 APPID 后面的长度字段当成 APDU 长度直接去读结果偏移全错后面解析出来的 gocbRef 是一堆乱码。3. 三组真实抓包逐字节对照普通报文、Comgoose、Goose33.1 普通报文解析gocbRef 与 allData 的 TLV 展开文档里第一组抓包是普通报文目的 MAC01 00 00 00 00 07源 MAC08 00 06 86 48 42TPID81 00TCI40 03以太网类型88 B8APPID00 07长度00 90保留位00 00APDU 头61 81 85。从80 25开始是 gocbRefTag80长度2537 字节值从50 32 41 31 4A 31 51 36 ...开始ASCII 解码是P2A1J1Q6Protection/LLN0$GSEprotection。接着81 02 05 00是 timeAllowedtoLiveTag81长度 2值05 00即 1280 毫秒。82 25是 datSet长度 37值和 gocbRef 一样。83 01 37是 goIDTag83这里文档标注为 goID值37对应 ASCII 的7。84 08是 t8 字节时间。85 01 01是 stNum1。86 03 02 70 A1是 sqNum值02 70 A1即 159905。87 01 00是 testFALSE。88 01 01是 confRev1。89 01 00是 ndsComFALSE。8A 01 04是 numDatSetEntries4。最后AB 10是 allData长度 16里面嵌套了四个小数据集83 01 00BOOL FALSE、84 03 02 00 00BIT-String、83 01 00、84 03 02 00 00。这里有个容易翻车的点83在 goID 位置和 allData 内部都出现了但含义不同。goID 的83是文档标注的 goID 类型而 allData 内部的83是 BOOL 类型。解析时不能只看 Tag 值必须结合上下文——在 APDU 顶层按字段顺序解析进入 allData 后按数据集成员类型解析。3.2 Comgoose 报文APPID 与 numDatSetEntries 的对应关系第二组是 Comgoose 抓包目的 MAC01 0c cd 01 00 04源 MAC01 0c cd 01 10 10以太网类型88 b8APPID00 04长度00 94保留位00 00APDU 头61 81 89。gocbRef 是80 1c长度 28值58 37 32 31 32 5f 32 48 42 50 52 4f 54 2f 4c 4c 4e 30 24 47 4f 24 67 6f 63 62 54 78ASCII 为X7212_2HBPROT/LLN0$GO$gocbTx。timeAllowedtoLive81 02 27 10即 10000。datSet82 1c长度 28值X7212_2HBPROT/LLN0$dsGooseTx。goID83 11长度 17值X7212_GOOSE_TX_ID。t84 088 字节。stNum85 01 01。sqNum86 01 0d即 13。test87 01 00。confRev88 01 01。ndsCom89 01 00。numDatSetEntries8a 01 08即 8。allDataab 18长度 24内部是 8 组83 01 00 84 01 00每组 3 字节8 组正好 24 字节。这组数据的关键验证点numDatSetEntries8allData 长度 2424/83每组恰好是83 01 00或84 01 00。如果你解析出来的 allData 长度和 numDatSetEntries 对不上说明 TLV 长度读错了。我一般会先算这个除法对不上就回头检查 Length 字段是不是用了长形式但没处理。3.3 Goose3 报文嵌套 allData 与 UTC 时间类型第三组 Goose3 抓包最复杂目的 MAC01 0c cd 01 01 ff源 MAC00 0d 60 9f 07 a6TPID81 00TCI80 00以太网类型88 b8APPID00 00长度01 79保留位00 00APDU 头61 82 01 6d。注意 APDU 头是61 82 01 6d长度用了两个字节01 6d即 365。gocbRef80 10长度 16值EDP01LD0/gooseST。timeAllowedtoLive81 01 0a即 10。datSet82 18长度 24值EDP01LD0/LLN0$All_ST_Pos。goID83 0c长度 12值LD0_Goose_ST。t84 088 字节全零。stNum85 01 01。sqNum86 01 00。test87 01 00。confRev88 01 20即 32。ndsCom89 01 00。numDatSetEntries8a 01 08即 8。allDataab 82 01 10长度 272。allData 内部是嵌套结构每个成员是a2 20开头的构造类型长度 32。展开一个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 18 83 01 00。这里面85 01 00是 Int 089 00是 BOOL FALSE86 01 00是 Unsigned 084 02 06 40是 BIT-String 长度 2 值06 4084 03 03 00 00是 BIT-String 长度 391 08是 UTC 时间 8 字节45 65 09 c2 7f ff ff 1883 01 00是 BOOL FALSE。这种嵌套结构在解析时必须递归处理不能只做一层 TLV 循环。注意Goose3 的 allData 里出现了91UTC 类型这是三组里唯一带时间戳的数据集成员。如果你的解析器只处理了83和84遇到91会直接跳过导致后续偏移错乱。4. 避坑与排查GOOSE 解析里最容易翻车的五个点4.1 现象解析 gocbRef 得到乱码长度字段明显不对原因把 APPID 后面的 APDU 长度字段当成了 APDU 头的长度。普通报文里 APPID 后跟的是00 90这是 m8不是 APDU 实际长度。APDU 头在保留位之后是61 81 85其中85才是 GOOSEPDU 的长度。解决偏移计算必须严格按“目的 MAC(6) 源 MAC(6) TPID(2) TCI(2) 以太网类型(2) APPID(2) 长度(2) 保留位(2)”累加到61才开始读 APDU。广播报文去掉 TPID 和 TCI 的 4 字节。4.2 现象allData 解析到一半就断了后面的数据全错位原因Length 字段用了长形式但没识别。比如82 01 10表示后面两个字节01 10是长度 272如果只读一个字节82后面的01就会把长度当成 1直接跳错。解决读 Length 时先判断最高位。如果length_byte 0x80为真低七位是后续长度字节数依次读出拼接。短形式直接取值。这个逻辑在 gocbRef 长度超过 127 时一定会触发。4.3 现象numDatSetEntries 和 allData 实际成员数对不上原因allData 内部有嵌套构造类型如a2一个顶层成员可能包含多个子 TLV。如果只数顶层 Tag会把嵌套结构当成一个成员。解决解析 allData 时递归展开。对于a2这类 constructed Tag进入内部继续按 TLV 解析直到遇到原始类型。文档里 Goose3 的 allData 长度 272numDatSetEntries8每个成员 34 字节a2 20加 32 字节内容8×34272正好对上。4.4 现象UTC 时间解析出来是 1970 年原因91类型的 8 字节 UTC 时间前 4 字节是秒后 4 字节是纳秒。文档里三组报文的 t 字段都是全零或接近零所以解析出来是01/01/1970_00:00:00.000000。这不是解析错误是装置本身没对时。解决先确认装置是否同步了时钟。如果 t 字段全零说明装置没有外部时间源这时候不要怀疑解析代码。实际工程中保护装置的 GOOSE 报文 t 字段通常来自装置内部时钟调试阶段经常看到 1970 年。4.5 现象Wireshark 能识别 GOOSE 但自己写的解析器读不出 APPID原因Wireshark 的 GOOSE 解析器会自动处理 VLAN 标签不管有没有 TPID 都能正确偏移。自己写代码时如果固定按带 VLAN 的偏移去读遇到不带 VLAN 的广播帧就会把以太网类型读成 APPID。解决先读以太网类型字段。如果偏移 12 处的两个字节是81 00说明有 VLAN偏移加 4如果直接是88 B8说明没有 VLAN。用这个判断动态调整偏移而不是写死。5. 进阶用 Scapy 构造 GOOSE 帧做回环验证解析搞明白之后反向构造一帧 GOOSE 能帮你验证对每个字段的理解是否到位。我一般用 Scapy 做这件事因为它能直接控制二层字段不需要真的连保护装置。from scapy.all import Ether, Dot1Q, Raw, sendp # 构造一个带 VLAN 的 GOOSE 帧 goose_payload bytes.fromhex( 61 81 85 # APDU 头 80 25 # gocbRef Tag 长度 50 32 41 31 4A 31 51 36 50 72 6F 74 65 63 74 69 6F 6E 2F 4C 4C 4E 30 24 47 53 45 70 72 6F 74 65 63 74 69 6F 6E 81 02 05 00 # timeAllowedtoLive 1280 82 25 # datSet 50 32 41 31 4A 31 51 36 50 72 6F 74 65 63 74 69 6F 6E 2F 4C 4C 4E 30 24 47 53 45 70 72 6F 74 65 63 74 69 6F 6E 83 01 37 # goID 7 84 08 00 00 00 00 00 00 00 00 # t 85 01 01 # stNum 1 86 03 02 70 A1 # sqNum 159905 87 01 00 # test FALSE 88 01 01 # confRev 1 89 01 00 # ndsCom FALSE 8A 01 04 # numDatSetEntries 4 AB 10 # allData 83 01 00 84 03 02 00 00 83 01 00 84 03 02 00 00 ) frame ( Ether(dst01:00:00:00:00:07, src08:00:06:86:48:42) / Dot1Q(vlan3, prio4) / Raw(loadbytes.fromhex(88 B8 00 07 00 90 00 00) goose_payload) ) sendp(frame, ifaceeth0, loop0)这段代码的逻辑Ether 层指定目的 MAC 和源 MACDot1Q 层构造 VLAN 标签TPID 由 Scapy 自动填81 00TCI 里 prio4、vlan3 对应文档里的40 03。Raw 层里先放以太网类型88 B8、APPID00 07、长度00 90、保留位00 00然后拼接 APDU。参数说明iface换成你实际抓包的网卡名loop0表示只发一次。发完之后用 Wireshark 抓包看它能不能把你的帧识别成 GOOSE再对照文档里的解析结果逐字段核对。这个回环验证的好处是你能精确控制每个字节如果 Wireshark 解析出来的 gocbRef 和你构造的不一样说明你的偏移理解有误。我见过有人构造的帧 Wireshark 直接标红原因是 APDU 头长度字段写错了——61 81 85里的85是 APDU 总长度不是 gocbRef 长度写错这个后面全乱。从那以后我每次改解析代码都先用 Scapy 构造一帧已知内容的 GOOSE跑一遍回环确认 Wireshark 和自写解析器输出一致再拿去解析真实抓包。这个习惯帮我省了很多对着乱码发呆的时间。希望帮到你。本文还有配套的精品资源点击获取