netprotocols 1.0.1__tar.gz → 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. {netprotocols-1.0.1 → netprotocols-1.2.0}/ARCHITECTURE.md +69 -11
  2. {netprotocols-1.0.1 → netprotocols-1.2.0}/CHANGELOG.md +45 -0
  3. {netprotocols-1.0.1 → netprotocols-1.2.0}/PKG-INFO +23 -13
  4. {netprotocols-1.0.1 → netprotocols-1.2.0}/README.md +22 -12
  5. {netprotocols-1.0.1 → netprotocols-1.2.0}/pyproject.toml +2 -1
  6. netprotocols-1.2.0/scripts/capture_fixtures_igmp.sh +66 -0
  7. {netprotocols-1.0.1 → netprotocols-1.2.0}/scripts/capture_fixtures_supplement.sh +0 -0
  8. {netprotocols-1.0.1 → netprotocols-1.2.0}/scripts/check_fixtures.py +14 -3
  9. {netprotocols-1.0.1 → netprotocols-1.2.0}/src/netprotocols/__init__.py +19 -1
  10. {netprotocols-1.0.1 → netprotocols-1.2.0}/src/netprotocols/_enums.py +8 -0
  11. netprotocols-1.2.0/src/netprotocols/checksum.py +163 -0
  12. netprotocols-1.2.0/src/netprotocols/layer3/__init__.py +21 -0
  13. netprotocols-1.2.0/src/netprotocols/layer3/igmp.py +95 -0
  14. {netprotocols-1.0.1 → netprotocols-1.2.0}/src/netprotocols/layer3/ip.py +34 -4
  15. netprotocols-1.2.0/src/netprotocols/layer3/ipv6_ext.py +253 -0
  16. netprotocols-1.2.0/src/netprotocols/layer4/_ports.py +34 -0
  17. {netprotocols-1.0.1 → netprotocols-1.2.0}/src/netprotocols/layer4/udp.py +9 -0
  18. netprotocols-1.2.0/src/netprotocols/layer7/__init__.py +3 -0
  19. netprotocols-1.2.0/src/netprotocols/layer7/dns.py +234 -0
  20. netprotocols-1.2.0/src/netprotocols/packet.py +88 -0
  21. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/MANIFEST.md +17 -14
  22. netprotocols-1.2.0/tests/fixtures/igmp.pcap +0 -0
  23. netprotocols-1.2.0/tests/fixtures/ipv6_fragments.pcap +0 -0
  24. netprotocols-1.2.0/tests/fixtures/ipv6_mld.pcap +0 -0
  25. netprotocols-1.2.0/tests/fixtures/staging/igmp.pcap +0 -0
  26. netprotocols-1.2.0/tests/fixtures/staging/ipv6_fragments.pcap +0 -0
  27. netprotocols-1.2.0/tests/fixtures/staging/ipv6_mld.pcap +0 -0
  28. netprotocols-1.2.0/tests/test_checksum.py +243 -0
  29. netprotocols-1.2.0/tests/test_dns.py +168 -0
  30. netprotocols-1.2.0/tests/test_fuzz.py +200 -0
  31. netprotocols-1.2.0/tests/test_igmp.py +113 -0
  32. netprotocols-1.2.0/tests/test_ipv6_ext.py +177 -0
  33. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/test_udp.py +9 -2
  34. {netprotocols-1.0.1 → netprotocols-1.2.0}/uv.lock +84 -1
  35. netprotocols-1.0.1/src/netprotocols/layer3/__init__.py +0 -4
  36. netprotocols-1.0.1/src/netprotocols/packet.py +0 -48
  37. {netprotocols-1.0.1 → netprotocols-1.2.0}/.github/workflows/ci.yml +0 -0
  38. {netprotocols-1.0.1 → netprotocols-1.2.0}/.github/workflows/release.yml +0 -0
  39. {netprotocols-1.0.1 → netprotocols-1.2.0}/.gitignore +0 -0
  40. {netprotocols-1.0.1 → netprotocols-1.2.0}/LICENSE +0 -0
  41. {netprotocols-1.0.1 → netprotocols-1.2.0}/scripts/capture_fixtures.sh +0 -0
  42. {netprotocols-1.0.1 → netprotocols-1.2.0}/src/netprotocols/_base.py +0 -0
  43. {netprotocols-1.0.1 → netprotocols-1.2.0}/src/netprotocols/layer2/__init__.py +0 -0
  44. {netprotocols-1.0.1 → netprotocols-1.2.0}/src/netprotocols/layer2/arp.py +0 -0
  45. {netprotocols-1.0.1 → netprotocols-1.2.0}/src/netprotocols/layer2/ethernet.py +0 -0
  46. {netprotocols-1.0.1 → netprotocols-1.2.0}/src/netprotocols/layer3/icmp.py +0 -0
  47. {netprotocols-1.0.1 → netprotocols-1.2.0}/src/netprotocols/layer4/__init__.py +0 -0
  48. {netprotocols-1.0.1 → netprotocols-1.2.0}/src/netprotocols/layer4/tcp.py +0 -0
  49. {netprotocols-1.0.1 → netprotocols-1.2.0}/src/netprotocols/py.typed +0 -0
  50. {netprotocols-1.0.1 → netprotocols-1.2.0}/src/netprotocols/utils/__init__.py +0 -0
  51. {netprotocols-1.0.1 → netprotocols-1.2.0}/src/netprotocols/utils/exceptions.py +0 -0
  52. {netprotocols-1.0.1 → netprotocols-1.2.0}/src/netprotocols/utils/ipv4.py +0 -0
  53. {netprotocols-1.0.1 → netprotocols-1.2.0}/src/netprotocols/utils/mac.py +0 -0
  54. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/conftest.py +0 -0
  55. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/arp_exchange.pcap +0 -0
  56. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/icmpv4_echo_lo.pcap +0 -0
  57. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/icmpv4_external.pcap +0 -0
  58. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/icmpv4_ttl_exceeded.pcap +0 -0
  59. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/icmpv6_echo_lo.pcap +0 -0
  60. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/ipv4_fragments.pcap +0 -0
  61. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/ipv6_ndp_mld.pcap +0 -0
  62. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/staging/arp_exchange.pcap +0 -0
  63. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/staging/icmpv4_echo_lo.pcap +0 -0
  64. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/staging/icmpv4_external.pcap +0 -0
  65. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/staging/icmpv4_ttl_exceeded.pcap +0 -0
  66. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/staging/icmpv6_echo_lo.pcap +0 -0
  67. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/staging/ipv4_fragments.pcap +0 -0
  68. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/staging/ipv6_ndp_mld.pcap +0 -0
  69. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/staging/tcp_http.pcap +0 -0
  70. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/staging/tcp_https.pcap +0 -0
  71. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/staging/udp_dns.pcap +0 -0
  72. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/tcp_http.pcap +0 -0
  73. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/tcp_https.pcap +0 -0
  74. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/fixtures/udp_dns.pcap +0 -0
  75. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/test_arp.py +0 -0
  76. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/test_contract.py +0 -0
  77. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/test_corpus.py +0 -0
  78. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/test_ethernet.py +0 -0
  79. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/test_icmp.py +0 -0
  80. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/test_ip.py +0 -0
  81. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/test_packet.py +0 -0
  82. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/test_tcp.py +0 -0
  83. {netprotocols-1.0.1 → netprotocols-1.2.0}/tests/test_utils.py +0 -0
