socketduct 0.1.25 → 0.1.26

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 (56) hide show
  1. package/AGENTS.md +2 -2
  2. package/changelog.d/20261010130509-bound-log-frame-compression-memory.md +1 -0
  3. package/docs/runbooks/jsonl-integrity-rollout.md +8 -6
  4. package/package.json +2 -2
  5. package/scripts/test.js +9 -14
  6. package/src/logging/compressed-log-writer.js +1 -1
  7. package/src/logging/zstd-frame-codec.js +15 -1
  8. package/test/e2e/gateway-process.test.js +93 -92
  9. package/test/e2e/logger-shutdown-process.test.js +40 -37
  10. package/test/e2e/reverse-client-recovery-process.test.js +107 -103
  11. package/test/e2e/reverse-gateway-process.test.js +328 -318
  12. package/test/integration/initial-target-recovery.test.js +333 -334
  13. package/test/integration/logging-cli.test.js +121 -119
  14. package/test/integration/logging-compressed-recovery.test.js +96 -95
  15. package/test/integration/multiplexed-compatibility.test.js +178 -177
  16. package/test/integration/multiplexed-corrections.test.js +360 -359
  17. package/test/integration/multiplexed-failures.test.js +253 -251
  18. package/test/integration/multiplexed-transport-set.test.js +334 -322
  19. package/test/integration/multiplexed-transport.test.js +135 -134
  20. package/test/integration/reverse-gateway-stream-id-reset.test.js +90 -89
  21. package/test/integration/reverse-gateway.test.js +2309 -2218
  22. package/test/integration/reverse-resume-continuity.test.js +219 -218
  23. package/test/integration/reverse-route-class.test.js +358 -341
  24. package/test/integration/socketduct-http-agent.test.js +314 -312
  25. package/test/integration/socketduct-https-agent.test.js +516 -485
  26. package/test/integration/stream-compression.test.js +862 -824
  27. package/test/integration/tcp-forwarding.test.js +2550 -2447
  28. package/test/integration/tls-forwarding.test.js +593 -592
  29. package/test/unit/auth/auth.test.js +43 -44
  30. package/test/unit/auth/static-token-store.test.js +9 -8
  31. package/test/unit/client/connect.test.js +41 -40
  32. package/test/unit/client/deferred-virtual-socket.test.js +95 -94
  33. package/test/unit/client/multiplexed-spool-budget.test.js +155 -154
  34. package/test/unit/client/multiplexed-transport-set.test.js +830 -748
  35. package/test/unit/client/socketduct-socket.test.js +768 -771
  36. package/test/unit/client/virtual-socket.test.js +198 -168
  37. package/test/unit/config/docker-gateway-files.test.js +224 -224
  38. package/test/unit/config/environment.test.js +472 -492
  39. package/test/unit/index.test.js +53 -52
  40. package/test/unit/logging/compressed-log-worker.test.js +23 -22
  41. package/test/unit/logging/compressed-log-writer.test.js +155 -154
  42. package/test/unit/logging/log-retention.test.js +52 -47
  43. package/test/unit/logging/log-segment-index.test.js +84 -81
  44. package/test/unit/logging/zstd-frame-codec.test.js +28 -19
  45. package/test/unit/protocol/compression.test.js +254 -298
  46. package/test/unit/protocol/frame.test.js +280 -276
  47. package/test/unit/protocol/v3-compression.test.js +21 -23
  48. package/test/unit/protocol/v3-frame.test.js +42 -41
  49. package/test/unit/release-configuration.test.js +15 -12
  50. package/test/unit/reverse/disconnect-attribution.test.js +65 -64
  51. package/test/unit/reverse-terminal-outcomes.test.js +53 -50
  52. package/test/unit/server/control-handler.test.js +232 -231
  53. package/test/unit/server/primary.test.js +51 -49
  54. package/test/unit/server/relay-runtime.test.js +280 -294
  55. package/test/unit/sessions/session.test.js +48 -62
  56. package/test/unit/spool/spool-file.test.js +401 -385
