From 0d382c302cf9840486a936acf2d2997dcd0af010 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Mon, 5 Oct 2026 02:44:49 -0400 Subject: [PATCH] feat(protocols)!: move OSPF and RARP to the application layer (#719) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layer is decided by designed function, with the IETF as the single source of truth: RFC 1812 §7 places routing protocols in the application layer and RFC 1122 §1.1.3 lists RARP there. Ruled on #719. * `OSPF` moves to `application/` with `Application` as its only base. `RARP` and `DRARP` move as `class RARP(Application, ARP)`: layer base first, because `ARP`'s chain reaches `Link`, which owns `__layer__`. `layer` is now `'Application'` for all three; `ARP` and `InARP` stay `'Link'`. * The matching `data/` and `schema/` modules and the `.rst` pages move too. No re-export is left at the old paths. `pcapkit/const` and `pcapkit/vendor` do not move. * Dispatch keys are unchanged: OSPF stays in `Internet.__proto__` at `TransType.OSPFIGP`, RARP in `Link.__proto__` by EtherType. Only the module each descriptor names changes. * Neither class overrides `__post_init__`, `_decode_next_layer` or `_import_next_layer`; both rely on the `-1` sentinel `Application` now accepts. * `OSPF.__proto__` becomes `ProtocolBase.__proto__`, so OSPF no longer sees Link's EtherType entries. `register` and `_read_protos` still resolve, on `ProtocolBase` rather than Link -- the strict difference Link minus Application minus ProtocolBase is empty. Inert either way: a `-1` lookup misses in both registries, resolves to `Raw`, and inserts nothing on the miss. * New conventions page `protocol-layer-placement`, and a 1.5.0 breaking-change entry listing the before/after import paths. Protochains are unchanged at every `layer=` value, `'internet'` included; only `_sigterm` moves, and on RARP as well as OSPF. Every tests/protocols subtree, tests/project and tests/foundation pass locally, run in separate chunks. --- CHANGELOG.md | 14 + docs/source/changelog/1.5.0.rst | 36 ++ .../conventions/documentation.rst | 3 +- .../source/contributing/conventions/index.rst | 2 + .../contributing/conventions/process.rst | 2 +- .../conventions/protocol-layer-placement.rst | 222 +++++++++++ docs/source/contributing/pep.rst | 17 +- docs/source/ext.rst | 14 +- docs/source/pcapkit/const/arp.rst | 2 +- docs/source/pcapkit/const/ospf.rst | 8 +- .../pcapkit/protocols/application/index.rst | 6 +- .../protocols/{link => application}/ospf.rst | 22 +- .../protocols/{link => application}/rarp.rst | 10 +- docs/source/pcapkit/protocols/index.rst | 21 +- docs/source/pcapkit/protocols/link/index.rst | 2 - docs/source/pcapkit/protocols/link/link.rst | 2 +- docs/source/pcapkit/protocols/link/vlan.rst | 4 +- docs/source/pcapkit/vendor/arp.rst | 2 +- docs/source/pcapkit/vendor/ospf.rst | 8 +- examples/generators/dispatch.py | 12 +- pcapkit/__init__.py | 7 +- pcapkit/const/arp/__init__.py | 2 +- pcapkit/const/ospf/__init__.py | 4 +- pcapkit/protocols/__init__.py | 5 +- pcapkit/protocols/application/__init__.py | 4 + .../protocols/{link => application}/ospf.py | 45 +-- .../protocols/{link => application}/rarp.py | 42 ++- .../protocols/data/application/__init__.py | 8 + .../data/{link => application}/ospf.py | 0 pcapkit/protocols/data/link/__init__.py | 8 - pcapkit/protocols/internet/internet.py | 16 +- pcapkit/protocols/link/__init__.py | 5 +- pcapkit/protocols/link/arp.py | 4 +- pcapkit/protocols/link/c_tag.py | 2 +- pcapkit/protocols/link/l2tpv2.py | 10 +- pcapkit/protocols/link/link.py | 8 +- pcapkit/protocols/link/vlan.py | 4 +- .../protocols/schema/application/__init__.py | 8 + .../schema/{link => application}/ospf.py | 2 +- pcapkit/protocols/schema/link/__init__.py | 4 - pcapkit/vendor/arp/__init__.py | 2 +- pcapkit/vendor/ospf/__init__.py | 4 +- tests/project/test_conventions_doc_claims.py | 8 +- .../application/test_layer_placement_unit.py | 352 ++++++++++++++++++ tests/protocols/application/test_ospf_unit.py | 183 +++++++++ tests/protocols/application/test_rarp_unit.py | 36 ++ tests/protocols/link/test_link_unit.py | 162 +------- .../protocols/test_dispatch_bindings_unit.py | 2 +- .../test_dispatch_reachability_unit.py | 2 +- .../protocols/test_dispatch_registry_unit.py | 2 +- 50 files changed, 1039 insertions(+), 311 deletions(-) create mode 100644 docs/source/contributing/conventions/protocol-layer-placement.rst rename docs/source/pcapkit/protocols/{link => application}/ospf.rst (74%) rename docs/source/pcapkit/protocols/{link => application}/rarp.rst (83%) rename pcapkit/protocols/{link => application}/ospf.py (91%) rename pcapkit/protocols/{link => application}/rarp.py (63%) rename pcapkit/protocols/data/{link => application}/ospf.py (100%) rename pcapkit/protocols/schema/{link => application}/ospf.py (96%) create mode 100644 tests/protocols/application/test_layer_placement_unit.py create mode 100644 tests/protocols/application/test_ospf_unit.py create mode 100644 tests/protocols/application/test_rarp_unit.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c1d403860..ecc73b9984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,20 @@ Preceded by `1.5.0a1` (2026-09-15), `1.5.0b1` and `1.5.0b2` (both 2026-09-18) an #### Changed +- **a breaking change to** `OSPF`, `RARP` and `DRARP`: they move from `pcapkit.protocols.link` to `pcapkit.protocols.application`, with no deprecated re-export at the old path ([#719](https://github.com/JarryShaw/PyPCAPKit/issues/719)). Layer is decided by designed function, with the IETF as the single source of truth -- [RFC 1812 Section 7](https://datatracker.ietf.org/doc/html/rfc1812#section-7) places routing protocols in the application layer and [RFC 1122 Section 1.1.3](https://datatracker.ietf.org/doc/html/rfc1122#section-1.1.3) lists RARP there -- and the rule is recorded in `docs/source/contributing/conventions/protocol-layer-placement.rst`. + +Import paths, before to after: + +- `pcapkit.protocols.link.ospf` to `pcapkit.protocols.application.ospf` +- `pcapkit.protocols.data.link.ospf` to `pcapkit.protocols.data.application.ospf` +- `pcapkit.protocols.schema.link.ospf` to `pcapkit.protocols.schema.application.ospf` +- `pcapkit.protocols.link.rarp` to `pcapkit.protocols.application.rarp` + +`from pcapkit import OSPF` and `from pcapkit.protocols import OSPF` (likewise `RARP` and `DRARP`) are unaffected; `from pcapkit.protocols.link import OSPF` and the deep paths above break. RARP has no `data` or `schema` module of its own, as it reuses ARP's. + +`layer` changes from `'Link'` to `'Application'` for all three. `OSPF` now takes `Application` as its only base, and `RARP` becomes `class RARP(Application, ARP)` -- layer base first, because `ARP`'s chain reaches `Link`, which owns `__layer__`. `ARP` and `InARP` stay `'Link'`. Dispatch keys are unchanged: OSPF is still reached from `Internet.__proto__` at `TransType.OSPFIGP` and RARP from `Link.__proto__` by EtherType. Leaving `Link` repoints `OSPF.__proto__` at `ProtocolBase.__proto__`, so OSPF no longer sees Link's EtherType entries -- which nothing used, since it is reached by protocol number, and a `-1` lookup falls back to `Raw` in either registry without inserting. `register` and `_read_protos` still resolve, on `ProtocolBase` rather than `Link`. `RARP` keeps Link's through `ARP`. + +Layer-limited extraction: `layer='link'` no longer sets the termination flag on OSPF and `layer='application'` now does -- the same inversion applies to RARP -- but the extracted protochain is unchanged at every `layer=` value, including `'internet'` (`IPv4:OSPFIGP` before and after, as IPv4 ends the chain itself). Measured: `IPv4:OSPFv2:Raw` for a headerless `IPV4` capture, `Ethernet:RARP:Raw` for a padded RARP frame. - renames with no compatibility alias left behind: PCAP-NG `Option` subclasses spell the namespace class keyword `ns=` instead of `namespace=` ([#439](https://github.com/JarryShaw/PyPCAPKit/issues/439)). - `tests/protocols/transport/test_tcp_udp_unit.py` now reaches the MP_JOIN dispatchers through `TCP()` itself, instead of assigning a Python `set` to `_flags` on a bare `TCP.__new__(TCP)`. A `set` answers the membership tests `_make_mptcp_join` and `_read_mptcp_join` use, so every flag branch ran and both TCP modules read 100% coverage, while the attribute had neither the `aenum.IntFlag` type production assigns nor the ordering that governs when it exists -- how [#587](https://github.com/JarryShaw/PyPCAPKit/issues/587) stayed invisible behind that number, and the `cast('Enum_Flags', 0)` no-op behind it. Reverting [#587](https://github.com/JarryShaw/PyPCAPKit/issues/587)'s hoist now fails two of the file's 17 tests with `AttributeError: 'TCP' object has no attribute '_flags'`, and restoring the `cast` fails two with `TypeError: argument of type 'int' is not a container or iterable`; all 17 passed before. The library is unchanged and coverage does not move -- the point is what the same numbers are now worth ([#603](https://github.com/JarryShaw/PyPCAPKit/issues/603)). - building a protocol through its constructor with a keyword that names nothing now raises `UnsupportedCall` instead of discarding it. **This is a behaviour change to a public API**: every `make` in the tree ends its signature with `**kwargs` and reads nothing out of it, so a misspelled keyword was accepted, dropped, and the field it named kept its default -- wrong octets, nothing said. That is what [#602](https://github.com/JarryShaw/PyPCAPKit/issues/602) cost: `examples/generators/options.py` asked for `seq=1` where `TCP.make` spells it `seq_no`, and 25 generated fixture frames carried sequence number `0`. [#541](https://github.com/JarryShaw/PyPCAPKit/issues/541) and [#556](https://github.com/JarryShaw/PyPCAPKit/issues/556) were the same silence. The schema layer was never so permissive (`Schema.__update__` warns `UnknownFieldWarning`), and that asymmetry is what this closes. The check sits in `ProtocolBase.__init__` rather than `make`, because `__post_init__` passes one `**kwargs` to the construction *and* to the parse of what it has just constructed, so a keyword declared only by `read` legitimately travels through `make` (`HIP.read` declares `extension`, which `HIP.make` does not, and the option generator depends on it). The accepted set is the union of every keyword-taking parameter of `make`, `read`, `pack`, `unpack`, `__post_init__` and `__init__` across the MRO, computed once per class from `inspect.signature`. Parsing is deliberately untouched: there the keywords are whatever the engines and the four `_import_next_layer` implementations forward, a protocol cannot know which its parent passed on, and a dropped parse keyword changes how a packet is read, not what its octets say. Two opt-in escapes exist per class through a new `__keywords__`: a set, for a keyword read out of `**kwargs` by name (`ESP.read` with `packet`); and `None`, for a dispatcher whose real signature belongs to a class chosen at call time (only `HTTP.make` forwarding to `HTTPv1`/`HTTPv2`). The message names the near neighbour (`seq` reports *did you mean 'seq_no'?*). `from_data` warns `UnknownFieldWarning` rather than raising, because its keywords are whatever `_make_data` returned, so the defect is a key of that mapping disagreeing with the signature it is spread into, and the person who meets it cannot fix it. This surfaced four latent bugs in this repository, all residue of [#602](https://github.com/JarryShaw/PyPCAPKit/issues/602) and fixed here: the `_TCP_BASE` of `examples/generators/dispatch.py` and three stale copies under `tests/protocols/transport/`, each building segments with sequence number `0`. Three more are reported, not fixed, being a defect per protocol: `Frame._make_data` returns `ts_src` where `make` declares `ts_sec`, `L2TPv2._make_data` returns `prio` where it declares `priority`, and `Header._make_data` returns a `magic_number` that `Header.make` does not take, so `from_data` has been dropping a frame's timestamp, an L2TPv2 priority bit and a capture's byte order, and now says so. One limitation: a *direct* `SomeProtocol.make(...)` call is not checked and still discards silently, since the check sits where every producer's keywords converge rather than inside each of the 30 `make` implementations; `object.__new__(cls).make(**kwargs)`, the idiom `HTTP.make` uses, reaches it ([#617](https://github.com/JarryShaw/PyPCAPKit/issues/617)). diff --git a/docs/source/changelog/1.5.0.rst b/docs/source/changelog/1.5.0.rst index f22428c920..418bc946b5 100644 --- a/docs/source/changelog/1.5.0.rst +++ b/docs/source/changelog/1.5.0.rst @@ -1151,6 +1151,42 @@ Added Changed ~~~~~~~ +* **a breaking change to** ``OSPF``, ``RARP`` and ``DRARP``: they move from + ``pcapkit.protocols.link`` to ``pcapkit.protocols.application``, with no deprecated + re-export at the old path (:issue:`719`). Layer is decided by designed function, with + the IETF as the single source of truth -- :rfc:`1812#section-7` places routing + protocols in the application layer and :rfc:`1122#section-1.1.3` lists RARP there -- + and the rule is recorded in ``docs/source/contributing/conventions/protocol-layer-placement.rst``. + + Import paths, before to after: + + * ``pcapkit.protocols.link.ospf`` to ``pcapkit.protocols.application.ospf`` + * ``pcapkit.protocols.data.link.ospf`` to ``pcapkit.protocols.data.application.ospf`` + * ``pcapkit.protocols.schema.link.ospf`` to ``pcapkit.protocols.schema.application.ospf`` + * ``pcapkit.protocols.link.rarp`` to ``pcapkit.protocols.application.rarp`` + + ``from pcapkit import OSPF`` and ``from pcapkit.protocols import OSPF`` (likewise + ``RARP`` and ``DRARP``) are unaffected; ``from pcapkit.protocols.link import OSPF`` + and the deep paths above break. RARP has no ``data`` or ``schema`` module of its own, as + it reuses ARP's. + + ``layer`` changes from ``'Link'`` to ``'Application'`` for all three. ``OSPF`` now takes + ``Application`` as its only base, and ``RARP`` becomes ``class RARP(Application, ARP)`` + -- layer base first, because ``ARP``'s chain reaches ``Link``, which owns + ``__layer__``. ``ARP`` and ``InARP`` stay ``'Link'``. Dispatch keys are unchanged: + OSPF is still reached from ``Internet.__proto__`` at ``TransType.OSPFIGP`` and RARP + from ``Link.__proto__`` by EtherType. Leaving ``Link`` repoints ``OSPF.__proto__`` at + ``ProtocolBase.__proto__``, so OSPF no longer sees Link's EtherType entries -- which + nothing used, since it is reached by protocol number, and a ``-1`` lookup falls back to + ``Raw`` in either registry without inserting. ``register`` and ``_read_protos`` still + resolve, on ``ProtocolBase`` rather than ``Link``. ``RARP`` keeps Link's through ``ARP``. + + Layer-limited extraction: ``layer='link'`` no longer sets the termination flag on OSPF + and ``layer='application'`` now does -- the same inversion applies to RARP -- but the + extracted protochain is unchanged at every + ``layer=`` value, including ``'internet'`` (``IPv4:OSPFIGP`` before and after, as IPv4 + ends the chain itself). Measured: ``IPv4:OSPFv2:Raw`` for a headerless ``IPV4`` capture, + ``Ethernet:RARP:Raw`` for a padded RARP frame. * renames with no compatibility alias left behind: PCAP-NG ``Option`` subclasses spell the namespace class keyword ``ns=`` instead of ``namespace=`` (:issue:`439`). * ``tests/protocols/transport/test_tcp_udp_unit.py`` now reaches the MP_JOIN diff --git a/docs/source/contributing/conventions/documentation.rst b/docs/source/contributing/conventions/documentation.rst index a128671b3d..58a5204afd 100644 --- a/docs/source/contributing/conventions/documentation.rst +++ b/docs/source/contributing/conventions/documentation.rst @@ -230,7 +230,8 @@ check the members one at a time.** Several of :issue:`719`'s findings were of ex shape: * A sweep asserted that every ``.. module::`` target in the documentation resolved. - One did not: :file:`docs/source/pcapkit/protocols/link/rarp.rst` declared + One did not: :file:`docs/source/pcapkit/protocols/link/rarp.rst` (its path at the + time; now under :file:`application/`) declared ``pcapkit.protocols.data.link.rarp``, which has never existed, because RARP and DRARP reuse ARP's data class. Fixed in ``68fbccd90``. * :issue:`911`'s ruling -- export the sentinel objects and leave their types out -- was diff --git a/docs/source/contributing/conventions/index.rst b/docs/source/contributing/conventions/index.rst index dbfe01ec06..562459cf18 100644 --- a/docs/source/contributing/conventions/index.rst +++ b/docs/source/contributing/conventions/index.rst @@ -12,6 +12,7 @@ House Conventions :mod:`pcapkit.const`, which is where the settled questions have mostly arisen. :ref:`sentinel-convention` governs :mod:`pcapkit.corekit`, :ref:`extension-header-subclassing` a protocol class hierarchy, + :ref:`protocol-layer-placement` which subpackage a dissector belongs in, :ref:`process` the repository rather than any of its code, and :ref:`documentation` the prose itself -- on these pages and in the API reference -- rather than any code at all. @@ -31,5 +32,6 @@ House Conventions sentinel-convention registry-protocol extension-header-subclassing + protocol-layer-placement process documentation diff --git a/docs/source/contributing/conventions/process.rst b/docs/source/contributing/conventions/process.rst index e3c9cab985..ef5c66b498 100644 --- a/docs/source/contributing/conventions/process.rst +++ b/docs/source/contributing/conventions/process.rst @@ -101,7 +101,7 @@ commands down instead of a figure that will be stale by the next merge: The grouping scheme was settled on :issue:`918`: **a section per top-level module, with** ``Added``/``Changed``/``Fixed`` **nested inside each** -- module granularity, not per-file and not per-subpackage. The file carries **9** module-level sections holding -161 entries, and no entry carries an inline kind label:: +162 entries, and no entry carries an inline kind label:: $ grep -cE '^\* \*\*(Added|Changed|Fixed)\*\*' docs/source/changelog/1.5.0.rst 0 diff --git a/docs/source/contributing/conventions/protocol-layer-placement.rst b/docs/source/contributing/conventions/protocol-layer-placement.rst new file mode 100644 index 0000000000..2f27cd7d99 --- /dev/null +++ b/docs/source/contributing/conventions/protocol-layer-placement.rst @@ -0,0 +1,222 @@ +.. _protocol-layer-placement: + +Protocol Layer Placement +------------------------ + +Which subpackage of :mod:`pcapkit.protocols` a new dissector belongs in, and which +base classes it names. Settled by the owner on +:issue:`719`. + +The Rule +~~~~~~~~ + +#. **Layer is decided by designed function, not by encapsulation.** What carries a + protocol on the wire is a separate question from what the protocol is *for*. +#. **The IETF is the single source of truth** -- :rfc:`1122#section-1.1.3` and + :rfc:`1812#section-7`. Where they place a protocol, so does this package. +#. **The operative question for a new protocol** is: *is this protocol a user of the + stack, or part of its forwarding path?* A user is application-layer; the + forwarding path is link, internet or transport by its own function. +#. **In the inheritance chain the layer base comes first, then the protocol family.** +#. **Encapsulation is the last tie-break**, never the first. + +The subpackages, functionally rather than by carrier: + +.. list-table:: + :header-rows: 1 + :widths: 22 78 + + * - Subpackage + - What it holds + * - :mod:`~pcapkit.protocols.link` + - frames, and link-address or tagging protocols + * - :mod:`~pcapkit.protocols.internet` + - layer-3 addressing and the forwarding path itself -- + :rfc:`1812#section-4.1` confines it to IP, ICMP and IGMP + * - :mod:`~pcapkit.protocols.transport` + - IANA transport protocols + * - :mod:`~pcapkit.protocols.application` + - protocols that are **users of the stack rather than part of its forwarding + path** + * - :mod:`~pcapkit.protocols.misc` + - file formats and layerless sentinels + +.. important:: + + :mod:`~pcapkit.protocols.application` is **not** "payloads reached by port or SCTP + PPID". :class:`~pcapkit.protocols.application.ospf.OSPF` is reached by IANA protocol + number and :class:`~pcapkit.protocols.application.rarp.RARP` by EtherType, and + neither has a port. + +Why Function and Not Encapsulation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:rfc:`1122` settles this inside one document. It lists **RARP in the application +layer** at :rfc:`1122#section-1.1.3`, among the *"support protocols, used for host +name mapping, booting, and management"*, while **ARP** sits in the Link Layer chapter +at :rfc:`1122#section-2.3.2`. Two sibling protocols, one frame format, one EtherType, +two layers -- decided on function alone. + +OSI says the same by construction. ITU-T X.200 §9.2.4.4 has a protocol declare its own +layer in terms of *"functions which pertain to a particular layer"*, and clause 7 +defines every layer by its purpose rather than by what encapsulates it. + +IANA Assignments Carry No Layer Claim +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +All three registries this package dispatches on -- *Protocol Numbers*, *Ether Types*, +and *Service Name and Transport Protocol Port Number* -- have **no layer field**. +*Protocol Numbers* frames itself as identifying *"the next level protocol"*, i.e. the +**encapsulated** one. A registry assignment is evidence of encapsulation and of +nothing else. + +.. caution:: + + This **retires the registration grid at** :file:`docs/source/ext.rst` (the + Protocol Type → Registry Function table) **as a placement rule.** It is a guide to + which registrar to call when extending the library, and it was never a layer + taxonomy. Read literally as one it makes ARP, RARP and both VLAN tags + internet-layer and all eight IPv6 extension headers transport-layer. A + contributor looking for a placement rule will find that grid first, which is why + this page says so explicitly. + +Routing Protocols: Control Plane, Not Data Plane +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:rfc:`1812` is Standards Track and organises itself by layer. Its §7 is titled +**"APPLICATION LAYER - ROUTING PROTOCOLS"**, with OSPF at §7.2.2, while +:rfc:`1812#section-4.1` reads *"This chapter and chapter 5 discuss the protocols used +at the Internet Layer: IP, ICMP, and IGMP."* So a routing protocol is application-layer +and the internet layer holds three protocols. + +**X.200 does not contradict this, and must not be cited as though it did.** X.200 +§7.5.2.1 says the Network Layer provides transport *"independence of routing and relay +considerations"* -- that is the network layer **performing forwarding** so transport +need not care, which is the data plane. X.200 treats routing as a *generic* function +parameterised by layer: §5.4.1.4 defines it as *"a function within a layer"*, and §5.9 +as *"a routing function within the (N)-layer enables communication to be relayed by a +chain of (N)-entities."* An (N)-layer function is not a layer assignment, and **X.200 +assigns none**. + +The distinction that dissolves the apparent conflict: computing a forwarding table is +the **control plane** and makes the protocol a user of the stack; forwarding packets is +the **data plane** and is internet-layer. OSPF does the former. + +Tunnelling Protocols Follow Their Payload +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A tunnelling protocol is placed by what it carries, not by what carries it. So +:class:`~pcapkit.protocols.link.l2tp.L2TP` and +:class:`~pcapkit.protocols.link.l2tpv2.L2TPv2` stay in +:mod:`~pcapkit.protocols.link` and are **not** application-layer, even though L2TP is +commonly seen over UDP. + +:rfc:`4949`'s ``$ tunnel`` entry is the citation: a tunnel is *"a logical +point-to-point link -- i.e., an OSIRM Layer 2 connection"*, and a tunnelling protocol +*"e.g., L2TP"* is *"layered below the tunneled Layer 2 protocol and above the +encapsulating protocol."* :rfc:`2473#section-3` and :rfc:`4213#section-3.4` (*"the +tunnel is a link"*) agree. + +The carrier was never single-valued for L2TP anyway: :rfc:`3931#section-4.1` makes +L2TP-over-IP (protocol 115) a **MUST** and UDP/1701 only a **SHOULD**. + +The Inheritance Convention +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Layer base first, protocol family second.** Where a protocol's layer and its parsing +family disagree, it names both, in that order. + +The order is load-bearing, and the mechanism is checkable. ``__layer__`` is resolved by +the MRO, so whichever base owns it *earliest* wins: + +.. code-block:: pycon + + >>> from pcapkit.protocols.link.link import Link + >>> from pcapkit.protocols.misc.raw import Raw + >>> '__layer__' in vars(Link), '__layer__' in vars(Raw) + (True, False) + +:class:`~pcapkit.protocols.misc.raw.Raw` does **not** own ``__layer__`` -- it inherits +:obj:`None` from :class:`~pcapkit.protocols.protocol.ProtocolBase` -- so +:class:`~pcapkit.protocols.application.ftp.FTP_DATA` would report ``'Application'`` in +either base order. :class:`~pcapkit.protocols.link.arp.ARP`'s chain reaches +:class:`~pcapkit.protocols.link.link.Link`, which **does** own it, so +``class RARP(ARP, Application)`` would report ``'Link'`` and be wrong. + +Ordering layer-first is correct in both cases, which is the point: a contributor does +not have to work out which case they are in. + +Two Things That Will Look Like Mistakes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Both are deliberate. Do not "fix" either. + +**A parsing base may live in another subpackage.** +:mod:`pcapkit.protocols.application.rarp` imports +:class:`~pcapkit.protocols.link.arp.ARP` from :mod:`pcapkit.protocols.link.arp`. This +is the first ``application/ → link/`` dependency in the package, and it is the honest +consequence of letting layer and parser disagree. + +**The dispatch tier is decoupled from the subpackage.** A protocol's subpackage says +nothing about which registry reaches it, and the move did not touch any dispatch key: + +.. list-table:: + :header-rows: 1 + :widths: 24 38 38 + + * - Class + - Dispatched from + - Subpackage + * - :class:`~pcapkit.protocols.application.ospf.OSPF` + - ``Internet.__proto__`` by + :attr:`~pcapkit.const.reg.transtype.TransType.OSPFIGP` + - :mod:`~pcapkit.protocols.application` + * - :class:`~pcapkit.protocols.application.rarp.RARP` + - ``Link.__proto__`` by EtherType + - :mod:`~pcapkit.protocols.application` + +Nothing else needs overriding. :class:`~pcapkit.protocols.application.application.Application` +accepts the ``-1`` sentinel (the undissected remainder) and resolves it to +:class:`~pcapkit.protocols.misc.raw.Raw`, so OSPF's body and the padding Ethernet adds +to a short RARP frame attach without either class touching +``__post_init__``, ``_decode_next_layer`` or ``_import_next_layer``. Only a real +protocol number is refused. + +**Swapping** :class:`~pcapkit.protocols.link.link.Link` **for** +:class:`~pcapkit.protocols.application.application.Application` **is not name-for-name.** +Counting the names each class body sets, whether new or overriding +:class:`~pcapkit.protocols.protocol.ProtocolBase`'s, ``Link`` has +seven -- ``__data__``, ``__layer__``, ``__proto__``, ``__schema__``, +``_read_protos``, ``register`` and ``layer`` -- and ``Application`` eight: the four +they share (``__data__``, ``__layer__``, ``__schema__``, ``layer``) plus +``__index__``, ``__post_init__``, ``_decode_next_layer`` and ``_import_next_layer``. +``Link`` overrides three of those that ``Application`` does not -- ``__proto__``, +``register`` and ``_read_protos`` -- but all three also exist on ``ProtocolBase``, so +the strict difference ``Link`` minus ``Application`` minus ``ProtocolBase`` is **empty** +and every one of them still resolves on ``OSPF``. What changes is which registry it +resolves *to*: ``OSPF.__proto__`` is now ``ProtocolBase.__proto__`` rather than +``Link.__proto__``, so OSPF no longer sees Link's EtherType entries. That is inert -- +nothing reaches OSPF by EtherType, and a ``-1`` lookup misses in both registries alike +and resolves to :class:`~pcapkit.protocols.misc.raw.Raw`, inserting nothing on the miss. +``RARP`` keeps ``Link``'s through ``ARP``. + +Tie-Break Order +~~~~~~~~~~~~~~~ + +For a genuinely ambiguous protocol, in this order: + +#. the protocol's own RFC's **functional self-description**; +#. the placement of its **closest functional peers** already in this package; +#. **encapsulation**, last. + +Why Not OSI's Seven Layers +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Asked and declined on :issue:`719`. The +four-layer Internet model is what the public API already declares -- +``Layers = Literal['link', 'internet', 'transport', 'application', 'none']`` in +:mod:`pcapkit.foundation.extraction` -- and the session and presentation layers have +no header on the wire and no IANA registry to dispatch on, so as subpackages they +would be structurally empty rather than merely sparse. OSI also resolves neither of +the cases that prompted this page: routing's control/data-plane split and L2TP's +carrier/payload split exist identically in both models. diff --git a/docs/source/contributing/pep.rst b/docs/source/contributing/pep.rst index 14561e874f..5d1c3792ed 100644 --- a/docs/source/contributing/pep.rst +++ b/docs/source/contributing/pep.rst @@ -284,7 +284,7 @@ than trusted to stay current: Most of those want a dissector written and are covered by the stub list above. A handful wanted only a table entry, because the dissector was already there: -* **Done.** :class:`~pcapkit.protocols.link.ospf.OSPF` is bound at +* **Done.** :class:`~pcapkit.protocols.application.ospf.OSPF` is bound at ``TransType`` 89 (``OSPFIGP``) and :class:`~pcapkit.protocols.link.l2tpv2.L2TPv2` at UDP port 1701. ``__index__`` raises on both, which is correct: neither is reached through a @@ -321,13 +321,12 @@ Three follow-ups the above deliberately left alone: being that index. :mod:`pcapkit.protocols.link.l2tp` records what v3 needs, and what ``L2F`` needs alongside it: the version nibble reading ``1`` selects L2F [:rfc:`2341`], a separate protocol, not an earlier L2TP. -* :class:`~pcapkit.protocols.link.ospf.OSPF` and the - :class:`~pcapkit.protocols.link.l2tp.L2TP` family both live under - :mod:`pcapkit.protocols.link` and so report ``layer == 'Link'``, although one - is carried inside IP and the other inside UDP. Moving them would change their - public import paths, so the misclassification is documented rather than - fixed. It is inert for layer-limited extraction, since IPv4 and IPv6 terminate - an ``internet`` extraction before either is reached. +* The :class:`~pcapkit.protocols.link.l2tp.L2TP` family lives under + :mod:`pcapkit.protocols.link` and so reports ``layer == 'Link'``, although it + is carried inside UDP. That is deliberate: a tunnelling protocol is placed by what + it carries (:ref:`protocol-layer-placement`). It is inert for layer-limited + extraction, since IPv4 and IPv6 terminate an ``internet`` extraction before it + is reached. * **Done.** :attr:`TCP.__proto__ ` and :attr:`UDP.__proto__ ` both point their HTTP ports at @@ -748,7 +747,7 @@ Ten protocols parse a checksum or CRC field — :class:`~pcapkit.protocols.internet.ipv6_opts.IPv6_Opts`, :class:`~pcapkit.protocols.internet.ipx.IPX`, :class:`~pcapkit.protocols.internet.mh.MH`, -:class:`~pcapkit.protocols.link.ospf.OSPF`, +:class:`~pcapkit.protocols.application.ospf.OSPF`, :class:`~pcapkit.protocols.transport.sctp.SCTP`, :class:`~pcapkit.protocols.transport.tcp.TCP` and :class:`~pcapkit.protocols.transport.udp.UDP` — and exactly one of them checks diff --git a/docs/source/ext.rst b/docs/source/ext.rst index 8595c8d739..3e8dac47ed 100644 --- a/docs/source/ext.rst +++ b/docs/source/ext.rst @@ -19,12 +19,8 @@ The protocol classes :mod:`pcapkit` ships: | Protocol Type | Protocol Class | +==================================================================+================+==============+======================================================================+ | | | :class:`pcapkit.protocols.link.arp.ARP` | -+ + +-----------------------+-------------------------------------------------------------+ -| | | :class:`pcapkit.protocols.link.arp.InARP` | + + ARP Family +-----------------------+-------------------------------------------------------------+ -| | | | :class:`pcapkit.protocols.link.rarp.RARP` | -+ + + RARP Family +-------------------------------------------------------------+ -| | | | :class:`pcapkit.protocols.link.rarp.DRARP` | +| | | :class:`pcapkit.protocols.link.arp.InARP` | + Link Layer +----------------+-----------------------+-------------------------------------------------------------+ | (:class:`~pcapkit.protocols.link.link.Link` subclasses) | :class:`pcapkit.protocols.link.ethernet.Ethernet` | + +----------------+-----------------------+-------------------------------------------------------------+ @@ -32,8 +28,6 @@ The protocol classes :mod:`pcapkit` ships: + + +-----------------------+-------------------------------------------------------------+ | | L2TP Family | :class:`pcapkit.protocols.link.l2tpv2.L2TPv2` | + +----------------+-----------------------+-------------------------------------------------------------+ -| | :class:`pcapkit.protocols.link.ospf.OSPF` | -+ +----------------+-----------------------+-------------------------------------------------------------+ | | | :class:`pcapkit.protocols.link.vlan.VLAN` | + + +-----------------------+-------------------------------------------------------------+ | | VLAN Family | :class:`pcapkit.protocols.link.c_tag.C_Tag` | @@ -83,6 +77,12 @@ The protocol classes :mod:`pcapkit` ships: | | | :class:`pcapkit.protocols.application.httpv2.HTTP` | + +----------------+-----------------------+-------------------------------------------------------------+ | | :class:`pcapkit.protocols.application.ngap.NGAP` | ++ +----------------+-----------------------+-------------------------------------------------------------+ +| | :class:`pcapkit.protocols.application.ospf.OSPF` | ++ +----------------+-----------------------+-------------------------------------------------------------+ +| | | :class:`pcapkit.protocols.application.rarp.RARP` | ++ + RARP Family +-----------------------+-------------------------------------------------------------+ +| | | :class:`pcapkit.protocols.application.rarp.DRARP` | +------------------------------------------------------------------+----------------+-----------------------+-------------------------------------------------------------+ | | | :class:`pcapkit.protocols.misc.pcap.header.Header` | + + PCAP Format +-----------------------+-------------------------------------------------------------+ diff --git a/docs/source/pcapkit/const/arp.rst b/docs/source/pcapkit/const/arp.rst index b853109f18..4d3bdb8b6e 100644 --- a/docs/source/pcapkit/const/arp.rst +++ b/docs/source/pcapkit/const/arp.rst @@ -5,7 +5,7 @@ .. module:: pcapkit.const.arp This module contains all constant enumerations of :class:`~pcapkit.protocols.link.arp.ARP` -and :class:`~pcapkit.protocols.link.rarp.RARP` implementations. Available +and :class:`~pcapkit.protocols.application.rarp.RARP` implementations. Available enumerations include: .. list-table:: diff --git a/docs/source/pcapkit/const/ospf.rst b/docs/source/pcapkit/const/ospf.rst index f7d9eeeba6..3d2ddfb4c0 100644 --- a/docs/source/pcapkit/const/ospf.rst +++ b/docs/source/pcapkit/const/ospf.rst @@ -1,11 +1,11 @@ -================================================================ -:class:`~pcapkit.protocols.link.ospf.OSPF` Constant Enumerations -================================================================ +======================================================================= +:class:`~pcapkit.protocols.application.ospf.OSPF` Constant Enumerations +======================================================================= .. module:: pcapkit.const.ospf This module contains all constant enumerations of -:class:`~pcapkit.protocols.link.ospf.OSPF` implementations. Available +:class:`~pcapkit.protocols.application.ospf.OSPF` implementations. Available enumerations include: .. list-table:: diff --git a/docs/source/pcapkit/protocols/application/index.rst b/docs/source/pcapkit/protocols/application/index.rst index 0423d668e4..f909d8e97e 100644 --- a/docs/source/pcapkit/protocols/application/index.rst +++ b/docs/source/pcapkit/protocols/application/index.rst @@ -6,7 +6,9 @@ Application Layer .. module:: pcapkit.protocols.schema.application :mod:`pcapkit.protocols.application` is collection of all protocols in -application layer, with detailed implementation and methods. +application layer, with detailed implementation and methods. Layer is decided by +function rather than encapsulation, see +:doc:`/contributing/conventions/protocol-layer-placement`. .. toctree:: :maxdepth: 1 @@ -17,6 +19,8 @@ application layer, with detailed implementation and methods. httpv2 ftp ngap + ospf + rarp .. todo:: diff --git a/docs/source/pcapkit/protocols/link/ospf.rst b/docs/source/pcapkit/protocols/application/ospf.rst similarity index 74% rename from docs/source/pcapkit/protocols/link/ospf.rst rename to docs/source/pcapkit/protocols/application/ospf.rst index eac567f2c7..883c7c37de 100644 --- a/docs/source/pcapkit/protocols/link/ospf.rst +++ b/docs/source/pcapkit/protocols/application/ospf.rst @@ -1,10 +1,10 @@ OSPF - Open Shortest Path First =============================== -.. module:: pcapkit.protocols.link.ospf +.. module:: pcapkit.protocols.application.ospf -:mod:`pcapkit.protocols.link.ospf` contains -:class:`~pcapkit.protocols.link.ospf.OSPF` only, +:mod:`pcapkit.protocols.application.ospf` contains +:class:`~pcapkit.protocols.application.ospf.OSPF` only, which implements extractor for Open Shortest Path First (OSPF) [*]_, whose structure is described as below: @@ -35,7 +35,7 @@ as below:
-.. autoclass:: pcapkit.protocols.link.ospf.OSPF +.. autoclass:: pcapkit.protocols.application.ospf.OSPF :no-members: :show-inheritance: @@ -57,31 +57,31 @@ as below: Header Schemas -------------- -.. module:: pcapkit.protocols.schema.link.ospf +.. module:: pcapkit.protocols.schema.application.ospf -.. autoclass:: pcapkit.protocols.schema.link.ospf.OSPF +.. autoclass:: pcapkit.protocols.schema.application.ospf.OSPF :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.schema.link.ospf.CrytographicAuthentication +.. autoclass:: pcapkit.protocols.schema.application.ospf.CrytographicAuthentication :members: :show-inheritance: Auxiliary Functions ~~~~~~~~~~~~~~~~~~~ -.. autofunction:: pcapkit.protocols.schema.link.ospf.ospf_auth_data_selector +.. autofunction:: pcapkit.protocols.schema.application.ospf.ospf_auth_data_selector Data Models ----------- -.. module:: pcapkit.protocols.data.link.ospf +.. module:: pcapkit.protocols.data.application.ospf -.. autoclass:: pcapkit.protocols.data.link.ospf.OSPF +.. autoclass:: pcapkit.protocols.data.application.ospf.OSPF :members: :show-inheritance: -.. autoclass:: pcapkit.protocols.data.link.ospf.CrytographicAuthentication +.. autoclass:: pcapkit.protocols.data.application.ospf.CrytographicAuthentication :members: :show-inheritance: diff --git a/docs/source/pcapkit/protocols/link/rarp.rst b/docs/source/pcapkit/protocols/application/rarp.rst similarity index 83% rename from docs/source/pcapkit/protocols/link/rarp.rst rename to docs/source/pcapkit/protocols/application/rarp.rst index 8546fdcb35..7350e660a8 100644 --- a/docs/source/pcapkit/protocols/link/rarp.rst +++ b/docs/source/pcapkit/protocols/application/rarp.rst @@ -1,10 +1,10 @@ RARP/DRARP - (Dynamic) Reverse Address Resolution Protocol ========================================================== -.. module:: pcapkit.protocols.link.rarp +.. module:: pcapkit.protocols.application.rarp -:mod:`pcapkit.protocols.link.rarp` contains -:class:`~pcapkit.protocols.link.rarp.RARP` only, +:mod:`pcapkit.protocols.application.rarp` contains +:class:`~pcapkit.protocols.application.rarp.RARP` only, which implements extractor for (Dynamic) Reverse Address Resolution Protocol (RARP/DRARP) [*]_, whose structure is described as below: @@ -23,7 +23,7 @@ Octets Bits Name Description 24 192 ``rarp.tpa`` Target Protocol Address ====== ========= ========================= ========================= -.. autoclass:: pcapkit.protocols.link.rarp.RARP +.. autoclass:: pcapkit.protocols.application.rarp.RARP :no-members: :show-inheritance: @@ -31,7 +31,7 @@ Octets Bits Name Description .. automethod:: __index__ -.. autoclass:: pcapkit.protocols.link.rarp.DRARP +.. autoclass:: pcapkit.protocols.application.rarp.DRARP :no-members: :show-inheritance: diff --git a/docs/source/pcapkit/protocols/index.rst b/docs/source/pcapkit/protocols/index.rst index 876bdabd11..8941a79903 100644 --- a/docs/source/pcapkit/protocols/index.rst +++ b/docs/source/pcapkit/protocols/index.rst @@ -29,14 +29,10 @@ diagram of the class hierarchy of :mod:`pcapkit.protocols`: A{{ProtocolMeta}} -.->|metaclass| B(ProtocolBase) subgraph link [Link Layer] - Link --> Ethernet & L2TP & OSPF & VLAN & ARP + Link --> Ethernet & L2TP & VLAN & ARP subgraph arp [ARP Family] - ARP --> InARP & RARP - - subgraph rarp [RARP Family] - RARP --> DRARP - end + ARP --> InARP end subgraph vlan [VLAN Family] @@ -69,7 +65,7 @@ diagram of the class hierarchy of :mod:`pcapkit.protocols`: end subgraph application [Application Layer] - Application --> HTTP & FTP + Application --> HTTP & FTP & OSPF & RARP subgraph http [HTTP Family] HTTP --> h1["HTTP/1.*"] & h2["HTTP/2"] @@ -78,6 +74,10 @@ diagram of the class hierarchy of :mod:`pcapkit.protocols`: subgraph ftp [FTP Family] FTP & FTP_DATA end + + subgraph rarp [RARP Family] + RARP --> DRARP + end end subgraph misc [Miscellaneous] @@ -96,6 +96,7 @@ diagram of the class hierarchy of :mod:`pcapkit.protocols`: B --> Header & Frame & PCAPNG & Raw & NoPayload Raw --> FTP_DATA + ARP --> RARP B --> C(Protocol) C --> D([user customisation ...]) @@ -109,14 +110,14 @@ diagram of the class hierarchy of :mod:`pcapkit.protocols`: click Ethernet "/pcapkit/protocols/link/ethernet.html#pcapkit.protocols.link.ethernet.Ethernet" click L2TP "/pcapkit/protocols/link/l2tp.html#pcapkit.protocols.link.l2tp.L2TP" click L2TPv2 "/pcapkit/protocols/link/l2tpv2.html#pcapkit.protocols.link.l2tpv2.L2TPv2" - click OSPF "/pcapkit/protocols/link/ospf.html#pcapkit.protocols.link.ospf.OSPF" + click OSPF "/pcapkit/protocols/application/ospf.html#pcapkit.protocols.application.ospf.OSPF" click VLAN "/pcapkit/protocols/link/vlan.html#pcapkit.protocols.link.vlan.VLAN" click C_Tag "/pcapkit/protocols/link/c_tag.html#pcapkit.protocols.link.c_tag.C_Tag" click S_Tag "/pcapkit/protocols/link/s_tag.html#pcapkit.protocols.link.s_tag.S_Tag" click ARP "/pcapkit/protocols/link/arp.html#pcapkit.protocols.link.arp.ARP" click InARP "/pcapkit/protocols/link/arp.html#pcapkit.protocols.link.arp.InARP" - click RARP "/pcapkit/protocols/link/rarp.html#pcapkit.protocols.link.rarp.RARP" - click DRARP "/pcapkit/protocols/link/rarp.html#pcapkit.protocols.link.rarp.DRARP" + click RARP "/pcapkit/protocols/application/rarp.html#pcapkit.protocols.application.rarp.RARP" + click DRARP "/pcapkit/protocols/application/rarp.html#pcapkit.protocols.application.rarp.DRARP" click Internet "/pcapkit/protocols/internet/internet.html#pcapkit.protocols.internet.Internet" click AH "/pcapkit/protocols/internet/ah.html#pcapkit.protocols.internet.ah.AH" diff --git a/docs/source/pcapkit/protocols/link/index.rst b/docs/source/pcapkit/protocols/link/index.rst index 71a34eaede..ca697e1b4a 100644 --- a/docs/source/pcapkit/protocols/link/index.rst +++ b/docs/source/pcapkit/protocols/link/index.rst @@ -14,10 +14,8 @@ link layer, with detailed implementation and methods. link ethernet arp - rarp l2tp l2tpv2 - ospf vlan c_tag s_tag diff --git a/docs/source/pcapkit/protocols/link/link.rst b/docs/source/pcapkit/protocols/link/link.rst index 1801a34fe1..6babae7922 100644 --- a/docs/source/pcapkit/protocols/link/link.rst +++ b/docs/source/pcapkit/protocols/link/link.rst @@ -6,7 +6,7 @@ Base Protocol :mod:`pcapkit.protocols.link.link` contains :class:`~pcapkit.protocols.link.link.Link`, which is a base class for link layer protocols, e.g. :class:`~pcapkit.protocols.link.arp.ARP`/InARP, :class:`~pcapkit.protocols.link.ethernet.Ethernet`, :class:`~pcapkit.protocols.link.l2tp.L2TP`, -:class:`~pcapkit.protocols.link.ospf.OSPF`, :class:`~pcapkit.protocols.link.rarp.RARP`/DRARP and etc. +:class:`~pcapkit.protocols.link.vlan.VLAN` and etc. .. autoclass:: pcapkit.protocols.link.link.Link :no-members: diff --git a/docs/source/pcapkit/protocols/link/vlan.rst b/docs/source/pcapkit/protocols/link/vlan.rst index c7b52559de..1c2ededdeb 100644 --- a/docs/source/pcapkit/protocols/link/vlan.rst +++ b/docs/source/pcapkit/protocols/link/vlan.rst @@ -61,8 +61,8 @@ Two distinct EtherTypes also means two distinct project's rule for when protocols get separate modules: siblings that *share* an index may share a module, as :class:`~pcapkit.protocols.link.arp.InARP` shares :mod:`~pcapkit.protocols.link.arp` and -:class:`~pcapkit.protocols.link.rarp.DRARP` shares -:mod:`~pcapkit.protocols.link.rarp`. This base declares no index of its own -- +:class:`~pcapkit.protocols.application.rarp.DRARP` shares +:mod:`~pcapkit.protocols.application.rarp`. This base declares no index of its own -- it is abstract and nothing dispatches to it -- so its ``__index__`` raises. .. autoclass:: pcapkit.protocols.link.vlan.VLAN diff --git a/docs/source/pcapkit/vendor/arp.rst b/docs/source/pcapkit/vendor/arp.rst index 552e60b72d..a6c96e874d 100644 --- a/docs/source/pcapkit/vendor/arp.rst +++ b/docs/source/pcapkit/vendor/arp.rst @@ -5,7 +5,7 @@ .. module:: pcapkit.vendor.arp This module contains all vendor crawlers of :class:`~pcapkit.protocols.link.arp.ARP` -and :class:`~pcapkit.protocols.link.rarp.RARP` implementations. Available +and :class:`~pcapkit.protocols.application.rarp.RARP` implementations. Available vendor crawlers include: .. list-table:: diff --git a/docs/source/pcapkit/vendor/ospf.rst b/docs/source/pcapkit/vendor/ospf.rst index 7357a1e650..d3086b10e2 100644 --- a/docs/source/pcapkit/vendor/ospf.rst +++ b/docs/source/pcapkit/vendor/ospf.rst @@ -1,11 +1,11 @@ -================================================================ -:class:`~pcapkit.protocols.link.ospf.OSPF` Vendor Crawlers -================================================================ +================================================================= +:class:`~pcapkit.protocols.application.ospf.OSPF` Vendor Crawlers +================================================================= .. module:: pcapkit.vendor.ospf This module contains all vendor crawlers of -:class:`~pcapkit.protocols.link.ospf.OSPF` implementations. Available +:class:`~pcapkit.protocols.application.ospf.OSPF` implementations. Available vendor crawlers include: .. list-table:: diff --git a/examples/generators/dispatch.py b/examples/generators/dispatch.py index 37d6fc0192..584222d0a4 100644 --- a/examples/generators/dispatch.py +++ b/examples/generators/dispatch.py @@ -11,7 +11,7 @@ a packet if it tried. :file:`tests/protocols/test_dispatch_bindings_unit.py` exists because exactly that shipped once: its own docstring records that -:class:`~pcapkit.protocols.link.ospf.OSPF` "was reachable from no table at all +:class:`~pcapkit.protocols.application.ospf.OSPF` "was reachable from no table at all and could not have parsed a packet if it had been". But that module hand-picks its eleven cases rather than enumerating, so it guards the entries someone remembered rather than the registries themselves. See GitHub issue #496. @@ -31,7 +31,7 @@ :class:`~pcapkit.corekit.protochain.ProtoChain` actually contains the class :data:`PINNED_TARGETS` says the code is *supposed* to reach -- not merely that the alias string looks right, since several of these classes rename themselves -on the wire (:class:`~pcapkit.protocols.link.arp.RARP` reports ``'ARP'`` for +on the wire (:class:`~pcapkit.protocols.application.rarp.RARP` reports ``'ARP'`` for ``oper in (1, 2)``; :class:`~pcapkit.protocols.internet.hip.HIP` reports ``'HIPv2'``) and one table entry's target *is* itself :class:`~pcapkit.protocols.misc.raw.Raw` rather than a defect. @@ -282,7 +282,7 @@ def _link_payload(code: 'int') -> 'bytes': from pcapkit.protocols.link.arp import ARP return bytes(ARP(oper=1)) if code == EtherType.Reverse_Address_Resolution_Protocol: - from pcapkit.protocols.link.rarp import RARP + from pcapkit.protocols.application.rarp import RARP # ARP/RARP/InARP/DRARP all report their alias from the wire ``oper`` # field rather than from the dispatching EtherType (link/arp.py:176-190 @@ -390,7 +390,7 @@ def _internet_payload(code: 'int') -> 'bytes': next=6, packet=1, version=2, checksum=b'\x00\x00', controls_anonymous=False, shit=0, rhit=0, payload=_tcp(9999))) if code == TransType.OSPFIGP: - from pcapkit.protocols.link.ospf import OSPF + from pcapkit.protocols.application.ospf import OSPF return bytes(OSPF()) if code == TransType.Shim6: # #904: no dedicated dissector exists for Shim6 -- IPv6_Ext @@ -604,7 +604,7 @@ def _internet_enum() -> 'Any': PINNED_TARGETS = { # -- Link.__proto__ (EtherType) ------------------------------------------- 'link/Address_Resolution_Protocol': ('pcapkit.protocols.link.arp', 'ARP'), - 'link/Reverse_Address_Resolution_Protocol': ('pcapkit.protocols.link.rarp', 'RARP'), + 'link/Reverse_Address_Resolution_Protocol': ('pcapkit.protocols.application.rarp', 'RARP'), 'link/Customer_VLAN_Tag_Type': ('pcapkit.protocols.link.c_tag', 'C_Tag'), 'link/IEEE_Std_802_1Q_Service_VLAN_tag_identifier': ('pcapkit.protocols.link.s_tag', 'S_Tag'), 'link/Internet_Protocol_version_4': ('pcapkit.protocols.internet.ipv4', 'IPv4'), @@ -627,7 +627,7 @@ def _internet_enum() -> 'Any': 'internet/Mobility_Header': ('pcapkit.protocols.internet.mh', 'MH'), 'internet/HIP': ('pcapkit.protocols.internet.hip', 'HIP'), 'internet/SCTP': ('pcapkit.protocols.transport.sctp', 'SCTP'), - 'internet/OSPFIGP': ('pcapkit.protocols.link.ospf', 'OSPF'), + 'internet/OSPFIGP': ('pcapkit.protocols.application.ospf', 'OSPF'), # #904: Shim6 (140) previously had no entry at all, and the default # factory made it resolve to Raw. It is now registered directly at # IPv6_Ext, which parses the RFC 6564 §4 generic layout it has diff --git a/pcapkit/__init__.py b/pcapkit/__init__.py index 01d3a70145..ff43e796bb 100644 --- a/pcapkit/__init__.py +++ b/pcapkit/__init__.py @@ -112,8 +112,8 @@ 'NoPayload', # No Payload 'Raw', # Raw Packet - 'ARP', 'C_Tag', 'DRARP', 'Ethernet', 'InARP', 'L2TP', # Link Layer - 'L2TPv2', 'OSPF', 'RARP', 'S_Tag', 'VLAN', + 'ARP', 'C_Tag', 'Ethernet', 'InARP', 'L2TP', # Link Layer + 'L2TPv2', 'S_Tag', 'VLAN', 'AH', 'ESP', 'IP', 'IPsec', 'IPv4', 'IPv6', 'IPX', # Internet Layer 'HIP', 'HOPOPT', 'IPv6_Frag', 'IPv6_Ext', 'IPv6_Opts', 'IPv6_Route', 'MH', @@ -122,7 +122,8 @@ 'TCP', 'UDP', 'SCTP', # Transport Layer 'FTP', 'FTP_DATA', # Application Layer - 'HTTP', 'HTTPv1', 'HTTPv2', 'NGAP', + 'HTTP', 'HTTPv1', 'HTTPv2', 'NGAP', 'OSPF', + 'RARP', 'DRARP', 'Data', # Protocol Data 'Schema', # Protocol Schema diff --git a/pcapkit/const/arp/__init__.py b/pcapkit/const/arp/__init__.py index b8424bc202..e21fabe30e 100644 --- a/pcapkit/const/arp/__init__.py +++ b/pcapkit/const/arp/__init__.py @@ -4,7 +4,7 @@ ===================================================================== This module contains all constant enumerations of :class:`~pcapkit.protocols.link.arp.ARP` -and :class:`~pcapkit.protocols.link.rarp.RARP` implementations. Available +and :class:`~pcapkit.protocols.application.rarp.RARP` implementations. Available enumerations include: .. list-table:: diff --git a/pcapkit/const/ospf/__init__.py b/pcapkit/const/ospf/__init__.py index b40d042361..e5af97b8a3 100644 --- a/pcapkit/const/ospf/__init__.py +++ b/pcapkit/const/ospf/__init__.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- # pylint: disable=unused-import -""":class:`~pcapkit.protocols.link.ospf.OSPF` Constant Enumerations +""":class:`~pcapkit.protocols.application.ospf.OSPF` Constant Enumerations ====================================================================== .. module:: pcapkit.const.ospf This module contains all constant enumerations of -:class:`~pcapkit.protocols.link.ospf.OSPF` implementations. Available +:class:`~pcapkit.protocols.application.ospf.OSPF` implementations. Available enumerations include: .. list-table:: diff --git a/pcapkit/protocols/__init__.py b/pcapkit/protocols/__init__.py index 0edb972133..1798841cc9 100644 --- a/pcapkit/protocols/__init__.py +++ b/pcapkit/protocols/__init__.py @@ -50,8 +50,7 @@ 'Raw', # Link Layer - 'ARP', 'C_Tag', 'DRARP', 'Ethernet', 'InARP', 'L2TP', 'L2TPv2', - 'OSPF', 'RARP', 'S_Tag', 'VLAN', + 'ARP', 'C_Tag', 'Ethernet', 'InARP', 'L2TP', 'L2TPv2', 'S_Tag', 'VLAN', # Internet Layer 'AH', 'ESP', 'IP', 'IPsec', 'IPv4', 'IPv6', 'IPX', @@ -67,6 +66,8 @@ 'FTP', 'FTP_DATA', 'HTTP', 'HTTPv1', 'HTTPv2', 'NGAP', + 'OSPF', + 'RARP', 'DRARP', ] #: dict[str, ~typing.Type[Protocol]]: Protocol registry. diff --git a/pcapkit/protocols/application/__init__.py b/pcapkit/protocols/application/__init__.py index 064ef28f95..deeff803d5 100644 --- a/pcapkit/protocols/application/__init__.py +++ b/pcapkit/protocols/application/__init__.py @@ -25,6 +25,8 @@ from pcapkit.protocols.application.httpv1 import HTTP as HTTPv1 from pcapkit.protocols.application.httpv2 import HTTP as HTTPv2 from pcapkit.protocols.application.ngap import NGAP +from pcapkit.protocols.application.ospf import OSPF +from pcapkit.protocols.application.rarp import RARP, DRARP # Deprecated / Base Classes from pcapkit.protocols.application.http import HTTP @@ -37,4 +39,6 @@ 'FTP', 'FTP_DATA', 'HTTP', 'HTTPv1', 'HTTPv2', 'NGAP', + 'OSPF', + 'RARP', 'DRARP', ] diff --git a/pcapkit/protocols/link/ospf.py b/pcapkit/protocols/application/ospf.py similarity index 91% rename from pcapkit/protocols/link/ospf.py rename to pcapkit/protocols/application/ospf.py index 18f5531bb6..abea78aaa7 100644 --- a/pcapkit/protocols/link/ospf.py +++ b/pcapkit/protocols/application/ospf.py @@ -2,10 +2,10 @@ """OSPF - Open Shortest Path First ===================================== -.. module:: pcapkit.protocols.link.ospf +.. module:: pcapkit.protocols.application.ospf -:mod:`pcapkit.protocols.link.ospf` contains -:class:`~pcapkit.protocols.link.ospf.OSPF` only, +:mod:`pcapkit.protocols.application.ospf` contains +:class:`~pcapkit.protocols.application.ospf.OSPF` only, which implements extractor for Open Shortest Path First (OSPF) [*]_, whose structure is described as below: @@ -43,12 +43,13 @@ from pcapkit.const.ospf.packet import Packet as Enum_Packet from pcapkit.const.reg.transtype import TransType as Enum_TransType from pcapkit.corekit.fields.ipaddress import parse_ip_address -from pcapkit.protocols.data.link.ospf import OSPF as Data_OSPF -from pcapkit.protocols.data.link.ospf import \ +from pcapkit.protocols.application.application import Application +from pcapkit.protocols.data.application.ospf import OSPF as Data_OSPF +from pcapkit.protocols.data.application.ospf import \ CrytographicAuthentication as Data_CrytographicAuthentication -from pcapkit.protocols.link.link import Link -from pcapkit.protocols.schema.link.ospf import OSPF as Schema_OSPF -from pcapkit.protocols.schema.link.ospf import \ +from pcapkit.protocols.protocol import ProtocolBase +from pcapkit.protocols.schema.application.ospf import OSPF as Schema_OSPF +from pcapkit.protocols.schema.application.ospf import \ CrytographicAuthentication as Schema_CrytographicAuthentication from pcapkit.utilities.exceptions import ProtocolError @@ -60,7 +61,6 @@ from aenum import IntEnum as AenumEnum from typing_extensions import Literal - from pcapkit.protocols.protocol import ProtocolBase from pcapkit.protocols.schema.schema import Schema __all__ = ['OSPF'] @@ -69,24 +69,25 @@ PAT_MAC_ADDR = re.compile(rb'(?i)(?:[0-9a-f]{2}[:-]){5}[0-9a-f]{2}') -class OSPF(Link[Data_OSPF, Schema_OSPF], +class OSPF(Application[Data_OSPF, Schema_OSPF], schema=Schema_OSPF, data=Data_OSPF): """This class implements Open Shortest Path First. - The protocol is dispatched from :attr:`Internet.__proto__ - ` at - :attr:`~pcapkit.const.reg.transtype.TransType.OSPFIGP` (IANA protocol number - 89), since OSPF rides directly on IP rather than on a link-layer frame. + A routing protocol computes the forwarding table rather than forwarding + packets, so it is a *user* of the stack rather than part of its forwarding + path. :rfc:`1812#section-7` places it accordingly, titling that chapter + "APPLICATION LAYER - ROUTING PROTOCOLS" with OSPF at §7.2.2, while + :rfc:`1812#section-4.1` confines the internet layer to IP, ICMP and IGMP. + Hence :class:`~pcapkit.protocols.application.application.Application` as the + base, and ``layer == 'Application'``. See + :doc:`/contributing/conventions/protocol-layer-placement`. Note: - It nonetheless subclasses :class:`~pcapkit.protocols.link.link.Link` and - so reports ``layer == 'Link'``, which is not where a protocol carried - inside IP belongs. That is a pre-existing classification, kept because - moving the module would change its public import path. It is inert for - layer-limited extraction -- IPv4 and IPv6 terminate an ``internet`` - extraction before OSPF is reached, and a ``link`` extraction stops at - Ethernet -- but :attr:`self.layer ` - does read ``'Link'`` on a parsed OSPF packet. + The subpackage does not track the dispatch tier. OSPF is still dispatched + from :attr:`Internet.__proto__ + ` at + :attr:`~pcapkit.const.reg.transtype.TransType.OSPFIGP` (IANA protocol + number 89), since IP is what carries it. """ #: Version number of corresponding protocol, as read off the header. Held on diff --git a/pcapkit/protocols/link/rarp.py b/pcapkit/protocols/application/rarp.py similarity index 63% rename from pcapkit/protocols/link/rarp.py rename to pcapkit/protocols/application/rarp.py index 541302a191..d864967d7f 100644 --- a/pcapkit/protocols/link/rarp.py +++ b/pcapkit/protocols/application/rarp.py @@ -2,10 +2,10 @@ """RARP/DRARP - (Dynamic) Reverse Address Resolution Protocol ================================================================ -.. module:: pcapkit.protocols.link.rarp +.. module:: pcapkit.protocols.application.rarp -:mod:`pcapkit.protocols.link.rarp` contains -:class:`~pcapkit.protocols.link.rarp.RARP` only, +:mod:`pcapkit.protocols.application.rarp` contains +:class:`~pcapkit.protocols.application.rarp.RARP` only, which implements extractor for (Dynamic) Reverse Address Resolution Protocol (RARP/DRARP) [*]_, whose structure is described as below: @@ -30,6 +30,7 @@ from typing import TYPE_CHECKING from pcapkit.const.reg.ethertype import EtherType as Enum_EtherType +from pcapkit.protocols.application.application import Application from pcapkit.protocols.data.link.arp import ARP as Data_ARP from pcapkit.protocols.link.arp import ARP from pcapkit.protocols.schema.link.arp import ARP as Schema_ARP @@ -40,8 +41,31 @@ __all__ = ['RARP', 'DRARP'] -class RARP(ARP, schema=Schema_ARP, data=Data_ARP): # pylint: disable=abstract-method - """This class implements Reverse Address Resolution Protocol.""" +class RARP(Application, ARP, schema=Schema_ARP, data=Data_ARP): # pylint: disable=abstract-method + """This class implements Reverse Address Resolution Protocol. + + :rfc:`1122#section-1.1.3` lists RARP in the application layer, among the + "support protocols, used for host name mapping, booting, and management", + while putting ARP in the Link Layer chapter at :rfc:`1122#section-2.3.2` -- + two sibling protocols sharing one frame format and one EtherType, placed on + function alone. Hence the two bases: the layer base + :class:`~pcapkit.protocols.application.application.Application` first, for + ``layer == 'Application'``, then the protocol family base + :class:`~pcapkit.protocols.link.arp.ARP` for the shared parser. See + :doc:`/contributing/conventions/protocol-layer-placement`. + + Note: + The order is load-bearing, not stylistic. :class:`ARP`'s chain reaches + :class:`~pcapkit.protocols.link.link.Link`, which *owns* ``__layer__``, + so ``class RARP(ARP, Application)`` would report ``'Link'``. + + The subpackage does not track the dispatch tier either: RARP is still + dispatched from :attr:`Link.__proto__ + ` at + :attr:`~pcapkit.const.reg.ethertype.EtherType.Reverse_Address_Resolution_Protocol`, + since an Ethernet frame is what carries it. + + """ ########################################################################## # Methods. @@ -70,7 +94,13 @@ def __index__(cls) -> 'Enum_EtherType': # pylint: disable=invalid-index-returne class DRARP(RARP): - """This class implements Dynamic Reverse Address Resolution Protocol.""" + """This class implements Dynamic Reverse Address Resolution Protocol. + + Inherits RARP's bases unchanged, so ``layer == 'Application'`` here too -- + :rfc:`1931` makes it RARP with dynamic allocation, the same function and so + the same layer. + + """ ########################################################################## # Methods. diff --git a/pcapkit/protocols/data/application/__init__.py b/pcapkit/protocols/data/application/__init__.py index 620823252b..955c5d1926 100644 --- a/pcapkit/protocols/data/application/__init__.py +++ b/pcapkit/protocols/data/application/__init__.py @@ -43,6 +43,11 @@ from pcapkit.protocols.data.application.ngap import Choice as NGAP_Choice from pcapkit.protocols.data.application.ngap import Sequence as NGAP_Sequence +# Open Shortest Path First +from pcapkit.protocols.data.application.ospf import OSPF +from pcapkit.protocols.data.application.ospf import \ + CrytographicAuthentication as OSPF_CrytographicAuthentication + __all__ = [ # File Transfer Protocol 'FTP', @@ -65,4 +70,7 @@ # NG Application Protocol 'NGAP', 'NGAP_IE', 'NGAP_Choice', 'NGAP_BitString', 'NGAP_Sequence', + + # Open Shortest Path First + 'OSPF', 'OSPF_CrytographicAuthentication', ] diff --git a/pcapkit/protocols/data/link/ospf.py b/pcapkit/protocols/data/application/ospf.py similarity index 100% rename from pcapkit/protocols/data/link/ospf.py rename to pcapkit/protocols/data/application/ospf.py diff --git a/pcapkit/protocols/data/link/__init__.py b/pcapkit/protocols/data/link/__init__.py index b4cf1d76f3..f7702e61ea 100644 --- a/pcapkit/protocols/data/link/__init__.py +++ b/pcapkit/protocols/data/link/__init__.py @@ -9,11 +9,6 @@ # Ethernet Protocol from pcapkit.protocols.data.link.ethernet import Ethernet -# Open Shortest Path First -from pcapkit.protocols.data.link.ospf import OSPF -from pcapkit.protocols.data.link.ospf import \ - CrytographicAuthentication as OSPF_CrytographicAuthentication - # 802.1Q Customer VLAN Tag Type from pcapkit.protocols.data.link.vlan import TCI as VLAN_TCI from pcapkit.protocols.data.link.vlan import VLAN @@ -25,9 +20,6 @@ # Ethernet Protocol 'Ethernet', - # Open Shortest Path First - 'OSPF', 'OSPF_CrytographicAuthentication', - # 802.1Q Customer VLAN Tag Type 'VLAN', 'VLAN_TCI', ] diff --git a/pcapkit/protocols/internet/internet.py b/pcapkit/protocols/internet/internet.py index 7240e5bd35..a6ca9bdc03 100644 --- a/pcapkit/protocols/internet/internet.py +++ b/pcapkit/protocols/internet/internet.py @@ -73,7 +73,7 @@ class Internet(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=ab * - :attr:`~pcapkit.const.reg.transtype.TransType.SCTP` - :class:`pcapkit.protocols.transport.sctp.SCTP` * - :attr:`~pcapkit.const.reg.transtype.TransType.OSPFIGP` - - :class:`pcapkit.protocols.link.ospf.OSPF` + - :class:`pcapkit.protocols.application.ospf.OSPF` """ @@ -107,13 +107,13 @@ class Internet(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=ab Enum_TransType.SCTP: ModuleDescriptor('pcapkit.protocols.transport.sctp', 'SCTP'), # OSPF rides directly on IP, so IANA protocol number 89 is its only - # dispatch point. The dissector lives under ``protocols.link`` - # despite that, and so reports ``__layer__ = 'Link'``; c.f. the note - # in :mod:`pcapkit.protocols.link.ospf`. The module is left where it - # is here -- moving it would break its import path -- and the - # mislabel is inert for layer-limited extraction, since IPv4/IPv6 - # terminate an ``internet`` extraction before OSPF is reached. - Enum_TransType.OSPFIGP: ModuleDescriptor('pcapkit.protocols.link.ospf', 'OSPF'), + # dispatch point. The dissector nonetheless lives under + # ``protocols.application`` and reports ``__layer__ = 'Application'``, + # because a routing protocol computes the forwarding table rather + # than forwarding packets -- the dispatch tier and the subpackage are + # deliberately decoupled, c.f. + # :doc:`/contributing/conventions/protocol-layer-placement`. + Enum_TransType.OSPFIGP: ModuleDescriptor('pcapkit.protocols.application.ospf', 'OSPF'), }, ) diff --git a/pcapkit/protocols/link/__init__.py b/pcapkit/protocols/link/__init__.py index 238f665006..64f37cddc1 100644 --- a/pcapkit/protocols/link/__init__.py +++ b/pcapkit/protocols/link/__init__.py @@ -17,8 +17,6 @@ # Utility Classes for Protocols from pcapkit.protocols.link.arp import ARP, InARP from pcapkit.protocols.link.ethernet import Ethernet -from pcapkit.protocols.link.ospf import OSPF -from pcapkit.protocols.link.rarp import RARP, DRARP # VLAN Tag Family from pcapkit.protocols.link.vlan import VLAN @@ -37,6 +35,5 @@ 'LINKTYPE', # Link Layer Protocols - 'ARP', 'C_Tag', 'DRARP', 'Ethernet', 'InARP', 'L2TP', 'L2TPv2', - 'OSPF', 'RARP', 'S_Tag', 'VLAN', + 'ARP', 'C_Tag', 'Ethernet', 'InARP', 'L2TP', 'L2TPv2', 'S_Tag', 'VLAN', ] diff --git a/pcapkit/protocols/link/arp.py b/pcapkit/protocols/link/arp.py index 21994824cf..c7869a4c7b 100644 --- a/pcapkit/protocols/link/arp.py +++ b/pcapkit/protocols/link/arp.py @@ -80,8 +80,8 @@ class ARP(Link[Data_ARP, Schema_ARP], """This class implements all protocols in ARP family. - Address Resolution Protocol (:class:`~pcapkit.protocols.link.arp.ARP`) [:rfc:`826`] - - Reverse Address Resolution Protocol (:class:`~pcapkit.protocols.link.rarp.RARP`) [:rfc:`903`] - - Dynamic Reverse Address Resolution Protocol (:class:`~pcapkit.protocols.link.rarp.DRARP`) [:rfc:`1931`] + - Reverse Address Resolution Protocol (:class:`~pcapkit.protocols.application.rarp.RARP`) [:rfc:`903`] + - Dynamic Reverse Address Resolution Protocol (:class:`~pcapkit.protocols.application.rarp.DRARP`) [:rfc:`1931`] - Inverse Address Resolution Protocol (:class:`~pcapkit.protocols.link.arp.InARP`) [:rfc:`2390`] """ diff --git a/pcapkit/protocols/link/c_tag.py b/pcapkit/protocols/link/c_tag.py index 52df3db8dc..f7c123e52d 100644 --- a/pcapkit/protocols/link/c_tag.py +++ b/pcapkit/protocols/link/c_tag.py @@ -41,7 +41,7 @@ # resolves an omitted schema by looking the *subclass name* up in # :mod:`pcapkit.protocols.schema`, and assigns unconditionally -- so leaving them # off would silently bind ``Schema_Raw``/``Data_Raw`` rather than falling back to -# the base class's pair. c.f. :class:`~pcapkit.protocols.link.rarp.RARP`, which +# the base class's pair. c.f. :class:`~pcapkit.protocols.application.rarp.RARP`, which # restates them for the same reason. class C_Tag(VLAN, schema=Schema_VLAN, data=Data_VLAN): """This class implements 802.1Q Customer VLAN Tag Type.""" diff --git a/pcapkit/protocols/link/l2tpv2.py b/pcapkit/protocols/link/l2tpv2.py index e4dd608ce3..b667aa65f7 100644 --- a/pcapkit/protocols/link/l2tpv2.py +++ b/pcapkit/protocols/link/l2tpv2.py @@ -103,12 +103,10 @@ class L2TPv2(L2TP[Data_L2TP, Schema_L2TP], that the datagram is not its own. See :class:`~pcapkit.protocols.link.l2tp.L2TP` for the measurement. - As with :class:`~pcapkit.protocols.link.ospf.OSPF`, the class subclasses - :class:`~pcapkit.protocols.link.link.Link` and so reports - ``layer == 'Link'`` even though it is carried inside UDP. That is a - pre-existing classification, kept because moving the module would change - its public import path. - + The class subclasses :class:`~pcapkit.protocols.link.link.Link` and so + reports ``layer == 'Link'`` even though it is carried inside UDP: a + tunnelling protocol is placed by what it carries. See + :doc:`/contributing/conventions/protocol-layer-placement`. """ ########################################################################## diff --git a/pcapkit/protocols/link/link.py b/pcapkit/protocols/link/link.py index af9544ef7c..f4377a1a44 100644 --- a/pcapkit/protocols/link/link.py +++ b/pcapkit/protocols/link/link.py @@ -11,8 +11,7 @@ :class:`~pcapkit.protocols.link.arp.ARP`/:class:`~pcapkit.protocols.link.arp.InARP`, :class:`~pcapkit.protocols.link.ethernet.Ethernet`, :class:`~pcapkit.protocols.link.l2tp.L2TP`, -:class:`~pcapkit.protocols.link.ospf.OSPF`, -:class:`~pcapkit.protocols.link.rarp.RARP`/:class:`~pcapkit.protocols.link.rarp.DRARP` +:class:`~pcapkit.protocols.link.vlan.VLAN` and etc. """ @@ -48,7 +47,7 @@ class Link(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=abstra * - :attr:`~pcapkit.const.reg.ethertype.EtherType.Address_Resolution_Protocol` - :class:`pcapkit.protocols.link.arp.ARP` * - :attr:`~pcapkit.const.reg.ethertype.EtherType.Reverse_Address_Resolution_Protocol` - - :class:`pcapkit.protocols.link.rarp.RARP` + - :class:`pcapkit.protocols.application.rarp.RARP` * - :attr:`~pcapkit.const.reg.ethertype.EtherType.Customer_VLAN_Tag_Type` - :class:`pcapkit.protocols.link.c_tag.C_Tag` * - :attr:`~pcapkit.const.reg.ethertype.EtherType.IEEE_Std_802_1Q_Service_VLAN_tag_identifier` @@ -76,7 +75,8 @@ class Link(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=abstra lambda: ModuleDescriptor('pcapkit.protocols.misc.raw', 'Raw'), { Enum_EtherType.Address_Resolution_Protocol: ModuleDescriptor('pcapkit.protocols.link.arp', 'ARP'), - Enum_EtherType.Reverse_Address_Resolution_Protocol: ModuleDescriptor('pcapkit.protocols.link.rarp', 'RARP'), + Enum_EtherType.Reverse_Address_Resolution_Protocol: + ModuleDescriptor('pcapkit.protocols.application.rarp', 'RARP'), # The 802.1Q customer tag and the 802.1ad service tag. Q-in-Q stacks # them -- the service tag's own next-EtherType is what selects the # customer tag -- so the two coexist in one frame rather than diff --git a/pcapkit/protocols/link/vlan.py b/pcapkit/protocols/link/vlan.py index a9a5db0e9b..748c042651 100644 --- a/pcapkit/protocols/link/vlan.py +++ b/pcapkit/protocols/link/vlan.py @@ -43,8 +43,8 @@ project's rule for when protocols get separate modules: siblings that *share* an index may share a module, as :class:`~pcapkit.protocols.link.arp.InARP` shares :mod:`~pcapkit.protocols.link.arp` and -:class:`~pcapkit.protocols.link.rarp.DRARP` shares -:mod:`~pcapkit.protocols.link.rarp`. This base declares no index of its own -- +:class:`~pcapkit.protocols.application.rarp.DRARP` shares +:mod:`~pcapkit.protocols.application.rarp`. This base declares no index of its own -- it is abstract and nothing dispatches to it -- so its ``__index__`` raises. .. [*] https://en.wikipedia.org/wiki/IEEE_802.1Q diff --git a/pcapkit/protocols/schema/application/__init__.py b/pcapkit/protocols/schema/application/__init__.py index a1f010f9f2..72a397d466 100644 --- a/pcapkit/protocols/schema/application/__init__.py +++ b/pcapkit/protocols/schema/application/__init__.py @@ -27,6 +27,11 @@ # NG Application Protocol from pcapkit.protocols.schema.application.ngap import NGAP +# Open Shortest Path First +from pcapkit.protocols.schema.application.ospf import OSPF +from pcapkit.protocols.schema.application.ospf import \ + CrytographicAuthentication as OSPF_CrytographicAuthentication + __all__ = [ # File Transfer Protocol 'FTP', @@ -43,4 +48,7 @@ # NG Application Protocol 'NGAP', + + # Open Shortest Path First + 'OSPF', 'OSPF_CrytographicAuthentication', ] diff --git a/pcapkit/protocols/schema/link/ospf.py b/pcapkit/protocols/schema/application/ospf.py similarity index 96% rename from pcapkit/protocols/schema/link/ospf.py rename to pcapkit/protocols/schema/application/ospf.py index c2f6cab9fb..1ba65d15c7 100644 --- a/pcapkit/protocols/schema/link/ospf.py +++ b/pcapkit/protocols/schema/application/ospf.py @@ -30,7 +30,7 @@ def ospf_auth_data_selector(pkt: 'dict[str, Any]') -> 'Field': Returns: * If :attr:`OSPF.auth_type` is 2, a :class:`~pcapkit.corekit.fields.misc.SchemaField` - wrapped :class:`~pcapkit.protocols.schema.link.ospf.CrytographicAuthentication` instance. + wrapped :class:`~pcapkit.protocols.schema.application.ospf.CrytographicAuthentication` instance. * Otherwise, a :class:`~pcapkit.corekit.fields.strings.BytesField` instance. """ diff --git a/pcapkit/protocols/schema/link/__init__.py b/pcapkit/protocols/schema/link/__init__.py index dce6b4c4dd..6f38d5aeae 100644 --- a/pcapkit/protocols/schema/link/__init__.py +++ b/pcapkit/protocols/schema/link/__init__.py @@ -4,9 +4,6 @@ from pcapkit.protocols.schema.link.arp import ARP from pcapkit.protocols.schema.link.ethernet import Ethernet from pcapkit.protocols.schema.link.l2tp import L2TP -from pcapkit.protocols.schema.link.ospf import OSPF -from pcapkit.protocols.schema.link.ospf import \ - CrytographicAuthentication as OSPF_CrytographicAuthentication from pcapkit.protocols.schema.link.vlan import TCI as VLAN_TCI from pcapkit.protocols.schema.link.vlan import VLAN @@ -14,6 +11,5 @@ 'ARP', 'Ethernet', 'L2TP', - 'OSPF', 'OSPF_CrytographicAuthentication', 'VLAN', 'VLAN_TCI', ] diff --git a/pcapkit/vendor/arp/__init__.py b/pcapkit/vendor/arp/__init__.py index 3bc90fa7d9..8056a83c1d 100644 --- a/pcapkit/vendor/arp/__init__.py +++ b/pcapkit/vendor/arp/__init__.py @@ -6,7 +6,7 @@ .. module:: pcapkit.vendor.arp This module contains all vendor crawlers of :class:`~pcapkit.protocols.link.arp.ARP` -and :class:`~pcapkit.protocols.link.rarp.RARP` implementations. Available +and :class:`~pcapkit.protocols.application.rarp.RARP` implementations. Available vendor crawlers include: .. list-table:: diff --git a/pcapkit/vendor/ospf/__init__.py b/pcapkit/vendor/ospf/__init__.py index 1ae24ecc5e..4b91c01bde 100644 --- a/pcapkit/vendor/ospf/__init__.py +++ b/pcapkit/vendor/ospf/__init__.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- # pylint: disable=unused-import -""":class:`~pcapkit.protocols.link.ospf.OSPF` Vendor Crawlers +""":class:`~pcapkit.protocols.application.ospf.OSPF` Vendor Crawlers ================================================================ .. module:: pcapkit.vendor.ospf This module contains all vendor crawlers of -:class:`~pcapkit.protocols.link.ospf.OSPF` implementations. Available +:class:`~pcapkit.protocols.application.ospf.OSPF` implementations. Available enumerations include: .. list-table:: diff --git a/tests/project/test_conventions_doc_claims.py b/tests/project/test_conventions_doc_claims.py index 0856379e93..629d8d25b6 100644 --- a/tests/project/test_conventions_doc_claims.py +++ b/tests/project/test_conventions_doc_claims.py @@ -105,13 +105,17 @@ #: page. ``documentation`` came after it, carrying the rulings GitHub issue #719 #: settled about the prose itself -- heading case, when a Mermaid graph beats a #: paragraph, and what a sentence on these pages may claim -- which had until then -#: lived only in that thread. Every anchor is the bare file stem, which is what -#: :data:`PAGES` below depends on. +#: lived only in that thread. ``protocol-layer-placement`` carries the other ruling +#: that thread settled -- which subpackage a dissector belongs in, decided by designed +#: function rather than by encapsulation -- and sits beside the other class-hierarchy +#: page. Every anchor is the bare file stem, which is what :data:`PAGES` below depends +#: on. ANCHORS = ( 'mint-criterion', 'sentinel-convention', 'registry-protocol', 'extension-header-subclassing', + 'protocol-layer-placement', 'process', 'documentation', ) diff --git a/tests/protocols/application/test_layer_placement_unit.py b/tests/protocols/application/test_layer_placement_unit.py new file mode 100644 index 0000000000..151f490b6b --- /dev/null +++ b/tests/protocols/application/test_layer_placement_unit.py @@ -0,0 +1,352 @@ +# -*- coding: utf-8 -*- +"""``OSPF`` and ``RARP`` are application-layer, by designed function. + +Settled on GitHub issue #719: layer is decided by what a protocol is *for*, not by +what encapsulates it, with the IETF as the single source of truth. +:rfc:`1812#section-7` is titled "APPLICATION LAYER - ROUTING PROTOCOLS" with OSPF at +§7.2.2 and confines the internet layer to IP, ICMP and IGMP at §4.1; +:rfc:`1122#section-1.1.3` lists RARP in the application layer while ARP sits in the +Link Layer chapter at §2.3.2. The rule and its citations are written down at +:doc:`/contributing/conventions/protocol-layer-placement`. + +Three separate things are pinned here, because each broke independently while the +move was being made: + +#. the ``__layer__`` values and the base classes, including the **order** of RARP's + two bases -- ``ARP``'s chain reaches ``Link``, which owns ``__layer__``, so + ``class RARP(ARP, Application)`` would still report ``'Link'``; +#. that the dispatch keys did **not** move with the modules, only the + ``ModuleDescriptor`` paths they point at; +#. that parsing still works, with **no per-class override**. ``Application`` accepts + the ``-1`` sentinel (the undissected remainder), so ``OSPF`` and ``RARP`` inherit + ``__post_init__``, ``_decode_next_layer`` and ``_import_next_layer`` unchanged. + +Every case builds its own octets in memory and reads no capture under +``examples/captures/``, which are generated rather than committed. + +""" +from __future__ import annotations + +import importlib +import importlib.util +import os +import struct +import tempfile +import unittest + +from tests._support import purge_modules + +RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') +HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) + + +def ospf_hello() -> bytes: + """An OSPFv2 header (24 octets) plus four octets of undissected body.""" + return bytes.fromhex('0201' '001c' '0a000001' '0a000000' '0000' '0000' + '0000000000000000' 'ffffff00') + + +def rarp_request() -> bytes: + """A RARP request, 28 octets, operation 3.""" + return bytes.fromhex('0001' '0800' '06' '04' '0003' '001122334455' '00000000' + '66778899aabb' '0a000001') + + +def ipv4(proto: 'int', payload: bytes) -> bytes: + """A minimal IPv4 header carrying ``payload`` under protocol ``proto``.""" + total = 20 + len(payload) + return struct.pack('!BBHHHBBH4s4s', 0x45, 0, total, 1, 0, 64, proto, 0, + bytes((10, 0, 0, 1)), bytes((224, 0, 0, 5))) + payload + + +def make_pcap(linktype: 'int', *frames: bytes) -> str: + """Write ``frames`` to a little-endian PCAP file with the given link type.""" + path = os.path.join(tempfile.mkdtemp(prefix='pcapkit-placement-'), 'placement.pcap') + with open(path, 'wb') as file: + file.write(struct.pack(' None: + purge_modules(['pcapkit']) + + def tearDown(self) -> None: + purge_modules(['pcapkit']) + + ########################################################################## + # Layer values and base classes. + ########################################################################## + + def test_ospf_and_rarp_report_the_application_layer(self) -> None: + from pcapkit.protocols.application.ospf import OSPF + from pcapkit.protocols.application.rarp import DRARP, RARP + + for cls in (OSPF, RARP, DRARP): + with self.subTest(cls=cls.__name__): + self.assertEqual(cls.__layer__, 'Application') + + # read off a parsed packet, not just off the class + self.assertEqual(OSPF(ospf_hello()).layer, 'Application') + self.assertEqual(RARP(rarp_request()).layer, 'Application') + + def test_arp_and_inarp_stay_on_the_link_layer(self) -> None: + """:rfc:`1122#section-2.3.2` keeps ARP where it is; only RARP moved.""" + from pcapkit.protocols.link.arp import ARP, InARP + + self.assertEqual(ARP.__layer__, 'Link') + self.assertEqual(InARP.__layer__, 'Link') + + def test_ospf_names_application_as_its_only_base(self) -> None: + from pcapkit.protocols.application.application import Application + from pcapkit.protocols.application.ospf import OSPF + from pcapkit.protocols.link.link import Link + + self.assertEqual(OSPF.__bases__, (Application,)) + self.assertFalse(issubclass(OSPF, Link)) + + def test_rarp_subclasses_both_application_and_arp_in_that_order(self) -> None: + """The order is load-bearing: ``Link`` owns ``__layer__``, ``Raw`` does not.""" + from pcapkit.protocols.application.application import Application + from pcapkit.protocols.application.rarp import DRARP, RARP + from pcapkit.protocols.link.arp import ARP + from pcapkit.protocols.link.link import Link + + self.assertTrue(issubclass(RARP, Application)) + self.assertTrue(issubclass(RARP, ARP)) + self.assertEqual(RARP.__bases__, (Application, ARP)) + + mro = RARP.__mro__ + self.assertLess(mro.index(Application), mro.index(ARP), + 'Application must precede ARP, or Link wins __layer__') + self.assertLess(mro.index(ARP), mro.index(Link)) + + # DRARP inherits the pair unchanged + self.assertEqual(DRARP.__bases__, (RARP,)) + + def test_link_owns_layer_but_raw_does_not(self) -> None: + """The mechanism the ordering convention rests on, asserted directly.""" + from pcapkit.protocols.link.link import Link + from pcapkit.protocols.misc.raw import Raw + + self.assertIn('__layer__', vars(Link)) + self.assertNotIn('__layer__', vars(Raw)) + + ########################################################################## + # Re-exports. + ########################################################################## + + def test_the_public_re_exports_are_unaffected_by_the_move(self) -> None: + import pcapkit + import pcapkit.protocols + from pcapkit.protocols.application.ospf import OSPF + from pcapkit.protocols.application.rarp import DRARP, RARP + + for name, cls in (('OSPF', OSPF), ('RARP', RARP), ('DRARP', DRARP)): + with self.subTest(name=name): + self.assertIs(getattr(pcapkit, name), cls) + self.assertIs(getattr(pcapkit.protocols, name), cls) + self.assertIn(name, pcapkit.__all__) + self.assertIn(name, pcapkit.protocols.__all__) + + def test_the_names_left_the_link_subpackage(self) -> None: + """A pure move: no deprecated re-export at the old path, by ruling.""" + import pcapkit.protocols.application + import pcapkit.protocols.link + + for name in ('OSPF', 'RARP', 'DRARP'): + with self.subTest(name=name): + self.assertNotIn(name, pcapkit.protocols.link.__all__) + self.assertIn(name, pcapkit.protocols.application.__all__) + + for path in ('pcapkit.protocols.link.ospf', 'pcapkit.protocols.link.rarp', + 'pcapkit.protocols.data.link.ospf', + 'pcapkit.protocols.schema.link.ospf'): + with self.subTest(path=path): + self.assertIsNone(importlib.util.find_spec(path)) + + ########################################################################## + # Dispatch. + ########################################################################## + + def test_dispatch_keys_did_not_move_with_the_modules(self) -> None: + """The dispatch tier is decoupled from the subpackage, deliberately.""" + from pcapkit.const.reg.ethertype import EtherType + from pcapkit.const.reg.transtype import TransType + from pcapkit.corekit.module import ModuleDescriptor + from pcapkit.protocols.application.ospf import OSPF + from pcapkit.protocols.application.rarp import RARP + from pcapkit.protocols.internet.internet import Internet + from pcapkit.protocols.link.link import Link + + cases = ( + ('OSPF', Internet.__proto__[TransType.OSPFIGP], + 'pcapkit.protocols.application.ospf', OSPF), + ('RARP', Link.__proto__[EtherType.Reverse_Address_Resolution_Protocol], + 'pcapkit.protocols.application.rarp', RARP), + ) + for name, entry, module, cls in cases: + with self.subTest(name=name): + self.assertIsInstance(entry, ModuleDescriptor) + self.assertEqual(entry.module, module) + self.assertEqual(entry.name, name) + # the descriptor must actually resolve, not merely read right + self.assertIs(getattr(importlib.import_module(entry.module), entry.name), cls) + + def test_ospf_leaves_links_registry_behind_and_rarp_keeps_it(self) -> None: + """``Link`` sets seven names beyond ``ProtocolBase``; OSPF loses three of them. + + ``__proto__``, ``register`` and ``_read_protos`` are ``Link``'s EtherType + machinery. The loss is inert because a ``-1`` lookup misses in either + registry and falls back to :class:`~pcapkit.protocols.misc.raw.Raw`. + """ + from pcapkit.protocols.application.ospf import OSPF + from pcapkit.protocols.application.rarp import RARP + from pcapkit.protocols.link.link import Link + from pcapkit.protocols.misc.raw import Raw + from pcapkit.protocols.protocol import ProtocolBase + + self.assertEqual( + sorted(name for name in vars(Link) + if name in ('__data__', '__layer__', '__proto__', '__schema__', + '_read_protos', 'layer', 'register')), + ['__data__', '__layer__', '__proto__', '__schema__', '_read_protos', 'layer', 'register']) + + self.assertIsNot(OSPF.__proto__, Link.__proto__) + self.assertIs(OSPF.__proto__, ProtocolBase.__proto__) + self.assertIs(OSPF._read_protos, ProtocolBase._read_protos) + self.assertIs(RARP._read_protos, Link._read_protos) + self.assertIsNot(OSPF.register.__func__, Link.register.__func__) + + self.assertIs(RARP.__proto__, Link.__proto__) + self.assertIs(RARP.register.__func__, Link.register.__func__) + + for registry in (Link.__proto__, ProtocolBase.__proto__): + with self.subTest(registry=registry is Link.__proto__): + self.assertNotIn(-1, registry) + self.assertIs(ProtocolBase._lookup_next_layer(registry, -1), Raw) + self.assertNotIn(-1, registry, 'a miss must not grow the registry') + + ########################################################################## + # Parsing -- no override of the next-layer hooks. + ########################################################################## + + def test_ospf_and_rarp_override_none_of_the_next_layer_hooks(self) -> None: + """Both inherit the hooks from ``Application`` rather than re-pointing them.""" + from pcapkit.protocols.application.application import Application + from pcapkit.protocols.application.ospf import OSPF + from pcapkit.protocols.application.rarp import DRARP, RARP + + hooks = ('__post_init__', '_decode_next_layer', '_import_next_layer') + for cls in (OSPF, RARP, DRARP): + for hook in hooks: + with self.subTest(cls=cls.__name__, hook=hook): + self.assertNotIn(hook, vars(cls)) + self.assertIs(getattr(cls, hook), getattr(Application, hook)) + + def test_ospf_still_attaches_its_undissected_body(self) -> None: + """OSPF dispatches its body on ``-1``, which ``Application`` accepts.""" + from pcapkit.protocols.application.ospf import OSPF + from pcapkit.protocols.misc.raw import Raw + + packet = OSPF(ospf_hello()) + self.assertEqual(str(packet.protochain), 'OSPFv2:Raw') + self.assertIsInstance(packet.payload, Raw) + + def test_rarp_still_attaches_a_padded_frames_trailer(self) -> None: + """Ethernet pads a short RARP frame; ``ARP.read`` dispatches the padding.""" + from pcapkit.protocols.application.rarp import RARP + from pcapkit.protocols.misc.null import NoPayload + from pcapkit.protocols.misc.raw import Raw + + unpadded = RARP(rarp_request()) + self.assertEqual(str(unpadded.protochain), 'RARP') + self.assertIsInstance(unpadded.payload, NoPayload) + + padded = RARP(rarp_request() + bytes(18)) + self.assertEqual(str(padded.protochain), 'RARP:Raw') + self.assertIsInstance(padded.payload, Raw) + + ########################################################################## + # Layer-limited extraction. + ########################################################################## + + def extract_ipv4_ospf(self, **kwargs): + """Extract a LinkType ``IPV4`` capture carrying IPv4/OSPF, no Ethernet.""" + import pcapkit + from pcapkit.const.reg.linktype import LinkType + + extraction = pcapkit.extract(fin=make_pcap(LinkType.IPV4, ipv4(89, ospf_hello())), + nofile=True, store=True, engine='pcapkit', **kwargs) + self.addCleanup(extraction.__del__) + return extraction.frame[0] + + def test_layer_link_extraction_of_a_headerless_ipv4_capture(self) -> None: + """Inert for the protochain, though the termination flag moves. Measured. + + ``_sigterm`` on the OSPF instance was ``True`` under ``layer='link'`` while + OSPF reported ``'Link'``, and is ``False`` now. The extracted result does + not change, because OSPF dispatches its body on the ``-1`` sentinel and + both the ``_sigterm`` branch of ``_import_next_layer`` and + ``ProtocolBase.__proto__``'s default resolve to + :class:`~pcapkit.protocols.misc.raw.Raw`. A ``link`` extraction of a + capture with no Ethernet header never reaches a link protocol at all. + """ + from pcapkit.protocols.application.ospf import OSPF + + frame = self.extract_ipv4_ospf(layer='link') + self.assertEqual(str(frame.protochain), 'IPv4:OSPFv2:Raw') + + ospf = frame.payload.payload + self.assertIsInstance(ospf, OSPF) + self.assertEqual(ospf.layer, 'Application') + self.assertFalse(ospf._sigterm, "'link' must no longer terminate at OSPF") + + def test_layer_application_extraction_now_terminates_at_ospf(self) -> None: + """The flip side: ``layer='application'`` now matches where it did not.""" + frame = self.extract_ipv4_ospf(layer='application') + self.assertEqual(str(frame.protochain), 'IPv4:OSPFv2:Raw') + + ospf = frame.payload.payload + self.assertTrue(ospf._sigterm, "'application' must terminate at OSPF") + + def test_layer_internet_extraction_stops_at_ipv4_as_before(self) -> None: + """OSPF is not reached under ``internet`` either way: IPv4 terminates first.""" + frame = self.extract_ipv4_ospf(layer='internet') + self.assertEqual(str(frame.protochain), 'IPv4:OSPFIGP') + + def test_rarp_over_ethernet_is_unchanged_under_every_layer_limit(self) -> None: + """Ethernet-framed RARP: the chain is the same for each ``layer=`` value.""" + import pcapkit + from pcapkit.const.reg.linktype import LinkType + + frame_bytes = bytes.fromhex('ffffffffffff' '001122334455' '8035') + rarp_request() + bytes(18) + expected = { + None: 'Ethernet:RARP:Raw', + 'link': 'Ethernet:Reverse_Address_Resolution_Protocol', + 'internet': 'Ethernet:RARP:Raw', + 'application': 'Ethernet:RARP:Raw', + } + for layer, chain in expected.items(): + with self.subTest(layer=layer): + kwargs = {} if layer is None else {'layer': layer} + extraction = pcapkit.extract( + fin=make_pcap(LinkType.ETHERNET, frame_bytes), + nofile=True, store=True, engine='pcapkit', **kwargs) + self.addCleanup(extraction.__del__) + self.assertEqual(str(extraction.frame[0].protochain), chain) + + def test_unlimited_extraction_is_unchanged(self) -> None: + frame = self.extract_ipv4_ospf() + self.assertEqual(str(frame.protochain), 'IPv4:OSPFv2:Raw') + self.assertFalse(frame.payload.payload._sigterm) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/application/test_ospf_unit.py b/tests/protocols/application/test_ospf_unit.py new file mode 100644 index 0000000000..df0d461dfe --- /dev/null +++ b/tests/protocols/application/test_ospf_unit.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +"""Unit tests for :mod:`pcapkit.protocols.application.ospf`. + +Relocated from ``tests/protocols/link/test_link_unit.py`` with the module +itself, under :issue:`719`. +""" +from __future__ import annotations + +import importlib.util +from ipaddress import ip_address +import types +import unittest +from unittest import mock + +from tests._support import purge_modules + +RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') +HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) + + +class DummyData(dict): + __getattr__ = dict.__getitem__ + + +@unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') +class OSPFUnitTests(unittest.TestCase): + def setUp(self) -> None: + purge_modules(['pcapkit']) + + def tearDown(self) -> None: + purge_modules(['pcapkit']) + + def test_ospf_index_is_its_transtype_and_make_data_preserves_header(self) -> None: + from pcapkit.const.ospf.packet import Packet + from pcapkit.const.reg.transtype import TransType + from pcapkit.protocols.application.ospf import OSPF + + data = DummyData( + version=2, + type=Packet.Hello, + router_id='192.0.2.1', + area_id='0.0.0.0', + chksum=b'\x12\x34', + autype=0, + auth=b'\x00' * 8, + __next_type__=None, + ) + proto = object.__new__(OSPF) + + self.assertEqual(OSPF.__index__(), TransType.OSPFIGP) + self.assertEqual(OSPF.__index__(), 89) + self.assertEqual(proto.__length_hint__(), 24) + values = OSPF._make_data(data) + self.assertEqual(values['version'], 2) + self.assertEqual(values['type'], Packet.Hello) + self.assertEqual(values['router_id'], '192.0.2.1') + self.assertEqual(values['area_id'], '0.0.0.0') + self.assertEqual(values['checksum'], b'\x12\x34') + self.assertEqual(values['auth_data'], b'\x00' * 8) + self.assertIn('payload', values) + + def test_ospf_properties_read_make_and_auth_helpers(self) -> None: + from pcapkit.const.ospf.authentication import Authentication + from pcapkit.const.ospf.packet import Packet + from pcapkit.protocols.data.application.ospf import \ + CrytographicAuthentication as DataCryptoAuth + from pcapkit.protocols.application.ospf import OSPF + from pcapkit.protocols.schema.application.ospf import \ + CrytographicAuthentication as SchemaCryptoAuth + from pcapkit.protocols.schema.application.ospf import OSPF as SchemaOSPF + from pcapkit.utilities.exceptions import ProtocolError + + ospf = object.__new__(OSPF) + # ``name``/``alias`` read ``_version``, not ``_info``: both are needed + # while ``read`` is still running, before ``_info`` exists. + ospf._version = 2 + ospf._info = types.SimpleNamespace(version=2, type=Packet.Hello) + + self.assertEqual(ospf.name, 'Open Shortest Path First version 2') + self.assertEqual(ospf.alias, 'OSPFv2') + self.assertEqual(ospf.length, 24) + self.assertEqual(ospf.type, Packet.Hello) + + reader = object.__new__(OSPF) + reader.__header__ = SchemaOSPF( + version=2, + type=Packet.Database_Description, + length=24, + router_id='192.0.2.1', + area_id='0.0.0.0', + checksum=b'\x12\x34', + auth_type=Authentication.No_Authentication, + auth_data=b'\x00' * 8, + payload=b'', + ) + reader._decode_next_layer = mock.Mock(side_effect=lambda data, proto, length: data) + data = reader.read() + self.assertEqual(data.auth, b'\x00' * 8) + self.assertEqual(str(data.router_id), '192.0.2.1') + # No next-protocol field on the wire, so the -1 sentinel is dispatched. + self.assertEqual(reader._decode_next_layer.call_args.args[1], -1) + + crypto_schema = SchemaCryptoAuth(key_id=1, len=16, seq=99) + crypto_reader = object.__new__(OSPF) + crypto_reader.__header__ = SchemaOSPF( + version=2, + type=Packet.Link_State_Request, + length=0, + router_id='192.0.2.2', + area_id='0.0.0.1', + checksum=b'\xab\xcd', + auth_type=Authentication.Cryptographic_authentication, + auth_data=crypto_schema, + payload=b'payload', + ) + crypto_reader.__cached__ = {} + crypto_reader._data = b'\x00' * 32 + crypto_reader._decode_next_layer = mock.Mock(side_effect=lambda data, proto, length: data) + crypto_data = crypto_reader.read() + self.assertEqual(crypto_data.auth.key_id, 1) + self.assertEqual(crypto_data.auth.seq, 99) + + maker = object.__new__(OSPF) + schema = maker.make( + version=3, + type=Packet.Link_State_Update, + router_id='192.0.2.3', + area_id='0.0.0.2', + checksum=b'\x56\x78', + auth_type=Authentication.No_Authentication, + auth_data=b'\x01' * 8, + payload=b'abcd', + ) + self.assertEqual(schema.length, 28) + self.assertEqual(schema.auth_data, b'\x01' * 8) + + crypto_data_model = DataCryptoAuth(key_id=2, len=20, seq=100) + crypto_schema_from_data = maker.make( + auth_type=Authentication.Cryptographic_authentication, + auth_data=crypto_data_model, + ) + self.assertEqual(crypto_schema_from_data.auth_data.key_id, 2) + self.assertIs(maker._make_encrypt_auth(crypto_schema), crypto_schema) + self.assertEqual(maker._make_encrypt_auth(b'\x02' * 8), b'\x02' * 8) + self.assertEqual(maker._read_encrypt_auth(crypto_schema).len, 16) + self.assertEqual(maker._read_id_numbers(b'\xc0\x00\x02\x04'), ip_address('192.0.2.4')) + self.assertEqual(maker._make_id_numbers('192.0.2.5'), b'\xc0\x00\x02\x05') + + with self.assertRaises(ProtocolError): + maker.make(auth_type=Authentication.No_Authentication, auth_data=crypto_data_model) + with self.assertRaises(ProtocolError): + maker._make_encrypt_auth(object()) + + def test_ospf_id_numbers_rejects_a_bool(self) -> None: + """A :obj:`bool` router/area ID must not be silently packed. See #540. + + Latent rather than live: nothing in this module calls ``_make_id_numbers`` + today -- ``OSPF.make`` builds ``router_id``/``area_id`` from its own + arguments rather than through this helper -- so only a unit test (this one, + and the pre-existing one above) reaches it. It is fixed alongside the three + live sites anyway, so that it does not resurface the moment a future caller + reaches it. Measured before the fix: + + .. code-block:: text + + OSPF._make_id_numbers(True) -> 00000001 (i.e. 0.0.0.1) + """ + from pcapkit.protocols.application.ospf import OSPF + from pcapkit.utilities.exceptions import BaseError, FieldValueError + + proto = object.__new__(OSPF) + + with self.assertRaises(FieldValueError) as context: + proto._make_id_numbers(True) # type: ignore[arg-type] + self.assertIsInstance(context.exception, BaseError) + self.assertIn('must not be a bool', str(context.exception)) + self.assertIn('int(True)', str(context.exception)) + + # a real ID still converts normally + self.assertEqual(proto._make_id_numbers('192.0.2.6'), b'\xc0\x00\x02\x06') + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/application/test_rarp_unit.py b/tests/protocols/application/test_rarp_unit.py new file mode 100644 index 0000000000..890f5af23b --- /dev/null +++ b/tests/protocols/application/test_rarp_unit.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +"""Unit tests for :mod:`pcapkit.protocols.application.rarp`. + +Relocated from ``tests/protocols/link/test_link_unit.py`` with the module +itself, under :issue:`719`. +""" +from __future__ import annotations + +import importlib.util +import unittest + +from tests._support import purge_modules + +RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') +HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) + + + +@unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') +class RARPUnitTests(unittest.TestCase): + def setUp(self) -> None: + purge_modules(['pcapkit']) + + def tearDown(self) -> None: + purge_modules(['pcapkit']) + + def test_rarp_ids_and_index_are_stable(self) -> None: + from pcapkit.const.reg.ethertype import EtherType + from pcapkit.protocols.application.rarp import DRARP, RARP + + self.assertEqual(RARP.id(), ('RARP', 'DRARP')) + self.assertEqual(DRARP.id(), ('DRARP',)) + self.assertEqual(RARP.__index__(), EtherType.Reverse_Address_Resolution_Protocol) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/link/test_link_unit.py b/tests/protocols/link/test_link_unit.py index 59cac6587c..f353a1dc91 100644 --- a/tests/protocols/link/test_link_unit.py +++ b/tests/protocols/link/test_link_unit.py @@ -141,14 +141,6 @@ def test_arp_id_index_and_length_hint_are_stable(self) -> None: self.assertEqual(ARP.__index__(), EtherType.Address_Resolution_Protocol) self.assertEqual(proto.__length_hint__(), 28) - def test_rarp_ids_and_index_are_stable(self) -> None: - from pcapkit.const.reg.ethertype import EtherType - from pcapkit.protocols.link.rarp import DRARP, RARP - - self.assertEqual(RARP.id(), ('RARP', 'DRARP')) - self.assertEqual(DRARP.id(), ('DRARP',)) - self.assertEqual(RARP.__index__(), EtherType.Reverse_Address_Resolution_Protocol) - def test_vlan_length_hint_is_stable(self) -> None: from pcapkit.protocols.link.c_tag import C_Tag from pcapkit.protocols.link.s_tag import S_Tag @@ -384,37 +376,6 @@ def test_l2tp_index_is_unsupported_and_make_data_preserves_flags(self) -> None: self.assertEqual(values['session_id'], 4) self.assertIn('payload', values) - def test_ospf_index_is_its_transtype_and_make_data_preserves_header(self) -> None: - from pcapkit.const.ospf.packet import Packet - from pcapkit.const.reg.transtype import TransType - from pcapkit.protocols.link.ospf import OSPF - - data = DummyData( - version=2, - type=Packet.Hello, - router_id='192.0.2.1', - area_id='0.0.0.0', - chksum=b'\x12\x34', - autype=0, - auth=b'\x00' * 8, - __next_type__=None, - ) - proto = object.__new__(OSPF) - - # This raised UnsupportedCall while OSPF was reachable from no registry - # at all; it is now dispatched from Internet.__proto__ at 89. - self.assertEqual(OSPF.__index__(), TransType.OSPFIGP) - self.assertEqual(OSPF.__index__(), 89) - self.assertEqual(proto.__length_hint__(), 24) - values = OSPF._make_data(data) - self.assertEqual(values['version'], 2) - self.assertEqual(values['type'], Packet.Hello) - self.assertEqual(values['router_id'], '192.0.2.1') - self.assertEqual(values['area_id'], '0.0.0.0') - self.assertEqual(values['checksum'], b'\x12\x34') - self.assertEqual(values['auth_data'], b'\x00' * 8) - self.assertIn('payload', values) - def test_link_schema_callbacks_resolve_payload_and_auth_fields(self) -> None: from pcapkit.const.ospf.authentication import Authentication from pcapkit.const.reg.ethertype import EtherType @@ -424,7 +385,7 @@ def test_link_schema_callbacks_resolve_payload_and_auth_fields(self) -> None: from pcapkit.protocols.link.ethernet import Ethernet from pcapkit.protocols.misc.raw import Raw from pcapkit.protocols.schema.link.ethernet import callback_payload - from pcapkit.protocols.schema.link.ospf import ( + from pcapkit.protocols.schema.application.ospf import ( CrytographicAuthentication, ospf_auth_data_selector, ) @@ -843,126 +804,5 @@ def test_vlan_dei_is_read_from_the_dei_bit_not_the_pcp(self) -> None: self.assertEqual(data.tci.pcp, pcp) self.assertEqual(data.tci.vid, 42) - def test_ospf_properties_read_make_and_auth_helpers(self) -> None: - from pcapkit.const.ospf.authentication import Authentication - from pcapkit.const.ospf.packet import Packet - from pcapkit.protocols.data.link.ospf import \ - CrytographicAuthentication as DataCryptoAuth - from pcapkit.protocols.link.ospf import OSPF - from pcapkit.protocols.schema.link.ospf import \ - CrytographicAuthentication as SchemaCryptoAuth - from pcapkit.protocols.schema.link.ospf import OSPF as SchemaOSPF - from pcapkit.utilities.exceptions import ProtocolError - - ospf = object.__new__(OSPF) - # ``name``/``alias`` read ``_version``, not ``_info``: both are needed - # while ``read`` is still running, before ``_info`` exists. - ospf._version = 2 - ospf._info = types.SimpleNamespace(version=2, type=Packet.Hello) - - self.assertEqual(ospf.name, 'Open Shortest Path First version 2') - self.assertEqual(ospf.alias, 'OSPFv2') - self.assertEqual(ospf.length, 24) - self.assertEqual(ospf.type, Packet.Hello) - - reader = object.__new__(OSPF) - reader.__header__ = SchemaOSPF( - version=2, - type=Packet.Database_Description, - length=24, - router_id='192.0.2.1', - area_id='0.0.0.0', - checksum=b'\x12\x34', - auth_type=Authentication.No_Authentication, - auth_data=b'\x00' * 8, - payload=b'', - ) - reader._decode_next_layer = mock.Mock(side_effect=lambda data, proto, length: data) - data = reader.read() - self.assertEqual(data.auth, b'\x00' * 8) - self.assertEqual(str(data.router_id), '192.0.2.1') - # No next-protocol field on the wire, so the -1 sentinel is dispatched. - self.assertEqual(reader._decode_next_layer.call_args.args[1], -1) - - crypto_schema = SchemaCryptoAuth(key_id=1, len=16, seq=99) - crypto_reader = object.__new__(OSPF) - crypto_reader.__header__ = SchemaOSPF( - version=2, - type=Packet.Link_State_Request, - length=0, - router_id='192.0.2.2', - area_id='0.0.0.1', - checksum=b'\xab\xcd', - auth_type=Authentication.Cryptographic_authentication, - auth_data=crypto_schema, - payload=b'payload', - ) - crypto_reader.__cached__ = {} - crypto_reader._data = b'\x00' * 32 - crypto_reader._decode_next_layer = mock.Mock(side_effect=lambda data, proto, length: data) - crypto_data = crypto_reader.read() - self.assertEqual(crypto_data.auth.key_id, 1) - self.assertEqual(crypto_data.auth.seq, 99) - - maker = object.__new__(OSPF) - schema = maker.make( - version=3, - type=Packet.Link_State_Update, - router_id='192.0.2.3', - area_id='0.0.0.2', - checksum=b'\x56\x78', - auth_type=Authentication.No_Authentication, - auth_data=b'\x01' * 8, - payload=b'abcd', - ) - self.assertEqual(schema.length, 28) - self.assertEqual(schema.auth_data, b'\x01' * 8) - - crypto_data_model = DataCryptoAuth(key_id=2, len=20, seq=100) - crypto_schema_from_data = maker.make( - auth_type=Authentication.Cryptographic_authentication, - auth_data=crypto_data_model, - ) - self.assertEqual(crypto_schema_from_data.auth_data.key_id, 2) - self.assertIs(maker._make_encrypt_auth(crypto_schema), crypto_schema) - self.assertEqual(maker._make_encrypt_auth(b'\x02' * 8), b'\x02' * 8) - self.assertEqual(maker._read_encrypt_auth(crypto_schema).len, 16) - self.assertEqual(maker._read_id_numbers(b'\xc0\x00\x02\x04'), ip_address('192.0.2.4')) - self.assertEqual(maker._make_id_numbers('192.0.2.5'), b'\xc0\x00\x02\x05') - - with self.assertRaises(ProtocolError): - maker.make(auth_type=Authentication.No_Authentication, auth_data=crypto_data_model) - with self.assertRaises(ProtocolError): - maker._make_encrypt_auth(object()) - - def test_ospf_id_numbers_rejects_a_bool(self) -> None: - """A :obj:`bool` router/area ID must not be silently packed. See #540. - - Latent rather than live: nothing in this module calls ``_make_id_numbers`` - today -- ``OSPF.make`` builds ``router_id``/``area_id`` from its own - arguments rather than through this helper -- so only a unit test (this one, - and the pre-existing one above) reaches it. It is fixed alongside the three - live sites anyway, so that it does not resurface the moment a future caller - reaches it. Measured before the fix: - - .. code-block:: text - - OSPF._make_id_numbers(True) -> 00000001 (i.e. 0.0.0.1) - """ - from pcapkit.protocols.link.ospf import OSPF - from pcapkit.utilities.exceptions import BaseError, FieldValueError - - proto = object.__new__(OSPF) - - with self.assertRaises(FieldValueError) as context: - proto._make_id_numbers(True) # type: ignore[arg-type] - self.assertIsInstance(context.exception, BaseError) - self.assertIn('must not be a bool', str(context.exception)) - self.assertIn('int(True)', str(context.exception)) - - # a real ID still converts normally - self.assertEqual(proto._make_id_numbers('192.0.2.6'), b'\xc0\x00\x02\x06') - - if __name__ == '__main__': unittest.main() diff --git a/tests/protocols/test_dispatch_bindings_unit.py b/tests/protocols/test_dispatch_bindings_unit.py index 836397adf1..044e43af57 100644 --- a/tests/protocols/test_dispatch_bindings_unit.py +++ b/tests/protocols/test_dispatch_bindings_unit.py @@ -6,7 +6,7 @@ What it pins is that a *registered* code actually reaches a working dissector. The tables and the dissectors were previously able to disagree without anything -noticing -- :class:`~pcapkit.protocols.link.ospf.OSPF` was reachable from no +noticing -- :class:`~pcapkit.protocols.application.ospf.OSPF` was reachable from no table at all and could not have parsed a packet if it had been -- so each case asserts on the parsed protocol chain rather than on the table entry alone. diff --git a/tests/protocols/test_dispatch_reachability_unit.py b/tests/protocols/test_dispatch_reachability_unit.py index a996bcab56..b9b4bc9120 100644 --- a/tests/protocols/test_dispatch_reachability_unit.py +++ b/tests/protocols/test_dispatch_reachability_unit.py @@ -11,7 +11,7 @@ :file:`test_dispatch_registry_unit.py` walks the problem from the other end: it takes each of the 38 ``__proto__`` entries that *exist* and checks the class it names can parse a packet. That cannot see a class nobody registered at all, -which is exactly how :class:`~pcapkit.protocols.link.ospf.OSPF` shipped +which is exactly how :class:`~pcapkit.protocols.application.ospf.OSPF` shipped reachable from no table (fixed in #436). This module walks it from the class side instead -- every :class:`~pcapkit.protocols.protocol.ProtocolBase` descendant whose diff --git a/tests/protocols/test_dispatch_registry_unit.py b/tests/protocols/test_dispatch_registry_unit.py index 3c836a7276..45e5f23888 100644 --- a/tests/protocols/test_dispatch_registry_unit.py +++ b/tests/protocols/test_dispatch_registry_unit.py @@ -10,7 +10,7 @@ :file:`test_dispatch_bindings_unit.py` checks that eleven hand-picked codes actually parse a packet into the class the table names -- the property that matters, since a resolvable entry whose target cannot parse a packet is -exactly what shipped once: :class:`~pcapkit.protocols.link.ospf.OSPF` was +exactly what shipped once: :class:`~pcapkit.protocols.application.ospf.OSPF` was reachable from no table at all, per that module's own docstring. This module closes the gap the same way