@@ -115,8 +115,23 @@ flowchart LR
115
115
  I6 -->|58| IC6[ICMPv6]
116
116
  I6 -->|6| T
117
117
  I6 -->|17| U
118
+ I6 -->|0, 43, 44, 60| X[IPv6 extension headers]
119
+ X --> X
120
+ X -->|58| IC6
121
+ X -->|6| T
122
+ X -->|17| U
118
123
  ```
119
124
 
125
+ IPv6 extension headers (Hop-by-Hop Options, Routing, Fragment,
126
+ Destination Options) chain like any other protocol — each names what
127
+ follows via `next_header` — and may stack. Two guardrails: the
128
+ registry hands them out only inside an IPv6 chain (a garbage IPv4
129
+ packet with `protocol=0` cannot conjure a Hop-by-Hop layer), and a
130
+ Fragment header chains onward only when `fragment_offset == 0` — a
131
+ non-first fragment carries a slice from the middle of the original
132
+ payload, so no upper-layer header exists at its start. The same
133
+ offset rule applies to fragmented IPv4.
134
+
120
135
  The numbers on the arrows are the wire values — EtherTypes out of
121
136
  Ethernet, IP protocol numbers out of IPv4/IPv6 — and they live in
122
137
  [`_enums.py`](src/netprotocols/_enums.py) as `EtherType` and
@@ -143,6 +158,26 @@ the graph acyclic: shared registries live in the dependency-free
143
158
  `_enums.py`, and `next_protocol()` mappings import their target
144
159
  classes *inside the function body*, at call time.
145
160
 
161
+ ## Port-based dispatch is best-effort
162
+
163
+ The EtherType and IP-protocol registries dispatch on a single
164
+ authoritative field. Application protocols break that model: they are
165
+ identified by *port*, which is a heuristic — any service may run on any
166
+ port, and the discriminator is split across the source and destination
167
+ ports. `UDP.next_protocol()` therefore consults a well-known-port
168
+ registry (`netprotocols.layer4._ports`), checking the destination port
169
+ first (a request targets the server's port) and then the source port
170
+ (a response comes from it). `DNS` is the first such protocol; a DNS
171
+ message on UDP port 53 now decodes as a fourth layer.
172
+
173
+ Because the guess can be wrong, the application class validates
174
+ strictly on decode: a non-DNS datagram that happens to use port 53
175
+ raises a `ProtocolError`, which the ordinary decode-error path absorbs
176
+ (the chain keeps the layers it did decode and records the failure). So
177
+ a mis-dispatch degrades to a diagnosed frame, never to garbage. Only
178
+ UDP is wired this way for now — DNS over TCP carries a 2-byte length
179
+ prefix (RFC 1035 §4.2.2) that needs separate handling.
180
+
146
181
  ## The encode side, and the round-trip guarantee
147
182
 
148
183
  Every class also works in reverse. Constructors take the friendly
@@ -159,10 +194,14 @@ Protocol.decode(bytes(header)) == header # object → wire → object
159
194
  bytes(Protocol.decode(raw)) == raw[:header_len] # wire → object → wire
160
195
  ```
