diff --git a/ssh_tunnel.go b/ssh_tunnel.go new file mode 100644 index 0000000..fbac31a --- /dev/null +++ b/ssh_tunnel.go @@ -0,0 +1,56 @@ +package sup + +import ( + "github.com/pkg/errors" + "fmt" + "net" + "io" + "strings" + "os" +) + +type SSHTunnel struct { + Tunnel Tunnel + SSHClient *SSHClient + err chan error +} + +func (t Tunnel) String() string { + return fmt.Sprintf("%d:%s:%d\n", t.ListenPort, t.Host, t.DstPort) +} + +func (t *SSHTunnel) StartTunnel() { + lAddr := fmt.Sprintf("localhost:%d", t.Tunnel.ListenPort) + ln, err := t.SSHClient.conn.Listen("tcp", lAddr) + if err != nil { + fmt.Fprintf(os.Stderr, "Remote tunnel listen failed: %s\n", err) + return + } + defer ln.Close() + + for { + conn, err := ln.Accept() + if err != nil { + fmt.Fprintf(os.Stderr, "Remote tunnel accept faild: %s\n", err) + } + go t.forward(conn) + } +} + +func (t *SSHTunnel) forward(remoteConn net.Conn) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", t.Tunnel.Host, t.Tunnel.DstPort)) + if err != nil { + t.err <- errors.Wrap(err, "local tunnel connection error") + } + + copy := func(writer, reader net.Conn) { + _, err:= io.Copy(writer, reader) + if err != nil && ! strings.Contains(err.Error(), "use of closed network connection") { + t.err <- errors.Wrap(err, "Tunnel forward error") + } + writer.Close() + } + + go copy(conn, remoteConn) + go copy(remoteConn, conn); +} diff --git a/sup.go b/sup.go index d528162..505c7eb 100644 --- a/sup.go +++ b/sup.go @@ -84,6 +84,14 @@ func (sup *Stackup) Run(network *Network, envVars EnvList, commands ...*Command) errCh <- errors.Wrap(err, "connecting to remote host failed") return } + for _, tunnel := range network.Tunnels { + sshTunnel := &SSHTunnel { + Tunnel: tunnel, + SSHClient: remote, + } + go sshTunnel.StartTunnel() + } + } clientCh <- remote }(i, host) diff --git a/supfile.go b/supfile.go index 87ff531..f9b78ec 100644 --- a/supfile.go +++ b/supfile.go @@ -29,8 +29,8 @@ type Network struct { Inventory string `yaml:"inventory"` Hosts []string `yaml:"hosts"` Bastion string `yaml:"bastion"` // Jump host for the environment + Tunnels []Tunnel `yaml:"tunnels"`// Add remote port forwarding } - // Command represents command(s) to be run remotely. type Command struct { Name string `yaml:"-"` // Command name. @@ -55,6 +55,13 @@ type Upload struct { Exc string `yaml:"exclude"` } +// Tunnel represents a Remote Port forwarding specification +type Tunnel struct { + ListenPort int `yaml:"listen"` + Host string `yaml:"host"` + DstPort int `yaml:"port"` +} + // EnvVar represents an environment variable type EnvVar struct { Key string