netprotocols 2.0.0__tar.gz → 2.2.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.0.0 → netprotocols-2.2.0}/.github/workflows/ci.yml +23 -0
- netprotocols-2.2.0/.github/workflows/fuzz.yml +64 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/.gitignore +5 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/ARCHITECTURE.md +26 -7
- {netprotocols-2.0.0 → netprotocols-2.2.0}/CHANGELOG.md +208 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/PKG-INFO +127 -11
- {netprotocols-2.0.0 → netprotocols-2.2.0}/README.md +126 -10
- {netprotocols-2.0.0 → netprotocols-2.2.0}/docs/CLAIMS.md +300 -171
- {netprotocols-2.0.0 → netprotocols-2.2.0}/pyproject.toml +1 -1
- netprotocols-2.2.0/scripts/benchmark_encode.py +107 -0
- netprotocols-2.2.0/scripts/benchmark_import.py +63 -0
- netprotocols-2.2.0/scripts/pyodide/check_in_pyodide.py +137 -0
- netprotocols-2.2.0/scripts/pyodide/package-lock.json +53 -0
- netprotocols-2.2.0/scripts/pyodide/package.json +9 -0
- netprotocols-2.2.0/scripts/pyodide/run_in_pyodide.mjs +44 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/__init__.py +26 -3
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/_base.py +79 -4
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/layer2/arp.py +48 -3
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/layer2/ethernet.py +11 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/layer2/vlan.py +14 -1
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/layer3/gre.py +11 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/layer3/ip.py +117 -5
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/layer3/ipv6_ext.py +104 -6
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/layer4/tcp.py +7 -0
- netprotocols-2.2.0/src/netprotocols/layer7/__init__.py +20 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/layer7/dhcp.py +138 -5
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/layer7/dns.py +199 -17
- netprotocols-2.2.0/src/netprotocols/pcap.py +504 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/utils/exceptions.py +18 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/conftest.py +13 -21
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_arp.py +16 -2
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_corpus.py +8 -8
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_dhcp.py +117 -3
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_dns.py +91 -2
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_ethernet.py +3 -1
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_fuzz.py +115 -4
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_gre.py +9 -6
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_igmp.py +2 -2
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_ip.py +163 -2
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_ipv6_ext.py +135 -3
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_pattern_matching.py +93 -8
- netprotocols-2.2.0/tests/test_pcap.py +367 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_vlan.py +3 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/uv.lock +1 -1
- netprotocols-2.0.0/src/netprotocols/layer7/__init__.py +0 -4
- {netprotocols-2.0.0 → netprotocols-2.2.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/.github/ISSUE_TEMPLATE/protocol_request.yml +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/.github/pull_request_template.md +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/.github/workflows/release.yml +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/CONTRIBUTING.md +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/LICENSE +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/benchmarks/baseline.json +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/scripts/benchmark.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/scripts/capture_fixtures.sh +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/scripts/capture_fixtures_dhcp.sh +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/scripts/capture_fixtures_dns_tcp.sh +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/scripts/capture_fixtures_gre.sh +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/scripts/capture_fixtures_igmp.sh +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/scripts/capture_fixtures_supplement.sh +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/scripts/capture_fixtures_vlan.sh +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/scripts/check_fixtures.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/_defaults.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/_enums.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/checksum.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/flow.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/layer2/__init__.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/layer3/__init__.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/layer3/icmp.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/layer3/igmp.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/layer4/__init__.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/layer4/_ports.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/layer4/udp.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/packet.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/py.typed +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/registry.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/utils/__init__.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/utils/ipv4.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/utils/mac.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/src/netprotocols/walk.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/MANIFEST.md +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/arp_exchange.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/dhcp.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/dns_tcp.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/gre.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/icmpv4_echo_lo.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/icmpv4_external.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/icmpv4_ttl_exceeded.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/icmpv6_echo_lo.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/igmp.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/ipv4_fragments.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/ipv6_fragments.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/ipv6_mld.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/ipv6_ndp_mld.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/staging/arp_exchange.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/staging/dhcp.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/staging/dns_tcp.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/staging/gre.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/staging/icmpv4_echo_lo.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/staging/icmpv4_external.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/staging/icmpv4_ttl_exceeded.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/staging/icmpv6_echo_lo.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/staging/igmp.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/staging/ipv4_fragments.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/staging/ipv6_fragments.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/staging/ipv6_mld.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/staging/ipv6_ndp_mld.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/staging/tcp_http.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/staging/tcp_https.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/staging/udp_dns.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/staging/vlan_icmp.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/tcp_http.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/tcp_https.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/udp_dns.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/fixtures/vlan_icmp.pcap +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/strategies.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_checksum.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_contract.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_diagnostics.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_docs.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_exports.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_flow.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_icmp.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_packet.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_registry.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_tcp.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_udp.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_utils.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_version.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_walk.py +0 -0
- {netprotocols-2.0.0 → netprotocols-2.2.0}/tests/test_workflows.py +0 -0
|
@@ -50,6 +50,29 @@ jobs:
|
|
|
50
50
|
- run: uv build
|
|
51
51
|
- run: uv run --isolated --no-project --with dist/*.whl python -c "import netprotocols; print(netprotocols.__version__)"
|
|
52
52
|
|
|
53
|
+
# Proves the README's browser/WASM claim (#99) against the real
|
|
54
|
+
# thing, not a simulation of it: boots an actual Pyodide runtime
|
|
55
|
+
# (scripts/pyodide/run_in_pyodide.mjs, via Node — no browser needed,
|
|
56
|
+
# same WebAssembly build either way) and decodes the whole
|
|
57
|
+
# real-capture corpus with the wheel this job just built. See
|
|
58
|
+
# scripts/pyodide/check_in_pyodide.py for what it checks and why.
|
|
59
|
+
pyodide:
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
steps:
|
|
62
|
+
- uses: actions/checkout@v5
|
|
63
|
+
- uses: astral-sh/setup-uv@v6
|
|
64
|
+
with:
|
|
65
|
+
python-version: "3.12"
|
|
66
|
+
- run: uv build
|
|
67
|
+
- uses: actions/setup-node@v4
|
|
68
|
+
with:
|
|
69
|
+
node-version: "22"
|
|
70
|
+
- run: npm ci
|
|
71
|
+
working-directory: scripts/pyodide
|
|
72
|
+
- run: >-
|
|
73
|
+
node scripts/pyodide/run_in_pyodide.mjs
|
|
74
|
+
"$GITHUB_WORKSPACE" scripts/pyodide/check_in_pyodide.py
|
|
75
|
+
|
|
53
76
|
# Blocking, at 15% below the committed baseline. The threshold has
|
|
54
77
|
# runner data behind it: this job's first run measured 3.7% away from
|
|
55
78
|
# a baseline recorded on entirely different hardware, and run-to-run
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: Nightly fuzz
|
|
2
|
+
|
|
3
|
+
# Separate from ci.yml on purpose: ci.yml is reused via workflow_call
|
|
4
|
+
# by release.yml specifically so the release gate and the PR gate never
|
|
5
|
+
# drift apart (see the comment at the top of ci.yml). Folding a
|
|
6
|
+
# schedule trigger into it would fire that whole ladder on a cron too,
|
|
7
|
+
# not just this exploratory job.
|
|
8
|
+
on:
|
|
9
|
+
schedule:
|
|
10
|
+
# Arbitrary; 03:00 UTC lands after most contributors' working day
|
|
11
|
+
# across common timezones, clear of any push/PR traffic.
|
|
12
|
+
- cron: "0 3 * * *"
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
# tests/test_fuzz.py's own "netprotocols" profile is deliberately
|
|
20
|
+
# derandomize=True at 200 examples, so CI has run the *same* 200
|
|
21
|
+
# inputs on every push since it was written. This job runs the whole
|
|
22
|
+
# suite under the "nightly" profile instead (10,000 examples, a real
|
|
23
|
+
# random seed each time — see tests/test_fuzz.py's module docstring),
|
|
24
|
+
# so it explores new ground every run rather than replaying the same
|
|
25
|
+
# counterexample search forever. Pinned to Python 3.12 only, unlike
|
|
26
|
+
# the "test" job's full matrix: this is an exploratory job, not a
|
|
27
|
+
# compatibility gate, and 3x the interpreters would 3x its cost for
|
|
28
|
+
# no extra coverage of what it is actually looking for.
|
|
29
|
+
fuzz:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v5
|
|
33
|
+
- uses: astral-sh/setup-uv@v6
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.12"
|
|
36
|
+
# Keyed by run id with a prefix restore-key: a fixed key would
|
|
37
|
+
# restore forever but (actions/cache never overwrites an existing
|
|
38
|
+
# key) never actually save a new one, so nothing would
|
|
39
|
+
# accumulate. This restores the most recent run's cache and saves
|
|
40
|
+
# today's as a new entry, so Hypothesis's example database grows
|
|
41
|
+
# night over night instead of starting cold every time.
|
|
42
|
+
- name: Restore accumulated Hypothesis examples
|
|
43
|
+
uses: actions/cache@v4
|
|
44
|
+
with:
|
|
45
|
+
path: .hypothesis
|
|
46
|
+
key: hypothesis-nightly-${{ github.run_id }}
|
|
47
|
+
restore-keys: |
|
|
48
|
+
hypothesis-nightly-
|
|
49
|
+
- name: Run the full suite under the nightly fuzz profile
|
|
50
|
+
env:
|
|
51
|
+
HYPOTHESIS_PROFILE: nightly
|
|
52
|
+
run: uv run --frozen pytest
|
|
53
|
+
# A failed scheduled run already notifies repo watchers by
|
|
54
|
+
# default (GitHub's standard behavior) — that red run *is* the
|
|
55
|
+
# report; nothing here files an issue on top of it. This just
|
|
56
|
+
# makes the minimal counterexample recoverable without needing
|
|
57
|
+
# repo write access to re-run the job locally.
|
|
58
|
+
- name: Upload the failing example database
|
|
59
|
+
if: failure()
|
|
60
|
+
uses: actions/upload-artifact@v4
|
|
61
|
+
with:
|
|
62
|
+
name: hypothesis-nightly-failure-${{ github.run_id }}
|
|
63
|
+
path: .hypothesis
|
|
64
|
+
retention-days: 30
|
|
@@ -150,3 +150,8 @@ amass.log
|
|
|
150
150
|
|
|
151
151
|
# Written by the benchmark CI step (scripts/benchmark.py output)
|
|
152
152
|
benchmark.out
|
|
153
|
+
|
|
154
|
+
# npm dependency (the pyodide package itself) for scripts/pyodide/ —
|
|
155
|
+
# CI installs it fresh from package-lock.json, same as any other
|
|
156
|
+
# lockfile-pinned dependency.
|
|
157
|
+
scripts/pyodide/node_modules/
|
|
@@ -81,8 +81,20 @@ match ip:
|
|
|
81
81
|
declaration order — `Ethernet.__match_args__ ==
|
|
82
82
|
('dst', 'src', 'ethertype')` — so a *positional* class pattern
|
|
83
83
|
(`case Ethernet(dst, src, ethertype)`) works with no extra code. An
|
|
84
|
-
ordinary class has to declare `__match_args__` by hand to get that
|
|
85
|
-
|
|
84
|
+
ordinary class has to declare `__match_args__` by hand to get that.
|
|
85
|
+
Most headers here never do either — but a few of the widest ones
|
|
86
|
+
are a deliberate exception: `IPv4`, `TCP` and `DHCP` run to 14, 11
|
|
87
|
+
and 15 fields respectively, so their full auto-generated tuple is
|
|
88
|
+
unusable positionally (nobody writes, or gets right, a
|
|
89
|
+
fourteen-slot pattern). Those three hand-declare a short
|
|
90
|
+
`__match_args__` instead — `IPv4.__match_args__ ==
|
|
91
|
+
('src', 'dst', 'protocol')` — the two or three fields someone
|
|
92
|
+
drafting a positional pattern actually reaches for. The override is
|
|
93
|
+
documented at each class and pinned by
|
|
94
|
+
`tests/test_pattern_matching.py`, so it cannot drift the way an
|
|
95
|
+
*accidental* override would; keyword patterns
|
|
96
|
+
(`case IPv4(protocol=6)`) are unaffected by any of this and stay
|
|
97
|
+
the documented default (#94).
|
|
86
98
|
2. **Every field with a fixed vocabulary of wire values is an
|
|
87
99
|
`IntEnum`**, defined in [`_enums.py`](src/netprotocols/_enums.py),
|
|
88
100
|
and `IntEnum` subclasses `int`. A *value* pattern therefore matches
|
|
@@ -107,11 +119,16 @@ there.)
|
|
|
107
119
|
**Keeping this working is a contributor obligation, not a given.**
|
|
108
120
|
Field declaration order is part of a class's public API — reordering
|
|
109
121
|
fields changes what a positional pattern binds to, silently, for
|
|
110
|
-
anyone matching by position.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
anyone matching by position. (The curated three are the exception:
|
|
123
|
+
`IPv4`/`TCP`/`DHCP`'s hand-declared `__match_args__` is independent of
|
|
124
|
+
field order, so reordering *their* fields cannot break positional
|
|
125
|
+
matching — but adding or renaming one of the fields the curated tuple
|
|
126
|
+
names does need the tuple, and the test pinning it, updated by hand.)
|
|
127
|
+
And a field carrying a closed set of wire values should stay an
|
|
128
|
+
`IntEnum` sourced from `_enums.py` rather than degrade to a bare
|
|
129
|
+
`int`; the moment it does, `case Header(field=SomeEnum.X)` stops
|
|
130
|
+
matching and fails silently (the pattern falls through to the next
|
|
131
|
+
`case`, it does not raise) rather than loudly.
|
|
115
132
|
|
|
116
133
|
## The decode contract
|
|
117
134
|
|
|
@@ -387,6 +404,8 @@ src/netprotocols/
|
|
|
387
404
|
├── packet.py Packet composition, with_checksums(), flow_key()
|
|
388
405
|
├── checksum.py RFC 1071: internet_checksum, compute, verify
|
|
389
406
|
├── flow.py FlowKey, flow_key(): canonical bidirectional keys
|
|
407
|
+
├── pcap.py read_captures()/read_pcap()/read_pcapng(): classic
|
|
408
|
+
│ pcap and pcapng readers, from bytes not filenames
|
|
390
409
|
├── layer2/ ethernet.py, arp.py, vlan.py (802.1Q / 802.1ad)
|
|
391
410
|
├── layer3/ ip.py (IPv4 + IPv6), icmp.py (ICMPv4 + ICMPv6),
|
|
392
411
|
│ igmp.py, gre.py,
|
|
@@ -5,6 +5,214 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.2.0] - 2026-09-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Typed accessors for every enum-backed field.** `_enums.py` defines
|
|
12
|
+
`EtherType`, `IPProtocol`, `ARPOperation` and `ARPHardwareType`, but
|
|
13
|
+
until now no decoded field exposed them — only the `*_name` display
|
|
14
|
+
strings, and the enums themselves were used only for dispatch. Every
|
|
15
|
+
field with a fixed wire vocabulary now has an `*_enum` companion
|
|
16
|
+
alongside the existing `int` field and its `*_name` string, mirroring
|
|
17
|
+
the `src`/`src_address` precedent 1.3.0 established for IP addresses:
|
|
18
|
+
`Ethernet.ethertype_enum`, `VLAN.ethertype_enum`, `GRE.protocol_enum`,
|
|
19
|
+
`IPv4.protocol_enum`, `IPv6.next_header_enum` (and the three IPv6
|
|
20
|
+
extension headers that share the field: `IPv6HopByHopOptions`,
|
|
21
|
+
`IPv6DestinationOptions`, `IPv6Routing`, `IPv6Fragment`),
|
|
22
|
+
`ARP.oper_enum`, `ARP.ptype_enum`, `ARP.htype_name`/`ARP.htype_enum`
|
|
23
|
+
(new — `ARPHardwareType` was exported but referenced nowhere), and
|
|
24
|
+
`DHCP.htype_name`/`DHCP.htype_enum`. Each returns `None` — never
|
|
25
|
+
raises — for a wire value this library does not enumerate, so
|
|
26
|
+
`bytes(decode(x)) == x` is unaffected for unrecognized values; the
|
|
27
|
+
raw `int` field stays canonical and round-trips regardless (#95).
|
|
28
|
+
- **IPv4, TCP and DHCP curate a short `__match_args__` by hand.** Their
|
|
29
|
+
full auto-generated positional form runs to 11-15 fields — unusable
|
|
30
|
+
positionally, since nobody writes, or gets right, a fourteen-slot
|
|
31
|
+
pattern. `IPv4.__match_args__ == ('src', 'dst', 'protocol')`,
|
|
32
|
+
`TCP.__match_args__ == ('src_port', 'dst_port', 'flags')`,
|
|
33
|
+
`DHCP.__match_args__ == ('op', 'chaddr', 'yiaddr')` — the two or
|
|
34
|
+
three fields someone drafting `case IPv4(...)` actually reaches for.
|
|
35
|
+
Every other header (including the still-wide `IPv6`, at 8 fields)
|
|
36
|
+
keeps the plain auto-generated tuple, and keyword patterns
|
|
37
|
+
(`case IPv4(protocol=6)`) are unaffected either way and stay the
|
|
38
|
+
documented default. `__match_args__` is part of the public API once
|
|
39
|
+
documented, so the chosen order is pinned by
|
|
40
|
+
`tests/test_pattern_matching.py` rather than left free to drift.
|
|
41
|
+
ARCHITECTURE.md and `docs/CLAIMS.md` are updated to describe this as
|
|
42
|
+
a deliberate, tested exception rather than the "this library never
|
|
43
|
+
hand-declares `__match_args__`" absolute they stated before (#94).
|
|
44
|
+
- **DNS resource records expose typed RDATA, and every question, not
|
|
45
|
+
just the first.** `DNSResourceRecord.rdata_text` decoded A/AAAA/MX/
|
|
46
|
+
SOA/etc. into a display string only; a new `rdata_value` field adds
|
|
47
|
+
the typed decoding alongside it (`rdata_text` is unchanged) —
|
|
48
|
+
`ipaddress.IPv4Address`/`IPv6Address` for A/AAAA, the decompressed
|
|
49
|
+
target `str` for NS/CNAME/PTR, a new `MXRecord(preference, exchange)`
|
|
50
|
+
for MX, a new `SOARecord(mname, rname, serial, refresh, retry,
|
|
51
|
+
expire, minimum)` for SOA, `list[str]` of character-strings for TXT,
|
|
52
|
+
and `None` — never raises — for the types this library does not
|
|
53
|
+
decode. Computed eagerly at parse time, like `rdata_text` always has
|
|
54
|
+
been: a name inside RDATA (a CNAME's target, for instance) can use
|
|
55
|
+
DNS compression against the *whole* message, so decoding it needs
|
|
56
|
+
the same message-wide context `rdata_text` already required, which a
|
|
57
|
+
property computed lazily from `rdata` alone could not resolve.
|
|
58
|
+
Separately, `DNS.questions` adds a `tuple[DNSQuestion, ...]` walking
|
|
59
|
+
every entry of the question section — `question_name`/`question_type`
|
|
60
|
+
/`question_class` exposed only the first and are unchanged (#96).
|
|
61
|
+
- **A typed `DHCPOption`, alongside `DHCP.option_map`.** `option_map`
|
|
62
|
+
was the only accessor in the library returning a bare
|
|
63
|
+
`dict[int, bytes]` rather than typed objects; it is unchanged (still
|
|
64
|
+
returns exactly what it always did). A new `parsed_options` property
|
|
65
|
+
wraps the same, already RFC-3396-concatenated, mapping into a
|
|
66
|
+
`tuple[DHCPOption, ...]`, mirroring `TCPOption`/`IPv4Option`: `code`,
|
|
67
|
+
`data`, `code_name`, and a decoded `.value` for the option codes this
|
|
68
|
+
library understands — a single `ipaddress.IPv4Address` for Subnet
|
|
69
|
+
Mask (1)/Requested IP Address (50)/Server Identifier (54), a tuple of
|
|
70
|
+
one or more for Router (3)/Domain Name Server (6) (RFC 2132 permits
|
|
71
|
+
repeating either), the integer seconds for IP Address Lease Time
|
|
72
|
+
(51), the raw byte for Message Type (53). `None` — never raises —
|
|
73
|
+
for codes this library does not decode and malformed option data
|
|
74
|
+
(#96).
|
|
75
|
+
- **`IPv4Option.value` decodes Record Route, Timestamp and Router
|
|
76
|
+
Alert.** `IPv4Option` had `kind_name` and no `.value` — the three
|
|
77
|
+
common kinds (RFC 791 §3.1, RFC 2113) were named but their contents
|
|
78
|
+
left raw, unlike `TCPOption`, which already decodes its values.
|
|
79
|
+
Record Route (7) decodes to a `tuple[ipaddress.IPv4Address, ...]` of
|
|
80
|
+
the addresses recorded so far — the option's own pointer byte says
|
|
81
|
+
how many of the address slots are actually filled, not the option's
|
|
82
|
+
total length. Timestamp (68) decodes to `tuple[int, ...]` of plain
|
|
83
|
+
millisecond timestamps when its flag selects that shape, or
|
|
84
|
+
`tuple[tuple[ipaddress.IPv4Address, int], ...]` of (address,
|
|
85
|
+
timestamp) pairs when the flag says each entry carries one — the
|
|
86
|
+
overflow counter and pointer are not decoded, read `data` raw for
|
|
87
|
+
those. Router Alert (148) decodes to the 2-byte value as `int`.
|
|
88
|
+
`None` — never raises — for every other kind and for malformed data
|
|
89
|
+
on one of these three (a bad pointer, an unrecognized Timestamp
|
|
90
|
+
flag, a short buffer) (#96).
|
|
91
|
+
- **`IPv6Routing.segments` and `IPv6Option.value` for Router Alert /
|
|
92
|
+
Jumbo Payload.** `IPv6Routing.data` was entirely unparsed — no
|
|
93
|
+
segment-list extraction for any routing type. RH0 (`routing_type`
|
|
94
|
+
``0``, deprecated by RFC 5095 but still seen — RFC 2460 §4.4) and
|
|
95
|
+
Mobile IPv6 (``2``, RFC 6275 §6.4) both store a 4-byte reserved field
|
|
96
|
+
followed by one IPv6 address per segment; `segments` decodes that
|
|
97
|
+
into a `tuple[ipaddress.IPv6Address, ...]`. RPL Source Routing (``3``,
|
|
98
|
+
RFC 6554) is deliberately **not** decoded: RFC 6554 §3 elides a
|
|
99
|
+
shared prefix from each intermediate address relative to the
|
|
100
|
+
enclosing packet's *destination* address, context this
|
|
101
|
+
one-extension-header accessor does not have — `data` stays available
|
|
102
|
+
raw. `None` for every other routing type and for malformed address
|
|
103
|
+
data. Separately, `IPv6Option.value` (Hop-by-Hop / Destination
|
|
104
|
+
Options TLVs) now decodes Router Alert (5, RFC 2711 §2.1) and Jumbo
|
|
105
|
+
Payload (194, RFC 2675 §2) into typed `int`s, the last of #96's four
|
|
106
|
+
pieces (`IPv6Option` "likewise decode[d] no values"); `None` for
|
|
107
|
+
every other type and malformed data, same contract throughout this
|
|
108
|
+
tier (#96).
|
|
109
|
+
- **CI proves the library runs under a real Pyodide (WebAssembly)
|
|
110
|
+
runtime**, not just that the modules `scapy`/`dpkt` need are
|
|
111
|
+
individually blocked. A new `pyodide` job (`.github/workflows/ci.yml`)
|
|
112
|
+
boots actual Pyodide under Node via `scripts/pyodide/run_in_pyodide.mjs`,
|
|
113
|
+
installs the wheel this job just built, and decodes the entire
|
|
114
|
+
real-capture fixture corpus with it — see
|
|
115
|
+
`scripts/pyodide/check_in_pyodide.py`. That surfaced a genuine bug
|
|
116
|
+
along the way: `IPv6.decode()`/`bytes(IPv6(...))` used
|
|
117
|
+
`socket.inet_ntop`/`inet_pton(AF_INET6, ...)`, and Pyodide's CPython
|
|
118
|
+
build has `AF_INET6` sockets disabled, so every IPv6 frame failed to
|
|
119
|
+
decode there. `_base.py`'s `ipv6_to_bytes`/`bytes_to_ipv6` are now a
|
|
120
|
+
pure-Python implementation (`ipaddress` for parsing; a hand-rolled
|
|
121
|
+
RFC 5952 canonicalizer, differentially verified against glibc's
|
|
122
|
+
`inet_ntop` across 500,000+ random addresses plus both of its
|
|
123
|
+
dotted-quad special cases, for formatting — `str(ipaddress.IPv6Address)`
|
|
124
|
+
was tried first and rejected: it disagrees with glibc on IPv4-mapped
|
|
125
|
+
addresses, and disagrees with *itself* between Python 3.11 and 3.12).
|
|
126
|
+
IPv4 addressing is unaffected; behavior for every existing platform
|
|
127
|
+
is unchanged, exception type included (#99).
|
|
128
|
+
- **Nightly fuzzing with a moving seed.** `tests/test_fuzz.py`'s
|
|
129
|
+
`"netprotocols"` Hypothesis profile is deterministic on purpose (200
|
|
130
|
+
examples, `derandomize=True`) so a pull request is reproducible — but
|
|
131
|
+
that also means every build since it was written has run the *same*
|
|
132
|
+
200 inputs. A new `"nightly"` profile (10,000 examples, a real random
|
|
133
|
+
seed each run) runs the whole suite on a new schedule,
|
|
134
|
+
`.github/workflows/fuzz.yml` (03:00 UTC daily, plus manual dispatch),
|
|
135
|
+
kept out of `ci.yml` deliberately — `ci.yml` is reused by
|
|
136
|
+
`release.yml` via `workflow_call`, and a `schedule:` trigger there
|
|
137
|
+
would fire the whole PR/release gate on a cron, not just this
|
|
138
|
+
exploratory job. `.hypothesis/`'s example database accumulates
|
|
139
|
+
across nightly runs via `actions/cache` (keyed by run id with a
|
|
140
|
+
prefix restore-key, since a fixed key would restore forever but
|
|
141
|
+
never actually save a new entry) and uploads as a workflow artifact
|
|
142
|
+
on failure, so a counterexample is recoverable without repo write
|
|
143
|
+
access; a failed scheduled run's own red build is the notification
|
|
144
|
+
(GitHub already does this by default), so nothing here files an
|
|
145
|
+
issue on top of it. Reproduce locally:
|
|
146
|
+
`HYPOTHESIS_PROFILE=nightly uv run pytest`. Also adds a targeted
|
|
147
|
+
strategy building well-formed TCP SYN options (MSS, window scale,
|
|
148
|
+
SACK-Permitted, SACK, timestamps) — the real-capture corpus never
|
|
149
|
+
caught a SYN, so unlike NOP/Timestamps these otherwise depend on
|
|
150
|
+
`max_examples` alone stumbling into a well-formed TLV by chance
|
|
151
|
+
(#98).
|
|
152
|
+
- **A pcap/pcapng reader that takes bytes, not filenames.** New
|
|
153
|
+
`netprotocols.pcap`: `read_captures(buffer)` auto-detects classic
|
|
154
|
+
pcap vs. pcapng from its magic number and yields `CapturedFrame`
|
|
155
|
+
(`timestamp` in nanoseconds since the Unix epoch, normalized from
|
|
156
|
+
whatever resolution the source recorded, `data` the frame's raw
|
|
157
|
+
bytes); `read_pcap()`/`read_pcapng()` are the same for a caller who
|
|
158
|
+
already knows the format. pcapng support covers exactly the block
|
|
159
|
+
types frames can come from — Section Header, Interface Description
|
|
160
|
+
(read only for its `if_tsresol` option), Enhanced Packet, and Simple
|
|
161
|
+
Packet (which the format gives no timestamp at all, hence `0`);
|
|
162
|
+
every other block type is skipped wholesale. A malformed or
|
|
163
|
+
truncated capture raises the new `MalformedCaptureError`
|
|
164
|
+
(`ProtocolError` family, no `lax` mode — a corrupt *container* is a
|
|
165
|
+
different failure shape than a malformed header inside one already-
|
|
166
|
+
extracted frame). Format detection is eager; producing frames is
|
|
167
|
+
lazy (a generator), so a bad record downstream doesn't invalidate
|
|
168
|
+
what already iterated cleanly, and a huge capture is never forced
|
|
169
|
+
into a list of frames nobody asked for.
|
|
170
|
+
|
|
171
|
+
`tests/conftest.py` drops the private classic-pcap reader every test
|
|
172
|
+
file reached for — `pcap_frames()` is now a thin adapter over the
|
|
173
|
+
shipped `read_pcap()`, and `~10` test files were migrated onto it (a
|
|
174
|
+
real migration, not a rename: `tests/test_pcap.py` keeps its own
|
|
175
|
+
independent reference reader, deliberately never importing the
|
|
176
|
+
module it is cross-checking, the same "standalone, so a shared bug
|
|
177
|
+
can't cancel itself out" precedent as `scripts/benchmark.py` and
|
|
178
|
+
`scripts/check_fixtures.py`).
|
|
179
|
+
|
|
180
|
+
One design idea was tried and reverted on measurement: slicing each
|
|
181
|
+
frame lazily out of a `memoryview` over the whole buffer, to keep
|
|
182
|
+
large captures zero-copy. Measured across synthetic captures up to
|
|
183
|
+
~140MB, it was 0.91x-0.98x — never faster, sometimes slower — because
|
|
184
|
+
a real capture is many *small* frames, and a `memoryview` slice's own
|
|
185
|
+
overhead is paid per frame; #88's identical finding for a single
|
|
186
|
+
frame generalizes rather than being contradicted. `docs/CLAIMS.md`
|
|
187
|
+
5.8 is corrected accordingly — it previously forward-referenced this
|
|
188
|
+
issue with an unverified "1.8x" figure (#100).
|
|
189
|
+
|
|
190
|
+
### Documentation
|
|
191
|
+
- **The comparative-claims embargo is lifted (#107, #124).** `docs/CLAIMS.md`
|
|
192
|
+
1.7 gained a per-project table auditing ten comparable Python packet
|
|
193
|
+
libraries' CI configurations (dpkt, scapy, pypacker, construct,
|
|
194
|
+
pcapkit, dnspython, pyshark, nfstream, stackforge, PyTCP-net_proto)
|
|
195
|
+
for an actual performance-regression gate — cited file:line or
|
|
196
|
+
workflow, dated 2026-09-04; none of the ten gate on one, so the
|
|
197
|
+
comparative form of 1.7's claim is now stated with that evidence
|
|
198
|
+
(#124). Every section that previously carried a
|
|
199
|
+
`COMPARATIVE — HELD` banner is re-measured against the current tree
|
|
200
|
+
and published: decode throughput (1.1, 1.2), encoding throughput
|
|
201
|
+
(1.3), import time (1.4), wheel size (1.5), decode depth (1.6), the
|
|
202
|
+
`mypy --strict` comparison (2.1), `match`/`case` exclusivity (2.2),
|
|
203
|
+
the five-property combination (2.3), Pyodide/browser support (3.1),
|
|
204
|
+
and the permissive-licence wedge (4.1). One finding moved against
|
|
205
|
+
this project rather than for it and is reported as such: three tiers
|
|
206
|
+
of added surface since Tier 1 (#87's dispatch rewrite, #88's chain
|
|
207
|
+
walker, #91's structured errors chief among them) reversed decode
|
|
208
|
+
throughput from 1.16× faster than dpkt back to roughly 11% slower —
|
|
209
|
+
still within the 15% band claim 1.2 names, just no longer ahead.
|
|
210
|
+
Two new reproduction scripts back claims that previously had none:
|
|
211
|
+
`scripts/benchmark_encode.py` (1.3) and `scripts/benchmark_import.py`
|
|
212
|
+
(1.4). `README.md` gains a "Why NETProtocols" section drawing the
|
|
213
|
+
now-published claims into the project's front door, in the tone
|
|
214
|
+
`docs/CLAIMS.md` 4.2 sets for writing about competitors.
|
|
215
|
+
|
|
8
216
|
## [2.0.0] - 2026-09-04
|
|
9
217
|
|
|
10
218
|
### Development
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: netprotocols
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.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
|
|
@@ -72,6 +72,80 @@ pip install netprotocols
|
|
|
72
72
|
|
|
73
73
|
Requires Python 3.12+. Fully typed (`py.typed`, mypy strict).
|
|
74
74
|
|
|
75
|
+
## Why NETProtocols
|
|
76
|
+
|
|
77
|
+
Measured against `dpkt 1.9.8` and `scapy 2.7.0` on a 97-frame corpus of
|
|
78
|
+
real captured traffic (CPython 3.12.3, x86-64 Linux; re-measured
|
|
79
|
+
2026-09-04 — see [docs/CLAIMS.md](docs/CLAIMS.md) for every number
|
|
80
|
+
below, its reproduction command, and its caveats):
|
|
81
|
+
|
|
82
|
+
- **Within 15% of dpkt on decode, 5.6× faster than scapy** —
|
|
83
|
+
`uv run --group bench python scripts/benchmark.py --compare`. dpkt is
|
|
84
|
+
the faster of the two comparators on this corpus; the gap moves both
|
|
85
|
+
ways as this library and dpkt each change, and this file's job is to
|
|
86
|
+
say so honestly rather than only when it's flattering.
|
|
87
|
+
- **Decodes further into the stack than dpkt on 27 of 97 corpus
|
|
88
|
+
frames** — DNS and DHCP payloads that dpkt leaves as raw bytes,
|
|
89
|
+
netprotocols continues decoding. A throughput number means little
|
|
90
|
+
without knowing how much work it bought.
|
|
91
|
+
- **4.5× faster than dpkt at re-encoding** (`bytes(header)`,
|
|
92
|
+
`scripts/benchmark_encode.py`) — the other half of "codec" that
|
|
93
|
+
nobody else benchmarks.
|
|
94
|
+
- **Imports in 54 ms; `scapy.all` takes 458+ ms** (`scripts/benchmark_import.py`),
|
|
95
|
+
and never touches the host doing it — importing scapy populates live
|
|
96
|
+
interface and routing tables as a side effect of the import
|
|
97
|
+
statement.
|
|
98
|
+
- **An 85.6 KB wheel against scapy's 2.47 MB** — about 30× smaller.
|
|
99
|
+
- **Regression-gated in CI, which — as far as we could establish by
|
|
100
|
+
auditing ten comparable Python packet libraries' CI configurations
|
|
101
|
+
(dpkt, scapy, pypacker, construct, pcapkit, dnspython, pyshark,
|
|
102
|
+
nfstream, stackforge, PyTCP-net_proto; full citations in
|
|
103
|
+
[docs/CLAIMS.md §1.7](docs/CLAIMS.md)) — none of them do.** Two of
|
|
104
|
+
the ten ship real benchmark code that simply never runs in CI
|
|
105
|
+
(construct disables it explicitly; stackforge's Criterion benches are
|
|
106
|
+
never invoked); the rest have no performance benchmark at all.
|
|
107
|
+
- **Typed where the alternatives are not.** `mypy --strict` over the
|
|
108
|
+
whole of `src/`, enforced in CI. scapy ships `py.typed` but enables
|
|
109
|
+
strict checking on 107 of its files, 2 of the 121 under
|
|
110
|
+
`scapy/layers/` — the dissectors most code touches stay `Any`. dpkt
|
|
111
|
+
ships no `py.typed` at all, and no third-party stub package exists
|
|
112
|
+
for it.
|
|
113
|
+
- **The only MIT-licensed, strictly-typed, zero-dependency packet codec
|
|
114
|
+
still under active maintenance.** scapy is GPL-2.0; pypacker is
|
|
115
|
+
GPLv2; the newer entrants `stackforge` and `PyTCP-net_proto` are both
|
|
116
|
+
GPL-3.0. The other permissive option, dpkt (BSD), last released
|
|
117
|
+
2022-08-18 and last committed 2024-05-05, still targets Python 2.7
|
|
118
|
+
and 3.5–3.9, and remains marked Beta after twelve years.
|
|
119
|
+
- **Runs where scapy cannot — including in the browser.** Verified
|
|
120
|
+
under a real Pyodide runtime in CI: scapy fails to import at all
|
|
121
|
+
under Pyodide (`from fcntl import ioctl` is unconditional in
|
|
122
|
+
`scapy/arch/`), where netprotocols, dpkt and pypacker all import
|
|
123
|
+
cleanly.
|
|
124
|
+
- **The only one of these libraries a `match`/`case` statement
|
|
125
|
+
dissects out of the box.** dpkt builds `__slots__` from a metaclass
|
|
126
|
+
and generates no `__match_args__`; scapy routes fields through
|
|
127
|
+
`__getattr__`; construct returns dicts. None gives you a pattern to
|
|
128
|
+
match against.
|
|
129
|
+
|
|
130
|
+
None of this makes scapy less than an extraordinary piece of software —
|
|
131
|
+
it crafts, sends, sniffs and fuzzes across thousands of protocols, and
|
|
132
|
+
this library does none of that. The comparison above is scoped to what
|
|
133
|
+
both are: a codec that turns bytes into typed objects and back.
|
|
134
|
+
|
|
135
|
+
One `mypy --strict` run says more than the bullets above:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
# scapy 2.7.0 — a field name that does not exist
|
|
139
|
+
p.ThisFieldDoesNotExist → Any (no error)
|
|
140
|
+
|
|
141
|
+
# dpkt 1.9.8 — the whole module
|
|
142
|
+
import dpkt.ethernet → error: missing library stubs or py.typed
|
|
143
|
+
|
|
144
|
+
# netprotocols — a typo in a real field name
|
|
145
|
+
ip.proto → error: "IPv4" has no attribute
|
|
146
|
+
"proto"; maybe "protocol"?
|
|
147
|
+
```
|
|
148
|
+
|
|
75
149
|
## Protocol coverage
|
|
76
150
|
|
|
77
151
|
| Layer | Protocol | Class | Notes |
|
|
@@ -185,6 +259,29 @@ to read `stopped_by`. Outside this one RFC-mandated case, a *complete*
|
|
|
185
259
|
frame that fails to decode is still a bug to raise on — `lax=True`
|
|
186
260
|
elsewhere is a capture tool's choice, not a default.
|
|
187
261
|
|
|
262
|
+
## Reading captures
|
|
263
|
+
|
|
264
|
+
`read_captures()` takes the bytes of a capture file — not a path — and
|
|
265
|
+
auto-detects classic pcap vs. pcapng from its magic number:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
from netprotocols import decode_frame, read_captures
|
|
269
|
+
|
|
270
|
+
data = open("traffic.pcap", "rb").read() # or however you got the bytes
|
|
271
|
+
for timestamp, frame in read_captures(data):
|
|
272
|
+
packet = decode_frame(frame, lax=True)
|
|
273
|
+
...
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Each `CapturedFrame` is `(timestamp, data)` — `timestamp` normalized to
|
|
277
|
+
nanoseconds since the Unix epoch regardless of the source format's own
|
|
278
|
+
resolution (classic pcap's microseconds or nanoseconds; pcapng's
|
|
279
|
+
per-interface `if_tsresol`). `read_pcap()`/`read_pcapng()` are the same
|
|
280
|
+
thing for a caller who already knows the format and wants to skip
|
|
281
|
+
detection. A malformed or truncated capture raises
|
|
282
|
+
`MalformedCaptureError`, the same `ProtocolError` family every other
|
|
283
|
+
exception in this library belongs to.
|
|
284
|
+
|
|
188
285
|
## Flow keys
|
|
189
286
|
|
|
190
287
|
`Packet.flow_key()` (or the free function, `netprotocols.flow_key()`,
|
|
@@ -212,6 +309,20 @@ named value against the plain `int` the wire actually carries. See
|
|
|
212
309
|
[ARCHITECTURE.md](ARCHITECTURE.md#structural-pattern-matching) for why
|
|
213
310
|
both of those hold and how to keep them holding.
|
|
214
311
|
|
|
312
|
+
`IPv4`, `TCP` and `DHCP` are wide enough (11-15 fields) that their
|
|
313
|
+
full auto-generated positional form is unusable, so those three
|
|
314
|
+
additionally curate a short `__match_args__` by hand — the fields
|
|
315
|
+
someone matching by position actually reaches for:
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
match ip:
|
|
319
|
+
case IPv4(src, dst, IPProtocol.TCP):
|
|
320
|
+
print(f"TCP: {src} -> {dst}")
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Keyword patterns (`case IPv4(protocol=IPProtocol.TCP)`) work on every
|
|
324
|
+
field regardless, curated or not, and stay the documented default.
|
|
325
|
+
|
|
215
326
|
```python
|
|
216
327
|
from netprotocols import IPv4, IPProtocol, TCP
|
|
217
328
|
|
|
@@ -294,24 +405,29 @@ monitor built on it (formerly Packet-Sniffer).
|
|
|
294
405
|
|
|
295
406
|
## Roadmap
|
|
296
407
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
[#
|
|
301
|
-
|
|
408
|
+
The post-1.3.0 roadmap,
|
|
409
|
+
[#107](https://github.com/EONRaider/NETProtocols/issues/107), is
|
|
410
|
+
complete — five tiers plus a competitor CI audit
|
|
411
|
+
([#124](https://github.com/EONRaider/NETProtocols/issues/124)) and the
|
|
412
|
+
comparative-claims embargo lift that closed it out:
|
|
302
413
|
|
|
303
414
|
| Version | Theme |
|
|
304
415
|
|---|---|
|
|
305
416
|
| [1.3.1](https://github.com/EONRaider/NETProtocols/issues/102) | Hygiene |
|
|
306
417
|
| [1.4.0](https://github.com/EONRaider/NETProtocols/issues/103) | Decode performance |
|
|
307
|
-
| [2.0.0](https://github.com/EONRaider/NETProtocols/issues/104) | A public protocol registry, a shipped chain walker, flow keys |
|
|
418
|
+
| [2.0.0](https://github.com/EONRaider/NETProtocols/issues/104) | A public protocol registry, a shipped chain walker, flow keys — **released** |
|
|
308
419
|
| [2.1.0](https://github.com/EONRaider/NETProtocols/issues/105) | Typed accessors and pattern-matching ergonomics |
|
|
309
420
|
| [2.2.0](https://github.com/EONRaider/NETProtocols/issues/106) | Universal round-trip properties, nightly fuzzing, a pcap reader |
|
|
310
421
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
422
|
+
Everything through 2.2.0 has landed on `master`; per the roadmap's own
|
|
423
|
+
release policy, only 2.0.0 was an actual PyPI release, and the
|
|
424
|
+
comparative claims above draw on the finished tree. New planned work
|
|
425
|
+
will open fresh issues rather than restating a list here, so this
|
|
426
|
+
section stays accurate without upkeep. The wave before this one — TCP
|
|
427
|
+
and IPv4 option parsing, ICMP message bodies, NDP, IPv6
|
|
428
|
+
extension-header TLV options, the GRE checksum arm, a DNS-over-TCP
|
|
429
|
+
corpus fixture and `ipaddress` accessors — shipped in 1.3.0; see
|
|
430
|
+
[CHANGELOG.md](CHANGELOG.md).
|
|
315
431
|
|
|
316
432
|
## Contributing
|
|
317
433
|
|