From 7d9a53a1915f03f2bea2a51301f79174312f0cef Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Fri, 30 Oct 2026 15:40:29 -0400 Subject: [PATCH 1/2] feat(internet): implement ICMP and ICMPv6 Closes #2436. - ICMP: abstract base for the family, as IP is for IPv4/IPv6; id() is ('ICMP', 'ICMPv6'). Carries only the id() composition -- no shared method body. - ICMPv4 (RFC 792): own read/make, register_type, and all five message bodies (Echo, Destination Unreachable, Time Exceeded, Parameter Problem, raw fallback). id() is ('ICMP',). - ICMPv6 (RFC 4443): own read/make, register_type, and its six message bodies (adds Packet Too Big). id() is ('ICMPv6',). - Bind TransType 1 to ICMPv4 and 58 to ICMPv6; export all three. - Add register_icmp_type to pcapkit.foundation.registry, routing a code to whichever class's own register_type owns its enum. - Add IANA const/vendor registries for ICMP/ICMPv6 types and codes. - The checksum is parsed and rebuilt as-is, never verified or computed (#2470). - Docs pages; tests for round trips, selectors on parsed frames, and registry routing. --- README.md | 2 +- docs/source/contributing/pep.rst | 45 +- docs/source/demo.rst | 12 +- docs/source/ext.rst | 10 + docs/source/pcapkit/const/icmp.rst | 82 +++ docs/source/pcapkit/const/icmpv6.rst | 82 +++ docs/source/pcapkit/const/index.rst | 2 + docs/source/pcapkit/foundation/registry.rst | 2 + docs/source/pcapkit/protocols/index.rst | 9 +- .../pcapkit/protocols/internet/icmp.rst | 21 + .../pcapkit/protocols/internet/icmpv4.rst | 124 ++++ .../pcapkit/protocols/internet/icmpv6.rst | 134 ++++ .../pcapkit/protocols/internet/index.rst | 5 +- docs/source/pcapkit/vendor/icmp.rst | 78 ++ docs/source/pcapkit/vendor/icmpv6.rst | 78 ++ docs/source/pcapkit/vendor/index.rst | 2 + examples/generators/dispatch.py | 24 +- pcapkit/__init__.py | 4 +- pcapkit/all.py | 5 +- pcapkit/const/__init__.py | 7 + pcapkit/const/icmp/__init__.py | 40 + .../icmp/destination_unreachable_code.py | 111 +++ pcapkit/const/icmp/parameter_problem_code.py | 70 ++ pcapkit/const/icmp/time_exceeded_code.py | 67 ++ pcapkit/const/icmp/type.py | 179 +++++ pcapkit/const/icmpv6/__init__.py | 40 + .../icmpv6/destination_unreachable_code.py | 91 +++ .../const/icmpv6/parameter_problem_code.py | 94 +++ pcapkit/const/icmpv6/time_exceeded_code.py | 67 ++ pcapkit/const/icmpv6/type.py | 209 ++++++ pcapkit/foundation/__init__.py | 1 + pcapkit/foundation/registry/__init__.py | 1 + pcapkit/foundation/registry/protocols.py | 41 +- pcapkit/protocols/__init__.py | 3 +- pcapkit/protocols/data/__init__.py | 10 + pcapkit/protocols/data/internet/__init__.py | 29 + pcapkit/protocols/data/internet/icmpv4.py | 147 ++++ pcapkit/protocols/data/internet/icmpv6.py | 167 +++++ .../protocols/internet/NotImplemented/icmp.py | 0 .../internet/NotImplemented/icmpv6.py | 0 pcapkit/protocols/internet/__init__.py | 8 +- pcapkit/protocols/internet/icmp.py | 48 ++ pcapkit/protocols/internet/icmpv4.py | 620 ++++++++++++++++ pcapkit/protocols/internet/icmpv6.py | 682 ++++++++++++++++++ pcapkit/protocols/internet/internet.py | 6 + pcapkit/protocols/schema/__init__.py | 8 + pcapkit/protocols/schema/internet/__init__.py | 33 + pcapkit/protocols/schema/internet/icmpv4.py | 135 ++++ pcapkit/protocols/schema/internet/icmpv6.py | 144 ++++ pcapkit/vendor/__init__.py | 7 + pcapkit/vendor/icmp/__init__.py | 40 + .../icmp/destination_unreachable_code.py | 80 ++ pcapkit/vendor/icmp/parameter_problem_code.py | 80 ++ pcapkit/vendor/icmp/time_exceeded_code.py | 80 ++ pcapkit/vendor/icmp/type.py | 78 ++ pcapkit/vendor/icmpv6/__init__.py | 40 + .../icmpv6/destination_unreachable_code.py | 80 ++ .../vendor/icmpv6/parameter_problem_code.py | 80 ++ pcapkit/vendor/icmpv6/time_exceeded_code.py | 73 ++ pcapkit/vendor/icmpv6/type.py | 78 ++ tests/const/test_const_enum_builtin_parity.py | 6 +- tests/const/test_const_enum_get.py | 4 +- tests/const/test_const_enum_lookup.py | 2 +- tests/const/test_const_registry_protocol.py | 4 +- .../registry/test_icmp_type_registrar_unit.py | 92 +++ .../registry/test_registry_symmetry_unit.py | 1 + tests/integration/test_cli_subprocess.py | 2 +- tests/integration/test_engine_runtime.py | 4 +- tests/integration/test_output_formats.py | 2 +- .../internet/test_icmp_selector_runtime.py | 108 +++ .../internet/test_icmpv4_roundtrip_unit.py | 268 +++++++ .../internet/test_icmpv6_roundtrip_unit.py | 312 ++++++++ .../protocols/internet/test_internet_unit.py | 24 +- tests/protocols/internet/test_ip_runtime.py | 17 +- tests/protocols/link/test_link_runtime.py | 9 +- .../protocols/misc/pcap/test_frame_runtime.py | 4 +- .../protocols/test_dispatch_registry_unit.py | 10 +- tests/vendor/test_vendor_missing_body_unit.py | 5 +- 78 files changed, 5295 insertions(+), 74 deletions(-) create mode 100644 docs/source/pcapkit/const/icmp.rst create mode 100644 docs/source/pcapkit/const/icmpv6.rst create mode 100644 docs/source/pcapkit/protocols/internet/icmp.rst create mode 100644 docs/source/pcapkit/protocols/internet/icmpv4.rst create mode 100644 docs/source/pcapkit/protocols/internet/icmpv6.rst create mode 100644 docs/source/pcapkit/vendor/icmp.rst create mode 100644 docs/source/pcapkit/vendor/icmpv6.rst create mode 100644 pcapkit/const/icmp/__init__.py create mode 100644 pcapkit/const/icmp/destination_unreachable_code.py create mode 100644 pcapkit/const/icmp/parameter_problem_code.py create mode 100644 pcapkit/const/icmp/time_exceeded_code.py create mode 100644 pcapkit/const/icmp/type.py create mode 100644 pcapkit/const/icmpv6/__init__.py create mode 100644 pcapkit/const/icmpv6/destination_unreachable_code.py create mode 100644 pcapkit/const/icmpv6/parameter_problem_code.py create mode 100644 pcapkit/const/icmpv6/time_exceeded_code.py create mode 100644 pcapkit/const/icmpv6/type.py create mode 100644 pcapkit/protocols/data/internet/icmpv4.py create mode 100644 pcapkit/protocols/data/internet/icmpv6.py delete mode 100644 pcapkit/protocols/internet/NotImplemented/icmp.py delete mode 100644 pcapkit/protocols/internet/NotImplemented/icmpv6.py create mode 100644 pcapkit/protocols/internet/icmp.py create mode 100644 pcapkit/protocols/internet/icmpv4.py create mode 100644 pcapkit/protocols/internet/icmpv6.py create mode 100644 pcapkit/protocols/schema/internet/icmpv4.py create mode 100644 pcapkit/protocols/schema/internet/icmpv6.py create mode 100644 pcapkit/vendor/icmp/__init__.py create mode 100644 pcapkit/vendor/icmp/destination_unreachable_code.py create mode 100644 pcapkit/vendor/icmp/parameter_problem_code.py create mode 100644 pcapkit/vendor/icmp/time_exceeded_code.py create mode 100644 pcapkit/vendor/icmp/type.py create mode 100644 pcapkit/vendor/icmpv6/__init__.py create mode 100644 pcapkit/vendor/icmpv6/destination_unreachable_code.py create mode 100644 pcapkit/vendor/icmpv6/parameter_problem_code.py create mode 100644 pcapkit/vendor/icmpv6/time_exceeded_code.py create mode 100644 pcapkit/vendor/icmpv6/type.py create mode 100644 tests/foundation/registry/test_icmp_type_registrar_unit.py create mode 100644 tests/protocols/internet/test_icmp_selector_runtime.py create mode 100644 tests/protocols/internet/test_icmpv4_roundtrip_unit.py create mode 100644 tests/protocols/internet/test_icmpv6_roundtrip_unit.py diff --git a/README.md b/README.md index 9c7b656334..63f76f3308 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ its own parser. 6 >>> frame = extraction.frame[0] >>> str(frame.protochain) -'Ethernet:IPv6:IPv6_ICMP' +'Ethernet:IPv6:ICMPv6' >>> frame.info.time datetime.datetime(2017, 11, 19, 15, 49, 5, 471719, tzinfo=datetime.timezone.utc) >>> frame.payload.payload.src diff --git a/docs/source/contributing/pep.rst b/docs/source/contributing/pep.rst index 5ba6486462..47244f0f3c 100644 --- a/docs/source/contributing/pep.rst +++ b/docs/source/contributing/pep.rst @@ -41,7 +41,7 @@ one. Grouped by each TCP/IP layer and ordered by protocol name alphabetically, the ones outstanding are, * Link Layer: DSL, EAPOL, FDDI, ISDN, NDP, PPP -* Internet Layer: ECN, ICMP, ICMPv6, Shim6 +* Internet Layer: ECN, Shim6 * Transport Layer: DCCP, RSVP * Application Layer: BGP, DHCP, DHCPv6, DNS, IMAP, LDAP, MQTT, NNTP, NTP, ONC/RPC, POP, QUIC, RIP, RTP, SIP, SMTP, SNMP, SSH, Telnet, @@ -49,12 +49,15 @@ the ones outstanding are, Several of these come in pairs that want a shared abstract base rather than two independent implementations, in the way -:class:`~pcapkit.protocols.internet.ip.IP` already covers its family: ICMP -with ICMPv6, and TLS/SSL with DTLS. **Done** for TLS/SSL with DTLS: -:class:`~pcapkit.protocols.application.ssl.SSL` is the shared record-layer base -of :class:`~pcapkit.protocols.application.tls.TLS` and +:class:`~pcapkit.protocols.internet.ip.IP` and +:class:`~pcapkit.protocols.internet.icmp.ICMP` already cover their families: +IP and IPsec, and TLS/SSL with DTLS. **Done** for TLS/SSL +with DTLS: :class:`~pcapkit.protocols.application.ssl.SSL` is the shared +record-layer base of :class:`~pcapkit.protocols.application.tls.TLS` and :class:`~pcapkit.protocols.application.dtls.DTLS` -- see the `DTLS`_ section -below. +below. Neighbor Discovery (:rfc:`4861`, the ``NDP`` above) and Multicast +Listener Discovery (:rfc:`9777`), both carried in ICMPv6, are not implemented, +nor is ICMP/ICMPv6 checksum computation or verification (:issue:`2470`). :class:`~pcapkit.protocols.link.vlan.VLAN` is the closer precedent for a pair whose *layout* is identical -- it holds the whole of the tag, and :class:`~pcapkit.protocols.link.c_tag.C_Tag` and @@ -362,8 +365,9 @@ Follow-ups to the bindings above: port number. That depends on the proxy identifying the version positively instead of guessing, since a guess path is wrong on non-HTTP input. -Beyond those, the gaps most likely to be met in a real capture are ICMP (1) -and ICMPv6 (58) on the internet layer, each of which has a stub. +**Done.** ICMP (1), ICMPv6 (58) and IGMP (2) -- the internet layer gaps most +likely to be met in a real capture -- are all now implemented; ECN and Shim6 +above remain the open ones. PCAPNG Support -------------- @@ -1082,11 +1086,19 @@ post-wave-1 consistency sweep described under `Release Plan — 1.5.0 in Two Steps`_ above. **Wave 2 — the protocol pairs that want a shared abstract base**, per `More -Protocols, More!!!`_ above: **ICMP with ICMPv6** and **TLS/SSL with DTLS**, -plus **IGMP** and **DCCP** on their own. These are grouped because the shared-base -question is the design work and doing either half of a pair alone would answer it -twice. **DCCP** is the transport-layer implementation, tracked by :issue:`2489`, and -blocked on :issue:`2479` for its pseudo-header checksum. +Protocols, More!!!`_ above: **TLS/SSL with DTLS** and **IGMP** and **DCCP** on +their own. These are grouped because the shared-base question is the design work +and doing either half of a pair alone would answer it twice. **DCCP** is the +transport-layer implementation, tracked by :issue:`2489`, and blocked on +:issue:`2479` for its pseudo-header checksum. + +**Done within wave 2 — ICMP/ICMPv6 shared base.** :class:`~pcapkit.protocols.internet.icmp.ICMP` +is the abstract base :class:`~pcapkit.protocols.internet.icmpv4.ICMPv4` and +:class:`~pcapkit.protocols.internet.icmpv6.ICMPv6` share for selector composition +(:issue:`2436`); each implements its own message-type parsing independently rather than +through a shared method body. The two are distinguished by whether they carry pseudo-header +checksums (ICMPv6 does; ICMPv4 does not) and parse without checksum computation or verification +(:issue:`2470`). **IGMP v1/v2 — done within wave 2** (:issue:`2439`). The versioned classes :class:`IGMPv1 ` and @@ -1132,8 +1144,11 @@ above, which describes the payloads in question and why a registry of their own is wanted instead of :meth:`~pcapkit.protocols.protocol.Protocol._decode_next_layer`. It is sequenced *after* wave 2 rather than by preference: the multicast options embed -:rfc:`3810` MLD, which rides in **ICMPv6**, and :rfc:`3376` **IGMPv3** group -records — ICMPv6 a wave 2 deliverable, IGMPv3 :issue:`2448`. Landing the +:rfc:`9777` MLD (which obsoletes :rfc:`3810`; :rfc:`2710` for MLDv1), which +rides in **ICMPv6**, and :rfc:`3376` **IGMPv3** group records. ICMPv6 and +IGMPv1/v2 are both wave 2 deliverables, now done; MLD itself remains a +follow-up to ICMPv6 (see `More Protocols, More!!!`_ above), and IGMPv3 is +:issue:`2448` -- neither is part of wave 2's own scope. Landing the registry first would give a dispatch table with nothing but :class:`~pcapkit.protocols.misc.raw.Raw` to register. The counter-argument is real though, and worth weighing rather than dismissing: changing the diff --git a/docs/source/demo.rst b/docs/source/demo.rst index c9519e471d..0660f06339 100644 --- a/docs/source/demo.rst +++ b/docs/source/demo.rst @@ -72,8 +72,8 @@ Here are some usage samples: $ pcapkit-cli in --auto-extension --format plist --verbose 🚨 Loading file 'in.pcap' - Frame 1: Ethernet:IPv6:IPv6_ICMP - Frame 2: Ethernet:IPv6:IPv6_ICMP + Frame 1: Ethernet:IPv6:ICMPv6 + Frame 2: Ethernet:IPv6:ICMPv6 Frame 3: Ethernet:IPv4:TCP Frame 4: Ethernet:IPv4:TCP Frame 5: Ethernet:IPv4:TCP @@ -93,8 +93,8 @@ Here are some usage samples: $ pcapkit-cli in --auto-extension --output out.json --format json --verbose 🚨 Loading file 'in.pcap' - Frame 1: Ethernet:IPv6:IPv6_ICMP - Frame 2: Ethernet:IPv6:IPv6_ICMP + Frame 1: Ethernet:IPv6:ICMPv6 + Frame 2: Ethernet:IPv6:ICMPv6 Frame 3: Ethernet:IPv4:TCP Frame 4: Ethernet:IPv4:TCP Frame 5: Ethernet:IPv4:TCP @@ -107,8 +107,8 @@ Here are some usage samples: $ pcapkit-cli in.pcap --output out.txt --format tree --verbose 🚨 Loading file 'in.pcap' - Frame 1: Ethernet:IPv6:IPv6_ICMP - Frame 2: Ethernet:IPv6:IPv6_ICMP + Frame 1: Ethernet:IPv6:ICMPv6 + Frame 2: Ethernet:IPv6:ICMPv6 Frame 3: Ethernet:IPv4:TCP Frame 4: Ethernet:IPv4:TCP Frame 5: Ethernet:IPv4:TCP diff --git a/docs/source/ext.rst b/docs/source/ext.rst index 9697b9394f..184af04427 100644 --- a/docs/source/ext.rst +++ b/docs/source/ext.rst @@ -68,6 +68,12 @@ The protocol classes :mod:`pcapkit` ships: + + + +-------------------------------------------------------------+ | | | | :class:`pcapkit.protocols.internet.esp.ESP` | + +----------------+-----------------------+-------------------------------------------------------------+ +| | | :class:`pcapkit.protocols.internet.icmp.ICMP` | ++ + +-----------------------+-------------------------------------------------------------+ +| | ICMP Family | :class:`pcapkit.protocols.internet.icmpv4.ICMPv4` | ++ + +-----------------------+-------------------------------------------------------------+ +| | | :class:`pcapkit.protocols.internet.icmpv6.ICMPv6` | ++ +----------------+-----------------------+-------------------------------------------------------------+ | | :class:`pcapkit.protocols.internet.ipx.IPX` | + +----------------+-----------------------+-------------------------------------------------------------+ | | :class:`pcapkit.protocols.internet.hip.HIP` | @@ -375,6 +381,10 @@ writes them: | | :class:`~pcapkit.protocols.internet.mh.MH` | :attr:`MH.__option__ ` | :func:`~pcapkit.foundation.registry.protocols.register_mh_option` | + + +-----------------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ | | | :attr:`MH.__extension__ ` | :func:`~pcapkit.foundation.registry.protocols.register_mh_extension` | ++ +------------------------------------------------------------+-----------------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ +| | :class:`~pcapkit.protocols.internet.icmpv4.ICMPv4` | :attr:`ICMPv4.__type__ ` | :meth:`~pcapkit.protocols.internet.icmpv4.ICMPv4.register_type` | ++ +------------------------------------------------------------+-----------------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ +| | :class:`~pcapkit.protocols.internet.icmpv6.ICMPv6` | :attr:`ICMPv6.__type__ ` | :meth:`~pcapkit.protocols.internet.icmpv6.ICMPv6.register_type` | +-------------------+------------------------------------------------------------+-----------------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ | | | :attr:`TCP.__option__ ` | :func:`~pcapkit.foundation.registry.protocols.register_tcp_option` | + Transport Layer + :class:`~pcapkit.protocols.transport.tcp.TCP` +-----------------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ diff --git a/docs/source/pcapkit/const/icmp.rst b/docs/source/pcapkit/const/icmp.rst new file mode 100644 index 0000000000..e7e484e4a4 --- /dev/null +++ b/docs/source/pcapkit/const/icmp.rst @@ -0,0 +1,82 @@ +======================================================================== +:class:`~pcapkit.protocols.internet.icmpv4.ICMPv4` Constant Enumerations +======================================================================== + +.. module:: pcapkit.const.icmp + +This module contains all constant enumerations of +:class:`~pcapkit.protocols.internet.icmpv4.ICMPv4` implementations. Available +enumerations include: + +.. list-table:: + + * - :class:`ICMP_Type ` + - ICMP Type Numbers [*]_ + * - :class:`ICMP_DestinationUnreachableCode ` + - ICMP Destination Unreachable Codes [*]_ + * - :class:`ICMP_TimeExceededCode ` + - ICMP Time Exceeded Codes [*]_ + * - :class:`ICMP_ParameterProblemCode ` + - ICMP Parameter Problem Codes [*]_ + +ICMP Type Numbers +================= + +.. module:: pcapkit.const.icmp.type + +This module contains the constant enumeration for **ICMP Type Numbers**, +which is automatically generated from :class:`pcapkit.vendor.icmp.type.Type`. + +.. autoclass:: pcapkit.const.icmp.type.Type + :members: + :undoc-members: + :show-inheritance: + +ICMP Destination Unreachable Codes +=================================== + +.. module:: pcapkit.const.icmp.destination_unreachable_code + +This module contains the constant enumeration for **ICMP Destination +Unreachable Codes**, which is automatically generated from +:class:`pcapkit.vendor.icmp.destination_unreachable_code.DestinationUnreachableCode`. + +.. autoclass:: pcapkit.const.icmp.destination_unreachable_code.DestinationUnreachableCode + :members: + :undoc-members: + :show-inheritance: + +ICMP Time Exceeded Codes +========================= + +.. module:: pcapkit.const.icmp.time_exceeded_code + +This module contains the constant enumeration for **ICMP Time Exceeded +Codes**, which is automatically generated from +:class:`pcapkit.vendor.icmp.time_exceeded_code.TimeExceededCode`. + +.. autoclass:: pcapkit.const.icmp.time_exceeded_code.TimeExceededCode + :members: + :undoc-members: + :show-inheritance: + +ICMP Parameter Problem Codes +============================== + +.. module:: pcapkit.const.icmp.parameter_problem_code + +This module contains the constant enumeration for **ICMP Parameter Problem +Codes**, which is automatically generated from +:class:`pcapkit.vendor.icmp.parameter_problem_code.ParameterProblemCode`. + +.. autoclass:: pcapkit.const.icmp.parameter_problem_code.ParameterProblemCode + :members: + :undoc-members: + :show-inheritance: + +.. rubric:: Footnotes + +.. [*] https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-types +.. [*] https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes +.. [*] https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes +.. [*] https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes diff --git a/docs/source/pcapkit/const/icmpv6.rst b/docs/source/pcapkit/const/icmpv6.rst new file mode 100644 index 0000000000..4702a571ca --- /dev/null +++ b/docs/source/pcapkit/const/icmpv6.rst @@ -0,0 +1,82 @@ +========================================================================= +:class:`~pcapkit.protocols.internet.icmpv6.ICMPv6` Constant Enumerations +========================================================================= + +.. module:: pcapkit.const.icmpv6 + +This module contains all constant enumerations of +:class:`~pcapkit.protocols.internet.icmpv6.ICMPv6` implementations. Available +enumerations include: + +.. list-table:: + + * - :class:`ICMPv6_Type ` + - ICMPv6 Type Numbers [*]_ + * - :class:`ICMPv6_DestinationUnreachableCode ` + - ICMPv6 Destination Unreachable Codes [*]_ + * - :class:`ICMPv6_TimeExceededCode ` + - ICMPv6 Time Exceeded Codes [*]_ + * - :class:`ICMPv6_ParameterProblemCode ` + - ICMPv6 Parameter Problem Codes [*]_ + +ICMPv6 Type Numbers +==================== + +.. module:: pcapkit.const.icmpv6.type + +This module contains the constant enumeration for **ICMPv6 Type Numbers**, +which is automatically generated from :class:`pcapkit.vendor.icmpv6.type.Type`. + +.. autoclass:: pcapkit.const.icmpv6.type.Type + :members: + :undoc-members: + :show-inheritance: + +ICMPv6 Destination Unreachable Codes +====================================== + +.. module:: pcapkit.const.icmpv6.destination_unreachable_code + +This module contains the constant enumeration for **ICMPv6 Destination +Unreachable Codes**, which is automatically generated from +:class:`pcapkit.vendor.icmpv6.destination_unreachable_code.DestinationUnreachableCode`. + +.. autoclass:: pcapkit.const.icmpv6.destination_unreachable_code.DestinationUnreachableCode + :members: + :undoc-members: + :show-inheritance: + +ICMPv6 Time Exceeded Codes +============================ + +.. module:: pcapkit.const.icmpv6.time_exceeded_code + +This module contains the constant enumeration for **ICMPv6 Time Exceeded +Codes**, which is automatically generated from +:class:`pcapkit.vendor.icmpv6.time_exceeded_code.TimeExceededCode`. + +.. autoclass:: pcapkit.const.icmpv6.time_exceeded_code.TimeExceededCode + :members: + :undoc-members: + :show-inheritance: + +ICMPv6 Parameter Problem Codes +================================ + +.. module:: pcapkit.const.icmpv6.parameter_problem_code + +This module contains the constant enumeration for **ICMPv6 Parameter +Problem Codes**, which is automatically generated from +:class:`pcapkit.vendor.icmpv6.parameter_problem_code.ParameterProblemCode`. + +.. autoclass:: pcapkit.const.icmpv6.parameter_problem_code.ParameterProblemCode + :members: + :undoc-members: + :show-inheritance: + +.. rubric:: Footnotes + +.. [*] https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml#icmpv6-parameters-types +.. [*] https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml#icmpv6-parameters-type-1 +.. [*] https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml#icmpv6-parameters-type-3 +.. [*] https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml#icmpv6-parameters-type-4 diff --git a/docs/source/pcapkit/const/index.rst b/docs/source/pcapkit/const/index.rst index b23be70340..e92355c628 100644 --- a/docs/source/pcapkit/const/index.rst +++ b/docs/source/pcapkit/const/index.rst @@ -80,6 +80,8 @@ Internet Layer esp hip + icmp + icmpv6 igmp ipv4 ipv6 diff --git a/docs/source/pcapkit/foundation/registry.rst b/docs/source/pcapkit/foundation/registry.rst index ffea7381a9..b37b6cec10 100644 --- a/docs/source/pcapkit/foundation/registry.rst +++ b/docs/source/pcapkit/foundation/registry.rst @@ -90,6 +90,8 @@ Internet Layer Registries .. autofunction:: pcapkit.foundation.registry.protocols.register_ipv4_option +.. autofunction:: pcapkit.foundation.registry.protocols.register_icmp_type + .. autofunction:: pcapkit.foundation.registry.protocols.register_hip_parameter .. autofunction:: pcapkit.foundation.registry.protocols.register_hopopt_option diff --git a/docs/source/pcapkit/protocols/index.rst b/docs/source/pcapkit/protocols/index.rst index 3178abb223..4244985bb9 100644 --- a/docs/source/pcapkit/protocols/index.rst +++ b/docs/source/pcapkit/protocols/index.rst @@ -83,12 +83,16 @@ from given information. The class hierarchy of :mod:`pcapkit.protocols`: end subgraph internet [Internet Layer] - Internet --> IPX & IP & IPsec & IPv6_Ext & IGMP + Internet --> IPX & IP & ICMP & IPsec & IPv6_Ext & IGMP subgraph ip [IP Family] IP --> IPv4 & IPv6 end + subgraph icmp [ICMP Family] + ICMP --> ICMPv4 & ICMPv6 + end + subgraph ipsec [IPsec Family] IPsec --> AH & ESP end @@ -143,6 +147,9 @@ from given information. The class hierarchy of :mod:`pcapkit.protocols`: click IGMPv1 "internet/igmpv1.html#pcapkit.protocols.internet.igmpv1.IGMP" click IGMPv2 "internet/igmpv2.html#pcapkit.protocols.internet.igmpv2.IGMP" click HOPOPT "internet/hopopt.html#pcapkit.protocols.internet.hopopt.HOPOPT" + click ICMP "internet/icmp.html#pcapkit.protocols.internet.icmp.ICMP" + click ICMPv4 "internet/icmpv4.html#pcapkit.protocols.internet.icmpv4.ICMPv4" + click ICMPv6 "internet/icmpv6.html#pcapkit.protocols.internet.icmpv6.ICMPv6" click IP "internet/ip.html#pcapkit.protocols.internet.ip.IP" click IPsec "internet/ipsec.html#pcapkit.protocols.internet.ipsec.IPsec" click IPv4 "internet/ipv4.html#pcapkit.protocols.internet.ipv4.IPv4" diff --git a/docs/source/pcapkit/protocols/internet/icmp.rst b/docs/source/pcapkit/protocols/internet/icmp.rst new file mode 100644 index 0000000000..652e18f238 --- /dev/null +++ b/docs/source/pcapkit/protocols/internet/icmp.rst @@ -0,0 +1,21 @@ +ICMP - Internet Control Message Protocol +======================================== + +.. module:: pcapkit.protocols.internet.icmp + +:mod:`pcapkit.protocols.internet.icmp` contains +:class:`~pcapkit.protocols.internet.icmp.ICMP` only, +which is a base class for Internet Control Message +Protocol (ICMP) protocol family [*]_, e.g. +:class:`~pcapkit.protocols.internet.icmpv4.ICMPv4` and +:class:`~pcapkit.protocols.internet.icmpv6.ICMPv6`. + +.. autoclass:: pcapkit.protocols.internet.icmp.ICMP + :no-members: + :show-inheritance: + + .. automethod:: id + +.. rubric:: Footnotes + +.. [*] https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol diff --git a/docs/source/pcapkit/protocols/internet/icmpv4.rst b/docs/source/pcapkit/protocols/internet/icmpv4.rst new file mode 100644 index 0000000000..f84b876a1c --- /dev/null +++ b/docs/source/pcapkit/protocols/internet/icmpv4.rst @@ -0,0 +1,124 @@ +ICMPv4 - Internet Control Message Protocol +========================================== + +.. module:: pcapkit.protocols.internet.icmpv4 + +:mod:`pcapkit.protocols.internet.icmpv4` contains +:class:`~pcapkit.protocols.internet.icmpv4.ICMPv4` only, +which implements extractor for Internet Control +Message Protocol (ICMP) [*]_, whose structure is +described as below: + +======= ========= ======================= =================================== +Octets Bits Name Description +======= ========= ======================= =================================== + 0 0 ``icmpv4.type`` Type + 1 8 ``icmpv4.code`` Code + 2 16 ``icmpv4.checksum`` Checksum + 4 32 ``icmpv4.data`` Message Body +======= ========= ======================= =================================== + +.. autoclass:: pcapkit.protocols.internet.icmpv4.ICMPv4 + :no-members: + :show-inheritance: + + .. autoproperty:: name + .. autoproperty:: alias + .. autoproperty:: length + + .. automethod:: id + .. automethod:: register_type + + .. automethod:: read + .. automethod:: make + + .. automethod:: _make_data + + .. automethod:: _read_data_type_none + .. automethod:: _read_data_type_echo + .. automethod:: _read_data_type_destination_unreachable + .. automethod:: _read_data_type_time_exceeded + .. automethod:: _read_data_type_parameter_problem + + .. automethod:: _make_data_type_none + .. automethod:: _make_data_type_echo + .. automethod:: _make_data_type_destination_unreachable + .. automethod:: _make_data_type_time_exceeded + .. automethod:: _make_data_type_parameter_problem + + .. autoattribute:: __type__ + :no-value: + + .. automethod:: __length_hint__ + .. automethod:: __index__ + +Header Schemas +-------------- + +.. module:: pcapkit.protocols.schema.internet.icmpv4 + +.. autoclass:: pcapkit.protocols.schema.internet.icmpv4.ICMPv4 + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.icmpv4.ICMPv4Type + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.icmpv4.Unknown + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.icmpv4.Echo + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.icmpv4.DestinationUnreachable + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.icmpv4.TimeExceeded + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.icmpv4.ParameterProblem + :members: + :show-inheritance: + +Auxiliary Functions +~~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: pcapkit.protocols.schema.internet.icmpv4.icmpv4_data_selector + +Data Models +----------- + +.. module:: pcapkit.protocols.data.internet.icmpv4 + +.. autoclass:: pcapkit.protocols.data.internet.icmpv4.ICMPv4 + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.icmpv4.Unknown + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.icmpv4.Echo + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.icmpv4.DestinationUnreachable + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.icmpv4.TimeExceeded + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.icmpv4.ParameterProblem + :members: + :show-inheritance: + +.. rubric:: Footnotes + +.. [*] https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol diff --git a/docs/source/pcapkit/protocols/internet/icmpv6.rst b/docs/source/pcapkit/protocols/internet/icmpv6.rst new file mode 100644 index 0000000000..bfb09ba422 --- /dev/null +++ b/docs/source/pcapkit/protocols/internet/icmpv6.rst @@ -0,0 +1,134 @@ +ICMPv6 - Internet Control Message Protocol for IPv6 +======================================================= + +.. module:: pcapkit.protocols.internet.icmpv6 + +:mod:`pcapkit.protocols.internet.icmpv6` contains +:class:`~pcapkit.protocols.internet.icmpv6.ICMPv6` only, +which implements extractor for Internet Control +Message Protocol for IPv6 (ICMPv6) [*]_, whose structure +is described as below: + +======= ========= ======================= =================================== +Octets Bits Name Description +======= ========= ======================= =================================== + 0 0 ``icmpv6.type`` Type + 1 8 ``icmpv6.code`` Code + 2 16 ``icmpv6.checksum`` Checksum + 4 32 ``icmpv6.data`` Message Body +======= ========= ======================= =================================== + +.. autoclass:: pcapkit.protocols.internet.icmpv6.ICMPv6 + :no-members: + :show-inheritance: + + .. autoproperty:: name + .. autoproperty:: alias + .. autoproperty:: length + + .. automethod:: id + .. automethod:: register_type + + .. automethod:: read + .. automethod:: make + + .. automethod:: _make_data + + .. automethod:: _read_data_type_none + .. automethod:: _read_data_type_destination_unreachable + .. automethod:: _read_data_type_packet_too_big + .. automethod:: _read_data_type_time_exceeded + .. automethod:: _read_data_type_parameter_problem + .. automethod:: _read_data_type_echo + + .. automethod:: _make_data_type_none + .. automethod:: _make_data_type_destination_unreachable + .. automethod:: _make_data_type_packet_too_big + .. automethod:: _make_data_type_time_exceeded + .. automethod:: _make_data_type_parameter_problem + .. automethod:: _make_data_type_echo + + .. autoattribute:: __type__ + :no-value: + + .. automethod:: __length_hint__ + .. automethod:: __index__ + +Header Schemas +-------------- + +.. module:: pcapkit.protocols.schema.internet.icmpv6 + +.. autoclass:: pcapkit.protocols.schema.internet.icmpv6.ICMPv6 + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.icmpv6.ICMPv6Type + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.icmpv6.Unknown + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.icmpv6.DestinationUnreachable + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.icmpv6.PacketTooBig + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.icmpv6.TimeExceeded + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.icmpv6.ParameterProblem + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.internet.icmpv6.Echo + :members: + :show-inheritance: + +Auxiliary Functions +~~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: pcapkit.protocols.schema.internet.icmpv6.icmpv6_data_selector + +Data Models +----------- + +.. module:: pcapkit.protocols.data.internet.icmpv6 + +.. autoclass:: pcapkit.protocols.data.internet.icmpv6.ICMPv6 + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.icmpv6.Unknown + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.icmpv6.DestinationUnreachable + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.icmpv6.PacketTooBig + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.icmpv6.TimeExceeded + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.icmpv6.ParameterProblem + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.internet.icmpv6.Echo + :members: + :show-inheritance: + +.. rubric:: Footnotes + +.. [*] https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol_for_IPv6 diff --git a/docs/source/pcapkit/protocols/internet/index.rst b/docs/source/pcapkit/protocols/internet/index.rst index 20641c94dc..11d9d166e2 100644 --- a/docs/source/pcapkit/protocols/internet/index.rst +++ b/docs/source/pcapkit/protocols/internet/index.rst @@ -25,6 +25,9 @@ internet layer, with detailed implementation and methods. ipsec ah esp + icmp + icmpv4 + icmpv6 hip igmp igmpv1 @@ -33,7 +36,7 @@ internet layer, with detailed implementation and methods. .. todo:: - Implement ECN, ICMP, ICMPv6, Shim6. + Implement ECN, Shim6. Protocol Registry ----------------- diff --git a/docs/source/pcapkit/vendor/icmp.rst b/docs/source/pcapkit/vendor/icmp.rst new file mode 100644 index 0000000000..022072c45e --- /dev/null +++ b/docs/source/pcapkit/vendor/icmp.rst @@ -0,0 +1,78 @@ +================================================================== +:class:`~pcapkit.protocols.internet.icmpv4.ICMPv4` Vendor Crawlers +================================================================== + +.. module:: pcapkit.vendor.icmp + +This module contains all vendor crawlers of +:class:`~pcapkit.protocols.internet.icmpv4.ICMPv4` implementations. Available +vendor crawlers include: + +.. list-table:: + + * - :class:`ICMP_Type ` + - ICMP Type Numbers [*]_ + * - :class:`ICMP_DestinationUnreachableCode ` + - ICMP Destination Unreachable Codes [*]_ + * - :class:`ICMP_TimeExceededCode ` + - ICMP Time Exceeded Codes [*]_ + * - :class:`ICMP_ParameterProblemCode ` + - ICMP Parameter Problem Codes [*]_ + +ICMP Type Numbers +================= + +.. module:: pcapkit.vendor.icmp.type + +This module contains the vendor crawler for **ICMP Type Numbers**, +which is automatically generating :class:`pcapkit.const.icmp.type.Type`. + +.. autoclass:: pcapkit.vendor.icmp.type.Type + :members: FLAG + :show-inheritance: + +ICMP Destination Unreachable Codes +=================================== + +.. module:: pcapkit.vendor.icmp.destination_unreachable_code + +This module contains the vendor crawler for **ICMP Destination Unreachable +Codes**, which is automatically generating +:class:`pcapkit.const.icmp.destination_unreachable_code.DestinationUnreachableCode`. + +.. autoclass:: pcapkit.vendor.icmp.destination_unreachable_code.DestinationUnreachableCode + :members: FLAG + :show-inheritance: + +ICMP Time Exceeded Codes +========================= + +.. module:: pcapkit.vendor.icmp.time_exceeded_code + +This module contains the vendor crawler for **ICMP Time Exceeded Codes**, +which is automatically generating +:class:`pcapkit.const.icmp.time_exceeded_code.TimeExceededCode`. + +.. autoclass:: pcapkit.vendor.icmp.time_exceeded_code.TimeExceededCode + :members: FLAG + :show-inheritance: + +ICMP Parameter Problem Codes +============================== + +.. module:: pcapkit.vendor.icmp.parameter_problem_code + +This module contains the vendor crawler for **ICMP Parameter Problem +Codes**, which is automatically generating +:class:`pcapkit.const.icmp.parameter_problem_code.ParameterProblemCode`. + +.. autoclass:: pcapkit.vendor.icmp.parameter_problem_code.ParameterProblemCode + :members: FLAG + :show-inheritance: + +.. rubric:: Footnotes + +.. [*] https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-types +.. [*] https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes +.. [*] https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes +.. [*] https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes diff --git a/docs/source/pcapkit/vendor/icmpv6.rst b/docs/source/pcapkit/vendor/icmpv6.rst new file mode 100644 index 0000000000..8712367514 --- /dev/null +++ b/docs/source/pcapkit/vendor/icmpv6.rst @@ -0,0 +1,78 @@ +================================================================== +:class:`~pcapkit.protocols.internet.icmpv6.ICMPv6` Vendor Crawlers +================================================================== + +.. module:: pcapkit.vendor.icmpv6 + +This module contains all vendor crawlers of +:class:`~pcapkit.protocols.internet.icmpv6.ICMPv6` implementations. Available +vendor crawlers include: + +.. list-table:: + + * - :class:`ICMPv6_Type ` + - ICMPv6 Type Numbers [*]_ + * - :class:`ICMPv6_DestinationUnreachableCode ` + - ICMPv6 Destination Unreachable Codes [*]_ + * - :class:`ICMPv6_TimeExceededCode ` + - ICMPv6 Time Exceeded Codes [*]_ + * - :class:`ICMPv6_ParameterProblemCode ` + - ICMPv6 Parameter Problem Codes [*]_ + +ICMPv6 Type Numbers +==================== + +.. module:: pcapkit.vendor.icmpv6.type + +This module contains the vendor crawler for **ICMPv6 Type Numbers**, +which is automatically generating :class:`pcapkit.const.icmpv6.type.Type`. + +.. autoclass:: pcapkit.vendor.icmpv6.type.Type + :members: FLAG + :show-inheritance: + +ICMPv6 Destination Unreachable Codes +====================================== + +.. module:: pcapkit.vendor.icmpv6.destination_unreachable_code + +This module contains the vendor crawler for **ICMPv6 Destination +Unreachable Codes**, which is automatically generating +:class:`pcapkit.const.icmpv6.destination_unreachable_code.DestinationUnreachableCode`. + +.. autoclass:: pcapkit.vendor.icmpv6.destination_unreachable_code.DestinationUnreachableCode + :members: FLAG + :show-inheritance: + +ICMPv6 Time Exceeded Codes +============================ + +.. module:: pcapkit.vendor.icmpv6.time_exceeded_code + +This module contains the vendor crawler for **ICMPv6 Time Exceeded +Codes**, which is automatically generating +:class:`pcapkit.const.icmpv6.time_exceeded_code.TimeExceededCode`. + +.. autoclass:: pcapkit.vendor.icmpv6.time_exceeded_code.TimeExceededCode + :members: FLAG + :show-inheritance: + +ICMPv6 Parameter Problem Codes +================================ + +.. module:: pcapkit.vendor.icmpv6.parameter_problem_code + +This module contains the vendor crawler for **ICMPv6 Parameter Problem +Codes**, which is automatically generating +:class:`pcapkit.const.icmpv6.parameter_problem_code.ParameterProblemCode`. + +.. autoclass:: pcapkit.vendor.icmpv6.parameter_problem_code.ParameterProblemCode + :members: FLAG + :show-inheritance: + +.. rubric:: Footnotes + +.. [*] https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml#icmpv6-parameters-types +.. [*] https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml#icmpv6-parameters-type-1 +.. [*] https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml#icmpv6-parameters-type-3 +.. [*] https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml#icmpv6-parameters-type-4 diff --git a/docs/source/pcapkit/vendor/index.rst b/docs/source/pcapkit/vendor/index.rst index 9016e1394a..e858ad44b6 100644 --- a/docs/source/pcapkit/vendor/index.rst +++ b/docs/source/pcapkit/vendor/index.rst @@ -56,6 +56,8 @@ Internet Layer esp hip + icmp + icmpv6 igmp ipv4 ipv6 diff --git a/examples/generators/dispatch.py b/examples/generators/dispatch.py index 08ee4cd2e7..31a42ac97b 100644 --- a/examples/generators/dispatch.py +++ b/examples/generators/dispatch.py @@ -342,10 +342,12 @@ def _link_build(code: 'int') -> 'Any': # Internet -- TransType. ############################################################################### -#: Codes dispatched over an IPv4 outer header. IGMP (2) is canonically -#: IPv4-only -- multicast's IPv6 equivalent is MLD, which rides in ICMPv6 -#: rather than as its own IP protocol number (#2439). -_INTERNET_OVER_IPV4 = frozenset({2, 4, 6, 17, 50, 51, 89, 111, 132}) +#: Codes dispatched over an IPv4 outer header. ICMP (1) is canonically +#: IPv4-only -- its IPv6 equivalent is ICMPv6 (58), dispatched separately +#: below. IGMP (2) is likewise IPv4-only -- multicast's IPv6 equivalent is +#: MLD, which rides in ICMPv6 rather than as its own IP protocol number +#: (#2439). +_INTERNET_OVER_IPV4 = frozenset({1, 2, 4, 6, 17, 50, 51, 89, 111, 132}) def _internet_registry() -> 'Any': @@ -362,6 +364,18 @@ def _internet_payload(code: 'int') -> 'bytes': return bytes(HOPOPT(options=[], next=6, payload=_tcp(9999))) if code == TransType.IPv4: # IP-in-IP. return _ip4(6, _tcp(9999)) + if code == TransType.ICMP: + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + + # NOTE: the checksum is left uncomputed (#2470). + return bytes(ICMPv4(type=8, code=0, + body={'id': 1, 'seq': 1, 'data': b'ping'})) + if code == TransType.IPv6_ICMP: + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + + # NOTE: the checksum is left uncomputed (#2470). + return bytes(ICMPv6(type=128, code=0, + body={'id': 1, 'seq': 1, 'data': b'ping'})) if code == TransType.TCP: return _tcp(9999) if code == TransType.UDP: @@ -678,9 +692,11 @@ def _link_enum() -> 'Any': # -- Internet.__proto__ (TransType) --------------------------------------- 'internet/HOPOPT': ('pcapkit.protocols.internet.hopopt', 'HOPOPT'), 'internet/IPv4': ('pcapkit.protocols.internet.ipv4', 'IPv4'), + 'internet/ICMP': ('pcapkit.protocols.internet.icmpv4', 'ICMPv4'), 'internet/TCP': ('pcapkit.protocols.transport.tcp', 'TCP'), 'internet/UDP': ('pcapkit.protocols.transport.udp', 'UDP'), 'internet/IPv6': ('pcapkit.protocols.internet.ipv6', 'IPv6'), + 'internet/IPv6_ICMP': ('pcapkit.protocols.internet.icmpv6', 'ICMPv6'), 'internet/IPv6_Route': ('pcapkit.protocols.internet.ipv6_route', 'IPv6_Route'), 'internet/IPv6_Frag': ('pcapkit.protocols.internet.ipv6_frag', 'IPv6_Frag'), 'internet/ESP': ('pcapkit.protocols.internet.esp', 'ESP'), diff --git a/pcapkit/__init__.py b/pcapkit/__init__.py index 19841fab0c..beeeef214d 100644 --- a/pcapkit/__init__.py +++ b/pcapkit/__init__.py @@ -114,8 +114,8 @@ 'ARP', 'C_Tag', 'Ethernet', 'InARP', 'L2TP', # Link Layer 'L2TPv2', 'LinuxSLL', 'LinuxSLL1', 'LinuxSLL2', 'Loopback', 'S_Tag', 'VLAN', - 'AH', 'ESP', 'IGMP', 'IGMPv1', 'IGMPv2', # Internet Layer - 'IP', 'IPsec', 'IPv4', 'IPv6', 'IPX', + 'AH', 'ESP', 'ICMP', 'ICMPv4', 'ICMPv6', # Internet Layer + 'IGMP', 'IGMPv1', 'IGMPv2', 'IP', 'IPsec', 'IPv4', 'IPv6', 'IPX', 'HIP', 'HOPOPT', 'IPv6_Frag', 'IPv6_Ext', 'IPv6_Opts', 'IPv6_Route', 'MH', # IPv6 Extension Header diff --git a/pcapkit/all.py b/pcapkit/all.py index 20b9563aa7..7f2e4fd8f9 100644 --- a/pcapkit/all.py +++ b/pcapkit/all.py @@ -91,6 +91,7 @@ 'register_pcap', 'register_pcapng', 'register_ethertype', 'register_transtype', + 'register_icmp_type', 'register_ipv4_option', 'register_hip_parameter', 'register_hopopt_option', 'register_ipv6_opts_option', 'register_ipv6_route_routing', 'register_mh_message', 'register_mh_option', 'register_mh_extension', @@ -122,8 +123,8 @@ 'Raw', # Raw Packet 'ARP', 'C_Tag', 'Ethernet', 'InARP', 'L2TP', 'L2TPv2', 'LinuxSLL', 'LinuxSLL1', 'LinuxSLL2', 'Loopback', 'S_Tag', 'VLAN', # Link Layer - 'AH', 'ESP', 'IGMP', 'IGMPv1', 'IGMPv2', # Internet Layer - 'IP', 'IPsec', 'IPv4', 'IPv6', 'IPX', + 'AH', 'ESP', 'ICMP', 'ICMPv4', 'ICMPv6', 'IGMP', 'IGMPv1', 'IGMPv2', + 'IP', 'IPsec', 'IPv4', 'IPv6', 'IPX', # Internet Layer 'HIP', 'HOPOPT', 'IPv6_Ext', 'IPv6_Frag', 'IPv6_Opts', 'IPv6_Route', 'MH', # IPv6 Extension Header 'TCP', 'UDP', 'SCTP', # Transport Layer diff --git a/pcapkit/const/__init__.py b/pcapkit/const/__init__.py index 06dae60c17..030d10b19d 100644 --- a/pcapkit/const/__init__.py +++ b/pcapkit/const/__init__.py @@ -20,6 +20,8 @@ from pcapkit.const.ftp import * from pcapkit.const.hip import * from pcapkit.const.http import * +from pcapkit.const.icmp import * +from pcapkit.const.icmpv6 import * from pcapkit.const.igmp import * from pcapkit.const.ipv4 import * from pcapkit.const.ipv6 import * @@ -49,6 +51,11 @@ 'HIP_Suite', 'HIP_Transport', 'HIP_EdDSACurve', # HTTP 'HTTP_ErrorCode', 'HTTP_Frame', 'HTTP_Method', 'HTTP_Setting', 'HTTP_Status', + # ICMP + 'ICMP_Type', 'ICMP_DestinationUnreachableCode', 'ICMP_TimeExceededCode', 'ICMP_ParameterProblemCode', + # ICMPv6 + 'ICMPv6_Type', 'ICMPv6_DestinationUnreachableCode', 'ICMPv6_TimeExceededCode', + 'ICMPv6_ParameterProblemCode', # IGMP 'IGMP_Type', # IPv4 diff --git a/pcapkit/const/icmp/__init__.py b/pcapkit/const/icmp/__init__.py new file mode 100644 index 0000000000..59486f645e --- /dev/null +++ b/pcapkit/const/icmp/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# pylint: disable=unused-import +""":class:`~pcapkit.protocols.internet.icmpv4.ICMPv4` Constant Enumerations +======================================================================== + +.. module:: pcapkit.const.icmp + +This module contains all constant enumerations of +:class:`~pcapkit.protocols.internet.icmpv4.ICMPv4` implementations. Available +enumerations include: + +.. list-table:: + + * - :class:`ICMP_Type ` + - ICMP Type Numbers [*]_ + * - :class:`ICMP_DestinationUnreachableCode ` + - ICMP Destination Unreachable Codes [*]_ + * - :class:`ICMP_TimeExceededCode ` + - ICMP Time Exceeded Codes [*]_ + * - :class:`ICMP_ParameterProblemCode ` + - ICMP Parameter Problem Codes [*]_ + +.. [*] https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-types +.. [*] https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes +.. [*] https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes +.. [*] https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes + +""" + +from pcapkit.const.icmp.destination_unreachable_code import \ + DestinationUnreachableCode as ICMP_DestinationUnreachableCode +from pcapkit.const.icmp.parameter_problem_code import \ + ParameterProblemCode as ICMP_ParameterProblemCode +from pcapkit.const.icmp.time_exceeded_code import TimeExceededCode as ICMP_TimeExceededCode +from pcapkit.const.icmp.type import Type as ICMP_Type + +__all__ = [ + 'ICMP_Type', + 'ICMP_DestinationUnreachableCode', 'ICMP_TimeExceededCode', 'ICMP_ParameterProblemCode', +] diff --git a/pcapkit/const/icmp/destination_unreachable_code.py b/pcapkit/const/icmp/destination_unreachable_code.py new file mode 100644 index 0000000000..dd4078e077 --- /dev/null +++ b/pcapkit/const/icmp/destination_unreachable_code.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""ICMP Destination Unreachable Codes +======================================== + +.. module:: pcapkit.const.icmp.destination_unreachable_code + +This module contains the constant enumeration for **ICMP Destination Unreachable Codes**, +which is automatically generated from :class:`pcapkit.vendor.icmp.destination_unreachable_code.DestinationUnreachableCode`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntEnum + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['DestinationUnreachableCode'] + + +class DestinationUnreachableCode(EnumRegistry, IntEnum): + """[DestinationUnreachableCode] ICMP Destination Unreachable Codes [:rfc:`792`]""" + + #: Net Unreachable [:rfc:`792`] + Net_Unreachable = 0 + + #: Host Unreachable [:rfc:`792`] + Host_Unreachable = 1 + + #: Protocol Unreachable [:rfc:`792`] + Protocol_Unreachable = 2 + + #: Port Unreachable [:rfc:`792`] + Port_Unreachable = 3 + + #: Fragmentation Needed and Don't Fragment was Set [:rfc:`792`] + Fragmentation_Needed_and_Don_t_Fragment_was_Set = 4 + + #: Source Route Failed [:rfc:`792`] + Source_Route_Failed = 5 + + #: Destination Network Unknown [:rfc:`1122`] + Destination_Network_Unknown = 6 + + #: Destination Host Unknown [:rfc:`1122`] + Destination_Host_Unknown = 7 + + #: Source Host Isolated [:rfc:`1122`] + Source_Host_Isolated = 8 + + #: Communication with Destination Network is Administratively + #: Prohibited [:rfc:`1122`] + Communication_with_Destination_Network_is_Administratively_Prohibited = 9 + + #: Communication with Destination Host is Administratively Prohibited + #: [:rfc:`1122`] + Communication_with_Destination_Host_is_Administratively_Prohibited = 10 + + #: Destination Network Unreachable for Type of Service [:rfc:`1122`] + Destination_Network_Unreachable_for_Type_of_Service = 11 + + #: Destination Host Unreachable for Type of Service [:rfc:`1122`] + Destination_Host_Unreachable_for_Type_of_Service = 12 + + #: Communication Administratively Prohibited [:rfc:`1812`] + Communication_Administratively_Prohibited = 13 + + #: Host Precedence Violation [:rfc:`1812`] + Host_Precedence_Violation = 14 + + #: Precedence cutoff in effect [:rfc:`1812`] + Precedence_cutoff_in_effect = 15 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'DestinationUnreachableCode': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + return cls._unregistered_member(value, 'Unassigned') diff --git a/pcapkit/const/icmp/parameter_problem_code.py b/pcapkit/const/icmp/parameter_problem_code.py new file mode 100644 index 0000000000..cb451e223f --- /dev/null +++ b/pcapkit/const/icmp/parameter_problem_code.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""ICMP Parameter Problem Codes +================================== + +.. module:: pcapkit.const.icmp.parameter_problem_code + +This module contains the constant enumeration for **ICMP Parameter Problem Codes**, +which is automatically generated from :class:`pcapkit.vendor.icmp.parameter_problem_code.ParameterProblemCode`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntEnum + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['ParameterProblemCode'] + + +class ParameterProblemCode(EnumRegistry, IntEnum): + """[ParameterProblemCode] ICMP Parameter Problem Codes [:rfc:`792`][:rfc:`1108`]""" + + #: Pointer indicates the error + Pointer_indicates_the_error = 0 + + #: Missing a Required Option [:rfc:`1108`] + Missing_a_Required_Option = 1 + + #: Bad Length + Bad_Length = 2 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'ParameterProblemCode': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + return cls._unregistered_member(value, 'Unassigned') diff --git a/pcapkit/const/icmp/time_exceeded_code.py b/pcapkit/const/icmp/time_exceeded_code.py new file mode 100644 index 0000000000..291727a6db --- /dev/null +++ b/pcapkit/const/icmp/time_exceeded_code.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""ICMP Time Exceeded Codes +============================== + +.. module:: pcapkit.const.icmp.time_exceeded_code + +This module contains the constant enumeration for **ICMP Time Exceeded Codes**, +which is automatically generated from :class:`pcapkit.vendor.icmp.time_exceeded_code.TimeExceededCode`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntEnum + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['TimeExceededCode'] + + +class TimeExceededCode(EnumRegistry, IntEnum): + """[TimeExceededCode] ICMP Time Exceeded Codes [:rfc:`792`]""" + + #: Time to Live exceeded in Transit + Time_to_Live_exceeded_in_Transit = 0 + + #: Fragment Reassembly Time Exceeded + Fragment_Reassembly_Time_Exceeded = 1 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'TimeExceededCode': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + return cls._unregistered_member(value, 'Unassigned') diff --git a/pcapkit/const/icmp/type.py b/pcapkit/const/icmp/type.py new file mode 100644 index 0000000000..1c5cfc4c08 --- /dev/null +++ b/pcapkit/const/icmp/type.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""ICMP Type Numbers +======================= + +.. module:: pcapkit.const.icmp.type + +This module contains the constant enumeration for **ICMP Type Numbers**, +which is automatically generated from :class:`pcapkit.vendor.icmp.type.Type`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntEnum + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['Type'] + + +class Type(EnumRegistry, IntEnum): + """[Type] ICMP Type Numbers [:rfc:`792`]""" + + #: Echo Reply [:rfc:`792`] + Echo_Reply = 0 + + #: Unassigned + Unassigned_1 = 1 + + #: Unassigned + Unassigned_2 = 2 + + #: Destination Unreachable [:rfc:`792`] + Destination_Unreachable = 3 + + #: Source Quench (Deprecated) [:rfc:`792`][:rfc:`6633`] + Source_Quench = 4 + + #: Redirect [:rfc:`792`] + Redirect = 5 + + #: Alternate Host Address (Deprecated) [:rfc:`6918`] + Alternate_Host_Address = 6 + + #: Unassigned + Unassigned_7 = 7 + + #: Echo [:rfc:`792`] + Echo = 8 + + #: Router Advertisement [:rfc:`1256`] + Router_Advertisement = 9 + + #: Router Solicitation [:rfc:`1256`] + Router_Solicitation = 10 + + #: Time Exceeded [:rfc:`792`] + Time_Exceeded = 11 + + #: Parameter Problem [:rfc:`792`] + Parameter_Problem = 12 + + #: Timestamp [:rfc:`792`] + Timestamp = 13 + + #: Timestamp Reply [:rfc:`792`] + Timestamp_Reply = 14 + + #: Information Request (Deprecated) [:rfc:`792`][:rfc:`6918`] + Information_Request = 15 + + #: Information Reply (Deprecated) [:rfc:`792`][:rfc:`6918`] + Information_Reply = 16 + + #: Address Mask Request (Deprecated) [:rfc:`950`][:rfc:`6918`] + Address_Mask_Request = 17 + + #: Address Mask Reply (Deprecated) [:rfc:`950`][:rfc:`6918`] + Address_Mask_Reply = 18 + + #: Reserved (for Security) [Solo] + Reserved_19 = 19 + + #: Traceroute (Deprecated) [:rfc:`1393`][:rfc:`6918`] + Traceroute = 30 + + #: Datagram Conversion Error (Deprecated) [:rfc:`1475`][:rfc:`6918`] + Datagram_Conversion_Error = 31 + + #: Mobile Host Redirect (Deprecated) [David Johnson][:rfc:`6918`] + Mobile_Host_Redirect = 32 + + #: IPv6 Where-Are-You (Deprecated) [Simpson][:rfc:`6918`] + IPv6_Where_Are_You = 33 + + #: IPv6 I-Am-Here (Deprecated) [Simpson][:rfc:`6918`] + IPv6_I_Am_Here = 34 + + #: Mobile Registration Request (Deprecated) [Simpson][:rfc:`6918`] + Mobile_Registration_Request = 35 + + #: Mobile Registration Reply (Deprecated) [Simpson][:rfc:`6918`] + Mobile_Registration_Reply = 36 + + #: Domain Name Request (Deprecated) [:rfc:`1788`][:rfc:`6918`] + Domain_Name_Request = 37 + + #: Domain Name Reply (Deprecated) [:rfc:`1788`][:rfc:`6918`] + Domain_Name_Reply = 38 + + #: SKIP (Deprecated) [Markson][:rfc:`6918`] + SKIP = 39 + + #: Photuris [:rfc:`2521`] + Photuris = 40 + + #: ICMP messages utilized by experimental mobility protocols such as + #: Seamoby [:rfc:`4065`] + ICMP_messages_utilized_by_experimental_mobility_protocols_such_as_Seamoby = 41 + + #: Extended Echo Request [:rfc:`8335`] + Extended_Echo_Request = 42 + + #: Extended Echo Reply [:rfc:`8335`] + Extended_Echo_Reply = 43 + + #: RFC3692-style Experiment 1 [:rfc:`4727`] + RFC3692_style_Experiment_1 = 253 + + #: RFC3692-style Experiment 2 [:rfc:`4727`] + RFC3692_style_Experiment_2 = 254 + + #: Reserved [JBP] + Reserved_255 = 255 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'Type': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + if 20 <= value <= 29: + #: Reserved (for Robustness Experiment) [ZSu] + return cls._unregistered_member(value, 'Reserved') + if 44 <= value <= 252: + #: Unassigned + return cls._unregistered_member(value, 'Unassigned') + return cls._unregistered_member(value, 'Unassigned') diff --git a/pcapkit/const/icmpv6/__init__.py b/pcapkit/const/icmpv6/__init__.py new file mode 100644 index 0000000000..6a244b3299 --- /dev/null +++ b/pcapkit/const/icmpv6/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# pylint: disable=unused-import +""":class:`~pcapkit.protocols.internet.icmpv6.ICMPv6` Constant Enumerations +=============================================================================== + +.. module:: pcapkit.const.icmpv6 + +This module contains all constant enumerations of +:class:`~pcapkit.protocols.internet.icmpv6.ICMPv6` implementations. Available +enumerations include: + +.. list-table:: + + * - :class:`ICMPv6_Type ` + - ICMPv6 Type Numbers [*]_ + * - :class:`ICMPv6_DestinationUnreachableCode ` + - ICMPv6 Destination Unreachable Codes [*]_ + * - :class:`ICMPv6_TimeExceededCode ` + - ICMPv6 Time Exceeded Codes [*]_ + * - :class:`ICMPv6_ParameterProblemCode ` + - ICMPv6 Parameter Problem Codes [*]_ + +.. [*] https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml#icmpv6-parameters-types +.. [*] https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml#icmpv6-parameters-type-1 +.. [*] https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml#icmpv6-parameters-type-3 +.. [*] https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml#icmpv6-parameters-type-4 + +""" + +from pcapkit.const.icmpv6.destination_unreachable_code import \ + DestinationUnreachableCode as ICMPv6_DestinationUnreachableCode +from pcapkit.const.icmpv6.parameter_problem_code import \ + ParameterProblemCode as ICMPv6_ParameterProblemCode +from pcapkit.const.icmpv6.time_exceeded_code import TimeExceededCode as ICMPv6_TimeExceededCode +from pcapkit.const.icmpv6.type import Type as ICMPv6_Type + +__all__ = [ + 'ICMPv6_Type', + 'ICMPv6_DestinationUnreachableCode', 'ICMPv6_TimeExceededCode', 'ICMPv6_ParameterProblemCode', +] diff --git a/pcapkit/const/icmpv6/destination_unreachable_code.py b/pcapkit/const/icmpv6/destination_unreachable_code.py new file mode 100644 index 0000000000..c3ff41b5c0 --- /dev/null +++ b/pcapkit/const/icmpv6/destination_unreachable_code.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""ICMPv6 Destination Unreachable Codes +========================================== + +.. module:: pcapkit.const.icmpv6.destination_unreachable_code + +This module contains the constant enumeration for **ICMPv6 Destination Unreachable Codes**, +which is automatically generated from :class:`pcapkit.vendor.icmpv6.destination_unreachable_code.DestinationUnreachableCode`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntEnum + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['DestinationUnreachableCode'] + + +class DestinationUnreachableCode(EnumRegistry, IntEnum): + """[DestinationUnreachableCode] ICMPv6 Destination Unreachable Codes [:rfc:`4443`]""" + + #: no route to destination + no_route_to_destination = 0 + + #: communication with destination administratively prohibited + communication_with_destination_administratively_prohibited = 1 + + #: beyond scope of source address [:rfc:`4443`] + beyond_scope_of_source_address = 2 + + #: address unreachable + address_unreachable = 3 + + #: port unreachable + port_unreachable = 4 + + #: source address failed ingress/egress policy [:rfc:`4443`] + source_address_failed_ingress_egress_policy = 5 + + #: reject route to destination [:rfc:`4443`] + reject_route_to_destination = 6 + + #: Error in Source Routing Header [:rfc:`6550`][:rfc:`6554`] + Error_in_Source_Routing_Header = 7 + + #: Headers too long [:rfc:`8883`] + Headers_too_long = 8 + + #: Error in P-Route [:rfc:`9914`] + Error_in_P_Route = 9 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'DestinationUnreachableCode': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + return cls._unregistered_member(value, 'Unassigned') diff --git a/pcapkit/const/icmpv6/parameter_problem_code.py b/pcapkit/const/icmpv6/parameter_problem_code.py new file mode 100644 index 0000000000..a3dbda3271 --- /dev/null +++ b/pcapkit/const/icmpv6/parameter_problem_code.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""ICMPv6 Parameter Problem Codes +==================================== + +.. module:: pcapkit.const.icmpv6.parameter_problem_code + +This module contains the constant enumeration for **ICMPv6 Parameter Problem Codes**, +which is automatically generated from :class:`pcapkit.vendor.icmpv6.parameter_problem_code.ParameterProblemCode`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntEnum + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['ParameterProblemCode'] + + +class ParameterProblemCode(EnumRegistry, IntEnum): + """[ParameterProblemCode] ICMPv6 Parameter Problem Codes [:rfc:`4443`][:rfc:`7112`]""" + + #: erroneous header field encountered + erroneous_header_field_encountered = 0 + + #: unrecognized Next Header type encountered + unrecognized_Next_Header_type_encountered = 1 + + #: unrecognized IPv6 option encountered + unrecognized_IPv6_option_encountered = 2 + + #: IPv6 First Fragment has incomplete IPv6 Header Chain [:rfc:`7112`] + IPv6_First_Fragment_has_incomplete_IPv6_Header_Chain = 3 + + #: SR Upper-layer Header Error [:rfc:`8754`] + SR_Upper_layer_Header_Error = 4 + + #: Unrecognized Next Header type encountered by intermediate node [:rfc:`8883`] + Unrecognized_Next_Header_type_encountered_by_intermediate_node = 5 + + #: Extension header too big [:rfc:`8883`] + Extension_header_too_big = 6 + + #: Extension header chain too long [:rfc:`8883`] + Extension_header_chain_too_long = 7 + + #: Too many extension headers [:rfc:`8883`] + Too_many_extension_headers = 8 + + #: Too many options in extension header [:rfc:`8883`] + Too_many_options_in_extension_header = 9 + + #: Option too big [:rfc:`8883`] + Option_too_big = 10 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'ParameterProblemCode': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + return cls._unregistered_member(value, 'Unassigned') diff --git a/pcapkit/const/icmpv6/time_exceeded_code.py b/pcapkit/const/icmpv6/time_exceeded_code.py new file mode 100644 index 0000000000..937ba44067 --- /dev/null +++ b/pcapkit/const/icmpv6/time_exceeded_code.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""ICMPv6 Time Exceeded Codes +================================ + +.. module:: pcapkit.const.icmpv6.time_exceeded_code + +This module contains the constant enumeration for **ICMPv6 Time Exceeded Codes**, +which is automatically generated from :class:`pcapkit.vendor.icmpv6.time_exceeded_code.TimeExceededCode`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntEnum + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['TimeExceededCode'] + + +class TimeExceededCode(EnumRegistry, IntEnum): + """[TimeExceededCode] ICMPv6 Time Exceeded Codes [:rfc:`4443`]""" + + #: hop limit exceeded in transit + hop_limit_exceeded_in_transit = 0 + + #: fragment reassembly time exceeded + fragment_reassembly_time_exceeded = 1 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'TimeExceededCode': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + return cls._unregistered_member(value, 'Unassigned') diff --git a/pcapkit/const/icmpv6/type.py b/pcapkit/const/icmpv6/type.py new file mode 100644 index 0000000000..fab29ebe4f --- /dev/null +++ b/pcapkit/const/icmpv6/type.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""ICMPv6 Type Numbers +========================= + +.. module:: pcapkit.const.icmpv6.type + +This module contains the constant enumeration for **ICMPv6 Type Numbers**, +which is automatically generated from :class:`pcapkit.vendor.icmpv6.type.Type`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntEnum + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['Type'] + + +class Type(EnumRegistry, IntEnum): + """[Type] ICMPv6 Type Numbers [:rfc:`4443`]""" + + #: Reserved + Reserved_0 = 0 + + #: Destination Unreachable [:rfc:`4443`] + Destination_Unreachable = 1 + + #: Packet Too Big [:rfc:`4443`] + Packet_Too_Big = 2 + + #: Time Exceeded [:rfc:`4443`] + Time_Exceeded = 3 + + #: Parameter Problem [:rfc:`4443`] + Parameter_Problem = 4 + + #: Private experimentation [:rfc:`4443`] + Private_experimentation_100 = 100 + + #: Private experimentation [:rfc:`4443`] + Private_experimentation_101 = 101 + + #: Reserved for expansion of ICMPv6 error messages [:rfc:`4443`] + Reserved_for_expansion_of_ICMPv6_error_messages = 127 + + #: Echo Request [:rfc:`4443`] + Echo_Request = 128 + + #: Echo Reply [:rfc:`4443`] + Echo_Reply = 129 + + #: Multicast Listener Query [:rfc:`2710`] + Multicast_Listener_Query = 130 + + #: Multicast Listener Report [:rfc:`2710`] + Multicast_Listener_Report = 131 + + #: Multicast Listener Done [:rfc:`2710`] + Multicast_Listener_Done = 132 + + #: Router Solicitation [:rfc:`4861`] + Router_Solicitation = 133 + + #: Router Advertisement [:rfc:`4861`] + Router_Advertisement = 134 + + #: Neighbor Solicitation [:rfc:`4861`] + Neighbor_Solicitation = 135 + + #: Neighbor Advertisement [:rfc:`4861`] + Neighbor_Advertisement = 136 + + #: Redirect Message [:rfc:`4861`] + Redirect_Message = 137 + + #: Router Renumbering [:rfc:`2894`] + Router_Renumbering = 138 + + #: ICMP Node Information Query [:rfc:`4620`] + ICMP_Node_Information_Query = 139 + + #: ICMP Node Information Response [:rfc:`4620`] + ICMP_Node_Information_Response = 140 + + #: Inverse Neighbor Discovery Solicitation Message [:rfc:`3122`] + Inverse_Neighbor_Discovery_Solicitation_Message = 141 + + #: Inverse Neighbor Discovery Advertisement Message [:rfc:`3122`] + Inverse_Neighbor_Discovery_Advertisement_Message = 142 + + #: Version 2 Multicast Listener Report [:rfc:`9777`] + Version_2_Multicast_Listener_Report = 143 + + #: Home Agent Address Discovery Request Message [:rfc:`6275`] + Home_Agent_Address_Discovery_Request_Message = 144 + + #: Home Agent Address Discovery Reply Message [:rfc:`6275`] + Home_Agent_Address_Discovery_Reply_Message = 145 + + #: Mobile Prefix Solicitation [:rfc:`6275`] + Mobile_Prefix_Solicitation = 146 + + #: Mobile Prefix Advertisement [:rfc:`6275`] + Mobile_Prefix_Advertisement = 147 + + #: Certification Path Solicitation Message [:rfc:`3971`] + Certification_Path_Solicitation_Message = 148 + + #: Certification Path Advertisement Message [:rfc:`3971`] + Certification_Path_Advertisement_Message = 149 + + #: ICMP messages utilized by experimental mobility protocols such as + #: Seamoby [:rfc:`4065`] + ICMP_messages_utilized_by_experimental_mobility_protocols_such_as_Seamoby = 150 + + #: Multicast Router Advertisement [:rfc:`4286`] + Multicast_Router_Advertisement = 151 + + #: Multicast Router Solicitation [:rfc:`4286`] + Multicast_Router_Solicitation = 152 + + #: Multicast Router Termination [:rfc:`4286`] + Multicast_Router_Termination = 153 + + #: FMIPv6 Messages [:rfc:`5568`] + FMIPv6_Messages = 154 + + #: RPL Control Message [:rfc:`6550`] + RPL_Control_Message = 155 + + #: ILNPv6 Locator Update Message [:rfc:`6743`] + ILNPv6_Locator_Update_Message = 156 + + #: Duplicate Address Request [:rfc:`6775`] + Duplicate_Address_Request = 157 + + #: Duplicate Address Confirmation [:rfc:`6775`] + Duplicate_Address_Confirmation = 158 + + #: MPL Control Message [:rfc:`7731`] + MPL_Control_Message = 159 + + #: Extended Echo Request [:rfc:`8335`] + Extended_Echo_Request = 160 + + #: Extended Echo Reply [:rfc:`8335`] + Extended_Echo_Reply = 161 + + #: Private experimentation [:rfc:`4443`] + Private_experimentation_200 = 200 + + #: Private experimentation [:rfc:`4443`] + Private_experimentation_201 = 201 + + #: Reserved for expansion of ICMPv6 informational messages [:rfc:`4443`] + Reserved_for_expansion_of_ICMPv6_informational_messages = 255 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'Type': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + if 5 <= value <= 99: + #: Unassigned + return cls._unregistered_member(value, 'Unassigned') + if 102 <= value <= 126: + #: Unassigned + return cls._unregistered_member(value, 'Unassigned') + if 162 <= value <= 199: + #: Unassigned + return cls._unregistered_member(value, 'Unassigned') + if 202 <= value <= 254: + #: Unassigned + return cls._unregistered_member(value, 'Unassigned') + return cls._unregistered_member(value, 'Unassigned') diff --git a/pcapkit/foundation/__init__.py b/pcapkit/foundation/__init__.py index 1a8789f9ed..aee568ac99 100644 --- a/pcapkit/foundation/__init__.py +++ b/pcapkit/foundation/__init__.py @@ -32,6 +32,7 @@ 'register_pcap', 'register_pcapng', 'register_ethertype', 'register_transtype', + 'register_icmp_type', 'register_ipv4_option', 'register_hip_parameter', 'register_hopopt_option', 'register_ipv6_opts_option', 'register_ipv6_route_routing', 'register_mh_message', 'register_mh_option', 'register_mh_extension', diff --git a/pcapkit/foundation/registry/__init__.py b/pcapkit/foundation/registry/__init__.py index 55157a3ee6..2c125930eb 100644 --- a/pcapkit/foundation/registry/__init__.py +++ b/pcapkit/foundation/registry/__init__.py @@ -33,6 +33,7 @@ 'register_ethertype', 'register_transtype', + 'register_icmp_type', 'register_ipv4_option', 'register_hip_parameter', 'register_hopopt_option', 'register_ipv6_opts_option', 'register_ipv6_route_routing', 'register_mh_message', 'register_mh_option', 'register_mh_extension', diff --git a/pcapkit/foundation/registry/protocols.py b/pcapkit/foundation/registry/protocols.py index 459e2d03e5..3541cac5f6 100644 --- a/pcapkit/foundation/registry/protocols.py +++ b/pcapkit/foundation/registry/protocols.py @@ -22,6 +22,8 @@ from pcapkit.const.hip.parameter import Parameter as HIP_Parameter from pcapkit.const.http.frame import Frame as HTTP_Frame +from pcapkit.const.icmp.type import Type as ICMPv4_Type +from pcapkit.const.icmpv6.type import Type as ICMPv6_Type from pcapkit.const.ipv4.option_number import OptionNumber as IPv4_OptionNumber from pcapkit.const.ipv6.option import Option as IPv6_Option from pcapkit.const.ipv6.routing import Routing as IPv6_Routing @@ -47,6 +49,8 @@ from pcapkit.protocols.application.httpv2 import HTTP as HTTPv2 from pcapkit.protocols.internet.hip import HIP from pcapkit.protocols.internet.hopopt import HOPOPT +from pcapkit.protocols.internet.icmpv4 import ICMPv4 +from pcapkit.protocols.internet.icmpv6 import ICMPv6 from pcapkit.protocols.internet.internet import Internet from pcapkit.protocols.internet.ipv4 import IPv4 from pcapkit.protocols.internet.ipv6_opts import IPv6_Opts @@ -81,7 +85,7 @@ from pcapkit.utilities.warnings import RegistryWarning, warn if TYPE_CHECKING: - from typing import Any, Iterator, Optional, Type + from typing import Any, Callable, Iterator, Optional, Type from pcapkit.const.reg.ethertype import EtherType from pcapkit.const.reg.linktype import LinkType @@ -131,6 +135,7 @@ 'register_ethertype', 'register_transtype', + 'register_icmp_type', 'register_ipv4_option', 'register_hip_parameter', 'register_hopopt_option', 'register_ipv6_opts_option', 'register_ipv6_route_routing', 'register_mh_message', 'register_mh_option', 'register_mh_extension', @@ -888,6 +893,40 @@ def register_ipv4_option(code: 'IPv4_OptionNumber', meth: 'str | tuple[IPv4_Opti logger.debug('registered IPv4 option parser: %s', code.name) +# NOTE: pcapkit.protocols.internet.icmpv4.ICMPv4.__type__, pcapkit.protocols.internet.icmpv6.ICMPv6.__type__ +def register_icmp_type(code: 'ICMPv4_Type | ICMPv6_Type', + meth: 'str | tuple[Callable[..., Any], Callable[..., Any]]') -> 'None': + """Register a message body parser. + + The function will register the given message body parser to the + :data:`~pcapkit.protocols.internet.icmpv4.ICMPv4.__type__` registry when + ``code`` is an ICMPv4 type, or to + :data:`~pcapkit.protocols.internet.icmpv6.ICMPv6.__type__` when it is an + ICMPv6 type. Each registry is written by that class's own + :meth:`register_type` classmethod. + + Args: + code: ICMP type as in :class:`~pcapkit.const.icmp.type.Type` or + :class:`~pcapkit.const.icmpv6.type.Type`. + meth: Method name, or a ``(parser, constructor)`` pair of callables, to + parse and construct the message body. A name names the + ``_read_data_type_*`` and ``_make_data_type_*`` methods of the + protocol, and has to name an existing ``_read_data_type_*`` one. + + Raises: + RegistryError: If ``code`` is not a member of either ICMP type enum, + or ``meth`` is a name with no matching ``_read_data_type_*`` method. + + """ + kind, protocol = (ICMPv6_Type, ICMPv6) if isinstance(code, ICMPv6_Type) else (ICMPv4_Type, ICMPv4) + _check_code(code, kind, protocol) + if isinstance(meth, str) and not hasattr(protocol, f'_read_data_type_{meth}'): + raise RegistryError('method must be a valid ICMP message parser function') + + protocol.register_type(code, meth) + logger.debug('registered ICMP type parser: %s', code.name) + + # NOTE: pcapkit.protocols.internet.hip.HIP.__parameter__ def register_hip_parameter(code: 'HIP_Parameter', meth: 'str | tuple[HIP_ParameterParser, HIP_ParameterConstructor]', *, schema: 'Optional[Type[Schema_HIP_Parameter]]' = None) -> 'None': diff --git a/pcapkit/protocols/__init__.py b/pcapkit/protocols/__init__.py index 5aab10622d..3a508809df 100644 --- a/pcapkit/protocols/__init__.py +++ b/pcapkit/protocols/__init__.py @@ -54,7 +54,8 @@ 'LinuxSLL2', 'Loopback', 'S_Tag', 'VLAN', # Internet Layer - 'AH', 'ESP', 'IGMP', 'IGMPv1', 'IGMPv2', 'IP', 'IPsec', 'IPv4', 'IPv6', 'IPX', + 'AH', 'ESP', 'ICMP', 'ICMPv4', 'ICMPv6', 'IGMP', 'IGMPv1', 'IGMPv2', + 'IP', 'IPsec', 'IPv4', 'IPv6', 'IPX', # IPv6 Extension Header 'HIP', 'HOPOPT', 'IPv6_Frag', 'IPv6_Ext', 'IPv6_Opts', diff --git a/pcapkit/protocols/data/__init__.py b/pcapkit/protocols/data/__init__.py index 4fee25a195..c8c62a4b30 100644 --- a/pcapkit/protocols/data/__init__.py +++ b/pcapkit/protocols/data/__init__.py @@ -112,6 +112,16 @@ 'HOPOPT_LineIdentificationOption', 'HOPOPT_JumboPayloadOption', 'HOPOPT_HomeAddressOption', 'HOPOPT_IPDFFOption', + # Internet Control Message Protocol + 'ICMPv4', + 'ICMPv4_Unknown', 'ICMPv4_Echo', 'ICMPv4_DestinationUnreachable', 'ICMPv4_TimeExceeded', + 'ICMPv4_ParameterProblem', + + # Internet Control Message Protocol for IPv6 + 'ICMPv6', + 'ICMPv6_Unknown', 'ICMPv6_DestinationUnreachable', 'ICMPv6_PacketTooBig', 'ICMPv6_TimeExceeded', + 'ICMPv6_ParameterProblem', 'ICMPv6_Echo', + # Internet Protocol version 4 'IPv4', 'IPv4_ToSField', 'IPv4_Flags', diff --git a/pcapkit/protocols/data/internet/__init__.py b/pcapkit/protocols/data/internet/__init__.py index bd7568364d..447b283c7d 100644 --- a/pcapkit/protocols/data/internet/__init__.py +++ b/pcapkit/protocols/data/internet/__init__.py @@ -104,6 +104,25 @@ TunnelEncapsulationLimitOption as HOPOPT_TunnelEncapsulationLimitOption from pcapkit.protocols.data.internet.hopopt import UnassignedOption as HOPOPT_UnassignedOption +# Internet Control Message Protocol +from pcapkit.protocols.data.internet.icmpv4 import ICMPv4 +from pcapkit.protocols.data.internet.icmpv4 import \ + DestinationUnreachable as ICMPv4_DestinationUnreachable +from pcapkit.protocols.data.internet.icmpv4 import Echo as ICMPv4_Echo +from pcapkit.protocols.data.internet.icmpv4 import ParameterProblem as ICMPv4_ParameterProblem +from pcapkit.protocols.data.internet.icmpv4 import TimeExceeded as ICMPv4_TimeExceeded +from pcapkit.protocols.data.internet.icmpv4 import Unknown as ICMPv4_Unknown + +# Internet Control Message Protocol for IPv6 +from pcapkit.protocols.data.internet.icmpv6 import ICMPv6 +from pcapkit.protocols.data.internet.icmpv6 import \ + DestinationUnreachable as ICMPv6_DestinationUnreachable +from pcapkit.protocols.data.internet.icmpv6 import Echo as ICMPv6_Echo +from pcapkit.protocols.data.internet.icmpv6 import PacketTooBig as ICMPv6_PacketTooBig +from pcapkit.protocols.data.internet.icmpv6 import ParameterProblem as ICMPv6_ParameterProblem +from pcapkit.protocols.data.internet.icmpv6 import TimeExceeded as ICMPv6_TimeExceeded +from pcapkit.protocols.data.internet.icmpv6 import Unknown as ICMPv6_Unknown + # Internet Group Management Protocol from pcapkit.protocols.data.internet.igmp import IGMP @@ -416,6 +435,16 @@ 'HOPOPT_LineIdentificationOption', 'HOPOPT_JumboPayloadOption', 'HOPOPT_HomeAddressOption', 'HOPOPT_IPDFFOption', + # Internet Control Message Protocol + 'ICMPv4', + 'ICMPv4_Unknown', 'ICMPv4_Echo', 'ICMPv4_DestinationUnreachable', 'ICMPv4_TimeExceeded', + 'ICMPv4_ParameterProblem', + + # Internet Control Message Protocol for IPv6 + 'ICMPv6', + 'ICMPv6_Unknown', 'ICMPv6_DestinationUnreachable', 'ICMPv6_PacketTooBig', 'ICMPv6_TimeExceeded', + 'ICMPv6_ParameterProblem', 'ICMPv6_Echo', + # Internet Group Management Protocol 'IGMP', diff --git a/pcapkit/protocols/data/internet/icmpv4.py b/pcapkit/protocols/data/internet/icmpv4.py new file mode 100644 index 0000000000..8ce62c2a0e --- /dev/null +++ b/pcapkit/protocols/data/internet/icmpv4.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +"""data model for ICMP protocol""" + +from typing import TYPE_CHECKING + +from pcapkit.corekit.infoclass import info_final +from pcapkit.protocols.data.protocol import Protocol + +if TYPE_CHECKING: + from pcapkit.const.icmp.type import Type as Enum_Type + +__all__ = [ + 'ICMPv4', + + 'Unknown', 'Echo', 'DestinationUnreachable', 'TimeExceeded', 'ParameterProblem', +] + + +class ICMPv4(Protocol): + """Data model for ICMP protocol. + + The fields below are the only ones every ICMP message shares + (:rfc:`792`); what follows them is read entirely differently depending on + :attr:`type`, which is why each message kind below adds its own fields + rather than this base class growing a union of them all. + + """ + + #: ICMP type. + type: 'Enum_Type' + #: ICMP code. Left untyped as a bare :obj:`int` here because its meaning + #: is entirely :attr:`type`-dependent; each subclass below documents + #: which registry (if any) its own ``code`` values are drawn from. + code: 'int' + #: Checksum, kept as on the wire; it is neither verified nor computed. + checksum: 'bytes' + #: Message length, in octets, header included. ICMP messages carry no + #: length field of their own -- this is simply how many octets + #: :meth:`ICMPv4.read ` was + #: handed, kept here so the whole message can be told apart from a + #: truncated capture. + length: 'int' + + +@info_final +class Unknown(ICMPv4): + """Data model for ICMP packet of unknown type. + + ``code`` carries no :mod:`pcapkit.const` registry for a type this + package does not recognise, so it is read as a bare :obj:`int`. + + """ + + #: Message body, as captured. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Type', code: 'int', checksum: 'bytes', length: 'int', data: 'bytes') -> 'None': ... # pylint: disable=unused-argument,multiple-statements,super-init-not-called,redefined-builtin,line-too-long + + +@info_final +class Echo(ICMPv4): + """Data model for ICMP Echo Request/Reply message (types 8 and 0). + + ``code`` is always ``0`` for both types (:rfc:`792`), so it is read as a + bare :obj:`int` rather than typed against a registry of one member. + + """ + + #: Identifier. + id: 'int' + #: Sequence number. + seq: 'int' + #: Data, echoed back verbatim by a reply to the same identifier and + #: sequence number. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Type', code: 'int', checksum: 'bytes', length: 'int', # pylint: disable=unused-argument,multiple-statements,super-init-not-called,redefined-builtin,line-too-long + id: 'int', seq: 'int', data: 'bytes') -> 'None': ... + + +@info_final +class DestinationUnreachable(ICMPv4): + """Data model for ICMP Destination Unreachable message (type 3). + + .. seealso:: + + :class:`pcapkit.const.icmp.destination_unreachable_code.DestinationUnreachableCode` + + """ + + #: Unused (four octets, :rfc:`792#page-4`), as captured. + unused: 'bytes' + #: Internet header plus the first eight octets of the original + #: datagram's payload (:rfc:`792#page-4`), as captured. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Type', code: 'int', checksum: 'bytes', length: 'int', # pylint: disable=unused-argument,multiple-statements,super-init-not-called,redefined-builtin,line-too-long + unused: 'bytes', data: 'bytes') -> 'None': ... + + +@info_final +class TimeExceeded(ICMPv4): + """Data model for ICMP Time Exceeded message (type 11). + + .. seealso:: + + :class:`pcapkit.const.icmp.time_exceeded_code.TimeExceededCode` + + """ + + #: Unused (four octets, :rfc:`792#page-5`), as captured. + unused: 'bytes' + #: Internet header plus the first eight octets of the original + #: datagram's payload (:rfc:`792#page-5`), as captured. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Type', code: 'int', checksum: 'bytes', length: 'int', # pylint: disable=unused-argument,multiple-statements,super-init-not-called,redefined-builtin,line-too-long + unused: 'bytes', data: 'bytes') -> 'None': ... + + +@info_final +class ParameterProblem(ICMPv4): + """Data model for ICMP Parameter Problem message (type 12). + + .. seealso:: + + :class:`pcapkit.const.icmp.parameter_problem_code.ParameterProblemCode` + + """ + + #: Pointer, identifying the octet of the original datagram that caused + #: the error (:rfc:`792#page-5`); meaningless for codes other than + #: ``0``, but read and written regardless, as the wire layout requires. + pointer: 'int' + #: Unused (three octets, :rfc:`792#page-5`), as captured. + unused: 'bytes' + #: Internet header plus the first eight octets of the original + #: datagram's payload (:rfc:`792#page-5`), as captured. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Type', code: 'int', checksum: 'bytes', length: 'int', # pylint: disable=unused-argument,multiple-statements,super-init-not-called,redefined-builtin,line-too-long + pointer: 'int', unused: 'bytes', data: 'bytes') -> 'None': ... diff --git a/pcapkit/protocols/data/internet/icmpv6.py b/pcapkit/protocols/data/internet/icmpv6.py new file mode 100644 index 0000000000..b3e48c49f2 --- /dev/null +++ b/pcapkit/protocols/data/internet/icmpv6.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +"""data model for ICMPv6 protocol""" + +from typing import TYPE_CHECKING + +from pcapkit.corekit.infoclass import info_final +from pcapkit.protocols.data.protocol import Protocol + +if TYPE_CHECKING: + from pcapkit.const.icmpv6.type import Type as Enum_Type + +__all__ = [ + 'ICMPv6', + + 'Unknown', 'DestinationUnreachable', 'PacketTooBig', 'TimeExceeded', + 'ParameterProblem', 'Echo', +] + + +class ICMPv6(Protocol): + """Data model for ICMPv6 protocol. + + The fields below are the only ones every ICMPv6 message shares + (:rfc:`4443`); what follows them is read entirely differently depending + on :attr:`type`, which is why each message kind below adds its own + fields rather than this base class growing a union of them all. + + """ + + #: ICMPv6 type. + type: 'Enum_Type' + #: ICMPv6 code. Left untyped as a bare :obj:`int` here because its + #: meaning is entirely :attr:`type`-dependent; each subclass below + #: documents which registry (if any) its own ``code`` values are drawn + #: from. + code: 'int' + #: Checksum, kept as on the wire; it is neither verified nor computed. + checksum: 'bytes' + #: Message length, in octets, header included. ICMPv6 messages carry no + #: length field of their own -- this is simply how many octets + #: :meth:`ICMPv6.read ` + #: was handed, kept here so the whole message can be told apart from a + #: truncated capture. + length: 'int' + + +@info_final +class Unknown(ICMPv6): + """Data model for ICMPv6 packet of unknown type. + + ``code`` carries no :mod:`pcapkit.const` registry for a type this + package does not recognise, so it is read as a bare :obj:`int`. + + """ + + #: Message body, as captured. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Type', code: 'int', checksum: 'bytes', length: 'int', data: 'bytes') -> 'None': ... # pylint: disable=unused-argument,multiple-statements,super-init-not-called,redefined-builtin,line-too-long + + +@info_final +class DestinationUnreachable(ICMPv6): + """Data model for ICMPv6 Destination Unreachable message (type 1). + + .. seealso:: + + :class:`pcapkit.const.icmpv6.destination_unreachable_code.DestinationUnreachableCode` + + """ + + #: Unused (four octets, :rfc:`4443#section-3.1`), as captured. + unused: 'bytes' + #: As much of the invoking packet as fits the minimum IPv6 MTU, per + #: :rfc:`4443#section-3.1`, as captured. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Type', code: 'int', checksum: 'bytes', length: 'int', # pylint: disable=unused-argument,multiple-statements,super-init-not-called,redefined-builtin,line-too-long + unused: 'bytes', data: 'bytes') -> 'None': ... + + +@info_final +class PacketTooBig(ICMPv6): + """Data model for ICMPv6 Packet Too Big message (type 2). + + ``code`` is always ``0`` (:rfc:`4443#section-3.2`), so it is read as a + bare :obj:`int` rather than typed against a registry of one member. + + """ + + #: MTU of the link that could not forward the invoking packet. + mtu: 'int' + #: As much of the invoking packet as fits the minimum IPv6 MTU, per + #: :rfc:`4443#section-3.2`, as captured. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Type', code: 'int', checksum: 'bytes', length: 'int', # pylint: disable=unused-argument,multiple-statements,super-init-not-called,redefined-builtin,line-too-long + mtu: 'int', data: 'bytes') -> 'None': ... + + +@info_final +class TimeExceeded(ICMPv6): + """Data model for ICMPv6 Time Exceeded message (type 3). + + .. seealso:: + + :class:`pcapkit.const.icmpv6.time_exceeded_code.TimeExceededCode` + + """ + + #: Unused (four octets, :rfc:`4443#section-3.3`), as captured. + unused: 'bytes' + #: As much of the invoking packet as fits the minimum IPv6 MTU, per + #: :rfc:`4443#section-3.3`, as captured. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Type', code: 'int', checksum: 'bytes', length: 'int', # pylint: disable=unused-argument,multiple-statements,super-init-not-called,redefined-builtin,line-too-long + unused: 'bytes', data: 'bytes') -> 'None': ... + + +@info_final +class ParameterProblem(ICMPv6): + """Data model for ICMPv6 Parameter Problem message (type 4). + + .. seealso:: + + :class:`pcapkit.const.icmpv6.parameter_problem_code.ParameterProblemCode` + + """ + + #: Pointer, identifying the octet offset within the invoking packet + #: where the error was detected (:rfc:`4443#section-3.4`). + pointer: 'int' + #: As much of the invoking packet as fits the minimum IPv6 MTU, per + #: :rfc:`4443#section-3.4`, as captured. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Type', code: 'int', checksum: 'bytes', length: 'int', # pylint: disable=unused-argument,multiple-statements,super-init-not-called,redefined-builtin,line-too-long + pointer: 'int', data: 'bytes') -> 'None': ... + + +@info_final +class Echo(ICMPv6): + """Data model for ICMPv6 Echo Request/Reply message (types 128 and 129). + + ``code`` is always ``0`` for both types (:rfc:`4443#section-4.1`, + :rfc:`4443#section-4.2`), so it is read as a bare :obj:`int` rather than + typed against a registry of one member. + + """ + + #: Identifier. + id: 'int' + #: Sequence number. + seq: 'int' + #: Data, echoed back verbatim by a reply to the same identifier and + #: sequence number. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Type', code: 'int', checksum: 'bytes', length: 'int', # pylint: disable=unused-argument,multiple-statements,super-init-not-called,redefined-builtin,line-too-long + id: 'int', seq: 'int', data: 'bytes') -> 'None': ... diff --git a/pcapkit/protocols/internet/NotImplemented/icmp.py b/pcapkit/protocols/internet/NotImplemented/icmp.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/pcapkit/protocols/internet/NotImplemented/icmpv6.py b/pcapkit/protocols/internet/NotImplemented/icmpv6.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/pcapkit/protocols/internet/__init__.py b/pcapkit/protocols/internet/__init__.py index c09576918f..95b598cdf9 100644 --- a/pcapkit/protocols/internet/__init__.py +++ b/pcapkit/protocols/internet/__init__.py @@ -9,7 +9,7 @@ in the internet layer, with detailed implementation and methods. """ -# TODO: Implement ECN, ICMP, ICMPv6, Shim6. +# TODO: Implement ECN, Shim6. # Base Class for Internet Layer from pcapkit.protocols.internet.internet import Internet @@ -17,6 +17,8 @@ # Utility Classes for Protocols from pcapkit.protocols.internet.ah import AH from pcapkit.protocols.internet.esp import ESP +from pcapkit.protocols.internet.icmpv4 import ICMPv4 +from pcapkit.protocols.internet.icmpv6 import ICMPv6 from pcapkit.protocols.internet.igmpv1 import IGMP as IGMPv1 from pcapkit.protocols.internet.igmpv2 import IGMP as IGMPv2 from pcapkit.protocols.internet.ipv4 import IPv4 @@ -36,13 +38,15 @@ from pcapkit.const.reg.ethertype import EtherType as ETHERTYPE # Base Classes +from pcapkit.protocols.internet.icmp import ICMP from pcapkit.protocols.internet.igmp import IGMP from pcapkit.protocols.internet.ip import IP from pcapkit.protocols.internet.ipsec import IPsec __all__ = [ 'ETHERTYPE', # Protocol Numbers - 'AH', 'ESP', 'IGMP', 'IGMPv1', 'IGMPv2', # Internet Layer + 'AH', 'ESP', 'ICMP', 'ICMPv4', 'ICMPv6', # Internet Layer + 'IGMP', 'IGMPv1', 'IGMPv2', 'IP', 'IPsec', 'IPv4', 'IPv6', 'IPX', 'HIP', 'HOPOPT', 'IPv6_Frag', 'IPv6_Ext', 'IPv6_Opts', 'IPv6_Route', 'MH', # IPv6 Extension Header diff --git a/pcapkit/protocols/internet/icmp.py b/pcapkit/protocols/internet/icmp.py new file mode 100644 index 0000000000..02baf39666 --- /dev/null +++ b/pcapkit/protocols/internet/icmp.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +"""ICMP - Internet Control Message Protocol +=============================================== + +.. module:: pcapkit.protocols.internet.icmp + +:mod:`pcapkit.protocols.internet.icmp` contains +:class:`~pcapkit.protocols.internet.icmp.ICMP` only, +which is a base class for Internet Control Message +Protocol (ICMP) protocol family [*]_, e.g. +:class:`~pcapkit.protocols.internet.icmpv4.ICMPv4` and +:class:`~pcapkit.protocols.internet.icmpv6.ICMPv6`. + +.. [*] https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol + +""" +from typing import TYPE_CHECKING, Generic + +from pcapkit.protocols.internet.internet import Internet +from pcapkit.protocols.protocol import _PT, _ST + +if TYPE_CHECKING: + from typing_extensions import Literal + +__all__ = ['ICMP'] + + +class ICMP(Internet[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=abstract-method + """Abstract base class for ICMP protocol family. + + - Internet Control Message Protocol (:class:`~pcapkit.protocols.internet.icmpv4.ICMPv4`) [:rfc:`792`] + - Internet Control Message Protocol for IPv6 (:class:`~pcapkit.protocols.internet.icmpv6.ICMPv6`) [:rfc:`4443`] + + """ + + ########################################################################## + # Methods. + ########################################################################## + + @classmethod + def id(cls) -> 'tuple[Literal["ICMP"], Literal["ICMPv6"]]': + """Index ID of the protocol. + + Returns: + Index ID of the protocol. + + """ + return ('ICMP', 'ICMPv6') diff --git a/pcapkit/protocols/internet/icmpv4.py b/pcapkit/protocols/internet/icmpv4.py new file mode 100644 index 0000000000..3889f99440 --- /dev/null +++ b/pcapkit/protocols/internet/icmpv4.py @@ -0,0 +1,620 @@ +# -*- coding: utf-8 -*- +"""ICMPv4 - Internet Control Message Protocol +================================================= + +.. module:: pcapkit.protocols.internet.icmpv4 + +:mod:`pcapkit.protocols.internet.icmpv4` contains +:class:`~pcapkit.protocols.internet.icmpv4.ICMPv4` only, +which implements extractor for Internet Control +Message Protocol (ICMP) [*]_, whose structure is +described as below: + +======= ========= ======================= =================================== +Octets Bits Name Description +======= ========= ======================= =================================== + 0 0 ``icmpv4.type`` Type + 1 8 ``icmpv4.code`` Code + 2 16 ``icmpv4.checksum`` Checksum + 4 32 ``icmpv4.data`` Message Body +======= ========= ======================= =================================== + +.. [*] https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol + +""" +import collections +from typing import TYPE_CHECKING, cast + +from pcapkit.const.icmp.type import Type as Enum_Type +from pcapkit.const.reg.transtype import TransType as Enum_TransType +from pcapkit.corekit.protochain import ProtoChain +from pcapkit.protocols.data.internet.icmpv4 import \ + DestinationUnreachable as Data_DestinationUnreachable +from pcapkit.protocols.data.internet.icmpv4 import Echo as Data_Echo +from pcapkit.protocols.data.internet.icmpv4 import ICMPv4 as Data_ICMPv4 +from pcapkit.protocols.data.internet.icmpv4 import ParameterProblem as Data_ParameterProblem +from pcapkit.protocols.data.internet.icmpv4 import TimeExceeded as Data_TimeExceeded +from pcapkit.protocols.data.internet.icmpv4 import Unknown as Data_Unknown +from pcapkit.protocols.internet.icmp import ICMP +from pcapkit.protocols.misc.null import NoPayload +from pcapkit.protocols.schema.internet.icmpv4 import \ + DestinationUnreachable as Schema_DestinationUnreachable +from pcapkit.protocols.schema.internet.icmpv4 import Echo as Schema_Echo +from pcapkit.protocols.schema.internet.icmpv4 import ICMPv4 as Schema_ICMPv4 +from pcapkit.protocols.schema.internet.icmpv4 import ICMPv4Type as Schema_ICMPv4Type +from pcapkit.protocols.schema.internet.icmpv4 import ParameterProblem as Schema_ParameterProblem +from pcapkit.protocols.schema.internet.icmpv4 import TimeExceeded as Schema_TimeExceeded +from pcapkit.protocols.schema.internet.icmpv4 import Unknown as Schema_Unknown +from pcapkit.utilities.exceptions import ProtocolError +from pcapkit.utilities.warnings import RegistryWarning, warn + +if TYPE_CHECKING: + from enum import IntEnum as StdlibEnum + from typing import Any, Callable, DefaultDict, NoReturn, Optional, Type + + from aenum import IntEnum as AenumEnum + from mypy_extensions import DefaultArg, KwArg, NamedArg + from typing_extensions import Literal + + TypeParser = Callable[[Schema_ICMPv4Type, NamedArg(Schema_ICMPv4, 'header'), NamedArg(int, 'length')], + Data_ICMPv4] + TypeConstructor = Callable[[Enum_Type, DefaultArg(Optional[Data_ICMPv4]), KwArg(Any)], Schema_ICMPv4Type] + +__all__ = ['ICMPv4'] + + +class ICMPv4(ICMP[Data_ICMPv4, Schema_ICMPv4], + schema=Schema_ICMPv4, data=Data_ICMPv4): + """This class implements Internet Control Message Protocol. + + This class parses the following ICMP message + types, which are registered in the + :attr:`self.__type__ ` + attribute: + + .. list-table:: + :header-rows: 1 + + * - Type + - Data Parser + - Data Constructor + * - Echo Reply / Echo Request + - :meth:`~pcapkit.protocols.internet.icmpv4.ICMPv4._read_data_type_echo` + - :meth:`~pcapkit.protocols.internet.icmpv4.ICMPv4._make_data_type_echo` + * - Destination Unreachable + - :meth:`~pcapkit.protocols.internet.icmpv4.ICMPv4._read_data_type_destination_unreachable` + - :meth:`~pcapkit.protocols.internet.icmpv4.ICMPv4._make_data_type_destination_unreachable` + * - Time Exceeded + - :meth:`~pcapkit.protocols.internet.icmpv4.ICMPv4._read_data_type_time_exceeded` + - :meth:`~pcapkit.protocols.internet.icmpv4.ICMPv4._make_data_type_time_exceeded` + * - Parameter Problem + - :meth:`~pcapkit.protocols.internet.icmpv4.ICMPv4._read_data_type_parameter_problem` + - :meth:`~pcapkit.protocols.internet.icmpv4.ICMPv4._make_data_type_parameter_problem` + + Any other type is kept as its raw message body, see + :meth:`_read_data_type_none` and :meth:`_make_data_type_none`. + + """ + + ########################################################################## + # Defaults. + ########################################################################## + + #: Type code to method mapping. A :obj:`str` value names the methods + #: ``_read_data_type_${name}`` and ``_make_data_type_${name}``, falling + #: back to ``_read_data_type_none`` / ``_make_data_type_none`` when they + #: do not exist; any other value is a ``(parser, constructor)`` pair of + #: callables. + __type__ = collections.defaultdict( + lambda: 'none', + { + Enum_Type.Echo_Reply: 'echo', + Enum_Type.Echo: 'echo', + Enum_Type.Destination_Unreachable: 'destination_unreachable', + Enum_Type.Time_Exceeded: 'time_exceeded', + Enum_Type.Parameter_Problem: 'parameter_problem', + }, + ) # type: DefaultDict[Enum_Type | int, str | tuple[TypeParser, TypeConstructor]] + + ########################################################################## + # Properties. + ########################################################################## + + @property + def name(self) -> 'Literal["Internet Control Message Protocol"]': + """Name of current protocol.""" + return 'Internet Control Message Protocol' + + @property + def alias(self) -> 'Literal["ICMP"]': + """Acronym of corresponding protocol.""" + return 'ICMP' + + @property + def length(self) -> 'int': + """Header length of current protocol.""" + return self._info.length + + ########################################################################## + # Methods. + ########################################################################## + + @classmethod + def id(cls) -> 'tuple[Literal["ICMP"]]': # type: ignore[override] + """Index ID of the protocol. + + :rfc:`792` names the protocol ``ICMP``, without a version. + + String ``==`` compares against ``(cls, *id())``, so ``== 'ICMPv4'`` is + :data:`False`, while ``'ICMPv4' in`` matches by class. + + Returns: + Index ID of the protocol. + + """ + return ('ICMP',) + + def read(self, length: 'Optional[int]' = None, *, + __packet__: 'Optional[dict[str, Any]]' = None, **kwargs: 'Any') -> 'Data_ICMPv4': # pylint: disable=unused-argument + """Read Internet Control Message Protocol (ICMP). + + Structure of ICMP header [:rfc:`792`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + . Message Body . + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + length: Length of packet data. + __packet__: Optional packet data, unused. + **kwargs: Arbitrary keyword arguments. + + Returns: + Parsed packet data. + + """ + if length is None: + length = len(self) + schema = self.__header__ + + name = self._lookup_registry(self.__type__, schema.type) + if isinstance(name, str): + name = f'_read_data_type_{name}' + meth = cast('TypeParser', + getattr(self, name, self._read_data_type_none)) + else: + meth = name[0] + icmp = meth(schema.data, header=schema, length=length) + + # NOTE: The quoted original datagram stays raw in the message body, + # so there is no next layer; the schema has no ``payload`` field. + self._next = NoPayload() # pylint: disable=attribute-defined-outside-init + self._protos = ProtoChain(self.__class__, self.alias) # pylint: disable=attribute-defined-outside-init + return icmp + + def make(self, + type: 'Enum_Type | StdlibEnum | AenumEnum | str | int' = Enum_Type.Echo, # pylint: disable=redefined-builtin + type_default: 'Optional[int]' = None, + type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + type_reversed: 'bool' = False, + code: 'int' = 0, + checksum: 'Optional[bytes]' = None, + # Echo identifier and sequence number, the shortest body. + body: 'bytes | Data_ICMPv4 | Schema_ICMPv4Type | dict[str, Any]' = b'\x00\x00\x00\x00', + **kwargs: 'Any') -> 'Schema_ICMPv4': + """Make (construct) packet data. + + Args: + type: ICMP type. + type_default: Default value of ICMP type. + type_namespace: Namespace of ICMP type. + type_reversed: If the namespace of ICMP type is reversed. + code: ICMP code; meaning is entirely ``type``-dependent. + checksum: Checksum, stored as given; :data:`None` leaves it + ``b'\\x00\\x00'``, uncomputed. + body: Message body. :obj:`bytes` is parsed against ``type``'s own + schema; a :obj:`dict` or a :class:`~pcapkit.protocols.data.internet.icmpv4.ICMPv4` + data model is handed to that type's own constructor method + (see :attr:`__type__`); any other + :class:`~pcapkit.protocols.schema.internet.icmpv4.ICMPv4Type` is + used as is. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed packet data. + + Raises: + ProtocolError: If ``body`` is :obj:`bytes` that do not parse as + the message body of ``type``, or is of none of the accepted + types. + + """ + type_val = cast('Enum_Type', + self._make_index(type, type_default, namespace=type_namespace, + reversed=type_reversed, pack=False)) + + if isinstance(body, bytes): + try: + data_val = Schema_ICMPv4Type.registry[type_val].unpack( # pylint: disable=unsubscriptable-object + body, len(body), {}) # type: Schema_ICMPv4Type + except Exception as exc: + raise ProtocolError(f'{self.alias}: [TypeNo {type_val}] message body ' + f'{body.hex()} does not parse as this type') from exc + elif isinstance(body, (dict, Data_ICMPv4)): + name = self._lookup_registry(self.__type__, type_val) + if isinstance(name, str): + name = f'_make_data_type_{name}' + meth = cast('TypeConstructor', + getattr(self, name, self._make_data_type_none)) + else: + meth = name[1] + + if isinstance(body, dict): + data_val = meth(type_val, **body) + else: + data_val = meth(type_val, body) + elif isinstance(body, Schema_ICMPv4Type): + data_val = body + else: + raise ProtocolError(f'{self.alias}: invalid message body type: {body.__class__}') + + if checksum is None: + checksum = b'\x00\x00' + + return Schema_ICMPv4( + type=type_val, + code=code, + checksum=checksum, + data=data_val, + ) + + @classmethod + def register_type(cls, code: 'Enum_Type', meth: 'str | tuple[TypeParser, TypeConstructor]') -> 'None': + """Register a message body parser. + + Args: + code: ICMP type code. + meth: Name ``X`` of the ``_read_data_type_X`` and + ``_make_data_type_X`` methods, or a ``(parser, constructor)`` + pair of callables, see :attr:`__type__`. + + Warns: + RegistryWarning: If a different entry is already registered for + ``code``; it is overwritten. + + """ + incumbent = cls.__type__.get(code) + if incumbent is not None and incumbent == meth: + return + if incumbent is not None: + warn(f'type {code} already registered, overwriting', RegistryWarning) + cls.__type__[code] = meth + + ########################################################################## + # Data models. + ########################################################################## + + def __length_hint__(self) -> 'Literal[4]': + """Return an estimated length for the object.""" + return 4 + + @classmethod + def __index__(cls) -> 'Enum_TransType': # pylint: disable=invalid-index-returned + """Numeral registry index of the protocol. + + Returns: + Numeral registry index of the protocol in `IANA`_. + + .. _IANA: https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml + + """ + return Enum_TransType.ICMP # type: ignore[return-value] + + ########################################################################## + # Utilities. + ########################################################################## + + @classmethod + def _make_data(cls, data: 'Data_ICMPv4') -> 'dict[str, Any]': # type: ignore[override] + """Create key-value pairs from ``data`` for protocol construction. + + Args: + data: protocol data + + Returns: + Key-value pairs for protocol construction. + + """ + return { + 'type': data.type, + 'code': data.code, + 'checksum': data.checksum, + 'body': data, + } + + def _read_data_type_none(self, schema: 'Schema_Unknown', *, header: 'Schema_ICMPv4', + length: 'int') -> 'Data_Unknown': + """Read ICMP message of unknown type. + + Args: + schema: parsed message body schema + header: parsed ICMP header schema + length: total length of the current ICMP message + + Returns: + Parsed message data. + + """ + return Data_Unknown( + type=header.type, + code=header.code, + checksum=header.checksum, + length=length, + data=schema.data, + ) + + def _read_data_type_echo(self, schema: 'Schema_Echo', *, header: 'Schema_ICMPv4', + length: 'int') -> 'Data_Echo': + """Read ICMP Echo Request/Reply message. + + Structure of ICMP Echo Request/Reply message [:rfc:`792`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identifier | Sequence Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + . Data . + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + schema: parsed message body schema + header: parsed ICMP header schema + length: total length of the current ICMP message + + Returns: + Parsed message data. + + """ + return Data_Echo( + type=header.type, + code=header.code, + checksum=header.checksum, + length=length, + id=schema.id, + seq=schema.seq, + data=schema.data, + ) + + def _read_data_type_destination_unreachable(self, schema: 'Schema_DestinationUnreachable', *, + header: 'Schema_ICMPv4', + length: 'int') -> 'Data_DestinationUnreachable': + """Read ICMP Destination Unreachable message. + + Structure of ICMP Destination Unreachable message [:rfc:`792`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | unused | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Internet Header + 64 bits of Original Data Datagram | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + schema: parsed message body schema + header: parsed ICMP header schema + length: total length of the current ICMP message + + Returns: + Parsed message data. + + """ + return Data_DestinationUnreachable( + type=header.type, + code=header.code, + checksum=header.checksum, + length=length, + unused=schema.unused, + data=schema.data, + ) + + def _read_data_type_time_exceeded(self, schema: 'Schema_TimeExceeded', *, header: 'Schema_ICMPv4', + length: 'int') -> 'Data_TimeExceeded': + """Read ICMP Time Exceeded message. + + Structure of ICMP Time Exceeded message [:rfc:`792`] is identical to + that of :meth:`_read_data_type_destination_unreachable`, but for the + type code. + + Args: + schema: parsed message body schema + header: parsed ICMP header schema + length: total length of the current ICMP message + + Returns: + Parsed message data. + + """ + return Data_TimeExceeded( + type=header.type, + code=header.code, + checksum=header.checksum, + length=length, + unused=schema.unused, + data=schema.data, + ) + + def _read_data_type_parameter_problem(self, schema: 'Schema_ParameterProblem', *, + header: 'Schema_ICMPv4', + length: 'int') -> 'Data_ParameterProblem': + """Read ICMP Parameter Problem message. + + Structure of ICMP Parameter Problem message [:rfc:`792`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Pointer | unused | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Internet Header + 64 bits of Original Data Datagram | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + schema: parsed message body schema + header: parsed ICMP header schema + length: total length of the current ICMP message + + Returns: + Parsed message data. + + """ + return Data_ParameterProblem( + type=header.type, + code=header.code, + checksum=header.checksum, + length=length, + pointer=schema.pointer, + unused=schema.unused, + data=schema.data, + ) + + def _make_data_type_none(self, type: 'Enum_Type', message: 'Optional[Data_Unknown]' = None, *, # pylint: disable=redefined-builtin,unused-argument + data: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_Unknown': + """Make ICMP message of unknown type. + + Args: + type: ICMP type. + message: message data. + data: raw message body as :obj:`bytes`. + **kwargs: arbitrary keyword arguments. + + Returns: + Constructed message body schema. + + """ + if message is not None: + data = message.data + return Schema_Unknown(data=data) + + def _make_data_type_echo(self, type: 'Enum_Type', message: 'Optional[Data_Echo]' = None, *, # pylint: disable=redefined-builtin,unused-argument + id: 'int' = 0, seq: 'int' = 0, data: 'bytes' = b'', # pylint: disable=redefined-builtin + **kwargs: 'Any') -> 'Schema_Echo': + """Make ICMP Echo Request/Reply message. + + Args: + type: ICMP type. + message: message data. + id: identifier. + seq: sequence number. + data: data, echoed back verbatim by a reply to the same + identifier and sequence number. + **kwargs: arbitrary keyword arguments. + + Returns: + Constructed message body schema. + + """ + if message is not None: + id = message.id + seq = message.seq + data = message.data + return Schema_Echo(id=id, seq=seq, data=data) + + def _make_data_type_destination_unreachable(self, type: 'Enum_Type', # pylint: disable=redefined-builtin,unused-argument + message: 'Optional[Data_DestinationUnreachable]' = None, *, + unused: 'bytes' = b'\x00\x00\x00\x00', + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_DestinationUnreachable': + """Make ICMP Destination Unreachable message. + + Args: + type: ICMP type. + message: message data. + unused: the four unused octets, truncated or zero-filled to four + octets. + data: internet header plus the first eight octets of the + original datagram's payload. + **kwargs: arbitrary keyword arguments. + + Returns: + Constructed message body schema. + + """ + if message is not None: + unused = getattr(message, 'unused', b'\x00\x00\x00\x00') + data = message.data + return Schema_DestinationUnreachable(unused=unused, data=data) + + def _make_data_type_time_exceeded(self, type: 'Enum_Type', # pylint: disable=redefined-builtin,unused-argument + message: 'Optional[Data_TimeExceeded]' = None, *, + unused: 'bytes' = b'\x00\x00\x00\x00', + data: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_TimeExceeded': + """Make ICMP Time Exceeded message. + + Args: + type: ICMP type. + message: message data. + unused: the four unused octets, truncated or zero-filled to four + octets. + data: internet header plus the first eight octets of the + original datagram's payload. + **kwargs: arbitrary keyword arguments. + + Returns: + Constructed message body schema. + + """ + if message is not None: + unused = getattr(message, 'unused', b'\x00\x00\x00\x00') + data = message.data + return Schema_TimeExceeded(unused=unused, data=data) + + def _make_data_type_parameter_problem(self, type: 'Enum_Type', # pylint: disable=redefined-builtin,unused-argument + message: 'Optional[Data_ParameterProblem]' = None, *, + pointer: 'int' = 0, unused: 'bytes' = b'\x00\x00\x00', + data: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_ParameterProblem': + """Make ICMP Parameter Problem message. + + Args: + type: ICMP type. + message: message data. + pointer: pointer to the octet of the original datagram that + caused the error. + unused: the three unused octets, truncated or zero-filled to + three octets. + data: internet header plus the first eight octets of the + original datagram's payload. + **kwargs: arbitrary keyword arguments. + + Returns: + Constructed message body schema. + + """ + if message is not None: + pointer = message.pointer + unused = getattr(message, 'unused', b'\x00\x00\x00') + data = message.data + return Schema_ParameterProblem(pointer=pointer, unused=unused, data=data) diff --git a/pcapkit/protocols/internet/icmpv6.py b/pcapkit/protocols/internet/icmpv6.py new file mode 100644 index 0000000000..5dca22af10 --- /dev/null +++ b/pcapkit/protocols/internet/icmpv6.py @@ -0,0 +1,682 @@ +# -*- coding: utf-8 -*- +"""ICMPv6 - Internet Control Message Protocol for IPv6 +=========================================================== + +.. module:: pcapkit.protocols.internet.icmpv6 + +:mod:`pcapkit.protocols.internet.icmpv6` contains +:class:`~pcapkit.protocols.internet.icmpv6.ICMPv6` only, +which implements extractor for Internet Control +Message Protocol for IPv6 (ICMPv6) [*]_, whose structure +is described as below: + +======= ========= ======================= =================================== +Octets Bits Name Description +======= ========= ======================= =================================== + 0 0 ``icmpv6.type`` Type + 1 8 ``icmpv6.code`` Code + 2 16 ``icmpv6.checksum`` Checksum + 4 32 ``icmpv6.data`` Message Body +======= ========= ======================= =================================== + +.. [*] https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol_for_IPv6 + +""" +import collections +from typing import TYPE_CHECKING, cast + +from pcapkit.const.icmpv6.type import Type as Enum_Type +from pcapkit.const.reg.transtype import TransType as Enum_TransType +from pcapkit.corekit.protochain import ProtoChain +from pcapkit.protocols.data.internet.icmpv6 import \ + DestinationUnreachable as Data_DestinationUnreachable +from pcapkit.protocols.data.internet.icmpv6 import Echo as Data_Echo +from pcapkit.protocols.data.internet.icmpv6 import ICMPv6 as Data_ICMPv6 +from pcapkit.protocols.data.internet.icmpv6 import PacketTooBig as Data_PacketTooBig +from pcapkit.protocols.data.internet.icmpv6 import ParameterProblem as Data_ParameterProblem +from pcapkit.protocols.data.internet.icmpv6 import TimeExceeded as Data_TimeExceeded +from pcapkit.protocols.data.internet.icmpv6 import Unknown as Data_Unknown +from pcapkit.protocols.internet.icmp import ICMP +from pcapkit.protocols.misc.null import NoPayload +from pcapkit.protocols.schema.internet.icmpv6 import \ + DestinationUnreachable as Schema_DestinationUnreachable +from pcapkit.protocols.schema.internet.icmpv6 import Echo as Schema_Echo +from pcapkit.protocols.schema.internet.icmpv6 import ICMPv6 as Schema_ICMPv6 +from pcapkit.protocols.schema.internet.icmpv6 import ICMPv6Type as Schema_ICMPv6Type +from pcapkit.protocols.schema.internet.icmpv6 import PacketTooBig as Schema_PacketTooBig +from pcapkit.protocols.schema.internet.icmpv6 import ParameterProblem as Schema_ParameterProblem +from pcapkit.protocols.schema.internet.icmpv6 import TimeExceeded as Schema_TimeExceeded +from pcapkit.protocols.schema.internet.icmpv6 import Unknown as Schema_Unknown +from pcapkit.utilities.exceptions import ProtocolError +from pcapkit.utilities.warnings import RegistryWarning, warn + +if TYPE_CHECKING: + from enum import IntEnum as StdlibEnum + from typing import Any, Callable, DefaultDict, NoReturn, Optional, Type + + from aenum import IntEnum as AenumEnum + from mypy_extensions import DefaultArg, KwArg, NamedArg + from typing_extensions import Literal + + TypeParser = Callable[[Schema_ICMPv6Type, NamedArg(Schema_ICMPv6, 'header'), NamedArg(int, 'length')], + Data_ICMPv6] + TypeConstructor = Callable[[Enum_Type, DefaultArg(Optional[Data_ICMPv6]), KwArg(Any)], Schema_ICMPv6Type] + +__all__ = ['ICMPv6'] + + +class ICMPv6(ICMP[Data_ICMPv6, Schema_ICMPv6], + schema=Schema_ICMPv6, data=Data_ICMPv6): + """This class implements Internet Control Message Protocol for IPv6. + + This class parses the following ICMPv6 message + types, which are registered in the + :attr:`self.__type__ ` + attribute: + + .. list-table:: + :header-rows: 1 + + * - Type + - Data Parser + - Data Constructor + * - Destination Unreachable + - :meth:`~pcapkit.protocols.internet.icmpv6.ICMPv6._read_data_type_destination_unreachable` + - :meth:`~pcapkit.protocols.internet.icmpv6.ICMPv6._make_data_type_destination_unreachable` + * - Packet Too Big + - :meth:`~pcapkit.protocols.internet.icmpv6.ICMPv6._read_data_type_packet_too_big` + - :meth:`~pcapkit.protocols.internet.icmpv6.ICMPv6._make_data_type_packet_too_big` + * - Time Exceeded + - :meth:`~pcapkit.protocols.internet.icmpv6.ICMPv6._read_data_type_time_exceeded` + - :meth:`~pcapkit.protocols.internet.icmpv6.ICMPv6._make_data_type_time_exceeded` + * - Parameter Problem + - :meth:`~pcapkit.protocols.internet.icmpv6.ICMPv6._read_data_type_parameter_problem` + - :meth:`~pcapkit.protocols.internet.icmpv6.ICMPv6._make_data_type_parameter_problem` + * - Echo Request / Echo Reply + - :meth:`~pcapkit.protocols.internet.icmpv6.ICMPv6._read_data_type_echo` + - :meth:`~pcapkit.protocols.internet.icmpv6.ICMPv6._make_data_type_echo` + + Any other type, including Neighbor Discovery (:rfc:`4861`) and Multicast + Listener Discovery (:rfc:`9777`), is kept as its raw message body, see + :meth:`_read_data_type_none` and :meth:`_make_data_type_none`. + + """ + + ########################################################################## + # Defaults. + ########################################################################## + + #: Type code to method mapping. A :obj:`str` value names the methods + #: ``_read_data_type_${name}`` and ``_make_data_type_${name}``, falling + #: back to ``_read_data_type_none`` / ``_make_data_type_none`` when they + #: do not exist; any other value is a ``(parser, constructor)`` pair of + #: callables. + __type__ = collections.defaultdict( + lambda: 'none', + { + Enum_Type.Destination_Unreachable: 'destination_unreachable', + Enum_Type.Packet_Too_Big: 'packet_too_big', + Enum_Type.Time_Exceeded: 'time_exceeded', + Enum_Type.Parameter_Problem: 'parameter_problem', + Enum_Type.Echo_Request: 'echo', + Enum_Type.Echo_Reply: 'echo', + }, + ) # type: DefaultDict[Enum_Type | int, str | tuple[TypeParser, TypeConstructor]] + + ########################################################################## + # Properties. + ########################################################################## + + @property + def name(self) -> 'Literal["Internet Control Message Protocol for IPv6"]': + """Name of current protocol.""" + return 'Internet Control Message Protocol for IPv6' + + @property + def alias(self) -> 'Literal["ICMPv6"]': + """Acronym of corresponding protocol.""" + return 'ICMPv6' + + @property + def length(self) -> 'int': + """Header length of current protocol.""" + return self._info.length + + ########################################################################## + # Methods. + ########################################################################## + + @classmethod + def id(cls) -> 'tuple[Literal["ICMPv6"]]': # type: ignore[override] + """Index ID of the protocol. + + Returns: + Index ID of the protocol. + + """ + return ('ICMPv6',) + + def read(self, length: 'Optional[int]' = None, *, + __packet__: 'Optional[dict[str, Any]]' = None, **kwargs: 'Any') -> 'Data_ICMPv6': # pylint: disable=unused-argument + """Read Internet Control Message Protocol for IPv6 (ICMPv6). + + Structure of ICMPv6 header [:rfc:`4443`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + Message Body + + | | + + Args: + length: Length of packet data. + __packet__: Optional packet data, unused. + **kwargs: Arbitrary keyword arguments. + + Returns: + Parsed packet data. + + """ + if length is None: + length = len(self) + schema = self.__header__ + + name = self._lookup_registry(self.__type__, schema.type) + if isinstance(name, str): + name = f'_read_data_type_{name}' + meth = cast('TypeParser', + getattr(self, name, self._read_data_type_none)) + else: + meth = name[0] + icmpv6 = meth(schema.data, header=schema, length=length) + + # NOTE: The quoted original packet stays raw in the message body, + # so there is no next layer; the schema has no ``payload`` field. + self._next = NoPayload() # pylint: disable=attribute-defined-outside-init + self._protos = ProtoChain(self.__class__, self.alias) # pylint: disable=attribute-defined-outside-init + return icmpv6 + + def make(self, + type: 'Enum_Type | StdlibEnum | AenumEnum | str | int' = Enum_Type.Echo_Request, # pylint: disable=redefined-builtin + type_default: 'Optional[int]' = None, + type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long + type_reversed: 'bool' = False, + code: 'int' = 0, + checksum: 'Optional[bytes]' = None, + # Echo identifier and sequence number, the shortest body. + body: 'bytes | Data_ICMPv6 | Schema_ICMPv6Type | dict[str, Any]' = b'\x00\x00\x00\x00', + **kwargs: 'Any') -> 'Schema_ICMPv6': + """Make (construct) packet data. + + Args: + type: ICMPv6 type. + type_default: Default value of ICMPv6 type. + type_namespace: Namespace of ICMPv6 type. + type_reversed: If the namespace of ICMPv6 type is reversed. + code: ICMPv6 code; meaning is entirely ``type``-dependent. + checksum: Checksum, stored as given; :data:`None` leaves it + ``b'\\x00\\x00'``, uncomputed. + body: Message body. :obj:`bytes` is parsed against ``type``'s own + schema; a :obj:`dict` or a + :class:`~pcapkit.protocols.data.internet.icmpv6.ICMPv6` data + model is handed to that type's own constructor method (see + :attr:`__type__`); any other + :class:`~pcapkit.protocols.schema.internet.icmpv6.ICMPv6Type` + is used as is. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed packet data. + + Raises: + ProtocolError: If ``body`` is :obj:`bytes` that do not parse as + the message body of ``type``, or is of none of the accepted + types. + + """ + type_val = cast('Enum_Type', + self._make_index(type, type_default, namespace=type_namespace, + reversed=type_reversed, pack=False)) + + if isinstance(body, bytes): + try: + data_val = Schema_ICMPv6Type.registry[type_val].unpack( # pylint: disable=unsubscriptable-object + body, len(body), {}) # type: Schema_ICMPv6Type + except Exception as exc: + raise ProtocolError(f'{self.alias}: [TypeNo {type_val}] message body ' + f'{body.hex()} does not parse as this type') from exc + elif isinstance(body, (dict, Data_ICMPv6)): + name = self._lookup_registry(self.__type__, type_val) + if isinstance(name, str): + name = f'_make_data_type_{name}' + meth = cast('TypeConstructor', + getattr(self, name, self._make_data_type_none)) + else: + meth = name[1] + + if isinstance(body, dict): + data_val = meth(type_val, **body) + else: + data_val = meth(type_val, body) + elif isinstance(body, Schema_ICMPv6Type): + data_val = body + else: + raise ProtocolError(f'{self.alias}: invalid message body type: {body.__class__}') + + if checksum is None: + checksum = b'\x00\x00' + + return Schema_ICMPv6( + type=type_val, + code=code, + checksum=checksum, + data=data_val, + ) + + @classmethod + def register_type(cls, code: 'Enum_Type', meth: 'str | tuple[TypeParser, TypeConstructor]') -> 'None': + """Register a message body parser. + + Args: + code: ICMPv6 type code. + meth: Name ``X`` of the ``_read_data_type_X`` and + ``_make_data_type_X`` methods, or a ``(parser, constructor)`` + pair of callables, see :attr:`__type__`. + + Warns: + RegistryWarning: If a different entry is already registered for + ``code``; it is overwritten. + + """ + incumbent = cls.__type__.get(code) + if incumbent is not None and incumbent == meth: + return + if incumbent is not None: + warn(f'type {code} already registered, overwriting', RegistryWarning) + cls.__type__[code] = meth + + ########################################################################## + # Data models. + ########################################################################## + + def __length_hint__(self) -> 'Literal[4]': + """Return an estimated length for the object.""" + return 4 + + @classmethod + def __index__(cls) -> 'Enum_TransType': # pylint: disable=invalid-index-returned + """Numeral registry index of the protocol. + + Returns: + Numeral registry index of the protocol in `IANA`_. + + .. _IANA: https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml + + """ + return Enum_TransType.IPv6_ICMP # type: ignore[return-value] + + ########################################################################## + # Utilities. + ########################################################################## + + @classmethod + def _make_data(cls, data: 'Data_ICMPv6') -> 'dict[str, Any]': # type: ignore[override] + """Create key-value pairs from ``data`` for protocol construction. + + Args: + data: protocol data + + Returns: + Key-value pairs for protocol construction. + + """ + return { + 'type': data.type, + 'code': data.code, + 'checksum': data.checksum, + 'body': data, + } + + def _read_data_type_none(self, schema: 'Schema_Unknown', *, header: 'Schema_ICMPv6', + length: 'int') -> 'Data_Unknown': + """Read ICMPv6 message of unknown type. + + Args: + schema: parsed message body schema + header: parsed ICMPv6 header schema + length: total length of the current ICMPv6 message + + Returns: + Parsed message data. + + """ + return Data_Unknown( + type=header.type, + code=header.code, + checksum=header.checksum, + length=length, + data=schema.data, + ) + + def _read_data_type_destination_unreachable(self, schema: 'Schema_DestinationUnreachable', *, + header: 'Schema_ICMPv6', + length: 'int') -> 'Data_DestinationUnreachable': + """Read ICMPv6 Destination Unreachable message. + + Structure of ICMPv6 Destination Unreachable message [:rfc:`4443`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Unused | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | As much of invoking packet | + | as will fit without the ICMPv6 | + | packet exceeding the minimum IPv6 MTU | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + schema: parsed message body schema + header: parsed ICMPv6 header schema + length: total length of the current ICMPv6 message + + Returns: + Parsed message data. + + """ + return Data_DestinationUnreachable( + type=header.type, + code=header.code, + checksum=header.checksum, + length=length, + unused=schema.unused, + data=schema.data, + ) + + def _read_data_type_packet_too_big(self, schema: 'Schema_PacketTooBig', *, header: 'Schema_ICMPv6', + length: 'int') -> 'Data_PacketTooBig': + """Read ICMPv6 Packet Too Big message. + + Structure of ICMPv6 Packet Too Big message [:rfc:`4443`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | MTU | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | As much of invoking packet | + | as will fit without the ICMPv6 | + | packet exceeding the minimum IPv6 MTU | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + schema: parsed message body schema + header: parsed ICMPv6 header schema + length: total length of the current ICMPv6 message + + Returns: + Parsed message data. + + """ + return Data_PacketTooBig( + type=header.type, + code=header.code, + checksum=header.checksum, + length=length, + mtu=schema.mtu, + data=schema.data, + ) + + def _read_data_type_time_exceeded(self, schema: 'Schema_TimeExceeded', *, header: 'Schema_ICMPv6', + length: 'int') -> 'Data_TimeExceeded': + """Read ICMPv6 Time Exceeded message. + + Structure of ICMPv6 Time Exceeded message [:rfc:`4443`] is identical + to that of :meth:`_read_data_type_destination_unreachable`, but for + the type code. + + Args: + schema: parsed message body schema + header: parsed ICMPv6 header schema + length: total length of the current ICMPv6 message + + Returns: + Parsed message data. + + """ + return Data_TimeExceeded( + type=header.type, + code=header.code, + checksum=header.checksum, + length=length, + unused=schema.unused, + data=schema.data, + ) + + def _read_data_type_parameter_problem(self, schema: 'Schema_ParameterProblem', *, + header: 'Schema_ICMPv6', + length: 'int') -> 'Data_ParameterProblem': + """Read ICMPv6 Parameter Problem message. + + Structure of ICMPv6 Parameter Problem message [:rfc:`4443`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Pointer | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | As much of invoking packet | + | as will fit without the ICMPv6 | + | packet exceeding the minimum IPv6 MTU | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + schema: parsed message body schema + header: parsed ICMPv6 header schema + length: total length of the current ICMPv6 message + + Returns: + Parsed message data. + + """ + return Data_ParameterProblem( + type=header.type, + code=header.code, + checksum=header.checksum, + length=length, + pointer=schema.pointer, + data=schema.data, + ) + + def _read_data_type_echo(self, schema: 'Schema_Echo', *, header: 'Schema_ICMPv6', + length: 'int') -> 'Data_Echo': + """Read ICMPv6 Echo Request/Reply message. + + Structure of ICMPv6 Echo Request/Reply message [:rfc:`4443`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Code | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identifier | Sequence Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + Data + + | | + + Args: + schema: parsed message body schema + header: parsed ICMPv6 header schema + length: total length of the current ICMPv6 message + + Returns: + Parsed message data. + + """ + return Data_Echo( + type=header.type, + code=header.code, + checksum=header.checksum, + length=length, + id=schema.id, + seq=schema.seq, + data=schema.data, + ) + + def _make_data_type_none(self, type: 'Enum_Type', message: 'Optional[Data_Unknown]' = None, *, # pylint: disable=redefined-builtin,unused-argument + data: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_Unknown': + """Make ICMPv6 message of unknown type. + + Args: + type: ICMPv6 type. + message: message data. + data: raw message body as :obj:`bytes`. + **kwargs: arbitrary keyword arguments. + + Returns: + Constructed message body schema. + + """ + if message is not None: + data = message.data + return Schema_Unknown(data=data) + + def _make_data_type_destination_unreachable(self, type: 'Enum_Type', # pylint: disable=redefined-builtin,unused-argument + message: 'Optional[Data_DestinationUnreachable]' = None, *, + unused: 'bytes' = b'\x00\x00\x00\x00', + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_DestinationUnreachable': + """Make ICMPv6 Destination Unreachable message. + + Args: + type: ICMPv6 type. + message: message data. + unused: the four unused octets, truncated or zero-filled to four + octets. + data: as much of the invoking packet as fits the minimum IPv6 + MTU. + **kwargs: arbitrary keyword arguments. + + Returns: + Constructed message body schema. + + """ + if message is not None: + unused = getattr(message, 'unused', b'\x00\x00\x00\x00') + data = message.data + return Schema_DestinationUnreachable(unused=unused, data=data) + + def _make_data_type_packet_too_big(self, type: 'Enum_Type', # pylint: disable=redefined-builtin,unused-argument + message: 'Optional[Data_PacketTooBig]' = None, *, + mtu: 'int' = 0, data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_PacketTooBig': + """Make ICMPv6 Packet Too Big message. + + Args: + type: ICMPv6 type. + message: message data. + mtu: MTU of the link that could not forward the invoking packet. + data: as much of the invoking packet as fits the minimum IPv6 + MTU. + **kwargs: arbitrary keyword arguments. + + Returns: + Constructed message body schema. + + """ + if message is not None: + mtu = message.mtu + data = message.data + return Schema_PacketTooBig(mtu=mtu, data=data) + + def _make_data_type_time_exceeded(self, type: 'Enum_Type', # pylint: disable=redefined-builtin,unused-argument + message: 'Optional[Data_TimeExceeded]' = None, *, + unused: 'bytes' = b'\x00\x00\x00\x00', + data: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_TimeExceeded': + """Make ICMPv6 Time Exceeded message. + + Args: + type: ICMPv6 type. + message: message data. + unused: the four unused octets, truncated or zero-filled to four + octets. + data: as much of the invoking packet as fits the minimum IPv6 + MTU. + **kwargs: arbitrary keyword arguments. + + Returns: + Constructed message body schema. + + """ + if message is not None: + unused = getattr(message, 'unused', b'\x00\x00\x00\x00') + data = message.data + return Schema_TimeExceeded(unused=unused, data=data) + + def _make_data_type_parameter_problem(self, type: 'Enum_Type', # pylint: disable=redefined-builtin,unused-argument + message: 'Optional[Data_ParameterProblem]' = None, *, + pointer: 'int' = 0, data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_ParameterProblem': + """Make ICMPv6 Parameter Problem message. + + Args: + type: ICMPv6 type. + message: message data. + pointer: pointer to the octet offset within the invoking packet + where the error was detected. + data: as much of the invoking packet as fits the minimum IPv6 + MTU. + **kwargs: arbitrary keyword arguments. + + Returns: + Constructed message body schema. + + """ + if message is not None: + pointer = message.pointer + data = message.data + return Schema_ParameterProblem(pointer=pointer, data=data) + + def _make_data_type_echo(self, type: 'Enum_Type', message: 'Optional[Data_Echo]' = None, *, # pylint: disable=redefined-builtin,unused-argument + id: 'int' = 0, seq: 'int' = 0, data: 'bytes' = b'', # pylint: disable=redefined-builtin + **kwargs: 'Any') -> 'Schema_Echo': + """Make ICMPv6 Echo Request/Reply message. + + Args: + type: ICMPv6 type. + message: message data. + id: identifier. + seq: sequence number. + data: data, echoed back verbatim by a reply to the same + identifier and sequence number. + **kwargs: arbitrary keyword arguments. + + Returns: + Constructed message body schema. + + """ + if message is not None: + id = message.id + seq = message.seq + data = message.data + return Schema_Echo(id=id, seq=seq, data=data) diff --git a/pcapkit/protocols/internet/internet.py b/pcapkit/protocols/internet/internet.py index aac12d60ca..2aae7acbac 100644 --- a/pcapkit/protocols/internet/internet.py +++ b/pcapkit/protocols/internet/internet.py @@ -60,12 +60,16 @@ class Internet(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=ab - :class:`pcapkit.protocols.internet.hopopt.HOPOPT` * - :attr:`~pcapkit.const.reg.transtype.TransType.IPv4` - :class:`pcapkit.protocols.internet.ipv4.IPv4` + * - :attr:`~pcapkit.const.reg.transtype.TransType.ICMP` + - :class:`pcapkit.protocols.internet.icmpv4.ICMPv4` * - :attr:`~pcapkit.const.reg.transtype.TransType.TCP` - :class:`pcapkit.protocols.transport.tcp.TCP` * - :attr:`~pcapkit.const.reg.transtype.TransType.UDP` - :class:`pcapkit.protocols.transport.udp.UDP` * - :attr:`~pcapkit.const.reg.transtype.TransType.IPv6` - :class:`pcapkit.protocols.internet.ipv6.IPv6` + * - :attr:`~pcapkit.const.reg.transtype.TransType.IPv6_ICMP` + - :class:`pcapkit.protocols.internet.icmpv6.ICMPv6` * - :attr:`~pcapkit.const.reg.transtype.TransType.IPv6_Route` - :class:`pcapkit.protocols.internet.ipv6_route.IPv6_Route` * - :attr:`~pcapkit.const.reg.transtype.TransType.IPv6_Frag` @@ -118,9 +122,11 @@ class Internet(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=ab { Enum_TransType.HOPOPT: ModuleDescriptor('pcapkit.protocols.internet.hopopt', 'HOPOPT'), Enum_TransType.IPv4: ModuleDescriptor('pcapkit.protocols.internet.ipv4', 'IPv4'), + Enum_TransType.ICMP: ModuleDescriptor('pcapkit.protocols.internet.icmpv4', 'ICMPv4'), Enum_TransType.TCP: ModuleDescriptor('pcapkit.protocols.transport.tcp', 'TCP'), Enum_TransType.UDP: ModuleDescriptor('pcapkit.protocols.transport.udp', 'UDP'), Enum_TransType.IPv6: ModuleDescriptor('pcapkit.protocols.internet.ipv6', 'IPv6'), + Enum_TransType.IPv6_ICMP: ModuleDescriptor('pcapkit.protocols.internet.icmpv6', 'ICMPv6'), Enum_TransType.IPv6_Route: ModuleDescriptor('pcapkit.protocols.internet.ipv6_route', 'IPv6_Route'), Enum_TransType.IPv6_Frag: ModuleDescriptor('pcapkit.protocols.internet.ipv6_frag', 'IPv6_Frag'), Enum_TransType.ESP: ModuleDescriptor('pcapkit.protocols.internet.esp', 'ESP'), diff --git a/pcapkit/protocols/schema/__init__.py b/pcapkit/protocols/schema/__init__.py index cf7cca6790..da9bad766b 100644 --- a/pcapkit/protocols/schema/__init__.py +++ b/pcapkit/protocols/schema/__init__.py @@ -63,6 +63,14 @@ 'HOPOPT_QuickStartReportOption', 'HOPOPT_RPLOption', 'HOPOPT_MPLOption', 'HOPOPT_ILNPOption', 'HOPOPT_LineIdentificationOption', 'HOPOPT_JumboPayloadOption', 'HOPOPT_HomeAddressOption', 'HOPOPT_IPDFFOption', + 'ICMPv4', + 'ICMPv4_ICMPv4Type', + 'ICMPv4_Unknown', 'ICMPv4_Echo', 'ICMPv4_DestinationUnreachable', 'ICMPv4_TimeExceeded', + 'ICMPv4_ParameterProblem', + 'ICMPv6', + 'ICMPv6_ICMPv6Type', + 'ICMPv6_Unknown', 'ICMPv6_DestinationUnreachable', 'ICMPv6_PacketTooBig', 'ICMPv6_TimeExceeded', + 'ICMPv6_ParameterProblem', 'ICMPv6_Echo', 'IPv4', 'IPv4_Option', 'IPv4_UnassignedOption', 'IPv4_EOOLOption', 'IPv4_NOPOption', diff --git a/pcapkit/protocols/schema/internet/__init__.py b/pcapkit/protocols/schema/internet/__init__.py index f1e95b4a74..a685ae0828 100644 --- a/pcapkit/protocols/schema/internet/__init__.py +++ b/pcapkit/protocols/schema/internet/__init__.py @@ -107,6 +107,27 @@ TunnelEncapsulationLimitOption as HOPOPT_TunnelEncapsulationLimitOption from pcapkit.protocols.schema.internet.hopopt import UnassignedOption as HOPOPT_UnassignedOption +# Internet Control Message Protocol +from pcapkit.protocols.schema.internet.icmpv4 import ICMPv4 +from pcapkit.protocols.schema.internet.icmpv4 import \ + DestinationUnreachable as ICMPv4_DestinationUnreachable +from pcapkit.protocols.schema.internet.icmpv4 import Echo as ICMPv4_Echo +from pcapkit.protocols.schema.internet.icmpv4 import ICMPv4Type as ICMPv4_ICMPv4Type +from pcapkit.protocols.schema.internet.icmpv4 import ParameterProblem as ICMPv4_ParameterProblem +from pcapkit.protocols.schema.internet.icmpv4 import TimeExceeded as ICMPv4_TimeExceeded +from pcapkit.protocols.schema.internet.icmpv4 import Unknown as ICMPv4_Unknown + +# Internet Control Message Protocol for IPv6 +from pcapkit.protocols.schema.internet.icmpv6 import ICMPv6 +from pcapkit.protocols.schema.internet.icmpv6 import \ + DestinationUnreachable as ICMPv6_DestinationUnreachable +from pcapkit.protocols.schema.internet.icmpv6 import Echo as ICMPv6_Echo +from pcapkit.protocols.schema.internet.icmpv6 import ICMPv6Type as ICMPv6_ICMPv6Type +from pcapkit.protocols.schema.internet.icmpv6 import PacketTooBig as ICMPv6_PacketTooBig +from pcapkit.protocols.schema.internet.icmpv6 import ParameterProblem as ICMPv6_ParameterProblem +from pcapkit.protocols.schema.internet.icmpv6 import TimeExceeded as ICMPv6_TimeExceeded +from pcapkit.protocols.schema.internet.icmpv6 import Unknown as ICMPv6_Unknown + # Internet Group Management Protocol from pcapkit.protocols.schema.internet.igmp import IGMP @@ -424,6 +445,18 @@ 'HOPOPT_LineIdentificationOption', 'HOPOPT_JumboPayloadOption', 'HOPOPT_HomeAddressOption', 'HOPOPT_IPDFFOption', + # Internet Control Message Protocol + 'ICMPv4', + 'ICMPv4_ICMPv4Type', + 'ICMPv4_Unknown', 'ICMPv4_Echo', 'ICMPv4_DestinationUnreachable', 'ICMPv4_TimeExceeded', + 'ICMPv4_ParameterProblem', + + # Internet Control Message Protocol for IPv6 + 'ICMPv6', + 'ICMPv6_ICMPv6Type', + 'ICMPv6_Unknown', 'ICMPv6_DestinationUnreachable', 'ICMPv6_PacketTooBig', 'ICMPv6_TimeExceeded', + 'ICMPv6_ParameterProblem', 'ICMPv6_Echo', + # Internet Group Management Protocol 'IGMP', diff --git a/pcapkit/protocols/schema/internet/icmpv4.py b/pcapkit/protocols/schema/internet/icmpv4.py new file mode 100644 index 0000000000..a7d82a322f --- /dev/null +++ b/pcapkit/protocols/schema/internet/icmpv4.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +# mypy: disable-error-code=assignment +"""header schema for ICMP protocol""" + +from typing import TYPE_CHECKING, cast + +from pcapkit.const.icmp.type import Type as Enum_Type +from pcapkit.corekit.fields.misc import SchemaField, SwitchField +from pcapkit.corekit.fields.numbers import EnumField, UInt8Field, UInt16Field +from pcapkit.corekit.fields.strings import BytesField, PaddingField +from pcapkit.protocols.schema.schema import EnumSchema, Schema, schema_final + +__all__ = [ + 'ICMPv4', + + 'ICMPv4Type', 'Unknown', 'Echo', 'DestinationUnreachable', 'TimeExceeded', 'ParameterProblem', +] + +if TYPE_CHECKING: + from typing import Any + + from pcapkit.corekit.fields.field import FieldBase as Field + + +def icmpv4_data_selector(pkt: 'dict[str, Any]') -> 'Field': + """Selector function for :attr:`ICMPv4.data` field. + + Args: + pkt: Packet data. + + Returns: + A :class:`~pcapkit.corekit.fields.misc.SchemaField` wrapped + :class:`~pcapkit.protocols.schema.internet.icmpv4.ICMPv4Type` instance + based on :attr:`ICMPv4.type `. + + """ + type = cast('Enum_Type', pkt['type']) + schema = ICMPv4Type.registry[type] + # NOTE: ``type``, ``code`` and ``checksum`` have already been deducted + # from ``__length__``, so it is exactly the message body length. + return SchemaField(length=cast('int', pkt['__length__']), schema=schema) + + +@schema_final +class ICMPv4(Schema): + """Header schema for ICMP packet.""" + + #: ICMP type. + type: 'Enum_Type' = EnumField(length=1, namespace=Enum_Type) + #: ICMP code. + code: 'int' = UInt8Field() + #: Checksum. + checksum: 'bytes' = BytesField(length=2) + #: Message body, selected by :attr:`type`. + data: 'ICMPv4Type' = SwitchField(selector=icmpv4_data_selector) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Type', code: 'int', checksum: 'bytes', + data: 'bytes | ICMPv4Type') -> 'None': ... + + +class ICMPv4Type(EnumSchema[Enum_Type]): + """Header schema for ICMP type-specific message body.""" + + __default__ = lambda: Unknown + + +@schema_final +class Unknown(ICMPv4Type): + """Header schema for ICMP message of unknown type.""" + + #: Message body. + data: 'bytes' = BytesField(length=lambda pkt: pkt['__length__']) + + if TYPE_CHECKING: + def __init__(self, data: 'bytes') -> 'None': ... + + +@schema_final +class Echo(ICMPv4Type, code=[Enum_Type.Echo, Enum_Type.Echo_Reply]): + """Header schema for ICMP Echo Request/Reply message.""" + + #: Identifier. + id: 'int' = UInt16Field() + #: Sequence number. + seq: 'int' = UInt16Field() + #: Data. + data: 'bytes' = BytesField(length=lambda pkt: pkt['__length__']) + + if TYPE_CHECKING: + def __init__(self, id: 'int', seq: 'int', data: 'bytes') -> 'None': ... # pylint: disable=redefined-builtin + + +@schema_final +class DestinationUnreachable(ICMPv4Type, code=Enum_Type.Destination_Unreachable): + """Header schema for ICMP Destination Unreachable message.""" + + #: Unused. + unused: 'bytes' = PaddingField(length=4) + #: Internet header plus the first eight octets of the original + #: datagram's payload. + data: 'bytes' = BytesField(length=lambda pkt: pkt['__length__']) + + if TYPE_CHECKING: + def __init__(self, unused: 'bytes', data: 'bytes') -> 'None': ... + + +@schema_final +class TimeExceeded(ICMPv4Type, code=Enum_Type.Time_Exceeded): + """Header schema for ICMP Time Exceeded message.""" + + #: Unused. + unused: 'bytes' = PaddingField(length=4) + #: Internet header plus the first eight octets of the original + #: datagram's payload. + data: 'bytes' = BytesField(length=lambda pkt: pkt['__length__']) + + if TYPE_CHECKING: + def __init__(self, unused: 'bytes', data: 'bytes') -> 'None': ... + + +@schema_final +class ParameterProblem(ICMPv4Type, code=Enum_Type.Parameter_Problem): + """Header schema for ICMP Parameter Problem message.""" + + #: Pointer. + pointer: 'int' = UInt8Field() + #: Unused. + unused: 'bytes' = PaddingField(length=3) + #: Internet header plus the first eight octets of the original + #: datagram's payload. + data: 'bytes' = BytesField(length=lambda pkt: pkt['__length__']) + + if TYPE_CHECKING: + def __init__(self, pointer: 'int', unused: 'bytes', data: 'bytes') -> 'None': ... diff --git a/pcapkit/protocols/schema/internet/icmpv6.py b/pcapkit/protocols/schema/internet/icmpv6.py new file mode 100644 index 0000000000..9405fa8750 --- /dev/null +++ b/pcapkit/protocols/schema/internet/icmpv6.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# mypy: disable-error-code=assignment +"""header schema for ICMPv6 protocol""" + +from typing import TYPE_CHECKING, cast + +from pcapkit.const.icmpv6.type import Type as Enum_Type +from pcapkit.corekit.fields.misc import SchemaField, SwitchField +from pcapkit.corekit.fields.numbers import EnumField, UInt8Field, UInt16Field, UInt32Field +from pcapkit.corekit.fields.strings import BytesField, PaddingField +from pcapkit.protocols.schema.schema import EnumSchema, Schema, schema_final + +__all__ = [ + 'ICMPv6', + + 'ICMPv6Type', 'Unknown', 'DestinationUnreachable', 'PacketTooBig', 'TimeExceeded', + 'ParameterProblem', 'Echo', +] + +if TYPE_CHECKING: + from typing import Any + + from pcapkit.corekit.fields.field import FieldBase as Field + + +def icmpv6_data_selector(pkt: 'dict[str, Any]') -> 'Field': + """Selector function for :attr:`ICMPv6.data` field. + + Args: + pkt: Packet data. + + Returns: + A :class:`~pcapkit.corekit.fields.misc.SchemaField` wrapped + :class:`~pcapkit.protocols.schema.internet.icmpv6.ICMPv6Type` instance + based on :attr:`ICMPv6.type `. + + """ + type = cast('Enum_Type', pkt['type']) + schema = ICMPv6Type.registry[type] + # NOTE: ``type``, ``code`` and ``checksum`` have already been deducted + # from ``__length__``, so it is exactly the message body length. + return SchemaField(length=cast('int', pkt['__length__']), schema=schema) + + +@schema_final +class ICMPv6(Schema): + """Header schema for ICMPv6 packet.""" + + #: ICMPv6 type. + type: 'Enum_Type' = EnumField(length=1, namespace=Enum_Type) + #: ICMPv6 code. + code: 'int' = UInt8Field() + #: Checksum. + checksum: 'bytes' = BytesField(length=2) + #: Message body, selected by :attr:`type`. + data: 'ICMPv6Type' = SwitchField(selector=icmpv6_data_selector) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Type', code: 'int', checksum: 'bytes', + data: 'bytes | ICMPv6Type') -> 'None': ... + + +class ICMPv6Type(EnumSchema[Enum_Type]): + """Header schema for ICMPv6 type-specific message body.""" + + __default__ = lambda: Unknown + + +@schema_final +class Unknown(ICMPv6Type): + """Header schema for ICMPv6 message of unknown type.""" + + #: Message body. + data: 'bytes' = BytesField(length=lambda pkt: pkt['__length__']) + + if TYPE_CHECKING: + def __init__(self, data: 'bytes') -> 'None': ... + + +@schema_final +class DestinationUnreachable(ICMPv6Type, code=Enum_Type.Destination_Unreachable): + """Header schema for ICMPv6 Destination Unreachable message.""" + + #: Unused. + unused: 'bytes' = PaddingField(length=4) + #: As much of the invoking packet as fits the minimum IPv6 MTU. + data: 'bytes' = BytesField(length=lambda pkt: pkt['__length__']) + + if TYPE_CHECKING: + def __init__(self, unused: 'bytes', data: 'bytes') -> 'None': ... + + +@schema_final +class PacketTooBig(ICMPv6Type, code=Enum_Type.Packet_Too_Big): + """Header schema for ICMPv6 Packet Too Big message.""" + + #: MTU of the link that could not forward the invoking packet. + mtu: 'int' = UInt32Field() + #: As much of the invoking packet as fits the minimum IPv6 MTU. + data: 'bytes' = BytesField(length=lambda pkt: pkt['__length__']) + + if TYPE_CHECKING: + def __init__(self, mtu: 'int', data: 'bytes') -> 'None': ... + + +@schema_final +class TimeExceeded(ICMPv6Type, code=Enum_Type.Time_Exceeded): + """Header schema for ICMPv6 Time Exceeded message.""" + + #: Unused. + unused: 'bytes' = PaddingField(length=4) + #: As much of the invoking packet as fits the minimum IPv6 MTU. + data: 'bytes' = BytesField(length=lambda pkt: pkt['__length__']) + + if TYPE_CHECKING: + def __init__(self, unused: 'bytes', data: 'bytes') -> 'None': ... + + +@schema_final +class ParameterProblem(ICMPv6Type, code=Enum_Type.Parameter_Problem): + """Header schema for ICMPv6 Parameter Problem message.""" + + #: Pointer. + pointer: 'int' = UInt32Field() + #: As much of the invoking packet as fits the minimum IPv6 MTU. + data: 'bytes' = BytesField(length=lambda pkt: pkt['__length__']) + + if TYPE_CHECKING: + def __init__(self, pointer: 'int', data: 'bytes') -> 'None': ... + + +@schema_final +class Echo(ICMPv6Type, code=[Enum_Type.Echo_Request, Enum_Type.Echo_Reply]): + """Header schema for ICMPv6 Echo Request/Reply message.""" + + #: Identifier. + id: 'int' = UInt16Field() + #: Sequence number. + seq: 'int' = UInt16Field() + #: Data. + data: 'bytes' = BytesField(length=lambda pkt: pkt['__length__']) + + if TYPE_CHECKING: + def __init__(self, id: 'int', seq: 'int', data: 'bytes') -> 'None': ... # pylint: disable=redefined-builtin diff --git a/pcapkit/vendor/__init__.py b/pcapkit/vendor/__init__.py index f1ed19bcab..8b4a153acc 100644 --- a/pcapkit/vendor/__init__.py +++ b/pcapkit/vendor/__init__.py @@ -110,6 +110,8 @@ def __getattr__(name: 'str') -> 'typing.NoReturn': from pcapkit.vendor.ftp import * from pcapkit.vendor.hip import * from pcapkit.vendor.http import * + from pcapkit.vendor.icmp import * + from pcapkit.vendor.icmpv6 import * from pcapkit.vendor.igmp import * from pcapkit.vendor.ipv4 import * from pcapkit.vendor.ipv6 import * @@ -144,6 +146,11 @@ def __getattr__(name: 'str') -> 'typing.NoReturn': 'HIP_Suite', 'HIP_Transport', 'HIP_EdDSACurve', # HTTP 'HTTP_ErrorCode', 'HTTP_Frame', 'HTTP_Method', 'HTTP_Setting', 'HTTP_StatusCode', + # ICMP + 'ICMP_Type', 'ICMP_DestinationUnreachableCode', 'ICMP_TimeExceededCode', 'ICMP_ParameterProblemCode', + # ICMPv6 + 'ICMPv6_Type', 'ICMPv6_DestinationUnreachableCode', 'ICMPv6_TimeExceededCode', + 'ICMPv6_ParameterProblemCode', # IGMP 'IGMP_Type', # IPv4 diff --git a/pcapkit/vendor/icmp/__init__.py b/pcapkit/vendor/icmp/__init__.py new file mode 100644 index 0000000000..c29ea3b496 --- /dev/null +++ b/pcapkit/vendor/icmp/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# pylint: disable=unused-import +""":class:`~pcapkit.protocols.internet.icmpv4.ICMPv4` Vendor Crawlers +================================================================== + +.. module:: pcapkit.vendor.icmp + +This module contains all vendor crawlers of +:class:`~pcapkit.protocols.internet.icmpv4.ICMPv4` implementations. Available +enumerations include: + +.. list-table:: + + * - :class:`ICMP_Type ` + - ICMP Type Numbers [*]_ + * - :class:`ICMP_DestinationUnreachableCode ` + - ICMP Destination Unreachable Codes [*]_ + * - :class:`ICMP_TimeExceededCode ` + - ICMP Time Exceeded Codes [*]_ + * - :class:`ICMP_ParameterProblemCode ` + - ICMP Parameter Problem Codes [*]_ + +.. [*] https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-types +.. [*] https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes +.. [*] https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes +.. [*] https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes + +""" + +from pcapkit.vendor.icmp.destination_unreachable_code import \ + DestinationUnreachableCode as ICMP_DestinationUnreachableCode +from pcapkit.vendor.icmp.parameter_problem_code import \ + ParameterProblemCode as ICMP_ParameterProblemCode +from pcapkit.vendor.icmp.time_exceeded_code import TimeExceededCode as ICMP_TimeExceededCode +from pcapkit.vendor.icmp.type import Type as ICMP_Type + +__all__ = [ + 'ICMP_Type', + 'ICMP_DestinationUnreachableCode', 'ICMP_TimeExceededCode', 'ICMP_ParameterProblemCode', +] diff --git a/pcapkit/vendor/icmp/destination_unreachable_code.py b/pcapkit/vendor/icmp/destination_unreachable_code.py new file mode 100644 index 0000000000..0ed8107af5 --- /dev/null +++ b/pcapkit/vendor/icmp/destination_unreachable_code.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +"""ICMP Destination Unreachable Codes +======================================== + +.. module:: pcapkit.vendor.icmp.destination_unreachable_code + +This module contains the vendor crawler for **ICMP Destination Unreachable +Codes**, which is automatically generating +:class:`pcapkit.const.icmp.destination_unreachable_code.DestinationUnreachableCode`. + +""" + +import csv +import re +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['DestinationUnreachableCode'] + + +class DestinationUnreachableCode(Vendor): + """ICMP Destination Unreachable Codes [:rfc:`792`]""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 255' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/icmp-parameters/icmp-parameters-codes-3.csv' + + def process(self, data: 'list[str]') -> 'tuple[list[str], list[str]]': + """Process CSV data. + + Args: + data: CSV data. + + Returns: + Enumeration fields and missing fields. + + """ + reader = csv.reader(data) + next(reader) # header + + enum = [] # type: list[str] + miss = [ + "return cls._unregistered_member(value, 'Unassigned')", + ] + for item in reader: + name = item[1] + rfcs = item[2] + + temp = [] # type: list[str] + for rfc in self.split_references(rfcs): + if re.match(r'\d+', rfc): + continue + if 'RFC' in rfc and re.match(r'\d+', rfc[3:]): + temp.append(f'[:rfc:`{rfc[3:]}`]') + else: + temp.append(f'[{rfc}]'.replace('_', ' ')) + tmp1 = f" {''.join(temp)}" if rfcs else '' + desc = self.wrap_comment(re.sub(r'\r*\n', ' ', f'{name}{tmp1}', flags=re.MULTILINE)) + + try: + code, _ = item[0], int(item[0]) + renm = self.rename(name, code) + + pres = f"{renm} = {code}" + sufs = f'#: {desc}' + + enum.append(f'{sufs}\n {pres}') + except ValueError: + start, stop = item[0].split('-') + + miss.append(f'if {start} <= value <= {stop}:') + miss.append(f' #: {desc}') + miss.append(f" return cls._unregistered_member(value, '{self.safe_name(name)}')") + return enum, miss + + +if __name__ == '__main__': + sys.exit(DestinationUnreachableCode()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/icmp/parameter_problem_code.py b/pcapkit/vendor/icmp/parameter_problem_code.py new file mode 100644 index 0000000000..051a92ca8f --- /dev/null +++ b/pcapkit/vendor/icmp/parameter_problem_code.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +"""ICMP Parameter Problem Codes +================================== + +.. module:: pcapkit.vendor.icmp.parameter_problem_code + +This module contains the vendor crawler for **ICMP Parameter Problem +Codes**, which is automatically generating +:class:`pcapkit.const.icmp.parameter_problem_code.ParameterProblemCode`. + +""" + +import csv +import re +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['ParameterProblemCode'] + + +class ParameterProblemCode(Vendor): + """ICMP Parameter Problem Codes [:rfc:`792`][:rfc:`1108`]""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 255' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/icmp-parameters/icmp-parameters-codes-12.csv' + + def process(self, data: 'list[str]') -> 'tuple[list[str], list[str]]': + """Process CSV data. + + Args: + data: CSV data. + + Returns: + Enumeration fields and missing fields. + + """ + reader = csv.reader(data) + next(reader) # header + + enum = [] # type: list[str] + miss = [ + "return cls._unregistered_member(value, 'Unassigned')", + ] + for item in reader: + name = item[1] + rfcs = item[2] + + temp = [] # type: list[str] + for rfc in self.split_references(rfcs): + if re.match(r'\d+', rfc): + continue + if 'RFC' in rfc and re.match(r'\d+', rfc[3:]): + temp.append(f'[:rfc:`{rfc[3:]}`]') + else: + temp.append(f'[{rfc}]'.replace('_', ' ')) + tmp1 = f" {''.join(temp)}" if rfcs else '' + desc = self.wrap_comment(re.sub(r'\r*\n', ' ', f'{name}{tmp1}', flags=re.MULTILINE)) + + try: + code, _ = item[0], int(item[0]) + renm = self.rename(name, code) + + pres = f"{renm} = {code}" + sufs = f'#: {desc}' + + enum.append(f'{sufs}\n {pres}') + except ValueError: + start, stop = item[0].split('-') + + miss.append(f'if {start} <= value <= {stop}:') + miss.append(f' #: {desc}') + miss.append(f" return cls._unregistered_member(value, '{self.safe_name(name)}')") + return enum, miss + + +if __name__ == '__main__': + sys.exit(ParameterProblemCode()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/icmp/time_exceeded_code.py b/pcapkit/vendor/icmp/time_exceeded_code.py new file mode 100644 index 0000000000..3aec1c08d2 --- /dev/null +++ b/pcapkit/vendor/icmp/time_exceeded_code.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +"""ICMP Time Exceeded Codes +============================== + +.. module:: pcapkit.vendor.icmp.time_exceeded_code + +This module contains the vendor crawler for **ICMP Time Exceeded Codes**, +which is automatically generating +:class:`pcapkit.const.icmp.time_exceeded_code.TimeExceededCode`. + +""" + +import csv +import re +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['TimeExceededCode'] + + +class TimeExceededCode(Vendor): + """ICMP Time Exceeded Codes [:rfc:`792`]""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 255' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/icmp-parameters/icmp-parameters-codes-11.csv' + + def process(self, data: 'list[str]') -> 'tuple[list[str], list[str]]': + """Process CSV data. + + Args: + data: CSV data. + + Returns: + Enumeration fields and missing fields. + + """ + reader = csv.reader(data) + next(reader) # header + + enum = [] # type: list[str] + miss = [ + "return cls._unregistered_member(value, 'Unassigned')", + ] + for item in reader: + name = item[1] + rfcs = item[2] + + temp = [] # type: list[str] + for rfc in self.split_references(rfcs): + if re.match(r'\d+', rfc): + continue + if 'RFC' in rfc and re.match(r'\d+', rfc[3:]): + temp.append(f'[:rfc:`{rfc[3:]}`]') + else: + temp.append(f'[{rfc}]'.replace('_', ' ')) + tmp1 = f" {''.join(temp)}" if rfcs else '' + desc = self.wrap_comment(re.sub(r'\r*\n', ' ', f'{name}{tmp1}', flags=re.MULTILINE)) + + try: + code, _ = item[0], int(item[0]) + renm = self.rename(name, code) + + pres = f"{renm} = {code}" + sufs = f'#: {desc}' + + enum.append(f'{sufs}\n {pres}') + except ValueError: + start, stop = item[0].split('-') + + miss.append(f'if {start} <= value <= {stop}:') + miss.append(f' #: {desc}') + miss.append(f" return cls._unregistered_member(value, '{self.safe_name(name)}')") + return enum, miss + + +if __name__ == '__main__': + sys.exit(TimeExceededCode()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/icmp/type.py b/pcapkit/vendor/icmp/type.py new file mode 100644 index 0000000000..62bcddb2c6 --- /dev/null +++ b/pcapkit/vendor/icmp/type.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +"""ICMP Type Numbers +====================== + +.. module:: pcapkit.vendor.icmp.type + +This module contains the vendor crawler for **ICMP Type Numbers**, +which is automatically generating :class:`pcapkit.const.icmp.type.Type`. + +""" + +import csv +import re +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['Type'] + + +class Type(Vendor): + """ICMP Type Numbers [:rfc:`792`]""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 255' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/icmp-parameters/icmp-parameters-types.csv' + + def process(self, data: 'list[str]') -> 'tuple[list[str], list[str]]': + """Process CSV data. + + Args: + data: CSV data. + + Returns: + Enumeration fields and missing fields. + + """ + reader = csv.reader(data) + next(reader) # header + + enum = [] # type: list[str] + miss = [] # type: list[str] + for item in reader: + name = item[1] + rfcs = item[2] + + temp = [] # type: list[str] + for rfc in self.split_references(rfcs): + if re.match(r'\d+', rfc): + continue + if 'RFC' in rfc and re.match(r'\d+', rfc[3:]): + temp.append(f'[:rfc:`{rfc[3:]}`]') + else: + temp.append(f'[{rfc}]'.replace('_', ' ')) + tmp1 = f" {''.join(temp)}" if rfcs else '' + desc = self.wrap_comment(re.sub(r'\r*\n', ' ', f'{name}{tmp1}', flags=re.MULTILINE)) + + try: + code, _ = item[0], int(item[0]) + renm = self.rename(name, code) + + pres = f"{renm} = {code}" + sufs = f'#: {desc}' + + enum.append(f'{sufs}\n {pres}') + except ValueError: + start, stop = item[0].split('-') + + miss.append(f'if {start} <= value <= {stop}:') + miss.append(f' #: {desc}') + miss.append(f" return cls._unregistered_member(value, '{self.safe_name(name)}')") + miss.append("return cls._unregistered_member(value, 'Unassigned')") + return enum, miss + + +if __name__ == '__main__': + sys.exit(Type()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/icmpv6/__init__.py b/pcapkit/vendor/icmpv6/__init__.py new file mode 100644 index 0000000000..9e4d689bb2 --- /dev/null +++ b/pcapkit/vendor/icmpv6/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# pylint: disable=unused-import +""":class:`~pcapkit.protocols.internet.icmpv6.ICMPv6` Vendor Crawlers +======================================================================== + +.. module:: pcapkit.vendor.icmpv6 + +This module contains all vendor crawlers of +:class:`~pcapkit.protocols.internet.icmpv6.ICMPv6` implementations. Available +enumerations include: + +.. list-table:: + + * - :class:`ICMPv6_Type ` + - ICMPv6 Type Numbers [*]_ + * - :class:`ICMPv6_DestinationUnreachableCode ` + - ICMPv6 Destination Unreachable Codes [*]_ + * - :class:`ICMPv6_TimeExceededCode ` + - ICMPv6 Time Exceeded Codes [*]_ + * - :class:`ICMPv6_ParameterProblemCode ` + - ICMPv6 Parameter Problem Codes [*]_ + +.. [*] https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml#icmpv6-parameters-types +.. [*] https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml#icmpv6-parameters-type-1 +.. [*] https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml#icmpv6-parameters-type-3 +.. [*] https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml#icmpv6-parameters-type-4 + +""" + +from pcapkit.vendor.icmpv6.destination_unreachable_code import \ + DestinationUnreachableCode as ICMPv6_DestinationUnreachableCode +from pcapkit.vendor.icmpv6.parameter_problem_code import \ + ParameterProblemCode as ICMPv6_ParameterProblemCode +from pcapkit.vendor.icmpv6.time_exceeded_code import TimeExceededCode as ICMPv6_TimeExceededCode +from pcapkit.vendor.icmpv6.type import Type as ICMPv6_Type + +__all__ = [ + 'ICMPv6_Type', + 'ICMPv6_DestinationUnreachableCode', 'ICMPv6_TimeExceededCode', 'ICMPv6_ParameterProblemCode', +] diff --git a/pcapkit/vendor/icmpv6/destination_unreachable_code.py b/pcapkit/vendor/icmpv6/destination_unreachable_code.py new file mode 100644 index 0000000000..98fa126449 --- /dev/null +++ b/pcapkit/vendor/icmpv6/destination_unreachable_code.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +"""ICMPv6 Destination Unreachable Codes +========================================== + +.. module:: pcapkit.vendor.icmpv6.destination_unreachable_code + +This module contains the vendor crawler for **ICMPv6 Destination +Unreachable Codes**, which is automatically generating +:class:`pcapkit.const.icmpv6.destination_unreachable_code.DestinationUnreachableCode`. + +""" + +import csv +import re +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['DestinationUnreachableCode'] + + +class DestinationUnreachableCode(Vendor): + """ICMPv6 Destination Unreachable Codes [:rfc:`4443`]""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 255' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters-codes-2.csv' + + def process(self, data: 'list[str]') -> 'tuple[list[str], list[str]]': + """Process CSV data. + + Args: + data: CSV data. + + Returns: + Enumeration fields and missing fields. + + """ + reader = csv.reader(data) + next(reader) # header + + enum = [] # type: list[str] + miss = [ + "return cls._unregistered_member(value, 'Unassigned')", + ] + for item in reader: + name = item[1] + rfcs = item[2] + + temp = [] # type: list[str] + for rfc in self.split_references(rfcs): + if re.match(r'\d+', rfc): + continue + if 'RFC' in rfc and re.match(r'\d+', rfc[3:]): + temp.append(f'[:rfc:`{rfc[3:]}`]') + else: + temp.append(f'[{rfc}]'.replace('_', ' ')) + tmp1 = f" {''.join(temp)}" if rfcs else '' + desc = self.wrap_comment(re.sub(r'\r*\n', ' ', f'{name}{tmp1}', flags=re.MULTILINE)) + + try: + code, _ = item[0], int(item[0]) + renm = self.rename(name, code) + + pres = f"{renm} = {code}" + sufs = f'#: {desc}' + + enum.append(f'{sufs}\n {pres}') + except ValueError: + start, stop = item[0].split('-') + + miss.append(f'if {start} <= value <= {stop}:') + miss.append(f' #: {desc}') + miss.append(f" return cls._unregistered_member(value, '{self.safe_name(name)}')") + return enum, miss + + +if __name__ == '__main__': + sys.exit(DestinationUnreachableCode()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/icmpv6/parameter_problem_code.py b/pcapkit/vendor/icmpv6/parameter_problem_code.py new file mode 100644 index 0000000000..73439debd5 --- /dev/null +++ b/pcapkit/vendor/icmpv6/parameter_problem_code.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +"""ICMPv6 Parameter Problem Codes +==================================== + +.. module:: pcapkit.vendor.icmpv6.parameter_problem_code + +This module contains the vendor crawler for **ICMPv6 Parameter Problem +Codes**, which is automatically generating +:class:`pcapkit.const.icmpv6.parameter_problem_code.ParameterProblemCode`. + +""" + +import csv +import re +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['ParameterProblemCode'] + + +class ParameterProblemCode(Vendor): + """ICMPv6 Parameter Problem Codes [:rfc:`4443`][:rfc:`7112`]""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 255' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters-codes-5.csv' + + def process(self, data: 'list[str]') -> 'tuple[list[str], list[str]]': + """Process CSV data. + + Args: + data: CSV data. + + Returns: + Enumeration fields and missing fields. + + """ + reader = csv.reader(data) + next(reader) # header + + enum = [] # type: list[str] + miss = [ + "return cls._unregistered_member(value, 'Unassigned')", + ] + for item in reader: + name = item[1] + rfcs = item[2] + + temp = [] # type: list[str] + for rfc in self.split_references(rfcs): + if re.match(r'\d+', rfc): + continue + if 'RFC' in rfc and re.match(r'\d+', rfc[3:]): + temp.append(f'[:rfc:`{rfc[3:]}`]') + else: + temp.append(f'[{rfc}]'.replace('_', ' ')) + tmp1 = f" {''.join(temp)}" if rfcs else '' + desc = self.wrap_comment(re.sub(r'\r*\n', ' ', f'{name}{tmp1}', flags=re.MULTILINE)) + + try: + code, _ = item[0], int(item[0]) + renm = self.rename(name, code) + + pres = f"{renm} = {code}" + sufs = f'#: {desc}' + + enum.append(f'{sufs}\n {pres}') + except ValueError: + start, stop = item[0].split('-') + + miss.append(f'if {start} <= value <= {stop}:') + miss.append(f' #: {desc}') + miss.append(f" return cls._unregistered_member(value, '{self.safe_name(name)}')") + return enum, miss + + +if __name__ == '__main__': + sys.exit(ParameterProblemCode()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/icmpv6/time_exceeded_code.py b/pcapkit/vendor/icmpv6/time_exceeded_code.py new file mode 100644 index 0000000000..2d368fc1f3 --- /dev/null +++ b/pcapkit/vendor/icmpv6/time_exceeded_code.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +"""ICMPv6 Time Exceeded Codes +================================ + +.. module:: pcapkit.vendor.icmpv6.time_exceeded_code + +This module contains the vendor crawler for **ICMPv6 Time Exceeded +Codes**, which is automatically generating +:class:`pcapkit.const.icmpv6.time_exceeded_code.TimeExceededCode`. + +""" + +import csv +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['TimeExceededCode'] + + +class TimeExceededCode(Vendor): + """ICMPv6 Time Exceeded Codes [:rfc:`4443`]""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 255' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters-codes-4.csv' + + def process(self, data: 'list[str]') -> 'tuple[list[str], list[str]]': + """Process CSV data. + + Unlike the other ICMP/ICMPv6 code registries, this one's IANA CSV + export carries no ``Reference`` column -- just ``Code,Name`` -- so + this override skips the RFC-citation handling the others do instead + of indexing a column that is not there. + + Args: + data: CSV data. + + Returns: + Enumeration fields and missing fields. + + """ + reader = csv.reader(data) + next(reader) # header + + enum = [] # type: list[str] + miss = [ + "return cls._unregistered_member(value, 'Unassigned')", + ] + for item in reader: + name = item[1] + desc = self.wrap_comment(name) + + try: + code, _ = item[0], int(item[0]) + renm = self.rename(name, code) + + pres = f"{renm} = {code}" + sufs = f'#: {desc}' + + enum.append(f'{sufs}\n {pres}') + except ValueError: + start, stop = item[0].split('-') + + miss.append(f'if {start} <= value <= {stop}:') + miss.append(f' #: {desc}') + miss.append(f" return cls._unregistered_member(value, '{self.safe_name(name)}')") + return enum, miss + + +if __name__ == '__main__': + sys.exit(TimeExceededCode()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/icmpv6/type.py b/pcapkit/vendor/icmpv6/type.py new file mode 100644 index 0000000000..263a585b4c --- /dev/null +++ b/pcapkit/vendor/icmpv6/type.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +"""ICMPv6 Type Numbers +========================= + +.. module:: pcapkit.vendor.icmpv6.type + +This module contains the vendor crawler for **ICMPv6 Type Numbers**, +which is automatically generating :class:`pcapkit.const.icmpv6.type.Type`. + +""" + +import csv +import re +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['Type'] + + +class Type(Vendor): + """ICMPv6 Type Numbers [:rfc:`4443`]""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 255' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters-2.csv' + + def process(self, data: 'list[str]') -> 'tuple[list[str], list[str]]': + """Process CSV data. + + Args: + data: CSV data. + + Returns: + Enumeration fields and missing fields. + + """ + reader = csv.reader(data) + next(reader) # header + + enum = [] # type: list[str] + miss = [] # type: list[str] + for item in reader: + name = item[1] + rfcs = item[2] + + temp = [] # type: list[str] + for rfc in self.split_references(rfcs): + if re.match(r'\d+', rfc): + continue + if 'RFC' in rfc and re.match(r'\d+', rfc[3:]): + temp.append(f'[:rfc:`{rfc[3:]}`]') + else: + temp.append(f'[{rfc}]'.replace('_', ' ')) + tmp1 = f" {''.join(temp)}" if rfcs else '' + desc = self.wrap_comment(re.sub(r'\r*\n', ' ', f'{name}{tmp1}', flags=re.MULTILINE)) + + try: + code, _ = item[0], int(item[0]) + renm = self.rename(name, code) + + pres = f"{renm} = {code}" + sufs = f'#: {desc}' + + enum.append(f'{sufs}\n {pres}') + except ValueError: + start, stop = item[0].split('-') + + miss.append(f'if {start} <= value <= {stop}:') + miss.append(f' #: {desc}') + miss.append(f" return cls._unregistered_member(value, '{self.safe_name(name)}')") + miss.append("return cls._unregistered_member(value, 'Unassigned')") + return enum, miss + + +if __name__ == '__main__': + sys.exit(Type()) # type: ignore[arg-type] diff --git a/tests/const/test_const_enum_builtin_parity.py b/tests/const/test_const_enum_builtin_parity.py index 912ed8a9a5..41f2f01e56 100644 --- a/tests/const/test_const_enum_builtin_parity.py +++ b/tests/const/test_const_enum_builtin_parity.py @@ -377,11 +377,11 @@ def test_the_sweep_size_is_pinned(self) -> None: # tests.const.test_const_enum_get's narrower (IntEnum, IntFlag)-only # sweep. The int count includes GitHub issue #880's ngap.ProcedureCode # and ngap.ProtocolIE, which are on EnumRegistry. - self.assertEqual(len(ints), 118) + self.assertEqual(len(ints), 124) self.assertEqual(len(flags), 6) self.assertEqual(len(strs), 10) - self.assertEqual(len(self.enums), 134) - self.assertEqual(len({obj.__module__ for obj in self.enums}), 128) + self.assertEqual(len(self.enums), 140) + self.assertEqual(len({obj.__module__ for obj in self.enums}), 134) def test_every_registry_rejects_a_negative_value(self) -> None: """The registry-wide form of GitHub issue #647. diff --git a/tests/const/test_const_enum_get.py b/tests/const/test_const_enum_get.py index e274b547c2..628ca4e1ce 100644 --- a/tests/const/test_const_enum_get.py +++ b/tests/const/test_const_enum_get.py @@ -297,7 +297,7 @@ def test_the_sweep_size_is_pinned(self) -> None: # pcapkit.protocols.application.ngap; neither needs an exception-set # entry, since both take the base's own get(key, default). names = {_qualname(obj) for obj in self.enums} - self.assertEqual(len(self.enums), 124) + self.assertEqual(len(self.enums), 130) for expected in (EXPECTED_TO_RESOLVE_ANYTHING, EXPECTED_WITHOUT_AN_INTEGER_DEFAULT, EXPECTED_WITHOUT_A_CACHEABLE_FALLBACK): self.assertTrue(expected.issubset(names), @@ -337,7 +337,7 @@ def test_every_integer_path_consults_the_default(self) -> None: # registry excused above: ``EXPECTED_TO_RESOLVE_ANYTHING`` and # ``EXPECTED_WITHOUT_A_CACHEABLE_FALLBACK``, with # ``EXPECTED_WITHOUT_AN_INTEGER_DEFAULT`` empty. - self.assertEqual(covered, 121) + self.assertEqual(covered, 127) def test_the_always_resolving_registries_have_nothing_to_fall_back_to(self) -> None: """The two registries excused from the sweep, and why. diff --git a/tests/const/test_const_enum_lookup.py b/tests/const/test_const_enum_lookup.py index d9b3e732b3..daa84e6477 100644 --- a/tests/const/test_const_enum_lookup.py +++ b/tests/const/test_const_enum_lookup.py @@ -200,7 +200,7 @@ def test_every_registry_enum_was_discovered(self) -> None: # ``_iter_const_int_flags``'s, and so are GitHub issue #880's # ngap.ProcedureCode and ngap.ProtocolIE, which are on EnumRegistry. names = {f'{obj.__module__}.{obj.__qualname__}' for obj in self.enums} - self.assertEqual(len(self.enums), 118) + self.assertEqual(len(self.enums), 124) self.assertTrue(EXPECTED_TO_REJECT_ZERO.issubset(names), f'expected reject-list entries missing from the sweep: ' f'{EXPECTED_TO_REJECT_ZERO - names}') diff --git a/tests/const/test_const_registry_protocol.py b/tests/const/test_const_registry_protocol.py index 9acc1ecf0b..aca0eafaae 100644 --- a/tests/const/test_const_registry_protocol.py +++ b/tests/const/test_const_registry_protocol.py @@ -846,8 +846,8 @@ def test_every_generated_const_module_is_accounted_for(self) -> None: generated = [path for path in all_files if path not in converted_relpaths and path not in EXCLUDED_FROM_INTENUM_CENSUS] - self.assertEqual(len(all_files), 128) - self.assertEqual(len(generated), 111) + self.assertEqual(len(all_files), 134) + self.assertEqual(len(generated), 117) for relpath in generated: with self.subTest(module=relpath): diff --git a/tests/foundation/registry/test_icmp_type_registrar_unit.py b/tests/foundation/registry/test_icmp_type_registrar_unit.py new file mode 100644 index 0000000000..761731f521 --- /dev/null +++ b/tests/foundation/registry/test_icmp_type_registrar_unit.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +"""``register_icmp_type`` routes each ICMP type to its own family's registry. + +The wrapper takes the family from the enum its code is a member of, validates +the code against that enum, and writes through the family class's own +``register_type``. Each test restores the shipped entry it overrides. + +""" + +import unittest + +from tests._support import reimport_once_per_class + +#: Test tier (#1958): a handful of hand-picked routing and rejection cases. +__tier__ = 'ALL' + + +class RegisterICMPTypeTests(unittest.TestCase): + """Routing and validation of :func:`register_icmp_type`.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def test_icmpv4_code_writes_icmpv4_registry_only(self) -> None: + """An ICMPv4 type lands in ``ICMPv4.__type__`` and nowhere else.""" + from pcapkit.const.icmp.type import Type + from pcapkit.foundation.registry.protocols import register_icmp_type + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + + code = Type.Timestamp + original = ICMPv4.__type__.get(code) + try: + register_icmp_type(code, 'none') + self.assertEqual(ICMPv4.__type__[code], 'none') + self.assertIsNone(ICMPv6.__type__.get(code)) + finally: + if original is None: + ICMPv4.__type__.pop(code, None) + else: + ICMPv4.__type__[code] = original + + def test_icmpv6_code_writes_icmpv6_registry_only(self) -> None: + """An ICMPv6 type lands in ``ICMPv6.__type__`` and nowhere else.""" + from pcapkit.const.icmpv6.type import Type + from pcapkit.foundation.registry.protocols import register_icmp_type + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + + code = Type.Router_Renumbering + original = ICMPv6.__type__.get(code) + try: + register_icmp_type(code, 'none') + self.assertEqual(ICMPv6.__type__[code], 'none') + self.assertIsNone(ICMPv4.__type__.get(code)) + finally: + if original is None: + ICMPv6.__type__.pop(code, None) + else: + ICMPv6.__type__[code] = original + + def test_rejects_a_code_that_is_not_an_icmp_type(self) -> None: + """A bare int, or a member of another enum, leaves both registries untouched.""" + from pcapkit.const.reg.transtype import TransType + from pcapkit.foundation.registry.protocols import register_icmp_type + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + from pcapkit.utilities.exceptions import RegistryError + + before = (dict(ICMPv4.__type__), dict(ICMPv6.__type__)) + with self.assertRaises(RegistryError): + register_icmp_type(8, 'none') # type: ignore[arg-type] + with self.assertRaises(RegistryError): + register_icmp_type(TransType.ICMP, 'none') # type: ignore[arg-type] + self.assertEqual(before, (dict(ICMPv4.__type__), dict(ICMPv6.__type__))) + + def test_rejects_a_method_name_with_no_parser(self) -> None: + """A name with no ``_read_data_type_*`` method is refused before any write.""" + from pcapkit.const.icmp.type import Type + from pcapkit.foundation.registry.protocols import register_icmp_type + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + from pcapkit.utilities.exceptions import RegistryError + + code = Type.Timestamp + original = ICMPv4.__type__.get(code) + with self.assertRaises(RegistryError): + register_icmp_type(code, 'no_such_kind') + self.assertEqual(ICMPv4.__type__.get(code), original) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/foundation/registry/test_registry_symmetry_unit.py b/tests/foundation/registry/test_registry_symmetry_unit.py index 77094e5dcd..35b5ade292 100644 --- a/tests/foundation/registry/test_registry_symmetry_unit.py +++ b/tests/foundation/registry/test_registry_symmetry_unit.py @@ -432,6 +432,7 @@ def test_every_public_registrar_is_covered_or_excused(self) -> None: 'register_reassembly_ipv6_callback': 'append-only, nothing to undo with', 'register_reassembly_tcp_callback': 'append-only, nothing to undo with', 'register_traceflow_tcp_callback': 'append-only, nothing to undo with', + 'register_icmp_type': 'no schema argument; override and restore are in test_icmp_type_registrar_unit', } self.assertEqual(sorted(set(registry.__all__) - covered - set(excused)), []) self.assertEqual(sorted((covered | set(excused)) - set(registry.__all__)), []) diff --git a/tests/integration/test_cli_subprocess.py b/tests/integration/test_cli_subprocess.py index b65aebc392..d106c88f73 100644 --- a/tests/integration/test_cli_subprocess.py +++ b/tests/integration/test_cli_subprocess.py @@ -96,7 +96,7 @@ def test_json_report_is_written_and_every_chain_is_printed(self) -> None: ]) self.assertIn(sample_path('in.pcap'), completed.stdout) - self.assertIn('Frame 1: Ethernet:IPv6:IPv6_ICMP', completed.stdout) + self.assertIn('Frame 1: Ethernet:IPv6:ICMPv6', completed.stdout) self.assertIn('Frame 6: Ethernet:IPv4:UDP:Raw', completed.stdout) # ``-o report`` is relative, and the run happens in the temporary # directory, so the name the tool reports is relative too. diff --git a/tests/integration/test_engine_runtime.py b/tests/integration/test_engine_runtime.py index 8e01fcb59d..c2b425b80a 100644 --- a/tests/integration/test_engine_runtime.py +++ b/tests/integration/test_engine_runtime.py @@ -31,7 +31,7 @@ def test_default_engine_exposes_native_frame_objects(self) -> None: self.assertEqual(extractor.length, 6) self.assertEqual(type(extractor.frame[0]).__name__, 'Frame') - self.assertEqual(str(extractor.frame[0].protochain), 'Ethernet:IPv6:IPv6_ICMP') + self.assertEqual(str(extractor.frame[0].protochain), 'Ethernet:IPv6:ICMPv6') @unittest.skipUnless(HAS_DPKT, 'dpkt not installed') def test_dpkt_engine_returns_dpkt_packets_and_toolkit_chain(self) -> None: @@ -53,7 +53,7 @@ def test_scapy_engine_returns_scapy_packets(self) -> None: # undissected, as ``Raw``. ``Ether`` is what the capture actually holds: frame 1 of # in.pcap is an Ethernet frame carrying an ICMPv6 neighbour solicitation over # IPv6, which is exactly what the two tests above independently report for the - # same frame -- 'Ethernet:IPv6:IPv6_ICMP' from the default engine and + # same frame -- 'Ethernet:IPv6:ICMPv6' from the default engine and # 'Ethernet:IP6:ICMP6' from DPKT. The three chains are three libraries' # spellings of one frame, so scapy agreeing here is parity, not a new claim. from pcapkit.interface import extract diff --git a/tests/integration/test_output_formats.py b/tests/integration/test_output_formats.py index e90ef19907..146c6ef4a8 100644 --- a/tests/integration/test_output_formats.py +++ b/tests/integration/test_output_formats.py @@ -24,7 +24,7 @@ #: Sections a report of :file:`in.pcap` has: the global header and six frames. IN_PCAP_SECTIONS = ['Global Header', 'Frame 1', 'Frame 2', 'Frame 3', 'Frame 4', 'Frame 5', 'Frame 6'] #: Protocol chain of the first frame of :file:`in.pcap`. -IN_PCAP_FIRST_CHAIN = 'Ethernet:IPv6:IPv6_ICMP' +IN_PCAP_FIRST_CHAIN = 'Ethernet:IPv6:ICMPv6' @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') diff --git a/tests/protocols/internet/test_icmp_selector_runtime.py b/tests/protocols/internet/test_icmp_selector_runtime.py new file mode 100644 index 0000000000..07b8d3c198 --- /dev/null +++ b/tests/protocols/internet/test_icmp_selector_runtime.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +"""Protocol selectors tell ICMPv4 and ICMPv6 apart on parsed frames. + +``'ICMP'`` names the abstract :class:`~pcapkit.protocols.internet.icmp.ICMP` +and so selects both versions; ``'ICMPv4'`` and ``'ICMPv6'`` select one each. +Frame 1 of ``little_endian.pcap`` is an ICMPv4 echo request, and frame 1 of +``in.pcap`` an ICMPv6 Neighbor Solicitation. + +""" + +from __future__ import annotations + +import importlib.util +import unittest +import warnings +from typing import TYPE_CHECKING + +from tests._support import close_extractor, reimport_once_per_class, sample_path + +if TYPE_CHECKING: + from typing import Any, Iterator + +RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') +HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) + +#: Capture holding each version in its first frame. +SAMPLES = {'ICMPv4': 'little_endian.pcap', 'ICMPv6': 'in.pcap'} + +#: Which version each selector matches. +MATCHES = { + 'ICMP': {'ICMPv4', 'ICMPv6'}, + 'ICMPv4': {'ICMPv4'}, + 'ICMPv6': {'ICMPv6'}, +} + + +@unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') +class ICMPSelectorRuntimeTests(unittest.TestCase): + """Pin ``in``, ``[]``, ``==``, the protochain and ``extract(protocol=)``.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def _frame(self, version: str, **kwargs: 'Any') -> 'Any': + from pcapkit.interface import extract + + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + extractor = extract(fin=sample_path(SAMPLES[version]), store=True, nofile=True, + engine='default', **kwargs) + self.addCleanup(close_extractor, extractor) + return extractor.frame[0] + + def _selectors(self) -> 'Iterator[tuple[str, Any, set[str]]]': + import pcapkit + + for name in MATCHES: + yield name, name, MATCHES[name] + yield f'{name} class', getattr(pcapkit, name), MATCHES[name] + + def test_frame_selectors(self) -> None: + from pcapkit.protocols.misc.null import NoPayload + from pcapkit.utilities.exceptions import ProtocolNotFound + + for version in SAMPLES: + frame = self._frame(version) + layer = frame.payload.payload.payload + self.assertEqual(type(layer).__name__, version) + self.assertIsInstance(layer.payload, NoPayload) + + for label, key, expected in self._selectors(): + match = version in expected + with self.subTest(frame=version, selector=label): + self.assertEqual(key in frame, match) + self.assertEqual(key in frame.protochain, match) + if match: + self.assertIs(frame[key], layer) + else: + with self.assertRaises(ProtocolNotFound): + frame[key] # pylint: disable=pointless-statement + + def test_layer_equality(self) -> None: + """``==`` against a name compares :meth:`~pcapkit.protocols.protocol.Protocol.id`.""" + v4 = self._frame('ICMPv4').payload.payload.payload + v6 = self._frame('ICMPv6').payload.payload.payload + + self.assertTrue(v4 == 'ICMP') + self.assertFalse(v4 == 'ICMPv6') + self.assertTrue(v6 == 'ICMPv6') + self.assertFalse(v6 == 'ICMP') + + def test_class_name_matches_by_in_not_by_equality(self) -> None: + """``==`` skips the class name, which is not in ``id()``; ``in`` resolves it.""" + v4 = self._frame('ICMPv4').payload.payload.payload + + self.assertFalse(v4 == 'ICMPv4') + self.assertIn('ICMPv4', v4) + + def test_extract_protocol_threshold(self) -> None: + for version in SAMPLES: + for name, expected in MATCHES.items(): + with self.subTest(frame=version, protocol=name): + layer = self._frame(version, protocol=name).payload.payload.payload + self.assertEqual(layer._sigterm, version in expected) # pylint: disable=protected-access + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/internet/test_icmpv4_roundtrip_unit.py b/tests/protocols/internet/test_icmpv4_roundtrip_unit.py new file mode 100644 index 0000000000..46cbf94f52 --- /dev/null +++ b/tests/protocols/internet/test_icmpv4_roundtrip_unit.py @@ -0,0 +1,268 @@ +# -*- coding: utf-8 -*- +"""ICMP (RFC 792) messages rebuild byte for byte through ``from_data``. + +Every case builds its own octets in memory and reads no capture. Classes are +imported inside each test, after +:func:`~tests._support.reimport_once_per_class`, not at module load, so that +they belong to the :mod:`pcapkit` import that is live when the test runs. + +""" + +import io +import struct +import unittest +import warnings + +from tests._support import reimport_once_per_class + +#: Test tier (#1958): hand-picked round-trip cases, not a sweep. +__tier__ = 'ALL' + +#: Internet header plus the first eight octets of the original datagram's +#: payload, as the Destination Unreachable / Time Exceeded / Parameter +#: Problem bodies quote it. Not a real IP header -- just 20 distinct octets, +#: so a byte-for-byte mismatch is easy to spot. +QUOTED = bytes(range(20)) + +#: Each entry is ``type || code || checksum`` followed by that type's own +#: body, built by hand against :rfc:`792`. +ICMP_PACKETS = { + 'echo-request': bytes.fromhex('0800' '1234' '0001' '0002') + b'abcd', + 'echo-reply': bytes.fromhex('0000' '4321' '0003' '0004') + b'wxyz', + 'destination-unreachable': bytes.fromhex('0301' 'abcd' 'deadbeef') + QUOTED, + 'time-exceeded': bytes.fromhex('0b01' 'abcd' '11223344') + QUOTED, + 'parameter-problem': bytes.fromhex('0c02' 'abcd' '05' 'f0f0f0') + QUOTED, + 'unknown-type': bytes.fromhex('2a00' '0000') + b'extended-echo-body', + 'empty-body': bytes.fromhex('0800' '0000' '0000' '0000'), +} + + +class ICMPv4RoundTripTests(unittest.TestCase): + """Pin the byte-for-byte rebuild of every ICMP message type covered.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def test_from_data_rebuilds_byte_for_byte(self) -> None: + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + + for name, packet in ICMP_PACKETS.items(): + with self.subTest(packet=name), warnings.catch_warnings(): + warnings.simplefilter('ignore') + parsed = ICMPv4(io.BytesIO(packet), len(packet)) + self.assertEqual(ICMPv4.from_data(parsed.info).data, packet, + f'{name} did not rebuild byte for byte') + + def test_echo_request_fields_are_parsed(self) -> None: + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + + packet = ICMP_PACKETS['echo-request'] + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + parsed = ICMPv4(io.BytesIO(packet), len(packet)) + + self.assertEqual(parsed.info.id, 1) + self.assertEqual(parsed.info.seq, 2) + self.assertEqual(parsed.info.data, b'abcd') + self.assertEqual(parsed.info.checksum, b'\x124') + + def test_destination_unreachable_fields_are_parsed(self) -> None: + from pcapkit.const.icmp.destination_unreachable_code import DestinationUnreachableCode + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + + packet = ICMP_PACKETS['destination-unreachable'] + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + parsed = ICMPv4(io.BytesIO(packet), len(packet)) + + self.assertEqual(parsed.info.code, DestinationUnreachableCode.Host_Unreachable) + self.assertEqual(parsed.info.unused, bytes.fromhex('deadbeef')) + self.assertEqual(parsed.info.data, QUOTED) + + def test_parameter_problem_pointer_is_parsed(self) -> None: + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + + packet = ICMP_PACKETS['parameter-problem'] + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + parsed = ICMPv4(io.BytesIO(packet), len(packet)) + + self.assertEqual(parsed.info.pointer, 5) + self.assertEqual(parsed.info.unused, bytes.fromhex('f0f0f0')) + self.assertEqual(parsed.info.data, QUOTED) + + def test_unknown_type_keeps_raw_body(self) -> None: + from pcapkit.protocols.data.internet.icmpv4 import Unknown + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + + packet = ICMP_PACKETS['unknown-type'] + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + parsed = ICMPv4(io.BytesIO(packet), len(packet)) + + self.assertIsInstance(parsed.info, Unknown) + self.assertEqual(parsed.info.data, b'extended-echo-body') + + def test_make_builds_echo_from_scratch(self) -> None: + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + + proto = object.__new__(ICMPv4) + schema = proto.make(type=8, code=0, checksum=b'\x124', + body={'id': 1, 'seq': 2, 'data': b'abcd'}) + self.assertEqual(schema.pack(), ICMP_PACKETS['echo-request']) + + def test_make_accepts_raw_bytes_body(self) -> None: + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + + proto = object.__new__(ICMPv4) + body = struct.pack('>HH', 1, 2) + b'abcd' + schema = proto.make(type=8, code=0, checksum=b'\x124', body=body) + self.assertEqual(schema.pack(), ICMP_PACKETS['echo-request']) + + def test_construct_with_no_arguments(self) -> None: + """``ICMPv4()`` builds a default Echo Request rather than raising.""" + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + + proto = ICMPv4() + # Type 8 (Echo), code 0, an uncomputed (zero) RFC 792 checksum over + # an all-zero 4-octet id/seq body, then that same body: this class + # does not compute or verify the checksum (#2470). + self.assertEqual(bytes(proto), bytes.fromhex('0800000000000000')) + + def test_dispatches_from_ipv4(self) -> None: + """IPv4's protocol-number dispatch reaches :class:`ICMPv4`.""" + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + from pcapkit.protocols.internet.ipv4 import IPv4 + + icmp_body = ICMP_PACKETS['echo-request'] + header = bytearray(bytes.fromhex( + '45' '00' '0000' '0000' '0000' '40' '01' '0000' + '7f000001' '7f000001' + )) + header[2:4] = struct.pack('>H', 20 + len(icmp_body)) + packet = bytes(header) + icmp_body + + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + parsed = IPv4(io.BytesIO(packet), len(packet)) + + self.assertIsInstance(parsed.payload, ICMPv4) + self.assertEqual(parsed.payload.info.id, 1) + + def test_id_index_and_length_hint(self) -> None: + from pcapkit.const.reg.transtype import TransType + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + + self.assertEqual(ICMPv4.id(), ('ICMP',)) + self.assertEqual(ICMPv4.__index__(), TransType.ICMP) + self.assertEqual(object.__new__(ICMPv4).__length_hint__(), 4) + + def test_register_type_warns_on_override(self) -> None: + from pcapkit.const.icmp.type import Type + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + from pcapkit.utilities.warnings import RegistryWarning + + original = ICMPv4.__type__[Type.Echo] + try: + with self.assertWarns(RegistryWarning): + ICMPv4.register_type(Type.Echo, 'none') + self.assertEqual(ICMPv4.__type__[Type.Echo], 'none') + + # Re-registering the same entry is a silent no-op. + with warnings.catch_warnings(): + warnings.simplefilter('error', RegistryWarning) + ICMPv4.register_type(Type.Echo, 'none') + finally: + ICMPv4.__type__[Type.Echo] = original + + def test_name_property(self) -> None: + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + + self.assertEqual(object.__new__(ICMPv4).name, 'Internet Control Message Protocol') + + def test_make_rejects_bytes_body_that_fails_to_parse(self) -> None: + """Parameter Problem's body needs at least four octets.""" + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + from pcapkit.utilities.exceptions import ProtocolError + + proto = object.__new__(ICMPv4) + with self.assertRaises(ProtocolError): + proto.make(type=12, code=0, checksum=b'\x00\x00', body=b'\x01') + + def test_make_rejects_an_unsupported_body_type(self) -> None: + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + from pcapkit.utilities.exceptions import ProtocolError + + proto = object.__new__(ICMPv4) + with self.assertRaises(ProtocolError): + proto.make(type=8, code=0, checksum=b'\x00\x00', body=12345) # type: ignore[arg-type] + + def test_make_accepts_a_pre_built_schema_body(self) -> None: + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + from pcapkit.protocols.schema.internet.icmpv4 import Echo as Schema_Echo + + proto = object.__new__(ICMPv4) + pre_built = Schema_Echo(id=1, seq=2, data=b'abcd') + schema = proto.make(type=8, code=0, checksum=b'\x124', body=pre_built) + self.assertEqual(schema.pack(), ICMP_PACKETS['echo-request']) + + def test_register_type_accepts_a_callable_pair(self) -> None: + """A ``(parser, constructor)`` pair bypasses the ``_read/_make_data_type_*`` lookup.""" + from pcapkit.const.icmp.type import Type + from pcapkit.protocols.data.internet.icmpv4 import Unknown as Data_Unknown + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + + def custom_parser(schema, *, header, length): # pylint: disable=unused-argument + return Data_Unknown(type=header.type, code=header.code, checksum=header.checksum, + length=length, data=b'custom-parsed') + + def custom_constructor(type, message=None, **kwargs): # pylint: disable=redefined-builtin,unused-argument + from pcapkit.protocols.schema.internet.icmpv4 import Unknown as Schema_Unknown + return Schema_Unknown(data=b'custom-made') + + # ``.get`` rather than ``[...]``: a defaultdict lookup by subscript + # would insert the default and make the registration below warn about + # "already registered" for a code that, from the caller's side, never + # was. + original = ICMPv4.__type__.get(Type.Timestamp) + try: + ICMPv4.register_type(Type.Timestamp, (custom_parser, custom_constructor)) + + packet = bytes.fromhex('0d00' '0000') + bytes(12) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + parsed = ICMPv4(io.BytesIO(packet), len(packet)) + self.assertEqual(parsed.info.data, b'custom-parsed') + + proto = object.__new__(ICMPv4) + schema = proto.make(type=Type.Timestamp, code=0, checksum=b'\x00\x00', body={}) + self.assertEqual(schema.data.data, b'custom-made') + finally: + # Delete rather than reassign when there was nothing to restore: + # the key was never present before this test ran, and writing + # back ``None`` would leave it mismatched against what the + # dispatch code in ``read``/``make`` expects. + if original is None: + ICMPv4.__type__.pop(Type.Timestamp, None) + else: + ICMPv4.__type__[Type.Timestamp] = original + + def test_make_error_messages_use_the_shared_alias(self) -> None: + """The shared make path words its errors with ``alias``, which is ``ICMP`` here.""" + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + from pcapkit.utilities.exceptions import ProtocolError + + proto = object.__new__(ICMPv4) + with warnings.catch_warnings(), self.assertRaises(ProtocolError) as ctx: + warnings.simplefilter('ignore') + proto.make(type=8, code=0, checksum=b'\x00\x00', body=b'\x01') + self.assertEqual(str(ctx.exception), + 'ICMP: [TypeNo 8] message body 01 does not parse as this type') + with warnings.catch_warnings(), self.assertRaises(ProtocolError) as ctx: + warnings.simplefilter('ignore') + proto.make(type=8, code=0, checksum=b'\x00\x00', body=12345) # type: ignore[arg-type] + self.assertEqual(str(ctx.exception), "ICMP: invalid message body type: ") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/internet/test_icmpv6_roundtrip_unit.py b/tests/protocols/internet/test_icmpv6_roundtrip_unit.py new file mode 100644 index 0000000000..825dc57116 --- /dev/null +++ b/tests/protocols/internet/test_icmpv6_roundtrip_unit.py @@ -0,0 +1,312 @@ +# -*- coding: utf-8 -*- +"""ICMPv6 (RFC 4443) messages rebuild byte for byte through ``from_data``. + +Every case builds its own octets in memory and reads no capture. Classes are +imported inside each test, after +:func:`~tests._support.reimport_once_per_class`, not at module load. + +""" + +import io +import struct +import unittest +import warnings + +from tests._support import reimport_once_per_class + +#: Test tier (#1958): hand-picked round-trip cases, not a sweep. +__tier__ = 'ALL' + +#: As much of an invoking packet as :rfc:`4443` quotes -- just 40 distinct +#: octets, so a byte-for-byte mismatch is easy to spot. +QUOTED = bytes(range(40)) + +#: Each entry is ``type || code || checksum`` followed by that type's own +#: body, built by hand against :rfc:`4443`. +ICMPV6_PACKETS = { + 'destination-unreachable': bytes.fromhex('0103' 'abcd' '00000000') + QUOTED, + 'packet-too-big': bytes.fromhex('0200' 'abcd' '00000500') + QUOTED, + 'time-exceeded': bytes.fromhex('0301' 'abcd' '00000000') + QUOTED, + 'parameter-problem': bytes.fromhex('0401' 'abcd' '00000028') + QUOTED, + 'echo-request': bytes.fromhex('8000' '1234' '0001' '0002') + b'abcd', + 'echo-reply': bytes.fromhex('8100' '4321' '0003' '0004') + b'wxyz', + 'unknown-type-router-sol': bytes.fromhex('8500' '0000') + bytes(8), +} + + +class ICMPv6RoundTripTests(unittest.TestCase): + """Pin the byte-for-byte rebuild of every ICMPv6 message type covered.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def test_from_data_rebuilds_byte_for_byte(self) -> None: + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + + for name, packet in ICMPV6_PACKETS.items(): + with self.subTest(packet=name), warnings.catch_warnings(): + warnings.simplefilter('ignore') + parsed = ICMPv6(io.BytesIO(packet), len(packet)) + self.assertEqual(ICMPv6.from_data(parsed.info).data, packet, + f'{name} did not rebuild byte for byte') + + def test_destination_unreachable_code_is_parsed(self) -> None: + from pcapkit.const.icmpv6.destination_unreachable_code import DestinationUnreachableCode + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + + packet = ICMPV6_PACKETS['destination-unreachable'] + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + parsed = ICMPv6(io.BytesIO(packet), len(packet)) + + self.assertEqual(parsed.info.code, DestinationUnreachableCode.address_unreachable) + self.assertEqual(parsed.info.data, QUOTED) + + def test_packet_too_big_mtu_is_parsed(self) -> None: + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + + packet = ICMPV6_PACKETS['packet-too-big'] + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + parsed = ICMPv6(io.BytesIO(packet), len(packet)) + + self.assertEqual(parsed.info.mtu, 0x500) + self.assertEqual(parsed.info.data, QUOTED) + + def test_parameter_problem_pointer_is_32_bit(self) -> None: + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + + packet = ICMPV6_PACKETS['parameter-problem'] + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + parsed = ICMPv6(io.BytesIO(packet), len(packet)) + + # Unlike ICMPv4's Parameter Problem, ICMPv6's Pointer is a full + # 32-bit offset into the invoking packet, not an 8-bit one. + self.assertEqual(parsed.info.pointer, 40) + + def test_echo_fields_are_parsed(self) -> None: + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + + packet = ICMPV6_PACKETS['echo-request'] + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + parsed = ICMPv6(io.BytesIO(packet), len(packet)) + + self.assertEqual(parsed.info.id, 1) + self.assertEqual(parsed.info.seq, 2) + self.assertEqual(parsed.info.data, b'abcd') + + def test_unknown_type_keeps_raw_body(self) -> None: + """Router Solicitation (Neighbor Discovery) is unimplemented.""" + from pcapkit.protocols.data.internet.icmpv6 import Unknown + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + + packet = ICMPV6_PACKETS['unknown-type-router-sol'] + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + parsed = ICMPv6(io.BytesIO(packet), len(packet)) + + self.assertIsInstance(parsed.info, Unknown) + self.assertEqual(parsed.info.data, bytes(8)) + + def test_make_builds_echo_from_scratch(self) -> None: + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + + proto = object.__new__(ICMPv6) + schema = proto.make(type=128, code=0, checksum=b'\x124', + body={'id': 1, 'seq': 2, 'data': b'abcd'}) + self.assertEqual(schema.pack(), ICMPV6_PACKETS['echo-request']) + + def test_construct_with_no_arguments(self) -> None: + """``ICMPv6()`` builds a default Echo Request rather than raising.""" + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + + proto = ICMPv6() + self.assertEqual(bytes(proto), bytes.fromhex('8000000000000000')) + + def test_icmpv6_over_ipv4_dispatches_without_crashing(self) -> None: + """ICMPv6 reached over IPv4 (protocol 58) dispatches cleanly. + + This is an unusual but legitimate framing (protocol 58 directly over + IPv4, without an enclosing IPv6 header), and must not crash the + whole layer down to :class:`~pcapkit.protocols.misc.raw.Raw`. + """ + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + from pcapkit.protocols.internet.ipv4 import IPv4 + + icmpv6_body = ICMPV6_PACKETS['echo-request'] + header = bytearray(bytes.fromhex( + '45' '00' '0000' '0000' '0000' '40' '3a' '0000' + '7f000001' '7f000001' + )) # protocol 0x3a = 58 = ICMPv6 + header[2:4] = struct.pack('>H', 20 + len(icmpv6_body)) + packet = bytes(header) + icmpv6_body + + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + parsed = IPv4(io.BytesIO(packet), len(packet)) + + self.assertIsInstance(parsed.payload, ICMPv6) + self.assertEqual(parsed.payload.info.id, 1) + + def test_dispatches_from_ipv6(self) -> None: + """IPv6's next-header dispatch reaches :class:`ICMPv6`.""" + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + from pcapkit.protocols.internet.ipv6 import IPv6 + + icmpv6_body = ICMPV6_PACKETS['echo-request'] + header = bytearray(40) + header[0] = 0x60 + header[4:6] = struct.pack('>H', len(icmpv6_body)) + header[6] = 58 # next header = ICMPv6 + header[7] = 64 # hop limit + header[8:24] = (b'\x00' * 15) + b'\x01' + header[24:40] = (b'\x00' * 15) + b'\x02' + packet = bytes(header) + icmpv6_body + + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + parsed = IPv6(io.BytesIO(packet), len(packet)) + + self.assertIsInstance(parsed.payload, ICMPv6) + self.assertEqual(parsed.payload.info.id, 1) + + def test_id_index_and_length_hint(self) -> None: + from pcapkit.const.reg.transtype import TransType + from pcapkit.protocols.internet.icmp import ICMP + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + + self.assertEqual(ICMPv6.id(), ('ICMPv6',)) + self.assertEqual(ICMP.id(), ('ICMP', 'ICMPv6')) + self.assertTrue(issubclass(ICMPv6, ICMP)) + self.assertFalse(issubclass(ICMPv6, ICMPv4)) + self.assertEqual(ICMPv6.__index__(), TransType.IPv6_ICMP) + self.assertEqual(object.__new__(ICMPv6).__length_hint__(), 4) + + def test_register_type_warns_on_override(self) -> None: + from pcapkit.const.icmpv6.type import Type + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + from pcapkit.utilities.warnings import RegistryWarning + + original = ICMPv6.__type__[Type.Echo_Request] + try: + with self.assertWarns(RegistryWarning): + ICMPv6.register_type(Type.Echo_Request, 'none') + self.assertEqual(ICMPv6.__type__[Type.Echo_Request], 'none') + + # Re-registering the same entry is a silent no-op. + with warnings.catch_warnings(): + warnings.simplefilter('error', RegistryWarning) + ICMPv6.register_type(Type.Echo_Request, 'none') + finally: + ICMPv6.__type__[Type.Echo_Request] = original + + def test_name_property(self) -> None: + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + + self.assertEqual(object.__new__(ICMPv6).name, + 'Internet Control Message Protocol for IPv6') + + def test_make_rejects_bytes_body_that_fails_to_parse(self) -> None: + """Parameter Problem's body needs at least four octets.""" + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + from pcapkit.utilities.exceptions import ProtocolError + + proto = object.__new__(ICMPv6) + with self.assertRaises(ProtocolError), warnings.catch_warnings(): + warnings.simplefilter('ignore') + proto.make(type=4, code=0, checksum=b'\x00\x00', body=b'\x01') + + def test_make_rejects_an_unsupported_body_type(self) -> None: + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + from pcapkit.utilities.exceptions import ProtocolError + + proto = object.__new__(ICMPv6) + with self.assertRaises(ProtocolError): + proto.make(type=128, code=0, checksum=b'\x00\x00', body=12345) # type: ignore[arg-type] + + def test_make_accepts_a_pre_built_schema_body(self) -> None: + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + from pcapkit.protocols.schema.internet.icmpv6 import Echo as Schema_Echo + + proto = object.__new__(ICMPv6) + pre_built = Schema_Echo(id=1, seq=2, data=b'abcd') + schema = proto.make(type=128, code=0, checksum=b'\x124', body=pre_built) + self.assertEqual(schema.pack(), ICMPV6_PACKETS['echo-request']) + + def test_register_type_accepts_a_callable_pair(self) -> None: + """A ``(parser, constructor)`` pair bypasses the ``_read/_make_data_type_*`` lookup.""" + from pcapkit.const.icmpv6.type import Type + from pcapkit.protocols.data.internet.icmpv6 import Unknown as Data_Unknown + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + + def custom_parser(schema, *, header, length): # pylint: disable=unused-argument + return Data_Unknown(type=header.type, code=header.code, checksum=header.checksum, + length=length, data=b'custom-parsed') + + def custom_constructor(type, message=None, **kwargs): # pylint: disable=redefined-builtin,unused-argument + from pcapkit.protocols.schema.internet.icmpv6 import Unknown as Schema_Unknown + return Schema_Unknown(data=b'custom-made') + + # ``.get`` rather than ``[...]``: a defaultdict lookup by subscript + # would insert the default and make the registration below warn about + # "already registered" for a code that, from the caller's side, never + # was. + original = ICMPv6.__type__.get(Type.Router_Renumbering) + try: + ICMPv6.register_type(Type.Router_Renumbering, (custom_parser, custom_constructor)) + + packet = bytes.fromhex('8a00' '0000') + bytes(12) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + parsed = ICMPv6(io.BytesIO(packet), len(packet)) + self.assertEqual(parsed.info.data, b'custom-parsed') + + proto = object.__new__(ICMPv6) + schema = proto.make(type=Type.Router_Renumbering, code=0, checksum=b'\x00\x00', body={}) + self.assertEqual(schema.data.data, b'custom-made') + finally: + if original is None: + ICMPv6.__type__.pop(Type.Router_Renumbering, None) + else: + ICMPv6.__type__[Type.Router_Renumbering] = original + + def test_make_error_messages_use_the_shared_alias(self) -> None: + """The shared make path words its errors with ``alias``, which is ``ICMPv6`` here.""" + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + from pcapkit.utilities.exceptions import ProtocolError + + proto = object.__new__(ICMPv6) + with warnings.catch_warnings(), self.assertRaises(ProtocolError) as ctx: + warnings.simplefilter('ignore') + proto.make(type=128, code=0, checksum=b'\x00\x00', body=b'\x01') + self.assertEqual(str(ctx.exception), + 'ICMPv6: [TypeNo 128] message body 01 does not parse as this type') + with warnings.catch_warnings(), self.assertRaises(ProtocolError) as ctx: + warnings.simplefilter('ignore') + proto.make(type=128, code=0, checksum=b'\x00\x00', body=12345) # type: ignore[arg-type] + self.assertEqual(str(ctx.exception), "ICMPv6: invalid message body type: ") + + def test_register_type_writes_icmpv6_type_registry_only(self) -> None: + """``ICMPv6.register_type`` writes ICMPv6's own ``__type__``, never ICMPv4's.""" + from pcapkit.const.icmpv6.type import Type + from pcapkit.protocols.internet.icmpv4 import ICMPv4 + from pcapkit.protocols.internet.icmpv6 import ICMPv6 + + code = Type.Router_Renumbering + original = ICMPv6.__type__.get(code) + try: + ICMPv6.register_type(code, 'none') + self.assertEqual(ICMPv6.__type__[code], 'none') + self.assertIsNone(ICMPv4.__type__.get(code)) + finally: + if original is None: + ICMPv6.__type__.pop(code, None) + else: + ICMPv6.__type__[code] = original + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/internet/test_internet_unit.py b/tests/protocols/internet/test_internet_unit.py index 18d82c0eaa..14c27f00ae 100644 --- a/tests/protocols/internet/test_internet_unit.py +++ b/tests/protocols/internet/test_internet_unit.py @@ -200,9 +200,11 @@ def test_dispatching_an_unregistered_transtype_leaves_the_registry_alone(self) - :func:`~pcapkit.foundation.registry.protocols.register_transtype` warn about an overwrite of something nobody ever registered. - ICMP is the cheapest demonstration: it has a well-known number that the - default registry does *not* carry, so a 24-byte IPv4 datagram declaring - ``proto=1`` is enough to leak it. + 253 (:rfc:`3692` experimentation) is the cheapest demonstration: it has + a well-known, reserved number that the default registry does *not* + carry, so a 24-byte IPv4 datagram declaring ``proto=253`` is enough to + leak it. ICMP does not serve: it has a real entry in + :attr:`Internet.__proto__ `. """ from pcapkit.const.reg.transtype import TransType @@ -211,14 +213,15 @@ def test_dispatching_an_unregistered_transtype_leaves_the_registry_alone(self) - from pcapkit.protocols.internet.ipv4 import IPv4 from pcapkit.protocols.misc.raw import Raw - # version/IHL, ToS, total length 24, id, flags/offset, TTL, proto=ICMP, - # checksum, 127.0.0.1 -> 127.0.0.1, then four bytes of payload. - packet = bytes.fromhex('450000180000000040010000' '7f000001' '7f000001') + b'abcd' + # version/IHL, ToS, total length 24, id, flags/offset, TTL, proto=253 + # (RFC 3692 experimentation), checksum, 127.0.0.1 -> 127.0.0.1, then + # four bytes of payload. + packet = bytes.fromhex('450000180000000040fd0000' '7f000001' '7f000001') + b'abcd' self.assertEqual(len(packet), 24) registry = Internet.__dict__['__proto__'] before = set(registry) - self.assertNotIn(TransType.ICMP, before) + self.assertNotIn(TransType.Use_for_experimentation_and_testing_253, before) try: ip = IPv4(packet) @@ -227,7 +230,8 @@ def test_dispatching_an_unregistered_transtype_leaves_the_registry_alone(self) - # number it arrived with -- only the registry write is skipped. self.assertIsInstance(ip.payload, Raw) self.assertEqual(bytes(ip.payload), b'abcd') - self.assertEqual(ip.payload.info.protocol, TransType.ICMP) + self.assertEqual(ip.payload.info.protocol, + TransType.Use_for_experimentation_and_testing_253) self.assertEqual(set(registry), before) # A second datagram must behave identically; a class-level leak from @@ -237,10 +241,10 @@ def test_dispatching_an_unregistered_transtype_leaves_the_registry_alone(self) - # And the number is still registrable without a bogus warning. with mock.patch('pcapkit.protocols.internet.internet.warn') as warn: - register_transtype(TransType.ICMP, Raw) + register_transtype(TransType.Use_for_experimentation_and_testing_253, Raw) self.assertEqual(warn.call_count, 0) finally: - registry.pop(TransType.ICMP, None) + registry.pop(TransType.Use_for_experimentation_and_testing_253, None) if __name__ == '__main__': diff --git a/tests/protocols/internet/test_ip_runtime.py b/tests/protocols/internet/test_ip_runtime.py index 4bfa98b061..0add801429 100644 --- a/tests/protocols/internet/test_ip_runtime.py +++ b/tests/protocols/internet/test_ip_runtime.py @@ -37,20 +37,27 @@ def test_ipv4_packet_exposes_addresses_ttl_and_udp_payload(self) -> None: self.assertEqual(int(udp.info.dstport), 12345) self.assertEqual(type(udp.payload).__name__, 'Raw') - def test_ipv6_icmp_payload_falls_back_to_raw_with_protocol_hint(self) -> None: + def test_ipv6_icmpv6_payload_dispatches_to_icmpv6(self) -> None: + """``IPv6_ICMP`` (58) dispatches to :class:`ICMPv6`. + + This frame's message (Neighbor Solicitation, type 135) is Neighbor + Discovery (:rfc:`4861`), which is unimplemented, so it is + :class:`~pcapkit.protocols.data.internet.icmpv6.Unknown`. + """ extractor = self._extract('ipv6.pcap') frame = extractor.frame[2] ipv6 = frame.payload.payload - raw = ipv6.payload + icmpv6 = ipv6.payload - self.assertEqual(str(frame.protochain), 'Ethernet:IPv6:IPv6_ICMP') + self.assertEqual(str(frame.protochain), 'Ethernet:IPv6:ICMPv6') self.assertEqual(ipv6.name, 'Internet Protocol version 6') self.assertEqual(str(ipv6.src), 'fe80::a423:b61d:7c92:70c6') self.assertEqual(str(ipv6.dst), 'fe80::821f:12ff:fec9:d13d') self.assertEqual(ipv6.info.limit, 255) self.assertEqual(ipv6.info.next.name, 'IPv6_ICMP') - self.assertEqual(type(raw).__name__, 'Raw') - self.assertEqual(raw.info.protocol.name, 'IPv6_ICMP') + self.assertEqual(type(icmpv6).__name__, 'ICMPv6') + self.assertEqual(icmpv6.info.type, 135) + self.assertEqual(type(icmpv6.info).__name__, 'Unknown') def test_ipv6_udp_packet_exposes_multicast_and_reply_addresses(self) -> None: extractor = self._extract('stream.pcap') diff --git a/tests/protocols/link/test_link_runtime.py b/tests/protocols/link/test_link_runtime.py index 3cc346b0f7..aa136d029d 100644 --- a/tests/protocols/link/test_link_runtime.py +++ b/tests/protocols/link/test_link_runtime.py @@ -58,15 +58,18 @@ def test_ipv6_chain_from_sample_includes_expected_protocol_names(self) -> None: extractor = self._extract('in.pcap') ethernet = extractor.frame[0].payload ipv6 = ethernet.payload - raw = ipv6.payload + icmpv6 = ipv6.payload self.assertEqual(ethernet.name, 'Ethernet Protocol') self.assertEqual(ipv6.name, 'Internet Protocol version 6') self.assertEqual(str(ipv6.src), 'fe80::a6:87f9:2793:16ee') self.assertEqual(str(ipv6.dst), 'fe80::1ccd:7c77:bac7:46b7') self.assertEqual(ipv6.info.limit, 255) - self.assertEqual(type(raw).__name__, 'Raw') - self.assertEqual(raw.info.protocol.name, 'IPv6_ICMP') + # Neighbor Solicitation (type 135) is Neighbor Discovery (RFC 4861), + # which is unimplemented, so it decodes as Unknown. + self.assertEqual(type(icmpv6).__name__, 'ICMPv6') + self.assertEqual(icmpv6.info.type, 135) + self.assertEqual(type(icmpv6.info).__name__, 'Unknown') self.assertIn('IPv6', str(ipv6.protochain)) self.assertEqual(ipv6.protochain.aliases[0], 'IPv6') diff --git a/tests/protocols/misc/pcap/test_frame_runtime.py b/tests/protocols/misc/pcap/test_frame_runtime.py index 0a9869c9f0..6b460f0cd8 100644 --- a/tests/protocols/misc/pcap/test_frame_runtime.py +++ b/tests/protocols/misc/pcap/test_frame_runtime.py @@ -22,8 +22,8 @@ def test_frame_exposes_expected_public_metadata(self) -> None: frame = extractor.frame[0] self.assertEqual(frame.name, 'Frame 1') - self.assertEqual(str(frame.protochain), 'Ethernet:IPv6:IPv6_ICMP') - self.assertEqual(frame.protochain.aliases, ('Ethernet', 'IPv6', 'IPv6_ICMP')) + self.assertEqual(str(frame.protochain), 'Ethernet:IPv6:ICMPv6') + self.assertEqual(frame.protochain.aliases, ('Ethernet', 'IPv6', 'ICMPv6')) self.assertEqual(frame.index('IPv6'), 1) self.assertEqual(frame.info.number, 1) # equal because ``in.pcap`` was captured whole, not because the two mean diff --git a/tests/protocols/test_dispatch_registry_unit.py b/tests/protocols/test_dispatch_registry_unit.py index 152a441229..218f6ff819 100644 --- a/tests/protocols/test_dispatch_registry_unit.py +++ b/tests/protocols/test_dispatch_registry_unit.py @@ -228,10 +228,10 @@ def test_cases_cover_every_table_named_in_the_issue(self) -> None: # LinkType.NULL and LinkType.LOOP in both link-type tables, the four # address families of Loopback.__proto__, #2438's # Frame.__proto__[LinkType.LINUX_SLL]/[LinkType.LINUX_SLL2] in both the - # pcap and pcapng frame tables, and #2439's IGMP. - self.assertEqual(len(cases), 54, - 'expected exactly 54 entries across the seven __proto__ ' - + # pcap and pcapng frame tables, #2439's IGMP, and #2436's + # Internet.__proto__[TransType.ICMP] and [TransType.IPv6_ICMP]. + self.assertEqual(len(cases), 56, + 'expected exactly 56 entries across the seven __proto__ ' 'tables named in GitHub issue #496 and the loopback table ' 'of #1574; a different count means a registry changed ' 'shape and this module was not updated to match') @@ -240,7 +240,7 @@ def test_cases_cover_every_table_named_in_the_issue(self) -> None: for case in cases: by_family[case.family] = by_family.get(case.family, 0) + 1 self.assertEqual(by_family, { - 'link': 7, 'internet': 18, 'tcp': 5, 'udp': 4, 'sctp': 2, + 'link': 7, 'internet': 20, 'tcp': 5, 'udp': 4, 'sctp': 2, 'pcap-frame': 7, 'pcapng-frame': 7, 'loopback': 4, }) diff --git a/tests/vendor/test_vendor_missing_body_unit.py b/tests/vendor/test_vendor_missing_body_unit.py index da0487a3fe..6a354df600 100644 --- a/tests/vendor/test_vendor_missing_body_unit.py +++ b/tests/vendor/test_vendor_missing_body_unit.py @@ -85,7 +85,10 @@ #: or under a different variable name entirely is skipped by construction too, #: same as one built by ``.append()``. Those are covered behaviourally, not #: statically, by :file:`tests/const/test_const_enum_lookup.py`. -EXPECTED_CRAWLERS_WITH_CONSTANT_MISS_LINES = 36 +#: The ICMP and ICMPv6 ``type`` crawlers seed ``miss`` with an empty list and +#: build every line dynamically, so they do not count here. The code, time_exceeded, +#: destination_unreachable, and parameter_problem crawlers emit constant bodies. +EXPECTED_CRAWLERS_WITH_CONSTANT_MISS_LINES = 42 def _constant_miss_lines() -> 'dict[str, list[str]]': From a247e314e6494748d2606d20dd5c729a00348899 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Sat, 31 Oct 2026 16:18:09 -0400 Subject: [PATCH 2/2] fix(tests): bump the census pins for ICMP/ICMPv6's 8 new enums ICMP and ICMPv6 each add 4 const modules (type + 3 *_code modules), 8 total, but the pin bumps across the 4 census test files were only +7. Bumped the remaining +1 everywhere: ints 124->125, enums 140->141, distinct modules 134->135 (test_const_enum_builtin_parity); enums 130->131, covered 127->128 (test_const_enum_get); enums 124->125 (test_const_enum_lookup); all_files 134->135, generated 117->118 (test_const_registry_protocol). Verified: tests/const/ and tests/vendor/ pass clean (580 tests, 27053 subtests). --- tests/const/test_const_enum_builtin_parity.py | 6 +++--- tests/const/test_const_enum_get.py | 4 ++-- tests/const/test_const_enum_lookup.py | 2 +- tests/const/test_const_registry_protocol.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/const/test_const_enum_builtin_parity.py b/tests/const/test_const_enum_builtin_parity.py index 41f2f01e56..eb999a4814 100644 --- a/tests/const/test_const_enum_builtin_parity.py +++ b/tests/const/test_const_enum_builtin_parity.py @@ -377,11 +377,11 @@ def test_the_sweep_size_is_pinned(self) -> None: # tests.const.test_const_enum_get's narrower (IntEnum, IntFlag)-only # sweep. The int count includes GitHub issue #880's ngap.ProcedureCode # and ngap.ProtocolIE, which are on EnumRegistry. - self.assertEqual(len(ints), 124) + self.assertEqual(len(ints), 126) self.assertEqual(len(flags), 6) self.assertEqual(len(strs), 10) - self.assertEqual(len(self.enums), 140) - self.assertEqual(len({obj.__module__ for obj in self.enums}), 134) + self.assertEqual(len(self.enums), 142) + self.assertEqual(len({obj.__module__ for obj in self.enums}), 136) def test_every_registry_rejects_a_negative_value(self) -> None: """The registry-wide form of GitHub issue #647. diff --git a/tests/const/test_const_enum_get.py b/tests/const/test_const_enum_get.py index 628ca4e1ce..84e8e30588 100644 --- a/tests/const/test_const_enum_get.py +++ b/tests/const/test_const_enum_get.py @@ -297,7 +297,7 @@ def test_the_sweep_size_is_pinned(self) -> None: # pcapkit.protocols.application.ngap; neither needs an exception-set # entry, since both take the base's own get(key, default). names = {_qualname(obj) for obj in self.enums} - self.assertEqual(len(self.enums), 130) + self.assertEqual(len(self.enums), 132) for expected in (EXPECTED_TO_RESOLVE_ANYTHING, EXPECTED_WITHOUT_AN_INTEGER_DEFAULT, EXPECTED_WITHOUT_A_CACHEABLE_FALLBACK): self.assertTrue(expected.issubset(names), @@ -337,7 +337,7 @@ def test_every_integer_path_consults_the_default(self) -> None: # registry excused above: ``EXPECTED_TO_RESOLVE_ANYTHING`` and # ``EXPECTED_WITHOUT_A_CACHEABLE_FALLBACK``, with # ``EXPECTED_WITHOUT_AN_INTEGER_DEFAULT`` empty. - self.assertEqual(covered, 127) + self.assertEqual(covered, 129) def test_the_always_resolving_registries_have_nothing_to_fall_back_to(self) -> None: """The two registries excused from the sweep, and why. diff --git a/tests/const/test_const_enum_lookup.py b/tests/const/test_const_enum_lookup.py index daa84e6477..0acaa213ac 100644 --- a/tests/const/test_const_enum_lookup.py +++ b/tests/const/test_const_enum_lookup.py @@ -200,7 +200,7 @@ def test_every_registry_enum_was_discovered(self) -> None: # ``_iter_const_int_flags``'s, and so are GitHub issue #880's # ngap.ProcedureCode and ngap.ProtocolIE, which are on EnumRegistry. names = {f'{obj.__module__}.{obj.__qualname__}' for obj in self.enums} - self.assertEqual(len(self.enums), 124) + self.assertEqual(len(self.enums), 126) self.assertTrue(EXPECTED_TO_REJECT_ZERO.issubset(names), f'expected reject-list entries missing from the sweep: ' f'{EXPECTED_TO_REJECT_ZERO - names}') diff --git a/tests/const/test_const_registry_protocol.py b/tests/const/test_const_registry_protocol.py index aca0eafaae..0e811831b9 100644 --- a/tests/const/test_const_registry_protocol.py +++ b/tests/const/test_const_registry_protocol.py @@ -846,8 +846,8 @@ def test_every_generated_const_module_is_accounted_for(self) -> None: generated = [path for path in all_files if path not in converted_relpaths and path not in EXCLUDED_FROM_INTENUM_CENSUS] - self.assertEqual(len(all_files), 134) - self.assertEqual(len(generated), 117) + self.assertEqual(len(all_files), 136) + self.assertEqual(len(generated), 119) for relpath in generated: with self.subTest(module=relpath):