NetWhy is a read-only Linux and Apple Silicon macOS CLI that explains why a network connection succeeds or fails.
Linux and macOS already provide excellent low-level tools for DNS, routes, sockets, TLS, and HTTP. The difficult part is correlating their output. NetWhy follows one connection through those layers and turns the evidence into a concise, deterministic diagnosis.
$ netwhy https://api.example.com
NetWhy 0.2.0
Result: FAIL
Target: https://api.example.com:443
Summary: TCP connects, but the TLS handshake fails.
Likely cause: the certificate name does not match the requested hostname
Next steps:
1. Check the certificate name, trust chain, system clock, SNI, and supported TLS versions.
Evidence:
[PASS] DNS 2 addresses in 8 ms
[PASS] ROUTE 203.0.113.10 · dev enp5s0 · via 192.168.1.1
[PASS] TCP 203.0.113.10:443 · 24 ms · connected
[FAIL] TLS 203.0.113.10:443 · certificate is not valid for api.example.com
Important
NetWhy v0.5.0 is the current stable release. The v0.5 roadmap is complete and qualified by the Linux release gate and native Apple Silicon CI validation.
- Explain, do not merely dump: gather evidence and identify the first failing layer.
- Read-only locally: never rewrite routes, firewall rules, resolver settings, or application configuration.
- Deterministic: diagnoses come from explicit rules and remain suitable for scripts and incident reports.
- Useful without root: basic diagnosis must work as a normal user; privileged features must degrade gracefully.
- Evidence before advice: show the observations supporting every conclusion.
- Privacy-aware: redact credentials and avoid collecting unrelated system state.
- Linux: full local diagnosis plus
--pid,--docker, and--podmanexecution-context selection. - Apple Silicon macOS (
aarch64-apple-darwin): local DNS, native route, TCP, TLS, and HTTP diagnosis. Linux execution-context selectors return a structuredCONTEXT_UNAVAILABLEerror instead of being ignored. - Intel macOS and other operating systems: unsupported.
One run correlates the requested execution context and target across these layers:
- Resolve the hostname with the system resolver; optionally collect supplemental wire-DNS A/AAAA evidence without changing the connection result.
- Ask the kernel which route, interface, gateway, source address, MTU, advertised MSS, table, type, metric, protocol, scope, and VRF it selects. Linux prefers a TCP-port-aware lookup and records a bounded address-only fallback when an older
iprejects selectors. - Test every retained address over TCP with bounded concurrency.
- Optionally connect through HTTP, HTTPS, SOCKS5, or SOCKS5H proxy transport.
- For HTTPS, validate the TLS handshake and certificate using the requested or overridden server name and selected Mozilla, native-platform, or custom-CA trust; an optional mTLS identity is loaded once before probing.
- For HTTP or HTTPS, send the selected
HEAD(default) orGETrequest; HTTPS prefers HTTP/2 through ALPN and falls back to bounded HTTP/1.1 parsing. - On Linux, collect read-only nftables, path-MTU, address-preference, systemd-resolved, NetworkManager, and VPN evidence when the relevant tools are available.
- Incorporate explicitly selected, versioned evidence plugins.
- Explain the most likely failure and suggest focused follow-up checks.
- Produce outcome-first text, a shareable redacted report, or a structural comparison of two reports.
Linux supports current-process, PID, local Docker, and local Podman contexts. Apple Silicon macOS supports local DNS, route, TCP, TLS, and HTTP diagnosis; Linux-only path collectors and context selectors report a structured skip or error. See the product roadmap for implementation and release-gate status.
netwhy [OPTIONS] <TARGET>
Arguments:
<TARGET> URL, hostname, IP address, or host:port
Options:
--json Emit a versioned JSON report
--pid <PID> Select a Linux process context
--docker <CONTAINER> Select a local Docker container context
--podman <CONTAINER> Select a local Podman container context
--ipv4 Test IPv4 only
--ipv6 Test IPv6 only
--timeout-ms <MILLIS> Per-operation timeout [default: 3000]
--deep-dns Collect supplemental wire-DNS evidence
--dns-server <IP:PORT> Use an explicit DNS server with --deep-dns
--tls-trust <SOURCE> mozilla, native, or file [default: mozilla]
--ca-file <PEM> Custom CA bundle; requires --tls-trust file
--client-cert <PEM> Client certificate; requires --client-key
--client-key <PEM> Client private key; requires --client-cert
--server-name <NAME> TLS SNI and certificate-name override
--http-method <METHOD> head or get [default: head]
--proxy-mode <MODE> direct or environment [default: direct]
--proxy-url <URL> Explicit HTTP(S), SOCKS5, or SOCKS5H proxy
--plugin <PROGRAM> Evidence plugin; repeatable up to eight times
netwhy report [OPTIONS] <TARGET> [--redaction standard|strict]
netwhy explain [OPTIONS] <TARGET> [--format json|markdown]
netwhy mcp serve
netwhy compare [--json] <LEFT.json> <RIGHT.json>
netwhy matrix [--json] [--context current|pid:PID|docker:CONTAINER|podman:CONTAINER] <TARGET>
netwhy bundle --output <NEW_FILE> <TARGET>
netwhy completions <bash|elvish|fish|powershell|zsh>
netwhy update [--check] [--prerelease]
netwhy version
Target interpretation is intentionally predictable:
| Input | Interpretation |
|---|---|
https://example.com/health |
HTTPS on the URL's port, defaulting to 443 |
http://example.com |
HTTP on the URL's port, defaulting to 80 |
example.com |
HTTPS on port 443 |
example.com:5432 |
Raw TCP on port 5432 |
192.0.2.10 |
HTTPS on port 443 |
[2001:db8::10]:443 |
Raw TCP on port 443 |
[fe80::1%3]:443 |
Raw TCP on port 443 using IPv6 interface scope index 3 |
Exit codes:
| Code | Meaning |
|---|---|
0 |
The target is reachable. Warnings such as partial IPv6 failure or an HTTP error status may still be present. |
1 |
Connectivity or the requested application protocol failed. |
2 |
The invocation or selected execution context was invalid, or NetWhy itself could not produce a report. |
NetWhy does not mutate local configuration. It performs active DNS, TCP, optional tracepath, TLS, and HTTP HEAD or GET probes, so it is not a passive observer. Deep DNS is supplemental and never replaces the system resolver result used to connect. nftables inspection is static and read-only; NetWhy does not install tracing rules or claim packet-perfect firewall attribution.
An explicitly selected evidence plugin is a separate executable and runs with NetWhy's operating-system privileges and execution context. Only use trusted plugins; NetWhy bounds their runtime and output but cannot make third-party code read-only.
Direct application transport remains the default even when proxy variables exist. --proxy-mode environment selects HTTP_PROXY, HTTPS_PROXY, or ALL_PROXY from the active execution context and honors exact host, domain suffix, port, IP, and CIDR NO_PROXY entries. --proxy-url selects an explicit proxy and takes precedence over environment selection. HTTP targets use absolute-form requests; HTTPS and raw TCP use HTTP CONNECT; HTTPS proxy URLs authenticate the proxy connection with TLS. SOCKS5 resolves the target locally, while SOCKS5H delegates target DNS to the proxy. --ipv4 and --ipv6 filter proxy endpoints and local SOCKS5 target DNS; a hostname with remote-resolving HTTP(S) or SOCKS5H transport is rejected when a family flag is set because the requested family cannot be guaranteed. Embedded HTTP Basic and SOCKS username/password credentials are supported and never serialized.
--pid <PID> selects another Linux process as the execution context. NetWhy opens the target context before changing its own process, enters a differing mount namespace and filesystem root before a differing network namespace, and only then creates the async runtime. Shared namespaces and roots require no capabilities. Entering a differing namespace requires CAP_SYS_ADMIN; entering a differing root requires CAP_SYS_CHROOT. NetWhy never attaches to or mutates the selected process. If its proxy environment cannot be read, the report records that limitation and continues with the selected resolver and network context.
--docker <CONTAINER> and --podman <CONTAINER> resolve a running container to its init PID through the corresponding CLI, then use the same pinned process-context path. These options are mutually exclusive with each other and with --pid. NetWhy accepts only a demonstrably local Unix-socket Docker context or a non-remote Podman service: a PID reported by a remote runtime belongs to another host and is unsafe to interpret through local /proc. Runtime commands are shell-free, time-bounded, and output-bounded. The container PID is checked again after its /proc context has been pinned so a concurrent restart fails cleanly instead of mixing contexts.
Rootless Podman owns its container namespaces from Podman's user namespace. Run NetWhy through podman unshare so it has the namespace-local capabilities needed to enter an isolated rootless container:
podman unshare netwhy --podman my-container https://example.comRoute inspection uses ip -j route get DESTINATION ipproto tcp dport PORT on Linux, falling back to an explicitly labeled address-only lookup only when selectors are unsupported, and /sbin/route -n get on Apple Silicon macOS. Linux additionally uses bounded nft, tracepath, resolvectl, and nmcli commands when installed. Every helper is shell-free, time-bounded, runs in an isolated process group, and has bounded output. Missing tools produce explicit skip evidence without suppressing network probes.
Reports reject target URL credentials, redact target query strings and fragments, and record the effective timeout, address-family selection, application transport, execution context, required capabilities, TLS profile, HTTP method, and TLS trust source. netwhy report --redaction strict additionally applies deterministic pseudonyms to target, address, interface, VRF, process/container, certificate-identity, resolver, firewall, and plugin fields. DNS evidence is capped at 32 unique addresses; deep DNS answers are capped separately and strict mode redacts them.
netwhy explain is the agent-ready interface. It emits a stable, strict-redacted JSON brief by default (or the same contract as Markdown), separates deterministic evidence from the diagnosis, caps unbounded lists, and marks every suggested next check as requiring confirmation. It never calls an external model, transmits report data, or applies repairs.
netwhy mcp serve exposes the same netwhy_explain contract through stdio MCP. It declares one read-only, network-facing tool; its output is both structured MCP content and serialized JSON text for compatible clients. The server accepts only a target and a bounded per-operation timeout, invokes no shell, and never exposes a repair or model-sampling tool.
netwhy matrix starts one shell-free, timeout-bounded report --redaction strict child per selected context, runs at most eight children, and retains the requested order. Its envelope intentionally pseudonymizes PID/container identifiers. netwhy bundle writes the current-context strict matrix as a versioned JSON support bundle with a checksum for each embedded report. The output must be a new file: NetWhy writes it owner-only, synchronizes it, publishes it through an atomic hard link, and never follows or overwrites an existing destination.
netwhy update is the sole explicit local mutation. It checks the project's published GitHub Releases, selects a newer archive for the running supported target, downloads its published SHA-256 checksum and archive over HTTPS, verifies the digest, extracts only the expected executable, and atomically replaces the running binary. It never invokes a shell or package manager. Stable installations select stable releases by default; prerelease installations continue on their prerelease channel, and --prerelease opts a stable installation into that channel. --check performs no write. The command requires write permission for the installed executable's directory, so package-manager-managed installs should continue to be updated by their package manager.
- Product definition and roadmap
- Technical design
- Human and agent output contract
- Agent-brief JSON schema
- JSON report schema
- JSON error schema
- JSON comparison schema
- Evidence plugin protocol
- Evidence plugin schema
NetWhy requires Rust 1.88 or newer. Stable Rust is recommended. Supported build targets are Linux and aarch64-apple-darwin; Intel macOS is intentionally unsupported.
cargo build
make test-unit # library, binary, and CLI parser unit tests
make test-integration # in-process direct/proxy DNS/TCP/TLS/HTTP pipeline tests
make test-cli # compiled commands, schemas, plugins, exit codes, and contexts
make test # the complete automated Rust test suiteRun formatting, the complete test suite, and strict Clippy checks with:
make checkThe complete offline release gate is one command:
make verifymake verify runs make check, enforces coverage, tests with Rust 1.88, builds and verifies the Cargo package, validates Debian/RPM/Homebrew packaging templates, and exercises staged installation, --help, --version, and uninstallation. It requires the Rust 1.88 toolchain and cargo-llvm-cov; it does not require a public network service.
The network-backed RustSec advisory audit is enforced separately in CI and before tagged releases. Run it locally with cargo audit --file Cargo.lock after installing cargo-audit.
Measure coverage with cargo-llvm-cov. The checked target requires at least 90% line coverage, 90% region coverage, and 95% function coverage:
rustup component add llvm-tools-preview
cargo install cargo-llvm-cov --locked
make coverageGenerate a browsable report under target/llvm-cov/html with make coverage-html.
Live smoke tests remain optional because they depend on the current network:
cargo run -- https://example.com
cargo run -- --json does-not-exist.invalid
cargo run -- --json --pid "$PID" https://example.com
cargo run -- --json --docker my-container https://example.com
cargo run -- --json --podman my-container https://example.com
cargo run -- --proxy-mode environment https://example.com
cargo run -- report --redaction strict https://example.comInstall directly from the working tree with Cargo:
cargo install --path . --lockedOr install the verified release binary under ~/.local/bin:
make installPREFIX and DESTDIR are supported for staged or system packaging:
make install DESTDIR=/tmp/netwhy-package PREFIX=/usr
make uninstall DESTDIR=/tmp/netwhy-package PREFIX=/usrBuild a local Cargo package without requiring a commit:
make packageTagged Linux releases also build Debian and RPM packages; Apple Silicon releases include a generated Homebrew formula. make test-packaging validates normal and prerelease template substitution without requiring package-builder tools.
Generate a completion file with, for example, netwhy completions zsh or netwhy completions bash. Install the output using the completion directory documented by your shell or package manager.
An installed release binary can update itself without manually downloading an archive:
netwhy update
netwhy update --checkSelf-update supports the published Linux x86_64 and Apple Silicon macOS archives. It will not downgrade, and it needs write access to the installed executable's directory. Use your system package manager, Homebrew, or Cargo to update installations those tools manage.
Set the package version in Cargo.toml, update Cargo.lock, run make verify, and push the release commit. Then create and push a matching tag:
git tag -a v<VERSION> -m "NetWhy v<VERSION>"
git push origin v<VERSION>The release workflow requires the tag to exactly match the package version. It runs the complete Linux release gate and native Apple Silicon tests before creating or updating the GitHub Release for that tag with these assets:
netwhy-v<VERSION>-x86_64-unknown-linux-gnu.tar.gzand its SHA-256 checksum;netwhy-v<VERSION>-aarch64-apple-darwin.tar.gzand its SHA-256 checksum;- an amd64 Debian package, x86_64 RPM, and package checksum file;
netwhy.rb, a Homebrew formula for the Apple Silicon archive.
Each archive contains the release binary, README.md, and LICENSE. Rerunning the workflow replaces matching assets, while a prerelease package version such as 0.2.0-rc.1 creates a prerelease.
See the historical v0.1 release checklist and the v0.2 release-qualification checklist for the exact verification contracts.
MIT