4c3ed5ab32
Move tailscaled's in-tree reactive users from of IPN bus Notify.NetMap updates to the narrower Notify.SelfChange signal introduced earlier in this series. Consumers that need additional state (peers, DNS config, etc.) fetch it on demand via the LocalAPI. It is a step toward the larger goal of not fanning Notify.NetMap out to every bus watcher on Linux/non-GUI hosts. A future change stops sending Notify.NetMap entirely on Linux and non-GUI platforms. (eventually once macOS/iOS/Windows migrate to the upcoming new Notify APIs, we'll remove ipn.Notify.NetMap entirely) Updates #12542 Change-Id: I51ea9d86bdca1909d6ac0e7d5bd3934a3a4e8516 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
287 lines
7.3 KiB
Go
287 lines
7.3 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package certs
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"testing"
|
|
"time"
|
|
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/kube/localclient"
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
// TestEnsureCertLoops tests that the certManager correctly starts and stops
|
|
// update loops for certs when the serve config changes. It tracks goroutine
|
|
// count and uses that as a validator that the expected number of cert loops are
|
|
// running.
|
|
func TestEnsureCertLoops(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
initialConfig *ipn.ServeConfig
|
|
updatedConfig *ipn.ServeConfig
|
|
initialGoroutines int64 // after initial serve config is applied
|
|
updatedGoroutines int64 // after updated serve config is applied
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "empty_serve_config",
|
|
initialConfig: &ipn.ServeConfig{},
|
|
initialGoroutines: 0,
|
|
},
|
|
{
|
|
name: "nil_serve_config",
|
|
initialConfig: nil,
|
|
initialGoroutines: 0,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "empty_to_one_service",
|
|
initialConfig: &ipn.ServeConfig{},
|
|
updatedConfig: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:my-app": {
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"my-app.tailnetxyz.ts.net:443": {},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
initialGoroutines: 0,
|
|
updatedGoroutines: 1,
|
|
},
|
|
{
|
|
name: "single_service",
|
|
initialConfig: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:my-app": {
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"my-app.tailnetxyz.ts.net:443": {},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
initialGoroutines: 1,
|
|
},
|
|
{
|
|
name: "multiple_services",
|
|
initialConfig: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:my-app": {
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"my-app.tailnetxyz.ts.net:443": {},
|
|
},
|
|
},
|
|
"svc:my-other-app": {
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"my-other-app.tailnetxyz.ts.net:443": {},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
initialGoroutines: 2, // one loop per domain across all services
|
|
},
|
|
{
|
|
name: "ignore_non_https_ports",
|
|
initialConfig: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:my-app": {
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"my-app.tailnetxyz.ts.net:443": {},
|
|
"my-app.tailnetxyz.ts.net:80": {},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
initialGoroutines: 1, // only one loop for the 443 endpoint
|
|
},
|
|
{
|
|
name: "remove_domain",
|
|
initialConfig: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:my-app": {
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"my-app.tailnetxyz.ts.net:443": {},
|
|
},
|
|
},
|
|
"svc:my-other-app": {
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"my-other-app.tailnetxyz.ts.net:443": {},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
updatedConfig: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:my-app": {
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"my-app.tailnetxyz.ts.net:443": {},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
initialGoroutines: 2, // initially two loops (one per service)
|
|
updatedGoroutines: 1, // one loop after removing service2
|
|
},
|
|
{
|
|
name: "tcp_terminate_tls",
|
|
initialConfig: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:my-apiserver": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
443: {
|
|
TCPForward: "localhost:80",
|
|
TerminateTLS: "my-apiserver.tailnetxyz.ts.net",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
initialGoroutines: 1,
|
|
},
|
|
{
|
|
name: "tcp_terminate_tls_and_web",
|
|
initialConfig: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:my-apiserver": {
|
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
|
443: {
|
|
TCPForward: "localhost:80",
|
|
TerminateTLS: "my-apiserver.tailnetxyz.ts.net",
|
|
},
|
|
},
|
|
},
|
|
"svc:my-app": {
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"my-app.tailnetxyz.ts.net:443": {},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
initialGoroutines: 2,
|
|
},
|
|
{
|
|
name: "add_domain",
|
|
initialConfig: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:my-app": {
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"my-app.tailnetxyz.ts.net:443": {},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
updatedConfig: &ipn.ServeConfig{
|
|
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
|
|
"svc:my-app": {
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"my-app.tailnetxyz.ts.net:443": {},
|
|
},
|
|
},
|
|
"svc:my-other-app": {
|
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
|
"my-other-app.tailnetxyz.ts.net:443": {},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
initialGoroutines: 1,
|
|
updatedGoroutines: 2,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
notifyChan := make(chan ipn.Notify)
|
|
go func() {
|
|
// SelfChange wakes the cert manager; cert domains are
|
|
// then fetched via FakeLocalClient.CertDomainsResult.
|
|
for {
|
|
notifyChan <- ipn.Notify{
|
|
SelfChange: &tailcfg.Node{StableID: "test"},
|
|
}
|
|
}
|
|
}()
|
|
cm := &CertManager{
|
|
lc: &localclient.FakeLocalClient{
|
|
FakeIPNBusWatcher: localclient.FakeIPNBusWatcher{
|
|
NotifyChan: notifyChan,
|
|
},
|
|
CertDomainsResult: []string{
|
|
"my-app.tailnetxyz.ts.net",
|
|
"my-other-app.tailnetxyz.ts.net",
|
|
"my-apiserver.tailnetxyz.ts.net",
|
|
},
|
|
},
|
|
logf: log.Printf,
|
|
certLoops: make(map[string]context.CancelFunc),
|
|
}
|
|
|
|
allDone := make(chan bool, 1)
|
|
defer cm.tracker.AddDoneCallback(func() {
|
|
cm.mu.Lock()
|
|
defer cm.mu.Unlock()
|
|
if cm.tracker.RunningGoroutines() > 0 {
|
|
return
|
|
}
|
|
select {
|
|
case allDone <- true:
|
|
default:
|
|
}
|
|
})()
|
|
|
|
err := cm.EnsureCertLoops(ctx, tt.initialConfig)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Fatalf("ensureCertLoops() error = %v", err)
|
|
}
|
|
|
|
if got := cm.tracker.RunningGoroutines(); got != tt.initialGoroutines {
|
|
t.Errorf("after initial config: got %d running goroutines, want %d", got, tt.initialGoroutines)
|
|
}
|
|
|
|
if tt.updatedConfig != nil {
|
|
if err := cm.EnsureCertLoops(ctx, tt.updatedConfig); err != nil {
|
|
t.Fatalf("ensureCertLoops() error on update = %v", err)
|
|
}
|
|
|
|
// Although starting goroutines and cancelling
|
|
// the context happens in the main goroutine, it
|
|
// the actual goroutine exit when a context is
|
|
// cancelled does not- so wait for a bit for the
|
|
// running goroutine count to reach the expected
|
|
// number.
|
|
deadline := time.After(5 * time.Second)
|
|
for {
|
|
if got := cm.tracker.RunningGoroutines(); got == tt.updatedGoroutines {
|
|
break
|
|
}
|
|
select {
|
|
case <-deadline:
|
|
t.Fatalf("timed out waiting for goroutine count to reach %d, currently at %d",
|
|
tt.updatedGoroutines, cm.tracker.RunningGoroutines())
|
|
case <-time.After(10 * time.Millisecond):
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
if tt.updatedGoroutines == 0 {
|
|
return // no goroutines to wait for
|
|
}
|
|
// cancel context to make goroutines exit
|
|
cancel()
|
|
select {
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("timed out waiting for goroutine to finish")
|
|
case <-allDone:
|
|
}
|
|
})
|
|
}
|
|
}
|