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.
- netprotocols-1.0.1/.github/workflows/ci.yml +47 -0
- netprotocols-1.0.1/.github/workflows/release.yml +18 -0
- netprotocols-1.0.1/.gitignore +149 -0
- netprotocols-1.0.1/ARCHITECTURE.md +222 -0
- netprotocols-1.0.1/CHANGELOG.md +74 -0
- netprotocols-1.0.1/LICENSE +21 -0
- netprotocols-1.0.1/PKG-INFO +169 -0
- netprotocols-1.0.1/README.md +144 -0
- netprotocols-1.0.1/pyproject.toml +75 -0
- netprotocols-1.0.1/scripts/capture_fixtures.sh +104 -0
- netprotocols-1.0.1/scripts/capture_fixtures_supplement.sh +71 -0
- netprotocols-1.0.1/scripts/check_fixtures.py +225 -0
- netprotocols-1.0.1/src/netprotocols/__init__.py +60 -0
- netprotocols-1.0.1/src/netprotocols/_base.py +128 -0
- netprotocols-1.0.1/src/netprotocols/_enums.py +77 -0
- netprotocols-1.0.1/src/netprotocols/layer2/__init__.py +4 -0
- netprotocols-1.0.1/src/netprotocols/layer2/arp.py +107 -0
- netprotocols-1.0.1/src/netprotocols/layer2/ethernet.py +66 -0
- netprotocols-1.0.1/src/netprotocols/layer3/__init__.py +4 -0
- netprotocols-1.0.1/src/netprotocols/layer3/icmp.py +135 -0
- netprotocols-1.0.1/src/netprotocols/layer3/ip.py +276 -0
- netprotocols-1.0.1/src/netprotocols/layer4/__init__.py +4 -0
- netprotocols-1.0.1/src/netprotocols/layer4/tcp.py +153 -0
- netprotocols-1.0.1/src/netprotocols/layer4/udp.py +50 -0
- netprotocols-1.0.1/src/netprotocols/packet.py +48 -0
- netprotocols-1.0.1/src/netprotocols/py.typed +0 -0
- netprotocols-1.0.1/src/netprotocols/utils/__init__.py +22 -0
- netprotocols-1.0.1/src/netprotocols/utils/exceptions.py +48 -0
- netprotocols-1.0.1/src/netprotocols/utils/ipv4.py +32 -0
- netprotocols-1.0.1/src/netprotocols/utils/mac.py +64 -0
- netprotocols-1.0.1/tests/conftest.py +111 -0
- netprotocols-1.0.1/tests/fixtures/MANIFEST.md +48 -0
- netprotocols-1.0.1/tests/fixtures/arp_exchange.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/icmpv4_echo_lo.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/icmpv4_external.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/icmpv4_ttl_exceeded.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/icmpv6_echo_lo.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/ipv4_fragments.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/ipv6_ndp_mld.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/staging/arp_exchange.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/staging/icmpv4_echo_lo.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/staging/icmpv4_external.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/staging/icmpv4_ttl_exceeded.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/staging/icmpv6_echo_lo.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/staging/ipv4_fragments.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/staging/ipv6_ndp_mld.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/staging/tcp_http.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/staging/tcp_https.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/staging/udp_dns.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/tcp_http.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/tcp_https.pcap +0 -0
- netprotocols-1.0.1/tests/fixtures/udp_dns.pcap +0 -0
- netprotocols-1.0.1/tests/test_arp.py +65 -0
- netprotocols-1.0.1/tests/test_contract.py +104 -0
- netprotocols-1.0.1/tests/test_corpus.py +167 -0
- netprotocols-1.0.1/tests/test_ethernet.py +40 -0
- netprotocols-1.0.1/tests/test_icmp.py +55 -0
- netprotocols-1.0.1/tests/test_ip.py +92 -0
- netprotocols-1.0.1/tests/test_packet.py +37 -0
- netprotocols-1.0.1/tests/test_tcp.py +80 -0
- netprotocols-1.0.1/tests/test_udp.py +24 -0
- netprotocols-1.0.1/tests/test_utils.py +66 -0
- netprotocols-1.0.1/uv.lock +475 -0
- netprotocols-0.8.0/PKG-INFO +0 -12
- netprotocols-0.8.0/netprotocols/__init__.py +0 -8
- netprotocols-0.8.0/netprotocols/base/packet.py +0 -34
- netprotocols-0.8.0/netprotocols/base/protocol.py +0 -131
- netprotocols-0.8.0/netprotocols/layer2/arp.py +0 -100
- netprotocols-0.8.0/netprotocols/layer2/ethernet.py +0 -45
- netprotocols-0.8.0/netprotocols/layer3/icmp.py +0 -148
- netprotocols-0.8.0/netprotocols/layer3/ip.py +0 -188
- netprotocols-0.8.0/netprotocols/layer4/tcp.py +0 -102
- netprotocols-0.8.0/netprotocols/layer4/udp.py +0 -34
- netprotocols-0.8.0/netprotocols/utils/__init__.py +0 -1
- netprotocols-0.8.0/netprotocols/utils/exceptions.py +0 -22
- netprotocols-0.8.0/netprotocols/utils/random_mac.py +0 -34
- netprotocols-0.8.0/netprotocols/utils/validation/_base.py +0 -22
- netprotocols-0.8.0/netprotocols/utils/validation/ipv4.py +0 -38
- netprotocols-0.8.0/netprotocols/utils/validation/mac.py +0 -48
- netprotocols-0.8.0/pyproject.toml +0 -22
- 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
|
+
[](https://github.com/EONRaider/NETProtocols/actions/workflows/ci.yml)
|
|
29
|
+

|
|
30
|
+
[](LICENSE)
|
|
31
|
+

|
|
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)
|