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
85 lines
3.1 KiB
Go
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
|
|
}
|