161
196
 
162
- One deliberate exception: **checksums are carried verbatim**. The
163
- library neither computes nor verifies them (yet — it's on the
164
- roadmap), which is exactly what you want when faithfully re-encoding
165
- captured traffic.
197
+ One deliberate boundary: **decode and encode carry checksums
198
+ verbatim** — exactly what you want when faithfully re-encoding
199
+ captured traffic. Computing and verifying them is explicit, via
200
+ `netprotocols.checksum`: `compute()`/`verify()` handle the IPv4 header
201
+ checksum, ICMPv4, and TCP/UDP/ICMPv6 with their IPv4/IPv6
202
+ pseudo-headers, and `Packet.with_checksums(payload)` fills a whole
203
+ stack before sending. (The RFC 768 subtlety lives where it belongs:
204
+ the `0x0000 → 0xFFFF` substitution applies to UDP alone.)
166
205
 
167
206
  `Packet` is the thin composition layer: it holds an ordered tuple of
168
207
  headers and `bytes(packet)` joins them, ready for a raw socket.
@@ -174,13 +213,24 @@ src/netprotocols/
174
213
  ├── __init__.py public API re-exports, __version__
175
214
  ├── _base.py Protocol ABC, decode contract, address helpers
176
215
  ├── _enums.py EtherType, IPProtocol, ARPOperation (imports nothing)
177
- ├── packet.py Packet composition
216
+ ├── packet.py Packet composition, with_checksums()
217
+ ├── checksum.py RFC 1071: internet_checksum, compute, verify
178
218
  ├── layer2/ ethernet.py, arp.py
179
- ├── layer3/ ip.py (IPv4 + IPv6), icmp.py (ICMPv4 + ICMPv6)
180
- ├── layer4/ tcp.py, udp.py
219
+ ├── layer3/ ip.py (IPv4 + IPv6), icmp.py (ICMPv4 + ICMPv6),
220
+ │ igmp.py,
221
+ │ ipv6_ext.py (Hop-by-Hop, Routing, Fragment,
222
+ │ Destination Options)
223
+ ├── layer4/ tcp.py, udp.py, _ports.py (well-known-port registry)
224
+ ├── layer7/ dns.py
181
225
  └── utils/ validators (mac.py, ipv4.py), exceptions.py
182
226
  tests/ one file per protocol + test_contract.py
