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
38 changes: 30 additions & 8 deletions pcapkit/protocols/data/internet/mh.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
if TYPE_CHECKING:
from datetime import datetime as dt_type
from datetime import timedelta
from ipaddress import IPv4Address, IPv6Address, IPv6Network
from ipaddress import IPv4Address, IPv6Address
from typing import Optional

from pcapkit.const.mh.access_type import AccessType as Enum_AccessType
Expand Down Expand Up @@ -932,15 +932,28 @@ class MobileNetworkPrefixOption(Option):

New required field ``reserved`` (:issue:`1437`).

.. versionchanged:: 1.6.0

New required field ``prefix_length``, and :attr:`prefix` is the
:class:`~ipaddress.IPv6Address` captured rather than an
:class:`~ipaddress.IPv6Network`, which cannot hold a prefix with bits set
past its length or a length past 128 (:issue:`2702`).

"""

#: The reserved octet, as captured.
reserved: 'bytes'
#: Mobile Network Prefix.
prefix: 'IPv6Network'
#: Prefix length, as captured.
prefix_length: 'int'
#: Mobile Network Prefix. Kept apart from :attr:`prefix_length` rather than
#: fused into an :class:`~ipaddress.IPv6Network`, so that bits past
#: :attr:`prefix_length`, which :rfc:`3963#section-6.2` reads as given, survive a
#: round trip. Same reasoning as :class:`HomeNetworkPrefixOption`.
prefix: 'IPv6Address'

if TYPE_CHECKING:
def __init__(self, type: 'Enum_Option', length: 'int', reserved: 'bytes', prefix: 'IPv6Network') -> 'None': ...
def __init__(self, type: 'Enum_Option', length: 'int', reserved: 'bytes', prefix_length: 'int',
prefix: 'IPv6Address') -> 'None': ...


@info_final
Expand Down Expand Up @@ -1310,17 +1323,26 @@ def __init__(self, seconds: 'int', fraction: 'int') -> 'None': ...

@info_final
class TimestampOption(Option):
"""Data model for MH Timestamp option."""
"""Data model for MH Timestamp option.

#: Timestamp.
timestamp: 'dt_type'
.. versionchanged:: 1.6.0

:attr:`timestamp` is :data:`None` for a time past the year 9999, which
:class:`~datetime.datetime` cannot hold, rather than the read raising
:exc:`ValueError` (:issue:`2702`).

"""

#: Timestamp, or :data:`None` past the year 9999, which the 48-bit second
#: count reaches and :class:`~datetime.datetime` cannot hold.
timestamp: 'Optional[dt_type]'
#: Timestamp in the fixed-point format of :rfc:`5213#section-8.8`, kept
#: alongside :attr:`timestamp` so that the 1/65536-second resolution of the
#: wire format survives a round trip.
pmip_timestamp: 'PMIPv6Timestamp'

if TYPE_CHECKING:
def __init__(self, type: 'Enum_Option', length: 'int', timestamp: 'dt_type',
def __init__(self, type: 'Enum_Option', length: 'int', timestamp: 'Optional[dt_type]',
pmip_timestamp: 'PMIPv6Timestamp') -> 'None': ...


Expand Down
95 changes: 74 additions & 21 deletions pcapkit/protocols/internet/mh.py
Original file line number Diff line number Diff line change
Expand Up @@ -3350,14 +3350,16 @@ def _read_opt_mnp(self, schema: 'Schema_MobileNetworkPrefixOption', *,
if schema.length != 18:
raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format')

prefix = cast('IPv6Network',
ipaddress.ip_network((schema.prefix, schema.prefix_length)))

# NOTE: The prefix and its length stay apart, as captured: an IPv6Network
# holds neither bits set past the length nor a length past 128. RFC 3963
# section 6.2 reads both fields as given, and answers a prefix the home
# agent cannot use with a status rather than discarding the option (#2702).
data = Data_MobileNetworkPrefixOption(
type=schema.type,
length=self._mh_option_length(schema.length),
reserved=schema.reserved,
prefix=prefix,
prefix_length=schema.prefix_length,
prefix=schema.prefix,
)
return data

Expand Down Expand Up @@ -4020,9 +4022,10 @@ def _read_opt_hnp(self, schema: 'Schema_HomeNetworkPrefixOption', *,
"""
if schema.length != 18:
raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format')
if schema.prefix_length > 128:
raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format')

