socketduct 0.1.15

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 (138) hide show
  1. package/.dockerignore +11 -0
  2. package/.env.example +131 -0
  3. package/AGENTS.md +168 -0
  4. package/Dockerfile +30 -0
  5. package/README.md +134 -0
  6. package/changelog.d/20260716083144-v2-ack-wire-format.md +3 -0
  7. package/changelog.d/20260716092044-v2-only-stream-framing.md +1 -0
  8. package/changelog.d/20260727113000-relay-connect-timeout-destroy.md +1 -0
  9. package/changelog.d/20260813093000-private-relay-hardening.md +1 -0
  10. package/changelog.d/20260824183412-initial-target-recovery-https.md +1 -0
  11. package/changelog.d/20260824230000-tls-relay-session-close.md +1 -0
  12. package/changelog.d/20260825120000-reconnect-close-target-timeout.md +1 -0
  13. package/changelog.d/20260829120000-protocol-v3-multiplexing.md +1 -0
  14. package/changelog.d/20260829160000-v3-review-corrections.md +1 -0
  15. package/changelog.d/20260830000000-fix-socket-idle-timeout.md +1 -0
  16. package/changelog.d/20260830090000-multi-gateway-transport-set.md +1 -0
  17. package/changelog.d/20260830093000-relay-close-settlement.md +1 -0
  18. package/changelog.d/20260830120000-immutable-gateway-image.md +1 -0
  19. package/changelog.d/20260902210000-native-reverse-gateway.md +1 -0
  20. package/changelog.d/20260902230000-relay-resume-terminal-order.md +1 -0
  21. package/changelog.d/20260902230708-multiplexed-open-ownership.md +1 -0
  22. package/changelog.d/20260902231541-multiplexed-terminal-close.md +1 -0
  23. package/changelog.d/20260902231542-reverse-close-position.md +1 -0
  24. package/changelog.d/20260902231543-reverse-terminal-connect.md +1 -0
  25. package/changelog.d/20260902233000-reverse-gateway-bounds.md +1 -0
  26. package/changelog.d/20260903001000-reverse-data-ordering.md +1 -0
  27. package/changelog.d/20260903003000-reverse-hub-restart-window.md +1 -0
  28. package/changelog.d/20260903010000-reverse-terminal-lifecycle.md +1 -0
  29. package/changelog.d/20260904142743-transport-set-readiness-wait.md +1 -0
  30. package/changelog.d/20260904152855-deferred-open-snapshots-binary-views.md +1 -0
  31. package/changelog.d/20260904163041-tensorbuzz-container-workflow.md +1 -0
  32. package/changelog.d/20260904174955-transport-set-terminal-causes.md +1 -0
  33. package/changelog.d/20260905081000-contain-reverse-auth-write-errors.md +1 -0
  34. package/changelog.d/20260905100520-reverse-hub-close-race.md +1 -0
  35. package/changelog.d/20260905120000-persistent-jsonl-logging.md +1 -0
  36. package/changelog.d/20260908071009-jsonl-partial-append-recovery.md +1 -0
  37. package/changelog.d/20260908074916-private-log-directory-setup.md +1 -0
  38. package/changelog.d/20260909093057-reverse-large-write-backpressure.md +1 -0
  39. package/changelog.d/20260909135830-actionable-error-diagnostics.md +1 -0
  40. package/changelog.d/20260909150000-transport-set-error-redaction.md +1 -0
  41. package/docker-compose.dev.yml +19 -0
  42. package/docker-compose.reverse-gateway.yml +36 -0
  43. package/docker-compose.yml +73 -0
  44. package/docs/architecture.md +264 -0
  45. package/docs/docker-topology.md +164 -0
  46. package/docs/docker.md +281 -0
  47. package/docs/logging.md +185 -0
  48. package/docs/plans/initial-implementation.md +205 -0
  49. package/docs/plans/socket-docker-integration.md +221 -0
  50. package/docs/protocol.md +558 -0
  51. package/docs/releasing.md +18 -0
  52. package/docs/reverse-gateway.md +186 -0
  53. package/docs/runbooks/jsonl-integrity-rollout.md +144 -0
  54. package/docs/running.md +215 -0
  55. package/docs/security.md +258 -0
  56. package/docs/session-lifecycle.md +127 -0
  57. package/docs/socket-api.md +448 -0
  58. package/eslint.config.js +36 -0
  59. package/package.json +44 -0
  60. package/scripts/deploy-gateway.js +194 -0
  61. package/scripts/test.js +28 -0
  62. package/src/auth/passwords.js +38 -0
  63. package/src/auth/static-token-store.js +41 -0
  64. package/src/auth/user-store.js +139 -0
  65. package/src/client/connect.js +76 -0
  66. package/src/client/deferred-virtual-socket.js +312 -0
  67. package/src/client/http-agent.js +101 -0
  68. package/src/client/https-agent.js +366 -0
  69. package/src/client/multiplexed-connection.js +1504 -0
  70. package/src/client/multiplexed-transport-set.js +917 -0
  71. package/src/client/multiplexed.js +3 -0
  72. package/src/client/socketduct-socket.js +1174 -0
  73. package/src/client/virtual-socket.js +256 -0
  74. package/src/config/environment.js +400 -0
  75. package/src/edge/tcp-edge.js +82 -0
  76. package/src/index.js +101 -0
  77. package/src/logging/structured-logger.js +745 -0
  78. package/src/protocol/compression.js +325 -0
  79. package/src/protocol/frame.js +341 -0
  80. package/src/protocol/v3-compression.js +44 -0
  81. package/src/protocol/v3-frame.js +209 -0
  82. package/src/proxy/multiplexed-relay.js +798 -0
  83. package/src/proxy/tcp-relay.js +1191 -0
  84. package/src/relay.js +3 -0
  85. package/src/reverse/client.js +721 -0
  86. package/src/reverse/common.js +88 -0
  87. package/src/reverse/gateway.js +663 -0
  88. package/src/reverse/hub.js +563 -0
  89. package/src/reverse/index.js +3 -0
  90. package/src/reverse/runtime.js +78 -0
  91. package/src/server/control-handler.js +170 -0
  92. package/src/server/primary.js +64 -0
  93. package/src/server/relay-runtime.js +124 -0
  94. package/src/server/worker.js +39 -0
  95. package/src/sessions/session.js +212 -0
  96. package/src/spool/spool-file.js +451 -0
  97. package/tensorbuzz.yml +36 -0
  98. package/test/e2e/gateway-process.test.js +262 -0
  99. package/test/e2e/reverse-gateway-process.test.js +285 -0
  100. package/test/fixtures/logging/enospc-matrix.mjs +238 -0
  101. package/test/fixtures/logging/enospc-recovery.mjs +146 -0
  102. package/test/fixtures/logging/process-writer.mjs +20 -0
  103. package/test/helpers/tls-certificates.js +115 -0
  104. package/test/integration/initial-target-recovery.test.js +518 -0
  105. package/test/integration/logging-recovery.test.js +163 -0
  106. package/test/integration/multiplexed-compatibility.test.js +254 -0
  107. package/test/integration/multiplexed-corrections.test.js +551 -0
  108. package/test/integration/multiplexed-failures.test.js +339 -0
  109. package/test/integration/multiplexed-transport-set.test.js +456 -0
  110. package/test/integration/multiplexed-transport.test.js +208 -0
  111. package/test/integration/reverse-gateway.test.js +1293 -0
  112. package/test/integration/socketduct-http-agent.test.js +435 -0
  113. package/test/integration/socketduct-https-agent.test.js +669 -0
  114. package/test/integration/stream-compression.test.js +1267 -0
  115. package/test/integration/tcp-forwarding.test.js +2878 -0
  116. package/test/integration/tls-forwarding.test.js +748 -0
  117. package/test/unit/auth/auth.test.js +64 -0
  118. package/test/unit/auth/static-token-store.test.js +14 -0
  119. package/test/unit/client/connect.test.js +58 -0
  120. package/test/unit/client/multiplexed-spool-budget.test.js +309 -0
  121. package/test/unit/client/multiplexed-transport-set.test.js +906 -0
  122. package/test/unit/client/socketduct-socket.test.js +1124 -0
  123. package/test/unit/client/virtual-socket.test.js +98 -0
  124. package/test/unit/config/docker-gateway-files.test.js +274 -0
  125. package/test/unit/config/environment.test.js +507 -0
  126. package/test/unit/index.test.js +78 -0
  127. package/test/unit/logging/structured-logger.test.js +674 -0
  128. package/test/unit/protocol/compression.test.js +421 -0
  129. package/test/unit/protocol/frame.test.js +336 -0
  130. package/test/unit/protocol/v3-compression.test.js +31 -0
  131. package/test/unit/protocol/v3-frame.test.js +57 -0
  132. package/test/unit/release-configuration.test.js +21 -0
  133. package/test/unit/server/control-handler.test.js +291 -0
  134. package/test/unit/server/primary.test.js +59 -0
  135. package/test/unit/server/relay-runtime.test.js +434 -0
  136. package/test/unit/sessions/session.test.js +84 -0
  137. package/test/unit/spool/spool-file.test.js +295 -0
  138. package/tsconfig.json +20 -0
