Add teleport networking subprocess for port/agent/x11 forwarding (#43756)

* Add networking subprocess for port and agent forwarding.

* Remove extraneous x11 forwarding logic.

* Add x11 forwarding to the networking subprocess.

* Fix lint.

* Try create host user before starting networking process.

* Resolve comments

* Fix networking process memory leak.

* Run networking subprocess as root but change to user login after PAM call.

* Update agent forwarding socket dir permissions.

* Handle networking requests synchronously to maintain the current PAM context.

* Use user environment in networking process.

* Fix issue with PAM thread state being locked to the main goroutine; Clean up homeDir logic; Fix PAM thread state for xauth commands.

* Selectively handle some networking requests in goroutines.

* Address comments.

* Use an interrupt signal to trigger graceful exit in the networking subprocess.

* Cleanup unix sockets from the child namespace.

* Address comments.

* Remove extraneous network forwarding socket validation.

* Fix tests and lint.

* Re-add networking process done channel to avoid deadlocks on reading unix sockets passed to a dead process.

* Remove broken test.

* Address comments.

* Cleanup; remove unused child error file.

* - Explicility list file paths to clean up at the end of the networking process.
- Use terminate FD instead of interrupt to signal the process to exit.
- Log errors from the parent process.
- Read networking request response without goroutine.

* Cleanup.

* Make request socket a stream.

* Remove extraneious Chmod.

* Send request level error to request conn.

* Don't unlink unix sockets from the parent process.

* Read full error message from stream.

* Remove remaining logs in child process.

* Fix typos.

* web: support SAML resource deletion in unified resources view (#44311)

* update SAML app edit mechanism to accomodate both edit and delete functions

* use user saml idp access to disable menu actions

* add comment to ResourceActionButton props

* refactor: SamlAppActionContext to edit and delete Saml application

* address review comments

* Add networking process tests to replace old tests.

* Don't close remote file descriptor before the child process has a chance to connect to it.

* Fix lint; skip broken test.

* Disable broken test and restore older test to cover for it.

* Remove unused agent forwarding test.

* Fix lint, replace broken test.

* Fix old test that used testify/require in goroutine.

* Close request context immediately to avoid deadlock (extended timeout) on reading from the closed process.

---------

Co-authored-by: Sakshyam Shah <[email protected]>
This commit is contained in:
Brian Joerger
2024-08-13 21:59:39 +00:00
committed by GitHub
co-authored by Sakshyam Shah
parent 8d12347ec1
commit d26ca00120
25 changed files with 1041 additions and 1525 deletions
+4 -7
View File
@@ -839,13 +839,10 @@ const (
// command execution (exec and shells).
ExecSubCommand = "exec"
// LocalForwardSubCommand is the sub-command Teleport uses to re-exec itself
// for local port forwarding.
LocalForwardSubCommand = "forwardv2"
// RemoteForwardSubCommand is the sub-command Teleport uses to re-exec itself
// for remote port forwarding.
RemoteForwardSubCommand = "remoteforward"
// NetworkingSubCommand is the sub-command Teleport uses to re-exec itself
// for networking operations. e.g. local/remote port forwarding, agent forwarding,
// or x11 forwarding.
NetworkingSubCommand = "networking"
// CheckHomeDirSubCommand is the sub-command Teleport uses to re-exec itself
// to check if the user's home directory exists.
-85
View File
@@ -1,85 +0,0 @@
/*
* 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 integration
import (
"os"
"os/user"
"runtime"
"syscall"
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
"github.com/gravitational/teleport/lib/teleagent"
)
func TestAgentSocketPermissions(t *testing.T) {
if !isRoot() {
t.Skip("This test will be skipped because tests are not being run as root.")
}
agentServer := teleagent.NewServer(nil)
agentServer.SetTestPermissions(func() {
// ListenUnixSocket should not have its uid changed from root
require.True(t, isRoot())
done := make(chan struct{})
// Start goroutine to attempt privilege escalation during
// permission updates on the unix socket.
//
// For each step of permission updating, it should be impossible
// for the user to unlink/remove the socket. If they can unlink
// or remove the socket, then it could be replaced with a symlink
// which can be used to acquire the permissions of the original socket.
go func() {
defer close(done)
// Update uid to nonroot
_, _, serr := syscall.Syscall(syscall.SYS_SETUID, 1000, 0, 0)
require.Zero(t, serr)
require.False(t, isRoot())
err := unix.Unlink(agentServer.Path)
require.Error(t, err)
err = os.Remove(agentServer.Path)
require.Error(t, err)
err = os.Rename(agentServer.Path, agentServer.Path)
require.Error(t, err)
}()
<-done
// ListenUnixSocket should not have its uid changed from root
require.True(t, isRoot())
})
nonRoot, err := user.LookupId("1000")
require.NoError(t, err)
// lock goroutine to root so that ListenUnixSocket doesn't
// pick up the syscall in the testPermissions func
runtime.LockOSThread()
defer runtime.UnlockOSThread()
err = agentServer.ListenUnixSocket("test", "sock.agent", nonRoot)
require.NoError(t, err)
}
+2 -2
View File
@@ -140,7 +140,7 @@ func ExternalSSHCommand(o CommandOptions) (*exec.Cmd, error) {
// CreateAgent creates a SSH agent with the passed in key ring that can be used
// in tests. This is useful so tests don't clobber your system agent.
func CreateAgent(me *user.User, keyRing *client.KeyRing) (*teleagent.AgentServer, string, string, error) {
func CreateAgent(keyRing *client.KeyRing) (*teleagent.AgentServer, string, string, error) {
// create a path to the unix socket
sockDirName := "int-test"
sockName := "agent.sock"
@@ -165,7 +165,7 @@ func CreateAgent(me *user.User, keyRing *client.KeyRing) (*teleagent.AgentServer
})
// start the SSH agent
err = teleAgent.ListenUnixSocket(sockDirName, sockName, me)
err = teleAgent.ListenUnixSocket(sockDirName, sockName, nil)
if err != nil {
return nil, "", "", trace.Wrap(err)
}
+4 -4
View File
@@ -4535,7 +4535,7 @@ func testExternalClient(t *testing.T, suite *integrationTestSuite) {
require.NoError(t, err)
// Start (and defer close) a agent that runs during this integration test.
teleAgent, socketDirPath, socketPath, err := helpers.CreateAgent(suite.Me, &creds.KeyRing)
teleAgent, socketDirPath, socketPath, err := helpers.CreateAgent(&creds.KeyRing)
require.NoError(t, err)
defer helpers.CloseAgent(teleAgent, socketDirPath)
@@ -4631,7 +4631,7 @@ func testControlMaster(t *testing.T, suite *integrationTestSuite) {
require.NoError(t, err)
// Start (and defer close) a agent that runs during this integration test.
teleAgent, socketDirPath, socketPath, err := helpers.CreateAgent(suite.Me, &creds.KeyRing)
teleAgent, socketDirPath, socketPath, err := helpers.CreateAgent(&creds.KeyRing)
require.NoError(t, err)
defer helpers.CloseAgent(teleAgent, socketDirPath)
@@ -4728,7 +4728,7 @@ func testX11Forwarding(t *testing.T, suite *integrationTestSuite) {
require.NoError(t, err)
// Start an agent that runs during this integration test.
teleAgent, socketDirPath, socketPath, err := helpers.CreateAgent(suite.Me, &creds.KeyRing)
teleAgent, socketDirPath, socketPath, err := helpers.CreateAgent(&creds.KeyRing)
require.NoError(t, err)
t.Cleanup(func() { helpers.CloseAgent(teleAgent, socketDirPath) })
@@ -4746,7 +4746,7 @@ func testX11Forwarding(t *testing.T, suite *integrationTestSuite) {
}
// Create and run an exec command twice. When ControlPath is set, this will cause
// re-use of the connection and creation of two sessions within the connection.
// re-use of the connection and creation of two sessions within the connection.
for i := 0; i < 2; i++ {
execCmd, err := helpers.ExternalSSHCommand(helpers.CommandOptions{
ForcePTY: true,
+2 -1
View File
@@ -29,6 +29,7 @@ import (
tracessh "github.com/gravitational/teleport/api/observability/tracing/ssh"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/sshutils/x11"
"github.com/gravitational/teleport/lib/utils"
)
// handleX11Forwarding handles X11 channel requests for the given server session.
@@ -198,7 +199,7 @@ func (ns *NodeSession) serveX11Channels(ctx context.Context, sess *tracessh.Sess
}
}()
if err := x11.Forward(ctx, xconn, xchan); err != nil {
if err := utils.ProxyConn(ctx, xconn, xchan); err != nil {
log.WithError(err).Debug("Encountered error during X11 forwarding")
}
})
+9 -155
View File
@@ -20,8 +20,6 @@ package srv
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
@@ -188,47 +186,6 @@ type Server interface {
TargetMetadata() apievents.ServerMetadata
}
// childProcessError is used to provide an underlying error
// from a re-executed Teleport child process to its parent.
type childProcessError struct {
Code int `json:"code"`
RawError []byte `json:"rawError"`
}
// writeChildError encodes the provided error
// as json and writes it to w. Special care
// is taken to preserve the error type by
// including the error code and raw message
// so that [DecodeChildError] will return
// the matching error type and message.
func writeChildError(w io.Writer, err error) {
if w == nil || err == nil {
return
}
data, jerr := json.Marshal(err)
if jerr != nil {
return
}
_ = json.NewEncoder(w).Encode(childProcessError{
Code: trace.ErrorToCode(err),
RawError: data,
})
}
// DecodeChildError consumes the output from a child
// process decoding it from its raw form back into
// a concrete error.
func DecodeChildError(r io.Reader) error {
var c childProcessError
if err := json.NewDecoder(r).Decode(&c); err != nil {
return nil
}
return trace.ReadError(c.Code, c.RawError)
}
// IdentityContext holds all identity information associated with the user
// logged on the connection.
type IdentityContext struct {
@@ -433,20 +390,6 @@ type ServerContext struct {
// by the server.
AllowFileCopying bool
// x11rdy{r,w} is used to signal from the child process to the
// parent process when X11 forwarding is set up.
x11rdyr *os.File
x11rdyw *os.File
// err{r,w} is used to propagate errors from the child process to the
// parent process so the parent can get more information about why the child
// process failed and act accordingly.
errr *os.File
errw *os.File
// x11Config holds the xauth and XServer listener config for this session.
x11Config *X11Config
// JoinOnly is set if the connection was created using a join-only principal and may only be used to join other sessions.
JoinOnly bool
@@ -598,24 +541,6 @@ func NewServerContext(ctx context.Context, parent *sshutils.ConnectionContext, s
child.AddCloser(child.killShellr)
child.AddCloser(child.killShellw)
// Create pipe used to get X11 forwarding ready signal from the child process.
child.x11rdyr, child.x11rdyw, err = os.Pipe()
if err != nil {
childErr := child.Close()
return nil, nil, trace.NewAggregate(err, childErr)
}
child.AddCloser(child.x11rdyr)
child.AddCloser(child.x11rdyw)
// Create pipe used to get errors from the child process.
child.errr, child.errw, err = os.Pipe()
if err != nil {
childErr := child.Close()
return nil, nil, trace.NewAggregate(err, childErr)
}
child.AddCloser(child.errr)
child.AddCloser(child.errw)
return ctx, child, nil
}
@@ -829,26 +754,12 @@ func (c *ServerContext) CheckSFTPAllowed(registry *SessionRegistry) error {
}
// OpenXServerListener opens a new XServer unix listener.
func (c *ServerContext) OpenXServerListener(x11Req x11.ForwardRequestPayload, displayOffset, maxDisplays int) error {
l, display, err := x11.OpenNewXServerListener(displayOffset, maxDisplays, x11Req.ScreenNumber)
func (c *ServerContext) HandleX11Listener(l net.Listener, singleConnection bool) error {
display, err := x11.ParseDisplayFromUnixSocket(l.Addr().String())
if err != nil {
return trace.Wrap(err)
}
err = c.setX11Config(&X11Config{
XServerUnixSocket: l.Addr().String(),
XAuthEntry: x11.XAuthEntry{
Display: display,
Proto: x11Req.AuthProtocol,
Cookie: x11Req.AuthCookie,
},
})
if err != nil {
l.Close()
return trace.Wrap(err)
}
c.AddCloser(l)
c.Parent().SetEnv(x11.DisplayEnv, display.String())
// Prepare X11 channel request payload
originHost, originPort, err := net.SplitHostPort(c.ServerConn.LocalAddr().String())
@@ -868,26 +779,16 @@ func (c *ServerContext) OpenXServerListener(x11Req x11.ForwardRequestPayload, di
for {
xconn, err := l.Accept()
if err != nil {
// listener is closed
if !utils.IsOKNetworkError(err) {
c.Logger.WithError(err).Debug("Encountered error accepting XServer connection")
}
return
}
go func() {
defer xconn.Close()
// If the session has not signaled that X11 forwarding is
// fully set up yet, then ignore any incoming connections.
// The client's session hasn't been fully set up yet so this
// could potentially be a break-in attempt.
if ok, err := c.x11Ready(); err != nil {
c.Logger.WithError(err).Debug("Failed to get X11 ready status")
return
} else if !ok {
c.Logger.WithError(err).Debug("Rejecting X11 request, XServer Proxy is not ready")
return
}
xchan, sin, err := c.ServerConn.OpenChannel(sshutils.X11ChannelRequest, x11ChannelReqPayload)
xchan, sin, err := c.ServerConn.OpenChannel(x11.ChannelRequest, x11ChannelReqPayload)
if err != nil {
c.Logger.WithError(err).Debug("Failed to open a new X11 channel")
return
@@ -905,12 +806,12 @@ func (c *ServerContext) OpenXServerListener(x11Req x11.ForwardRequestPayload, di
}
}()
if err := x11.Forward(ctx, xconn, xchan); err != nil {
if err := utils.ProxyConn(ctx, xconn, xchan); err != nil {
c.Logger.WithError(err).Debug("Encountered error during X11 forwarding")
}
}()
if x11Req.SingleConnection {
if singleConnection {
l.Close()
return
}
@@ -920,52 +821,6 @@ func (c *ServerContext) OpenXServerListener(x11Req x11.ForwardRequestPayload, di
return nil
}
// getX11Config gets the x11 config for this server session.
func (c *ServerContext) getX11Config() X11Config {
c.mu.Lock()
defer c.mu.Unlock()
if c.x11Config != nil {
return *c.x11Config
}
return X11Config{}
}
// setX11Config sets X11 config for the session, or returns an error if already set.
func (c *ServerContext) setX11Config(cfg *X11Config) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.x11Config != nil {
return trace.AlreadyExists("X11 forwarding is already set up for this session")
}
c.x11Config = cfg
return nil
}
// x11Ready returns whether the X11 unix listener is ready to accept connections.
func (c *ServerContext) x11Ready() (bool, error) {
// Wait for child process to send signal (1 byte)
// or EOF if signal was already received.
_, err := io.ReadFull(c.x11rdyr, make([]byte, 1))
if errors.Is(err, io.EOF) {
return true, nil
} else if err != nil {
return false, trace.Wrap(err)
}
// signal received, close writer so future calls read EOF.
if err := c.x11rdyw.Close(); err != nil {
return false, trace.Wrap(err)
}
return true, nil
}
// GetChildError returns the error from the child process
func (c *ServerContext) GetChildError() error {
return DecodeChildError(c.errr)
}
// takeClosers returns all resources that should be closed and sets the properties to null
// we do this to avoid calling Close() under lock to avoid potential deadlocks
func (c *ServerContext) takeClosers() []io.Closer {
@@ -1201,7 +1056,6 @@ func (c *ServerContext) ExecCommand() (*ExecCommand, error) {
PAMConfig: pamConfig,
IsTestStub: c.IsTestStub,
UaccMetadata: *uaccMetadata,
X11Config: c.getX11Config(),
}, nil
}
-16
View File
@@ -19,14 +19,11 @@
package srv
import (
"bytes"
"context"
"os/user"
"testing"
"github.com/gogo/protobuf/proto"
"github.com/google/go-cmp/cmp"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
"google.golang.org/protobuf/testing/protocmp"
@@ -39,19 +36,6 @@ import (
"github.com/gravitational/teleport/lib/sshutils"
)
// TestDecodeChildError ensures that child error message marshaling
// and unmarshaling returns the original values.
func TestDecodeChildError(t *testing.T) {
var buf bytes.Buffer
require.NoError(t, DecodeChildError(&buf))
targetErr := trace.NotFound(user.UnknownUserError("test").Error())
writeChildError(&buf, targetErr)
require.ErrorIs(t, DecodeChildError(&buf), targetErr)
}
func TestCheckSFTPAllowed(t *testing.T) {
srv := newMockServer(t)
ctx := newTestServerContext(t, srv, nil)
-35
View File
@@ -32,12 +32,7 @@ import (
"time"
"github.com/gravitational/trace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/host"
)
func TestOSCommandPrep(t *testing.T) {
@@ -137,36 +132,6 @@ func TestConfigureCommand(t *testing.T) {
require.NotContains(t, cmd.Env, unexpectedKey+"="+unexpectedValue)
}
func TestRootConfigureCommand(t *testing.T) {
utils.RequireRoot(t)
login := utils.GenerateLocalUsername(t)
_, err := host.UserAdd(login, nil, "", "", "")
require.NoError(t, err)
t.Cleanup(func() {
_, err := host.UserDel(login)
require.NoError(t, err)
})
srv := newMockServer(t)
scx := newExecServerContext(t, srv)
scx.Identity.Login = login
scx.ExecType = teleport.TCPIPForwardRequest
u, err := user.Lookup(login)
require.NoError(t, err)
uid, err := strconv.ParseUint(u.Uid, 10, 32)
require.NoError(t, err)
gid, err := strconv.ParseUint(u.Gid, 10, 32)
require.NoError(t, err)
cmd, err := ConfigureCommand(scx)
require.NoError(t, err)
// Verify that the configured command will run as the expected user.
assert.Equal(t, uint32(uid), cmd.SysProcAttr.Credential.Uid)
assert.Equal(t, uint32(gid), cmd.SysProcAttr.Credential.Gid)
}
// TestContinue tests if the process hangs if a continue signal is not sent
// and makes sure the process continues once it has been sent.
func TestContinue(t *testing.T) {
+3 -3
View File
@@ -1286,7 +1286,7 @@ func (s *Server) dispatch(ctx context.Context, ch ssh.Channel, req *ssh.Request,
return s.handleEnvs(ctx, ch, req, scx)
case sshutils.SubsystemRequest:
return s.handleSubsystem(ctx, ch, req, scx)
case sshutils.X11ForwardRequest:
case x11.ForwardRequest:
return s.handleX11Forward(ctx, ch, req, scx)
case sshutils.AgentForwardRequest:
// to maintain interoperability with OpenSSH, agent forwarding requests
@@ -1361,7 +1361,7 @@ func (s *Server) handleX11ChannelRequest(ctx context.Context, nch ssh.NewChannel
defer sch.Close()
// setup outbound X11 channel to client
cch, cin, err := s.sconn.OpenChannel(sshutils.X11ChannelRequest, nch.ExtraData())
cch, cin, err := s.sconn.OpenChannel(x11.ChannelRequest, nch.ExtraData())
if err != nil {
s.log.Errorf("X11 channel fwd failed: %v", err)
return
@@ -1386,7 +1386,7 @@ func (s *Server) handleX11ChannelRequest(ctx context.Context, nch ssh.NewChannel
}
}()
if err := x11.Forward(ctx, cch, sch); err != nil {
if err := utils.ProxyConn(ctx, cch, sch); err != nil {
s.log.WithError(err).Debug("Encountered error during x11 forwarding")
}
}
+277 -240
View File
@@ -25,7 +25,6 @@ import (
"errors"
"fmt"
"io"
"log/slog"
"net"
"os"
"os/exec"
@@ -34,11 +33,13 @@ import (
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"time"
"github.com/gravitational/trace"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
@@ -48,6 +49,7 @@ import (
"github.com/gravitational/teleport/lib/shell"
"github.com/gravitational/teleport/lib/srv/uacc"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/sshutils/networking"
"github.com/gravitational/teleport/lib/sshutils/x11"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/envutils"
@@ -76,12 +78,6 @@ const (
// to pid 1 and "live forever". Killing the shell should not prevent processes
// preventing SIGHUP to be reassigned (ex. processes running with nohup).
TerminateFile
// X11File is used to communicate to the parent process that the child
// process has set up X11 forwarding.
X11File
// ErrorFile is used to communicate any errors terminating the child process
// to the parent process
ErrorFile
// PTYFile is a PTY the parent process passes to the child process.
PTYFile
// TTYFile is a TTY the parent process passes to the child process.
@@ -89,7 +85,7 @@ const (
// FirstExtraFile is the first file descriptor that will be valid when
// extra files are passed to child processes without a terminal.
FirstExtraFile FileFD = ErrorFile + 1
FirstExtraFile FileFD = TerminateFile + 1
)
func fdName(f FileFD) string {
@@ -156,9 +152,6 @@ type ExecCommand struct {
// UaccMetadata contains metadata needed for user accounting.
UaccMetadata UaccMetadata `json:"uacc_meta"`
// X11Config contains an xauth entry to be added to the command user's xauthority.
X11Config X11Config `json:"x11_config"`
// ExtraFilesLen is the number of extra files that are inherited from
// the parent process. These files start at file descriptor 3 of the
// child process, and are only valid for processes without a terminal.
@@ -178,14 +171,6 @@ type PAMConfig struct {
Environment map[string]string `json:"environment"`
}
// X11Config contains information used by the child process to set up X11 forwarding.
type X11Config struct {
// XAuthEntry contains xauth data used for X11 forwarding.
XAuthEntry x11.XAuthEntry `json:"xauth_entry,omitempty"`
// XServerUnixSocket is the name of an open XServer unix socket used for X11 forwarding.
XServerUnixSocket string `json:"xserver_unix_socket"`
}
// UaccMetadata contains information the child needs from the parent for user accounting.
type UaccMetadata struct {
// The hostname of the node.
@@ -242,8 +227,8 @@ func RunCommand() (errw io.Writer, code int, err error) {
_ = readyfd.Close()
}()
termiantefd := os.NewFile(TerminateFile, fdName(TerminateFile))
if termiantefd == nil {
terminatefd := os.NewFile(TerminateFile, fdName(TerminateFile))
if terminatefd == nil {
return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("terminate pipe not found")
}
@@ -398,70 +383,6 @@ func RunCommand() (errw io.Writer, code int, err error) {
}
}
if c.X11Config.XServerUnixSocket != "" {
// Set the open XServer unix socket's owner to the localuser
// to prevent a potential privilege escalation vulnerability.
uid, err := strconv.Atoi(localUser.Uid)
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
gid, err := strconv.Atoi(localUser.Gid)
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
if err := os.Lchown(c.X11Config.XServerUnixSocket, uid, gid); err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
// Update localUser's xauth database for X11 forwarding. We set
// cmd.SysProcAttr.Setsid, cmd.Env, and cmd.Dir so that the xauth command
// acts as if called within the following shell/exec, so that the
// xauthority files is put into the correct place ($HOME/.Xauthority)
// with the right permissions.
removeCmd := x11.NewXAuthCommand(context.Background(), "")
removeCmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
Credential: cmd.SysProcAttr.Credential,
}
removeCmd.Env = cmd.Env
removeCmd.Dir = cmd.Dir
if err := removeCmd.RemoveEntries(c.X11Config.XAuthEntry.Display); err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
addCmd := x11.NewXAuthCommand(context.Background(), "")
addCmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
Credential: cmd.SysProcAttr.Credential,
}
addCmd.Env = cmd.Env
addCmd.Dir = cmd.Dir
if err := addCmd.AddEntry(c.X11Config.XAuthEntry); err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
// Set $DISPLAY so that XServer requests forwarded to the X11 unix listener.
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", x11.DisplayEnv, c.X11Config.XAuthEntry.Display.String()))
// Open x11rdy fd to signal parent process once X11 forwarding is set up.
x11rdyfd := os.NewFile(X11File, fdName(X11File))
if x11rdyfd == nil {
return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("continue pipe not found")
}
// Write a single byte to signal to the parent process that X11 forwarding is set up.
if _, err := x11rdyfd.Write([]byte{0}); err != nil {
if err2 := x11rdyfd.Close(); err2 != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.NewAggregate(err, err2)
}
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
if err := x11rdyfd.Close(); err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
}
if err := setNeutralOOMScore(); err != nil {
log.WithError(err).Warnf("failed to adjust OOM score")
}
@@ -473,7 +394,7 @@ func RunCommand() (errw io.Writer, code int, err error) {
parkerCancel()
err = waitForShell(termiantefd, cmd)
err = waitForShell(terminatefd, cmd)
if uaccEnabled {
uaccErr := uacc.Close(c.UaccMetadata.UtmpPath, c.UaccMetadata.WtmpPath, tty)
@@ -486,14 +407,14 @@ func RunCommand() (errw io.Writer, code int, err error) {
}
// waitForShell waits either for the command to return or the kill signal from the parent Teleport process.
func waitForShell(termiantefd *os.File, cmd *exec.Cmd) error {
func waitForShell(terminatefd *os.File, cmd *exec.Cmd) error {
terminateChan := make(chan error)
go func() {
buf := make([]byte, 1)
// Wait for the terminate file descriptor to be closed. The FD will be closed when Teleport
// parent process wants to terminate the remote command and all childs.
_, err := termiantefd.Read(buf)
_, err := terminatefd.Read(buf)
if errors.Is(err, io.EOF) {
// Kill the shell process
err = trace.Errorf("shell process has been killed: %w", cmd.Process.Kill())
@@ -613,87 +534,7 @@ func (o *osWrapper) startNewParker(ctx context.Context, credential *syscall.Cred
return nil
}
type forwardHandler func(ctx context.Context, addr string, file *os.File) error
func handleLocalPortForward(ctx context.Context, addr string, file *os.File) error {
conn, err := uds.FromFile(file)
_ = file.Close()
if err != nil {
return trace.Wrap(err)
}
defer conn.Close()
var d net.Dialer
remote, err := d.DialContext(ctx, "tcp", addr)
if err != nil {
return trace.Wrap(err)
}
defer remote.Close()
if err := utils.ProxyConn(ctx, conn, remote); err != nil {
return trace.Wrap(err)
}
return nil
}
func createRemotePortForwardingListener(ctx context.Context, addr string) (*os.File, error) {
lc := net.ListenConfig{
Control: func(network, addr string, conn syscall.RawConn) error {
var err error
err2 := conn.Control(func(descriptor uintptr) {
// Disable address reuse to prevent socket replacement.
err = syscall.SetsockoptInt(int(descriptor), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 0)
})
return trace.NewAggregate(err2, err)
},
}
listener, err := lc.Listen(ctx, "tcp", addr)
if err != nil {
return nil, trace.Wrap(err)
}
defer listener.Close()
tcpListener, _ := listener.(*net.TCPListener)
if tcpListener == nil {
return nil, trace.Errorf("expected listener to be of type *net.TCPListener, but was %T", listener)
}
listenerFD, err := tcpListener.File()
return listenerFD, trace.Wrap(err)
}
func handleRemotePortForward(ctx context.Context, addr string, file *os.File) error {
controlConn, err := uds.FromFile(file)
_ = file.Close()
if err != nil {
return trace.Wrap(err)
}
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
// unblock the final write
context.AfterFunc(ctx, func() { _ = controlConn.Close() })
go func() {
defer cancel()
_, _ = controlConn.Read(make([]byte, 1))
}()
var payload []byte
var files []*os.File
listenerFD, err := createRemotePortForwardingListener(ctx, addr)
if err == nil {
files = []*os.File{listenerFD}
} else {
payload = []byte(err.Error())
}
_, _, err2 := uds.WriteWithFDs(controlConn, payload, files)
return trace.NewAggregate(err, err2)
}
// runForward reads in the command to run from the parent process (over a
// pipe) then port forwards.
func runForward(handler forwardHandler) (errw io.Writer, code int, err error) {
func RunNetworking() (errw io.Writer, code int, err error) {
// errorWriter is used to return any error message back to the client.
// Use stderr so that it's not forwarded to the remote client.
errorWriter := os.Stderr
@@ -704,16 +545,11 @@ func runForward(handler forwardHandler) (errw io.Writer, code int, err error) {
return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("command pipe not found")
}
// Parent receives any errors on the sixth file descriptor.
errfd := os.NewFile(ErrorFile, fdName(ErrorFile))
if errfd == nil {
return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("error pipe not found")
terminatefd := os.NewFile(TerminateFile, fdName(TerminateFile))
if terminatefd == nil {
return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("terminate pipe not found")
}
defer func() {
writeChildError(errfd, err)
}()
// Read in the command payload.
var c ExecCommand
if err := json.NewDecoder(cmdfd).Decode(&c); err != nil {
@@ -721,8 +557,9 @@ func runForward(handler forwardHandler) (errw io.Writer, code int, err error) {
}
// If PAM is enabled, open a PAM context. This has to be done before anything
// else because PAM is sometimes used to create the local user used to
// launch the shell under.
// else because PAM is sometimes used to create the local user used for
// networking requests.
var pamEnvironment []string
if c.PAMConfig != nil {
// Open the PAM context.
pamContext, err := pam.Open(&servicecfg.PAMConfig{
@@ -740,20 +577,73 @@ func runForward(handler forwardHandler) (errw io.Writer, code int, err error) {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
defer pamContext.Close()
pamEnvironment = pamContext.Environment()
}
if _, err := user.Lookup(c.Login); err != nil {
// Once the PAM stack is called with parent process permissions, set the process uid
// and gid to the requested user. This way, the user's networking requests will be
// done with the user's permissions.
localUser, err := user.Lookup(c.Login)
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.NotFound(err.Error())
}
// build forwarder from first extra file that was passed to command
cred, err := getCmdCredential(localUser)
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
if os.Getuid() != int(cred.Uid) || os.Getgid() != int(cred.Gid) {
if !cred.NoSetGroups {
groups := make([]int, len(cred.Groups))
for i, g := range cred.Groups {
groups[i] = int(g)
}
if err := unix.Setgroups(groups); err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err, "failed to set groups for networking process")
}
}
if err := unix.Setgid(int(cred.Gid)); err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err, "failed to set gid for networking process")
}
if err := unix.Setuid(int(cred.Uid)); err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err, "failed to set uid for networking process")
}
}
// Create a minimal default environment for the user.
homeDir := localUser.HomeDir
if !utils.IsDir(homeDir) {
homeDir = "/"
}
os.Setenv("HOME", localUser.HomeDir)
os.Setenv("USER", c.Login)
// Apply any additional environment variables from PAM.
for _, kv := range pamEnvironment {
key, value, ok := strings.Cut(strings.TrimSpace(kv), "=")
if !ok {
return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("bad environment variable from PAM, expected format \"key=value\" but got %q", kv)
}
if err := os.Setenv(key, value); err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
}
// Ensure that the working directory is one that the local user has access to.
if err := os.Chdir(homeDir); err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err, "failed to set working directory for networking process")
}
// Build request listener from first extra file that was passed to command.
ffd := os.NewFile(FirstExtraFile, "listener")
if ffd == nil {
return errorWriter, teleport.RemoteCommandFailure, trace.BadParameter("missing socket fd")
}
conn, err := uds.FromFile(ffd)
ffd.Close()
parentConn, err := uds.FromFile(ffd)
_ = ffd.Close()
if err != nil {
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
@@ -761,42 +651,219 @@ func runForward(handler forwardHandler) (errw io.Writer, code int, err error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
writeErrorToConn := func(conn io.Writer, err error) {
conn.Write([]byte(err.Error()))
}
// Maintain a list of file paths to cleanup at the end of the process. This
// ensures that file cleanup is handled by the child in cases where the parent
// fails to cleanup due to filesystem namespace discrepancy (pam_namespace)
var filePathsToCleanup []string
defer func() {
for _, path := range filePathsToCleanup {
os.Remove(path)
}
}()
// parentConn is a datagram Unix socket, which is not connection oriented
// and thus won't unblock when the parent closes its side of the connection.
// Instead we use an interrupt signal from the parent process to unblock.
go func() {
_, _ = terminatefd.Read(make([]byte, 1))
parentConn.Close()
}()
for {
buf := make([]byte, 1024)
fbuf := make([]*os.File, 1)
n, fn, err := uds.ReadWithFDs(conn, buf, fbuf)
n, fn, err := uds.ReadWithFDs(parentConn, buf, fbuf)
if err != nil {
if utils.IsOKNetworkError(err) {
// parent connection closed, process should exit.
return errorWriter, teleport.RemoteCommandSuccess, nil
}
return errorWriter, teleport.RemoteCommandFailure, trace.Wrap(err)
}
addr := string(buf[:n])
if fn == 0 {
log.Errorf("Parent did not send a file descriptor for address %q.", addr)
writeErrorToConn(parentConn, trace.Wrap(err, "error reading networking request from parent"))
continue
}
go func() {
if err := handler(ctx, addr, fbuf[0]); err != nil {
log.WithError(err).Errorf("Error handling forwarding request for address %q.", addr)
if fn == 0 {
writeErrorToConn(parentConn, trace.BadParameter("networking request requires a control file"))
continue
}
requestConn, err := uds.FromFile(fbuf[0])
_ = fbuf[0].Close()
if err != nil {
writeErrorToConn(parentConn, trace.Wrap(err, "failed to get a connection from control file"))
continue
}
var req networking.Request
if err := json.Unmarshal(buf[:n], &req); err != nil {
writeErrorToConn(requestConn, trace.Wrap(err, "error parsing networking request"))
_ = requestConn.Close()
continue
}
// Some PAM modules (e.g. pam_namespace) do not behave properly in multithreaded contexts.
// Therefore we favor handling requests and cleanup on the main PAM thread for requests that
// are expected to be impacted (unix socket listeners).
switch req.Operation {
case networking.NetworkingOperationDial, networking.NetworkingOperationListen:
switch req.Network {
case "tcp":
// There are currently no known issues with tcp listen/dial in a multithreaded PAM context.
go handleNetworkingRequest(ctx, requestConn, req)
default:
// Note: we don't currently support non-tcp network forwarding, so this branch is not
// currently reached. If in the future we add unix socket forwarding similar to OpenSSH's
// [email protected] extension, we should revisit this multithreading limitation
// to prevent performance degradation.
filePaths := handleNetworkingRequest(ctx, requestConn, req)
filePathsToCleanup = append(filePathsToCleanup, filePaths...)
}
}()
case networking.NetworkingOperationListenAgent, networking.NetworkingOperationListenX11:
// Agent and X11 forwarding requests should occur very rarely, so handling
// them in the main thread should have negligible performance impact.
cleanupFilePaths := handleNetworkingRequest(ctx, requestConn, req)
filePathsToCleanup = append(filePathsToCleanup, cleanupFilePaths...)
}
}
}
// RunLocalForward reads in the command to run from the parent process (over a
// pipe) then port forwards.
func RunLocalForward() (errw io.Writer, code int, err error) {
errw, code, err = runForward(handleLocalPortForward)
return errw, code, trace.Wrap(err)
func handleNetworkingRequest(ctx context.Context, conn *net.UnixConn, req networking.Request) []string {
defer conn.Close()
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
go func() {
defer cancel()
_, _ = conn.Read(make([]byte, 1))
}()
netFile, filePaths, err := createNetworkingFile(ctx, req)
if err != nil {
conn.Write([]byte(trace.Wrap(err, "failed to create networking file").Error()))
return nil
}
defer netFile.Close()
if _, _, err := uds.WriteWithFDs(conn, nil, []*os.File{netFile}); err != nil {
conn.Write([]byte(trace.Wrap(err, "failed to write networking file to control conn").Error()))
return nil
}
return filePaths
}
// RunRemoteForward reads in the command to run from the parent process (over a
// pipe) then listens for port forwarding.
func RunRemoteForward() (errw io.Writer, code int, err error) {
errw, code, err = runForward(handleRemotePortForward)
return errw, code, trace.Wrap(err)
func createNetworkingFile(ctx context.Context, req networking.Request) (*os.File, []string, error) {
switch req.Operation {
case networking.NetworkingOperationDial:
var d net.Dialer
conn, err := d.DialContext(ctx, req.Network, req.Address)
if err != nil {
return nil, nil, trace.Wrap(err)
}
defer conn.Close()
connFD, err := getConnFile(conn)
return connFD, nil, trace.Wrap(err)
case networking.NetworkingOperationListen:
listener, err := net.Listen(req.Network, req.Address)
if err != nil {
return nil, nil, trace.Wrap(err)
}
defer listener.Close()
listenerFD, err := getListenerFile(listener)
if err != nil {
return nil, nil, trace.Wrap(err)
}
return listenerFD, []string{listener.Addr().String()}, trace.Wrap(err)
case networking.NetworkingOperationListenAgent:
// Create a temp directory to hold the agent socket.
sockDir, err := os.MkdirTemp("", "teleport-")
if err != nil {
return nil, nil, trace.Wrap(err)
}
sockPath := filepath.Join(sockDir, "agent.sock")
listener, err := net.Listen("unix", sockPath)
if err != nil {
os.RemoveAll(sockDir)
return nil, nil, trace.Wrap(err)
}
defer listener.Close()
listenerFD, err := getListenerFile(listener)
if err != nil {
os.RemoveAll(sockDir)
return nil, nil, trace.Wrap(err)
}
return listenerFD, []string{sockPath, sockDir}, nil
case networking.NetworkingOperationListenX11:
listener, display, err := x11.OpenNewXServerListener(req.X11Request.DisplayOffset, req.X11Request.MaxDisplay, req.X11Request.ScreenNumber)
if err != nil {
return nil, nil, trace.Wrap(err)
}
defer listener.Close()
removeCmd := x11.NewXAuthCommand(ctx, req.X11Request.XauthFile)
if err := removeCmd.RemoveEntries(display); err != nil {
return nil, nil, trace.Wrap(err)
}
addCmd := x11.NewXAuthCommand(ctx, req.X11Request.XauthFile)
if err := addCmd.AddEntry(x11.XAuthEntry{
Display: display,
Proto: req.X11Request.AuthProtocol,
Cookie: req.X11Request.AuthCookie,
}); err != nil {
return nil, nil, trace.Wrap(err)
}
listenerFD, err := getListenerFile(listener)
if err != nil {
return nil, nil, trace.Wrap(err)
}
return listenerFD, []string{listener.Addr().String()}, trace.Wrap(err)
default:
return nil, nil, trace.BadParameter("unsupported networking operation %q", req.Operation)
}
}
func getListenerFile(listener net.Listener) (*os.File, error) {
switch l := listener.(type) {
case *net.UnixListener:
// Unlinking the socket here will cause the parent process to open a new
// socket in the parent namespace, which may be inaccessible for the user.
// Instead we close the listener without unlinking the socket, and cleanup
// the socket in the child namepsace once the process is closed.
l.SetUnlinkOnClose(false)
listenerFD, err := l.File()
return listenerFD, trace.Wrap(err)
case *net.TCPListener:
listenerFD, err := l.File()
return listenerFD, trace.Wrap(err)
default:
return nil, trace.Errorf("expected listener to be of type *net.UnixListener or *net.TCPListener, but was %T", l)
}
}
func getConnFile(conn net.Conn) (*os.File, error) {
switch c := conn.(type) {
case *net.UnixConn:
connFD, err := c.File()
return connFD, trace.Wrap(err)
case *net.TCPConn:
connFD, err := c.File()
return connFD, trace.Wrap(err)
default:
return nil, trace.Errorf("expected connection to be of type *net.UnixConn or *net.TCPConn, but was %T", conn)
}
}
// runCheckHomeDir check's if the active user's $HOME dir exists.
@@ -831,10 +898,8 @@ func RunAndExit(commandType string) {
switch commandType {
case teleport.ExecSubCommand:
w, code, err = RunCommand()
case teleport.LocalForwardSubCommand:
w, code, err = RunLocalForward()
case teleport.RemoteForwardSubCommand:
w, code, err = RunRemoteForward()
case teleport.NetworkingSubCommand:
w, code, err = RunNetworking()
case teleport.CheckHomeDirSubCommand:
w, code, err = runCheckHomeDir()
case teleport.ParkSubCommand:
@@ -854,7 +919,7 @@ func RunAndExit(commandType string) {
func IsReexec() bool {
if len(os.Args) == 2 {
switch os.Args[1] {
case teleport.ExecSubCommand, teleport.LocalForwardSubCommand, teleport.RemoteForwardSubCommand,
case teleport.ExecSubCommand, teleport.NetworkingSubCommand,
teleport.CheckHomeDirSubCommand, teleport.ParkSubCommand, teleport.SFTPSubCommand:
return true
}
@@ -865,7 +930,7 @@ func IsReexec() bool {
// buildCommand constructs a command that will execute the users shell. This
// function is run by Teleport while it's re-executing.
func buildCommand(c *ExecCommand, localUser *user.User, tty *os.File, pty *os.File, pamEnvironment []string) (*exec.Cmd, error) {
func buildCommand(c *ExecCommand, localUser *user.User, tty *os.File, _ *os.File, pamEnvironment []string) (*exec.Cmd, error) {
var cmd exec.Cmd
isReexec := false
@@ -1067,14 +1132,11 @@ func ConfigureCommand(ctx *ServerContext, extraFiles ...*os.File) (*exec.Cmd, er
}
executableDir, _ := filepath.Split(executable)
// The channel/request type determines the subcommand to execute (execution or
// port forwarding).
// The channel/request type determines the subcommand to execute.
var subCommand string
switch ctx.ExecType {
case teleport.ChanDirectTCPIP:
subCommand = teleport.LocalForwardSubCommand
case teleport.TCPIPForwardRequest:
subCommand = teleport.RemoteForwardSubCommand
case teleport.NetworkingSubCommand:
subCommand = teleport.NetworkingSubCommand
default:
subCommand = teleport.ExecSubCommand
}
@@ -1098,8 +1160,6 @@ func ConfigureCommand(ctx *ServerContext, extraFiles ...*os.File) (*exec.Cmd, er
ctx.contr,
ctx.readyw,
ctx.killShellr,
ctx.x11rdyw,
ctx.errw,
},
}
// Add extra files if applicable.
@@ -1107,29 +1167,6 @@ func ConfigureCommand(ctx *ServerContext, extraFiles ...*os.File) (*exec.Cmd, er
cmd.ExtraFiles = append(cmd.ExtraFiles, extraFiles...)
}
// For remote port forwarding, the child needs to run as the user to
// create listeners with the correct permissions.
if subCommand == teleport.RemoteForwardSubCommand {
localUser, err := user.Lookup(ctx.Identity.Login)
if err != nil {
return nil, trace.Wrap(err)
}
// Ensure that the working directory is one that the child has access to.
cmd.Dir = "/"
credential, err := getCmdCredential(localUser)
if err != nil {
return nil, trace.Wrap(err)
}
log := slog.With("uid", credential.Uid, "gid", credential.Gid, "groups", credential.Groups)
if os.Getuid() != int(credential.Uid) || os.Getgid() != int(credential.Gid) {
cmd.SysProcAttr = &syscall.SysProcAttr{Credential: credential}
log.DebugContext(ctx.Context, "Creating process with new credentials.")
} else {
log.DebugContext(ctx.Context, "Creating process with environment credentials.")
}
}
// Perform OS-specific tweaks to the command.
reexecCommandOSTweaks(cmd)
+174 -76
View File
@@ -20,6 +20,9 @@ package srv
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"io"
"net"
@@ -28,18 +31,21 @@ import (
"os"
"os/exec"
"os/user"
"path/filepath"
"strconv"
"syscall"
"testing"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh/agent"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/sshutils/networking"
"github.com/gravitational/teleport/lib/sshutils/x11"
"github.com/gravitational/teleport/lib/teleagent"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/host"
"github.com/gravitational/teleport/lib/utils/uds"
)
type stubUser struct {
@@ -183,21 +189,6 @@ func TestStartNewParker(t *testing.T) {
}
}
func newSocketPair(t *testing.T) (localConn *net.UnixConn, remoteFD *os.File) {
localConn, remoteConn, err := uds.NewSocketpair(uds.SocketTypeDatagram)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, remoteConn.Close())
require.NoError(t, localConn.Close())
})
remoteFD, err = remoteConn.File()
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, remoteFD.Close())
})
return localConn, remoteFD
}
func newHTTPTestServer(t *testing.T, listener net.Listener) *httptest.Server {
var err error
if listener == nil {
@@ -213,30 +204,68 @@ func newHTTPTestServer(t *testing.T, listener net.Listener) *httptest.Server {
return tsrv
}
func TestLocalPortForwardCommand(t *testing.T) {
func TestNetworkingCommand(t *testing.T) {
t.Parallel()
srv := newMockServer(t)
scx := newExecServerContext(t, srv)
scx.ExecType = teleport.ChanDirectTCPIP
testNetworkingCommand(t, "")
}
// Start forwarding subprocess.
controlConn, controlFD := newSocketPair(t)
command, err := ConfigureCommand(scx, controlFD)
// TestRootRemotePortForwardCommand tests that networking commands work
// for a user different than the one running a node (which we need to run
// as root to create).
func TestRootNetworkingCommand(t *testing.T) {
utils.RequireRoot(t)
login := utils.GenerateLocalUsername(t)
_, err := host.UserAdd(login, nil, "", "", "")
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, command.Process.Kill())
_, err := host.UserDel(login)
require.NoError(t, err)
})
require.NoError(t, command.Start())
// Create a client that will dial via the forwarder.
testNetworkingCommand(t, login)
}
func testNetworkingCommand(t *testing.T, login string) {
ctx := context.Background()
srv := newMockServer(t)
scx := newExecServerContext(t, srv)
scx.ExecType = teleport.NetworkingSubCommand
if login != "" {
scx.Identity.Login = login
}
// Start networking subprocess.
command, err := ConfigureCommand(scx)
require.NoError(t, err)
proc, err := networking.NewProcess(ctx, command)
require.NoError(t, err)
t.Cleanup(func() { proc.Close() })
t.Run("local port forward", func(t *testing.T) {
testLocalPortForward(ctx, t, proc)
})
t.Run("remote port forward", func(t *testing.T) {
testRemotePortForward(ctx, t, proc)
})
t.Run("agent forward", func(t *testing.T) {
testAgentForward(ctx, t, proc)
})
t.Run("x11 forward", func(t *testing.T) {
testX11Forward(ctx, t, proc, login)
})
}
func testLocalPortForward(ctx context.Context, t *testing.T, proc *networking.Process) {
// Create a client that will dial via the networking process.
httpClient := http.Client{
Transport: &http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
dialConn, dialFD := newSocketPair(t)
if _, _, err := uds.WriteWithFDs(controlConn, []byte(addr), []*os.File{dialFD}); err != nil {
return nil, trace.Wrap(err)
}
return dialConn, nil
return proc.Dial(ctx, network, addr)
},
},
}
@@ -251,35 +280,11 @@ func TestLocalPortForwardCommand(t *testing.T) {
require.Equal(t, "Hello, world", string(body))
}
func testRemotePortForwardCommand(t *testing.T, login string) {
srv := newMockServer(t)
scx := newExecServerContext(t, srv)
scx.ExecType = teleport.TCPIPForwardRequest
if login != "" {
scx.Identity.Login = login
}
// Start forwarding subprocess.
controlConn, controlFD := newSocketPair(t)
command, err := ConfigureCommand(scx, controlFD)
require.NoError(t, err)
require.NoError(t, command.Start())
t.Cleanup(func() {
require.NoError(t, command.Process.Kill())
_, err := command.Process.Wait()
require.NoError(t, err)
})
// Request a listener from the forwarder.
replyConn, replyFD := newSocketPair(t)
_, _, err = uds.WriteWithFDs(controlConn, []byte("127.0.0.1:0"), []*os.File{replyFD})
require.NoError(t, err)
var fbuf [1]*os.File
_, fn, err := uds.ReadWithFDs(replyConn, nil, fbuf[:])
require.NoError(t, err)
require.Equal(t, 1, fn)
listener, err := net.FileListener(fbuf[0])
func testRemotePortForward(ctx context.Context, t *testing.T, proc *networking.Process) {
// Request a listener from the networking process.
listener, err := proc.Listen(ctx, "tcp", "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() { listener.Close() })
// Test the listener on an http server.
tsrv := newHTTPTestServer(t, listener)
@@ -291,24 +296,117 @@ func testRemotePortForwardCommand(t *testing.T, login string) {
require.Equal(t, "Hello, world", string(body))
}
func TestRemotePortForwardCommand(t *testing.T) {
t.Parallel()
testRemotePortForwardCommand(t, "")
}
func testAgentForward(ctx context.Context, t *testing.T, proc *networking.Process) {
// Create an agent keyring with a test key.
keyring, ok := agent.NewKeyring().(agent.ExtendedAgent)
require.True(t, ok)
// TestRootRemotePortForwardCommand tests that remote port forwarding works
// for a user different than the one running a node (which we need to run
// as root to create).
func TestRootRemotePortForwardCommand(t *testing.T) {
utils.RequireRoot(t)
login := utils.GenerateLocalUsername(t)
_, err := host.UserAdd(login, nil, "", "", "")
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
t.Cleanup(func() {
_, err := host.UserDel(login)
require.NoError(t, err)
testKey := agent.AddedKey{
PrivateKey: key,
Comment: "test-key",
}
err = keyring.Add(testKey)
require.NoError(t, err)
teleAgent := teleagent.NewServer(func() (teleagent.Agent, error) {
return teleagent.NopCloser(keyring), nil
})
testRemotePortForwardCommand(t, login)
// Forward the agent over the networking process.
listener, err := proc.ListenAgent(ctx)
require.NoError(t, err)
teleAgent.SetListener(listener)
go teleAgent.Serve()
t.Cleanup(func() {
teleAgent.Close()
})
agentConn, err := net.Dial(listener.Addr().Network(), listener.Addr().String())
require.NoError(t, err)
agentClient := agent.NewClient(agentConn)
keys, err := agentClient.List()
require.NoError(t, err)
require.Len(t, keys, 1)
}
func testX11Forward(ctx context.Context, t *testing.T, proc *networking.Process, login string) {
if os.Getenv("TELEPORT_XAUTH_TEST") == "" {
t.Skip("Skipping test as xauth is not enabled")
}
localUser, err := user.Current()
if login != "" {
localUser, err = user.Lookup(login)
}
require.NoError(t, err)
cred, err := getCmdCredential(localUser)
require.NoError(t, err)
// Create a temporary xauth file path belonging to the user.
tempDir, err := os.MkdirTemp("", "xauth-temp")
require.NoError(t, err)
err = os.Chown(tempDir, int(cred.Uid), int(cred.Gid))
require.NoError(t, err)
xauthTempFilePath := filepath.Join(tempDir, ".Xauthority")
fakeXauthEntry, err := x11.NewFakeXAuthEntry(x11.Display{})
require.NoError(t, err)
// Request a listener from the networking process.
listener, err := proc.ListenX11(ctx, networking.X11Request{
XauthFile: xauthTempFilePath,
ForwardRequestPayload: x11.ForwardRequestPayload{
AuthProtocol: fakeXauthEntry.Proto,
AuthCookie: fakeXauthEntry.Cookie,
},
DisplayOffset: x11.DefaultDisplayOffset,
MaxDisplay: x11.DefaultMaxDisplays,
})
require.NoError(t, err)
t.Cleanup(func() { listener.Close() })
// Make the listener an echo server, since testing with an
// actual X client and server is not feasible
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return
}
_, _ = io.Copy(conn, conn)
conn.Close()
}
}()
display, err := x11.ParseDisplayFromUnixSocket(listener.Addr().String())
require.NoError(t, err)
// Try connecting to the x11 listener to ensure it's working.
conn, err := display.Dial()
require.NoError(t, err)
echoMsg := []byte("echo")
_, err = conn.Write(echoMsg)
require.NoError(t, err)
buf := make([]byte, 4)
_, err = conn.Read(buf)
require.NoError(t, err)
require.Equal(t, echoMsg, buf)
// Check that the xauth entry was stored for the listener's corresponding x11 display
// in the user's xauth file.
fakeXauthEntry.Display = display
xauthCmd := x11.NewXAuthCommand(ctx, xauthTempFilePath)
xauthCmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
Credential: cred,
}
readXauthEntry, err := xauthCmd.ReadEntry(display)
require.NoError(t, err)
require.Equal(t, fakeXauthEntry, readXauthEntry)
}
+111 -258
View File
@@ -34,8 +34,6 @@ import (
"runtime"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/gravitational/trace"
@@ -69,10 +67,10 @@ import (
"github.com/gravitational/teleport/lib/srv"
"github.com/gravitational/teleport/lib/srv/ingress"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/sshutils/networking"
"github.com/gravitational/teleport/lib/sshutils/x11"
"github.com/gravitational/teleport/lib/teleagent"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/uds"
)
var log = logrus.WithFields(logrus.Fields{
@@ -1111,157 +1109,54 @@ func (s *Server) getServerResource() (types.Resource, error) {
return s.getServerInfo(), nil
}
// getDirectTCPIPForwarder sets up a connection-level subprocess that handles forwarding connections. Subsequent
// calls from the same connection context reuse the same forwarder.
func (s *Server) getDirectTCPIPForwardDialer(scx *srv.ServerContext) (sshutils.TCPIPForwardDialer, error) {
if d, ok := scx.Parent().GetDirectTCPIPForwardDialer(); ok {
return d, nil
}
proc, err := s.startForwardingSubprocess(scx)
// dialTCPIP dials the given tcpip address through the network process.
func (s *Server) dialTCPIP(scx *srv.ServerContext, addr string) (net.Conn, error) {
proc, err := s.getNetworkingProcess(scx)
if err != nil {
return nil, trace.Wrap(err)
}
// set up a dial function that sends the address + fd as a unix datagram message. the forwarder subprocess
// interprets all such messages in this way, and will dial the specified address and proxy all traffic
// to the desired endpoint.
dialer := func(addr string) (net.Conn, error) {
local, remote, err := uds.NewSocketpair(uds.SocketTypeStream)
if err != nil {
return nil, trace.Wrap(err)
}
defer remote.Close()
remoteFD, err := remote.File()
if err != nil {
local.Close()
return nil, trace.Wrap(err)
}
defer remoteFD.Close()
_, _, err = uds.WriteWithFDs(proc.Conn, []byte(addr), []*os.File{remoteFD})
if err != nil {
local.Close()
return nil, trace.Wrap(err)
}
return local, nil
conn, err := proc.Dial(context.Background(), "tcp", addr)
if err != nil {
return nil, trace.Wrap(err)
}
// try to register with the parent context.
if other, ok := scx.Parent().TrySetDirectTCPIPForwardDialer(dialer); !ok {
// another forwarder was concurrently created. this isn't actually a problem, multiple forwarders
// being registered is harmless, but it does result in slightly higher resource utilization, so its
// preferable to use the existing forwarder and close ours in the background.
go proc.Close()
return other, nil
}
// successfully registered this dialer, add closer to context.
scx.Parent().AddCloser(proc)
return dialer, nil
return conn, nil
}
// listenTCPIP creates a new listener in the forwarding process.
func (s *Server) listenTCPIP(scx *srv.ServerContext, addr string) (*net.TCPListener, error) {
proc, err := s.getTCPIPForwardProcess(scx)
// listenTCPIP creates a new listener in the networking process.
func (s *Server) listenTCPIP(scx *srv.ServerContext, addr string) (net.Listener, error) {
proc, err := s.getNetworkingProcess(scx)
if err != nil {
return nil, trace.Wrap(err)
}
// Create a socket to receive new connections on.
localConn, remoteConn, err := uds.NewSocketpair(uds.SocketTypeDatagram)
if err != nil {
return nil, trace.Wrap(err)
}
defer localConn.Close()
defer remoteConn.Close()
remoteFD, err := remoteConn.File()
if err != nil {
localConn.Close()
return nil, trace.Wrap(err)
}
defer remoteFD.Close()
_, _, err = uds.WriteWithFDs(proc.Conn, []byte(addr), []*os.File{remoteFD})
listener, err := proc.Listen(context.Background(), "tcp", addr)
if err != nil {
return nil, trace.Wrap(err)
}
// The forwarding process may have chosen its own port, so we need to get the
// new listen address.
fileCh := make(chan *os.File)
// Read addr in another goroutine so we can cancel it if the forwarding process
// stops.
go func() {
defer close(fileCh)
fbuf := make([]*os.File, 1)
if _, fn, _ := uds.ReadWithFDs(localConn, nil, fbuf); fn == 0 {
fileCh <- nil
}
select {
case fileCh <- fbuf[0]:
case <-proc.Done:
fbuf[0].Close()
}
}()
var listenerFD *os.File
select {
case <-proc.Done:
localConn.Close()
return nil, trace.Errorf("forwarding process is closed")
case listenerFD = <-fileCh:
}
if listenerFD == nil {
return nil, trace.BadParameter("forwarding process did not return a listener")
}
if err := validateListenerSocket(scx, localConn, listenerFD); err != nil {
return nil, trace.Wrap(err)
}
listener, err := net.FileListener(listenerFD)
tcpListener, ok := listener.(*net.TCPListener)
if !ok {
return nil, trace.BadParameter("listener is not a TCPListener")
}
return tcpListener, trace.Wrap(err)
return listener, nil
}
func controlSyscallConn(conn syscall.Conn, f func(fd uintptr) error) error {
syscallConn, err := conn.SyscallConn()
if err != nil {
return trace.Wrap(err)
}
if cErr := syscallConn.Control(func(fd uintptr) {
err = f(fd)
}); cErr != nil {
return trace.Wrap(cErr)
}
return trace.Wrap(err)
}
// getDirectTCPIPForwarder sets up a connection-level subprocess that handles
// remote forwarding connections. Subsequent calls from the same connection
// context reuse the same forwarder.
func (s *Server) getTCPIPForwardProcess(scx *srv.ServerContext) (*sshutils.TCPIPForwardProcess, error) {
if proc, ok := scx.Parent().GetTCPIPForwardProcess(); ok {
// getNetworkingProcess sets up a connection-level subprocess that handles
// networking requests. Subsequent calls from the same connection context
// reuse the same networking process.
func (s *Server) getNetworkingProcess(scx *srv.ServerContext) (*networking.Process, error) {
if proc, ok := scx.Parent().GetNetworkingProcess(); ok {
return proc, nil
}
proc, err := s.startForwardingSubprocess(scx)
proc, err := s.startNetworkingProcess(scx)
if err != nil {
return nil, trace.Wrap(err)
}
// Try to register with the parent context.
if otherProc, ok := scx.Parent().TrySetTCPIPForwardProcess(proc); !ok {
// Another forwarder was concurrently created. this isn't actually a problem, multiple forwarders
// being registered is harmless, but it does result in slightly higher resource utilization, so its
// preferable to use the existing forwarder and close ours in the background.
if otherProc, ok := scx.Parent().SetNetworkingProcess(proc); !ok {
// Another networking process was concurrently created. this isn't actually a problem, multiple networking
// processes being registered is harmless, but it does result in slightly higher resource utilization, so
// it's preferable to use the existing networking process and close ours in the background.
go proc.Close()
return otherProc, nil
}
@@ -1270,110 +1165,36 @@ func (s *Server) getTCPIPForwardProcess(scx *srv.ServerContext) (*sshutils.TCPIP
return proc, nil
}
// startForwardingSubprocess launches the forwarding process. It returns a
// conn to communicate with the process and a close func to close the process
// when finished.
func (s *Server) startForwardingSubprocess(scx *srv.ServerContext) (*sshutils.TCPIPForwardProcess, error) {
// Create the socket to communicate over.
remoteConn, localConn, err := uds.NewSocketpair(uds.SocketTypeDatagram)
if err != nil {
// startNetworkingProcess launches a new networking process. It should be closed once
// the server connection is closed.
func (s *Server) startNetworkingProcess(scx *srv.ServerContext) (*networking.Process, error) {
// The networking process runs with the user's local credentials.
// Ensure that the user has been created first.
if err := s.termHandlers.SessionRegistry.TryCreateHostUser(scx); err != nil {
return nil, trace.Wrap(err)
}
defer remoteConn.Close()
remoteFD, err := remoteConn.File()
if err != nil {
localConn.Close()
if err := s.termHandlers.SessionRegistry.TryWriteSudoersFile(scx); err != nil {
return nil, trace.Wrap(err)
}
defer remoteFD.Close()
// Create command to re-exec Teleport which will handle forwarding. The
// Create context for the networking process.
_, nsctx, err := srv.NewServerContext(context.Background(), scx.ConnectionContext, s, scx.Identity)
if err != nil {
return nil, trace.Wrap(err)
}
nsctx.ExecType = teleport.NetworkingSubCommand
scx.Parent().AddCloser(nsctx)
// Create command to re-exec Teleport which will handle networking requests. The
// reason it's not done directly is because the PAM stack needs to be called
// from the child process.
cmd, err := srv.ConfigureCommand(scx, remoteFD)
cmd, err := srv.ConfigureCommand(nsctx)
if err != nil {
localConn.Close()
return nil, trace.Wrap(err)
}
// Propagate stderr from the spawned Teleport process to log any errors.
cmd.Stderr = os.Stderr
// Start the child process that will be used to listen for connections.
if err := cmd.Start(); err != nil {
localConn.Close()
return nil, trace.Wrap(err)
}
cdone := make(chan struct{})
var explicitlyClosed atomic.Bool
go func() {
defer close(cdone)
defer localConn.Close()
// Ensure unexpected cmd failures get logged.
if err := cmd.Wait(); err != nil && !explicitlyClosed.Load() {
s.Logger.Warnf("Remote forwarder process exited early with unexpected error: %v", err)
}
}()
processCloser := utils.CloseFunc(func() error {
// Set flag indicating that the exit of the child is expected (changes logging behavior).
explicitlyClosed.Store(true)
localConn.Close()
// We expect closing the conn to cause the child process to exit, but it's
// best to verify.
select {
case <-cdone:
case <-time.After(time.Second * 3):
// forcibly kill the child.
s.Logger.Warn("Forcibly terminating remote forwarder subprocess.")
cmd.Process.Kill()
}
return nil
})
return &sshutils.TCPIPForwardProcess{
Conn: localConn,
Done: cdone,
Closer: processCloser,
}, nil
}
// serveAgent will build the a sock path for this user and serve an SSH agent on unix socket.
func (s *Server) serveAgent(ctx *srv.ServerContext) error {
// gather information about user and process. this will be used to set the
// socket path and permissions
systemUser, err := user.Lookup(ctx.Identity.Login)
if err != nil {
return trace.ConvertSystemError(err)
}
pid := os.Getpid()
socketDir := "teleport"
socketName := fmt.Sprintf("teleport-%v.socket", pid)
// start an agent server on a unix socket. each incoming connection
// will result in a separate agent request.
agentServer := teleagent.NewServer(ctx.Parent().StartAgentChannel)
err = agentServer.ListenUnixSocket(socketDir, socketName, systemUser)
if err != nil {
return trace.Wrap(err)
}
ctx.Parent().SetEnv(teleport.SSHAuthSock, agentServer.Path)
ctx.Parent().SetEnv(teleport.SSHAgentPID, fmt.Sprintf("%v", pid))
ctx.Parent().AddCloser(agentServer)
ctx.Debugf("Starting agent server for Teleport user %v and socket %v.", ctx.Identity.TeleportUser, agentServer.Path)
go func() {
if err := agentServer.Serve(); err != nil {
ctx.Errorf("agent server for user %q stopped: %v", ctx.Identity.TeleportUser, err)
}
}()
return nil
proc, err := networking.NewProcess(nsctx.Context, cmd)
return proc, trace.Wrap(err)
}
// HandleRequest processes global out-of-band requests. Global out-of-band
@@ -1631,7 +1452,10 @@ func (s *Server) handleDirectTCPIPRequest(ctx context.Context, ccx *sshutils.Con
return
}
dialFunc, err := s.getDirectTCPIPForwardDialer(scx)
scx.Debugf("Opening direct-tcpip channel from %v to %v.", scx.SrcAddr, scx.DstAddr)
defer scx.Debugf("Closing direct-tcpip channel from %v to %v.", scx.SrcAddr, scx.DstAddr)
conn, err := s.dialTCPIP(scx, scx.DstAddr)
if err != nil {
if errors.Is(err, trace.NotFound(user.UnknownUserError(scx.Identity.Login).Error())) || errors.Is(err, trace.BadParameter("unknown user")) {
// user does not exist for the provided login. Terminate the connection.
@@ -1647,15 +1471,6 @@ func (s *Server) handleDirectTCPIPRequest(ctx context.Context, ccx *sshutils.Con
return
}
scx.Debugf("Opening direct-tcpip channel from %v to %v.", scx.SrcAddr, scx.DstAddr)
defer scx.Debugf("Closing direct-tcpip channel from %v to %v.", scx.SrcAddr, scx.DstAddr)
conn, err := dialFunc(scx.DstAddr)
if err != nil {
writeStderr(channel, err.Error())
return
}
if err := utils.ProxyConn(ctx, conn, channel); err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, os.ErrClosed) {
s.Logger.Warnf("Connection problem in direct-tcpip channel: %v %T.", trace.DebugReport(err), err)
}
@@ -1882,12 +1697,6 @@ func (s *Server) dispatch(ctx context.Context, ch ssh.Channel, req *ssh.Request,
}
switch req.Type {
case sshutils.ExecRequest:
if err := s.termHandlers.SessionRegistry.TryCreateHostUser(serverContext); err != nil {
return trace.Wrap(err)
}
if err := s.termHandlers.SessionRegistry.TryWriteSudoersFile(serverContext); err != nil {
return trace.Wrap(err)
}
return s.termHandlers.HandleExec(ctx, ch, req, serverContext)
case sshutils.PTYRequest:
return s.termHandlers.HandlePTYReq(ctx, ch, req, serverContext)
@@ -1915,7 +1724,7 @@ func (s *Server) dispatch(ctx context.Context, ch ssh.Channel, req *ssh.Request,
// subsystems are SSH subsystems defined in http://tools.ietf.org/html/rfc4254 6.6
// they are in essence SSH session extensions, allowing to implement new SSH commands
return s.handleSubsystem(ctx, ch, req, serverContext)
case sshutils.X11ForwardRequest:
case x11.ForwardRequest:
return s.handleX11Forward(ch, req, serverContext)
case sshutils.AgentForwardRequest:
// This happens when SSH client has agent forwarding enabled, in this case
@@ -1924,14 +1733,6 @@ func (s *Server) dispatch(ctx context.Context, ch ssh.Channel, req *ssh.Request,
// https://tools.ietf.org/html/draft-ietf-secsh-agent-02
// the open ssh proto spec that we implement is here:
// http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.agent
if err := s.termHandlers.SessionRegistry.TryCreateHostUser(serverContext); err != nil {
s.Logger.Warn(err)
return nil
}
if err := s.termHandlers.SessionRegistry.TryWriteSudoersFile(serverContext); err != nil {
s.Logger.Warn(err)
return nil
}
// to maintain interoperability with OpenSSH, agent forwarding requests
// should never fail, all errors should be logged and we should continue
@@ -1956,23 +1757,49 @@ func (s *Server) dispatch(ctx context.Context, ch ssh.Channel, req *ssh.Request,
// handleAgentForwardNode will create a unix socket and serve the agent running
// on the client on it.
func (s *Server) handleAgentForwardNode(req *ssh.Request, ctx *srv.ServerContext) error {
func (s *Server) handleAgentForwardNode(_ *ssh.Request, scx *srv.ServerContext) error {
// check if the user's RBAC role allows agent forwarding
err := s.authHandlers.CheckAgentForward(ctx)
err := s.authHandlers.CheckAgentForward(scx)
if err != nil {
return trace.Wrap(err)
}
// Enable agent forwarding for the broader connection-level
// context.
ctx.Parent().SetForwardAgent(true)
scx.Parent().SetForwardAgent(true)
if err := s.serveAgent(scx); err != nil {
return trace.Wrap(err)
}
// serve an agent on a unix socket on this node
err = s.serveAgent(ctx)
return nil
}
// serveAgent will build the a sock path for this user and serve an SSH agent on unix socket.
func (s *Server) serveAgent(scx *srv.ServerContext) error {
proc, err := s.getNetworkingProcess(scx)
if err != nil {
return trace.Wrap(err)
}
listener, err := proc.ListenAgent(context.Background())
if err != nil {
return trace.Wrap(err)
}
// start an agent server on a unix socket. each incoming connection
// will result in a separate agent request.
agentServer := teleagent.NewServer(scx.Parent().StartAgentChannel)
agentServer.SetListener(listener)
scx.Parent().AddCloser(agentServer)
scx.Parent().SetEnv(teleport.SSHAuthSock, listener.Addr().String())
scx.Parent().SetEnv(teleport.SSHAgentPID, fmt.Sprintf("%v", os.Getpid()))
scx.Debugf("Starting agent server for Teleport user %v and socket %v.", scx.Identity.TeleportUser, agentServer.Path)
go func() {
if err := agentServer.Serve(); err != nil {
scx.Errorf("agent server for user %q stopped: %v", scx.Identity.TeleportUser, err)
}
}()
return nil
}
@@ -1981,7 +1808,7 @@ func (s *Server) handleAgentForwardNode(req *ssh.Request, ctx *srv.ServerContext
// request will do nothing. To maintain interoperability, agent forwarding
// requests should never fail, all errors should be logged and we should
// continue processing requests.
func (s *Server) handleAgentForwardProxy(req *ssh.Request, ctx *srv.ServerContext) error {
func (s *Server) handleAgentForwardProxy(_ *ssh.Request, ctx *srv.ServerContext) error {
// Forwarding an agent to the proxy is only supported when the proxy is in
// recording mode.
if !services.IsRecordAtProxy(ctx.SessionRecordingConfig.GetMode()) {
@@ -2002,16 +1829,16 @@ func (s *Server) handleAgentForwardProxy(req *ssh.Request, ctx *srv.ServerContex
}
// handleX11Forward handles an X11 forwarding request from the client.
func (s *Server) handleX11Forward(ch ssh.Channel, req *ssh.Request, ctx *srv.ServerContext) (err error) {
func (s *Server) handleX11Forward(ch ssh.Channel, req *ssh.Request, scx *srv.ServerContext) (err error) {
event := &apievents.X11Forward{
Metadata: apievents.Metadata{
Type: events.X11ForwardEvent,
Code: events.X11ForwardCode,
},
UserMetadata: ctx.Identity.GetUserMetadata(),
UserMetadata: scx.Identity.GetUserMetadata(),
ConnectionMetadata: apievents.ConnectionMetadata{
LocalAddr: ctx.ServerConn.LocalAddr().String(),
RemoteAddr: ctx.ServerConn.RemoteAddr().String(),
LocalAddr: scx.ServerConn.LocalAddr().String(),
RemoteAddr: scx.ServerConn.RemoteAddr().String(),
},
Status: apievents.Status{
Success: true,
@@ -2041,7 +1868,7 @@ func (s *Server) handleX11Forward(ch ssh.Channel, req *ssh.Request, ctx *srv.Ser
}
// Check if the user's RBAC role allows X11 forwarding.
if err := s.authHandlers.CheckX11Forward(ctx); err != nil {
if err := s.authHandlers.CheckX11Forward(scx); err != nil {
return trace.Wrap(err)
}
@@ -2050,7 +1877,22 @@ func (s *Server) handleX11Forward(ch ssh.Channel, req *ssh.Request, ctx *srv.Ser
return trace.Wrap(err)
}
if err := ctx.OpenXServerListener(x11Req, s.x11.DisplayOffset, s.x11.MaxDisplay); err != nil {
proc, err := s.getNetworkingProcess(scx)
if err != nil {
return trace.Wrap(err)
}
listener, err := proc.ListenX11(context.Background(), networking.X11Request{
ForwardRequestPayload: x11Req,
DisplayOffset: s.x11.DisplayOffset,
MaxDisplay: s.x11.MaxDisplay,
})
if err != nil {
return trace.Wrap(err)
}
scx.Parent().AddCloser(listener)
if err := scx.HandleX11Listener(listener, x11Req.SingleConnection); err != nil {
if trace.IsLimitExceeded(err) {
return trace.AccessDenied("The server cannot support any more X11 forwarding sessions at this time")
}
@@ -2364,6 +2206,17 @@ func (s *Server) handleTCPIPForwardRequest(ctx context.Context, ccx *sshutils.Co
}
s.remoteForwardingMap.Store(scx.SrcAddr, listener)
// Close the listener once the connection is closed, if it hasn't
// been closed already via a cancel-tcpip-forward request.
ccx.AddCloser(utils.CloseFunc(func() error {
listener, ok := s.remoteForwardingMap.LoadAndDelete(scx.SrcAddr)
if ok {
return trace.Wrap(listener.Close())
}
return nil
}))
return nil
}
-35
View File
@@ -1,35 +0,0 @@
//go:build !darwin && !linux
// +build !darwin,!linux
/*
* Teleport
* Copyright (C) 2024 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 (
"net"
"os"
"runtime"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/srv"
)
func validateListenerSocket(scx *srv.ServerContext, controlConn *net.UnixConn, listenerFD *os.File) error {
return trace.BadParameter("remote forwarding is not implemented for %s", runtime.GOOS)
}
+45 -87
View File
@@ -33,10 +33,7 @@ import (
"os"
"os/exec"
"os/user"
"strconv"
"strings"
"sync"
"syscall"
"testing"
"time"
@@ -1001,7 +998,9 @@ func TestX11Forward(t *testing.T) {
MaxDisplay: x11.DefaultMaxDisplays,
}
ctx := context.Background()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
roleName := services.RoleNameForUser(f.user)
role, err := f.testSrv.Auth().GetRole(ctx, roleName)
require.NoError(t, err)
@@ -1021,79 +1020,28 @@ func TestX11Forward(t *testing.T) {
// Create multiple XServer requests, the server should
// handle multiple concurrent XServer requests.
var wg sync.WaitGroup
wg.Add(1)
errCh := make(chan error)
go func() {
defer wg.Done()
x11EchoRequest(t, serverDisplay)
errCh <- x11EchoRequest(serverDisplay)
}()
wg.Add(1)
go func() {
defer wg.Done()
x11EchoRequest(t, serverDisplay)
errCh <- x11EchoRequest(serverDisplay)
}()
wg.Add(1)
go func() {
defer wg.Done()
x11EchoRequest(t, serverDisplay2)
errCh <- x11EchoRequest(serverDisplay2)
}()
wg.Add(1)
go func() {
defer wg.Done()
x11EchoRequest(t, serverDisplay2)
errCh <- x11EchoRequest(serverDisplay2)
}()
wg.Wait()
}
// TestRootX11ForwardPermissions tests that X11 forwarding sessions are set up
// with the connecting user's file permissions (where needed), rather than root.
func TestRootX11ForwardPermissions(t *testing.T) {
utils.RequireRoot(t)
if os.Getenv("TELEPORT_XAUTH_TEST") == "" {
t.Skip("Skipping test as xauth is not enabled")
for i := 0; i > 4; i++ {
select {
case err := <-errCh:
assert.NoError(t, err)
case <-ctx.Done():
assert.NoError(t, context.Cause(ctx))
}
}
t.Parallel()
f := newFixtureWithoutDiskBasedLogging(t)
f.ssh.srv.x11 = &x11.ServerConfig{
Enabled: true,
DisplayOffset: x11.DefaultDisplayOffset,
MaxDisplay: x11.DefaultMaxDisplays,
}
ctx := context.Background()
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)
// Create a new X11 session as a non-root nonroot in the system.
nonroot, err := user.LookupId("1000")
require.NoError(t, err)
client := f.newSSHClient(ctx, t, nonroot)
serverDisplay := x11EchoSession(ctx, t, client)
// Check that the xauth entry is readable for the connecting user.
xauthCmd := x11.NewXAuthCommand(ctx, "")
uid, err := strconv.ParseUint(nonroot.Uid, 10, 32)
require.NoError(t, err)
gid, err := strconv.ParseUint(nonroot.Gid, 10, 32)
require.NoError(t, err)
xauthCmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
Credential: &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
},
}
xauthCmd.Dir = nonroot.HomeDir
xauthCmd.Env = []string{fmt.Sprintf("HOME=%v", nonroot.HomeDir)}
_, err = xauthCmd.ReadEntry(serverDisplay)
require.NoError(t, err)
}
// x11EchoSession creates a new ssh session and handles x11 forwarding for the session,
@@ -1115,7 +1063,7 @@ func x11EchoSession(ctx context.Context, t *testing.T, clt *tracessh.Client) x11
return
}
_, err = io.Copy(conn, conn)
require.NoError(t, err)
assert.NoError(t, err)
conn.Close()
}
}()
@@ -1125,13 +1073,13 @@ func x11EchoSession(ctx context.Context, t *testing.T, clt *tracessh.Client) x11
// and start x11 forwarding to the client display.
err = x11.ServeChannelRequests(ctx, clt.Client, func(ctx context.Context, nch ssh.NewChannel) {
sch, sin, err := nch.Accept()
require.NoError(t, err)
assert.NoError(t, err)
defer sch.Close()
clientConn, err := net.Dial("tcp", fakeClientDisplay.Addr().String())
require.NoError(t, err)
assert.NoError(t, err)
clientXConn, ok := clientConn.(*net.TCPConn)
require.True(t, ok)
assert.True(t, ok)
defer clientConn.Close()
go func() {
@@ -1141,8 +1089,8 @@ func x11EchoSession(ctx context.Context, t *testing.T, clt *tracessh.Client) x11
}
}()
err = x11.Forward(ctx, clientXConn, sch)
require.NoError(t, err)
err = utils.ProxyConn(ctx, clientXConn, sch)
assert.NoError(t, err)
})
require.NoError(t, err)
@@ -1184,7 +1132,7 @@ func x11EchoSession(ctx context.Context, t *testing.T, clt *tracessh.Client) x11
return true
}
return false
}, 5*time.Second, 10*time.Millisecond, "failed to read display")
}, 10*time.Second, 100*time.Millisecond, "failed to read display")
// Make a new connection to the XServer proxy, the client
// XServer should echo back anything written on it.
@@ -1196,20 +1144,30 @@ func x11EchoSession(ctx context.Context, t *testing.T, clt *tracessh.Client) x11
// x11EchoRequest sends a message to the serverDisplay and expects the
// server to echo the message back to it.
func x11EchoRequest(t *testing.T, serverDisplay x11.Display) {
func x11EchoRequest(serverDisplay x11.Display) error {
conn, err := serverDisplay.Dial()
require.NoError(t, err)
if err != nil {
return err
}
defer conn.Close()
msg := "msg"
_, err = conn.Write([]byte(msg))
require.NoError(t, err)
err = conn.CloseWrite()
require.NoError(t, err)
if err != nil {
return err
}
echo, err := io.ReadAll(conn)
require.NoError(t, err)
require.Equal(t, msg, string(echo))
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) {
@@ -2202,7 +2160,7 @@ func x11Handler(ctx context.Context, conn *ssh.ServerConn, chs <-chan ssh.NewCha
return nil
}
if req.Type != sshutils.X11ForwardRequest {
if req.Type != x11.ForwardRequest {
return trace.BadParameter("Unexpected request type %q", req.Type)
}
@@ -2211,7 +2169,7 @@ func x11Handler(ctx context.Context, conn *ssh.ServerConn, chs <-chan ssh.NewCha
}
// start a fake X11 channel
xch, _, err := conn.OpenChannel(sshutils.X11ChannelRequest, nil)
xch, _, err := conn.OpenChannel(x11.ChannelRequest, nil)
if err != nil {
return trace.Wrap(err)
}
@@ -2554,11 +2512,11 @@ func TestX11ProxySupport(t *testing.T) {
require.NoError(t, err)
// register X11 channel handler before requesting forwarding to avoid races
xchs := clt.HandleChannelOpen(sshutils.X11ChannelRequest)
xchs := clt.HandleChannelOpen(x11.ChannelRequest)
require.NotNil(t, xchs)
// Send an X11 forwarding request to the server
ok, err = sess.SendRequest(ctx, sshutils.X11ForwardRequest, true, nil)
ok, err = sess.SendRequest(ctx, x11.ForwardRequest, true, nil)
require.NoError(t, err)
require.True(t, ok)
@@ -2570,7 +2528,7 @@ func TestX11ProxySupport(t *testing.T) {
require.Fail(t, "Timeout waiting for X11 channel open from %v", node.addr)
}
require.NotNil(t, xnc)
require.Equal(t, sshutils.X11ChannelRequest, xnc.ChannelType())
require.Equal(t, x11.ChannelRequest, xnc.ChannelType())
xch, _, err := xnc.Accept()
require.NoError(t, err)
-68
View File
@@ -1,68 +0,0 @@
//go:build unix
// +build unix
/*
* Teleport
* Copyright (C) 2024 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 (
"net"
"os"
"github.com/gravitational/trace"
"golang.org/x/sys/unix"
"github.com/gravitational/teleport/lib/srv"
)
// validateListenerSocket checks that the socket and listener file descriptor
// sent from the forwarding process have the expected properties.
func validateListenerSocket(_ *srv.ServerContext, _ *net.UnixConn, listenerFD *os.File) error {
if err := controlSyscallConn(listenerFD, func(fd uintptr) error {
// Verify the socket type
if sockType, err := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_TYPE); err != nil {
return trace.Wrap(err)
} else if sockType != unix.SOCK_STREAM {
return trace.AccessDenied("socket is not of the expected type (STREAM)")
}
// Verify that reuse is not enabled on the socket
if reuseAddr, err := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR); err != nil {
return trace.Wrap(err)
} else if reuseAddr != 0 {
return trace.AccessDenied("SO_REUSEADDR is enabled on the socket")
}
if reusePort, err := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT); err != nil {
// Some systems may not support SO_REUSEPORT, so we ignore the error here
} else if reusePort != 0 {
return trace.AccessDenied("SO_REUSEPORT is enabled on the socket")
}
// Verify that the listener is already listening.
if acceptConn, err := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_ACCEPTCONN); err != nil {
return trace.Wrap(err)
} else if acceptConn == 0 {
return trace.AccessDenied("SO_ACCEPTCONN is not set, socket is not listening")
}
return nil
}); err != nil {
return trace.Wrap(err)
}
return nil
}
-102
View File
@@ -22,120 +22,18 @@ package regular
import (
"context"
"net"
"os"
"os/user"
"sync"
"syscall"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
tracessh "github.com/gravitational/teleport/api/observability/tracing/ssh"
"github.com/gravitational/teleport/lib/srv"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/host"
"github.com/gravitational/teleport/lib/utils/uds"
)
func TestValidateListenerSocket(t *testing.T) {
t.Parallel()
newSocketFiles := func(t *testing.T) (*net.UnixConn, *os.File) {
left, right, err := uds.NewSocketpair(uds.SocketTypeStream)
require.NoError(t, err)
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
tcpListener := listener.(*net.TCPListener)
listenerFD, err := tcpListener.File()
require.NoError(t, err)
conn, err := tcpListener.SyscallConn()
require.NoError(t, err)
err2 := conn.Control(func(descriptor uintptr) {
// Disable address reuse to prevent socket replacement.
err = syscall.SetsockoptInt(int(descriptor), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 0)
})
require.NoError(t, err2)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, left.Close())
require.NoError(t, right.Close())
})
return left, listenerFD
}
tests := []struct {
name string
mutateFiles func(*testing.T, *net.UnixConn, *os.File) (*net.UnixConn, *os.File)
mutateConn func(*testing.T, *os.File)
assert require.ErrorAssertionFunc
}{
{
name: "ok",
assert: require.NoError,
},
{
name: "socket type not STREAM",
mutateFiles: func(t *testing.T, conn *net.UnixConn, file *os.File) (*net.UnixConn, *os.File) {
left, right, err := uds.NewSocketpair(uds.SocketTypeDatagram)
require.NoError(t, err)
listenerFD, err := right.File()
require.NoError(t, err)
require.NoError(t, right.Close())
t.Cleanup(func() {
require.NoError(t, left.Close())
require.NoError(t, listenerFD.Close())
})
return left, listenerFD
},
assert: require.Error,
},
{
name: "SO_REUSEADDR enabled",
mutateConn: func(t *testing.T, file *os.File) {
fd := file.Fd()
err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
require.NoError(t, err)
},
assert: require.Error,
},
{
name: "listener socket is not listening",
mutateFiles: func(t *testing.T, conn *net.UnixConn, file *os.File) (*net.UnixConn, *os.File) {
left, right, err := uds.NewSocketpair(uds.SocketTypeStream)
require.NoError(t, err)
listenerFD, err := right.File()
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, left.Close())
require.NoError(t, listenerFD.Close())
})
return left, listenerFD
},
assert: require.Error,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
conn, listenerFD := newSocketFiles(t)
if tc.mutateFiles != nil {
conn, listenerFD = tc.mutateFiles(t, conn, listenerFD)
}
if tc.mutateConn != nil {
tc.mutateConn(t, listenerFD)
}
err := validateListenerSocket(&srv.ServerContext{}, conn, listenerFD)
tc.assert(t, err)
})
}
}
// BenchmarkRootExecCommand measures performance of running multiple exec requests
// over a single ssh connection. The same test is run with and without host user
// creation support to catch any performance degradation caused by user provisioning.
+12 -59
View File
@@ -30,35 +30,10 @@ import (
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"github.com/gravitational/teleport/lib/sshutils/networking"
"github.com/gravitational/teleport/lib/teleagent"
)
// TCPIPForwardDialer represents a dialer used to handle TCPIP forward requests.
type TCPIPForwardDialer func(string) (net.Conn, error)
// TCPIPForwardProcess represents an instance of a port forwarding process.
type TCPIPForwardProcess struct {
// Conn is the socket used to request a dialer or listener in the process.
Conn *net.UnixConn
// Done signals when the process completes.
Done <-chan struct{}
// Closer contains and extra io.Closer to run when the process as a whole
// is closed.
Closer io.Closer
}
// Close stops the process and frees up its related resources.
func (p *TCPIPForwardProcess) Close() error {
var errs []error
if p.Conn != nil {
errs = append(errs, p.Conn.Close())
}
if p.Closer != nil {
errs = append(errs, p.Closer.Close())
}
return trace.NewAggregate(errs...)
}
// ConnectionContext manages connection-level state.
type ConnectionContext struct {
// NetConn is the base connection object.
@@ -82,12 +57,9 @@ type ConnectionContext struct {
// when handling node-side connections for users with MaxSessions applied.
sessions int64
// tcpipForwardDialer is a lazily initialized dialer used to handle all tcpip
// forwarding requests.
tcpipForwardDialer TCPIPForwardDialer
// tcpipForwardProcess is a lazily initialized connection to the subprocess that
// handles remote port forwarding.
tcpipForwardProcess *TCPIPForwardProcess
// networkingProcess is a lazily initialized connection to the subprocess that
// handles connection-level networking requests. e.g. port/agent forwarding.
networkingProcess *networking.Process
// closers is a list of io.Closer that will be called when session closes
// this is handy as sometimes client closes session, in this case resources
@@ -258,25 +230,6 @@ func (c *ConnectionContext) UpdateClientActivity() {
c.clientLastActive = c.clock.Now().UTC()
}
// TrySetDirectTCPIPForwardDialer attempts to registers a DirectTCPIPForwardDialer. If a different dialer was
// concurrently registered, ok is false and the previously registered dialer is returned.
func (c *ConnectionContext) TrySetDirectTCPIPForwardDialer(d TCPIPForwardDialer) (registered TCPIPForwardDialer, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
if c.tcpipForwardDialer != nil {
return c.tcpipForwardDialer, false
}
c.tcpipForwardDialer = d
return c.tcpipForwardDialer, true
}
// GetDirectTCPIPForwardDialer gets the registered DirectTCPIPForwardDialer if one exists.
func (c *ConnectionContext) GetDirectTCPIPForwardDialer() (d TCPIPForwardDialer, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
return c.tcpipForwardDialer, c.tcpipForwardDialer != nil
}
// AddCloser adds any closer in ctx that will be called
// when the underlying connection is closed.
func (c *ConnectionContext) AddCloser(closer io.Closer) {
@@ -291,24 +244,24 @@ func (c *ConnectionContext) AddCloser(closer io.Closer) {
c.closers = append(c.closers, closer)
}
// TrySetTCPIPForwardProcess attempts to registers a TCPIPForwardProcess. If a
// SetNetworkingProcess attempts to registers a networking process. If a
// different process was concurrently registered, ok is false and the previously
// registered process is returned.
func (c *ConnectionContext) TrySetTCPIPForwardProcess(proc *TCPIPForwardProcess) (*TCPIPForwardProcess, bool) {
func (c *ConnectionContext) SetNetworkingProcess(proc *networking.Process) (*networking.Process, bool) {
c.mu.Lock()
defer c.mu.Unlock()
if c.tcpipForwardProcess != nil {
return c.tcpipForwardProcess, false
if c.networkingProcess != nil {
return c.networkingProcess, false
}
c.tcpipForwardProcess = proc
c.networkingProcess = proc
return proc, true
}
// GetTCPIPForwardProcess gets the registered TCPIPForwardProcess if one exists.
func (c *ConnectionContext) GetTCPIPForwardProcess() (*TCPIPForwardProcess, bool) {
// GetNetworkingProcess gets the registered networking process if one exists.
func (c *ConnectionContext) GetNetworkingProcess() (*networking.Process, bool) {
c.mu.Lock()
defer c.mu.Unlock()
return c.tcpipForwardProcess, c.tcpipForwardProcess != nil
return c.networkingProcess, c.networkingProcess != nil
}
// takeClosers returns all resources that should be closed and sets the properties to null
+329
View File
@@ -0,0 +1,329 @@
/*
* Teleport
* Copyright (C) 2024 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 networking handles ssh networking requests via the teleport networking subprocess,
// including port forwarding, agent forwarding, and x11 forwarding.
//
// IPC protocol summary:
// - Start: The parent teleport process creates a unix socket pair and passes one side to the
// networking subprocess on start. This is used as a unidirectional pipe for the parent
// to make networking requests.
// - Request: The parent creates a new request-level socket pair and sends one side through the
// main pipe, along with the request payload (e.g. dial tcp 8080).
// - Handle: The subprocess watches for new requests on the main pipe. When a request is received,
// the subprocess prepares a networking file matching the request (e.g. tcp conn file) and writes
// it (or an error) to the request-level socket.
// - Response: The parent reads the networking file from the request-level socket, keeping the file
// and closing the request-level socket.
package networking
import (
"context"
"encoding/json"
"io"
"log/slog"
"net"
"os"
"os/exec"
"sync/atomic"
"time"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/lib/sshutils/x11"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/uds"
)
// RequestBufferSize is the maximum amount of data we're comfortable writing at
// once through a default unix datagram socket. Should fit comfortably in both
// linux and darwin with default settings.
const RequestBufferSize = 1024
// Process represents an instance of a networking process.
type Process struct {
// cmd is the running process command.
cmd *exec.Cmd
// conn is the socket used to request a dialer or listener in the process.
conn *net.UnixConn
// done signals when the process completes.
done chan struct{}
// killed is set to true when the process was killed forcibly.
killed atomic.Bool
}
// Request is a networking request.
type Request struct {
// Operation is a networking operation.
Operation Operation
// Network is a network type.
Network string
// Address is a network address.
Address string
// X11Request contains additional info needed for x11 networking requests.
X11Request X11Request
}
// Operation is a networking operation.
type Operation string
const (
// NetworkingOperationDial is used to connect to a network address.
NetworkingOperationDial Operation = "dial"
// NetworkingOperationListen is used to create a local network listener.
NetworkingOperationListen Operation = "listen"
// NetworkingOperationListenAgent is used to create a local ssh-agent listener.
NetworkingOperationListenAgent Operation = "listen-agent"
// NetworkingOperationListenX11 is used to create a local x11 listener.
NetworkingOperationListenX11 Operation = "listen-x11"
)
// X11Config contains information used by the child process to set up X11 forwarding.
type X11Request struct {
x11.ForwardRequestPayload
// DisplayOffset is the first display that we should try to get a unix socket for.
DisplayOffset int
// MaxDisplay is the last display that we should try to get a unix socket for, if all
// displays before it are taken.
MaxDisplay int
// XauthFile is an optional XauthFile to use instead of the default ~/.Xauthority. Used in tests.
XauthFile string
}
// NewProcess starts a new networking process with the given command, which should
// be pre-configured from a ssh server context with Teleport reexec settings.
func NewProcess(ctx context.Context, cmd *exec.Cmd) (*Process, error) {
// Create the socket to communicate over.
remoteConn, localConn, err := uds.NewSocketpair(uds.SocketTypeDatagram)
if err != nil {
return nil, trace.Wrap(err)
}
defer remoteConn.Close()
remoteFD, err := remoteConn.File()
if err != nil {
localConn.Close()
return nil, trace.Wrap(err)
}
defer remoteFD.Close()
cmd.ExtraFiles = append(cmd.ExtraFiles, remoteFD)
// Propagate stderr from the spawned Teleport process to log any errors.
cmd.Stderr = os.Stderr
proc := &Process{
cmd: cmd,
conn: localConn,
done: make(chan struct{}),
}
if err := proc.start(ctx); err != nil {
return nil, trace.Wrap(err)
}
return proc, nil
}
// start the the networking process.
func (p *Process) start(ctx context.Context) error {
if err := p.cmd.Start(); err != nil {
p.conn.Close()
return trace.Wrap(err)
}
// The child process writes errors to the parent connection for logging purposes.
go func() {
for {
buf := make([]byte, RequestBufferSize)
n, err := p.conn.Read(buf)
if err != nil {
if utils.IsOKNetworkError(err) {
return
}
slog.WarnContext(ctx, "Failed to read error from networking process.", "error", err)
return
}
if n > 0 {
slog.WarnContext(ctx, "Received unexpected error from networking process.", "error", string(buf[:n]))
}
}
}()
go func() {
defer close(p.done)
defer p.conn.Close()
// Ensure unexpected cmd failures get logged.
if err := p.cmd.Wait(); err != nil && !p.killed.Load() {
slog.WarnContext(ctx, "Networking process exited early with unexpected error.", "error", err)
}
}()
return nil
}
// Close stops the process and frees up its related resources.
func (p *Process) Close() error {
p.conn.Close()
select {
case <-p.done:
case <-time.After(time.Second * 3):
slog.WarnContext(context.Background(), "Forcibly terminating networking subprocess.")
p.killed.Store(true)
p.cmd.Process.Kill()
}
return nil
}
// Dial requests a network connection from the networking subprocess.
func (p *Process) Dial(ctx context.Context, network string, addr string) (net.Conn, error) {
connFD, err := p.sendRequest(ctx, Request{
Operation: NetworkingOperationDial,
Network: network,
Address: addr,
})
if err != nil {
return nil, trace.Wrap(err)
}
defer connFD.Close()
conn, err := net.FileConn(connFD)
return conn, trace.Wrap(err)
}
// Listen requests a local listener from the networking subprocess.
func (p *Process) Listen(ctx context.Context, network string, addr string) (net.Listener, error) {
listenerFD, err := p.sendRequest(ctx, Request{
Operation: NetworkingOperationListen,
Network: network,
Address: addr,
})
if err != nil {
return nil, trace.Wrap(err)
}
defer listenerFD.Close()
listener, err := net.FileListener(listenerFD)
return listener, trace.Wrap(err)
}
// ListenAgent requests a local ssh-agent listener from the networking subprocess.
func (p *Process) ListenAgent(ctx context.Context) (net.Listener, error) {
listenerFD, err := p.sendRequest(ctx, Request{
Operation: NetworkingOperationListenAgent,
})
if err != nil {
return nil, trace.Wrap(err)
}
defer listenerFD.Close()
listener, err := net.FileListener(listenerFD)
if err != nil {
return nil, trace.Wrap(err)
}
return listener, trace.Wrap(err)
}
// ListenX11 requests a local XServer listener from the networking subprocess.
func (p *Process) ListenX11(ctx context.Context, req X11Request) (net.Listener, error) {
listenerFD, err := p.sendRequest(ctx, Request{
Operation: NetworkingOperationListenX11,
X11Request: req,
})
if err != nil {
return nil, trace.Wrap(err)
}
defer listenerFD.Close()
listener, err := net.FileListener(listenerFD)
return listener, trace.Wrap(err)
}
// sendRequest sends a networking request to the networking process and waits
// for a file corresponding to an open network connection or listener.
func (p *Process) sendRequest(ctx context.Context, req Request) (*os.File, error) {
ctx, cancel := context.WithTimeout(ctx, defaults.DefaultIOTimeout)
defer cancel()
slog.DebugContext(ctx, "Sending networking request to child process", "request", req)
jsonReq, err := json.Marshal(req)
if err != nil {
return nil, trace.Wrap(err)
}
// We can use a stream instead of a datagram because we only expect bytes
// or a file descriptor in response, not both.
requestConn, remoteConn, err := uds.NewSocketpair(uds.SocketTypeStream)
if err != nil {
return nil, trace.Wrap(err)
}
// Close the request stream once the context is closed or the process is closed.
// This avoids a race condition where the process is closed mid request and thus
// fails to close the stream, resulting in a deadlock on read below.
go func() {
defer requestConn.Close()
select {
case <-ctx.Done():
case <-p.done:
}
}()
remoteFD, err := remoteConn.File()
remoteConn.Close()
if err != nil {
return nil, trace.Wrap(err)
}
defer remoteFD.Close()
if _, _, err = uds.WriteWithFDs(p.conn, jsonReq, []*os.File{remoteFD}); err != nil {
return nil, trace.Wrap(err)
}
file, err := readResponse(requestConn)
if err != nil {
return nil, trace.Wrap(err)
}
return file, nil
}
// readResponse attempts to read a file descriptor from the given connection until it is closed.
func readResponse(conn *net.UnixConn) (*os.File, error) {
buf := make([]byte, RequestBufferSize)
fbuf := make([]*os.File, 1)
n, fn, err := uds.ReadWithFDs(conn, buf, fbuf)
if err != nil {
return nil, trace.Wrap(err)
} else if fn == 0 {
if n > 0 {
// The networking process only ever writes to the request conn if an error occurs.
// Read the rest of the connection to ensure we don't return just a partial stream.
errMsg, err := io.ReadAll(io.LimitReader(conn, int64(cap(buf)-len(buf))))
if err != nil {
return nil, trace.Wrap(err)
}
errMsg = append(buf[:n], errMsg...)
return nil, trace.Errorf("error returned by networking process: %v", string(errMsg))
}
return nil, trace.BadParameter("networking process did not return a listener")
}
return fbuf[0], nil
}
+36 -47
View File
@@ -19,40 +19,27 @@
package x11
import (
"errors"
"fmt"
"math"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"unicode"
"github.com/gravitational/trace"
)
const (
// DefaultDisplayOffset is the default display offset when
// searching for an open XServer unix socket.
DefaultDisplayOffset = 10
// DefaultMaxDisplays is the default maximum number of displays
// supported when searching for an open XServer unix socket.
DefaultMaxDisplays = 1000
// MaxDisplay is the theoretical max display value which
// X Clients and serverwill be able to parse into a unix socket.
MaxDisplayNumber = math.MaxInt32
// DisplayEnv is an environment variable used to determine what
// local display should be connected to during X11 forwarding.
DisplayEnv = "DISPLAY"
// x11SocketDirName is the name of the directory where X11 unix sockets are kept.
x11SocketDirName = ".X11-unix"
// x11BasePort is the base port used for XServer tcp addresses.
// e.g. DISPLAY=localhost:10 -> net.Dial("tcp", "localhost:6010")
// Used by some XServer clients, such as openSSH and MobaXTerm.
x11BasePort = 6000
// x11SocketDirName is the name of the directory where X11 unix sockets are kept.
x11SocketDirName = ".X11-unix"
)
// Display is an XServer display.
@@ -71,53 +58,42 @@ func (d *Display) String() string {
return fmt.Sprintf("%s:%d.%d", d.HostName, d.DisplayNumber, d.ScreenNumber)
}
// Dial opens an XServer connection to the display
func (d *Display) Dial() (XServerConn, error) {
var conn XServerConn
func (d *Display) getNetAddr() (net.Addr, error) {
unixSock, unixErr := d.unixSocket()
if unixErr == nil {
if conn, unixErr = net.DialUnix("unix", nil, unixSock); unixErr == nil {
return conn, nil
}
return unixSock, nil
}
tcpSock, tcpErr := d.tcpSocket()
if tcpErr == nil {
if conn, tcpErr = net.DialTCP("tcp", nil, tcpSock); tcpErr == nil {
return conn, nil
}
return tcpSock, nil
}
return nil, trace.NewAggregate(unixErr, tcpErr)
}
// Dial opens an XServer connection to the display
func (d *Display) Dial() (net.Conn, error) {
netAddr, err := d.getNetAddr()
if err != nil {
return nil, trace.Wrap(err)
}
conn, err := net.Dial(netAddr.Network(), netAddr.String())
return conn, trace.Wrap(err)
}
// Listen opens an XServer listener. It will attempt to listen on the display
// address for both tcp and unix and return an aggregate error, unless one
// results in an addr in use error.
func (d *Display) Listen() (XServerListener, error) {
unixSock, unixErr := d.unixSocket()
if unixErr == nil {
var l *net.UnixListener
if l, unixErr = net.ListenUnix("unix", unixSock); unixErr == nil {
return &xserverUnixListener{l}, nil
} else if errors.Is(unixErr, syscall.EADDRINUSE) {
return nil, trace.Wrap(unixErr)
}
func (d *Display) Listen() (net.Listener, error) {
netAddr, err := d.getNetAddr()
if err != nil {
return nil, trace.Wrap(err)
}
tcpSock, tcpErr := d.tcpSocket()
if tcpErr == nil {
var l *net.TCPListener
if l, tcpErr = net.ListenTCP("tcp", tcpSock); tcpErr == nil {
return &xserverTCPListener{l}, nil
} else if errors.Is(tcpErr, syscall.EADDRINUSE) {
return nil, trace.Wrap(tcpErr)
}
}
return nil, trace.NewAggregate(unixErr, tcpErr)
listener, err := net.Listen(netAddr.Network(), netAddr.String())
return listener, trace.Wrap(err)
}
// xserverUnixSocket returns the display's associated unix socket.
@@ -148,6 +124,20 @@ func (d *Display) unixSocket() (*net.UnixAddr, error) {
return nil, trace.BadParameter("display is not a unix socket")
}
// ParseDisplay parses the given display value and returns the host,
// display number, and screen number, or a parsing error. display must be
// in one of the following formats - hostname:d[.s], unix:d[.s], :d[.s], ::d[.s].
func ParseDisplayFromUnixSocket(socket string) (Display, error) {
if filepath.Dir(socket) != x11SockDir() {
return Display{}, trace.BadParameter("parsing x11 sockets outside of the standard /tmp/.X11-unix path is not supported")
}
// The file name should look like X[d].[s] and we want :[d].[s]
fileName := filepath.Base(socket)
displayName := strings.Replace(fileName, "X", ":", 1)
return ParseDisplay(displayName)
}
// xserverTCPSocket returns the display's associated tcp socket.
// e.g. "hostname:<6000+display_number>"
func (d *Display) tcpSocket() (*net.TCPAddr, error) {
@@ -183,7 +173,6 @@ func GetXDisplay() (Display, error) {
// display number, and screen number, or a parsing error. display must be
// in one of the following formats - hostname:d[.s], unix:d[.s], :d[.s], ::d[.s].
func ParseDisplay(displayString string) (Display, error) {
if displayString == "" {
return Display{}, trace.BadParameter("display cannot be an empty string")
}
+8 -39
View File
@@ -22,49 +22,18 @@ package x11
import (
"context"
"io"
"sync"
"github.com/gravitational/trace"
"golang.org/x/crypto/ssh"
"github.com/gravitational/teleport/lib/sshutils"
)
// forwardIO forwards io between two XServer connections until
// one of the connections is closed. If the ctx is closed early,
// the function will return, but forwarding will continue until
// the XServer connnections are closed.
func Forward(ctx context.Context, client, server XServerConn) error {
errs := make(chan error)
var wg sync.WaitGroup
const (
// ForwardRequest is a request to initiate X11 forwarding.
ForwardRequest = "x11-req"
wg.Add(1)
go func() {
defer wg.Done()
_, err := io.Copy(client, server)
errs <- trace.Wrap(err)
// Send other goroutine an EOF
err = client.CloseWrite()
errs <- trace.Wrap(err)
}()
wg.Add(1)
go func() {
defer wg.Done()
_, err := io.Copy(server, client)
errs <- trace.Wrap(err)
// Send other goroutine an EOF
err = server.CloseWrite()
errs <- trace.Wrap(err)
}()
go func() {
wg.Wait()
close(errs)
}()
return trace.NewAggregateFromChannel(errs, ctx)
}
// ChannelRequest is the type of an X11 forwarding channel.
ChannelRequest = "x11"
)
// ForwardRequestPayload according to http://www.ietf.org/rfc/rfc4254.txt
type ForwardRequestPayload struct {
@@ -93,7 +62,7 @@ func RequestForwarding(sess *ssh.Session, xauthEntry *XAuthEntry) error {
ScreenNumber: uint32(xauthEntry.Display.ScreenNumber),
}
ok, err := sess.SendRequest(sshutils.X11ForwardRequest, true, ssh.Marshal(payload))
ok, err := sess.SendRequest(ForwardRequest, true, ssh.Marshal(payload))
if err != nil {
return trace.Wrap(err)
} else if !ok {
@@ -116,7 +85,7 @@ type x11ChannelHandler func(ctx context.Context, nch ssh.NewChannel)
// ServeChannelRequests opens an X11 channel handler and starts a
// goroutine to serve any channels received with the handler provided.
func ServeChannelRequests(ctx context.Context, clt *ssh.Client, handler x11ChannelHandler) error {
channels := clt.HandleChannelOpen(sshutils.X11ChannelRequest)
channels := clt.HandleChannelOpen(ChannelRequest)
if channels == nil {
return trace.Wrap(trace.AlreadyExists("X11 forwarding channel already open"))
}
-90
View File
@@ -1,90 +0,0 @@
/*
* 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 x11
import (
"context"
"net"
"testing"
"github.com/stretchr/testify/require"
)
func TestForward(t *testing.T) {
ctx := context.Background()
// Open a dual sided connection on a new tcp listener
openConn := func(t *testing.T) (clientConn net.Conn, serverConn net.Conn) {
l, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, l.Close()) })
serverErrC := make(chan error)
serverConnC := make(chan net.Conn)
go func() {
serverConn, err := l.Accept()
if err != nil {
serverErrC <- err
close(serverConnC)
}
serverConnC <- serverConn
close(serverErrC)
}()
clientConn, err = net.Dial("tcp", l.Addr().String())
require.NoError(t, err)
t.Cleanup(func() { clientConn.Close() })
serverConn = <-serverConnC
require.NoError(t, <-serverErrC)
t.Cleanup(func() { serverConn.Close() })
return clientConn, serverConn
}
cConn1, sConn1 := openConn(t)
cConn2, sConn2 := openConn(t)
// Start forwarding between connections so that we get
// this flow: cConn1 -> sConn1 -> cConn2 -> sConn2.
serverConnToForward, ok := sConn1.(*net.TCPConn)
require.True(t, ok)
clientConnToForward, ok := cConn2.(*net.TCPConn)
require.True(t, ok)
forwardErrC := make(chan error, 1)
go func() {
forwardErrC <- Forward(ctx, serverConnToForward, clientConnToForward)
}()
// Write a msg to client connection 1, which should propagate to server connection 2.
message := "msg"
_, err := cConn1.Write([]byte(message))
require.NoError(t, err)
buf := make([]byte, len(message))
_, err = sConn2.Read(buf)
require.NoError(t, err)
require.Equal(t, message, string(buf))
// Fowarding should stop once the other sides of the forwarded connections are closed.
require.NoError(t, cConn1.Close())
require.NoError(t, sConn2.Close())
require.NoError(t, <-forwardErrC)
}
@@ -20,7 +20,6 @@ package x11
import (
"errors"
"io"
"math"
"net"
"os"
@@ -29,49 +28,23 @@ import (
"github.com/gravitational/trace"
)
// XServerConn is a connection to an XServer through a direct or forwarded connection.
// It can either be a tcp conn, unix conn, or an X11 channel.
type XServerConn interface {
io.ReadWriteCloser
// must implement CloseWrite to prevent read loop from halting pre-maturely
CloseWrite() error
}
// XServerListener is a proxy XServer listener used to forward XServer requests.
// to an actual XServer. The underlying listener may be a unix or tcp listener.
type XServerListener interface {
// Accept waits for and returns the next connection to the listener.
Accept() (XServerConn, error)
// Close closes the listener.
// Any blocked Accept operations will be unblocked and return errors.
Close() error
// Addr returns the listener's network address.
Addr() net.Addr
}
type xserverUnixListener struct {
*net.UnixListener
}
func (l *xserverUnixListener) Accept() (XServerConn, error) {
return l.AcceptUnix()
}
type xserverTCPListener struct {
*net.TCPListener
}
func (l *xserverTCPListener) Accept() (XServerConn, error) {
return l.AcceptTCP()
}
const (
// DefaultDisplayOffset is the default display offset when
// searching for an open XServer unix socket.
DefaultDisplayOffset = 10
// DefaultMaxDisplays is the default maximum number of displays
// supported when searching for an open XServer unix socket.
DefaultMaxDisplays = 1000
// MaxDisplay is the theoretical max display value which
// X Clients and serverwill be able to parse into a unix socket.
MaxDisplayNumber = math.MaxInt32
)
// OpenNewXServerListener opens an XServerListener for the first available Display.
// displayOffset will determine what display number to start from when searching for
// an open display unix socket, and maxDisplays in optional limit for the number of
// display sockets which can be opened at once.
func OpenNewXServerListener(displayOffset int, maxDisplay int, screen uint32) (XServerListener, Display, error) {
func OpenNewXServerListener(displayOffset int, maxDisplay int, screen uint32) (net.Listener, Display, error) {
if displayOffset > maxDisplay {
return nil, Display{}, trace.BadParameter("displayOffset (%d) cannot be larger than maxDisplay (%d)", displayOffset, maxDisplay)
} else if maxDisplay > MaxDisplayNumber {
@@ -84,7 +57,7 @@ func OpenNewXServerListener(displayOffset int, maxDisplay int, screen uint32) (X
}
for displayNumber := displayOffset; displayNumber <= maxDisplay; displayNumber++ {
display := Display{DisplayNumber: displayNumber}
display := Display{DisplayNumber: displayNumber, ScreenNumber: int(screen)}
if l, err := display.Listen(); err == nil {
return l, display, nil
} else if !errors.Is(err, syscall.EADDRINUSE) {
+8 -66
View File
@@ -25,14 +25,12 @@ import (
"os"
"os/user"
"path/filepath"
"strconv"
"time"
"github.com/gravitational/trace"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh/agent"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/utils"
)
@@ -68,10 +66,6 @@ type AgentServer struct {
listener net.Listener
Path string
Dir string
// testPermissions is a test provided function used to test
// the permissions of the agent server during potentially
// vulnerable moments in permission changes.
testPermissions func()
}
// NewServer returns new instance of agent server
@@ -79,14 +73,19 @@ func NewServer(getter Getter) *AgentServer {
return &AgentServer{getAgent: getter}
}
func (a *AgentServer) SetListener(l net.Listener) {
a.listener = l
a.Path = l.Addr().String()
a.Dir = filepath.Dir(a.Path)
}
// ListenUnixSocket starts listening on a new unix socket.
func (a *AgentServer) ListenUnixSocket(sockDir, sockName string, user *user.User) error {
func (a *AgentServer) ListenUnixSocket(sockDir, sockName string, _ *user.User) error {
// Create a temp directory to hold the agent socket.
sockDir, err := os.MkdirTemp(os.TempDir(), sockDir+"-")
if err != nil {
return trace.Wrap(err)
}
a.Dir = sockDir
sockPath := filepath.Join(sockDir, sockName)
l, err := net.Listen("unix", sockPath)
@@ -95,67 +94,10 @@ func (a *AgentServer) ListenUnixSocket(sockDir, sockName string, user *user.User
return trace.Wrap(err)
}
a.listener = l
a.Path = sockPath
if err := a.updatePermissions(user); err != nil {
a.Close()
return trace.Wrap(err)
}
a.SetListener(l)
return nil
}
// Update the agent server permissions to give the user sole ownership
// of the socket path and prevent other users from accessing or seeing it.
func (a *AgentServer) updatePermissions(user *user.User) error {
// Tests may provide a testPermissions function to test potentially
// vulnerable moments during permission updating.
testPermissions := func() {
if a.testPermissions != nil {
a.testPermissions()
}
}
testPermissions()
uid, err := strconv.Atoi(user.Uid)
if err != nil {
return trace.Wrap(err)
}
gid, err := strconv.Atoi(user.Gid)
if err != nil {
return trace.Wrap(err)
}
testPermissions()
if err := os.Chmod(a.Path, teleport.FileMaskOwnerOnly); err != nil {
return trace.ConvertSystemError(err)
}
testPermissions()
if err := os.Lchown(a.Path, uid, gid); err != nil {
return trace.ConvertSystemError(err)
}
testPermissions()
// To prevent a privilege escalation attack, this must occur
// after the socket permissions are updated.
if err := os.Lchown(a.Dir, uid, gid); err != nil {
return trace.ConvertSystemError(err)
}
return nil
}
// SetTestPermissions can be used by tests to test agent socket permissions.
func (a *AgentServer) SetTestPermissions(testPermissions func()) {
a.testPermissions = testPermissions
}
// Serve starts serving on the listener, assumes that Listen was called before
func (a *AgentServer) Serve() error {
if a.listener == nil {
+3 -6
View File
@@ -105,8 +105,7 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con
scpc := app.Command("scp", "Server-side implementation of SCP.").Hidden()
sftp := app.Command(teleport.SFTPSubCommand, "Server-side implementation of SFTP.").Hidden()
exec := app.Command(teleport.ExecSubCommand, "Used internally by Teleport to re-exec itself to run a command.").Hidden()
forward := app.Command(teleport.LocalForwardSubCommand, "Used internally by Teleport to re-exec itself to port forward.").Hidden()
remoteForward := app.Command(teleport.RemoteForwardSubCommand, "Used internally by Teleport to re-exec itself to remote port forward.").Hidden()
networking := app.Command(teleport.NetworkingSubCommand, "Used internally by Teleport to re-exec itself to handle networking requests.").Hidden()
checkHomeDir := app.Command(teleport.CheckHomeDirSubCommand, "Used internally by Teleport to re-exec itself to check access to a directory.").Hidden()
park := app.Command(teleport.ParkSubCommand, "Used internally by Teleport to re-exec itself to do nothing.").Hidden()
app.HelpFlag.Short('h')
@@ -645,10 +644,8 @@ Examples:
err = onConfigDump(dumpFlags)
case exec.FullCommand():
srv.RunAndExit(teleport.ExecSubCommand)
case forward.FullCommand():
srv.RunAndExit(teleport.LocalForwardSubCommand)
case remoteForward.FullCommand():
srv.RunAndExit(teleport.RemoteForwardSubCommand)
case networking.FullCommand():
srv.RunAndExit(teleport.NetworkingSubCommand)
case checkHomeDir.FullCommand():
srv.RunAndExit(teleport.CheckHomeDirSubCommand)
case park.FullCommand():
+1 -4
View File
@@ -943,9 +943,6 @@ func TestList(t *testing.T) {
func createAgent(t *testing.T) (agent.ExtendedAgent, string) {
t.Helper()
currentUser, err := user.Current()
require.NoError(t, err)
sockDir := "test"
sockName := "agent.sock"
@@ -957,7 +954,7 @@ func createAgent(t *testing.T) (agent.ExtendedAgent, string) {
})
// Start the SSH agent.
err = teleAgent.ListenUnixSocket(sockDir, sockName, currentUser)
err := teleAgent.ListenUnixSocket(sockDir, sockName, nil)
require.NoError(t, err)
go teleAgent.Serve()
t.Cleanup(func() {