Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
86774cf
wip
zmb3 Apr 10, 2024
f4b581a
Merge remote-tracking branch 'origin/master' into zmb3/desktop-latenc…
probakowski Mar 3, 2025
74450eb
add ping message and latency from desktop side
probakowski Mar 5, 2025
fa07c79
version
probakowski Mar 14, 2025
236cbec
Fix backward compatibility
probakowski Mar 15, 2025
cd45be8
godocs
probakowski Mar 17, 2025
c0f2dfb
formatting
probakowski Mar 17, 2025
78f17d3
Merge remote-tracking branch 'origin/master' into probakowski/desktop…
probakowski Mar 17, 2025
4906593
fix imports
probakowski Mar 17, 2025
8b7ae4b
formatting
probakowski Mar 17, 2025
c9b6a01
formatting
probakowski Mar 17, 2025
d8b7aad
log and gci
probakowski Mar 17, 2025
5b08181
lint
probakowski Mar 17, 2025
88db008
Apply tooltip on the icon directly, instead of on the Menu component
gzdunek Mar 19, 2025
853b91c
Use consistent spacing between top bar elements
gzdunek Mar 19, 2025
25a7ef0
Merge remote-tracking branch 'origin/master' into probakowski/desktop…
probakowski Mar 24, 2025
71e14d0
updates from origin
probakowski Mar 25, 2025
2b8b31a
Merge remote-tracking branch 'origin/master' into probakowski/desktop…
probakowski Mar 25, 2025
eabea28
Merge remote-tracking branch 'origin/master' into probakowski/desktop…
probakowski Mar 26, 2025
58bd4f3
e
probakowski Mar 26, 2025
bbc1073
e
probakowski Mar 26, 2025
0a1e727
lint
probakowski Mar 26, 2025
a3f5c80
Merge remote-tracking branch 'origin/master' into probakowski/desktop…
probakowski Apr 8, 2025
aea59ed
rework UI after merge
probakowski Apr 8, 2025
8b80181
Rename fields in backend
probakowski Apr 8, 2025
02d2ea6
Merge branch 'master' into probakowski/desktop-latency-detector
probakowski Apr 8, 2025
8491486
prettier
probakowski Apr 8, 2025
aee5f35
Merge remote-tracking branch 'origin/master' into probakowski/desktop…
probakowski Apr 9, 2025
989af81
Update web/packages/shared/components/DesktopSession/TopBar.tsx
probakowski Apr 10, 2025
f6f8df1
review comments
probakowski Apr 10, 2025
5383d6b
Merge branch 'master' into probakowski/desktop-latency-detector
probakowski Apr 10, 2025
46c85fe
fix ui
probakowski Apr 10, 2025
512175d
add env var to disable windows desktop "ping"
probakowski Apr 15, 2025
b2ba75f
review comment
probakowski Apr 16, 2025
31d5ee0
review comment
probakowski Apr 16, 2025
de874d7
Update lib/web/desktop.go
probakowski Apr 18, 2025
4641324
Refactor latency monitoring
probakowski Apr 22, 2025
5942a2b
Merge remote-tracking branch 'origin/master' into probakowski/desktop…
probakowski Apr 22, 2025
4eb3c0a
fix spelling
probakowski Apr 22, 2025
ae258df
Merge branch 'master' into probakowski/desktop-latency-detector
probakowski Apr 22, 2025
bf49275
remove monitorSessionLatency
probakowski Apr 22, 2025
3a30f62
gci
probakowski Apr 22, 2025
da25c99
review comment
probakowski Apr 23, 2025
b5b4c66
review comment
probakowski Apr 23, 2025
cc98086
Update RFD and version
probakowski May 6, 2025
f21aec9
Merge branch 'master' into probakowski/desktop-latency-detector
probakowski May 6, 2025
b7edc60
fix gaps
probakowski May 6, 2025
b5e0f90
Merge branch 'master' into probakowski/desktop-latency-detector
probakowski May 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions lib/srv/desktop/rdp/rdpclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ import (
"encoding/binary"
"fmt"
"log/slog"
"net"
"os"
"runtime/cgo"
"strconv"
"sync"
"sync/atomic"
"time"
Expand Down Expand Up @@ -414,6 +416,9 @@ func (c *Client) startInputStreaming(stopCh chan struct{}) error {
c.cfg.Logger.InfoContext(context.Background(), "TDP input streaming starting")
defer c.cfg.Logger.InfoContext(context.Background(), "TDP input streaming finished")

// we will disable ping only if the env var is truthy
disableDesktopPing, _ := strconv.ParseBool(os.Getenv("TELEPORT_DISABLE_DESKTOP_LATENCY_DETECTOR_PING"))

var withheldResize *tdp.ClientScreenSpec
for {
select {
Expand All @@ -432,6 +437,22 @@ func (c *Client) startInputStreaming(stopCh chan struct{}) error {
c.cfg.Logger.WarnContext(context.Background(), "Failed reading TDP input message", "error", err)
return err
}
if m, ok := msg.(tdp.Ping); ok {
Comment thread
probakowski marked this conversation as resolved.
// Upon receiving a ping message, we make a connection
// to the host and send the same message back to the proxy.
// The proxy will then compute the round trip time.
if !disableDesktopPing {
conn, err := net.Dial("tcp", c.cfg.Addr)
if err == nil {
conn.Close()
}
}
Comment on lines +444 to +449
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will just block the whole stream of messages until the dial is completed on every ping, won't it?

if err := c.cfg.Conn.WriteMessage(m); err != nil {
c.cfg.Logger.WarnContext(context.Background(), "Failed writing TDP ping message", "error", err)
return err
}
continue
}

if atomic.LoadUint32(&c.readyForInput) == 0 {
switch m := msg.(type) {
Expand Down
43 changes: 43 additions & 0 deletions lib/srv/desktop/tdp/proto.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"image/png"
"io"

"github.com/google/uuid"
"github.com/gravitational/trace"

authproto "github.com/gravitational/teleport/api/client/proto"
Expand Down Expand Up @@ -82,6 +83,8 @@ const (
TypeSyncKeys = MessageType(32)
TypeSharedDirectoryTruncateRequest = MessageType(33)
TypeSharedDirectoryTruncateResponse = MessageType(34)
TypeLatencyStats = MessageType(35)
TypePing = MessageType(36)
)

// Message is a Go representation of a desktop protocol message.
Expand Down Expand Up @@ -182,6 +185,8 @@ func decodeMessage(firstByte byte, in byteReader) (Message, error) {
return decodeSharedDirectoryTruncateRequest(in)
case TypeSharedDirectoryTruncateResponse:
return decodeSharedDirectoryTruncateResponse(in)
case TypePing:
return decodePing(in)
default:
return nil, trace.BadParameter("unsupported desktop protocol message type %d", firstByte)
}
Expand Down Expand Up @@ -1631,6 +1636,44 @@ func decodeSharedDirectoryTruncateResponse(in io.Reader) (SharedDirectoryTruncat
return res, err
}

// LatencyStats is used to report the latency of the connection(s) to the client.
type LatencyStats struct {
Comment thread
zmb3 marked this conversation as resolved.
ClientLatency uint32
ServerLatency uint32
}

func (l LatencyStats) Encode() ([]byte, error) {
buf := new(bytes.Buffer)
buf.WriteByte(byte(TypeLatencyStats))
writeUint32(buf, l.ClientLatency)
writeUint32(buf, l.ServerLatency)
return buf.Bytes(), nil
}

// Ping is used to measure the latency of the connection(s) between proxy and desktop (includes
// latency between proxy and Windows Desktop Service and between WDS and desktop).
type Ping struct {

// UUID is used to correlate message send by proxy and received from the Windows Desktop Service
UUID uuid.UUID
}

func (p Ping) Encode() ([]byte, error) {
buf := new(bytes.Buffer)
buf.WriteByte(byte(TypePing))
buf.Write(p.UUID[:])
return buf.Bytes(), nil
}

func decodePing(in io.Reader) (Ping, error) {
var ping Ping
_, err := io.ReadFull(in, ping.UUID[:])
if err != nil {
return ping, trace.Wrap(err)
}
return ping, nil
}

// encodeString encodes strings for TDP. Strings are encoded as UTF-8 with
// a 32-bit length prefix (in bytes):
// https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md#field-types
Expand Down
150 changes: 111 additions & 39 deletions lib/web/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ import (
"net/http"
"sync"

"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/julienschmidt/httprouter"
"golang.org/x/sync/errgroup"

"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
Expand All @@ -47,6 +50,7 @@ import (
"github.com/gravitational/teleport/lib/reversetunnelclient"
"github.com/gravitational/teleport/lib/srv/desktop/tdp"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/diagnostics/latency"
logutils "github.com/gravitational/teleport/lib/utils/log"
)

Expand Down Expand Up @@ -194,7 +198,7 @@ func (h *Handler) createDesktopConnection(
clientSrcAddr: clientSrcAddr,
clientDstAddr: clientDstAddr,
}
serviceConn, _, err := c.connectToWindowsService(ctx, clusterName, validServiceIDs)
serviceConn, version, err := c.connectToWindowsService(ctx, clusterName, validServiceIDs)
if err != nil {
return sendTDPError(trace.Wrap(err, "cannot connect to Windows Desktop Service"))
}
Expand Down Expand Up @@ -233,7 +237,7 @@ func (h *Handler) createDesktopConnection(
// proxyWebsocketConn hangs here until connection is closed
handleProxyWebsocketConnErr(
ctx,
proxyWebsocketConn(ws, serviceConnTLS),
proxyWebsocketConn(ctx, ws, serviceConnTLS, log, version),
log,
)

Expand Down Expand Up @@ -535,19 +539,108 @@ func (c *connector) tryConnect(ctx context.Context, clusterName, desktopServiceI
return conn, ver, trace.Wrap(err)
}

// desktopPinger measures latency between proxy and the desktop by sending tdp.Ping messages
// Windows Desktop Service and measuring the time it takes to receive message with the same UUID back.
type desktopPinger struct {
wds net.Conn
ch <-chan tdp.Ping
}

func (d desktopPinger) Ping(ctx context.Context) error {
ping := tdp.Ping{
UUID: uuid.New(),
}
buf, err := ping.Encode()
if err != nil {
return trace.Wrap(err)
}
_, err = d.wds.Write(buf)
if err != nil {
return trace.Wrap(err)
}
for {
select {
case pong := <-d.ch:
if pong.UUID == ping.UUID {
return nil
}
case <-ctx.Done():
return trace.Wrap(ctx.Err())
}
}
}

// proxyWebsocketConn does a bidrectional copy between the websocket
// connection to the browser (ws) and the mTLS connection to Windows
// Desktop Serivce (wds)
func proxyWebsocketConn(ws *websocket.Conn, wds net.Conn) error {
func proxyWebsocketConn(ctx context.Context, ws *websocket.Conn, wds net.Conn, log *slog.Logger, version string) error {
ctx, cancel := context.WithCancel(ctx)
var closeOnce sync.Once
close := func() {
cancel()
ws.Close()
wds.Close()
}

errs := make(chan error, 2)
tdpMessagesToSend := make(chan tdp.Message)

latencySupported, err := utils.MinVerWithoutPreRelease(version, "17.5.0")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of relying on indirect info such as the version string (in a heartbeat, even, not even from the actual connection in use) can we start thinking about exchanging capabilities as part of the TDP "handshake", so to speak?

if err != nil {
return trace.Wrap(err)
}

pings := make(chan tdp.Ping)

if latencySupported {
pinger := desktopPinger{
wds: wds,
ch: pings,
}

go monitorLatency(ctx, clockwork.NewRealClock(), ws, pinger,
latency.ReporterFunc(func(ctx context.Context, stats latency.Statistics) error {
tdpMessagesToSend <- tdp.LatencyStats{
ClientLatency: uint32(stats.Client),
ServerLatency: uint32(stats.Server),
}
return nil
}),
)

}

var errs errgroup.Group

// run a goroutine to pick TDP messages up from a channel and send
// them to the browser
errs.Go(func() error {
for msg := range tdpMessagesToSend {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this not just block forever and leak the goroutine at the end of a connection? Nothing closes tdpMessagesToSend.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does, could you please check 0551d44 where I tried to fix all 3 problems you mention

if ping, ok := msg.(tdp.Ping); ok {
pings <- ping
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also block if the context for monitorLatency is canceled and nothing reads from the pings channel anymore.

continue
}
if ls, ok := msg.(tdp.LatencyStats); ok {
log.DebugContext(ctx, "sending latency stats", "client", ls.ClientLatency, "server", ls.ServerLatency)
}
encoded, err := msg.Encode()
if err != nil {
return err
}

err = ws.WriteMessage(websocket.BinaryMessage, encoded)
if utils.IsOKNetworkError(err) {
return err
}
if err != nil {
return err
}
}
return nil
})

go func() {
// run a second goroutine to read TDP messages from the Windows
// agent and write them to our send channel
errs.Go(func() error {
defer closeOnce.Do(close)

// we avoid using io.Copy here, as we want to make sure
Expand All @@ -563,8 +656,7 @@ func proxyWebsocketConn(ws *websocket.Conn, wds net.Conn) error {
for {
msg, err := tc.ReadMessage()
if utils.IsOKNetworkError(err) {
errs <- nil
return
return err
} else if err != nil {
isFatal := tdp.IsFatalErr(err)
severity := tdp.SeverityError
Expand All @@ -585,58 +677,38 @@ func proxyWebsocketConn(ws *websocket.Conn, wds net.Conn) error {
if sendErr != nil {
err = sendErr
}
errs <- err
return
}
encoded, err := msg.Encode()
if err != nil {
errs <- err
return
}
err = ws.WriteMessage(websocket.BinaryMessage, encoded)
if utils.IsOKNetworkError(err) {
errs <- nil
return
}
if err != nil {
errs <- err
return
return err
}
tdpMessagesToSend <- msg
}
}()
})

go func() {
// run a goroutine to read TDP messages coming from the browser
// and pass them on to the Windows agent
errs.Go(func() error {
defer closeOnce.Do(close)

var buf bytes.Buffer
for {
_, reader, err := ws.NextReader()
switch {
case utils.IsOKNetworkError(err):
errs <- nil
return
return err
case err != nil:
errs <- err
return
return err
}
buf.Reset()
if _, err := io.Copy(&buf, reader); err != nil {
errs <- err
return
return err
}

if _, err := wds.Write(buf.Bytes()); err != nil {
errs <- trace.Wrap(err, "sending TDP message to desktop agent")
return
return trace.Wrap(err, "sending TDP message to desktop agent")
}
}
}()
})

var retErrs []error
for i := 0; i < 2; i++ {
retErrs = append(retErrs, <-errs)
}
return trace.NewAggregate(retErrs...)
return trace.Wrap(errs.Wait())
}

// handleProxyWebsocketConnErr handles the error returned by proxyWebsocketConn by
Expand Down
Loading
Loading