Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/node/extHostTunnelService.ts
3296 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 * as fs from 'fs';
7
import { exec } from 'child_process';
8
import { VSBuffer } from '../../../base/common/buffer.js';
9
import { Emitter } from '../../../base/common/event.js';
10
import { DisposableStore } from '../../../base/common/lifecycle.js';
11
import { MovingAverage } from '../../../base/common/numbers.js';
12
import { isLinux } from '../../../base/common/platform.js';
13
import * as resources from '../../../base/common/resources.js';
14
import { URI } from '../../../base/common/uri.js';
15
import * as pfs from '../../../base/node/pfs.js';
16
import { ISocket, SocketCloseEventType } from '../../../base/parts/ipc/common/ipc.net.js';
17
import { ILogService } from '../../../platform/log/common/log.js';
18
import { ManagedSocket, RemoteSocketHalf, connectManagedSocket } from '../../../platform/remote/common/managedSocket.js';
19
import { ManagedRemoteConnection } from '../../../platform/remote/common/remoteAuthorityResolver.js';
20
import { ISignService } from '../../../platform/sign/common/sign.js';
21
import { isAllInterfaces, isLocalhost } from '../../../platform/tunnel/common/tunnel.js';
22
import { NodeRemoteTunnel } from '../../../platform/tunnel/node/tunnelService.js';
23
import { IExtHostInitDataService } from '../common/extHostInitDataService.js';
24
import { IExtHostRpcService } from '../common/extHostRpcService.js';
25
import { ExtHostTunnelService } from '../common/extHostTunnelService.js';
26
import { CandidatePort, parseAddress } from '../../services/remote/common/tunnelModel.js';
27
import * as vscode from 'vscode';
28
import { IExtHostConfiguration } from '../common/extHostConfiguration.js';
29
30
export function getSockets(stdout: string): Record<string, { pid: number; socket: number }> {
31
const lines = stdout.trim().split('\n');
32
const mapped: { pid: number; socket: number }[] = [];
33
lines.forEach(line => {
34
const match = /\/proc\/(\d+)\/fd\/\d+ -> socket:\[(\d+)\]/.exec(line)!;
35
if (match && match.length >= 3) {
36
mapped.push({
37
pid: parseInt(match[1], 10),
38
socket: parseInt(match[2], 10)
39
});
40
}
41
});
42
const socketMap = mapped.reduce((m: Record<string, typeof mapped[0]>, socket) => {
43
m[socket.socket] = socket;
44
return m;
45
}, {});
46
return socketMap;
47
}
48
49
export function loadListeningPorts(...stdouts: string[]): { socket: number; ip: string; port: number }[] {
50
const table = ([] as Record<string, string>[]).concat(...stdouts.map(loadConnectionTable));
51
return [
52
...new Map(
53
table.filter(row => row.st === '0A')
54
.map(row => {
55
const address = row.local_address.split(':');
56
return {
57
socket: parseInt(row.inode, 10),
58
ip: parseIpAddress(address[0]),
59
port: parseInt(address[1], 16)
60
};
61
}).map(port => [port.ip + ':' + port.port, port])
62
).values()
63
];
64
}
65
66
export function parseIpAddress(hex: string): string {
67
let result = '';
68
if (hex.length === 8) {
69
for (let i = hex.length - 2; i >= 0; i -= 2) {
70
result += parseInt(hex.substr(i, 2), 16);
71
if (i !== 0) {
72
result += '.';
73
}
74
}
75
} else {
76
// Nice explanation of host format in tcp6 file: https://serverfault.com/questions/592574/why-does-proc-net-tcp6-represents-1-as-1000
77
for (let i = 0; i < hex.length; i += 8) {
78
const word = hex.substring(i, i + 8);
79
let subWord = '';
80
for (let j = 8; j >= 2; j -= 2) {
81
subWord += word.substring(j - 2, j);
82
if ((j === 6) || (j === 2)) {
83
// Trim leading zeros
84
subWord = parseInt(subWord, 16).toString(16);
85
result += `${subWord}`;
86
subWord = '';
87
if (i + j !== hex.length - 6) {
88
result += ':';
89
}
90
}
91
}
92
}
93
}
94
return result;
95
}
96
97
export function loadConnectionTable(stdout: string): Record<string, string>[] {
98
const lines = stdout.trim().split('\n');
99
const names = lines.shift()!.trim().split(/\s+/)
100
.filter(name => name !== 'rx_queue' && name !== 'tm->when');
101
const table = lines.map(line => line.trim().split(/\s+/).reduce((obj: Record<string, string>, value, i) => {
102
obj[names[i] || i] = value;
103
return obj;
104
}, {}));
105
return table;
106
}
107
108
function knownExcludeCmdline(command: string): boolean {
109
if (command.length > 500) {
110
return false;
111
}
112
return !!command.match(/.*\.vscode-server-[a-zA-Z]+\/bin.*/)
113
|| (command.indexOf('out/server-main.js') !== -1)
114
|| (command.indexOf('_productName=VSCode') !== -1);
115
}
116
117
export function getRootProcesses(stdout: string) {
118
const lines = stdout.trim().split('\n');
119
const mapped: { pid: number; cmd: string; ppid: number }[] = [];
120
lines.forEach(line => {
121
const match = /^\d+\s+\D+\s+root\s+(\d+)\s+(\d+).+\d+\:\d+\:\d+\s+(.+)$/.exec(line)!;
122
if (match && match.length >= 4) {
123
mapped.push({
124
pid: parseInt(match[1], 10),
125
ppid: parseInt(match[2]),
126
cmd: match[3]
127
});
128
}
129
});
130
return mapped;
131
}
132
133
export async function findPorts(connections: { socket: number; ip: string; port: number }[], socketMap: Record<string, { pid: number; socket: number }>, processes: { pid: number; cwd: string; cmd: string }[]): Promise<CandidatePort[]> {
134
const processMap = processes.reduce((m: Record<string, typeof processes[0]>, process) => {
135
m[process.pid] = process;
136
return m;
137
}, {});
138
139
const ports: CandidatePort[] = [];
140
connections.forEach(({ socket, ip, port }) => {
141
const pid = socketMap[socket] ? socketMap[socket].pid : undefined;
142
const command: string | undefined = pid ? processMap[pid]?.cmd : undefined;
143
if (pid && command && !knownExcludeCmdline(command)) {
144
ports.push({ host: ip, port, detail: command, pid });
145
}
146
});
147
return ports;
148
}
149
150
export function tryFindRootPorts(connections: { socket: number; ip: string; port: number }[], rootProcessesStdout: string, previousPorts: Map<number, CandidatePort & { ppid: number }>): Map<number, CandidatePort & { ppid: number }> {
151
const ports: Map<number, CandidatePort & { ppid: number }> = new Map();
152
const rootProcesses = getRootProcesses(rootProcessesStdout);
153
154
for (const connection of connections) {
155
const previousPort = previousPorts.get(connection.port);
156
if (previousPort) {
157
ports.set(connection.port, previousPort);
158
continue;
159
}
160
const rootProcessMatch = rootProcesses.find((value) => value.cmd.includes(`${connection.port}`));
161
if (rootProcessMatch) {
162
let bestMatch = rootProcessMatch;
163
// There are often several processes that "look" like they could match the port.
164
// The one we want is usually the child of the other. Find the most child process.
165
let mostChild: { pid: number; cmd: string; ppid: number } | undefined;
166
do {
167
mostChild = rootProcesses.find(value => value.ppid === bestMatch.pid);
168
if (mostChild) {
169
bestMatch = mostChild;
170
}
171
} while (mostChild);
172
ports.set(connection.port, { host: connection.ip, port: connection.port, pid: bestMatch.pid, detail: bestMatch.cmd, ppid: bestMatch.ppid });
173
} else {
174
ports.set(connection.port, { host: connection.ip, port: connection.port, ppid: Number.MAX_VALUE });
175
}
176
}
177
178
return ports;
179
}
180
181
export class NodeExtHostTunnelService extends ExtHostTunnelService {
182
private _initialCandidates: CandidatePort[] | undefined = undefined;
183
private _foundRootPorts: Map<number, CandidatePort & { ppid: number }> = new Map();
184
private _candidateFindingEnabled: boolean = false;
185
186
constructor(
187
@IExtHostRpcService extHostRpc: IExtHostRpcService,
188
@IExtHostInitDataService private readonly initData: IExtHostInitDataService,
189
@ILogService logService: ILogService,
190
@ISignService private readonly signService: ISignService,
191
@IExtHostConfiguration private readonly configurationService: IExtHostConfiguration,
192
) {
193
super(extHostRpc, initData, logService);
194
if (isLinux && initData.remote.isRemote && initData.remote.authority) {
195
this._proxy.$setRemoteTunnelService(process.pid);
196
this.setInitialCandidates();
197
}
198
}
199
200
override async $registerCandidateFinder(enable: boolean): Promise<void> {
201
if (enable && this._candidateFindingEnabled) {
202
// already enabled
203
return;
204
}
205
206
this._candidateFindingEnabled = enable;
207
let oldPorts: { host: string; port: number; detail?: string }[] | undefined = undefined;
208
209
// If we already have found initial candidates send those immediately.
210
if (this._initialCandidates) {
211
oldPorts = this._initialCandidates;
212
await this._proxy.$onFoundNewCandidates(this._initialCandidates);
213
}
214
215
// Regularly scan to see if the candidate ports have changed.
216
const movingAverage = new MovingAverage();
217
let scanCount = 0;
218
while (this._candidateFindingEnabled) {
219
const startTime = new Date().getTime();
220
const newPorts = (await this.findCandidatePorts()).filter(candidate => (isLocalhost(candidate.host) || isAllInterfaces(candidate.host)));
221
this.logService.trace(`ForwardedPorts: (ExtHostTunnelService) found candidate ports ${newPorts.map(port => port.port).join(', ')}`);
222
const timeTaken = new Date().getTime() - startTime;
223
this.logService.trace(`ForwardedPorts: (ExtHostTunnelService) candidate port scan took ${timeTaken} ms.`);
224
// Do not count the first few scans towards the moving average as they are likely to be slower.
225
if (scanCount++ > 3) {
226
movingAverage.update(timeTaken);
227
}
228
if (!oldPorts || (JSON.stringify(oldPorts) !== JSON.stringify(newPorts))) {
229
oldPorts = newPorts;
230
await this._proxy.$onFoundNewCandidates(oldPorts);
231
}
232
const delay = this.calculateDelay(movingAverage.value);
233
this.logService.trace(`ForwardedPorts: (ExtHostTunnelService) next candidate port scan in ${delay} ms.`);
234
await (new Promise<void>(resolve => setTimeout(() => resolve(), delay)));
235
}
236
}
237
238
private calculateDelay(movingAverage: number) {
239
// Some local testing indicated that the moving average might be between 50-100 ms.
240
return Math.max(movingAverage * 20, 2000);
241
}
242
243
private async setInitialCandidates(): Promise<void> {
244
this._initialCandidates = await this.findCandidatePorts();
245
this.logService.trace(`ForwardedPorts: (ExtHostTunnelService) Initial candidates found: ${this._initialCandidates.map(c => c.port).join(', ')}`);
246
}
247
248
private async findCandidatePorts(): Promise<CandidatePort[]> {
249
let tcp: string = '';
250
let tcp6: string = '';
251
try {
252
tcp = await fs.promises.readFile('/proc/net/tcp', 'utf8');
253
tcp6 = await fs.promises.readFile('/proc/net/tcp6', 'utf8');
254
} catch (e) {
255
// File reading error. No additional handling needed.
256
}
257
const connections: { socket: number; ip: string; port: number }[] = loadListeningPorts(tcp, tcp6);
258
259
const procSockets: string = await (new Promise(resolve => {
260
exec('ls -l /proc/[0-9]*/fd/[0-9]* | grep socket:', (error, stdout, stderr) => {
261
resolve(stdout);
262
});
263
}));
264
const socketMap = getSockets(procSockets);
265
266
const procChildren = await pfs.Promises.readdir('/proc');
267
const processes: {
268
pid: number; cwd: string; cmd: string;
269
}[] = [];
270
for (const childName of procChildren) {
271
try {
272
const pid: number = Number(childName);
273
const childUri = resources.joinPath(URI.file('/proc'), childName);
274
const childStat = await fs.promises.stat(childUri.fsPath);
275
if (childStat.isDirectory() && !isNaN(pid)) {
276
const cwd = await fs.promises.readlink(resources.joinPath(childUri, 'cwd').fsPath);
277
const cmd = await fs.promises.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8');
278
processes.push({ pid, cwd, cmd });
279
}
280
} catch (e) {
281
//
282
}
283
}
284
285
const unFoundConnections: { socket: number; ip: string; port: number }[] = [];
286
const filteredConnections = connections.filter((connection => {
287
const foundConnection = socketMap[connection.socket];
288
if (!foundConnection) {
289
unFoundConnections.push(connection);
290
}
291
return foundConnection;
292
}));
293
294
const foundPorts = findPorts(filteredConnections, socketMap, processes);
295
let heuristicPorts: CandidatePort[] | undefined;
296
this.logService.trace(`ForwardedPorts: (ExtHostTunnelService) number of possible root ports ${unFoundConnections.length}`);
297
if (unFoundConnections.length > 0) {
298
const rootProcesses: string = await (new Promise(resolve => {
299
exec('ps -F -A -l | grep root', (error, stdout, stderr) => {
300
resolve(stdout);
301
});
302
}));
303
this._foundRootPorts = tryFindRootPorts(unFoundConnections, rootProcesses, this._foundRootPorts);
304
heuristicPorts = Array.from(this._foundRootPorts.values());
305
this.logService.trace(`ForwardedPorts: (ExtHostTunnelService) heuristic ports ${heuristicPorts.map(heuristicPort => heuristicPort.port).join(', ')}`);
306
307
}
308
return foundPorts.then(foundCandidates => {
309
if (heuristicPorts) {
310
return foundCandidates.concat(heuristicPorts);
311
} else {
312
return foundCandidates;
313
}
314
});
315
}
316
317
private async defaultTunnelHost(): Promise<string> {
318
const settingValue = (await this.configurationService.getConfigProvider()).getConfiguration('remote').get('localPortHost');
319
return (!settingValue || settingValue === 'localhost') ? '127.0.0.1' : '0.0.0.0';
320
}
321
322
protected override makeManagedTunnelFactory(authority: vscode.ManagedResolvedAuthority): vscode.RemoteAuthorityResolver['tunnelFactory'] {
323
return async (tunnelOptions) => {
324
const t = new NodeRemoteTunnel(
325
{
326
commit: this.initData.commit,
327
quality: this.initData.quality,
328
logService: this.logService,
329
ipcLogger: null,
330
// services and address providers have stubs since we don't need
331
// the connection identification that the renderer process uses
332
remoteSocketFactoryService: {
333
_serviceBrand: undefined,
334
async connect(_connectTo: ManagedRemoteConnection, path: string, query: string, debugLabel: string): Promise<ISocket> {
335
const result = await authority.makeConnection();
336
return ExtHostManagedSocket.connect(result, path, query, debugLabel);
337
},
338
register() {
339
throw new Error('not implemented');
340
},
341
},
342
addressProvider: {
343
getAddress() {
344
return Promise.resolve({
345
connectTo: new ManagedRemoteConnection(0),
346
connectionToken: authority.connectionToken,
347
});
348
},
349
},
350
signService: this.signService,
351
},
352
await this.defaultTunnelHost(),
353
tunnelOptions.remoteAddress.host || 'localhost',
354
tunnelOptions.remoteAddress.port,
355
tunnelOptions.localAddressPort,
356
);
357
358
await t.waitForReady();
359
360
const disposeEmitter = new Emitter<void>();
361
362
return {
363
localAddress: parseAddress(t.localAddress) ?? t.localAddress,
364
remoteAddress: { port: t.tunnelRemotePort, host: t.tunnelRemoteHost },
365
onDidDispose: disposeEmitter.event,
366
dispose: () => {
367
t.dispose();
368
disposeEmitter.fire();
369
disposeEmitter.dispose();
370
},
371
};
372
};
373
}
374
}
375
376
class ExtHostManagedSocket extends ManagedSocket {
377
public static connect(
378
passing: vscode.ManagedMessagePassing,
379
path: string, query: string, debugLabel: string,
380
): Promise<ExtHostManagedSocket> {
381
const d = new DisposableStore();
382
const half: RemoteSocketHalf = {
383
onClose: d.add(new Emitter()),
384
onData: d.add(new Emitter()),
385
onEnd: d.add(new Emitter()),
386
};
387
388
d.add(passing.onDidReceiveMessage(d => half.onData.fire(VSBuffer.wrap(d))));
389
d.add(passing.onDidEnd(() => half.onEnd.fire()));
390
d.add(passing.onDidClose(error => half.onClose.fire({
391
type: SocketCloseEventType.NodeSocketCloseEvent,
392
error,
393
hadError: !!error
394
})));
395
396
const socket = new ExtHostManagedSocket(passing, debugLabel, half);
397
socket._register(d);
398
return connectManagedSocket(socket, path, query, debugLabel, half);
399
}
400
401
constructor(
402
private readonly passing: vscode.ManagedMessagePassing,
403
debugLabel: string,
404
half: RemoteSocketHalf,
405
) {
406
super(debugLabel, half);
407
}
408
409
public override write(buffer: VSBuffer): void {
410
this.passing.send(buffer.buffer);
411
}
412
protected override closeRemote(): void {
413
this.passing.end();
414
}
415
416
public override async drain(): Promise<void> {
417
await this.passing.drain?.();
418
}
419
}
420
421