Troubleshooting and FAQ
Behaviour that surprises people, and the questions that come up most. Where an answer depends on an exact flag or default, it points at the command’s own help rather than repeating it.
netdoc
A row says WARN. Is that a failure?
No. Warn never counts as a failure and never changes the exit code. It means “this worked, but something about it is worth knowing”: high latency, one address family down, resolvers disagreeing, an ambiguous source interface, a bulk write that went unacknowledged.
If you are scripting, key on the verdict field or the exit code, not on the
absence of ! rows.
Why is a row N/A instead of failing?
N/A means the question does not apply, and it is deliberately distinct from
SKIP:
- N/A: there is nothing to ask. DNS against an IP literal. The proxy row
with no proxy configured, or with
NO_PROXYexempting the probe host. The public-DNS row when outbound DNS is unavailable. - SKIP: we could not ask. A prerequisite failed, so this probe never ran.
The Path MTU row also reports N/A when the peer hung up first: the result is genuinely inconclusive, and the probe will not guess.
Why does ping work but netdoc says the network is broken? (or vice versa)
Because no verdict depends on ICMP, on purpose. Plenty of healthy hosts drop
ping, so a failed ping proves nothing, and a successful one proves less than
a TCP connect, since ICMP and TCP can take different paths and hit different
filters.
Round-trip time comes from the TCP connect handshake instead. ping is available
as a drill-down tool, where it is evidence for you to interpret rather than an
input to the verdict.
The QUIC row fails but everything else works
TCP/443 works, UDP/443 does not, so something on the path filters QUIC. The verdict
is degraded, not broken, because browsers and applications fall back to TCP
automatically. The symptom is usually slower page starts, not an outage.
A QUIC timeout says only that this endpoint did not complete the UDP/443 exchange. It is not proof that a firewall was responsible, and Network Doctor does not claim otherwise.
Encrypted DNS fails but dig works fine
These are separate network capabilities, which is why they are separate rows hanging off the interface rather than one under the other. A network can carry ordinary port-53 DNS while blocking DoH and DoT.
This is the shape of “my terminal resolves fine but my browser cannot”, since modern browsers try encrypted DNS first.
The row never falls back to port 53, since falling back would answer a different
question. And note what it actually proves: a correlated DNS exchange over DoH
or DoT. NOERROR and NXDOMAIN both pass even with no answer records. A
standard-query error such as SERVFAIL or REFUSED warns, because the
resolver was reached but did not complete the query, which is never diagnosed as a
network block.
The DNS rows disagree with each other
Split-horizon DNS or filtering. On a VPN or a corporate network this is usually correct and deliberate, which is exactly why it is a Warn and never a Fail. It becomes interesting only when you did not expect it.
--public-dns and privacy
The second-opinion row queries a fixed public resolver directly. If a strict egress policy or a privacy requirement means that must not happen:
netdoc --public-dns "" host
The row is then absent from the TUI and from the JSON, and no query is sent:
it is not a skipped row, it does not exist. --public-dns <ip> points it at a
different resolver instead.
It takes an IP literal only. A hostname is rejected with exit 2, because resolving it would go through the very resolver the row exists to cross-check.
Note the scope: this flag governs only the DNS second opinion. The egress rows still reach fixed anycast endpoints and the captive-portal endpoint, and the encrypted-DNS row still reaches its own. See what it contacts.
Everything below the first row is nonsense
Check whether you are behind a captive portal. This is tested ahead of everything else for exactly this reason: behind a portal, every layer below is answering for the portal, so nothing further down the stack means what it says. Sign in and rerun.
The row includes the sign-in URL when the intercepting response supplied a valid one. The app displays it but never opens it.
The Path MTU row warned. What do I actually do?
That is the classic tunnel / VPN / PPPoE fault: a path MTU smaller than your interface’s, on a path that also filters the ICMP that would say so. TCP connects, then dies as soon as anyone sends a real packet.
The fix is almost always to lower the MSS or MTU on the tunnel interface. The row names the evidence and suggests the experiment. Background: Path MTU without root.
On Windows this probe can miss a black hole. Windows exposes no way to query the socket’s outstanding send queue, so it falls back to inferring delivery from the send buffer, and the row says so. The TLS/HTTP timeouts beside it still show up.
A tool hotkey does nothing
The toolbox greys out tools whose binary is missing and shows an install hint.
Common cases: mtr on macOS comes from Homebrew; nmap is never preinstalled;
traceroute is a separate package on some minimal Linux images.
Two tools, the nmap port scan (n) and the LAN map (v), are gated behind an
explicit confirmation, because unlike everything else they actively probe other
hosts. The LAN scan runs host discovery without raw sockets or root and caps its
scope at the source address’s /24.
--iface did not change what a drill-down tool did
Not every tool can be bound, and Network Doctor refuses to fake it. Some are left unbound on purpose:
digandnslookupquery the system resolver (often a loopback stub), not the target.- The
nmapconnect scan’s-Sis a spoofing option, not documented connect-scan binding. - Local-state tools (
ip route,ss,netstat,route print) open no sockets at all.
Where binding is supported, Linux and macOS tools use the interface name and
Windows tools bind by source address, with the caveat that ping and tracert
on Windows can do so only for IPv6 destinations, and curl.exe cannot bind by
interface name at all. The README’s
Usage section
has the full matrix.
A dual-stack selection with no other signal is left unbound rather than guessed.
go install gave me a command called network-doctor
go install names the binary after the module. Rename it to netdoc if you like;
everything else is identical. Packages install it as netdoc already.
SSH login (S) will not accept my password on Windows
The password handoff needs OpenSSH_for_Windows 8.6p1 or newer. Network Doctor
checks ssh -V before connecting; an older or unrecognised client leaves the form
open with an explanation rather than launching ssh with a password it would
ignore.
Leave the password field blank on that client to have ssh ask on the terminal.
SSH login refuses to answer a prompt
By design. Forcing askpass routes every prompt to the helper, so the helper answers only password and passphrase prompts and refuses the rest.
Three cases you will hit:
- First connection to an unknown host.
sshwants you to verify the host key. Run once with the password field blank, which puts that question back on your terminal where it belongs. - A
ProxyJumpin yourssh_config. The wholesshsubtree inherits the askpass setting, so the jump host’ssshasks the helper too. Prompts naming a host are answered only for the target; prompts naming nobody are refused outright on a proxied connection, because the wording is no evidence: keyboard-interactive text is written by the far end, so a jump host can call its question a passphrase. ssh -Gfails. With no resolved config there is no way to tell whose prompt is whose, so password-assisted login is refused outright and the form says so.
Also worth knowing what the password handoff does and does not buy: the secret
stays out of argv and out of your shell history, but not out of memory. It is
copied into ssh’s environment, where it stays until ssh exits and is inherited
by the whole subtree ssh starts. On Linux that environment is readable through
/proc by your own processes and by root.
Can I clear the saved target history?
--no-history turns it off for one run, so the file is neither read nor written,
so what you type stays in that session. It deliberately leaves an existing file
untouched: exit netdoc and delete it to clear what is already saved.
The location is under your platform’s config directory (~/.config/netdoc/history
on Linux by default); the README’s
Usage section
has all three paths.
Should I run netdoc in a container?
No. Diagnosing your own machine’s network from inside a container would diagnose
the container. netdoc needs no container on any platform.
The container image exists for netdoc-sim, which is a different situation
entirely; see below.
netdoc-sim
There is no netdoc-sim on my macOS / Windows install
Correct, and intended. The simulator’s backend is Linux namespaces and there is
no other one, so those downloads ship netdoc alone.
Run the published image on any Linux container runtime instead. That is the same binary on the same backend, on the Linux kernel your runtime already provides: packaging, not a port. See Simulator Overview.
cannot create the user, network and mount namespaces a simulation runs in
The host or the container refused the namespace. A run that is refused says so and stops rather than falling back to anything.
Three causes, in rough order of likelihood:
| Cause | Fix |
|---|---|
Docker’s default seccomp profile refuses clone(CLONE_NEWUSER) unless the container was configured with CAP_SYS_ADMIN |
Add the capability, or relax the syscall filter directly, and both forms are tested. Podman’s default profile permits it and needs neither. |
AppArmor on Ubuntu/Debian Docker Engine, where the docker-default profile denies mount |
--security-opt apparmor=unconfined |
| Ubuntu 24.04+ restricts unprivileged user namespaces outright | Host policy. The same restriction affects native runs; CI clears it with a sysctl. |
The exact flags are in
docs/simulation.md,
where they are tested against the real image.
Note that netdoc-sim capabilities cannot predict the seccomp case: it reports
host knobs it can read cheaply, and a container’s seccomp profile is not one of
them.
Does the container need --privileged?
No, and neither does anything else on the usual list. No NET_ADMIN, no host
network, no host PID namespace, no bind mounts, no Docker socket, no access to any
host path.
The simulator needs no capability at all: it creates a user namespace and becomes root inside it, and on the host side the kernel gives it nothing. A test proves this by taking every capability away and running a full simulation. The one capability the docs hand out is there for Docker’s seccomp profile, not for the simulation, and the image runs as an unprivileged user so that the capability is not what the work is done with.
--cap-add NET_ADMIN on its own does not even work, which is the clearest
statement of what the requirement actually is.
My run graded the wrong netdoc
Check the resolution order, since the binary is picked once, in the launcher. Two traps:
go run ./cmd/netdoc-simputs the binary in a build cache, so the “siblingnetdoc” step finds nothing. Buildnetdoc-sim, or pass-netdoc.- Build with
CGO_ENABLED=0. A cgo build may resolve through the host’s system resolver instead of the node’s private/etc/resolv.conf, testing the host rather than the simulation.
Every result records the absolute path and the version string that binary printed, so you can always check after the fact. Details: Which netdoc gets run.
The healthy scenario started failing and I did not touch the simulator
You probably changed a fixed probe endpoint in internal/diagnostic. That failure
is intentional: healthy is the canary, and it fails with a false_positive
suggestion naming the stale probe.
The simulation has no internet, so scenarios claim netdoc’s compiled-in public
addresses as node aliases and serve its fixed probe names from simulator DNS. Update
the affected aliases, DNS records, and expectations, then rerun. Do not make the
control tolerant. See
the healthy canary.
My challenge scored no_result
The round was void, not lost. A challenge is scoreable only when the run
completed and cleaned up, netdoc produced a diagnosis, and the answer is
established independently.
The usual cause: the mutation did not actually take effect, so there is no independent evidence and therefore no truth to grade against. A mutation that failed cannot beat Network Doctor.
The other cause: you repaired the fault from inside the challenge shell. Truth is
collected after netdoc runs, so a repaired fault stops being observed.
netdoc-sim challenge exits immediately in a script
A challenge needs a terminal, because a person is being asked a question. A piped stdin is consumed by the shell, so a run without a terminal and without a submission scores a give-up.
For automation, pass -answer <name> or -give-up, which is what -json
requires anyway.
Are challenge results comparable between machines?
Yes, for the puzzle. A challenge id resolves with no state on disk and no network, so the same id is the same broken network on anyone’s machine, natively on Linux or through the image on any host.
Two things are not reproducible from the id, and are marked as session data
rather than challenge data: your elapsed time, and which netdoc build answered
it.
Why is my daily challenge different from my colleague’s?
It should not be, if you played on the same UTC date. The daily is keyed to the UTC calendar date, not your local one, precisely so that two people whose clocks disagree about what day it is still get the same challenge.
Check whether one of you crossed UTC midnight.
Project
Is there a Windows or macOS simulator planned?
No. The backend is Linux namespaces, and the container image is the supported answer for other hosts. That is packaging around the one backend, not a second one.
Is netdoc-sim part of the product?
It is a second binary, not a second product. internal/simulation is imported
by cmd/netdoc-sim alone; none of it links into netdoc. It ships in the Linux
packages so that Challenge Mode is playable without a Go toolchain.
Will you add more fault models / scenarios?
Only for a real bug, a diagnostic blind spot, a reproducible field condition, a regression, or an identified missing network behaviour relevant to Network Doctor. General simulator expansion is explicitly not a project goal; see maintenance scope.
Is the JSON output stable?
Yes. Field names, the status vocabulary (PASS/WARN/FAIL/SKIP/N/A), the
verdict vocabulary, and the probe IDs are stable and safe to script against.
Optional per-check fields are omitted when empty rather than emitted as null,
so write your parser accordingly.
--json --watch emits NDJSON: the same document compacted onto one line, plus a
ts field. One-shot output stays pretty-printed and has no ts.
How do I verify a downloaded release?
Releases carry a signed attestation binding each artifact to the workflow run that
built it: binaries, the source tarball, the .deb/.rpm/.apk packages, and the
Windows .zip. The gh attestation verify invocation is in the README’s
Verify your download
section.
Two caveats it documents: v1.8.4 and earlier predate attestation, and COPR packages are rebuilt on Fedora’s builders, so COPR’s own signature covers those rather than this attestation.
Where do I report a security issue?
Privately, through a GitHub Security Advisory or the contact in SECURITY.md, not a public issue.
Still stuck?
Open an issue with a netdoc --json run attached, your OS and architecture, and
the smallest sequence that reproduces it. See
Development and Contributing
for what makes a report actionable, and check what you paste for private
hostnames and addresses first.
This page has one editable copy: edit it at the source.