netprotocols 2.3.0__tar.gz → 2.5.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 (232) hide show
  1. {netprotocols-2.3.0 → netprotocols-2.5.0}/.github/workflows/mutation.yml +13 -0
  2. {netprotocols-2.3.0 → netprotocols-2.5.0}/ARCHITECTURE.md +368 -49
  3. {netprotocols-2.3.0 → netprotocols-2.5.0}/CHANGELOG.md +48 -0
  4. {netprotocols-2.3.0 → netprotocols-2.5.0}/CONTRIBUTING.md +13 -6
  5. {netprotocols-2.3.0 → netprotocols-2.5.0}/PKG-INFO +70 -33
  6. {netprotocols-2.3.0 → netprotocols-2.5.0}/README.md +69 -32
  7. {netprotocols-2.3.0 → netprotocols-2.5.0}/SECURITY.md +4 -0
  8. netprotocols-2.5.0/benchmarks/baseline.json +13 -0
  9. {netprotocols-2.3.0 → netprotocols-2.5.0}/docs/CLAIMS.md +259 -28
  10. {netprotocols-2.3.0 → netprotocols-2.5.0}/pyproject.toml +2 -2
  11. netprotocols-2.5.0/scripts/capture_fixtures_bgp.sh +495 -0
  12. netprotocols-2.5.0/scripts/capture_fixtures_ipip.sh +161 -0
  13. netprotocols-2.5.0/scripts/capture_fixtures_ipsec.sh +124 -0
  14. netprotocols-2.5.0/scripts/capture_fixtures_ospf.sh +348 -0
  15. netprotocols-2.5.0/scripts/capture_fixtures_pim.sh +373 -0
  16. netprotocols-2.5.0/scripts/capture_fixtures_rip.sh +285 -0
  17. netprotocols-2.5.0/scripts/capture_fixtures_sctp.sh +206 -0
  18. netprotocols-2.5.0/scripts/capture_fixtures_vrrp.sh +349 -0
  19. netprotocols-2.5.0/scripts/capture_fixtures_vxlan.sh +224 -0
  20. netprotocols-2.5.0/scripts/check_fixtures.py +1142 -0
  21. netprotocols-2.5.0/scripts/ipsec_fixture_packets.py +418 -0
  22. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/__init__.py +81 -2
  23. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/_base.py +47 -3
  24. netprotocols-2.5.0/src/netprotocols/_defaults.py +186 -0
  25. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/_enums.py +16 -0
  26. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/_tlv.py +58 -36
  27. netprotocols-2.5.0/src/netprotocols/checksum.py +520 -0
  28. netprotocols-2.5.0/src/netprotocols/flow.py +135 -0
  29. netprotocols-2.5.0/src/netprotocols/layer3/__init__.py +76 -0
  30. netprotocols-2.5.0/src/netprotocols/layer3/ah.py +245 -0
  31. netprotocols-2.5.0/src/netprotocols/layer3/esp.py +116 -0
  32. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/layer3/ip.py +12 -7
  33. netprotocols-2.5.0/src/netprotocols/layer3/ospf.py +971 -0
  34. netprotocols-2.5.0/src/netprotocols/layer3/pim.py +1025 -0
  35. netprotocols-2.5.0/src/netprotocols/layer3/vrrp.py +377 -0
  36. netprotocols-2.5.0/src/netprotocols/layer4/__init__.py +5 -0
  37. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/layer4/_ports.py +4 -3
  38. netprotocols-2.5.0/src/netprotocols/layer4/sctp.py +378 -0
  39. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/layer7/__init__.py +16 -0
  40. netprotocols-2.5.0/src/netprotocols/layer7/bgp.py +1162 -0
  41. netprotocols-2.5.0/src/netprotocols/layer7/rip.py +570 -0
  42. netprotocols-2.5.0/src/netprotocols/layer7/vxlan.py +190 -0
  43. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/packet.py +36 -20
  44. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/registry.py +51 -9
  45. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/utils/exceptions.py +14 -11
  46. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/walk.py +23 -5
  47. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/conftest.py +94 -0
  48. netprotocols-2.5.0/tests/fixtures/MANIFEST.md +483 -0
  49. netprotocols-2.5.0/tests/fixtures/ah.pcap +0 -0
  50. netprotocols-2.5.0/tests/fixtures/bgp/bgp_session.pcap +0 -0
  51. netprotocols-2.5.0/tests/fixtures/bgp.pcap +0 -0
  52. netprotocols-2.5.0/tests/fixtures/esp.pcap +0 -0
  53. netprotocols-2.5.0/tests/fixtures/ipip.pcap +0 -0
  54. netprotocols-2.5.0/tests/fixtures/ospf.pcap +0 -0
  55. netprotocols-2.5.0/tests/fixtures/pim.pcap +0 -0
  56. netprotocols-2.5.0/tests/fixtures/rip.pcap +0 -0
  57. netprotocols-2.5.0/tests/fixtures/sctp.pcap +0 -0
  58. netprotocols-2.5.0/tests/fixtures/staging/ah.pcap +0 -0
  59. netprotocols-2.5.0/tests/fixtures/staging/bgp/bgp_session.pcap +0 -0
  60. netprotocols-2.5.0/tests/fixtures/staging/bgp.pcap +0 -0
  61. netprotocols-2.5.0/tests/fixtures/staging/esp.pcap +0 -0
  62. netprotocols-2.5.0/tests/fixtures/staging/ipip.pcap +0 -0
  63. netprotocols-2.5.0/tests/fixtures/staging/ospf.pcap +0 -0
  64. netprotocols-2.5.0/tests/fixtures/staging/pim.pcap +0 -0
  65. netprotocols-2.5.0/tests/fixtures/staging/rip.pcap +0 -0
  66. netprotocols-2.5.0/tests/fixtures/staging/sctp.pcap +0 -0
  67. netprotocols-2.5.0/tests/fixtures/staging/vrrp.pcap +0 -0
  68. netprotocols-2.5.0/tests/fixtures/staging/vxlan.pcap +0 -0
  69. netprotocols-2.5.0/tests/fixtures/vrrp.pcap +0 -0
  70. netprotocols-2.5.0/tests/fixtures/vxlan.pcap +0 -0
  71. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/strategies.py +395 -0
  72. netprotocols-2.5.0/tests/test_ah.py +545 -0
  73. netprotocols-2.5.0/tests/test_bgp.py +1283 -0
  74. netprotocols-2.5.0/tests/test_checksum.py +1287 -0
  75. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_contract.py +110 -0
  76. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_corpus.py +27 -5
  77. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_diagnostics.py +12 -0
  78. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_docs.py +36 -4
  79. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_enums.py +11 -0
  80. netprotocols-2.5.0/tests/test_esp.py +307 -0
  81. netprotocols-2.5.0/tests/test_flow.py +356 -0
  82. netprotocols-2.5.0/tests/test_fuzz.py +1122 -0
  83. netprotocols-2.5.0/tests/test_ipip.py +250 -0
  84. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_ipv6_ext.py +17 -3
  85. netprotocols-2.5.0/tests/test_ospf.py +1219 -0
  86. netprotocols-2.5.0/tests/test_pim.py +1273 -0
  87. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_registry.py +87 -13
  88. netprotocols-2.5.0/tests/test_rip.py +767 -0
  89. netprotocols-2.5.0/tests/test_sctp.py +573 -0
  90. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_tlv.py +225 -0
  91. netprotocols-2.5.0/tests/test_vrrp.py +707 -0
  92. netprotocols-2.5.0/tests/test_vxlan.py +562 -0
  93. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_workflows.py +27 -0
  94. {netprotocols-2.3.0 → netprotocols-2.5.0}/uv.lock +1 -1
  95. netprotocols-2.3.0/benchmarks/baseline.json +0 -13
  96. netprotocols-2.3.0/scripts/check_fixtures.py +0 -332
  97. netprotocols-2.3.0/src/netprotocols/_defaults.py +0 -105
  98. netprotocols-2.3.0/src/netprotocols/checksum.py +0 -189
  99. netprotocols-2.3.0/src/netprotocols/flow.py +0 -96
  100. netprotocols-2.3.0/src/netprotocols/layer3/__init__.py +0 -28
  101. netprotocols-2.3.0/src/netprotocols/layer4/__init__.py +0 -4
  102. netprotocols-2.3.0/tests/fixtures/MANIFEST.md +0 -128
  103. netprotocols-2.3.0/tests/test_checksum.py +0 -424
  104. netprotocols-2.3.0/tests/test_flow.py +0 -174
  105. netprotocols-2.3.0/tests/test_fuzz.py +0 -564
  106. {netprotocols-2.3.0 → netprotocols-2.5.0}/.claude/skills/steward/SKILL.md +0 -0
  107. {netprotocols-2.3.0 → netprotocols-2.5.0}/.gitattributes +0 -0
  108. {netprotocols-2.3.0 → netprotocols-2.5.0}/.github/CODEOWNERS +0 -0
  109. {netprotocols-2.3.0 → netprotocols-2.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  110. {netprotocols-2.3.0 → netprotocols-2.5.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  111. {netprotocols-2.3.0 → netprotocols-2.5.0}/.github/ISSUE_TEMPLATE/protocol_request.yml +0 -0
  112. {netprotocols-2.3.0 → netprotocols-2.5.0}/.github/dependabot.yml +0 -0
  113. {netprotocols-2.3.0 → netprotocols-2.5.0}/.github/pull_request_template.md +0 -0
  114. {netprotocols-2.3.0 → netprotocols-2.5.0}/.github/workflows/ci.yml +0 -0
  115. {netprotocols-2.3.0 → netprotocols-2.5.0}/.github/workflows/codeql.yml +0 -0
  116. {netprotocols-2.3.0 → netprotocols-2.5.0}/.github/workflows/docs-lint.yml +0 -0
  117. {netprotocols-2.3.0 → netprotocols-2.5.0}/.github/workflows/fuzz.yml +0 -0
  118. {netprotocols-2.3.0 → netprotocols-2.5.0}/.github/workflows/pr-title-lint.yml +0 -0
  119. {netprotocols-2.3.0 → netprotocols-2.5.0}/.github/workflows/release.yml +0 -0
  120. {netprotocols-2.3.0 → netprotocols-2.5.0}/.github/workflows/scorecard.yml +0 -0
  121. {netprotocols-2.3.0 → netprotocols-2.5.0}/.gitignore +0 -0
  122. {netprotocols-2.3.0 → netprotocols-2.5.0}/LICENSE +0 -0
  123. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/_pcap.py +0 -0
  124. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/benchmark.py +0 -0
  125. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/benchmark_encode.py +0 -0
  126. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/benchmark_import.py +0 -0
  127. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/capture_fixtures.sh +0 -0
  128. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/capture_fixtures_dhcp.sh +0 -0
  129. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/capture_fixtures_dns_tcp.sh +0 -0
  130. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/capture_fixtures_gre.sh +0 -0
  131. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/capture_fixtures_http.sh +0 -0
  132. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/capture_fixtures_igmp.sh +0 -0
  133. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/capture_fixtures_supplement.sh +0 -0
  134. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/capture_fixtures_vlan.sh +0 -0
  135. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/check_mutmut_survivors.py +0 -0
  136. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/extract_http_streams.py +0 -0
  137. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/mutmut_survivors.txt +0 -0
  138. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/pyodide/check_in_pyodide.py +0 -0
  139. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/pyodide/package-lock.json +0 -0
  140. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/pyodide/package.json +0 -0
  141. {netprotocols-2.3.0 → netprotocols-2.5.0}/scripts/pyodide/run_in_pyodide.mjs +0 -0
  142. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/layer2/__init__.py +0 -0
  143. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/layer2/arp.py +0 -0
  144. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/layer2/ethernet.py +0 -0
  145. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/layer2/vlan.py +0 -0
  146. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/layer3/gre.py +0 -0
  147. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/layer3/icmp.py +0 -0
  148. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/layer3/igmp.py +0 -0
  149. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/layer3/ipv6_ext.py +0 -0
  150. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/layer4/tcp.py +0 -0
  151. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/layer4/udp.py +0 -0
  152. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/layer7/dhcp.py +0 -0
  153. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/layer7/dns.py +0 -0
  154. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/layer7/http.py +0 -0
  155. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/pcap.py +0 -0
  156. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/py.typed +0 -0
  157. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/utils/__init__.py +0 -0
  158. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/utils/ipv4.py +0 -0
  159. {netprotocols-2.3.0 → netprotocols-2.5.0}/src/netprotocols/utils/mac.py +0 -0
  160. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/arp_exchange.pcap +0 -0
  161. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/dhcp.pcap +0 -0
  162. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/dns_tcp.pcap +0 -0
  163. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/gre.pcap +0 -0
  164. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/http/01.request +0 -0
  165. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/http/01.response +0 -0
  166. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/http/02.request +0 -0
  167. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/http/02.response +0 -0
  168. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/http/03.request +0 -0
  169. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/http/03.response +0 -0
  170. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/http/04.request +0 -0
  171. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/http/04.response +0 -0
  172. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/http/05.request +0 -0
  173. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/http/05.response +0 -0
  174. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/http/06.request +0 -0
  175. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/http/06.response +0 -0
  176. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/http/07.request +0 -0
  177. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/http/07.response +0 -0
  178. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/http/http.pcap +0 -0
  179. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/icmpv4_echo_lo.pcap +0 -0
  180. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/icmpv4_external.pcap +0 -0
  181. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/icmpv4_ttl_exceeded.pcap +0 -0
  182. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/icmpv6_echo_lo.pcap +0 -0
  183. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/igmp.pcap +0 -0
  184. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/ipv4_fragments.pcap +0 -0
  185. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/ipv6_fragments.pcap +0 -0
  186. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/ipv6_mld.pcap +0 -0
  187. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/ipv6_ndp_mld.pcap +0 -0
  188. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/arp_exchange.pcap +0 -0
  189. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/dhcp.pcap +0 -0
  190. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/dns_tcp.pcap +0 -0
  191. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/gre.pcap +0 -0
  192. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/http.pcap +0 -0
  193. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/icmpv4_echo_lo.pcap +0 -0
  194. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/icmpv4_external.pcap +0 -0
  195. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/icmpv4_ttl_exceeded.pcap +0 -0
  196. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/icmpv6_echo_lo.pcap +0 -0
  197. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/igmp.pcap +0 -0
  198. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/ipv4_fragments.pcap +0 -0
  199. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/ipv6_fragments.pcap +0 -0
  200. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/ipv6_mld.pcap +0 -0
  201. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/ipv6_ndp_mld.pcap +0 -0
  202. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/tcp_http.pcap +0 -0
  203. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/tcp_https.pcap +0 -0
  204. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/udp_dns.pcap +0 -0
  205. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/staging/vlan_icmp.pcap +0 -0
  206. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/tcp_http.pcap +0 -0
  207. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/tcp_https.pcap +0 -0
  208. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/udp_dns.pcap +0 -0
  209. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fixtures/vlan_icmp.pcap +0 -0
  210. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fuzz/fuzz_decode_frame.py +0 -0
  211. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/fuzz/fuzz_http_decode.py +0 -0
  212. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_arp.py +0 -0
  213. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_dhcp.py +0 -0
  214. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_dns.py +0 -0
  215. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_ethernet.py +0 -0
  216. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_exports.py +0 -0
  217. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_extract_http_streams.py +0 -0
  218. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_gre.py +0 -0
  219. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_http.py +0 -0
  220. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_icmp.py +0 -0
  221. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_igmp.py +0 -0
  222. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_ip.py +0 -0
  223. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_mutmut_gate.py +0 -0
  224. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_packet.py +0 -0
  225. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_pattern_matching.py +0 -0
  226. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_pcap.py +0 -0
  227. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_tcp.py +0 -0
  228. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_udp.py +0 -0
  229. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_utils.py +0 -0
  230. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_version.py +0 -0
  231. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_vlan.py +0 -0
  232. {netprotocols-2.3.0 → netprotocols-2.5.0}/tests/test_walk.py +0 -0
@@ -49,6 +49,19 @@ jobs:
49
49
  key: mutmut-nightly-${{ github.run_id }}
50
50
  restore-keys: |
51
51
  mutmut-nightly-
52
+ # The restored cache also carries mutants/mutmut-stats.json, the
53
+ # map from each function to the tests that call it. When that map
54
+ # exists, mutmut 3.8 re-traces only tests it has never seen, so a
55
+ # function added since the cached run (a new helper called only
56
+ # by existing tests) maps to no tests and every one of its
57
+ # mutants reads "no tests"; so does an existing test edited to
58
+ # call a function it didn't before. Without the map, mutmut
59
+ # traces the configured test selection in full, which costs one
60
+ # traced pytest pass. Nothing else is lost: with the positional
61
+ # filters below, mutmut re-runs every in-scope mutant regardless
62
+ # of a cached result.
63
+ - name: Drop the cached function-to-test map
64
+ run: rm -f mutants/mutmut-stats.json
52
65
  - name: Install the project with dev dependencies
53
66
  run: uv sync --group dev --frozen
54
67
  # File-level scope (src/netprotocols/checksum.py, _tlv.py,
@@ -151,16 +151,72 @@ the same three rules (spelled out in [`_base.py`](src/netprotocols/_base.py)):
151
151
  how many bytes this header consumed — a *property*, so IPv4 answers
152
152
  `ihl * 4` and TCP answers `data_offset * 4`.
153
153
 
154
- **Text framing is the one variation.** `HTTP` has no fixed portion to
155
- unpack: `decode()` scans for the empty line that ends the head, then
156
- for the end the head's framing gives the body, so rule 1 is a scan
157
- rather than an unpack and `header_len` covers the whole message, not a
158
- `_struct` size. Its `decode()` also takes keyword-only context the
159
- bytes cannot carry — `request_method` (a response to `HEAD`, or a 2xx
160
- to `CONNECT`, has no body whatever its headers say) and `closed` (the
161
- buffer ends where the connection did) — and `request_method` is stored,
162
- but normalized: kept only when it changes the framing, so two messages
163
- with equal bytes and equal framing compare equal.
154
+ **Three protocols vary this.** The first is text framing. `HTTP` has no
155
+ fixed portion to unpack: `decode()` scans for the empty line that ends
156
+ the head, then for the end the head's framing gives the body, so rule 1
157
+ is a scan rather than an unpack and `header_len` covers the whole
158
+ message, not a `_struct` size. Its `decode()` also takes keyword-only
159
+ context the bytes cannot carry — `request_method` (a response to `HEAD`,
160
+ or a 2xx to `CONNECT`, has no body whatever its headers say) and
161
+ `closed` (the buffer ends where the connection did) — and
162
+ `request_method` is stored, but normalized: kept only when it changes
163
+ the framing, so two messages with equal bytes and equal framing compare
164
+ equal.
165
+
166
+ The second is `SCTP`. Like ICMPv4, ICMPv6 and IGMP it has no length of
167
+ its own: the packet is the whole IP payload, and the walk does not trim
168
+ a frame to the IP header's declared length. Those three keep the whole
169
+ buffer, Ethernet's zero padding included: ICMPv4's and IGMP's internet
170
+ checksum is unchanged by appended zero bytes, and an IPv6 frame carrying
171
+ ICMPv6 is never short enough to be padded. SCTP's CRC32c does change
172
+ when zero bytes are appended, so `decode()` walks the chunk headers to
173
+ find where the packet ends and leaves a run of zero bytes where the next
174
+ chunk would start undecoded, since that cannot begin a chunk and is what
175
+ an Ethernet sender normally pads a short frame with. Rule 3 holds for
176
+ those zeros; any other trailing bytes become part of the packet. Past
177
+ the 12-byte common header `decode()` never raises, like the three it
178
+ resembles: a remainder the walk cannot parse — a chunk cut by a snap
179
+ length, an IP fragment or an ICMP error's quote, or a nonzero trailer
180
+ such as an Ethernet FCS, which cannot be told from a broken chunk — is
181
+ kept verbatim in `chunks`, the ports and verification tag still decode,
182
+ and `verify()` returns `False`. `parsed_chunks` raises only when the
183
+ remainder does not walk: a trailer that happens to (an FCS whose bytes
184
+ read as a chunk header) parses as a phantom chunk, so `verify()` is the
185
+ reliable signal. See
186
+ [`layer4/sctp.py`](src/netprotocols/layer4/sctp.py)'s module docstring.
187
+
188
+ The third is `PIM`. It has no length either, and bounds itself by its
189
+ own structure:
190
+ a Join/Prune, Register-Stop or Assert ends where its counts and
191
+ encoded addresses say, so anything after it is left undecoded, and a
192
+ Hello, whose options run to the end of the message, leaves a run of
193
+ zero bytes where the next option would start, as SCTP leaves one where
194
+ a chunk would. A Register's encapsulated packet runs to the end of the
195
+ buffer, which its checksum, over the first 8 bytes only, does not
196
+ cover. Past the 4-byte header it never raises either: a body its
197
+ parser cannot read is kept whole, for `PIM.message` to report. See
198
+ [`layer3/pim.py`](src/netprotocols/layer3/pim.py)'s module docstring.
199
+
200
+ `RIP` has the same problem one layer up: its 20-byte entries fill the
201
+ rest of the UDP datagram, and the walk does not trim to UDP's length
202
+ either. It keeps the whole buffer after its 4-byte header, as ICMPv4
203
+ and IGMP do, leaving no trailing bytes, so `bytes(RIP.decode(x)) == x`.
204
+ Its accessors read whole entries, up to the RFC 4822 authentication
205
+ trailer that the first entry's Packet Length locates (when the bytes
206
+ there start as a trailer does, `0xFFFF 0x0001`), and report what
207
+ is left, fewer than 20 bytes, as `remainder`: an entry a snap length
208
+ cut, or a trailer such as an Ethernet FCS, which only the UDP length
209
+ could tell apart. Past the header neither `decode()` nor any accessor
210
+ raises. Ethernet's zero padding reaches only a message with no entries,
211
+ since one entry already fills the minimum frame. See
212
+ [`layer7/rip.py`](src/netprotocols/layer7/rip.py)'s module docstring.
213
+
214
+ `VRRP` has no length field either, but needs no walk: its version and
215
+ address count give its exact length (8 bytes, the addresses, and
216
+ VRRPv2's 8 bytes of authentication data), so it obeys rule 3 like a
217
+ header with a length field and leaves Ethernet's padding undecoded. It
218
+ must: a VRRPv3 advert for one IPv4 address is 12 bytes, padded on the
219
+ wire, and VRRPv3's pseudo-header carries the message's length.
164
220
 
165
221
  Decoding accepts `bytes` or `memoryview`. Views are a decode-time
166
222
  transient only: every stored field is a scalar or materialized
@@ -193,15 +249,19 @@ except ProtocolError as e:
193
249
  print(e.protocol, e.field, e.offset, e.frame_offset, e.expected, e.actual)
194
250
  ```
195
251
 
196
- `offset` is relative to whatever buffer the error was found in: the
197
- `data` argument of a `decode()` call when `field` is `None` or names a
198
- fixed-header value, or the attribute `field` names when it's a raw
199
- `bytes` field parsed on demand (`options`, `body`, `sections`) — a TCP
200
- option error's `offset` is relative to `header.options`, not to the
201
- frame. `decode_frame` is the only code holding the cursor needed to
202
- rebase a layer's own offset to the whole frame, so it does — the
203
- result lands in `frame_offset`, left `None` by a bare
204
- `SomeClass.decode()` call with no frame to rebase against. A
252
+ `offset` is relative to whatever buffer the error was found in. An
253
+ error raised by `decode()` is relative to that call's `data` argument,
254
+ whatever `field` names: OSPF's framing errors name `field="body"` (or
255
+ `"auth_trailer"`) and still count from the start of the OSPF header,
256
+ which is what lets `decode_frame` rebase them. An error raised later,
257
+ by an accessor that parses a raw `bytes` attribute on demand
258
+ (`options`, `body`, `sections`, `chunks`), is relative to that
259
+ attribute — a TCP option error's `offset` is relative to
260
+ `header.options`, an ICMPv6 NDP option's or an IGMPv3 group record's
261
+ to `body`, not to the frame. `decode_frame` is the only code holding
262
+ the cursor needed to rebase a layer's own offset to the whole frame,
263
+ so it does — the result lands in `frame_offset`, left `None` by a
264
+ bare `SomeClass.decode()` call with no frame to rebase against. A
205
265
  `__post_init__` validation error (values, not bytes) leaves `offset`
206
266
  `None` too; there is nothing to be positioned in. `HTTP` is the
207
267
  exception on both counts: its `field` names a grammar element
@@ -219,8 +279,28 @@ holds unchanged.
219
279
  A captured frame is a chain of headers, each one naming what it
220
280
  carries. In this library that fact is typed: every decoded header
221
281
  answers `next_protocol()` with the *class* that decodes its payload,
222
- or `None` when the chain ends (nothing encapsulated, or a protocol
223
- this library doesn't implement yet).
282
+ or `None` when the chain ends (nothing encapsulated, a protocol this
283
+ library doesn't implement yet, or a payload no header names in the
284
+ clear — ESP's Next Header sits in its protected trailer, encrypted
285
+ unless the Security Association uses a confidentiality-free transform
286
+ (e.g. NULL, RFC 2410, or AES-GMAC, RFC 4543), and at an offset only
287
+ the SA determines, so the decoder never reads it).
288
+
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,
301
+ [`_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.
224
304
 
225
305
  ```mermaid
226
306
  flowchart LR
@@ -235,14 +315,45 @@ flowchart LR
235
315
  I4 -->|1| IC4[ICMPv4]
236
316
  I4 -->|6| T[TCP]
237
317
  I4 -->|17| U[UDP]
318
+ I4 -->|132| S[SCTP]
319
+ I4 -->|103| P[PIM]
238
320
  I6 -->|58| IC6[ICMPv6]
239
321
  I6 -->|6| T
240
322
  I6 -->|17| U
323
+ I6 -->|132| S
324
+ I6 -->|103| P
325
+ I4 -->|4| I4
326
+ I4 -->|41| I6
327
+ I6 -->|4| I4
328
+ I6 -->|41| I6
329
+ I4 -->|50| ESP[ESP]
330
+ I6 -->|50| ESP
241
331
  I6 -->|0, 43, 44, 60| X[IPv6 extension headers]
242
332
  X --> X
243
333
  X -->|58| IC6
244
334
  X -->|6| T
245
335
  X -->|17| U
336
+ X -->|132| S
337
+ X -->|50| ESP
338
+ I4 -->|51| AH[AH]
339
+ AH -->|6| T
340
+ AH -->|17| U
341
+ AH -->|50| ESP
342
+ I6 -->|51| AH6[AHv6]
343
+ X -->|51| AH6
344
+ AH6 -->|0, 43, 44, 60| X
345
+ AH6 -->|6| T
346
+ AH6 -->|17| U
347
+ AH6 -->|50| ESP
348
+ I4 -->|89| OSPF[OSPFv2]
349
+ I4 -->|112| VR[VRRP]
350
+ I6 -->|112| VR6[VRRPv6]
351
+ X -->|112| VR6
352
+ U -->|4789| VX[VXLAN]
353
+ VX -->|always| E
354
+ U -->|520| R[RIP]
355
+ T -->|179| BGP[BGP message]
356
+ BGP -->|another message in the segment| BGP
246
357
  ```
247
358
 
248
359
  IPv6 extension headers (Hop-by-Hop Options, Routing, Fragment,
@@ -255,6 +366,20 @@ non-first fragment carries a slice from the middle of the original
255
366
  payload, so no upper-layer header exists at its start. The same
256
367
  offset rule applies to fragmented IPv4.
257
368
 
369
+ IPsec's AH chains the same way, through its own `next_header`, onward
370
+ to anything the IP header before it could have carried (the diagram
371
+ draws only the transport, ESP and extension-header edges). It comes as
372
+ a pair for that reason: `AH` after IPv4 and `AHv6` after IPv6 have one
373
+ wire format but dispatch through their own family's table, so IPv4 →
374
+ AH → `0` stops rather than conjuring a Hop-by-Hop layer, while IPv6 →
375
+ AH → Destination Options — an order RFC 8200 §4.1 permits — keeps
376
+ decoding. VRRP is the other pair, for a different reason: it ends the
377
+ walk, but its list of virtual addresses is 4 bytes an entry after IPv4
378
+ and 16 after IPv6, which nothing in the VRRP header says, so `VRRP`
379
+ reads the one and its subclass `VRRPv6` the other. ESP, the other IPsec header,
380
+ ends the walk: what it carries is named only inside its protected
381
+ trailer.
382
+
258
383
  VLAN tags chain the same way: a tag's `next_protocol` dispatches the
259
384
  inner EtherType through the same registry Ethernet uses, so a QinQ
260
385
  (`0x88A8`) or legacy double-tagged (`0x9100`) frame decodes as one
@@ -269,8 +394,13 @@ The numbers on the arrows are the wire values — EtherTypes out of
269
394
  Ethernet, IP protocol numbers out of IPv4/IPv6 — and they live in
270
395
  [`_enums.py`](src/netprotocols/_enums.py) as `EtherType` and
271
396
  `IPProtocol`. IPv4 and IPv6 share one registry because the number
272
- space is shared (1 is ICMPv4, 58 is ICMPv6, 6 is TCP, 17 is UDP — no
273
- collisions).
397
+ space is shared (1 is ICMPv4, 58 is ICMPv6, 6 is TCP, 17 is UDP, 132
398
+ is SCTP, 103 is PIM — no collisions; 51 is AH and 112 is VRRP in
399
+ both, each decoded by a class of each family's own). 89 is the
400
+ exception: it is OSPF in both families, but OSPFv2 (RFC 2328) after
401
+ IPv4 and OSPFv3 (RFC 5340), a different wire format, after IPv6, and
402
+ this library decodes only OSPFv2, so `OSPF` is reached from IPv4
403
+ alone.
274
404
 
275
405
  A complete frame walk is a five-line loop — which is exactly why it
276
406
  ships as [`decode_frame()`](src/netprotocols/walk.py) rather than as
@@ -310,22 +440,54 @@ field they dispatch on:
310
440
  | Table | Dispatches on | Read by |
311
441
  |---|---|---|
312
442
  | `ethertype` | Ethernet II EtherType | `Ethernet`, `VLAN`, `GRE` |
313
- | `ip.proto` | IPv4 `protocol` | `IPv4` |
314
- | `ip.proto.v6` | IPv6 `next_header` | `IPv6`, extension headers |
443
+ | `ip.proto` | IPv4 `protocol` | `IPv4`, `AH` |
444
+ | `ip.proto.v6` | IPv6 `next_header` | `IPv6`, extension headers, `AHv6` |
315
445
  | `udp.port` | UDP well-known port | `UDP` |
316
446
  | `tcp.port` | TCP well-known port | `TCP` |
317
447
 
318
448
  `ip.proto.v6` **inherits** `ip.proto`: the two share a number space
319
449
  but not a table, because the four extension headers must be reachable
320
- only inside an IPv6 chain. Registering in `ip.proto` reaches both;
321
- registering in `ip.proto.v6` reaches the v6 chain alone. Inheritance
450
+ only inside an IPv6 chain. Registering in `ip.proto` reaches both,
451
+ except at a key `ip.proto.v6` owns itself (51 and 112, below); registering in
452
+ `ip.proto.v6` reaches the v6 chain alone. Inheritance
322
453
  is resolved when you register, not when you look up, so the gating
323
454
  costs nothing on the hot path — an IPv4 packet with `protocol=0`
324
455
  cannot conjure a Hop-by-Hop layer because the table it reads has no
325
456
  entry for `0`.
326
457
 
458
+ A child's own registration also *shadows* the inherited entry, which
459
+ is how AH's and VRRP's pairs are wired: `AHv6` is registered in
460
+ `ip.proto.v6` first, then `AH` in `ip.proto` (and `VRRPv6`, then
461
+ `VRRP`, likewise), and the parent write is not inherited over a
462
+ key the child already owns. That works the other way round too: a
463
+ third party replacing protocol 51 or 112 in `ip.proto` (`override=True`)
464
+ changes IPv4 dispatch only, and must register in `ip.proto.v6` as well
465
+ to reach IPv6. `_defaults.FAMILY_SPECIFIC_NUMBERS` names the numbers
466
+ wired this way, and the tests hold the two tables to differing only at
467
+ those keys, the IPv6-only numbers and the IPv4-only one below.
468
+
469
+ A child can also own a key with no class. Protocol 89 is OSPFv2 in
470
+ `ip.proto` and would be inherited into `ip.proto.v6`, where it means
471
+ OSPFv3, so every OSPFv3 packet would fail to decode as OSPFv2; instead
472
+ the defaults withhold 89 from `ip.proto.v6` (`_defaults.IPV4_ONLY_NUMBERS`)
473
+ before registering `OSPF` in `ip.proto`. A withheld key is skipped by
474
+ inheritance as an override is, so an IPv6 packet with next header 89
475
+ ends its walk at the IPv6 header, as it did before `OSPF` existed, and
476
+ a later write to `ip.proto` does not reach it; an OSPFv3 decoder
477
+ registered directly in `ip.proto.v6` fills it. `OSPF.decode()` also
478
+ rejects any version but 2, for a caller who sends it OSPFv3 on purpose
479
+ (`start=` or `decode_as`).
480
+
481
+ A row need not name a class of its own. IP-in-IP (protocols 4 and 41)
482
+ has no header between the outer IP header and the inner one, so its
483
+ rows in `ip.proto` point straight at `IPv4` and `IPv6`, and a tunneled
484
+ packet walks `IPv4 → IPv4 → …` with no marker layer, as in scapy and
485
+ dpkt. Anything that needs "the" IP header of a transport layer must
486
+ therefore take the nearest one before it, as `Packet.with_checksums()`
487
+ and `Packet.flow_key()` do, not the first.
488
+
327
489
  **Extending the walk without forking.** The tables are public. A
328
- protocol this library does not implement — MPLS, VXLAN, a proprietary
490
+ protocol this library does not implement — MPLS, a proprietary
329
491
  telemetry header — is one registration away from participating in
330
492
  frame walks like any built-in:
331
493
 
@@ -355,8 +517,15 @@ registrations live together in
355
517
  [`_defaults.py`](src/netprotocols/_defaults.py), whose imports sit
356
518
  inside `install()` and run once from `__init__.py` after every
357
519
  protocol class exists. Shared enums stay in the dependency-free
358
- `_enums.py`. The layer modules bind their table at import and never
359
- import each other's classes at all.
520
+ `_enums.py`. The layer modules bind their table at import, and every
521
+ 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
523
+ another's class, where there is no field to look up. `layer3/icmp.py`
524
+ 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.
360
529
 
361
530
  ## Port-based dispatch is best-effort
362
531
 
@@ -372,14 +541,104 @@ fourth layer; on TCP, port 53 names `DNSOverTCP`, a shim that consumes
372
541
  the 2-byte length prefix (RFC 1035 §4.2.2) and chains onward, so the
373
542
  walk stays uniform.
374
543
 
375
- Because the guess can be wrong, the application class validates
376
- strictly on decode: a non-DNS datagram that happens to use port 53
377
- raises a `ProtocolError`, which the ordinary decode-error path absorbs
378
- (the chain keeps the layers it did decode and records the failure). So
379
- a mis-dispatch degrades to a diagnosed frame, never to garbage. This
380
- is why the port tables can be opened to third parties as safely as the
381
- authoritative ones: a wrong registration costs a diagnosed frame, not
382
- a wrong answer.
544
+ VXLAN on UDP 4789 (RFC 7348) is the one port entry whose payload is a
545
+ whole Ethernet frame. `VXLAN.next_protocol()` always answers
546
+ `Ethernet`, and the walk carries on from there exactly as it does at
547
+ the start of a frame — the inner frame's VLAN tags, ARP, IPv4 or IPv6
548
+ dispatch through the same `ethertype` table, and the registry a caller
549
+ passed reaches them too. The walker needed no change for this:
550
+ `start=` only chooses the first class, and every later one comes from
551
+ the previous header's `next_protocol()`, whatever its layer. An
552
+ encapsulating endpoint normally sends to 4789, the IANA port (RFC 7348
553
+ §5), from a source port hashed from the inner flow, so the
554
+ destination-first lookup finds VXLAN even when that hash lands on
555
+ another registered port. The same precedence means a DNS reply to a
556
+ client whose ephemeral port happens to be 4789 dispatches to VXLAN,
557
+ never to DNS (and is then rejected at the I flag about half the time).
558
+ Only 4789 is registered, and Linux still gives a vxlan device `dstport`
559
+ 8472, the port it used before IANA assigned one, when `ip link add`
560
+ names none, so a capture from such a device needs
561
+ `decode_as={"udp.port": {8472: VXLAN}}`. An ICMP echo tunnelled through
562
+ VXLAN walks seven layers (`Ethernet → IPv4 → UDP → VXLAN → Ethernet →
563
+ IPv4 → ICMPv4`), and a hostile frame nested in itself gains four per
564
+ level until `max_depth` stops it.
565
+
566
+ Because the guess can be wrong, each application class validates on
567
+ decode, but it can only reject what its format lets it tell apart from
568
+ noise, and for most formats that is little. DNS needs only its 12-byte
569
+ header and DHCP its 236-byte fixed part; past that, almost any bytes
570
+ decode cleanly as either, and even a datagram to port 53 with no
571
+ payload at all, padded to Ethernet's 60-byte minimum, decodes the
572
+ padding as a DNS header. VXLAN can reject more, because its header has
573
+ one bit it requires: the I flag, which random bytes leave clear half the
574
+ time. Over 10,000 random 300-byte UDP payloads per port, decoded with
575
+ `decode_frame(..., lax=True)`, the walk ended with `stopped_by` set for
576
+ 50.1% of those sent to 4789 and for none of those sent to 53 or 67.
577
+ With lengths drawn uniformly from 0-600 bytes instead, in frames not
578
+ padded to Ethernet's minimum, the figures are about 52%, 2% and 39%,
579
+ and every DNS and DHCP rejection is a payload too short for the fixed
580
+ header; padded to the minimum, as on the wire, the DNS figure falls to
581
+ 0%, since the padding itself decodes as a DNS header. The VXLAN
582
+ header cannot reject more than it does: RFC 7348 says to ignore its
583
+ reserved bits, so rejecting those would refuse real traffic, such as
584
+ the Group Based Policy extension's.
585
+
586
+ RIP on UDP 520 rejects the most, because two of its four header bytes
587
+ have values no RIP message carries: `RIP.decode()` requires version 1
588
+ or 2 and a nonzero command, which 510 of the 65,536 two-byte prefixes
589
+ satisfy, so about 99.2% of random datagrams are rejected. Measured the
590
+ same way, the walk stopped for about 99.2% in each case: the 300-byte
591
+ payloads sent to 520, and the 0-600 byte ones unpadded and padded, the
592
+ differences between them within sampling noise; an empty datagram
593
+ padded to the minimum is rejected too, since the padding reads as
594
+ version 0. It goes no further. The obsolete and extension commands
595
+ (TraceOn, TraceOff, the triggered and update messages) decode, and so
596
+ does a RIPv1 message with nonzero must-be-zero fields, which RFC 1058
597
+ tells a receiver to ignore but a decoder should show. Past the header,
598
+ any 20 bytes are an entry.
599
+
600
+ BGP on TCP 179 (RFC 4271) has the strongest guard of any port entry:
601
+ every message opens with a 16-byte Marker of all ones, which random
602
+ bytes match once in 2^128, and then a Length and Type whose bounds
603
+ depend on each other (§6.1). Measured the same way, over 10,000 random
604
+ 300-byte payloads and 10,000 of uniformly random length 0-600, padded
605
+ or not, every walk to port 179 ended with `stopped_by` set and none
606
+ decoded a BGP layer; with the Marker forced to all ones, the Length and
607
+ Type checks alone still rejected 9,998 to 10,000 of 10,000 random
608
+ 300-byte payloads (`random.Random` seeds 0-4). What the port cannot tell the decoder is where a message
609
+ starts: TCP cuts its byte stream where it likes, so a segment can hold
610
+ several messages, the tail of one, or the head of the next. Each
611
+ message decodes as one `BGP` layer, and `BGP.next_protocol()` answers
612
+ `BGP` while another message begins in the same segment. It is handed no
613
+ bytes, so `BGP.decode()` records the answer on the instance, in a field
614
+ outside its comparison and hash (see *Walking a frame* above). A
615
+ segment that ends inside a message raises `TruncatedHeaderError` at
616
+ that message (lax mode keeps the ones before it); one that begins
617
+ inside a message fails the Marker check, or raises
618
+ `TruncatedHeaderError` if it is shorter than a header; a bare ACK, like
619
+ one on port 53, sends an empty payload into the decoder. A segment of many small messages is a deep
620
+ chain, and here `max_depth` bounds correctness, not just cost: the
621
+ default leaves room for 29 messages after Ethernet, IPv4 and TCP, and
622
+ valid segments hold more. A 1448-byte one holds 76 KEEPALIVEs or 30
623
+ one-prefix UPDATEs, and a capture taken with GRO or TSO on, over
624
+ loopback or on a jumbo-frame link sees segments of up to 64 KB, so a
625
+ strict walk raises `MaxDepthExceededError` on valid traffic and a lax
626
+ one stops with `stopped_by` set and `consumed` short of the frame. A
627
+ larger `max_depth` or a second walk,
628
+ `decode_frame(frame[packet.consumed:], start=BGP)`, decodes the rest;
629
+ the busiest segment of the captured session in the corpus, taken with
630
+ offload off, carries 7. Reassembling
631
+ the stream is the caller's, as it is for `HTTP` below: see
632
+ [`layer7/bgp.py`](src/netprotocols/layer7/bgp.py)'s module docstring.
633
+
634
+ A rejected guess takes the ordinary decode-error path: the chain keeps
635
+ the layers it did decode and records the failure. An accepted one
636
+ yields a layer that decoded cleanly and is wrong, and nothing in the
637
+ `Packet` says so. A caller who needs certainty does not leave it to the
638
+ port: `decode_as` or a derived `Registry` names the class for a port
639
+ the caller knows, overriding the default for that walk. Third-party
640
+ entries in the port tables carry the same risk and no more: a wrong
641
+ registration costs what a wrong guess does.
383
642
 
384
643
  `HTTP` is deliberately absent from the `tcp.port` table. The unit a
385
644
  datagram protocol decodes is the payload; HTTP's is a message, which
@@ -396,10 +655,17 @@ bytes instead.
396
655
  ## The encode side, and the round-trip guarantee
397
656
 
398
657
  Every class also works in reverse. Constructors take the friendly
399
- forms (string addresses, integer fields), validate them in
400
- `__post_init__` (address formats; and IHL/data-offset must agree with
401
- `len(options)` — you cannot construct a self-contradictory header),
402
- and `bytes()` re-emits the exact on-wire form.
658
+ forms (string addresses, integer fields) and validate some of them in
659
+ `__post_init__`: Ethernet, ARP and IPv4 check address formats (IPv6,
660
+ DHCP, VRRP and OSPF do not, so a malformed address there fails only
661
+ in `bytes()`; for OSPF's `router_id`, `area_id` and its records'
662
+ address fields, such as an LSA header's `advertising_router`, that
663
+ is the `OSError` from `socket.inet_pton`), and IPv4's IHL and TCP's
664
+ data offset must agree with
665
+ `len(options)`. For a valid header, `bytes()` re-emits the exact
666
+ on-wire form. Constructors do not check everything decode does, so
667
+ some headers can be built that decode would never produce (see
668
+ below).
403
669
 
404
670
  The test suite enforces the round trip in both directions for every
405
671
  protocol:
@@ -409,14 +675,60 @@ Protocol.decode(bytes(header)) == header # object → wire → object
409
675
  bytes(Protocol.decode(raw)) == raw[:header_len] # wire → object → wire
410
676
  ```
411
677
 
678
+ The first holds for canonical headers, the ones decode itself
679
+ produces, when decode is given the context it produced them with
680
+ (HTTP's `request_method` and `closed`). A constructor accepts more
681
+ than that, and for those headers the first line can fail: spellings
682
+ are normalised on the way back (a MAC written with dashes decodes with
683
+ colons, an IPv6 address decodes compressed); GRE's optional `fields`
684
+ must agree with its flag bits; and VXLAN accepts a clear I flag on
685
+ purpose, so that a test of another stack can send one, though
686
+ `VXLAN.decode()` rejects it as its guard against a wrong port guess
687
+ (see *Port-based dispatch is best-effort*). The round-trip property
688
+ tests generate canonical headers only: addresses spelled as decode
689
+ spells them, GRE fields sized from its flags, VXLAN with the I flag
690
+ set, and HTTP messages that either need no context or are decoded with
691
+ the context they carry.
692
+
412
693
  One deliberate boundary: **decode and encode carry checksums
413
694
  verbatim** — exactly what you want when faithfully re-encoding
414
695
  captured traffic. Computing and verifying them is explicit, via
415
696
  `netprotocols.checksum`: `compute()`/`verify()` handle the IPv4 header
416
697
  checksum, ICMPv4, and TCP/UDP/ICMPv6 with their IPv4/IPv6
417
- pseudo-headers, and `Packet.with_checksums(payload)` fills a whole
698
+ pseudo-headers, PIM (the pseudo-header over IPv6 only, a Register's
699
+ first 8 bytes only), and `Packet.with_checksums(payload)` fills a whole
418
700
  stack before sending. (The RFC 768 subtlety lives where it belongs:
419
- the `0x0000 → 0xFFFF` substitution applies to UDP alone.)
701
+ the `0x0000 → 0xFFFF` substitution applies to UDP alone.) SCTP is the
702
+ one transport that does not use the internet checksum: its CRC32c
703
+ (`crc32c()`, RFC 9260 Appendix A) covers the whole packet with no
704
+ pseudo-header, and goes on the wire least significant byte first, so
705
+ `compute()` returns it byte-swapped to match the field as decoded.
706
+ VRRP's checksum is the internet checksum of the message alone in
707
+ VRRPv2 and behind the IP pseudo-header in VRRPv3. RFC 5798 §5.2.8
708
+ cites only IPv6's pseudo-header; adding IPv4's too is keepalived's
709
+ default, and `compute()`'s. RFC 9568, which obsoletes RFC 5798, leaves
710
+ it out over IPv4, and `verify()` returns `False` for an advert
711
+ checksummed that way.
712
+
713
+ That is also why `SCTP.decode()` bounds its own extent (see "The decode
714
+ contract" above): otherwise Ethernet's padding would land inside the
715
+ CRC's span, and a packet under 46 bytes from a real NIC would fail to
716
+ verify.
717
+
718
+ OSPF carries two checksums. The packet's is the internet checksum of
719
+ the whole packet but its 64-bit Authentication field, with no
720
+ pseudo-header, and `compute()` covers only the packet's own bytes, so
721
+ an RFC 5613 LLS block after it never enters; under cryptographic
722
+ authentication (AuType 2, or 3 from RFC 7474) none is computed (the
723
+ digest protects the packet), so `compute()` raises, `verify()` returns
724
+ `True` and `with_checksums()` leaves the field alone. Each LSA's LS
725
+ Checksum is an ISO 8473 Fletcher checksum of the LSA but its LS Age;
726
+ an LSA is not a layer, so it has functions of its own:
727
+ `lsa_checksum()` generates the field with `fletcher_checksum()`, and
728
+ `verify_lsa()` checks it the way RFC 905 B.4 does, by both running sums
729
+ being zero, so a byte stored as `0x00` where the generator writes
730
+ `0xff` passes; and, since RFC 2328 §12.1.7 says the field cannot be
731
+ zero, it fails a field of `0x0000` even where those sums are zero.
420
732
 
421
733
  `Packet` is the thin composition layer: it holds an ordered tuple of
422
734
  headers and `bytes(packet)` joins them, ready for a raw socket.
@@ -428,24 +740,31 @@ src/netprotocols/
428
740
  ├── __init__.py public API re-exports, __version__, registry install
429
741
  ├── _base.py Protocol ABC, decode contract, address helpers
430
742
  ├── _tlv.py walk_kind_length_value(): shared TCP/IPv4/IPv6
431
- │ options TLV walker
743
+ │ options and PIM Hello options TLV walker
432
744
  ├── _enums.py EtherType, IPProtocol, ARPOperation (imports nothing)
433
745
  ├── registry.py public dispatch tables: register(), Registry
434
746
  ├── walk.py decode_frame(): the shipped chain walker
435
747
  ├── _defaults.py the built-in decoder map, installed at import
436
748
  ├── packet.py Packet composition, with_checksums(), flow_key()
437
- ├── checksum.py RFC 1071: internet_checksum, compute, verify
749
+ ├── checksum.py RFC 1071 internet_checksum, SCTP's crc32c,
750
+ │ compute, verify, OSPF's LSA Fletcher checksum
438
751
  ├── flow.py FlowKey, flow_key(): canonical bidirectional keys
439
752
  ├── pcap.py read_captures()/read_pcap()/read_pcapng(): classic
440
753
  │ pcap and pcapng readers, from bytes not filenames
441
754
  ├── layer2/ ethernet.py, arp.py, vlan.py (802.1Q / 802.1ad)
442
755
  ├── layer3/ ip.py (IPv4 + IPv6), icmp.py (ICMPv4 + ICMPv6),
443
- │ igmp.py, gre.py,
756
+ │ igmp.py, gre.py, esp.py (IPsec ESP),
757
+ │ ah.py (IPsec AH: AH + AHv6),
758
+ │ ospf.py (OSPFv2: OSPF + its message records),
759
+ │ pim.py (PIM-SM),
760
+ │ vrrp.py (VRRPv2/v3: VRRP + VRRPv6),
444
761
  │ ipv6_ext.py (Hop-by-Hop, Routing, Fragment,
445
762
  │ Destination Options)
446
- ├── layer4/ tcp.py, udp.py, _ports.py (port-table dispatch)
763
+ ├── layer4/ tcp.py, udp.py, sctp.py, _ports.py (port-table
764
+ │ dispatch)
447
765
  ├── layer7/ dns.py, dhcp.py, http.py (the HTTP/1.1 grammar
448
- │ and the HTTP codec class)
766
+ │ and the HTTP codec class), vxlan.py, rip.py
767
+ │ (RIPv1 + RIPv2), bgp.py (BGP-4 messages)
449
768
  └── utils/ validators (mac.py, ipv4.py), exceptions.py
450
769
  tests/ one file per protocol + test_contract.py
451
770
  (truncation, lying lengths, chain walks),