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