Network Doctor

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_PROXY exempting 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:

  • dig and nslookup query the system resolver (often a loopback stub), not the target.
  • The nmap connect scan’s -S is 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. ssh wants 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 ProxyJump in your ssh_config. The whole ssh subtree inherits the askpass setting, so the jump host’s ssh asks 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 -G fails. 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-sim puts the binary in a build cache, so the “sibling netdoc” step finds nothing. Build netdoc-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.