PyTCP 2.7.9__tar.gz → 3.0.4__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 (44) hide show
  1. pytcp-3.0.4/PKG-INFO +685 -0
  2. pytcp-3.0.4/PyTCP.egg-info/PKG-INFO +685 -0
  3. pytcp-3.0.4/PyTCP.egg-info/SOURCES.txt +34 -0
  4. pytcp-3.0.4/PyTCP.egg-info/top_level.txt +3 -0
  5. pytcp-3.0.4/README.md +664 -0
  6. pytcp-3.0.4/net_addr/__init__.py +136 -0
  7. pytcp-3.0.4/net_addr/address.py +93 -0
  8. pytcp-3.0.4/net_addr/base.py +71 -0
  9. pytcp-3.0.4/net_addr/click_types.py +358 -0
  10. pytcp-3.0.4/net_addr/errors.py +190 -0
  11. pytcp-3.0.4/net_addr/ip.py +69 -0
  12. pytcp-3.0.4/net_addr/ip4_address.py +267 -0
  13. pytcp-3.0.4/net_addr/ip4_host.py +134 -0
  14. pytcp-3.0.4/net_addr/ip4_host_origin.py +45 -0
  15. pytcp-3.0.4/net_addr/ip4_mask.py +103 -0
  16. pytcp-3.0.4/net_addr/ip4_network.py +108 -0
  17. pytcp-3.0.4/net_addr/ip6_address.py +288 -0
  18. pytcp-3.0.4/net_addr/ip6_host.py +284 -0
  19. pytcp-3.0.4/net_addr/ip6_host_origin.py +46 -0
  20. pytcp-3.0.4/net_addr/ip6_mask.py +94 -0
  21. pytcp-3.0.4/net_addr/ip6_network.py +98 -0
  22. pytcp-3.0.4/net_addr/ip_address.py +107 -0
  23. pytcp-3.0.4/net_addr/ip_host.py +167 -0
  24. pytcp-3.0.4/net_addr/ip_host_origin.py +39 -0
  25. pytcp-3.0.4/net_addr/ip_mask.py +101 -0
  26. pytcp-3.0.4/net_addr/ip_network.py +122 -0
  27. pytcp-3.0.4/net_addr/ip_version.py +49 -0
  28. pytcp-3.0.4/net_addr/mac_address.py +159 -0
  29. pytcp-3.0.4/net_proto/__init__.py +828 -0
  30. pytcp-3.0.4/pyproject.toml +109 -0
  31. pytcp-3.0.4/pytcp/__init__.py +38 -0
  32. pytcp-3.0.4/pytcp/template.py +31 -0
  33. pytcp-2.7.9/PKG-INFO +0 -355
  34. pytcp-2.7.9/PyTCP.egg-info/PKG-INFO +0 -355
  35. pytcp-2.7.9/PyTCP.egg-info/SOURCES.txt +0 -10
  36. pytcp-2.7.9/PyTCP.egg-info/top_level.txt +0 -1
  37. pytcp-2.7.9/README.md +0 -334
  38. pytcp-2.7.9/pyproject.toml +0 -73
  39. pytcp-2.7.9/pytcp/__init__.py +0 -150
  40. pytcp-2.7.9/pytcp/config.py +0 -146
  41. {pytcp-2.7.9 → pytcp-3.0.4}/LICENSE +0 -0
  42. {pytcp-2.7.9 → pytcp-3.0.4}/PyTCP.egg-info/dependency_links.txt +0 -0
  43. {pytcp-2.7.9 → pytcp-3.0.4}/pytcp/py.typed +0 -0
  44. {pytcp-2.7.9 → pytcp-3.0.4}/setup.cfg +0 -0
