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

Add support for remote port fowarding for SSH connections used by Provisioners #13432

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions communicator/ssh/communicator.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,55 @@ func (c *Communicator) Connect(o terraform.UIOutput) (err error) {
o.Output("Connected!")
}

for _, rf := range c.connInfo.RemoteForwardVal {
src := fmt.Sprintf("%v:%v",
rf.BindHost, rf.BindPort)
dest := fmt.Sprintf("%v:%v",
rf.Host, rf.Port)
log.Printf("[DEBUG] Setting up remote port forwarding (remote src %v) (local destination %v)", src, dest)

// Request the remote side to open src port
forwardListener, err := c.client.Listen("tcp", src)
if err != nil {
log.Printf("[WARN] Unable to register remote src %v: %v", src, err.Error())
} else {
go portForwardAccept(forwardListener, dest)
}
}
return err
}

func portForwardAccept(l net.Listener, dest string) {
defer l.Close()
for {
sshConn, err := l.Accept()
if err != nil {
// will return err != null when ssh connection shutdown
break
}
go forwardTraffic(sshConn, dest)
}
}

func forwardTraffic(sshConn net.Conn, dest string) {
localConn, err := net.Dial("tcp", dest)
if err != nil {
log.Printf("[WARN] Unable to connect to local destination %v: %v", dest, err.Error())
sshConn.Close()
return
}

go func() {
io.Copy(sshConn, localConn)
sshConn.Close() // shutdown other side of connection
}()

go func() {
io.Copy(localConn, sshConn)
localConn.Close() // shutdown other side of connection
}()
}

// Disconnect implementation of communicator.Communicator interface
func (c *Communicator) Disconnect() error {
c.lock.Lock()
Expand Down
44 changes: 44 additions & 0 deletions communicator/ssh/provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
"log"
"net"
"os"
"regexp"
"strconv"
"strings"
"time"

"github.com/hashicorp/terraform/communicator/shared"
Expand All @@ -31,6 +34,13 @@ const (
DefaultTimeout = 5 * time.Minute
)

type remoteForward struct {
BindHost string
BindPort int
Host string
Port int
}

// connectionInfo is decoded from the ConnInfo of the resource. These are the
// only keys we look at. If a PrivateKey is given, that is used instead
// of a password.
Expand All @@ -50,6 +60,9 @@ type connectionInfo struct {
BastionPrivateKey string `mapstructure:"bastion_private_key"`
BastionHost string `mapstructure:"bastion_host"`
BastionPort int `mapstructure:"bastion_port"`

RemoteForward string `mapstructure:"remote_forward"`
RemoteForwardVal []remoteForward `mapstructure:"-"`
}

// parseConnectionInfo is used to convert the ConnInfo of the InstanceState into
Expand Down Expand Up @@ -117,6 +130,37 @@ func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
}
}

rflist := strings.Split(connInfo.RemoteForward, ",")
connInfo.RemoteForwardVal = make([]remoteForward, 0, len(rflist))
for _, rf := range rflist {
if rf == "" {
continue
}
var v remoteForward
// expect format of remote forward connection: [bind_address:]port:host:hostport
e := regexp.MustCompile("\\[[^]]*\\]|[^:]+").FindAllString(rf, -1)
if len(e) < 3 || len(e) > 4 {
return connInfo, fmt.Errorf("Failed to parse remote_foward: %s, expected [bind_address:]port:host:hostport", rf)
}
offset := 0
if len(e) == 3 {
v.BindHost = "localhost"
} else {
v.BindHost = shared.IpFormat(e[0])
offset++
}

v.BindPort, err = strconv.Atoi(e[offset])
if err != nil {
return connInfo, fmt.Errorf("Failed to parse port of remote_foward: %s, expected [bind_address:]port:host:hostport", rf)
}
v.Host = shared.IpFormat(e[offset+1])
v.Port, err = strconv.Atoi(e[offset+2])
if err != nil {
return connInfo, fmt.Errorf("Failed to parse hostport of remote_foward: %s, expected [bind_address:]port:host:hostport", rf)
}
connInfo.RemoteForwardVal = append(connInfo.RemoteForwardVal, v)
}
return connInfo, nil
}

Expand Down
86 changes: 86 additions & 0 deletions communicator/ssh/provisioner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,89 @@ func TestProvisioner_connInfoHostname(t *testing.T) {
t.Fatalf("bad %v", conf)
}
}

func TestProvisioner_connInfoRemoteForward(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "root",
"password": "supersecret",
"private_key": "someprivatekeycontents",
"host": "example.com",
"port": "22",
"timeout": "30s",

"bastion_host": "example.com",
"remote_forward": "127.0.1.1:8080:127.0.0.1:80,8081:host:8081",
},
},
}

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

if len(conf.RemoteForwardVal) != 2 {
t.Fatalf("bad: %v", conf)
}

if conf.RemoteForwardVal[0].BindPort != 8080 {
t.Fatalf("bad: %v", conf)
}

if conf.RemoteForwardVal[0].BindHost != "127.0.1.1" {
t.Fatalf("bad %v", conf)
}

if conf.RemoteForwardVal[0].Port != 80 {
t.Fatalf("bad: %v", conf)
}

if conf.RemoteForwardVal[0].Host != "127.0.0.1" {
t.Fatalf("bad %v", conf)
}

if conf.RemoteForwardVal[1].BindPort != 8081 {
t.Fatalf("bad: %v", conf)
}

if conf.RemoteForwardVal[1].BindHost != "localhost" {
t.Fatalf("bad %v", conf)
}

if conf.RemoteForwardVal[1].Port != 8081 {
t.Fatalf("bad: %v", conf)
}

if conf.RemoteForwardVal[1].Host != "host" {
t.Fatalf("bad %v", conf)
}

}

func TestProvisioner_connInfoRemoteForwardNone(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "root",
"password": "supersecret",
"private_key": "someprivatekeycontents",
"host": "example.com",
"port": "22",
"timeout": "30s",
},
},
}

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

if len(conf.RemoteForwardVal) != 0 {
t.Fatalf("bad: %v", conf)
}
}
14 changes: 14 additions & 0 deletions website/source/docs/provisioners/connection.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ provisioner "file" {
only supported SSH authentication agent is
[Pageant](http://the.earth.li/~sgtatham/putty/0.66/htmldoc/Chapter9.html#pageant).

* `remote_forward` - The set of ports to forward from the remote host (the host
being provisioned) to a given host on the local side of the SSH connection (the side
running Terraform). The format is a comma separated list of
forward instructions similar to `ssh -R`, i.e. `[bind_address]:port:host:hostport`.
`bind_address` is the address to bind on the remote host. It is optional and defaults
to `localhost`. `port` is the listen port on the remote side of the SSH connection.
`host` is the host to connect to on the local side of the SSH connection.
`hostport` is the port to connect to on the local side of the SSH connection.
For example `localhost:8080:chef.example.com:80` will forward the port 8080 on the
remote host (the host being provisioned) to port 80 of `chef.example.com` on the
local side of the SSH connection (the host running Terraform). The feature is
useful during bootstrapping if the host being provisioned
does not yet have direct network access to required resources, e.g. a Chef Server.

**Additional arguments only supported by the `winrm` connection type:**

* `https` - Set to `true` to connect using HTTPS instead of HTTP.
Expand Down