Chapter 14 · Prove it All machines
The sandbox
Every other diagram in this guide is a story someone wrote down: on step three, this happens. This one has no steps. It has machines, a network, a policy, and an engine that works out the consequence of whatever you do to them. Close a port, kill a link, leak a key, put a hostile machine on the wire — and read what actually happens, at which rung, with real numbers.
This is a model of the topology in this guide, running in your browser.
Nothing here is a container and no packet leaves your machine. What it gives you is
the thing a diagram cannot: a consequence you did not read in advance. What it cannot
give you is a surprise — it only knows what has been modelled, and the real world does
not agree to be modelled. When you want the real thing, everything you do here can be
exported as a script and run against the three VMs in
chapter 13, or you can run the same topology as containers with
docker compose up in lab/, which
has the same eleven checks and the same panels as this page — with one exception.
Tailcat — the escape hatch, at the bottom of the controls, has no counterpart
over there yet: a tunnel needs a relay to find its far end, and that lab is deliberately
offline. Until it grows one, that panel is the one place here you cannot go and confirm
against real containers.
Inside this page there is no risk at all — no packet leaves your browser and no machine
exists to break. The script tab is the exception, because it hands you real commands
that delete firewall rules and rewrite sshd_config. Run those on a machine you
rely on, in the wrong order, and you will lock yourself out of it.
A green result here is a statement about a model, not a promise about your network. Your versions, your provider and your topology are not these. Practise on the throwaway VMs in chapter 13, keep a proven second way in, and read each line before you run it. Everything in this guide is provided as is with no warranty and you accept the risk of using it — the full terms are in the disclaimer and the CC BY-SA 4.0 licence.
lab-vps that anyone in the world can reach.
Everything the console would let you prove is also written out longhand in
chapter 13, as commands you can run yourself.
The height of the line is the point. One that runs straight across the middle never leaves the tailnet. One that bends up through the box in the centre is being relayed through DERP. One that dips into the red band at the bottom has crossed the public internet, where anyone can reach it — that shape is the difference between a private path and an exposed one.
Three machines, a default-deny tailnet, and public :22 still open.
Configurations
Four states, each one somebody's real machine. Load one, then audit it.
Eleven checks, the same eleven every time — including two that ask whether you can still get in, because a configuration that locks you out is not secure, it is broken.
Probe
One connection, walked down the ladder rung by rung.
Attacks
Each one sets up whatever it needs, runs, and tells you which defence answered it.
Not an attack — nothing here is hostile and nothing is defending. This one is
the session layer rather than the network layer, and it runs at
lab-vps's public address on purpose.
Load a configuration above, or build your own with the switches, then press Audit this configuration. Eleven checks, scored, with every other configuration alongside for comparison.
Paste it at the three VMs from chapter 13 and you get the same outcome on real kernels.
Checking what this browser will let us encrypt with…
How to read a failure
The engine never answers yes or no. It answers which rung, because that is the only answer you can act on. These are the same five rungs chapter 12 teaches, in the same order, and every probe stops at exactly one of them.
| Rung | The question | What a failure here means |
|---|---|---|
| 1 | Is the machine up, and is anything leaving it? | Nothing left the machine at all — a dead link or a stopped box. Nothing else belongs here |
| 2 | Is there a path, and is it the tailnet or the ordinary network? | The two ends could not find each other at all. A key nobody signed, or one that expired, is answered here too: such a machine is not on the tailnet, so what is left is the ordinary network |
| 3 | Does the tailnet policy allow it? | Refused before a packet reached the far machine. Its firewall never saw anything |
| 4 | Does the host firewall allow it? | The packet arrived and the box dropped it. Or — the interesting case — Docker let it in first |
| 5 | Is anything listening, on an address this path reaches? | Everything permitted it and nothing answered. Usually a bind address, not a rule |
They are two firewalls, owned by different people, and they fail differently. A rung-3
denial happens in the tailnet — the target machine is not involved and has no log line to
show you. A rung-4 denial happens on the box, where ufw can tell you about it.
When something is unreachable and the far end swears it saw nothing, that asymmetry is
usually the reason.
Nine things worth doing here first
The console will let you do anything in any order. These nine are the ones that change how you think, roughly in the order they are worth doing. The last one is the odd one out: it is not an attack, and it is about the session layer rather than the network.
1 · Close port 22 while you are still logged in
Under The host, turn off allow 22/tcp from anywhere. Then run the
ssh from the laptop probe: still fine, over the tailnet. Then run
the attacker, to :22: gone. That is the whole of
chapter 08 in two clicks, and it is the move people
postpone for weeks on a real VPS.
2 · Now do it in the wrong order
Press Run the build order wrong. It removes the public rule and the
tailscale0 rule, which is the mistake that reads as harmless right up until
you press return. Both probes fail. On a real box that is a console session and an
apologetic email. Chapter 11's build order exists
entirely to stop this, and it is much cheaper to feel it here.
3 · Block UDP and watch the path change
In The network, set udp/41641 to dropped on
lab-ubuntu. The chip in the middle of the diagram flips from
direct to relay "fra" and the round-trip time in the packet log
roughly quadruples. Nothing else changed. This is what a hotel network does to you,
and chapter 01 is about why it still works at all.
Then read tailscale netcheck in the status tab, because it says
something you might not expect: UDP: true, with WireGuard's port on the floor.
That is what the real command does. It probes UDP reachability using STUN on port 3478,
which you did not block, so it reports the truth about UDP and says nothing about 41641.
The path is what moved, and tailscale status is where you read it. This page
printed UDP: false for months, until the containers in
lab/ ran the same command for real and did not
agree.
4 · Publish a Docker port
Press Publish a Docker port. UFW is default-deny, says so in the rules tab,
and :8080 answers a scan from the open internet regardless — because Docker
writes into a chain that is consulted before UFW's. If you take one thing from this page,
take this one; it is the trap most likely to be sitting on a machine you already own.
5 · Capture the traffic, then try to read it
Press Sit on the wire and capture. evil-box moves onto
lab-ubuntu's segment and captures a frame — and the hex in the packet log is
real ciphertext, sealed under a shared secret derived from real keypairs generated in this
tab. Then it tries to open it with its own key and fails, and the error you are shown is
the one the cipher actually threw. Nobody wrote that outcome down; it is what happened.
6 · Leak a node key, and watch the wrong door open
Press Join with a stolen node key from the boot state, with tailnet lock on.
Lock works: nobody with a signing key vouched for that node, so it gets no tailnet session
and the ACL is never consulted. Then read the rung. It is 5, and the address in the
packet log is 203.0.113.11 — the public one. evil-box did not need
the tailnet. Refusing its key put it exactly where any stranger on the internet stands, and
from there :22 still answers.
Now turn off allow 22/tcp from anywhere in The host and press it again. Rung 4, nothing delivered. That is the pair working: tailnet lock decides who is a member, and the firewall decides who can reach the machine. Neither is a substitute for the other, and a page that stopped a refused key at rung 1 was quietly claiming the first did the second's job. Turn lock off instead and the key becomes a login on the tailnet too, with only the policy file in the way — chapter 10 is about which of those worlds you are living in.
7 · Let a key expire
Press Let a key expire. lab-roam drops out of the tailnet on its own,
which is the whole point of expiry — the phone left in a taxi stops being a member whether
or not anyone remembers to remove it. From the boot state it then reaches sshd
anyway, at rung 5, for the same reason as above: it stopped being a member, and a
non-member is a stranger, and strangers can reach an open :22. Close the public
rule and run it again to see expiry actually buy you something. Expiry ends membership; it
does not close a port.
8 · Try to reach the laptop from the internet
Load Day one — no tailnet at all — then probe from evil-box to
lab-ubuntu on :22. It stops at rung 2, and the reason is not a
firewall: nat-ubuntu masquerades the laptop outbound and forwards nothing in,
so there is no address for a stranger to aim at. Probe lab-vps instead and
you are straight through to sshd, because that one does have a public
address. The difference between those two machines is the whole reason
chapter 08 exists.
Worth knowing which of the two you have been picturing — and worth noticing that it lets nothing off the hook. The moment the laptop joins a tailnet it is reachable by every other member, which is what the two hostile member checks in the audit are watching.
9 · Watch a blackout fail to kill an SSH session
Press ssh and mosh, through a 20-second outage. Two sessions open from
lab-roam to lab-vps's public address — deliberately not
the tailnet, for a reason in a moment — and then the demonstration does two things to
them, in this order.
Act one takes eth0 away for twenty seconds and gives it back. Both
sessions survive, and if that surprises you, it is the most useful surprise on this page.
TCP does not give up on a stalled connection anywhere near that fast: it retransmits with
backoff, and Linux's default gives it about fifteen minutes before it stops trying. A
tunnel, a lift or a dead spot is not what ends your session.
Act two leaves eth0 up and changes lab-roam's address
instead — 10.0.27.2 to 10.0.27.77, which is what a phone moving
from Wi-Fi to a tower actually does. SSH stops dead: a TCP connection is a four-tuple, and
one corner of it no longer exists. Mosh carries on counting, because it is not holding a
connection to lose — a datagram is genuine because it decrypts, not because it came from an
expected address. That is chapter 03, and the two acts
together are the reason it is a chapter rather than a paragraph.
Now the address it ran at. Over the tailnet both sessions survive both acts, because the
tailnet address does not change when the network under it does — which is
chapter 01 earning its keep, and is a different lesson from
the one act two is teaching. So the demonstration aims at the public address, exactly as
the containers in lab/ do. If your configuration
has already closed public :22, it will say so and stop, which is
chapter 08 answering rather than a broken button.
What is real here, and what is modelled
A model that oversells itself is worse than no model. Here is the honest split.
| Real | Modelled | Absent |
|---|---|---|
| The keypairs, the ECDH shared secret and the AES-GCM seal. The failed decrypt is a genuine cipher rejection in your browser | Path selection, NAT behaviour, the ACL evaluator, the firewall ordering including Docker's bypass, the loss and latency arithmetic, and the tick counts in the outage demonstration — which are computed from its schedule, where lab/ reads the same numbers off two real sessions |
Any actual packet, kernel, container or daemon. Nothing is transmitted and nothing listens |
| The generated commands — each is the real command for the change you made | The anti-replay counter, and the shape of tailscale status and netcheck output |
Anything Tailscale changes after August 2026. A model cannot learn |
It cannot surprise you. Every outcome here was put in by someone who already understood
the system, which makes this an excellent way to learn the shape of the thing and
a poor way to discover that your assumptions are wrong. For that you need real
kernels — the three VMs in chapter 13,
lab/ in this repository, or a machine you
are willing to break. Use the script tab: it turns everything you did here into
the commands that do it for real.
That is not hypothetical. lab/ runs this same topology as containers, scores
the same eleven checks, and has twice caught this page being wrong: it gave every machine
a public address when a laptop behind a NAT has none, and it printed
netcheck: UDP: false for a blocked port that netcheck does not
even look at. Both were corrected here afterwards. Once it went the other way: this page
worked out that a refused node key was stopping traffic that had nothing to do with the
tailnet, the containers turned out to be scoring the same check just as kindly, and it
was the lab that changed. Which direction a gap points is not decided in advance, and
that is rather the point of having both.
Its README keeps the findings, the fixes, and the one
remaining disagreement, which is a genuine difference between Headscale and Tailscale
rather than a modelling error.
Checklist
- Closed public
:22and confirmed the tailnet route still works. - Ran the build order wrong on purpose, and read which rung caught it.
- Blocked UDP and watched the path fall back to
relay. - Read
tailscale netcheckstill reportingUDP: truewithudp/41641dropped, and understood which question it was answering. - Reproduced the Docker UFW bypass and scanned
:8080from outside. - Captured a frame and watched the decrypt fail with a real cipher error.
- Tried a stolen node key with tailnet lock on, and read the rung it actually
stopped at — then closed public
:22and tried it again. - Turned a default-deny ACL into an allow-all one and seen what membership then buys.
- Let a key expire, watched the node fall out on its own, and noticed that falling out of the tailnet is not the same as being kept off the machine.
- Bound
sshdto the tailnet address and seen a rung-5 failure that no firewall rule explains. - Exported the script and read it against chapter 13.
- Probed the laptop from outside with no tailnet, and read rung 2 rather than a way in.
- Watched a twenty-second blackout leave both sessions alive, then watched the address change kill one of them — and noticed which of the two events the folklore blames.
The commands in this guide change firewall and login settings, and can lock you out of a machine. Practise on something disposable first. Everything here is provided as is, with no warranty — you accept the risk of running it. Read the disclaimer.