mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-15 23:42:48 -04:00
LocalBackend.Shutdown waits for the ACME refresh loop and active SSH sessions. Both can be blocked acquiring LocalBackend.mu, so waiting while holding that mutex deadlocks shutdown. Detach the SSH server under the mutex, then stop both subsystems after releasing it. Prevent their work from restarting once shutdown begins, and serialize repeated Shutdown calls with sync.Once. Add regression tests that verify subsystem shutdown runs without LocalBackend.mu held. Updates tailscale/corp#45964 Change-Id: I37ead4f26fbfb5703a83882668d98a8862ba7d67 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
152 lines
5.6 KiB
Go
152 lines
5.6 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package ipnlocal
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"tailscale.com/feature"
|
|
"tailscale.com/ipn"
|
|
)
|
|
|
|
// TLSCertKeyPair is a TLS public and private key, and whether they were
|
|
// obtained from cache or freshly obtained.
|
|
type TLSCertKeyPair struct {
|
|
CertPEM []byte // public key, in PEM form
|
|
KeyPEM []byte // private key, in PEM form
|
|
Cached bool // whether result came from cache
|
|
}
|
|
|
|
// errNoCerts is returned by the wrapper methods below when ACME/cert
|
|
// support is not compiled into this build.
|
|
var errNoCerts = errors.New("cert support not compiled in this build")
|
|
|
|
// Hooks installed by the feature/acme package at init time. In builds
|
|
// without ACME support (js or ts_omit_acme), feature/acme is not linked
|
|
// in and these hooks remain unset; the wrapper methods below then
|
|
// behave as no-ops or return errNoCerts.
|
|
var (
|
|
// HookGetCertPEM implements [LocalBackend.GetCertPEMWithValidity].
|
|
HookGetCertPEM feature.Hook[func(ctx context.Context, b *LocalBackend, domain string, minValidity time.Duration) (*TLSCertKeyPair, error)]
|
|
|
|
// HookGetACMETLSALPNCert returns the ACME tls-alpn-01 challenge
|
|
// certificate for hi, if any.
|
|
HookGetACMETLSALPNCert feature.Hook[func(b *LocalBackend, hi *tls.ClientHelloInfo) (*tls.Certificate, bool)]
|
|
|
|
// HookGetACMETLSALPNProto reports whether the ACME ALPN protocol
|
|
// should be advertised for hi.
|
|
HookGetACMETLSALPNProto feature.Hook[func(b *LocalBackend, hi *tls.ClientHelloInfo) (string, bool)]
|
|
|
|
// HookUpdateCertRefreshLoop is called when [LocalBackend]'s state
|
|
// or serve config changes, so the cert refresh loop can be
|
|
// (re)started or stopped. It is invoked with b.mu held.
|
|
HookUpdateCertRefreshLoop feature.Hook[func(b *LocalBackend, state ipn.State, sc ipn.ServeConfigView)]
|
|
|
|
// HookShutdownCertRefreshLoop is called from
|
|
// [LocalBackend.Shutdown] to cancel the cert refresh loop.
|
|
HookShutdownCertRefreshLoop feature.Hook[func(b *LocalBackend)]
|
|
|
|
// HookConfigureCertsForTest implements
|
|
// [LocalBackend.ConfigureCertsForTest].
|
|
HookConfigureCertsForTest feature.Hook[func(b *LocalBackend, getCert func(string) (*TLSCertKeyPair, error))]
|
|
|
|
// HookHandleC2NTLSCertStatus handles the GET /tls-cert-status
|
|
// control-to-node request.
|
|
HookHandleC2NTLSCertStatus feature.Hook[func(b *LocalBackend, w http.ResponseWriter, r *http.Request)]
|
|
)
|
|
|
|
func init() {
|
|
RegisterC2N("GET /tls-cert-status", func(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
|
if f, ok := HookHandleC2NTLSCertStatus.GetOk(); ok {
|
|
f(b, w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
io.WriteString(w, `{"Missing":true}`) // a minimal tailcfg.C2NTLSCertInfo
|
|
})
|
|
}
|
|
|
|
// GetCertPEM returns a TLSCertKeyPair for domain, either from the local
|
|
// cache or by issuing a new cert via ACME. See
|
|
// [LocalBackend.GetCertPEMWithValidity] for the full semantics.
|
|
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertKeyPair, error) {
|
|
return b.GetCertPEMWithValidity(ctx, domain, 0)
|
|
}
|
|
|
|
// GetCertPEMWithValidity is like [LocalBackend.GetCertPEM] but with a
|
|
// minimum cert validity duration. If the cached cert would expire
|
|
// sooner than minValidity, it is renewed synchronously.
|
|
//
|
|
// The domain must be one of:
|
|
//
|
|
// - An exact CertDomain (e.g., "node.ts.net")
|
|
// - A wildcard domain (e.g., "*.node.ts.net")
|
|
// - A bring-your-own Funnel domain referenced by the local serve
|
|
// config (e.g., "foo.com" when ServeConfig.AllowFunnel has
|
|
// "foo.com:443").
|
|
//
|
|
// On an ACME rate-limit failure the returned error implements
|
|
// [tsweb.HTTPStatuser] with a 429 + Retry-After response; other errors
|
|
// are returned unchanged.
|
|
func (b *LocalBackend) GetCertPEMWithValidity(ctx context.Context, domain string, minValidity time.Duration) (*TLSCertKeyPair, error) {
|
|
if f, ok := HookGetCertPEM.GetOk(); ok {
|
|
return f(ctx, b, domain, minValidity)
|
|
}
|
|
return nil, errNoCerts
|
|
}
|
|
|
|
// getACMETLSALPNCert returns the short-lived ACME challenge certificate
|
|
// for hi.ServerName, if any. The ok result reports whether hi offered
|
|
// acme-tls/1 and an ACME order is actively waiting on that challenge
|
|
// for hi.ServerName.
|
|
func (b *LocalBackend) getACMETLSALPNCert(hi *tls.ClientHelloInfo) (*tls.Certificate, bool) {
|
|
if f, ok := HookGetACMETLSALPNCert.GetOk(); ok {
|
|
return f(b, hi)
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// getACMETLSALPNProto reports whether serveTLSConfig should advertise
|
|
// an ACME ALPN protocol for this ClientHello.
|
|
func (b *LocalBackend) getACMETLSALPNProto(hi *tls.ClientHelloInfo) (string, bool) {
|
|
if f, ok := HookGetACMETLSALPNProto.GetOk(); ok {
|
|
return f(b, hi)
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// updateCertRefreshLoopLocked is called when b.state or b.serveConfig
|
|
// changes, so the cert refresh loop can be (re)started or stopped.
|
|
// b.mu must be held.
|
|
func (b *LocalBackend) updateCertRefreshLoopLocked() {
|
|
if b.shutdownCalled {
|
|
return
|
|
}
|
|
if f, ok := HookUpdateCertRefreshLoop.GetOk(); ok {
|
|
f(b, b.state, b.serveConfig)
|
|
}
|
|
}
|
|
|
|
// shutdownCertRefreshLoop is called from [LocalBackend.Shutdown] to cancel the
|
|
// cert refresh loop. It must be called without b.mu held because it waits for
|
|
// the loop, which can be blocked acquiring b.mu.
|
|
func (b *LocalBackend) shutdownCertRefreshLoop() {
|
|
if f, ok := HookShutdownCertRefreshLoop.GetOk(); ok {
|
|
f(b)
|
|
}
|
|
}
|
|
|
|
// serveTLSNextProtos returns the baseline ALPN protocols for ordinary
|
|
// Serve TLS traffic. ACME tls-alpn-01 is intentionally not advertised
|
|
// here; it is added dynamically by serveTLSConfig only while a matching
|
|
// challenge certificate is pending.
|
|
func serveTLSNextProtos() []string {
|
|
return []string{"h2", "http/1.1"}
|
|
}
|