mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
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:
co-authored by
Sakshyam Shah
parent
8d12347ec1
commit
d26ca00120
+4
-7
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user