socketduct 0.1.24 → 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 (60) 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/client/multiplexed-transport-set.js +20 -2
  7. package/src/logging/compressed-log-writer.js +1 -1
  8. package/src/logging/zstd-frame-codec.js +15 -1
  9. package/src/reverse/client.js +44 -1
  10. package/src/reverse/hub.js +10 -1
  11. package/src/reverse/runtime.js +11 -2
  12. package/test/e2e/gateway-process.test.js +93 -92
  13. package/test/e2e/logger-shutdown-process.test.js +40 -37
  14. package/test/e2e/reverse-client-recovery-process.test.js +107 -103
  15. package/test/e2e/reverse-gateway-process.test.js +328 -318
  16. package/test/integration/initial-target-recovery.test.js +333 -334
  17. package/test/integration/logging-cli.test.js +121 -119
  18. package/test/integration/logging-compressed-recovery.test.js +96 -95
  19. package/test/integration/multiplexed-compatibility.test.js +178 -177
  20. package/test/integration/multiplexed-corrections.test.js +360 -359
  21. package/test/integration/multiplexed-failures.test.js +253 -251
  22. package/test/integration/multiplexed-transport-set.test.js +334 -322
  23. package/test/integration/multiplexed-transport.test.js +135 -134
  24. package/test/integration/reverse-gateway-stream-id-reset.test.js +90 -89
  25. package/test/integration/reverse-gateway.test.js +2309 -2218
  26. package/test/integration/reverse-resume-continuity.test.js +219 -218
  27. package/test/integration/reverse-route-class.test.js +363 -278
  28. package/test/integration/socketduct-http-agent.test.js +314 -312
  29. package/test/integration/socketduct-https-agent.test.js +516 -485
  30. package/test/integration/stream-compression.test.js +862 -824
  31. package/test/integration/tcp-forwarding.test.js +2550 -2447
  32. package/test/integration/tls-forwarding.test.js +593 -592
  33. package/test/unit/auth/auth.test.js +43 -44
  34. package/test/unit/auth/static-token-store.test.js +9 -8
  35. package/test/unit/client/connect.test.js +41 -40
  36. package/test/unit/client/deferred-virtual-socket.test.js +95 -94
  37. package/test/unit/client/multiplexed-spool-budget.test.js +155 -154
  38. package/test/unit/client/multiplexed-transport-set.test.js +845 -732
  39. package/test/unit/client/socketduct-socket.test.js +768 -771
  40. package/test/unit/client/virtual-socket.test.js +198 -168
  41. package/test/unit/config/docker-gateway-files.test.js +224 -224
  42. package/test/unit/config/environment.test.js +472 -492
  43. package/test/unit/index.test.js +53 -52
  44. package/test/unit/logging/compressed-log-worker.test.js +23 -22
  45. package/test/unit/logging/compressed-log-writer.test.js +155 -154
  46. package/test/unit/logging/log-retention.test.js +52 -47
  47. package/test/unit/logging/log-segment-index.test.js +84 -81
  48. package/test/unit/logging/zstd-frame-codec.test.js +28 -19
  49. package/test/unit/protocol/compression.test.js +254 -298
  50. package/test/unit/protocol/frame.test.js +280 -276
  51. package/test/unit/protocol/v3-compression.test.js +21 -23
  52. package/test/unit/protocol/v3-frame.test.js +42 -41
  53. package/test/unit/release-configuration.test.js +15 -12
  54. package/test/unit/reverse/disconnect-attribution.test.js +65 -64
  55. package/test/unit/reverse-terminal-outcomes.test.js +53 -50
  56. package/test/unit/server/control-handler.test.js +232 -231
  57. package/test/unit/server/primary.test.js +51 -49
  58. package/test/unit/server/relay-runtime.test.js +280 -294
  59. package/test/unit/sessions/session.test.js +48 -62
  60. package/test/unit/spool/spool-file.test.js +401 -385
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable jsdoc/escape-inline-tags, jsdoc/require-returns */
2
- import assert from "node:assert/strict"
2
+ import {describe, expect, it, withTestCleanup} from "@velocious/testing"
3
3
  import {readFileSync} from "node:fs"
