netprotocols 1.2.0__tar.gz → 1.3.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-1.3.0/.github/ISSUE_TEMPLATE/bug_report.yml +60 -0
- netprotocols-1.3.0/.github/ISSUE_TEMPLATE/config.yml +10 -0
- netprotocols-1.3.0/.github/ISSUE_TEMPLATE/protocol_request.yml +66 -0
- netprotocols-1.3.0/.github/pull_request_template.md +40 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/ARCHITECTURE.md +16 -1
- netprotocols-1.3.0/CHANGELOG.md +296 -0
- netprotocols-1.3.0/CONTRIBUTING.md +143 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/PKG-INFO +31 -12
- {netprotocols-1.2.0 → netprotocols-1.3.0}/README.md +30 -11
- {netprotocols-1.2.0 → netprotocols-1.3.0}/pyproject.toml +1 -1
- netprotocols-1.3.0/scripts/capture_fixtures_dhcp.sh +94 -0
- netprotocols-1.3.0/scripts/capture_fixtures_dns_tcp.sh +112 -0
- netprotocols-1.3.0/scripts/capture_fixtures_gre.sh +112 -0
- netprotocols-1.3.0/scripts/capture_fixtures_vlan.sh +217 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/scripts/check_fixtures.py +44 -7
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/__init__.py +20 -6
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/_enums.py +8 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/checksum.py +22 -2
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer2/__init__.py +2 -1
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer2/arp.py +15 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer2/ethernet.py +37 -13
- netprotocols-1.3.0/src/netprotocols/layer2/vlan.py +89 -0
- netprotocols-1.3.0/src/netprotocols/layer3/gre.py +157 -0
- netprotocols-1.3.0/src/netprotocols/layer3/icmp.py +328 -0
- netprotocols-1.3.0/src/netprotocols/layer3/igmp.py +269 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer3/ip.py +124 -3
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer3/ipv6_ext.py +91 -2
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer4/_ports.py +22 -5
- netprotocols-1.3.0/src/netprotocols/layer4/tcp.py +293 -0
- netprotocols-1.3.0/src/netprotocols/layer7/dhcp.py +286 -0
- netprotocols-1.3.0/src/netprotocols/layer7/dns.py +436 -0
- netprotocols-1.3.0/tests/fixtures/MANIFEST.md +96 -0
- netprotocols-1.3.0/tests/fixtures/dhcp.pcap +0 -0
- netprotocols-1.3.0/tests/fixtures/dns_tcp.pcap +0 -0
- netprotocols-1.3.0/tests/fixtures/gre.pcap +0 -0
- netprotocols-1.3.0/tests/fixtures/staging/dhcp.pcap +0 -0
- netprotocols-1.3.0/tests/fixtures/staging/dns_tcp.pcap +0 -0
- netprotocols-1.3.0/tests/fixtures/staging/gre.pcap +0 -0
- netprotocols-1.3.0/tests/fixtures/staging/vlan_icmp.pcap +0 -0
- netprotocols-1.3.0/tests/fixtures/vlan_icmp.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/test_arp.py +16 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/test_checksum.py +82 -3
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/test_contract.py +6 -2
- netprotocols-1.3.0/tests/test_corpus.py +311 -0
- netprotocols-1.3.0/tests/test_dhcp.py +260 -0
- netprotocols-1.3.0/tests/test_dns.py +489 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/test_fuzz.py +62 -0
- netprotocols-1.3.0/tests/test_gre.py +245 -0
- netprotocols-1.3.0/tests/test_icmp.py +217 -0
- netprotocols-1.3.0/tests/test_igmp.py +329 -0
- netprotocols-1.3.0/tests/test_ip.py +221 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/test_ipv6_ext.py +93 -0
- netprotocols-1.3.0/tests/test_tcp.py +204 -0
- netprotocols-1.3.0/tests/test_vlan.py +107 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/uv.lock +1 -1
- netprotocols-1.2.0/CHANGELOG.md +0 -119
- netprotocols-1.2.0/src/netprotocols/layer3/icmp.py +0 -135
- netprotocols-1.2.0/src/netprotocols/layer3/igmp.py +0 -95
- netprotocols-1.2.0/src/netprotocols/layer4/tcp.py +0 -153
- netprotocols-1.2.0/src/netprotocols/layer7/dns.py +0 -234
- netprotocols-1.2.0/tests/fixtures/MANIFEST.md +0 -51
- netprotocols-1.2.0/tests/test_corpus.py +0 -167
- netprotocols-1.2.0/tests/test_dns.py +0 -168
- netprotocols-1.2.0/tests/test_icmp.py +0 -55
- netprotocols-1.2.0/tests/test_igmp.py +0 -113
- netprotocols-1.2.0/tests/test_ip.py +0 -92
- netprotocols-1.2.0/tests/test_tcp.py +0 -80
- {netprotocols-1.2.0 → netprotocols-1.3.0}/.github/workflows/ci.yml +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/.github/workflows/release.yml +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/.gitignore +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/LICENSE +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/scripts/capture_fixtures.sh +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/scripts/capture_fixtures_igmp.sh +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/scripts/capture_fixtures_supplement.sh +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/_base.py +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer3/__init__.py +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer4/__init__.py +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer4/udp.py +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer7/__init__.py +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/packet.py +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/py.typed +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/utils/__init__.py +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/utils/exceptions.py +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/utils/ipv4.py +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/utils/mac.py +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/conftest.py +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/arp_exchange.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/icmpv4_echo_lo.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/icmpv4_external.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/icmpv4_ttl_exceeded.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/icmpv6_echo_lo.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/igmp.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/ipv4_fragments.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/ipv6_fragments.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/ipv6_mld.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/ipv6_ndp_mld.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/arp_exchange.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/icmpv4_echo_lo.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/icmpv4_external.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/icmpv4_ttl_exceeded.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/icmpv6_echo_lo.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/igmp.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/ipv4_fragments.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/ipv6_fragments.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/ipv6_mld.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/ipv6_ndp_mld.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/tcp_http.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/tcp_https.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/udp_dns.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/tcp_http.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/tcp_https.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/udp_dns.pcap +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/test_ethernet.py +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/test_packet.py +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/test_udp.py +0 -0
- {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/test_utils.py +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: A protocol decodes, encodes, or chains incorrectly, or raises the wrong error.
|
|
3
|
+
labels: ["bug"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Thanks for the report. netprotocols decodes real traffic, so the most
|
|
9
|
+
useful reports come with the actual bytes that trigger the problem.
|
|
10
|
+
Please search [existing issues](https://github.com/EONRaider/NETProtocols/issues)
|
|
11
|
+
first.
|
|
12
|
+
- type: input
|
|
13
|
+
id: version
|
|
14
|
+
attributes:
|
|
15
|
+
label: Version
|
|
16
|
+
description: >-
|
|
17
|
+
netprotocols version
|
|
18
|
+
(`python -c "import netprotocols; print(netprotocols.__version__)"`)
|
|
19
|
+
and your Python version.
|
|
20
|
+
placeholder: "netprotocols 1.2.0, Python 3.12.3"
|
|
21
|
+
validations:
|
|
22
|
+
required: true
|
|
23
|
+
- type: textarea
|
|
24
|
+
id: what-happened
|
|
25
|
+
attributes:
|
|
26
|
+
label: What happened
|
|
27
|
+
description: What did you decode or build, and what went wrong?
|
|
28
|
+
placeholder: "IPv4.decode(...) returned X, but the header on the wire says Y."
|
|
29
|
+
validations:
|
|
30
|
+
required: true
|
|
31
|
+
- type: textarea
|
|
32
|
+
id: repro
|
|
33
|
+
attributes:
|
|
34
|
+
label: Reproduction
|
|
35
|
+
description: >-
|
|
36
|
+
A minimal snippet. Include the raw bytes as a hex string so it runs
|
|
37
|
+
standalone — that is by far the fastest path to a fix. Rendered as
|
|
38
|
+
Python; no backticks needed.
|
|
39
|
+
render: python
|
|
40
|
+
placeholder: |
|
|
41
|
+
from netprotocols import Ethernet
|
|
42
|
+
|
|
43
|
+
frame = bytes.fromhex("ffffffffffff0007...")
|
|
44
|
+
print(Ethernet.decode(frame))
|
|
45
|
+
validations:
|
|
46
|
+
required: true
|
|
47
|
+
- type: textarea
|
|
48
|
+
id: expected
|
|
49
|
+
attributes:
|
|
50
|
+
label: Expected behavior
|
|
51
|
+
description: What should have happened instead?
|
|
52
|
+
validations:
|
|
53
|
+
required: true
|
|
54
|
+
- type: checkboxes
|
|
55
|
+
id: checks
|
|
56
|
+
attributes:
|
|
57
|
+
label: Checklist
|
|
58
|
+
options:
|
|
59
|
+
- label: I included the raw bytes (or a pcap) needed to reproduce this.
|
|
60
|
+
- label: I searched existing issues for a duplicate.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Freeform issues stay enabled (the roadmap and design discussions don't
|
|
2
|
+
# fit a form); the templates just make the common cases structured.
|
|
3
|
+
blank_issues_enabled: true
|
|
4
|
+
contact_links:
|
|
5
|
+
- name: Contributing guide
|
|
6
|
+
url: https://github.com/EONRaider/NETProtocols/blob/master/CONTRIBUTING.md
|
|
7
|
+
about: Dev setup, the QA ladder, and how to add a protocol.
|
|
8
|
+
- name: Protocol roadmap
|
|
9
|
+
url: https://github.com/EONRaider/NETProtocols/issues/22
|
|
10
|
+
about: Planned protocols and their ordering — check before requesting one.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
name: Protocol request
|
|
2
|
+
description: Request support for a protocol netprotocols does not decode yet.
|
|
3
|
+
labels: ["enhancement", "roadmap"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
The roadmap ([#22](https://github.com/EONRaider/NETProtocols/issues/22))
|
|
9
|
+
tracks planned protocols and their ordering — check it and existing
|
|
10
|
+
issues first. Adding a protocol follows the cookbook in
|
|
11
|
+
[ARCHITECTURE.md](https://github.com/EONRaider/NETProtocols/blob/master/ARCHITECTURE.md);
|
|
12
|
+
contributions are welcome.
|
|
13
|
+
- type: input
|
|
14
|
+
id: protocol
|
|
15
|
+
attributes:
|
|
16
|
+
label: Protocol
|
|
17
|
+
placeholder: "e.g. DHCP, GRE, 802.1X"
|
|
18
|
+
validations:
|
|
19
|
+
required: true
|
|
20
|
+
- type: input
|
|
21
|
+
id: spec
|
|
22
|
+
attributes:
|
|
23
|
+
label: Specification
|
|
24
|
+
description: The RFC or standard number(s) that define the header.
|
|
25
|
+
placeholder: "RFC 2131"
|
|
26
|
+
validations:
|
|
27
|
+
required: true
|
|
28
|
+
- type: dropdown
|
|
29
|
+
id: layer
|
|
30
|
+
attributes:
|
|
31
|
+
label: Layer
|
|
32
|
+
description: Roughly where it sits in the stack.
|
|
33
|
+
options:
|
|
34
|
+
- Link (2)
|
|
35
|
+
- Network (3)
|
|
36
|
+
- Transport (4)
|
|
37
|
+
- Application (7)
|
|
38
|
+
- Other / not sure
|
|
39
|
+
validations:
|
|
40
|
+
required: true
|
|
41
|
+
- type: textarea
|
|
42
|
+
id: dispatch
|
|
43
|
+
attributes:
|
|
44
|
+
label: How it is reached
|
|
45
|
+
description: >-
|
|
46
|
+
What carries it — which lower layer, and which dispatch value
|
|
47
|
+
(EtherType, IP protocol number, or port)?
|
|
48
|
+
placeholder: "Behind UDP on ports 67/68."
|
|
49
|
+
validations:
|
|
50
|
+
required: false
|
|
51
|
+
- type: textarea
|
|
52
|
+
id: sample
|
|
53
|
+
attributes:
|
|
54
|
+
label: Sample traffic
|
|
55
|
+
description: >-
|
|
56
|
+
A pcap or a hex dump of a real frame makes this far easier to
|
|
57
|
+
implement and test — the fixture corpus is captured from real traffic.
|
|
58
|
+
validations:
|
|
59
|
+
required: false
|
|
60
|
+
- type: checkboxes
|
|
61
|
+
id: checks
|
|
62
|
+
attributes:
|
|
63
|
+
label: Checklist
|
|
64
|
+
options:
|
|
65
|
+
- label: I checked the roadmap (#22) and existing issues.
|
|
66
|
+
- label: I can provide a real capture (pcap or hex) of this protocol.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Thanks for contributing to netprotocols! Fill in the sections below and
|
|
3
|
+
delete this comment. CONTRIBUTING.md has the full workflow; the short
|
|
4
|
+
version is: keep the QA ladder green and update the docs your change
|
|
5
|
+
touches. Relative links below resolve from the repo root once this
|
|
6
|
+
becomes the PR description.
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
<!-- What does this change do, and why? Link any related issue, e.g. "Closes #22". -->
|
|
12
|
+
|
|
13
|
+
## What's included
|
|
14
|
+
|
|
15
|
+
<!-- The concrete changes, one bullet each: new/edited modules, tests, docs. -->
|
|
16
|
+
|
|
17
|
+
-
|
|
18
|
+
|
|
19
|
+
## Verification
|
|
20
|
+
|
|
21
|
+
<!-- Paste the QA-ladder output, or tick the boxes. All four gates run in CI. -->
|
|
22
|
+
|
|
23
|
+
- [ ] `uv run ruff check` and `uv run ruff format --check` are clean
|
|
24
|
+
- [ ] `uv run mypy` is clean (strict)
|
|
25
|
+
- [ ] `uv run pytest` passes locally (CI runs it on Python 3.12 / 3.13 / 3.14)
|
|
26
|
+
- [ ] `CHANGELOG.md` has an entry under `## [Unreleased]`
|
|
27
|
+
|
|
28
|
+
### New protocol or dispatch change — also:
|
|
29
|
+
|
|
30
|
+
<!-- Delete this block if it doesn't apply. See ARCHITECTURE.md's cookbook. -->
|
|
31
|
+
|
|
32
|
+
- [ ] Followed the add-a-protocol cookbook in [ARCHITECTURE.md](ARCHITECTURE.md) and the decode contract in `src/netprotocols/_base.py`
|
|
33
|
+
- [ ] New `EtherType` / `IPProtocol` numbers carry display names in lockstep (a completeness test enforces it)
|
|
34
|
+
- [ ] Registered the class in the fuzz suite (`tests/test_fuzz.py::ALL_PROTOCOLS`)
|
|
35
|
+
- [ ] Added a fixture and updated [tests/fixtures/MANIFEST.md](tests/fixtures/MANIFEST.md) — a real capture where possible (see CONTRIBUTING.md for the capture scripts)
|
|
36
|
+
- [ ] Updated the README coverage table and the ARCHITECTURE.md chain diagram
|
|
37
|
+
|
|
38
|
+
## Notes
|
|
39
|
+
|
|
40
|
+
<!-- Anything reviewers should know: trade-offs, follow-ups, companion PRs. Optional. -->
|
|
@@ -109,6 +109,11 @@ flowchart LR
|
|
|
109
109
|
E[Ethernet] -->|0x0806| A[ARP]
|
|
110
110
|
E -->|0x0800| I4[IPv4]
|
|
111
111
|
E -->|0x86DD| I6[IPv6]
|
|
112
|
+
E -->|0x8100, 0x88A8, 0x9100| V[VLAN tag]
|
|
113
|
+
V -->|0x8100, 0x88A8, 0x9100| V
|
|
114
|
+
V -->|0x0806| A
|
|
115
|
+
V -->|0x0800| I4
|
|
116
|
+
V -->|0x86DD| I6
|
|
112
117
|
I4 -->|1| IC4[ICMPv4]
|
|
113
118
|
I4 -->|6| T[TCP]
|
|
114
119
|
I4 -->|17| U[UDP]
|
|
@@ -132,6 +137,16 @@ non-first fragment carries a slice from the middle of the original
|
|
|
132
137
|
payload, so no upper-layer header exists at its start. The same
|
|
133
138
|
offset rule applies to fragmented IPv4.
|
|
134
139
|
|
|
140
|
+
VLAN tags chain the same way: a tag's `next_protocol` dispatches the
|
|
141
|
+
inner EtherType through the same registry Ethernet uses, so a QinQ
|
|
142
|
+
(`0x88A8`) or legacy double-tagged (`0x9100`) frame decodes as one
|
|
143
|
+
`VLAN` layer per tag and the innermost payload (IPv4/IPv6/ARP) chains
|
|
144
|
+
normally. The Tag Control Information word is split into its semantic
|
|
145
|
+
bitfields — `pcp`, `dei`, `vid` — as dataclass fields validated in
|
|
146
|
+
`__post_init__`, with the packed 16-bit view kept as the `tci`
|
|
147
|
+
property (the same pattern `IPv6` uses for `version` /
|
|
148
|
+
`traffic_class` / `flow_label`).
|
|
149
|
+
|
|
135
150
|
The numbers on the arrows are the wire values — EtherTypes out of
|
|
136
151
|
Ethernet, IP protocol numbers out of IPv4/IPv6 — and they live in
|
|
137
152
|
[`_enums.py`](src/netprotocols/_enums.py) as `EtherType` and
|
|
@@ -226,7 +241,7 @@ src/netprotocols/
|
|
|
226
241
|
tests/ one file per protocol + test_contract.py
|
|
227
242
|
(truncation, lying lengths, chain walks),
|
|
228
243
|
test_corpus.py (invariants over the real-capture
|
|
229
|
-
corpus in tests/fixtures/,
|
|
244
|
+
corpus in tests/fixtures/, 93 frames across 16
|
|
230
245
|
scenarios — see its MANIFEST.md), test_checksum.py
|
|
231
246
|
(corpus checksums recompute to wire values), and
|
|
232
247
|
test_fuzz.py (hypothesis properties: decode never
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.3.0] - 2026-09-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **IEEE 802.1Q VLAN tags** (`VLAN`, 802.1Q-2018 §9.6 / 802.1ad QinQ):
|
|
12
|
+
single and stacked (QinQ `0x88A8`, legacy double-tagged `0x9100`)
|
|
13
|
+
tags decode as one layer per tag; the Tag Control Information word is
|
|
14
|
+
split into `pcp`/`dei`/`vid` dataclass fields validated in
|
|
15
|
+
`__post_init__` (`InvalidFieldError`), with the packed 16-bit view
|
|
16
|
+
kept as the `tci` property; `VLAN` is registered in the
|
|
17
|
+
property-based fuzz suite and the `EtherType` display names are
|
|
18
|
+
covered by the enum completeness test.
|
|
19
|
+
- **IGMPv3 group records** (`IGMPv3GroupRecord`, RFC 3376 §4.2): a v3
|
|
20
|
+
Membership Report (type `0x22`) now parses its group-record array on
|
|
21
|
+
demand. `IGMP.group_records` yields one `IGMPv3GroupRecord` per record
|
|
22
|
+
(record type + display name, multicast group, source-address list,
|
|
23
|
+
raw auxiliary data) and `IGMP.num_group_records` reads the count;
|
|
24
|
+
other message types return `None`. Parsing reads the raw body and
|
|
25
|
+
never re-encodes, so the byte-exact round-trip is preserved, and a
|
|
26
|
+
lying record/source count or a truncated record raises
|
|
27
|
+
`InvalidFieldError` (bounded — never hangs or over-reads).
|
|
28
|
+
- **IGMPv3 query fields** (RFC 3376 §4.1): a v3 Membership Query (type
|
|
29
|
+
`0x11`) now exposes its fields past the group address — the `s_flag`
|
|
30
|
+
(suppress router-side processing), `qrv`, `qqic`, and
|
|
31
|
+
`query_source_addresses` accessors parse the raw body on demand. A v2
|
|
32
|
+
(8-byte) query and non-query types return `None`, and a source count
|
|
33
|
+
that runs past the message raises `InvalidFieldError`.
|
|
34
|
+
- **DHCP** (`netprotocols.DHCP`, RFC 2131/2132) — the fixed BOOTP header
|
|
35
|
+
(op, xid, the client/your/server/gateway addresses, the 16-byte client
|
|
36
|
+
hardware address, and the server-name / boot-file fields) decodes in
|
|
37
|
+
full; the magic cookie and TLV options are kept raw and parsed on
|
|
38
|
+
demand. `option_map` walks the options (concatenating a value split
|
|
39
|
+
across appearances, RFC 3396) and `message_type` / `message_type_name`
|
|
40
|
+
read the DHCP message type (option 53). Dispatched from UDP by
|
|
41
|
+
well-known port (67/68); terminal, with a byte-exact round-trip and
|
|
42
|
+
the same bounded-parse contract as DNS (`InvalidFieldError` on a
|
|
43
|
+
missing cookie or an option that overruns the buffer).
|
|
44
|
+
- **GRE** (`netprotocols.GRE`, RFC 2784/2890) — Generic Routing
|
|
45
|
+
Encapsulation, dispatched from IPv4/IPv6 protocol 47. The four-byte
|
|
46
|
+
header (flags/version + protocol type) decodes with the optional
|
|
47
|
+
checksum, key, and sequence-number fields that the flag bits announce
|
|
48
|
+
kept raw and surfaced through accessors. The payload chains onward by
|
|
49
|
+
the `protocol_type` EtherType, so a GRE-tunnelled IPv4/IPv6 packet
|
|
50
|
+
keeps decoding; the round-trip stays byte-exact regardless of which
|
|
51
|
+
optional fields are present.
|
|
52
|
+
- **GRE checksum arm** (`netprotocols.checksum`, RFC 2784 §2.5):
|
|
53
|
+
`compute`/`verify` now cover GRE — the internet checksum over the GRE
|
|
54
|
+
header plus its payload with the checksum field zeroed and no
|
|
55
|
+
pseudo-header. `compute(gre, payload=...)` requires the
|
|
56
|
+
Checksum-Present bit (and its field) and raises `InvalidFieldError`
|
|
57
|
+
otherwise; `verify` of a header whose Checksum-Present bit is clear
|
|
58
|
+
returns `True` — a frame cannot fail a checksum it does not carry —
|
|
59
|
+
mirroring the UDP-over-IPv4 zero rule.
|
|
60
|
+
- **Richer address accessors** (README roadmap): read-only `_address`
|
|
61
|
+
properties return stdlib `ipaddress` objects alongside the canonical
|
|
62
|
+
`str` fields, for comparison, subnet membership, and arithmetic —
|
|
63
|
+
`IPv4.src_address`/`dst_address` and `ARP.spa_address`/`tpa_address`
|
|
64
|
+
as `ipaddress.IPv4Address`, `IPv6.src_address`/`dst_address` as
|
|
65
|
+
`ipaddress.IPv6Address`, and the four `DHCP` address fields as
|
|
66
|
+
`ciaddr_address`/`yiaddr_address`/`siaddr_address`/`giaddr_address`.
|
|
67
|
+
Purely additive: the `str` fields stay the round-tripping
|
|
68
|
+
representation. MAC addresses stay `str` — the stdlib has no EUI
|
|
69
|
+
type.
|
|
70
|
+
- **IPv4 options** (`IPv4Option`, RFC 791 §3.1): the options TLV list
|
|
71
|
+
now parses on demand, mirroring the TCP-option work —
|
|
72
|
+
`IPv4.parsed_options` yields one `IPv4Option` per option in wire
|
|
73
|
+
order (kind + `kind_name`, raw `data`). The common kinds are named:
|
|
74
|
+
End of Option List and No-Operation (single-byte; EOL ends the parse,
|
|
75
|
+
so padding after it is not returned), Record Route (7), Timestamp
|
|
76
|
+
(68), and Router Alert (148, RFC 2113); unknown kinds keep their raw
|
|
77
|
+
`data` with a numeric fallback name. Parsing reads the raw `options`
|
|
78
|
+
bytes and never re-encodes, so the byte-exact round-trip is
|
|
79
|
+
preserved; an option length below the 2-byte minimum or one that runs
|
|
80
|
+
past the options raises `InvalidFieldError` (bounded — never hangs or
|
|
81
|
+
over-reads).
|
|
82
|
+
- **DNS resource records** (`DNSResourceRecord`, RFC 1035 §4.1.3): the
|
|
83
|
+
answer, authority, and additional sections parse on demand —
|
|
84
|
+
`DNS.answers` / `DNS.authorities` / `DNS.additionals` yield records
|
|
85
|
+
(name, type + `rtype_name`, class, TTL, raw `rdata`, and a decoded
|
|
86
|
+
`rdata_text`: IPv4/IPv6 for A/AAAA, the target name for CNAME/NS/PTR,
|
|
87
|
+
and MX/TXT/SOA; hexadecimal otherwise). Parsing reads the raw sections
|
|
88
|
+
and never re-encodes, so the byte-exact round-trip holds; a record or
|
|
89
|
+
compressed name that runs past the message raises `InvalidFieldError`.
|
|
90
|
+
- **TCP options** (`TCPOption`, RFC 9293 §3.1): the options TLV list
|
|
91
|
+
now parses on demand — `TCP.parsed_options` yields one `TCPOption`
|
|
92
|
+
per option in wire order (kind + `kind_name`, raw `data`, and a
|
|
93
|
+
decoded `value` where this library understands the kind: the segment
|
|
94
|
+
size for MSS, the shift count for Window Scale, a `(tsval, tsecr)`
|
|
95
|
+
pair for Timestamps, and `(left_edge, right_edge)` pairs for SACK;
|
|
96
|
+
RFC 7323/2018). EOL and NOP are single-byte (EOL ends the parse);
|
|
97
|
+
unknown kinds keep their raw `data` with `value` degrading to `None`.
|
|
98
|
+
Parsing reads the raw `options` bytes and never re-encodes, so the
|
|
99
|
+
byte-exact round-trip is preserved, and an option length below the
|
|
100
|
+
TLV minimum or one that runs past the options raises
|
|
101
|
+
`InvalidFieldError` (bounded — never hangs or over-reads).
|
|
102
|
+
- **ICMP message bodies** (RFC 792 / RFC 4443): `ICMPv4`/`ICMPv6` gain a
|
|
103
|
+
raw `body` field — the message data after the 8-byte header — so a
|
|
104
|
+
decoded message is self-contained (like `IGMP`/`DNS`), `header_len`
|
|
105
|
+
consumes the whole IP payload, and the layer stays terminal with a
|
|
106
|
+
byte-exact round-trip. Echo requests/replies (v4 types 8/0, v6
|
|
107
|
+
128/129) expose `identifier` / `sequence_number` split from `rest`,
|
|
108
|
+
and the error messages (v4 Destination Unreachable / Redirect / Time
|
|
109
|
+
Exceeded / Parameter Problem; v6 types 1-4) expose `embedded_packet` —
|
|
110
|
+
the invoking datagram, decodable as `IPv4`/`IPv6`. The accessors read
|
|
111
|
+
on demand and degrade to `None` for other message types or an empty
|
|
112
|
+
body, never raising. A decoded message now carries its body in
|
|
113
|
+
`bytes(layer)`, so `checksum.compute`/`verify` need no separate
|
|
114
|
+
`payload` for it (passing one for a header-only object still works).
|
|
115
|
+
- **IPv6 Neighbor Discovery** (`NDPOption`, RFC 4861): `ICMPv6` now
|
|
116
|
+
parses NDP messages on demand — `ndp_target_address` reads the
|
|
117
|
+
16-byte target of a Neighbor Solicitation/Advertisement (135/136),
|
|
118
|
+
and `ndp_options` walks the option TLVs of Router
|
|
119
|
+
Solicitation/Advertisement, Neighbor Solicitation/Advertisement, and
|
|
120
|
+
Redirect at each message's own options offset. Each `NDPOption`
|
|
121
|
+
carries its `type` + `type_name` and raw `data`; a Source/Target
|
|
122
|
+
Link-Layer Address option (1/2) reads back as a MAC string via
|
|
123
|
+
`link_layer_address`, and Prefix Information (3) / MTU (5) stay raw.
|
|
124
|
+
Non-NDP message types return `None`. Option lengths count in 8-octet
|
|
125
|
+
units: a zero length (which must not loop), a length past the
|
|
126
|
+
message, or a body shorter than the message's fixed fields raises
|
|
127
|
+
- **IPv6 extension-header options** (`IPv6Option`, RFC 8200 §4.2): the
|
|
128
|
+
Hop-by-Hop and Destination Options headers now parse their option
|
|
129
|
+
TLVs on demand — `parsed_options` yields one `IPv6Option` per option
|
|
130
|
+
in wire order, padding included (Pad1 is a lone type byte; everything
|
|
131
|
+
else is type/length/data). Each option carries its `type` +
|
|
132
|
+
`type_name` (Pad1, PadN, Router Alert per RFC 2711, Jumbo Payload per
|
|
133
|
+
RFC 2675; unknown types keep their numeric value) and raw `data`, and
|
|
134
|
+
exposes the action-on-unrecognized bits (the two high bits of the
|
|
135
|
+
type) as `unrecognized_action`. Parsing reads the raw `options` bytes
|
|
136
|
+
and never re-encodes, so the byte-exact round-trip is preserved; a
|
|
137
|
+
missing length byte or option data that runs past the header raises
|
|
138
|
+
`InvalidFieldError` (bounded — never hangs or over-reads).
|
|
139
|
+
- **DNS over TCP** (`DNSOverTCP`, RFC 1035 §4.2.2): `TCP.next_protocol()`
|
|
140
|
+
now dispatches application protocols by well-known port
|
|
141
|
+
(`layer4._ports.tcp_app_class`). DNS over TCP is length-prefixed, so
|
|
142
|
+
port 53 chains through a 2-byte `DNSOverTCP` length shim — a layer
|
|
143
|
+
between `TCP` and the `DNS` message, like a VLAN tag between Ethernet
|
|
144
|
+
and its payload — and the walk reaches the DNS message (records and
|
|
145
|
+
all) at its true offset. Non-DNS ports still end the chain at TCP.
|
|
146
|
+
|
|
147
|
+
### Development
|
|
148
|
+
- The real-capture fixture corpus gained `vlan_icmp.pcap` (802.1Q
|
|
149
|
+
single tag VID 100 + 802.1ad QinQ S-VID 200 / C-VID 30, over ARP and
|
|
150
|
+
ICMPv4), so the `VLAN` layer rides the corpus-wide invariants and the
|
|
151
|
+
transport-checksum recompute like every other protocol. It is a
|
|
152
|
+
direct tcpdump capture of real kernel-tagged frames — built over
|
|
153
|
+
802.1Q / 802.1ad vlan devices on a veth pair with VLAN and checksum
|
|
154
|
+
offload disabled so the tags stay in-band in the saved bytes and the
|
|
155
|
+
inner checksums are genuine kernel output; see
|
|
156
|
+
`tests/fixtures/MANIFEST.md` and `scripts/capture_fixtures_vlan.sh`.
|
|
157
|
+
- The corpus gained real-capture `dhcp.pcap` (a DHCP DORA exchange from
|
|
158
|
+
`dnsmasq` + `dhclient`) and `gre.pcap` (IPv4-in-GRE ICMPv4 echo over a
|
|
159
|
+
plain and a keyed kernel tunnel), so the `DHCP` and `GRE` layers ride
|
|
160
|
+
the corpus-wide invariants on genuine bytes.
|
|
161
|
+
`scripts/capture_fixtures_dhcp.sh` and `scripts/capture_fixtures_gre.sh`
|
|
162
|
+
produce them, and `scripts/check_fixtures.py` now peels a GRE header to
|
|
163
|
+
verify the tunnelled inner IPv4/ICMP checksums.
|
|
164
|
+
- The corpus gained real-capture `dns_tcp.pcap` (an A and a TXT query +
|
|
165
|
+
response from `dig +tcp` against `dnsmasq` over a veth pair, offload
|
|
166
|
+
disabled), so the TCP application dispatch and the `DNSOverTCP`
|
|
167
|
+
length shim (#57) ride the corpus-wide invariants and the
|
|
168
|
+
TCP-checksum recompute on genuine bytes — every frame decodes
|
|
169
|
+
`Ethernet→IPv4→TCP→DNSOverTCP→DNS` with the length prefix agreeing
|
|
170
|
+
with the message it frames and a resolvable answer.
|
|
171
|
+
`scripts/capture_fixtures_dns_tcp.sh` produces it; the corpus is now
|
|
172
|
+
97 frames across 17 scenarios.
|
|
173
|
+
- Contributor documentation: a `CONTRIBUTING.md` (dev setup, the QA
|
|
174
|
+
ladder, the decode contract, the add-a-protocol enforcement points,
|
|
175
|
+
and the fixture-capture workflow), a pull-request template, and
|
|
176
|
+
bug-report / protocol-request issue templates under `.github/`.
|
|
177
|
+
- README refresh: the Protocol coverage table gains the shipped `DHCP`
|
|
178
|
+
and `GRE` rows, the `DNS` row notes resource-record parsing and the
|
|
179
|
+
DNS-over-TCP length shim, and the `IGMP` row notes the IGMPv3 report
|
|
180
|
+
and query parsing. The Roadmap section now lists only the remaining
|
|
181
|
+
items, linking the open decoder-depth issues (#59-#66, tracked by
|
|
182
|
+
#67), and the stale corpus figure is corrected to 93 frames across 16
|
|
183
|
+
scenarios here and in `ARCHITECTURE.md`.
|
|
184
|
+
|
|
185
|
+
## [1.2.0] - 2026-08-30
|
|
186
|
+
|
|
187
|
+
### Added
|
|
188
|
+
- **IGMP** (`netprotocols.IGMP`, RFC 1112/2236/3376) — IPv4 multicast
|
|
189
|
+
group management, dispatched from IPv4 protocol 2. The common
|
|
190
|
+
four-byte header (type, max-response code, checksum) is decoded; the
|
|
191
|
+
`group_address` accessor reads the group for the message types that
|
|
192
|
+
carry one, and `netprotocols.checksum` gains an IGMP arm. v3
|
|
193
|
+
group-record parsing is a roadmap follow-up.
|
|
194
|
+
- **DNS** (`netprotocols.DNS`, RFC 1035) — the first application-layer
|
|
195
|
+
protocol. The 12-byte header and flag bits are decoded; the four
|
|
196
|
+
message sections are kept raw with on-demand, read-only accessors
|
|
197
|
+
(`question_name`, `question_type`, `question_class`) that decompress
|
|
198
|
+
names safely (bounded pointer-following; malformed or looping names
|
|
199
|
+
raise `InvalidFieldError`, never hang). Round-trip stays byte-exact.
|
|
200
|
+
- **Port-based dispatch** on UDP: `UDP.next_protocol()` reaches an
|
|
201
|
+
application protocol by well-known port (`netprotocols.layer4._ports`,
|
|
202
|
+
`{53: DNS}` for now), checked destination-then-source. Documented as
|
|
203
|
+
best-effort — a mis-dispatch degrades to the malformed-frame path.
|
|
204
|
+
|
|
205
|
+
## [1.1.0] - 2026-08-29
|
|
206
|
+
|
|
207
|
+
### Added
|
|
208
|
+
- **IPv6 extension headers** (RFC 8200 §4.3-4.6): `IPv6HopByHopOptions`,
|
|
209
|
+
`IPv6Routing`, `IPv6Fragment`, `IPv6DestinationOptions`. The decode
|
|
210
|
+
chain now reaches ICMPv6/TCP/UDP behind them (MLD reports decode
|
|
211
|
+
fully). Extension headers dispatch only inside an IPv6 chain, and a
|
|
212
|
+
Fragment header chains onward only from the first fragment.
|
|
213
|
+
- **Checksums** (`netprotocols.checksum`): `internet_checksum()`
|
|
214
|
+
(RFC 1071), `compute()`/`verify()` covering the IPv4 header checksum,
|
|
215
|
+
ICMPv4, and TCP/UDP/ICMPv6 with IPv4/IPv6 pseudo-headers, and
|
|
216
|
+
`Packet.with_checksums()` for filling a whole stack on encode. The
|
|
217
|
+
UDP-only zero substitution is confined to the UDP arm.
|
|
218
|
+
- **Property-based fuzzing** of the decode path (hypothesis, dev-only):
|
|
219
|
+
decode() raises nothing outside `ProtocolError` for any input; the
|
|
220
|
+
chain walk never consumes past the frame; constrained round trips.
|
|
221
|
+
- `IPProtocol` gains `HOPOPT`, `IPV6_ROUTE`, `IPV6_FRAG`,
|
|
222
|
+
`IPV6_DSTOPTS` with display names.
|
|
223
|
+
|
|
224
|
+
### Development
|
|
225
|
+
- The real-capture fixture corpus grew to 65 frames across 12
|
|
226
|
+
scenarios (MLD behind hop-by-hop, IPv6 fragment pairs); checksum
|
|
227
|
+
tests recompute every verifiable corpus frame to its captured wire
|
|
228
|
+
value.
|
|
229
|
+
|
|
230
|
+
## [1.0.1] - 2026-08-29
|
|
231
|
+
|
|
232
|
+
### Fixed
|
|
233
|
+
- `IPv4.next_protocol()` chained non-first fragments
|
|
234
|
+
(`fragment_offset > 0`) into garbage upper-layer decodes — a slice
|
|
235
|
+
from the middle of a fragmented payload has no header at its start.
|
|
236
|
+
Discovered by the new real-capture fixture corpus on live fragmented
|
|
237
|
+
traffic; the chain now ends at the IPv4 layer for those frames, as it
|
|
238
|
+
already did for unknown protocols.
|
|
239
|
+
|
|
240
|
+
### Added (development)
|
|
241
|
+
- A real-capture fixture corpus under `tests/fixtures/` (56 frames,
|
|
242
|
+
10 scenarios, every checksum internally verified — see its
|
|
243
|
+
MANIFEST.md) with corpus-wide invariant tests, plus the capture and
|
|
244
|
+
validation scripts under `scripts/`.
|
|
245
|
+
|
|
246
|
+
## [1.0.0] - 2026-08-21
|
|
247
|
+
|
|
248
|
+
Complete rewrite. The public API is new; see the README and
|
|
249
|
+
ARCHITECTURE.md for the current usage.
|
|
250
|
+
|
|
251
|
+
### Changed
|
|
252
|
+
- The parsing core moved from `ctypes.BigEndianStructure` subclasses to
|
|
253
|
+
frozen, slotted dataclasses parsed with `struct.Struct`. Decoded and
|
|
254
|
+
constructed instances are now identical in type and compare equal
|
|
255
|
+
field-by-field.
|
|
256
|
+
- `next_protocol()` (returning the class that decodes the payload, or
|
|
257
|
+
`None`) replaces the stringly-typed `encapsulated_proto` chain.
|
|
258
|
+
Protocol number registries are exposed as `EtherType`, `IPProtocol`,
|
|
259
|
+
and `ARPOperation` enums.
|
|
260
|
+
- `header_len` is now an instance property: IPv4 honors IHL and TCP
|
|
261
|
+
honors the data offset, and both expose their raw `options` bytes.
|
|
262
|
+
- Field names modernized (`ethertype`, `src_port`, `total_length`,
|
|
263
|
+
`checksum`, ...); display helpers are properties such as
|
|
264
|
+
`protocol_name`, `flags_str`, and `checksum_hex_str`.
|
|
265
|
+
- Packaging moved to PEP 621 (`hatchling` + `uv`), src layout, Python
|
|
266
|
+
3.12+, MIT license, `py.typed` (mypy strict clean).
|
|
267
|
+
- Validators are plain functions (`validate_mac_addr`,
|
|
268
|
+
`validate_ipv4_addr`) raising exceptions rooted at `ProtocolError`.
|
|
269
|
+
|
|
270
|
+
### Fixed
|
|
271
|
+
- `TCP.header_len` was hardcoded to 32 bytes; the base header is 20 and
|
|
272
|
+
the real length follows the data offset.
|
|
273
|
+
- Decoding ignored IPv4 IHL and TCP data offset, so headers carrying
|
|
274
|
+
options were mis-sliced.
|
|
275
|
+
- Serialization returned a zeroed buffer (`Protocol.__str__`);
|
|
276
|
+
`bytes(header)` now re-emits the exact on-wire form, options included.
|
|
277
|
+
- `Packet` rejected instances of indirect `Protocol` subclasses
|
|
278
|
+
(e.g. `IPv4`, `ICMPv4`) due to validation against direct subclasses
|
|
279
|
+
only; membership is now checked with `isinstance`.
|
|
280
|
+
- Malformed input (truncated buffers, lying IHL/data-offset values) now
|
|
281
|
+
raises typed exceptions (`TruncatedHeaderError`, `InvalidFieldError`)
|
|
282
|
+
instead of arbitrary `ValueError`/`struct.error` leaks.
|
|
283
|
+
|
|
284
|
+
### Removed
|
|
285
|
+
- The `Validator` descriptor infrastructure (it stored state on the
|
|
286
|
+
descriptor itself — class-shared — and was never wired into any
|
|
287
|
+
protocol class).
|
|
288
|
+
- Poetry manifests and the `setup.py`-era metadata.
|
|
289
|
+
|
|
290
|
+
## [0.8.0] - 2022-08-19
|
|
291
|
+
|
|
292
|
+
Last release of the ctypes-based implementation, published to PyPI in
|
|
293
|
+
2022. The repository had drifted behind the published releases; the
|
|
294
|
+
0.8.0 sources were recovered from the PyPI sdist and committed (tagged
|
|
295
|
+
`v0.8.0`) before the 1.0.0 rewrite. Releases 0.4.4 through 0.8.0 exist
|
|
296
|
+
on PyPI only; their individual histories were not preserved.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Contributing to netprotocols
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in contributing. This library decodes and
|
|
4
|
+
builds low-level network protocol headers; correctness against real
|
|
5
|
+
traffic is the whole point, so the bar for changes is a green QA ladder
|
|
6
|
+
and a fixture that proves the change against real bytes.
|
|
7
|
+
|
|
8
|
+
Start with [ARCHITECTURE.md](ARCHITECTURE.md) for the design and the
|
|
9
|
+
add-a-protocol cookbook; this file covers the mechanics.
|
|
10
|
+
|
|
11
|
+
## Development setup
|
|
12
|
+
|
|
13
|
+
Development uses [uv](https://docs.astral.sh/uv/) and targets Python
|
|
14
|
+
3.12+ (CI runs 3.12, 3.13, and 3.14).
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv sync # create the virtualenv and install dev dependencies
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## The QA ladder
|
|
21
|
+
|
|
22
|
+
These four checks are enforced by CI on every push and pull request.
|
|
23
|
+
Run them locally before you push — one validated push beats three
|
|
24
|
+
red ones:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv run ruff check # lint
|
|
28
|
+
uv run ruff format --check # formatting (drop --check to apply)
|
|
29
|
+
uv run mypy # type checking, strict
|
|
30
|
+
uv run pytest # the full suite
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
CI additionally builds the wheel and imports it. `ruff`'s line length
|
|
34
|
+
is 80; Markdown is excluded from linting.
|
|
35
|
+
|
|
36
|
+
## Project layout
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
src/netprotocols/
|
|
40
|
+
_base.py the Protocol base class and the decode contract
|
|
41
|
+
_enums.py EtherType / IPProtocol / ARP* registries + display names
|
|
42
|
+
checksum.py RFC 1071 internet checksum, compute()/verify()
|
|
43
|
+
packet.py Packet: an ordered stack of headers, with_checksums()
|
|
44
|
+
layer2/ Ethernet, VLAN, ARP
|
|
45
|
+
layer3/ IPv4, IPv6, IPv6 extension headers, ICMPv4/v6, IGMP
|
|
46
|
+
layer4/ TCP, UDP (+ port-based dispatch)
|
|
47
|
+
layer7/ DNS
|
|
48
|
+
utils/ exceptions, MAC/IPv4 helpers
|
|
49
|
+
tests/ unit tests, corpus invariants, fuzzing, fixtures/
|
|
50
|
+
scripts/ fixture capture + validation
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## The decode contract
|
|
54
|
+
|
|
55
|
+
Every protocol is a frozen, slotted dataclass whose fields mirror the
|
|
56
|
+
on-wire header. `decode(data)` receives the entire remaining buffer and
|
|
57
|
+
must parse its fixed portion, tolerate trailing bytes, and raise a
|
|
58
|
+
subclass of `ProtocolError` (never a bare `struct.error` or
|
|
59
|
+
`ValueError`) on malformed input. The authoritative description lives in
|
|
60
|
+
the module docstring of
|
|
61
|
+
[`src/netprotocols/_base.py`](src/netprotocols/_base.py) — read it
|
|
62
|
+
before adding a protocol.
|
|
63
|
+
|
|
64
|
+
## Adding a protocol
|
|
65
|
+
|
|
66
|
+
Follow the six-step cookbook in [ARCHITECTURE.md](ARCHITECTURE.md)
|
|
67
|
+
("Cookbook: adding a protocol"). In short:
|
|
68
|
+
|
|
69
|
+
1. Model the fixed header as a frozen, slotted dataclass with a
|
|
70
|
+
class-level `struct.Struct`; split wire bitfields into semantic
|
|
71
|
+
fields (as `IPv6` does with `version`/`traffic_class`/`flow_label`),
|
|
72
|
+
validating ranges in `__post_init__` with `InvalidFieldError`.
|
|
73
|
+
2. Implement `decode()` and `__bytes__()` so `bytes(decode(x)) == x`
|
|
74
|
+
holds for well-formed input.
|
|
75
|
+
3. Wire the chain: the layer below returns your class from
|
|
76
|
+
`next_protocol()`. For a new `EtherType` or IP protocol number, add
|
|
77
|
+
the value to `_enums.py` **with its display name in lockstep** — a
|
|
78
|
+
completeness test enforces this — and one mapping entry in the
|
|
79
|
+
dispatcher, using a deferred import.
|
|
80
|
+
4. Add display helpers that degrade gracefully on unknown values
|
|
81
|
+
(return `"unknown (47)"`, never raise from a display property).
|
|
82
|
+
5. Export the class from the package `__init__.py` and the layer
|
|
83
|
+
`__init__.py`.
|
|
84
|
+
|
|
85
|
+
Two enforcement points that are easy to miss:
|
|
86
|
+
|
|
87
|
+
- **Fuzz registration.** Add the class to `ALL_PROTOCOLS` in
|
|
88
|
+
[`tests/test_fuzz.py`](tests/test_fuzz.py) so the "decode never
|
|
89
|
+
escapes `ProtocolError`" property covers it.
|
|
90
|
+
- **Enum lockstep.** The display-name completeness tests live in
|
|
91
|
+
`tests/test_ipv6_ext.py`; new enum members must have display names.
|
|
92
|
+
|
|
93
|
+
## Tests and the fixture corpus
|
|
94
|
+
|
|
95
|
+
The suite is anchored by a corpus of real captured traffic under
|
|
96
|
+
[`tests/fixtures/`](tests/fixtures/), described frame by frame in
|
|
97
|
+
[`tests/fixtures/MANIFEST.md`](tests/fixtures/MANIFEST.md). The
|
|
98
|
+
corpus-wide invariants and the checksum recompute run over every frame
|
|
99
|
+
automatically, so **dropping a `.pcap` into the corpus is often most of
|
|
100
|
+
the test coverage for a new protocol.**
|
|
101
|
+
|
|
102
|
+
Fixtures are captured and validated with the scripts under
|
|
103
|
+
[`scripts/`](scripts/):
|
|
104
|
+
|
|
105
|
+
- `capture_fixtures*.sh` — capture a scenario with `tcpdump` into
|
|
106
|
+
`tests/fixtures/staging/`, then validate it.
|
|
107
|
+
- `check_fixtures.py` — a standalone (stdlib-only) checksum validator.
|
|
108
|
+
**Every committed fixture must pass it**:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
python3 scripts/check_fixtures.py tests/fixtures/staging
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Prefer a real capture, and build the conditions for one where a
|
|
115
|
+
protocol won't appear in a normal environment. VLAN tags, for instance,
|
|
116
|
+
are stripped by a switch on an access port, so
|
|
117
|
+
`scripts/capture_fixtures_vlan.sh` stands up real 802.1Q / 802.1ad vlan
|
|
118
|
+
devices over a veth pair and captures the kernel-tagged frames on the
|
|
119
|
+
parent device. Only where even that isn't possible — a kernel without
|
|
120
|
+
the `8021q` driver — may a fixture be derived from a real capture, and
|
|
121
|
+
only if the derivation is byte-faithful to the wire and the provenance
|
|
122
|
+
is recorded in the MANIFEST; that script's synthesis fallback (splicing
|
|
123
|
+
tag shims over a real untagged capture) is the worked example.
|
|
124
|
+
|
|
125
|
+
## Changelog
|
|
126
|
+
|
|
127
|
+
This project keeps a [Keep a Changelog](https://keepachangelog.com/)
|
|
128
|
+
file. Add an entry under `## [Unreleased]` in
|
|
129
|
+
[`CHANGELOG.md`](CHANGELOG.md): user-facing changes under `### Added` /
|
|
130
|
+
`### Changed` / `### Fixed`, test-and-tooling changes under
|
|
131
|
+
`### Development`.
|
|
132
|
+
|
|
133
|
+
## Pull requests
|
|
134
|
+
|
|
135
|
+
- Keep changes focused; don't widen a PR beyond what it needs.
|
|
136
|
+
- Fill in the pull-request template and keep the QA ladder green.
|
|
137
|
+
- CI on a first-time fork PR waits for a maintainer to approve the run —
|
|
138
|
+
that's expected, not a failure on your end.
|
|
139
|
+
- Commits and PRs describe the change and the reasoning; the diff shows
|
|
140
|
+
the *what*, the message explains the *why*.
|
|
141
|
+
|
|
142
|
+
By contributing, you agree that your contributions are licensed under
|
|
143
|
+
the project's [MIT license](LICENSE).
|