Skip to main content

route-explain

Kernel-backed Linux routing forensics. Ask why a flow took this path.

By Artur Panek · PyPI · Project page

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.

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.

Alpha software. v0.4.1 fixes native nftables trace ingestion and makes explicit protocol selectors reach the kernel lookup even when no ports are supplied.

Why this is different

The project sits between low-level networking primitives and control-plane software:

Tool category Typical job route-explain
iproute2 expose authoritative kernel routing state and lookups uses those primitives as evidence, then explains and correlates them
routing / split-tunnel controllers install routes, manage policy, run daemons, reconcile state does not control routing state
userspace route simulators calculate what route should win from a state dump does not replace the kernel decision
network troubleshooting toolboxes collect many useful commands in one environment builds one flow-scoped explanation with explicit evidence levels

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.

Non-goals

route-explain is deliberately not trying to:

  • install, remove, or reconcile routes;
  • manage VPN or split-tunnel policy;
  • run a persistent privileged daemon;
  • emulate the full Linux RPDB/FIB decision tree in userspace;
  • turn incomplete state into a confident verdict.

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.

Quick example

$ route-explain 10.70.0.12 \
    --from 10.10.0.24 \
    --protocol tcp --sport 51123 --dport 443 \
    --mark 0x42 \
    --why-not tailscale0

ROUTE-EXPLAIN
flow:  tcp 10.70.0.12:443 from 10.10.0.24:51123
meta:  mark=0x42

Kernel decision
  matched prefix: default
  dev:            eth0
  table:          main

Why this path
  KERNEL kernel resolved the flow through table main on eth0
  KERNEL fibmatch selected prefix default in table main
  CHECK  a more-specific route exists in table 52: 10.70.0.0/24 via tailscale0

WHY NOT tailscale0?

Route exists:
  10.70.0.0/24 dev tailscale0 table 52

But:
  kernel selected table main on eth0

The selected path is kernel evidence. Routes in other tables are context, not a fake simulation of the RPDB walk.

Requirements

  • Linux
  • Python 3.11+
  • iproute2 with JSON output
  • nsenter from util-linux for --pid / --container
  • optional: wg for WireGuard peer context
  • optional: tailscale for host Tailscale context
  • optional: nft for runtime trace observation

Ordinary route lookups are read-only. Some namespace and nftables operations may require privileges depending on the host.

Install

Available on PyPI.

For a system CLI, pipx or uv tool is recommended:

pipx install route-explain
# or
uv tool install route-explain

Install from source for development:

git clone https://github.com/artur-panek/route-explain.git
cd route-explain
python -m venv .venv
source .venv/bin/activate
python -m pip install -e .

Core lookup

route-explain 10.70.0.12

Model a specific TCP flow:

route-explain 10.70.0.12 \
  --source 10.10.0.24 \
  --protocol tcp \
  --sport 51123 \
  --dport 443 \
  --mark 0x42

Supported kernel lookup context includes source, mark, TOS, incoming/output interface, VRF, protocol, and TCP/UDP ports. An explicit --protocol is sent to the kernel even without ports; when ports are supplied without --protocol, TCP is assumed.

Automation expectations

A live lookup can also act as a routing assertion. A mismatch exits with status 3, distinct from collection/input errors (status 2):

route-explain 10.70.0.12 \
  --expect-dev tailscale0 \
  --expect-table 52 \
  --expect-prefix 10.70.0.0/24

This is useful in smoke tests, VPN checks and network-change validation without turning route-explain into a routing controller.

Why not this route?

route-explain 10.70.0.12 --why-not tailscale0
route-explain 10.70.0.12 --why-not dev:wg0
route-explain 10.70.0.12 --why-not table:52

--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.

It deliberately does not claim to know a single “losing rule” unless there is runtime evidence for it.

Network namespaces and containers

Run the same explanation inside an iproute2 namespace:

route-explain 1.1.1.1 --netns blue

Enter an existing process network namespace:

route-explain 1.1.1.1 --pid 18422

Resolve a running Docker or Podman container and enter its network namespace:

route-explain 1.1.1.1 --container api

The same context switches are available to snapshot, doctor, and trace.

Snapshot and replay

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.

route-explain snapshot 10.70.0.12 --mark 0x42 > before.json
route-explain replay before.json

Aliases are available for the earlier terminology:

route-explain capture 10.70.0.12 > case.json
route-explain analyze case.json

Write directly to a file:

route-explain snapshot 10.70.0.12 -o case.json

Replay never asks the current kernel for a new decision. It rebuilds the report from the stored evidence.

Snapshot privacy

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.

Before/after diff

route-explain snapshot 10.70.0.12 -o before.json

# change a VPN, route or policy rule

route-explain snapshot 10.70.0.12 -o after.json
route-explain diff before.json after.json

Example:

ROUTE-EXPLAIN DIFF

ROUTING DECISION CHANGED
  before:
    table:  main
    prefix: default
    dev:    eth0
  after:
    table:  52
    prefix: 10.70.0.0/24
    dev:    tailscale0

WHY
  + routes: [...]
  + rules: [...]

Diffs compare the selected decision plus route/rule set changes. If snapshots describe different flows, the output explicitly warns about it.

Routing doctor

route-explain doctor
route-explain doctor --netns blue
route-explain doctor --container api

