tunnelscope 0.2.1__tar.gz → 0.2.2__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.
- {tunnelscope-0.2.1/tunnelscope.egg-info → tunnelscope-0.2.2}/PKG-INFO +4 -4
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/README.md +3 -3
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/receiver-observations.md +12 -3
- tunnelscope-0.2.2/docs/releases/v0.2.2.md +19 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/pyproject.toml +1 -1
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/tests/test_delivery.py +44 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2/tunnelscope.egg-info}/PKG-INFO +4 -4
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/tunnelscope.egg-info/SOURCES.txt +1 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/tunnelscope.py +27 -4
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/LICENSE +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/MANIFEST.in +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/bidirectional.md +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/demo.md +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/images/two-vm-results.png +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/releases/v0.2.1.md +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/v0.1-scope.md +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/validation/ack-return-blocked.md +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/validation/mtu-boundary.md +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/validation/pmtu-blackhole.md +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/validation/two-vm-field-test.md +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/validation/two-vm-receiver-correlation.md +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/examples/mtu-boundary.json +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/examples/pmtu-blackhole.json +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/labs/ack_return_blocked.py +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/labs/mtu_boundary.py +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/labs/pmtu_blackhole.py +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/setup.cfg +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/tests/test_fragmentation.py +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/tunnelscope-logo-blue.png +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/tunnelscope.egg-info/dependency_links.txt +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/tunnelscope.egg-info/entry_points.txt +0 -0
- {tunnelscope-0.2.1 → tunnelscope-0.2.2}/tunnelscope.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tunnelscope
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Evidence-based MTU and packet-size diagnostics for Linux networks
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/lazypanic999/tunnelscope
|
|
@@ -22,7 +22,7 @@ Dynamic: license-file
|
|
|
22
22
|
|
|
23
23
|
Evidence-based MTU and packet-size diagnostics for Linux networks.
|
|
24
24
|
|
|
25
|
-
**Status: v0.2.
|
|
25
|
+
**Status: v0.2.2 alpha prototype. UDP delivery probes, Linux IPv4 Don't Fragment mode, per-direction report combination, optional receiver observations and controlled MTU labs are available. Automatic path-MTU discovery and root-cause diagnosis are not implemented.**
|
|
26
26
|
|
|
27
27
|
## The problem
|
|
28
28
|
|
|
@@ -59,8 +59,8 @@ The [two-VM correlation record](https://github.com/lazypanic999/tunnelscope/blob
|
|
|
59
59
|
The [blocked ACK return lab](https://github.com/lazypanic999/tunnelscope/blob/main/docs/validation/ack-return-blocked.md) validates a receiver-observed sender timeout.
|
|
60
60
|
|
|
61
61
|
Use `tunnelscope show report.json` for a short terminal table of a probe,
|
|
62
|
-
bidirectional comparison, correlation, or lab report.
|
|
63
|
-
machine-readable source of evidence.
|
|
62
|
+
receiver observation, bidirectional comparison, correlation, or lab report.
|
|
63
|
+
JSON remains the machine-readable source of evidence.
|
|
64
64
|
|
|
65
65
|
## Install locally
|
|
66
66
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Evidence-based MTU and packet-size diagnostics for Linux networks.
|
|
8
8
|
|
|
9
|
-
**Status: v0.2.
|
|
9
|
+
**Status: v0.2.2 alpha prototype. UDP delivery probes, Linux IPv4 Don't Fragment mode, per-direction report combination, optional receiver observations and controlled MTU labs are available. Automatic path-MTU discovery and root-cause diagnosis are not implemented.**
|
|
10
10
|
|
|
11
11
|
## The problem
|
|
12
12
|
|
|
@@ -43,8 +43,8 @@ The [two-VM correlation record](https://github.com/lazypanic999/tunnelscope/blob
|
|
|
43
43
|
The [blocked ACK return lab](https://github.com/lazypanic999/tunnelscope/blob/main/docs/validation/ack-return-blocked.md) validates a receiver-observed sender timeout.
|
|
44
44
|
|
|
45
45
|
Use `tunnelscope show report.json` for a short terminal table of a probe,
|
|
46
|
-
bidirectional comparison, correlation, or lab report.
|
|
47
|
-
machine-readable source of evidence.
|
|
46
|
+
receiver observation, bidirectional comparison, correlation, or lab report.
|
|
47
|
+
JSON remains the machine-readable source of evidence.
|
|
48
48
|
|
|
49
49
|
## Install locally
|
|
50
50
|
|
|
@@ -12,6 +12,12 @@ Start the receiver on endpoint B and give it an output path:
|
|
|
12
12
|
tunnelscope receive --bind B_IP --output receiver.json
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
After the receiver exits, `tunnelscope show receiver.json` groups its
|
|
16
|
+
observations by run ID and payload size. It shows how many probes arrived and
|
|
17
|
+
how many acknowledgement sends succeeded or failed locally. An empty capture
|
|
18
|
+
still displays its exit reason and a zero count. A successful send call does not
|
|
19
|
+
confirm that the acknowledgement reached the sender.
|
|
20
|
+
|
|
15
21
|
Run the sender on endpoint A:
|
|
16
22
|
|
|
17
23
|
```sh
|
|
@@ -38,9 +44,12 @@ filtered, sent to another endpoint or missed because capture started late.
|
|
|
38
44
|
`unmatched_receiver_observations` counts valid probes seen by the receiver that
|
|
39
45
|
were not present in the sender file. It can reveal a mixed or incorrect pairing.
|
|
40
46
|
|
|
41
|
-
`pairing.run_id_check` is `matched` when the receiver observed this sender
|
|
42
|
-
|
|
43
|
-
run.
|
|
47
|
+
`pairing.run_id_check` is `matched` when the receiver observed only this sender
|
|
48
|
+
run, or `matched_with_other_runs` when the receiver file also contains probes
|
|
49
|
+
from another run. The terminal view shows unmatched receiver observations to
|
|
50
|
+
make mixed captures easier to spot. The command rejects a receiver file
|
|
51
|
+
containing only observations from another run. An empty receiver file cannot
|
|
52
|
+
establish which run it covered and is marked
|
|
44
53
|
`unverified_no_receiver_observations`. `pairing.endpoint_checks` compares the
|
|
45
54
|
target port and, unless the receiver bound to `0.0.0.0`, the target IPv4 address.
|
|
46
55
|
Mismatches need review; address translation can legitimately change them.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# TunnelScope v0.2.2
|
|
2
|
+
|
|
3
|
+
Small maintenance release of the Python command-line prototype.
|
|
4
|
+
|
|
5
|
+
## Changes
|
|
6
|
+
|
|
7
|
+
- `tunnelscope show receiver.json` summarizes receiver observations by run ID
|
|
8
|
+
and payload size, including local acknowledgement send results.
|
|
9
|
+
- Empty receiver reports render without an error.
|
|
10
|
+
- Correlation identifies receiver captures containing the selected sender run
|
|
11
|
+
alongside other runs and shows the number of unmatched observations.
|
|
12
|
+
|
|
13
|
+
## Limits
|
|
14
|
+
|
|
15
|
+
A receiver observation proves arrival at that receiver, while a successful
|
|
16
|
+
acknowledgement send call only proves that its local socket accepted the reply.
|
|
17
|
+
Timeouts remain inconclusive without additional evidence. The tool does not
|
|
18
|
+
discover an exact end-to-end path MTU or prove a root cause on an unknown
|
|
19
|
+
network. Use its unauthenticated protocol only in controlled environments.
|
|
@@ -172,6 +172,14 @@ class DeliveryTests(unittest.TestCase):
|
|
|
172
172
|
self.assertEqual(shown.returncode, 0, shown.stderr)
|
|
173
173
|
self.assertIn("2/2 observed at receiver", shown.stdout)
|
|
174
174
|
self.assertIn("Receiver saw", shown.stdout)
|
|
175
|
+
receiver_shown = subprocess.run(
|
|
176
|
+
[sys.executable, str(Path(tunnelscope.__file__)), "show", str(receiver_path)],
|
|
177
|
+
capture_output=True, text=True, timeout=5,
|
|
178
|
+
)
|
|
179
|
+
self.assertEqual(receiver_shown.returncode, 0, receiver_shown.stderr)
|
|
180
|
+
self.assertIn("Receiver observations: 2 probes", receiver_shown.stdout)
|
|
181
|
+
self.assertIn(probe["run_id"], receiver_shown.stdout)
|
|
182
|
+
self.assertIn("ACK sent", receiver_shown.stdout)
|
|
175
183
|
self.assertIn("64", tunnelscope.format_report(probe))
|
|
176
184
|
finally:
|
|
177
185
|
if process.poll() is None:
|
|
@@ -208,6 +216,42 @@ class DeliveryTests(unittest.TestCase):
|
|
|
208
216
|
result = tunnelscope.correlate_reports(probe, receiver)
|
|
209
217
|
self.assertEqual(result["pairing"]["run_id_check"], "unverified_no_receiver_observations")
|
|
210
218
|
|
|
219
|
+
def test_correlation_flags_receiver_capture_with_other_runs(self):
|
|
220
|
+
probe = {"schema_version": 1, "run_id": "11" * 8, "results": [
|
|
221
|
+
{"probe_id": "ab" * 16, "payload_bytes": 64, "status": "acknowledged"},
|
|
222
|
+
]}
|
|
223
|
+
receiver = {"schema_version": 1, "kind": "receiver_observations", "observations": [
|
|
224
|
+
{"probe_id": "ab" * 16, "run_id": "11" * 8, "payload_bytes": 64},
|
|
225
|
+
{"probe_id": "cd" * 16, "run_id": "22" * 8, "payload_bytes": 1400},
|
|
226
|
+
]}
|
|
227
|
+
result = tunnelscope.correlate_reports(probe, receiver)
|
|
228
|
+
self.assertEqual(result["pairing"]["run_id_check"], "matched_with_other_runs")
|
|
229
|
+
self.assertEqual(result["summary"]["unmatched_receiver_observations"], 1)
|
|
230
|
+
shown = tunnelscope.format_report(result)
|
|
231
|
+
self.assertIn("1 unmatched receiver observations", shown)
|
|
232
|
+
self.assertIn("matched_with_other_runs", shown)
|
|
233
|
+
|
|
234
|
+
receiver["observations"][0]["run_id"] = "22" * 8
|
|
235
|
+
receiver["observations"].append({"probe_id": "ef" * 16, "run_id": "11" * 8,
|
|
236
|
+
"payload_bytes": 64})
|
|
237
|
+
with self.assertRaisesRegex(ValueError, "matching probe ID has a different run ID"):
|
|
238
|
+
tunnelscope.correlate_reports(probe, receiver)
|
|
239
|
+
|
|
240
|
+
def test_receiver_view_separates_runs_and_ack_send_errors(self):
|
|
241
|
+
report = {"schema_version": 1, "kind": "receiver_observations", "exit_reason": "idle_timeout",
|
|
242
|
+
"observations": [
|
|
243
|
+
{"run_id": "11" * 8, "payload_bytes": 64, "ack_send_status": "sent"},
|
|
244
|
+
{"run_id": "11" * 8, "payload_bytes": 64, "ack_send_status": "socket_error"},
|
|
245
|
+
{"run_id": "22" * 8, "payload_bytes": 1400, "ack_send_status": "sent"},
|
|
246
|
+
]}
|
|
247
|
+
rendered = tunnelscope.format_report(report)
|
|
248
|
+
self.assertIn("Receiver observations: 3 probes; exit: idle_timeout", rendered)
|
|
249
|
+
self.assertIn("1111111111111111 64 2 1 1", rendered)
|
|
250
|
+
self.assertIn("2222222222222222 1400 1 1 0", rendered)
|
|
251
|
+
self.assertIn("not that it arrived", rendered)
|
|
252
|
+
report["observations"] = []
|
|
253
|
+
self.assertIn("Receiver observations: 0 probes", tunnelscope.format_report(report))
|
|
254
|
+
|
|
211
255
|
|
|
212
256
|
if __name__ == "__main__":
|
|
213
257
|
unittest.main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tunnelscope
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Evidence-based MTU and packet-size diagnostics for Linux networks
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/lazypanic999/tunnelscope
|
|
@@ -22,7 +22,7 @@ Dynamic: license-file
|
|
|
22
22
|
|
|
23
23
|
Evidence-based MTU and packet-size diagnostics for Linux networks.
|
|
24
24
|
|
|
25
|
-
**Status: v0.2.
|
|
25
|
+
**Status: v0.2.2 alpha prototype. UDP delivery probes, Linux IPv4 Don't Fragment mode, per-direction report combination, optional receiver observations and controlled MTU labs are available. Automatic path-MTU discovery and root-cause diagnosis are not implemented.**
|
|
26
26
|
|
|
27
27
|
## The problem
|
|
28
28
|
|
|
@@ -59,8 +59,8 @@ The [two-VM correlation record](https://github.com/lazypanic999/tunnelscope/blob
|
|
|
59
59
|
The [blocked ACK return lab](https://github.com/lazypanic999/tunnelscope/blob/main/docs/validation/ack-return-blocked.md) validates a receiver-observed sender timeout.
|
|
60
60
|
|
|
61
61
|
Use `tunnelscope show report.json` for a short terminal table of a probe,
|
|
62
|
-
bidirectional comparison, correlation, or lab report.
|
|
63
|
-
machine-readable source of evidence.
|
|
62
|
+
receiver observation, bidirectional comparison, correlation, or lab report.
|
|
63
|
+
JSON remains the machine-readable source of evidence.
|
|
64
64
|
|
|
65
65
|
## Install locally
|
|
66
66
|
|
|
@@ -297,7 +297,7 @@ def correlate_reports(probe_report, receiver_report):
|
|
|
297
297
|
elif run_id not in receiver_run_ids:
|
|
298
298
|
raise ValueError("receiver report has observations from a different run")
|
|
299
299
|
else:
|
|
300
|
-
run_id_check = "matched"
|
|
300
|
+
run_id_check = "matched_with_other_runs" if len(receiver_run_ids) > 1 else "matched"
|
|
301
301
|
observations = {row["probe_id"]: row for row in received}
|
|
302
302
|
sent_ids = {row["probe_id"] for row in sent}
|
|
303
303
|
matched = []
|
|
@@ -305,7 +305,7 @@ def correlate_reports(probe_report, receiver_report):
|
|
|
305
305
|
observation = observations.get(row["probe_id"])
|
|
306
306
|
if observation is not None and observation["payload_bytes"] != row["payload_bytes"]:
|
|
307
307
|
raise ValueError("matching probe IDs have different payload sizes")
|
|
308
|
-
if observation is not None and run_id_check
|
|
308
|
+
if observation is not None and run_id_check in ("matched", "matched_with_other_runs") and observation["run_id"] != run_id:
|
|
309
309
|
raise ValueError("matching probe ID has a different run ID")
|
|
310
310
|
matched.append({
|
|
311
311
|
"probe_id": row["probe_id"],
|
|
@@ -398,7 +398,7 @@ def combine_reports(a_to_b, b_to_a):
|
|
|
398
398
|
|
|
399
399
|
def text_table(headers, rows):
|
|
400
400
|
cells = [[str(value) for value in row] for row in rows]
|
|
401
|
-
widths = [max(len(header), *(len(row[index]) for row in cells)) for index, header in enumerate(headers)]
|
|
401
|
+
widths = [max([len(header), *(len(row[index]) for row in cells)]) for index, header in enumerate(headers)]
|
|
402
402
|
def line(values):
|
|
403
403
|
return " ".join(value.ljust(width) for value, width in zip(values, widths))
|
|
404
404
|
return "\n".join([line(headers), line(["-" * width for width in widths]), *(line(row) for row in cells)])
|
|
@@ -429,6 +429,28 @@ def format_report(report):
|
|
|
429
429
|
return "Probe results (UDP payload bytes)\n" + text_table(
|
|
430
430
|
["Bytes", "Ack", "Too large", "Timeout", "Error", "Assessment"], values
|
|
431
431
|
) + "\nTimeouts are inconclusive; an ACK confirms delivery and a small reply."
|
|
432
|
+
if kind == "receiver_observations":
|
|
433
|
+
observations = report.get("observations")
|
|
434
|
+
if not isinstance(observations, list) or any(not isinstance(row, dict) for row in observations):
|
|
435
|
+
raise ValueError("receiver report has no valid observations")
|
|
436
|
+
counts = {}
|
|
437
|
+
for row in observations:
|
|
438
|
+
run_id, size = row.get("run_id") or "unknown", row.get("payload_bytes")
|
|
439
|
+
if not isinstance(run_id, str) or not isinstance(size, int):
|
|
440
|
+
raise ValueError("invalid receiver observation")
|
|
441
|
+
key = (run_id, size)
|
|
442
|
+
if key not in counts:
|
|
443
|
+
counts[key] = {"observed": 0, "ack_sent": 0, "ack_errors": 0}
|
|
444
|
+
counts[key]["observed"] += 1
|
|
445
|
+
if row.get("ack_send_status") == "sent":
|
|
446
|
+
counts[key]["ack_sent"] += 1
|
|
447
|
+
elif row.get("ack_send_status") == "socket_error":
|
|
448
|
+
counts[key]["ack_errors"] += 1
|
|
449
|
+
values = [(run_id, size, item["observed"], item["ack_sent"], item["ack_errors"])
|
|
450
|
+
for (run_id, size), item in sorted(counts.items())]
|
|
451
|
+
return (f"Receiver observations: {len(observations)} probes; exit: {report.get('exit_reason', 'unknown')}\n"
|
|
452
|
+
+ text_table(["Run ID", "Bytes", "Seen", "ACK sent", "ACK error"], values)
|
|
453
|
+
+ "\nACK sent means the local socket accepted the reply, not that it arrived.")
|
|
432
454
|
if kind == "bidirectional_comparison":
|
|
433
455
|
comparison = report.get("comparison")
|
|
434
456
|
rows = comparison.get("by_payload_size") if isinstance(comparison, dict) else None
|
|
@@ -452,7 +474,8 @@ def format_report(report):
|
|
|
452
474
|
pairing = report.get("pairing")
|
|
453
475
|
run_check = pairing.get("run_id_check", "unavailable") if isinstance(pairing, dict) else "unavailable"
|
|
454
476
|
return (f"Correlation: {summary.get('receiver_observed', '?')}/{summary.get('sent_probes', '?')} observed at receiver; "
|
|
455
|
-
f"{summary.get('timed_out_but_receiver_observed', '?')} timed out after arrival;
|
|
477
|
+
f"{summary.get('timed_out_but_receiver_observed', '?')} timed out after arrival; "
|
|
478
|
+
f"{summary.get('unmatched_receiver_observations', '?')} unmatched receiver observations; run ID: {run_check}\n"
|
|
456
479
|
+ text_table(["Bytes", "Sender", "Receiver saw", "ACK send"], values)
|
|
457
480
|
+ "\nNo receiver observation is inconclusive without confirmed capture coverage.")
|
|
458
481
|
raise ValueError(f"unsupported report kind: {kind}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|