Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

communicator/ssh: Add support SSH over HTTP Proxy #30274

Merged
merged 2 commits into from Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upon further reading, I think we could omit this new type, and use a *url.URL representing the proxy connection instead. While I haven't tried it, so I might be missing something, I think this would be simpler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


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