Files
nebula/main_test.go

157 lines
5.0 KiB
Go

package nebula
import (
"fmt"
"net/netip"
"os"
"path/filepath"
"testing"
"time"
"github.com/slackhq/nebula/cert"
cert_test "github.com/slackhq/nebula/cert_test"
"github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/test"
"github.com/slackhq/nebula/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
)
func TestParseCpuAffinity(t *testing.T) {
l := test.NewLogger()
// newConfig returns a config.C with tun.cpu_affinity set to v. A nil v
// leaves the key unset.
newConfig := func(v any) *config.C {
c := config.NewC(l)
if v != nil {
c.Settings["tun"] = map[string]any{"cpu_affinity": v}
}
return c
}
// unset -> nil (listenIn falls back to spreading across the allowed set)
assert.Nil(t, parseCpuAffinity(newConfig(nil), l, 1, nil))
// Pick a CPU we're actually allowed to run on so a valid list survives
// validation regardless of the host's affinity mask.
allowed, _ := util.AllowedCPUs()
validCPU := 0
if len(allowed) > 0 {
validCPU = allowed[0]
}
// valid list -> parsed through unchanged
assert.Equal(t, []int{validCPU, validCPU}, parseCpuAffinity(newConfig([]any{validCPU, validCPU}), l, 2, nil))
// a negative entry is out of range on every platform -> disables the override
assert.Nil(t, parseCpuAffinity(newConfig([]any{validCPU, -1}), l, 2, nil))
// a non-integer entry -> disables the override
assert.Nil(t, parseCpuAffinity(newConfig([]any{validCPU, "not-a-cpu"}), l, 2, nil))
// a CPU id outside the allowed set -> disables the override. Only assertable
// where we can enumerate the allowed set (e.g. linux); 1<<20 is far beyond
// any representable CPU id so it can never be in the mask.
if len(allowed) > 0 {
assert.Nil(t, parseCpuAffinity(newConfig([]any{1 << 20}), l, 1, nil))
}
// an entry also listed in tun.avoid_cpus -> disables the override
assert.Nil(t, parseCpuAffinity(newConfig([]any{validCPU}), l, 1, []int{validCPU}))
}
func TestParseAvoidCPUs(t *testing.T) {
l := test.NewLogger()
for _, tc := range []struct {
value any
want []int
}{
{nil, nil}, {[]any{}, []int{}}, {[]any{1, 1 << 20}, []int{1, 1 << 20}},
{"auto", nil}, {"1,2", nil}, {[]any{-1}, nil}, {[]any{1.5}, nil}, {[]any{1, "2"}, nil},
} {
c := config.NewC(l)
if tc.value != nil {
c.Settings["tun"] = map[string]any{"avoid_cpus": tc.value}
}
assert.Equal(t, tc.want, parseAvoidCPUs(c, l), "%v", tc.value)
}
// avoiding every allowed CPU leaves nowhere to pin; only checkable where the allowed set is known
if allowed, _ := util.AllowedCPUs(); len(allowed) > 0 {
all := make([]any, len(allowed))
for i, cpu := range allowed {
all[i] = cpu
}
c := config.NewC(l)
c.Settings["tun"] = map[string]any{"avoid_cpus": all}
assert.Nil(t, parseAvoidCPUs(c, l))
}
}
// TestMain_ConfigTestReleasesItsGoroutines pins the rule that Main only leaves goroutines running
// when it hands back a Control to stop them with.
//
// A config test gets no Control, so anything it started had nothing to stop it: the lighthouse
// query worker, and a hostname resolver per dns named static host, ran for the life of the
// process. That matters to every embedder that validates a config in process rather than by
// exec'ing, dnclient and the apple clients included, because they do it on each config load and
// the leak accumulates.
func TestMain_ConfigTestReleasesItsGoroutines(t *testing.T) {
defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
l := test.NewLogger()
dir := t.TempDir()
before := time.Now().Add(-time.Hour)
after := time.Now().Add(time.Hour)
ca, _, caKey, caPEM := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, before, after, nil, nil, nil)
networks := []netip.Prefix{netip.MustParsePrefix("10.0.0.1/24")}
_, _, keyPEM, certPEM := cert_test.NewTestCert(
cert.Version2, cert.Curve_CURVE25519, ca, caKey, "config-test", before, after, networks, nil, nil)
caPath := filepath.Join(dir, "ca.pem")
certPath := filepath.Join(dir, "cert.pem")
keyPath := filepath.Join(dir, "key.pem")
require.NoError(t, os.WriteFile(caPath, caPEM, 0o600))
require.NoError(t, os.WriteFile(certPath, certPEM, 0o600))
require.NoError(t, os.WriteFile(keyPath, keyPEM, 0o600))
// A static host by address, not by name: the query worker is the goroutine under test and a
// hostname would drag a real dns lookup into a unit test.
configBody := fmt.Sprintf(`
pki:
ca: %s
cert: %s
key: %s
static_host_map:
"10.0.0.2": ["192.0.2.1:4242"]
lighthouse:
hosts:
- "10.0.0.2"
listen:
host: 127.0.0.1
port: 0
tun:
disabled: true
firewall:
outbound:
- port: any
proto: any
host: any
inbound:
- port: any
proto: any
host: any
`, caPath, certPath, keyPath)
require.NoError(t, os.WriteFile(filepath.Join(dir, "config.yml"), []byte(configBody), 0o600))
c := config.NewC(l)
require.NoError(t, c.Load(dir))
ctrl, err := Main(c, true, "config-test", l, nil)
require.NoError(t, err)
require.Nil(t, ctrl, "a config test hands back nothing to stop, so it must stop itself")
}