netprotocols 2.5.0__tar.gz → 2.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. {netprotocols-2.5.0 → netprotocols-2.6.0}/ARCHITECTURE.md +179 -34
  2. {netprotocols-2.5.0 → netprotocols-2.6.0}/CHANGELOG.md +28 -0
  3. {netprotocols-2.5.0 → netprotocols-2.6.0}/CONTRIBUTING.md +5 -2
  4. {netprotocols-2.5.0 → netprotocols-2.6.0}/PKG-INFO +51 -31
  5. {netprotocols-2.5.0 → netprotocols-2.6.0}/README.md +50 -30
  6. {netprotocols-2.5.0 → netprotocols-2.6.0}/SECURITY.md +8 -0
  7. {netprotocols-2.5.0 → netprotocols-2.6.0}/docs/CLAIMS.md +228 -29
  8. {netprotocols-2.5.0 → netprotocols-2.6.0}/pyproject.toml +2 -2
  9. netprotocols-2.6.0/scripts/capture_fixtures_llc.sh +348 -0
  10. netprotocols-2.6.0/scripts/capture_fixtures_lldp.sh +313 -0
  11. netprotocols-2.6.0/scripts/capture_fixtures_mpls.sh +259 -0
  12. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/check_fixtures.py +364 -9
  13. netprotocols-2.6.0/scripts/llc_fixture_frames.py +189 -0
  14. netprotocols-2.6.0/scripts/mpls_fixture_packets.py +253 -0
  15. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/__init__.py +22 -1
  16. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/_base.py +70 -7
  17. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/_defaults.py +40 -1
  18. netprotocols-2.6.0/src/netprotocols/_enums.py +180 -0
  19. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/checksum.py +50 -5
  20. netprotocols-2.6.0/src/netprotocols/layer2/__init__.py +26 -0
  21. netprotocols-2.6.0/src/netprotocols/layer2/cdp.py +642 -0
  22. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer2/ethernet.py +87 -6
  23. netprotocols-2.6.0/src/netprotocols/layer2/llc.py +481 -0
  24. netprotocols-2.6.0/src/netprotocols/layer2/lldp.py +522 -0
  25. netprotocols-2.6.0/src/netprotocols/layer2/mpls.py +392 -0
  26. netprotocols-2.6.0/src/netprotocols/layer2/stp.py +634 -0
  27. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer2/vlan.py +45 -4
  28. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/packet.py +2 -1
  29. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/registry.py +104 -13
  30. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/walk.py +64 -15
  31. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/conftest.py +57 -0
  32. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/MANIFEST.md +160 -8
  33. netprotocols-2.6.0/tests/fixtures/llc.pcap +0 -0
  34. netprotocols-2.6.0/tests/fixtures/lldp.pcap +0 -0
  35. netprotocols-2.6.0/tests/fixtures/mpls/mpls_snaplen.pcap +0 -0
  36. netprotocols-2.6.0/tests/fixtures/mpls.pcap +0 -0
  37. netprotocols-2.6.0/tests/fixtures/staging/llc.pcap +0 -0
  38. netprotocols-2.6.0/tests/fixtures/staging/lldp.pcap +0 -0
  39. netprotocols-2.6.0/tests/fixtures/staging/mpls/mpls_snaplen.pcap +0 -0
  40. netprotocols-2.6.0/tests/fixtures/staging/mpls.pcap +0 -0
  41. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/strategies.py +141 -0
  42. netprotocols-2.6.0/tests/test_cdp.py +922 -0
  43. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_checksum.py +171 -2
  44. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_contract.py +34 -0
  45. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_corpus.py +12 -0
  46. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_docs.py +93 -1
  47. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_enums.py +20 -3
  48. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_ethernet.py +4 -2
  49. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_fuzz.py +410 -1
  50. netprotocols-2.6.0/tests/test_llc.py +812 -0
  51. netprotocols-2.6.0/tests/test_lldp.py +819 -0
  52. netprotocols-2.6.0/tests/test_mpls.py +1012 -0
  53. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_registry.py +114 -10
  54. netprotocols-2.6.0/tests/test_stp.py +1139 -0
  55. {netprotocols-2.5.0 → netprotocols-2.6.0}/uv.lock +1 -1
  56. netprotocols-2.5.0/src/netprotocols/_enums.py +0 -109
  57. netprotocols-2.5.0/src/netprotocols/layer2/__init__.py +0 -5
  58. {netprotocols-2.5.0 → netprotocols-2.6.0}/.claude/skills/steward/SKILL.md +0 -0
  59. {netprotocols-2.5.0 → netprotocols-2.6.0}/.gitattributes +0 -0
  60. {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/CODEOWNERS +0 -0
  61. {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  62. {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  63. {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/ISSUE_TEMPLATE/protocol_request.yml +0 -0
  64. {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/dependabot.yml +0 -0
  65. {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/pull_request_template.md +0 -0
  66. {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/workflows/ci.yml +0 -0
  67. {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/workflows/codeql.yml +0 -0
  68. {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/workflows/docs-lint.yml +0 -0
  69. {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/workflows/fuzz.yml +0 -0
  70. {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/workflows/mutation.yml +0 -0
  71. {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/workflows/pr-title-lint.yml +0 -0
  72. {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/workflows/release.yml +0 -0
  73. {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/workflows/scorecard.yml +0 -0
  74. {netprotocols-2.5.0 → netprotocols-2.6.0}/.gitignore +0 -0
  75. {netprotocols-2.5.0 → netprotocols-2.6.0}/LICENSE +0 -0
  76. {netprotocols-2.5.0 → netprotocols-2.6.0}/benchmarks/baseline.json +0 -0
  77. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/_pcap.py +0 -0
  78. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/benchmark.py +0 -0
  79. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/benchmark_encode.py +0 -0
  80. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/benchmark_import.py +0 -0
  81. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures.sh +0 -0
  82. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_bgp.sh +0 -0
  83. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_dhcp.sh +0 -0
  84. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_dns_tcp.sh +0 -0
  85. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_gre.sh +0 -0
  86. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_http.sh +0 -0
  87. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_igmp.sh +0 -0
  88. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_ipip.sh +0 -0
  89. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_ipsec.sh +0 -0
  90. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_ospf.sh +0 -0
  91. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_pim.sh +0 -0
  92. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_rip.sh +0 -0
  93. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_sctp.sh +0 -0
  94. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_supplement.sh +0 -0
  95. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_vlan.sh +0 -0
  96. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_vrrp.sh +0 -0
  97. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_vxlan.sh +0 -0
  98. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/check_mutmut_survivors.py +0 -0
  99. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/extract_http_streams.py +0 -0
  100. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/ipsec_fixture_packets.py +0 -0
  101. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/mutmut_survivors.txt +0 -0
  102. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/pyodide/check_in_pyodide.py +0 -0
  103. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/pyodide/package-lock.json +0 -0
  104. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/pyodide/package.json +0 -0
  105. {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/pyodide/run_in_pyodide.mjs +0 -0
  106. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/_tlv.py +0 -0
  107. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/flow.py +0 -0
  108. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer2/arp.py +0 -0
  109. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/__init__.py +0 -0
  110. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/ah.py +0 -0
  111. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/esp.py +0 -0
  112. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/gre.py +0 -0
  113. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/icmp.py +0 -0
  114. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/igmp.py +0 -0
  115. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/ip.py +0 -0
  116. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/ipv6_ext.py +0 -0
  117. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/ospf.py +0 -0
  118. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/pim.py +0 -0
  119. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/vrrp.py +0 -0
  120. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer4/__init__.py +0 -0
  121. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer4/_ports.py +0 -0
  122. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer4/sctp.py +0 -0
  123. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer4/tcp.py +0 -0
  124. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer4/udp.py +0 -0
  125. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer7/__init__.py +0 -0
  126. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer7/bgp.py +0 -0
  127. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer7/dhcp.py +0 -0
  128. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer7/dns.py +0 -0
  129. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer7/http.py +0 -0
  130. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer7/rip.py +0 -0
  131. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer7/vxlan.py +0 -0
  132. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/pcap.py +0 -0
  133. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/py.typed +0 -0
  134. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/utils/__init__.py +0 -0
  135. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/utils/exceptions.py +0 -0
  136. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/utils/ipv4.py +0 -0
  137. {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/utils/mac.py +0 -0
  138. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/ah.pcap +0 -0
  139. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/arp_exchange.pcap +0 -0
  140. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/bgp/bgp_session.pcap +0 -0
  141. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/bgp.pcap +0 -0
  142. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/dhcp.pcap +0 -0
  143. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/dns_tcp.pcap +0 -0
  144. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/esp.pcap +0 -0
  145. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/gre.pcap +0 -0
  146. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/01.request +0 -0
  147. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/01.response +0 -0
  148. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/02.request +0 -0
  149. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/02.response +0 -0
  150. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/03.request +0 -0
  151. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/03.response +0 -0
  152. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/04.request +0 -0
  153. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/04.response +0 -0
  154. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/05.request +0 -0
  155. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/05.response +0 -0
  156. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/06.request +0 -0
  157. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/06.response +0 -0
  158. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/07.request +0 -0
  159. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/07.response +0 -0
  160. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/http.pcap +0 -0
  161. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/icmpv4_echo_lo.pcap +0 -0
  162. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/icmpv4_external.pcap +0 -0
  163. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/icmpv4_ttl_exceeded.pcap +0 -0
  164. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/icmpv6_echo_lo.pcap +0 -0
  165. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/igmp.pcap +0 -0
  166. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/ipip.pcap +0 -0
  167. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/ipv4_fragments.pcap +0 -0
  168. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/ipv6_fragments.pcap +0 -0
  169. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/ipv6_mld.pcap +0 -0
  170. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/ipv6_ndp_mld.pcap +0 -0
  171. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/ospf.pcap +0 -0
  172. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/pim.pcap +0 -0
  173. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/rip.pcap +0 -0
  174. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/sctp.pcap +0 -0
  175. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/ah.pcap +0 -0
  176. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/arp_exchange.pcap +0 -0
  177. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/bgp/bgp_session.pcap +0 -0
  178. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/bgp.pcap +0 -0
  179. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/dhcp.pcap +0 -0
  180. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/dns_tcp.pcap +0 -0
  181. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/esp.pcap +0 -0
  182. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/gre.pcap +0 -0
  183. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/http.pcap +0 -0
  184. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/icmpv4_echo_lo.pcap +0 -0
  185. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/icmpv4_external.pcap +0 -0
  186. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/icmpv4_ttl_exceeded.pcap +0 -0
  187. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/icmpv6_echo_lo.pcap +0 -0
  188. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/igmp.pcap +0 -0
  189. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/ipip.pcap +0 -0
  190. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/ipv4_fragments.pcap +0 -0
  191. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/ipv6_fragments.pcap +0 -0
  192. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/ipv6_mld.pcap +0 -0
  193. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/ipv6_ndp_mld.pcap +0 -0
  194. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/ospf.pcap +0 -0
  195. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/pim.pcap +0 -0
  196. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/rip.pcap +0 -0
  197. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/sctp.pcap +0 -0
  198. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/tcp_http.pcap +0 -0
  199. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/tcp_https.pcap +0 -0
  200. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/udp_dns.pcap +0 -0
  201. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/vlan_icmp.pcap +0 -0
  202. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/vrrp.pcap +0 -0
  203. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/vxlan.pcap +0 -0
  204. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/tcp_http.pcap +0 -0
  205. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/tcp_https.pcap +0 -0
  206. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/udp_dns.pcap +0 -0
  207. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/vlan_icmp.pcap +0 -0
  208. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/vrrp.pcap +0 -0
  209. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/vxlan.pcap +0 -0
  210. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fuzz/fuzz_decode_frame.py +0 -0
  211. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fuzz/fuzz_http_decode.py +0 -0
  212. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_ah.py +0 -0
  213. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_arp.py +0 -0
  214. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_bgp.py +0 -0
  215. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_dhcp.py +0 -0
  216. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_diagnostics.py +0 -0
  217. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_dns.py +0 -0
  218. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_esp.py +0 -0
  219. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_exports.py +0 -0
  220. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_extract_http_streams.py +0 -0
  221. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_flow.py +0 -0
  222. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_gre.py +0 -0
  223. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_http.py +0 -0
  224. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_icmp.py +0 -0
  225. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_igmp.py +0 -0
  226. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_ip.py +0 -0
  227. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_ipip.py +0 -0
  228. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_ipv6_ext.py +0 -0
  229. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_mutmut_gate.py +0 -0
  230. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_ospf.py +0 -0
  231. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_packet.py +0 -0
  232. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_pattern_matching.py +0 -0
  233. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_pcap.py +0 -0
  234. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_pim.py +0 -0
  235. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_rip.py +0 -0
  236. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_sctp.py +0 -0
  237. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_tcp.py +0 -0
  238. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_tlv.py +0 -0
  239. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_udp.py +0 -0
  240. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_utils.py +0 -0
  241. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_version.py +0 -0
  242. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_vlan.py +0 -0
  243. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_vrrp.py +0 -0
  244. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_vxlan.py +0 -0
  245. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_walk.py +0 -0
  246. {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_workflows.py +0 -0
@@ -140,7 +140,10 @@ the same three rules (spelled out in [`_base.py`](src/netprotocols/_base.py)):
140
140
  1. **It receives the entire remaining buffer** — everything from the
141
141
  start of its header to the end of the captured frame — and parses
142
142
  the fixed portion with `_struct.unpack_from`. A buffer too short
143
- for the fixed portion raises `TruncatedHeaderError`.
143
+ for the fixed portion raises `TruncatedHeaderError`. One enclosing
144
+ length shortens that buffer in a walk: after an Ethernet or VLAN
145
+ header holding an IEEE 802.3 Length, every later layer's buffer ends
146
+ where the Length ends the LLC PDU (see *802.3 framing* below).
144
147
  2. **Variable-length headers finish the job themselves.** IPv4 reads
145
148
  its IHL and TCP reads its data offset, sanity-checks it (5–15, and
146
149
  the declared bytes must exist in the buffer — a header that *lies*
@@ -217,6 +220,80 @@ VRRPv2's 8 bytes of authentication data), so it obeys rule 3 like a
217
220
  header with a length field and leaves Ethernet's padding undecoded. It
218
221
  must: a VRRPv3 advert for one IPv4 address is 12 bytes, padded on the
219
222
  wire, and VRRPv3's pseudo-header carries the message's length.
223
+ `STP` is the same case at layer 2: a BPDU has no length field, but its
224
+ BPDU Type gives its exact length (4, 35 or 36 octets), so it leaves
225
+ whatever follows undecoded, including the extra octets of a PDU longer
226
+ than its BPDU, which a receiver ignores (§14.4 of the IEEE 802.1 draft
227
+ the module cites). See
228
+ [`layer2/stp.py`](src/netprotocols/layer2/stp.py)'s module docstring.
229
+
230
+ `LLDP` has no length for the whole LLDPDU either, but bounds itself
231
+ by its last TLV: its TLVs each carry a Length, and the End of LLDPDU
232
+ TLV ends the walk, so it obeys rule 3 like a header with a length
233
+ field and leaves Ethernet's padding and an FCS undecoded. A TLV whose
234
+ Length runs past the buffer raises `TruncatedHeaderError`, as a lying
235
+ length does in rule 2. Wireshark's dissector notes that 802.1AB-2016
236
+ made the End TLV optional, so an LLDPDU that reaches the end of the
237
+ buffer without one decodes too; there nothing marks where a trailer
238
+ starts, so the walk reads one as TLVs: two or more zero bytes read as
239
+ an End TLV, and other bytes as another TLV or a framing error. The
240
+ mandatory Chassis ID, Port ID and Time To Live TLVs are read by
241
+ accessors from the first three positions, which answer `None` rather
242
+ than raise when the order is broken, since that is not framing. See
243
+ [`layer2/lldp.py`](src/netprotocols/layer2/lldp.py)'s module
244
+ docstring.
245
+
246
+ **802.3 framing.** Bytes 12-13 of an Ethernet header are a type or a
247
+ length (IEEE 802.3 Clause 3.2.6): from 0x0600 an EtherType, up to 1500
248
+ the length of an IEEE 802.2 LLC PDU, and undefined between, where the
249
+ walk ends. `Ethernet.ethertype` holds the raw value either way, and
250
+ `VLAN` reads the 2 bytes after a tag the same way. Neither `LLC` nor
251
+ `SNAP` has a length of its own, and a short PDU arrives padded to
252
+ Ethernet's minimum, often with an FCS after it; STP's BPDUs are that
253
+ short, and CDP's checksum covers exactly its PDU. So the Length is the
254
+ one enclosing length `decode_frame()` honours: after a header that
255
+ holds one (a class setting `_BOUNDS_PAYLOAD`, answering
256
+ `_payload_extent()`, both private while #235 settles the public form),
257
+ it ends the buffer it hands every later layer at the PDU's end. A
258
+ bound past the end of the frame bounds nothing, so a frame a snap
259
+ length cut decodes what was captured; a later, looser bound never
260
+ widens an earlier one; and a Length shorter than the LLC header leaves
261
+ `LLC.decode()` too few bytes, so the walk raises
262
+ `TruncatedHeaderError`. IPv4's, IPv6's and UDP's lengths are still not
263
+ honoured (#235), so padding behind a short IPv4 packet in Ethernet II
264
+ framing reaches its last layer as before. A third-party tag header
265
+ (an E-tag, a VN-tag) can answer `LLC` from its own `next_protocol()`,
266
+ but can bound the PDU after it only through that private hook, pending
267
+ #235. Called directly,
268
+ `LLC.decode()` has no Length to go by and leaves the rest of its buffer
269
+ to the caller, who can slice `frame[14 : 14 + eth.length]`. See
270
+ [`layer2/llc.py`](src/netprotocols/layer2/llc.py)'s module docstring.
271
+
272
+ `CDP`, which SNAP dispatches at OUI 00-00-0C PID 0x2000, is the first
273
+ decoder behind that bound. It has no length of its own: its TLVs run to
274
+ the end of the PDU, and its checksum covers the whole PDU, so in a walk
275
+ it decodes the PDU and nothing else. Its `decode()` walks TLVs
276
+ to the end of the buffer it is handed and stops early, without
277
+ raising, only where every byte left is zero, so Ethernet's zero padding
278
+ is left undecoded when it is called directly; rule 3 holds for those
279
+ zeros only, as for SCTP. Otherwise a TLV Length below 4 raises
280
+ `InvalidFieldError`, 1-3 bytes left after the last TLV raise
281
+ `TruncatedHeaderError`, and so does a TLV whose Length runs past the
282
+ buffer, in a walk too, where those bytes are inside the 802.3 Length.
283
+ Wireshark's dissector instead stops at a Length below 4 whatever
284
+ follows, and marks 1-3 bytes left malformed even when they are zero.
285
+ The zero rule has one blind spot: zeros after the last TLV but inside
286
+ the 802.3 Length belong to the PDU the sender declared, yet a walk
287
+ leaves them undecoded without raising, so `consumed` stopping short of
288
+ the Length's end is the only structural signal. `verify()` sums the
289
+ bytes decoded, without them; where the TLVs end at an odd offset on a
290
+ nonzero byte and the sender summed the whole Length, it returns
291
+ `False`.
292
+ `scripts/check_fixtures.py` rejects such a frame, and Wireshark marks
293
+ 1-3 such zeros malformed and shows 4 or more as a TLV of invalid
294
+ Length 0.
295
+ See [`layer2/cdp.py`](src/netprotocols/layer2/cdp.py)'s module
296
+ docstring.
220
297
 
221
298
  Decoding accepts `bytes` or `memoryview`. Views are a decode-time
222
299
  transient only: every stored field is a scalar or materialized
@@ -286,21 +363,32 @@ unless the Security Association uses a confidentiality-free transform
286
363
  (e.g. NULL, RFC 2410, or AES-GMAC, RFC 4543), and at an offset only
287
364
  the SA determines, so the decoder never reads it).
288
365
 
289
- `BGP` is the one exception, and it answers from decode context rather
290
- than from a wire field. A BGP message carries no payload: a TCP segment
291
- can hold several messages back to back, each decoded as its own layer,
292
- so `BGP.next_protocol()` names the next message *beside* it — `BGP`
293
- when the bytes after this message begin another one, `None` when they
294
- do not. Only those bytes can say which, and `next_protocol()` is handed
295
- none, so `decode()` records the answer on the instance in a field kept
296
- out of the dataclass's comparison and hash (`compare=False`, the
297
- mechanism `HTTP` uses for its `_message` parse cache). Two equal
298
- messages compare equal however they were found, and can still chain
299
- differently. The rules of the decode contract hold for it unchanged;
300
- see *Port-based dispatch is best-effort* below,
366
+ `BGP`, `MPLS` and `LLC` are the three exceptions: they answer from
367
+ decode context rather than from a wire field alone. A BGP message
368
+ carries no payload: a TCP segment can hold several messages back to
369
+ back, each decoded as its own layer, so `BGP.next_protocol()` names the
370
+ next message *beside* it — `BGP` when the bytes after this message
371
+ begin another one, `None` when they do not. An MPLS label stack carries
372
+ a packet but no field naming its protocol (see *MPLS: dispatch by
373
+ convention* below), so `MPLS.next_protocol()` infers one, partly from
374
+ the first byte after the stack. SAP 0x42 carries GARP's PDUs (Protocol
375
+ Identifier 0x0001) beside STP's BPDUs (0x0000), so `LLC.decode()`
376
+ records, for a DSAP 0x42 PDU it would dispatch, whether the two octets
377
+ after its Control are present and not 0x0000, and `LLC.next_protocol()`
378
+ then answers `None`, whatever `llc.dsap` holds at 0x42; fewer than two
379
+ octets still reach `STP`, which reports the truncation. Only the bytes
380
+ can say, and `next_protocol()` is handed none, so each `decode()`
381
+ records what it saw on the instance in a field kept out of the
382
+ dataclass's comparison and hash (`compare=False`, the mechanism `HTTP`
383
+ uses for its `_message` parse cache). Two equal headers compare equal
384
+ however they were found, and can still chain differently. The rules of
385
+ the decode contract hold for all three unchanged; see *Port-based
386
+ dispatch is best-effort* below,
301
387
  [`_base.py`](src/netprotocols/_base.py)'s *Decode context in
302
- next_protocol*, and
303
- [`layer7/bgp.py`](src/netprotocols/layer7/bgp.py)'s module docstring.
388
+ next_protocol*, the module docstrings of
389
+ [`layer7/bgp.py`](src/netprotocols/layer7/bgp.py) and
390
+ [`layer2/mpls.py`](src/netprotocols/layer2/mpls.py), and
391
+ [`layer2/llc.py`](src/netprotocols/layer2/llc.py)'s *DSAP 0x42*.
304
392
 
305
393
  ```mermaid
306
394
  flowchart LR
@@ -308,10 +396,23 @@ flowchart LR
308
396
  E -->|0x0800| I4[IPv4]
309
397
  E -->|0x86DD| I6[IPv6]
310
398
  E -->|0x8100, 0x88A8, 0x9100| V[VLAN tag]
399
+ E -->|length ≤ 1500| L[LLC]
400
+ V -->|length ≤ 1500| L
401
+ L -->|DSAP 0xAA, SSAP 0xAA, UI| SN[SNAP]
402
+ L -->|DSAP 0x42, UI or I, Protocol ID 0| STP[STP BPDU]
403
+ SN -->|OUI 00-00-00 or 00-00-F8, PID 0x0806| A
404
+ SN -->|OUI 00-00-00 or 00-00-F8, PID 0x0800| I4
405
+ SN -->|OUI 00-00-0C, PID 0x2000| CDP[CDP]
311
406
  V -->|0x8100, 0x88A8, 0x9100| V
312
407
  V -->|0x0806| A
313
408
  V -->|0x0800| I4
314
409
  V -->|0x86DD| I6
410
+ E -->|0x8847, 0x8848| M[MPLS label stack]
411
+ V -->|0x8847, 0x8848| M
412
+ M -->|bottom label 0, or first nibble 4| I4
413
+ M -->|bottom label 2, or first nibble 6| I6
414
+ E -->|0x88CC| LD[LLDP]
415
+ V -->|0x88CC| LD
315
416
  I4 -->|1| IC4[ICMPv4]
316
417
  I4 -->|6| T[TCP]
317
418
  I4 -->|17| U[UDP]
@@ -390,8 +491,36 @@ bitfields — `pcp`, `dei`, `vid` — as dataclass fields validated in
390
491
  property (the same pattern `IPv6` uses for `version` /
391
492
  `traffic_class` / `flow_label`).
392
493
 
494
+ **MPLS: dispatch by convention.** An MPLS label stack (RFC 3032) sits
495
+ where a VLAN tag does, after Ethernet's EtherType (0x8847, or 0x8848
496
+ for a multicast frame whose top label is upstream-assigned, RFC 5332),
497
+ and decodes as one `MPLS` layer however many 4-byte entries it holds:
498
+ the stack ends at the entry with its Bottom of Stack bit set, so it
499
+ bounds itself, and a deep stack counts once against `max_depth`. What
500
+ it does not have is a field naming the packet it carries. RFC 3032
501
+ §2.2 leaves that to the binding of the label popped at the egress,
502
+ which a capture never shows, so `MPLS.next_protocol()` follows a
503
+ convention. A bottom label reserved for a payload settles it — 0, IPv4
504
+ Explicit NULL, is IPv4 and 2, IPv6 Explicit NULL, is IPv6, whatever
505
+ the bytes say, while 13 (GAL) and 14 (OAM Alert) are followed by a
506
+ channel header or an OAM message and stop the walk. Otherwise the
507
+ first nibble after the stack is read as an IP version, 4 or 6, as
508
+ routers that load-balance IP over MPLS do (RFC 4928); anything else, a
509
+ pseudowire control word's 0 among them, stops the walk. The class
510
+ comes from the `ethertype` table at 0x0800 or 0x86DD, so a `registry`
511
+ or `decode_as` override reaches the labelled packet too. The nibble is
512
+ a guess, as a port is: an Ethernet pseudowire sent without a control
513
+ word, whose inner destination MAC address happens to begin with 4 or
514
+ 6, decodes as an IP header, which is the misreading RFC 4928 and RFC
515
+ 4385's control word exist to prevent. GRE dispatches its Protocol Type
516
+ through the `ethertype` table too, so MPLS in GRE (RFC 4023) reaches
517
+ `MPLS` with no code of its own. See
518
+ [`layer2/mpls.py`](src/netprotocols/layer2/mpls.py)'s module
519
+ docstring.
520
+
393
521
  The numbers on the arrows are the wire values — EtherTypes out of
394
- Ethernet, IP protocol numbers out of IPv4/IPv6 — and they live in
522
+ Ethernet, IP protocol numbers out of IPv4/IPv6; MPLS's arrows, which
523
+ no wire field names, are the convention above — and they live in
395
524
  [`_enums.py`](src/netprotocols/_enums.py) as `EtherType` and
396
525
  `IPProtocol`. IPv4 and IPv6 share one registry because the number
397
526
  space is shared (1 is ICMPv4, 58 is ICMPv6, 6 is TCP, 17 is UDP, 132
@@ -413,11 +542,13 @@ packet = decode_frame(frame) # Packet(Ethernet(...), IPv4(...), TCP(...
413
542
  Underneath it is the loop the two answers imply:
414
543
 
415
544
  ```python
416
- layers, cursor, protocol = [], 0, start
545
+ layers, cursor, end, protocol = [], 0, len(frame), start
417
546
  while protocol is not None:
418
- header = protocol.decode(frame[cursor:])
547
+ header = protocol.decode(frame[cursor:end])
419
548
  layers.append(header)
420
549
  cursor += header.header_len
550
+ if header._BOUNDS_PAYLOAD: # an 802.3 Length (see above)
551
+ end = _bounded(header, cursor, end)
421
552
  protocol = header.next_protocol(registry)
422
553
  ```
423
554
 
@@ -434,12 +565,14 @@ Wrapping each frame in a `memoryview` internally measured *slower*
434
565
  **Where do those arrows live?** Not in the protocol classes. Every
435
566
  one of them is a row in a dispatch table owned by
436
567
  [`registry.py`](src/netprotocols/registry.py), and `next_protocol()`
437
- is a single `dict.get` against one of five tables named after the wire
568
+ is a single `dict.get` against one of seven tables named after the wire
438
569
  field they dispatch on:
439
570
 
440
571
  | Table | Dispatches on | Read by |
441
572
  |---|---|---|
442
- | `ethertype` | Ethernet II EtherType | `Ethernet`, `VLAN`, `GRE` |
573
+ | `ethertype` | Ethernet II EtherType (from Ethernet and VLAN, 0x0600 and up only; from GRE and SNAP, any value) | `Ethernet`, `VLAN`, `GRE`, `SNAP` (OUI 00-00-00, 00-00-F8); `MPLS` at 0x0800 and 0x86DD, by convention |
574
+ | `llc.dsap` | IEEE 802.2 LLC DSAP | `LLC` |
575
+ | `llc.snap` | SNAP `(oui << 16) \| pid`, any other OUI | `SNAP` |
443
576
  | `ip.proto` | IPv4 `protocol` | `IPv4`, `AH` |
444
577
  | `ip.proto.v6` | IPv6 `next_header` | `IPv6`, extension headers, `AHv6` |
445
578
  | `udp.port` | UDP well-known port | `UDP` |
@@ -487,16 +620,18 @@ therefore take the nearest one before it, as `Packet.with_checksums()`
487
620
  and `Packet.flow_key()` do, not the first.
488
621
 
489
622
  **Extending the walk without forking.** The tables are public. A
490
- protocol this library does not implement — MPLS, a proprietary
491
- telemetry header — is one registration away from participating in
623
+ protocol this library does not implement — a proprietary telemetry
624
+ header, say, sent under 0x88B5, which IANA's EtherType registry lists
625
+ as "IEEE Std 802 - Local Experimental Ethertype", so no built-in
626
+ decoder holds it — is one registration away from participating in
492
627
  frame walks like any built-in:
493
628
 
494
629
  ```python
495
630
  from netprotocols import Protocol
496
631
  from netprotocols.registry import register
497
632
 
498
- @register("ethertype", 0x8847)
499
- class MPLS(Protocol):
633
+ @register("ethertype", 0x88B5)
634
+ class Telemetry(Protocol):
500
635
  ...
501
636
  ```
502
637
 
@@ -519,19 +654,23 @@ inside `install()` and run once from `__init__.py` after every
519
654
  protocol class exists. Shared enums stay in the dependency-free
520
655
  `_enums.py`. The layer modules bind their table at import, and every
521
656
  class a header chooses by a field value (EtherType, IP protocol, port)
522
- comes out of a table, never from an import. Two modules do import
657
+ comes out of a table, never from an import. Three modules do import
523
658
  another's class, where there is no field to look up. `layer3/icmp.py`
524
659
  names `IPv4` and `IPv6` to start the walk of the packet an ICMP error
525
- quotes, which is not dispatch, and `layer7/vxlan.py` names `Ethernet`
526
- directly, because a VXLAN payload is always an Ethernet frame. Neither
527
- can form a cycle: `ip.py` and `ethernet.py` import no other layer
528
- module.
660
+ quotes, which is not dispatch; `layer7/vxlan.py` names `Ethernet`
661
+ directly, because a VXLAN payload is always an Ethernet frame; and
662
+ `layer2/ethernet.py` names `LLC`, because whatever an 802.3 Length's
663
+ value, an LLC header follows it. None can form a cycle: `ip.py` and
664
+ `llc.py` import no other layer module, and `ethernet.py` imports only
665
+ `llc.py`.
529
666
 
530
667
  ## Port-based dispatch is best-effort
531
668
 
532
- The `ethertype` and `ip.proto` tables dispatch on a single
533
- authoritative field: it exists to say what follows. The two port
534
- tables break that model, and differ in kind rather than just in name.
669
+ The `ethertype`, `llc.*` and `ip.proto` tables dispatch on a single
670
+ authoritative field: it exists to say what follows (MPLS, which looks
671
+ up the `ethertype` table by convention, is the exception described
672
+ above). The two port tables break that model, and differ in kind
673
+ rather than just in name.
535
674
  A port is a *guess* — any service may run on any port, and the
536
675
  discriminator is split across the source and destination ports.
537
676
  Dispatch therefore checks the destination port first (a request
@@ -747,11 +886,17 @@ src/netprotocols/
747
886
  ├── _defaults.py the built-in decoder map, installed at import
748
887
  ├── packet.py Packet composition, with_checksums(), flow_key()
749
888
  ├── checksum.py RFC 1071 internet_checksum, SCTP's crc32c,
750
- │ compute, verify, OSPF's LSA Fletcher checksum
889
+ │ compute, verify (CDP's odd-byte sum among them),
890
+ │ OSPF's LSA Fletcher checksum
751
891
  ├── flow.py FlowKey, flow_key(): canonical bidirectional keys
752
892
  ├── pcap.py read_captures()/read_pcap()/read_pcapng(): classic
753
893
  │ pcap and pcapng readers, from bytes not filenames
754
- ├── layer2/ ethernet.py, arp.py, vlan.py (802.1Q / 802.1ad)
894
+ ├── layer2/ ethernet.py, arp.py, vlan.py (802.1Q / 802.1ad),
895
+ │ llc.py (802.2 LLC + SNAP, after an 802.3 Length),
896
+ │ cdp.py (Cisco Discovery Protocol, after SNAP),
897
+ │ lldp.py (LLDP, IEEE 802.1AB),
898
+ │ mpls.py (MPLS label stack: MPLS + MPLSLabel),
899
+ │ stp.py (802.1D / 802.1w BPDUs)
755
900
  ├── layer3/ ip.py (IPv4 + IPv6), icmp.py (ICMPv4 + ICMPv6),
756
901
  │ igmp.py, gre.py, esp.py (IPsec ESP),
757
902
  │ ah.py (IPsec AH: AH + AHv6),
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.6.0] - 2026-10-28
11
+
12
+ ### Added
13
+
14
+ - **`MPLS`: the MPLS label stack (RFC 3032 §2.1, #187).** EtherTypes 0x8847 (`EtherType.MPLS`, "MPLS") and 0x8848 (`EtherType.MPLS_UPSTREAM_ASSIGNED`, "MPLS with upstream-assigned label": IANA's names; RFC 5332 §4 retired 0x8848's original "multicast" meaning) now dispatch to `netprotocols.MPLS` (also `netprotocols.layer2.MPLS`), from Ethernet, VLAN tags and GRE alike, so MPLS in GRE (RFC 4023) needs no code of its own. One `MPLS` layer holds the whole stack, top first, as `labels`, a tuple of `MPLSLabel` entries (`label`, `traffic_class` — RFC 5462's name for RFC 3032's Exp field — `bottom_of_stack` and `ttl`, with `word`, `bytes()`, `is_special_purpose`, and `label_name`, the base special-purpose labels named as IANA's registry names them); `bottom` is the last entry. `decode()` reads entries until the first with the S bit set, so the stack bounds itself and `bytes(MPLS.decode(x)) == x[:header_len]`; what follows, Ethernet padding and FCS included, is the labelled packet's. A stack whose S bit never comes raises `TruncatedHeaderError` (`field="labels"`, `offset` where the missing entry would start), in time linear in the buffer, and a deep stack counts once against `max_depth`. Construction requires a non-empty stack with S set on the last entry alone, so every stack that can be built round-trips. MPLS names no payload protocol, so `next_protocol()` follows a documented convention: a bottom label of 0 or 2 (IPv4 or IPv6 Explicit NULL) names IPv4 or IPv6, 13 (GAL) and 14 (OAM Alert) stop the walk, and otherwise the first nibble after the stack is read as an IP version, 4 or 6 (RFC 4928 §2), so a pseudowire control word or associated channel header (first nibble 0 or 1, RFC 4385) stops it. The class comes from the `ethertype` table at 0x0800 or 0x86DD, so `registry` and `decode_as` overrides reach the labelled packet; `payload_ethertype` says which the convention chose. `next_protocol()` is handed no bytes, so `decode()` records the nibble on the instance outside its comparison and hash, as `BGP` records whether another message follows; a stack built rather than decoded answers from its bottom label alone. The guess fails, as RFC 4928 describes, on an Ethernet pseudowire sent without a control word whose inner destination MAC begins with nibble 4 or 6, which decodes as an IP header; the module docstring says how to walk such a payload explicitly. Since 0x8847 and 0x8848 are now built in, code that registered a different class at either on the default registry (as the `register()` examples in 2.5.0's `registry.py`, README.md, ARCHITECTURE.md and `docs/CLAIMS.md` did), or on one built from it with `Registry.from_defaults()` or `derive()`, now raises `RegistryConflictError`; pass `override=True` to replace `MPLS`, or `decode_as={"ethertype": {0x8847: ..., 0x8848: ...}}` (each key you used) to override it for one walk.
15
+ - **`LLDP`: the Link Layer Discovery Protocol (IEEE 802.1AB), layer 2 (#186).** EtherType 0x88CC (`EtherType.LLDP`, "LLDP") now dispatches to `netprotocols.LLDP` (also `netprotocols.layer2.LLDP`) from Ethernet and from a VLAN tag, and the walk ends there. An LLDPDU is a run of TLVs, each a 16-bit header word (a 7-bit Type, a 9-bit Length) and Length bytes of value; `LLDP.tlvs` keeps every one, in wire order, as an `LLDPTLV(type, value)` with `length` and `type_name` (Wireshark's names for types 0-8 and 127, "unknown (n)" for the reserved 9-126), through the End of LLDPDU TLV, so `bytes(LLDP.decode(x)) == x[:header_len]` holds by construction. The End TLV bounds the LLDPDU: Ethernet padding, an FCS or anything else after it is left undecoded. An LLDPDU that reaches the end of the buffer without one decodes too, since 802.1AB-2016 made the End TLV optional, as Wireshark's dissector notes (lldpd 1.0.18's receiver still discards one); without an End TLV a trailer is read as TLVs: two or more zero bytes read as an End TLV, and anything else as another TLV or a framing error. `decode` checks framing only: a buffer shorter than one TLV header raises `TruncatedHeaderError` as any too-short header does (offset 0, no `field`), a later TLV header cut short or a TLV whose Length runs past the buffer raises it with `field="tlvs"` and the TLV's offset, and an End TLV with a nonzero Length raises `InvalidFieldError`. The mandatory order is reported, not enforced: `chassis_id` (`LLDPChassisID`) and `port_id` (`LLDPPortID`), each a `subtype` and the raw `id` with `subtype_name`, and `ttl` (an `int`, its first two bytes) read the first, second and third TLV and are `None` when that TLV is of another type, an ID's value is not 2 to 256 bytes or the TTL's is shorter than 2 bytes, so none raises; `has_mandatory_tlvs` says whether all three are there (any subtype passes), `is_shutdown` whether the TTL is 0, and `tlv(type)` finds the first TLV of a type. Subtype numbers follow the IEEE 802.1 LLDP MIBs and the display names Wireshark's dissector. Every other TLV, organizationally specific ones included, keeps its value raw. The walk is local to `layer2/lldp.py`; `_tlv.walk_kind_length_value`, which TCP, IPv4, IPv6 and PIM share, is unchanged. EtherType 0x88CC no longer serves the tests and README as the example of an unknown EtherType; 0x88B5, IEEE 802's Local Experimental EtherType 1, does.
16
+ - **`LLC` and `SNAP`: IEEE 802.2 LLC and SNAP headers after an IEEE 802.3 Length (#188).** An Ethernet or VLAN type/length field of 1500 or less is a frame Length (IEEE 802.3 Clause 3.2.6), and `next_protocol()` now answers `netprotocols.LLC` (also `netprotocols.layer2`) for it: DSAP, SSAP and a 1-octet (U-format) or 2-octet (I- and S-format) Control, stored with its first octet as the integer's low byte so the format bits are its two low bits. I-format and UI PDUs dispatch on the DSAP through the new `llc.dsap` table (`registry.TABLE_LLC_DSAP`), at DSAP 0x42 only when the two octets after the Control are 0x0000, or fewer than two octets follow (next entry); other formats, such as XID and TEST, end the walk. Accessors: `control_format`, `is_group`, `is_response`, `is_raw_ipx`, `dsap_name`, `ssap_name`, `dsap_enum`. The new `LLCSAP` enum holds the IEEE Registration Authority's assigned LSAPs and the global DSAP, with display names. `SNAP` (OUI and PID, RFC 1042) is a built-in `llc.dsap` entry, at 0xAA, reached only when the SSAP is 0xAA too and the Control is UI; `STP`, at 0x42, is the other (next entry). Under OUI 00-00-00 (RFC 1042) or 00-00-F8 (IEEE 802.1H bridge-tunnel) the PID is an EtherType and dispatches through `ethertype`; under any other OUI it goes through the new `llc.snap` table (`registry.TABLE_LLC_SNAP`), keyed `(oui << 16) | pid` (`SNAP.snap_key`). This departs from #188, which recommended reading every PID in `ethertype`: RFC 9542 §3 makes the PID a number the OUI's owner assigns, so CDP (OUI 00-00-0C, PID 0x2000) is not EtherType 0x2000. A Novell raw 802.3 frame (0xFFFF where the SAPs would be: an IPX checksum) decodes as an `LLC` layer with `is_raw_ipx` set and ends the walk; that layer's other fields and accessors then read IPX bytes and are meaningless (the fixture's frame reads as an I-format Control 0x1E00, the IPX length). STP (DSAP 0x42) decodes as `STP` (next entry), and CDP as `CDP` (the entry after it).
17
+ - **`STP`: Spanning Tree BPDUs, IEEE 802.1D and 802.1w (RSTP) (#189).** LLC DSAP 0x42 (`LLCSAP.SPANNING_TREE`) now dispatches, for a UI or I-format PDU, to `netprotocols.STP` (also `netprotocols.layer2.STP`), the walk's last layer, unless the two octets after the Control are present and not 0x0000: GARP's PDUs, such as GVRP's, share the SAP with Protocol Identifier 0x0001 (IEEE P802.1aq/D1.5++ §14.5 NOTE 1 leaves other Protocol Identifiers to other protocols), and their walk ends at `LLC`, whatever class `llc.dsap` holds at 0x42. `LLC.decode()` records that check on the instance outside its comparison and hash, so equal LLC headers still compare and hash equal; a header built with the constructor or `dataclasses.replace()` has seen no bytes and dispatches. Fewer than two octets still reach `STP`, which reports the truncation. One class decodes the three BPDU Types and branches on the type, as ICMP does: the Configuration BPDU (type 0x00, 35 octets, 802.1D-2004 §9.3.1), the RST BPDU (0x02, 36 octets, §9.3.3: the same fields and a Version 1 Length) and the Topology Change Notification (0x80, §9.3.2), which is the 4-octet prefix alone and decodes as the short form of the class, every field after `bpdu_type` `None`. Fields: `protocol_id`, `version`, `bpdu_type`, then `flags`, `root_priority`, `root_mac`, `root_path_cost`, `bridge_priority`, `bridge_mac`, `port_id`, `message_age`, `max_age`, `hello_time`, `forward_delay` (the timers raw, in 1/256 s) and `version_1_length` (RST only). The flag accessors answer `None` in a type that leaves their bit unused: `topology_change` (Configuration and RST), `topology_change_ack` (Configuration only), and `proposal`, `port_role`/`port_role_name`, `learning`, `forwarding` and `agreement` (RST only); the raw octet stays in `flags`. The two priority octets of a Bridge Identifier are kept whole (802.1D-1998 made all 16 bits a priority) and split as 802.1t does by `root_priority_component`/`root_system_id_extension` and `bridge_priority_component`/`bridge_system_id_extension`; also `root_id_str`/`bridge_id_str` (Wireshark's priority/extension/MAC form), `port_priority`/`port_number`, `*_seconds` for the four timers, `bpdu_type_name` and `version_name`. The flag bits, port role values and field layout follow the unapproved IEEE 802.1 draft "P802.1aq/D1.5++suggested changes, May 16, 2009" (`aq-seaman-merged-bpdu-encoding-0509.pdf`, whose Clause 14 merges 802.1D Clause 9 with 802.1Q-2005 Clause 14), except that its §14.4 o) mislabels octets 34-35 as Max Age: they carry Forward Delay, as in Wireshark's `packet-bpdu.c` and Linux's `br_stp_bpdu.c`. The type and version names and port role names 1-3 are Wireshark's (`packet-bpdu.c`, fetched 2026-10-25); role 0, which Wireshark does not name, is `Unknown`, 802.1D-2004's name for it (802.1Q calls it Master; the draft's §14.2.9 NOTE). `header_len` is exactly the type's length: Ethernet padding and an FCS never reach the decoder in a walk (the 802.3 Length bounds the PDU), a direct `decode()` leaves them undecoded, and the octets of a PDU longer than its BPDU, which a receiver ignores (§14.4 and §14.6 i)3) of that draft), stay after the layer. Cisco PVST+'s SSTP BPDUs, the ones that carry TLVs, travel in SNAP under OUI 00-00-0C, PID 0x010B, not at DSAP 0x42 (Wireshark parses their TLVs only there), and walk as far as `SNAP`; an `llc.snap` entry at `(0x00000C << 16) | 0x010B` would reach `STP` from there. PVST+'s VLAN 1 BPDUs go untagged to the IEEE address, at DSAP 0x42, and decode as `STP` (Cisco does not say they carry a TLV; Wireshark parses PVST+ TLVs only under PID 0x010B) (Cisco document 24063, "Troubleshoot Spanning Tree PVID- and Type-Inconsistencies"; the full URL is in `layer2/stp.py`'s module docstring). A Protocol Identifier other than 0 (on a direct `STP.decode()`; a walk from LLC DSAP 0x42 does not reach it) and a BPDU Type other than the three raise `InvalidFieldError` (`field="protocol_id"`, offset 0, or `field="bpdu_type"`, offset 3), since neither leaves a layout to read; a buffer shorter than its type needs raises `TruncatedHeaderError` (`field="bpdu_type"`). The version, unused flag bits and a nonzero Version 1 Length are carried verbatim, and a type 0x02 BPDU is laid out as RST whatever its version. **MSTP is out of scope**: an MST BPDU (802.1s/802.1Q, version 3) decodes as its 36-octet RST prefix, and its Version 3 Length, MST configuration and MSTI messages are left undecoded after the layer, as a version 2 implementation reads them (§14.6 i), and §14.5 d) and e), of the draft). In a walk the 802.3 Length already ends the PDU, but kept raw in the layer they would need an end on a direct `decode()` too, where only the Version 3 Length could give one, and that field is unreliable on the wire (Wireshark's `packet-bpdu.c` notes a Cisco C3550 release that appears to send it in MSTI-message units, and MST BPDUs that carry a Version 3 Length of 0 and put the length in the format selector octet): read to the end of the buffer instead, they would take in padding and an FCS. In a walk a caller recovers them with `frame[packet.consumed : 14 + packet[Ethernet].length]` (behind one VLAN tag, `18 + packet[VLAN].length`). The real BPDUs are the corpus's 802.1D ones; RST BPDUs are tested with constructed frames only, since neither the kernel bridge nor any installable package here speaks RSTP.
18
+ - **`CDP`: the Cisco Discovery Protocol, versions 1 and 2 (#190).** SNAP OUI 00-00-0C PID 0x2000 now dispatches through `llc.snap` to `netprotocols.CDP` (also `netprotocols.layer2`), which decodes the 4-byte header (`version`, `ttl`, `checksum`) and the TLVs after it as `CDPTLV(type, value)`, each a 2-byte Type and a 2-byte Length that counts the 4-byte TLV header, kept raw in wire order with `type_name` from Wireshark's packet-cdp.c (commit 31bf0857, fetched 2026-10-25), so `bytes(CDP.decode(x)) == x[:header_len]`. Any Version decodes, since the SNAP header says outright that CDP follows. Accessors read the first TLV of their type and return `None` when it is missing or malformed: `device_id`, `port_id`, `software_version`, `platform` (ASCII, a byte outside it as a backslash escape), `capabilities` (the 32-bit mask) and `capability_names`, and `addresses` and `management_addresses` (type 22), lists of `CDPAddress` (protocol type, protocol, address, with `ip` for NLPID 0xCC IPv4 and the 802.2 form of EtherType 0x86DD IPv6), plus `tlv(type)`. CDP has no PDU length: in a walk the 802.3 Length bounds it, so padding and an FCS stay out. `decode` stops early without raising only where every byte left is zero, so zero padding is left undecoded when it is called directly; otherwise a TLV Length below 4 raises `InvalidFieldError` and 1-3 bytes left after the last TLV raise `TruncatedHeaderError`, in a walk too (both `field="tlvs"`, at the offset where the TLV would start), as does a TLV that runs past the buffer. Zeros after the last TLV but inside the 802.3 Length are part of the declared PDU, yet a walk leaves them undecoded without raising: `consumed` stopping short of the Length's end is the only structural signal, `verify()` (which sums the bytes decoded) fails on them only where the TLVs end at an odd offset on a nonzero byte and the sender summed the whole Length, and `scripts/check_fixtures.py` rejects such a frame. Wireshark's packet-cdp.c differs: it stops at a Length below 4 whatever follows, and marks 1-3 bytes left malformed even when they are zero. A nonzero trailer that happens to read as whole TLVs (a 4-byte FCS ending in 0x0004) decodes as phantom TLVs; there `verify()` is the check that the bytes decoded are the PDU the sender summed. Wireshark's Port ID length heuristic is not reproduced. The walk ends at CDP.
19
+ - **CDP checksums in `compute`/`verify`/`Packet.with_checksums()`.** The internet checksum of the whole PDU, checksum field zeroed, no pseudo-header (`ip` and `payload` ignored), except that an odd final byte `v` is added as the low byte of a last word below 0x80 and as `0xFF00 + v - 1` from 0x80, as lldpd 1.0.18's `frame_checksum()` and Wireshark's packet-cdp.c sum it; `internet_checksum()` alone gives the wrong value for an odd-length PDU whose final byte is not 0x00 (there the two coincide). `with_checksums()` fills it as it fills IPv4's.
20
+ - **`Ethernet.is_length`/`Ethernet.length` and `VLAN.is_length`/`VLAN.length`**: whether the raw `ethertype` field holds an 802.3 Length, and the Length or `None`. The field itself is unchanged in type and meaning.
21
+
22
+ ### Changed
23
+
24
+ - **Ethernet and VLAN dispatch branch on type versus length.** A value of 0x0600 or more dispatches through `ethertype` exactly as before; 1500 or less answers `LLC` (it used to look the value up in `ethertype`, which accepted a key there: registering one, such as 0x0026, a Configuration BPDU's Length, was the only way to decode STP; see the Behaviour change entry below); 1501-1535, which IEEE 802.3 leaves undefined, answers `None`. GRE's Protocol Type and a SNAP PID are EtherTypes only and keep the plain lookup. No frame in the 2.5.0 corpus has a type/length value below 0x0600, and every one of them decodes exactly as before.
25
+ - **`decode_frame()` ends every later layer's buffer at the end of the LLC PDU an 802.3 Length gives**, so Ethernet zero padding and an FCS after a short PDU reach no decoder and stay past `packet.consumed`. A Length past the end of the frame bounds nothing (a frame a snap length cut decodes what was captured), a tighter bound is never widened by a later one, and a Length shorter than the 3-byte LLC header makes `LLC.decode()` raise `TruncatedHeaderError`. It is the only enclosing length the walk honours; IPv4's, IPv6's and UDP's are not (#235). The hook is private (`Protocol._BOUNDS_PAYLOAD`, `_payload_extent()`) while #235 settles a public form.
26
+ - **Behaviour change: an `ethertype` entry at 1500 or less is no longer reached from Ethernet or VLAN.** The table accepts any key, as in 2.5.0, but Ethernet and VLAN no longer look a type/length value of 1500 or less up there: they answer `LLC` for it, and nothing for 1501-1535 (see the entry above). So a class registered at, say, 0x0026 to decode STP over raw Ethernet is no longer reached from an Ethernet frame. GRE still looks any value up in `ethertype`, so such an entry still answers GRE Protocol Type 0x0026, as in 2.5.0, and the new `SNAP` looks a PID under OUI 00-00-00 or 00-00-F8 up there too, so it now also answers an RFC 1042 or 802.1H SNAP PID of 0x0026. RFC 1701's "Current List of Protocol Types" assigns GRE three values below 0x0600, SNA 0x0004, OSI network layer 0x00FE and PUP 0x0200, and an entry at 0x00FE is reached from GRE Protocol Type 0x00FE exactly as before. This dispatch change is inherent to #188's type/length reading. To migrate a raw-Ethernet decoder, register it in `llc.dsap` under its DSAP or, for a SNAP protocol, in `llc.snap` under `(oui << 16) | pid`. STP needs no migration: the built-in `STP` now holds DSAP 0x42, the one key that covers what 2.5.0 needed one key per 802.3 Length for (0x0026 for a Configuration BPDU, 0x0007 for a Topology Change Notification, 0x0027 for an RST BPDU, and more for MST BPDUs, whose Length varies); a decoder of one's own goes there with `override=True`, or in a registry of its own (`Registry.from_defaults().derive(...)`, or `decode_frame(decode_as=...)`), and is handed, like `STP`, only a PDU whose first two octets are 0x0000, or that holds fewer than two, never GARP's. The decoder is handed different bytes: in 2.5.0 it got everything from the LLC header to the end of the frame, padding and FCS included, and from `llc.dsap` it gets the PDU after the LLC header (from `llc.snap`, after the SNAP header), ending where the 802.3 Length does, so a migrated decoder must parse from there. The two new tables refuse a key their field cannot carry, `llc.dsap` one outside 0-0xFF and `llc.snap` one outside 0 to 2**40 - 1, raising the new `InvalidRegistryKeyError` (a `ProtocolError`, exported from `netprotocols` and `netprotocols.registry`) from `register()`, `register_all()`, `Registry.derive()` and `decode_frame(decode_as=...)` (whatever `lax` says). `register_all()` is not atomic, for this error as for a `RegistryConflictError`: the keys before the one that raises stay registered. Every other table, `ethertype` included, accepts any key, as before.
27
+
28
+ ### Development
29
+
30
+ - **Every comparative number the README quotes was re-measured for this release** (Rule 4 of `docs/CLAIMS.md`), on the same machine class as 2.5.0's figures but on the 293-frame corpus Tier 3 grew from 265 (30 scenarios, from 27), against `dpkt 1.9.8` and `scapy 2.8.0`. The headline ratios held: dpkt decodes the corpus 1.33× faster (1.28-1.34× over three runs) and netprotocols stays 3.4× ahead of scapy. **The README's sub-claim that the 140 frames of 2.4.0's corpus "are still within 10%" of dpkt no longer holds and is replaced**: over sixteen alternating runs of a file-by-file harness, dpkt leads on them by a median 1.14× on this tree against 1.07× on 2.5.0's. Whether Tier 3's type-versus-length dispatch and payload bound cost the old frames anything is not resolved: netprotocols' median over the 265 frames both trees share is 3.3% lower than 2.5.0's (lower in 12 of 16 alternating pairs, with dpkt's own median flat), inside this machine's run-to-run spread of about ±8%, and it was not profiled ([#250](https://github.com/EONRaider/NETProtocols/issues/250)). On the new MPLS and LLC/SNAP captures the two libraries are level; dpkt does not decode LLDP, so it is about 5× faster on those six frames. Encode is 4.66× ahead of dpkt, import 89.8 ms (from 70.0 ms; the five new modules cost about 15 ms against the 2.5.0 tree run alternately on the same machine) against `scapy.all`'s 436.2 ms, and the wheel is 220.5 KiB, 12.2× smaller than scapy 2.8.0's 2.63 MiB. Decode depth against dpkt is 94 of 293 frames deeper; of the 15 new, 7 are frames dpkt leaves raw (the six LLDP frames and a VLAN-tagged 802.3 SNAP frame), and 8 are MPLS and SNAP frames on which netprotocols makes the label stack or the SNAP header a layer of its own where dpkt folds it into `Ethernet` or `LLC`; STP and CDP are decoded by dpkt too. dpkt cannot decode one frame, the 4-byte STP Topology Change Notification (`NeedData`), so `--depth` skips it and every comparison reports dpkt at 292/293. `docs/CLAIMS.md` records each run, the per-file table, the A/B and the frame-by-frame depth breakdown.
31
+ - **`scripts/check_fixtures.py` checks RST BPDUs and a Configuration BPDU's flags (#189).** Its independent `_check_bpdu` now knows the RST BPDU (type 0x02, 36 octets, a Protocol Version of 2 or more, which a receiver's validation requires) and counts each type (`stp-config`, `stp-rst`, `stp-tcn`). Its BPDU check covers only the DSAP 0x42 PDUs the library dispatches, UI (either P/F) and I-format; any other Control there (XID, TEST, S-format) is counted (`llc-dsap-0x42-control-0xaf`, say) and not checked. A UI or I-format PDU whose Protocol Identifier is not 0, such as GARP's, is counted (`llc-dsap-0x42-protocol-0x0001`) and not checked rather than failed as a malformed BPDU, and an I-format PDU's BPDU is read after its 2-octet Control, as the library reads it. It also holds each BPDU to what its sender must transmit, which a receiver ignores or tolerates: the Flags bits its type leaves unused 0 (six in a Configuration BPDU, bit 8 in an RST BPDU) and an RST BPDU's Version 1 Length 0. The `llc.pcap` BPDUs now decode to `STP`, and the corpus coverage test requires it; no frame is added, so the corpus counts are unchanged.
32
+ - **The three CDP frames of `llc.pcap` now decode to `CDP`**: the corpus coverage set, the corpus checksum cases (`tests/test_checksum.py`, in the mutation audit's selection) and the fuzz suites (`ALL_PROTOCOLS`, a round-trip strategy, accessor and trailer properties) cover it. No frame was added. `scripts/check_fixtures.py` keeps its own CDP check, which shares no code with the library.
33
+ - **An IEEE 802.3 LLC/SNAP capture joins the corpus: `llc.pcap`, 14 frames (293 across 30 scenarios).** `scripts/capture_fixtures_llc.sh` runs two Linux bridges with STP on in two netns and lldpd 1.0.18 in its own UTS namespace, and captures on a bridge port: five kernel-built 802.1D BPDUs (both bridges' Configuration BPDUs, the root's with TC and with TC + TCA, a 21-byte TCN), CDPv1 and CDPv2 from lldpd (and a CDPv1 frame whose odd-length PDU ends in 0xA9: CDP's checksum adds a final byte from 0x80 as 0xFF00 plus the byte less 1, as lldpd's `frame_checksum()` and Wireshark's packet-cdp.c do, and the frame shows it is not padded as RFC 1071 pads it and that the `- 1` matters; only a test, `test_cdp_odd_byte_is_lldpds_form`, tells that form from adding the byte less 0x100), and six frames the new stdlib-only `scripts/llc_fixture_frames.py` builds and sends through `AF_PACKET` (RFC 1042 ARP padded to 60 bytes and ICMP-in-IPv4, 802.1H ARP, RFC 1042 ARP behind an 802.1Q tag, Novell raw 802.3 IPX, and a type/length value of 1501). It keeps the first frame of each kind and asserts the composition exactly. `scripts/check_fixtures.py` now reads the type/length field as IEEE 802.3 does, after VLAN tags too, holds each Length against its frame (only zero padding may follow a PDU shorter than 46 bytes), checks the LLC Control width, SNAP framing, BPDU and raw IPX lengths, walks the CDP TLVs and verifies the CDP checksum over exactly the PDU, and checks an RFC 1042 or 802.1H payload as its EtherType, IPv4 and ICMP checksums included.
34
+ - **A real LLDP capture joins the corpus: `lldp.pcap`, 6 frames (279 across 29 scenarios).** `scripts/capture_fixtures_lldp.sh` runs two lldpd 1.0.18 agents, LLDP only, in two netns over a veth pair, each configured from its own file before its first LLDPDU: A with its MAC address as Chassis ID, its interface name as Port ID and a 304-byte custom organizationally specific TLV (which needs the 9th Length bit), B with a locally assigned Chassis ID, its MAC address as Port ID and a TTL of 180. It then changes A's Port ID to a locally assigned value at run time, which makes lldpd send a shutdown LLDPDU for the old one, and stops both agents, which send one each. It keeps the first LLDPDU of each kind (sender, Port ID subtype, shutdown or not) and asserts the six exactly: their Chassis ID, Port ID, TTL and TLV type sequence. `scripts/check_fixtures.py` checks LLDPDUs structurally, since LLDP has no checksum: the TLVs must walk to an empty End TLV with only zero bytes after it, the first three must be a well-formed Chassis ID, Port ID and 2-byte TTL that do not recur, and a shutdown LLDPDU must carry nothing else; its ARP and fallback arms move into a `_check_non_ip` helper so `_check_l3` does not grow. `tests/test_lldp.py` holds the checker to failing tampered copies. `LLDP` joins the fuzz suite (`ALL_PROTOCOLS`, a round-trip strategy, an accessor-safety property fed LLDP-shaped buffers, and a padding and trailer property), the truncation and lying-length contract tests and the corpus coverage set; a decode-cost test counts the bytes a 10,000-TLV LLDPDU's walk reads by slicing or copying it whole, and another, with `tracemalloc`, checks that decoding a 1,000-TLV LLDPDU followed by a megabyte after its End TLV, as `bytes` and as a `memoryview`, peaks under half a megabyte of allocation, which a copy of the rest of the buffer at any TLV, over a megabyte, would exceed. Neither bounds per-TLV quadratic work that leaves no large allocation behind, such as rebuilding the TLV list at every TLV.
35
+ - **A captured MPLS fixture joins the corpus: `mpls.pcap`, 8 frames (273 across 28 scenarios).** The capture host's kernel has `CONFIG_MPLS_ROUTING` off, so `scripts/capture_fixtures_mpls.sh` has the kernel send two ICMP echo requests and two UDP datagrams over a veth pair between two netns, offload off, captures them inbound, and then `scripts/mpls_fixture_packets.py` (stdlib only) wraps those datagrams in script-built Ethernet headers and label stacks and sends them through an `AF_PACKET` socket: one label, two labels, three labels with distinct traffic classes, an Entropy Label Indicator and entropy label, IPv4 and IPv6 Explicit NULL bottom labels (the IPv6 datagram script-built, since the host has IPv6 disabled; the IPv4 Explicit NULL frame zero padded to 60 bytes), EtherType 0x8848 to a multicast MAC, and an Ethernet pseudowire with a control word. The script reads the capture back and fails unless every byte arrived as built, and the capture script fails unless the capture holds exactly those eight frames. A second tcpdump captures the same frames with a 20-byte snap length as `tests/fixtures/mpls/mpls_snaplen.pcap`, outside the corpus globs like `bgp/bgp_session.pcap`, so truncation is tested on captured frames too: six stacks cut after their first entry, and two whole one-entry stacks whose IPv4 header is cut. `scripts/check_fixtures.py` walks each label stack to its bottom entry in its own code and verifies the labelled packet's checksums, IPv4 or IPv6 by the same convention, and the Ethernet frame after the pseudowire's control word; its ARP branch moved into a `_check_arp` helper beside the new `_check_mpls`, so `_check_l3` dispatches the two through a table and stays at radon B. `tests/test_mpls.py` holds the checker to failing a flipped byte in each frame's labelled packet. MPLS joins the fuzz suite (`ALL_PROTOCOLS`, a round-trip strategy, accessor-safety and trailer properties), the truncation and memoryview contract tests and the corpus coverage set.
36
+ - **The `register()` examples no longer use MPLS's EtherType.** `registry.py`, README.md, ARCHITECTURE.md and `docs/CLAIMS.md` §5.5 registered a hypothetical class at 0x8847 as "a protocol this library does not implement", which now raises `RegistryConflictError`; they, and `tests/test_registry.py`'s stand-ins, use 0x88B5 ("IEEE Std 802 - Local Experimental Ethertype" in IANA's registry) and a `Telemetry` class instead, and `tests/test_docs.py` runs each document's example, and the three in `registry.py`'s docstrings, to check each registers on a free key. README's "Why NETProtocols" now names the corpus its figures were measured on, which this release's re-measurement (the first Development entry) makes the 293 frames the corpus holds at 2.6.0.
37
+
10
38
  ## [2.5.0] - 2026-10-25
11
39
 
12
40
  ### Added
@@ -38,11 +38,14 @@ is 80; Markdown is excluded from linting.
38
38
  ```
39
39
  src/netprotocols/
40
40
  _base.py the Protocol base class and the decode contract
41
- _enums.py EtherType / IPProtocol / ARP* registries + display names
41
+ _enums.py EtherType / IPProtocol / LLCSAP / ARP* registries +
42
+ display names
42
43
  checksum.py RFC 1071 internet checksum, SCTP's CRC32c, the
43
44
  Fletcher checksum of OSPF's LSAs, compute()/verify()
45
+ (CDP's odd-byte variant among them)
44
46
  packet.py Packet: an ordered stack of headers, with_checksums()
45
- layer2/ Ethernet, VLAN, ARP
47
+ layer2/ Ethernet, VLAN, ARP, LLC + SNAP (802.2), STP (802.1D),
48
+ LLDP, MPLS, CDP
46
49
  layer3/ IPv4, IPv6, IPv6 extension headers, ICMPv4/v6, IGMP,
47
50
  GRE, IPsec ESP and AH, VRRP, OSPFv2, PIM
48
51
  layer4/ TCP, UDP, SCTP (+ port-based dispatch)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: netprotocols
3
- Version: 2.5.0
3
+ Version: 2.6.0
4
4
  Summary: Low-level implementations of common networking protocols
5
5
  Project-URL: Homepage, https://github.com/EONRaider/NETProtocols
6
6
  Project-URL: Repository, https://github.com/EONRaider/NETProtocols
@@ -76,33 +76,39 @@ Requires Python 3.12+. Fully typed (`py.typed`, mypy strict).
76
76
 
77
77
  ## Why NETProtocols
78
78
 
79
- Measured against `dpkt 1.9.8` and `scapy 2.8.0` on the 265-frame corpus
80
- of real captured traffic (CPython 3.12.3, x86-64 Linux; re-measured for
81
- 2.5.0 on 2026-10-25 — see [docs/CLAIMS.md](docs/CLAIMS.md) for every
82
- number below, its reproduction command, and its caveats):
79
+ Measured against `dpkt 1.9.8` and `scapy 2.8.0` on the 293 frames of
80
+ real captured traffic the corpus held at 2.6.0 (CPython 3.12.3, x86-64
81
+ Linux; re-measured for 2.6.0 on 2026-10-28 — see
82
+ [docs/CLAIMS.md](docs/CLAIMS.md) for every number below, its
83
+ reproduction command, and its caveats):
83
84
 
84
85
  - **3.4× faster than scapy on decode; dpkt is 1.3× faster** —
85
86
  `uv run --group bench python scripts/benchmark.py --compare`. dpkt is
86
- the faster of the two comparators. On the 140 frames the corpus held
87
- at 2.4.0 the two are still within 10% of each other; the
88
- routing captures 2.5.0 added (OSPF, BGP, PIM, RIP, VRRP) are where
89
- dpkt pulls ahead, taking about two thirds of netprotocols' time on
90
- them. The gap moves both ways as this library and dpkt each change,
91
- and this file's job is to say so honestly rather than only when it's
92
- flattering.
93
- - **Decodes further into the stack than dpkt on 79 of those 265
94
- frames** — BGP, RIP, DNS, DHCP and VXLAN-tunnelled payloads that dpkt
95
- leaves as raw bytes, and 802.1Q tags it folds into the Ethernet
96
- header, netprotocols decodes as layers of their own. A throughput
97
- number means little without knowing how much work it bought.
98
- - **4.6× faster than dpkt at re-encoding** (`bytes(header)`,
87
+ the faster of the two comparators. The routing captures 2.5.0 added
88
+ (OSPF, BGP, PIM, RIP, VRRP) are where it pulls ahead, taking about
89
+ two thirds of netprotocols' time on them. On the 140 frames the
90
+ corpus held at 2.4.0 the two are no longer within 10% of each other:
91
+ dpkt is ahead there by a median 1.14× across runs. On the MPLS and
92
+ LLC/SNAP captures 2.6.0 added they are level; dpkt leaves LLDP
93
+ undecoded, so it is about 5× faster on those six frames. Narrowing
94
+ the gap is
95
+ [#250](https://github.com/EONRaider/NETProtocols/issues/250). The gap
96
+ moves both ways as this library and dpkt each change, and this file's
97
+ job is to say so honestly rather than only when it's flattering.
98
+ - **Decodes further into the stack than dpkt on 94 of those 293
99
+ frames** — BGP, RIP, LLDP, DNS, DHCP and VXLAN-tunnelled payloads
100
+ that dpkt leaves as raw bytes, and 802.1Q tags, MPLS label stacks and
101
+ SNAP headers it folds into the Ethernet and LLC headers, netprotocols
102
+ decodes as layers of their own. A throughput number means little
103
+ without knowing how much work it bought.
104
+ - **4.7× faster than dpkt at re-encoding** (`bytes(header)`,
99
105
  `scripts/benchmark_encode.py`) — the other half of "codec" that
100
106
  nobody else benchmarks.
101
- - **Imports in 70 ms; `scapy.all` takes 397+ ms** (`scripts/benchmark_import.py`),
107
+ - **Imports in 90 ms; `scapy.all` takes 436+ ms** (`scripts/benchmark_import.py`),
102
108
  and never touches the host doing it — importing scapy populates live
103
109
  interface and routing tables as a side effect of the import
104
110
  statement.
105
- - **A 176.5 KiB wheel against scapy's 2.63 MiB** — 15.3× smaller.
111
+ - **A 220.5 KiB wheel against scapy's 2.63 MiB** — 12.2× smaller.
106
112
  - **Regression-gated in CI, which — as far as we could establish by
107
113
  auditing ten comparable Python packet libraries' CI configurations
108
114
  (dpkt, scapy, pypacker, construct, pcapkit, dnspython, pyshark,
@@ -164,9 +170,15 @@ ip.proto → error: "IPv4" has no attribute
164
170
 
165
171
  | Layer | Protocol | Class | Notes |
166
172
  |---|---|---|---|
167
- | 2 | Ethernet II | `Ethernet` | IEEE 802.3 |
168
- | 2 | IEEE 802.1Q VLAN tag (802.1ad QinQ) | `VLAN` | 802.1Q-2018 §9.6, PCP/DEI/VID, tag stacking |
173
+ | 2 | Ethernet II / IEEE 802.3 | `Ethernet` | IEEE 802.3; the type/length field dispatches as an EtherType from 0x0600 and as an 802.3 Length (then LLC) up to 1500, which also bounds the walk's later layers |
174
+ | 2 | IEEE 802.1Q VLAN tag (802.1ad QinQ) | `VLAN` | 802.1Q-2018 §9.6, PCP/DEI/VID, tag stacking; the inner type/length field reads as Ethernet's does |
175
+ | 2 | IEEE 802.2 LLC | `LLC` | ISO/IEC 8802-2, DSAP/SSAP and a 1- or 2-octet Control; I-format and UI PDUs dispatch on DSAP (`llc.dsap`); Novell raw 802.3 recognized (`is_raw_ipx`; the other fields are then IPX bytes), never dispatched |
176
+ | 2 | SNAP | `SNAP` | IEEE Std 802, RFC 1042; OUI and PID: OUI 00-00-00 and 00-00-F8 dispatch the PID as an EtherType, any other OUI through `llc.snap` |
177
+ | 2 | LLDP | `LLDP`, `LLDPTLV`, `LLDPChassisID`, `LLDPPortID` | IEEE 802.1AB, EtherType 0x88CC; every TLV through End of LLDPDU, the End TLV's bound leaving padding and an FCS undecoded; Chassis ID and Port ID (subtype and ID) and Time To Live typed, every other TLV raw with its type named |
178
+ | 2 | Spanning Tree BPDU (STP, RSTP) | `STP` | IEEE 802.1D-2004 §9.3, 802.1w; LLC DSAP 0x42 (`llc.dsap`); Configuration (35 octets), RST (36) and Topology Change Notification (4) BPDUs in one class, branching on the BPDU Type; per-type flag accessors (TC, TCA, Proposal, Port Role, Learning, Forwarding, Agreement), the 802.1t priority / system ID extension split, timers raw and in seconds; MST BPDUs decoded only as far as their RST prefix |
179
+ | 2 | CDP | `CDP` | Cisco Discovery Protocol, versions 1 and 2, after SNAP OUI 00-00-0C PID 0x2000 (`llc.snap`); the 4-byte header and the TLVs, kept raw and named from Wireshark's packet-cdp.c, with accessors for Device ID, Addresses, Port ID, Capabilities, Software version, Platform and Management Address; its checksum (RFC 1071 but for an odd final byte) in `compute`/`verify` |
169
180
  | 2 | ARP | `ARP` | RFC 826, IPv4-over-Ethernet binding |
181
+ | 2 | MPLS label stack | `MPLS` | RFC 3032 §2.1, EtherTypes 0x8847 and 0x8848 (and GRE's Protocol Type, RFC 4023); the whole stack as one layer of `MPLSLabel` entries (label, traffic class, S, TTL); no field names the payload, so it dispatches by convention to IPv4 or IPv6 — the bottom label when it is an Explicit NULL, otherwise the first nibble after the stack — and stops at a pseudowire control word |
170
182
  | 3 | IPv4 | `IPv4` | RFC 791, IHL/options aware |
171
183
  | 3 | IPv6 | `IPv6` | RFC 8200 |
172
184
  | 3 | IPv6 Hop-by-Hop Options | `IPv6HopByHopOptions` | RFC 8200 §4.3 |
@@ -427,13 +439,14 @@ implement can join the walk without forking it:
427
439
  from netprotocols import Protocol
428
440
  from netprotocols.registry import register
429
441
 
430
- @register("ethertype", 0x8847)
431
- class MPLS(Protocol):
442
+ @register("ethertype", 0x88B5) # IEEE 802 Local Experimental
443
+ class Telemetry(Protocol):
432
444
  ...
433
445
  ```
434
446
 
435
- The five tables are `ethertype`, `ip.proto`, `ip.proto.v6`, `udp.port`
436
- and `tcp.port`, each named after the wire field it dispatches on. To
447
+ The seven tables are `ethertype`, `llc.dsap`, `llc.snap`, `ip.proto`,
448
+ `ip.proto.v6`, `udp.port` and `tcp.port`, each named after the wire
449
+ field it dispatches on. To
437
450
  change decoding for one call only — DNS on a nonstandard port in one
438
451
  capture — say so per call instead of registering globally:
439
452
 
@@ -466,7 +479,8 @@ raw = bytes(frame) # ready for a raw socket
466
479
  Checksums are computed and verified on request — never silently:
467
480
  `compute()`/`verify()` in `netprotocols.checksum` (the internet
468
481
  checksum, VRRP's with or without its pseudo-header by version, PIM's,
469
- and SCTP's CRC32c via `crc32c()`; OSPF's LSA checksums via
482
+ CDP's with its odd final byte summed as Cisco does, and SCTP's CRC32c
483
+ via `crc32c()`; OSPF's LSA checksums via
470
484
  `lsa_checksum()`/`verify_lsa()`), and
471
485
  `Packet.with_checksums()` to fill a whole stack before sending.
472
486
 
@@ -476,7 +490,7 @@ Every class exposes human-readable properties next to the raw fields:
476
490
  `Ethernet.ethertype_name`, `IPv4.protocol_name`, `IPv4.flags_name`,
477
491
  `TCP.flags_str` (`"SYN ACK"`), `ARP.oper_name`, `ICMPv4.type_name`,
478
492
  and hexadecimal renderings such as `checksum_hex_str`. Unknown values
479
- degrade gracefully (`"0x88cc"`, `"unknown (47)"`) instead of raising.
493
+ degrade gracefully (`"0x88b5"`, `"unknown (47)"`) instead of raising.
480
494
 
481
495
  ## How it works
482
496
 
@@ -512,7 +526,13 @@ and the second — VRRP, RIP, OSPF, PIM and BGP
512
526
  [#245](https://github.com/EONRaider/NETProtocols/pull/245),
513
527
  [#246](https://github.com/EONRaider/NETProtocols/pull/246),
514
528
  [#247](https://github.com/EONRaider/NETProtocols/pull/247),
515
- [#248](https://github.com/EONRaider/NETProtocols/pull/248)) — as 2.5.0
529
+ [#248](https://github.com/EONRaider/NETProtocols/pull/248)) — as 2.5.0,
530
+ and the third — MPLS, LLDP, LLC/SNAP, STP and CDP
531
+ ([#252](https://github.com/EONRaider/NETProtocols/pull/252),
532
+ [#253](https://github.com/EONRaider/NETProtocols/pull/253),
533
+ [#254](https://github.com/EONRaider/NETProtocols/pull/254),
534
+ [#255](https://github.com/EONRaider/NETProtocols/pull/255),
535
+ [#256](https://github.com/EONRaider/NETProtocols/pull/256)) — as 2.6.0
516
536
  (see [CHANGELOG.md](CHANGELOG.md)).
517
537
 
518
538
  The roadmap before it, the post-1.3.0
@@ -539,7 +559,7 @@ released as
539
559
  2.1.0's and 1.3.1/1.4.0's work shipped folded into 2.0.0 and 2.2.0
540
560
  rather than as their own versions, but the roadmap's original "only at
541
561
  a major version bump" release policy wasn't actually held to: 2.0.0,
542
- 2.2.0, 2.2.1, 2.3.0, 2.4.0 and now 2.5.0 are all on PyPI, and the comparative claims above
562
+ 2.2.0, 2.2.1, 2.3.0, 2.4.0, 2.5.0 and now 2.6.0 are all on PyPI, and the comparative claims above
543
563
  draw on the finished tree. The wave before that one — TCP
544
564
  and IPv4 option parsing, ICMP message bodies, NDP, IPv6
545
565
  extension-header TLV options, the GRE checksum arm, a DNS-over-TCP
@@ -552,7 +572,7 @@ Development uses [uv](https://docs.astral.sh/uv/): `uv sync`, then
552
572
  `uv run pytest`, `uv run mypy`, and `uv run ruff check` — all three are
553
573
  enforced by CI; the test suite runs across Python 3.12–3.14, while
554
574
  `mypy` and `ruff check` run on 3.12. The test suite is anchored by a
555
- 265-frame corpus of real captured traffic across 27 scenarios
575
+ 293-frame corpus of real captured traffic across 30 scenarios
556
576
  ([tests/fixtures/MANIFEST.md](tests/fixtures/MANIFEST.md), which says
557
577
  which layers a script built rather than the kernel) plus
558
578
  property-based fuzzing of the decode path. See