Skip to content

Commit c8e3e0e

Browse files
committed
fix(internet)!: keep MH option 6 prefixes with host bits and option 27 times past 9999 (#2702)
- The Mobile Network Prefix option fused its prefix and length into an IPv6Network, so a prefix with bits set past its length, or a length past 128, raised a bare ValueError. RFC 3963 section 6.2 reads both fields as given and answers a prefix it cannot use with a status, not a discard, so the data model keeps them apart, as Home Network Prefix does: prefix_length is a new required field and prefix is the IPv6Address captured, the breaking change. The maker checks the length against its 8-bit field alone, takes a prefix_length keyword, and writes the bits past it. - The Timestamp option turned a 48-bit second count past 9999 into a datetime, which raised a bare ValueError. RFC 5213 section 5.5 has the LMA answer such a time with a status rather than discard it, so timestamp is None there and pmip_timestamp keeps it. The seconds and fraction are no longer summed as one float, which carried 23:59:59 and 65535/65536 past the year's end. - The mutation sweep's known crashes and the maker sweep's unparsed maximum go. New tests fail on 57a6c36 without the pcapkit/ change; MH and guard modules pass.
1 parent 57a6c36 commit c8e3e0e

6 files changed

Lines changed: 269 additions & 41 deletions

File tree

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

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
if TYPE_CHECKING:
1111
from datetime import datetime as dt_type
1212
from datetime import timedelta
13-
from ipaddress import IPv4Address, IPv6Address, IPv6Network
13+
from ipaddress import IPv4Address, IPv6Address
1414
from typing import Optional
1515

1616
from pcapkit.const.mh.access_type import AccessType as Enum_AccessType
@@ -932,15 +932,28 @@ class MobileNetworkPrefixOption(Option):
932932
933933
New required field ``reserved`` (:issue:`1437`).
934934
935+
.. versionchanged:: 1.6.0
936+
937+
New required field ``prefix_length``, and :attr:`prefix` is the
938+
:class:`~ipaddress.IPv6Address` captured rather than an
939+
:class:`~ipaddress.IPv6Network`, which cannot hold a prefix with bits set
940+
past its length or a length past 128 (:issue:`2702`).
941+
935942
"""
936943

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

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

945958

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

13111324
@info_final
13121325
class TimestampOption(Option):
1313-
"""Data model for MH Timestamp option."""
1326+
"""Data model for MH Timestamp option.
13141327
1315-
#: Timestamp.
1316-
timestamp: 'dt_type'
1328+
.. versionchanged:: 1.6.0
1329+
1330+
:attr:`timestamp` is :data:`None` for a time past the year 9999, which
1331+
:class:`~datetime.datetime` cannot hold, rather than the read raising
1332+
:exc:`ValueError` (:issue:`2702`).
1333+
1334+
"""
1335+
1336+
#: Timestamp, or :data:`None` past the year 9999, which the 48-bit second
1337+
#: count reaches and :class:`~datetime.datetime` cannot hold.
1338+
timestamp: 'Optional[dt_type]'
13171339
#: Timestamp in the fixed-point format of :rfc:`5213#section-8.8`, kept
13181340
#: alongside :attr:`timestamp` so that the 1/65536-second resolution of the
13191341
#: wire format survives a round trip.
13201342
pmip_timestamp: 'PMIPv6Timestamp'
13211343

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

13261348

‎pcapkit/protocols/internet/mh.py‎

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3350,14 +3350,16 @@ def _read_opt_mnp(self, schema: 'Schema_MobileNetworkPrefixOption', *,
33503350
if schema.length != 18:
33513351
raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format')
33523352

3353-
prefix = cast('IPv6Network',
3354-
ipaddress.ip_network((schema.prefix, schema.prefix_length)))
3355-
3353+
# NOTE: The prefix and its length stay apart, as captured: an IPv6Network
3354+
# holds neither bits set past the length nor a length past 128. RFC 3963
3355+
# section 6.2 reads both fields as given, and answers a prefix the home
3356+
# agent cannot use with a status rather than discarding the option (#2702).
33563357
data = Data_MobileNetworkPrefixOption(
33573358
type=schema.type,
33583359
length=self._mh_option_length(schema.length),
33593360
reserved=schema.reserved,
3360-
prefix=prefix,
3361+
prefix_length=schema.prefix_length,
3362+
prefix=schema.prefix,
33613363
)
33623364
return data
33633365

@@ -4237,12 +4239,23 @@ def _read_opt_timestamp(self, schema: 'Schema_TimestampOption', *,
42374239
seconds = schema.timestamp['seconds']
42384240
fraction = schema.timestamp['fraction']
42394241

4242+
try:
4243+
# NOTE: Not the stdlib's ``fromtimestamp``, which takes no time
4244+
# after 3000 on Windows (#2099). The whole seconds go in as an int
4245+
# and the fraction as a timedelta, which rounds it to microseconds
4246+
# half to even as one float did, but loses no precision to the
4247+
# seconds, so the last second of 9999 is not carried past it.
4248+
timestamp = (fromtimestamp(seconds, datetime.timezone.utc)
4249+
+ datetime.timedelta(seconds=fraction / 65536)) # type: Optional[dt_type]
4250+
except ValueError:
4251+
# past the year 9999, which the 48-bit count reaches and datetime
4252+
# cannot hold, so pmip_timestamp alone keeps it (#2702)
4253+
timestamp = None
4254+
42404255
data = Data_TimestampOption(
42414256
type=schema.type,
42424257
length=self._mh_option_length(schema.length),
4243-
# not the stdlib's ``fromtimestamp``, which takes no time after 3000
4244-
# on Windows (#2099)
4245-
timestamp=fromtimestamp(seconds + fraction / 65536, datetime.timezone.utc),
4258+
timestamp=timestamp,
42464259
pmip_timestamp=Data_PMIPv6Timestamp(seconds=seconds, fraction=fraction),
42474260
)
42484261
return data
@@ -8258,32 +8271,62 @@ def _make_opt_bad(self, type: 'Enum_Option', option: 'Optional[Data_Authorizatio
82588271
)
82598272

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

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

82748291
Returns:
82758292
Constructed option schema.
82768293

8294+
Raises:
8295+
ProtocolError: If ``prefix`` is not an IPv6 address or network, or
8296+
``prefix_length`` is negative, a :obj:`bool`, or does not fit in
8297+
one octet.
8298+
8299+
Note:
8300+
The prefix length is checked against its 8-bit field alone, as
8301+
:rfc:`3963#section-6.2` has a home agent answer a prefix it cannot
8302+
use with a status rather than discard the option, so whatever the
8303+
reader keeps is written back.
8304+
8305+
.. versionchanged:: 1.6.0
8306+
8307+
New argument ``prefix_length``. ``prefix`` keeps the bits set past
8308+
its length, rather than raising :exc:`ValueError`, and a prefix
8309+
length past 128 is written (:issue:`2702`).
8310+
82778311
"""
82788312
if option is not None:
82798313
reserved = getattr(option, 'reserved', b'\x00')
8280-
prefix = option.prefix
8281-
8282-
prefix_val = ipaddress.ip_network(prefix)
8283-
if prefix_val.version != 6:
8284-
raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid mobile network prefix: {prefix!r}')
8285-
prefix_length = prefix_val.prefixlen
8286-
prefix_addr = prefix_val.network_address
8314+
prefix_length = option.prefix_length
8315+
prefix_addr = option.prefix
8316+
else:
8317+
try:
8318+
# NOTE: An interface, unlike a network, keeps the bits past its
8319+
# length, and takes an address, a network or text alike. A bool
8320+
# is refused first, as it would pass for the address ``::1``.
8321+
prefix_val = None if isinstance(prefix, bool) else ipaddress.IPv6Interface(prefix)
8322+
except ValueError:
8323+
prefix_val = None
8324+
if prefix_val is None:
8325+
raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid mobile network prefix: {prefix!r}')
8326+
if prefix_length is None:
8327+
prefix_length = prefix_val.network.prefixlen
8328+
prefix_addr = prefix_val.ip
8329+
prefix_length = self._make_uint(prefix_length, bits=8, name='prefix length')
82878330

82888331
return Schema_MobileNetworkPrefixOption(
82898332
type=type,

‎tests/protocols/internet/test_internet_maker_ranges_2614_unit.py‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,7 @@ class MHMakerTests(_Case):
447447
'bra': ('Binding_Refresh_Advice', {}, {'interval': (0xFFFF, 'refresh interval')}),
448448
'ni': ('Nonce_Indices', {}, {'home': (0xFFFF, 'home nonce index'),
449449
'careof': (0xFFFF, 'care-of nonce index')}),
450+
'mnp': ('Mobile_Network_Prefix_Option', {}, {'prefix_length': (0xFF, 'prefix length')}),
450451
'auth': ('AUTH_OPTION_TYPE', {'data': b'\x00\x00'}, {'spi': (0xFFFFFFFF, 'SPI')}),
451452
'badf': ('Binding_Authorization_Data_for_FMIPv6', {'data': b'\x00'},
452453
{'spi': (0xFFFFFFFF, 'SPI')}),
@@ -562,11 +563,6 @@ class MHMakerTests(_Case):
562563
'maximum retransmissions'),
563564
)
564565

