简介:这是一份面向电力自动化、变电站通信协议开发与测试人员的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 ...,很多人第一反应是“这啥”。这不是普通以太网帧,是 GOOSE(Generic Object Oriented Substation Event),IEC 61850 里负责跳闸、联闭锁这类毫秒级信号传输的报文。它跑在二层,不走 TCP/IP,所以用普通协议解析器根本拆不开。这份《GOOSE报文解析.pdf》的价值就在这:它把 GOOSE 的帧结构、ASN.1 BER 编码规则、以及三组真实抓包(普通报文、Comgoose、Goose3)的逐字节拆解摆在一起,让你能对着十六进制一个字节一个字节地读。适合刚接触智能变电站调试的运维、做保护装置测试的工程师,以及需要自己写解析脚本的人。读完你至少能做到:拿到一帧 GOOSE,知道从哪个字节开始是 APDU,83 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 固定为0x8100,TCI 里包含用户优先级、CFI 和 VID。以太网类型对 GOOSE 来说是0x88B8,这是识别 GOOSE 帧的第一道标志。APPID 是应用标识,用来区分同一网段里不同的 GOOSE 控制块。APDU 长度字段的值是 m+8,m 是 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 编码,核心就是 TLV:Tag + Length + Value。Tag 一个字节,高两位(Bit 7,6)表示类型类别,Bit 5 表示是原始类型还是构造类型,低五位(Bit 4-0)是 Tag 值。文档里列出的数据类型映射很关键:
| Tag 值 | 数据类型 | 说明 |
|---|---|---|
| 83 | BOOL | 布尔型,值 00 为 FALSE,01 为 TRUE |
| 84 | BIT-String | 位串,长度字段后跟实际位数和填充 |
| 85 | Int | 整型 |
| 86 | Unsigned | 无符号整型 |
| 87 | BOOL | test 标志常用 |
| 88 | Unsigned | confRev 常用 |
| 89 | BOOL | ndsCom 常用 |
| 8A | Unsigned | numDatSetEntries |
| 91 | UTC | 时间类型 |
| AB | 构造类型 | allData 数据集 |
Length 字段的编码有短形式和长形式。短形式就是一个字节,值小于 128;长形式第一个字节最高位为 1,低七位表示后面跟几个字节的长度值。比如81 85表示后面一个字节85是长度,即 133 字节。这个规则在解析 gocbRef 这种长字符串时一定会遇到。
Value 部分对字符串类型直接用 ASCII 编码,对数值类型按 BER 规则编码。比如85 01 01表示 Int 类型、长度 1、值 1,对应 stNum=1。
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:offset+2] offset += 2 length = int.from_bytes(data[offset:offset+2], '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:offset+len_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:offset+n], 'big') offset += n value = data[offset:offset+l] offset += l if tag == 0x80: result['gocbRef'] = value.decode('ascii', errors='ignore') 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、Goose3
3.1 普通报文解析:gocbRef 与 allData 的 TLV 展开
文档里第一组抓包是普通报文,目的 MAC01 00 00 00 00 07,源 MAC08 00 06 86 48 42,TPID81 00,TCI40 03,以太网类型88 B8,APPID00 07,长度00 90,保留位00 00,APDU 头61 81 85。
从80 25开始是 gocbRef:Tag80,长度25(37 字节),值从50 32 41 31 4A 31 51 36 ...开始,ASCII 解码是P2A1J1Q6Protection/LLN0$GSEprotection。接着81 02 05 00是 timeAllowedtoLive,Tag81,长度 2,值05 00即 1280 毫秒。82 25是 datSet,长度 37,值和 gocbRef 一样。83 01 37是 goID,Tag83这里文档标注为 goID,值37对应 ASCII 的7。84 08是 t,8 字节时间。85 01 01是 stNum=1。86 03 02 70 A1是 sqNum,值02 70 A1即 159905。87 01 00是 test=FALSE。88 01 01是 confRev=1。89 01 00是 ndsCom=FALSE。8A 01 04是 numDatSetEntries=4。最后AB 10是 allData,长度 16,里面嵌套了四个小数据集:83 01 00(BOOL FALSE)、84 03 02 00 00(BIT-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 b8,APPID00 04,长度00 94,保留位00 00,APDU 头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 78,ASCII 为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 字节。
这组数据的关键验证点:numDatSetEntries=8,allData 长度 24,24/8=3,每组恰好是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 a6,TPID81 00,TCI80 00,以太网类型88 b8,APPID00 00,长度01 79,保留位00 00,APDU 头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 0,89 00是 BOOL FALSE,86 01 00是 Unsigned 0,84 02 06 40是 BIT-String 长度 2 值06 40,84 03 03 00 00是 BIT-String 长度 3,91 08是 UTC 时间 8 字节45 65 09 c2 7f ff ff 18,83 01 00是 BOOL FALSE。这种嵌套结构在解析时必须递归处理,不能只做一层 TLV 循环。
注意:Goose3 的 allData 里出现了
91UTC 类型,这是三组里唯一带时间戳的数据集成员。如果你的解析器只处理了83和84,遇到91会直接跳过,导致后续偏移错乱。
4. 避坑与排查:GOOSE 解析里最容易翻车的五个点
4.1 现象:解析 gocbRef 得到乱码,长度字段明显不对
原因:把 APPID 后面的 APDU 长度字段当成了 APDU 头的长度。普通报文里 APPID 后跟的是00 90,这是 m+8,不是 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 长度 272,numDatSetEntries=8,每个成员 34 字节(a2 20加 32 字节内容),8×34=272,正好对上。
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(dst="01:00:00:00:00:07", src="08:00:06:86:48:42") / Dot1Q(vlan=3, prio=4) / Raw(load=bytes.fromhex("88 B8 00 07 00 90 00 00") + goose_payload) ) sendp(frame, iface="eth0", loop=0)这段代码的逻辑:Ether 层指定目的 MAC 和源 MAC,Dot1Q 层构造 VLAN 标签(TPID 由 Scapy 自动填81 00,TCI 里 prio=4、vlan=3 对应文档里的40 03)。Raw 层里先放以太网类型88 B8、APPID00 07、长度00 90、保留位00 00,然后拼接 APDU。参数说明:iface换成你实际抓包的网卡名,loop=0表示只发一次。发完之后用 Wireshark 抓包,看它能不能把你的帧识别成 GOOSE,再对照文档里的解析结果逐字段核对。
这个回环验证的好处是:你能精确控制每个字节,如果 Wireshark 解析出来的 gocbRef 和你构造的不一样,说明你的偏移理解有误。我见过有人构造的帧 Wireshark 直接标红,原因是 APDU 头长度字段写错了——61 81 85里的85是 APDU 总长度,不是 gocbRef 长度,写错这个后面全乱。
从那以后我每次改解析代码,都先用 Scapy 构造一帧已知内容的 GOOSE,跑一遍回环,确认 Wireshark 和自写解析器输出一致,再拿去解析真实抓包。这个习惯帮我省了很多对着乱码发呆的时间。希望帮到你。
本文还有配套的精品资源,点击获取