Current doctor checks include:

  • non-standard route tables with no direct RPDB lookup rule
  • multiple default-route paths within the same address family
  • advanced RPDB selectors/modifiers
  • overlay-like interfaces
  • Docker/Podman/CNI bridge and veth context

The doctor is intentionally conservative. It reports suspicious structure; it does not label every unusual topology as broken.

WireGuard and Tailscale context

When available, snapshots and live explanations add overlay evidence:

  • WireGuard peer AllowedIPs containing the destination
  • Tailscale peer ownership of an exact Tailscale IP
  • Tailscale PrimaryRoutes / AllowedIPs containing the destination

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.

nftables runtime trace

sudo route-explain trace 10.70.0.12 \
  --from 10.10.0.24 \
  --seconds 5

This runs a read-only nft monitor trace observer, parses nftables' native trace id ... records, groups related packet/rule/policy events by trace ID, and filters them to the supplied flow.

Important: nftables only emits trace events for packets already marked for tracing, typically by a rule containing:

meta nftrace set 1

route-explain does not inject that rule, change the ruleset, or generate packets automatically. Native nftables trace records are normalized into route-explain's trace JSON schema. If trace events expose packet marks, they are surfaced next to chain/rule/verdict context.

nft reconstructs printed table/chain/rule text from ruleset state read when the monitor starts, so changing the ruleset while a trace capture is running can make that printed rule text stale. route-explain surfaces this as a CHECK rather than silently treating it as immutable evidence.

Correlate observed nft state with a kernel lookup

sudo route-explain trace 10.70.0.12 \
  --from 10.10.0.24 \
  --seconds 5 \
  --correlate

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:

nft runtime trace
      ↓
observed mark / iif
      ↓
kernel re-lookup with observed selectors
      ↓
RPDB context
      ↓
selected FIB result
      ↓
baseline vs observed-state comparison

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.

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.

Evidence model

The human report separates three levels:

  • KERNEL — direct ip route get / fibmatch evidence.
  • INFO — useful context derived from current route/rule/link/overlay state.
  • CHECK — ambiguity, a conflict, or an evidence boundary worth investigating.

The project rule is simple: unknown is better than confidently wrong.

Machine-readable output

Normal explain:

route-explain 10.70.0.12 --json

Other workflows also support JSON where useful:

route-explain replay case.json --json
route-explain diff before.json after.json --json
route-explain doctor --json
route-explain trace 10.70.0.12 --json

The main report JSON retains its existing schema version. Snapshot, diff, doctor, and trace payloads have their own schema/version markers. Native nftables trace output is normalized as trace schema version 2 with source_format: "nft-native-trace".

What v0.4 still does not claim

route-explain still does not automatically reconstruct:

  • every nftables/iptables traversal when nftrace is not enabled
  • NAT transformations end-to-end
  • conntrack state/correlation
  • where a packet mark originally came from unless trace evidence shows it
  • every advanced RPDB selector
  • suppressor/goto semantics as a complete RPDB execution trace
  • arbitrary offline route decisions for destinations that were not captured

Snapshots are flow-scoped specifically to avoid turning replay into an invented userspace routing simulator.

Roadmap

  • kernel-backed full-flow selectors
  • fibmatch selected-prefix evidence
  • selector-aware RPDB candidate evaluation
  • --why-not device/table explanation
  • flow-scoped snapshot + replay
  • before/after diff
  • network namespace / PID / Docker / Podman context
  • routing doctor
  • WireGuard AllowedIPs context
  • Tailscale peer/subnet-route context
  • read-only nftables trace observation
  • correlate nft trace → observed mark/iif → kernel re-lookup → RPDB/FIB result
  • richer VRF/l3mdev explanation
  • conntrack/NAT correlation
  • opt-in assisted nft trace setup with explicit confirmation
  • richer snapshot redaction tooling

Development

python -m pip install -e '.[dev]'
ruff check .
pytest

See docs/design.md for the evidence model, docs/positioning.md for the product boundary, docs/snapshots.md for snapshot semantics, and docs/releasing.md for the Trusted Publishing release flow.

License

MIT

Metadata

Release files for route-explain 0.4.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for route-explain 0.4.1
File Size Uploaded
route_explain-0.4.1.tar.gz 39.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for route-explain 0.4.1
File Interpreter ABI Platform
route_explain-0.4.1-py3-none-any.whl Python 3 none any Details

Total release size: 72.2 kB

Release files / route_explain-0.4.1.tar.gz

Download URL route_explain-0.4.1.tar.gz
Size 39.3 kB
Tags Source
SHA-256 checksum
How to use checksums
d660d9c9aa6e04ea2ce002a3154ca61c0ff0f9a04e4d7d0402e737459ff6acda
BLAKE2b-256 checksum
How to use checksums
2a1655cb81287009bf5a6d1af13a6f223268447de0bf29eea589d8ed7b9811c5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.

Transparency log

Release files / route_explain-0.4.1-py3-none-any.whl

Download URL route_explain-0.4.1-py3-none-any.whl
Size 32.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8fd9c0077d75bd4533d08f924f88480cb2e975ab6251e5b70b6e7e9835c2d42b
BLAKE2b-256 checksum
How to use checksums
f3f3215137718060dd9a9a8eae0c519e850d5953424997f90c5e1bd11003efc2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.

Transparency log

Release history Release notifications | RSS feed

0.4.2

2 release files

This release

0.4.1 This release

2 release files

0.4.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page