From 1287819ea21cdfe0b2250f1dc0c7e9014b05c683 Mon Sep 17 00:00:00 2001 From: David Obando Date: Tue, 22 Sep 2026 10:24:55 -0700 Subject: [PATCH 1/2] Remove obsolete SSH tunnel protocol Remove the SSH value from the shared tunnel protocol contracts and regenerate the TypeScript, Go, Java, and Rust SDK surfaces. Stop requesting the retired SSH gateway public key when relay hosts update their endpoints. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 57ace76a-7df5-4dc5-b59f-1a4b8b239960 --- cs/src/Connections/TunnelRelayTunnelHost.cs | 15 ++------------- cs/src/Contracts/TunnelPort.cs | 4 ++-- cs/src/Contracts/TunnelProtocol.cs | 5 ----- go/tunnels/tunnel_port.go | 4 ++-- go/tunnels/tunnel_protocol.go | 3 --- .../microsoft/tunnels/contracts/TunnelPort.java | 4 ++-- .../tunnels/contracts/TunnelProtocol.java | 5 ----- rs/src/contracts/tunnel_port.rs | 4 ++-- rs/src/contracts/tunnel_protocol.rs | 3 --- ts/src/connections/tunnelRelayTunnelHost.ts | 13 ++----------- ts/src/contracts/tunnelPort.ts | 4 ++-- ts/src/contracts/tunnelProtocol.ts | 5 ----- 12 files changed, 14 insertions(+), 55 deletions(-) diff --git a/cs/src/Connections/TunnelRelayTunnelHost.cs b/cs/src/Connections/TunnelRelayTunnelHost.cs index 288a7293..0de92535 100644 --- a/cs/src/Connections/TunnelRelayTunnelHost.cs +++ b/cs/src/Connections/TunnelRelayTunnelHost.cs @@ -130,12 +130,10 @@ protected override async Task CreateTunnelConnectorAsync(Cance Requires.Argument(this.accessToken != null, nameof(Tunnel), $"There is no access token for {TunnelAccessScope} scope on the tunnel."); var hostPublicKey = HostPrivateKey.GetPublicKeyBytes(HostPrivateKey.KeyAlgorithmName).ToBase64(); - var tunnelHasSshPort = Tunnel.Ports != null && - Tunnel.Ports.Any((p) => p.Protocol == TunnelProtocol.Ssh); var endpointSignature = $"{Tunnel.TunnelId}.{Tunnel.ClusterId}:" + $"{Tunnel.Name}.{Tunnel.Domain}:" + - $"{tunnelHasSshPort}:{this.hostId}:{hostPublicKey}"; + $"{this.hostId}:{hostPublicKey}"; if (!string.Equals(endpointSignature, EndpointSignature, StringComparison.OrdinalIgnoreCase) || RelayUri == null) @@ -147,19 +145,10 @@ protected override async Task CreateTunnelConnectorAsync(Cance HostPublicKeys = new[] { hostPublicKey }, }; - List>? additionalQueryParams = null; - if (tunnelHasSshPort) - { - additionalQueryParams = new () {new KeyValuePair("includeSshGatewayPublicKey", "true")}; - } - endpoint = (TunnelRelayTunnelEndpoint)await ManagementClient!.UpdateTunnelEndpointAsync( Tunnel, endpoint, - options: new TunnelRequestOptions() - { - AdditionalQueryParameters = additionalQueryParams, - }, + options: null, cancellation); EndpointSignature = endpointSignature; diff --git a/cs/src/Contracts/TunnelPort.cs b/cs/src/Contracts/TunnelPort.cs index 34ee515f..b7b5f9aa 100644 --- a/cs/src/Contracts/TunnelPort.cs +++ b/cs/src/Contracts/TunnelPort.cs @@ -92,7 +92,7 @@ public TunnelPort() /// /// Selection of a default port for a connection also depends on matching the connection to the /// port , so it is possible to configure separate defaults for distinct - /// protocols like and . + /// protocols like and . /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public bool IsDefault { get; set; } @@ -134,7 +134,7 @@ public TunnelPort() /// Gets or sets the username for the ssh service user is trying to forward. /// /// - /// Should be provided if the is Ssh. + /// This property is retained for compatibility with legacy SSH tunnel ports. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [StringLength(SshUserMaxLength)] diff --git a/cs/src/Contracts/TunnelProtocol.cs b/cs/src/Contracts/TunnelProtocol.cs index 696297c9..d13e071b 100644 --- a/cs/src/Contracts/TunnelProtocol.cs +++ b/cs/src/Contracts/TunnelProtocol.cs @@ -27,11 +27,6 @@ public static class TunnelProtocol /// public const string Udp = "udp"; - /// - /// SSH protocol. - /// - public const string Ssh = "ssh"; - /// /// Remote desktop protocol. /// diff --git a/go/tunnels/tunnel_port.go b/go/tunnels/tunnel_port.go index da25d172..11582dc5 100644 --- a/go/tunnels/tunnel_port.go +++ b/go/tunnels/tunnel_port.go @@ -39,7 +39,7 @@ type TunnelPort struct { // // Selection of a default port for a connection also depends on matching the connection // to the port `TunnelPort.Protocol`, so it is possible to configure separate defaults - // for distinct protocols like `TunnelProtocol.Http` and `TunnelProtocol.Ssh`. + // for distinct protocols like `TunnelProtocol.Http` and `TunnelProtocol.Tcp`. IsDefault bool `json:"isDefault,omitempty"` // Gets or sets a dictionary mapping from scopes to tunnel access tokens. @@ -61,7 +61,7 @@ type TunnelPort struct { // Gets or sets the username for the ssh service user is trying to forward. // - // Should be provided if the `TunnelProtocol` is Ssh. + // This property is retained for compatibility with legacy SSH tunnel ports. SshUser string `json:"sshUser,omitempty"` // Gets or sets web forwarding URIs. If set, it's a list of absolute URIs where the port diff --git a/go/tunnels/tunnel_protocol.go b/go/tunnels/tunnel_protocol.go index d68c4058..6305a76a 100644 --- a/go/tunnels/tunnel_protocol.go +++ b/go/tunnels/tunnel_protocol.go @@ -17,9 +17,6 @@ const ( // Unknown UDP protocol. TunnelProtocolUdp TunnelProtocol = "udp" - // SSH protocol. - TunnelProtocolSsh TunnelProtocol = "ssh" - // Remote desktop protocol. TunnelProtocolRdp TunnelProtocol = "rdp" diff --git a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelPort.java b/java/src/main/java/com/microsoft/tunnels/contracts/TunnelPort.java index 51f2a4e1..7689b8b4 100644 --- a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelPort.java +++ b/java/src/main/java/com/microsoft/tunnels/contracts/TunnelPort.java @@ -67,7 +67,7 @@ public class TunnelPort { * Selection of a default port for a connection also depends on matching the * connection to the port {@link TunnelPort#protocol}, so it is possible to configure * separate defaults for distinct protocols like {@link TunnelProtocol#http} and - * {@link TunnelProtocol#ssh}. + * {@link TunnelProtocol#tcp}. */ @Expose public boolean isDefault; @@ -105,7 +105,7 @@ public class TunnelPort { /** * Gets or sets the username for the ssh service user is trying to forward. * - * Should be provided if the {@link TunnelProtocol} is Ssh. + * This property is retained for compatibility with legacy SSH tunnel ports. */ @Expose public String sshUser; diff --git a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelProtocol.java b/java/src/main/java/com/microsoft/tunnels/contracts/TunnelProtocol.java index 483e2f64..291331b7 100644 --- a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelProtocol.java +++ b/java/src/main/java/com/microsoft/tunnels/contracts/TunnelProtocol.java @@ -23,11 +23,6 @@ public class TunnelProtocol { */ public static final String udp = "udp"; - /** - * SSH protocol. - */ - public static final String ssh = "ssh"; - /** * Remote desktop protocol. */ diff --git a/rs/src/contracts/tunnel_port.rs b/rs/src/contracts/tunnel_port.rs index fecb2b8b..08d03534 100644 --- a/rs/src/contracts/tunnel_port.rs +++ b/rs/src/contracts/tunnel_port.rs @@ -52,7 +52,7 @@ pub struct TunnelPort { // Selection of a default port for a connection also depends on matching the // connection to the port `TunnelPort.Protocol`, so it is possible to configure // separate defaults for distinct protocols like `TunnelProtocol.Http` and - // `TunnelProtocol.Ssh`. + // `TunnelProtocol.Tcp`. #[serde(default)] pub is_default: bool, @@ -79,7 +79,7 @@ pub struct TunnelPort { // Gets or sets the username for the ssh service user is trying to forward. // - // Should be provided if the `TunnelProtocol` is Ssh. + // This property is retained for compatibility with legacy SSH tunnel ports. #[serde(skip_serializing_if = "Option::is_none")] pub ssh_user: Option, diff --git a/rs/src/contracts/tunnel_protocol.rs b/rs/src/contracts/tunnel_protocol.rs index d0ca39a8..18a86952 100644 --- a/rs/src/contracts/tunnel_protocol.rs +++ b/rs/src/contracts/tunnel_protocol.rs @@ -13,9 +13,6 @@ pub const TUNNEL_PROTOCOL_TCP: &str = r#"tcp"#; // Unknown UDP protocol. pub const TUNNEL_PROTOCOL_UDP: &str = r#"udp"#; -// SSH protocol. -pub const TUNNEL_PROTOCOL_SSH: &str = r#"ssh"#; - // Remote desktop protocol. pub const TUNNEL_PROTOCOL_RDP: &str = r#"rdp"#; diff --git a/ts/src/connections/tunnelRelayTunnelHost.ts b/ts/src/connections/tunnelRelayTunnelHost.ts index 715fc073..03d2a2fc 100644 --- a/ts/src/connections/tunnelRelayTunnelHost.ts +++ b/ts/src/connections/tunnelRelayTunnelHost.ts @@ -3,7 +3,6 @@ import { TunnelConnectionMode, - TunnelProtocol, TunnelRelayTunnelEndpoint, TunnelPort, Tunnel, @@ -284,11 +283,10 @@ export class TunnelRelayTunnelHost extends TunnelConnectionSession implements Tu this.hostPublicKeys = [buffer.toString('base64')]; } - const tunnelHasSshPort = this.tunnel?.ports != null && this.tunnel.ports.find((v) => v.protocol === TunnelProtocol.Ssh); const endpointSignature = `${this.tunnel?.tunnelId}.${this.tunnel?.clusterId}:` + `${this.tunnel?.name}.${this.tunnel?.domain}:` + - `${tunnelHasSshPort}:${this.hostId}:${this.hostPublicKeys}`; + `${this.hostId}:${this.hostPublicKeys}`; if (!this.relayUri || this.endpointSignature !== endpointSignature) { if (!this.tunnel) { @@ -302,14 +300,7 @@ export class TunnelRelayTunnelHost extends TunnelConnectionSession implements Tu connectionMode: TunnelConnectionMode.TunnelRelay, }; - let additionalQueryParameters = undefined; - if (tunnelHasSshPort) { - additionalQueryParameters = { includeSshGatewayPublicKey: 'true' }; - } - - endpoint = await this.managementClient!.updateTunnelEndpoint(this.tunnel, endpoint, { - additionalQueryParameters: additionalQueryParameters, - }); + endpoint = await this.managementClient!.updateTunnelEndpoint(this.tunnel, endpoint); this.relayUri = endpoint.hostRelayUri!; this.endpointSignature = endpointSignature; diff --git a/ts/src/contracts/tunnelPort.ts b/ts/src/contracts/tunnelPort.ts index 4c5080c2..5051aee1 100644 --- a/ts/src/contracts/tunnelPort.ts +++ b/ts/src/contracts/tunnelPort.ts @@ -60,7 +60,7 @@ export interface TunnelPort { * Selection of a default port for a connection also depends on matching the * connection to the port {@link TunnelPort.protocol}, so it is possible to configure * separate defaults for distinct protocols like {@link TunnelProtocol.http} and - * {@link TunnelProtocol.ssh}. + * {@link TunnelProtocol.tcp}. */ isDefault?: boolean; @@ -93,7 +93,7 @@ export interface TunnelPort { /** * Gets or sets the username for the ssh service user is trying to forward. * - * Should be provided if the {@link TunnelProtocol} is Ssh. + * This property is retained for compatibility with legacy SSH tunnel ports. */ sshUser?: string; diff --git a/ts/src/contracts/tunnelProtocol.ts b/ts/src/contracts/tunnelProtocol.ts index 3a414704..1e03c744 100644 --- a/ts/src/contracts/tunnelProtocol.ts +++ b/ts/src/contracts/tunnelProtocol.ts @@ -22,11 +22,6 @@ export enum TunnelProtocol { */ Udp = 'udp', - /** - * SSH protocol. - */ - Ssh = 'ssh', - /** * Remote desktop protocol. */ From 6b64b9888fc09330464b4721015711e69cf0de29 Mon Sep 17 00:00:00 2001 From: David Obando Date: Tue, 22 Sep 2026 12:01:41 -0700 Subject: [PATCH 2/2] Add coverage for legacy SSH ports Capture relay endpoint request options in the C# and TypeScript test clients and verify legacy SSH ports no longer request an SSH gateway key. Bump the Go SDK patch version for the contract API change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 57ace76a-7df5-4dc5-b59f-1a4b8b239960 --- .../Mocks/MockTunnelManagementClient.cs | 3 ++ .../TunnelHostAndClientTests.cs | 29 +++++++++++++++++++ go/tunnels/tunnels.go | 2 +- .../mocks/mockTunnelManagementClient.ts | 2 ++ .../tunnels-test/tunnelHostAndClientTests.ts | 16 ++++++++++ 5 files changed, 51 insertions(+), 1 deletion(-) diff --git a/cs/test/TunnelsSDK.Test/Mocks/MockTunnelManagementClient.cs b/cs/test/TunnelsSDK.Test/Mocks/MockTunnelManagementClient.cs index 2532518c..abd954fb 100644 --- a/cs/test/TunnelsSDK.Test/Mocks/MockTunnelManagementClient.cs +++ b/cs/test/TunnelsSDK.Test/Mocks/MockTunnelManagementClient.cs @@ -121,6 +121,7 @@ public Task UpdateTunnelEndpointAsync( CancellationToken cancellation = default) { TunnelEndpointsUpdated++; + LastTunnelEndpointUpdateOptions = options; tunnel.Endpoints ??= Array.Empty(); for (int i = 0; i < tunnel.Endpoints.Length; i++) @@ -153,6 +154,8 @@ public Task UpdateTunnelEndpointAsync( public int TunnelEndpointsUpdated { get; private set; } + public TunnelRequestOptions LastTunnelEndpointUpdateOptions { get; private set; } + public Task DeleteTunnelEndpointsAsync( Tunnel tunnel, string Id, diff --git a/cs/test/TunnelsSDK.Test/TunnelHostAndClientTests.cs b/cs/test/TunnelsSDK.Test/TunnelHostAndClientTests.cs index 576ba93f..df21e6cc 100644 --- a/cs/test/TunnelsSDK.Test/TunnelHostAndClientTests.cs +++ b/cs/test/TunnelsSDK.Test/TunnelHostAndClientTests.cs @@ -714,6 +714,35 @@ public async Task ConnectRelayHost() await clientSshSession.ConnectAsync(clientRelayStream, TestContext.Current.CancellationToken); } + [Fact] + public async Task ConnectRelayHostWithLegacySshPortDoesNotRequestSshGatewayKey() + { + var managementClient = new MockTunnelManagementClient + { + HostRelayUri = MockHostRelayUri, + }; + var relayHost = new TunnelRelayTunnelHost(managementClient, TestTS); + var tunnel = CreateRelayTunnel(); + tunnel.Ports = + [ + new TunnelPort + { + PortNumber = 22, + Protocol = "ssh", + }, + ]; + + using var serverSshSession = await ConnectRelayHostAsync( + relayHost, + tunnel, + cancellation: TestContext.Current.CancellationToken); + + Assert.Equal(1, managementClient.TunnelEndpointsUpdated); + Assert.Null(managementClient.LastTunnelEndpointUpdateOptions); + + await relayHost.DisposeAsync(); + } + [Fact] public async Task ConnectRelayHostAfterDisconnect() { diff --git a/go/tunnels/tunnels.go b/go/tunnels/tunnels.go index 5d89436a..61c4f390 100644 --- a/go/tunnels/tunnels.go +++ b/go/tunnels/tunnels.go @@ -10,7 +10,7 @@ import ( "github.com/rodaine/table" ) -const PackageVersion = "0.2.0" +const PackageVersion = "0.2.1" func (tunnel *Tunnel) requestObject() (*Tunnel, error) { convertedTunnel := &Tunnel{ diff --git a/ts/test/tunnels-test/mocks/mockTunnelManagementClient.ts b/ts/test/tunnels-test/mocks/mockTunnelManagementClient.ts index a690472d..0ba23d3d 100644 --- a/ts/test/tunnels-test/mocks/mockTunnelManagementClient.ts +++ b/ts/test/tunnels-test/mocks/mockTunnelManagementClient.ts @@ -143,6 +143,7 @@ export class MockTunnelManagementClient implements TunnelManagementClient { } public tunnelEndpointsUpdated: number = 0; + public lastTunnelEndpointUpdateOptions?: TunnelRequestOptions; updateTunnelEndpoint( tunnel: Tunnel, @@ -150,6 +151,7 @@ export class MockTunnelManagementClient implements TunnelManagementClient { options?: TunnelRequestOptions, ): Promise { this.tunnelEndpointsUpdated++; + this.lastTunnelEndpointUpdateOptions = options; if (!tunnel.endpoints) { tunnel.endpoints = []; } diff --git a/ts/test/tunnels-test/tunnelHostAndClientTests.ts b/ts/test/tunnels-test/tunnelHostAndClientTests.ts index c74f5196..e02d9c3d 100644 --- a/ts/test/tunnels-test/tunnelHostAndClientTests.ts +++ b/ts/test/tunnels-test/tunnelHostAndClientTests.ts @@ -769,6 +769,22 @@ export class TunnelHostAndClientTests { assert.strictEqual(relayHost.connectionStatus, ConnectionStatus.Disconnected); } + @test + public async connectRelayHostWithLegacySshPortDoesNotRequestSshGatewayKey() { + const managementClient = new MockTunnelManagementClient(); + managementClient.hostRelayUri = this.mockHostRelayUri; + const relayHost = new TunnelRelayTunnelHost(managementClient); + const tunnel = this.createRelayTunnel(); + tunnel.ports = [{ portNumber: 22, protocol: 'ssh' }]; + + await this.connectRelayHost({ relayHost, tunnel }); + + assert.equal(1, managementClient.tunnelEndpointsUpdated); + assert.strictEqual(managementClient.lastTunnelEndpointUpdateOptions, undefined); + + await relayHost.dispose(); + } + @test public async connectRelayHostAfterDisconnect() { const managementClient = new MockTunnelManagementClient();