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
63 lines
1.9 KiB
Go
63 lines
1.9 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package acme registers the ACME/TLS-cert feature and implements its
|
|
// associated [ipnext.Extension]. The extension owns the per-LocalBackend
|
|
// state previously held in package-level globals and on LocalBackend
|
|
// fields (ACME serialization mutex, in-flight cert tracking, etc.).
|
|
//
|
|
// The cert code that runs against this state still lives in
|
|
// [tailscale.com/ipn/ipnlocal]; this extension simply owns the state
|
|
// and installs a hook so cert.go can find it from a *LocalBackend.
|
|
package acme
|
|
|
|
import (
|
|
"tailscale.com/feature"
|
|
"tailscale.com/ipn/ipnext"
|
|
"tailscale.com/ipn/ipnlocal"
|
|
"tailscale.com/types/logger"
|
|
)
|
|
|
|
// featureName is the name of the feature implemented by this package.
|
|
const featureName = "acme"
|
|
|
|
func init() {
|
|
feature.Register(featureName)
|
|
ipnext.RegisterExtension(featureName, newExtension)
|
|
ipnlocal.HookCertState.Set(certStateFor)
|
|
}
|
|
|
|
func newExtension(logf logger.Logf, _ ipnext.SafeBackend) (ipnext.Extension, error) {
|
|
return &extension{
|
|
state: new(ipnlocal.CertState),
|
|
logf: logger.WithPrefix(logf, featureName+": "),
|
|
}, nil
|
|
}
|
|
|
|
// extension is an [ipnext.Extension] that owns the per-LocalBackend
|
|
// ACME/cert state. Most of the cert logic still lives in ipnlocal;
|
|
// this extension exists to give that state a non-global home.
|
|
type extension struct {
|
|
state *ipnlocal.CertState
|
|
logf logger.Logf
|
|
}
|
|
|
|
// Name implements [ipnext.Extension].
|
|
func (e *extension) Name() string { return featureName }
|
|
|
|
// Init implements [ipnext.Extension].
|
|
func (e *extension) Init(ipnext.Host) error { return nil }
|
|
|
|
// Shutdown implements [ipnext.Extension].
|
|
func (e *extension) Shutdown() error { return nil }
|
|
|
|
// certStateFor returns the [ipnlocal.CertState] owned by the acme
|
|
// extension registered on b, or nil if none.
|
|
func certStateFor(b *ipnlocal.LocalBackend) *ipnlocal.CertState {
|
|
e, ok := ipnlocal.GetExt[*extension](b)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return e.state
|
|
}
|