Skip to content

Commit

Permalink
RDP client implementation (gravitational#7824)
Browse files Browse the repository at this point in the history
* RDP client implementation

This is a minimal RDP client on top of the rdp-rs Rust crate.
The crate is wrapped with an FFI Rust adapter, statically compiled and
called from Go via CGO.

This PR also has toy web client to test the RDP code, and just enough of
the desktop wire protocol implementation to make it work.

I excluded the RDP client from the build via tags for now to avoid
bloating the teleport binaries.

Many more things missing that will come in later PRs, to keep this one
reviewable.

* flag tweaks and updated readme for macos

* Switch to C-style strings between Go and Rust

* Use regular top-level C function instead ofthe cgo jump function

* Fix bitmap buffer memory leak

* Consistent naming for CGO types

* Pass rust object reference to Go

* Clean up FFI error string management

* Extract a thin C wrapper for read_rdp_output

* Use the log crate in rust

* Small rust cleanups

* Fix shellcheck nit in run.sh

* Fix RDP client memory release

* Allow Rust code to compile on any unix

* Force Alpha channel to 100% always

For some reason, on Windows 10 with bitmap decompression the Alpha
always ends up as 0. This makes everything transparent.

* Implement screen size and credential negotiation

* desktop protocol: remove password prompt

It was decided that supporting passwords natively is a bad product
decision. We will only support certificate-based authn and only in
ActiveDirectory environments.

Until we implement certificate support, passwords can be injected via an
environment variable for testing. It will be removed before the beta
release.

* Address review feedback

Co-authored-by: Isaiah Becker-Mayer <isaiah@goteleport.com>
  • Loading branch information
Andrew Lytvynov and Isaiah Becker-Mayer authored Aug 23, 2021
1 parent dc6e728 commit b9e9b53
Show file tree
Hide file tree
Showing 14 changed files with 2,569 additions and 19 deletions.
273 changes: 273 additions & 0 deletions lib/srv/desktop/deskproto/proto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
/*
Copyright 2021 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package deskproto implements the desktop protocol encoder/decoder.
// See https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md
//
// TODO(awly): complete the implementation of all messages, even if we don't
// use them yet.
package deskproto

import (
"bytes"
"encoding/binary"
"image"
"image/png"
"io"

"github.com/gravitational/trace"
)

// MessageType identifies the type of the message.
type MessageType byte

// For descriptions of each message type see:
// https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md#message-types
const (
TypeClientScreenSpec = MessageType(1)
TypePNGFrame = MessageType(2)
TypeMouseMove = MessageType(3)
TypeMouseButton = MessageType(4)
TypeKeyboardButton = MessageType(5)
TypeClipboardData = MessageType(6)
TypeClientUsername = MessageType(7)
)

// Message is a Go representation of a desktop protocol message.
type Message interface {
Encode() ([]byte, error)
}

// Decode decodes the wire representation of a message.
func Decode(buf []byte) (Message, error) {
if len(buf) == 0 {
return nil, trace.BadParameter("input desktop protocol message is empty")
}
switch MessageType(buf[0]) {
case TypeClientScreenSpec:
return decodeClientScreenSpec(buf)
case TypePNGFrame:
return decodePNGFrame(buf)
case TypeMouseMove:
return decodeMouseMove(buf)
case TypeMouseButton:
return decodeMouseButton(buf)
case TypeKeyboardButton:
return decodeKeyboardButton(buf)
case TypeClientUsername:
return decodeClientUsername(buf)
default:
return nil, trace.BadParameter("unsupported desktop protocol message type %d", buf[0])
}
}

// PNGFrame is the PNG frame message
// https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md#2---png-frame
type PNGFrame struct {
Img image.Image
}

func (f PNGFrame) Encode() ([]byte, error) {
type header struct {
Type byte
Left, Top uint32
Right, Bottom uint32
}

buf := new(bytes.Buffer)
if err := binary.Write(buf, binary.BigEndian, header{
Type: byte(TypePNGFrame),
Left: uint32(f.Img.Bounds().Min.X),
Top: uint32(f.Img.Bounds().Min.Y),
Right: uint32(f.Img.Bounds().Max.X),
Bottom: uint32(f.Img.Bounds().Max.Y),
}); err != nil {
return nil, trace.Wrap(err)
}
if err := png.Encode(buf, f.Img); err != nil {
return nil, trace.Wrap(err)
}
return buf.Bytes(), nil
}

func decodePNGFrame(buf []byte) (PNGFrame, error) {
var header struct {
Left, Top uint32
Right, Bottom uint32
}
r := bytes.NewReader(buf[1:])
if err := binary.Read(r, binary.BigEndian, &header); err != nil {
return PNGFrame{}, trace.Wrap(err)
}
img, err := png.Decode(r)
if err != nil {
return PNGFrame{}, trace.Wrap(err)
}
// PNG encoding does not preserve offset image bounds.
// Opportunistically restore them based on the header.
switch img := img.(type) {
case *image.RGBA:
img.Rect = image.Rect(int(header.Left), int(header.Top), int(header.Right), int(header.Bottom))
case *image.NRGBA:
img.Rect = image.Rect(int(header.Left), int(header.Top), int(header.Right), int(header.Bottom))
}
return PNGFrame{Img: img}, nil
}

// MouseMove is the mouse movement message.
// https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md#3---mouse-move
type MouseMove struct {
X, Y uint32
}

func (m MouseMove) Encode() ([]byte, error) {
buf := new(bytes.Buffer)
buf.WriteByte(byte(TypeMouseMove))
if err := binary.Write(buf, binary.BigEndian, m); err != nil {
return nil, trace.Wrap(err)
}
return buf.Bytes(), nil
}

func decodeMouseMove(buf []byte) (MouseMove, error) {
var m MouseMove
err := binary.Read(bytes.NewReader(buf[1:]), binary.BigEndian, &m)
return m, trace.Wrap(err)
}

// MouseButtonType identifies a specific button on the mouse.
type MouseButtonType byte

const (
LeftMouseButton = MouseButtonType(0)
MiddleMouseButton = MouseButtonType(1)
RightMouseButton = MouseButtonType(2)
)

// ButtonState is the press state of a keyboard or mouse button.
type ButtonState byte

const (
ButtonNotPressed = ButtonState(0)
ButtonPressed = ButtonState(1)
)

// MouseButton is the mouse button press message.
// https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md#4---mouse-button
type MouseButton struct {
Button MouseButtonType
State ButtonState
}

func (m MouseButton) Encode() ([]byte, error) {
return []byte{byte(TypeMouseButton), byte(m.Button), byte(m.State)}, nil
}

func decodeMouseButton(buf []byte) (MouseButton, error) {
var m MouseButton
err := binary.Read(bytes.NewReader(buf[1:]), binary.BigEndian, &m)
return m, trace.Wrap(err)
}

// KeyboardButton is the keyboard button press message.
// https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md#4---keyboard-input
type KeyboardButton struct {
KeyCode uint32
State ButtonState
}

func (k KeyboardButton) Encode() ([]byte, error) {
buf := new(bytes.Buffer)
buf.WriteByte(byte(TypeKeyboardButton))
if err := binary.Write(buf, binary.BigEndian, k); err != nil {
return nil, trace.Wrap(err)
}
return buf.Bytes(), nil
}

func decodeKeyboardButton(buf []byte) (KeyboardButton, error) {
var k KeyboardButton
err := binary.Read(bytes.NewReader(buf[1:]), binary.BigEndian, &k)
return k, trace.Wrap(err)
}

// ClientScreenSpec is the client screen specification.
// https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md#1---client-screen-spec
type ClientScreenSpec struct {
Width uint32
Height uint32
}

func (s ClientScreenSpec) Encode() ([]byte, error) {
buf := new(bytes.Buffer)
buf.WriteByte(byte(TypeClientScreenSpec))
if err := binary.Write(buf, binary.BigEndian, s); err != nil {
return nil, trace.Wrap(err)
}
return buf.Bytes(), nil
}

func decodeClientScreenSpec(buf []byte) (ClientScreenSpec, error) {
var s ClientScreenSpec
err := binary.Read(bytes.NewReader(buf[1:]), binary.BigEndian, &s)
return s, trace.Wrap(err)
}

// ClientUsername is the client username.
// https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md#7---client-username
type ClientUsername struct {
Username string
}

func (r ClientUsername) Encode() ([]byte, error) {
buf := new(bytes.Buffer)
buf.WriteByte(byte(TypeClientUsername))
if err := encodeString(buf, r.Username); err != nil {
return nil, trace.Wrap(err)
}
return buf.Bytes(), nil
}

func decodeClientUsername(buf []byte) (ClientUsername, error) {
r := bytes.NewReader(buf[1:])
username, err := decodeString(r)
if err != nil {
return ClientUsername{}, trace.Wrap(err)
}
return ClientUsername{Username: username}, nil
}

func encodeString(w io.Writer, s string) error {
if err := binary.Write(w, binary.BigEndian, uint32(len(s))); err != nil {
return trace.Wrap(err)
}
if _, err := w.Write([]byte(s)); err != nil {
return trace.Wrap(err)
}
return nil
}

func decodeString(r io.Reader) (string, error) {
var length uint32
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
return "", trace.Wrap(err)
}
s := make([]byte, int(length))
if _, err := io.ReadFull(r, s); err != nil {
return "", trace.Wrap(err)
}
return string(s), nil
}
60 changes: 60 additions & 0 deletions lib/srv/desktop/deskproto/proto_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
Copyright 2021 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package deskproto

import (
"image"
"image/color"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
)

func TestEncodeDecode(t *testing.T) {
for _, m := range []Message{
MouseMove{X: 1, Y: 2},
MouseButton{Button: MiddleMouseButton, State: ButtonPressed},
KeyboardButton{KeyCode: 1, State: ButtonPressed},
func() Message {
img := image.NewNRGBA(image.Rect(5, 5, 10, 10))
for x := img.Rect.Min.X; x < img.Rect.Max.X; x++ {
for y := img.Rect.Min.Y; y < img.Rect.Max.Y; y++ {
img.Set(x, y, color.NRGBA{1, 2, 3, 4})
}
}
return PNGFrame{Img: img}
}(),
ClientScreenSpec{Width: 123, Height: 456},
ClientUsername{Username: "admin"},
} {

buf, err := m.Encode()
require.NoError(t, err)

out, err := Decode(buf)
require.NoError(t, err)

require.Empty(t, cmp.Diff(m, out))
}
}

func TestBadDecode(t *testing.T) {
// 254 is an unknown message type.
_, err := Decode([]byte{254})
require.Error(t, err)
}
2 changes: 2 additions & 0 deletions lib/srv/desktop/rdp/rdpclient/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
main
Loading

0 comments on commit b9e9b53

Please sign in to comment.