@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.
Files changed (193) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/build-settings.d.mts.map +1 -1
  3. package/build-settings.d.ts.map +1 -1
  4. package/build-settings.js +4 -5
  5. package/build-settings.js.map +1 -1
  6. package/build-settings.mjs +4 -5
  7. package/build-settings.mjs.map +1 -1
  8. package/client.d.mts +8 -5
  9. package/client.d.mts.map +1 -1
  10. package/client.d.ts +8 -5
  11. package/client.d.ts.map +1 -1
  12. package/client.js +4 -4
  13. package/client.js.map +1 -1
  14. package/client.mjs +2 -2
  15. package/client.mjs.map +1 -1
  16. package/destination-tunnel-dialer.d.mts +30 -2
  17. package/destination-tunnel-dialer.d.mts.map +1 -1
  18. package/destination-tunnel-dialer.d.ts +30 -2
  19. package/destination-tunnel-dialer.d.ts.map +1 -1
  20. package/destination-tunnel-dialer.js +349 -65
  21. package/destination-tunnel-dialer.js.map +1 -1
  22. package/destination-tunnel-dialer.mjs +345 -64
  23. package/destination-tunnel-dialer.mjs.map +1 -1
  24. package/destination-tunnel-har.d.mts +23 -0
  25. package/destination-tunnel-har.d.mts.map +1 -0
  26. package/destination-tunnel-har.d.ts +23 -0
  27. package/destination-tunnel-har.d.ts.map +1 -0
  28. package/destination-tunnel-har.js +164 -0
  29. package/destination-tunnel-har.js.map +1 -0
  30. package/destination-tunnel-har.mjs +160 -0
  31. package/destination-tunnel-har.mjs.map +1 -0
  32. package/destination-tunnel-inspection.d.mts +71 -0
  33. package/destination-tunnel-inspection.d.mts.map +1 -0
  34. package/destination-tunnel-inspection.d.ts +71 -0
  35. package/destination-tunnel-inspection.d.ts.map +1 -0
  36. package/destination-tunnel-inspection.js +267 -0
  37. package/destination-tunnel-inspection.js.map +1 -0
  38. package/destination-tunnel-inspection.mjs +261 -0
  39. package/destination-tunnel-inspection.mjs.map +1 -0
  40. package/destination-tunnel.d.mts +119 -14
  41. package/destination-tunnel.d.mts.map +1 -1
  42. package/destination-tunnel.d.ts +119 -14
  43. package/destination-tunnel.d.ts.map +1 -1
  44. package/destination-tunnel.js +407 -98
  45. package/destination-tunnel.js.map +1 -1
  46. package/destination-tunnel.mjs +374 -73
  47. package/destination-tunnel.mjs.map +1 -1
  48. package/exec-client.d.mts +9 -0
  49. package/exec-client.d.mts.map +1 -1
  50. package/exec-client.d.ts +9 -0
  51. package/exec-client.d.ts.map +1 -1
  52. package/exec-client.js.map +1 -1
  53. package/exec-client.mjs.map +1 -1
  54. package/index.d.mts +8 -1
  55. package/index.d.mts.map +1 -1
  56. package/index.d.ts +8 -1
  57. package/index.d.ts.map +1 -1
  58. package/index.js +26 -2
  59. package/index.js.map +1 -1
  60. package/index.mjs +7 -1
  61. package/index.mjs.map +1 -1
  62. package/instance-client.d.mts +107 -1
  63. package/instance-client.d.mts.map +1 -1
  64. package/instance-client.d.ts +107 -1
  65. package/instance-client.d.ts.map +1 -1
  66. package/instance-client.js +83 -1
  67. package/instance-client.js.map +1 -1
  68. package/instance-client.mjs +83 -1
  69. package/instance-client.mjs.map +1 -1
  70. package/internal/destination-tunnel-management.d.mts +4 -3
  71. package/internal/destination-tunnel-management.d.mts.map +1 -1
  72. package/internal/destination-tunnel-management.d.ts +4 -3
  73. package/internal/destination-tunnel-management.d.ts.map +1 -1
  74. package/internal/destination-tunnel-management.js +64 -46
  75. package/internal/destination-tunnel-management.js.map +1 -1
  76. package/internal/destination-tunnel-management.mjs +55 -37
  77. package/internal/destination-tunnel-management.mjs.map +1 -1
  78. package/internal/destination-tunnel-url.d.mts +2 -0
  79. package/internal/destination-tunnel-url.d.mts.map +1 -1
  80. package/internal/destination-tunnel-url.d.ts +2 -0
  81. package/internal/destination-tunnel-url.d.ts.map +1 -1
  82. package/internal/destination-tunnel-url.js +45 -5
  83. package/internal/destination-tunnel-url.js.map +1 -1
  84. package/internal/destination-tunnel-url.mjs +44 -5
  85. package/internal/destination-tunnel-url.mjs.map +1 -1
  86. package/internal/destination-tunnel-wire-reader.d.mts +23 -0
  87. package/internal/destination-tunnel-wire-reader.d.mts.map +1 -0
  88. package/internal/destination-tunnel-wire-reader.d.ts +23 -0
  89. package/internal/destination-tunnel-wire-reader.d.ts.map +1 -0
  90. package/internal/destination-tunnel-wire-reader.js +110 -0
  91. package/internal/destination-tunnel-wire-reader.js.map +1 -0
  92. package/internal/destination-tunnel-wire-reader.mjs +93 -0
  93. package/internal/destination-tunnel-wire-reader.mjs.map +1 -0
  94. package/internal/persist-option.d.mts +14 -0
  95. package/internal/persist-option.d.mts.map +1 -0
  96. package/internal/persist-option.d.ts +14 -0
  97. package/internal/persist-option.d.ts.map +1 -0
  98. package/internal/persist-option.js +18 -0
  99. package/internal/persist-option.js.map +1 -0
  100. package/internal/persist-option.mjs +15 -0
  101. package/internal/persist-option.mjs.map +1 -0
  102. package/internal/proxy-transport.d.mts +27 -0
  103. package/internal/proxy-transport.d.mts.map +1 -1
  104. package/internal/proxy-transport.d.ts +27 -0
  105. package/internal/proxy-transport.d.ts.map +1 -1
  106. package/internal/proxy-transport.js +206 -0
  107. package/internal/proxy-transport.js.map +1 -1
  108. package/internal/proxy-transport.mjs +205 -0
  109. package/internal/proxy-transport.mjs.map +1 -1
  110. package/internal/session-stream.d.mts +39 -0
  111. package/internal/session-stream.d.mts.map +1 -0
  112. package/internal/session-stream.d.ts +39 -0
  113. package/internal/session-stream.d.ts.map +1 -0
  114. package/internal/session-stream.js +144 -0
  115. package/internal/session-stream.js.map +1 -0
  116. package/internal/session-stream.mjs +139 -0
  117. package/internal/session-stream.mjs.map +1 -0
  118. package/internal/tslib.js +4 -4
  119. package/ios-client.d.mts +63 -4
  120. package/ios-client.d.mts.map +1 -1
  121. package/ios-client.d.ts +63 -4
  122. package/ios-client.d.ts.map +1 -1
  123. package/ios-client.js +48 -2
  124. package/ios-client.js.map +1 -1
  125. package/ios-client.mjs +48 -2
  126. package/ios-client.mjs.map +1 -1
  127. package/package.json +22 -1
  128. package/resources/android-instances-helpers.d.mts +25 -0
  129. package/resources/android-instances-helpers.d.mts.map +1 -0
  130. package/resources/android-instances-helpers.d.ts +25 -0
  131. package/resources/android-instances-helpers.d.ts.map +1 -0
  132. package/resources/android-instances-helpers.js +37 -0
  133. package/resources/android-instances-helpers.js.map +1 -0
  134. package/resources/android-instances-helpers.mjs +33 -0
  135. package/resources/android-instances-helpers.mjs.map +1 -0
  136. package/resources/index.d.mts +6 -3
  137. package/resources/index.d.mts.map +1 -1
  138. package/resources/index.d.ts +6 -3
  139. package/resources/index.d.ts.map +1 -1
  140. package/resources/index.js +6 -5
  141. package/resources/index.js.map +1 -1
  142. package/resources/index.mjs +3 -3
  143. package/resources/index.mjs.map +1 -1
  144. package/resources/ios-instances-helpers.d.mts +25 -0
  145. package/resources/ios-instances-helpers.d.mts.map +1 -0
  146. package/resources/ios-instances-helpers.d.ts +25 -0
  147. package/resources/ios-instances-helpers.d.ts.map +1 -0
  148. package/resources/ios-instances-helpers.js +37 -0
  149. package/resources/ios-instances-helpers.js.map +1 -0
  150. package/resources/ios-instances-helpers.mjs +33 -0
  151. package/resources/ios-instances-helpers.mjs.map +1 -0
  152. package/resources/session-artifacts.d.mts +70 -0
  153. package/resources/session-artifacts.d.mts.map +1 -0
  154. package/resources/session-artifacts.d.ts +70 -0
  155. package/resources/session-artifacts.d.ts.map +1 -0
  156. package/resources/session-artifacts.js +8 -0
  157. package/resources/session-artifacts.js.map +1 -0
  158. package/resources/session-artifacts.mjs +7 -0
  159. package/resources/session-artifacts.mjs.map +1 -0
  160. package/resources/xcode-instances-helpers.d.mts +55 -0
  161. package/resources/xcode-instances-helpers.d.mts.map +1 -1
  162. package/resources/xcode-instances-helpers.d.ts +55 -0
  163. package/resources/xcode-instances-helpers.d.ts.map +1 -1
  164. package/resources/xcode-instances-helpers.js +49 -3
  165. package/resources/xcode-instances-helpers.js.map +1 -1
  166. package/resources/xcode-instances-helpers.mjs +47 -2
  167. package/resources/xcode-instances-helpers.mjs.map +1 -1
  168. package/src/build-settings.ts +4 -5
  169. package/src/client.ts +12 -2
  170. package/src/destination-tunnel-dialer.ts +403 -64
  171. package/src/destination-tunnel-har.ts +200 -0
  172. package/src/destination-tunnel-inspection.ts +367 -0
  173. package/src/destination-tunnel.ts +510 -88
  174. package/src/exec-client.ts +9 -0
  175. package/src/index.ts +58 -0
  176. package/src/instance-client.ts +273 -5
  177. package/src/internal/destination-tunnel-management.ts +67 -43
  178. package/src/internal/destination-tunnel-url.ts +61 -5
  179. package/src/internal/destination-tunnel-wire-reader.ts +109 -0
  180. package/src/internal/persist-option.ts +26 -0
  181. package/src/internal/proxy-transport.ts +248 -0
  182. package/src/internal/session-stream.ts +157 -0
  183. package/src/ios-client.ts +129 -8
  184. package/src/resources/android-instances-helpers.ts +45 -0
  185. package/src/resources/index.ts +14 -2
  186. package/src/resources/ios-instances-helpers.ts +45 -0
  187. package/src/resources/session-artifacts.ts +95 -0
  188. package/src/resources/xcode-instances-helpers.ts +116 -5
  189. package/src/version.ts +1 -1
  190. package/version.d.mts +1 -1
  191. package/version.d.ts +1 -1
  192. package/version.js +1 -1
  193. 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 routes = (0, destination_tunnel_1.validateDestinationTunnelRoutes)(options.routes);
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
- if (!connection.socket.destroyed)
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, routes);
351
+ kind = (0, destination_tunnel_1.assertDestinationTunnelOpenAllowed)(message, selectors);
213
352
  }
214
353
  catch {
215
- sendOpenFailure(message.connId, 'route_not_allowed');
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 socket = net_1.default.createConnection({
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
- if (pausedForBackpressure) {
247
- socket.pause();
248
- }
249
- socket.once('connect', () => {
250
- if (connections.get(message.connId) !== connection)
251
- return;
252
- clearTimeout(connectTimer);
253
- connection.phase = 'open';
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
- connection.localInputEnded = true;
266
- sendControl({ type: 'fin', connId: message.connId });
267
- });
268
- socket.once('error', (error) => {
269
- if (connections.get(message.connId) !== connection)
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
- else {
275
- sendReset(message.connId, 'connection_error', error.code);
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
- removeConnection(message.connId, true);
278
- });
279
- socket.once('close', () => {
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
- if (connection.phase === 'connecting') {
283
- sendOpenFailure(message.connId, 'internal');
284
- }
285
- else if (!connection.localInputEnded || !connection.remoteInputEnded) {
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 ${routes.length} route(s)`);
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(`destination tunnel failed: ${message.code}`);
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({ type: 'start', version: destination_tunnel_1.DESTINATION_TUNNEL_VERSION, routes });
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