From c43925cd24e96adad44992f6834826c6b6a1e85a Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Wed, 7 Oct 2026 12:25:27 -0400 Subject: [PATCH] fix(ipv6-opts): RPL option 0x23 raises AttributeError in _read_opt_unassigned The HOPOPT and IPv6-Opts option registries mapped only RPL_Option_0x63 to the RPL reader and maker. Type 0x23 (the RFC 9008 code) fell through to the unassigned handler, which read schema.data off the RPL schema. Register 0x23 alongside 0x63 in both registries and their docstring tables, and add tests/protocols/internet/test_rpl_option_unit.py. Closes #1196 --- pcapkit/protocols/internet/hopopt.py | 4 + pcapkit/protocols/internet/ipv6_opts.py | 4 + .../internet/test_rpl_option_unit.py | 74 +++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 tests/protocols/internet/test_rpl_option_unit.py diff --git a/pcapkit/protocols/internet/hopopt.py b/pcapkit/protocols/internet/hopopt.py index fc3bdaef82..6d23eff4e1 100644 --- a/pcapkit/protocols/internet/hopopt.py +++ b/pcapkit/protocols/internet/hopopt.py @@ -159,6 +159,9 @@ class HOPOPT(IPv6_Ext[Data_HOPOPT, Schema_HOPOPT], * - :attr:`~pcapkit.const.ipv6.option.Option.Quick_Start` - :meth:`~pcapkit.protocols.internet.hopopt.HOPOPT._read_opt_qs` - :meth:`~pcapkit.protocols.internet.hopopt.HOPOPT._make_opt_qs` + * - :attr:`~pcapkit.const.ipv6.option.Option.RPL_Option_0x23` + - :meth:`~pcapkit.protocols.internet.hopopt.HOPOPT._read_opt_rpl` + - :meth:`~pcapkit.protocols.internet.hopopt.HOPOPT._make_opt_rpl` * - :attr:`~pcapkit.const.ipv6.option.Option.RPL_Option_0x63` - :meth:`~pcapkit.protocols.internet.hopopt.HOPOPT._read_opt_rpl` - :meth:`~pcapkit.protocols.internet.hopopt.HOPOPT._make_opt_rpl` @@ -204,6 +207,7 @@ class HOPOPT(IPv6_Ext[Data_HOPOPT, Schema_HOPOPT], Enum_Option.SMF_DPD: 'smf_dpd', # [RFC 6621] Enum_Option.PDM: 'pdm', # [RFC 8250] 10 Enum_Option.Quick_Start: 'qs', # [RFC 4782][RFC Errata 2034] 6 + Enum_Option.RPL_Option_0x23: 'rpl', # [RFC 9008] Enum_Option.RPL_Option_0x63: 'rpl', # [RFC 6553] Enum_Option.MPL_Option: 'mpl', # [RFC 7731] Enum_Option.ILNP_Nonce: 'ilnp', # [RFC 6744] diff --git a/pcapkit/protocols/internet/ipv6_opts.py b/pcapkit/protocols/internet/ipv6_opts.py index a8e267bdbe..8a3983fd3e 100644 --- a/pcapkit/protocols/internet/ipv6_opts.py +++ b/pcapkit/protocols/internet/ipv6_opts.py @@ -163,6 +163,9 @@ class IPv6_Opts(IPv6_Ext[Data_IPv6_Opts, Schema_IPv6_Opts], * - :attr:`~pcapkit.const.ipv6.option.Option.Quick_Start` - :meth:`~pcapkit.protocols.internet.ipv6_opts.IPv6_Opts._read_opt_qs` - :meth:`~pcapkit.protocols.internet.ipv6_opts.IPv6_Opts._make_opt_qs` + * - :attr:`~pcapkit.const.ipv6.option.Option.RPL_Option_0x23` + - :meth:`~pcapkit.protocols.internet.ipv6_opts.IPv6_Opts._read_opt_rpl` + - :meth:`~pcapkit.protocols.internet.ipv6_opts.IPv6_Opts._make_opt_rpl` * - :attr:`~pcapkit.const.ipv6.option.Option.RPL_Option_0x63` - :meth:`~pcapkit.protocols.internet.ipv6_opts.IPv6_Opts._read_opt_rpl` - :meth:`~pcapkit.protocols.internet.ipv6_opts.IPv6_Opts._make_opt_rpl` @@ -208,6 +211,7 @@ class IPv6_Opts(IPv6_Ext[Data_IPv6_Opts, Schema_IPv6_Opts], Enum_Option.SMF_DPD: 'smf_dpd', # [RFC 6621] Enum_Option.PDM: 'pdm', # [RFC 8250] 10 Enum_Option.Quick_Start: 'qs', # [RFC 4782][RFC Errata 2034] 6 + Enum_Option.RPL_Option_0x23: 'rpl', # [RFC 9008] Enum_Option.RPL_Option_0x63: 'rpl', # [RFC 6553] Enum_Option.MPL_Option: 'mpl', # [RFC 7731] Enum_Option.ILNP_Nonce: 'ilnp', # [RFC 6744] diff --git a/tests/protocols/internet/test_rpl_option_unit.py b/tests/protocols/internet/test_rpl_option_unit.py new file mode 100644 index 0000000000..8a6c1d2b27 --- /dev/null +++ b/tests/protocols/internet/test_rpl_option_unit.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +"""HOPOPT and IPv6-Opts read and rebuild the RPL option under type 0x23. + +GitHub issue #1196: the ``__option__`` registries of :class:`HOPOPT` and +:class:`IPv6_Opts` mapped only ``RPL_Option_0x63`` to the RPL reader. Type +0x23, the RPL option code assigned by :rfc:`9008`, fell through to +``_read_opt_none``, which read ``schema.data`` off the RPL schema and raised +:exc:`AttributeError`. + +Every case builds its own octets in memory and reads no capture. The protocol +classes are imported inside each test, after +:func:`~tests._support.reimport_once_per_class`, as in +:mod:`tests.protocols.link.test_ethernet_mac_roundtrip_unit`. + +""" + +import unittest + +from tests._support import reimport_once_per_class + +#: Extension headers carrying one RPL option [:rfc:`6553#section-3`], next +#: header 59: flags ``O``, ``R`` and ``F`` set, RPLInstanceID 0x2A, SenderRank +#: 0x0102. Type 0x63 is already routed to the RPL reader and is the control. +HEADERS = { + 0x23: bytes.fromhex('3b002304e02a0102'), + 0x63: bytes.fromhex('3b006304e02a0102'), +} + + +def _classes() -> 'dict[str, type]': + from pcapkit.protocols.internet.hopopt import HOPOPT + from pcapkit.protocols.internet.ipv6_opts import IPv6_Opts + return {'HOPOPT': HOPOPT, 'IPv6_Opts': IPv6_Opts} + + +class TestRPLOption(unittest.TestCase): + """Pin the RPL option for both of its option types.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def test_rpl_option_is_read_by_the_rpl_reader(self) -> None: + for name, cls in _classes().items(): + for code, octets in HEADERS.items(): + with self.subTest(proto=name, type=hex(code)): + parsed = cls(octets, len(octets), extension=True) + opt = list(parsed.info.options.values())[0] + self.assertEqual(opt.type, code) + self.assertEqual(opt.length, 6) + self.assertEqual((opt.flags.down, opt.flags.rank_err, opt.flags.fwd_err), + (True, True, True)) + self.assertEqual(opt.id, 0x2A) + self.assertEqual(opt.rank, 0x0102) + + def test_rpl_option_rebuilds_byte_for_byte(self) -> None: + for name, cls in _classes().items(): + for code, octets in HEADERS.items(): + with self.subTest(proto=name, type=hex(code)): + parsed = cls(octets, len(octets), extension=True) + self.assertEqual(cls.from_data(parsed.info).data, octets) + + def test_malformed_rpl_option_raises_protocol_error(self) -> None: + from pcapkit.utilities.exceptions import ProtocolError + + # Opt Data Len 0, where the RPL option needs 4. + octets = bytes.fromhex('3b00230001020000') + for name, cls in _classes().items(): + with self.subTest(proto=name): + with self.assertRaises(ProtocolError): + cls(octets, len(octets), extension=True) + + +if __name__ == '__main__': + unittest.main()