netprotocols 2.5.0__tar.gz → 2.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {netprotocols-2.5.0 → netprotocols-2.6.0}/ARCHITECTURE.md +179 -34
- {netprotocols-2.5.0 → netprotocols-2.6.0}/CHANGELOG.md +28 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/CONTRIBUTING.md +5 -2
- {netprotocols-2.5.0 → netprotocols-2.6.0}/PKG-INFO +51 -31
- {netprotocols-2.5.0 → netprotocols-2.6.0}/README.md +50 -30
- {netprotocols-2.5.0 → netprotocols-2.6.0}/SECURITY.md +8 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/docs/CLAIMS.md +228 -29
- {netprotocols-2.5.0 → netprotocols-2.6.0}/pyproject.toml +2 -2
- netprotocols-2.6.0/scripts/capture_fixtures_llc.sh +348 -0
- netprotocols-2.6.0/scripts/capture_fixtures_lldp.sh +313 -0
- netprotocols-2.6.0/scripts/capture_fixtures_mpls.sh +259 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/check_fixtures.py +364 -9
- netprotocols-2.6.0/scripts/llc_fixture_frames.py +189 -0
- netprotocols-2.6.0/scripts/mpls_fixture_packets.py +253 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/__init__.py +22 -1
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/_base.py +70 -7
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/_defaults.py +40 -1
- netprotocols-2.6.0/src/netprotocols/_enums.py +180 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/checksum.py +50 -5
- netprotocols-2.6.0/src/netprotocols/layer2/__init__.py +26 -0
- netprotocols-2.6.0/src/netprotocols/layer2/cdp.py +642 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer2/ethernet.py +87 -6
- netprotocols-2.6.0/src/netprotocols/layer2/llc.py +481 -0
- netprotocols-2.6.0/src/netprotocols/layer2/lldp.py +522 -0
- netprotocols-2.6.0/src/netprotocols/layer2/mpls.py +392 -0
- netprotocols-2.6.0/src/netprotocols/layer2/stp.py +634 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer2/vlan.py +45 -4
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/packet.py +2 -1
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/registry.py +104 -13
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/walk.py +64 -15
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/conftest.py +57 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/MANIFEST.md +160 -8
- netprotocols-2.6.0/tests/fixtures/llc.pcap +0 -0
- netprotocols-2.6.0/tests/fixtures/lldp.pcap +0 -0
- netprotocols-2.6.0/tests/fixtures/mpls/mpls_snaplen.pcap +0 -0
- netprotocols-2.6.0/tests/fixtures/mpls.pcap +0 -0
- netprotocols-2.6.0/tests/fixtures/staging/llc.pcap +0 -0
- netprotocols-2.6.0/tests/fixtures/staging/lldp.pcap +0 -0
- netprotocols-2.6.0/tests/fixtures/staging/mpls/mpls_snaplen.pcap +0 -0
- netprotocols-2.6.0/tests/fixtures/staging/mpls.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/strategies.py +141 -0
- netprotocols-2.6.0/tests/test_cdp.py +922 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_checksum.py +171 -2
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_contract.py +34 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_corpus.py +12 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_docs.py +93 -1
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_enums.py +20 -3
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_ethernet.py +4 -2
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_fuzz.py +410 -1
- netprotocols-2.6.0/tests/test_llc.py +812 -0
- netprotocols-2.6.0/tests/test_lldp.py +819 -0
- netprotocols-2.6.0/tests/test_mpls.py +1012 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_registry.py +114 -10
- netprotocols-2.6.0/tests/test_stp.py +1139 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/uv.lock +1 -1
- netprotocols-2.5.0/src/netprotocols/_enums.py +0 -109
- netprotocols-2.5.0/src/netprotocols/layer2/__init__.py +0 -5
- {netprotocols-2.5.0 → netprotocols-2.6.0}/.claude/skills/steward/SKILL.md +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/.gitattributes +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/CODEOWNERS +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/ISSUE_TEMPLATE/protocol_request.yml +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/dependabot.yml +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/pull_request_template.md +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/workflows/ci.yml +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/workflows/codeql.yml +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/workflows/docs-lint.yml +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/workflows/fuzz.yml +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/workflows/mutation.yml +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/workflows/pr-title-lint.yml +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/workflows/release.yml +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/.github/workflows/scorecard.yml +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/.gitignore +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/LICENSE +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/benchmarks/baseline.json +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/_pcap.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/benchmark.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/benchmark_encode.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/benchmark_import.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures.sh +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_bgp.sh +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_dhcp.sh +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_dns_tcp.sh +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_gre.sh +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_http.sh +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_igmp.sh +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_ipip.sh +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_ipsec.sh +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_ospf.sh +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_pim.sh +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_rip.sh +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_sctp.sh +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_supplement.sh +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_vlan.sh +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_vrrp.sh +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/capture_fixtures_vxlan.sh +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/check_mutmut_survivors.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/extract_http_streams.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/ipsec_fixture_packets.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/mutmut_survivors.txt +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/pyodide/check_in_pyodide.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/pyodide/package-lock.json +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/pyodide/package.json +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/scripts/pyodide/run_in_pyodide.mjs +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/_tlv.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/flow.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer2/arp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/__init__.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/ah.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/esp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/gre.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/icmp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/igmp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/ip.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/ipv6_ext.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/ospf.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/pim.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer3/vrrp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer4/__init__.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer4/_ports.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer4/sctp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer4/tcp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer4/udp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer7/__init__.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer7/bgp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer7/dhcp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer7/dns.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer7/http.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer7/rip.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/layer7/vxlan.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/pcap.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/py.typed +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/utils/__init__.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/utils/exceptions.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/utils/ipv4.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/src/netprotocols/utils/mac.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/ah.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/arp_exchange.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/bgp/bgp_session.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/bgp.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/dhcp.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/dns_tcp.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/esp.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/gre.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/01.request +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/01.response +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/02.request +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/02.response +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/03.request +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/03.response +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/04.request +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/04.response +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/05.request +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/05.response +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/06.request +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/06.response +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/07.request +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/07.response +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/http/http.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/icmpv4_echo_lo.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/icmpv4_external.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/icmpv4_ttl_exceeded.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/icmpv6_echo_lo.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/igmp.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/ipip.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/ipv4_fragments.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/ipv6_fragments.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/ipv6_mld.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/ipv6_ndp_mld.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/ospf.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/pim.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/rip.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/sctp.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/ah.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/arp_exchange.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/bgp/bgp_session.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/bgp.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/dhcp.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/dns_tcp.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/esp.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/gre.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/http.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/icmpv4_echo_lo.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/icmpv4_external.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/icmpv4_ttl_exceeded.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/icmpv6_echo_lo.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/igmp.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/ipip.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/ipv4_fragments.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/ipv6_fragments.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/ipv6_mld.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/ipv6_ndp_mld.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/ospf.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/pim.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/rip.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/sctp.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/tcp_http.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/tcp_https.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/udp_dns.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/vlan_icmp.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/vrrp.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/staging/vxlan.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/tcp_http.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/tcp_https.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/udp_dns.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/vlan_icmp.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/vrrp.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fixtures/vxlan.pcap +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fuzz/fuzz_decode_frame.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/fuzz/fuzz_http_decode.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_ah.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_arp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_bgp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_dhcp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_diagnostics.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_dns.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_esp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_exports.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_extract_http_streams.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_flow.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_gre.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_http.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_icmp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_igmp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_ip.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_ipip.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_ipv6_ext.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_mutmut_gate.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_ospf.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_packet.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_pattern_matching.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_pcap.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_pim.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_rip.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_sctp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_tcp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_tlv.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_udp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_utils.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_version.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_vlan.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_vrrp.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_vxlan.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_walk.py +0 -0
- {netprotocols-2.5.0 → netprotocols-2.6.0}/tests/test_workflows.py +0 -0
|
@@ -140,7 +140,10 @@ the same three rules (spelled out in [`_base.py`](src/netprotocols/_base.py)):
|
|
|
140
140
|
1. **It receives the entire remaining buffer** — everything from the
|
|
141
141
|
start of its header to the end of the captured frame — and parses
|
|
142
142
|
the fixed portion with `_struct.unpack_from`. A buffer too short
|
|
143
|
-
for the fixed portion raises `TruncatedHeaderError`.
|
|
143
|
+
for the fixed portion raises `TruncatedHeaderError`. One enclosing
|
|
144
|
+
length shortens that buffer in a walk: after an Ethernet or VLAN
|
|
145
|
+
header holding an IEEE 802.3 Length, every later layer's buffer ends
|
|
146
|
+
where the Length ends the LLC PDU (see *802.3 framing* below).
|
|
144
147
|
2. **Variable-length headers finish the job themselves.** IPv4 reads
|
|
145
148
|
its IHL and TCP reads its data offset, sanity-checks it (5–15, and
|
|
146
149
|
the declared bytes must exist in the buffer — a header that *lies*
|
|
@@ -217,6 +220,80 @@ VRRPv2's 8 bytes of authentication data), so it obeys rule 3 like a
|
|
|
217
220
|
header with a length field and leaves Ethernet's padding undecoded. It
|
|
218
221
|
must: a VRRPv3 advert for one IPv4 address is 12 bytes, padded on the
|
|
219
222
|
wire, and VRRPv3's pseudo-header carries the message's length.
|
|
223
|
+
`STP` is the same case at layer 2: a BPDU has no length field, but its
|
|
224
|
+
BPDU Type gives its exact length (4, 35 or 36 octets), so it leaves
|
|
225
|
+
whatever follows undecoded, including the extra octets of a PDU longer
|
|
226
|
+
than its BPDU, which a receiver ignores (§14.4 of the IEEE 802.1 draft
|
|
227
|
+
the module cites). See
|
|
228
|
+
[`layer2/stp.py`](src/netprotocols/layer2/stp.py)'s module docstring.
|
|
229
|
+
|
|
230
|
+
`LLDP` has no length for the whole LLDPDU either, but bounds itself
|
|
231
|
+
by its last TLV: its TLVs each carry a Length, and the End of LLDPDU
|
|
232
|
+
TLV ends the walk, so it obeys rule 3 like a header with a length
|
|
233
|
+
field and leaves Ethernet's padding and an FCS undecoded. A TLV whose
|
|
234
|
+
Length runs past the buffer raises `TruncatedHeaderError`, as a lying
|
|
235
|
+
length does in rule 2. Wireshark's dissector notes that 802.1AB-2016
|
|
236
|
+
made the End TLV optional, so an LLDPDU that reaches the end of the
|
|
237
|
+
buffer without one decodes too; there nothing marks where a trailer
|
|
238
|
+
starts, so the walk reads one as TLVs: two or more zero bytes read as
|
|
239
|
+
an End TLV, and other bytes as another TLV or a framing error. The
|
|
240
|
+
mandatory Chassis ID, Port ID and Time To Live TLVs are read by
|
|
241
|
+
accessors from the first three positions, which answer `None` rather
|
|
242
|
+
than raise when the order is broken, since that is not framing. See
|
|
243
|
+
[`layer2/lldp.py`](src/netprotocols/layer2/lldp.py)'s module
|
|
244
|
+
docstring.
|
|
245
|
+
|
|
246
|
+
**802.3 framing.** Bytes 12-13 of an Ethernet header are a type or a
|
|
247
|
+
length (IEEE 802.3 Clause 3.2.6): from 0x0600 an EtherType, up to 1500
|
|
248
|
+
the length of an IEEE 802.2 LLC PDU, and undefined between, where the
|
|
249
|
+
walk ends. `Ethernet.ethertype` holds the raw value either way, and
|
|
250
|
+
`VLAN` reads the 2 bytes after a tag the same way. Neither `LLC` nor
|
|
251
|
+
`SNAP` has a length of its own, and a short PDU arrives padded to
|
|
252
|
+
Ethernet's minimum, often with an FCS after it; STP's BPDUs are that
|
|
253
|
+
short, and CDP's checksum covers exactly its PDU. So the Length is the
|
|
254
|
+
one enclosing length `decode_frame()` honours: after a header that
|
|
255
|
+
holds one (a class setting `_BOUNDS_PAYLOAD`, answering
|
|
256
|
+
`_payload_extent()`, both private while #235 settles the public form),
|
|
257
|
+
it ends the buffer it hands every later layer at the PDU's end. A
|
|
258
|
+
bound past the end of the frame bounds nothing, so a frame a snap
|
|
259
|
+
length cut decodes what was captured; a later, looser bound never
|
|
260
|
+
widens an earlier one; and a Length shorter than the LLC header leaves
|
|
261
|
+
`LLC.decode()` too few bytes, so the walk raises
|
|
262
|
+
`TruncatedHeaderError`. IPv4's, IPv6's and UDP's lengths are still not
|
|
263
|
+
honoured (#235), so padding behind a short IPv4 packet in Ethernet II
|
|
264
|
+
framing reaches its last layer as before. A third-party tag header
|
|
265
|
+
(an E-tag, a VN-tag) can answer `LLC` from its own `next_protocol()`,
|
|
266
|
+
but can bound the PDU after it only through that private hook, pending
|
|
267
|
+
#235. Called directly,
|
|
268
|
+
`LLC.decode()` has no Length to go by and leaves the rest of its buffer
|
|
269
|
+
to the caller, who can slice `frame[14 : 14 + eth.length]`. See
|
|
270
|
+
[`layer2/llc.py`](src/netprotocols/layer2/llc.py)'s module docstring.
|
|
271
|
+
|
|
272
|
+
`CDP`, which SNAP dispatches at OUI 00-00-0C PID 0x2000, is the first
|
|
273
|
+
decoder behind that bound. It has no length of its own: its TLVs run to
|
|
274
|
+
the end of the PDU, and its checksum covers the whole PDU, so in a walk
|
|
275
|
+
it decodes the PDU and nothing else. Its `decode()` walks TLVs
|
|
276
|
+
to the end of the buffer it is handed and stops early, without
|
|
277
|
+
raising, only where every byte left is zero, so Ethernet's zero padding
|
|
278
|
+
is left undecoded when it is called directly; rule 3 holds for those
|
|
279
|
+
zeros only, as for SCTP. Otherwise a TLV Length below 4 raises
|
|
280
|
+
`InvalidFieldError`, 1-3 bytes left after the last TLV raise
|
|
281
|
+
`TruncatedHeaderError`, and so does a TLV whose Length runs past the
|
|
282
|
+
buffer, in a walk too, where those bytes are inside the 802.3 Length.
|
|
283
|
+
Wireshark's dissector instead stops at a Length below 4 whatever
|
|
284
|
+
follows, and marks 1-3 bytes left malformed even when they are zero.
|
|
285
|
+
The zero rule has one blind spot: zeros after the last TLV but inside
|
|
286
|
+
the 802.3 Length belong to the PDU the sender declared, yet a walk
|
|
287
|
+
leaves them undecoded without raising, so `consumed` stopping short of
|
|
288
|
+
the Length's end is the only structural signal. `verify()` sums the
|
|
289
|
+
bytes decoded, without them; where the TLVs end at an odd offset on a
|
|
290
|
+
nonzero byte and the sender summed the whole Length, it returns
|
|
291
|
+
`False`.
|
|
292
|
+
`scripts/check_fixtures.py` rejects such a frame, and Wireshark marks
|
|
293
|
+
1-3 such zeros malformed and shows 4 or more as a TLV of invalid
|
|
294
|
+
Length 0.
|
|
295
|
+
See [`layer2/cdp.py`](src/netprotocols/layer2/cdp.py)'s module
|
|
296
|
+
docstring.
|
|
220
297
|
|
|
221
298
|
Decoding accepts `bytes` or `memoryview`. Views are a decode-time
|
|
222
299
|
transient only: every stored field is a scalar or materialized
|
|
@@ -286,21 +363,32 @@ unless the Security Association uses a confidentiality-free transform
|
|
|
286
363
|
(e.g. NULL, RFC 2410, or AES-GMAC, RFC 4543), and at an offset only
|
|
287
364
|
the SA determines, so the decoder never reads it).
|
|
288
365
|
|
|
289
|
-
`BGP`
|
|
290
|
-
than from a wire field. A BGP message
|
|
291
|
-
can hold several messages back to
|
|
292
|
-
so `BGP.next_protocol()` names the
|
|
293
|
-
when the bytes after this message
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
366
|
+
`BGP`, `MPLS` and `LLC` are the three exceptions: they answer from
|
|
367
|
+
decode context rather than from a wire field alone. A BGP message
|
|
368
|
+
carries no payload: a TCP segment can hold several messages back to
|
|
369
|
+
back, each decoded as its own layer, so `BGP.next_protocol()` names the
|
|
370
|
+
next message *beside* it — `BGP` when the bytes after this message
|
|
371
|
+
begin another one, `None` when they do not. An MPLS label stack carries
|
|
372
|
+
a packet but no field naming its protocol (see *MPLS: dispatch by
|
|
373
|
+
convention* below), so `MPLS.next_protocol()` infers one, partly from
|
|
374
|
+
the first byte after the stack. SAP 0x42 carries GARP's PDUs (Protocol
|
|
375
|
+
Identifier 0x0001) beside STP's BPDUs (0x0000), so `LLC.decode()`
|
|
376
|
+
records, for a DSAP 0x42 PDU it would dispatch, whether the two octets
|
|
377
|
+
after its Control are present and not 0x0000, and `LLC.next_protocol()`
|
|
378
|
+
then answers `None`, whatever `llc.dsap` holds at 0x42; fewer than two
|
|
379
|
+
octets still reach `STP`, which reports the truncation. Only the bytes
|
|
380
|
+
can say, and `next_protocol()` is handed none, so each `decode()`
|
|
381
|
+
records what it saw on the instance in a field kept out of the
|
|
382
|
+
dataclass's comparison and hash (`compare=False`, the mechanism `HTTP`
|
|
383
|
+
uses for its `_message` parse cache). Two equal headers compare equal
|
|
384
|
+
however they were found, and can still chain differently. The rules of
|
|
385
|
+
the decode contract hold for all three unchanged; see *Port-based
|
|
386
|
+
dispatch is best-effort* below,
|
|
301
387
|
[`_base.py`](src/netprotocols/_base.py)'s *Decode context in
|
|
302
|
-
next_protocol*,
|
|
303
|
-
[`layer7/bgp.py`](src/netprotocols/layer7/bgp.py)
|
|
388
|
+
next_protocol*, the module docstrings of
|
|
389
|
+
[`layer7/bgp.py`](src/netprotocols/layer7/bgp.py) and
|
|
390
|
+
[`layer2/mpls.py`](src/netprotocols/layer2/mpls.py), and
|
|
391
|
+
[`layer2/llc.py`](src/netprotocols/layer2/llc.py)'s *DSAP 0x42*.
|
|
304
392
|
|
|
305
393
|
```mermaid
|
|
306
394
|
flowchart LR
|
|
@@ -308,10 +396,23 @@ flowchart LR
|
|
|
308
396
|
E -->|0x0800| I4[IPv4]
|
|
309
397
|
E -->|0x86DD| I6[IPv6]
|
|
310
398
|
E -->|0x8100, 0x88A8, 0x9100| V[VLAN tag]
|
|
399
|
+
E -->|length ≤ 1500| L[LLC]
|
|
400
|
+
V -->|length ≤ 1500| L
|
|
401
|
+
L -->|DSAP 0xAA, SSAP 0xAA, UI| SN[SNAP]
|
|
402
|
+
L -->|DSAP 0x42, UI or I, Protocol ID 0| STP[STP BPDU]
|
|
403
|
+
SN -->|OUI 00-00-00 or 00-00-F8, PID 0x0806| A
|
|
404
|
+
SN -->|OUI 00-00-00 or 00-00-F8, PID 0x0800| I4
|
|
405
|
+
SN -->|OUI 00-00-0C, PID 0x2000| CDP[CDP]
|
|
311
406
|
V -->|0x8100, 0x88A8, 0x9100| V
|
|
312
407
|
V -->|0x0806| A
|
|
313
408
|
V -->|0x0800| I4
|
|
314
409
|
V -->|0x86DD| I6
|
|
410
|
+
E -->|0x8847, 0x8848| M[MPLS label stack]
|
|
411
|
+
V -->|0x8847, 0x8848| M
|
|
412
|
+
M -->|bottom label 0, or first nibble 4| I4
|
|
413
|
+
M -->|bottom label 2, or first nibble 6| I6
|
|
414
|
+
E -->|0x88CC| LD[LLDP]
|
|
415
|
+
V -->|0x88CC| LD
|
|
315
416
|
I4 -->|1| IC4[ICMPv4]
|
|
316
417
|
I4 -->|6| T[TCP]
|
|
317
418
|
I4 -->|17| U[UDP]
|
|
@@ -390,8 +491,36 @@ bitfields — `pcp`, `dei`, `vid` — as dataclass fields validated in
|
|
|
390
491
|
property (the same pattern `IPv6` uses for `version` /
|
|
391
492
|
`traffic_class` / `flow_label`).
|
|
392
493
|
|
|
494
|
+
**MPLS: dispatch by convention.** An MPLS label stack (RFC 3032) sits
|
|
495
|
+
where a VLAN tag does, after Ethernet's EtherType (0x8847, or 0x8848
|
|
496
|
+
for a multicast frame whose top label is upstream-assigned, RFC 5332),
|
|
497
|
+
and decodes as one `MPLS` layer however many 4-byte entries it holds:
|
|
498
|
+
the stack ends at the entry with its Bottom of Stack bit set, so it
|
|
499
|
+
bounds itself, and a deep stack counts once against `max_depth`. What
|
|
500
|
+
it does not have is a field naming the packet it carries. RFC 3032
|
|
501
|
+
§2.2 leaves that to the binding of the label popped at the egress,
|
|
502
|
+
which a capture never shows, so `MPLS.next_protocol()` follows a
|
|
503
|
+
convention. A bottom label reserved for a payload settles it — 0, IPv4
|
|
504
|
+
Explicit NULL, is IPv4 and 2, IPv6 Explicit NULL, is IPv6, whatever
|
|
505
|
+
the bytes say, while 13 (GAL) and 14 (OAM Alert) are followed by a
|
|
506
|
+
channel header or an OAM message and stop the walk. Otherwise the
|
|
507
|
+
first nibble after the stack is read as an IP version, 4 or 6, as
|
|
508
|
+
routers that load-balance IP over MPLS do (RFC 4928); anything else, a
|
|
509
|
+
pseudowire control word's 0 among them, stops the walk. The class
|
|
510
|
+
comes from the `ethertype` table at 0x0800 or 0x86DD, so a `registry`
|
|
511
|
+
or `decode_as` override reaches the labelled packet too. The nibble is
|
|
512
|
+
a guess, as a port is: an Ethernet pseudowire sent without a control
|
|
513
|
+
word, whose inner destination MAC address happens to begin with 4 or
|
|
514
|
+
6, decodes as an IP header, which is the misreading RFC 4928 and RFC
|
|
515
|
+
4385's control word exist to prevent. GRE dispatches its Protocol Type
|
|
516
|
+
through the `ethertype` table too, so MPLS in GRE (RFC 4023) reaches
|
|
517
|
+
`MPLS` with no code of its own. See
|
|
518
|
+
[`layer2/mpls.py`](src/netprotocols/layer2/mpls.py)'s module
|
|
519
|
+
docstring.
|
|
520
|
+
|
|
393
521
|
The numbers on the arrows are the wire values — EtherTypes out of
|
|
394
|
-
Ethernet, IP protocol numbers out of IPv4/IPv6
|
|
522
|
+
Ethernet, IP protocol numbers out of IPv4/IPv6; MPLS's arrows, which
|
|
523
|
+
no wire field names, are the convention above — and they live in
|
|
395
524
|
[`_enums.py`](src/netprotocols/_enums.py) as `EtherType` and
|
|
396
525
|
`IPProtocol`. IPv4 and IPv6 share one registry because the number
|
|
397
526
|
space is shared (1 is ICMPv4, 58 is ICMPv6, 6 is TCP, 17 is UDP, 132
|
|
@@ -413,11 +542,13 @@ packet = decode_frame(frame) # Packet(Ethernet(...), IPv4(...), TCP(...
|
|
|
413
542
|
Underneath it is the loop the two answers imply:
|
|
414
543
|
|
|
415
544
|
```python
|
|
416
|
-
layers, cursor, protocol = [], 0, start
|
|
545
|
+
layers, cursor, end, protocol = [], 0, len(frame), start
|
|
417
546
|
while protocol is not None:
|
|
418
|
-
header = protocol.decode(frame[cursor:])
|
|
547
|
+
header = protocol.decode(frame[cursor:end])
|
|
419
548
|
layers.append(header)
|
|
420
549
|
cursor += header.header_len
|
|
550
|
+
if header._BOUNDS_PAYLOAD: # an 802.3 Length (see above)
|
|
551
|
+
end = _bounded(header, cursor, end)
|
|
421
552
|
protocol = header.next_protocol(registry)
|
|
422
553
|
```
|
|
423
554
|
|
|
@@ -434,12 +565,14 @@ Wrapping each frame in a `memoryview` internally measured *slower*
|
|
|
434
565
|
**Where do those arrows live?** Not in the protocol classes. Every
|
|
435
566
|
one of them is a row in a dispatch table owned by
|
|
436
567
|
[`registry.py`](src/netprotocols/registry.py), and `next_protocol()`
|
|
437
|
-
is a single `dict.get` against one of
|
|
568
|
+
is a single `dict.get` against one of seven tables named after the wire
|
|
438
569
|
field they dispatch on:
|
|
439
570
|
|
|
440
571
|
| Table | Dispatches on | Read by |
|
|
441
572
|
|---|---|---|
|
|
442
|
-
| `ethertype` | Ethernet II EtherType | `Ethernet`, `VLAN`, `GRE` |
|
|
573
|
+
| `ethertype` | Ethernet II EtherType (from Ethernet and VLAN, 0x0600 and up only; from GRE and SNAP, any value) | `Ethernet`, `VLAN`, `GRE`, `SNAP` (OUI 00-00-00, 00-00-F8); `MPLS` at 0x0800 and 0x86DD, by convention |
|
|
574
|
+
| `llc.dsap` | IEEE 802.2 LLC DSAP | `LLC` |
|
|
575
|
+
| `llc.snap` | SNAP `(oui << 16) \| pid`, any other OUI | `SNAP` |
|
|
443
576
|
| `ip.proto` | IPv4 `protocol` | `IPv4`, `AH` |
|
|
444
577
|
| `ip.proto.v6` | IPv6 `next_header` | `IPv6`, extension headers, `AHv6` |
|
|
445
578
|
| `udp.port` | UDP well-known port | `UDP` |
|
|
@@ -487,16 +620,18 @@ therefore take the nearest one before it, as `Packet.with_checksums()`
|
|
|
487
620
|
and `Packet.flow_key()` do, not the first.
|
|
488
621
|
|
|
489
622
|
**Extending the walk without forking.** The tables are public. A
|
|
490
|
-
protocol this library does not implement —
|
|
491
|
-
|
|
623
|
+
protocol this library does not implement — a proprietary telemetry
|
|
624
|
+
header, say, sent under 0x88B5, which IANA's EtherType registry lists
|
|
625
|
+
as "IEEE Std 802 - Local Experimental Ethertype", so no built-in
|
|
626
|
+
decoder holds it — is one registration away from participating in
|
|
492
627
|
frame walks like any built-in:
|
|
493
628
|
|
|
494
629
|
```python
|
|
495
630
|
from netprotocols import Protocol
|
|
496
631
|
from netprotocols.registry import register
|
|
497
632
|
|
|
498
|
-
@register("ethertype",
|
|
499
|
-
class
|
|
633
|
+
@register("ethertype", 0x88B5)
|
|
634
|
+
class Telemetry(Protocol):
|
|
500
635
|
...
|
|
501
636
|
```
|
|
502
637
|
|
|
@@ -519,19 +654,23 @@ inside `install()` and run once from `__init__.py` after every
|
|
|
519
654
|
protocol class exists. Shared enums stay in the dependency-free
|
|
520
655
|
`_enums.py`. The layer modules bind their table at import, and every
|
|
521
656
|
class a header chooses by a field value (EtherType, IP protocol, port)
|
|
522
|
-
comes out of a table, never from an import.
|
|
657
|
+
comes out of a table, never from an import. Three modules do import
|
|
523
658
|
another's class, where there is no field to look up. `layer3/icmp.py`
|
|
524
659
|
names `IPv4` and `IPv6` to start the walk of the packet an ICMP error
|
|
525
|
-
quotes, which is not dispatch
|
|
526
|
-
directly, because a VXLAN payload is always an Ethernet frame
|
|
527
|
-
|
|
528
|
-
|
|
660
|
+
quotes, which is not dispatch; `layer7/vxlan.py` names `Ethernet`
|
|
661
|
+
directly, because a VXLAN payload is always an Ethernet frame; and
|
|
662
|
+
`layer2/ethernet.py` names `LLC`, because whatever an 802.3 Length's
|
|
663
|
+
value, an LLC header follows it. None can form a cycle: `ip.py` and
|
|
664
|
+
`llc.py` import no other layer module, and `ethernet.py` imports only
|
|
665
|
+
`llc.py`.
|
|
529
666
|
|
|
530
667
|
## Port-based dispatch is best-effort
|
|
531
668
|
|
|
532
|
-
The `ethertype` and `ip.proto` tables dispatch on a single
|
|
533
|
-
authoritative field: it exists to say what follows
|
|
534
|
-
|
|
669
|
+
The `ethertype`, `llc.*` and `ip.proto` tables dispatch on a single
|
|
670
|
+
authoritative field: it exists to say what follows (MPLS, which looks
|
|
671
|
+
up the `ethertype` table by convention, is the exception described
|
|
672
|
+
above). The two port tables break that model, and differ in kind
|
|
673
|
+
rather than just in name.
|
|
535
674
|
A port is a *guess* — any service may run on any port, and the
|
|
536
675
|
discriminator is split across the source and destination ports.
|
|
537
676
|
Dispatch therefore checks the destination port first (a request
|
|
@@ -747,11 +886,17 @@ src/netprotocols/
|
|
|
747
886
|
├── _defaults.py the built-in decoder map, installed at import
|
|
748
887
|
├── packet.py Packet composition, with_checksums(), flow_key()
|
|
749
888
|
├── checksum.py RFC 1071 internet_checksum, SCTP's crc32c,
|
|
750
|
-
│ compute, verify
|
|
889
|
+
│ compute, verify (CDP's odd-byte sum among them),
|
|
890
|
+
│ OSPF's LSA Fletcher checksum
|
|
751
891
|
├── flow.py FlowKey, flow_key(): canonical bidirectional keys
|
|
752
892
|
├── pcap.py read_captures()/read_pcap()/read_pcapng(): classic
|
|
753
893
|
│ pcap and pcapng readers, from bytes not filenames
|
|
754
|
-
├── layer2/ ethernet.py, arp.py, vlan.py (802.1Q / 802.1ad)
|
|
894
|
+
├── layer2/ ethernet.py, arp.py, vlan.py (802.1Q / 802.1ad),
|
|
895
|
+
│ llc.py (802.2 LLC + SNAP, after an 802.3 Length),
|
|
896
|
+
│ cdp.py (Cisco Discovery Protocol, after SNAP),
|
|
897
|
+
│ lldp.py (LLDP, IEEE 802.1AB),
|
|
898
|
+
│ mpls.py (MPLS label stack: MPLS + MPLSLabel),
|
|
899
|
+
│ stp.py (802.1D / 802.1w BPDUs)
|
|
755
900
|
├── layer3/ ip.py (IPv4 + IPv6), icmp.py (ICMPv4 + ICMPv6),
|
|
756
901
|
│ igmp.py, gre.py, esp.py (IPsec ESP),
|
|
757
902
|
│ ah.py (IPsec AH: AH + AHv6),
|
|
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.6.0] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`MPLS`: the MPLS label stack (RFC 3032 §2.1, #187).** EtherTypes 0x8847 (`EtherType.MPLS`, "MPLS") and 0x8848 (`EtherType.MPLS_UPSTREAM_ASSIGNED`, "MPLS with upstream-assigned label": IANA's names; RFC 5332 §4 retired 0x8848's original "multicast" meaning) now dispatch to `netprotocols.MPLS` (also `netprotocols.layer2.MPLS`), from Ethernet, VLAN tags and GRE alike, so MPLS in GRE (RFC 4023) needs no code of its own. One `MPLS` layer holds the whole stack, top first, as `labels`, a tuple of `MPLSLabel` entries (`label`, `traffic_class` — RFC 5462's name for RFC 3032's Exp field — `bottom_of_stack` and `ttl`, with `word`, `bytes()`, `is_special_purpose`, and `label_name`, the base special-purpose labels named as IANA's registry names them); `bottom` is the last entry. `decode()` reads entries until the first with the S bit set, so the stack bounds itself and `bytes(MPLS.decode(x)) == x[:header_len]`; what follows, Ethernet padding and FCS included, is the labelled packet's. A stack whose S bit never comes raises `TruncatedHeaderError` (`field="labels"`, `offset` where the missing entry would start), in time linear in the buffer, and a deep stack counts once against `max_depth`. Construction requires a non-empty stack with S set on the last entry alone, so every stack that can be built round-trips. MPLS names no payload protocol, so `next_protocol()` follows a documented convention: a bottom label of 0 or 2 (IPv4 or IPv6 Explicit NULL) names IPv4 or IPv6, 13 (GAL) and 14 (OAM Alert) stop the walk, and otherwise the first nibble after the stack is read as an IP version, 4 or 6 (RFC 4928 §2), so a pseudowire control word or associated channel header (first nibble 0 or 1, RFC 4385) stops it. The class comes from the `ethertype` table at 0x0800 or 0x86DD, so `registry` and `decode_as` overrides reach the labelled packet; `payload_ethertype` says which the convention chose. `next_protocol()` is handed no bytes, so `decode()` records the nibble on the instance outside its comparison and hash, as `BGP` records whether another message follows; a stack built rather than decoded answers from its bottom label alone. The guess fails, as RFC 4928 describes, on an Ethernet pseudowire sent without a control word whose inner destination MAC begins with nibble 4 or 6, which decodes as an IP header; the module docstring says how to walk such a payload explicitly. Since 0x8847 and 0x8848 are now built in, code that registered a different class at either on the default registry (as the `register()` examples in 2.5.0's `registry.py`, README.md, ARCHITECTURE.md and `docs/CLAIMS.md` did), or on one built from it with `Registry.from_defaults()` or `derive()`, now raises `RegistryConflictError`; pass `override=True` to replace `MPLS`, or `decode_as={"ethertype": {0x8847: ..., 0x8848: ...}}` (each key you used) to override it for one walk.
|
|
15
|
+
- **`LLDP`: the Link Layer Discovery Protocol (IEEE 802.1AB), layer 2 (#186).** EtherType 0x88CC (`EtherType.LLDP`, "LLDP") now dispatches to `netprotocols.LLDP` (also `netprotocols.layer2.LLDP`) from Ethernet and from a VLAN tag, and the walk ends there. An LLDPDU is a run of TLVs, each a 16-bit header word (a 7-bit Type, a 9-bit Length) and Length bytes of value; `LLDP.tlvs` keeps every one, in wire order, as an `LLDPTLV(type, value)` with `length` and `type_name` (Wireshark's names for types 0-8 and 127, "unknown (n)" for the reserved 9-126), through the End of LLDPDU TLV, so `bytes(LLDP.decode(x)) == x[:header_len]` holds by construction. The End TLV bounds the LLDPDU: Ethernet padding, an FCS or anything else after it is left undecoded. An LLDPDU that reaches the end of the buffer without one decodes too, since 802.1AB-2016 made the End TLV optional, as Wireshark's dissector notes (lldpd 1.0.18's receiver still discards one); without an End TLV a trailer is read as TLVs: two or more zero bytes read as an End TLV, and anything else as another TLV or a framing error. `decode` checks framing only: a buffer shorter than one TLV header raises `TruncatedHeaderError` as any too-short header does (offset 0, no `field`), a later TLV header cut short or a TLV whose Length runs past the buffer raises it with `field="tlvs"` and the TLV's offset, and an End TLV with a nonzero Length raises `InvalidFieldError`. The mandatory order is reported, not enforced: `chassis_id` (`LLDPChassisID`) and `port_id` (`LLDPPortID`), each a `subtype` and the raw `id` with `subtype_name`, and `ttl` (an `int`, its first two bytes) read the first, second and third TLV and are `None` when that TLV is of another type, an ID's value is not 2 to 256 bytes or the TTL's is shorter than 2 bytes, so none raises; `has_mandatory_tlvs` says whether all three are there (any subtype passes), `is_shutdown` whether the TTL is 0, and `tlv(type)` finds the first TLV of a type. Subtype numbers follow the IEEE 802.1 LLDP MIBs and the display names Wireshark's dissector. Every other TLV, organizationally specific ones included, keeps its value raw. The walk is local to `layer2/lldp.py`; `_tlv.walk_kind_length_value`, which TCP, IPv4, IPv6 and PIM share, is unchanged. EtherType 0x88CC no longer serves the tests and README as the example of an unknown EtherType; 0x88B5, IEEE 802's Local Experimental EtherType 1, does.
|
|
16
|
+
- **`LLC` and `SNAP`: IEEE 802.2 LLC and SNAP headers after an IEEE 802.3 Length (#188).** An Ethernet or VLAN type/length field of 1500 or less is a frame Length (IEEE 802.3 Clause 3.2.6), and `next_protocol()` now answers `netprotocols.LLC` (also `netprotocols.layer2`) for it: DSAP, SSAP and a 1-octet (U-format) or 2-octet (I- and S-format) Control, stored with its first octet as the integer's low byte so the format bits are its two low bits. I-format and UI PDUs dispatch on the DSAP through the new `llc.dsap` table (`registry.TABLE_LLC_DSAP`), at DSAP 0x42 only when the two octets after the Control are 0x0000, or fewer than two octets follow (next entry); other formats, such as XID and TEST, end the walk. Accessors: `control_format`, `is_group`, `is_response`, `is_raw_ipx`, `dsap_name`, `ssap_name`, `dsap_enum`. The new `LLCSAP` enum holds the IEEE Registration Authority's assigned LSAPs and the global DSAP, with display names. `SNAP` (OUI and PID, RFC 1042) is a built-in `llc.dsap` entry, at 0xAA, reached only when the SSAP is 0xAA too and the Control is UI; `STP`, at 0x42, is the other (next entry). Under OUI 00-00-00 (RFC 1042) or 00-00-F8 (IEEE 802.1H bridge-tunnel) the PID is an EtherType and dispatches through `ethertype`; under any other OUI it goes through the new `llc.snap` table (`registry.TABLE_LLC_SNAP`), keyed `(oui << 16) | pid` (`SNAP.snap_key`). This departs from #188, which recommended reading every PID in `ethertype`: RFC 9542 §3 makes the PID a number the OUI's owner assigns, so CDP (OUI 00-00-0C, PID 0x2000) is not EtherType 0x2000. A Novell raw 802.3 frame (0xFFFF where the SAPs would be: an IPX checksum) decodes as an `LLC` layer with `is_raw_ipx` set and ends the walk; that layer's other fields and accessors then read IPX bytes and are meaningless (the fixture's frame reads as an I-format Control 0x1E00, the IPX length). STP (DSAP 0x42) decodes as `STP` (next entry), and CDP as `CDP` (the entry after it).
|
|
17
|
+
- **`STP`: Spanning Tree BPDUs, IEEE 802.1D and 802.1w (RSTP) (#189).** LLC DSAP 0x42 (`LLCSAP.SPANNING_TREE`) now dispatches, for a UI or I-format PDU, to `netprotocols.STP` (also `netprotocols.layer2.STP`), the walk's last layer, unless the two octets after the Control are present and not 0x0000: GARP's PDUs, such as GVRP's, share the SAP with Protocol Identifier 0x0001 (IEEE P802.1aq/D1.5++ §14.5 NOTE 1 leaves other Protocol Identifiers to other protocols), and their walk ends at `LLC`, whatever class `llc.dsap` holds at 0x42. `LLC.decode()` records that check on the instance outside its comparison and hash, so equal LLC headers still compare and hash equal; a header built with the constructor or `dataclasses.replace()` has seen no bytes and dispatches. Fewer than two octets still reach `STP`, which reports the truncation. One class decodes the three BPDU Types and branches on the type, as ICMP does: the Configuration BPDU (type 0x00, 35 octets, 802.1D-2004 §9.3.1), the RST BPDU (0x02, 36 octets, §9.3.3: the same fields and a Version 1 Length) and the Topology Change Notification (0x80, §9.3.2), which is the 4-octet prefix alone and decodes as the short form of the class, every field after `bpdu_type` `None`. Fields: `protocol_id`, `version`, `bpdu_type`, then `flags`, `root_priority`, `root_mac`, `root_path_cost`, `bridge_priority`, `bridge_mac`, `port_id`, `message_age`, `max_age`, `hello_time`, `forward_delay` (the timers raw, in 1/256 s) and `version_1_length` (RST only). The flag accessors answer `None` in a type that leaves their bit unused: `topology_change` (Configuration and RST), `topology_change_ack` (Configuration only), and `proposal`, `port_role`/`port_role_name`, `learning`, `forwarding` and `agreement` (RST only); the raw octet stays in `flags`. The two priority octets of a Bridge Identifier are kept whole (802.1D-1998 made all 16 bits a priority) and split as 802.1t does by `root_priority_component`/`root_system_id_extension` and `bridge_priority_component`/`bridge_system_id_extension`; also `root_id_str`/`bridge_id_str` (Wireshark's priority/extension/MAC form), `port_priority`/`port_number`, `*_seconds` for the four timers, `bpdu_type_name` and `version_name`. The flag bits, port role values and field layout follow the unapproved IEEE 802.1 draft "P802.1aq/D1.5++suggested changes, May 16, 2009" (`aq-seaman-merged-bpdu-encoding-0509.pdf`, whose Clause 14 merges 802.1D Clause 9 with 802.1Q-2005 Clause 14), except that its §14.4 o) mislabels octets 34-35 as Max Age: they carry Forward Delay, as in Wireshark's `packet-bpdu.c` and Linux's `br_stp_bpdu.c`. The type and version names and port role names 1-3 are Wireshark's (`packet-bpdu.c`, fetched 2026-10-25); role 0, which Wireshark does not name, is `Unknown`, 802.1D-2004's name for it (802.1Q calls it Master; the draft's §14.2.9 NOTE). `header_len` is exactly the type's length: Ethernet padding and an FCS never reach the decoder in a walk (the 802.3 Length bounds the PDU), a direct `decode()` leaves them undecoded, and the octets of a PDU longer than its BPDU, which a receiver ignores (§14.4 and §14.6 i)3) of that draft), stay after the layer. Cisco PVST+'s SSTP BPDUs, the ones that carry TLVs, travel in SNAP under OUI 00-00-0C, PID 0x010B, not at DSAP 0x42 (Wireshark parses their TLVs only there), and walk as far as `SNAP`; an `llc.snap` entry at `(0x00000C << 16) | 0x010B` would reach `STP` from there. PVST+'s VLAN 1 BPDUs go untagged to the IEEE address, at DSAP 0x42, and decode as `STP` (Cisco does not say they carry a TLV; Wireshark parses PVST+ TLVs only under PID 0x010B) (Cisco document 24063, "Troubleshoot Spanning Tree PVID- and Type-Inconsistencies"; the full URL is in `layer2/stp.py`'s module docstring). A Protocol Identifier other than 0 (on a direct `STP.decode()`; a walk from LLC DSAP 0x42 does not reach it) and a BPDU Type other than the three raise `InvalidFieldError` (`field="protocol_id"`, offset 0, or `field="bpdu_type"`, offset 3), since neither leaves a layout to read; a buffer shorter than its type needs raises `TruncatedHeaderError` (`field="bpdu_type"`). The version, unused flag bits and a nonzero Version 1 Length are carried verbatim, and a type 0x02 BPDU is laid out as RST whatever its version. **MSTP is out of scope**: an MST BPDU (802.1s/802.1Q, version 3) decodes as its 36-octet RST prefix, and its Version 3 Length, MST configuration and MSTI messages are left undecoded after the layer, as a version 2 implementation reads them (§14.6 i), and §14.5 d) and e), of the draft). In a walk the 802.3 Length already ends the PDU, but kept raw in the layer they would need an end on a direct `decode()` too, where only the Version 3 Length could give one, and that field is unreliable on the wire (Wireshark's `packet-bpdu.c` notes a Cisco C3550 release that appears to send it in MSTI-message units, and MST BPDUs that carry a Version 3 Length of 0 and put the length in the format selector octet): read to the end of the buffer instead, they would take in padding and an FCS. In a walk a caller recovers them with `frame[packet.consumed : 14 + packet[Ethernet].length]` (behind one VLAN tag, `18 + packet[VLAN].length`). The real BPDUs are the corpus's 802.1D ones; RST BPDUs are tested with constructed frames only, since neither the kernel bridge nor any installable package here speaks RSTP.
|
|
18
|
+
- **`CDP`: the Cisco Discovery Protocol, versions 1 and 2 (#190).** SNAP OUI 00-00-0C PID 0x2000 now dispatches through `llc.snap` to `netprotocols.CDP` (also `netprotocols.layer2`), which decodes the 4-byte header (`version`, `ttl`, `checksum`) and the TLVs after it as `CDPTLV(type, value)`, each a 2-byte Type and a 2-byte Length that counts the 4-byte TLV header, kept raw in wire order with `type_name` from Wireshark's packet-cdp.c (commit 31bf0857, fetched 2026-10-25), so `bytes(CDP.decode(x)) == x[:header_len]`. Any Version decodes, since the SNAP header says outright that CDP follows. Accessors read the first TLV of their type and return `None` when it is missing or malformed: `device_id`, `port_id`, `software_version`, `platform` (ASCII, a byte outside it as a backslash escape), `capabilities` (the 32-bit mask) and `capability_names`, and `addresses` and `management_addresses` (type 22), lists of `CDPAddress` (protocol type, protocol, address, with `ip` for NLPID 0xCC IPv4 and the 802.2 form of EtherType 0x86DD IPv6), plus `tlv(type)`. CDP has no PDU length: in a walk the 802.3 Length bounds it, so padding and an FCS stay out. `decode` stops early without raising only where every byte left is zero, so zero padding is left undecoded when it is called directly; otherwise a TLV Length below 4 raises `InvalidFieldError` and 1-3 bytes left after the last TLV raise `TruncatedHeaderError`, in a walk too (both `field="tlvs"`, at the offset where the TLV would start), as does a TLV that runs past the buffer. Zeros after the last TLV but inside the 802.3 Length are part of the declared PDU, yet a walk leaves them undecoded without raising: `consumed` stopping short of the Length's end is the only structural signal, `verify()` (which sums the bytes decoded) fails on them only where the TLVs end at an odd offset on a nonzero byte and the sender summed the whole Length, and `scripts/check_fixtures.py` rejects such a frame. Wireshark's packet-cdp.c differs: it stops at a Length below 4 whatever follows, and marks 1-3 bytes left malformed even when they are zero. A nonzero trailer that happens to read as whole TLVs (a 4-byte FCS ending in 0x0004) decodes as phantom TLVs; there `verify()` is the check that the bytes decoded are the PDU the sender summed. Wireshark's Port ID length heuristic is not reproduced. The walk ends at CDP.
|
|
19
|
+
- **CDP checksums in `compute`/`verify`/`Packet.with_checksums()`.** The internet checksum of the whole PDU, checksum field zeroed, no pseudo-header (`ip` and `payload` ignored), except that an odd final byte `v` is added as the low byte of a last word below 0x80 and as `0xFF00 + v - 1` from 0x80, as lldpd 1.0.18's `frame_checksum()` and Wireshark's packet-cdp.c sum it; `internet_checksum()` alone gives the wrong value for an odd-length PDU whose final byte is not 0x00 (there the two coincide). `with_checksums()` fills it as it fills IPv4's.
|
|
20
|
+
- **`Ethernet.is_length`/`Ethernet.length` and `VLAN.is_length`/`VLAN.length`**: whether the raw `ethertype` field holds an 802.3 Length, and the Length or `None`. The field itself is unchanged in type and meaning.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **Ethernet and VLAN dispatch branch on type versus length.** A value of 0x0600 or more dispatches through `ethertype` exactly as before; 1500 or less answers `LLC` (it used to look the value up in `ethertype`, which accepted a key there: registering one, such as 0x0026, a Configuration BPDU's Length, was the only way to decode STP; see the Behaviour change entry below); 1501-1535, which IEEE 802.3 leaves undefined, answers `None`. GRE's Protocol Type and a SNAP PID are EtherTypes only and keep the plain lookup. No frame in the 2.5.0 corpus has a type/length value below 0x0600, and every one of them decodes exactly as before.
|
|
25
|
+
- **`decode_frame()` ends every later layer's buffer at the end of the LLC PDU an 802.3 Length gives**, so Ethernet zero padding and an FCS after a short PDU reach no decoder and stay past `packet.consumed`. A Length past the end of the frame bounds nothing (a frame a snap length cut decodes what was captured), a tighter bound is never widened by a later one, and a Length shorter than the 3-byte LLC header makes `LLC.decode()` raise `TruncatedHeaderError`. It is the only enclosing length the walk honours; IPv4's, IPv6's and UDP's are not (#235). The hook is private (`Protocol._BOUNDS_PAYLOAD`, `_payload_extent()`) while #235 settles a public form.
|
|
26
|
+
- **Behaviour change: an `ethertype` entry at 1500 or less is no longer reached from Ethernet or VLAN.** The table accepts any key, as in 2.5.0, but Ethernet and VLAN no longer look a type/length value of 1500 or less up there: they answer `LLC` for it, and nothing for 1501-1535 (see the entry above). So a class registered at, say, 0x0026 to decode STP over raw Ethernet is no longer reached from an Ethernet frame. GRE still looks any value up in `ethertype`, so such an entry still answers GRE Protocol Type 0x0026, as in 2.5.0, and the new `SNAP` looks a PID under OUI 00-00-00 or 00-00-F8 up there too, so it now also answers an RFC 1042 or 802.1H SNAP PID of 0x0026. RFC 1701's "Current List of Protocol Types" assigns GRE three values below 0x0600, SNA 0x0004, OSI network layer 0x00FE and PUP 0x0200, and an entry at 0x00FE is reached from GRE Protocol Type 0x00FE exactly as before. This dispatch change is inherent to #188's type/length reading. To migrate a raw-Ethernet decoder, register it in `llc.dsap` under its DSAP or, for a SNAP protocol, in `llc.snap` under `(oui << 16) | pid`. STP needs no migration: the built-in `STP` now holds DSAP 0x42, the one key that covers what 2.5.0 needed one key per 802.3 Length for (0x0026 for a Configuration BPDU, 0x0007 for a Topology Change Notification, 0x0027 for an RST BPDU, and more for MST BPDUs, whose Length varies); a decoder of one's own goes there with `override=True`, or in a registry of its own (`Registry.from_defaults().derive(...)`, or `decode_frame(decode_as=...)`), and is handed, like `STP`, only a PDU whose first two octets are 0x0000, or that holds fewer than two, never GARP's. The decoder is handed different bytes: in 2.5.0 it got everything from the LLC header to the end of the frame, padding and FCS included, and from `llc.dsap` it gets the PDU after the LLC header (from `llc.snap`, after the SNAP header), ending where the 802.3 Length does, so a migrated decoder must parse from there. The two new tables refuse a key their field cannot carry, `llc.dsap` one outside 0-0xFF and `llc.snap` one outside 0 to 2**40 - 1, raising the new `InvalidRegistryKeyError` (a `ProtocolError`, exported from `netprotocols` and `netprotocols.registry`) from `register()`, `register_all()`, `Registry.derive()` and `decode_frame(decode_as=...)` (whatever `lax` says). `register_all()` is not atomic, for this error as for a `RegistryConflictError`: the keys before the one that raises stay registered. Every other table, `ethertype` included, accepts any key, as before.
|
|
27
|
+
|
|
28
|
+
### Development
|
|
29
|
+
|
|
30
|
+
- **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.5.0's figures but on the 293-frame corpus Tier 3 grew from 265 (30 scenarios, from 27), against `dpkt 1.9.8` and `scapy 2.8.0`. The headline ratios held: dpkt decodes the corpus 1.33× faster (1.28-1.34× over three runs) and netprotocols stays 3.4× ahead of scapy. **The README's sub-claim that the 140 frames of 2.4.0's corpus "are still within 10%" of dpkt no longer holds and is replaced**: over sixteen alternating runs of a file-by-file harness, dpkt leads on them by a median 1.14× on this tree against 1.07× on 2.5.0's. Whether Tier 3's type-versus-length dispatch and payload bound cost the old frames anything is not resolved: netprotocols' median over the 265 frames both trees share is 3.3% lower than 2.5.0's (lower in 12 of 16 alternating pairs, with dpkt's own median flat), inside this machine's run-to-run spread of about ±8%, and it was not profiled ([#250](https://github.com/EONRaider/NETProtocols/issues/250)). On the new MPLS and LLC/SNAP captures the two libraries are level; dpkt does not decode LLDP, so it is about 5× faster on those six frames. Encode is 4.66× ahead of dpkt, import 89.8 ms (from 70.0 ms; the five new modules cost about 15 ms against the 2.5.0 tree run alternately on the same machine) against `scapy.all`'s 436.2 ms, and the wheel is 220.5 KiB, 12.2× smaller than scapy 2.8.0's 2.63 MiB. Decode depth against dpkt is 94 of 293 frames deeper; of the 15 new, 7 are frames dpkt leaves raw (the six LLDP frames and a VLAN-tagged 802.3 SNAP frame), and 8 are MPLS and SNAP frames on which netprotocols makes the label stack or the SNAP header a layer of its own where dpkt folds it into `Ethernet` or `LLC`; STP and CDP are decoded by dpkt too. dpkt cannot decode one frame, the 4-byte STP Topology Change Notification (`NeedData`), so `--depth` skips it and every comparison reports dpkt at 292/293. `docs/CLAIMS.md` records each run, the per-file table, the A/B and the frame-by-frame depth breakdown.
|
|
31
|
+
- **`scripts/check_fixtures.py` checks RST BPDUs and a Configuration BPDU's flags (#189).** Its independent `_check_bpdu` now knows the RST BPDU (type 0x02, 36 octets, a Protocol Version of 2 or more, which a receiver's validation requires) and counts each type (`stp-config`, `stp-rst`, `stp-tcn`). Its BPDU check covers only the DSAP 0x42 PDUs the library dispatches, UI (either P/F) and I-format; any other Control there (XID, TEST, S-format) is counted (`llc-dsap-0x42-control-0xaf`, say) and not checked. A UI or I-format PDU whose Protocol Identifier is not 0, such as GARP's, is counted (`llc-dsap-0x42-protocol-0x0001`) and not checked rather than failed as a malformed BPDU, and an I-format PDU's BPDU is read after its 2-octet Control, as the library reads it. It also holds each BPDU to what its sender must transmit, which a receiver ignores or tolerates: the Flags bits its type leaves unused 0 (six in a Configuration BPDU, bit 8 in an RST BPDU) and an RST BPDU's Version 1 Length 0. The `llc.pcap` BPDUs now decode to `STP`, and the corpus coverage test requires it; no frame is added, so the corpus counts are unchanged.
|
|
32
|
+
- **The three CDP frames of `llc.pcap` now decode to `CDP`**: the corpus coverage set, the corpus checksum cases (`tests/test_checksum.py`, in the mutation audit's selection) and the fuzz suites (`ALL_PROTOCOLS`, a round-trip strategy, accessor and trailer properties) cover it. No frame was added. `scripts/check_fixtures.py` keeps its own CDP check, which shares no code with the library.
|
|
33
|
+
- **An IEEE 802.3 LLC/SNAP capture joins the corpus: `llc.pcap`, 14 frames (293 across 30 scenarios).** `scripts/capture_fixtures_llc.sh` runs two Linux bridges with STP on in two netns and lldpd 1.0.18 in its own UTS namespace, and captures on a bridge port: five kernel-built 802.1D BPDUs (both bridges' Configuration BPDUs, the root's with TC and with TC + TCA, a 21-byte TCN), CDPv1 and CDPv2 from lldpd (and a CDPv1 frame whose odd-length PDU ends in 0xA9: CDP's checksum adds a final byte from 0x80 as 0xFF00 plus the byte less 1, as lldpd's `frame_checksum()` and Wireshark's packet-cdp.c do, and the frame shows it is not padded as RFC 1071 pads it and that the `- 1` matters; only a test, `test_cdp_odd_byte_is_lldpds_form`, tells that form from adding the byte less 0x100), and six frames the new stdlib-only `scripts/llc_fixture_frames.py` builds and sends through `AF_PACKET` (RFC 1042 ARP padded to 60 bytes and ICMP-in-IPv4, 802.1H ARP, RFC 1042 ARP behind an 802.1Q tag, Novell raw 802.3 IPX, and a type/length value of 1501). It keeps the first frame of each kind and asserts the composition exactly. `scripts/check_fixtures.py` now reads the type/length field as IEEE 802.3 does, after VLAN tags too, holds each Length against its frame (only zero padding may follow a PDU shorter than 46 bytes), checks the LLC Control width, SNAP framing, BPDU and raw IPX lengths, walks the CDP TLVs and verifies the CDP checksum over exactly the PDU, and checks an RFC 1042 or 802.1H payload as its EtherType, IPv4 and ICMP checksums included.
|
|
34
|
+
- **A real LLDP capture joins the corpus: `lldp.pcap`, 6 frames (279 across 29 scenarios).** `scripts/capture_fixtures_lldp.sh` runs two lldpd 1.0.18 agents, LLDP only, in two netns over a veth pair, each configured from its own file before its first LLDPDU: A with its MAC address as Chassis ID, its interface name as Port ID and a 304-byte custom organizationally specific TLV (which needs the 9th Length bit), B with a locally assigned Chassis ID, its MAC address as Port ID and a TTL of 180. It then changes A's Port ID to a locally assigned value at run time, which makes lldpd send a shutdown LLDPDU for the old one, and stops both agents, which send one each. It keeps the first LLDPDU of each kind (sender, Port ID subtype, shutdown or not) and asserts the six exactly: their Chassis ID, Port ID, TTL and TLV type sequence. `scripts/check_fixtures.py` checks LLDPDUs structurally, since LLDP has no checksum: the TLVs must walk to an empty End TLV with only zero bytes after it, the first three must be a well-formed Chassis ID, Port ID and 2-byte TTL that do not recur, and a shutdown LLDPDU must carry nothing else; its ARP and fallback arms move into a `_check_non_ip` helper so `_check_l3` does not grow. `tests/test_lldp.py` holds the checker to failing tampered copies. `LLDP` joins the fuzz suite (`ALL_PROTOCOLS`, a round-trip strategy, an accessor-safety property fed LLDP-shaped buffers, and a padding and trailer property), the truncation and lying-length contract tests and the corpus coverage set; a decode-cost test counts the bytes a 10,000-TLV LLDPDU's walk reads by slicing or copying it whole, and another, with `tracemalloc`, checks that decoding a 1,000-TLV LLDPDU followed by a megabyte after its End TLV, as `bytes` and as a `memoryview`, peaks under half a megabyte of allocation, which a copy of the rest of the buffer at any TLV, over a megabyte, would exceed. Neither bounds per-TLV quadratic work that leaves no large allocation behind, such as rebuilding the TLV list at every TLV.
|
|
35
|
+
- **A captured MPLS fixture joins the corpus: `mpls.pcap`, 8 frames (273 across 28 scenarios).** The capture host's kernel has `CONFIG_MPLS_ROUTING` off, so `scripts/capture_fixtures_mpls.sh` has the kernel send two ICMP echo requests and two UDP datagrams over a veth pair between two netns, offload off, captures them inbound, and then `scripts/mpls_fixture_packets.py` (stdlib only) wraps those datagrams in script-built Ethernet headers and label stacks and sends them through an `AF_PACKET` socket: one label, two labels, three labels with distinct traffic classes, an Entropy Label Indicator and entropy label, IPv4 and IPv6 Explicit NULL bottom labels (the IPv6 datagram script-built, since the host has IPv6 disabled; the IPv4 Explicit NULL frame zero padded to 60 bytes), EtherType 0x8848 to a multicast MAC, and an Ethernet pseudowire with a control word. The script reads the capture back and fails unless every byte arrived as built, and the capture script fails unless the capture holds exactly those eight frames. A second tcpdump captures the same frames with a 20-byte snap length as `tests/fixtures/mpls/mpls_snaplen.pcap`, outside the corpus globs like `bgp/bgp_session.pcap`, so truncation is tested on captured frames too: six stacks cut after their first entry, and two whole one-entry stacks whose IPv4 header is cut. `scripts/check_fixtures.py` walks each label stack to its bottom entry in its own code and verifies the labelled packet's checksums, IPv4 or IPv6 by the same convention, and the Ethernet frame after the pseudowire's control word; its ARP branch moved into a `_check_arp` helper beside the new `_check_mpls`, so `_check_l3` dispatches the two through a table and stays at radon B. `tests/test_mpls.py` holds the checker to failing a flipped byte in each frame's labelled packet. MPLS joins the fuzz suite (`ALL_PROTOCOLS`, a round-trip strategy, accessor-safety and trailer properties), the truncation and memoryview contract tests and the corpus coverage set.
|
|
36
|
+
- **The `register()` examples no longer use MPLS's EtherType.** `registry.py`, README.md, ARCHITECTURE.md and `docs/CLAIMS.md` §5.5 registered a hypothetical class at 0x8847 as "a protocol this library does not implement", which now raises `RegistryConflictError`; they, and `tests/test_registry.py`'s stand-ins, use 0x88B5 ("IEEE Std 802 - Local Experimental Ethertype" in IANA's registry) and a `Telemetry` class instead, and `tests/test_docs.py` runs each document's example, and the three in `registry.py`'s docstrings, to check each registers on a free key. README's "Why NETProtocols" now names the corpus its figures were measured on, which this release's re-measurement (the first Development entry) makes the 293 frames the corpus holds at 2.6.0.
|
|
37
|
+
|
|
10
38
|
## [2.5.0] - 2026-10-25
|
|
11
39
|
|
|
12
40
|
### Added
|
|
@@ -38,11 +38,14 @@ is 80; Markdown is excluded from linting.
|
|
|
38
38
|
```
|
|
39
39
|
src/netprotocols/
|
|
40
40
|
_base.py the Protocol base class and the decode contract
|
|
41
|
-
_enums.py EtherType / IPProtocol / ARP* registries +
|
|
41
|
+
_enums.py EtherType / IPProtocol / LLCSAP / ARP* registries +
|
|
42
|
+
display names
|
|
42
43
|
checksum.py RFC 1071 internet checksum, SCTP's CRC32c, the
|
|
43
44
|
Fletcher checksum of OSPF's LSAs, compute()/verify()
|
|
45
|
+
(CDP's odd-byte variant among them)
|
|
44
46
|
packet.py Packet: an ordered stack of headers, with_checksums()
|
|
45
|
-
layer2/ Ethernet, VLAN, ARP
|
|
47
|
+
layer2/ Ethernet, VLAN, ARP, LLC + SNAP (802.2), STP (802.1D),
|
|
48
|
+
LLDP, MPLS, CDP
|
|
46
49
|
layer3/ IPv4, IPv6, IPv6 extension headers, ICMPv4/v6, IGMP,
|
|
47
50
|
GRE, IPsec ESP and AH, VRRP, OSPFv2, PIM
|
|
48
51
|
layer4/ TCP, UDP, SCTP (+ port-based dispatch)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: netprotocols
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.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,33 +76,39 @@ 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
|
|
80
|
-
|
|
81
|
-
2.
|
|
82
|
-
|
|
79
|
+
Measured against `dpkt 1.9.8` and `scapy 2.8.0` on the 293 frames of
|
|
80
|
+
real captured traffic the corpus held at 2.6.0 (CPython 3.12.3, x86-64
|
|
81
|
+
Linux; re-measured for 2.6.0 on 2026-10-28 — see
|
|
82
|
+
[docs/CLAIMS.md](docs/CLAIMS.md) for every number below, its
|
|
83
|
+
reproduction command, and its caveats):
|
|
83
84
|
|
|
84
85
|
- **3.4× faster than scapy on decode; dpkt is 1.3× faster** —
|
|
85
86
|
`uv run --group bench python scripts/benchmark.py --compare`. dpkt is
|
|
86
|
-
the faster of the two comparators.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
87
|
+
the faster of the two comparators. The routing captures 2.5.0 added
|
|
88
|
+
(OSPF, BGP, PIM, RIP, VRRP) are where it pulls ahead, taking about
|
|
89
|
+
two thirds of netprotocols' time on them. On the 140 frames the
|
|
90
|
+
corpus held at 2.4.0 the two are no longer within 10% of each other:
|
|
91
|
+
dpkt is ahead there by a median 1.14× across runs. On the MPLS and
|
|
92
|
+
LLC/SNAP captures 2.6.0 added they are level; dpkt leaves LLDP
|
|
93
|
+
undecoded, so it is about 5× faster on those six frames. Narrowing
|
|
94
|
+
the gap is
|
|
95
|
+
[#250](https://github.com/EONRaider/NETProtocols/issues/250). The gap
|
|
96
|
+
moves both ways as this library and dpkt each change, and this file's
|
|
97
|
+
job is to say so honestly rather than only when it's flattering.
|
|
98
|
+
- **Decodes further into the stack than dpkt on 94 of those 293
|
|
99
|
+
frames** — BGP, RIP, LLDP, DNS, DHCP and VXLAN-tunnelled payloads
|
|
100
|
+
that dpkt leaves as raw bytes, and 802.1Q tags, MPLS label stacks and
|
|
101
|
+
SNAP headers it folds into the Ethernet and LLC headers, netprotocols
|
|
102
|
+
decodes as layers of their own. A throughput number means little
|
|
103
|
+
without knowing how much work it bought.
|
|
104
|
+
- **4.7× faster than dpkt at re-encoding** (`bytes(header)`,
|
|
99
105
|
`scripts/benchmark_encode.py`) — the other half of "codec" that
|
|
100
106
|
nobody else benchmarks.
|
|
101
|
-
- **Imports in
|
|
107
|
+
- **Imports in 90 ms; `scapy.all` takes 436+ ms** (`scripts/benchmark_import.py`),
|
|
102
108
|
and never touches the host doing it — importing scapy populates live
|
|
103
109
|
interface and routing tables as a side effect of the import
|
|
104
110
|
statement.
|
|
105
|
-
- **A
|
|
111
|
+
- **A 220.5 KiB wheel against scapy's 2.63 MiB** — 12.2× smaller.
|
|
106
112
|
- **Regression-gated in CI, which — as far as we could establish by
|
|
107
113
|
auditing ten comparable Python packet libraries' CI configurations
|
|
108
114
|
(dpkt, scapy, pypacker, construct, pcapkit, dnspython, pyshark,
|
|
@@ -164,9 +170,15 @@ ip.proto → error: "IPv4" has no attribute
|
|
|
164
170
|
|
|
165
171
|
| Layer | Protocol | Class | Notes |
|
|
166
172
|
|---|---|---|---|
|
|
167
|
-
| 2 | Ethernet II | `Ethernet` | IEEE 802.3 |
|
|
168
|
-
| 2 | IEEE 802.1Q VLAN tag (802.1ad QinQ) | `VLAN` | 802.1Q-2018 §9.6, PCP/DEI/VID, tag stacking |
|
|
173
|
+
| 2 | Ethernet II / IEEE 802.3 | `Ethernet` | IEEE 802.3; the type/length field dispatches as an EtherType from 0x0600 and as an 802.3 Length (then LLC) up to 1500, which also bounds the walk's later layers |
|
|
174
|
+
| 2 | IEEE 802.1Q VLAN tag (802.1ad QinQ) | `VLAN` | 802.1Q-2018 §9.6, PCP/DEI/VID, tag stacking; the inner type/length field reads as Ethernet's does |
|
|
175
|
+
| 2 | IEEE 802.2 LLC | `LLC` | ISO/IEC 8802-2, DSAP/SSAP and a 1- or 2-octet Control; I-format and UI PDUs dispatch on DSAP (`llc.dsap`); Novell raw 802.3 recognized (`is_raw_ipx`; the other fields are then IPX bytes), never dispatched |
|
|
176
|
+
| 2 | SNAP | `SNAP` | IEEE Std 802, RFC 1042; OUI and PID: OUI 00-00-00 and 00-00-F8 dispatch the PID as an EtherType, any other OUI through `llc.snap` |
|
|
177
|
+
| 2 | LLDP | `LLDP`, `LLDPTLV`, `LLDPChassisID`, `LLDPPortID` | IEEE 802.1AB, EtherType 0x88CC; every TLV through End of LLDPDU, the End TLV's bound leaving padding and an FCS undecoded; Chassis ID and Port ID (subtype and ID) and Time To Live typed, every other TLV raw with its type named |
|
|
178
|
+
| 2 | Spanning Tree BPDU (STP, RSTP) | `STP` | IEEE 802.1D-2004 §9.3, 802.1w; LLC DSAP 0x42 (`llc.dsap`); Configuration (35 octets), RST (36) and Topology Change Notification (4) BPDUs in one class, branching on the BPDU Type; per-type flag accessors (TC, TCA, Proposal, Port Role, Learning, Forwarding, Agreement), the 802.1t priority / system ID extension split, timers raw and in seconds; MST BPDUs decoded only as far as their RST prefix |
|
|
179
|
+
| 2 | CDP | `CDP` | Cisco Discovery Protocol, versions 1 and 2, after SNAP OUI 00-00-0C PID 0x2000 (`llc.snap`); the 4-byte header and the TLVs, kept raw and named from Wireshark's packet-cdp.c, with accessors for Device ID, Addresses, Port ID, Capabilities, Software version, Platform and Management Address; its checksum (RFC 1071 but for an odd final byte) in `compute`/`verify` |
|
|
169
180
|
| 2 | ARP | `ARP` | RFC 826, IPv4-over-Ethernet binding |
|
|
181
|
+
| 2 | MPLS label stack | `MPLS` | RFC 3032 §2.1, EtherTypes 0x8847 and 0x8848 (and GRE's Protocol Type, RFC 4023); the whole stack as one layer of `MPLSLabel` entries (label, traffic class, S, TTL); no field names the payload, so it dispatches by convention to IPv4 or IPv6 — the bottom label when it is an Explicit NULL, otherwise the first nibble after the stack — and stops at a pseudowire control word |
|
|
170
182
|
| 3 | IPv4 | `IPv4` | RFC 791, IHL/options aware |
|
|
171
183
|
| 3 | IPv6 | `IPv6` | RFC 8200 |
|
|
172
184
|
| 3 | IPv6 Hop-by-Hop Options | `IPv6HopByHopOptions` | RFC 8200 §4.3 |
|
|
@@ -427,13 +439,14 @@ implement can join the walk without forking it:
|
|
|
427
439
|
from netprotocols import Protocol
|
|
428
440
|
from netprotocols.registry import register
|
|
429
441
|
|
|
430
|
-
@register("ethertype",
|
|
431
|
-
class
|
|
442
|
+
@register("ethertype", 0x88B5) # IEEE 802 Local Experimental
|
|
443
|
+
class Telemetry(Protocol):
|
|
432
444
|
...
|
|
433
445
|
```
|
|
434
446
|
|
|
435
|
-
The
|
|
436
|
-
and `tcp.port`, each named after the wire
|
|
447
|
+
The seven tables are `ethertype`, `llc.dsap`, `llc.snap`, `ip.proto`,
|
|
448
|
+
`ip.proto.v6`, `udp.port` and `tcp.port`, each named after the wire
|
|
449
|
+
field it dispatches on. To
|
|
437
450
|
change decoding for one call only — DNS on a nonstandard port in one
|
|
438
451
|
capture — say so per call instead of registering globally:
|
|
439
452
|
|
|
@@ -466,7 +479,8 @@ raw = bytes(frame) # ready for a raw socket
|
|
|
466
479
|
Checksums are computed and verified on request — never silently:
|
|
467
480
|
`compute()`/`verify()` in `netprotocols.checksum` (the internet
|
|
468
481
|
checksum, VRRP's with or without its pseudo-header by version, PIM's,
|
|
469
|
-
|
|
482
|
+
CDP's with its odd final byte summed as Cisco does, and SCTP's CRC32c
|
|
483
|
+
via `crc32c()`; OSPF's LSA checksums via
|
|
470
484
|
`lsa_checksum()`/`verify_lsa()`), and
|
|
471
485
|
`Packet.with_checksums()` to fill a whole stack before sending.
|
|
472
486
|
|
|
@@ -476,7 +490,7 @@ Every class exposes human-readable properties next to the raw fields:
|
|
|
476
490
|
`Ethernet.ethertype_name`, `IPv4.protocol_name`, `IPv4.flags_name`,
|
|
477
491
|
`TCP.flags_str` (`"SYN ACK"`), `ARP.oper_name`, `ICMPv4.type_name`,
|
|
478
492
|
and hexadecimal renderings such as `checksum_hex_str`. Unknown values
|
|
479
|
-
degrade gracefully (`"
|
|
493
|
+
degrade gracefully (`"0x88b5"`, `"unknown (47)"`) instead of raising.
|
|
480
494
|
|
|
481
495
|
## How it works
|
|
482
496
|
|
|
@@ -512,7 +526,13 @@ and the second — VRRP, RIP, OSPF, PIM and BGP
|
|
|
512
526
|
[#245](https://github.com/EONRaider/NETProtocols/pull/245),
|
|
513
527
|
[#246](https://github.com/EONRaider/NETProtocols/pull/246),
|
|
514
528
|
[#247](https://github.com/EONRaider/NETProtocols/pull/247),
|
|
515
|
-
[#248](https://github.com/EONRaider/NETProtocols/pull/248)) — as 2.5.0
|
|
529
|
+
[#248](https://github.com/EONRaider/NETProtocols/pull/248)) — as 2.5.0,
|
|
530
|
+
and the third — MPLS, LLDP, LLC/SNAP, STP and CDP
|
|
531
|
+
([#252](https://github.com/EONRaider/NETProtocols/pull/252),
|
|
532
|
+
[#253](https://github.com/EONRaider/NETProtocols/pull/253),
|
|
533
|
+
[#254](https://github.com/EONRaider/NETProtocols/pull/254),
|
|
534
|
+
[#255](https://github.com/EONRaider/NETProtocols/pull/255),
|
|
535
|
+
[#256](https://github.com/EONRaider/NETProtocols/pull/256)) — as 2.6.0
|
|
516
536
|
(see [CHANGELOG.md](CHANGELOG.md)).
|
|
517
537
|
|
|
518
538
|
The roadmap before it, the post-1.3.0
|
|
@@ -539,7 +559,7 @@ released as
|
|
|
539
559
|
2.1.0's and 1.3.1/1.4.0's work shipped folded into 2.0.0 and 2.2.0
|
|
540
560
|
rather than as their own versions, but the roadmap's original "only at
|
|
541
561
|
a major version bump" release policy wasn't actually held to: 2.0.0,
|
|
542
|
-
2.2.0, 2.2.1, 2.3.0, 2.4.0 and now 2.
|
|
562
|
+
2.2.0, 2.2.1, 2.3.0, 2.4.0, 2.5.0 and now 2.6.0 are all on PyPI, and the comparative claims above
|
|
543
563
|
draw on the finished tree. The wave before that one — TCP
|
|
544
564
|
and IPv4 option parsing, ICMP message bodies, NDP, IPv6
|
|
545
565
|
extension-header TLV options, the GRE checksum arm, a DNS-over-TCP
|
|
@@ -552,7 +572,7 @@ Development uses [uv](https://docs.astral.sh/uv/): `uv sync`, then
|
|
|
552
572
|
`uv run pytest`, `uv run mypy`, and `uv run ruff check` — all three are
|
|
553
573
|
enforced by CI; the test suite runs across Python 3.12–3.14, while
|
|
554
574
|
`mypy` and `ruff check` run on 3.12. The test suite is anchored by a
|
|
555
|
-
|
|
575
|
+
293-frame corpus of real captured traffic across 30 scenarios
|
|
556
576
|
([tests/fixtures/MANIFEST.md](tests/fixtures/MANIFEST.md), which says
|
|
557
577
|
which layers a script built rather than the kernel) plus
|
|
558
578
|
property-based fuzzing of the decode path. See
|