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..01882a6a03 --- /dev/null +++ b/docs/source/pcapkit/const/sctp.rst @@ -0,0 +1,79 @@ +===================================================================== +: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 [*]_ + +SCTP Chunk Types +================ + +.. module:: pcapkit.const.sctp.chunk + +This module contains the constant enumeration for **SCTP Chunk Types**, +which is automatically generated from :class:`pcapkit.vendor.sctp.chunk.Chunk`. + +.. 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 automatically generated from :class:`pcapkit.vendor.sctp.parameter.Parameter`. + +.. 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 automatically generated from :class:`pcapkit.vendor.sctp.cause_code.CauseCode`. + +.. 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 automatically generated from :class:`pcapkit.vendor.sctp.payload_protocol_identifier.PayloadProtocolIdentifier`. + +.. 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/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 44308d152f..a0fa1f4808 100644 --- a/docs/source/pep.rst +++ b/docs/source/pep.rst @@ -21,6 +21,21 @@ 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. + + 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. + 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 +43,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..937dcb17fb --- /dev/null +++ b/pcapkit/const/sctp/__init__.py @@ -0,0 +1,37 @@ +# -*- 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 + +""" + +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..ff0afc3f13 --- /dev/null +++ b/pcapkit/const/sctp/cause_code.py @@ -0,0 +1,132 @@ +# -*- 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 automatically generated from :class:`pcapkit.vendor.sctp.cause_code.CauseCode`. + +""" + +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..6ea087ae13 --- /dev/null +++ b/pcapkit/const/sctp/chunk.py @@ -0,0 +1,150 @@ +# -*- 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 automatically generated from :class:`pcapkit.vendor.sctp.chunk.Chunk`. + +""" + +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..8b128b8599 --- /dev/null +++ b/pcapkit/const/sctp/parameter.py @@ -0,0 +1,156 @@ +# -*- 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 automatically generated from :class:`pcapkit.vendor.sctp.parameter.Parameter`. + +""" + +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..a8d162d9e3 --- /dev/null +++ b/pcapkit/const/sctp/payload_protocol_identifier.py @@ -0,0 +1,299 @@ +# -*- 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 automatically generated from :class:`pcapkit.vendor.sctp.payload_protocol_identifier.PayloadProtocolIdentifier`. + +""" + +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 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 + + #: 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 + #: 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 + + #: 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..5a8b8103ac --- /dev/null +++ b/pcapkit/protocols/transport/sctp.py @@ -0,0 +1,3520 @@ +# -*- 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.decorators import beholder +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. + + 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. + + """ + 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. + + 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/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 new file mode 100644 index 0000000000..1ed07a51f1 --- /dev/null +++ b/tests/protocols/transport/test_sctp_unit.py @@ -0,0 +1,1219 @@ +"""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 -- 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:PayloadProtocolIdentifier_3GPP_NG_Application_Protocol') + + # 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_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 + + 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()