Implemented local command execution

Added two things to `tsh`:

1. `--local` flag. This tells `tsh` to execute a given command
   _locally_. This is useful in combination with `-L` flag (port
   forwarding)

2. Added support for "bind_interface" for `-L` flag for compatibility
   with OpenSSH

3. Minor refactoring

4. Updated docs
This commit is contained in:
Ev Kontsevoy 2016-03-31 16:02:39 -07:00
parent 7b79dd0187
commit ba381fd54e
5 changed files with 80 additions and 27 deletions

View file

@ -131,6 +131,7 @@ Flags:
-p, --port SSH port on a remote host
-l, --login Remote host login
-L, --forward Forward localhost connections to remote server
--local Execute command on localhost after connecting to SSH node
Args:
<[user@]host> Remote hostname and the login to use
@ -150,7 +151,13 @@ using familiar SSH syntax:
### Port Forwarding
`tsh ssh` supports OpenSSH `-L` flag which allows to forward incoming connections from localhost
to the specified remote host:port.
to the specified remote host:port. The syntax of `-L` flag is:
```
-L [bind_interface]:listen_port:remote_host:remote_port
```
where "bind_interface" defaults to `127.0.0.1`.
Exmaple:
```
@ -161,6 +168,17 @@ Will connect to remote server `node` via `work` proxy, then it will open a liste
`localhost:5000` and will forward all incoming connections to `web.remote:80` via this SSH
tunnel.
It is often convenient to establish port forwarding, execute a local command which uses such
connection and disconnect. Yon can do this via `--local` flag.
Example:
```
> tsh --proxy=work ssh -L 5000:google.com:80 --local node curl http://localhost:5000
```
This forwards just one curl request for `localhost:5000` to `google:80` via "node" server located
behind "work" proxy and terminates.
### Resolving Node Names
`tsh` supports multiple methods to resolve remote node names.

View file

@ -27,6 +27,7 @@ import (
"math/rand"
"net"
"os"
"os/exec"
"os/signal"
"os/user"
"path/filepath"
@ -52,6 +53,7 @@ import (
)
type ForwardedPort struct {
SrcIP string
SrcPort int
DestPort int
DestHost string
@ -225,7 +227,7 @@ func (tc *TeleportClient) getTargetNodes(proxy *ProxyClient) ([]string, error) {
// SSH connects to a node and, if 'command' is specified, executes the command on it,
// otherwise runs interactive shell
func (tc *TeleportClient) SSH(command string) error {
func (tc *TeleportClient) SSH(command []string, runLocally bool) error {
// connect to proxy first:
if !tc.Config.ProxySpecified() {
return trace.Wrap(teleport.BadParameter("server", "proxy server is not specified"))
@ -245,31 +247,42 @@ func (tc *TeleportClient) SSH(command string) error {
return trace.Wrap(teleport.BadParameter("host", "no target host specified"))
}
// execute non-interactive SSH command:
if len(command) > 0 {
return tc.runCommand(nodeAddrs, proxyClient, command)
}
// more than one node for an interactive shell?
// that can't be!
if len(nodeAddrs) != 1 {
return trace.Errorf("Cannot launch shell on multiple nodes: %v", nodeAddrs)
}
// execute SSH shell on a single node:
nodeClient, err := proxyClient.ConnectToNode(nodeAddrs[0], tc.Config.HostLogin)
if err != nil {
return trace.Wrap(err)
}
defer nodeClient.Close()
// proxy local ports (forward incoming connections to remote host ports)
for _, fp := range tc.Config.LocalForwardPorts {
socket, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(fp.SrcPort)))
if len(tc.Config.LocalForwardPorts) > 0 {
nodeClient, err := proxyClient.ConnectToNode(nodeAddrs[0], tc.Config.HostLogin)
if err != nil {
return trace.Wrap(err)
}
go nodeClient.listenAndForward(socket, net.JoinHostPort(fp.DestHost, strconv.Itoa(fp.DestPort)))
for _, fp := range tc.Config.LocalForwardPorts {
socket, err := net.Listen("tcp", net.JoinHostPort(fp.SrcIP, strconv.Itoa(fp.SrcPort)))
if err != nil {
return trace.Wrap(err)
}
go nodeClient.listenAndForward(socket, net.JoinHostPort(fp.DestHost, strconv.Itoa(fp.DestPort)))
}
}
// local execution?
if runLocally {
if len(tc.Config.LocalForwardPorts) == 0 {
fmt.Println("Executing command locally without connecting to any servers. This makes no sense.")
}
return runLocalCommand(command)
}
// execute command(s) or a shell on remote node(s)
if len(command) > 0 {
return tc.runCommand(nodeAddrs, proxyClient, command)
}
nodeClient, err := proxyClient.ConnectToNode(nodeAddrs[0], tc.Config.HostLogin)
if err != nil {
return trace.Wrap(err)
}
return tc.runShell(nodeClient, "")
}
@ -431,8 +444,7 @@ func (tc *TeleportClient) ListNodes() ([]services.Server, error) {
}
// runCommand executes a given bash command on a bunch of remote nodes
func (tc *TeleportClient) runCommand(nodeAddresses []string, proxyClient *ProxyClient, command string) error {
func (tc *TeleportClient) runCommand(nodeAddresses []string, proxyClient *ProxyClient, command []string) error {
resultsC := make(chan error, len(nodeAddresses))
for _, address := range nodeAddresses {
go func(address string) {
@ -865,3 +877,13 @@ func ParseLabelSpec(spec string) (map[string]string, error) {
func authMethodFromAgent(ag agent.Agent) ssh.AuthMethod {
return ssh.PublicKeysCallback(ag.Signers)
}
// Executes the given command on the client machine (localhost). If no command is given,
// executes shell
func runLocalCommand(command []string) error {
cmd := exec.Command(command[0], command[1:]...)
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
return cmd.Run()
}

View file

@ -25,6 +25,7 @@ import (
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
@ -394,7 +395,7 @@ func (client *NodeClient) Shell(width, height int, sessionID session.ID) (io.Rea
// Run executes command on the remote server and writes its stdout to
// the 'output' argument
func (client *NodeClient) Run(cmd string, output io.Writer) error {
func (client *NodeClient) Run(cmd []string, output io.Writer) error {
session, err := client.Client.NewSession()
if err != nil {
return trace.Wrap(err)
@ -402,7 +403,7 @@ func (client *NodeClient) Run(cmd string, output io.Writer) error {
session.Stdout = output
if err := session.Run(cmd); err != nil {
if err := session.Run(strings.Join(cmd, " ")); err != nil {
return trace.Wrap(err)
}
@ -521,6 +522,7 @@ func (client *NodeClient) scp(scpCommand scp.Command, shellCmd string) error {
// to the given remote address via
func (client *NodeClient) listenAndForward(socket net.Listener, remoteAddr string) {
defer socket.Close()
defer client.Close()
for {
incoming, err := socket.Accept()
if err != nil {

View file

@ -69,6 +69,8 @@ type CLIConf struct {
RecursiveCopy bool
// -L flag for ssh. Local port forwarding like 'ssh -L 80:remote.host:80 -L 443:remote.host:443'
LocalForwardPorts []string
// --local flag for ssh
LocalExec bool
}
// run executes TSH client. same as main() but easier to test
@ -95,6 +97,7 @@ func run(args []string, underTest bool) {
ssh.Flag("port", "SSH port on a remote host").Short('p').Int16Var(&cf.NodePort)
ssh.Flag("login", "Remote host login").Short('l').StringVar(&cf.NodeLogin)
ssh.Flag("forward", "Forward localhost connections to remote server").Short('L').StringsVar(&cf.LocalForwardPorts)
ssh.Flag("local", "Execute command on localhost after connecting to SSH node").Default("false").BoolVar(&cf.LocalExec)
// join
join := app.Command("join", "Join the active SSH session")
join.Arg("session-id", "ID of the session to join").Required().SetValue(&cf.SessionID)
@ -221,7 +224,7 @@ func onSSH(cf *CLIConf) {
utils.FatalError(err)
}
if err = tc.SSH(strings.Join(cf.RemoteCommand, " ")); err != nil {
if err = tc.SSH(cf.RemoteCommand, cf.LocalExec); err != nil {
utils.FatalError(err)
}
}
@ -309,6 +312,7 @@ func printHeader(t *goterm.Table, cols []string) {
fmt.Fprint(t, strings.Join(dots, "\t")+"\n")
}
// parsePortForwardSpec parses parameter to -L flag, i.e. strings like "[ip]:80:remote.host:3000"
func parsePortForwardSpec(spec []string) (ports []client.ForwardedPort, err error) {
if len(spec) == 0 {
return ports, nil
@ -318,16 +322,20 @@ func parsePortForwardSpec(spec []string) (ports []client.ForwardedPort, err erro
for i, str := range spec {
parts := strings.Split(str, ":")
if len(parts) != 3 {
if len(parts) < 3 || len(parts) > 4 {
return nil, fmt.Errorf(errTemplate, str)
}
if len(parts) == 3 {
parts = append([]string{"127.0.0.1"}, parts...)
}
p := &ports[i]
p.SrcPort, err = strconv.Atoi(parts[0])
p.SrcIP = parts[0]
p.SrcPort, err = strconv.Atoi(parts[1])
if err != nil {
return nil, fmt.Errorf(errTemplate, str)
}
p.DestHost = parts[1]
p.DestPort, err = strconv.Atoi(parts[2])
p.DestHost = parts[2]
p.DestPort, err = strconv.Atoi(parts[3])
if err != nil {
return nil, fmt.Errorf(errTemplate, str)
}

View file

@ -69,6 +69,7 @@ func (s *MainTestSuite) TestMakeClient(c *check.C) {
c.Assert(tc.Config.HostLogin, check.Equals, "root")
c.Assert(tc.Config.LocalForwardPorts, check.DeepEquals, []client.ForwardedPort{
{
SrcIP: "127.0.0.1",
SrcPort: 80,
DestHost: "remote",
DestPort: 180,
@ -87,18 +88,20 @@ func (s *MainTestSuite) TestPortsParsing(c *check.C) {
// not empty (but valid)
spec := []string{
"80:remote.host:180",
"443:deep.host:1443",
"10.0.10.1:443:deep.host:1443",
}
ports, err = parsePortForwardSpec(spec)
c.Assert(err, check.IsNil)
c.Assert(ports, check.HasLen, 2)
c.Assert(ports, check.DeepEquals, []client.ForwardedPort{
{
SrcIP: "127.0.0.1",
SrcPort: 80,
DestHost: "remote.host",
DestPort: 180,
},
{
SrcIP: "10.0.10.1",
SrcPort: 443,
DestHost: "deep.host",
DestPort: 1443,