@microsoft/dev-tunnels-connections
Version:
Tunnels library for Visual Studio tools
565 lines • 30.1 kB
JavaScript
"use strict";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
Object.defineProperty(exports, "__esModule", { value: true });
exports.TunnelRelayTunnelHost = void 0;
const dev_tunnels_contracts_1 = require("@microsoft/dev-tunnels-contracts");
const dev_tunnels_ssh_1 = require("@microsoft/dev-tunnels-ssh");
const dev_tunnels_ssh_tcp_1 = require("@microsoft/dev-tunnels-ssh-tcp");
const sshHelpers_1 = require("./sshHelpers");
const multiModeTunnelHost_1 = require("./multiModeTunnelHost");
const sessionPortKey_1 = require("./sessionPortKey");
const portRelayConnectRequestMessage_1 = require("./messages/portRelayConnectRequestMessage");
const portRelayConnectResponseMessage_1 = require("./messages/portRelayConnectResponseMessage");
const uuid_1 = require("uuid");
const sshHelpers_2 = require("./sshHelpers");
const tunnelConnectionSession_1 = require("./tunnelConnectionSession");
const webSocketSubProtocol = 'tunnel-relay-host';
const webSocketSubProtocolv2 = 'tunnel-relay-host-v2-dev';
// Check for an environment variable to determine which protocol version to use.
// By default, prefer V2 and fall back to V1.
const protocolVersion = (process === null || process === void 0 ? void 0 : process.env) && process.env.DEVTUNNELS_PROTOCOL_VERSION;
const connectionProtocols = protocolVersion === '1' ? [webSocketSubProtocol] :
protocolVersion === '2' ? [webSocketSubProtocolv2] :
[webSocketSubProtocolv2, webSocketSubProtocol];
/**
* Tunnel host implementation that uses data-plane relay
* to accept client connections.
*/
class TunnelRelayTunnelHost extends tunnelConnectionSession_1.TunnelConnectionSession {
constructor(managementClient, trace) {
super(dev_tunnels_contracts_1.TunnelAccessScopes.Host, connectionProtocols, managementClient, trace);
this.clientSessionPromises = [];
this.reconnectableSessions = [];
/**
* Sessions created between this host and clients
* @internal
*/
this.sshSessions = [];
/**
* Port Forwarders between host and clients
*/
this.remoteForwarders = new Map();
this.loopbackIp = '127.0.0.1';
this.forwardConnectionsToLocalPortsValue = (0, sshHelpers_2.isNode)();
const publicKey = dev_tunnels_ssh_1.SshAlgorithms.publicKey.ecdsaSha2Nistp384;
if (publicKey) {
this.hostPrivateKeyPromise = publicKey.generateKeyPair();
}
this.hostId = multiModeTunnelHost_1.MultiModeTunnelHost.hostId;
this.id = (0, uuid_1.v4)() + "-relay";
}
get connectionId() {
return this.hostId;
}
/**
* A value indicating whether the port-forwarding service forwards connections to local TCP sockets.
* Forwarded connections are not possible if the host is not NodeJS (e.g. browser).
* The default value for NodeJS hosts is true.
*/
get forwardConnectionsToLocalPorts() {
return this.forwardConnectionsToLocalPortsValue;
}
set forwardConnectionsToLocalPorts(value) {
if (value === this.forwardConnectionsToLocalPortsValue) {
return;
}
if (value && !(0, sshHelpers_2.isNode)()) {
throw new Error('Cannot forward connections to local TCP sockets on this platform.');
}
this.forwardConnectionsToLocalPortsValue = value;
}
/**
* Connects to a tunnel as a host and starts accepting incoming connections
* to local ports as defined on the tunnel.
* @deprecated Use `connect()` instead.
*/
async start(tunnel) {
await this.connect(tunnel);
}
/**
* Connects to a tunnel as a host and starts accepting incoming connections
* to local ports as defined on the tunnel.
*/
async connect(tunnel, options, cancellation) {
await this.connectTunnelSession(tunnel, options, cancellation);
}
/**
* Connect to the tunnel session with the tunnel connector.
* @param tunnel Tunnel to use for the connection.
* Undefined if the connection information is already known and the tunnel is not needed.
* Tunnel object to get the connection information from that tunnel.
*/
async connectTunnelSession(tunnel, options, cancellation) {
if (this.disconnectReason === dev_tunnels_ssh_1.SshDisconnectReason.tooManyConnections) {
// If another host for the same tunnel connects, the first connection is disconnected
// with "too many connections" reason. Reconnecting it again would cause the second host to
// be kicked out, and then it would try to reconnect, kicking out this one.
// To prevent this tug of war, do not allow reconnection in this case.
throw new dev_tunnels_ssh_1.SshConnectionError('Cannot retry connection because another host for this tunnel has connected. ' +
'Only one host connection at a time is supported.', dev_tunnels_ssh_1.SshDisconnectReason.tooManyConnections);
}
await super.connectTunnelSession(tunnel, options, cancellation);
}
/**
* Configures the tunnel session with the given stream.
* @internal
*/
async configureSession(stream, protocol, isReconnect, cancellation) {
this.connectionProtocol = protocol;
let session;
if (this.connectionProtocol === webSocketSubProtocol) {
// The V1 protocol always configures no security, equivalent to SSH MultiChannelStream.
// The websocket transport is still encrypted and authenticated.
session = new dev_tunnels_ssh_1.SshClientSession(new dev_tunnels_ssh_1.SshSessionConfiguration(false)); // no encryption
}
else {
session = sshHelpers_1.SshHelpers.createSshClientSession((config) => {
// The V2 protocol configures optional encryption, including "none" as an enabled
// and preferred key-exchange algorithm, because encryption of the outer SSH
// session is optional since it is already over a TLS websocket.
config.keyExchangeAlgorithms.splice(0, 0, dev_tunnels_ssh_1.SshAlgorithms.keyExchange.none);
config.addService(dev_tunnels_ssh_tcp_1.PortForwardingService);
});
const hostPfs = session.activateService(dev_tunnels_ssh_tcp_1.PortForwardingService);
hostPfs.messageFactory = this;
hostPfs.onForwardedPortConnecting(this.onForwardedPortConnecting, this, this.sshSessionDisposables);
}
session.onChannelOpening(this.hostSession_ChannelOpening, this, this.sshSessionDisposables);
session.onClosed(this.onSshSessionClosed, this, this.sshSessionDisposables);
session.trace = this.trace;
session.onReportProgress((args) => this.raiseReportProgress(args.progress, args.sessionNumber), this, this.sshSessionDisposables);
this.sshSession = session;
await session.connect(stream, cancellation);
// SSH authentication is skipped in V1 protocol, optional in V2 depending on whether the
// session performed a key exchange (as indicated by having a session ID or not). In the
// latter case a password is not required. Strong authentication was already handled by
// the relay service via the tunnel access token used for the websocket connection.
if (session.sessionId) {
await session.authenticate({ username: 'tunnel' });
}
if (this.connectionProtocol === webSocketSubProtocolv2) {
// In the v2 protocol, the host starts "forwarding" the ports as soon as it connects.
// Then the relay will forward the forwarded ports to clients as they connect.
await this.startForwardingExistingPorts(session);
}
}
/**
* Validate the {@link tunnel} and get data needed to connect to it, if the tunnel is provided;
* otherwise, ensure that there is already sufficient data to connect to a tunnel.
* @internal
*/
async onConnectingToTunnel() {
var _a, _b, _c, _d, _e;
if (!this.hostPrivateKey || !this.hostPublicKeys) {
if (!this.hostPrivateKeyPromise) {
throw new Error('Cannot create host keys');
}
this.hostPrivateKey = await this.hostPrivateKeyPromise;
const buffer = await this.hostPrivateKey.getPublicKeyBytes(this.hostPrivateKey.keyAlgorithmName);
if (!buffer) {
throw new Error('Host private key public key bytes is not initialized');
}
this.hostPublicKeys = [buffer.toString('base64')];
}
const tunnelHasSshPort = ((_a = this.tunnel) === null || _a === void 0 ? void 0 : _a.ports) != null && this.tunnel.ports.find((v) => v.protocol === dev_tunnels_contracts_1.TunnelProtocol.Ssh);
const endpointSignature = `${(_b = this.tunnel) === null || _b === void 0 ? void 0 : _b.tunnelId}.${(_c = this.tunnel) === null || _c === void 0 ? void 0 : _c.clusterId}:` +
`${(_d = this.tunnel) === null || _d === void 0 ? void 0 : _d.name}.${(_e = this.tunnel) === null || _e === void 0 ? void 0 : _e.domain}:` +
`${tunnelHasSshPort}:${this.hostId}:${this.hostPublicKeys}`;
if (!this.relayUri || this.endpointSignature !== endpointSignature) {
if (!this.tunnel) {
throw new Error('Tunnel is required');
}
let endpoint = {
id: this.id,
hostId: this.hostId,
hostPublicKeys: this.hostPublicKeys,
connectionMode: dev_tunnels_contracts_1.TunnelConnectionMode.TunnelRelay,
};
let additionalQueryParameters = undefined;
if (tunnelHasSshPort) {
additionalQueryParameters = { includeSshGatewayPublicKey: 'true' };
}
endpoint = await this.managementClient.updateTunnelEndpoint(this.tunnel, endpoint, {
additionalQueryParameters: additionalQueryParameters,
});
this.relayUri = endpoint.hostRelayUri;
this.endpointSignature = endpointSignature;
}
}
/**
* Disposes this tunnel session, closing all client connections, the host SSH session, and deleting the endpoint.
*/
async dispose() {
await super.dispose();
const promises = Object.assign([], this.clientSessionPromises);
// No new client session should be added because the channel requests are rejected when the tunnel host is disposed.
this.clientSessionPromises.length = 0;
// If the tunnel is present, the endpoint was created, and this host was not closed because of
// too many connections, delete the endpoint.
// Too many connections closure means another host has connected, and that other host, while
// connecting, would have updated the endpoint. So this host won't be able to delete it anyway.
if (this.tunnel &&
this.endpointSignature &&
this.disconnectReason !== dev_tunnels_ssh_1.SshDisconnectReason.tooManyConnections) {
const promise = this.managementClient.deleteTunnelEndpoints(this.tunnel, this.id);
promises.push(promise);
}
for (const forwarder of this.remoteForwarders.values()) {
forwarder.dispose();
}
// When client session promises finish, they remove the sessions from this.sshSessions
await Promise.all(promises);
}
hostSession_ChannelOpening(e) {
if (!e.isRemoteRequest) {
// Auto approve all local requests (not that there are any for the time being).
return;
}
if (this.connectionProtocol === webSocketSubProtocolv2 &&
e.channel.channelType === 'forwarded-tcpip') {
// With V2 protocol, the relay server always sends an extended channel open message
// with a property indicating whether E2E encryption is requested for the connection.
// The host returns an extended response message indicating if E2EE is enabled.
const relayRequestMessage = e.channel.openMessage
.convertTo(new portRelayConnectRequestMessage_1.PortRelayConnectRequestMessage());
const responseMessage = new portRelayConnectResponseMessage_1.PortRelayConnectResponseMessage();
// The host can enable encryption for the channel if the client requested it.
responseMessage.isE2EEncryptionEnabled = this.enableE2EEncryption &&
relayRequestMessage.isE2EEncryptionRequested;
// In the future the relay might send additional information in the connect
// request message, for example a user identifier that would enable the host to
// group channels by user.
e.openingPromise = Promise.resolve(responseMessage);
return;
}
else if (e.channel.channelType !== TunnelRelayTunnelHost.clientStreamChannelType) {
e.failureDescription = `Unknown channel type: ${e.channel.channelType}`;
e.failureReason = dev_tunnels_ssh_1.SshChannelOpenFailureReason.unknownChannelType;
return;
}
// V1 protocol.
// Increase max window size to work around channel congestion bug.
// This does not entirely eliminate the problem, but reduces the chance.
e.channel.maxWindowSize = dev_tunnels_ssh_1.SshChannel.defaultMaxWindowSize * 5;
if (this.isDisposed) {
e.failureDescription = 'The host is disconnecting.';
e.failureReason = dev_tunnels_ssh_1.SshChannelOpenFailureReason.connectFailed;
return;
}
const promise = this.acceptClientSession(e.channel, this.disposeToken);
this.clientSessionPromises.push(promise);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
promise.then(() => {
const index = this.clientSessionPromises.indexOf(promise);
this.clientSessionPromises.splice(index, 1);
});
}
onForwardedPortConnecting(e) {
const channel = e.stream.channel;
const relayRequestMessage = channel.openMessage.convertTo(new portRelayConnectRequestMessage_1.PortRelayConnectRequestMessage());
const isE2EEncryptionEnabled = this.enableE2EEncryption &&
relayRequestMessage.isE2EEncryptionRequested;
if (isE2EEncryptionEnabled) {
// Increase the max window size so that it is at least larger than the window
// size of one client channel.
channel.maxWindowSize = dev_tunnels_ssh_1.SshChannel.defaultMaxWindowSize * 2;
const serverCredentials = {
publicKeys: [this.hostPrivateKey]
};
const secureStream = new dev_tunnels_ssh_1.SecureStream(e.stream, serverCredentials, this.reconnectableSessions);
secureStream.trace = this.trace;
// The client was already authenticated by the relay.
secureStream.onAuthenticating((authEvent) => authEvent.authenticationPromise = Promise.resolve({}));
// The client will connect to the secure stream after the channel is opened.
secureStream.connect().catch((err) => {
this.trace(dev_tunnels_ssh_1.TraceLevel.Error, 0, `Error connecting encrypted channel: ${err}`);
});
e.transformPromise = Promise.resolve(secureStream);
}
super.onForwardedPortConnecting(e);
}
async acceptClientSession(clientSessionChannel, cancellation) {
try {
const stream = new dev_tunnels_ssh_1.SshStream(clientSessionChannel);
await this.connectAndRunClientSession(stream, cancellation);
}
catch (ex) {
if (!(ex instanceof dev_tunnels_ssh_1.CancellationError) || !cancellation.isCancellationRequested) {
this.trace(dev_tunnels_ssh_1.TraceLevel.Error, 0, `Error running client SSH session: ${ex}`);
}
}
}
/**
* Creates an SSH server session for a client (V1 protocol), runs the session,
* and waits for it to close.
*/
async connectAndRunClientSession(stream, cancellation) {
if (cancellation.isCancellationRequested) {
stream.destroy();
throw new dev_tunnels_ssh_1.CancellationError();
}
const clientChannelId = stream.channel.channelId;
const session = sshHelpers_1.SshHelpers.createSshServerSession(this.reconnectableSessions, (config) => {
var _a;
config.protocolExtensions.push(dev_tunnels_ssh_1.SshProtocolExtensionNames.sessionReconnect);
config.addService(dev_tunnels_ssh_tcp_1.PortForwardingService);
// Configure keep-alive if requested
const keepAliveInterval = (_a = this.connectionOptions) === null || _a === void 0 ? void 0 : _a.keepAliveIntervalInSeconds;
if (keepAliveInterval && keepAliveInterval > 0) {
config.keepAliveTimeoutInSeconds = keepAliveInterval;
}
});
session.trace = this.trace;
session.onReportProgress((args) => this.raiseReportProgress(args.progress, args.sessionNumber), this, this.sshSessionDisposables);
session.credentials = {
publicKeys: [this.hostPrivateKey],
};
const tcs = new dev_tunnels_ssh_1.PromiseCompletionSource();
const authenticatingEventRegistration = session.onAuthenticating((e) => {
this.onSshClientAuthenticating(e);
});
session.onClientAuthenticated(() => {
// This call is async and will catch and log any async errors.
void this.onSshClientAuthenticated(session);
});
const requestRegistration = session.onRequest((e) => {
this.onClientSessionRequest(e, session);
});
const channelOpeningEventRegistration = session.onChannelOpening((e) => {
this.onSshChannelOpening(e, session);
});
const reconnectedEventRegistration = session.onReconnected(() => {
this.onClientSessionReconnecting(session, clientChannelId);
});
const closedEventRegistration = session.onClosed((e) => {
this.onClientSessionClosed(session, e, clientChannelId, cancellation);
tcs.resolve();
});
session.onKeepAliveFailed((count) => this.onKeepAliveFailed(count));
session.onKeepAliveSucceeded((count) => this.onKeepAliveSucceeded(count));
try {
const nodeStream = new dev_tunnels_ssh_1.NodeStream(stream);
await session.connect(nodeStream);
this.sshSessions.push(session);
cancellation.onCancellationRequested((e) => {
tcs.reject(new dev_tunnels_ssh_1.CancellationError());
});
if (this.tunnel && this.managementClient) {
const connectedEvent = {
name: 'host_client_connect',
properties: {
ClientChannelId: clientChannelId.toString(),
ClientSessionId: this.getShortSessionId(session),
HostSessionId: this.connectionId,
}
};
this.managementClient.reportEvent(this.tunnel, connectedEvent);
}
await tcs.promise;
}
finally {
authenticatingEventRegistration.dispose();
requestRegistration.dispose();
channelOpeningEventRegistration.dispose();
reconnectedEventRegistration.dispose();
closedEventRegistration.dispose();
await session.close(dev_tunnels_ssh_1.SshDisconnectReason.byApplication);
session.dispose();
}
}
onSshClientAuthenticating(e) {
if (e.authenticationType === dev_tunnels_ssh_1.SshAuthenticationType.clientNone) {
// For now, the client is allowed to skip SSH authentication;
// they must have a valid tunnel access token already to get this far.
e.authenticationPromise = Promise.resolve({});
}
else {
// Other authentication types are not implemented. Doing nothing here
// results in a client authentication failure.
}
}
async onSshClientAuthenticated(session) {
void this.startForwardingExistingPorts(session);
}
async startForwardingExistingPorts(session) {
var _a, _b;
const pfs = session.activateService(dev_tunnels_ssh_tcp_1.PortForwardingService);
pfs.forwardConnectionsToLocalPorts = this.forwardConnectionsToLocalPorts;
// Ports must be forwarded sequentially because the TS SSH lib
// does not yet support concurrent requests.
for (const port of (_b = (_a = this.tunnel) === null || _a === void 0 ? void 0 : _a.ports) !== null && _b !== void 0 ? _b : []) {
this.trace(dev_tunnels_ssh_1.TraceLevel.Verbose, 0, `Forwarding port ${port.portNumber}`);
try {
await this.forwardPort(pfs, port);
}
catch (ex) {
this.traceError(`Error forwarding port ${port.portNumber}: ${ex}`);
}
}
}
onClientSessionRequest(e, session) {
if (e.requestType === 'RefreshPorts') {
e.responsePromise = (async () => {
await this.refreshPorts();
return new dev_tunnels_ssh_1.SessionRequestSuccessMessage();
})();
}
}
onSshChannelOpening(e, session) {
if (!(e.request instanceof dev_tunnels_ssh_tcp_1.PortForwardChannelOpenMessage)) {
// This is to let the Go SDK open an unused session channel
if (e.request.channelType === dev_tunnels_ssh_1.SshChannel.sessionChannelType) {
return;
}
this.trace(dev_tunnels_ssh_1.TraceLevel.Warning, 0, 'Rejecting request to open non-portforwarding channel.');
e.failureReason = dev_tunnels_ssh_1.SshChannelOpenFailureReason.administrativelyProhibited;
return;
}
const portForwardRequest = e.request;
if (portForwardRequest.channelType === 'direct-tcpip') {
if (!this.tunnel.ports.some((p) => p.portNumber === portForwardRequest.port)) {
this.trace(dev_tunnels_ssh_1.TraceLevel.Warning, 0, 'Rejecting request to connect to non-forwarded port:' + portForwardRequest.port);
e.failureReason = dev_tunnels_ssh_1.SshChannelOpenFailureReason.administrativelyProhibited;
}
}
else if (portForwardRequest.channelType === 'forwarded-tcpip') {
const eventArgs = new dev_tunnels_ssh_tcp_1.ForwardedPortConnectingEventArgs(portForwardRequest.port, false, new dev_tunnels_ssh_1.SshStream(e.channel));
super.onForwardedPortConnecting(eventArgs);
}
else {
// For forwarded-tcpip do not check remoteForwarders because they may not be updated yet.
// There is a small time interval in forwardPort() between the port
// being forwarded with forwardFromRemotePort and remoteForwarders updated.
// Setting PFS.acceptRemoteConnectionsForNonForwardedPorts to false makes PFS reject forwarding requests from the
// clients for the ports that are not forwarded and are missing in PFS.remoteConnectors.
// Call to pfs.forwardFromRemotePort() in forwardPort() adds the connector to PFS.remoteConnectors.
this.trace(dev_tunnels_ssh_1.TraceLevel.Warning, 0, 'Nonrecognized channel type ' + portForwardRequest.channelType);
e.failureReason = dev_tunnels_ssh_1.SshChannelOpenFailureReason.unknownChannelType;
}
}
onClientSessionReconnecting(session, clientChannelId) {
if (this.tunnel && this.managementClient) {
const reconnectedEvent = {
name: 'host_client_reconnect',
properties: {
ClientChannelId: clientChannelId.toString(),
ClientSessionId: this.getShortSessionId(session),
HostSessionId: this.connectionId,
}
};
this.managementClient.reportEvent(this.tunnel, reconnectedEvent);
}
}
onClientSessionClosed(session, e, clientChannelId, cancellation) {
// Determine severity based on the disconnect reason
let severity;
let details;
// Reconnecting client session may cause the new session to close with 'None' reason.
if (e.reason === dev_tunnels_ssh_1.SshDisconnectReason.byApplication) {
details = 'Client ssh session closed by application.';
this.traceInfo(details);
}
else if (cancellation.isCancellationRequested) {
details = 'Client ssh session cancelled.';
this.traceInfo(details);
}
else if (e.reason !== dev_tunnels_ssh_1.SshDisconnectReason.none) {
severity = dev_tunnels_contracts_1.TunnelEvent.error;
details = `Client ssh session closed unexpectedly due to ${e.reason}, ` +
`"${e.message}"\n${e.error}`;
this.traceError(details);
}
else {
details = 'Client ssh session closed.';
}
// Report client disconnected event
if (this.tunnel && this.managementClient) {
const disconnectedEvent = {
timestamp: new Date(),
name: 'host_client_disconnect',
severity: severity,
details: details,
properties: {
ClientChannelId: clientChannelId.toString(),
ClientSessionId: this.getShortSessionId(session),
HostSessionId: this.connectionId,
}
};
this.managementClient.reportEvent(this.tunnel, disconnectedEvent);
}
for (const [key, forwarder] of this.remoteForwarders.entries()) {
if (forwarder.session === session) {
forwarder.dispose();
this.remoteForwarders.delete(key);
}
}
const index = this.sshSessions.indexOf(session);
if (index >= 0) {
this.sshSessions.splice(index, 1);
}
}
async refreshPorts(cancellation) {
var _a, _b;
this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.StartingRefreshPorts);
if (!await this.refreshTunnel(true, cancellation)) {
return;
}
const ports = (_b = (_a = this.tunnel) === null || _a === void 0 ? void 0 : _a.ports) !== null && _b !== void 0 ? _b : [];
let sessions = this.sshSessions;
if (this.connectionProtocol === webSocketSubProtocolv2 && this.sshSession) {
// In the V2 protocol, ports are forwarded directly on the host session.
// (But even when the host is V2, some clients may still connect with V1.)
sessions = [...sessions, this.sshSession];
}
const forwardPromises = [];
for (const port of ports) {
// For all sessions which are connected and authenticated, forward any added/updated
// ports. For sessions that are not yet authenticated, the ports will be forwarded
// immediately after authentication completes - see onSshClientAuthenticated().
// (Session requests may not be sent before the session is authenticated, for sessions
// that require authentication; For V2 sessions that are not encrypted/authenticated
// at all, the session ID is null.)
for (const session of sessions.filter((s) => s.isConnected && (!s.sessionId || s.principal))) {
const key = new sessionPortKey_1.SessionPortKey(session.sessionId, Number(port.portNumber));
const forwarder = this.remoteForwarders.get(key.toString());
if (!forwarder) {
const pfs = session.getService(dev_tunnels_ssh_tcp_1.PortForwardingService);
forwardPromises.push(this.forwardPort(pfs, port));
}
}
}
for (const [key, forwarder] of Object.entries(this.remoteForwarders)) {
if (!ports.some((p) => p.portNumber === forwarder.localPort)) {
this.remoteForwarders.delete(key);
forwarder.dispose();
}
}
await Promise.all(forwardPromises);
this.raiseReportProgress(dev_tunnels_contracts_1.TunnelProgress.CompletedRefreshPorts);
}
async forwardPort(pfs, port) {
const portNumber = Number(port.portNumber);
if (pfs.localForwardedPorts.find((p) => p.localPort === portNumber)) {
// The port is already forwarded. This may happen if we try to add the same port twice after reconnection.
return;
}
// When forwarding from a Remote port we assume that the RemotePortNumber
// and requested LocalPortNumber are the same.
const forwarder = await pfs.forwardFromRemotePort(this.loopbackIp, portNumber, 'localhost', portNumber);
if (!forwarder) {
// The forwarding request was rejected by the client.
return;
}
const key = new sessionPortKey_1.SessionPortKey(pfs.session.sessionId, Number(forwarder.localPort));
this.remoteForwarders.set(key.toString(), forwarder);
}
}
exports.TunnelRelayTunnelHost = TunnelRelayTunnelHost;
TunnelRelayTunnelHost.webSocketSubProtocol = webSocketSubProtocol;
TunnelRelayTunnelHost.webSocketSubProtocolv2 = webSocketSubProtocolv2;
/**
* Ssh channel type in host relay ssh session where client session streams are passed.
*/
TunnelRelayTunnelHost.clientStreamChannelType = 'client-ssh-session-stream';
//# sourceMappingURL=tunnelRelayTunnelHost.js.map