Secure Shell Relay Server Protocols

Some servers are accessible only behind a different secure server, and over a web protocol like XHR or WebSockets. To support those use cases, Secure Shell supports connecting to machines through relay servers.

Here we document the relay server protocol in case people want to create their own relay servers for their own networks. Google does not offer any public relay servers for you to access. See the FAQ for details on open source relay server implementations.

This does not document the Secure Shell options it uses itself to connect through relays. See the Options document for those details.

Corp Relay

This uses the id corp-relay@google.com (e.g. when using --proxy-mode=).

The main implementation for this can be found in nassh_google_relay.js with supporting stream logic in nassh_stream_google_relay.js.

Protocol Overview

 +--------+      Phase I         +---------------+
 |  USER  |  ---(1 /cookie)--->  | COOKIE SERVER |
 +--------+                      +---------------+
 | | |    |
 | | |    |
 | | |  Phase II
 | | |    |
 | | |    `---(3 /proxy)--->  +--------------+
 | | |                        |              |
Phase III                     |              |  ----->  +------------+
 | | |                        | RELAY SERVER |          | SSH SERVER |
 | | `------(4 /read)------>  |              |  <-----  +------------+
 | `--------(5 /write)----->  |              |
 `----------(6 /connect)--->  +--------------+

You can think of this as a three phase process:

  • (I) Client talks to a cookie server in order to authenticate the user, and for the cookie server to tell the user which relay server to use.
  • (II) Client talks to the relay server to establish a new session to the ssh server (and allow the relay server to authenticate the user).
  • (III) Client talks to the ssh server through the relay server using the established session.

Across those phases are some major steps:

  1. Connect to the cookie server using /cookie.

    This system may initiate a single-sign-on (SSO) flow if necessary, as well as verify client certificates.

  2. When the cookie server is done with authenticating the user, it responds with details for which relay server to talk to in order to connect to the actual ssh server. Its response format is described below in /cookie.

    The relay host is expected to respond to requests for /proxy and either /connect (for WebSockets) or /read & /write (for XHR).

  3. Send a request to /proxy which establishes the ssh session with the remote ssh server.

  4. When using XHR, use /read to read binary data from the ssh server via the established relay server session. Every read requires a new connection to be created.

  5. When using XHR, use /write to send binary data to the ssh server via the established relay server session. Every write requires a new connection to be created.

  6. When using WebSockets, use /connect to create a long lived bidirectional socket for reading and writing binary data to the ssh sever via the established relay server session.

/cookie Protocol

Make a GET request to PROTOCOL://COOKIE_HOST:COOKIE_PORT/cookie to find the relay server to talk to. This allows the server to select from pools to help load balance internally.

PROTOCOL may be http or https. Secure Shell defaults to http.

COOKIE_HOST is the user-specified hostname for the cookie server. This system could offer other services too which is why the port is not fixed.

COOKIE_PORT is the port on the server to connect to which defaults to 8022.

