netprotocols 1.0.0__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {netprotocols-1.0.0 → netprotocols-1.1.0}/.github/workflows/ci.yml +1 -1
- {netprotocols-1.0.0 → netprotocols-1.1.0}/.gitignore +1 -1
- {netprotocols-1.0.0 → netprotocols-1.1.0}/ARCHITECTURE.md +15 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/CHANGELOG.md +41 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/PKG-INFO +9 -9
- {netprotocols-1.0.0 → netprotocols-1.1.0}/README.md +8 -8
- {netprotocols-1.0.0 → netprotocols-1.1.0}/pyproject.toml +2 -1
- netprotocols-1.1.0/scripts/capture_fixtures.sh +104 -0
- netprotocols-1.1.0/scripts/capture_fixtures_supplement.sh +71 -0
- netprotocols-1.1.0/scripts/check_fixtures.py +228 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/src/netprotocols/__init__.py +15 -1
- {netprotocols-1.0.0 → netprotocols-1.1.0}/src/netprotocols/_enums.py +8 -0
- netprotocols-1.1.0/src/netprotocols/checksum.py +158 -0
- netprotocols-1.1.0/src/netprotocols/layer3/__init__.py +19 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/src/netprotocols/layer3/ip.py +40 -4
- netprotocols-1.1.0/src/netprotocols/layer3/ipv6_ext.py +253 -0
- netprotocols-1.1.0/src/netprotocols/packet.py +88 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/tests/conftest.py +37 -1
- netprotocols-1.1.0/tests/fixtures/MANIFEST.md +50 -0
- netprotocols-1.1.0/tests/fixtures/arp_exchange.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/icmpv4_echo_lo.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/icmpv4_external.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/icmpv4_ttl_exceeded.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/icmpv6_echo_lo.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/ipv4_fragments.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/ipv6_fragments.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/ipv6_mld.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/ipv6_ndp_mld.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/staging/arp_exchange.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/staging/icmpv4_echo_lo.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/staging/icmpv4_external.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/staging/icmpv4_ttl_exceeded.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/staging/icmpv6_echo_lo.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/staging/ipv4_fragments.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/staging/ipv6_fragments.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/staging/ipv6_mld.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/staging/ipv6_ndp_mld.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/staging/tcp_http.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/staging/tcp_https.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/staging/udp_dns.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/tcp_http.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/tcp_https.pcap +0 -0
- netprotocols-1.1.0/tests/fixtures/udp_dns.pcap +0 -0
- netprotocols-1.1.0/tests/test_checksum.py +239 -0
- netprotocols-1.1.0/tests/test_corpus.py +167 -0
- netprotocols-1.1.0/tests/test_fuzz.py +182 -0
- netprotocols-1.1.0/tests/test_ipv6_ext.py +177 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/uv.lock +84 -1
- netprotocols-1.0.0/src/netprotocols/layer3/__init__.py +0 -4
- netprotocols-1.0.0/src/netprotocols/packet.py +0 -48
- {netprotocols-1.0.0 → netprotocols-1.1.0}/.github/workflows/release.yml +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/LICENSE +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/src/netprotocols/_base.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/src/netprotocols/layer2/__init__.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/src/netprotocols/layer2/arp.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/src/netprotocols/layer2/ethernet.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/src/netprotocols/layer3/icmp.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/src/netprotocols/layer4/__init__.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/src/netprotocols/layer4/tcp.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/src/netprotocols/layer4/udp.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/src/netprotocols/py.typed +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/src/netprotocols/utils/__init__.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/src/netprotocols/utils/exceptions.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/src/netprotocols/utils/ipv4.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/src/netprotocols/utils/mac.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/tests/test_arp.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/tests/test_contract.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/tests/test_ethernet.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/tests/test_icmp.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/tests/test_ip.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/tests/test_packet.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/tests/test_tcp.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.0}/tests/test_udp.py +0 -0
- {netprotocols-1.0.0 → netprotocols-1.1.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
|
|
@@ -5,6 +5,47 @@ 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.1.0] - 2026-08-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **IPv6 extension headers** (RFC 8200 §4.3-4.6): `IPv6HopByHopOptions`,
|
|
12
|
+
`IPv6Routing`, `IPv6Fragment`, `IPv6DestinationOptions`. The decode
|
|
13
|
+
chain now reaches ICMPv6/TCP/UDP behind them (MLD reports decode
|
|
14
|
+
fully). Extension headers dispatch only inside an IPv6 chain, and a
|
|
15
|
+
Fragment header chains onward only from the first fragment.
|
|
16
|
+
- **Checksums** (`netprotocols.checksum`): `internet_checksum()`
|
|
17
|
+
(RFC 1071), `compute()`/`verify()` covering the IPv4 header checksum,
|
|
18
|
+
ICMPv4, and TCP/UDP/ICMPv6 with IPv4/IPv6 pseudo-headers, and
|
|
19
|
+
`Packet.with_checksums()` for filling a whole stack on encode. The
|
|
20
|
+
UDP-only zero substitution is confined to the UDP arm.
|
|
21
|
+
- **Property-based fuzzing** of the decode path (hypothesis, dev-only):
|
|
22
|
+
decode() raises nothing outside `ProtocolError` for any input; the
|
|
23
|
+
chain walk never consumes past the frame; constrained round trips.
|
|
24
|
+
- `IPProtocol` gains `HOPOPT`, `IPV6_ROUTE`, `IPV6_FRAG`,
|
|
25
|
+
`IPV6_DSTOPTS` with display names.
|
|
26
|
+
|
|
27
|
+
### Development
|
|
28
|
+
- The real-capture fixture corpus grew to 65 frames across 12
|
|
29
|
+
scenarios (MLD behind hop-by-hop, IPv6 fragment pairs); checksum
|
|
30
|
+
tests recompute every verifiable corpus frame to its captured wire
|
|
31
|
+
value.
|
|
32
|
+
|
|
33
|
+
## [1.0.1] - 2026-08-29
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- `IPv4.next_protocol()` chained non-first fragments
|
|
37
|
+
(`fragment_offset > 0`) into garbage upper-layer decodes — a slice
|
|
38
|
+
from the middle of a fragmented payload has no header at its start.
|
|
39
|
+
Discovered by the new real-capture fixture corpus on live fragmented
|
|
40
|
+
traffic; the chain now ends at the IPv4 layer for those frames, as it
|
|
41
|
+
already did for unknown protocols.
|
|
42
|
+
|
|
43
|
+
### Added (development)
|
|
44
|
+
- A real-capture fixture corpus under `tests/fixtures/` (56 frames,
|
|
45
|
+
10 scenarios, every checksum internally verified — see its
|
|
46
|
+
MANIFEST.md) with corpus-wide invariant tests, plus the capture and
|
|
47
|
+
validation scripts under `scripts/`.
|
|
48
|
+
|
|
8
49
|
## [1.0.0] - 2026-08-21
|
|
9
50
|
|
|
10
51
|
Complete rewrite. The public API is new; see the README and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: netprotocols
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.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,7 +67,11 @@ 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
|
|
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 |
|
|
73
77
|
| 4 | TCP | `TCP` | RFC 9293, data-offset/options aware |
|
|
@@ -127,8 +131,9 @@ frame = Packet(
|
|
|
127
131
|
raw = bytes(frame) # ready for a raw socket
|
|
128
132
|
```
|
|
129
133
|
|
|
130
|
-
Checksums are
|
|
131
|
-
|
|
134
|
+
Checksums are computed and verified on request — never silently:
|
|
135
|
+
`compute()`/`verify()` in `netprotocols.checksum`, and
|
|
136
|
+
`Packet.with_checksums()` to fill a whole stack before sending.
|
|
132
137
|
|
|
133
138
|
## Display helpers
|
|
134
139
|
|
|
@@ -150,12 +155,7 @@ network traffic monitor built on it.
|
|
|
150
155
|
|
|
151
156
|
## Roadmap
|
|
152
157
|
|
|
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
158
|
- 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
159
|
|
|
160
160
|
## Contributing
|
|
161
161
|
|
|
@@ -42,7 +42,11 @@ 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
|
|
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 |
|
|
48
52
|
| 4 | TCP | `TCP` | RFC 9293, data-offset/options aware |
|
|
@@ -102,8 +106,9 @@ frame = Packet(
|
|
|
102
106
|
raw = bytes(frame) # ready for a raw socket
|
|
103
107
|
```
|
|
104
108
|
|
|
105
|
-
Checksums are
|
|
106
|
-
|
|
109
|
+
Checksums are computed and verified on request — never silently:
|
|
110
|
+
`compute()`/`verify()` in `netprotocols.checksum`, and
|
|
111
|
+
`Packet.with_checksums()` to fill a whole stack before sending.
|
|
107
112
|
|
|
108
113
|
## Display helpers
|
|
109
114
|
|
|
@@ -125,12 +130,7 @@ network traffic monitor built on it.
|
|
|
125
130
|
|
|
126
131
|
## Roadmap
|
|
127
132
|
|
|
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
133
|
- 802.1Q VLAN tags, DNS, DHCP, IGMP, GRE.
|
|
132
|
-
- Checksum computation on encode and verification flags on decode.
|
|
133
|
-
- Fuzzing of the decode path.
|
|
134
134
|
|
|
135
135
|
## Contributing
|
|
136
136
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "netprotocols"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.1.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,104 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Capture the real-frame fixture corpus (issue #27, plan item L-0).
|
|
3
|
+
#
|
|
4
|
+
# Run once with sudo from the repository root:
|
|
5
|
+
#
|
|
6
|
+
# sudo scripts/capture_fixtures.sh
|
|
7
|
+
#
|
|
8
|
+
# Writes per-scenario pcaps into tests/fixtures/staging/ and finishes by
|
|
9
|
+
# running scripts/check_fixtures.py over them. Frames used for checksum
|
|
10
|
+
# ground truth are captured INBOUND only (outbound frames often carry
|
|
11
|
+
# checksums left unfilled for NIC offload). Nothing is committed by this
|
|
12
|
+
# script: review the checker report, then hand the staging directory to
|
|
13
|
+
# the test-writing step.
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
if [[ $EUID -ne 0 ]]; then
|
|
18
|
+
echo "This script needs root for tcpdump (or run it via sudo)." >&2
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
command -v tcpdump >/dev/null || { echo "tcpdump is required." >&2; exit 1; }
|
|
23
|
+
|
|
24
|
+
OUT="tests/fixtures/staging"
|
|
25
|
+
mkdir -p "$OUT"
|
|
26
|
+
|
|
27
|
+
IFACE=$(ip route show default | awk '/default/ {print $5; exit}')
|
|
28
|
+
GATEWAY=$(ip route show default | awk '/default/ {print $3; exit}')
|
|
29
|
+
[[ -n $IFACE ]] || { echo "No default interface found." >&2; exit 1; }
|
|
30
|
+
echo "[*] Default interface: $IFACE (gateway $GATEWAY)"
|
|
31
|
+
echo "[*] Writing to $OUT/"
|
|
32
|
+
|
|
33
|
+
# capture NAME IFACE COUNT FILTER -- TRAFFIC_CMD...
|
|
34
|
+
# Starts a bounded tcpdump, runs the traffic generator, waits.
|
|
35
|
+
capture() {
|
|
36
|
+
local name=$1 iface=$2 count=$3 filter=$4; shift 4; shift # consume --
|
|
37
|
+
echo "[>] $name"
|
|
38
|
+
tcpdump -i "$iface" -c "$count" -w "$OUT/$name.pcap" --immediate-mode \
|
|
39
|
+
-q $filter >/dev/null 2>&1 &
|
|
40
|
+
local pid=$!
|
|
41
|
+
sleep 1
|
|
42
|
+
"$@" >/dev/null 2>&1 || true
|
|
43
|
+
# Give tcpdump up to 15s to fill its count, then stop it either way.
|
|
44
|
+
for _ in $(seq 15); do kill -0 $pid 2>/dev/null || break; sleep 1; done
|
|
45
|
+
kill $pid 2>/dev/null || true
|
|
46
|
+
wait $pid 2>/dev/null || true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# --- Loopback scenarios (checksum ground truth: lo has no offload issue
|
|
50
|
+
# --- for inbound copies; we still filter inbound where it matters).
|
|
51
|
+
capture icmpv4_echo_lo lo 6 "icmp" -- ping -c 3 -i 0.3 127.0.0.1
|
|
52
|
+
capture icmpv6_echo_lo lo 6 "icmp6" -- ping -c 3 -i 0.3 ::1
|
|
53
|
+
|
|
54
|
+
# --- Default-interface scenarios, inbound-only where checksums matter.
|
|
55
|
+
capture udp_dns "$IFACE" 4 "inbound and udp and port 53" -- \
|
|
56
|
+
sh -c 'dig +tries=1 +time=3 example.com @1.1.1.1; dig +tries=1 +time=3 AAAA example.com @1.1.1.1'
|
|
57
|
+
capture tcp_http "$IFACE" 12 "inbound and tcp and port 80" -- \
|
|
58
|
+
curl -s -m 8 -o /dev/null http://example.com/
|
|
59
|
+
capture tcp_https "$IFACE" 12 "inbound and tcp and port 443" -- \
|
|
60
|
+
curl -s -m 8 -o /dev/null https://example.com/
|
|
61
|
+
capture icmpv4_external "$IFACE" 4 "inbound and icmp" -- \
|
|
62
|
+
ping -c 3 -i 0.4 -W 2 1.1.1.1
|
|
63
|
+
# A 2000-byte ping fragments on any 1500-MTU path: inbound echo replies
|
|
64
|
+
# arrive as an IPv4 fragment pair.
|
|
65
|
+
capture ipv4_fragments "$IFACE" 6 "inbound and (icmp or ip[6:2] & 0x3fff != 0)" -- \
|
|
66
|
+
ping -c 3 -i 0.4 -W 2 -s 2000 1.1.1.1
|
|
67
|
+
# TTL-exceeded errors from intermediate hops.
|
|
68
|
+
capture icmpv4_ttl_exceeded "$IFACE" 4 "inbound and icmp[icmptype] == 11" -- \
|
|
69
|
+
sh -c 'ping -c 2 -W 2 -t 2 1.1.1.1; ping -c 2 -W 2 -t 3 1.1.1.1'
|
|
70
|
+
|
|
71
|
+
# --- ARP (no checksums; both directions welcome). Flush the gateway
|
|
72
|
+
# --- neighbor entry to force a fresh exchange.
|
|
73
|
+
arp_traffic() {
|
|
74
|
+
ip neigh del "$GATEWAY" dev "$IFACE" 2>/dev/null || true
|
|
75
|
+
ping -c 1 -W 2 "$GATEWAY"
|
|
76
|
+
}
|
|
77
|
+
capture arp_exchange "$IFACE" 4 "arp" -- arp_traffic
|
|
78
|
+
|
|
79
|
+
# --- IPv6 NDP / MLD, ambient + provoked. MLD reports ride behind a
|
|
80
|
+
# --- hop-by-hop header (the L-1 fixture). Joining a multicast group
|
|
81
|
+
# --- provokes a report; pinging the all-nodes address provokes NDP.
|
|
82
|
+
ipv6_traffic() {
|
|
83
|
+
ping -6 -c 2 -W 2 "ff02::1%$IFACE" || true
|
|
84
|
+
python3 - "$IFACE" <<'PY' || true
|
|
85
|
+
import socket, struct, sys, time
|
|
86
|
+
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
|
87
|
+
ifindex = socket.if_nametoindex(sys.argv[1])
|
|
88
|
+
group = socket.inet_pton(socket.AF_INET6, "ff02::deca:fbad")
|
|
89
|
+
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP,
|
|
90
|
+
group + struct.pack("@I", ifindex))
|
|
91
|
+
time.sleep(3)
|
|
92
|
+
PY
|
|
93
|
+
}
|
|
94
|
+
capture ipv6_ndp_mld "$IFACE" 10 "icmp6" -- ipv6_traffic
|
|
95
|
+
|
|
96
|
+
echo
|
|
97
|
+
echo "[*] Captures done. Validating..."
|
|
98
|
+
CHECKER_PY=$(command -v python3)
|
|
99
|
+
if [[ -n ${SUDO_USER:-} ]]; then
|
|
100
|
+
chown -R "$SUDO_USER" "$OUT"
|
|
101
|
+
sudo -u "$SUDO_USER" "$CHECKER_PY" scripts/check_fixtures.py "$OUT"
|
|
102
|
+
else
|
|
103
|
+
"$CHECKER_PY" scripts/check_fixtures.py "$OUT"
|
|
104
|
+
fi
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Supplementary corpus capture: the two scenarios the first run missed
|
|
3
|
+
# (see tests/fixtures/MANIFEST.md "Known gaps").
|
|
4
|
+
#
|
|
5
|
+
# sudo scripts/capture_fixtures_supplement.sh
|
|
6
|
+
#
|
|
7
|
+
# 1. MLD behind hop-by-hop: the original run filtered with BPF `icmp6`,
|
|
8
|
+
# which matches next_header == 58 directly and therefore *excludes*
|
|
9
|
+
# MLD (which rides behind a hop-by-hop header, next_header == 0).
|
|
10
|
+
# This captures `ip6 proto 0` while joining/leaving a multicast
|
|
11
|
+
# group to provoke reports.
|
|
12
|
+
# 2. IPv6 fragment pair: an oversized ping -6 to an external host
|
|
13
|
+
# fragments on any 1500-MTU path (ip6 proto 44, inbound).
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
if [[ $EUID -ne 0 ]]; then
|
|
18
|
+
echo "This script needs root for tcpdump (or run it via sudo)." >&2
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
OUT="tests/fixtures/staging"
|
|
23
|
+
mkdir -p "$OUT"
|
|
24
|
+
IFACE=$(ip route show default | awk '/default/ {print $5; exit}')
|
|
25
|
+
echo "[*] Default interface: $IFACE"
|
|
26
|
+
|
|
27
|
+
capture() {
|
|
28
|
+
local name=$1 iface=$2 count=$3 filter=$4; shift 4; shift
|
|
29
|
+
echo "[>] $name"
|
|
30
|
+
tcpdump -i "$iface" -c "$count" -w "$OUT/$name.pcap" --immediate-mode \
|
|
31
|
+
-q $filter >/dev/null 2>&1 &
|
|
32
|
+
local pid=$!
|
|
33
|
+
sleep 1
|
|
34
|
+
"$@" >/dev/null 2>&1 || true
|
|
35
|
+
for _ in $(seq 20); do kill -0 $pid 2>/dev/null || break; sleep 1; done
|
|
36
|
+
kill $pid 2>/dev/null || true
|
|
37
|
+
wait $pid 2>/dev/null || true
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
mld_traffic() {
|
|
41
|
+
python3 - "$IFACE" <<'PY' || true
|
|
42
|
+
import socket, struct, sys, time
|
|
43
|
+
ifindex = socket.if_nametoindex(sys.argv[1])
|
|
44
|
+
group = socket.inet_pton(socket.AF_INET6, "ff02::deca:fbad")
|
|
45
|
+
mreq = group + struct.pack("@I", ifindex)
|
|
46
|
+
for _ in range(3):
|
|
47
|
+
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
|
48
|
+
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
|
|
49
|
+
time.sleep(1.5)
|
|
50
|
+
sock.close() # leaving also emits an MLD Done/Report
|
|
51
|
+
time.sleep(1.5)
|
|
52
|
+
PY
|
|
53
|
+
}
|
|
54
|
+
capture ipv6_mld "$IFACE" 4 "ip6 proto 0" -- mld_traffic
|
|
55
|
+
|
|
56
|
+
ipv6_frag_traffic() {
|
|
57
|
+
# An IPv6-reachable anycast resolver; 2000-byte payload fragments
|
|
58
|
+
# on the way back.
|
|
59
|
+
ping -6 -c 3 -i 0.5 -W 3 -s 2000 2606:4700:4700::1111
|
|
60
|
+
}
|
|
61
|
+
capture ipv6_fragments "$IFACE" 8 "inbound and ip6 proto 44" -- ipv6_frag_traffic
|
|
62
|
+
|
|
63
|
+
echo
|
|
64
|
+
echo "[*] Validating supplement..."
|
|
65
|
+
CHECKER_PY=$(command -v python3)
|
|
66
|
+
if [[ -n ${SUDO_USER:-} ]]; then
|
|
67
|
+
chown -R "$SUDO_USER" "$OUT"
|
|
68
|
+
sudo -u "$SUDO_USER" "$CHECKER_PY" scripts/check_fixtures.py "$OUT"
|
|
69
|
+
else
|
|
70
|
+
"$CHECKER_PY" scripts/check_fixtures.py "$OUT"
|
|
71
|
+
fi
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate the internal checksum consistency of captured fixture pcaps.
|
|
3
|
+
|
|
4
|
+
Deliberately standalone (stdlib only, its own RFC 1071 implementation,
|
|
5
|
+
its own minimal parsers): it must be able to disqualify a capture even
|
|
6
|
+
if netprotocols itself has a bug, and later it cross-validates the
|
|
7
|
+
library's checksum module (plan item L-2) with an independent
|
|
8
|
+
implementation.
|
|
9
|
+
|
|
10
|
+
Usage: python3 scripts/check_fixtures.py tests/fixtures/staging
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import struct
|
|
16
|
+
import sys
|
|
17
|
+
from collections import Counter
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def internet_checksum(data: bytes) -> int:
|
|
22
|
+
if len(data) % 2:
|
|
23
|
+
data += b"\x00"
|
|
24
|
+
total = sum(struct.unpack(f"!{len(data) // 2}H", data))
|
|
25
|
+
while total >> 16:
|
|
26
|
+
total = (total & 0xFFFF) + (total >> 16)
|
|
27
|
+
return ~total & 0xFFFF
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def read_pcap(path: Path):
|
|
31
|
+
data = path.read_bytes()
|
|
32
|
+
if len(data) < 24:
|
|
33
|
+
raise ValueError("not a pcap: too short")
|
|
34
|
+
magic = data[:4]
|
|
35
|
+
if magic in (b"\xa1\xb2\xc3\xd4", b"\xa1\xb2\x3c\x4d"):
|
|
36
|
+
endian = ">"
|
|
37
|
+
elif magic in (b"\xd4\xc3\xb2\xa1", b"\x4d\x3c\xb2\xa1"):
|
|
38
|
+
endian = "<"
|
|
39
|
+
else:
|
|
40
|
+
raise ValueError(f"not a pcap: magic {magic.hex()}")
|
|
41
|
+
(network,) = struct.unpack_from(f"{endian}I", data, 20)
|
|
42
|
+
if network != 1:
|
|
43
|
+
raise ValueError(f"linktype {network} is not Ethernet")
|
|
44
|
+
cursor = 24
|
|
45
|
+
while cursor + 16 <= len(data):
|
|
46
|
+
_, _, incl_len, _ = struct.unpack_from(f"{endian}IIII", data, cursor)
|
|
47
|
+
cursor += 16
|
|
48
|
+
yield data[cursor : cursor + incl_len]
|
|
49
|
+
cursor += incl_len
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def pseudo_v4(src: bytes, dst: bytes, proto: int, length: int) -> bytes:
|
|
53
|
+
return src + dst + struct.pack("!BBH", 0, proto, length)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def pseudo_v6(src: bytes, dst: bytes, proto: int, length: int) -> bytes:
|
|
57
|
+
return src + dst + struct.pack("!IBBBB", length, 0, 0, 0, proto)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def check_frame(frame: bytes, stats: Counter) -> list[str]:
|
|
61
|
+
"""Validate one Ethernet frame; returns a list of failure strings."""
|
|
62
|
+
failures: list[str] = []
|
|
63
|
+
if len(frame) < 14:
|
|
64
|
+
stats["runt"] += 1
|
|
65
|
+
return failures
|
|
66
|
+
ethertype = struct.unpack_from("!H", frame, 12)[0]
|
|
67
|
+
payload = frame[14:]
|
|
68
|
+
|
|
69
|
+
if ethertype == 0x0806:
|
|
70
|
+
stats["arp"] += 1
|
|
71
|
+
return failures
|
|
72
|
+
|
|
73
|
+
if ethertype == 0x0800 and len(payload) >= 20:
|
|
74
|
+
ihl = (payload[0] & 0x0F) * 4
|
|
75
|
+
header = payload[:ihl]
|
|
76
|
+
stats["ipv4"] += 1
|
|
77
|
+
if (
|
|
78
|
+
internet_checksum(header[:10] + b"\x00\x00" + header[12:ihl])
|
|
79
|
+
!= struct.unpack_from("!H", header, 10)[0]
|
|
80
|
+
):
|
|
81
|
+
failures.append("ipv4-header-checksum")
|
|
82
|
+
src, dst = header[12:16], header[16:20]
|
|
83
|
+
proto = header[9]
|
|
84
|
+
total_length = struct.unpack_from("!H", header, 2)[0]
|
|
85
|
+
flags_frag = struct.unpack_from("!H", header, 6)[0]
|
|
86
|
+
if flags_frag & 0x1FFF:
|
|
87
|
+
stats["ipv4-fragment"] += 1
|
|
88
|
+
return failures # non-first fragment: no upper-layer header
|
|
89
|
+
if flags_frag & 0x2000 == 0x2000 and flags_frag & 0x1FFF == 0:
|
|
90
|
+
stats["ipv4-first-fragment"] += 1
|
|
91
|
+
return failures # first fragment: upper checksum spans all parts
|
|
92
|
+
upper = payload[ihl:total_length]
|
|
93
|
+
upper_len = len(upper)
|
|
94
|
+
if proto == 1 and upper_len >= 8:
|
|
95
|
+
stats["icmpv4"] += 1
|
|
96
|
+
if (
|
|
97
|
+
internet_checksum(upper[:2] + b"\x00\x00" + upper[4:])
|
|
98
|
+
!= struct.unpack_from("!H", upper, 2)[0]
|
|
99
|
+
):
|
|
100
|
+
failures.append("icmpv4-checksum")
|
|
101
|
+
elif proto == 6 and upper_len >= 20:
|
|
102
|
+
stats["tcp"] += 1
|
|
103
|
+
zeroed = upper[:16] + b"\x00\x00" + upper[18:]
|
|
104
|
+
if (
|
|
105
|
+
internet_checksum(pseudo_v4(src, dst, 6, upper_len) + zeroed)
|
|
106
|
+
!= struct.unpack_from("!H", upper, 16)[0]
|
|
107
|
+
):
|
|
108
|
+
failures.append("tcp-checksum")
|
|
109
|
+
elif proto == 17 and upper_len >= 8:
|
|
110
|
+
stats["udp"] += 1
|
|
111
|
+
wire = struct.unpack_from("!H", upper, 6)[0]
|
|
112
|
+
if wire == 0:
|
|
113
|
+
stats["udp-no-checksum"] += 1
|
|
114
|
+
else:
|
|
115
|
+
zeroed = upper[:6] + b"\x00\x00" + upper[8:]
|
|
116
|
+
computed = (
|
|
117
|
+
internet_checksum(
|
|
118
|
+
pseudo_v4(src, dst, 17, upper_len) + zeroed
|
|
119
|
+
)
|
|
120
|
+
or 0xFFFF
|
|
121
|
+
)
|
|
122
|
+
if computed != wire:
|
|
123
|
+
failures.append("udp-checksum")
|
|
124
|
+
return failures
|
|
125
|
+
|
|
126
|
+
if ethertype == 0x86DD and len(payload) >= 40:
|
|
127
|
+
stats["ipv6"] += 1
|
|
128
|
+
payload_length, next_header = struct.unpack_from("!HB", payload, 4)
|
|
129
|
+
src, dst = payload[8:24], payload[24:40]
|
|
130
|
+
upper = payload[40 : 40 + payload_length]
|
|
131
|
+
# Walk extension headers (hop-by-hop 0, routing 43, dest-opts 60).
|
|
132
|
+
while next_header in (0, 43, 60) and len(upper) >= 8:
|
|
133
|
+
stats["ipv6-ext-header"] += 1
|
|
134
|
+
ext_len = (upper[1] + 1) * 8
|
|
135
|
+
next_header, upper = upper[0], upper[ext_len:]
|
|
136
|
+
if next_header == 44 and len(upper) >= 8:
|
|
137
|
+
if struct.unpack_from("!H", upper, 2)[0] & 0xFFF8:
|
|
138
|
+
stats["ipv6-fragment"] += 1
|
|
139
|
+
return failures # non-first fragment: no upper header
|
|
140
|
+
stats["ipv6-first-fragment"] += 1
|
|
141
|
+
# The upper-layer checksum spans the reassembled datagram,
|
|
142
|
+
# not this fragment's slice: nothing verifiable here.
|
|
143
|
+
return failures
|
|
144
|
+
upper_len = len(upper)
|
|
145
|
+
if next_header == 58 and upper_len >= 4:
|
|
146
|
+
stats["icmpv6"] += 1
|
|
147
|
+
zeroed = upper[:2] + b"\x00\x00" + upper[4:]
|
|
148
|
+
if (
|
|
149
|
+
internet_checksum(pseudo_v6(src, dst, 58, upper_len) + zeroed)
|
|
150
|
+
!= struct.unpack_from("!H", upper, 2)[0]
|
|
151
|
+
):
|
|
152
|
+
failures.append("icmpv6-checksum")
|
|
153
|
+
elif next_header == 6 and upper_len >= 20:
|
|
154
|
+
stats["tcp6"] += 1
|
|
155
|
+
zeroed = upper[:16] + b"\x00\x00" + upper[18:]
|
|
156
|
+
if (
|
|
157
|
+
internet_checksum(pseudo_v6(src, dst, 6, upper_len) + zeroed)
|
|
158
|
+
!= struct.unpack_from("!H", upper, 16)[0]
|
|
159
|
+
):
|
|
160
|
+
failures.append("tcp6-checksum")
|
|
161
|
+
elif next_header == 17 and upper_len >= 8:
|
|
162
|
+
stats["udp6"] += 1
|
|
163
|
+
zeroed = upper[:6] + b"\x00\x00" + upper[8:]
|
|
164
|
+
computed = (
|
|
165
|
+
internet_checksum(pseudo_v6(src, dst, 17, upper_len) + zeroed)
|
|
166
|
+
or 0xFFFF
|
|
167
|
+
)
|
|
168
|
+
if computed != struct.unpack_from("!H", upper, 6)[0]:
|
|
169
|
+
failures.append("udp6-checksum")
|
|
170
|
+
return failures
|
|
171
|
+
|
|
172
|
+
stats[f"other-0x{ethertype:04x}"] += 1
|
|
173
|
+
return failures
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def main() -> int:
|
|
177
|
+
staging = Path(
|
|
178
|
+
sys.argv[1] if len(sys.argv) > 1 else "tests/fixtures/staging"
|
|
179
|
+
)
|
|
180
|
+
pcaps = sorted(staging.glob("*.pcap"))
|
|
181
|
+
if not pcaps:
|
|
182
|
+
print(f"No pcaps found in {staging}/")
|
|
183
|
+
return 1
|
|
184
|
+
|
|
185
|
+
corpus_stats: Counter = Counter()
|
|
186
|
+
bad_files = 0
|
|
187
|
+
total_frames = 0
|
|
188
|
+
for pcap in pcaps:
|
|
189
|
+
stats: Counter = Counter()
|
|
190
|
+
failures: Counter = Counter()
|
|
191
|
+
frames = 0
|
|
192
|
+
try:
|
|
193
|
+
for frame in read_pcap(pcap):
|
|
194
|
+
frames += 1
|
|
195
|
+
for failure in check_frame(frame, stats):
|
|
196
|
+
failures[failure] += 1
|
|
197
|
+
except ValueError as e:
|
|
198
|
+
print(f"[FAIL] {pcap.name}: {e}")
|
|
199
|
+
bad_files += 1
|
|
200
|
+
continue
|
|
201
|
+
total_frames += frames
|
|
202
|
+
corpus_stats.update(stats)
|
|
203
|
+
verdict = "OK " if not failures else "FAIL"
|
|
204
|
+
detail = ", ".join(f"{k}x{v}" for k, v in sorted(stats.items()))
|
|
205
|
+
print(f"[{verdict}] {pcap.name}: {frames} frames ({detail})")
|
|
206
|
+
for failure, count in sorted(failures.items()):
|
|
207
|
+
print(f" !! {failure} x{count}")
|
|
208
|
+
if failures:
|
|
209
|
+
bad_files += 1
|
|
210
|
+
|
|
211
|
+
print(f"\nCorpus: {total_frames} frames across {len(pcaps)} files")
|
|
212
|
+
print(
|
|
213
|
+
"Coverage:",
|
|
214
|
+
", ".join(f"{k}={v}" for k, v in sorted(corpus_stats.items())),
|
|
215
|
+
)
|
|
216
|
+
if bad_files:
|
|
217
|
+
print(
|
|
218
|
+
f"\n{bad_files} file(s) FAILED checksum consistency — "
|
|
219
|
+
f"do not commit these; re-capture or investigate offload "
|
|
220
|
+
f"(ethtool -K <iface> tx off rx off)."
|
|
221
|
+
)
|
|
222
|
+
return 1
|
|
223
|
+
print("\nAll files internally consistent.")
|
|
224
|
+
return 0
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
if __name__ == "__main__":
|
|
228
|
+
raise SystemExit(main())
|
|
@@ -12,10 +12,17 @@ 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
|
|
18
19
|
from netprotocols.layer3.ip import IPv4, IPv6
|
|
20
|
+
from netprotocols.layer3.ipv6_ext import (
|
|
21
|
+
IPv6DestinationOptions,
|
|
22
|
+
IPv6Fragment,
|
|
23
|
+
IPv6HopByHopOptions,
|
|
24
|
+
IPv6Routing,
|
|
25
|
+
)
|
|
19
26
|
from netprotocols.layer4.tcp import TCP
|
|
20
27
|
from netprotocols.layer4.udp import UDP
|
|
21
28
|
from netprotocols.packet import Packet
|
|
@@ -30,7 +37,7 @@ from netprotocols.utils.exceptions import (
|
|
|
30
37
|
from netprotocols.utils.ipv4 import validate_ipv4_addr
|
|
31
38
|
from netprotocols.utils.mac import random_mac, validate_mac_addr
|
|
32
39
|
|
|
33
|
-
__version__ = "1.
|
|
40
|
+
__version__ = "1.1.0"
|
|
34
41
|
|
|
35
42
|
__all__ = [
|
|
36
43
|
"ARP",
|
|
@@ -45,6 +52,10 @@ __all__ = [
|
|
|
45
52
|
"IPProtocol",
|
|
46
53
|
"IPv4",
|
|
47
54
|
"IPv6",
|
|
55
|
+
"IPv6DestinationOptions",
|
|
56
|
+
"IPv6Fragment",
|
|
57
|
+
"IPv6HopByHopOptions",
|
|
58
|
+
"IPv6Routing",
|
|
48
59
|
"InvalidFieldError",
|
|
49
60
|
"InvalidIPv4AddressError",
|
|
50
61
|
"InvalidMACAddressError",
|
|
@@ -54,7 +65,10 @@ __all__ = [
|
|
|
54
65
|
"ProtocolError",
|
|
55
66
|
"TruncatedHeaderError",
|
|
56
67
|
"__version__",
|
|
68
|
+
"compute",
|
|
69
|
+
"internet_checksum",
|
|
57
70
|
"random_mac",
|
|
58
71
|
"validate_ipv4_addr",
|
|
59
72
|
"validate_mac_addr",
|
|
73
|
+
"verify",
|
|
60
74
|
]
|
|
@@ -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
|
|