Skip to content

Latest commit

 

History

History
792 lines (558 loc) · 42.1 KB

0067-desktop-access-file-system-sharing.md

File metadata and controls

792 lines (558 loc) · 42.1 KB
authors state
Isaiah Becker-Mayer (isaiah@goteleport.com)
implemented (v10.2.0)

Required Approvers

  • Engineering: @zmb3 && (@probakowski || @LKozlowski)

Desktop Access File System Sharing

Introduction

At a high level, implementing drive (folder) redirection for Teleport is a matter of taking RDP's File System Virtual Channel Extension protocol (described in [MS-RDPEFS]), and converting it to the TDP protocol (described in this document).

                        Teleport Desktop Protocol                         RDP
               ------------------------------------------        ---------------------
               |                                        |        |                   |
+----------------------+     +------------------+  +------------------+     +------------------+
|                      |     |                  |  |                  |     |                  |
|                      |     |                  |  |    Teleport      |     |                  |
|  User's Web Browser  ------|  Teleport Proxy -----  Windows Desktop ------|  Windows Desktop |
|                      |     |                  |  |     Service      |     |                  |
+----------------------+     +------------------+  +------------------+     +------------------+

RDP is an old protocol, and is designed in some ways to be deeply compatible with Windows operating system, which means that it contains a lot of details that aren't necessarily relevant or available to our client, which is limited to sort of file information is available to us through the browser. While this reality creates its own set of implementation difficulties for us, it also creates an opportunity for us to simplify drive redirection (aka "directory sharing") in the TDP protocol.

Being such a longstanding, large, and complex protocol, it can sometimes be difficult to tell from it's documentation precisely how RDP is actually supposed to work. Adding to that difficulty is the File System Virtual Channel Extension designer's decision to leave some aspects of client and server dynamics unspecified:

"This protocol forwards server requests from the server-based application and returns replies from the client-file system. There are no specific rules implied by this protocol as to how and when a particular message is sent from the server and what the client is to reply." ([MS-RDPEFS] 3.1.5.1)

In cases of protocol ambiguity, the open source project FreeRDP is an invaluable resource as a canonical implementation, which will be used for reference. This document attempts to be exhaustive for core drive sharing functionality; for any cases that aren't accounted for in this document, it should be assumed that we follow FreeRDP's conventions and algorithms.

RDP Background

Device I/O Requests

After the initial drive negotiation, RDP directs the remote machine's use of the drive by sending Device I/O Requests. These requests are only sent from the server (the Windows Desktop) to the client (the Rust client running on Teleport's Windows Desktop Service). Each Device I/O Request contains the fields described below, and may contain an additional data structure determined by it's MajorFunction/MinorFunction.

DeviceId

The DeviceId uniquely identifies a shared directory. The initial implementation will only support sharing a single directory, so we will use the same ID in all responses.

In order to support sharing multiple directories simultaneously, Teleport would need to maintain a mapping between the selected ID and the local directory.

Since it's trivial to add, we will include a corresponding field directory_id in TDP that corresponds with this field, which will set us up add a multiple-directory-sharing feature more easily in the future.

FileId

A FileId is generated by the client upon receipt of a Device I/O Request where MajorFunction = IRP_MJ_CREATE and sent back to the server, which then uses it to denote that file in subsequent Device I/O Requests. The FileId is valid until the client receives a Device I/O Request with MajorFunction = IRP_MJ_CLOSE, at which point it becomes invalid and can be recycled.

The semantics of these MajorFunctions and how this system works isn't obvious at first glance, and will be clarified in the Typical Operation section below. TDP won't make use of FileId, electing to use a file or directory's (unix-like) relative path in each request. The relative path combined with directory_id gives a unique "id" to any shared file-like object for any number of root-level shared directories. Therefore, the responsibility for mapping between FileId and a file path will be the responsibility of the Teleport Desktop Service.

CompletionId

