github.com/HashDataInc/packer@v1.3.2/helper/communicator/step_connect_ssh.go (about) 1 package communicator 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "log" 8 "net" 9 "os" 10 "strings" 11 "time" 12 13 "github.com/hashicorp/packer/communicator/ssh" 14 "github.com/hashicorp/packer/helper/multistep" 15 helperssh "github.com/hashicorp/packer/helper/ssh" 16 "github.com/hashicorp/packer/packer" 17 gossh "golang.org/x/crypto/ssh" 18 "golang.org/x/crypto/ssh/agent" 19 "golang.org/x/net/proxy" 20 ) 21 22 // StepConnectSSH is a step that only connects to SSH. 23 // 24 // In general, you should use StepConnect. 25 type StepConnectSSH struct { 26 // All the fields below are documented on StepConnect 27 Config *Config 28 Host func(multistep.StateBag) (string, error) 29 SSHConfig func(multistep.StateBag) (*gossh.ClientConfig, error) 30 SSHPort func(multistep.StateBag) (int, error) 31 } 32 33 func (s *StepConnectSSH) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { 34 ui := state.Get("ui").(packer.Ui) 35 36 var comm packer.Communicator 37 var err error 38 39 cancel := make(chan struct{}) 40 waitDone := make(chan bool, 1) 41 go func() { 42 ui.Say("Waiting for SSH to become available...") 43 comm, err = s.waitForSSH(state, cancel) 44 waitDone <- true 45 }() 46 47 log.Printf("[INFO] Waiting for SSH, up to timeout: %s", s.Config.SSHTimeout) 48 timeout := time.After(s.Config.SSHTimeout) 49 WaitLoop: 50 for { 51 // Wait for either SSH to become available, a timeout to occur, 52 // or an interrupt to come through. 53 select { 54 case <-waitDone: 55 if err != nil { 56 ui.Error(fmt.Sprintf("Error waiting for SSH: %s", err)) 57 state.Put("error", err) 58 return multistep.ActionHalt 59 } 60 61 ui.Say("Connected to SSH!") 62 state.Put("communicator", comm) 63 break WaitLoop 64 case <-timeout: 65 err := fmt.Errorf("Timeout waiting for SSH.") 66 state.Put("error", err) 67 ui.Error(err.Error()) 68 close(cancel) 69 return multistep.ActionHalt 70 case <-time.After(1 * time.Second): 71 if _, ok := state.GetOk(multistep.StateCancelled); ok { 72 // The step sequence was cancelled, so cancel waiting for SSH 73 // and just start the halting process. 74 close(cancel) 75 log.Println("[WARN] Interrupt detected, quitting waiting for SSH.") 76 return multistep.ActionHalt 77 } 78 } 79 } 80 81 return multistep.ActionContinue 82 } 83 84 func (s *StepConnectSSH) Cleanup(multistep.StateBag) { 85 } 86 87 func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan struct{}) (packer.Communicator, error) { 88 // Determine if we're using a bastion host, and if so, retrieve 89 // that configuration. This configuration doesn't change so we 90 // do this one before entering the retry loop. 91 var bProto, bAddr string 92 var bConf *gossh.ClientConfig 93 var pAddr string 94 var pAuth *proxy.Auth 95 if s.Config.SSHBastionHost != "" { 96 // The protocol is hardcoded for now, but may be configurable one day 97 bProto = "tcp" 98 bAddr = fmt.Sprintf( 99 "%s:%d", s.Config.SSHBastionHost, s.Config.SSHBastionPort) 100 101 conf, err := sshBastionConfig(s.Config) 102 if err != nil { 103 return nil, fmt.Errorf("Error configuring bastion: %s", err) 104 } 105 bConf = conf 106 } 107 108 if s.Config.SSHProxyHost != "" { 109 pAddr = fmt.Sprintf("%s:%d", s.Config.SSHProxyHost, s.Config.SSHProxyPort) 110 if s.Config.SSHProxyUsername != "" { 111 pAuth = new(proxy.Auth) 112 pAuth.User = s.Config.SSHBastionUsername 113 pAuth.Password = s.Config.SSHBastionPassword 114 } 115 116 } 117 118 handshakeAttempts := 0 119 120 var comm packer.Communicator 121 first := true 122 for { 123 // Don't check for cancel or wait on first iteration 124 if !first { 125 select { 126 case <-cancel: 127 log.Println("[DEBUG] SSH wait cancelled. Exiting loop.") 128 return nil, errors.New("SSH wait cancelled") 129 case <-time.After(5 * time.Second): 130 } 131 } 132 first = false 133 134 // First we request the TCP connection information 135 host, err := s.Host(state) 136 if err != nil { 137 log.Printf("[DEBUG] Error getting SSH address: %s", err) 138 continue 139 } 140 port := s.Config.SSHPort 141 if s.SSHPort != nil { 142 port, err = s.SSHPort(state) 143 if err != nil { 144 log.Printf("[DEBUG] Error getting SSH port: %s", err) 145 continue 146 } 147 } 148 149 // Retrieve the SSH configuration 150 sshConfig, err := s.SSHConfig(state) 151 if err != nil { 152 log.Printf("[DEBUG] Error getting SSH config: %s", err) 153 continue 154 } 155 156 // Attempt to connect to SSH port 157 var connFunc func() (net.Conn, error) 158 address := fmt.Sprintf("%s:%d", host, port) 159 if bAddr != "" { 160 // We're using a bastion host, so use the bastion connfunc 161 connFunc = ssh.BastionConnectFunc( 162 bProto, bAddr, bConf, "tcp", address) 163 } else if pAddr != "" { 164 // Connect via SOCKS5 proxy 165 connFunc = ssh.ProxyConnectFunc(pAddr, pAuth, "tcp", address) 166 } else { 167 // No bastion host, connect directly 168 connFunc = ssh.ConnectFunc("tcp", address) 169 } 170 171 nc, err := connFunc() 172 if err != nil { 173 log.Printf("[DEBUG] TCP connection to SSH ip/port failed: %s", err) 174 continue 175 } 176 nc.Close() 177 178 // Then we attempt to connect via SSH 179 config := &ssh.Config{ 180 Connection: connFunc, 181 SSHConfig: sshConfig, 182 Pty: s.Config.SSHPty, 183 DisableAgentForwarding: s.Config.SSHDisableAgentForwarding, 184 UseSftp: s.Config.SSHFileTransferMethod == "sftp", 185 KeepAliveInterval: s.Config.SSHKeepAliveInterval, 186 Timeout: s.Config.SSHReadWriteTimeout, 187 } 188 189 log.Println("[INFO] Attempting SSH connection...") 190 comm, err = ssh.New(address, config) 191 if err != nil { 192 log.Printf("[DEBUG] SSH handshake err: %s", err) 193 194 // Only count this as an attempt if we were able to attempt 195 // to authenticate. Note this is very brittle since it depends 196 // on the string of the error... but I don't see any other way. 197 if strings.Contains(err.Error(), "authenticate") { 198 log.Printf( 199 "[DEBUG] Detected authentication error. Increasing handshake attempts.") 200 handshakeAttempts += 1 201 } 202 203 if handshakeAttempts < s.Config.SSHHandshakeAttempts { 204 // Try to connect via SSH a handful of times. We sleep here 205 // so we don't get a ton of authentication errors back to back. 206 time.Sleep(2 * time.Second) 207 continue 208 } 209 210 return nil, err 211 } 212 213 break 214 } 215 216 return comm, nil 217 } 218 219 func sshBastionConfig(config *Config) (*gossh.ClientConfig, error) { 220 auth := make([]gossh.AuthMethod, 0, 2) 221 if config.SSHBastionPassword != "" { 222 auth = append(auth, 223 gossh.Password(config.SSHBastionPassword), 224 gossh.KeyboardInteractive( 225 ssh.PasswordKeyboardInteractive(config.SSHBastionPassword))) 226 } 227 228 if config.SSHBastionPrivateKeyFile != "" { 229 signer, err := helperssh.FileSigner(config.SSHBastionPrivateKeyFile) 230 if err != nil { 231 return nil, err 232 } 233 234 auth = append(auth, gossh.PublicKeys(signer)) 235 } 236 237 if config.SSHBastionAgentAuth { 238 authSock := os.Getenv("SSH_AUTH_SOCK") 239 if authSock == "" { 240 return nil, fmt.Errorf("SSH_AUTH_SOCK is not set") 241 } 242 243 sshAgent, err := net.Dial("unix", authSock) 244 if err != nil { 245 return nil, fmt.Errorf("Cannot connect to SSH Agent socket %q: %s", authSock, err) 246 } 247 248 auth = append(auth, gossh.PublicKeysCallback(agent.NewClient(sshAgent).Signers)) 249 } 250 251 return &gossh.ClientConfig{ 252 User: config.SSHBastionUsername, 253 Auth: auth, 254 HostKeyCallback: gossh.InsecureIgnoreHostKey(), 255 }, nil 256 }