Documentation
¶
Overview ¶
Package daemon is the public library API for running the tailsync synchronization service. Embedders (CLI, Android app wrappers, other hosts) construct a Daemon with New and call Daemon.Run.
The CLI lives in cmd/tailsync. Android/gomobile bind packages should live in the app repository and depend on this package rather than a bind surface here.
Unstable hooks ¶
Config.AfterReconcile, Config.AfterSyncPeers, and Config.AfterNotify exist for in-module tests. They are not a supported production embedder API and may change or be removed without notice.
Index ¶
Constants ¶
const ( DefaultPort = 5960 DefaultScanInterval = 30 * time.Second DefaultSyncInterval = 45 * time.Second // DefaultWatchDebounce is how long to wait after an FS event before // reconciling when filesystem watching is active. DefaultWatchDebounce = time.Second // DefaultBlockSize is the rsync-style delta block size when Config.BlockSize // is zero (4096 bytes). DefaultBlockSize = 4096 // DefaultMaxFileBytes is the max single-file size loaded into memory for // transfer/delta (v1 keeps whole-file buffers). Wire framing is capped // separately so peer messages cannot allocate unboundedly. DefaultMaxFileBytes = 64 << 20 // 64 MiB // DefaultDialTimeout is how long an outbound peer dial/handshake may block // before failing during discovery. DefaultDialTimeout = 30 * time.Second // DefaultTombstoneTTL is how long deletion tombstones are retained before // garbage collection when Config.TombstoneTTL is zero (30 days). DefaultTombstoneTTL = 30 * 24 * time.Hour // DefaultDiscoveryConcurrency caps concurrent discovery dials (in-flight only). DefaultDiscoveryConcurrency = 32 // DefaultPullStreamConcurrency caps concurrent pull streams across peers // (each may buffer up to MaxFileBytes). DefaultPullStreamConcurrency = 8 // DefaultHeartbeatInterval is the app-level ping interval on each session. DefaultHeartbeatInterval = 20 * time.Second )
Default configuration values applied by New when the corresponding Config field is zero. CLI flags and embedder UIs should reference these rather than re-hardcoding.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// Dir is the directory to synchronize (required).
Dir string
// StateDir holds the index and, when NetMode is NetModeTSNet, tsnet state.
// Defaults to Dir/.tailsync.
StateDir string
// Hostname is the protocol / tsnet node name.
// - NetModeTSNet: advertised tsnet hostname (default tailsync-<os-hostname>).
// - NetModeHost: always overwritten from LocalAPI Self (MagicDNS → HostName →
// StableID); any configured value is ignored for protocol identity.
// - NetModePlain: wire-protocol node id when set.
Hostname string
// Port is the UDP port for QUIC peer sessions over the tailnet (or localhost in plain mode).
Port int
// AuthKey is an optional Tailscale auth key for NetModeTSNet
// (else interactive login / existing tsnet state). Unused in host mode.
AuthKey string
// ScanInterval is the safety-net full rescan period. When filesystem
// watching is active, local edits are reconciled via debounced FS events;
// this interval still walks the tree to catch missed events.
ScanInterval time.Duration
// SyncInterval is the backup peer pull period for catch-up (offline peers,
// missed notifies). Local index changes fan out best-effort notifies; peers
// pull on their own. Correctness does not depend on notify delivery.
SyncInterval time.Duration
// WatchDebounce is how long to wait after an FS event before reconciling
// (0 = DefaultWatchDebounce). Ignored when DisableWatch is set or watch
// fails to start.
WatchDebounce time.Duration
// DisableWatch skips filesystem watching and relies on ScanInterval only.
// Useful in tests and on platforms where watching is unavailable.
DisableWatch bool
// BlockSize for rsync-style signatures.
BlockSize int
// MaxFileBytes rejects local files larger than this for serve/pull (0 = default).
MaxFileBytes int64
// DialTimeout is the max wait for an outbound peer QUIC dial+handshake
// during discovery (0 = DefaultDialTimeout).
DialTimeout time.Duration
// DiscoveryConcurrency caps concurrent discovery dials (0 = default 32).
// The semaphore is in-flight only (released before backoff sleep).
DiscoveryConcurrency int
// PullStreamConcurrency caps concurrent content pull streams across all
// peers (0 = default 8). Separate from discovery concurrency.
PullStreamConcurrency int
// HeartbeatInterval is the app-level ping period on each peer session
// (0 = DefaultHeartbeatInterval). QUIC also has its own keep-alive.
HeartbeatInterval time.Duration
// TombstoneTTL drops old deletion tombstones from the index (0 = DefaultTombstoneTTL).
TombstoneTTL time.Duration
// Logger defaults to slog.Default().
Logger *slog.Logger
// NetMode selects networking: host (default), tsnet, or plain. See NetMode constants.
NetMode NetMode
// ListenHost is used when NetMode is NetModePlain (default 127.0.0.1).
ListenHost string
// Peers is an optional explicit list of peer addresses (host:port) for tests
// and overrides. When empty, discovery uses mesh trust on Online status
// peers: untagged Self → same UserID; tagged Self → peer must carry MeshTag
// (excludes self, sharees, Mullvad, and identity mismatches). When set,
// status discovery is skipped (test determinism), but host/tsnet handshakes
// still enforce WhoIs + the same trust policy. Soft dial failures and
// backoff handle nodes that are Online but not running tailsync.
Peers []string
// MeshTag is the ACL tag peers must share when this machine is tagged
// (for example "tag:tailsync"). Required at listen when Self is tagged
// and must appear on Self. Must be empty when Self is untagged. Non-empty
// values are trimmed, lowercased, and checked with Tailscale's tag rules
// at [New] (tag:<letter…>, letters/digits/dashes only).
MeshTag string
// OnReady, if non-nil, is called once after the daemon is listening and before
// the main loop. Used by library hosts so Start/lifecycle wrappers can wait
// for listen success or a fast failure. Must not block indefinitely.
OnReady func()
// OnAuthURL, if non-nil, is called when interactive Tailscale login is needed
// during NetModeTSNet bring-up and an auth/login URL is available (for example
// browser login when AuthKey is empty and no enrolled tsnet state exists).
// Invoked from a background goroutine while Up is still waiting. Called at
// most once per distinct URL per listen attempt. Must return quickly.
// Not used for host or plain modes. Never receives AuthKey material.
OnAuthURL func(url string)
// AfterReconcile, if non-nil, is called after each successful reconcile with
// whether peer-visible local index content changed.
//
// Unstable / test-only: not a supported production API; may change or be
// removed without notice. Must return quickly and must not call back into
// the daemon.
AfterReconcile func(changed bool)
// AfterSyncPeers, if non-nil, is called after each pullPeers batch completes
// (including empty peer lists).
//
// Unstable / test-only: not a supported production API; may change or be
// removed without notice. Must return quickly and must not call back into
// the daemon.
AfterSyncPeers func()
// AfterNotify, if non-nil, is called when scheduleNotify actually schedules
// at least one notify goroutine (not when deduped or no candidates; not
// after each peer dial finishes).
//
// Unstable / test-only: not a supported production API; may change or be
// removed without notice. Must return quickly and must not call back into
// the daemon.
AfterNotify func()
}
Config holds daemon configuration.
type Daemon ¶
type Daemon struct {
// contains filtered or unexported fields
}
Daemon is the synchronization service.
Locking:
- syncMu serializes multi-step local reconcile and remote apply (decide → optional network → re-check LWW → disk/index commit). Concurrent peer applies may run network I/O in parallel while unlocked; only decide and commit hold syncMu. The main Run loop keeps reconcile→notify responsive; pull batches run on a single-flight worker (pullWG) and do not block notify scheduling. Notify fan-out is fire-and-forget and does not hold syncMu.
- index.Index has its own RWMutex for map access. Holding syncMu does not replace index locks; index methods still lock internally. Callers that need a stable multi-step view of the index relative to disk must hold syncMu around the whole operation (see reconcile, applyRemote).
func (*Daemon) InjectNetworkChange ¶
func (d *Daemon) InjectNetworkChange()
InjectNetworkChange signals tsnet's netmon that host connectivity changed (Android ConnectivityManager updates). No-op when not in tsnet mode, before Up succeeds, or after network backend teardown. Safe concurrent with Daemon.Run and Run shutdown (context cancel): copies the inject func under netMu, then invokes it outside the lock.
func (*Daemon) Run ¶
Run starts the daemon until ctx is cancelled.
Main loop roles:
- FS watch (debounced) and ScanInterval both request reconcile
- successful reconcile with peer-visible changes fans out best-effort notifies on persistent peer sessions (fire-and-forget)
- SyncInterval and inbound notifies schedule pull on a single-flight background worker so long pull batches do not delay reconcile→notify
- peer discovery and session accept run in the peer manager
type NetMode ¶
type NetMode int
NetMode selects how the daemon attaches to the network.
const ( // NetModeHost uses the system tailscaled via LocalAPI (default). // Traffic stays on the host Tailscale identity; no extra node is registered. NetModeHost NetMode = iota // NetModeTSNet runs an embedded tsnet node (registers as a separate machine). NetModeTSNet // NetModePlain uses plain QUIC (UDP) on ListenHost (tests only). NetModePlain )