CompletionIds are generated by the server and sent with each Device I/O Request. All Device I/O Requests demand a Device I/O Response in response, and the server matches Device I/O Responses to Device I/O Requests by the CompletionId field.

This design allows operations to take place asynchronously/concurrently, and we will mimic it in our TDP translation.

MajorFunction & MinorFunction

Device I/O Requests are classified by their MajorFunction field, which can contain the following values/semantics:

Value Meaning
IRP_MJ_CREATE Create request
IRP_MJ_CLOSE Close request
IRP_MJ_READ Read request
IRP_MJ_WRITE Write request
IRP_MJ_DEVICE_CONTROL Device control request
IRP_MJ_QUERY_VOLUME_INFORMATION Query volume information request
IRP_MJ_SET_VOLUME_INFORMATION Set volume information request
IRP_MJ_QUERY_INFORMATION Query information request
IRP_MJ_SET_INFORMATION Set information request
IRP_MJ_DIRECTORY_CONTROL Directory control request
IRP_MJ_LOCK_CONTROL File lock control request

Device I/O Requests are always the headers of more detailed request messages. For example, a Device I/O Request with its MajorFunction set to IRP_MJ_CREATE denotes the beginning of a Device Create Request, which contains further fields that specify the details of the request.

The MinorFunction further specifies IRP_MJ_DIRECTORY_CONTROL requests.

We will need to handle each of these request types, though in some cases that simply means sending back an empty message to RDP. The details of how each of these will be handled is documented in the RDP --> TDP Translation section below.

Typical Operation

As mentioned previously, the semantics of the MajorFunctions isn't necessarily obvious at first glance. For example, one might assume that an IRP_MJ_CREATE means "create a new file or directory", however it turns out this is only the case given a specific CreateDisposition in the attendant Device Create Request.

In fact, IRP_MJ_CREATE is sent at the beginning of any operation the server wants to execute. It most commonly just tells the client "create a reference to a file/directory and give it a FileId". The client does so and sends that FileId back to the server in response, and that FileId is then used in subsequent Device I/O Requests that act on that file/directory, such as reading or writing. Once the operation is complete, the server sends a IRP_MJ_CLOSE, which typically means "remove the reference you created previously".

As an example, here is what happens when the client first announces a new folder for redirection:

               RDP Server                            RDP Client
               (Windows Machine)                     (Windows Desktop Service)
                       |                                    |
                       | Client Device List Announce Request|
                       |<-----------------------------------+
                       |                                    |
                       |Server Device Announce Response     |
                       +----------------------------------->|
                       |                                    |
                       |Device Create Request               |Generates a FileId corresponding to
                       |(IRP_MJ_CREATE)                     |the requested Path (which is "", meaning
                       +----------------------------------->|the top level directory), internally creating some
                       |                                    |sort of file handle.
                       |Device Create Response              |
                       |<-----------------------------------+and responds with the FileId.
                       |                                    |
                       |Drive Query Information Request     |
Asks for metadata about|(IRP_MJ_QUERY_INFORMATION)          |
the tld.               +----------------------------------->|
                       |                                    |
                       |Drive Query Information Response    |
                       |<-----------------------------------+Replies with the metadata
                       |                                    |
                       |Device Close Request                |
                       |(IRP_MJ_CLOSE)                      |Operation complete, the previously
                       +----------------------------------->|generated FileId can now be recycled
                       |                                    |and internal file handle can be deleted.
                       |                                    |
                       |                                    |
                       |                                    |

A similar process would then take place for other operations, for example a read of a file in the shared directory named example.txt would look like

  1. server sends IRP_MJ_CREATE with Path: "example.txt"
  2. client responds with a FileId
  3. server sends IRP_MJ_READ for FileId for bytes 0 - 1024
  4. client executes the read and responds with the data
  5. server sends IRP_MJ_CLOSE

TDP File Shared Directory Extension

Our RDP client lives on the Windows Desktop Service, while the directory we're sharing is exposed to us via the user's browser. This means that in order to get information to and from the shared directory, we must extend the TDP protocol (also see the diagram in the Introduction section for reference).

