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.
- {netprotocols-2.4.0 → netprotocols-2.5.0}/ARCHITECTURE.md +190 -31
- {netprotocols-2.4.0 → netprotocols-2.5.0}/CHANGELOG.md +24 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/CONTRIBUTING.md +7 -5
- {netprotocols-2.4.0 → netprotocols-2.5.0}/PKG-INFO +35 -20
- {netprotocols-2.4.0 → netprotocols-2.5.0}/README.md +34 -19
- {netprotocols-2.4.0 → netprotocols-2.5.0}/SECURITY.md +4 -0
- netprotocols-2.5.0/benchmarks/baseline.json +13 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/docs/CLAIMS.md +149 -23
- {netprotocols-2.4.0 → netprotocols-2.5.0}/pyproject.toml +2 -2
- netprotocols-2.5.0/scripts/capture_fixtures_bgp.sh +495 -0
- netprotocols-2.5.0/scripts/capture_fixtures_ospf.sh +348 -0
- netprotocols-2.5.0/scripts/capture_fixtures_pim.sh +373 -0
- netprotocols-2.5.0/scripts/capture_fixtures_rip.sh +285 -0
- netprotocols-2.5.0/scripts/capture_fixtures_vrrp.sh +349 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/check_fixtures.py +403 -14
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/__init__.py +70 -2
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/_base.py +26 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/_defaults.py +50 -6
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/_enums.py +6 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/_tlv.py +58 -36
- netprotocols-2.5.0/src/netprotocols/checksum.py +520 -0
- netprotocols-2.5.0/src/netprotocols/layer3/__init__.py +76 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer3/ip.py +12 -11
- netprotocols-2.5.0/src/netprotocols/layer3/ospf.py +971 -0
- netprotocols-2.5.0/src/netprotocols/layer3/pim.py +1025 -0
- netprotocols-2.5.0/src/netprotocols/layer3/vrrp.py +377 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer7/__init__.py +14 -0
- netprotocols-2.5.0/src/netprotocols/layer7/bgp.py +1162 -0
- netprotocols-2.5.0/src/netprotocols/layer7/rip.py +570 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/packet.py +8 -2
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/registry.py +37 -3
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/utils/exceptions.py +14 -11
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/walk.py +23 -10
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/conftest.py +56 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/MANIFEST.md +234 -11
- netprotocols-2.5.0/tests/fixtures/bgp/bgp_session.pcap +0 -0
- netprotocols-2.5.0/tests/fixtures/bgp.pcap +0 -0
- netprotocols-2.5.0/tests/fixtures/ospf.pcap +0 -0
- netprotocols-2.5.0/tests/fixtures/pim.pcap +0 -0
- netprotocols-2.5.0/tests/fixtures/rip.pcap +0 -0
- netprotocols-2.5.0/tests/fixtures/staging/bgp/bgp_session.pcap +0 -0
- netprotocols-2.5.0/tests/fixtures/staging/bgp.pcap +0 -0
- netprotocols-2.5.0/tests/fixtures/staging/ospf.pcap +0 -0
- netprotocols-2.5.0/tests/fixtures/staging/pim.pcap +0 -0
- netprotocols-2.5.0/tests/fixtures/staging/rip.pcap +0 -0
- netprotocols-2.5.0/tests/fixtures/staging/vrrp.pcap +0 -0
- netprotocols-2.5.0/tests/fixtures/vrrp.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/strategies.py +308 -0
- netprotocols-2.5.0/tests/test_bgp.py +1283 -0
- netprotocols-2.5.0/tests/test_checksum.py +1287 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_contract.py +76 -5
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_corpus.py +10 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_docs.py +3 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_enums.py +7 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_fuzz.py +478 -2
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_ipv6_ext.py +6 -1
- netprotocols-2.5.0/tests/test_ospf.py +1219 -0
- netprotocols-2.5.0/tests/test_pim.py +1273 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_registry.py +46 -2
- netprotocols-2.5.0/tests/test_rip.py +767 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_tlv.py +225 -0
- netprotocols-2.5.0/tests/test_vrrp.py +707 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/uv.lock +1 -1
- netprotocols-2.4.0/benchmarks/baseline.json +0 -13
- netprotocols-2.4.0/src/netprotocols/checksum.py +0 -255
- netprotocols-2.4.0/src/netprotocols/layer3/__init__.py +0 -33
- netprotocols-2.4.0/tests/test_checksum.py +0 -602
- {netprotocols-2.4.0 → netprotocols-2.5.0}/.claude/skills/steward/SKILL.md +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/.gitattributes +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/CODEOWNERS +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/ISSUE_TEMPLATE/protocol_request.yml +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/dependabot.yml +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/pull_request_template.md +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/workflows/ci.yml +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/workflows/codeql.yml +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/workflows/docs-lint.yml +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/workflows/fuzz.yml +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/workflows/mutation.yml +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/workflows/pr-title-lint.yml +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/workflows/release.yml +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/.github/workflows/scorecard.yml +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/.gitignore +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/LICENSE +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/_pcap.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/benchmark.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/benchmark_encode.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/benchmark_import.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures.sh +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_dhcp.sh +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_dns_tcp.sh +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_gre.sh +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_http.sh +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_igmp.sh +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_ipip.sh +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_ipsec.sh +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_sctp.sh +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_supplement.sh +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_vlan.sh +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/capture_fixtures_vxlan.sh +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/check_mutmut_survivors.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/extract_http_streams.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/ipsec_fixture_packets.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/mutmut_survivors.txt +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/pyodide/check_in_pyodide.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/pyodide/package-lock.json +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/pyodide/package.json +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/scripts/pyodide/run_in_pyodide.mjs +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/flow.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer2/__init__.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer2/arp.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer2/ethernet.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer2/vlan.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer3/ah.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer3/esp.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer3/gre.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer3/icmp.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer3/igmp.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer3/ipv6_ext.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer4/__init__.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer4/_ports.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer4/sctp.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer4/tcp.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer4/udp.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer7/dhcp.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer7/dns.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer7/http.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/layer7/vxlan.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/pcap.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/py.typed +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/utils/__init__.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/utils/ipv4.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/src/netprotocols/utils/mac.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/ah.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/arp_exchange.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/dhcp.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/dns_tcp.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/esp.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/gre.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/01.request +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/01.response +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/02.request +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/02.response +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/03.request +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/03.response +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/04.request +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/04.response +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/05.request +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/05.response +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/06.request +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/06.response +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/07.request +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/07.response +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/http/http.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/icmpv4_echo_lo.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/icmpv4_external.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/icmpv4_ttl_exceeded.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/icmpv6_echo_lo.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/igmp.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/ipip.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/ipv4_fragments.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/ipv6_fragments.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/ipv6_mld.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/ipv6_ndp_mld.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/sctp.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/ah.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/arp_exchange.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/dhcp.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/dns_tcp.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/esp.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/gre.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/http.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/icmpv4_echo_lo.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/icmpv4_external.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/icmpv4_ttl_exceeded.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/icmpv6_echo_lo.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/igmp.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/ipip.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/ipv4_fragments.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/ipv6_fragments.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/ipv6_mld.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/ipv6_ndp_mld.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/sctp.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/tcp_http.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/tcp_https.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/udp_dns.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/vlan_icmp.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/staging/vxlan.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/tcp_http.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/tcp_https.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/udp_dns.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/vlan_icmp.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fixtures/vxlan.pcap +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fuzz/fuzz_decode_frame.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/fuzz/fuzz_http_decode.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_ah.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_arp.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_dhcp.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_diagnostics.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_dns.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_esp.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_ethernet.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_exports.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_extract_http_streams.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_flow.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_gre.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_http.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_icmp.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_igmp.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_ip.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_ipip.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_mutmut_gate.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_packet.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_pattern_matching.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_pcap.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_sctp.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_tcp.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_udp.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_utils.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_version.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_vlan.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_vxlan.py +0 -0
- {netprotocols-2.4.0 → netprotocols-2.5.0}/tests/test_walk.py +0 -0
- {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
|
-
**
|
|
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
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
`
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
`
|
|
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
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
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
|
|
391
|
-
then `AH` in `ip.proto
|
|
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
|
|
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
|
|
531
|
-
`bytes()
|
|
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,
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
- **
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
92
|
-
frames** — DNS, DHCP and VXLAN-tunnelled payloads that dpkt
|
|
93
|
-
raw bytes, and 802.1Q tags it folds into the Ethernet
|
|
94
|
-
netprotocols decodes as layers of their own. A throughput
|
|
95
|
-
means little without knowing how much work it bought.
|
|
96
|
-
- **4.
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|