|
| 1 | +//go:build linux && !android && !e2e_testing |
| 2 | + |
| 3 | +package main |
| 4 | + |
| 5 | +import ( |
| 6 | + "fmt" |
| 7 | + "net/netip" |
| 8 | + "os" |
| 9 | + "path/filepath" |
| 10 | + "runtime" |
| 11 | + "testing" |
| 12 | + "time" |
| 13 | + |
| 14 | + "github.com/slackhq/nebula" |
| 15 | + "github.com/slackhq/nebula/cert" |
| 16 | + cert_test "github.com/slackhq/nebula/cert_test" |
| 17 | + "github.com/slackhq/nebula/config" |
| 18 | + "github.com/slackhq/nebula/test" |
| 19 | + "github.com/stretchr/testify/require" |
| 20 | +) |
| 21 | + |
| 22 | +// TestControlStopClosesOnTimer reproduces the dnclient lifecycle: nebula runs as |
| 23 | +// a library, and on a config update dnclient calls Stop() in-process to tear the |
| 24 | +// old instance down before starting a new one. This boots a real nebula (real |
| 25 | +// blocking UDP sockets, tun disabled), lets it run, then Stop()s it on a timer |
| 26 | +// and asserts it actually closes. If the reader goroutines parked in recvmmsg |
| 27 | +// don't wake on Close(), Wait() blocks forever and this fails with a goroutine |
| 28 | +// dump instead of relying on a process signal to unstick them. |
| 29 | +func TestControlStopClosesOnTimer(t *testing.T) { |
| 30 | + l := test.NewLogger() |
| 31 | + dir := t.TempDir() |
| 32 | + |
| 33 | + before := time.Now().Add(-time.Hour) |
| 34 | + after := time.Now().Add(time.Hour) |
| 35 | + ca, _, caKey, caPEM := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, before, after, nil, nil, nil) |
| 36 | + networks := []netip.Prefix{netip.MustParsePrefix("10.0.0.1/24")} |
| 37 | + _, _, keyPEM, certPEM := cert_test.NewTestCert(cert.Version2, cert.Curve_CURVE25519, ca, caKey, "close-on-timer", before, after, networks, nil, nil) |
| 38 | + |
| 39 | + caPath := filepath.Join(dir, "ca.pem") |
| 40 | + certPath := filepath.Join(dir, "cert.pem") |
| 41 | + keyPath := filepath.Join(dir, "key.pem") |
| 42 | + require.NoError(t, os.WriteFile(caPath, caPEM, 0o600)) |
| 43 | + require.NoError(t, os.WriteFile(certPath, certPEM, 0o600)) |
| 44 | + require.NoError(t, os.WriteFile(keyPath, keyPEM, 0o600)) |
| 45 | + |
| 46 | + // tun disabled so no device/root is needed; routines: 2 so we exercise the |
| 47 | + // multi-socket (SO_REUSEPORT) teardown, which is where dnclient runs. |
| 48 | + configBody := fmt.Sprintf(` |
| 49 | +pki: |
| 50 | + ca: %s |
| 51 | + cert: %s |
| 52 | + key: %s |
| 53 | +listen: |
| 54 | + host: 127.0.0.1 |
| 55 | + port: 0 |
| 56 | +tun: |
| 57 | + disabled: true |
| 58 | +firewall: |
| 59 | + outbound: |
| 60 | + - port: any |
| 61 | + proto: any |
| 62 | + host: any |
| 63 | + inbound: |
| 64 | + - port: any |
| 65 | + proto: any |
| 66 | + host: any |
| 67 | +routines: 2 |
| 68 | +`, caPath, certPath, keyPath) |
| 69 | + require.NoError(t, os.WriteFile(filepath.Join(dir, "config.yml"), []byte(configBody), 0o600)) |
| 70 | + |
| 71 | + c := config.NewC(l) |
| 72 | + require.NoError(t, c.Load(dir)) |
| 73 | + |
| 74 | + ctrl, err := nebula.Main(c, false, "close-on-timer", l, nil) |
| 75 | + require.NoError(t, err) |
| 76 | + require.NoError(t, ctrl.Start()) |
| 77 | + |
| 78 | + // Run like a live nebula, then close on a timer, exactly as dnclient does. |
| 79 | + <-time.NewTimer(5 * time.Second).C |
| 80 | + |
| 81 | + stopped := make(chan struct{}) |
| 82 | + go func() { |
| 83 | + ctrl.Stop() // closes the udp sockets (shutdown(2)) and the tun |
| 84 | + ctrl.Wait() // blocks until every reader goroutine has returned |
| 85 | + close(stopped) |
| 86 | + }() |
| 87 | + |
| 88 | + select { |
| 89 | + case <-stopped: |
| 90 | + t.Log("nebula closed cleanly on timer") |
| 91 | + case <-time.After(10 * time.Second): |
| 92 | + buf := make([]byte, 1<<20) |
| 93 | + n := runtime.Stack(buf, true) |
| 94 | + t.Fatalf("nebula did NOT close within 10s of Stop(): a blocking reader never woke\n%s", buf[:n]) |
| 95 | + } |
| 96 | +} |
0 commit comments