Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/node/tunnelHostMainService.ts
13394 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import type { Tunnel } from '@microsoft/dev-tunnels-contracts';
7
import type { TunnelManagementHttpClient } from '@microsoft/dev-tunnels-management';
8
import { connect } from 'net';
9
import { hostname } from 'os';
10
import { Emitter, Event } from '../../../base/common/event.js';
11
import { Disposable, MutableDisposable } from '../../../base/common/lifecycle.js';
12
import { joinPath } from '../../../base/common/resources.js';
13
import { IConfigurationService } from '../../configuration/common/configuration.js';
14
import { INativeEnvironmentService } from '../../environment/common/environment.js';
15
import { ILogger, ILoggerService } from '../../log/common/log.js';
16
import { localize } from '../../../nls.js';
17
import { CONFIGURATION_KEY_HOST_NAME } from '../../remoteTunnel/common/remoteTunnel.js';
18
import {
19
ITunnelAgentHostHostingService,
20
PROTOCOL_VERSION_TAG_PREFIX,
21
TUNNEL_AGENT_HOST_PORT,
22
TUNNEL_HOST_LOG_ID,
23
TUNNEL_LAUNCHER_LABEL,
24
TUNNEL_MIN_PROTOCOL_VERSION,
25
type ITunnelHostInfo,
26
type TunnelHostStatus,
27
} from '../common/tunnelAgentHost.js';
28
import type { IAgentHostSocketInfo } from '../common/agentService.js';
29
30
/** State of a currently hosted tunnel. */
31
interface IActiveTunnel {
32
readonly info: ITunnelHostInfo;
33
readonly tunnel: Tunnel;
34
readonly host: { dispose(): void };
35
readonly client: TunnelManagementHttpClient;
36
}
37
38
export class TunnelHostMainService extends Disposable implements ITunnelAgentHostHostingService {
39
declare readonly _serviceBrand: undefined;
40
41
private readonly _onDidChangeStatus = this._register(new Emitter<TunnelHostStatus>());
42
readonly onDidChangeStatus: Event<TunnelHostStatus> = this._onDidChangeStatus.event;
43
44
private readonly _activeTunnel = this._register(new MutableDisposable());
45
private _active: IActiveTunnel | undefined;
46
47
private readonly _logger: ILogger;
48
49
constructor(
50
@IConfigurationService private readonly _configurationService: IConfigurationService,
51
@ILoggerService loggerService: ILoggerService,
52
@INativeEnvironmentService environmentService: INativeEnvironmentService,
53
) {
54
super();
55
56
this._logger = this._register(loggerService.createLogger(
57
joinPath(environmentService.logsHome, `${TUNNEL_HOST_LOG_ID}.log`),
58
{ id: TUNNEL_HOST_LOG_ID, name: localize('tunnelHost.log', "Remote Connections") },
59
));
60
}
61
62
async startHosting(token: string, authProvider: 'github' | 'microsoft', socketInfo: IAgentHostSocketInfo): Promise<ITunnelHostInfo> {
63
// Stop any existing tunnel first
64
if (this._active) {
65
await this.stopHosting();
66
}
67
68
const tunnelName = this._getTunnelName();
69
this._logger.info(`Starting tunnel hosting as '${tunnelName}'...`);
70
71
const client = await this._createManagementClient(token, authProvider);
72
73
// Create tunnel with agent host port and appropriate labels
74
const protocolVersionTag = `${PROTOCOL_VERSION_TAG_PREFIX}${TUNNEL_MIN_PROTOCOL_VERSION}`;
75
76
const newTunnel: Tunnel = {
77
ports: [{
78
portNumber: TUNNEL_AGENT_HOST_PORT,
79
protocol: 'https',
80
}],
81
labels: [TUNNEL_LAUNCHER_LABEL, tunnelName, protocolVersionTag],
82
};
83
84
const tunnelRequestOptions = {
85
tokenScopes: ['host', 'connect'],
86
includePorts: true,
87
};
88
89
const tunnel = await client.createOrUpdateTunnel(newTunnel, tunnelRequestOptions);
90
this._logger.info(`Tunnel created: ${tunnel.tunnelId} in cluster ${tunnel.clusterId}`);
91
92
// Host the tunnel using TunnelRelayTunnelHost.
93
// We disable automatic local port forwarding so that we can capture
94
// the raw data stream and pipe it into the agent host process
95
// directly, without needing a physical TCP listener on port 31546.
96
const { TunnelRelayTunnelHost } = await import('@microsoft/dev-tunnels-connections');
97
const host = new TunnelRelayTunnelHost(client);
98
host.forwardConnectionsToLocalPorts = false;
99
host.trace = (_level: unknown, _eventId: unknown, msg: string) => {
100
this._logger.debug(`relay: ${msg}`);
101
};
102
103
// When a remote client connects to the tunnel port, the SDK fires
104
// the forwardedPortConnecting event with the port number and an
105
// SshStream for the connection. We pipe that stream into a
106
// connection to the local agent host process.
107
const { socketPath } = socketInfo;
108
109
host.forwardedPortConnecting((e: { port: number; stream: NodeJS.ReadWriteStream }) => {
110
if (e.port === TUNNEL_AGENT_HOST_PORT) {
111
this._logger.info(`Incoming connection on port ${TUNNEL_AGENT_HOST_PORT}, piping to local agent host`);
112
this._pipeToLocalAgentHost(e.stream, socketPath);
113
} else {
114
this._logger.warn(`Unexpected port ${e.port}, closing stream`);
115
e.stream.end?.();
116
}
117
});
118
119
await host.connect(tunnel);
120
this._logger.info(`Tunnel relay host connected`);
121
122
const domain = tunnel.ports?.[0]?.portForwardingUris?.[0] ?? `${tunnel.tunnelId}.${tunnel.clusterId}.devtunnels.ms`;
123
const info: ITunnelHostInfo = {
124
tunnelName,
125
tunnelId: tunnel.tunnelId!,
126
clusterId: tunnel.clusterId!,
127
domain: typeof domain === 'string' ? domain : `${tunnel.tunnelId}.${tunnel.clusterId}.devtunnels.ms`,
128
};
129
130
this._active = { info, tunnel, host, client };
131
this._activeTunnel.value = {
132
dispose: () => {
133
host.dispose();
134
this._active = undefined;
135
}
136
};
137
138
this._onDidChangeStatus.fire({ active: true, info });
139
return info;
140
}
141
142
async stopHosting(): Promise<void> {
143
if (!this._active) {
144
return;
145
}
146
147
const { tunnel, client } = this._active;
148
this._logger.info(`Stopping tunnel hosting...`);
149
150
// Delete the tunnel from the management service before
151
// tearing down the local relay so we can retry on failure
152
try {
153
await client.deleteTunnel(tunnel);
154
this._logger.info(`Tunnel deleted`);
155
} catch (err) {
156
this._logger.warn(`Failed to delete tunnel`, err);
157
}
158
159
this._activeTunnel.clear();
160
161
this._onDidChangeStatus.fire({ active: false });
162
}
163
164
async getStatus(): Promise<TunnelHostStatus> {
165
if (this._active) {
166
return { active: true, info: this._active.info };
167
}
168
return { active: false };
169
}
170
171
/**
172
* Get the sanitized tunnel name from configuration or OS hostname.
173
*/
174
private _getTunnelName(): string {
175
let name = this._configurationService.getValue<string>(CONFIGURATION_KEY_HOST_NAME) || hostname();
176
name = name.replace(/^-+/g, '').replace(/[^\w-]/g, '').substring(0, 20);
177
return name || 'vscode';
178
}
179
180
private async _createManagementClient(token: string, authProvider: 'github' | 'microsoft'): Promise<TunnelManagementHttpClient> {
181
const mgmt = await import('@microsoft/dev-tunnels-management');
182
const authHeader = authProvider === 'github' ? `github ${token}` : `Bearer ${token}`;
183
184
return new mgmt.TunnelManagementHttpClient(
185
'vscode-sessions',
186
mgmt.ManagementApiVersions.Version20230927preview,
187
async () => authHeader,
188
);
189
}
190
191
/**
192
* Pipe an incoming tunnel stream to the local agent host.
193
* The SshStream from the dev tunnels SDK is a Node.js duplex stream — we
194
* connect to the agent host's local socket and bidirectionally pipe data.
195
*/
196
private _pipeToLocalAgentHost(incomingStream: NodeJS.ReadWriteStream, socketPath: string): void {
197
const socket = connect(socketPath);
198
199
socket.on('connect', () => {
200
this._logger.debug(`Connected to local agent host socket`);
201
incomingStream.pipe(socket);
202
socket.pipe(incomingStream);
203
});
204
205
socket.on('error', (err) => {
206
this._logger.error(`Socket error`, err);
207
incomingStream.end?.();
208
});
209
210
incomingStream.on('error', () => {
211
socket.destroy();
212
});
213
}
214
215
override dispose(): void {
216
if (this._active) {
217
// Best-effort cleanup on dispose — don't await
218
this.stopHosting().catch(() => { /* ignore */ });
219
}
220
super.dispose();
221
}
222
}
223
224