Documentation
¶
Overview ¶
Package tailscale wraps the small surface area of the Tailscale CLI the bridge needs for HTTPS auto-pilot: detecting whether Tailscale is installed and what the local node's MagicDNS name is, and minting an HTTPS certificate via `tailscale cert`. Deliberately CLI-based (not LocalAPI) so the bridge keeps a small dependency footprint and runs fine on hosts without Tailscale installed at all.
Test seam: `commandContext` defaults to `exec.CommandContext`. Tests inject a fake to drive specific stdout/stderr/exit-code shapes without spawning real processes. Production code MUST NOT mutate it.
Index ¶
Constants ¶
const FreshnessThreshold = 14 * 24 * time.Hour
FreshnessThreshold is how long before notAfter the renewer treats the cert as "due for re-mint". Tailscale's LE certs are 90-day; 14 days of headroom matches Let's Encrypt's typical renewal window with safety margin so a single failed renew tick still leaves another 13 days to recover.
Variables ¶
var ErrCLINotFound = errors.New("tailscale: CLI binary not found")
ErrCLINotFound surfaces "tailscale binary not on $PATH and not at the macOS App Store fallback path". Caller-distinguishable via errors.Is.
var ErrHTTPSCertsDisabled = errors.New("tailscale: HTTPS Certificates not enabled in tailnet")
ErrHTTPSCertsDisabled is the typed error returned when MintCert detects the tailnet hasn't enabled HTTPS Certificates. The admin console renders distinct guidance for this case.
var ErrPermission = errors.New("tailscale: permission denied talking to local daemon (Linux: add the running user to the tailscale group, or run `sudo tailscale cert <magicdns>` once manually)")
ErrPermission is the typed error returned when the local tailscaled socket refuses connections from the running user (Linux gotcha).
Functions ¶
func EnsureCertDir ¶
EnsureCertDir creates the <dataDir>/tls/ directory at 0o700. Idempotent. Caller invokes this once before MintCert so the cert/key write target directory exists. Mirrors the convention internal/backup uses for ensuring its <dataDir>/backups/ root.
**Follow-up `os.Chmod` is load-bearing** (Qodo on PR #102): `MkdirAll` only sets the mode at directory-create time. If the directory was created earlier (manually, by a prior bridge build at a more permissive mode, or by a test fixture) `MkdirAll` returns nil without modifying the existing mode. The Chmod follow-up forces 0o700 regardless of pre-existing mode — these files include the Tailscale private key and must stay owner-only.
func LECertPaths ¶
LECertPaths returns the canonical on-disk paths for the LE cert + key. Fixed filenames (NOT magicDNS-keyed) so a tailnet/host rename or reinstallation doesn't leave orphan files in dataDir; matches the `server.crt` / `server.key` convention. Lives under <dataDir>/tls/ (created 0o700 by the caller).
func MintCert ¶
MintCert exec's `tailscale cert --cert-file=<certPath> --key-file=<keyPath> <magicDNS>`. Returns nil on success, a typed error on the well-known failure modes:
- ErrHTTPSCertsDisabled — the tailnet hasn't enabled the HTTPS Certificates feature in the admin console. Operator must visit login.tailscale.com/admin/dns and toggle it on.
- ErrPermission — the local tailscaled socket refuses connections from the running user (Linux-specific; user needs to be in the `tailscale` group or run with sudo).
- generic error — any other failure (network, rate-limit, etc.). Log the message and surface in the admin tile.
Idempotent: running it on a host that already has a fresh cert just re-mints (Tailscale serves a cached LE response if one is current). Caller is responsible for deciding whether to re-mint based on the existing cert's notAfter.
Types ¶
type NodeInfo ¶
type NodeInfo struct {
// CLIAvailable is true when a runnable `tailscale` binary was found.
CLIAvailable bool
// BinaryPath is the resolved absolute path to the tailscale binary.
// Empty when CLIAvailable is false. Stored so subsequent calls
// (MintCert, status refreshes) can use the exact same binary the
// detection picked, avoiding a TOCTOU race against $PATH changes
// or the App Store install moving.
BinaryPath string
// NodeName is the local node's short name (e.g. "home-pc").
// Sourced from `tailscale status --json`'s `Self.HostName`.
NodeName string
// MagicDNSName is the fully-qualified MagicDNS hostname
// (e.g. "home-pc.sable-eagle.ts.net"). Empty when MagicDNS is
// disabled in the tailnet.
MagicDNSName string
// TailnetSuffix is the bare tailnet suffix (e.g. "sable-eagle.ts.net"),
// used by the SNI cert switcher to decide which connections route
// to the LE cert vs the self-signed cert.
TailnetSuffix string
// TailscaleIPs are the local node's tailnet-assigned addresses
// (CGNAT 100.x IPv4 + ULA fd7a:115c:a1e0::/48 IPv6) parsed from
// `tailscale status --json`'s `Self.TailscaleIPs` array. Surfaced
// so the admin `TailscaleProvider` adapter can carry them into
// `admin.TailscaleStatus.TailscaleIPs` for `/v1/health.endpoints`
// advertising. Empty when MagicDNS is off OR the node isn't
// joined to a tailnet.
TailscaleIPs []string
// LastError is the human-readable error message from the most-recent
// failed CLI invocation, empty on success. Surfaced verbatim in the
// admin tile.
LastError string
}
NodeInfo describes the local Tailscale node, populated by Detect.
The zero value (CLIAvailable=false) is the "Tailscale not installed" state — every getter / consumer treats that as a soft "skip" condition, not an error. Tailscale not being installed is a perfectly valid state for hosts that pair iOS clients only over LAN.
func Detect ¶
Detect runs `which tailscale` (with the macOS App Store fallback) followed by `tailscale status --json` and returns a populated NodeInfo.
CLI-not-found is NOT an error — it returns NodeInfo{CLIAvailable: false} with nil err. A genuine error (CLI present but `status` fails, JSON can't be parsed, MagicDNS suffix can't be derived) returns a non-nil err so the caller can log at .error level.