@limrun/api 0.48.4 → 0.49.0
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 +4 -0
- package/client.d.mts +6 -3
- package/client.d.mts.map +1 -1
- package/client.d.ts +6 -3
- 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/index.d.mts +6 -0
- package/index.d.mts.map +1 -1
- package/index.d.ts +6 -0
- package/index.d.ts.map +1 -1
- package/index.js +23 -2
- package/index.js.map +1 -1
- package/index.mjs +5 -0
- package/index.mjs.map +1 -1
- package/instance-client.d.mts +105 -1
- package/instance-client.d.mts.map +1 -1
- package/instance-client.d.ts +105 -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 +61 -4
- package/ios-client.d.mts.map +1 -1
- package/ios-client.d.ts +61 -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 +5 -2
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +5 -2
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js +5 -5
- package/resources/index.js.map +1 -1
- package/resources/index.mjs +2 -2
- 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/src/client.ts +4 -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/index.ts +52 -0
- package/src/instance-client.ts +271 -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 +127 -8
- package/src/resources/android-instances-helpers.ts +45 -0
- package/src/resources/index.ts +9 -2
- package/src/resources/ios-instances-helpers.ts +45 -0
- package/src/resources/session-artifacts.ts +95 -0
- 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,10 +1,39 @@
|
|
|
1
|
+
import dns from 'dns';
|
|
1
2
|
import net from 'net';
|
|
3
|
+
import tls from 'tls';
|
|
2
4
|
import { WebSocket } from 'ws';
|
|
3
5
|
import { nodeProxyTransport } from "./internal/proxy-transport.mjs";
|
|
4
|
-
import {
|
|
6
|
+
import { toBuffer } from "./internal/destination-tunnel-wire-reader.mjs";
|
|
7
|
+
import { startDestinationTunnelInspectionStream, } from "./destination-tunnel-inspection.mjs";
|
|
8
|
+
import { DESTINATION_TUNNEL_DEFAULT_WINDOW, DESTINATION_TUNNEL_VERSION, DestinationTunnelProtocolError, assertDestinationTunnelOpenAllowed, assertDestinationTunnelReady, classifyDestinationTunnelSelectors, decodeDestinationTunnelDataFrame, decodeDestinationTunnelServerMessage, disabledDestinationTunnelInspection, destinationTunnelConfigHash, encodeDestinationTunnelDataFrame, encodeDestinationTunnelClientMessage, normalizeDestinationTunnelInspection, validateDestinationTunnelSelectors, } from "./destination-tunnel.mjs";
|
|
9
|
+
const blockedResolvedAddresses = new net.BlockList();
|
|
10
|
+
blockedResolvedAddresses.addSubnet('0.0.0.0', 8, 'ipv4');
|
|
11
|
+
blockedResolvedAddresses.addSubnet('127.0.0.0', 8, 'ipv4');
|
|
12
|
+
blockedResolvedAddresses.addSubnet('169.254.0.0', 16, 'ipv4');
|
|
13
|
+
blockedResolvedAddresses.addSubnet('224.0.0.0', 3, 'ipv4');
|
|
14
|
+
blockedResolvedAddresses.addSubnet('::', 96, 'ipv6');
|
|
15
|
+
blockedResolvedAddresses.addSubnet('fe80::', 10, 'ipv6');
|
|
16
|
+
blockedResolvedAddresses.addSubnet('ff00::', 8, 'ipv6');
|
|
17
|
+
blockedResolvedAddresses.addSubnet('::ffff:0.0.0.0', 104, 'ipv6');
|
|
18
|
+
blockedResolvedAddresses.addSubnet('::ffff:127.0.0.0', 104, 'ipv6');
|
|
19
|
+
blockedResolvedAddresses.addSubnet('::ffff:169.254.0.0', 112, 'ipv6');
|
|
20
|
+
blockedResolvedAddresses.addSubnet('::ffff:224.0.0.0', 99, 'ipv6');
|
|
21
|
+
/** Thrown (message prefix) when the server terminates the session with `error`. */
|
|
22
|
+
export const DESTINATION_TUNNEL_SERVER_ERROR_PREFIX = 'destination tunnel failed: ';
|
|
23
|
+
/**
|
|
24
|
+
* True when the failure is a terminal protocol/policy rejection from the
|
|
25
|
+
* server rather than a transient transport problem. Reconnect supervisors
|
|
26
|
+
* must not retry terminal failures.
|
|
27
|
+
*/
|
|
28
|
+
export function isTerminalDestinationTunnelError(error) {
|
|
29
|
+
return error instanceof Error && error.message.startsWith(DESTINATION_TUNNEL_SERVER_ERROR_PREFIX);
|
|
30
|
+
}
|
|
5
31
|
export async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
6
|
-
const
|
|
32
|
+
const selectors = validateDestinationTunnelSelectors(options.selectors);
|
|
33
|
+
const routes = classifyDestinationTunnelSelectors(selectors).routes ?? [];
|
|
34
|
+
const inspection = normalizeDestinationTunnelInspection(options.inspection ?? disabledDestinationTunnelInspection());
|
|
7
35
|
const logLevel = options.logLevel ?? 'info';
|
|
36
|
+
const creditWindow = positiveInteger(options.window ?? DESTINATION_TUNNEL_DEFAULT_WINDOW, 'window');
|
|
8
37
|
const maxConnections = positiveInteger(options.maxConnections ?? 64, 'maxConnections');
|
|
9
38
|
const maxPendingBytesPerConnection = positiveInteger(options.maxPendingBytesPerConnection ?? 16 * 1024 * 1024, 'maxPendingBytesPerConnection');
|
|
10
39
|
const maxTotalPendingBytes = positiveInteger(options.maxTotalPendingBytes ?? 16 * 1024 * 1024, 'maxTotalPendingBytes');
|
|
@@ -51,6 +80,7 @@ export async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
51
80
|
let livenessTimer;
|
|
52
81
|
let tunnelReady = false;
|
|
53
82
|
let closed = false;
|
|
83
|
+
let inspectionStream;
|
|
54
84
|
let pausedForBackpressure = false;
|
|
55
85
|
let totalPendingWriteBytes = 0;
|
|
56
86
|
const updateConnectionState = (state) => {
|
|
@@ -84,9 +114,10 @@ export async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
84
114
|
if (!connection)
|
|
85
115
|
return;
|
|
86
116
|
connections.delete(connId);
|
|
117
|
+
connection.abortController.abort();
|
|
87
118
|
clearTimeout(connection.connectTimer);
|
|
88
119
|
markRecentlyClosed(connId);
|
|
89
|
-
if (destroySocket && !connection.socket.destroyed) {
|
|
120
|
+
if (destroySocket && connection.socket && !connection.socket.destroyed) {
|
|
90
121
|
connection.socket.destroy();
|
|
91
122
|
}
|
|
92
123
|
};
|
|
@@ -117,6 +148,8 @@ export async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
117
148
|
livenessTimer = undefined;
|
|
118
149
|
}
|
|
119
150
|
closeAllConnections();
|
|
151
|
+
inspectionStream?.close();
|
|
152
|
+
inspectionStream = undefined;
|
|
120
153
|
ws.removeAllListeners('open');
|
|
121
154
|
ws.removeAllListeners('message');
|
|
122
155
|
ws.removeAllListeners('ping');
|
|
@@ -135,12 +168,13 @@ export async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
135
168
|
reject(error);
|
|
136
169
|
}
|
|
137
170
|
};
|
|
171
|
+
const mayReadFromSocket = (connection) => !pausedForBackpressure && connection.creditQueueBytes === 0;
|
|
138
172
|
const pauseForBackpressure = () => {
|
|
139
173
|
if (pausedForBackpressure)
|
|
140
174
|
return;
|
|
141
175
|
pausedForBackpressure = true;
|
|
142
176
|
for (const connection of connections.values()) {
|
|
143
|
-
if (!connection.socket.destroyed)
|
|
177
|
+
if (connection.socket && !connection.socket.destroyed)
|
|
144
178
|
connection.socket.pause();
|
|
145
179
|
}
|
|
146
180
|
};
|
|
@@ -149,8 +183,12 @@ export async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
149
183
|
return;
|
|
150
184
|
pausedForBackpressure = false;
|
|
151
185
|
for (const connection of connections.values()) {
|
|
152
|
-
|
|
186
|
+
// Flows still waiting on per-flow credit stay paused; they resume
|
|
187
|
+
// when the server grants more window, keeping one stalled flow from
|
|
188
|
+
// holding every other flow back.
|
|
189
|
+
if (connection.socket && !connection.socket.destroyed && mayReadFromSocket(connection)) {
|
|
153
190
|
connection.socket.resume();
|
|
191
|
+
}
|
|
154
192
|
}
|
|
155
193
|
};
|
|
156
194
|
const frameQueued = () => {
|
|
@@ -202,12 +240,110 @@ export async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
202
240
|
...(osCode ? { osCode } : {}),
|
|
203
241
|
});
|
|
204
242
|
};
|
|
243
|
+
/**
|
|
244
|
+
* Push local socket bytes toward the server through the per-flow credit
|
|
245
|
+
* gate. Chunks beyond the current credit wait in a FIFO queue with the
|
|
246
|
+
* local socket paused, so a server-throttled flow stops reading instead
|
|
247
|
+
* of buffering unboundedly.
|
|
248
|
+
*/
|
|
249
|
+
const pushThroughCreditGate = (connId, connection, payload) => {
|
|
250
|
+
if (payload && payload.length > 0) {
|
|
251
|
+
connection.creditQueue.push(payload);
|
|
252
|
+
connection.creditQueueBytes += payload.length;
|
|
253
|
+
}
|
|
254
|
+
while (connection.creditQueue.length > 0 && connection.sendCredit > 0) {
|
|
255
|
+
const chunk = connection.creditQueue[0];
|
|
256
|
+
if (chunk.length <= connection.sendCredit) {
|
|
257
|
+
connection.creditQueue.shift();
|
|
258
|
+
connection.creditQueueBytes -= chunk.length;
|
|
259
|
+
connection.sendCredit -= chunk.length;
|
|
260
|
+
sendData(connId, chunk);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
const portion = chunk.subarray(0, connection.sendCredit);
|
|
264
|
+
connection.creditQueue[0] = chunk.subarray(connection.sendCredit);
|
|
265
|
+
connection.creditQueueBytes -= portion.length;
|
|
266
|
+
connection.sendCredit = 0;
|
|
267
|
+
sendData(connId, portion);
|
|
268
|
+
}
|
|
269
|
+
if (closed)
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (connection.creditQueue.length === 0 && connection.finPending) {
|
|
273
|
+
// The local socket ended while data was still waiting on credit; the
|
|
274
|
+
// queue has drained, so half-close can now be signaled in order.
|
|
275
|
+
connection.finPending = false;
|
|
276
|
+
connection.localInputEnded = true;
|
|
277
|
+
sendControl({ type: 'fin', connId });
|
|
278
|
+
}
|
|
279
|
+
if (!connection.socket || connection.socket.destroyed)
|
|
280
|
+
return;
|
|
281
|
+
if (connection.creditQueueBytes > 0) {
|
|
282
|
+
connection.socket.pause();
|
|
283
|
+
}
|
|
284
|
+
else if (!pausedForBackpressure) {
|
|
285
|
+
connection.socket.resume();
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
const dialTarget = async (message) => {
|
|
289
|
+
// Domain selectors resolve through the user's OS resolver and may only
|
|
290
|
+
// reach ordinary unicast targets. Loopback and similar special targets
|
|
291
|
+
// require an explicit exact route grant.
|
|
292
|
+
const startedAt = Date.now();
|
|
293
|
+
const results = await dns.promises.lookup(message.host, { all: true, verbatim: true });
|
|
294
|
+
const dnsMs = Date.now() - startedAt;
|
|
295
|
+
for (const result of results) {
|
|
296
|
+
if (isDialableResolvedAddress(result.address, message.port, routes)) {
|
|
297
|
+
return { host: result.address, dnsMs };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const blockedError = new Error(`all resolved addresses for ${message.host} are blocked`);
|
|
301
|
+
blockedError.code = 'EBLOCKED';
|
|
302
|
+
throw blockedError;
|
|
303
|
+
};
|
|
304
|
+
const wrapTls = (socket, transport, abortController) => new Promise((resolveTls, rejectTls) => {
|
|
305
|
+
const tlsSocket = tls.connect({
|
|
306
|
+
socket,
|
|
307
|
+
rejectUnauthorized: true,
|
|
308
|
+
servername: transport.serverName,
|
|
309
|
+
ALPNProtocols: transport.alpnProtocols,
|
|
310
|
+
});
|
|
311
|
+
tlsSocket.allowHalfOpen = true;
|
|
312
|
+
const cleanup = () => {
|
|
313
|
+
tlsSocket.removeListener('secureConnect', onSecureConnect);
|
|
314
|
+
tlsSocket.removeListener('error', onError);
|
|
315
|
+
abortController.signal.removeEventListener('abort', onAbort);
|
|
316
|
+
};
|
|
317
|
+
const onSecureConnect = () => {
|
|
318
|
+
cleanup();
|
|
319
|
+
tlsSocket.pause();
|
|
320
|
+
resolveTls(tlsSocket);
|
|
321
|
+
};
|
|
322
|
+
const onError = (error) => {
|
|
323
|
+
cleanup();
|
|
324
|
+
rejectTls(error);
|
|
325
|
+
};
|
|
326
|
+
const onAbort = () => {
|
|
327
|
+
cleanup();
|
|
328
|
+
tlsSocket.destroy();
|
|
329
|
+
rejectTls(Object.assign(new Error('TLS connection cancelled'), { code: 'ECANCELED' }));
|
|
330
|
+
};
|
|
331
|
+
tlsSocket.once('secureConnect', onSecureConnect);
|
|
332
|
+
tlsSocket.once('error', onError);
|
|
333
|
+
if (abortController.signal.aborted) {
|
|
334
|
+
onAbort();
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
abortController.signal.addEventListener('abort', onAbort, { once: true });
|
|
338
|
+
}
|
|
339
|
+
});
|
|
205
340
|
const handleOpen = (message) => {
|
|
341
|
+
let kind;
|
|
206
342
|
try {
|
|
207
|
-
assertDestinationTunnelOpenAllowed(message,
|
|
343
|
+
kind = assertDestinationTunnelOpenAllowed(message, selectors);
|
|
208
344
|
}
|
|
209
345
|
catch {
|
|
210
|
-
sendOpenFailure(message.connId, '
|
|
346
|
+
sendOpenFailure(message.connId, 'selector_not_allowed');
|
|
211
347
|
return;
|
|
212
348
|
}
|
|
213
349
|
if (connections.has(message.connId) || recentlyClosed.has(message.connId)) {
|
|
@@ -217,75 +353,126 @@ export async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
217
353
|
sendOpenFailure(message.connId, 'resource_exhausted');
|
|
218
354
|
return;
|
|
219
355
|
}
|
|
220
|
-
const
|
|
221
|
-
host: message.host,
|
|
222
|
-
port: message.port,
|
|
223
|
-
allowHalfOpen: true,
|
|
224
|
-
});
|
|
356
|
+
const abortController = new AbortController();
|
|
225
357
|
const connectTimer = setTimeout(() => {
|
|
226
358
|
if (connections.get(message.connId)?.phase !== 'connecting')
|
|
227
359
|
return;
|
|
360
|
+
logger.warn(`Failed to connect to ${message.host}:${message.port}: timed out`);
|
|
228
361
|
sendOpenFailure(message.connId, 'connection_timed_out', 'ETIMEDOUT');
|
|
229
362
|
removeConnection(message.connId, true);
|
|
230
363
|
}, connectTimeoutMs);
|
|
231
364
|
connectTimer.unref();
|
|
232
365
|
const connection = {
|
|
233
|
-
socket,
|
|
234
366
|
phase: 'connecting',
|
|
367
|
+
abortController,
|
|
235
368
|
connectTimer,
|
|
236
369
|
localInputEnded: false,
|
|
237
370
|
remoteInputEnded: false,
|
|
238
371
|
pendingWriteBytes: 0,
|
|
372
|
+
sendCredit: message.window,
|
|
373
|
+
creditQueue: [],
|
|
374
|
+
creditQueueBytes: 0,
|
|
375
|
+
finPending: false,
|
|
376
|
+
deliveredSinceUpdate: 0,
|
|
239
377
|
};
|
|
240
378
|
connections.set(message.connId, connection);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
sendControl({ type: 'openOk', connId: message.connId });
|
|
250
|
-
logger.debug(`Connected ${message.connId} to ${message.host}:${message.port}`);
|
|
251
|
-
});
|
|
252
|
-
socket.on('data', (payload) => {
|
|
253
|
-
if (connections.get(message.connId) !== connection || connection.phase !== 'open')
|
|
254
|
-
return;
|
|
255
|
-
sendData(message.connId, payload);
|
|
256
|
-
});
|
|
257
|
-
socket.once('end', () => {
|
|
258
|
-
if (connections.get(message.connId) !== connection || connection.phase !== 'open')
|
|
379
|
+
void (async () => {
|
|
380
|
+
let host = message.host;
|
|
381
|
+
if (kind === 'domain') {
|
|
382
|
+
const resolved = await dialTarget(message);
|
|
383
|
+
host = resolved.host;
|
|
384
|
+
connection.dnsMs = resolved.dnsMs;
|
|
385
|
+
}
|
|
386
|
+
if (connections.get(message.connId) !== connection || closed)
|
|
259
387
|
return;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
388
|
+
const tcp = await nodeProxyTransport.connectTcp({
|
|
389
|
+
host,
|
|
390
|
+
port: message.port,
|
|
391
|
+
proxyLookupHost: message.host,
|
|
392
|
+
proxyLookupProtocol: message.transport.type === 'tls' ? 'https:' : 'http:',
|
|
393
|
+
timeoutMs: connectTimeoutMs,
|
|
394
|
+
signal: abortController.signal,
|
|
395
|
+
});
|
|
396
|
+
if (connections.get(message.connId) !== connection || closed) {
|
|
397
|
+
tcp.socket.destroy();
|
|
265
398
|
return;
|
|
266
|
-
if (connection.phase === 'connecting') {
|
|
267
|
-
sendOpenFailure(message.connId, classifyOpenFailure(error), error.code);
|
|
268
399
|
}
|
|
269
|
-
|
|
270
|
-
|
|
400
|
+
connection.socket = tcp.socket;
|
|
401
|
+
let socket = tcp.socket;
|
|
402
|
+
let tlsMs;
|
|
403
|
+
if (message.transport.type === 'tls') {
|
|
404
|
+
const tlsStartedAt = Date.now();
|
|
405
|
+
socket = await wrapTls(tcp.socket, message.transport, abortController);
|
|
406
|
+
tlsMs = Date.now() - tlsStartedAt;
|
|
407
|
+
if (connections.get(message.connId) !== connection || closed) {
|
|
408
|
+
socket.destroy();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
connection.socket = socket;
|
|
271
412
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
413
|
+
clearTimeout(connectTimer);
|
|
414
|
+
socket.on('data', (payload) => {
|
|
415
|
+
if (connections.get(message.connId) !== connection || connection.phase !== 'open')
|
|
416
|
+
return;
|
|
417
|
+
pushThroughCreditGate(message.connId, connection, payload);
|
|
418
|
+
});
|
|
419
|
+
socket.once('end', () => {
|
|
420
|
+
if (connections.get(message.connId) !== connection || connection.phase !== 'open')
|
|
421
|
+
return;
|
|
422
|
+
if (connection.creditQueueBytes > 0) {
|
|
423
|
+
// Queued bytes are still waiting on send credit; fin must follow
|
|
424
|
+
// them, so defer it until the credit gate drains the queue.
|
|
425
|
+
connection.finPending = true;
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
connection.localInputEnded = true;
|
|
429
|
+
sendControl({ type: 'fin', connId: message.connId });
|
|
430
|
+
});
|
|
431
|
+
socket.once('error', (error) => {
|
|
432
|
+
if (connections.get(message.connId) !== connection)
|
|
433
|
+
return;
|
|
434
|
+
sendReset(message.connId, 'connection_error', error.code);
|
|
435
|
+
removeConnection(message.connId, true);
|
|
436
|
+
});
|
|
437
|
+
socket.once('close', () => {
|
|
438
|
+
if (connections.get(message.connId) !== connection)
|
|
439
|
+
return;
|
|
440
|
+
if (!connection.localInputEnded || !connection.remoteInputEnded) {
|
|
441
|
+
sendReset(message.connId, 'connection_error');
|
|
442
|
+
}
|
|
443
|
+
removeConnection(message.connId, false);
|
|
444
|
+
});
|
|
445
|
+
connection.phase = 'open';
|
|
446
|
+
sendControl({
|
|
447
|
+
type: 'openOk',
|
|
448
|
+
connId: message.connId,
|
|
449
|
+
transport: {
|
|
450
|
+
type: message.transport.type,
|
|
451
|
+
...(tcp.remoteAddress === undefined ? {} : { remoteAddress: tcp.remoteAddress }),
|
|
452
|
+
...(connection.dnsMs === undefined ? {} : { dnsMs: connection.dnsMs }),
|
|
453
|
+
connectMs: tcp.connectMs,
|
|
454
|
+
...(tlsMs === undefined ? {} : { tlsMs }),
|
|
455
|
+
...(socket instanceof tls.TLSSocket && socket.alpnProtocol ?
|
|
456
|
+
{ alpnProtocol: socket.alpnProtocol }
|
|
457
|
+
: {}),
|
|
458
|
+
},
|
|
459
|
+
window: creditWindow,
|
|
460
|
+
});
|
|
461
|
+
logger.debug(`Forwarding connection ${message.connId} to ${message.host}:${message.port}`);
|
|
462
|
+
if (!pausedForBackpressure && connection.creditQueueBytes === 0)
|
|
463
|
+
socket.resume();
|
|
464
|
+
})().catch((error) => {
|
|
275
465
|
if (connections.get(message.connId) !== connection)
|
|
276
466
|
return;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
sendReset(message.connId, 'connection_error');
|
|
282
|
-
}
|
|
283
|
-
removeConnection(message.connId, false);
|
|
467
|
+
logger.warn(`Failed to connect to ${message.host}:${message.port}: ${error.code ?? error.message}`);
|
|
468
|
+
const reason = error.code === 'EBLOCKED' ? 'selector_not_allowed' : classifyOpenFailure(error);
|
|
469
|
+
sendOpenFailure(message.connId, reason, error.code === 'EBLOCKED' ? undefined : error.code);
|
|
470
|
+
removeConnection(message.connId, true);
|
|
284
471
|
});
|
|
285
472
|
};
|
|
286
473
|
const findOpenConnection = (connId) => {
|
|
287
474
|
const connection = connections.get(connId);
|
|
288
|
-
if (connection?.phase === 'open')
|
|
475
|
+
if (connection?.phase === 'open' && connection.socket)
|
|
289
476
|
return connection;
|
|
290
477
|
if (connection) {
|
|
291
478
|
sendReset(connId, 'protocol_error');
|
|
@@ -314,6 +501,14 @@ export async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
314
501
|
removeConnection(connId, true);
|
|
315
502
|
}
|
|
316
503
|
};
|
|
504
|
+
const handleRemoteWindowUpdate = (connId, increment) => {
|
|
505
|
+
const connection = connections.get(connId);
|
|
506
|
+
// Updates racing a local close are expected; ignore unknown flows.
|
|
507
|
+
if (!connection || connection.phase !== 'open')
|
|
508
|
+
return;
|
|
509
|
+
connection.sendCredit += increment;
|
|
510
|
+
pushThroughCreditGate(connId, connection);
|
|
511
|
+
};
|
|
317
512
|
const handleBinary = (frame) => {
|
|
318
513
|
if (!tunnelReady) {
|
|
319
514
|
throw new DestinationTunnelProtocolError('received tunnel data before READY');
|
|
@@ -344,15 +539,32 @@ export async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
344
539
|
const osCode = error.code;
|
|
345
540
|
sendReset(connId, 'connection_error', osCode);
|
|
346
541
|
removeConnection(connId, true);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
// Replenish the server's send window once the local socket accepted
|
|
545
|
+
// the bytes, batching updates to roughly half the window.
|
|
546
|
+
if (connections.get(connId) !== connection)
|
|
547
|
+
return;
|
|
548
|
+
connection.deliveredSinceUpdate += payload.length;
|
|
549
|
+
if (connection.deliveredSinceUpdate >= Math.ceil(creditWindow / 2)) {
|
|
550
|
+
const increment = connection.deliveredSinceUpdate;
|
|
551
|
+
connection.deliveredSinceUpdate = 0;
|
|
552
|
+
if (!closed && ws.readyState === WebSocket.OPEN) {
|
|
553
|
+
sendControl({ type: 'windowUpdate', connId, increment });
|
|
554
|
+
}
|
|
347
555
|
}
|
|
348
556
|
});
|
|
349
557
|
};
|
|
350
558
|
const handleControl = (message) => {
|
|
351
559
|
switch (message.type) {
|
|
352
|
-
case 'ready':
|
|
560
|
+
case 'ready': {
|
|
353
561
|
if (tunnelReady)
|
|
354
562
|
throw new DestinationTunnelProtocolError('received duplicate READY');
|
|
355
563
|
assertDestinationTunnelReady(message);
|
|
564
|
+
const expected = destinationTunnelConfigHash(selectors, inspection);
|
|
565
|
+
if (message.configHash !== expected) {
|
|
566
|
+
throw new DestinationTunnelProtocolError(`server acknowledged config ${message.configHash} but ${expected} was negotiated`);
|
|
567
|
+
}
|
|
356
568
|
tunnelReady = true;
|
|
357
569
|
if (handshakeTimer) {
|
|
358
570
|
clearTimeout(handshakeTimer);
|
|
@@ -360,16 +572,37 @@ export async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
360
572
|
}
|
|
361
573
|
armLivenessDeadline();
|
|
362
574
|
updateConnectionState('connected');
|
|
363
|
-
logger.info(`Destination tunnel ready with ${
|
|
575
|
+
logger.info(`Destination tunnel ready with ${selectors.length} selector(s)`);
|
|
576
|
+
if (inspection.enabled) {
|
|
577
|
+
try {
|
|
578
|
+
inspectionStream = startDestinationTunnelInspectionStream(remoteURL, message.tunnelId, token, {
|
|
579
|
+
...(options.onInspectionEvent ? { onEvent: options.onInspectionEvent } : {}),
|
|
580
|
+
...(options.onInspectionError ? { onError: options.onInspectionError } : {}),
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
try {
|
|
585
|
+
options.onInspectionError?.(error instanceof Error ? error : new Error(String(error)));
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
// Inspection callbacks are isolated from the main tunnel.
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
364
592
|
resolve({
|
|
365
593
|
tunnelId: message.tunnelId,
|
|
594
|
+
selectors: message.selectors,
|
|
595
|
+
configHash: message.configHash,
|
|
596
|
+
inspection,
|
|
597
|
+
...(inspectionStream ? { inspectionStream } : {}),
|
|
366
598
|
close,
|
|
367
599
|
getConnectionState,
|
|
368
600
|
onConnectionStateChange,
|
|
369
601
|
});
|
|
370
602
|
return;
|
|
603
|
+
}
|
|
371
604
|
case 'error':
|
|
372
|
-
throw new Error(
|
|
605
|
+
throw new Error(`${DESTINATION_TUNNEL_SERVER_ERROR_PREFIX}${message.code}`);
|
|
373
606
|
case 'open':
|
|
374
607
|
if (!tunnelReady)
|
|
375
608
|
throw new DestinationTunnelProtocolError('received OPEN before READY');
|
|
@@ -380,6 +613,11 @@ export async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
380
613
|
throw new DestinationTunnelProtocolError('received FIN before READY');
|
|
381
614
|
handleRemoteFIN(message.connId);
|
|
382
615
|
return;
|
|
616
|
+
case 'windowUpdate':
|
|
617
|
+
if (!tunnelReady)
|
|
618
|
+
throw new DestinationTunnelProtocolError('received windowUpdate before READY');
|
|
619
|
+
handleRemoteWindowUpdate(message.connId, message.increment);
|
|
620
|
+
return;
|
|
383
621
|
case 'reset':
|
|
384
622
|
if (!tunnelReady)
|
|
385
623
|
throw new DestinationTunnelProtocolError('received RESET before READY');
|
|
@@ -401,7 +639,13 @@ export async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
401
639
|
}, handshakeTimeoutMs);
|
|
402
640
|
handshakeTimer.unref();
|
|
403
641
|
ws.on('open', () => {
|
|
404
|
-
sendControl({
|
|
642
|
+
sendControl({
|
|
643
|
+
type: 'start',
|
|
644
|
+
version: DESTINATION_TUNNEL_VERSION,
|
|
645
|
+
selectors,
|
|
646
|
+
inspection,
|
|
647
|
+
window: creditWindow,
|
|
648
|
+
});
|
|
405
649
|
pingInterval = setInterval(() => {
|
|
406
650
|
if (ws.readyState === WebSocket.OPEN) {
|
|
407
651
|
ws.ping(undefined, true, frameSent);
|
|
@@ -439,6 +683,28 @@ export async function startDestinationTcpTunnel(remoteURL, token, options) {
|
|
|
439
683
|
});
|
|
440
684
|
});
|
|
441
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* Whether a resolved domain address may be dialed. Ordinary unicast targets
|
|
688
|
+
* (including private ranges the user's machine can reach) are allowed;
|
|
689
|
+
* loopback, link-local, multicast, unspecified, broadcast, and similar
|
|
690
|
+
* special targets are blocked unless an exact route grants them.
|
|
691
|
+
*/
|
|
692
|
+
export function isDialableResolvedAddress(address, port, routes) {
|
|
693
|
+
for (const route of routes) {
|
|
694
|
+
if (route.port !== port)
|
|
695
|
+
continue;
|
|
696
|
+
if (route.host === address)
|
|
697
|
+
return true;
|
|
698
|
+
if (route.host === 'localhost' && (address === '127.0.0.1' || address === '::1'))
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
const version = net.isIP(address);
|
|
702
|
+
if (version === 4)
|
|
703
|
+
return !blockedResolvedAddresses.check(address, 'ipv4');
|
|
704
|
+
if (version === 6)
|
|
705
|
+
return !blockedResolvedAddresses.check(address, 'ipv6');
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
442
708
|
export function classifyOpenFailure(error) {
|
|
443
709
|
switch (error.code) {
|
|
444
710
|
case 'ENOTFOUND':
|
|
@@ -456,6 +722,7 @@ export function classifyOpenFailure(error) {
|
|
|
456
722
|
return 'unreachable';
|
|
457
723
|
case 'EACCES':
|
|
458
724
|
case 'EPERM':
|
|
725
|
+
case 'EPROXYAUTH':
|
|
459
726
|
return 'permission_denied';
|
|
460
727
|
case 'EMFILE':
|
|
461
728
|
case 'ENFILE':
|
|
@@ -464,7 +731,30 @@ export function classifyOpenFailure(error) {
|
|
|
464
731
|
return 'resource_exhausted';
|
|
465
732
|
case 'ECANCELED':
|
|
466
733
|
return 'cancelled';
|
|
734
|
+
case 'CERT_HAS_EXPIRED':
|
|
735
|
+
case 'CERT_NOT_YET_VALID':
|
|
736
|
+
case 'CERT_REJECTED':
|
|
737
|
+
case 'CERT_REVOKED':
|
|
738
|
+
case 'CERT_SIGNATURE_FAILURE':
|
|
739
|
+
case 'CERT_UNTRUSTED':
|
|
740
|
+
case 'DEPTH_ZERO_SELF_SIGNED_CERT':
|
|
741
|
+
case 'ERR_TLS_CERT_ALTNAME_FORMAT':
|
|
742
|
+
case 'SELF_SIGNED_CERT_IN_CHAIN':
|
|
743
|
+
case 'HOSTNAME_MISMATCH':
|
|
744
|
+
case 'INVALID_CA':
|
|
745
|
+
case 'INVALID_PURPOSE':
|
|
746
|
+
case 'PATH_LENGTH_EXCEEDED':
|
|
747
|
+
case 'UNABLE_TO_GET_ISSUER_CERT':
|
|
748
|
+
case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY':
|
|
749
|
+
case 'UNABLE_TO_VERIFY_LEAF_SIGNATURE':
|
|
750
|
+
case 'ERR_TLS_CERT_ALTNAME_INVALID':
|
|
751
|
+
return 'tls_validation_failed';
|
|
752
|
+
case 'EPROTO':
|
|
753
|
+
return 'tls_protocol_error';
|
|
467
754
|
default:
|
|
755
|
+
if (error.code?.startsWith('ERR_SSL_') || error.code?.startsWith('ERR_TLS_')) {
|
|
756
|
+
return 'tls_handshake_failed';
|
|
757
|
+
}
|
|
468
758
|
return 'internal';
|
|
469
759
|
}
|
|
470
760
|
}
|
|
@@ -474,13 +764,4 @@ function positiveInteger(value, name) {
|
|
|
474
764
|
}
|
|
475
765
|
return value;
|
|
476
766
|
}
|
|
477
|
-
function toBuffer(data) {
|
|
478
|
-
if (Buffer.isBuffer(data))
|
|
479
|
-
return data;
|
|
480
|
-
if (Array.isArray(data))
|
|
481
|
-
return Buffer.concat(data);
|
|
482
|
-
if (data instanceof ArrayBuffer)
|
|
483
|
-
return Buffer.from(data);
|
|
484
|
-
throw new DestinationTunnelProtocolError('unsupported WebSocket payload type');
|
|
485
|
-
}
|
|
486
767
|
//# sourceMappingURL=destination-tunnel-dialer.mjs.map
|