mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
communicator/ssh: Add support SSH over HTTP Proxy (#30274)
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`
This commit is contained in:
parent
de65cc43f3
commit
4cfb6bc893
@ -72,6 +72,26 @@ var ConnectionBlockSupersetSchema = &configschema.Block{
|
|||||||
Type: cty.String,
|
Type: cty.String,
|
||||||
Optional: true,
|
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": {
|
"bastion_host": {
|
||||||
Type: cty.String,
|
Type: cty.String,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
|
@ -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)
|
hostAndPort := fmt.Sprintf("%s:%d", c.connInfo.Host, c.connInfo.Port)
|
||||||
log.Printf("[DEBUG] Connecting to %s for SSH", hostAndPort)
|
log.Printf("[DEBUG] Connecting to %s for SSH", hostAndPort)
|
||||||
c.conn, err = c.config.connection()
|
c.conn, err = c.config.connection()
|
||||||
@ -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
|
// ConnectFunc is a convenience method for returning a function
|
||||||
// that just uses net.Dial to communicate with the remote end that
|
// that just uses net.Dial to communicate with the remote end that
|
||||||
// is suitable for use with the SSH communicator configuration.
|
// 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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -792,10 +816,38 @@ func BastionConnectFunc(
|
|||||||
bAddr string,
|
bAddr string,
|
||||||
bConf *ssh.ClientConfig,
|
bConf *ssh.ClientConfig,
|
||||||
proto string,
|
proto string,
|
||||||
addr string) func() (net.Conn, error) {
|
addr string,
|
||||||
|
p *proxyInfo) func() (net.Conn, error) {
|
||||||
return func() (net.Conn, error) {
|
return func() (net.Conn, error) {
|
||||||
log.Printf("[DEBUG] Connecting to bastion: %s", bAddr)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error connecting to bastion: %s", err)
|
return nil, fmt.Errorf("Error connecting to bastion: %s", err)
|
||||||
}
|
}
|
||||||
|
152
internal/communicator/ssh/http_proxy.go
Normal file
152
internal/communicator/ssh/http_proxy.go
Normal file
@ -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
|
||||||
|
}
|
@ -62,6 +62,12 @@ type connectionInfo struct {
|
|||||||
Timeout string
|
Timeout string
|
||||||
TimeoutVal time.Duration
|
TimeoutVal time.Duration
|
||||||
|
|
||||||
|
ProxyScheme string
|
||||||
|
ProxyHost string
|
||||||
|
ProxyPort uint16
|
||||||
|
ProxyUserName string
|
||||||
|
ProxyUserPassword string
|
||||||
|
|
||||||
BastionUser string
|
BastionUser string
|
||||||
BastionPassword string
|
BastionPassword string
|
||||||
BastionPrivateKey string
|
BastionPrivateKey string
|
||||||
@ -112,6 +118,18 @@ func decodeConnInfo(v cty.Value) (*connectionInfo, error) {
|
|||||||
connInfo.TargetPlatform = v.AsString()
|
connInfo.TargetPlatform = v.AsString()
|
||||||
case "timeout":
|
case "timeout":
|
||||||
connInfo.Timeout = v.AsString()
|
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":
|
case "bastion_user":
|
||||||
connInfo.BastionUser = v.AsString()
|
connInfo.BastionUser = v.AsString()
|
||||||
case "bastion_password":
|
case "bastion_password":
|
||||||
@ -254,7 +272,18 @@ func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) {
|
|||||||
return nil, err
|
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
|
var bastionConf *ssh.ClientConfig
|
||||||
if connInfo.BastionHost != "" {
|
if connInfo.BastionHost != "" {
|
||||||
@ -273,7 +302,7 @@ func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
connectFunc = BastionConnectFunc("tcp", bastionHost, bastionConf, "tcp", host)
|
connectFunc = BastionConnectFunc("tcp", bastionHost, bastionConf, "tcp", host, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
config := &sshConfig{
|
config := &sshConfig{
|
||||||
|
@ -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) {
|
func TestProvisioner_stringBastionPort(t *testing.T) {
|
||||||
v := cty.ObjectVal(map[string]cty.Value{
|
v := cty.ObjectVal(map[string]cty.Value{
|
||||||
"type": cty.StringVal("ssh"),
|
"type": cty.StringVal("ssh"),
|
||||||
|
@ -191,6 +191,26 @@ var connectionBlockSupersetSchema = &configschema.Block{
|
|||||||
Type: cty.String,
|
Type: cty.String,
|
||||||
Optional: true,
|
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": {
|
"bastion_host": {
|
||||||
Type: cty.String,
|
Type: cty.String,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
|
@ -113,6 +113,18 @@ indirectly with a [bastion host](https://en.wikipedia.org/wiki/Bastion_host).
|
|||||||
| `bastion_private_key` | The contents of an SSH key file to use for the bastion host. These can be loaded from a file on disk using [the `file` function](/language/functions/file). | The value of the `private_key` field. |
|
| `bastion_private_key` | The contents of an SSH key file to use for the bastion host. These can be loaded from a file on disk using [the `file` function](/language/functions/file). | The value of the `private_key` field. |
|
||||||
| `bastion_certificate` | The contents of a signed CA Certificate. The certificate argument must be used in conjunction with a `bastion_private_key`. These can be loaded from a file on disk using the [the `file` function](/language/functions/file). |
|
| `bastion_certificate` | The contents of a signed CA Certificate. The certificate argument must be used in conjunction with a `bastion_private_key`. These can be loaded from a file on disk using the [the `file` function](/language/functions/file). |
|
||||||
|
|
||||||
|
## Connection through a HTTP Proxy with SSH
|
||||||
|
|
||||||
|
The `ssh` connection also supports the following fields to facilitate connections by SSH over HTTP proxy.
|
||||||
|
|
||||||
|
| Argument | Description | Default |
|
||||||
|
|---------------|-------------|---------|
|
||||||
|
| `proxy_scheme` | http or https | |
|
||||||
|
| `proxy_host` | Setting this enables the SSH over HTTP connection. This host will be connected to first, and then the `host` or `bastion_host` connection will be made from there. | |
|
||||||
|
| `proxy_port` | The port to use connect to the proxy host. | |
|
||||||
|
| `proxy_user_name` | The username to use connect to the private proxy host. This argument should be specified only if authentication is required for the HTTP Proxy server. | |
|
||||||
|
| `proxy_user_password` | The password to use connect to the private proxy host. This argument should be specified only if authentication is required for the HTTP Proxy server. | |
|
||||||
|
|
||||||
## How Provisioners Execute Remote Scripts
|
## How Provisioners Execute Remote Scripts
|
||||||
|
|
||||||
Provisioners which execute commands on a remote system via a protocol such as
|
Provisioners which execute commands on a remote system via a protocol such as
|
||||||
|
Loading…
Reference in New Issue
Block a user