f5eac39ea ("feature/acme, ipn/ipnlocal: start moving ACME/cert state
into an extension") started to move the cert code into feature/acme
but was meant as a baby step.
This goes further, moving almost everything, leaving only some hooks
in ipnlocal.
When we later move "serve" support out to feature/serve, this will
look a bit different in that the hooks currently in ipnlocal will move
to feature/serve (cert support already depends on serve).
As part of this, cert-related tests move to feaure/acme too, which
means some test infra from ipnlocal now moves to shared ipnlocaltest.
(it's not big at the moment, but I imagine it growing)
Updates #12614
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I9ea89aa9754f12d54b81751b6bd830f2664241ff
145 lines
5.3 KiB
Go
145 lines
5.3 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").
|
|
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 f, ok := HookUpdateCertRefreshLoop.GetOk(); ok {
|
|
f(b, b.state, b.serveConfig)
|
|
}
|
|
}
|
|
|
|
// shutdownCertRefreshLoopLocked is called from
|
|
// [LocalBackend.Shutdown] to cancel the cert refresh loop.
|
|
// b.mu must be held.
|
|
func (b *LocalBackend) shutdownCertRefreshLoopLocked() {
|
|
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"}
|
|
}
|