tsnet

package
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 8, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package tsnet wraps the upstream tailscale.com/tsnet library so the bridge can run as its own embedded tailnet node — no external `tailscaled` daemon, no CLI shell-out, no on-disk Let's Encrypt cert files.

The bridge's tailscale integration has two paths:

  • mode=cli (default, internal/tailscale): shells out to the `tailscale` CLI for endpoint detection (`tailscale status --json`) and LE cert minting (`tailscale cert <magicDNS>`). Requires a host-level tailscaled and `tailscale` in $PATH; LE cert/key files are written to disk for the SNI cert switcher to load. This is the historical flow.

  • mode=tsnet (this package): the bridge process IS the tailnet node. tsnet.Server.ListenTLS terminates LE in-process, so there's no on-disk cert material at all and no SNI switcher is needed. State (machine identity) persists under <dataDir>/tailscale/.

The two paths converge on the same internal/api mux. Operators opt into tsnet via `tailscale.mode: tsnet` in bridge.yaml; the CLI path stays default until the tsnet path soaks for at least one release.

Lifecycle is owned by cmd/bridge/main.go: construct a *Server, call Start (blocks until tailnet-reachable; surfaces an interactive AuthURL on first run when no AuthKey is supplied), call ListenTLS to get the second http listener for *.ts.net connections, and call Close on shutdown to drain magicsock / netcheck / control-plane goroutines (without Close, integration tests would leak goroutines per spawn).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// AuthKey, if non-empty, is used on first run. Empty triggers
	// interactive OAuth (Server.Start prints the AuthURL via the
	// logger and blocks until the operator authorizes in a browser).
	// Once tsnet has persisted state, this field is ignored.
	//
	// Precedence at startup (Tailscale-standard idiom):
	//   1. TS_AUTHKEY environment variable (preferred — keeps
	//      secrets out of yaml-on-disk)
	//   2. This field (fallback for ops who can't set env vars)
	//   3. Empty → triggers interactive OAuth on first run
	//
	// See startUnlocked for the resolution code.
	AuthKey string
	// Hostname is the magic-DNS hostname tsnet registers with.
	// Empty falls back to the deviceName / library name supplied
	// by the caller via this field.
	Hostname string
	// StateDir is the on-disk location of tsnet's state store.
	// MUST be set explicitly — empty causes tsnet to fall into
	// ephemeral mode (re-registers as a new device on every restart,
	// surprises the operator's admin console). Typically
	// <dataDir>/tailscale/, mode 0700 on POSIX.
	StateDir string
	// Logger receives both UserLogf (operator-facing AuthURL +
	// status) and Logf (verbose backend). The two channels are
	// adapted into different slog levels — see logfFromSlog and
	// noisyLogfFromSlog.
	Logger *slog.Logger
}

Config carries everything NewServer needs. Mode is unused here — the dispatcher in cmd/bridge/main.go branches on it before constructing a *Server. Logger MUST be non-nil.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server wraps tailscale.com/tsnet.Server with the bridge-specific lifecycle (interactive auth, slog-routed logging, secure state-dir guard, idempotent Start/Close). Construct with NewServer; call Start once to bring the node up; call Close on shutdown.

Concurrency model: Start and Close are caller-serialized in production (cmd/bridge/main.go single-threaded). Read paths (Status, ListenTLS, CertDomains, AuthURL) AND the userLogf callback (invoked from tsnet's internal goroutines during the Up() phase) need cheap, non-blocking access — so the lock surface is split:

  • lifecycleMu protects started + server. Start releases this lock during the long Up() I/O so userLogf can take authMu without deadlocking. Close holds it through the upstream Close() call (~ms).
  • authMu protects authURL only. userLogf takes it for ~µs per write; AuthURL() takes it for one read. Doesn't block on Up() I/O.

Caller-side: Start and Close are NOT safe to call concurrently. The wrapper detects double-Start (second caller sees the in-progress flag and returns an error) but doesn't try to be fully reentrant — cmd/bridge/main.go is the sole caller and runs them serially.

func NewServer

func NewServer(cfg Config) (*Server, error)

NewServer validates the configuration and constructs an unstarted Server. The actual tailnet-up dance happens in Start.

func (*Server) AuthURL

func (s *Server) AuthURL() string

AuthURL returns the most recent interactive-auth URL captured from UserLogf, or empty if none was emitted. Used by `bridge tsnet auth` to echo the URL explicitly to the operator's terminal in case the slog output was filtered or piped elsewhere.

Read path is on a separate mutex from the lifecycle state so AuthURL() polls during a long Up() don't block on Start.

func (*Server) CertDomains

func (s *Server) CertDomains() []string

CertDomains returns the magic-DNS names tsnet will serve LE certs for. Empty slice before Start.

func (*Server) Close

func (s *Server) Close() error

Close tears down the embedded tsnet.Server. MUST be called on bridge shutdown to drain magicsock / netcheck / control-plane goroutines — without it, every Start/Close cycle (e.g. integration tests) leaks goroutines until process exit.

func (*Server) ListenTLS

func (s *Server) ListenTLS(addr string) (net.Listener, error)

ListenTLS returns an HTTPS listener with auto-renewing Let's Encrypt certs. Backed by tsnet's in-process ACME — there's no on-disk cert material. addr should be ":443" or ":7798"; the network is always "tcp".

func (*Server) Start

func (s *Server) Start(ctx context.Context) error

Start brings the tailnet node up. Idempotent: a second Start on an already-started server is a no-op (returns nil). A concurrent Start while another is in flight returns an error rather than racing — cmd/bridge/main.go calls Start serially, so this is a programmer-error guard, not a performance path.

First-run behaviour: if no AuthKey is configured AND no persisted state exists, tsnet emits an AuthURL via UserLogf — Start blocks until the operator opens the URL and authorizes the device.

Subsequent runs (state persisted) re-authenticate from the state store and return as soon as the tailnet is reachable.

func (*Server) Status

func (s *Server) Status(ctx context.Context) (*ipnstate.Status, error)

Status returns the live tailnet view (peer reachability, magic-DNS hostname, assigned IPs). Returned shape is the upstream ipnstate.Status so admin templates that already parse `tailscale status --json` output don't need a translation layer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL