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.

What this is, and what it is not

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.

Nothing here can hurt you. What you carry out of here can

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.

This page needs JavaScript. Without it you get the diagram below and nothing else, because the whole point of the page is an engine that recomputes the drawing when you change something. The topology is real either way: three machines behind three NATs, a tailnet in the middle, and a public address on 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 MACHINES THE PUBLIC INTERNET — anyone, from anywhere, to 203.0.113.11 NAT NAT THE TAILNET coordination server DERP relay · fra direct · punched lab-roam the phone's stand-in 100.71.4.27 lab-ubuntu the laptop 100.71.4.13 lab-vps the public box 100.71.4.11 lab-ubuntu → lab-vps:22

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.

RungThe questionWhat 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
Rung 3 and rung 4 are the pair worth internalising

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.

RealModelledAbsent
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
The thing a model can never do

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 :22 and 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 netcheck still reporting UDP: true with udp/41641 dropped, and understood which question it was answering.
  • Reproduced the Docker UFW bypass and scanned :8080 from 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 :22 and 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 sshd to 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.