Skip to content

Commit 9dd46d8

Browse files
authored
fix(hip)!: read REG_FAILED's Failure Type and Reg Types as RFC 8003 lays them out (#2309) (#2336)
RFC 8003 section 4.5 gives REG_FAILED a Failure Type octet followed by Reg Type octets, and no lifetime. pcapkit read the first octet as a `lifetime` timedelta and decoded the Reg Types with the RegistrationFailure registry. Replace `lifetime` with `failure_type` (a RegistrationFailure) in the schema, the data model and the maker, with no alias, and decode `reg_type` / `reg_failed` with the Registration registry. The octets are unchanged. Tests: test_hip_roundtrip_unit pins the parsed types, parse->rebuild over Failure Types 0-255 with several Reg Types, and make->parse octets.
1 parent b7b02c0 commit 9dd46d8

7 files changed

Lines changed: 155 additions & 45 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ Layer-limited extraction: `layer='link'` no longer sets the termination flag on
225225
- **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)).
226226
- **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)).
227227
- **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)).
228+
- **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)).
228229

229230
#### Fixed
230231

‎docs/source/changelog/1.5.0.rst‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,6 +1489,15 @@ Changed
14891489
:class:`~pcapkit.foundation.engines.pcapng.PCAPNG` engine has not assigned the section
14901490
it opens. Code reading ``block.context`` for the section reads
14911491
``block.section_context`` (:issue:`2172`, :pr:`2205`).
1492+
* **a breaking change to** HIP's ``REG_FAILED`` parameter: its first octet is the
1493+
Failure Type, and it carries no lifetime (:rfc:`8003#section-4.5`), so
1494+
:class:`pcapkit.protocols.data.internet.hip.RegFailedParameter` replaces ``lifetime``
1495+
with ``failure_type``, a :class:`~pcapkit.const.hip.registration_failure.RegistrationFailure`,
1496+
with no alias, and its ``reg_type`` holds
1497+
:class:`~pcapkit.const.hip.registration.Registration` values rather than failure
1498+
types. :meth:`.HIP._make_param_reg_failed` takes ``failure_type`` in place of
1499+
``lifetime``, and ``reg_failed`` takes registration types. The octets are unchanged
1500+
(:issue:`2309`).
14921501

14931502
Fixed
14941503
~~~~~