Each * Request (such as Shared Directory Info Request, Shared Directory Create Request, etc.) and * Response (Shared Directory Info Response, Shared Directory Create Response, etc.) TDP message contains a completion_id field, with * Requests being responsible for generating the completion_ids, and * Responses being responsible for including the correct completion_id to signify which * Request the response is intended for.

Note on terminology

From here on out, the term "client" will refer to the browser-based TDP client, and the term "server" will refer to the Windows Desktop Service based TDP server, unless otherwise specified.

11 - Shared Directory Announce

| message type (11) | directory_id uint32 | name_length uint32 | name []byte |

This message announces a new directory to be shared over TDP. directory_id must be a unique identifier. Attempting to share multiple different directories using the same directory_id is undefined behavior.

name_length is the length in bytes of the name. The maximum allowed length is equivalent to Windows' MAX_PATH (260 characters).

name is the name of the directory (without any path prefix).

12 - Shared Directory Acknowledge

| message type (12) | err_code uint32 | directory_id uint32 |

Acknowledges a Shared Directory Announce was received.

err_code is an error code. 0 ("nil") means the Shared Directory Announce was successfully processed, 1 ("operation failed") means processing failed.

directory_id is the directory_id of the top level directory being shared, as specified in the Announce Shared Directory message this message is acknowledging.

13 - Shared Directory Info Request

| message type (13) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte |

This message is sent from the server to the client to request information about a file. The server will be expecting a Shared Directory Info Response.

completion_id is generated by the server and must be returned by the client in its corresponding Shared Directory Info Response or Shared Directory Error response.

directory_id is the directory_id of the top level directory being shared, as specified in a previous Announce Shared Directory message.

path_length is the length in bytes of the path.

path is the unix-style relative path (from the root-level directory specified by directory_id) to the file or directory, excepting special path characters ".", "..". Absolute paths, or those containing path elements with either of the special characters, will result in an error. Info can be requested for the root-level directory by setting path_length: 0 and path: "".

14 - Shared Directory Info Response

| message type (14) | completion_id uint32 | err_code uint32 | file_system_object fso |

This message is sent by the client to the server in response to a Shared Directory Info Request.

completion_id must match the completion_id of the Shared Directory Info Request that this message is responding to.

err_code is an error code. If a file or directory at path does not exist, this should be set to 2 ("resource does not exist").

file_system_object is the file system object.

15 - Shared Directory Create Request

| message type (15) | completion_id uint32 | directory_id uint32 | file_type uint32 | path_length uint32 | path []byte |

This message is sent by the server to the client to request the creation of a new file or directory.

completion_id is generated by the server and must be returned by the client in its corresponding Shared Directory Create Response or Shared Directory Error response.

directory_id is the directory_id of the top level directory being shared, as specified in a previous Announce Shared Directory message.

file_type matches the specification in Shared Directory Info Response.

path_length is the length in bytes of the path.

path is the unix-style relative path (from the root-level directory specified by directory_id) to the file or directory, excepting special path characters ".", "..". Absolute paths, or those containing path elements with either of the special characters, will result in an error.

16 - Shared Directory Create Response

| message type (16) | completion_id uint32 | err_code uint32 | file_system_object fso |

This message is sent by the client to the server to acknowledge a Shared Directory Create Request was successfully executed. A Shared Directory Create Request that fails should respond with an "operation failed" Shared Directory Error.

completion_id must match the completion_id of the Shared Directory Create Request that this message is responding to.

err_code is an error code. If a filesystem object at path already exists, this should be set to 3 ("resource already exists").

file_system_object is the file system object that was created.

17 - Shared Directory Delete Request

| message type (17) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte |

This message is sent by the server to the client to request the deletion of a file or directory at path.

completion_id is generated by the server and must be returned by the client in its corresponding Shared Directory Delete Response or Shared Directory Error response.

