Skip to content

Commit 269cdad

Browse files
committed
fix(mh): give LMAA the code its address family needs (#2280)
Problem: MH._make_opt_lmaa derived the option length from the address family but defaulted code to IPv6_LMAA regardless, so address='10.0.0.1' packed 290601000a000001, and an explicit code was never checked against the family RFC 5949 section 6.2.2 ties it to. Fix: code defaults to None and is then the family's own code; an explicit code that disagrees raises ProtocolError ("invalid code: code does not match address family: N"). The option= path keeps the captured code, so a captured mismatch still rebuilds byte for byte. Test: test_mh_lmaa_code_follows_the_address_family pins both families derived and explicit, three mismatches, and the captured-mismatch rebuild.
1 parent 7b57bb0 commit 269cdad

2 files changed

Lines changed: 70 additions & 4 deletions

File tree

‎pcapkit/protocols/internet/mh.py‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9470,7 +9470,7 @@ def _make_opt_cr(self, type: 'Enum_Option', option: 'Optional[Data_ContextReques
94709470
)
94719471

94729472
def _make_opt_lmaa(self, type: 'Enum_Option', option: 'Optional[Data_LMAAddressOption]' = None, *,
9473-
code: 'LMAAddressCode | StdlibEnum | AenumEnum | str | int' = LMAAddressCode.IPv6_LMAA,
9473+
code: 'Optional[LMAAddressCode | StdlibEnum | AenumEnum | str | int]' = None,
94749474
code_default: 'Optional[int]' = None,
94759475
code_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long
94769476
code_reversed: 'bool' = False,
@@ -9483,7 +9483,8 @@ def _make_opt_lmaa(self, type: 'Enum_Option', option: 'Optional[Data_LMAAddressO
94839483
type: Option type.
94849484
option: Option data model.
94859485
code: Option code, see
9486-
:class:`~pcapkit.protocols.internet.mh.LMAAddressCode`.
9486+
:class:`~pcapkit.protocols.internet.mh.LMAAddressCode`. If not
9487+
given, the code of the address family of ``address``.
94879488
code_default: Default option code.
94889489
code_namespace: Option code namespace.
94899490
code_reversed: Reverse option code namespace.
@@ -9495,15 +9496,24 @@ def _make_opt_lmaa(self, type: 'Enum_Option', option: 'Optional[Data_LMAAddressO
94959496
Returns:
94969497
Constructed option schema.
94979498
9499+
Raises:
9500+
ProtocolError: If an explicit ``code`` is not the code
9501+
:rfc:`5949#section-6.2.2` gives the address family of
9502+
``address`` (:issue:`2280`).
9503+
94989504
Note:
94999505
The option length is derived from the address family rather than from
9500-
``code``, so that the two cannot be emitted disagreeing.
9506+
``code``, so that the two cannot be emitted disagreeing. A data model
9507+
passed as ``option`` keeps its code as captured, even where it
9508+
disagrees with the family, so that the capture rebuilds byte for byte.
95019509
95029510
"""
95039511
if option is not None:
95049512
reserved = getattr(option, 'reserved', b'\x00')
9505-
code_val = int(option.code)
9513+
code_val = int(option.code) # type: Optional[int]
95069514
address = option.address
9515+
elif code is None:
9516+
code_val = None
95079517
else:
95089518
code_val = self._make_index(code, code_default, namespace=code_namespace,
95099519
reversed=code_reversed, pack=False)
@@ -9522,6 +9532,17 @@ def _make_opt_lmaa(self, type: 'Enum_Option', option: 'Optional[Data_LMAAddressO
95229532
addr = parse_ip_address(
95239533
address, f'{self.alias}: [OptNo {type}] invalid address')
95249534

9535+
# NOTE: RFC 5949 ties each code to one address family, so a code the
9536+
# caller did not choose is the family's own, and one they did choose has
9537+
# to agree with it (#2280). A data model passed as ``option`` skips the
9538+
# check, so that a captured mismatch still rebuilds byte for byte.
9539+
family_code = LMAAddressCode.IPv4_LMAA if addr.version == 4 else LMAAddressCode.IPv6_LMAA
9540+
if code_val is None:
9541+
code_val = int(family_code)
9542+
elif option is None and code_val != family_code:
9543+
raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid code: '
9544+
f'code does not match address family: {code_val}')
9545+
95259546
return Schema_LMAAddressOption(
95269547
type=type,
95279548
length=6 if addr.version == 4 else 18,

‎tests/protocols/internet/test_mh_unit.py‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3067,6 +3067,51 @@ def test_mh_lmaa_rejects_a_bytes_address_of_the_wrong_width(self) -> None:
30673067
self.assertEqual(schema.length, length)
30683068
self.assertEqual(schema.address, expected)
30693069

3070+
def test_mh_lmaa_code_follows_the_address_family(self) -> None:
3071+
"""The LMAA code is the one :rfc:`5949#section-6.2.2` gives the family. See #2280.
3072+
3073+
The maker derives the length from the address, but the code defaulted
3074+
to ``IPv6_LMAA`` regardless, so ``address='10.0.0.1'`` packed
3075+
``290601000a000001``: an IPv4 length with the IPv6 code.
3076+
"""
3077+
from pcapkit.const.mh.option import Option
3078+
from pcapkit.protocols.internet.mh import MH, LMAAddressCode
3079+
from pcapkit.protocols.schema.internet.mh import LMAAddressOption
3080+
from pcapkit.utilities.exceptions import BaseError, ProtocolError
3081+
3082+
proto = object.__new__(MH)
3083+
lmaa = Option.Local_Mobility_Anchor_Address_Option
3084+
3085+
for address, octets in (
3086+
('10.0.0.1', '290602000a000001'),
3087+
('2001:db8::1', '291201' '00' '20010db8' + '00' * 10 + '0001'),
3088+
):
3089+
with self.subTest(address=address, code='derived'):
3090+
schema = proto._make_opt_lmaa(lmaa, address=address) # type: ignore[arg-type]
3091+
self.assertEqual(schema.pack().hex(), octets)
3092+
with self.subTest(address=address, code='explicit, matching'):
3093+
code = LMAAddressCode.IPv4_LMAA if '.' in address else LMAAddressCode.IPv6_LMAA
3094+
schema = proto._make_opt_lmaa(lmaa, address=address, code=code) # type: ignore[arg-type]
3095+
self.assertEqual(schema.pack().hex(), octets)
3096+
3097+
for address, code in (('10.0.0.1', LMAAddressCode.IPv6_LMAA),
3098+
('2001:db8::1', LMAAddressCode.IPv4_LMAA),
3099+
('10.0.0.1', LMAAddressCode.Reserved)):
3100+
with self.subTest(address=address, code=code):
3101+
with self.assertRaises(ProtocolError) as context:
3102+
proto._make_opt_lmaa(lmaa, address=address, code=code) # type: ignore[arg-type]
3103+
self.assertIsInstance(context.exception, BaseError)
3104+
self.assertIn(f'invalid code: code does not match address family: {int(code)}',
3105+
str(context.exception))
3106+
3107+
# a captured mismatch is data, not a request, so it still rebuilds as read
3108+
captured = bytes.fromhex('290601000a000001')
3109+
data = proto._read_opt_lmaa( # type: ignore[arg-type]
3110+
LMAAddressOption.unpack(captured), options=None)
3111+
self.assertEqual(data.code, LMAAddressCode.IPv6_LMAA)
3112+
rebuilt = proto._make_opt_lmaa(lmaa, data) # type: ignore[arg-type]
3113+
self.assertEqual(rebuilt.pack(), captured)
3114+
30703115
def test_mh_target_careof_address_suboption_round_trips_both_families(self) -> None:
30713116
"""The sub-option length is derived from the family, so both widths need pinning.
30723117

0 commit comments

Comments
 (0)