netprotocols 0.8.0__tar.gz → 1.0.1__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 (81) hide show
  1. netprotocols-1.0.1/.github/workflows/ci.yml +47 -0
  2. netprotocols-1.0.1/.github/workflows/release.yml +18 -0
  3. netprotocols-1.0.1/.gitignore +149 -0
  4. netprotocols-1.0.1/ARCHITECTURE.md +222 -0
  5. netprotocols-1.0.1/CHANGELOG.md +74 -0
  6. netprotocols-1.0.1/LICENSE +21 -0
  7. netprotocols-1.0.1/PKG-INFO +169 -0
  8. netprotocols-1.0.1/README.md +144 -0
  9. netprotocols-1.0.1/pyproject.toml +75 -0
  10. netprotocols-1.0.1/scripts/capture_fixtures.sh +104 -0
  11. netprotocols-1.0.1/scripts/capture_fixtures_supplement.sh +71 -0
  12. netprotocols-1.0.1/scripts/check_fixtures.py +225 -0
  13. netprotocols-1.0.1/src/netprotocols/__init__.py +60 -0
  14. netprotocols-1.0.1/src/netprotocols/_base.py +128 -0
  15. netprotocols-1.0.1/src/netprotocols/_enums.py +77 -0
  16. netprotocols-1.0.1/src/netprotocols/layer2/__init__.py +4 -0
  17. netprotocols-1.0.1/src/netprotocols/layer2/arp.py +107 -0
  18. netprotocols-1.0.1/src/netprotocols/layer2/ethernet.py +66 -0
  19. netprotocols-1.0.1/src/netprotocols/layer3/__init__.py +4 -0
  20. netprotocols-1.0.1/src/netprotocols/layer3/icmp.py +135 -0
  21. netprotocols-1.0.1/src/netprotocols/layer3/ip.py +276 -0
  22. netprotocols-1.0.1/src/netprotocols/layer4/__init__.py +4 -0
  23. netprotocols-1.0.1/src/netprotocols/layer4/tcp.py +153 -0
  24. netprotocols-1.0.1/src/netprotocols/layer4/udp.py +50 -0
  25. netprotocols-1.0.1/src/netprotocols/packet.py +48 -0
  26. netprotocols-1.0.1/src/netprotocols/py.typed +0 -0
  27. netprotocols-1.0.1/src/netprotocols/utils/__init__.py +22 -0
  28. netprotocols-1.0.1/src/netprotocols/utils/exceptions.py +48 -0
  29. netprotocols-1.0.1/src/netprotocols/utils/ipv4.py +32 -0
  30. netprotocols-1.0.1/src/netprotocols/utils/mac.py +64 -0
  31. netprotocols-1.0.1/tests/conftest.py +111 -0
  32. netprotocols-1.0.1/tests/fixtures/MANIFEST.md +48 -0
  33. netprotocols-1.0.1/tests/fixtures/arp_exchange.pcap +0 -0
  34. netprotocols-1.0.1/tests/fixtures/icmpv4_echo_lo.pcap +0 -0
  35. netprotocols-1.0.1/tests/fixtures/icmpv4_external.pcap +0 -0
  36. netprotocols-1.0.1/tests/fixtures/icmpv4_ttl_exceeded.pcap +0 -0
  37. netprotocols-1.0.1/tests/fixtures/icmpv6_echo_lo.pcap +0 -0
  38. netprotocols-1.0.1/tests/fixtures/ipv4_fragments.pcap +0 -0
  39. netprotocols-1.0.1/tests/fixtures/ipv6_ndp_mld.pcap +0 -0
  40. netprotocols-1.0.1/tests/fixtures/staging/arp_exchange.pcap +0 -0
  41. netprotocols-1.0.1/tests/fixtures/staging/icmpv4_echo_lo.pcap +0 -0
  42. netprotocols-1.0.1/tests/fixtures/staging/icmpv4_external.pcap +0 -0
  43. netprotocols-1.0.1/tests/fixtures/staging/icmpv4_ttl_exceeded.pcap +0 -0
  44. netprotocols-1.0.1/tests/fixtures/staging/icmpv6_echo_lo.pcap +0 -0
  45. netprotocols-1.0.1/tests/fixtures/staging/ipv4_fragments.pcap +0 -0
  46. netprotocols-1.0.1/tests/fixtures/staging/ipv6_ndp_mld.pcap +0 -0
  47. netprotocols-1.0.1/tests/fixtures/staging/tcp_http.pcap +0 -0
  48. netprotocols-1.0.1/tests/fixtures/staging/tcp_https.pcap +0 -0
  49. netprotocols-1.0.1/tests/fixtures/staging/udp_dns.pcap +0 -0
  50. netprotocols-1.0.1/tests/fixtures/tcp_http.pcap +0 -0
  51. netprotocols-1.0.1/tests/fixtures/tcp_https.pcap +0 -0
  52. netprotocols-1.0.1/tests/fixtures/udp_dns.pcap +0 -0
  53. netprotocols-1.0.1/tests/test_arp.py +65 -0
  54. netprotocols-1.0.1/tests/test_contract.py +104 -0
  55. netprotocols-1.0.1/tests/test_corpus.py +167 -0
  56. netprotocols-1.0.1/tests/test_ethernet.py +40 -0
  57. netprotocols-1.0.1/tests/test_icmp.py +55 -0
  58. netprotocols-1.0.1/tests/test_ip.py +92 -0
  59. netprotocols-1.0.1/tests/test_packet.py +37 -0
  60. netprotocols-1.0.1/tests/test_tcp.py +80 -0
  61. netprotocols-1.0.1/tests/test_udp.py +24 -0
  62. netprotocols-1.0.1/tests/test_utils.py +66 -0
  63. netprotocols-1.0.1/uv.lock +475 -0
  64. netprotocols-0.8.0/PKG-INFO +0 -12
  65. netprotocols-0.8.0/netprotocols/__init__.py +0 -8
  66. netprotocols-0.8.0/netprotocols/base/packet.py +0 -34
  67. netprotocols-0.8.0/netprotocols/base/protocol.py +0 -131
  68. netprotocols-0.8.0/netprotocols/layer2/arp.py +0 -100
  69. netprotocols-0.8.0/netprotocols/layer2/ethernet.py +0 -45
  70. netprotocols-0.8.0/netprotocols/layer3/icmp.py +0 -148
  71. netprotocols-0.8.0/netprotocols/layer3/ip.py +0 -188
  72. netprotocols-0.8.0/netprotocols/layer4/tcp.py +0 -102
  73. netprotocols-0.8.0/netprotocols/layer4/udp.py +0 -34
  74. netprotocols-0.8.0/netprotocols/utils/__init__.py +0 -1
  75. netprotocols-0.8.0/netprotocols/utils/exceptions.py +0 -22
  76. netprotocols-0.8.0/netprotocols/utils/random_mac.py +0 -34
  77. netprotocols-0.8.0/netprotocols/utils/validation/_base.py +0 -22
  78. netprotocols-0.8.0/netprotocols/utils/validation/ipv4.py +0 -38
  79. netprotocols-0.8.0/netprotocols/utils/validation/mac.py +0 -48
  80. netprotocols-0.8.0/pyproject.toml +0 -22
  81. netprotocols-0.8.0/setup.py +0 -32
