@limrun/api 0.48.4 → 0.49.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/build-settings.d.mts.map +1 -1
- package/build-settings.d.ts.map +1 -1
- package/build-settings.js +4 -5
- package/build-settings.js.map +1 -1
- package/build-settings.mjs +4 -5
- package/build-settings.mjs.map +1 -1
- package/client.d.mts +8 -5
- package/client.d.mts.map +1 -1
- package/client.d.ts +8 -5
- package/client.d.ts.map +1 -1
- package/client.js +4 -4
- package/client.js.map +1 -1
- package/client.mjs +2 -2
- package/client.mjs.map +1 -1
- package/destination-tunnel-dialer.d.mts +30 -2
- package/destination-tunnel-dialer.d.mts.map +1 -1
- package/destination-tunnel-dialer.d.ts +30 -2
- package/destination-tunnel-dialer.d.ts.map +1 -1
- package/destination-tunnel-dialer.js +349 -65
- package/destination-tunnel-dialer.js.map +1 -1
- package/destination-tunnel-dialer.mjs +345 -64
- package/destination-tunnel-dialer.mjs.map +1 -1
- package/destination-tunnel-har.d.mts +23 -0
- package/destination-tunnel-har.d.mts.map +1 -0
- package/destination-tunnel-har.d.ts +23 -0
- package/destination-tunnel-har.d.ts.map +1 -0
- package/destination-tunnel-har.js +164 -0
- package/destination-tunnel-har.js.map +1 -0
- package/destination-tunnel-har.mjs +160 -0
- package/destination-tunnel-har.mjs.map +1 -0
- package/destination-tunnel-inspection.d.mts +71 -0
- package/destination-tunnel-inspection.d.mts.map +1 -0
- package/destination-tunnel-inspection.d.ts +71 -0
- package/destination-tunnel-inspection.d.ts.map +1 -0
- package/destination-tunnel-inspection.js +267 -0
- package/destination-tunnel-inspection.js.map +1 -0
- package/destination-tunnel-inspection.mjs +261 -0
- package/destination-tunnel-inspection.mjs.map +1 -0
- package/destination-tunnel.d.mts +119 -14
- package/destination-tunnel.d.mts.map +1 -1
- package/destination-tunnel.d.ts +119 -14
- package/destination-tunnel.d.ts.map +1 -1
- package/destination-tunnel.js +407 -98
- package/destination-tunnel.js.map +1 -1
- package/destination-tunnel.mjs +374 -73
- package/destination-tunnel.mjs.map +1 -1
- package/exec-client.d.mts +9 -0
- package/exec-client.d.mts.map +1 -1
- package/exec-client.d.ts +9 -0
- package/exec-client.d.ts.map +1 -1
- package/exec-client.js.map +1 -1
- package/exec-client.mjs.map +1 -1
- package/index.d.mts +8 -1
- package/index.d.mts.map +1 -1
- package/index.d.ts +8 -1
- package/index.d.ts.map +1 -1
- package/index.js +26 -2
- package/index.js.map +1 -1
- package/index.mjs +7 -1
- package/index.mjs.map +1 -1
- package/instance-client.d.mts +107 -1
- package/instance-client.d.mts.map +1 -1
- package/instance-client.d.ts +107 -1
- package/instance-client.d.ts.map +1 -1
- package/instance-client.js +83 -1
- package/instance-client.js.map +1 -1
- package/instance-client.mjs +83 -1
- package/instance-client.mjs.map +1 -1
- package/internal/destination-tunnel-management.d.mts +4 -3
- package/internal/destination-tunnel-management.d.mts.map +1 -1
- package/internal/destination-tunnel-management.d.ts +4 -3
- package/internal/destination-tunnel-management.d.ts.map +1 -1
- package/internal/destination-tunnel-management.js +64 -46
- package/internal/destination-tunnel-management.js.map +1 -1
- package/internal/destination-tunnel-management.mjs +55 -37
- package/internal/destination-tunnel-management.mjs.map +1 -1
- package/internal/destination-tunnel-url.d.mts +2 -0
- package/internal/destination-tunnel-url.d.mts.map +1 -1
- package/internal/destination-tunnel-url.d.ts +2 -0
- package/internal/destination-tunnel-url.d.ts.map +1 -1
- package/internal/destination-tunnel-url.js +45 -5
- package/internal/destination-tunnel-url.js.map +1 -1
- package/internal/destination-tunnel-url.mjs +44 -5
- package/internal/destination-tunnel-url.mjs.map +1 -1
- package/internal/destination-tunnel-wire-reader.d.mts +23 -0
- package/internal/destination-tunnel-wire-reader.d.mts.map +1 -0
- package/internal/destination-tunnel-wire-reader.d.ts +23 -0
- package/internal/destination-tunnel-wire-reader.d.ts.map +1 -0
- package/internal/destination-tunnel-wire-reader.js +110 -0
- package/internal/destination-tunnel-wire-reader.js.map +1 -0
- package/internal/destination-tunnel-wire-reader.mjs +93 -0
- package/internal/destination-tunnel-wire-reader.mjs.map +1 -0
- package/internal/persist-option.d.mts +14 -0
- package/internal/persist-option.d.mts.map +1 -0
- package/internal/persist-option.d.ts +14 -0
- package/internal/persist-option.d.ts.map +1 -0
- package/internal/persist-option.js +18 -0
- package/internal/persist-option.js.map +1 -0
- package/internal/persist-option.mjs +15 -0
- package/internal/persist-option.mjs.map +1 -0
- package/internal/proxy-transport.d.mts +27 -0
- package/internal/proxy-transport.d.mts.map +1 -1
- package/internal/proxy-transport.d.ts +27 -0
- package/internal/proxy-transport.d.ts.map +1 -1
- package/internal/proxy-transport.js +206 -0
- package/internal/proxy-transport.js.map +1 -1
- package/internal/proxy-transport.mjs +205 -0
- package/internal/proxy-transport.mjs.map +1 -1
- package/internal/session-stream.d.mts +39 -0
- package/internal/session-stream.d.mts.map +1 -0
- package/internal/session-stream.d.ts +39 -0
- package/internal/session-stream.d.ts.map +1 -0
- package/internal/session-stream.js +144 -0
- package/internal/session-stream.js.map +1 -0
- package/internal/session-stream.mjs +139 -0
- package/internal/session-stream.mjs.map +1 -0
- package/internal/tslib.js +4 -4
- package/ios-client.d.mts +63 -4
- package/ios-client.d.mts.map +1 -1
- package/ios-client.d.ts +63 -4
- package/ios-client.d.ts.map +1 -1
- package/ios-client.js +48 -2
- package/ios-client.js.map +1 -1
- package/ios-client.mjs +48 -2
- package/ios-client.mjs.map +1 -1
- package/package.json +22 -1
- package/resources/android-instances-helpers.d.mts +25 -0
- package/resources/android-instances-helpers.d.mts.map +1 -0
- package/resources/android-instances-helpers.d.ts +25 -0
- package/resources/android-instances-helpers.d.ts.map +1 -0
- package/resources/android-instances-helpers.js +37 -0
- package/resources/android-instances-helpers.js.map +1 -0
- package/resources/android-instances-helpers.mjs +33 -0
- package/resources/android-instances-helpers.mjs.map +1 -0
- package/resources/index.d.mts +6 -3
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +6 -3
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js +6 -5
- package/resources/index.js.map +1 -1
- package/resources/index.mjs +3 -3
- package/resources/index.mjs.map +1 -1
- package/resources/ios-instances-helpers.d.mts +25 -0
- package/resources/ios-instances-helpers.d.mts.map +1 -0
- package/resources/ios-instances-helpers.d.ts +25 -0
- package/resources/ios-instances-helpers.d.ts.map +1 -0
- package/resources/ios-instances-helpers.js +37 -0
- package/resources/ios-instances-helpers.js.map +1 -0
- package/resources/ios-instances-helpers.mjs +33 -0
- package/resources/ios-instances-helpers.mjs.map +1 -0
- package/resources/session-artifacts.d.mts +70 -0
- package/resources/session-artifacts.d.mts.map +1 -0
- package/resources/session-artifacts.d.ts +70 -0
- package/resources/session-artifacts.d.ts.map +1 -0
- package/resources/session-artifacts.js +8 -0
- package/resources/session-artifacts.js.map +1 -0
- package/resources/session-artifacts.mjs +7 -0
- package/resources/session-artifacts.mjs.map +1 -0
- package/resources/xcode-instances-helpers.d.mts +55 -0
- package/resources/xcode-instances-helpers.d.mts.map +1 -1
- package/resources/xcode-instances-helpers.d.ts +55 -0
- package/resources/xcode-instances-helpers.d.ts.map +1 -1
- package/resources/xcode-instances-helpers.js +49 -3
- package/resources/xcode-instances-helpers.js.map +1 -1
- package/resources/xcode-instances-helpers.mjs +47 -2
- package/resources/xcode-instances-helpers.mjs.map +1 -1
- package/src/build-settings.ts +4 -5
- package/src/client.ts +12 -2
- package/src/destination-tunnel-dialer.ts +403 -64
- package/src/destination-tunnel-har.ts +200 -0
- package/src/destination-tunnel-inspection.ts +367 -0
- package/src/destination-tunnel.ts +510 -88
- package/src/exec-client.ts +9 -0
- package/src/index.ts +58 -0
- package/src/instance-client.ts +273 -5
- package/src/internal/destination-tunnel-management.ts +67 -43
- package/src/internal/destination-tunnel-url.ts +61 -5
- package/src/internal/destination-tunnel-wire-reader.ts +109 -0
- package/src/internal/persist-option.ts +26 -0
- package/src/internal/proxy-transport.ts +248 -0
- package/src/internal/session-stream.ts +157 -0
- package/src/ios-client.ts +129 -8
- package/src/resources/android-instances-helpers.ts +45 -0
- package/src/resources/index.ts +14 -2
- package/src/resources/ios-instances-helpers.ts +45 -0
- package/src/resources/session-artifacts.ts +95 -0
- package/src/resources/xcode-instances-helpers.ts +116 -5
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
|
@@ -1,15 +1,47 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DESTINATION_TUNNEL_SERVER_ERROR_PREFIX = void 0;
|
|
4
|
+
exports.isTerminalDestinationTunnelError = isTerminalDestinationTunnelError;
|
|
3
5
|
exports.startDestinationTcpTunnel = startDestinationTcpTunnel;
|
|
6
|
+
exports.isDialableResolvedAddress = isDialableResolvedAddress;
|
|
4
7
|
exports.classifyOpenFailure = classifyOpenFailure;
|
|
5
8
|
const tslib_1 = require("./internal/tslib.js");
|
|
9
|
+
const dns_1 = tslib_1.__importDefault(require("dns"));
|
|
6
10
|
const net_1 = tslib_1.__importDefault(require("net"));
|
|
11
|
+
const tls_1 = tslib_1.__importDefault(require("tls"));
|
|
7
12
|
const ws_1 = require("ws");
|
|
8
13
|
const proxy_transport_1 = require("./internal/proxy-transport.js");
|
|
14
|
+
const destination_tunnel_wire_reader_1 = require("./internal/destination-tunnel-wire-reader.js");
|
|
15
|
+
const destination_tunnel_inspection_1 = require("./destination-tunnel-inspection.js");
|
|
9
16
|
const destination_tunnel_1 = require("./destination-tunnel.js");
|
|
17
|
+
const blockedResolvedAddresses = new net_1.default.BlockList();
|
|
18
|
+
blockedResolvedAddresses.addSubnet('0.0.0.0', 8, 'ipv4');
|
|
19
|
+
blockedResolvedAddresses.addSubnet('127.0.0.0', 8, 'ipv4');
|
|
20
|
+
blockedResolvedAddresses.addSubnet('169.254.0.0', 16, 'ipv4');
|
|
21
|
+
blockedResolvedAddresses.addSubnet('224.0.0.0', 3, 'ipv4');
|
|
22
|
+
blockedResolvedAddresses.addSubnet('::', 96, 'ipv6');
|
|
23
|
+
blockedResolvedAddresses.addSubnet('fe80::', 10, 'ipv6');
|
|
24
|
+
blockedResolvedAddresses.addSubnet('ff00::', 8, 'ipv6');
|
|
25
|
+
blockedResolvedAddresses.addSubnet('::ffff:0.0.0.0', 104, 'ipv6');
|
|
26
|
+
blockedResolvedAddresses.addSubnet('::ffff:127.0.0.0', 104, 'ipv6');
|
|
27
|
+
blockedResolvedAddresses.addSubnet('::ffff:169.254.0.0', 112, 'ipv6');
|
|
28
|
+
blockedResolvedAddresses.addSubnet('::ffff:224.0.0.0', 99, 'ipv6');
|
|
29
|
+
/** Thrown (message prefix) when the server terminates the session with `error`. */
|
|
30
|
+
exports.DESTINATION_TUNNEL_SERVER_ERROR_PREFIX = 'destination tunnel failed: ';
|
|
31
|
+
/**
|
|
32
|
+
* True when the failure is a terminal protocol/policy rejection from the
|
|
33
|
+
* server rather than a transient transport problem. Reconnect supervisors
|
|
34
|
+
* must not retry terminal failures.
|
|
35
|
+
*/
|
|
36
|
+
function isTerminalDestinationTunnelError(error) {
|
|
37
|
+
return error instanceof Error && error.message.startsWith(exports.DESTINATION_TUNNEL_SERVER_ERROR_PREFIX);
|
|
38
|
+
}
|
|
10
39
|
async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
11
|
-
const
|
|
40
|
+
const selectors = (0, destination_tunnel_1.validateDestinationTunnelSelectors)(options.selectors);
|
|
41
|
+
const routes = (0, destination_tunnel_1.classifyDestinationTunnelSelectors)(selectors).routes ?? [];
|
|
42
|
+
const inspection = (0, destination_tunnel_1.normalizeDestinationTunnelInspection)(options.inspection ?? (0, destination_tunnel_1.disabledDestinationTunnelInspection)());
|
|
12
43
|
const logLevel = options.logLevel ?? 'info';
|
|
44
|
+
const creditWindow = positiveInteger(options.window ?? destination_tunnel_1.DESTINATION_TUNNEL_DEFAULT_WINDOW, 'window');
|
|
13
45
|
const maxConnections = positiveInteger(options.maxConnections ?? 64, 'maxConnections');
|
|
14
46
|
const maxPendingBytesPerConnection = positiveInteger(options.maxPendingBytesPerConnection ?? 16 * 1024 * 1024, 'maxPendingBytesPerConnection');
|
|
15
47
|
const maxTotalPendingBytes = positiveInteger(options.maxTotalPendingBytes ?? 16 * 1024 * 1024, 'maxTotalPendingBytes');
|
|
@@ -56,6 +88,7 @@ async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
56
88
|
let livenessTimer;
|
|
57
89
|
let tunnelReady = false;
|
|
58
90
|
let closed = false;
|
|
91
|
+
let inspectionStream;
|
|
59
92
|
let pausedForBackpressure = false;
|
|
60
93
|
let totalPendingWriteBytes = 0;
|
|
61
94
|
const updateConnectionState = (state) => {
|
|
@@ -89,9 +122,10 @@ async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
89
122
|
if (!connection)
|
|
90
123
|
return;
|
|
91
124
|
connections.delete(connId);
|
|
125
|
+
connection.abortController.abort();
|
|
92
126
|
clearTimeout(connection.connectTimer);
|
|
93
127
|
markRecentlyClosed(connId);
|
|
94
|
-
if (destroySocket && !connection.socket.destroyed) {
|
|
128
|
+
if (destroySocket && connection.socket && !connection.socket.destroyed) {
|
|
95
129
|
connection.socket.destroy();
|
|
96
130
|
}
|
|
97
131
|
};
|
|
@@ -122,6 +156,8 @@ async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
122
156
|
livenessTimer = undefined;
|
|
123
157
|
}
|
|
124
158
|
closeAllConnections();
|
|
159
|
+
inspectionStream?.close();
|
|
160
|
+
inspectionStream = undefined;
|
|
125
161
|
ws.removeAllListeners('open');
|
|
126
162
|
ws.removeAllListeners('message');
|
|
127
163
|
ws.removeAllListeners('ping');
|
|
@@ -140,12 +176,13 @@ async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
140
176
|
reject(error);
|
|
141
177
|
}
|
|
142
178
|
};
|
|
179
|
+
const mayReadFromSocket = (connection) => !pausedForBackpressure && connection.creditQueueBytes === 0;
|
|
143
180
|
const pauseForBackpressure = () => {
|
|
144
181
|
if (pausedForBackpressure)
|
|
145
182
|
return;
|
|
146
183
|
pausedForBackpressure = true;
|
|
147
184
|
for (const connection of connections.values()) {
|
|
148
|
-
if (!connection.socket.destroyed)
|
|
185
|
+
if (connection.socket && !connection.socket.destroyed)
|
|
149
186
|
connection.socket.pause();
|
|
150
187
|
}
|
|
151
188
|
};
|
|
@@ -154,8 +191,12 @@ async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
154
191
|
return;
|
|
155
192
|
pausedForBackpressure = false;
|
|
156
193
|
for (const connection of connections.values()) {
|
|
157
|
-
|
|
194
|
+
// Flows still waiting on per-flow credit stay paused; they resume
|
|
195
|
+
// when the server grants more window, keeping one stalled flow from
|
|
196
|
+
// holding every other flow back.
|
|
197
|
+
if (connection.socket && !connection.socket.destroyed && mayReadFromSocket(connection)) {
|
|
158
198
|
connection.socket.resume();
|
|
199
|
+
}
|
|
159
200
|
}
|
|
160
201
|
};
|
|
161
202
|
const frameQueued = () => {
|
|
@@ -207,12 +248,110 @@ async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
207
248
|
...(osCode ? { osCode } : {}),
|
|
208
249
|
});
|
|
209
250
|
};
|
|
251
|
+
/**
|
|
252
|
+
* Push local socket bytes toward the server through the per-flow credit
|
|
253
|
+
* gate. Chunks beyond the current credit wait in a FIFO queue with the
|
|
254
|
+
* local socket paused, so a server-throttled flow stops reading instead
|
|
255
|
+
* of buffering unboundedly.
|
|
256
|
+
*/
|
|
257
|
+
const pushThroughCreditGate = (connId, connection, payload) => {
|
|
258
|
+
if (payload && payload.length > 0) {
|
|
259
|
+
connection.creditQueue.push(payload);
|
|
260
|
+
connection.creditQueueBytes += payload.length;
|
|
261
|
+
}
|
|
262
|
+
while (connection.creditQueue.length > 0 && connection.sendCredit > 0) {
|
|
263
|
+
const chunk = connection.creditQueue[0];
|
|
264
|
+
if (chunk.length <= connection.sendCredit) {
|
|
265
|
+
connection.creditQueue.shift();
|
|
266
|
+
connection.creditQueueBytes -= chunk.length;
|
|
267
|
+
connection.sendCredit -= chunk.length;
|
|
268
|
+
sendData(connId, chunk);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
const portion = chunk.subarray(0, connection.sendCredit);
|
|
272
|
+
connection.creditQueue[0] = chunk.subarray(connection.sendCredit);
|
|
273
|
+
connection.creditQueueBytes -= portion.length;
|
|
274
|
+
connection.sendCredit = 0;
|
|
275
|
+
sendData(connId, portion);
|
|
276
|
+
}
|
|
277
|
+
if (closed)
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (connection.creditQueue.length === 0 && connection.finPending) {
|
|
281
|
+
// The local socket ended while data was still waiting on credit; the
|
|
282
|
+
// queue has drained, so half-close can now be signaled in order.
|
|
283
|
+
connection.finPending = false;
|
|
284
|
+
connection.localInputEnded = true;
|
|
285
|
+
sendControl({ type: 'fin', connId });
|
|
286
|
+
}
|
|
287
|
+
if (!connection.socket || connection.socket.destroyed)
|
|
288
|
+
return;
|
|
289
|
+
if (connection.creditQueueBytes > 0) {
|
|
290
|
+
connection.socket.pause();
|
|
291
|
+
}
|
|
292
|
+
else if (!pausedForBackpressure) {
|
|
293
|
+
connection.socket.resume();
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
const dialTarget = async (message) => {
|
|
297
|
+
// Domain selectors resolve through the user's OS resolver and may only
|
|
298
|
+
// reach ordinary unicast targets. Loopback and similar special targets
|
|
299
|
+
// require an explicit exact route grant.
|
|
300
|
+
const startedAt = Date.now();
|
|
301
|
+
const results = await dns_1.default.promises.lookup(message.host, { all: true, verbatim: true });
|
|
302
|
+
const dnsMs = Date.now() - startedAt;
|
|
303
|
+
for (const result of results) {
|
|
304
|
+
if (isDialableResolvedAddress(result.address, message.port, routes)) {
|
|
305
|
+
return { host: result.address, dnsMs };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const blockedError = new Error(`all resolved addresses for ${message.host} are blocked`);
|
|
309
|
+
blockedError.code = 'EBLOCKED';
|
|
310
|
+
throw blockedError;
|
|
311
|
+
};
|
|
312
|
+
const wrapTls = (socket, transport, abortController) => new Promise((resolveTls, rejectTls) => {
|
|
313
|
+
const tlsSocket = tls_1.default.connect({
|
|
314
|
+
socket,
|
|
315
|
+
rejectUnauthorized: true,
|
|
316
|
+
servername: transport.serverName,
|
|
317
|
+
ALPNProtocols: transport.alpnProtocols,
|
|
318
|
+
});
|
|
319
|
+
tlsSocket.allowHalfOpen = true;
|
|
320
|
+
const cleanup = () => {
|
|
321
|
+
tlsSocket.removeListener('secureConnect', onSecureConnect);
|
|
322
|
+
tlsSocket.removeListener('error', onError);
|
|
323
|
+
abortController.signal.removeEventListener('abort', onAbort);
|
|
324
|
+
};
|
|
325
|
+
const onSecureConnect = () => {
|
|
326
|
+
cleanup();
|
|
327
|
+
tlsSocket.pause();
|
|
328
|
+
resolveTls(tlsSocket);
|
|
329
|
+
};
|
|
330
|
+
const onError = (error) => {
|
|
331
|
+
cleanup();
|
|
332
|
+
rejectTls(error);
|
|
333
|
+
};
|
|
334
|
+
const onAbort = () => {
|
|
335
|
+
cleanup();
|
|
336
|
+
tlsSocket.destroy();
|
|
337
|
+
rejectTls(Object.assign(new Error('TLS connection cancelled'), { code: 'ECANCELED' }));
|
|
338
|
+
};
|
|
339
|
+
tlsSocket.once('secureConnect', onSecureConnect);
|
|
340
|
+
tlsSocket.once('error', onError);
|
|
341
|
+
if (abortController.signal.aborted) {
|
|
342
|
+
onAbort();
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
abortController.signal.addEventListener('abort', onAbort, { once: true });
|
|
346
|
+
}
|
|
347
|
+
});
|
|
210
348
|
const handleOpen = (message) => {
|
|
349
|
+
let kind;
|
|
211
350
|
try {
|
|
212
|
-
(0, destination_tunnel_1.assertDestinationTunnelOpenAllowed)(message,
|
|
351
|
+
kind = (0, destination_tunnel_1.assertDestinationTunnelOpenAllowed)(message, selectors);
|
|
213
352
|
}
|
|
214
353
|
catch {
|
|
215
|
-
sendOpenFailure(message.connId, '
|
|
354
|
+
sendOpenFailure(message.connId, 'selector_not_allowed');
|
|
216
355
|
return;
|
|
217
356
|
}
|
|
218
357
|
if (connections.has(message.connId) || recentlyClosed.has(message.connId)) {
|
|
@@ -222,75 +361,126 @@ async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
222
361
|
sendOpenFailure(message.connId, 'resource_exhausted');
|
|
223
362
|
return;
|
|
224
363
|
}
|
|
225
|
-
const
|
|
226
|
-
host: message.host,
|
|
227
|
-
port: message.port,
|
|
228
|
-
allowHalfOpen: true,
|
|
229
|
-
});
|
|
364
|
+
const abortController = new AbortController();
|
|
230
365
|
const connectTimer = setTimeout(() => {
|
|
231
366
|
if (connections.get(message.connId)?.phase !== 'connecting')
|
|
232
367
|
return;
|
|
368
|
+
logger.warn(`Failed to connect to ${message.host}:${message.port}: timed out`);
|
|
233
369
|
sendOpenFailure(message.connId, 'connection_timed_out', 'ETIMEDOUT');
|
|
234
370
|
removeConnection(message.connId, true);
|
|
235
371
|
}, connectTimeoutMs);
|
|
236
372
|
connectTimer.unref();
|
|
237
373
|
const connection = {
|
|
238
|
-
socket,
|
|
239
374
|
phase: 'connecting',
|
|
375
|
+
abortController,
|
|
240
376
|
connectTimer,
|
|
241
377
|
localInputEnded: false,
|
|
242
378
|
remoteInputEnded: false,
|
|
243
379
|
pendingWriteBytes: 0,
|
|
380
|
+
sendCredit: message.window,
|
|
381
|
+
creditQueue: [],
|
|
382
|
+
creditQueueBytes: 0,
|
|
383
|
+
finPending: false,
|
|
384
|
+
deliveredSinceUpdate: 0,
|
|
244
385
|
};
|
|
245
386
|
connections.set(message.connId, connection);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
sendControl({ type: 'openOk', connId: message.connId });
|
|
255
|
-
logger.debug(`Connected ${message.connId} to ${message.host}:${message.port}`);
|
|
256
|
-
});
|
|
257
|
-
socket.on('data', (payload) => {
|
|
258
|
-
if (connections.get(message.connId) !== connection || connection.phase !== 'open')
|
|
259
|
-
return;
|
|
260
|
-
sendData(message.connId, payload);
|
|
261
|
-
});
|
|
262
|
-
socket.once('end', () => {
|
|
263
|
-
if (connections.get(message.connId) !== connection || connection.phase !== 'open')
|
|
387
|
+
void (async () => {
|
|
388
|
+
let host = message.host;
|
|
389
|
+
if (kind === 'domain') {
|
|
390
|
+
const resolved = await dialTarget(message);
|
|
391
|
+
host = resolved.host;
|
|
392
|
+
connection.dnsMs = resolved.dnsMs;
|
|
393
|
+
}
|
|
394
|
+
if (connections.get(message.connId) !== connection || closed)
|
|
264
395
|
return;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
396
|
+
const tcp = await proxy_transport_1.nodeProxyTransport.connectTcp({
|
|
397
|
+
host,
|
|
398
|
+
port: message.port,
|
|
399
|
+
proxyLookupHost: message.host,
|
|
400
|
+
proxyLookupProtocol: message.transport.type === 'tls' ? 'https:' : 'http:',
|
|
401
|
+
timeoutMs: connectTimeoutMs,
|
|
402
|
+
signal: abortController.signal,
|
|
403
|
+
});
|
|
404
|
+
if (connections.get(message.connId) !== connection || closed) {
|
|
405
|
+
tcp.socket.destroy();
|
|
270
406
|
return;
|
|
271
|
-
if (connection.phase === 'connecting') {
|
|
272
|
-
sendOpenFailure(message.connId, classifyOpenFailure(error), error.code);
|
|
273
407
|
}
|
|
274
|
-
|
|
275
|
-
|
|
408
|
+
connection.socket = tcp.socket;
|
|
409
|
+
let socket = tcp.socket;
|
|
410
|
+
let tlsMs;
|
|
411
|
+
if (message.transport.type === 'tls') {
|
|
412
|
+
const tlsStartedAt = Date.now();
|
|
413
|
+
socket = await wrapTls(tcp.socket, message.transport, abortController);
|
|
414
|
+
tlsMs = Date.now() - tlsStartedAt;
|
|
415
|
+
if (connections.get(message.connId) !== connection || closed) {
|
|
416
|
+
socket.destroy();
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
connection.socket = socket;
|
|
276
420
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
421
|
+
clearTimeout(connectTimer);
|
|
422
|
+
socket.on('data', (payload) => {
|
|
423
|
+
if (connections.get(message.connId) !== connection || connection.phase !== 'open')
|
|
424
|
+
return;
|
|
425
|
+
pushThroughCreditGate(message.connId, connection, payload);
|
|
426
|
+
});
|
|
427
|
+
socket.once('end', () => {
|
|
428
|
+
if (connections.get(message.connId) !== connection || connection.phase !== 'open')
|
|
429
|
+
return;
|
|
430
|
+
if (connection.creditQueueBytes > 0) {
|
|
431
|
+
// Queued bytes are still waiting on send credit; fin must follow
|
|
432
|
+
// them, so defer it until the credit gate drains the queue.
|
|
433
|
+
connection.finPending = true;
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
connection.localInputEnded = true;
|
|
437
|
+
sendControl({ type: 'fin', connId: message.connId });
|
|
438
|
+
});
|
|
439
|
+
socket.once('error', (error) => {
|
|
440
|
+
if (connections.get(message.connId) !== connection)
|
|
441
|
+
return;
|
|
442
|
+
sendReset(message.connId, 'connection_error', error.code);
|
|
443
|
+
removeConnection(message.connId, true);
|
|
444
|
+
});
|
|
445
|
+
socket.once('close', () => {
|
|
446
|
+
if (connections.get(message.connId) !== connection)
|
|
447
|
+
return;
|
|
448
|
+
if (!connection.localInputEnded || !connection.remoteInputEnded) {
|
|
449
|
+
sendReset(message.connId, 'connection_error');
|
|
450
|
+
}
|
|
451
|
+
removeConnection(message.connId, false);
|
|
452
|
+
});
|
|
453
|
+
connection.phase = 'open';
|
|
454
|
+
sendControl({
|
|
455
|
+
type: 'openOk',
|
|
456
|
+
connId: message.connId,
|
|
457
|
+
transport: {
|
|
458
|
+
type: message.transport.type,
|
|
459
|
+
...(tcp.remoteAddress === undefined ? {} : { remoteAddress: tcp.remoteAddress }),
|
|
460
|
+
...(connection.dnsMs === undefined ? {} : { dnsMs: connection.dnsMs }),
|
|
461
|
+
connectMs: tcp.connectMs,
|
|
462
|
+
...(tlsMs === undefined ? {} : { tlsMs }),
|
|
463
|
+
...(socket instanceof tls_1.default.TLSSocket && socket.alpnProtocol ?
|
|
464
|
+
{ alpnProtocol: socket.alpnProtocol }
|
|
465
|
+
: {}),
|
|
466
|
+
},
|
|
467
|
+
window: creditWindow,
|
|
468
|
+
});
|
|
469
|
+
logger.debug(`Forwarding connection ${message.connId} to ${message.host}:${message.port}`);
|
|
470
|
+
if (!pausedForBackpressure && connection.creditQueueBytes === 0)
|
|
471
|
+
socket.resume();
|
|
472
|
+
})().catch((error) => {
|
|
280
473
|
if (connections.get(message.connId) !== connection)
|
|
281
474
|
return;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
sendReset(message.connId, 'connection_error');
|
|
287
|
-
}
|
|
288
|
-
removeConnection(message.connId, false);
|
|
475
|
+
logger.warn(`Failed to connect to ${message.host}:${message.port}: ${error.code ?? error.message}`);
|
|
476
|
+
const reason = error.code === 'EBLOCKED' ? 'selector_not_allowed' : classifyOpenFailure(error);
|
|
477
|
+
sendOpenFailure(message.connId, reason, error.code === 'EBLOCKED' ? undefined : error.code);
|
|
478
|
+
removeConnection(message.connId, true);
|
|
289
479
|
});
|
|
290
480
|
};
|
|
291
481
|
const findOpenConnection = (connId) => {
|
|
292
482
|
const connection = connections.get(connId);
|
|
293
|
-
if (connection?.phase === 'open')
|
|
483
|
+
if (connection?.phase === 'open' && connection.socket)
|
|
294
484
|
return connection;
|
|
295
485
|
if (connection) {
|
|
296
486
|
sendReset(connId, 'protocol_error');
|
|
@@ -319,6 +509,14 @@ async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
319
509
|
removeConnection(connId, true);
|
|
320
510
|
}
|
|
321
511
|
};
|
|
512
|
+
const handleRemoteWindowUpdate = (connId, increment) => {
|
|
513
|
+
const connection = connections.get(connId);
|
|
514
|
+
// Updates racing a local close are expected; ignore unknown flows.
|
|
515
|
+
if (!connection || connection.phase !== 'open')
|
|
516
|
+
return;
|
|
517
|
+
connection.sendCredit += increment;
|
|
518
|
+
pushThroughCreditGate(connId, connection);
|
|
519
|
+
};
|
|
322
520
|
const handleBinary = (frame) => {
|
|
323
521
|
if (!tunnelReady) {
|
|
324
522
|
throw new destination_tunnel_1.DestinationTunnelProtocolError('received tunnel data before READY');
|
|
@@ -349,15 +547,32 @@ async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
349
547
|
const osCode = error.code;
|
|
350
548
|
sendReset(connId, 'connection_error', osCode);
|
|
351
549
|
removeConnection(connId, true);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
// Replenish the server's send window once the local socket accepted
|
|
553
|
+
// the bytes, batching updates to roughly half the window.
|
|
554
|
+
if (connections.get(connId) !== connection)
|
|
555
|
+
return;
|
|
556
|
+
connection.deliveredSinceUpdate += payload.length;
|
|
557
|
+
if (connection.deliveredSinceUpdate >= Math.ceil(creditWindow / 2)) {
|
|
558
|
+
const increment = connection.deliveredSinceUpdate;
|
|
559
|
+
connection.deliveredSinceUpdate = 0;
|
|
560
|
+
if (!closed && ws.readyState === ws_1.WebSocket.OPEN) {
|
|
561
|
+
sendControl({ type: 'windowUpdate', connId, increment });
|
|
562
|
+
}
|
|
352
563
|
}
|
|
353
564
|
});
|
|
354
565
|
};
|
|
355
566
|
const handleControl = (message) => {
|
|
356
567
|
switch (message.type) {
|
|
357
|
-
case 'ready':
|
|
568
|
+
case 'ready': {
|
|
358
569
|
if (tunnelReady)
|
|
359
570
|
throw new destination_tunnel_1.DestinationTunnelProtocolError('received duplicate READY');
|
|
360
571
|
(0, destination_tunnel_1.assertDestinationTunnelReady)(message);
|
|
572
|
+
const expected = (0, destination_tunnel_1.destinationTunnelConfigHash)(selectors, inspection);
|
|
573
|
+
if (message.configHash !== expected) {
|
|
574
|
+
throw new destination_tunnel_1.DestinationTunnelProtocolError(`server acknowledged config ${message.configHash} but ${expected} was negotiated`);
|
|
575
|
+
}
|
|
361
576
|
tunnelReady = true;
|
|
362
577
|
if (handshakeTimer) {
|
|
363
578
|
clearTimeout(handshakeTimer);
|
|
@@ -365,16 +580,37 @@ async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
365
580
|
}
|
|
366
581
|
armLivenessDeadline();
|
|
367
582
|
updateConnectionState('connected');
|
|
368
|
-
logger.info(`Destination tunnel ready with ${
|
|
583
|
+
logger.info(`Destination tunnel ready with ${selectors.length} selector(s)`);
|
|
584
|
+
if (inspection.enabled) {
|
|
585
|
+
try {
|
|
586
|
+
inspectionStream = (0, destination_tunnel_inspection_1.startDestinationTunnelInspectionStream)(remoteURL, message.tunnelId, token, {
|
|
587
|
+
...(options.onInspectionEvent ? { onEvent: options.onInspectionEvent } : {}),
|
|
588
|
+
...(options.onInspectionError ? { onError: options.onInspectionError } : {}),
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
try {
|
|
593
|
+
options.onInspectionError?.(error instanceof Error ? error : new Error(String(error)));
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
// Inspection callbacks are isolated from the main tunnel.
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
369
600
|
resolve({
|
|
370
601
|
tunnelId: message.tunnelId,
|
|
602
|
+
selectors: message.selectors,
|
|
603
|
+
configHash: message.configHash,
|
|
604
|
+
inspection,
|
|
605
|
+
...(inspectionStream ? { inspectionStream } : {}),
|
|
371
606
|
close,
|
|
372
607
|
getConnectionState,
|
|
373
608
|
onConnectionStateChange,
|
|
374
609
|
});
|
|
375
610
|
return;
|
|
611
|
+
}
|
|
376
612
|
case 'error':
|
|
377
|
-
throw new Error(
|
|
613
|
+
throw new Error(`${exports.DESTINATION_TUNNEL_SERVER_ERROR_PREFIX}${message.code}`);
|
|
378
614
|
case 'open':
|
|
379
615
|
if (!tunnelReady)
|
|
380
616
|
throw new destination_tunnel_1.DestinationTunnelProtocolError('received OPEN before READY');
|
|
@@ -385,6 +621,11 @@ async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
385
621
|
throw new destination_tunnel_1.DestinationTunnelProtocolError('received FIN before READY');
|
|
386
622
|
handleRemoteFIN(message.connId);
|
|
387
623
|
return;
|
|
624
|
+
case 'windowUpdate':
|
|
625
|
+
if (!tunnelReady)
|
|
626
|
+
throw new destination_tunnel_1.DestinationTunnelProtocolError('received windowUpdate before READY');
|
|
627
|
+
handleRemoteWindowUpdate(message.connId, message.increment);
|
|
628
|
+
return;
|
|
388
629
|
case 'reset':
|
|
389
630
|
if (!tunnelReady)
|
|
390
631
|
throw new destination_tunnel_1.DestinationTunnelProtocolError('received RESET before READY');
|
|
@@ -406,7 +647,13 @@ async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
406
647
|
}, handshakeTimeoutMs);
|
|
407
648
|
handshakeTimer.unref();
|
|
408
649
|
ws.on('open', () => {
|
|
409
|
-
sendControl({
|
|
650
|
+
sendControl({
|
|
651
|
+
type: 'start',
|
|
652
|
+
version: destination_tunnel_1.DESTINATION_TUNNEL_VERSION,
|
|
653
|
+
selectors,
|
|
654
|
+
inspection,
|
|
655
|
+
window: creditWindow,
|
|
656
|
+
});
|
|
410
657
|
pingInterval = setInterval(() => {
|
|
411
658
|
if (ws.readyState === ws_1.WebSocket.OPEN) {
|
|
412
659
|
ws.ping(undefined, true, frameSent);
|
|
@@ -419,10 +666,10 @@ async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
419
666
|
try {
|
|
420
667
|
armLivenessDeadline();
|
|
421
668
|
if (isBinary) {
|
|
422
|
-
handleBinary(toBuffer(data));
|
|
669
|
+
handleBinary((0, destination_tunnel_wire_reader_1.toBuffer)(data));
|
|
423
670
|
}
|
|
424
671
|
else {
|
|
425
|
-
handleControl((0, destination_tunnel_1.decodeDestinationTunnelServerMessage)(JSON.parse(toBuffer(data).toString('utf8'))));
|
|
672
|
+
handleControl((0, destination_tunnel_1.decodeDestinationTunnelServerMessage)(JSON.parse((0, destination_tunnel_wire_reader_1.toBuffer)(data).toString('utf8'))));
|
|
426
673
|
}
|
|
427
674
|
}
|
|
428
675
|
catch (error) {
|
|
@@ -444,6 +691,28 @@ async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
444
691
|
});
|
|
445
692
|
});
|
|
446
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Whether a resolved domain address may be dialed. Ordinary unicast targets
|
|
696
|
+
* (including private ranges the user's machine can reach) are allowed;
|
|
697
|
+
* loopback, link-local, multicast, unspecified, broadcast, and similar
|
|
698
|
+
* special targets are blocked unless an exact route grants them.
|
|
699
|
+
*/
|
|
700
|
+
function isDialableResolvedAddress(address, port, routes) {
|
|
701
|
+
for (const route of routes) {
|
|
702
|
+
if (route.port !== port)
|
|
703
|
+
continue;
|
|
704
|
+
if (route.host === address)
|
|
705
|
+
return true;
|
|
706
|
+
if (route.host === 'localhost' && (address === '127.0.0.1' || address === '::1'))
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
const version = net_1.default.isIP(address);
|
|
710
|
+
if (version === 4)
|
|
711
|
+
return !blockedResolvedAddresses.check(address, 'ipv4');
|
|
712
|
+
if (version === 6)
|
|
713
|
+
return !blockedResolvedAddresses.check(address, 'ipv6');
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
447
716
|
function classifyOpenFailure(error) {
|
|
448
717
|
switch (error.code) {
|
|
449
718
|
case 'ENOTFOUND':
|
|
@@ -461,6 +730,7 @@ function classifyOpenFailure(error) {
|
|
|
461
730
|
return 'unreachable';
|
|
462
731
|
case 'EACCES':
|
|
463
732
|
case 'EPERM':
|
|
733
|
+
case 'EPROXYAUTH':
|
|
464
734
|
return 'permission_denied';
|
|
465
735
|
case 'EMFILE':
|
|
466
736
|
case 'ENFILE':
|
|
@@ -469,7 +739,30 @@ function classifyOpenFailure(error) {
|
|
|
469
739
|
return 'resource_exhausted';
|
|
470
740
|
case 'ECANCELED':
|
|
471
741
|
return 'cancelled';
|
|
742
|
+
case 'CERT_HAS_EXPIRED':
|
|
743
|
+
case 'CERT_NOT_YET_VALID':
|
|
744
|
+
case 'CERT_REJECTED':
|
|
745
|
+
case 'CERT_REVOKED':
|
|
746
|
+
case 'CERT_SIGNATURE_FAILURE':
|
|
747
|
+
case 'CERT_UNTRUSTED':
|
|
748
|
+
case 'DEPTH_ZERO_SELF_SIGNED_CERT':
|
|
749
|
+
case 'ERR_TLS_CERT_ALTNAME_FORMAT':
|
|
750
|
+
case 'SELF_SIGNED_CERT_IN_CHAIN':
|
|
751
|
+
case 'HOSTNAME_MISMATCH':
|
|
752
|
+
case 'INVALID_CA':
|
|
753
|
+
case 'INVALID_PURPOSE':
|
|
754
|
+
case 'PATH_LENGTH_EXCEEDED':
|
|
755
|
+
case 'UNABLE_TO_GET_ISSUER_CERT':
|
|
756
|
+
case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY':
|
|
757
|
+
case 'UNABLE_TO_VERIFY_LEAF_SIGNATURE':
|
|
758
|
+
case 'ERR_TLS_CERT_ALTNAME_INVALID':
|
|
759
|
+
return 'tls_validation_failed';
|
|
760
|
+
case 'EPROTO':
|
|
761
|
+
return 'tls_protocol_error';
|
|
472
762
|
default:
|
|
763
|
+
if (error.code?.startsWith('ERR_SSL_') || error.code?.startsWith('ERR_TLS_')) {
|
|
764
|
+
return 'tls_handshake_failed';
|
|
765
|
+
}
|
|
473
766
|
return 'internal';
|
|
474
767
|
}
|
|
475
768
|
}
|
|
@@ -479,13 +772,4 @@ function positiveInteger(value, name) {
|
|
|
479
772
|
}
|
|
480
773
|
return value;
|
|
481
774
|
}
|
|
482
|
-
function toBuffer(data) {
|
|
483
|
-
if (Buffer.isBuffer(data))
|
|
484
|
-
return data;
|
|
485
|
-
if (Array.isArray(data))
|
|
486
|
-
return Buffer.concat(data);
|
|
487
|
-
if (data instanceof ArrayBuffer)
|
|
488
|
-
return Buffer.from(data);
|
|
489
|
-
throw new destination_tunnel_1.DestinationTunnelProtocolError('unsupported WebSocket payload type');
|
|
490
|
-
}
|
|
491
775
|
//# sourceMappingURL=destination-tunnel-dialer.js.map
|