Files
tailscale/ipn/ipnlocal/cert_state.go
T
Brad FitzpatrickandBrad Fitzpatrick f5eac39ea7 feature/acme, ipn/ipnlocal: start moving ACME/cert state into an extension
The ACME serialization mutex (acmeMu) was a package-level global, and
several ACME-related fields lived on LocalBackend even though the
cert code is conditional and not linked into every binary. With
multiple tsnet.Servers in one process (each its own LocalBackend),
a process-wide acmeMu also serialized unrelated backends.

Introduce a new feature/acme extension that owns the per-LocalBackend
ACME/cert state in an ipnlocal.CertState value:

  - acmeMu, renewMu, renewCertAt (previously package globals)
  - pendingACMETLSALPNCerts, pendingCertDomains{,Mu},
    getCertForTest, certRefreshCancel (previously LocalBackend
    fields, only meaningful when ACME was compiled in)

ipnlocal/cert.go now reaches the state through b.certState(), which
is routed by a feature.Hook installed at init by feature/acme. The
CertState type lives in ipnlocal so cert.go can access its fields
directly without a method explosion; the extension in feature/acme
constructs and owns it.

This is a baby step. The end goal is for the entire cert/ACME code
to live in feature/acme, with ipnlocal only retaining whatever thin
hooks the rest of LocalBackend needs to call into it. The current
split (CertState and most of cert.go in ipnlocal, extension wrapper
in feature/acme) is a deliberately temporary middle ground that
keeps this PR small while making the next moves mechanical.

The package is named feature/acme to match the existing HasACME /
ts_omit_acme naming. condregister/maybe_acme.go wires it in for
non-js builds.

Updates #12614
Updates #20248
Updates #20249

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I520909f24ad11a9622ef33c2290fe36ad44d6f71
2026-06-26 09:48:24 -07:00

85 lines
3.1 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package ipnlocal
import (
"context"
"crypto/tls"
"sync"
"time"
"tailscale.com/feature"
"tailscale.com/syncs"
"tailscale.com/util/set"
)
// CertState holds the per-[LocalBackend] state owned by the
// feature/acme extension. The struct lives in this package so that
// cert.go can access its fields directly without method indirection,
// while the extension in feature/acme remains the canonical owner.
//
// In builds without ACME support (js or ts_omit_acme), no extension
// constructs a CertState, and [LocalBackend.certState] returns nil.
//
// CertState is safe for concurrent use; the individual fields document
// their own synchronization.
//
// TODO(bradfitz): continue moving all this cert code into feature/acme's package.
// This type being here was a compromise to keep the PR small during the move.
type CertState struct {
// acmeMu serializes ACME operations so concurrent requests for
// certs don't slam ACME. The first goroutine through populates the
// on-disk cache and the rest reuse it.
acmeMu syncs.Mutex
// renewMu guards renewCertAt.
// Lock order: acmeMu before renewMu.
renewMu syncs.Mutex
renewCertAt map[string]time.Time // lazily initialized under renewMu
// pendingACMETLSALPNCerts maps SNI names to short-lived ACME
// tls-alpn-01 challenge certificates while an ACME order is
// waiting for validation. Entries are deleted by the cleanup
// function returned from storeACMETLSALPNCert after the challenge
// validation path finishes, whether it succeeds or fails.
pendingACMETLSALPNCerts syncs.Map[string, *tls.Certificate] // "foo.bar.com" => challenge cert
// pendingCertDomains tracks the set of domains for which an ACME
// issuance is currently in flight with no usable cached cert. It
// backs the tls-cert-pending health Warnable.
// Guarded by pendingCertDomainsMu.
pendingCertDomainsMu sync.Mutex
pendingCertDomains set.Set[string]
// getCertForTest is used to retrieve TLS certificates in tests.
// See [LocalBackend.ConfigureCertsForTest]. Guarded by the
// containing [LocalBackend]'s mutex (b.mu).
getCertForTest func(hostname string) (*TLSCertKeyPair, error)
// certRefreshCancel cancels the background TLS cert refresh loop
// that periodically pokes [LocalBackend.GetCertPEM] so renewals
// happen on idle nodes. Guarded by the containing [LocalBackend]'s
// mutex (b.mu). Non-nil while the loop is running.
certRefreshCancel context.CancelFunc
}
// hookCertState is set by the feature/acme extension at init time
// to a function that returns the [CertState] for backend b, or nil
// if the cert extension is not registered (e.g. in builds with
// ts_omit_acme or js).
var hookCertState feature.Hook[func(*LocalBackend) *CertState]
// HookCertState exposes [hookCertState] to the feature/acme package
// for installation. It must be set exactly once at init time.
var HookCertState = &hookCertState
// certState returns the cert state for b, or nil if the cert
// extension is not registered.
func (b *LocalBackend) certState() *CertState {
if f, ok := hookCertState.GetOk(); ok {
return f(b)
}
return nil
}