Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion pcapkit/protocols/data/internet/hip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
21 changes: 15 additions & 6 deletions pcapkit/protocols/internet/hip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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']),
Expand Down Expand Up @@ -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,
Expand All @@ -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``.
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions pcapkit/protocols/schema/internet/hip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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."""
Expand Down
87 changes: 87 additions & 0 deletions tests/protocols/internet/test_hip_fixed_bits_unit.py
Original file line number Diff line number Diff line change
@@ -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()
33 changes: 13 additions & 20 deletions tests/protocols/internet/test_hip_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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):
Expand Down
Loading