pytcp-3.0.4/PKG-INFO ADDED
@@ -0,0 +1,685 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyTCP
3
+ Version: 3.0.4
4
+ Summary: Pure-Python, zero-dependency TCP/IP stack — Ethernet through RFC 9293 TCP — running in user space on a TAP/TUN interface, with a Berkeley-sockets API.
5
+ Author-email: Sebastian Majewski <ccie18643@gmail.com>
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/ccie18643/PyTCP
8
+ Project-URL: Bug Tracker, https://github.com/ccie18643/PyTCP/issues
9
+ Keywords: pytcp,stack,networking,tcp,ip,ipv4,ipv6,arp,ethernet,icmp
10
+ Classifier: Programming Language :: Python :: 3.14
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Natural Language :: English
14
+ Classifier: Environment :: Console
15
+ Classifier: Topic :: System :: Networking
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.14
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # PyTCP
23
+ **The TCP/IP stack written in Python**
24
+ <br>
25
+
26
+ [![GitHub release](https://img.shields.io/github/v/release/ccie18643/PyTCP)](https://github.com/ccie18643/PyTCP/releases)
27
+ [![OS](https://img.shields.io/badge/os-Linux-blue)](https://kernel.org)
28
+ [![Supported Versions](https://img.shields.io/pypi/pyversions/PyTCP.svg)](https://pypi.org/project/PyTCP)
29
+ [![GitHub License](https://img.shields.io/badge/license-GPL--3.0-yellowgreen)](https://github.com/ccie18643/PyTCP/blob/master/LICENSE)
30
+ [![CI](https://github.com/ccie18643/PyTCP/actions/workflows/ci.yml/badge.svg)](https://github.com/ccie18643/PyTCP/actions/workflows/ci.yml)
31
+
32
+ [![GitHub watchers](https://img.shields.io/github/watchers/ccie18643/PyTCP.svg?style=social&label=Watch&maxAge=2592000)](https://GitHub.com/ccie18643/PyTCP/watchers/)
33
+ [![GitHub forks](https://img.shields.io/github/forks/ccie18643/PyTCP.svg?style=social&label=Fork&maxAge=2592000)](https://GitHub.com/ccie18643/PyTCP/network/)
34
+ [![GitHub stars](https://img.shields.io/github/stars/ccie18643/PyTCP.svg?style=social&label=Star&maxAge=2592000)](https://GitHub.com/ccie18643/PyTCP/stargazers/)
35
+
36
+ <br>
37
+
38
+ **PyTCP is a TCP/IP stack written in pure Python.** It runs in user space, attached to a Linux TAP/TUN interface, and implements the protocol layers itself rather than calling the host stack.
39
+
40
+ The stack covers Ethernet II and IEEE 802.3 framing, ARP, IPv4 and IPv6 (extension headers and fragmentation), ICMPv4 and ICMPv6, IPv6 Neighbor Discovery and SLAAC, a DHCPv4 client, UDP, and RFC 9293 TCP. The TCP implementation includes the full finite state machine, congestion control (CUBIC, NewReno, PRR, HyStart++), SACK and RACK-TLP loss recovery, and RFC 5961 hardening. It exchanges traffic with other hosts on the local segment and over the Internet.
41
+
42
+ The project's goal is a pure-Python stack that is feature-equivalent to the Linux kernel network stack. RFC text is the primary authority; where a spec is silent or offers a choice, PyTCP follows Linux. Host-stack parity is the current scope; router-grade forwarding is planned.
43
+
44
+ Behaviour is covered by roughly 11,000 unit and integration tests and tracked against more than 100 per-RFC adherence audits kept in the repository under `docs/rfc/`.
45
+
46
+ The stack has zero runtime dependencies (standard library only), is organised as three packages (`net_addr`, `net_proto`, `pytcp`), and exposes a Berkeley-sockets-style API so it can be used in place of the standard socket layer.
47
+
48
+ Contributions are welcome.
49
+
50
+ ---
51
+
52
+
53
+ ### Features
54
+
55
+ #### Stack & sockets (engineering, non-RFC)
56
+
57
+ - Zero-copy packet parser and assembler (buffer-protocol / memoryview based).
58
+ - `net_addr` value-type libraries for MAC / IPv4 / IPv6 addresses, networks, hosts and masks - no Python standard-library dependency.
59
+ - Importable as a zero-runtime-dependency library (stdlib only), split into three independent packages: `net_addr`, `net_proto`, `pytcp`.
60
+ - Event-driven millisecond-resolution timer (heap-based deadline scheduler, no polling tick).
61
+ - Runtime-tunable sysctl registry mirroring the Linux `/proc/sys/net/` surface (boot-time and live overrides).
62
+ - Link control API (ip-link-style): per-interface MAC / MTU / state / counters.
63
+ - Per-protocol packet-flow stat counters; TX-path feedback so send failures reach sockets.
64
+ - Homegrown high-performance logger (no third-party logging dependency).
65
+ - Berkeley-sockets-style API for TCP / UDP / RAW: `fileno()`/eventfd + `selectors` integration, blocking & non-blocking modes, errno-mapped `OSError`, `getaddrinfo` family, common `setsockopt` options, `IP_RECVERR`/`MSG_ERRQUEUE` error queue.
66
+ - Native `unittest` suite (~11,000 unit + integration tests); per-RFC adherence audits in `docs/rfc/`.
67
+
68
+ #### Ethernet
69
+
70
+ - Ethernet II framing with EtherType demux, broadcast and multicast mapping (RFC 894)
71
+ - Inbound IEEE 802.3 / LLC + SNAP support (RFC 1042)
72
+
73
+ #### ARP
74
+
75
+ - ARP resolution with a neighbor cache, replies and queries (RFC 826, RFC 1122)
76
+ - IPv4 Address Conflict Detection — probe, announce, defend (RFC 5227)
77
+ - IANA-correct ARP codepoint handling (RFC 5494)
78
+
79
+ #### IPv4
80
+
81
+ - IPv4 with options parsing, inbound reassembly and outbound fragmentation (RFC 791, RFC 815)
82
+ - Multiple host addresses; private, special-purpose and broadcast address handling (RFC 1918, RFC 6890, RFC 919, RFC 922)
83
+ - ECN, DSCP and Router Alert support (RFC 3168, RFC 2474, RFC 6398)
84
+ - IPv4 link-local autoconfiguration (RFC 3927)
85
+ - Host-side IP multicasting (RFC 1112)
86
+
87
+ #### ICMPv4
88
+
89
+ - Echo, Destination Unreachable, Time Exceeded and Parameter Problem, with RFC-correct generation gating and rate-limiting (RFC 792, RFC 1122)
90
+ - Obsolete message types correctly omitted (RFC 6633, RFC 6918)
91
+
92
+ #### IPv6
93
+
94
+ - IPv6 with the full extension-header chain and TLV options (RFC 8200)
95
+ - Inbound reassembly and outbound fragmentation, with fragmentation hardening (RFC 5722, RFC 6946, RFC 7739)
96
+ - Unique-local and special-purpose addressing (RFC 4193, RFC 8190)
97
+ - Flow-label generation (RFC 6437)
98
+ - Default source-address selection (RFC 6724); Path MTU Discovery (RFC 8201); node requirements (RFC 8504)
99
+
100
+ #### ICMPv6 / Neighbor Discovery
101
+
102
+ - Full ICMPv6 message set including Packet Too Big (RFC 4443)
103
+ - Stateless Address Autoconfiguration: link-local, DAD, RA prefixes and lifetimes (RFC 4862)
104
+ - Stable opaque and temporary (privacy) addresses (RFC 7217, RFC 8981)
105
+ - Optimistic DAD, Enhanced DAD and Gratuitous NA (RFC 4429, RFC 7527, RFC 9131)
106
+ - Neighbor Discovery with a NUD cache and Router Solicitation backoff (RFC 4861, RFC 7559)
107
+ - MLDv2 listener (RFC 3810)
108
+
109
+ #### UDP
110
+
111
+ - UDP with full host-requirements conformance (RFC 768, RFC 1122)
112
+ - Zero-checksum UDP over IPv6 (RFC 6935)
113
+ - Ephemeral-port randomisation (RFC 6056)
114
+ - Echo / Discard / Daytime example services
115
+
116
+ #### TCP
117
+
118
+ - Complete TCP: full finite state machine and reliable bulk transfer (RFC 9293, RFC 1122)
119
+ - Modern congestion control — CUBIC, NewReno, PRR, HyStart++, ABE, IW10 (RFC 9438, RFC 6582, RFC 6937, RFC 9406, RFC 8511, RFC 6928)
120
+ - Advanced loss recovery — SACK, D-SACK, RACK-TLP, F-RTO, limited transmit (RFC 2018, RFC 2883, RFC 8985, RFC 5682, RFC 3042)
121
+ - RFC-correct RTO with Karn's algorithm and backoff (RFC 6298, RFC 8961)
122
+ - Window Scale, Timestamps, PAWS, MSS and TCP Fast Open (RFC 7323, RFC 6691, RFC 7413)
123
+ - ECN and Accurate ECN (RFC 3168, RFC 9768)
124
+ - Blind-attack and ICMP-attack hardening, randomised ISS and ports, robust TIME-WAIT (RFC 5961, RFC 5927, RFC 6528, RFC 1337, RFC 6191)
125
+ - Keep-alive, zero-window probing, silly-window-syndrome avoidance, Nagle
126
+
127
+ #### DHCPv4 client
128
+
129
+ - Full DHCPv4 client: lease acquisition, RENEW / REBIND / DECLINE (RFC 2131, RFC 1542)
130
+ - Detecting Network Attachment and client-ID handling (RFC 4436, RFC 6842, RFC 4361)
131
+
132
+ ---
133
+
134
+
135
+ ### Principle of operation and the test setup
136
+
137
+ The PyTCP stack depends on a Linux TAP/TUN interface. The TAP interface is a virtual interface that,
138
+ on the network end, can be 'plugged' into existing virtual network infrastructure via either Linux
139
+ bridge or Open vSwitch. On the internal end, the TAP interface can be used like any other NIC by
140
+ programmatically sending and receiving packets to/from it.
141
+
142
+ If you wish to test the PyTCP stack in your local network, I'd suggest creating the following network
143
+ setup that will allow you to connect both the Linux kernel (essentially your Linux OS) and the
144
+ PyTCP stack to your local network at the same time.
145
+
146
+ ```console
147
+ <INTERNET> <---> [ROUTER] <---> (eth0)-[Linux bridge]-(br0) <---> [Linux TCP/IP stack]
148
+ |
149
+ |--(tap7) <---> [PyTCP TCP/IP stack]
150
+ ```
151
+
152
+ After the example program (either client or service) starts the stack, it can communicate with it
153
+ via simplified BSD Sockets like API interface. There is also the possibility of sending packets
154
+ directly by calling one of the internal ```_phtx_*()``` methods on the ```PacketHandler```.
155
+
156
+ ---
157
+
158
+
159
+ ### Cloning PyTCP from the GitHub repository
160
+
161
+ In most cases, PyTCP should be cloned directly from the [GitHub repository](https://github.com/ccie18643/PyTCP),
162
+ as this type of installation provides full development and testing environment.
163
+
164
+ ```shell
165
+ git clone https://github.com/ccie18643/PyTCP
166
+ ```
167
+
168
+ After cloning, we can run one of the included examples:
169
+ - Go to the stack root directory (it is called 'PyTCP').
170
+ - Run the ```sudo make bridge``` command to create the 'br0' bridge if needed.
171
+ - Run the ```sudo make tap7``` command to create the tap7 interface and assign it to the 'br0' bridge.
172
+ - Run the ```make venv``` command to create the virtual environment for development and testing.
173
+ - Run ```. venv/bin/activate``` command to activate the virtual environment.
174
+ - Execute any example, e.g., ```python -m examples.stack``` (see the ```examples/``` directory; pass ```--help``` for options).
175
+ - Hit Ctrl-C to stop it.
176
+
177
+ Stack parameters are configured per run via the ```stack.init(...)``` keyword arguments and the runtime sysctl registry (see ```pytcp/stack/```), not a static config file.
178
+
179
+ ---
180
+
181
+
182
+ ### Installing PyTCP from the PyPi repository
183
+
184
+ PyTCP can also be installed as a regular module from the [PyPi repository](https://pypi.org/project/PyTCP/).
185
+
186
+ ```console
187
+ python -m pip install PyTCP
188
+ ```
189
+
190
+ After installation, please ensure the TAP interface is operational and added to the bridge.
191
+
192
+ ```console
193
+ sudo ip tuntap add name tap7 mode tap
194
+ sudo ip link set dev tap7 up
195
+ sudo ip link add name br0 type bridge
196
+ sudo ip link set dev br0 up
197
+ sudo ip link set dev tap7 master br0
198
+ ```
199
+
200
+ PyTCP is consumed as a library through the ```pytcp.stack``` lifecycle API
201
+ (```stack.init(...)``` → ```stack.start()``` → ```stack.stop()```) and the
202
+ ```pytcp.socket``` Berkeley-sockets-style API. The subsystems run in their own
203
+ threads; after ```start()``` control returns to your code.
204
+
205
+ For a complete, runnable reference — opening the TAP/TUN file descriptor,
206
+ calling ```stack.init(...)```, and driving the stack — see
207
+ [```examples/stack.py```](examples/stack.py) and the other programs in the
208
+ [```examples/```](examples/) directory.
209
+
210
+ ---
211
+
212
+
213
+ ### Examples
214
+
215
+ All output below is captured from a live stack on a Linux `tap7`
216
+ interface bridged to a LAN — PyTCP's own log plus a `tshark` wire
217
+ capture. RFC back-off delays (RFC 5227 ACD, RFC 4862 DAD) are
218
+ visible in the timestamps.
219
+
220
+ Every wire block uses the same columns:
221
+
222
+ ```text
223
+ time(s) PROTO src → dst summary
224
+ ```
225
+
226
+ `src → dst` is the IPv4/IPv6 source → destination; for ARP it is
227
+ the ARP-payload **sender → target**. `—` marks IPv6 ND/MLD frames
228
+ whose link-local/multicast endpoints are named in the summary
229
+ instead (the `boot` capture did not record them as columns).
230
+
231
+ Every example is produced by the bundled `tools/capture` runner
232
+ and is reproducible. With the TAP/bridge up and the venv built —
233
+
234
+ ```bash
235
+ sudo make tap7 && sudo make bridge && make venv
236
+ ```
237
+
238
+ — run any example with the exact command listed under it (loss is
239
+ random, so a `--loss` run differs every time; everything else is
240
+ deterministic). The general form is
241
+ `sudo PYTHONPATH=. venv/bin/python -m tools.capture [GLOBAL OPTS] <scenario>`;
242
+ `python -m tools.capture --help` lists every scenario and option.
243
+
244
+ #### Stack startup — IPv6 SLAAC + DAD, MLDv2, IPv4 ACD
245
+
246
+ **Reproduce:**
247
+
248
+ ```bash
249
+ sudo PYTHONPATH=. venv/bin/python -m tools.capture boot
250
+ ```
251
+
252
+ On start the stack autoconfigures itself: it derives an IPv6
253
+ link-local address and runs Duplicate Address Detection, reports its
254
+ multicast groups via MLDv2, solicits routers, builds a global
255
+ address from the Router Advertisement and DADs that too, then runs
256
+ RFC 5227 conflict detection for its IPv4 address.
257
+
258
+ Stack log:
259
+
260
+ ```text
261
+ 0000.05 | STACK | ICMPv6 ND DAD - Starting process for fe80::7bde:94e9:3254:9daf
262
+ 0001.28 | STACK | ICMPv6 ND DAD - No duplicate address detected for fe80::7bde:94e9:3254:9daf
263
+ 0001.28 | STACK | Successfully claimed IPv6 address fe80::7bde:94e9:3254:9daf/64
264
+ 0001.28 | STACK | Sent out ICMPv6 ND Router Solicitation
265
+ 0001.28 | STACK | ICMPv6 ND DAD - Starting process for 2603:808c:2800:4301:7d08:ba99:95db:c5
266
+ 0002.78 | STACK | Successfully claimed IPv6 address 2603:808c:2800:4301:7d08:ba99:95db:c5/64
267
+ 0006.21 | STACK | Sent out ARP Announcement for 192.168.1.77
268
+ 0008.21 | STACK | Successfully claimed IPv4 address 192.168.1.77
269
+ ```
270
+
271
+ Wire capture (`tshark -i tap7`, rebased to the first frame; IPv6
272
+ ND/MLD endpoints are now real columns, not `—`):
273
+
274
+ ```text
275
+ 0.000 ARP 0.0.0.0 → 192.168.1.77 Who has 192.168.1.77? (ARP Probe)
276
+ 0.177 ICMPv6 :: → ff02::1:ff54:9daf Neighbor Solicitation for fe80::7bde:94e9:3254:9daf (link-local DAD)
277
+ 1.178 ICMPv6 fe80::7bde:94e9:3254:9daf → ff02::16 Multicast Listener Report Message v2
278
+ 1.179 ICMPv6 fe80::7bde:94e9:3254:9daf → ff02::2 Router Solicitation from 02:00:00:77:77:77
279
+ 1.679 ICMPv6 :: → ff02::1:ffdb:c5 Neighbor Solicitation for 2603:808c:2800:4301:7d08:ba99:95db:c5 (SLAAC GUA DAD)
280
+ 6.107 ARP 192.168.1.77 → 192.168.1.77 ARP Announcement for 192.168.1.77
281
+ 6.180 ICMPv6 fe80::7bde:94e9:3254:9daf → fe80::2e0:67ff:fe26:88cb Neighbor Advertisement fe80::7bde:94e9:3254:9daf (sol) is at 02:00:00:77:77:77
282
+ 8.108 ARP 192.168.1.77 → 192.168.1.77 ARP Announcement for 192.168.1.77
283
+ ```
284
+
285
+ #### ARP Probe / Announcement (RFC 5227 Address Conflict Detection)
286
+
287
+ **Reproduce:**
288
+
289
+ ```bash
290
+ sudo PYTHONPATH=. venv/bin/python -m tools.capture arp-acd
291
+ ```
292
+
293
+ The stack defends each configured IPv4 address: it sends three ARP
294
+ **Probes** (sender `0.0.0.0`), and if no host objects, claims the
295
+ address with two ARP **Announcements** (sender = target).
296
+
297
+ Wire capture (`tshark -i tap7 -f arp`):
298
+
299
+ ```text
300
+ 0.00 ARP 0.0.0.0 → 192.168.1.77 ARP Probe — Who has 192.168.1.77?
301
+ 1.83 ARP 0.0.0.0 → 192.168.1.77 ARP Probe — Who has 192.168.1.77?
302
+ 3.38 ARP 0.0.0.0 → 192.168.1.77 ARP Probe — Who has 192.168.1.77?
303
+ 6.44 ARP 192.168.1.77 → 192.168.1.77 ARP Announcement for 192.168.1.77
304
+ 8.45 ARP 192.168.1.77 → 192.168.1.77 ARP Announcement for 192.168.1.77
305
+ ```
306
+
307
+ Probe vs. Announcement, decoded (`tshark -V`):
308
+
309
+ ```text
310
+ ARP Probe Opcode: request Sender IP: 0.0.0.0 Target IP: 192.168.1.77
311
+ ARP Announcement Opcode: request Sender IP: 192.168.1.77 Target IP: 192.168.1.77
312
+ ```
313
+
314
+ #### ARP resolution and ICMP Echo
315
+
316
+ **Reproduce:**
317
+
318
+ ```bash
319
+ sudo PYTHONPATH=. venv/bin/python -m tools.capture ip4-icmp-echo
320
+ ```
321
+
322
+ A host on the segment pings the stack. Having learned the stack's MAC
323
+ from its ARP Announcement, the host sends the Echo Request directly;
324
+ the stack then resolves the *host's* MAC via ARP before replying:
325
+
326
+ Wire capture (`tshark -i tap7`, rebased to the first Echo Request):
327
+
328
+ ```text
329
+ 0.000 ICMP 192.168.1.10 → 192.168.1.77 Echo (ping) request id=0x626e, seq=1, ttl=64
330
+ 0.001 ARP 192.168.1.77 → 192.168.1.10 Who has 192.168.1.10? Tell 192.168.1.77
331
+ 0.001 ARP 192.168.1.10 → 192.168.1.77 192.168.1.10 is at a2:4b:a1:00:92:56
332
+ 0.001 ICMP 192.168.1.77 → 192.168.1.10 Echo (ping) reply id=0x626e, seq=1, ttl=64
333
+ 1.001 ICMP 192.168.1.10 → 192.168.1.77 Echo (ping) request id=0x626e, seq=2, ttl=64
334
+ 1.002 ICMP 192.168.1.77 → 192.168.1.10 Echo (ping) reply id=0x626e, seq=2, ttl=64
335
+ 2.032 ICMP 192.168.1.10 → 192.168.1.77 Echo (ping) request id=0x626e, seq=3, ttl=64
336
+ 2.033 ICMP 192.168.1.77 → 192.168.1.10 Echo (ping) reply id=0x626e, seq=3, ttl=64
337
+ ```
338
+
339
+ From the pinging host:
340
+ `3 packets transmitted, 3 received, 0% packet loss; rtt min/avg/max/mdev = 0.693/0.873/1.185/0.221 ms`.
341
+
342
+ #### ICMPv6 Echo over IPv6 (Neighbor Discovery + ping6)
343
+
344
+ **Reproduce:**
345
+
346
+ ```bash
347
+ sudo PYTHONPATH=. venv/bin/python -m tools.capture ip6-icmp-echo
348
+ ```
349
+
350
+ The IPv6 counterpart: a host on a ULA pings the stack's IPv6
351
+ address. The host sends the Echo Request directly; the stack
352
+ resolves the *host* with ICMPv6 Neighbor Discovery (Neighbor
353
+ Solicitation → Neighbor Advertisement) before replying:
354
+
355
+ Wire capture (`tshark -i tap7`, rebased to the first Echo
356
+ Request; unrelated LAN router/host traffic filtered out):
357
+
358
+ ```text
359
+ 0.000 ICMPv6 fd00:1::1 → fd00:1::77 Echo (ping) request id=0x626f, seq=1, hlim=64
360
+ 0.001 ICMPv6 fd00:1::77 → ff02::1:ff00:1 Neighbor Solicitation for fd00:1::1 (from 02:00:00:77:77:77)
361
+ 0.001 ICMPv6 fd00:1::1 → fd00:1::77 Neighbor Advertisement — fd00:1::1 is at a2:4b:a1:00:92:56
362
+ 0.001 ICMPv6 fd00:1::77 → fd00:1::1 Echo (ping) reply id=0x626f, seq=1, hlim=255
363
+ 1.001 ICMPv6 fd00:1::1 → fd00:1::77 Echo (ping) request id=0x626f, seq=2, hlim=64
364
+ 1.002 ICMPv6 fd00:1::77 → fd00:1::1 Echo (ping) reply id=0x626f, seq=2, hlim=255
365
+ 2.044 ICMPv6 fd00:1::1 → fd00:1::77 Echo (ping) request id=0x626f, seq=3, hlim=64
366
+ 2.045 ICMPv6 fd00:1::77 → fd00:1::1 Echo (ping) reply id=0x626f, seq=3, hlim=255
367
+ ```
368
+
369
+ (`tshark`'s heuristic dissector tags the Echo payload as
370
+ "HiPerConTracer" — a harmless false positive; the frames are plain
371
+ ICMPv6 Echo.)
372
+
373
+ From the pinging host:
374
+ `3 packets transmitted, 3 received, 0% packet loss; rtt min/avg/max/mdev = 0.680/0.882/1.276/0.278 ms`.
375
+
376
+ #### Monkeys over TCP
377
+
378
+ **Reproduce:**
379
+
380
+ ```bash
381
+ sudo PYTHONPATH=. venv/bin/python -m tools.capture ip4-tcp-monkeys
382
+ ```
383
+
384
+ PyTCP ships a matching TCP echo client and service
385
+ (`examples/client__tcp_echo.py` / `examples/service__tcp_echo.py`).
386
+ As a quick end-to-end check the client streams two ASCII-art
387
+ "monkeys" as the payload and the service echoes them back over the
388
+ TCP connection — the original "two monkeys delivered via TCP" demo,
389
+ now reproducible as plain text. Connecting to the service returns
390
+ its banner, then the monkeys make the full round trip through the
391
+ stack's TCP path intact; sending `quit` asks the service to close,
392
+ and PyTCP performs the graceful active close itself:
393
+
394
+ ```text
395
+ $ { printf 'malpi\n'; sleep 3; printf 'quit\n'; } | nc 192.168.1.77 7
396
+ ***CLIENT OPEN / SERVICE OPEN***
397
+ ______AAAA_______________AAAA______
398
+ VVVV VVVV
399
+ (__) (__)
400
+ \ \ / /
401
+ .="=. \ \ / /
402
+ _/.-.-.\_ _ > \ .="=. / <
403
+ ( ( o o ) ) )) > \ / \ / <
404
+ |/ " \| // > \\_o_o_// <
405
+ \'---'/ // > ( (_) ) <
406
+ /`---`\ (( >| |<
407
+ / /_,_\ \ \\ / |\___/| \
408
+ \_\_'__/ \ )) / \_____/ \
409
+ /` /`~\ |// / \
410
+ / / \ / / o \
411
+ ,--`,--'\/\ / ) ___ (
412
+ '-- "--' '--' / / \ \
413
+ ( / \ )
414
+ >< ><
415
+ ///\ /\\\
416
+ ''' '''
417
+ ***CLIENT OPEN, SERVICE CLOSING***
418
+ ```
419
+
420
+ On the wire (`tshark -i tap7`, rebased to the SYN) — the full
421
+ RFC 9293 exchange, handshake through graceful close:
422
+
423
+ ```text
424
+ 0.000 TCP 192.168.1.10 → 192.168.1.77 [SYN] Seq=0 MSS=1460 SACK_PERM WS=1024 TSopt
425
+ 0.002 ARP 192.168.1.77 → 192.168.1.10 Who has 192.168.1.10? Tell 192.168.1.77
426
+ 0.002 ARP 192.168.1.10 → 192.168.1.77 192.168.1.10 is at a2:4b:a1:00:92:56
427
+ 0.002 TCP 192.168.1.77 → 192.168.1.10 [SYN,ACK] Seq=0 Ack=1 MSS=1460 SACK_PERM WS=128 TSopt
428
+ 0.002 TCP 192.168.1.10 → 192.168.1.77 [ACK] Seq=1 Ack=1
429
+ 0.002 TCP 192.168.1.10 → 192.168.1.77 [PSH,ACK] len 6 "malpi\n" (request)
430
+ 0.005 TCP 192.168.1.77 → 192.168.1.10 [ACK] len 1448 banner + monkeys, segment 1 (full MSS)
431
+ 0.005 TCP 192.168.1.10 → 192.168.1.77 [ACK] Ack=1449
432
+ 0.007 TCP 192.168.1.77 → 192.168.1.10 [PSH,ACK] len 146 monkeys, segment 2
433
+ 0.007 TCP 192.168.1.10 → 192.168.1.77 [ACK] Ack=1595
434
+ 2.999 TCP 192.168.1.10 → 192.168.1.77 [PSH,ACK] len 5 "quit\n" (request)
435
+ 3.000 TCP 192.168.1.77 → 192.168.1.10 [PSH,ACK] len 35 "SERVICE CLOSING" banner
436
+ 3.000 TCP 192.168.1.10 → 192.168.1.77 [ACK] Ack=1630
437
+ 3.003 TCP 192.168.1.77 → 192.168.1.10 [FIN,ACK] PyTCP active close
438
+ 3.044 TCP 192.168.1.10 → 192.168.1.77 [ACK] Ack=1631 peer acks the FIN
439
+ 6.000 TCP 192.168.1.10 → 192.168.1.77 [FIN,ACK] peer closes its half
440
+ 6.001 TCP 192.168.1.77 → 192.168.1.10 [ACK] Ack=13 connection fully closed (no RST)
441
+ ```
442
+
443
+ The stack negotiates MSS / SACK-permitted / window-scale /
444
+ timestamps on the handshake, resolves the peer's MAC via ARP
445
+ mid-handshake, segments the echoed monkeys to the MSS, tracks the
446
+ peer's cumulative ACKs, and on `quit` performs the RFC 9293 §3.6
447
+ active close — FIN, peer ACK, peer FIN, FIN ACK — a complete TCP
448
+ connection opened, used, and gracefully torn down entirely by
449
+ pure-Python code.
450
+
451
+ #### Monkeys over TCP — over IPv6
452
+
453
+ **Reproduce:**
454
+
455
+ ```bash
456
+ sudo PYTHONPATH=. venv/bin/python -m tools.capture ip6-tcp-monkeys
457
+ ```
458
+
459
+ The same demo, unchanged, over IPv6 (the service bound to a ULA;
460
+ the host resolves it with ICMPv6 Neighbor Discovery instead of
461
+ ARP). The IPv6 MSS is 1440 (vs 1460 on IPv4 — the 20-byte-larger
462
+ fixed header). Same handshake, echo, and RFC 9293 §3.6 graceful
463
+ close:
464
+
465
+ ```text
466
+ 0.000 ICMPv6 fd00:1::1 → ff02::1:ff00:77 Neighbor Solicitation for fd00:1::77 (from a2:4b:a1:00:92:56)
467
+ 0.001 ICMPv6 fd00:1::77 → fd00:1::1 Neighbor Advertisement — fd00:1::77 is at 02:00:00:77:77:77
468
+ 0.001 TCP fd00:1::1 → fd00:1::77 [SYN] Seq=0 MSS=1440 SACK_PERM WS=1024 TSopt
469
+ 0.003 TCP fd00:1::77 → fd00:1::1 [SYN,ACK] Seq=0 Ack=1 MSS=1440 SACK_PERM WS=128 TSopt
470
+ 0.003 TCP fd00:1::1 → fd00:1::77 [ACK] Seq=1 Ack=1
471
+ 0.003 TCP fd00:1::1 → fd00:1::77 [PSH,ACK] len 6 "malpi\n" (request)
472
+ 0.006 TCP fd00:1::77 → fd00:1::1 [ACK] len 1428 banner + monkeys, segment 1 (full MSS)
473
+ 0.006 TCP fd00:1::1 → fd00:1::77 [ACK] Ack=1429
474
+ 0.008 TCP fd00:1::77 → fd00:1::1 [PSH,ACK] len 166 monkeys, segment 2
475
+ 0.008 TCP fd00:1::1 → fd00:1::77 [ACK] Ack=1595
476
+ 2.999 TCP fd00:1::1 → fd00:1::77 [PSH,ACK] len 5 "quit\n" (request)
477
+ 3.001 TCP fd00:1::77 → fd00:1::1 [PSH,ACK] len 35 "SERVICE CLOSING" banner
478
+ 3.001 TCP fd00:1::1 → fd00:1::77 [ACK] Ack=1630
479
+ 3.005 TCP fd00:1::77 → fd00:1::1 [FIN,ACK] PyTCP active close
480
+ 3.045 TCP fd00:1::1 → fd00:1::77 [ACK] Ack=1631 peer acks the FIN
481
+ 6.001 TCP fd00:1::1 → fd00:1::77 [FIN,ACK] peer closes its half
482
+ 6.002 TCP fd00:1::77 → fd00:1::1 [ACK] Ack=13 connection fully closed (no RST)
483
+ ```
484
+
485
+ (`tshark` labels the port-7 data segments "ECHO" — a heuristic;
486
+ they are plain TCP.)
487
+
488
+ #### Monkeys over UDP — IPv4 fragmentation
489
+
490
+ **Reproduce:**
491
+
492
+ ```bash
493
+ sudo PYTHONPATH=. venv/bin/python -m tools.capture ip4-udp-monkeys
494
+ ```
495
+
496
+ The same ASCII monkeys, echoed over the UDP service. The reply
497
+ (~1.5 KB) exceeds the 1500-byte link MTU, so the stack
498
+ IPv4-fragments it — the classic "IP fragmentation" demo, captured
499
+ for real.
500
+
501
+ ```text
502
+ $ printf 'malpi\n' | nc -u 192.168.1.77 7
503
+ ______AAAA_______________AAAA______
504
+ VVVV VVVV
505
+ (__) (__)
506
+ \ \ / /
507
+ .="=. \ \ / /
508
+ _/.-.-.\_ _ > \ .="=. / <
509
+ ( ( o o ) ) )) > \ / \ / <
510
+ |/ " \| // > \\_o_o_// <
511
+ \'---'/ // > ( (_) ) <
512
+ /`---`\ (( >| |<
513
+ / /_,_\ \ \\ / |\___/| \
514
+ \_\_'__/ \ )) / \_____/ \
515
+ /` /`~\ |// / \
516
+ / / \ / / o \
517
+ ,--`,--'\/\ / ) ___ (
518
+ '-- "--' '--' / / \ \
519
+ ( / \ )
520
+ >< ><
521
+ ///\ /\\\
522
+ ''' '''
523
+ ```
524
+
525
+ On the wire (`tshark -i tap7`, rebased to the request; the
526
+ summary carries the IPv4 fragmentation fields — IP-id, MF,
527
+ frag-offset):
528
+
529
+ ```text
530
+ 0.000 UDP 192.168.1.10 → 192.168.1.77 id=0x9655 MF=0 off=0 UDP "malpi\n" request (14 B)
531
+ 0.001 ARP 192.168.1.77 → 192.168.1.10 Who has 192.168.1.10? Tell 192.168.1.77
532
+ 0.001 ARP 192.168.1.10 → 192.168.1.77 192.168.1.10 is at a2:4b:a1:00:92:56
533
+ 0.001 UDP 192.168.1.77 → 192.168.1.10 id=0x0001 MF=1 off=0 fragment 1 — UDP header + first 1480 B
534
+ 0.002 UDP 192.168.1.77 → 192.168.1.10 id=0x0001 MF=0 off=185 fragment 2 — final 89 B (offset 185×8 = 1480)
535
+ ```
536
+
537
+ The oversized UDP datagram is split into two IPv4 fragments sharing
538
+ one IP id; the peer's kernel reassembles them and `nc -u` prints
539
+ the monkeys. The first datagram is held in the per-neighbour queue
540
+ until the ARP reply resolves the peer's MAC (RFC 1122 §2.3.2.2),
541
+ then both fragments are flushed in order — a fragmented datagram
542
+ delivered to a cold neighbour, lost by neither the DF bit nor a
543
+ single-slot queue.
544
+
545
+ #### Monkeys over UDP — over IPv6
546
+
547
+ **Reproduce:**
548
+
549
+ ```bash
550
+ sudo PYTHONPATH=. venv/bin/python -m tools.capture ip6-udp-monkeys
551
+ ```
552
+
553
+ The same oversized echo over IPv6. IPv6 fragments differently from
554
+ IPv4: the base header is never modified — the source inserts a
555
+ **Fragment extension header** (RFC 8200 §4.5), and only the source
556
+ may fragment. The stack resolves the peer via ICMPv6 Neighbor
557
+ Discovery (NS → NA), then emits the ~1.5 KB reply as two IPv6
558
+ fragments sharing one identification:
559
+
560
+ ```text
561
+ 0.000 UDP fd00:1::1 → fd00:1::77 "malpi\n" request
562
+ 0.001 ICMPv6 fd00:1::77 → ff02::1:ff00:1 Neighbor Solicitation for fd00:1::1 (from 02:00:00:77:77:77)
563
+ 0.001 ICMPv6 fd00:1::1 → fd00:1::77 Neighbor Advertisement — fd00:1::1 is at a2:4b:a1:00:92:56
564
+ 0.002 IPv6 fd00:1::77 → fd00:1::1 Fragment header: off=0 more=1 ident=0xc6713a45 next=UDP (fragment 1)
565
+ 0.002 UDP fd00:1::77 → fd00:1::1 final fragment — reassembles to the 1569-byte datagram
566
+ ```
567
+
568
+ (`tshark` labels the port-7 datagrams "ECHO" — a heuristic; they
569
+ are plain UDP. The 1561-byte reply + 8-byte UDP header = 1569 B,
570
+ over the 1500-byte link MTU, so the stack splits it across the two
571
+ fragments above.)
572
+
573
+ #### Inbound IPv4 reassembly (oversized ping)
574
+
575
+ **Reproduce:**
576
+
577
+ ```bash
578
+ sudo PYTHONPATH=. venv/bin/python -m tools.capture ip4-icmp-frag-rx --count 1
579
+ ```
580
+
581
+ The receive-side counterpart of the fragmentation demos. The host
582
+ sends a 4000-byte `ping`, which its kernel splits into three IPv4
583
+ fragments. The stack **reassembles** them into one Echo Request,
584
+ then replies with a 4000-byte Echo Reply that it **itself
585
+ fragments** into three:
586
+
587
+ ```text
588
+ 0.000 IPv4 192.168.1.10 → 192.168.1.77 id=0xf29f MF=1 off=0 Echo Request — fragment 1/3
589
+ 0.000 IPv4 192.168.1.10 → 192.168.1.77 id=0xf29f MF=1 off=185 fragment 2/3 (off 185×8 = 1480 B)
590
+ 0.000 IPv4 192.168.1.10 → 192.168.1.77 id=0xf29f MF=0 off=370 fragment 3/3 → reassembles to Echo Request id=0x6271, seq=1
591
+ 0.001 ARP 192.168.1.77 → 192.168.1.10 Who has 192.168.1.10? Tell 192.168.1.77
592
+ 0.001 ARP 192.168.1.10 → 192.168.1.77 192.168.1.10 is at a2:4b:a1:00:92:56
593
+ 0.002 IPv4 192.168.1.77 → 192.168.1.10 id=0x0001 MF=1 off=0 Echo Reply — fragment 1/3
594
+ 0.002 IPv4 192.168.1.77 → 192.168.1.10 id=0x0001 MF=1 off=185 fragment 2/3
595
+ 0.002 IPv4 192.168.1.77 → 192.168.1.10 id=0x0001 MF=0 off=370 fragment 3/3 → Echo Reply id=0x6271, seq=1
596
+ ```
597
+
598
+ From the pinging host:
599
+ `1 packets transmitted, 1 received, 0% packet loss; rtt min/avg/max/mdev = 2.048/2.048/2.048/0.000 ms`
600
+ (`4008 bytes from 192.168.1.77` — the full 4000-byte payload made
601
+ the round trip, reassembled on both ends).
602
+
603
+ #### DHCPv4 client lease
604
+
605
+ **Reproduce** (needs a DHCPv4 server reachable on the bridge):
606
+
607
+ ```bash
608
+ sudo PYTHONPATH=. venv/bin/python -m tools.capture ip4-dhcp
609
+ ```
610
+
611
+ With no static IPv4 configured, the stack runs its DHCPv4 client:
612
+ the full DORA exchange (Discover → Offer → Request → ACK), and
613
+ then — because the address is unverified — RFC 5227 Address
614
+ Conflict Detection on the *DHCP-assigned* address before it is
615
+ used. A randomized RFC 2131 initial-desync delay (~6.8 s here)
616
+ precedes the first Discover:
617
+
618
+ ```text
619
+ 0.000 DHCP 0.0.0.0 → 255.255.255.255 DHCP Discover xid 0x3207aee
620
+ 0.000 DHCP 192.168.1.1 → 255.255.255.255 DHCP Offer xid 0x3207aee (offers 192.168.1.145)
621
+ 3.002 DHCP 0.0.0.0 → 255.255.255.255 DHCP Request xid 0x3207aee (requesting 192.168.1.145)
622
+ 3.002 DHCP 192.168.1.1 → 255.255.255.255 DHCP ACK xid 0x3207aee (lease 3600 s)
623
+ 3.810 ARP 0.0.0.0 → 192.168.1.145 ARP Probe — Who has 192.168.1.145? (RFC 5227 ACD on the leased address)
624
+ 5.599 ARP 0.0.0.0 → 192.168.1.145 ARP Probe — Who has 192.168.1.145?
625
+ 6.891 ARP 0.0.0.0 → 192.168.1.145 ARP Probe — Who has 192.168.1.145?
626
+ 10.252 ARP 192.168.1.145 → 192.168.1.145 ARP Announcement for 192.168.1.145
627
+ 12.252 ARP 192.168.1.145 → 192.168.1.145 ARP Announcement for 192.168.1.145
628
+ ```
629
+
630
+ Stack log:
631
+
632
+ ```text
633
+ 0015.05 | DHCP4 | Initial desync delay: 6.83s
634
+ 0021.89 | DHCP4 | TX - DHCPv4 Request ... [message_type Discover ...]
635
+ 0021.89 | DHCP4 | RX - DHCPv4 Reply ... yiaddr 192.168.1.145 ... [message_type Offer, server_id 192.168.1.1 ...]
636
+ 0024.89 | DHCP4 | TX - DHCPv4 Request ... [message_type Request, server_id 192.168.1.1, req_ip_addr 192.168.1.145 ...]
637
+ 0024.89 | DHCP4 | RX - DHCPv4 Reply ... [message_type ACK, lease_time 3600 ...]
638
+ 0032.14 | DHCP4 | Lease acquired: 192.168.1.145/24 (lease_time=3600s, server=192.168.1.1)
639
+ ```
640
+
641
+ #### TCP under packet loss — retransmission & recovery
642
+
643
+ **Reproduce** (asserts the connection still completes — exits
644
+ non-zero if it does not):
645
+
646
+ ```bash
647
+ sudo PYTHONPATH=. venv/bin/python -m tools.capture \
648
+ --loss 20 --expect-wire '\[FIN, ACK\]' ip4-tcp-monkeys
649
+ # … → [PASS] wire: /\[FIN, ACK\]/ , exit 0
650
+ ```
651
+
652
+ Every example above runs on a clean bridge, so the loss-recovery
653
+ machinery never fires. Driven through a `tc netem loss 20%`
654
+ qdisc, the same TCP monkeys exchange has segments dropped in both
655
+ directions — and the stack recovers: it retransmits its own
656
+ segments on RTO, the peer SACKs the holes, and the connection
657
+ still completes and closes cleanly (no RST). One representative
658
+ run (loss is random — every run drops different packets; the
659
+ invariant is that it *completes*), rebased to the SYN:
660
+
661
+ ```text
662
+ 0.000 TCP 192.168.1.10 → 192.168.1.77 [SYN] Seq=0 MSS=1460 SACK_PERM WS=1024
663
+ 0.003 TCP 192.168.1.77 → 192.168.1.10 [SYN,ACK] Seq=0 Ack=1
664
+ 0.003 TCP 192.168.1.10 → 192.168.1.77 [ACK]
665
+ 0.006 TCP 192.168.1.77 → 192.168.1.10 [PSH,ACK] open banner, Len 33
666
+ 0.035 TCP 192.168.1.77 → 192.168.1.10 [PSH,ACK] [TCP Retransmission] Seq=1 Len 33 (banner drop → RTO resend)
667
+ 0.035 TCP 192.168.1.10 → 192.168.1.77 [ACK] [Previous segment not captured] SACK SLE=1 SRE=34
668
+ 0.036 TCP 192.168.1.77 → 192.168.1.10 [ACK] [TCP Dup ACK]
669
+ 0.208 TCP 192.168.1.10 → 192.168.1.77 [PSH,ACK] [TCP Retransmission] "malpi\n" request resent
670
+ 0.211 TCP 192.168.1.77 → 192.168.1.10 [PSH,ACK] monkeys, segment 1
671
+ 0.279 TCP 192.168.1.77 → 192.168.1.10 [PSH,ACK] [TCP Retransmission] Seq=1482 Len 113 monkeys seg 2 resent
672
+ 0.279 TCP 192.168.1.10 → 192.168.1.77 [ACK] SACK SLE=1482 SRE=1595
673
+ 3.210 TCP 192.168.1.10 → 192.168.1.77 [PSH,ACK] "quit\n" request
674
+ 4.001 TCP 192.168.1.77 → 192.168.1.10 [PSH,ACK] [TCP Retransmission] "SERVICE CLOSING" banner resent
675
+ 4.004 TCP 192.168.1.77 → 192.168.1.10 [FIN,ACK] PyTCP active close
676
+ 4.044 TCP 192.168.1.10 → 192.168.1.77 [ACK] peer acks the FIN
677
+ 6.000 TCP 192.168.1.10 → 192.168.1.77 [FIN,ACK] peer closes its half
678
+ 6.001 TCP 192.168.1.77 → 192.168.1.10 [ACK] connection fully closed (no RST)
679
+ ```
680
+
681
+ `--loss` (and `--delay-ms` / `--reorder` / `--duplicate` /
682
+ `--corrupt`) plus the `--expect-log` / `--expect-wire` /
683
+ `--expect-client` assertions are global options that go before
684
+ *any* scenario, so any capture can be turned into a loss /
685
+ latency e2e check.