Skip to content

Commit 37ee3cc

Browse files
authored
fix(internet)!: keep MH option 6 prefixes with host bits and option 27 times past 9999 (#2702) (#2709)
- 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 Home Network Prefix option refused a length past 128 to read and to make. RFC 5213 section 5.3.2 answers a prefix it cannot authorise with a status, so both take the 8-bit field's width, as option 6 does. - 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, and the Windows-range test reads a time past 9999 as Linux does, not raising. New tests fail on 57a6c36 without the pcapkit/ change; MH and guard modules pass.
1 parent bd82b76 commit 37ee3cc

7 files changed

Lines changed: 316 additions & 50 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: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3351,14 +3351,16 @@ def _read_opt_mnp(self, schema: 'Schema_MobileNetworkPrefixOption', *,
33513351
if schema.length != 18:
33523352
raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format')
33533353

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

@@ -4021,9 +4023,10 @@ def _read_opt_hnp(self, schema: 'Schema_HomeNetworkPrefixOption', *,
40214023
"""
40224024
if schema.length != 18:
40234025
raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format')
4024-
if schema.prefix_length > 128:
4025-
raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format')
40264026

4027+
# NOTE: A prefix length past 128 is kept, as for option 6: RFC 5213
4028+
# section 5.3.2 answers a prefix the local mobility anchor cannot
4029+
# authorise with a status rather than discarding the option (#2702).
40274030
data = Data_HomeNetworkPrefixOption(
40284031
type=schema.type,
40294032
length=self._mh_option_length(schema.length),
@@ -4238,12 +4241,23 @@ def _read_opt_timestamp(self, schema: 'Schema_TimestampOption', *,
42384241
seconds = schema.timestamp['seconds']
42394242
fraction = schema.timestamp['fraction']
42404243

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

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

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

82348252
Returns:
82358253
Constructed option schema.
82368254

8255+
Raises:
8256+
ProtocolError: If ``prefix`` is not an IPv6 address or network, or
8257+
``prefix_length`` is negative, a :obj:`bool`, or does not fit in
8258+
one octet.
8259+
8260+
Note:
8261+
The prefix length is checked against its 8-bit field alone, as
8262+
:rfc:`3963#section-6.2` has a home agent answer a prefix it cannot
8263+
use with a status rather than discard the option, so whatever the
8264+
reader keeps is written back.
8265+
8266+
.. versionchanged:: 1.6.0
8267+
8268+
New argument ``prefix_length``. ``prefix`` keeps the bits set past
8269+
its length, rather than raising :exc:`ValueError`, and a prefix
8270+
length past 128 is written (:issue:`2702`).
8271+
82378272
"""
82388273
if option is not None:
82398274
reserved = getattr(option, 'reserved', b'\x00')
8240-
prefix = option.prefix
8241-
8242-
prefix_val = ipaddress.ip_network(prefix)
8243-
if prefix_val.version != 6:
8244-
raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid mobile network prefix: {prefix!r}')
8245-
prefix_length = prefix_val.prefixlen
8246-
prefix_addr = prefix_val.network_address
8275+
prefix_length = option.prefix_length
8276+
prefix_addr = option.prefix
8277+
else:
8278+
try:
8279+
# NOTE: An interface, unlike a network, keeps the bits past its
8280+
# length, and takes an address, a network or text alike. A bool
8281+
# is refused first, as it would pass for the address ``::1``.
8282+
prefix_val = None if isinstance(prefix, bool) else ipaddress.IPv6Interface(prefix)
8283+
except ValueError:
8284+
prefix_val = None
8285+
if prefix_val is None:
8286+
raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid mobile network prefix: {prefix!r}')
8287+
if prefix_length is None:
8288+
prefix_length = prefix_val.network.prefixlen
8289+
prefix_addr = prefix_val.ip
8290+
prefix_length = self._make_uint(prefix_length, bits=8, name='prefix length')
82478291

82488292
return Schema_MobileNetworkPrefixOption(
82498293
type=type,
@@ -9005,7 +9049,18 @@ def _make_opt_hnp(self, type: 'Enum_Option', option: 'Optional[Data_HomeNetworkP
90059049

90069050
Raises:
90079051
ProtocolError: If ``prefix_length`` is negative, a :obj:`bool`,
9008-
or longer than the address.
9052+
or does not fit in one octet.
9053+
9054+
Note:
9055+
The prefix length is checked against its 8-bit field alone, as
9056+
:rfc:`5213#section-5.3.2` has a local mobility anchor answer a
9057+
prefix it cannot authorise with a status rather than discard the
9058+
option, so whatever the reader keeps is written back.
9059+
9060+
.. versionchanged:: 1.6.0
9061+
9062+
A prefix length longer than the address is written, rather than
9063+
refused, as the reader keeps one (:issue:`2702`).
90099064

90109065
"""
90119066
if option is not None:
@@ -9014,8 +9069,6 @@ def _make_opt_hnp(self, type: 'Enum_Option', option: 'Optional[Data_HomeNetworkP
90149069
prefix = option.prefix
90159070

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

90209073
return Schema_HomeNetworkPrefixOption(
90219074
type=type,

‎tests/protocols/internet/test_internet_maker_ranges_2614_unit.py‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -465,14 +465,15 @@ class MHMakerTests(_Case):
465465
'bra': ('Binding_Refresh_Advice', {}, {'interval': (0xFFFF, 'refresh interval')}),
466466
'ni': ('Nonce_Indices', {}, {'home': (0xFFFF, 'home nonce index'),
467467
'careof': (0xFFFF, 'care-of nonce index')}),
468+
'mnp': ('Mobile_Network_Prefix_Option', {}, {'prefix_length': (0xFF, 'prefix length')}),
468469
'auth': ('AUTH_OPTION_TYPE', {'data': b'\x00\x00'}, {'spi': (0xFFFFFFFF, 'SPI')}),
469470
'badf': ('Binding_Authorization_Data_for_FMIPv6', {'data': b'\x00'},
470471
{'spi': (0xFFFFFFFF, 'SPI')}),
471472
'ipv6_ap': ('Mobility_Header_IPv6_Address_Prefix', {}, {'prefix_length': (128, 'prefix length')}),
472473
'dns': ('DNS_UPDATE_TYPE', {}, {'reserved': (0x7F, 'reserved bits')}),
473474
'vendor': ('Vendor_Specific_Mobility_Option', {}, {'vendor': (0xFFFFFFFF, 'vendor ID'),
474475
'subtype': (0xFF, 'vendor subtype')}),
475-
'hnp': ('Home_Network_Prefix_Option', {}, {'prefix_length': (128, 'prefix length')}),
476+
'hnp': ('Home_Network_Prefix_Option', {}, {'prefix_length': (0xFF, 'prefix length')}),
476477
'hi': ('Handoff_Indicator_Option', {}, {'reserved': (0xFF, 'reserved octet')}),
477478
'timestamp': ('Timestamp_Option', {}, {'seconds': ((1 << 48) - 1, 'timestamp seconds'),
478479
'fraction': (0xFFFF, 'timestamp fraction')}),
@@ -580,11 +581,6 @@ class MHMakerTests(_Case):
580581
'maximum retransmissions'),
581582
)
582583

583-
UNPARSED_MAXIMA = {
584-
'_make_opt_timestamp.seconds': 'MH._read_opt_timestamp turns the seconds into a '
585-
'datetime, which cannot hold the 48-bit maximum',
586-
}
587-
588584
#: Integer arguments outside the sweep, and why.
589585
EXEMPT = {
590586
# 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)