@@ -163,9 +163,22 @@ struct CachedIdentity {
163163 expires_at : Instant ,
164164}
165165
166+ /// Abstraction over the Tailscale LocalAPI, enabling test doubles.
167+ ///
168+ /// The production implementation ([`LocalApiWhoisProvider`]) contacts the
169+ /// real Tailscale daemon over its Unix socket. Tests use `FakeWhoisProvider`
170+ /// to inject pre-canned responses without needing a running Tailscale daemon.
166171pub ( crate ) trait TailscaleWhoisProvider : Send + Sync {
172+ /// Verify that the provider is reachable.
173+ ///
174+ /// Called once at server startup via [`AuthContext::verify`] to surface
175+ /// misconfigurations early (e.g., wrong socket path, daemon not running).
167176 fn verify ( & self ) -> BoxFuture < ' _ , Result < ( ) , AuthError > > ;
168177
178+ /// Resolve the identity of the given peer address.
179+ ///
180+ /// Returns [`AuthError::Unauthorized`] for non-tailnet addresses and
181+ /// [`AuthError::ProviderUnavailable`] if the daemon cannot be reached.
169182 fn whois (
170183 & self ,
171184 address : SocketAddr ,
@@ -225,6 +238,12 @@ impl From<TsWhoisResponse> for TailscaleWhoisResponse {
225238}
226239
227240// ── Direct Unix-socket HTTP client for the Tailscale LocalAPI ────────
241+ //
242+ // Tailscale does not bind a TCP port for its LocalAPI — it only listens on
243+ // a Unix domain socket. This provider opens a new connection for every
244+ // request (no connection pooling) because whois calls are rare and caching
245+ // makes per-request cost negligible. Each call issues one HTTP/1.1 GET
246+ // and reads the full response body before closing the socket.
228247
229248#[ derive( Clone ) ]
230249struct LocalApiWhoisProvider {
@@ -322,6 +341,31 @@ impl TailscaleWhoisProvider for LocalApiWhoisProvider {
322341}
323342
324343/// Request authentication state shared by the HTTP and gRPC frontends.
344+ ///
345+ /// `AuthContext` is cheaply `Clone`d (it wraps `Arc` internally) and intended
346+ /// to be inserted as Axum router state and tonic service state.
347+ ///
348+ /// Resolved identities are cached by peer IP for [`cache_ttl`] to avoid a
349+ /// Unix socket round-trip on every request. A cache hit requires only an
350+ /// `RwLock` read — no I/O. The cache is never actively evicted; stale entries
351+ /// are ignored on the next lookup if their TTL has expired.
352+ ///
353+ /// # Configuration examples
354+ ///
355+ /// ```rust,ignore
356+ /// // Local development — no auth required
357+ /// let auth = AuthContext::no_auth();
358+ ///
359+ /// // Tailscale (production default)
360+ /// let auth = AuthContext::tailscale(
361+ /// Role::Read, // default role for untagged nodes
362+ /// "/run/tailscale/tailscaled.sock",
363+ /// Duration::from_secs(60), // identity cache TTL
364+ /// );
365+ ///
366+ /// // Guest mode — unauthenticated callers get read access
367+ /// let auth = AuthContext::guest(Role::Read);
368+ /// ```
325369#[ derive( Clone ) ]
326370pub struct AuthContext {
327371 mode : AuthMode ,
@@ -373,6 +417,13 @@ impl AuthContext {
373417 & self . mode
374418 }
375419
420+ /// Verify that the auth provider is reachable.
421+ ///
422+ /// Called once at server startup before accepting connections. In
423+ /// `NoAuth` and `Guest` modes this is a no-op. In `Tailscale` mode it
424+ /// issues a test request to the Tailscale daemon so a misconfigured
425+ /// socket path or a stopped daemon surfaces immediately at startup rather
426+ /// than on the first real request.
376427 pub async fn verify ( & self ) -> Result < ( ) , AuthError > {
377428 match & self . provider {
378429 Some ( provider) => provider. verify ( ) . await ,
@@ -493,6 +544,10 @@ pub fn identity_from_tailscale(whois: &TailscaleWhoisResponse, default_role: &Ro
493544 }
494545}
495546
547+ /// Pick the most human-readable name for a Tailscale node.
548+ ///
549+ /// Preference order: `ComputedName` → `Hostinfo.Hostname` →
550+ /// `ComputedNameWithHost` → first label of the FQDN `Name`.
496551fn preferred_node_name ( node : & TsNode ) -> String {
497552 if !node. computed_name . is_empty ( ) {
498553 return node. computed_name . clone ( ) ;
0 commit comments