route-explain 0.4.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.
Files changed (36) hide show
  1. route_explain-0.4.0/.github/workflows/ci.yml +49 -0
  2. route_explain-0.4.0/.github/workflows/publish.yml +130 -0
  3. route_explain-0.4.0/.gitignore +9 -0
  4. route_explain-0.4.0/CHANGELOG.md +47 -0
  5. route_explain-0.4.0/CONTRIBUTING.md +29 -0
  6. route_explain-0.4.0/LICENSE +21 -0
  7. route_explain-0.4.0/PKG-INFO +413 -0
  8. route_explain-0.4.0/README.md +380 -0
  9. route_explain-0.4.0/docs/design.md +150 -0
  10. route_explain-0.4.0/docs/positioning.md +75 -0
  11. route_explain-0.4.0/docs/releasing.md +47 -0
  12. route_explain-0.4.0/docs/snapshots.md +64 -0
  13. route_explain-0.4.0/pyproject.toml +55 -0
  14. route_explain-0.4.0/src/route_explain/__init__.py +3 -0
  15. route_explain-0.4.0/src/route_explain/__main__.py +3 -0
  16. route_explain-0.4.0/src/route_explain/analyze.py +459 -0
  17. route_explain-0.4.0/src/route_explain/cli.py +390 -0
  18. route_explain-0.4.0/src/route_explain/collect.py +123 -0
  19. route_explain-0.4.0/src/route_explain/context.py +87 -0
  20. route_explain-0.4.0/src/route_explain/correlation.py +240 -0
  21. route_explain-0.4.0/src/route_explain/diffing.py +164 -0
  22. route_explain-0.4.0/src/route_explain/doctor.py +169 -0
  23. route_explain-0.4.0/src/route_explain/model.py +85 -0
  24. route_explain-0.4.0/src/route_explain/nfttrace.py +148 -0
  25. route_explain-0.4.0/src/route_explain/overlay.py +120 -0
  26. route_explain-0.4.0/src/route_explain/render.py +152 -0
  27. route_explain-0.4.0/src/route_explain/snapshot.py +103 -0
  28. route_explain-0.4.0/src/route_explain/why.py +39 -0
  29. route_explain-0.4.0/tests/test_analyze.py +175 -0
  30. route_explain-0.4.0/tests/test_collect.py +67 -0
  31. route_explain-0.4.0/tests/test_doctor.py +39 -0
  32. route_explain-0.4.0/tests/test_metadata.py +13 -0
  33. route_explain-0.4.0/tests/test_overlay_trace.py +102 -0
  34. route_explain-0.4.0/tests/test_render.py +29 -0
  35. route_explain-0.4.0/tests/test_v03.py +98 -0
  36. route_explain-0.4.0/tests/test_v04.py +152 -0
