1
0

sshd_test.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. //go:build e2e_testing
  2. // +build e2e_testing
  3. package e2e
  4. import (
  5. "crypto/ed25519"
  6. "crypto/rand"
  7. "encoding/pem"
  8. "net"
  9. "strings"
  10. "testing"
  11. "time"
  12. "github.com/slackhq/nebula/cert"
  13. "github.com/slackhq/nebula/cert_test"
  14. "github.com/stretchr/testify/assert"
  15. "github.com/stretchr/testify/require"
  16. "golang.org/x/crypto/ssh"
  17. )
  18. func TestSSHDLifecycle(t *testing.T) {
  19. // TestSSHDLifecycle exercises the in-process sshd through several config reloads and a Control.Stop.
  20. ca, _, caKey, _ := cert_test.NewTestCaCert(
  21. cert.Version1, cert.Curve_CURVE25519,
  22. time.Now(), time.Now().Add(10*time.Minute),
  23. nil, nil, []string{},
  24. )
  25. hostKeyPEM := generateSSHHostKey(t)
  26. clientSigner, clientAuthKey := generateSSHClientKey(t)
  27. sshdAddr := allocLoopbackPort(t)
  28. overrides := m{
  29. "sshd": m{
  30. "enabled": true,
  31. "listen": sshdAddr,
  32. "host_key": hostKeyPEM,
  33. "authorized_users": []m{{
  34. "user": "tester",
  35. "keys": []string{clientAuthKey},
  36. }},
  37. },
  38. }
  39. control, _, _, _ := newSimpleServer(cert.Version1, ca, caKey, "sshd-test", "10.222.0.1/24", overrides)
  40. control.Start()
  41. t.Cleanup(func() { control.Stop() })
  42. // sshd binds in a goroutine after Start returns; wait for it.
  43. require.Eventually(t, func() bool { return canDial(sshdAddr) }, 2*time.Second, 25*time.Millisecond,
  44. "sshd never started listening")
  45. for i := 1; i <= 3; i++ {
  46. out := sshExecReload(t, sshdAddr, clientSigner)
  47. assert.Contains(t, out, "Reloading config", "reload cycle %d", i)
  48. require.Eventually(t, func() bool { return canDial(sshdAddr) }, 2*time.Second, 25*time.Millisecond,
  49. "sshd not listening after reload cycle %d", i)
  50. }
  51. control.Stop()
  52. require.Eventually(t, func() bool { return !canDial(sshdAddr) }, 2*time.Second, 25*time.Millisecond,
  53. "sshd still listening after Control.Stop")
  54. }
  55. func canDial(addr string) bool {
  56. c, err := net.DialTimeout("tcp", addr, 100*time.Millisecond)
  57. if err != nil {
  58. return false
  59. }
  60. _ = c.Close()
  61. return true
  62. }
  63. // allocLoopbackPort grabs an unused TCP port on 127.0.0.1, closes it, and returns the address. There
  64. // is a small race between releasing the port and the sshd reclaiming it; in practice the OS keeps the
  65. // port available long enough for the test to bind it.
  66. func allocLoopbackPort(t *testing.T) string {
  67. t.Helper()
  68. l, err := net.Listen("tcp", "127.0.0.1:0")
  69. require.NoError(t, err)
  70. addr := l.Addr().String()
  71. require.NoError(t, l.Close())
  72. return addr
  73. }
  74. func generateSSHHostKey(t *testing.T) string {
  75. t.Helper()
  76. _, priv, err := ed25519.GenerateKey(rand.Reader)
  77. require.NoError(t, err)
  78. block, err := ssh.MarshalPrivateKey(priv, "nebula-e2e-host")
  79. require.NoError(t, err)
  80. return string(pem.EncodeToMemory(block))
  81. }
  82. func generateSSHClientKey(t *testing.T) (ssh.Signer, string) {
  83. t.Helper()
  84. _, priv, err := ed25519.GenerateKey(rand.Reader)
  85. require.NoError(t, err)
  86. signer, err := ssh.NewSignerFromKey(priv)
  87. require.NoError(t, err)
  88. auth := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey())))
  89. return signer, auth
  90. }
  91. func sshExecReload(t *testing.T, addr string, signer ssh.Signer) string {
  92. t.Helper()
  93. cfg := &ssh.ClientConfig{
  94. User: "tester",
  95. Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
  96. HostKeyCallback: ssh.InsecureIgnoreHostKey(),
  97. Timeout: 2 * time.Second,
  98. }
  99. client, err := ssh.Dial("tcp", addr, cfg)
  100. require.NoError(t, err)
  101. defer client.Close()
  102. sess, err := client.NewSession()
  103. require.NoError(t, err)
  104. defer sess.Close()
  105. // reload tears the channel down before sending exit-status, so Output returns an error on the
  106. // channel close. The output buffer still has whatever the reload callback wrote before that.
  107. out, _ := sess.Output("reload")
  108. return string(out)
  109. }