directory_id is the directory_id of the top level directory being shared, as specified in a previous Shared Directory Announce message.

path_length is the length in bytes of the path.

path is the unix-style relative path (from the root-level directory specified by directory_id) to the file or directory, excepting special path characters ".", "..". Absolute paths, or those containing path elements with either of the special characters, will result in an error.

18 - Shared Directory Delete Response

| message type (18) | completion_id uint32 | err_code uint32 |

This message is sent by the client to the server to acknowledge a Shared Directory Delete Request was successfully executed. A Shared Directory Create Request that fails should respond with an appropriate Shared Directory Error.

completion_id must match the completion_id of the Shared Directory Delete Request that this message is responding to.

err_code is an error code. If the delete fails, this should be set to 1 ("operation failed"). If the file or directory does not exist, this should be set to 2 ("resource does not exist").

19 - Shared Directory Read Request

| message type (19) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte | offset uint64 | length uint32 |

This message is sent by the server to the client to request a maximum of length bytes be read from the file at path, starting at byte offset offset.

completion_id is generated by the server and must be returned by the client in its corresponding Shared Directory Read Response or Shared Directory Error response.

directory_id is the directory_id of the top level directory being shared, as specified in a previous Announce Shared Directory message.

path_length is the length in bytes of the path.

path is the unix-style relative path (from the root-level directory specified by directory_id) to the file or directory, excepting special path characters ".", "..". Absolute paths, or those containing path elements with either of the special characters, will result in an error.

offset specifies the file offset where the read operation is performed.

length specifies the maximum number of bytes to be read from the device.

20 - Shared Directory Read Response

| message type (20) | completion_id uint32 | err_code uint32 | read_data_length uint32 | read_data []byte |

This message is sent by the client to the server in response to a Shared Directory Read Request.

completion_id must match the completion_id of the Shared Directory Read Request that this message is responding to.

err_code is an error code. If the file does not exist or the path is to a directory, this field should be set to 2 ("resource does not exist"). For any other error, this field should be set to 1 ("operation failed").

read_data_length specifies the number of bytes in the read_data field.

read_data are the raw bytes that were read.

21 - Shared Directory Write Request

| message type (21) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte | offset uint64 | write_data_length uint32 | write_data []byte |

This message is sent by the server to the client to request write_data be written to the file specified by path.

completion_id is generated by the server and must be returned by the client in its corresponding Shared Directory Write Response or Shared Directory Error response.

directory_id is the directory_id of the top level directory being shared, as specified in a previous Announce Shared Directory message.

path_length is the length in bytes of the path.

path is the unix-style relative path (from the root-level directory specified by directory_id) to the file or directory, excepting special path characters ".", "..". Absolute paths, or those containing path elements with either of the special characters, will result in an error.

offset specifies the file offset where the write operation should start from.

write_data_length is the length of write_data

write_data is the raw data to be written.

22 - Shared Directory Write Response

| message type (22) | completion_id uint32 | err_code uint32 | bytes_written uint32 |

This message is sent by the client to the server in response to a Shared Directory Write Request.

completion_id must match the completion_id of the Shared Directory Write Request that this message is responding to.

err_code is an error code. If the file does not exist or the path is to a directory, this field should be set to 2 ("resource does not exist"). For any other error, this field should be set to 1 ("operation failed").

bytes_written specifies the number of bytes that were written.

23 - Shared Directory Move Request

| message type (23) | completion_id uint32 | directory_id uint32 | original_path_length uint32 | original_path []byte | new_path_length uint32 | new_path []byte |

This message is sent by the server to the client to request a file or directory be moved (or renamed). It should be expected to work like the linux mv utility (with no flags).

completion_id is generated by the server and must be returned by the client in its corresponding Shared Directory Move Response or Shared Directory Error response.

directory_id is the directory_id of the top level directory being shared, as specified in a previous Announce Shared Directory message.

original_path_length is the length of original_path

original_path is the existing path of the file or directory being moved.

