Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions cs/src/Connections/TunnelRelayConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ public abstract class TunnelRelayConnection : TunnelConnection, IRelayClient, IP

#endregion

/// <summary>
/// Request header that a host sends to the relay to identify its own process.
/// </summary>
/// <remarks>
/// The value is <see cref="MultiModeTunnelHost.HostId" />, which stays the same for the
/// lifetime of the process. It lets the relay recognize a host that is reconnecting to a
/// tunnel it already holds, rather than treating it as a different host taking the tunnel
/// over. Clients do not send this header.
/// </remarks>
public const string HostIdHeaderName = "X-Tunnels-Host-Process-Id";

/// <summary>
/// Maximum retry delay, ms.
/// After the 6th attempt the delay will reach 2^7 * 100ms = 12.8s and stop doubling
Expand Down
10 changes: 10 additions & 0 deletions cs/src/Connections/TunnelRelayStreamFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ public class TunnelRelayStreamFactory : ITunnelRelayStreamFactory
TraceSource trace,
CancellationToken cancellation)
{
var isHostConnection =
Array.IndexOf(subprotocols, TunnelRelayConnection.HostWebSocketSubProtocol) >= 0 ||
Array.IndexOf(subprotocols, TunnelRelayConnection.HostWebSocketSubProtocolV2) >= 0;

void ConfigureWebSocketOptions(ClientWebSocketOptions options)
{
foreach (var subprotocol in subprotocols)
Expand All @@ -36,6 +40,12 @@ void ConfigureWebSocketOptions(ClientWebSocketOptions options)
{
options.SetRequestHeader("Authorization", "tunnel " + accessToken);
}

if (isHostConnection && !string.IsNullOrEmpty(MultiModeTunnelHost.HostId))
{
options.SetRequestHeader(
TunnelRelayConnection.HostIdHeaderName, MultiModeTunnelHost.HostId);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The endpoint and the WebSocket can end up using different host IDs here. TunnelRelayTunnelHost captures MultiModeTunnelHost.HostId in its constructor and publishes that captured value as TunnelEndpoint.HostId, while this factory rereads the publicly settable static on every connection and reconnect. If an application changes the supported HostId setting after constructing a host, the endpoint can advertise ID A while this header sends ID B; existing hosts can then be mistaken for another process, or distinct hosts can be treated as the same process, undermining the arbitration this change adds. The TypeScript implementation has the same split between its captured this.hostId and the static read in DefaultTunnelRelayStreamFactory. Can we pass the host instance’s captured ID into stream creation, or otherwise make the process ID immutable before hosts are constructed?

}
}

var stream = await WebSocketStream.ConnectToWebSocketAsync(
Expand Down
8 changes: 8 additions & 0 deletions rs/src/connections/relay_tunnel_host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ type PortMap = HashMap<u32, mpsc::UnboundedSender<ForwardedPortConnection>>;
// large responses continue making progress.
const CHANNEL_WRITE_CHUNK_SIZE: usize = 32 * 1024;

// Identifies the host process to the relay, so it can tell a host reconnecting
// from a genuinely different host competing for the same tunnel. The value is
// the same `host_id` reported on the TunnelEndpoint and stays constant for the
// lifetime of a RelayTunnelHost. Only host connections send it; clients do not.
const HOST_ID_HEADER_NAME: &str = "X-Tunnels-Host-Process-Id";

/// The RelayTunnelHost can host connections via the tunneling service. After
/// creating it, you will generally want to run `connect()` to create a new
/// a new connection.
Expand Down Expand Up @@ -427,12 +433,14 @@ impl RelayTunnelHost {
.as_deref()
.ok_or(TunnelError::MissingHostEndpoint)?;

let host_id = self.host_id.to_string();
let req = build_websocket_request(
url,
&[
("Sec-WebSocket-Protocol", "tunnel-relay-host"),
("Authorization", &format!("tunnel {}", host_token)),
("User-Agent", self.mgmt.user_agent.to_str().unwrap()),
(HOST_ID_HEADER_NAME, &host_id),
],
)?;

Expand Down
22 changes: 22 additions & 0 deletions ts/src/connections/defaultTunnelRelayStreamFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,23 @@
import { Stream } from '@microsoft/dev-tunnels-ssh';
import { TunnelRelayStreamFactory } from './tunnelRelayStreamFactory';
import { isNode, SshHelpers } from './sshHelpers';
import { MultiModeTunnelHost } from './multiModeTunnelHost';
import { IClientConfig } from 'websocket';

/**
* Request header that a host sends to the relay to identify its own process.
*
* The value is `MultiModeTunnelHost.hostId`, which stays the same for the lifetime of the
* process. It lets the relay recognize a host that is reconnecting to a tunnel it already
* holds, rather than treating it as a different host taking the tunnel over. Clients do not
* send this header.
*/
const hostIdHeaderName = 'X-Tunnels-Host-Process-Id';

// Mirrors the host sub-protocols in tunnelRelayTunnelHost.ts. They are duplicated here rather
// than imported because that module reaches back to this one through the connection session.
const hostSubProtocols = ['tunnel-relay-host', 'tunnel-relay-host-v2-dev'];

/**
* Default factory for creating streams to a tunnel relay.
*/
Expand All @@ -17,17 +32,24 @@ export class DefaultTunnelRelayStreamFactory implements TunnelRelayStreamFactory
clientConfig?: IClientConfig,
): Promise<{ stream: Stream, protocol: string }> {
if (isNode()) {
const isHostConnection = protocols.some((p) => hostSubProtocols.includes(p));
const stream = await SshHelpers.openConnection(
relayUri,
protocols,
{
...(accessToken && { Authorization: `tunnel ${accessToken}` }),
...(isHostConnection &&
MultiModeTunnelHost.hostId && {
[hostIdHeaderName]: MultiModeTunnelHost.hostId,
}),
},
clientConfig,
);
return { stream, protocol: stream.protocol! };
} else {
// Web sockets don't support auth. Authenticate TunnelRelay by sending accessToken as a subprotocol.
// Request headers aren't available here either, so a host running in the browser cannot
// identify its process to the relay.
if (accessToken) {
protocols = [...protocols, accessToken];
}
Expand Down
Loading