1
0

sshd_test.go 3.6 KB

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