Files
nexus/internal/sshclient/sshclient_test.go
T

153 lines
3.9 KiB
Go

package sshclient
import (
"crypto/ed25519"
"crypto/rand"
"net"
"testing"
"time"
"github.com/andydixon/nexus/internal/models"
"golang.org/x/crypto/ssh"
)
func TestConnectRequiresHostKeyCallback(t *testing.T) {
profile := models.NewProfile(models.ProtocolSSH)
client := NewClient(&profile)
err := client.Connect(nil, nil)
if err == nil {
t.Fatal("expected Connect to reject missing host key callback")
}
}
func TestJumpHostClientConfigUsesFinalHostKeyCallback(t *testing.T) {
called := false
hostKeyCallback := func(hostname string, _ net.Addr, _ ssh.PublicKey) error {
called = true
if hostname != "jump.example:2222" {
t.Fatalf("hostname = %q, want jump.example:2222", hostname)
}
return nil
}
finalConfig := &ssh.ClientConfig{
HostKeyCallback: hostKeyCallback,
Timeout: 12 * time.Second,
}
jump := models.JumpHost{
Host: "jump.example",
Port: 2222,
Username: "alice",
}
got := jumpHostClientConfig(jump, nil, finalConfig)
if got.User != jump.Username {
t.Fatalf("User = %q, want %q", got.User, jump.Username)
}
if got.Timeout != finalConfig.Timeout {
t.Fatalf("Timeout = %s, want %s", got.Timeout, finalConfig.Timeout)
}
if got.HostKeyCallback == nil {
t.Fatal("HostKeyCallback is nil")
}
if err := got.HostKeyCallback("jump.example:2222", nil, nil); err != nil {
t.Fatalf("HostKeyCallback() error = %v", err)
}
if !called {
t.Fatal("final host key callback was not used")
}
}
func TestPingReturnsAfterSSHTransportCloses(t *testing.T) {
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("GenerateKey() error = %v", err)
}
signer, err := ssh.NewSignerFromKey(privateKey)
if err != nil {
t.Fatalf("NewSignerFromKey() error = %v", err)
}
serverConfig := &ssh.ServerConfig{NoClientAuth: true}
serverConfig.AddHostKey(signer)
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Listen() error = %v", err)
}
t.Cleanup(func() { _ = listener.Close() })
serverReady := make(chan *ssh.ServerConn, 1)
serverErrors := make(chan error, 1)
go func() {
conn, acceptErr := listener.Accept()
if acceptErr != nil {
serverErrors <- acceptErr
return
}
serverConn, _, _, handshakeErr := ssh.NewServerConn(conn, serverConfig)
if handshakeErr != nil {
_ = conn.Close()
serverErrors <- handshakeErr
return
}
serverReady <- serverConn
}()
conn, err := net.DialTimeout("tcp", listener.Addr().String(), 2*time.Second)
if err != nil {
t.Fatalf("DialTimeout() error = %v", err)
}
clientConn, channels, requests, err := ssh.NewClientConn(conn, listener.Addr().String(), &ssh.ClientConfig{
User: "test",
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // Test server uses an ephemeral key.
})
if err != nil {
_ = conn.Close()
t.Fatalf("NewClientConn() error = %v", err)
}
rawClient := ssh.NewClient(clientConn, channels, requests)
var serverConn *ssh.ServerConn
select {
case serverConn = <-serverReady:
case serverErr := <-serverErrors:
t.Fatalf("SSH server handshake error = %v", serverErr)
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for SSH server handshake")
}
client := NewClient(&models.Profile{})
client.mu.Lock()
client.sshClient = rawClient
client.connected = true
client.mu.Unlock()
t.Cleanup(func() { _ = client.Close() })
if err := serverConn.Close(); err != nil {
t.Fatalf("server Close() error = %v", err)
}
waitDone := make(chan error, 1)
go func() { waitDone <- rawClient.Wait() }()
select {
case <-waitDone:
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for SSH transport to close")
}
pingDone := make(chan error, 1)
go func() {
_, pingErr := client.Ping()
pingDone <- pingErr
}()
select {
case pingErr := <-pingDone:
if pingErr == nil {
t.Fatal("Ping() succeeded after the SSH transport closed")
}
case <-time.After(2 * time.Second):
t.Fatal("Ping() did not return after the SSH transport closed")
}
}