netprotocols 1.1.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.1.0 → netprotocols-1.3.0}/ARCHITECTURE.md +69 -11
- netprotocols-1.3.0/CHANGELOG.md +296 -0
- netprotocols-1.3.0/CONTRIBUTING.md +143 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/PKG-INFO +35 -6
- {netprotocols-1.1.0 → netprotocols-1.3.0}/README.md +34 -5
- {netprotocols-1.1.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_igmp.sh +66 -0
- netprotocols-1.3.0/scripts/capture_fixtures_vlan.sh +217 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/scripts/check_fixtures.py +52 -7
- {netprotocols-1.1.0 → netprotocols-1.3.0}/src/netprotocols/__init__.py +22 -4
- {netprotocols-1.1.0 → netprotocols-1.3.0}/src/netprotocols/_enums.py +8 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/src/netprotocols/checksum.py +27 -2
- {netprotocols-1.1.0 → netprotocols-1.3.0}/src/netprotocols/layer2/__init__.py +2 -1
- {netprotocols-1.1.0 → netprotocols-1.3.0}/src/netprotocols/layer2/arp.py +15 -0
- {netprotocols-1.1.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.1.0 → netprotocols-1.3.0}/src/netprotocols/layer3/__init__.py +2 -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.1.0 → netprotocols-1.3.0}/src/netprotocols/layer3/ip.py +126 -3
- {netprotocols-1.1.0 → netprotocols-1.3.0}/src/netprotocols/layer3/ipv6_ext.py +91 -2
- netprotocols-1.3.0/src/netprotocols/layer4/_ports.py +51 -0
- netprotocols-1.3.0/src/netprotocols/layer4/tcp.py +293 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/src/netprotocols/layer4/udp.py +9 -0
- netprotocols-1.3.0/src/netprotocols/layer7/__init__.py +3 -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/igmp.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/igmp.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.1.0 → netprotocols-1.3.0}/tests/test_arp.py +16 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/test_checksum.py +106 -23
- {netprotocols-1.1.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.1.0 → netprotocols-1.3.0}/tests/test_fuzz.py +80 -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.1.0 → netprotocols-1.3.0}/tests/test_ipv6_ext.py +93 -0
- netprotocols-1.3.0/tests/test_tcp.py +204 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/test_udp.py +9 -2
- netprotocols-1.3.0/tests/test_vlan.py +107 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/uv.lock +1 -1
- netprotocols-1.1.0/CHANGELOG.md +0 -99
- netprotocols-1.1.0/src/netprotocols/layer3/icmp.py +0 -135
- netprotocols-1.1.0/src/netprotocols/layer4/tcp.py +0 -153
- netprotocols-1.1.0/tests/fixtures/MANIFEST.md +0 -50
- netprotocols-1.1.0/tests/test_corpus.py +0 -167
- netprotocols-1.1.0/tests/test_icmp.py +0 -55
- netprotocols-1.1.0/tests/test_ip.py +0 -92
- netprotocols-1.1.0/tests/test_tcp.py +0 -80
- {netprotocols-1.1.0 → netprotocols-1.3.0}/.github/workflows/ci.yml +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/.github/workflows/release.yml +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/.gitignore +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/LICENSE +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/scripts/capture_fixtures.sh +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/scripts/capture_fixtures_supplement.sh +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/src/netprotocols/_base.py +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/src/netprotocols/layer4/__init__.py +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/src/netprotocols/packet.py +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/src/netprotocols/py.typed +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/src/netprotocols/utils/__init__.py +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/src/netprotocols/utils/exceptions.py +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/src/netprotocols/utils/ipv4.py +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/src/netprotocols/utils/mac.py +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/conftest.py +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/arp_exchange.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/icmpv4_echo_lo.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/icmpv4_external.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/icmpv4_ttl_exceeded.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/icmpv6_echo_lo.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/ipv4_fragments.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/ipv6_fragments.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/ipv6_mld.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/ipv6_ndp_mld.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/staging/arp_exchange.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/staging/icmpv4_echo_lo.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/staging/icmpv4_external.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/staging/icmpv4_ttl_exceeded.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/staging/icmpv6_echo_lo.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/staging/ipv4_fragments.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/staging/ipv6_fragments.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/staging/ipv6_mld.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/staging/ipv6_ndp_mld.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/staging/tcp_http.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/staging/tcp_https.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/staging/udp_dns.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/tcp_http.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/tcp_https.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/fixtures/udp_dns.pcap +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/test_ethernet.py +0 -0
- {netprotocols-1.1.0 → netprotocols-1.3.0}/tests/test_packet.py +0 -0
- {netprotocols-1.1.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
|
|
@@ -158,6 +173,26 @@ the graph acyclic: shared registries live in the dependency-free
|
|
|
158
173
|
`_enums.py`, and `next_protocol()` mappings import their target
|
|
159
174
|
classes *inside the function body*, at call time.
|
|
160
175
|
|
|
176
|
+
## Port-based dispatch is best-effort
|
|
177
|
+
|
|
178
|
+
The EtherType and IP-protocol registries dispatch on a single
|
|
179
|
+
authoritative field. Application protocols break that model: they are
|
|
180
|
+
identified by *port*, which is a heuristic — any service may run on any
|
|
181
|
+
port, and the discriminator is split across the source and destination
|
|
182
|
+
ports. `UDP.next_protocol()` therefore consults a well-known-port
|
|
183
|
+
registry (`netprotocols.layer4._ports`), checking the destination port
|
|
184
|
+
first (a request targets the server's port) and then the source port
|
|
185
|
+
(a response comes from it). `DNS` is the first such protocol; a DNS
|
|
186
|
+
message on UDP port 53 now decodes as a fourth layer.
|
|
187
|
+
|
|
188
|
+
Because the guess can be wrong, the application class validates
|
|
189
|
+
strictly on decode: a non-DNS datagram that happens to use port 53
|
|
190
|
+
raises a `ProtocolError`, which the ordinary decode-error path absorbs
|
|
191
|
+
(the chain keeps the layers it did decode and records the failure). So
|
|
192
|
+
a mis-dispatch degrades to a diagnosed frame, never to garbage. Only
|
|
193
|
+
UDP is wired this way for now — DNS over TCP carries a 2-byte length
|
|
194
|
+
prefix (RFC 1035 §4.2.2) that needs separate handling.
|
|
195
|
+
|
|
161
196
|
## The encode side, and the round-trip guarantee
|
|
162
197
|
|
|
163
198
|
Every class also works in reverse. Constructors take the friendly
|
|
@@ -174,10 +209,14 @@ Protocol.decode(bytes(header)) == header # object → wire → object
|
|
|
174
209
|
bytes(Protocol.decode(raw)) == raw[:header_len] # wire → object → wire
|
|
175
210
|
```
|
|
176
211
|
|
|
177
|
-
One deliberate
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
212
|
+
One deliberate boundary: **decode and encode carry checksums
|
|
213
|
+
verbatim** — exactly what you want when faithfully re-encoding
|
|
214
|
+
captured traffic. Computing and verifying them is explicit, via
|
|
215
|
+
`netprotocols.checksum`: `compute()`/`verify()` handle the IPv4 header
|
|
216
|
+
checksum, ICMPv4, and TCP/UDP/ICMPv6 with their IPv4/IPv6
|
|
217
|
+
pseudo-headers, and `Packet.with_checksums(payload)` fills a whole
|
|
218
|
+
stack before sending. (The RFC 768 subtlety lives where it belongs:
|
|
219
|
+
the `0x0000 → 0xFFFF` substitution applies to UDP alone.)
|
|
181
220
|
|
|
182
221
|
`Packet` is the thin composition layer: it holds an ordered tuple of
|
|
183
222
|
headers and `bytes(packet)` joins them, ready for a raw socket.
|
|
@@ -189,13 +228,24 @@ src/netprotocols/
|
|
|
189
228
|
├── __init__.py public API re-exports, __version__
|
|
190
229
|
├── _base.py Protocol ABC, decode contract, address helpers
|
|
191
230
|
├── _enums.py EtherType, IPProtocol, ARPOperation (imports nothing)
|
|
192
|
-
├── packet.py Packet composition
|
|
231
|
+
├── packet.py Packet composition, with_checksums()
|
|
232
|
+
├── checksum.py RFC 1071: internet_checksum, compute, verify
|
|
193
233
|
├── layer2/ ethernet.py, arp.py
|
|
194
|
-
├── layer3/ ip.py (IPv4 + IPv6), icmp.py (ICMPv4 + ICMPv6)
|
|
195
|
-
|
|
234
|
+
├── layer3/ ip.py (IPv4 + IPv6), icmp.py (ICMPv4 + ICMPv6),
|
|
235
|
+
│ igmp.py,
|
|
236
|
+
│ ipv6_ext.py (Hop-by-Hop, Routing, Fragment,
|
|
237
|
+
│ Destination Options)
|
|
238
|
+
├── layer4/ tcp.py, udp.py, _ports.py (well-known-port registry)
|
|
239
|
+
├── layer7/ dns.py
|
|
196
240
|
└── utils/ validators (mac.py, ipv4.py), exceptions.py
|
|
197
241
|
tests/ one file per protocol + test_contract.py
|
|
198
|
-
(truncation, lying lengths, chain walks)
|
|
242
|
+
(truncation, lying lengths, chain walks),
|
|
243
|
+
test_corpus.py (invariants over the real-capture
|
|
244
|
+
corpus in tests/fixtures/, 93 frames across 16
|
|
245
|
+
scenarios — see its MANIFEST.md), test_checksum.py
|
|
246
|
+
(corpus checksums recompute to wire values), and
|
|
247
|
+
test_fuzz.py (hypothesis properties: decode never
|
|
248
|
+
raises outside ProtocolError)
|
|
199
249
|
```
|
|
200
250
|
|
|
201
251
|
Tooling: [uv](https://docs.astral.sh/uv/) manages the environment
|
|
@@ -217,12 +267,20 @@ the same:
|
|
|
217
267
|
(raise `TruncatedHeaderError` when the buffer can't satisfy it) and
|
|
218
268
|
materialize the variable part as `bytes`.
|
|
219
269
|
3. **Implement `__bytes__()`.** Pack the same fields back; the
|
|
220
|
-
round-trip tests will hold you honest.
|
|
270
|
+
round-trip tests will hold you honest. When a payload is
|
|
271
|
+
self-referential — DNS names compress by pointing at earlier bytes,
|
|
272
|
+
so re-encoding a parsed record cannot reproduce the original — keep
|
|
273
|
+
that region as raw `bytes` and parse it through read-only accessors
|
|
274
|
+
that never re-encode. `bytes(decode(x)) == x` then holds by
|
|
275
|
+
construction (see `layer7/dns.py`).
|
|
221
276
|
4. **Wire the chain.** The layer below decides when your class is
|
|
222
277
|
next. For DNS that means UDP would implement `next_protocol()`
|
|
223
278
|
consulting the ports. (For a new EtherType or IP protocol number,
|
|
224
|
-
add the value to `_enums.py`
|
|
225
|
-
`ethernet.py`/`ip.py`, using
|
|
279
|
+
add the value to `_enums.py` — display name in lockstep, a test
|
|
280
|
+
enforces it — and one mapping entry in `ethernet.py`/`ip.py`, using
|
|
281
|
+
a deferred import. Numbers valid only inside an IPv6 chain follow
|
|
282
|
+
the extension headers' example: `_ip_protocol_class` hands them out
|
|
283
|
+
only when `ipv6=True`.)
|
|
226
284
|
5. **Add display properties** for anything a human would want rendered
|
|
227
285
|
(`flags_str`-style names, hex strings). Degrade gracefully on
|
|
228
286
|
unknown values — return `"unknown (47)"`, never raise from a
|
|
@@ -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.
|