new_path_length is the length of new_path

new_path is the new path for the file or directory being moved.

24 - Shared Directory Move Response

| message type (24) | completion_id uint32 | err_code uint32 |

This message is sent by the client to the server in response to a Shared Directory Move Request to alert the server of a successful move operation.

completion_id must match the completion_id of the Shared Directory Move Request that this message is responding to.

err_code is an error code. If the original_path in the Shared Directory Move Request does not exist, this field should be set to 2 ("resource does not exist"). For any other error, this field should be set to 1 ("operation failed").

25 - Shared Directory List Request

| message type (25) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte |

This message is sent by the server to the client to request the contents of a directory.

completion_id is generated by the server and must be returned by the client in its corresponding Shared Directory List Response or Shared Directory Error response.

directory_id is the directory_id of the top level directory being shared, as specified in a previous Announce Shared Directory message.

path_length is the length in bytes of the path.

path is the unix-style relative path (from the root-level directory specified by directory_id) to the file or directory, excepting special path characters ".", "..". Absolute paths, or those containing path elements with either of the special characters, will result in an error. Info can be requested for the root-level directory by setting path_length: 0 and path: "".

26 - Shared Directory List Response

| message type (26) | completion_id uint32 | err_code uint32 | fso_list_length uint32 | fso_list fso[] |

This message is sent by the client to the server in response to a Shared Directory Info Request.

completion_id must match the completion_id of the Shared Directory List Request that this message is responding to.

err_code is an error code. If the original_path in the Shared Directory Move Request does not exist or the path is to a file, this field should be set to 2 ("resource does not exist"). For any other error, this field should be set to 1 ("operation failed").

fso_list_length is the number of entries in the fso_list.

fso_list is a list of fso objects representing all the files and directories in the directory specified by the path variable in the originating Shared Directory List Request.

33 - Shared Directory Truncate Request

| message type (33) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte | end_of_file uint32 |

This message is sent by the server to the client to request a file be truncated to end_of_file bytes.

completion_id is generated by the server and must be returned by the client in its corresponding Shared Directory Truncate Response or Shared Directory Error

directory_id is the directory_id of the top level directory being shared, as specified in a previous Announce Shared Directory message.

path_length is the length in bytes of the path.

path is the unix-style relative path (from the root-level directory specified by directory_id) to the file which should be truncated.

end_of_file is the new size of the file in bytes.

34 - Shared Directory Truncate Response

| message type (34) | completion_id uint32 | err_code uint32 |

This message is sent by the client to the server in response to a Shared Directory Truncate Request to alert the server of a successful truncate operation.

completion_id must match the completion_id of the Shared Directory Truncate Request that this message is responding to.

err_code is an error code. If the file does not exist or the path is to a directory, this field should be set to 2 ("resource does not exist"). For any other error, this field should be set to 1 ("operation failed").

File System Object (fso)
| last_modified uint64 | size uint64 | file_type uint32 | is_empty bool | path_length uint32 | path byte[] |

The design of this message is constrained by what information is made available to us by the browser and which File Information Classes are potentially requested by RDP.

For files, last_modified is the last modified time of the file as specified by the mtime, in milliseconds since the UNIX epoch. For directories, last_modified should also be set to the directory's mtime when such information is available. If such information is unavailable for a directory, such as in a browser environment, this value should be assigned the UNIX epoch itself (0).

For files, size is the size of the file in bytes. For directories, size is not the total size of the contents of the directory, but rather the size the directory itself takes up on disk. If such information is unavailable for a directory, such as in a browser environment, this can be set to the contemporary de facto Unix default of 4096 bytes (see mke2fsc.onf).

file_types currently represents only the simple file/directory distinction. Later it may be modified to support more types such as those corresponding to the types available in RDP's File Attributes fields:

  1. file
  2. directory

is_empty says whether or not a directory is empty. For objects where file_type = 0 (files), this field should be ignored.

path_length is the length in bytes of the path.

