Skip to content

Commit

Permalink
Merge pull request #2266 from mitchellh/f-bastion
Browse files Browse the repository at this point in the history
communicator/ssh: support for bastion SSH
  • Loading branch information
mitchellh committed Jun 17, 2015
2 parents b20e26b + 6cdc17d commit c401937
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 2 deletions.
43 changes: 43 additions & 0 deletions communicator/ssh/connect.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package ssh

import (
"fmt"
"net"
"time"

"golang.org/x/crypto/ssh"
)

// ConnectFunc is a convenience method for returning a function
Expand All @@ -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()
}
22 changes: 22 additions & 0 deletions helper/communicator/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -77,6 +82,16 @@ func (c *Config) prepareSSH(ctx *interpolate.Context) []error {
c.SSHHandshakeAttempts = 10
}

if c.SSHBastionHost != "" {
if c.SSHBastionPort == 0 {
c.SSHBastionPort = 22
}

if c.SSHBastionPrivateKey == "" && c.SSHPrivateKey != "" {
c.SSHBastionPrivateKey = c.SSHPrivateKey
}
}

// Validation
var errs []error
if c.SSHUsername == "" {
Expand All @@ -93,6 +108,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
}

Expand Down
56 changes: 54 additions & 2 deletions helper/communicator/step_connect_ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -117,10 +137,18 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru
continue
}

// Attempt to connect to SSH port
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)
}

// Attempt to connect to SSH port
connFunc := ssh.ConnectFunc("tcp", address)
nc, err := connFunc()
if err != nil {
log.Printf("[DEBUG] TCP connection to SSH ip/port failed: %s", err)
Expand Down Expand Up @@ -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
}

0 comments on commit c401937

Please sign in to comment.