183
- (truncation, lying lengths, chain walks)
227
+ (truncation, lying lengths, chain walks),
228
+ test_corpus.py (invariants over the real-capture
229
+ corpus in tests/fixtures/, 65 frames across 12
230
+ scenarios — see its MANIFEST.md), test_checksum.py
231
+ (corpus checksums recompute to wire values), and
232
+ test_fuzz.py (hypothesis properties: decode never
233
+ raises outside ProtocolError)
184
234
  ```
185
235
 
186
236
  Tooling: [uv](https://docs.astral.sh/uv/) manages the environment
@@ -202,12 +252,20 @@ the same:
202
252
  (raise `TruncatedHeaderError` when the buffer can't satisfy it) and
203
253
  materialize the variable part as `bytes`.
204
254
  3. **Implement `__bytes__()`.** Pack the same fields back; the
205
- round-trip tests will hold you honest.
255
+ round-trip tests will hold you honest. When a payload is
256
+ self-referential — DNS names compress by pointing at earlier bytes,
257
+ so re-encoding a parsed record cannot reproduce the original — keep
258
+ that region as raw `bytes` and parse it through read-only accessors
259
+ that never re-encode. `bytes(decode(x)) == x` then holds by
260
+ construction (see `layer7/dns.py`).
206
261
  4. **Wire the chain.** The layer below decides when your class is
207
262
  next. For DNS that means UDP would implement `next_protocol()`
208
263
  consulting the ports. (For a new EtherType or IP protocol number,
209
- add the value to `_enums.py` and one mapping entry in
210
- `ethernet.py`/`ip.py`, using a deferred import.)
264
+ add the value to `_enums.py` — display name in lockstep, a test
265
+ enforces it — and one mapping entry in `ethernet.py`/`ip.py`, using
266
+ a deferred import. Numbers valid only inside an IPv6 chain follow
267
+ the extension headers' example: `_ip_protocol_class` hands them out
268
+ only when `ipv6=True`.)
211
269
  5. **Add display properties** for anything a human would want rendered
212
270
  (`flags_str`-style names, hex strings). Degrade gracefully on
213
271
  unknown values — return `"unknown (47)"`, never raise from a
@@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.0] - 2026-08-30
9
+
10
+ ### Added
11
+ - **IGMP** (`netprotocols.IGMP`, RFC 1112/2236/3376) — IPv4 multicast
12
+ group management, dispatched from IPv4 protocol 2. The common
13
+ four-byte header (type, max-response code, checksum) is decoded; the
14
+ `group_address` accessor reads the group for the message types that
15
+ carry one, and `netprotocols.checksum` gains an IGMP arm. v3
16
+ group-record parsing is a roadmap follow-up.
17
+ - **DNS** (`netprotocols.DNS`, RFC 1035) — the first application-layer
18
+ protocol. The 12-byte header and flag bits are decoded; the four
19
+ message sections are kept raw with on-demand, read-only accessors
20
+ (`question_name`, `question_type`, `question_class`) that decompress
21
+ names safely (bounded pointer-following; malformed or looping names
22
+ raise `InvalidFieldError`, never hang). Round-trip stays byte-exact.
23
+ - **Port-based dispatch** on UDP: `UDP.next_protocol()` reaches an
24
+ application protocol by well-known port (`netprotocols.layer4._ports`,
25
+ `{53: DNS}` for now), checked destination-then-source. Documented as
26
+ best-effort — a mis-dispatch degrades to the malformed-frame path.
27
+
28
+ ## [1.1.0] - 2026-08-29
29
+
30
+ ### Added
31
+ - **IPv6 extension headers** (RFC 8200 §4.3-4.6): `IPv6HopByHopOptions`,
32
+ `IPv6Routing`, `IPv6Fragment`, `IPv6DestinationOptions`. The decode
33
+ chain now reaches ICMPv6/TCP/UDP behind them (MLD reports decode
34
+ fully). Extension headers dispatch only inside an IPv6 chain, and a
35
+ Fragment header chains onward only from the first fragment.
36
+ - **Checksums** (`netprotocols.checksum`): `internet_checksum()`
37
+ (RFC 1071), `compute()`/`verify()` covering the IPv4 header checksum,
38
+ ICMPv4, and TCP/UDP/ICMPv6 with IPv4/IPv6 pseudo-headers, and
39
+ `Packet.with_checksums()` for filling a whole stack on encode. The
40
+ UDP-only zero substitution is confined to the UDP arm.
41
+ - **Property-based fuzzing** of the decode path (hypothesis, dev-only):
42
+ decode() raises nothing outside `ProtocolError` for any input; the
43
+ chain walk never consumes past the frame; constrained round trips.
44
+ - `IPProtocol` gains `HOPOPT`, `IPV6_ROUTE`, `IPV6_FRAG`,
45
+ `IPV6_DSTOPTS` with display names.
46
+
47
+ ### Development
48
+ - The real-capture fixture corpus grew to 65 frames across 12
49
+ scenarios (MLD behind hop-by-hop, IPv6 fragment pairs); checksum
50
+ tests recompute every verifiable corpus frame to its captured wire
51
+ value.
52
+
8
53
  ## [1.0.1] - 2026-08-29
9
54
 
10
55
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: netprotocols
3
- Version: 1.0.1
3
+ Version: 1.2.0
4
4
  Summary: Low-level implementations of common networking protocols
5
5
  Project-URL: Homepage, https://github.com/EONRaider/NETProtocols
6
6
  Project-URL: Repository, https://github.com/EONRaider/NETProtocols
@@ -67,11 +67,17 @@ Requires Python 3.12+. Fully typed (`py.typed`, mypy strict).
67
67
  | 2 | Ethernet II | `Ethernet` | IEEE 802.3 |
68
68
  | 2 | ARP | `ARP` | RFC 826, IPv4-over-Ethernet binding |
69
69
  | 3 | IPv4 | `IPv4` | RFC 791, IHL/options aware |
70
- | 3 | IPv6 | `IPv6` | RFC 8200, fixed header |
70
+ | 3 | IPv6 | `IPv6` | RFC 8200 |
71
+ | 3 | IPv6 Hop-by-Hop Options | `IPv6HopByHopOptions` | RFC 8200 §4.3 |
72
+ | 3 | IPv6 Routing | `IPv6Routing` | RFC 8200 §4.4 |
73
+ | 3 | IPv6 Fragment | `IPv6Fragment` | RFC 8200 §4.5, first-fragment chaining |
74
+ | 3 | IPv6 Destination Options | `IPv6DestinationOptions` | RFC 8200 §4.6 |
71
75
  | 3 | ICMPv4 | `ICMPv4` | RFC 792, 8-byte header |
72
76
  | 3 | ICMPv6 | `ICMPv6` | RFC 4443, 8-byte header |
77
+ | 3 | IGMP | `IGMP` | RFC 1112/2236/3376, IPv4 multicast management |
73
78
  | 4 | TCP | `TCP` | RFC 9293, data-offset/options aware |
74
79
  | 4 | UDP | `UDP` | RFC 768 |
80
+ | 7 | DNS | `DNS` | RFC 1035, over UDP; header + on-demand name decompression |
75
81
 
76
82
  ## Decoding a captured frame
77
83
 
@@ -127,8 +133,9 @@ frame = Packet(
127
133
  raw = bytes(frame) # ready for a raw socket
128
134
  ```
