From 38b1b1158c9832d177c06442b81365a8636085ce Mon Sep 17 00:00:00 2001 From: Matej Vasek Date: Mon, 13 Sep 2021 16:02:52 +0200 Subject: [PATCH] Support for SSH connection Signed-off-by: Matej Vasek --- cmd/cmd.go | 178 +++- go.mod | 1 - go.sum | 6 +- internal/sshdialer/ssh_dialer.go | 288 ++++++ internal/sshdialer/ssh_dialer_test.go | 926 ++++++++++++++++++ internal/sshdialer/testdata/Dockerfile | 31 + internal/sshdialer/testdata/entrypoint.sh | 5 + .../testdata/etc/ssh/ssh_host_dsa_key | 21 + .../testdata/etc/ssh/ssh_host_dsa_key.pub | 1 + .../testdata/etc/ssh/ssh_host_ecdsa_key | 9 + .../testdata/etc/ssh/ssh_host_ecdsa_key.pub | 1 + .../testdata/etc/ssh/ssh_host_ed25519_key | 7 + .../testdata/etc/ssh/ssh_host_ed25519_key.pub | 1 + .../testdata/etc/ssh/ssh_host_rsa_key | 38 + .../testdata/etc/ssh/ssh_host_rsa_key.pub | 1 + .../sshdialer/testdata/etc/ssh/sshd_config | 119 +++ internal/sshdialer/testdata/go.mod | 3 + internal/sshdialer/testdata/id_dsa | 2 + internal/sshdialer/testdata/id_dsa.pub | 2 + internal/sshdialer/testdata/id_ed25519 | 7 + internal/sshdialer/testdata/id_ed25519.pub | 1 + internal/sshdialer/testdata/id_rsa | 50 + internal/sshdialer/testdata/id_rsa.pub | 1 + internal/sshdialer/testdata/main.go | 33 + 24 files changed, 1725 insertions(+), 7 deletions(-) create mode 100644 internal/sshdialer/ssh_dialer.go create mode 100644 internal/sshdialer/ssh_dialer_test.go create mode 100644 internal/sshdialer/testdata/Dockerfile create mode 100755 internal/sshdialer/testdata/entrypoint.sh create mode 100644 internal/sshdialer/testdata/etc/ssh/ssh_host_dsa_key create mode 100644 internal/sshdialer/testdata/etc/ssh/ssh_host_dsa_key.pub create mode 100644 internal/sshdialer/testdata/etc/ssh/ssh_host_ecdsa_key create mode 100644 internal/sshdialer/testdata/etc/ssh/ssh_host_ecdsa_key.pub create mode 100644 internal/sshdialer/testdata/etc/ssh/ssh_host_ed25519_key create mode 100644 internal/sshdialer/testdata/etc/ssh/ssh_host_ed25519_key.pub create mode 100644 internal/sshdialer/testdata/etc/ssh/ssh_host_rsa_key create mode 100644 internal/sshdialer/testdata/etc/ssh/ssh_host_rsa_key.pub create mode 100644 internal/sshdialer/testdata/etc/ssh/sshd_config create mode 100644 internal/sshdialer/testdata/go.mod create mode 100644 internal/sshdialer/testdata/id_dsa create mode 100644 internal/sshdialer/testdata/id_dsa.pub create mode 100644 internal/sshdialer/testdata/id_ed25519 create mode 100644 internal/sshdialer/testdata/id_ed25519.pub create mode 100644 internal/sshdialer/testdata/id_rsa create mode 100644 internal/sshdialer/testdata/id_rsa.pub create mode 100644 internal/sshdialer/testdata/main.go diff --git a/cmd/cmd.go b/cmd/cmd.go index 2e54061c09..d99192f18c 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -1,6 +1,23 @@ package cmd import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "strings" + + "golang.org/x/crypto/ssh" + "golang.org/x/term" + + "github.com/buildpacks/pack/internal/sshdialer" + + dockerClient "github.com/docker/docker/client" "github.com/heroku/color" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -133,10 +150,169 @@ func initConfig() (config.Config, string, error) { } func initClient(logger logging.Logger, cfg config.Config) (pack.Client, error) { - client, err := pack.NewClient(pack.WithLogger(logger), pack.WithExperimental(cfg.Experimental), pack.WithRegistryMirrors(cfg.RegistryMirrors)) + dc, err := initDockerClient() + if err != nil { + return pack.Client{}, err + } + client, err := pack.NewClient(pack.WithLogger(logger), pack.WithExperimental(cfg.Experimental), pack.WithRegistryMirrors(cfg.RegistryMirrors), pack.WithDockerClient(dc)) if err != nil { return pack.Client{}, err } return *client, nil } + +func initDockerClient() (dockerClient.CommonAPIClient, error) { + dockerClientOpts := []dockerClient.Opt{ + dockerClient.WithVersion("1.38"), + } + + usesSSH := false + if dockerHost, ok := os.LookupEnv("DOCKER_HOST"); ok { + if _url, err := url.Parse(dockerHost); err == nil && _url.Scheme == "ssh" { + usesSSH = true + credentialsConfig := sshdialer.Config{ + Identity: os.Getenv("DOCKER_HOST_SSH_IDENTITY"), + PassPhrase: os.Getenv("DOCKER_HOST_SSH_IDENTITY_PASSPHRASE"), + PasswordCallback: newPasswordCbk(), + PassPhraseCallback: newPassPhraseCbk(), + HostKeyCallback: newHostKeyCbk(), + } + dialer, err := sshdialer.NewDialer(_url, credentialsConfig) + if err != nil { + return nil, err + } + + path := _url.Path + if path == "" { + path = "/var/run/docker.sock" + } + + dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialer.DialContext(ctx, "unix", path) + } + + httpClient := &http.Client{ + // No tls + // No proxy + Transport: &http.Transport{ + DialContext: dialContext, + }, + } + dockerClientOpts = append(dockerClientOpts, + dockerClient.WithHTTPClient(httpClient), + dockerClient.WithHost("http://example.com/"), + dockerClient.WithDialContext(dialContext), + ) + } + } + if !usesSSH { + dockerClientOpts = append(dockerClientOpts, dockerClient.FromEnv) + } + + return dockerClient.NewClientWithOpts(dockerClientOpts...) +} + +// readSecret prompts for a secret and returns value input by user from stdin +// Unlike terminal.ReadPassword(), $(echo $SECRET | podman...) is supported. +// Additionally, all input after `/n` is queued to podman command. +// +// NOTE: this code is based on "github.com/containers/podman/v3/pkg/terminal" +func readSecret(prompt string) (pw []byte, err error) { + fd := int(os.Stdin.Fd()) + if term.IsTerminal(fd) { + fmt.Fprint(os.Stderr, prompt) + pw, err = term.ReadPassword(fd) + fmt.Fprintln(os.Stderr) + return + } + + var b [1]byte + for { + n, err := os.Stdin.Read(b[:]) + // terminal.readSecret discards any '\r', so we do the same + if n > 0 && b[0] != '\r' { + if b[0] == '\n' { + return pw, nil + } + pw = append(pw, b[0]) + // limit size, so that a wrong input won't fill up the memory + if len(pw) > 1024 { + err = errors.New("password too long, 1024 byte limit") + } + } + if err != nil { + // terminal.readSecret accepts EOF-terminated passwords + // if non-empty, so we do the same + if err == io.EOF && len(pw) > 0 { + err = nil + } + return pw, err + } + } +} + +func newPasswordCbk() sshdialer.PasswordCallback { + var pwdSet bool + var pwd string + return func() (string, error) { + if pwdSet { + return pwd, nil + } + + p, err := readSecret("please enter password:") + if err != nil { + return "", err + } + pwdSet = true + pwd = string(p) + + return pwd, err + } +} + +func newPassPhraseCbk() sshdialer.PassPhraseCallback { + var pwdSet bool + var pwd string + return func() (string, error) { + if pwdSet { + return pwd, nil + } + + p, err := readSecret("please enter passphrase to private key:") + if err != nil { + return "", err + } + pwdSet = true + pwd = string(p) + + return pwd, err + } +} + +func newHostKeyCbk() sshdialer.HostKeyCallback { + var trust []byte + return func(hostPort string, pubKey ssh.PublicKey) error { + if bytes.Equal(trust, pubKey.Marshal()) { + return nil + } + msg := `The authenticity of host %s cannot be established. +%s key fingerprint is SHA256:%s +Are you sure you want to continue connecting (yes/no)? ` + fmt.Fprintf(os.Stdout, msg, hostPort, pubKey.Type(), ssh.FingerprintSHA256(pubKey)) + reader := bufio.NewReader(os.Stdin) + answer, err := reader.ReadString('\n') + if err != nil { + return err + } + answer = strings.TrimRight(answer, "\r\n") + answer = strings.ToLower(answer) + + if answer == "yes" || answer == "y" { + trust = pubKey.Marshal() + return nil + } + + return errors.New("key rejected") + } +} diff --git a/go.mod b/go.mod index 811a7f7d1e..59de524029 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,6 @@ require ( github.com/mattn/go-colorable v0.1.8 // indirect github.com/mitchellh/ioprogress v0.0.0-20180201004757-6a23b12fa88e github.com/moby/sys/mount v0.2.0 // indirect - github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf // indirect github.com/onsi/gomega v1.16.0 github.com/opencontainers/image-spec v1.0.1 github.com/pelletier/go-toml v1.9.4 diff --git a/go.sum b/go.sum index 0fcead5234..19f5c995e9 100644 --- a/go.sum +++ b/go.sum @@ -251,8 +251,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= @@ -590,9 +588,8 @@ github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2J github.com/moby/sys/mountinfo v0.4.1 h1:1O+1cHA1aujwEwwVMa2Xm2l+gIpUHyd3+D+d7LZh1kM= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd h1:aY7OQNf2XqY/JQ6qREWamhI/81os/agb2BAGpcx5yWI= github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= -github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf h1:Un6PNx5oMK6CCwO3QTUyPiK2mtZnPrpDl5UnZ64eCkw= -github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -1042,7 +1039,6 @@ golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/sshdialer/ssh_dialer.go b/internal/sshdialer/ssh_dialer.go new file mode 100644 index 0000000000..46e4da53a3 --- /dev/null +++ b/internal/sshdialer/ssh_dialer.go @@ -0,0 +1,288 @@ +// NOTE: this code is based on "github.com/containers/podman/v3/pkg/bindings" + +package sshdialer + +import ( + "bufio" + "bytes" + "context" + "errors" + "fmt" + "io/ioutil" + "net" + urlPkg "net/url" + "os" + "path/filepath" + "time" + + "github.com/docker/docker/pkg/homedir" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" + "golang.org/x/crypto/ssh/knownhosts" +) + +type PasswordCallback func() (string, error) +type PassPhraseCallback func() (string, error) +type HostKeyCallback func(hostPort string, pubKey ssh.PublicKey) error + +type Config struct { + Identity string + PassPhrase string + PasswordCallback PasswordCallback + PassPhraseCallback PassPhraseCallback + HostKeyCallback HostKeyCallback +} + +type Dialer struct { + sshClient *ssh.Client +} + +func NewDialer(url *urlPkg.URL, config Config) (*Dialer, error) { + sshConfig, err := prepareConfig(url, config) + if err != nil { + return nil, err + } + + port := url.Port() + if port == "" { + port = "22" + } + host := url.Hostname() + + sshClient, err := ssh.Dial("tcp", net.JoinHostPort(host, port), sshConfig) + if err != nil { + return nil, fmt.Errorf("failed to dial ssh: %w", err) + } + + return &Dialer{sshClient: sshClient}, nil +} + +func (d *Dialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { + conn, err := d.Dial(network, addr) + if err != nil { + return nil, err + } + go func() { + <-ctx.Done() + conn.Close() + }() + return conn, nil +} + +func (d *Dialer) Dial(network, addr string) (net.Conn, error) { + return d.sshClient.Dial(network, addr) +} + +func (d *Dialer) Close() error { + return d.sshClient.Close() +} + +// Default key names. +var knownKeyNames = []string{"id_rsa", "id_dsa", "id_ecdsa", "id_ecdsa_sk", "id_ed25519", "id_ed25519_sk"} + +func prepareConfig(url *urlPkg.URL, credentialsConfig Config) (*ssh.ClientConfig, error) { + var ( + authMethods []ssh.AuthMethod + signers []ssh.Signer + err error + ) + + if pw, found := url.User.Password(); found { + authMethods = append(authMethods, ssh.Password(pw)) + } + + // add signer from explicit identity parameter + if credentialsConfig.Identity != "" { + s, err := publicKey(credentialsConfig.Identity, []byte(credentialsConfig.Identity), credentialsConfig.PassPhraseCallback) + if err != nil { + return nil, fmt.Errorf("failed to parse identity file: %w", err) + } + signers = append(signers, s) + } + + // add signers from ssh-agent + if sock, found := os.LookupEnv("SSH_AUTH_SOCK"); found && sock != "" { + var agentSigners []ssh.Signer + var agentConn net.Conn + agentConn, err = net.Dial("unix", sock) + if err != nil { + return nil, fmt.Errorf("failed to connect to ssh-agent's socket: %w", err) + } + agentSigners, err = agent.NewClient(agentConn).Signers() + if err != nil { + return nil, fmt.Errorf("failed to get signers from ssh-agent: %w", err) + } + signers = append(signers, agentSigners...) + } + + // if there is no explicit identity file nor keys from ssh-agent then + // add keys with standard name from ~/.ssh/ + if len(signers) == 0 { + var defaultKeyPaths []string + if home, err := os.UserHomeDir(); err == nil { + for _, keyName := range knownKeyNames { + p := filepath.Join(home, ".ssh", keyName) + + fi, err := os.Stat(p) + if err != nil { + continue + } + if fi.Mode().IsRegular() { + defaultKeyPaths = append(defaultKeyPaths, p) + } + } + } + + if len(defaultKeyPaths) == 1 { + s, err := publicKey(defaultKeyPaths[0], []byte(credentialsConfig.PassPhrase), credentialsConfig.PassPhraseCallback) + if err != nil { + return nil, err + } + signers = append(signers, s) + } + } + + if len(signers) > 0 { + var dedup = make(map[string]ssh.Signer) + // Dedup signers based on fingerprint, ssh-agent keys override explicit identity + for _, s := range signers { + fp := ssh.FingerprintSHA256(s.PublicKey()) + //if _, found := dedup[fp]; found { + // key updated + //} + dedup[fp] = s + } + + var uniq []ssh.Signer + for _, s := range dedup { + uniq = append(uniq, s) + } + authMethods = append(authMethods, ssh.PublicKeysCallback(func() ([]ssh.Signer, error) { + return uniq, nil + })) + } + + if len(authMethods) == 0 && credentialsConfig.PasswordCallback != nil { + authMethods = append(authMethods, ssh.PasswordCallback(credentialsConfig.PasswordCallback)) + } + + const sshTimeout = 5 + clientConfig := &ssh.ClientConfig{ + User: url.User.Username(), + Auth: authMethods, + HostKeyCallback: createHostKeyCallback(credentialsConfig.HostKeyCallback), + HostKeyAlgorithms: []string{ + ssh.KeyAlgoECDSA256, + ssh.KeyAlgoECDSA384, + ssh.KeyAlgoECDSA521, + ssh.KeyAlgoED25519, + ssh.SigAlgoRSASHA2512, + ssh.SigAlgoRSASHA2256, + ssh.KeyAlgoRSA, + ssh.KeyAlgoDSA, + }, + Timeout: sshTimeout * time.Second, + } + + return clientConfig, nil +} + +func publicKey(path string, passphrase []byte, passPhraseCallback PassPhraseCallback) (ssh.Signer, error) { + key, err := ioutil.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read key file: %w", err) + } + + signer, err := ssh.ParsePrivateKey(key) + if err != nil { + var missingPhraseError *ssh.PassphraseMissingError + if ok := errors.As(err, &missingPhraseError); !ok { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + + if len(passphrase) == 0 && passPhraseCallback != nil { + b, err := passPhraseCallback() + if err != nil { + return nil, err + } + passphrase = []byte(b) + } + + return ssh.ParsePrivateKeyWithPassphrase(key, passphrase) + } + + return signer, nil +} + +func createHostKeyCallback(hostKeyCallback HostKeyCallback) func(hostPort string, remote net.Addr, key ssh.PublicKey) error { + return func(hostPort string, remote net.Addr, pubKey ssh.PublicKey) error { + host, port := hostPort, "22" + if _h, _p, err := net.SplitHostPort(host); err == nil { + host, port = _h, _p + } + + knownHosts := filepath.Join(homedir.Get(), ".ssh", "known_hosts") + + _, err := os.Stat(knownHosts) + if err != nil && errors.Is(err, os.ErrNotExist) { + if hostKeyCallback != nil && hostKeyCallback(hostPort, pubKey) == nil { + return nil + } + return errUnknownServerKey + } + + f, err := os.Open(knownHosts) + if err != nil { + return fmt.Errorf("failed to open known_hosts: %w", err) + } + defer f.Close() + + hashhost := knownhosts.HashHostname(host) + + var errs []error + scanner := bufio.NewScanner(f) + for scanner.Scan() { + _, hostPorts, _key, _, _, err := ssh.ParseKnownHosts(scanner.Bytes()) + if err != nil { + errs = append(errs, err) + continue + } + + for _, hp := range hostPorts { + h, p := hp, "22" + if _h, _p, err := net.SplitHostPort(hp); err == nil { + h, p = _h, _p + } + + if (h == host || h == hashhost) && port == p { + if pubKey.Type() != _key.Type() { + errs = append(errs, fmt.Errorf("missmatch in type of a key")) + continue + } + if bytes.Equal(_key.Marshal(), pubKey.Marshal()) { + return nil + } + + return errBadServerKey + } + } + } + + if hostKeyCallback != nil && hostKeyCallback(hostPort, pubKey) == nil { + return nil + } + + if len(errs) > 0 { + return fmt.Errorf("server is not trusted (%v)", errs) + } + + return errUnknownServerKey + } +} + +var ErrBadServerKeyMsg = "server key for given host differs from key in known_host" +var ErrUnknownServerKeyMsg = "server key not found in known_hosts" + +// I would expose those but since ssh pkg doesn't do correct error wrapping it would be entirely futile +var errBadServerKey = errors.New(ErrBadServerKeyMsg) +var errUnknownServerKey = errors.New(ErrUnknownServerKeyMsg) diff --git a/internal/sshdialer/ssh_dialer_test.go b/internal/sshdialer/ssh_dialer_test.go new file mode 100644 index 0000000000..d083e95d7f --- /dev/null +++ b/internal/sshdialer/ssh_dialer_test.go @@ -0,0 +1,926 @@ +package sshdialer_test + +import ( + "context" + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "testing" + "text/template" + "time" + + "github.com/buildpacks/pack/internal/sshdialer" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/homedir" + "github.com/docker/go-connections/nat" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" + + th "github.com/buildpacks/pack/testhelpers" +) + +const ( + imageName = "buildpacks/sshdialer-test-img" + containerName = "sshdialer-test-ctr" +) + +var containerIP4 = "" +var containerIP6 = "" + +// We need to set up the test container running sshd against which we will run tests. +// This will return IPv4 and IPv6 of the container, +// cleanUp procedure to remove the test container and possibly error. +func prepareSSHServer(t *testing.T) (cleanUp func(), err error) { + t.Helper() + + th.RequireDocker(t) + + containerIP4 = "127.0.0.1" + containerIP6 = "::1" + + var cleanUps []func() + cleanUp = func() { + for i := range cleanUps { + cleanUps[i]() + } + } + + ctx := context.Background() + + cli, err := client.NewClientWithOpts( + client.FromEnv, + client.WithAPIVersionNegotiation(), + ) + if err != nil { + return + } + + info, err := cli.Info(ctx) + th.SkipIf(t, info.OSType == "windows", "These tests are not yet compatible with Windows-based containers") + + wd, err := os.Getwd() + if err != nil { + return + } + + th.CreateImageFromDir(t, cli, imageName, filepath.Join(wd, "testdata")) + + config := container.Config{ + Image: imageName, + } + + var hostConfig *container.HostConfig + if runtime.GOOS != "linux" { + hostConfig = &container.HostConfig{ + PortBindings: map[nat.Port][]nat.PortBinding{ + "22/tcp": {nat.PortBinding{HostIP: "localhost", HostPort: "22"}}, + "2222/tcp": {nat.PortBinding{HostIP: "localhost", HostPort: "2222"}}, + }, + } + } + ctr, err := cli.ContainerCreate(ctx, &config, hostConfig, nil, nil, containerName) + if err != nil { + return + } + + defer func() { + f := func() { cli.ContainerRemove(ctx, ctr.ID, types.ContainerRemoveOptions{Force: true}) } + if err != nil { + f() + } else { + cleanUps = append(cleanUps, f) + } + }() + + ctrStartOpts := types.ContainerStartOptions{} + err = cli.ContainerStart(ctx, ctr.ID, ctrStartOpts) + if err != nil { + return + } + + defer func() { + f := func() { cli.ContainerKill(ctx, ctr.ID, "SIGKILL") } + if err != nil { + f() + } else { + cleanUps = append(cleanUps, f) + } + }() + + var ctrJSON types.ContainerJSON + if runtime.GOOS == "linux" { + ctrJSON, err = cli.ContainerInspect(ctx, ctr.ID) + if err != nil { + return + } + + containerIP4 = ctrJSON.NetworkSettings.IPAddress + containerIP6 = ctrJSON.NetworkSettings.GlobalIPv6Address + } + + // wait for ssh container to start serving ssh + timeoutChan := time.After(time.Second * 10) + for { + select { + case <-timeoutChan: + err = fmt.Errorf("test container failed to start serving ssh") + return + case <-time.After(time.Millisecond * 500): + } + + conn, err := net.Dial("tcp", net.JoinHostPort(containerIP4, "22")) + if err != nil { + continue + } + conn.Close() + + break + } + + return cleanUp, err +} + +// function that prepares testing environment and returns clean up function +// this should be used in conjunction with defer: `defer fn()()` +// e.g. sets environment variables or starts mock up services +// it returns clean up procedure that restores old values of environment variables +// or shuts down mock up services +type setUpEnvFn func(t *testing.T) func() + +// combines multiple setUp routines into one setUp routine +func all(fns ...setUpEnvFn) setUpEnvFn { + return func(t *testing.T) func() { + t.Helper() + var cleanUps []func() + for _, fn := range fns { + cleanUps = append(cleanUps, fn(t)) + } + + return func() { + for i := len(cleanUps) - 1; i >= 0; i-- { + cleanUps[i]() + } + } + } +} + +func TestCreateDialer(t *testing.T) { + defer withoutSSHAgent(t)() + defer withCleanHome(t)() + + cleanUp, err := prepareSSHServer(t) + if err != nil { + t.Fatal(err) + } + defer cleanUp() + + type args struct { + connStr string + credentialConfig sshdialer.Config + } + type testParams struct { + name string + args args + setUpEnv setUpEnvFn + skipOnWin bool + CreateError string + DialError string + } + tests := []testParams{ + { + name: "read password from input", + args: args{ + connStr: fmt.Sprintf("ssh://testuser@%s:2222/home/testuser/test.sock", containerIP4), + credentialConfig: sshdialer.Config{PasswordCallback: func() (string, error) { + return "idkfa", nil + }}, + }, + setUpEnv: all(withoutSSHAgent, withCleanHome, withKnowHosts), + }, + { + name: "password in url", + args: args{connStr: fmt.Sprintf("ssh://testuser:idkfa@%s:2222/home/testuser/test.sock", containerIP4)}, + setUpEnv: all(withoutSSHAgent, withCleanHome, withKnowHosts), + }, + { + name: "password in url standard ssh port", + args: args{connStr: fmt.Sprintf("ssh://testuser:idkfa@%s/home/testuser/test.sock", containerIP4)}, + setUpEnv: all(withoutSSHAgent, withCleanHome, withKnowHosts), + }, + { + name: "server key is not in known_hosts (the file doesn't exists)", + args: args{connStr: fmt.Sprintf("ssh://testuser:idkfa@%s:2222/home/testuser/test.sock", containerIP4)}, + setUpEnv: all(withoutSSHAgent, withCleanHome), + CreateError: sshdialer.ErrUnknownServerKeyMsg, + }, + { + name: "server key is not in known_hosts (the file exists)", + args: args{connStr: fmt.Sprintf("ssh://testuser:idkfa@%s:2222/home/testuser/test.sock", containerIP4)}, + setUpEnv: all(withoutSSHAgent, withCleanHome, withEmptyKnownHosts), + CreateError: sshdialer.ErrUnknownServerKeyMsg, + }, + { + name: "server key is not in known_hosts (the filed doesn't exists) - user force trust", + args: args{ + connStr: fmt.Sprintf("ssh://testuser:idkfa@%s:2222/home/testuser/test.sock", containerIP4), + credentialConfig: sshdialer.Config{HostKeyCallback: func(hostPort string, pubKey ssh.PublicKey) error { + return nil + }}, + }, + setUpEnv: all(withoutSSHAgent, withCleanHome), + }, + { + name: "server key is not in known_hosts (the file exists) - user force trust", + args: args{ + connStr: fmt.Sprintf("ssh://testuser:idkfa@%s:2222/home/testuser/test.sock", containerIP4), + credentialConfig: sshdialer.Config{HostKeyCallback: func(hostPort string, pubKey ssh.PublicKey) error { + return nil + }}, + }, + setUpEnv: all(withoutSSHAgent, withCleanHome, withEmptyKnownHosts), + }, + { + name: "server key does not match the respective key in known_host", + args: args{connStr: fmt.Sprintf("ssh://testuser:idkfa@%s:2222/home/testuser/test.sock", containerIP4)}, + setUpEnv: all(withoutSSHAgent, withCleanHome, withBadKnownHosts), + CreateError: sshdialer.ErrBadServerKeyMsg, + }, + { + name: "key from identity parameter", + args: args{ + connStr: fmt.Sprintf("ssh://testuser@%s:2222/home/testuser/test.sock", containerIP4), + credentialConfig: sshdialer.Config{Identity: filepath.Join("testdata", "id_ed25519")}, + }, + setUpEnv: all(withoutSSHAgent, withCleanHome, withKnowHosts), + }, + { + name: "key at standard location with need to read passphrase", + args: args{ + connStr: fmt.Sprintf("ssh://testuser@%s:2222/home/testuser/test.sock", containerIP4), + credentialConfig: sshdialer.Config{PassPhraseCallback: func() (string, error) { + return "idfa", nil + }}, + }, + setUpEnv: all(withoutSSHAgent, withCleanHome, withKey(t, "id_rsa"), withKnowHosts), + }, + { + name: "key at standard location with explicitly set passphrase", + args: args{ + connStr: fmt.Sprintf("ssh://testuser@%s:2222/home/testuser/test.sock", containerIP4), + credentialConfig: sshdialer.Config{PassPhrase: "idfa"}, + }, + setUpEnv: all(withoutSSHAgent, withCleanHome, withKey(t, "id_rsa"), withKnowHosts), + }, + { + name: "key at standard location with no passphrase", + args: args{connStr: fmt.Sprintf("ssh://testuser@%s:2222/home/testuser/test.sock", containerIP4)}, + setUpEnv: all(withoutSSHAgent, withCleanHome, withKey(t, "id_ed25519"), withKnowHosts), + }, + { + name: "key from ssh-agent", + args: args{connStr: fmt.Sprintf("ssh://testuser@%s:2222/home/testuser/test.sock", containerIP4)}, + setUpEnv: all(withGoodSSHAgent, withCleanHome, withKnowHosts), + }, + { + name: "password in url with IPv6", + args: args{connStr: fmt.Sprintf("ssh://testuser:idkfa@[%s]:2222/home/testuser/test.sock", containerIP6)}, + setUpEnv: all(withoutSSHAgent, withCleanHome, withKnowHosts), + }, + { + name: "password in url with IPv6 standard port", + args: args{connStr: fmt.Sprintf("ssh://testuser:idkfa@[%s]/home/testuser/test.sock", containerIP6)}, + setUpEnv: all(withoutSSHAgent, withCleanHome, withKnowHosts), + }, + { + name: "broken known host", + args: args{connStr: fmt.Sprintf("ssh://testuser:idkfa@%s:2222/home/testuser/test.sock", containerIP4)}, + setUpEnv: all(withoutSSHAgent, withCleanHome, withBrokenKnownHosts), + CreateError: "invalid entry in known_hosts", + }, + { + name: "inaccessible known host", + args: args{connStr: fmt.Sprintf("ssh://testuser:idkfa@%s:2222/home/testuser/test.sock", containerIP4)}, + setUpEnv: all(withoutSSHAgent, withCleanHome, withInaccessibleKnownHosts), + skipOnWin: true, + CreateError: "failed to open known_hosts", + }, + { + name: "failing pass phrase cbk", + args: args{ + connStr: fmt.Sprintf("ssh://testuser:idkfa@%s:2222/home/testuser/test.sock", containerIP4), + credentialConfig: sshdialer.Config{PassPhraseCallback: func() (string, error) { + return "", errors.New("test_error_msg") + }}, + }, + setUpEnv: all(withoutSSHAgent, withCleanHome, withKey(t, "id_rsa"), withKnowHosts), + CreateError: "test_error_msg", + }, + { + name: "with broken key at default location", + args: args{connStr: fmt.Sprintf("ssh://testuser:idkfa@%s:2222/home/testuser/test.sock", containerIP4)}, + setUpEnv: all(withoutSSHAgent, withCleanHome, withKey(t, "id_dsa"), withKnowHosts), + CreateError: "failed to parse private key", + }, + { + name: "with broken key explicit", + args: args{ + connStr: fmt.Sprintf("ssh://testuser:idkfa@%s:2222/home/testuser/test.sock", containerIP4), + credentialConfig: sshdialer.Config{Identity: filepath.Join("testdata", "id_dsa")}, + }, + setUpEnv: all(withoutSSHAgent, withCleanHome, withKnowHosts), + CreateError: "failed to parse private key", + }, + { + name: "with inaccessible key", + args: args{connStr: fmt.Sprintf("ssh://testuser:idkfa@%s:2222/home/testuser/test.sock", containerIP4)}, + setUpEnv: all(withoutSSHAgent, withCleanHome, withInaccessibleKey(t, "id_rsa"), withKnowHosts), + skipOnWin: true, + CreateError: "failed to read key file", + }, + { + name: "socket doesn't exist in remote", + args: args{ + connStr: fmt.Sprintf("ssh://testuser@%s:2222/does/not/exist/test.sock", containerIP4), + credentialConfig: sshdialer.Config{PasswordCallback: func() (string, error) { + return "idkfa", nil + }}, + }, + setUpEnv: all(withoutSSHAgent, withCleanHome, withKnowHosts), + DialError: "failed to dial unix socket in the remote", + }, + { + name: "ssh agent non-existent socket", + args: args{ + connStr: fmt.Sprintf("ssh://testuser@%s:2222/does/not/exist/test.sock", containerIP4), + }, + setUpEnv: all(withoutSSHAgentBadSocket, withCleanHome, withKnowHosts), + CreateError: "failed to connect to ssh-agent's socket", + }, + { + name: "bad ssh agent", + args: args{ + connStr: fmt.Sprintf("ssh://testuser@%s:2222/does/not/exist/test.sock", containerIP4), + }, + setUpEnv: all(withBadSSHAgent, withCleanHome, withKnowHosts), + CreateError: "failed to get signers from ssh-agent", + }, + } + + for _, ttx := range tests { + tt := ttx + t.Run(tt.name, func(t *testing.T) { + // this test cannot be parallelized as they use process wide environment variable $HOME + + u, err := url.Parse(tt.args.connStr) + if err != nil { + t.Fatal(err) + } + + if (u.Port() == "" || u.Port() == "22") && runtime.GOOS != "linux" { + t.Skip("skipping test against standard port (22) on non-linux platform") + } + + if net.ParseIP(u.Hostname()).To4() == nil && containerIP6 == "" { + t.Skip("skipping ipv6 test since test environment doesn't support ipv6 connection") + } + + if tt.skipOnWin && runtime.GOOS == "windows" { + t.Skip("skipping this test on windows") + } + + defer tt.setUpEnv(t)() + + dialer, err := sshdialer.NewDialer(u, tt.args.credentialConfig) + + if tt.CreateError == "" { + th.AssertEq(t, err, nil) + } else { + // I wish I could use errors.Is(), + // however foreign code is not wrapping errors thoroughly + if err != nil { + th.AssertContains(t, err.Error(), tt.CreateError) + } else { + t.Error("expected error but got nil") + } + } + if err != nil { + return + } + defer dialer.Close() + + dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialer.DialContext(ctx, "unix", u.Path) + } + + transport := http.Transport{DialContext: dialContext} + httpClient := http.Client{Transport: &transport} + + resp, err := httpClient.Get("http://docker/") + if tt.DialError == "" { + th.AssertEq(t, err, nil) + } else { + // I wish I could use errors.Is(), + // however foreign code is not wrapping errors thoroughly + if err != nil { + th.AssertContains(t, err.Error(), tt.CreateError) + } else { + t.Error("expected error but got nil") + } + } + if err != nil { + return + } + defer resp.Body.Close() + + b, err := ioutil.ReadAll(resp.Body) + th.AssertTrue(t, err == nil) + if err != nil { + return + } + th.AssertEq(t, string(b), "Hello there!") + }) + } +} + +func cp(src, dest string) error { + srcFs, err := os.Stat(src) + if err != nil { + return fmt.Errorf("the cp() function failed to stat source file: %w", err) + } + + data, err := ioutil.ReadFile(src) + if err != nil { + return fmt.Errorf("the cp() function failed to read source file: %w", err) + } + + _, err = os.Stat(dest) + if err == nil { + return fmt.Errorf("destination file already exists: %w", os.ErrExist) + } + + return ioutil.WriteFile(dest, data, srcFs.Mode()) +} + +// puts key from ./testdata/{keyName} to $HOME/.ssh/{keyName} +// those keys are also supposed to be in authorized_keys of the test container +func withKey(t *testing.T, keyName string) func(t *testing.T) func() { + t.Helper() + + return func(t *testing.T) func() { + t.Helper() + var err error + + home, err := os.UserHomeDir() + if err != nil { + t.Fatal(err) + } + + err = os.MkdirAll(filepath.Join(home, ".ssh"), 0700) + if err != nil { + t.Fatal(err) + } + + keySrc := filepath.Join("testdata", keyName) + keyDest := filepath.Join(home, ".ssh", keyName) + err = cp(keySrc, keyDest) + if err != nil { + t.Fatal(err) + } + + return func() { + os.Remove(keyDest) + } + } +} + +func withInaccessibleKey(t *testing.T, keyName string) func(t *testing.T) func() { + t.Helper() + + return func(t *testing.T) func() { + t.Helper() + var err error + + home, err := os.UserHomeDir() + if err != nil { + t.Fatal(err) + } + + err = os.MkdirAll(filepath.Join(home, ".ssh"), 0700) + if err != nil { + t.Fatal(err) + } + + keyDest := filepath.Join(home, ".ssh", keyName) + _, err = os.OpenFile(keyDest, os.O_CREATE|os.O_WRONLY, 0000) + if err != nil { + t.Fatal(err) + } + + return func() { + os.Remove(keyDest) + } + } +} + +// sets clean temporary $HOME for test +// this prevents interaction with actual user home which may contain .ssh/ +func withCleanHome(t *testing.T) func() { + t.Helper() + homeName := "HOME" + if runtime.GOOS == "windows" { + homeName = "USERPROFILE" + } + tmpDir, err := ioutil.TempDir("", "tmpHome") + if err != nil { + t.Fatal(err) + } + oldHome, hadHome := os.LookupEnv(homeName) + os.Setenv(homeName, tmpDir) + + return func() { + if hadHome { + os.Setenv(homeName, oldHome) + } else { + os.Unsetenv(homeName) + } + os.RemoveAll(tmpDir) + } +} + +// generates `known_hosts` with test container keys and puts it into $HOME/.ssh/known_hosts +func withKnowHosts(t *testing.T) func() { + t.Helper() + knownHosts := filepath.Join(homedir.Get(), ".ssh", "known_hosts") + + err := os.MkdirAll(filepath.Join(homedir.Get(), ".ssh"), 0700) + if err != nil { + t.Fatal(err) + } + + _, err = os.Stat(knownHosts) + if err == nil || !errors.Is(err, os.ErrNotExist) { + t.Fatal("known_hosts already exists") + } + + f, err := os.OpenFile(knownHosts, os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + // generate known_hosts + serverKeysDir := filepath.Join("testdata", "etc", "ssh") + for _, k := range []string{"ecdsa"} { + keyPath := filepath.Join(serverKeysDir, fmt.Sprintf("ssh_host_%s_key.pub", k)) + key, err := ioutil.ReadFile(keyPath) + if err != nil { + t.Fatal(t) + } + + fmt.Fprintf(f, "%s %s", containerIP4, string(key)) + fmt.Fprintf(f, "[%s]:2222 %s", containerIP4, string(key)) + + if containerIP6 != "" { + fmt.Fprintf(f, "%s %s", containerIP6, string(key)) + fmt.Fprintf(f, "[%s]:2222 %s", containerIP6, string(key)) + } + } + + return func() { + os.Remove(knownHosts) + } +} + +// creates $HOME/.ssh/known_hosts such that is does not match with keys in the test container +func withBadKnownHosts(t *testing.T) func() { + t.Helper() + + knownHosts := filepath.Join(homedir.Get(), ".ssh", "known_hosts") + + err := os.MkdirAll(filepath.Join(homedir.Get(), ".ssh"), 0700) + if err != nil { + t.Fatal(err) + } + + _, err = os.Stat(knownHosts) + if err == nil || !errors.Is(err, os.ErrNotExist) { + t.Fatal("known_hosts already exists") + } + + f, err := os.OpenFile(knownHosts, os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + knownHostTemplate := `{{range $host := .}}{{$host}} ssh-dss AAAAB3NzaC1kc3MAAACBAKH4ufS3ABVb780oTgEL1eu+pI1p6YOq/1KJn5s3zm+L3cXXq76r5OM/roGEYrXWUDGRtfVpzYTAKoMWuqcVc0AZ2zOdYkoy1fSjJ3MqDGF53QEO3TXIUt3gUzmLOewwmZWle0RgMa9GHccv7XVVIZB36RR68ZEUswLaTnlVhXQ1AAAAFQCl4t/LnY7kuUI+tL2qT2XmxmiyqwAAAIB72XaO+LfyIiqBOaTkQf+5rvH1i6y6LDO1QD9pzGWUYw3y03AEveHJMjW0EjnYBKJjK39wcZNTieRyU54lhH/HWeWABn9NcQ3duEf1WSO/s7SPsFO2R6quqVSsStkqf2Yfdy4fl24mH41olwtNA6ft5nkVfkqrIa51si4jU8fBVAAAAIB8SSvyYBcyMGLUlQjzQqhhhAHer9x/1YbknVz+y5PHJLLjHjMC4ZRfLgNEojvMKQW46Te9Pwnudcwv19ho4F+kkCOfss7xjyH70gQm6Sj76DxClmnnPoSRq3qEAOMy5Oh+7vyzxm68KHqd/aOmUaiT1LgqgViS9+kNdCoVMGAMOg== mvasek@bellatrix +{{$host}} ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLTxVVaQ93ReqHNlbjg5/nBRpuRuG6JIgNeJXWT1V4Dl+dMMrnad3uJBfyrNpvn8rv2qnn6gMTZVtTbLdo96pG0= mvasek@bellatrix +{{$host}} ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOKymJNQszrxetVffPZRfZGKWK786r0mNcg/Wah4+2wn mvasek@bellatrix +{{$host}} ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/1/OCwec2Gyv5goNYYvos4iOA+a0NolOGsZA/93jmSArPY1zZS1UWeJ6dDTmxGoL/e7jm9lM6NJY7a/zM0C/GqCNRGR/aCUHBJTIgGtH+79FDKO/LWY6ClGY7Lw8qNgZpugbBw3N3HqTtyb2lELhFLT0FEb+le4WUbryooLK2zsz6DnqV4JvTYyyHcanS0h68iSXC7XbkZchvL99l5LT0gD1oDteBPKKFdNOwIjpMkk/IrbFM24xoNkaTDXN87EpQPQzYDfsoGymprc5OZZ8kzrtErQR+yfuunHfzzqDHWi7ga5pbgkuxNt10djWgCfBRsy07FTEgV0JirS0TCfwTBbqRzdjf3dgi8AP+WtkW3mcv4a1XYeqoBo2o9TbfyiA9kERs79UBN0mCe3KNX3Ns0PvutsRLaHmdJ49eaKWkJ6GgL37aqSlIwTixz2xY3eoDSkqHoZpx6Q1MdpSIl5gGVzlaobM/PNM1jqVdyUj+xpjHyiXwHQMKc3eJna7s8Jc= mvasek@bellatrix +{{end}}` + + tmpl := template.New(knownHostTemplate) + tmpl, err = tmpl.Parse(knownHostTemplate) + if err != nil { + t.Fatal(err) + } + + hosts := make([]string, 0, 4) + hosts = append(hosts, containerIP4, fmt.Sprintf("[%s]:2222", containerIP4)) + if containerIP6 != "" { + hosts = append(hosts, containerIP6, fmt.Sprintf("[%s]:2222", containerIP6)) + } + + err = tmpl.Execute(f, hosts) + if err != nil { + t.Fatal(err) + } + + return func() { + os.Remove(knownHosts) + } +} + +func withBrokenKnownHosts(t *testing.T) func() { + t.Helper() + + knownHosts := filepath.Join(homedir.Get(), ".ssh", "known_hosts") + + err := os.MkdirAll(filepath.Join(homedir.Get(), ".ssh"), 0700) + if err != nil { + t.Fatal(err) + } + + _, err = os.Stat(knownHosts) + if err == nil || !errors.Is(err, os.ErrNotExist) { + t.Fatal("known_hosts already exists") + } + + f, err := os.OpenFile(knownHosts, os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + _, err = f.WriteString("somegarbage\nsome rubish\n stuff\tqwerty") + if err != nil { + t.Fatal(err) + } + + return func() { + os.Remove(knownHosts) + } +} + +func withInaccessibleKnownHosts(t *testing.T) func() { + t.Helper() + + knownHosts := filepath.Join(homedir.Get(), ".ssh", "known_hosts") + + err := os.MkdirAll(filepath.Join(homedir.Get(), ".ssh"), 0700) + if err != nil { + t.Fatal(err) + } + + _, err = os.Stat(knownHosts) + if err == nil || !errors.Is(err, os.ErrNotExist) { + t.Fatal("known_hosts already exists") + } + + f, err := os.OpenFile(knownHosts, os.O_CREATE|os.O_WRONLY, 0000) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + return func() { + os.Remove(knownHosts) + } +} + +func withEmptyKnownHosts(t *testing.T) func() { + t.Helper() + + knownHosts := filepath.Join(homedir.Get(), ".ssh", "known_hosts") + + err := os.MkdirAll(filepath.Join(homedir.Get(), ".ssh"), 0700) + if err != nil { + t.Fatal(err) + } + + _, err = os.Stat(knownHosts) + if err == nil || !errors.Is(err, os.ErrNotExist) { + t.Fatal("known_hosts already exists") + } + + _, err = os.Create(knownHosts) + if err != nil { + t.Fatal(err) + } + + return func() { + os.Remove(knownHosts) + } +} + +// unsets environment variable so ssh-agent is not used by test +func withoutSSHAgent(t *testing.T) func() { + t.Helper() + oldAuthSock, hadAuthSock := os.LookupEnv("SSH_AUTH_SOCK") + os.Unsetenv("SSH_AUTH_SOCK") + + return func() { + if hadAuthSock { + os.Setenv("SSH_AUTH_SOCK", oldAuthSock) + } else { + os.Unsetenv("SSH_AUTH_SOCK") + } + } +} + +// unsets environment variable so ssh-agent is not used by test +func withoutSSHAgentBadSocket(t *testing.T) func() { + t.Helper() + oldAuthSock, hadAuthSock := os.LookupEnv("SSH_AUTH_SOCK") + os.Setenv("SSH_AUTH_SOCK", "/does/not/exists.sock") + + return func() { + if hadAuthSock { + os.Setenv("SSH_AUTH_SOCK", oldAuthSock) + } else { + os.Unsetenv("SSH_AUTH_SOCK") + } + } +} + +// starts serving ssh-agent on temporary unix socket +// returns clean up routine that stops the server +func withGoodSSHAgent(t *testing.T) func() { + t.Helper() + + key, err := ioutil.ReadFile(filepath.Join("testdata", "id_ed25519")) + if err != nil { + t.Fatal(err) + } + signer, err := ssh.ParsePrivateKey(key) + if err != nil { + t.Fatal(err) + } + + return _withSSHAgent(t, signerAgent{signer}) +} + +func withBadSSHAgent(t *testing.T) func() { + return _withSSHAgent(t, badAgent{}) +} + +func _withSSHAgent(t *testing.T, ag agent.Agent) func() { + t.Helper() + tmpDirForSocket, err := ioutil.TempDir("", "tmpHome") + if err != nil { + t.Fatal(err) + } + agentSocketPath := filepath.Join(tmpDirForSocket, "agent.sock") + unixListener, err := net.Listen("unix", agentSocketPath) + if err != nil { + t.Fatal(err) + } + os.Setenv("SSH_AUTH_SOCK", agentSocketPath) + + ctx, cancel := context.WithCancel(context.Background()) + errChan := make(chan error, 1) + var wg sync.WaitGroup + + go func() { + for { + conn, err := unixListener.Accept() + if err != nil { + errChan <- err + + return + } + + wg.Add(1) + go func(conn net.Conn) { + defer wg.Done() + go func() { + <-ctx.Done() + conn.Close() + }() + err := agent.ServeAgent(ag, conn) + if err != nil { + // we can use this once we use go 1.16 + // if !errors.Is(err, net.ErrClosed) { + if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { + fmt.Fprintf(os.Stderr, "agent.ServeAgent() failed: %v\n", err) + } + } + }(conn) + } + }() + + return func() { + os.Unsetenv("SSH_AUTH_SOCK") + + err := unixListener.Close() + if err != nil { + t.Fatal(err) + } + err = <-errChan + + // we can use this once we use go 1.16 + // if !errors.Is(err, net.ErrClosed) { + if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { + t.Fatal(err) + } + cancel() + wg.Wait() + os.RemoveAll(tmpDirForSocket) + } +} + +type signerAgent struct { + impl ssh.Signer +} + +func (a signerAgent) List() ([]*agent.Key, error) { + return []*agent.Key{{ + Format: a.impl.PublicKey().Type(), + Blob: a.impl.PublicKey().Marshal(), + }}, nil +} + +func (a signerAgent) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { + return a.impl.Sign(nil, data) +} + +func (a signerAgent) Add(key agent.AddedKey) error { + panic("implement me") +} + +func (a signerAgent) Remove(key ssh.PublicKey) error { + panic("implement me") +} + +func (a signerAgent) RemoveAll() error { + panic("implement me") +} + +func (a signerAgent) Lock(passphrase []byte) error { + panic("implement me") +} + +func (a signerAgent) Unlock(passphrase []byte) error { + panic("implement me") +} + +func (a signerAgent) Signers() ([]ssh.Signer, error) { + panic("implement me") +} + +var errBadAgent = errors.New("bad agent error") + +type badAgent struct{} + +func (b badAgent) List() ([]*agent.Key, error) { + return nil, errBadAgent +} + +func (b badAgent) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { + return nil, errBadAgent +} + +func (b badAgent) Add(key agent.AddedKey) error { + return errBadAgent +} + +func (b badAgent) Remove(key ssh.PublicKey) error { + return errBadAgent +} + +func (b badAgent) RemoveAll() error { + return errBadAgent +} + +func (b badAgent) Lock(passphrase []byte) error { + return errBadAgent +} + +func (b badAgent) Unlock(passphrase []byte) error { + return errBadAgent +} + +func (b badAgent) Signers() ([]ssh.Signer, error) { + return nil, errBadAgent +} diff --git a/internal/sshdialer/testdata/Dockerfile b/internal/sshdialer/testdata/Dockerfile new file mode 100644 index 0000000000..d985033cec --- /dev/null +++ b/internal/sshdialer/testdata/Dockerfile @@ -0,0 +1,31 @@ +FROM docker.io/library/golang:1.16 AS builder + +RUN mkdir /workspace/ +COPY main.go go.mod /workspace/ +WORKDIR /workspace/ +ENV CGO_ENABLED=0 +RUN go build -o serve-socket + +FROM docker.io/library/alpine:3.13.6 + +RUN apk --update add --no-cache openssh bash shadow && rm -rf /var/cache/apk/* + +COPY etc/ssh /etc/ssh +RUN chmod og-rwx /etc/ssh/*_key +COPY entrypoint.sh /usr/local/bin +RUN chmod a+x /usr/local/bin/entrypoint.sh + +RUN addgroup testuser && adduser testuser -G testuser -D +RUN echo "root:iddqd" | chpasswd +RUN echo "testuser:idkfa" | chpasswd + +RUN su testuser && mkdir /home/testuser/.ssh/ +COPY --chown=testuser:testuser id_ed25519.pub id_rsa.pub /tmp/ +RUN cat /tmp/id_ed25519.pub /tmp/id_rsa.pub >> /home/testuser/.ssh/authorized_keys + +COPY --from=builder /workspace/serve-socket /usr/local/bin + +EXPOSE 22 +EXPOSE 2222 + +CMD ["/usr/local/bin/entrypoint.sh"] diff --git a/internal/sshdialer/testdata/entrypoint.sh b/internal/sshdialer/testdata/entrypoint.sh new file mode 100755 index 0000000000..9bf7b506b2 --- /dev/null +++ b/internal/sshdialer/testdata/entrypoint.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +/usr/sbin/sshd + +su - testuser -c '/usr/local/bin/serve-socket "/home/testuser/test.sock"' diff --git a/internal/sshdialer/testdata/etc/ssh/ssh_host_dsa_key b/internal/sshdialer/testdata/etc/ssh/ssh_host_dsa_key new file mode 100644 index 0000000000..b6cfa3ba17 --- /dev/null +++ b/internal/sshdialer/testdata/etc/ssh/ssh_host_dsa_key @@ -0,0 +1,21 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsgAAAAdzc2gtZH +NzAAAAgQDhtFRnJxDyn1xru4PNNnOyvaYlPzt+l8Jwu1PozLsU+2LPJmZQtYXjUl7d7w/P +MklwmwH/4xuQ+YjkaWD8I+4IA4FZfAJjEuTPXJsze48Fefcorg9TkxRxDPk6vetn3TKSuR +YeBpLDq+Dq1vo/2vBM70KahRQDxVNlYdNbiBG5dQAAABUAjg1kaaW8DHwj326XhjKpkoXR +zwEAAACAfSdptBoh5kQHp1ZAMr4EBzH+PMhRf6sdfHhpUOO+4uhRC5hqIgIm3IA1UG8H5V +2Py6xNvIvELX/UVrtzTeqjxZ8+47WKM+j6fTUp3gx+InLq23bD9U43BP+sUcXIs322LUkb +fBoQzWnXx8lkOQzaeofGc4Cg2NIIA5pWewsEACQAAACBAK2mPPqr8PSHjnW9bqv8MEuc3K +qKHuL4a5fGQA5G5upbFxXgB3MtstTtWO27LVD5wrlLOpEz7Lzwv51GUFpxYRTSv49qliKP +c31QAoEjlpST6Jv9JpERGcCTETwNwyH8TH4gx7Ku0JXaysyJH0rn2UTCUAhNmMpiO5tSb5 +PCwK94AAAB6ItbM9KLWzPSAAAAB3NzaC1kc3MAAACBAOG0VGcnEPKfXGu7g802c7K9piU/ +O36XwnC7U+jMuxT7Ys8mZlC1heNSXt3vD88ySXCbAf/jG5D5iORpYPwj7ggDgVl8AmMS5M +9cmzN7jwV59yiuD1OTFHEM+Tq962fdMpK5Fh4GksOr4OrW+j/a8EzvQpqFFAPFU2Vh01uI +Ebl1AAAAFQCODWRppbwMfCPfbpeGMqmShdHPAQAAAIB9J2m0GiHmRAenVkAyvgQHMf48yF +F/qx18eGlQ477i6FELmGoiAibcgDVQbwflXY/LrE28i8Qtf9RWu3NN6qPFnz7jtYoz6Pp9 +NSneDH4icurbdsP1TjcE/6xRxcizfbYtSRt8GhDNadfHyWQ5DNp6h8ZzgKDY0ggDmlZ7Cw +QAJAAAAIEAraY8+qvw9IeOdb1uq/wwS5zcqooe4vhrl8ZADkbm6lsXFeAHcy2y1O1Y7bst +UPnCuUs6kTPsvPC/nUZQWnFhFNK/j2qWIo9zfVACgSOWlJPom/0mkREZwJMRPA3DIfxMfi +DHsq7QldrKzIkfSufZRMJQCE2YymI7m1Jvk8LAr3gAAAAUdboNp8quoeOloagm/Or8qP1d +zwMAAAAQbXZhc2VrQGJlbGxhdHJpeAEC +-----END OPENSSH PRIVATE KEY----- diff --git a/internal/sshdialer/testdata/etc/ssh/ssh_host_dsa_key.pub b/internal/sshdialer/testdata/etc/ssh/ssh_host_dsa_key.pub new file mode 100644 index 0000000000..291a3f583e --- /dev/null +++ b/internal/sshdialer/testdata/etc/ssh/ssh_host_dsa_key.pub @@ -0,0 +1 @@ +ssh-dss AAAAB3NzaC1kc3MAAACBAOG0VGcnEPKfXGu7g802c7K9piU/O36XwnC7U+jMuxT7Ys8mZlC1heNSXt3vD88ySXCbAf/jG5D5iORpYPwj7ggDgVl8AmMS5M9cmzN7jwV59yiuD1OTFHEM+Tq962fdMpK5Fh4GksOr4OrW+j/a8EzvQpqFFAPFU2Vh01uIEbl1AAAAFQCODWRppbwMfCPfbpeGMqmShdHPAQAAAIB9J2m0GiHmRAenVkAyvgQHMf48yFF/qx18eGlQ477i6FELmGoiAibcgDVQbwflXY/LrE28i8Qtf9RWu3NN6qPFnz7jtYoz6Pp9NSneDH4icurbdsP1TjcE/6xRxcizfbYtSRt8GhDNadfHyWQ5DNp6h8ZzgKDY0ggDmlZ7CwQAJAAAAIEAraY8+qvw9IeOdb1uq/wwS5zcqooe4vhrl8ZADkbm6lsXFeAHcy2y1O1Y7bstUPnCuUs6kTPsvPC/nUZQWnFhFNK/j2qWIo9zfVACgSOWlJPom/0mkREZwJMRPA3DIfxMfiDHsq7QldrKzIkfSufZRMJQCE2YymI7m1Jvk8LAr3g= mvasek@bellatrix diff --git a/internal/sshdialer/testdata/etc/ssh/ssh_host_ecdsa_key b/internal/sshdialer/testdata/etc/ssh/ssh_host_ecdsa_key new file mode 100644 index 0000000000..08a9d8c0fa --- /dev/null +++ b/internal/sshdialer/testdata/etc/ssh/ssh_host_ecdsa_key @@ -0,0 +1,9 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS +1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQRDB1fPXY58fSwaqyoj5lCfLtQ/NcIs +grKTA11vypVy9MUCWtdAQIXczmtRMTFCVozk3lwt9M4iKc79nCkkkfyrAAAAsPgat2v4Gr +drAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEMHV89djnx9LBqr +KiPmUJ8u1D81wiyCspMDXW/KlXL0xQJa10BAhdzOa1ExMUJWjOTeXC30ziIpzv2cKSSR/K +sAAAAhAIcs2smJGAEKOvzL8Rfz5b1IpQqB8GzxycT3/53XOzaSAAAAEG12YXNla0BiZWxs +YXRyaXgBAgMEBQYH +-----END OPENSSH PRIVATE KEY----- diff --git a/internal/sshdialer/testdata/etc/ssh/ssh_host_ecdsa_key.pub b/internal/sshdialer/testdata/etc/ssh/ssh_host_ecdsa_key.pub new file mode 100644 index 0000000000..922ed1926d --- /dev/null +++ b/internal/sshdialer/testdata/etc/ssh/ssh_host_ecdsa_key.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEMHV89djnx9LBqrKiPmUJ8u1D81wiyCspMDXW/KlXL0xQJa10BAhdzOa1ExMUJWjOTeXC30ziIpzv2cKSSR/Ks= mvasek@bellatrix diff --git a/internal/sshdialer/testdata/etc/ssh/ssh_host_ed25519_key b/internal/sshdialer/testdata/etc/ssh/ssh_host_ed25519_key new file mode 100644 index 0000000000..914ede587b --- /dev/null +++ b/internal/sshdialer/testdata/etc/ssh/ssh_host_ed25519_key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAhhd7wuRBaf9R7Q/HQi7lEWoukb/HrYDg394NpeOgsbAAAAJjz02VI89Nl +SAAAAAtzc2gtZWQyNTUxOQAAACAhhd7wuRBaf9R7Q/HQi7lEWoukb/HrYDg394NpeOgsbA +AAAEC9lvHqAASWXcZSm/Rih3V78uMejs+6sc6SOVhaogLwHyGF3vC5EFp/1HtD8dCLuURa +i6Rv8etgODf3g2l46CxsAAAAEG12YXNla0BiZWxsYXRyaXgBAgMEBQ== +-----END OPENSSH PRIVATE KEY----- diff --git a/internal/sshdialer/testdata/etc/ssh/ssh_host_ed25519_key.pub b/internal/sshdialer/testdata/etc/ssh/ssh_host_ed25519_key.pub new file mode 100644 index 0000000000..328d03fc7c --- /dev/null +++ b/internal/sshdialer/testdata/etc/ssh/ssh_host_ed25519_key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICGF3vC5EFp/1HtD8dCLuURai6Rv8etgODf3g2l46Cxs mvasek@bellatrix diff --git a/internal/sshdialer/testdata/etc/ssh/ssh_host_rsa_key b/internal/sshdialer/testdata/etc/ssh/ssh_host_rsa_key new file mode 100644 index 0000000000..8958a0e2a5 --- /dev/null +++ b/internal/sshdialer/testdata/etc/ssh/ssh_host_rsa_key @@ -0,0 +1,38 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn +NhAAAAAwEAAQAAAYEA1VHmRdtHzOhCZCmcrYJdF9VjdPTXc9Hid9Bzexk0QfnG6C15gOyb +bmX5YVxgViHzrrpaodLDCYBWu4+l2DPeG5gaIa8a1WAhgvEQRfJh09JcVkVWdz9A7Umgtb +6k3d0QwlYaZrLS1SZGYg+ioxolebzAs/+dm1yfkSRXNf3fPuhyubO6AIBMBdcnIz3cwu3N +Zv6o3LshFYym7DaXA/LdHwmMZe2JaksqSbafbaxqZ6kIjvWUeOrI6R/3uLWM1BTMir9inn +8iRrcxnAtSVG+Q5XgvakPuZDXvzyPP66kOcnT1x8DDHcy4PD0SyztDIjHWXp/XBwqDqWQb +hiUBaOjeyYJ6qo9ZJnrGoaip5A8TlKtaDFS6aX1obHxYet+SvA5sKja/cx33NLh0FRfXXZ +mkngEvrmP9oGX/AegMTbzE+t2kKDU80Mye1LmxWN/e8Rudib41hVTZ0U8PZcHmgAOMcmGf +A4iVmWc+vEc5oYeY0WSk/zQKBTYNlQsNw6+O7qT3AAAFiCBt4WEgbeFhAAAAB3NzaC1yc2 +EAAAGBANVR5kXbR8zoQmQpnK2CXRfVY3T013PR4nfQc3sZNEH5xugteYDsm25l+WFcYFYh +8666WqHSwwmAVruPpdgz3huYGiGvGtVgIYLxEEXyYdPSXFZFVnc/QO1JoLW+pN3dEMJWGm +ay0tUmRmIPoqMaJXm8wLP/nZtcn5EkVzX93z7ocrmzugCATAXXJyM93MLtzWb+qNy7IRWM +puw2lwPy3R8JjGXtiWpLKkm2n22samepCI71lHjqyOkf97i1jNQUzIq/Yp5/Ika3MZwLUl +RvkOV4L2pD7mQ1788jz+upDnJ09cfAwx3MuDw9Ess7QyIx1l6f1wcKg6lkG4YlAWjo3smC +eqqPWSZ6xqGoqeQPE5SrWgxUuml9aGx8WHrfkrwObCo2v3Md9zS4dBUX112ZpJ4BL65j/a +Bl/wHoDE28xPrdpCg1PNDMntS5sVjf3vEbnYm+NYVU2dFPD2XB5oADjHJhnwOIlZlnPrxH +OaGHmNFkpP80CgU2DZULDcOvju6k9wAAAAMBAAEAAAGALccVk4grMF3nYXdMmC+RqruwTD +j+w2wXHX8uSQxvmnjvpoObv38HG/nmOm6IffNrR+PV70Q7dp6D/lwlSvBWibVqZjAdogyv +JFp3E4ugUsSh7CGVHKIGXOWgB2CSIMp//jRcFg3qELPWBtU0IaxKvoUzFW2VdPG7jHov/P +YuImHfvNpE4DaoGdjCHV35Mhu2KJQdyMCfqPA2IhrU7ZQAv9hcuMLw6k6XFJqMPAz0CKrN +m2A4LHq2AtFJZ+oN/rU3izl01xvH3WndwEJJ16L49ItvDvPCb8WJJJx5jmvMF7o3qJlOmM +T39JvPB1zyQnClv6wOHQDUPLR0MNiKJ2OFVZWw9Ay7hgYXERqazkKctI0Hh4DtgFJNtOSN +ML5EEAgEYRlKsikhYRra922Gi6p7DJthhtd/e2QnHKnRLX2fed0rkBVS5qlN7baR/ujF9n +5C+SN1+OsNSBhbe4aPAYapXKnN9UCDMyz6kDv0eSQs/arkgacoGzCtfnd/z5Jpl9kBAAAA +wETMrjKmEAVT/w2xheu/Asky7ldZ6L4Y/Tm6g5htjgJYMz6PGNrTLQTz5fFlH5yphps+g4 +oqYxJHgqbAD4DT4v8Lfc8CkQ/vy9xZ/jYrn6qD4+/5Mm8Vd7eOTvFJfBut8ZT9ZDvc5kGi +2rfKSq2kvq0mSGhcdLM+6jbjC4twIQ373O3i3M1XNAO/L7FFhMSex0go5O+41Ni08kHllq +vcQXd9XvVzGkgZTI3wWrFe6+C0h3qLLLrr4Kgt39ZoHoOsBwAAAMEA+/0ZAd4/7hogsstf +np+OXNS4deFssz630m7ncJQDUEpgCQnRZphwABJoinfzj9oKSBfuiVfgvL/DJn/OjNhNT+ +knGY1nVB3mHIQbqb+AeLKilO3QG4tjOZzI7uxN0mmswN/brHcEKd4lQP6AMyXndF5S6Xv5 +8HmdFm+8HvPmcDPS8Mh8NM2gaPVH1O67Iv//DUBeriaN1GqXBXOTnT03es8cAw10I5898U +KO9s/+Pe9dFcXVg7cemDNBr6Oz1E9JAAAAwQDYtzggjIQbtn3xHNC+5iIkXz+4oRkSYtC0 +Feymq7bmo3Xy1Dp85C2Oz3DSKjwR2mo5vPREV784NMiyIDFnnGxSX0Aq0xAPfmwvMlF9DU +N4aJ/LIA2Fk2c3M2ZirUi600QNDweuaNnPN5OFBLMHxp6+olBZ22A/utN9ErWxSDJIUlix +cDFkcDPhozdShr+hwzN3bGGJZq8+UZ6/y6gSb1gcNf7Ly1vupINRvmRin5xKNp/t4MaqZp +pAnGpiWXM8Ej8AAAAQbXZhc2VrQGJlbGxhdHJpeAECAw== +-----END OPENSSH PRIVATE KEY----- diff --git a/internal/sshdialer/testdata/etc/ssh/ssh_host_rsa_key.pub b/internal/sshdialer/testdata/etc/ssh/ssh_host_rsa_key.pub new file mode 100644 index 0000000000..6276d2d9ae --- /dev/null +++ b/internal/sshdialer/testdata/etc/ssh/ssh_host_rsa_key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDVUeZF20fM6EJkKZytgl0X1WN09Ndz0eJ30HN7GTRB+cboLXmA7JtuZflhXGBWIfOuulqh0sMJgFa7j6XYM94bmBohrxrVYCGC8RBF8mHT0lxWRVZ3P0DtSaC1vqTd3RDCVhpmstLVJkZiD6KjGiV5vMCz/52bXJ+RJFc1/d8+6HK5s7oAgEwF1ycjPdzC7c1m/qjcuyEVjKbsNpcD8t0fCYxl7YlqSypJtp9trGpnqQiO9ZR46sjpH/e4tYzUFMyKv2KefyJGtzGcC1JUb5DleC9qQ+5kNe/PI8/rqQ5ydPXHwMMdzLg8PRLLO0MiMdZen9cHCoOpZBuGJQFo6N7Jgnqqj1kmesahqKnkDxOUq1oMVLppfWhsfFh635K8DmwqNr9zHfc0uHQVF9ddmaSeAS+uY/2gZf8B6AxNvMT63aQoNTzQzJ7UubFY397xG52JvjWFVNnRTw9lweaAA4xyYZ8DiJWZZz68Rzmhh5jRZKT/NAoFNg2VCw3Dr47upPc= mvasek@bellatrix diff --git a/internal/sshdialer/testdata/etc/ssh/sshd_config b/internal/sshdialer/testdata/etc/ssh/sshd_config new file mode 100644 index 0000000000..95740bbc89 --- /dev/null +++ b/internal/sshdialer/testdata/etc/ssh/sshd_config @@ -0,0 +1,119 @@ +# $OpenBSD: sshd_config,v 1.103 2018/04/09 20:41:22 tj Exp $ + +# This is the sshd server system-wide configuration file. See +# sshd_config(5) for more information. + +# This sshd was compiled with PATH=/bin:/usr/bin:/sbin:/usr/sbin + +# The strategy used for options in the default sshd_config shipped with +# OpenSSH is to specify options with their default value where +# possible, but leave them commented. Uncommented options override the +# default value. + +Port 22 +Port 2222 +AddressFamily any +ListenAddress 0.0.0.0 +ListenAddress :: + +HostKey /etc/ssh/ssh_host_ecdsa_key +HostKey /etc/ssh/ssh_host_ed25519_key +HostKey /etc/ssh/ssh_host_rsa_key +HostKey /etc/ssh/ssh_host_dsa_key + +# Ciphers and keying +#RekeyLimit default none + +# Logging +#SyslogFacility AUTH +#LogLevel INFO + +# Authentication: + +#LoginGraceTime 2m +PermitRootLogin yes +#StrictModes yes +#MaxAuthTries 6 +#MaxSessions 10 + +#PubkeyAuthentication yes + +# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2 +# but this is overridden so installations will only check .ssh/authorized_keys +AuthorizedKeysFile .ssh/authorized_keys + +#AuthorizedPrincipalsFile none + +#AuthorizedKeysCommand none +#AuthorizedKeysCommandUser nobody + +# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts +#HostbasedAuthentication no +# Change to yes if you don't trust ~/.ssh/known_hosts for +# HostbasedAuthentication +#IgnoreUserKnownHosts no +# Don't read the user's ~/.rhosts and ~/.shosts files +#IgnoreRhosts yes + +# To disable tunneled clear text passwords, change to no here! +PasswordAuthentication yes +#PermitEmptyPasswords no + +# Change to no to disable s/key passwords +#ChallengeResponseAuthentication yes + +# Kerberos options +#KerberosAuthentication no +#KerberosOrLocalPasswd yes +#KerberosTicketCleanup yes +#KerberosGetAFSToken no + +# GSSAPI options +#GSSAPIAuthentication no +#GSSAPICleanupCredentials yes + +# Set this to 'yes' to enable PAM authentication, account processing, +# and session processing. If this is enabled, PAM authentication will +# be allowed through the ChallengeResponseAuthentication and +# PasswordAuthentication. Depending on your PAM configuration, +# PAM authentication via ChallengeResponseAuthentication may bypass +# the setting of "PermitRootLogin without-password". +# If you just want the PAM account and session checks to run without +# PAM authentication, then enable this but set PasswordAuthentication +# and ChallengeResponseAuthentication to 'no'. +#UsePAM no + +#AllowAgentForwarding yes +# Feel free to re-enable these if your use case requires them. +AllowTcpForwarding yes +GatewayPorts no +X11Forwarding no +#X11DisplayOffset 10 +#X11UseLocalhost yes +#PermitTTY yes +#PrintMotd yes +#PrintLastLog yes +#TCPKeepAlive yes +#PermitUserEnvironment no +#Compression delayed +#ClientAliveInterval 0 +#ClientAliveCountMax 3 +#UseDNS no +#PidFile /run/sshd.pid +#MaxStartups 10:30:100 +#PermitTunnel no +#ChrootDirectory none +#VersionAddendum none + +# no default banner path +#Banner none + +# override default of no subsystems +Subsystem sftp /usr/lib/ssh/sftp-server + +# Example of overriding settings on a per-user basis +#Match User anoncvs +# X11Forwarding no +# AllowTcpForwarding no +# PermitTTY no +# ForceCommand cvs server diff --git a/internal/sshdialer/testdata/go.mod b/internal/sshdialer/testdata/go.mod new file mode 100644 index 0000000000..63aed47b89 --- /dev/null +++ b/internal/sshdialer/testdata/go.mod @@ -0,0 +1,3 @@ +module serve-socket + +go 1.16 diff --git a/internal/sshdialer/testdata/id_dsa b/internal/sshdialer/testdata/id_dsa new file mode 100644 index 0000000000..5678a77ee1 --- /dev/null +++ b/internal/sshdialer/testdata/id_dsa @@ -0,0 +1,2 @@ +somegarbage +somerugish \ No newline at end of file diff --git a/internal/sshdialer/testdata/id_dsa.pub b/internal/sshdialer/testdata/id_dsa.pub new file mode 100644 index 0000000000..5678a77ee1 --- /dev/null +++ b/internal/sshdialer/testdata/id_dsa.pub @@ -0,0 +1,2 @@ +somegarbage +somerugish \ No newline at end of file diff --git a/internal/sshdialer/testdata/id_ed25519 b/internal/sshdialer/testdata/id_ed25519 new file mode 100644 index 0000000000..f04bad68d0 --- /dev/null +++ b/internal/sshdialer/testdata/id_ed25519 @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACCVbPGj8U+Zjq5NEgyP2996RFk+lIrXNMYLqLLikeJRJQAAAJjDwa3Yw8Gt +2AAAAAtzc2gtZWQyNTUxOQAAACCVbPGj8U+Zjq5NEgyP2996RFk+lIrXNMYLqLLikeJRJQ +AAAEDti7Y7pPMfJq3Cwztd3ZiM1orRTIibsTH2Y/NQPPFiHpVs8aPxT5mOrk0SDI/b33pE +WT6Uitc0xguosuKR4lElAAAAFHRlc3R1c2VyQGV4YW1wbGUuY29tAQ== +-----END OPENSSH PRIVATE KEY----- diff --git a/internal/sshdialer/testdata/id_ed25519.pub b/internal/sshdialer/testdata/id_ed25519.pub new file mode 100644 index 0000000000..446c7f4bcc --- /dev/null +++ b/internal/sshdialer/testdata/id_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJVs8aPxT5mOrk0SDI/b33pEWT6Uitc0xguosuKR4lEl testuser@example.com diff --git a/internal/sshdialer/testdata/id_rsa b/internal/sshdialer/testdata/id_rsa new file mode 100644 index 0000000000..a194f218c3 --- /dev/null +++ b/internal/sshdialer/testdata/id_rsa @@ -0,0 +1,50 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +MUnDXSP1KUo0fm3Ojh6J25+2w= +-----END OPENSSH PRIVATE KEY----- diff --git a/internal/sshdialer/testdata/id_rsa.pub b/internal/sshdialer/testdata/id_rsa.pub new file mode 100644 index 0000000000..383c923400 --- /dev/null +++ b/internal/sshdialer/testdata/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDe2RqNezOI3cRJq+PQYlcASjYRaJgFd/AjAYtB+u8C7C8OkuelIaiYavjUn1+Sx3VOkqSVwA7J7zUzUPcx/83BjCffvHtXLAmYfxojd9Z7Bh05kV1Ayx7Pn8xEbEkZpffLGrIy98Vs2MYUU7K7JhLOInkEfD1qolV5IpoIKazkulJEpsdTalrSn53IO0Afa+aayZIG+Fc4RiTHUqk6YcLZQnAcUAoa6WCC31cIOhbllv83hVwjx9b5ZgX509o/WrBsR2cnL7qemBDJmq4RkL6RVcM0lyaGyMn+PfYCXbx4pn3bxdw7An3BS3/O/hTEFLh2tCJc55u7xbKJX265RpKMt/BdU8+VLuA8assVBkNhDwlLI+jvmC8fzqE7H/PLWO2XgTkMpDybEy2n4+8nRElnj4RB2J+vagiJPZFrr5SWQHMUD8fHAYvWo0AHPLly+NgkEu2ZRoAV3gRwZm4C8u9tXMNf4UNVcyxpgaylZwxnaiLNjwx6UChcA0TAAGPygJ3UJ8D/Gakj3iekOd7Zcl7oE9dzByjIolVaPqXChAjfB8wWSiJ2LZRhVklLwHm+wKLEpIBhzai4OnF6k4NTyo1wxlfHb90rbyfn6/arUTJX7cU1NFd7cUw0lh+fL/Q15nJZsd5oCNt3yCTS9fpo9rPPPjCkL+XqUzVVOgbRK5Iknw== testuser@example.com diff --git a/internal/sshdialer/testdata/main.go b/internal/sshdialer/testdata/main.go new file mode 100644 index 0000000000..5d2bf107ae --- /dev/null +++ b/internal/sshdialer/testdata/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "context" + "net" + "net/http" + "os" + "os/signal" + "syscall" + "time" +) + +// simple HTTP server to verify that tunneling works +func main() { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + unixListener, err := net.Listen("unix", os.Args[1]) + if err != nil { + panic(err) + } + var handler http.HandlerFunc = func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + w.Write([]byte("Hello there!")) + } + server := http.Server{Handler: handler} + go func() { + <-sigs + shutdownCtx, _ := context.WithTimeout(context.Background(), time.Second*5) + server.Shutdown(shutdownCtx) + }() + server.Serve(unixListener) +}