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
32 changes: 27 additions & 5 deletions pcapkit/protocols/internet/mh.py
Original file line number Diff line number Diff line change
Expand Up @@ -9470,7 +9470,7 @@ def _make_opt_cr(self, type: 'Enum_Option', option: 'Optional[Data_ContextReques
)

def _make_opt_lmaa(self, type: 'Enum_Option', option: 'Optional[Data_LMAAddressOption]' = None, *,
code: 'LMAAddressCode | StdlibEnum | AenumEnum | str | int' = LMAAddressCode.IPv6_LMAA,
code: 'Optional[LMAAddressCode | StdlibEnum | AenumEnum | str | int]' = None,
code_default: 'Optional[int]' = None,
code_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long
code_reversed: 'bool' = False,
Expand All @@ -9483,7 +9483,8 @@ def _make_opt_lmaa(self, type: 'Enum_Option', option: 'Optional[Data_LMAAddressO
type: Option type.
option: Option data model.
code: Option code, see
:class:`~pcapkit.protocols.internet.mh.LMAAddressCode`.
:class:`~pcapkit.protocols.internet.mh.LMAAddressCode`. If not
given, the code of the address family of ``address``.
code_default: Default option code.
code_namespace: Option code namespace.
code_reversed: Reverse option code namespace.
Expand All @@ -9495,17 +9496,27 @@ def _make_opt_lmaa(self, type: 'Enum_Option', option: 'Optional[Data_LMAAddressO
Returns:
Constructed option schema.

Raises:
ProtocolError: If an explicit ``code`` is not the code
:rfc:`5949#section-6.2.2` gives the address family of
``address`` (:issue:`2280`).

Note:
The option length is derived from the address family rather than from
``code``, so that the two cannot be emitted disagreeing.
``code``, so that the two cannot be emitted disagreeing. A data model
passed as ``option`` keeps its code as captured, even where it
disagrees with the family, so that the capture rebuilds byte for byte.

"""
if option is not None:
reserved = getattr(option, 'reserved', b'\x00')
code_val = int(option.code)
code_val = int(option.code) # type: Optional[int]
address = option.address
elif code is None:
code_val = None
else:
code_val = self._make_index(code, code_default, namespace=code_namespace,
code_val = self._make_index(code, code_default,
namespace=code_namespace if code_namespace is not None else LMAAddressCode,
reversed=code_reversed, pack=False)

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

# NOTE: RFC 5949 ties each code to one address family, so a code the
# caller did not choose is the family's own, and one they did choose has
# to agree with it (#2280). A data model passed as ``option`` skips the
# check, so that a captured mismatch still rebuilds byte for byte.
family_code = LMAAddressCode.IPv4_LMAA if addr.version == 4 else LMAAddressCode.IPv6_LMAA
if code_val is None:
code_val = int(family_code)
elif option is None and code_val != family_code:
raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid code: '
f'code does not match address family: {code_val}')

return Schema_LMAAddressOption(
type=type,
length=6 if addr.version == 4 else 18,
Expand Down
60 changes: 60 additions & 0 deletions tests/protocols/internet/test_mh_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -3067,6 +3067,66 @@ def test_mh_lmaa_rejects_a_bytes_address_of_the_wrong_width(self) -> None:
self.assertEqual(schema.length, length)
self.assertEqual(schema.address, expected)

def test_mh_lmaa_code_follows_the_address_family(self) -> None:
"""The LMAA code is the one :rfc:`5949#section-6.2.2` gives the family. See #2280.

The maker derives the length from the address, but the code defaulted
to ``IPv6_LMAA`` regardless, so ``address='10.0.0.1'`` packed
``290601000a000001``: an IPv4 length with the IPv6 code.
"""
from pcapkit.const.mh.option import Option
from pcapkit.protocols.internet.mh import MH, LMAAddressCode
from pcapkit.protocols.schema.internet.mh import LMAAddressOption
from pcapkit.utilities.exceptions import BaseError, ProtocolError

proto = object.__new__(MH)
lmaa = Option.Local_Mobility_Anchor_Address_Option

for address, octets in (
('10.0.0.1', '290602000a000001'),
('2001:db8::1', '291201' '00' '20010db8' + '00' * 10 + '0001'),
):
with self.subTest(address=address, code='derived'):
schema = proto._make_opt_lmaa(lmaa, address=address) # type: ignore[arg-type]
self.assertEqual(schema.pack().hex(), octets)
with self.subTest(address=address, code='explicit, matching'):
code = LMAAddressCode.IPv4_LMAA if '.' in address else LMAAddressCode.IPv6_LMAA
schema = proto._make_opt_lmaa(lmaa, address=address, code=code) # type: ignore[arg-type]
self.assertEqual(schema.pack().hex(), octets)
with self.subTest(address=address, code='explicit, matching, by name'):
# #2280: a str code name has to resolve against LMAAddressCode even
# when code_namespace is not given, as every sibling _make_opt_*
# does -- _make_index's str resolution needs an enum default.
name = 'IPv4_LMAA' if '.' in address else 'IPv6_LMAA'
schema = proto._make_opt_lmaa(lmaa, address=address, code=name) # type: ignore[arg-type]
self.assertEqual(schema.pack().hex(), octets)

for address, code in (('10.0.0.1', LMAAddressCode.IPv6_LMAA),
('2001:db8::1', LMAAddressCode.IPv4_LMAA),
('10.0.0.1', LMAAddressCode.Reserved)):
with self.subTest(address=address, code=code):
with self.assertRaises(ProtocolError) as context:
proto._make_opt_lmaa(lmaa, address=address, code=code) # type: ignore[arg-type]
self.assertIsInstance(context.exception, BaseError)
self.assertIn(f'invalid code: code does not match address family: {int(code)}',
str(context.exception))

for address, name in (('10.0.0.1', 'IPv6_LMAA'), ('2001:db8::1', 'IPv4_LMAA')):
with self.subTest(address=address, code=name):
with self.assertRaises(ProtocolError) as context:
proto._make_opt_lmaa(lmaa, address=address, code=name) # type: ignore[arg-type]
self.assertIsInstance(context.exception, BaseError)
self.assertIn('invalid code: code does not match address family:',
str(context.exception))

# a captured mismatch is data, not a request, so it still rebuilds as read
captured = bytes.fromhex('290601000a000001')
data = proto._read_opt_lmaa( # type: ignore[arg-type]
LMAAddressOption.unpack(captured), options=None)
self.assertEqual(data.code, LMAAddressCode.IPv6_LMAA)
rebuilt = proto._make_opt_lmaa(lmaa, data) # type: ignore[arg-type]
self.assertEqual(rebuilt.pack(), captured)

def test_mh_target_careof_address_suboption_round_trips_both_families(self) -> None:
"""The sub-option length is derived from the family, so both widths need pinning.

Expand Down
Loading