diff --git a/communicator/ssh/connect.go b/communicator/ssh/connect.go index b280f3ead..43277595c 100644 --- a/communicator/ssh/connect.go +++ b/communicator/ssh/connect.go @@ -1,8 +1,11 @@ package ssh import ( + "fmt" "net" "time" + + "golang.org/x/crypto/ssh" ) // ConnectFunc is a convenience method for returning a function @@ -23,3 +26,43 @@ func ConnectFunc(network, addr string) func() (net.Conn, error) { return c, nil } } + +// BastionConnectFunc is a convenience method for returning a function +// that connects to a host over a bastion connection. +func BastionConnectFunc( + bProto string, + bAddr string, + bConf *ssh.ClientConfig, + proto string, + addr string) func() (net.Conn, error) { + return func() (net.Conn, error) { + // Connect to the bastion + bastion, err := ssh.Dial(bProto, bAddr, bConf) + if err != nil { + return nil, fmt.Errorf("Error connecting to bastion: %s", err) + } + + // Connect through to the end host + conn, err := bastion.Dial(proto, addr) + if err != nil { + bastion.Close() + return nil, err + } + + // Wrap it up so we close both things properly + return &bastionConn{ + Conn: conn, + Bastion: bastion, + }, nil + } +} + +type bastionConn struct { + net.Conn + Bastion *ssh.Client +} + +func (c *bastionConn) Close() error { + c.Conn.Close() + return c.Bastion.Close() +} diff --git a/helper/communicator/config.go b/helper/communicator/config.go index 4c316ff69..4d10bbbb6 100644 --- a/helper/communicator/config.go +++ b/helper/communicator/config.go @@ -23,6 +23,11 @@ type Config struct { SSHPty bool `mapstructure:"ssh_pty"` SSHTimeout time.Duration `mapstructure:"ssh_timeout"` SSHHandshakeAttempts int `mapstructure:"ssh_handshake_attempts"` + SSHBastionHost string `mapstructure:"ssh_bastion_host"` + SSHBastionPort int `mapstructure:"ssh_bastion_port"` + SSHBastionUsername string `mapstructure:"ssh_bastion_username"` + SSHBastionPassword string `mapstructure:"ssh_bastion_password"` + SSHBastionPrivateKey string `mapstructure:"ssh_bastion_private_key_file"` // WinRM WinRMUser string `mapstructure:"winrm_username"` @@ -77,6 +82,12 @@ func (c *Config) prepareSSH(ctx *interpolate.Context) []error { c.SSHHandshakeAttempts = 10 } + if c.SSHBastionHost != "" { + if c.SSHBastionPort == 0 { + c.SSHBastionPort = 22 + } + } + // Validation var errs []error if c.SSHUsername == "" { @@ -93,6 +104,13 @@ func (c *Config) prepareSSH(ctx *interpolate.Context) []error { } } + if c.SSHBastionHost != "" { + if c.SSHBastionPassword == "" && c.SSHBastionPrivateKey == "" { + errs = append(errs, errors.New( + "ssh_bastion_password or ssh_bastion_private_key_file must be specified")) + } + } + return errs } diff --git a/helper/communicator/step_connect_ssh.go b/helper/communicator/step_connect_ssh.go index 4b664fe4c..fd6b585f8 100644 --- a/helper/communicator/step_connect_ssh.go +++ b/helper/communicator/step_connect_ssh.go @@ -4,10 +4,12 @@ import ( "errors" "fmt" "log" + "net" "strings" "time" "github.com/mitchellh/multistep" + commonssh "github.com/mitchellh/packer/common/ssh" "github.com/mitchellh/packer/communicator/ssh" "github.com/mitchellh/packer/packer" gossh "golang.org/x/crypto/ssh" @@ -79,6 +81,24 @@ func (s *StepConnectSSH) Cleanup(multistep.StateBag) { } func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan struct{}) (packer.Communicator, error) { + // Determine if we're using a bastion host, and if so, retrieve + // that configuration. This configuration doesn't change so we + // do this one before entering the retry loop. + var bProto, bAddr string + var bConf *gossh.ClientConfig + if s.Config.SSHBastionHost != "" { + // The protocol is hardcoded for now, but may be configurable one day + bProto = "tcp" + bAddr = fmt.Sprintf( + "%s:%d", s.Config.SSHBastionHost, s.Config.SSHBastionPort) + + conf, err := sshBastionConfig(s.Config) + if err != nil { + return nil, fmt.Errorf("Error configuring bastion: %s", err) + } + bConf = conf + } + handshakeAttempts := 0 var comm packer.Communicator @@ -117,10 +137,18 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru continue } - address := fmt.Sprintf("%s:%d", host, port) - // Attempt to connect to SSH port - connFunc := ssh.ConnectFunc("tcp", address) + var connFunc func() (net.Conn, error) + address := fmt.Sprintf("%s:%d", host, port) + if bAddr != "" { + // We're using a bastion host, so use the bastion connfunc + connFunc = ssh.BastionConnectFunc( + bProto, bAddr, bConf, "tcp", address) + } else { + // No bastion host, connect directly + connFunc = ssh.ConnectFunc("tcp", address) + } + nc, err := connFunc() if err != nil { log.Printf("[DEBUG] TCP connection to SSH ip/port failed: %s", err) @@ -164,3 +192,27 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru return comm, nil } + +func sshBastionConfig(config *Config) (*gossh.ClientConfig, error) { + auth := make([]gossh.AuthMethod, 0, 2) + if config.SSHBastionPassword != "" { + auth = append(auth, + gossh.Password(config.SSHBastionPassword), + gossh.KeyboardInteractive( + ssh.PasswordKeyboardInteractive(config.SSHBastionPassword))) + } + + if config.SSHBastionPrivateKey != "" { + signer, err := commonssh.FileSigner(config.SSHBastionPrivateKey) + if err != nil { + return nil, err + } + + auth = append(auth, gossh.PublicKeys(signer)) + } + + return &gossh.ClientConfig{ + User: config.SSHBastionUsername, + Auth: auth, + }, nil +}