Files
teleport/lib/srv/regular/sshserver_test.go
T
Edoardo Spadolini 3da0f44c80 session-helper: embeddable binary and embedding logic (#65784)
* Clean up unneeded forwarder for reexec.IsReexec

* Disallow reexec through common.Run

* Remove the checkhomedir SSH subsystem

* Add true reexec subcommand

* Assume /proc/self/exe is always usable

* Add sessionhelper embed logic
2026-04-17 14:16:53 +00:00

3766 lines
114 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package regular
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"log/slog"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/exec"
"os/user"
"strings"
"sync"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/moby/term"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"google.golang.org/grpc"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/breaker"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
apidefaults "github.com/gravitational/teleport/api/defaults"
decisionpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1"
stableunixusersv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/stableunixusers/v1"
tracessh "github.com/gravitational/teleport/api/observability/tracing/ssh"
apissh "github.com/gravitational/teleport/api/ssh"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/auth/authtest"
"github.com/gravitational/teleport/lib/auth/testauthority"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/bpf"
"github.com/gravitational/teleport/lib/cryptosuites"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/events/eventstest"
"github.com/gravitational/teleport/lib/limiter"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/observability/tracing"
libproxy "github.com/gravitational/teleport/lib/proxy"
"github.com/gravitational/teleport/lib/reversetunnel"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/readonly"
sess "github.com/gravitational/teleport/lib/session"
"github.com/gravitational/teleport/lib/srv"
"github.com/gravitational/teleport/lib/sshagent"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/sshutils/x11forward"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/log/logtest"
"github.com/gravitational/teleport/session/networking/x11"
"github.com/gravitational/teleport/session/pam/pamcfg"
"github.com/gravitational/teleport/session/reexec"
)
// teleportTestUser is additional user used for tests
const teleportTestUser = "teleport-test"
// wildcardAllow is used in tests to allow access to all labels.
var wildcardAllow = types.Labels{
types.Wildcard: []string{types.Wildcard},
}
// 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) {
reexec.MaybeReexec()
logtest.InitLogger(testing.Verbose)
modules.SetInsecureTestMode(true)
code := m.Run()
os.Exit(code)
}
type sshInfo struct {
srv *Server
srvAddress string
srvPort string
srvHostPort string
clt *tracessh.Client
cltConfig apissh.ClientConfig
assertCltClose require.ErrorAssertionFunc
}
type sshTestFixture struct {
ssh sshInfo
up *upack
signer ssh.Signer
user string
clock *clockwork.FakeClock
testSrv *authtest.Server
}
func newFixture(t *testing.T) *sshTestFixture {
return newCustomFixture(t, func(*authtest.ServerConfig) {})
}
func newFixtureWithoutDiskBasedLogging(t testing.TB, sshOpts ...ServerOption) *sshTestFixture {
t.Helper()
f := newCustomFixture(t, func(cfg *authtest.ServerConfig) {
cfg.Auth.AuditLog = events.NewDiscardAuditLog()
}, sshOpts...)
// disable recording because the disk-based uploader
// that runs in the background introduces races with test cleanup
recConfig := types.DefaultSessionRecordingConfig()
recConfig.SetMode(types.RecordOff)
_, err := f.testSrv.Auth().UpsertSessionRecordingConfig(t.Context(), recConfig)
require.NoError(t, err)
return f
}
func (f *sshTestFixture) SSHClientConfig() apissh.ClientConfig {
return apissh.ClientConfig{
User: f.user,
PublicKeyAuth: apissh.PublicKeyAuthConfig{
Signers: func() ([]ssh.Signer, error) {
return []ssh.Signer{f.up.certSigner}, nil
},
},
HostKeyCallback: ssh.FixedHostKey(f.signer.PublicKey()),
}
}
func (f *sshTestFixture) newSSHClient(ctx context.Context, t testing.TB, user *user.User) *tracessh.Client {
// set up SSH client using the user private key for signing
up, err := newUpack(ctx, f.testSrv, user.Username, []string{user.Username}, wildcardAllow)
require.NoError(t, err)
// set up an agent server and a client that uses agent for forwarding
keyring := agent.NewKeyring()
addedKey := agent.AddedKey{
PrivateKey: up.pkey,
Certificate: up.pcert,
}
require.NoError(t, keyring.Add(addedKey))
cltConfig := apissh.ClientConfig{
User: user.Username,
PublicKeyAuth: apissh.PublicKeyAuthConfig{
Signers: func() ([]ssh.Signer, error) {
return []ssh.Signer{up.certSigner}, nil
},
},
HostKeyCallback: ssh.FixedHostKey(f.signer.PublicKey()),
}
client, err := apissh.Dial(ctx, "tcp", f.ssh.srvAddress, cltConfig)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, client.Close())
})
return client
}
func setChildLogConfigForTest() ServerOption {
return func(s *Server) error {
s.childLogConfig = &srv.ChildLogConfig{
ExecLogConfig: reexec.ExecLogConfig{
Level: slog.LevelDebug,
},
Writer: os.Stderr,
}
return nil
}
}
func newCustomFixture(t testing.TB, mutateCfg func(*authtest.ServerConfig), sshOpts ...ServerOption) *sshTestFixture {
ctx := t.Context()
u, err := user.Current()
require.NoError(t, err)
clock := clockwork.NewFakeClock()
serverCfg := authtest.ServerConfig{
Auth: authtest.AuthServerConfig{
ClusterName: "localhost",
Dir: t.TempDir(),
Clock: clock,
},
}
mutateCfg(&serverCfg)
testServer, err := authtest.NewTestServer(serverCfg)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, testServer.Close()) })
signer := newSigner(t, ctx, testServer)
nodeID := uuid.New().String()
nodeClient, err := testServer.NewClient(authtest.TestIdentity{
I: authz.BuiltinRole{
Role: types.RoleNode,
Username: nodeID,
},
})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, nodeClient.Close()) })
lockWatcher := newLockWatcher(ctx, t, nodeClient)
sessionController, err := srv.NewSessionController(srv.SessionControllerConfig{
Semaphores: nodeClient,
AccessPoint: nodeClient,
LockEnforcer: lockWatcher,
Emitter: nodeClient,
Component: teleport.ComponentNode,
ServerID: nodeID,
})
require.NoError(t, err)
nodeDir := t.TempDir()
serverOptions := []ServerOption{
SetUUID(nodeID),
SetNamespace(apidefaults.Namespace),
SetEmitter(nodeClient),
SetPAMConfig(&pamcfg.PAMConfig{Enabled: false}),
SetLabels(map[string]string{"foo": "bar"}, nil, nil),
SetBPF(&bpf.NOP{}),
SetClock(clock),
SetLockWatcher(lockWatcher),
SetX11ForwardingConfig(&x11.ServerConfig{}),
SetSessionController(sessionController),
SetStoragePresenceService(testServer.AuthServer.AuthServer.PresenceInternal),
SetConnectedProxyGetter(reversetunnel.NewConnectedProxyGetter()),
setChildLogConfigForTest(),
}
serverOptions = append(serverOptions, sshOpts...)
sshSrv, err := New(
ctx,
utils.NetAddr{AddrNetwork: "tcp", Addr: "127.0.0.1:0"},
testServer.ClusterName(),
sshutils.StaticHostSigners(signer),
nodeClient,
nodeDir,
"",
utils.NetAddr{},
nodeClient,
serverOptions...)
require.NoError(t, err)
require.NoError(t, sshSrv.Start())
t.Cleanup(func() {
require.NoError(t, sshSrv.Close())
sshSrv.Wait()
})
server, err := sshSrv.getServerInfo(ctx)
require.NoError(t, err)
_, err = testServer.Auth().UpsertNode(ctx, server)
require.NoError(t, err)
sshSrvAddress := sshSrv.Addr()
_, sshSrvPort, err := net.SplitHostPort(sshSrvAddress)
require.NoError(t, err)
sshSrvHostPort := net.JoinHostPort(testServer.ClusterName(), sshSrvPort)
// set up SSH client using the user private key for signing
up, err := newUpack(t.Context(), testServer, u.Username, []string{u.Username}, wildcardAllow)
require.NoError(t, err)
// set up an agent server and a client that uses agent for forwarding
keyring := agent.NewKeyring()
addedKey := agent.AddedKey{
PrivateKey: up.pkey,
Certificate: up.pcert,
}
require.NoError(t, keyring.Add(addedKey))
cltConfig := apissh.ClientConfig{
User: u.Username,
PublicKeyAuth: apissh.PublicKeyAuthConfig{
Signers: func() ([]ssh.Signer, error) {
return []ssh.Signer{up.certSigner}, nil
},
},
HostKeyCallback: ssh.FixedHostKey(signer.PublicKey()),
}
client, err := apissh.Dial(ctx, "tcp", sshSrv.Addr(), cltConfig)
require.NoError(t, err)
f := &sshTestFixture{
ssh: sshInfo{
srv: sshSrv,
srvAddress: sshSrvAddress,
srvPort: sshSrvPort,
srvHostPort: sshSrvHostPort,
clt: client,
cltConfig: cltConfig,
assertCltClose: require.NoError,
},
up: up,
signer: signer,
user: u.Username,
clock: clock,
testSrv: testServer,
}
t.Cleanup(func() { f.ssh.assertCltClose(t, client.Close()) })
require.NoError(t, agent.ForwardToAgent(client.Client, keyring))
return f
}
// TestTerminalSizeRequest validates that terminal size requests are processed and
// responded to appropriately. Namely, it ensures that a response is sent if the client
// requests a reply whether processing the request was successful or not.
func TestTerminalSizeRequest(t *testing.T) {
f := newFixtureWithoutDiskBasedLogging(t)
ctx := t.Context()
t.Run("Invalid session", func(t *testing.T) {
ok, resp, err := f.ssh.clt.SendRequest(ctx, teleport.TerminalSizeRequest, true, []byte("1234"))
require.NoError(t, err)
require.False(t, ok)
require.Empty(t, resp)
})
t.Run("Active session", func(t *testing.T) {
se, err := f.ssh.clt.NewSession(ctx)
require.NoError(t, err)
defer se.Close()
require.NoError(t, se.Shell(ctx))
sessions, err := f.ssh.srv.termHandlers.SessionRegistry.SessionTrackerService.GetActiveSessionTrackers(ctx)
require.NoError(t, err)
require.Len(t, sessions, 1)
sessionID := sessions[0].GetSessionID()
expectedSize := term.Winsize{Height: 100, Width: 200}
// Explicitly set the window size to the expected value.
require.NoError(t, se.WindowChange(ctx, int(expectedSize.Height), int(expectedSize.Width)))
// Wait for the window change request to be reflected in the session before
// initiating the client request for the window size to prevent flakiness.
require.EventuallyWithT(t, func(t *assert.CollectT) {
size, err := f.ssh.srv.termHandlers.SessionRegistry.GetTerminalSize(sessionID)
require.NoError(t, err)
require.Empty(t, cmp.Diff(expectedSize, *size, cmp.AllowUnexported(term.Winsize{})))
}, 10*time.Second, 100*time.Millisecond)
// Send a request for the window size now that we know the window change
// request was honored.
ok, resp, err := f.ssh.clt.SendRequest(ctx, teleport.TerminalSizeRequest, true, []byte(sessionID))
require.NoError(t, err)
require.True(t, ok)
require.NotNil(t, resp)
// Assert that the window size matches the expected dimensions.
var ws term.Winsize
require.NoError(t, json.Unmarshal(resp, &ws))
require.Empty(t, cmp.Diff(expectedSize, ws, cmp.AllowUnexported(term.Winsize{})))
})
}
// TestMultipleExecCommands asserts that multiple SSH exec commands can not be
// sent over a single SSH channel, which is disallowed by the SSH standard
// https://www.ietf.org/rfc/rfc4254.txt
//
// Conformant clients (tsh, openssh) will never try to do this, but we must
// correctly handle the case where an attacker would try to send multiple
// commands over the same channel to try to cover their tracks in the audit log
// or do other nefarious things.
//
// We make sure that:
// - the first command is correctly added to the audit log
// - the second command does not appear in the audit log (as a proxy for testing
// that it was blocked/not executed)
// - there are no panics or unexpected errors
// - and we give the race detector a chance to detect any possible race
// conditions on this code path.
func TestMultipleExecCommands(t *testing.T) {
f := newFixtureWithoutDiskBasedLogging(t)
ctx := t.Context()
// Set up a mock emitter so we can capture audit events.
emitter := eventstest.NewChannelEmitter(32)
f.ssh.srv.StreamEmitter = events.StreamerAndEmitter{
Streamer: events.NewDiscardStreamer(),
Emitter: emitter,
}
// Manually open an ssh channel
channel, _, err := f.ssh.clt.OpenChannel(ctx, "session", nil)
require.NoError(t, err)
sendExec := func(cmd string) error {
type execRequest struct {
Command string
}
// Intentionally don't request a reply so that SendRequest won't wait,
// we want to maximize the chance of triggering any potential race
// condition.
_, err := channel.SendRequest(ctx, "exec", false /*wantReply*/, ssh.Marshal(&execRequest{Command: cmd}))
return err
}
t.Log("sending first exec request over channel")
err = sendExec("echo 1")
require.NoError(t, err)
t.Log("sending second exec request over channel")
// Since we don't wait for a reply, this may or may not return an error, and
// we don't really care either way
_ = sendExec("echo 2")
// Wait for session.end event
timeout := time.After(10 * time.Second)
var execEvent *apievents.Exec
loop:
for {
select {
case event := <-emitter.C():
switch e := event.(type) {
case *apievents.Exec:
require.Nil(t, execEvent, "found more than 1 exec event in audit log")
execEvent = e
case *apievents.SessionEnd:
break loop
}
case <-timeout:
require.Fail(t, "hit timeout while waiting for session.end event")
}
}
// The first command, which was actually executed, should be recorded
// correctly in the audit log
require.NotNil(t, execEvent)
require.Equal(t, "echo 1", execEvent.CommandMetadata.Command)
}
// TestSessionAuditLog tests that the expected audit events are emitted for a
// session with various subsystems involved.
//
// Note: This is a regression test for a bug which resulted in extra, empty session.data
// events for networking requests.
// See https://github.com/gravitational/teleport/issues/48728.
func TestSessionAuditLog(t *testing.T) {
ctx := t.Context()
t.Parallel()
f := newCustomFixture(t, func(cfg *authtest.ServerConfig) {
cfg.Auth.AuditLog = events.NewDiscardAuditLog()
})
// use a sync recording mode because the disk-based uploader
// that runs in the background introduces races with test cleanup
recConfig := types.DefaultSessionRecordingConfig()
recConfig.SetMode(types.RecordAtNodeSync)
_, err := f.testSrv.Auth().UpsertSessionRecordingConfig(t.Context(), recConfig)
require.NoError(t, err)
// Set up a mock emitter so we can capture audit events.
emitter := eventstest.NewChannelEmitter(32)
f.ssh.srv.StreamEmitter = events.StreamerAndEmitter{
Streamer: events.NewDiscardStreamer(),
Emitter: emitter,
}
// Enable x11 forwarding
f.ssh.srv.x11 = &x11.ServerConfig{
Enabled: true,
DisplayOffset: x11.DefaultDisplayOffset,
MaxDisplay: x11.DefaultMaxDisplays,
}
// Allow x11, agent, and port forwarding for the user.
roleName := services.RoleNameForUser(f.user)
role, err := f.testSrv.Auth().GetRole(ctx, roleName)
require.NoError(t, err)
roleOptions := role.GetOptions()
roleOptions.PermitX11Forwarding = types.NewBool(true)
roleOptions.ForwardAgent = types.NewBool(true)
role.SetOptions(roleOptions)
_, err = f.testSrv.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
// Start a new session
se, err := f.ssh.clt.NewSession(ctx)
require.NoError(t, err)
// start interactive SSH session (new shell):
err = se.Shell(ctx)
require.NoError(t, err)
e := <-emitter.C()
startEvent, ok := e.(*apievents.SessionStart)
require.True(t, ok, "expected SessionStart event but got event of type %T", e)
require.NotEmpty(t, startEvent.SessionID, "expected non empty sessionID")
sessionID := startEvent.SessionID
// Request agent forwarding, no individual event emitted.
err = sshagent.RequestAgentForwarding(ctx, se)
require.NoError(t, err)
// Request x11 forwarding, event should be emitted immediately.
clientXAuthEntry, err := x11.NewFakeXAuthEntry(x11.Display{})
require.NoError(t, err)
err = x11forward.RequestForwarding(ctx, se, clientXAuthEntry)
require.NoError(t, err)
x11Event := <-emitter.C()
require.IsType(t, &apievents.X11Forward{}, x11Event, "expected X11Forward event but got event of tgsype %T", x11Event)
// LOCAL PORT FORWARDING
// Start up a test server that doesn't do any remote port forwarding
nonForwardServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello, world")
}))
t.Cleanup(nonForwardServer.Close)
nonForwardServer.Start()
// Each locally forwarded dial should result in a new "start" event and each closed connection should result in a "stop"
// event. Note that we don't know what port the server will forward the connection on, so we don't have an easy way to validate the
// event's addr field.
localConn, err := f.ssh.clt.DialContext(t.Context(), "tcp", nonForwardServer.Listener.Addr().String())
require.NoError(t, err)
e = <-emitter.C()
localForwardStart, ok := e.(*apievents.PortForward)
require.True(t, ok, "expected PortForward event but got event of type %T", e)
require.Equal(t, events.PortForwardLocalEvent, localForwardStart.GetType())
require.Equal(t, events.PortForwardCode, localForwardStart.GetCode())
require.Equal(t, nonForwardServer.Listener.Addr().String(), localForwardStart.Addr)
// closed connections should result in PortForwardLocal stop events
localConn.Close()
e = <-emitter.C()
localForwardStop, ok := e.(*apievents.PortForward)
require.True(t, ok, "expected PortForward event but got event of type %T", e)
require.Equal(t, events.PortForwardLocalEvent, localForwardStop.GetType())
require.Equal(t, events.PortForwardStopCode, localForwardStop.GetCode())
require.Equal(t, nonForwardServer.Listener.Addr().String(), localForwardStop.Addr)
// REMOTE PORT FORWARDING
// Creation of a port forwarded listener should generate PortForwardRemote start events
listener, err := f.ssh.clt.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
e = <-emitter.C()
remoteForwardStart, ok := e.(*apievents.PortForward)
require.True(t, ok, "expected PortForward event but got event of type %T", e)
require.Equal(t, listener.Addr().String(), remoteForwardStart.Addr)
require.Equal(t, events.PortForwardRemoteEvent, remoteForwardStart.GetType())
require.Equal(t, events.PortForwardCode, remoteForwardStart.GetCode())
// Start up a test server that uses the remote port forwarded listener.
remoteForwardServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello, world")
}))
t.Cleanup(remoteForwardServer.Close)
remoteForwardServer.Listener = listener
remoteForwardServer.Start()
// Each dial to the remote listener should result in a new "start" event and each closed connection should result in a "stop" event.
// Note that we don't know what port the server will forward the connection on, so we don't have an easy way to validate the event's
// addr field.
remoteConn, err := net.Dial("tcp", listener.Addr().String())
require.NoError(t, err)
e = <-emitter.C()
remoteConnStart, ok := e.(*apievents.PortForward)
require.True(t, ok, "expected PortForward event but got event of type %T", e)
require.Equal(t, events.PortForwardRemoteConnEvent, remoteConnStart.GetType())
require.Equal(t, events.PortForwardCode, remoteConnStart.GetCode())
remoteConn.Close()
e = <-emitter.C()
remoteConnStop, ok := e.(*apievents.PortForward)
require.True(t, ok, "expected PortForward event but got event of type %T", e)
require.Equal(t, events.PortForwardRemoteConnEvent, remoteConnStop.GetType())
require.Equal(t, events.PortForwardStopCode, remoteConnStop.GetCode())
// Closing the server (and therefore the listener) should generate an PortForwardRemote stop event
remoteForwardServer.Close()
e = <-emitter.C()
remoteForwardStop, ok := e.(*apievents.PortForward)
require.True(t, ok, "expected PortForward event but got event of type %T", e)
require.Equal(t, events.PortForwardRemoteEvent, remoteForwardStop.GetType())
require.Equal(t, events.PortForwardStopCode, remoteForwardStop.Code)
require.Equal(t, listener.Addr().String(), remoteForwardStop.Addr)
// End the session. Session leave, data, and end events should be emitted.
se.Close()
e = <-emitter.C()
leaveEvent, ok := e.(*apievents.SessionLeave)
require.True(t, ok, "expected SessionLeave event but got event of type %T", e)
require.Equal(t, sessionID, leaveEvent.SessionID)
e = <-emitter.C()
dataEvent, ok := e.(*apievents.SessionData)
require.True(t, ok, "expected SessionData event but got event of type %T", e)
require.Equal(t, sessionID, dataEvent.SessionID)
e = <-emitter.C()
endEvent, ok := e.(*apievents.SessionEnd)
require.True(t, ok, "expected SessionEnd event but got event of type %T", e)
require.Equal(t, sessionID, endEvent.SessionID)
}
func newProxyClient(t *testing.T, testSvr *authtest.Server) (*authclient.Client, string) {
// create proxy client used in some tests
proxyID := uuid.New().String()
proxyClient, err := testSvr.NewClient(authtest.TestIdentity{
I: authz.BuiltinRole{
Role: types.RoleProxy,
Username: proxyID,
},
})
require.NoError(t, err)
return proxyClient, proxyID
}
func newNodeClient(t *testing.T, testSvr *authtest.Server) (*authclient.Client, string) {
nodeID := uuid.New().String()
nodeClient, err := testSvr.NewClient(authtest.TestIdentity{
I: authz.BuiltinRole{
Role: types.RoleNode,
Username: nodeID,
},
})
require.NoError(t, err)
return nodeClient, nodeID
}
const hostID = "00000000-0000-0000-0000-000000000000"
func startReadAll(r io.Reader) <-chan []byte {
ch := make(chan []byte)
go func() {
data, _ := io.ReadAll(r)
ch <- data
}()
return ch
}
func waitForBytes(ctx context.Context, ch <-chan []byte) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
select {
case data := <-ch:
return data, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
func TestInactivityTimeout(t *testing.T) {
const timeoutMessage = "You snooze, you lose."
// Given
// * a running auth server configured with a 5s inactivity timeout,
// * a running SSH server configured with a given disconnection message
// * a client connected to the SSH server,
// * an SSH session running over the client connection
mutateCfg := func(cfg *authtest.ServerConfig) {
networkCfg := types.DefaultClusterNetworkingConfig()
networkCfg.SetClientIdleTimeout(5 * time.Second)
networkCfg.SetClientIdleTimeoutMessage(timeoutMessage)
cfg.Auth.ClusterNetworkingConfig = networkCfg
}
waitForTimeout := func(t *testing.T, f *sshTestFixture, se *tracessh.Session) {
stderr, err := se.StderrPipe()
require.NoError(t, err)
stdErrCh := startReadAll(stderr)
endCh := make(chan error)
go func() { endCh <- f.ssh.clt.Wait() }()
t.Cleanup(func() { f.ssh.clt.Close() })
// When I let the session idle (with the clock running at approx 10x speed)...
sessionHasFinished := func() bool {
f.clock.Advance(1 * time.Second)
select {
case <-endCh:
return true
default:
return false
}
}
require.Eventually(t, sessionHasFinished, 6*time.Second, 100*time.Millisecond,
"Timed out waiting for session to finish")
// Expect that the idle timeout has been delivered via stderr
text, err := waitForBytes(t.Context(), stdErrCh)
require.NoError(t, err)
require.Equal(t, timeoutMessage, string(text))
}
t.Run("Normal timeout", func(t *testing.T) {
f := newCustomFixture(t, mutateCfg)
// If all goes well, the client will be closed by the time cleanup happens,
// so change the assertion on closing the client to expect it to fail
f.ssh.assertCltClose = require.Error
se, err := f.ssh.clt.NewSessionWithParams(t.Context(), nil)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, err) })
waitForTimeout(t, f, se)
})
t.Run("Reset timeout on input", func(t *testing.T) {
f := newCustomFixture(t, mutateCfg)
// If all goes well, the client will be closed by the time cleanup happens,
// so change the assertion on closing the client to expect it to fail
f.ssh.assertCltClose = require.Error
se, err := f.ssh.clt.NewSessionWithParams(t.Context(), nil)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, err) })
stdin, err := se.StdinPipe()
require.NoError(t, err)
t.Cleanup(func() { require.ErrorIs(t, stdin.Close(), io.EOF) })
endCh := make(chan error)
go func() { endCh <- f.ssh.clt.Wait() }()
t.Cleanup(func() { f.ssh.clt.Close() })
f.clock.Advance(3 * time.Second)
// Input should reset idle timeout.
_, err = stdin.Write([]byte("echo hello\n"))
require.NoError(t, err)
f.clock.Advance(3 * time.Second)
select {
case <-endCh:
require.Fail(t, "Session timed out too early")
default:
}
waitForTimeout(t, f, se)
})
}
func TestLockInForce(t *testing.T) {
t.Parallel()
ctx := t.Context()
f := newFixtureWithoutDiskBasedLogging(t)
// If all goes well, the client will be closed by the time cleanup happens,
// so change the assertion on closing the client to expect it to fail.
f.ssh.assertCltClose = require.Error
se, err := f.ssh.clt.NewSession(ctx)
require.NoError(t, err)
stderr, err := se.StderrPipe()
require.NoError(t, err)
stdErrCh := startReadAll(stderr)
endCh := make(chan error)
go func() { endCh <- f.ssh.clt.Wait() }()
t.Cleanup(func() { f.ssh.clt.Close() })
lock, err := types.NewLock("test-lock", types.LockSpecV2{
Target: types.LockTarget{Login: f.user},
})
require.NoError(t, err)
watcher := f.ssh.srv.GetLockWatcher()
sub, err := watcher.Subscribe(ctx, lock.Target())
require.NoError(t, err)
require.NoError(t, f.testSrv.Auth().UpsertLock(ctx, lock))
// Wait for the lock to appear before proceeding.
timeout := time.After(20 * time.Second)
for wait := true; wait; {
select {
case evt := <-sub.Events():
if evt.Type != types.OpPut {
continue
}
eventLock, ok := evt.Resource.(types.Lock)
require.True(t, ok)
require.Empty(t, cmp.Diff(lock.Target(), eventLock.Target()))
wait = false
case <-sub.Done():
t.Fatalf("lock subscription terminated unexpectedly %v", sub.Error())
case <-timeout:
t.Fatal("timed out waiting for lock target event")
}
}
require.NoError(t, sub.Close())
// Expect the session to eventually be terminated because of the lock.
select {
case <-endCh:
case <-time.After(15 * time.Second):
t.Fatal("timed out waiting for session to finish")
}
// Expect the lock-in-force message to have been delivered via stderr.
lockInForceMsg := services.LockInForceAccessDenied(lock).Error()
text, err := waitForBytes(t.Context(), stdErrCh)
require.NoError(t, err)
require.Equal(t, lockInForceMsg, string(text))
// As long as the lock is in force, new sessions cannot be opened.
newClient, err := apissh.Dial(ctx, "tcp", f.ssh.srvAddress, f.ssh.cltConfig)
require.NoError(t, err)
t.Cleanup(func() {
// The client is expected to be closed by the lock monitor therefore expect
// an error on this second attempt.
require.Error(t, newClient.Close())
})
_, err = newClient.NewSession(ctx)
require.Error(t, err)
require.Contains(t, err.Error(), lockInForceMsg)
// Once the lock is lifted, new sessions should go through without error.
require.NoError(t, f.testSrv.Auth().DeleteLock(ctx, "test-lock"))
newClient2, err := apissh.Dial(ctx, "tcp", f.ssh.srvAddress, f.ssh.cltConfig)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, newClient2.Close()) })
_, err = newClient2.NewSession(ctx)
require.NoError(t, err)
}
func setPortForwarding(t *testing.T, ctx context.Context, f *sshTestFixture, legacy, remote, local *types.BoolOption) {
roleName := services.RoleNameForUser(f.user)
role, err := f.testSrv.Auth().GetRole(ctx, roleName)
require.NoError(t, err)
roleOptions := role.GetOptions()
roleOptions.PermitX11Forwarding = types.NewBool(true)
roleOptions.ForwardAgent = types.NewBool(true)
//nolint:staticcheck // this field is preserved for existing deployments, but shouldn't be used going forward
roleOptions.PortForwarding = legacy
if remote != nil || local != nil {
roleOptions.SSHPortForwarding = &types.SSHPortForwarding{
Remote: &types.SSHRemotePortForwarding{
Enabled: remote,
},
Local: &types.SSHLocalPortForwarding{
Enabled: local,
},
}
}
role.SetOptions(roleOptions)
_, err = f.testSrv.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
}
// 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 TestDirectTCPIP(t *testing.T) {
ctx := t.Context()
setup := func(t *testing.T) (*sshTestFixture, *httptest.Server, *url.URL) {
f := newFixtureWithoutDiskBasedLogging(t)
// 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")
}))
// Extract the host:port the test HTTP server is running on.
u, err := url.Parse(ts.URL)
require.NoError(t, err)
return f, ts, u
}
t.Run("Local forwarding is successful", func(t *testing.T) {
f, ts, u := setup(t)
defer ts.Close()
// 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 f.ssh.clt.DialContext(t.Context(), "tcp", u.Host)
},
},
}
// Perform a HTTP GET to the test HTTP server through a "direct-tcpip" request.
resp, err := httpClient.Get(ts.URL)
require.NoError(t, err)
defer resp.Body.Close()
// Make sure the response is what was expected.
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, []byte("hello, world\n"), body)
})
t.Run("Local forwarding fails when access is denied", func(t *testing.T) {
f, ts, u := setup(t)
defer ts.Close()
// update rules before creating conn to ensure that permissions are calculated
// using the updated rules.
setPortForwarding(t, ctx, f, nil, nil, types.NewBoolOption(false))
// create a new client connection to the node
clientConn, err := apissh.Dial(ctx, "tcp", f.ssh.srvAddress, f.ssh.cltConfig)
require.NoError(t, err)
defer clientConn.Close()
// create an http client that does forwarding through the node
httpClient := http.Client{
Transport: &http.Transport{
Dial: func(network string, addr string) (net.Conn, error) {
return clientConn.DialContext(t.Context(), "tcp", u.Host)
},
},
}
// Perform a HTTP GET to the test HTTP server through a "direct-tcpip" request.
//nolint:bodyclose // We expect an error here, no need to close.
_, err = httpClient.Get(ts.URL)
require.Error(t, err)
})
t.Run("Local forwarding fails when access is denied by legacy config", func(t *testing.T) {
f, ts, u := setup(t)
defer ts.Close()
// update rules before creating conn to ensure that permissions are calculated
// using the updated rules.
setPortForwarding(t, ctx, f, types.NewBoolOption(false), nil, nil)
// create a new client connection to the node
clientConn, err := apissh.Dial(ctx, "tcp", f.ssh.srvAddress, f.ssh.cltConfig)
require.NoError(t, err)
defer clientConn.Close()
// create an http client that does forwarding through the node
httpClient := http.Client{
Transport: &http.Transport{
Dial: func(network string, addr string) (net.Conn, error) {
return clientConn.DialContext(t.Context(), "tcp", u.Host)
},
},
}
// Perform a HTTP GET to the test HTTP server through a "direct-tcpip" request.
//nolint:bodyclose // We expect an error here, no need to close.
_, err = httpClient.Get(ts.URL)
require.Error(t, err)
})
t.Run("SessionJoinPrincipal cannot use direct-tcpip", func(t *testing.T) {
f, ts, u := setup(t)
defer ts.Close()
// Ensure that ssh client using SessionJoinPrincipal as Login, cannot
// connect using "direct-tcpip".
ctx := t.Context()
cliUsingSessionJoin := f.newSSHClient(ctx, t, &user.User{Username: teleport.SSHSessionJoinPrincipal})
httpClientUsingSessionJoin := http.Client{
Transport: &http.Transport{
Dial: func(network string, addr string) (net.Conn, error) {
return cliUsingSessionJoin.DialContext(ctx, "tcp", u.Host)
},
},
}
//nolint:bodyclose // We expect an error here, no need to close.
_, err := httpClientUsingSessionJoin.Get(ts.URL)
require.ErrorContains(t, err, "ssh: rejected: administratively prohibited (attempted direct-tcpip channel open in join-only mode")
})
}
// TestTCPIPForward ensures that the server can create a listener from a
// "tcpip-forward" request and do remote port forwarding.
func TestTCPIPForward(t *testing.T) {
t.Parallel()
tests := []struct {
name string
listenAddr string
legacyAllow *types.BoolOption
remoteAllow *types.BoolOption
localAllow *types.BoolOption
expectErr bool
}{
{
name: "localhost",
listenAddr: "localhost:0",
},
{
name: "ip address",
listenAddr: "127.0.0.1:0",
},
{
name: "remote deny",
listenAddr: "localhost:0",
remoteAllow: types.NewBoolOption(false),
expectErr: true,
},
{
name: "legacy deny",
listenAddr: "localhost:0",
legacyAllow: types.NewBoolOption(false),
expectErr: true,
},
{
name: "local deny",
listenAddr: "localhost:0",
localAllow: types.NewBoolOption(false),
expectErr: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
f := newFixtureWithoutDiskBasedLogging(t)
setPortForwarding(t, t.Context(), f, tc.legacyAllow, tc.remoteAllow, tc.localAllow)
// create a new client connection to the node which will have its permissions
// calculated with the updated rules.
clientConn, err := apissh.Dial(t.Context(), "tcp", f.ssh.srvAddress, f.ssh.cltConfig)
require.NoError(t, err)
defer clientConn.Close()
// Request a listener from the server.
listener, err := clientConn.Listen("tcp", tc.listenAddr)
if tc.expectErr {
require.Error(t, err)
return
} else {
require.NoError(t, err)
}
// Start up a test server that uses the port forwarded listener.
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello, world")
}))
t.Cleanup(ts.Close)
ts.Listener = listener
ts.Start()
// Dial the test server over the SSH connection.
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
t.Cleanup(cancel)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
require.NoError(t, err)
resp, err := ts.Client().Do(req)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
// Make sure the response is what was expected.
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, []byte("hello, world\n"), body)
})
}
t.Run("SessionJoinPrincipal cannot use tcpip-forward", func(t *testing.T) {
// Ensure that ssh client using SessionJoinPrincipal as Login, cannot
// connect using "tcpip-forward".
f := newFixtureWithoutDiskBasedLogging(t)
ctx := t.Context()
cliUsingSessionJoin := f.newSSHClient(ctx, t, &user.User{Username: teleport.SSHSessionJoinPrincipal})
_, err := cliUsingSessionJoin.Listen("tcp", "127.0.0.1:0")
require.ErrorContains(t, err, "ssh: tcpip-forward request denied by peer")
})
}
func TestAdvertiseAddr(t *testing.T) {
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
// No advertiseAddr was set in fixture, should default to srvAddress.
require.Equal(t, f.ssh.srv.Addr(), f.ssh.srv.AdvertiseAddr())
var (
advIP = utils.MustParseAddr("10.10.10.1")
advIPPort = utils.MustParseAddr("10.10.10.1:1234")
advBadAddr = &utils.NetAddr{Addr: "localhost:badport", AddrNetwork: "tcp"}
)
// IP-only advertiseAddr should use the port from srvAddress.
f.ssh.srv.setAdvertiseAddr(advIP)
require.Equal(t, net.JoinHostPort(advIP.String(), f.ssh.srvPort), f.ssh.srv.AdvertiseAddr())
// IP and port advertiseAddr should fully override srvAddress.
f.ssh.srv.setAdvertiseAddr(advIPPort)
require.Equal(t, advIPPort.String(), f.ssh.srv.AdvertiseAddr())
// nil advertiseAddr should default to srvAddress.
f.ssh.srv.setAdvertiseAddr(nil)
require.Equal(t, f.ssh.srvAddress, f.ssh.srv.AdvertiseAddr())
// Invalid advertiseAddr should fall back to srvAddress.
f.ssh.srv.setAdvertiseAddr(advBadAddr)
require.Equal(t, f.ssh.srvAddress, f.ssh.srv.AdvertiseAddr())
}
// TestAgentForwardPermission makes sure if RBAC rules don't allow agent
// forwarding, we don't start an agent even if requested.
func TestAgentForwardPermission(t *testing.T) {
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
ctx := t.Context()
// make sure the role does not allow agent forwarding
roleName := services.RoleNameForUser(f.user)
role, err := f.testSrv.Auth().GetRole(ctx, roleName)
require.NoError(t, err)
roleOptions := role.GetOptions()
roleOptions.ForwardAgent = types.NewBool(false)
role.SetOptions(roleOptions)
_, err = f.testSrv.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
// create a new client connection to the node which will have its permissions
// calculated with the updated rules.
clientConn, err := apissh.Dial(ctx, "tcp", f.ssh.srvAddress, f.ssh.cltConfig)
require.NoError(t, err)
defer clientConn.Close()
se, err := clientConn.NewSession(ctx)
require.NoError(t, err)
t.Cleanup(func() { se.Close() })
// to interoperate with OpenSSH, requests for agent forwarding always succeed.
// however that does not mean the users agent will actually be forwarded.
require.NoError(t, sshagent.RequestAgentForwarding(ctx, se))
// the output of env, we should not see SSH_AUTH_SOCK in the output
output, err := se.Output(ctx, "env")
require.NoError(t, err)
require.NotContains(t, string(output), "SSH_AUTH_SOCK")
}
// TestMaxSessions makes sure that MaxSessions RBAC rules prevent
// too many concurrent sessions.
func TestMaxSessions(t *testing.T) {
t.Parallel()
const maxSessions int64 = 2
f := newFixtureWithoutDiskBasedLogging(t)
ctx := t.Context()
// make sure the role does not allow agent forwarding
roleName := services.RoleNameForUser(f.user)
role, err := f.testSrv.Auth().GetRole(ctx, roleName)
require.NoError(t, err)
roleOptions := role.GetOptions()
roleOptions.MaxSessions = maxSessions
role.SetOptions(roleOptions)
_, err = f.testSrv.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
// create a new client connection to the node which will have its permissions
// calculated with the updated rules.
clientConn, err := apissh.Dial(ctx, "tcp", f.ssh.srvAddress, f.ssh.cltConfig)
require.NoError(t, err)
defer clientConn.Close()
for range maxSessions {
se, err := clientConn.NewSession(ctx)
require.NoError(t, err)
defer se.Close()
}
_, err = clientConn.NewSession(ctx)
require.Error(t, err)
require.Contains(t, err.Error(), "too many session channels")
// verify that max sessions does not affect max connections.
for i := int64(0); i <= maxSessions; i++ {
clt, err := apissh.Dial(ctx, "tcp", f.ssh.srv.Addr(), f.ssh.cltConfig)
require.NoError(t, err)
require.NoError(t, clt.Close())
}
}
// TestExecLongCommand makes sure that commands that are longer than the
// maximum pipe size on the OS can still be started. This tests the reexec
// functionality of Teleport as Teleport will reexec itself when launching a
// command and send the command to then launch through a pipe.
func TestExecLongCommand(t *testing.T) {
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
ctx := t.Context()
// Get the path to where the "echo" command is on disk.
echoPath, err := exec.LookPath("echo")
require.NoError(t, err)
se, err := f.ssh.clt.NewSession(ctx)
require.NoError(t, err)
defer se.Close()
// Write a message that larger than the maximum pipe size.
_, err = se.Output(ctx, fmt.Sprintf("%v %v", echoPath, strings.Repeat("a", maxPipeSize)))
require.NoError(t, err)
}
// TestOpenExecSessionSetsSession tests that OpenExecSession()
// sets ServerContext session.
func TestOpenExecSessionSetsSession(t *testing.T) {
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
ctx := t.Context()
se, err := f.ssh.clt.NewSession(ctx)
require.NoError(t, err)
defer se.Close()
// This will trigger an exec request, which will start a non-interactive session,
// which then triggers setting env for SSH_SESSION_ID.
output, err := se.Output(ctx, "env")
require.NoError(t, err)
require.Contains(t, string(output), teleport.SSHSessionID)
}
// TestAgentForward tests agent forwarding via unix sockets
func TestAgentForward(t *testing.T) {
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
ctx := t.Context()
roleName := services.RoleNameForUser(f.user)
role, err := f.testSrv.Auth().GetRole(ctx, roleName)
require.NoError(t, err)
roleOptions := role.GetOptions()
roleOptions.ForwardAgent = types.NewBool(true)
role.SetOptions(roleOptions)
_, err = f.testSrv.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
se, err := f.ssh.clt.NewSession(ctx)
require.NoError(t, err)
t.Cleanup(func() { se.Close() })
err = sshagent.RequestAgentForwarding(ctx, se)
require.NoError(t, err)
// prepare to send virtual "keyboard input" into the shell:
keyboard, err := se.StdinPipe()
require.NoError(t, err)
t.Cleanup(func() { keyboard.Close() })
// start interactive SSH session (new shell):
err = se.Shell(ctx)
require.NoError(t, err)
// create a temp file to collect the shell output into:
tmpFile, err := os.CreateTemp(t.TempDir(), "teleport-agent-forward-test")
require.NoError(t, err)
tmpFile.Close()
// type 'printenv SSH_AUTH_SOCK > /path/to/tmp/file' into the session (dumping the value of SSH_AUTH_STOCK into the temp file)
_, err = fmt.Fprintf(keyboard, "printenv %v >> %s\n\r", teleport.SSHAuthSock, tmpFile.Name())
require.NoError(t, err)
// wait for the output
var socketPath string
require.Eventually(t, func() bool {
output, err := os.ReadFile(tmpFile.Name())
if err == nil && len(output) != 0 {
socketPath = strings.TrimSpace(string(output))
return true
}
return false
}, 10*time.Second, 100*time.Millisecond, "failed to read socket path")
// try dialing the ssh agent socket:
file, err := net.Dial("unix", socketPath)
require.NoError(t, err)
clientAgent := agent.NewClient(file)
sshConfig := apissh.ClientConfig{
User: f.user,
PublicKeyAuth: apissh.PublicKeyAuthConfig{
Signers: func() ([]ssh.Signer, error) {
return clientAgent.Signers()
},
},
HostKeyCallback: ssh.FixedHostKey(f.signer.PublicKey()),
}
client, err := apissh.Dial(ctx, "tcp", f.ssh.srv.Addr(), sshConfig)
require.NoError(t, err)
err = client.Close()
require.NoError(t, err)
// make sure the socket persists after the session is closed.
// (agents are started from specific sessions, but apply to all
// sessions on the connection).
err = se.Close()
require.NoError(t, err)
// Pause to allow closure to propagate.
time.Sleep(150 * time.Millisecond)
_, err = net.Dial("unix", socketPath)
require.NoError(t, err)
// make sure the socket is gone after we closed the connection. Note that
// we now expect the client close to fail during the test cleanup, so we
// change the assertion accordingly
require.NoError(t, f.ssh.clt.Close())
f.ssh.assertCltClose = require.Error
// clt must be nullified to prevent double-close during test cleanup
f.ssh.clt = nil
require.Eventually(t, func() bool {
_, err := clientAgent.List()
return err != nil
},
10*time.Second, 100*time.Millisecond,
"expected socket to be closed, still could dial")
}
// TestX11Forward tests x11 forwarding via unix sockets
func TestX11Forward(t *testing.T) {
ctx := t.Context()
if os.Getenv("TELEPORT_XAUTH_TEST") == "" {
t.Skip("Skipping test as xauth is not enabled")
}
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
f.ssh.srv.x11 = &x11.ServerConfig{
Enabled: true,
DisplayOffset: x11.DefaultDisplayOffset,
MaxDisplay: x11.DefaultMaxDisplays,
}
roleName := services.RoleNameForUser(f.user)
role, err := f.testSrv.Auth().GetRole(ctx, roleName)
require.NoError(t, err)
roleOptions := role.GetOptions()
roleOptions.PermitX11Forwarding = types.NewBool(true)
role.SetOptions(roleOptions)
_, err = f.testSrv.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
// Open two x11 sessions from two separate clients to
// ensure concurrent X11 forwarding sessions are supported.
serverDisplay := x11EchoSession(ctx, t, f.ssh.clt)
user, err := user.Current()
require.NoError(t, err)
client2 := f.newSSHClient(ctx, t, user)
serverDisplay2 := x11EchoSession(ctx, t, client2)
// Create multiple XServer requests, the server should
// handle multiple concurrent XServer requests.
errCh := make(chan error)
go func() {
errCh <- x11EchoRequest(serverDisplay)
}()
go func() {
errCh <- x11EchoRequest(serverDisplay)
}()
go func() {
errCh <- x11EchoRequest(serverDisplay2)
}()
go func() {
errCh <- x11EchoRequest(serverDisplay2)
}()
for range 4 {
select {
case err := <-errCh:
assert.NoError(t, err)
case <-ctx.Done():
assert.NoError(t, context.Cause(ctx))
}
}
}
// x11EchoSession creates a new ssh session and handles x11 forwarding for the session,
// echoing XServer requests received back to the client. Returns the Display opened on the
// session, which is set in $DISPLAY.
func x11EchoSession(ctx context.Context, t *testing.T, clt *tracessh.Client) x11.Display {
se, err := clt.NewSessionWithParams(ctx, nil)
require.NoError(t, err)
t.Cleanup(func() { se.Close() })
// Create a fake client XServer listener which echos
// back whatever it receives.
fakeClientDisplay, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
go func() {
for {
conn, err := fakeClientDisplay.Accept()
if err != nil {
return
}
_, err = io.Copy(conn, conn)
assert.NoError(t, err)
conn.Close()
}
}()
t.Cleanup(func() { fakeClientDisplay.Close() })
// Handle any x11 channel requests received from the server
// and start x11 forwarding to the client display.
err = x11forward.ServeChannelRequests(ctx, clt.Client, func(ctx context.Context, nch ssh.NewChannel) {
sch, sin, err := nch.Accept()
assert.NoError(t, err)
defer sch.Close()
clientConn, err := net.Dial("tcp", fakeClientDisplay.Addr().String())
assert.NoError(t, err)
clientXConn, ok := clientConn.(*net.TCPConn)
assert.True(t, ok)
defer clientConn.Close()
go func() {
_ = sshutils.ForwardRequests(ctx, sin, se)
}()
err = utils.ProxyConn(ctx, clientXConn, sch)
// Error should be nil if the ssh client is closed first, or canceled if the context is closed first.
if !errors.Is(err, context.Canceled) {
assert.NoError(t, err)
}
})
require.NoError(t, err)
// Client requests x11 forwarding for the server session.
clientXAuthEntry, err := x11.NewFakeXAuthEntry(x11.Display{})
require.NoError(t, err)
err = x11forward.RequestForwarding(ctx, se, clientXAuthEntry)
require.NoError(t, err)
// prepare to send virtual "keyboard input" into the shell:
keyboard, err := se.StdinPipe()
require.NoError(t, err)
// start interactive SSH session with x11 forwarding enabled (new shell):
err = se.Shell(ctx)
require.NoError(t, err)
// create a temp file to collect the shell output into:
tmpFile, err := os.CreateTemp(os.TempDir(), "teleport-x11-forward-test")
require.NoError(t, err)
// Allow non-root user to write to the temp file
err = tmpFile.Chmod(fs.FileMode(0o777))
require.NoError(t, err)
t.Cleanup(func() {
os.Remove(tmpFile.Name())
})
// Reading the display may fail if the session is not fully initialized
// and the write to stdin is swallowed.
display := make(chan string, 1)
require.EventuallyWithT(t, func(t *assert.CollectT) {
// enter 'printenv DISPLAY > /path/to/tmp/file' into the session (dumping the value of DISPLAY into the temp file)
_, err = fmt.Fprintf(keyboard, "printenv %v > %s\n\r", x11.DisplayEnv, tmpFile.Name())
require.NoError(t, err)
require.Eventually(t, func() bool {
output, err := os.ReadFile(tmpFile.Name())
if err == nil && len(output) != 0 {
select {
case display <- strings.TrimSpace(string(output)):
default:
}
return true
}
return false
}, time.Second, 100*time.Millisecond, "failed to read display")
}, 10*time.Second, 1*time.Second)
// Make a new connection to the XServer proxy, the client
// XServer should echo back anything written on it.
serverDisplay, err := x11.ParseDisplay(<-display)
require.NoError(t, err)
return serverDisplay
}
// x11EchoRequest sends a message to the serverDisplay and expects the
// server to echo the message back to it.
func x11EchoRequest(serverDisplay x11.Display) error {
conn, err := serverDisplay.Dial()
if err != nil {
return err
}
defer conn.Close()
msg := "msg"
_, err = conn.Write([]byte(msg))
if err != nil {
return err
}
buf := make([]byte, 3)
_, err = conn.Read(buf)
if err != nil {
return err
}
if string(buf) != msg {
return trace.Errorf("x11 echo request returned a different message than expected")
}
return nil
}
func TestAllowedUsers(t *testing.T) {
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
sshConfig := f.SSHClientConfig()
client, err := apissh.Dial(t.Context(), "tcp", f.ssh.srv.Addr(), sshConfig)
require.NoError(t, err)
require.NoError(t, client.Close())
// now remove OS user from valid principals
up, err := newUpack(t.Context(), f.testSrv, f.user, []string{"otheruser"}, wildcardAllow)
require.NoError(t, err)
sshConfig = apissh.ClientConfig{
User: f.user,
PublicKeyAuth: apissh.PublicKeyAuthConfig{
Signers: func() ([]ssh.Signer, error) {
return []ssh.Signer{up.certSigner}, nil
},
},
HostKeyCallback: ssh.FixedHostKey(f.signer.PublicKey()),
}
_, err = apissh.Dial(t.Context(), "tcp", f.ssh.srv.Addr(), sshConfig)
require.Error(t, err)
}
func TestAllowedLabels(t *testing.T) {
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t,
SetLabels(
map[string]string{"foo": "bar"},
services.CommandLabels{
"baz": &types.CommandLabelV2{
Period: types.NewDuration(time.Second),
Command: []string{"expr", "1", "+", "3"},
},
},
nil,
),
)
tests := []struct {
desc string
inLabelMap types.Labels
outError bool
}{
// Valid static label.
{
desc: "Valid Static",
inLabelMap: types.Labels{"foo": []string{"bar"}},
outError: false,
},
// Invalid static label.
{
desc: "Invalid Static",
inLabelMap: types.Labels{"foo": []string{"baz"}},
outError: true,
},
// Valid dynamic label.
{
desc: "Valid Dynamic",
inLabelMap: types.Labels{"baz": []string{"4"}},
outError: false,
},
// Invalid dynamic label.
{
desc: "Invalid Dynamic",
inLabelMap: types.Labels{"baz": []string{"5"}},
outError: true,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
up, err := newUpack(t.Context(), f.testSrv, f.user, []string{f.user}, tt.inLabelMap)
require.NoError(t, err)
sshConfig := apissh.ClientConfig{
User: f.user,
PublicKeyAuth: apissh.PublicKeyAuthConfig{
Signers: func() ([]ssh.Signer, error) {
return []ssh.Signer{up.certSigner}, nil
},
},
HostKeyCallback: ssh.FixedHostKey(f.signer.PublicKey()),
}
_, err = apissh.Dial(t.Context(), "tcp", f.ssh.srv.Addr(), sshConfig)
if tt.outError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestInvalidSessionID(t *testing.T) {
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
ctx := t.Context()
session, err := f.ssh.clt.NewSession(ctx)
require.NoError(t, err)
err = session.Setenv(ctx, sshutils.SessionEnvVar, "foo")
require.NoError(t, err)
err = session.Shell(ctx)
require.Error(t, err)
}
func TestSessionHijack(t *testing.T) {
t.Parallel()
ctx := t.Context()
_, err := user.Lookup(teleportTestUser)
if err != nil {
t.Skipf("user %v is not found, skipping test", teleportTestUser)
}
f := newFixtureWithoutDiskBasedLogging(t)
se, err := f.ssh.clt.NewSession(ctx)
require.NoError(t, err)
defer se.Close()
firstSessionID := string(sess.NewID())
err = se.Setenv(ctx, sshutils.SessionEnvVar, firstSessionID)
require.NoError(t, err)
err = se.Shell(ctx)
require.NoError(t, err)
// user 2 does not have s.user as a listed principal
up2, err := newUpack(t.Context(), f.testSrv, teleportTestUser, []string{teleportTestUser}, wildcardAllow)
require.NoError(t, err)
sshConfig2 := apissh.ClientConfig{
User: teleportTestUser,
PublicKeyAuth: apissh.PublicKeyAuthConfig{
Signers: func() ([]ssh.Signer, error) {
return []ssh.Signer{up2.certSigner}, nil
},
},
HostKeyCallback: ssh.FixedHostKey(f.signer.PublicKey()),
}
client2, err := apissh.Dial(ctx, "tcp", f.ssh.srv.Addr(), sshConfig2)
require.NoError(t, err)
defer func() {
err := client2.Close()
require.NoError(t, err)
}()
se2, err := client2.NewSession(ctx)
require.NoError(t, err)
defer se2.Close()
err = se2.Setenv(ctx, sshutils.SessionEnvVar, firstSessionID)
require.NoError(t, err)
// attempt to hijack, should return error
err = se2.Shell(ctx)
require.Error(t, err)
}
// testClient dials targetAddr via proxyAddr and executes 2+3 command
func testClient(t *testing.T, f *sshTestFixture, proxyAddr, targetAddr, remoteAddr string, sshConfig apissh.ClientConfig) {
ctx := t.Context()
// Connect to node using registered address
client, err := apissh.Dial(ctx, "tcp", proxyAddr, sshConfig)
require.NoError(t, err)
defer client.Close()
se, err := client.NewSession(ctx)
require.NoError(t, err)
defer se.Close()
writer, err := se.StdinPipe()
require.NoError(t, err)
reader, err := se.StdoutPipe()
require.NoError(t, err)
// Request opening TCP connection to the remote host
require.NoError(t, se.RequestSubsystem(ctx, fmt.Sprintf("proxy:%v", targetAddr)))
local, err := utils.ParseAddr("tcp://" + proxyAddr)
require.NoError(t, err)
remote, err := utils.ParseAddr("tcp://" + remoteAddr)
require.NoError(t, err)
pipeNetConn := utils.NewPipeNetConn(
reader,
writer,
se,
local,
remote,
)
defer pipeNetConn.Close()
// Open SSH connection via TCP
conn, chans, reqs, err := apissh.NewClientConn(
ctx,
pipeNetConn,
f.ssh.srv.Addr(),
sshConfig,
)
require.NoError(t, err)
defer conn.Close()
// using this connection as regular SSH
client2 := tracessh.NewClient(conn, chans, reqs)
defer client2.Close()
se2, err := client2.NewSession(ctx)
require.NoError(t, err)
defer se2.Close()
out, err := se2.Output(ctx, "echo hello")
require.NoError(t, err)
require.Equal(t, "hello\n", string(out))
}
func mustListen(t *testing.T) (net.Listener, utils.NetAddr) {
l, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
addr := utils.NetAddr{AddrNetwork: "tcp", Addr: l.Addr().String()}
return l, addr
}
func noCache(clt authclient.ClientI, cacheName []string) (authclient.RemoteProxyAccessPoint, error) {
return clt, nil
}
func TestProxyRoundRobin(t *testing.T) {
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
ctx := t.Context()
proxyClient, _ := newProxyClient(t, f.testSrv)
nodeClient, _ := newNodeClient(t, f.testSrv)
listener, reverseTunnelAddress := mustListen(t)
defer listener.Close()
lockWatcher := newLockWatcher(ctx, t, proxyClient)
nodeWatcher := newNodeWatcher(ctx, t, proxyClient)
caWatcher := newCertAuthorityWatcher(ctx, t, proxyClient)
reverseTunnelServer, err := reversetunnel.NewServer(reversetunnel.Config{
GetClientTLSCertificate: func() (*tls.Certificate, error) {
return &proxyClient.TLSConfig().Certificates[0], nil
},
ClusterName: f.testSrv.ClusterName(),
ID: hostID,
Listener: listener,
GetHostSigners: sshutils.StaticHostSigners(f.signer),
LocalAuthClient: proxyClient,
LocalAccessPoint: proxyClient,
NewCachingAccessPoint: noCache,
DataDir: t.TempDir(),
Emitter: proxyClient,
LockWatcher: lockWatcher,
NodeWatcher: nodeWatcher,
GitServerWatcher: newGitServerWatcher(ctx, t, proxyClient),
AppServerWatcher: newAppServerWatcher(ctx, t, proxyClient),
DatabaseServerWatcher: newDatabaseServerWatcher(ctx, t, proxyClient),
CertAuthorityWatcher: caWatcher,
CircuitBreakerConfig: breaker.NoopBreakerConfig(),
EICESigner: func(ctx context.Context, target types.Server, integration types.Integration, login, token string, ap cryptosuites.AuthPreferenceGetter) (ssh.Signer, error) {
return nil, errors.New("eice disabled in tests")
},
EICEDialer: func(ctx context.Context, target types.Server, integration types.Integration, token string) (net.Conn, error) {
return nil, errors.New("eice disabled in tests")
},
})
require.NoError(t, err)
require.NoError(t, reverseTunnelServer.Start())
defer reverseTunnelServer.Close()
router, err := libproxy.NewRouter(libproxy.RouterConfig{
ClusterName: f.testSrv.ClusterName(),
LocalAccessPoint: proxyClient,
ClusterGetter: reverseTunnelServer,
TracerProvider: tracing.NoopProvider(),
})
require.NoError(t, err)
sessionController, err := srv.NewSessionController(srv.SessionControllerConfig{
Semaphores: proxyClient,
AccessPoint: proxyClient,
LockEnforcer: lockWatcher,
Emitter: proxyClient,
Component: teleport.ComponentNode,
ServerID: hostID,
})
require.NoError(t, err)
proxy, err := New(
ctx,
utils.NetAddr{AddrNetwork: "tcp", Addr: "localhost:0"},
f.testSrv.ClusterName(),
sshutils.StaticHostSigners(f.signer),
proxyClient,
t.TempDir(),
"",
utils.NetAddr{},
proxyClient,
SetUUID(uuid.NewString()),
SetProxyMode("", reverseTunnelServer, proxyClient, router),
SetEmitter(nodeClient),
SetNamespace(apidefaults.Namespace),
SetPAMConfig(&pamcfg.PAMConfig{Enabled: false}),
SetBPF(&bpf.NOP{}),
SetClock(f.clock),
SetLockWatcher(lockWatcher),
SetSessionController(sessionController),
SetConnectedProxyGetter(reversetunnel.NewConnectedProxyGetter()),
setChildLogConfigForTest(),
)
require.NoError(t, err)
require.NoError(t, proxy.Start())
defer proxy.Close()
resolver := func(context.Context) (*utils.NetAddr, types.ProxyListenerMode, error) {
return &utils.NetAddr{Addr: reverseTunnelAddress.Addr, AddrNetwork: "tcp"}, types.ProxyListenerMode_Separate, nil
}
pool1, err := reversetunnel.NewAgentPool(ctx, reversetunnel.AgentPoolConfig{
Resolver: resolver,
Client: proxyClient,
AccessPoint: proxyClient,
PublicKeyAuth: apissh.PublicKeyAuthConfig{
Signers: func() ([]ssh.Signer, error) {
return []ssh.Signer{f.signer}, nil
},
},
HostUUID: hostID + "." + f.testSrv.ClusterName(),
Cluster: "remote",
})
require.NoError(t, err)
err = pool1.Start()
require.NoError(t, err)
defer pool1.Stop()
pool2, err := reversetunnel.NewAgentPool(ctx, reversetunnel.AgentPoolConfig{
Resolver: resolver,
Client: proxyClient,
AccessPoint: proxyClient,
PublicKeyAuth: apissh.PublicKeyAuthConfig{
Signers: func() ([]ssh.Signer, error) {
return []ssh.Signer{f.signer}, nil
},
},
HostUUID: hostID + "." + f.testSrv.ClusterName(),
Cluster: "remote",
})
require.NoError(t, err)
err = pool2.Start()
require.NoError(t, err)
defer pool2.Stop()
sshConfig := f.SSHClientConfig()
var wg sync.WaitGroup
for range 3 {
wg.Go(func() {
testClient(t, f, proxy.Addr(), f.ssh.srvAddress, f.ssh.srv.Addr(), sshConfig)
})
}
wg.Wait()
// close first connection, and test it again
pool1.Stop()
for range 3 {
wg.Go(func() {
testClient(t, f, proxy.Addr(), f.ssh.srvAddress, f.ssh.srv.Addr(), sshConfig)
})
}
wg.Wait()
}
// TestProxyDirectAccess tests direct access via proxy bypassing
// reverse tunnel
func TestProxyDirectAccess(t *testing.T) {
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
ctx := t.Context()
listener, _ := mustListen(t)
proxyClient, _ := newProxyClient(t, f.testSrv)
lockWatcher := newLockWatcher(ctx, t, proxyClient)
nodeWatcher := newNodeWatcher(ctx, t, proxyClient)
caWatcher := newCertAuthorityWatcher(ctx, t, proxyClient)
reverseTunnelServer, err := reversetunnel.NewServer(reversetunnel.Config{
GetClientTLSCertificate: func() (*tls.Certificate, error) {
return &proxyClient.TLSConfig().Certificates[0], nil
},
ID: hostID,
ClusterName: f.testSrv.ClusterName(),
Listener: listener,
GetHostSigners: sshutils.StaticHostSigners(f.signer),
LocalAuthClient: proxyClient,
LocalAccessPoint: proxyClient,
NewCachingAccessPoint: noCache,
DataDir: t.TempDir(),
Emitter: proxyClient,
LockWatcher: lockWatcher,
NodeWatcher: nodeWatcher,
GitServerWatcher: newGitServerWatcher(ctx, t, proxyClient),
AppServerWatcher: newAppServerWatcher(ctx, t, proxyClient),
DatabaseServerWatcher: newDatabaseServerWatcher(ctx, t, proxyClient),
CertAuthorityWatcher: caWatcher,
CircuitBreakerConfig: breaker.NoopBreakerConfig(),
EICESigner: func(ctx context.Context, target types.Server, integration types.Integration, login, token string, ap cryptosuites.AuthPreferenceGetter) (ssh.Signer, error) {
return nil, errors.New("eice disabled in tests")
},
EICEDialer: func(ctx context.Context, target types.Server, integration types.Integration, token string) (net.Conn, error) {
return nil, errors.New("eice disabled in tests")
},
})
require.NoError(t, err)
require.NoError(t, reverseTunnelServer.Start())
defer reverseTunnelServer.Close()
nodeClient, _ := newNodeClient(t, f.testSrv)
router, err := libproxy.NewRouter(libproxy.RouterConfig{
ClusterName: f.testSrv.ClusterName(),
LocalAccessPoint: proxyClient,
ClusterGetter: reverseTunnelServer,
TracerProvider: tracing.NoopProvider(),
})
require.NoError(t, err)
sessionController, err := srv.NewSessionController(srv.SessionControllerConfig{
Semaphores: nodeClient,
AccessPoint: nodeClient,
LockEnforcer: lockWatcher,
Emitter: nodeClient,
Component: teleport.ComponentNode,
ServerID: hostID,
})
require.NoError(t, err)
proxy, err := New(
ctx,
utils.NetAddr{AddrNetwork: "tcp", Addr: "localhost:0"},
f.testSrv.ClusterName(),
sshutils.StaticHostSigners(f.signer),
proxyClient,
t.TempDir(),
"",
utils.NetAddr{},
proxyClient,
SetUUID(uuid.NewString()),
SetProxyMode("", reverseTunnelServer, proxyClient, router),
SetEmitter(nodeClient),
SetNamespace(apidefaults.Namespace),
SetPAMConfig(&pamcfg.PAMConfig{Enabled: false}),
SetBPF(&bpf.NOP{}),
SetClock(f.clock),
SetLockWatcher(lockWatcher),
SetSessionController(sessionController),
SetConnectedProxyGetter(reversetunnel.NewConnectedProxyGetter()),
setChildLogConfigForTest(),
)
require.NoError(t, err)
require.NoError(t, proxy.Start())
defer proxy.Close()
testClient(t, f, proxy.Addr(), f.ssh.srvAddress, f.ssh.srv.Addr(), f.SSHClientConfig())
}
// TestPTY requests PTY for an interactive session
func TestPTY(t *testing.T) {
t.Parallel()
ctx := t.Context()
f := newFixtureWithoutDiskBasedLogging(t)
se, err := f.ssh.clt.NewSession(ctx)
require.NoError(t, err)
defer se.Close()
// request PTY with valid size
require.NoError(t, se.RequestPty(ctx, "xterm", 30, 30, ssh.TerminalModes{}))
// request PTY with invalid size, should still work (selects defaults)
require.NoError(t, se.RequestPty(ctx, "xterm", 0, 0, ssh.TerminalModes{}))
}
// TestEnv requests setting environment variables via
// a "env" request.
func TestEnv(t *testing.T) {
t.Parallel()
ctx := t.Context()
f := newFixtureWithoutDiskBasedLogging(t)
se, err := f.ssh.clt.NewSession(ctx)
require.NoError(t, err)
defer se.Close()
require.NoError(t, se.Setenv(ctx, "HOME_TEST", "/test"))
output, err := se.Output(ctx, "env")
require.NoError(t, err)
require.Contains(t, string(output), "HOME_TEST=/test")
}
// TestEnvs requests setting environment variables via
// a "[email protected]" request.
func TestEnvs(t *testing.T) {
t.Parallel()
ctx := t.Context()
f := newFixtureWithoutDiskBasedLogging(t)
se, err := f.ssh.clt.NewSession(ctx)
require.NoError(t, err)
defer se.Close()
envs := map[string]string{
"HOME_TEST": "/test",
"LLAMA": "ALPACA",
"FISH": "FROG",
}
require.NoError(t, se.SetEnvs(ctx, envs))
output, err := se.Output(ctx, "env")
require.NoError(t, err)
for k, v := range envs {
require.Contains(t, string(output), k+"="+v)
}
}
// TestUnknownRequest validates that any unknown session
// requests do not terminate the session.
func TestUnknownRequest(t *testing.T) {
t.Parallel()
ctx := t.Context()
f := newFixtureWithoutDiskBasedLogging(t)
se, err := f.ssh.clt.NewSession(ctx)
require.NoError(t, err)
defer se.Close()
// send a random request that won't be handled
ok, err := se.SendRequest(ctx, uuid.NewString(), true, nil)
require.NoError(t, err)
require.False(t, ok)
// ensure the session is still active
require.NoError(t, se.Setenv(ctx, "HOME_TEST", "/test"))
output, err := se.Output(ctx, "env")
require.NoError(t, err)
require.Contains(t, string(output), "HOME_TEST=/test")
}
// TestNoAuth tries to log in with no auth methods and should be rejected
func TestNoAuth(t *testing.T) {
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
config := &ssh.ClientConfig{ //nolint: forbidigo // Testing no auth methods.
User: f.user,
Auth: []ssh.AuthMethod{},
HostKeyCallback: ssh.FixedHostKey(f.signer.PublicKey()),
}
_, err := tracessh.Dial(t.Context(), "tcp", f.ssh.srv.Addr(), config) //nolint: forbidigo // Testing no auth methods.
require.Error(t, err)
}
// TestPasswordAuth tries to log in with empty pass and should be rejected
func TestPasswordAuth(t *testing.T) {
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
config := &ssh.ClientConfig{ //nolint: forbidigo // Testing password auth
Auth: []ssh.AuthMethod{ssh.Password("")},
HostKeyCallback: ssh.FixedHostKey(f.signer.PublicKey()),
}
_, err := tracessh.Dial(t.Context(), "tcp", f.ssh.srv.Addr(), config) //nolint: forbidigo // Testing password auth
require.Error(t, err)
}
func TestClientDisconnect(t *testing.T) {
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
ctx := t.Context()
clt, err := apissh.Dial(ctx, "tcp", f.ssh.srv.Addr(), f.SSHClientConfig())
require.NoError(t, err)
require.NotNil(t, clt)
se, err := f.ssh.clt.NewSession(ctx)
require.NoError(t, err)
require.NoError(t, se.Shell(ctx))
require.NoError(t, clt.Close())
}
func TestLimiter(t *testing.T) {
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
ctx := t.Context()
getConns := func(t *testing.T, limiter *limiter.Limiter, token string, num int64) func() bool {
return func() bool {
connNumber, err := limiter.GetNumConnection(token)
return err == nil && connNumber == num
}
}
limiter, err := limiter.NewLimiter(
limiter.Config{
Clock: f.clock,
MaxConnections: 2,
Rates: []limiter.Rate{
{
Period: 10 * time.Second,
Average: 1,
Burst: 3,
},
{
Period: 40 * time.Millisecond,
Average: 10,
Burst: 30,
},
},
},
)
require.NoError(t, err)
nodeClient, _ := newNodeClient(t, f.testSrv)
lockWatcher := newLockWatcher(ctx, t, nodeClient)
sessionController, err := srv.NewSessionController(srv.SessionControllerConfig{
Semaphores: nodeClient,
AccessPoint: nodeClient,
LockEnforcer: lockWatcher,
Emitter: nodeClient,
Component: teleport.ComponentNode,
ServerID: hostID,
})
require.NoError(t, err)
nodeStateDir := t.TempDir()
srv, err := New(
ctx,
utils.NetAddr{AddrNetwork: "tcp", Addr: "127.0.0.1:0"},
f.testSrv.ClusterName(),
sshutils.StaticHostSigners(f.signer),
nodeClient,
nodeStateDir,
"",
utils.NetAddr{},
nodeClient,
SetUUID(uuid.NewString()),
SetLimiter(limiter),
SetEmitter(nodeClient),
SetNamespace(apidefaults.Namespace),
SetPAMConfig(&pamcfg.PAMConfig{Enabled: false}),
SetBPF(&bpf.NOP{}),
SetClock(f.clock),
SetLockWatcher(lockWatcher),
SetSessionController(sessionController),
SetConnectedProxyGetter(reversetunnel.NewConnectedProxyGetter()),
setChildLogConfigForTest(),
)
require.NoError(t, err)
require.NoError(t, srv.Start())
defer srv.Close()
config := f.SSHClientConfig()
clt0, err := apissh.Dial(ctx, "tcp", srv.Addr(), config)
require.NoError(t, err)
require.NotNil(t, clt0)
se0, err := clt0.NewSession(ctx)
require.NoError(t, err)
require.NoError(t, se0.Shell(ctx))
// current connections = 1
clt, err := apissh.Dial(ctx, "tcp", srv.Addr(), config)
require.NoError(t, err)
require.NotNil(t, clt)
se, err := clt.NewSession(ctx)
require.NoError(t, err)
require.NoError(t, se.Shell(ctx))
// current connections = 2
_, err = apissh.Dial(ctx, "tcp", srv.Addr(), config)
require.Error(t, err)
require.NoError(t, se.Close())
se.Wait()
require.NoError(t, clt.Close())
require.ErrorIs(t, clt.Wait(), net.ErrClosed)
require.Eventually(t, getConns(t, limiter, "127.0.0.1", 1), time.Second*10, time.Millisecond*100)
// current connections = 1
clt, err = apissh.Dial(ctx, "tcp", srv.Addr(), config)
require.NoError(t, err)
require.NotNil(t, clt)
se, err = clt.NewSession(ctx)
require.NoError(t, err)
require.NoError(t, se.Shell(ctx))
// current connections = 2
_, err = apissh.Dial(ctx, "tcp", srv.Addr(), config)
require.Error(t, err)
require.NoError(t, se.Close())
se.Wait()
require.NoError(t, clt.Close())
require.ErrorIs(t, clt.Wait(), net.ErrClosed)
require.Eventually(t, getConns(t, limiter, "127.0.0.1", 1), time.Second*10, time.Millisecond*100)
// current connections = 1
// requests rate should exceed now
clt, err = apissh.Dial(ctx, "tcp", srv.Addr(), config)
require.NoError(t, err)
require.NotNil(t, clt)
_, err = clt.NewSession(ctx)
require.Error(t, err)
clt.Close()
}
// TestServerAliveInterval simulates ServerAliveInterval and OpenSSH
// interoperability by sending a [email protected] global request to the
// server and expecting a response in return.
func TestServerAliveInterval(t *testing.T) {
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
ok, _, err := f.ssh.clt.SendRequest(t.Context(), teleport.KeepAliveReqType, true, nil)
require.NoError(t, err)
require.True(t, ok)
}
// TestGlobalRequestClusterDetails simulates sending a global out-of-band
// [email protected] request.
func TestGlobalRequestClusterDetails(t *testing.T) {
t.Parallel()
ctx := t.Context()
cases := []struct {
name string
fips bool
mode string
expected sshutils.ClusterDetails
}{
{
name: "node recording and fips",
fips: true,
mode: types.RecordAtNode,
expected: sshutils.ClusterDetails{
RecordingProxy: false,
FIPSEnabled: true,
},
},
{
name: "node recording and not fips",
fips: false,
mode: types.RecordAtNode,
expected: sshutils.ClusterDetails{
RecordingProxy: false,
FIPSEnabled: false,
},
},
{
name: "proxy recording and fips",
fips: true,
mode: types.RecordAtProxy,
expected: sshutils.ClusterDetails{
RecordingProxy: true,
FIPSEnabled: true,
},
},
{
name: "proxy recording and not fips",
fips: false,
mode: types.RecordAtProxy,
expected: sshutils.ClusterDetails{
RecordingProxy: true,
FIPSEnabled: false,
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
f := newCustomFixture(t, func(*authtest.ServerConfig) {}, SetFIPS(tt.fips))
recConfig, err := types.NewSessionRecordingConfigFromConfigFile(types.SessionRecordingConfigSpecV2{Mode: tt.mode})
require.NoError(t, err)
_, err = f.testSrv.Auth().UpsertSessionRecordingConfig(ctx, recConfig)
require.NoError(t, err)
ok, responseBytes, err := f.ssh.clt.SendRequest(ctx, teleport.ClusterDetailsReqType, true, nil)
require.NoError(t, err)
require.True(t, ok)
var details sshutils.ClusterDetails
require.NoError(t, ssh.Unmarshal(responseBytes, &details))
require.Empty(t, cmp.Diff(tt.expected, details))
})
}
}
// rawNode is a basic non-teleport node which holds a
// valid teleport cert and allows any client to connect.
// useful for simulating basic behaviors of openssh nodes.
type rawNode struct {
listener net.Listener
cfg ssh.ServerConfig
addr string
errCh chan error
}
// accept an incoming connection and perform a basic ssh handshake
func (r *rawNode) accept() (*ssh.ServerConn, <-chan ssh.NewChannel, <-chan *ssh.Request, error) {
netConn, err := r.listener.Accept()
if err != nil {
return nil, nil, nil, trace.Wrap(err)
}
srvConn, chs, reqs, err := ssh.NewServerConn(netConn, &r.cfg)
if err != nil {
netConn.Close()
return nil, nil, nil, trace.Wrap(err)
}
return srvConn, chs, reqs, nil
}
func (r *rawNode) Close() error {
return trace.Wrap(r.listener.Close())
}
// newRawNode constructs a new raw node instance.
func newRawNode(t *testing.T, authSrv *auth.Server) *rawNode {
hostname, err := os.Hostname()
require.NoError(t, err)
priv, pub, err := testauthority.GenerateKeyPair()
require.NoError(t, err)
tlsPub, err := authtest.PrivateKeyToPublicKeyTLS(priv)
require.NoError(t, err)
// Create host key and certificate for node.
certs, err := authSrv.GenerateHostCerts(t.Context(),
auth.HostCertsParams{
Req: &proto.HostCertsRequest{
HostID: "raw-node",
NodeName: "raw-node",
Role: types.RoleNode,
AdditionalPrincipals: []string{hostname},
DNSNames: []string{hostname},
PublicSSHKey: pub,
PublicTLSKey: tlsPub,
},
})
require.NoError(t, err)
signer, err := sshutils.NewSigner(priv, certs.SSH)
require.NoError(t, err)
// configure a server which allows any client to connect
cfg := ssh.ServerConfig{
NoClientAuth: true,
PasswordCallback: func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
return &ssh.Permissions{}, nil
},
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
return &ssh.Permissions{}, nil
},
}
cfg.AddHostKey(signer)
listener, err := net.Listen("tcp", ":0")
require.NoError(t, err)
_, port, err := net.SplitHostPort(listener.Addr().String())
require.NoError(t, err)
addr := net.JoinHostPort(hostname, port)
return &rawNode{
listener: listener,
cfg: cfg,
addr: addr,
errCh: make(chan error),
}
}
// startX11EchoServer starts a fake node which, for each incoming SSH connection, accepts an
// X11 forwarding request and then dials a single X11 channel which echoes all bytes written
// to it. Used to verify the behavior of X11 forwarding in recording proxies. Returns a
// node and an error channel that can be monitored for asynchronous failures.
// x11Handler handles requests received by the x11 echo server
func x11Handler(ctx context.Context, conn *ssh.ServerConn, chs <-chan ssh.NewChannel) error {
// expect client to open a session channel
var nch ssh.NewChannel
select {
case nch = <-chs:
case <-time.After(time.Second * 3):
return trace.LimitExceeded("Timeout waiting for session channel")
case <-ctx.Done():
return nil
}
if nch.ChannelType() != teleport.ChanSession {
return trace.BadParameter("Unexpected channel type: %q", nch.ChannelType())
}
sch, creqs, err := nch.Accept()
if err != nil {
return trace.Wrap(err)
}
defer sch.Close()
// expect client to send an X11 forwarding request
var req *ssh.Request
select {
case req = <-creqs:
case <-time.After(time.Second * 3):
return trace.LimitExceeded("Timeout waiting for X11 forwarding request")
case <-ctx.Done():
return nil
}
if req.Type != x11.ForwardRequest {
return trace.BadParameter("Unexpected request type %q", req.Type)
}
if err = req.Reply(true, nil); err != nil {
return trace.Wrap(err)
}
// start a fake X11 channel
xch, _, err := conn.OpenChannel(x11.ChannelRequest, nil)
if err != nil {
return trace.Wrap(err)
}
defer xch.Close()
// echo all bytes back across the X11 channel
_, err = io.Copy(xch, xch)
if err != nil {
return trace.Wrap(err)
}
return trace.Wrap(xch.CloseWrite())
}
// startX11EchoServer starts a fake node which, for each incoming SSH connection, accepts an
// X11 forwarding request and then dials a single X11 channel which echoes all bytes written
// to it. Used to verify the behavior of X11 forwarding in recording proxies. Returns a
// node and an error channel that can be monitored for asynchronous failures.
func startX11EchoServer(ctx context.Context, t *testing.T, authSrv *auth.Server) (*rawNode, <-chan error) {
node := newRawNode(t, authSrv)
errorCh := make(chan error, 1)
go func() {
for {
conn, chs, _, err := node.accept()
if err != nil {
return
}
go func() {
if err := x11Handler(ctx, conn, chs); err != nil {
errorCh <- err
}
conn.Close()
}()
}
}()
return node, errorCh
}
// startGatheringErrors starts a goroutine that pulls error values from a
// channel and aggregates them. Returns a channel where the goroutine will post
// the aggregated errors when the routine is stopped.
func startGatheringErrors(ctx context.Context, errCh <-chan error) <-chan []error {
doneGathering := make(chan []error)
go func() {
errors := []error{}
for {
select {
case err := <-errCh:
errors = append(errors, err)
case <-ctx.Done():
doneGathering <- errors
return
}
}
}()
return doneGathering
}
// requireNoErrors waits for any aggregated errors to appear on the supplied channel
// and asserts that the aggregation is empty
func requireNoErrors(t *testing.T, errsCh <-chan []error) {
timeoutCtx, cancel := context.WithTimeout(t.Context(), 1*time.Second)
defer cancel()
select {
case errs := <-errsCh:
require.Empty(t, errs)
case <-timeoutCtx.Done():
require.Fail(t, "Timed out waiting for errors")
}
}
// TestParseSubsystemRequest verifies parseSubsystemRequest accepts the correct subsystems in depending on the runtime configuration.
func TestParseSubsystemRequest(t *testing.T) {
ctx := t.Context()
// start a listener to accept connections; this will be needed for the proxy test to pass, otherwise nothing will be there to handle the call.
agentlessListener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
t.Cleanup(func() { _ = agentlessListener.Close() })
go func() {
for {
// accept connections, but don't do anything else except for closing the connection on cleanup.
conn, err := agentlessListener.Accept()
if err != nil {
return
}
t.Cleanup(func() {
_ = conn.Close()
})
}
}()
agentlessSrv := types.ServerV2{
Kind: types.KindNode,
SubKind: types.SubKindOpenSSHNode,
Version: types.V2,
Metadata: types.Metadata{
Name: uuid.NewString(),
},
Spec: types.ServerSpecV2{
Addr: agentlessListener.Addr().String(),
Hostname: "agentless",
},
}
getNonProxySession := func() func() *tracessh.Session {
f := newFixtureWithoutDiskBasedLogging(t, SetAllowFileCopying(true))
return func() *tracessh.Session {
se, err := f.ssh.clt.NewSessionWithParams(t.Context(), nil)
require.NoError(t, err)
t.Cleanup(func() { _ = se.Close() })
return se
}
}()
getProxySession := func() func() *tracessh.Session {
f := newFixtureWithoutDiskBasedLogging(t)
listener, _ := mustListen(t)
proxyClient, _ := newProxyClient(t, f.testSrv)
lockWatcher := newLockWatcher(ctx, t, proxyClient)
nodeWatcher := newNodeWatcher(ctx, t, proxyClient)
caWatcher := newCertAuthorityWatcher(ctx, t, proxyClient)
reverseTunnelServer, err := reversetunnel.NewServer(reversetunnel.Config{
GetClientTLSCertificate: func() (*tls.Certificate, error) {
return &proxyClient.TLSConfig().Certificates[0], nil
},
ID: hostID,
ClusterName: f.testSrv.ClusterName(),
Listener: listener,
GetHostSigners: sshutils.StaticHostSigners(f.signer),
LocalAuthClient: proxyClient,
LocalAccessPoint: proxyClient,
NewCachingAccessPoint: noCache,
DataDir: t.TempDir(),
Emitter: proxyClient,
LockWatcher: lockWatcher,
NodeWatcher: nodeWatcher,
GitServerWatcher: newGitServerWatcher(ctx, t, proxyClient),
AppServerWatcher: newAppServerWatcher(ctx, t, proxyClient),
DatabaseServerWatcher: newDatabaseServerWatcher(ctx, t, proxyClient),
CertAuthorityWatcher: caWatcher,
EICESigner: func(ctx context.Context, target types.Server, integration types.Integration, login, token string, ap cryptosuites.AuthPreferenceGetter) (ssh.Signer, error) {
return nil, errors.New("eice disabled in tests")
},
EICEDialer: func(ctx context.Context, target types.Server, integration types.Integration, token string) (net.Conn, error) {
return nil, errors.New("eice disabled in tests")
},
})
require.NoError(t, err)
require.NoError(t, reverseTunnelServer.Start())
t.Cleanup(func() { _ = reverseTunnelServer.Close() })
nodeClient, _ := newNodeClient(t, f.testSrv)
_, err = nodeClient.UpsertNode(ctx, &agentlessSrv)
require.NoError(t, err)
router, err := libproxy.NewRouter(libproxy.RouterConfig{
ClusterName: f.testSrv.ClusterName(),
LocalAccessPoint: proxyClient,
ClusterGetter: reverseTunnelServer,
TracerProvider: tracing.NoopProvider(),
})
require.NoError(t, err)
sessionController, err := srv.NewSessionController(srv.SessionControllerConfig{
Semaphores: proxyClient,
AccessPoint: proxyClient,
LockEnforcer: lockWatcher,
Emitter: proxyClient,
Component: teleport.ComponentProxy,
ServerID: hostID,
})
require.NoError(t, err)
proxy, err := New(
ctx,
utils.NetAddr{AddrNetwork: "tcp", Addr: "localhost:0"},
f.testSrv.ClusterName(),
sshutils.StaticHostSigners(f.signer),
proxyClient,
t.TempDir(),
"",
utils.NetAddr{},
proxyClient,
SetUUID(uuid.NewString()),
SetProxyMode("", reverseTunnelServer, proxyClient, router),
SetEmitter(nodeClient),
SetNamespace(apidefaults.Namespace),
SetPAMConfig(&pamcfg.PAMConfig{Enabled: false}),
SetBPF(&bpf.NOP{}),
SetClock(f.clock),
SetLockWatcher(lockWatcher),
SetSessionController(sessionController),
SetConnectedProxyGetter(reversetunnel.NewConnectedProxyGetter()),
setChildLogConfigForTest(),
)
require.NoError(t, err)
require.NoError(t, proxy.Start())
t.Cleanup(func() { _ = proxy.Close() })
return func() *tracessh.Session {
// Connect SSH client to proxy
client, err := apissh.Dial(ctx, "tcp", proxy.Addr(), f.SSHClientConfig())
require.NoError(t, err)
t.Cleanup(func() { _ = client.Close() })
se, err := client.NewSession(ctx)
require.NoError(t, err)
return se
}
}()
tests := []struct {
name string
subsystemOverride string
wantErrInProxyMode bool
wantErrInRegularMode bool
}{
{
name: "invalid",
wantErrInProxyMode: true,
wantErrInRegularMode: true,
},
{
name: teleport.SFTPSubsystem,
wantErrInProxyMode: true,
wantErrInRegularMode: false,
},
{
name: "gethomedir",
wantErrInProxyMode: true,
wantErrInRegularMode: true,
},
{
name: "proxysites",
wantErrInProxyMode: false,
wantErrInRegularMode: true,
},
{
name: "proxy:agentlessServer",
subsystemOverride: "proxy:" + agentlessSrv.Spec.Addr,
wantErrInProxyMode: false,
wantErrInRegularMode: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
subsystem := tt.name
if tt.subsystemOverride != "" {
subsystem = tt.subsystemOverride
}
err := getProxySession().RequestSubsystem(ctx, subsystem)
if tt.wantErrInProxyMode {
require.Error(t, err)
} else {
require.NoError(t, err)
}
err = getNonProxySession().RequestSubsystem(ctx, subsystem)
if tt.wantErrInRegularMode {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
// TestX11ProxySupport verifies that recording proxies correctly forward
// X11 request/channels.
func TestX11ProxySupport(t *testing.T) {
t.Parallel()
f := newFixture(t)
ctx := t.Context()
// set cluster config to record at the proxy
recConfig, err := types.NewSessionRecordingConfigFromConfigFile(types.SessionRecordingConfigSpecV2{
Mode: types.RecordAtProxy,
})
require.NoError(t, err)
_, err = f.testSrv.Auth().UpsertSessionRecordingConfig(ctx, recConfig)
require.NoError(t, err)
// verify that the proxy is in recording mode
ok, responseBytes, err := f.ssh.clt.SendRequest(ctx, teleport.ClusterDetailsReqType, true, nil)
require.NoError(t, err)
require.True(t, ok)
var details sshutils.ClusterDetails
require.NoError(t, ssh.Unmarshal(responseBytes, &details))
require.NoError(t, err)
require.True(t, details.RecordingProxy)
// setup our fake X11 echo server.
x11Ctx, x11Cancel := context.WithCancel(ctx)
node, errCh := startX11EchoServer(x11Ctx, t, f.testSrv.Auth())
// start gathering errors from the X11 server
doneGathering := startGatheringErrors(x11Ctx, errCh)
defer requireNoErrors(t, doneGathering)
// The error gathering routine needs this context to expire or it will wait
// forever on the X11 server to exit. Hence we defer a call to the x11cancel
// here rather than directly below the context creation
defer x11Cancel()
// Create a direct TCP/IP connection from proxy to our X11 test server.
netConn, err := f.ssh.clt.DialContext(x11Ctx, "tcp", node.addr)
require.NoError(t, err)
defer netConn.Close()
// make an insecure version of our client config (this test is only about X11 forwarding,
// so we don't bother to verify recording proxy key generation here).
cltConfig := f.ssh.cltConfig
cltConfig.HostKeyCallback = ssh.InsecureIgnoreHostKey()
// Perform ssh handshake and setup client for X11 test server.
cltConn, chs, reqs, err := apissh.NewClientConn(ctx, netConn, node.addr, cltConfig)
require.NoError(t, err)
clt := tracessh.NewClient(cltConn, chs, reqs)
sess, err := clt.NewSession(ctx)
require.NoError(t, err)
// register X11 channel handler before requesting forwarding to avoid races
xchs := clt.HandleChannelOpen(x11.ChannelRequest)
require.NotNil(t, xchs)
// Send an X11 forwarding request to the server
ok, err = sess.SendRequest(ctx, x11.ForwardRequest, true, nil)
require.NoError(t, err)
require.True(t, ok)
// wait for server to start an X11 channel
var xnc ssh.NewChannel
select {
case xnc = <-xchs:
case <-time.After(time.Second * 3):
require.Fail(t, "Timeout waiting for X11 channel open from %v", node.addr)
}
require.NotNil(t, xnc)
require.Equal(t, x11.ChannelRequest, xnc.ChannelType())
xch, _, err := xnc.Accept()
require.NoError(t, err)
defer xch.Close()
// write some data to the channel
msg := []byte("testing!")
_, err = xch.Write(msg)
require.NoError(t, err)
// send EOF
require.NoError(t, xch.CloseWrite())
// expect node to successfully echo the data
rsp := make([]byte, len(msg))
_, err = io.ReadFull(xch, rsp)
require.NoError(t, err)
require.Equal(t, string(msg), string(rsp))
}
// TestIgnorePuTTYSimpleChannel verifies that any request from the PuTTY SSH client for
// its "simple" mode is ignored and connections remain open.
func TestIgnorePuTTYSimpleChannel(t *testing.T) {
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
ctx := t.Context()
listener, _ := mustListen(t)
proxyClient, _ := newProxyClient(t, f.testSrv)
lockWatcher := newLockWatcher(ctx, t, proxyClient)
nodeWatcher := newNodeWatcher(ctx, t, proxyClient)
caWatcher := newCertAuthorityWatcher(ctx, t, proxyClient)
reverseTunnelServer, err := reversetunnel.NewServer(reversetunnel.Config{
GetClientTLSCertificate: func() (*tls.Certificate, error) {
return &proxyClient.TLSConfig().Certificates[0], nil
},
ID: hostID,
ClusterName: f.testSrv.ClusterName(),
Listener: listener,
GetHostSigners: sshutils.StaticHostSigners(f.signer),
LocalAuthClient: proxyClient,
LocalAccessPoint: proxyClient,
NewCachingAccessPoint: noCache,
DataDir: t.TempDir(),
Emitter: proxyClient,
LockWatcher: lockWatcher,
NodeWatcher: nodeWatcher,
GitServerWatcher: newGitServerWatcher(ctx, t, proxyClient),
AppServerWatcher: newAppServerWatcher(ctx, t, proxyClient),
DatabaseServerWatcher: newDatabaseServerWatcher(ctx, t, proxyClient),
CertAuthorityWatcher: caWatcher,
EICESigner: func(ctx context.Context, target types.Server, integration types.Integration, login, token string, ap cryptosuites.AuthPreferenceGetter) (ssh.Signer, error) {
return nil, errors.New("eice disabled in tests")
},
EICEDialer: func(ctx context.Context, target types.Server, integration types.Integration, token string) (net.Conn, error) {
return nil, errors.New("eice disabled in tests")
},
})
require.NoError(t, err)
require.NoError(t, reverseTunnelServer.Start())
defer reverseTunnelServer.Close()
nodeClient, _ := newNodeClient(t, f.testSrv)
router, err := libproxy.NewRouter(libproxy.RouterConfig{
ClusterName: f.testSrv.ClusterName(),
LocalAccessPoint: proxyClient,
ClusterGetter: reverseTunnelServer,
TracerProvider: tracing.NoopProvider(),
})
require.NoError(t, err)
sessionController, err := srv.NewSessionController(srv.SessionControllerConfig{
Semaphores: proxyClient,
AccessPoint: proxyClient,
LockEnforcer: lockWatcher,
Emitter: proxyClient,
Component: teleport.ComponentProxy,
ServerID: hostID,
})
require.NoError(t, err)
proxy, err := New(
ctx,
utils.NetAddr{AddrNetwork: "tcp", Addr: "localhost:0"},
f.testSrv.ClusterName(),
sshutils.StaticHostSigners(f.signer),
proxyClient,
t.TempDir(),
"",
utils.NetAddr{},
proxyClient,
SetUUID(uuid.NewString()),
SetProxyMode("", reverseTunnelServer, proxyClient, router),
SetEmitter(nodeClient),
SetNamespace(apidefaults.Namespace),
SetPAMConfig(&pamcfg.PAMConfig{Enabled: false}),
SetBPF(&bpf.NOP{}),
SetClock(f.clock),
SetLockWatcher(lockWatcher),
SetSessionController(sessionController),
SetConnectedProxyGetter(reversetunnel.NewConnectedProxyGetter()),
setChildLogConfigForTest(),
)
require.NoError(t, err)
require.NoError(t, proxy.Start())
defer proxy.Close()
// Connect SSH client to proxy
client, err := apissh.Dial(ctx, "tcp", proxy.Addr(), f.SSHClientConfig())
require.NoError(t, err)
defer client.Close()
se, err := client.NewSession(ctx)
require.NoError(t, err)
defer se.Close()
writer, err := se.StdinPipe()
require.NoError(t, err)
reader, err := se.StdoutPipe()
require.NoError(t, err)
// Request the PuTTY-specific "[email protected]" channel type
// This request should be ignored, and the connection should remain open.
_, err = se.SendRequest(ctx, sshutils.PuTTYSimpleRequest, false, []byte{})
require.NoError(t, err)
// Request proxy subsystem routing TCP connection to the remote host
require.NoError(t, se.RequestSubsystem(ctx, fmt.Sprintf("proxy:%v", f.ssh.srvAddress)))
local, err := utils.ParseAddr("tcp://" + proxy.Addr())
require.NoError(t, err)
remote, err := utils.ParseAddr("tcp://" + f.ssh.srv.Addr())
require.NoError(t, err)
pipeNetConn := utils.NewPipeNetConn(
reader,
writer,
se,
local,
remote,
)
defer pipeNetConn.Close()
// Run commands over this connection like regular SSH
client2, err := apissh.NewClient(t.Context(), pipeNetConn, f.ssh.srv.Addr(), f.SSHClientConfig())
require.NoError(t, err)
defer client2.Close()
se2, err := client2.NewSession(ctx)
require.NoError(t, err)
defer se2.Close()
out, err := se2.Output(ctx, "echo hello again")
require.NoError(t, err)
require.Equal(t, "hello again\n", string(out))
}
// TestHandlePuTTYWinadj verifies that any request from the PuTTY SSH client for its "winadj"
// channel is correctly responded to with a failure message and connections remain open.
func TestHandlePuTTYWinadj(t *testing.T) {
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
ctx := t.Context()
se, err := f.ssh.clt.NewSession(ctx)
require.NoError(t, err)
t.Cleanup(func() { se.Close() })
// send a PuTTY winadj request to the server. it shouldn't error, but the response
// should be a failure.
ok, err := se.SendRequest(ctx, sshutils.PuTTYWinadjRequest, true, nil)
require.NoError(t, err)
require.False(t, ok)
// echo something to make sure the connection is still alive following the request
out, err := se.Output(ctx, "echo hello once more")
require.NoError(t, err)
require.Equal(t, "hello once more\n", string(out))
}
func TestEventMetadata(t *testing.T) {
ctx := t.Context()
testServer, err := authtest.NewTestServer(authtest.ServerConfig{
Auth: authtest.AuthServerConfig{
ClusterName: "localhost",
Dir: t.TempDir(),
Clock: clockwork.NewFakeClock(),
},
})
require.NoError(t, err)
nodeID := uuid.New().String()
nodeClient, err := testServer.NewClient(authtest.TestIdentity{
I: authz.BuiltinRole{
Role: types.RoleNode,
Username: nodeID,
},
})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, nodeClient.Close()) })
lockWatcher := newLockWatcher(ctx, t, nodeClient)
sessionController, err := srv.NewSessionController(srv.SessionControllerConfig{
Semaphores: nodeClient,
AccessPoint: nodeClient,
LockEnforcer: lockWatcher,
Emitter: nodeClient,
Component: teleport.ComponentNode,
ServerID: nodeID,
})
require.NoError(t, err)
nodeDir := t.TempDir()
serverOptions := []ServerOption{
SetUUID(nodeID),
SetNamespace(apidefaults.Namespace),
SetEmitter(nodeClient),
SetPAMConfig(&pamcfg.PAMConfig{Enabled: false}),
SetLabels(
map[string]string{"foo": "bar"},
services.CommandLabels{
"baz": &types.CommandLabelV2{
Period: types.NewDuration(time.Second),
Command: []string{"expr", "1", "+", "3"},
},
}, nil,
),
SetBPF(&bpf.NOP{}),
SetLockWatcher(lockWatcher),
SetX11ForwardingConfig(&x11.ServerConfig{}),
SetSessionController(sessionController),
SetConnectedProxyGetter(reversetunnel.NewConnectedProxyGetter()),
setChildLogConfigForTest(),
}
sshSrv, err := New(
ctx,
utils.NetAddr{AddrNetwork: "tcp", Addr: "127.0.0.1:0"},
testServer.ClusterName(),
sshutils.StaticHostSigners(newSigner(t, ctx, testServer)),
nodeClient,
nodeDir,
"",
utils.NetAddr{},
nodeClient,
serverOptions...)
require.NoError(t, err)
metadata := sshSrv.EventMetadata()
require.Equal(t, nodeID, metadata.ServerID)
require.Equal(t, apidefaults.Namespace, metadata.ServerNamespace)
require.Empty(t, metadata.ServerAddr)
require.Equal(t, "localhost", metadata.ServerHostname)
require.Contains(t, metadata.ServerLabels, "foo")
require.Contains(t, metadata.ServerLabels, "baz")
}
// upack holds all ssh signing artifacts needed for signing and checking user keys
type upack struct {
// key is a raw private user key
key []byte
// pkey is parsed private SSH key
pkey any
// pub is a public user key
pub []byte
// cert is a certificate signed by user CA
cert []byte
// pcert is a parsed ssh Certificae
pcert *ssh.Certificate
// signer is a signer that answers signing challenges using private key
signer ssh.Signer
// certSigner is a signer that answers signing challenges using private
// key and a certificate issued by user certificate authority
certSigner ssh.Signer
}
func newUpack(ctx context.Context, testSvr *authtest.Server, username string, allowedLogins []string, allowedLabels types.Labels) (*upack, error) {
auth := testSvr.Auth()
upriv, upub, err := testauthority.GenerateKeyPair()
if err != nil {
return nil, trace.Wrap(err)
}
user, err := types.NewUser(username)
if err != nil {
return nil, trace.Wrap(err)
}
role := services.RoleForUser(user)
rules := role.GetRules(types.Allow)
rules = append(rules, types.NewRule(types.Wildcard, services.RW()))
role.SetRules(types.Allow, rules)
opts := role.GetOptions()
opts.PermitX11Forwarding = types.NewBool(true)
//nolint:staticcheck // this field is preserved for existing deployments, but shouldn't be used going forward
opts.CreateHostUser = types.NewBoolOption(true)
role.SetOptions(opts)
role.SetLogins(types.Allow, allowedLogins)
role.SetNodeLabels(types.Allow, allowedLabels)
_, err = auth.UpsertRole(ctx, role)
if err != nil {
return nil, trace.Wrap(err)
}
user.AddRole(role.GetName())
user, err = auth.UpsertUser(ctx, user)
if err != nil {
return nil, trace.Wrap(err)
}
ucert, err := testSvr.AuthServer.GenerateUserCert(upub, user.GetName(), 5*time.Minute, constants.CertificateFormatStandard)
if err != nil {
return nil, trace.Wrap(err)
}
upkey, err := ssh.ParseRawPrivateKey(upriv)
if err != nil {
return nil, trace.Wrap(err)
}
usigner, err := ssh.NewSignerFromKey(upkey)
if err != nil {
return nil, trace.Wrap(err)
}
pcert, _, _, _, err := ssh.ParseAuthorizedKey(ucert)
if err != nil {
return nil, trace.Wrap(err)
}
ucertSigner, err := ssh.NewCertSigner(pcert.(*ssh.Certificate), usigner)
if err != nil {
return nil, trace.Wrap(err)
}
return &upack{
key: upriv,
pkey: upkey,
pub: upub,
cert: ucert,
pcert: pcert.(*ssh.Certificate),
signer: usigner,
certSigner: ucertSigner,
}, nil
}
func newLockWatcher(ctx context.Context, t testing.TB, client types.Events) *services.LockWatcher {
lockWatcher, err := services.NewLockWatcher(ctx, services.LockWatcherConfig{
ResourceWatcherConfig: services.ResourceWatcherConfig{
Component: "test",
Client: client,
},
})
require.NoError(t, err)
t.Cleanup(lockWatcher.Close)
return lockWatcher
}
func newNodeWatcher(ctx context.Context, t *testing.T, client *authclient.Client) *services.GenericWatcher[types.Server, readonly.Server] {
nodeWatcher, err := services.NewNodeWatcher(ctx, services.NodeWatcherConfig{
ResourceWatcherConfig: services.ResourceWatcherConfig{
Component: "test",
Client: client,
},
NodesGetter: client,
})
require.NoError(t, err)
t.Cleanup(nodeWatcher.Close)
return nodeWatcher
}
func newGitServerWatcher(ctx context.Context, t *testing.T, client *authclient.Client) *services.GenericWatcher[types.Server, readonly.Server] {
watcher, err := services.NewGitServerWatcher(ctx, services.GitServerWatcherConfig{
ResourceWatcherConfig: services.ResourceWatcherConfig{
Component: "test",
Client: client,
},
GitServerGetter: client.GitServerReadOnlyClient(),
})
require.NoError(t, err)
t.Cleanup(watcher.Close)
return watcher
}
func newCertAuthorityWatcher(ctx context.Context, t *testing.T, client types.Events) *services.CertAuthorityWatcher {
//nolint:staticcheck // SA1019 This should be updated to use [services.NewCertAuthorityWatcher]
caWatcher, err := services.DeprecatedNewCertAuthorityWatcher(ctx, services.CertAuthorityWatcherConfig{
ResourceWatcherConfig: services.ResourceWatcherConfig{
Component: "test",
Client: client,
},
Types: []types.CertAuthType{types.HostCA, types.UserCA},
})
require.NoError(t, err)
t.Cleanup(caWatcher.Close)
return caWatcher
}
func newAppServerWatcher(ctx context.Context, t *testing.T, client *authclient.Client) *services.GenericWatcher[types.AppServer, readonly.AppServer] {
t.Helper()
appServerWatcher, err := services.NewAppServersWatcher(ctx, services.AppServersWatcherConfig{
ResourceWatcherConfig: services.ResourceWatcherConfig{
Component: "test",
Client: client,
},
})
require.NoError(t, err)
t.Cleanup(appServerWatcher.Close)
return appServerWatcher
}
func newDatabaseServerWatcher(ctx context.Context, t *testing.T, client *authclient.Client) *services.GenericWatcher[types.DatabaseServer, readonly.DatabaseServer] {
t.Helper()
databaseServerWatcher, err := services.NewDatabaseServerWatcher(ctx, services.DatabaseServerWatcherConfig{
ResourceWatcherConfig: services.ResourceWatcherConfig{
Component: "test",
Client: client,
},
})
require.NoError(t, err)
t.Cleanup(databaseServerWatcher.Close)
return databaseServerWatcher
}
// newSigner creates a new SSH signer that can be used by the Server.
func newSigner(t testing.TB, ctx context.Context, testServer *authtest.Server) ssh.Signer {
t.Helper()
priv, pub, err := testauthority.GenerateKeyPair()
require.NoError(t, err)
tlsPub, err := authtest.PrivateKeyToPublicKeyTLS(priv)
require.NoError(t, err)
certs, err := testServer.Auth().GenerateHostCerts(ctx,
auth.HostCertsParams{
Req: &proto.HostCertsRequest{
HostID: hostID,
NodeName: testServer.ClusterName(),
Role: types.RoleNode,
PublicSSHKey: pub,
PublicTLSKey: tlsPub,
},
})
require.NoError(t, err)
// set up user CA and set up a user that has access to the server
signer, err := sshutils.NewSigner(priv, certs.SSH)
require.NoError(t, err)
return signer
}
// maxPipeSize is one larger than the maximum pipe size for most operating
// systems which appears to be 65536 bytes.
//
// The maximum pipe size for Linux could potentially be obtained, however
// getting it for macOS is much harder, and unclear if even possible. Therefor
// just hard code it.
//
// See the following links for more details.
//
// https://man7.org/linux/man-pages/man7/pipe.7.html
// https://github.com/afborchert/pipebuf
// https://unix.stackexchange.com/questions/11946/how-big-is-the-pipe-buffer
const maxPipeSize = 65536 + 1
func TestHostUserCreationProxy(t *testing.T) {
f := newFixtureWithoutDiskBasedLogging(t)
ctx := t.Context()
proxyClient, _ := newProxyClient(t, f.testSrv)
nodeClient, _ := newNodeClient(t, f.testSrv)
listener, _ := mustListen(t)
defer listener.Close()
lockWatcher := newLockWatcher(ctx, t, proxyClient)
nodeWatcher := newNodeWatcher(ctx, t, proxyClient)
caWatcher := newCertAuthorityWatcher(ctx, t, proxyClient)
reverseTunnelServer, err := reversetunnel.NewServer(reversetunnel.Config{
GetClientTLSCertificate: func() (*tls.Certificate, error) {
return &proxyClient.TLSConfig().Certificates[0], nil
},
ClusterName: f.testSrv.ClusterName(),
ID: hostID,
Listener: listener,
GetHostSigners: sshutils.StaticHostSigners(f.signer),
LocalAuthClient: proxyClient,
LocalAccessPoint: proxyClient,
NewCachingAccessPoint: noCache,
DataDir: t.TempDir(),
Emitter: proxyClient,
LockWatcher: lockWatcher,
NodeWatcher: nodeWatcher,
GitServerWatcher: newGitServerWatcher(ctx, t, proxyClient),
AppServerWatcher: newAppServerWatcher(ctx, t, proxyClient),
DatabaseServerWatcher: newDatabaseServerWatcher(ctx, t, proxyClient),
CertAuthorityWatcher: caWatcher,
CircuitBreakerConfig: breaker.NoopBreakerConfig(),
EICESigner: func(ctx context.Context, target types.Server, integration types.Integration, login, token string, ap cryptosuites.AuthPreferenceGetter) (ssh.Signer, error) {
return nil, errors.New("eice disabled in tests")
},
EICEDialer: func(ctx context.Context, target types.Server, integration types.Integration, token string) (net.Conn, error) {
return nil, errors.New("eice disabled in tests")
},
})
require.NoError(t, err)
require.NoError(t, reverseTunnelServer.Start())
defer reverseTunnelServer.Close()
router, err := libproxy.NewRouter(libproxy.RouterConfig{
ClusterName: f.testSrv.ClusterName(),
LocalAccessPoint: proxyClient,
ClusterGetter: reverseTunnelServer,
TracerProvider: tracing.NoopProvider(),
})
require.NoError(t, err)
sessionController, err := srv.NewSessionController(srv.SessionControllerConfig{
Semaphores: proxyClient,
AccessPoint: proxyClient,
LockEnforcer: lockWatcher,
Emitter: proxyClient,
Component: teleport.ComponentNode,
ServerID: hostID,
})
require.NoError(t, err)
proxy, err := New(
ctx,
utils.NetAddr{AddrNetwork: "tcp", Addr: "localhost:0"},
f.testSrv.ClusterName(),
sshutils.StaticHostSigners(f.signer),
proxyClient,
t.TempDir(),
"",
utils.NetAddr{},
proxyClient,
SetUUID(uuid.NewString()),
SetProxyMode("", reverseTunnelServer, proxyClient, router),
SetEmitter(nodeClient),
SetNamespace(apidefaults.Namespace),
SetPAMConfig(&pamcfg.PAMConfig{Enabled: false}),
SetBPF(&bpf.NOP{}),
SetClock(f.clock),
SetLockWatcher(lockWatcher),
SetSessionController(sessionController),
SetConnectedProxyGetter(reversetunnel.NewConnectedProxyGetter()),
setChildLogConfigForTest(),
)
require.NoError(t, err)
sudoers := &fakeHostSudoers{}
proxy.sudoers = sudoers
usersBackend := &fakeHostUsersBackend{}
proxy.users = usersBackend
// Explicitly enabled host user creation on the proxy, even though this
// should never happen, to test that the proxy will not honor this setting.
proxy.createHostUser = true
proxy.proxyMode = true
reg, err := srv.NewSessionRegistry(srv.SessionRegistryConfig{Srv: proxy, SessionTrackerService: proxyClient})
require.NoError(t, err)
_, err = reg.WriteSudoersFile(srv.IdentityContext{
AccessPermit: &decisionpb.SSHAccessPermit{
HostSudoers: []string{"test1", "test2", "test3"},
},
})
assert.NoError(t, err)
assert.Equal(t, 0, sudoers.writeAttempts)
_, _, err = reg.UpsertHostUser(srv.IdentityContext{
AccessPermit: &decisionpb.SSHAccessPermit{
HostSudoers: []string{"test1", "test2", "test3"},
},
}, srv.ObtainFallbackUIDFunc(nil))
assert.NoError(t, err)
assert.Empty(t, usersBackend.calls, 0)
}
func TestObtainFallbackUID(t *testing.T) {
t.Parallel()
ctx := t.Context()
type testCase struct {
config *types.StableUNIXUserConfig
obtainFunc func(username string) (int32, error)
username string
required func(uid int32, ok bool, err error)
}
for _, tc := range []testCase{{
config: nil,
obtainFunc: func(username string) (int32, error) {
require.FailNow(t, "called obtainFunc")
panic("unreachable")
},
username: "",
required: func(uid int32, ok bool, err error) {
require.NoError(t, err)
require.False(t, ok)
},
}, {
config: &types.StableUNIXUserConfig{
Enabled: false,
},
obtainFunc: func(username string) (int32, error) {
require.FailNow(t, "called obtainFunc")
panic("unreachable")
},
username: "",
required: func(uid int32, ok bool, err error) {
require.NoError(t, err)
require.False(t, ok)
},
}, {
config: &types.StableUNIXUserConfig{
Enabled: true,
},
obtainFunc: func(username string) (int32, error) {
require.Equal(t, "foo", username)
return 325872, nil
},
username: "foo",
required: func(uid int32, ok bool, err error) {
require.NoError(t, err)
require.True(t, ok)
require.Equal(t, int32(325872), uid)
},
}, {
config: &types.StableUNIXUserConfig{
Enabled: true,
},
obtainFunc: func(username string) (int32, error) {
require.Equal(t, "bar", username)
return 0, trace.LimitExceeded("no UIDs or something")
},
username: "bar",
required: func(uid int32, ok bool, err error) {
require.ErrorIs(t, err, &trace.LimitExceededError{Message: "no UIDs or something"})
},
}, {
config: &types.StableUNIXUserConfig{
Enabled: true,
},
obtainFunc: func(username string) (int32, error) {
require.Equal(t, "baz", username)
return 0, trace.BadParameter("some bug, idk")
},
username: "baz",
required: func(uid int32, ok bool, err error) {
require.ErrorIs(t, err, &trace.BadParameterError{Message: "some bug, idk"})
},
}} {
srv := &Server{
authService: getAuthPreferenceAccessPoint{
authPreference: &types.AuthPreferenceV2{Spec: types.AuthPreferenceSpecV2{
StableUnixUserConfig: tc.config,
}},
},
stableUnixUsers: obtainUIDForUsernameFunc(tc.obtainFunc),
}
tc.required(srv.obtainFallbackUID(ctx, tc.username))
}
}
type getAuthPreferenceAccessPoint struct {
srv.AccessPoint
authPreference types.AuthPreference
}
func (a getAuthPreferenceAccessPoint) GetAuthPreference(ctx context.Context) (types.AuthPreference, error) {
return a.authPreference, nil
}
type obtainUIDForUsernameFunc func(username string) (int32, error)
func (f obtainUIDForUsernameFunc) ObtainUIDForUsername(ctx context.Context, in *stableunixusersv1.ObtainUIDForUsernameRequest, opts ...grpc.CallOption) (*stableunixusersv1.ObtainUIDForUsernameResponse, error) {
uid, err := f(in.GetUsername())
if err != nil {
return nil, trace.Wrap(err)
}
return &stableunixusersv1.ObtainUIDForUsernameResponse{Uid: uid}, nil
}
func (f obtainUIDForUsernameFunc) ListStableUNIXUsers(ctx context.Context, in *stableunixusersv1.ListStableUNIXUsersRequest, opts ...grpc.CallOption) (*stableunixusersv1.ListStableUNIXUsersResponse, error) {
return nil, trace.NotImplemented("ListStableUNIXUsers")
}
type fakeHostSudoers struct {
writeAttempts int
}
func (f *fakeHostSudoers) WriteSudoers(name string, sudoers []string) error {
f.writeAttempts++
return nil
}
func (f *fakeHostSudoers) RemoveSudoers(name string) error {
return nil
}
type fakeHostUsersBackend struct {
srv.HostUsers
calls map[string]int
}
func (f *fakeHostUsersBackend) functionCalled(name string) {
if f.calls == nil {
f.calls = make(map[string]int)
}
f.calls[name]++
}
func (f *fakeHostUsersBackend) UpsertUser(name string, hostRoleInfo *decisionpb.HostUsersInfo, opts ...srv.UpsertHostUserOption) (io.Closer, error) {
f.functionCalled("UpsertUser")
return nil, trace.NotImplemented("")
}
func (f *fakeHostUsersBackend) DeleteUser(name, gid string) error {
f.functionCalled("DeleteUser")
return trace.NotImplemented("")
}
func (f *fakeHostUsersBackend) DeleteAllUsers() error {
f.functionCalled("DeleteAllUsers")
return trace.NotImplemented("")
}
func (f *fakeHostUsersBackend) UserCleanup() {
f.functionCalled("UserCleanup")
}
func (f *fakeHostUsersBackend) Shutdown() {
f.functionCalled("ShutDown")
}
func (f *fakeHostUsersBackend) UserExists(name string) error {
f.functionCalled("UserExists")
return trace.NotImplemented("")
}
func (f *fakeHostUsersBackend) SetHostUserDeletionGrace(grace time.Duration) {
f.functionCalled("SetHostUserDeletionGrace")
}
func TestSessionParams(t *testing.T) {
f := newFixtureWithoutDiskBasedLogging(t)
ctx := t.Context()
// Start one session that can be used to test joining in the test cases below.
se, err := f.ssh.clt.NewSession(ctx)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, se.Close()) })
require.NoError(t, se.Shell(ctx))
sessions, err := f.ssh.srv.termHandlers.SessionRegistry.SessionTrackerService.GetActiveSessionTrackers(ctx)
require.NoError(t, err)
require.Len(t, sessions, 1)
joinSID := sessions[0].GetSessionID()
for _, baseCase := range []struct {
name string
params *tracessh.SessionParams
}{
{
name: "new session",
params: &tracessh.SessionParams{
Reason: "For science.",
Invited: []string{"Esqueleto"},
},
}, {
name: "join session",
params: &tracessh.SessionParams{
JoinSessionID: joinSID,
JoinMode: types.SessionObserverMode,
},
},
} {
t.Run(baseCase.name, func(t *testing.T) {
envVars := map[string]string{
teleport.EnvSSHSessionReason: baseCase.params.Reason,
sshutils.SessionEnvVar: baseCase.params.JoinSessionID,
teleport.EnvSSHJoinMode: string(baseCase.params.JoinMode),
}
if baseCase.params.Invited != nil {
invitedJSON, err := json.Marshal(baseCase.params.Invited)
require.NoError(t, err)
envVars[teleport.EnvSSHSessionInvited] = string(invitedJSON)
}
for _, sessionCase := range []struct {
name string
params *tracessh.SessionParams
envVars map[string]string
}{
{
// TODO(Joerger): DELETE IN v20.0.0 - params are needed to join a session.
name: "env vars only", // v18- client
params: nil,
envVars: envVars,
}, {
// TODO(Joerger): DELETE IN v20.0.0 - only params are needed to join a session.
name: "params and env vars", // v19 client
params: baseCase.params,
envVars: envVars,
}, {
name: "params only", // v20+ client
params: baseCase.params,
},
} {
t.Run(sessionCase.name, func(t *testing.T) {
sidC := make(chan string)
err := f.ssh.clt.HandleSessionRequest(ctx, teleport.CurrentSessionIDRequest, func(ctx context.Context, req *ssh.Request) {
sidC <- string(req.Payload)
})
require.NoError(t, err)
se, err := f.ssh.clt.NewSessionWithParams(ctx, sessionCase.params)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, se.Close()) })
// If this isn't a join session, wait for the server to send the session ID.
sid := baseCase.params.JoinSessionID
if sid == "" {
select {
case sid = <-sidC:
case <-time.After(time.Second):
t.Fatalf("Failed to received session ID from server")
}
}
if sessionCase.envVars != nil {
err := se.SetEnvs(ctx, sessionCase.envVars)
require.NoError(t, err)
}
require.NoError(t, se.Shell(ctx))
sessionTracker, err := f.ssh.srv.termHandlers.SessionRegistry.SessionTrackerService.GetSessionTracker(ctx, sid)
require.NoError(t, err)
require.Equal(t, baseCase.params.Invited, sessionTracker.GetInvited())
require.Equal(t, baseCase.params.Reason, sessionTracker.GetReason())
participants := sessionTracker.GetParticipants()
if baseCase.params.JoinSessionID != "" {
require.Equal(t, baseCase.params.JoinSessionID, sid, "Expected to join session %q, but got a new session ID instead", baseCase.params.JoinSessionID)
require.Len(t, participants, 2)
require.Equal(t, string(baseCase.params.JoinMode), participants[1].Mode)
} else {
require.Len(t, participants, 1)
require.Equal(t, string(types.SessionPeerMode), participants[0].Mode)
}
})
}
})
}
}
func TestServerInfo(t *testing.T) {
scope := "/aa"
f := newFixtureWithoutDiskBasedLogging(t, SetScope(scope))
require.Equal(t, f.ssh.srv.scope, scope)
info, err := f.ssh.srv.getServerInfo(t.Context())
require.NoError(t, err)
require.Equal(t, scope, info.Scope)
}