@@ -0,0 +1,2878 @@
1
+ import assert from "node:assert/strict"
2
+ import {mkdtemp, rm} from "node:fs/promises"
3
+ import {tmpdir} from "node:os"
4
+ import {join} from "node:path"
5
+ import {createConnection, createServer} from "node:net"
6
+ import zlib from "node:zlib"
7
+ import test from "node:test"
8
+
9
+ import {InMemoryUserStore, login, TokenStore} from "../../src/auth/user-store.js"
10
+ import {connectSocketduct} from "../../src/client/socketduct-socket.js"
11
+ import {startTcpEdge} from "../../src/edge/tcp-edge.js"
12
+ import {encodeCompactAck, encodeCompactData, encodeFrame as encodeEnvelope, FrameDecoder} from "../../src/protocol/frame.js"
13
+ import {zstdAvailable} from "../../src/protocol/compression.js"
14
+ import {startTcpRelay} from "../../src/proxy/tcp-relay.js"
15
+ import {SessionStore} from "../../src/sessions/session.js"
16
+ import {SessionSpool} from "../../src/spool/spool-file.js"
17
+
18
+ /**
19
+ * Encode controls as envelopes and post-handshake stream records as compact V2.
20
+ * @param {import('../../src/protocol/frame.js').FrameHeader} header - Frame metadata.
21
+ * @param {Buffer} [payload] - Frame payload.
22
+ * @returns {Buffer} Encoded wire bytes.
23
+ */
24
+ function encodeFrame(header, payload = Buffer.alloc(0)) {
25
+ if (header.type === "stream.data") {
26
+ return encodeCompactData({
27
+ sequence: /** @type {number} */ (header.sequence),
28
+ payload,
29
+ ...(header.encoding === "zstd" ? {dataLength: /** @type {number} */ (header.dataLength)} : {})
30
+ })
31
+ }
32
+ if (header.type === "stream.ack") return encodeCompactAck(/** @type {number} */ (header.ackSequence))
33
+ const framedHeader = header.type === "session.create" || header.type === "session.resume"
34
+ ? {...header, framing: header.framing ?? [2]}
35
+ : header
36
+ return encodeEnvelope(framedHeader, payload)
37
+ }
38
+
39
+ /**
40
+ * @param {import('node:net').Server} server - Server to bind.
41
+ * @returns {Promise<number>} - Bound port.
42
+ */
43
+ async function listen(server) {
44
+ await new Promise((resolve, reject) => {
45
+ server.once("error", reject)
46
+ server.listen(0, "127.0.0.1", () => {
47
+ server.off("error", reject)
48
+ resolve(undefined)
49
+ })
50
+ })
51
+ const address = server.address()
52
+ if (address === null || typeof address === "string") {
53
+ throw new Error("Server did not bind to a TCP port")
54
+ }
55
+ return /** @type {import('node:net').AddressInfo} */ (address).port
56
+ }
57
+
58
+ /**
59
+ * @param {import('node:net').Socket} socket - Socket to read from.
60
+ * @returns {Promise<Buffer>} - Next data chunk.
61
+ */
62
+ async function readOnce(socket) {
63
+ return new Promise((resolve) => {
64
+ socket.once("data", (chunk) => resolve(chunk))
65
+ })
66
+ }
67
+
68
+ /**
69
+ * @param {import('node:net').Socket} socket
70
+ * @param {number} [timeoutMs]
71
+ * @returns {Promise<Buffer>} Next socket data.
72
+ */
73
+ async function readOnceWithin(socket, timeoutMs = 1_000) {
74
+ return await new Promise((resolve, reject) => {
75
+ /** @param {Buffer} chunk */
76
+ const onData = (chunk) => {
77
+ clearTimeout(timer)
78
+ resolve(chunk)
79
+ }
80
+ const timer = setTimeout(() => {
81
+ socket.off("data", onData)
82
+ reject(new Error("timed out waiting for socket data"))
83
+ }, timeoutMs)
84
+ socket.once("data", onData)
85
+ })
86
+ }
87
+
88
+ /**
89
+ * @param {import('node:net').Socket} socket - Socket to wait on.
90
+ * @returns {Promise<void>} - Resolves after connection.
91
+ */
92
+ async function waitConnect(socket) {
93
+ await new Promise((resolve, reject) => {
94
+ socket.once("error", reject)
95
+ socket.once("connect", () => {
96
+ socket.off("error", reject)
97
+ resolve(undefined)
98
+ })
99
+ })
100
+ }
101
+
102
+ /**
103
+ * @param {import('node:net').Socket} socket - Socket to wait on.
104
+ * @returns {Promise<void>} - Resolves when closed.
105
+ */
106
+ async function waitClose(socket) {
107
+ if (socket.destroyed) {
108
+ return
109
+ }
110
+ await new Promise((resolve) => {
111
+ socket.once("close", () => resolve(undefined))
112
+ })
113
+ }
114
+
115
+ /**
116
+ * @param {() => boolean} predicate - Condition to await.
117
+ * @param {number} [timeoutMs] - Maximum wait.
118
+ * @returns {Promise<void>} Resolves when the condition is true.
119
+ */
120
+ async function waitUntil(predicate, timeoutMs = 1_000) {
121
+ const deadline = Date.now() + timeoutMs
122
+ while (!predicate()) {
123
+ if (Date.now() >= deadline) throw new Error("timed out waiting for condition")
124
+ await new Promise((resolve) => setTimeout(resolve, 5))
125
+ }
126
+ }
127
+
128
+ test("TCP edge relays traffic and replays spooled client data after relay reconnect", async () => {
129
+ const tempDir = await mkdtemp(join(tmpdir(), "socketduct-forwarding-test-"))
130
+ const echoServer = createServer((socket) => {
131
+ socket.on("data", (chunk) => socket.write(chunk))
132
+ })
133
+ const targetPort = await listen(echoServer)
134
+
135
+ const users = new InMemoryUserStore()
136
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
137
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
138
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
139
+
140
+ const relay = await startTcpRelay({
141
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
142
+ tokens,
143
+ sessions: new SessionStore({now: () => 1000})
144
+ })
145
+ const edge = await startTcpEdge({
146
+ relayHost: "127.0.0.1",
147
+ relayPort: relay.port,
148
+ token: token.value,
149
+ sessionName: "integration session",
150
+ spool: new SessionSpool({rootDir: tempDir}),
151
+ reconnectDelayMs: 10,
152
+ target: {host: "127.0.0.1", port: targetPort}
153
+ })
154
+
155
+ const appSocket = createConnection({host: "127.0.0.1", port: edge.port})
156
+ await waitConnect(appSocket)
157
+ appSocket.write("first")
158
+ assert.equal((await readOnce(appSocket)).toString("utf8"), "first")
159
+
160
+ await relay.closeActiveTunnel()
161
+ appSocket.write("second")
162
+ assert.equal((await readOnce(appSocket)).toString("utf8"), "second")
163
+
164
+ appSocket.destroy()
165
+ await edge.close()
166
+ await relay.close()
167
+ await new Promise((resolve, reject) => {
168
+ echoServer.close((error) => {
169
+ if (error) {
170
+ reject(error)
171
+ return
172
+ }
173
+ resolve(undefined)
174
+ })
175
+ })
176
+ await rm(tempDir, {recursive: true, force: true})
177
+ })
178
+
179
+ test("TCP edge includes the requested target in session create and relay connects to it", async () => {
180
+ const tempDir = await mkdtemp(join(tmpdir(), "socketduct-target-create-test-"))
181
+ let targetConnections = 0
182
+ const targetServer = createServer((socket) => {
183
+ targetConnections += 1
184
+ socket.on("data", (chunk) => socket.write(chunk))
185
+ })
186
+ const targetPort = await listen(targetServer)
187
+
188
+ const users = new InMemoryUserStore()
189
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
190
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
191
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
192
+ const sessions = new SessionStore({now: () => 1000})
193
+ /** @type {Array<{host: string, port: number}>} */
194
+ const authorizedTargets = []
195
+
196
+ const relay = await startTcpRelay({
197
+ authorizeTarget: ({target}) => {
198
+ authorizedTargets.push(target)
199
+ return target.host === "127.0.0.1" && target.port === targetPort
200
+ },
201
+ tokens,
202
+ sessions
203
+ })
204
+ const edge = await startTcpEdge({
205
+ relayHost: "127.0.0.1",
206
+ relayPort: relay.port,
207
+ token: token.value,
208
+ sessionName: "target create session",
209
+ target: {host: "127.0.0.1", port: targetPort},
210
+ spool: new SessionSpool({rootDir: tempDir}),
211
+ reconnectDelayMs: 10
212
+ })
213
+
214
+ const appSocket = createConnection({host: "127.0.0.1", port: edge.port})
215
+ await waitConnect(appSocket)
216
+ appSocket.write("target-ok")
217
+ assert.equal((await readOnce(appSocket)).toString("utf8"), "target-ok")
218
+
219
+ assert.deepEqual(authorizedTargets, [{host: "127.0.0.1", port: targetPort}])
220
+ assert.equal(targetConnections, 1)
221
+
222
+ appSocket.destroy()
223
+ await edge.close()
224
+ await relay.close()
225
+ await new Promise((resolve, reject) => {
226
+ targetServer.close((error) => {
227
+ if (error) {
228
+ reject(error)
229
+ return
230
+ }
231
+ resolve(undefined)
232
+ })
233
+ })
234
+ await rm(tempDir, {recursive: true, force: true})
235
+ })
236
+
237
+ test("TCP relay rejects unauthorized targets during session create", async () => {
238
+ const users = new InMemoryUserStore()
239
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
240
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
241
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
242
+ const relay = await startTcpRelay({
243
+ authorizeTarget: () => false,
244
+ tokens,
245
+ sessions: new SessionStore({now: () => 1000})
246
+ })
247
+
248
+ const socket = createConnection({host: "127.0.0.1", port: relay.port})
249
+ const decoder = new FrameDecoder({compactDirection: "target-to-client"})
250
+ await waitConnect(socket)
251
+ socket.write(encodeFrame({
252
+ type: "session.create",
253
+ token: token.value,
254
+ name: "blocked",
255
+ target: {host: "127.0.0.1", port: 9}
256
+ }))
257
+ const [frame] = decoder.push(await readOnce(socket))
258
+ assert.equal(frame?.header.type, "error")
259
+ assert.equal(frame?.header.code, "TARGET_UNAUTHORIZED")
260
+
261
+ socket.destroy()
262
+ await relay.close()
263
+ })
264
+
265
+ test("TCP relay keeps target socket alive across tunnel reconnect", async () => {
266
+ let targetConnections = 0
267
+ const targetServer = createServer((socket) => {
268
+ targetConnections += 1
269
+ socket.on("data", (chunk) => socket.write(chunk))
270
+ })
271
+ const targetPort = await listen(targetServer)
272
+
273
+ const users = new InMemoryUserStore()
274
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
275
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
276
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
277
+
278
+ const relay = await startTcpRelay({
279
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
280
+ tokens,
281
+ sessions: new SessionStore({now: () => 1000}),
282
+ resumeWindowMs: 1_000
283
+ })
284
+
285
+ const firstTunnel = createConnection({host: "127.0.0.1", port: relay.port})
286
+ const firstDecoder = new FrameDecoder({compactDirection: "target-to-client"})
287
+ await waitConnect(firstTunnel)
288
+ firstTunnel.write(encodeFrame({
289
+ type: "session.create",
290
+ token: token.value,
291
+ name: "retention-test",
292
+ identifier: "session-retained-target",
293
+ target: {host: "127.0.0.1", port: targetPort}
294
+ }))
295
+ firstDecoder.push(await readOnce(firstTunnel))
296
+ firstTunnel.write(encodeFrame({type: "stream.data", direction: "client-to-target", sequence: 5}, Buffer.from("first")))
297
+ await readOnce(firstTunnel)
298
+ assert.equal(targetConnections, 1)
299
+
300
+ relay.closeActiveTunnel()
301
+ await waitClose(firstTunnel)
302
+
303
+ const secondTunnel = createConnection({host: "127.0.0.1", port: relay.port})
304
+ const secondDecoder = new FrameDecoder({compactDirection: "target-to-client"})
305
+ await waitConnect(secondTunnel)
306
+ secondTunnel.write(encodeFrame({type: "session.resume", token: token.value, identifier: "session-retained-target"}))
307
+ secondDecoder.push(await readOnce(secondTunnel))
308
+ secondTunnel.write(encodeFrame({type: "stream.data", direction: "client-to-target", sequence: 11}, Buffer.from("second")))
309
+
310
+ let echoed
311
+ while (echoed === undefined) {
312
+ for (const frame of secondDecoder.push(await readOnce(secondTunnel))) {
313
+ if (frame.header.type === "stream.data") {
314
+ echoed = frame.payload.toString("utf8")
315
+ }
316
+ }
317
+ }
318
+
319
+ assert.equal(echoed, "second")
320
+ assert.equal(targetConnections, 1)
321
+
322
+ secondTunnel.destroy()
323
+ await relay.close()
324
+ await new Promise((resolve, reject) => {
325
+ targetServer.close((error) => {
326
+ if (error) {
327
+ reject(error)
328
+ return
329
+ }
330
+ resolve(undefined)
331
+ })
332
+ })
333
+ })
334
+
335
+ test("TCP relay rejects stream data before session authentication", async () => {
336
+ const echoServer = createServer((socket) => {
337
+ socket.on("data", (chunk) => socket.write(chunk))
338
+ })
339
+ const targetPort = await listen(echoServer)
340
+
341
+ const relay = await startTcpRelay({
342
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
343
+ tokens: new TokenStore(),
344
+ sessions: new SessionStore()
345
+ })
346
+
347
+ const socket = createConnection({host: "127.0.0.1", port: relay.port})
348
+ const decoder = new FrameDecoder({compactDirection: "target-to-client"})
349
+ await waitConnect(socket)
350
+ socket.write(encodeFrame({
351
+ type: "stream.data",
352
+ direction: "client-to-target",
353
+ sequence: 1
354
+ }, Buffer.from("blocked")))
355
+
356
+ const frames = decoder.push(await readOnce(socket))
357
+ const [frame] = frames
358
+ if (frame === undefined) {
359
+ throw new Error("Relay did not return an error frame")
360
+ }
361
+ assert.equal(frame.header.type, "error")
362
+ assert.equal(frame.header.code, "AUTH_REQUIRED")
363
+
364
+ socket.destroy()
365
+ await relay.close()
366
+ await new Promise((resolve, reject) => {
367
+ echoServer.close((error) => {
368
+ if (error) {
369
+ reject(error)
370
+ return
371
+ }
372
+ resolve(undefined)
373
+ })
374
+ })
375
+ })
376
+
377
+ test("TCP relay closes malformed frames without crashing the server", async () => {
378
+ const echoServer = createServer((socket) => {
379
+ socket.on("data", (chunk) => socket.write(chunk))
380
+ })
381
+ const targetPort = await listen(echoServer)
382
+
383
+ const relay = await startTcpRelay({
384
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
385
+ tokens: new TokenStore(),
386
+ sessions: new SessionStore()
387
+ })
388
+
389
+ const malformed = createConnection({host: "127.0.0.1", port: relay.port})
390
+ await waitConnect(malformed)
391
+ malformed.write(Buffer.alloc(4))
392
+ await new Promise((resolve) => { setTimeout(resolve, 20) })
393
+ malformed.destroy()
394
+
395
+ const healthy = createConnection({host: "127.0.0.1", port: relay.port})
396
+ const decoder = new FrameDecoder({compactDirection: "target-to-client"})
397
+ await waitConnect(healthy)
398
+ healthy.write(encodeFrame({type: "stream.data", direction: "client-to-target", sequence: 1}, Buffer.from("blocked")))
399
+ const [frame] = decoder.push(await readOnce(healthy))
400
+ if (frame === undefined) {
401
+ throw new Error("Relay did not return an error frame after malformed input")
402
+ }
403
+ assert.equal(frame.header.type, "error")
404
+ assert.equal(frame.header.code, "AUTH_REQUIRED")
405
+
406
+ healthy.destroy()
407
+ await relay.close()
408
+ await new Promise((resolve, reject) => {
409
+ echoServer.close((error) => {
410
+ if (error) {
411
+ reject(error)
412
+ return
413
+ }
414
+ resolve(undefined)
415
+ })
416
+ })
417
+ })
418
+
419
+ test("TCP relay persists committed client sequence before acknowledging writes", async () => {
420
+ const echoServer = createServer((socket) => {
421
+ socket.resume()
422
+ })
423
+ const targetPort = await listen(echoServer)
424
+
425
+ const users = new InMemoryUserStore()
426
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
427
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
428
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
429
+ const sessions = new SessionStore({now: () => 1000})
430
+
431
+ const relay = await startTcpRelay({
432
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
433
+ tokens,
434
+ sessions
435
+ })
436
+
437
+ const socket = createConnection({host: "127.0.0.1", port: relay.port})
438
+ const decoder = new FrameDecoder({compactDirection: "target-to-client"})
439
+ await waitConnect(socket)
440
+ socket.write(encodeFrame({type: "session.create", token: token.value, name: "seq-test", identifier: "session-seq", target: {host: "127.0.0.1", port: targetPort}}))
441
+ decoder.push(await readOnce(socket))
442
+ socket.write(encodeFrame({type: "stream.data", direction: "client-to-target", session: "session-seq", sequence: 5}, Buffer.from("hello")))
443
+
444
+ let acked = false
445
+ while (!acked) {
446
+ for (const frame of decoder.push(await readOnce(socket))) {
447
+ if (frame.header.type === "stream.ack" && frame.header.direction === "client-to-target") {
448
+ acked = true
449
+ }
450
+ }
451
+ }
452
+
453
+ assert.equal(sessions.get("session-seq")?.clientSequence, 5)
454
+
455
+ socket.destroy()
456
+ await relay.close()
457
+ await new Promise((resolve, reject) => {
458
+ echoServer.close((error) => {
459
+ if (error) {
460
+ reject(error)
461
+ return
462
+ }
463
+ resolve(undefined)
464
+ })
465
+ })
466
+ })
467
+
468
+ test("TCP relay buffers target bytes while a resumable tunnel is down", async () => {
469
+ /** @type {import('node:net').Socket | undefined} */
470
+ let targetSocket
471
+ const targetServer = createServer((socket) => {
472
+ targetSocket = socket
473
+ socket.resume()
474
+ })
475
+ const targetPort = await listen(targetServer)
476
+
477
+ const users = new InMemoryUserStore()
478
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
479
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
480
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
481
+ const sessions = new SessionStore({now: () => 1000})
482
+ const relay = await startTcpRelay({
483
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
484
+ tokens,
485
+ sessions,
486
+ resumeWindowMs: 1_000
487
+ })
488
+
489
+ const firstTunnel = createConnection({host: "127.0.0.1", port: relay.port})
490
+ const firstDecoder = new FrameDecoder({compactDirection: "target-to-client"})
491
+ await waitConnect(firstTunnel)
492
+ firstTunnel.write(encodeFrame({type: "session.create", token: token.value, name: "buffer-test", identifier: "session-buffer", target: {host: "127.0.0.1", port: targetPort}}))
493
+ firstDecoder.push(await readOnce(firstTunnel))
494
+
495
+ relay.closeActiveTunnel()
496
+ await waitClose(firstTunnel)
497
+ targetSocket?.write("during-drop")
498
+
499
+ const secondTunnel = createConnection({host: "127.0.0.1", port: relay.port})
500
+ const secondDecoder = new FrameDecoder({compactDirection: "target-to-client"})
501
+ await waitConnect(secondTunnel)
502
+ secondTunnel.write(encodeFrame({type: "session.resume", token: token.value, identifier: "session-buffer"}))
503
+
504
+ let received
505
+ while (received === undefined) {
506
+ for (const frame of secondDecoder.push(await readOnce(secondTunnel))) {
507
+ if (frame.header.type === "stream.data") {
508
+ received = frame.payload.toString("utf8")
509
+ }
510
+ }
511
+ }
512
+
513
+ assert.equal(received, "during-drop")
514
+ assert.equal(sessions.get("session-buffer")?.relaySequence, "during-drop".length)
515
+
516
+ secondTunnel.destroy()
517
+ await relay.close()
518
+ await new Promise((resolve, reject) => {
519
+ targetServer.close((error) => {
520
+ if (error) {
521
+ reject(error)
522
+ return
523
+ }
524
+ resolve(undefined)
525
+ })
526
+ })
527
+ })
528
+
529
+ test("TCP relay ignores stale tunnel data after session resume", async () => {
530
+ /** @type {string[]} */
531
+ const targetWrites = []
532
+ const targetServer = createServer((socket) => {
533
+ socket.on("data", (chunk) => {
534
+ targetWrites.push(chunk.toString("utf8"))
535
+ socket.write(chunk)
536
+ })
537
+ })
538
+ const targetPort = await listen(targetServer)
539
+
540
+ const users = new InMemoryUserStore()
541
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
542
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
543
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
544
+ const sessions = new SessionStore({now: () => 1000})
545
+ const relay = await startTcpRelay({
546
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
547
+ tokens,
548
+ sessions,
549
+ resumeWindowMs: 1_000
550
+ })
551
+
552
+ const tunnelA = createConnection({host: "127.0.0.1", port: relay.port})
553
+ const decoderA = new FrameDecoder({compactDirection: "target-to-client"})
554
+ await waitConnect(tunnelA)
555
+ tunnelA.write(encodeFrame({
556
+ type: "session.create",
557
+ token: token.value,
558
+ name: "authority-test",
559
+ identifier: "session-authority",
560
+ target: {host: "127.0.0.1", port: targetPort}
561
+ }))
562
+ decoderA.push(await readOnce(tunnelA))
563
+ tunnelA.write(encodeFrame({type: "stream.data", direction: "client-to-target", sequence: 5}, Buffer.from("first")))
564
+ let firstEcho
565
+ while (firstEcho === undefined) {
566
+ for (const frame of decoderA.push(await readOnce(tunnelA))) {
567
+ if (frame.header.type === "stream.data") {
568
+ firstEcho = frame.payload.toString("utf8")
569
+ }
570
+ }
571
+ }
572
+ assert.equal(firstEcho, "first")
573
+
574
+ const tunnelB = createConnection({host: "127.0.0.1", port: relay.port})
575
+ const decoderB = new FrameDecoder({compactDirection: "target-to-client"})
576
+ await waitConnect(tunnelB)
577
+ tunnelB.write(encodeFrame({type: "session.resume", token: token.value, identifier: "session-authority"}))
578
+ decoderB.push(await readOnce(tunnelB))
579
+
580
+ tunnelA.write(encodeFrame({type: "stream.data", direction: "client-to-target", sequence: 999}, Buffer.from("stale")))
581
+
582
+ const staleResponse = await Promise.race([
583
+ readOnce(tunnelA).then(() => "data"),
584
+ new Promise((resolve) => setTimeout(() => resolve("timeout"), 200))
585
+ ])
586
+ assert.equal(staleResponse, "timeout", "stale tunnel must not receive ack")
587
+
588
+ tunnelB.write(encodeFrame({type: "stream.data", direction: "client-to-target", sequence: 10}, Buffer.from("legit")))
589
+ let legitEcho
590
+ while (legitEcho === undefined) {
591
+ for (const frame of decoderB.push(await readOnce(tunnelB))) {
592
+ if (frame.header.type === "stream.data") {
593
+ legitEcho = frame.payload.toString("utf8")
594
+ }
595
+ }
596
+ }
597
+ assert.equal(legitEcho, "legit")
598
+
599
+ assert.deepEqual(targetWrites, ["first", "legit"])
600
+ assert.equal(sessions.get("session-authority")?.clientSequence, 10)
601
+
602
+ tunnelA.destroy()
603
+ tunnelB.write(encodeFrame({type: "stream.data", direction: "client-to-target", sequence: 21}, Buffer.from("after-close")))
604
+ let afterCloseEcho
605
+ while (afterCloseEcho === undefined) {
606
+ for (const frame of decoderB.push(await readOnce(tunnelB))) {
607
+ if (frame.header.type === "stream.data") {
608
+ afterCloseEcho = frame.payload.toString("utf8")
609
+ }
610
+ }
611
+ }
612
+ assert.equal(afterCloseEcho, "after-close")
613
+ assert.deepEqual(targetWrites, ["first", "legit", "after-close"])
614
+
615
+ tunnelB.destroy()
616
+ await relay.close()
617
+ await new Promise((resolve, reject) => {
618
+ targetServer.close((error) => {
619
+ if (error) {
620
+ reject(error)
621
+ return
622
+ }
623
+ resolve(undefined)
624
+ })
625
+ })
626
+ })
627
+
628
+ test("TCP relay ignores stale tunnel target-to-client ACK after session resume", async () => {
629
+ const echoServer = createServer((socket) => {
630
+ socket.on("data", (chunk) => socket.write(chunk))
631
+ })
632
+ const targetPort = await listen(echoServer)
633
+
634
+ const users = new InMemoryUserStore()
635
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
636
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
637
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
638
+ const sessions = new SessionStore({now: () => 1000})
639
+
640
+ const relay = await startTcpRelay({
641
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
642
+ tokens,
643
+ sessions,
644
+ resumeWindowMs: 2_000
645
+ })
646
+
647
+ const tunnel1 = createConnection({host: "127.0.0.1", port: relay.port})
648
+ const decoder1 = new FrameDecoder({compactDirection: "target-to-client"})
649
+ await waitConnect(tunnel1)
650
+ tunnel1.write(encodeFrame({
651
+ type: "session.create",
652
+ token: token.value,
653
+ name: "stale-ack-test",
654
+ identifier: "session-stale-ack",
655
+ target: {host: "127.0.0.1", port: targetPort}
656
+ }))
657
+ decoder1.push(await readOnce(tunnel1))
658
+
659
+ tunnel1.write(encodeFrame({type: "stream.data", direction: "client-to-target", sequence: 5}, Buffer.from("alpha")))
660
+ /** @type {string[]} */
661
+ const tunnel1Chunks = []
662
+ while (tunnel1Chunks.length < 1) {
663
+ for (const frame of decoder1.push(await readOnce(tunnel1))) {
664
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
665
+ tunnel1Chunks.push(frame.payload.toString("utf8"))
666
+ }
667
+ }
668
+ }
669
+
670
+ tunnel1.write(encodeFrame({type: "stream.data", direction: "client-to-target", sequence: 10}, Buffer.from("bravo")))
671
+ while (tunnel1Chunks.length < 2) {
672
+ for (const frame of decoder1.push(await readOnce(tunnel1))) {
673
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
674
+ tunnel1Chunks.push(frame.payload.toString("utf8"))
675
+ }
676
+ }
677
+ }
678
+
679
+ const tunnel2 = createConnection({host: "127.0.0.1", port: relay.port})
680
+ const decoder2 = new FrameDecoder({compactDirection: "target-to-client"})
681
+ await waitConnect(tunnel2)
682
+ tunnel2.write(encodeFrame({type: "session.resume", token: token.value, identifier: "session-stale-ack"}))
683
+
684
+ let resumed2 = false
685
+ const replayed2 = []
686
+ while (!resumed2 || replayed2.length < 2) {
687
+ for (const frame of decoder2.push(await readOnce(tunnel2))) {
688
+ if (frame.header.type === "session.resumed") {
689
+ resumed2 = true
690
+ }
691
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
692
+ replayed2.push(frame.payload.toString("utf8"))
693
+ }
694
+ }
695
+ }
696
+ assert.deepEqual(replayed2, ["alpha", "bravo"])
697
+
698
+ const bravoRelaySeq = sessions.get("session-stale-ack")?.relaySequence ?? 0
699
+ assert.ok(bravoRelaySeq > 0, "relaySequence must be > 0 after two echoed chunks")
700
+
701
+ tunnel1.write(encodeFrame({type: "stream.ack", direction: "target-to-client", ackSequence: bravoRelaySeq}))
702
+
703
+ await new Promise((resolve) => { setTimeout(resolve, 100) })
704
+
705
+ tunnel2.destroy()
706
+ await waitClose(tunnel2)
707
+
708
+ const tunnel3 = createConnection({host: "127.0.0.1", port: relay.port})
709
+ const decoder3 = new FrameDecoder({compactDirection: "target-to-client"})
710
+ await waitConnect(tunnel3)
711
+ tunnel3.write(encodeFrame({type: "session.resume", token: token.value, identifier: "session-stale-ack"}))
712
+
713
+ let resumed3 = false
714
+ const replayed3 = []
715
+ while (!resumed3 || replayed3.length < 2) {
716
+ for (const frame of decoder3.push(await readOnce(tunnel3))) {
717
+ if (frame.header.type === "session.resumed") {
718
+ resumed3 = true
719
+ }
720
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
721
+ replayed3.push(frame.payload.toString("utf8"))
722
+ }
723
+ }
724
+ }
725
+ assert.deepEqual(replayed3, ["alpha", "bravo"])
726
+
727
+ tunnel1.destroy()
728
+ tunnel3.destroy()
729
+ await relay.close()
730
+ await new Promise((resolve, reject) => {
731
+ echoServer.close((error) => {
732
+ if (error) {
733
+ reject(error)
734
+ return
735
+ }
736
+ resolve(undefined)
737
+ })
738
+ })
739
+ })
740
+
741
+ test("TCP edge closes the app socket when relay rejects the session", async () => {
742
+ const echoServer = createServer((socket) => {
743
+ socket.on("data", (chunk) => socket.write(chunk))
744
+ })
745
+ const targetPort = await listen(echoServer)
746
+ const relay = await startTcpRelay({
747
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
748
+ tokens: new TokenStore(),
749
+ sessions: new SessionStore()
750
+ })
751
+ const tempDir = await mkdtemp(join(tmpdir(), "socketduct-invalid-token-test-"))
752
+ const edge = await startTcpEdge({
753
+ relayHost: "127.0.0.1",
754
+ relayPort: relay.port,
755
+ token: "invalid-token",
756
+ sessionName: "invalid session",
757
+ spool: new SessionSpool({rootDir: tempDir}),
758
+ reconnectDelayMs: 10,
759
+ target: {host: "127.0.0.1", port: targetPort}
760
+ })
761
+
762
+ const appSocket = createConnection({host: "127.0.0.1", port: edge.port})
763
+ await waitConnect(appSocket)
764
+ appSocket.write("blocked")
765
+ await waitClose(appSocket)
766
+
767
+ await edge.close()
768
+ await relay.close()
769
+ await new Promise((resolve, reject) => {
770
+ echoServer.close((error) => {
771
+ if (error) {
772
+ reject(error)
773
+ return
774
+ }
775
+ resolve(undefined)
776
+ })
777
+ })
778
+ await rm(tempDir, {recursive: true, force: true})
779
+ })
780
+
781
+ test("TCP relay replays target-to-client data not acknowledged before tunnel disconnect", async () => {
782
+ const echoServer = createServer((socket) => {
783
+ socket.on("data", (chunk) => socket.write(chunk))
784
+ })
785
+ const targetPort = await listen(echoServer)
786
+
787
+ const users = new InMemoryUserStore()
788
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
789
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
790
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
791
+ const sessions = new SessionStore({now: () => 1000})
792
+
793
+ const relay = await startTcpRelay({
794
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
795
+ tokens,
796
+ sessions,
797
+ resumeWindowMs: 2_000
798
+ })
799
+
800
+ const tunnel1 = createConnection({host: "127.0.0.1", port: relay.port})
801
+ const decoder1 = new FrameDecoder({compactDirection: "target-to-client"})
802
+ await waitConnect(tunnel1)
803
+ tunnel1.write(encodeFrame({
804
+ type: "session.create",
805
+ token: token.value,
806
+ name: "replay-test",
807
+ identifier: "session-replay",
808
+ target: {host: "127.0.0.1", port: targetPort}
809
+ }))
810
+ decoder1.push(await readOnce(tunnel1))
811
+
812
+ tunnel1.write(encodeFrame({type: "stream.data", direction: "client-to-target", sequence: 5}, Buffer.from("alpha")))
813
+ let alphaEcho
814
+ while (alphaEcho === undefined) {
815
+ for (const frame of decoder1.push(await readOnce(tunnel1))) {
816
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
817
+ alphaEcho = frame.payload.toString("utf8")
818
+ }
819
+ }
820
+ }
821
+ assert.equal(alphaEcho, "alpha")
822
+
823
+ tunnel1.write(encodeFrame({type: "stream.data", direction: "client-to-target", sequence: 10}, Buffer.from("bravo")))
824
+ let bravoEcho
825
+ while (bravoEcho === undefined) {
826
+ for (const frame of decoder1.push(await readOnce(tunnel1))) {
827
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
828
+ bravoEcho = frame.payload.toString("utf8")
829
+ }
830
+ }
831
+ }
832
+ assert.equal(bravoEcho, "bravo")
833
+
834
+ tunnel1.destroy()
835
+ await waitClose(tunnel1)
836
+
837
+ const tunnel2 = createConnection({host: "127.0.0.1", port: relay.port})
838
+ const decoder2 = new FrameDecoder({compactDirection: "target-to-client"})
839
+ await waitConnect(tunnel2)
840
+ tunnel2.write(encodeFrame({type: "session.resume", token: token.value, identifier: "session-replay"}))
841
+
842
+ let resumed
843
+ /** @type {string[]} */
844
+ const replayedChunks = []
845
+ while (resumed === undefined || replayedChunks.length < 2) {
846
+ for (const frame of decoder2.push(await readOnce(tunnel2))) {
847
+ if (frame.header.type === "session.resumed") {
848
+ resumed = true
849
+ }
850
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
851
+ replayedChunks.push(frame.payload.toString("utf8"))
852
+ }
853
+ }
854
+ }
855
+
856
+ assert.deepEqual(replayedChunks, ["alpha", "bravo"])
857
+ assert.equal(sessions.get("session-replay")?.relaySequence, 10)
858
+
859
+ tunnel2.destroy()
860
+ await relay.close()
861
+ await new Promise((resolve, reject) => {
862
+ echoServer.close((error) => {
863
+ if (error) {
864
+ reject(error)
865
+ return
866
+ }
867
+ resolve(undefined)
868
+ })
869
+ })
870
+ })
871
+
872
+ test("TCP relay delivers buffered data then session.close when target closes while tunnel is detached", async () => {
873
+ let targetConnectionCount = 0
874
+ /** @type {import('node:net').Socket | undefined} */
875
+ let heldTargetSocket
876
+ const targetServer = createServer((socket) => {
877
+ targetConnectionCount += 1
878
+ heldTargetSocket = socket
879
+ socket.on("data", (chunk) => socket.write(chunk))
880
+ })
881
+ const targetPort = await listen(targetServer)
882
+
883
+ const users = new InMemoryUserStore()
884
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
885
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
886
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
887
+ const sessions = new SessionStore({now: () => 1000})
888
+
889
+ const relay = await startTcpRelay({
890
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
891
+ tokens,
892
+ sessions,
893
+ resumeWindowMs: 2_000
894
+ })
895
+
896
+ const tunnel1 = createConnection({host: "127.0.0.1", port: relay.port})
897
+ const decoder1 = new FrameDecoder({compactDirection: "target-to-client"})
898
+ await waitConnect(tunnel1)
899
+ tunnel1.write(encodeFrame({
900
+ type: "session.create",
901
+ token: token.value,
902
+ name: "eof-test",
903
+ identifier: "session-eof",
904
+ target: {host: "127.0.0.1", port: targetPort}
905
+ }))
906
+ decoder1.push(await readOnce(tunnel1))
907
+ tunnel1.write(encodeFrame({type: "stream.data", direction: "client-to-target", sequence: 4}, Buffer.from("ping")))
908
+ let pingEcho
909
+ while (pingEcho === undefined) {
910
+ for (const frame of decoder1.push(await readOnce(tunnel1))) {
911
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
912
+ pingEcho = frame.payload.toString("utf8")
913
+ }
914
+ }
915
+ }
916
+ assert.equal(pingEcho, "ping")
917
+ assert.equal(targetConnectionCount, 1)
918
+
919
+ relay.closeActiveTunnel()
920
+ await waitClose(tunnel1)
921
+
922
+ if (heldTargetSocket !== undefined) {
923
+ heldTargetSocket.write("final")
924
+ heldTargetSocket.end()
925
+ await waitClose(heldTargetSocket)
926
+ }
927
+
928
+ const tunnel2 = createConnection({host: "127.0.0.1", port: relay.port})
929
+ const decoder2 = new FrameDecoder({compactDirection: "target-to-client"})
930
+ await waitConnect(tunnel2)
931
+ tunnel2.write(encodeFrame({type: "session.resume", token: token.value, identifier: "session-eof", targetToClientAck: 0}))
932
+
933
+ let resumed = false
934
+ /** @type {string[]} */
935
+ const targetChunks = []
936
+ let gotClose = false
937
+ while (!resumed || !gotClose) {
938
+ for (const frame of decoder2.push(await readOnce(tunnel2))) {
939
+ if (frame.header.type === "session.resumed") {
940
+ resumed = true
941
+ }
942
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
943
+ targetChunks.push(frame.payload.toString("utf8"))
944
+ }
945
+ if (frame.header.type === "session.close") {
946
+ gotClose = true
947
+ }
948
+ }
949
+ }
950
+
951
+ assert.deepEqual(targetChunks, ["ping", "final"])
952
+ assert.equal(gotClose, true)
953
+ assert.equal(targetConnectionCount, 1)
954
+
955
+ await new Promise((resolve) => { setTimeout(resolve, 200) })
956
+ assert.equal(targetConnectionCount, 1)
957
+
958
+ tunnel2.destroy()
959
+ await relay.close()
960
+ await new Promise((resolve, reject) => {
961
+ targetServer.close((error) => {
962
+ if (error) {
963
+ reject(error)
964
+ return
965
+ }
966
+ resolve(undefined)
967
+ })
968
+ })
969
+ })
970
+
971
+ test("TCP relay replays buffered target-to-client data within the per-session byte limit after reconnect", async () => {
972
+ /** @type {import('node:net').Socket | undefined} */
973
+ let targetSocket
974
+ const targetServer = createServer((socket) => {
975
+ targetSocket = socket
976
+ socket.resume()
977
+ })
978
+ const targetPort = await listen(targetServer)
979
+
980
+ const users = new InMemoryUserStore()
981
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
982
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
983
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
984
+ const sessions = new SessionStore({now: () => 1000})
985
+
986
+ const relay = await startTcpRelay({
987
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
988
+ tokens,
989
+ sessions,
990
+ resumeWindowMs: 2_000,
991
+ maxTargetBufferBytes: 100
992
+ })
993
+
994
+ const tunnel1 = createConnection({host: "127.0.0.1", port: relay.port})
995
+ const decoder1 = new FrameDecoder({compactDirection: "target-to-client"})
996
+ await waitConnect(tunnel1)
997
+ tunnel1.write(encodeFrame({
998
+ type: "session.create",
999
+ token: token.value,
1000
+ name: "buffer-limit-test",
1001
+ identifier: "session-buf-limit",
1002
+ target: {host: "127.0.0.1", port: targetPort}
1003
+ }))
1004
+ decoder1.push(await readOnce(tunnel1))
1005
+
1006
+ relay.closeActiveTunnel()
1007
+ await waitClose(tunnel1)
1008
+
1009
+ const withinLimitPayload = Buffer.alloc(80, 0x41)
1010
+ targetSocket?.write(withinLimitPayload)
1011
+
1012
+ const tunnel2 = createConnection({host: "127.0.0.1", port: relay.port})
1013
+ const decoder2 = new FrameDecoder({compactDirection: "target-to-client"})
1014
+ await waitConnect(tunnel2)
1015
+ tunnel2.write(encodeFrame({
1016
+ type: "session.resume",
1017
+ token: token.value,
1018
+ identifier: "session-buf-limit"
1019
+ }))
1020
+
1021
+ let resumed = false
1022
+ /** @type {Buffer[]} */
1023
+ const receivedChunks = []
1024
+ while (!resumed || receivedChunks.length < 1) {
1025
+ for (const frame of decoder2.push(await readOnce(tunnel2))) {
1026
+ if (frame.header.type === "session.resumed") {
1027
+ resumed = true
1028
+ }
1029
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
1030
+ receivedChunks.push(frame.payload)
1031
+ }
1032
+ }
1033
+ }
1034
+
1035
+ assert.equal(receivedChunks.length, 1)
1036
+ const firstChunk = /** @type {Buffer} */ (receivedChunks[0])
1037
+ assert.equal(firstChunk.length, 80)
1038
+ assert.deepEqual(firstChunk, withinLimitPayload)
1039
+
1040
+ tunnel2.destroy()
1041
+ await relay.close()
1042
+ await new Promise((resolve, reject) => {
1043
+ targetServer.close((error) => {
1044
+ if (error) {
1045
+ reject(error)
1046
+ return
1047
+ }
1048
+ resolve(undefined)
1049
+ })
1050
+ })
1051
+ })
1052
+
1053
+ test("TCP relay terminates session with nonretryable TARGET_BUFFER_OVERFLOW when per-session buffer limit is exceeded", async () => {
1054
+ /** @type {import('node:net').Socket | undefined} */
1055
+ let targetSocket
1056
+ const targetServer = createServer((socket) => {
1057
+ targetSocket = socket
1058
+ socket.resume()
1059
+ })
1060
+ const targetPort = await listen(targetServer)
1061
+
1062
+ const users = new InMemoryUserStore()
1063
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
1064
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
1065
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
1066
+ const sessions = new SessionStore({now: () => 1000})
1067
+
1068
+ const relay = await startTcpRelay({
1069
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
1070
+ tokens,
1071
+ sessions,
1072
+ resumeWindowMs: 2_000,
1073
+ maxTargetBufferBytes: 10
1074
+ })
1075
+
1076
+ const tunnel1 = createConnection({host: "127.0.0.1", port: relay.port})
1077
+ const decoder1 = new FrameDecoder({compactDirection: "target-to-client"})
1078
+ await waitConnect(tunnel1)
1079
+ tunnel1.write(encodeFrame({
1080
+ type: "session.create",
1081
+ token: token.value,
1082
+ name: "overflow-test",
1083
+ identifier: "session-overflow",
1084
+ target: {host: "127.0.0.1", port: targetPort}
1085
+ }))
1086
+ decoder1.push(await readOnce(tunnel1))
1087
+
1088
+ relay.closeActiveTunnel()
1089
+ await waitClose(tunnel1)
1090
+
1091
+ const overflowPayload = Buffer.alloc(128, 0x42)
1092
+ targetSocket?.write(overflowPayload)
1093
+
1094
+ await new Promise((resolve) => { setTimeout(resolve, 100) })
1095
+
1096
+ const tunnel2 = createConnection({host: "127.0.0.1", port: relay.port})
1097
+ const decoder2 = new FrameDecoder({compactDirection: "target-to-client"})
1098
+ await waitConnect(tunnel2)
1099
+ tunnel2.write(encodeFrame({
1100
+ type: "session.resume",
1101
+ token: token.value,
1102
+ identifier: "session-overflow"
1103
+ }))
1104
+
1105
+ let resumed = false
1106
+ /** @type {Array<{type: string, code?: string, retryable?: boolean}>} */
1107
+ const receivedFrames = []
1108
+ while (!resumed) {
1109
+ for (const frame of decoder2.push(await readOnce(tunnel2))) {
1110
+ receivedFrames.push(frame.header)
1111
+ if (frame.header.type === "session.resumed") {
1112
+ resumed = true
1113
+ }
1114
+ }
1115
+ }
1116
+
1117
+ const errorFrame = receivedFrames.find((f) => f.type === "error")
1118
+ assert.ok(errorFrame !== undefined, "expected an error frame after reconnect for overflow session")
1119
+ assert.equal(errorFrame?.code, "TARGET_BUFFER_OVERFLOW")
1120
+ assert.equal(errorFrame?.retryable, false)
1121
+
1122
+ tunnel2.destroy()
1123
+ await relay.close()
1124
+ await new Promise((resolve, reject) => {
1125
+ targetServer.close((error) => {
1126
+ if (error) {
1127
+ reject(error)
1128
+ return
1129
+ }
1130
+ resolve(undefined)
1131
+ })
1132
+ })
1133
+ })
1134
+
1135
+ test("TCP relay enforces process-wide total buffer limit across two detached sessions: first resumes, second gets TARGET_BUFFER_OVERFLOW", async () => {
1136
+ /** @type {import('node:net').Socket | undefined} */
1137
+ let targetSocket1
1138
+ const targetServer1 = createServer((socket) => {
1139
+ targetSocket1 = socket
1140
+ socket.resume()
1141
+ })
1142
+ const targetPort1 = await listen(targetServer1)
1143
+
1144
+ /** @type {import('node:net').Socket | undefined} */
1145
+ let targetSocket2
1146
+ const targetServer2 = createServer((socket) => {
1147
+ targetSocket2 = socket
1148
+ socket.resume()
1149
+ })
1150
+ const targetPort2 = await listen(targetServer2)
1151
+
1152
+ const users = new InMemoryUserStore()
1153
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
1154
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
1155
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
1156
+ const sessions = new SessionStore({now: () => 1000})
1157
+
1158
+ const relay = await startTcpRelay({
1159
+ authorizeTarget: ({target}) =>
1160
+ target.host === "127.0.0.1" && (target.port === targetPort1 || target.port === targetPort2),
1161
+ tokens,
1162
+ sessions,
1163
+ resumeWindowMs: 2_000,
1164
+ maxTargetBufferBytes: 100,
1165
+ maxTotalBufferBytes: 150
1166
+ })
1167
+
1168
+ const tunnel1 = createConnection({host: "127.0.0.1", port: relay.port})
1169
+ const decoder1 = new FrameDecoder({compactDirection: "target-to-client"})
1170
+ await waitConnect(tunnel1)
1171
+ tunnel1.write(encodeFrame({
1172
+ type: "session.create",
1173
+ token: token.value,
1174
+ name: "proc-limit-s1",
1175
+ identifier: "proc-limit-s1",
1176
+ target: {host: "127.0.0.1", port: targetPort1}
1177
+ }))
1178
+ decoder1.push(await readOnce(tunnel1))
1179
+
1180
+ const tunnel2 = createConnection({host: "127.0.0.1", port: relay.port})
1181
+ const decoder2 = new FrameDecoder({compactDirection: "target-to-client"})
1182
+ await waitConnect(tunnel2)
1183
+ tunnel2.write(encodeFrame({
1184
+ type: "session.create",
1185
+ token: token.value,
1186
+ name: "proc-limit-s2",
1187
+ identifier: "proc-limit-s2",
1188
+ target: {host: "127.0.0.1", port: targetPort2}
1189
+ }))
1190
+ decoder2.push(await readOnce(tunnel2))
1191
+
1192
+ relay.closeActiveTunnel()
1193
+ await waitClose(tunnel1)
1194
+ await waitClose(tunnel2)
1195
+
1196
+ const s1Payload = Buffer.alloc(80, 0x41)
1197
+ const s2Payload = Buffer.alloc(80, 0x42)
1198
+ targetSocket1?.write(s1Payload)
1199
+ await new Promise((resolve) => { setTimeout(resolve, 50) })
1200
+ targetSocket2?.write(s2Payload)
1201
+ await new Promise((resolve) => { setTimeout(resolve, 50) })
1202
+
1203
+ const tunnel3 = createConnection({host: "127.0.0.1", port: relay.port})
1204
+ const decoder3 = new FrameDecoder({compactDirection: "target-to-client"})
1205
+ await waitConnect(tunnel3)
1206
+ tunnel3.write(encodeFrame({type: "session.resume", token: token.value, identifier: "proc-limit-s1"}))
1207
+
1208
+ let resumed1 = false
1209
+ const s1Received = []
1210
+ while (!resumed1 || s1Received.length < 1) {
1211
+ for (const frame of decoder3.push(await readOnce(tunnel3))) {
1212
+ if (frame.header.type === "session.resumed") {
1213
+ resumed1 = true
1214
+ }
1215
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
1216
+ s1Received.push(frame.payload)
1217
+ }
1218
+ }
1219
+ }
1220
+
1221
+ assert.equal(s1Received.length, 1)
1222
+ const s1Chunk = /** @type {Buffer} */ (s1Received[0])
1223
+ assert.equal(s1Chunk.length, 80)
1224
+ assert.deepEqual(s1Chunk, s1Payload)
1225
+
1226
+ tunnel3.destroy()
1227
+
1228
+ const tunnel4 = createConnection({host: "127.0.0.1", port: relay.port})
1229
+ const decoder4 = new FrameDecoder({compactDirection: "target-to-client"})
1230
+ await waitConnect(tunnel4)
1231
+ tunnel4.write(encodeFrame({type: "session.resume", token: token.value, identifier: "proc-limit-s2"}))
1232
+
1233
+ let resumed2 = false
1234
+ /** @type {Array<{type: string, code?: string, retryable?: boolean, message?: string, direction?: string}>} */
1235
+ const s2Headers = []
1236
+ while (!resumed2) {
1237
+ for (const frame of decoder4.push(await readOnce(tunnel4))) {
1238
+ s2Headers.push(frame.header)
1239
+ if (frame.header.type === "session.resumed") {
1240
+ resumed2 = true
1241
+ }
1242
+ }
1243
+ }
1244
+
1245
+ const errorHeader = s2Headers.find((h) => h.type === "error")
1246
+ assert.ok(errorHeader !== undefined, "expected error frame after resume for overflowed session")
1247
+ assert.equal(errorHeader?.code, "TARGET_BUFFER_OVERFLOW")
1248
+ assert.equal(errorHeader?.retryable, false)
1249
+ assert.ok(
1250
+ typeof errorHeader?.message === "string" && errorHeader.message.includes("Process-wide"),
1251
+ `message should reference Process-wide limit, got: ${errorHeader?.message}`
1252
+ )
1253
+
1254
+ const s2DataHeaders = s2Headers.filter((h) => h.type === "stream.data" && h.direction === "target-to-client")
1255
+ assert.equal(s2DataHeaders.length, 0, "overflowed session must not receive stream.data from a replacement target")
1256
+
1257
+ tunnel4.destroy()
1258
+ await relay.close()
1259
+ await new Promise((resolve, reject) => {
1260
+ targetServer1.close((error) => {
1261
+ if (error) { reject(error); return }
1262
+ resolve(undefined)
1263
+ })
1264
+ })
1265
+ await new Promise((resolve, reject) => {
1266
+ targetServer2.close((error) => {
1267
+ if (error) { reject(error); return }
1268
+ resolve(undefined)
1269
+ })
1270
+ })
1271
+ })
1272
+
1273
+ test("TCP relay rejects a spoofed compact marker on a length-prefixed stream.data after a v2 handshake", async () => {
1274
+ const echoServer = createServer((socket) => {
1275
+ socket.on("data", (chunk) => socket.write(chunk))
1276
+ })
1277
+ const targetPort = await listen(echoServer)
1278
+
1279
+ const users = new InMemoryUserStore()
1280
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
1281
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
1282
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
1283
+ const sessions = new SessionStore({now: () => 1000})
1284
+
1285
+ const relay = await startTcpRelay({
1286
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
1287
+ tokens,
1288
+ sessions
1289
+ })
1290
+
1291
+ const socket = createConnection({host: "127.0.0.1", port: relay.port})
1292
+ const decoder = new FrameDecoder({compactDirection: "target-to-client"})
1293
+ await waitConnect(socket)
1294
+ socket.write(encodeFrame({
1295
+ type: "session.create",
1296
+ token: token.value,
1297
+ name: "neg-seq-test",
1298
+ identifier: "session-neg-seq",
1299
+ target: {host: "127.0.0.1", port: targetPort}
1300
+ }))
1301
+ decoder.push(await readOnce(socket))
1302
+
1303
+ socket.write(encodeEnvelope({
1304
+ type: "stream.data",
1305
+ direction: "client-to-target",
1306
+ sequence: -5,
1307
+ compact: true
1308
+ }, Buffer.from("negdata")))
1309
+
1310
+ let errorFrame
1311
+ while (errorFrame === undefined) {
1312
+ for (const frame of decoder.push(await readOnce(socket))) {
1313
+ if (frame.header.type === "error") {
1314
+ errorFrame = frame.header
1315
+ }
1316
+ }
1317
+ }
1318
+ assert.equal(errorFrame?.code, "PROTOCOL_VIOLATION")
1319
+ assert.ok(
1320
+ typeof errorFrame?.message === "string" && errorFrame.message.includes("Length-prefixed stream.data"),
1321
+ `unexpected message: ${errorFrame?.message}`
1322
+ )
1323
+
1324
+ assert.equal(sessions.get("session-neg-seq")?.clientSequence, 0)
1325
+
1326
+ socket.destroy()
1327
+ await relay.close()
1328
+ await new Promise((resolve, reject) => {
1329
+ echoServer.close((error) => {
1330
+ if (error) { reject(error); return }
1331
+ resolve(undefined)
1332
+ })
1333
+ })
1334
+ })
1335
+
1336
+ test("TCP relay rejects length-prefixed stream.ack after a v2 handshake", async () => {
1337
+ const echoServer = createServer((socket) => {
1338
+ socket.on("data", (chunk) => socket.write(chunk))
1339
+ })
1340
+ const targetPort = await listen(echoServer)
1341
+
1342
+ const users = new InMemoryUserStore()
1343
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
1344
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
1345
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
1346
+ const sessions = new SessionStore({now: () => 1000})
1347
+
1348
+ const relay = await startTcpRelay({
1349
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
1350
+ tokens,
1351
+ sessions
1352
+ })
1353
+
1354
+ const socket = createConnection({host: "127.0.0.1", port: relay.port})
1355
+ const decoder = new FrameDecoder({compactDirection: "target-to-client"})
1356
+ await waitConnect(socket)
1357
+ socket.write(encodeFrame({
1358
+ type: "session.create",
1359
+ token: token.value,
1360
+ name: "nan-seq-test",
1361
+ identifier: "session-nan-seq",
1362
+ target: {host: "127.0.0.1", port: targetPort}
1363
+ }))
1364
+ decoder.push(await readOnce(socket))
1365
+
1366
+ socket.write(encodeEnvelope({
1367
+ type: "stream.ack",
1368
+ direction: "target-to-client",
1369
+ ackSequence: 0,
1370
+ compact: true
1371
+ }))
1372
+
1373
+ let errorFrame
1374
+ while (errorFrame === undefined) {
1375
+ for (const frame of decoder.push(await readOnceWithin(socket))) {
1376
+ if (frame.header.type === "error") {
1377
+ errorFrame = frame.header
1378
+ }
1379
+ }
1380
+ }
1381
+ assert.equal(errorFrame?.code, "PROTOCOL_VIOLATION")
1382
+ assert.match(String(errorFrame?.message), /Length-prefixed stream\.ack/)
1383
+ assert.equal(sessions.get("session-nan-seq")?.clientSequence, 0)
1384
+
1385
+ socket.destroy()
1386
+ await relay.close()
1387
+ await new Promise((resolve, reject) => {
1388
+ echoServer.close((error) => {
1389
+ if (error) { reject(error); return }
1390
+ resolve(undefined)
1391
+ })
1392
+ })
1393
+ })
1394
+
1395
+ test("TCP relay rejects a length-prefixed stream.data regardless of envelope sequence metadata", async () => {
1396
+ const echoServer = createServer((socket) => {
1397
+ socket.on("data", (chunk) => socket.write(chunk))
1398
+ })
1399
+ const targetPort = await listen(echoServer)
1400
+
1401
+ const users = new InMemoryUserStore()
1402
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
1403
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
1404
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
1405
+ const sessions = new SessionStore({now: () => 1000})
1406
+
1407
+ const relay = await startTcpRelay({
1408
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
1409
+ tokens,
1410
+ sessions
1411
+ })
1412
+
1413
+ const socket = createConnection({host: "127.0.0.1", port: relay.port})
1414
+ const decoder = new FrameDecoder({compactDirection: "target-to-client"})
1415
+ await waitConnect(socket)
1416
+ socket.write(encodeFrame({
1417
+ type: "session.create",
1418
+ token: token.value,
1419
+ name: "unsafe-seq-test",
1420
+ identifier: "session-unsafe-seq",
1421
+ target: {host: "127.0.0.1", port: targetPort}
1422
+ }))
1423
+ decoder.push(await readOnce(socket))
1424
+
1425
+ socket.write(encodeEnvelope({
1426
+ type: "stream.data",
1427
+ direction: "client-to-target",
1428
+ sequence: Number.MAX_SAFE_INTEGER + 1
1429
+ }, Buffer.from("toobig")))
1430
+
1431
+ let errorFrame
1432
+ while (errorFrame === undefined) {
1433
+ for (const frame of decoder.push(await readOnce(socket))) {
1434
+ if (frame.header.type === "error") {
1435
+ errorFrame = frame.header
1436
+ }
1437
+ }
1438
+ }
1439
+ assert.equal(errorFrame?.code, "PROTOCOL_VIOLATION")
1440
+ assert.equal(sessions.get("session-unsafe-seq")?.clientSequence, 0)
1441
+
1442
+ socket.destroy()
1443
+ await relay.close()
1444
+ await new Promise((resolve, reject) => {
1445
+ echoServer.close((error) => {
1446
+ if (error) { reject(error); return }
1447
+ resolve(undefined)
1448
+ })
1449
+ })
1450
+ })
1451
+
1452
+ test("TCP relay rejects stream.data with a gapped client-to-target sequence and does not write the target", async () => {
1453
+ /** @type {string[]} */
1454
+ const targetWrites = []
1455
+ const targetServer = createServer((socket) => {
1456
+ socket.on("data", (chunk) => {
1457
+ targetWrites.push(chunk.toString("utf8"))
1458
+ socket.write(chunk)
1459
+ })
1460
+ })
1461
+ const targetPort = await listen(targetServer)
1462
+
1463
+ const users = new InMemoryUserStore()
1464
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
1465
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
1466
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
1467
+ const sessions = new SessionStore({now: () => 1000})
1468
+
1469
+ const relay = await startTcpRelay({
1470
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
1471
+ tokens,
1472
+ sessions
1473
+ })
1474
+
1475
+ const socket = createConnection({host: "127.0.0.1", port: relay.port})
1476
+ const decoder = new FrameDecoder({compactDirection: "target-to-client"})
1477
+ await waitConnect(socket)
1478
+ socket.write(encodeFrame({
1479
+ type: "session.create",
1480
+ token: token.value,
1481
+ name: "gap-test",
1482
+ identifier: "session-gap",
1483
+ target: {host: "127.0.0.1", port: targetPort}
1484
+ }))
1485
+ decoder.push(await readOnce(socket))
1486
+
1487
+ socket.write(encodeFrame({
1488
+ type: "stream.data",
1489
+ direction: "client-to-target",
1490
+ sequence: 12
1491
+ }, Buffer.from("gapped")))
1492
+
1493
+ let errorFrame
1494
+ while (errorFrame === undefined) {
1495
+ for (const frame of decoder.push(await readOnce(socket))) {
1496
+ if (frame.header.type === "error") {
1497
+ errorFrame = frame.header
1498
+ }
1499
+ }
1500
+ }
1501
+ assert.equal(errorFrame?.code, "PROTOCOL_VIOLATION")
1502
+ assert.ok(
1503
+ typeof errorFrame?.message === "string" && errorFrame.message.includes("gap"),
1504
+ `unexpected message: ${errorFrame?.message}`
1505
+ )
1506
+
1507
+ assert.deepEqual(targetWrites, [])
1508
+ assert.equal(sessions.get("session-gap")?.clientSequence, 0)
1509
+
1510
+ socket.destroy()
1511
+ await relay.close()
1512
+ await new Promise((resolve, reject) => {
1513
+ targetServer.close((error) => {
1514
+ if (error) { reject(error); return }
1515
+ resolve(undefined)
1516
+ })
1517
+ })
1518
+ })
1519
+
1520
+ test("TCP relay rejects stream.data with an overlapping client-to-target sequence and does not mutate state", async () => {
1521
+ /** @type {string[]} */
1522
+ const targetWrites = []
1523
+ const targetServer = createServer((socket) => {
1524
+ socket.on("data", (chunk) => {
1525
+ targetWrites.push(chunk.toString("utf8"))
1526
+ socket.write(chunk)
1527
+ })
1528
+ })
1529
+ const targetPort = await listen(targetServer)
1530
+
1531
+ const users = new InMemoryUserStore()
1532
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
1533
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
1534
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
1535
+ const sessions = new SessionStore({now: () => 1000})
1536
+
1537
+ const relay = await startTcpRelay({
1538
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
1539
+ tokens,
1540
+ sessions
1541
+ })
1542
+
1543
+ const socket = createConnection({host: "127.0.0.1", port: relay.port})
1544
+ const decoder = new FrameDecoder({compactDirection: "target-to-client"})
1545
+ await waitConnect(socket)
1546
+ socket.write(encodeFrame({
1547
+ type: "session.create",
1548
+ token: token.value,
1549
+ name: "overlap-test",
1550
+ identifier: "session-overlap",
1551
+ target: {host: "127.0.0.1", port: targetPort}
1552
+ }))
1553
+ decoder.push(await readOnce(socket))
1554
+
1555
+ socket.write(encodeFrame({
1556
+ type: "stream.data",
1557
+ direction: "client-to-target",
1558
+ sequence: 10
1559
+ }, Buffer.from("1234567890")))
1560
+
1561
+ let acked = false
1562
+ while (!acked) {
1563
+ for (const frame of decoder.push(await readOnce(socket))) {
1564
+ if (frame.header.type === "stream.ack" && frame.header.direction === "client-to-target") {
1565
+ acked = true
1566
+ }
1567
+ }
1568
+ }
1569
+ assert.deepEqual(targetWrites, ["1234567890"])
1570
+ assert.equal(sessions.get("session-overlap")?.clientSequence, 10)
1571
+
1572
+ socket.write(encodeFrame({
1573
+ type: "stream.data",
1574
+ direction: "client-to-target",
1575
+ sequence: 13
1576
+ }, Buffer.from("abcdef")))
1577
+
1578
+ let errorFrame
1579
+ while (errorFrame === undefined) {
1580
+ for (const frame of decoder.push(await readOnce(socket))) {
1581
+ if (frame.header.type === "error") {
1582
+ errorFrame = frame.header
1583
+ }
1584
+ }
1585
+ }
1586
+ assert.equal(errorFrame?.code, "PROTOCOL_VIOLATION")
1587
+ assert.ok(
1588
+ typeof errorFrame?.message === "string" && errorFrame.message.includes("overlap"),
1589
+ `unexpected message: ${errorFrame?.message}`
1590
+ )
1591
+ assert.equal(sessions.get("session-overlap")?.clientSequence, 10)
1592
+
1593
+ socket.destroy()
1594
+ await relay.close()
1595
+ await new Promise((resolve, reject) => {
1596
+ targetServer.close((error) => {
1597
+ if (error) { reject(error); return }
1598
+ resolve(undefined)
1599
+ })
1600
+ })
1601
+ })
1602
+
1603
+ test("TCP relay acks an already-committed duplicate client-to-target sequence without writing the target", async () => {
1604
+ /** @type {string[]} */
1605
+ const targetWrites = []
1606
+ const targetServer = createServer((socket) => {
1607
+ socket.on("data", (chunk) => {
1608
+ targetWrites.push(chunk.toString("utf8"))
1609
+ socket.write(chunk)
1610
+ })
1611
+ })
1612
+ const targetPort = await listen(targetServer)
1613
+
1614
+ const users = new InMemoryUserStore()
1615
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
1616
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
1617
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
1618
+ const sessions = new SessionStore({now: () => 1000})
1619
+
1620
+ const relay = await startTcpRelay({
1621
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
1622
+ tokens,
1623
+ sessions
1624
+ })
1625
+
1626
+ const socket = createConnection({host: "127.0.0.1", port: relay.port})
1627
+ const decoder = new FrameDecoder({compactDirection: "target-to-client"})
1628
+ await waitConnect(socket)
1629
+ socket.write(encodeFrame({
1630
+ type: "session.create",
1631
+ token: token.value,
1632
+ name: "dup-test",
1633
+ identifier: "session-dup",
1634
+ target: {host: "127.0.0.1", port: targetPort}
1635
+ }))
1636
+ decoder.push(await readOnce(socket))
1637
+
1638
+ socket.write(encodeFrame({
1639
+ type: "stream.data",
1640
+ direction: "client-to-target",
1641
+ sequence: 5
1642
+ }, Buffer.from("hello")))
1643
+
1644
+ let acked = false
1645
+ while (!acked) {
1646
+ for (const frame of decoder.push(await readOnce(socket))) {
1647
+ if (frame.header.type === "stream.ack" && frame.header.direction === "client-to-target") {
1648
+ acked = true
1649
+ }
1650
+ }
1651
+ }
1652
+ assert.deepEqual(targetWrites, ["hello"])
1653
+ assert.equal(sessions.get("session-dup")?.clientSequence, 5)
1654
+
1655
+ socket.write(encodeFrame({
1656
+ type: "stream.data",
1657
+ direction: "client-to-target",
1658
+ sequence: 5
1659
+ }, Buffer.from("hello")))
1660
+
1661
+ let dupAck = false
1662
+ let dupError = false
1663
+ while (!dupAck && !dupError) {
1664
+ for (const frame of decoder.push(await readOnce(socket))) {
1665
+ if (frame.header.type === "stream.ack" && frame.header.direction === "client-to-target" && frame.header.ackSequence === 5) {
1666
+ dupAck = true
1667
+ }
1668
+ if (frame.header.type === "error") {
1669
+ dupError = true
1670
+ }
1671
+ }
1672
+ }
1673
+ assert.ok(dupAck, "duplicate frame must receive ack")
1674
+ assert.equal(dupError, false, "duplicate frame must not receive an error")
1675
+ assert.deepEqual(targetWrites, ["hello"])
1676
+ assert.equal(sessions.get("session-dup")?.clientSequence, 5)
1677
+
1678
+ socket.destroy()
1679
+ await relay.close()
1680
+ await new Promise((resolve, reject) => {
1681
+ targetServer.close((error) => {
1682
+ if (error) { reject(error); return }
1683
+ resolve(undefined)
1684
+ })
1685
+ })
1686
+ })
1687
+
1688
+ test("TCP relay accepts consecutive contiguous stream.data frames decoded in one TCP chunk while the first target write is still pending", async () => {
1689
+ /** @type {string[]} */
1690
+ const targetWrites = []
1691
+ const targetServer = createServer((socket) => {
1692
+ socket.on("data", (chunk) => {
1693
+ targetWrites.push(chunk.toString("utf8"))
1694
+ socket.write(chunk)
1695
+ })
1696
+ })
1697
+ const targetPort = await listen(targetServer)
1698
+
1699
+ const users = new InMemoryUserStore()
1700
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
1701
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
1702
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
1703
+ const sessions = new SessionStore({now: () => 1000})
1704
+
1705
+ const relay = await startTcpRelay({
1706
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
1707
+ tokens,
1708
+ sessions
1709
+ })
1710
+
1711
+ const socket = createConnection({host: "127.0.0.1", port: relay.port})
1712
+ const decoder = new FrameDecoder({compactDirection: "target-to-client"})
1713
+ await waitConnect(socket)
1714
+ socket.write(encodeFrame({
1715
+ type: "session.create",
1716
+ token: token.value,
1717
+ name: "contiguous-one-chunk",
1718
+ identifier: "session-contig",
1719
+ target: {host: "127.0.0.1", port: targetPort}
1720
+ }))
1721
+ decoder.push(await readOnce(socket))
1722
+
1723
+ const frame1 = encodeFrame({
1724
+ type: "stream.data",
1725
+ direction: "client-to-target",
1726
+ sequence: 10
1727
+ }, Buffer.from("1234567890"))
1728
+
1729
+ const frame2 = encodeFrame({
1730
+ type: "stream.data",
1731
+ direction: "client-to-target",
1732
+ sequence: 20
1733
+ }, Buffer.from("abcdefghij"))
1734
+
1735
+ const combined = Buffer.concat([frame1, frame2])
1736
+ socket.write(combined)
1737
+
1738
+ let ackCount = 0
1739
+ let errorCount = 0
1740
+ while (ackCount < 2 && errorCount === 0) {
1741
+ for (const frame of decoder.push(await readOnce(socket))) {
1742
+ if (frame.header.type === "stream.ack" && frame.header.direction === "client-to-target") {
1743
+ ackCount++
1744
+ }
1745
+ if (frame.header.type === "error") {
1746
+ errorCount++
1747
+ }
1748
+ }
1749
+ }
1750
+
1751
+ assert.equal(errorCount, 0, "no protocol violation for contiguous frames in one chunk")
1752
+ assert.equal(ackCount, 2, "both frames must receive an ack")
1753
+ const receivedData = targetWrites.join("")
1754
+ assert.equal(receivedData, "1234567890abcdefghij")
1755
+ assert.equal(sessions.get("session-contig")?.clientSequence, 20)
1756
+
1757
+ socket.destroy()
1758
+ await relay.close()
1759
+ await new Promise((resolve, reject) => {
1760
+ targetServer.close((error) => {
1761
+ if (error) { reject(error); return }
1762
+ resolve(undefined)
1763
+ })
1764
+ })
1765
+ })
1766
+
1767
+ test("TCP relay replays unacknowledged target-to-client data exactly once after reconnect and does not replay acknowledged chunks", async () => {
1768
+ const echoServer = createServer((socket) => {
1769
+ socket.on("data", (chunk) => socket.write(chunk))
1770
+ })
1771
+ const targetPort = await listen(echoServer)
1772
+
1773
+ const users = new InMemoryUserStore()
1774
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
1775
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
1776
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
1777
+ const sessions = new SessionStore({now: () => 1000})
1778
+
1779
+ const relay = await startTcpRelay({
1780
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
1781
+ tokens,
1782
+ sessions,
1783
+ resumeWindowMs: 2_000
1784
+ })
1785
+
1786
+ const tunnel1 = createConnection({host: "127.0.0.1", port: relay.port})
1787
+ const decoder1 = new FrameDecoder({compactDirection: "target-to-client"})
1788
+ await waitConnect(tunnel1)
1789
+ tunnel1.write(encodeFrame({
1790
+ type: "session.create",
1791
+ token: token.value,
1792
+ name: "replay-once-test",
1793
+ identifier: "session-replay-once",
1794
+ target: {host: "127.0.0.1", port: targetPort}
1795
+ }))
1796
+ decoder1.push(await readOnce(tunnel1))
1797
+
1798
+ tunnel1.write(encodeFrame({type: "stream.data", direction: "client-to-target", sequence: 5}, Buffer.from("alpha")))
1799
+ let alphaRelaySeq = 0
1800
+ while (alphaRelaySeq === 0) {
1801
+ for (const frame of decoder1.push(await readOnce(tunnel1))) {
1802
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
1803
+ alphaRelaySeq = /** @type {number} */ (frame.header.sequence ?? 0)
1804
+ }
1805
+ }
1806
+ }
1807
+ assert.ok(alphaRelaySeq > 0, "alpha must be echoed with a target-to-client sequence")
1808
+
1809
+ tunnel1.write(encodeFrame({type: "stream.data", direction: "client-to-target", sequence: 11}, Buffer.from("bravo6")))
1810
+ let bravoRelaySeq = 0
1811
+ while (bravoRelaySeq === 0) {
1812
+ for (const frame of decoder1.push(await readOnce(tunnel1))) {
1813
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client" && frame.payload.toString("utf8") === "bravo6") {
1814
+ bravoRelaySeq = /** @type {number} */ (frame.header.sequence ?? 0)
1815
+ }
1816
+ }
1817
+ }
1818
+
1819
+ tunnel1.write(encodeFrame({type: "stream.ack", direction: "target-to-client", ackSequence: alphaRelaySeq}))
1820
+
1821
+ tunnel1.destroy()
1822
+ await waitClose(tunnel1)
1823
+
1824
+ const tunnel2 = createConnection({host: "127.0.0.1", port: relay.port})
1825
+ const decoder2 = new FrameDecoder({compactDirection: "target-to-client"})
1826
+ await waitConnect(tunnel2)
1827
+ tunnel2.write(encodeFrame({type: "session.resume", token: token.value, identifier: "session-replay-once", targetToClientAck: alphaRelaySeq}))
1828
+
1829
+ let resumed = false
1830
+ /** @type {string[]} */
1831
+ const replayedChunks = []
1832
+ while (!resumed || replayedChunks.length < 1) {
1833
+ for (const frame of decoder2.push(await readOnce(tunnel2))) {
1834
+ if (frame.header.type === "session.resumed") {
1835
+ resumed = true
1836
+ }
1837
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
1838
+ replayedChunks.push(frame.payload.toString("utf8"))
1839
+ }
1840
+ }
1841
+ }
1842
+
1843
+ assert.deepEqual(replayedChunks, ["bravo6"], "only unacknowledged bytes must be replayed")
1844
+
1845
+ tunnel2.destroy()
1846
+ await relay.close()
1847
+ await new Promise((resolve, reject) => {
1848
+ echoServer.close((error) => {
1849
+ if (error) { reject(error); return }
1850
+ resolve(undefined)
1851
+ })
1852
+ })
1853
+ })
1854
+
1855
+ test("TCP relay handshake timeout sends HANDSHAKE_TIMEOUT before auth, not after", async () => {
1856
+ const relay = await startTcpRelay({
1857
+ authorizeTarget: () => false,
1858
+ tokens: new TokenStore(),
1859
+ sessions: new SessionStore(),
1860
+ handshakeTimeoutMs: 50
1861
+ })
1862
+
1863
+ const unauthed = createConnection({host: "127.0.0.1", port: relay.port})
1864
+ const decoder = new FrameDecoder({compactDirection: "target-to-client"})
1865
+ await waitConnect(unauthed)
1866
+
1867
+ const handshakeTimeout = new Promise((resolve, reject) => {
1868
+ unauthed.on("close", () => reject(new Error("socket closed without error frame")))
1869
+ unauthed.on("data", (chunk) => {
1870
+ for (const frame of decoder.push(chunk)) {
1871
+ if (frame.header.type === "error") {
1872
+ resolve(frame.header)
1873
+ }
1874
+ }
1875
+ })
1876
+ })
1877
+
1878
+ const errorHeader = await Promise.race([
1879
+ handshakeTimeout,
1880
+ new Promise((_, reject) => setTimeout(() => reject(new Error("test timed out waiting for error frame")), 1000))
1881
+ ])
1882
+
1883
+ assert.equal(errorHeader.type, "error")
1884
+ assert.equal(errorHeader.code, "HANDSHAKE_TIMEOUT")
1885
+
1886
+ const users = new InMemoryUserStore()
1887
+ const authedTokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
1888
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
1889
+ const token = await login(users, authedTokens, {username: "kasper", password: "correct horse battery staple"})
1890
+
1891
+ const authed = createConnection({host: "127.0.0.1", port: relay.port})
1892
+ const authedDecoder = new FrameDecoder({compactDirection: "target-to-client"})
1893
+ await waitConnect(authed)
1894
+ authed.write(encodeFrame({
1895
+ type: "session.create",
1896
+ token: token.value,
1897
+ name: "timeout-do-not-fire",
1898
+ identifier: "session-timeout-ok",
1899
+ target: {host: "127.0.0.1", port: 9}
1900
+ }))
1901
+
1902
+ const authData = await Promise.race([
1903
+ new Promise((resolve) => authed.once("data", (chunk) => { authedDecoder.push(chunk); resolve(undefined) })),
1904
+ new Promise((_, reject) => setTimeout(() => reject(new Error("authenticated connection timed out")), 500))
1905
+ ])
1906
+ assert.notEqual(authData, "timeout", "authenticated connection must receive a response")
1907
+
1908
+ for (const frame of authedDecoder.push(Buffer.alloc(0))) {
1909
+ assert.notEqual(frame.header.code, "HANDSHAKE_TIMEOUT", "authenticated connection must not receive handshake timeout")
1910
+ }
1911
+
1912
+ authed.destroy()
1913
+ await relay.close()
1914
+ })
1915
+
1916
+ test("TCP relay enforces maxInputBytesPerConnection before auth and lifts it after auth", async () => {
1917
+ const echoServer = createServer((socket) => {
1918
+ socket.on("data", (chunk) => socket.write(chunk))
1919
+ })
1920
+ const targetPort = await listen(echoServer)
1921
+
1922
+ const users = new InMemoryUserStore()
1923
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
1924
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
1925
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
1926
+
1927
+ const relay = await startTcpRelay({
1928
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
1929
+ tokens,
1930
+ sessions: new SessionStore({now: () => 1000}),
1931
+ maxInputBytesPerConnection: 256,
1932
+ handshakeTimeoutMs: 5000
1933
+ })
1934
+
1935
+ const errorHeader = await new Promise((resolve, reject) => {
1936
+ const preAuth = createConnection({host: "127.0.0.1", port: relay.port})
1937
+ const decoder = new FrameDecoder({compactDirection: "target-to-client"})
1938
+
1939
+ preAuth.on("connect", () => {
1940
+ const hugePayload = Buffer.alloc(300, 0x41)
1941
+ const hugeFrame = encodeFrame({
1942
+ type: "stream.data",
1943
+ direction: "client-to-target",
1944
+ sequence: 1
1945
+ }, hugePayload)
1946
+
1947
+ const half = Math.floor(hugeFrame.length / 2)
1948
+ preAuth.write(hugeFrame.subarray(0, half))
1949
+ preAuth.write(hugeFrame.subarray(half))
1950
+ })
1951
+
1952
+ preAuth.on("data", (chunk) => {
1953
+ for (const frame of decoder.push(chunk)) {
1954
+ if (frame.header.type === "error") {
1955
+ resolve(frame.header)
1956
+ }
1957
+ }
1958
+ })
1959
+
1960
+ preAuth.on("close", () => reject(new Error("socket closed without error frame")))
1961
+
1962
+ setTimeout(() => reject(new Error("timed out waiting for error frame")), 1000)
1963
+ })
1964
+
1965
+ assert.equal(errorHeader.type, "error")
1966
+ assert.equal(errorHeader.code, "PROTOCOL_VIOLATION")
1967
+
1968
+ const createdHeader = await new Promise((resolve, reject) => {
1969
+ const authed = createConnection({host: "127.0.0.1", port: relay.port})
1970
+ const decoder = new FrameDecoder({compactDirection: "target-to-client"})
1971
+
1972
+ authed.on("connect", () => {
1973
+ authed.write(encodeFrame({
1974
+ type: "session.create",
1975
+ token: token.value,
1976
+ name: "budget-lift-test",
1977
+ identifier: "session-budget-lift",
1978
+ target: {host: "127.0.0.1", port: targetPort}
1979
+ }))
1980
+ })
1981
+
1982
+ authed.on("data", (chunk) => {
1983
+ for (const frame of decoder.push(chunk)) {
1984
+ if (frame.header.type === "session.created") {
1985
+ resolve(frame.header)
1986
+ }
1987
+ }
1988
+ })
1989
+
1990
+ authed.on("close", () => reject(new Error("authed socket closed without session.created")))
1991
+
1992
+ setTimeout(() => reject(new Error("timed out waiting for session.created")), 1000)
1993
+ })
1994
+
1995
+ assert.equal(createdHeader.type, "session.created")
1996
+
1997
+ const largePayload = Buffer.alloc(256, 0x42)
1998
+
1999
+ const echoed = await new Promise((resolve, reject) => {
2000
+ const authed2 = createConnection({host: "127.0.0.1", port: relay.port})
2001
+ const decoder = new FrameDecoder({compactDirection: "target-to-client"})
2002
+
2003
+ authed2.on("connect", () => {
2004
+ authed2.write(encodeFrame({
2005
+ type: "session.create",
2006
+ token: token.value,
2007
+ name: "budget-lift-large",
2008
+ identifier: "session-budget-large",
2009
+ target: {host: "127.0.0.1", port: targetPort}
2010
+ }))
2011
+ })
2012
+
2013
+ authed2.on("data", (chunk) => {
2014
+ for (const frame of decoder.push(chunk)) {
2015
+ if (frame.header.type === "session.created") {
2016
+ authed2.write(encodeFrame({
2017
+ type: "stream.data",
2018
+ direction: "client-to-target",
2019
+ sequence: largePayload.length
2020
+ }, largePayload))
2021
+ }
2022
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
2023
+ resolve(frame.payload)
2024
+ }
2025
+ }
2026
+ })
2027
+
2028
+ authed2.on("close", () => reject(new Error("authed2 socket closed without echo")))
2029
+
2030
+ setTimeout(() => reject(new Error("timed out waiting for echo")), 1000)
2031
+ })
2032
+
2033
+ assert.ok(Buffer.isBuffer(echoed))
2034
+ assert.equal(echoed.length, largePayload.length)
2035
+ assert.deepEqual(echoed, largePayload)
2036
+
2037
+ await relay.close()
2038
+ await new Promise((resolve, reject) => {
2039
+ echoServer.close((error) => {
2040
+ if (error) { reject(error); return }
2041
+ resolve(undefined)
2042
+ })
2043
+ })
2044
+ })
2045
+
2046
+ test("TCP relay terminal clean-up on SocketductSocket.end() terminates target and rejects resume despite long resume window", async () => {
2047
+ const tempDir = await mkdtemp(join(tmpdir(), "socketduct-terminal-close-test-"))
2048
+ const echoServer = createServer((socket) => {
2049
+ socket.on("data", (chunk) => socket.write(chunk))
2050
+ })
2051
+ const targetPort = await listen(echoServer)
2052
+
2053
+ const users = new InMemoryUserStore()
2054
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
2055
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
2056
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
2057
+ const sessions = new SessionStore({now: () => 1000})
2058
+
2059
+ const relay = await startTcpRelay({
2060
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
2061
+ tokens,
2062
+ sessions,
2063
+ resumeWindowMs: 10_000
2064
+ })
2065
+
2066
+ const client = connectSocketduct({
2067
+ relay: {host: "127.0.0.1", port: relay.port, token: token.value},
2068
+ sessionName: "terminal-close-socket",
2069
+ target: {host: "127.0.0.1", port: targetPort},
2070
+ spoolDirectory: tempDir
2071
+ })
2072
+ await new Promise((resolve) => client.once("connect", resolve))
2073
+
2074
+ client.write("alpha")
2075
+ const echo = await new Promise((resolve) => {
2076
+ client.once("data", (data) => resolve(data.toString("utf8")))
2077
+ })
2078
+ assert.equal(echo, "alpha")
2079
+
2080
+ const sessionId = client.sessionIdentifier
2081
+ assert.ok(typeof sessionId === "string", "session identifier must be assigned")
2082
+
2083
+ const sessionBeforeClose = sessions.get(sessionId)
2084
+ assert.ok(sessionBeforeClose !== undefined, "session must exist before close")
2085
+
2086
+ const closePromise = new Promise((resolve) => client.once("close", resolve))
2087
+ client.end()
2088
+ await closePromise
2089
+
2090
+ const sessionAfterClose = sessions.get(sessionId)
2091
+ assert.ok(sessionAfterClose === undefined, "session must be removed after terminal close")
2092
+
2093
+ const tunnel2 = createConnection({host: "127.0.0.1", port: relay.port})
2094
+ const decoder2 = new FrameDecoder({compactDirection: "target-to-client"})
2095
+ await waitConnect(tunnel2)
2096
+ tunnel2.write(encodeFrame({type: "session.resume", token: token.value, identifier: sessionId}))
2097
+
2098
+ let resumeResponse
2099
+ while (resumeResponse === undefined) {
2100
+ for (const frame of decoder2.push(await readOnce(tunnel2))) {
2101
+ resumeResponse = frame.header
2102
+ }
2103
+ }
2104
+ assert.equal(resumeResponse.type, "error")
2105
+ assert.ok(resumeResponse.code === "SESSION_NOT_FOUND" || resumeResponse.code === "SESSION_EXPIRED", "session must not be resumable after terminal close")
2106
+
2107
+ tunnel2.destroy()
2108
+ await relay.close()
2109
+ await new Promise((resolve, reject) => {
2110
+ echoServer.close((error) => {
2111
+ if (error) { reject(error); return }
2112
+ resolve(undefined)
2113
+ })
2114
+ })
2115
+ await rm(tempDir, {recursive: true, force: true})
2116
+ })
2117
+
2118
+ test("TCP relay retains resumable session after unexpected tunnel close via closeActiveTunnel", async () => {
2119
+ const echoServer = createServer((socket) => {
2120
+ socket.on("data", (chunk) => socket.write(chunk))
2121
+ })
2122
+ const targetPort = await listen(echoServer)
2123
+
2124
+ const users = new InMemoryUserStore()
2125
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
2126
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
2127
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
2128
+ const sessions = new SessionStore({now: () => 1000})
2129
+
2130
+ const relay = await startTcpRelay({
2131
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
2132
+ tokens,
2133
+ sessions,
2134
+ resumeWindowMs: 5_000
2135
+ })
2136
+
2137
+ const tunnel1 = createConnection({host: "127.0.0.1", port: relay.port})
2138
+ const decoder1 = new FrameDecoder({compactDirection: "target-to-client"})
2139
+ await waitConnect(tunnel1)
2140
+ tunnel1.write(encodeFrame({
2141
+ type: "session.create",
2142
+ token: token.value,
2143
+ name: "force-close-resumable",
2144
+ identifier: "session-force-close-resumable",
2145
+ target: {host: "127.0.0.1", port: targetPort}
2146
+ }))
2147
+ decoder1.push(await readOnce(tunnel1))
2148
+
2149
+ tunnel1.write(encodeFrame({type: "stream.data", direction: "client-to-target", sequence: 5}, Buffer.from("first")))
2150
+ let firstEcho
2151
+ while (firstEcho === undefined) {
2152
+ for (const frame of decoder1.push(await readOnce(tunnel1))) {
2153
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
2154
+ firstEcho = frame.payload.toString("utf8")
2155
+ }
2156
+ }
2157
+ }
2158
+ assert.equal(firstEcho, "first")
2159
+
2160
+ relay.closeActiveTunnel()
2161
+ await waitClose(tunnel1)
2162
+
2163
+ const sessionBeforeResume = sessions.get("session-force-close-resumable")
2164
+ assert.ok(sessionBeforeResume !== undefined, "session must still exist after unexpected tunnel close")
2165
+ assert.ok(sessionBeforeResume.state === "interrupted" || sessionBeforeResume.state === "resuming" || sessionBeforeResume.state === "active", "session must be resumable after unexpected tunnel close")
2166
+
2167
+ const tunnel2 = createConnection({host: "127.0.0.1", port: relay.port})
2168
+ const decoder2 = new FrameDecoder({compactDirection: "target-to-client"})
2169
+ await waitConnect(tunnel2)
2170
+ tunnel2.write(encodeFrame({type: "session.resume", token: token.value, identifier: "session-force-close-resumable"}))
2171
+
2172
+ let resumed = false
2173
+ let gotReplay = false
2174
+ while (!resumed || !gotReplay) {
2175
+ for (const frame of decoder2.push(await readOnce(tunnel2))) {
2176
+ if (frame.header.type === "session.resumed") {
2177
+ resumed = true
2178
+ }
2179
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
2180
+ gotReplay = true
2181
+ }
2182
+ }
2183
+ }
2184
+ assert.ok(resumed, "session must be resumable after unexpected tunnel drop")
2185
+ assert.ok(gotReplay, "unacknowledged target-to-client data must be replayed on resume")
2186
+
2187
+ tunnel2.destroy()
2188
+ await relay.close()
2189
+ await new Promise((resolve, reject) => {
2190
+ echoServer.close((error) => {
2191
+ if (error) { reject(error); return }
2192
+ resolve(undefined)
2193
+ })
2194
+ })
2195
+ })
2196
+
2197
+ test("TCP relay rejects resume after resume window expires", async () => {
2198
+ const echoServer = createServer((socket) => {
2199
+ socket.on("data", (chunk) => socket.write(chunk))
2200
+ })
2201
+ const targetPort = await listen(echoServer)
2202
+
2203
+ const users = new InMemoryUserStore()
2204
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
2205
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
2206
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
2207
+ const sessions = new SessionStore({now: () => 1000})
2208
+
2209
+ const relay = await startTcpRelay({
2210
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
2211
+ tokens,
2212
+ sessions,
2213
+ resumeWindowMs: 100
2214
+ })
2215
+
2216
+ const tunnel1 = createConnection({host: "127.0.0.1", port: relay.port})
2217
+ const decoder1 = new FrameDecoder({compactDirection: "target-to-client"})
2218
+ await waitConnect(tunnel1)
2219
+ tunnel1.write(encodeFrame({
2220
+ type: "session.create",
2221
+ token: token.value,
2222
+ name: "expiry-test",
2223
+ identifier: "session-expiry-test",
2224
+ target: {host: "127.0.0.1", port: targetPort}
2225
+ }))
2226
+ decoder1.push(await readOnce(tunnel1))
2227
+
2228
+ tunnel1.write(encodeFrame({type: "stream.data", direction: "client-to-target", sequence: 4}, Buffer.from("ping")))
2229
+ let echo
2230
+ while (echo === undefined) {
2231
+ for (const frame of decoder1.push(await readOnce(tunnel1))) {
2232
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
2233
+ echo = frame.payload.toString("utf8")
2234
+ }
2235
+ }
2236
+ }
2237
+ assert.equal(echo, "ping")
2238
+
2239
+ tunnel1.destroy()
2240
+ await waitClose(tunnel1)
2241
+
2242
+ const sessionBeforeExpiry = sessions.get("session-expiry-test")
2243
+ assert.ok(sessionBeforeExpiry !== undefined, "session must exist before expiry")
2244
+
2245
+ await new Promise((resolve) => setTimeout(resolve, 200))
2246
+
2247
+ const sessionAfterExpiry = sessions.get("session-expiry-test")
2248
+ assert.ok(sessionAfterExpiry === undefined, "session must be removed after resume window expires")
2249
+
2250
+ const tunnel2 = createConnection({host: "127.0.0.1", port: relay.port})
2251
+ const decoder2 = new FrameDecoder({compactDirection: "target-to-client"})
2252
+ await waitConnect(tunnel2)
2253
+ tunnel2.write(encodeFrame({type: "session.resume", token: token.value, identifier: "session-expiry-test"}))
2254
+
2255
+ let resumeResponse
2256
+ while (resumeResponse === undefined) {
2257
+ for (const frame of decoder2.push(await readOnce(tunnel2))) {
2258
+ resumeResponse = frame.header
2259
+ }
2260
+ }
2261
+ assert.equal(resumeResponse.type, "error")
2262
+ assert.ok(resumeResponse.code === "SESSION_NOT_FOUND" || resumeResponse.code === "SESSION_EXPIRED", "session must not be resumable after expiry")
2263
+
2264
+ tunnel2.destroy()
2265
+ await relay.close()
2266
+ await new Promise((resolve, reject) => {
2267
+ echoServer.close((error) => {
2268
+ if (error) { reject(error); return }
2269
+ resolve(undefined)
2270
+ })
2271
+ })
2272
+ })
2273
+
2274
+ /**
2275
+ * @param {number} size - Uncompressed byte length to produce.
2276
+ * @returns {Buffer} - Highly compressible text payload.
2277
+ */
2278
+ function compressibleChunk(size) {
2279
+ const unit = "GET /api/v1/users?page=2 HTTP/1.1\r\nHost: example.com\r\nAccept: application/json\r\n\r\n"
2280
+ return Buffer.from(unit.repeat(Math.ceil(size / unit.length))).subarray(0, size)
2281
+ }
2282
+
2283
+ /**
2284
+ * Holds the next asynchronous Zstd completion callback.
2285
+ * @returns {{entered: Promise<unknown>, release: () => void, restore: () => void}} Test controller.
2286
+ */
2287
+ function holdNextZstdCompression() {
2288
+ const originalCompress = /** @type {NonNullable<typeof zlib.zstdCompress>} */ (zlib.zstdCompress)
2289
+ /** @type {import('node:zlib').CompressCallback | undefined} */
2290
+ let heldCallback
2291
+ /** @type {Buffer | undefined} */
2292
+ let heldInput
2293
+ /** @type {import('node:zlib').ZstdOptions | undefined} */
2294
+ let heldOptions
2295
+ /** @type {() => void} */
2296
+ let enteredResolve = () => {}
2297
+ const entered = new Promise((resolve) => { enteredResolve = () => resolve(undefined) })
2298
+ /** @type {(input: import('node:zlib').InputType, options: import('node:zlib').ZstdOptions, callback: import('node:zlib').CompressCallback) => void} */
2299
+ const patchedCompress = (input, options, callback) => {
2300
+ if (heldCallback === undefined) {
2301
+ heldCallback = callback
2302
+ heldInput = /** @type {Buffer} */ (/** @type {unknown} */ (input))
2303
+ heldOptions = options
2304
+ enteredResolve()
2305
+ return
2306
+ }
2307
+ originalCompress(input, options, callback)
2308
+ }
2309
+ // @ts-expect-error - TS lib __promisify__ required on typeof zstdCompress vs runtime
2310
+ zlib.zstdCompress = patchedCompress
2311
+ return {
2312
+ entered,
2313
+ release() {
2314
+ zlib.zstdCompress = originalCompress
2315
+ if (heldCallback !== undefined && heldInput !== undefined && heldOptions !== undefined) {
2316
+ originalCompress(heldInput, heldOptions, heldCallback)
2317
+ }
2318
+ },
2319
+ restore() {
2320
+ zlib.zstdCompress = originalCompress
2321
+ }
2322
+ }
2323
+ }
2324
+
2325
+ test("TCP relay retains a queued target close across disconnect and resumes it after data", {skip: !zstdAvailable()}, async () => {
2326
+ /** @type {import('node:net').Socket | undefined} */
2327
+ let targetSocket
2328
+ const targetServer = createServer((socket) => {
2329
+ targetSocket = socket
2330
+ socket.resume()
2331
+ })
2332
+ const targetPort = await listen(targetServer)
2333
+ const users = new InMemoryUserStore()
2334
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
2335
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
2336
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
2337
+ const relay = await startTcpRelay({
2338
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
2339
+ tokens,
2340
+ sessions: new SessionStore({now: () => 1000}),
2341
+ resumeWindowMs: 2_000
2342
+ })
2343
+ const tunnel1 = createConnection({host: "127.0.0.1", port: relay.port})
2344
+ const decoder1 = new FrameDecoder({compactDirection: "target-to-client"})
2345
+ await waitConnect(tunnel1)
2346
+ tunnel1.write(encodeFrame({
2347
+ type: "session.create", token: token.value, name: "queued-terminal",
2348
+ identifier: "session-queued-terminal", target: {host: "127.0.0.1", port: targetPort}, compression: ["zstd"]
2349
+ }))
2350
+ let created = false
2351
+ while (!created) {
2352
+ for (const frame of decoder1.push(await readOnceWithin(tunnel1))) {
2353
+ if (frame.header.type === "session.created") created = true
2354
+ }
2355
+ }
2356
+ while (targetSocket === undefined) await new Promise((resolve) => setTimeout(resolve, 1))
2357
+
2358
+ const held = holdNextZstdCompression()
2359
+ const payload = compressibleChunk(16_384)
2360
+ try {
2361
+ targetSocket.write(payload)
2362
+ await held.entered
2363
+ targetSocket.end()
2364
+ await waitClose(targetSocket)
2365
+ await relay.closeActiveTunnel()
2366
+ await waitClose(tunnel1)
2367
+ held.release()
2368
+
2369
+ const tunnel2 = createConnection({host: "127.0.0.1", port: relay.port})
2370
+ const decoder2 = new FrameDecoder({compactDirection: "target-to-client"})
2371
+ await waitConnect(tunnel2)
2372
+ tunnel2.write(encodeFrame({
2373
+ type: "session.resume", token: token.value, identifier: "session-queued-terminal",
2374
+ targetToClientAck: 0, compression: ["zstd"]
2375
+ }))
2376
+ /** @type {string[]} */
2377
+ const ordered = []
2378
+ while (!ordered.includes("close")) {
2379
+ for (const frame of decoder2.push(await readOnceWithin(tunnel2))) {
2380
+ if (frame.header.type === "stream.data") ordered.push("data")
2381
+ if (frame.header.type === "session.close") ordered.push("close")
2382
+ }
2383
+ }
2384
+ assert.deepEqual(ordered, ["data", "close"])
2385
+ tunnel2.destroy()
2386
+ } finally {
2387
+ held.restore()
2388
+ tunnel1.destroy()
2389
+ await relay.close()
2390
+ await new Promise((resolve) => targetServer.close(() => resolve(undefined)))
2391
+ }
2392
+ })
2393
+
2394
+ test("TCP relay orders a target close created during resume after queued target data", {skip: !zstdAvailable()}, async () => {
2395
+ /** @type {import('node:net').Socket | undefined} */
2396
+ let targetSocket
2397
+ let targetConnections = 0
2398
+ const targetServer = createServer((socket) => {
2399
+ targetConnections += 1
2400
+ targetSocket = socket
2401
+ socket.resume()
2402
+ })
2403
+ const targetPort = await listen(targetServer)
2404
+ const users = new InMemoryUserStore()
2405
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
2406
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
2407
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
2408
+ const relay = await startTcpRelay({
2409
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
2410
+ tokens,
2411
+ sessions: new SessionStore({now: () => 1000}),
2412
+ resumeWindowMs: 2_000
2413
+ })
2414
+ const tunnel1 = createConnection({host: "127.0.0.1", port: relay.port})
2415
+ const decoder1 = new FrameDecoder({compactDirection: "target-to-client"})
2416
+ await waitConnect(tunnel1)
2417
+ tunnel1.write(encodeFrame({
2418
+ type: "session.create", token: token.value, name: "resume-terminal-order",
2419
+ identifier: "session-resume-terminal-order", target: {host: "127.0.0.1", port: targetPort}, compression: ["zstd"]
2420
+ }))
2421
+ let created = false
2422
+ while (!created) {
2423
+ for (const frame of decoder1.push(await readOnceWithin(tunnel1))) {
2424
+ if (frame.header.type === "session.created") created = true
2425
+ }
2426
+ }
2427
+ while (targetSocket === undefined) await new Promise((resolve) => setImmediate(resolve))
2428
+
2429
+ const replayPayload = compressibleChunk(16_384)
2430
+ targetSocket.write(replayPayload)
2431
+ let receivedReplay = false
2432
+ while (!receivedReplay) {
2433
+ for (const frame of decoder1.push(await readOnceWithin(tunnel1))) {
2434
+ if (frame.header.type === "stream.data") receivedReplay = true
2435
+ }
2436
+ }
2437
+ await relay.closeActiveTunnel()
2438
+ await waitClose(tunnel1)
2439
+
2440
+ const held = holdNextZstdCompression()
2441
+ /** @type {import('node:net').Socket | undefined} */
2442
+ let tunnel2
2443
+ try {
2444
+ tunnel2 = createConnection({host: "127.0.0.1", port: relay.port})
2445
+ const decoder2 = new FrameDecoder({compactDirection: "target-to-client"})
2446
+ /** @type {string[]} */
2447
+ const ordered = []
2448
+ let resumed = false
2449
+ tunnel2.on("data", (chunk) => {
2450
+ for (const frame of decoder2.push(chunk)) {
2451
+ if (frame.header.type === "session.resumed") resumed = true
2452
+ if (frame.header.type === "stream.data") ordered.push("data")
2453
+ if (frame.header.type === "session.close") ordered.push("close")
2454
+ }
2455
+ })
2456
+ await waitConnect(tunnel2)
2457
+ tunnel2.write(encodeFrame({
2458
+ type: "session.resume", token: token.value, identifier: "session-resume-terminal-order",
2459
+ targetToClientAck: 0, compression: ["zstd"]
2460
+ }))
2461
+ await held.entered
2462
+ await waitUntil(() => resumed)
2463
+
2464
+ targetSocket.end(Buffer.from("final-target-record"))
2465
+ await waitClose(targetSocket)
2466
+ held.release()
2467
+
2468
+ await waitUntil(() => ordered.includes("close"))
2469
+ assert.deepEqual(ordered, ["data", "data", "close"])
2470
+ assert.equal(targetConnections, 1)
2471
+ } finally {
2472
+ held.restore()
2473
+ tunnel2?.destroy()
2474
+ tunnel1.destroy()
2475
+ await relay.close()
2476
+ await new Promise((resolve) => targetServer.close(() => resolve(undefined)))
2477
+ }
2478
+ })
2479
+
2480
+ test("TCP relay preserves send queue across tunnel close when zstd compression callback is delayed past resume", {skip: !zstdAvailable()}, async () => {
2481
+ /** @type {import('node:net').Socket | undefined} */
2482
+ let targetSocket
2483
+ const targetServer = createServer((socket) => {
2484
+ targetSocket = socket
2485
+ socket.on("data", (chunk) => socket.write(chunk))
2486
+ })
2487
+ const targetPort = await listen(targetServer)
2488
+
2489
+ const users = new InMemoryUserStore()
2490
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
2491
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
2492
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
2493
+ const sessions = new SessionStore({now: () => 1000})
2494
+
2495
+ const relay = await startTcpRelay({
2496
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
2497
+ tokens,
2498
+ sessions,
2499
+ resumeWindowMs: 2_000,
2500
+ maxTargetBufferBytes: 8_192
2501
+ })
2502
+
2503
+ const tunnel1 = createConnection({host: "127.0.0.1", port: relay.port})
2504
+ const decoder1 = new FrameDecoder({compactDirection: "target-to-client"})
2505
+ await waitConnect(tunnel1)
2506
+ tunnel1.write(encodeFrame({
2507
+ type: "session.create",
2508
+ token: token.value,
2509
+ name: "zstd-queue-race",
2510
+ identifier: "session-zstd-queue-race",
2511
+ target: {host: "127.0.0.1", port: targetPort},
2512
+ compression: ["zstd"]
2513
+ }))
2514
+
2515
+ const createdFrames = decoder1.push(await readOnce(tunnel1))
2516
+ const created = createdFrames[0]
2517
+ if (created === undefined) throw new Error("session.created expected")
2518
+ assert.equal(created.header.compression, "zstd")
2519
+
2520
+ const chunk1 = compressibleChunk(8192)
2521
+ tunnel1.write(encodeFrame({
2522
+ type: "stream.data",
2523
+ direction: "client-to-target",
2524
+ sequence: chunk1.length
2525
+ }, chunk1))
2526
+
2527
+ let echo1Received = false
2528
+ while (!echo1Received) {
2529
+ for (const frame of decoder1.push(await readOnce(tunnel1))) {
2530
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
2531
+ echo1Received = true
2532
+ }
2533
+ }
2534
+ }
2535
+
2536
+ const originalCompress = /** @type {NonNullable<typeof zlib.zstdCompress>} */ (zlib.zstdCompress)
2537
+ /** @type {import('node:zlib').CompressCallback | undefined} */
2538
+ let heldCallback
2539
+ /** @type {Buffer | undefined} */
2540
+ let heldInput
2541
+ /** @type {object | undefined} */
2542
+ let heldOptions
2543
+ /** @type {(() => void) | undefined} */
2544
+ let resolveCompressorEntered
2545
+
2546
+ const compressorEntered = new Promise((resolve) => {
2547
+ resolveCompressorEntered = () => resolve(undefined)
2548
+ })
2549
+
2550
+ // Test-only monkey-patch: holds the next zstd compression callback to
2551
+ // reproduce a send-queue race where async encode outlives a tunnel close.
2552
+ /**
2553
+ * @param {import('node:zlib').InputType} input
2554
+ * @param {import('node:zlib').ZstdOptions} options
2555
+ * @param {import('node:zlib').CompressCallback} callback
2556
+ * @returns {void}
2557
+ */
2558
+ const patchedCompress = (input, options, callback) => {
2559
+ if (heldCallback === undefined) {
2560
+ heldCallback = callback
2561
+ heldInput = /** @type {Buffer} */ (/** @type {unknown} */ (input))
2562
+ heldOptions = options
2563
+ if (resolveCompressorEntered) resolveCompressorEntered()
2564
+ return
2565
+ }
2566
+ originalCompress(input, options, callback)
2567
+ }
2568
+ // @ts-expect-error - TS lib __promisify__ required on typeof zstdCompress vs runtime
2569
+ zlib.zstdCompress = patchedCompress
2570
+
2571
+ try {
2572
+ const chunk2 = compressibleChunk(16384)
2573
+ targetSocket?.write(chunk2)
2574
+ await compressorEntered
2575
+
2576
+ // Awaiting this relay helper means its server-side close handler has removed
2577
+ // the sender, so the next target write is definitely detached.
2578
+ await relay.closeActiveTunnel()
2579
+ await waitClose(tunnel1)
2580
+
2581
+ const chunk3 = Buffer.from("detached-after-delayed-send")
2582
+ targetSocket?.write(chunk3)
2583
+
2584
+ const tunnel2 = createConnection({host: "127.0.0.1", port: relay.port})
2585
+ const decoder2 = new FrameDecoder({compactDirection: "target-to-client"})
2586
+ await waitConnect(tunnel2)
2587
+ tunnel2.write(encodeFrame({
2588
+ type: "session.resume",
2589
+ token: token.value,
2590
+ identifier: "session-zstd-queue-race",
2591
+ compression: ["zstd"]
2592
+ }))
2593
+
2594
+ let resumed = false
2595
+ while (!resumed) {
2596
+ for (const frame of decoder2.push(await readOnceWithin(tunnel2))) {
2597
+ if (frame.header.type === "session.resumed") {
2598
+ resumed = true
2599
+ }
2600
+ }
2601
+ }
2602
+
2603
+ // Disconnect again while the first send still owns the queue. The queued
2604
+ // flush must leave chunk3 buffered for the next resume.
2605
+ // Ensure the queued resume flush sees its sender removed before completion.
2606
+ const closingTunnel2 = relay.closeActiveTunnel()
2607
+ await waitClose(tunnel2)
2608
+
2609
+ zlib.zstdCompress = originalCompress
2610
+
2611
+ if (heldCallback !== undefined && heldInput !== undefined && heldOptions !== undefined) {
2612
+ originalCompress(heldInput, heldOptions, heldCallback)
2613
+ }
2614
+ await closingTunnel2
2615
+
2616
+ const tunnel3 = createConnection({host: "127.0.0.1", port: relay.port})
2617
+ const decoder3 = new FrameDecoder({compactDirection: "target-to-client"})
2618
+ await waitConnect(tunnel3)
2619
+ tunnel3.write(encodeFrame({
2620
+ type: "session.resume",
2621
+ token: token.value,
2622
+ identifier: "session-zstd-queue-race",
2623
+ compression: ["zstd"]
2624
+ }))
2625
+
2626
+ /** @type {Array<{sequence: number, chunk: Buffer}>} */
2627
+ const received = []
2628
+ let resumedAgain = false
2629
+ let done = false
2630
+ while (!done || !resumedAgain) {
2631
+ const nextFrameBytes = await readOnceWithin(tunnel3)
2632
+ for (const frame of decoder3.push(nextFrameBytes)) {
2633
+ if (frame.header.type === "session.resumed") resumedAgain = true
2634
+ if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
2635
+ received.push({
2636
+ sequence: /** @type {number} */ (frame.header.sequence),
2637
+ chunk: Buffer.from(frame.payload)
2638
+ })
2639
+ if (received.length >= 3) {
2640
+ done = true
2641
+ }
2642
+ }
2643
+ }
2644
+ }
2645
+
2646
+ assert.equal(received.length, 3, "must receive exactly three target-to-client frames — no drops, no duplicates")
2647
+ const frame1 = /** @type {NonNullable<typeof received[0]>} */ (received[0])
2648
+ const frame2 = /** @type {NonNullable<typeof received[1]>} */ (received[1])
2649
+ const frame3 = /** @type {NonNullable<typeof received[2]>} */ (received[2])
2650
+ assert.ok(frame1.sequence < frame2.sequence && frame2.sequence < frame3.sequence, "sequences must be in ascending order")
2651
+ assert.equal(frame1.sequence, chunk1.length, "first frame sequence must equal chunk1 size")
2652
+ assert.equal(frame2.sequence, chunk1.length + chunk2.length, "second frame sequence must equal cumulative size")
2653
+ assert.equal(frame3.sequence, chunk1.length + chunk2.length + chunk3.length, "detached frame must follow the delayed pre-close frame")
2654
+ assert.ok(frame1.chunk.length < chunk1.length, "wire frame for chunk1 must be compressed")
2655
+ assert.ok(frame2.chunk.length < chunk2.length, "wire frame for chunk2 must be compressed")
2656
+ assert.deepEqual(frame3.chunk, chunk3)
2657
+
2658
+ tunnel3.destroy()
2659
+ } finally {
2660
+ zlib.zstdCompress = originalCompress
2661
+ await relay.close()
2662
+ await new Promise((resolve, reject) => {
2663
+ targetServer.close((error) => {
2664
+ if (error) { reject(error); return }
2665
+ resolve(undefined)
2666
+ })
2667
+ })
2668
+ }
2669
+ })
2670
+
2671
+ test("TCP relay enforces detached buffer overflow while an earlier zstd send is stalled", {skip: !zstdAvailable()}, async () => {
2672
+ /** @type {import('node:net').Socket | undefined} */
2673
+ let targetSocket
2674
+ const targetServer = createServer((socket) => {
2675
+ targetSocket = socket
2676
+ socket.resume()
2677
+ })
2678
+ const targetPort = await listen(targetServer)
2679
+ const users = new InMemoryUserStore()
2680
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
2681
+ await users.createUser({username: "buffer-race", password: "correct horse battery staple"})
2682
+ const token = await login(users, tokens, {username: "buffer-race", password: "correct horse battery staple"})
2683
+ const sessions = new SessionStore({now: () => 1000})
2684
+ const relay = await startTcpRelay({
2685
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
2686
+ tokens,
2687
+ sessions,
2688
+ resumeWindowMs: 2_000,
2689
+ maxTargetBufferBytes: 16
2690
+ })
2691
+ const tunnel1 = createConnection({host: "127.0.0.1", port: relay.port})
2692
+ const decoder1 = new FrameDecoder({compactDirection: "target-to-client"})
2693
+ let hold
2694
+ try {
2695
+ await waitConnect(tunnel1)
2696
+ tunnel1.write(encodeFrame({
2697
+ type: "session.create", token: token.value, name: "buffer-race", identifier: "buffer-race",
2698
+ target: {host: "127.0.0.1", port: targetPort}, compression: ["zstd"]
2699
+ }))
2700
+ decoder1.push(await readOnceWithin(tunnel1))
2701
+ await waitUntil(() => targetSocket !== undefined)
2702
+
2703
+ const prime = compressibleChunk(8_192)
2704
+ targetSocket?.write(prime)
2705
+ decoder1.push(await readOnceWithin(tunnel1))
2706
+
2707
+ hold = holdNextZstdCompression()
2708
+ targetSocket?.write(compressibleChunk(16_384))
2709
+ await Promise.race([hold.entered, new Promise((resolve, reject) => setTimeout(() => reject(new Error("compressor did not stall")), 1_000))])
2710
+ // Await relay-side detachment before testing detached-buffer admission.
2711
+ await relay.closeActiveTunnel()
2712
+ await waitClose(tunnel1)
2713
+
2714
+ const targetClosed = targetSocket === undefined ? Promise.reject(new Error("target socket missing")) : waitClose(targetSocket)
2715
+ targetSocket?.write(Buffer.alloc(32, 0x42))
2716
+ await Promise.race([targetClosed, new Promise((resolve, reject) => setTimeout(() => reject(new Error("overflow did not close target immediately")), 1_000))])
2717
+
2718
+ const tunnel2 = createConnection({host: "127.0.0.1", port: relay.port})
2719
+ const decoder2 = new FrameDecoder({compactDirection: "target-to-client"})
2720
+ await waitConnect(tunnel2)
2721
+ tunnel2.write(encodeFrame({type: "session.resume", token: token.value, identifier: "buffer-race", compression: ["zstd"]}))
2722
+ hold.release()
2723
+ let overflow
2724
+ while (overflow === undefined) {
2725
+ for (const frame of decoder2.push(await readOnceWithin(tunnel2))) {
2726
+ if (frame.header.code === "TARGET_BUFFER_OVERFLOW") overflow = frame.header
2727
+ }
2728
+ }
2729
+ assert.equal(overflow.retryable, false)
2730
+ tunnel2.destroy()
2731
+ } finally {
2732
+ hold?.restore()
2733
+ tunnel1.destroy()
2734
+ await relay.close()
2735
+ await new Promise((resolve) => targetServer.close(() => resolve(undefined)))
2736
+ }
2737
+ })
2738
+
2739
+ test("TCP relay ignores delayed send completion after expiry and identifier reuse", {skip: !zstdAvailable()}, async () => {
2740
+ /** @type {import('node:net').Socket[]} */
2741
+ const targetSockets = []
2742
+ const targetServer = createServer((socket) => {
2743
+ targetSockets.push(socket)
2744
+ socket.resume()
2745
+ })
2746
+ const targetPort = await listen(targetServer)
2747
+ const users = new InMemoryUserStore()
2748
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
2749
+ await users.createUser({username: "expiry-race", password: "correct horse battery staple"})
2750
+ const token = await login(users, tokens, {username: "expiry-race", password: "correct horse battery staple"})
2751
+ const sessions = new SessionStore({now: () => 1000})
2752
+ const relay = await startTcpRelay({
2753
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
2754
+ tokens,
2755
+ sessions,
2756
+ resumeWindowMs: 50
2757
+ })
2758
+ const tunnel1 = createConnection({host: "127.0.0.1", port: relay.port})
2759
+ let tunnel2
2760
+ let hold
2761
+ try {
2762
+ await waitConnect(tunnel1)
2763
+ const decoder1 = new FrameDecoder({compactDirection: "target-to-client"})
2764
+ tunnel1.write(encodeFrame({
2765
+ type: "session.create", token: token.value, name: "old lifecycle", identifier: "reused-lifecycle",
2766
+ target: {host: "127.0.0.1", port: targetPort}, compression: ["zstd"]
2767
+ }))
2768
+ decoder1.push(await readOnceWithin(tunnel1))
2769
+ await waitUntil(() => targetSockets.length === 1)
2770
+ const oldTarget = targetSockets[0]
2771
+ assert.ok(oldTarget !== undefined)
2772
+ oldTarget.write(compressibleChunk(8_192))
2773
+ decoder1.push(await readOnceWithin(tunnel1))
2774
+
2775
+ hold = holdNextZstdCompression()
2776
+ const staleChunk = compressibleChunk(16_384)
2777
+ oldTarget.write(staleChunk)
2778
+ await Promise.race([hold.entered, new Promise((resolve, reject) => setTimeout(() => reject(new Error("compressor did not stall")), 1_000))])
2779
+ tunnel1.destroy()
2780
+ await waitClose(tunnel1)
2781
+
2782
+ await new Promise((resolve) => setTimeout(resolve, 100))
2783
+ assert.equal(sessions.get("reused-lifecycle"), undefined)
2784
+
2785
+ tunnel2 = createConnection({host: "127.0.0.1", port: relay.port})
2786
+ const decoder2 = new FrameDecoder({compactDirection: "target-to-client"})
2787
+ await waitConnect(tunnel2)
2788
+ tunnel2.write(encodeFrame({
2789
+ type: "session.create", token: token.value, name: "new lifecycle", identifier: "reused-lifecycle",
2790
+ target: {host: "127.0.0.1", port: targetPort}
2791
+ }))
2792
+ const created = decoder2.push(await readOnceWithin(tunnel2))
2793
+ assert.equal(created[0]?.header.type, "session.created")
2794
+ await waitUntil(() => targetSockets.length === 2)
2795
+ hold.release()
2796
+ await new Promise((resolve) => setTimeout(resolve, 50))
2797
+
2798
+ const newTarget = targetSockets[1]
2799
+ assert.ok(newTarget !== undefined)
2800
+ const freshChunk = Buffer.from("fresh-lifecycle-data")
2801
+ newTarget.write(freshChunk)
2802
+ let freshFrame
2803
+ while (freshFrame === undefined) {
2804
+ for (const frame of decoder2.push(await readOnceWithin(tunnel2))) {
2805
+ if (frame.header.type === "stream.data") freshFrame = frame
2806
+ }
2807
+ }
2808
+ assert.equal(freshFrame.header.sequence, freshChunk.length, "stale completion must not advance the reused session")
2809
+ assert.deepEqual(freshFrame.payload, freshChunk)
2810
+ } finally {
2811
+ hold?.restore()
2812
+ tunnel1.destroy()
2813
+ tunnel2?.destroy()
2814
+ await relay.close()
2815
+ await new Promise((resolve) => targetServer.close(() => resolve(undefined)))
2816
+ }
2817
+ })
2818
+
2819
+ test("compact v2 relay coalesces only target chunks naturally queued behind async compression", {skip: !zstdAvailable()}, async () => {
2820
+ /** @type {import('node:net').Socket | undefined} */
2821
+ let targetSocket
2822
+ const targetServer = createServer((socket) => {
2823
+ targetSocket = socket
2824
+ socket.setNoDelay(true)
2825
+ })
2826
+ const targetPort = await listen(targetServer)
2827
+ const users = new InMemoryUserStore()
2828
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
2829
+ await users.createUser({username: "compact-coalesce", password: "correct horse battery staple"})
2830
+ const token = await login(users, tokens, {username: "compact-coalesce", password: "correct horse battery staple"})
2831
+ const relay = await startTcpRelay({
2832
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
2833
+ tokens,
2834
+ sessions: new SessionStore({now: () => 1000})
2835
+ })
2836
+ const tunnel = createConnection({host: "127.0.0.1", port: relay.port})
2837
+ const decoder = new FrameDecoder({compactDirection: "target-to-client"})
2838
+ let hold
2839
+ try {
2840
+ await waitConnect(tunnel)
2841
+ tunnel.write(encodeFrame({
2842
+ type: "session.create", token: token.value, name: "compact coalescing",
2843
+ target: {host: "127.0.0.1", port: targetPort}, compression: ["zstd"], framing: [2]
2844
+ }))
2845
+ const created = decoder.push(await readOnceWithin(tunnel))[0]
2846
+ assert.equal(created?.header.framing, 2)
2847
+ assert.equal(created?.header.compression, "zstd")
2848
+ await waitUntil(() => targetSocket !== undefined)
2849
+
2850
+ hold = holdNextZstdCompression()
2851
+ const first = compressibleChunk(16_384)
2852
+ const second = compressibleChunk(4_096)
2853
+ const third = compressibleChunk(2_048)
2854
+ targetSocket?.write(first)
2855
+ await Promise.race([hold.entered, new Promise((resolve, reject) => setTimeout(() => reject(new Error("compressor did not stall")), 1_000))])
2856
+ targetSocket?.write(second)
2857
+ await new Promise((resolve) => setTimeout(resolve, 10))
2858
+ targetSocket?.write(third)
2859
+ await new Promise((resolve) => setTimeout(resolve, 10))
2860
+ hold.release()
2861
+
2862
+ const dataFrames = []
2863
+ while (dataFrames.length < 2) {
2864
+ for (const frame of decoder.push(await readOnceWithin(tunnel))) {
2865
+ if (frame.header.type === "stream.data") dataFrames.push(frame)
2866
+ }
2867
+ }
2868
+ assert.equal(dataFrames.length, 2, "the first chunk is isolated and only already-queued adjacent chunks merge")
2869
+ assert.equal(dataFrames[0]?.header.sequence, first.length)
2870
+ assert.equal(dataFrames[1]?.header.sequence, first.length + second.length + third.length)
2871
+ assert.equal(dataFrames[1]?.header.dataLength, second.length + third.length)
2872
+ } finally {
2873
+ hold?.restore()
2874
+ tunnel.destroy()
2875
+ await relay.close()
2876
+ await new Promise((resolve) => targetServer.close(() => resolve(undefined)))
2877
+ }
2878
+ })