The query string settings:

  • ext=EXT_ID (required): The Chrome extension id that the cookie server should redirect to when it is finished.
  • path=PATH (required): The path under the extension that the cookie server should redirect to when it is finished.
  • version=VERSION: The version of the cookie protocol. Only 2 is supported. If version= is omitted, then the older version 1 is used (note: version=1 is not supported!).
  • method=METHOD: The redirection method in the response (only used by version=2. See the method field section below for more details.

It responds with an eventual redirect to chrome://EXT_ID/PATH. The server might trigger intermediate redirects for its own purposes. The exact redirect method might be HTTP 302, or an HTML meta refresh, or a JavaScript redirect using a <script> tag. The client only cares about the eventual redirect to chrome://EXT_ID/PATH.

The response format is determined by the version and method settings.

Version 1 will set the URI fragment (the part after the #) to USER@RELAY_HOST:RELAY_PORT where each component is URI encoded as needed. The RELAY_PORT part is optional. The USER field is the username used to authenticate with the cookie server. Secure Shell itself ignores this field though, so you can too.

Version 2 will send a JSON object; the response depends on the METHOD specified earlier (see the method field for more details). That object will have the fields:

  • endpoint (string): The RELAY_HOST:RELAY_PORT (where RELAY_PORT is optional).
  • error (string): In case the cookie server rejects the request (e.g. bad credentials), a human readable string can be placed here for display.

If RELAY_PORT is not specified, the default will be based on the protocol used later when connecting to RELAY_HOST. Typically http and ws use port 80 while https and wss use port 443. It is up to the client to select the RELAY_PROTOCOL themselves.

method field

The method field is used with version 2 only. It may be direct or js-redirect.

The direct method will return a JSON response with an XSSI header. The client is responsible for parsing that response and operating on it (e.g. redirecting itself to chrome://EXT_ID/PATH).

The js-redirect method will generate a HTML document with a <script> tag to redirect to the chrome://EXT_ID/PATH path. The JSON response is base64url encoded in the URI fragment.

Secure Shell only supports js-redirect currently.

/proxy Protocol

This establishes a new session with the actual ssh server. The RELAY_HOST:RELAY_PORT settings were passed back from the /cookie connection made previously.

Make a GET request to PROTOCOL://RELAY_HOST:RELAY_PORT/proxy with the query string:

  • host=HOST: The ssh server to connect to. It may be an IP address or a hostname, and does not need to be resolvable or accessible by the client.
  • port=PORT: The port the ssh server is listening on.

This will return a session id (and optionally more query string fields) as plain text. The client should then paste this directly into the next phase -- either with /connect (for WebSockets) or /read & /write (for XHR).

For example, it might be 4b2fbe8f4eff640b&host=foovpn-1.system.example.com. The session id allows the remote relay server to associate the connection with an existing one, and the additional query string fields allow for things like load distribution. The extra fields are left unspecified to allow the relay server implementation the freedom to pass through whatever they want.

/connect Protocol (WebSocket)

If possible, you should use WebSockets as the protocol is much more efficient.

For WebSockets, you need to only make one connection to /connect to the RELAY_PROTOCOL://RELAY_HOST:RELAY_PORT host to create a bidirectional socket. Use arraybuffer for the binaryType field.

The fields specified in the query string:

  • sid=SESSION_ID: The session id given returned from the /proxy call.
  • ack=READ_ACK: For relays that support support reconnects, this will be the last read ack the client received. New connections start at 0.
  • pos=WRITE_ACK: For relays that support support reconnects, this will be the last write ack the client received. New connections start at 0.
  • try=TRY: For relays that support connection attempt reporting, the number of times we've tried to connect. New connections start at 1.

The SESSION_ID allows the relay server to validate the connection. This is why you should always use wss instead of ws to avoid spoofing.

Reads

Incoming data starts with a 4 byte WRITE_ACK (big endian). If the value is larger than 24-bits, it indicates a connection error, and the connection is shut down. Otherwise, the server specifies how many bytes it has successfully read, and the client updates its write queue accordingly.

The rest of the data is binary data from the ssh server.

Writes

Outgoing data is broken up into 32KiB chunks (which includes 4 byte ACKs).

The first 4 bytes are a 24-bit READ_ACK (big endian) that tells the server how much data the client has successfully read so far. Once more than 24-bits worth of data have been read, the value is wrapped. i.e. The client only cares about the low 24-bits of the count.

The rest of the write is all the data to be sent to the ssh server in binary form.

The client keeps track of how much data it has written so it can check the WRITE_ACK sent from the server during reads.

Latency Reporting

If the optional ack latency reporting extension is enabled, the client keeps track of how long it takes for READ_ACKs and WRITE_ACKs to be synchronized, and then it reports the average inline using the textual form A:integer. Secure Shell maintains a running average over 10 samples, but there is no requirement as to how often the client reports this information.

There is also reply latency tracking in the form R:integer, although Secure Shell doesn't currently report it.

This is entirely optional and is purely for servers that want to keep track of performance of client connections.

/read Protocol (XHR)

Since every read requires setting up a new HTTP connection, and only one read request may be pending at a time, it's highly recommended you use the WebSocket /connect instead.

For XHR connections, you make a GET request to RELAY_PROTOCOL://RELAY_HOST:RELAY_PORT/read to read data from the ssh server.

The query string settings:

  • sid=SESSION_ID: The session id given returned from the /proxy call.
  • rcnt=READ_COUNT: The number of bytes the client has successfully read. New connections start at 0.

The SESSION_ID allows the relay server to validate the connection. This is why you should always use https instead of http to avoid spoofing.

The relay server will send HTTP 200 with base64url encoded data in the response when there is new data available from the ssh server. Data might not be available immediately in which case the relay server might not respond immediately.

It may send HTTP 410 when the ssh server is no longer reachable.

All other HTTP errors are ignored, and a new /read is created for all errors except for HTTP 410.

/write Protocol (XHR)

Since every write requires setting up a new HTTP connection, and only one write request may be pending at a time, and write chunks are small (typically 1KiB), it's highly recommended you use the WebSocket /connect instead.

For XHR connections, you make a GET request to RELAY_PROTOCOL://RELAY_HOST:RELAY_PORT/write to write data to the ssh server.

The fields specified in the query string:

  • sid=SESSION_ID: The session id given returned from the /proxy call.
  • wcnt=WRITE_COUNT: The number of bytes the client has successfully written. New connections start at 0.
  • data=DATA: The data to write to the ssh server. It is base64url encoded. This field is typically limited to 1KiB.

The SESSION_ID allows the relay server to validate the connection. This is why you should always use https instead of http to avoid spoofing.

The relay server will send HTTP 200 once the relay server has passed the data through to the ssh server.

It may send HTTP 410 when the ssh server is no longer reachable.

All other HTTP errors are ignored. Data will continue to be sent via new /write requests until HTTP 410 is returned.

SSH-FE

This uses the id ssh-fe@google.com (e.g. when using --proxy-mode=).

TODO