Ten lines of prose before the first prompt assumed the reader had never heard of Tailscale. Anyone already running it does not need to be told what it is, and having to scroll past it every run is the cost of writing for the other reader. The prompt comes first now, and `?` is an answer. Typing it prints the full description and asks again; not typing it costs nothing. confirm() takes an optional help function as its third argument. Where one is given the prompt becomes [Y/n/?], so the explanation announces that it is available without taking up room. The same hook is there for any other section that wants it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
131 lines
5.6 KiB
Bash
131 lines
5.6 KiB
Bash
#!/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."
|
|
}
|
|
|
|
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 }'
|
|
}
|