@@ -1,9 +1,8 @@
1
- import assert from "node:assert/strict"
1
+ import {describe, expect, it, withTestCleanup} from "@velocious/testing"
2
2
  import {createConnection, createServer} from "node:net"
3
3
  import {mkdtempSync, rmSync} from "node:fs"
4
4
  import {tmpdir} from "node:os"
5
5
  import {join} from "node:path"
6
- import test from "node:test"
7
6
 
8
7
  import {encodeFrame, FrameDecoder} from "../../../src/protocol/frame.js"
9
8
  import {encodeV3Control, encodeV3Frame, V3FrameType} from "../../../src/protocol/v3-frame.js"
@@ -47,142 +46,163 @@ async function startTargetServer() {
47
46
  }
48
47
  }
49
48
 
50
- test("startRelayRuntime starts a token-authenticated allowlisted relay", async () => {
51
- const target = await startTargetServer()
52
- const runtime = await startRelayRuntime({
53
- mode: "relay",
54
- relayHost: "127.0.0.1",
55
- relayPort: 0,
56
- resumeWindowMs: 30_000,
57
- maxTargetBufferBytes: 1_048_576,
58
- maxTotalBufferBytes: 16_777_216,
59
- handshakeTimeoutMs: 10_000,
60
- maxInputBytesPerConnection: 65_536,
61
- authTokens: [{username: "edge", token: "secret-token"}],
62
- targetAllowlist: [{host: target.host, port: target.port}],
63
- allowInsecurePublic: false
64
- })
65
-
66
- test.after(async () => {
67
- await runtime.close()
68
- await target.close()
69
- })
70
-
71
- assert.deepEqual(runtime.relay.status(), {
72
- activeTunnels: 0,
73
- activeTargets: 0,
74
- sessions: 0,
75
- attachedSessions: 0,
76
- detachedSessions: 0,
77
- bufferedTargetBytes: 0,
78
- targetConnectionAttempts: 0,
79
- targetRetryTimers: 0
49
+ /**
50
+ * @param {import('node:net').Socket} socket - Connected relay socket.
51
+ * @param {Record<string, unknown> & {type: string}} header - Control frame header.
52
+ * @returns {Promise<Record<string, unknown>>} Decoded response header.
53
+ */
54
+ async function sendControl(socket, header) {
55
+ const decoder = new FrameDecoder()
56
+ return await new Promise((resolve, reject) => {
57
+ socket.on("data", (chunk) => {
58
+ for (const frame of decoder.push(chunk)) {
59
+ resolve(frame.header)
60
+ }
61
+ })
62
+ socket.once("error", reject)
63
+ const framedHeader = header.type === "session.create" || header.type === "session.resume"
64
+ ? {...header, framing: header.framing ?? [2]}
65
+ : header
66
+ socket.write(encodeFrame(framedHeader))
80
67
  })
68
+ }
81
69
 
82
- const socket = await new Promise((resolve, reject) => {
83
- const connection = createConnection({host: "127.0.0.1", port: runtime.relay.port})
84
- connection.once("error", reject)
85
- connection.once("connect", () => {
86
- connection.off("error", reject)
87
- resolve(connection)
70
+ describe("relay runtime", () => {
71
+ it("startRelayRuntime starts a token-authenticated allowlisted relay", async () => withTestCleanup(async (after) => {
72
+ const target = await startTargetServer()
73
+ const runtime = await startRelayRuntime({
74
+ mode: "relay",
75
+ relayHost: "127.0.0.1",
76
+ relayPort: 0,
77
+ resumeWindowMs: 30_000,
78
+ maxTargetBufferBytes: 1_048_576,
79
+ maxTotalBufferBytes: 16_777_216,
80
+ handshakeTimeoutMs: 10_000,
81
+ maxInputBytesPerConnection: 65_536,
82
+ authTokens: [{username: "edge", token: "secret-token"}],
83
+ targetAllowlist: [{host: target.host, port: target.port}],
84
+ allowInsecurePublic: false
88
85
  })
89
- })
90
86
 
91
- const response = await sendControl(socket, {
92
- type: "session.create",
93
- token: "secret-token",
94
- name: "docker-test",
95
- target: {host: target.host, port: target.port}
96
- })
87
+ after(async () => {
88
+ await runtime.close()
89
+ await target.close()
90
+ })
97
91
 
98
- assert.equal(response.type, "session.created")
99
- socket.destroy()
100
- })
92
+ expect(runtime.relay.status()).toEqual({
93
+ activeTunnels: 0,
94
+ activeTargets: 0,
95
+ sessions: 0,
96
+ attachedSessions: 0,
97
+ detachedSessions: 0,
98
+ bufferedTargetBytes: 0,
99
+ targetConnectionAttempts: 0,
100
+ targetRetryTimers: 0
101
+ })
101
102
 
102
- test("startRelayRuntime can select protocol v3 while v2 remains the default", async () => {
103
- const target = await startTargetServer()
104
- const runtime = await startRelayRuntime({
105
- mode: "relay",
106
- protocolVersion: 3,
107
- relayHost: "127.0.0.1",
108
- relayPort: 0,
109
- resumeWindowMs: 30_000,
110
- maxTargetBufferBytes: 1_048_576,
111
- maxTotalBufferBytes: 16_777_216,
112
- handshakeTimeoutMs: 10_000,
113
- maxInputBytesPerConnection: 65_536,
114
- authTokens: [{username: "edge", token: "secret-token"}],
115
- targetAllowlist: [{host: target.host, port: target.port}],
116
- allowInsecurePublic: false
117
- })
118
- const connection = new MultiplexedSocketductConnection({
119
- relay: {host: "127.0.0.1", port: runtime.relay.port, token: "secret-token"}
120
- })
121
- try {
122
- const socket = connection.createConnection({host: target.host, port: target.port})
123
- await new Promise((resolve, reject) => {
124
- socket.once("connect", resolve)
125
- socket.once("error", reject)
103
+ const socket = await new Promise((resolve, reject) => {
104
+ const connection = createConnection({host: "127.0.0.1", port: runtime.relay.port})
105
+ connection.once("error", reject)
106
+ connection.once("connect", () => {
107
+ connection.off("error", reject)
108
+ resolve(connection)
109
+ })
126
110
  })
127
- const response = new Promise((resolve, reject) => {
128
- socket.once("data", (chunk) => resolve(chunk.toString()))
129
- socket.once("error", reject)
111
+
112
+ const response = await sendControl(socket, {
113
+ type: "session.create",
114
+ token: "secret-token",
115
+ name: "docker-test",
116
+ target: {host: target.host, port: target.port}
130
117
  })
131
- socket.write("v3-runtime")
132
- assert.equal(await response, "v3-runtime")
133
- assert.equal(/** @type {{physicalAccepts: number}} */ (runtime.relay.status()).physicalAccepts, 1)
118
+
119
+ expect(response.type).toBe("session.created")
134
120
  socket.destroy()
135
- } finally {
136
- await connection.close()
137
- await runtime.close()
138
- await target.close()
139
- }
140
- })
121
+ }))
141
122
 
142
- test("startRelayRuntime enforces configured cumulative v3 unauthenticated input bytes", async () => {
143
- const runtime = await startRelayRuntime({
144
- mode: "relay",
145
- protocolVersion: 3,
146
- relayHost: "127.0.0.1",
147
- relayPort: 0,
148
- resumeWindowMs: 30_000,
149
- maxTargetBufferBytes: 1_048_576,
150
- maxTotalBufferBytes: 16_777_216,
151
- handshakeTimeoutMs: 10_000,
152
- maxInputBytesPerConnection: 32,
153
- authTokens: [{username: "edge", token: "secret-token"}],
154
- targetAllowlist: [{host: "127.0.0.1", port: 1}],
155
- allowInsecurePublic: false
123
+ it("startRelayRuntime can select protocol v3 while v2 remains the default", async () => {
124
+ const target = await startTargetServer()
125
+ const runtime = await startRelayRuntime({
126
+ mode: "relay",
127
+ protocolVersion: 3,
128
+ relayHost: "127.0.0.1",
129
+ relayPort: 0,
130
+ resumeWindowMs: 30_000,
131
+ maxTargetBufferBytes: 1_048_576,
132
+ maxTotalBufferBytes: 16_777_216,
133
+ handshakeTimeoutMs: 10_000,
134
+ maxInputBytesPerConnection: 65_536,
135
+ authTokens: [{username: "edge", token: "secret-token"}],
136
+ targetAllowlist: [{host: target.host, port: target.port}],
137
+ allowInsecurePublic: false
138
+ })
139
+ const connection = new MultiplexedSocketductConnection({
140
+ relay: {host: "127.0.0.1", port: runtime.relay.port, token: "secret-token"}
141
+ })
142
+ try {
143
+ const socket = connection.createConnection({host: target.host, port: target.port})
144
+ await new Promise((resolve, reject) => {
145
+ socket.once("connect", resolve)
146
+ socket.once("error", reject)
147
+ })
148
+ const response = new Promise((resolve, reject) => {
149
+ socket.once("data", (chunk) => resolve(chunk.toString()))
150
+ socket.once("error", reject)
151
+ })
152
+ socket.write("v3-runtime")
153
+ expect(await response).toBe("v3-runtime")
154
+ expect(/** @type {{physicalAccepts: number}} */ (runtime.relay.status()).physicalAccepts).toBe(1)
155
+ socket.destroy()
156
+ } finally {
157
+ await connection.close()
158
+ await runtime.close()
159
+ await target.close()
160
+ }
156
161
  })
157
- const socket = createConnection({host: "127.0.0.1", port: runtime.relay.port})
158
- try {
159
- await new Promise((resolve, reject) => {
160
- socket.once("connect", resolve)
161
- socket.once("error", reject)
162
+
163
+ it("startRelayRuntime enforces configured cumulative v3 unauthenticated input bytes", async () => {
164
+ const runtime = await startRelayRuntime({
165
+ mode: "relay",
166
+ protocolVersion: 3,
167
+ relayHost: "127.0.0.1",
168
+ relayPort: 0,
169
+ resumeWindowMs: 30_000,
170
+ maxTargetBufferBytes: 1_048_576,
171
+ maxTotalBufferBytes: 16_777_216,
172
+ handshakeTimeoutMs: 10_000,
173
+ maxInputBytesPerConnection: 32,
174
+ authTokens: [{username: "edge", token: "secret-token"}],
175
+ targetAllowlist: [{host: "127.0.0.1", port: 1}],
176
+ allowInsecurePublic: false
162
177
  })
163
- const auth = encodeV3Frame({type: V3FrameType.AUTH, streamId: 0, sequence: 1}, encodeV3Control({
164
- version: 3,
165
- token: "secret-token",
166
- connectionId: "runtime_input_limit_01",
167
- generation: 1
168
- }))
169
- assert(auth.length > 33)
170
- socket.resume()
171
- const closed = new Promise((resolve) => socket.once("close", resolve))
172
- socket.write(auth.subarray(0, 33))
173
- await Promise.race([
174
- closed,
175
- new Promise((_, reject) => setTimeout(() => reject(new Error("v3 unauthenticated input limit was not enforced")), 250))
176
- ])
177
- } finally {
178
- socket.destroy()
179
- await runtime.close()
180
- }
181
- })
178
+ const socket = createConnection({host: "127.0.0.1", port: runtime.relay.port})
179
+ try {
180
+ await new Promise((resolve, reject) => {
181
+ socket.once("connect", resolve)
182
+ socket.once("error", reject)
183
+ })
184
+ const auth = encodeV3Frame({type: V3FrameType.AUTH, streamId: 0, sequence: 1}, encodeV3Control({
185
+ version: 3,
186
+ token: "secret-token",
187
+ connectionId: "runtime_input_limit_01",
188
+ generation: 1
189
+ }))
190
+ expect(auth.length > 33).toBeTruthy()
191
+ socket.resume()
192
+ const closed = new Promise((resolve) => socket.once("close", resolve))
193
+ socket.write(auth.subarray(0, 33))
194
+ await Promise.race([
195
+ closed,
196
+ new Promise((_, reject) => setTimeout(() => reject(new Error("v3 unauthenticated input limit was not enforced")), 250))
197
+ ])
198
+ } finally {
199
+ socket.destroy()
200
+ await runtime.close()
201
+ }
202
+ })
182
203
 
183
- test("startRelayRuntime requires tokens and target allowlist", async () => {
184
- await assert.rejects(
185
- () => startRelayRuntime({
204
+ it("startRelayRuntime requires tokens and target allowlist", async () => {
205
+ await expect(startRelayRuntime({
186
206
  mode: "relay",
187
207
  relayHost: "127.0.0.1",
188
208
  relayPort: 0,
@@ -194,12 +214,9 @@ test("startRelayRuntime requires tokens and target allowlist", async () => {
194
214
  authTokens: [],
195
215
  targetAllowlist: [{host: "127.0.0.1", port: 1}],
196
216
  allowInsecurePublic: false
197
- }),
198
- /SOCKETDUCT_AUTH_TOKENS/
199
- )
217
+ })).rejects.toThrow(/SOCKETDUCT_AUTH_TOKENS/)
200
218
 
201
- await assert.rejects(
202
- () => startRelayRuntime({
219
+ await expect(startRelayRuntime({
203
220
  mode: "relay",
204
221
  relayHost: "127.0.0.1",
205
222
  relayPort: 0,
@@ -211,35 +228,11 @@ test("startRelayRuntime requires tokens and target allowlist", async () => {
211
228
  authTokens: [{username: "edge", token: "secret-token"}],
212
229
  targetAllowlist: [],
213
230
  allowInsecurePublic: false
214
- }),
215
- /SOCKETDUCT_TARGET_ALLOWLIST/
216
- )
217
- })
218
-
219
- /**
220
- * @param {import('node:net').Socket} socket - Connected relay socket.
221
- * @param {Record<string, unknown> & {type: string}} header - Control frame header.
222
- * @returns {Promise<Record<string, unknown>>} Decoded response header.
223
- */
224
- async function sendControl(socket, header) {
225
- const decoder = new FrameDecoder()
226
- return await new Promise((resolve, reject) => {
227
- socket.on("data", (chunk) => {
228
- for (const frame of decoder.push(chunk)) {
229
- resolve(frame.header)
230
- }
231
- })
232
- socket.once("error", reject)
233
- const framedHeader = header.type === "session.create" || header.type === "session.resume"
234
- ? {...header, framing: header.framing ?? [2]}
235
- : header
236
- socket.write(encodeFrame(framedHeader))
231
+ })).rejects.toThrow(/SOCKETDUCT_TARGET_ALLOWLIST/)
237
232
  })
238
- }
239
233
 
240
- test("startRelayRuntime refuses non-loopback public bind without TLS or opt-out", async () => {
241
- await assert.rejects(
242
- () => startRelayRuntime({
234
+ it("startRelayRuntime refuses non-loopback public bind without TLS or opt-out", async () => {
235
+ await expect(startRelayRuntime({
243
236
  mode: "relay",
244
237
  relayHost: "0.0.0.0",
245
238
  relayPort: 0,
@@ -251,12 +244,9 @@ test("startRelayRuntime refuses non-loopback public bind without TLS or opt-out"
251
244
  authTokens: [{username: "edge", token: "secret-token"}],
252
245
  targetAllowlist: [{host: "127.0.0.1", port: 1}],
253
246
  allowInsecurePublic: false
254
- }),
255
- /public host.*without TLS/
256
- )
247
+ })).rejects.toThrow(/public host.*without TLS/)
257
248
 
258
- await assert.rejects(
259
- () => startRelayRuntime({
249
+ await expect(startRelayRuntime({
260
250
  mode: "relay",
261
251
  relayHost: "192.168.1.1",
262
252
  relayPort: 0,
@@ -268,125 +258,122 @@ test("startRelayRuntime refuses non-loopback public bind without TLS or opt-out"
268
258
  authTokens: [{username: "edge", token: "secret-token"}],
269
259
  targetAllowlist: [{host: "127.0.0.1", port: 1}],
270
260
  allowInsecurePublic: false
271
- }),
272
- /public host.*without TLS/
273
- )
274
- })
275
-
276
- test("startRelayRuntime allows non-loopback public bind with explicit opt-out", async () => {
277
- const target = await startTargetServer()
278
- const runtime = await startRelayRuntime({
279
- mode: "relay",
280
- relayHost: "0.0.0.0",
281
- relayPort: 0,
282
- resumeWindowMs: 30_000,
283
- maxTargetBufferBytes: 1_048_576,
284
- maxTotalBufferBytes: 16_777_216,
285
- handshakeTimeoutMs: 10_000,
286
- maxInputBytesPerConnection: 65_536,
287
- authTokens: [{username: "edge", token: "secret-token"}],
288
- targetAllowlist: [{host: target.host, port: target.port}],
289
- allowInsecurePublic: true
261
+ })).rejects.toThrow(/public host.*without TLS/)
290
262
  })
291
263
 
292
- test.after(async () => {
293
- await runtime.close()
294
- await target.close()
295
- })
264
+ it("startRelayRuntime allows non-loopback public bind with explicit opt-out", async () => withTestCleanup(async (after) => {
265
+ const target = await startTargetServer()
266
+ const runtime = await startRelayRuntime({
267
+ mode: "relay",
268
+ relayHost: "0.0.0.0",
269
+ relayPort: 0,
270
+ resumeWindowMs: 30_000,
271
+ maxTargetBufferBytes: 1_048_576,
272
+ maxTotalBufferBytes: 16_777_216,
273
+ handshakeTimeoutMs: 10_000,
274
+ maxInputBytesPerConnection: 65_536,
275
+ authTokens: [{username: "edge", token: "secret-token"}],
276
+ targetAllowlist: [{host: target.host, port: target.port}],
277
+ allowInsecurePublic: true
278
+ })
296
279
 
297
- const socket = await new Promise((resolve, reject) => {
298
- const connection = createConnection({host: "127.0.0.1", port: runtime.relay.port})
299
- connection.once("error", reject)
300
- connection.once("connect", () => {
301
- connection.off("error", reject)
302
- resolve(connection)
280
+ after(async () => {
281
+ await runtime.close()
282
+ await target.close()
303
283
  })
304
- })
305
284
 
306
- const response = await sendControl(socket, {
307
- type: "session.create",
308
- token: "secret-token",
309
- name: "insecure-test",
310
- target: {host: target.host, port: target.port}
311
- })
285
+ const socket = await new Promise((resolve, reject) => {
286
+ const connection = createConnection({host: "127.0.0.1", port: runtime.relay.port})
287
+ connection.once("error", reject)
288
+ connection.once("connect", () => {
289
+ connection.off("error", reject)
290
+ resolve(connection)
291
+ })
292
+ })
312
293
 
313
- assert.equal(response.type, "session.created")
314
- socket.destroy()
315
- })
294
+ const response = await sendControl(socket, {
295
+ type: "session.create",
296
+ token: "secret-token",
297
+ name: "insecure-test",
298
+ target: {host: target.host, port: target.port}
299
+ })
316
300
 
317
- test("startRelayRuntime accepts loopback bind without TLS", async () => {
318
- const target = await startTargetServer()
319
- const runtime = await startRelayRuntime({
320
- mode: "relay",
321
- relayHost: "127.0.0.1",
322
- relayPort: 0,
323
- resumeWindowMs: 30_000,
324
- maxTargetBufferBytes: 1_048_576,
325
- maxTotalBufferBytes: 16_777_216,
326
- handshakeTimeoutMs: 10_000,
327
- maxInputBytesPerConnection: 65_536,
328
- authTokens: [{username: "edge", token: "secret-token"}],
329
- targetAllowlist: [{host: target.host, port: target.port}],
330
- allowInsecurePublic: false
331
- })
301
+ expect(response.type).toBe("session.created")
302
+ socket.destroy()
303
+ }))
332
304
 
333
- test.after(async () => {
334
- await runtime.close()
335
- await target.close()
336
- })
305
+ it("startRelayRuntime accepts loopback bind without TLS", async () => withTestCleanup(async (after) => {
306
+ const target = await startTargetServer()
307
+ const runtime = await startRelayRuntime({
308
+ mode: "relay",
309
+ relayHost: "127.0.0.1",
310
+ relayPort: 0,
311
+ resumeWindowMs: 30_000,
312
+ maxTargetBufferBytes: 1_048_576,
313
+ maxTotalBufferBytes: 16_777_216,
314
+ handshakeTimeoutMs: 10_000,
315
+ maxInputBytesPerConnection: 65_536,
316
+ authTokens: [{username: "edge", token: "secret-token"}],
317
+ targetAllowlist: [{host: target.host, port: target.port}],
318
+ allowInsecurePublic: false
319
+ })
337
320
 
338
- const socket = await new Promise((resolve, reject) => {
339
- const connection = createConnection({host: "127.0.0.1", port: runtime.relay.port})
340
- connection.once("error", reject)
341
- connection.once("connect", () => {
342
- connection.off("error", reject)
343
- resolve(connection)
321
+ after(async () => {
322
+ await runtime.close()
323
+ await target.close()
344
324
  })
345
- })
346
325
 
347
- const response = await sendControl(socket, {
348
- type: "session.create",
349
- token: "secret-token",
350
- name: "loopback-test",
351
- target: {host: target.host, port: target.port}
352
- })
326
+ const socket = await new Promise((resolve, reject) => {
327
+ const connection = createConnection({host: "127.0.0.1", port: runtime.relay.port})
328
+ connection.once("error", reject)
329
+ connection.once("connect", () => {
330
+ connection.off("error", reject)
331
+ resolve(connection)
332
+ })
333
+ })
353
334
 
354
- assert.equal(response.type, "session.created")
355
- socket.destroy()
356
- })
335
+ const response = await sendControl(socket, {
336
+ type: "session.create",
337
+ token: "secret-token",
338
+ name: "loopback-test",
339
+ target: {host: target.host, port: target.port}
340
+ })
357
341
 
358
- test("startRelayRuntime starts a TLS relay with valid certificate files", async () => {
359
- const target = await startTargetServer()
360
- const certDir = mkdtempSync(join(tmpdir(), "socketduct-tls-startup-"))
361
- const certs = generateTestCertificates(certDir)
362
-
363
- const runtime = await startRelayRuntime({
364
- mode: "relay",
365
- relayHost: "127.0.0.1",
366
- relayPort: 0,
367
- resumeWindowMs: 30_000,
368
- maxTargetBufferBytes: 1_048_576,
369
- maxTotalBufferBytes: 16_777_216,
370
- handshakeTimeoutMs: 10_000,
371
- maxInputBytesPerConnection: 65_536,
372
- authTokens: [{username: "edge", token: "secret-token"}],
373
- targetAllowlist: [{host: target.host, port: target.port}],
374
- relayTls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile},
375
- allowInsecurePublic: false
376
- })
342
+ expect(response.type).toBe("session.created")
343
+ socket.destroy()
344
+ }))
377
345
 
378
- test.after(async () => {
379
- await runtime.close()
380
- await target.close()
381
- rmSync(certDir, {recursive: true, force: true})
382
- })
346
+ it("startRelayRuntime starts a TLS relay with valid certificate files", async () => withTestCleanup(async (after) => {
347
+ const target = await startTargetServer()
348
+ const certDir = mkdtempSync(join(tmpdir(), "socketduct-tls-startup-"))
349
+ const certs = generateTestCertificates(certDir)
383
350
 
384
- assert.ok(runtime.relay.port > 0)
385
- })
351
+ const runtime = await startRelayRuntime({
352
+ mode: "relay",
353
+ relayHost: "127.0.0.1",
354
+ relayPort: 0,
355
+ resumeWindowMs: 30_000,
356
+ maxTargetBufferBytes: 1_048_576,
357
+ maxTotalBufferBytes: 16_777_216,
358
+ handshakeTimeoutMs: 10_000,
359
+ maxInputBytesPerConnection: 65_536,
360
+ authTokens: [{username: "edge", token: "secret-token"}],
361
+ targetAllowlist: [{host: target.host, port: target.port}],
362
+ relayTls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile},
363
+ allowInsecurePublic: false
364
+ })
365
+
366
+ after(async () => {
367
+ await runtime.close()
368
+ await target.close()
369
+ rmSync(certDir, {recursive: true, force: true})
370
+ })
371
+
372
+ expect(runtime.relay.port > 0).toBeTruthy()
373
+ }))
386
374
 
387
- test("startRelayRuntime fails loudly on missing TLS certificate file", async () => {
388
- await assert.rejects(
389
- () => startRelayRuntime({
375
+ it("startRelayRuntime fails loudly on missing TLS certificate file", async () => {
376
+ await expect(startRelayRuntime({
390
377
  mode: "relay",
391
378
  relayHost: "127.0.0.1",
392
379
  relayPort: 0,
@@ -399,36 +386,35 @@ test("startRelayRuntime fails loudly on missing TLS certificate file", async ()
399
386
  targetAllowlist: [{host: "127.0.0.1", port: 1}],
400
387
  relayTls: {certFile: "/nonexistent/cert.crt", keyFile: "/nonexistent/key.key"},
401
388
  allowInsecurePublic: false
402
- }),
403
- /ENOENT/
404
- )
405
- })
406
-
407
- test("startRelayRuntime allows non-loopback bind with TLS configured", async () => {
408
- const target = await startTargetServer()
409
- const certDir = mkdtempSync(join(tmpdir(), "socketduct-tls-loopback-"))
410
- const certs = generateTestCertificates(certDir)
411
-
412
- const runtime = await startRelayRuntime({
413
- mode: "relay",
414
- relayHost: "0.0.0.0",
415
- relayPort: 0,
416
- resumeWindowMs: 30_000,
417
- maxTargetBufferBytes: 1_048_576,
418
- maxTotalBufferBytes: 16_777_216,
419
- handshakeTimeoutMs: 10_000,
420
- maxInputBytesPerConnection: 65_536,
421
- authTokens: [{username: "edge", token: "secret-token"}],
422
- targetAllowlist: [{host: target.host, port: target.port}],
423
- relayTls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile},
424
- allowInsecurePublic: false
389
+ })).rejects.toThrow(/ENOENT/)
425
390
  })
426
391
 
427
- test.after(async () => {
428
- await runtime.close()
429
- await target.close()
430
- rmSync(certDir, {recursive: true, force: true})
431
- })
392
+ it("startRelayRuntime allows non-loopback bind with TLS configured", async () => withTestCleanup(async (after) => {
393
+ const target = await startTargetServer()
394
+ const certDir = mkdtempSync(join(tmpdir(), "socketduct-tls-loopback-"))
395
+ const certs = generateTestCertificates(certDir)
396
+
397
+ const runtime = await startRelayRuntime({
398
+ mode: "relay",
399
+ relayHost: "0.0.0.0",
400
+ relayPort: 0,
401
+ resumeWindowMs: 30_000,
402
+ maxTargetBufferBytes: 1_048_576,
403
+ maxTotalBufferBytes: 16_777_216,
404
+ handshakeTimeoutMs: 10_000,
405
+ maxInputBytesPerConnection: 65_536,
406
+ authTokens: [{username: "edge", token: "secret-token"}],
407
+ targetAllowlist: [{host: target.host, port: target.port}],
408
+ relayTls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile},
409
+ allowInsecurePublic: false
410
+ })
411
+
412
+ after(async () => {
413
+ await runtime.close()
414
+ await target.close()
415
+ rmSync(certDir, {recursive: true, force: true})
416
+ })
432
417
 
433
- assert.ok(runtime.relay.port > 0)
418
+ expect(runtime.relay.port > 0).toBeTruthy()
419
+ }))
434
420
  })