# NOTE: A prefix length past 128 is kept, as for option 6: RFC 5213
# section 5.3.2 answers a prefix the local mobility anchor cannot
# authorise with a status rather than discarding the option (#2702).
data = Data_HomeNetworkPrefixOption(
type=schema.type,
length=self._mh_option_length(schema.length),
Expand Down Expand Up @@ -4237,12 +4240,23 @@ def _read_opt_timestamp(self, schema: 'Schema_TimestampOption', *,
seconds = schema.timestamp['seconds']
fraction = schema.timestamp['fraction']

try:
# NOTE: Not the stdlib's ``fromtimestamp``, which takes no time
# after 3000 on Windows (#2099). The whole seconds go in as an int
# and the fraction as a timedelta, which rounds it to microseconds
# half to even as one float did, but loses no precision to the
# seconds, so the last second of 9999 is not carried past it.
timestamp = (fromtimestamp(seconds, datetime.timezone.utc)
+ datetime.timedelta(seconds=fraction / 65536)) # type: Optional[dt_type]
except ValueError:
# past the year 9999, which the 48-bit count reaches and datetime
# cannot hold, so pmip_timestamp alone keeps it (#2702)
timestamp = None

data = Data_TimestampOption(
type=schema.type,
length=self._mh_option_length(schema.length),
# not the stdlib's ``fromtimestamp``, which takes no time after 3000
# on Windows (#2099)
timestamp=fromtimestamp(seconds + fraction / 65536, datetime.timezone.utc),
timestamp=timestamp,
pmip_timestamp=Data_PMIPv6Timestamp(seconds=seconds, fraction=fraction),
)
return data
Expand Down Expand Up @@ -8258,32 +8272,62 @@ def _make_opt_bad(self, type: 'Enum_Option', option: 'Optional[Data_Authorizatio
)

def _make_opt_mnp(self, type: 'Enum_Option', option: 'Optional[Data_MobileNetworkPrefixOption]' = None, *,
prefix: 'bytes | str | IPv6Network' = '::/0',
prefix: 'bytes | str | int | IPv6Address | IPv6Network' = '::/0',
prefix_length: 'Optional[int]' = None,
reserved: 'bytes' = b'\x00',
**kwargs: 'Any') -> 'Schema_MobileNetworkPrefixOption':
"""Make MH mobile network prefix option.

Args:
type: Option type.
option: Option data model.
prefix: Mobile network prefix.
prefix: Mobile network prefix, an IPv6 address or network, whose
bits past ``prefix_length`` are written as given.
prefix_length: Prefix length; defaults to that of a network or of
text ending in ``/<length>``, and to 128 for an address.
reserved: The reserved octet as captured, truncated or zero-filled to 1
octet.
**kwargs: Arbitrary keyword arguments.

Returns:
Constructed option schema.

Raises:
ProtocolError: If ``prefix`` is not an IPv6 address or network, or
``prefix_length`` is negative, a :obj:`bool`, or does not fit in
one octet.

Note:
The prefix length is checked against its 8-bit field alone, as
:rfc:`3963#section-6.2` has a home agent answer a prefix it cannot
use with a status rather than discard the option, so whatever the
reader keeps is written back.

.. versionchanged:: 1.6.0

New argument ``prefix_length``. ``prefix`` keeps the bits set past
its length, rather than raising :exc:`ValueError`, and a prefix
length past 128 is written (:issue:`2702`).

"""
if option is not None:
reserved = getattr(option, 'reserved', b'\x00')
prefix = option.prefix

prefix_val = ipaddress.ip_network(prefix)
if prefix_val.version != 6:
raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid mobile network prefix: {prefix!r}')
prefix_length = prefix_val.prefixlen
prefix_addr = prefix_val.network_address
prefix_length = option.prefix_length
prefix_addr = option.prefix
else:
try:
# NOTE: An interface, unlike a network, keeps the bits past its
# length, and takes an address, a network or text alike. A bool
# is refused first, as it would pass for the address ``::1``.
prefix_val = None if isinstance(prefix, bool) else ipaddress.IPv6Interface(prefix)
except ValueError:
prefix_val = None
if prefix_val is None:
raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid mobile network prefix: {prefix!r}')
if prefix_length is None:
prefix_length = prefix_val.network.prefixlen
prefix_addr = prefix_val.ip
prefix_length = self._make_uint(prefix_length, bits=8, name='prefix length')

return Schema_MobileNetworkPrefixOption(
type=type,
Expand Down Expand Up @@ -9088,7 +9132,18 @@ def _make_opt_hnp(self, type: 'Enum_Option', option: 'Optional[Data_HomeNetworkP

Raises:
ProtocolError: If ``prefix_length`` is negative, a :obj:`bool`,
or longer than the address.
or does not fit in one octet.

Note:
The prefix length is checked against its 8-bit field alone, as
:rfc:`5213#section-5.3.2` has a local mobility anchor answer a
prefix it cannot authorise with a status rather than discard the
option, so whatever the reader keeps is written back.

.. versionchanged:: 1.6.0

A prefix length longer than the address is written, rather than
refused, as the reader keeps one (:issue:`2702`).

"""
if option is not None:
Expand All @@ -9097,8 +9152,6 @@ def _make_opt_hnp(self, type: 'Enum_Option', option: 'Optional[Data_HomeNetworkP
prefix = option.prefix

prefix_length = self._make_uint(prefix_length, bits=8, name='prefix length')
if prefix_length > 128:
raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid prefix length: {prefix_length}')

return Schema_HomeNetworkPrefixOption(
type=type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -447,14 +447,15 @@ class MHMakerTests(_Case):
'bra': ('Binding_Refresh_Advice', {}, {'interval': (0xFFFF, 'refresh interval')}),
'ni': ('Nonce_Indices', {}, {'home': (0xFFFF, 'home nonce index'),
'careof': (0xFFFF, 'care-of nonce index')}),
'mnp': ('Mobile_Network_Prefix_Option', {}, {'prefix_length': (0xFF, 'prefix length')}),
'auth': ('AUTH_OPTION_TYPE', {'data': b'\x00\x00'}, {'spi': (0xFFFFFFFF, 'SPI')}),
'badf': ('Binding_Authorization_Data_for_FMIPv6', {'data': b'\x00'},
{'spi': (0xFFFFFFFF, 'SPI')}),
'ipv6_ap': ('Mobility_Header_IPv6_Address_Prefix', {}, {'prefix_length': (128, 'prefix length')}),
'dns': ('DNS_UPDATE_TYPE', {}, {'reserved': (0x7F, 'reserved bits')}),
'vendor': ('Vendor_Specific_Mobility_Option', {}, {'vendor': (0xFFFFFFFF, 'vendor ID'),
'subtype': (0xFF, 'vendor subtype')}),
'hnp': ('Home_Network_Prefix_Option', {}, {'prefix_length': (128, 'prefix length')}),
'hnp': ('Home_Network_Prefix_Option', {}, {'prefix_length': (0xFF, 'prefix length')}),
'hi': ('Handoff_Indicator_Option', {}, {'reserved': (0xFF, 'reserved octet')}),
'timestamp': ('Timestamp_Option', {}, {'seconds': ((1 << 48) - 1, 'timestamp seconds'),
'fraction': (0xFFFF, 'timestamp fraction')}),
Expand Down Expand Up @@ -562,11 +563,6 @@ class MHMakerTests(_Case):
'maximum retransmissions'),
)

UNPARSED_MAXIMA = {
'_make_opt_timestamp.seconds': 'MH._read_opt_timestamp turns the seconds into a '
'datetime, which cannot hold the 48-bit maximum',
}

#: Integer arguments outside the sweep, and why.
EXEMPT = {
# The CGA extension makers are reached only through a CGA parameter
Expand Down
10 changes: 3 additions & 7 deletions tests/protocols/internet/test_mh_option_mutation_2684_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,9 @@
LONG_KEY = b'\x30\x81\x9f' + bytes(range(159))

#: Options a one-octet change makes the reader raise an error that is not
#: pcapkit's, by the reader: its data model has no form for the octets, a prefix
#: with host bits set or a time past datetime.MAXYEAR. Inside IPv6 the header is
#: then Raw, so the round trip holds. Found by this sweep, not fixed here.
KNOWN_CRASHES = {
'Mobile_Network_Prefix_Option': '_read_opt_mnp',
'Timestamp_Option': '_read_opt_timestamp',
}
#: pcapkit's, by the reader whose data model has no form for the octets. Inside
#: IPv6 the header is then Raw, so the round trip holds.
KNOWN_CRASHES = {} # type: dict[str, str]


def _ipv6(body: bytes) -> bytes:
Expand Down
11 changes: 6 additions & 5 deletions tests/protocols/internet/test_mh_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import datetime
import io
from ipaddress import ip_address, ip_network
from ipaddress import ip_address
import importlib.util
from types import SimpleNamespace
import unittest
Expand Down Expand Up @@ -353,12 +353,13 @@ def test_mh_option_readers_cover_known_options_and_guards(self) -> None:
schema.AuthorizationDataOption(type=Option.Authorization_Data, length=8, data=b'12345678'),
options=options,
).data, b'12345678')
self.assertEqual(str(proto._read_opt_mnp(
mnp = proto._read_opt_mnp(
schema.MobileNetworkPrefixOption(type=Option.Mobile_Network_Prefix_Option,
length=18, prefix_length=64,
prefix='2001:db8::'),
options=options,
).prefix), '2001:db8::/64')
)
self.assertEqual((mnp.prefix_length, str(mnp.prefix)), (64, '2001:db8::'))
self.assertEqual(proto._read_opt_lla(
schema.LinkLayerAddressOption(type=Option.Mobility_Header_Link_Layer_Address_option,
length=7, code=LLACode.MN, lla=b'abcdef'),
Expand Down Expand Up @@ -789,8 +790,8 @@ def make_extension(code: CGAExtension, extension: data.UnknownExtension | None =
self.assertEqual(proto._make_opt_mnp(
Option.Mobile_Network_Prefix_Option,
data.MobileNetworkPrefixOption(type=Option.Mobile_Network_Prefix_Option,
length=18, reserved=b'\x00',
prefix=ip_network('2001:db8:1::/64')),
length=18, reserved=b'\x00', prefix_length=64,
prefix=ip_address('2001:db8:1::')),
).prefix_length, 64)
self.assertEqual(proto._make_opt_lla(
Option.Mobility_Header_Link_Layer_Address_option,
Expand Down
Loading
Loading