path is the unix-style relative path (from the root-level directory specified by directory_id) to the file or directory, excluding special path characters ".", "..". Absolute paths, or those containing path elements with either of the special characters, are considered an error. A root-level shared directory is specified by setting path_length: 0 and path: "".

err_code's

err_code is a uint32 sized field specifying an error

  1. nil (no error, operation succeeded)
  2. operation failed
  3. resource does not exist
  4. resource already exists

For now, all errors will be unspecified, and translated into NTSTATUS::STATUS_UNSUCCESSFUL over RDP. Later, we can add more specific error messages for more specific RDP error handling, as they do in FreeRDP.

bool

bool is a uint8 specifying True or False:

  1. False
  2. True

RDP --> TDP Translation for IRP_MJ_READ

It's beyond the scope of this document to detail the RDP --> TDP translation of every MajorFunction, however I here is one example of the task at hand for IRP_MJ_READ, for clarity's sake.

RDP request: Device Read Request

RDP response: Device Read Response

FreeRDP: entry point

Our Windows Desktop Service receives the following Device Read Request

DeviceReadRequest {
  DeviceIoRequest: DeviceIoRequest {
    Header: omitted,
    DeviceId: 2,
    FileId: 3,
    CompletionId: 4,
    MajorFunction: IRP_MJ_READ,
    MinorFunction: 0x00000000,
  },
  Length: 1024,
  Offset: 2048,
}

First we consult our internal cache of pseudo "file handles" for an entry where FileId = 3, which would have been created while fielding a previous successful IRP_MJ_CREATE. From that we'll grab the path corresponding to this file (lets say "example/file.txt"). No we have all the information required to create a TDP Shared Directory Read Request:

SharedDirectoryReadRequest: {
  message_type: 19,
  completion_id: 4,
  directory_id: 2,
  path_length: 16,
  path: "example/file.txt",
  offset: 2048,
  length: 1024,
}

Presuming the request succeeds, we expect back a response like:

SharedDirectoryReadResponse {
  completion_id: 4,
  err: 0,
  read_data_length: 1024,
  read_data: [...],
}

Which we can then package into an RDP Device Read Response

DeviceReadResponse {
  DeviceIoReply: DeviceIoResponse {
    Header: omitted,
    DeviceId: 2,
    CompletionId: 4,
    IoStatus: NTSTATUS.STATUS_SUCCESS,
  }
  Length: 1024,
  ReadData: [...],
}

This is clearly the "happy path" for this process, error mode handling that been omitted here. Note that not all MajorFunctions have such straightforward translations, and some will require multiple TDP messages to complete their translation.

Read-Only Mode

Depending on customer demand, we may wish to add a "read-only" mode that allows users to share a local directory with the remote Windows machine for file transfer while disallowing any write operations for enhanced security. RDP doesn't offer such a feature natively, thus it would need to be implemented at the TDP layer only.

To do so, we would extend Shared Directory Announce to include a read-only boolean field:

| message type (11) | directory_id uint32 | read_only bool | name_length uint32 | name []byte |

When set to true, any mutating TDP Requests such as Shared Directory Write Request and Shared Directory Delete Request will become invalid. An initial thought might be to have such messages always result in a corresponding Response with a non-null err_code, however this would make the read-only guarantee dependent on a friendly client implementation, which we can not rely on. Instead, this read-only setting will be enforced by the Windows Desktop Service itself: whenever we receive an RDP message that in non-read-only mode would be translated into a mutating TDP Request, we will send back an RDP error.

For example, if we receive an IRP_MJ_WRITE, at the point where would ordinarily send a Shared Directory Write Request to our client (the browser), we will instead send the Windows machine an RDP Device I/O Response with IoStatus set to STATUS_ACCESS_DENIED. Realistically this situation might happen if the user is sharing a directory in read-only mode, opens a file in Notepad, changes it and then tries to save it.

UX

The UX for this feature is discussed in RFD 0058 and may be expounded upon further in a future RFD.

