From 947f90c85ecdfb905f048f67e0e1da7339754f22 Mon Sep 17 00:00:00 2001 From: Andrew Lamarra Date: Fri, 17 Apr 2026 11:48:40 -0400 Subject: [PATCH 1/3] Add support for HSRPv1 Advertise packets --- scapy/layers/hsrp.py | 45 ++++++++++++++++++++++++++++++-------- test/scapy/layers/hsrp.uts | 20 +++++++++++++++++ 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/scapy/layers/hsrp.py b/scapy/layers/hsrp.py index 82e82606357..e404982dfba 100644 --- a/scapy/layers/hsrp.py +++ b/scapy/layers/hsrp.py @@ -13,25 +13,52 @@ """ from scapy.config import conf -from scapy.fields import ByteEnumField, ByteField, IPField, SourceIPField, \ +from scapy.fields import ByteEnumField, ByteField, ConditionalField, \ + IntField, IPField, ShortEnumField, ShortField, SourceIPField, \ StrFixedLenField, XIntField, XShortField from scapy.packet import Packet, bind_layers, bind_bottom_up from scapy.layers.inet import DestIPField, UDP +def _is_advertise(pkt) -> bool: + return pkt.opcode == 3 + + +def _is_not_advertise(pkt) -> bool: + return pkt.opcode != 3 + + class HSRP(Packet): name = "HSRP" fields_desc = [ ByteField("version", 0), ByteEnumField("opcode", 0, {0: "Hello", 1: "Coup", 2: "Resign", 3: "Advertise"}), # noqa: E501 - ByteEnumField("state", 16, {0: "Initial", 1: "Learn", 2: "Listen", 4: "Speak", 8: "Standby", 16: "Active"}), # noqa: E501 - ByteField("hellotime", 3), - ByteField("holdtime", 10), - ByteField("priority", 120), - ByteField("group", 1), - ByteField("reserved", 0), - StrFixedLenField("auth", b"cisco" + b"\00" * 3, 8), - IPField("virtualIP", "192.168.1.1")] + ConditionalField( + ByteEnumField("state", 16, { + 0: "Initial", + 1: "Learn", + 2: "Listen", + 4: "Speak", + 8: "Standby", + 16: "Active", + }), + _is_not_advertise + ), + ConditionalField(ByteField("hellotime", 3), _is_not_advertise), + ConditionalField(ByteField("holdtime", 10), _is_not_advertise), + ConditionalField(ByteField("priority", 120), _is_not_advertise), + ConditionalField(ByteField("group", 1), _is_not_advertise), + ConditionalField(ByteField("reserved", 0), _is_not_advertise), + ConditionalField(StrFixedLenField("auth", b"cisco" + b"\00" * 3, 8), _is_not_advertise), + ConditionalField(IPField("virtualIP", "192.168.1.1"), _is_not_advertise), + ConditionalField(ShortEnumField("adv_type", 1, {1: "HSRP interface state"}), _is_advertise), + ConditionalField(ShortField("adv_length", 10), _is_advertise), + ConditionalField(ByteEnumField("adv_state", 1, {1: "Active", 2: "Passive"}), _is_advertise), + ConditionalField(ByteField("adv_reserved", 0), _is_advertise), + ConditionalField(ShortField("activegroups", 0), _is_advertise), + ConditionalField(ShortField("passivegroups", 0), _is_advertise), + ConditionalField(IntField("reserved2", 0), _is_advertise) + ] def guess_payload_class(self, payload): if self.underlayer.len > 28: diff --git a/test/scapy/layers/hsrp.uts b/test/scapy/layers/hsrp.uts index eeabeb0fea3..f0a89137822 100644 --- a/test/scapy/layers/hsrp.uts +++ b/test/scapy/layers/hsrp.uts @@ -12,4 +12,24 @@ assert pkt[IP].dst == "224.0.0.2" and pkt[UDP].sport == pkt[UDP].dport == 1985 assert pkt[HSRP].opcode == 0 and pkt[HSRP].state == 16 assert pkt[HSRPmd5].type == 4 and pkt[HSRPmd5].sourceip == defaddr += HSRP - Advertise build & dissection +advertise_raw = b"\x00\x03\x00\x01\x00\x0e\x02\x00\x00\x00\x00\x01o\x00\x00\x00" +pkt = HSRP(advertise_raw) +assert pkt.opcode == 3 +assert pkt.adv_type == 1 and pkt.adv_length == 14 +assert pkt.adv_state == 2 and pkt.adv_reserved == 0 +assert pkt.activegroups == 0 and pkt.passivegroups == 1 +assert pkt.reserved2 == 0x6f000000 +assert raw( + HSRP( + opcode=3, + adv_type=1, + adv_length=14, + adv_state=2, + adv_reserved=0, + activegroups=0, + passivegroups=1, + reserved2=0x6f000000, + ) +) == advertise_raw From 8b6b047102f2cf3c457db7e103f5e19a29bd96b4 Mon Sep 17 00:00:00 2001 From: Andrew Lamarra Date: Fri, 17 Apr 2026 12:44:01 -0400 Subject: [PATCH 2/3] Rename a few of the advertise fields --- scapy/layers/hsrp.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scapy/layers/hsrp.py b/scapy/layers/hsrp.py index e404982dfba..5786f97cc0c 100644 --- a/scapy/layers/hsrp.py +++ b/scapy/layers/hsrp.py @@ -54,10 +54,10 @@ class HSRP(Packet): ConditionalField(ShortEnumField("adv_type", 1, {1: "HSRP interface state"}), _is_advertise), ConditionalField(ShortField("adv_length", 10), _is_advertise), ConditionalField(ByteEnumField("adv_state", 1, {1: "Active", 2: "Passive"}), _is_advertise), - ConditionalField(ByteField("adv_reserved", 0), _is_advertise), - ConditionalField(ShortField("activegroups", 0), _is_advertise), - ConditionalField(ShortField("passivegroups", 0), _is_advertise), - ConditionalField(IntField("reserved2", 0), _is_advertise) + ConditionalField(ByteField("adv_reserved1", 0), _is_advertise), + ConditionalField(ShortField("adv_active_grps", 0), _is_advertise), + ConditionalField(ShortField("adv_passive_grps", 0), _is_advertise), + ConditionalField(IntField("adv_reserved2", 0), _is_advertise) ] def guess_payload_class(self, payload): From e2ed46d3ec0c1a83b12098b988b52ee0417362d6 Mon Sep 17 00:00:00 2001 From: Andrew Lamarra Date: Mon, 20 Apr 2026 22:19:24 -0400 Subject: [PATCH 3/3] Use dispatch_hook instead of many ConditionalFields --- scapy/layers/hsrp.py | 86 ++++++++++++++++++++++---------------- test/scapy/layers/hsrp.uts | 17 ++++---- 2 files changed, 59 insertions(+), 44 deletions(-) diff --git a/scapy/layers/hsrp.py b/scapy/layers/hsrp.py index 5786f97cc0c..b64b9425d16 100644 --- a/scapy/layers/hsrp.py +++ b/scapy/layers/hsrp.py @@ -13,53 +13,48 @@ """ from scapy.config import conf -from scapy.fields import ByteEnumField, ByteField, ConditionalField, \ - IntField, IPField, ShortEnumField, ShortField, SourceIPField, \ - StrFixedLenField, XIntField, XShortField +from scapy.compat import orb +from scapy.fields import ByteEnumField, ByteField, IntField, IPField, \ + ShortEnumField, ShortField, SourceIPField, StrFixedLenField, \ + XIntField, XShortField from scapy.packet import Packet, bind_layers, bind_bottom_up from scapy.layers.inet import DestIPField, UDP -def _is_advertise(pkt) -> bool: - return pkt.opcode == 3 - - -def _is_not_advertise(pkt) -> bool: - return pkt.opcode != 3 +_HSRP_OPCODES = {0: "Hello", 1: "Coup", 2: "Resign", 3: "Advertise"} +_HSRP_STATES = { + 0: "Initial", + 1: "Learn", + 2: "Listen", + 4: "Speak", + 8: "Standby", + 16: "Active", +} +_HSRP_ADVERTISE_TYPES = {1: "HSRP interface state"} +_HSRP_ADVERTISE_STATES = {1: "Active", 2: "Passive"} class HSRP(Packet): name = "HSRP" fields_desc = [ ByteField("version", 0), - ByteEnumField("opcode", 0, {0: "Hello", 1: "Coup", 2: "Resign", 3: "Advertise"}), # noqa: E501 - ConditionalField( - ByteEnumField("state", 16, { - 0: "Initial", - 1: "Learn", - 2: "Listen", - 4: "Speak", - 8: "Standby", - 16: "Active", - }), - _is_not_advertise - ), - ConditionalField(ByteField("hellotime", 3), _is_not_advertise), - ConditionalField(ByteField("holdtime", 10), _is_not_advertise), - ConditionalField(ByteField("priority", 120), _is_not_advertise), - ConditionalField(ByteField("group", 1), _is_not_advertise), - ConditionalField(ByteField("reserved", 0), _is_not_advertise), - ConditionalField(StrFixedLenField("auth", b"cisco" + b"\00" * 3, 8), _is_not_advertise), - ConditionalField(IPField("virtualIP", "192.168.1.1"), _is_not_advertise), - ConditionalField(ShortEnumField("adv_type", 1, {1: "HSRP interface state"}), _is_advertise), - ConditionalField(ShortField("adv_length", 10), _is_advertise), - ConditionalField(ByteEnumField("adv_state", 1, {1: "Active", 2: "Passive"}), _is_advertise), - ConditionalField(ByteField("adv_reserved1", 0), _is_advertise), - ConditionalField(ShortField("adv_active_grps", 0), _is_advertise), - ConditionalField(ShortField("adv_passive_grps", 0), _is_advertise), - ConditionalField(IntField("adv_reserved2", 0), _is_advertise) + ByteEnumField("opcode", 0, _HSRP_OPCODES), + ByteEnumField("state", 16, _HSRP_STATES), + ByteField("hellotime", 3), + ByteField("holdtime", 10), + ByteField("priority", 120), + ByteField("group", 1), + ByteField("reserved", 0), + StrFixedLenField("auth", b"cisco" + b"\00" * 3, 8), + IPField("virtualIP", "192.168.1.1") ] + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 2 and orb(_pkt[1:2]) == 3: + return HSRPAdvertise + return cls + def guess_payload_class(self, payload): if self.underlayer.len > 28: return HSRPmd5 @@ -67,6 +62,27 @@ def guess_payload_class(self, payload): return Packet.guess_payload_class(self, payload) +class HSRPAdvertise(Packet): + name = "HSRP Advertise" + fields_desc = [ + ByteField("version", 0), + ByteEnumField("opcode", 3, _HSRP_OPCODES), + ShortEnumField("type", 1, _HSRP_ADVERTISE_TYPES), + ShortField("length", 10), + ByteEnumField("state", 1, _HSRP_ADVERTISE_STATES), + ByteField("reserved1", 0), + ShortField("activegroups", 0), + ShortField("passivegroups", 0), + IntField("reserved2", 0), + ] + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 2 and orb(_pkt[1:2]) != 3: + return HSRP + return cls + + class HSRPmd5(Packet): name = "HSRP MD5 Authentication" fields_desc = [ diff --git a/test/scapy/layers/hsrp.uts b/test/scapy/layers/hsrp.uts index f0a89137822..00bbbb7340d 100644 --- a/test/scapy/layers/hsrp.uts +++ b/test/scapy/layers/hsrp.uts @@ -15,21 +15,20 @@ assert pkt[HSRPmd5].type == 4 and pkt[HSRPmd5].sourceip == defaddr = HSRP - Advertise build & dissection advertise_raw = b"\x00\x03\x00\x01\x00\x0e\x02\x00\x00\x00\x00\x01o\x00\x00\x00" pkt = HSRP(advertise_raw) +assert isinstance(pkt, HSRPAdvertise) assert pkt.opcode == 3 -assert pkt.adv_type == 1 and pkt.adv_length == 14 -assert pkt.adv_state == 2 and pkt.adv_reserved == 0 +assert pkt.type == 1 and pkt.length == 14 +assert pkt.state == 2 and pkt.reserved1 == 0 assert pkt.activegroups == 0 and pkt.passivegroups == 1 assert pkt.reserved2 == 0x6f000000 assert raw( - HSRP( - opcode=3, - adv_type=1, - adv_length=14, - adv_state=2, - adv_reserved=0, + HSRPAdvertise( + type=1, + length=14, + state=2, + reserved1=0, activegroups=0, passivegroups=1, reserved2=0x6f000000, ) ) == advertise_raw -