#!/bin/bash # ============================================================================= # machine-setup — Tailscale # ============================================================================= # # Definitions only, like the other lib/ files. # # ── Why this runs early ── # # It is a second way into the machine. The section that can lock you out is SSH # hardening, and everything after this one can break networking in some smaller # way; having the tailnet up first means a mistake is recoverable rather than a # trip to a rescue console. # # ── Why it matters to Officer specifically ── # # The platform's CLAUDE.md is explicit: the perimeter IS the tailnet. # ALLOW_ANY_ORIGIN defaults ON, and that is only defensible because the machine is # not reachable from the open internet in the first place — a valid token plus the # tailnet is the lock. An Officer install with no tailnet is an Officer install # with one fewer layer than it was designed around. # # ── Why the original hung ── # # It passed --authkey unconditionally, and its prompt accepted an empty answer. # `tailscale up --authkey ""` falls back to interactive login: it prints a URL and # blocks, with no timeout, forever. Nothing here passes an empty key, every call # has a timeout, and the state is read before anything is run. [[ -n "${MACHINE_SETUP_TAILSCALE_LOADED:-}" ]] && return 0 MACHINE_SETUP_TAILSCALE_LOADED=1 TS_EXIT_SYSCTL=/etc/sysctl.d/99-tailscale-exit.conf TS_DISPATCHER=/etc/networkd-dispatcher/routable.d/50-tailscale-exit # Printed only when asked for. The section leads with the question rather than # with ten lines of explanation: somebody who runs Tailscale already does not need # to be told what it is, and somebody who does not can type ?. tailscale_help() { echo " Tailscale is a private network between your own machines, over" echo " WireGuard. Every device you enrol gets a stable 100.x address and" echo " can reach every other, wherever they are — through NAT, across" echo " providers, without either end having a public address." echo "" echo " Nothing is published to the open internet to make that work: no" echo " port forwarding, no exposed ports, no holes in the firewall." echo "" echo " For Officer it is not a convenience. The platform is built assuming" echo " the tailnet IS the perimeter — ALLOW_ANY_ORIGIN defaults on, and" echo " that is only defensible because the machine is not reachable from" echo " outside in the first place. A valid token plus the tailnet is the" echo " lock; without the tailnet it is one layer short of its design." echo "" echo " It is installed at this point in the run, before anything that can" echo " lock you out of the machine, so there is always a second way in." } # The menu itself, in a function because it is shown twice — once to ask, and # again after ? has printed the long answer, so the reader is not dropped back at # a bare prompt having forgotten what the options were. tailscale_network_menu() { info "Which network should this machine join?" echo "" echo " [1] set up your own network — offscale" echo " Your own coordination server, here on this machine. The protocol" echo " on the wire is Tailscale's and the encryption is WireGuard's;" echo " offscale changes neither — it runs headscale's open-source code." echo " What changes is the work: one command to install, certificates" echo " included, managed from an app rather than a terminal, and" echo " enrolling a device is a link and a tap." echo " https://officer.dev/infrastructure/offscale.html" echo "" echo " [2] use a network you already run — headscale or offscale" echo " You already have a coordination server somewhere. Point this" echo " machine at it and it joins that network alongside the rest." echo "" echo " [3] the easy route — tailscale.com" echo " Tailscale runs the coordination for you. Nothing to host and" echo " nothing to maintain, free for personal use; the trade is that" echo " the list of your machines lives with them." echo "" echo " [?] what are tailscale, headscale and offscale?" echo "" } # The long answer, printed when somebody types ?. Covers all three names, # because the menu offers all three and two of them are not words anyone outside # this project would know. tailscale_networks_help() { echo " Tailscale, headscale and offscale are three answers to one question:" echo " who keeps the list of your machines and hands out the keys they use" echo " to find each other." echo "" echo " The network itself is the same in all three cases. Machines talk" echo " directly to each other over WireGuard, encrypted end to end. What" echo " differs is only the coordination server — the thing that knows which" echo " machines are yours. It never carries your traffic." echo "" echo " TAILSCALE" echo " The company's own coordination server. Nothing to run, nothing to" echo " maintain, free for personal use. You sign in with an existing" echo " identity and your machines appear in their admin console." echo " The trade is that the list of your machines lives with them." echo "" echo " HEADSCALE" echo " An open-source coordination server you run yourself. The same" echo " Tailscale clients connect to it, so the machines behave identically;" echo " the difference is that nobody else holds the list. The cost is that" echo " it is now a service you host, and it needs to be reachable." echo "" echo " OFFSCALE" echo " Our own distribution of headscale, which is to say: headscale. The" echo " protocol on the wire is Tailscale's and the encryption is" echo " WireGuard's, and offscale changes neither — it runs the same" echo " open-source project. A machine on an offscale network behaves" echo " exactly as it would on either of the other two. There is no offscale" echo " protocol to be locked into, because there is no offscale protocol." echo "" echo " Clients: stock Tailscale on computers. On iPhone, iPad and Android" echo " there is our own app — the Tailscale client, our branding, and one" echo " real difference: it takes an invite from the server directly. That" echo " is the part of running headscale people give up at, because the" echo " official app has to be talked into using a server that is not" echo " Tailscale's. Desktop apps of our own are not there yet; on a" echo " computer you point the official client at your own server." echo "" echo " What it does that plain headscale does not:" echo " · installs in one command, with the certificates handled" echo " · health, logs, restarts and access policies from the app," echo " instead of a config file and a CLI" echo " · enrolling a device is a link and a tap — the key is minted" echo " and handed over for you" echo " · several networks at once, and services reachable across them" echo "" echo " https://officer.dev/infrastructure/offscale.html" echo "" echo " FOR OFFICER" echo " Whichever you pick, the tailnet is what Officer treats as its" echo " perimeter. ALLOW_ANY_ORIGIN defaults on, and that is only" echo " defensible because the machine is not reachable from the open" echo " internet in the first place. Installed at this point in the run," echo " before anything that can lock you out, so there is always a second" echo " way in." echo "" echo " Officer also administers it. Its Headscale app talks to headscale" echo " and offscale servers alike: register as many as you run, see which" echo " are actually up — each is probed, not remembered — and switch" echo " between them. On whichever is active you get the nodes, the users," echo " the pre-auth keys, the invites and the ACL policy, with an" echo " assistant for writing it, plus a console and diagnostics. So the" echo " server this section sets up is managed from the same place as" echo " everything else on this machine, rather than over ssh and a CLI." } tailscale_is_installed() { command -v tailscale &>/dev/null; } # NeedsLogin, Running, Stopped, NoState… Read before acting, because the original's # failure was running `up` blindly against a node that was already up. tailscale_state() { tailscale status --json 2>/dev/null | awk -F'"' '/"BackendState"/ { print $4; exit }' } tailscale_ip() { tailscale ip -4 2>/dev/null | head -1; } # Which control plane this node is talking to. Empty means Tailscale's own. tailscale_control_url() { tailscale debug prefs 2>/dev/null | awk -F'"' '/"ControlURL"/ { print $4; exit }' } tailscale_install() { curl -fsSL https://tailscale.com/install.sh | sh; } # Routing has to be on before this machine can forward anyone else's packets, # whether as an exit node or as a subnet router. Written as a drop-in so it is # visible as this script's doing. enable_ip_forwarding() { cat >"$TS_EXIT_SYSCTL" <<'EOF' # Written by machine-setup: required to forward traffic for other tailnet nodes, # as an exit node or as a subnet router. net.ipv4.ip_forward = 1 net.ipv6.conf.all.forwarding = 1 EOF sysctl --system >/dev/null 2>&1 } # UDP GRO forwarding, which Tailscale documents as roughly doubling throughput on # a node that forwards for others. Applied on every routable event rather than # once, because the settings are per-interface and do not survive the link going # down and back up. install_exit_node_tuning() { pkg_is_installed networkd-dispatcher || pkg_install_now networkd-dispatcher mkdir -p "$(dirname "$TS_DISPATCHER")" cat >"$TS_DISPATCHER" <<'EOF' #!/usr/bin/env bash # Written by machine-setup. NIC offload settings for a Tailscale exit node or # subnet router — Tailscale's own recommendation for forwarding throughput. set -Eeuo pipefail IF="${IFACE:-}" if [[ -z "${IF}" ]]; then IF="$(ip -o route get 8.8.8.8 2>/dev/null | awk '{for (i = 1; i <= NF; i++) if ($i == "dev") {print $(i + 1); exit}}')" fi [[ -n "${IF}" ]] || exit 0 command -v ethtool >/dev/null 2>&1 || exit 0 ethtool -k "${IF}" 2>/dev/null | grep -q "^generic-receive-offload: " && ethtool -K "${IF}" gro on || true ethtool -k "${IF}" 2>/dev/null | grep -q "^rx-udp-gro-forwarding: " && ethtool -K "${IF}" rx-udp-gro-forwarding on || true ethtool -k "${IF}" 2>/dev/null | grep -q "^large-receive-offload: " && ethtool -K "${IF}" lro off || true exit 0 EOF chmod 755 "$TS_DISPATCHER" systemctl enable --now networkd-dispatcher >/dev/null 2>&1 || true # And once now, for the interface that is already up. IFACE="$(default_iface)" bash "$TS_DISPATCHER" >/dev/null 2>&1 || true } # The LAN this machine sits on, as a CIDR — the useful default for a subnet # router, and the number nobody remembers offhand. lan_cidr() { local iface iface="$(default_iface)" ip -4 route show dev "$iface" 2>/dev/null | awk '$1 ~ /\// && $1 !~ /^default/ { print $1; exit }' }