Security

Security will be discussed in a future RFD.

Audit Events

Audit events would ideally be focused exclusively on directory sharing events that are germaine to system security such as:

  • A file was transferred from the remote Windows machine onto the local machine (into the shared directory)
  • A file was transferred from the local machine (from the shared directory) onto the remote Windows machine
  • etc

However due to our limited visibility into the Windows side of the equation it is not possible for us to determine such events with precision. For example, if a user transfers a file from the Windows box into the shared directory, we see that as a Shared Directory Create sequence followed by a Shared Directory Write sequence. But then that same sequence could just have been a user creating a brand new file within the shared directory and then writing to it (which is not of particular note from a security perspective). Given this limitation, we will instead simply log all events that indicate security-relevant information, which is to say events indicating a data transfer between the local and remote machines, which is to say Shared Directory Read ("desktop.directory.read") and Shared Directory Write ("desktop.directory.write") events.

Shared Directory Announce/Acknowledge ("desktop.directory.share") is also included in the audit event log to make the sequence of events more easily comprehensible.

TDP Shared Directory Messages Logged

The following list includes the type of TDP messages that will be logged and their proposed corresponding event names.

  • Shared Directory Announce/Acknowledge
  • Shared Directory ReadRequest/ReadResponse
  • Shared Directory WriteRequest/WriteResponse

TDP Shared Directory Messages Skipped

  • Shared Directory Info
  • Shared Directory List
  • Shared Directory Create
  • Shared Directory Delete
  • Shared Directory Move

For Shared Directory Read and Shared Directory Write, which contain raw file data, the length (number of bytes) of the data transfer will be logged rather than the data itself. This by default prevents the audit log from blowing up in size if large files are shared, and from becoming a source of potential data theft. That said, given that there is apparently already consumer demand for complete file data logging in the audit log, it's worth considering making this configurable (out of scope for this RFD).

Events

DesktopSharedDirectoryStart

Emitted when a successful Shared Directory Acknowledge is received. Due to technical limitations, we will for now just be logging this event on a successful directory sharing initialization. We can attempt a more complex implementation that takes into account failed initialization attempts, pending user demand.

// DesktopSharedDirectoryStart is emitted when Teleport
// attempt to begin sharing a new directory to a remote desktop.
message DesktopSharedDirectoryStart {
  // Metadata is common event metadata.
  Metadata Metadata = 1 [
    (gogoproto.nullable) = false,
    (gogoproto.embed) = true,
    (gogoproto.jsontag) = ""
  ];
  // User is common user event metadata.
  UserMetadata User = 2 [
    (gogoproto.nullable) = false,
    (gogoproto.embed) = true,
    (gogoproto.jsontag) = ""
  ];
  // Session is common event session metadata.
  SessionMetadata Session = 3 [
    (gogoproto.nullable) = false,
    (gogoproto.embed) = true,
    (gogoproto.jsontag) = ""
  ];
  // Connection holds information about the connection.
  ConnectionMetadata Connection = 4 [
    (gogoproto.nullable) = false,
    (gogoproto.embed) = true,
    (gogoproto.jsontag) = ""
  ];
  // DesktopAddr is the address of the desktop being accessed.
  string DesktopAddr = 5 [(gogoproto.jsontag) = "desktop_addr"];
  // DirectoryName is the name of the directory being shared.
  string DirectoryName = 6 [(gogoproto.jsontag) = "directory_name"];
  // DirectoryID is the ID of the directory being shared (unique to the Windows Desktop Session).
  uint32 DirectoryID = 7 [(gogoproto.jsontag) = "directory_id"];
}

Note: the inclusion of DirectoryID is looking forward to if/when we allow for multiple directories to be shared at once, at which point DirectoryName will no longer necessarily be a unique identifier.

DesktopSharedDirectoryRead

