From 6dfb55d81b719f740fb80083f7eaf18cd45cd36a Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Mon, 26 Oct 2026 14:55:50 -0400 Subject: [PATCH] docs(link,transport): seventh whole-repo pass over link and transport protocols (#1103) Full read of pcapkit/protocols/link, transport and their data/schema models. - L2TP: the tunnel ID is the Session ID's lower half; L2TPv2.read's version check and Raises. - ARP/Ethernet make: Raises name the address makers' exceptions. - SCTP: padding clamp per RFC 9260 section 3.2, folded padding chunks, SACK a_rwnd, PPID member names. - TCP: Flags has its own _missing_; FieldValueError in MPTCP Raises. --- pcapkit/protocols/link/arp.py | 14 +++++++++----- pcapkit/protocols/link/ethernet.py | 4 ++++ pcapkit/protocols/link/l2tp.py | 9 ++++++--- pcapkit/protocols/link/l2tpv2.py | 7 +++++-- pcapkit/protocols/schema/transport/sctp.py | 5 +++-- pcapkit/protocols/transport/sctp.py | 17 ++++++++++------- pcapkit/protocols/transport/tcp.py | 10 +++++++--- 7 files changed, 44 insertions(+), 22 deletions(-) diff --git a/pcapkit/protocols/link/arp.py b/pcapkit/protocols/link/arp.py index 0af47798bf..479f3225ab 100644 --- a/pcapkit/protocols/link/arp.py +++ b/pcapkit/protocols/link/arp.py @@ -261,10 +261,14 @@ def make(self, *, Constructed packet data. Raises: - ProtocolError: If a packed hardware address is not ``hlen`` octets - long (with ``hlen`` not given and ``htype`` Ethernet, not 6 - octets long), or a packed protocol address is not ``plen`` - octets long. + FieldValueError: If ``spa`` or ``tpa`` is rejected by + :meth:`_make_proto_resolve`, e.g. a :obj:`bool`. + ProtocolError: If ``sha`` or ``tha`` is rejected by + :meth:`_make_addr_resolve`, or ``spa`` or ``tpa`` by + :meth:`_make_proto_resolve`; if a packed hardware address is + not ``hlen`` octets long (with ``hlen`` not given and + ``htype`` Ethernet, not 6 octets long), or a packed protocol + address is not ``plen`` octets long. """ _htype = self._make_index(htype, htype_default, namespace=htype_namespace, @@ -474,7 +478,7 @@ def _make_proto_resolve(self, addr: 'IPv4Address | IPv6Address | str | bytes', p pack as ``00000001`` (IPv4) or ``::1`` (IPv6) with no exception and no warning. - The description below uses :attr:`self.__class__.__name__ + The messages are prefixed with :attr:`self.__class__.__name__ ` rather than :attr:`self.alias `, even though the latter is this project's usual choice: :attr:`alias` here reads diff --git a/pcapkit/protocols/link/ethernet.py b/pcapkit/protocols/link/ethernet.py index 4c51710b3e..6e0b9352fa 100644 --- a/pcapkit/protocols/link/ethernet.py +++ b/pcapkit/protocols/link/ethernet.py @@ -156,6 +156,10 @@ def make(self, Returns: Constructed packet data. + Raises: + ProtocolError: If ``dst`` or ``src`` is rejected by + :meth:`_make_mac_addr`. + """ _type = self._make_index(type, type_default, namespace=type_namespace, reversed=type_reversed, pack=False) diff --git a/pcapkit/protocols/link/l2tp.py b/pcapkit/protocols/link/l2tp.py index e18f82b193..b946c78c2e 100644 --- a/pcapkit/protocols/link/l2tp.py +++ b/pcapkit/protocols/link/l2tp.py @@ -47,10 +47,13 @@ session header is, in :rfc:`3931` §4.1.1.1's own words, *"free of any restrictions imposed by coexistence with L2TPv2 and L2F"* -- a data message opens with the raw 32-bit Session ID and carries **no version nibble at all**, so there is nothing a -v2 parser could test to recognise that the datagram is not its own. Measured, +v2 parser could test to recognise that the datagram is not its own. Without the +version check in :meth:`L2TPv2.read `, that binding reports ``version=4``, ``tunnelid=0x5678`` and ``sessionid=0xff03`` -for a v3-over-IP datagram: a complete header assembled out of the top half of a -Session ID and the first two octets of the PPP frame behind it. 115 is a missing +for a v3-over-IP datagram: a complete header assembled out of a Session ID and +the first two octets of the PPP frame behind it. The check refuses that datagram +only because its Session ID does not put ``2`` in the nibble; one that does +parses as v2. 115 is a missing *class*, not a missing registration, and until that class exists an undissected payload is the honest answer. diff --git a/pcapkit/protocols/link/l2tpv2.py b/pcapkit/protocols/link/l2tpv2.py index 298ec94776..bcdf6095f7 100644 --- a/pcapkit/protocols/link/l2tpv2.py +++ b/pcapkit/protocols/link/l2tpv2.py @@ -186,6 +186,9 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_L2TP': Returns: Parsed packet data. + Raises: + ProtocolError: If the version nibble is not ``2``. + """ schema = self.__header__ _flag = schema.flags @@ -199,8 +202,8 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_L2TP': # reports a tunnel and session ID assembled out of octets that are # neither: an :rfc:`3931` §4.1.1 L2TPv3-over-IP datagram would otherwise # yield ``version=4``, ``tunnelid=0x5678`` and ``sessionid=0xff03``, read - # out of the top half of a Session ID and the first two octets of the PPP - # frame behind it (#548). This is also what keeps the hard-coded + # out of the lower half of a Session ID and the first two octets of the + # PPP frame behind it (#548). This is also what keeps the hard-coded # ``Literal[2]`` of ``version`` true, instead of disagreeing with # ``info.version`` on the same datagram. if _flag['version'] != 2: diff --git a/pcapkit/protocols/schema/transport/sctp.py b/pcapkit/protocols/schema/transport/sctp.py index 58f5d1060e..b501173591 100644 --- a/pcapkit/protocols/schema/transport/sctp.py +++ b/pcapkit/protocols/schema/transport/sctp.py @@ -94,8 +94,9 @@ def padding_length(pkt: 'dict[str, Any]') -> 'int': Returns: Number of padding bytes, clamped to the number of bytes left in the - enclosing structure, since :rfc:`9260#section-3.2` allows the final - padding of a packet to be omitted. When packing, it is clamped as well + enclosing structure, so that a final chunk captured without (all of) + its padding still parses, although :rfc:`9260#section-3.2` has the + sender pad every chunk. When packing, it is clamped as well to the ``__padding__`` width :func:`pre_pack_padding` hands over, so a final chunk captured without (all of) its padding rebuilds without it (:issue:`1474`). diff --git a/pcapkit/protocols/transport/sctp.py b/pcapkit/protocols/transport/sctp.py index f7dcc21963..2af5492854 100644 --- a/pcapkit/protocols/transport/sctp.py +++ b/pcapkit/protocols/transport/sctp.py @@ -325,8 +325,9 @@ class SCTP(Transport[Data_SCTP, Schema_SCTP], >>> SCTP.register(PayloadProtocolIdentifier.PayloadProtocolIdentifier_3GPP_NG_Application_Protocol, NGAP) >>> SCTP.register(60, NGAP) # equivalent, PPID given as a plain integer - By default, PPIDs 60 (``NG_Application_Protocol``) and 66 - (``NGAP_over_DTLS_over_SCTP``) are registered to + By default, PPIDs 60 (``PayloadProtocolIdentifier_3GPP_NG_Application_Protocol``) + and 66 (``PayloadProtocolIdentifier_3GPP_NGAP_over_DTLS_over_SCTP``) are + registered to :class:`~pcapkit.protocols.application.ngap.NGAP`, and every other PPID resolves to :class:`~pcapkit.protocols.misc.raw.Raw`. @@ -1037,10 +1038,12 @@ def _make_sctp_chunks(self, chunks: 'list[Schema_Chunk | tuple[Enum_Chunk, dict[ No alignment fix-up happens here, unlike :meth:`TCP._make_tcp_options `: every - chunk schema carries its own trailing - :class:`~pcapkit.corekit.fields.strings.PaddingField`, computed from - the chunk's own ``length``, so a chunk pads itself to the four-byte - boundary required by :rfc:`9260#section-3.2`. + chunk pads itself to the four-byte boundary required by + :rfc:`9260#section-3.2`. A chunk without a nested list carries its + own trailing :class:`~pcapkit.corekit.fields.strings.PaddingField`, + computed from the chunk's own ``length``; one with a nested + parameter or error cause list folds its padding into that list, see + :func:`~pcapkit.protocols.schema.transport.sctp.nested_length`. """ chunks_list = [] # type: list[Schema_Chunk | bytes] @@ -2068,7 +2071,7 @@ def _make_chunk_init_ack(self, code: 'Enum_Chunk', chunk: 'Optional[Data_INITACK def _make_chunk_sack(self, code: 'Enum_Chunk', chunk: 'Optional[Data_SACKChunk]' = None, *, flags: 'bytes' = b'\x00', cum_tsn_ack: 'int' = 0, - a_rwnd: 'int' = 1500, # minimum permitted by [RFC 9260] + a_rwnd: 'int' = 1500, # the INIT minimum [RFC 9260]; a SACK may advertise less gap_blocks: 'Optional[list[Schema_GapAckBlock | Data_GapAckBlock | tuple[int, int]]]' = None, # pylint: disable=line-too-long dup_tsn: 'Optional[list[int]]' = None, **kwargs: 'Any') -> 'Schema_SACKChunk': diff --git a/pcapkit/protocols/transport/tcp.py b/pcapkit/protocols/transport/tcp.py index 856010acea..bee20b146f 100644 --- a/pcapkit/protocols/transport/tcp.py +++ b/pcapkit/protocols/transport/tcp.py @@ -527,9 +527,9 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_TCP': # segment whose flags octet is all zero: the ``|=`` below is the only promotion and # never runs, and ``Enum_Flags.SYN in self._flags`` would raise ``TypeError: # argument of type 'int' is not a container or iterable``. A segment with any flag - # set would mask that. :class:`Enum_Flags` is an ``aenum.IntFlag`` with no - # ``_missing_`` of its own, so ``Enum_Flags(0)`` is a valid flagless member that - # compares equal to ``0`` and ORs as usual. This is the type :attr:`connection` + # set would mask that. :class:`Enum_Flags` is an ``aenum.IntFlag`` whose + # ``_missing_`` accepts any value from ``0`` to ``0xFFFF``, so ``Enum_Flags(0)`` is + # a valid flagless member that compares equal to ``0`` and ORs as usual. This is the type :attr:`connection` # advertises. _flag = Enum_Flags(0) for key, val in schema.flags.items(): @@ -2163,6 +2163,8 @@ def _make_tcp_options(self, options: 'list[Schema_Option | tuple[Enum_Option, di ProtocolNotImplemented: If an option argument given by name, an MPTCP ``subtype`` or an alternate checksum ``algorithm``, is not in its namespace and has no default. + FieldValueError: If an ADD_ADDR ``addr`` is a :obj:`bool` or not a + valid IP address. Note: The options are emitted in the given order, ``NOP`` and ``EOOL`` @@ -3245,6 +3247,8 @@ def _make_mptcp_addaddr(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_M Raises: ProtocolError: If ``hmac`` is not 8 octets, or is given together with ``echo`` set. + FieldValueError: If ``addr`` is a :obj:`bool` or not a valid IP + address. Note: :rfc:`8684` section 3.4.1 allows an ADD_ADDR without the truncated