From 1c43fcd035777e0b4cbc1f68bf3123eabe9bcb3a Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Mon, 14 Sep 2026 13:19:51 -0400 Subject: [PATCH 1/2] sctp: implement SCTP as a transport protocol Only a 0-byte stub existed under NotImplemented/, so IP protocol 132 fell through to Raw and nothing could ride SCTP. RFC 9260, following the shape TCP uses for options: a common header, then chunks dispatched through a table, each with a read handler and a make counterpart. Covers all 13 chunk types the RFC defines, all 8 chunk parameters and all 13 error causes. Unassigned, reserved and extension types fall through to a generic handler that records raw flags and value rather than raising - as do ECNE and CWR, whose numbers the RFC assigns but whose formats it reserves. CRC32c is both recorded verbatim and verifiable: unlike TCP and UDP the digest covers only the SCTP packet with the field zeroed, no IP pseudo-header, so it can be checked from the SCTP bytes alone. The table is generated at import from the reflected polynomial and asserted against RFC 9260 Appendix A. The wire bytes are little-endian, which a test pins against the big-endian form. Dispatch is keyed on the DATA chunk's Payload Protocol Identifier rather than a port, so register_sctp(60, ...) is what NGAP will later call. Deliberately not wired into register_apptype's fan-out, which writes port numbers. Const modules under pcapkit/const/sctp/ are hand-written, but generated by driving the real Vendor.context() against the IANA CSVs, so they match what a future crawler would emit rather than diverging from it. Two corekit weaknesses were worked around locally rather than fixed in shared code: Schema.pack shares one packet dict with nested schemas, so a nested parameter's length overwrote the enclosing chunk's; and ListField.unpack can loop forever on a truncated list of SchemaField items. --- docs/source/ext.rst | 10 + docs/source/pcapkit/const/index.rst | 1 + docs/source/pcapkit/const/sctp.rst | 90 + docs/source/pcapkit/foundation/registry.rst | 2 + docs/source/pcapkit/protocols/index.rst | 3 +- .../pcapkit/protocols/transport/index.rst | 3 +- .../pcapkit/protocols/transport/sctp.rst | 523 +++ .../pcapkit/protocols/transport/transport.rst | 5 +- docs/source/pep.rst | 19 +- pcapkit/__init__.py | 2 +- pcapkit/all.py | 4 +- pcapkit/const/__init__.py | 3 + pcapkit/const/sctp/__init__.py | 43 + pcapkit/const/sctp/cause_code.py | 135 + pcapkit/const/sctp/chunk.py | 153 + pcapkit/const/sctp/parameter.py | 159 + .../const/sctp/payload_protocol_identifier.py | 302 ++ pcapkit/foundation/__init__.py | 2 +- pcapkit/foundation/registry/__init__.py | 2 +- pcapkit/foundation/registry/protocols.py | 60 +- pcapkit/protocols/__init__.py | 2 +- pcapkit/protocols/data/__init__.py | 22 + pcapkit/protocols/data/transport/__init__.py | 87 + pcapkit/protocols/data/transport/sctp.py | 574 +++ pcapkit/protocols/internet/internet.py | 3 + pcapkit/protocols/schema/__init__.py | 22 + .../protocols/schema/transport/__init__.py | 85 + pcapkit/protocols/schema/transport/sctp.py | 895 +++++ .../transport/NotImplemented/sctp.py | 0 pcapkit/protocols/transport/__init__.py | 5 +- pcapkit/protocols/transport/sctp.py | 3459 +++++++++++++++++ pcapkit/protocols/transport/transport.py | 5 +- tests/protocols/transport/test_sctp_unit.py | 1146 ++++++ 33 files changed, 7810 insertions(+), 16 deletions(-) create mode 100644 docs/source/pcapkit/const/sctp.rst create mode 100644 docs/source/pcapkit/protocols/transport/sctp.rst create mode 100644 pcapkit/const/sctp/__init__.py create mode 100644 pcapkit/const/sctp/cause_code.py create mode 100644 pcapkit/const/sctp/chunk.py create mode 100644 pcapkit/const/sctp/parameter.py create mode 100644 pcapkit/const/sctp/payload_protocol_identifier.py create mode 100644 pcapkit/protocols/data/transport/sctp.py create mode 100644 pcapkit/protocols/schema/transport/sctp.py delete mode 100644 pcapkit/protocols/transport/NotImplemented/sctp.py create mode 100644 pcapkit/protocols/transport/sctp.py create mode 100644 tests/protocols/transport/test_sctp_unit.py diff --git a/docs/source/ext.rst b/docs/source/ext.rst index 5ce2d947f2..9a5df1bc79 100644 --- a/docs/source/ext.rst +++ b/docs/source/ext.rst @@ -63,6 +63,8 @@ The following table shows all available protocol classes in :mod:`pcapkit`: | Transport Layer | :class:`pcapkit.protocols.transport.tcp.TCP` | + (:class:`~pcapkit.protocols.transport.transport.Transport` +----------------+-----------------------+-------------------------------------------------------------+ | subclasses) | :class:`pcapkit.protocols.transport.udp.UDP` | ++ +----------------+-----------------------+-------------------------------------------------------------+ +| | :class:`pcapkit.protocols.transport.sctp.SCTP` | +------------------------------------------------------------------+----------------+-----------------------+-------------------------------------------------------------+ | | | :class:`pcapkit.protocols.application.ftp.FTP` | + + FTP Family +-----------------------+-------------------------------------------------------------+ @@ -145,6 +147,8 @@ functions: | | | :func:`~pcapkit.foundation.registry.protocols.register_tcp` | the left columns | + Application Layer + :func:`~pcapkit.foundation.registry.protocols.register_apptype` +----------------------------------------------------------------+ when registering + | | | :func:`~pcapkit.foundation.registry.protocols.register_udp` | new protocols. | ++ +-------------------------------------------------------------------+----------------------------------------------------------------+ + +| | | :func:`~pcapkit.foundation.registry.protocols.register_sctp` | | +-------------------+-------------------------------------------------------------------+----------------------------------------------------------------+--------------------+ Samples @@ -261,6 +265,12 @@ available extensible items and the helper registry functions: | | | :attr:`TCP.__option__ ` | :func:`~pcapkit.foundation.registry.protocols.register_tcp_option` | + Transport Layer + :class:`~pcapkit.protocols.transport.tcp.TCP` +-----------------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ | | | :attr:`TCP.__mp_option__ ` | :func:`~pcapkit.foundation.registry.protocols.register_tcp_mp_option` | ++ +------------------------------------------------------------+-----------------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ +| | | :attr:`SCTP.__chunk__ ` | :meth:`~pcapkit.protocols.transport.sctp.SCTP.register_chunk` | ++ + +-----------------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ +| | :class:`~pcapkit.protocols.transport.sctp.SCTP` | :attr:`SCTP.__parameter__ ` | :meth:`~pcapkit.protocols.transport.sctp.SCTP.register_parameter` | ++ + +-----------------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ +| | | :attr:`SCTP.__cause__ ` | :meth:`~pcapkit.protocols.transport.sctp.SCTP.register_cause` | +-------------------+------------------------------------------------------------+-----------------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ | Application Layer | :class:`~pcapkit.protocols.application.httpv2.HTTP` | :attr:`HTTP.__frame__ ` | :func:`~pcapkit.foundation.registry.protocols.register_http_frame` | +-------------------+------------------------------------------------------------+-----------------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ diff --git a/docs/source/pcapkit/const/index.rst b/docs/source/pcapkit/const/index.rst index 341081bd03..02149563c8 100644 --- a/docs/source/pcapkit/const/index.rst +++ b/docs/source/pcapkit/const/index.rst @@ -51,6 +51,7 @@ Transport Layer .. toctree:: :maxdepth: 2 + sctp tcp Application Layer diff --git a/docs/source/pcapkit/const/sctp.rst b/docs/source/pcapkit/const/sctp.rst new file mode 100644 index 0000000000..cf7e0c05a4 --- /dev/null +++ b/docs/source/pcapkit/const/sctp.rst @@ -0,0 +1,90 @@ +===================================================================== +:class:`~pcapkit.protocols.transport.sctp.SCTP` Constant Enumerations +===================================================================== + +.. module:: pcapkit.const.sctp + +This module contains all constant enumerations of +:class:`~pcapkit.protocols.transport.sctp.SCTP` implementations. Available +enumerations include: + +.. list-table:: + + * - :class:`SCTP_Chunk ` + - SCTP Chunk Types [*]_ + * - :class:`SCTP_Parameter ` + - SCTP Chunk Parameter Types [*]_ + * - :class:`SCTP_CauseCode ` + - SCTP Error Cause Codes [*]_ + * - :class:`SCTP_PayloadProtocolIdentifier ` + - SCTP Payload Protocol Identifiers [*]_ + +.. note:: + + Unlike most constant enumerations of :mod:`pcapkit`, the SCTP enumerations + are **hand-maintained**, as there is no crawler for them under + :mod:`pcapkit.vendor` yet. Should one be added later, it would target the + registries linked above. + +SCTP Chunk Types +================ + +.. module:: pcapkit.const.sctp.chunk + +This module contains the constant enumeration for **SCTP Chunk Types**, +which is maintained manually against the IANA registry, as there +is currently no vendor crawler for SCTP under :mod:`pcapkit.vendor`. + +.. autoclass:: pcapkit.const.sctp.chunk.Chunk + :members: + :undoc-members: + :show-inheritance: + +SCTP Chunk Parameter Types +========================== + +.. module:: pcapkit.const.sctp.parameter + +This module contains the constant enumeration for **SCTP Chunk Parameter Types**, +which is maintained manually against the IANA registry, as there +is currently no vendor crawler for SCTP under :mod:`pcapkit.vendor`. + +.. autoclass:: pcapkit.const.sctp.parameter.Parameter + :members: + :undoc-members: + :show-inheritance: + +SCTP Error Cause Codes +====================== + +.. module:: pcapkit.const.sctp.cause_code + +This module contains the constant enumeration for **SCTP Error Cause Codes**, +which is maintained manually against the IANA registry, as there +is currently no vendor crawler for SCTP under :mod:`pcapkit.vendor`. + +.. autoclass:: pcapkit.const.sctp.cause_code.CauseCode + :members: + :undoc-members: + :show-inheritance: + +SCTP Payload Protocol Identifiers +================================= + +.. module:: pcapkit.const.sctp.payload_protocol_identifier + +This module contains the constant enumeration for **SCTP Payload Protocol Identifiers**, +which is maintained manually against the IANA registry, as there +is currently no vendor crawler for SCTP under :mod:`pcapkit.vendor`. + +.. autoclass:: pcapkit.const.sctp.payload_protocol_identifier.PayloadProtocolIdentifier + :members: + :undoc-members: + :show-inheritance: + +.. rubric:: Footnotes + +.. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-1 +.. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-2 +.. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-24 +.. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-25 diff --git a/docs/source/pcapkit/foundation/registry.rst b/docs/source/pcapkit/foundation/registry.rst index 8d7bb8ffef..e508286ab5 100644 --- a/docs/source/pcapkit/foundation/registry.rst +++ b/docs/source/pcapkit/foundation/registry.rst @@ -99,6 +99,8 @@ Transport Layer Registries .. autofunction:: pcapkit.foundation.registry.protocols.register_udp +.. autofunction:: pcapkit.foundation.registry.protocols.register_sctp + Application Layer Registries ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/source/pcapkit/protocols/index.rst b/docs/source/pcapkit/protocols/index.rst index b7817ec34e..e1878b92fc 100644 --- a/docs/source/pcapkit/protocols/index.rst +++ b/docs/source/pcapkit/protocols/index.rst @@ -57,7 +57,7 @@ diagram of the class hierarchy of :mod:`pcapkit.protocols`: end subgraph transport [Transport Layer] - Transport --> TCP & UDP + Transport --> TCP & UDP & SCTP end subgraph application [Application Layer] @@ -124,6 +124,7 @@ diagram of the class hierarchy of :mod:`pcapkit.protocols`: click Transport "/pcapkit/protocols/transport/transport.html#pcapkit.protocols.transport.Transport" click TCP "/pcapkit/protocols/transport/tcp.html#pcapkit.protocols.internet.tcp.TCP" click UDP "/pcapkit/protocols/transport/udp.html#pcapkit.protocols.internet.udp.UDP" + click SCTP "/pcapkit/protocols/transport/sctp.html#pcapkit.protocols.transport.sctp.SCTP" click Application "/pcapkit/protocols/application/application.html#pcapkit.protocols.application.Application" click HTTP "/pcapkit/protocols/application/http.html#pcapkit.protocols.application.http.HTTP" diff --git a/docs/source/pcapkit/protocols/transport/index.rst b/docs/source/pcapkit/protocols/transport/index.rst index 14e9878857..928186c11a 100644 --- a/docs/source/pcapkit/protocols/transport/index.rst +++ b/docs/source/pcapkit/protocols/transport/index.rst @@ -12,12 +12,13 @@ transport layer, with detailed implementation and methods. :maxdepth: 1 transport + sctp tcp udp .. todo:: - Implements DCCP, RSVP, SCTP. + Implements DCCP, RSVP. Protocol Registry ----------------- diff --git a/docs/source/pcapkit/protocols/transport/sctp.rst b/docs/source/pcapkit/protocols/transport/sctp.rst new file mode 100644 index 0000000000..24e9661739 --- /dev/null +++ b/docs/source/pcapkit/protocols/transport/sctp.rst @@ -0,0 +1,523 @@ +SCTP - Stream Control Transmission Protocol +=========================================== + +.. module:: pcapkit.protocols.transport.sctp + +:mod:`pcapkit.protocols.transport.sctp` contains +:class:`~pcapkit.protocols.transport.sctp.SCTP` only, +which implements extractor for Stream Control +Transmission Protocol (SCTP) [*]_, whose structure is +described as below: + +======= ========= ========================= ======================================= +Octets Bits Name Description +======= ========= ========================= ======================================= + 0 0 ``sctp.srcport`` Source Port + 2 16 ``sctp.dstport`` Destination Port + 4 32 ``sctp.vtag`` Verification Tag + 8 64 ``sctp.chksum`` Checksum (CRC32c) + 12 96 ``sctp.chunks`` Chunks +======= ========= ========================= ======================================= + +.. autoclass:: pcapkit.protocols.transport.sctp.SCTP + :no-members: + :show-inheritance: + + .. autoproperty:: name + .. autoproperty:: length + .. autoproperty:: src + .. autoproperty:: dst + .. autoproperty:: ppid + .. autoproperty:: checksum_valid + + .. automethod:: read + .. automethod:: make + + .. automethod:: register + .. automethod:: register_chunk + .. automethod:: register_parameter + .. automethod:: register_cause + + .. automethod:: crc32c + .. automethod:: calculate_checksum + .. automethod:: validate_checksum + + .. automethod:: _make_data + .. automethod:: _get_payload + .. automethod:: _decode_next_layer + + .. automethod:: _read_sctp_chunks + .. automethod:: _make_sctp_chunks + .. automethod:: _make_sctp_chunk + .. automethod:: _read_sctp_parameters + .. automethod:: _make_sctp_parameters + .. automethod:: _make_sctp_parameter + .. automethod:: _read_sctp_causes + .. automethod:: _make_sctp_causes + .. automethod:: _make_sctp_cause + + .. automethod:: _read_chunk_donone + .. automethod:: _read_chunk_data + .. automethod:: _read_chunk_init + .. automethod:: _read_chunk_init_ack + .. automethod:: _read_chunk_sack + .. automethod:: _read_chunk_heartbeat + .. automethod:: _read_chunk_heartbeat_ack + .. automethod:: _read_chunk_abort + .. automethod:: _read_chunk_shutdown + .. automethod:: _read_chunk_shutdown_ack + .. automethod:: _read_chunk_error + .. automethod:: _read_chunk_cookie_echo + .. automethod:: _read_chunk_cookie_ack + .. automethod:: _read_chunk_shutdown_complete + + .. automethod:: _make_chunk_donone + .. automethod:: _make_chunk_data + .. automethod:: _make_chunk_init + .. automethod:: _make_chunk_init_ack + .. automethod:: _make_chunk_sack + .. automethod:: _make_chunk_heartbeat + .. automethod:: _make_chunk_heartbeat_ack + .. automethod:: _make_chunk_abort + .. automethod:: _make_chunk_shutdown + .. automethod:: _make_chunk_shutdown_ack + .. automethod:: _make_chunk_error + .. automethod:: _make_chunk_cookie_echo + .. automethod:: _make_chunk_cookie_ack + .. automethod:: _make_chunk_shutdown_complete + + .. automethod:: _read_param_donone + .. automethod:: _read_param_hbinfo + .. automethod:: _read_param_ipv4 + .. automethod:: _read_param_ipv6 + .. automethod:: _read_param_cookie + .. automethod:: _read_param_unrecognized + .. automethod:: _read_param_preservative + .. automethod:: _read_param_hostname + .. automethod:: _read_param_addrtypes + + .. automethod:: _make_param_donone + .. automethod:: _make_param_hbinfo + .. automethod:: _make_param_ipv4 + .. automethod:: _make_param_ipv6 + .. automethod:: _make_param_cookie + .. automethod:: _make_param_unrecognized + .. automethod:: _make_param_preservative + .. automethod:: _make_param_hostname + .. automethod:: _make_param_addrtypes + + .. automethod:: _read_cause_donone + .. automethod:: _read_cause_invalid_stream + .. automethod:: _read_cause_missing_param + .. automethod:: _read_cause_stale_cookie + .. automethod:: _read_cause_out_of_resource + .. automethod:: _read_cause_unresolvable_addr + .. automethod:: _read_cause_unrecognized_chunk + .. automethod:: _read_cause_invalid_param + .. automethod:: _read_cause_unrecognized_params + .. automethod:: _read_cause_no_user_data + .. automethod:: _read_cause_cookie_shutdown + .. automethod:: _read_cause_restart_addr + .. automethod:: _read_cause_user_abort + .. automethod:: _read_cause_protocol_violation + + .. automethod:: _make_cause_donone + .. automethod:: _make_cause_invalid_stream + .. automethod:: _make_cause_missing_param + .. automethod:: _make_cause_stale_cookie + .. automethod:: _make_cause_out_of_resource + .. automethod:: _make_cause_unresolvable_addr + .. automethod:: _make_cause_unrecognized_chunk + .. automethod:: _make_cause_invalid_param + .. automethod:: _make_cause_unrecognized_params + .. automethod:: _make_cause_no_user_data + .. automethod:: _make_cause_cookie_shutdown + .. automethod:: _make_cause_restart_addr + .. automethod:: _make_cause_user_abort + .. automethod:: _make_cause_protocol_violation + + .. autoattribute:: __proto__ + :no-value: + .. autoattribute:: __chunk__ + :no-value: + .. autoattribute:: __parameter__ + :no-value: + .. autoattribute:: __cause__ + :no-value: + + .. automethod:: __index__ + +Header Schemas +-------------- + +.. module:: pcapkit.protocols.schema.transport.sctp + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.SCTP + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.Chunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.UnknownChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.DATAChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.INITChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.INITACKChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.SACKChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.HeartbeatChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.HeartbeatACKChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.AbortChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.ShutdownChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.ShutdownACKChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.ErrorChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.CookieEchoChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.CookieACKChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.ShutdownCompleteChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.GapAckBlock + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.Parameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.UnknownParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.HeartbeatInfoParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.IPv4AddressParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.IPv6AddressParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.StateCookieParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.UnrecognizedParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.CookiePreservativeParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.HostNameAddressParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.SupportedAddressTypesParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.ErrorCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.UnknownCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.InvalidStreamIdentifierCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.MissingMandatoryParameterCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.StaleCookieCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.OutOfResourceCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.UnresolvableAddressCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.UnrecognizedChunkTypeCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.InvalidMandatoryParameterCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.UnrecognizedParametersCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.NoUserDataCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.CookieReceivedWhileShuttingDownCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.RestartOfAnAssociationWithNewAddressesCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.UserInitiatedAbortCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.ProtocolViolationCause + :members: + :show-inheritance: + +Type Stubs +~~~~~~~~~~ + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.DATAChunkFlags + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.sctp.TBitFlags + :members: + :show-inheritance: + +Auxiliary Functions +~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: pcapkit.protocols.schema.transport.sctp.padding_length +.. autofunction:: pcapkit.protocols.schema.transport.sctp.nested_length + +Data Models +----------- + +.. module:: pcapkit.protocols.data.transport.sctp + +.. autoclass:: pcapkit.protocols.data.transport.sctp.SCTP + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.DATAChunkFlags + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.TBitFlags + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.GapAckBlock + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.Chunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.UnknownChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.DATAChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.INITChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.INITACKChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.SACKChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.HeartbeatChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.HeartbeatACKChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.AbortChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.ShutdownChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.ShutdownACKChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.ErrorChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.CookieEchoChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.CookieACKChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.ShutdownCompleteChunk + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.Parameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.UnknownParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.HeartbeatInfoParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.IPv4AddressParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.IPv6AddressParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.StateCookieParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.UnrecognizedParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.CookiePreservativeParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.HostNameAddressParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.SupportedAddressTypesParameter + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.ErrorCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.UnknownCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.InvalidStreamIdentifierCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.MissingMandatoryParameterCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.StaleCookieCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.OutOfResourceCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.UnresolvableAddressCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.UnrecognizedChunkTypeCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.InvalidMandatoryParameterCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.UnrecognizedParametersCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.NoUserDataCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.CookieReceivedWhileShuttingDownCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.RestartOfAnAssociationWithNewAddressesCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.UserInitiatedAbortCause + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.sctp.ProtocolViolationCause + :members: + :show-inheritance: + +.. rubric:: Footnotes + +.. [*] https://en.wikipedia.org/wiki/Stream_Control_Transmission_Protocol diff --git a/docs/source/pcapkit/protocols/transport/transport.rst b/docs/source/pcapkit/protocols/transport/transport.rst index 3847e3a848..4bb4e83bba 100644 --- a/docs/source/pcapkit/protocols/transport/transport.rst +++ b/docs/source/pcapkit/protocols/transport/transport.rst @@ -6,8 +6,9 @@ Base Protocol :mod:`pcapkit.protocols.transport.transport` contains :class:`~pcapkit.protocols.transport.transport.Transport`, which is a base class for transport layer protocols, eg. -:class:`~pcapkit.protocols.transport.transport.tcp.TCP` and -:class:`~pcapkit.protocols.transport.transport.udp.UDP`. +:class:`~pcapkit.protocols.transport.tcp.TCP`, +:class:`~pcapkit.protocols.transport.udp.UDP` and +:class:`~pcapkit.protocols.transport.sctp.SCTP`. .. autoclass:: pcapkit.protocols.transport.transport.Transport :no-members: diff --git a/docs/source/pep.rst b/docs/source/pep.rst index 44308d152f..c24c5b50b7 100644 --- a/docs/source/pep.rst +++ b/docs/source/pep.rst @@ -21,6 +21,23 @@ Wish you enjoy **PyPCAPKit**!!! More Protocols, More!!! ----------------------- +.. note:: + + **SCTP** is now **done**. It is implemented as a first-class transport layer + protocol per :rfc:`9260`: the common header, all thirteen chunk types the + RFC defines, chunk parameters, error causes, and CRC32c checksum + verification. Chunk types, parameters and error causes that are registered + but not yet implemented fall through to the generic handlers rather than + failing the extraction. + + Two notes on how it differs from its siblings. Its constant enumerations + under :mod:`pcapkit.const.sctp` are hand-maintained, as there is no + ``pcapkit.vendor.sctp`` crawler yet. And the next layer is dispatched on the + DATA chunk's *payload protocol identifier* through + :func:`~pcapkit.foundation.registry.protocols.register_sctp`, not on port + numbers, so :func:`~pcapkit.foundation.registry.protocols.register_apptype` + deliberately does not fan out to it. + As you may have noticed, there are some protocol-named files under the ``NotImplemented`` folders. These protocols are what I planned to implement but not yet done. Namely, grouped by each TCP/IP layer and ordered by protocol @@ -28,7 +45,7 @@ name alphabetically, * Link Layer: DSL, EAPOL, FDDI, ISDN, PPP * Internet Layer: ECN, ESP, ICMP, ICMPv6, IGMP, NDP, Shim6 -* Transport Layer: DCCP, QUIC, RSVP, SCTP +* Transport Layer: DCCP, QUIC, RSVP * Application Layer: BGP, DHCP, DHCPv6, DNS, IMAP, LDAP, MQTT, NNTP, NTP, ONC/RPC, POP, RIP, RTP, SIP, SMTP, SNMP, SSH, Telnet, TLS/SSL, XMPP diff --git a/pcapkit/__init__.py b/pcapkit/__init__.py index 835735dc3e..4ce1b21711 100644 --- a/pcapkit/__init__.py +++ b/pcapkit/__init__.py @@ -114,7 +114,7 @@ 'HIP', 'HOPOPT', 'IPv6_Frag', 'IPv6_Opts', 'IPv6_Route', 'MH', # IPv6 Extension Header - 'TCP', 'UDP', # Transport Layer + 'TCP', 'UDP', 'SCTP', # Transport Layer 'FTP', 'FTP_DATA', # Application Layer 'HTTP', diff --git a/pcapkit/all.py b/pcapkit/all.py index 08ec9666bf..c4c179f1ea 100644 --- a/pcapkit/all.py +++ b/pcapkit/all.py @@ -88,7 +88,7 @@ 'register_ipv6_opts_option', 'register_ipv6_route_routing', 'register_mh_message', 'register_mh_option', 'register_mh_extension', 'register_apptype', - 'register_tcp', 'register_udp', + 'register_tcp', 'register_udp', 'register_sctp', 'register_tcp_option', 'register_tcp_mp_option', 'register_http_frame', 'register_pcapng_block', 'register_pcapng_option', 'register_pcapng_secrets', @@ -113,7 +113,7 @@ 'AH', 'IP', 'IPsec', 'IPv4', 'IPv6', 'IPX', # Internet Layer 'HIP', 'HOPOPT', 'IPv6_Frag', 'IPv6_Opts', 'IPv6_Route', 'MH', # IPv6 Extension Header - 'TCP', 'UDP', # Transport Layer + 'TCP', 'UDP', 'SCTP', # Transport Layer 'FTP', 'FTP_DATA', # Application Layer 'HTTP', 'Schema', 'schema', # Protocol Schema diff --git a/pcapkit/const/__init__.py b/pcapkit/const/__init__.py index 19ea2fb594..4f9dd953d4 100644 --- a/pcapkit/const/__init__.py +++ b/pcapkit/const/__init__.py @@ -25,6 +25,7 @@ from pcapkit.const.l2tp import * from pcapkit.const.mh import * from pcapkit.const.ospf import * +from pcapkit.const.sctp import * from pcapkit.const.tcp import * from pcapkit.const.vlan import * @@ -70,6 +71,8 @@ 'MH_CGAExtension', 'MH_CGASec', 'MH_BindingError', # OSPF 'OSPF_Authentication', 'OSPF_Packet', + # SCTP + 'SCTP_Chunk', 'SCTP_Parameter', 'SCTP_CauseCode', 'SCTP_PayloadProtocolIdentifier', # TCP 'TCP_Checksum', 'TCP_Option', 'TCP_MPTCPOption', 'TCP_Flags', # VLAN diff --git a/pcapkit/const/sctp/__init__.py b/pcapkit/const/sctp/__init__.py new file mode 100644 index 0000000000..fab70701b5 --- /dev/null +++ b/pcapkit/const/sctp/__init__.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# pylint: disable=unused-import +""":class:`~pcapkit.protocols.transport.sctp.SCTP` Constant Enumerations +========================================================================== + +.. module:: pcapkit.const.sctp + +This module contains all constant enumerations of +:class:`~pcapkit.protocols.transport.sctp.SCTP` implementations. Available +enumerations include: + +.. list-table:: + + * - :class:`SCTP_Chunk ` + - SCTP Chunk Types [*]_ + * - :class:`SCTP_Parameter ` + - SCTP Chunk Parameter Types [*]_ + * - :class:`SCTP_CauseCode ` + - SCTP Error Cause Codes [*]_ + * - :class:`SCTP_PayloadProtocolIdentifier ` + - SCTP Payload Protocol Identifiers [*]_ + +.. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-1 +.. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-2 +.. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-24 +.. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-25 + +Note: + Unlike most constant enumerations of :mod:`pcapkit`, the SCTP enumerations + are **hand-maintained**, as there is no crawler for them under + :mod:`pcapkit.vendor` yet. Should one be added later, it would target the + registries linked above. + +""" + +from pcapkit.const.sctp.cause_code import CauseCode as SCTP_CauseCode +from pcapkit.const.sctp.chunk import Chunk as SCTP_Chunk +from pcapkit.const.sctp.parameter import Parameter as SCTP_Parameter +from pcapkit.const.sctp.payload_protocol_identifier import \ + PayloadProtocolIdentifier as SCTP_PayloadProtocolIdentifier + +__all__ = ['SCTP_Chunk', 'SCTP_Parameter', 'SCTP_CauseCode', + 'SCTP_PayloadProtocolIdentifier'] diff --git a/pcapkit/const/sctp/cause_code.py b/pcapkit/const/sctp/cause_code.py new file mode 100644 index 0000000000..31f4678343 --- /dev/null +++ b/pcapkit/const/sctp/cause_code.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""SCTP Error Cause Codes +============================ + +.. module:: pcapkit.const.sctp.cause_code + +This module contains the constant enumeration for **SCTP Error Cause Codes**, +which is maintained manually against the `IANA`_ registry, as there +is currently no vendor crawler for SCTP under :mod:`pcapkit.vendor`. + +.. _IANA: https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-24 + +""" + +from aenum import IntEnum, extend_enum + +__all__ = ['CauseCode'] + + +class CauseCode(IntEnum): + """[CauseCode] SCTP Error Cause Codes""" + + #: Invalid Stream Identifier [:rfc:`9260`] + Invalid_Stream_Identifier = 1 + + #: Missing Mandatory Parameter [:rfc:`9260`] + Missing_Mandatory_Parameter = 2 + + #: Stale Cookie [:rfc:`9260`] + Stale_Cookie = 3 + + #: Out of Resource [:rfc:`9260`] + Out_of_Resource = 4 + + #: Unresolvable Address [:rfc:`9260`] + Unresolvable_Address = 5 + + #: Unrecognized Chunk Type [:rfc:`9260`] + Unrecognized_Chunk_Type = 6 + + #: Invalid Mandatory Parameter [:rfc:`9260`] + Invalid_Mandatory_Parameter = 7 + + #: Unrecognized Parameters [:rfc:`9260`] + Unrecognized_Parameters = 8 + + #: No User Data [:rfc:`9260`] + No_User_Data = 9 + + #: Cookie Received While Shutting Down [:rfc:`9260`] + Cookie_Received_While_Shutting_Down = 10 + + #: Restart of an Association with New Addresses [:rfc:`9260`] + Restart_of_an_Association_with_New_Addresses = 11 + + #: User-Initiated Abort [:rfc:`9260`] + User_Initiated_Abort = 12 + + #: Protocol Violation [:rfc:`9260`] + Protocol_Violation = 13 + + #: Missing DTLS Chunk Support (TEMPORARY - registered 2026-08-13, expires + #: 2027-08-13) [draft-ietf-tsvwg-sctp-dtls-chunk-04] + Missing_DTLS_Chunk_Support = 100 + + #: No Common DTLS Key Management Method (TEMPORARY - registered 2026-08-13, + #: expires 2027-08-13) [draft-ietf-tsvwg-sctp-dtls-chunk-04] + No_Common_DTLS_Key_Management_Method = 101 + + #: DTLS Key Management Tie Breaker Collision (TEMPORARY - registered + #: 2026-08-13, expires 2027-08-13) [draft-ietf-tsvwg-sctp-dtls-chunk-04] + DTLS_Key_Management_Tie_Breaker_Collision = 102 + + #: Incompatible DTLS Key Management Roles (TEMPORARY - registered 2026-08-13, + #: expires 2027-08-13) [draft-ietf-tsvwg-sctp-dtls-chunk-04] + Incompatible_DTLS_Key_Management_Roles = 103 + + #: Request to Delete Last Remaining IP Address [:rfc:`5061`] + Request_to_Delete_Last_Remaining_IP_Address = 160 + + #: Operation Refused Due to Resource Shortage [:rfc:`5061`] + Operation_Refused_Due_to_Resource_Shortage = 161 + + #: Request to Delete Source IP Address [:rfc:`5061`] + Request_to_Delete_Source_IP_Address = 162 + + #: Association Aborted due to illegal ASCONF-ACK [:rfc:`5061`] + Association_Aborted_due_to_illegal_ASCONF_ACK = 163 + + #: Request refused - no authorization [:rfc:`5061`] + Request_refused_no_authorization = 164 + + #: Unsupported HMAC Identifier [:rfc:`4895`] + Unsupported_HMAC_Identifier = 261 + + @staticmethod + def get(key: 'int | str', default: 'int' = -1) -> 'CauseCode': + """Backport support for original codes. + + Args: + key: Key to get enum item. + default: Default value if not found. + + :meta private: + """ + if isinstance(key, int): + return CauseCode(key) + if key not in CauseCode._member_map_: # pylint: disable=no-member + return extend_enum(CauseCode, key, default) + return CauseCode[key] # type: ignore[misc] + + @classmethod + def _missing_(cls, value: 'int') -> 'CauseCode': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 65535): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + if 14 <= value <= 99: + #: Unassigned + return extend_enum(cls, 'Unassigned_%d' % value, value) + if 104 <= value <= 159: + #: Unassigned + return extend_enum(cls, 'Unassigned_%d' % value, value) + if 165 <= value <= 260: + #: Unassigned + return extend_enum(cls, 'Unassigned_%d' % value, value) + if 262 <= value <= 65535: + #: Unassigned + return extend_enum(cls, 'Unassigned_%d' % value, value) + return super()._missing_(value) diff --git a/pcapkit/const/sctp/chunk.py b/pcapkit/const/sctp/chunk.py new file mode 100644 index 0000000000..1239b551b1 --- /dev/null +++ b/pcapkit/const/sctp/chunk.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""SCTP Chunk Types +====================== + +.. module:: pcapkit.const.sctp.chunk + +This module contains the constant enumeration for **SCTP Chunk Types**, +which is maintained manually against the `IANA`_ registry, as there +is currently no vendor crawler for SCTP under :mod:`pcapkit.vendor`. + +.. _IANA: https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-1 + +""" + +from aenum import IntEnum, extend_enum + +__all__ = ['Chunk'] + + +class Chunk(IntEnum): + """[Chunk] SCTP Chunk Types""" + + #: Payload Data (DATA) [:rfc:`9260`] + Payload_Data = 0 + + #: Initiation (INIT) [:rfc:`9260`] + Initiation = 1 + + #: Initiation Acknowledgement (INIT ACK) [:rfc:`9260`] + Initiation_Acknowledgement = 2 + + #: Selective Acknowledgement (SACK) [:rfc:`9260`] + Selective_Acknowledgement = 3 + + #: Heartbeat Request (HEARTBEAT) [:rfc:`9260`] + Heartbeat_Request = 4 + + #: Heartbeat Acknowledgement (HEARTBEAT ACK) [:rfc:`9260`] + Heartbeat_Acknowledgement = 5 + + #: Abort (ABORT) [:rfc:`9260`] + Abort = 6 + + #: Shutdown (SHUTDOWN) [:rfc:`9260`] + Shutdown = 7 + + #: Shutdown Acknowledgement (SHUTDOWN ACK) [:rfc:`9260`] + Shutdown_Acknowledgement = 8 + + #: Operation Error (ERROR) [:rfc:`9260`] + Operation_Error = 9 + + #: State Cookie (COOKIE ECHO) [:rfc:`9260`] + State_Cookie = 10 + + #: Cookie Acknowledgement (COOKIE ACK) [:rfc:`9260`] + Cookie_Acknowledgement = 11 + + #: Reserved for Explicit Congestion Notification Echo (ECNE) [:rfc:`9260`] + Reserved_for_Explicit_Congestion_Notification_Echo = 12 + + #: Reserved for Congestion Window Reduced (CWR) [:rfc:`9260`] + Reserved_for_Congestion_Window_Reduced = 13 + + #: Shutdown Complete (SHUTDOWN COMPLETE) [:rfc:`9260`] + Shutdown_Complete = 14 + + #: Authentication Chunk (AUTH) [:rfc:`4895`] + Authentication_Chunk = 15 + + #: Reserved for IETF-defined Chunk Extensions [:rfc:`9260`] + Reserved_for_IETF_defined_Chunk_Extensions_63 = 63 + + #: Payload Data supporting Interleaving (I-DATA) [:rfc:`8260`] + Payload_Data_supporting_Interleaving = 64 + + #: DTLS (TEMPORARY - registered 2026-02-20, expires 2027-02-20) [draft-ietf- + #: tsvwg-sctp-dtls-chunk-01] + DTLS = 65 + + #: Reserved for IETF-defined Chunk Extensions [:rfc:`9260`] + Reserved_for_IETF_defined_Chunk_Extensions_127 = 127 + + #: Address Configuration Acknowledgment (ASCONF-ACK) [:rfc:`5061`] + Address_Configuration_Acknowledgment = 128 + + #: Unassigned + Unassigned_129 = 129 + + #: Re-configuration Chunk (RE-CONFIG) [:rfc:`6525`] + Re_configuration_Chunk = 130 + + #: Unassigned + Unassigned_131 = 131 + + #: Padding Chunk (PAD) [:rfc:`4820`] + Padding_Chunk = 132 + + #: Reserved for IETF-defined Chunk Extensions [:rfc:`9260`] + Reserved_for_IETF_defined_Chunk_Extensions_191 = 191 + + #: Forward TSN [:rfc:`3758`] + Forward_TSN = 192 + + #: Address Configuration Change Chunk (ASCONF) [:rfc:`5061`] + Address_Configuration_Change_Chunk = 193 + + #: I-FORWARD-TSN [:rfc:`8260`] + I_FORWARD_TSN = 194 + + #: Reserved for IETF-defined Chunk Extensions [:rfc:`9260`] + Reserved_for_IETF_defined_Chunk_Extensions_255 = 255 + + @staticmethod + def get(key: 'int | str', default: 'int' = -1) -> 'Chunk': + """Backport support for original codes. + + Args: + key: Key to get enum item. + default: Default value if not found. + + :meta private: + """ + if isinstance(key, int): + return Chunk(key) + if key not in Chunk._member_map_: # pylint: disable=no-member + return extend_enum(Chunk, key, default) + return Chunk[key] # type: ignore[misc] + + @classmethod + def _missing_(cls, value: 'int') -> 'Chunk': + """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 16 <= value <= 62: + #: Unassigned + return extend_enum(cls, 'Unassigned_%d' % value, value) + if 66 <= value <= 126: + #: Unassigned + return extend_enum(cls, 'Unassigned_%d' % value, value) + if 133 <= value <= 190: + #: Unassigned + return extend_enum(cls, 'Unassigned_%d' % value, value) + if 195 <= value <= 254: + #: Unassigned + return extend_enum(cls, 'Unassigned_%d' % value, value) + return super()._missing_(value) diff --git a/pcapkit/const/sctp/parameter.py b/pcapkit/const/sctp/parameter.py new file mode 100644 index 0000000000..6e061352d0 --- /dev/null +++ b/pcapkit/const/sctp/parameter.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""SCTP Chunk Parameter Types +================================ + +.. module:: pcapkit.const.sctp.parameter + +This module contains the constant enumeration for **SCTP Chunk Parameter Types**, +which is maintained manually against the `IANA`_ registry, as there +is currently no vendor crawler for SCTP under :mod:`pcapkit.vendor`. + +.. _IANA: https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-2 + +""" + +from aenum import IntEnum, extend_enum + +__all__ = ['Parameter'] + + +class Parameter(IntEnum): + """[Parameter] SCTP Chunk Parameter Types""" + + #: Heartbeat Info [:rfc:`9260`] + Heartbeat_Info = 1 + + #: IPv4 Address [:rfc:`9260`] + IPv4_Address = 5 + + #: IPv6 Address [:rfc:`9260`] + IPv6_Address = 6 + + #: State Cookie [:rfc:`9260`] + State_Cookie = 7 + + #: Unrecognized Parameter [:rfc:`9260`] + Unrecognized_Parameter = 8 + + #: Cookie Preservative [:rfc:`9260`] + Cookie_Preservative = 9 + + #: Unassigned + Unassigned_10 = 10 + + #: Host Name Address [:rfc:`9260`] + Host_Name_Address = 11 + + #: Supported Address Types [:rfc:`9260`] + Supported_Address_Types = 12 + + #: Outgoing SSN Reset Request Parameter [:rfc:`6525`] + Outgoing_SSN_Reset_Request_Parameter = 13 + + #: Incoming SSN Reset Request Parameter [:rfc:`6525`] + Incoming_SSN_Reset_Request_Parameter = 14 + + #: SSN/TSN Reset Request Parameter [:rfc:`6525`] + SSN_TSN_Reset_Request_Parameter = 15 + + #: Re-configuration Response Parameter [:rfc:`6525`] + Re_configuration_Response_Parameter = 16 + + #: Add Outgoing Streams Request Parameter [:rfc:`6525`] + Add_Outgoing_Streams_Request_Parameter = 17 + + #: Add Incoming Streams Request Parameter [:rfc:`6525`] + Add_Incoming_Streams_Request_Parameter = 18 + + #: Reserved for ECN Capable (0x8000) [:rfc:`9260`] + Reserved_for_ECN_Capable = 32768 + + #: Zero Checksum Acceptable (0x8001) [:rfc:`9653`] + Zero_Checksum_Acceptable = 32769 + + #: Random (0x8002) [:rfc:`4895`] + Random = 32770 + + #: Chunk List (0x8003) [:rfc:`4895`] + Chunk_List = 32771 + + #: Requested HMAC Algorithm Parameter (0x8004) [:rfc:`4895`] + Requested_HMAC_Algorithm_Parameter = 32772 + + #: Padding (0x8005) + Padding = 32773 + + #: DTLS Key Management (0x8006) (TEMPORARY - registered 2026-02-20, expires + #: 2027-02-20) [draft-ietf-tsvwg-sctp-dtls-chunk-01] + DTLS_Key_Management = 32774 + + #: Unassigned + Unassigned_32775 = 32775 + + #: Supported Extensions (0x8008) [:rfc:`5061`] + Supported_Extensions = 32776 + + #: Forward TSN supported (0xC000) [:rfc:`3758`] + Forward_TSN_supported = 49152 + + #: Add IP Address (0xC001) [:rfc:`5061`] + Add_IP_Address = 49153 + + #: Delete IP Address (0xC002) [:rfc:`5061`] + Delete_IP_Address = 49154 + + #: Error Cause Indication (0xC003) [:rfc:`5061`] + Error_Cause_Indication = 49155 + + #: Set Primary Address (0xC004) [:rfc:`5061`] + Set_Primary_Address = 49156 + + #: Success Indication (0xC005) [:rfc:`5061`] + Success_Indication = 49157 + + #: Adaptation Layer Indication (0xC006) [:rfc:`5061`] + Adaptation_Layer_Indication = 49158 + + #: Reserved for IETF-defined Chunk Extensions [:rfc:`9260`] + Reserved_for_IETF_defined_Chunk_Extensions = 65535 + + @staticmethod + def get(key: 'int | str', default: 'int' = -1) -> 'Parameter': + """Backport support for original codes. + + Args: + key: Key to get enum item. + default: Default value if not found. + + :meta private: + """ + if isinstance(key, int): + return Parameter(key) + if key not in Parameter._member_map_: # pylint: disable=no-member + return extend_enum(Parameter, key, default) + return Parameter[key] # type: ignore[misc] + + @classmethod + def _missing_(cls, value: 'int') -> 'Parameter': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 65535): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + if 2 <= value <= 4: + #: Unassigned + return extend_enum(cls, 'Unassigned_%d' % value, value) + if 19 <= value <= 32767: + #: Unassigned + return extend_enum(cls, 'Unassigned_%d' % value, value) + if 32777 <= value <= 49151: + #: Unassigned + return extend_enum(cls, 'Unassigned_%d' % value, value) + if 49159 <= value <= 65534: + #: Unassigned + return extend_enum(cls, 'Unassigned_%d' % value, value) + return super()._missing_(value) diff --git a/pcapkit/const/sctp/payload_protocol_identifier.py b/pcapkit/const/sctp/payload_protocol_identifier.py new file mode 100644 index 0000000000..b9a784e1d5 --- /dev/null +++ b/pcapkit/const/sctp/payload_protocol_identifier.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""SCTP Payload Protocol Identifiers +======================================= + +.. module:: pcapkit.const.sctp.payload_protocol_identifier + +This module contains the constant enumeration for **SCTP Payload Protocol Identifiers**, +which is maintained manually against the `IANA`_ registry, as there +is currently no vendor crawler for SCTP under :mod:`pcapkit.vendor`. + +.. _IANA: https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-25 + +""" + +from aenum import IntEnum, extend_enum + +__all__ = ['PayloadProtocolIdentifier'] + + +class PayloadProtocolIdentifier(IntEnum): + """[PayloadProtocolIdentifier] SCTP Payload Protocol Identifiers""" + + #: Reserved by SCTP [:rfc:`9260`] + Reserved_by_SCTP = 0 + + #: IUA [:rfc:`4233`] + IUA = 1 + + #: M2UA [:rfc:`3331`] + M2UA = 2 + + #: M3UA [:rfc:`4666`] + M3UA = 3 + + #: SUA [:rfc:`3868`] + SUA = 4 + + #: M2PA [:rfc:`4165`] + M2PA = 5 + + #: V5UA [:rfc:`3807`] + V5UA = 6 + + #: H.248 [ITU-T Recommendation H.248 Annex H, "Transport over SCTP",November + #: 2000.] + H_248 = 7 + + #: BICC/Q.2150.3 [ITU-T Recommendation Q.1902.1, "Bearer Independent + #: CallControl protocol (Capability Set 2): Functional description",July + #: 2001.][ITU-T Recommendation Q.2150.3, "Signalling Transport ConverterOn + #: SCTP", to be published.] + BICC_Q_2150_3 = 8 + + #: TALI [:rfc:`3094`] + TALI = 9 + + #: DUA [:rfc:`4129`] + DUA = 10 + + #: ASAP [:rfc:`5352`] + ASAP = 11 + + #: ENRP [:rfc:`5353`] + ENRP = 12 + + #: H.323 [http://standard.pictel.com/ftp/avc-site/0206 Bru/AVD-2198.zip][H.323 + #: over SCTP October 2002.] + H_323 = 13 + + #: Q.IPC/Q.2150.3 [ITU-T Recommendation Q.2631.1 "IP Connection Control + #: SignalingProtocol - Capability Set 1", to be published.][ITU-T + #: Recommendation Q.2150.3, "Signalling Transport ConverterOn SCTP", to be + #: published.] + Q_IPC_Q_2150_3 = 14 + + #: SIMCO [draft-kiesel-midcom-simco- + #: sctp-00][Sebastian Kiesel] + SIMCO_draft_kiesel_midcom_simco_sctp_00_txt = 15 + + #: DDP Segment Chunk [:rfc:`5043`] + DDP_Segment_Chunk = 16 + + #: DDP Stream Session Control [:rfc:`5043`] + DDP_Stream_Session_Control = 17 + + #: S1 Application Protocol (S1AP) [3GPP TS 23.401][3GPP TS 36.413][Rajeev + #: Koodli] + S1_Application_Protocol = 18 + + #: RUA [3GPP TS 25.467][3GPP TS 25.468][Dongwook Kim] + RUA = 19 + + #: HNBAP [3GPP TS 25.467][3GPP TS 25.469][Dongwook Kim] + HNBAP = 20 + + #: ForCES-HP [:rfc:`5811`] + ForCES_HP = 21 + + #: ForCES-MP [:rfc:`5811`] + ForCES_MP = 22 + + #: ForCES-LP [:rfc:`5811`] + ForCES_LP = 23 + + #: SBc-AP [3GPP TS 29.168][Kimmo Kymalainen] + SBc_AP = 24 + + #: NBAP [3GPP TS 25.433][Kimmo Kymalainen] + NBAP = 25 + + #: Unassigned + Unassigned_26 = 26 + + #: X2AP [3GPP TS 36.423][Kimmo Kymalainen] + X2AP = 27 + + #: IRCP - Inter Router Capability Protocol [Randall Stewart] + IRCP_Inter_Router_Capability_Protocol = 28 + + #: LCS-AP [3GPP TS 29.271][Kimmo Kymalainen] + LCS_AP = 29 + + #: MPICH2 [Michael Tuexen][http://www.mcs.anl.gov/research/projects/mpich2/] + MPICH2 = 30 + + #: Service Area Broadcast Protocol (SABP) [3GPP TS 25.467][3GPP TS + #: 25.419][Dongwook Kim] + Service_Area_Broadcast_Protocol = 31 + + #: Fractal Generator Protocol (FGP) [Thomas + #: Dreibholz][https://www.nntb.no/~dreibh/rserpool/] + Fractal_Generator_Protocol = 32 + + #: Ping Pong Protocol (PPP) [Thomas + #: Dreibholz][https://www.nntb.no/~dreibh/rserpool/] + Ping_Pong_Protocol = 33 + + #: CalcApp Protocol (CALCAPP) [Thomas + #: Dreibholz][https://www.nntb.no/~dreibh/rserpool/] + CalcApp_Protocol = 34 + + #: Scripting Service Protocol (SSP) [Thomas + #: Dreibholz][https://www.nntb.no/~dreibh/rserpool/] + Scripting_Service_Protocol = 35 + + #: NetPerfMeter Protocol Control Channel (NPMP-CONTROL) [Thomas + #: Dreibholz][https://www.nntb.no/~dreibh/netperfmeter/] + NetPerfMeter_Protocol_Control_Channel = 36 + + #: NetPerfMeter Protocol Data Channel (NPMP-DATA) [Thomas + #: Dreibholz][https://www.nntb.no/~dreibh/netperfmeter/] + NetPerfMeter_Protocol_Data_Channel = 37 + + #: Echo (ECHO) [Thomas Dreibholz][https://www.nntb.no/~dreibh/rserpool/] + Echo = 38 + + #: Discard (DISCARD) [Thomas Dreibholz][https://www.nntb.no/~dreibh/rserpool/] + Discard = 39 + + #: Daytime (DAYTIME) [Thomas Dreibholz][https://www.nntb.no/~dreibh/rserpool/] + Daytime = 40 + + #: Character Generator (CHARGEN) [Thomas + #: Dreibholz][https://www.nntb.no/~dreibh/rserpool/] + Character_Generator = 41 + + #: 3GPP RNA [Tonesi][3GPP TS 25.471] + PayloadProtocolIdentifier_3GPP_RNA = 42 + + #: 3GPP M2AP [Tonesi][3GPP TS 36.442][3GPP TS 36.443] + PayloadProtocolIdentifier_3GPP_M2AP = 43 + + #: 3GPP M3AP [Tonesi][3GPP TS 36.442][3GPP TS 36.444] + PayloadProtocolIdentifier_3GPP_M3AP = 44 + + #: SSH over SCTP [Michael Tuexen] + SSH_over_SCTP = 45 + + #: Diameter in a SCTP DATA chunk [:rfc:`6733`] + Diameter_in_a_SCTP_DATA_chunk = 46 + + #: Diameter in a DTLS/SCTP DATA chunk [:rfc:`6733`] + Diameter_in_a_DTLS_SCTP_DATA_chunk = 47 + + #: R14P. BER Encoded ASN.1 over SCTP [Josip + #: Djuricic][http://www.release14.org/wp-content/uploads/2012/07/r14p.asn] + R14P_BER_Encoded_ASN_1_over_SCTP = 48 + + #: Generic Data Transfer (GDT) Protocol [Damir + #: Franusic][https://github.com/link-mink] + Generic_Data_Transfer_Protocol = 49 + + #: WebRTC DCEP [:rfc:`8832`] + WebRTC_DCEP = 50 + + #: WebRTC String [:rfc:`8831`] + WebRTC_String = 51 + + #: WebRTC Binary Partial (deprecated) [:rfc:`8831`] + WebRTC_Binary_Partial = 52 + + #: WebRTC Binary [:rfc:`8831`] + WebRTC_Binary = 53 + + #: WebRTC String Partial (deprecated) [:rfc:`8831`] + WebRTC_String_Partial = 54 + + #: 3GPP PUA [Dario S Tonesi][3GPP TS 25.470][3GPP TS 25.467] + PayloadProtocolIdentifier_3GPP_PUA = 55 + + #: WebRTC String Empty [:rfc:`8831`] + WebRTC_String_Empty = 56 + + #: WebRTC Binary Empty [:rfc:`8831`] + WebRTC_Binary_Empty = 57 + + #: 3GPP XwAP [ 3GPP TS 36.462][KIMBA DIT ADAMOU Boubacar] + PayloadProtocolIdentifier_3GPP_XwAP = 58 + + #: 3GPP Xw-Control Plane [ 3GPP TS 36.462][KIMBA DIT ADAMOU Boubacar] + PayloadProtocolIdentifier_3GPP_Xw_Control_Plane = 59 + + #: 3GPP NG Application Protocol (NGAP) [ 3GPP TS 38.413][Luis Lopes] + PayloadProtocolIdentifier_3GPP_NG_Application_Protocol = 60 + + #: 3GPP Xn Application Protocol (XnAP) [ 3GPP TS 38.423][Luis Lopes] + PayloadProtocolIdentifier_3GPP_Xn_Application_Protocol = 61 + + #: 3GPP F1 Application Protocol (F1 AP) [ 3GPP TS 38.473][Luis Lopes] + PayloadProtocolIdentifier_3GPP_F1_Application_Protocol = 62 + + #: HTTP/SCTP [Michael Tuexen] + HTTP_SCTP = 63 + + #: 3GPP E1 Application Protocol (E1AP) [ 3GPP TS 38.463][Yang Xudong] + PayloadProtocolIdentifier_3GPP_E1_Application_Protocol = 64 + + #: ELE2 Lawful Interception [http://ele2.io][Damir Franusic] + ELE2_Lawful_Interception = 65 + + #: 3GPP NGAP over DTLS over SCTP [ 3GPP TS 38.413][Yang Xudong] + PayloadProtocolIdentifier_3GPP_NGAP_over_DTLS_over_SCTP = 66 + + #: 3GPP XnAP over DTLS over SCTP [ 3GPP TS 38.423][Yang Xudong] + PayloadProtocolIdentifier_3GPP_XnAP_over_DTLS_over_SCTP = 67 + + #: 3GPP F1AP over DTLS over SCTP [ 3GPP TS 38.473][Yang Xudong] + PayloadProtocolIdentifier_3GPP_F1AP_over_DTLS_over_SCTP = 68 + + #: 3GPP E1AP over DTLS over SCTP [ 3GPP TS 38.463][Yang Xudong] + PayloadProtocolIdentifier_3GPP_E1AP_over_DTLS_over_SCTP = 69 + + #: E2-CP [O-RAN Alliance][Jun Hyuk Song] + E2_CP = 70 + + #: O-RAN D2 [O-RAN Alliance][Jun Hyuk Song] + O_RAN_D2 = 71 + + #: E2-DU [O-RAN Alliance][Jun Hyuk Song] + E2_DU = 72 + + #: 3GPP W1AP [3GPP TS 37.473][Lionel Morand] + PayloadProtocolIdentifier_3GPP_W1AP = 73 + + #: DTLS Chunk Key-Management Messages [draft-westerlund-tsvwg-sctp-dtls- + #: chunk-01] + DTLS_Chunk_Key_Management_Messages = 4242 + + @staticmethod + def get(key: 'int | str', default: 'int' = -1) -> 'PayloadProtocolIdentifier': + """Backport support for original codes. + + Args: + key: Key to get enum item. + default: Default value if not found. + + :meta private: + """ + if isinstance(key, int): + return PayloadProtocolIdentifier(key) + if key not in PayloadProtocolIdentifier._member_map_: # pylint: disable=no-member + return extend_enum(PayloadProtocolIdentifier, key, default) + return PayloadProtocolIdentifier[key] # type: ignore[misc] + + @classmethod + def _missing_(cls, value: 'int') -> 'PayloadProtocolIdentifier': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 4294967295): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + if 74 <= value <= 4241: + #: Unassigned + return extend_enum(cls, 'Unassigned_%d' % value, value) + if 4243 <= value <= 4294967295: + #: Unassigned + return extend_enum(cls, 'Unassigned_%d' % value, value) + return super()._missing_(value) diff --git a/pcapkit/foundation/__init__.py b/pcapkit/foundation/__init__.py index 8ef5008cdb..1ec7115251 100644 --- a/pcapkit/foundation/__init__.py +++ b/pcapkit/foundation/__init__.py @@ -33,7 +33,7 @@ 'register_ipv6_opts_option', 'register_ipv6_route_routing', 'register_mh_message', 'register_mh_option', 'register_mh_extension', 'register_apptype', - 'register_tcp', 'register_udp', + 'register_tcp', 'register_udp', 'register_sctp', 'register_tcp_option', 'register_tcp_mp_option', 'register_http_frame', 'register_pcapng_block', 'register_pcapng_option', 'register_pcapng_secrets', diff --git a/pcapkit/foundation/registry/__init__.py b/pcapkit/foundation/registry/__init__.py index 5c149ed959..0d9baad0f3 100644 --- a/pcapkit/foundation/registry/__init__.py +++ b/pcapkit/foundation/registry/__init__.py @@ -31,7 +31,7 @@ 'register_mh_message', 'register_mh_option', 'register_mh_extension', 'register_apptype', - 'register_tcp', 'register_udp', + 'register_tcp', 'register_udp', 'register_sctp', 'register_tcp_option', 'register_tcp_mp_option', 'register_http_frame', diff --git a/pcapkit/foundation/registry/protocols.py b/pcapkit/foundation/registry/protocols.py index 7fba0b273e..691d8b5917 100644 --- a/pcapkit/foundation/registry/protocols.py +++ b/pcapkit/foundation/registry/protocols.py @@ -42,6 +42,7 @@ from pcapkit.protocols.schema.misc.pcapng import Option as Schema_PCAPNG_Option from pcapkit.protocols.schema.transport.tcp import MPTCP as Schema_TCP_MPTCP from pcapkit.protocols.schema.transport.tcp import Option as Schema_TCP_Option +from pcapkit.protocols.transport.sctp import SCTP from pcapkit.protocols.transport.tcp import TCP from pcapkit.protocols.transport.udp import UDP from pcapkit.utilities.exceptions import RegistryError @@ -65,6 +66,8 @@ from pcapkit.const.reg.ethertype import EtherType from pcapkit.const.reg.linktype import LinkType from pcapkit.const.reg.transtype import TransType + from pcapkit.const.sctp.payload_protocol_identifier import \ + PayloadProtocolIdentifier as SCTP_PayloadProtocolIdentifier from pcapkit.const.tcp.mp_tcp_option import MPTCPOption as TCP_MPTCPOption from pcapkit.const.tcp.option import Option as TCP_Option from pcapkit.protocols.application.httpv2 import FrameConstructor as HTTP_FrameConstructor @@ -113,7 +116,7 @@ 'register_mh_message', 'register_mh_option', 'register_mh_extension', 'register_apptype', - 'register_tcp', 'register_udp', + 'register_tcp', 'register_udp', 'register_sctp', 'register_tcp_option', 'register_tcp_mp_option', 'register_http_frame', @@ -585,9 +588,20 @@ def register_apptype(code: 'int | Enum_AppType', module: 'str | ModuleDescriptor class\_: class name proto: protocol name (must be a valid transport protocol) + Important: + :class:`~pcapkit.protocols.transport.sctp.SCTP` is deliberately **not** + part of this fan-out, even for application types that name ``sctp`` in + their :class:`~pcapkit.const.reg.apptype.TransportProtocol`. Its + :data:`~pcapkit.protocols.transport.sctp.SCTP.__proto__` registry is + keyed by the DATA chunk's payload protocol identifier rather than by + port number, so writing a port number into it would dispatch on a + number from the wrong registry. Use + :func:`pcapkit.foundation.registry.register_sctp` instead. + See Also: * :func:`pcapkit.foundation.registry.register_tcp` * :func:`pcapkit.foundation.registry.register_udp` + * :func:`pcapkit.foundation.registry.register_sctp` """ if isinstance(code, Enum_AppType): @@ -749,6 +763,50 @@ def register_udp(code: 'int | Enum_AppType', module: 'str | ModuleDescriptor[Pro register_protocol(module) +@overload +def register_sctp(code: 'int | SCTP_PayloadProtocolIdentifier', module: 'ModuleDescriptor[Protocol] | Type[Protocol]') -> 'None': ... +@overload +def register_sctp(code: 'int | SCTP_PayloadProtocolIdentifier', module: 'str', class_: 'str') -> 'None': ... + + +# NOTE: pcapkit.protocols.transport.sctp.SCTP.__proto__ +def register_sctp(code: 'int | SCTP_PayloadProtocolIdentifier', module: 'str | ModuleDescriptor[Protocol] | Type[Protocol]', + class_: 'str' = NULL) -> 'None': + r"""Register a new protocol class. + + Notes: + The full qualified class name of the new protocol class + should be as ``{module}.{class_}``. + + The function will register the given protocol class to the + :data:`pcapkit.protocols.transport.sctp.SCTP.__proto__` registry. + + Arguments: + code: payload protocol identifier (PPID), as in + :class:`~pcapkit.const.sctp.payload_protocol_identifier.PayloadProtocolIdentifier` + module: module name or module descriptor or a + :class:`~pcapkit.protocols.protocol.Protocol` subclass + class\_: class name + + Important: + Unlike :func:`register_tcp` and :func:`register_udp`, ``code`` is a + *payload protocol identifier* taken from the DATA chunk, **not** a port + number: SCTP names its upper layer per DATA chunk rather than per + association. See :rfc:`9260#section-3.3.1`. + + """ + if isinstance(module, str): + module = cast('ModuleDescriptor[Protocol]', ModuleDescriptor(module, class_)) + + SCTP.register(code, module) + logger.info('registered SCTP payload protocol identifier: %s', code) + + # register protocol to protocol registry + if isinstance(module, ModuleDescriptor): + module = module.klass + register_protocol(module) + + ############################################################################### # Application Layer Registries ############################################################################### diff --git a/pcapkit/protocols/__init__.py b/pcapkit/protocols/__init__.py index f1fc034f54..e0f00ab3b6 100644 --- a/pcapkit/protocols/__init__.py +++ b/pcapkit/protocols/__init__.py @@ -61,7 +61,7 @@ 'IPv6_Route', 'MH', # Transport Layer - 'TCP', 'UDP', + 'TCP', 'UDP', 'SCTP', # Application Layer 'FTP', 'FTP_DATA', diff --git a/pcapkit/protocols/data/__init__.py b/pcapkit/protocols/data/__init__.py index 3ac7ebff41..c5da5bb591 100644 --- a/pcapkit/protocols/data/__init__.py +++ b/pcapkit/protocols/data/__init__.py @@ -163,6 +163,28 @@ 'TCP_MPTCPJoin', 'TCP_MPTCPJoinSYN', 'TCP_MPTCPJoinSYNACK', 'TCP_MPTCPJoinACK', + # Stream Control Transmission Protocol + 'SCTP', + 'SCTP_DATAChunkFlags', 'SCTP_TBitFlags', 'SCTP_GapAckBlock', + 'SCTP_Chunk', + 'SCTP_UnknownChunk', 'SCTP_DATAChunk', 'SCTP_INITChunk', 'SCTP_INITACKChunk', 'SCTP_SACKChunk', + 'SCTP_HeartbeatChunk', 'SCTP_HeartbeatACKChunk', 'SCTP_AbortChunk', 'SCTP_ShutdownChunk', + 'SCTP_ShutdownACKChunk', 'SCTP_ErrorChunk', 'SCTP_CookieEchoChunk', 'SCTP_CookieACKChunk', + 'SCTP_ShutdownCompleteChunk', + 'SCTP_Parameter', + 'SCTP_UnknownParameter', 'SCTP_HeartbeatInfoParameter', 'SCTP_IPv4AddressParameter', + 'SCTP_IPv6AddressParameter', 'SCTP_StateCookieParameter', 'SCTP_UnrecognizedParameter', + 'SCTP_CookiePreservativeParameter', 'SCTP_HostNameAddressParameter', + 'SCTP_SupportedAddressTypesParameter', + 'SCTP_ErrorCause', + 'SCTP_UnknownCause', 'SCTP_InvalidStreamIdentifierCause', + 'SCTP_MissingMandatoryParameterCause', 'SCTP_StaleCookieCause', 'SCTP_OutOfResourceCause', + 'SCTP_UnresolvableAddressCause', 'SCTP_UnrecognizedChunkTypeCause', + 'SCTP_InvalidMandatoryParameterCause', 'SCTP_UnrecognizedParametersCause', + 'SCTP_NoUserDataCause', 'SCTP_CookieReceivedWhileShuttingDownCause', + 'SCTP_RestartOfAnAssociationWithNewAddressesCause', 'SCTP_UserInitiatedAbortCause', + 'SCTP_ProtocolViolationCause', + # User Datagram Protocol 'UDP', diff --git a/pcapkit/protocols/data/transport/__init__.py b/pcapkit/protocols/data/transport/__init__.py index 933d554fae..51f29e8475 100644 --- a/pcapkit/protocols/data/transport/__init__.py +++ b/pcapkit/protocols/data/transport/__init__.py @@ -46,6 +46,71 @@ from pcapkit.protocols.data.transport.tcp import UserTimeout as TCP_UserTimeout from pcapkit.protocols.data.transport.tcp import WindowScale as TCP_WindowScale +# Stream Control Transmission Protocol +from pcapkit.protocols.data.transport.sctp import SCTP +from pcapkit.protocols.data.transport.sctp import AbortChunk as SCTP_AbortChunk +from pcapkit.protocols.data.transport.sctp import Chunk as SCTP_Chunk +from pcapkit.protocols.data.transport.sctp import CookieACKChunk as SCTP_CookieACKChunk +from pcapkit.protocols.data.transport.sctp import CookieEchoChunk as SCTP_CookieEchoChunk +from pcapkit.protocols.data.transport.sctp import \ + CookiePreservativeParameter as SCTP_CookiePreservativeParameter +from pcapkit.protocols.data.transport.sctp import \ + CookieReceivedWhileShuttingDownCause as SCTP_CookieReceivedWhileShuttingDownCause +from pcapkit.protocols.data.transport.sctp import DATAChunk as SCTP_DATAChunk +from pcapkit.protocols.data.transport.sctp import DATAChunkFlags as SCTP_DATAChunkFlags +from pcapkit.protocols.data.transport.sctp import ErrorCause as SCTP_ErrorCause +from pcapkit.protocols.data.transport.sctp import ErrorChunk as SCTP_ErrorChunk +from pcapkit.protocols.data.transport.sctp import GapAckBlock as SCTP_GapAckBlock +from pcapkit.protocols.data.transport.sctp import HeartbeatACKChunk as SCTP_HeartbeatACKChunk +from pcapkit.protocols.data.transport.sctp import HeartbeatChunk as SCTP_HeartbeatChunk +from pcapkit.protocols.data.transport.sctp import \ + HeartbeatInfoParameter as SCTP_HeartbeatInfoParameter +from pcapkit.protocols.data.transport.sctp import \ + HostNameAddressParameter as SCTP_HostNameAddressParameter +from pcapkit.protocols.data.transport.sctp import INITACKChunk as SCTP_INITACKChunk +from pcapkit.protocols.data.transport.sctp import INITChunk as SCTP_INITChunk +from pcapkit.protocols.data.transport.sctp import \ + InvalidMandatoryParameterCause as SCTP_InvalidMandatoryParameterCause +from pcapkit.protocols.data.transport.sctp import \ + InvalidStreamIdentifierCause as SCTP_InvalidStreamIdentifierCause +from pcapkit.protocols.data.transport.sctp import \ + IPv4AddressParameter as SCTP_IPv4AddressParameter +from pcapkit.protocols.data.transport.sctp import \ + IPv6AddressParameter as SCTP_IPv6AddressParameter +from pcapkit.protocols.data.transport.sctp import \ + MissingMandatoryParameterCause as SCTP_MissingMandatoryParameterCause +from pcapkit.protocols.data.transport.sctp import NoUserDataCause as SCTP_NoUserDataCause +from pcapkit.protocols.data.transport.sctp import OutOfResourceCause as SCTP_OutOfResourceCause +from pcapkit.protocols.data.transport.sctp import Parameter as SCTP_Parameter +from pcapkit.protocols.data.transport.sctp import \ + ProtocolViolationCause as SCTP_ProtocolViolationCause +from pcapkit.protocols.data.transport.sctp import \ + RestartOfAnAssociationWithNewAddressesCause as \ + SCTP_RestartOfAnAssociationWithNewAddressesCause +from pcapkit.protocols.data.transport.sctp import SACKChunk as SCTP_SACKChunk +from pcapkit.protocols.data.transport.sctp import ShutdownACKChunk as SCTP_ShutdownACKChunk +from pcapkit.protocols.data.transport.sctp import ShutdownChunk as SCTP_ShutdownChunk +from pcapkit.protocols.data.transport.sctp import \ + ShutdownCompleteChunk as SCTP_ShutdownCompleteChunk +from pcapkit.protocols.data.transport.sctp import StaleCookieCause as SCTP_StaleCookieCause +from pcapkit.protocols.data.transport.sctp import TBitFlags as SCTP_TBitFlags +from pcapkit.protocols.data.transport.sctp import \ + StateCookieParameter as SCTP_StateCookieParameter +from pcapkit.protocols.data.transport.sctp import \ + SupportedAddressTypesParameter as SCTP_SupportedAddressTypesParameter +from pcapkit.protocols.data.transport.sctp import UnknownCause as SCTP_UnknownCause +from pcapkit.protocols.data.transport.sctp import UnknownChunk as SCTP_UnknownChunk +from pcapkit.protocols.data.transport.sctp import UnknownParameter as SCTP_UnknownParameter +from pcapkit.protocols.data.transport.sctp import \ + UnrecognizedChunkTypeCause as SCTP_UnrecognizedChunkTypeCause +from pcapkit.protocols.data.transport.sctp import \ + UnrecognizedParameter as SCTP_UnrecognizedParameter +from pcapkit.protocols.data.transport.sctp import \ + UnrecognizedParametersCause as SCTP_UnrecognizedParametersCause +from pcapkit.protocols.data.transport.sctp import \ + UnresolvableAddressCause as SCTP_UnresolvableAddressCause +from pcapkit.protocols.data.transport.sctp import \ + UserInitiatedAbortCause as SCTP_UserInitiatedAbortCause # User Datagram Protocol from pcapkit.protocols.data.transport.udp import UDP @@ -66,6 +131,28 @@ 'TCP_MPTCPJoin', 'TCP_MPTCPJoinSYN', 'TCP_MPTCPJoinSYNACK', 'TCP_MPTCPJoinACK', + # Stream Control Transmission Protocol + 'SCTP', + 'SCTP_DATAChunkFlags', 'SCTP_TBitFlags', 'SCTP_GapAckBlock', + 'SCTP_Chunk', + 'SCTP_UnknownChunk', 'SCTP_DATAChunk', 'SCTP_INITChunk', 'SCTP_INITACKChunk', 'SCTP_SACKChunk', + 'SCTP_HeartbeatChunk', 'SCTP_HeartbeatACKChunk', 'SCTP_AbortChunk', 'SCTP_ShutdownChunk', + 'SCTP_ShutdownACKChunk', 'SCTP_ErrorChunk', 'SCTP_CookieEchoChunk', 'SCTP_CookieACKChunk', + 'SCTP_ShutdownCompleteChunk', + 'SCTP_Parameter', + 'SCTP_UnknownParameter', 'SCTP_HeartbeatInfoParameter', 'SCTP_IPv4AddressParameter', + 'SCTP_IPv6AddressParameter', 'SCTP_StateCookieParameter', 'SCTP_UnrecognizedParameter', + 'SCTP_CookiePreservativeParameter', 'SCTP_HostNameAddressParameter', + 'SCTP_SupportedAddressTypesParameter', + 'SCTP_ErrorCause', + 'SCTP_UnknownCause', 'SCTP_InvalidStreamIdentifierCause', + 'SCTP_MissingMandatoryParameterCause', 'SCTP_StaleCookieCause', 'SCTP_OutOfResourceCause', + 'SCTP_UnresolvableAddressCause', 'SCTP_UnrecognizedChunkTypeCause', + 'SCTP_InvalidMandatoryParameterCause', 'SCTP_UnrecognizedParametersCause', + 'SCTP_NoUserDataCause', 'SCTP_CookieReceivedWhileShuttingDownCause', + 'SCTP_RestartOfAnAssociationWithNewAddressesCause', 'SCTP_UserInitiatedAbortCause', + 'SCTP_ProtocolViolationCause', + # User Datagram Protocol 'UDP', ] diff --git a/pcapkit/protocols/data/transport/sctp.py b/pcapkit/protocols/data/transport/sctp.py new file mode 100644 index 0000000000..5df0a08211 --- /dev/null +++ b/pcapkit/protocols/data/transport/sctp.py @@ -0,0 +1,574 @@ +# -*- coding: utf-8 -*- +"""data model for SCTP protocol""" + +from typing import TYPE_CHECKING + +from pcapkit.corekit.infoclass import info_final +from pcapkit.protocols.data.data import Data +from pcapkit.protocols.data.protocol import Protocol + +if TYPE_CHECKING: + from ipaddress import IPv4Address, IPv6Address + from typing import Union + + from pcapkit.const.reg.apptype import AppType + from pcapkit.const.sctp.cause_code import CauseCode + from pcapkit.const.sctp.chunk import Chunk as ChunkType + from pcapkit.const.sctp.parameter import Parameter as ParameterType + from pcapkit.const.sctp.payload_protocol_identifier import PayloadProtocolIdentifier + from pcapkit.corekit.multidict import OrderedMultiDict + + IPAddress = Union[IPv4Address, IPv6Address] + +__all__ = [ + 'SCTP', + + 'DATAChunkFlags', 'TBitFlags', 'GapAckBlock', + + 'Chunk', + 'UnknownChunk', 'DATAChunk', 'INITChunk', 'INITACKChunk', 'SACKChunk', + 'HeartbeatChunk', 'HeartbeatACKChunk', 'AbortChunk', 'ShutdownChunk', + 'ShutdownACKChunk', 'ErrorChunk', 'CookieEchoChunk', 'CookieACKChunk', + 'ShutdownCompleteChunk', + + 'Parameter', + 'UnknownParameter', 'HeartbeatInfoParameter', 'IPv4AddressParameter', + 'IPv6AddressParameter', 'StateCookieParameter', 'UnrecognizedParameter', + 'CookiePreservativeParameter', 'HostNameAddressParameter', + 'SupportedAddressTypesParameter', + + 'ErrorCause', + 'UnknownCause', 'InvalidStreamIdentifierCause', 'MissingMandatoryParameterCause', + 'StaleCookieCause', 'OutOfResourceCause', 'UnresolvableAddressCause', + 'UnrecognizedChunkTypeCause', 'InvalidMandatoryParameterCause', + 'UnrecognizedParametersCause', 'NoUserDataCause', + 'CookieReceivedWhileShuttingDownCause', + 'RestartOfAnAssociationWithNewAddressesCause', 'UserInitiatedAbortCause', + 'ProtocolViolationCause', +] + + +@info_final +class DATAChunkFlags(Data): + """Data model for SCTP DATA chunk flags.""" + + #: (I)mmediate bit, i.e., request a SACK chunk without delay. + I: 'bool' + #: (U)nordered bit, i.e., no stream sequence number is assigned. + U: 'bool' + #: (B)eginning fragment bit. + B: 'bool' + #: (E)nding fragment bit. + E: 'bool' + + if TYPE_CHECKING: + def __init__(self, I: 'bool', U: 'bool', B: 'bool', E: 'bool') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class TBitFlags(Data): + """Data model for SCTP chunk flags carrying only the T bit, i.e., ABORT and + SHUTDOWN COMPLETE chunks.""" + + #: T bit, i.e., the verification tag has been reflected. + T: 'bool' + + if TYPE_CHECKING: + def __init__(self, T: 'bool') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class GapAckBlock(Data): + """Data model for SCTP SACK chunk gap ack blocks.""" + + #: Start offset TSN of the gap ack block. + start: 'int' + #: End offset TSN of the gap ack block. + end: 'int' + + if TYPE_CHECKING: + def __init__(self, start: 'int', end: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +class ErrorCause(Data): + """Data model for SCTP error causes.""" + + #: Cause code. + code: 'CauseCode' + #: Cause length. + length: 'int' + + +@info_final +class UnknownCause(ErrorCause): + """Data model for SCTP error causes with unknown cause codes.""" + + #: Cause-specific information. + value: 'bytes' + + if TYPE_CHECKING: + def __init__(self, code: 'CauseCode', length: 'int', value: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class InvalidStreamIdentifierCause(ErrorCause): + """Data model for SCTP invalid stream identifier error cause.""" + + #: Stream identifier of the offending DATA chunk. + stream_id: 'int' + + if TYPE_CHECKING: + def __init__(self, code: 'CauseCode', length: 'int', stream_id: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class MissingMandatoryParameterCause(ErrorCause): + """Data model for SCTP missing mandatory parameter error cause.""" + + #: Number of missing parameters. + num: 'int' + #: Missing parameter types. + types: 'tuple[ParameterType, ...]' + + if TYPE_CHECKING: + def __init__(self, code: 'CauseCode', length: 'int', num: 'int', types: 'tuple[ParameterType, ...]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class StaleCookieCause(ErrorCause): + """Data model for SCTP stale cookie error cause.""" + + #: Measure of staleness, in microseconds. + staleness: 'int' + + if TYPE_CHECKING: + def __init__(self, code: 'CauseCode', length: 'int', staleness: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class OutOfResourceCause(ErrorCause): + """Data model for SCTP out of resource error cause.""" + + if TYPE_CHECKING: + def __init__(self, code: 'CauseCode', length: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class UnresolvableAddressCause(ErrorCause): + """Data model for SCTP unresolvable address error cause.""" + + #: The offending address parameter, complete with its type and length. + value: 'bytes' + + if TYPE_CHECKING: + def __init__(self, code: 'CauseCode', length: 'int', value: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class UnrecognizedChunkTypeCause(ErrorCause): + """Data model for SCTP unrecognized chunk type error cause.""" + + #: The unrecognized chunk, complete with its type, flags and length. + value: 'bytes' + + if TYPE_CHECKING: + def __init__(self, code: 'CauseCode', length: 'int', value: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class InvalidMandatoryParameterCause(ErrorCause): + """Data model for SCTP invalid mandatory parameter error cause.""" + + if TYPE_CHECKING: + def __init__(self, code: 'CauseCode', length: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class UnrecognizedParametersCause(ErrorCause): + """Data model for SCTP unrecognized parameters error cause.""" + + #: The unrecognized parameters, complete with their types and lengths. + value: 'bytes' + + if TYPE_CHECKING: + def __init__(self, code: 'CauseCode', length: 'int', value: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class NoUserDataCause(ErrorCause): + """Data model for SCTP no user data error cause.""" + + #: TSN of the offending DATA chunk. + tsn: 'int' + + if TYPE_CHECKING: + def __init__(self, code: 'CauseCode', length: 'int', tsn: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class CookieReceivedWhileShuttingDownCause(ErrorCause): + """Data model for SCTP cookie received while shutting down error cause.""" + + if TYPE_CHECKING: + def __init__(self, code: 'CauseCode', length: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class RestartOfAnAssociationWithNewAddressesCause(ErrorCause): + """Data model for SCTP restart of an association with new addresses error cause.""" + + #: The new address parameters, complete with their types and lengths. + value: 'bytes' + + if TYPE_CHECKING: + def __init__(self, code: 'CauseCode', length: 'int', value: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class UserInitiatedAbortCause(ErrorCause): + """Data model for SCTP user-initiated abort error cause.""" + + #: Upper layer abort reason. + info: 'bytes' + + if TYPE_CHECKING: + def __init__(self, code: 'CauseCode', length: 'int', info: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class ProtocolViolationCause(ErrorCause): + """Data model for SCTP protocol violation error cause.""" + + #: Additional information. + info: 'bytes' + + if TYPE_CHECKING: + def __init__(self, code: 'CauseCode', length: 'int', info: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +class Parameter(Data): + """Data model for SCTP chunk parameters.""" + + #: Parameter type. + type: 'ParameterType' + #: Parameter length. + length: 'int' + + +@info_final +class UnknownParameter(Parameter): + """Data model for SCTP chunk parameters with unknown types.""" + + #: Parameter value. + value: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'ParameterType', length: 'int', value: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class HeartbeatInfoParameter(Parameter): + """Data model for SCTP heartbeat info parameter.""" + + #: Sender-specific heartbeat info. + info: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'ParameterType', length: 'int', info: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class IPv4AddressParameter(Parameter): + """Data model for SCTP IPv4 address parameter.""" + + #: IPv4 address of the sending endpoint. + address: 'IPv4Address' + + if TYPE_CHECKING: + def __init__(self, type: 'ParameterType', length: 'int', address: 'IPv4Address') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class IPv6AddressParameter(Parameter): + """Data model for SCTP IPv6 address parameter.""" + + #: IPv6 address of the sending endpoint. + address: 'IPv6Address' + + if TYPE_CHECKING: + def __init__(self, type: 'ParameterType', length: 'int', address: 'IPv6Address') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class StateCookieParameter(Parameter): + """Data model for SCTP state cookie parameter.""" + + #: State cookie. + cookie: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'ParameterType', length: 'int', cookie: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class UnrecognizedParameter(Parameter): + """Data model for SCTP unrecognized parameter parameter.""" + + #: The unrecognized parameter, complete with its type and length. + value: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'ParameterType', length: 'int', value: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class CookiePreservativeParameter(Parameter): + """Data model for SCTP cookie preservative parameter.""" + + #: Suggested cookie life-span increment, in milliseconds. + increment: 'int' + + if TYPE_CHECKING: + def __init__(self, type: 'ParameterType', length: 'int', increment: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class HostNameAddressParameter(Parameter): + """Data model for SCTP host name address parameter.""" + + #: Host name, including at least one null terminator. + name: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'ParameterType', length: 'int', name: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class SupportedAddressTypesParameter(Parameter): + """Data model for SCTP supported address types parameter.""" + + #: Supported address types, given as address parameter types. + types: 'tuple[ParameterType, ...]' + + if TYPE_CHECKING: + def __init__(self, type: 'ParameterType', length: 'int', types: 'tuple[ParameterType, ...]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +class Chunk(Data): + """Data model for SCTP chunks.""" + + #: Chunk type. + type: 'ChunkType' + #: Chunk length, excluding any trailing padding. + length: 'int' + + +@info_final +class UnknownChunk(Chunk): + """Data model for SCTP chunks with unknown types.""" + + #: Raw chunk flags. + flags: 'bytes' + #: Chunk value. + value: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'ChunkType', length: 'int', flags: 'bytes', value: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class DATAChunk(Chunk): + """Data model for SCTP DATA chunks.""" + + #: Chunk flags. + flags: 'DATAChunkFlags' + #: Transmission sequence number. + tsn: 'int' + #: Stream identifier. + stream_id: 'int' + #: Stream sequence number. + stream_seq: 'int' + #: Payload protocol identifier. + ppid: 'PayloadProtocolIdentifier' + #: User data. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'ChunkType', length: 'int', flags: 'DATAChunkFlags', tsn: 'int', stream_id: 'int', stream_seq: 'int', ppid: 'PayloadProtocolIdentifier', data: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class INITChunk(Chunk): + """Data model for SCTP INIT chunks.""" + + #: Initiate tag. + init_tag: 'int' + #: Advertised receiver window credit. + a_rwnd: 'int' + #: Number of outbound streams. + outbound_streams: 'int' + #: Number of inbound streams. + inbound_streams: 'int' + #: Initial transmission sequence number. + init_tsn: 'int' + #: Optional and variable-length parameters. + parameters: 'OrderedMultiDict[ParameterType, Parameter]' + + if TYPE_CHECKING: + def __init__(self, type: 'ChunkType', length: 'int', init_tag: 'int', a_rwnd: 'int', outbound_streams: 'int', inbound_streams: 'int', init_tsn: 'int', parameters: 'OrderedMultiDict[ParameterType, Parameter]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class INITACKChunk(Chunk): + """Data model for SCTP INIT ACK chunks.""" + + #: Initiate tag. + init_tag: 'int' + #: Advertised receiver window credit. + a_rwnd: 'int' + #: Number of outbound streams. + outbound_streams: 'int' + #: Number of inbound streams. + inbound_streams: 'int' + #: Initial transmission sequence number. + init_tsn: 'int' + #: Optional and variable-length parameters. + parameters: 'OrderedMultiDict[ParameterType, Parameter]' + + if TYPE_CHECKING: + def __init__(self, type: 'ChunkType', length: 'int', init_tag: 'int', a_rwnd: 'int', outbound_streams: 'int', inbound_streams: 'int', init_tsn: 'int', parameters: 'OrderedMultiDict[ParameterType, Parameter]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class SACKChunk(Chunk): + """Data model for SCTP SACK chunks.""" + + #: Cumulative TSN ack. + cum_tsn_ack: 'int' + #: Advertised receiver window credit. + a_rwnd: 'int' + #: Number of gap ack blocks. + num_gap_blocks: 'int' + #: Number of duplicate TSNs. + num_dup_tsn: 'int' + #: Gap ack blocks. + gap_blocks: 'tuple[GapAckBlock, ...]' + #: Duplicate TSNs. + dup_tsn: 'tuple[int, ...]' + + if TYPE_CHECKING: + def __init__(self, type: 'ChunkType', length: 'int', cum_tsn_ack: 'int', a_rwnd: 'int', num_gap_blocks: 'int', num_dup_tsn: 'int', gap_blocks: 'tuple[GapAckBlock, ...]', dup_tsn: 'tuple[int, ...]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class HeartbeatChunk(Chunk): + """Data model for SCTP HEARTBEAT chunks.""" + + #: Heartbeat information parameters. + parameters: 'OrderedMultiDict[ParameterType, Parameter]' + + if TYPE_CHECKING: + def __init__(self, type: 'ChunkType', length: 'int', parameters: 'OrderedMultiDict[ParameterType, Parameter]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class HeartbeatACKChunk(Chunk): + """Data model for SCTP HEARTBEAT ACK chunks.""" + + #: Heartbeat information parameters. + parameters: 'OrderedMultiDict[ParameterType, Parameter]' + + if TYPE_CHECKING: + def __init__(self, type: 'ChunkType', length: 'int', parameters: 'OrderedMultiDict[ParameterType, Parameter]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class AbortChunk(Chunk): + """Data model for SCTP ABORT chunks.""" + + #: Chunk flags. + flags: 'TBitFlags' + #: Zero or more error causes. + error: 'OrderedMultiDict[CauseCode, ErrorCause]' + + if TYPE_CHECKING: + def __init__(self, type: 'ChunkType', length: 'int', flags: 'TBitFlags', error: 'OrderedMultiDict[CauseCode, ErrorCause]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class ShutdownChunk(Chunk): + """Data model for SCTP SHUTDOWN chunks.""" + + #: Cumulative TSN ack. + cum_tsn_ack: 'int' + + if TYPE_CHECKING: + def __init__(self, type: 'ChunkType', length: 'int', cum_tsn_ack: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class ShutdownACKChunk(Chunk): + """Data model for SCTP SHUTDOWN ACK chunks.""" + + if TYPE_CHECKING: + def __init__(self, type: 'ChunkType', length: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class ErrorChunk(Chunk): + """Data model for SCTP ERROR chunks.""" + + #: One or more error causes. + error: 'OrderedMultiDict[CauseCode, ErrorCause]' + + if TYPE_CHECKING: + def __init__(self, type: 'ChunkType', length: 'int', error: 'OrderedMultiDict[CauseCode, ErrorCause]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class CookieEchoChunk(Chunk): + """Data model for SCTP COOKIE ECHO chunks.""" + + #: State cookie, as received in the INIT ACK chunk's state cookie parameter. + cookie: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'ChunkType', length: 'int', cookie: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class CookieACKChunk(Chunk): + """Data model for SCTP COOKIE ACK chunks.""" + + if TYPE_CHECKING: + def __init__(self, type: 'ChunkType', length: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class ShutdownCompleteChunk(Chunk): + """Data model for SCTP SHUTDOWN COMPLETE chunks.""" + + #: Chunk flags. + flags: 'TBitFlags' + + if TYPE_CHECKING: + def __init__(self, type: 'ChunkType', length: 'int', flags: 'TBitFlags') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class SCTP(Protocol): + """Data model for SCTP packet.""" + + #: Source port. + srcport: 'AppType' + #: Destination port. + dstport: 'AppType' + #: Verification tag. + vtag: 'int' + #: Checksum, as a CRC32c over the whole packet with this field zeroed. + chksum: 'bytes' + #: Chunks. + chunks: 'OrderedMultiDict[ChunkType, Chunk]' + + if TYPE_CHECKING: + def __init__(self, srcport: 'AppType', dstport: 'AppType', vtag: 'int', chksum: 'bytes', chunks: 'OrderedMultiDict[ChunkType, Chunk]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin diff --git a/pcapkit/protocols/internet/internet.py b/pcapkit/protocols/internet/internet.py index fd9233daec..9666a6f94a 100644 --- a/pcapkit/protocols/internet/internet.py +++ b/pcapkit/protocols/internet/internet.py @@ -69,6 +69,8 @@ class Internet(Protocol[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=abstra - :class:`pcapkit.protocols.internet.mh.MH` * - :attr:`~pcapkit.const.reg.transtype.TransType.HIP` - :class:`pcapkit.protocols.internet.hip.HIP` + * - :attr:`~pcapkit.const.reg.transtype.TransType.SCTP` + - :class:`pcapkit.protocols.transport.sctp.SCTP` """ @@ -98,6 +100,7 @@ class Internet(Protocol[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=abstra Enum_TransType.IPX_in_IP: ModuleDescriptor('pcapkit.protocols.internet.ipx', 'IPX'), Enum_TransType.Mobility_Header: ModuleDescriptor('pcapkit.protocols.internet.mh', 'MH'), Enum_TransType.HIP: ModuleDescriptor('pcapkit.protocols.internet.hip', 'HIP'), + Enum_TransType.SCTP: ModuleDescriptor('pcapkit.protocols.transport.sctp', 'SCTP'), }, ) diff --git a/pcapkit/protocols/schema/__init__.py b/pcapkit/protocols/schema/__init__.py index 493bddc325..c567dca89c 100644 --- a/pcapkit/protocols/schema/__init__.py +++ b/pcapkit/protocols/schema/__init__.py @@ -99,6 +99,28 @@ 'TCP_MPTCPJoinSYN', 'TCP_MPTCPJoinSYNACK', 'TCP_MPTCPJoinACK', 'UDP', + # Stream Control Transmission Protocol + 'SCTP', + 'SCTP_GapAckBlock', + 'SCTP_Chunk', + 'SCTP_UnknownChunk', 'SCTP_DATAChunk', 'SCTP_INITChunk', 'SCTP_INITACKChunk', 'SCTP_SACKChunk', + 'SCTP_HeartbeatChunk', 'SCTP_HeartbeatACKChunk', 'SCTP_AbortChunk', 'SCTP_ShutdownChunk', + 'SCTP_ShutdownACKChunk', 'SCTP_ErrorChunk', 'SCTP_CookieEchoChunk', 'SCTP_CookieACKChunk', + 'SCTP_ShutdownCompleteChunk', + 'SCTP_Parameter', + 'SCTP_UnknownParameter', 'SCTP_HeartbeatInfoParameter', 'SCTP_IPv4AddressParameter', + 'SCTP_IPv6AddressParameter', 'SCTP_StateCookieParameter', 'SCTP_UnrecognizedParameter', + 'SCTP_CookiePreservativeParameter', 'SCTP_HostNameAddressParameter', + 'SCTP_SupportedAddressTypesParameter', + 'SCTP_ErrorCause', + 'SCTP_UnknownCause', 'SCTP_InvalidStreamIdentifierCause', + 'SCTP_MissingMandatoryParameterCause', 'SCTP_StaleCookieCause', 'SCTP_OutOfResourceCause', + 'SCTP_UnresolvableAddressCause', 'SCTP_UnrecognizedChunkTypeCause', + 'SCTP_InvalidMandatoryParameterCause', 'SCTP_UnrecognizedParametersCause', + 'SCTP_NoUserDataCause', 'SCTP_CookieReceivedWhileShuttingDownCause', + 'SCTP_RestartOfAnAssociationWithNewAddressesCause', 'SCTP_UserInitiatedAbortCause', + 'SCTP_ProtocolViolationCause', + # Application Layer Protocols 'FTP', 'HTTPv1', diff --git a/pcapkit/protocols/schema/transport/__init__.py b/pcapkit/protocols/schema/transport/__init__.py index 944381df8f..aa7df7b7e4 100644 --- a/pcapkit/protocols/schema/transport/__init__.py +++ b/pcapkit/protocols/schema/transport/__init__.py @@ -45,6 +45,69 @@ from pcapkit.protocols.schema.transport.tcp import UserTimeout as TCP_UserTimeout from pcapkit.protocols.schema.transport.tcp import WindowScale as TCP_WindowScale +# Stream Control Transmission Protocol +from pcapkit.protocols.schema.transport.sctp import SCTP +from pcapkit.protocols.schema.transport.sctp import AbortChunk as SCTP_AbortChunk +from pcapkit.protocols.schema.transport.sctp import Chunk as SCTP_Chunk +from pcapkit.protocols.schema.transport.sctp import CookieACKChunk as SCTP_CookieACKChunk +from pcapkit.protocols.schema.transport.sctp import CookieEchoChunk as SCTP_CookieEchoChunk +from pcapkit.protocols.schema.transport.sctp import \ + CookiePreservativeParameter as SCTP_CookiePreservativeParameter +from pcapkit.protocols.schema.transport.sctp import \ + CookieReceivedWhileShuttingDownCause as SCTP_CookieReceivedWhileShuttingDownCause +from pcapkit.protocols.schema.transport.sctp import DATAChunk as SCTP_DATAChunk +from pcapkit.protocols.schema.transport.sctp import ErrorCause as SCTP_ErrorCause +from pcapkit.protocols.schema.transport.sctp import ErrorChunk as SCTP_ErrorChunk +from pcapkit.protocols.schema.transport.sctp import GapAckBlock as SCTP_GapAckBlock +from pcapkit.protocols.schema.transport.sctp import HeartbeatACKChunk as SCTP_HeartbeatACKChunk +from pcapkit.protocols.schema.transport.sctp import HeartbeatChunk as SCTP_HeartbeatChunk +from pcapkit.protocols.schema.transport.sctp import \ + HeartbeatInfoParameter as SCTP_HeartbeatInfoParameter +from pcapkit.protocols.schema.transport.sctp import \ + HostNameAddressParameter as SCTP_HostNameAddressParameter +from pcapkit.protocols.schema.transport.sctp import INITACKChunk as SCTP_INITACKChunk +from pcapkit.protocols.schema.transport.sctp import INITChunk as SCTP_INITChunk +from pcapkit.protocols.schema.transport.sctp import \ + InvalidMandatoryParameterCause as SCTP_InvalidMandatoryParameterCause +from pcapkit.protocols.schema.transport.sctp import \ + InvalidStreamIdentifierCause as SCTP_InvalidStreamIdentifierCause +from pcapkit.protocols.schema.transport.sctp import \ + IPv4AddressParameter as SCTP_IPv4AddressParameter +from pcapkit.protocols.schema.transport.sctp import \ + IPv6AddressParameter as SCTP_IPv6AddressParameter +from pcapkit.protocols.schema.transport.sctp import \ + MissingMandatoryParameterCause as SCTP_MissingMandatoryParameterCause +from pcapkit.protocols.schema.transport.sctp import NoUserDataCause as SCTP_NoUserDataCause +from pcapkit.protocols.schema.transport.sctp import OutOfResourceCause as SCTP_OutOfResourceCause +from pcapkit.protocols.schema.transport.sctp import Parameter as SCTP_Parameter +from pcapkit.protocols.schema.transport.sctp import \ + ProtocolViolationCause as SCTP_ProtocolViolationCause +from pcapkit.protocols.schema.transport.sctp import \ + RestartOfAnAssociationWithNewAddressesCause as \ + SCTP_RestartOfAnAssociationWithNewAddressesCause +from pcapkit.protocols.schema.transport.sctp import SACKChunk as SCTP_SACKChunk +from pcapkit.protocols.schema.transport.sctp import ShutdownACKChunk as SCTP_ShutdownACKChunk +from pcapkit.protocols.schema.transport.sctp import ShutdownChunk as SCTP_ShutdownChunk +from pcapkit.protocols.schema.transport.sctp import \ + ShutdownCompleteChunk as SCTP_ShutdownCompleteChunk +from pcapkit.protocols.schema.transport.sctp import StaleCookieCause as SCTP_StaleCookieCause +from pcapkit.protocols.schema.transport.sctp import \ + StateCookieParameter as SCTP_StateCookieParameter +from pcapkit.protocols.schema.transport.sctp import \ + SupportedAddressTypesParameter as SCTP_SupportedAddressTypesParameter +from pcapkit.protocols.schema.transport.sctp import UnknownCause as SCTP_UnknownCause +from pcapkit.protocols.schema.transport.sctp import UnknownChunk as SCTP_UnknownChunk +from pcapkit.protocols.schema.transport.sctp import UnknownParameter as SCTP_UnknownParameter +from pcapkit.protocols.schema.transport.sctp import \ + UnrecognizedChunkTypeCause as SCTP_UnrecognizedChunkTypeCause +from pcapkit.protocols.schema.transport.sctp import \ + UnrecognizedParameter as SCTP_UnrecognizedParameter +from pcapkit.protocols.schema.transport.sctp import \ + UnrecognizedParametersCause as SCTP_UnrecognizedParametersCause +from pcapkit.protocols.schema.transport.sctp import \ + UnresolvableAddressCause as SCTP_UnresolvableAddressCause +from pcapkit.protocols.schema.transport.sctp import \ + UserInitiatedAbortCause as SCTP_UserInitiatedAbortCause # User Datagram Protocol from pcapkit.protocols.schema.transport.udp import UDP @@ -64,6 +127,28 @@ 'TCP_MPTCPJoin', 'TCP_MPTCPJoinSYN', 'TCP_MPTCPJoinSYNACK', 'TCP_MPTCPJoinACK', + # Stream Control Transmission Protocol + 'SCTP', + 'SCTP_GapAckBlock', + 'SCTP_Chunk', + 'SCTP_UnknownChunk', 'SCTP_DATAChunk', 'SCTP_INITChunk', 'SCTP_INITACKChunk', 'SCTP_SACKChunk', + 'SCTP_HeartbeatChunk', 'SCTP_HeartbeatACKChunk', 'SCTP_AbortChunk', 'SCTP_ShutdownChunk', + 'SCTP_ShutdownACKChunk', 'SCTP_ErrorChunk', 'SCTP_CookieEchoChunk', 'SCTP_CookieACKChunk', + 'SCTP_ShutdownCompleteChunk', + 'SCTP_Parameter', + 'SCTP_UnknownParameter', 'SCTP_HeartbeatInfoParameter', 'SCTP_IPv4AddressParameter', + 'SCTP_IPv6AddressParameter', 'SCTP_StateCookieParameter', 'SCTP_UnrecognizedParameter', + 'SCTP_CookiePreservativeParameter', 'SCTP_HostNameAddressParameter', + 'SCTP_SupportedAddressTypesParameter', + 'SCTP_ErrorCause', + 'SCTP_UnknownCause', 'SCTP_InvalidStreamIdentifierCause', + 'SCTP_MissingMandatoryParameterCause', 'SCTP_StaleCookieCause', 'SCTP_OutOfResourceCause', + 'SCTP_UnresolvableAddressCause', 'SCTP_UnrecognizedChunkTypeCause', + 'SCTP_InvalidMandatoryParameterCause', 'SCTP_UnrecognizedParametersCause', + 'SCTP_NoUserDataCause', 'SCTP_CookieReceivedWhileShuttingDownCause', + 'SCTP_RestartOfAnAssociationWithNewAddressesCause', 'SCTP_UserInitiatedAbortCause', + 'SCTP_ProtocolViolationCause', + # User Datagram Protocol 'UDP', ] diff --git a/pcapkit/protocols/schema/transport/sctp.py b/pcapkit/protocols/schema/transport/sctp.py new file mode 100644 index 0000000000..978ce017e5 --- /dev/null +++ b/pcapkit/protocols/schema/transport/sctp.py @@ -0,0 +1,895 @@ +# -*- coding: utf-8 -*- +# mypy: disable-error-code=assignment +"""header schema for stream control transmission protocol""" + +from typing import TYPE_CHECKING + +from pcapkit.const.reg.apptype import AppType as Enum_AppType +from pcapkit.const.reg.apptype import TransportProtocol as Enum_TransportProtocol +from pcapkit.const.sctp.cause_code import CauseCode as Enum_CauseCode +from pcapkit.const.sctp.chunk import Chunk as Enum_Chunk +from pcapkit.const.sctp.parameter import Parameter as Enum_Parameter +from pcapkit.const.sctp.payload_protocol_identifier import \ + PayloadProtocolIdentifier as Enum_PayloadProtocolIdentifier +from pcapkit.corekit.fields.collections import ListField, OptionField +from pcapkit.corekit.fields.ipaddress import IPv4AddressField, IPv6AddressField +from pcapkit.corekit.fields.misc import SchemaField +from pcapkit.corekit.fields.numbers import EnumField, UInt16Field, UInt32Field +from pcapkit.corekit.fields.strings import BitField, BytesField, PaddingField +from pcapkit.protocols.schema.schema import EnumSchema, Schema, schema_final +from pcapkit.utilities.logging import SPHINX_TYPE_CHECKING + +__all__ = [ + 'SCTP', + + 'Chunk', + 'UnknownChunk', 'DATAChunk', 'INITChunk', 'INITACKChunk', 'SACKChunk', + 'HeartbeatChunk', 'HeartbeatACKChunk', 'AbortChunk', 'ShutdownChunk', + 'ShutdownACKChunk', 'ErrorChunk', 'CookieEchoChunk', 'CookieACKChunk', + 'ShutdownCompleteChunk', + + 'GapAckBlock', + + 'Parameter', + 'UnknownParameter', 'HeartbeatInfoParameter', 'IPv4AddressParameter', + 'IPv6AddressParameter', 'StateCookieParameter', 'UnrecognizedParameter', + 'CookiePreservativeParameter', 'HostNameAddressParameter', + 'SupportedAddressTypesParameter', + + 'ErrorCause', + 'UnknownCause', 'InvalidStreamIdentifierCause', 'MissingMandatoryParameterCause', + 'StaleCookieCause', 'OutOfResourceCause', 'UnresolvableAddressCause', + 'UnrecognizedChunkTypeCause', 'InvalidMandatoryParameterCause', + 'UnrecognizedParametersCause', 'NoUserDataCause', + 'CookieReceivedWhileShuttingDownCause', + 'RestartOfAnAssociationWithNewAddressesCause', 'UserInitiatedAbortCause', + 'ProtocolViolationCause', +] + +if TYPE_CHECKING: + from ipaddress import IPv4Address, IPv6Address + from typing import Any, Callable + + from pcapkit.protocols.protocol import ProtocolBase as Protocol + +if SPHINX_TYPE_CHECKING: # pragma: no cover + from typing_extensions import TypedDict + + class DATAChunkFlags(TypedDict): + """SCTP DATA chunk flags.""" + + #: (I)mmediate bit, i.e., request a SACK chunk without delay. + I: int + #: (U)nordered bit, i.e., no stream sequence number is assigned. + U: int + #: (B)eginning fragment bit. + B: int + #: (E)nding fragment bit. + E: int + + class TBitFlags(TypedDict): + """SCTP chunk flags carrying only the T bit, i.e., ABORT and + SHUTDOWN COMPLETE chunks.""" + + #: T bit, i.e., the verification tag has been reflected. + T: int + + +def padding_length(pkt: 'dict[str, Any]') -> 'int': + """Length of the trailing padding of an SCTP type-length-value structure. + + Chunks, chunk parameters and error causes are all padded with all-zero + bytes to a multiple of four bytes, and per :rfc:`9260#section-3.2` that + padding is **not** counted in the ``length`` field. The padding is still + on the wire, though, so it has to be consumed for the enclosing list to + stay aligned. + + Args: + pkt: Packet data. + + Returns: + Number of padding bytes, clamped to the number of bytes left in the + enclosing structure, since :rfc:`9260#section-3.2` allows the final + padding of a packet to be omitted. + + """ + length = pkt.get('length') or 0 + padding = -length % 4 + + remaining = pkt.get('__length__') + if not isinstance(remaining, int) or remaining < 0: + return padding + return min(padding, remaining) + + +def bounded(length: 'Callable[[dict[str, Any]], int]') -> 'Callable[[dict[str, Any]], int]': + """Clamp a list field's computed length to the bytes actually available. + + Args: + length: Callback computing the field's nominal length. + + Returns: + A callback returning that length, never exceeding the bytes left in the + enclosing structure. + + A count field and the list it counts can disagree on a malformed packet, and + :meth:`ListField.unpack ` + subtracts the *parsed* size of each item from its budget -- so a + :class:`~pcapkit.corekit.fields.misc.SchemaField` item that runs out of bytes + parses to nothing, subtracts nothing, and the loop never terminates. Clamping + to the bytes on hand turns that hang into a short list, which the read + handlers then reject against the declared length. + + Only list fields are wrapped: :meth:`ListField.pack + ` ignores the length + entirely, so clamping cannot truncate anything on construction. + + """ + def callback(pkt: 'dict[str, Any]') -> 'int': + value = length(pkt) + remaining = pkt.get('__length__') + if isinstance(remaining, int) and remaining >= 0: + return min(value, remaining) + return value + return callback + + +def nested_length(base: 'int') -> 'Callable[[dict[str, Any]], int]': + """Build a length callback for a chunk's nested type-length-value list. + + Args: + base: Size of the chunk's fixed-length fields, including the four-byte + chunk header. + + Returns: + A callback returning the size of the chunk's nested list *including* the + chunk's own trailing padding. + + The chunks that carry a nested list -- INIT, INIT ACK, HEARTBEAT, + HEARTBEAT ACK, ABORT and ERROR -- deliberately have no separate + :class:`~pcapkit.corekit.fields.strings.PaddingField`, and fold the chunk's + trailing padding into the nested list's own span instead. There are two + reasons, and both matter: + + * On parsing, a sender is allowed by :rfc:`9260#section-3.2` to leave the + final parameter's padding out of the chunk length, so the padding has to + be consumed whether or not the length accounts for it. Folding it into the + list's span does that, and leaves it visible as the field's + ``__option_padding__``. + * On construction, a separate padding field could not compute its own size: + :meth:`Schema.pack ` passes + one shared ``packet`` mapping down into the nested schemas, and each + nested parameter overwrites ``packet['length']`` with *its* length, so a + trailing field would size itself from the last parameter rather than from + the chunk. Folding avoids the question: the constructors always declare a + chunk length that already covers every parameter's padding, hence a + multiple of four, hence no chunk-level padding to emit. + + """ + def callback(pkt: 'dict[str, Any]') -> 'int': + length = pkt.get('length') or 0 + return max(length - base + (-length % 4), 0) + return callback + + +class PortEnumField(EnumField): + """Enumerated value for protocol fields. + + Args: + length: Field size (in bytes); if a callable is given, it should return + an integer value and accept the current packet as its only argument. + default: Field default value, if any. + signed: Whether the field is signed. + byteorder: Field byte order. + bit_length: Field bit length. + callback: Callback function to be called upon + :meth:`self.__call__ `. + + Important: + This class is specifically designed for :class:`~pcapkit.const.reg.apptype.AppType` + as it is actually a :class:`~enum.StrEnum` class. + + """ + if TYPE_CHECKING: + _namespace: 'Enum_AppType' + + def pre_process(self, value: 'int | Enum_AppType', packet: 'dict[str, Any]') -> 'int | bytes': + """Process field value before construction (packing). + + Arguments: + value: Field value. + packet: Packet data. + + Returns: + Processed field value. + + """ + if isinstance(value, Enum_AppType): + value = value.port + return super().pre_process(value, packet) + + def post_process(self, value: 'int | bytes', packet: 'dict[str, Any]') -> 'Enum_AppType': + """Process field value after parsing (unpacked). + + Args: + value: Field value. + packet: Packet data. + + Returns: + Processed field value. + + """ + value = super(EnumField, self).post_process(value, packet) + return self._namespace.get(value, proto=Enum_TransportProtocol.sctp) + + +class ErrorCause(EnumSchema[Enum_CauseCode]): + """Header schema for SCTP error causes.""" + + __default__ = lambda: UnknownCause + + #: Cause code. + code: 'Enum_CauseCode' = EnumField(length=2, namespace=Enum_CauseCode) + #: Cause length. + length: 'int' = UInt16Field() + + +@schema_final +class UnknownCause(ErrorCause): + """Header schema for SCTP error causes with unknown cause codes.""" + + #: Cause-specific information. + value: 'bytes' = BytesField(length=lambda pkt: max(pkt['length'] - 4, 0)) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, code: 'Enum_CauseCode', length: 'int', value: 'bytes') -> 'None': ... + + +@schema_final +class InvalidStreamIdentifierCause(ErrorCause, code=Enum_CauseCode.Invalid_Stream_Identifier): + """Header schema for SCTP invalid stream identifier error cause.""" + + #: Stream identifier of the offending DATA chunk. + stream_id: 'int' = UInt16Field() + #: Reserved. + reserved: 'bytes' = PaddingField(length=2) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, code: 'Enum_CauseCode', length: 'int', stream_id: 'int') -> 'None': ... + + +@schema_final +class MissingMandatoryParameterCause(ErrorCause, code=Enum_CauseCode.Missing_Mandatory_Parameter): + """Header schema for SCTP missing mandatory parameter error cause.""" + + #: Number of missing parameters. + num: 'int' = UInt32Field() + #: Missing parameter types. + types: 'list[Enum_Parameter]' = ListField( + length=bounded(lambda pkt: min(max(pkt['num'], 0) * 2, max(pkt['length'] - 8, 0))), + item_type=EnumField(length=2, namespace=Enum_Parameter), + ) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, code: 'Enum_CauseCode', length: 'int', num: 'int', + types: 'list[Enum_Parameter]') -> 'None': ... + + +@schema_final +class StaleCookieCause(ErrorCause, code=Enum_CauseCode.Stale_Cookie): + """Header schema for SCTP stale cookie error cause.""" + + #: Measure of staleness, in microseconds. + staleness: 'int' = UInt32Field() + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, code: 'Enum_CauseCode', length: 'int', staleness: 'int') -> 'None': ... + + +@schema_final +class OutOfResourceCause(ErrorCause, code=Enum_CauseCode.Out_of_Resource): + """Header schema for SCTP out of resource error cause.""" + + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, code: 'Enum_CauseCode', length: 'int') -> 'None': ... + + +@schema_final +class UnresolvableAddressCause(ErrorCause, code=Enum_CauseCode.Unresolvable_Address): + """Header schema for SCTP unresolvable address error cause.""" + + #: The offending address parameter, complete with its type and length. + value: 'bytes' = BytesField(length=lambda pkt: max(pkt['length'] - 4, 0)) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, code: 'Enum_CauseCode', length: 'int', value: 'bytes') -> 'None': ... + + +@schema_final +class UnrecognizedChunkTypeCause(ErrorCause, code=Enum_CauseCode.Unrecognized_Chunk_Type): + """Header schema for SCTP unrecognized chunk type error cause.""" + + #: The unrecognized chunk, complete with its type, flags and length. + value: 'bytes' = BytesField(length=lambda pkt: max(pkt['length'] - 4, 0)) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, code: 'Enum_CauseCode', length: 'int', value: 'bytes') -> 'None': ... + + +@schema_final +class InvalidMandatoryParameterCause(ErrorCause, code=Enum_CauseCode.Invalid_Mandatory_Parameter): + """Header schema for SCTP invalid mandatory parameter error cause.""" + + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, code: 'Enum_CauseCode', length: 'int') -> 'None': ... + + +@schema_final +class UnrecognizedParametersCause(ErrorCause, code=Enum_CauseCode.Unrecognized_Parameters): + """Header schema for SCTP unrecognized parameters error cause.""" + + #: The unrecognized parameters, complete with their types and lengths. + value: 'bytes' = BytesField(length=lambda pkt: max(pkt['length'] - 4, 0)) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, code: 'Enum_CauseCode', length: 'int', value: 'bytes') -> 'None': ... + + +@schema_final +class NoUserDataCause(ErrorCause, code=Enum_CauseCode.No_User_Data): + """Header schema for SCTP no user data error cause.""" + + #: TSN of the offending DATA chunk. + tsn: 'int' = UInt32Field() + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, code: 'Enum_CauseCode', length: 'int', tsn: 'int') -> 'None': ... + + +@schema_final +class CookieReceivedWhileShuttingDownCause(ErrorCause, code=Enum_CauseCode.Cookie_Received_While_Shutting_Down): # pylint: disable=line-too-long + """Header schema for SCTP cookie received while shutting down error cause.""" + + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, code: 'Enum_CauseCode', length: 'int') -> 'None': ... + + +@schema_final +class RestartOfAnAssociationWithNewAddressesCause(ErrorCause, code=Enum_CauseCode.Restart_of_an_Association_with_New_Addresses): # pylint: disable=line-too-long + """Header schema for SCTP restart of an association with new addresses error cause.""" + + #: The new address parameters, complete with their types and lengths. + value: 'bytes' = BytesField(length=lambda pkt: max(pkt['length'] - 4, 0)) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, code: 'Enum_CauseCode', length: 'int', value: 'bytes') -> 'None': ... + + +@schema_final +class UserInitiatedAbortCause(ErrorCause, code=Enum_CauseCode.User_Initiated_Abort): + """Header schema for SCTP user-initiated abort error cause.""" + + #: Upper layer abort reason. + info: 'bytes' = BytesField(length=lambda pkt: max(pkt['length'] - 4, 0)) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, code: 'Enum_CauseCode', length: 'int', info: 'bytes') -> 'None': ... + + +@schema_final +class ProtocolViolationCause(ErrorCause, code=Enum_CauseCode.Protocol_Violation): + """Header schema for SCTP protocol violation error cause.""" + + #: Additional information. + info: 'bytes' = BytesField(length=lambda pkt: max(pkt['length'] - 4, 0)) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, code: 'Enum_CauseCode', length: 'int', info: 'bytes') -> 'None': ... + + +class Parameter(EnumSchema[Enum_Parameter]): + """Header schema for SCTP chunk parameters.""" + + __default__ = lambda: UnknownParameter + + #: Parameter type. + type: 'Enum_Parameter' = EnumField(length=2, namespace=Enum_Parameter) + #: Parameter length. + length: 'int' = UInt16Field() + + +@schema_final +class UnknownParameter(Parameter): + """Header schema for SCTP chunk parameters with unknown types.""" + + #: Parameter value. + value: 'bytes' = BytesField(length=lambda pkt: max(pkt['length'] - 4, 0)) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Parameter', length: 'int', value: 'bytes') -> 'None': ... + + +@schema_final +class HeartbeatInfoParameter(Parameter, code=Enum_Parameter.Heartbeat_Info): + """Header schema for SCTP heartbeat info parameter.""" + + #: Sender-specific heartbeat info. + info: 'bytes' = BytesField(length=lambda pkt: max(pkt['length'] - 4, 0)) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Parameter', length: 'int', info: 'bytes') -> 'None': ... + + +@schema_final +class IPv4AddressParameter(Parameter, code=Enum_Parameter.IPv4_Address): + """Header schema for SCTP IPv4 address parameter.""" + + #: IPv4 address of the sending endpoint. + address: 'IPv4Address' = IPv4AddressField() + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Parameter', length: 'int', + address: 'IPv4Address | int | str | bytes') -> 'None': ... + + +@schema_final +class IPv6AddressParameter(Parameter, code=Enum_Parameter.IPv6_Address): + """Header schema for SCTP IPv6 address parameter.""" + + #: IPv6 address of the sending endpoint. + address: 'IPv6Address' = IPv6AddressField() + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Parameter', length: 'int', + address: 'IPv6Address | int | str | bytes') -> 'None': ... + + +@schema_final +class StateCookieParameter(Parameter, code=Enum_Parameter.State_Cookie): + """Header schema for SCTP state cookie parameter.""" + + #: State cookie. + cookie: 'bytes' = BytesField(length=lambda pkt: max(pkt['length'] - 4, 0)) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Parameter', length: 'int', cookie: 'bytes') -> 'None': ... + + +@schema_final +class UnrecognizedParameter(Parameter, code=Enum_Parameter.Unrecognized_Parameter): + """Header schema for SCTP unrecognized parameter parameter.""" + + #: The unrecognized parameter, complete with its type and length. + value: 'bytes' = BytesField(length=lambda pkt: max(pkt['length'] - 4, 0)) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Parameter', length: 'int', value: 'bytes') -> 'None': ... + + +@schema_final +class CookiePreservativeParameter(Parameter, code=Enum_Parameter.Cookie_Preservative): + """Header schema for SCTP cookie preservative parameter.""" + + #: Suggested cookie life-span increment, in milliseconds. + increment: 'int' = UInt32Field() + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Parameter', length: 'int', increment: 'int') -> 'None': ... + + +@schema_final +class HostNameAddressParameter(Parameter, code=Enum_Parameter.Host_Name_Address): + """Header schema for SCTP host name address parameter.""" + + #: Host name, including at least one null terminator. + name: 'bytes' = BytesField(length=lambda pkt: max(pkt['length'] - 4, 0)) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Parameter', length: 'int', name: 'bytes') -> 'None': ... + + +@schema_final +class SupportedAddressTypesParameter(Parameter, code=Enum_Parameter.Supported_Address_Types): + """Header schema for SCTP supported address types parameter.""" + + #: Supported address types, given as address parameter types. + types: 'list[Enum_Parameter]' = ListField( + length=bounded(lambda pkt: max(pkt['length'] - 4, 0)), + item_type=EnumField(length=2, namespace=Enum_Parameter), + ) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Parameter', length: 'int', + types: 'list[Enum_Parameter]') -> 'None': ... + + +@schema_final +class GapAckBlock(Schema): + """Header schema for SCTP SACK chunk gap ack blocks.""" + + #: Start offset TSN of the gap ack block. + start: 'int' = UInt16Field() + #: End offset TSN of the gap ack block. + end: 'int' = UInt16Field() + + if TYPE_CHECKING: + def __init__(self, start: 'int', end: 'int') -> 'None': ... + + +class Chunk(EnumSchema[Enum_Chunk]): + """Header schema for SCTP chunks.""" + + __default__ = lambda: UnknownChunk + + #: Chunk type. + type: 'Enum_Chunk' = EnumField(length=1, namespace=Enum_Chunk) + #: Chunk flags, whose meaning depends on the chunk type. + flags: 'bytes' = BytesField(length=1) + #: Chunk length, excluding any trailing padding. + length: 'int' = UInt16Field() + + +@schema_final +class UnknownChunk(Chunk): + """Header schema for SCTP chunks with unknown types.""" + + #: Chunk value. + value: 'bytes' = BytesField(length=lambda pkt: max(pkt['length'] - 4, 0)) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Chunk', flags: 'bytes', length: 'int', + value: 'bytes') -> 'None': ... + + +@schema_final +class DATAChunk(Chunk, code=Enum_Chunk.Payload_Data): + """Header schema for SCTP DATA chunks.""" + + #: Chunk flags. + flags: 'DATAChunkFlags' = BitField(length=1, namespace={ + 'I': (4, 1), + 'U': (5, 1), + 'B': (6, 1), + 'E': (7, 1), + }) + #: Transmission sequence number. + tsn: 'int' = UInt32Field() + #: Stream identifier. + stream_id: 'int' = UInt16Field() + #: Stream sequence number. + stream_seq: 'int' = UInt16Field() + #: Payload protocol identifier. + ppid: 'Enum_PayloadProtocolIdentifier' = EnumField( + length=4, namespace=Enum_PayloadProtocolIdentifier) + #: User data. + data: 'bytes' = BytesField(length=lambda pkt: max(pkt['length'] - 16, 0)) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Chunk', flags: 'DATAChunkFlags', length: 'int', tsn: 'int', + stream_id: 'int', stream_seq: 'int', + ppid: 'Enum_PayloadProtocolIdentifier | int', data: 'bytes') -> 'None': ... + + +@schema_final +class INITChunk(Chunk, code=Enum_Chunk.Initiation): + """Header schema for SCTP INIT chunks.""" + + #: Initiate tag. + init_tag: 'int' = UInt32Field() + #: Advertised receiver window credit. + a_rwnd: 'int' = UInt32Field() + #: Number of outbound streams. + outbound_streams: 'int' = UInt16Field() + #: Number of inbound streams. + inbound_streams: 'int' = UInt16Field() + #: Initial transmission sequence number. + init_tsn: 'int' = UInt32Field() + #: Optional and variable-length parameters, including the chunk's own + #: trailing padding; see :func:`nested_length`. + parameters: 'list[Parameter]' = OptionField( + length=nested_length(20), + base_schema=Parameter, + type_name='type', + registry=Parameter.registry, + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Chunk', flags: 'bytes', length: 'int', init_tag: 'int', + a_rwnd: 'int', outbound_streams: 'int', inbound_streams: 'int', + init_tsn: 'int', + parameters: 'list[Parameter | bytes] | bytes') -> 'None': ... + + +@schema_final +class INITACKChunk(Chunk, code=Enum_Chunk.Initiation_Acknowledgement): + """Header schema for SCTP INIT ACK chunks.""" + + #: Initiate tag. + init_tag: 'int' = UInt32Field() + #: Advertised receiver window credit. + a_rwnd: 'int' = UInt32Field() + #: Number of outbound streams. + outbound_streams: 'int' = UInt16Field() + #: Number of inbound streams. + inbound_streams: 'int' = UInt16Field() + #: Initial transmission sequence number. + init_tsn: 'int' = UInt32Field() + #: Optional and variable-length parameters, including the chunk's own + #: trailing padding; see :func:`nested_length`. + parameters: 'list[Parameter]' = OptionField( + length=nested_length(20), + base_schema=Parameter, + type_name='type', + registry=Parameter.registry, + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Chunk', flags: 'bytes', length: 'int', init_tag: 'int', + a_rwnd: 'int', outbound_streams: 'int', inbound_streams: 'int', + init_tsn: 'int', + parameters: 'list[Parameter | bytes] | bytes') -> 'None': ... + + +@schema_final +class SACKChunk(Chunk, code=Enum_Chunk.Selective_Acknowledgement): + """Header schema for SCTP SACK chunks.""" + + #: Cumulative TSN ack. + cum_tsn_ack: 'int' = UInt32Field() + #: Advertised receiver window credit. + a_rwnd: 'int' = UInt32Field() + #: Number of gap ack blocks. + num_gap_blocks: 'int' = UInt16Field() + #: Number of duplicate TSNs. + num_dup_tsn: 'int' = UInt16Field() + #: Gap ack blocks. + gap_blocks: 'list[GapAckBlock]' = ListField( + length=bounded( + lambda pkt: min(max(pkt['num_gap_blocks'], 0) * 4, max(pkt['length'] - 16, 0))), + item_type=SchemaField(length=4, schema=GapAckBlock), + ) + #: Duplicate TSNs. + dup_tsn: 'list[int]' = ListField( + length=bounded( + lambda pkt: min(max(pkt['num_dup_tsn'], 0) * 4, + max(pkt['length'] - 16 - max(pkt['num_gap_blocks'], 0) * 4, 0))), + item_type=UInt32Field(), + ) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Chunk', flags: 'bytes', length: 'int', cum_tsn_ack: 'int', + a_rwnd: 'int', num_gap_blocks: 'int', num_dup_tsn: 'int', + gap_blocks: 'list[GapAckBlock] | bytes', + dup_tsn: 'list[int] | bytes') -> 'None': ... + + +@schema_final +class HeartbeatChunk(Chunk, code=Enum_Chunk.Heartbeat_Request): + """Header schema for SCTP HEARTBEAT chunks.""" + + #: Heartbeat information parameters, including the chunk's own trailing + #: padding; see :func:`nested_length`. + parameters: 'list[Parameter]' = OptionField( + length=nested_length(4), + base_schema=Parameter, + type_name='type', + registry=Parameter.registry, + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Chunk', flags: 'bytes', length: 'int', + parameters: 'list[Parameter | bytes] | bytes') -> 'None': ... + + +@schema_final +class HeartbeatACKChunk(Chunk, code=Enum_Chunk.Heartbeat_Acknowledgement): + """Header schema for SCTP HEARTBEAT ACK chunks.""" + + #: Heartbeat information parameters, including the chunk's own trailing + #: padding; see :func:`nested_length`. + parameters: 'list[Parameter]' = OptionField( + length=nested_length(4), + base_schema=Parameter, + type_name='type', + registry=Parameter.registry, + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Chunk', flags: 'bytes', length: 'int', + parameters: 'list[Parameter | bytes] | bytes') -> 'None': ... + + +@schema_final +class AbortChunk(Chunk, code=Enum_Chunk.Abort): + """Header schema for SCTP ABORT chunks.""" + + #: Chunk flags. + flags: 'TBitFlags' = BitField(length=1, namespace={ + 'T': (7, 1), + }) + #: Zero or more error causes, including the chunk's own trailing padding; + #: see :func:`nested_length`. + error: 'list[ErrorCause]' = OptionField( + length=nested_length(4), + base_schema=ErrorCause, + type_name='code', + registry=ErrorCause.registry, + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Chunk', flags: 'TBitFlags', length: 'int', + error: 'list[ErrorCause | bytes] | bytes') -> 'None': ... + + +@schema_final +class ShutdownChunk(Chunk, code=Enum_Chunk.Shutdown): + """Header schema for SCTP SHUTDOWN chunks.""" + + #: Cumulative TSN ack. + cum_tsn_ack: 'int' = UInt32Field() + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Chunk', flags: 'bytes', length: 'int', + cum_tsn_ack: 'int') -> 'None': ... + + +@schema_final +class ShutdownACKChunk(Chunk, code=Enum_Chunk.Shutdown_Acknowledgement): + """Header schema for SCTP SHUTDOWN ACK chunks.""" + + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Chunk', flags: 'bytes', length: 'int') -> 'None': ... + + +@schema_final +class ErrorChunk(Chunk, code=Enum_Chunk.Operation_Error): + """Header schema for SCTP ERROR chunks.""" + + #: One or more error causes, including the chunk's own trailing padding; + #: see :func:`nested_length`. + error: 'list[ErrorCause]' = OptionField( + length=nested_length(4), + base_schema=ErrorCause, + type_name='code', + registry=ErrorCause.registry, + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Chunk', flags: 'bytes', length: 'int', + error: 'list[ErrorCause | bytes] | bytes') -> 'None': ... + + +@schema_final +class CookieEchoChunk(Chunk, code=Enum_Chunk.State_Cookie): + """Header schema for SCTP COOKIE ECHO chunks.""" + + #: State cookie, as received in the INIT ACK chunk's state cookie parameter. + cookie: 'bytes' = BytesField(length=lambda pkt: max(pkt['length'] - 4, 0)) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Chunk', flags: 'bytes', length: 'int', + cookie: 'bytes') -> 'None': ... + + +@schema_final +class CookieACKChunk(Chunk, code=Enum_Chunk.Cookie_Acknowledgement): + """Header schema for SCTP COOKIE ACK chunks.""" + + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Chunk', flags: 'bytes', length: 'int') -> 'None': ... + + +@schema_final +class ShutdownCompleteChunk(Chunk, code=Enum_Chunk.Shutdown_Complete): + """Header schema for SCTP SHUTDOWN COMPLETE chunks.""" + + #: Chunk flags. + flags: 'TBitFlags' = BitField(length=1, namespace={ + 'T': (7, 1), + }) + #: Padding. + padding: 'bytes' = PaddingField(length=padding_length) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Chunk', flags: 'TBitFlags', length: 'int') -> 'None': ... + + +@schema_final +class SCTP(Schema): + """Header schema for SCTP packets. + + Note: + Unlike :class:`~pcapkit.protocols.schema.transport.tcp.TCP` and + :class:`~pcapkit.protocols.schema.transport.udp.UDP`, there is **no** + payload field, since SCTP carries its user data inside DATA chunks + rather than after the common header. See + :meth:`SCTP._get_payload ` + for how the next layer is located. + + """ + + #: Source port. + srcport: 'Enum_AppType' = PortEnumField(length=2, namespace=Enum_AppType) + #: Destination port. + dstport: 'Enum_AppType' = PortEnumField(length=2, namespace=Enum_AppType) + #: Verification tag. + vtag: 'int' = UInt32Field() + #: Checksum, as a CRC32c over the whole packet with this field zeroed. + chksum: 'bytes' = BytesField(length=4) + #: Chunks. + chunks: 'list[Chunk]' = OptionField( + length=lambda pkt: max(pkt['__length__'], 0), + base_schema=Chunk, + type_name='type', + registry=Chunk.registry, + ) + + if TYPE_CHECKING: + def __init__(self, srcport: 'Enum_AppType | int', dstport: 'Enum_AppType | int', + vtag: 'int', chksum: 'bytes', + chunks: 'list[Chunk | bytes] | bytes') -> 'None': ... diff --git a/pcapkit/protocols/transport/NotImplemented/sctp.py b/pcapkit/protocols/transport/NotImplemented/sctp.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/pcapkit/protocols/transport/__init__.py b/pcapkit/protocols/transport/__init__.py index 5e4b2a7e5d..876841a2f8 100644 --- a/pcapkit/protocols/transport/__init__.py +++ b/pcapkit/protocols/transport/__init__.py @@ -9,12 +9,13 @@ transport layer, with detailed implementation and methods. """ -# TODO: Implements DCCP, RSVP, SCTP. +# TODO: Implements DCCP, RSVP. # Base Class for Transport Layer from pcapkit.protocols.transport.transport import Transport # Utility Classes for Protocols +from pcapkit.protocols.transport.sctp import SCTP from pcapkit.protocols.transport.tcp import TCP from pcapkit.protocols.transport.udp import UDP @@ -23,5 +24,5 @@ __all__ = [ 'TRANSTYPE', # Protocol Numbers - 'TCP', 'UDP', # Transport Layer Protocols + 'TCP', 'UDP', 'SCTP', # Transport Layer Protocols ] diff --git a/pcapkit/protocols/transport/sctp.py b/pcapkit/protocols/transport/sctp.py new file mode 100644 index 0000000000..5293d5f5fb --- /dev/null +++ b/pcapkit/protocols/transport/sctp.py @@ -0,0 +1,3459 @@ +# -*- coding: utf-8 -*- +# mypy: disable-error-code=dict-item +"""SCTP - Stream Control Transmission Protocol +================================================ + +.. module:: pcapkit.protocols.transport.sctp + +:mod:`pcapkit.protocols.transport.sctp` contains +:class:`~pcapkit.protocols.transport.sctp.SCTP` only, +which implements extractor for Stream Control +Transmission Protocol (SCTP) [*]_, whose structure is +described as below: + +======= ========= ========================= ======================================= +Octets Bits Name Description +======= ========= ========================= ======================================= + 0 0 ``sctp.srcport`` Source Port + 2 16 ``sctp.dstport`` Destination Port + 4 32 ``sctp.vtag`` Verification Tag + 8 64 ``sctp.chksum`` Checksum (CRC32c) + 12 96 ``sctp.chunks`` Chunks +======= ========= ========================= ======================================= + +.. [*] https://en.wikipedia.org/wiki/Stream_Control_Transmission_Protocol + +""" +import collections +import struct +from typing import TYPE_CHECKING, cast + +from pcapkit.const.reg.transtype import TransType as Enum_TransType +from pcapkit.const.sctp.cause_code import CauseCode as Enum_CauseCode +from pcapkit.const.sctp.chunk import Chunk as Enum_Chunk +from pcapkit.const.sctp.parameter import Parameter as Enum_Parameter +from pcapkit.const.sctp.payload_protocol_identifier import \ + PayloadProtocolIdentifier as Enum_PayloadProtocolIdentifier +from pcapkit.corekit.module import ModuleDescriptor +from pcapkit.corekit.multidict import OrderedMultiDict +from pcapkit.protocols.data.transport.sctp import SCTP as Data_SCTP +from pcapkit.protocols.data.transport.sctp import AbortChunk as Data_AbortChunk +from pcapkit.protocols.data.transport.sctp import CookieACKChunk as Data_CookieACKChunk +from pcapkit.protocols.data.transport.sctp import CookieEchoChunk as Data_CookieEchoChunk +from pcapkit.protocols.data.transport.sctp import \ + CookiePreservativeParameter as Data_CookiePreservativeParameter +from pcapkit.protocols.data.transport.sctp import \ + CookieReceivedWhileShuttingDownCause as Data_CookieReceivedWhileShuttingDownCause +from pcapkit.protocols.data.transport.sctp import DATAChunk as Data_DATAChunk +from pcapkit.protocols.data.transport.sctp import DATAChunkFlags as Data_DATAChunkFlags +from pcapkit.protocols.data.transport.sctp import ErrorChunk as Data_ErrorChunk +from pcapkit.protocols.data.transport.sctp import GapAckBlock as Data_GapAckBlock +from pcapkit.protocols.data.transport.sctp import HeartbeatACKChunk as Data_HeartbeatACKChunk +from pcapkit.protocols.data.transport.sctp import HeartbeatChunk as Data_HeartbeatChunk +from pcapkit.protocols.data.transport.sctp import \ + HeartbeatInfoParameter as Data_HeartbeatInfoParameter +from pcapkit.protocols.data.transport.sctp import \ + HostNameAddressParameter as Data_HostNameAddressParameter +from pcapkit.protocols.data.transport.sctp import INITACKChunk as Data_INITACKChunk +from pcapkit.protocols.data.transport.sctp import INITChunk as Data_INITChunk +from pcapkit.protocols.data.transport.sctp import \ + InvalidMandatoryParameterCause as Data_InvalidMandatoryParameterCause +from pcapkit.protocols.data.transport.sctp import \ + InvalidStreamIdentifierCause as Data_InvalidStreamIdentifierCause +from pcapkit.protocols.data.transport.sctp import IPv4AddressParameter as Data_IPv4AddressParameter +from pcapkit.protocols.data.transport.sctp import IPv6AddressParameter as Data_IPv6AddressParameter +from pcapkit.protocols.data.transport.sctp import \ + MissingMandatoryParameterCause as Data_MissingMandatoryParameterCause +from pcapkit.protocols.data.transport.sctp import NoUserDataCause as Data_NoUserDataCause +from pcapkit.protocols.data.transport.sctp import OutOfResourceCause as Data_OutOfResourceCause +from pcapkit.protocols.data.transport.sctp import \ + ProtocolViolationCause as Data_ProtocolViolationCause +from pcapkit.protocols.data.transport.sctp import \ + RestartOfAnAssociationWithNewAddressesCause as Data_RestartOfAnAssociationWithNewAddressesCause +from pcapkit.protocols.data.transport.sctp import SACKChunk as Data_SACKChunk +from pcapkit.protocols.data.transport.sctp import ShutdownACKChunk as Data_ShutdownACKChunk +from pcapkit.protocols.data.transport.sctp import ShutdownChunk as Data_ShutdownChunk +from pcapkit.protocols.data.transport.sctp import \ + ShutdownCompleteChunk as Data_ShutdownCompleteChunk +from pcapkit.protocols.data.transport.sctp import StaleCookieCause as Data_StaleCookieCause +from pcapkit.protocols.data.transport.sctp import StateCookieParameter as Data_StateCookieParameter +from pcapkit.protocols.data.transport.sctp import \ + SupportedAddressTypesParameter as Data_SupportedAddressTypesParameter +from pcapkit.protocols.data.transport.sctp import TBitFlags as Data_TBitFlags +from pcapkit.protocols.data.transport.sctp import UnknownCause as Data_UnknownCause +from pcapkit.protocols.data.transport.sctp import UnknownChunk as Data_UnknownChunk +from pcapkit.protocols.data.transport.sctp import UnknownParameter as Data_UnknownParameter +from pcapkit.protocols.data.transport.sctp import \ + UnrecognizedChunkTypeCause as Data_UnrecognizedChunkTypeCause +from pcapkit.protocols.data.transport.sctp import \ + UnrecognizedParameter as Data_UnrecognizedParameter +from pcapkit.protocols.data.transport.sctp import \ + UnrecognizedParametersCause as Data_UnrecognizedParametersCause +from pcapkit.protocols.data.transport.sctp import \ + UnresolvableAddressCause as Data_UnresolvableAddressCause +from pcapkit.protocols.data.transport.sctp import \ + UserInitiatedAbortCause as Data_UserInitiatedAbortCause +from pcapkit.protocols.protocol import ProtocolBase +from pcapkit.protocols.schema.schema import Schema +from pcapkit.protocols.schema.transport.sctp import SCTP as Schema_SCTP +from pcapkit.protocols.schema.transport.sctp import AbortChunk as Schema_AbortChunk +from pcapkit.protocols.schema.transport.sctp import CookieACKChunk as Schema_CookieACKChunk +from pcapkit.protocols.schema.transport.sctp import CookieEchoChunk as Schema_CookieEchoChunk +from pcapkit.protocols.schema.transport.sctp import \ + CookiePreservativeParameter as Schema_CookiePreservativeParameter +from pcapkit.protocols.schema.transport.sctp import \ + CookieReceivedWhileShuttingDownCause as Schema_CookieReceivedWhileShuttingDownCause +from pcapkit.protocols.schema.transport.sctp import DATAChunk as Schema_DATAChunk +from pcapkit.protocols.schema.transport.sctp import ErrorChunk as Schema_ErrorChunk +from pcapkit.protocols.schema.transport.sctp import GapAckBlock as Schema_GapAckBlock +from pcapkit.protocols.schema.transport.sctp import HeartbeatACKChunk as Schema_HeartbeatACKChunk +from pcapkit.protocols.schema.transport.sctp import HeartbeatChunk as Schema_HeartbeatChunk +from pcapkit.protocols.schema.transport.sctp import \ + HeartbeatInfoParameter as Schema_HeartbeatInfoParameter +from pcapkit.protocols.schema.transport.sctp import \ + HostNameAddressParameter as Schema_HostNameAddressParameter +from pcapkit.protocols.schema.transport.sctp import INITACKChunk as Schema_INITACKChunk +from pcapkit.protocols.schema.transport.sctp import INITChunk as Schema_INITChunk +from pcapkit.protocols.schema.transport.sctp import \ + InvalidMandatoryParameterCause as Schema_InvalidMandatoryParameterCause +from pcapkit.protocols.schema.transport.sctp import \ + InvalidStreamIdentifierCause as Schema_InvalidStreamIdentifierCause +from pcapkit.protocols.schema.transport.sctp import \ + IPv4AddressParameter as Schema_IPv4AddressParameter +from pcapkit.protocols.schema.transport.sctp import \ + IPv6AddressParameter as Schema_IPv6AddressParameter +from pcapkit.protocols.schema.transport.sctp import \ + MissingMandatoryParameterCause as Schema_MissingMandatoryParameterCause +from pcapkit.protocols.schema.transport.sctp import NoUserDataCause as Schema_NoUserDataCause +from pcapkit.protocols.schema.transport.sctp import OutOfResourceCause as Schema_OutOfResourceCause +from pcapkit.protocols.schema.transport.sctp import \ + ProtocolViolationCause as Schema_ProtocolViolationCause +from pcapkit.protocols.schema.transport.sctp import \ + RestartOfAnAssociationWithNewAddressesCause as \ + Schema_RestartOfAnAssociationWithNewAddressesCause +from pcapkit.protocols.schema.transport.sctp import SACKChunk as Schema_SACKChunk +from pcapkit.protocols.schema.transport.sctp import ShutdownACKChunk as Schema_ShutdownACKChunk +from pcapkit.protocols.schema.transport.sctp import ShutdownChunk as Schema_ShutdownChunk +from pcapkit.protocols.schema.transport.sctp import \ + ShutdownCompleteChunk as Schema_ShutdownCompleteChunk +from pcapkit.protocols.schema.transport.sctp import StaleCookieCause as Schema_StaleCookieCause +from pcapkit.protocols.schema.transport.sctp import \ + StateCookieParameter as Schema_StateCookieParameter +from pcapkit.protocols.schema.transport.sctp import \ + SupportedAddressTypesParameter as Schema_SupportedAddressTypesParameter +from pcapkit.protocols.schema.transport.sctp import UnknownCause as Schema_UnknownCause +from pcapkit.protocols.schema.transport.sctp import UnknownChunk as Schema_UnknownChunk +from pcapkit.protocols.schema.transport.sctp import UnknownParameter as Schema_UnknownParameter +from pcapkit.protocols.schema.transport.sctp import \ + UnrecognizedChunkTypeCause as Schema_UnrecognizedChunkTypeCause +from pcapkit.protocols.schema.transport.sctp import \ + UnrecognizedParameter as Schema_UnrecognizedParameter +from pcapkit.protocols.schema.transport.sctp import \ + UnrecognizedParametersCause as Schema_UnrecognizedParametersCause +from pcapkit.protocols.schema.transport.sctp import \ + UnresolvableAddressCause as Schema_UnresolvableAddressCause +from pcapkit.protocols.schema.transport.sctp import \ + UserInitiatedAbortCause as Schema_UserInitiatedAbortCause +from pcapkit.protocols.transport.transport import Transport +from pcapkit.utilities.exceptions import ProtocolError, RegistryError +from pcapkit.utilities.warnings import RegistryWarning, warn + +if TYPE_CHECKING: + from ipaddress import IPv4Address, IPv6Address + from typing import Any, Callable, DefaultDict, Optional, Type + + from mypy_extensions import DefaultArg, KwArg, NamedArg + from typing_extensions import Literal + + from pcapkit.const.reg.apptype import AppType as Enum_AppType + from pcapkit.protocols.data.transport.sctp import Chunk as Data_Chunk + from pcapkit.protocols.data.transport.sctp import ErrorCause as Data_ErrorCause + from pcapkit.protocols.data.transport.sctp import Parameter as Data_Parameter + from pcapkit.protocols.protocol import ProtocolBase as Protocol + from pcapkit.protocols.schema.transport.sctp import Chunk as Schema_Chunk + from pcapkit.protocols.schema.transport.sctp import ErrorCause as Schema_ErrorCause + from pcapkit.protocols.schema.transport.sctp import Parameter as Schema_Parameter + + Chunks = OrderedMultiDict[Enum_Chunk, Data_Chunk] + Parameters = OrderedMultiDict[Enum_Parameter, Data_Parameter] + Causes = OrderedMultiDict[Enum_CauseCode, Data_ErrorCause] + + ChunkParser = Callable[[Schema_Chunk, NamedArg(Chunks, 'chunks')], Data_Chunk] + ChunkConstructor = Callable[[Enum_Chunk, DefaultArg(Optional[Data_Chunk]), + KwArg(Any)], Schema_Chunk] + + ParameterParser = Callable[[Schema_Parameter, + NamedArg(Parameters, 'parameters')], Data_Parameter] + ParameterConstructor = Callable[[Enum_Parameter, DefaultArg(Optional[Data_Parameter]), + KwArg(Any)], Schema_Parameter] + + CauseParser = Callable[[Schema_ErrorCause, NamedArg(Causes, 'causes')], Data_ErrorCause] + CauseConstructor = Callable[[Enum_CauseCode, DefaultArg(Optional[Data_ErrorCause]), + KwArg(Any)], Schema_ErrorCause] + +__all__ = ['SCTP'] + +#: Reflected CRC32c (Castagnoli) lookup table, as generated by the sample code +#: of :rfc:`9260#appendix-A` with ``TB_POLY=0x1EDC6F41`` and ``TB_REVER=TRUE``. +CRC32C_TABLE = [] # type: list[int] +for _index in range(256): + _crc = _index + for _ in range(8): + _crc = (_crc >> 1) ^ (0x82F63B78 if _crc & 1 else 0) + CRC32C_TABLE.append(_crc) +del _index, _crc + + +class SCTP(Transport[Data_SCTP, Schema_SCTP], + schema=Schema_SCTP, data=Data_SCTP): + """This class implements Stream Control Transmission Protocol. + + Unlike :class:`~pcapkit.protocols.transport.tcp.TCP` and + :class:`~pcapkit.protocols.transport.udp.UDP`, SCTP does **not** dispatch + the next layer on port numbers: user data travels inside DATA chunks, and + each DATA chunk names its upper layer through its *payload protocol + identifier* (PPID). The :attr:`self.__proto__ ` registry is + therefore keyed by PPID rather than by port number, and is populated through + :meth:`SCTP.register`:: + + >>> SCTP.register(Enum_PayloadProtocolIdentifier.PayloadProtocolIdentifier_3GPP_NG_Application_Protocol, NGAP) + >>> SCTP.register(60, NGAP) # equivalent, PPID given as a plain integer + + No PPID is registered by default. + + This class currently supports parsing of the following SCTP chunks, which + are directly mapped to the :class:`pcapkit.const.sctp.chunk.Chunk` + enumeration: + + .. list-table:: + :header-rows: 1 + + * - Chunk Type + - Chunk Parser + - Chunk Constructor + * - :attr:`~pcapkit.const.sctp.chunk.Chunk.Payload_Data` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_chunk_data` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_chunk_data` + * - :attr:`~pcapkit.const.sctp.chunk.Chunk.Initiation` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_chunk_init` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_chunk_init` + * - :attr:`~pcapkit.const.sctp.chunk.Chunk.Initiation_Acknowledgement` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_chunk_init_ack` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_chunk_init_ack` + * - :attr:`~pcapkit.const.sctp.chunk.Chunk.Selective_Acknowledgement` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_chunk_sack` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_chunk_sack` + * - :attr:`~pcapkit.const.sctp.chunk.Chunk.Heartbeat_Request` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_chunk_heartbeat` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_chunk_heartbeat` + * - :attr:`~pcapkit.const.sctp.chunk.Chunk.Heartbeat_Acknowledgement` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_chunk_heartbeat_ack` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_chunk_heartbeat_ack` + * - :attr:`~pcapkit.const.sctp.chunk.Chunk.Abort` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_chunk_abort` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_chunk_abort` + * - :attr:`~pcapkit.const.sctp.chunk.Chunk.Shutdown` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_chunk_shutdown` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_chunk_shutdown` + * - :attr:`~pcapkit.const.sctp.chunk.Chunk.Shutdown_Acknowledgement` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_chunk_shutdown_ack` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_chunk_shutdown_ack` + * - :attr:`~pcapkit.const.sctp.chunk.Chunk.Operation_Error` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_chunk_error` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_chunk_error` + * - :attr:`~pcapkit.const.sctp.chunk.Chunk.State_Cookie` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_chunk_cookie_echo` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_chunk_cookie_echo` + * - :attr:`~pcapkit.const.sctp.chunk.Chunk.Cookie_Acknowledgement` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_chunk_cookie_ack` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_chunk_cookie_ack` + * - :attr:`~pcapkit.const.sctp.chunk.Chunk.Shutdown_Complete` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_chunk_shutdown_complete` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_chunk_shutdown_complete` + + Any other chunk type -- unassigned, reserved, or defined by an SCTP + extension that :mod:`pcapkit` does not implement -- falls through to + :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_chunk_donone`, which + records the chunk's raw flags and value verbatim rather than raising. + + This class currently supports parsing of the following chunk parameters, + which are directly mapped to the + :class:`pcapkit.const.sctp.parameter.Parameter` enumeration: + + .. list-table:: + :header-rows: 1 + + * - Parameter Type + - Parameter Parser + - Parameter Constructor + * - :attr:`~pcapkit.const.sctp.parameter.Parameter.Heartbeat_Info` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_param_hbinfo` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_param_hbinfo` + * - :attr:`~pcapkit.const.sctp.parameter.Parameter.IPv4_Address` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_param_ipv4` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_param_ipv4` + * - :attr:`~pcapkit.const.sctp.parameter.Parameter.IPv6_Address` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_param_ipv6` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_param_ipv6` + * - :attr:`~pcapkit.const.sctp.parameter.Parameter.State_Cookie` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_param_cookie` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_param_cookie` + * - :attr:`~pcapkit.const.sctp.parameter.Parameter.Unrecognized_Parameter` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_param_unrecognized` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_param_unrecognized` + * - :attr:`~pcapkit.const.sctp.parameter.Parameter.Cookie_Preservative` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_param_preservative` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_param_preservative` + * - :attr:`~pcapkit.const.sctp.parameter.Parameter.Host_Name_Address` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_param_hostname` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_param_hostname` + * - :attr:`~pcapkit.const.sctp.parameter.Parameter.Supported_Address_Types` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_param_addrtypes` + - :meth:`~pcapkit.protocols.transport.sctp.SCTP._make_param_addrtypes` + + This class currently supports parsing of all thirteen error causes defined + by :rfc:`9260#section-3.3.10`, which are directly mapped to the + :class:`pcapkit.const.sctp.cause_code.CauseCode` enumeration; see + :attr:`self.__cause__ ` for the mapping. Unknown cause + codes fall through to + :meth:`~pcapkit.protocols.transport.sctp.SCTP._read_cause_donone`. + + """ + + ########################################################################## + # Defaults. + ########################################################################## + + #: Payload of the packet, i.e. the user data of the first DATA chunk found + #: in the packet, as located by :meth:`self.read `. + _payload = b'' # type: bytes + + #: Payload protocol identifier of the first DATA chunk found in the packet. + _ppid = None # type: Optional[Enum_PayloadProtocolIdentifier] + + #: DefaultDict[int, ModuleDescriptor[Protocol] | Type[Protocol]]: Protocol + #: index mapping for decoding next layer, c.f. + #: :meth:`self._decode_next_layer ` + #: & :meth:`self._import_next_layer `. + #: + #: Important: + #: Keyed by the DATA chunk's *payload protocol identifier* (PPID), **not** + #: by port number as in :class:`~pcapkit.protocols.transport.tcp.TCP` and + #: :class:`~pcapkit.protocols.transport.udp.UDP`. + __proto__ = collections.defaultdict( + lambda: ModuleDescriptor('pcapkit.protocols.misc.raw', 'Raw'), + {}, + ) # type: DefaultDict[int, ModuleDescriptor[Protocol] | Type[Protocol]] + + #: DefaultDict[Enum_Chunk, str | tuple[ChunkParser, ChunkConstructor]]: Chunk + #: type to method mapping, c.f. :meth:`_read_sctp_chunks` and + #: :meth:`_make_sctp_chunks`. Method names are expected to be referred to + #: the class by ``_read_chunk_${name}`` and ``_make_chunk_${name}``, and if + #: such name not found, the value should then be a method that can parse the + #: chunk by itself. + __chunk__ = collections.defaultdict( + lambda: 'donone', + { + Enum_Chunk.Payload_Data: 'data', # [RFC 9260] DATA + Enum_Chunk.Initiation: 'init', # [RFC 9260] INIT + Enum_Chunk.Initiation_Acknowledgement: 'init_ack', # [RFC 9260] INIT ACK + Enum_Chunk.Selective_Acknowledgement: 'sack', # [RFC 9260] SACK + Enum_Chunk.Heartbeat_Request: 'heartbeat', # [RFC 9260] HEARTBEAT + Enum_Chunk.Heartbeat_Acknowledgement: 'heartbeat_ack', # [RFC 9260] HEARTBEAT ACK + Enum_Chunk.Abort: 'abort', # [RFC 9260] ABORT + Enum_Chunk.Shutdown: 'shutdown', # [RFC 9260] SHUTDOWN + Enum_Chunk.Shutdown_Acknowledgement: 'shutdown_ack', # [RFC 9260] SHUTDOWN ACK + Enum_Chunk.Operation_Error: 'error', # [RFC 9260] ERROR + Enum_Chunk.State_Cookie: 'cookie_echo', # [RFC 9260] COOKIE ECHO + Enum_Chunk.Cookie_Acknowledgement: 'cookie_ack', # [RFC 9260] COOKIE ACK + Enum_Chunk.Shutdown_Complete: 'shutdown_complete', # [RFC 9260] SHUTDOWN COMPLETE + }, + ) # type: DefaultDict[int, str | tuple[ChunkParser, ChunkConstructor]] + + #: DefaultDict[Enum_Parameter, str | tuple[ParameterParser, ParameterConstructor]]: + #: Chunk parameter type to method mapping, c.f. :meth:`_read_sctp_parameters` + #: and :meth:`_make_sctp_parameters`. Method names are expected to be + #: referred to the class by ``_read_param_${name}`` and + #: ``_make_param_${name}``, and if such name not found, the value should then + #: be a method that can parse the parameter by itself. + __parameter__ = collections.defaultdict( + lambda: 'donone', + { + Enum_Parameter.Heartbeat_Info: 'hbinfo', # [RFC 9260] Heartbeat Info + Enum_Parameter.IPv4_Address: 'ipv4', # [RFC 9260] IPv4 Address + Enum_Parameter.IPv6_Address: 'ipv6', # [RFC 9260] IPv6 Address + Enum_Parameter.State_Cookie: 'cookie', # [RFC 9260] State Cookie + Enum_Parameter.Unrecognized_Parameter: 'unrecognized', # [RFC 9260] Unrecognized + Enum_Parameter.Cookie_Preservative: 'preservative', # [RFC 9260] Cookie Preservative + Enum_Parameter.Host_Name_Address: 'hostname', # [RFC 9260] Host Name Address + Enum_Parameter.Supported_Address_Types: 'addrtypes', # [RFC 9260] Supported Addr Types + }, + ) # type: DefaultDict[int, str | tuple[ParameterParser, ParameterConstructor]] + + #: DefaultDict[Enum_CauseCode, str | tuple[CauseParser, CauseConstructor]]: Error + #: cause code to method mapping, c.f. :meth:`_read_sctp_causes` and + #: :meth:`_make_sctp_causes`. Method names are expected to be referred to the + #: class by ``_read_cause_${name}`` and ``_make_cause_${name}``, and if such + #: name not found, the value should then be a method that can parse the error + #: cause by itself. + __cause__ = collections.defaultdict( + lambda: 'donone', + { + Enum_CauseCode.Invalid_Stream_Identifier: 'invalid_stream', + Enum_CauseCode.Missing_Mandatory_Parameter: 'missing_param', + Enum_CauseCode.Stale_Cookie: 'stale_cookie', + Enum_CauseCode.Out_of_Resource: 'out_of_resource', + Enum_CauseCode.Unresolvable_Address: 'unresolvable_addr', + Enum_CauseCode.Unrecognized_Chunk_Type: 'unrecognized_chunk', + Enum_CauseCode.Invalid_Mandatory_Parameter: 'invalid_param', + Enum_CauseCode.Unrecognized_Parameters: 'unrecognized_params', + Enum_CauseCode.No_User_Data: 'no_user_data', + Enum_CauseCode.Cookie_Received_While_Shutting_Down: 'cookie_shutdown', + Enum_CauseCode.Restart_of_an_Association_with_New_Addresses: 'restart_addr', + Enum_CauseCode.User_Initiated_Abort: 'user_abort', + Enum_CauseCode.Protocol_Violation: 'protocol_violation', + }, + ) # type: DefaultDict[int, str | tuple[CauseParser, CauseConstructor]] + + ########################################################################## + # Properties. + ########################################################################## + + @property + def name(self) -> 'Literal["Stream Control Transmission Protocol"]': + """Name of current protocol.""" + return 'Stream Control Transmission Protocol' + + @property + def length(self) -> 'Literal[12]': + """Header length of current protocol, i.e. the SCTP common header.""" + return 12 + + @property + def src(self) -> 'Enum_AppType': + """Source port.""" + return self._info.srcport + + @property + def dst(self) -> 'Enum_AppType': + """Destination port.""" + return self._info.dstport + + @property + def ppid(self) -> 'Optional[Enum_PayloadProtocolIdentifier]': + """Payload protocol identifier of the first DATA chunk of the packet. + + Returns: + The PPID used to dispatch the next layer, or :obj:`None` if the + packet carries no DATA chunk. + + """ + return self._ppid + + @property + def checksum_valid(self) -> 'bool': + """Whether the recorded CRC32c checksum matches the packet. + + The SCTP checksum covers the common header and every chunk with the + checksum field itself zeroed, and -- unlike the TCP and UDP checksums -- + involves no IP pseudo-header, so it can be verified from the SCTP packet + alone. See :rfc:`9260#section-6.8`. + + """ + return self.validate_checksum(bytes(self.__header__)) + + ########################################################################## + # Methods. + ########################################################################## + + def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_SCTP': # pylint: disable=unused-argument + """Read Stream Control Transmission Protocol (SCTP). + + Structure of SCTP common header [:rfc:`9260`]: + + .. 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Source Port Number | Destination Port Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Verification Tag | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Chunk #1 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | ... | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Chunk #n | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + length: Length of packet data. + **kwargs: Arbitrary keyword arguments. + + Returns: + Parsed packet data. + + """ + if length is None: + length = len(self) + schema = self.__header__ + + sctp = Data_SCTP( + srcport=schema.srcport, + dstport=schema.dstport, + vtag=schema.vtag, + chksum=schema.chksum, + chunks=self._read_sctp_chunks(), + ) + + # NOTE: The next layer is named by the DATA chunk's payload protocol + # identifier, not by a port number. A packet may bundle several DATA + # chunks; we dispatch on the first one, and the rest stay recorded in + # the chunk list. + self._ppid = None + self._payload = b'' + for chunk in schema.chunks: + if chunk.type == Enum_Chunk.Payload_Data: + chunk = cast('Schema_DATAChunk', chunk) + self._ppid = chunk.ppid + self._payload = chunk.data + break + + return self._decode_next_layer(sctp, self._ppid, len(self._payload)) + + def make(self, + srcport: 'Enum_AppType | int' = 0, + dstport: 'Enum_AppType | int' = 0, + vtag: 'int' = 0, + chksum: 'Optional[bytes]' = None, + chunks: 'Optional[list[Schema_Chunk | tuple[Enum_Chunk, dict[str, Any]] | bytes] | Chunks]' = None, # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_SCTP': + """Make (construct) packet data. + + Args: + srcport: Source port. + dstport: Destination port. + vtag: Verification tag. + chksum: Checksum. If :obj:`None`, the CRC32c checksum of the + constructed packet is computed and inserted, per + :rfc:`9260#section-6.8`. + chunks: SCTP chunks. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed packet data. + + Note: + There is no ``payload`` argument: SCTP carries its user data in the + ``data`` field of a DATA chunk, so the payload is supplied as part + of that chunk. + + """ + if chunks is not None: + chunks_value = self._make_sctp_chunks(chunks) + else: + chunks_value = [] + + schema = Schema_SCTP( + srcport=srcport, + dstport=dstport, + vtag=vtag, + chksum=b'\x00\x00\x00\x00' if chksum is None else chksum, + chunks=chunks_value, + ) + + if chksum is None: + schema.chksum = self.calculate_checksum(schema.pack()) + return schema + + @classmethod + def register(cls, code: 'Enum_PayloadProtocolIdentifier | int', protocol: 'ModuleDescriptor[Protocol] | Type[Protocol]') -> 'None': # type: ignore[override] # pylint: disable=line-too-long + r"""Register a new protocol class for a payload protocol identifier. + + Notes: + The full qualified class name of the new protocol class + should be as ``{protocol.module}.{protocol.name}``. + + Arguments: + code: payload protocol identifier (PPID), as in + :class:`~pcapkit.const.sctp.payload_protocol_identifier.PayloadProtocolIdentifier` + protocol: module descriptor or a + :class:`~pcapkit.protocols.protocol.Protocol` subclass + + Important: + SCTP overrides :meth:`Transport.register + ` because + its :attr:`self.__proto__ ` registry is keyed by + PPID rather than by port number. + + """ + if isinstance(protocol, ModuleDescriptor): + protocol = protocol.klass + if not issubclass(protocol, ProtocolBase): + raise RegistryError(f'protocol must be a Protocol subclass, not {protocol!r}') + if code in cls.__proto__: + warn(f'payload protocol identifier {code} already registered, overwriting', + RegistryWarning) + cls.__proto__[code] = protocol + + @classmethod + def register_chunk(cls, code: 'Enum_Chunk', meth: 'str | tuple[ChunkParser, ChunkConstructor]') -> 'None': + """Register a chunk parser. + + Args: + code: SCTP chunk type. + meth: Method name or callable to parse and/or construct the chunk. + + """ + if code in cls.__chunk__: + warn(f'chunk {code} already registered, overwriting', RegistryWarning) + cls.__chunk__[code] = meth + + @classmethod + def register_parameter(cls, code: 'Enum_Parameter', meth: 'str | tuple[ParameterParser, ParameterConstructor]') -> 'None': + """Register a chunk parameter parser. + + Args: + code: SCTP chunk parameter type. + meth: Method name or callable to parse and/or construct the parameter. + + """ + if code in cls.__parameter__: + warn(f'parameter {code} already registered, overwriting', RegistryWarning) + cls.__parameter__[code] = meth + + @classmethod + def register_cause(cls, code: 'Enum_CauseCode', meth: 'str | tuple[CauseParser, CauseConstructor]') -> 'None': + """Register an error cause parser. + + Args: + code: SCTP error cause code. + meth: Method name or callable to parse and/or construct the cause. + + """ + if code in cls.__cause__: + warn(f'error cause {code} already registered, overwriting', RegistryWarning) + cls.__cause__[code] = meth + + @staticmethod + def crc32c(data: 'bytes') -> 'int': + """Calculate the CRC32c of ``data``. + + SCTP uses the CRC32c (Castagnoli) polynomial rather than the one's + complement internet checksum used by TCP, UDP and IP. The algorithm is + the reflected, table-driven one given by :rfc:`9260#appendix-A`, with + the remainder register initialised to all ones and the result + complemented. + + Args: + data: Data to checksum. + + Returns: + The CRC32c value, in host order. + + """ + crc = 0xFFFFFFFF + for byte in data: + crc = (crc >> 8) ^ CRC32C_TABLE[(crc ^ byte) & 0xFF] + return crc ^ 0xFFFFFFFF + + @classmethod + def calculate_checksum(cls, packet: 'bytes') -> 'bytes': + """Calculate the checksum field of an SCTP packet. + + Per :rfc:`9260#section-6.8`, the checksum field is first zeroed, the + CRC32c of the whole packet is then computed, and the result is written + back into the checksum field. Per :rfc:`9260#appendix-A` the resulting + four bytes are the CRC32c value in *little*-endian order. + + Args: + packet: Whole SCTP packet, i.e. the common header followed by every + chunk. The current contents of the checksum field are ignored. + + Returns: + The four bytes to place in the checksum field. + + Raises: + ProtocolError: If ``packet`` is shorter than the 12-byte SCTP + common header. + + """ + if len(packet) < 12: + raise ProtocolError('SCTP: invalid format') + + zeroed = packet[:8] + b'\x00\x00\x00\x00' + packet[12:] + return struct.pack(' 'bool': + """Validate the checksum field of an SCTP packet. + + Args: + packet: Whole SCTP packet, i.e. the common header followed by every + chunk. + + Returns: + Whether the checksum field matches the packet contents. + + Raises: + ProtocolError: If ``packet`` is shorter than the 12-byte SCTP + common header. + + """ + return cls.calculate_checksum(packet) == packet[8:12] + + ########################################################################## + # Data models. + ########################################################################## + + def __length_hint__(self) -> 'Literal[12]': + """Return an estimated length for the object.""" + return 12 + + @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.SCTP # type: ignore[return-value] + + ########################################################################## + # Utilities. + ########################################################################## + + @classmethod + def _make_data(cls, data: 'Data_SCTP') -> '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 { + 'srcport': data.srcport, + 'dstport': data.dstport, + 'vtag': data.vtag, + 'chksum': data.chksum, + 'chunks': data.chunks, + } + + def _get_payload(self) -> 'bytes': + """Get payload of the packet. + + SCTP has no payload field in its header schema -- user data travels + inside DATA chunks -- so this returns the user data of the *first* DATA + chunk found by :meth:`self.read `, which is also the chunk + whose payload protocol identifier selects the next layer. Should the + packet carry no DATA chunk, an empty :obj:`bytes` is returned and the + next layer resolves to + :class:`~pcapkit.protocols.misc.null.NoPayload`. + + Returns: + Payload of the packet as :obj:`bytes`. + + """ + return self._payload + + def _decode_next_layer(self, dict_: 'Data_SCTP', proto: 'Optional[int]' = None, # type: ignore[override] + length: 'Optional[int]' = None, *, + packet: 'Optional[dict[str, Any]]' = None) -> 'Data_SCTP': + r"""Decode next layer protocol. + + Arguments: + dict\_: info buffer + proto: payload protocol identifier of the DATA chunk carrying the + payload, if any + length: valid (*non-padding*) length + packet: packet info (passed from :meth:`self.unpack `) + + Returns: + Current protocol with next layer extracted. + + Important: + This deliberately bypasses :meth:`Transport._decode_next_layer + `, + which keys the lookup on port numbers, since SCTP keys it on the + DATA chunk's payload protocol identifier instead. + + """ + if proto is not None and proto not in self.__proto__: + proto = None + return ProtocolBase._decode_next_layer( # pylint: disable=protected-access + self, dict_, proto, length, packet=packet) # type: ignore[arg-type,return-value] + + def _read_sctp_chunks(self) -> 'Chunks': + """Read SCTP chunk list. + + Returns: + Extracted SCTP chunks. + + """ + chunks = OrderedMultiDict() # type: Chunks + + for schema in self.__header__.chunks: + code = schema.type + name = self.__chunk__[code] + + if isinstance(name, str): + meth_name = f'_read_chunk_{name}' + meth = cast('ChunkParser', + getattr(self, meth_name, self._read_chunk_donone)) + else: + meth = name[0] + chunks.add(code, meth(schema, chunks=chunks)) + return chunks + + def _make_sctp_chunks(self, chunks: 'list[Schema_Chunk | tuple[Enum_Chunk, dict[str, Any]] | bytes] | Chunks') -> 'list[Schema_Chunk | bytes]': # pylint: disable=line-too-long + """Make chunks for SCTP. + + Args: + chunks: SCTP chunks. + + Returns: + Constructed chunk schemas. + + Note: + No alignment fix-up happens here, unlike + :meth:`TCP._make_tcp_options + `: every + chunk schema carries its own trailing + :class:`~pcapkit.corekit.fields.strings.PaddingField`, computed from + the chunk's own ``length``, so a chunk pads itself to the four-byte + boundary required by :rfc:`9260#section-3.2`. + + """ + chunks_list = [] # type: list[Schema_Chunk | bytes] + + if isinstance(chunks, list): + for schema in chunks: + if isinstance(schema, bytes): + chunks_list.append(schema) + elif isinstance(schema, Schema): + chunks_list.append(schema) + else: + code, args = cast('tuple[Enum_Chunk, dict[str, Any]]', schema) + chunks_list.append(self._make_sctp_chunk(code, None, **args)) + return chunks_list + + for code, chunk in chunks.items(multi=True): + chunks_list.append(self._make_sctp_chunk(code, chunk)) + return chunks_list + + def _make_sctp_chunk(self, code: 'Enum_Chunk', chunk: 'Optional[Data_Chunk]' = None, + **kwargs: 'Any') -> 'Schema_Chunk': + """Dispatch to the chunk constructor registered for ``code``. + + Args: + code: SCTP chunk type. + chunk: Chunk data, if constructing from a parsed data model. + **kwargs: Arbitrary keyword arguments for the constructor. + + Returns: + Constructed chunk schema. + + """ + name = self.__chunk__[code] + if isinstance(name, str): + meth_name = f'_make_chunk_{name}' + meth = cast('ChunkConstructor', + getattr(self, meth_name, self._make_chunk_donone)) + else: + meth = name[1] + return meth(code, chunk, **kwargs) + + def _read_sctp_parameters(self, schemas: 'list[Schema_Parameter]') -> 'Parameters': + """Read SCTP chunk parameter list. + + Args: + schemas: Parsed parameter schemas. + + Returns: + Extracted SCTP chunk parameters. + + """ + parameters = OrderedMultiDict() # type: Parameters + + for schema in schemas: + code = schema.type + name = self.__parameter__[code] + + if isinstance(name, str): + meth_name = f'_read_param_{name}' + meth = cast('ParameterParser', + getattr(self, meth_name, self._read_param_donone)) + else: + meth = name[0] + parameters.add(code, meth(schema, parameters=parameters)) + return parameters + + def _make_sctp_parameters(self, parameters: 'list[Schema_Parameter | tuple[Enum_Parameter, dict[str, Any]] | bytes] | Parameters') -> 'list[Schema_Parameter | bytes]': # pylint: disable=line-too-long + """Make chunk parameters for SCTP. + + Args: + parameters: SCTP chunk parameters. + + Returns: + Constructed parameter schemas. + + """ + parameters_list = [] # type: list[Schema_Parameter | bytes] + + if isinstance(parameters, list): + for schema in parameters: + if isinstance(schema, (bytes, Schema)): + parameters_list.append(schema) + else: + code, args = cast('tuple[Enum_Parameter, dict[str, Any]]', schema) + parameters_list.append(self._make_sctp_parameter(code, None, **args)) + return parameters_list + + for code, parameter in parameters.items(multi=True): + parameters_list.append(self._make_sctp_parameter(code, parameter)) + return parameters_list + + def _make_sctp_parameter(self, code: 'Enum_Parameter', parameter: 'Optional[Data_Parameter]' = None, + **kwargs: 'Any') -> 'Schema_Parameter': + """Dispatch to the parameter constructor registered for ``code``. + + Args: + code: SCTP chunk parameter type. + parameter: Parameter data, if constructing from a parsed data model. + **kwargs: Arbitrary keyword arguments for the constructor. + + Returns: + Constructed parameter schema. + + """ + name = self.__parameter__[code] + if isinstance(name, str): + meth_name = f'_make_param_{name}' + meth = cast('ParameterConstructor', + getattr(self, meth_name, self._make_param_donone)) + else: + meth = name[1] + return meth(code, parameter, **kwargs) + + def _read_sctp_causes(self, schemas: 'list[Schema_ErrorCause]') -> 'Causes': + """Read SCTP error cause list. + + Args: + schemas: Parsed error cause schemas. + + Returns: + Extracted SCTP error causes. + + """ + causes = OrderedMultiDict() # type: Causes + + for schema in schemas: + code = schema.code + name = self.__cause__[code] + + if isinstance(name, str): + meth_name = f'_read_cause_{name}' + meth = cast('CauseParser', + getattr(self, meth_name, self._read_cause_donone)) + else: + meth = name[0] + causes.add(code, meth(schema, causes=causes)) + return causes + + def _make_sctp_causes(self, causes: 'list[Schema_ErrorCause | tuple[Enum_CauseCode, dict[str, Any]] | bytes] | Causes') -> 'list[Schema_ErrorCause | bytes]': # pylint: disable=line-too-long + """Make error causes for SCTP. + + Args: + causes: SCTP error causes. + + Returns: + Constructed error cause schemas. + + """ + causes_list = [] # type: list[Schema_ErrorCause | bytes] + + if isinstance(causes, list): + for schema in causes: + if isinstance(schema, (bytes, Schema)): + causes_list.append(schema) + else: + code, args = cast('tuple[Enum_CauseCode, dict[str, Any]]', schema) + causes_list.append(self._make_sctp_cause(code, None, **args)) + return causes_list + + for code, cause in causes.items(multi=True): + causes_list.append(self._make_sctp_cause(code, cause)) + return causes_list + + def _make_sctp_cause(self, code: 'Enum_CauseCode', cause: 'Optional[Data_ErrorCause]' = None, + **kwargs: 'Any') -> 'Schema_ErrorCause': + """Dispatch to the error cause constructor registered for ``code``. + + Args: + code: SCTP error cause code. + cause: Cause data, if constructing from a parsed data model. + **kwargs: Arbitrary keyword arguments for the constructor. + + Returns: + Constructed error cause schema. + + """ + name = self.__cause__[code] + if isinstance(name, str): + meth_name = f'_make_cause_{name}' + meth = cast('CauseConstructor', + getattr(self, meth_name, self._make_cause_donone)) + else: + meth = name[1] + return meth(code, cause, **kwargs) + + def _read_chunk_donone(self, schema: 'Schema_UnknownChunk', *, chunks: 'Chunks') -> 'Data_UnknownChunk': # pylint: disable=unused-argument + """Read SCTP chunk of an unsupported type. + + This is the fall-through for every chunk type :mod:`pcapkit` does not + implement -- unassigned, reserved, or defined by an SCTP extension -- + as well as for the SCTP-defined but reserved ECNE and CWR chunks. The + chunk's raw flags and value are recorded verbatim rather than raising, + so that a bundle containing an unknown chunk still parses. + + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks + + Returns: + Parsed chunk data. + + """ + return Data_UnknownChunk( + type=schema.type, + length=schema.length, + flags=schema.flags, + value=schema.value, + ) + + def _read_chunk_data(self, schema: 'Schema_DATAChunk', *, chunks: 'Chunks') -> 'Data_DATAChunk': # pylint: disable=unused-argument + """Read SCTP DATA chunk. + + Structure of SCTP DATA chunk [:rfc:`9260#section-3.3.1`]: + + .. 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 = 0 | Res |I|U|B|E| Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | TSN | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Stream Identifier S | Stream Sequence Number n | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Payload Protocol Identifier | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + \\ \\ + / User Data (seq n of Stream S) / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks + + Returns: + Parsed chunk data. + + Raises: + ProtocolError: If ``length`` is **NOT** greater than ``16``, since + :rfc:`9260#section-3.3.1` requires at least one byte of user + data. + + """ + if schema.length <= 16: + raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') + + return Data_DATAChunk( + type=schema.type, + length=schema.length, + flags=Data_DATAChunkFlags( + I=bool(schema.flags['I']), + U=bool(schema.flags['U']), + B=bool(schema.flags['B']), + E=bool(schema.flags['E']), + ), + tsn=schema.tsn, + stream_id=schema.stream_id, + stream_seq=schema.stream_seq, + ppid=schema.ppid, + data=schema.data, + ) + + def _read_chunk_init(self, schema: 'Schema_INITChunk', *, chunks: 'Chunks') -> 'Data_INITChunk': # pylint: disable=unused-argument + """Read SCTP INIT chunk. + + Structure of SCTP INIT chunk [:rfc:`9260#section-3.3.2`]: + + .. 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 = 1 | Chunk Flags | Chunk Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Initiate Tag | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Advertised Receiver Window Credit (a_rwnd) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Number of Outbound Streams | Number of Inbound Streams | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Initial TSN | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + \\ \\ + / Optional/Variable-Length Parameters / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks + + Returns: + Parsed chunk data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``20``. + + Note: + The chunk flags are reserved by :rfc:`9260#section-3.3.2` and are + therefore not exposed on the data model. + + """ + if schema.length < 20: + raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') + + return Data_INITChunk( + type=schema.type, + length=schema.length, + init_tag=schema.init_tag, + a_rwnd=schema.a_rwnd, + outbound_streams=schema.outbound_streams, + inbound_streams=schema.inbound_streams, + init_tsn=schema.init_tsn, + parameters=self._read_sctp_parameters(schema.parameters), + ) + + def _read_chunk_init_ack(self, schema: 'Schema_INITACKChunk', *, chunks: 'Chunks') -> 'Data_INITACKChunk': # pylint: disable=unused-argument + """Read SCTP INIT ACK chunk. + + Structure of SCTP INIT ACK chunk [:rfc:`9260#section-3.3.3`]: + + .. 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 = 2 | Chunk Flags | Chunk Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Initiate Tag | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Advertised Receiver Window Credit | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Number of Outbound Streams | Number of Inbound Streams | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Initial TSN | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + \\ \\ + / Optional/Variable-Length Parameters / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks + + Returns: + Parsed chunk data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``20``. + + """ + if schema.length < 20: + raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') + + return Data_INITACKChunk( + type=schema.type, + length=schema.length, + init_tag=schema.init_tag, + a_rwnd=schema.a_rwnd, + outbound_streams=schema.outbound_streams, + inbound_streams=schema.inbound_streams, + init_tsn=schema.init_tsn, + parameters=self._read_sctp_parameters(schema.parameters), + ) + + def _read_chunk_sack(self, schema: 'Schema_SACKChunk', *, chunks: 'Chunks') -> 'Data_SACKChunk': # pylint: disable=unused-argument + """Read SCTP SACK chunk. + + Structure of SCTP SACK chunk [:rfc:`9260#section-3.3.4`]: + + .. 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 = 3 | Chunk Flags | Chunk Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Cumulative TSN Ack | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Advertised Receiver Window Credit (a_rwnd) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Number of Gap Ack Blocks = N | Number of Duplicate TSNs = M | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Gap Ack Block #1 Start | Gap Ack Block #1 End | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / ... / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Duplicate TSN 1 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / ... / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks + + Returns: + Parsed chunk data. + + Raises: + ProtocolError: If ``length`` does **NOT** match the declared number + of gap ack blocks and duplicate TSNs. + + """ + if schema.length != 16 + schema.num_gap_blocks * 4 + schema.num_dup_tsn * 4: + raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') + + return Data_SACKChunk( + type=schema.type, + length=schema.length, + cum_tsn_ack=schema.cum_tsn_ack, + a_rwnd=schema.a_rwnd, + num_gap_blocks=schema.num_gap_blocks, + num_dup_tsn=schema.num_dup_tsn, + gap_blocks=tuple( + Data_GapAckBlock( + start=block.start, + end=block.end, + ) for block in schema.gap_blocks + ), + dup_tsn=tuple(schema.dup_tsn), + ) + + def _read_chunk_heartbeat(self, schema: 'Schema_HeartbeatChunk', *, chunks: 'Chunks') -> 'Data_HeartbeatChunk': # pylint: disable=unused-argument + """Read SCTP HEARTBEAT chunk. + + Structure of SCTP HEARTBEAT chunk [:rfc:`9260#section-3.3.5`]: + + .. 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 = 4 | Chunk Flags | Heartbeat Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + \\ \\ + / Heartbeat Information TLV (Variable-Length) / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks + + Returns: + Parsed chunk data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``4``. + + Note: + :rfc:`9260#section-3.3.5` mandates exactly one Heartbeat Info + parameter, but the parameters are modelled as a list so that a + sender emitting more (or none) still parses. + + """ + if schema.length < 4: + raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') + + return Data_HeartbeatChunk( + type=schema.type, + length=schema.length, + parameters=self._read_sctp_parameters(schema.parameters), + ) + + def _read_chunk_heartbeat_ack(self, schema: 'Schema_HeartbeatACKChunk', *, chunks: 'Chunks') -> 'Data_HeartbeatACKChunk': # pylint: disable=unused-argument + """Read SCTP HEARTBEAT ACK chunk. + + Structure of SCTP HEARTBEAT ACK chunk [:rfc:`9260#section-3.3.6`]: + + .. 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 = 5 | Chunk Flags | Heartbeat Ack Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + \\ \\ + / Heartbeat Information TLV (Variable-Length) / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks + + Returns: + Parsed chunk data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``4``. + + """ + if schema.length < 4: + raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') + + return Data_HeartbeatACKChunk( + type=schema.type, + length=schema.length, + parameters=self._read_sctp_parameters(schema.parameters), + ) + + def _read_chunk_abort(self, schema: 'Schema_AbortChunk', *, chunks: 'Chunks') -> 'Data_AbortChunk': # pylint: disable=unused-argument + """Read SCTP ABORT chunk. + + Structure of SCTP ABORT chunk [:rfc:`9260#section-3.3.7`]: + + .. 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 = 6 | Reserved |T| Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + \\ \\ + / zero or more Error Causes / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks + + Returns: + Parsed chunk data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``4``. + + """ + if schema.length < 4: + raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') + + return Data_AbortChunk( + type=schema.type, + length=schema.length, + flags=Data_TBitFlags( + T=bool(schema.flags['T']), + ), + error=self._read_sctp_causes(schema.error), + ) + + def _read_chunk_shutdown(self, schema: 'Schema_ShutdownChunk', *, chunks: 'Chunks') -> 'Data_ShutdownChunk': # pylint: disable=unused-argument + """Read SCTP SHUTDOWN chunk. + + Structure of SCTP SHUTDOWN chunk [:rfc:`9260#section-3.3.8`]: + + .. 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 = 7 | Chunk Flags | Length = 8 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Cumulative TSN Ack | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks + + Returns: + Parsed chunk data. + + Raises: + ProtocolError: If ``length`` is **NOT** ``8``. + + """ + if schema.length != 8: + raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') + + return Data_ShutdownChunk( + type=schema.type, + length=schema.length, + cum_tsn_ack=schema.cum_tsn_ack, + ) + + def _read_chunk_shutdown_ack(self, schema: 'Schema_ShutdownACKChunk', *, chunks: 'Chunks') -> 'Data_ShutdownACKChunk': # pylint: disable=unused-argument + """Read SCTP SHUTDOWN ACK chunk. + + Structure of SCTP SHUTDOWN ACK chunk [:rfc:`9260#section-3.3.9`]: + + .. 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 = 8 | Chunk Flags | Length = 4 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks + + Returns: + Parsed chunk data. + + Raises: + ProtocolError: If ``length`` is **NOT** ``4``. + + """ + if schema.length != 4: + raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') + + return Data_ShutdownACKChunk( + type=schema.type, + length=schema.length, + ) + + def _read_chunk_error(self, schema: 'Schema_ErrorChunk', *, chunks: 'Chunks') -> 'Data_ErrorChunk': # pylint: disable=unused-argument + """Read SCTP ERROR chunk. + + Structure of SCTP ERROR chunk [:rfc:`9260#section-3.3.10`]: + + .. 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 = 9 | Chunk Flags | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + \\ \\ + / one or more Error Causes / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks + + Returns: + Parsed chunk data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``4``. + + """ + if schema.length < 4: + raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') + + return Data_ErrorChunk( + type=schema.type, + length=schema.length, + error=self._read_sctp_causes(schema.error), + ) + + def _read_chunk_cookie_echo(self, schema: 'Schema_CookieEchoChunk', *, chunks: 'Chunks') -> 'Data_CookieEchoChunk': # pylint: disable=unused-argument + """Read SCTP COOKIE ECHO chunk. + + Structure of SCTP COOKIE ECHO chunk [:rfc:`9260#section-3.3.11`]: + + .. 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 = 10 | Chunk Flags | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / Cookie / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks + + Returns: + Parsed chunk data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``4``. + + Note: + A COOKIE ECHO chunk carries the *contents* of the state cookie + parameter rather than the parameter itself, so the cookie is a plain + :obj:`bytes` here rather than a + :class:`~pcapkit.protocols.data.transport.sctp.StateCookieParameter`. + + """ + if schema.length < 4: + raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') + + return Data_CookieEchoChunk( + type=schema.type, + length=schema.length, + cookie=schema.cookie, + ) + + def _read_chunk_cookie_ack(self, schema: 'Schema_CookieACKChunk', *, chunks: 'Chunks') -> 'Data_CookieACKChunk': # pylint: disable=unused-argument + """Read SCTP COOKIE ACK chunk. + + Structure of SCTP COOKIE ACK chunk [:rfc:`9260#section-3.3.12`]: + + .. 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 = 11 | Chunk Flags | Length = 4 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks + + Returns: + Parsed chunk data. + + Raises: + ProtocolError: If ``length`` is **NOT** ``4``. + + """ + if schema.length != 4: + raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') + + return Data_CookieACKChunk( + type=schema.type, + length=schema.length, + ) + + def _read_chunk_shutdown_complete(self, schema: 'Schema_ShutdownCompleteChunk', *, chunks: 'Chunks') -> 'Data_ShutdownCompleteChunk': # pylint: disable=unused-argument + """Read SCTP SHUTDOWN COMPLETE chunk. + + Structure of SCTP SHUTDOWN COMPLETE chunk [:rfc:`9260#section-3.3.13`]: + + .. 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 = 14 | Reserved |T| Length = 4 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks + + Returns: + Parsed chunk data. + + Raises: + ProtocolError: If ``length`` is **NOT** ``4``. + + """ + if schema.length != 4: + raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') + + return Data_ShutdownCompleteChunk( + type=schema.type, + length=schema.length, + flags=Data_TBitFlags( + T=bool(schema.flags['T']), + ), + ) + + def _make_chunk_donone(self, code: 'Enum_Chunk', chunk: 'Optional[Data_UnknownChunk]' = None, *, + flags: 'bytes' = b'\x00', + value: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_UnknownChunk': + """Make SCTP chunk of an unsupported type. + + Args: + code: chunk type + chunk: chunk data + flags: raw chunk flags, as a single byte + value: chunk value in :obj:`bytes` + **kwargs: arbitrary keyword arguments + + Returns: + Constructed chunk schema. + + Raises: + ProtocolError: If ``flags`` is **NOT** exactly one byte. + + """ + if chunk is not None: + flags = chunk.flags + value = chunk.value + + if len(flags) != 1: + raise ProtocolError(f'{self.alias}: [Chunk {code}] invalid format') + + return Schema_UnknownChunk( + type=code, + flags=flags, + length=len(value) + 4, + value=value, + ) + + def _make_chunk_data(self, code: 'Enum_Chunk', chunk: 'Optional[Data_DATAChunk]' = None, *, + I: 'bool' = False, # noqa: E741 + U: 'bool' = False, + B: 'bool' = True, + E: 'bool' = True, + tsn: 'int' = 0, + stream_id: 'int' = 0, + stream_seq: 'int' = 0, + ppid: 'Enum_PayloadProtocolIdentifier | int' = 0, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_DATAChunk': + """Make SCTP DATA chunk. + + Args: + code: chunk type + chunk: chunk data + I: immediate bit + U: unordered bit + B: beginning fragment bit + E: ending fragment bit + tsn: transmission sequence number + stream_id: stream identifier + stream_seq: stream sequence number + ppid: payload protocol identifier + data: user data + **kwargs: arbitrary keyword arguments + + Returns: + Constructed chunk schema. + + Raises: + ProtocolError: If ``data`` is empty, since :rfc:`9260#section-3.3.1` + requires at least one byte of user data. + + """ + if chunk is not None: + I = chunk.flags.I # noqa: E741 + U = chunk.flags.U + B = chunk.flags.B + E = chunk.flags.E + tsn = chunk.tsn + stream_id = chunk.stream_id + stream_seq = chunk.stream_seq + ppid = chunk.ppid + data = chunk.data + + if not data: + raise ProtocolError(f'{self.alias}: [Chunk {code}] invalid format') + + return Schema_DATAChunk( + type=code, + flags={ + 'I': int(I), + 'U': int(U), + 'B': int(B), + 'E': int(E), + }, + length=len(data) + 16, + tsn=tsn, + stream_id=stream_id, + stream_seq=stream_seq, + ppid=ppid, + data=data, + ) + + def _make_chunk_init(self, code: 'Enum_Chunk', chunk: 'Optional[Data_INITChunk]' = None, *, + init_tag: 'int' = 0, + a_rwnd: 'int' = 1500, # minimum permitted by [RFC 9260] + outbound_streams: 'int' = 1, + inbound_streams: 'int' = 1, + init_tsn: 'int' = 0, + parameters: 'Optional[list[Schema_Parameter | tuple[Enum_Parameter, dict[str, Any]] | bytes] | Parameters]' = None, # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_INITChunk': + """Make SCTP INIT chunk. + + Args: + code: chunk type + chunk: chunk data + init_tag: initiate tag + a_rwnd: advertised receiver window credit + outbound_streams: number of outbound streams + inbound_streams: number of inbound streams + init_tsn: initial transmission sequence number + parameters: optional and variable-length parameters + **kwargs: arbitrary keyword arguments + + Returns: + Constructed chunk schema. + + Note: + The chunk flags are reserved by :rfc:`9260#section-3.3.2` and are + always emitted as zero. + + """ + if chunk is not None: + init_tag = chunk.init_tag + a_rwnd = chunk.a_rwnd + outbound_streams = chunk.outbound_streams + inbound_streams = chunk.inbound_streams + init_tsn = chunk.init_tsn + parameters = chunk.parameters + + if parameters is not None: + parameters_value = self._make_sctp_parameters(parameters) + else: + parameters_value = [] + length = 20 + sum(len(param) if isinstance(param, bytes) else len(param.pack()) + for param in parameters_value) + + return Schema_INITChunk( + type=code, + flags=b'\x00', + length=length, + init_tag=init_tag, + a_rwnd=a_rwnd, + outbound_streams=outbound_streams, + inbound_streams=inbound_streams, + init_tsn=init_tsn, + parameters=parameters_value, + ) + + def _make_chunk_init_ack(self, code: 'Enum_Chunk', chunk: 'Optional[Data_INITACKChunk]' = None, *, + init_tag: 'int' = 0, + a_rwnd: 'int' = 1500, # minimum permitted by [RFC 9260] + outbound_streams: 'int' = 1, + inbound_streams: 'int' = 1, + init_tsn: 'int' = 0, + parameters: 'Optional[list[Schema_Parameter | tuple[Enum_Parameter, dict[str, Any]] | bytes] | Parameters]' = None, # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_INITACKChunk': + """Make SCTP INIT ACK chunk. + + Args: + code: chunk type + chunk: chunk data + init_tag: initiate tag + a_rwnd: advertised receiver window credit + outbound_streams: number of outbound streams + inbound_streams: number of inbound streams + init_tsn: initial transmission sequence number + parameters: optional and variable-length parameters + **kwargs: arbitrary keyword arguments + + Returns: + Constructed chunk schema. + + """ + if chunk is not None: + init_tag = chunk.init_tag + a_rwnd = chunk.a_rwnd + outbound_streams = chunk.outbound_streams + inbound_streams = chunk.inbound_streams + init_tsn = chunk.init_tsn + parameters = chunk.parameters + + if parameters is not None: + parameters_value = self._make_sctp_parameters(parameters) + else: + parameters_value = [] + length = 20 + sum(len(param) if isinstance(param, bytes) else len(param.pack()) + for param in parameters_value) + + return Schema_INITACKChunk( + type=code, + flags=b'\x00', + length=length, + init_tag=init_tag, + a_rwnd=a_rwnd, + outbound_streams=outbound_streams, + inbound_streams=inbound_streams, + init_tsn=init_tsn, + parameters=parameters_value, + ) + + def _make_chunk_sack(self, code: 'Enum_Chunk', chunk: 'Optional[Data_SACKChunk]' = None, *, + cum_tsn_ack: 'int' = 0, + a_rwnd: 'int' = 1500, # minimum permitted by [RFC 9260] + gap_blocks: 'Optional[list[Schema_GapAckBlock | Data_GapAckBlock | tuple[int, int]]]' = None, # pylint: disable=line-too-long + dup_tsn: 'Optional[list[int]]' = None, + **kwargs: 'Any') -> 'Schema_SACKChunk': + """Make SCTP SACK chunk. + + Args: + code: chunk type + chunk: chunk data + cum_tsn_ack: cumulative TSN ack + a_rwnd: advertised receiver window credit + gap_blocks: gap ack blocks, each as a schema, a data model or a + ``(start, end)`` pair + dup_tsn: duplicate TSNs + **kwargs: arbitrary keyword arguments + + Returns: + Constructed chunk schema. + + Note: + The counts of gap ack blocks and duplicate TSNs are derived from the + supplied lists rather than taken as arguments, so that they cannot + disagree with the lists they count. + + """ + if chunk is not None: + cum_tsn_ack = chunk.cum_tsn_ack + a_rwnd = chunk.a_rwnd + gap_blocks = list(chunk.gap_blocks) + dup_tsn = list(chunk.dup_tsn) + + blocks = [] # type: list[Schema_GapAckBlock] + for block in gap_blocks or []: + if isinstance(block, Schema_GapAckBlock): + blocks.append(block) + elif isinstance(block, Data_GapAckBlock): + blocks.append(Schema_GapAckBlock(start=block.start, end=block.end)) + else: + start, end = cast('tuple[int, int]', block) + blocks.append(Schema_GapAckBlock(start=start, end=end)) + tsn_list = list(dup_tsn or []) + + return Schema_SACKChunk( + type=code, + flags=b'\x00', + length=16 + len(blocks) * 4 + len(tsn_list) * 4, + cum_tsn_ack=cum_tsn_ack, + a_rwnd=a_rwnd, + num_gap_blocks=len(blocks), + num_dup_tsn=len(tsn_list), + gap_blocks=blocks, + dup_tsn=tsn_list, + ) + + def _make_chunk_heartbeat(self, code: 'Enum_Chunk', chunk: 'Optional[Data_HeartbeatChunk]' = None, *, + parameters: 'Optional[list[Schema_Parameter | tuple[Enum_Parameter, dict[str, Any]] | bytes] | Parameters]' = None, # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_HeartbeatChunk': + """Make SCTP HEARTBEAT chunk. + + Args: + code: chunk type + chunk: chunk data + parameters: heartbeat information parameters + **kwargs: arbitrary keyword arguments + + Returns: + Constructed chunk schema. + + """ + if chunk is not None: + parameters = chunk.parameters + + if parameters is not None: + parameters_value = self._make_sctp_parameters(parameters) + else: + parameters_value = [] + length = 4 + sum(len(param) if isinstance(param, bytes) else len(param.pack()) + for param in parameters_value) + + return Schema_HeartbeatChunk( + type=code, + flags=b'\x00', + length=length, + parameters=parameters_value, + ) + + def _make_chunk_heartbeat_ack(self, code: 'Enum_Chunk', chunk: 'Optional[Data_HeartbeatACKChunk]' = None, *, + parameters: 'Optional[list[Schema_Parameter | tuple[Enum_Parameter, dict[str, Any]] | bytes] | Parameters]' = None, # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_HeartbeatACKChunk': + """Make SCTP HEARTBEAT ACK chunk. + + Args: + code: chunk type + chunk: chunk data + parameters: heartbeat information parameters + **kwargs: arbitrary keyword arguments + + Returns: + Constructed chunk schema. + + """ + if chunk is not None: + parameters = chunk.parameters + + if parameters is not None: + parameters_value = self._make_sctp_parameters(parameters) + else: + parameters_value = [] + length = 4 + sum(len(param) if isinstance(param, bytes) else len(param.pack()) + for param in parameters_value) + + return Schema_HeartbeatACKChunk( + type=code, + flags=b'\x00', + length=length, + parameters=parameters_value, + ) + + def _make_chunk_abort(self, code: 'Enum_Chunk', chunk: 'Optional[Data_AbortChunk]' = None, *, + T: 'bool' = False, + error: 'Optional[list[Schema_ErrorCause | tuple[Enum_CauseCode, dict[str, Any]] | bytes] | Causes]' = None, # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_AbortChunk': + """Make SCTP ABORT chunk. + + Args: + code: chunk type + chunk: chunk data + T: whether the verification tag has been reflected + error: zero or more error causes + **kwargs: arbitrary keyword arguments + + Returns: + Constructed chunk schema. + + """ + if chunk is not None: + T = chunk.flags.T + error = chunk.error + + if error is not None: + error_value = self._make_sctp_causes(error) + else: + error_value = [] + length = 4 + sum(len(cause) if isinstance(cause, bytes) else len(cause.pack()) + for cause in error_value) + + return Schema_AbortChunk( + type=code, + flags={ + 'T': int(T), + }, + length=length, + error=error_value, + ) + + def _make_chunk_shutdown(self, code: 'Enum_Chunk', chunk: 'Optional[Data_ShutdownChunk]' = None, *, + cum_tsn_ack: 'int' = 0, + **kwargs: 'Any') -> 'Schema_ShutdownChunk': + """Make SCTP SHUTDOWN chunk. + + Args: + code: chunk type + chunk: chunk data + cum_tsn_ack: cumulative TSN ack + **kwargs: arbitrary keyword arguments + + Returns: + Constructed chunk schema. + + """ + if chunk is not None: + cum_tsn_ack = chunk.cum_tsn_ack + + return Schema_ShutdownChunk( + type=code, + flags=b'\x00', + length=8, + cum_tsn_ack=cum_tsn_ack, + ) + + def _make_chunk_shutdown_ack(self, code: 'Enum_Chunk', chunk: 'Optional[Data_ShutdownACKChunk]' = None, + **kwargs: 'Any') -> 'Schema_ShutdownACKChunk': + """Make SCTP SHUTDOWN ACK chunk. + + Args: + code: chunk type + chunk: chunk data + **kwargs: arbitrary keyword arguments + + Returns: + Constructed chunk schema. + + """ + return Schema_ShutdownACKChunk( + type=code, + flags=b'\x00', + length=4, + ) + + def _make_chunk_error(self, code: 'Enum_Chunk', chunk: 'Optional[Data_ErrorChunk]' = None, *, + error: 'Optional[list[Schema_ErrorCause | tuple[Enum_CauseCode, dict[str, Any]] | bytes] | Causes]' = None, # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_ErrorChunk': + """Make SCTP ERROR chunk. + + Args: + code: chunk type + chunk: chunk data + error: one or more error causes + **kwargs: arbitrary keyword arguments + + Returns: + Constructed chunk schema. + + """ + if chunk is not None: + error = chunk.error + + if error is not None: + error_value = self._make_sctp_causes(error) + else: + error_value = [] + length = 4 + sum(len(cause) if isinstance(cause, bytes) else len(cause.pack()) + for cause in error_value) + + return Schema_ErrorChunk( + type=code, + flags=b'\x00', + length=length, + error=error_value, + ) + + def _make_chunk_cookie_echo(self, code: 'Enum_Chunk', chunk: 'Optional[Data_CookieEchoChunk]' = None, *, + cookie: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_CookieEchoChunk': + """Make SCTP COOKIE ECHO chunk. + + Args: + code: chunk type + chunk: chunk data + cookie: state cookie, as received in the INIT ACK chunk's state + cookie parameter + **kwargs: arbitrary keyword arguments + + Returns: + Constructed chunk schema. + + """ + if chunk is not None: + cookie = chunk.cookie + + return Schema_CookieEchoChunk( + type=code, + flags=b'\x00', + length=len(cookie) + 4, + cookie=cookie, + ) + + def _make_chunk_cookie_ack(self, code: 'Enum_Chunk', chunk: 'Optional[Data_CookieACKChunk]' = None, + **kwargs: 'Any') -> 'Schema_CookieACKChunk': + """Make SCTP COOKIE ACK chunk. + + Args: + code: chunk type + chunk: chunk data + **kwargs: arbitrary keyword arguments + + Returns: + Constructed chunk schema. + + """ + return Schema_CookieACKChunk( + type=code, + flags=b'\x00', + length=4, + ) + + def _make_chunk_shutdown_complete(self, code: 'Enum_Chunk', chunk: 'Optional[Data_ShutdownCompleteChunk]' = None, *, + T: 'bool' = False, + **kwargs: 'Any') -> 'Schema_ShutdownCompleteChunk': + """Make SCTP SHUTDOWN COMPLETE chunk. + + Args: + code: chunk type + chunk: chunk data + T: whether the verification tag has been reflected + **kwargs: arbitrary keyword arguments + + Returns: + Constructed chunk schema. + + """ + if chunk is not None: + T = chunk.flags.T + + return Schema_ShutdownCompleteChunk( + type=code, + flags={ + 'T': int(T), + }, + length=4, + ) + + def _read_param_donone(self, schema: 'Schema_UnknownParameter', *, parameters: 'Parameters') -> 'Data_UnknownParameter': # pylint: disable=unused-argument + """Read SCTP chunk parameter of an unsupported type. + + This is the fall-through for every chunk parameter type :mod:`pcapkit` + does not implement. The parameter's value is recorded verbatim rather + than raising, so a chunk carrying an unknown parameter still parses. + + Arguments: + schema: parsed parameter schema + parameters: extracted SCTP chunk parameters + + Returns: + Parsed parameter data. + + """ + return Data_UnknownParameter( + type=schema.type, + length=schema.length, + value=schema.value, + ) + + def _read_param_hbinfo(self, schema: 'Schema_HeartbeatInfoParameter', *, parameters: 'Parameters') -> 'Data_HeartbeatInfoParameter': # pylint: disable=unused-argument + """Read SCTP heartbeat info parameter. + + Structure of SCTP heartbeat info parameter [:rfc:`9260#section-3.3.5`]: + + .. 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Heartbeat Info Type = 1 | HB Info Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / Sender-Specific Heartbeat Info / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed parameter schema + parameters: extracted SCTP chunk parameters + + Returns: + Parsed parameter data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``4``. + + """ + if schema.length < 4: + raise ProtocolError(f'{self.alias}: [Param {schema.type}] invalid format') + + return Data_HeartbeatInfoParameter( + type=schema.type, + length=schema.length, + info=schema.info, + ) + + def _read_param_ipv4(self, schema: 'Schema_IPv4AddressParameter', *, parameters: 'Parameters') -> 'Data_IPv4AddressParameter': # pylint: disable=unused-argument + """Read SCTP IPv4 address parameter. + + Structure of SCTP IPv4 address parameter [:rfc:`9260#section-3.3.2.1.1`]: + + .. 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 = 5 | Length = 8 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | IPv4 Address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed parameter schema + parameters: extracted SCTP chunk parameters + + Returns: + Parsed parameter data. + + Raises: + ProtocolError: If ``length`` is **NOT** ``8``. + + """ + if schema.length != 8: + raise ProtocolError(f'{self.alias}: [Param {schema.type}] invalid format') + + return Data_IPv4AddressParameter( + type=schema.type, + length=schema.length, + address=schema.address, + ) + + def _read_param_ipv6(self, schema: 'Schema_IPv6AddressParameter', *, parameters: 'Parameters') -> 'Data_IPv6AddressParameter': # pylint: disable=unused-argument + """Read SCTP IPv6 address parameter. + + Structure of SCTP IPv6 address parameter [:rfc:`9260#section-3.3.2.1.2`]: + + .. 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 = 6 | Length = 20 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | IPv6 Address | + | | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed parameter schema + parameters: extracted SCTP chunk parameters + + Returns: + Parsed parameter data. + + Raises: + ProtocolError: If ``length`` is **NOT** ``20``. + + """ + if schema.length != 20: + raise ProtocolError(f'{self.alias}: [Param {schema.type}] invalid format') + + return Data_IPv6AddressParameter( + type=schema.type, + length=schema.length, + address=schema.address, + ) + + def _read_param_cookie(self, schema: 'Schema_StateCookieParameter', *, parameters: 'Parameters') -> 'Data_StateCookieParameter': # pylint: disable=unused-argument + """Read SCTP state cookie parameter. + + Structure of SCTP state cookie parameter [:rfc:`9260#section-3.3.3.1.1`]: + + .. 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 = 7 | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / Cookie / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed parameter schema + parameters: extracted SCTP chunk parameters + + Returns: + Parsed parameter data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``4``. + + """ + if schema.length < 4: + raise ProtocolError(f'{self.alias}: [Param {schema.type}] invalid format') + + return Data_StateCookieParameter( + type=schema.type, + length=schema.length, + cookie=schema.cookie, + ) + + def _read_param_unrecognized(self, schema: 'Schema_UnrecognizedParameter', *, parameters: 'Parameters') -> 'Data_UnrecognizedParameter': # pylint: disable=unused-argument + """Read SCTP unrecognized parameter parameter. + + Structure of SCTP unrecognized parameter [:rfc:`9260#section-3.3.3.1.2`]: + + .. 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 = 8 | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / Unrecognized Parameter / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed parameter schema + parameters: extracted SCTP chunk parameters + + Returns: + Parsed parameter data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``4``. + + Note: + The offending parameter is recorded as raw :obj:`bytes`, complete + with its own type and length, rather than being parsed recursively: + by definition the sender did not recognise it, so neither + interpretation nor validation of its contents would be meaningful. + + """ + if schema.length < 4: + raise ProtocolError(f'{self.alias}: [Param {schema.type}] invalid format') + + return Data_UnrecognizedParameter( + type=schema.type, + length=schema.length, + value=schema.value, + ) + + def _read_param_preservative(self, schema: 'Schema_CookiePreservativeParameter', *, parameters: 'Parameters') -> 'Data_CookiePreservativeParameter': # pylint: disable=unused-argument + """Read SCTP cookie preservative parameter. + + Structure of SCTP cookie preservative parameter [:rfc:`9260#section-3.3.2.1.3`]: + + .. 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 = 9 | Length = 8 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Suggested Cookie Life-Span Increment (msec.) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed parameter schema + parameters: extracted SCTP chunk parameters + + Returns: + Parsed parameter data. + + Raises: + ProtocolError: If ``length`` is **NOT** ``8``. + + """ + if schema.length != 8: + raise ProtocolError(f'{self.alias}: [Param {schema.type}] invalid format') + + return Data_CookiePreservativeParameter( + type=schema.type, + length=schema.length, + increment=schema.increment, + ) + + def _read_param_hostname(self, schema: 'Schema_HostNameAddressParameter', *, parameters: 'Parameters') -> 'Data_HostNameAddressParameter': # pylint: disable=unused-argument + """Read SCTP host name address parameter. + + Structure of SCTP host name address parameter [:rfc:`9260#section-3.3.2.1.4`]: + + .. 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 = 11 | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / Host Name / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed parameter schema + parameters: extracted SCTP chunk parameters + + Returns: + Parsed parameter data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``4``. + + Note: + The usage of this parameter is deprecated by + :rfc:`9260#section-3.3.2.1.4`; it is parsed so that a packet + carrying one can still be inspected. The host name is kept as raw + :obj:`bytes`, including its null terminator, since the encoding is + not specified on the wire. + + """ + if schema.length < 4: + raise ProtocolError(f'{self.alias}: [Param {schema.type}] invalid format') + + return Data_HostNameAddressParameter( + type=schema.type, + length=schema.length, + name=schema.name, + ) + + def _read_param_addrtypes(self, schema: 'Schema_SupportedAddressTypesParameter', *, parameters: 'Parameters') -> 'Data_SupportedAddressTypesParameter': # pylint: disable=unused-argument + """Read SCTP supported address types parameter. + + Structure of SCTP supported address types parameter [:rfc:`9260#section-3.3.2.1.5`]: + + .. 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 = 12 | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Address Type #1 | Address Type #2 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | ...... | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed parameter schema + parameters: extracted SCTP chunk parameters + + Returns: + Parsed parameter data. + + Raises: + ProtocolError: If ``length`` is **NOT** ``4`` plus a multiple of + ``2``. + + """ + if schema.length < 4 or (schema.length - 4) % 2: + raise ProtocolError(f'{self.alias}: [Param {schema.type}] invalid format') + + return Data_SupportedAddressTypesParameter( + type=schema.type, + length=schema.length, + types=tuple(schema.types), + ) + + def _make_param_donone(self, code: 'Enum_Parameter', param: 'Optional[Data_UnknownParameter]' = None, *, + value: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_UnknownParameter': + """Make SCTP chunk parameter of an unsupported type. + + Args: + code: parameter type + param: parameter data + value: parameter value in :obj:`bytes` + **kwargs: arbitrary keyword arguments + + Returns: + Constructed parameter schema. + + """ + if param is not None: + value = param.value + + return Schema_UnknownParameter( + type=code, + length=len(value) + 4, + value=value, + ) + + def _make_param_hbinfo(self, code: 'Enum_Parameter', param: 'Optional[Data_HeartbeatInfoParameter]' = None, *, + info: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_HeartbeatInfoParameter': + """Make SCTP heartbeat info parameter. + + Args: + code: parameter type + param: parameter data + info: sender-specific heartbeat info + **kwargs: arbitrary keyword arguments + + Returns: + Constructed parameter schema. + + """ + if param is not None: + info = param.info + + return Schema_HeartbeatInfoParameter( + type=code, + length=len(info) + 4, + info=info, + ) + + def _make_param_ipv4(self, code: 'Enum_Parameter', param: 'Optional[Data_IPv4AddressParameter]' = None, *, + address: 'IPv4Address | int | str | bytes' = '0.0.0.0', # nosec: B104 + **kwargs: 'Any') -> 'Schema_IPv4AddressParameter': + """Make SCTP IPv4 address parameter. + + Args: + code: parameter type + param: parameter data + address: IPv4 address of the sending endpoint + **kwargs: arbitrary keyword arguments + + Returns: + Constructed parameter schema. + + """ + if param is not None: + address = param.address + + return Schema_IPv4AddressParameter( + type=code, + length=8, + address=address, + ) + + def _make_param_ipv6(self, code: 'Enum_Parameter', param: 'Optional[Data_IPv6AddressParameter]' = None, *, + address: 'IPv6Address | int | str | bytes' = '::', + **kwargs: 'Any') -> 'Schema_IPv6AddressParameter': + """Make SCTP IPv6 address parameter. + + Args: + code: parameter type + param: parameter data + address: IPv6 address of the sending endpoint + **kwargs: arbitrary keyword arguments + + Returns: + Constructed parameter schema. + + """ + if param is not None: + address = param.address + + return Schema_IPv6AddressParameter( + type=code, + length=20, + address=address, + ) + + def _make_param_cookie(self, code: 'Enum_Parameter', param: 'Optional[Data_StateCookieParameter]' = None, *, + cookie: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_StateCookieParameter': + """Make SCTP state cookie parameter. + + Args: + code: parameter type + param: parameter data + cookie: state cookie + **kwargs: arbitrary keyword arguments + + Returns: + Constructed parameter schema. + + """ + if param is not None: + cookie = param.cookie + + return Schema_StateCookieParameter( + type=code, + length=len(cookie) + 4, + cookie=cookie, + ) + + def _make_param_unrecognized(self, code: 'Enum_Parameter', param: 'Optional[Data_UnrecognizedParameter]' = None, *, + value: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_UnrecognizedParameter': + """Make SCTP unrecognized parameter parameter. + + Args: + code: parameter type + param: parameter data + value: the unrecognized parameter, complete with its type and length + **kwargs: arbitrary keyword arguments + + Returns: + Constructed parameter schema. + + """ + if param is not None: + value = param.value + + return Schema_UnrecognizedParameter( + type=code, + length=len(value) + 4, + value=value, + ) + + def _make_param_preservative(self, code: 'Enum_Parameter', param: 'Optional[Data_CookiePreservativeParameter]' = None, *, + increment: 'int' = 0, + **kwargs: 'Any') -> 'Schema_CookiePreservativeParameter': + """Make SCTP cookie preservative parameter. + + Args: + code: parameter type + param: parameter data + increment: suggested cookie life-span increment, in milliseconds + **kwargs: arbitrary keyword arguments + + Returns: + Constructed parameter schema. + + """ + if param is not None: + increment = param.increment + + return Schema_CookiePreservativeParameter( + type=code, + length=8, + increment=increment, + ) + + def _make_param_hostname(self, code: 'Enum_Parameter', param: 'Optional[Data_HostNameAddressParameter]' = None, *, + name: 'bytes' = b'\x00', + **kwargs: 'Any') -> 'Schema_HostNameAddressParameter': + """Make SCTP host name address parameter. + + Args: + code: parameter type + param: parameter data + name: host name, including at least one null terminator + **kwargs: arbitrary keyword arguments + + Returns: + Constructed parameter schema. + + Raises: + ProtocolError: If ``name`` is not null-terminated, as required by + :rfc:`9260#section-3.3.2.1.4`. + + """ + if param is not None: + name = param.name + + if not name.endswith(b'\x00'): + raise ProtocolError(f'{self.alias}: [Param {code}] invalid format') + + return Schema_HostNameAddressParameter( + type=code, + length=len(name) + 4, + name=name, + ) + + def _make_param_addrtypes(self, code: 'Enum_Parameter', param: 'Optional[Data_SupportedAddressTypesParameter]' = None, *, + types: 'Optional[list[Enum_Parameter | int]]' = None, + **kwargs: 'Any') -> 'Schema_SupportedAddressTypesParameter': + """Make SCTP supported address types parameter. + + Args: + code: parameter type + param: parameter data + types: supported address types, given as address parameter types + **kwargs: arbitrary keyword arguments + + Returns: + Constructed parameter schema. + + """ + if param is not None: + types = list(param.types) + + types_value = [Enum_Parameter.get(item) if isinstance(item, int) else item + for item in types or []] + + return Schema_SupportedAddressTypesParameter( + type=code, + length=len(types_value) * 2 + 4, + types=types_value, + ) + + def _read_cause_donone(self, schema: 'Schema_UnknownCause', *, causes: 'Causes') -> 'Data_UnknownCause': # pylint: disable=unused-argument + """Read SCTP error cause of an unsupported cause code. + + This is the fall-through for every error cause code :mod:`pcapkit` does + not implement, e.g. those registered by SCTP extensions. The + cause-specific information is recorded verbatim rather than raising. + + Arguments: + schema: parsed error cause schema + causes: extracted SCTP error causes + + Returns: + Parsed error cause data. + + """ + return Data_UnknownCause( + code=schema.code, + length=schema.length, + value=schema.value, + ) + + def _read_cause_invalid_stream(self, schema: 'Schema_InvalidStreamIdentifierCause', *, causes: 'Causes') -> 'Data_InvalidStreamIdentifierCause': # pylint: disable=unused-argument + """Read SCTP invalid stream identifier error cause. + + Structure of the cause [:rfc:`9260#section-3.3.10.1`]: + + .. 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Cause Code = 1 | Cause Length = 8 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Stream Identifier | (Reserved) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed error cause schema + causes: extracted SCTP error causes + + Returns: + Parsed error cause data. + + Raises: + ProtocolError: If ``length`` is **NOT** ``8``. + + """ + if schema.length != 8: + raise ProtocolError(f'{self.alias}: [Cause {schema.code}] invalid format') + + return Data_InvalidStreamIdentifierCause( + code=schema.code, + length=schema.length, + stream_id=schema.stream_id, + ) + + def _read_cause_missing_param(self, schema: 'Schema_MissingMandatoryParameterCause', *, causes: 'Causes') -> 'Data_MissingMandatoryParameterCause': # pylint: disable=unused-argument + """Read SCTP missing mandatory parameter error cause. + + Structure of the cause [:rfc:`9260#section-3.3.10.2`]: + + .. 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Cause Code = 2 | Cause Length = 8 + N * 2 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Number of missing params = N | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Missing Param Type #1 | Missing Param Type #2 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed error cause schema + causes: extracted SCTP error causes + + Returns: + Parsed error cause data. + + Raises: + ProtocolError: If ``length`` does **NOT** match the declared number + of missing parameters. + + """ + if schema.length != 8 + schema.num * 2: + raise ProtocolError(f'{self.alias}: [Cause {schema.code}] invalid format') + + return Data_MissingMandatoryParameterCause( + code=schema.code, + length=schema.length, + num=schema.num, + types=tuple(schema.types), + ) + + def _read_cause_stale_cookie(self, schema: 'Schema_StaleCookieCause', *, causes: 'Causes') -> 'Data_StaleCookieCause': # pylint: disable=unused-argument + """Read SCTP stale cookie error cause. + + Structure of the cause [:rfc:`9260#section-3.3.10.3`]: + + .. 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Cause Code = 3 | Cause Length = 8 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Measure of Staleness (usec.) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed error cause schema + causes: extracted SCTP error causes + + Returns: + Parsed error cause data. + + Raises: + ProtocolError: If ``length`` is **NOT** ``8``. + + """ + if schema.length != 8: + raise ProtocolError(f'{self.alias}: [Cause {schema.code}] invalid format') + + return Data_StaleCookieCause( + code=schema.code, + length=schema.length, + staleness=schema.staleness, + ) + + def _read_cause_out_of_resource(self, schema: 'Schema_OutOfResourceCause', *, causes: 'Causes') -> 'Data_OutOfResourceCause': # pylint: disable=unused-argument + """Read SCTP out of resource error cause. + + Structure of the cause [:rfc:`9260#section-3.3.10.4`]: + + .. 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Cause Code = 4 | Cause Length = 4 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed error cause schema + causes: extracted SCTP error causes + + Returns: + Parsed error cause data. + + Raises: + ProtocolError: If ``length`` is **NOT** ``4``. + + """ + if schema.length != 4: + raise ProtocolError(f'{self.alias}: [Cause {schema.code}] invalid format') + + return Data_OutOfResourceCause( + code=schema.code, + length=schema.length, + ) + + def _read_cause_unresolvable_addr(self, schema: 'Schema_UnresolvableAddressCause', *, causes: 'Causes') -> 'Data_UnresolvableAddressCause': # pylint: disable=unused-argument + """Read SCTP unresolvable address error cause. + + Structure of the cause [:rfc:`9260#section-3.3.10.5`]: + + .. 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Cause Code = 5 | Cause Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / Unresolvable Address / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed error cause schema + causes: extracted SCTP error causes + + Returns: + Parsed error cause data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``4``. + + """ + if schema.length < 4: + raise ProtocolError(f'{self.alias}: [Cause {schema.code}] invalid format') + + return Data_UnresolvableAddressCause( + code=schema.code, + length=schema.length, + value=schema.value, + ) + + def _read_cause_unrecognized_chunk(self, schema: 'Schema_UnrecognizedChunkTypeCause', *, causes: 'Causes') -> 'Data_UnrecognizedChunkTypeCause': # pylint: disable=unused-argument + """Read SCTP unrecognized chunk type error cause. + + Structure of the cause [:rfc:`9260#section-3.3.10.6`]: + + .. 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Cause Code = 6 | Cause Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / Unrecognized Chunk / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed error cause schema + causes: extracted SCTP error causes + + Returns: + Parsed error cause data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``4``. + + """ + if schema.length < 4: + raise ProtocolError(f'{self.alias}: [Cause {schema.code}] invalid format') + + return Data_UnrecognizedChunkTypeCause( + code=schema.code, + length=schema.length, + value=schema.value, + ) + + def _read_cause_invalid_param(self, schema: 'Schema_InvalidMandatoryParameterCause', *, causes: 'Causes') -> 'Data_InvalidMandatoryParameterCause': # pylint: disable=unused-argument + """Read SCTP invalid mandatory parameter error cause. + + Structure of the cause [:rfc:`9260#section-3.3.10.7`]: + + .. 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Cause Code = 7 | Cause Length = 4 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed error cause schema + causes: extracted SCTP error causes + + Returns: + Parsed error cause data. + + Raises: + ProtocolError: If ``length`` is **NOT** ``4``. + + """ + if schema.length != 4: + raise ProtocolError(f'{self.alias}: [Cause {schema.code}] invalid format') + + return Data_InvalidMandatoryParameterCause( + code=schema.code, + length=schema.length, + ) + + def _read_cause_unrecognized_params(self, schema: 'Schema_UnrecognizedParametersCause', *, causes: 'Causes') -> 'Data_UnrecognizedParametersCause': # pylint: disable=unused-argument + """Read SCTP unrecognized parameters error cause. + + Structure of the cause [:rfc:`9260#section-3.3.10.8`]: + + .. 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Cause Code = 8 | Cause Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / Unrecognized Parameters / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed error cause schema + causes: extracted SCTP error causes + + Returns: + Parsed error cause data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``4``. + + """ + if schema.length < 4: + raise ProtocolError(f'{self.alias}: [Cause {schema.code}] invalid format') + + return Data_UnrecognizedParametersCause( + code=schema.code, + length=schema.length, + value=schema.value, + ) + + def _read_cause_no_user_data(self, schema: 'Schema_NoUserDataCause', *, causes: 'Causes') -> 'Data_NoUserDataCause': # pylint: disable=unused-argument + """Read SCTP no user data error cause. + + Structure of the cause [:rfc:`9260#section-3.3.10.9`]: + + .. 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Cause Code = 9 | Cause Length = 8 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | TSN | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed error cause schema + causes: extracted SCTP error causes + + Returns: + Parsed error cause data. + + Raises: + ProtocolError: If ``length`` is **NOT** ``8``. + + """ + if schema.length != 8: + raise ProtocolError(f'{self.alias}: [Cause {schema.code}] invalid format') + + return Data_NoUserDataCause( + code=schema.code, + length=schema.length, + tsn=schema.tsn, + ) + + def _read_cause_cookie_shutdown(self, schema: 'Schema_CookieReceivedWhileShuttingDownCause', *, causes: 'Causes') -> 'Data_CookieReceivedWhileShuttingDownCause': # pylint: disable=unused-argument + """Read SCTP cookie received while shutting down error cause. + + Structure of the cause [:rfc:`9260#section-3.3.10.10`]: + + .. 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Cause Code = 10 | Cause Length = 4 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed error cause schema + causes: extracted SCTP error causes + + Returns: + Parsed error cause data. + + Raises: + ProtocolError: If ``length`` is **NOT** ``4``. + + """ + if schema.length != 4: + raise ProtocolError(f'{self.alias}: [Cause {schema.code}] invalid format') + + return Data_CookieReceivedWhileShuttingDownCause( + code=schema.code, + length=schema.length, + ) + + def _read_cause_restart_addr(self, schema: 'Schema_RestartOfAnAssociationWithNewAddressesCause', *, causes: 'Causes') -> 'Data_RestartOfAnAssociationWithNewAddressesCause': # pylint: disable=unused-argument + """Read SCTP restart of an association with new addresses error cause. + + Structure of the cause [:rfc:`9260#section-3.3.10.11`]: + + .. 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Cause Code = 11 | Cause Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / New Address TLVs / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed error cause schema + causes: extracted SCTP error causes + + Returns: + Parsed error cause data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``4``. + + """ + if schema.length < 4: + raise ProtocolError(f'{self.alias}: [Cause {schema.code}] invalid format') + + return Data_RestartOfAnAssociationWithNewAddressesCause( + code=schema.code, + length=schema.length, + value=schema.value, + ) + + def _read_cause_user_abort(self, schema: 'Schema_UserInitiatedAbortCause', *, causes: 'Causes') -> 'Data_UserInitiatedAbortCause': # pylint: disable=unused-argument + """Read SCTP user-initiated abort error cause. + + Structure of the cause [:rfc:`9260#section-3.3.10.12`]: + + .. 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Cause Code = 12 | Cause Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / Upper Layer Abort Reason / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed error cause schema + causes: extracted SCTP error causes + + Returns: + Parsed error cause data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``4``. + + """ + if schema.length < 4: + raise ProtocolError(f'{self.alias}: [Cause {schema.code}] invalid format') + + return Data_UserInitiatedAbortCause( + code=schema.code, + length=schema.length, + info=schema.info, + ) + + def _read_cause_protocol_violation(self, schema: 'Schema_ProtocolViolationCause', *, causes: 'Causes') -> 'Data_ProtocolViolationCause': # pylint: disable=unused-argument + """Read SCTP protocol violation error cause. + + Structure of the cause [:rfc:`9260#section-3.3.10.13`]: + + .. 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Cause Code = 13 | Cause Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / Additional Information / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed error cause schema + causes: extracted SCTP error causes + + Returns: + Parsed error cause data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``4``. + + """ + if schema.length < 4: + raise ProtocolError(f'{self.alias}: [Cause {schema.code}] invalid format') + + return Data_ProtocolViolationCause( + code=schema.code, + length=schema.length, + info=schema.info, + ) + + def _make_cause_donone(self, code: 'Enum_CauseCode', cause: 'Optional[Data_UnknownCause]' = None, *, + value: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_UnknownCause': + """Make SCTP error cause of an unsupported cause code. + + Args: + code: error cause code + cause: error cause data + value: cause-specific information in :obj:`bytes` + **kwargs: arbitrary keyword arguments + + Returns: + Constructed error cause schema. + + """ + if cause is not None: + value = cause.value + + return Schema_UnknownCause( + code=code, + length=len(value) + 4, + value=value, + ) + + def _make_cause_invalid_stream(self, code: 'Enum_CauseCode', cause: 'Optional[Data_InvalidStreamIdentifierCause]' = None, *, + stream_id: 'int' = 0, + **kwargs: 'Any') -> 'Schema_InvalidStreamIdentifierCause': + """Make SCTP invalid stream identifier error cause. + + Args: + code: error cause code + cause: error cause data + stream_id: stream identifier of the offending DATA chunk + **kwargs: arbitrary keyword arguments + + Returns: + Constructed error cause schema. + + """ + if cause is not None: + stream_id = cause.stream_id + + return Schema_InvalidStreamIdentifierCause( + code=code, + length=8, + stream_id=stream_id, + ) + + def _make_cause_missing_param(self, code: 'Enum_CauseCode', cause: 'Optional[Data_MissingMandatoryParameterCause]' = None, *, + types: 'Optional[list[Enum_Parameter | int]]' = None, + **kwargs: 'Any') -> 'Schema_MissingMandatoryParameterCause': + """Make SCTP missing mandatory parameter error cause. + + Args: + code: error cause code + cause: error cause data + types: missing parameter types + **kwargs: arbitrary keyword arguments + + Returns: + Constructed error cause schema. + + Note: + The count of missing parameters is derived from ``types`` rather + than taken as an argument, so that it cannot disagree with the list + it counts. + + """ + if cause is not None: + types = list(cause.types) + + types_value = [Enum_Parameter.get(item) if isinstance(item, int) else item + for item in types or []] + + return Schema_MissingMandatoryParameterCause( + code=code, + length=8 + len(types_value) * 2, + num=len(types_value), + types=types_value, + ) + + def _make_cause_stale_cookie(self, code: 'Enum_CauseCode', cause: 'Optional[Data_StaleCookieCause]' = None, *, + staleness: 'int' = 0, + **kwargs: 'Any') -> 'Schema_StaleCookieCause': + """Make SCTP stale cookie error cause. + + Args: + code: error cause code + cause: error cause data + staleness: measure of staleness, in microseconds + **kwargs: arbitrary keyword arguments + + Returns: + Constructed error cause schema. + + """ + if cause is not None: + staleness = cause.staleness + + return Schema_StaleCookieCause( + code=code, + length=8, + staleness=staleness, + ) + + def _make_cause_out_of_resource(self, code: 'Enum_CauseCode', cause: 'Optional[Data_OutOfResourceCause]' = None, + **kwargs: 'Any') -> 'Schema_OutOfResourceCause': + """Make SCTP out of resource error cause. + + Args: + code: error cause code + cause: error cause data + **kwargs: arbitrary keyword arguments + + Returns: + Constructed error cause schema. + + """ + return Schema_OutOfResourceCause( + code=code, + length=4, + ) + + def _make_cause_unresolvable_addr(self, code: 'Enum_CauseCode', cause: 'Optional[Data_UnresolvableAddressCause]' = None, *, + value: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_UnresolvableAddressCause': + """Make SCTP unresolvable address error cause. + + Args: + code: error cause code + cause: error cause data + value: the offending address parameter, complete with its type and + length + **kwargs: arbitrary keyword arguments + + Returns: + Constructed error cause schema. + + """ + if cause is not None: + value = cause.value + + return Schema_UnresolvableAddressCause( + code=code, + length=len(value) + 4, + value=value, + ) + + def _make_cause_unrecognized_chunk(self, code: 'Enum_CauseCode', cause: 'Optional[Data_UnrecognizedChunkTypeCause]' = None, *, + value: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_UnrecognizedChunkTypeCause': + """Make SCTP unrecognized chunk type error cause. + + Args: + code: error cause code + cause: error cause data + value: the unrecognized chunk, complete with its type, flags and + length + **kwargs: arbitrary keyword arguments + + Returns: + Constructed error cause schema. + + """ + if cause is not None: + value = cause.value + + return Schema_UnrecognizedChunkTypeCause( + code=code, + length=len(value) + 4, + value=value, + ) + + def _make_cause_invalid_param(self, code: 'Enum_CauseCode', cause: 'Optional[Data_InvalidMandatoryParameterCause]' = None, + **kwargs: 'Any') -> 'Schema_InvalidMandatoryParameterCause': + """Make SCTP invalid mandatory parameter error cause. + + Args: + code: error cause code + cause: error cause data + **kwargs: arbitrary keyword arguments + + Returns: + Constructed error cause schema. + + """ + return Schema_InvalidMandatoryParameterCause( + code=code, + length=4, + ) + + def _make_cause_unrecognized_params(self, code: 'Enum_CauseCode', cause: 'Optional[Data_UnrecognizedParametersCause]' = None, *, + value: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_UnrecognizedParametersCause': + """Make SCTP unrecognized parameters error cause. + + Args: + code: error cause code + cause: error cause data + value: the unrecognized parameters, complete with their types and + lengths + **kwargs: arbitrary keyword arguments + + Returns: + Constructed error cause schema. + + """ + if cause is not None: + value = cause.value + + return Schema_UnrecognizedParametersCause( + code=code, + length=len(value) + 4, + value=value, + ) + + def _make_cause_no_user_data(self, code: 'Enum_CauseCode', cause: 'Optional[Data_NoUserDataCause]' = None, *, + tsn: 'int' = 0, + **kwargs: 'Any') -> 'Schema_NoUserDataCause': + """Make SCTP no user data error cause. + + Args: + code: error cause code + cause: error cause data + tsn: TSN of the offending DATA chunk + **kwargs: arbitrary keyword arguments + + Returns: + Constructed error cause schema. + + """ + if cause is not None: + tsn = cause.tsn + + return Schema_NoUserDataCause( + code=code, + length=8, + tsn=tsn, + ) + + def _make_cause_cookie_shutdown(self, code: 'Enum_CauseCode', cause: 'Optional[Data_CookieReceivedWhileShuttingDownCause]' = None, + **kwargs: 'Any') -> 'Schema_CookieReceivedWhileShuttingDownCause': + """Make SCTP cookie received while shutting down error cause. + + Args: + code: error cause code + cause: error cause data + **kwargs: arbitrary keyword arguments + + Returns: + Constructed error cause schema. + + """ + return Schema_CookieReceivedWhileShuttingDownCause( + code=code, + length=4, + ) + + def _make_cause_restart_addr(self, code: 'Enum_CauseCode', cause: 'Optional[Data_RestartOfAnAssociationWithNewAddressesCause]' = None, *, + value: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_RestartOfAnAssociationWithNewAddressesCause': + """Make SCTP restart of an association with new addresses error cause. + + Args: + code: error cause code + cause: error cause data + value: the new address parameters, complete with their types and + lengths + **kwargs: arbitrary keyword arguments + + Returns: + Constructed error cause schema. + + """ + if cause is not None: + value = cause.value + + return Schema_RestartOfAnAssociationWithNewAddressesCause( + code=code, + length=len(value) + 4, + value=value, + ) + + def _make_cause_user_abort(self, code: 'Enum_CauseCode', cause: 'Optional[Data_UserInitiatedAbortCause]' = None, *, + info: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_UserInitiatedAbortCause': + """Make SCTP user-initiated abort error cause. + + Args: + code: error cause code + cause: error cause data + info: upper layer abort reason + **kwargs: arbitrary keyword arguments + + Returns: + Constructed error cause schema. + + """ + if cause is not None: + info = cause.info + + return Schema_UserInitiatedAbortCause( + code=code, + length=len(info) + 4, + info=info, + ) + + def _make_cause_protocol_violation(self, code: 'Enum_CauseCode', cause: 'Optional[Data_ProtocolViolationCause]' = None, *, + info: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_ProtocolViolationCause': + """Make SCTP protocol violation error cause. + + Args: + code: error cause code + cause: error cause data + info: additional information + **kwargs: arbitrary keyword arguments + + Returns: + Constructed error cause schema. + + """ + if cause is not None: + info = cause.info + + return Schema_ProtocolViolationCause( + code=code, + length=len(info) + 4, + info=info, + ) diff --git a/pcapkit/protocols/transport/transport.py b/pcapkit/protocols/transport/transport.py index 551e6d645e..9faca552e3 100644 --- a/pcapkit/protocols/transport/transport.py +++ b/pcapkit/protocols/transport/transport.py @@ -7,8 +7,9 @@ :mod:`pcapkit.protocols.transport.transport` contains :class:`~pcapkit.protocols.transport.transport.Transport`, which is a base class for transport layer protocols, eg. -:class:`~pcapkit.protocols.transport.transport.tcp.TCP` and -:class:`~pcapkit.protocols.transport.transport.udp.UDP`. +:class:`~pcapkit.protocols.transport.tcp.TCP`, +:class:`~pcapkit.protocols.transport.udp.UDP` and +:class:`~pcapkit.protocols.transport.sctp.SCTP`. """ import io diff --git a/tests/protocols/transport/test_sctp_unit.py b/tests/protocols/transport/test_sctp_unit.py new file mode 100644 index 0000000000..40714d78a5 --- /dev/null +++ b/tests/protocols/transport/test_sctp_unit.py @@ -0,0 +1,1146 @@ +"""Unit tests for :mod:`pcapkit.protocols.transport.sctp`. + +Every chunk type is exercised three ways, because each catches a different class +of mistake: + +* **Round trip** -- ``make`` the chunk, pack it, read it back, and compare field + by field. This catches a constructor and a parser that disagree, but it cannot + catch the two of them being wrong in the same way. +* **Wire conformance** -- parse a byte string written out by hand from the packet + diagrams in :rfc:`9260`, and assert the field values those diagrams imply. + This is what catches a misread of the RFC, which a round trip cannot. +* **Cross-check against scapy** -- build the same chunk with + :mod:`scapy.layers.sctp`, write it to a capture, and assert :mod:`pcapkit` + reads the values scapy put in. This is the independent check: it catches a + misreading that happens to be self-consistent across our own reader and + writer, and it catches the byte order of the CRC32c checksum. + +""" + +from __future__ import annotations + +import importlib.util +import unittest +from unittest import mock + +from tests._support import close_extractor + +RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') +HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) +HAS_SCAPY = importlib.util.find_spec('scapy') is not None + + +def bitfield_packs_zero_bits() -> bool: + """Whether :class:`~pcapkit.corekit.fields.strings.BitField` packs a clear bit. + + ``BitField.pre_process`` seeds its working buffer with NUL bytes and then + writes the ASCII characters ``b'0'`` and ``b'1'`` into it, before collapsing + the buffer by truth-testing each byte -- and ``b'0'`` is ``0x30``, which is + truthy. Every named bit therefore comes out **set** regardless of its value, + so a flag word constructed with any bit clear does not survive a round trip. + + That is a library-wide defect in :mod:`pcapkit.corekit`, not an SCTP one, and + it is being fixed separately. The tests that need clear bits on the + *construction* side are gated on this probe so that they start running of + their own accord once the fix lands. Parsing is unaffected -- + ``BitField.post_process`` is correct -- so the wire-conformance and scapy + cross-check tests below exercise mixed flags unconditionally. + + """ + from pcapkit.corekit.fields.strings import BitField + + field = BitField(length=1, namespace={'a': (0, 1), 'b': (7, 1)}) + return field.pre_process({'a': 0, 'b': 1}, {}) == b'\x01' + + +@unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') +class SCTPUnitTests(unittest.TestCase): + # NOTE: Unlike the sibling TCP/UDP unit tests, this module does not purge + # and re-import :mod:`pcapkit` per test: nothing here depends on import-time + # behaviour, and the re-import costs several seconds a test. Every test that + # mutates a class-level registry restores it in a ``finally``. + + ########################################################################## + # Helpers. + ########################################################################## + + @staticmethod + def _packet(raw: bytes): + """Parse ``raw`` as a whole SCTP packet.""" + import io + + from pcapkit.protocols.transport.sctp import SCTP + + return SCTP(io.BytesIO(raw), len(raw)) + + @staticmethod + def _build(chunks, **kwargs) -> bytes: + """Construct a whole SCTP packet from a list of chunk specifications.""" + from pcapkit.protocols.transport.sctp import SCTP + + proto = SCTP.__new__(SCTP) + params = {'srcport': 9899, 'dstport': 38412, 'vtag': 0x11223344} + params.update(kwargs) + return SCTP.make(proto, chunks=chunks, **params).pack() + + ########################################################################## + # Common header and CRC32c checksum. + ########################################################################## + + def test_crc32c_matches_the_rfc9260_appendix_a_table(self) -> None: + from pcapkit.protocols.transport.sctp import CRC32C_TABLE, SCTP + + # Spot values quoted verbatim from the crc_c[] table in RFC 9260 + # Appendix A, which is what pins the polynomial and the reflection. + self.assertEqual(len(CRC32C_TABLE), 256) + self.assertEqual(CRC32C_TABLE[0], 0x00000000) + self.assertEqual(CRC32C_TABLE[1], 0xF26B8303) + self.assertEqual(CRC32C_TABLE[2], 0xE13B70F7) + self.assertEqual(CRC32C_TABLE[3], 0x1350F3F4) + self.assertEqual(CRC32C_TABLE[16], 0x105EC76F) + + # The CRC32c of the empty string is 0, and of b'123456789' is the + # Castagnoli check value 0xE3069283. + self.assertEqual(SCTP.crc32c(b''), 0x00000000) + self.assertEqual(SCTP.crc32c(b'123456789'), 0xE3069283) + + def test_common_header_wire_conformance(self) -> None: + from pcapkit.const.reg.apptype import AppType, TransportProtocol + from pcapkit.const.sctp.chunk import Chunk + + # RFC 9260 section 3.1: source port, destination port, verification + # tag, checksum -- then chunks. Followed by a COOKIE ACK chunk, which + # is the shortest chunk there is (length 4, no value). + raw = bytes.fromhex( + '26ab' # source port = 9899 + '960c' # destination port = 38412 + '11223344' # verification tag + '00000000' # checksum, zeroed + '0b000004' # COOKIE ACK chunk: type 11, flags 0, length 4 + ) + proto = self._packet(raw) + info = proto.info + + self.assertEqual(info.srcport.port, 9899) + self.assertEqual(info.dstport.port, 38412) + self.assertEqual(info.srcport, AppType.get(9899, proto=TransportProtocol.sctp)) + self.assertEqual(info.vtag, 0x11223344) + self.assertEqual(info.chksum, b'\x00\x00\x00\x00') + self.assertEqual(list(info.chunks.keys()), [Chunk.Cookie_Acknowledgement]) + + self.assertEqual(proto.length, 12) + self.assertEqual(proto.__length_hint__(), 12) + self.assertEqual(proto.name, 'Stream Control Transmission Protocol') + self.assertEqual(proto.src.port, 9899) + self.assertEqual(proto.dst.port, 38412) + + # A zeroed checksum is not the CRC32c of this packet. + self.assertFalse(proto.checksum_valid) + + def test_checksum_is_the_little_endian_crc32c_of_the_zeroed_packet(self) -> None: + import struct + + from pcapkit.protocols.transport.sctp import SCTP + + raw = self._build([]) + self.assertEqual(len(raw), 12) + + zeroed = raw[:8] + b'\x00\x00\x00\x00' + expect = struct.pack('I', SCTP.crc32c(zeroed))) + + self.assertTrue(SCTP.validate_checksum(raw)) + self.assertEqual(SCTP.calculate_checksum(raw), raw[8:12]) + # calculate_checksum must ignore whatever is already in the field. + self.assertEqual(SCTP.calculate_checksum(raw[:8] + b'\xff\xff\xff\xff'), raw[8:12]) + self.assertFalse(SCTP.validate_checksum(raw[:8] + b'\xff\xff\xff\xff')) + + self.assertTrue(self._packet(raw).checksum_valid) + + def test_checksum_helpers_reject_a_short_packet(self) -> None: + from pcapkit.protocols.transport.sctp import SCTP + from pcapkit.utilities.exceptions import ProtocolError + + with self.assertRaises(ProtocolError): + SCTP.calculate_checksum(b'\x00' * 11) + with self.assertRaises(ProtocolError): + SCTP.validate_checksum(b'\x00' * 11) + + def test_index_is_the_iana_protocol_number(self) -> None: + from pcapkit.const.reg.transtype import TransType + from pcapkit.protocols.transport.sctp import SCTP + + self.assertEqual(SCTP.__index__(), TransType.SCTP) + self.assertEqual(int(SCTP.__index__()), 132) + + def test_internet_dispatches_protocol_132_to_sctp(self) -> None: + from pcapkit.const.reg.transtype import TransType + from pcapkit.corekit.module import ModuleDescriptor + from pcapkit.protocols.internet.internet import Internet + from pcapkit.protocols.transport.sctp import SCTP + + entry = Internet.__proto__[TransType.SCTP] + if isinstance(entry, ModuleDescriptor): + entry = entry.klass + self.assertIs(entry, SCTP) + + ########################################################################## + # Chunks: round trip. + ########################################################################## + + def test_every_chunk_type_round_trips(self) -> None: + from pcapkit.const.sctp.cause_code import CauseCode + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.const.sctp.parameter import Parameter + + # Every flag in the flag-carrying chunks is set, so that the tests are + # not blocked on the BitField construction defect described by + # :func:`bitfield_packs_zero_bits`; mixed flags are covered on the + # parsing side by the wire-conformance and scapy tests below, and on the + # construction side by the gated test that follows this one. + cases = [ + (Chunk.Payload_Data, + dict(I=True, U=True, B=True, E=True, tsn=0x0A0B0C0D, stream_id=1, + stream_seq=2, ppid=60, data=b'ngap-pdu'), + dict(length=24, tsn=0x0A0B0C0D, stream_id=1, stream_seq=2, ppid=60, + data=b'ngap-pdu')), + (Chunk.Initiation, + dict(init_tag=0x11223344, a_rwnd=106496, outbound_streams=10, + inbound_streams=10, init_tsn=0x55667788, + parameters=[(Parameter.IPv4_Address, {'address': '10.0.0.1'})]), + dict(length=28, init_tag=0x11223344, a_rwnd=106496, outbound_streams=10, + inbound_streams=10, init_tsn=0x55667788)), + (Chunk.Initiation_Acknowledgement, + dict(init_tag=0x99AABBCC, a_rwnd=4660, outbound_streams=3, + inbound_streams=4, init_tsn=7, + parameters=[(Parameter.State_Cookie, {'cookie': b'\xde\xad\xbe\xef\x01'})]), + dict(length=32, init_tag=0x99AABBCC, a_rwnd=4660, outbound_streams=3, + inbound_streams=4, init_tsn=7)), + (Chunk.Selective_Acknowledgement, + dict(cum_tsn_ack=12, a_rwnd=4660, gap_blocks=[(2, 3), (5, 5)], + dup_tsn=[19, 19]), + dict(length=32, cum_tsn_ack=12, a_rwnd=4660, num_gap_blocks=2, + num_dup_tsn=2, dup_tsn=(19, 19))), + (Chunk.Heartbeat_Request, + dict(parameters=[(Parameter.Heartbeat_Info, {'info': b'\xca\xfe\xba\xbe'})]), + dict(length=12)), + (Chunk.Heartbeat_Acknowledgement, + dict(parameters=[(Parameter.Heartbeat_Info, {'info': b'\xca\xfe\xba\xbe'})]), + dict(length=12)), + (Chunk.Abort, + dict(T=True, error=[(CauseCode.User_Initiated_Abort, {'info': b'bye'})]), + dict(length=12)), + (Chunk.Shutdown, + dict(cum_tsn_ack=0x0A0B0C10), + dict(length=8, cum_tsn_ack=0x0A0B0C10)), + (Chunk.Shutdown_Acknowledgement, {}, dict(length=4)), + (Chunk.Operation_Error, + dict(error=[(CauseCode.Invalid_Stream_Identifier, {'stream_id': 9})]), + dict(length=12)), + (Chunk.State_Cookie, + dict(cookie=b'\xde\xad\xbe\xef\xfe\xed'), + dict(length=10, cookie=b'\xde\xad\xbe\xef\xfe\xed')), + (Chunk.Cookie_Acknowledgement, {}, dict(length=4)), + (Chunk.Shutdown_Complete, dict(T=True), dict(length=4)), + ] + + for code, args, expect in cases: + with self.subTest(chunk=code.name): + raw = self._build([(code, args)]) + # The whole packet is always a multiple of four bytes: each + # chunk pads itself per RFC 9260 section 3.2. + self.assertEqual(len(raw) % 4, 0) + + proto = self._packet(raw) + self.assertTrue(proto.checksum_valid) + + chunk = proto.info.chunks[code] + self.assertEqual(chunk.type, code) + for key, value in expect.items(): + self.assertEqual(getattr(chunk, key), value, + f'{code.name}.{key}') + + # Flags survive the round trip when every named bit is set. + raw = self._build([(Chunk.Payload_Data, + dict(I=True, U=True, B=True, E=True, tsn=1, data=b'x'))]) + flags = self._packet(raw).info.chunks[Chunk.Payload_Data].flags + self.assertTrue(flags.I and flags.U and flags.B and flags.E) + + raw = self._build([(Chunk.Abort, dict(T=True))]) + self.assertTrue(self._packet(raw).info.chunks[Chunk.Abort].flags.T) + + raw = self._build([(Chunk.Shutdown_Complete, dict(T=True))]) + self.assertTrue(self._packet(raw).info.chunks[Chunk.Shutdown_Complete].flags.T) + + def test_chunk_flags_round_trip_with_mixed_bits(self) -> None: + from pcapkit.const.sctp.chunk import Chunk + + if not bitfield_packs_zero_bits(): + self.skipTest('BitField.pre_process sets every named bit regardless ' + 'of value; see bitfield_packs_zero_bits()') + + raw = self._build([(Chunk.Payload_Data, + dict(I=False, U=False, B=True, E=True, tsn=1, data=b'x'))]) + flags = self._packet(raw).info.chunks[Chunk.Payload_Data].flags + self.assertFalse(flags.I) + self.assertFalse(flags.U) + self.assertTrue(flags.B) + self.assertTrue(flags.E) + + raw = self._build([(Chunk.Abort, dict(T=False))]) + self.assertFalse(self._packet(raw).info.chunks[Chunk.Abort].flags.T) + + def test_bundled_chunks_all_parse(self) -> None: + from pcapkit.const.sctp.chunk import Chunk + + raw = self._build([ + (Chunk.Selective_Acknowledgement, dict(cum_tsn_ack=5, a_rwnd=4660)), + (Chunk.Payload_Data, dict(I=True, U=True, B=True, E=True, tsn=6, + ppid=60, data=b'seven')), + (Chunk.Cookie_Acknowledgement, {}), + ]) + proto = self._packet(raw) + self.assertTrue(proto.checksum_valid) + self.assertEqual(list(proto.info.chunks.keys()), + [Chunk.Selective_Acknowledgement, Chunk.Payload_Data, + Chunk.Cookie_Acknowledgement]) + # A five-byte payload leaves the DATA chunk needing three bytes of + # padding; the COOKIE ACK after it must still land on its type byte. + self.assertEqual(proto.info.chunks[Chunk.Payload_Data].data, b'seven') + self.assertEqual(proto.info.chunks[Chunk.Cookie_Acknowledgement].length, 4) + + def test_make_data_reconstructs_the_packet(self) -> None: + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.protocols.transport.sctp import SCTP + + raw = self._build([ + (Chunk.Payload_Data, dict(I=True, U=True, B=True, E=True, tsn=9, + stream_id=1, stream_seq=2, ppid=60, + data=b'payload')), + (Chunk.Cookie_Acknowledgement, {}), + ]) + proto = self._packet(raw) + + values = SCTP._make_data(proto.info) + self.assertEqual(values['srcport'], proto.info.srcport) + self.assertEqual(values['dstport'], proto.info.dstport) + self.assertEqual(values['vtag'], 0x11223344) + self.assertEqual(values['chksum'], proto.info.chksum) + self.assertIs(values['chunks'], proto.info.chunks) + # SCTP has no payload argument: user data lives inside a DATA chunk. + self.assertNotIn('payload', values) + + rebuilt = SCTP.from_data(proto.info) + self.assertEqual(bytes(rebuilt), raw) + + ########################################################################## + # Chunks: wire conformance, from the RFC 9260 packet diagrams. + ########################################################################## + + def test_chunk_wire_conformance(self) -> None: + from pcapkit.const.sctp.cause_code import CauseCode + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.const.sctp.parameter import Parameter + + header = bytes.fromhex('26ab960c1122334400000000') + + # RFC 9260 section 3.3.1: type 0, Res|I|U|B|E, length, TSN, stream id, + # stream sequence number, PPID, user data. Flags 0x03 is B|E, i.e. an + # unfragmented message -- which pins B at bit 6 and E at bit 7. + proto = self._packet(header + bytes.fromhex( + '00' '03' '0018' '0a0b0c0d' '0001' '0002' '0000003c' + '0015000800000400')) + chunk = proto.info.chunks[Chunk.Payload_Data] + self.assertEqual(chunk.length, 24) + self.assertFalse(chunk.flags.I) + self.assertFalse(chunk.flags.U) + self.assertTrue(chunk.flags.B) + self.assertTrue(chunk.flags.E) + self.assertEqual(chunk.tsn, 0x0A0B0C0D) + self.assertEqual(chunk.stream_id, 1) + self.assertEqual(chunk.stream_seq, 2) + self.assertEqual(chunk.ppid, 60) + self.assertEqual(chunk.data, bytes.fromhex('0015000800000400')) + + # Flags 0x0c is I|U, which pins I at bit 4 and U at bit 5. + proto = self._packet(header + bytes.fromhex( + '00' '0c' '0011' '00000001' '0000' '0000' '00000000' '41000000')) + chunk = proto.info.chunks[Chunk.Payload_Data] + self.assertTrue(chunk.flags.I) + self.assertTrue(chunk.flags.U) + self.assertFalse(chunk.flags.B) + self.assertFalse(chunk.flags.E) + self.assertEqual(chunk.data, b'A') + + # RFC 9260 section 3.3.2: type 1, flags, length, initiate tag, a_rwnd, + # outbound streams, inbound streams, initial TSN, then parameters. + proto = self._packet(header + bytes.fromhex( + '01' '00' '0024' '11223344' '0001a000' '000a' '000a' '55667788' + '00050008' '0a000001' + '000c0008' '0005' '0006')) + chunk = proto.info.chunks[Chunk.Initiation] + self.assertEqual(chunk.length, 36) + self.assertEqual(chunk.init_tag, 0x11223344) + self.assertEqual(chunk.a_rwnd, 106496) + self.assertEqual(chunk.outbound_streams, 10) + self.assertEqual(chunk.inbound_streams, 10) + self.assertEqual(chunk.init_tsn, 0x55667788) + self.assertEqual(list(chunk.parameters.keys()), + [Parameter.IPv4_Address, Parameter.Supported_Address_Types]) + + # RFC 9260 section 3.3.3: same fixed fields as INIT, type 2. + proto = self._packet(header + bytes.fromhex( + '02' '00' '0020' '99aabbcc' '00001234' '0003' '0004' '00000007' + '00070009' 'deadbeef01' '000000')) + chunk = proto.info.chunks[Chunk.Initiation_Acknowledgement] + self.assertEqual(chunk.init_tag, 0x99AABBCC) + self.assertEqual(chunk.a_rwnd, 4660) + self.assertEqual(chunk.outbound_streams, 3) + self.assertEqual(chunk.inbound_streams, 4) + self.assertEqual(chunk.init_tsn, 7) + cookie = chunk.parameters[Parameter.State_Cookie] + self.assertEqual(cookie.cookie, bytes.fromhex('deadbeef01')) + + # RFC 9260 section 3.3.4, using the worked example from that section: + # cumulative TSN ack 12, a_rwnd 4660, two gap ack blocks (2..3 and + # 5..5), no duplicates -- plus two duplicate TSNs of 19, from the + # duplicate-TSN example immediately below it. + proto = self._packet(header + bytes.fromhex( + '03' '00' '0020' '0000000c' '00001234' '0002' '0002' + '0002' '0003' '0005' '0005' '00000013' '00000013')) + chunk = proto.info.chunks[Chunk.Selective_Acknowledgement] + self.assertEqual(chunk.length, 32) + self.assertEqual(chunk.cum_tsn_ack, 12) + self.assertEqual(chunk.a_rwnd, 4660) + self.assertEqual(chunk.num_gap_blocks, 2) + self.assertEqual(chunk.num_dup_tsn, 2) + self.assertEqual([(b.start, b.end) for b in chunk.gap_blocks], [(2, 3), (5, 5)]) + self.assertEqual(chunk.dup_tsn, (19, 19)) + + # RFC 9260 sections 3.3.5 and 3.3.6: type 4 / 5 with one heartbeat info + # parameter. + for type_byte, code in (('04', Chunk.Heartbeat_Request), + ('05', Chunk.Heartbeat_Acknowledgement)): + proto = self._packet(header + bytes.fromhex( + type_byte + '00' '000c' '00010008' 'cafebabe')) + chunk = proto.info.chunks[code] + self.assertEqual(chunk.length, 12) + info = chunk.parameters[Parameter.Heartbeat_Info] + self.assertEqual(info.info, bytes.fromhex('cafebabe')) + + # RFC 9260 section 3.3.7: type 6, Reserved|T, length, error causes. + # Flags 0x01 sets T, which pins T at bit 7. + proto = self._packet(header + bytes.fromhex( + '06' '01' '000c' '000c0007' '62796500')) + chunk = proto.info.chunks[Chunk.Abort] + self.assertEqual(chunk.length, 12) + self.assertTrue(chunk.flags.T) + self.assertEqual(chunk.error[CauseCode.User_Initiated_Abort].info, b'bye') + + proto = self._packet(header + bytes.fromhex('06' '00' '0004')) + chunk = proto.info.chunks[Chunk.Abort] + self.assertFalse(chunk.flags.T) + self.assertEqual(len(chunk.error), 0) + + # RFC 9260 section 3.3.8: type 7, length 8, cumulative TSN ack. + proto = self._packet(header + bytes.fromhex('07' '00' '0008' '0a0b0c10')) + chunk = proto.info.chunks[Chunk.Shutdown] + self.assertEqual(chunk.length, 8) + self.assertEqual(chunk.cum_tsn_ack, 0x0A0B0C10) + + # RFC 9260 section 3.3.9: type 8, length 4, no parameters. + proto = self._packet(header + bytes.fromhex('08' '00' '0004')) + self.assertEqual(proto.info.chunks[Chunk.Shutdown_Acknowledgement].length, 4) + + # RFC 9260 section 3.3.10: type 9, one or more error causes. + proto = self._packet(header + bytes.fromhex( + '09' '00' '0010' '00010008' '0009' '0000' '00040004')) + chunk = proto.info.chunks[Chunk.Operation_Error] + self.assertEqual(chunk.length, 16) + self.assertEqual(list(chunk.error.keys()), + [CauseCode.Invalid_Stream_Identifier, CauseCode.Out_of_Resource]) + self.assertEqual(chunk.error[CauseCode.Invalid_Stream_Identifier].stream_id, 9) + + # RFC 9260 section 3.3.11: type 10, length, cookie -- the cookie is the + # *contents* of the state cookie parameter, not the parameter itself. + proto = self._packet(header + bytes.fromhex( + '0a' '00' '000a' 'deadbeeffeed' '0000')) + chunk = proto.info.chunks[Chunk.State_Cookie] + self.assertEqual(chunk.length, 10) + self.assertEqual(chunk.cookie, bytes.fromhex('deadbeeffeed')) + + # RFC 9260 section 3.3.12: type 11, length 4. + proto = self._packet(header + bytes.fromhex('0b' '00' '0004')) + self.assertEqual(proto.info.chunks[Chunk.Cookie_Acknowledgement].length, 4) + + # RFC 9260 section 3.3.13: type 14, Reserved|T, length 4. + proto = self._packet(header + bytes.fromhex('0e' '01' '0004')) + chunk = proto.info.chunks[Chunk.Shutdown_Complete] + self.assertEqual(chunk.length, 4) + self.assertTrue(chunk.flags.T) + + proto = self._packet(header + bytes.fromhex('0e' '00' '0004')) + self.assertFalse(proto.info.chunks[Chunk.Shutdown_Complete].flags.T) + + def test_final_chunk_padding_may_be_omitted_from_the_length(self) -> None: + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.const.sctp.parameter import Parameter + + header = bytes.fromhex('26ab960c1122334400000000') + + # RFC 9260 section 3.2 says a robust implementation accepts a chunk + # whether or not the final padding is counted in the chunk length. Here + # the INIT ACK declares 29 (the state cookie's own three padding bytes + # excluded) while 32 bytes are actually present, and a COOKIE ACK + # follows -- so a parser that trusts the length blindly loses alignment + # and misreads the next chunk. + proto = self._packet(header + bytes.fromhex( + '02' '00' '001d' '99aabbcc' '00001234' '0003' '0004' '00000007' + '00070009' 'deadbeef01' '000000' + '0b000004')) + self.assertEqual(list(proto.info.chunks.keys()), + [Chunk.Initiation_Acknowledgement, Chunk.Cookie_Acknowledgement]) + chunk = proto.info.chunks[Chunk.Initiation_Acknowledgement] + self.assertEqual(chunk.length, 29) + self.assertEqual(chunk.parameters[Parameter.State_Cookie].cookie, + bytes.fromhex('deadbeef01')) + self.assertEqual(proto.info.chunks[Chunk.Cookie_Acknowledgement].length, 4) + + ########################################################################## + # Chunk parameters. + ########################################################################## + + def test_chunk_parameter_wire_conformance(self) -> None: + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.const.sctp.parameter import Parameter + + header = bytes.fromhex('26ab960c1122334400000000') + params = bytes.fromhex( + '00050008' '0a000001' # IPv4 address, s. 3.3.2.1.1 + '00060014' '20010db8000000000000000000000001' # IPv6 address, s. 3.3.2.1.2 + '00090008' '000003e8' # cookie preservative, s. 3.3.2.1.3 + '000b000e' '6c6f63616c686f737400' '0000' # host name, s. 3.3.2.1.4 + '000c0008' '0005' '0006' # supported addr types, s. 3.3.2.1.5 + '00070008' 'deadbeef' # state cookie, s. 3.3.3.1.1 + '00080008' '80000004' # unrecognized param, s. 3.3.3.1.2 + '00010008' 'cafebabe' # heartbeat info, s. 3.3.5 + '99990006' '4142' '0000' # unassigned type -> generic + ) + length = 20 + len(params) + raw = (header + bytes.fromhex('0100') + length.to_bytes(2, 'big') + + bytes.fromhex('11223344' '0001a000' '000a' '000a' '55667788') + params) + chunk = self._packet(raw).info.chunks[Chunk.Initiation] + + self.assertEqual(chunk.length, length) + self.assertEqual(list(chunk.parameters.keys()), [ + Parameter.IPv4_Address, Parameter.IPv6_Address, Parameter.Cookie_Preservative, + Parameter.Host_Name_Address, Parameter.Supported_Address_Types, + Parameter.State_Cookie, Parameter.Unrecognized_Parameter, + Parameter.Heartbeat_Info, Parameter.get(0x9999), + ]) + + import ipaddress + + self.assertEqual(chunk.parameters[Parameter.IPv4_Address].address, + ipaddress.IPv4Address('10.0.0.1')) + self.assertEqual(chunk.parameters[Parameter.IPv6_Address].address, + ipaddress.IPv6Address('2001:db8::1')) + self.assertEqual(chunk.parameters[Parameter.Cookie_Preservative].increment, 1000) + self.assertEqual(chunk.parameters[Parameter.Host_Name_Address].name, + b'localhost\x00') + self.assertEqual(chunk.parameters[Parameter.Supported_Address_Types].types, + (Parameter.IPv4_Address, Parameter.IPv6_Address)) + self.assertEqual(chunk.parameters[Parameter.State_Cookie].cookie, + bytes.fromhex('deadbeef')) + self.assertEqual(chunk.parameters[Parameter.Unrecognized_Parameter].value, + bytes.fromhex('80000004')) + self.assertEqual(chunk.parameters[Parameter.Heartbeat_Info].info, + bytes.fromhex('cafebabe')) + # An unassigned parameter type falls through to the generic handler + # rather than raising, and keeps its value verbatim. + generic = chunk.parameters[Parameter.get(0x9999)] + self.assertEqual(generic.length, 6) + self.assertEqual(generic.value, b'AB') + + def test_every_chunk_parameter_round_trips(self) -> None: + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.const.sctp.parameter import Parameter + + cases = [ + (Parameter.Heartbeat_Info, {'info': b'\xca\xfe\xba\xbe'}, 'info', + b'\xca\xfe\xba\xbe'), + (Parameter.IPv4_Address, {'address': '10.0.0.1'}, 'address', None), + (Parameter.IPv6_Address, {'address': '2001:db8::1'}, 'address', None), + (Parameter.State_Cookie, {'cookie': b'\xde\xad\xbe\xef'}, 'cookie', + b'\xde\xad\xbe\xef'), + (Parameter.Unrecognized_Parameter, {'value': b'\x80\x00\x00\x04'}, 'value', + b'\x80\x00\x00\x04'), + (Parameter.Cookie_Preservative, {'increment': 1000}, 'increment', 1000), + (Parameter.Host_Name_Address, {'name': b'localhost\x00'}, 'name', + b'localhost\x00'), + (Parameter.Supported_Address_Types, + {'types': [Parameter.IPv4_Address, Parameter.IPv6_Address]}, 'types', + (Parameter.IPv4_Address, Parameter.IPv6_Address)), + (Parameter.get(0x9999), {'value': b'AB'}, 'value', b'AB'), + ] + + for code, args, attr, expect in cases: + with self.subTest(parameter=code.name): + raw = self._build([(Chunk.Initiation, + dict(init_tag=1, parameters=[(code, args)]))]) + self.assertEqual(len(raw) % 4, 0) + chunk = self._packet(raw).info.chunks[Chunk.Initiation] + param = chunk.parameters[code] + self.assertEqual(param.type, code) + if expect is not None: + self.assertEqual(getattr(param, attr), expect) + else: + self.assertEqual(str(getattr(param, attr)), args['address']) + + def test_host_name_parameter_must_be_null_terminated(self) -> None: + from pcapkit.const.sctp.parameter import Parameter + from pcapkit.protocols.transport.sctp import SCTP + from pcapkit.utilities.exceptions import ProtocolError + + proto = SCTP.__new__(SCTP) + with self.assertRaises(ProtocolError): + SCTP._make_param_hostname(proto, Parameter.Host_Name_Address, + name=b'localhost') + + ########################################################################## + # Error causes. + ########################################################################## + + def test_every_error_cause_wire_conformance(self) -> None: + from pcapkit.const.sctp.cause_code import CauseCode + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.const.sctp.parameter import Parameter + + header = bytes.fromhex('26ab960c1122334400000000') + causes = bytes.fromhex( + '00010008' '0009' '0000' # invalid stream identifier, s. 3.3.10.1 + '0002000a' '00000001' '0007' '0000' # missing mandatory param, s. 3.3.10.2 + '00030008' '000003e8' # stale cookie, s. 3.3.10.3 + '00040004' # out of resource, s. 3.3.10.4 + '0005000c' '00050008' '0a000001' # unresolvable address, s. 3.3.10.5 + '00060008' '9900' '0004' # unrecognized chunk type, s. 3.3.10.6 + '00070004' # invalid mandatory param, s. 3.3.10.7 + '00080008' '80000004' # unrecognized parameters, s. 3.3.10.8 + '00090008' '0000000c' # no user data, s. 3.3.10.9 + '000a0004' # cookie while shutting down, s. 3.3.10.10 + '000b000c' '00050008' '0a000002' # restart with new addresses, s. 3.3.10.11 + '000c0007' '62796500' # user-initiated abort, s. 3.3.10.12 + '000d0008' '6e6f7065' # protocol violation, s. 3.3.10.13 + '01010006' '4142' '0000' # unassigned code -> generic + ) + length = 4 + len(causes) + raw = (header + bytes.fromhex('0900') + length.to_bytes(2, 'big') + causes) + chunk = self._packet(raw).info.chunks[Chunk.Operation_Error] + error = chunk.error + + self.assertEqual(chunk.length, length) + self.assertEqual(list(error.keys()), [ + CauseCode.Invalid_Stream_Identifier, CauseCode.Missing_Mandatory_Parameter, + CauseCode.Stale_Cookie, CauseCode.Out_of_Resource, + CauseCode.Unresolvable_Address, CauseCode.Unrecognized_Chunk_Type, + CauseCode.Invalid_Mandatory_Parameter, CauseCode.Unrecognized_Parameters, + CauseCode.No_User_Data, CauseCode.Cookie_Received_While_Shutting_Down, + CauseCode.Restart_of_an_Association_with_New_Addresses, + CauseCode.User_Initiated_Abort, CauseCode.Protocol_Violation, + CauseCode.get(0x0101), + ]) + + self.assertEqual(error[CauseCode.Invalid_Stream_Identifier].stream_id, 9) + + missing = error[CauseCode.Missing_Mandatory_Parameter] + self.assertEqual(missing.num, 1) + self.assertEqual(missing.types, (Parameter.State_Cookie,)) + + self.assertEqual(error[CauseCode.Stale_Cookie].staleness, 1000) + self.assertEqual(error[CauseCode.Out_of_Resource].length, 4) + self.assertEqual(error[CauseCode.Unresolvable_Address].value, + bytes.fromhex('000500080a000001')) + self.assertEqual(error[CauseCode.Unrecognized_Chunk_Type].value, + bytes.fromhex('99000004')) + self.assertEqual(error[CauseCode.Invalid_Mandatory_Parameter].length, 4) + self.assertEqual(error[CauseCode.Unrecognized_Parameters].value, + bytes.fromhex('80000004')) + self.assertEqual(error[CauseCode.No_User_Data].tsn, 12) + self.assertEqual(error[CauseCode.Cookie_Received_While_Shutting_Down].length, 4) + self.assertEqual(error[CauseCode.Restart_of_an_Association_with_New_Addresses].value, + bytes.fromhex('000500080a000002')) + self.assertEqual(error[CauseCode.User_Initiated_Abort].info, b'bye') + self.assertEqual(error[CauseCode.Protocol_Violation].info, b'nope') + + # An unassigned cause code falls through to the generic handler. + generic = error[CauseCode.get(0x0101)] + self.assertEqual(generic.length, 6) + self.assertEqual(generic.value, b'AB') + + def test_every_error_cause_round_trips(self) -> None: + from pcapkit.const.sctp.cause_code import CauseCode + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.const.sctp.parameter import Parameter + + cases = [ + (CauseCode.Invalid_Stream_Identifier, {'stream_id': 9}, 'stream_id', 9), + (CauseCode.Missing_Mandatory_Parameter, + {'types': [Parameter.State_Cookie, Parameter.IPv4_Address]}, 'types', + (Parameter.State_Cookie, Parameter.IPv4_Address)), + (CauseCode.Stale_Cookie, {'staleness': 1000}, 'staleness', 1000), + (CauseCode.Out_of_Resource, {}, 'length', 4), + (CauseCode.Unresolvable_Address, {'value': b'\x00\x05\x00\x08\x0a\x00\x00\x01'}, + 'value', b'\x00\x05\x00\x08\x0a\x00\x00\x01'), + (CauseCode.Unrecognized_Chunk_Type, {'value': b'\x99\x00\x00\x04'}, 'value', + b'\x99\x00\x00\x04'), + (CauseCode.Invalid_Mandatory_Parameter, {}, 'length', 4), + (CauseCode.Unrecognized_Parameters, {'value': b'\x80\x00\x00\x04'}, 'value', + b'\x80\x00\x00\x04'), + (CauseCode.No_User_Data, {'tsn': 12}, 'tsn', 12), + (CauseCode.Cookie_Received_While_Shutting_Down, {}, 'length', 4), + (CauseCode.Restart_of_an_Association_with_New_Addresses, + {'value': b'\x00\x05\x00\x08\x0a\x00\x00\x02'}, 'value', + b'\x00\x05\x00\x08\x0a\x00\x00\x02'), + (CauseCode.User_Initiated_Abort, {'info': b'bye'}, 'info', b'bye'), + (CauseCode.Protocol_Violation, {'info': b'nope'}, 'info', b'nope'), + (CauseCode.get(0x0101), {'value': b'AB'}, 'value', b'AB'), + ] + + for code, args, attr, expect in cases: + with self.subTest(cause=code.name): + raw = self._build([(Chunk.Operation_Error, + dict(error=[(code, args)]))]) + self.assertEqual(len(raw) % 4, 0) + chunk = self._packet(raw).info.chunks[Chunk.Operation_Error] + cause = chunk.error[code] + self.assertEqual(cause.code, code) + self.assertEqual(getattr(cause, attr), expect) + + def test_missing_mandatory_parameter_count_must_match_the_length(self) -> None: + from pcapkit.utilities.exceptions import ProtocolError + + header = bytes.fromhex('26ab960c1122334400000000') + # Declares two missing parameters but carries only one. + with self.assertRaises(ProtocolError): + self._packet(header + bytes.fromhex( + '09' '00' '000e' '0002000a' '00000002' '0007' '0000')) + + ########################################################################## + # Unknown chunk types. + ########################################################################## + + def test_unknown_chunk_types_fall_through_to_the_generic_handler(self) -> None: + from pcapkit.const.sctp.chunk import Chunk + + header = bytes.fromhex('26ab960c1122334400000000') + + # Unassigned (200), reserved for IETF extensions (63), an extension + # chunk pcapkit does not implement (AUTH, 15), and the two chunk types + # RFC 9260 defines but reserves (ECNE 12 and CWR 13). None of these may + # raise -- they carry their raw flags and value through instead. + for code, type_byte in ((Chunk.get(200), 'c8'), + (Chunk.Reserved_for_IETF_defined_Chunk_Extensions_63, '3f'), + (Chunk.Authentication_Chunk, '0f'), + (Chunk.Reserved_for_Explicit_Congestion_Notification_Echo, + '0c'), + (Chunk.Reserved_for_Congestion_Window_Reduced, '0d')): + with self.subTest(chunk=code.name): + proto = self._packet(header + bytes.fromhex( + type_byte + 'ab' '0007' '010203' '00')) + chunk = proto.info.chunks[code] + self.assertEqual(chunk.type, code) + self.assertEqual(chunk.length, 7) + self.assertEqual(chunk.flags, b'\xab') + self.assertEqual(chunk.value, bytes.fromhex('010203')) + + # ... and a bundle whose first chunk is unknown still yields the rest. + proto = self._packet(header + bytes.fromhex('c8ab0007010203' '00' '0b000004')) + self.assertEqual(list(proto.info.chunks.keys()), + [Chunk.get(200), Chunk.Cookie_Acknowledgement]) + + def test_unknown_chunk_round_trips_through_the_generic_constructor(self) -> None: + from pcapkit.const.sctp.chunk import Chunk + + code = Chunk.get(200) + raw = self._build([(code, dict(flags=b'\xab', value=b'\x01\x02\x03'))]) + chunk = self._packet(raw).info.chunks[code] + self.assertEqual(chunk.flags, b'\xab') + self.assertEqual(chunk.value, b'\x01\x02\x03') + self.assertEqual(chunk.length, 7) + + def test_generic_chunk_constructor_rejects_a_bad_flags_width(self) -> None: + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.protocols.transport.sctp import SCTP + from pcapkit.utilities.exceptions import ProtocolError + + proto = SCTP.__new__(SCTP) + with self.assertRaises(ProtocolError): + SCTP._make_chunk_donone(proto, Chunk.get(200), flags=b'\x00\x00') + + ########################################################################## + # Malformed lengths. + ########################################################################## + + def test_malformed_chunk_lengths_raise(self) -> None: + from pcapkit.utilities.exceptions import ProtocolError + + header = bytes.fromhex('26ab960c1122334400000000') + # Each case declares a length that the chunk's own definition forbids, + # while still spanning exactly the bytes supplied -- so the failure is + # the read handler rejecting the length, not the schema running off the + # end of the buffer. + cases = { + # DATA carries no user data, but RFC 9260 3.3.1 requires at least + # one byte, i.e. a length above 16. + 'data': '00' '03' '0010' '0a0b0c0d' '0001' '0002' '0000003c', + # INIT is shorter than its own mandatory fixed fields. + 'init': '01' '00' '0010' '11223344' '0001a000' '000a' '000a' '55667788', + # SACK declares three gap ack blocks but a length that fits two. + 'sack': '03' '00' '0018' '0000000c' '00001234' '0003' '0000' + '0002' '0003' '0005' '0005', + # SHUTDOWN must be exactly 8. + 'shutdown': '07' '00' '0004' '0a0b0c10', + # SHUTDOWN ACK must be exactly 4. + 'shutdown_ack': '08' '00' '0008', + # COOKIE ACK must be exactly 4. + 'cookie_ack': '0b' '00' '0008', + # SHUTDOWN COMPLETE must be exactly 4. + 'shutdown_complete': '0e' '00' '0008', + } + for name, body in cases.items(): + with self.subTest(chunk=name): + with self.assertRaises(ProtocolError): + self._packet(header + bytes.fromhex(body)) + + def test_data_chunk_constructor_requires_user_data(self) -> None: + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.protocols.transport.sctp import SCTP + from pcapkit.utilities.exceptions import ProtocolError + + proto = SCTP.__new__(SCTP) + with self.assertRaises(ProtocolError): + SCTP._make_chunk_data(proto, Chunk.Payload_Data, data=b'') + + ########################################################################## + # The payload protocol identifier dispatch hook. + ########################################################################## + + def test_ppid_dispatch_hook(self) -> None: + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.const.sctp.payload_protocol_identifier import PayloadProtocolIdentifier + from pcapkit.protocols.misc.raw import Raw + from pcapkit.protocols.transport.sctp import SCTP + + NGAP_PPID = PayloadProtocolIdentifier.PayloadProtocolIdentifier_3GPP_NG_Application_Protocol # noqa: E501 + self.assertEqual(int(NGAP_PPID), 60) + + raw = self._build([(Chunk.Payload_Data, + dict(I=True, U=True, B=True, E=True, tsn=1, ppid=NGAP_PPID, + data=b'ngap-pdu'))]) + + # With nothing registered on the PPID, the payload is raw. + self.assertNotIn(NGAP_PPID, SCTP.__proto__) + proto = self._packet(raw) + self.assertEqual(proto.ppid, NGAP_PPID) + self.assertIsInstance(proto.payload, Raw) + self.assertEqual(bytes(proto.payload), b'ngap-pdu') + self.assertEqual(str(proto.protochain), 'SCTP:Raw') + + # This is exactly the call a future NGAP class makes. + try: + SCTP.register(NGAP_PPID, Raw) + self.assertIs(SCTP.__proto__[NGAP_PPID], Raw) + self.assertIs(SCTP.__proto__[60], Raw) + + proto = self._packet(raw) + self.assertIsInstance(proto.payload, Raw) + self.assertEqual(bytes(proto.payload), b'ngap-pdu') + finally: + SCTP.__proto__.clear() + + def test_packet_without_a_data_chunk_has_no_payload(self) -> None: + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.protocols.misc.null import NoPayload + + raw = self._build([(Chunk.Cookie_Acknowledgement, {})]) + proto = self._packet(raw) + self.assertIsNone(proto.ppid) + self.assertEqual(proto._get_payload(), b'') + self.assertIsInstance(proto.payload, NoPayload) + + def test_first_data_chunk_of_a_bundle_selects_the_next_layer(self) -> None: + from pcapkit.const.sctp.chunk import Chunk + + raw = self._build([ + (Chunk.Payload_Data, dict(I=True, U=True, B=True, E=True, tsn=1, + ppid=60, data=b'first')), + (Chunk.Payload_Data, dict(I=True, U=True, B=True, E=True, tsn=2, + ppid=53, data=b'second')), + ]) + proto = self._packet(raw) + self.assertEqual(proto.ppid, 60) + self.assertEqual(proto._get_payload(), b'first') + # The bundled chunk is still recorded, just not dispatched. + self.assertEqual([c.data for c in proto.info.chunks.getlist(Chunk.Payload_Data)], + [b'first', b'second']) + + def test_register_rejects_a_non_protocol(self) -> None: + from pcapkit.protocols.transport.sctp import SCTP + from pcapkit.utilities.exceptions import RegistryError + + with self.assertRaises(RegistryError): + SCTP.register(60, int) # type: ignore[arg-type] + + def test_register_warns_on_overwrite(self) -> None: + from pcapkit.protocols.misc.raw import Raw + from pcapkit.protocols.transport import sctp as sctp_module + from pcapkit.protocols.transport.sctp import SCTP + + try: + SCTP.register(60, Raw) + with mock.patch.object(sctp_module, 'warn') as warned: + SCTP.register(60, Raw) + self.assertEqual(warned.call_count, 1) + self.assertIn('payload protocol identifier', warned.call_args.args[0]) + finally: + SCTP.__proto__.clear() + + def test_register_sctp_wrapper_writes_the_ppid_registry(self) -> None: + from pcapkit.foundation.registry.protocols import register_sctp + from pcapkit.protocols.misc.raw import Raw + from pcapkit.protocols.transport.sctp import SCTP + + try: + register_sctp(60, 'pcapkit.protocols.misc.raw', 'Raw') + self.assertIs(SCTP.__proto__[60], Raw) + finally: + SCTP.__proto__.clear() + + ########################################################################## + # Sub-registry registration. + ########################################################################## + + def test_sub_registries_warn_on_overwrite(self) -> None: + from pcapkit.const.sctp.cause_code import CauseCode + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.const.sctp.parameter import Parameter + from pcapkit.protocols.transport import sctp as sctp_module + from pcapkit.protocols.transport.sctp import SCTP + + for register, code, registry in ( + (SCTP.register_chunk, Chunk.Payload_Data, SCTP.__chunk__), + (SCTP.register_parameter, Parameter.Heartbeat_Info, SCTP.__parameter__), + (SCTP.register_cause, CauseCode.Stale_Cookie, SCTP.__cause__), + ): + with self.subTest(register=register.__name__): + original = registry[code] + try: + with mock.patch.object(sctp_module, 'warn') as warned: + register(code, 'donone') + self.assertEqual(warned.call_count, 1) + self.assertEqual(registry[code], 'donone') + finally: + registry[code] = original + + def test_callable_sub_registry_entries_are_used(self) -> None: + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.protocols.transport.sctp import SCTP + + sentinel = object() + parser = mock.Mock(return_value=sentinel) + constructor = mock.Mock() + original = SCTP.__chunk__[Chunk.Cookie_Acknowledgement] + try: + SCTP.__chunk__[Chunk.Cookie_Acknowledgement] = (parser, constructor) + raw = bytes.fromhex('26ab960c1122334400000000' '0b000004') + proto = self._packet(raw) + self.assertIs(proto.info.chunks[Chunk.Cookie_Acknowledgement], sentinel) + self.assertEqual(parser.call_count, 1) + + proto = SCTP.__new__(SCTP) + SCTP._make_sctp_chunk(proto, Chunk.Cookie_Acknowledgement, None) + self.assertEqual(constructor.call_count, 1) + finally: + SCTP.__chunk__[Chunk.Cookie_Acknowledgement] = original + + def test_chunks_accept_prebuilt_schemas_and_raw_bytes(self) -> None: + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.protocols.schema.transport.sctp import CookieACKChunk + + schema = CookieACKChunk(type=Chunk.Cookie_Acknowledgement, flags=b'\x00', length=4) + raw = self._build([schema, bytes.fromhex('08000004')]) + proto = self._packet(raw) + self.assertEqual(list(proto.info.chunks.keys()), + [Chunk.Cookie_Acknowledgement, Chunk.Shutdown_Acknowledgement]) + + ########################################################################## + # Cross-check against scapy. + ########################################################################## + + @unittest.skipUnless(HAS_SCAPY, 'scapy not installed') + def test_scapy_cross_check(self) -> None: + import ipaddress + import os + import tempfile + + from scapy.layers.inet import IP + from scapy.layers.l2 import Ether + from scapy.layers.sctp import SCTP as Scapy_SCTP + from scapy.layers.sctp import (SCTPChunkAbort, SCTPChunkCookieEcho, SCTPChunkData, + SCTPChunkHeartbeatReq, SCTPChunkInit, + SCTPChunkParamHeartbeatInfo, SCTPChunkParamIPv4Addr, + SCTPChunkParamSupportedAddrTypes, SCTPChunkSACK, + SCTPChunkShutdown) + from scapy.utils import wrpcap + + from pcapkit.const.sctp.cause_code import CauseCode + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.const.sctp.parameter import Parameter + from pcapkit.interface import extract + + def frame(chunk): + return (Ether(src='02:00:00:00:00:01', dst='02:00:00:00:00:02') + / IP(src='10.0.0.1', dst='10.0.0.2') / chunk) + + packets = [ + frame(Scapy_SCTP(sport=9899, dport=38412, tag=0) + / SCTPChunkInit(init_tag=0x11223344, a_rwnd=106496, n_out_streams=10, + n_in_streams=10, init_tsn=0x55667788, + params=[SCTPChunkParamIPv4Addr(addr='10.0.0.1'), + SCTPChunkParamSupportedAddrTypes( + addr_type_list=[5, 6])])), + frame(Scapy_SCTP(sport=9899, dport=38412, tag=0x11223344) + / SCTPChunkData(delay_sack=0, unordered=0, beginning=1, ending=1, + tsn=0x0A0B0C0D, stream_id=1, stream_seq=2, + proto_id=60, data=b'\x00\x15\x00\x08\x00\x00\x04\x00')), + frame(Scapy_SCTP(sport=38412, dport=9899, tag=0x11223344) + / SCTPChunkSACK(cumul_tsn_ack=0x0A0B0C0D, a_rwnd=106496, + gap_ack_list=[(2, 3), (5, 5)], dup_tsn_list=[19, 19])), + frame(Scapy_SCTP(sport=9899, dport=38412, tag=0x11223344) + / SCTPChunkHeartbeatReq( + params=[SCTPChunkParamHeartbeatInfo(data=b'\xca\xfe\xba\xbe')])), + frame(Scapy_SCTP(sport=9899, dport=38412, tag=0x11223344) + / SCTPChunkShutdown(cumul_tsn_ack=0x0A0B0C10)), + frame(Scapy_SCTP(sport=9899, dport=38412, tag=0x11223344) + / SCTPChunkCookieEcho(cookie=b'\xde\xad\xbe\xef\xfe\xed')), + frame(Scapy_SCTP(sport=9899, dport=38412, tag=0x11223344) + / SCTPChunkAbort(TCB=1)), + ] + + handle, path = tempfile.mkstemp(prefix='pcapkit-sctp-', suffix='.pcap', dir='/tmp') + os.close(handle) + self.addCleanup(os.unlink, path) + wrpcap(path, packets) + + # Round-trip the scapy packets through bytes, so that the fields scapy + # computes at build time -- the chunk lengths and the CRC32c checksum -- + # are populated. Comparing against these is comparing against what + # actually went on the wire, not against what we asked for. + packets = [Ether(bytes(packet)) for packet in packets] + + extractor = extract(fin=path, nofile=True, store=True) + self.addCleanup(close_extractor, extractor) + frames = list(extractor.frame) + self.assertEqual(len(frames), len(packets)) + + from pcapkit.protocols.transport.sctp import SCTP + + parsed = [] + for index, got in enumerate(frames): + with self.subTest(frame=index): + self.assertEqual(str(got.protochain).split(':')[:3], + ['Ethernet', 'IPv4', 'SCTP']) + sctp = got[SCTP].info + parsed.append(sctp) + scapy_sctp = packets[index]['SCTP'] + self.assertEqual(sctp.vtag, scapy_sctp.tag) + self.assertEqual(sctp.srcport.port, scapy_sctp.sport) + self.assertEqual(sctp.dstport.port, scapy_sctp.dport) + # scapy computes the CRC32c itself, so validating the checksum + # it wrote is an independent check that our polynomial and byte + # order match a second implementation. + on_the_wire = bytes(scapy_sctp) + self.assertEqual(sctp.chksum, on_the_wire[8:12]) + self.assertTrue(SCTP.validate_checksum(on_the_wire)) + + # INIT, field by field against what scapy was told to emit. + init = parsed[0].chunks[Chunk.Initiation] + scapy_init = packets[0]['SCTPChunkInit'] + self.assertEqual(init.length, scapy_init.len) + self.assertEqual(init.init_tag, scapy_init.init_tag) + self.assertEqual(init.a_rwnd, scapy_init.a_rwnd) + self.assertEqual(init.outbound_streams, scapy_init.n_out_streams) + self.assertEqual(init.inbound_streams, scapy_init.n_in_streams) + self.assertEqual(init.init_tsn, scapy_init.init_tsn) + self.assertEqual(list(init.parameters.keys()), + [Parameter.IPv4_Address, Parameter.Supported_Address_Types]) + self.assertEqual(init.parameters[Parameter.IPv4_Address].address, + ipaddress.IPv4Address(scapy_init.params[0].addr)) + self.assertEqual(init.parameters[Parameter.Supported_Address_Types].types, + tuple(Parameter.get(item) + for item in scapy_init.params[1].addr_type_list)) + + # DATA, field by field. + data = parsed[1].chunks[Chunk.Payload_Data] + scapy_data = packets[1]['SCTPChunkData'] + self.assertEqual(data.length, scapy_data.len) + self.assertEqual(data.tsn, scapy_data.tsn) + self.assertEqual(data.stream_id, scapy_data.stream_id) + self.assertEqual(data.stream_seq, scapy_data.stream_seq) + self.assertEqual(int(data.ppid), scapy_data.proto_id) + self.assertEqual(data.data, scapy_data.data) + self.assertEqual(data.flags.I, bool(scapy_data.delay_sack)) + self.assertEqual(data.flags.U, bool(scapy_data.unordered)) + self.assertEqual(data.flags.B, bool(scapy_data.beginning)) + self.assertEqual(data.flags.E, bool(scapy_data.ending)) + + # SACK, field by field. + sack = parsed[2].chunks[Chunk.Selective_Acknowledgement] + scapy_sack = packets[2]['SCTPChunkSACK'] + self.assertEqual(sack.length, scapy_sack.len) + self.assertEqual(sack.cum_tsn_ack, scapy_sack.cumul_tsn_ack) + self.assertEqual(sack.a_rwnd, scapy_sack.a_rwnd) + self.assertEqual(sack.num_gap_blocks, len(scapy_sack.gap_ack_list)) + self.assertEqual(sack.num_dup_tsn, len(scapy_sack.dup_tsn_list)) + # A re-parsed scapy SACK renders each gap ack block as a ``'start:end'`` + # string rather than as the tuple it was built from. + def gap_pair(item): + if isinstance(item, str): + start, end = item.split(':') + return int(start), int(end) + return tuple(item) + + self.assertEqual([(b.start, b.end) for b in sack.gap_blocks], + [gap_pair(item) for item in scapy_sack.gap_ack_list]) + self.assertEqual(list(sack.dup_tsn), list(scapy_sack.dup_tsn_list)) + + # HEARTBEAT, SHUTDOWN, COOKIE ECHO, ABORT. + heartbeat = parsed[3].chunks[Chunk.Heartbeat_Request] + self.assertEqual(heartbeat.length, packets[3]['SCTPChunkHeartbeatReq'].len) + self.assertEqual(heartbeat.parameters[Parameter.Heartbeat_Info].info, + packets[3]['SCTPChunkParamHeartbeatInfo'].data) + + shutdown = parsed[4].chunks[Chunk.Shutdown] + self.assertEqual(shutdown.length, 8) + self.assertEqual(shutdown.cum_tsn_ack, + packets[4]['SCTPChunkShutdown'].cumul_tsn_ack) + + echo = parsed[5].chunks[Chunk.State_Cookie] + self.assertEqual(echo.length, packets[5]['SCTPChunkCookieEcho'].len) + self.assertEqual(echo.cookie, packets[5]['SCTPChunkCookieEcho'].cookie) + + abort = parsed[6].chunks[Chunk.Abort] + self.assertEqual(abort.length, 4) + self.assertTrue(abort.flags.T) + self.assertEqual(len(abort.error), 0) + self.assertNotIn(CauseCode.Out_of_Resource, abort.error) + + +if __name__ == '__main__': + unittest.main() From 2d67f86dea4baae6e3b09b437e0726bdd824cb39 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Mon, 14 Sep 2026 15:09:07 -0400 Subject: [PATCH 2/2] sctp: add the vendor crawlers, and stop an unregistered PPID mutating state The const modules under pcapkit/const/sctp/ had no pcapkit/vendor/sctp/ counterpart, which every other const package has and the project's rule requires for registry-derived enums. Four crawlers now generate them from the IANA SCTP parameters registry: chunk types (-1), chunk parameter types (-2), error cause codes (-24) and payload protocol identifiers (-25). The committed modules regenerate with no member or value changing. The only differences are docstrings and #: comments, and four of those are corrections: the throwaway script that first produced these files split on every newline rather than on CRLF, which made csv.reader swallow the separator inside IANA's multi-line quoted reference fields and ran words together - "Transport over SCTP",November and "Bearer Independent CallControl protocol" among them. The crawlers' docstrings replace the "maintained manually" wording, which is no longer true, here and in the docs and on the Help Wanted page. Separately, a Copilot finding on the PR: an unregistered Payload Protocol Identifier was blanked to None before dispatch, and __proto__ is a defaultdict, so reading it inserted a None key into class-level state shared by every later SCTP instance and dispatched the payload under alias None. The PPID now reaches the next layer unchanged, as Internet does with an unregistered protocol number, and an _import_next_layer override looks the registry up without mutating it, falling back to the Raw the registry already declares. An unregistered PPID now yields Raw with the payload intact, info.protocol set to the real identifier, and no new registry key. The same defaultdict-read shape affects __chunk__, __parameter__ and __cause__ at six further call sites: those insert the real code rather than None, so dispatch stays right, but the growth is unbounded and it makes a later legitimate register_chunk() warn that the code is already registered. Left for its own change. Full suite: 473 passed, 4 skipped, 249 subtests passed. --- docs/source/pcapkit/const/sctp.rst | 19 +---- docs/source/pcapkit/vendor/index.rst | 1 + docs/source/pcapkit/vendor/sctp.rst | 75 ++++++++++++++++++ docs/source/pep.rst | 6 +- pcapkit/const/sctp/__init__.py | 6 -- pcapkit/const/sctp/cause_code.py | 5 +- pcapkit/const/sctp/chunk.py | 5 +- pcapkit/const/sctp/parameter.py | 5 +- .../const/sctp/payload_protocol_identifier.py | 37 ++++----- pcapkit/protocols/transport/sctp.py | 65 +++++++++++++++- pcapkit/vendor/__init__.py | 4 + pcapkit/vendor/sctp/__init__.py | 37 +++++++++ pcapkit/vendor/sctp/cause_code.py | 29 +++++++ pcapkit/vendor/sctp/chunk.py | 29 +++++++ pcapkit/vendor/sctp/parameter.py | 29 +++++++ .../sctp/payload_protocol_identifier.py | 29 +++++++ tests/protocols/transport/test_sctp_unit.py | 77 ++++++++++++++++++- 17 files changed, 397 insertions(+), 61 deletions(-) create mode 100644 docs/source/pcapkit/vendor/sctp.rst create mode 100644 pcapkit/vendor/sctp/__init__.py create mode 100644 pcapkit/vendor/sctp/cause_code.py create mode 100644 pcapkit/vendor/sctp/chunk.py create mode 100644 pcapkit/vendor/sctp/parameter.py create mode 100644 pcapkit/vendor/sctp/payload_protocol_identifier.py diff --git a/docs/source/pcapkit/const/sctp.rst b/docs/source/pcapkit/const/sctp.rst index cf7e0c05a4..01882a6a03 100644 --- a/docs/source/pcapkit/const/sctp.rst +++ b/docs/source/pcapkit/const/sctp.rst @@ -19,21 +19,13 @@ enumerations include: * - :class:`SCTP_PayloadProtocolIdentifier ` - SCTP Payload Protocol Identifiers [*]_ -.. note:: - - Unlike most constant enumerations of :mod:`pcapkit`, the SCTP enumerations - are **hand-maintained**, as there is no crawler for them under - :mod:`pcapkit.vendor` yet. Should one be added later, it would target the - registries linked above. - SCTP Chunk Types ================ .. module:: pcapkit.const.sctp.chunk This module contains the constant enumeration for **SCTP Chunk Types**, -which is maintained manually against the IANA registry, as there -is currently no vendor crawler for SCTP under :mod:`pcapkit.vendor`. +which is automatically generated from :class:`pcapkit.vendor.sctp.chunk.Chunk`. .. autoclass:: pcapkit.const.sctp.chunk.Chunk :members: @@ -46,8 +38,7 @@ SCTP Chunk Parameter Types .. module:: pcapkit.const.sctp.parameter This module contains the constant enumeration for **SCTP Chunk Parameter Types**, -which is maintained manually against the IANA registry, as there -is currently no vendor crawler for SCTP under :mod:`pcapkit.vendor`. +which is automatically generated from :class:`pcapkit.vendor.sctp.parameter.Parameter`. .. autoclass:: pcapkit.const.sctp.parameter.Parameter :members: @@ -60,8 +51,7 @@ SCTP Error Cause Codes .. module:: pcapkit.const.sctp.cause_code This module contains the constant enumeration for **SCTP Error Cause Codes**, -which is maintained manually against the IANA registry, as there -is currently no vendor crawler for SCTP under :mod:`pcapkit.vendor`. +which is automatically generated from :class:`pcapkit.vendor.sctp.cause_code.CauseCode`. .. autoclass:: pcapkit.const.sctp.cause_code.CauseCode :members: @@ -74,8 +64,7 @@ SCTP Payload Protocol Identifiers .. module:: pcapkit.const.sctp.payload_protocol_identifier This module contains the constant enumeration for **SCTP Payload Protocol Identifiers**, -which is maintained manually against the IANA registry, as there -is currently no vendor crawler for SCTP under :mod:`pcapkit.vendor`. +which is automatically generated from :class:`pcapkit.vendor.sctp.payload_protocol_identifier.PayloadProtocolIdentifier`. .. autoclass:: pcapkit.const.sctp.payload_protocol_identifier.PayloadProtocolIdentifier :members: diff --git a/docs/source/pcapkit/vendor/index.rst b/docs/source/pcapkit/vendor/index.rst index ceadfd265b..56c96e3de4 100644 --- a/docs/source/pcapkit/vendor/index.rst +++ b/docs/source/pcapkit/vendor/index.rst @@ -61,6 +61,7 @@ Transport Layer .. toctree:: :maxdepth: 2 + sctp tcp Application Layer diff --git a/docs/source/pcapkit/vendor/sctp.rst b/docs/source/pcapkit/vendor/sctp.rst new file mode 100644 index 0000000000..063ad77ef4 --- /dev/null +++ b/docs/source/pcapkit/vendor/sctp.rst @@ -0,0 +1,75 @@ +===================================================================== +:class:`~pcapkit.protocols.transport.sctp.SCTP` Vendor Crawlers +===================================================================== + +.. module:: pcapkit.vendor.sctp + +This module contains all vendor crawlers of +:class:`~pcapkit.protocols.transport.sctp.SCTP` implementations. Available +vendor crawlers include: + +.. list-table:: + + * - :class:`SCTP_Chunk ` + - SCTP Chunk Types [*]_ + * - :class:`SCTP_Parameter ` + - SCTP Chunk Parameter Types [*]_ + * - :class:`SCTP_CauseCode ` + - SCTP Error Cause Codes [*]_ + * - :class:`SCTP_PayloadProtocolIdentifier ` + - SCTP Payload Protocol Identifiers [*]_ + +SCTP Chunk Types +================ + +.. module:: pcapkit.vendor.sctp.chunk + +This module contains the vendor crawler for **SCTP Chunk Types**, +which is automatically generating :class:`pcapkit.const.sctp.chunk.Chunk`. + +.. autoclass:: pcapkit.vendor.sctp.chunk.Chunk + :members: FLAG, LINK + :show-inheritance: + +SCTP Chunk Parameter Types +========================== + +.. module:: pcapkit.vendor.sctp.parameter + +This module contains the vendor crawler for **SCTP Chunk Parameter Types**, +which is automatically generating :class:`pcapkit.const.sctp.parameter.Parameter`. + +.. autoclass:: pcapkit.vendor.sctp.parameter.Parameter + :members: FLAG, LINK + :show-inheritance: + +SCTP Error Cause Codes +====================== + +.. module:: pcapkit.vendor.sctp.cause_code + +This module contains the vendor crawler for **SCTP Error Cause Codes**, +which is automatically generating :class:`pcapkit.const.sctp.cause_code.CauseCode`. + +.. autoclass:: pcapkit.vendor.sctp.cause_code.CauseCode + :members: FLAG, LINK + :show-inheritance: + +SCTP Payload Protocol Identifiers +================================= + +.. module:: pcapkit.vendor.sctp.payload_protocol_identifier + +This module contains the vendor crawler for **SCTP Payload Protocol Identifiers**, +which is automatically generating :class:`pcapkit.const.sctp.payload_protocol_identifier.PayloadProtocolIdentifier`. + +.. autoclass:: pcapkit.vendor.sctp.payload_protocol_identifier.PayloadProtocolIdentifier + :members: FLAG, LINK + :show-inheritance: + +.. rubric:: Footnotes + +.. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-1 +.. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-2 +.. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-24 +.. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-25 diff --git a/docs/source/pep.rst b/docs/source/pep.rst index c24c5b50b7..a0fa1f4808 100644 --- a/docs/source/pep.rst +++ b/docs/source/pep.rst @@ -30,10 +30,8 @@ More Protocols, More!!! but not yet implemented fall through to the generic handlers rather than failing the extraction. - Two notes on how it differs from its siblings. Its constant enumerations - under :mod:`pcapkit.const.sctp` are hand-maintained, as there is no - ``pcapkit.vendor.sctp`` crawler yet. And the next layer is dispatched on the - DATA chunk's *payload protocol identifier* through + One note on how it differs from its siblings. The next layer is dispatched + on the DATA chunk's *payload protocol identifier* through :func:`~pcapkit.foundation.registry.protocols.register_sctp`, not on port numbers, so :func:`~pcapkit.foundation.registry.protocols.register_apptype` deliberately does not fan out to it. diff --git a/pcapkit/const/sctp/__init__.py b/pcapkit/const/sctp/__init__.py index fab70701b5..937dcb17fb 100644 --- a/pcapkit/const/sctp/__init__.py +++ b/pcapkit/const/sctp/__init__.py @@ -25,12 +25,6 @@ .. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-24 .. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-25 -Note: - Unlike most constant enumerations of :mod:`pcapkit`, the SCTP enumerations - are **hand-maintained**, as there is no crawler for them under - :mod:`pcapkit.vendor` yet. Should one be added later, it would target the - registries linked above. - """ from pcapkit.const.sctp.cause_code import CauseCode as SCTP_CauseCode diff --git a/pcapkit/const/sctp/cause_code.py b/pcapkit/const/sctp/cause_code.py index 31f4678343..ff0afc3f13 100644 --- a/pcapkit/const/sctp/cause_code.py +++ b/pcapkit/const/sctp/cause_code.py @@ -6,10 +6,7 @@ .. module:: pcapkit.const.sctp.cause_code This module contains the constant enumeration for **SCTP Error Cause Codes**, -which is maintained manually against the `IANA`_ registry, as there -is currently no vendor crawler for SCTP under :mod:`pcapkit.vendor`. - -.. _IANA: https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-24 +which is automatically generated from :class:`pcapkit.vendor.sctp.cause_code.CauseCode`. """ diff --git a/pcapkit/const/sctp/chunk.py b/pcapkit/const/sctp/chunk.py index 1239b551b1..6ea087ae13 100644 --- a/pcapkit/const/sctp/chunk.py +++ b/pcapkit/const/sctp/chunk.py @@ -6,10 +6,7 @@ .. module:: pcapkit.const.sctp.chunk This module contains the constant enumeration for **SCTP Chunk Types**, -which is maintained manually against the `IANA`_ registry, as there -is currently no vendor crawler for SCTP under :mod:`pcapkit.vendor`. - -.. _IANA: https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-1 +which is automatically generated from :class:`pcapkit.vendor.sctp.chunk.Chunk`. """ diff --git a/pcapkit/const/sctp/parameter.py b/pcapkit/const/sctp/parameter.py index 6e061352d0..8b128b8599 100644 --- a/pcapkit/const/sctp/parameter.py +++ b/pcapkit/const/sctp/parameter.py @@ -6,10 +6,7 @@ .. module:: pcapkit.const.sctp.parameter This module contains the constant enumeration for **SCTP Chunk Parameter Types**, -which is maintained manually against the `IANA`_ registry, as there -is currently no vendor crawler for SCTP under :mod:`pcapkit.vendor`. - -.. _IANA: https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-2 +which is automatically generated from :class:`pcapkit.vendor.sctp.parameter.Parameter`. """ diff --git a/pcapkit/const/sctp/payload_protocol_identifier.py b/pcapkit/const/sctp/payload_protocol_identifier.py index b9a784e1d5..a8d162d9e3 100644 --- a/pcapkit/const/sctp/payload_protocol_identifier.py +++ b/pcapkit/const/sctp/payload_protocol_identifier.py @@ -6,10 +6,7 @@ .. module:: pcapkit.const.sctp.payload_protocol_identifier This module contains the constant enumeration for **SCTP Payload Protocol Identifiers**, -which is maintained manually against the `IANA`_ registry, as there -is currently no vendor crawler for SCTP under :mod:`pcapkit.vendor`. - -.. _IANA: https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-25 +which is automatically generated from :class:`pcapkit.vendor.sctp.payload_protocol_identifier.PayloadProtocolIdentifier`. """ @@ -42,13 +39,13 @@ class PayloadProtocolIdentifier(IntEnum): #: V5UA [:rfc:`3807`] V5UA = 6 - #: H.248 [ITU-T Recommendation H.248 Annex H, "Transport over SCTP",November + #: H.248 [ITU-T Recommendation H.248 Annex H, "Transport over SCTP", November #: 2000.] H_248 = 7 - #: BICC/Q.2150.3 [ITU-T Recommendation Q.1902.1, "Bearer Independent - #: CallControl protocol (Capability Set 2): Functional description",July - #: 2001.][ITU-T Recommendation Q.2150.3, "Signalling Transport ConverterOn + #: BICC/Q.2150.3 [ITU-T Recommendation Q.1902.1, "Bearer Independent Call + #: Control protocol (Capability Set 2): Functional description", July + #: 2001.][ITU-T Recommendation Q.2150.3, "Signalling Transport Converter On #: SCTP", to be published.] BICC_Q_2150_3 = 8 @@ -69,8 +66,8 @@ class PayloadProtocolIdentifier(IntEnum): H_323 = 13 #: Q.IPC/Q.2150.3 [ITU-T Recommendation Q.2631.1 "IP Connection Control - #: SignalingProtocol - Capability Set 1", to be published.][ITU-T - #: Recommendation Q.2150.3, "Signalling Transport ConverterOn SCTP", to be + #: Signaling Protocol - Capability Set 1", to be published.][ITU-T + #: Recommendation Q.2150.3, "Signalling Transport Converter On SCTP", to be #: published.] Q_IPC_Q_2150_3 = 14 @@ -215,40 +212,40 @@ class PayloadProtocolIdentifier(IntEnum): #: WebRTC Binary Empty [:rfc:`8831`] WebRTC_Binary_Empty = 57 - #: 3GPP XwAP [ 3GPP TS 36.462][KIMBA DIT ADAMOU Boubacar] + #: 3GPP XwAP [ 3GPP TS 36.462][KIMBA DIT ADAMOU Boubacar] PayloadProtocolIdentifier_3GPP_XwAP = 58 - #: 3GPP Xw-Control Plane [ 3GPP TS 36.462][KIMBA DIT ADAMOU Boubacar] + #: 3GPP Xw-Control Plane [ 3GPP TS 36.462][KIMBA DIT ADAMOU Boubacar] PayloadProtocolIdentifier_3GPP_Xw_Control_Plane = 59 - #: 3GPP NG Application Protocol (NGAP) [ 3GPP TS 38.413][Luis Lopes] + #: 3GPP NG Application Protocol (NGAP) [ 3GPP TS 38.413][Luis Lopes] PayloadProtocolIdentifier_3GPP_NG_Application_Protocol = 60 - #: 3GPP Xn Application Protocol (XnAP) [ 3GPP TS 38.423][Luis Lopes] + #: 3GPP Xn Application Protocol (XnAP) [ 3GPP TS 38.423][Luis Lopes] PayloadProtocolIdentifier_3GPP_Xn_Application_Protocol = 61 - #: 3GPP F1 Application Protocol (F1 AP) [ 3GPP TS 38.473][Luis Lopes] + #: 3GPP F1 Application Protocol (F1 AP) [ 3GPP TS 38.473][Luis Lopes] PayloadProtocolIdentifier_3GPP_F1_Application_Protocol = 62 #: HTTP/SCTP [Michael Tuexen] HTTP_SCTP = 63 - #: 3GPP E1 Application Protocol (E1AP) [ 3GPP TS 38.463][Yang Xudong] + #: 3GPP E1 Application Protocol (E1AP) [ 3GPP TS 38.463][Yang Xudong] PayloadProtocolIdentifier_3GPP_E1_Application_Protocol = 64 #: ELE2 Lawful Interception [http://ele2.io][Damir Franusic] ELE2_Lawful_Interception = 65 - #: 3GPP NGAP over DTLS over SCTP [ 3GPP TS 38.413][Yang Xudong] + #: 3GPP NGAP over DTLS over SCTP [ 3GPP TS 38.413][Yang Xudong] PayloadProtocolIdentifier_3GPP_NGAP_over_DTLS_over_SCTP = 66 - #: 3GPP XnAP over DTLS over SCTP [ 3GPP TS 38.423][Yang Xudong] + #: 3GPP XnAP over DTLS over SCTP [ 3GPP TS 38.423][Yang Xudong] PayloadProtocolIdentifier_3GPP_XnAP_over_DTLS_over_SCTP = 67 - #: 3GPP F1AP over DTLS over SCTP [ 3GPP TS 38.473][Yang Xudong] + #: 3GPP F1AP over DTLS over SCTP [ 3GPP TS 38.473][Yang Xudong] PayloadProtocolIdentifier_3GPP_F1AP_over_DTLS_over_SCTP = 68 - #: 3GPP E1AP over DTLS over SCTP [ 3GPP TS 38.463][Yang Xudong] + #: 3GPP E1AP over DTLS over SCTP [ 3GPP TS 38.463][Yang Xudong] PayloadProtocolIdentifier_3GPP_E1AP_over_DTLS_over_SCTP = 69 #: E2-CP [O-RAN Alliance][Jun Hyuk Song] diff --git a/pcapkit/protocols/transport/sctp.py b/pcapkit/protocols/transport/sctp.py index 5293d5f5fb..5a8b8103ac 100644 --- a/pcapkit/protocols/transport/sctp.py +++ b/pcapkit/protocols/transport/sctp.py @@ -155,6 +155,7 @@ from pcapkit.protocols.schema.transport.sctp import \ UserInitiatedAbortCause as Schema_UserInitiatedAbortCause from pcapkit.protocols.transport.transport import Transport +from pcapkit.utilities.decorators import beholder from pcapkit.utilities.exceptions import ProtocolError, RegistryError from pcapkit.utilities.warnings import RegistryWarning, warn @@ -786,12 +787,72 @@ def _decode_next_layer(self, dict_: 'Data_SCTP', proto: 'Optional[int]' = None, which keys the lookup on port numbers, since SCTP keys it on the DATA chunk's payload protocol identifier instead. + The PPID is passed through **unchanged**, registered or not, so that + an unregistered payload is still labelled with the identifier it + arrived with -- as :meth:`Internet._import_next_layer + ` + does for an unregistered transport type. Resolving it to + :class:`~pcapkit.protocols.misc.raw.Raw` is + :meth:`self._import_next_layer `'s job. + """ - if proto is not None and proto not in self.__proto__: - proto = None return ProtocolBase._decode_next_layer( # pylint: disable=protected-access self, dict_, proto, length, packet=packet) # type: ignore[arg-type,return-value] + @beholder # type: ignore[arg-type] + def _import_next_layer(self, proto: 'int', length: 'Optional[int]' = None, *, + packet: 'Optional[dict[str, Any]]' = None) -> 'Protocol': + """Import next layer extractor. + + Arguments: + proto: payload protocol identifier (PPID) of the DATA chunk carrying + the payload, or :obj:`None` if the packet carries no user data + length: valid (*non-padding*) length + packet: packet info (passed from :meth:`self.unpack `) + + Returns: + Instance of next layer. + + Important: + This overrides :meth:`ProtocolBase._import_next_layer + ` for one + reason only: to look the PPID up **without** mutating + :attr:`self.__proto__ `. + + The registry is a :class:`collections.defaultdict`, so the base + implementation's ``self.__proto__[proto]`` *inserts* any key it is + handed. Every packet with an unregistered PPID would therefore grow + a registry that is class-level -- shared by every :class:`SCTP` + instance in the process -- and make + :meth:`self.register ` report that PPID as already + registered. The fallback itself is unchanged: an unregistered PPID + still resolves to :class:`~pcapkit.protocols.misc.raw.Raw`, which is + what :attr:`self.__proto__ ` declares as its default. + + """ + if TYPE_CHECKING: + protocol: 'Type[Protocol]' + + file_ = self._get_payload() + if length is None: + length = len(file_) + + if length == 0: + from pcapkit.protocols.misc.null import NoPayload as protocol # isort: skip # pylint: disable=import-outside-toplevel + elif self._sigterm: + from pcapkit.protocols.misc.raw import Raw as protocol # isort: skip # pylint: disable=import-outside-toplevel + elif proto in self.__proto__: + protocol = self.__proto__[proto] # type: ignore[assignment] + if isinstance(protocol, ModuleDescriptor): + protocol = protocol.klass # type: ignore[unreachable] + self.__proto__[proto] = protocol # update mapping upon import + else: + from pcapkit.protocols.misc.raw import Raw as protocol # isort: skip # pylint: disable=import-outside-toplevel + + next_ = protocol(file_, length, alias=proto, packet=packet, # type: ignore[abstract] + layer=self._exlayer, protocol=self._exproto) + return next_ + def _read_sctp_chunks(self) -> 'Chunks': """Read SCTP chunk list. diff --git a/pcapkit/vendor/__init__.py b/pcapkit/vendor/__init__.py index 989a314bf9..798044af14 100644 --- a/pcapkit/vendor/__init__.py +++ b/pcapkit/vendor/__init__.py @@ -49,6 +49,7 @@ from pcapkit.vendor.l2tp import * from pcapkit.vendor.mh import * from pcapkit.vendor.ospf import * +from pcapkit.vendor.sctp import * from pcapkit.vendor.tcp import * from pcapkit.vendor.vlan import * @@ -94,6 +95,9 @@ 'MH_CGAExtension', 'MH_CGASec', 'MH_BindingError', # OSPF 'OSPF_Authentication', 'OSPF_Packet', + # SCTP + 'SCTP_Chunk', 'SCTP_Parameter', 'SCTP_CauseCode', + 'SCTP_PayloadProtocolIdentifier', # TCP 'TCP_Checksum', 'TCP_Option', 'TCP_MPTCPOption', 'TCP_Flags', # VLAN diff --git a/pcapkit/vendor/sctp/__init__.py b/pcapkit/vendor/sctp/__init__.py new file mode 100644 index 0000000000..1561c676ce --- /dev/null +++ b/pcapkit/vendor/sctp/__init__.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# pylint: disable=unused-import +""":class:`~pcapkit.protocols.transport.sctp.SCTP` Vendor Crawlers +===================================================================== + +.. module:: pcapkit.vendor.sctp + +This module contains all vendor crawlers of +:class:`~pcapkit.protocols.transport.sctp.SCTP` implementations. Available +enumerations include: + +.. list-table:: + + * - :class:`SCTP_Chunk ` + - SCTP Chunk Types [*]_ + * - :class:`SCTP_Parameter ` + - SCTP Chunk Parameter Types [*]_ + * - :class:`SCTP_CauseCode ` + - SCTP Error Cause Codes [*]_ + * - :class:`SCTP_PayloadProtocolIdentifier ` + - SCTP Payload Protocol Identifiers [*]_ + +.. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-1 +.. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-2 +.. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-24 +.. [*] https://www.iana.org/assignments/sctp-parameters/sctp-parameters.xhtml#sctp-parameters-25 + +""" + +from pcapkit.vendor.sctp.cause_code import CauseCode as SCTP_CauseCode +from pcapkit.vendor.sctp.chunk import Chunk as SCTP_Chunk +from pcapkit.vendor.sctp.parameter import Parameter as SCTP_Parameter +from pcapkit.vendor.sctp.payload_protocol_identifier import \ + PayloadProtocolIdentifier as SCTP_PayloadProtocolIdentifier + +__all__ = ['SCTP_Chunk', 'SCTP_Parameter', 'SCTP_CauseCode', + 'SCTP_PayloadProtocolIdentifier'] diff --git a/pcapkit/vendor/sctp/cause_code.py b/pcapkit/vendor/sctp/cause_code.py new file mode 100644 index 0000000000..c10515079c --- /dev/null +++ b/pcapkit/vendor/sctp/cause_code.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +"""SCTP Error Cause Codes +============================ + +.. module:: pcapkit.vendor.sctp.cause_code + +This module contains the vendor crawler for **SCTP Error Cause Codes**, +which is automatically generating :class:`pcapkit.const.sctp.cause_code.CauseCode`. + +""" + +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['CauseCode'] + + +class CauseCode(Vendor): + """SCTP Error Cause Codes""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 65535' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/sctp-parameters/sctp-parameters-24.csv' + + +if __name__ == '__main__': + sys.exit(CauseCode()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/sctp/chunk.py b/pcapkit/vendor/sctp/chunk.py new file mode 100644 index 0000000000..c3a045b469 --- /dev/null +++ b/pcapkit/vendor/sctp/chunk.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +"""SCTP Chunk Types +====================== + +.. module:: pcapkit.vendor.sctp.chunk + +This module contains the vendor crawler for **SCTP Chunk Types**, +which is automatically generating :class:`pcapkit.const.sctp.chunk.Chunk`. + +""" + +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['Chunk'] + + +class Chunk(Vendor): + """SCTP Chunk Types""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 255' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/sctp-parameters/sctp-parameters-1.csv' + + +if __name__ == '__main__': + sys.exit(Chunk()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/sctp/parameter.py b/pcapkit/vendor/sctp/parameter.py new file mode 100644 index 0000000000..c2fa6db590 --- /dev/null +++ b/pcapkit/vendor/sctp/parameter.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +"""SCTP Chunk Parameter Types +================================ + +.. module:: pcapkit.vendor.sctp.parameter + +This module contains the vendor crawler for **SCTP Chunk Parameter Types**, +which is automatically generating :class:`pcapkit.const.sctp.parameter.Parameter`. + +""" + +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['Parameter'] + + +class Parameter(Vendor): + """SCTP Chunk Parameter Types""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 65535' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/sctp-parameters/sctp-parameters-2.csv' + + +if __name__ == '__main__': + sys.exit(Parameter()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/sctp/payload_protocol_identifier.py b/pcapkit/vendor/sctp/payload_protocol_identifier.py new file mode 100644 index 0000000000..bc1a74c38b --- /dev/null +++ b/pcapkit/vendor/sctp/payload_protocol_identifier.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +"""SCTP Payload Protocol Identifiers +======================================= + +.. module:: pcapkit.vendor.sctp.payload_protocol_identifier + +This module contains the vendor crawler for **SCTP Payload Protocol Identifiers**, +which is automatically generating :class:`pcapkit.const.sctp.payload_protocol_identifier.PayloadProtocolIdentifier`. + +""" + +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['PayloadProtocolIdentifier'] + + +class PayloadProtocolIdentifier(Vendor): + """SCTP Payload Protocol Identifiers""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 4294967295' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/sctp-parameters/sctp-parameters-25.csv' + + +if __name__ == '__main__': + sys.exit(PayloadProtocolIdentifier()) # type: ignore[arg-type] diff --git a/tests/protocols/transport/test_sctp_unit.py b/tests/protocols/transport/test_sctp_unit.py index 40714d78a5..1ed07a51f1 100644 --- a/tests/protocols/transport/test_sctp_unit.py +++ b/tests/protocols/transport/test_sctp_unit.py @@ -844,13 +844,17 @@ def test_ppid_dispatch_hook(self) -> None: dict(I=True, U=True, B=True, E=True, tsn=1, ppid=NGAP_PPID, data=b'ngap-pdu'))]) - # With nothing registered on the PPID, the payload is raw. + # With nothing registered on the PPID, the payload is raw -- but it is + # still named after the PPID it arrived with, the way an unregistered + # transport type is (``IPv4:Use_for_experimentation_and_testing_253``), + # rather than being anonymised to a bare ``Raw``. self.assertNotIn(NGAP_PPID, SCTP.__proto__) proto = self._packet(raw) self.assertEqual(proto.ppid, NGAP_PPID) self.assertIsInstance(proto.payload, Raw) self.assertEqual(bytes(proto.payload), b'ngap-pdu') - self.assertEqual(str(proto.protochain), 'SCTP:Raw') + self.assertEqual(str(proto.protochain), + 'SCTP:PayloadProtocolIdentifier_3GPP_NG_Application_Protocol') # This is exactly the call a future NGAP class makes. try: @@ -864,6 +868,75 @@ def test_ppid_dispatch_hook(self) -> None: finally: SCTP.__proto__.clear() + def test_unregistered_ppid_does_not_mutate_the_class_registry(self) -> None: + """An unregistered PPID reaches :class:`Raw` without touching ``__proto__``. + + :attr:`SCTP.__proto__ ` + is a :class:`collections.defaultdict`, so *reading* a missing key + inserts it. Dispatching an unregistered PPID through that read mutates + class-level state shared by every later :class:`SCTP` instance in the + process, and it makes + :meth:`SCTP.register ` + subsequently claim the PPID was "already registered". + + The symptom is invisible unless looked for, hence the before/after + comparison of the registry's keys and the second instance: a leak is + class-level, so it shows up on the *next* packet rather than this one. + + """ + from pcapkit.const.sctp.chunk import Chunk + from pcapkit.const.sctp.payload_protocol_identifier import PayloadProtocolIdentifier + from pcapkit.protocols.misc.raw import Raw + from pcapkit.protocols.transport import sctp as sctp_module + from pcapkit.protocols.transport.sctp import SCTP + + # 4243 is the first code in the trailing "Unassigned" block of the IANA + # registry, i.e. a PPID that is well-formed but cannot be registered. + UNREGISTERED = 4243 + self.assertEqual(PayloadProtocolIdentifier(UNREGISTERED).name, 'Unassigned_4243') + + raw = self._build([(Chunk.Payload_Data, + dict(I=True, U=True, B=True, E=True, tsn=1, + ppid=UNREGISTERED, data=b'unknown-pdu'))]) + + try: + before = set(SCTP.__proto__) + self.assertNotIn(UNREGISTERED, before) + + first = self._packet(raw) + + # (a) The payload is still reachable, as Raw, and is still labelled + # with its real PPID rather than being anonymised to ``None``. + # This is how Internet.__proto__ names an unregistered code -- + # cf. ``IPv4:Use_for_experimentation_and_testing_253``. + self.assertIsInstance(first.payload, Raw) + self.assertEqual(bytes(first.payload), b'unknown-pdu') + self.assertEqual(int(first.ppid), UNREGISTERED) + self.assertEqual(first.payload.info.protocol, UNREGISTERED) + self.assertEqual(str(first.protochain), 'SCTP:Unassigned_4243') + + # (b) The class-level registry gained nothing: no ``None`` key, and + # no key for the unregistered PPID either. + self.assertNotIn(None, SCTP.__proto__) + self.assertNotIn(UNREGISTERED, SCTP.__proto__) + self.assertEqual(set(SCTP.__proto__), before) + + # A second instance in the same process must see the same registry + # and behave identically -- class-level leakage would show here. + second = self._packet(raw) + self.assertIsInstance(second.payload, Raw) + self.assertEqual(bytes(second.payload), b'unknown-pdu') + self.assertEqual(str(second.protochain), 'SCTP:Unassigned_4243') + self.assertEqual(set(SCTP.__proto__), before) + + # And the PPID is still registrable without a bogus overwrite + # warning, which a leaked key would have triggered. + with mock.patch.object(sctp_module, 'warn') as warned: + SCTP.register(UNREGISTERED, Raw) + self.assertEqual(warned.call_count, 0) + finally: + SCTP.__proto__.clear() + def test_packet_without_a_data_chunk_has_no_payload(self) -> None: from pcapkit.const.sctp.chunk import Chunk from pcapkit.protocols.misc.null import NoPayload