Skip to content
C.W.K.
Lesson 01 of 05 · published

The Tailnet Names the Caller

~13 min · authentication, tailscale, wireguard, identity, zero-secrets

Level 0Loose File
0 XP0/43 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

The Question Every Request Has to Answer

Before Delta serves a byte, it has to answer one question: which Mac is asking? Not which user, not which app. Which machine of the family. Every share policy in Track 2 is written in those terms ("the workbench may read", "every Mac in the fleet may write"), so the whole authorization layer stands on one string: the caller's fleet label. Get that string wrong and every policy above it is decoration.

SSH answered the question with keys. Each Mac held a private key, every other Mac held a list of public keys, and somebody had to keep those lists in step across the fleet. Delta was built to stop carrying data over SSH, and it would have been a strange win to bring SSH's key distribution along with it. So Delta asks a different question: who already knows, for certain, which machine sent this packet? On the family's network the answer is WireGuard.

What WireGuard Already Proved

Every family Mac is a node on one Tailscale network, the tailnet. Tailscale is built on WireGuard, and WireGuard has a property that is easy to read past: cryptokey routing. Each peer is configured with a public key and the tailnet addresses it is allowed to use. A packet that arrives through the tunnel claiming a source address can only have been sealed by the private key bound to that address. Spoofing the source address inside the tunnel is not hard; it is impossible without the key.

So when a connection reaches the gateway on this Mac's own tailnet address, from an address in the tailnet's range (100.64.0.0/10, the carrier-grade NAT block of RFC 6598), the source address is already an authenticated fact. Delta does not need to prove it again. It needs to translate it: address to node, node to fleet label.

From Address to Label in Two Lookups

  1. Address to node. tailscale status --json prints Tailscale's own roster: the Self node and every Peer, each with its TailscaleIPs and its DNSName (the MagicDNS name). Delta reads that output into a table from each tailnet address to the node it belongs to.
  2. Node to label. The family registry lists every fleet Mac with its MagicDNS name. Delta lowercases both sides, strips the trailing dot, and keeps only the nodes the registry names. The result is a dictionary from tailnet address to fleet label.

The Roster class keeps that dictionary and refreshes it when it is a minute old. A request from an address that is not in it (a node that joined a moment ago) triggers one extra read, at most once every five seconds, so a burst of unknown callers costs one subprocess, not a hundred.

Three Kinds of Stranger

CallerWhat Delta knowsAnswer
A fleet Mac on the tailnetIts label, from the rosterAdmitted as that label; share policy decides the rest
A tailnet node the registry does not list (a phone, a borrowed laptop)That it is on the tailnet and is not a family Mac403: "tailnet node … is not a registered family Mac"
A listed Mac outside Delta's fleet (the household's Mac)Its labelAdmitted as that label, then refused by every share whose policy does not name its group (lesson 5)

The phone is the interesting row. It holds a perfectly valid WireGuard key; the tunnel trusts it. Delta still refuses it, because membership of the tailnet is not membership of the family. The registry draws that line, and the gateway enforces it per connection.

"I Cannot Tell" Is Not "You Are a Stranger"

The roster comes from a subprocess, and subprocesses fail. Tailscale's CLI can be missing from PATH under launchd, can time out, can print something that is not JSON. The first version of Delta already kept the previous roster when a read failed, and logged a warning. What it could not do was tell the caller. An address the old roster did not hold (or any address at all, when no read had succeeded since the daemon started) got the stranger's answer: 403 not a registered family Mac, to a family Mac, because a CLI was broken.

The 1110-7 review fixed that in two moves. First, a failed read now records why it failed, beside the roster it kept. Second, the gateway tells the two cases apart. When an address is unknown and the last read failed, the answer is unavailable, naming the cause: "this Mac cannot read the tailnet roster (…), so it cannot tell which Mac … is". A stranger gets 403; a Mac Delta cannot currently identify gets 503 and a sentence. The caller sees a failure it can wait out, not a verdict about who it is.

The same review closed a quieter cost. While the CLI was failing, every tailnet request launched it again, each attempt allowed up to ten seconds. Now requests share one attempt, at most one every five seconds, and a caller that arrives while a roster is already in hand does not wait for the next read at all.

