@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,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 { DESTINATION_TUNNEL_VERSION, DestinationTunnelProtocolError, assertDestinationTunnelOpenAllowed, assertDestinationTunnelReady, decodeDestinationTunnelDataFrame, decodeDestinationTunnelServerMessage, encodeDestinationTunnelDataFrame, encodeDestinationTunnelClientMessage, validateDestinationTunnelRoutes, } from "./destination-tunnel.mjs";
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 routes = validateDestinationTunnelRoutes(options.routes);
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
- if (!connection.socket.destroyed)
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, routes);
343
+ kind = assertDestinationTunnelOpenAllowed(message, selectors);
208
344
  }
209
345
  catch {
210
- sendOpenFailure(message.connId, 'route_not_allowed');
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 socket = net.createConnection({
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
- if (pausedForBackpressure) {
242
- socket.pause();
243
- }
244
- socket.once('connect', () => {
245
- if (connections.get(message.connId) !== connection)
246
- return;
247
- clearTimeout(connectTimer);
248
- connection.phase = 'open';
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
- connection.localInputEnded = true;
261
- sendControl({ type: 'fin', connId: message.connId });
262
- });
263
- socket.once('error', (error) => {
264
- if (connections.get(message.connId) !== connection)
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
- else {
270
- sendReset(message.connId, 'connection_error', error.code);
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
- removeConnection(message.connId, true);
273
- });
274
- socket.once('close', () => {
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
- if (connection.phase === 'connecting') {
278
- sendOpenFailure(message.connId, 'internal');
279
- }
280
- else if (!connection.localInputEnded || !connection.remoteInputEnded) {
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 ${routes.length} route(s)`);
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(`destination tunnel failed: ${message.code}`);
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({ type: 'start', version: DESTINATION_TUNNEL_VERSION, routes });
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