Challenge Mode
Challenge Mode drops you into a deliberately broken network without telling you what is wrong. You investigate it with the tools you would normally use, commit to a diagnosis, and then Network Doctor takes a shot at the exact same problem. Both answers are graded against the simulator’s independently observed ground truth.
It is the hunt with the contestants swapped: instead of the oracle grading
netdoc alone, a person diagnoses the network first, and both answers are judged
against the same evidence.
Playing your first one
On macOS, Windows, or Linux, one container image is the whole install: no
clone, no Go toolchain, no namespace knowledge, and nothing on your real network
is touched. The exact image name and run flags are in the README’s
Challenge Mode section
and in
docs/simulation.md;
both are tested against the real image, which is why they are not duplicated here.
That is the real Linux namespace simulator running inside a Linux container, not a macOS or Windows imitation of it.
On Linux, any package installs netdoc-sim and it runs natively:
netdoc-sim challenge # draw one and play it
That single command with no arguments is the whole of what a first-time player needs.
The session
One command runs the entire session, and that is a deliberate design choice: a
simulated network only exists while the process holding its namespaces does.
start / shell / diagnose subcommands would need a daemon and a state file,
and would make it possible for you and netdoc to be handed different
networks.
- The briefing. Names the machine you are standing on and the host you were asked about.
- The shell. You land inside the broken machine. Use
ping,dig,curl,ip route,ss,traceroute,nc, whatever you would reach for on a real call-out. - The menu. Type
exitin the shell to reach it. Pick your diagnosis by number or type it by name.breprints the briefing,sreturns to the shell,qgives up. - The reveal. Ground truth, the evidence behind it, your answer, Network Doctor’s answer, how long you took, and who got it right.
The target host is real
The briefing names a host like invoices-8f42c1.test. It is a genuine part of
the simulated network: the node’s resolver answers it, whatever serves it presents
a certificate for it, and it is the target the graded netdoc run is handed.
.test is reserved by RFC 6761, so
nothing about it can reach public DNS or the internet.
In the shell it is also in the environment, as $NETDOC_CHALLENGE_TARGET and
$NETDOC_CHALLENGE_HOST.
The name is derived from the challenge id and nothing else, so replaying an
id presents the same host, and the name tells you nothing about which fault was
set. It deliberately carries no challenge label, because the target is passed to
netdoc as its ordinary argument and the simulator does not hand that process a
token saying a challenge is happening.
Answers
Answers are picked by number or typed by name, and the names are the ones on the
menu, so nothing has to be memorised. Matching folds case, spaces, hyphens, and
underscores together, so "TCP port blocked", tcp_port_blocked, and blocked
are one answer.
There is no fuzzy matching, on purpose: an answer selected by resemblance is a diagnosis nobody committed to.
The answer vocabulary is printed by the menu, so it is not duplicated anywhere, including here.
Ways to draw a challenge
netdoc-sim challenge # draw one at random
netdoc-sim challenge -daily # today's, the same for everybody
netdoc-sim challenge -starter fundamentals # a curated one to learn on
netdoc-sim challenge -authored no-default-route # a hand-written case
netdoc-sim challenge -id <ID> # replay the one a friend sent you
netdoc-sim challenge -difficulty hard
netdoc-sim starters lists the curated packs and what each teaches;
netdoc-sim authored lists the hand-written cases and their ids. Those commands
are the inventory, and this page names no ids of its own, because an id printed in a
wiki page cannot be checked by the repository’s tests and would go stale silently.
Starter packs
A pack is a curated list of ordinary challenge ids and nothing else. There is no second simulator, no second generator, and no second scoring path: every entry resolves through the same builder, plays on the same namespaces, is graded by the same oracle, and is replayable by its own id.
A pack names the layer you are practising, which is a hint you asked for. Two design rules keep it a hint rather than an answer key:
- Every pack holds at least two possible answers.
- One entry per pack is a network with nothing wrong with it.
That second rule is why there is no DNS pack: the answer vocabulary has exactly
one DNS entry on purpose, so a “DNS” pack would be the answer. DNS is practised
inside fundamentals, where it is one of four things it could be.
netdoc-sim starters <pack> prints a pack’s ids in order, so it can be worked
through deliberately. -starter draws one at random, because the command keeps no
record of what you have played; a progress file would be the wrong trade for a
game whose appeal is leaving nothing behind.
Authored challenges
A generated challenge is a draw: an id seeds a search over base scenarios and case numbers. To teach a particular fault that way you would scan ids until one fell out, which is a poor way to write a lesson, because you end up teaching whichever case the search reached first.
An authored challenge names its base scenario and its fault directly. Everything after that choice is shared: the fault is produced by the ordinary mutation operator against the ordinary base scenario, truth is established by the same predicate, and scoring is the same function. There is no second correctness path, and nothing hand-writes a node name, port, or address that could drift out of step with the topology.
They come in deliberately confusable pairs (the port that answers versus the
port that says nothing, an expired certificate versus one for the wrong name) so
netdoc-sim authored is the fastest way to practise a distinction you keep
getting wrong. Each line says what telling it apart requires, never what it is.
The daily
netdoc-sim challenge -daily # today, UTC
netdoc-sim challenge -daily=2026-03-04 # a particular day
Everybody who plays today gets the same broken network, so results are comparable.
The date is the UTC calendar date, not your local one, because two people whose clocks disagree about what day it is have to get the same challenge, and a daily keyed to local midnight would give them different ones.
The mapping is a pure function of that date: no server, no account, no network, no filesystem, and no clock beyond the date itself. It is derivable offline on any machine.
The daily resolves to an ordinary challenge id, and that id is the artefact: it
replays forever, with no -daily and no dependence on today still being that day.
A result played as the daily carries the date, which is what makes two people’s
results comparable as the same day’s puzzle.
-daily refuses to be combined with anything that would pick a different
challenge (an explicit id, -difficulty, -starter) by name, rather than by
inventing a precedence. Silently honouring one of the two would hand somebody a
result they would post as the day’s challenge when it was not.
Challenge ids
An id looks like V4- followed by six hex digits: a generator version and a
case. It resolves with no state on disk and no network, so the same id is the
same puzzle on anyone’s machine, natively on Linux, or through the image on any
host.
The version is part of the id because it has to be. What an id means depends on the selection rules, the hunt generator behind them, and the base scenario YAML they draw from, so a change to any of the three would repoint every id already shared. Such a change adds a new version instead, leaving old ids resolving through the rules they were minted under. Each earlier version keeps its own frozen condition list and its own generator version.
A1- is the authored namespace. It is an id version like any other and parses,
resolves, replays, and shares identically; its digits name a case somebody wrote
rather than a seed to search from.
Run
netdoc-sim challengeornetdoc-sim authoredto get current ids. The current generation is stated in the README and the man page, which are checked byTestCurrentChallengeExamplesUseCurrentGeneration.
Why V4 picks differently
Worth knowing if you played early ids and wondered why you kept getting DNS.
V1 to V3 drew a base scenario and a case number, let the generator produce whatever mutation it produced, and retried until one was challenge-capable. That works, and it quietly let three implementation details decide what the game was about: how many mutation variants a family happened to have, how many base scenarios its operator applied to, and which case the scan reached first.
Measured across 4000 V3 ids, that put DNS at 23.4% and a missing subnet route at 1.6%, a sixteenfold spread nobody chose, in a game whose value is practising the rare ones.
V4 picks the answer first, uniformly over the playable vocabulary, and only then searches for a base and case number that express it. The search still rejects, but it can no longer decide what the game is about: rejection changes how long an id takes to resolve, never which diagnosis it resolves to. The same measurement puts every family between 5.9% and 7.1%, with a healthy network at one draw in six as before.
Uniform over answers rather than over mutations is the deliberate choice. A player who diagnoses “DNS resolution” has named one thing; weighting by mutation would hand out twice as much of it for a reason internal to the simulator.
How scoring works
Each contestant is graded separately against the observed truth, and the
match-up is derived from the two scores. There is no partial credit and no scoring
on prose: you pick from a fixed menu, and netdoc is read through its own cause
vocabulary, structured per-family verdicts, and verdict class.
Network Doctor’s score is one of three:
| Score | Meaning |
|---|---|
correct |
Its own report states the condition the simulator observed. It wins the round unless you also named it, which is a draw. |
incorrect |
It has a verdict for this condition and produced a different one. |
unrecognized |
Its vocabulary has no verdict that could state this condition, so no report it could have written would have won. |
Both of the last two lose the round. They are kept separate because the difference is the entire point of the contract:
“Network Doctor did not recognize this fault” is a challenger victory, not a reason the challenge could not be set.
Why netdoc never gets to see the answer
Challenge Mode asks one question: can the simulator produce a real, verified network fault that Network Doctor fails to diagnose? That only means anything if the set of possible challenges is decided without reference to what Network Doctor can already do.
The pipeline is one-directional and never loops back:
graph TD
A["Simulator injects a fault"] --> B["Independent evidence proves<br/>it reached live traffic"]
B --> C["Observed truth names<br/>the real condition"]
C --> D["Network Doctor produces<br/>its diagnosis"]
D --> E["The judge compares<br/>the diagnosis with the truth"]
A fault becomes challengeable when it passes four tests:
- Independently observable: the executed run left evidence, read off the wire or off the kernel, that the fault met live traffic.
- Deterministic and replayable: the same id sets the same puzzle, and the condition holds for the whole run or not at all.
- The same network for both contestants: you in the shell and the
netdocprocess must be able to see the same thing. - Inside the diagnostic scope: a fault of the network itself, rather than of an application the network delivered correctly.
“Network Doctor already recognizes it” is deliberately not on that list, and
nothing in the generation path can reach the recognition table or a diagnosis. A
condition netdoc has no vocabulary for is still eligible, and it is a loss for
netdoc, not a challenge that could never be set.
What “ground truth” means here
Nothing Network Doctor produces. Truth comes from the scenario state, the
applied mutation, and observations the simulator collected for itself from
inside the node namespaces: service records, kernel counters, routing tables read
back with ip route show, and the simulator’s own TCP dials.
A mutation having been generated, applied, or expected to break something establishes nothing on its own. It becomes truth only when the executed run left independent evidence that it met live traffic.
A consequence worth knowing: a mutation that failed to take effect cannot beat Network Doctor. With no independent evidence there is no truth to grade against, and the round is void rather than won.
Your time
The result reports how long you took: it starts when the briefing has been printed
and you can begin, and stops when your answer is accepted. Building the
namespaces, injecting the fault, netdoc’s own run, and tearing everything down
are all outside it, so it measures you rather than the host.
Rereading the briefing, going back to the shell, and retyping a mistaken answer are all inside it, because they are part of solving it.
It is measured on a monotonic clock and is deliberately not part of the
match-up, because netdoc is automated and a person is not. Nothing about it reaches
challenge generation, the id, the truth, or replay.
Sharing a result
The session ends with a block you can paste anywhere. It carries the challenge id, the date if it was a daily, and two check marks, and it never names the fault, so sharing your result does not spoil the puzzle for whoever plays it next.
A starter pack’s name is deliberately kept out of it too, since a pack names a layer and would narrow the answer for the next player.
The briefing prints the id, the difficulty, the node, and the host. The base
scenario, seed, case number, and mutation are the answer, so they appear only
in the reveal, and a test asserts the briefing contains none of them. Rereading
the briefing with b goes through the same renderer, so there is no second, more
generous version of it to drift.
-v, a JSON report, or reading the source obviously defeats all of this. It is a game, not a security boundary.
Requirements and cleanup
Exactly what any other simulator run needs, the Linux namespace backend and no root, plus a terminal, because a person is being asked a question. On macOS or Windows the Linux part comes from a container runtime.
The shell enters the node through the same nsenter argument slice the netdoc
run uses, gains no privilege the simulator did not already have, and is given the
simulator’s trust anchors so a generated certificate verifies for you exactly as
it does for netdoc.
Nothing survives the command. A challenge never keeps its simulation: the namespaces go when the director exits, the workspace is removed on every exit path including an abandoned session, and no state record is written at all. No account, no server, no leaderboard.
Editing the network from inside the challenge shell is possible and is its own
punishment: truth is collected after netdoc runs, so a repaired fault stops
being observed and the challenge scores no_result.
For automation
-answer or -give-up submits without opening a shell, which is what -json
requires: a piped stdin is consumed by the shell, so a challenge run without a
terminal and without a submission scores a give-up. -json stays parseable on
stdout because the session prints to stderr.
A machine-readable result keeps the stable identifier and the display name side
by side, so a script keys on answer and a human reads label.
Three parts of a result describe the session rather than the challenge, and a
replay of the same id will not reproduce them: the timing, which netdoc build
answered it, and the daily date. Everything else is determined by the id and the
network.
Extending it
Adding a new playable diagnosis, or a new authored case, is a documented workflow
with no hidden allowlists: the healthy oracle, the playable answer set, and V4
generation all derive from the condition row you add rather than repeating it.
The steps, the tests that will catch a mistake, and the reasoning are in
docs/simulation.md,
beside the code.
Where next
- Hunts and Triage: the same machinery without the human.
- Simulator Overview: what is underneath all of this.
- Understanding Your Diagnosis: useful practice
material for beating
netdoc.
This page has one editable copy: edit it at the source.