SSH tunneling support
Support for both local and remote TCP port tunneling. Includes updated docs and tests. Does not implement dynamic port forwarding (SSH's built-in SOCKS) (uncertain difficulty) nor unix socket (potentially easy).
This commit is contained in:
parent
f2a517dfd7
commit
3b64620234
|
@ -35,6 +35,24 @@ type comm struct {
|
||||||
address string
|
address string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TunnelDirection is the supported tunnel directions
|
||||||
|
type TunnelDirection int
|
||||||
|
|
||||||
|
const (
|
||||||
|
UnsetTunnel TunnelDirection = iota
|
||||||
|
RemoteTunnel
|
||||||
|
LocalTunnel
|
||||||
|
)
|
||||||
|
|
||||||
|
// TunnelSpec represents a request to map a port on one side of the SSH connection to the other
|
||||||
|
type TunnelSpec struct {
|
||||||
|
Direction TunnelDirection
|
||||||
|
ListenType string
|
||||||
|
ListenAddr string
|
||||||
|
ForwardType string
|
||||||
|
ForwardAddr string
|
||||||
|
}
|
||||||
|
|
||||||
// Config is the structure used to configure the SSH communicator.
|
// Config is the structure used to configure the SSH communicator.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// The configuration of the Go SSH connection
|
// The configuration of the Go SSH connection
|
||||||
|
@ -64,6 +82,8 @@ type Config struct {
|
||||||
|
|
||||||
// Timeout is how long to wait for a read or write to succeed.
|
// Timeout is how long to wait for a read or write to succeed.
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
|
|
||||||
|
Tunnels []TunnelSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new packer.Communicator implementation over SSH. This takes
|
// Creates a new packer.Communicator implementation over SSH. This takes
|
||||||
|
@ -344,10 +364,69 @@ func (c *comm) reconnect() (err error) {
|
||||||
c.client = ssh.NewClient(sshConn, sshChan, req)
|
c.client = ssh.NewClient(sshConn, sshChan, req)
|
||||||
}
|
}
|
||||||
c.connectToAgent()
|
c.connectToAgent()
|
||||||
|
c.connectTunnels(sshConn)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *comm) connectTunnels(sshConn ssh.Conn) {
|
||||||
|
if c.client == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start remote forwards of ports to ourselves.
|
||||||
|
log.Printf("[DEBUG] Tunnel Configuration: %v", c.config.Tunnels)
|
||||||
|
for _, v := range c.config.Tunnels {
|
||||||
|
done := make(chan struct{})
|
||||||
|
switch v.Direction {
|
||||||
|
case RemoteTunnel:
|
||||||
|
// This requests the sshd Host to bind a port and send traffic back to us
|
||||||
|
listener, err := c.client.Listen(v.ListenType, v.ListenAddr)
|
||||||
|
// TODO How can we get this failure to ui.Error?
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] Tunnel: unable to bind remote tunnel ('%v'): %s", v, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("[INFO] Tunnel: Remote bound on %s forwarding to %s", v.ListenAddr, v.ForwardAddr)
|
||||||
|
connectFunc := ConnectFunc(v.ForwardType, v.ForwardAddr)
|
||||||
|
go ProxyServe(listener, done, connectFunc)
|
||||||
|
// Wait for our sshConn to be shutdown
|
||||||
|
// FIXME: Is there a better "on-shutdown" we can wait on?
|
||||||
|
go shutdownProxyTunnel(sshConn, done, listener)
|
||||||
|
case LocalTunnel:
|
||||||
|
// This binds locally and sends traffic back to the sshd host
|
||||||
|
listener, err := net.Listen(v.ListenType, v.ListenAddr)
|
||||||
|
if err != nil {
|
||||||
|
// TODO How can we get this failure to ui.Error?
|
||||||
|
log.Printf("[ERROR] Tunnel: unable to bind local tunnel ('%v'): %s", v, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("[INFO] Tunnel: Local bound on %s forwarding to %s", v.ListenAddr, v.ForwardAddr)
|
||||||
|
connectFunc := func() (net.Conn, error) {
|
||||||
|
// This Dial occurs on the SSH server's side
|
||||||
|
return c.client.Dial(v.ForwardType, v.ForwardAddr)
|
||||||
|
}
|
||||||
|
go ProxyServe(listener, done, connectFunc)
|
||||||
|
// FIXME: Is there a better "on-shutdown" we can wait on?
|
||||||
|
go shutdownProxyTunnel(sshConn, done, listener)
|
||||||
|
default:
|
||||||
|
log.Printf("[ERROR] Tunnel: Unknown tunnel type ('%v'): %v", v, v.Direction)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// shutdownProxyTunnel waits for our sshConn to be shutdown and closes the listeners
|
||||||
|
func shutdownProxyTunnel(sshConn ssh.Conn, done chan struct{}, listener net.Listener) {
|
||||||
|
sshConn.Wait()
|
||||||
|
log.Printf("[INFO] Tunnel: Shutting down listener %v", listener)
|
||||||
|
done <- struct{}{}
|
||||||
|
close(done)
|
||||||
|
listener.Close()
|
||||||
|
}
|
||||||
|
|
||||||
func (c *comm) connectToAgent() {
|
func (c *comm) connectToAgent() {
|
||||||
if c.client == nil {
|
if c.client == nil {
|
||||||
return
|
return
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProxyServe starts Accepting connections
|
||||||
|
func ProxyServe(l net.Listener, done <-chan struct{}, dialer func() (net.Conn, error)) {
|
||||||
|
for {
|
||||||
|
// Accept will return if either the underlying connection is closed or if a connection is made.
|
||||||
|
// after returning, check to see if c.done can be received. If so, then Accept() returned because
|
||||||
|
// the connection has been closed.
|
||||||
|
client, err := l.Accept()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
log.Printf("[WARN] Tunnel: received Done event: %v", err)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] Tunnel: listen.Accept failed: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG] Tunnel: client '%s' accepted", client.RemoteAddr())
|
||||||
|
// Proxy bytes from one side to the other
|
||||||
|
go handleProxyClient(client, dialer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleProxyClient will open a connection using the dialer, and ensure close events propagate to the brokers
|
||||||
|
func handleProxyClient(clientConn net.Conn, dialer func() (net.Conn, error)) {
|
||||||
|
//We have a client connected, open an upstream connection to the destination
|
||||||
|
upstreamConn, err := dialer()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] Tunnel: failed to open connection to upstream: %v", err)
|
||||||
|
clientConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// channels to wait on the close event for each connection
|
||||||
|
serverClosed := make(chan struct{}, 1)
|
||||||
|
upstreamClosed := make(chan struct{}, 1)
|
||||||
|
|
||||||
|
go brokerData(clientConn, upstreamConn, upstreamClosed)
|
||||||
|
go brokerData(upstreamConn, clientConn, serverClosed)
|
||||||
|
|
||||||
|
// Now we wait for the connections to close and notify the other side of the event
|
||||||
|
select {
|
||||||
|
case <-upstreamClosed:
|
||||||
|
clientConn.Close()
|
||||||
|
<-serverClosed
|
||||||
|
case <-serverClosed:
|
||||||
|
upstreamConn.Close()
|
||||||
|
<-upstreamClosed
|
||||||
|
}
|
||||||
|
log.Printf("[DEBUG] Tunnel: client ('%s') proxy closed", clientConn.RemoteAddr())
|
||||||
|
}
|
||||||
|
|
||||||
|
// brokerData is responsible for copying data src => dest. It will also close the src when there are no more bytes to transfer
|
||||||
|
func brokerData(src net.Conn, dest net.Conn, srcClosed chan struct{}) {
|
||||||
|
_, err := io.Copy(src, dest)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] Tunnel: Copy error: %s", err)
|
||||||
|
}
|
||||||
|
if err := src.Close(); err != nil {
|
||||||
|
log.Printf("[ERROR] Tunnel: Close error: %s", err)
|
||||||
|
}
|
||||||
|
srcClosed <- struct{}{}
|
||||||
|
}
|
|
@ -44,6 +44,8 @@ type Config struct {
|
||||||
SSHBastionPassword string `mapstructure:"ssh_bastion_password"`
|
SSHBastionPassword string `mapstructure:"ssh_bastion_password"`
|
||||||
SSHBastionPrivateKeyFile string `mapstructure:"ssh_bastion_private_key_file"`
|
SSHBastionPrivateKeyFile string `mapstructure:"ssh_bastion_private_key_file"`
|
||||||
SSHFileTransferMethod string `mapstructure:"ssh_file_transfer_method"`
|
SSHFileTransferMethod string `mapstructure:"ssh_file_transfer_method"`
|
||||||
|
SSHRemoteTunnels []string `mapstructure:"ssh_remote_tunnels"`
|
||||||
|
SSHLocalTunnels []string `mapstructure:"ssh_local_tunnels"`
|
||||||
SSHProxyHost string `mapstructure:"ssh_proxy_host"`
|
SSHProxyHost string `mapstructure:"ssh_proxy_host"`
|
||||||
SSHProxyPort int `mapstructure:"ssh_proxy_port"`
|
SSHProxyPort int `mapstructure:"ssh_proxy_port"`
|
||||||
SSHProxyUsername string `mapstructure:"ssh_proxy_username"`
|
SSHProxyUsername string `mapstructure:"ssh_proxy_username"`
|
||||||
|
@ -305,6 +307,22 @@ func (c *Config) prepareSSH(ctx *interpolate.Context) []error {
|
||||||
errs = append(errs, errors.New("please specify either ssh_bastion_host or ssh_proxy_host, not both"))
|
errs = append(errs, errors.New("please specify either ssh_bastion_host or ssh_proxy_host, not both"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, v := range c.SSHLocalTunnels {
|
||||||
|
_, err := helperssh.ParseTunnelArgument(v, packerssh.UnsetTunnel)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf(
|
||||||
|
"ssh_local_tunnels ('%s') is invalid: %s", v, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range c.SSHRemoteTunnels {
|
||||||
|
_, err := helperssh.ParseTunnelArgument(v, packerssh.UnsetTunnel)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf(
|
||||||
|
"ssh_remote_tunnels ('%s') is invalid: %s", v, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -172,6 +172,25 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, ctx context.Contex
|
||||||
}
|
}
|
||||||
nc.Close()
|
nc.Close()
|
||||||
|
|
||||||
|
// Parse out all the requested Port Tunnels that will go over our SSH connection
|
||||||
|
var tunnels []ssh.TunnelSpec
|
||||||
|
for _, v := range s.Config.SSHLocalTunnels {
|
||||||
|
t, err := helperssh.ParseTunnelArgument(v, ssh.LocalTunnel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"Error parsing port forwarding: %s", err)
|
||||||
|
}
|
||||||
|
tunnels = append(tunnels, t)
|
||||||
|
}
|
||||||
|
for _, v := range s.Config.SSHRemoteTunnels {
|
||||||
|
t, err := helperssh.ParseTunnelArgument(v, ssh.RemoteTunnel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"Error parsing port forwarding: %s", err)
|
||||||
|
}
|
||||||
|
tunnels = append(tunnels, t)
|
||||||
|
}
|
||||||
|
|
||||||
// Then we attempt to connect via SSH
|
// Then we attempt to connect via SSH
|
||||||
config := &ssh.Config{
|
config := &ssh.Config{
|
||||||
Connection: connFunc,
|
Connection: connFunc,
|
||||||
|
@ -181,6 +200,7 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, ctx context.Contex
|
||||||
UseSftp: s.Config.SSHFileTransferMethod == "sftp",
|
UseSftp: s.Config.SSHFileTransferMethod == "sftp",
|
||||||
KeepAliveInterval: s.Config.SSHKeepAliveInterval,
|
KeepAliveInterval: s.Config.SSHKeepAliveInterval,
|
||||||
Timeout: s.Config.SSHReadWriteTimeout,
|
Timeout: s.Config.SSHReadWriteTimeout,
|
||||||
|
Tunnels: tunnels,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[INFO] Attempting SSH connection to %s...", address)
|
log.Printf("[INFO] Attempting SSH connection to %s...", address)
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer/communicator/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseTunnelArgument parses an SSH tunneling argument compatible with the openssh client form.
|
||||||
|
// Valid formats:
|
||||||
|
// `port:host:hostport`
|
||||||
|
// NYI `[bind_address:]port:host:hostport`
|
||||||
|
func ParseTunnelArgument(forward string, direction ssh.TunnelDirection) (ssh.TunnelSpec, error) {
|
||||||
|
parts := strings.SplitN(forward, ":", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return ssh.TunnelSpec{}, fmt.Errorf("Error parsing tunnel '%s': %v", forward, parts)
|
||||||
|
}
|
||||||
|
listeningPort, forwardingAddr := parts[0], parts[1]
|
||||||
|
|
||||||
|
_, sPort, err := net.SplitHostPort(forwardingAddr)
|
||||||
|
if err != nil {
|
||||||
|
return ssh.TunnelSpec{}, fmt.Errorf("Error parsing forwarding, must be a tcp address: %s", err)
|
||||||
|
}
|
||||||
|
_, err = strconv.Atoi(sPort)
|
||||||
|
if err != nil {
|
||||||
|
return ssh.TunnelSpec{}, fmt.Errorf("Error parsing forwarding port, must be a valid port: %s", err)
|
||||||
|
}
|
||||||
|
_, err = strconv.Atoi(listeningPort)
|
||||||
|
if err != nil {
|
||||||
|
return ssh.TunnelSpec{}, fmt.Errorf("Error parsing listening port, must be a valid port: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ssh.TunnelSpec{
|
||||||
|
Direction: direction,
|
||||||
|
ForwardAddr: forwardingAddr,
|
||||||
|
ForwardType: "tcp",
|
||||||
|
ListenAddr: fmt.Sprintf("localhost:%s", listeningPort),
|
||||||
|
ListenType: "tcp",
|
||||||
|
}, nil
|
||||||
|
// So we parsed all that, and are just going to ignore it now. We would
|
||||||
|
// have used the information to set the type here.
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/packer/communicator/ssh"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tunnel8080ToLocal = "8080:localhost:1234"
|
||||||
|
tunnel8080ToRemote = "8080:example.com:80"
|
||||||
|
bindRemoteAddress_NYI = "redis:6379:localhost:6379"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTCPToLocalTCP(t *testing.T) {
|
||||||
|
tun, err := ParseTunnelArgument(tunnel8080ToLocal, ssh.UnsetTunnel)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
expectedTun := ssh.TunnelSpec{
|
||||||
|
Direction: ssh.UnsetTunnel,
|
||||||
|
ForwardAddr: "localhost:1234",
|
||||||
|
ForwardType: "tcp",
|
||||||
|
ListenAddr: "localhost:8080",
|
||||||
|
ListenType: "tcp",
|
||||||
|
}
|
||||||
|
if tun != expectedTun {
|
||||||
|
t.Errorf("Parsed tunnel (%v), want %v", tun, expectedTun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTCPToRemoteTCP(t *testing.T) {
|
||||||
|
tun, err := ParseTunnelArgument(tunnel8080ToRemote, ssh.UnsetTunnel)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
expectedTun := ssh.TunnelSpec{
|
||||||
|
Direction: ssh.UnsetTunnel,
|
||||||
|
ForwardAddr: "example.com:80",
|
||||||
|
ForwardType: "tcp",
|
||||||
|
ListenAddr: "localhost:8080",
|
||||||
|
ListenType: "tcp",
|
||||||
|
}
|
||||||
|
if tun != expectedTun {
|
||||||
|
t.Errorf("Parsed tunnel (%v), want %v", tun, expectedTun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindAddress_NYI(t *testing.T) {
|
||||||
|
tun, err := ParseTunnelArgument(bindRemoteAddress_NYI, ssh.UnsetTunnel)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
expectedTun := ssh.TunnelSpec{
|
||||||
|
Direction: ssh.UnsetTunnel,
|
||||||
|
ForwardAddr: "redis:6379",
|
||||||
|
ForwardType: "tcp",
|
||||||
|
ListenAddr: "localhost:6379",
|
||||||
|
ListenType: "tcp",
|
||||||
|
}
|
||||||
|
if tun == expectedTun {
|
||||||
|
t.Errorf("Parsed tunnel (%v), want %v", tun, expectedTun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidTunnels(t *testing.T) {
|
||||||
|
invalids := []string{
|
||||||
|
"nope:8080", // insufficient parts
|
||||||
|
"nope:localhost:8080", // listen port is not a number
|
||||||
|
"8080:localhost:nope", // forwarding port is not a number
|
||||||
|
"/unix/is/no/go:/path/to/nowhere", // unix socket is unsupported
|
||||||
|
}
|
||||||
|
for _, tunnelStr := range invalids {
|
||||||
|
tun, err := ParseTunnelArgument(tunnelStr, ssh.UnsetTunnel)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Parsed tunnel %v, want error", tun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
#!/usr/bin/env bats
|
||||||
|
#
|
||||||
|
# This tests the ssh communicator using AWS builder. The teardown function will automatically
|
||||||
|
# delete any AMIs with a tag of `packer-test` being equal to "true" so
|
||||||
|
# be sure any test cases set this.
|
||||||
|
|
||||||
|
load test_helper
|
||||||
|
verify_aws_cli
|
||||||
|
fixtures communicator-ssh
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
cd $FIXTURE_ROOT
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
aws_ami_cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "shell provisioner: local port tunneling" {
|
||||||
|
run packer build $FIXTURE_ROOT/local-tunnel.json
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"Connection to localhost port 10022 [tcp/*] succeeded"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "shell provisioner: remote port tunneling" {
|
||||||
|
run packer build $FIXTURE_ROOT/remote-tunnel.json
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
MY_LOCAL_IP=$(curl -s https://ifconfig.co/)
|
||||||
|
[[ "$output" == *"$MY_LOCAL_IP"* ]]
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"builders": [{
|
||||||
|
"type": "amazon-ebs",
|
||||||
|
"ami_name": "packer-test {{timestamp}}",
|
||||||
|
"instance_type": "m1.small",
|
||||||
|
"region": "us-east-1",
|
||||||
|
"ssh_username": "ubuntu",
|
||||||
|
"ssh_local_tunnels": ["10022:localhost:22"],
|
||||||
|
"source_ami": "ami-0568456c",
|
||||||
|
"tags": {
|
||||||
|
"packer-test": "true"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
|
||||||
|
"provisioners": [{
|
||||||
|
"type": "shell-local",
|
||||||
|
"inline": [
|
||||||
|
"echo | nc -G 5 -w 5 -v localhost 10022 2>&1"
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"builders": [{
|
||||||
|
"type": "amazon-ebs",
|
||||||
|
"ami_name": "packer-test {{timestamp}}",
|
||||||
|
"instance_type": "t2.micro",
|
||||||
|
"region": "us-east-1",
|
||||||
|
"ssh_username": "ubuntu",
|
||||||
|
"ssh_remote_tunnels": ["8443:ifconfig.co:443"],
|
||||||
|
"source_ami": "ami-0111e8c43a763eb71",
|
||||||
|
"tags": {
|
||||||
|
"packer-test": "true"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"provisioners": [{
|
||||||
|
"inline": [
|
||||||
|
"curl -kvs --connect-to ifconfig.co:443:localhost:8443 https://ifconfig.co/"
|
||||||
|
],
|
||||||
|
"type": "shell"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
@ -106,6 +106,12 @@ The SSH communicator has the following options:
|
||||||
messages to the server. Set to a negative value (`-1s`) to disable. Example
|
messages to the server. Set to a negative value (`-1s`) to disable. Example
|
||||||
value: `10s`. Defaults to `5s`.
|
value: `10s`. Defaults to `5s`.
|
||||||
|
|
||||||
|
- `ssh_local_tunnels` (array of strings) - An array of OpenSSH-style tunnels to
|
||||||
|
create. The port is bound on the *local packer host* and connections are
|
||||||
|
forwarded to the remote destinations. Note unless `GatewayPorts=yes` is set
|
||||||
|
in SSHD dameon, the target *must* be `localhost`. Example value:
|
||||||
|
`8080:localhost:8000`
|
||||||
|
|
||||||
- `ssh_password` (string) - A plaintext password to use to authenticate with
|
- `ssh_password` (string) - A plaintext password to use to authenticate with
|
||||||
SSH.
|
SSH.
|
||||||
|
|
||||||
|
@ -132,6 +138,11 @@ The SSH communicator has the following options:
|
||||||
command to end. This might be useful if, for example, packer hangs on a
|
command to end. This might be useful if, for example, packer hangs on a
|
||||||
connection after a reboot. Example: `5m`. Disabled by default.
|
connection after a reboot. Example: `5m`. Disabled by default.
|
||||||
|
|
||||||
|
- `ssh_remote_tunnels` (array of strings) - An array of OpenSSH-style tunnels
|
||||||
|
to create. The port is bound on the *remote build host* and connections are
|
||||||
|
forwarded to the packer host's network. Non-localhost destinations may be set here.
|
||||||
|
Example value: `8443:git.example.com:443`
|
||||||
|
|
||||||
- `ssh_timeout` (string) - The time to wait for SSH to become available.
|
- `ssh_timeout` (string) - The time to wait for SSH to become available.
|
||||||
Packer uses this to determine when the machine has booted so this is
|
Packer uses this to determine when the machine has booted so this is
|
||||||
usually quite long. Example value: `10m`.
|
usually quite long. Example value: `10m`.
|
||||||
|
|
Loading…
Reference in New Issue