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,
|
||||
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,
|
||||
|
@ -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()
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
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
|
||||
TimeoutVal time.Duration
|
||||
|
||||
ProxyScheme string
|
||||
ProxyHost string
|
||||
ProxyPort uint16
|
||||
ProxyUserName string
|
||||
ProxyUserPassword string
|
||||
|
||||
BastionUser string
|
||||
BastionPassword string
|
||||
BastionPrivateKey string
|
||||
@ -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":
|
||||
@ -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 != "" {
|
||||
@ -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{
|
||||
|
@ -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"),
|
||||
|
@ -191,6 +191,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,
|
||||
|
@ -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_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
|
||||
|
||||
Provisioners which execute commands on a remote system via a protocol such as
|
||||
|
Loading…
Reference in New Issue
Block a user