As in crypto/tls, we record the connection against a reference implementation, OpenSSH in our case, and run part of our integration tests by replaying these recorded SSH connections. Change-Id: If042b5f650b267bd3ede34a05ec3a6fa6d1a86b3 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/644436 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Carlos Amedee <carlos@golang.org> Auto-Submit: Filippo Valsorda <filippo@golang.org> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
249 lines
6.1 KiB
Go
249 lines
6.1 KiB
Go
// Copyright 2012 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || plan9 || solaris
|
|
|
|
package test
|
|
|
|
// functional test harness for unix.
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"golang.org/x/crypto/internal/testenv"
|
|
"golang.org/x/crypto/ssh"
|
|
"golang.org/x/crypto/ssh/testdata"
|
|
)
|
|
|
|
// unixConnection creates two halves of a connected net.UnixConn. It
|
|
// is used for connecting the Go SSH client with sshd without opening
|
|
// ports.
|
|
func unixConnection() (*net.UnixConn, *net.UnixConn, error) {
|
|
dir, err := os.MkdirTemp("", "unixConnection")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer os.Remove(dir)
|
|
|
|
addr := filepath.Join(dir, "ssh")
|
|
listener, err := net.Listen("unix", addr)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer listener.Close()
|
|
c1, err := net.Dial("unix", addr)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
c2, err := listener.Accept()
|
|
if err != nil {
|
|
c1.Close()
|
|
return nil, nil, err
|
|
}
|
|
|
|
return c1.(*net.UnixConn), c2.(*net.UnixConn), nil
|
|
}
|
|
|
|
func (s *server) TryDial(config *ssh.ClientConfig) (*ssh.Client, error) {
|
|
return s.TryDialWithAddr(config, "")
|
|
}
|
|
|
|
// addr is the user specified host:port. While we don't actually dial it,
|
|
// we need to know this for host key matching
|
|
func (s *server) TryDialWithAddr(config *ssh.ClientConfig, addr string) (client *ssh.Client, err error) {
|
|
sshd, err := exec.LookPath("sshd")
|
|
if err != nil {
|
|
s.t.Skipf("skipping test: %v", err)
|
|
}
|
|
|
|
c1, c2, err := unixConnection()
|
|
if err != nil {
|
|
s.t.Fatalf("unixConnection: %v", err)
|
|
}
|
|
defer func() {
|
|
// Close c2 after we've started the sshd command so that it won't prevent c1
|
|
// from returning EOF when the sshd command exits.
|
|
c2.Close()
|
|
|
|
// Leave c1 open if we're returning a client that wraps it.
|
|
// (The client is responsible for closing it.)
|
|
// Otherwise, close it to free up the socket.
|
|
if client == nil {
|
|
c1.Close()
|
|
}
|
|
}()
|
|
|
|
f, err := c2.File()
|
|
if err != nil {
|
|
s.t.Fatalf("UnixConn.File: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
cmd := testenv.Command(s.t, sshd, "-f", s.configfile, "-i", "-e")
|
|
cmd.Stdin = f
|
|
cmd.Stdout = f
|
|
cmd.Stderr = new(bytes.Buffer)
|
|
|
|
if s.sshdTestPwSo != "" {
|
|
if s.testUser == "" {
|
|
s.t.Fatal("user missing from sshd_test_pw.so config")
|
|
}
|
|
if s.testPasswd == "" {
|
|
s.t.Fatal("password missing from sshd_test_pw.so config")
|
|
}
|
|
cmd.Env = append(os.Environ(),
|
|
fmt.Sprintf("LD_PRELOAD=%s", s.sshdTestPwSo),
|
|
fmt.Sprintf("TEST_USER=%s", s.testUser),
|
|
fmt.Sprintf("TEST_PASSWD=%s", s.testPasswd))
|
|
}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
s.t.Fatalf("s.cmd.Start: %v", err)
|
|
}
|
|
s.lastDialConn = c1
|
|
s.t.Cleanup(func() {
|
|
// Don't check for errors; if it fails it's most
|
|
// likely "os: process already finished", and we don't
|
|
// care about that. Use os.Interrupt, so child
|
|
// processes are killed too.
|
|
cmd.Process.Signal(os.Interrupt)
|
|
cmd.Wait()
|
|
if s.t.Failed() || testing.Verbose() {
|
|
// log any output from sshd process
|
|
s.t.Logf("sshd:\n%s", cmd.Stderr)
|
|
}
|
|
})
|
|
|
|
conn, chans, reqs, err := ssh.NewClientConn(c1, addr, config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ssh.NewClient(conn, chans, reqs), nil
|
|
}
|
|
|
|
func (s *server) Dial(config *ssh.ClientConfig) *ssh.Client {
|
|
conn, err := s.TryDial(config)
|
|
if err != nil {
|
|
s.t.Fatalf("ssh.Client: %v", err)
|
|
}
|
|
return conn
|
|
}
|
|
|
|
// generate random password
|
|
func randomPassword() (string, error) {
|
|
b := make([]byte, 12)
|
|
_, err := rand.Read(b)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
|
}
|
|
|
|
// setTestPassword is used for setting user and password data for sshd_test_pw.so
|
|
// This function also checks that ./sshd_test_pw.so exists and if not calls s.t.Skip()
|
|
func (s *server) setTestPassword(user, passwd string) error {
|
|
wd, _ := os.Getwd()
|
|
wrapper := filepath.Join(wd, "sshd_test_pw.so")
|
|
if _, err := os.Stat(wrapper); err != nil {
|
|
s.t.Skip(fmt.Errorf("sshd_test_pw.so is not available"))
|
|
return err
|
|
}
|
|
|
|
s.sshdTestPwSo = wrapper
|
|
s.testUser = user
|
|
s.testPasswd = passwd
|
|
return nil
|
|
}
|
|
|
|
// newServer returns a new mock ssh server.
|
|
func newServer(t *testing.T) *server {
|
|
return newServerForConfig(t, "default", map[string]string{})
|
|
}
|
|
|
|
// newServerForConfig returns a new mock ssh server.
|
|
func newServerForConfig(t *testing.T, config string, configVars map[string]string) *server {
|
|
if testing.Short() {
|
|
t.Skip("skipping test due to -short")
|
|
}
|
|
u, err := user.Current()
|
|
if err != nil {
|
|
t.Fatalf("user.Current: %v", err)
|
|
}
|
|
uname := u.Name
|
|
if uname == "" {
|
|
// Check the value of u.Username as u.Name
|
|
// can be "" on some OSes like AIX.
|
|
uname = u.Username
|
|
}
|
|
if uname == "root" {
|
|
t.Skip("skipping test because current user is root")
|
|
}
|
|
dir, err := os.MkdirTemp("", "sshtest")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, err := os.Create(filepath.Join(dir, "sshd_config"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := configTmpl[config]; ok == false {
|
|
t.Fatal(fmt.Errorf("Invalid server config '%s'", config))
|
|
}
|
|
configVars["Dir"] = dir
|
|
err = configTmpl[config].Execute(f, configVars)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.Close()
|
|
|
|
writeFile(filepath.Join(dir, "banner"), []byte("Server Banner"))
|
|
|
|
for k, v := range testdata.PEMBytes {
|
|
filename := "id_" + k
|
|
writeFile(filepath.Join(dir, filename), v)
|
|
writeFile(filepath.Join(dir, filename+".pub"), ssh.MarshalAuthorizedKey(testPublicKeys[k]))
|
|
}
|
|
|
|
for k, v := range testdata.SSHCertificates {
|
|
filename := "id_" + k + "-cert.pub"
|
|
writeFile(filepath.Join(dir, filename), v)
|
|
}
|
|
|
|
var authkeys bytes.Buffer
|
|
for k := range testdata.PEMBytes {
|
|
authkeys.Write(ssh.MarshalAuthorizedKey(testPublicKeys[k]))
|
|
}
|
|
writeFile(filepath.Join(dir, "authorized_keys"), authkeys.Bytes())
|
|
t.Cleanup(func() {
|
|
if err := os.RemoveAll(dir); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
|
|
return &server{
|
|
t: t,
|
|
configfile: f.Name(),
|
|
}
|
|
}
|
|
|
|
func newTempSocket(t *testing.T) (string, func()) {
|
|
dir, err := os.MkdirTemp("", "socket")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
deferFunc := func() { os.RemoveAll(dir) }
|
|
addr := filepath.Join(dir, "sock")
|
|
return addr, deferFunc
|
|
}
|