mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-24 10:41:45 +01:00
ipn/ipnlocal: set certificate retrieval function directly in tests
Signed-off-by: Harry Harpham <harry@tailscale.com>
This commit is contained in:
parent
1584825c9a
commit
a44e9d9c08
@ -45,7 +45,6 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tempfork/acme"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/testenv"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
@ -309,38 +308,13 @@ func (b *LocalBackend) getCertStore() (certStore, error) {
|
||||
return certFileStore{dir: dir, testRoots: testX509Roots}, nil
|
||||
}
|
||||
|
||||
// SetCertsForTest configures the TLS certificates used by this local backend.
|
||||
// This should only be used in testing and can be used to skip the usual ACME
|
||||
// certificate registration.
|
||||
//
|
||||
// Certificates will be served based on the subject name or subject alternative
|
||||
// names (SANs) in the certificates. If this backend should serve certificates
|
||||
// for hostnames like foo.tail-scale.ts.net or test-service.tail-scale.ts.net,
|
||||
// then those names need to appear as a subject name or SAN.
|
||||
func (b *LocalBackend) SetCertsForTest(certs ...TLSCertKeyPair) {
|
||||
// ConfigureCertsForTest sets a certificate retrieval function to be used by
|
||||
// this local backend, skipping the usual ACME certificate registration. Should
|
||||
// only be used in tests.
|
||||
func (b *LocalBackend) ConfigureCertsForTest(getCert func(hostname string) (*TLSCertKeyPair, error)) {
|
||||
testenv.AssertInTest()
|
||||
m := map[string]TLSCertKeyPair{}
|
||||
for _, c := range certs {
|
||||
cert, err := tls.X509KeyPair(c.CertPEM, c.KeyPEM)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("parse error: %v", err))
|
||||
}
|
||||
names := set.Of(append(cert.Leaf.DNSNames, cert.Leaf.Subject.CommonName)...)
|
||||
for _, name := range names.Slice() {
|
||||
if _, ok := m[name]; ok {
|
||||
panic(fmt.Sprintf("duplicate subject name %v", name))
|
||||
}
|
||||
m[name] = c
|
||||
}
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.getCertForTest = func(domain string) (*TLSCertKeyPair, error) {
|
||||
c, ok := m[domain]
|
||||
if !ok {
|
||||
return nil, errors.New("cert not found")
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
b.getCertForTest = getCert
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
|
||||
@ -400,9 +400,9 @@ type LocalBackend struct {
|
||||
// bind the node identity to this device.
|
||||
hardwareAttested atomic.Bool
|
||||
|
||||
// getCertForTest is used to configure TLS certificates for testing
|
||||
// purposes. See [LocalBackend.SetCertsForTest].
|
||||
getCertForTest func(domain string) (*TLSCertKeyPair, error)
|
||||
// getCertForTest is used to retrieve TLS certificates in tests.
|
||||
// See [LocalBackend.ConfigureCertsForTest].
|
||||
getCertForTest func(hostname string) (*TLSCertKeyPair, error)
|
||||
}
|
||||
|
||||
// SetHardwareAttested enables hardware attestation key signatures in map
|
||||
|
||||
@ -1274,10 +1274,11 @@ func (s *Server) ListenService(name string, port uint16, opts ...ServiceOption)
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// - try above with simple TCP listener first
|
||||
// - handle Services with multiple ports defined
|
||||
// - support web handlers
|
||||
// - make sure extras like PROXY mode are supported
|
||||
// - create example for a Service with multiple ports
|
||||
// - support web handlers?
|
||||
// - at least app capabilities need to work
|
||||
// - everything else could serve directly or use http.ReverseProxy, right?
|
||||
// - maybe worth an http.ReverseProxy example?
|
||||
// - support TUN mode
|
||||
|
||||
// Process options.
|
||||
|
||||
@ -30,6 +30,7 @@ import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@ -138,6 +139,9 @@ func startControl(t *testing.T) (controlURL string, control *testcontrol.Server)
|
||||
}
|
||||
|
||||
type testCertIssuer struct {
|
||||
mu sync.Mutex
|
||||
certs map[string]ipnlocal.TLSCertKeyPair // keyed by hostname
|
||||
|
||||
root *x509.Certificate
|
||||
rootKey *ecdsa.PrivateKey
|
||||
}
|
||||
@ -170,33 +174,48 @@ func newCertIssuer() *testCertIssuer {
|
||||
return &testCertIssuer{
|
||||
root: rootCA,
|
||||
rootKey: rootKey,
|
||||
certs: map[string]ipnlocal.TLSCertKeyPair{},
|
||||
}
|
||||
}
|
||||
|
||||
func (tci *testCertIssuer) makeCert(domain string) (certPEM, keyPEM []byte, err error) {
|
||||
func (tci *testCertIssuer) getCert(hostname string) (*ipnlocal.TLSCertKeyPair, error) {
|
||||
tci.mu.Lock()
|
||||
defer tci.mu.Unlock()
|
||||
cert, ok := tci.certs[hostname]
|
||||
if ok {
|
||||
return &cert, nil
|
||||
}
|
||||
|
||||
certPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
certTmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
DNSNames: []string{domain},
|
||||
DNSNames: []string{hostname},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
}
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, certTmpl, tci.root, &certPrivKey.PublicKey, tci.rootKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
certPEM = pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certDER,
|
||||
})
|
||||
keyPEM = pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: must.Get(x509.MarshalPKCS8PrivateKey(certPrivKey)),
|
||||
})
|
||||
return certPEM, keyPEM, nil
|
||||
keyDER, err := x509.MarshalPKCS8PrivateKey(certPrivKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert = ipnlocal.TLSCertKeyPair{
|
||||
CertPEM: pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certDER,
|
||||
}),
|
||||
KeyPEM: pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: keyDER,
|
||||
}),
|
||||
}
|
||||
tci.certs[hostname] = cert
|
||||
return &cert, nil
|
||||
}
|
||||
|
||||
func (tci *testCertIssuer) Pool() *x509.CertPool {
|
||||
@ -228,10 +247,7 @@ func startServer(t *testing.T, ctx context.Context, controlURL, hostname string)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
nodeFQDN := hostname + "." + status.CurrentTailnet.MagicDNSSuffix
|
||||
certPEM, keyPEM, err := testCertRoot.makeCert(nodeFQDN)
|
||||
s.lb.SetCertsForTest(ipnlocal.TLSCertKeyPair{CertPEM: certPEM, KeyPEM: keyPEM})
|
||||
s.lb.ConfigureCertsForTest(testCertRoot.getCert)
|
||||
|
||||
return s, status.TailscaleIPs[0], status.Self.PublicKey
|
||||
}
|
||||
@ -809,15 +825,6 @@ func TestListenService(t *testing.T) {
|
||||
serviceHostNode.Tags = append(serviceHostNode.Tags, "some-tag")
|
||||
control.UpdateNode(serviceHostNode)
|
||||
|
||||
// Configure a certificate for the Service domain (in production,
|
||||
// the local backend would use an ACME client to obtain a certPEM).
|
||||
// This is only used when serving over TLS.
|
||||
certPEM, keyPEM := must.Get2(testCertRoot.makeCert(serviceFQDN))
|
||||
serviceHost.lb.SetCertsForTest(ipnlocal.TLSCertKeyPair{
|
||||
CertPEM: certPEM,
|
||||
KeyPEM: keyPEM,
|
||||
})
|
||||
|
||||
// The service client must accept routes advertised by other nodes
|
||||
// (RouteAll is equivalent to --accept-routes).
|
||||
must.Get(serviceClient.localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user