From b4d07c2af35789590c5799dd19e1e82a6f552eec Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Thu, 8 Oct 2026 22:56:08 -0400 Subject: [PATCH] fix(hip)!: parse and keep the two fixed header bits (#1459) RFC 7401 section 5.1 reserves the leading bit of Packet Type and the low-order bit of Version for SHIM6: "MUST be set as shown when sending and MUST be ignored when receiving". HIP.read raised ProtocolError unless they read 0 and 1. - read() no longer validates them and keeps them as the new required Data_HIP fields packet_fixed and version_fixed. - make() takes packet_fixed=0 and version_fixed=1, the RFC values, and _make_data passes them through, so from_data rebuilds byte-exact. - test_hip_unit: the two cases that pinned the rejection now pin the parse. --- pcapkit/protocols/data/internet/hip.py | 10 ++- pcapkit/protocols/internet/hip.py | 21 +++-- pcapkit/protocols/schema/internet/hip.py | 10 +-- .../internet/test_hip_fixed_bits_unit.py | 87 +++++++++++++++++++ tests/protocols/internet/test_hip_unit.py | 33 +++---- 5 files changed, 129 insertions(+), 32 deletions(-) create mode 100644 tests/protocols/internet/test_hip_fixed_bits_unit.py diff --git a/pcapkit/protocols/data/internet/hip.py b/pcapkit/protocols/data/internet/hip.py index 874af5fd70..a77a60e556 100644 --- a/pcapkit/protocols/data/internet/hip.py +++ b/pcapkit/protocols/data/internet/hip.py @@ -100,11 +100,19 @@ class HIP(Protocol): length: 'int' #: Packet type. type: 'Packet' + #: Fixed bit -- the leading bit of the ``Packet Type`` octet, sent as ``0`` + #: and ignored on receipt (:rfc:`7401#section-5.1`), carried verbatim so + #: that re-serialising reproduces it. + packet_fixed: 'int' #: Version. version: 'int' #: Reserved bits -- the three bits between ``Version`` and the fixed #: low-order bit, carried verbatim so that re-serialising reproduces them. reserved: 'int' + #: Fixed bit -- the low-order bit of the ``Version`` octet, sent as ``1`` + #: and ignored on receipt (:rfc:`7401#section-5.1`), carried verbatim so + #: that re-serialising reproduces it. + version_fixed: 'int' #: Checksum. chksum: 'bytes' #: Control @@ -118,7 +126,7 @@ class HIP(Protocol): #: HIP parameters. parameters: 'OrderedMultiDict[Enum_Parameter, Parameter]' - def __init__(self, next: 'TransType', length: 'int', type: 'Packet', version: 'int', reserved: 'int', chksum: 'bytes', control: 'Control', shit: 'int', rhit: 'int') -> 'None': ... # pylint: disable=unused-argument,multiple-statements,redefined-builtin,super-init-not-called,line-too-long + def __init__(self, next: 'TransType', length: 'int', type: 'Packet', packet_fixed: 'int', version: 'int', reserved: 'int', version_fixed: 'int', chksum: 'bytes', control: 'Control', shit: 'int', rhit: 'int') -> 'None': ... # pylint: disable=unused-argument,multiple-statements,redefined-builtin,super-init-not-called,line-too-long @info_final diff --git a/pcapkit/protocols/internet/hip.py b/pcapkit/protocols/internet/hip.py index 637ffc2a68..49e000460d 100644 --- a/pcapkit/protocols/internet/hip.py +++ b/pcapkit/protocols/internet/hip.py @@ -568,10 +568,9 @@ def read(self, length: 'Optional[int]' = None, *, extension: bool = False, **kwa length = len(self) schema = self.__header__ - if schema.pkt['bit_0'] != 0: - raise ProtocolError('HIP: invalid format') - if schema.ver['bit_1'] != 1: - raise ProtocolError('HIP: invalid format') + # NOTE: The two fixed bits are reserved for SHIM6 compatibility and + # "MUST be ignored when receiving" (:rfc:`7401#section-5.1`), so they + # are kept as read rather than validated. # Header Length below 4 cannot cover the two HITs, and would leave a # negative parameter length below. if schema.len < 4: @@ -581,8 +580,10 @@ def read(self, length: 'Optional[int]' = None, *, extension: bool = False, **kwa next=schema.next, length=schema.len * 8 + 8, type=Enum_Packet(schema.pkt['type']), + packet_fixed=schema.pkt['bit_0'], version=schema.ver['version'], reserved=schema.ver['reserved'], + version_fixed=schema.ver['bit_1'], chksum=schema.checksum, control=Data_Control( anonymous=bool(schema.control['anonymous']), @@ -616,8 +617,10 @@ def make(self, packet_default: 'Optional[int]' = None, packet_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long packet_reversed: 'bool' = False, + packet_fixed: 'int' = 0, version: 'int' = 2, version_reserved: 'int' = 0, + version_fixed: 'int' = 1, checksum: 'bytes' = b'\x00\x00', controls_anonymous: 'bool' = False, controls_reserved: 'int' = 0, @@ -637,9 +640,13 @@ def make(self, packet_default: Default value for HIP packet type. packet_namespace: Namespace for HIP packet type. packet_reversed: If the HIP packet type is reversed. + packet_fixed: The fixed leading bit of the ``Packet Type`` octet; + :rfc:`7401#section-5.1` prescribes ``0`` when sending. version: HIP version. version_reserved: The 3 reserved bits between ``Version`` and the fixed low-order bit. + version_fixed: The fixed low-order bit of the ``Version`` octet; + :rfc:`7401#section-5.1` prescribes ``1`` when sending. checksum: Checksum. controls_anonymous: If the sender is anonymous. controls_reserved: The 15 reserved high-order bits of ``Controls``. @@ -672,11 +679,11 @@ def make(self, next=next_value, # type: ignore[arg-type] len=length, pkt = { - 'bit_0': 0, + 'bit_0': packet_fixed, 'type': packet_value, }, ver = { - 'bit_1': 1, + 'bit_1': version_fixed, 'version': version, 'reserved': version_reserved, }, @@ -779,8 +786,10 @@ def _make_data(cls, data: 'Data_HIP') -> 'dict[str, Any]': # type: ignore[overr return { 'next': data.next, 'packet': data.type, + 'packet_fixed': data.packet_fixed, 'version': data.version, 'version_reserved': data.reserved, + 'version_fixed': data.version_fixed, 'checksum': data.chksum, 'controls_anonymous': data.control.anonymous, 'controls_reserved': data.control.reserved, diff --git a/pcapkit/protocols/schema/internet/hip.py b/pcapkit/protocols/schema/internet/hip.py index 7b1e6945ba..27d8932883 100644 --- a/pcapkit/protocols/schema/internet/hip.py +++ b/pcapkit/protocols/schema/internet/hip.py @@ -67,13 +67,13 @@ from pcapkit.protocols.protocol import ProtocolBase if SPHINX_TYPE_CHECKING: # pragma: no cover - from typing_extensions import Literal, TypedDict + from typing_extensions import TypedDict class PacketType(TypedDict): """Packet type.""" - #: Reversed bit. - bit_0: Literal[0] + #: Fixed bit, ``0`` when sent and ignored on receipt. + bit_0: int #: Packet type. type: int @@ -84,8 +84,8 @@ class VersionType(TypedDict): version: int #: Reserved bits. reserved: int - #: Reversed bit. - bit_1: Literal[1] + #: Fixed bit, ``1`` when sent and ignored on receipt. + bit_1: int class ControlsType(TypedDict): """Controls type.""" diff --git a/tests/protocols/internet/test_hip_fixed_bits_unit.py b/tests/protocols/internet/test_hip_fixed_bits_unit.py new file mode 100644 index 0000000000..632cea05a7 --- /dev/null +++ b/tests/protocols/internet/test_hip_fixed_bits_unit.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +"""HIP parses and keeps the two fixed header bits, whatever their value. + +GitHub issue #1459. The leading bit of the ``Packet Type`` octet and the +low-order bit of the ``Version`` octet are reserved for SHIM6 compatibility: +they "MUST be set as shown when sending and MUST be ignored when receiving" +(:rfc:`7401#section-5.1`). ``HIP.read`` raised ``ProtocolError`` unless they +read ``0`` and ``1``. + +The fix stops validating them, carries them as ``packet_fixed`` and +``version_fixed``, and lets ``make()`` write them, defaulting to the +RFC-prescribed values. + +Every case builds its own octets in memory and reads no capture. Classes are +imported inside each test, after :func:`~tests._support.reimport_once_per_class`. +""" + +import unittest + +from tests._support import reimport_once_per_class + +#: Header fields shared by every HIP packet built here. +HIP_BASE = { + 'next': 59, 'packet': 16, 'version': 2, 'checksum': b'\x00\x00', + 'controls_anonymous': False, 'shit': 0, 'rhit': 0, 'payload': b'', +} + +#: The two HITs that follow the 8-octet fixed header. +HITS = b'\x11' * 16 + b'\x22' * 16 + + +def _raw(pkt: int, ver: int) -> bytes: + """Build a 40-octet HIP header with the given ``Packet Type`` and ``Version`` octets.""" + return bytes([59, 4, pkt, ver]) + bytes(4) + HITS + + +class TestHIPFixedBits(unittest.TestCase): + """Pin the two fixed header bits across parse, rebuild and make.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def test_parse_keeps_fixed_bits(self) -> None: + from pcapkit.protocols.internet.hip import HIP + + cases = { + 'packet-fixed clear, version-fixed clear': (0x10, 0x20, 0, 0), + 'packet-fixed set, version-fixed set': (0x90, 0x21, 1, 1), + 'both inverted': (0x90, 0x20, 1, 0), + 'as sent': (0x10, 0x21, 0, 1), + 'inverted with reserved bits': (0x90, 0x2e, 1, 0), + } + for label, (pkt, ver, pfix, vfix) in cases.items(): + with self.subTest(label): + raw = _raw(pkt, ver) + parsed = HIP(raw, len(raw), extension=True) + self.assertEqual(parsed.info.packet_fixed, pfix) + self.assertEqual(parsed.info.version_fixed, vfix) + self.assertEqual(int(parsed.info.type), pkt & 0x7f) + self.assertEqual(parsed.info.version, ver >> 4) + self.assertEqual(parsed.info.reserved, (ver >> 1) & 0b111) + self.assertEqual(HIP.from_data(parsed.info).data, raw) + + def test_make_defaults_to_rfc_values(self) -> None: + from pcapkit.protocols.internet.hip import HIP + + built = HIP(extension=True, **HIP_BASE).data + self.assertEqual(built[2], 0x10) + self.assertEqual(built[3], 0x21) + parsed = HIP(built, len(built), extension=True) + self.assertEqual(parsed.info.packet_fixed, 0) + self.assertEqual(parsed.info.version_fixed, 1) + + def test_make_honours_explicit_values(self) -> None: + from pcapkit.protocols.internet.hip import HIP + + built = HIP(packet_fixed=1, version_fixed=0, extension=True, **HIP_BASE).data + self.assertEqual(built[2], 0x90) + self.assertEqual(built[3], 0x20) + parsed = HIP(built, len(built), extension=True) + self.assertEqual(parsed.info.packet_fixed, 1) + self.assertEqual(parsed.info.version_fixed, 0) + self.assertEqual(HIP.from_data(parsed.info).data, built) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/internet/test_hip_unit.py b/tests/protocols/internet/test_hip_unit.py index 0ef570c145..05e2e99429 100644 --- a/tests/protocols/internet/test_hip_unit.py +++ b/tests/protocols/internet/test_hip_unit.py @@ -30,8 +30,10 @@ def test_hip_index_length_alias_and_make_data(self) -> None: data = DummyDict( next=TransType.TCP, type=Packet.I1, + packet_fixed=0, version=2, reserved=0, + version_fixed=1, chksum=b'\x12\x34', control=DummyDict(anonymous=True, reserved=0), shit=b'source-hit', @@ -59,6 +61,8 @@ def test_hip_index_length_alias_and_make_data(self) -> None: self.assertEqual(values['packet'], Packet.I1) self.assertEqual(values['version'], 2) self.assertEqual(values['version_reserved'], 0) + self.assertEqual(values['packet_fixed'], 0) + self.assertEqual(values['version_fixed'], 1) self.assertEqual(values['checksum'], b'\x12\x34') self.assertEqual(values['controls_anonymous'], True) self.assertEqual(values['shit'], b'source-hit') @@ -107,35 +111,24 @@ def test_hip_read_make_post_init_and_parameter_list_edges(self) -> None: self.assertEqual(proto.read(length=40), 'decoded') decode.assert_called_once() - with self.assertRaises(ProtocolError): - proto.__header__ = hip_schema.HIP( - next=TransType.UDP, - len=4, - pkt={'bit_0': 1, 'type': Packet.I1}, - ver={'bit_1': 1, 'version': 2, 'reserved': 0}, - checksum=b'\x00\x00', - control={'anonymous': 0}, - shit=0, - rhit=0, - param=[], - payload=b'', - ) - proto.read(length=40, extension=True) - - with self.assertRaises(ProtocolError): + # The fixed bits are ignored on receipt (RFC 7401 section 5.1) and + # kept as read (#1459). + for bit_0, bit_1 in ((1, 1), (0, 0)): proto.__header__ = hip_schema.HIP( next=TransType.UDP, len=4, - pkt={'bit_0': 0, 'type': Packet.I1}, - ver={'bit_1': 0, 'version': 2, 'reserved': 0}, + pkt={'bit_0': bit_0, 'type': Packet.I1}, + ver={'bit_1': bit_1, 'version': 2, 'reserved': 0}, checksum=b'\x00\x00', - control={'anonymous': 0}, + control={'anonymous': 0, 'reserved': 0}, shit=0, rhit=0, param=[], payload=b'', ) - proto.read(length=40, extension=True) + parsed = proto.read(length=40, extension=True) + self.assertEqual(parsed.packet_fixed, bit_0) + self.assertEqual(parsed.version_fixed, bit_1) proto._extf = True with self.assertRaises(UnsupportedCall):