diff --git a/pcapkit/protocols/data/internet/hip.py b/pcapkit/protocols/data/internet/hip.py index 0dbec4b870..6eb8490c50 100644 --- a/pcapkit/protocols/data/internet/hip.py +++ b/pcapkit/protocols/data/internet/hip.py @@ -177,13 +177,16 @@ class Locator(Data): length: 'int' #: Preferred flag. preferred: 'bool' + #: Reserved bits -- the 7 high-order bits of the flags octet, carried + #: verbatim so that re-serialising reproduces them. + reserved: 'int' #: Locator lifetime. lifetime: 'timedelta' #: Locator data. locator: 'LocatorData | IPv6Address' if TYPE_CHECKING: - def __init__(self, traffic: 'int', type: 'int', length: 'int', preferred: 'bool', lifetime: 'timedelta', locator: 'LocatorData | IPv6Address') -> 'None': ... # pylint: disable=super-init-not-called,unused-argument,multiple-statements,redefined-builtin,line-too-long + def __init__(self, traffic: 'int', type: 'int', length: 'int', preferred: 'bool', reserved: 'int', lifetime: 'timedelta', locator: 'LocatorData | IPv6Address') -> 'None': ... # pylint: disable=super-init-not-called,unused-argument,multiple-statements,redefined-builtin,line-too-long @info_final diff --git a/pcapkit/protocols/internet/hip.py b/pcapkit/protocols/internet/hip.py index a1b5fa0a18..79dca4159e 100644 --- a/pcapkit/protocols/internet/hip.py +++ b/pcapkit/protocols/internet/hip.py @@ -247,6 +247,8 @@ class Locator(TypedDict): type: 'int' #: Preferred flag. preferred: 'bool' + #: Reserved bits of the flags octet. + reserved: 'NotRequired[int]' #: Lifetime. lifetime: 'timedelta | int' #: IP address. @@ -1031,6 +1033,7 @@ def _read_locator(locator: 'Schema_Locator') -> 'Data_LocatorData | IPv6Address' type=_loct, length=_locl, preferred=bool(_resp['preferred']), + reserved=_resp['reserved'], lifetime=datetime.timedelta(seconds=_life), locator=_lobj, )) @@ -3157,6 +3160,7 @@ def _make_locator(locator: 'Optional[Data_Locator]' = None, *, traffic: 'int' = 0, type: 'int' = 0, preferred: 'bool' = False, + reserved: 'int' = 0, lifetime: 'timedelta | int' = 0, ip: 'IPv6Address | bytes | int | str' = '::', spi: 'Optional[int]' = None, @@ -3168,6 +3172,7 @@ def _make_locator(locator: 'Optional[Data_Locator]' = None, *, traffic: traffic type type: locator type preferred: preferred flag + reserved: the 7 reserved high-order bits of the flags octet lifetime: lifetime ip: IP address spi: SPI @@ -3193,6 +3198,7 @@ def _make_locator(locator: 'Optional[Data_Locator]' = None, *, type = locator.type length = locator.length // 4 preferred = locator.preferred + reserved = locator.reserved lifetime = math.floor(locator.lifetime.total_seconds()) else: # NOTE: Through ``parse_ip_address`` because the locator is packed @@ -3225,6 +3231,7 @@ def _make_locator(locator: 'Optional[Data_Locator]' = None, *, type=type, len=length, flags={ + 'reserved': reserved, 'preferred': preferred, }, lifetime=lifetime, diff --git a/pcapkit/protocols/schema/internet/hip.py b/pcapkit/protocols/schema/internet/hip.py index f50391f7a1..5f3c93fe6f 100644 --- a/pcapkit/protocols/schema/internet/hip.py +++ b/pcapkit/protocols/schema/internet/hip.py @@ -96,6 +96,8 @@ class ControlsType(TypedDict): class LocatorFlags(TypedDict): """Locator flags.""" + #: Reserved bits. + reserved: int #: Preferred flag. preferred: bool @@ -594,6 +596,7 @@ class Locator(Schema): flags: 'LocatorFlags' = BitField( length=1, namespace={ + 'reserved': (0, 7), 'preferred': (7, 1), }, ) diff --git a/tests/protocols/internet/test_hip_locator_flags_unit.py b/tests/protocols/internet/test_hip_locator_flags_unit.py new file mode 100644 index 0000000000..fdca4b425f --- /dev/null +++ b/tests/protocols/internet/test_hip_locator_flags_unit.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +"""HIP ``LOCATOR_SET`` keeps the seven reserved bits of each locator's flags octet. + +GitHub issue #1365. :rfc:`8046#section-4` lays the fourth octet of a locator out +as ``Reserved`` (7 bits) followed by ``P`` (1 bit). ``Locator.flags`` declared only +``preferred: (7, 1)``, so the seven high-order bits were dropped on rebuild: flags +``0xfe`` parsed and rebuilt through ``from_data`` as ``0x00``. The fix names them +``reserved: (0, 7)``, per the #654 per-site policy, and carries them through +:class:`~pcapkit.protocols.data.internet.hip.Locator` and ``make()``. + +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 +import warnings + +from tests._support import reimport_once_per_class + +#: Header fields shared by every HIP packet built here. +HIP_BASE = { + 'next': 6, 'packet': 1, 'version': 2, 'checksum': b'\x00\x00', + 'controls_anonymous': False, 'shit': 0, 'rhit': 0, 'payload': b'', +} + +#: Offset of the first locator's flags octet: the 40-octet HIP header, the +#: parameter's ``Type`` and ``Length``, then ``Traffic``, ``Type`` and ``Length``. +FLAGS_OFFSET = 40 + 4 + 3 + + +class TestHIPLocatorFlags(unittest.TestCase): + """Pin every bit of the locator flags octet across parse, rebuild and make.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def _frame(self, flags: int) -> bytes: + """Build a one-locator ``LOCATOR_SET`` packet whose flags octet is ``flags``.""" + from pcapkit.const.hip.parameter import Parameter + from pcapkit.protocols.internet.hip import HIP + + octets = bytearray(HIP( + parameters=[(Parameter.LOCATOR_SET, + {'locator_set': [{'ip': '2001:db8::1'}]})], + extension=True, **HIP_BASE).data) + self.assertEqual(octets[FLAGS_OFFSET], 0) + octets[FLAGS_OFFSET] = flags + return bytes(octets) + + def _parse(self, frame: bytes): # type: ignore[no-untyped-def] + from pcapkit.protocols.internet.hip import HIP + + with warnings.catch_warnings(): + # Nested ``Locator`` schemas warn about their placeholder length; + # that is pre-existing noise unrelated to the flags octet. + warnings.simplefilter('ignore') + return HIP(frame, len(frame), extension=True) + + def test_from_data_rebuilds_every_flags_bit(self) -> None: + from pcapkit.const.hip.parameter import Parameter + from pcapkit.protocols.internet.hip import HIP + + for flags in (0x00, 0x01, 0xfe, 0xff, 0x80, 0x55): + with self.subTest(flags=hex(flags)): + frame = self._frame(flags) + parsed = self._parse(frame) + rebuilt = HIP.from_data(parsed.info) + self.assertEqual(rebuilt.data[FLAGS_OFFSET], flags) + self.assertEqual(rebuilt.data, frame) + + locator = parsed.info.parameters[Parameter.LOCATOR_SET].locator_set[0] + self.assertEqual(locator.reserved, flags >> 1) + self.assertEqual(locator.preferred, bool(flags & 1)) + + def test_make_writes_reserved_from_keywords(self) -> None: + from pcapkit.const.hip.parameter import Parameter + from pcapkit.protocols.internet.hip import HIP + + built = HIP( + parameters=[(Parameter.LOCATOR_SET, + {'locator_set': [{'ip': '2001:db8::1', 'reserved': 0x7f, + 'preferred': False}]})], + extension=True, **HIP_BASE) + self.assertEqual(built.data[FLAGS_OFFSET], 0xfe) + self.assertEqual(built.data, self._frame(0xfe)) + + def test_make_defaults_reserved_to_zero(self) -> None: + from pcapkit.const.hip.parameter import Parameter + from pcapkit.protocols.internet.hip import HIP + + built = HIP( + parameters=[(Parameter.LOCATOR_SET, + {'locator_set': [{'ip': '2001:db8::1', 'preferred': True}]})], + extension=True, **HIP_BASE) + self.assertEqual(built.data[FLAGS_OFFSET], 0x01) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/internet/test_hip_locator_set_length_unit.py b/tests/protocols/internet/test_hip_locator_set_length_unit.py index 2757965a9c..c83a7d64a6 100644 --- a/tests/protocols/internet/test_hip_locator_set_length_unit.py +++ b/tests/protocols/internet/test_hip_locator_set_length_unit.py @@ -231,7 +231,7 @@ def test_padding_comes_from_the_parameters_length_not_the_locators(self) -> None from pcapkit.protocols.schema.internet.hip import (Locator, LocatorSetParameter, locator_set_padding_len) - locator = Locator(traffic=0, type=0, len=4, flags={'preferred': False}, + locator = Locator(traffic=0, type=0, len=4, flags={'reserved': 0, 'preferred': False}, lifetime=0, value=b'\x20\x01\x0d\xb8' + b'\x00' * 11 + b'\x01') schema = LocatorSetParameter(type=Parameter.LOCATOR_SET, len=9, locators=[locator]) diff --git a/tests/protocols/internet/test_hip_unit.py b/tests/protocols/internet/test_hip_unit.py index 0176e96449..d5afc0a1f7 100644 --- a/tests/protocols/internet/test_hip_unit.py +++ b/tests/protocols/internet/test_hip_unit.py @@ -492,10 +492,10 @@ def test_hip_parameter_readers_cover_simple_models_and_guards(self) -> None: len=48, locators=[ hip_schema.Locator(traffic=1, type=0, len=4, - flags={'preferred': 1}, lifetime=5, + flags={'reserved': 0, 'preferred': 1}, lifetime=5, value=ip_address('2001:db8::1')), hip_schema.Locator(traffic=2, type=1, len=5, - flags={'preferred': 0}, lifetime=6, + flags={'reserved': 0, 'preferred': 0}, lifetime=6, value=hip_schema.LocatorData( spi=7, ip=ip_address('2001:db8::2'), @@ -513,7 +513,7 @@ def test_hip_parameter_readers_cover_simple_models_and_guards(self) -> None: type=Parameter.LOCATOR_SET, len=8, locators=[hip_schema.Locator(traffic=1, type=3, len=4, - flags={'preferred': 0}, lifetime=1, + flags={'reserved': 0, 'preferred': 0}, lifetime=1, value=ip_address('2001:db8::1'))], ), version=2, @@ -1392,10 +1392,10 @@ def test_hip_parameter_constructors_cover_data_model_and_default_paths(self) -> ).counter, 2) locator_ipv6 = hip_data.Locator(traffic=1, type=0, length=16, - preferred=True, lifetime=dt1, + preferred=True, reserved=0, lifetime=dt1, locator=ip_address('2001:db8::1')) locator_data = hip_data.Locator(traffic=2, type=1, length=20, - preferred=False, lifetime=dt2, + preferred=False, reserved=0, lifetime=dt2, locator=hip_data.LocatorData( spi=7, ip=ip_address('2001:db8::2'), @@ -1417,7 +1417,7 @@ def test_hip_parameter_constructors_cover_data_model_and_default_paths(self) -> critical=False, length=8, locator_set=(hip_data.Locator( - traffic=1, type=0, length=4, preferred=False, + traffic=1, type=0, length=4, preferred=False, reserved=0, lifetime=dt1, locator=b'bad', ),), ),