close_on_timer_test.go 2.9 KB

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