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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ Layer-limited extraction: `layer='link'` no longer sets the termination flag on
- **a breaking change to** `NTPTimestamp`: it and `PMIPv6Timestamp` are `Info` classes in `pcapkit.protocols.data.internet.mh` rather than tuples, and `NTPTimestamp` keeps no alias at its 1.4.x home in `pcapkit.protocols.internet.mh`. As Mappings they no longer index, unpack, compare or hash as a tuple does; `.seconds` and `.fraction` still work, and the dumpers write both as keys ([#1886](https://github.com/JarryShaw/PyPCAPKit/issues/1886), [#1887](https://github.com/JarryShaw/PyPCAPKit/pull/1887)).
- **a breaking change to** rebuilding from `to_dict()`: the payload is dispatched only from the `__next_name__` and `__next_type__` keys, and the IPv6 extension-header chain only from `__exthdr__`, never from key order, which on PyPy follows an earlier instance's attribute layout and so picked the wrong layer. `to_dict()`, iteration and `len()` carry the dunder keys a new `Info.__included__` names, and `Info.__excluded__` only keeps a key out of `repr`, `str` and the dumps. A hand-written dict needs those keys, `__short_read__` among them, to rebuild, and a custom `Info` lists its own dunder keys in `__included__` ([#2003](https://github.com/JarryShaw/PyPCAPKit/issues/2003), [#2014](https://github.com/JarryShaw/PyPCAPKit/pull/2014)).
- **a breaking change to** `PCAPNG.context`: the block's section `Context`, which it returned from 1.0.0 through 1.4.x, is `PCAPNG.section_context`, with no alias, and `context` is the caller-supplied `ContextRegistry` every protocol inherits from `ProtocolBase`, on a Section Header Block too. `PCAPNG.section_context` raises `UnsupportedCall` where `context` did: on a block that holds no section, such as a Section Header Block the `PCAPNG` engine has not assigned the section it opens. Code reading `block.context` for the section reads `block.section_context` ([#2172](https://github.com/JarryShaw/PyPCAPKit/issues/2172), [#2205](https://github.com/JarryShaw/PyPCAPKit/pull/2205)).
- **a breaking change to** HIP's `REG_FAILED` parameter: its first octet is the Failure Type, and it carries no lifetime ([RFC 8003 Section 4.5](https://datatracker.ietf.org/doc/html/rfc8003#section-4.5)), so `pcapkit.protocols.data.internet.hip.RegFailedParameter` replaces `lifetime` with `failure_type`, a `RegistrationFailure`, with no alias, and its `reg_type` holds `Registration` values rather than failure types. `HIP._make_param_reg_failed()` takes `failure_type` in place of `lifetime`, and `reg_failed` takes registration types. The octets are unchanged ([#2309](https://github.com/JarryShaw/PyPCAPKit/issues/2309)).

#### Fixed

Expand Down
9 changes: 9 additions & 0 deletions docs/source/changelog/1.5.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1489,6 +1489,15 @@ Changed
:class:`~pcapkit.foundation.engines.pcapng.PCAPNG` engine has not assigned the section
it opens. Code reading ``block.context`` for the section reads
``block.section_context`` (:issue:`2172`, :pr:`2205`).
* **a breaking change to** HIP's ``REG_FAILED`` parameter: its first octet is the
Failure Type, and it carries no lifetime (:rfc:`8003#section-4.5`), so
:class:`pcapkit.protocols.data.internet.hip.RegFailedParameter` replaces ``lifetime``
with ``failure_type``, a :class:`~pcapkit.const.hip.registration_failure.RegistrationFailure`,
with no alias, and its ``reg_type`` holds
:class:`~pcapkit.const.hip.registration.Registration` values rather than failure
types. :meth:`.HIP._make_param_reg_failed` takes ``failure_type`` in place of
``lifetime``, and ``reg_failed`` takes registration types. The octets are unchanged
(:issue:`2309`).

Fixed
~~~~~
Expand Down
20 changes: 15 additions & 5 deletions pcapkit/protocols/data/internet/hip.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,15 +610,25 @@ def __init__(self, type: 'Enum_Parameter', critical: 'bool', length: 'int', life

@info_final
class RegFailedParameter(Parameter):
"""Data model for HIP ``REG_FAILED`` parameter."""
"""Data model for HIP ``REG_FAILED`` parameter.

.. versionchanged:: 1.5.0

``lifetime`` is replaced by :attr:`failure_type`, with no alias, and
:attr:`reg_type` holds :class:`~pcapkit.const.hip.registration.Registration`
values: ``REG_FAILED`` carries a Failure Type octet ahead of its
registration types and no lifetime (:rfc:`8003#section-4.5`,
:issue:`2309`).

"""

#: Registration lifetime.
lifetime: 'timedelta'
#: Registration failure type.
reg_type: 'tuple[RegistrationFailure, ...]'
failure_type: 'RegistrationFailure'
#: Registration types that failed with :attr:`failure_type`.
reg_type: 'tuple[Registration, ...]'

if TYPE_CHECKING:
def __init__(self, type: 'Enum_Parameter', critical: 'bool', length: 'int', lifetime: 'timedelta', reg_type: 'tuple[RegistrationFailure, ...]') -> 'None': ... # pylint: disable=unused-argument,multiple-statements,redefined-builtin,super-init-not-called,line-too-long
def __init__(self, type: 'Enum_Parameter', critical: 'bool', length: 'int', failure_type: 'RegistrationFailure', reg_type: 'tuple[Registration, ...]') -> 'None': ... # pylint: disable=unused-argument,multiple-statements,redefined-builtin,super-init-not-called,line-too-long


@info_final
Expand Down
44 changes: 28 additions & 16 deletions pcapkit/protocols/internet/hip.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
from pcapkit.const.hip.notify_message import NotifyMessage as Enum_NotifyMessage
from pcapkit.const.hip.packet import Packet as Enum_Packet
from pcapkit.const.hip.parameter import Parameter as Enum_Parameter
from pcapkit.const.hip.registration_failure import RegistrationFailure as Enum_RegistrationFailure
from pcapkit.const.reg.transtype import TransType as Enum_TransType
from pcapkit.corekit.fields.ipaddress import parse_ip_address
from pcapkit.corekit.multidict import OrderedMultiDict
Expand Down Expand Up @@ -217,8 +218,6 @@
from pcapkit.const.hip.esp_transform_suite import ESPTransformSuite as Enum_ESPTransformSuite
from pcapkit.const.hip.nat_traversal import NATTraversal as Enum_NATTraversal
from pcapkit.const.hip.registration import Registration as Enum_Registration
from pcapkit.const.hip.registration_failure import \
RegistrationFailure as Enum_RegistrationFailure
from pcapkit.const.hip.suite import Suite as Enum_Suite
from pcapkit.const.hip.transport import Transport as Enum_Transport
from pcapkit.corekit.protochain import ProtoChain
Expand Down Expand Up @@ -1998,7 +1997,7 @@ def _read_param_reg_failed(self, schema: 'Schema_RegFailedParameter', *, version
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Lifetime | Reg Type #1 | Reg Type #2 | Reg Type #3 |
| Failure Type | Reg Type #1 | Reg Type #2 | Reg Type #3 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ... | ... | Reg Type #n | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Padding +
Expand All @@ -2018,7 +2017,7 @@ def _read_param_reg_failed(self, schema: 'Schema_RegFailedParameter', *, version
type=schema.type,
critical=bool(schema.type & 0b1),
length=parameter_total_len(schema.len),
lifetime=datetime.timedelta(seconds=schema.lifetime),
failure_type=schema.failure_type,
reg_type=tuple(schema.reg_failed),
)
return reg_failed
Expand Down Expand Up @@ -4432,33 +4431,48 @@ def _make_param_reg_response(self, code: 'Enum_Parameter', param: 'Optional[Data

def _make_param_reg_failed(self, code: 'Enum_Parameter', param: 'Optional[Data_RegFailedParameter]' = None, *, # pylint: disable=unused-argument
version: 'int',
lifetime: 'int | timedelta' = 0,
reg_failed: 'Optional[list[Enum_RegistrationFailure | StdlibEnum | AenumEnum | str | int]]' = None,
failure_type: 'Enum_RegistrationFailure | StdlibEnum | AenumEnum | str | int' = Enum_RegistrationFailure.Registration_requires_additional_credentials, # pylint: disable=line-too-long
failure_type_default: 'Optional[int]' = None,
failure_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long
failure_type_reversed: 'bool' = False,
reg_failed: 'Optional[list[Enum_Registration | StdlibEnum | AenumEnum | str | int]]' = None,
reg_failed_default: 'Optional[int]' = None,
reg_failed_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long
reg_failed_reversed: 'bool' = False,
**kwargs: 'Any') -> 'Schema_RegFailedParameter':
"""Make HIP ``REG_FAILED`` parameter.

.. versionchanged:: 1.5.0

``lifetime`` is replaced by ``failure_type``, with no alias, and
``reg_failed`` takes registration types rather than failure types
(:issue:`2309`).

Args:
code: parameter code
param: parameter data
version: HIP protocol version
lifetime: lifetime
reg_failed: registration failure list
reg_failed_default: default registration failure
reg_failed_namespace: registration failure namespace
reg_failed_reversed: reverse registration failure namespace
failure_type: registration failure type
failure_type_default: default registration failure type
failure_type_namespace: registration failure type namespace
failure_type_reversed: reverse registration failure type namespace
reg_failed: list of registration types that failed
reg_failed_default: default registration type
reg_failed_namespace: registration type namespace
reg_failed_reversed: reverse registration type namespace
**kwargs: arbitrary keyword arguments

Returns:
HIP parameter schema.

"""
if param is not None:
reg_type = cast('list[Enum_RegistrationFailure]', param.reg_type)
lt = math.floor(param.lifetime.total_seconds())
failure = param.failure_type
reg_type = cast('list[Enum_Registration]', param.reg_type)
else:
failure = self._make_index(failure_type, failure_type_default, namespace=failure_type_namespace, # type: ignore[assignment]
reversed=failure_type_reversed, pack=False)

if reg_failed is None:
reg_failed = []

Expand All @@ -4467,12 +4481,10 @@ def _make_param_reg_failed(self, code: 'Enum_Parameter', param: 'Optional[Data_R
reg_type.append(self._make_index(reg, reg_failed_default, namespace=reg_failed_namespace, # type: ignore[arg-type]
reversed=reg_failed_reversed, pack=False))

lt = lifetime if isinstance(lifetime, int) else math.floor(lifetime.total_seconds())

return Schema_RegFailedParameter(
type=code,
len=1 + len(reg_type),
lifetime=lt,
failure_type=failure,
reg_failed=reg_type,
)

Expand Down
17 changes: 9 additions & 8 deletions pcapkit/protocols/schema/internet/hip.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,9 @@ def registration_type_list_len(pkt: 'dict[str, Any]') -> 'int':
Used by the ``reg_request``, ``reg_response`` and ``reg_failed`` fields of
:class:`RegRequestParameter`, :class:`RegResponseParameter` and
:class:`RegFailedParameter` respectively, each of which follows a single
``lifetime`` octet with a list of registration type octets sized by the
remainder of the parameter.
prefix octet (the ``lifetime`` of the first two, the ``failure_type`` of
the last) with a list of registration type octets sized by the remainder
of the parameter.

Args:
pkt: Parameter unpacked schema.
Expand All @@ -197,7 +198,7 @@ def registration_type_list_len(pkt: 'dict[str, Any]') -> 'int':

Raises:
FieldValueError: If the parameter's ``Length`` on the wire is too
short to hold the ``lifetime`` octet already read, which would
short to hold the prefix octet already read, which would
otherwise underflow the list length below zero.

"""
Expand Down Expand Up @@ -1171,18 +1172,18 @@ def __init__(self, type: 'Enum_Parameter', len: 'int', lifetime: 'int', reg_resp
class RegFailedParameter(Parameter, code=Enum_Parameter.REG_FAILED):
"""Header schema for HIP ``REG_FAILED`` parameters."""

#: Lifetime.
lifetime: 'int' = UInt8Field()
#: Registration failure type.
failure_type: 'Enum_RegistrationFailure' = EnumField(length=1, namespace=Enum_RegistrationFailure)
#: Registration types.
reg_failed: 'list[Enum_RegistrationFailure]' = ListField(
reg_failed: 'list[Enum_Registration]' = ListField(
length=registration_type_list_len,
item_type=EnumField(length=1, namespace=Enum_RegistrationFailure),
item_type=EnumField(length=1, namespace=Enum_Registration),
)
#: Padding.
padding: 'bytes' = PaddingField(length=parameter_padding_len)

if TYPE_CHECKING:
def __init__(self, type: 'Enum_Parameter', len: 'int', lifetime: 'int', reg_failed: 'list[Enum_RegistrationFailure]') -> 'None': ...
def __init__(self, type: 'Enum_Parameter', len: 'int', failure_type: 'Enum_RegistrationFailure', reg_failed: 'list[Enum_Registration]') -> 'None': ...


@schema_final
Expand Down
71 changes: 71 additions & 0 deletions tests/protocols/internet/test_hip_roundtrip_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
:class:`~datetime.timedelta`, so 197 of the 256 octets could not be rebuilt.
* #1328 -- the reserved bits of ``Controls`` and of ``ESP_INFO`` were written
back as zero.
* #2309 -- ``REG_FAILED`` read its Failure Type octet as a ``lifetime`` and its
Reg Type octets as failure types, rather than as :rfc:`8003#section-4.5`
lays them out.

Every case builds its own octets in memory and reads no capture. :class:`HIP`
is imported inside each test, after :func:`~tests._support.reimport_once_per_class`,
Expand All @@ -35,6 +38,13 @@ def packet(params: bytes = b'', controls: bytes = b'\x00\x00') -> bytes:
+ controls + bytes(32) + params)


def reg_failed(failure_type: int, reg_types: 'list[int]') -> bytes:
"""Return a ``REG_FAILED`` parameter, zero-padded to 8 octets."""
body = (936).to_bytes(2, 'big') + (1 + len(reg_types)).to_bytes(2, 'big') \
+ bytes([failure_type, *reg_types])
return body + bytes(-len(body) % 8)


class TestHIPRoundTrip(unittest.TestCase):
"""Pin the rebuilt octets of parsed HIP packets."""

Expand Down Expand Up @@ -106,6 +116,67 @@ def test_esp_info_reserved_round_trips(self) -> None:
self.assertRoundTrips(packet(bytes.fromhex('0041000c' 'abcd' '0102')
+ bytes.fromhex('0a0b0c0d' '01020304')))

def test_reg_failed_reads_failure_type_then_registration_types(self) -> None:
"""#2309: the first octet is a Failure Type, and the rest are Reg Types."""
from pcapkit.const.hip.parameter import Parameter
from pcapkit.const.hip.registration import Registration
from pcapkit.const.hip.registration_failure import RegistrationFailure
from pcapkit.protocols.internet.hip import HIP

wire = packet(reg_failed(1, [1, 2, 3]))
param = HIP(io.BytesIO(wire), len(wire), extension=True).info.parameters[Parameter.REG_FAILED]

# Both registries are integer enums, so ``==`` alone cannot tell a
# Failure Type from a Reg Type of the same value: compare the types too.
self.assertIs(type(param.failure_type), RegistrationFailure)
self.assertEqual(param.failure_type, RegistrationFailure.Registration_type_unavailable)
self.assertEqual([type(reg) for reg in param.reg_type], [Registration] * 3)
self.assertEqual(param.reg_type, (Registration.RENDEZVOUS,
Registration.RELAY_UDP_HIP,
Registration.RELAY_UDP_ESP))
# No ``lifetime`` alias is kept: ``REG_FAILED`` has no lifetime.
self.assertNotIn('lifetime', param)
self.assertFalse(hasattr(param, 'lifetime'))

def test_reg_failed_parse_then_rebuild_is_byte_exact(self) -> None:
"""#2309: every Failure Type, with several Reg Types, rebuilds unchanged."""
for failure_type in (0, 1, 2, 3, 9, 201, 255):
for reg_types in ([], [1], [1, 2, 3], [0, 4, 200, 255, 2, 1, 3]):
with self.subTest(failure_type=failure_type, reg_types=reg_types):
self.assertRoundTrips(packet(reg_failed(failure_type, reg_types)))

def test_reg_failed_make_then_parse_is_byte_exact(self) -> None:
"""#2309: ``failure_type`` and ``reg_failed`` build the RFC 8003 octets."""
from pcapkit.const.hip.parameter import Parameter
from pcapkit.const.hip.registration import Registration
from pcapkit.const.hip.registration_failure import RegistrationFailure
from pcapkit.protocols.internet.hip import HIP

for failure_type, reg_types, octets in (
(RegistrationFailure.Insufficient_resources,
[Registration.RELAY_UDP_ESP, Registration.RENDEZVOUS], (2, [3, 1])),
(3, [1, 2, 3], (3, [1, 2, 3])),
(RegistrationFailure.Registration_requires_additional_credentials, [], (0, [])),
):
with self.subTest(failure_type=failure_type, reg_types=reg_types):
wire = HIP.__new__(HIP).make(parameters=[(Parameter.REG_FAILED, {
'failure_type': failure_type,
'reg_failed': reg_types,
})]).pack()
self.assertEqual(wire[40:].hex(), reg_failed(*octets).hex())

param = HIP(io.BytesIO(wire), len(wire), extension=True).info.parameters[Parameter.REG_FAILED]
self.assertIs(type(param.failure_type), RegistrationFailure)
self.assertEqual(param.failure_type, octets[0])
self.assertEqual([type(reg) for reg in param.reg_type],
[Registration] * len(octets[1]))
self.assertEqual(list(param.reg_type), octets[1])
self.assertRoundTrips(wire)

# The default Failure Type is octet 0, as the removed ``lifetime`` was.
wire = HIP.__new__(HIP).make(parameters=[(Parameter.REG_FAILED, {})]).pack()
self.assertEqual(wire[40:].hex(), reg_failed(0, []).hex())


if __name__ == '__main__':
unittest.main()
Loading
Loading