Skip to content

Commit

Permalink
communicator/ssh: Add support SSH over HTTP Proxy
Browse files Browse the repository at this point in the history
Terraform's remote-exec provision hangs out when it execs on HTTP Proxy bacause it dosen't support SSH over HTTP Proxy.
This commits enables Terraform's remote-exec to support SSH over HTTP Proxy.

* adds `proxy_*` fields to `connection` which add configuration for a proxy host
* if `proxy_host` set, connect to that proxy host via CONNECT method, then make the SSH connection to `host` or `bastion_host`
  • Loading branch information
htamakos committed Jan 9, 2022
1 parent 1e9075b commit 1715ce1
Show file tree
Hide file tree
Showing 7 changed files with 359 additions and 6 deletions.
20 changes: 20 additions & 0 deletions internal/communicator/shared/shared.go
Expand Up @@ -72,6 +72,26 @@ var ConnectionBlockSupersetSchema = &configschema.Block{
Type: cty.String,
Optional: true,
},
"proxy_scheme": {
Type: cty.String,
Optional: true,
},
"proxy_host": {
Type: cty.String,
Optional: true,
},
"proxy_port": {
Type: cty.String,
Optional: true,
},
"proxy_user_name": {
Type: cty.String,
Optional: true,
},
"proxy_user_password": {
Type: cty.String,
Optional: true,
},
"bastion_host": {
Type: cty.String,
Optional: true,
Expand Down
60 changes: 56 additions & 4 deletions internal/communicator/ssh/communicator.go
Expand Up @@ -172,6 +172,20 @@ func (c *Communicator) Connect(o provisioners.UIOutput) (err error) {
}
}

if c.connInfo.ProxyHost != "" {
o.Output(fmt.Sprintf(
"Using configured proxy host...\n"+
" ProxyHost: %s\n"+
" ProxyPort: %d\n"+
" ProxyUserName: %s\n"+
" ProxyUserPassword: %t",
c.connInfo.ProxyHost,
c.connInfo.ProxyPort,
c.connInfo.ProxyUserName,
c.connInfo.ProxyUserPassword != "",
))
}