‎pcapkit/protocols/data/internet/hip.py‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -610,15 +610,25 @@ def __init__(self, type: 'Enum_Parameter', critical: 'bool', length: 'int', life
610610

611611
@info_final
612612
class RegFailedParameter(Parameter):
613-
"""Data model for HIP ``REG_FAILED`` parameter."""
613+
"""Data model for HIP ``REG_FAILED`` parameter.
614+
615+
.. versionchanged:: 1.5.0
616+
617+
``lifetime`` is replaced by :attr:`failure_type`, with no alias, and
618+
:attr:`reg_type` holds :class:`~pcapkit.const.hip.registration.Registration`
619+
values: ``REG_FAILED`` carries a Failure Type octet ahead of its
620+
registration types and no lifetime (:rfc:`8003#section-4.5`,
621+
:issue:`2309`).
622+
623+
"""
614624

615-
#: Registration lifetime.
616-
lifetime: 'timedelta'
617625
#: Registration failure type.
618-
reg_type: 'tuple[RegistrationFailure, ...]'
626+
failure_type: 'RegistrationFailure'
627+
#: Registration types that failed with :attr:`failure_type`.
628+
reg_type: 'tuple[Registration, ...]'
619629

620630
if TYPE_CHECKING:
621-
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
631+
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
622632

623633

624634
@info_final

‎pcapkit/protocols/internet/hip.py‎

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from pcapkit.const.hip.notify_message import NotifyMessage as Enum_NotifyMessage
5050
from pcapkit.const.hip.packet import Packet as Enum_Packet
5151
from pcapkit.const.hip.parameter import Parameter as Enum_Parameter
52+
from pcapkit.const.hip.registration_failure import RegistrationFailure as Enum_RegistrationFailure
5253
from pcapkit.const.reg.transtype import TransType as Enum_TransType
5354
from pcapkit.corekit.fields.ipaddress import parse_ip_address
5455
from pcapkit.corekit.multidict import OrderedMultiDict
@@ -217,8 +218,6 @@
217218
from pcapkit.const.hip.esp_transform_suite import ESPTransformSuite as Enum_ESPTransformSuite
218219
from pcapkit.const.hip.nat_traversal import NATTraversal as Enum_NATTraversal
219220
from pcapkit.const.hip.registration import Registration as Enum_Registration
220-
from pcapkit.const.hip.registration_failure import \
221-
RegistrationFailure as Enum_RegistrationFailure
222221
from pcapkit.const.hip.suite import Suite as Enum_Suite
223222
from pcapkit.const.hip.transport import Transport as Enum_Transport
224223
from pcapkit.corekit.protochain import ProtoChain
@@ -1998,7 +1997,7 @@ def _read_param_reg_failed(self, schema: 'Schema_RegFailedParameter', *, version
19981997
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
19991998
| Type | Length |
20001999
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2001-
| Lifetime | Reg Type #1 | Reg Type #2 | Reg Type #3 |
2000+
| Failure Type | Reg Type #1 | Reg Type #2 | Reg Type #3 |
20022001
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
20032002
| ... | ... | Reg Type #n | |
20042003
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Padding +
@@ -2018,7 +2017,7 @@ def _read_param_reg_failed(self, schema: 'Schema_RegFailedParameter', *, version
20182017
type=schema.type,
20192018
critical=bool(schema.type & 0b1),
20202019
length=parameter_total_len(schema.len),
2021-
lifetime=datetime.timedelta(seconds=schema.lifetime),
2020+
failure_type=schema.failure_type,
20222021
reg_type=tuple(schema.reg_failed),
20232022
)
20242023
return reg_failed
@@ -4432,33 +4431,48 @@ def _make_param_reg_response(self, code: 'Enum_Parameter', param: 'Optional[Data
44324431

44334432
def _make_param_reg_failed(self, code: 'Enum_Parameter', param: 'Optional[Data_RegFailedParameter]' = None, *, # pylint: disable=unused-argument
44344433
version: 'int',
4435-
lifetime: 'int | timedelta' = 0,
4436-
reg_failed: 'Optional[list[Enum_RegistrationFailure | StdlibEnum | AenumEnum | str | int]]' = None,
4434+
failure_type: 'Enum_RegistrationFailure | StdlibEnum | AenumEnum | str | int' = Enum_RegistrationFailure.Registration_requires_additional_credentials, # pylint: disable=line-too-long
4435+
failure_type_default: 'Optional[int]' = None,
4436+
failure_type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long
4437+
failure_type_reversed: 'bool' = False,
4438+
reg_failed: 'Optional[list[Enum_Registration | StdlibEnum | AenumEnum | str | int]]' = None,
44374439
reg_failed_default: 'Optional[int]' = None,
44384440
reg_failed_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long
44394441
reg_failed_reversed: 'bool' = False,
44404442
**kwargs: 'Any') -> 'Schema_RegFailedParameter':
44414443
"""Make HIP ``REG_FAILED`` parameter.
44424444
4445+
.. versionchanged:: 1.5.0
4446+
4447+
``lifetime`` is replaced by ``failure_type``, with no alias, and
4448+
``reg_failed`` takes registration types rather than failure types
4449+
(:issue:`2309`).
4450+
44434451
Args:
44444452
code: parameter code
44454453
param: parameter data
44464454
version: HIP protocol version
4447-
lifetime: lifetime
4448-
reg_failed: registration failure list
4449-
reg_failed_default: default registration failure
4450-
reg_failed_namespace: registration failure namespace
4451-
reg_failed_reversed: reverse registration failure namespace
4455+
failure_type: registration failure type
4456+
failure_type_default: default registration failure type
4457+
failure_type_namespace: registration failure type namespace
4458+
failure_type_reversed: reverse registration failure type namespace
4459+
reg_failed: list of registration types that failed
4460+
reg_failed_default: default registration type
4461+
reg_failed_namespace: registration type namespace
4462+
reg_failed_reversed: reverse registration type namespace
44524463
**kwargs: arbitrary keyword arguments
44534464
44544465
Returns:
44554466
HIP parameter schema.
44564467
44574468
"""
44584469
if param is not None:
4459-
reg_type = cast('list[Enum_RegistrationFailure]', param.reg_type)
4460-
lt = math.floor(param.lifetime.total_seconds())
4470+
failure = param.failure_type
4471+
reg_type = cast('list[Enum_Registration]', param.reg_type)
44614472
else:
4473+
failure = self._make_index(failure_type, failure_type_default, namespace=failure_type_namespace, # type: ignore[assignment]
4474+
reversed=failure_type_reversed, pack=False)
4475+
44624476
if reg_failed is None:
44634477
reg_failed = []
44644478

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

4470-
lt = lifetime if isinstance(lifetime, int) else math.floor(lifetime.total_seconds())
4471-
44724484
return Schema_RegFailedParameter(
44734485
type=code,
44744486
len=1 + len(reg_type),
4475-
lifetime=lt,
4487+
failure_type=failure,
44764488
reg_failed=reg_type,
44774489
)
44784490

‎pcapkit/protocols/schema/internet/hip.py‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,9 @@ def registration_type_list_len(pkt: 'dict[str, Any]') -> 'int':
186186
Used by the ``reg_request``, ``reg_response`` and ``reg_failed`` fields of
187187
:class:`RegRequestParameter`, :class:`RegResponseParameter` and
188188
:class:`RegFailedParameter` respectively, each of which follows a single
189-
``lifetime`` octet with a list of registration type octets sized by the
190-
remainder of the parameter.
189+
prefix octet (the ``lifetime`` of the first two, the ``failure_type`` of
190+
the last) with a list of registration type octets sized by the remainder
191+
of the parameter.
191192
192193
Args:
193194
pkt: Parameter unpacked schema.
@@ -197,7 +198,7 @@ def registration_type_list_len(pkt: 'dict[str, Any]') -> 'int':
197198
198199
Raises:
199200
FieldValueError: If the parameter's ``Length`` on the wire is too
200-
short to hold the ``lifetime`` octet already read, which would
201+
short to hold the prefix octet already read, which would
201202
otherwise underflow the list length below zero.
202203
203204
"""
@@ -1171,18 +1172,18 @@ def __init__(self, type: 'Enum_Parameter', len: 'int', lifetime: 'int', reg_resp
11711172
class RegFailedParameter(Parameter, code=Enum_Parameter.REG_FAILED):
11721173
"""Header schema for HIP ``REG_FAILED`` parameters."""
11731174

1174-
#: Lifetime.
1175-
lifetime: 'int' = UInt8Field()
1175+
#: Registration failure type.
1176+
failure_type: 'Enum_RegistrationFailure' = EnumField(length=1, namespace=Enum_RegistrationFailure)
11761177
#: Registration types.
1177-
reg_failed: 'list[Enum_RegistrationFailure]' = ListField(
1178+
reg_failed: 'list[Enum_Registration]' = ListField(
11781179
length=registration_type_list_len,
1179-
item_type=EnumField(length=1, namespace=Enum_RegistrationFailure),
1180+
item_type=EnumField(length=1, namespace=Enum_Registration),
11801181
)
11811182
#: Padding.
11821183
padding: 'bytes' = PaddingField(length=parameter_padding_len)
11831184

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

11871188

11881189
@schema_final

‎tests/protocols/internet/test_hip_roundtrip_unit.py‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
:class:`~datetime.timedelta`, so 197 of the 256 octets could not be rebuilt.
1414
* #1328 -- the reserved bits of ``Controls`` and of ``ESP_INFO`` were written
1515
back as zero.
16+
* #2309 -- ``REG_FAILED`` read its Failure Type octet as a ``lifetime`` and its
17+
Reg Type octets as failure types, rather than as :rfc:`8003#section-4.5`
18+
lays them out.
1619
1720
Every case builds its own octets in memory and reads no capture. :class:`HIP`
1821
is imported inside each test, after :func:`~tests._support.reimport_once_per_class`,
@@ -35,6 +38,13 @@ def packet(params: bytes = b'', controls: bytes = b'\x00\x00') -> bytes:
3538
+ controls + bytes(32) + params)
3639

3740

41+
def reg_failed(failure_type: int, reg_types: 'list[int]') -> bytes:
42+
"""Return a ``REG_FAILED`` parameter, zero-padded to 8 octets."""
43+
body = (936).to_bytes(2, 'big') + (1 + len(reg_types)).to_bytes(2, 'big') \
44+
+ bytes([failure_type, *reg_types])
45+
return body + bytes(-len(body) % 8)
46+
47+
3848
class TestHIPRoundTrip(unittest.TestCase):
3949
"""Pin the rebuilt octets of parsed HIP packets."""
4050

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

119+
def test_reg_failed_reads_failure_type_then_registration_types(self) -> None:
120+
"""#2309: the first octet is a Failure Type, and the rest are Reg Types."""
121+
from pcapkit.const.hip.parameter import Parameter
122+
from pcapkit.const.hip.registration import Registration
123+
from pcapkit.const.hip.registration_failure import RegistrationFailure
124+
from pcapkit.protocols.internet.hip import HIP
125+
126+
wire = packet(reg_failed(1, [1, 2, 3]))
127+
param = HIP(io.BytesIO(wire), len(wire), extension=True).info.parameters[Parameter.REG_FAILED]
128+
129+
# Both registries are integer enums, so ``==`` alone cannot tell a
130+
# Failure Type from a Reg Type of the same value: compare the types too.
131+
self.assertIs(type(param.failure_type), RegistrationFailure)
132+
self.assertEqual(param.failure_type, RegistrationFailure.Registration_type_unavailable)
133+
self.assertEqual([type(reg) for reg in param.reg_type], [Registration] * 3)
134+
self.assertEqual(param.reg_type, (Registration.RENDEZVOUS,
135+
Registration.RELAY_UDP_HIP,
136+
Registration.RELAY_UDP_ESP))
137+
# No ``lifetime`` alias is kept: ``REG_FAILED`` has no lifetime.
138+
self.assertNotIn('lifetime', param)
139+
self.assertFalse(hasattr(param, 'lifetime'))
140+
141+
def test_reg_failed_parse_then_rebuild_is_byte_exact(self) -> None:
142+
"""#2309: every Failure Type, with several Reg Types, rebuilds unchanged."""
143+
for failure_type in (0, 1, 2, 3, 9, 201, 255):
144+
for reg_types in ([], [1], [1, 2, 3], [0, 4, 200, 255, 2, 1, 3]):
145+
with self.subTest(failure_type=failure_type, reg_types=reg_types):
146+
self.assertRoundTrips(packet(reg_failed(failure_type, reg_types)))
147+
148+
def test_reg_failed_make_then_parse_is_byte_exact(self) -> None:
149+
"""#2309: ``failure_type`` and ``reg_failed`` build the RFC 8003 octets."""
150+
from pcapkit.const.hip.parameter import Parameter
151+
from pcapkit.const.hip.registration import Registration
152+
from pcapkit.const.hip.registration_failure import RegistrationFailure
153+
from pcapkit.protocols.internet.hip import HIP
154+
155+
for failure_type, reg_types, octets in (
156+
(RegistrationFailure.Insufficient_resources,
157+
[Registration.RELAY_UDP_ESP, Registration.RENDEZVOUS], (2, [3, 1])),
158+
(3, [1, 2, 3], (3, [1, 2, 3])),
159+
(RegistrationFailure.Registration_requires_additional_credentials, [], (0, [])),
160+
):
161+
with self.subTest(failure_type=failure_type, reg_types=reg_types):
162+
wire = HIP.__new__(HIP).make(parameters=[(Parameter.REG_FAILED, {
163+
'failure_type': failure_type,
164+
'reg_failed': reg_types,
165+
})]).pack()
166+
self.assertEqual(wire[40:].hex(), reg_failed(*octets).hex())
167+
168+
param = HIP(io.BytesIO(wire), len(wire), extension=True).info.parameters[Parameter.REG_FAILED]
169+
self.assertIs(type(param.failure_type), RegistrationFailure)
170+
self.assertEqual(param.failure_type, octets[0])
171+
self.assertEqual([type(reg) for reg in param.reg_type],
172+
[Registration] * len(octets[1]))
173+
self.assertEqual(list(param.reg_type), octets[1])
174+
self.assertRoundTrips(wire)
175+
176+
# The default Failure Type is octet 0, as the removed ``lifetime`` was.
177+
wire = HIP.__new__(HIP).make(parameters=[(Parameter.REG_FAILED, {})]).pack()
178+
self.assertEqual(wire[40:].hex(), reg_failed(0, []).hex())
179+
109180

110181
if __name__ == '__main__':
111182
unittest.main()

0 commit comments

Comments
 (0)