// DesktopSharedDirectoryRead is emitted when Teleport
// attempts to read from a file in a shared directory at
// the behest of the remote desktop.
message DesktopSharedDirectoryRead {
  // Metadata, UserMetadata, SessionMetadata, ConnectionMetadata omitted

  // DesktopAddr is the address of the desktop being accessed.
  string DesktopAddr = 5 [(gogoproto.jsontag) = "desktop_addr"];
  // DirectoryName is the name of the directory being shared.
  string DirectoryName = 6 [(gogoproto.jsontag) = "directory_name"];
  // DirectoryID is the ID of the directory being shared (unique to the Windows Desktop Session).
  uint32 DirectoryID = 7 [(gogoproto.jsontag) = "directory_id"];
  // Path is the path within the shared directory where the file is located.
  string Path = 8 [(gogoproto.jsontag) = "file_path"];
  // Length is the number of bytes read.
  uint32 Length = 9 [(gogoproto.jsontag) = "length"];
  // Offset is the offset the bytes were read from.
  uint32 Offset = 10 [(gogoproto.jsontag) = "offset"];
}

DesktopSharedDirectoryWrite

// DesktopSharedDirectoryWrite is emitted when Teleport
// attempts to write to a file in a shared directory at
// the behest of the remote desktop.
message DesktopSharedDirectoryWrite {
  // Metadata, UserMetadata, SessionMetadata, ConnectionMetadata omitted

  // DesktopAddr is the address of the desktop being accessed.
  string DesktopAddr = 5 [(gogoproto.jsontag) = "desktop_addr"];
  // DirectoryName is the name of the directory being shared.
  string DirectoryName = 6 [(gogoproto.jsontag) = "directory_name"];
  // DirectoryID is the ID of the directory being shared (unique to the Windows Desktop Session).
  uint32 DirectoryID = 7 [(gogoproto.jsontag) = "directory_id"];
  // Path is the path within the shared directory where the file is located.
  string Path = 8 [(gogoproto.jsontag) = "file_path"];
  // Length is the number of bytes written.
  uint32 Length = 9 [(gogoproto.jsontag) = "length"];
  // Offset is the offset the bytes were written to.
  uint32 Offset = 10 [(gogoproto.jsontag) = "offset"];
}

Event Names and Codes

Shared Directory Announce/Acknowledge Shared Directory ReadRequest/ReadResponse Shared Directory WriteRequest/WriteResponse
Name "desktop.directory.share" "desktop.directory.read" "desktop.directory.write"
Success "TDP04I" "TDP05I" "TDP06I"
Failure "TDP04W" "TDP05W" "TDP06W"

The failure codes in the table above will usually correspond to a Shared Directory Acknowledge/ReadResponse/WriteResponse being returned with a non-nil error code.

Because of the asynchronous nature of these request/response pairs, audit information will need to be cached, and there is therefore a chance of some information being lost in the audit events due to a programmer error causing the cache to get out of sync. Such a possibility is accounted for in the code, with the missing information being recorded as "unknown", and the event code being recorded as the failure code. Because such a situation should in practice never happen, it won't be accounted for in the Audit Events UI, and all failure codes will show up in the UI as some variation of "Start/Read/Write failed".

Debouncing for read/write

The read/write events that are sent to us are fundamentally determined by the program on the Windows machine that's reading or writing to a shared file. In the case of copying files in/out of the shared directory, that program is typically File Explorer, which empirically does read/writes at a maximum of 1MB (exactly 2^20 bytes) at a time. For files larger than 1MB, this manifests as several read/writes in a row showing up at time in such a case.

In order to avoid the logs getting spammed with multiple messages corresponding to one "operation" (such as a multi MB read of a large file), we can create an algorithm which upon receipt of a read or write event, sets a short timer (say 1s), and if it receives another non-overlapping read/write for the same file before the timer runs out, stops the timer and amalgamates the events together, repeating until the timer runs out at which point a single event is written into the log (credit to @zmb3 for the algo design).

This feature will be considered beyond the scope of the initial implementation, which will naively write all reads/writes as individual events.