129
135
 
130
- Checksums are carried verbatim in both directions: the library neither
131
- computes nor verifies them (planned — see the roadmap).
136
+ Checksums are computed and verified on request — never silently:
137
+ `compute()`/`verify()` in `netprotocols.checksum`, and
138
+ `Packet.with_checksums()` to fill a whole stack before sending.
132
139
 
133
140
  ## Display helpers
134
141
 
@@ -145,23 +152,26 @@ byte layout maps onto a dataclass, the decode contract, the
145
152
  `next_protocol()` chain, and a cookbook for adding a new protocol.
146
153
 
147
154
  This library is the engine behind
148
- [Packet-Sniffer](https://github.com/EONRaider/Packet-Sniffer), a
149
- network traffic monitor built on it.
155
+ [RootWire](https://github.com/EONRaider/RootWire), a network traffic
156
+ monitor built on it (formerly Packet-Sniffer).
150
157
 
151
158
  ## Roadmap
152
159
 
153
- - IPv6 extension headers (hop-by-hop, routing, fragment, destination
154
- options) — today the decode chain ends at `IPv6` for traffic behind
155
- them (e.g. MLD).
156
- - 802.1Q VLAN tags, DNS, DHCP, IGMP, GRE.
157
- - Checksum computation on encode and verification flags on decode.
158
- - Fuzzing of the decode path.
160
+ - More protocols: 802.1Q VLAN tags, DHCP, GRE (DNS and IGMP shipped in 1.2).
161
+ - Full DNS resource-record parsing (the header and question are decoded today; answer/authority/additional sections are kept raw).
162
+ - IGMPv3 group-record parsing (the common header is decoded today; v3 report records are kept raw in the body).
163
+ - Optional richer address accessors (`ipaddress` / EUI objects
164
+ alongside the `str` fields).
165
+ - TLV parsing inside IPv6 extension-header options.
159
166
 
160
167
  ## Contributing
161
168
 
162
169
  Development uses [uv](https://docs.astral.sh/uv/): `uv sync`, then
163
170
  `uv run pytest`, `uv run mypy`, and `uv run ruff check` — all three are
164
- enforced by CI on Python 3.12–3.14. Start with
171
+ enforced by CI on Python 3.12–3.14. The test suite is anchored by a
172
+ 65-frame corpus of real captured traffic
173
+ ([tests/fixtures/MANIFEST.md](tests/fixtures/MANIFEST.md)) plus
174
+ property-based fuzzing of the decode path. Start with
165
175
  [ARCHITECTURE.md](ARCHITECTURE.md).
166
176
 
167
177
  ## License
@@ -42,11 +42,17 @@ Requires Python 3.12+. Fully typed (`py.typed`, mypy strict).
42
42
  | 2 | Ethernet II | `Ethernet` | IEEE 802.3 |
43
43
  | 2 | ARP | `ARP` | RFC 826, IPv4-over-Ethernet binding |
44
44
  | 3 | IPv4 | `IPv4` | RFC 791, IHL/options aware |
45
- | 3 | IPv6 | `IPv6` | RFC 8200, fixed header |
45
+ | 3 | IPv6 | `IPv6` | RFC 8200 |
46
+ | 3 | IPv6 Hop-by-Hop Options | `IPv6HopByHopOptions` | RFC 8200 §4.3 |
47
+ | 3 | IPv6 Routing | `IPv6Routing` | RFC 8200 §4.4 |
48
+ | 3 | IPv6 Fragment | `IPv6Fragment` | RFC 8200 §4.5, first-fragment chaining |
49
+ | 3 | IPv6 Destination Options | `IPv6DestinationOptions` | RFC 8200 §4.6 |
46
50
  | 3 | ICMPv4 | `ICMPv4` | RFC 792, 8-byte header |
47
51
  | 3 | ICMPv6 | `ICMPv6` | RFC 4443, 8-byte header |
52
+ | 3 | IGMP | `IGMP` | RFC 1112/2236/3376, IPv4 multicast management |
48
53
  | 4 | TCP | `TCP` | RFC 9293, data-offset/options aware |
49
54
  | 4 | UDP | `UDP` | RFC 768 |
55
+ | 7 | DNS | `DNS` | RFC 1035, over UDP; header + on-demand name decompression |
50
56
 
51
57
  ## Decoding a captured frame
52
58
 
@@ -102,8 +108,9 @@ frame = Packet(
102
108
  raw = bytes(frame) # ready for a raw socket
103
109
  ```
104
110
 
105
- Checksums are carried verbatim in both directions: the library neither
106
- computes nor verifies them (planned — see the roadmap).
111
+ Checksums are computed and verified on request — never silently:
112
+ `compute()`/`verify()` in `netprotocols.checksum`, and
113
+ `Packet.with_checksums()` to fill a whole stack before sending.
107
114
 
108
115
  ## Display helpers
109
116
 
@@ -120,23 +127,26 @@ byte layout maps onto a dataclass, the decode contract, the
120
127
  `next_protocol()` chain, and a cookbook for adding a new protocol.
121
128
 
122
129
  This library is the engine behind
123
- [Packet-Sniffer](https://github.com/EONRaider/Packet-Sniffer), a
124
- network traffic monitor built on it.
130
+ [RootWire](https://github.com/EONRaider/RootWire), a network traffic
131
+ monitor built on it (formerly Packet-Sniffer).
125
132
 
126
133
  ## Roadmap
127
134
 
128
- - IPv6 extension headers (hop-by-hop, routing, fragment, destination
129
- options) — today the decode chain ends at `IPv6` for traffic behind
130
- them (e.g. MLD).
131
- - 802.1Q VLAN tags, DNS, DHCP, IGMP, GRE.
132
- - Checksum computation on encode and verification flags on decode.
133
- - Fuzzing of the decode path.
135
+ - More protocols: 802.1Q VLAN tags, DHCP, GRE (DNS and IGMP shipped in 1.2).
136
+ - Full DNS resource-record parsing (the header and question are decoded today; answer/authority/additional sections are kept raw).
137
+ - IGMPv3 group-record parsing (the common header is decoded today; v3 report records are kept raw in the body).
138
+ - Optional richer address accessors (`ipaddress` / EUI objects
139
+ alongside the `str` fields).
140
+ - TLV parsing inside IPv6 extension-header options.
134
141
 
135
142
  ## Contributing
136
143
 
137
144
  Development uses [uv](https://docs.astral.sh/uv/): `uv sync`, then
138
145
  `uv run pytest`, `uv run mypy`, and `uv run ruff check` — all three are
139
- enforced by CI on Python 3.12–3.14. Start with
146
+ enforced by CI on Python 3.12–3.14. The test suite is anchored by a
147
+ 65-frame corpus of real captured traffic
148
+ ([tests/fixtures/MANIFEST.md](tests/fixtures/MANIFEST.md)) plus
149
+ property-based fuzzing of the decode path. Start with
140
150
  [ARCHITECTURE.md](ARCHITECTURE.md).
141
151
 
142
152
  ## License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "netprotocols"
3
- version = "1.0.1"
3
+ version = "1.2.0"
4
4
  description = "Low-level implementations of common networking protocols"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -29,6 +29,7 @@ Changelog = "https://github.com/EONRaider/NETProtocols/blob/master/CHANGELOG.md"
29
29
 
30
30
  [dependency-groups]
31
31
  dev = [
32
+ "hypothesis>=6.100",
32
33
  "mypy>=1.14",
33
34
  "pytest>=8.3",
34
35
  "pytest-cov>=6.0",
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env bash
2
+ # Capture an IGMP fixture for the corpus (plan item I-2).
3
+ #
4
+ # sudo scripts/capture_fixtures_igmp.sh
5
+ #
6
+ # IGMP is IPv4 multicast group management — it never appeared in the
7
+ # original corpus (which provoked IPv6 MLD, not IPv4 IGMP). Joining an
8
+ # IPv4 multicast group makes the kernel emit a membership report
9
+ # (IGMPv2 type 0x16 or IGMPv3 type 0x22, depending on the host's
10
+ # configured version); a querier on the LAN may also produce a
11
+ # membership query (type 0x11). This captures `ip proto 2` on the
12
+ # default interface while joining a group, then validates.
13
+
14
+ set -euo pipefail
15
+
16
+ if [[ $EUID -ne 0 ]]; then
17
+ echo "This script needs root for tcpdump (or run it via sudo)." >&2
18
+ exit 1
19
+ fi
20
+
21
+ command -v tcpdump >/dev/null || { echo "tcpdump is required." >&2; exit 1; }
22
+
23
+ OUT="tests/fixtures/staging"
24
+ mkdir -p "$OUT"
25
+ IFACE=$(ip route show default | awk '/default/ {print $5; exit}')
26
+ [[ -n $IFACE ]] || { echo "No default interface found." >&2; exit 1; }
27
+ echo "[*] Default interface: $IFACE"
28
+
29
+ join_group() {
30
+ python3 - "$IFACE" <<'PY' || true
31
+ import socket, struct, sys, time
32
+ group = "239.255.42.99" # an administratively-scoped group (RFC 2365)
33
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
34
+ mreq = socket.inet_aton(group) + socket.inet_aton("0.0.0.0")
35
+ sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
36
+ time.sleep(3) # report is emitted on join; hold briefly
37
+ sock.setsockopt(socket.IPPROTO_IP, socket.IP_DROP_MEMBERSHIP, mreq)
38
+ sys.stderr.write(f"joined and left {group} on {sys.argv[1]}\n")
39
+ PY
40
+ }
41
+
42
+ echo "[>] igmp (join 239.255.42.99 to provoke a membership report)"
43
+ tcpdump -i "$IFACE" -c 4 -w "$OUT/igmp.pcap" --immediate-mode -q \
44
+ ip proto 2 >/dev/null 2>&1 &
45
+ pid=$!
46
+ sleep 1
47
+ join_group
48
+ for _ in $(seq 15); do kill -0 $pid 2>/dev/null || break; sleep 1; done
49
+ kill $pid 2>/dev/null || true
50
+ wait $pid 2>/dev/null || true
51
+
52
+ echo
53
+ echo "[*] Validating..."
54
+ CHECKER_PY=$(command -v python3)
55
+ if [[ -n ${SUDO_USER:-} ]]; then
56
+ chown -R "$SUDO_USER" "$OUT"
57
+ sudo -u "$SUDO_USER" "$CHECKER_PY" scripts/check_fixtures.py "$OUT"
58
+ else
59
+ "$CHECKER_PY" scripts/check_fixtures.py "$OUT"
60
+ fi
61
+
62
+ echo
63
+ echo "[i] If no frames were captured, your host may emit IGMPv3 only when"
64
+ echo " a querier is present, or multicast may be disabled on $IFACE."
65
+ echo " The igmp.pcap in staging (if non-empty and OK above) is ready to"
66
+ echo " fold into the corpus."
@@ -98,6 +98,14 @@ def check_frame(frame: bytes, stats: Counter) -> list[str]:
98
98
  != struct.unpack_from("!H", upper, 2)[0]
99
99
  ):
100
100
  failures.append("icmpv4-checksum")
101
+ elif proto == 2 and upper_len >= 8:
102
+ stats["igmp"] += 1
103
+ # IGMP: whole message, checksum at bytes 2-3, no pseudo-header.
104
+ if (
105
+ internet_checksum(upper[:2] + b"\x00\x00" + upper[4:])
106
+ != struct.unpack_from("!H", upper, 2)[0]
107
+ ):
108
+ failures.append("igmp-checksum")
101
109
  elif proto == 6 and upper_len >= 20:
102
110
  stats["tcp"] += 1
103
111
  zeroed = upper[:16] + b"\x00\x00" + upper[18:]
@@ -134,10 +142,13 @@ def check_frame(frame: bytes, stats: Counter) -> list[str]:
134
142
  ext_len = (upper[1] + 1) * 8
135
143
  next_header, upper = upper[0], upper[ext_len:]
136
144
  if next_header == 44 and len(upper) >= 8:
137
- stats["ipv6-fragment"] += 1
138
145
  if struct.unpack_from("!H", upper, 2)[0] & 0xFFF8:
139
- return failures
140
- next_header, upper = upper[0], upper[8:]
146
+ stats["ipv6-fragment"] += 1
147
+ return failures # non-first fragment: no upper header
148
+ stats["ipv6-first-fragment"] += 1
149
+ # The upper-layer checksum spans the reassembled datagram,
150
+ # not this fragment's slice: nothing verifiable here.
151
+ return failures
141
152
  upper_len = len(upper)
142
153
  if next_header == 58 and upper_len >= 4:
143
154
  stats["icmpv6"] += 1
@@ -12,12 +12,21 @@ from netprotocols._enums import (
12
12
  EtherType,
13
13
  IPProtocol,
14
14
  )
15
+ from netprotocols.checksum import compute, internet_checksum, verify
15
16
  from netprotocols.layer2.arp import ARP
16
17
  from netprotocols.layer2.ethernet import Ethernet
17
18
  from netprotocols.layer3.icmp import ICMPv4, ICMPv6
19
+ from netprotocols.layer3.igmp import IGMP
18
20
  from netprotocols.layer3.ip import IPv4, IPv6
21
+ from netprotocols.layer3.ipv6_ext import (
22
+ IPv6DestinationOptions,
23
+ IPv6Fragment,
24
+ IPv6HopByHopOptions,
25
+ IPv6Routing,
26
+ )
19
27
  from netprotocols.layer4.tcp import TCP
20
28
  from netprotocols.layer4.udp import UDP
29
+ from netprotocols.layer7.dns import DNS
21
30
  from netprotocols.packet import Packet
22
31
  from netprotocols.utils.exceptions import (
23
32
  InvalidFieldError,
@@ -30,10 +39,12 @@ from netprotocols.utils.exceptions import (
30
39
  from netprotocols.utils.ipv4 import validate_ipv4_addr
31
40
  from netprotocols.utils.mac import random_mac, validate_mac_addr
32
41
 
33
- __version__ = "1.0.1"
42
+ __version__ = "1.2.0"
34
43
 
35
44
  __all__ = [
36
45
  "ARP",
46
+ "DNS",
47
+ "IGMP",
37
48
  "TCP",
38
49
  "UDP",
39
50
  "ARPHardwareType",
@@ -45,6 +56,10 @@ __all__ = [
45
56
  "IPProtocol",
46
57
  "IPv4",
47
58
  "IPv6",
59
+ "IPv6DestinationOptions",
60
+ "IPv6Fragment",
61
+ "IPv6HopByHopOptions",
62
+ "IPv6Routing",
48
63
  "InvalidFieldError",
49
64
  "InvalidIPv4AddressError",
50
65
  "InvalidMACAddressError",
@@ -54,7 +69,10 @@ __all__ = [
54
69
  "ProtocolError",
55
70
  "TruncatedHeaderError",
56
71
  "__version__",
72
+ "compute",
73
+ "internet_checksum",
57
74
  "random_mac",
58
75
  "validate_ipv4_addr",
59
76
  "validate_mac_addr",
77
+ "verify",
60
78
  ]
@@ -36,11 +36,15 @@ class IPProtocol(IntEnum):
36
36
  ``next_header``, so a single registry serves both.
37
37
  """
38
38
 
39
+ HOPOPT = 0x00
39
40
  ICMP = 0x01
40
41
  IGMP = 0x02
41
42
  TCP = 0x06
42
43
  UDP = 0x11
44
+ IPV6_ROUTE = 0x2B
45
+ IPV6_FRAG = 0x2C
43
46
  IPV6_ICMP = 0x3A
47
+ IPV6_DSTOPTS = 0x3C
44
48
 
45
49
  @property
46
50
  def display_name(self) -> str:
@@ -48,11 +52,15 @@ class IPProtocol(IntEnum):
48
52
 
49
53
 
50
54
  _IP_PROTOCOL_NAMES = {
55
+ IPProtocol.HOPOPT: "IPv6 Hop-by-Hop Options",
51
56
  IPProtocol.ICMP: "ICMP",
52
57
  IPProtocol.IGMP: "IGMP",
53
58
  IPProtocol.TCP: "TCP",
54
59
  IPProtocol.UDP: "UDP",
60
+ IPProtocol.IPV6_ROUTE: "IPv6 Routing",
61
+ IPProtocol.IPV6_FRAG: "IPv6 Fragment",
55
62
  IPProtocol.IPV6_ICMP: "IPv6-ICMP",
63
+ IPProtocol.IPV6_DSTOPTS: "IPv6 Destination Options",
56
64
  }
57
65
 
58
66