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.
Files changed (32) hide show
  1. {tunnelscope-0.2.1/tunnelscope.egg-info → tunnelscope-0.2.2}/PKG-INFO +4 -4
  2. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/README.md +3 -3
  3. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/receiver-observations.md +12 -3
  4. tunnelscope-0.2.2/docs/releases/v0.2.2.md +19 -0
  5. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/pyproject.toml +1 -1
  6. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/tests/test_delivery.py +44 -0
  7. {tunnelscope-0.2.1 → tunnelscope-0.2.2/tunnelscope.egg-info}/PKG-INFO +4 -4
  8. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/tunnelscope.egg-info/SOURCES.txt +1 -0
  9. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/tunnelscope.py +27 -4
  10. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/LICENSE +0 -0
  11. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/MANIFEST.in +0 -0
  12. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/bidirectional.md +0 -0
  13. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/demo.md +0 -0
  14. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/images/two-vm-results.png +0 -0
  15. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/releases/v0.2.1.md +0 -0
  16. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/v0.1-scope.md +0 -0
  17. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/validation/ack-return-blocked.md +0 -0
  18. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/validation/mtu-boundary.md +0 -0
  19. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/validation/pmtu-blackhole.md +0 -0
  20. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/validation/two-vm-field-test.md +0 -0
  21. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/docs/validation/two-vm-receiver-correlation.md +0 -0
  22. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/examples/mtu-boundary.json +0 -0
  23. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/examples/pmtu-blackhole.json +0 -0
  24. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/labs/ack_return_blocked.py +0 -0
  25. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/labs/mtu_boundary.py +0 -0
  26. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/labs/pmtu_blackhole.py +0 -0
  27. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/setup.cfg +0 -0
  28. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/tests/test_fragmentation.py +0 -0
  29. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/tunnelscope-logo-blue.png +0 -0
  30. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/tunnelscope.egg-info/dependency_links.txt +0 -0
  31. {tunnelscope-0.2.1 → tunnelscope-0.2.2}/tunnelscope.egg-info/entry_points.txt +0 -0
  32. {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.1
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.1 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.**
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. JSON remains the
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.1 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.**
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. JSON remains the
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 run.
42
- The command rejects a receiver file containing only observations from another
43
- run. An empty receiver file cannot establish which run it covered and is marked
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.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tunnelscope"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "Evidence-based MTU and packet-size diagnostics for Linux networks"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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.1
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.1 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.**
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. JSON remains the
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
 
@@ -10,6 +10,7 @@ docs/receiver-observations.md
10
10
  docs/v0.1-scope.md
11
11
  docs/images/two-vm-results.png
12
12
  docs/releases/v0.2.1.md
13
+ docs/releases/v0.2.2.md
13
14
  docs/validation/ack-return-blocked.md
14
15
  docs/validation/mtu-boundary.md
15
16
  docs/validation/pmtu-blackhole.md
@@ -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 == "matched" and observation["run_id"] != run_id:
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; run ID: {run_check}\n"
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