mirror of
https://github.com/gravitational/teleport
synced 2024-10-21 17:53:28 +00:00
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:
parent
7b79dd0187
commit
ba381fd54e
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue