Files
tailscale/ipn/ipnlocal/cert_state.go
T
Brad FitzpatrickandBrad Fitzpatrick 4bb6f35c1f ipn/ipnlocal: consolidate test-only LocalBackend methods behind ForTest
Move all the FooForTest methods on LocalBackend to instead be
methods on a new unexported forTest type which is then given out
to callers in other packages via an exported ForTest method
(panicking in non-test contexts) that returns that unexported type.

This is unusual style (exported returning unexported) but declutters
godoc and makes call sites both more explicit and easier to read
without the "ForTest" suffix polluting the symbols. Now FooForTest()
changes into ForTest().Foo().

This was motivated by a pending change moving a bunch of code out of
LocalBackend into other packages that required adding more ForTest
methods to LocalBackend to keep the tests (now in other packages)
working. Instead, do this refactor now so the future change is prettier.

Updates #12614
Updates #cleanup

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ib25e6d76d48dc8622ac3a955e0b1220d582e63a8
2026-06-27 16:11:42 -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 [forTest.ConfigureCerts]. 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
}