Why No Password Ever Appears Here

Look at what this lesson never mentioned: a password, an API key, a key file Delta created, a list someone has to keep in step. The tailnet path has no Delta secret at all. The proof is WireGuard's; the meaning ("this node is the workbench") is the registry's; Delta only joins the two. That is the track's title in its purest form. The next three lessons add secrets back, one at a time, for routes where WireGuard is not underneath, and each one is minted where this lesson's proof already holds.

Code

Delta's identity module: the roster and the gateway's tailnet branch (trimmed excerpt)·python
# delta/identity.py and delta/server.py (trimmed excerpt)

class Roster:
    """Tailscale's authenticated view of the tailnet, mapped onto fleet labels."""

    REFRESH_SECONDS = 60.0
    MISS_REFRESH_SECONDS = 5.0   # also the pause between attempts while the roster cannot be read

    def refresh(self) -> None:
        status = self._loader()                     # tailscale status --json
        by_dns = self._label_by_dns()               # MagicDNS name -> fleet label, from the registry
        by_ip = {}
        me = status.get("Self") or {}
        for node in [me, *(status.get("Peer") or {}).values()]:
            dns = str(node.get("DNSName") or "").lower().rstrip(".")
            label = by_dns.get(dns)
            if not label:
                continue                            # on the tailnet, not in the family
            for ip in node.get("TailscaleIPs") or []:
                by_ip[ip] = label
        with self._lock:
            self._by_ip, self._loaded_at = by_ip, time.monotonic()


def _authenticate(self) -> str:   # Handler method; only the tailnet branch shown
    if self.ctx.kind == "tailnet":
        roster = self.delta.roster
        label = roster.label_for(self.ctx.peer_ip)
        if not label:
            if roster.error:   # who it is cannot be told, which is not the same as a stranger
                raise Unavailable(f"this Mac cannot read the tailnet roster ({roster.error}), so it cannot "
                                  f"tell which Mac {self.ctx.peer_ip} is")
            raise Forbidden(f"tailnet node {self.ctx.peer_ip} is not a registered family Mac")
        self.ctx.label = label
        return label
    ...
Your own roster: tailnet address to node name, from tailscale status --json·python
#!/usr/bin/env python3
"""Print the tailnet roster the way Delta reads it: address -> MagicDNS name.

Run on any Mac with the Tailscale CLI. Pass the names you consider family;
every other node is listed as a stranger.
"""
import ipaddress
import json
import os
import shutil
import subprocess
import sys

TAILNET = ipaddress.ip_network("100.64.0.0/10")   # RFC 6598 shared address space


def tailscale() -> str:
    found = shutil.which("tailscale")
    for candidate in (found, "/Applications/Tailscale.app/Contents/MacOS/Tailscale"):
        if candidate and os.path.exists(candidate):
            return candidate
    sys.exit("no tailscale CLI found")


def roster() -> dict:
    out = subprocess.run([tailscale(), "status", "--json"], capture_output=True,
                         text=True, timeout=10, check=True).stdout
    status = json.loads(out)
    table = {}
    for node in [status.get("Self") or {}, *(status.get("Peer") or {}).values()]:
        name = str(node.get("DNSName") or "").lower().rstrip(".")
        for ip in node.get("TailscaleIPs") or []:
            table[ip] = name
    return table


if __name__ == "__main__":
    family = {n.lower() for n in sys.argv[1:]}
    for ip, name in sorted(roster().items()):
        if ":" in ip or ipaddress.ip_address(ip) not in TAILNET:
            continue                               # keep the IPv4 tailnet addresses
        short = name.split(".")[0]
        verdict = "family" if short in family else "stranger"
        print(f"{ip:<16} {short:<24} {verdict}")

External links

Exercise

Run the roster script above on one of your Macs, passing the short MagicDNS names you consider family. Then check two things by hand: that every node it calls family is one you would let read your files, and what your own server would do today if tailscale status --json failed. Write the error a caller would receive in each of the three cases (family, stranger, roster unreadable).
Hint
To simulate a broken CLI, temporarily put a directory first on PATH that holds a script named tailscale which exits 1. If your server answers that case with the same message as a stranger, you have found the bug this lesson describes.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.