Skip to content

Commit 55db694

Browse files
authored
fix(mh): give LMAA the code its address family needs (#2280) (#2350)
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 8a4cdd3 commit 55db694

2 files changed

Lines changed: 87 additions & 5 deletions

File tree

‎pcapkit/protocols/internet/mh.py‎

Lines changed: 27 additions & 5 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,17 +9496,27 @@ 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:
9508-
code_val = self._make_index(code, code_default, namespace=code_namespace,
9518+
code_val = self._make_index(code, code_default,
9519+
namespace=code_namespace if code_namespace is not None else LMAAddressCode,
95099520
reversed=code_reversed, pack=False)
95109521

95119522
if isinstance(address, bytes) and len(address) not in (4, 16):
@@ -9522,6 +9533,17 @@ def _make_opt_lmaa(self, type: 'Enum_Option', option: 'Optional[Data_LMAAddressO
95229533
addr = parse_ip_address(
95239534
address, f'{self.alias}: [OptNo {type}] invalid address')
95249535

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

‎tests/protocols/internet/test_mh_unit.py‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3067,6 +3067,66 @@ 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+
with self.subTest(address=address, code='explicit, matching, by name'):
3097+
# #2280: a str code name has to resolve against LMAAddressCode even
3098+
# when code_namespace is not given, as every sibling _make_opt_*
3099+
# does -- _make_index's str resolution needs an enum default.
3100+
name = 'IPv4_LMAA' if '.' in address else 'IPv6_LMAA'
3101+
schema = proto._make_opt_lmaa(lmaa, address=address, code=name) # type: ignore[arg-type]
3102+
self.assertEqual(schema.pack().hex(), octets)
3103+
3104+
for address, code in (('10.0.0.1', LMAAddressCode.IPv6_LMAA),
3105+
('2001:db8::1', LMAAddressCode.IPv4_LMAA),
3106+
('10.0.0.1', LMAAddressCode.Reserved)):
3107+
with self.subTest(address=address, code=code):
3108+
with self.assertRaises(ProtocolError) as context:
3109+
proto._make_opt_lmaa(lmaa, address=address, code=code) # type: ignore[arg-type]
3110+
self.assertIsInstance(context.exception, BaseError)
3111+
self.assertIn(f'invalid code: code does not match address family: {int(code)}',
3112+
str(context.exception))
3113+
3114+
for address, name in (('10.0.0.1', 'IPv6_LMAA'), ('2001:db8::1', 'IPv4_LMAA')):
3115+
with self.subTest(address=address, code=name):
3116+
with self.assertRaises(ProtocolError) as context:
3117+
proto._make_opt_lmaa(lmaa, address=address, code=name) # type: ignore[arg-type]
3118+
self.assertIsInstance(context.exception, BaseError)
3119+
self.assertIn('invalid code: code does not match address family:',
3120+
str(context.exception))
3121+
3122+
# a captured mismatch is data, not a request, so it still rebuilds as read
3123+
captured = bytes.fromhex('290601000a000001')
3124+
data = proto._read_opt_lmaa( # type: ignore[arg-type]
3125+
LMAAddressOption.unpack(captured), options=None)
3126+
self.assertEqual(data.code, LMAAddressCode.IPv6_LMAA)
3127+
rebuilt = proto._make_opt_lmaa(lmaa, data) # type: ignore[arg-type]
3128+
self.assertEqual(rebuilt.pack(), captured)
3129+
30703130
def test_mh_target_careof_address_suboption_round_trips_both_families(self) -> None:
30713131
"""The sub-option length is derived from the family, so both widths need pinning.
30723132

0 commit comments

Comments
 (0)