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.
Files changed (116) hide show
  1. netprotocols-1.3.0/.github/ISSUE_TEMPLATE/bug_report.yml +60 -0
  2. netprotocols-1.3.0/.github/ISSUE_TEMPLATE/config.yml +10 -0
  3. netprotocols-1.3.0/.github/ISSUE_TEMPLATE/protocol_request.yml +66 -0
  4. netprotocols-1.3.0/.github/pull_request_template.md +40 -0
  5. {netprotocols-1.2.0 → netprotocols-1.3.0}/ARCHITECTURE.md +16 -1
  6. netprotocols-1.3.0/CHANGELOG.md +296 -0
  7. netprotocols-1.3.0/CONTRIBUTING.md +143 -0
  8. {netprotocols-1.2.0 → netprotocols-1.3.0}/PKG-INFO +31 -12
  9. {netprotocols-1.2.0 → netprotocols-1.3.0}/README.md +30 -11
  10. {netprotocols-1.2.0 → netprotocols-1.3.0}/pyproject.toml +1 -1
  11. netprotocols-1.3.0/scripts/capture_fixtures_dhcp.sh +94 -0
  12. netprotocols-1.3.0/scripts/capture_fixtures_dns_tcp.sh +112 -0
  13. netprotocols-1.3.0/scripts/capture_fixtures_gre.sh +112 -0
  14. netprotocols-1.3.0/scripts/capture_fixtures_vlan.sh +217 -0
  15. {netprotocols-1.2.0 → netprotocols-1.3.0}/scripts/check_fixtures.py +44 -7
  16. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/__init__.py +20 -6
  17. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/_enums.py +8 -0
  18. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/checksum.py +22 -2
  19. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer2/__init__.py +2 -1
  20. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer2/arp.py +15 -0
  21. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer2/ethernet.py +37 -13
  22. netprotocols-1.3.0/src/netprotocols/layer2/vlan.py +89 -0
  23. netprotocols-1.3.0/src/netprotocols/layer3/gre.py +157 -0
  24. netprotocols-1.3.0/src/netprotocols/layer3/icmp.py +328 -0
  25. netprotocols-1.3.0/src/netprotocols/layer3/igmp.py +269 -0
  26. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer3/ip.py +124 -3
  27. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer3/ipv6_ext.py +91 -2
  28. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer4/_ports.py +22 -5
  29. netprotocols-1.3.0/src/netprotocols/layer4/tcp.py +293 -0
  30. netprotocols-1.3.0/src/netprotocols/layer7/dhcp.py +286 -0
  31. netprotocols-1.3.0/src/netprotocols/layer7/dns.py +436 -0
  32. netprotocols-1.3.0/tests/fixtures/MANIFEST.md +96 -0
  33. netprotocols-1.3.0/tests/fixtures/dhcp.pcap +0 -0
  34. netprotocols-1.3.0/tests/fixtures/dns_tcp.pcap +0 -0
  35. netprotocols-1.3.0/tests/fixtures/gre.pcap +0 -0
  36. netprotocols-1.3.0/tests/fixtures/staging/dhcp.pcap +0 -0
  37. netprotocols-1.3.0/tests/fixtures/staging/dns_tcp.pcap +0 -0
  38. netprotocols-1.3.0/tests/fixtures/staging/gre.pcap +0 -0
  39. netprotocols-1.3.0/tests/fixtures/staging/vlan_icmp.pcap +0 -0
  40. netprotocols-1.3.0/tests/fixtures/vlan_icmp.pcap +0 -0
  41. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/test_arp.py +16 -0
  42. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/test_checksum.py +82 -3
  43. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/test_contract.py +6 -2
  44. netprotocols-1.3.0/tests/test_corpus.py +311 -0
  45. netprotocols-1.3.0/tests/test_dhcp.py +260 -0
  46. netprotocols-1.3.0/tests/test_dns.py +489 -0
  47. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/test_fuzz.py +62 -0
  48. netprotocols-1.3.0/tests/test_gre.py +245 -0
  49. netprotocols-1.3.0/tests/test_icmp.py +217 -0
  50. netprotocols-1.3.0/tests/test_igmp.py +329 -0
  51. netprotocols-1.3.0/tests/test_ip.py +221 -0
  52. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/test_ipv6_ext.py +93 -0
  53. netprotocols-1.3.0/tests/test_tcp.py +204 -0
  54. netprotocols-1.3.0/tests/test_vlan.py +107 -0
  55. {netprotocols-1.2.0 → netprotocols-1.3.0}/uv.lock +1 -1
  56. netprotocols-1.2.0/CHANGELOG.md +0 -119
  57. netprotocols-1.2.0/src/netprotocols/layer3/icmp.py +0 -135
  58. netprotocols-1.2.0/src/netprotocols/layer3/igmp.py +0 -95
  59. netprotocols-1.2.0/src/netprotocols/layer4/tcp.py +0 -153
  60. netprotocols-1.2.0/src/netprotocols/layer7/dns.py +0 -234
  61. netprotocols-1.2.0/tests/fixtures/MANIFEST.md +0 -51
  62. netprotocols-1.2.0/tests/test_corpus.py +0 -167
  63. netprotocols-1.2.0/tests/test_dns.py +0 -168
  64. netprotocols-1.2.0/tests/test_icmp.py +0 -55
  65. netprotocols-1.2.0/tests/test_igmp.py +0 -113
  66. netprotocols-1.2.0/tests/test_ip.py +0 -92
  67. netprotocols-1.2.0/tests/test_tcp.py +0 -80
  68. {netprotocols-1.2.0 → netprotocols-1.3.0}/.github/workflows/ci.yml +0 -0
  69. {netprotocols-1.2.0 → netprotocols-1.3.0}/.github/workflows/release.yml +0 -0
  70. {netprotocols-1.2.0 → netprotocols-1.3.0}/.gitignore +0 -0
  71. {netprotocols-1.2.0 → netprotocols-1.3.0}/LICENSE +0 -0
  72. {netprotocols-1.2.0 → netprotocols-1.3.0}/scripts/capture_fixtures.sh +0 -0
  73. {netprotocols-1.2.0 → netprotocols-1.3.0}/scripts/capture_fixtures_igmp.sh +0 -0
  74. {netprotocols-1.2.0 → netprotocols-1.3.0}/scripts/capture_fixtures_supplement.sh +0 -0
  75. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/_base.py +0 -0
  76. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer3/__init__.py +0 -0
  77. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer4/__init__.py +0 -0
  78. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer4/udp.py +0 -0
  79. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/layer7/__init__.py +0 -0
  80. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/packet.py +0 -0
  81. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/py.typed +0 -0
  82. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/utils/__init__.py +0 -0
  83. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/utils/exceptions.py +0 -0
  84. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/utils/ipv4.py +0 -0
  85. {netprotocols-1.2.0 → netprotocols-1.3.0}/src/netprotocols/utils/mac.py +0 -0
  86. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/conftest.py +0 -0
  87. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/arp_exchange.pcap +0 -0
  88. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/icmpv4_echo_lo.pcap +0 -0
  89. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/icmpv4_external.pcap +0 -0
  90. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/icmpv4_ttl_exceeded.pcap +0 -0
  91. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/icmpv6_echo_lo.pcap +0 -0
  92. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/igmp.pcap +0 -0
  93. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/ipv4_fragments.pcap +0 -0
  94. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/ipv6_fragments.pcap +0 -0
  95. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/ipv6_mld.pcap +0 -0
  96. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/ipv6_ndp_mld.pcap +0 -0
  97. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/arp_exchange.pcap +0 -0
  98. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/icmpv4_echo_lo.pcap +0 -0
  99. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/icmpv4_external.pcap +0 -0
  100. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/icmpv4_ttl_exceeded.pcap +0 -0
  101. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/icmpv6_echo_lo.pcap +0 -0
  102. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/igmp.pcap +0 -0
  103. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/ipv4_fragments.pcap +0 -0
  104. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/ipv6_fragments.pcap +0 -0
  105. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/ipv6_mld.pcap +0 -0
  106. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/ipv6_ndp_mld.pcap +0 -0
  107. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/tcp_http.pcap +0 -0
  108. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/tcp_https.pcap +0 -0
  109. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/staging/udp_dns.pcap +0 -0
  110. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/tcp_http.pcap +0 -0
  111. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/tcp_https.pcap +0 -0
  112. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/fixtures/udp_dns.pcap +0 -0
  113. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/test_ethernet.py +0 -0
  114. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/test_packet.py +0 -0
  115. {netprotocols-1.2.0 → netprotocols-1.3.0}/tests/test_udp.py +0 -0
  116. {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/, 65 frames across 12
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).