Skip to content

Commit

Permalink
communicator/ssh: Add support SSH over HTTP Proxy (#30274)
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 Apr 27, 2022
1 parent de65cc4 commit 4cfb6bc
Show file tree
Hide file tree
Showing 7 changed files with 337 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.Number,
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
152 changes: 152 additions & 0 deletions internal/communicator/ssh/http_proxy.go
@@ -0,0 +1,152 @@
package ssh

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

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

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

type proxyInfo struct {
// HTTP Proxy host or host:port
host string
// HTTP Proxy scheme
scheme string
// An immutable encapsulation of username and password details for a URL
userInfo *url.Userinfo
}

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

p.userInfo = url.UserPassword(username, password)

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

return p
}

func (p *proxyInfo) url() *url.URL {
return &url.URL{
Scheme: p.scheme,
User: p.userInfo,
Host: p.host,
}
}

func (p *proxyDialer) 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 := &url.URL{
Scheme: "",
Host: addr,
}

// 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.userInfo.String() != "" {
username := p.proxy.userInfo.Username()
password, _ := p.proxy.userInfo.Password()
req.SetBasicAuth(username, password)
req.Header.Add("Proxy-Authorization", req.Header.Get("Authorization"))
}

// 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 := &proxyDialer{
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) {
pd, err := proxy.FromURL(p.url(), proxy.Direct)

if err != nil {
return nil, err
}

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

if err != nil {
return nil, err
}

return proxyConn, err
}
33 changes: 31 additions & 2 deletions internal/communicator/ssh/provisioner.go
Expand Up @@ -62,6 +62,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 +118,18 @@ 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":
if err := gocty.FromCtyValue(v, &connInfo.ProxyPort); err != nil {
return nil, err
}
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 +272,18 @@ func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) {
return nil, err
}

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

if connInfo.ProxyHost != "" {
p = newProxyInfo(
fmt.Sprintf("%s:%d", connInfo.ProxyHost, connInfo.ProxyPort),
connInfo.ProxyScheme,
connInfo.ProxyUserName,
connInfo.ProxyUserPassword,
)
}

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

var bastionConf *ssh.ClientConfig
if connInfo.BastionHost != "" {
Expand All @@ -273,7 +302,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
46 changes: 46 additions & 0 deletions internal/communicator/ssh/provisioner_test.go
Expand Up @@ -137,6 +137,52 @@ func TestProvisioner_connInfoEmptyHostname(t *testing.T) {
}
}

func TestProvisioner_connInfoProxy(t *testing.T) {
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("root"),
"password": cty.StringVal("supersecret"),
"private_key": cty.StringVal("someprivatekeycontents"),
"host": cty.StringVal("example.com"),
"port": cty.StringVal("22"),
"timeout": cty.StringVal("30s"),
"proxy_scheme": cty.StringVal("http"),
"proxy_host": cty.StringVal("proxy.example.com"),
"proxy_port": cty.StringVal("80"),
"proxy_user_name": cty.StringVal("proxyuser"),
"proxy_user_password": cty.StringVal("proxyuser_password"),
})

conf, err := parseConnectionInfo(v)
if err != nil {
t.Fatalf("err: %v", err)
}

if conf.Host != "example.com" {
t.Fatalf("bad: %v", conf)
}

if conf.ProxyScheme != "http" {
t.Fatalf("bad: %v", conf)
}

if conf.ProxyHost != "proxy.example.com" {
t.Fatalf("bad: %v", conf)
}

if conf.ProxyPort != 80 {
t.Fatalf("bad: %v", conf)
}

if conf.ProxyUserName != "proxyuser" {
t.Fatalf("bad: %v", conf)
}

if conf.ProxyUserPassword != "proxyuser_password" {
t.Fatalf("bad: %v", conf)
}
}

func TestProvisioner_stringBastionPort(t *testing.T) {
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
Expand Down

0 comments on commit 4cfb6bc

Please sign in to comment.