tunnelscope 0.2.2__tar.gz → 0.3.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.
- {tunnelscope-0.2.2/tunnelscope.egg-info → tunnelscope-0.3.0}/PKG-INFO +26 -7
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/README.md +25 -6
- tunnelscope-0.3.0/docs/environments.md +67 -0
- tunnelscope-0.3.0/docs/images/report-ui.png +0 -0
- tunnelscope-0.3.0/docs/ipv6.md +37 -0
- tunnelscope-0.3.0/docs/releases/v0.3.0.md +25 -0
- tunnelscope-0.3.0/docs/report-ui.md +21 -0
- tunnelscope-0.3.0/docs/size-sweep.md +29 -0
- tunnelscope-0.3.0/docs/validation/two-vm-size-sweep.md +44 -0
- tunnelscope-0.3.0/labs/ipv6_boundary.py +102 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/labs/mtu_boundary.py +10 -0
- tunnelscope-0.3.0/labs/vxlan_boundary.py +108 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/pyproject.toml +2 -2
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/tests/test_delivery.py +95 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/tests/test_fragmentation.py +48 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0/tunnelscope.egg-info}/PKG-INFO +26 -7
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/tunnelscope.egg-info/SOURCES.txt +10 -0
- tunnelscope-0.3.0/tunnelscope.egg-info/top_level.txt +2 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/tunnelscope.py +123 -42
- tunnelscope-0.3.0/tunnelscope_view.py +125 -0
- tunnelscope-0.2.2/tunnelscope.egg-info/top_level.txt +0 -1
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/LICENSE +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/MANIFEST.in +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/docs/bidirectional.md +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/docs/demo.md +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/docs/images/two-vm-results.png +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/docs/receiver-observations.md +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/docs/releases/v0.2.1.md +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/docs/releases/v0.2.2.md +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/docs/v0.1-scope.md +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/docs/validation/ack-return-blocked.md +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/docs/validation/mtu-boundary.md +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/docs/validation/pmtu-blackhole.md +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/docs/validation/two-vm-field-test.md +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/docs/validation/two-vm-receiver-correlation.md +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/examples/mtu-boundary.json +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/examples/pmtu-blackhole.json +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/labs/ack_return_blocked.py +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/labs/pmtu_blackhole.py +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/setup.cfg +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/tunnelscope-logo-blue.png +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/tunnelscope.egg-info/dependency_links.txt +0 -0
- {tunnelscope-0.2.2 → tunnelscope-0.3.0}/tunnelscope.egg-info/entry_points.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tunnelscope
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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.
|
|
25
|
+
**Status: v0.3.0 alpha. IPv4/IPv6 UDP delivery probes, Linux no-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
|
|
|
@@ -49,7 +49,7 @@ A failed probe alone will not be labeled as proof of an MTU problem. Reports sep
|
|
|
49
49
|
- [x] **M4 - Observe at the receiver:** record which identified probes reached the cooperating receiver and correlate them with sender outcomes.
|
|
50
50
|
- [x] **M5 - Make evidence usable:** validate receiver-observed timeouts in a controlled lab and provide a concise terminal view of reports.
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
The [tunnel and pod workflows](https://github.com/lazypanic999/tunnelscope/blob/main/docs/environments.md) include a validated local VXLAN overlay and guidance for existing VPNs and Kubernetes pods. The VPN and pod workflows have not yet been validated on a real deployment. 5G user-plane tunnels remain outside the current scope.
|
|
53
53
|
|
|
54
54
|
For a quick walkthrough of both controlled failure modes, see the [two-minute Linux demo](https://github.com/lazypanic999/tunnelscope/blob/main/docs/demo.md).
|
|
55
55
|
The [two-VM validation record](https://github.com/lazypanic999/tunnelscope/blob/main/docs/validation/two-vm-field-test.md) documents a successful bidirectional DF run between Ubuntu and Debian guests.
|
|
@@ -62,6 +62,14 @@ Use `tunnelscope show report.json` for a short terminal table of a probe,
|
|
|
62
62
|
receiver observation, bidirectional comparison, correlation, or lab report.
|
|
63
63
|
JSON remains the machine-readable source of evidence.
|
|
64
64
|
|
|
65
|
+
The CLI can also create a [local HTML report view](https://github.com/lazypanic999/tunnelscope/blob/main/docs/report-ui.md):
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
tunnelscope html report.json --output report.html
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+

|
|
72
|
+
|
|
65
73
|
## Install locally
|
|
66
74
|
|
|
67
75
|
From the repository root, in a Python 3.10+ virtual environment:
|
|
@@ -73,6 +81,17 @@ tunnelscope --help
|
|
|
73
81
|
|
|
74
82
|
The package installs the `tunnelscope` command. It has no third-party runtime dependencies. Published releases can be installed with `python3 -m pip install tunnelscope`. The laboratory scripts still run from a source checkout.
|
|
75
83
|
|
|
84
|
+
The CLI also supports `--sweep START:STOP:STEP` for a bounded
|
|
85
|
+
grid of UDP payload sizes. It includes the stop size even when the step does
|
|
86
|
+
not land on it. For example:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
tunnelscope probe 192.0.2.2 --sweep 64:1400:100 --count 2 --dont-fragment --output sweep.json
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The grid tests at most 16 sizes. The result records observations at those
|
|
93
|
+
sizes; it is not an exact path-MTU measurement. See the [size sweep guide](https://github.com/lazypanic999/tunnelscope/blob/main/docs/size-sweep.md).
|
|
94
|
+
|
|
76
95
|
## Getting started
|
|
77
96
|
|
|
78
97
|
Requires Python 3.10 or newer. No third-party runtime packages are needed. Start with a local experiment in two terminals, from the repository directory. Use `tunnelscope` in place of `python tunnelscope.py` after installing the package.
|
|
@@ -93,7 +112,7 @@ The receiver exits after 100 datagrams or 30 seconds without a datagram. Restart
|
|
|
93
112
|
|
|
94
113
|
Probe sizes are **UDP payload bytes**, including TunnelScope's 24-byte header. They exclude IP and UDP headers. The receiver sends only a small acknowledgement, so this is not a same-sized round-trip test. Reverse endpoint roles to investigate delivery in the other direction.
|
|
95
114
|
|
|
96
|
-
Each probe report includes a `source` snapshot taken from the first connected sender socket. It records the local IPv4 address. On Linux, it also records the selected interface and its link MTU when discoverable, plus `kernel_route_mtu_bytes` when the kernel supplies it. The link MTU and kernel route MTU are different facts; neither is an independently measured end-to-end path MTU. A failed connection may leave `source` as `null`, and optional fields may be absent.
|
|
115
|
+
Each probe report includes a `source` snapshot taken from the first connected sender socket. It records the local IPv4 or IPv6 address. On Linux, it also records the selected interface and its link MTU when discoverable, plus `kernel_route_mtu_bytes` when the kernel supplies it. The link MTU and kernel route MTU are different facts; neither is an independently measured end-to-end path MTU. A failed connection may leave `source` as `null`, and optional fields may be absent.
|
|
97
116
|
|
|
98
117
|
`acknowledged` means that the receiver got that payload and its acknowledgement returned. `timeout` means no matching acknowledgement arrived before the deadline; it does not prove an MTU issue. `socket_error` records a socket error. Without `--dont-fragment`, fragmentation follows OS defaults; the tool does not force it on or off. The CLI exits successfully when it produces a report; inspect per-probe statuses for delivery outcomes.
|
|
99
118
|
|
|
@@ -109,13 +128,13 @@ python tunnelscope.py combine a-to-b.json b-to-a.json --output bidirectional.jso
|
|
|
109
128
|
|
|
110
129
|
The command preserves the two reports and aligns per-size assessments; it does not initiate a remote measurement. See the [two-endpoint workflow](https://github.com/lazypanic999/tunnelscope/blob/main/docs/bidirectional.md) for the receiver and sender steps. `endpoint_check` compares the recorded IPv4 addresses but cannot authenticate endpoints or account for address translation.
|
|
111
130
|
|
|
112
|
-
### Linux
|
|
131
|
+
### Linux no-fragment mode
|
|
113
132
|
|
|
114
133
|
```sh
|
|
115
134
|
python tunnelscope.py probe 192.0.2.2 --sizes 64,1252,1253,1400 --count 1 --dont-fragment --output report.json
|
|
116
135
|
```
|
|
117
136
|
|
|
118
|
-
Use the address of your lab receiver; `192.0.2.2` is an example.
|
|
137
|
+
Use the address of your lab receiver; `192.0.2.2` is an example. For IPv4, this mode sets `IP_MTU_DISCOVER=IP_PMTUDISC_DO` on each sender socket. The kernel sets DF and rejects datagrams above its currently known path MTU. For IPv6, it sets Linux `IPV6_DONTFRAG` to prevent source fragmentation. It does not bypass cached PMTU information or search automatically for a boundary. The flag requires Linux, including WSL2. Other platforms reject it; socket-option errors stop the experiment rather than silently reverting to defaults. See the [IPv6 workflow](https://github.com/lazypanic999/tunnelscope/blob/main/docs/ipv6.md).
|
|
119
138
|
|
|
120
139
|
In DF mode, `EMSGSIZE` is reported as `packet_too_large`, with its numeric `errno`. When available, `kernel_reported_mtu_bytes` contains the connected socket's `IP_MTU` value. That value is kernel knowledge, not an independently measured end-to-end MTU. Timeouts remain inconclusive. The JSON schema version remains 1 with additive fields and the new `packet_too_large` status; consumers must handle it explicitly.
|
|
121
140
|
|
|
@@ -143,7 +162,7 @@ This lab creates a sender, router and receiver in temporary network namespaces.
|
|
|
143
162
|
|
|
144
163
|
The known lab topology explains these timeouts; a timeout on an unknown network remains inconclusive. See the [validation record](https://github.com/lazypanic999/tunnelscope/blob/main/docs/validation/pmtu-blackhole.md) and [captured report](https://github.com/lazypanic999/tunnelscope/blob/main/examples/pmtu-blackhole.json). This lab demonstrates one controlled failure mode; it does not establish automatic black-hole detection.
|
|
145
164
|
|
|
146
|
-
The receiver binds to loopback by default. For your own two-host lab, use `--bind <receiver-lab-
|
|
165
|
+
The receiver binds to IPv4 loopback by default. For your own two-host lab, use `--bind <receiver-lab-address>` and probe that address. An IPv6 literal such as `::1` selects IPv6; `--family ipv6` selects an AAAA record for a hostname. The experimental protocol is unencrypted and unauthenticated; use a controlled lab, not a public service.
|
|
147
166
|
|
|
148
167
|
Run checks:
|
|
149
168
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Evidence-based MTU and packet-size diagnostics for Linux networks.
|
|
8
8
|
|
|
9
|
-
**Status: v0.
|
|
9
|
+
**Status: v0.3.0 alpha. IPv4/IPv6 UDP delivery probes, Linux no-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
|
|
|
@@ -33,7 +33,7 @@ A failed probe alone will not be labeled as proof of an MTU problem. Reports sep
|
|
|
33
33
|
- [x] **M4 - Observe at the receiver:** record which identified probes reached the cooperating receiver and correlate them with sender outcomes.
|
|
34
34
|
- [x] **M5 - Make evidence usable:** validate receiver-observed timeouts in a controlled lab and provide a concise terminal view of reports.
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
The [tunnel and pod workflows](https://github.com/lazypanic999/tunnelscope/blob/main/docs/environments.md) include a validated local VXLAN overlay and guidance for existing VPNs and Kubernetes pods. The VPN and pod workflows have not yet been validated on a real deployment. 5G user-plane tunnels remain outside the current scope.
|
|
37
37
|
|
|
38
38
|
For a quick walkthrough of both controlled failure modes, see the [two-minute Linux demo](https://github.com/lazypanic999/tunnelscope/blob/main/docs/demo.md).
|
|
39
39
|
The [two-VM validation record](https://github.com/lazypanic999/tunnelscope/blob/main/docs/validation/two-vm-field-test.md) documents a successful bidirectional DF run between Ubuntu and Debian guests.
|
|
@@ -46,6 +46,14 @@ Use `tunnelscope show report.json` for a short terminal table of a probe,
|
|
|
46
46
|
receiver observation, bidirectional comparison, correlation, or lab report.
|
|
47
47
|
JSON remains the machine-readable source of evidence.
|
|
48
48
|
|
|
49
|
+
The CLI can also create a [local HTML report view](https://github.com/lazypanic999/tunnelscope/blob/main/docs/report-ui.md):
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
tunnelscope html report.json --output report.html
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+

|
|
56
|
+
|
|
49
57
|
## Install locally
|
|
50
58
|
|
|
51
59
|
From the repository root, in a Python 3.10+ virtual environment:
|
|
@@ -57,6 +65,17 @@ tunnelscope --help
|
|
|
57
65
|
|
|
58
66
|
The package installs the `tunnelscope` command. It has no third-party runtime dependencies. Published releases can be installed with `python3 -m pip install tunnelscope`. The laboratory scripts still run from a source checkout.
|
|
59
67
|
|
|
68
|
+
The CLI also supports `--sweep START:STOP:STEP` for a bounded
|
|
69
|
+
grid of UDP payload sizes. It includes the stop size even when the step does
|
|
70
|
+
not land on it. For example:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
tunnelscope probe 192.0.2.2 --sweep 64:1400:100 --count 2 --dont-fragment --output sweep.json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The grid tests at most 16 sizes. The result records observations at those
|
|
77
|
+
sizes; it is not an exact path-MTU measurement. See the [size sweep guide](https://github.com/lazypanic999/tunnelscope/blob/main/docs/size-sweep.md).
|
|
78
|
+
|
|
60
79
|
## Getting started
|
|
61
80
|
|
|
62
81
|
Requires Python 3.10 or newer. No third-party runtime packages are needed. Start with a local experiment in two terminals, from the repository directory. Use `tunnelscope` in place of `python tunnelscope.py` after installing the package.
|
|
@@ -77,7 +96,7 @@ The receiver exits after 100 datagrams or 30 seconds without a datagram. Restart
|
|
|
77
96
|
|
|
78
97
|
Probe sizes are **UDP payload bytes**, including TunnelScope's 24-byte header. They exclude IP and UDP headers. The receiver sends only a small acknowledgement, so this is not a same-sized round-trip test. Reverse endpoint roles to investigate delivery in the other direction.
|
|
79
98
|
|
|
80
|
-
Each probe report includes a `source` snapshot taken from the first connected sender socket. It records the local IPv4 address. On Linux, it also records the selected interface and its link MTU when discoverable, plus `kernel_route_mtu_bytes` when the kernel supplies it. The link MTU and kernel route MTU are different facts; neither is an independently measured end-to-end path MTU. A failed connection may leave `source` as `null`, and optional fields may be absent.
|
|
99
|
+
Each probe report includes a `source` snapshot taken from the first connected sender socket. It records the local IPv4 or IPv6 address. On Linux, it also records the selected interface and its link MTU when discoverable, plus `kernel_route_mtu_bytes` when the kernel supplies it. The link MTU and kernel route MTU are different facts; neither is an independently measured end-to-end path MTU. A failed connection may leave `source` as `null`, and optional fields may be absent.
|
|
81
100
|
|
|
82
101
|
`acknowledged` means that the receiver got that payload and its acknowledgement returned. `timeout` means no matching acknowledgement arrived before the deadline; it does not prove an MTU issue. `socket_error` records a socket error. Without `--dont-fragment`, fragmentation follows OS defaults; the tool does not force it on or off. The CLI exits successfully when it produces a report; inspect per-probe statuses for delivery outcomes.
|
|
83
102
|
|
|
@@ -93,13 +112,13 @@ python tunnelscope.py combine a-to-b.json b-to-a.json --output bidirectional.jso
|
|
|
93
112
|
|
|
94
113
|
The command preserves the two reports and aligns per-size assessments; it does not initiate a remote measurement. See the [two-endpoint workflow](https://github.com/lazypanic999/tunnelscope/blob/main/docs/bidirectional.md) for the receiver and sender steps. `endpoint_check` compares the recorded IPv4 addresses but cannot authenticate endpoints or account for address translation.
|
|
95
114
|
|
|
96
|
-
### Linux
|
|
115
|
+
### Linux no-fragment mode
|
|
97
116
|
|
|
98
117
|
```sh
|
|
99
118
|
python tunnelscope.py probe 192.0.2.2 --sizes 64,1252,1253,1400 --count 1 --dont-fragment --output report.json
|
|
100
119
|
```
|
|
101
120
|
|
|
102
|
-
Use the address of your lab receiver; `192.0.2.2` is an example.
|
|
121
|
+
Use the address of your lab receiver; `192.0.2.2` is an example. For IPv4, this mode sets `IP_MTU_DISCOVER=IP_PMTUDISC_DO` on each sender socket. The kernel sets DF and rejects datagrams above its currently known path MTU. For IPv6, it sets Linux `IPV6_DONTFRAG` to prevent source fragmentation. It does not bypass cached PMTU information or search automatically for a boundary. The flag requires Linux, including WSL2. Other platforms reject it; socket-option errors stop the experiment rather than silently reverting to defaults. See the [IPv6 workflow](https://github.com/lazypanic999/tunnelscope/blob/main/docs/ipv6.md).
|
|
103
122
|
|
|
104
123
|
In DF mode, `EMSGSIZE` is reported as `packet_too_large`, with its numeric `errno`. When available, `kernel_reported_mtu_bytes` contains the connected socket's `IP_MTU` value. That value is kernel knowledge, not an independently measured end-to-end MTU. Timeouts remain inconclusive. The JSON schema version remains 1 with additive fields and the new `packet_too_large` status; consumers must handle it explicitly.
|
|
105
124
|
|
|
@@ -127,7 +146,7 @@ This lab creates a sender, router and receiver in temporary network namespaces.
|
|
|
127
146
|
|
|
128
147
|
The known lab topology explains these timeouts; a timeout on an unknown network remains inconclusive. See the [validation record](https://github.com/lazypanic999/tunnelscope/blob/main/docs/validation/pmtu-blackhole.md) and [captured report](https://github.com/lazypanic999/tunnelscope/blob/main/examples/pmtu-blackhole.json). This lab demonstrates one controlled failure mode; it does not establish automatic black-hole detection.
|
|
129
148
|
|
|
130
|
-
The receiver binds to loopback by default. For your own two-host lab, use `--bind <receiver-lab-
|
|
149
|
+
The receiver binds to IPv4 loopback by default. For your own two-host lab, use `--bind <receiver-lab-address>` and probe that address. An IPv6 literal such as `::1` selects IPv6; `--family ipv6` selects an AAAA record for a hostname. The experimental protocol is unencrypted and unauthenticated; use a controlled lab, not a public service.
|
|
131
150
|
|
|
132
151
|
Run checks:
|
|
133
152
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Controlled tunnel and pod workflows
|
|
2
|
+
|
|
3
|
+
TunnelScope sends ordinary UDP payloads between a cooperating sender and
|
|
4
|
+
receiver. The tool does not configure a VPN, container network, Kubernetes
|
|
5
|
+
cluster or network policy. Run it only where both endpoints and the UDP path
|
|
6
|
+
are under your control. Use the tunnel or pod address as the destination, and
|
|
7
|
+
measure each direction separately.
|
|
8
|
+
|
|
9
|
+
## VXLAN overlay lab
|
|
10
|
+
|
|
11
|
+
On Linux with root and `iproute2`:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
sudo python3 labs/vxlan_boundary.py > vxlan-boundary.json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The lab creates two temporary network namespaces. Their 1500-byte underlay
|
|
18
|
+
carries a VXLAN overlay with a 1450-byte interface MTU. In IPv4 DF mode, the
|
|
19
|
+
known maximum UDP payload on that overlay is 1422 bytes (1450 minus 20-byte
|
|
20
|
+
IPv4 and 8-byte UDP headers). The lab checks 1418–1426 bytes in both
|
|
21
|
+
directions, correlates receiver observations and removes its namespaces on
|
|
22
|
+
ordinary completion. It changes no host interface or route.
|
|
23
|
+
|
|
24
|
+
This validates an encapsulated virtual path, not a real VPN or a Kubernetes
|
|
25
|
+
cluster. A measured local tunnel-interface MTU and a kernel route MTU remain
|
|
26
|
+
different from independent proof of an unknown end-to-end path MTU.
|
|
27
|
+
|
|
28
|
+
## Existing VPN or tunnel
|
|
29
|
+
|
|
30
|
+
Use the addresses assigned inside the tunnel. Start the receiver on endpoint
|
|
31
|
+
B, then probe from endpoint A:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
tunnelscope receive --bind B_TUNNEL_IP --output b-receiver.json
|
|
35
|
+
tunnelscope probe B_TUNNEL_IP --sweep 1200:1400:25 --count 2 --dont-fragment --output a-to-b.json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Run the commands in separate terminals. Repeat with A and B swapped, then use
|
|
39
|
+
`combine` for direction comparison and `correlate` with each receiver report.
|
|
40
|
+
Check the selected `source.interface` and its MTU in the probe report. If the
|
|
41
|
+
route unexpectedly uses a non-tunnel interface, the experiment did not measure
|
|
42
|
+
the intended VPN path. Use `--family ipv6` for an IPv6 hostname or an IPv6
|
|
43
|
+
literal address. Timeouts remain inconclusive without receiver or path
|
|
44
|
+
evidence.
|
|
45
|
+
|
|
46
|
+
## Existing Kubernetes pods
|
|
47
|
+
|
|
48
|
+
If two controlled pods already contain TunnelScope v0.3.0 and Python 3.10+,
|
|
49
|
+
use their pod addresses directly. In separate local terminals, for example:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
kubectl exec -n NAMESPACE POD_B -- tunnelscope receive --bind POD_B_IP --output /tmp/receiver.json
|
|
53
|
+
kubectl exec -n NAMESPACE POD_A -- tunnelscope probe POD_B_IP --sizes 64,1200,1400 --count 2 --dont-fragment --output /tmp/probe.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Collect the JSON files from the pods and correlate them. Repeat with the roles
|
|
57
|
+
reversed. A pod may have more than one container; add `-c CONTAINER` to
|
|
58
|
+
`kubectl exec` when needed. Ensure that the intended UDP port (default 9471)
|
|
59
|
+
and the small ACK return traffic are allowed by the cluster's network policy.
|
|
60
|
+
Kubernetes [pod networking](https://kubernetes.io/docs/concepts/services-networking/)
|
|
61
|
+
and [NetworkPolicy](https://kubernetes.io/docs/concepts/services-networking/network-policies/)
|
|
62
|
+
are implemented by the cluster's network plugin; behavior can differ between
|
|
63
|
+
clusters. See the official [`kubectl exec` reference](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_exec/).
|
|
64
|
+
|
|
65
|
+
This pod recipe is untested here because no Kubernetes cluster was available.
|
|
66
|
+
It does not claim cluster-wide MTU discovery, support for every CNI, or the
|
|
67
|
+
ability to distinguish overlay loss from filtering on its own.
|
|
Binary file
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# IPv6 delivery experiments
|
|
2
|
+
|
|
3
|
+
From v0.3.0, the CLI supports IPv6 UDP payload probes and receiver observations.
|
|
4
|
+
Use an IPv6 literal for `probe` or `receive --bind`; the default receiver still
|
|
5
|
+
binds to IPv4 loopback. For a hostname, `--family ipv6` selects an AAAA record.
|
|
6
|
+
The default hostname behavior continues to prefer IPv4 for compatibility.
|
|
7
|
+
|
|
8
|
+
On one machine, start the receiver in one terminal:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
tunnelscope receive --bind ::1 --output receiver-v6.json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
In another terminal:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
tunnelscope probe ::1 --sizes 64,1200,1400 --count 2 --dont-fragment --output probe-v6.json
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
For a two-host test, use the receiver's reachable IPv6 address and repeat the
|
|
21
|
+
procedure with endpoint roles reversed. Link-local addresses need a zone
|
|
22
|
+
identifier such as `%eth0` on the sender. Reports use `source.ipv6`,
|
|
23
|
+
`target.ipv6`, `receiver.bind_ipv6` and `source_ipv6` in receiver observations.
|
|
24
|
+
The existing report schema version remains 1 with additive IPv6 fields.
|
|
25
|
+
|
|
26
|
+
On Linux, `--dont-fragment` sets `IPV6_DONTFRAG` on the sender socket. An
|
|
27
|
+
`EMSGSIZE` error is recorded as `packet_too_large`; a reported `IPV6_MTU`
|
|
28
|
+
value is the kernel's current route knowledge, not a separately measured
|
|
29
|
+
end-to-end path MTU. Without the flag, source fragmentation follows OS
|
|
30
|
+
defaults. Timeouts remain inconclusive. The receiver's acknowledgement is
|
|
31
|
+
small, so reverse roles to test the opposite direction with the same payload
|
|
32
|
+
size.
|
|
33
|
+
|
|
34
|
+
The [isolated IPv6 boundary lab](../labs/ipv6_boundary.py) verifies a
|
|
35
|
+
1280-byte veth link: 1228–1232-byte UDP payloads arrive, while 1233–1236-byte
|
|
36
|
+
payloads are rejected locally in no-fragment mode. It creates and removes
|
|
37
|
+
temporary network namespaces and requires root and `iproute2`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# TunnelScope v0.3.0
|
|
2
|
+
|
|
3
|
+
Alpha release of the Python CLI with more controlled network experiments and
|
|
4
|
+
a local report view.
|
|
5
|
+
|
|
6
|
+
## Highlights
|
|
7
|
+
|
|
8
|
+
- IPv6 UDP probes, cooperating receiver reports, run correlation and
|
|
9
|
+
bidirectional comparisons. Linux `--dont-fragment` uses `IPV6_DONTFRAG` for
|
|
10
|
+
IPv6 and retains IPv4 DF behavior.
|
|
11
|
+
- Bounded `--sweep START:STOP:STEP` payload-size grids (at most 16 sizes).
|
|
12
|
+
- Reproducible network namespace labs for a known IPv6 link boundary and a
|
|
13
|
+
VXLAN overlay boundary, both checked in each direction.
|
|
14
|
+
- `tunnelscope html report.json --output report.html` creates a searchable
|
|
15
|
+
local report view. The report content remains in the generated HTML.
|
|
16
|
+
- VPN and Kubernetes pod workflow guidance. Those two deployment types are
|
|
17
|
+
documented but have not been validated on a real deployment.
|
|
18
|
+
|
|
19
|
+
## Known limits
|
|
20
|
+
|
|
21
|
+
The tool does not discover an exact end-to-end path MTU or prove a root cause
|
|
22
|
+
on an unknown network. A timeout remains inconclusive without additional
|
|
23
|
+
receiver or path evidence. The UDP protocol is unauthenticated and unencrypted;
|
|
24
|
+
use it only in a controlled environment. The HTML view fetches the project
|
|
25
|
+
logo from GitHub when online, but does not send the report to a service.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Local HTML report view
|
|
2
|
+
|
|
3
|
+
From v0.3.0, the CLI can turn an existing JSON report into a searchable HTML
|
|
4
|
+
file. It does not start a server or send the report to a remote service.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
tunnelscope html probe.json --output probe.html
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Open `probe.html` in a browser. Search filters visible rows; the raw JSON is
|
|
11
|
+
available in a collapsed section. The view supports probe, receiver,
|
|
12
|
+
correlation, bidirectional comparison and lab reports. It presents evidence
|
|
13
|
+
already in the JSON; it does not run a new measurement or infer a root cause.
|
|
14
|
+
The logo is fetched from the project's GitHub image when online, while report
|
|
15
|
+
content works without network access.
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
The generated HTML contains the complete report, including addresses and any
|
|
20
|
+
other fields in its JSON. Sanitize the JSON before sharing the HTML. Report
|
|
21
|
+
values are HTML-escaped before rendering.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Bounded payload-size sweep
|
|
2
|
+
|
|
3
|
+
From v0.3.0, `probe --sweep START:STOP:STEP` expands
|
|
4
|
+
an ascending grid of UDP payload sizes, always including `STOP` even if `STEP`
|
|
5
|
+
does not land exactly on it. The bounds are 32 to 8192 bytes and at most 16
|
|
6
|
+
sizes. `--sweep` and `--sizes` cannot be used together. `--count` repeats each
|
|
7
|
+
size; existing per-size status counts and individual results remain in JSON.
|
|
8
|
+
|
|
9
|
+
On a controlled Linux IPv4 link, start the receiver first and run:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
tunnelscope receive --bind RECEIVER_IP --output receiver.json
|
|
13
|
+
tunnelscope probe RECEIVER_IP --sweep 1200:1400:25 --count 2 --dont-fragment --output sweep.json
|
|
14
|
+
tunnelscope show sweep.json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Run the receiver and sender commands on separate terminals or hosts. The
|
|
18
|
+
receiver's small acknowledgement does not test a same-sized return packet.
|
|
19
|
+
Repeat with the endpoint roles reversed to test the other direction.
|
|
20
|
+
|
|
21
|
+
A sweep samples only the listed sizes. A successful size is an observed working
|
|
22
|
+
UDP payload size under these conditions, not the exact path MTU. A
|
|
23
|
+
`packet_too_large` result reflects the sender kernel's DF rejection and
|
|
24
|
+
current route knowledge. A timeout is inconclusive even if nearby smaller
|
|
25
|
+
sizes succeeded. Compare receiver observations with `correlate` to learn
|
|
26
|
+
whether a timed-out probe reached the cooperating receiver.
|
|
27
|
+
|
|
28
|
+
The [direct-link MTU lab](../labs/mtu_boundary.py) checks this option against a
|
|
29
|
+
known 1280-byte virtual link in both directions.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Two-VM payload-size sweep validation
|
|
2
|
+
|
|
3
|
+
Recorded on 2026-10-28 using separate Ubuntu 24.04 and Debian 12 VirtualBox
|
|
4
|
+
guests connected through a temporary 1500-byte host-only link. Ubuntu ran
|
|
5
|
+
Python 3.12.3 and Debian ran Python 3.11.2. Both guests ran the same
|
|
6
|
+
`tunnelscope.py` file (SHA-256
|
|
7
|
+
`43b0cb76dfa6b58452b9ecc844a5db27c2d057cb7cad4c3735233f75d454a857`).
|
|
8
|
+
The temporary host-only addresses and raw JSON reports are kept locally in
|
|
9
|
+
ignored `output/field-test-2026-10-28/`; they are not committed.
|
|
10
|
+
|
|
11
|
+
## Procedure
|
|
12
|
+
|
|
13
|
+
The destination ran `receive --max-packets 5 --idle-timeout 20 --output receiver.json`.
|
|
14
|
+
The source then ran the development CLI in Linux IPv4 DF mode:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
python3 tunnelscope.py probe PEER_IP --sweep 1468:1476:1 --count 1 --timeout 2 --dont-fragment --output probe.json
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The two files were correlated after each direction. The same procedure was
|
|
21
|
+
repeated with guest roles reversed. The direct-link expected UDP payload
|
|
22
|
+
boundary is 1472 bytes: 1500-byte link MTU minus 20-byte IPv4 and 8-byte UDP
|
|
23
|
+
headers.
|
|
24
|
+
|
|
25
|
+
## Results
|
|
26
|
+
|
|
27
|
+
| Direction | 1468–1472 bytes | 1473–1476 bytes | Receiver observed | Run ID |
|
|
28
|
+
| --- | --- | --- | ---: | --- |
|
|
29
|
+
| Ubuntu to Debian | 5/5 acknowledged | 4/4 `packet_too_large` | 5/5 | matched |
|
|
30
|
+
| Debian to Ubuntu | 5/5 acknowledged | 4/4 `packet_too_large` | 5/5 | matched |
|
|
31
|
+
|
|
32
|
+
Each sender identified its host-only interface with MTU 1500. All eight
|
|
33
|
+
oversized attempts reported kernel MTU 1500. Both receiver reports exited at
|
|
34
|
+
`max_packets`; the correlations had zero unmatched observations and no
|
|
35
|
+
receiver-observed timeouts. The source file was checked by SHA-256 on both
|
|
36
|
+
guests before testing.
|
|
37
|
+
|
|
38
|
+
## Limits
|
|
39
|
+
|
|
40
|
+
This confirms the size grid and known boundary on one directly connected
|
|
41
|
+
virtual link. It does not discover an unknown multi-hop path MTU. The
|
|
42
|
+
`packet_too_large` statuses are local kernel rejections, and the successful
|
|
43
|
+
ACKs only confirm forward payload delivery plus a small reply. The same
|
|
44
|
+
experiment on an unknown path can produce timeouts for several reasons.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Validate an IPv6 no-fragment payload boundary on an isolated 1280-byte link."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import selectors
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
14
|
+
import tunnelscope
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def ip(*args):
|
|
18
|
+
return subprocess.run(["ip", *args], check=True, capture_output=True, text=True, timeout=10)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main():
|
|
22
|
+
if not sys.platform.startswith("linux") or os.geteuid() != 0 or not shutil.which("ip"):
|
|
23
|
+
raise RuntimeError("This lab requires root on Linux and the iproute2 'ip' command")
|
|
24
|
+
suffix = uuid.uuid4().hex[:10]
|
|
25
|
+
left, right = f"ts6-{suffix}-a", f"ts6-{suffix}-b"
|
|
26
|
+
created = []
|
|
27
|
+
script = str(Path(tunnelscope.__file__).resolve())
|
|
28
|
+
report = {"lab": "direct-veth-ipv6-no-fragment", "link_mtu_bytes": 1280,
|
|
29
|
+
"ipv6_header_bytes": 40, "udp_header_bytes": 8,
|
|
30
|
+
"expected_payload_boundary_bytes": 1232, "directions": []}
|
|
31
|
+
try:
|
|
32
|
+
for namespace in (left, right):
|
|
33
|
+
ip("netns", "add", namespace)
|
|
34
|
+
created.append(namespace)
|
|
35
|
+
ip("-n", left, "link", "add", "ts6-a", "type", "veth", "peer", "name", "ts6-b")
|
|
36
|
+
ip("-n", left, "link", "set", "ts6-b", "netns", right)
|
|
37
|
+
for namespace, interface, address in ((left, "ts6-a", "fd00:5453::1/64"),
|
|
38
|
+
(right, "ts6-b", "fd00:5453::2/64")):
|
|
39
|
+
ip("-n", namespace, "link", "set", "lo", "up")
|
|
40
|
+
ip("-n", namespace, "link", "set", interface, "mtu", "1280", "up")
|
|
41
|
+
ip("-n", namespace, "-6", "addr", "add", address, "dev", interface, "nodad")
|
|
42
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
43
|
+
for label, source, target, target_ip in (
|
|
44
|
+
("a-to-b", left, right, "fd00:5453::2"),
|
|
45
|
+
("b-to-a", right, left, "fd00:5453::1"),
|
|
46
|
+
):
|
|
47
|
+
receiver_path = Path(directory) / f"{label}-receiver.json"
|
|
48
|
+
receiver = subprocess.Popen(
|
|
49
|
+
["ip", "netns", "exec", target, sys.executable, "-u", script,
|
|
50
|
+
"receive", "--bind", target_ip, "--max-packets", "5",
|
|
51
|
+
"--idle-timeout", "10", "--output", str(receiver_path)],
|
|
52
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
|
53
|
+
)
|
|
54
|
+
try:
|
|
55
|
+
with selectors.DefaultSelector() as ready:
|
|
56
|
+
ready.register(receiver.stdout, selectors.EVENT_READ)
|
|
57
|
+
if not ready.select(timeout=5):
|
|
58
|
+
raise RuntimeError("IPv6 receiver startup timed out")
|
|
59
|
+
if not receiver.stdout.readline().startswith("Listening on"):
|
|
60
|
+
raise RuntimeError("IPv6 receiver did not start")
|
|
61
|
+
sent = subprocess.run(
|
|
62
|
+
["ip", "netns", "exec", source, sys.executable, script,
|
|
63
|
+
"probe", target_ip, "--sweep", "1228:1236:1",
|
|
64
|
+
"--count", "1", "--timeout", "2", "--dont-fragment"],
|
|
65
|
+
check=True, capture_output=True, text=True, timeout=25,
|
|
66
|
+
)
|
|
67
|
+
probe = json.loads(sent.stdout)
|
|
68
|
+
if receiver.wait(timeout=12) != 0:
|
|
69
|
+
raise RuntimeError("IPv6 receiver failed")
|
|
70
|
+
observed = json.loads(receiver_path.read_text(encoding="utf-8"))
|
|
71
|
+
correlation = tunnelscope.correlate_reports(probe, observed)
|
|
72
|
+
expected = ["acknowledged"] * 5 + ["packet_too_large"] * 4
|
|
73
|
+
if [row["status"] for row in probe["results"]] != expected:
|
|
74
|
+
raise RuntimeError(f"Unexpected IPv6 boundary: {probe}")
|
|
75
|
+
if probe["source"].get("interface_mtu_bytes") != 1280:
|
|
76
|
+
raise RuntimeError(f"Unexpected IPv6 source interface: {probe['source']}")
|
|
77
|
+
if correlation["summary"]["receiver_observed"] != 5:
|
|
78
|
+
raise RuntimeError("Unexpected IPv6 receiver observation count")
|
|
79
|
+
report["directions"].append({"direction": label, "probe": probe,
|
|
80
|
+
"receiver": observed, "correlation": correlation})
|
|
81
|
+
finally:
|
|
82
|
+
if receiver.poll() is None:
|
|
83
|
+
receiver.terminate()
|
|
84
|
+
try:
|
|
85
|
+
receiver.wait(timeout=3)
|
|
86
|
+
except subprocess.TimeoutExpired:
|
|
87
|
+
receiver.kill()
|
|
88
|
+
receiver.wait(timeout=3)
|
|
89
|
+
receiver.stdout.close()
|
|
90
|
+
receiver.stderr.close()
|
|
91
|
+
finally:
|
|
92
|
+
for namespace in reversed(created):
|
|
93
|
+
ip("netns", "delete", namespace)
|
|
94
|
+
print(json.dumps(report, indent=2))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
try:
|
|
99
|
+
main()
|
|
100
|
+
except (RuntimeError, OSError, subprocess.SubprocessError) as error:
|
|
101
|
+
print(f"Lab failed: {error}", file=sys.stderr)
|
|
102
|
+
sys.exit(1)
|
|
@@ -55,6 +55,16 @@ def main():
|
|
|
55
55
|
for row in direction["results"][2:]:
|
|
56
56
|
if row.get("kernel_reported_mtu_bytes") != 1280:
|
|
57
57
|
raise RuntimeError(f"Expected kernel MTU 1280: {row}")
|
|
58
|
+
sweep_result = subprocess.run(
|
|
59
|
+
["ip", "netns", "exec", source, sys.executable, script, "probe", target_ip,
|
|
60
|
+
"--sweep", "1200:1300:25", "--count", "1", "--dont-fragment"],
|
|
61
|
+
check=True, capture_output=True, text=True, timeout=15,
|
|
62
|
+
)
|
|
63
|
+
sweep = json.loads(sweep_result.stdout)
|
|
64
|
+
expected_sweep = ["acknowledged"] * 3 + ["packet_too_large"] * 2
|
|
65
|
+
if [row["status"] for row in sweep["results"]] != expected_sweep:
|
|
66
|
+
raise RuntimeError(f"Unexpected sweep result: {sweep}")
|
|
67
|
+
direction["sweep"] = sweep
|
|
58
68
|
report["directions"].append(direction)
|
|
59
69
|
finally:
|
|
60
70
|
if receiver.poll() is None:
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Validate an IPv4 UDP payload boundary across an isolated VXLAN overlay."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import selectors
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
14
|
+
import tunnelscope
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def ip(*args):
|
|
18
|
+
return subprocess.run(["ip", *args], check=True, capture_output=True, text=True, timeout=10)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main():
|
|
22
|
+
if not sys.platform.startswith("linux") or os.geteuid() != 0 or not shutil.which("ip"):
|
|
23
|
+
raise RuntimeError("This lab requires root on Linux and the iproute2 'ip' command")
|
|
24
|
+
suffix = uuid.uuid4().hex[:10]
|
|
25
|
+
left, right = f"ts-vx-{suffix}-a", f"ts-vx-{suffix}-b"
|
|
26
|
+
created = []
|
|
27
|
+
script = str(Path(tunnelscope.__file__).resolve())
|
|
28
|
+
report = {"lab": "vxlan-overlay-ipv4-df", "underlay_mtu_bytes": 1500,
|
|
29
|
+
"overlay_mtu_bytes": 1450, "expected_payload_boundary_bytes": 1422,
|
|
30
|
+
"directions": []}
|
|
31
|
+
try:
|
|
32
|
+
for namespace in (left, right):
|
|
33
|
+
ip("netns", "add", namespace)
|
|
34
|
+
created.append(namespace)
|
|
35
|
+
ip("-n", left, "link", "add", "ts-under-a", "type", "veth", "peer", "name", "ts-under-b")
|
|
36
|
+
ip("-n", left, "link", "set", "ts-under-b", "netns", right)
|
|
37
|
+
for namespace, underlay, local, remote, overlay in (
|
|
38
|
+
(left, "ts-under-a", "192.0.2.1", "192.0.2.2", "198.51.100.1"),
|
|
39
|
+
(right, "ts-under-b", "192.0.2.2", "192.0.2.1", "198.51.100.2"),
|
|
40
|
+
):
|
|
41
|
+
ip("-n", namespace, "link", "set", "lo", "up")
|
|
42
|
+
ip("-n", namespace, "addr", "add", local + "/30", "dev", underlay)
|
|
43
|
+
ip("-n", namespace, "link", "set", underlay, "mtu", "1500", "up")
|
|
44
|
+
ip("-n", namespace, "link", "add", "ts-vx", "type", "vxlan", "id", "42",
|
|
45
|
+
"local", local, "remote", remote, "dev", underlay, "dstport", "4789", "nolearning")
|
|
46
|
+
ip("-n", namespace, "addr", "add", overlay + "/24", "dev", "ts-vx")
|
|
47
|
+
ip("-n", namespace, "link", "set", "ts-vx", "mtu", "1450", "up")
|
|
48
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
49
|
+
for label, source, target, target_ip in (
|
|
50
|
+
("a-to-b", left, right, "198.51.100.2"),
|
|
51
|
+
("b-to-a", right, left, "198.51.100.1"),
|
|
52
|
+
):
|
|
53
|
+
receiver_path = Path(directory) / f"{label}-receiver.json"
|
|
54
|
+
receiver = subprocess.Popen(
|
|
55
|
+
["ip", "netns", "exec", target, sys.executable, "-u", script,
|
|
56
|
+
"receive", "--bind", target_ip, "--max-packets", "5",
|
|
57
|
+
"--idle-timeout", "10", "--output", str(receiver_path)],
|
|
58
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
|
59
|
+
)
|
|
60
|
+
try:
|
|
61
|
+
with selectors.DefaultSelector() as ready:
|
|
62
|
+
ready.register(receiver.stdout, selectors.EVENT_READ)
|
|
63
|
+
if not ready.select(timeout=5):
|
|
64
|
+
raise RuntimeError("VXLAN receiver startup timed out")
|
|
65
|
+
if not receiver.stdout.readline().startswith("Listening on"):
|
|
66
|
+
raise RuntimeError("VXLAN receiver did not start")
|
|
67
|
+
sent = subprocess.run(
|
|
68
|
+
["ip", "netns", "exec", source, sys.executable, script,
|
|
69
|
+
"probe", target_ip, "--sweep", "1418:1426:1",
|
|
70
|
+
"--count", "1", "--timeout", "2", "--dont-fragment"],
|
|
71
|
+
check=True, capture_output=True, text=True, timeout=25,
|
|
72
|
+
)
|
|
73
|
+
probe = json.loads(sent.stdout)
|
|
74
|
+
if receiver.wait(timeout=12) != 0:
|
|
75
|
+
raise RuntimeError("VXLAN receiver failed")
|
|
76
|
+
observed = json.loads(receiver_path.read_text(encoding="utf-8"))
|
|
77
|
+
correlation = tunnelscope.correlate_reports(probe, observed)
|
|
78
|
+
expected = ["acknowledged"] * 5 + ["packet_too_large"] * 4
|
|
79
|
+
if [row["status"] for row in probe["results"]] != expected:
|
|
80
|
+
raise RuntimeError(f"Unexpected VXLAN boundary: {probe}")
|
|
81
|
+
if probe["source"].get("interface_mtu_bytes") != 1450:
|
|
82
|
+
raise RuntimeError(f"Unexpected VXLAN source interface: {probe['source']}")
|
|
83
|
+
if correlation["summary"]["receiver_observed"] != 5:
|
|
84
|
+
raise RuntimeError("Unexpected VXLAN receiver observation count")
|
|
85
|
+
report["directions"].append({"direction": label, "probe": probe,
|
|
86
|
+
"receiver": observed, "correlation": correlation})
|
|
87
|
+
finally:
|
|
88
|
+
if receiver.poll() is None:
|
|
89
|
+
receiver.terminate()
|
|
90
|
+
try:
|
|
91
|
+
receiver.wait(timeout=3)
|
|
92
|
+
except subprocess.TimeoutExpired:
|
|
93
|
+
receiver.kill()
|
|
94
|
+
receiver.wait(timeout=3)
|
|
95
|
+
receiver.stdout.close()
|
|
96
|
+
receiver.stderr.close()
|
|
97
|
+
finally:
|
|
98
|
+
for namespace in reversed(created):
|
|
99
|
+
ip("netns", "delete", namespace)
|
|
100
|
+
print(json.dumps(report, indent=2))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
try:
|
|
105
|
+
main()
|
|
106
|
+
except (RuntimeError, OSError, subprocess.SubprocessError) as error:
|
|
107
|
+
print(f"Lab failed: {error}", file=sys.stderr)
|
|
108
|
+
sys.exit(1)
|