@@ -0,0 +1,49 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ matrix:
15
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
16
+ steps:
17
+ - uses: actions/checkout@v6
18
+ - uses: actions/setup-python@v6
19
+ with:
20
+ python-version: ${{ matrix.python-version }}
21
+ cache: pip
22
+ - run: python -m pip install -U pip
23
+ - run: python -m pip install -e ".[dev]"
24
+ - run: ruff check .
25
+ - run: pytest
26
+
27
+ package:
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - uses: actions/checkout@v6
31
+ - uses: actions/setup-python@v6
32
+ with:
33
+ python-version: "3.14"
34
+ cache: pip
35
+ - run: python -m pip install -U pip
36
+ - run: python -m pip install -e ".[release]"
37
+ - name: Build wheel and sdist
38
+ run: python -m build
39
+ - name: Validate package metadata
40
+ run: python -m twine check dist/*
41
+ - name: Smoke-test built wheel
42
+ shell: bash
43
+ run: |
44
+ set -euo pipefail
45
+ python -m venv /tmp/route-explain-smoke
46
+ /tmp/route-explain-smoke/bin/python -m pip install --upgrade pip
47
+ /tmp/route-explain-smoke/bin/python -m pip install dist/*.whl
48
+ /tmp/route-explain-smoke/bin/route-explain --version
49
+ /tmp/route-explain-smoke/bin/route-explain --help >/dev/null
@@ -0,0 +1,130 @@
1
+ name: Publish route-explain v0.4.0
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - release/v0.4.0
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ build:
13
+ name: Build distributions
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v6
17
+ - uses: actions/setup-python@v6
18
+ with:
19
+ python-version: "3.14"
20
+ cache: pip
21
+
22
+ - run: python -m pip install -U pip
23
+ - run: python -m pip install -e ".[release]"
24
+
25
+ - name: Verify release branch matches package version
26
+ shell: bash
27
+ run: |
28
+ python - <<'PY'
29
+ import os
30
+ import tomllib
31
+ from pathlib import Path
32
+
33
+ version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
34
+ branch = os.environ["GITHUB_REF_NAME"]
35
+ expected = f"release/v{version}"
36
+ if branch != expected:
37
+ raise SystemExit(
38
+ f"release branch {branch!r} does not match package version {expected!r}"
39
+ )
40
+ print(f"release version verified: {version}")
41
+ PY
42
+
43
+ - name: Build wheel and sdist
44
+ run: python -m build
45
+
46
+ - name: Validate package metadata
47
+ run: python -m twine check dist/*
48
+
49
+ - name: Smoke-test built wheel
50
+ shell: bash
51
+ run: |
52
+ set -euo pipefail
53
+ python -m venv /tmp/route-explain-release-smoke
54
+ /tmp/route-explain-release-smoke/bin/python -m pip install --upgrade pip
55
+ /tmp/route-explain-release-smoke/bin/python -m pip install dist/*.whl
56
+ /tmp/route-explain-release-smoke/bin/route-explain --version
57
+ /tmp/route-explain-release-smoke/bin/route-explain --help >/dev/null
58
+
59
+ - uses: actions/upload-artifact@v4
60
+ with:
61
+ name: python-package-distributions
62
+ path: dist/
63
+ if-no-files-found: error
64
+ retention-days: 7
65
+
66
+ publish:
67
+ name: Publish distributions to PyPI
68
+ needs: build
69
+ runs-on: ubuntu-latest
70
+ environment:
71
+ name: pypi
72
+ url: https://pypi.org/p/route-explain
73
+ permissions:
74
+ id-token: write
75
+
76
+ steps:
77
+ - uses: actions/download-artifact@v5
78
+ with:
79
+ name: python-package-distributions
80
+ path: dist/
81
+
82
+ - name: Publish to PyPI with Trusted Publishing
83
+ uses: pypa/gh-action-pypi-publish@release/v1
84
+
85
+ release:
86
+ name: Publish GitHub Release
87
+ needs: publish
88
+ runs-on: ubuntu-latest
89
+ permissions:
90
+ contents: write
91
+ env:
92
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
93
+ GH_REPO: artur-panek/route-explain
94
+
95
+ steps:
96
+ - name: Create v0.4.0 release
97
+ shell: bash
98
+ run: |
99
+ cat > /tmp/release-notes.md <<'EOF'
100
+ ## route-explain v0.4.0
101
+
102
+ First PyPI release of route-explain, a read-only, kernel-backed Linux routing forensics CLI.
103
+
104
+ ### Highlights
105
+
106
+ - nftables runtime trace → observed mark/input-interface → kernel re-lookup correlation
107
+ - `--expect-dev`, `--expect-table`, and `--expect-prefix` automation assertions
108
+ - flow-scoped snapshots, replay, and before/after diff
109
+ - network namespace, PID, Docker, and Podman context
110
+ - routing doctor
111
+ - WireGuard AllowedIPs and Tailscale route context
112
+ - Python 3.11–3.14 support
113
+ - wheel + sdist packaging with Trusted Publishing
114
+
115
+ ### Install
116
+
117
+ ```bash
118
+ pipx install route-explain
119
+ ```
120
+
121
+ or:
122
+
123
+ ```bash
124
+ uv tool install route-explain
125
+ ```
126
+
127
+ The project remains read-only by default: it explains routing state rather than managing it.
128
+ EOF
129
+
130
+ gh release create v0.4.0 --repo "$GH_REPO" --target main --title "route-explain v0.4.0" --notes-file /tmp/release-notes.md
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .venv/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .coverage
@@ -0,0 +1,47 @@
1
+ # Changelog
2
+
3
+ All notable user-facing changes are documented here.
4
+
5
+ ## Unreleased
6
+
7
+ ## 0.4.0
8
+
9
+ - add `trace --correlate` to extract observed nftables packet mark/input-interface state and run a kernel-backed routing probe with those selectors;
10
+ - compare correlated kernel decisions with the baseline lookup while preserving an explicit no-fake-reroute caveat;
11
+ - add `--expect-dev`, `--expect-table`, and `--expect-prefix` assertions with exit status 3 for automation;
12
+ - treat source-specific RPDB rules as `MAYBE` when source context is missing instead of silently discarding them;
13
+ - treat `default` and `0.0.0.0/0` / `::/0` as equivalent when marking the selected route;
14
+ - add Python 3.14 CI coverage and package build/sdist/wheel smoke testing;
15
+ - add a PyPI Trusted Publishing workflow using GitHub OIDC with no long-lived PyPI token;
16
+ - reposition the project explicitly as read-only, kernel-backed Linux routing forensics rather than a routing controller or userspace simulator;
17
+ - document the product boundary and align package/contributor guidance with the evidence-first, non-mutating design.
18
+
19
+ ## 0.3.0
20
+
21
+ - add flow-scoped snapshot capture and evidence-only replay;
22
+ - add before/after route and RPDB diffing;
23
+ - add `--why-not` explanations for devices and route tables;
24
+ - add host, iproute2 netns, PID, Docker, and Podman network-namespace execution;
25
+ - add a conservative routing `doctor` for table/RPDB/default-route/overlay/bridge context;
26
+ - add WireGuard peer `AllowedIPs` evidence;
27
+ - add Tailscale exact-IP and subnet-route context in host mode;
28
+ - add read-only nftables runtime trace observation without mutating the ruleset;
29
+ - expand regression coverage for the new workflows;
30
+ - keep the existing v0.2 explain invocation backward compatible.
31
+
32
+ ## 0.2.0
33
+
34
+ - send flow selectors such as marks, interfaces, TOS and TCP/UDP ports into the real kernel route lookup;
35
+ - add `fibmatch` evidence for the exact selected FIB prefix;
36
+ - evaluate common RPDB selectors instead of treating every source/destination-compatible rule as equally plausible;
37
+ - distinguish `MATCH` from `MAYBE` when selector context is missing;
38
+ - add structured `KERNEL`, `INFO` and `CHECK` evidence to human and JSON reports;
39
+ - make selected-route highlighting prefix-aware;
40
+ - version the JSON schema;
41
+ - expand tests and CI to Python 3.13.
42
+
43
+ ## 0.1.0
44
+
45
+ - initial kernel-backed routing decision report;
46
+ - policy-rule, all-table route and overlay context;
47
+ - text and JSON output.
@@ -0,0 +1,29 @@
1
+ # Contributing
2
+
3
+ Contributions are welcome, especially reproducible routing edge cases.
4
+
5
+ The project has two non-negotiable rules:
6
+
7
+ 1. **Do not turn an inference into a verdict unless the implementation has evidence for it.**
8
+ 2. **Prefer read-only evidence collection over state mutation.** A feature that changes routes, firewall rules, VPN policy, or other network state needs an explicit opt-in design and must not become the normal diagnostic path.
9
+
10
+ ## Local setup
11
+
12
+ ```bash
13
+ python -m venv .venv
14
+ source .venv/bin/activate
15
+ python -m pip install -e '.[dev]'
16
+ ruff check .
17
+ pytest
18
+ ```
19
+
20
+ ## Good bug reports
21
+
22
+ Please include:
23
+
24
+ - the `route-explain` command you ran;
25
+ - sanitized `route-explain --json` output when possible;
26
+ - relevant `ip -j rule show` and `ip -j route show table all` output;
27
+ - the result you expected and why.
28
+
29
+ Never post secrets, private keys or credentials. Public IPs and internal topology may also be sensitive, so sanitize them if needed.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Artur Panek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,413 @@
1
+ Metadata-Version: 2.5
2
+ Name: route-explain
3
+ Version: 0.4.0
4
+ Summary: Read-only, kernel-backed Linux routing forensics for explaining why a flow took its path.
5
+ Project-URL: Homepage, https://artur.panek.tech/work/route-explain/
6
+ Project-URL: Repository, https://github.com/artur-panek/route-explain
7
+ Project-URL: Issues, https://github.com/artur-panek/route-explain/issues
8
+ Project-URL: Changelog, https://github.com/artur-panek/route-explain/blob/main/CHANGELOG.md
9
+ Project-URL: Documentation, https://github.com/artur-panek/route-explain/tree/main/docs
10
+ Author: Artur Panek
11
+ License: MIT
12
+ License-File: LICENSE
13
+ Keywords: cli,debugging,forensics,iproute2,linux,netns,networking,nftables,observability,policy-routing,read-only,routing,tailscale,wireguard
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Environment :: Console
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: System :: Networking
24
+ Classifier: Topic :: System :: Systems Administration
25
+ Requires-Python: >=3.11
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == 'dev'
28
+ Requires-Dist: ruff>=0.7; extra == 'dev'
29
+ Provides-Extra: release
30
+ Requires-Dist: build>=1.2; extra == 'release'
31
+ Requires-Dist: twine>=6.0; extra == 'release'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # route-explain
35
+
36
+ **Kernel-backed Linux routing forensics. Ask why a flow took this path.**
37
+
38
+ By [Artur Panek](https://artur.panek.tech/) · [Project page](https://artur.panek.tech/work/route-explain/)
39
+
40
+ `route-explain` is a **read-only Linux networking forensic CLI**. It asks the running kernel for the authoritative routing decision for a specific flow, then correlates RPDB, FIB, namespace, overlay, snapshot, and optional nftables trace evidence around that answer.
41
+
42
+ It is intentionally **not** a routing controller, split-tunnel manager, background daemon, or userspace route simulator. It does not install routes, reconcile desired state, manage VPN policy, or replace the kernel with its own idea of what should have happened.
43
+
44
+ > Alpha software. v0.4 adds evidence-backed nftables → kernel routing correlation, automation expectations, Python 3.14 coverage, and a Trusted Publishing release pipeline on top of the v0.3 diagnostic workflows.
45
+
46
+ ## Why this is different
47
+
48
+ The project sits between low-level networking primitives and control-plane software:
49
+
50
+ | Tool category | Typical job | `route-explain` |
51
+ | --- | --- | --- |
52
+ | `iproute2` | expose authoritative kernel routing state and lookups | uses those primitives as evidence, then explains and correlates them |
53
+ | routing / split-tunnel controllers | install routes, manage policy, run daemons, reconcile state | **does not control routing state** |
54
+ | userspace route simulators | calculate what route should win from a state dump | **does not replace the kernel decision** |
55
+ | network troubleshooting toolboxes | collect many useful commands in one environment | builds one flow-scoped explanation with explicit evidence levels |
56
+
57
+ The kernel remains the routing oracle. `ip route get` and `fibmatch` establish the selected path; everything else is labelled as context, inference, or a limitation.
58
+
59
+ ### Non-goals
60
+
61
+ `route-explain` is deliberately not trying to:
62
+
63
+ - install, remove, or reconcile routes;
64
+ - manage VPN or split-tunnel policy;
65
+ - run a persistent privileged daemon;
66
+ - emulate the full Linux RPDB/FIB decision tree in userspace;
67
+ - turn incomplete state into a confident verdict.
68
+
69
+ That boundary is a feature: the tool is meant to help investigate a running system without becoming another component that can change the system being investigated.
70
+ ## Quick example
71
+
72
+ ```console
73
+ $ route-explain 10.70.0.12 \
74
+ --from 10.10.0.24 \
75
+ --protocol tcp --sport 51123 --dport 443 \
76
+ --mark 0x42 \
77
+ --why-not tailscale0
78
+
79
+ ROUTE-EXPLAIN
80
+ flow: tcp 10.70.0.12:443 from 10.10.0.24:51123
81
+ meta: mark=0x42
82
+
83
+ Kernel decision
84
+ matched prefix: default
85
+ dev: eth0
86
+ table: main
87
+
88
+ Why this path
89
+ KERNEL kernel resolved the flow through table main on eth0
90
+ KERNEL fibmatch selected prefix default in table main
91
+ CHECK a more-specific route exists in table 52: 10.70.0.0/24 via tailscale0
92
+
93
+ WHY NOT tailscale0?
94
+
95
+ Route exists:
96
+ 10.70.0.0/24 dev tailscale0 table 52
97
+
98
+ But:
99
+ kernel selected table main on eth0
100
+ ```
101
+
102
+ The selected path is kernel evidence. Routes in other tables are context, not a fake simulation of the RPDB walk.
103
+
104
+ ## Requirements
105
+
106
+ - Linux
107
+ - Python 3.11+
108
+ - `iproute2` with JSON output
109
+ - `nsenter` from util-linux for `--pid` / `--container`
110
+ - optional: `wg` for WireGuard peer context
111
+ - optional: `tailscale` for host Tailscale context
112
+ - optional: `nft` for runtime trace observation
113
+
114
+ Ordinary route lookups are read-only. Some namespace and nftables operations may require privileges depending on the host.
115
+
116
+ ## Install
117
+
118
+ The first PyPI release pipeline is prepared for v0.4.0. Once the package is published, the preferred CLI installs are:
119
+
120
+ ```bash
121
+ pipx install route-explain
122
+ # or
123
+ uv tool install route-explain
124
+ ```
125
+
126
+ Until the first PyPI release is published, install from source:
127
+
128
+
129
+
130
+ ```bash
131
+ git clone https://github.com/artur-panek/route-explain.git
132
+ cd route-explain
133
+ python -m venv .venv
134
+ source .venv/bin/activate
135
+ python -m pip install -e .
136
+ ```
137
+
138
+ ## Core lookup
139
+
140
+ ```bash
141
+ route-explain 10.70.0.12
142
+ ```
143
+
144
+ Model a specific TCP flow:
145
+
146
+ ```bash
147
+ route-explain 10.70.0.12 \
148
+ --source 10.10.0.24 \
149
+ --protocol tcp \
150
+ --sport 51123 \
151
+ --dport 443 \
152
+ --mark 0x42
153
+ ```
154
+
155
+ Supported kernel lookup context includes source, mark, TOS, incoming/output interface, VRF, protocol, and TCP/UDP ports.
156
+
157
+ ## Automation expectations
158
+
159
+ A live lookup can also act as a routing assertion. A mismatch exits with status **3**, distinct from collection/input errors (status 2):
160
+
161
+ ```bash
162
+ route-explain 10.70.0.12 \
163
+ --expect-dev tailscale0 \
164
+ --expect-table 52 \
165
+ --expect-prefix 10.70.0.0/24
166
+ ```
167
+
168
+ This is useful in smoke tests, VPN checks and network-change validation without turning route-explain into a routing controller.
169
+
170
+ ## Why not this route?
171
+
172
+ ```bash
173
+ route-explain 10.70.0.12 --why-not tailscale0
174
+ route-explain 10.70.0.12 --why-not dev:wg0
175
+ route-explain 10.70.0.12 --why-not table:52
176
+ ```
177
+
178
+ `--why-not` checks whether a matching route exists on the requested device/table, shows relevant policy-rule candidates, and contrasts that context with the kernel-selected path.
179
+
180
+ It deliberately does **not** claim to know a single “losing rule” unless there is runtime evidence for it.
181
+
182
+ ## Network namespaces and containers
183
+
184
+ Run the same explanation inside an iproute2 namespace:
185
+
186
+ ```bash
187
+ route-explain 1.1.1.1 --netns blue
188
+ ```
189
+
190
+ Enter an existing process network namespace:
191
+
192
+ ```bash
193
+ route-explain 1.1.1.1 --pid 18422
194
+ ```
195
+
196
+ Resolve a running Docker or Podman container and enter its network namespace:
197
+
198
+ ```bash
199
+ route-explain 1.1.1.1 --container api
200
+ ```
201
+
202
+ The same context switches are available to `snapshot`, `doctor`, and `trace`.
203
+
204
+ ## Snapshot and replay
205
+
206
+ A snapshot is **flow-scoped**. It stores the kernel lookup for one flow plus the route/rule/link and overlay evidence used to explain it.
207
+
208
+ ```bash
209
+ route-explain snapshot 10.70.0.12 --mark 0x42 > before.json
210
+ route-explain replay before.json
211
+ ```
212
+
213
+ Aliases are available for the earlier terminology:
214
+
215
+ ```bash
216
+ route-explain capture 10.70.0.12 > case.json
217
+ route-explain analyze case.json
218
+ ```
219
+
220
+ Write directly to a file:
221
+
222
+ ```bash
223
+ route-explain snapshot 10.70.0.12 -o case.json
224
+ ```
225
+
226
+ Replay never asks the current kernel for a new decision. It rebuilds the report from the stored evidence.
227
+
228
+ ### Snapshot privacy
229
+
230
+ Snapshots can contain internal IPs, route topology, interface names, WireGuard peer public keys/endpoints, and Tailscale metadata. They contain diagnostic state, not private keys, but you should still sanitize snapshots before attaching them to a public issue.
231
+
232
+ ## Before/after diff
233
+
234
+ ```bash
235
+ route-explain snapshot 10.70.0.12 -o before.json
236
+
237
+ # change a VPN, route or policy rule
238
+
239
+ route-explain snapshot 10.70.0.12 -o after.json
240
+ route-explain diff before.json after.json
241
+ ```
242
+
243
+ Example:
244
+
245
+ ```text
246
+ ROUTE-EXPLAIN DIFF
247
+
248
+ ROUTING DECISION CHANGED
249
+ before:
250
+ table: main
251
+ prefix: default
252
+ dev: eth0
253
+ after:
254
+ table: 52
255
+ prefix: 10.70.0.0/24
256
+ dev: tailscale0
257
+
258
+ WHY
259
+ + routes: [...]
260
+ + rules: [...]
261
+ ```
262
+
263
+ Diffs compare the selected decision plus route/rule set changes. If snapshots describe different flows, the output explicitly warns about it.
264
+
265
+ ## Routing doctor
266
+
267
+ ```bash
268
+ route-explain doctor
269
+ route-explain doctor --netns blue
270
+ route-explain doctor --container api
271
+ ```
272
+
273
+ Current doctor checks include:
274
+
275
+ - non-standard route tables with no direct RPDB lookup rule
276
+ - multiple default-route paths within the same address family
277
+ - advanced RPDB selectors/modifiers
278
+ - overlay-like interfaces
279
+ - Docker/Podman/CNI bridge and veth context
280
+
281
+ The doctor is intentionally conservative. It reports suspicious structure; it does not label every unusual topology as broken.
282
+
283
+ ## WireGuard and Tailscale context
284
+
285
+ When available, snapshots and live explanations add overlay evidence:
286
+
287
+ - WireGuard peer `AllowedIPs` containing the destination
288
+ - Tailscale peer ownership of an exact Tailscale IP
289
+ - Tailscale `PrimaryRoutes` / `AllowedIPs` containing the destination
290
+
291
+ Tailscale daemon status is only collected in host context. `tailscale status` communicates through a Unix socket, so attributing host daemon state to a namespace/container would be misleading.
292
+
293
+ ## nftables runtime trace
294
+
295
+ ```bash
296
+ sudo route-explain trace 10.70.0.12 \
297
+ --from 10.10.0.24 \
298
+ --seconds 5
299
+ ```
300
+
301
+ This runs a **read-only** `nft -j monitor trace` observer and filters trace events for the supplied flow.
302
+
303
+ Important: nftables only emits trace events for packets already marked for tracing, typically by a rule containing:
304
+
305
+ ```text
306
+ meta nftrace set 1
307
+ ```
308
+
309
+ `route-explain` does **not** inject that rule, change the ruleset, or generate packets automatically. If trace events expose packet marks, they are surfaced next to chain/rule/verdict context.
310
+
311
+ ### Correlate observed nft state with a kernel lookup
312
+
313
+ ```bash
314
+ sudo route-explain trace 10.70.0.12 \
315
+ --from 10.10.0.24 \
316
+ --seconds 5 \
317
+ --correlate
318
+ ```
319
+
320
+ With `--correlate`, route-explain extracts **observed** routing-relevant state from matching nft trace events (currently packet mark and named input interface), then asks the kernel again using those observed selectors:
321
+
322
+ ```text
323
+ nft runtime trace
324
+ ↓
325
+ observed mark / iif
326
+ ↓
327
+ kernel re-lookup with observed selectors
328
+ ↓
329
+ RPDB context
330
+ ↓
331
+ selected FIB result
332
+ ↓
333
+ baseline vs observed-state comparison
334
+ ```
335
+
336
+ An observed output interface is shown as context but is not forced into the re-lookup, because doing so would turn an observation after route selection into an artificial input.
337
+
338
+ The report also keeps an explicit caveat: a correlated re-lookup proves what the kernel returns **for that observed selector state**. It does not by itself prove that Linux actually performed a reroute at that nftables hook.
339
+ ## Evidence model
340
+
341
+ The human report separates three levels:
342
+
343
+ - **KERNEL** — direct `ip route get` / `fibmatch` evidence.
344
+ - **INFO** — useful context derived from current route/rule/link/overlay state.
345
+ - **CHECK** — ambiguity, a conflict, or an evidence boundary worth investigating.
346
+
347
+ The project rule is simple: **unknown is better than confidently wrong**.
348
+
349
+ ## Machine-readable output
350
+
351
+ Normal explain:
352
+
353
+ ```bash
354
+ route-explain 10.70.0.12 --json
355
+ ```
356
+
357
+ Other workflows also support JSON where useful:
358
+
359
+ ```bash
360
+ route-explain replay case.json --json
361
+ route-explain diff before.json after.json --json
362
+ route-explain doctor --json
363
+ route-explain trace 10.70.0.12 --json
364
+ ```
365
+
366
+ The main report JSON retains its existing schema version. Snapshot, diff, doctor, and trace payloads have their own schema/version markers.
367
+
368
+ ## What v0.4 still does not claim
369
+
370
+ `route-explain` still does **not** automatically reconstruct:
371
+
372
+ - every nftables/iptables traversal when `nftrace` is not enabled
373
+ - NAT transformations end-to-end
374
+ - conntrack state/correlation
375
+ - where a packet mark originally came from unless trace evidence shows it
376
+ - every advanced RPDB selector
377
+ - suppressor/goto semantics as a complete RPDB execution trace
378
+ - arbitrary offline route decisions for destinations that were not captured
379
+
380
+ Snapshots are flow-scoped specifically to avoid turning replay into an invented userspace routing simulator.
381
+
382
+ ## Roadmap
383
+
384
+ - [x] kernel-backed full-flow selectors
385
+ - [x] `fibmatch` selected-prefix evidence
386
+ - [x] selector-aware RPDB candidate evaluation
387
+ - [x] `--why-not` device/table explanation
388
+ - [x] flow-scoped snapshot + replay
389
+ - [x] before/after `diff`
390
+ - [x] network namespace / PID / Docker / Podman context
391
+ - [x] routing `doctor`
392
+ - [x] WireGuard `AllowedIPs` context
393
+ - [x] Tailscale peer/subnet-route context
394
+ - [x] read-only nftables trace observation
395
+ - [x] correlate nft trace → observed mark/iif → kernel re-lookup → RPDB/FIB result
396
+ - [ ] richer VRF/l3mdev explanation
397
+ - [ ] conntrack/NAT correlation
398
+ - [ ] opt-in assisted nft trace setup with explicit confirmation
399
+ - [ ] richer snapshot redaction tooling
400
+
401
+ ## Development
402
+
403
+ ```bash
404
+ python -m pip install -e '.[dev]'
405
+ ruff check .
406
+ pytest
407
+ ```
408
+
409
+ See [`docs/design.md`](docs/design.md) for the evidence model, [`docs/positioning.md`](docs/positioning.md) for the product boundary, [`docs/snapshots.md`](docs/snapshots.md) for snapshot semantics, and [`docs/releasing.md`](docs/releasing.md) for the Trusted Publishing release flow.
410
+
411
+ ## License
412
+
413
+ MIT