Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport #46879 to branch/v15 #48019

Merged
merged 1 commit into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions lib/client/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -555,13 +555,6 @@ func (ns *NodeSession) runCommand(ctx context.Context, mode types.SessionPartici
)
defer span.End()

// If stdin is not a terminal, refuse to allocate terminal on the server and
// fallback to non-interactive mode
if interactive && !ns.terminal.IsAttached() {
interactive = false
fmt.Fprintf(ns.nodeClient.TC.Stderr, "TTY will not be allocated on the server because stdin is not a terminal\n")
}

// Start a interactive session ("exec" request with a TTY).
//
// Note that because a TTY was allocated, the terminal is in raw mode and any
Expand Down
47 changes: 45 additions & 2 deletions tool/tsh/common/tsh.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import (
"github.com/alecthomas/kingpin/v2"
"github.com/dustin/go-humanize"
"github.com/ghodss/yaml"
"github.com/google/uuid"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -241,8 +242,17 @@ type CLIConf struct {
DatabaseRoles string
// AppName specifies proxied application name.
AppName string
// Interactive, when set to true, launches remote command with the terminal attached
// Interactive sessions will allocate a PTY and create interactive "shell"
// sessions.
Interactive bool
// NonInteractive sessions will not allocate a PTY and create
// non-interactive "exec" sessions. This variable is needed due to
// limitations in kingpin (lack of an inverse short flag) which forces
// the registration of both flags.
NonInteractive bool
// ShowVersion is an OpenSSH compatibility flag that prints out the version
// of tsh. Useful for users that alias ssh to "tsh ssh".
ShowVersion bool
// Quiet mode, -q command (disables progress printing)
Quiet bool
// Namespace is used to select cluster namespace
Expand Down Expand Up @@ -774,7 +784,7 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
// ssh
// Use Interspersed(false) to forward all flags to ssh.
ssh := app.Command("ssh", "Run shell or execute a command on a remote SSH node.").Interspersed(false)
ssh.Arg("[user@]host", "Remote hostname and the login to use").Required().StringVar(&cf.UserHost)
ssh.Arg("[user@]host", "Remote hostname and the login to use, this argument is required").StringVar(&cf.UserHost)
ssh.Arg("command", "Command to execute on a remote host").StringsVar(&cf.RemoteCommand)
app.Flag("jumphost", "SSH jumphost").Short('J').StringVar(&cf.ProxyJump)
ssh.Flag("port", "SSH port on a remote host").Short('p').Int32Var(&cf.NodePort)
Expand All @@ -799,6 +809,14 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
ssh.Flag("log-dir", "Directory to log separated command output, when executing on multiple nodes. If set, output from each node will also be labeled in the terminal.").StringVar(&cf.SSHLogDir)
ssh.Flag("no-resume", "Disable SSH connection resumption").Envar(noResumeEnvVar).BoolVar(&cf.DisableSSHResumption)
ssh.Flag("relogin", "Permit performing an authentication attempt on a failed command").Default("true").BoolVar(&cf.Relogin)
// The following flags are OpenSSH compatibility flags. They are used for
// users that alias "ssh" to "tsh ssh." The following OpenSSH flags are
// implemented. From "man 1 ssh":
//
// * "-V Display the version number and exit."
// * "-T Disable pseudo-terminal allocation."
ssh.Flag(uuid.New().String(), "").Short('T').Hidden().BoolVar(&cf.NonInteractive)
ssh.Flag(uuid.New().String(), "").Short('V').Hidden().BoolVar(&cf.ShowVersion)

resolve := app.Command("resolve", "Resolves an SSH host.")
resolve.Arg("host", "Remote hostname to resolve").Required().StringVar(&cf.UserHost)
Expand Down Expand Up @@ -3655,6 +3673,31 @@ func onResolve(cf *CLIConf) error {

// onSSH executes 'tsh ssh' command
func onSSH(cf *CLIConf) error {
// If "tsh ssh -V" is invoked, tsh is in OpenSSH compatibility mode, show
// the version and exit.
if cf.ShowVersion {
modules.GetModules().PrintVersion()
return nil
}

// If "tsh ssh" is invoked with the "-t" or "-T" flag, manually validate
// "-t" and "-T" flags for "tsh ssh" due to the lack of inverse short flags
// in kingpin.
if cf.Interactive && cf.NonInteractive {
return trace.BadParameter("either -t or -T can be specified, not both")
}
if cf.NonInteractive {
cf.Interactive = false
}

// If "tsh ssh" is invoked the user must specify some host to connect to.
// In the past, this was handled by making "UserHost" required in kingpin.
// However, to support "tsh ssh -V" this was no longer possible. This
// property is how enforced in this function.
if cf.UserHost == "" {
return trace.BadParameter("required argument '[user@]host' not provided")
}

tc, err := makeClient(cf)
if err != nil {
return trace.Wrap(err)
Expand Down
114 changes: 114 additions & 0 deletions tool/tsh/common/tsh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6047,3 +6047,117 @@ func TestResolve(t *testing.T) {
})
}
}

// TestInteractiveCompatibilityFlags verifies that "tsh ssh -t" and "tsh ssh -T"
// behave similar to OpenSSH.
func TestInteractiveCompatibilityFlags(t *testing.T) {
// Require the "tty" program exist somewhere in $PATH, otherwise fail.
tty, err := exec.LookPath("tty")
require.NoError(t, err)

// Create roles and users that will be used in test.
local, err := user.Current()
require.NoError(t, err)
nodeAccess, err := types.NewRole("foo", types.RoleSpecV6{
Allow: types.RoleConditions{
Logins: []string{local.Username},
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
},
})
require.NoError(t, err)
user, err := types.NewUser("[email protected]")
require.NoError(t, err)
user.SetRoles([]string{"access", "foo"})

// Create Auth, Proxy, and Node Service used in tests.
hostname := uuid.NewString()
connector := mockConnector(t)
authProcess, proxyProcess := makeTestServers(t,
withBootstrap(connector, nodeAccess, user),
withConfig(func(cfg *servicecfg.Config) {
cfg.Hostname = hostname
cfg.SSH.Enabled = true
cfg.SSH.Addr = utils.NetAddr{
AddrNetwork: "tcp",
Addr: net.JoinHostPort("127.0.0.1", ports.Pop()),
}
}))

// Extract Auth Service and Proxy Service address.
authServer := authProcess.GetAuthServer()
require.NotNil(t, authServer)
proxyAddr, err := proxyProcess.ProxyWebAddr()
require.NoError(t, err)

// Run "tsh login".
home := t.TempDir()
err = Run(context.Background(), []string{
"login",
"--insecure",
"--debug",
"--proxy", proxyAddr.String()},
setHomePath(home),
setMockSSOLogin(authServer, user, connector.GetName()))
require.NoError(t, err)

// Test compatibility with OpenSSH "-T" and "-t" flags. Note that multiple
// -t options is still not supported.
//
// From "man 1 ssh".
//
// -T Disable pseudo-terminal allocation.
// -t Force pseudo-terminal allocation. This can be used to execute
// arbitrary screen-based programs on a remote machine, which can
// be very useful, e.g. when implementing menu services. Multiple
// -t options force tty allocation, even if ssh has no local tty.
tests := []struct {
name string
flag string
assertError require.ErrorAssertionFunc
}{
{
name: "disable pseudo-terminal allocation",
flag: "-T",
assertError: func(t require.TestingT, err error, i ...interface{}) {
var exiterr *exec.ExitError
if errors.As(err, &exiterr) {
require.Equal(t, 1, exiterr.ExitCode())
} else {
require.Fail(t, "Non *exec.ExitError type: %T.", err)
}
},
},
{
name: "force pseudo-terminal allocation",
flag: "-t",
assertError: require.NoError,
},
}

// Turn the binary running tests into a fake "tsh" binary so it can
// re-execute itself.
t.Setenv(types.HomeEnvVar, home)
t.Setenv(tshBinMainTestEnv, "1")
tshBin, err := os.Executable()
require.NoError(t, err)

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := exec.Command(tshBin, "ssh", tt.flag, hostname, tty).Run()
tt.assertError(t, err)
})
}
}

// TestVersionCompatibilityFlags verifies that "tsh ssh -V" returns Teleport
// version similar to OpenSSH.
func TestVersionCompatibilityFlags(t *testing.T) {
t.Setenv(tshBinMainTestEnv, "1")
tshBin, err := os.Executable()
require.NoError(t, err)

output, err := exec.Command(tshBin, "ssh", "-V").CombinedOutput()
require.NoError(t, err, output)
require.Equal(t, "Teleport CLI", string(bytes.TrimSpace(output)))
}
Loading