netprotocols 2.4.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 (225) hide show
  1. {netprotocols-2.4.0 → netprotocols-2.5.0}/ARCHITECTURE.md +190 -31
  2. {netprotocols-2.4.0 → netprotocols-2.5.0}/CHANGELOG.md +24 -0
  3. {netprotocols-2.4.0 → netprotocols-2.5.0}/CONTRIBUTING.md +7 -5
  4. {netprotocols-2.4.0 → netprotocols-2.5.0}/PKG-INFO +35 -20
  5. {netprotocols-2.4.0 → netprotocols-2.5.0}/README.md +34 -19
  6. {netprotocols-2.4.0 → netprotocols-2.5.0}/SECURITY.md +4 -0
  7. netprotocols-2.5.0/benchmarks/baseline.json +13 -0
  8. {netprotocols-2.4.0 → netprotocols-2.5.0}/docs/CLAIMS.md +149 -23
  9. {netprotocols-2.4.0 → netprotocols-2.5.0}/pyproject.toml +2 -2
  10. netprotocols-2.5.0/scripts/capture_fixtures_bgp.sh +495 -0
  11. netprotocols-2.5.0/scripts/capture_fixtures_ospf.sh +348 -0
  12. netprotocols-2.5.0/scripts/capture_fixtures_pim.sh +373 -0
  13. netprotocols-2.5.0/scripts/capture_fixtures_rip.sh +285 -0
  14. netprotocols-2.5.0/scripts/capture_fixtures_vrrp.sh +349 -0
  15. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/check_fixtures.py +403 -14
  16. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/__init__.py +70 -2
  17. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/_base.py +26 -0
  18. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/_defaults.py +50 -6
  19. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/_enums.py +6 -0
  20. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/_tlv.py +58 -36
  21. netprotocols-2.5.0/src/netprotocols/checksum.py +520 -0
  22. netprotocols-2.5.0/src/netprotocols/layer3/__init__.py +76 -0
  23. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer3/ip.py +12 -11
  24. netprotocols-2.5.0/src/netprotocols/layer3/ospf.py +971 -0
  25. netprotocols-2.5.0/src/netprotocols/layer3/pim.py +1025 -0
  26. netprotocols-2.5.0/src/netprotocols/layer3/vrrp.py +377 -0
  27. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer7/__init__.py +14 -0
  28. netprotocols-2.5.0/src/netprotocols/layer7/bgp.py +1162 -0
  29. netprotocols-2.5.0/src/netprotocols/layer7/rip.py +570 -0
  30. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/packet.py +8 -2
  31. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/registry.py +37 -3
  32. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/utils/exceptions.py +14 -11
  33. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/walk.py +23 -10
  34. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/conftest.py +56 -0
  35. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/MANIFEST.md +234 -11
  36. netprotocols-2.5.0/tests/fixtures/bgp/bgp_session.pcap +0 -0
  37. netprotocols-2.5.0/tests/fixtures/bgp.pcap +0 -0
  38. netprotocols-2.5.0/tests/fixtures/ospf.pcap +0 -0
  39. netprotocols-2.5.0/tests/fixtures/pim.pcap +0 -0
  40. netprotocols-2.5.0/tests/fixtures/rip.pcap +0 -0
  41. netprotocols-2.5.0/tests/fixtures/staging/bgp/bgp_session.pcap +0 -0
  42. netprotocols-2.5.0/tests/fixtures/staging/bgp.pcap +0 -0
  43. netprotocols-2.5.0/tests/fixtures/staging/ospf.pcap +0 -0
  44. netprotocols-2.5.0/tests/fixtures/staging/pim.pcap +0 -0
  45. netprotocols-2.5.0/tests/fixtures/staging/rip.pcap +0 -0
  46. netprotocols-2.5.0/tests/fixtures/staging/vrrp.pcap +0 -0
  47. netprotocols-2.5.0/tests/fixtures/vrrp.pcap +0 -0
  48. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/strategies.py +308 -0
  49. netprotocols-2.5.0/tests/test_bgp.py +1283 -0
  50. netprotocols-2.5.0/tests/test_checksum.py +1287 -0
  51. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_contract.py +76 -5
  52. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_corpus.py +10 -0
  53. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_docs.py +3 -0
  54. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_enums.py +7 -0
  55. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_fuzz.py +478 -2
  56. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_ipv6_ext.py +6 -1
  57. netprotocols-2.5.0/tests/test_ospf.py +1219 -0
  58. netprotocols-2.5.0/tests/test_pim.py +1273 -0
  59. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_registry.py +46 -2
  60. netprotocols-2.5.0/tests/test_rip.py +767 -0
  61. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_tlv.py +225 -0
  62. netprotocols-2.5.0/tests/test_vrrp.py +707 -0
  63. {netprotocols-2.4.0 → netprotocols-2.5.0}/uv.lock +1 -1
  64. netprotocols-2.4.0/benchmarks/baseline.json +0 -13
  65. netprotocols-2.4.0/src/netprotocols/checksum.py +0 -255
  66. netprotocols-2.4.0/src/netprotocols/layer3/__init__.py +0 -33
  67. netprotocols-2.4.0/tests/test_checksum.py +0 -602
  68. {netprotocols-2.4.0 → netprotocols-2.5.0}/.claude/skills/steward/SKILL.md +0 -0
  69. {netprotocols-2.4.0 → netprotocols-2.5.0}/.gitattributes +0 -0
  70. {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/CODEOWNERS +0 -0
  71. {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  72. {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  73. {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/ISSUE_TEMPLATE/protocol_request.yml +0 -0
  74. {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/dependabot.yml +0 -0
  75. {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/pull_request_template.md +0 -0
  76. {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/workflows/ci.yml +0 -0
  77. {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/workflows/codeql.yml +0 -0
  78. {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/workflows/docs-lint.yml +0 -0
  79. {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/workflows/fuzz.yml +0 -0
  80. {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/workflows/mutation.yml +0 -0
  81. {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/workflows/pr-title-lint.yml +0 -0
  82. {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/workflows/release.yml +0 -0
  83. {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/workflows/scorecard.yml +0 -0
  84. {netprotocols-2.4.0 → netprotocols-2.5.0}/.gitignore +0 -0
  85. {netprotocols-2.4.0 → netprotocols-2.5.0}/LICENSE +0 -0
  86. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/_pcap.py +0 -0
  87. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/benchmark.py +0 -0
  88. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/benchmark_encode.py +0 -0
  89. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/benchmark_import.py +0 -0
  90. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures.sh +0 -0
  91. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_dhcp.sh +0 -0
  92. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_dns_tcp.sh +0 -0
  93. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_gre.sh +0 -0
  94. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_http.sh +0 -0
  95. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_igmp.sh +0 -0
  96. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_ipip.sh +0 -0
  97. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_ipsec.sh +0 -0
  98. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_sctp.sh +0 -0
  99. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_supplement.sh +0 -0
  100. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_vlan.sh +0 -0
  101. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_vxlan.sh +0 -0
  102. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/check_mutmut_survivors.py +0 -0
  103. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/extract_http_streams.py +0 -0
  104. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/ipsec_fixture_packets.py +0 -0
  105. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/mutmut_survivors.txt +0 -0
  106. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/pyodide/check_in_pyodide.py +0 -0
  107. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/pyodide/package-lock.json +0 -0
  108. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/pyodide/package.json +0 -0
  109. {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/pyodide/run_in_pyodide.mjs +0 -0
  110. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/flow.py +0 -0
  111. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer2/__init__.py +0 -0
  112. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer2/arp.py +0 -0
  113. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer2/ethernet.py +0 -0
  114. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer2/vlan.py +0 -0
  115. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer3/ah.py +0 -0
  116. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer3/esp.py +0 -0
  117. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer3/gre.py +0 -0
  118. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer3/icmp.py +0 -0
  119. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer3/igmp.py +0 -0
  120. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer3/ipv6_ext.py +0 -0
  121. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer4/__init__.py +0 -0
  122. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer4/_ports.py +0 -0
  123. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer4/sctp.py +0 -0
  124. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer4/tcp.py +0 -0
  125. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer4/udp.py +0 -0
  126. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer7/dhcp.py +0 -0
  127. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer7/dns.py +0 -0
  128. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer7/http.py +0 -0
  129. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer7/vxlan.py +0 -0
  130. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/pcap.py +0 -0
  131. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/py.typed +0 -0
  132. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/utils/__init__.py +0 -0
  133. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/utils/ipv4.py +0 -0
  134. {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/utils/mac.py +0 -0
  135. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/ah.pcap +0 -0
  136. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/arp_exchange.pcap +0 -0
  137. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/dhcp.pcap +0 -0
  138. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/dns_tcp.pcap +0 -0
  139. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/esp.pcap +0 -0
  140. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/gre.pcap +0 -0
  141. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/01.request +0 -0
  142. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/01.response +0 -0
  143. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/02.request +0 -0
  144. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/02.response +0 -0
  145. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/03.request +0 -0
  146. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/03.response +0 -0
  147. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/04.request +0 -0
  148. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/04.response +0 -0
  149. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/05.request +0 -0
  150. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/05.response +0 -0
  151. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/06.request +0 -0
  152. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/06.response +0 -0
  153. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/07.request +0 -0
  154. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/07.response +0 -0
  155. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/http.pcap +0 -0
  156. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/icmpv4_echo_lo.pcap +0 -0
  157. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/icmpv4_external.pcap +0 -0
  158. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/icmpv4_ttl_exceeded.pcap +0 -0
  159. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/icmpv6_echo_lo.pcap +0 -0
  160. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/igmp.pcap +0 -0
  161. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/ipip.pcap +0 -0
  162. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/ipv4_fragments.pcap +0 -0
  163. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/ipv6_fragments.pcap +0 -0
  164. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/ipv6_mld.pcap +0 -0
  165. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/ipv6_ndp_mld.pcap +0 -0
  166. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/sctp.pcap +0 -0
  167. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/ah.pcap +0 -0
  168. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/arp_exchange.pcap +0 -0
  169. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/dhcp.pcap +0 -0
  170. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/dns_tcp.pcap +0 -0
  171. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/esp.pcap +0 -0
  172. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/gre.pcap +0 -0
  173. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/http.pcap +0 -0
  174. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/icmpv4_echo_lo.pcap +0 -0
  175. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/icmpv4_external.pcap +0 -0
  176. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/icmpv4_ttl_exceeded.pcap +0 -0
  177. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/icmpv6_echo_lo.pcap +0 -0
  178. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/igmp.pcap +0 -0
  179. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/ipip.pcap +0 -0
  180. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/ipv4_fragments.pcap +0 -0
  181. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/ipv6_fragments.pcap +0 -0
  182. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/ipv6_mld.pcap +0 -0
  183. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/ipv6_ndp_mld.pcap +0 -0
  184. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/sctp.pcap +0 -0
  185. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/tcp_http.pcap +0 -0
  186. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/tcp_https.pcap +0 -0
  187. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/udp_dns.pcap +0 -0
  188. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/vlan_icmp.pcap +0 -0
  189. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/vxlan.pcap +0 -0
  190. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/tcp_http.pcap +0 -0
  191. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/tcp_https.pcap +0 -0
  192. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/udp_dns.pcap +0 -0
  193. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/vlan_icmp.pcap +0 -0
  194. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/vxlan.pcap +0 -0
  195. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fuzz/fuzz_decode_frame.py +0 -0
  196. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fuzz/fuzz_http_decode.py +0 -0
  197. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_ah.py +0 -0
  198. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_arp.py +0 -0
  199. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_dhcp.py +0 -0
  200. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_diagnostics.py +0 -0
  201. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_dns.py +0 -0
  202. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_esp.py +0 -0
  203. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_ethernet.py +0 -0
  204. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_exports.py +0 -0
  205. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_extract_http_streams.py +0 -0
  206. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_flow.py +0 -0
  207. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_gre.py +0 -0
  208. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_http.py +0 -0
  209. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_icmp.py +0 -0
  210. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_igmp.py +0 -0
  211. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_ip.py +0 -0
  212. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_ipip.py +0 -0
  213. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_mutmut_gate.py +0 -0
  214. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_packet.py +0 -0
  215. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_pattern_matching.py +0 -0
  216. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_pcap.py +0 -0
  217. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_sctp.py +0 -0
  218. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_tcp.py +0 -0
  219. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_udp.py +0 -0
  220. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_utils.py +0 -0
  221. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_version.py +0 -0
  222. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_vlan.py +0 -0
  223. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_vxlan.py +0 -0
  224. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_walk.py +0 -0
  225. {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_workflows.py +0 -0
@@ -151,7 +151,7 @@ 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
- **Two protocols vary this.** The first is text framing. `HTTP` has no
154
+ **Three protocols vary this.** The first is text framing. `HTTP` has no
155
155
  fixed portion to unpack: `decode()` scans for the empty line that ends
156
156
  the head, then for the end the head's framing gives the body, so rule 1
157
157
  is a scan rather than an unpack and `header_len` covers the whole
@@ -185,6 +185,39 @@ read as a chunk header) parses as a phantom chunk, so `verify()` is the
185
185
  reliable signal. See
186
186
  [`layer4/sctp.py`](src/netprotocols/layer4/sctp.py)'s module docstring.
187
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.
220
+
188
221
  Decoding accepts `bytes` or `memoryview`. Views are a decode-time
189
222
  transient only: every stored field is a scalar or materialized
190
223
  `bytes`, so no decoded object ever aliases the buffer it came from.
@@ -216,15 +249,19 @@ except ProtocolError as e:
216
249
  print(e.protocol, e.field, e.offset, e.frame_offset, e.expected, e.actual)
217
250
  ```
218
251
 
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
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
228
265
  `__post_init__` validation error (values, not bytes) leaves `offset`
229
266
  `None` too; there is nothing to be positioned in. `HTTP` is the
230
267
  exception on both counts: its `field` names a grammar element
@@ -249,6 +286,22 @@ unless the Security Association uses a confidentiality-free transform
249
286
  (e.g. NULL, RFC 2410, or AES-GMAC, RFC 4543), and at an offset only
250
287
  the SA determines, so the decoder never reads it).
251
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.
304
+
252
305
  ```mermaid
253
306
  flowchart LR
254
307
  E[Ethernet] -->|0x0806| A[ARP]
@@ -263,10 +316,12 @@ flowchart LR
263
316
  I4 -->|6| T[TCP]
264
317
  I4 -->|17| U[UDP]
265
318
  I4 -->|132| S[SCTP]
319
+ I4 -->|103| P[PIM]
266
320
  I6 -->|58| IC6[ICMPv6]
267
321
  I6 -->|6| T
268
322
  I6 -->|17| U
269
323
  I6 -->|132| S
324
+ I6 -->|103| P
270
325
  I4 -->|4| I4
271
326
  I4 -->|41| I6
272
327
  I6 -->|4| I4
@@ -290,8 +345,15 @@ flowchart LR
290
345
  AH6 -->|6| T
291
346
  AH6 -->|17| U
292
347
  AH6 -->|50| ESP
348
+ I4 -->|89| OSPF[OSPFv2]
349
+ I4 -->|112| VR[VRRP]
350
+ I6 -->|112| VR6[VRRPv6]
351
+ X -->|112| VR6
293
352
  U -->|4789| VX[VXLAN]
294
353
  VX -->|always| E
354
+ U -->|520| R[RIP]
355
+ T -->|179| BGP[BGP message]
356
+ BGP -->|another message in the segment| BGP
295
357
  ```
296
358
 
297
359
  IPv6 extension headers (Hop-by-Hop Options, Routing, Fragment,
@@ -306,12 +368,15 @@ offset rule applies to fragmented IPv4.
306
368
 
307
369
  IPsec's AH chains the same way, through its own `next_header`, onward
308
370
  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,
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,
315
380
  ends the walk: what it carries is named only inside its protected
316
381
  trailer.
317
382
 
@@ -330,8 +395,12 @@ Ethernet, IP protocol numbers out of IPv4/IPv6 — and they live in
330
395
  [`_enums.py`](src/netprotocols/_enums.py) as `EtherType` and
331
396
  `IPProtocol`. IPv4 and IPv6 share one registry because the number
332
397
  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).
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.
335
404
 
336
405
  A complete frame walk is a five-line loop — which is exactly why it
337
406
  ships as [`decode_frame()`](src/netprotocols/walk.py) rather than as
@@ -379,7 +448,7 @@ field they dispatch on:
379
448
  `ip.proto.v6` **inherits** `ip.proto`: the two share a number space
380
449
  but not a table, because the four extension headers must be reachable
381
450
  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
451
+ except at a key `ip.proto.v6` owns itself (51 and 112, below); registering in
383
452
  `ip.proto.v6` reaches the v6 chain alone. Inheritance
384
453
  is resolved when you register, not when you look up, so the gating
385
454
  costs nothing on the hot path — an IPv4 packet with `protocol=0`
@@ -387,14 +456,27 @@ cannot conjure a Hop-by-Hop layer because the table it reads has no
387
456
  entry for `0`.
388
457
 
389
458
  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
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
392
462
  key the child already owns. That works the other way round too: a
393
- third party replacing protocol 51 in `ip.proto` (`override=True`)
463
+ third party replacing protocol 51 or 112 in `ip.proto` (`override=True`)
394
464
  changes IPv4 dispatch only, and must register in `ip.proto.v6` as well
395
465
  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.
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`).
398
480
 
399
481
  A row need not name a class of its own. IP-in-IP (protocols 4 and 41)
400
482
  has no header between the outer IP header and the inner one, so its
@@ -501,6 +583,54 @@ header cannot reject more than it does: RFC 7348 says to ignore its
501
583
  reserved bits, so rejecting those would refuse real traffic, such as
502
584
  the Group Based Policy extension's.
503
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
+
504
634
  A rejected guess takes the ordinary decode-error path: the chain keeps
505
635
  the layers it did decode and records the failure. An accepted one
506
636
  yields a layer that decoded cleanly and is wrong, and nothing in the
@@ -526,9 +656,12 @@ bytes instead.
526
656
 
527
657
  Every class also works in reverse. Constructors take the friendly
528
658
  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
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
532
665
  `len(options)`. For a valid header, `bytes()` re-emits the exact
533
666
  on-wire form. Constructors do not check everything decode does, so
534
667
  some headers can be built that decode would never produce (see
@@ -562,19 +695,41 @@ verbatim** — exactly what you want when faithfully re-encoding
562
695
  captured traffic. Computing and verifying them is explicit, via
563
696
  `netprotocols.checksum`: `compute()`/`verify()` handle the IPv4 header
564
697
  checksum, ICMPv4, and TCP/UDP/ICMPv6 with their IPv4/IPv6
565
- 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
566
700
  stack before sending. (The RFC 768 subtlety lives where it belongs:
567
701
  the `0x0000 → 0xFFFF` substitution applies to UDP alone.) SCTP is the
568
702
  one transport that does not use the internet checksum: its CRC32c
569
703
  (`crc32c()`, RFC 9260 Appendix A) covers the whole packet with no
570
704
  pseudo-header, and goes on the wire least significant byte first, so
571
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.
572
712
 
573
713
  That is also why `SCTP.decode()` bounds its own extent (see "The decode
574
714
  contract" above): otherwise Ethernet's padding would land inside the
575
715
  CRC's span, and a packet under 46 bytes from a real NIC would fail to
576
716
  verify.
577
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.
732
+
578
733
  `Packet` is the thin composition layer: it holds an ordered tuple of
579
734
  headers and `bytes(packet)` joins them, ready for a raw socket.
580
735
 
@@ -585,14 +740,14 @@ src/netprotocols/
585
740
  ├── __init__.py public API re-exports, __version__, registry install
586
741
  ├── _base.py Protocol ABC, decode contract, address helpers
587
742
  ├── _tlv.py walk_kind_length_value(): shared TCP/IPv4/IPv6
588
- │ options TLV walker
743
+ │ options and PIM Hello options TLV walker
589
744
  ├── _enums.py EtherType, IPProtocol, ARPOperation (imports nothing)
590
745
  ├── registry.py public dispatch tables: register(), Registry
591
746
  ├── walk.py decode_frame(): the shipped chain walker
592
747
  ├── _defaults.py the built-in decoder map, installed at import
593
748
  ├── packet.py Packet composition, with_checksums(), flow_key()
594
749
  ├── checksum.py RFC 1071 internet_checksum, SCTP's crc32c,
595
- │ compute, verify
750
+ │ compute, verify, OSPF's LSA Fletcher checksum
596
751
  ├── flow.py FlowKey, flow_key(): canonical bidirectional keys
597
752
  ├── pcap.py read_captures()/read_pcap()/read_pcapng(): classic
598
753
  │ pcap and pcapng readers, from bytes not filenames
@@ -600,12 +755,16 @@ src/netprotocols/
600
755
  ├── layer3/ ip.py (IPv4 + IPv6), icmp.py (ICMPv4 + ICMPv6),
601
756
  │ igmp.py, gre.py, esp.py (IPsec ESP),
602
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),
603
761
  │ ipv6_ext.py (Hop-by-Hop, Routing, Fragment,
604
762
  │ Destination Options)
605
763
  ├── layer4/ tcp.py, udp.py, sctp.py, _ports.py (port-table
606
764
  │ dispatch)
607
765
  ├── layer7/ dns.py, dhcp.py, http.py (the HTTP/1.1 grammar
608
- │ and the HTTP codec class), vxlan.py
766
+ │ and the HTTP codec class), vxlan.py, rip.py
767
+ │ (RIPv1 + RIPv2), bgp.py (BGP-4 messages)
609
768
  └── utils/ validators (mac.py, ipv4.py), exceptions.py
610
769
  tests/ one file per protocol + test_contract.py
611
770
  (truncation, lying lengths, chain walks),
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.5.0] - 2026-10-25
11
+
12
+ ### Added
13
+
14
+ - **`VRRP` and `VRRPv6`: the Virtual Router Redundancy Protocol, VRRPv2 (RFC 3768) and VRRPv3 (RFC 5798) (#181).** IP protocol 112 (`IPProtocol.VRRP`, "VRRP") now dispatches to `netprotocols.VRRP` from IPv4 and to its subclass `VRRPv6` from IPv6 and its extension headers. One class decodes both versions and branches on the version nibble: the fixed fields `version`, `type`, `vrid`, `priority`, `count_ip_addrs` and `checksum`, the 16 bits between Count and Checksum kept raw as `adver_word` and read through per-version accessors that return `None` where the version has no such field (`auth_type`/`auth_type_name` and `adver_int` in seconds for VRRPv2; `reserved` and `max_adver_int` in centiseconds for VRRPv3; `advertisement_interval` in seconds for either), the virtual `addresses`, and VRRPv2's 8 bytes of `auth_data` (the null-padded password under Auth Type 1, keepalived's `auth_type PASS`). The address width depends on the IP family, not on anything in the header, which is why the class comes as a pair: `VRRP` reads 4-byte addresses and `VRRPv6` 16-byte ones, and `VRRPv6` accepts VRRPv3 only, since RFC 3768 defines VRRPv2 for IPv4 alone. VRRP has no length field, but the version and count give its exact length, which is `header_len`, so Ethernet padding and an FCS after the message are left undecoded and `bytes(VRRP.decode(x)) == x[:header_len]`. A version other than 2 or 3 raises `InvalidFieldError` (`field="version"`, `offset=0`), because the version decides the layout; an unknown type, a priority of 0 (a master resigning), a count of 0 and the reserved bits are carried verbatim. A count, or VRRPv2's authentication data, past the buffer raises `TruncatedHeaderError` (`field="count_ip_addrs"` or `"auth_data"`). Construction also checks `count_ip_addrs == len(addresses)`, the authentication data's length against the version (8 bytes or none), and that `type` fits its nibble. The walk ends at VRRP. Registering over protocol 112 in `ip.proto` reaches IPv4 only, as for AH's 51; `_defaults.FAMILY_SPECIFIC_NUMBERS` now names both.
15
+ - **VRRP checksums in `compute`/`verify`/`Packet.with_checksums()`.** The internet checksum of the whole message with the checksum field zeroed: alone for VRRPv2 (RFC 3768 §5.3.8, `ip` not needed), behind the IP pseudo-header with protocol 112 for VRRPv3 over IPv4 and IPv6 (`ip` required). `payload` is ignored, as for IGMP. Over IPv6 that is RFC 5798 §5.2.8's rule; over IPv4, §5.2.8 cites only RFC 2460's pseudo-header, which is IPv6's, and the IPv4 one is what keepalived 2.2.8 sends by default. RFC 9568 §5.2.8, which obsoletes RFC 5798, leaves the pseudo-header out over IPv4, as keepalived's `v3_checksum_as_v2` option does, and `verify()` returns `False` for such an advert.
16
+ - **`RIP`: the Routing Information Protocol, versions 1 and 2 (RFC 1058, RFC 2453, #182).** UDP port 520 dispatches, best-effort like every port entry, to the new `netprotocols.RIP` (also `netprotocols.layer7.RIP`), which decodes the 4-byte header into `command`, `version` and `reserved` (bytes 2-3: must be zero in RFC 1058 and RFC 2453, RFC 1388's Routing Domain) and keeps everything after it as raw `body`, so `bytes(RIP.decode(x)) == x`. `entries` parses the 20-byte entries on demand: a route is a `RIPEntry` (`afi`, `route_tag`, `address`, `subnet_mask`, `next_hop`, `metric`; RIPv1 zeroes the v2-only fields, which are kept verbatim either way), and an entry with AFI 0xFFFF is a `RIPAuthentication` (`auth_type`, the raw 16 `data` bytes, and `password` for type 2, or `packet_length`, `key_id`, `auth_data_len` and `sequence_number` for RFC 4822's type 3), never a route; `routes` and `authentication` select them. RIP has no length field and the walk does not trim to UDP's (#235), so the entries end at the RFC 4822 authentication trailer, which the first entry's Packet Length locates when the bytes there start with the trailer's `0xFFFF 0x0001` (`authentication_trailer`, raw, to the end of the body: Auth Data Len does not size it, since BIRD declares 20 for a 16-byte MD5 digest and 32 for a 32-byte HMAC-SHA-256 one), or at the last whole entry, with anything shorter than an entry after it — an entry cut by a snap length, or an Ethernet FCS — in `remainder`; past the header neither decode nor any accessor raises. Against a wrong port guess, `decode()` rejects a version other than 1 or 2 and command 0 (`InvalidFieldError`), which 99.2% of random datagrams fail; construction accepts them. The other commands, TraceOn to Update Acknowledge (`command_name`, with IANA's names), and RIPv1 messages with nonzero must-be-zero fields decode. RIPng (RFC 2080, UDP 521, the IPv6 protocol) is not decoded; RIP itself has no IPv6 form.
17
+ - **`OSPF`: OSPFv2 (RFC 2328), IP protocol 89 (#183).** IPv4 protocol 89 (`IPProtocol.OSPF`, "OSPF") now dispatches to `netprotocols.OSPF` (also `netprotocols.layer3.OSPF`), which decodes the 24-byte common header — `version`, `type`, `packet_length`, `router_id`, `area_id`, `checksum`, `instance_id` and `au_type` (one byte each: RFC 6549 §2 split RFC 2328's 16-bit AuType into an Instance ID and an 8-bit AuType), the raw 8-byte `authentication` — and keeps the message as raw `body`, so `bytes(OSPF.decode(x)) == x[:header_len]` by construction. One accessor per message type parses the body on demand and returns `None` for the other types: `hello` (`OSPFHello`: network mask, hello and dead intervals, options, priority, DR, BDR and the neighbor list), `database_description` (`OSPFDatabaseDescription`: interface MTU, options, the I/M/MS flags, DD sequence number and its `OSPFLSAHeader`s), `ls_requests` (`OSPFLSRequest`s), `lsas` (a Link State Update's `OSPFLSA`s, each a decoded 20-byte `OSPFLSAHeader` and a raw `body`) and `ls_acknowledgments` (`OSPFLSAHeader`s). **LSA bodies are not decoded**: the Router, Network, Summary and AS-external (and other) LSA contents stay raw `bytes`, as follow-up work. `decode()` and the constructor check each known message's framing (whole neighbors, LSA headers and requests; an update's LSA count and lengths walking its body exactly), raising `InvalidFieldError` with `field="body"` otherwise, so no accessor raises; a type outside 1-5 keeps its body as is. Packet Length bounds the packet: below 24 raises `InvalidFieldError`, past the buffer `TruncatedHeaderError`. Under cryptographic authentication (AuType 2) the digest after the packet, Auth Data Len bytes, is kept in `auth_trailer` and counted in `header_len`, with `key_id`, `auth_data_len` and `crypto_sequence_number` accessors; AuType 3 (RFC 7474, extended sequence numbers) is framed the same way, its trailer the 64-bit sequence number and the digest, which Auth Data Len counts together, and the same accessors read its 32-bit Key ID and 64-bit sequence number; nothing else after the packet is consumed, so an RFC 5613 LLS block, Ethernet padding or an FCS is left after the OSPF layer. Records have `__bytes__`, so a packet can be built from them, and display helpers (`type_name`, `au_type_name`, `options_str`, `flags_str`, `ls_type_name`, hex strings). Only version 2 decodes: OSPFv3 (RFC 5340), which IPv6 carries under the same number, is not implemented, so `ip.proto.v6` withholds protocol 89 (`_defaults.IPV4_ONLY_NUMBERS`) and an IPv6 packet with next header 89 still ends its walk at the IPv6 header; `OSPF.decode()` rejects version 3 with `InvalidFieldError` (`field="version"`) for a caller who sends it there anyway. The registry gains the private mechanism for that: a withheld key owns no class, inheritance skips it as it skips an override, `derive()` copies it, and a registration made directly in the child table fills it.
18
+ - **OSPF checksums in `netprotocols.checksum` (the new functions are also exported from `netprotocols`).** `compute(ospf)` is the internet checksum of the packet with its checksum field zeroed and its Authentication field left out (RFC 2328 §D.4.1), no pseudo-header (`ip` and `payload` are ignored); under cryptographic authentication (AuType 2 or 3), which computes none (§D.4.3; RFC 7474 §5 keeps that for AuType 3), `compute()` raises `InvalidFieldError`, `verify()` returns `True` (the digest, which this library cannot check without the key, protects the packet) and `Packet.with_checksums()` leaves the field alone. New `fletcher_checksum(data, offset)` computes the ISO 8473 Fletcher checksum field (RFC 905 Annex B), raising `ValueError` unless the 2-byte field at `offset` lies inside `data`. `lsa_checksum(lsa)` generates an `OSPFLSA`'s LS Checksum, over the LSA but its LS Age (RFC 2328 §12.1.7), and `verify_lsa(lsa)` checks it as RFC 905 B.4 does, by both running sums being zero, so a field stored with `0x00` where the generator writes `0xff` (equal modulo 255) verifies too, plus RFC 2328 §12.1.7's rule that the LS Checksum cannot be zero: a field of `0x0000` fails even where those sums are zero.
19
+ - **PIM-SM (#184): `netprotocols.PIM`, layer 3, RFC 7761.** IP protocol 103 (`IPProtocol.PIM`, "PIM") now dispatches from IPv4 and IPv6, extension headers included, to one `PIM` class for both families, which decodes the 4-byte common header (`version`, `type`, `reserved`, `checksum`) and keeps the rest as raw `body`, so `bytes(PIM.decode(x)) == x[:header_len]` holds by construction. `PIM.message` dispatches on `type`, through a parser table in the shape of IGMP's message types, to a typed message parsed on demand: `PIMHello` (type 0; its option TLVs as `PIMHelloOption(type, value)` with `type_name`, plus `holdtime`, `lan_prune_delay` (`PIMLANPruneDelay`), `dr_priority`, `generation_id` and `address_list` read from them), `PIMRegister` (1; the B and N bits and the encapsulated packet, which `encapsulated` decodes in a lax walk from its IP header), `PIMRegisterStop` (2), `PIMJoinPrune` (3, and Graft and Graft-Ack, 6 and 7, which share its format; `PIMGroupSet`s of joined and pruned sources) and `PIMAssert` (5). Bootstrap (4), Candidate-RP-Advertisement (8), State Refresh (9) and other types decode with their body raw, and `message` is `None` for them; `type_name` names types 0-12 (DF Election, ECMP Redirect and PIM Flooding Mechanism, 10-12, from the IANA registry). Encoded-Unicast, Encoded-Group and Encoded-Source addresses (§4.9.1) are `PIMEncodedUnicast`, `PIMEncodedGroup` (with `bidirectional`, `admin_scope`) and `PIMEncodedSource` (with `sparse`, `wildcard`, `rpt`), sized by their own address family, so an IPv6-encoded message decodes whatever IP family carries it; only native IPv4 and IPv6 encodings are supported, and any other family or encoding (RFC 5384's join-attribute Encoded-Source included) makes `message` raise `InvalidFieldError`. Every typed message, option and address has a `bytes()`, so a message can be built from them. PIM has no length field, and the walk does not trim to the IP length, so `decode` bounds the message by its own structure: Join/Prune, Graft, Graft-Ack, Register-Stop and Assert end where their counts and addresses say, leaving any trailer (Ethernet padding, an FCS) undecoded; a Hello's options are walked and a run of zero bytes where the next option would start is left undecoded as padding; a Register's data packet and the raw types' bodies run to the end of the buffer. Past the header `decode` never raises: a body its parser cannot read (cut short, an unknown address family, a nonzero trailer after a Hello that does not walk) is kept whole, for `message` to raise on. A version other than 2 raises `InvalidFieldError` (`field="version"`) from `decode` and the constructor.
20
+ - **PIM checksums in `compute`/`verify`/`Packet.with_checksums()`.** `compute(pim, ip=...)` is the internet checksum over the whole message, or over a Register's first 8 bytes (RFC 7761 §4.9.3), with the IPv6 pseudo-header (next header 103, the covered length) in front over IPv6 and none over IPv4; `ip` is required, since the family decides which. `verify()` also accepts a Register whose checksum covers the whole message, which §4.9.3 says should be accepted "for interoperability reasons"; `with_checksums()` fills PIM layers like the transports, so it rewrites such a Register to the 8-byte form.
21
+ - **`BGP`: BGP-4 messages (RFC 4271) over TCP port 179 (#185).** TCP port 179 now dispatches, best-effort like every port entry, to `netprotocols.BGP` (also `netprotocols.layer7.BGP`), one message per instance: the 19-byte common header as `marker`, `length` and `type`, and the body kept raw in `body`, so `bytes(BGP.decode(x)) == x[:header_len]`, where `header_len` is the message's Length, not the buffer. Decode applies RFC 4271 §6.1's header checks — an all-ones Marker, a Type of 1-5 (OPEN, UPDATE, NOTIFICATION, KEEPALIVE, ROUTE-REFRESH) and that type's Length bounds (KEEPALIVE exactly 19, OPEN 29-4096, UPDATE and ROUTE-REFRESH from 23 and NOTIFICATION from 21, up to the 65535 of RFC 8654's extended messages) — and raises `TruncatedHeaderError` for a message the buffer cuts short. Accessors parse the body on demand and return `None` for other types: `version`, `my_as`, `hold_time`, `bgp_identifier`, `optional_parameters` (`BGPOptionalParameter`, either the RFC 4271 encoding or RFC 9072's extended one, flagged by `extended_optional_parameters` and recorded on each parameter as `extended_length`, which sets the width of the Parameter Length `bytes(parameter)` writes and is part of its equality; Capabilities named, its value raw) for OPEN; `withdrawn_routes` and `nlri` (`BGPPrefix`, IPv4, bit lengths up to 32) and `path_attributes` (`BGPPathAttribute`: flags, type code and raw `data`, the Attribute Length one or two bytes by the Extended Length flag; ORIGIN, NEXT_HOP, MULTI_EXIT_DISC and LOCAL_PREF values decoded in `value`, and every permanent type code in the IANA BGP Path Attributes registry named, its value kept raw) for UPDATE; `error_code`, `error_subcode`, `error_data`, their names as the IANA BGP Parameters registry (fetched 2026-10-25) assigns them (error codes 0-9 and the subcodes of every code with a subcode registry, RFC 4271 §4.5's "Unspecific" 0 for the codes without one), and an RFC 9003 `shutdown_communication` (strict UTF-8, `None` when absent or invalid) for NOTIFICATION. A malformed body raises `InvalidFieldError` from its accessor, not from decode. ROUTE-REFRESH's body stays raw. A TCP segment holding several messages decodes as one `BGP` layer each: `next_protocol()` answers `BGP` while another message begins in the same segment, which `decode()` records outside the instance's comparison and hash. A message the segment cuts raises `TruncatedHeaderError` (lax mode keeps the messages before it), a segment that starts inside one fails the Marker check, and reassembly stays the caller's, as for `HTTP`. Each message being a layer, a segment of more than 29 messages exceeds the default `max_depth`, so valid traffic can raise `MaxDepthExceededError` (or, lax, stop short): a 1448-byte segment holds up to 76 KEEPALIVEs or 30 one-prefix UPDATEs, and GRO, TSO, loopback and jumbo-frame captures carry larger segments. The module docstring, `walk.py`, ARCHITECTURE.md and `docs/CLAIMS.md` §5.7 document it and the two ways past it: a larger `max_depth`, or a second walk with `decode_frame(frame[packet.consumed:], start=BGP)`. Out of scope and left raw, as #185 asks: AS_PATH (its AS number width, 2 or 4 bytes, is negotiated in the OPENs), capability values, ADD-PATH path identifiers, ROUTE-REFRESH's fields and the MP_REACH/MP_UNREACH attributes that carry IPv6 routes. The capture host has no IPv6, so BGP over IPv6 is tested with constructed frames.
22
+
23
+ ### Development
24
+
25
+ - **Every comparative number the README quotes was re-measured for this release** (Rule 4 of `docs/CLAIMS.md`), on the same machine class as 2.4.0's figures but on the 265-frame corpus Tier 2 grew from 140, against `dpkt 1.9.8` and `scapy 2.8.0`. **The README's "within 15% of dpkt on decode" no longer holds and is replaced**: dpkt decodes the corpus 1.30× faster (1.27-1.30× over three runs), while netprotocols stays 3.4× ahead of scapy. A file-by-file A/B against the 2.4.0 tree puts the gap in the five new routing captures, on which dpkt takes about two thirds of netprotocols' time, while the 140 frames 2.4.0 was measured on still read within 10% of dpkt on both trees, so it is the workload rather than a regression; why dpkt is faster on those protocols was not profiled. Encode is 4.62× ahead of dpkt, import 70.0 ms (from 55.0 ms, the five new modules) against `scapy.all`'s 396.9 ms, and the wheel is 176.5 KiB, 15.3× smaller than scapy 2.8.0's 2.63 MiB. Decode depth against dpkt is 79 of 265 frames deeper, the 40 new ones being the BGP and RIP frames, which dpkt leaves as raw bytes. `docs/CLAIMS.md` records each run, the per-file table and the A/B, and renames claim 1.2 for the figure it now holds.
26
+ - **The decode benchmark baseline is re-recorded on the 265-frame corpus.** `benchmarks/baseline.json` was recorded at 2.2.1 on the 97-frame corpus (6.5677 normalized); it now holds 2.5.0 on 265 frames, 5.4344 normalized (97,453 f/s, 30 repetitions, best of 9, CPython 3.12.3), via `scripts/benchmark.py --update-baseline`. The corpus `--check` walks grew with Tier 1 and Tier 2 captures that cost more per frame than the old ones, so normalized throughput fell with the workload: the same library code read -14.1% on master (`03387af`) and -17.1% (-16.5% on the re-measure) on the release PR, either side of the 15% gate on runner noise alone, and the file-by-file A/B of 2.4.0 against 2.5.0 in `docs/CLAIMS.md` shows no regression beyond that harness's noise on the 140 frames 2.4.0 had. Re-anchoring accepts the drift since 2.2.1: the gate now guards against regressions relative to 2.5.0 on this corpus.
27
+ - **A real BGP capture joins the corpus: `bgp.pcap`, 19 frames (265 across 27 scenarios), and its whole session goes into `tests/fixtures/bgp/`.** `scripts/capture_fixtures_bgp.sh` runs three BIRD 2.14 speakers in netns on one bridge, an eBGP and an iBGP session, with offload disabled, through OPENs, KEEPALIVEs, UPDATEs (five prefixes each with its own MED, 600 /24s that BIRD packs into a few UPDATEs, some longer than a segment), a withdrawal and a Cease NOTIFICATION with an RFC 9003 shutdown communication. It reassembles each direction to assert what was announced and withdrawn, prefix by prefix, and that the capture holds a segment of several whole messages, one of whole messages then a cut one, and one starting inside a message; it writes every frame to `bgp/bgp_session.pcap` (94 frames, outside the corpus globs) and the frames whose payload is whole messages to `bgp.pcap`. `scripts/check_fixtures.py` needed no change: it verifies the TCP checksums that cover the BGP payload. The corpus's deepest chain is now 10 layers (a segment of 7 UPDATEs); `walk.py` and `docs/CLAIMS.md` §5.7 say so. `tests/test_fuzz.py` gains BGP accessor-safety and built-UPDATE properties.
28
+ - **A real PIM-SM capture joins the corpus: `pim.pcap`, 13 frames (246 across 26 scenarios).** `scripts/capture_fixtures_pim.sh` runs two FRR 8.4 routers (zebra + pimd) between a multicast source and an IGMPv3 receiver, four netns over three veth pairs, with static RPs on opposite routers for two groups so that a (*,G) tree and a Register exchange both cross the captured link, then has the receiver leave and stops one pimd. It keeps the first frame of each of 13 kinds — Hellos with Holdtime, LAN Prune Delay, DR Priority, Generation ID and Address List options, a Holdtime-0 goodbye, a data Register and a Null-Register, a Register-Stop, and Join/Prune messages with joined and pruned (*,G), (S,G) and (S,G,rpt) entries — and fails unless it saw all of them. FRR sums its Registers' checksums over the first 8 bytes. `scripts/check_fixtures.py` verifies every PIM checksum (IPv4, and IPv6 with its pseudo-header), accepts either Register form, and checks the Register's encapsulated packet in turn, skipping only a Null-Register's zero dummy IPv4 header checksum, which RFC 7761 says a receiver must not check; an IPv6 Null-Register's dummy PIM header (version 0, after the dummy IPv6 header) is checked by its own checksum over the dummy header's source and group, as §4.9.3 asks, rather than as a PIM message; `tests/test_pim.py` runs it on the frames and on tampered copies. The capture host has no IPv6, so PIM over IPv6 is covered by constructed frames whose pseudo-header checksums the test computes itself; Assert, Graft and the raw types are covered by constructed messages, and real frames are tested with zero padding and a 4-byte trailer appended. PIM joins the fuzz suite (`ALL_PROTOCOLS`, a round-trip strategy, an accessor-safety property and trailer properties), the truncation and damaged-payload contract tests, the corpus checksum cases and the corpus coverage set.
29
+ - **`_tlv.walk_kind_length_value` takes a `width`** (default 1), the size of the kind and length fields, so PIM Hello options (2-byte type and length) walk through it. The floor and overrun message are now derived once per call, which takes the walker from radon C (13) to B (10); a seeded 3000-case comparison against a verbatim copy of the previous walker pins every byte-wide caller (TCP, IPv4, IPv6 options) to identical results and errors. `checksum.compute()`'s PIM arm is one call to a `_pim_checksum` helper, beside the `_pseudo_header_checksum` helper the ICMPv6, TCP and UDP arms already use, so `compute()` stays at radon B. The scoped mutation runs on `netprotocols._tlv.*` and `netprotocols.checksum.*` leave no survivor beyond the one already allowlisted.
30
+ - **A real OSPFv2 capture joins the corpus: `ospf.pcap`, 54 frames (233 across 25 scenarios).** `scripts/capture_fixtures_ospf.sh` runs FRR 8.4's `zebra` and `ospfd` in two netns over a veth pair and captures three adjacencies in turn, one per AuType (none, simple password, keyed MD5), each with every message type: Hellos, the Database Description exchange, Link State Requests, Link State Updates carrying router, network and AS-external LSAs, and Link State Acknowledgments. The script fails unless each adjacency holds exactly 6 Hellos, 5 Database Descriptions, 2 requests, 2 acknowledgments and six LSAs (two router LSAs from each router, A's network and AS-external LSAs) in 3 or 4 updates, and no frame carries data after its packet (FRR sent no LLS). The test password and MD5 key are published in `tests/fixtures/MANIFEST.md`. `scripts/check_fixtures.py` verifies, with its own implementations, every AuType 0/1 packet checksum, the AuType 1 password, every AuType 2 keyed-MD5 digest (and that its packet checksum is zero) and every LSA's Fletcher checksum, in RFC 905 B.4's verification form plus RFC 2328 §12.1.7's rule that it is never zero; it reads AuType from header byte 15, past RFC 6549's Instance ID. `tests/test_ospf.py` loads the checker and holds it to failing a flipped body byte, LSA byte, password byte, digest byte, key ID and checksum, two swapped LSA bytes (which move only one Fletcher sum), a change that moves only the other, and an LS Checksum of `0x0000` whose sums are zero, and to passing a valid frame with a nonzero Instance ID. The kernel had no IPv6, so OSPFv3 over IPv6 is covered by a constructed frame, as are LLS blocks, padding and FCS trailers. `OSPF` joins the fuzz suite (`ALL_PROTOCOLS`, a round-trip strategy built from the message records, and an accessor-safety property that also feeds OSPF-shaped buffers), the truncation and lying-length contract tests, the corpus checksum cases and the corpus coverage set. The registry tests now hold `ip.proto` and `ip.proto.v6` to differing at the IPv6-only numbers, at 51, and at 89, which only `ip.proto` holds.
31
+ - **A real RIP capture joins the corpus: `rip.pcap`, 21 frames (179 across 24 scenarios).** `scripts/capture_fixtures_rip.sh` runs two BIRD 2.14 routers in two netns over a veth pair through five scenarios — RIPv1 broadcast, RIPv2 multicast, a plain-text password, keyed MD5 and HMAC-SHA-256 — and keeps a fixed, asserted selection of each: every router's first Request and first Response, and one whole-table periodic update with poisoned-reverse routes. `scripts/check_fixtures.py` now verifies each RFC 4822 digest with the published test keys (`rip-keyed-md5`, `rip-hmac-sha256`), fails on a Key ID with no published key (`rip-unknown-sa`) or a missing trailer header (`rip-trailer`), and counts RIP datagrams (`rip`); its VXLAN peeling moves into `_check_vxlan`. `RIP` joins the fuzz suite (`ALL_PROTOCOLS`, a round-trip strategy, and a property that decode and every accessor are total and that entries, trailer and remainder tile the body), the contract tests (truncation, a trailer and a cut entry under strict and lax walks) and the corpus coverage set.
32
+ - **A real VRRP capture joins the corpus: `vrrp.pcap`, 18 frames (158 across 23 scenarios).** `scripts/capture_fixtures_vrrp.sh` runs keepalived 2.2.8 as two routers in two netns over a veth pair through four virtual routers: VRRPv2 with `auth_type PASS` and two addresses, failing over from A to B with a priority-0 advert from each; VRRPv2 without authentication; VRRPv3 with one address, failing over the same way; and VRRPv3 unicast with three addresses and a 0.5 s interval. A master advertises for as long as it runs, so the script drives each step by watching the capture, keeps each router's first two live adverts and its resignation per segment, and fails unless the kept frames are exactly those 18. keepalived builds each IPv4 header (`IP_HDRINCL`) and every VRRP byte; the kernel fills the IPv4 header checksum. `scripts/check_fixtures.py` verifies every VRRP checksum independently (no pseudo-header for VRRPv2, the IPv4 or IPv6 one for VRRPv3), and fails a message whose length disagrees with its version and count or the IP payload, a version other than 2 or 3, and VRRPv2 over IPv6. veth does not pad, so Ethernet padding, a nonzero trailer and VRRPv3 over IPv6 (the capture host has no IPv6) are covered by constructed frames in `tests/test_vrrp.py`, whose checksums it computes itself and also runs through `check_fixtures.py`, as it does tampered copies of the capture. `VRRP` and `VRRPv6` join the fuzz suite (`ALL_PROTOCOLS`, round-trip strategies, an accessor property, a padding and trailer property), the truncation contract test, the corpus coverage set and `tests/test_checksum.py`; `checksum.compute()`'s UDP arm moves into a helper, as GRE's and SCTP's did, to keep its complexity where it was.
33
+
10
34
  ## [2.4.0] - 2026-10-25
11
35
 
12
36
  ### Added
@@ -39,14 +39,14 @@ is 80; Markdown is excluded from linting.
39
39
  src/netprotocols/
40
40
  _base.py the Protocol base class and the decode contract
41
41
  _enums.py EtherType / IPProtocol / ARP* registries + display names
42
- checksum.py RFC 1071 internet checksum, SCTP's CRC32c,
43
- compute()/verify()
42
+ checksum.py RFC 1071 internet checksum, SCTP's CRC32c, the
43
+ Fletcher checksum of OSPF's LSAs, compute()/verify()
44
44
  packet.py Packet: an ordered stack of headers, with_checksums()
45
45
  layer2/ Ethernet, VLAN, ARP
46
46
  layer3/ IPv4, IPv6, IPv6 extension headers, ICMPv4/v6, IGMP,
47
- GRE, IPsec ESP and AH
47
+ GRE, IPsec ESP and AH, VRRP, OSPFv2, PIM
48
48
  layer4/ TCP, UDP, SCTP (+ port-based dispatch)
49
- layer7/ DNS, DHCP, HTTP, VXLAN
49
+ layer7/ DNS, DHCP, HTTP, VXLAN, RIP, BGP
50
50
  utils/ exceptions, MAC/IPv4 helpers
51
51
  tests/ unit tests, corpus invariants, fuzzing, fixtures/
52
52
  scripts/ fixture capture + validation
@@ -61,7 +61,9 @@ subclass of `ProtocolError` (never a bare `struct.error` or
61
61
  `ValueError`) on malformed input. (SCTP, which has no length field
62
62
  and a CRC32c that trailing zeros change, finds its own extent instead:
63
63
  it leaves only zero padding after it undecoded, and keeps any other
64
- trailing or unparseable bytes in its `chunks` rather than raising.)
64
+ trailing or unparseable bytes in its `chunks` rather than raising.
65
+ PIM, which has no length field either, ends a message where its own
66
+ structure does and keeps a body it cannot parse whole, likewise.)
65
67
  The authoritative description lives in the module docstring of
66
68
  [`src/netprotocols/_base.py`](src/netprotocols/_base.py) — read it
67
69
  before adding a protocol.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: netprotocols
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: Low-level implementations of common networking protocols
5
5
  Project-URL: Homepage, https://github.com/EONRaider/NETProtocols
6
6
  Project-URL: Repository, https://github.com/EONRaider/NETProtocols
@@ -76,31 +76,33 @@ Requires Python 3.12+. Fully typed (`py.typed`, mypy strict).
76
76
 
77
77
  ## Why NETProtocols
78
78
 
79
- Measured against `dpkt 1.9.8` and `scapy 2.8.0` on the 140-frame corpus
79
+ Measured against `dpkt 1.9.8` and `scapy 2.8.0` on the 265-frame corpus
80
80
  of real captured traffic (CPython 3.12.3, x86-64 Linux; re-measured for
81
- 2.4.0 on 2026-10-25 — see [docs/CLAIMS.md](docs/CLAIMS.md) for every
81
+ 2.5.0 on 2026-10-25 — see [docs/CLAIMS.md](docs/CLAIMS.md) for every
82
82
  number below, its reproduction command, and its caveats):
83
83
 
84
- - **Within 15% of dpkt on decode, 4.1× faster than scapy** —
84
+ - **3.4× faster than scapy on decode; dpkt is 1.3× faster** —
85
85
  `uv run --group bench python scripts/benchmark.py --compare`. dpkt is
86
- the faster of the two comparators, and on this corpus it and
87
- netprotocols are at parity, the sign of the gap flipping between
88
- runs. The gap moves both ways as this library and dpkt each change, and this
89
- file's job is to say so honestly rather than only when it's
86
+ the faster of the two comparators. On the 140 frames the corpus held
87
+ at 2.4.0 the two are still within 10% of each other; the
88
+ routing captures 2.5.0 added (OSPF, BGP, PIM, RIP, VRRP) are where
89
+ dpkt pulls ahead, taking about two thirds of netprotocols' time on
90
+ them. The gap moves both ways as this library and dpkt each change,
91
+ and this file's job is to say so honestly rather than only when it's
90
92
  flattering.
91
- - **Decodes further into the stack than dpkt on 39 of those 140
92
- frames** — DNS, DHCP and VXLAN-tunnelled payloads that dpkt leaves as
93
- raw bytes, and 802.1Q tags it folds into the Ethernet header,
94
- netprotocols decodes as layers of their own. A throughput number
95
- means little without knowing how much work it bought.
96
- - **4.2× faster than dpkt at re-encoding** (`bytes(header)`,
93
+ - **Decodes further into the stack than dpkt on 79 of those 265
94
+ frames** — BGP, RIP, DNS, DHCP and VXLAN-tunnelled payloads that dpkt
95
+ leaves as raw bytes, and 802.1Q tags it folds into the Ethernet
96
+ header, netprotocols decodes as layers of their own. A throughput
97
+ number means little without knowing how much work it bought.
98
+ - **4.6× faster than dpkt at re-encoding** (`bytes(header)`,
97
99
  `scripts/benchmark_encode.py`) — the other half of "codec" that
98
100
  nobody else benchmarks.
99
- - **Imports in 55 ms; `scapy.all` takes 428+ ms** (`scripts/benchmark_import.py`),
101
+ - **Imports in 70 ms; `scapy.all` takes 397+ ms** (`scripts/benchmark_import.py`),
100
102
  and never touches the host doing it — importing scapy populates live
101
103
  interface and routing tables as a side effect of the import
102
104
  statement.
103
- - **A 124.4 KiB wheel against scapy's 2.63 MiB** — 21.7× smaller.
105
+ - **A 176.5 KiB wheel against scapy's 2.63 MiB** — 15.3× smaller.
104
106
  - **Regression-gated in CI, which — as far as we could establish by
105
107
  auditing ten comparable Python packet libraries' CI configurations
106
108
  (dpkt, scapy, pypacker, construct, pcapkit, dnspython, pyshark,
@@ -178,12 +180,17 @@ ip.proto → error: "IPv4" has no attribute
178
180
  | 3 | IP-in-IP | `IPv4`, `IPv6` | RFC 2003/4213/2473, IP protocols 4 and 41; no header of its own, so the outer IP header dispatches straight to the inner one |
179
181
  | 3 | IPsec ESP | `ESP` | RFC 4303, IP protocol 50; SPI and sequence number decoded, the protected remainder kept as opaque `ciphertext` — the chain ends here by design, since without the Security Association nothing says where the Next Header is |
180
182
  | 3 | IPsec AH | `AH`, `AHv6` | RFC 4302, IP protocol 51; fixed fields decoded and the ICV kept raw (never verified), then the payload chains onward by `next_header` through the outer IP family's table — `AHv6`, an `AH` subclass, is what IPv6 dispatches to |
183
+ | 3 | OSPFv2 | `OSPF` | RFC 2328, IP protocol 89 over IPv4; the 24-byte header, then Hello, Database Description, LS Request, LS Update and LS Acknowledgment parsed by typed accessors; LSA headers decoded, LSA bodies kept raw; packet checksum and LSA Fletcher checksum compute/verify. OSPFv3 (RFC 5340, IPv6) is not decoded: IPv6 protocol 89 ends the walk |
184
+ | 3 | PIM-SM | `PIM` | RFC 7761, IP protocol 103 from IPv4 and IPv6; common header + message-type dispatch: Hello options, Register (encapsulated packet decodable), Register-Stop, Join/Prune (also Graft/Graft-Ack), Assert parsed on demand; other types kept raw; checksum compute/verify, IPv6 pseudo-header included |
185
+ | 3 | VRRP | `VRRP`, `VRRPv6` | RFC 3768 (VRRPv2) and RFC 5798 (VRRPv3), IP protocol 112; one class branches on the version nibble (VRRPv2's Auth Type, Adver Int and authentication data; VRRPv3's Max Adver Int) and decodes the virtual-address list at the width of the IP family that carried it — `VRRPv6`, a `VRRP` subclass, is what IPv6 dispatches to; checksum compute/verify |
181
186
  | 4 | TCP | `TCP` | RFC 9293, data-offset/options aware |
182
187
  | 4 | UDP | `UDP` | RFC 768 |
183
188
  | 4 | SCTP | `SCTP` | RFC 9260, IP protocol 132; common header + chunk list (`SCTPChunk`) parsed on demand; CRC32c compute/verify |
184
189
  | 7 | DNS | `DNS` | RFC 1035, over UDP and TCP (`DNSOverTCP` length shim); on-demand name decompression + resource-record parsing |
185
190
  | 7 | DHCP | `DHCP` | RFC 2131/2132, over UDP 67/68; TLV options parsed on demand |
186
191
  | 7 | VXLAN | `VXLAN` | RFC 7348, over UDP 4789; the payload is a whole Ethernet frame, which walks on through the EtherType table |
192
+ | 7 | RIP | `RIP` | RFC 1058/2453, RIPv1 and RIPv2 over UDP 520; route entries (`RIPEntry`) and the authentication entry (`RIPAuthentication`: plain-text password, RFC 4822 keyed MD5/HMAC-SHA) parsed on demand; RIPng is not decoded |
193
+ | 7 | BGP-4 | `BGP` | RFC 4271, over TCP 179; one layer per message, chaining to the next in the same segment; OPEN fields and optional parameters, UPDATE withdrawn routes, path attributes (`BGPPathAttribute`, ORIGIN/NEXT_HOP/MED/LOCAL_PREF values decoded) and NLRI (`BGPPrefix`), NOTIFICATION codes; KEEPALIVE and ROUTE-REFRESH; no reassembly |
187
194
  | 7 | HTTP/1.1 | `HTTP` | RFC 9112/9110; one message per decode from a buffer you reassembled; ambiguities reported as anomalies; not port-dispatched |
188
195
 
189
196
  ## Decoding a captured frame
@@ -458,7 +465,9 @@ raw = bytes(frame) # ready for a raw socket
458
465
 
459
466
  Checksums are computed and verified on request — never silently:
460
467
  `compute()`/`verify()` in `netprotocols.checksum` (the internet
461
- checksum, and SCTP's CRC32c via `crc32c()`), and
468
+ checksum, VRRP's with or without its pseudo-header by version, PIM's,
469
+ and SCTP's CRC32c via `crc32c()`; OSPF's LSA checksums via
470
+ `lsa_checksum()`/`verify_lsa()`), and
462
471
  `Packet.with_checksums()` to fill a whole stack before sending.
463
472
 
464
473
  ## Display helpers
@@ -497,7 +506,13 @@ and SCTP
497
506
  [#231](https://github.com/EONRaider/NETProtocols/pull/231),
498
507
  [#232](https://github.com/EONRaider/NETProtocols/pull/232),
499
508
  [#233](https://github.com/EONRaider/NETProtocols/pull/233),
500
- [#234](https://github.com/EONRaider/NETProtocols/pull/234)) — as 2.4.0
509
+ [#234](https://github.com/EONRaider/NETProtocols/pull/234)) — as 2.4.0,
510
+ and the second — VRRP, RIP, OSPF, PIM and BGP
511
+ ([#243](https://github.com/EONRaider/NETProtocols/pull/243),
512
+ [#245](https://github.com/EONRaider/NETProtocols/pull/245),
513
+ [#246](https://github.com/EONRaider/NETProtocols/pull/246),
514
+ [#247](https://github.com/EONRaider/NETProtocols/pull/247),
515
+ [#248](https://github.com/EONRaider/NETProtocols/pull/248)) — as 2.5.0
501
516
  (see [CHANGELOG.md](CHANGELOG.md)).
502
517
 
503
518
  The roadmap before it, the post-1.3.0
@@ -524,7 +539,7 @@ released as
524
539
  2.1.0's and 1.3.1/1.4.0's work shipped folded into 2.0.0 and 2.2.0
525
540
  rather than as their own versions, but the roadmap's original "only at
526
541
  a major version bump" release policy wasn't actually held to: 2.0.0,
527
- 2.2.0, 2.2.1, 2.3.0 and now 2.4.0 are all on PyPI, and the comparative claims above
542
+ 2.2.0, 2.2.1, 2.3.0, 2.4.0 and now 2.5.0 are all on PyPI, and the comparative claims above
528
543
  draw on the finished tree. The wave before that one — TCP
529
544
  and IPv4 option parsing, ICMP message bodies, NDP, IPv6
530
545
  extension-header TLV options, the GRE checksum arm, a DNS-over-TCP
@@ -537,7 +552,7 @@ Development uses [uv](https://docs.astral.sh/uv/): `uv sync`, then
537
552
  `uv run pytest`, `uv run mypy`, and `uv run ruff check` — all three are
538
553
  enforced by CI; the test suite runs across Python 3.12–3.14, while
539
554
  `mypy` and `ruff check` run on 3.12. The test suite is anchored by a
540
- 140-frame corpus of real captured traffic across 22 scenarios
555
+ 265-frame corpus of real captured traffic across 27 scenarios
541
556
  ([tests/fixtures/MANIFEST.md](tests/fixtures/MANIFEST.md), which says
542
557
  which layers a script built rather than the kernel) plus
543
558
  property-based fuzzing of the decode path. See