From e93e4c36556c555d11ed8c058305322ee10f35f3 Mon Sep 17 00:00:00 2001 From: Russell Jones Date: Wed, 12 Feb 2020 21:29:30 +0000 Subject: [PATCH] Added support for reexec during port forwarding. Added support for reexec during port forwarding. This allows Teleport nodes to run PAM code before port forwarding requests. This makes any memory leaks in PAM code less dangerous as well as bringing port forwarding logic in-line with execution requests (exec or shell). --- constants.go | 15 +- integration/integration_test.go | 5 +- lib/srv/ctx.go | 36 ++- lib/srv/exec.go | 387 +--------------------- lib/srv/exec_test.go | 4 +- lib/srv/forward/sshserver.go | 23 +- lib/srv/reexec.go | 522 ++++++++++++++++++++++++++++++ lib/srv/regular/sshserver.go | 146 +++++---- lib/srv/regular/sshserver_test.go | 44 ++- lib/srv/term.go | 2 +- lib/web/apiserver_test.go | 5 +- tool/teleport/common/teleport.go | 25 +- 12 files changed, 724 insertions(+), 490 deletions(-) create mode 100644 lib/srv/reexec.go diff --git a/constants.go b/constants.go index 1a6d02cffb5cc..705f1f86702c6 100644 --- a/constants.go +++ b/constants.go @@ -597,8 +597,21 @@ const ( ) const ( - // ExecSubCommand is the sub-command Teleport uses to re-exec itself. + // ExecSubCommand is the sub-command Teleport uses to re-exec itself for + // command execution (exec and shells). ExecSubCommand = "exec" + + // ForwardSubCommand is the sub-command Teleport uses to re-exec itself + // for port forwarding. + ForwardSubCommand = "forward" +) + +const ( + // ChanDirectTCPIP is a SSH channel of type "direct-tcpip". + ChanDirectTCPIP = "direct-tcpip" + + // ChanSession is a SSH channel of type "session". + ChanSession = "session" ) // RSAKeySize is the size of the RSA key. diff --git a/integration/integration_test.go b/integration/integration_test.go index 2c6cb5b31dc14..eb83ac55e3682 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -88,8 +88,9 @@ var _ = check.Suite(&IntSuite{}) func TestMain(m *testing.M) { // If the test is re-executing itself, execute the command that comes over // the pipe. - if len(os.Args) == 2 && os.Args[1] == teleport.ExecSubCommand { - srv.RunAndExit() + if len(os.Args) == 2 && + (os.Args[1] == teleport.ExecSubCommand || os.Args[1] == teleport.ForwardSubCommand) { + srv.RunAndExit(os.Args[1]) return } diff --git a/lib/srv/ctx.go b/lib/srv/ctx.go index 1b28e7dc6b130..645e5186931e5 100644 --- a/lib/srv/ctx.go +++ b/lib/srv/ctx.go @@ -273,6 +273,20 @@ type ServerContext struct { // to the child process. contr *os.File contw *os.File + + // ChannelType holds the type of the channel. For example "session" or + // "direct-tcpip". Used to create correct subcommand during re-exec. + ChannelType string + + // SrcAddr is the source address of the request. This the originator IP + // address and port in a SSH "direct-tcpip" request. This value is only + // populated for port forwarding requests. + SrcAddr string + + // DstAddr is the destination address of the request. This is the host and + // port to connect to in a "direct-tcpip" request. This value is only + // populated for port forwarding requests. + DstAddr string } // NewServerContext creates a new *ServerContext which is used to pass and @@ -652,14 +666,30 @@ func (c *ServerContext) ExecCommand() (*execCommand, error) { roleNames = c.Identity.RoleSet.RoleNames() } + // Extract the command to be executed. This only exists if command execution + // (exec or shell) is being requested, port forwarding has no command to + // execute. + var command string + if c.ExecRequest != nil { + command = c.ExecRequest.GetCommand() + } + + // Extract the request type. This only exists for command execution (exec + // or shell), port forwarding requests have no request type. + var requestType string + if c.request != nil { + requestType = c.request.Type + } + // Create the execCommand that will be sent to the child process. return &execCommand{ - Command: c.ExecRequest.GetCommand(), + Command: command, + DestinationAddress: c.DstAddr, Username: c.Identity.TeleportUser, Login: c.Identity.Login, Roles: roleNames, - Terminal: c.termAllocated || c.ExecRequest.GetCommand() == "", - RequestType: c.request.Type, + Terminal: c.termAllocated || command == "", + RequestType: requestType, PermitUserEnvironment: c.srv.PermitUserEnvironment(), Environment: buildEnvironment(c), PAM: pamEnabled, diff --git a/lib/srv/exec.go b/lib/srv/exec.go index 37a7e0126c813..69aa5645b917e 100644 --- a/lib/srv/exec.go +++ b/lib/srv/exec.go @@ -20,13 +20,10 @@ import ( "bufio" "bytes" "context" - "encoding/json" "fmt" "io" - "io/ioutil" "os" "os/exec" - "os/user" "path/filepath" "strconv" "strings" @@ -38,10 +35,7 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/lib/bpf" "github.com/gravitational/teleport/lib/events" - "github.com/gravitational/teleport/lib/pam" "github.com/gravitational/teleport/lib/services" - "github.com/gravitational/teleport/lib/shell" - "github.com/gravitational/teleport/lib/sshutils" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/trace" @@ -55,52 +49,6 @@ const ( defaultLoginDefsPath = "/etc/login.defs" ) -// execCommand contains the payload to "teleport exec" will will be used to -// construct and execute a shell. -type execCommand struct { - // Command is the command to execute. If a interactive session is being - // requested, will be empty. - Command string `json:"command"` - - // Username is the username associated with the Teleport identity. - Username string `json:"username"` - - // Login is the local *nix account. - Login string `json:"login"` - - // Roles is the list of Teleport roles assigned to the Teleport identity. - Roles []string `json:"roles"` - - // ClusterName is the name of the Teleport cluster. - ClusterName string `json:"cluster_name"` - - // Terminal indicates if a TTY has been allocated for the session. This is - // typically set if either an shell was requested or a TTY was explicitly - // allocated for a exec request. - Terminal bool `json:"term"` - - // RequestType is the type of request: either "exec" or "shell". This will - // be used to control where to connect std{out,err} based on the request - // type: "exec" or "shell". - RequestType string `json:"request_type"` - - // PAM indicates if PAM support was requested by the node. - PAM bool `json:"pam"` - - // ServiceName is the name of the PAM service requested if PAM is enabled. - ServiceName string `json:"service_name"` - - // Environment is a list of environment variables to add to the defaults. - Environment []string `json:"environment"` - - // PermitUserEnvironment is set to allow reading in ~/.tsh/environment - // upon login. - PermitUserEnvironment bool `json:"permit_user_environment"` - - // IsTestStub is used by tests to mock the shell. - IsTestStub bool `json:"is_test_stub"` -} - // ExecResult is used internally to send the result of a command execution from // a goroutine to SSH request handler and back to the calling client type ExecResult struct { @@ -199,7 +147,7 @@ func (e *localExec) Start(channel ssh.Channel) (*ExecResult, error) { } // Create the command that will actually execute. - e.Cmd, err = configureCommand(e.Ctx) + e.Cmd, err = ConfigureCommand(e.Ctx) if err != nil { return nil, trace.Wrap(err) } @@ -281,133 +229,6 @@ func (e *localExec) String() string { return fmt.Sprintf("Exec(Command=%v)", e.Command) } -// RunAndExit will run the requested command and then exit. -func RunAndExit() { - w, code, err := RunCommand() - if err != nil { - s := fmt.Sprintf("Failed to launch shell: %v.\r\n", err) - io.Copy(w, bytes.NewBufferString(s)) - } - os.Exit(code) -} - -// RunCommand reads in the command to run from the parent process (over a -// pipe) then constructs and runs the command. -func RunCommand() (io.Writer, int, error) { - // errorWriter is used to return any error message back to the client. By - // default it writes to stdout, but if a TTY is allocated, it will write - // to it instead. - errorWriter := os.Stdout - - // Parent sends the command payload in the third file descriptor. - cmdfd := os.NewFile(uintptr(3), "/proc/self/fd/3") - if cmdfd == nil { - return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("command pipe not found") - } - contfd := os.NewFile(uintptr(4), "/proc/self/fd/4") - if cmdfd == nil { - return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("continue pipe not found") - } - - // Read in the command payload. - var b bytes.Buffer - _, err := b.ReadFrom(cmdfd) - if err != nil { - return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err) - } - var c execCommand - err = json.Unmarshal(b.Bytes(), &c) - if err != nil { - return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err) - } - - var tty *os.File - var pty *os.File - - // If a terminal was requested, file descriptor 4 and 5 always point to the - // PTY and TTY. Extract them and set the controlling TTY. Otherwise, connect - // std{in,out,err} directly. - if c.Terminal { - pty = os.NewFile(uintptr(5), "/proc/self/fd/5") - tty = os.NewFile(uintptr(6), "/proc/self/fd/6") - if pty == nil || tty == nil { - return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("pty and tty not found") - } - errorWriter = tty - } - - // If PAM is enabled, open a PAM context. This has to be done before anything - // else because PAM is sometimes used to create the local user used to - // launch the shell under. - var pamEnvironment []string - if c.PAM { - // Connect std{in,out,err} to the TTY if it's a shell request, otherwise - // discard std{out,err}. If this was not done, things like MOTD would be - // printed for "exec" requests. - var stdin io.Reader - var stdout io.Writer - var stderr io.Writer - if c.RequestType == sshutils.ShellRequest { - stdin = tty - stdout = tty - stderr = tty - } else { - stdin = os.Stdin - stdout = ioutil.Discard - stderr = ioutil.Discard - } - - // Set Teleport specific environment variables that PAM modules like - // pam_script.so can pick up to potentially customize the account/session. - os.Setenv("TELEPORT_USERNAME", c.Username) - os.Setenv("TELEPORT_LOGIN", c.Login) - os.Setenv("TELEPORT_ROLES", strings.Join(c.Roles, " ")) - - // Open the PAM context. - pamContext, err := pam.Open(&pam.Config{ - ServiceName: c.ServiceName, - Login: c.Login, - Stdin: stdin, - Stdout: stdout, - Stderr: stderr, - }) - if err != nil { - return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err) - } - defer pamContext.Close() - - // Save off any environment variables that come from PAM. - pamEnvironment = pamContext.Environment() - } - - // Build the actual command that will launch the shell. - cmd, err := buildCommand(&c, tty, pty, pamEnvironment) - if err != nil { - return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err) - } - - // Wait until the continue signal is received from Teleport signaling that - // the child process has been placed in a cgroup. - err = waitForContinue(contfd) - if err != nil { - return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err) - } - - // Start the command. - err = cmd.Start() - if err != nil { - return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err) - } - - // Wait for the command to exit. It doesn't make sense to print an error - // message here because the shell has successfully started. If an error - // occured during shell execution or the shell exits with an error (like - // running exit 2), the shell will print an error if appropriate and return - // an exit code. - err = cmd.Wait() - return ioutil.Discard, exitCode(err), trace.Wrap(err) -} - func (e *localExec) transformSecureCopy() error { // split up command by space to grab the first word. if we don't have anything // it's an interactive shell the user requested and not scp, return @@ -437,202 +258,6 @@ func (e *localExec) transformSecureCopy() error { return nil } -// configureCommand creates a command fully configured to execute. This -// function is used by Teleport to re-execute itself and pass whatever data -// is need to the child to actually execute the shell. -func configureCommand(ctx *ServerContext) (*exec.Cmd, error) { - // Marshal the parts needed from the *ServerContext into a *execCommand. - cmdmsg, err := ctx.ExecCommand() - if err != nil { - return nil, trace.Wrap(err) - } - cmdbytes, err := json.Marshal(cmdmsg) - if err != nil { - return nil, trace.Wrap(err) - } - - // Write command bytes to pipe. The child process will read the command - // to execute from this pipe. - _, err = io.Copy(ctx.cmdw, bytes.NewReader(cmdbytes)) - if err != nil { - return nil, trace.Wrap(err) - } - err = ctx.cmdw.Close() - if err != nil { - return nil, trace.Wrap(err) - } - // Set to nil so the close in the context doesn't attempt to re-close. - ctx.cmdw = nil - - // Find the Teleport executable and it's directory on disk. - executable, err := os.Executable() - if err != nil { - return nil, trace.Wrap(err) - } - executableDir, _ := filepath.Split(executable) - - // Build the list of arguments to have Teleport re-exec itself. The "-d" flag - // is appended if Teleport is running in debug mode. - args := []string{executable, teleport.ExecSubCommand} - - // Build the "teleport exec" command. - return &exec.Cmd{ - Path: executable, - Args: args, - Dir: executableDir, - ExtraFiles: []*os.File{ - ctx.cmdr, - ctx.contr, - }, - }, nil -} - -// buildCommand construct a command that will execute the users shell. This -// function is run by Teleport while it's re-executing. -func buildCommand(c *execCommand, tty *os.File, pty *os.File, pamEnvironment []string) (*exec.Cmd, error) { - var cmd exec.Cmd - - // Lookup the UID and GID for the user. - localUser, err := user.Lookup(c.Login) - if err != nil { - return nil, trace.Wrap(err) - } - uid, err := strconv.Atoi(localUser.Uid) - if err != nil { - return nil, trace.Wrap(err) - } - gid, err := strconv.Atoi(localUser.Gid) - if err != nil { - return nil, trace.Wrap(err) - } - - // Lookup supplementary groups for the user. - userGroups, err := localUser.GroupIds() - if err != nil { - return nil, trace.Wrap(err) - } - groups := make([]uint32, 0) - for _, sgid := range userGroups { - igid, err := strconv.Atoi(sgid) - if err != nil { - log.Warnf("Cannot interpret user group: '%v'", sgid) - } else { - groups = append(groups, uint32(igid)) - } - } - if len(groups) == 0 { - groups = append(groups, uint32(gid)) - } - - // Get the login shell for the user (or fallback to the default). - shellPath, err := shell.GetLoginShell(c.Login) - if err != nil { - log.Debugf("Failed to get login shell for %v: %v. Using default: %v.", - c.Login, err, shell.DefaultShell) - } - if c.IsTestStub { - shellPath = "/bin/sh" - } - - // If no command was given, configure a shell to run in 'login' mode. - // Otherwise, execute a command through the shell. - if c.Command == "" { - // Set the path to the path of the shell. - cmd.Path = shellPath - - // Configure the shell to run in 'login' mode. From OpenSSH source: - // "If we have no command, execute the shell. In this case, the shell - // name to be passed in argv[0] is preceded by '-' to indicate that - // this is a login shell." - // https://github.com/openssh/openssh-portable/blob/master/session.c - cmd.Args = []string{"-" + filepath.Base(shellPath)} - } else { - // Execute commands like OpenSSH does: - // https://github.com/openssh/openssh-portable/blob/master/session.c - cmd.Path = shellPath - cmd.Args = []string{shellPath, "-c", c.Command} - } - - // Create default environment for user. - cmd.Env = []string{ - "LANG=en_US.UTF-8", - getDefaultEnvPath(localUser.Uid, defaultLoginDefsPath), - "HOME=" + localUser.HomeDir, - "USER=" + c.Login, - "SHELL=" + shellPath, - } - - // Add in Teleport specific environment variables. - cmd.Env = append(cmd.Env, c.Environment...) - - // If the server allows reading in of ~/.tsh/environment read it in - // and pass environment variables along to new session. - if c.PermitUserEnvironment { - filename := filepath.Join(localUser.HomeDir, ".tsh", "environment") - userEnvs, err := utils.ReadEnvironmentFile(filename) - if err != nil { - return nil, trace.Wrap(err) - } - cmd.Env = append(cmd.Env, userEnvs...) - } - - // If any additional environment variables come from PAM, apply them as well. - cmd.Env = append(cmd.Env, pamEnvironment...) - - // Set the home directory for the user. - cmd.Dir = localUser.HomeDir - - // If a terminal was requested, connect std{in,out,err} to the TTY and set - // the controlling TTY. Otherwise, connect std{in,out,err} to - // os.Std{in,out,err}. - if c.Terminal { - cmd.Stdin = tty - cmd.Stdout = tty - cmd.Stderr = tty - - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setsid: true, - Setctty: true, - Ctty: int(tty.Fd()), - } - } else { - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setsid: true, - } - } - - // Only set process credentials if the UID/GID of the requesting user are - // different than the process (Teleport). - // - // Note, the above is important because setting the credentials struct - // triggers calling of the SETUID and SETGID syscalls during process start. - // If the caller does not have permission to call those two syscalls (for - // example, if Teleport is started from a shell), this will prevent the - // process from spawning shells with the error: "operation not permitted". To - // workaround this, the credentials struct is only set if the credentials - // are different from the process itself. If the credentials are not, simply - // pick up the ambient credentials of the process. - if strconv.Itoa(os.Getuid()) != localUser.Uid || strconv.Itoa(os.Getgid()) != localUser.Gid { - cmd.SysProcAttr.Credential = &syscall.Credential{ - Uid: uint32(uid), - Gid: uint32(gid), - Groups: groups, - } - - log.Debugf("Creating process with UID %v, GID: %v, and Groups: %v.", - uid, gid, groups) - } else { - log.Debugf("Credential process with ambient credentials UID %v, GID: %v, Groups: %v.", - uid, gid, groups) - } - - return &cmd, nil -} - // waitForContinue will wait 10 seconds for the continue signal, if not // received, it will stop waiting and exit. func waitForContinue(contfd *os.File) error { @@ -909,13 +534,3 @@ func exitCode(err error) int { return teleport.RemoteCommandFailure } } - -// errorAndExit writes the error to the io.Writer (stdout or a TTY) and -// exits with the given code. -func errorAndExit(w io.Writer, code int, err error) { - s := fmt.Sprintf("Failed to launch shell: %v.\r\n", err) - if err != nil { - io.Copy(w, bytes.NewBufferString(s)) - } - os.Exit(code) -} diff --git a/lib/srv/exec_test.go b/lib/srv/exec_test.go index 542ac1a40ad7f..674554b912a10 100644 --- a/lib/srv/exec_test.go +++ b/lib/srv/exec_test.go @@ -68,7 +68,7 @@ func TestMain(m *testing.M) { // If the test is re-executing itself, execute the command that comes over // the pipe. if len(os.Args) == 2 && os.Args[1] == teleport.ExecSubCommand { - RunAndExit() + RunAndExit(teleport.ExecSubCommand) return } @@ -276,7 +276,7 @@ func (s *ExecSuite) TestContinue(c *check.C) { } // Create an exec.Cmd to execute through Teleport. - cmd, err := configureCommand(ctx) + cmd, err := ConfigureCommand(ctx) c.Assert(err, check.IsNil) // Create a context that will be used to signal that execution is complete. diff --git a/lib/srv/forward/sshserver.go b/lib/srv/forward/sshserver.go index 0d357d0c25164..0250c9ca452a9 100644 --- a/lib/srv/forward/sshserver.go +++ b/lib/srv/forward/sshserver.go @@ -614,7 +614,7 @@ func (s *Server) handleChannel(nch ssh.NewChannel) { switch channelType { // Channels of type "session" handle requests that are involved in running // commands on a server, subsystem requests, and agent forwarding. - case "session": + case teleport.ChanSession: ch, requests, err := nch.Accept() if err != nil { s.log.Warnf("Unable to accept channel: %v", err) @@ -623,7 +623,7 @@ func (s *Server) handleChannel(nch ssh.NewChannel) { } go s.handleSessionRequests(ch, requests) // Channels of type "direct-tcpip" handles request for port forwarding. - case "direct-tcpip": + case teleport.ChanDirectTCPIP: req, err := sshutils.ParseDirectTCPIPReq(nch.ExtraData()) if err != nil { s.log.Errorf("Failed to parse request data: %v, err: %v", string(nch.ExtraData()), err) @@ -644,9 +644,6 @@ func (s *Server) handleChannel(nch ssh.NewChannel) { // handleDirectTCPIPRequest handles port forwarding requests. func (s *Server) handleDirectTCPIPRequest(ch ssh.Channel, req *sshutils.DirectTCPIPReq) { - srcAddr := fmt.Sprintf("%v:%d", req.Orig, req.OrigPort) - dstAddr := fmt.Sprintf("%v:%d", req.Host, req.Port) - // Create context for this channel. This context will be closed when // forwarding is complete. ctx, err := srv.NewServerContext(s, s.sconn, s.identityContext) @@ -657,29 +654,32 @@ func (s *Server) handleDirectTCPIPRequest(ch ssh.Channel, req *sshutils.DirectTC } ctx.Connection = s.serverConn ctx.RemoteClient = s.remoteClient + ctx.ChannelType = teleport.ChanDirectTCPIP + ctx.SrcAddr = fmt.Sprintf("%v:%d", req.Orig, req.OrigPort) + ctx.DstAddr = fmt.Sprintf("%v:%d", req.Host, req.Port) defer ctx.Close() // Check if the role allows port forwarding for this user. - err = s.authHandlers.CheckPortForward(dstAddr, ctx) + err = s.authHandlers.CheckPortForward(ctx.DstAddr, ctx) if err != nil { ch.Stderr().Write([]byte(err.Error())) return } - s.log.Debugf("Opening direct-tcpip channel from %v to %v in context %v.", srcAddr, dstAddr, ctx.ID()) - defer s.log.Debugf("Completing direct-tcpip request from %v to %v in context %v.", srcAddr, dstAddr, ctx.ID()) + s.log.Debugf("Opening direct-tcpip channel from %v to %v in context %v.", ctx.SrcAddr, ctx.DstAddr, ctx.ID()) + defer s.log.Debugf("Completing direct-tcpip request from %v to %v in context %v.", ctx.SrcAddr, ctx.DstAddr, ctx.ID()) // Create "direct-tcpip" channel from the remote host to the target host. - conn, err := s.remoteClient.Dial("tcp", dstAddr) + conn, err := s.remoteClient.Dial("tcp", ctx.DstAddr) if err != nil { - ctx.Infof("Failed to connect to: %v: %v", dstAddr, err) + ctx.Infof("Failed to connect to: %v: %v", ctx.DstAddr, err) return } defer conn.Close() // Emit a port forwarding audit event. s.EmitAuditEvent(events.PortForward, events.EventFields{ - events.PortForwardAddr: dstAddr, + events.PortForwardAddr: ctx.DstAddr, events.PortForwardSuccess: true, events.EventLogin: s.identityContext.Login, events.EventUser: s.identityContext.TeleportUser, @@ -722,6 +722,7 @@ func (s *Server) handleSessionRequests(ch ssh.Channel, in <-chan *ssh.Request) { ctx.Connection = s.serverConn ctx.RemoteClient = s.remoteClient ctx.AddCloser(ch) + ctx.ChannelType = teleport.ChanSession defer ctx.Close() // Create a "session" channel on the remote host. diff --git a/lib/srv/reexec.go b/lib/srv/reexec.go new file mode 100644 index 0000000000000..c4e3babd98c47 --- /dev/null +++ b/lib/srv/reexec.go @@ -0,0 +1,522 @@ +/* +Copyright 2020 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package srv + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net" + "os" + "os/exec" + "os/user" + "path/filepath" + "strconv" + "strings" + "syscall" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport" + "github.com/gravitational/teleport/lib/pam" + "github.com/gravitational/teleport/lib/shell" + "github.com/gravitational/teleport/lib/sshutils" + "github.com/gravitational/teleport/lib/utils" + + log "github.com/sirupsen/logrus" +) + +// execCommand contains the payload to "teleport exec" which will be used to +// construct and execute a shell. +type execCommand struct { + // Command is the command to execute. If an interactive session is being + // requested, will be empty. + Command string `json:"command"` + + // DestinationAddress is the target address to dial to. + DestinationAddress string `json:"dst_addr"` + + // Username is the username associated with the Teleport identity. + Username string `json:"username"` + + // Login is the local *nix account. + Login string `json:"login"` + + // Roles is the list of Teleport roles assigned to the Teleport identity. + Roles []string `json:"roles"` + + // ClusterName is the name of the Teleport cluster. + ClusterName string `json:"cluster_name"` + + // Terminal indicates if a TTY has been allocated for the session. This is + // typically set if either an shell was requested or a TTY was explicitly + // allocated for a exec request. + Terminal bool `json:"term"` + + // RequestType is the type of request: either "exec" or "shell". This will + // be used to control where to connect std{out,err} based on the request + // type: "exec" or "shell". + RequestType string `json:"request_type"` + + // PAM indicates if PAM support was requested by the node. + PAM bool `json:"pam"` + + // ServiceName is the name of the PAM service requested if PAM is enabled. + ServiceName string `json:"service_name"` + + // Environment is a list of environment variables to add to the defaults. + Environment []string `json:"environment"` + + // PermitUserEnvironment is set to allow reading in ~/.tsh/environment + // upon login. + PermitUserEnvironment bool `json:"permit_user_environment"` + + // IsTestStub is used by tests to mock the shell. + IsTestStub bool `json:"is_test_stub"` +} + +// RunCommand reads in the command to run from the parent process (over a +// pipe) then constructs and runs the command. +func RunCommand() (io.Writer, int, error) { + // errorWriter is used to return any error message back to the client. By + // default it writes to stdout, but if a TTY is allocated, it will write + // to it instead. + errorWriter := os.Stdout + + // Parent sends the command payload in the third file descriptor. + cmdfd := os.NewFile(uintptr(3), "/proc/self/fd/3") + if cmdfd == nil { + return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("command pipe not found") + } + contfd := os.NewFile(uintptr(4), "/proc/self/fd/4") + if contfd == nil { + return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("continue pipe not found") + } + + // Read in the command payload. + var b bytes.Buffer + _, err := b.ReadFrom(cmdfd) + if err != nil { + return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err) + } + var c execCommand + err = json.Unmarshal(b.Bytes(), &c) + if err != nil { + return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err) + } + + var tty *os.File + var pty *os.File + + // If a terminal was requested, file descriptor 4 and 5 always point to the + // PTY and TTY. Extract them and set the controlling TTY. Otherwise, connect + // std{in,out,err} directly. + if c.Terminal { + pty = os.NewFile(uintptr(5), "/proc/self/fd/5") + tty = os.NewFile(uintptr(6), "/proc/self/fd/6") + if pty == nil || tty == nil { + return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("pty and tty not found") + } + errorWriter = tty + } + + // If PAM is enabled, open a PAM context. This has to be done before anything + // else because PAM is sometimes used to create the local user used to + // launch the shell under. + var pamEnvironment []string + if c.PAM { + // Connect std{in,out,err} to the TTY if it's a shell request, otherwise + // discard std{out,err}. If this was not done, things like MOTD would be + // printed for "exec" requests. + var stdin io.Reader + var stdout io.Writer + var stderr io.Writer + if c.RequestType == sshutils.ShellRequest { + stdin = tty + stdout = tty + stderr = tty + } else { + stdin = os.Stdin + stdout = ioutil.Discard + stderr = ioutil.Discard + } + + // Set Teleport specific environment variables that PAM modules like + // pam_script.so can pick up to potentially customize the account/session. + os.Setenv("TELEPORT_USERNAME", c.Username) + os.Setenv("TELEPORT_LOGIN", c.Login) + os.Setenv("TELEPORT_ROLES", strings.Join(c.Roles, " ")) + + // Open the PAM context. + pamContext, err := pam.Open(&pam.Config{ + ServiceName: c.ServiceName, + Login: c.Login, + Stdin: stdin, + Stdout: stdout, + Stderr: stderr, + }) + if err != nil { + return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err) + } + defer pamContext.Close() + + // Save off any environment variables that come from PAM. + pamEnvironment = pamContext.Environment() + } + + // Build the actual command that will launch the shell. + cmd, err := buildCommand(&c, tty, pty, pamEnvironment) + if err != nil { + return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err) + } + + // Wait until the continue signal is received from Teleport signaling that + // the child process has been placed in a cgroup. + err = waitForContinue(contfd) + if err != nil { + return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err) + } + + // Start the command. + err = cmd.Start() + if err != nil { + return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err) + } + + // Wait for the command to exit. It doesn't make sense to print an error + // message here because the shell has successfully started. If an error + // occured during shell execution or the shell exits with an error (like + // running exit 2), the shell will print an error if appropriate and return + // an exit code. + err = cmd.Wait() + return ioutil.Discard, exitCode(err), trace.Wrap(err) +} + +// RunForward reads in the command to run from the parent process (over a +// pipe) then port forwards. +func RunForward() (io.Writer, int, error) { + // errorWriter is used to return any error message back to the client. + errorWriter := os.Stdout + + // Parent sends the command payload in the third file descriptor. + cmdfd := os.NewFile(uintptr(3), "/proc/self/fd/3") + if cmdfd == nil { + return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("command pipe not found") + } + + // Read in the command payload. + var b bytes.Buffer + _, err := b.ReadFrom(cmdfd) + if err != nil { + return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err) + } + var c execCommand + err = json.Unmarshal(b.Bytes(), &c) + if err != nil { + return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err) + } + + // If PAM is enabled, open a PAM context. This has to be done before anything + // else because PAM is sometimes used to create the local user used to + // launch the shell under. + if c.PAM { + // Set Teleport specific environment variables that PAM modules like + // pam_script.so can pick up to potentially customize the account/session. + os.Setenv("TELEPORT_USERNAME", c.Username) + os.Setenv("TELEPORT_LOGIN", c.Login) + os.Setenv("TELEPORT_ROLES", strings.Join(c.Roles, " ")) + + // Open the PAM context. + pamContext, err := pam.Open(&pam.Config{ + ServiceName: c.ServiceName, + Login: c.Login, + Stdin: os.Stdin, + Stdout: ioutil.Discard, + Stderr: ioutil.Discard, + }) + if err != nil { + return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err) + } + defer pamContext.Close() + } + + // Connect to the target host. + conn, err := net.Dial("tcp", c.DestinationAddress) + if err != nil { + return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err) + } + defer conn.Close() + + // Start copy routines that copy from channel to stdin pipe and from stdout + // pipe to channel. + errorCh := make(chan error, 2) + go func() { + defer os.Stdout.Close() + defer os.Stdin.Close() + + _, err := io.Copy(os.Stdout, conn) + errorCh <- err + }() + go func() { + defer os.Stdout.Close() + defer os.Stdin.Close() + + _, err := io.Copy(conn, os.Stdin) + errorCh <- err + }() + + // Block until copy is complete and the child process is done executing. + var errs []error + for i := 0; i < 2; i++ { + select { + case err := <-errorCh: + if err != nil && err != io.EOF { + errs = append(errs, err) + } + } + } + + return ioutil.Discard, teleport.RemoteCommandSuccess, trace.NewAggregate(errs...) +} + +// RunAndExit will run the requested command and then exit. This wrapper +// allows Run{Command,Forward} to use defers and makes sure error messages +// are consistent across both. +func RunAndExit(commandType string) { + var w io.Writer + var code int + var err error + + switch commandType { + case teleport.ExecSubCommand: + w, code, err = RunCommand() + case teleport.ForwardSubCommand: + w, code, err = RunForward() + default: + w, code, err = os.Stderr, teleport.RemoteCommandFailure, fmt.Errorf("unknown command type: %v", commandType) + } + if err != nil { + s := fmt.Sprintf("Failed to launch: %v.\r\n", err) + io.Copy(w, bytes.NewBufferString(s)) + } + os.Exit(code) +} + +// buildCommand constructs a command that will execute the users shell. This +// function is run by Teleport while it's re-executing. +func buildCommand(c *execCommand, tty *os.File, pty *os.File, pamEnvironment []string) (*exec.Cmd, error) { + var cmd exec.Cmd + + // Lookup the UID and GID for the user. + localUser, err := user.Lookup(c.Login) + if err != nil { + return nil, trace.Wrap(err) + } + uid, err := strconv.Atoi(localUser.Uid) + if err != nil { + return nil, trace.Wrap(err) + } + gid, err := strconv.Atoi(localUser.Gid) + if err != nil { + return nil, trace.Wrap(err) + } + + // Lookup supplementary groups for the user. + userGroups, err := localUser.GroupIds() + if err != nil { + return nil, trace.Wrap(err) + } + groups := make([]uint32, 0) + for _, sgid := range userGroups { + igid, err := strconv.Atoi(sgid) + if err != nil { + log.Warnf("Cannot interpret user group: '%v'", sgid) + } else { + groups = append(groups, uint32(igid)) + } + } + if len(groups) == 0 { + groups = append(groups, uint32(gid)) + } + + // Get the login shell for the user (or fallback to the default). + shellPath, err := shell.GetLoginShell(c.Login) + if err != nil { + log.Debugf("Failed to get login shell for %v: %v. Using default: %v.", + c.Login, err, shell.DefaultShell) + } + if c.IsTestStub { + shellPath = "/bin/sh" + } + + // If no command was given, configure a shell to run in 'login' mode. + // Otherwise, execute a command through the shell. + if c.Command == "" { + // Set the path to the path of the shell. + cmd.Path = shellPath + + // Configure the shell to run in 'login' mode. From OpenSSH source: + // "If we have no command, execute the shell. In this case, the shell + // name to be passed in argv[0] is preceded by '-' to indicate that + // this is a login shell." + // https://github.com/openssh/openssh-portable/blob/master/session.c + cmd.Args = []string{"-" + filepath.Base(shellPath)} + } else { + // Execute commands like OpenSSH does: + // https://github.com/openssh/openssh-portable/blob/master/session.c + cmd.Path = shellPath + cmd.Args = []string{shellPath, "-c", c.Command} + } + + // Create default environment for user. + cmd.Env = []string{ + "LANG=en_US.UTF-8", + getDefaultEnvPath(localUser.Uid, defaultLoginDefsPath), + "HOME=" + localUser.HomeDir, + "USER=" + c.Login, + "SHELL=" + shellPath, + } + + // Add in Teleport specific environment variables. + cmd.Env = append(cmd.Env, c.Environment...) + + // If the server allows reading in of ~/.tsh/environment read it in + // and pass environment variables along to new session. + if c.PermitUserEnvironment { + filename := filepath.Join(localUser.HomeDir, ".tsh", "environment") + userEnvs, err := utils.ReadEnvironmentFile(filename) + if err != nil { + return nil, trace.Wrap(err) + } + cmd.Env = append(cmd.Env, userEnvs...) + } + + // If any additional environment variables come from PAM, apply them as well. + cmd.Env = append(cmd.Env, pamEnvironment...) + + // Set the home directory for the user. + cmd.Dir = localUser.HomeDir + + // If a terminal was requested, connect std{in,out,err} to the TTY and set + // the controlling TTY. Otherwise, connect std{in,out,err} to + // os.Std{in,out,err}. + if c.Terminal { + cmd.Stdin = tty + cmd.Stdout = tty + cmd.Stderr = tty + + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, + Setctty: true, + Ctty: int(tty.Fd()), + } + } else { + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, + } + } + + // Only set process credentials if the UID/GID of the requesting user are + // different than the process (Teleport). + // + // Note, the above is important because setting the credentials struct + // triggers calling of the SETUID and SETGID syscalls during process start. + // If the caller does not have permission to call those two syscalls (for + // example, if Teleport is started from a shell), this will prevent the + // process from spawning shells with the error: "operation not permitted". To + // workaround this, the credentials struct is only set if the credentials + // are different from the process itself. If the credentials are not, simply + // pick up the ambient credentials of the process. + if strconv.Itoa(os.Getuid()) != localUser.Uid || strconv.Itoa(os.Getgid()) != localUser.Gid { + cmd.SysProcAttr.Credential = &syscall.Credential{ + Uid: uint32(uid), + Gid: uint32(gid), + Groups: groups, + } + + log.Debugf("Creating process with UID %v, GID: %v, and Groups: %v.", + uid, gid, groups) + } else { + log.Debugf("Creating process with ambient credentials UID %v, GID: %v, Groups: %v.", + uid, gid, groups) + } + + return &cmd, nil +} + +// ConfigureCommand creates a command fully configured to execute. This +// function is used by Teleport to re-execute itself and pass whatever data +// is need to the child to actually execute the shell. +func ConfigureCommand(ctx *ServerContext) (*exec.Cmd, error) { + // Marshal the parts needed from the *ServerContext into an *execCommand. + cmdmsg, err := ctx.ExecCommand() + if err != nil { + return nil, trace.Wrap(err) + } + cmdbytes, err := json.Marshal(cmdmsg) + if err != nil { + return nil, trace.Wrap(err) + } + + // Write command bytes to pipe. The child process will read the command + // to execute from this pipe. + _, err = io.Copy(ctx.cmdw, bytes.NewReader(cmdbytes)) + if err != nil { + return nil, trace.Wrap(err) + } + err = ctx.cmdw.Close() + if err != nil { + return nil, trace.Wrap(err) + } + // Set to nil so the close in the context doesn't attempt to re-close. + ctx.cmdw = nil + + // Find the Teleport executable and its directory on disk. + executable, err := os.Executable() + if err != nil { + return nil, trace.Wrap(err) + } + executableDir, _ := filepath.Split(executable) + + // The channel type determines the subcommand to execute (execution or + // port forwarding). + subCommand := teleport.ExecSubCommand + if ctx.ChannelType == teleport.ChanDirectTCPIP { + subCommand = teleport.ForwardSubCommand + } + + // Build the list of arguments to have Teleport re-exec itself. The "-d" flag + // is appended if Teleport is running in debug mode. + args := []string{executable, subCommand} + + // Build the "teleport exec" command. + return &exec.Cmd{ + Path: executable, + Args: args, + Dir: executableDir, + ExtraFiles: []*os.File{ + ctx.cmdr, + ctx.contr, + }, + }, nil +} diff --git a/lib/srv/regular/sshserver.go b/lib/srv/regular/sshserver.go index d43c5a4523c95..ebcf4661fad1e 100644 --- a/lib/srv/regular/sshserver.go +++ b/lib/srv/regular/sshserver.go @@ -815,13 +815,6 @@ func (s *Server) HandleRequest(r *ssh.Request) { } } -const ( - // ChanDirectTCPIP is a direct tcp ip channel - ChanDirectTCPIP = "direct-tcpip" - // ChanSession is a SSH session channel - ChanSession = "session" -) - // HandleNewChan is called when new channel is opened func (s *Server) HandleNewChan(wconn net.Conn, sconn *ssh.ServerConn, nch ssh.NewChannel) { identityContext, err := s.authHandlers.CreateIdentityContext(sconn) @@ -835,7 +828,7 @@ func (s *Server) HandleNewChan(wconn net.Conn, sconn *ssh.ServerConn, nch ssh.Ne switch channelType { // Channels of type "direct-tcpip", for proxies, it's equivalent // of teleport proxy: subsystem - case ChanDirectTCPIP: + case teleport.ChanDirectTCPIP: req, err := sshutils.ParseDirectTCPIPReq(nch.ExtraData()) if err != nil { log.Errorf("Failed to parse request data: %v, err: %v.", string(nch.ExtraData()), err) @@ -853,7 +846,7 @@ func (s *Server) HandleNewChan(wconn net.Conn, sconn *ssh.ServerConn, nch ssh.Ne // Channels of type "session" handle requests that are involved in running // commands on a server. In the case of proxy mode subsystem and agent // forwarding requests occur over the "session" channel. - case ChanSession: + case teleport.ChanSession: ch, requests, err := nch.Accept() if err != nil { log.Warnf("Unable to accept channel: %v.", err) @@ -871,7 +864,7 @@ func (s *Server) HandleNewChan(wconn net.Conn, sconn *ssh.ServerConn, nch ssh.Ne switch channelType { // Channels of type "session" handle requests that are involved in running // commands on a server, subsystem requests, and agent forwarding. - case ChanSession: + case teleport.ChanSession: ch, requests, err := nch.Accept() if err != nil { log.Warnf("Unable to accept channel: %v.", err) @@ -880,7 +873,7 @@ func (s *Server) HandleNewChan(wconn net.Conn, sconn *ssh.ServerConn, nch ssh.Ne } go s.handleSessionRequests(wconn, sconn, identityContext, ch, requests) // Channels of type "direct-tcpip" handles request for port forwarding. - case ChanDirectTCPIP: + case teleport.ChanDirectTCPIP: req, err := sshutils.ParseDirectTCPIPReq(nch.ExtraData()) if err != nil { log.Errorf("Failed to parse request data: %v, err: %v.", string(nch.ExtraData()), err) @@ -900,94 +893,104 @@ func (s *Server) HandleNewChan(wconn net.Conn, sconn *ssh.ServerConn, nch ssh.Ne } // handleDirectTCPIPRequest handles port forwarding requests. -func (s *Server) handleDirectTCPIPRequest(wconn net.Conn, sconn *ssh.ServerConn, identityContext srv.IdentityContext, ch ssh.Channel, req *sshutils.DirectTCPIPReq) { +func (s *Server) handleDirectTCPIPRequest(wconn net.Conn, sconn *ssh.ServerConn, identityContext srv.IdentityContext, channel ssh.Channel, req *sshutils.DirectTCPIPReq) { // Create context for this channel. This context will be closed when // forwarding is complete. ctx, err := srv.NewServerContext(s, sconn, identityContext) if err != nil { - ctx.Errorf("Unable to create connection context: %v.", err) - ch.Stderr().Write([]byte("Unable to create connection context.")) + log.Errorf("Unable to create connection context: %v.", err) + channel.Stderr().Write([]byte("Unable to create connection context.")) return } ctx.Connection = wconn ctx.IsTestStub = s.isTestStub - ctx.AddCloser(ch) - defer ctx.Debugf("direct-tcp closed") + ctx.AddCloser(channel) + ctx.ChannelType = teleport.ChanDirectTCPIP + ctx.SrcAddr = net.JoinHostPort(req.Orig, strconv.Itoa(int(req.OrigPort))) + ctx.DstAddr = net.JoinHostPort(req.Host, strconv.Itoa(int(req.Port))) defer ctx.Close() - srcAddr := net.JoinHostPort(req.Orig, strconv.Itoa(int(req.OrigPort))) - dstAddr := net.JoinHostPort(req.Host, strconv.Itoa(int(req.Port))) - - // check if the role allows port forwarding for this user - err = s.authHandlers.CheckPortForward(dstAddr, ctx) + // Check if the role allows port forwarding for this user. + err = s.authHandlers.CheckPortForward(ctx.DstAddr, ctx) if err != nil { - ch.Stderr().Write([]byte(err.Error())) + channel.Stderr().Write([]byte(err.Error())) return } - ctx.Debugf("Opening direct-tcpip channel from %v to %v", srcAddr, dstAddr) - - // If PAM is enabled check the account and open a session. - var pamContext *pam.PAM - if s.pamConfig != nil && s.pamConfig.Enabled { - // Note, stdout/stderr is discarded here, otherwise MOTD would be printed to - // the users screen during port forwarding. - pamContext, err = pam.Open(&pam.Config{ - ServiceName: s.pamConfig.ServiceName, - Login: ctx.Identity.Login, - Stdin: ch, - Stderr: ioutil.Discard, - Stdout: ioutil.Discard, - }) - if err != nil { - ctx.Errorf("Unable to open PAM context for direct-tcpip request: %v.", err) - ch.Stderr().Write([]byte(err.Error())) - return - } + ctx.Debugf("Opening direct-tcpip channel from %v to %v.", ctx.SrcAddr, ctx.DstAddr) + defer ctx.Debugf("Closing direct-tcpip channel from %v to %v.", ctx.SrcAddr, ctx.DstAddr) + + // Create command to re-exec Teleport which will perform a net.Dial. The + // reason it's not done directly is because the PAM stack needs to be called + // from another process. + cmd, err := srv.ConfigureCommand(ctx) + if err != nil { + channel.Stderr().Write([]byte(err.Error())) + } - ctx.Debugf("Opening PAM context for direct-tcpip request.") + // Create a pipe for std{in,out} that will be used to transfer data between + // parent and child. + pr, err := cmd.StdoutPipe() + if err != nil { + log.Fatal(err) + } + pw, err := cmd.StdinPipe() + if err != nil { + log.Fatal(err) } - conn, err := net.Dial("tcp", dstAddr) + // Start the child process that will be used to make the actual connection + // to the target host. + err = cmd.Start() if err != nil { - ctx.Infof("Failed to connect to: %v: %v", dstAddr, err) + channel.Stderr().Write([]byte(err.Error())) return } - defer conn.Close() - // audit event: - s.EmitAuditEvent(events.PortForward, events.EventFields{ - events.PortForwardAddr: dstAddr, - events.PortForwardSuccess: true, - events.EventLogin: ctx.Identity.Login, - events.EventUser: ctx.Identity.TeleportUser, - events.LocalAddr: sconn.LocalAddr().String(), - events.RemoteAddr: sconn.RemoteAddr().String(), - }) - wg := &sync.WaitGroup{} - wg.Add(1) + // Start copy routines that copy from channel to stdin pipe and from stdout + // pipe to channel. + errorCh := make(chan error, 2) go func() { - defer wg.Done() - io.Copy(ch, conn) - ch.Close() + defer pw.Close() + defer pr.Close() + + _, err := io.Copy(pw, channel) + errorCh <- err }() - wg.Add(1) go func() { - defer wg.Done() - io.Copy(conn, srv.NewTrackingReader(ctx, ch)) - conn.Close() + defer pw.Close() + defer pr.Close() + + _, err := io.Copy(channel, pr) + errorCh <- err }() - wg.Wait() - // If PAM is enabled, close the PAM context after port forwarding is complete. - if s.pamConfig != nil && s.pamConfig.Enabled { - err = pamContext.Close() - if err != nil { - ctx.Errorf("Unable to close PAM context for direct-tcpip request: %v.", err) - return + // Block until copy is complete and the child process is done executing. + for i := 0; i < 2; i++ { + select { + case err := <-errorCh: + if err != nil && err != io.EOF { + log.Warnf("Connection problem in \"direct-tcpip\" channel: %v %T.", trace.DebugReport(err), err) + } + case <-s.ctx.Done(): + break } - ctx.Debugf("Closing PAM context for direct-tcpip request.") } + err = cmd.Wait() + if err != nil { + channel.Stderr().Write([]byte(err.Error())) + return + } + + // Emit a port forwarding event. + s.EmitAuditEvent(events.PortForward, events.EventFields{ + events.PortForwardAddr: ctx.DstAddr, + events.PortForwardSuccess: true, + events.EventLogin: ctx.Identity.Login, + events.EventUser: ctx.Identity.TeleportUser, + events.LocalAddr: sconn.LocalAddr().String(), + events.RemoteAddr: sconn.RemoteAddr().String(), + }) } // handleSessionRequests handles out of band session requests once the session @@ -1005,6 +1008,7 @@ func (s *Server) handleSessionRequests(conn net.Conn, sconn *ssh.ServerConn, ide ctx.Connection = conn ctx.IsTestStub = s.isTestStub ctx.AddCloser(ch) + ctx.ChannelType = teleport.ChanSession defer ctx.Close() // Create a close context used to signal between the server and the diff --git a/lib/srv/regular/sshserver_test.go b/lib/srv/regular/sshserver_test.go index c5b053503db1f..172bd20ce53cd 100644 --- a/lib/srv/regular/sshserver_test.go +++ b/lib/srv/regular/sshserver_test.go @@ -24,6 +24,9 @@ import ( "io" "io/ioutil" "net" + "net/http" + "net/http/httptest" + "net/url" "os" "os/user" "strconv" @@ -86,8 +89,9 @@ var _ = Suite(&SrvSuite{}) // TestMain will re-execute Teleport to run a command if "exec" is passed to // it as an argument. Otherwise it will run tests as normal. func TestMain(m *testing.M) { - if len(os.Args) == 2 && os.Args[1] == teleport.ExecSubCommand { - srv.RunAndExit() + if len(os.Args) == 2 && + (os.Args[1] == teleport.ExecSubCommand || os.Args[1] == teleport.ForwardSubCommand) { + srv.RunAndExit(os.Args[1]) return } @@ -214,6 +218,42 @@ func (s *SrvSuite) TearDownTest(c *C) { } } +// TestDirectTCPIP ensures that the server can create a "direct-tcpip" +// channel to the target address. The "direct-tcpip" channel is what port +// forwarding is built upon. +func (s *SrvSuite) TestDirectTCPIP(c *C) { + // Startup a test server that will reply with "hello, world\n" + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "hello, world") + })) + defer ts.Close() + + // Extract the host:port the test HTTP server is running on. + u, err := url.Parse(ts.URL) + c.Assert(err, IsNil) + + // Build a http.Client that will dial through the server to establish the + // connection. That's why a custom dialer is used and the dialer uses + // s.clt.Dial (which performs the "direct-tcpip" request). + httpClient := http.Client{ + Transport: &http.Transport{ + Dial: func(network string, addr string) (net.Conn, error) { + return s.clt.Dial("tcp", u.Host) + }, + }, + } + + // Perform a HTTP GET to the test HTTP server through a "direct-tcpip" request. + resp, err := httpClient.Get(ts.URL) + c.Assert(err, IsNil) + defer resp.Body.Close() + + // Make sure the response is what was expected. + body, err := ioutil.ReadAll(resp.Body) + c.Assert(err, IsNil) + c.Assert(body, DeepEquals, []byte("hello, world\n")) +} + func (s *SrvSuite) TestAdvertiseAddr(c *C) { c.Assert(strings.Index(s.srv.AdvertiseAddr(), "127.0.0.1:"), Equals, 0) s.srv.setAdvertiseIP("10.10.10.1") diff --git a/lib/srv/term.go b/lib/srv/term.go index 689e808442612..78a2334531176 100644 --- a/lib/srv/term.go +++ b/lib/srv/term.go @@ -173,7 +173,7 @@ func (t *terminal) Run() error { defer t.closeTTY() // Create the command that will actually execute. - t.cmd, err = configureCommand(t.ctx) + t.cmd, err = ConfigureCommand(t.ctx) if err != nil { return trace.Wrap(err) } diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index 70743210123d3..ff136599c5ebc 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -110,8 +110,9 @@ var _ = Suite(&WebSuite{ func TestMain(m *testing.M) { // If the test is re-executing itself, execute the command that comes over // the pipe. - if len(os.Args) == 2 && os.Args[1] == teleport.ExecSubCommand { - srv.RunAndExit() + if len(os.Args) == 2 && + (os.Args[1] == teleport.ExecSubCommand || os.Args[1] == teleport.ForwardSubCommand) { + srv.RunAndExit(os.Args[1]) return } diff --git a/tool/teleport/common/teleport.go b/tool/teleport/common/teleport.go index 48491e86ade0f..9dea02c72994c 100644 --- a/tool/teleport/common/teleport.go +++ b/tool/teleport/common/teleport.go @@ -64,7 +64,6 @@ func Run(options Options) (executedCommand string, conf *service.Config) { // define global flags: var ccf config.CommandLineFlags var scpFlags scp.Flags - var execDebug bool // define commands: start := app.Command("start", "Starts the Teleport service.") @@ -72,7 +71,8 @@ func Run(options Options) (executedCommand string, conf *service.Config) { dump := app.Command("configure", "Print the sample config file into stdout.") ver := app.Command("version", "Print the version.") scpc := app.Command("scp", "Server-side implementation of SCP.").Hidden() - exec := app.Command("exec", "Used internally by Teleport to re-exec itself.").Hidden() + exec := app.Command("exec", "Used internally by Teleport to re-exec itself to run a command.").Hidden() + forward := app.Command("forward", "Used internally by Teleport to re-exec itself to port forward.").Hidden() app.HelpFlag.Short('h') // define start flags: @@ -140,9 +140,6 @@ func Run(options Options) (executedCommand string, conf *service.Config) { scpc.Flag("local-addr", "local address which accepted the request").StringVar(&scpFlags.LocalAddr) scpc.Arg("target", "").StringsVar(&scpFlags.Target) - // Define flags for the "exec" subcommand. - exec.Flag("debug", "Debug mode").Short('d').Default("false").BoolVar(&execDebug) - // parse CLI commands+flags: command, err := app.Parse(options.Args) if err != nil { @@ -174,7 +171,9 @@ func Run(options Options) (executedCommand string, conf *service.Config) { case dump.FullCommand(): onConfigDump() case exec.FullCommand(): - err = onExec(execDebug) + err = onExec() + case forward.FullCommand(): + err = onForward() case ver.FullCommand(): utils.PrintVersion() } @@ -266,9 +265,17 @@ func onSCP(scpFlags *scp.Flags) (err error) { return trace.Wrap(cmd.Execute(&StdReadWriter{})) } -// onExec will re-execute Teleport. -func onExec(debug bool) error { - srv.RunAndExit() +// onExec is a subcommand used to re-execute Teleport for execution. Used for +// "exec" or "shell" requests over a "session" channel on Teleport nodes. +func onExec() error { + srv.RunAndExit(teleport.ExecSubCommand) + return nil +} + +// onForward is a subcommand used to re-execute Teleport for port forwarding. +// Used with "direct-tcpip" channel on Teleport nodes. +func onForward() error { + srv.RunAndExit(teleport.ForwardSubCommand) return nil }