@@ -0,0 +1,47 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ branches: [master]
8
+
9
+ jobs:
10
+ lint:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v5
14
+ - uses: astral-sh/setup-uv@v6
15
+ with:
16
+ python-version: "3.12"
17
+ - run: uv run ruff check
18
+ - run: uv run ruff format --check
19
+
20
+ typecheck:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v5
24
+ - uses: astral-sh/setup-uv@v6
25
+ with:
26
+ python-version: "3.12"
27
+ - run: uv run mypy
28
+
29
+ test:
30
+ runs-on: ubuntu-latest
31
+ strategy:
32
+ matrix:
33
+ python-version: ["3.12", "3.13", "3.14"]
34
+ steps:
35
+ - uses: actions/checkout@v5
36
+ - uses: astral-sh/setup-uv@v6
37
+ with:
38
+ python-version: ${{ matrix.python-version }}
39
+ - run: uv run --frozen pytest --cov=netprotocols --cov-report=term
40
+
41
+ build:
42
+ runs-on: ubuntu-latest
43
+ steps:
44
+ - uses: actions/checkout@v5
45
+ - uses: astral-sh/setup-uv@v6
46
+ - run: uv build
47
+ - run: uv run --isolated --no-project --with dist/*.whl python -c "import netprotocols; print(netprotocols.__version__)"
@@ -0,0 +1,18 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ environment: pypi
11
+ permissions:
12
+ id-token: write
13
+ contents: read
14
+ steps:
15
+ - uses: actions/checkout@v5
16
+ - uses: astral-sh/setup-uv@v6
17
+ - run: uv build
18
+ - run: uv publish --trusted-publishing always
@@ -0,0 +1,149 @@
1
+ ### Python template
2
+ # Byte-compiled / optimized / DLL files
3
+ __pycache__/
4
+ *.py[cod]
5
+ *$py.class
6
+
7
+ # C extensions
8
+ *.so
9
+
10
+ # Distribution / packaging
11
+ .Python
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ cover/
54
+
55
+ # Translations
56
+ *.mo
57
+ *.pot
58
+
59
+ # Django stuff:
60
+ *.log
61
+ local_settings.py
62
+ db.sqlite3
63
+ db.sqlite3-journal
64
+
65
+ # Flask stuff:
66
+ instance/
67
+ .webassets-cache
68
+
69
+ # Scrapy stuff:
70
+ .scrapy
71
+
72
+ # Sphinx documentation
73
+ docs/_build/
74
+
75
+ # PyBuilder
76
+ .pybuilder/
77
+ target/
78
+
79
+ # Jupyter Notebook
80
+ .ipynb_checkpoints
81
+
82
+ # IPython
83
+ profile_default/
84
+ ipython_config.py
85
+
86
+ # pyenv
87
+ # For a library or package, you might want to ignore these files since the code is
88
+ # intended to run in multiple environments; otherwise, check them in:
89
+ # .python-version
90
+
91
+ # pipenv
92
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
94
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
95
+ # install all needed dependencies.
96
+ #Pipfile.lock
97
+
98
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
99
+ __pypackages__/
100
+
101
+ # Celery stuff
102
+ celerybeat-schedule
103
+ celerybeat.pid
104
+
105
+ # SageMath parsed files
106
+ *.sage.py
107
+
108
+ # Environments
109
+ .env
110
+ .venv
111
+ env/
112
+ venv/
113
+ ENV/
114
+ env.bak/
115
+ venv.bak/
116
+
117
+ # Spyder project settings
118
+ .spyderproject
119
+ .spyproject
120
+
121
+ # Rope project settings
122
+ .ropeproject
123
+
124
+ # mkdocs documentation
125
+ /site
126
+
127
+ # mypy
128
+ .mypy_cache/
129
+ .dmypy.json
130
+ dmypy.json
131
+
132
+ # Pyre type checker
133
+ .pyre/
134
+
135
+ # pytype static type analyzer
136
+ .pytype/
137
+
138
+ # Cython debug symbols
139
+ cython_debug/
140
+
141
+ # Pycharm
142
+ .idea/
143
+
144
+ # OWASP Amass
145
+ amass.txt
146
+ amass.log
147
+
148
+ # Application-specific
149
+ .logs/tests/fixtures/staging/
@@ -0,0 +1,222 @@
1
+ # How NETProtocols works
2
+
3
+ This is a guided tour of the library's inner workings, written for
4
+ anyone who wants to contribute a protocol, fix a bug, or simply
5
+ understand how raw bytes become Python objects and back. No prior
6
+ experience with packet dissection is assumed — only curiosity and a
7
+ little Python.
8
+
9
+ ## A header is just bytes at fixed positions
10
+
11
+ Here are the first 14 bytes of a real captured frame — an Ethernet II
12
+ header announcing an ARP packet:
13
+
14
+ ```
15
+ ff ff ff ff ff ff 00 07 0d af f4 54 08 06
16
+ └───────────────┘ └───────────────┘ └───┘
17
+ destination MAC source MAC EtherType
18
+ (broadcast) 0x0806 = ARP
19
+ ```
20
+
21
+ Every protocol header works like this: a sequence of fields, each at a
22
+ known offset with a known width, in network byte order (big-endian).
23
+ Parsing one is nothing more than slicing the right bytes and
24
+ interpreting them as integers or addresses.
25
+
26
+ ## From bytes to a dataclass
27
+
28
+ Each protocol is a **frozen, slotted dataclass** whose fields mirror
29
+ the wire format, plus a class-level `struct.Struct` describing the
30
+ fixed portion's layout. For the header above:
31
+
32
+ ```python
33
+ @dataclass(frozen=True, slots=True)
34
+ class Ethernet(Protocol):
35
+ dst: str # "ff:ff:ff:ff:ff:ff"
36
+ src: str # "00:07:0d:af:f4:54"
37
+ ethertype: int # 0x0806
38
+
39
+ _struct: ClassVar[Struct] = Struct("!6s6sH")
40
+ ```
41
+
42
+ The format string `"!6s6sH"` reads: network byte order (`!`), two
43
+ 6-byte strings (the MACs), one unsigned 16-bit integer (the
44
+ EtherType). The `Struct` is compiled once at class definition time —
45
+ decoding a million frames reuses the same compiled object.
46
+
47
+ Three design choices worth knowing:
48
+
49
+ - **Frozen + slots**: instances are immutable (safe to share, hash,
50
+ and compare) and carry no `__dict__` (cheap to allocate by the
51
+ million during live capture).
52
+ - **Friendly field types**: addresses are stored as their canonical
53
+ strings, everything else as `int`, options as `bytes`. What you see
54
+ in a `repr()` is what you work with.
55
+ - **Bitfields are explicit**: where the wire packs several fields into
56
+ one integer (IPv4's version/IHL byte, TCP's offset/flags word), the
57
+ decode method splits them with shifts and masks you can read:
58
+
59
+ ```python
60
+ ihl = ver_ihl & 0x0F
61
+ version = ver_ihl >> 4
62
+ ```
63
+
64
+ ## The decode contract
65
+
66
+ `decode(data)` is the heart of the library, and every protocol obeys
67
+ the same three rules (spelled out in [`_base.py`](src/netprotocols/_base.py)):
68
+
69
+ 1. **It receives the entire remaining buffer** — everything from the
70
+ start of its header to the end of the captured frame — and parses
71
+ the fixed portion with `_struct.unpack_from`. A buffer too short
72
+ for the fixed portion raises `TruncatedHeaderError`.
73
+ 2. **Variable-length headers finish the job themselves.** IPv4 reads
74
+ its IHL and TCP reads its data offset, sanity-checks it (5–15, and
75
+ the declared bytes must exist in the buffer — a header that *lies*
76
+ about its length raises `TruncatedHeaderError`), then slices its
77
+ `options` as materialized `bytes`.
78
+ 3. **Trailing bytes are fine.** Whatever follows the header belongs to
79
+ the next layer. After decoding, `instance.header_len` says exactly
80
+ how many bytes this header consumed — a *property*, so IPv4 answers
81
+ `ihl * 4` and TCP answers `data_offset * 4`.
82
+
83
+ Decoding accepts `bytes` or `memoryview`. Views are a decode-time
84
+ transient only: every stored field is a scalar or materialized
85
+ `bytes`, so no decoded object ever aliases the buffer it came from.
86
+ You can safely reuse or mutate capture buffers.
87
+
88
+ Errors form a small typed hierarchy rooted at `ProtocolError`:
89
+
90
+ ```
91
+ ProtocolError
92
+ ├── TruncatedHeaderError buffer shorter than the header claims
93
+ └── InvalidFieldError field value violates the protocol's rules
94
+ ├── InvalidMACAddressError
95
+ ├── InvalidIPv4AddressError
96
+ └── InvalidManufacturerCodeError
97
+ ```
98
+
99
+ ## Walking a frame: the next_protocol() chain
100
+
101
+ A captured frame is a chain of headers, each one naming what it
102
+ carries. In this library that fact is typed: every decoded header
103
+ answers `next_protocol()` with the *class* that decodes its payload,
104
+ or `None` when the chain ends (nothing encapsulated, or a protocol
105
+ this library doesn't implement yet).
106
+
107
+ ```mermaid
108
+ flowchart LR
109
+ E[Ethernet] -->|0x0806| A[ARP]
110
+ E -->|0x0800| I4[IPv4]
111
+ E -->|0x86DD| I6[IPv6]
112
+ I4 -->|1| IC4[ICMPv4]
113
+ I4 -->|6| T[TCP]
114
+ I4 -->|17| U[UDP]
115
+ I6 -->|58| IC6[ICMPv6]
116
+ I6 -->|6| T
117
+ I6 -->|17| U
118
+ ```
119
+
120
+ The numbers on the arrows are the wire values — EtherTypes out of
121
+ Ethernet, IP protocol numbers out of IPv4/IPv6 — and they live in
122
+ [`_enums.py`](src/netprotocols/_enums.py) as `EtherType` and
123
+ `IPProtocol`. IPv4 and IPv6 share one registry because the number
124
+ space is shared (1 is ICMPv4, 58 is ICMPv6, 6 is TCP, 17 is UDP — no
125
+ collisions).
126
+
127
+ A complete frame walk is a five-line loop:
128
+
129
+ ```python
130
+ layers, cursor, protocol = [], 0, Ethernet
131
+ while protocol is not None:
132
+ header = protocol.decode(frame[cursor:])
133
+ layers.append(header)
134
+ cursor += header.header_len
135
+ protocol = header.next_protocol()
136
+ ```
137
+
138
+ **Why the deferred imports?** `Ethernet.next_protocol()` must name
139
+ `ARP`, `IPv4`, and `IPv6` — but `arp.py` also imports from the
140
+ Ethernet side of the world (EtherType names). Importing layer modules
141
+ from each other at module level would create cycles, so two rules keep
142
+ the graph acyclic: shared registries live in the dependency-free
143
+ `_enums.py`, and `next_protocol()` mappings import their target
144
+ classes *inside the function body*, at call time.
145
+
146
+ ## The encode side, and the round-trip guarantee
147
+
148
+ Every class also works in reverse. Constructors take the friendly
149
+ forms (string addresses, integer fields), validate them in
150
+ `__post_init__` (address formats; and IHL/data-offset must agree with
151
+ `len(options)` — you cannot construct a self-contradictory header),
152
+ and `bytes()` re-emits the exact on-wire form.
153
+
154
+ The test suite enforces the round trip in both directions for every
155
+ protocol:
156
+
157
+ ```python
158
+ Protocol.decode(bytes(header)) == header # object → wire → object
159
+ bytes(Protocol.decode(raw)) == raw[:header_len] # wire → object → wire
160
+ ```
161
+
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.
166
+
167
+ `Packet` is the thin composition layer: it holds an ordered tuple of
168
+ headers and `bytes(packet)` joins them, ready for a raw socket.
169
+
170
+ ## Layout map
171
+
172
+ ```
173
+ src/netprotocols/
174
+ ├── __init__.py public API re-exports, __version__
175
+ ├── _base.py Protocol ABC, decode contract, address helpers
176
+ ├── _enums.py EtherType, IPProtocol, ARPOperation (imports nothing)
177
+ ├── packet.py Packet composition
178
+ ├── layer2/ ethernet.py, arp.py
179
+ ├── layer3/ ip.py (IPv4 + IPv6), icmp.py (ICMPv4 + ICMPv6)
180
+ ├── layer4/ tcp.py, udp.py
181
+ └── utils/ validators (mac.py, ipv4.py), exceptions.py
182
+ tests/ one file per protocol + test_contract.py
183
+ (truncation, lying lengths, chain walks)
184
+ ```
185
+
186
+ Tooling: [uv](https://docs.astral.sh/uv/) manages the environment
187
+ (`uv sync`); `uv run pytest`, `uv run mypy` (strict), and
188
+ `uv run ruff check` are the three gates CI enforces on Python
189
+ 3.12–3.14.
190
+
191
+ ## Cookbook: adding a protocol
192
+
193
+ Say you want to add DNS (sitting behind UDP). The pattern is always
194
+ the same:
195
+
196
+ 1. **Model the fixed header.** Create `layer7/dns.py` with a frozen,
197
+ slotted dataclass: one field per header field, a `_struct` for the
198
+ fixed portion (`"!HHHHHH"` for DNS: id, flags, and four counts).
199
+ 2. **Implement `decode()`.** Call `cls._unpack_fixed(data)`, split any
200
+ bitfields explicitly, and construct the instance. If the header is
201
+ variable-length, validate the declared length against `len(data)`
202
+ (raise `TruncatedHeaderError` when the buffer can't satisfy it) and
203
+ materialize the variable part as `bytes`.
204
+ 3. **Implement `__bytes__()`.** Pack the same fields back; the
205
+ round-trip tests will hold you honest.
206
+ 4. **Wire the chain.** The layer below decides when your class is
207
+ next. For DNS that means UDP would implement `next_protocol()`
208
+ 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.)
211
+ 5. **Add display properties** for anything a human would want rendered
212
+ (`flags_str`-style names, hex strings). Degrade gracefully on
213
+ unknown values — return `"unknown (47)"`, never raise from a
214
+ display helper.
215
+ 6. **Test with real bytes.** Capture or craft a real header, add it as
216
+ a fixture, and cover: a decode with asserted fields, both round
217
+ trips, truncated input, and (if variable-length) an options case
218
+ and a lying-length case. Export the class from `__init__.py`.
219
+
220
+ If you follow the six steps, your protocol automatically composes with
221
+ `Packet`, participates in frame walks, and inherits the library's
222
+ error behavior — that's the whole point of the shared contract.
@@ -0,0 +1,74 @@
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.0.1] - 2026-08-29
9
+
10
+ ### Fixed
11
+ - `IPv4.next_protocol()` chained non-first fragments
12
+ (`fragment_offset > 0`) into garbage upper-layer decodes — a slice
13
+ from the middle of a fragmented payload has no header at its start.
14
+ Discovered by the new real-capture fixture corpus on live fragmented
15
+ traffic; the chain now ends at the IPv4 layer for those frames, as it
16
+ already did for unknown protocols.
17
+
18
+ ### Added (development)
19
+ - A real-capture fixture corpus under `tests/fixtures/` (56 frames,
20
+ 10 scenarios, every checksum internally verified — see its
21
+ MANIFEST.md) with corpus-wide invariant tests, plus the capture and
22
+ validation scripts under `scripts/`.
23
+
24
+ ## [1.0.0] - 2026-08-21
25
+
26
+ Complete rewrite. The public API is new; see the README and
27
+ ARCHITECTURE.md for the current usage.
28
+
29
+ ### Changed
30
+ - The parsing core moved from `ctypes.BigEndianStructure` subclasses to
31
+ frozen, slotted dataclasses parsed with `struct.Struct`. Decoded and
32
+ constructed instances are now identical in type and compare equal
33
+ field-by-field.
34
+ - `next_protocol()` (returning the class that decodes the payload, or
35
+ `None`) replaces the stringly-typed `encapsulated_proto` chain.
36
+ Protocol number registries are exposed as `EtherType`, `IPProtocol`,
37
+ and `ARPOperation` enums.
38
+ - `header_len` is now an instance property: IPv4 honors IHL and TCP
39
+ honors the data offset, and both expose their raw `options` bytes.
40
+ - Field names modernized (`ethertype`, `src_port`, `total_length`,
41
+ `checksum`, ...); display helpers are properties such as
42
+ `protocol_name`, `flags_str`, and `checksum_hex_str`.
43
+ - Packaging moved to PEP 621 (`hatchling` + `uv`), src layout, Python
44
+ 3.12+, MIT license, `py.typed` (mypy strict clean).
45
+ - Validators are plain functions (`validate_mac_addr`,
46
+ `validate_ipv4_addr`) raising exceptions rooted at `ProtocolError`.
47
+
48
+ ### Fixed
49
+ - `TCP.header_len` was hardcoded to 32 bytes; the base header is 20 and
50
+ the real length follows the data offset.
51
+ - Decoding ignored IPv4 IHL and TCP data offset, so headers carrying
52
+ options were mis-sliced.
53
+ - Serialization returned a zeroed buffer (`Protocol.__str__`);
54
+ `bytes(header)` now re-emits the exact on-wire form, options included.
55
+ - `Packet` rejected instances of indirect `Protocol` subclasses
56
+ (e.g. `IPv4`, `ICMPv4`) due to validation against direct subclasses
57
+ only; membership is now checked with `isinstance`.
58
+ - Malformed input (truncated buffers, lying IHL/data-offset values) now
59
+ raises typed exceptions (`TruncatedHeaderError`, `InvalidFieldError`)
60
+ instead of arbitrary `ValueError`/`struct.error` leaks.
61
+
62
+ ### Removed
63
+ - The `Validator` descriptor infrastructure (it stored state on the
64
+ descriptor itself — class-shared — and was never wired into any
65
+ protocol class).
66
+ - Poetry manifests and the `setup.py`-era metadata.
67
+
68
+ ## [0.8.0] - 2022-08-19
69
+
70
+ Last release of the ctypes-based implementation, published to PyPI in
71
+ 2022. The repository had drifted behind the published releases; the
72
+ 0.8.0 sources were recovered from the PyPI sdist and committed (tagged
73
+ `v0.8.0`) before the 1.0.0 rewrite. Releases 0.4.4 through 0.8.0 exist
74
+ on PyPI only; their individual histories were not preserved.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021-2026 EONRaider
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.5
2
+ Name: netprotocols
3
+ Version: 1.0.1
4
+ Summary: Low-level implementations of common networking protocols
5
+ Project-URL: Homepage, https://github.com/EONRaider/NETProtocols
6
+ Project-URL: Repository, https://github.com/EONRaider/NETProtocols
7
+ Project-URL: Changelog, https://github.com/EONRaider/NETProtocols/blob/master/CHANGELOG.md
8
+ Author-email: EONRaider <livewire_voodoo@protonmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: arp,ethernet,icmp,ip,networking,packet,protocols,tcp,udp
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Topic :: System :: Networking
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.12
24
+ Description-Content-Type: text/markdown
25
+
26
+ # NETProtocols
27
+
28
+ [![CI](https://github.com/EONRaider/NETProtocols/actions/workflows/ci.yml/badge.svg)](https://github.com/EONRaider/NETProtocols/actions/workflows/ci.yml)
29
+ ![Python Version](https://img.shields.io/badge/python-3.12%2B-blue?logo=python)
30
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
31
+ ![Typed](https://img.shields.io/badge/typing-strict-informational)
32
+
33
+ Low-level implementations of common networking protocols, in pure
34
+ Python with zero dependencies.
35
+
36
+ Decode raw header bytes into typed, immutable protocol objects — or
37
+ build those objects from field values and serialize them back to their
38
+ exact on-wire form. Every header is a frozen dataclass whose fields
39
+ mirror the wire format, so decoded traffic is introspectable,
40
+ comparable, and round-trippable.
41
+
42
+ ```pycon
43
+ >>> from netprotocols import Ethernet
44
+ >>> eth = Ethernet.decode(frame)
45
+ >>> eth
46
+ Ethernet(dst='ff:ff:ff:ff:ff:ff', src='00:07:0d:af:f4:54', ethertype=2054)
47
+ >>> eth.ethertype_name
48
+ 'ARP'
49
+ >>> eth.next_protocol()
50
+ <class 'netprotocols.layer2.arp.ARP'>
51
+ >>> bytes(eth) == frame[:eth.header_len]
52
+ True
53
+ ```
54
+
55
+ ## Installation
56
+
57
+ ```
58
+ pip install netprotocols
59
+ ```
60
+
61
+ Requires Python 3.12+. Fully typed (`py.typed`, mypy strict).
62
+
63
+ ## Protocol coverage
64
+
65
+ | Layer | Protocol | Class | Notes |
66
+ |---|---|---|---|
67
+ | 2 | Ethernet II | `Ethernet` | IEEE 802.3 |
68
+ | 2 | ARP | `ARP` | RFC 826, IPv4-over-Ethernet binding |
69
+ | 3 | IPv4 | `IPv4` | RFC 791, IHL/options aware |
70
+ | 3 | IPv6 | `IPv6` | RFC 8200, fixed header |
71
+ | 3 | ICMPv4 | `ICMPv4` | RFC 792, 8-byte header |
72
+ | 3 | ICMPv6 | `ICMPv6` | RFC 4443, 8-byte header |
73
+ | 4 | TCP | `TCP` | RFC 9293, data-offset/options aware |
74
+ | 4 | UDP | `UDP` | RFC 768 |
75
+
76
+ ## Decoding a captured frame
77
+
78
+ Each class decodes its own header from the start of a buffer and
79
+ tolerates trailing bytes, so you can walk a whole frame with two pieces
80
+ of information every header provides: `header_len` (how many bytes it
81
+ consumed) and `next_protocol()` (which class decodes what follows).
82
+
83
+ ```python
84
+ from netprotocols import Ethernet
85
+
86
+ def decode_frame(frame: bytes) -> list:
87
+ layers, cursor, protocol = [], 0, Ethernet
88
+ while protocol is not None:
89
+ header = protocol.decode(frame[cursor:])
90
+ layers.append(header)
91
+ cursor += header.header_len
92
+ protocol = header.next_protocol()
93
+ return layers # e.g. [Ethernet(...), IPv4(...), TCP(...)]
94
+ ```
95
+
96
+ Malformed input raises exceptions rooted at a single base class:
97
+
98
+ ```python
99
+ from netprotocols import ProtocolError, TruncatedHeaderError, InvalidFieldError
100
+
101
+ try:
102
+ layers = decode_frame(frame)
103
+ except TruncatedHeaderError: # buffer shorter than the header claims
104
+ ...
105
+ except InvalidFieldError: # nonsense field values (IHL < 5, bad address)
106
+ ...
107
+ except ProtocolError: # catches every library error
108
+ ...
109
+ ```
110
+
111
+ ## Building and serializing headers
112
+
113
+ Constructors take friendly values (string MAC/IP addresses, integer
114
+ fields) and validate them; `bytes()` emits the exact on-wire form.
115
+ `Packet` concatenates a stack of layers:
116
+
117
+ ```python
118
+ from netprotocols import ARP, Ethernet, Packet, random_mac
119
+
120
+ sha = random_mac()
121
+ frame = Packet(
122
+ Ethernet(dst="ff:ff:ff:ff:ff:ff", src=sha, ethertype=0x0806),
123
+ ARP(htype=1, ptype=0x0800, hlen=6, plen=4, oper=1,
124
+ sha=sha, spa="192.168.1.96",
125
+ tha="00:00:00:00:00:00", tpa="192.168.1.254"),
126
+ )
127
+ raw = bytes(frame) # ready for a raw socket
128
+ ```
129
+
130
+ Checksums are carried verbatim in both directions: the library neither
131
+ computes nor verifies them (planned — see the roadmap).
132
+
133
+ ## Display helpers
134
+
135
+ Every class exposes human-readable properties next to the raw fields:
136
+ `Ethernet.ethertype_name`, `IPv4.protocol_name`, `IPv4.flags_name`,
137
+ `TCP.flags_str` (`"SYN ACK"`), `ARP.oper_name`, `ICMPv4.type_name`,
138
+ and hexadecimal renderings such as `checksum_hex_str`. Unknown values
139
+ degrade gracefully (`"0x88cc"`, `"unknown (47)"`) instead of raising.
140
+
141
+ ## How it works
142
+
143
+ See [ARCHITECTURE.md](ARCHITECTURE.md) for a guided tour: how a header
144
+ byte layout maps onto a dataclass, the decode contract, the
145
+ `next_protocol()` chain, and a cookbook for adding a new protocol.
146
+
147
+ This library is the engine behind
148
+ [Packet-Sniffer](https://github.com/EONRaider/Packet-Sniffer), a
149
+ network traffic monitor built on it.
150
+
151
+ ## Roadmap
152
+
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.
159
+
160
+ ## Contributing
161
+
162
+ Development uses [uv](https://docs.astral.sh/uv/): `uv sync`, then
163
+ `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
165
+ [ARCHITECTURE.md](ARCHITECTURE.md).
166
+
167
+ ## License
168
+
169
+ [MIT](LICENSE)