hostAndPort := fmt.Sprintf("%s:%d", c.connInfo.Host, c.connInfo.Port)
log.Printf("[DEBUG] Connecting to %s for SSH", hostAndPort)
c.conn, err = c.config.connection()
Expand Down Expand Up @@ -770,9 +784,19 @@ func scpUploadDir(root string, fs []os.FileInfo, w io.Writer, r *bufio.Reader) e
// ConnectFunc is a convenience method for returning a function
// that just uses net.Dial to communicate with the remote end that
// is suitable for use with the SSH communicator configuration.
func ConnectFunc(network, addr string) func() (net.Conn, error) {
func ConnectFunc(network, addr string, p *proxyInfo) func() (net.Conn, error) {
return func() (net.Conn, error) {
c, err := net.DialTimeout(network, addr, 15*time.Second)
var c net.Conn
var err error

// Wrap connection to host if proxy server is configured
if p != nil {
RegisterDialerType()
c, err = NewHttpProxyConn(p, addr)
} else {
c, err = net.DialTimeout(network, addr, 15*time.Second)
}

if err != nil {
return nil, err
}
Expand All @@ -792,10 +816,38 @@ func BastionConnectFunc(
bAddr string,
bConf *ssh.ClientConfig,
proto string,
addr string) func() (net.Conn, error) {
addr string,
p *proxyInfo) func() (net.Conn, error) {
return func() (net.Conn, error) {
log.Printf("[DEBUG] Connecting to bastion: %s", bAddr)
bastion, err := ssh.Dial(bProto, bAddr, bConf)
var bastion *ssh.Client
var err error

// Wrap connection to bastion server if proxy server is configured
if p != nil {
var pConn net.Conn
var bConn ssh.Conn
var bChans <-chan ssh.NewChannel
var bReq <-chan *ssh.Request

RegisterDialerType()
pConn, err = NewHttpProxyConn(p, bAddr)

if err != nil {
return nil, fmt.Errorf("Error connecting to proxy: %s", err)
}

bConn, bChans, bReq, err = ssh.NewClientConn(pConn, bAddr, bConf)

if err != nil {
return nil, fmt.Errorf("Error creating new client connection via proxy: %s", err)
}

bastion = ssh.NewClient(bConn, bChans, bReq)
} else {
bastion, err = ssh.Dial(bProto, bAddr, bConf)
}

if err != nil {
return nil, fmt.Errorf("Error connecting to bastion: %s", err)
}
Expand Down
168 changes: 168 additions & 0 deletions internal/communicator/ssh/http_proxy.go
@@ -0,0 +1,168 @@
package ssh

import (
"bufio"
"encoding/base64"
"fmt"
"net"
"net/http"
"net/url"
"time"

"golang.org/x/net/proxy"
)

// Dialer implements for SSH over HTTP Proxy.
type Dialer struct {
proxy proxyInfo
// forwarding Dialer
forward proxy.Dialer
}

type proxyInfo struct {
// HTTP Proxy host or host:port
host string
// HTTP Proxy scheme
scheme string
// User name if http proxy needs authentication
username string
// User password if http proxy needs authentication
password string
// Whether the HTTP Proxy requires authentication
auth bool
}

func newProxyInfo(host, scheme, username, password string) *proxyInfo {
p := &proxyInfo{
host: host,
scheme: scheme,
}

if username != "" && password != "" {
p.auth = true
p.username = username
p.password = password
}

if p.scheme == "" {
p.scheme = "http"
}

return p
}

func (p *proxyInfo) url() (*url.URL, error) {
base := p.scheme + "://"

if p.auth {
base = base + p.username + ":" + p.password + "@"
}

return url.Parse(base + p.host)
}

func (p *Dialer) Dial(network, addr string) (net.Conn, error) {
// Dial the proxy host
c, err := p.forward.Dial(network, p.proxy.host)

if err != nil {
return nil, err
}

err = c.SetDeadline(time.Now().Add(15 * time.Second))
if err != nil {
return nil, err
}

// Generate request URL to host accessed through the proxy
reqUrl, err := url.Parse("http://" + addr)
if err != nil {
c.Close()
return nil, err
}
reqUrl.Scheme = ""

// Create a request object using the CONNECT method to instruct the proxy server to tunnel a protocol other than HTTP.
req, err := http.NewRequest("CONNECT", reqUrl.String(), nil)
if err != nil {
c.Close()
return nil, err
}

// If http proxy requires authentication, configure settings for basic authentication.
if p.proxy.auth {
req.SetBasicAuth(p.proxy.username, p.proxy.password)
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(p.proxy.username+":"+p.proxy.password)))
}

// Do not close the connection after sending this request and reading its response.
req.Close = false

// Writes the request in the form expected by an HTTP proxy.
err = req.Write(c)
if err != nil {
c.Close()
return nil, err
}

res, err := http.ReadResponse(bufio.NewReader(c), req)

if err != nil {
res.Body.Close()
c.Close()
return nil, err
}

res.Body.Close()

if res.StatusCode != http.StatusOK {
c.Close()
return nil, fmt.Errorf("Connection Error: StatusCode: %d", res.StatusCode)
}

return c, nil
}

// NewHttpProxyDialer generate Http Proxy Dialer
func NewHttpProxyDialer(u *url.URL, forward proxy.Dialer) (proxy.Dialer, error) {
var proxyUserName, proxyPassword string
if u.User != nil {
proxyUserName = u.User.Username()
proxyPassword, _ = u.User.Password()
}

pd := &Dialer{
proxy: *newProxyInfo(u.Host, u.Scheme, proxyUserName, proxyPassword),
forward: forward,
}

return pd, nil
}

// RegisterDialerType register schemes used by `proxy.FromURL`
func RegisterDialerType() {
proxy.RegisterDialerType("http", NewHttpProxyDialer)
proxy.RegisterDialerType("https", NewHttpProxyDialer)
}

// NewHttpProxyConn create a connection to connect through the proxy server.
func NewHttpProxyConn(p *proxyInfo, targetAddr string) (net.Conn, error) {
proxyURL, err := p.url()
if err != nil {
return nil, err
}

proxyDialer, err := proxy.FromURL(proxyURL, proxy.Direct)

if err != nil {
return nil, err
}

proxyConn, err := proxyDialer.Dial("tcp", targetAddr)

if err != nil {
return nil, err
}

return proxyConn, err
}
36 changes: 34 additions & 2 deletions internal/communicator/ssh/provisioner.go
Expand Up @@ -10,6 +10,7 @@ import (
"net"
"os"
"path/filepath"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -62,6 +63,12 @@ type connectionInfo struct {
Timeout string
TimeoutVal time.Duration

ProxyScheme string
ProxyHost string
ProxyPort uint16
ProxyUserName string
ProxyUserPassword string

BastionUser string
BastionPassword string
BastionPrivateKey string
Expand Down Expand Up @@ -112,6 +119,20 @@ func decodeConnInfo(v cty.Value) (*connectionInfo, error) {
connInfo.TargetPlatform = v.AsString()
case "timeout":
connInfo.Timeout = v.AsString()
case "proxy_scheme":
connInfo.ProxyScheme = v.AsString()
case "proxy_host":
connInfo.ProxyHost = v.AsString()
case "proxy_port":
p, err := strconv.ParseUint(v.AsString(), 10, 16)
if err != nil {
return nil, err
}
connInfo.ProxyPort = uint16(p)
case "proxy_user_name":
connInfo.ProxyUserName = v.AsString()
case "proxy_user_password":
connInfo.ProxyUserPassword = v.AsString()
case "bastion_user":
connInfo.BastionUser = v.AsString()
case "bastion_password":
Expand Down Expand Up @@ -254,7 +275,18 @@ func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) {
return nil, err
}

connectFunc := ConnectFunc("tcp", host)
var p *proxyInfo

if connInfo.ProxyHost != "" {
p = newProxyInfo(
connInfo.ProxyHost+":"+strconv.FormatUint(uint64(connInfo.ProxyPort), 10),
connInfo.ProxyScheme,
connInfo.ProxyUserName,
connInfo.ProxyUserPassword,
)
}

connectFunc := ConnectFunc("tcp", host, p)

var bastionConf *ssh.ClientConfig
if connInfo.BastionHost != "" {
Expand All @@ -273,7 +305,7 @@ func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) {
return nil, err
}

connectFunc = BastionConnectFunc("tcp", bastionHost, bastionConf, "tcp", host)
connectFunc = BastionConnectFunc("tcp", bastionHost, bastionConf, "tcp", host, p)
}

config := &sshConfig{
Expand Down

0 comments on commit 1715ce1

Please sign in to comment.