netprotocols 2.4.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 (254) hide show
  1. {netprotocols-2.4.0 → netprotocols-2.6.0}/ARCHITECTURE.md +354 -50
  2. {netprotocols-2.4.0 → netprotocols-2.6.0}/CHANGELOG.md +52 -0
  3. {netprotocols-2.4.0 → netprotocols-2.6.0}/CONTRIBUTING.md +12 -7
  4. {netprotocols-2.4.0 → netprotocols-2.6.0}/PKG-INFO +65 -30
  5. {netprotocols-2.4.0 → netprotocols-2.6.0}/README.md +64 -29
  6. {netprotocols-2.4.0 → netprotocols-2.6.0}/SECURITY.md +12 -0
  7. netprotocols-2.6.0/benchmarks/baseline.json +13 -0
  8. {netprotocols-2.4.0 → netprotocols-2.6.0}/docs/CLAIMS.md +361 -36
  9. {netprotocols-2.4.0 → netprotocols-2.6.0}/pyproject.toml +2 -2
  10. netprotocols-2.6.0/scripts/capture_fixtures_bgp.sh +495 -0
  11. netprotocols-2.6.0/scripts/capture_fixtures_llc.sh +348 -0
  12. netprotocols-2.6.0/scripts/capture_fixtures_lldp.sh +313 -0
  13. netprotocols-2.6.0/scripts/capture_fixtures_mpls.sh +259 -0
  14. netprotocols-2.6.0/scripts/capture_fixtures_ospf.sh +348 -0
  15. netprotocols-2.6.0/scripts/capture_fixtures_pim.sh +373 -0
  16. netprotocols-2.6.0/scripts/capture_fixtures_rip.sh +285 -0
  17. netprotocols-2.6.0/scripts/capture_fixtures_vrrp.sh +349 -0
  18. netprotocols-2.6.0/scripts/check_fixtures.py +1497 -0
  19. netprotocols-2.6.0/scripts/llc_fixture_frames.py +189 -0
  20. netprotocols-2.6.0/scripts/mpls_fixture_packets.py +253 -0
  21. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/__init__.py +91 -2
  22. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/_base.py +89 -0
  23. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/_defaults.py +90 -7
  24. netprotocols-2.6.0/src/netprotocols/_enums.py +180 -0
  25. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/_tlv.py +58 -36
  26. netprotocols-2.6.0/src/netprotocols/checksum.py +565 -0
  27. netprotocols-2.6.0/src/netprotocols/layer2/__init__.py +26 -0
  28. netprotocols-2.6.0/src/netprotocols/layer2/cdp.py +642 -0
  29. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer2/ethernet.py +87 -6
  30. netprotocols-2.6.0/src/netprotocols/layer2/llc.py +481 -0
  31. netprotocols-2.6.0/src/netprotocols/layer2/lldp.py +522 -0
  32. netprotocols-2.6.0/src/netprotocols/layer2/mpls.py +392 -0
  33. netprotocols-2.6.0/src/netprotocols/layer2/stp.py +634 -0
  34. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer2/vlan.py +45 -4
  35. netprotocols-2.6.0/src/netprotocols/layer3/__init__.py +76 -0
  36. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer3/ip.py +12 -11
  37. netprotocols-2.6.0/src/netprotocols/layer3/ospf.py +971 -0
  38. netprotocols-2.6.0/src/netprotocols/layer3/pim.py +1025 -0
  39. netprotocols-2.6.0/src/netprotocols/layer3/vrrp.py +377 -0
  40. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer7/__init__.py +14 -0
  41. netprotocols-2.6.0/src/netprotocols/layer7/bgp.py +1162 -0
  42. netprotocols-2.6.0/src/netprotocols/layer7/rip.py +570 -0
  43. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/packet.py +9 -2
  44. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/registry.py +141 -16
  45. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/utils/exceptions.py +14 -11
  46. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/walk.py +87 -25
  47. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/conftest.py +113 -0
  48. netprotocols-2.6.0/tests/fixtures/MANIFEST.md +635 -0
  49. netprotocols-2.6.0/tests/fixtures/bgp/bgp_session.pcap +0 -0
  50. netprotocols-2.6.0/tests/fixtures/bgp.pcap +0 -0
  51. netprotocols-2.6.0/tests/fixtures/llc.pcap +0 -0
  52. netprotocols-2.6.0/tests/fixtures/lldp.pcap +0 -0
  53. netprotocols-2.6.0/tests/fixtures/mpls/mpls_snaplen.pcap +0 -0
  54. netprotocols-2.6.0/tests/fixtures/mpls.pcap +0 -0
  55. netprotocols-2.6.0/tests/fixtures/ospf.pcap +0 -0
  56. netprotocols-2.6.0/tests/fixtures/pim.pcap +0 -0
  57. netprotocols-2.6.0/tests/fixtures/rip.pcap +0 -0
  58. netprotocols-2.6.0/tests/fixtures/staging/bgp/bgp_session.pcap +0 -0
  59. netprotocols-2.6.0/tests/fixtures/staging/bgp.pcap +0 -0
  60. netprotocols-2.6.0/tests/fixtures/staging/llc.pcap +0 -0
  61. netprotocols-2.6.0/tests/fixtures/staging/lldp.pcap +0 -0
  62. netprotocols-2.6.0/tests/fixtures/staging/mpls/mpls_snaplen.pcap +0 -0
  63. netprotocols-2.6.0/tests/fixtures/staging/mpls.pcap +0 -0
  64. netprotocols-2.6.0/tests/fixtures/staging/ospf.pcap +0 -0
  65. netprotocols-2.6.0/tests/fixtures/staging/pim.pcap +0 -0
  66. netprotocols-2.6.0/tests/fixtures/staging/rip.pcap +0 -0
  67. netprotocols-2.6.0/tests/fixtures/staging/vrrp.pcap +0 -0
  68. netprotocols-2.6.0/tests/fixtures/vrrp.pcap +0 -0
  69. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/strategies.py +449 -0
  70. netprotocols-2.6.0/tests/test_bgp.py +1283 -0
  71. netprotocols-2.6.0/tests/test_cdp.py +922 -0
  72. netprotocols-2.6.0/tests/test_checksum.py +1456 -0
  73. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_contract.py +110 -5
  74. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_corpus.py +22 -0
  75. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_docs.py +96 -1
  76. netprotocols-2.6.0/tests/test_enums.py +46 -0
  77. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_ethernet.py +4 -2
  78. netprotocols-2.6.0/tests/test_fuzz.py +1531 -0
  79. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_ipv6_ext.py +6 -1
  80. netprotocols-2.6.0/tests/test_llc.py +812 -0
  81. netprotocols-2.6.0/tests/test_lldp.py +819 -0
  82. netprotocols-2.6.0/tests/test_mpls.py +1012 -0
  83. netprotocols-2.6.0/tests/test_ospf.py +1219 -0
  84. netprotocols-2.6.0/tests/test_pim.py +1273 -0
  85. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_registry.py +160 -12
  86. netprotocols-2.6.0/tests/test_rip.py +767 -0
  87. netprotocols-2.6.0/tests/test_stp.py +1139 -0
  88. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_tlv.py +225 -0
  89. netprotocols-2.6.0/tests/test_vrrp.py +707 -0
  90. {netprotocols-2.4.0 → netprotocols-2.6.0}/uv.lock +1 -1
  91. netprotocols-2.4.0/benchmarks/baseline.json +0 -13
  92. netprotocols-2.4.0/scripts/check_fixtures.py +0 -753
  93. netprotocols-2.4.0/src/netprotocols/_enums.py +0 -103
  94. netprotocols-2.4.0/src/netprotocols/checksum.py +0 -255
  95. netprotocols-2.4.0/src/netprotocols/layer2/__init__.py +0 -5
  96. netprotocols-2.4.0/src/netprotocols/layer3/__init__.py +0 -33
  97. netprotocols-2.4.0/tests/fixtures/MANIFEST.md +0 -260
  98. netprotocols-2.4.0/tests/test_checksum.py +0 -602
  99. netprotocols-2.4.0/tests/test_enums.py +0 -22
  100. netprotocols-2.4.0/tests/test_fuzz.py +0 -646
  101. {netprotocols-2.4.0 → netprotocols-2.6.0}/.claude/skills/steward/SKILL.md +0 -0
  102. {netprotocols-2.4.0 → netprotocols-2.6.0}/.gitattributes +0 -0
  103. {netprotocols-2.4.0 → netprotocols-2.6.0}/.github/CODEOWNERS +0 -0
  104. {netprotocols-2.4.0 → netprotocols-2.6.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  105. {netprotocols-2.4.0 → netprotocols-2.6.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  106. {netprotocols-2.4.0 → netprotocols-2.6.0}/.github/ISSUE_TEMPLATE/protocol_request.yml +0 -0
  107. {netprotocols-2.4.0 → netprotocols-2.6.0}/.github/dependabot.yml +0 -0
  108. {netprotocols-2.4.0 → netprotocols-2.6.0}/.github/pull_request_template.md +0 -0
  109. {netprotocols-2.4.0 → netprotocols-2.6.0}/.github/workflows/ci.yml +0 -0
  110. {netprotocols-2.4.0 → netprotocols-2.6.0}/.github/workflows/codeql.yml +0 -0
  111. {netprotocols-2.4.0 → netprotocols-2.6.0}/.github/workflows/docs-lint.yml +0 -0
  112. {netprotocols-2.4.0 → netprotocols-2.6.0}/.github/workflows/fuzz.yml +0 -0
  113. {netprotocols-2.4.0 → netprotocols-2.6.0}/.github/workflows/mutation.yml +0 -0
  114. {netprotocols-2.4.0 → netprotocols-2.6.0}/.github/workflows/pr-title-lint.yml +0 -0
  115. {netprotocols-2.4.0 → netprotocols-2.6.0}/.github/workflows/release.yml +0 -0
  116. {netprotocols-2.4.0 → netprotocols-2.6.0}/.github/workflows/scorecard.yml +0 -0
  117. {netprotocols-2.4.0 → netprotocols-2.6.0}/.gitignore +0 -0
  118. {netprotocols-2.4.0 → netprotocols-2.6.0}/LICENSE +0 -0
  119. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/_pcap.py +0 -0
  120. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/benchmark.py +0 -0
  121. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/benchmark_encode.py +0 -0
  122. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/benchmark_import.py +0 -0
  123. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/capture_fixtures.sh +0 -0
  124. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/capture_fixtures_dhcp.sh +0 -0
  125. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/capture_fixtures_dns_tcp.sh +0 -0
  126. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/capture_fixtures_gre.sh +0 -0
  127. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/capture_fixtures_http.sh +0 -0
  128. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/capture_fixtures_igmp.sh +0 -0
  129. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/capture_fixtures_ipip.sh +0 -0
  130. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/capture_fixtures_ipsec.sh +0 -0
  131. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/capture_fixtures_sctp.sh +0 -0
  132. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/capture_fixtures_supplement.sh +0 -0
  133. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/capture_fixtures_vlan.sh +0 -0
  134. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/capture_fixtures_vxlan.sh +0 -0
  135. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/check_mutmut_survivors.py +0 -0
  136. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/extract_http_streams.py +0 -0
  137. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/ipsec_fixture_packets.py +0 -0
  138. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/mutmut_survivors.txt +0 -0
  139. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/pyodide/check_in_pyodide.py +0 -0
  140. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/pyodide/package-lock.json +0 -0
  141. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/pyodide/package.json +0 -0
  142. {netprotocols-2.4.0 → netprotocols-2.6.0}/scripts/pyodide/run_in_pyodide.mjs +0 -0
  143. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/flow.py +0 -0
  144. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer2/arp.py +0 -0
  145. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer3/ah.py +0 -0
  146. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer3/esp.py +0 -0
  147. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer3/gre.py +0 -0
  148. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer3/icmp.py +0 -0
  149. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer3/igmp.py +0 -0
  150. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer3/ipv6_ext.py +0 -0
  151. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer4/__init__.py +0 -0
  152. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer4/_ports.py +0 -0
  153. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer4/sctp.py +0 -0
  154. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer4/tcp.py +0 -0
  155. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer4/udp.py +0 -0
  156. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer7/dhcp.py +0 -0
  157. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer7/dns.py +0 -0
  158. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer7/http.py +0 -0
  159. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/layer7/vxlan.py +0 -0
  160. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/pcap.py +0 -0
  161. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/py.typed +0 -0
  162. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/utils/__init__.py +0 -0
  163. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/utils/ipv4.py +0 -0
  164. {netprotocols-2.4.0 → netprotocols-2.6.0}/src/netprotocols/utils/mac.py +0 -0
  165. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/ah.pcap +0 -0
  166. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/arp_exchange.pcap +0 -0
  167. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/dhcp.pcap +0 -0
  168. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/dns_tcp.pcap +0 -0
  169. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/esp.pcap +0 -0
  170. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/gre.pcap +0 -0
  171. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/http/01.request +0 -0
  172. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/http/01.response +0 -0
  173. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/http/02.request +0 -0
  174. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/http/02.response +0 -0
  175. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/http/03.request +0 -0
  176. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/http/03.response +0 -0
  177. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/http/04.request +0 -0
  178. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/http/04.response +0 -0
  179. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/http/05.request +0 -0
  180. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/http/05.response +0 -0
  181. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/http/06.request +0 -0
  182. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/http/06.response +0 -0
  183. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/http/07.request +0 -0
  184. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/http/07.response +0 -0
  185. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/http/http.pcap +0 -0
  186. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/icmpv4_echo_lo.pcap +0 -0
  187. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/icmpv4_external.pcap +0 -0
  188. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/icmpv4_ttl_exceeded.pcap +0 -0
  189. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/icmpv6_echo_lo.pcap +0 -0
  190. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/igmp.pcap +0 -0
  191. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/ipip.pcap +0 -0
  192. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/ipv4_fragments.pcap +0 -0
  193. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/ipv6_fragments.pcap +0 -0
  194. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/ipv6_mld.pcap +0 -0
  195. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/ipv6_ndp_mld.pcap +0 -0
  196. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/sctp.pcap +0 -0
  197. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/ah.pcap +0 -0
  198. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/arp_exchange.pcap +0 -0
  199. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/dhcp.pcap +0 -0
  200. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/dns_tcp.pcap +0 -0
  201. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/esp.pcap +0 -0
  202. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/gre.pcap +0 -0
  203. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/http.pcap +0 -0
  204. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/icmpv4_echo_lo.pcap +0 -0
  205. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/icmpv4_external.pcap +0 -0
  206. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/icmpv4_ttl_exceeded.pcap +0 -0
  207. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/icmpv6_echo_lo.pcap +0 -0
  208. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/igmp.pcap +0 -0
  209. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/ipip.pcap +0 -0
  210. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/ipv4_fragments.pcap +0 -0
  211. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/ipv6_fragments.pcap +0 -0
  212. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/ipv6_mld.pcap +0 -0
  213. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/ipv6_ndp_mld.pcap +0 -0
  214. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/sctp.pcap +0 -0
  215. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/tcp_http.pcap +0 -0
  216. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/tcp_https.pcap +0 -0
  217. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/udp_dns.pcap +0 -0
  218. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/vlan_icmp.pcap +0 -0
  219. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/staging/vxlan.pcap +0 -0
  220. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/tcp_http.pcap +0 -0
  221. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/tcp_https.pcap +0 -0
  222. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/udp_dns.pcap +0 -0
  223. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/vlan_icmp.pcap +0 -0
  224. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fixtures/vxlan.pcap +0 -0
  225. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fuzz/fuzz_decode_frame.py +0 -0
  226. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/fuzz/fuzz_http_decode.py +0 -0
  227. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_ah.py +0 -0
  228. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_arp.py +0 -0
  229. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_dhcp.py +0 -0
  230. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_diagnostics.py +0 -0
  231. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_dns.py +0 -0
  232. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_esp.py +0 -0
  233. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_exports.py +0 -0
  234. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_extract_http_streams.py +0 -0
  235. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_flow.py +0 -0
  236. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_gre.py +0 -0
  237. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_http.py +0 -0
  238. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_icmp.py +0 -0
  239. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_igmp.py +0 -0
  240. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_ip.py +0 -0
  241. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_ipip.py +0 -0
  242. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_mutmut_gate.py +0 -0
  243. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_packet.py +0 -0
  244. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_pattern_matching.py +0 -0
  245. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_pcap.py +0 -0
  246. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_sctp.py +0 -0
  247. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_tcp.py +0 -0
  248. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_udp.py +0 -0
  249. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_utils.py +0 -0
  250. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_version.py +0 -0
  251. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_vlan.py +0 -0
  252. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_vxlan.py +0 -0
  253. {netprotocols-2.4.0 → netprotocols-2.6.0}/tests/test_walk.py +0 -0
  254. {netprotocols-2.4.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*
@@ -151,7 +154,7 @@ the same three rules (spelled out in [`_base.py`](src/netprotocols/_base.py)):
151
154
  how many bytes this header consumed — a *property*, so IPv4 answers
152
155
  `ihl * 4` and TCP answers `data_offset * 4`.
153
156
 
154
- **Two protocols vary this.** The first is text framing. `HTTP` has no
157
+ **Three protocols vary this.** The first is text framing. `HTTP` has no
155
158
  fixed portion to unpack: `decode()` scans for the empty line that ends
156
159
  the head, then for the end the head's framing gives the body, so rule 1
157
160
  is a scan rather than an unpack and `header_len` covers the whole
@@ -185,6 +188,113 @@ read as a chunk header) parses as a phantom chunk, so `verify()` is the
185
188
  reliable signal. See
186
189
  [`layer4/sctp.py`](src/netprotocols/layer4/sctp.py)'s module docstring.
187
190
 
191
+ The third is `PIM`. It has no length either, and bounds itself by its
192
+ own structure:
193
+ a Join/Prune, Register-Stop or Assert ends where its counts and
194
+ encoded addresses say, so anything after it is left undecoded, and a
195
+ Hello, whose options run to the end of the message, leaves a run of
196
+ zero bytes where the next option would start, as SCTP leaves one where
197
+ a chunk would. A Register's encapsulated packet runs to the end of the
198
+ buffer, which its checksum, over the first 8 bytes only, does not
199
+ cover. Past the 4-byte header it never raises either: a body its
200
+ parser cannot read is kept whole, for `PIM.message` to report. See
201
+ [`layer3/pim.py`](src/netprotocols/layer3/pim.py)'s module docstring.
202
+
203
+ `RIP` has the same problem one layer up: its 20-byte entries fill the
204
+ rest of the UDP datagram, and the walk does not trim to UDP's length
205
+ either. It keeps the whole buffer after its 4-byte header, as ICMPv4
206
+ and IGMP do, leaving no trailing bytes, so `bytes(RIP.decode(x)) == x`.
207
+ Its accessors read whole entries, up to the RFC 4822 authentication
208
+ trailer that the first entry's Packet Length locates (when the bytes
209
+ there start as a trailer does, `0xFFFF 0x0001`), and report what
210
+ is left, fewer than 20 bytes, as `remainder`: an entry a snap length
211
+ cut, or a trailer such as an Ethernet FCS, which only the UDP length
212
+ could tell apart. Past the header neither `decode()` nor any accessor
213
+ raises. Ethernet's zero padding reaches only a message with no entries,
214
+ since one entry already fills the minimum frame. See
215
+ [`layer7/rip.py`](src/netprotocols/layer7/rip.py)'s module docstring.
216
+
217
+ `VRRP` has no length field either, but needs no walk: its version and
218
+ address count give its exact length (8 bytes, the addresses, and
219
+ VRRPv2's 8 bytes of authentication data), so it obeys rule 3 like a
220
+ header with a length field and leaves Ethernet's padding undecoded. It
221
+ must: a VRRPv3 advert for one IPv4 address is 12 bytes, padded on the
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.
297
+
188
298
  Decoding accepts `bytes` or `memoryview`. Views are a decode-time
189
299
  transient only: every stored field is a scalar or materialized
190
300
  `bytes`, so no decoded object ever aliases the buffer it came from.
@@ -216,15 +326,19 @@ except ProtocolError as e:
216
326
  print(e.protocol, e.field, e.offset, e.frame_offset, e.expected, e.actual)
217
327
  ```
218
328
 
219
- `offset` is relative to whatever buffer the error was found in: the
220
- `data` argument of a `decode()` call when `field` is `None` or names a
221
- fixed-header value, or the attribute `field` names when it's a raw
222
- `bytes` field parsed on demand (`options`, `body`, `sections`) — a TCP
223
- option error's `offset` is relative to `header.options`, not to the
224
- frame. `decode_frame` is the only code holding the cursor needed to
225
- rebase a layer's own offset to the whole frame, so it does — the
226
- result lands in `frame_offset`, left `None` by a bare
227
- `SomeClass.decode()` call with no frame to rebase against. A
329
+ `offset` is relative to whatever buffer the error was found in. An
330
+ error raised by `decode()` is relative to that call's `data` argument,
331
+ whatever `field` names: OSPF's framing errors name `field="body"` (or
332
+ `"auth_trailer"`) and still count from the start of the OSPF header,
333
+ which is what lets `decode_frame` rebase them. An error raised later,
334
+ by an accessor that parses a raw `bytes` attribute on demand
335
+ (`options`, `body`, `sections`, `chunks`), is relative to that
336
+ attribute — a TCP option error's `offset` is relative to
337
+ `header.options`, an ICMPv6 NDP option's or an IGMPv3 group record's
338
+ to `body`, not to the frame. `decode_frame` is the only code holding
339
+ the cursor needed to rebase a layer's own offset to the whole frame,
340
+ so it does — the result lands in `frame_offset`, left `None` by a
341
+ bare `SomeClass.decode()` call with no frame to rebase against. A
228
342
  `__post_init__` validation error (values, not bytes) leaves `offset`
229
343
  `None` too; there is nothing to be positioned in. `HTTP` is the
230
344
  exception on both counts: its `field` names a grammar element
@@ -249,24 +363,66 @@ unless the Security Association uses a confidentiality-free transform
249
363
  (e.g. NULL, RFC 2410, or AES-GMAC, RFC 4543), and at an offset only
250
364
  the SA determines, so the decoder never reads it).
251
365
 
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,
387
+ [`_base.py`](src/netprotocols/_base.py)'s *Decode context in
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*.
392
+
252
393
  ```mermaid
253
394
  flowchart LR
254
395
  E[Ethernet] -->|0x0806| A[ARP]
255
396
  E -->|0x0800| I4[IPv4]
256
397
  E -->|0x86DD| I6[IPv6]
257
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]
258
406
  V -->|0x8100, 0x88A8, 0x9100| V
259
407
  V -->|0x0806| A
260
408
  V -->|0x0800| I4
261
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
262
416
  I4 -->|1| IC4[ICMPv4]
263
417
  I4 -->|6| T[TCP]
264
418
  I4 -->|17| U[UDP]
265
419
  I4 -->|132| S[SCTP]
420
+ I4 -->|103| P[PIM]
266
421
  I6 -->|58| IC6[ICMPv6]
267
422
  I6 -->|6| T
268
423
  I6 -->|17| U
269
424
  I6 -->|132| S
425
+ I6 -->|103| P
270
426
  I4 -->|4| I4
271
427
  I4 -->|41| I6
272
428
  I6 -->|4| I4
@@ -290,8 +446,15 @@ flowchart LR
290
446
  AH6 -->|6| T
291
447
  AH6 -->|17| U
292
448
  AH6 -->|50| ESP
449
+ I4 -->|89| OSPF[OSPFv2]
450
+ I4 -->|112| VR[VRRP]
451
+ I6 -->|112| VR6[VRRPv6]
452
+ X -->|112| VR6
293
453
  U -->|4789| VX[VXLAN]
294
454
  VX -->|always| E
455
+ U -->|520| R[RIP]
456
+ T -->|179| BGP[BGP message]
457
+ BGP -->|another message in the segment| BGP
295
458
  ```
296
459
 
297
460
  IPv6 extension headers (Hop-by-Hop Options, Routing, Fragment,
@@ -306,12 +469,15 @@ offset rule applies to fragmented IPv4.
306
469
 
307
470
  IPsec's AH chains the same way, through its own `next_header`, onward
308
471
  to anything the IP header before it could have carried (the diagram
309
- draws only the transport, ESP and extension-header edges). It is the
310
- one header that comes as a pair for that reason: `AH` after IPv4 and
311
- `AHv6` after IPv6 have one wire format but dispatch through their own
312
- family's table, so IPv4 → AH → `0` stops rather than conjuring a
313
- Hop-by-Hop layer, while IPv6 → AH → Destination Options — an order
314
- RFC 8200 §4.1 permits — keeps decoding. ESP, the other IPsec header,
472
+ draws only the transport, ESP and extension-header edges). It comes as
473
+ a pair for that reason: `AH` after IPv4 and `AHv6` after IPv6 have one
474
+ wire format but dispatch through their own family's table, so IPv4 →
475
+ AH → `0` stops rather than conjuring a Hop-by-Hop layer, while IPv6 →
476
+ AH → Destination Options — an order RFC 8200 §4.1 permits — keeps
477
+ decoding. VRRP is the other pair, for a different reason: it ends the
478
+ walk, but its list of virtual addresses is 4 bytes an entry after IPv4
479
+ and 16 after IPv6, which nothing in the VRRP header says, so `VRRP`
480
+ reads the one and its subclass `VRRPv6` the other. ESP, the other IPsec header,
315
481
  ends the walk: what it carries is named only inside its protected
316
482
  trailer.
317
483
 
@@ -325,13 +491,45 @@ bitfields — `pcp`, `dei`, `vid` — as dataclass fields validated in
325
491
  property (the same pattern `IPv6` uses for `version` /
326
492
  `traffic_class` / `flow_label`).
327
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
+
328
521
  The numbers on the arrows are the wire values — EtherTypes out of
329
- 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
330
524
  [`_enums.py`](src/netprotocols/_enums.py) as `EtherType` and
331
525
  `IPProtocol`. IPv4 and IPv6 share one registry because the number
332
526
  space is shared (1 is ICMPv4, 58 is ICMPv6, 6 is TCP, 17 is UDP, 132
333
- is SCTP — no collisions; 51 is AH in both, decoded by a class of each
334
- family's own).
527
+ is SCTP, 103 is PIM — no collisions; 51 is AH and 112 is VRRP in
528
+ both, each decoded by a class of each family's own). 89 is the
529
+ exception: it is OSPF in both families, but OSPFv2 (RFC 2328) after
530
+ IPv4 and OSPFv3 (RFC 5340), a different wire format, after IPv6, and
531
+ this library decodes only OSPFv2, so `OSPF` is reached from IPv4
532
+ alone.
335
533
 
336
534
  A complete frame walk is a five-line loop — which is exactly why it
337
535
  ships as [`decode_frame()`](src/netprotocols/walk.py) rather than as
@@ -344,11 +542,13 @@ packet = decode_frame(frame) # Packet(Ethernet(...), IPv4(...), TCP(...
344
542
  Underneath it is the loop the two answers imply:
345
543
 
346
544
  ```python
347
- layers, cursor, protocol = [], 0, start
545
+ layers, cursor, end, protocol = [], 0, len(frame), start
348
546
  while protocol is not None:
349
- header = protocol.decode(frame[cursor:])
547
+ header = protocol.decode(frame[cursor:end])
350
548
  layers.append(header)
351
549
  cursor += header.header_len
550
+ if header._BOUNDS_PAYLOAD: # an 802.3 Length (see above)
551
+ end = _bounded(header, cursor, end)
352
552
  protocol = header.next_protocol(registry)
353
553
  ```
354
554
 
@@ -365,12 +565,14 @@ Wrapping each frame in a `memoryview` internally measured *slower*
365
565
  **Where do those arrows live?** Not in the protocol classes. Every
366
566
  one of them is a row in a dispatch table owned by
367
567
  [`registry.py`](src/netprotocols/registry.py), and `next_protocol()`
368
- 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
369
569
  field they dispatch on:
370
570
 
371
571
  | Table | Dispatches on | Read by |
372
572
  |---|---|---|
373
- | `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` |
374
576
  | `ip.proto` | IPv4 `protocol` | `IPv4`, `AH` |
375
577
  | `ip.proto.v6` | IPv6 `next_header` | `IPv6`, extension headers, `AHv6` |
376
578
  | `udp.port` | UDP well-known port | `UDP` |
@@ -379,7 +581,7 @@ field they dispatch on:
379
581
  `ip.proto.v6` **inherits** `ip.proto`: the two share a number space
380
582
  but not a table, because the four extension headers must be reachable
381
583
  only inside an IPv6 chain. Registering in `ip.proto` reaches both,
382
- except at a key `ip.proto.v6` owns itself (51, below); registering in
584
+ except at a key `ip.proto.v6` owns itself (51 and 112, below); registering in
383
585
  `ip.proto.v6` reaches the v6 chain alone. Inheritance
384
586
  is resolved when you register, not when you look up, so the gating
385
587
  costs nothing on the hot path — an IPv4 packet with `protocol=0`
@@ -387,14 +589,27 @@ cannot conjure a Hop-by-Hop layer because the table it reads has no
387
589
  entry for `0`.
388
590
 
389
591
  A child's own registration also *shadows* the inherited entry, which
390
- is how AH's pair is wired: `AHv6` is registered in `ip.proto.v6` first,
391
- then `AH` in `ip.proto`, and the parent write is not inherited over a
592
+ is how AH's and VRRP's pairs are wired: `AHv6` is registered in
593
+ `ip.proto.v6` first, then `AH` in `ip.proto` (and `VRRPv6`, then
594
+ `VRRP`, likewise), and the parent write is not inherited over a
392
595
  key the child already owns. That works the other way round too: a
393
- third party replacing protocol 51 in `ip.proto` (`override=True`)
596
+ third party replacing protocol 51 or 112 in `ip.proto` (`override=True`)
394
597
  changes IPv4 dispatch only, and must register in `ip.proto.v6` as well
395
598
  to reach IPv6. `_defaults.FAMILY_SPECIFIC_NUMBERS` names the numbers
396
- wired this way, and the tests hold the two tables to differing at
397
- those keys alone.
599
+ wired this way, and the tests hold the two tables to differing only at
600
+ those keys, the IPv6-only numbers and the IPv4-only one below.
601
+
602
+ A child can also own a key with no class. Protocol 89 is OSPFv2 in
603
+ `ip.proto` and would be inherited into `ip.proto.v6`, where it means
604
+ OSPFv3, so every OSPFv3 packet would fail to decode as OSPFv2; instead
605
+ the defaults withhold 89 from `ip.proto.v6` (`_defaults.IPV4_ONLY_NUMBERS`)
606
+ before registering `OSPF` in `ip.proto`. A withheld key is skipped by
607
+ inheritance as an override is, so an IPv6 packet with next header 89
608
+ ends its walk at the IPv6 header, as it did before `OSPF` existed, and
609
+ a later write to `ip.proto` does not reach it; an OSPFv3 decoder
610
+ registered directly in `ip.proto.v6` fills it. `OSPF.decode()` also
611
+ rejects any version but 2, for a caller who sends it OSPFv3 on purpose
612
+ (`start=` or `decode_as`).
398
613
 
399
614
  A row need not name a class of its own. IP-in-IP (protocols 4 and 41)
400
615
  has no header between the outer IP header and the inner one, so its
@@ -405,16 +620,18 @@ therefore take the nearest one before it, as `Packet.with_checksums()`
405
620
  and `Packet.flow_key()` do, not the first.
406
621
 
407
622
  **Extending the walk without forking.** The tables are public. A
408
- protocol this library does not implement — MPLS, a proprietary
409
- 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
410
627
  frame walks like any built-in:
411
628
 
412
629
  ```python
413
630
  from netprotocols import Protocol
414
631
  from netprotocols.registry import register
415
632
 
416
- @register("ethertype", 0x8847)
417
- class MPLS(Protocol):
633
+ @register("ethertype", 0x88B5)
634
+ class Telemetry(Protocol):
418
635
  ...
419
636
  ```
420
637
 
@@ -437,19 +654,23 @@ inside `install()` and run once from `__init__.py` after every
437
654
  protocol class exists. Shared enums stay in the dependency-free
438
655
  `_enums.py`. The layer modules bind their table at import, and every
439
656
  class a header chooses by a field value (EtherType, IP protocol, port)
440
- 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
441
658
  another's class, where there is no field to look up. `layer3/icmp.py`
442
659
  names `IPv4` and `IPv6` to start the walk of the packet an ICMP error
443
- quotes, which is not dispatch, and `layer7/vxlan.py` names `Ethernet`
444
- directly, because a VXLAN payload is always an Ethernet frame. Neither
445
- can form a cycle: `ip.py` and `ethernet.py` import no other layer
446
- 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`.
447
666
 
448
667
  ## Port-based dispatch is best-effort
449
668
 
450
- The `ethertype` and `ip.proto` tables dispatch on a single
451
- authoritative field: it exists to say what follows. The two port
452
- 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.
453
674
  A port is a *guess* — any service may run on any port, and the
454
675
  discriminator is split across the source and destination ports.
455
676
  Dispatch therefore checks the destination port first (a request
@@ -501,6 +722,54 @@ header cannot reject more than it does: RFC 7348 says to ignore its
501
722
  reserved bits, so rejecting those would refuse real traffic, such as
502
723
  the Group Based Policy extension's.
503
724
 
725
+ RIP on UDP 520 rejects the most, because two of its four header bytes
726
+ have values no RIP message carries: `RIP.decode()` requires version 1
727
+ or 2 and a nonzero command, which 510 of the 65,536 two-byte prefixes
728
+ satisfy, so about 99.2% of random datagrams are rejected. Measured the
729
+ same way, the walk stopped for about 99.2% in each case: the 300-byte
730
+ payloads sent to 520, and the 0-600 byte ones unpadded and padded, the
731
+ differences between them within sampling noise; an empty datagram
732
+ padded to the minimum is rejected too, since the padding reads as
733
+ version 0. It goes no further. The obsolete and extension commands
734
+ (TraceOn, TraceOff, the triggered and update messages) decode, and so
735
+ does a RIPv1 message with nonzero must-be-zero fields, which RFC 1058
736
+ tells a receiver to ignore but a decoder should show. Past the header,
737
+ any 20 bytes are an entry.
738
+
739
+ BGP on TCP 179 (RFC 4271) has the strongest guard of any port entry:
740
+ every message opens with a 16-byte Marker of all ones, which random
741
+ bytes match once in 2^128, and then a Length and Type whose bounds
742
+ depend on each other (§6.1). Measured the same way, over 10,000 random
743
+ 300-byte payloads and 10,000 of uniformly random length 0-600, padded
744
+ or not, every walk to port 179 ended with `stopped_by` set and none
745
+ decoded a BGP layer; with the Marker forced to all ones, the Length and
746
+ Type checks alone still rejected 9,998 to 10,000 of 10,000 random
747
+ 300-byte payloads (`random.Random` seeds 0-4). What the port cannot tell the decoder is where a message
748
+ starts: TCP cuts its byte stream where it likes, so a segment can hold
749
+ several messages, the tail of one, or the head of the next. Each
750
+ message decodes as one `BGP` layer, and `BGP.next_protocol()` answers
751
+ `BGP` while another message begins in the same segment. It is handed no
752
+ bytes, so `BGP.decode()` records the answer on the instance, in a field
753
+ outside its comparison and hash (see *Walking a frame* above). A
754
+ segment that ends inside a message raises `TruncatedHeaderError` at
755
+ that message (lax mode keeps the ones before it); one that begins
756
+ inside a message fails the Marker check, or raises
757
+ `TruncatedHeaderError` if it is shorter than a header; a bare ACK, like
758
+ one on port 53, sends an empty payload into the decoder. A segment of many small messages is a deep
759
+ chain, and here `max_depth` bounds correctness, not just cost: the
760
+ default leaves room for 29 messages after Ethernet, IPv4 and TCP, and
761
+ valid segments hold more. A 1448-byte one holds 76 KEEPALIVEs or 30
762
+ one-prefix UPDATEs, and a capture taken with GRO or TSO on, over
763
+ loopback or on a jumbo-frame link sees segments of up to 64 KB, so a
764
+ strict walk raises `MaxDepthExceededError` on valid traffic and a lax
765
+ one stops with `stopped_by` set and `consumed` short of the frame. A
766
+ larger `max_depth` or a second walk,
767
+ `decode_frame(frame[packet.consumed:], start=BGP)`, decodes the rest;
768
+ the busiest segment of the captured session in the corpus, taken with
769
+ offload off, carries 7. Reassembling
770
+ the stream is the caller's, as it is for `HTTP` below: see
771
+ [`layer7/bgp.py`](src/netprotocols/layer7/bgp.py)'s module docstring.
772
+
504
773
  A rejected guess takes the ordinary decode-error path: the chain keeps
505
774
  the layers it did decode and records the failure. An accepted one
506
775
  yields a layer that decoded cleanly and is wrong, and nothing in the
@@ -526,9 +795,12 @@ bytes instead.
526
795
 
527
796
  Every class also works in reverse. Constructors take the friendly
528
797
  forms (string addresses, integer fields) and validate some of them in
529
- `__post_init__`: Ethernet, ARP and IPv4 check address formats (IPv6
530
- and DHCP do not, so a malformed address there fails only in
531
- `bytes()`), and IPv4's IHL and TCP's data offset must agree with
798
+ `__post_init__`: Ethernet, ARP and IPv4 check address formats (IPv6,
799
+ DHCP, VRRP and OSPF do not, so a malformed address there fails only
800
+ in `bytes()`; for OSPF's `router_id`, `area_id` and its records'
801
+ address fields, such as an LSA header's `advertising_router`, that
802
+ is the `OSError` from `socket.inet_pton`), and IPv4's IHL and TCP's
803
+ data offset must agree with
532
804
  `len(options)`. For a valid header, `bytes()` re-emits the exact
533
805
  on-wire form. Constructors do not check everything decode does, so
534
806
  some headers can be built that decode would never produce (see
@@ -562,19 +834,41 @@ verbatim** — exactly what you want when faithfully re-encoding
562
834
  captured traffic. Computing and verifying them is explicit, via
563
835
  `netprotocols.checksum`: `compute()`/`verify()` handle the IPv4 header
564
836
  checksum, ICMPv4, and TCP/UDP/ICMPv6 with their IPv4/IPv6
565
- pseudo-headers, and `Packet.with_checksums(payload)` fills a whole
837
+ pseudo-headers, PIM (the pseudo-header over IPv6 only, a Register's
838
+ first 8 bytes only), and `Packet.with_checksums(payload)` fills a whole
566
839
  stack before sending. (The RFC 768 subtlety lives where it belongs:
567
840
  the `0x0000 → 0xFFFF` substitution applies to UDP alone.) SCTP is the
568
841
  one transport that does not use the internet checksum: its CRC32c
569
842
  (`crc32c()`, RFC 9260 Appendix A) covers the whole packet with no
570
843
  pseudo-header, and goes on the wire least significant byte first, so
571
844
  `compute()` returns it byte-swapped to match the field as decoded.
845
+ VRRP's checksum is the internet checksum of the message alone in
846
+ VRRPv2 and behind the IP pseudo-header in VRRPv3. RFC 5798 §5.2.8
847
+ cites only IPv6's pseudo-header; adding IPv4's too is keepalived's
848
+ default, and `compute()`'s. RFC 9568, which obsoletes RFC 5798, leaves
849
+ it out over IPv4, and `verify()` returns `False` for an advert
850
+ checksummed that way.
572
851
 
573
852
  That is also why `SCTP.decode()` bounds its own extent (see "The decode
574
853
  contract" above): otherwise Ethernet's padding would land inside the
575
854
  CRC's span, and a packet under 46 bytes from a real NIC would fail to
576
855
  verify.
577
856
 
857
+ OSPF carries two checksums. The packet's is the internet checksum of
858
+ the whole packet but its 64-bit Authentication field, with no
859
+ pseudo-header, and `compute()` covers only the packet's own bytes, so
860
+ an RFC 5613 LLS block after it never enters; under cryptographic
861
+ authentication (AuType 2, or 3 from RFC 7474) none is computed (the
862
+ digest protects the packet), so `compute()` raises, `verify()` returns
863
+ `True` and `with_checksums()` leaves the field alone. Each LSA's LS
864
+ Checksum is an ISO 8473 Fletcher checksum of the LSA but its LS Age;
865
+ an LSA is not a layer, so it has functions of its own:
866
+ `lsa_checksum()` generates the field with `fletcher_checksum()`, and
867
+ `verify_lsa()` checks it the way RFC 905 B.4 does, by both running sums
868
+ being zero, so a byte stored as `0x00` where the generator writes
869
+ `0xff` passes; and, since RFC 2328 §12.1.7 says the field cannot be
870
+ zero, it fails a field of `0x0000` even where those sums are zero.
871
+
578
872
  `Packet` is the thin composition layer: it holds an ordered tuple of
579
873
  headers and `bytes(packet)` joins them, ready for a raw socket.
580
874
 
@@ -585,27 +879,37 @@ src/netprotocols/
585
879
  ├── __init__.py public API re-exports, __version__, registry install
586
880
  ├── _base.py Protocol ABC, decode contract, address helpers
587
881
  ├── _tlv.py walk_kind_length_value(): shared TCP/IPv4/IPv6
588
- │ options TLV walker
882
+ │ options and PIM Hello options TLV walker
589
883
  ├── _enums.py EtherType, IPProtocol, ARPOperation (imports nothing)
590
884
  ├── registry.py public dispatch tables: register(), Registry
591
885
  ├── walk.py decode_frame(): the shipped chain walker
592
886
  ├── _defaults.py the built-in decoder map, installed at import
593
887
  ├── packet.py Packet composition, with_checksums(), flow_key()
594
888
  ├── checksum.py RFC 1071 internet_checksum, SCTP's crc32c,
595
- │ compute, verify
889
+ │ compute, verify (CDP's odd-byte sum among them),
890
+ │ OSPF's LSA Fletcher checksum
596
891
  ├── flow.py FlowKey, flow_key(): canonical bidirectional keys
597
892
  ├── pcap.py read_captures()/read_pcap()/read_pcapng(): classic
598
893
  │ pcap and pcapng readers, from bytes not filenames
599
- ├── 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)
600
900
  ├── layer3/ ip.py (IPv4 + IPv6), icmp.py (ICMPv4 + ICMPv6),
601
901
  │ igmp.py, gre.py, esp.py (IPsec ESP),
602
902
  │ ah.py (IPsec AH: AH + AHv6),
903
+ │ ospf.py (OSPFv2: OSPF + its message records),
904
+ │ pim.py (PIM-SM),
905
+ │ vrrp.py (VRRPv2/v3: VRRP + VRRPv6),
603
906
  │ ipv6_ext.py (Hop-by-Hop, Routing, Fragment,
604
907
  │ Destination Options)
605
908
  ├── layer4/ tcp.py, udp.py, sctp.py, _ports.py (port-table
606
909
  │ dispatch)
607
910
  ├── layer7/ dns.py, dhcp.py, http.py (the HTTP/1.1 grammar
608
- │ and the HTTP codec class), vxlan.py
911
+ │ and the HTTP codec class), vxlan.py, rip.py
912
+ │ (RIPv1 + RIPv2), bgp.py (BGP-4 messages)
609
913
  └── utils/ validators (mac.py, ipv4.py), exceptions.py
610
914
  tests/ one file per protocol + test_contract.py
611
915
  (truncation, lying lengths, chain walks),