diff --git a/docs/source/contributing/pep.rst b/docs/source/contributing/pep.rst index f3619484b7..ddaa2b4e47 100644 --- a/docs/source/contributing/pep.rst +++ b/docs/source/contributing/pep.rst @@ -42,7 +42,7 @@ the ones outstanding are, * Link Layer: DSL, EAPOL, FDDI, ISDN, NDP, PPP * Internet Layer: ECN, Shim6 -* Transport Layer: DCCP, RSVP +* Transport Layer: RSVP * Application Layer: BGP, DHCP, DHCPv6, DNS, IMAP, LDAP, MQTT, NNTP, NTP, ONC/RPC, POP, QUIC, RIP, RTP, SIP, SMTP, SNMP, SSH, Telnet, XMPP @@ -818,12 +818,13 @@ These protocols parse a checksum or CRC field — :class:`~pcapkit.protocols.internet.ipx.IPX`, :class:`~pcapkit.protocols.internet.mh.MH`, :class:`~pcapkit.protocols.application.ospf.OSPF`, +:class:`~pcapkit.protocols.transport.dccp.DCCP`, :class:`~pcapkit.protocols.transport.sctp.SCTP`, :class:`~pcapkit.protocols.transport.tcp.TCP` and :class:`~pcapkit.protocols.transport.udp.UDP` — and SCTP was the first to check whether the value is *right*, through :attr:`SCTP.checksum_valid `. -IPv4, OSPF, TCP and UDP check it too, as do ICMP and ICMPv6, as the +IPv4, OSPF, TCP, UDP and DCCP check it too, as do ICMP and ICMPv6, as the one's-complement half below describes. For HIP, HOPOPT, IPv6-Opts, IPX and MH the field is recorded and never questioned, so a corrupted capture parses as cleanly as an intact one. @@ -832,9 +833,9 @@ as an intact one. of them is cryptographic and the distinction decides what is actually hard. **The one's-complement Internet checksum**, which covers IPv4's header, TCP, UDP, -ICMP/ICMPv6 and OSPF, needs no cryptography at all — it is sixteen-bit addition +DCCP, ICMP/ICMPv6 and OSPF, needs no cryptography at all — it is sixteen-bit addition with end-around carry, and implementing it is an afternoon. What blocks it is the -**IP pseudo-header**: the TCP, UDP and ICMPv6 checksums are computed over source +**IP pseudo-header**: the TCP, UDP, DCCP and ICMPv6 checksums are computed over source and destination addresses, the protocol number and the payload length, all of which live in the *enclosing* layer. So verification needs a parsed protocol to reach back to its parent, which is a structural question about @@ -924,6 +925,20 @@ offload heuristic on :issue:`2470`. checksum covers, so it is verified when the fragment holds that much, and :attr:`~pcapkit.corekit.checksum.internet.ChecksumStatus.TRUNCATED` otherwise. +* **DCCP covers what its Checksum Coverage gives** (:rfc:`4340#section-9.2`), over + a pseudo-header that carries the whole packet's length, as TCP's does, so a + fragment is + :attr:`~pcapkit.corekit.checksum.internet.ChecksumStatus.NO_PSEUDO_HEADER` + too. A coverage running past the packet, which the RFC forbids and has the + receiver ignore, is + :attr:`~pcapkit.corekit.checksum.internet.ChecksumStatus.INVALID` and + warns. Linux, until it dropped DCCP in 6.16, never sent one: its + ``dccp_csum_outgoing`` wrote a CsCov of 0 wherever the coverage reached the + end of the packet, and ``make()`` does the same when it computes the + checksum. No value is + :attr:`~pcapkit.corekit.checksum.internet.ChecksumStatus.NOT_COMPUTED`: + DCCP has no checksum to leave out, and Linux computed it in software rather + than offloading it. Settled at implementation (:issue:`2489`). The cryptographic half is sequenced for **wave 2 or 3**, and could be done sooner since ESP has already laid the groundwork. @@ -1123,9 +1138,15 @@ Steps`_ above. **Wave 2 — the protocol pairs that want a shared abstract base**, per `More Protocols, More!!!`_ above: **TLS/SSL with DTLS** and **IGMP** and **DCCP** on their own. These are grouped because the shared-base question is the design work -and doing either half of a pair alone would answer it twice. **DCCP** is the -transport-layer implementation, tracked by :issue:`2489`, and blocked on -:issue:`2479` for its pseudo-header checksum. +and doing either half of a pair alone would answer it twice. + +**DCCP — done within wave 2** (:issue:`2489`). +:class:`~pcapkit.protocols.transport.dccp.DCCP` dissects every :rfc:`4340` packet +type and DCCP-Listen (:rfc:`5596`), with short and long sequence numbers, and the +options :rfc:`4340` defines; its checksum is verified and computed over +:class:`~pcapkit.protocols.misc.checksum.ip_pseudo.IP_Pseudo`, as `Checksum and +Integrity Verification`_ above describes. The Data Checksum option's CRC-32c is +recorded and not checked. **Done within wave 2 — ICMP/ICMPv6 shared base.** :class:`~pcapkit.protocols.internet.icmp.ICMP` is the abstract base :class:`~pcapkit.protocols.internet.icmpv4.ICMPv4` and @@ -1311,7 +1332,7 @@ and its signature parameters could start whenever, since :class:`~pcapkit.protocols.internet.esp.ESP` has already established both the keying-context channel and the report-rather-than-raise policy. The one's-complement half (IPv4, TCP, UDP, ICMP/ICMPv6, OSPF) is **done** -(:issue:`2470`, :issue:`2472` to :issue:`2475`). +(:issue:`2470`, :issue:`2472` to :issue:`2475`), and DCCP's with it (:issue:`2489`). **IP pseudo-header — done** (:issue:`2471`). :class:`~pcapkit.protocols.misc.checksum.ip_pseudo.IP_Pseudo` is the diff --git a/docs/source/ext.rst b/docs/source/ext.rst index c77920ca5e..91b82d078b 100644 --- a/docs/source/ext.rst +++ b/docs/source/ext.rst @@ -91,6 +91,8 @@ The protocol classes :mod:`pcapkit` ships: | subclasses) | :class:`pcapkit.protocols.transport.udp.UDP` | + +----------------+-----------------------+-------------------------------------------------------------+ | | :class:`pcapkit.protocols.transport.sctp.SCTP` | ++ +----------------+-----------------------+-------------------------------------------------------------+ +| | :class:`pcapkit.protocols.transport.dccp.DCCP` | +------------------------------------------------------------------+----------------+-----------------------+-------------------------------------------------------------+ | | | :class:`pcapkit.protocols.application.ftp.FTP` | + + FTP Family +-----------------------+-------------------------------------------------------------+ @@ -194,6 +196,8 @@ The registry function per protocol type: | | | :func:`~pcapkit.foundation.registry.protocols.register_udp` | new protocols. | + +-------------------------------------------------------------------+----------------------------------------------------------------+ + | | | :func:`~pcapkit.foundation.registry.protocols.register_sctp` | | ++ +-------------------------------------------------------------------+----------------------------------------------------------------+ + +| | | :func:`~pcapkit.foundation.registry.protocols.register_dccp` | | +-------------------+-------------------------------------------------------------------+----------------------------------------------------------------+--------------------+ A protocol carried over IPX is keyed by IPX Packet Type rather than by IP protocol @@ -405,6 +409,8 @@ writes them: | | :class:`~pcapkit.protocols.transport.sctp.SCTP` | :attr:`SCTP.__parameter__ ` | :func:`~pcapkit.foundation.registry.protocols.register_sctp_parameter` | + + +-----------------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ | | | :attr:`SCTP.__cause__ ` | :func:`~pcapkit.foundation.registry.protocols.register_sctp_cause` | ++ +------------------------------------------------------------+-----------------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ +| | :class:`~pcapkit.protocols.transport.dccp.DCCP` | :attr:`DCCP.__option__ ` | :func:`~pcapkit.foundation.registry.protocols.register_dccp_option` | +-------------------+------------------------------------------------------------+-----------------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ | Application Layer | :class:`~pcapkit.protocols.application.httpv2.HTTPv2` | :attr:`HTTPv2.__frame__ ` | :func:`~pcapkit.foundation.registry.protocols.register_http_frame` | +-------------------+------------------------------------------------------------+-----------------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ diff --git a/docs/source/pcapkit/const/dccp.rst b/docs/source/pcapkit/const/dccp.rst new file mode 100644 index 0000000000..5f0d20ed6f --- /dev/null +++ b/docs/source/pcapkit/const/dccp.rst @@ -0,0 +1,111 @@ +===================================================================== +:class:`~pcapkit.protocols.transport.dccp.DCCP` Constant Enumerations +===================================================================== + +.. module:: pcapkit.const.dccp + +This module contains all constant enumerations of +:class:`~pcapkit.protocols.transport.dccp.DCCP` implementations. Available +enumerations include: + +.. list-table:: + + * - :class:`DCCP_PacketType ` + - DCCP Packet Types [*]_ + * - :class:`DCCP_ResetCode ` + - DCCP Reset Codes [*]_ + * - :class:`DCCP_Option ` + - DCCP Option Types [*]_ + * - :class:`DCCP_Feature ` + - DCCP Feature Numbers [*]_ + * - :class:`DCCP_AckVectorState ` + - DCCP Ack Vector States [*]_ + * - :class:`DCCP_DropCode ` + - DCCP Drop Codes [*]_ + +DCCP Packet Types +================= + +.. module:: pcapkit.const.dccp.packet_type + +This module contains the constant enumeration for **DCCP Packet Types**, +which is automatically generated from :class:`pcapkit.vendor.dccp.packet_type.PacketType`. + +.. autoclass:: pcapkit.const.dccp.packet_type.PacketType + :members: + :undoc-members: + :show-inheritance: + +DCCP Reset Codes +================ + +.. module:: pcapkit.const.dccp.reset_code + +This module contains the constant enumeration for **DCCP Reset Codes**, +which is automatically generated from :class:`pcapkit.vendor.dccp.reset_code.ResetCode`. + +.. autoclass:: pcapkit.const.dccp.reset_code.ResetCode + :members: + :undoc-members: + :show-inheritance: + +DCCP Option Types +================= + +.. module:: pcapkit.const.dccp.option + +This module contains the constant enumeration for **DCCP Option Types**, +which is automatically generated from :class:`pcapkit.vendor.dccp.option.Option`. + +.. autoclass:: pcapkit.const.dccp.option.Option + :members: + :undoc-members: + :show-inheritance: + +DCCP Feature Numbers +==================== + +.. module:: pcapkit.const.dccp.feature + +This module contains the constant enumeration for **DCCP Feature Numbers**, +which is automatically generated from :class:`pcapkit.vendor.dccp.feature.Feature`. + +.. autoclass:: pcapkit.const.dccp.feature.Feature + :members: + :undoc-members: + :show-inheritance: + +DCCP Ack Vector States +====================== + +.. module:: pcapkit.const.dccp.ack_vector_state + +This module contains the constant enumeration for **DCCP Ack Vector States**, +which is automatically generated from :class:`pcapkit.vendor.dccp.ack_vector_state.AckVectorState`. + +.. autoclass:: pcapkit.const.dccp.ack_vector_state.AckVectorState + :members: + :undoc-members: + :show-inheritance: + +DCCP Drop Codes +=============== + +.. module:: pcapkit.const.dccp.drop_code + +This module contains the constant enumeration for **DCCP Drop Codes**, +which is automatically generated from :class:`pcapkit.vendor.dccp.drop_code.DropCode`. + +.. autoclass:: pcapkit.const.dccp.drop_code.DropCode + :members: + :undoc-members: + :show-inheritance: + +.. rubric:: Footnotes + +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-1 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-3 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-5 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-7 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-10 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-11 diff --git a/docs/source/pcapkit/const/index.rst b/docs/source/pcapkit/const/index.rst index 2535c35fdd..fa0a275647 100644 --- a/docs/source/pcapkit/const/index.rst +++ b/docs/source/pcapkit/const/index.rst @@ -95,6 +95,7 @@ Transport Layer .. toctree:: :maxdepth: 2 + dccp sctp tcp diff --git a/docs/source/pcapkit/foundation/registry.rst b/docs/source/pcapkit/foundation/registry.rst index a002428874..4d47b51b0a 100644 --- a/docs/source/pcapkit/foundation/registry.rst +++ b/docs/source/pcapkit/foundation/registry.rst @@ -137,6 +137,10 @@ Transport Layer Registries .. autofunction:: pcapkit.foundation.registry.protocols.register_sctp_cause +.. autofunction:: pcapkit.foundation.registry.protocols.register_dccp + +.. autofunction:: pcapkit.foundation.registry.protocols.register_dccp_option + Application Layer Registries ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/source/pcapkit/protocols/index.rst b/docs/source/pcapkit/protocols/index.rst index 080bb356eb..7be34c09b8 100644 --- a/docs/source/pcapkit/protocols/index.rst +++ b/docs/source/pcapkit/protocols/index.rst @@ -111,7 +111,7 @@ from given information. The class hierarchy of :mod:`pcapkit.protocols`: end subgraph transport [Transport Layer] - Transport --> TCP & UDP & SCTP + Transport --> TCP & UDP & SCTP & DCCP end B --> Link & Internet & Transport & Application @@ -171,6 +171,7 @@ from given information. The class hierarchy of :mod:`pcapkit.protocols`: click TCP "transport/tcp.html#pcapkit.protocols.transport.tcp.TCP" click UDP "transport/udp.html#pcapkit.protocols.transport.udp.UDP" click SCTP "transport/sctp.html#pcapkit.protocols.transport.sctp.SCTP" + click DCCP "transport/dccp.html#pcapkit.protocols.transport.dccp.DCCP" click Application "application/application.html#pcapkit.protocols.application.application.Application" click HTTP "application/http.html#pcapkit.protocols.application.http.HTTP" diff --git a/docs/source/pcapkit/protocols/transport/dccp.rst b/docs/source/pcapkit/protocols/transport/dccp.rst new file mode 100644 index 0000000000..8f52fd7cef --- /dev/null +++ b/docs/source/pcapkit/protocols/transport/dccp.rst @@ -0,0 +1,263 @@ +DCCP - Datagram Congestion Control Protocol +=========================================== + +.. module:: pcapkit.protocols.transport.dccp + +:mod:`pcapkit.protocols.transport.dccp` contains +:class:`~pcapkit.protocols.transport.dccp.DCCP` only, +which implements the extractor for Datagram Congestion +Control Protocol (DCCP) [*]_, whose generic header is +described as below: + +======= ========= ========================= ======================================= +Octets Bits Name Description +======= ========= ========================= ======================================= + 0 0 ``dccp.srcport`` Source Port + 2 16 ``dccp.dstport`` Destination Port + 4 32 ``dccp.hdr_len`` Data Offset + 5 40 ``dccp.ccval`` HC-Sender CCID Value (CCVal) + 5 44 ``dccp.cscov`` Checksum Coverage (CsCov) + 6 48 ``dccp.checksum`` Checksum + 8 64 ``dccp.reserved`` Reserved (Res) + 8 67 ``dccp.type`` Packet Type + 8 71 ``dccp.x`` Extended Sequence Numbers (X) + 9 72 ``dccp.seq_reserved`` Reserved (if X set) + 10 80 ``dccp.seq`` Sequence Number (48 bits if X set) +======= ========= ========================= ======================================= + +With ``X`` clear, the Sequence Number is 24 bits at octet 9 and the generic +header 12 octets long. The fields of the packet type, then the options, follow. + +.. versionadded:: 1.6.0 + +.. autoclass:: pcapkit.protocols.transport.dccp.DCCP + :no-members: + :show-inheritance: + + .. autoproperty:: name + .. autoproperty:: length + .. autoproperty:: src + .. autoproperty:: dst + + .. automethod:: register_option + + .. automethod:: read + .. automethod:: make + + .. automethod:: _make_data + .. automethod:: _verify_pseudo_checksum + + .. automethod:: _read_dccp_options + .. automethod:: _read_opt_unassigned + .. automethod:: _read_opt_padding + .. automethod:: _read_opt_mandatory + .. automethod:: _read_opt_slow_receiver + .. automethod:: _read_opt_change + .. automethod:: _read_opt_confirm + .. automethod:: _read_opt_init_cookie + .. automethod:: _read_opt_ndp_count + .. automethod:: _read_opt_ack_vector + .. automethod:: _read_opt_data_dropped + .. automethod:: _read_opt_timestamp + .. automethod:: _read_opt_timestamp_echo + .. automethod:: _read_opt_elapsed_time + .. automethod:: _read_opt_data_checksum + + .. automethod:: _make_dccp_options + .. automethod:: _make_opt_unassigned + .. automethod:: _make_opt_padding + .. automethod:: _make_opt_mandatory + .. automethod:: _make_opt_slow_receiver + .. automethod:: _make_opt_change + .. automethod:: _make_opt_confirm + .. automethod:: _make_opt_init_cookie + .. automethod:: _make_opt_ndp_count + .. automethod:: _make_opt_ack_vector + .. automethod:: _make_opt_data_dropped + .. automethod:: _make_opt_timestamp + .. automethod:: _make_opt_timestamp_echo + .. automethod:: _make_opt_elapsed_time + .. automethod:: _make_opt_data_checksum + + .. autoattribute:: __proto__ + :no-value: + .. autoattribute:: __proto_key__ + :no-value: + .. autoattribute:: __option__ + :no-value: + + .. automethod:: __index__ + +.. autodata:: pcapkit.protocols.transport.dccp.DCCP_MAX_HEADER_LEN + +Header Schemas +-------------- + +.. module:: pcapkit.protocols.schema.transport.dccp + +.. autodata:: pcapkit.protocols.schema.transport.dccp.DCCP_MIN_HEADER_LEN + +.. autodata:: pcapkit.protocols.schema.transport.dccp.DCCP_SINGLE_OCTET_OPTION_MAX + +.. autodata:: pcapkit.protocols.schema.transport.dccp.ACK_TYPES + :no-value: + +.. autodata:: pcapkit.protocols.schema.transport.dccp.SERVICE_TYPES + :no-value: + +.. autodata:: pcapkit.protocols.schema.transport.dccp.RESET_TYPE + +.. autofunction:: pcapkit.protocols.schema.transport.dccp.header_length + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.DCCP + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.Coverage + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.TypeField + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.Option + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.UnassignedOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.Padding + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.Mandatory + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.SlowReceiver + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.Change + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.Confirm + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.InitCookie + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.NDPCount + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.AckVector + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.DataDropped + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.Timestamp + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.TimestampEcho + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.ElapsedTime + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.schema.transport.dccp.DataChecksum + :members: + :show-inheritance: + +Data Models +----------- + +.. module:: pcapkit.protocols.data.transport.dccp + +.. autoclass:: pcapkit.protocols.data.transport.dccp.DCCP + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.dccp.Option + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.dccp.UnassignedOption + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.dccp.Padding + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.dccp.Mandatory + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.dccp.SlowReceiver + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.dccp.Change + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.dccp.Confirm + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.dccp.InitCookie + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.dccp.NDPCount + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.dccp.AckVector + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.dccp.AckVectorRun + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.dccp.DataDropped + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.dccp.DataDroppedBlock + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.dccp.Timestamp + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.dccp.TimestampEcho + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.dccp.ElapsedTime + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.transport.dccp.DataChecksum + :members: + :show-inheritance: + +.. rubric:: Footnotes + +.. [*] https://en.wikipedia.org/wiki/Datagram_Congestion_Control_Protocol diff --git a/docs/source/pcapkit/protocols/transport/index.rst b/docs/source/pcapkit/protocols/transport/index.rst index 24328fca2d..4665b35605 100644 --- a/docs/source/pcapkit/protocols/transport/index.rst +++ b/docs/source/pcapkit/protocols/transport/index.rst @@ -12,13 +12,14 @@ transport layer, with detailed implementation and methods. :maxdepth: 1 transport + dccp sctp tcp udp .. todo:: - Implement DCCP, RSVP. + Implement RSVP. Protocol Registry ----------------- diff --git a/docs/source/pcapkit/vendor/dccp.rst b/docs/source/pcapkit/vendor/dccp.rst new file mode 100644 index 0000000000..bfdaeebe18 --- /dev/null +++ b/docs/source/pcapkit/vendor/dccp.rst @@ -0,0 +1,105 @@ +=============================================================== +:class:`~pcapkit.protocols.transport.dccp.DCCP` Vendor Crawlers +=============================================================== + +.. module:: pcapkit.vendor.dccp + +This module contains all vendor crawlers of +:class:`~pcapkit.protocols.transport.dccp.DCCP` implementations. Available +vendor crawlers include: + +.. list-table:: + + * - :class:`DCCP_PacketType ` + - DCCP Packet Types [*]_ + * - :class:`DCCP_ResetCode ` + - DCCP Reset Codes [*]_ + * - :class:`DCCP_Option ` + - DCCP Option Types [*]_ + * - :class:`DCCP_Feature ` + - DCCP Feature Numbers [*]_ + * - :class:`DCCP_AckVectorState ` + - DCCP Ack Vector States [*]_ + * - :class:`DCCP_DropCode ` + - DCCP Drop Codes [*]_ + +DCCP Packet Types +================= + +.. module:: pcapkit.vendor.dccp.packet_type + +This module contains the vendor crawler for **DCCP Packet Types**, +which is automatically generating :class:`pcapkit.const.dccp.packet_type.PacketType`. + +.. autoclass:: pcapkit.vendor.dccp.packet_type.PacketType + :members: FLAG, LINK + :show-inheritance: + +DCCP Reset Codes +================ + +.. module:: pcapkit.vendor.dccp.reset_code + +This module contains the vendor crawler for **DCCP Reset Codes**, +which is automatically generating :class:`pcapkit.const.dccp.reset_code.ResetCode`. + +.. autoclass:: pcapkit.vendor.dccp.reset_code.ResetCode + :members: FLAG, LINK + :show-inheritance: + +DCCP Option Types +================= + +.. module:: pcapkit.vendor.dccp.option + +This module contains the vendor crawler for **DCCP Option Types**, +which is automatically generating :class:`pcapkit.const.dccp.option.Option`. + +.. autoclass:: pcapkit.vendor.dccp.option.Option + :members: FLAG, LINK + :show-inheritance: + +DCCP Feature Numbers +==================== + +.. module:: pcapkit.vendor.dccp.feature + +This module contains the vendor crawler for **DCCP Feature Numbers**, +which is automatically generating :class:`pcapkit.const.dccp.feature.Feature`. + +.. autoclass:: pcapkit.vendor.dccp.feature.Feature + :members: FLAG, LINK + :show-inheritance: + +DCCP Ack Vector States +====================== + +.. module:: pcapkit.vendor.dccp.ack_vector_state + +This module contains the vendor crawler for **DCCP Ack Vector States**, +which is automatically generating :class:`pcapkit.const.dccp.ack_vector_state.AckVectorState`. + +.. autoclass:: pcapkit.vendor.dccp.ack_vector_state.AckVectorState + :members: FLAG, LINK + :show-inheritance: + +DCCP Drop Codes +=============== + +.. module:: pcapkit.vendor.dccp.drop_code + +This module contains the vendor crawler for **DCCP Drop Codes**, +which is automatically generating :class:`pcapkit.const.dccp.drop_code.DropCode`. + +.. autoclass:: pcapkit.vendor.dccp.drop_code.DropCode + :members: FLAG, LINK + :show-inheritance: + +.. rubric:: Footnotes + +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-1 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-3 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-5 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-7 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-10 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-11 diff --git a/docs/source/pcapkit/vendor/index.rst b/docs/source/pcapkit/vendor/index.rst index f5ec23c922..14eb04673f 100644 --- a/docs/source/pcapkit/vendor/index.rst +++ b/docs/source/pcapkit/vendor/index.rst @@ -70,6 +70,7 @@ Transport Layer .. toctree:: :maxdepth: 2 + dccp sctp tcp diff --git a/examples/generators/dispatch.py b/examples/generators/dispatch.py index 7136e968fd..5a752ded91 100644 --- a/examples/generators/dispatch.py +++ b/examples/generators/dispatch.py @@ -380,6 +380,9 @@ def _internet_payload(code: 'int') -> 'bytes': return _tcp(9999) if code == TransType.UDP: return _udp(9999) + if code == TransType.DCCP: + from pcapkit.protocols.transport.dccp import DCCP + return bytes(DCCP(srcport=50000, dstport=9999, seq=1, payload=b'\x01\x02\x03')) if code == TransType.IPv6: return _ip6(6, _tcp(9999)) if code == TransType.IPv6_Route: @@ -695,6 +698,7 @@ def _link_enum() -> 'Any': 'internet/ICMP': ('pcapkit.protocols.internet.icmpv4', 'ICMPv4'), 'internet/TCP': ('pcapkit.protocols.transport.tcp', 'TCP'), 'internet/UDP': ('pcapkit.protocols.transport.udp', 'UDP'), + 'internet/DCCP': ('pcapkit.protocols.transport.dccp', 'DCCP'), 'internet/IPv6': ('pcapkit.protocols.internet.ipv6', 'IPv6'), 'internet/IPv6_ICMP': ('pcapkit.protocols.internet.icmpv6', 'ICMPv6'), 'internet/IPv6_Route': ('pcapkit.protocols.internet.ipv6_route', 'IPv6_Route'), diff --git a/examples/generators/options.py b/examples/generators/options.py index c8a4fe7ae1..cb75275856 100644 --- a/examples/generators/options.py +++ b/examples/generators/options.py @@ -16,7 +16,7 @@ :file:`options-ipv4.pcap` IPv4 options :file:`options-ipv6.pcap` HOPOPT, IPv6-Opts and IPv6-Route :file:`options-transport.pcap` SCTP chunks, parameters and error causes, - and HTTP/2 frames + HTTP/2 frames, and DCCP options :file:`options-internet.pcap` Mobility Header messages, options and extensions, and HIP parameters =============================== ============================================= @@ -1103,6 +1103,68 @@ def _hip_rebuild(parameters: 'Any') -> 'Any': return HIP(parameters=parameters, extension=True, **base) +############################################################################### +# DCCP -- options +############################################################################### + +#: Header fields shared by every constructed DCCP packet: a DCCP-Ack (type 3), +#: whose Acknowledgement Number the Ack Vector and Data Dropped options read +#: against (RFC 4340 sections 11.4 and 11.7). ``checksum`` is pinned, so a +#: mismatch points at the option under test and not at a recomputed checksum. +DCCP_BASE = { + 'srcport': 50000, 'dstport': 5001, 'type': 3, 'x': True, 'seq': 1, 'ack': 1, + 'checksum': b'\x00\x00', 'payload': b'', +} + + +def _dccp_registry() -> 'Any': + from pcapkit.protocols.transport.dccp import DCCP + return DCCP.__option__ + + +def _dccp_overrides() -> 'dict[Any, dict[str, Any]]': + from pcapkit.const.dccp.feature import Feature as Enum_Feature + from pcapkit.const.dccp.option import Option as Enum_Option + + # Each default is an empty or zero field; these give every variable part + # at least one octet, and the Elapsed Time its four-octet form. + vector = [(0, 3), (1, 0), (3, 63)] + return { + Enum_Option.Change_L: {'feature': Enum_Feature.Congestion_Control_ID, 'value': b'\x02\x03'}, + Enum_Option.Confirm_L: {'feature': Enum_Feature.Congestion_Control_ID, 'value': b'\x02\x02\x03'}, + Enum_Option.Change_R: {'feature': Enum_Feature.Send_Ack_Vector, 'value': b'\x01'}, + Enum_Option.Confirm_R: {'feature': Enum_Feature.Send_Ack_Vector, 'value': b'\x01'}, + Enum_Option.Init_Cookie: {'cookie': b'\x01\x02\x03'}, + Enum_Option.NDP_Count: {'count': 0x0102}, + Enum_Option.Ack_Vector_Nonce_0: {'vector': vector}, + Enum_Option.Ack_Vector_Nonce_1: {'vector': vector}, + Enum_Option.Data_Dropped: {'vector': [(None, 5), (3, 2)]}, + Enum_Option.Timestamp: {'timestamp': 0x11223344}, + Enum_Option.Timestamp_Echo: {'timestamp': 0x11223344, 'elapsed': 0x1234}, + Enum_Option.Elapsed_Time: {'elapsed': 0x123456}, + Enum_Option.Data_Checksum: {'checksum': b'\x01\x02\x03\x04'}, + } + + +def _dccp_build(code: 'Any', kwargs: 'dict[str, Any]') -> 'Any': + from pcapkit.protocols.transport.dccp import DCCP + return DCCP(options=[(code, kwargs)], **DCCP_BASE) + + +def _dccp_parse(octets: 'bytes') -> 'Any': + from pcapkit.protocols.transport.dccp import DCCP + return DCCP(octets, len(octets)) + + +def _dccp_extract(parsed: 'Any') -> 'Any': + return parsed.info.options + + +def _dccp_rebuild(options: 'Any') -> 'Any': + from pcapkit.protocols.transport.dccp import DCCP + return DCCP(options=options, **DCCP_BASE) + + ############################################################################### # HTTP/2 -- frames ############################################################################### @@ -1583,6 +1645,9 @@ def _pcapng_secrets_build(code: 'Any', kwargs: 'dict[str, Any]') -> 'Any': Family('httpv2-frame', _httpv2_registry, _httpv2_overrides, _httpv2_build, _httpv2_parse, _httpv2_extract, _httpv2_rebuild, capture='options-transport.pcap', envelope='tcp'), + Family('dccp-option', _dccp_registry, _dccp_overrides, + _dccp_build, _dccp_parse, _dccp_extract, _dccp_rebuild, + capture='options-transport.pcap', envelope='ipv4', proto=33), Family('mh-message', _mh_message_registry, _mh_message_overrides, _mh_message_build, _mh_parse, _mh_message_extract, _mh_message_rebuild, capture='options-internet.pcap', envelope='ipv6', proto=135), diff --git a/pcapkit/__init__.py b/pcapkit/__init__.py index a00ab4c6e7..736b6875e0 100644 --- a/pcapkit/__init__.py +++ b/pcapkit/__init__.py @@ -122,7 +122,7 @@ # IPv6 Extension Header 'IP_Pseudo', # IP Pseudo-Header - 'TCP', 'UDP', 'SCTP', # Transport Layer + 'DCCP', 'TCP', 'UDP', 'SCTP', # Transport Layer 'FTP', 'FTP_DATA', # Application Layer 'HTTP', 'HTTPv1', 'HTTPv2', 'NGAP', 'OSPF', diff --git a/pcapkit/all.py b/pcapkit/all.py index cc6453af7f..11bf704ae0 100644 --- a/pcapkit/all.py +++ b/pcapkit/all.py @@ -102,9 +102,10 @@ 'register_ipv6_opts_option', 'register_ipv6_route_routing', 'register_mh_message', 'register_mh_option', 'register_mh_extension', 'register_apptype', - 'register_tcp', 'register_udp', 'register_sctp', + 'register_tcp', 'register_udp', 'register_sctp', 'register_dccp', 'register_tcp_option', 'register_tcp_mp_option', 'register_sctp_chunk', 'register_sctp_parameter', 'register_sctp_cause', + 'register_dccp_option', 'register_http_frame', 'register_pcapng_block', 'register_pcapng_option', 'register_pcapng_secrets', 'register_pcapng_record', @@ -135,7 +136,7 @@ 'HIP', 'HOPOPT', 'IPv6_Ext', 'IPv6_Frag', 'IPv6_Opts', 'IPv6_Route', 'MH', # IPv6 Extension Header 'IP_Pseudo', # IP Pseudo-Header - 'TCP', 'UDP', 'SCTP', # Transport Layer + 'DCCP', 'TCP', 'UDP', 'SCTP', # Transport Layer 'FTP', 'FTP_DATA', # Application Layer 'HTTP', 'HTTPv1', 'HTTPv2', 'NGAP', 'OSPF', 'RARP', 'DRARP', 'SSL', 'SSLv2', 'SSLv3', 'TLS', 'DTLS', diff --git a/pcapkit/const/__init__.py b/pcapkit/const/__init__.py index b7bfe86b36..1fbe215b30 100644 --- a/pcapkit/const/__init__.py +++ b/pcapkit/const/__init__.py @@ -16,6 +16,7 @@ # per protocol from pcapkit.const.arp import * +from pcapkit.const.dccp import * from pcapkit.const.esp import * from pcapkit.const.ftp import * from pcapkit.const.hip import * @@ -41,6 +42,9 @@ 'ETHERTYPE', 'LINKTYPE', 'TRANSTYPE', 'APPTYPE', # ARP 'ARP_Hardware', 'ARP_Operation', + # DCCP + 'DCCP_PacketType', 'DCCP_ResetCode', 'DCCP_Option', 'DCCP_Feature', + 'DCCP_AckVectorState', 'DCCP_DropCode', # ESP 'ESP_Cipher', 'ESP_Integrity', # FTP diff --git a/pcapkit/const/dccp/__init__.py b/pcapkit/const/dccp/__init__.py new file mode 100644 index 0000000000..da7abfab5f --- /dev/null +++ b/pcapkit/const/dccp/__init__.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# pylint: disable=unused-import +""":class:`~pcapkit.protocols.transport.dccp.DCCP` Constant Enumerations +========================================================================== + +.. module:: pcapkit.const.dccp + +This module contains all constant enumerations of +:class:`~pcapkit.protocols.transport.dccp.DCCP` implementations. Available +enumerations include: + +.. list-table:: + + * - :class:`DCCP_PacketType ` + - DCCP Packet Types [*]_ + * - :class:`DCCP_ResetCode ` + - DCCP Reset Codes [*]_ + * - :class:`DCCP_Option ` + - DCCP Option Types [*]_ + * - :class:`DCCP_Feature ` + - DCCP Feature Numbers [*]_ + * - :class:`DCCP_AckVectorState ` + - DCCP Ack Vector States [*]_ + * - :class:`DCCP_DropCode ` + - DCCP Drop Codes [*]_ + +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-1 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-3 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-5 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-7 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-10 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-11 + +""" + +from pcapkit.const.dccp.ack_vector_state import AckVectorState as DCCP_AckVectorState +from pcapkit.const.dccp.drop_code import DropCode as DCCP_DropCode +from pcapkit.const.dccp.feature import Feature as DCCP_Feature +from pcapkit.const.dccp.option import Option as DCCP_Option +from pcapkit.const.dccp.packet_type import PacketType as DCCP_PacketType +from pcapkit.const.dccp.reset_code import ResetCode as DCCP_ResetCode + +__all__ = ['DCCP_PacketType', 'DCCP_ResetCode', 'DCCP_Option', 'DCCP_Feature', + 'DCCP_AckVectorState', 'DCCP_DropCode'] diff --git a/pcapkit/const/dccp/ack_vector_state.py b/pcapkit/const/dccp/ack_vector_state.py new file mode 100644 index 0000000000..12a73b4912 --- /dev/null +++ b/pcapkit/const/dccp/ack_vector_state.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""DCCP Ack Vector States +============================ + +.. module:: pcapkit.const.dccp.ack_vector_state + +This module contains the constant enumeration for **DCCP Ack Vector States**, +which is automatically generated from :class:`pcapkit.vendor.dccp.ack_vector_state.AckVectorState`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntEnum + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['AckVectorState'] + + +class AckVectorState(EnumRegistry, IntEnum): + """[AckVectorState] DCCP Ack Vector States""" + + #: Received [:rfc:`4340`] + Received = 0 + + #: Received ECN Marked [:rfc:`4340`] + Received_ECN_Marked = 1 + + #: Unassigned [:rfc:`4340`] + Unassigned_2 = 2 + + #: Not Yet Received [:rfc:`4340`] + Not_Yet_Received = 3 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 3): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'AckVectorState': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 3): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + return super()._missing_(value) diff --git a/pcapkit/const/dccp/drop_code.py b/pcapkit/const/dccp/drop_code.py new file mode 100644 index 0000000000..c12260a7ac --- /dev/null +++ b/pcapkit/const/dccp/drop_code.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""DCCP Drop Codes +===================== + +.. module:: pcapkit.const.dccp.drop_code + +This module contains the constant enumeration for **DCCP Drop Codes**, +which is automatically generated from :class:`pcapkit.vendor.dccp.drop_code.DropCode`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntEnum + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['DropCode'] + + +class DropCode(EnumRegistry, IntEnum): + """[DropCode] DCCP Drop Codes""" + + #: Protocol Constraints [:rfc:`4340`] + Protocol_Constraints = 0 + + #: Application Not Listening [:rfc:`4340`] + Application_Not_Listening = 1 + + #: Receive Buffer [:rfc:`4340`] + Receive_Buffer = 2 + + #: Corrupt [:rfc:`4340`] + Corrupt = 3 + + #: Delivered Corrupt [:rfc:`4340`] + Delivered_Corrupt = 7 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 7): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'DropCode': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 7): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + if 4 <= value <= 6: + #: Unassigned [:rfc:`4340`] + return cls._unregistered_member(value, 'Unassigned') + return super()._missing_(value) diff --git a/pcapkit/const/dccp/feature.py b/pcapkit/const/dccp/feature.py new file mode 100644 index 0000000000..afa7caaded --- /dev/null +++ b/pcapkit/const/dccp/feature.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""DCCP Feature Numbers +========================== + +.. module:: pcapkit.const.dccp.feature + +This module contains the constant enumeration for **DCCP Feature Numbers**, +which is automatically generated from :class:`pcapkit.vendor.dccp.feature.Feature`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntEnum + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['Feature'] + + +class Feature(EnumRegistry, IntEnum): + """[Feature] DCCP Feature Numbers""" + + #: Reserved [:rfc:`4340`] + Reserved_0 = 0 + + #: Congestion Control ID (CCID) [:rfc:`4340`] + Congestion_Control_ID = 1 + + #: Allow Short Seqnos [:rfc:`4340`] + Allow_Short_Seqnos = 2 + + #: Sequence Window [:rfc:`4340`] + Sequence_Window = 3 + + #: ECN Incapable [:rfc:`4340`] + ECN_Incapable = 4 + + #: Ack Ratio [:rfc:`4340`] + Ack_Ratio = 5 + + #: Send Ack Vector [:rfc:`4340`] + Send_Ack_Vector = 6 + + #: Send NDP Count [:rfc:`4340`] + Send_NDP_Count = 7 + + #: Minimum Checksum Coverage [:rfc:`4340`] + Minimum_Checksum_Coverage = 8 + + #: Check Data Checksum [:rfc:`4340`] + Check_Data_Checksum = 9 + + #: Multipath Capable [:rfc:`9897`] + Multipath_Capable = 10 + + #: Unassigned [:rfc:`4340`] + Unassigned_127 = 127 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'Feature': + """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 11 <= value <= 119: + #: Unassigned [:rfc:`4340`] + return cls._unregistered_member(value, 'Unassigned') + if 120 <= value <= 126: + #: Reserved [:rfc:`4340`] + return cls._unregistered_member(value, 'Reserved') + if 128 <= value <= 255: + #: CCID-specific codes [:rfc:`4340`] + return cls._unregistered_member(value, 'CCID_specific_codes') + return super()._missing_(value) diff --git a/pcapkit/const/dccp/option.py b/pcapkit/const/dccp/option.py new file mode 100644 index 0000000000..1e74c44013 --- /dev/null +++ b/pcapkit/const/dccp/option.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""DCCP Option Types +======================= + +.. module:: pcapkit.const.dccp.option + +This module contains the constant enumeration for **DCCP Option Types**, +which is automatically generated from :class:`pcapkit.vendor.dccp.option.Option`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntEnum + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['Option'] + + +class Option(EnumRegistry, IntEnum): + """[Option] DCCP Option Types""" + + #: Padding [:rfc:`4340`] + Padding = 0 + + #: Mandatory [:rfc:`4340`] + Mandatory = 1 + + #: Slow Receiver [:rfc:`4340`] + Slow_Receiver = 2 + + #: Reserved for experimental and testing use [:rfc:`4340`] + Reserved_for_experimental_and_testing_use = 31 + + #: Change L [:rfc:`4340`] + Change_L = 32 + + #: Confirm L [:rfc:`4340`] + Confirm_L = 33 + + #: Change R [:rfc:`4340`] + Change_R = 34 + + #: Confirm R [:rfc:`4340`] + Confirm_R = 35 + + #: Init Cookie [:rfc:`4340`] + Init_Cookie = 36 + + #: NDP Count [:rfc:`4340`] + NDP_Count = 37 + + #: Ack Vector [Nonce 0] [:rfc:`4340`] + Ack_Vector_Nonce_0 = 38 + + #: Ack Vector [Nonce 1] [:rfc:`4340`] + Ack_Vector_Nonce_1 = 39 + + #: Data Dropped [:rfc:`4340`] + Data_Dropped = 40 + + #: Timestamp [:rfc:`4340`] + Timestamp = 41 + + #: Timestamp Echo [:rfc:`4340`] + Timestamp_Echo = 42 + + #: Elapsed Time [:rfc:`4340`] + Elapsed_Time = 43 + + #: Data Checksum [:rfc:`4340`] + Data_Checksum = 44 + + #: Quick-Start Response [:rfc:`5634`] + Quick_Start_Response = 45 + + #: Multipath [:rfc:`9897`] + Multipath = 46 + + #: Unassigned [:rfc:`4340`] + Unassigned_127 = 127 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'Option': + """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 3 <= value <= 30: + #: Unassigned [:rfc:`4340`] + return cls._unregistered_member(value, 'Unassigned') + if 47 <= value <= 119: + #: Unassigned [:rfc:`4340`] + return cls._unregistered_member(value, 'Unassigned') + if 120 <= value <= 126: + #: Reserved [:rfc:`4340`] + return cls._unregistered_member(value, 'Reserved') + if 128 <= value <= 255: + #: CCID-specific codes [:rfc:`4340`] + return cls._unregistered_member(value, 'CCID_specific_codes') + return super()._missing_(value) diff --git a/pcapkit/const/dccp/packet_type.py b/pcapkit/const/dccp/packet_type.py new file mode 100644 index 0000000000..c5de410fa9 --- /dev/null +++ b/pcapkit/const/dccp/packet_type.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""DCCP Packet Types +======================= + +.. module:: pcapkit.const.dccp.packet_type + +This module contains the constant enumeration for **DCCP Packet Types**, +which is automatically generated from :class:`pcapkit.vendor.dccp.packet_type.PacketType`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntEnum + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['PacketType'] + + +class PacketType(EnumRegistry, IntEnum): + """[PacketType] DCCP Packet Types""" + + #: DCCP-Request [:rfc:`4340`] + DCCP_Request = 0 + + #: DCCP-Response [:rfc:`4340`] + DCCP_Response = 1 + + #: DCCP-Data [:rfc:`4340`] + DCCP_Data = 2 + + #: DCCP-Ack [:rfc:`4340`] + DCCP_Ack = 3 + + #: DCCP-DataAck [:rfc:`4340`] + DCCP_DataAck = 4 + + #: DCCP-CloseReq [:rfc:`4340`] + DCCP_CloseReq = 5 + + #: DCCP-Close [:rfc:`4340`] + DCCP_Close = 6 + + #: DCCP-Reset [:rfc:`4340`] + DCCP_Reset = 7 + + #: DCCP-Sync [:rfc:`4340`] + DCCP_Sync = 8 + + #: DCCP-SyncAck [:rfc:`4340`] + DCCP_SyncAck = 9 + + #: DCCP-Listen [:rfc:`5596`] + DCCP_Listen = 10 + + #: Reserved for experimental and testing use [:rfc:`4340`] + Reserved_for_experimental_and_testing_use = 14 + + #: Unassigned [:rfc:`4340`] + Unassigned_15 = 15 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 15): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'PacketType': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 15): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + if 11 <= value <= 13: + #: Unassigned [:rfc:`4340`] + return cls._unregistered_member(value, 'Unassigned') + return super()._missing_(value) diff --git a/pcapkit/const/dccp/reset_code.py b/pcapkit/const/dccp/reset_code.py new file mode 100644 index 0000000000..c86b074e3b --- /dev/null +++ b/pcapkit/const/dccp/reset_code.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""DCCP Reset Codes +====================== + +.. module:: pcapkit.const.dccp.reset_code + +This module contains the constant enumeration for **DCCP Reset Codes**, +which is automatically generated from :class:`pcapkit.vendor.dccp.reset_code.ResetCode`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntEnum + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['ResetCode'] + + +class ResetCode(EnumRegistry, IntEnum): + """[ResetCode] DCCP Reset Codes""" + + #: Unspecified [:rfc:`4340`] + Unspecified = 0 + + #: Closed [:rfc:`4340`] + Closed = 1 + + #: Aborted [:rfc:`4340`] + Aborted = 2 + + #: No Connection [:rfc:`4340`] + No_Connection = 3 + + #: Packet Error [:rfc:`4340`] + Packet_Error = 4 + + #: Option Error [:rfc:`4340`] + Option_Error = 5 + + #: Mandatory Error [:rfc:`4340`] + Mandatory_Error = 6 + + #: Connection Refused [:rfc:`4340`] + Connection_Refused = 7 + + #: Bad Service Code [:rfc:`4340`] + Bad_Service_Code = 8 + + #: Too Busy [:rfc:`4340`] + Too_Busy = 9 + + #: Bad Init Cookie [:rfc:`4340`] + Bad_Init_Cookie = 10 + + #: Aggression Penalty [:rfc:`4340`] + Aggression_Penalty = 11 + + #: Encapsulated Port Reuse [:rfc:`6773`] + Encapsulated_Port_Reuse = 12 + + #: Abrupt MP termination [:rfc:`9897`] + Abrupt_MP_termination = 13 + + #: Unassigned [:rfc:`4340`] + Unassigned_127 = 127 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 255): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'ResetCode': + """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 14 <= value <= 119: + #: Unassigned [:rfc:`4340`] + return cls._unregistered_member(value, 'Unassigned') + if 120 <= value <= 126: + #: Reserved [:rfc:`4340`] + return cls._unregistered_member(value, 'Reserved') + if 128 <= value <= 255: + #: CCID-specific codes [:rfc:`4340`] + return cls._unregistered_member(value, 'CCID_specific_codes') + return super()._missing_(value) diff --git a/pcapkit/foundation/__init__.py b/pcapkit/foundation/__init__.py index 517b4bd2bf..c7fbfa971e 100644 --- a/pcapkit/foundation/__init__.py +++ b/pcapkit/foundation/__init__.py @@ -44,9 +44,10 @@ 'register_ipv6_opts_option', 'register_ipv6_route_routing', 'register_mh_message', 'register_mh_option', 'register_mh_extension', 'register_apptype', - 'register_tcp', 'register_udp', 'register_sctp', + 'register_tcp', 'register_udp', 'register_sctp', 'register_dccp', 'register_tcp_option', 'register_tcp_mp_option', 'register_sctp_chunk', 'register_sctp_parameter', 'register_sctp_cause', + 'register_dccp_option', 'register_http_frame', 'register_pcapng_block', 'register_pcapng_option', 'register_pcapng_secrets', 'register_pcapng_record', diff --git a/pcapkit/foundation/registry/__init__.py b/pcapkit/foundation/registry/__init__.py index e5d6eac8eb..d98fb14be0 100644 --- a/pcapkit/foundation/registry/__init__.py +++ b/pcapkit/foundation/registry/__init__.py @@ -42,9 +42,10 @@ 'register_mh_message', 'register_mh_option', 'register_mh_extension', 'register_apptype', - 'register_tcp', 'register_udp', 'register_sctp', + 'register_tcp', 'register_udp', 'register_sctp', 'register_dccp', 'register_tcp_option', 'register_tcp_mp_option', 'register_sctp_chunk', 'register_sctp_parameter', 'register_sctp_cause', + 'register_dccp_option', 'register_http_frame', diff --git a/pcapkit/foundation/registry/protocols.py b/pcapkit/foundation/registry/protocols.py index 811ccbc181..6c56943de3 100644 --- a/pcapkit/foundation/registry/protocols.py +++ b/pcapkit/foundation/registry/protocols.py @@ -20,6 +20,7 @@ import aenum +from pcapkit.const.dccp.option import Option as DCCP_Option from pcapkit.const.hip.parameter import Parameter as HIP_Parameter from pcapkit.const.http.frame import Frame as HTTP_Frame from pcapkit.const.icmp.type import Type as ICMPv4_Type @@ -85,11 +86,13 @@ from pcapkit.protocols.schema.misc.pcapng import \ NameResolutionRecord as Schema_PCAPNG_NameResolutionRecord from pcapkit.protocols.schema.misc.pcapng import Option as Schema_PCAPNG_Option +from pcapkit.protocols.schema.transport.dccp import Option as Schema_DCCP_Option from pcapkit.protocols.schema.transport.sctp import Chunk as Schema_SCTP_Chunk from pcapkit.protocols.schema.transport.sctp import ErrorCause as Schema_SCTP_ErrorCause from pcapkit.protocols.schema.transport.sctp import Parameter as Schema_SCTP_Parameter 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.dccp import DCCP from pcapkit.protocols.transport.sctp import SCTP from pcapkit.protocols.transport.tcp import TCP from pcapkit.protocols.transport.udp import UDP @@ -133,6 +136,8 @@ from pcapkit.protocols.misc.pcapng import RecordParser as PCAPNG_RecordParser from pcapkit.protocols.misc.pcapng import SecretsConstructor as PCAPNG_SecretsConstructor from pcapkit.protocols.misc.pcapng import SecretsParser as PCAPNG_SecretsParser + from pcapkit.protocols.transport.dccp import OptionConstructor as DCCP_OptionConstructor + from pcapkit.protocols.transport.dccp import OptionParser as DCCP_OptionParser from pcapkit.protocols.transport.sctp import CauseConstructor as SCTP_CauseConstructor from pcapkit.protocols.transport.sctp import CauseParser as SCTP_CauseParser from pcapkit.protocols.transport.sctp import ChunkConstructor as SCTP_ChunkConstructor @@ -163,9 +168,10 @@ 'register_mh_message', 'register_mh_option', 'register_mh_extension', 'register_apptype', - 'register_tcp', 'register_udp', 'register_sctp', + 'register_tcp', 'register_udp', 'register_sctp', 'register_dccp', 'register_tcp_option', 'register_tcp_mp_option', 'register_sctp_chunk', 'register_sctp_parameter', 'register_sctp_cause', + 'register_dccp_option', 'register_http_frame', @@ -1968,6 +1974,90 @@ def register_sctp_cause(code: 'SCTP_CauseCode', meth: 'str | tuple[SCTP_CausePar logger.debug('registered SCTP error cause parser: %s', code.name) +@overload +def register_dccp(code: 'int | Enum_AppType', module: 'ModuleDescriptor[ProtocolBase] | Type[ProtocolBase]') -> 'None': ... +@overload +def register_dccp(code: 'int | Enum_AppType', module: 'str', class_: 'str') -> 'None': ... + + +# NOTE: pcapkit.protocols.transport.dccp.DCCP.__proto__ +def register_dccp(code: 'int | Enum_AppType', module: 'str | ModuleDescriptor[ProtocolBase] | Type[ProtocolBase]', + class_: 'str | NullType' = NULL) -> 'None': + r"""Register a new protocol class. + + Notes: + The fully qualified class name of the new protocol class + should be ``{module}.{class_}``. + + The function will register the given protocol class to the + :data:`pcapkit.protocols.transport.dccp.DCCP.__proto__` registry. + + Arguments: + code: port number + module: module name or module descriptor or a + :class:`~pcapkit.protocols.protocol.ProtocolBase` subclass + class\_: class name + + Raises: + RegistryError: If ``code`` is neither an :class:`int` (a :class:`bool` + is not accepted, even though it is one) nor an + :class:`~pcapkit.const.reg.apptype.apptype.AppType` member, e.g. a + member of another enum. + + .. versionadded:: 1.6.0 + + """ + _check_code(code, Enum_AppType, DCCP, number=True) + if isinstance(code, Enum_AppType): + code = code.port + if isinstance(module, str): + module = cast('ModuleDescriptor[ProtocolBase]', ModuleDescriptor(module, class_)) + + DCCP.register(code, module) + logger.debug('registered DCCP port: %s', code) + + # register protocol to protocol registry + if isinstance(module, ModuleDescriptor): + module = module.klass + _register_name(module, (DCCP, code)) + + +# NOTE: pcapkit.protocols.transport.dccp.DCCP.__option__ +def register_dccp_option(code: 'DCCP_Option', meth: 'str | tuple[DCCP_OptionParser, DCCP_OptionConstructor]', *, + schema: 'Optional[Type[Schema_DCCP_Option]]' = None) -> 'None': + """Register an option parser. + + The function will register the given option parser to the + :data:`pcapkit.protocols.transport.dccp.DCCP.__option__` registry. + + Args: + code: :class:`~pcapkit.protocols.transport.dccp.DCCP` option type as in + :class:`~pcapkit.const.dccp.option.Option`. + meth: Method name, or a ``(parser, constructor)`` pair of callables, to + parse and construct the option. A name names the + ``_read_opt_*`` and ``_make_opt_*`` methods, and has to name + an existing ``_read_opt_*`` one. + schema: :class:`~pcapkit.protocols.schema.schema.Schema` class for the option. + It should be a subclass of :class:`pcapkit.protocols.schema.transport.dccp.Option`. + + Raises: + RegistryError: If ``code`` is not a :class:`~pcapkit.const.dccp.option.Option` + member, e.g. a member of another enum, or ``meth`` + is a name with no matching ``_read_opt_*`` method. + + .. versionadded:: 1.6.0 + + """ + _check_code(code, DCCP_Option, DCCP) + if isinstance(meth, str) and not hasattr(DCCP, f'_read_opt_{meth}'): + raise RegistryError('method must be a valid DCCP option parser function') + + DCCP.register_option(code, meth) + if schema is not None: + Schema_DCCP_Option.register(code, schema) + logger.debug('registered DCCP option parser: %s', code.name) + + ############################################################################### # Application Layer Registries ############################################################################### diff --git a/pcapkit/protocols/__init__.py b/pcapkit/protocols/__init__.py index cc40f6a774..bab21da932 100644 --- a/pcapkit/protocols/__init__.py +++ b/pcapkit/protocols/__init__.py @@ -65,7 +65,7 @@ 'IPv6_Route', 'MH', # Transport Layer - 'TCP', 'UDP', 'SCTP', + 'DCCP', 'TCP', 'UDP', 'SCTP', # Application Layer 'FTP', 'FTP_DATA', diff --git a/pcapkit/protocols/data/__init__.py b/pcapkit/protocols/data/__init__.py index 2cf167ad04..b97db2d370 100644 --- a/pcapkit/protocols/data/__init__.py +++ b/pcapkit/protocols/data/__init__.py @@ -229,6 +229,13 @@ 'MH_CGAExtension', 'MH_UnknownExtension', 'MH_MultiPrefixExtension', 'MH_ExperimentalExtension', + # Datagram Congestion Control Protocol + 'DCCP', 'DCCP_AckVectorRun', 'DCCP_DataDroppedBlock', + 'DCCP_Option', + 'DCCP_UnassignedOption', 'DCCP_Padding', 'DCCP_Mandatory', 'DCCP_SlowReceiver', 'DCCP_Change', + 'DCCP_Confirm', 'DCCP_InitCookie', 'DCCP_NDPCount', 'DCCP_AckVector', 'DCCP_DataDropped', + 'DCCP_Timestamp', 'DCCP_TimestampEcho', 'DCCP_ElapsedTime', 'DCCP_DataChecksum', + # Transmission Control Protocol 'TCP', 'TCP_Flags', 'TCP_SACKBlock', diff --git a/pcapkit/protocols/data/transport/__init__.py b/pcapkit/protocols/data/transport/__init__.py index 51f29e8475..b17e53651e 100644 --- a/pcapkit/protocols/data/transport/__init__.py +++ b/pcapkit/protocols/data/transport/__init__.py @@ -1,6 +1,26 @@ # -*- coding: utf-8 -*- """data models for transport layer protocols""" +# Datagram Congestion Control Protocol +from pcapkit.protocols.data.transport.dccp import DCCP +from pcapkit.protocols.data.transport.dccp import AckVector as DCCP_AckVector +from pcapkit.protocols.data.transport.dccp import AckVectorRun as DCCP_AckVectorRun +from pcapkit.protocols.data.transport.dccp import Change as DCCP_Change +from pcapkit.protocols.data.transport.dccp import Confirm as DCCP_Confirm +from pcapkit.protocols.data.transport.dccp import DataChecksum as DCCP_DataChecksum +from pcapkit.protocols.data.transport.dccp import DataDropped as DCCP_DataDropped +from pcapkit.protocols.data.transport.dccp import DataDroppedBlock as DCCP_DataDroppedBlock +from pcapkit.protocols.data.transport.dccp import ElapsedTime as DCCP_ElapsedTime +from pcapkit.protocols.data.transport.dccp import InitCookie as DCCP_InitCookie +from pcapkit.protocols.data.transport.dccp import Mandatory as DCCP_Mandatory +from pcapkit.protocols.data.transport.dccp import NDPCount as DCCP_NDPCount +from pcapkit.protocols.data.transport.dccp import Option as DCCP_Option +from pcapkit.protocols.data.transport.dccp import Padding as DCCP_Padding +from pcapkit.protocols.data.transport.dccp import SlowReceiver as DCCP_SlowReceiver +from pcapkit.protocols.data.transport.dccp import Timestamp as DCCP_Timestamp +from pcapkit.protocols.data.transport.dccp import TimestampEcho as DCCP_TimestampEcho +from pcapkit.protocols.data.transport.dccp import UnassignedOption as DCCP_UnassignedOption + # Transmission Control Protocol from pcapkit.protocols.data.transport.tcp import CC as TCP_CC from pcapkit.protocols.data.transport.tcp import MPTCP as TCP_MPTCP @@ -115,6 +135,13 @@ from pcapkit.protocols.data.transport.udp import UDP __all__ = [ + # Datagram Congestion Control Protocol + 'DCCP', 'DCCP_AckVectorRun', 'DCCP_DataDroppedBlock', + 'DCCP_Option', + 'DCCP_UnassignedOption', 'DCCP_Padding', 'DCCP_Mandatory', 'DCCP_SlowReceiver', 'DCCP_Change', + 'DCCP_Confirm', 'DCCP_InitCookie', 'DCCP_NDPCount', 'DCCP_AckVector', 'DCCP_DataDropped', + 'DCCP_Timestamp', 'DCCP_TimestampEcho', 'DCCP_ElapsedTime', 'DCCP_DataChecksum', + # Transmission Control Protocol 'TCP', 'TCP_Flags', 'TCP_SACKBlock', diff --git a/pcapkit/protocols/data/transport/dccp.py b/pcapkit/protocols/data/transport/dccp.py new file mode 100644 index 0000000000..e10f225c27 --- /dev/null +++ b/pcapkit/protocols/data/transport/dccp.py @@ -0,0 +1,276 @@ +# -*- coding: utf-8 -*- +"""data model for DCCP 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 typing import Optional + + from pcapkit.const.dccp.ack_vector_state import AckVectorState + from pcapkit.const.dccp.drop_code import DropCode + from pcapkit.const.dccp.feature import Feature + from pcapkit.const.dccp.option import Option as OptionType + from pcapkit.const.dccp.packet_type import PacketType + from pcapkit.const.dccp.reset_code import ResetCode + from pcapkit.const.reg.apptype import AppType + from pcapkit.corekit.checksum.internet import ChecksumStatus + from pcapkit.corekit.infoclass import OrderedMultiInfo + +__all__ = [ + 'DCCP', + + 'Option', + 'UnassignedOption', 'Padding', 'Mandatory', 'SlowReceiver', 'Change', 'Confirm', + 'InitCookie', 'NDPCount', 'AckVector', 'DataDropped', 'Timestamp', 'TimestampEcho', + 'ElapsedTime', 'DataChecksum', + + 'AckVectorRun', 'DataDroppedBlock', +] + + +@info_final +class DCCP(Protocol): + """Data model for DCCP packet.""" + + #: Source port. + srcport: 'AppType' + #: Destination port. + dstport: 'AppType' + #: Header length, i.e. the Data Offset in octets. + hdr_len: 'int' + #: Value used by the HC-Sender CCID (``CCVal``). + ccval: 'int' + #: Checksum coverage (``CsCov``). + cscov: 'int' + #: Checksum. + checksum: 'bytes' + #: Reserved bits (``Res``). + reserved: 'int' + #: Packet type. + type: 'PacketType' + #: Extended sequence numbers (``X``), i.e. 48-bit rather than 24-bit ones. + x: 'bool' + #: Sequence number. + seq: 'int' + + if TYPE_CHECKING: + #: Reserved octet ahead of a 48-bit sequence number, present only when + #: :attr:`x` is set. + seq_reserved: 'int' + #: Reserved bits of the acknowledgement number subheader, present only + #: in a packet type that carries one. + ack_reserved: 'int' + #: Acknowledgement number, present only in a packet type that carries + #: one. + ack: 'int' + #: Service code, present only in a DCCP-Request, DCCP-Response or + #: DCCP-Listen. + service_code: 'int' + #: Reset code, present only in a DCCP-Reset. + reset_code: 'ResetCode' + #: Data 1, Data 2 and Data 3 of a DCCP-Reset, present only in one. + reset_data: 'bytes' + #: Options, present only when the header has an option area. + options: 'OrderedMultiInfo[OptionType, Option]' + #: Outcome of verifying :attr:`checksum`, present only when the + #: enclosing IP layer verified it under + #: :data:`~pcapkit.utilities.config.CHECKSUM_VERIFY`. + checksum_status: 'ChecksumStatus' + + def __init__(self, srcport: 'AppType', dstport: 'AppType', hdr_len: 'int', ccval: 'int', cscov: 'int', + checksum: 'bytes', reserved: 'int', type: 'PacketType', x: 'bool', seq: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +class Option(Data): + """Data model for DCCP options.""" + + #: Option type. + type: 'OptionType' + #: Option length, ``1`` for a single-octet option. + length: 'int' + + +@info_final +class UnassignedOption(Option): + """Data model for unassigned DCCP option.""" + + #: Option data, empty for a single-octet option. + data: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'OptionType', length: 'int', data: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class Padding(Option): + """Data model for DCCP padding option.""" + + if TYPE_CHECKING: + def __init__(self, type: 'OptionType', length: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class Mandatory(Option): + """Data model for DCCP mandatory option.""" + + if TYPE_CHECKING: + def __init__(self, type: 'OptionType', length: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class SlowReceiver(Option): + """Data model for DCCP slow receiver option.""" + + if TYPE_CHECKING: + def __init__(self, type: 'OptionType', length: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class Change(Option): + """Data model for DCCP Change L and Change R options.""" + + #: Feature number, :data:`None` if absent. + feature: 'Optional[Feature]' + #: Preference list or value, as on the wire. + value: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'OptionType', length: 'int', feature: 'Optional[Feature]', value: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class Confirm(Option): + """Data model for DCCP Confirm L and Confirm R options.""" + + #: Feature number, :data:`None` if absent. + feature: 'Optional[Feature]' + #: Value, then the preference list for a server-priority feature, as on + #: the wire. + value: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'OptionType', length: 'int', feature: 'Optional[Feature]', value: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class InitCookie(Option): + """Data model for DCCP init cookie option.""" + + #: Cookie. + cookie: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'OptionType', length: 'int', cookie: 'bytes') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class NDPCount(Option): + """Data model for DCCP NDP count option.""" + + #: Count of consecutive non-data packets. + count: 'int' + + if TYPE_CHECKING: + def __init__(self, type: 'OptionType', length: 'int', count: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class AckVectorRun(Data): + """Data model for one octet of a DCCP Ack Vector.""" + + #: State of the packets in the run. + state: 'AckVectorState' + #: Number of packets preceding the first one, in the same state. + run_length: 'int' + + if TYPE_CHECKING: + def __init__(self, state: 'AckVectorState', run_length: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class AckVector(Option): + """Data model for DCCP Ack Vector options.""" + + #: ECN Nonce Echo the option type implies, ``0`` or ``1``. + nonce: 'int' + #: Vector, newest packet first. + vector: 'tuple[AckVectorRun, ...]' + + if TYPE_CHECKING: + def __init__(self, type: 'OptionType', length: 'int', nonce: 'int', vector: 'tuple[AckVectorRun, ...]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class DataDroppedBlock(Data): + """Data model for one octet of a DCCP Data Dropped vector.""" + + #: Whether it is a drop block rather than a normal one. + dropped: 'bool' + #: Drop code of a drop block, :data:`None` for a normal one. + code: 'Optional[DropCode]' + #: Number of packets preceding the first one, in the same block. + run_length: 'int' + + if TYPE_CHECKING: + def __init__(self, dropped: 'bool', code: 'Optional[DropCode]', run_length: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class DataDropped(Option): + """Data model for DCCP data dropped option.""" + + #: Vector, newest packet first. + vector: 'tuple[DataDroppedBlock, ...]' + + if TYPE_CHECKING: + def __init__(self, type: 'OptionType', length: 'int', vector: 'tuple[DataDroppedBlock, ...]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class Timestamp(Option): + """Data model for DCCP timestamp option.""" + + #: Timestamp, in units of 10 microseconds. + timestamp: 'int' + + if TYPE_CHECKING: + def __init__(self, type: 'OptionType', length: 'int', timestamp: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class TimestampEcho(Option): + """Data model for DCCP timestamp echo option.""" + + #: Timestamp echoed. + timestamp: 'int' + #: Elapsed time, in hundredths of milliseconds, :data:`None` if absent. + elapsed: 'Optional[int]' + + if TYPE_CHECKING: + def __init__(self, type: 'OptionType', length: 'int', timestamp: 'int', elapsed: 'Optional[int]') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class ElapsedTime(Option): + """Data model for DCCP elapsed time option.""" + + #: Elapsed time, in hundredths of milliseconds. + elapsed: 'int' + + if TYPE_CHECKING: + def __init__(self, type: 'OptionType', length: 'int', elapsed: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,line-too-long,redefined-builtin + + +@info_final +class DataChecksum(Option): + """Data model for DCCP data checksum option.""" + + #: CRC-32c of the application data, as on the wire. + checksum: 'bytes' + + if TYPE_CHECKING: + def __init__(self, type: 'OptionType', length: 'int', checksum: 'bytes') -> '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 a5ce07358f..5147f7ff9e 100644 --- a/pcapkit/protocols/internet/internet.py +++ b/pcapkit/protocols/internet/internet.py @@ -67,6 +67,8 @@ class Internet(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=ab - :class:`pcapkit.protocols.transport.tcp.TCP` * - :attr:`~pcapkit.const.reg.transtype.TransType.UDP` - :class:`pcapkit.protocols.transport.udp.UDP` + * - :attr:`~pcapkit.const.reg.transtype.TransType.DCCP` + - :class:`pcapkit.protocols.transport.dccp.DCCP` * - :attr:`~pcapkit.const.reg.transtype.TransType.IPv6` - :class:`pcapkit.protocols.internet.ipv6.IPv6` * - :attr:`~pcapkit.const.reg.transtype.TransType.IPv6_ICMP` @@ -127,6 +129,7 @@ class Internet(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=ab Enum_TransType.ICMP: ModuleDescriptor('pcapkit.protocols.internet.icmpv4', 'ICMPv4'), Enum_TransType.TCP: ModuleDescriptor('pcapkit.protocols.transport.tcp', 'TCP'), Enum_TransType.UDP: ModuleDescriptor('pcapkit.protocols.transport.udp', 'UDP'), + Enum_TransType.DCCP: ModuleDescriptor('pcapkit.protocols.transport.dccp', 'DCCP'), Enum_TransType.IPv6: ModuleDescriptor('pcapkit.protocols.internet.ipv6', 'IPv6'), Enum_TransType.IPv6_ICMP: ModuleDescriptor('pcapkit.protocols.internet.icmpv6', 'ICMPv6'), Enum_TransType.IPv6_Route: ModuleDescriptor('pcapkit.protocols.internet.ipv6_route', 'IPv6_Route'), diff --git a/pcapkit/protocols/schema/__init__.py b/pcapkit/protocols/schema/__init__.py index 9964874063..9b61b8f4b9 100644 --- a/pcapkit/protocols/schema/__init__.py +++ b/pcapkit/protocols/schema/__init__.py @@ -151,6 +151,11 @@ 'MH_UnknownExtension', 'MH_MultiPrefixExtension', 'MH_ExperimentalExtension', # Transport Layer Protocols + 'DCCP', + 'DCCP_Option', + 'DCCP_UnassignedOption', 'DCCP_Padding', 'DCCP_Mandatory', 'DCCP_SlowReceiver', 'DCCP_Change', + 'DCCP_Confirm', 'DCCP_InitCookie', 'DCCP_NDPCount', 'DCCP_AckVector', 'DCCP_DataDropped', + 'DCCP_Timestamp', 'DCCP_TimestampEcho', 'DCCP_ElapsedTime', 'DCCP_DataChecksum', 'TCP', 'TCP_SACKBlock', 'TCP_Option', diff --git a/pcapkit/protocols/schema/transport/__init__.py b/pcapkit/protocols/schema/transport/__init__.py index aa7df7b7e4..654bab4572 100644 --- a/pcapkit/protocols/schema/transport/__init__.py +++ b/pcapkit/protocols/schema/transport/__init__.py @@ -1,6 +1,24 @@ # -*- coding: utf-8 -*- """header schema for transport layer protocols""" +# Datagram Congestion Control Protocol +from pcapkit.protocols.schema.transport.dccp import DCCP +from pcapkit.protocols.schema.transport.dccp import AckVector as DCCP_AckVector +from pcapkit.protocols.schema.transport.dccp import Change as DCCP_Change +from pcapkit.protocols.schema.transport.dccp import Confirm as DCCP_Confirm +from pcapkit.protocols.schema.transport.dccp import DataChecksum as DCCP_DataChecksum +from pcapkit.protocols.schema.transport.dccp import DataDropped as DCCP_DataDropped +from pcapkit.protocols.schema.transport.dccp import ElapsedTime as DCCP_ElapsedTime +from pcapkit.protocols.schema.transport.dccp import InitCookie as DCCP_InitCookie +from pcapkit.protocols.schema.transport.dccp import Mandatory as DCCP_Mandatory +from pcapkit.protocols.schema.transport.dccp import NDPCount as DCCP_NDPCount +from pcapkit.protocols.schema.transport.dccp import Option as DCCP_Option +from pcapkit.protocols.schema.transport.dccp import Padding as DCCP_Padding +from pcapkit.protocols.schema.transport.dccp import SlowReceiver as DCCP_SlowReceiver +from pcapkit.protocols.schema.transport.dccp import Timestamp as DCCP_Timestamp +from pcapkit.protocols.schema.transport.dccp import TimestampEcho as DCCP_TimestampEcho +from pcapkit.protocols.schema.transport.dccp import UnassignedOption as DCCP_UnassignedOption + # Transmission Control Protocol from pcapkit.protocols.schema.transport.tcp import CC as TCP_CC from pcapkit.protocols.schema.transport.tcp import MPTCP as TCP_MPTCP @@ -112,6 +130,13 @@ from pcapkit.protocols.schema.transport.udp import UDP __all__ = [ + # Datagram Congestion Control Protocol + 'DCCP', + 'DCCP_Option', + 'DCCP_UnassignedOption', 'DCCP_Padding', 'DCCP_Mandatory', 'DCCP_SlowReceiver', 'DCCP_Change', + 'DCCP_Confirm', 'DCCP_InitCookie', 'DCCP_NDPCount', 'DCCP_AckVector', 'DCCP_DataDropped', + 'DCCP_Timestamp', 'DCCP_TimestampEcho', 'DCCP_ElapsedTime', 'DCCP_DataChecksum', + # Transmission Control Protocol 'TCP', 'TCP_SACKBlock', diff --git a/pcapkit/protocols/schema/transport/dccp.py b/pcapkit/protocols/schema/transport/dccp.py new file mode 100644 index 0000000000..ead4e47d6b --- /dev/null +++ b/pcapkit/protocols/schema/transport/dccp.py @@ -0,0 +1,381 @@ +# -*- coding: utf-8 -*- +# mypy: disable-error-code=assignment +"""header schema for datagram congestion control protocol""" + +from typing import TYPE_CHECKING + +from pcapkit.const.dccp.feature import Feature as Enum_Feature +from pcapkit.const.dccp.option import Option as Enum_Option +from pcapkit.const.dccp.packet_type import PacketType as Enum_PacketType +from pcapkit.const.dccp.reset_code import ResetCode as Enum_ResetCode +from pcapkit.const.reg.apptype import AppType as Enum_AppType +from pcapkit.const.reg.apptype import TransportProtocol as Enum_TransportProtocol +from pcapkit.corekit.fields.collections import OptionField +from pcapkit.corekit.fields.misc import ConditionalField, PayloadField +from pcapkit.corekit.fields.numbers import (EnumField, NumberField, PortEnumField, UInt8Field, + UInt32Field) +from pcapkit.corekit.fields.strings import BitField, BytesField +from pcapkit.protocols.schema.schema import EnumSchema, Schema, schema_final +from pcapkit.utilities.logging import SPHINX_TYPE_CHECKING + +__all__ = [ + 'DCCP', + + 'Option', + 'UnassignedOption', 'Padding', 'Mandatory', 'SlowReceiver', 'Change', 'Confirm', + 'InitCookie', 'NDPCount', 'AckVector', 'DataDropped', 'Timestamp', 'TimestampEcho', + 'ElapsedTime', 'DataChecksum', +] + +if TYPE_CHECKING: + from typing import Any, Optional + + from pcapkit.protocols.protocol import ProtocolBase + +#: Length of the generic header with 24-bit sequence numbers (``X`` clear), the +#: shortest a DCCP header can be, :rfc:`4340#section-5.1`. The one home of this +#: value (:issue:`1739`); it is fixed by the RFC, so it is never configurable. +DCCP_MIN_HEADER_LEN = 12 + +#: Option types up to this one are single octets, without a length, +#: :rfc:`4340#section-5.8`. +DCCP_SINGLE_OCTET_OPTION_MAX = 31 + +#: Packet types carrying an Acknowledgement Number Subheader: every one +#: :rfc:`4340#section-5` defines but DCCP-Request and DCCP-Data. DCCP-Listen +#: (:rfc:`5596#section-2.2.1`) carries none either. +ACK_TYPES = frozenset({ + Enum_PacketType.DCCP_Response, Enum_PacketType.DCCP_Ack, Enum_PacketType.DCCP_DataAck, + Enum_PacketType.DCCP_CloseReq, Enum_PacketType.DCCP_Close, Enum_PacketType.DCCP_Reset, + Enum_PacketType.DCCP_Sync, Enum_PacketType.DCCP_SyncAck, +}) + +#: Packet types carrying a Service Code: DCCP-Request, DCCP-Response and +#: DCCP-Listen (:rfc:`4340#section-5.2`, :rfc:`5596#section-2.2.1`). +SERVICE_TYPES = frozenset({ + Enum_PacketType.DCCP_Request, Enum_PacketType.DCCP_Response, Enum_PacketType.DCCP_Listen, +}) + +#: Packet type of DCCP-Reset, which carries a Reset Code and three Data octets, +#: :rfc:`4340#section-5.6`. +RESET_TYPE = Enum_PacketType.DCCP_Reset + +if SPHINX_TYPE_CHECKING: # pragma: no cover + from typing_extensions import TypedDict + + class Coverage(TypedDict): + """DCCP ``CCVal`` and checksum coverage field.""" + + #: Value used by the HC-Sender CCID. + ccval: int + #: Checksum coverage. + cscov: int + + class TypeField(TypedDict): + """DCCP reserved bits, packet type and extended sequence numbers flag.""" + + #: Reserved bits (``Res``). + reserved: int + #: Packet type. + type: int + #: Extended sequence numbers (``X``). + x: int + + +def header_length(field: 'TypeField') -> 'int': + """Length of the generic header and the fields its packet type adds. + + Args: + field: The packet type and extended sequence numbers flag. + + Returns: + The octets ahead of the options, :rfc:`4340#section-5`. + + """ + extended = field['x'] + code = field['type'] + + length = DCCP_MIN_HEADER_LEN + (4 if extended else 0) + if code in ACK_TYPES: + length += 8 if extended else 4 + if code in SERVICE_TYPES: + length += 4 + if code == RESET_TYPE: + length += 4 + return length + + +class Option(EnumSchema[Enum_Option]): + """Header schema for DCCP options.""" + + __default__ = lambda: UnassignedOption + + #: Option type. + type: 'Enum_Option' = EnumField(length=1, namespace=Enum_Option) + #: Option length, absent from a single-octet option. + length: 'int' = ConditionalField( + UInt8Field(), + lambda pkt: pkt['type'] > DCCP_SINGLE_OCTET_OPTION_MAX, + ) + + def post_process(self, packet: 'dict[str, Any]') -> 'Schema': + """Revise ``schema`` data after unpacking process. + + Args: + packet: Unpacked data. + + Returns: + Revised schema. + + """ + if self.type <= DCCP_SINGLE_OCTET_OPTION_MAX: + self.length = 1 + return self + + +@schema_final +class UnassignedOption(Option): + """Header schema for DCCP unassigned options.""" + + #: Option data, absent from a single-octet option. + data: 'bytes' = ConditionalField( + BytesField(length=lambda pkt: pkt['length'] - 2), + lambda pkt: pkt['type'] > DCCP_SINGLE_OCTET_OPTION_MAX, + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', data: 'Optional[bytes]' = None) -> 'None': ... + + +@schema_final +class Padding(Option, code=Enum_Option.Padding): + """Header schema for DCCP padding option.""" + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int') -> 'None': ... + + +@schema_final +class Mandatory(Option, code=Enum_Option.Mandatory): + """Header schema for DCCP mandatory option.""" + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int') -> 'None': ... + + +@schema_final +class SlowReceiver(Option, code=Enum_Option.Slow_Receiver): + """Header schema for DCCP slow receiver option.""" + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int') -> 'None': ... + + +@schema_final +class Change(Option, code=[Enum_Option.Change_L, Enum_Option.Change_R]): + """Header schema for DCCP Change L and Change R options.""" + + #: Feature number, absent from a 2-octet option. + feature: 'Enum_Feature' = ConditionalField( + EnumField(length=1, namespace=Enum_Feature), + lambda pkt: pkt['length'] > 2, + ) + #: Preference list or value; a length under 2 is refused, as in every + #: option that has a length (:rfc:`4340#section-5.8`). + value: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - (3 if pkt['length'] > 2 else 2)) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', value: 'bytes', + feature: 'Optional[Enum_Feature | int]' = None) -> 'None': ... + + +@schema_final +class Confirm(Option, code=[Enum_Option.Confirm_L, Enum_Option.Confirm_R]): + """Header schema for DCCP Confirm L and Confirm R options.""" + + #: Feature number, absent from a 2-octet option. + feature: 'Enum_Feature' = ConditionalField( + EnumField(length=1, namespace=Enum_Feature), + lambda pkt: pkt['length'] > 2, + ) + #: Value, then the preference list for a server-priority feature; a length + #: under 2 is refused, as in every option that has a length + #: (:rfc:`4340#section-5.8`). + value: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - (3 if pkt['length'] > 2 else 2)) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', value: 'bytes', + feature: 'Optional[Enum_Feature | int]' = None) -> 'None': ... + + +@schema_final +class InitCookie(Option, code=Enum_Option.Init_Cookie): + """Header schema for DCCP init cookie option.""" + + #: Cookie. + cookie: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 2) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', cookie: 'bytes') -> 'None': ... + + +@schema_final +class NDPCount(Option, code=Enum_Option.NDP_Count): + """Header schema for DCCP NDP count option.""" + + #: NDP count, ``length - 2`` octets, read and rebuilt at that width; + #: :rfc:`4340#section-7.7` allows one to six. + count: 'int' = NumberField(length=lambda pkt: pkt['length'] - 2, signed=False) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', count: 'int') -> 'None': ... + + +@schema_final +class AckVector(Option, code=[Enum_Option.Ack_Vector_Nonce_0, Enum_Option.Ack_Vector_Nonce_1]): + """Header schema for DCCP Ack Vector options.""" + + #: Vector, one state and run length per octet. + vector: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 2) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', vector: 'bytes') -> 'None': ... + + +@schema_final +class DataDropped(Option, code=Enum_Option.Data_Dropped): + """Header schema for DCCP data dropped option.""" + + #: Vector, one normal or drop block per octet. + vector: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 2) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', vector: 'bytes') -> 'None': ... + + +@schema_final +class Timestamp(Option, code=Enum_Option.Timestamp): + """Header schema for DCCP timestamp option.""" + + #: Timestamp value. + timestamp: 'int' = NumberField(length=lambda pkt: pkt['length'] - 2, signed=False) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', timestamp: 'int') -> 'None': ... + + +@schema_final +class TimestampEcho(Option, code=Enum_Option.Timestamp_Echo): + """Header schema for DCCP timestamp echo option.""" + + #: Timestamp echo. + timestamp: 'int' = NumberField(length=lambda pkt: min(pkt['length'] - 2, 4), signed=False) + #: Elapsed time, absent from a 6-octet option. + elapsed: 'int' = ConditionalField( + NumberField(length=lambda pkt: pkt['length'] - 6, signed=False), + lambda pkt: pkt['length'] > 6, + ) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', timestamp: 'int', + elapsed: 'Optional[int]' = None) -> 'None': ... + + +@schema_final +class ElapsedTime(Option, code=Enum_Option.Elapsed_Time): + """Header schema for DCCP elapsed time option.""" + + #: Elapsed time, two or four octets. + elapsed: 'int' = NumberField(length=lambda pkt: pkt['length'] - 2, signed=False) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', elapsed: 'int') -> 'None': ... + + +@schema_final +class DataChecksum(Option, code=Enum_Option.Data_Checksum): + """Header schema for DCCP data checksum option.""" + + #: CRC-32c of the application data. + checksum: 'bytes' = BytesField(length=lambda pkt: pkt['length'] - 2) + + if TYPE_CHECKING: + def __init__(self, type: 'Enum_Option', length: 'int', checksum: 'bytes') -> 'None': ... + + +@schema_final +class DCCP(Schema): + """Header schema for DCCP packet.""" + + #: Source port. + srcport: 'Enum_AppType' = PortEnumField(length=2, namespace=Enum_AppType, + proto=Enum_TransportProtocol.dccp) + #: Destination port. + dstport: 'Enum_AppType' = PortEnumField(length=2, namespace=Enum_AppType, + proto=Enum_TransportProtocol.dccp) + #: Data offset, in 32-bit words. + offset: 'int' = UInt8Field() + #: ``CCVal`` and checksum coverage. + cscov: 'Coverage' = BitField(length=1, namespace={ + 'ccval': (0, 4), + 'cscov': (4, 4), + }) + #: Checksum. + checksum: 'bytes' = BytesField(length=2) + #: Reserved bits, packet type and extended sequence numbers flag. + type: 'TypeField' = BitField(length=1, namespace={ + 'reserved': (0, 3), + 'type': (3, 4), + 'x': (7, 1), + }) + #: Reserved octet ahead of a 48-bit sequence number. + seq_reserved: 'int' = ConditionalField( + UInt8Field(), + lambda pkt: pkt['type']['x'] == 1, + ) + #: Sequence number, 48 or 24 bits. + seq: 'int' = NumberField(length=lambda pkt: 6 if pkt['type']['x'] else 3, signed=False) + #: Reserved bits of the acknowledgement number subheader. + ack_reserved: 'int' = ConditionalField( + NumberField(length=lambda pkt: 2 if pkt['type']['x'] else 1, signed=False), + lambda pkt: pkt['type']['type'] in ACK_TYPES, + ) + #: Acknowledgement number, 48 or 24 bits. + ack: 'int' = ConditionalField( + NumberField(length=lambda pkt: 6 if pkt['type']['x'] else 3, signed=False), + lambda pkt: pkt['type']['type'] in ACK_TYPES, + ) + #: Service code. + service_code: 'int' = ConditionalField( + UInt32Field(), + lambda pkt: pkt['type']['type'] in SERVICE_TYPES, + ) + #: Reset code. + reset_code: 'Enum_ResetCode' = ConditionalField( + EnumField(length=1, namespace=Enum_ResetCode), + lambda pkt: pkt['type']['type'] == RESET_TYPE, + ) + #: Data 1, 2 and 3 of a reset. + reset_data: 'bytes' = ConditionalField( + BytesField(length=3), + lambda pkt: pkt['type']['type'] == RESET_TYPE, + ) + #: Options, cut short where the data is; :meth:`DCCP.read + #: ` refuses such a header. + options: 'list[Option]' = OptionField( + length=lambda pkt: max(min(pkt['offset'] * 4 - header_length(pkt['type']), pkt['__length__']), 0), + base_schema=Option, + type_name='type', + registry=Option.registry, + ) + #: Payload, i.e. the application data area. + payload: 'bytes' = PayloadField() + + if TYPE_CHECKING: + def __init__(self, srcport: 'Enum_AppType | int', dstport: 'Enum_AppType | int', offset: 'int', + cscov: 'Coverage', checksum: 'bytes', type: 'TypeField', seq: 'int', + options: 'list[Option | bytes] | bytes', payload: 'bytes | ProtocolBase | Schema', + seq_reserved: 'Optional[int]' = None, ack_reserved: 'Optional[int]' = None, + ack: 'Optional[int]' = None, service_code: 'Optional[int]' = None, + reset_code: 'Optional[Enum_ResetCode | int]' = None, + reset_data: 'Optional[bytes]' = None) -> 'None': ... diff --git a/pcapkit/protocols/transport/NotImplemented/dccp.py b/pcapkit/protocols/transport/NotImplemented/dccp.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/pcapkit/protocols/transport/__init__.py b/pcapkit/protocols/transport/__init__.py index bbe6320833..f71009ba3b 100644 --- a/pcapkit/protocols/transport/__init__.py +++ b/pcapkit/protocols/transport/__init__.py @@ -9,12 +9,13 @@ the transport layer, with detailed implementation and methods. """ -# TODO: Implement DCCP, RSVP. +# TODO: Implement RSVP. # Base Class for Transport Layer from pcapkit.protocols.transport.transport import Transport # Transport Layer Protocols +from pcapkit.protocols.transport.dccp import DCCP from pcapkit.protocols.transport.sctp import SCTP from pcapkit.protocols.transport.tcp import TCP from pcapkit.protocols.transport.udp import UDP @@ -24,5 +25,5 @@ __all__ = [ 'TRANSTYPE', # Protocol Numbers - 'TCP', 'UDP', 'SCTP', # Transport Layer Protocols + 'DCCP', 'TCP', 'UDP', 'SCTP', # Transport Layer Protocols ] diff --git a/pcapkit/protocols/transport/dccp.py b/pcapkit/protocols/transport/dccp.py new file mode 100644 index 0000000000..d40d01da5e --- /dev/null +++ b/pcapkit/protocols/transport/dccp.py @@ -0,0 +1,1480 @@ +# -*- coding: utf-8 -*- +# mypy: disable-error-code=dict-item +"""DCCP - Datagram Congestion Control Protocol +================================================= + +.. module:: pcapkit.protocols.transport.dccp + +:mod:`pcapkit.protocols.transport.dccp` contains +:class:`~pcapkit.protocols.transport.dccp.DCCP` only, +which implements the extractor for Datagram Congestion +Control Protocol (DCCP) [*]_, whose generic header is +described as below: + +======= ========= ========================= ======================================= +Octets Bits Name Description +======= ========= ========================= ======================================= + 0 0 ``dccp.srcport`` Source Port + 2 16 ``dccp.dstport`` Destination Port + 4 32 ``dccp.hdr_len`` Data Offset + 5 40 ``dccp.ccval`` HC-Sender CCID Value (CCVal) + 5 44 ``dccp.cscov`` Checksum Coverage (CsCov) + 6 48 ``dccp.checksum`` Checksum + 8 64 ``dccp.reserved`` Reserved (Res) + 8 67 ``dccp.type`` Packet Type + 8 71 ``dccp.x`` Extended Sequence Numbers (X) + 9 72 ``dccp.seq_reserved`` Reserved (if X set) + 10 80 ``dccp.seq`` Sequence Number (48 bits if X set) +======= ========= ========================= ======================================= + +With ``X`` clear, the Sequence Number is 24 bits at octet 9 and the generic +header 12 octets long. The fields of the packet type, then the options, follow. + +.. [*] https://en.wikipedia.org/wiki/Datagram_Congestion_Control_Protocol + +.. versionadded:: 1.6.0 + +""" +import collections +from typing import TYPE_CHECKING, cast + +from pcapkit.const.dccp.ack_vector_state import AckVectorState as Enum_AckVectorState +from pcapkit.const.dccp.drop_code import DropCode as Enum_DropCode +from pcapkit.const.dccp.feature import Feature as Enum_Feature +from pcapkit.const.dccp.option import Option as Enum_Option +from pcapkit.const.dccp.packet_type import PacketType as Enum_PacketType +from pcapkit.const.dccp.reset_code import ResetCode as Enum_ResetCode +from pcapkit.const.reg.apptype import AppType as Enum_AppType +from pcapkit.const.reg.apptype import TransportProtocol as Enum_TransportProtocol +from pcapkit.const.reg.transtype import TransType as Enum_TransType +from pcapkit.corekit.module import ModuleDescriptor +from pcapkit.corekit.multidict import OrderedMultiDict +from pcapkit.protocols.data.transport.dccp import DCCP as Data_DCCP +from pcapkit.protocols.data.transport.dccp import AckVector as Data_AckVector +from pcapkit.protocols.data.transport.dccp import AckVectorRun as Data_AckVectorRun +from pcapkit.protocols.data.transport.dccp import Change as Data_Change +from pcapkit.protocols.data.transport.dccp import Confirm as Data_Confirm +from pcapkit.protocols.data.transport.dccp import DataChecksum as Data_DataChecksum +from pcapkit.protocols.data.transport.dccp import DataDropped as Data_DataDropped +from pcapkit.protocols.data.transport.dccp import DataDroppedBlock as Data_DataDroppedBlock +from pcapkit.protocols.data.transport.dccp import ElapsedTime as Data_ElapsedTime +from pcapkit.protocols.data.transport.dccp import InitCookie as Data_InitCookie +from pcapkit.protocols.data.transport.dccp import Mandatory as Data_Mandatory +from pcapkit.protocols.data.transport.dccp import NDPCount as Data_NDPCount +from pcapkit.protocols.data.transport.dccp import Padding as Data_Padding +from pcapkit.protocols.data.transport.dccp import SlowReceiver as Data_SlowReceiver +from pcapkit.protocols.data.transport.dccp import Timestamp as Data_Timestamp +from pcapkit.protocols.data.transport.dccp import TimestampEcho as Data_TimestampEcho +from pcapkit.protocols.data.transport.dccp import UnassignedOption as Data_UnassignedOption +from pcapkit.protocols.schema.schema import Schema +from pcapkit.protocols.schema.transport.dccp import ACK_TYPES +from pcapkit.protocols.schema.transport.dccp import DCCP as Schema_DCCP +from pcapkit.protocols.schema.transport.dccp import (DCCP_MIN_HEADER_LEN, + DCCP_SINGLE_OCTET_OPTION_MAX, RESET_TYPE, + SERVICE_TYPES) +from pcapkit.protocols.schema.transport.dccp import AckVector as Schema_AckVector +from pcapkit.protocols.schema.transport.dccp import Change as Schema_Change +from pcapkit.protocols.schema.transport.dccp import Confirm as Schema_Confirm +from pcapkit.protocols.schema.transport.dccp import DataChecksum as Schema_DataChecksum +from pcapkit.protocols.schema.transport.dccp import DataDropped as Schema_DataDropped +from pcapkit.protocols.schema.transport.dccp import ElapsedTime as Schema_ElapsedTime +from pcapkit.protocols.schema.transport.dccp import InitCookie as Schema_InitCookie +from pcapkit.protocols.schema.transport.dccp import Mandatory as Schema_Mandatory +from pcapkit.protocols.schema.transport.dccp import NDPCount as Schema_NDPCount +from pcapkit.protocols.schema.transport.dccp import Padding as Schema_Padding +from pcapkit.protocols.schema.transport.dccp import SlowReceiver as Schema_SlowReceiver +from pcapkit.protocols.schema.transport.dccp import Timestamp as Schema_Timestamp +from pcapkit.protocols.schema.transport.dccp import TimestampEcho as Schema_TimestampEcho +from pcapkit.protocols.schema.transport.dccp import UnassignedOption as Schema_UnassignedOption +from pcapkit.protocols.schema.transport.dccp import header_length +from pcapkit.protocols.transport.transport import Transport +from pcapkit.utilities import config +from pcapkit.utilities.exceptions import ProtocolError +from pcapkit.utilities.warnings import RegistryWarning, warn + +if TYPE_CHECKING: + from typing import Any, Callable, ClassVar, DefaultDict, Iterable, Optional, Type + + from mypy_extensions import DefaultArg, KwArg, NamedArg + from typing_extensions import Literal + + from pcapkit.corekit.enum import EnumRegistry + from pcapkit.protocols.data.transport.dccp import Option as Data_Option + from pcapkit.protocols.misc.checksum.ip_pseudo import IP_Pseudo + from pcapkit.protocols.protocol import ProtocolBase + from pcapkit.protocols.schema.transport.dccp import Option as Schema_Option + from pcapkit.protocols.schema.transport.dccp import TypeField as Schema_TypeField + + Option = OrderedMultiDict[Enum_Option, Data_Option] + OptionParser = Callable[[Schema_Option, NamedArg(Option, 'options')], Data_Option] + OptionConstructor = Callable[[Enum_Option, DefaultArg(Optional[Data_Option]), + KwArg(Any)], Schema_Option] + +__all__ = ['DCCP'] + +#: Longest a DCCP header can be: a Data Offset of 255 words, :rfc:`4340#section-5`. +DCCP_MAX_HEADER_LEN = 1020 + + +class DCCP(Transport[Data_DCCP, Schema_DCCP], + schema=Schema_DCCP, data=Data_DCCP): + """This class implements Datagram Congestion Control Protocol. + + No application protocol is registered in :attr:`self.__proto__ + `, so the application data + area is :class:`~pcapkit.protocols.misc.raw.Raw` unless one is. + + This class supports parsing of the following DCCP options, which are + directly mapped to the :class:`pcapkit.const.dccp.option.Option` + enumeration: + + .. list-table:: + :header-rows: 1 + + * - Option Code + - Option Parser + - Option Constructor + * - :attr:`~pcapkit.const.dccp.option.Option.Padding` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._read_opt_padding` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._make_opt_padding` + * - :attr:`~pcapkit.const.dccp.option.Option.Mandatory` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._read_opt_mandatory` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._make_opt_mandatory` + * - :attr:`~pcapkit.const.dccp.option.Option.Slow_Receiver` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._read_opt_slow_receiver` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._make_opt_slow_receiver` + * - :attr:`~pcapkit.const.dccp.option.Option.Change_L` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._read_opt_change` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._make_opt_change` + * - :attr:`~pcapkit.const.dccp.option.Option.Confirm_L` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._read_opt_confirm` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._make_opt_confirm` + * - :attr:`~pcapkit.const.dccp.option.Option.Change_R` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._read_opt_change` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._make_opt_change` + * - :attr:`~pcapkit.const.dccp.option.Option.Confirm_R` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._read_opt_confirm` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._make_opt_confirm` + * - :attr:`~pcapkit.const.dccp.option.Option.Init_Cookie` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._read_opt_init_cookie` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._make_opt_init_cookie` + * - :attr:`~pcapkit.const.dccp.option.Option.NDP_Count` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._read_opt_ndp_count` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._make_opt_ndp_count` + * - :attr:`~pcapkit.const.dccp.option.Option.Ack_Vector_Nonce_0` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._read_opt_ack_vector` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._make_opt_ack_vector` + * - :attr:`~pcapkit.const.dccp.option.Option.Ack_Vector_Nonce_1` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._read_opt_ack_vector` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._make_opt_ack_vector` + * - :attr:`~pcapkit.const.dccp.option.Option.Data_Dropped` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._read_opt_data_dropped` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._make_opt_data_dropped` + * - :attr:`~pcapkit.const.dccp.option.Option.Timestamp` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._read_opt_timestamp` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._make_opt_timestamp` + * - :attr:`~pcapkit.const.dccp.option.Option.Timestamp_Echo` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._read_opt_timestamp_echo` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._make_opt_timestamp_echo` + * - :attr:`~pcapkit.const.dccp.option.Option.Elapsed_Time` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._read_opt_elapsed_time` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._make_opt_elapsed_time` + * - :attr:`~pcapkit.const.dccp.option.Option.Data_Checksum` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._read_opt_data_checksum` + - :meth:`~pcapkit.protocols.transport.dccp.DCCP._make_opt_data_checksum` + + Any other option is kept as its type, length and data, through + :meth:`_read_opt_unassigned` and :meth:`_make_opt_unassigned`. + + The checksum is computed when :meth:`make` is given none, over the + ``pseudo`` it is given or by the enclosing IP layer, and verified by that + layer under :data:`~pcapkit.utilities.config.CHECKSUM_VERIFY`, over the + part of the packet the Checksum Coverage gives (:rfc:`4340#section-9`). + + """ + + ########################################################################## + # Defaults. + ########################################################################## + + #: Protocol index mapping for decoding next layer, see + #: :meth:`self._decode_next_layer + #: ` & + #: :meth:`self._import_next_layer + #: `. Keyed by + #: port number, and empty until a protocol is registered. + __proto__ = collections.defaultdict( + lambda: ModuleDescriptor('pcapkit.protocols.misc.raw', 'Raw'), + ) # type: DefaultDict[int, ModuleDescriptor[ProtocolBase] | Type[ProtocolBase]] + + #: What :attr:`__proto__` is keyed by: the enum a key must be a member of, + #: and whether a bare :class:`int` is a valid key there too, as for + #: :attr:`TCP.__proto_key__ `. + __proto_key__: 'ClassVar[tuple[Type[EnumRegistry], bool]]' = (Enum_AppType, True) + + #: Option type to method mapping, see :meth:`_read_dccp_options` and + #: :meth:`_make_dccp_options`. A :class:`str` value names the methods + #: ``_read_opt_${name}`` and ``_make_opt_${name}``, falling back to + #: :meth:`_read_opt_unassigned` and :meth:`_make_opt_unassigned` when the + #: class has no such method; any other value is a ``(parser, constructor)`` + #: pair of callables. + __option__ = collections.defaultdict( + lambda: 'unassigned', + { + Enum_Option.Padding: 'padding', # [RFC 4340] Padding + Enum_Option.Mandatory: 'mandatory', # [RFC 4340] Mandatory + Enum_Option.Slow_Receiver: 'slow_receiver', # [RFC 4340] Slow Receiver + Enum_Option.Change_L: 'change', # [RFC 4340] Change L + Enum_Option.Confirm_L: 'confirm', # [RFC 4340] Confirm L + Enum_Option.Change_R: 'change', # [RFC 4340] Change R + Enum_Option.Confirm_R: 'confirm', # [RFC 4340] Confirm R + Enum_Option.Init_Cookie: 'init_cookie', # [RFC 4340] Init Cookie + Enum_Option.NDP_Count: 'ndp_count', # [RFC 4340] NDP Count + Enum_Option.Ack_Vector_Nonce_0: 'ack_vector', # [RFC 4340] Ack Vector [Nonce 0] + Enum_Option.Ack_Vector_Nonce_1: 'ack_vector', # [RFC 4340] Ack Vector [Nonce 1] + Enum_Option.Data_Dropped: 'data_dropped', # [RFC 4340] Data Dropped + Enum_Option.Timestamp: 'timestamp', # [RFC 4340] Timestamp + Enum_Option.Timestamp_Echo: 'timestamp_echo', # [RFC 4340] Timestamp Echo + Enum_Option.Elapsed_Time: 'elapsed_time', # [RFC 4340] Elapsed Time + Enum_Option.Data_Checksum: 'data_checksum', # [RFC 4340] Data Checksum + }, + ) # type: DefaultDict[int, str | tuple[OptionParser, OptionConstructor]] + + #: Whether :meth:`make` left the checksum for the enclosing IP layer to + #: compute, see :mod:`pcapkit.protocols.misc.checksum.ip_pseudo`. + _checksum_pending = False + + ########################################################################## + # Properties. + ########################################################################## + + @property + def name(self) -> 'Literal["Datagram Congestion Control Protocol"]': + """Name of current protocol.""" + return 'Datagram Congestion Control Protocol' + + @property + def length(self) -> 'int': + """Header length of current protocol.""" + return self._info.hdr_len + + @property + def src(self) -> 'Enum_AppType': + """Source port.""" + return self._info.srcport + + @property + def dst(self) -> 'Enum_AppType': + """Destination port.""" + return self._info.dstport + + ########################################################################## + # Methods. + ########################################################################## + + def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_DCCP': # pylint: disable=unused-argument + """Read Datagram Congestion Control Protocol (DCCP). + + Structure of DCCP generic header with ``X`` set [:rfc:`4340`]:: + + 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 | Dest Port | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Data Offset | CCVal | CsCov | Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | |X| | . + | Res | Type |=| Reserved | Sequence Number (high bits) . + | | |1| | . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + . Sequence Number (low bits) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + The Acknowledgement Number Subheader of every packet type but + DCCP-Request, DCCP-Data and DCCP-Listen follows, then the Service Code + of a DCCP-Request, DCCP-Response or DCCP-Listen (:rfc:`5596`), or the + Reset Code and Data octets of a DCCP-Reset, then the options. + + Args: + length: Length of packet data. + **kwargs: Arbitrary keyword arguments. + + Returns: + Parsed packet data. + + Raises: + ProtocolError: If the Data Offset is smaller than the header the + packet type has, or declares more header than the data holds + (:rfc:`4340#section-5.1`). + + """ + if length is None: + length = len(self) + schema = self.__header__ + + code = schema.type['type'] + dccp = Data_DCCP( + srcport=schema.srcport, + dstport=schema.dstport, + hdr_len=schema.offset * 4, + ccval=schema.cscov['ccval'], + cscov=schema.cscov['cscov'], + checksum=schema.checksum, + reserved=schema.type['reserved'], + type=Enum_PacketType.get(code), + x=bool(schema.type['x']), + seq=schema.seq, + ) + + fixed = header_length(schema.type) + if dccp.hdr_len < fixed: + raise ProtocolError(f'{self.alias}: header length {dccp.hdr_len} is shorter than the ' + f'{fixed} octets of a {dccp.type.name}') + if len(self) < dccp.hdr_len: + raise ProtocolError(f'{self.alias}: header length {dccp.hdr_len} runs past the end of ' + f'the data ({len(self)} octets)') + + if dccp.x: + dccp.__update__(seq_reserved=schema.seq_reserved) + if code in ACK_TYPES: + dccp.__update__(ack_reserved=schema.ack_reserved, ack=schema.ack) + if code in SERVICE_TYPES: + dccp.__update__(service_code=schema.service_code) + if code == RESET_TYPE: + dccp.__update__(reset_code=Enum_ResetCode.get(schema.reset_code), + reset_data=schema.reset_data) + if dccp.hdr_len > fixed: + dccp.__update__(options=self._read_dccp_options()) + + return self._decode_next_layer(dccp, (dccp.srcport.port, dccp.dstport.port), length - dccp.hdr_len) + + def make(self, + srcport: 'Enum_AppType | int' = 0, + dstport: 'Enum_AppType | int' = 0, + ccval: 'int' = 0, + cscov: 'int' = 0, + checksum: 'Optional[bytes]' = None, + reserved: 'int' = 0, + type: 'Enum_PacketType | int' = Enum_PacketType.DCCP_Request, # pylint: disable=redefined-builtin + x: 'bool' = True, + seq_reserved: 'int' = 0, + seq: 'int' = 0, + ack_reserved: 'int' = 0, + ack: 'int' = 0, + service_code: 'int' = 0, + reset_code: 'Enum_ResetCode | int' = Enum_ResetCode.Unspecified, + reset_data: 'bytes' = bytes(3), + options: 'Optional[list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes] | Option]' = None, # pylint: disable=line-too-long + payload: 'bytes | ProtocolBase | Schema' = b'', + pseudo: 'Optional[IP_Pseudo]' = None, + **kwargs: 'Any') -> 'Schema_DCCP': + """Make (construct) packet data. + + Args: + srcport: Source port. + dstport: Destination port. + ccval: Value used by the HC-Sender CCID. + cscov: Checksum coverage, see :rfc:`4340#section-9.2`; written as + zero where it reaches the end of the packet and the checksum is + computed, as Linux did. + checksum: Checksum; if :data:`None` and + :data:`~pcapkit.utilities.config.CHECKSUM_COMPUTE` is on, + computed over ``pseudo`` and the part of the packet ``cscov`` + gives, or left to an IP layer this packet is the payload of; + zero otherwise. + reserved: Reserved bits (``Res``). + type: Packet type. + x: Whether the sequence and acknowledgement numbers are 48 bits + rather than 24 bits. + seq_reserved: Reserved octet ahead of a 48-bit sequence number. + seq: Sequence number. + ack_reserved: Reserved bits of the acknowledgement number + subheader, in a packet type that carries one. + ack: Acknowledgement number, in a packet type that carries one. + service_code: Service code of a DCCP-Request, DCCP-Response or + DCCP-Listen. + reset_code: Reset code of a DCCP-Reset. + reset_data: Data 1, Data 2 and Data 3 of a DCCP-Reset. + options: DCCP options, padded with Padding options to a multiple + of 32 bits. + payload: Payload, i.e. the application data area. + pseudo: Pseudo-header of the enclosing IP layer; its length is + replaced by this packet's own. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed packet data. + + Raises: + ProtocolError: If ``srcport`` or ``dstport`` is a :obj:`bool`, the + header with its options runs past the 1020 octets a Data Offset + can give, or an option maker rejects its option. + EnumValueError: If ``type`` is out of its 4-bit range. + + """ + code = Enum_PacketType.get(type) + type_field = { + 'reserved': reserved, + 'type': int(code), + 'x': int(x), + } # type: Schema_TypeField + + if options is not None: + options_value, options_length = self._make_dccp_options(options) + else: + options_value, options_length = [], 0 + + hdr_len = header_length(type_field) + options_length + if hdr_len > DCCP_MAX_HEADER_LEN: + raise ProtocolError(f'{self.alias}: header length {hdr_len} exceeds ' + f'{DCCP_MAX_HEADER_LEN} octets') + + schema = Schema_DCCP( + srcport=self._make_port(srcport, Enum_TransportProtocol.dccp), + dstport=self._make_port(dstport, Enum_TransportProtocol.dccp), + offset=hdr_len // 4, + cscov={ + 'ccval': ccval, + 'cscov': cscov, + }, + checksum=b'\x00\x00' if checksum is None else checksum, + type=type_field, + seq_reserved=seq_reserved, + seq=seq, + ack_reserved=ack_reserved, + ack=ack, + service_code=service_code, + reset_code=reset_code, + reset_data=reset_data, + options=options_value, + payload=payload, + ) + + self._checksum_pending = False + if checksum is None and config.CHECKSUM_COMPUTE: + if pseudo is None: + self._checksum_pending = True + else: + from pcapkit.corekit.checksum.internet import compute_checksum # isort: skip # pylint: disable=import-outside-toplevel + from pcapkit.protocols.misc.checksum.ip_pseudo import pseudo_header # isort: skip # pylint: disable=import-outside-toplevel + + octets = schema.pack() + coverage = self._checksum_coverage(cscov, hdr_len, len(octets)) + # NOTE: A coverage reaching the end is written as CsCov 0, as Linux's + # dccp_csum_outgoing did, so a computed checksum never covers past it. + if cscov and coverage >= len(octets): + schema.cscov = {'ccval': ccval, 'cscov': 0} + octets, coverage = schema.pack(), len(octets) + schema.checksum = compute_checksum(octets[:coverage], + prefix=pseudo_header(pseudo, len(octets))) + return schema + + @classmethod + def register_option(cls, code: 'Enum_Option', meth: 'str | tuple[OptionParser, OptionConstructor]') -> 'None': + """Register an option parser. + + Args: + code: DCCP option type. + meth: Method name, or a ``(parser, constructor)`` pair of + callables, to parse and construct the option; see + :attr:`__option__`. + + Warns: + RegistryWarning: If a different entry is already registered for + ``code``; it is overwritten. Handing back the entry already + stored, or one equal to it, is a silent no-op. + + """ + incumbent = cls.__option__.get(code) + if incumbent is not None and incumbent == meth: + return + if incumbent is not None: + warn(f'option {code} already registered, overwriting', RegistryWarning) + cls.__option__[code] = meth + + ########################################################################## + # Data models. + ########################################################################## + + def __length_hint__(self) -> 'Literal[12]': + """Return an estimated length for the object.""" + return DCCP_MIN_HEADER_LEN # type: ignore[return-value] + + @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.DCCP # type: ignore[return-value] + + ########################################################################## + # Utilities. + ########################################################################## + + @classmethod + def _make_data(cls, data: 'Data_DCCP') -> '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, + 'ccval': data.ccval, + 'cscov': data.cscov, + 'checksum': data.checksum, + 'reserved': data.reserved, + 'type': data.type, + 'x': data.x, + 'seq_reserved': data.get('seq_reserved', 0), + 'seq': data.seq, + 'ack_reserved': data.get('ack_reserved', 0), + 'ack': data.get('ack', 0), + 'service_code': data.get('service_code', 0), + 'reset_code': data.get('reset_code', Enum_ResetCode.Unspecified), + 'reset_data': data.get('reset_data', bytes(3)), + 'options': data.get('options'), + 'payload': cls._make_payload(data), + } + + def _read_dccp_options(self) -> 'Option': + """Read DCCP option list. + + Returns: + Extracted DCCP options. + + """ + options = OrderedMultiDict() # type: Option + for schema in self.__header__.options: + code = schema.type + name = self._lookup_registry(self.__option__, code) + + if isinstance(name, str): + meth = cast('OptionParser', + getattr(self, f'_read_opt_{name}', self._read_opt_unassigned)) + else: + meth = name[0] + options.add(code, meth(schema, options=options)) + return options + + def _make_dccp_options(self, options: 'list[Schema_Option | tuple[Enum_Option, dict[str, Any]] | bytes] | Option') -> 'tuple[list[Schema_Option | bytes], int]': # pylint: disable=line-too-long + """Make options for DCCP. + + Args: + options: DCCP options + + Returns: + Tuple of options and total length of options. + + Note: + The options are emitted in the given order, Padding included, so + that a parsed option list re-emits the same octets. Padding + options are added after them only when the list does not end on a + 32-bit boundary (:rfc:`4340#section-5.8`). + + """ + options_list = [] # type: list[Schema_Option | bytes] + if isinstance(options, list): + for item in options: + if isinstance(item, (bytes, Schema)): + options_list.append(item) + else: + code, args = cast('tuple[Enum_Option, dict[str, Any]]', item) + options_list.append(self._option_maker(code)(code, **args)) + else: + for code, option in options.items(multi=True): + options_list.append(self._option_maker(code)(code, option)) + + total_length = sum(len(item) if isinstance(item, bytes) else len(item.pack()) + for item in options_list) + if total_length % 4: + pad_len = 4 - total_length % 4 + options_list.extend(self._make_opt_padding(Enum_Option.Padding) # type: ignore[arg-type] + for _ in range(pad_len)) + total_length += pad_len + return options_list, total_length + + @staticmethod + def _checksum_coverage(cscov: 'int', hdr_len: 'int', length: 'int') -> 'int': + """Octets the checksum covers, pseudo-header aside (:rfc:`4340#section-9.2`). + + Args: + cscov: Checksum coverage. + hdr_len: Header length. + length: Length of the packet. + + Returns: + ``length`` if ``cscov`` is zero; otherwise the header and the first + ``(cscov - 1) * 4`` octets of the application data, which may run + past ``length``. + + """ + if not cscov: + return length + return hdr_len + (cscov - 1) * 4 + + def _read_opt_unassigned(self, schema: 'Schema_UnassignedOption', *, options: 'Option') -> 'Data_UnassignedOption': # pylint: disable=unused-argument + """Read DCCP unassigned option. + + Args: + schema: parsed option schema + options: extracted DCCP options + + Returns: + Parsed option data. + + """ + return Data_UnassignedOption( + type=schema.type, + length=schema.length, + data=schema.data if schema.type > DCCP_SINGLE_OCTET_OPTION_MAX else b'', + ) + + def _read_opt_padding(self, schema: 'Schema_Padding', *, options: 'Option') -> 'Data_Padding': # pylint: disable=unused-argument + """Read DCCP padding option. + + Structure of DCCP padding option [:rfc:`4340#section-5.8.1`]: + + .. code-block:: text + + +--------+ + |00000000| + +--------+ + Type=0 + + Args: + schema: parsed option schema + options: extracted DCCP options + + Returns: + Parsed option data. + + """ + return Data_Padding( + type=schema.type, + length=1, + ) + + def _read_opt_mandatory(self, schema: 'Schema_Mandatory', *, options: 'Option') -> 'Data_Mandatory': # pylint: disable=unused-argument + """Read DCCP mandatory option. + + Structure of DCCP mandatory option [:rfc:`4340#section-5.8.2`]: + + .. code-block:: text + + +--------+ + |00000001| + +--------+ + Type=1 + + Args: + schema: parsed option schema + options: extracted DCCP options + + Returns: + Parsed option data. + + """ + return Data_Mandatory( + type=schema.type, + length=1, + ) + + def _read_opt_slow_receiver(self, schema: 'Schema_SlowReceiver', *, options: 'Option') -> 'Data_SlowReceiver': # pylint: disable=unused-argument + """Read DCCP slow receiver option. + + Structure of DCCP slow receiver option [:rfc:`4340#section-11.6`]: + + .. code-block:: text + + +--------+ + |00000010| + +--------+ + Type=2 + + Args: + schema: parsed option schema + options: extracted DCCP options + + Returns: + Parsed option data. + + """ + return Data_SlowReceiver( + type=schema.type, + length=1, + ) + + def _read_opt_change(self, schema: 'Schema_Change', *, options: 'Option') -> 'Data_Change': # pylint: disable=unused-argument + """Read DCCP Change L or Change R option. + + Structure of DCCP Change options [:rfc:`4340#section-6.1`]: + + .. code-block:: text + + +--------+--------+--------+--------+-------- + | Type | Length |Feature#| Value(s) ... + +--------+--------+--------+--------+-------- + Type=32 or 34 + + Args: + schema: parsed option schema + options: extracted DCCP options + + Returns: + Parsed option data. + + """ + return Data_Change( + type=schema.type, + length=schema.length, + feature=Enum_Feature.get(schema.feature) if schema.length > 2 else None, + value=schema.value, + ) + + def _read_opt_confirm(self, schema: 'Schema_Confirm', *, options: 'Option') -> 'Data_Confirm': # pylint: disable=unused-argument + """Read DCCP Confirm L or Confirm R option. + + Structure of DCCP Confirm options [:rfc:`4340#section-6.2`]: + + .. code-block:: text + + +--------+--------+--------+--------+-------- + | Type | Length |Feature#| Value(s) ... + +--------+--------+--------+--------+-------- + Type=33 or 35 + + Args: + schema: parsed option schema + options: extracted DCCP options + + Returns: + Parsed option data. + + """ + return Data_Confirm( + type=schema.type, + length=schema.length, + feature=Enum_Feature.get(schema.feature) if schema.length > 2 else None, + value=schema.value, + ) + + def _read_opt_init_cookie(self, schema: 'Schema_InitCookie', *, options: 'Option') -> 'Data_InitCookie': # pylint: disable=unused-argument + """Read DCCP init cookie option. + + Structure of DCCP init cookie option [:rfc:`4340#section-8.1.4`]: + + .. code-block:: text + + +--------+--------+--------+--------+--------+-------- + |00100100| Length | Init Cookie Value ... + +--------+--------+--------+--------+--------+-------- + Type=36 + + Args: + schema: parsed option schema + options: extracted DCCP options + + Returns: + Parsed option data. + + """ + return Data_InitCookie( + type=schema.type, + length=schema.length, + cookie=schema.cookie, + ) + + def _read_opt_ndp_count(self, schema: 'Schema_NDPCount', *, options: 'Option') -> 'Data_NDPCount': # pylint: disable=unused-argument + """Read DCCP NDP count option. + + Structure of DCCP NDP count option [:rfc:`4340#section-7.7`]: + + .. code-block:: text + + +--------+--------+-------- ... --------+ + |00100101| Length | NDP Count | + +--------+--------+-------- ... --------+ + Type=37 Len=3-8 (1-6 bytes) + + Args: + schema: parsed option schema + options: extracted DCCP options + + Returns: + Parsed option data. + + """ + return Data_NDPCount( + type=schema.type, + length=schema.length, + count=schema.count, + ) + + def _read_opt_ack_vector(self, schema: 'Schema_AckVector', *, options: 'Option') -> 'Data_AckVector': # pylint: disable=unused-argument + """Read DCCP Ack Vector option. + + Structure of DCCP Ack Vector options [:rfc:`4340#section-11.4`]: + + .. code-block:: text + + +--------+--------+--------+--------+--------+-------- + |0010011?| Length |SSLLLLLL|SSLLLLLL|SSLLLLLL| ... + +--------+--------+--------+--------+--------+-------- + Type=38/39 \\___________ Vector ___________... + + Args: + schema: parsed option schema + options: extracted DCCP options + + Returns: + Parsed option data. + + """ + return Data_AckVector( + type=schema.type, + length=schema.length, + nonce=schema.type - Enum_Option.Ack_Vector_Nonce_0, + vector=tuple(Data_AckVectorRun( + state=Enum_AckVectorState.get(octet >> 6), + run_length=octet & 0x3F, + ) for octet in schema.vector), + ) + + def _read_opt_data_dropped(self, schema: 'Schema_DataDropped', *, options: 'Option') -> 'Data_DataDropped': # pylint: disable=unused-argument + """Read DCCP data dropped option. + + Structure of DCCP data dropped option [:rfc:`4340#section-11.7`]: + + .. code-block:: text + + +--------+--------+--------+--------+--------+-------- + |00101000| Length | Block | Block | Block | ... + +--------+--------+--------+--------+--------+-------- + Type=40 \\___________ Vector ___________ ... + + A block is a normal block, ``0`` then a 7-bit run length, or a drop + block, ``1`` then a 3-bit drop code and a 4-bit run length. + + Args: + schema: parsed option schema + options: extracted DCCP options + + Returns: + Parsed option data. + + """ + return Data_DataDropped( + type=schema.type, + length=schema.length, + vector=tuple(Data_DataDroppedBlock( + dropped=True, + code=Enum_DropCode.get((octet >> 4) & 0x07), + run_length=octet & 0x0F, + ) if octet & 0x80 else Data_DataDroppedBlock( + dropped=False, + code=None, + run_length=octet, + ) for octet in schema.vector), + ) + + def _read_opt_timestamp(self, schema: 'Schema_Timestamp', *, options: 'Option') -> 'Data_Timestamp': # pylint: disable=unused-argument + """Read DCCP timestamp option. + + Structure of DCCP timestamp option [:rfc:`4340#section-13.1`]: + + .. code-block:: text + + +--------+--------+--------+--------+--------+--------+ + |00101001|00000110| Timestamp Value | + +--------+--------+--------+--------+--------+--------+ + Type=41 Length=6 + + Args: + schema: parsed option schema + options: extracted DCCP options + + Returns: + Parsed option data. + + """ + return Data_Timestamp( + type=schema.type, + length=schema.length, + timestamp=schema.timestamp, + ) + + def _read_opt_timestamp_echo(self, schema: 'Schema_TimestampEcho', *, options: 'Option') -> 'Data_TimestampEcho': # pylint: disable=unused-argument + """Read DCCP timestamp echo option. + + Structure of DCCP timestamp echo option [:rfc:`4340#section-13.3`]: + + .. code-block:: text + + +--------+--------+--------+--------+--------+--------+ + |00101010| Length | Timestamp Echo | + +--------+--------+--------+--------+--------+--------+ + Type=42 + +--------+--------+------- ... -------+ + | Elapsed Time | (6 or 8 octets) | (Length 8 or 10) + +--------+--------+------- ... -------+ + + Args: + schema: parsed option schema + options: extracted DCCP options + + Returns: + Parsed option data. + + """ + return Data_TimestampEcho( + type=schema.type, + length=schema.length, + timestamp=schema.timestamp, + elapsed=schema.elapsed if schema.length > 6 else None, + ) + + def _read_opt_elapsed_time(self, schema: 'Schema_ElapsedTime', *, options: 'Option') -> 'Data_ElapsedTime': # pylint: disable=unused-argument + """Read DCCP elapsed time option. + + Structure of DCCP elapsed time option [:rfc:`4340#section-13.2`]: + + .. code-block:: text + + +--------+--------+--------+--------+ + |00101011|00000100| Elapsed Time | + +--------+--------+--------+--------+ + Type=43 Len=4 + +--------+--------+--------+--------+--------+--------+ + |00101011|00000110| Elapsed Time | + +--------+--------+--------+--------+--------+--------+ + Type=43 Len=6 + + Args: + schema: parsed option schema + options: extracted DCCP options + + Returns: + Parsed option data. + + """ + return Data_ElapsedTime( + type=schema.type, + length=schema.length, + elapsed=schema.elapsed, + ) + + def _read_opt_data_checksum(self, schema: 'Schema_DataChecksum', *, options: 'Option') -> 'Data_DataChecksum': # pylint: disable=unused-argument + """Read DCCP data checksum option. + + Structure of DCCP data checksum option [:rfc:`4340#section-9.3`]: + + .. code-block:: text + + +--------+--------+--------+--------+--------+--------+ + |00101100|00000110| CRC-32c | + +--------+--------+--------+--------+--------+--------+ + Type=44 Length=6 + + Args: + schema: parsed option schema + options: extracted DCCP options + + Returns: + Parsed option data. + + """ + return Data_DataChecksum( + type=schema.type, + length=schema.length, + checksum=schema.checksum, + ) + + def _make_opt_unassigned(self, code: 'Enum_Option', opt: 'Optional[Data_UnassignedOption]' = None, *, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_UnassignedOption': + """Make DCCP unassigned option. + + Args: + code: option type + opt: option data + data: option data in :obj:`bytes`, none for a single-octet type + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + Raises: + ProtocolError: If ``data`` is given for a single-octet type. + + """ + if opt is not None: + data = opt.data + + if code <= DCCP_SINGLE_OCTET_OPTION_MAX: + if data: + raise ProtocolError(f'{self.alias}: [Opt {code}] single-octet option carries no data') + return Schema_UnassignedOption( + type=code, + length=1, + ) + return Schema_UnassignedOption( + type=code, + length=self._option_length(code, len(data) + 2), + data=data, + ) + + def _make_opt_padding(self, code: 'Enum_Option', opt: 'Optional[Data_Padding]' = None, + **kwargs: 'Any') -> 'Schema_Padding': + """Make DCCP padding option. + + Args: + code: option type + opt: option data + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + """ + return Schema_Padding( + type=code, + length=1, + ) + + def _make_opt_mandatory(self, code: 'Enum_Option', opt: 'Optional[Data_Mandatory]' = None, + **kwargs: 'Any') -> 'Schema_Mandatory': + """Make DCCP mandatory option. + + Args: + code: option type + opt: option data + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + """ + return Schema_Mandatory( + type=code, + length=1, + ) + + def _make_opt_slow_receiver(self, code: 'Enum_Option', opt: 'Optional[Data_SlowReceiver]' = None, + **kwargs: 'Any') -> 'Schema_SlowReceiver': + """Make DCCP slow receiver option. + + Args: + code: option type + opt: option data + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + """ + return Schema_SlowReceiver( + type=code, + length=1, + ) + + def _make_opt_change(self, code: 'Enum_Option', opt: 'Optional[Data_Change]' = None, *, + feature: 'Optional[Enum_Feature | int]' = Enum_Feature.Congestion_Control_ID, + value: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_Change': + """Make DCCP Change L or Change R option. + + Args: + code: option type + opt: option data + feature: feature number, none if :data:`None` + value: preference list or value, as on the wire + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + Raises: + ProtocolError: If ``value`` is given without a ``feature``. + + """ + if opt is not None: + feature, value = opt.feature, opt.value + + if feature is None: + if value: + raise ProtocolError(f'{self.alias}: [Opt {code}] value without a feature number') + return Schema_Change( + type=code, + length=2, + value=b'', + ) + return Schema_Change( + type=code, + length=self._option_length(code, len(value) + 3), + feature=feature, + value=value, + ) + + def _make_opt_confirm(self, code: 'Enum_Option', opt: 'Optional[Data_Confirm]' = None, *, + feature: 'Optional[Enum_Feature | int]' = Enum_Feature.Congestion_Control_ID, + value: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_Confirm': + """Make DCCP Confirm L or Confirm R option. + + Args: + code: option type + opt: option data + feature: feature number, none if :data:`None` + value: value, then the preference list for a server-priority + feature, as on the wire + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + Raises: + ProtocolError: If ``value`` is given without a ``feature``. + + """ + if opt is not None: + feature, value = opt.feature, opt.value + + if feature is None: + if value: + raise ProtocolError(f'{self.alias}: [Opt {code}] value without a feature number') + return Schema_Confirm( + type=code, + length=2, + value=b'', + ) + return Schema_Confirm( + type=code, + length=self._option_length(code, len(value) + 3), + feature=feature, + value=value, + ) + + def _make_opt_init_cookie(self, code: 'Enum_Option', opt: 'Optional[Data_InitCookie]' = None, *, + cookie: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_InitCookie': + """Make DCCP init cookie option. + + Args: + code: option type + opt: option data + cookie: cookie + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + """ + if opt is not None: + cookie = opt.cookie + + return Schema_InitCookie( + type=code, + length=self._option_length(code, len(cookie) + 2), + cookie=cookie, + ) + + def _make_opt_ndp_count(self, code: 'Enum_Option', opt: 'Optional[Data_NDPCount]' = None, *, + count: 'int' = 0, + **kwargs: 'Any') -> 'Schema_NDPCount': + """Make DCCP NDP count option. + + Args: + code: option type + opt: option data, whose length is kept, even past the 8 octets + :rfc:`4340#section-7.7` allows + count: count of consecutive non-data packets, written in as few + octets as hold it + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + Raises: + ProtocolError: If ``count`` does not fit six octets. + + """ + if opt is not None: + count, length = opt.count, opt.length + else: + length = max((count.bit_length() + 7) // 8, 1) + 2 + if length > 8: + raise ProtocolError(f'{self.alias}: [Opt {code}] NDP count {count} does not fit 6 octets') + + return Schema_NDPCount( + type=code, + length=length, + count=count, + ) + + def _make_opt_ack_vector(self, code: 'Enum_Option', opt: 'Optional[Data_AckVector]' = None, *, + vector: 'Iterable[Data_AckVectorRun | tuple[Enum_AckVectorState | int, int]]' = (), # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_AckVector': + """Make DCCP Ack Vector option. + + Args: + code: option type, whose nonce the option carries + opt: option data + vector: runs, each a :class:`~pcapkit.protocols.data.transport.dccp.AckVectorRun` + or a ``(state, run_length)`` pair + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + Raises: + ProtocolError: If a state does not fit 2 bits or a run length + 6 bits. + + """ + if opt is not None: + vector = opt.vector + + octets = bytearray() + for run in vector: + state, run_length = (run.state, run.run_length) if isinstance(run, Data_AckVectorRun) else run + if not (0 <= state <= 0x03 and 0 <= run_length <= 0x3F): + raise ProtocolError(f'{self.alias}: [Opt {code}] invalid Ack Vector run ' + f'({state}, {run_length})') + octets.append((state << 6) | run_length) + + return Schema_AckVector( + type=code, + length=self._option_length(code, len(octets) + 2), + vector=bytes(octets), + ) + + def _make_opt_data_dropped(self, code: 'Enum_Option', opt: 'Optional[Data_DataDropped]' = None, *, + vector: 'Iterable[Data_DataDroppedBlock | tuple[Optional[Enum_DropCode | int], int]]' = (), # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_DataDropped': + """Make DCCP data dropped option. + + Args: + code: option type + opt: option data + vector: blocks, each a + :class:`~pcapkit.protocols.data.transport.dccp.DataDroppedBlock` + or a ``(code, run_length)`` pair, ``code`` being :data:`None` + for a normal block + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + Raises: + ProtocolError: If a run length does not fit 7 bits in a normal + block, or a drop code 3 bits or a run length 4 bits in a drop + block. + + """ + if opt is not None: + vector = opt.vector + + octets = bytearray() + for block in vector: + drop, run_length = (block.code, block.run_length) if isinstance(block, Data_DataDroppedBlock) else block + if drop is None and 0 <= run_length <= 0x7F: + octets.append(run_length) + elif drop is not None and 0 <= drop <= 0x07 and 0 <= run_length <= 0x0F: + octets.append(0x80 | (drop << 4) | run_length) + else: + raise ProtocolError(f'{self.alias}: [Opt {code}] invalid Data Dropped block ' + f'({drop}, {run_length})') + + return Schema_DataDropped( + type=code, + length=self._option_length(code, len(octets) + 2), + vector=bytes(octets), + ) + + def _make_opt_timestamp(self, code: 'Enum_Option', opt: 'Optional[Data_Timestamp]' = None, *, + timestamp: 'int' = 0, + **kwargs: 'Any') -> 'Schema_Timestamp': + """Make DCCP timestamp option. + + Args: + code: option type + opt: option data + timestamp: timestamp, in units of 10 microseconds + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + """ + if opt is not None: + timestamp, length = opt.timestamp, opt.length + else: + length = 6 + + return Schema_Timestamp( + type=code, + length=length, + timestamp=timestamp, + ) + + def _make_opt_timestamp_echo(self, code: 'Enum_Option', opt: 'Optional[Data_TimestampEcho]' = None, *, + timestamp: 'int' = 0, + elapsed: 'Optional[int]' = None, + **kwargs: 'Any') -> 'Schema_TimestampEcho': + """Make DCCP timestamp echo option. + + Args: + code: option type + opt: option data + timestamp: timestamp echoed + elapsed: elapsed time, in hundredths of milliseconds, in two + octets if it fits and four otherwise; none if :data:`None` + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + """ + if opt is not None: + timestamp, elapsed, length = opt.timestamp, opt.elapsed, opt.length + elif elapsed is None: + length = 6 + else: + length = 8 if elapsed <= 0xFFFF else 10 + + return Schema_TimestampEcho( + type=code, + length=length, + timestamp=timestamp, + elapsed=elapsed, + ) + + def _make_opt_elapsed_time(self, code: 'Enum_Option', opt: 'Optional[Data_ElapsedTime]' = None, *, + elapsed: 'int' = 0, + **kwargs: 'Any') -> 'Schema_ElapsedTime': + """Make DCCP elapsed time option. + + Args: + code: option type + opt: option data + elapsed: elapsed time, in hundredths of milliseconds, in two + octets if it fits and four otherwise + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + """ + if opt is not None: + elapsed, length = opt.elapsed, opt.length + else: + length = 4 if elapsed <= 0xFFFF else 6 + + return Schema_ElapsedTime( + type=code, + length=length, + elapsed=elapsed, + ) + + def _make_opt_data_checksum(self, code: 'Enum_Option', opt: 'Optional[Data_DataChecksum]' = None, *, + checksum: 'bytes' = bytes(4), + **kwargs: 'Any') -> 'Schema_DataChecksum': + """Make DCCP data checksum option. + + Args: + code: option type + opt: option data + checksum: CRC-32c of the application data, as on the wire + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + """ + if opt is not None: + checksum = opt.checksum + + return Schema_DataChecksum( + type=code, + length=self._option_length(code, len(checksum) + 2), + checksum=checksum, + ) + + def _verify_pseudo_checksum(self, pseudo: 'Optional[IP_Pseudo]') -> 'None': + """Verify the checksum over the pseudo-header of the enclosing IP layer. + + That layer calls this once parsed, see + :func:`~pcapkit.protocols.misc.checksum.ip_pseudo.verify_upper`. The + checksum covers what the Checksum Coverage gives; one covering past the + end of the packet is invalid (:rfc:`4340#section-9.2`). + + Args: + pseudo: Pseudo-header, or :data:`None` if the IP layer gives none. + + Warns: + ChecksumWarning: If the checksum or its coverage is invalid. + + """ + from pcapkit.corekit.checksum.internet import ChecksumStatus, _rebuilding, verify_checksum # isort: skip # pylint: disable=import-outside-toplevel + from pcapkit.utilities.exceptions import stacklevel # isort: skip # pylint: disable=import-outside-toplevel + from pcapkit.utilities.warnings import ChecksumWarning # isort: skip # pylint: disable=import-outside-toplevel + + if pseudo is None: + status = ChecksumStatus.NO_PSEUDO_HEADER + else: + length = pseudo.info.len + coverage = self._checksum_coverage(self._info.cscov, self._info.hdr_len, length) + if coverage <= length: + status = verify_checksum(self.alias, self._data, self._info.checksum, + length=coverage, prefix=pseudo.data) + else: + if not _rebuilding.get(): + warn(f'{self.alias}: checksum coverage {self._info.cscov} runs past the ' + f'{length} octets of the packet', ChecksumWarning, stacklevel=stacklevel()) + status = ChecksumStatus.INVALID + self._info.__update__(checksum_status=status) + + def _option_maker(self, code: 'Enum_Option') -> 'OptionConstructor': + """The constructor :attr:`__option__` holds for ``code``. + + Args: + code: option type + + Returns: + The ``_make_opt_*`` method named, or the callable registered. + + """ + name = self._lookup_registry(self.__option__, code) + if isinstance(name, str): + return cast('OptionConstructor', getattr(self, f'_make_opt_{name}', self._make_opt_unassigned)) + return name[1] + + def _option_length(self, code: 'Enum_Option', length: 'int') -> 'int': + """Check an option length fits its octet. + + Args: + code: option type + length: option length + + Returns: + ``length``. + + Raises: + ProtocolError: If ``length`` exceeds 255. + + """ + if length > 0xFF: + raise ProtocolError(f'{self.alias}: [Opt {code}] option length {length} exceeds 255') + return length diff --git a/pcapkit/utilities/config.py b/pcapkit/utilities/config.py index 865627d885..9de3fd4157 100644 --- a/pcapkit/utilities/config.py +++ b/pcapkit/utilities/config.py @@ -414,7 +414,7 @@ def read_flag(name: 'str', default: 'bool' = False) -> 'bool': ############################################################################### #: bool: Whether parsing verifies the one's-complement checksums, of the IPv4 -#: header, TCP, UDP, ICMP, ICMPv6 and OSPF, :envvar:`PCAPKIT_CHECKSUM_VERIFY`. +#: header, TCP, UDP, DCCP, ICMP, ICMPv6 and OSPF, :envvar:`PCAPKIT_CHECKSUM_VERIFY`. #: #: Off by default, since checksumming every packet costs time on a large #: capture (:issue:`2471`). When on, each records a diff --git a/pcapkit/vendor/__init__.py b/pcapkit/vendor/__init__.py index 26820d0125..38e94ba818 100644 --- a/pcapkit/vendor/__init__.py +++ b/pcapkit/vendor/__init__.py @@ -106,6 +106,7 @@ def __getattr__(name: 'str') -> 'typing.NoReturn': # per protocol from pcapkit.vendor.arp import * + from pcapkit.vendor.dccp import * from pcapkit.vendor.esp import * from pcapkit.vendor.ftp import * from pcapkit.vendor.hip import * @@ -137,6 +138,9 @@ def __getattr__(name: 'str') -> 'typing.NoReturn': 'AppType_TCP', 'AppType_UDP', 'AppType_SCTP', 'AppType_DCCP', # ARP 'ARP_Hardware', 'ARP_Operation', + # DCCP + 'DCCP_PacketType', 'DCCP_ResetCode', 'DCCP_Option', 'DCCP_Feature', + 'DCCP_AckVectorState', 'DCCP_DropCode', # ESP 'ESP_Cipher', 'ESP_Integrity', # FTP diff --git a/pcapkit/vendor/dccp/__init__.py b/pcapkit/vendor/dccp/__init__.py new file mode 100644 index 0000000000..1ce7dcd117 --- /dev/null +++ b/pcapkit/vendor/dccp/__init__.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# pylint: disable=unused-import +""":class:`~pcapkit.protocols.transport.dccp.DCCP` Vendor Crawlers +===================================================================== + +.. module:: pcapkit.vendor.dccp + +This module contains all vendor crawlers of +:class:`~pcapkit.protocols.transport.dccp.DCCP` implementations. Available +enumerations include: + +.. list-table:: + + * - :class:`DCCP_PacketType ` + - DCCP Packet Types [*]_ + * - :class:`DCCP_ResetCode ` + - DCCP Reset Codes [*]_ + * - :class:`DCCP_Option ` + - DCCP Option Types [*]_ + * - :class:`DCCP_Feature ` + - DCCP Feature Numbers [*]_ + * - :class:`DCCP_AckVectorState ` + - DCCP Ack Vector States [*]_ + * - :class:`DCCP_DropCode ` + - DCCP Drop Codes [*]_ + +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-1 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-3 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-5 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-7 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-10 +.. [*] https://www.iana.org/assignments/dccp-parameters/dccp-parameters.xhtml#dccp-parameters-11 + +""" + +from pcapkit.vendor.dccp.ack_vector_state import AckVectorState as DCCP_AckVectorState +from pcapkit.vendor.dccp.drop_code import DropCode as DCCP_DropCode +from pcapkit.vendor.dccp.feature import Feature as DCCP_Feature +from pcapkit.vendor.dccp.option import Option as DCCP_Option +from pcapkit.vendor.dccp.packet_type import PacketType as DCCP_PacketType +from pcapkit.vendor.dccp.reset_code import ResetCode as DCCP_ResetCode + +__all__ = ['DCCP_PacketType', 'DCCP_ResetCode', 'DCCP_Option', 'DCCP_Feature', + 'DCCP_AckVectorState', 'DCCP_DropCode'] diff --git a/pcapkit/vendor/dccp/ack_vector_state.py b/pcapkit/vendor/dccp/ack_vector_state.py new file mode 100644 index 0000000000..30e5234d89 --- /dev/null +++ b/pcapkit/vendor/dccp/ack_vector_state.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +"""DCCP Ack Vector States +========================= + +.. module:: pcapkit.vendor.dccp.ack_vector_state + +This module contains the vendor crawler for **DCCP Ack Vector States**, +which is automatically generating :class:`pcapkit.const.dccp.ack_vector_state.AckVectorState`. + +""" + +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['AckVectorState'] + + +class AckVectorState(Vendor): + """DCCP Ack Vector States""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 3' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/dccp-parameters/dccp-parameters-10.csv' + + +if __name__ == '__main__': + sys.exit(AckVectorState()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/dccp/drop_code.py b/pcapkit/vendor/dccp/drop_code.py new file mode 100644 index 0000000000..6f46d231d1 --- /dev/null +++ b/pcapkit/vendor/dccp/drop_code.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +"""DCCP Drop Codes +================== + +.. module:: pcapkit.vendor.dccp.drop_code + +This module contains the vendor crawler for **DCCP Drop Codes**, +which is automatically generating :class:`pcapkit.const.dccp.drop_code.DropCode`. + +""" + +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['DropCode'] + + +class DropCode(Vendor): + """DCCP Drop Codes""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 7' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/dccp-parameters/dccp-parameters-11.csv' + + +if __name__ == '__main__': + sys.exit(DropCode()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/dccp/feature.py b/pcapkit/vendor/dccp/feature.py new file mode 100644 index 0000000000..bcd5ce7d88 --- /dev/null +++ b/pcapkit/vendor/dccp/feature.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +"""DCCP Feature Numbers +======================= + +.. module:: pcapkit.vendor.dccp.feature + +This module contains the vendor crawler for **DCCP Feature Numbers**, +which is automatically generating :class:`pcapkit.const.dccp.feature.Feature`. + +""" + +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['Feature'] + + +class Feature(Vendor): + """DCCP Feature Numbers""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 255' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/dccp-parameters/dccp-parameters-7.csv' + + +if __name__ == '__main__': + sys.exit(Feature()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/dccp/option.py b/pcapkit/vendor/dccp/option.py new file mode 100644 index 0000000000..42563de1ce --- /dev/null +++ b/pcapkit/vendor/dccp/option.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +"""DCCP Option Types +==================== + +.. module:: pcapkit.vendor.dccp.option + +This module contains the vendor crawler for **DCCP Option Types**, +which is automatically generating :class:`pcapkit.const.dccp.option.Option`. + +""" + +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['Option'] + + +class Option(Vendor): + """DCCP Option Types""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 255' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/dccp-parameters/dccp-parameters-5.csv' + + +if __name__ == '__main__': + sys.exit(Option()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/dccp/packet_type.py b/pcapkit/vendor/dccp/packet_type.py new file mode 100644 index 0000000000..9ed386f2c0 --- /dev/null +++ b/pcapkit/vendor/dccp/packet_type.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +"""DCCP Packet Types +==================== + +.. module:: pcapkit.vendor.dccp.packet_type + +This module contains the vendor crawler for **DCCP Packet Types**, +which is automatically generating :class:`pcapkit.const.dccp.packet_type.PacketType`. + +""" + +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['PacketType'] + + +class PacketType(Vendor): + """DCCP Packet Types""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 15' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/dccp-parameters/dccp-parameters-1.csv' + + +if __name__ == '__main__': + sys.exit(PacketType()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/dccp/reset_code.py b/pcapkit/vendor/dccp/reset_code.py new file mode 100644 index 0000000000..75cdc85d75 --- /dev/null +++ b/pcapkit/vendor/dccp/reset_code.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +"""DCCP Reset Codes +=================== + +.. module:: pcapkit.vendor.dccp.reset_code + +This module contains the vendor crawler for **DCCP Reset Codes**, +which is automatically generating :class:`pcapkit.const.dccp.reset_code.ResetCode`. + +""" + +import sys + +from pcapkit.vendor.default import Vendor + +__all__ = ['ResetCode'] + + +class ResetCode(Vendor): + """DCCP Reset Codes""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 255' + #: Link to registry. + LINK = 'https://www.iana.org/assignments/dccp-parameters/dccp-parameters-3.csv' + + +if __name__ == '__main__': + sys.exit(ResetCode()) # type: ignore[arg-type] diff --git a/tests/const/test_const_enum_builtin_parity.py b/tests/const/test_const_enum_builtin_parity.py index 47fd1d8f64..1ff453b01e 100644 --- a/tests/const/test_const_enum_builtin_parity.py +++ b/tests/const/test_const_enum_builtin_parity.py @@ -377,12 +377,12 @@ def test_the_sweep_size_is_pinned(self) -> None: # tests.const.test_const_enum_get's narrower (IntEnum, IntFlag)-only # sweep. The int count includes GitHub issue #880's ngap.ProcedureCode # and ngap.ProtocolIE, which are on EnumRegistry, as are the ICMP, - # ICMPv6 and IGMP registry families added on the base branch. - self.assertEqual(len(ints), 129) + # ICMPv6, IGMP and DCCP registry families. + self.assertEqual(len(ints), 135) self.assertEqual(len(flags), 6) self.assertEqual(len(strs), 10) - self.assertEqual(len(self.enums), 145) - self.assertEqual(len({obj.__module__ for obj in self.enums}), 139) + self.assertEqual(len(self.enums), 151) + self.assertEqual(len({obj.__module__ for obj in self.enums}), 145) def test_every_registry_rejects_a_negative_value(self) -> None: """The registry-wide form of GitHub issue #647. diff --git a/tests/const/test_const_enum_get.py b/tests/const/test_const_enum_get.py index dc5febd8da..aadec644bc 100644 --- a/tests/const/test_const_enum_get.py +++ b/tests/const/test_const_enum_get.py @@ -297,7 +297,7 @@ def test_the_sweep_size_is_pinned(self) -> None: # pcapkit.protocols.application.ngap; neither needs an exception-set # entry, since both take the base's own get(key, default). names = {_qualname(obj) for obj in self.enums} - self.assertEqual(len(self.enums), 135) + self.assertEqual(len(self.enums), 141) for expected in (EXPECTED_TO_RESOLVE_ANYTHING, EXPECTED_WITHOUT_AN_INTEGER_DEFAULT, EXPECTED_WITHOUT_A_CACHEABLE_FALLBACK): self.assertTrue(expected.issubset(names), @@ -337,7 +337,7 @@ def test_every_integer_path_consults_the_default(self) -> None: # registry excused above: ``EXPECTED_TO_RESOLVE_ANYTHING`` and # ``EXPECTED_WITHOUT_A_CACHEABLE_FALLBACK``, with # ``EXPECTED_WITHOUT_AN_INTEGER_DEFAULT`` empty. - self.assertEqual(covered, 132) + self.assertEqual(covered, 138) def test_the_always_resolving_registries_have_nothing_to_fall_back_to(self) -> None: """The two registries excused from the sweep, and why. diff --git a/tests/const/test_const_enum_lookup.py b/tests/const/test_const_enum_lookup.py index dd8320691f..ebd5ea32f4 100644 --- a/tests/const/test_const_enum_lookup.py +++ b/tests/const/test_const_enum_lookup.py @@ -200,7 +200,7 @@ def test_every_registry_enum_was_discovered(self) -> None: # ``_iter_const_int_flags``'s, and so are GitHub issue #880's # ngap.ProcedureCode and ngap.ProtocolIE, which are on EnumRegistry. names = {f'{obj.__module__}.{obj.__qualname__}' for obj in self.enums} - self.assertEqual(len(self.enums), 129) + self.assertEqual(len(self.enums), 135) self.assertTrue(EXPECTED_TO_REJECT_ZERO.issubset(names), f'expected reject-list entries missing from the sweep: ' f'{EXPECTED_TO_REJECT_ZERO - names}') diff --git a/tests/const/test_const_registry_protocol.py b/tests/const/test_const_registry_protocol.py index 06d84d331f..0a9bf484eb 100644 --- a/tests/const/test_const_registry_protocol.py +++ b/tests/const/test_const_registry_protocol.py @@ -846,8 +846,8 @@ def test_every_generated_const_module_is_accounted_for(self) -> None: generated = [path for path in all_files if path not in converted_relpaths and path not in EXCLUDED_FROM_INTENUM_CENSUS] - self.assertEqual(len(all_files), 139) - self.assertEqual(len(generated), 122) + self.assertEqual(len(all_files), 145) + self.assertEqual(len(generated), 128) for relpath in generated: with self.subTest(module=relpath): source = (REPO_ROOT / relpath).read_text() diff --git a/tests/foundation/registry/test_registrar_code_check_unit.py b/tests/foundation/registry/test_registrar_code_check_unit.py index ad9185dc6b..6dcf92eeb8 100644 --- a/tests/foundation/registry/test_registrar_code_check_unit.py +++ b/tests/foundation/registry/test_registrar_code_check_unit.py @@ -70,6 +70,8 @@ 'pcapkit.protocols.schema.transport.sctp:Parameter'), ('register_sctp_cause', 'pcapkit.protocols.transport.sctp:SCTP', 'pcapkit.protocols.schema.transport.sctp:ErrorCause'), + ('register_dccp_option', 'pcapkit.protocols.transport.dccp:DCCP', + 'pcapkit.protocols.schema.transport.dccp:Option'), ('register_http_frame', 'pcapkit.protocols.application.httpv2:HTTPv2', 'pcapkit.protocols.schema.application.httpv2:FrameType'), ('register_pcapng_block', 'pcapkit.protocols.misc.pcapng:PCAPNG', @@ -113,6 +115,7 @@ 'register_sctp_chunk': 'pcapkit.const.sctp.chunk:Chunk', 'register_sctp_parameter': 'pcapkit.const.sctp.parameter:Parameter', 'register_sctp_cause': 'pcapkit.const.sctp.cause_code:CauseCode', + 'register_dccp_option': 'pcapkit.const.dccp.option:Option', 'register_http_frame': 'pcapkit.const.http.frame:Frame', 'register_pcapng_block': 'pcapkit.const.pcapng.block_type:BlockType', 'register_pcapng_option': 'pcapkit.const.pcapng.option_type:OptionType', @@ -135,6 +138,7 @@ ('register_tcp', 80, ('pcapkit.protocols.transport.tcp:TCP',), ()), ('register_udp', 53, ('pcapkit.protocols.transport.udp:UDP',), ()), ('register_sctp', 60, ('pcapkit.protocols.transport.sctp:SCTP',), ()), + ('register_dccp', 5004, ('pcapkit.protocols.transport.dccp:DCCP',), ()), ('register_apptype', 80, ('pcapkit.protocols.transport.tcp:TCP', 'pcapkit.protocols.transport.udp:UDP'), ('tcp',)), ('register_frad', 0xCC, ('pcapkit.protocols.link.frad:FRAD',), ()), diff --git a/tests/foundation/registry/test_registry_symmetry_unit.py b/tests/foundation/registry/test_registry_symmetry_unit.py index d472889849..3d9955116e 100644 --- a/tests/foundation/registry/test_registry_symmetry_unit.py +++ b/tests/foundation/registry/test_registry_symmetry_unit.py @@ -139,6 +139,9 @@ class MethodRegistrar(NamedTuple): MethodRegistrar('register_sctp_cause', 'pcapkit.protocols.transport.sctp:SCTP', '__cause__', 'pcapkit.protocols.schema.transport.sctp:ErrorCause', ('pcapkit.const.sctp.cause_code:CauseCode.Stale_Cookie',)), + MethodRegistrar('register_dccp_option', 'pcapkit.protocols.transport.dccp:DCCP', '__option__', + 'pcapkit.protocols.schema.transport.dccp:Option', + ('pcapkit.const.dccp.option:Option.Timestamp',)), MethodRegistrar('register_http_frame', 'pcapkit.protocols.application.httpv2:HTTPv2', '__frame__', 'pcapkit.protocols.schema.application.httpv2:FrameType', ('pcapkit.const.http.frame:Frame.HEADERS',)), @@ -453,6 +456,7 @@ def test_every_public_registrar_is_covered_or_excused(self) -> None: 'register_icmp_type': 'no schema argument; override and restore are in test_icmp_type_registrar_unit', 'register_ipx': 'IPX.__proto__ ships no entry, so an override has nothing to be restored to', 'register_netlink': 'Netlink.__proto__ ships no entry, so there is none to restore', + 'register_dccp': 'DCCP.__proto__ ships no entry, so there is none to restore', } self.assertEqual(sorted(set(registry.__all__) - covered - set(excused)), []) self.assertEqual(sorted((covered | set(excused)) - set(registry.__all__)), []) diff --git a/tests/foundation/registry/test_registry_wrapper_coverage_2511_unit.py b/tests/foundation/registry/test_registry_wrapper_coverage_2511_unit.py index 7f722928ea..138417bcd8 100644 --- a/tests/foundation/registry/test_registry_wrapper_coverage_2511_unit.py +++ b/tests/foundation/registry/test_registry_wrapper_coverage_2511_unit.py @@ -52,6 +52,8 @@ #: ``map path -> (the classmethod its owner inherits, why it fits the map's keys)``. INHERITED = { + 'pcapkit.protocols.transport.dccp.DCCP.__proto__': + ('pcapkit.protocols.transport.transport.Transport.register', 'typed for the port the map is keyed by'), 'pcapkit.protocols.transport.tcp.TCP.__proto__': ('pcapkit.protocols.transport.transport.Transport.register', 'typed for the port the map is keyed by'), 'pcapkit.protocols.transport.udp.UDP.__proto__': diff --git a/tests/project/test_rfc_constants_one_home_1739_unit.py b/tests/project/test_rfc_constants_one_home_1739_unit.py index b55282106e..e0a9a1ada7 100644 --- a/tests/project/test_rfc_constants_one_home_1739_unit.py +++ b/tests/project/test_rfc_constants_one_home_1739_unit.py @@ -58,6 +58,7 @@ ('pcapkit.protocols.schema.internet.ah', 'AH_HEADER_LEN', 12), ('pcapkit.protocols.schema.transport.tcp', 'TCP_MIN_HEADER_LEN', 20), ('pcapkit.protocols.schema.transport.tcp', 'TCP_DATA_OFFSET_OCTET', 12), + ('pcapkit.protocols.schema.transport.dccp', 'DCCP_MIN_HEADER_LEN', 12), ('pcapkit.protocols.schema.misc.pcap.header', 'GLOBAL_HEADER_LEN', 24), ('pcapkit.protocols.schema.misc.pcap.frame', 'RECORD_HEADER_LEN', 16), ('pcapkit.protocols.schema.misc.pcap.header', 'MAGIC_NUMBER', { @@ -178,6 +179,8 @@ ('pcapkit/protocols/internet/ah.py', 'pcapkit.protocols.schema.internet.ah', 'AH_HEADER_LEN'), ('pcapkit/protocols/transport/tcp.py', 'pcapkit.protocols.schema.transport.tcp', 'TCP_MIN_HEADER_LEN'), + ('pcapkit/protocols/transport/dccp.py', 'pcapkit.protocols.schema.transport.dccp', + 'DCCP_MIN_HEADER_LEN'), ) #: Readers that reach a home as a module attribute, ``Module.NAME``, rather than @@ -466,6 +469,12 @@ 'pcapkit/protocols/transport/sctp.py': ((12, 16, 20), 'SCTP chunk and parameter sizes'), 'pcapkit/protocols/schema/transport/sctp.py': ((6, 16, 20), 'SCTP chunk and parameter sizes, ' 'and a flag bit'), + 'pcapkit/protocols/transport/dccp.py': ((6, 65535), 'DCCP option lengths, the 2-bit shift of ' + 'an Ack Vector state, and the largest 2-octet ' + 'Elapsed Time'), + 'pcapkit/protocols/schema/transport/dccp.py': ((6,), 'a 48-bit DCCP sequence or ' + 'acknowledgement number, and a Timestamp Echo ' + 'option without its Elapsed Time'), 'pcapkit/protocols/transport/tcp.py': ((6, 16, 24, 32, 65535), 'TCP and MPTCP option sizes, ' 'and the 16-bit window and MSS defaults'), 'pcapkit/protocols/schema/transport/tcp.py': ((6, 16, 28), 'TCP and MPTCP option sizes, and ' diff --git a/tests/protocols/test_dispatch_registry_unit.py b/tests/protocols/test_dispatch_registry_unit.py index 218f6ff819..de96208684 100644 --- a/tests/protocols/test_dispatch_registry_unit.py +++ b/tests/protocols/test_dispatch_registry_unit.py @@ -228,10 +228,11 @@ def test_cases_cover_every_table_named_in_the_issue(self) -> None: # LinkType.NULL and LinkType.LOOP in both link-type tables, the four # address families of Loopback.__proto__, #2438's # Frame.__proto__[LinkType.LINUX_SLL]/[LinkType.LINUX_SLL2] in both the - # pcap and pcapng frame tables, #2439's IGMP, and #2436's - # Internet.__proto__[TransType.ICMP] and [TransType.IPv6_ICMP]. - self.assertEqual(len(cases), 56, - 'expected exactly 56 entries across the seven __proto__ ' + # pcap and pcapng frame tables, #2439's IGMP, #2436's + # Internet.__proto__[TransType.ICMP] and [TransType.IPv6_ICMP], and + # #2489's Internet.__proto__[TransType.DCCP]. + self.assertEqual(len(cases), 57, + 'expected exactly 57 entries across the seven __proto__ ' 'tables named in GitHub issue #496 and the loopback table ' 'of #1574; a different count means a registry changed ' 'shape and this module was not updated to match') @@ -240,7 +241,7 @@ def test_cases_cover_every_table_named_in_the_issue(self) -> None: for case in cases: by_family[case.family] = by_family.get(case.family, 0) + 1 self.assertEqual(by_family, { - 'link': 7, 'internet': 20, 'tcp': 5, 'udp': 4, 'sctp': 2, + 'link': 7, 'internet': 21, 'tcp': 5, 'udp': 4, 'sctp': 2, 'pcap-frame': 7, 'pcapng-frame': 7, 'loopback': 4, }) diff --git a/tests/protocols/test_parser_registrar_identity_unit.py b/tests/protocols/test_parser_registrar_identity_unit.py index bd4d5cc777..88dea7c175 100644 --- a/tests/protocols/test_parser_registrar_identity_unit.py +++ b/tests/protocols/test_parser_registrar_identity_unit.py @@ -37,6 +37,7 @@ ('pcapkit.protocols.transport.sctp', 'SCTP', 'register_chunk', '__chunk__'), ('pcapkit.protocols.transport.sctp', 'SCTP', 'register_parameter', '__parameter__'), ('pcapkit.protocols.transport.sctp', 'SCTP', 'register_cause', '__cause__'), + ('pcapkit.protocols.transport.dccp', 'DCCP', 'register_option', '__option__'), ('pcapkit.protocols.application.httpv2', 'HTTPv2', 'register_frame', '__frame__'), ) diff --git a/tests/protocols/test_protocol_code_registration_unit.py b/tests/protocols/test_protocol_code_registration_unit.py index 1ddd8e733c..4bc794d094 100644 --- a/tests/protocols/test_protocol_code_registration_unit.py +++ b/tests/protocols/test_protocol_code_registration_unit.py @@ -569,6 +569,7 @@ def test_every_destination_kind_row_is_enforced(self) -> None: from pcapkit.protocols.misc.pcap.frame import Frame from pcapkit.protocols.misc.pcapng import PCAPNG from pcapkit.protocols.misc.raw import Raw + from pcapkit.protocols.transport.dccp import DCCP from pcapkit.protocols.transport.sctp import SCTP from pcapkit.protocols.transport.tcp import TCP from pcapkit.protocols.transport.udp import UDP @@ -591,6 +592,7 @@ def test_every_destination_kind_row_is_enforced(self) -> None: PCAPNG: (LinkType, False, TransType.ICMP, 1), TCP: (AppType, True, LinkType(53), 54324), UDP: (AppType, True, LinkType(53), 54324), + DCCP: (AppType, True, LinkType(53), 54324), SCTP: (PayloadProtocolIdentifier, True, TransType.ICMP, 54324), } diff --git a/tests/protocols/test_short_read_roundtrip_unit.py b/tests/protocols/test_short_read_roundtrip_unit.py index 81e204f4ba..7e64726934 100644 --- a/tests/protocols/test_short_read_roundtrip_unit.py +++ b/tests/protocols/test_short_read_roundtrip_unit.py @@ -86,6 +86,8 @@ def _ipv6(code: int, body: bytes) -> bytes: ('esp', 'internet.esp:ESP', {}, bytes.fromhex('0000000100000002') + b'\x77' * 16), ('tcp', 'transport.tcp:TCP', {}, bytes.fromhex('9c409c41000000010000000060020000ffff0000020405b4')), ('udp', 'transport.udp:UDP', {}, bytes.fromhex('9c409c41000c000061626364')), + ('dccp', 'transport.dccp:DCCP', {}, bytes.fromhex( + '9c40138906008f3a0100000000000001123456782004010268656c6c6f')), ('sctp', 'transport.sctp:SCTP', {}, bytes.fromhex( '13881389deadbeef000000000003001400000001000100020000000061626364')), ('ftp', 'application.ftp:FTP', {}, b'XYZW arg\r\n'), diff --git a/tests/protocols/transport/test_dccp_unit.py b/tests/protocols/transport/test_dccp_unit.py new file mode 100644 index 0000000000..5b332fc57e --- /dev/null +++ b/tests/protocols/transport/test_dccp_unit.py @@ -0,0 +1,545 @@ +# -*- coding: utf-8 -*- +"""DCCP dissection, construction and its checksum over the IP pseudo-header. + +GitHub issue #2489: :class:`~pcapkit.protocols.transport.dccp.DCCP` parses the +:rfc:`4340` generic header with short and long sequence numbers, the fields each +packet type adds, DCCP-Listen (:rfc:`5596`) and the options, and rebuilds every +packet byte for byte. The enclosing IPv4 or IPv6 layer verifies and computes the +checksum over the part of the packet the Checksum Coverage gives +(:rfc:`4340#section-9`). The octets here are built by hand; the checksummed +packets were checked against Wireshark's DCCP dissector, and the expected sums +are the hand-written one of ``tests/foundation/_roundtrip.py``. + +""" +from __future__ import annotations + +import struct +import unittest +import warnings +from unittest import mock + +from tests._support import reimport_once_per_class +from tests.foundation import _roundtrip as wire + +#: Test tier (#1958): hand-picked parse -> rebuild and make -> parse round trips. +__tier__ = 'ALL' + +SRC, DST = 40000, 5001 + + +def generic(type_: int, *, x: bool = True, seq: int = 1, offset: int = 0, ccval: int = 0, + cscov: int = 0, checksum: bytes = b'\x00\x00', res: int = 0, seq_res: int = 0) -> bytes: + """The generic header (RFC 4340 section 5.1); ``offset`` in 32-bit words.""" + head = struct.pack('!HHBB', SRC, DST, offset, (ccval << 4) | cscov) + checksum + if x: + return head + bytes([(res << 5) | (type_ << 1) | 1, seq_res]) + seq.to_bytes(6, 'big') + return head + bytes([(res << 5) | (type_ << 1)]) + seq.to_bytes(3, 'big') + + +def ack(number: int, *, x: bool = True, res: int = 0) -> bytes: + """The Acknowledgement Number Subheader.""" + if x: + return res.to_bytes(2, 'big') + number.to_bytes(6, 'big') + return res.to_bytes(1, 'big') + number.to_bytes(3, 'big') + + +def packet(type_: int, extra: bytes = b'', options: bytes = b'', data: bytes = b'', **kwargs: object) -> bytes: + """A DCCP packet, its Data Offset filled in.""" + words = (len(generic(type_, **kwargs)) + len(extra) + len(options)) // 4 # type: ignore[arg-type] + return generic(type_, offset=words, **kwargs) + extra + options + data # type: ignore[arg-type] + + +#: IPv4 and IPv6 packets carrying DCCP with their checksum computed, each one +#: reported correct by Wireshark: a DCCP-Request with a Change L option, a +#: DCCP-DataAck with CsCov 2 over IPv4, a DCCP-Reset over IPv6, and a DCCP-Data +#: with CsCov 1, which leaves all of its data out. +CHECKSUMMED = ( + bytes.fromhex('45000031000000000021ce75c0000201c63364029c40138906008f3a0100000000000001' + '123456782004010268656c6c6f'), + bytes.fromhex('4500003c000000000021ce6ac0000201c63364029c4013890602ca360800000700000006' + '290600000005000030313233343536373839616263646566'), + bytes.fromhex('60000000001f214020010db800000000000000000000000120010db800000000000000000000' + '00029c401389070004e90f000000000000090000000000000008052001026f6464'), + bytes.fromhex('45000025000000000021ce81c0000201c63364029c40138903015cc8040000036162636465'), +) + + +def pseudo4(length: int) -> bytes: + """The IPv4 pseudo-header of DCCP (RFC 4340 section 9.1).""" + return wire.IPV4_A + wire.IPV4_B + b'\x00\x21' + length.to_bytes(2, 'big') + + +def pseudo6(length: int) -> bytes: + """The IPv6 pseudo-header of DCCP.""" + return wire.IPV6_A + wire.IPV6_B + length.to_bytes(4, 'big') + b'\x00\x00\x00\x21' + + +def expected(prefix: bytes, datagram: bytes, covered: int) -> bytes: + """The checksum ``datagram`` should carry over ``prefix`` and its first ``covered`` octets.""" + return wire.checksum(prefix + (datagram[:6] + b'\x00\x00' + datagram[8:])[:covered]).to_bytes(2, 'big') + + +class DCCPTestCase(unittest.TestCase): + """Imports shared by the DCCP tests.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + from pcapkit.const.dccp.ack_vector_state import AckVectorState + from pcapkit.const.dccp.drop_code import DropCode + from pcapkit.const.dccp.feature import Feature + from pcapkit.const.dccp.option import Option + from pcapkit.const.dccp.packet_type import PacketType + from pcapkit.const.dccp.reset_code import ResetCode + from pcapkit.corekit.checksum.internet import ChecksumStatus + from pcapkit.protocols.internet.ipv4 import IPv4 + from pcapkit.protocols.internet.ipv6 import IPv6 + from pcapkit.protocols.misc.checksum.ip_pseudo import IP_Pseudo + from pcapkit.protocols.transport.dccp import DCCP + from pcapkit.utilities import config + from pcapkit.utilities.exceptions import ProtocolError + from pcapkit.utilities.warnings import ChecksumWarning, ProtocolWarning + + self.DCCP, self.IPv4, self.IPv6, self.IP_Pseudo = DCCP, IPv4, IPv6, IP_Pseudo + self.Type, self.Option, self.Feature, self.ResetCode = PacketType, Option, Feature, ResetCode + self.State, self.DropCode, self.Status = AckVectorState, DropCode, ChecksumStatus + self.config, self.ProtocolError = config, ProtocolError + self.ChecksumWarning, self.ProtocolWarning = ChecksumWarning, ProtocolWarning + + def assertRoundTrip(self, octets: bytes) -> 'object': # pylint: disable=invalid-name + """``octets`` parse, and rebuild from the parse, as themselves; the parse.""" + layer = self.DCCP(octets) + self.assertEqual(layer.data, octets) + self.assertEqual(self.DCCP.from_data(layer.info).data, octets) + self.assertEqual(self.DCCP.from_data(layer.info.to_dict()).data, octets) + return layer + + +class DCCPPacketTypeTests(DCCPTestCase): + """Each packet type, against the RFC 4340 field layout.""" + + def test_request_with_long_sequence_numbers(self) -> None: + octets = packet(0, struct.pack('!I', 0x12345678), data=b'hello', seq=0x0102030405, ccval=3, seq_res=0x5A) + info = self.assertRoundTrip(octets).info + self.assertEqual((info.srcport.port, info.dstport.port), (SRC, DST)) + self.assertEqual((info.hdr_len, info.type, info.x), (20, self.Type.DCCP_Request, True)) + self.assertEqual((info.seq, info.seq_reserved, info.ccval), (0x0102030405, 0x5A, 3)) + self.assertEqual(info.service_code, 0x12345678) + self.assertNotIn('ack', info) + self.assertNotIn('options', info) + + def test_response_carries_an_ack_and_a_service_code(self) -> None: + octets = packet(1, ack(0xABCDEF012345, res=0x0102) + struct.pack('!I', 7)) + info = self.assertRoundTrip(octets).info + self.assertEqual((info.hdr_len, info.type), (28, self.Type.DCCP_Response)) + self.assertEqual((info.ack, info.ack_reserved, info.service_code), (0xABCDEF012345, 0x0102, 7)) + + def test_data_with_short_sequence_numbers(self) -> None: + octets = packet(2, data=b'xy', x=False, seq=0xABCDEF) + layer = self.assertRoundTrip(octets) + self.assertEqual((layer.info.hdr_len, layer.info.x, layer.info.seq), (12, False, 0xABCDEF)) + self.assertNotIn('seq_reserved', layer.info) + self.assertNotIn('ack', layer.info) + self.assertEqual((layer.length, bytes(layer.payload)), (12, b'xy')) + self.assertEqual((layer.name, layer.src.port, layer.dst.port), + ('Datagram Congestion Control Protocol', SRC, DST)) + + def test_ack_and_data_ack_take_either_sequence_number_length(self) -> None: + for type_ in (3, 4): + for x in (True, False): + with self.subTest(type=type_, x=x): + octets = packet(type_, ack(0x0A0B0C, x=x, res=1), data=b'z', x=x, seq=0x010203) + info = self.assertRoundTrip(octets).info + self.assertEqual((info.ack, info.ack_reserved), (0x0A0B0C, 1)) + self.assertEqual(info.hdr_len, 24 if x else 16) + + def test_close_sync_and_their_acks_carry_only_an_ack(self) -> None: + for type_ in (5, 6, 8, 9): + with self.subTest(type=type_): + info = self.assertRoundTrip(packet(type_, ack(99))).info + self.assertEqual((info.type, info.ack, info.hdr_len), (self.Type(type_), 99, 24)) + self.assertNotIn('service_code', info) + + def test_reset_carries_a_code_and_three_data_octets(self) -> None: + octets = packet(7, ack(8) + bytes([5, 32, 1, 2]), data=b'Option Error') + info = self.assertRoundTrip(octets).info + self.assertEqual((info.reset_code, info.reset_data), (self.ResetCode.Option_Error, b'\x20\x01\x02')) + self.assertEqual(info.hdr_len, 28) + + def test_listen_carries_a_service_code_and_no_ack(self) -> None: + # RFC 5596 section 2.2.1: the generic header with X set, then a Service Code. + info = self.assertRoundTrip(packet(10, struct.pack('!I', 0x01020304), seq=0)).info + self.assertEqual((info.type, info.service_code), (self.Type.DCCP_Listen, 0x01020304)) + self.assertNotIn('ack', info) + + def test_a_reserved_type_keeps_its_bits(self) -> None: + # Type 11 is unassigned, so only the generic header is known; Res is kept too. + info = self.assertRoundTrip(packet(11, data=b'?', res=5)).info + self.assertEqual((info.type, info.reserved, info.hdr_len), (self.Type(11), 5, 16)) + + def test_make_builds_each_packet_type(self) -> None: + for type_ in range(11): + for x in (True, False): + with self.subTest(type=type_, x=x): + made = self.DCCP(srcport=SRC, dstport=DST, type=type_, x=x, seq=5, ack=4, + service_code=3, reset_code=2, reset_data=b'\x01\x02\x03', + checksum=b'\x00\x00', payload=b'data') + self.assertRoundTrip(made.data) + self.assertEqual(self.DCCP(made.data).info.type, self.Type(type_)) + + def test_a_data_offset_short_of_the_type_is_refused(self) -> None: + octets = bytearray(packet(1, ack(1) + bytes(4))) + octets[4] = 6 # 24 octets, where a DCCP-Response takes 28 + with self.assertRaisesRegex(self.ProtocolError, 'shorter than the 28 octets of a DCCP_Response'): + self.DCCP(bytes(octets)) + + def test_a_data_offset_past_the_data_is_refused(self) -> None: + octets = bytearray(packet(2, x=False)) + octets[4] = 4 + with self.assertRaisesRegex(self.ProtocolError, 'runs past the end of the data'): + self.DCCP(bytes(octets)) + + def test_a_refused_header_leaves_the_ip_payload_raw(self) -> None: + octets = bytearray(packet(2, x=False)) + octets[4] = 4 + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + ip = self.IPv4(wire.ipv4(bytes(octets), proto=33)) + self.assertEqual(type(ip.payload).__name__, 'Raw') + + def test_the_ip_layer_dispatches_protocol_33(self) -> None: + ip = self.IPv4(wire.ipv4(packet(2, data=b'hi', x=False), proto=33)) + self.assertIsInstance(ip.payload, self.DCCP) + self.assertIn('DCCP', ip) + self.assertEqual(bytes(ip.payload.payload), b'hi') + self.assertEqual(self.DCCP.__index__(), 33) + + def test_an_oversized_header_is_refused(self) -> None: + cookie = (self.Option.Init_Cookie, {'cookie': bytes(253)}) + with self.assertRaisesRegex(self.ProtocolError, 'exceeds 1020 octets'): + self.DCCP(type=0, options=[cookie] * 4, checksum=b'\x00\x00') + + +class DCCPOptionTests(DCCPTestCase): + """Options, against the RFC 4340 option formats.""" + + def options(self, area: bytes) -> 'object': + """The options of a DCCP-Ack carrying ``area``, after a round trip.""" + return self.assertRoundTrip(packet(3, ack(1), area)).info.options + + def test_single_octet_options(self) -> None: + options = self.options(bytes([0, 1, 2, 3])) + self.assertEqual([(int(code), option.length) for code, option in options.items(multi=True)], + [(0, 1), (1, 1), (2, 1), (3, 1)]) + self.assertEqual([type(option).__name__ for _, option in options.items(multi=True)], + ['Padding', 'Mandatory', 'SlowReceiver', 'UnassignedOption']) + self.assertEqual(options[self.Option(3)].data, b'') + + def test_feature_negotiation(self) -> None: + options = self.options(bytes([32, 4, 1, 2, 33, 5, 1, 2, 3, 34, 3, 6, 35, 3, 6, 0, 0, 0])) + change = options[self.Option.Change_L] + self.assertEqual((change.feature, change.value), (self.Feature.Congestion_Control_ID, b'\x02')) + self.assertEqual(options[self.Option.Confirm_L].value, b'\x02\x03') + self.assertEqual(options[self.Option.Change_R].feature, self.Feature.Send_Ack_Vector) + # An empty Confirm (RFC 4340 section 6.6.7) has no value. + self.assertEqual(options[self.Option.Confirm_R].value, b'') + + def test_a_feature_option_without_its_number(self) -> None: + # Length 2 leaves no room for the feature number, which is then absent. + options = self.options(bytes([32, 2, 35, 2])) + self.assertEqual([(option.feature, option.value) for _, option in options.items(multi=True)], + [(None, b''), (None, b'')]) + made = self.DCCP(type=3, ack=1, checksum=b'\x00\x00', + options=[(self.Option.Confirm_R, {'feature': None})]) + self.assertEqual(made.data[24:], bytes([35, 2, 0, 0])) + for code in (self.Option.Change_L, self.Option.Confirm_L): + with self.subTest(code=code), self.assertRaises(self.ProtocolError): + self.DCCP(type=3, checksum=b'\x00\x00', options=[(code, {'feature': None, 'value': b'\x01'})]) + + def test_a_feature_option_shorter_than_its_header_is_refused(self) -> None: + # RFC 4340 section 5.8: a length under 2 is nonsensical, as in every option + # with a length; under IP the whole layer is then Raw. + for code in (32, 33, 34, 35): + for length in (0, 1): + with self.subTest(code=code, length=length): + octets = packet(3, ack(1), bytes([code, length, 0, 0])) + with self.assertRaisesRegex(self.ProtocolError, 'negative length'): + self.DCCP(octets) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + ip = self.IPv4(wire.ipv4(octets, proto=33)) + self.assertEqual(type(ip.payload).__name__, 'Raw') + self.assertEqual(ip.payload.data, octets) + + def test_cookie_and_ndp_count(self) -> None: + options = self.options(bytes([36, 5, 9, 8, 7, 37, 4, 0, 1, 0, 0, 0])) + self.assertEqual(options[self.Option.Init_Cookie].cookie, b'\x09\x08\x07') + # The count keeps its width: a leading zero octet rebuilds as one. + self.assertEqual((options[self.Option.NDP_Count].count, options[self.Option.NDP_Count].length), (1, 4)) + + def test_an_ndp_count_past_eight_octets_keeps_its_width(self) -> None: + # RFC 4340 section 7.7 allows a length of 3 to 8; a longer one still rebuilds as itself. + for length in (9, 12, 255): + with self.subTest(length=length): + area = bytes([37, length]) + bytes(length - 3) + b'\x07' + ndp = self.options(area + bytes(-len(area) % 4))[self.Option.NDP_Count] + self.assertEqual((ndp.length, ndp.count), (length, 7)) + + def test_ack_vector_states_and_run_lengths(self) -> None: + options = self.options(bytes([38, 5, 0x03, 0x7F, 0xC1, 39, 3, 0x40, 0, 0])) + nonce0, nonce1 = options[self.Option.Ack_Vector_Nonce_0], options[self.Option.Ack_Vector_Nonce_1] + self.assertEqual([(run.state, run.run_length) for run in nonce0.vector], + [(self.State.Received, 3), (self.State.Received_ECN_Marked, 63), + (self.State.Not_Yet_Received, 1)]) + self.assertEqual((nonce0.nonce, nonce1.nonce), (0, 1)) + self.assertEqual(nonce1.vector[0].state, self.State.Received_ECN_Marked) + + def test_data_dropped_blocks(self) -> None: + dropped = self.options(bytes([40, 4, 0x05, 0xB2, 0]))[self.Option.Data_Dropped] + self.assertEqual([(block.dropped, block.code, block.run_length) for block in dropped.vector], + [(False, None, 5), (True, self.DropCode.Corrupt, 2)]) + + def test_timing_and_data_checksum_options(self) -> None: + options = self.options(bytes([41, 6, 0, 0, 1, 0, 42, 8, 0, 0, 0, 9, 0, 5, 43, 6, 0, 1, 0, 0, + 42, 6, 0, 0, 0, 8, 43, 4, 0, 7, 44, 6, 1, 2, 3, 4])) + self.assertEqual(options[self.Option.Timestamp].timestamp, 256) + echoes = options.getlist(self.Option.Timestamp_Echo) + self.assertEqual([(echo.timestamp, echo.elapsed) for echo in echoes], [(9, 5), (8, None)]) + self.assertEqual([option.elapsed for option in options.getlist(self.Option.Elapsed_Time)], [65536, 7]) + self.assertEqual(options[self.Option.Data_Checksum].checksum, b'\x01\x02\x03\x04') + + def test_an_unknown_option_keeps_its_data(self) -> None: + options = self.options(bytes([128, 4, 0xAA, 0xBB, 46, 2, 0, 0])) + self.assertEqual(options[self.Option(128)].data, b'\xaa\xbb') + self.assertEqual(options[self.Option.Multipath].length, 2) + + def test_an_option_past_the_option_area_is_refused(self) -> None: + # The Timestamp declares 6 octets with 2 left in the area, the payload behind it. + with self.assertRaises(self.ProtocolError): + self.DCCP(packet(3, ack(1), bytes([2, 2, 41, 6]), data=b'\x00\x00\x00\x01')) + + def test_make_pads_to_32_bits(self) -> None: + made = self.DCCP(type=3, ack=1, checksum=b'\x00\x00', + options=[(self.Option.Timestamp, {'timestamp': 1}), + (self.Option.Ack_Vector_Nonce_0, {'vector': [(3, 2)]})]) + self.assertEqual(made.data[24:], bytes([41, 6, 0, 0, 0, 1, 38, 3, 0xC2, 0, 0, 0])) + self.assertEqual(made.info.hdr_len, 36) + self.assertRoundTrip(made.data) + + def test_make_sizes_the_variable_fields(self) -> None: + made = self.DCCP(type=3, ack=1, checksum=b'\x00\x00', options=[ + (self.Option.NDP_Count, {'count': 0x010000}), + (self.Option.Elapsed_Time, {'elapsed': 0x10000}), + (self.Option.Timestamp_Echo, {'timestamp': 1, 'elapsed': 2}), + (self.Option.Data_Dropped, {'vector': [(None, 127), (self.DropCode.Receive_Buffer, 15)]}), + (self.Option(3), {}), (self.Option(200), {'data': b'\x01'}), + (self.Option.Change_R, {'feature': self.Feature.ECN_Incapable, 'value': b'\x01'}), + ]) + self.assertEqual(made.data[24:], bytes([37, 5, 1, 0, 0, 43, 6, 0, 1, 0, 0, 42, 8, 0, 0, 0, 1, 0, 2, + 40, 4, 0x7F, 0xAF, 3, 200, 3, 1, 34, 4, 4, 1, 0])) + self.assertRoundTrip(made.data) + + def test_make_writes_the_fixed_layouts(self) -> None: + made = self.DCCP(type=3, ack=1, checksum=b'\x00\x00', options=[ + (self.Option.Confirm_L, {'feature': self.Feature.Sequence_Window, 'value': b'\x00\x64'}), + (self.Option.Timestamp_Echo, {'timestamp': 2, 'elapsed': 0x10000}), + (self.Option.Data_Checksum, {'checksum': b'\xde\xad\xbe\xef'}), + (self.Option.Timestamp_Echo, {'timestamp': 3}), + ]) + self.assertEqual(made.data[24:], bytes([33, 5, 3, 0, 0x64, 42, 10, 0, 0, 0, 2, 0, 1, 0, 0, + 44, 6, 0xDE, 0xAD, 0xBE, 0xEF, 42, 6, 0, 0, 0, 3, 0])) + self.assertRoundTrip(made.data) + + def test_make_takes_schemas_octets_and_parsed_options(self) -> None: + parsed = self.options(bytes([1, 41, 6, 0, 0, 0, 7, 0])) + rebuilt = self.DCCP(type=3, ack=1, checksum=b'\x00\x00', options=parsed) + self.assertEqual(rebuilt.data[24:], bytes([1, 41, 6, 0, 0, 0, 7, 0])) + layer = self.DCCP(rebuilt.data) + given = self.DCCP(type=3, ack=1, checksum=b'\x00\x00', + options=[b'\x02', layer.__header__.options[1]]) + self.assertEqual(given.data[24:], bytes([2, 41, 6, 0, 0, 0, 7, 0])) + + def test_make_refuses_what_an_option_cannot_hold(self) -> None: + for option in ((self.Option.NDP_Count, {'count': 1 << 48}), + (self.Option.Ack_Vector_Nonce_0, {'vector': [(4, 0)]}), + (self.Option.Ack_Vector_Nonce_0, {'vector': [(0, 64)]}), + (self.Option.Data_Dropped, {'vector': [(None, 128)]}), + (self.Option.Data_Dropped, {'vector': [(8, 0)]}), + (self.Option.Data_Dropped, {'vector': [(1, 16)]}), + (self.Option(3), {'data': b'\x01'}), + (self.Option.Init_Cookie, {'cookie': bytes(254)})): + with self.subTest(option=option): + with self.assertRaises(self.ProtocolError): + self.DCCP(type=3, checksum=b'\x00\x00', options=[option]) + + def test_register_option_and_its_wrapper(self) -> None: + from pcapkit.foundation.registry.protocols import register_dccp_option + from pcapkit.protocols.data.transport.dccp import UnassignedOption + from pcapkit.protocols.schema.transport.dccp import UnassignedOption as Schema_UnassignedOption + from pcapkit.utilities.exceptions import RegistryError + from pcapkit.utilities.warnings import RegistryWarning + + code = self.Option(130) + seen = [] + + def parser(schema: 'object', *, options: 'object') -> 'object': + seen.append(schema.data) + return UnassignedOption(type=schema.type, length=schema.length, data=schema.data[::-1]) + + def maker(code: 'object', opt: 'object' = None, **kwargs: 'object') -> 'object': + return Schema_UnassignedOption(type=code, length=4, data=opt.data[::-1]) + + self.addCleanup(self.DCCP.__option__.pop, code, None) + register_dccp_option(code, (parser, maker)) + options = self.options(bytes([130, 4, 1, 2])) + self.assertEqual((set(seen), options[code].data), ({b'\x01\x02'}, b'\x02\x01')) + with self.assertWarns(RegistryWarning): + self.DCCP.register_option(code, 'timestamp') + self.DCCP.register_option(code, 'timestamp') + with self.assertRaises(RegistryError): + register_dccp_option(code, 'nosuch') + + def test_register_dccp_dispatches_a_port(self) -> None: + from pcapkit.foundation.registry.protocols import register_dccp + from pcapkit.protocols.misc.raw import Raw + + class Probe(Raw): # pylint: disable=too-few-public-methods + """A stand-in application protocol.""" + + self.addCleanup(self.DCCP.__proto__.pop, DST, None) + register_dccp(DST, Probe) + self.assertIsInstance(self.DCCP(packet(2, data=b'app', x=False)).payload, Probe) + + # A port's application type, and a protocol named by its module and class. + from pcapkit.const.reg.apptype import AppType, TransportProtocol + from pcapkit.utilities.warnings import RegistryWarning + with self.assertWarns(RegistryWarning): + register_dccp(AppType.get(DST, proto=TransportProtocol.dccp), 'pcapkit.protocols.misc.raw', 'Raw') + self.assertIs(type(self.DCCP(packet(2, data=b'app', x=False)).payload), Raw) + + +class DCCPChecksumTests(DCCPTestCase): + """The checksum over the IP pseudo-header and the Checksum Coverage.""" + + def parse(self, octets: bytes, cls: 'type | None' = None, *, + verify: bool = True) -> 'tuple[object, list[tuple[type, str]]]': + """``octets`` parsed as ``cls``, IPv4 by default, and its ProtocolWarnings.""" + with mock.patch.object(self.config, 'CHECKSUM_VERIFY', verify), \ + warnings.catch_warnings(record=True) as caught: + warnings.simplefilter('always') + layer = (cls or self.IPv4)(octets) + return layer, [(item.category, str(item.message)) + for item in caught if issubclass(item.category, self.ProtocolWarning)] + + def status(self, octets: bytes, cls: 'type | None' = None, **kwargs: object) -> 'tuple[object, list]': + """The DCCP checksum status of ``octets``, and its ProtocolWarnings.""" + layer, messages = self.parse(octets, cls, **kwargs) + return layer['DCCP'].info.get('checksum_status'), messages + + def test_the_checksummed_packets_verify(self) -> None: + for octets, cls, start in zip(CHECKSUMMED, (self.IPv4, self.IPv4, self.IPv6, self.IPv4), (20, 20, 40, 20)): + with self.subTest(packet=octets.hex()): + datagram = octets[start:] + prefix = (pseudo6 if start == 40 else pseudo4)(len(datagram)) + cscov, hdr_len = datagram[5] & 0x0F, datagram[4] * 4 + covered = hdr_len + (cscov - 1) * 4 if cscov else len(datagram) + self.assertEqual(datagram[6:8], expected(prefix, datagram, covered)) + self.assertEqual(self.status(octets, cls), (self.Status.VALID, [])) + layer, _ = self.parse(octets, cls) + self.assertEqual(cls.from_data(layer.info).data, octets) + + def test_a_corrupted_checksum_warns(self) -> None: + octets = CHECKSUMMED[0][:26] + b'\x8f\x3b' + CHECKSUMMED[0][28:] + status, messages = self.status(octets) + self.assertIs(status, self.Status.INVALID) + self.assertEqual(messages, [(self.ChecksumWarning, 'DCCP: checksum 0x8f3b is wrong; 0x8f3a expected')]) + layer, _ = self.parse(octets) + self.assertEqual(self.IPv4.from_data(layer.info).data, octets) + + def test_data_past_the_coverage_is_not_covered(self) -> None: + # CsCov 2 covers the header and the first 4 data octets; CsCov 1 none of them. + for octets, start in ((CHECKSUMMED[1], 48), (CHECKSUMMED[3], 32)): + with self.subTest(packet=octets.hex()): + changed = octets[:start] + bytes(len(octets) - start) + self.assertEqual(self.status(changed), (self.Status.VALID, [])) + changed = CHECKSUMMED[1][:47] + b'!' + CHECKSUMMED[1][48:] + self.assertIs(self.status(changed)[0], self.Status.INVALID) + + def test_a_coverage_past_the_packet_is_invalid(self) -> None: + octets = bytearray(CHECKSUMMED[3]) + octets[25] = 0x05 # (5 - 1) * 4 octets of data, where it has 5 + status, messages = self.status(bytes(octets)) + self.assertIs(status, self.Status.INVALID) + self.assertEqual(messages, [(self.ChecksumWarning, + 'DCCP: checksum coverage 5 runs past the 17 octets of the packet')]) + # Rebuilt inside IP-in-IP, the inner layer verifies it twice and warns once. + tunnel = wire.ipv4(bytes(octets), proto=4) + layer, _ = self.parse(tunnel) + with mock.patch.object(self.config, 'CHECKSUM_VERIFY', True), \ + warnings.catch_warnings(record=True) as caught: + warnings.simplefilter('always') + self.assertEqual(self.IPv4.from_data(layer.info).data, tunnel) + self.assertEqual([item.category for item in caught if issubclass(item.category, self.ChecksumWarning)], + [self.ChecksumWarning]) + + def test_a_truncated_capture(self) -> None: + self.assertEqual(self.status(CHECKSUMMED[0][:-1]), (self.Status.TRUNCATED, [])) + # What a reduced coverage covers can still be all there. + self.assertEqual(self.status(CHECKSUMMED[1][:-8]), (self.Status.VALID, [])) + layer, _ = self.parse(CHECKSUMMED[0][:-1]) + self.assertEqual(self.IPv4.from_data(layer.info).data, CHECKSUMMED[0][:-1]) + + def test_a_fragment_has_no_pseudo_header(self) -> None: + octets = bytearray(CHECKSUMMED[0]) + octets[6] = 0x20 # More Fragments + octets[10:12] = b'\x00\x00' + octets[10:12] = wire.checksum(bytes(octets[:20])).to_bytes(2, 'big') + self.assertEqual(self.status(bytes(octets)), (self.Status.NO_PSEUDO_HEADER, [])) + + def test_off_verifies_nothing(self) -> None: + self.assertEqual(self.status(CHECKSUMMED[0][:26] + b'\x00\x00' + CHECKSUMMED[0][28:], verify=False), + (None, [])) + + def test_the_ip_layer_computes_it(self) -> None: + dccp = self.DCCP(srcport=SRC, dstport=DST, type=0, seq=1, service_code=0x12345678, + options=[(self.Option.Change_L, {'feature': 1, 'value': b'\x02'})], payload=b'hello') + octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol='DCCP', payload=dccp).data + self.assertEqual(octets, CHECKSUMMED[0]) + dccp = self.DCCP(srcport=SRC, dstport=DST, type=7, seq=9, ack=8, reset_code=5, + reset_data=b'\x20\x01\x02', payload=b'odd') + octets = self.IPv6(src='2001:db8::1', dst='2001:db8::2', next='DCCP', payload=dccp).data + self.assertEqual(octets, CHECKSUMMED[2]) + + def test_make_takes_the_pseudo_header(self) -> None: + pseudo = self.IP_Pseudo(src='192.0.2.1', dst='198.51.100.2', protocol='DCCP', upper_length=0) + made = self.DCCP(srcport=SRC, dstport=DST, type=4, x=False, seq=7, ack=6, cscov=2, + options=[(self.Option.Timestamp, {'timestamp': 5})], + payload=b'0123456789abcdef', pseudo=pseudo).data + self.assertEqual(made, CHECKSUMMED[1][20:]) + + def test_a_coverage_reaching_the_end_is_written_as_zero(self) -> None: + # As Linux's dccp_csum_outgoing: CsCov 0 wherever the coverage reaches the end. + pseudo = self.IP_Pseudo(src='192.0.2.1', dst='198.51.100.2', protocol='DCCP', upper_length=0) + for cscov, payload in ((15, b'abc'), (2, b'abcd')): + with self.subTest(cscov=cscov, payload=payload): + made = self.DCCP(type=2, x=False, ccval=5, cscov=cscov, payload=payload, pseudo=pseudo).data + self.assertEqual(made[5], 0x50) + self.assertEqual(made[6:8], expected(pseudo4(len(made)), made, len(made))) + dccp = self.DCCP(type=2, x=False, ccval=5, cscov=cscov, payload=payload) + octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol='DCCP', payload=dccp).data + self.assertEqual(octets[20:], made) + self.assertEqual(self.status(octets), (self.Status.VALID, [])) + # One short of the end, and one written as given, keep their value. + self.assertEqual(self.DCCP(type=2, x=False, cscov=2, payload=b'abcde', pseudo=pseudo).data[5], 2) + self.assertEqual(self.DCCP(type=2, x=False, cscov=15, checksum=b'\x00\x00', pseudo=pseudo).data[5], 15) + + def test_off_or_given_is_written_as_is(self) -> None: + with mock.patch.object(self.config, 'CHECKSUM_COMPUTE', False): + octets = self.IPv4(src='192.0.2.1', dst='198.51.100.2', protocol='DCCP', + payload=self.DCCP(type=2, x=False)).data + self.assertEqual(octets[26:28], b'\x00\x00') + self.assertEqual(self.DCCP(type=2, checksum=b'\xbe\xef').data[6:8], b'\xbe\xef') + self.assertEqual(self.DCCP(type=2).data[6:8], b'\x00\x00') + + +if __name__ == '__main__': + unittest.main()