| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124 |
- //go:build e2e_testing
- package e2e
- import (
- "crypto/ed25519"
- "crypto/rand"
- "encoding/pem"
- "net"
- "strings"
- "testing"
- "time"
- "github.com/slackhq/nebula/cert"
- "github.com/slackhq/nebula/cert_test"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "golang.org/x/crypto/ssh"
- )
- func TestSSHDLifecycle(t *testing.T) {
- // TestSSHDLifecycle exercises the in-process sshd through several config reloads and a Control.Stop.
- ca, _, caKey, _ := cert_test.NewTestCaCert(
- cert.Version1, cert.Curve_CURVE25519,
- time.Now(), time.Now().Add(10*time.Minute),
- nil, nil, []string{},
- )
- hostKeyPEM := generateSSHHostKey(t)
- clientSigner, clientAuthKey := generateSSHClientKey(t)
- sshdAddr := allocLoopbackPort(t)
- overrides := m{
- "sshd": m{
- "enabled": true,
- "listen": sshdAddr,
- "host_key": hostKeyPEM,
- "authorized_users": []m{{
- "user": "tester",
- "keys": []string{clientAuthKey},
- }},
- },
- }
- control, _, _, _ := newSimpleServer(cert.Version1, ca, caKey, "sshd-test", "10.222.0.1/24", overrides)
- control.Start()
- t.Cleanup(func() { control.Stop() })
- // sshd binds in a goroutine after Start returns; wait for it.
- require.Eventually(t, func() bool { return canDial(sshdAddr) }, 2*time.Second, 25*time.Millisecond,
- "sshd never started listening")
- for i := 1; i <= 3; i++ {
- out := sshExecReload(t, sshdAddr, clientSigner)
- assert.Contains(t, out, "Reloading config", "reload cycle %d", i)
- require.Eventually(t, func() bool { return canDial(sshdAddr) }, 2*time.Second, 25*time.Millisecond,
- "sshd not listening after reload cycle %d", i)
- }
- control.Stop()
- require.Eventually(t, func() bool { return !canDial(sshdAddr) }, 2*time.Second, 25*time.Millisecond,
- "sshd still listening after Control.Stop")
- }
- func canDial(addr string) bool {
- c, err := net.DialTimeout("tcp", addr, 100*time.Millisecond)
- if err != nil {
- return false
- }
- _ = c.Close()
- return true
- }
- // allocLoopbackPort grabs an unused TCP port on 127.0.0.1, closes it, and returns the address. There
- // is a small race between releasing the port and the sshd reclaiming it; in practice the OS keeps the
- // port available long enough for the test to bind it.
- func allocLoopbackPort(t *testing.T) string {
- t.Helper()
- l, err := net.Listen("tcp", "127.0.0.1:0")
- require.NoError(t, err)
- addr := l.Addr().String()
- require.NoError(t, l.Close())
- return addr
- }
- func generateSSHHostKey(t *testing.T) string {
- t.Helper()
- _, priv, err := ed25519.GenerateKey(rand.Reader)
- require.NoError(t, err)
- block, err := ssh.MarshalPrivateKey(priv, "nebula-e2e-host")
- require.NoError(t, err)
- return string(pem.EncodeToMemory(block))
- }
- func generateSSHClientKey(t *testing.T) (ssh.Signer, string) {
- t.Helper()
- _, priv, err := ed25519.GenerateKey(rand.Reader)
- require.NoError(t, err)
- signer, err := ssh.NewSignerFromKey(priv)
- require.NoError(t, err)
- auth := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey())))
- return signer, auth
- }
- func sshExecReload(t *testing.T, addr string, signer ssh.Signer) string {
- t.Helper()
- cfg := &ssh.ClientConfig{
- User: "tester",
- Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
- HostKeyCallback: ssh.InsecureIgnoreHostKey(),
- Timeout: 2 * time.Second,
- }
- client, err := ssh.Dial("tcp", addr, cfg)
- require.NoError(t, err)
- defer client.Close()
- sess, err := client.NewSession()
- require.NoError(t, err)
- defer sess.Close()
- // reload tears the channel down before sending exit-status, so Output returns an error on the
- // channel close. The output buffer still has whatever the reload callback wrote before that.
- out, _ := sess.Output("reload")
- return string(out)
- }
|