4
4
  import {mkdtemp, rm} from "node:fs/promises"
5
5
  import http from "node:http"
@@ -7,7 +7,6 @@ import {createServer} from "node:net"
7
7
  import {tmpdir} from "node:os"
8
8
  import {join} from "node:path"
9
9
  import {connect as tlsConnect, createServer as createTlsServer} from "node:tls"
10
- import test from "node:test"
11
10
 
12
11
  import {StaticTokenStore} from "../../src/auth/static-token-store.js"
13
12
  import {MultiplexedSocketductConnection} from "../../src/client/multiplexed-connection.js"
@@ -33,7 +32,7 @@ async function listen(server) {
33
32
  })
34
33
  })
35
34
  const address = server.address()
36
- assert(address !== null && typeof address !== "string")
35
+ if (address === null || typeof address === "string") throw new Error("Expected a bound TCP address")
37
36
  return address.port
38
37
  }
39
38
 
@@ -75,180 +74,182 @@ async function fixture(targetPort) {
75
74
  return {relay, connection}
76
75
  }
77
76
 
78
- test("stock Node HTTP uses only createConnection, reuses keep-alive, and handles close-delimited responses", async (t) => {
79
- let targetAccepts = 0
80
- const target = http.createServer((req, res) => {
81
- if (req.url === "/close") {
82
- res.shouldKeepAlive = false
83
- res.writeHead(200, {connection: "close"})
84
- res.end("close-delimited")
85
- return
86
- }
87
- res.end(`ok:${req.url}`)
88
- })
89
- target.on("connection", () => { targetAccepts += 1 })
90
- const targetPort = await listen(target)
91
- t.after(() => closeServer(target))
92
- const {relay, connection} = await fixture(targetPort)
93
- t.after(() => relay.close())
94
- t.after(() => connection.close())
95
- const agent = new http.Agent({keepAlive: true, maxSockets: 1})
96
- agent.createConnection = connection.createConnection
97
- t.after(() => agent.destroy())
98
-
99
- assert.deepEqual(await request({agent, host: "127.0.0.1", port: targetPort, path: "/one"}), {statusCode: 200, body: "ok:/one"})
100
- assert.deepEqual(await request({agent, host: "127.0.0.1", port: targetPort, path: "/two"}), {statusCode: 200, body: "ok:/two"})
101
- assert.equal(targetAccepts, 1)
102
- assert.deepEqual(await request({agent, host: "127.0.0.1", port: targetPort, path: "/close"}), {statusCode: 200, body: "close-delimited"})
103
- assert.equal(relay.status().physicalAccepts, 1)
104
- })
105
-
106
- test("request abort resets one HTTP stream without closing a sibling", async (t) => {
107
- const target = http.createServer((req, res) => {
108
- if (req.url === "/slow") {
109
- setTimeout(() => res.end("too-late"), 500).unref()
110
- return
111
- }
112
- res.end("sibling-ok")
113
- })
114
- const targetPort = await listen(target)
115
- t.after(() => closeServer(target))
116
- const {relay, connection} = await fixture(targetPort)
117
- t.after(() => relay.close())
118
- t.after(() => connection.close())
119
- const agent = new http.Agent({keepAlive: false, maxSockets: 2})
120
- agent.createConnection = connection.createConnection
121
- t.after(() => agent.destroy())
122
-
123
- const aborted = new Promise((resolve) => {
124
- const req = http.request({agent, host: "127.0.0.1", port: targetPort, path: "/slow"})
125
- req.on("error", (error) => resolve(error))
126
- req.end()
127
- req.once("socket", () => req.destroy(new Error("cancelled")))
128
- })
129
- const sibling = await request({agent, host: "127.0.0.1", port: targetPort, path: "/fast"})
130
- assert.match((await aborted).message, /cancelled/)
131
- assert.equal(sibling.body, "sibling-ok")
132
- assert.equal(relay.status().physicalConnections, 1)
133
- })
134
-
135
- test("HTTP upgrade/hijack and long-lived streaming work over virtual sockets", async (t) => {
136
- const target = http.createServer((req, res) => {
137
- res.writeHead(200)
138
- res.write("part-1|")
139
- setTimeout(() => res.end("part-2"), 20).unref()
140
- })
141
- target.on("upgrade", (_req, socket) => {
142
- socket.write("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: test\r\n\r\nhello")
143
- socket.on("data", (chunk) => socket.write(chunk))
144
- })
145
- const targetPort = await listen(target)
146
- t.after(() => closeServer(target))
147
- const {relay, connection} = await fixture(targetPort)
148
- t.after(() => relay.close())
149
- t.after(() => connection.close())
150
- const agent = new http.Agent({keepAlive: false})
151
- agent.createConnection = connection.createConnection
152
- t.after(() => agent.destroy())
153
-
154
- assert.equal((await request({agent, host: "127.0.0.1", port: targetPort, path: "/stream"})).body, "part-1|part-2")
155
- const upgraded = await new Promise((resolve, reject) => {
156
- const timer = setTimeout(() => reject(new Error("timed out waiting for HTTP upgrade echo")), 2_000)
157
- const req = http.request({agent, host: "127.0.0.1", port: targetPort, headers: {connection: "Upgrade", upgrade: "test"}})
158
- req.on("upgrade", (_res, socket, head) => {
159
- let value = head.toString()
160
- const finish = () => {
161
- if (!value.includes("hello") || !value.includes("ping")) return
162
- socket.destroy()
163
- clearTimeout(timer)
164
- resolve(value)
77
+ describe("multiplexed compatibility", () => {
78
+ it("stock Node HTTP uses only createConnection, reuses keep-alive, and handles close-delimited responses", async () => withTestCleanup(async (after) => {
79
+ let targetAccepts = 0
80
+ const target = http.createServer((req, res) => {
81
+ if (req.url === "/close") {
82
+ res.shouldKeepAlive = false
83
+ res.writeHead(200, {connection: "close"})
84
+ res.end("close-delimited")
85
+ return
165
86
  }
166
- socket.on("data", (chunk) => { value += chunk.toString(); finish() })
167
- socket.write("ping")
168
- finish()
87
+ res.end(`ok:${req.url}`)
169
88
  })
170
- req.on("error", (error) => { clearTimeout(timer); reject(error) })
171
- req.end()
172
- })
173
- assert.match(upgraded, /hello/)
174
- assert.match(upgraded, /ping/)
175
- })
176
-
177
- test("node:tls can wrap a VirtualSocket for transitional inner TLS", async (t) => {
178
- const root = await mkdtemp(join(tmpdir(), "socketduct-v3-inner-tls-"))
179
- t.after(() => rm(root, {recursive: true, force: true}))
180
- const certs = generateTestCertificates(join(root, "certs"))
181
- const target = createTlsServer({
182
- cert: readFileSync(certs.serverCertFile),
183
- key: readFileSync(certs.serverKeyFile)
184
- }, (socket) => socket.on("data", (chunk) => socket.write(chunk)))
185
- const targetPort = await listen(target)
186
- t.after(() => closeServer(target))
187
- const {relay, connection} = await fixture(targetPort)
188
- t.after(() => relay.close())
189
- t.after(() => connection.close())
190
-
191
- const raw = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "inner-tls"})
192
- const secure = tlsConnect({socket: raw, ca: readFileSync(certs.caCertFile), servername: "localhost"})
193
- await new Promise((resolve, reject) => {
194
- secure.once("secureConnect", resolve)
195
- secure.once("error", reject)
196
- })
197
- const echoed = new Promise((resolve, reject) => {
198
- secure.once("data", (chunk) => resolve(chunk.toString()))
199
- secure.once("error", reject)
200
- })
201
- secure.write("secure-echo")
202
- assert.equal(await echoed, "secure-echo")
203
- secure.destroy()
204
- })
205
-
206
- test("v3 authenticates once over a verified outer TLS transport", async (t) => {
207
- const root = await mkdtemp(join(tmpdir(), "socketduct-v3-outer-tls-"))
208
- t.after(() => rm(root, {recursive: true, force: true}))
209
- const certs = generateTestCertificates(join(root, "certs"))
210
- const target = createServer((socket) => socket.on("data", (chunk) => socket.write(chunk)))
211
- const targetPort = await listen(target)
212
- t.after(() => closeServer(target))
213
- const relay = await startMultiplexedTcpRelay({
214
- tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
215
- authorizeTarget: ({target: requested}) => requested.port === targetPort,
216
- tls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile}
217
- })
218
- t.after(() => relay.close())
219
- const connection = new MultiplexedSocketductConnection({
220
- relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"},
221
- tls: {caFile: certs.caCertFile, servername: "localhost"}
222
- })
223
- t.after(() => connection.close())
224
- const first = connection.createConnection({host: "127.0.0.1", port: targetPort})
225
- const second = connection.createConnection({host: "127.0.0.1", port: targetPort})
226
- await Promise.all([
227
- new Promise((resolve, reject) => { first.once("connect", resolve); first.once("error", reject) }),
228
- new Promise((resolve, reject) => { second.once("connect", resolve); second.once("error", reject) })
229
- ])
230
- const echoed = new Promise((resolve, reject) => { second.once("data", (chunk) => resolve(chunk.toString())); second.once("error", reject) })
231
- second.write("outer-tls")
232
- assert.equal(await echoed, "outer-tls")
233
- assert.equal(relay.status().physicalAccepts, 1)
234
- assert.equal(relay.status().authenticatedConnections, 1)
235
- first.destroy()
236
- second.destroy()
237
- })
238
-
239
- test("raw TCP pre-connect writes are buffered until stream.opened", async (t) => {
240
- const target = createServer((socket) => socket.on("data", (chunk) => socket.write(chunk)))
241
- const targetPort = await listen(target)
242
- t.after(() => closeServer(target))
243
- const {relay, connection} = await fixture(targetPort)
244
- t.after(() => relay.close())
245
- t.after(() => connection.close())
246
- const socket = connection.createConnection({host: "127.0.0.1", port: targetPort})
247
- const echoed = new Promise((resolve, reject) => {
248
- socket.once("data", (chunk) => resolve(chunk.toString()))
249
- socket.once("error", reject)
250
- })
251
- socket.write("early")
252
- assert.equal(await echoed, "early")
253
- socket.destroy()
89
+ target.on("connection", () => { targetAccepts += 1 })
90
+ const targetPort = await listen(target)
91
+ after(() => closeServer(target))
92
+ const {relay, connection} = await fixture(targetPort)
93
+ after(() => relay.close())
94
+ after(() => connection.close())
95
+ const agent = new http.Agent({keepAlive: true, maxSockets: 1})
96
+ agent.createConnection = connection.createConnection
97
+ after(() => agent.destroy())
98
+
99
+ expect(await request({agent, host: "127.0.0.1", port: targetPort, path: "/one"})).toEqual({statusCode: 200, body: "ok:/one"})
100
+ expect(await request({agent, host: "127.0.0.1", port: targetPort, path: "/two"})).toEqual({statusCode: 200, body: "ok:/two"})
101
+ expect(targetAccepts).toBe(1)
102
+ expect(await request({agent, host: "127.0.0.1", port: targetPort, path: "/close"})).toEqual({statusCode: 200, body: "close-delimited"})
103
+ expect(relay.status().physicalAccepts).toBe(1)
104
+ }))
105
+
106
+ it("request abort resets one HTTP stream without closing a sibling", async () => withTestCleanup(async (after) => {
107
+ const target = http.createServer((req, res) => {
108
+ if (req.url === "/slow") {
109
+ setTimeout(() => res.end("too-late"), 500).unref()
110
+ return
111
+ }
112
+ res.end("sibling-ok")
113
+ })
114
+ const targetPort = await listen(target)
115
+ after(() => closeServer(target))
116
+ const {relay, connection} = await fixture(targetPort)
117
+ after(() => relay.close())
118
+ after(() => connection.close())
119
+ const agent = new http.Agent({keepAlive: false, maxSockets: 2})
120
+ agent.createConnection = connection.createConnection
121
+ after(() => agent.destroy())
122
+
123
+ const aborted = new Promise((resolve) => {
124
+ const req = http.request({agent, host: "127.0.0.1", port: targetPort, path: "/slow"})
125
+ req.on("error", (error) => resolve(error))
126
+ req.end()
127
+ req.once("socket", () => req.destroy(new Error("cancelled")))
128
+ })
129
+ const sibling = await request({agent, host: "127.0.0.1", port: targetPort, path: "/fast"})
130
+ expect((await aborted).message).toMatch(/cancelled/)
131
+ expect(sibling.body).toBe("sibling-ok")
132
+ expect(relay.status().physicalConnections).toBe(1)
133
+ }))
134
+
135
+ it("HTTP upgrade/hijack and long-lived streaming work over virtual sockets", async () => withTestCleanup(async (after) => {
136
+ const target = http.createServer((req, res) => {
137
+ res.writeHead(200)
138
+ res.write("part-1|")
139
+ setTimeout(() => res.end("part-2"), 20).unref()
140
+ })
141
+ target.on("upgrade", (_req, socket) => {
142
+ socket.write("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: test\r\n\r\nhello")
143
+ socket.on("data", (chunk) => socket.write(chunk))
144
+ })
145
+ const targetPort = await listen(target)
146
+ after(() => closeServer(target))
147
+ const {relay, connection} = await fixture(targetPort)
148
+ after(() => relay.close())
149
+ after(() => connection.close())
150
+ const agent = new http.Agent({keepAlive: false})
151
+ agent.createConnection = connection.createConnection
152
+ after(() => agent.destroy())
153
+
154
+ expect((await request({agent, host: "127.0.0.1", port: targetPort, path: "/stream"})).body).toBe("part-1|part-2")
155
+ const upgraded = await new Promise((resolve, reject) => {
156
+ const timer = setTimeout(() => reject(new Error("timed out waiting for HTTP upgrade echo")), 2_000)
157
+ const req = http.request({agent, host: "127.0.0.1", port: targetPort, headers: {connection: "Upgrade", upgrade: "test"}})
158
+ req.on("upgrade", (_res, socket, head) => {
159
+ let value = head.toString()
160
+ const finish = () => {
161
+ if (!value.includes("hello") || !value.includes("ping")) return
162
+ socket.destroy()
163
+ clearTimeout(timer)
164
+ resolve(value)
165
+ }
166
+ socket.on("data", (chunk) => { value += chunk.toString(); finish() })
167
+ socket.write("ping")
168
+ finish()
169
+ })
170
+ req.on("error", (error) => { clearTimeout(timer); reject(error) })
171
+ req.end()
172
+ })
173
+ expect(upgraded).toMatch(/hello/)
174
+ expect(upgraded).toMatch(/ping/)
175
+ }))
176
+
177
+ it("node:tls can wrap a VirtualSocket for transitional inner TLS", async () => withTestCleanup(async (after) => {
178
+ const root = await mkdtemp(join(tmpdir(), "socketduct-v3-inner-tls-"))
179
+ after(() => rm(root, {recursive: true, force: true}))
180
+ const certs = generateTestCertificates(join(root, "certs"))
181
+ const target = createTlsServer({
182
+ cert: readFileSync(certs.serverCertFile),
183
+ key: readFileSync(certs.serverKeyFile)
184
+ }, (socket) => socket.on("data", (chunk) => socket.write(chunk)))
185
+ const targetPort = await listen(target)
186
+ after(() => closeServer(target))
187
+ const {relay, connection} = await fixture(targetPort)
188
+ after(() => relay.close())
189
+ after(() => connection.close())
190
+
191
+ const raw = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "inner-tls"})
192
+ const secure = tlsConnect({socket: raw, ca: readFileSync(certs.caCertFile), servername: "localhost"})
193
+ await new Promise((resolve, reject) => {
194
+ secure.once("secureConnect", resolve)
195
+ secure.once("error", reject)
196
+ })
197
+ const echoed = new Promise((resolve, reject) => {
198
+ secure.once("data", (chunk) => resolve(chunk.toString()))
199
+ secure.once("error", reject)
200
+ })
201
+ secure.write("secure-echo")
202
+ expect(await echoed).toBe("secure-echo")
203
+ secure.destroy()
204
+ }))
205
+
206
+ it("v3 authenticates once over a verified outer TLS transport", async () => withTestCleanup(async (after) => {
207
+ const root = await mkdtemp(join(tmpdir(), "socketduct-v3-outer-tls-"))
208
+ after(() => rm(root, {recursive: true, force: true}))
209
+ const certs = generateTestCertificates(join(root, "certs"))
210
+ const target = createServer((socket) => socket.on("data", (chunk) => socket.write(chunk)))
211
+ const targetPort = await listen(target)
212
+ after(() => closeServer(target))
213
+ const relay = await startMultiplexedTcpRelay({
214
+ tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
215
+ authorizeTarget: ({target: requested}) => requested.port === targetPort,
216
+ tls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile}
217
+ })
218
+ after(() => relay.close())
219
+ const connection = new MultiplexedSocketductConnection({
220
+ relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"},
221
+ tls: {caFile: certs.caCertFile, servername: "localhost"}
222
+ })
223
+ after(() => connection.close())
224
+ const first = connection.createConnection({host: "127.0.0.1", port: targetPort})
225
+ const second = connection.createConnection({host: "127.0.0.1", port: targetPort})
226
+ await Promise.all([
227
+ new Promise((resolve, reject) => { first.once("connect", resolve); first.once("error", reject) }),
228
+ new Promise((resolve, reject) => { second.once("connect", resolve); second.once("error", reject) })
229
+ ])
230
+ const echoed = new Promise((resolve, reject) => { second.once("data", (chunk) => resolve(chunk.toString())); second.once("error", reject) })
231
+ second.write("outer-tls")
232
+ expect(await echoed).toBe("outer-tls")
233
+ expect(relay.status().physicalAccepts).toBe(1)
234
+ expect(relay.status().authenticatedConnections).toBe(1)
235
+ first.destroy()
236
+ second.destroy()
237
+ }))
238
+
239
+ it("raw TCP pre-connect writes are buffered until stream.opened", async () => withTestCleanup(async (after) => {
240
+ const target = createServer((socket) => socket.on("data", (chunk) => socket.write(chunk)))
241
+ const targetPort = await listen(target)
242
+ after(() => closeServer(target))
243
+ const {relay, connection} = await fixture(targetPort)
244
+ after(() => relay.close())
245
+ after(() => connection.close())
246
+ const socket = connection.createConnection({host: "127.0.0.1", port: targetPort})
247
+ const echoed = new Promise((resolve, reject) => {
248
+ socket.once("data", (chunk) => resolve(chunk.toString()))
249
+ socket.once("error", reject)
250
+ })
251
+ socket.write("early")
252
+ expect(await echoed).toBe("early")
253
+ socket.destroy()
254
+ }))
254
255
  })