565-
UNPARSED_MAXIMA = {
566-
'_make_opt_timestamp.seconds': 'MH._read_opt_timestamp turns the seconds into a '
567-
'datetime, which cannot hold the 48-bit maximum',
568-
}
569-
570566
#: Integer arguments outside the sweep, and why.
571567
EXEMPT = {
572568
# The CGA extension makers are reached only through a CGA parameter

‎tests/protocols/internet/test_mh_option_mutation_2684_unit.py‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,9 @@
3131
LONG_KEY = b'\x30\x81\x9f' + bytes(range(159))
3232

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

4238

4339
def _ipv6(body: bytes) -> bytes:

‎tests/protocols/internet/test_mh_unit.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import datetime
44
import io
5-
from ipaddress import ip_address, ip_network
5+
from ipaddress import ip_address
66
import importlib.util
77
from types import SimpleNamespace
88
import unittest
@@ -353,12 +353,13 @@ def test_mh_option_readers_cover_known_options_and_guards(self) -> None:
353353
schema.AuthorizationDataOption(type=Option.Authorization_Data, length=8, data=b'12345678'),
354354
options=options,
355355
).data, b'12345678')
356-
self.assertEqual(str(proto._read_opt_mnp(
356+
mnp = proto._read_opt_mnp(
357357
schema.MobileNetworkPrefixOption(type=Option.Mobile_Network_Prefix_Option,
358358
length=18, prefix_length=64,
359359
prefix='2001:db8::'),
360360
options=options,
361-
).prefix), '2001:db8::/64')
361+
)
362+
self.assertEqual((mnp.prefix_length, str(mnp.prefix)), (64, '2001:db8::'))
362363
self.assertEqual(proto._read_opt_lla(
363364
schema.LinkLayerAddressOption(type=Option.Mobility_Header_Link_Layer_Address_option,
364365
length=7, code=LLACode.MN, lla=b'abcdef'),
@@ -789,8 +790,8 @@ def make_extension(code: CGAExtension, extension: data.UnknownExtension | None =
789790
self.assertEqual(proto._make_opt_mnp(
790791
Option.Mobile_Network_Prefix_Option,
791792
data.MobileNetworkPrefixOption(type=Option.Mobile_Network_Prefix_Option,
792-
length=18, reserved=b'\x00',
793-
prefix=ip_network('2001:db8:1::/64')),
793+
length=18, reserved=b'\x00', prefix_length=64,
794+
prefix=ip_address('2001:db8:1::')),
794795
).prefix_length, 64)
795796
self.assertEqual(proto._make_opt_lla(
796797
Option.Mobility_Header_Link_Layer_Address_option,

0 commit comments

Comments
 (0)