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);
}
}

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