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.
- package/AGENTS.md +2 -2
- package/changelog.d/20261010130509-bound-log-frame-compression-memory.md +1 -0
- package/docs/runbooks/jsonl-integrity-rollout.md +8 -6
- package/package.json +2 -2
- package/scripts/test.js +9 -14
- package/src/client/multiplexed-transport-set.js +20 -2
- package/src/logging/compressed-log-writer.js +1 -1
- package/src/logging/zstd-frame-codec.js +15 -1
- package/src/reverse/client.js +44 -1
- package/src/reverse/hub.js +10 -1
- package/src/reverse/runtime.js +11 -2
- package/test/e2e/gateway-process.test.js +93 -92
- package/test/e2e/logger-shutdown-process.test.js +40 -37
- package/test/e2e/reverse-client-recovery-process.test.js +107 -103
- package/test/e2e/reverse-gateway-process.test.js +328 -318
- package/test/integration/initial-target-recovery.test.js +333 -334
- package/test/integration/logging-cli.test.js +121 -119
- package/test/integration/logging-compressed-recovery.test.js +96 -95
- package/test/integration/multiplexed-compatibility.test.js +178 -177
- package/test/integration/multiplexed-corrections.test.js +360 -359
- package/test/integration/multiplexed-failures.test.js +253 -251
- package/test/integration/multiplexed-transport-set.test.js +334 -322
- package/test/integration/multiplexed-transport.test.js +135 -134
- package/test/integration/reverse-gateway-stream-id-reset.test.js +90 -89
- package/test/integration/reverse-gateway.test.js +2309 -2218
- package/test/integration/reverse-resume-continuity.test.js +219 -218
- package/test/integration/reverse-route-class.test.js +363 -278
- package/test/integration/socketduct-http-agent.test.js +314 -312
- package/test/integration/socketduct-https-agent.test.js +516 -485
- package/test/integration/stream-compression.test.js +862 -824
- package/test/integration/tcp-forwarding.test.js +2550 -2447
- package/test/integration/tls-forwarding.test.js +593 -592
- package/test/unit/auth/auth.test.js +43 -44
- package/test/unit/auth/static-token-store.test.js +9 -8
- package/test/unit/client/connect.test.js +41 -40
- package/test/unit/client/deferred-virtual-socket.test.js +95 -94
- package/test/unit/client/multiplexed-spool-budget.test.js +155 -154
- package/test/unit/client/multiplexed-transport-set.test.js +845 -732
- package/test/unit/client/socketduct-socket.test.js +768 -771
- package/test/unit/client/virtual-socket.test.js +198 -168
- package/test/unit/config/docker-gateway-files.test.js +224 -224
- package/test/unit/config/environment.test.js +472 -492
- package/test/unit/index.test.js +53 -52
- package/test/unit/logging/compressed-log-worker.test.js +23 -22
- package/test/unit/logging/compressed-log-writer.test.js +155 -154
- package/test/unit/logging/log-retention.test.js +52 -47
- package/test/unit/logging/log-segment-index.test.js +84 -81
- package/test/unit/logging/zstd-frame-codec.test.js +28 -19
- package/test/unit/protocol/compression.test.js +254 -298
- package/test/unit/protocol/frame.test.js +280 -276
- package/test/unit/protocol/v3-compression.test.js +21 -23
- package/test/unit/protocol/v3-frame.test.js +42 -41
- package/test/unit/release-configuration.test.js +15 -12
- package/test/unit/reverse/disconnect-attribution.test.js +65 -64
- package/test/unit/reverse-terminal-outcomes.test.js +53 -50
- package/test/unit/server/control-handler.test.js +232 -231
- package/test/unit/server/primary.test.js +51 -49
- package/test/unit/server/relay-runtime.test.js +280 -294
- package/test/unit/sessions/session.test.js +48 -62
- package/test/unit/spool/spool-file.test.js +401 -385
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/* eslint-disable jsdoc/escape-inline-tags, jsdoc/require-param, jsdoc/require-returns */
|
|
2
|
-
import
|
|
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 {createConnection, createServer} from "node:net"
|
|
6
6
|
import {tmpdir} from "node:os"
|
|
7
7
|
import {join} from "node:path"
|
|
8
|
-
import test from "node:test"
|
|
9
8
|
import {createServer as createTlsServer} from "node:tls"
|
|
10
9
|
|
|
11
10
|
import {StaticTokenStore} from "../../src/auth/static-token-store.js"
|
|
@@ -22,7 +21,7 @@ async function listen(server) {
|
|
|
22
21
|
server.listen(0, "127.0.0.1", () => { server.off("error", reject); resolve(undefined) })
|
|
23
22
|
})
|
|
24
23
|
const address = server.address()
|
|
25
|
-
|
|
24
|
+
if (address === null || typeof address === "string") throw new Error("Expected a bound TCP address")
|
|
26
25
|
return address.port
|
|
27
26
|
}
|
|
28
27
|
|
|
@@ -59,7 +58,7 @@ async function waitForStatus(set, predicate) {
|
|
|
59
58
|
/** @template T @param {T[]} values - Values. @param {number} index - Index. @returns {T} */
|
|
60
59
|
function at(values, index) {
|
|
61
60
|
const value = values[index]
|
|
62
|
-
|
|
61
|
+
if (value === undefined) throw new Error(`Expected a value at index ${index}`)
|
|
63
62
|
return value
|
|
64
63
|
}
|
|
65
64
|
|
|
@@ -115,338 +114,351 @@ async function createFirstDataDropProxy(relayPort) {
|
|
|
115
114
|
}
|
|
116
115
|
}
|
|
117
116
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
117
|
+
describe("multiplexed transport set", () => {
|
|
118
|
+
it("three pinned gateways warm once and balance virtual sockets without extra physical accepts", async () => withTestCleanup(async (after) => {
|
|
119
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-set-integration-"))
|
|
120
|
+
const target = createServer((socket) => socket.on("data", (chunk) => socket.write(chunk)))
|
|
121
|
+
const targetPort = await listen(target)
|
|
122
|
+
after(() => new Promise((resolve) => target.close(() => resolve(undefined))))
|
|
123
|
+
const relays = await Promise.all(["starlink", "zyxel", "o2gateway"].map(() => startMultiplexedTcpRelay({
|
|
124
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
125
|
+
authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort,
|
|
126
|
+
maxStreams: 256
|
|
127
|
+
})))
|
|
128
|
+
const gatewayIds = ["starlink", "zyxel", "o2gateway"]
|
|
129
|
+
after(async () => {
|
|
130
|
+
await Promise.all(relays.map((relay) => relay.close()))
|
|
131
|
+
})
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
133
|
+
const set = new MultiplexedSocketductTransportSet({
|
|
134
|
+
connectionId: "integration_process_01",
|
|
135
|
+
spoolDirectory: root,
|
|
136
|
+
minimumReadyMembers: 3,
|
|
137
|
+
maxTotalBufferBytes: 32 * 1024 * 1024,
|
|
138
|
+
maxTotalSpoolBytes: 32 * 1024 * 1024,
|
|
139
|
+
maxTotalQueuedBytes: 33 * 1024 * 1024,
|
|
140
|
+
memberOptions: {maxStreams: 256, reconnectDelayMs: 10},
|
|
141
|
+
gateways: relays.map((relay, index) => ({
|
|
142
|
+
gatewayId: at(gatewayIds, index),
|
|
143
|
+
endpoint: {relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"}}
|
|
144
|
+
}))
|
|
145
|
+
})
|
|
146
|
+
after(async () => {
|
|
147
|
+
await set.close()
|
|
148
|
+
await rm(root, {recursive: true, force: true})
|
|
149
|
+
})
|
|
150
|
+
const started = set.start()
|
|
151
|
+
const firstSocket = set.createConnection({
|
|
152
|
+
host: "127.0.0.1",
|
|
153
|
+
port: targetPort,
|
|
154
|
+
sessionName: "set-flow-1"
|
|
155
|
+
})
|
|
156
|
+
const firstConnected = onceEvent(firstSocket, "connect")
|
|
157
|
+
expect(firstSocket.gatewayId).toBeUndefined()
|
|
158
|
+
await Promise.all([started, firstConnected])
|
|
159
|
+
expect(relays.map((relay) => relay.status().physicalAccepts)).toEqual([1, 1, 1])
|
|
160
|
+
expect(relays.map((relay) => relay.status().virtualStreams)).toEqual([1, 0, 0])
|
|
159
161
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
})
|
|
162
|
+
const sockets = [firstSocket, ...Array.from({length: 99}, (_, index) => set.createConnection({
|
|
163
|
+
host: "127.0.0.1",
|
|
164
|
+
port: targetPort,
|
|
165
|
+
sessionName: `set-flow-${index + 2}`
|
|
166
|
+
}))]
|
|
167
|
+
await Promise.all(sockets.slice(1).map((socket) => onceEvent(socket, "connect")))
|
|
168
|
+
expect(set.status().members.map((member) => member.assignments)).toEqual([34, 33, 33])
|
|
169
|
+
expect(relays.map((relay) => relay.status().physicalAccepts)).toEqual([1, 1, 1])
|
|
170
|
+
expect(relays.map((relay) => relay.status().virtualStreams)).toEqual([34, 33, 33])
|
|
171
|
+
sockets.forEach((socket) => socket.destroy())
|
|
172
|
+
}))
|
|
171
173
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
174
|
+
it("a failed gateway keeps its streams pinned while healthy siblings take new opens and recovery rejoins", async () => withTestCleanup(async (after) => {
|
|
175
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-set-recovery-"))
|
|
176
|
+
const target = createServer((socket) => socket.on("data", (chunk) => socket.write(chunk)))
|
|
177
|
+
const targetPort = await listen(target)
|
|
178
|
+
after(() => new Promise((resolve) => target.close(() => resolve(undefined))))
|
|
179
|
+
const relays = await Promise.all([0, 1, 2].map(() => startMultiplexedTcpRelay({
|
|
180
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
181
|
+
authorizeTarget: ({target: requested}) => requested.port === targetPort
|
|
182
|
+
})))
|
|
183
|
+
const gatewayIds = ["starlink", "zyxel", "o2gateway"]
|
|
184
|
+
after(async () => {
|
|
185
|
+
await Promise.all(relays.map((relay) => relay.close()))
|
|
186
|
+
})
|
|
187
|
+
const set = new MultiplexedSocketductTransportSet({
|
|
188
|
+
connectionId: "integration_process_recovery_01",
|
|
189
|
+
spoolDirectory: root,
|
|
190
|
+
maxTotalBufferBytes: 32 * 1024 * 1024,
|
|
191
|
+
maxTotalSpoolBytes: 32 * 1024 * 1024,
|
|
192
|
+
maxTotalQueuedBytes: 33 * 1024 * 1024,
|
|
193
|
+
memberOptions: {reconnectDelayMs: 100},
|
|
194
|
+
gateways: relays.map((relay, index) => ({
|
|
195
|
+
gatewayId: at(gatewayIds, index),
|
|
196
|
+
endpoint: {relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"}}
|
|
197
|
+
}))
|
|
198
|
+
})
|
|
199
|
+
after(async () => {
|
|
200
|
+
await set.close()
|
|
201
|
+
await rm(root, {recursive: true, force: true})
|
|
202
|
+
})
|
|
203
|
+
await set.start()
|
|
204
|
+
await waitForStatus(set, (status) => status.readyMembers === 3)
|
|
205
|
+
const sticky = Array.from({length: 3}, () => set.createConnection({host: "127.0.0.1", port: targetPort}))
|
|
206
|
+
await Promise.all(sticky.map((socket) => onceEvent(socket, "connect")))
|
|
207
|
+
const recovered = onceEvent(at(sticky, 1), "ready")
|
|
208
|
+
await at(relays, 1).closeActiveTransport()
|
|
209
|
+
await waitForStatus(set, (status) => at(status.members, 1).state === "reconnecting")
|
|
206
210
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
})
|
|
211
|
+
const siblingOpens = Array.from({length: 10}, () => set.createConnection({host: "127.0.0.1", port: targetPort}))
|
|
212
|
+
expect(at(set.status().members, 1).assignments).toBe(1)
|
|
213
|
+
const siblingCounts = [at(set.status().members, 0).assignments, at(set.status().members, 2).assignments]
|
|
214
|
+
expect(Math.max(...siblingCounts) - Math.min(...siblingCounts) <= 1).toBeTruthy()
|
|
215
|
+
await Promise.all(siblingOpens.map((socket) => onceEvent(socket, "connect")))
|
|
216
|
+
await recovered
|
|
217
|
+
await waitForStatus(set, (status) => status.readyMembers === 3)
|
|
218
|
+
const before = at(set.status().members, 1).assignments
|
|
219
|
+
const rejoined = Array.from({length: 3}, () => set.createConnection({host: "127.0.0.1", port: targetPort}))
|
|
220
|
+
expect(at(set.status().members, 1).assignments > before).toBeTruthy()
|
|
221
|
+
expect(at(sticky, 1).destroyed).toBe(false)
|
|
222
|
+
expect(at(relays, 1).status().physicalAccepts).toBe(2)
|
|
223
|
+
sticky.forEach((socket) => socket.destroy())
|
|
224
|
+
siblingOpens.forEach((socket) => socket.destroy())
|
|
225
|
+
rejoined.forEach((socket) => socket.destroy())
|
|
226
|
+
}))
|
|
223
227
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
228
|
+
it("all reconnecting members hold a new socket until one real member recovers", async () => withTestCleanup(async (after) => {
|
|
229
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-set-all-reconnect-"))
|
|
230
|
+
const target = createServer((socket) => socket.on("data", (chunk) => socket.write(chunk)))
|
|
231
|
+
const targetPort = await listen(target)
|
|
232
|
+
const relays = await Promise.all([0, 1, 2].map(() => startMultiplexedTcpRelay({
|
|
233
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
234
|
+
authorizeTarget: ({target: requested}) => requested.port === targetPort
|
|
235
|
+
})))
|
|
236
|
+
const gatewayIds = ["starlink", "zyxel", "o2gateway"]
|
|
237
|
+
const set = new MultiplexedSocketductTransportSet({
|
|
238
|
+
connectionId: "integration_all_reconnect_01",
|
|
239
|
+
spoolDirectory: root,
|
|
240
|
+
minimumReadyMembers: 3,
|
|
241
|
+
memberOptions: {reconnectDelayMs: 1_000},
|
|
242
|
+
gateways: relays.map((relay, index) => ({
|
|
243
|
+
gatewayId: at(gatewayIds, index),
|
|
244
|
+
endpoint: {relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"}}
|
|
245
|
+
}))
|
|
246
|
+
})
|
|
247
|
+
let siblingRelaysClosed = false
|
|
248
|
+
after(async () => {
|
|
249
|
+
await set.close()
|
|
250
|
+
if (!siblingRelaysClosed) await Promise.all(relays.slice(1).map((relay) => relay.close()))
|
|
251
|
+
await at(relays, 0).close()
|
|
252
|
+
await new Promise((resolve) => target.close(() => resolve(undefined)))
|
|
253
|
+
await rm(root, {recursive: true, force: true})
|
|
254
|
+
})
|
|
255
|
+
await set.start()
|
|
256
|
+
await Promise.all(relays.map((relay) => relay.closeActiveTransport()))
|
|
257
|
+
await waitForStatus(set, (status) => status.readyMembers === 0)
|
|
258
|
+
await Promise.all(relays.slice(1).map((relay) => relay.close()))
|
|
259
|
+
siblingRelaysClosed = true
|
|
256
260
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
})
|
|
261
|
+
const socket = set.createConnection({host: "127.0.0.1", port: targetPort})
|
|
262
|
+
expect(socket.gatewayId).toBeUndefined()
|
|
263
|
+
expect(set.status().pendingOpens).toBe(1)
|
|
264
|
+
await onceEvent(socket, "connect")
|
|
265
|
+
expect(socket.gatewayId).toBe("starlink")
|
|
266
|
+
expect(set.status().pendingOpens).toBe(0)
|
|
267
|
+
expect(set.status().members.map((member) => member.assignments)).toEqual([1, 0, 0])
|
|
268
|
+
socket.destroy()
|
|
269
|
+
}))
|
|
266
270
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
})
|
|
273
|
-
const set = new MultiplexedSocketductTransportSet({
|
|
274
|
-
connectionId: "integration_start_failure_01",
|
|
275
|
-
spoolDirectory: root,
|
|
276
|
-
gateways: [{gatewayId: "starlink", endpoint: {relay: {host: "127.0.0.1", port: relay.port, token: "invalid-token"}}}]
|
|
277
|
-
})
|
|
278
|
-
t.after(async () => {
|
|
279
|
-
await set.close()
|
|
280
|
-
await relay.close()
|
|
281
|
-
await rm(root, {recursive: true, force: true})
|
|
282
|
-
})
|
|
283
|
-
const started = set.start()
|
|
284
|
-
let callbacks = 0
|
|
285
|
-
const callbackResult = new Promise((resolve) => {
|
|
286
|
-
set.createConnection({host: "target.internal", port: 443}, (error, socket) => {
|
|
287
|
-
callbacks += 1
|
|
288
|
-
resolve({error, socket})
|
|
271
|
+
it("terminal authentication failure rejects start and the pending socket once", async () => withTestCleanup(async (after) => {
|
|
272
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-set-start-failure-"))
|
|
273
|
+
const relay = await startMultiplexedTcpRelay({
|
|
274
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
275
|
+
authorizeTarget: () => true
|
|
289
276
|
})
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
test("createTlsConnection preserves callback ownership on the selected sticky member", async (t) => {
|
|
301
|
-
const root = await mkdtemp(join(tmpdir(), "socketduct-set-tls-"))
|
|
302
|
-
const certs = generateTestCertificates(join(root, "certs"))
|
|
303
|
-
const target = createTlsServer({
|
|
304
|
-
cert: readFileSync(certs.serverCertFile),
|
|
305
|
-
key: readFileSync(certs.serverKeyFile)
|
|
306
|
-
}, (socket) => socket.on("data", (chunk) => socket.write(chunk)))
|
|
307
|
-
const targetPort = await listen(target)
|
|
308
|
-
const relay = await startMultiplexedTcpRelay({
|
|
309
|
-
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
310
|
-
authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort
|
|
311
|
-
})
|
|
312
|
-
const set = new MultiplexedSocketductTransportSet({
|
|
313
|
-
connectionId: "integration_process_tls_01",
|
|
314
|
-
spoolDirectory: root,
|
|
315
|
-
gateways: [{gatewayId: "starlink", endpoint: {relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"}}}]
|
|
316
|
-
})
|
|
317
|
-
t.after(async () => {
|
|
318
|
-
await set.close()
|
|
319
|
-
await relay.close()
|
|
320
|
-
await new Promise((resolve) => target.close(() => resolve(undefined)))
|
|
321
|
-
await rm(root, {recursive: true, force: true})
|
|
322
|
-
})
|
|
323
|
-
const started = set.start()
|
|
324
|
-
let callbacks = 0
|
|
325
|
-
const secure = /** @type {import('node:tls').TLSSocket & {readonly gatewayId: string | undefined}} */ (await new Promise((resolve, reject) => {
|
|
326
|
-
const returned = set.createTlsConnection({
|
|
327
|
-
host: "127.0.0.1",
|
|
328
|
-
port: targetPort,
|
|
329
|
-
servername: "localhost",
|
|
330
|
-
ca: readFileSync(certs.caCertFile)
|
|
331
|
-
}, (error, socket) => {
|
|
332
|
-
callbacks += 1
|
|
333
|
-
if (error === null) resolve(socket)
|
|
334
|
-
else reject(error)
|
|
277
|
+
const set = new MultiplexedSocketductTransportSet({
|
|
278
|
+
connectionId: "integration_start_failure_01",
|
|
279
|
+
spoolDirectory: root,
|
|
280
|
+
gateways: [{gatewayId: "starlink", endpoint: {relay: {host: "127.0.0.1", port: relay.port, token: "invalid-token"}}}]
|
|
281
|
+
})
|
|
282
|
+
after(async () => {
|
|
283
|
+
await set.close()
|
|
284
|
+
await relay.close()
|
|
285
|
+
await rm(root, {recursive: true, force: true})
|
|
335
286
|
})
|
|
336
|
-
|
|
337
|
-
|
|
287
|
+
const started = set.start()
|
|
288
|
+
let callbacks = 0
|
|
289
|
+
const callbackResult = new Promise((resolve) => {
|
|
290
|
+
set.createConnection({host: "target.internal", port: 443}, (error, socket) => {
|
|
291
|
+
callbacks += 1
|
|
292
|
+
resolve({error, socket})
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
let error
|
|
296
|
+
try {
|
|
297
|
+
await started
|
|
298
|
+
} catch (caught) {
|
|
299
|
+
error = caught
|
|
300
|
+
}
|
|
301
|
+
expect(error).toBeDefined()
|
|
302
|
+
expect(error).toBeInstanceOf(AggregateError)
|
|
303
|
+
const result = /** @type {{error: Error, socket: import('../../src/client/virtual-socket.js').VirtualSocket}} */ (await callbackResult)
|
|
304
|
+
expect(result.error).toBeInstanceOf(AggregateError)
|
|
305
|
+
expect(callbacks).toBe(1)
|
|
306
|
+
expect(result.socket.destroyed).toBe(true)
|
|
307
|
+
expect(set.status().state).toBe("failed")
|
|
308
|
+
expect(relay.status().virtualStreams).toBe(0)
|
|
338
309
|
}))
|
|
339
|
-
await started
|
|
340
|
-
assert.equal(callbacks, 1)
|
|
341
|
-
assert.equal(secure.gatewayId, "starlink")
|
|
342
|
-
const response = new Promise((resolve, reject) => {
|
|
343
|
-
secure.once("data", (chunk) => resolve(Buffer.from(chunk)))
|
|
344
|
-
secure.once("error", reject)
|
|
345
|
-
})
|
|
346
|
-
secure.write("tls-over-set")
|
|
347
|
-
const chunk = /** @type {Buffer} */ (await response)
|
|
348
|
-
assert.equal(chunk.toString(), "tls-over-set")
|
|
349
|
-
assert.equal(at(set.status().members, 0).assignments, 1)
|
|
350
|
-
secure.destroy()
|
|
351
|
-
})
|
|
352
310
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (received.length === 32) resolveReceived()
|
|
311
|
+
it("createTlsConnection preserves callback ownership on the selected sticky member", async () => withTestCleanup(async (after) => {
|
|
312
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-set-tls-"))
|
|
313
|
+
const certs = generateTestCertificates(join(root, "certs"))
|
|
314
|
+
const target = createTlsServer({
|
|
315
|
+
cert: readFileSync(certs.serverCertFile),
|
|
316
|
+
key: readFileSync(certs.serverKeyFile)
|
|
317
|
+
}, (socket) => socket.on("data", (chunk) => socket.write(chunk)))
|
|
318
|
+
const targetPort = await listen(target)
|
|
319
|
+
const relay = await startMultiplexedTcpRelay({
|
|
320
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
321
|
+
authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort
|
|
365
322
|
})
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
gatewayId
|
|
392
|
-
endpoint: {
|
|
393
|
-
relay: {host: "127.0.0.1", port: index === 0 ? fault.port : relay.port, token: "valid-token"},
|
|
394
|
-
createPhysicalSocket: at(physicalFactories, index)
|
|
395
|
-
}
|
|
323
|
+
const set = new MultiplexedSocketductTransportSet({
|
|
324
|
+
connectionId: "integration_process_tls_01",
|
|
325
|
+
spoolDirectory: root,
|
|
326
|
+
gateways: [{gatewayId: "starlink", endpoint: {relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"}}}]
|
|
327
|
+
})
|
|
328
|
+
after(async () => {
|
|
329
|
+
await set.close()
|
|
330
|
+
await relay.close()
|
|
331
|
+
await new Promise((resolve) => target.close(() => resolve(undefined)))
|
|
332
|
+
await rm(root, {recursive: true, force: true})
|
|
333
|
+
})
|
|
334
|
+
const started = set.start()
|
|
335
|
+
let callbacks = 0
|
|
336
|
+
const secure = /** @type {import('node:tls').TLSSocket & {readonly gatewayId: string | undefined}} */ (await new Promise((resolve, reject) => {
|
|
337
|
+
const returned = set.createTlsConnection({
|
|
338
|
+
host: "127.0.0.1",
|
|
339
|
+
port: targetPort,
|
|
340
|
+
servername: "localhost",
|
|
341
|
+
ca: readFileSync(certs.caCertFile)
|
|
342
|
+
}, (error, socket) => {
|
|
343
|
+
callbacks += 1
|
|
344
|
+
if (error === null) resolve(socket)
|
|
345
|
+
else reject(error)
|
|
346
|
+
})
|
|
347
|
+
if (returned === undefined) throw new Error("Expected createTlsConnection to return a socket")
|
|
348
|
+
expect(returned.gatewayId).toBeUndefined()
|
|
396
349
|
}))
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
350
|
+
await started
|
|
351
|
+
expect(callbacks).toBe(1)
|
|
352
|
+
expect(secure.gatewayId).toBe("starlink")
|
|
353
|
+
const response = new Promise((resolve, reject) => {
|
|
354
|
+
secure.once("data", (chunk) => resolve(Buffer.from(chunk)))
|
|
355
|
+
secure.once("error", reject)
|
|
356
|
+
})
|
|
357
|
+
secure.write("tls-over-set")
|
|
358
|
+
const chunk = /** @type {Buffer} */ (await response)
|
|
359
|
+
expect(chunk.toString()).toBe("tls-over-set")
|
|
360
|
+
expect(at(set.status().members, 0).assignments).toBe(1)
|
|
361
|
+
secure.destroy()
|
|
362
|
+
}))
|
|
407
363
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
364
|
+
it("process replacement recovers a durable stream on its original named gateway before start", async () => withTestCleanup(async (after) => {
|
|
365
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-set-process-recovery-"))
|
|
366
|
+
let targetAccepts = 0
|
|
367
|
+
let received = Buffer.alloc(0)
|
|
368
|
+
/** @type {() => void} */
|
|
369
|
+
let resolveReceived = () => {}
|
|
370
|
+
const receivedAll = new Promise((resolve) => { resolveReceived = () => resolve(undefined) })
|
|
371
|
+
const target = createServer((socket) => {
|
|
372
|
+
targetAccepts += 1
|
|
373
|
+
socket.on("data", (chunk) => {
|
|
374
|
+
received = Buffer.concat([received, chunk])
|
|
375
|
+
if (received.length === 32) resolveReceived()
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
const targetPort = await listen(target)
|
|
379
|
+
const relays = await Promise.all([0, 1, 2].map(() => startMultiplexedTcpRelay({
|
|
380
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
381
|
+
authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort,
|
|
382
|
+
initialWindowBytes: 32,
|
|
383
|
+
maxFrameBytes: 32,
|
|
384
|
+
resumeWindowMs: 2_000
|
|
385
|
+
})))
|
|
386
|
+
const fault = await createFirstDataDropProxy(at(relays, 0).port)
|
|
387
|
+
const gatewayIds = ["starlink", "zyxel", "o2gateway"]
|
|
388
|
+
const physicalCreates = [0, 0, 0]
|
|
389
|
+
const physicalFactories = relays.map((_relay, index) => (/** @type {{host: string, port: number}} */ options) => {
|
|
390
|
+
physicalCreates[index] = at(physicalCreates, index) + 1
|
|
391
|
+
return createConnection(options)
|
|
392
|
+
})
|
|
393
|
+
const setOptions = {
|
|
394
|
+
connectionId: "replacement_process_identity_01",
|
|
395
|
+
spoolDirectory: root,
|
|
396
|
+
minimumReadyMembers: 3,
|
|
397
|
+
maxTotalBufferBytes: 128,
|
|
398
|
+
maxTotalSpoolBytes: 128,
|
|
399
|
+
maxTotalQueuedBytes: 1024,
|
|
400
|
+
memberOptions: {initialWindowBytes: 32, maxFrameBytes: 32, reconnectDelayMs: 1_000},
|
|
401
|
+
gateways: relays.map((relay, index) => ({
|
|
402
|
+
gatewayId: at(gatewayIds, index),
|
|
403
|
+
endpoint: {
|
|
404
|
+
relay: {host: "127.0.0.1", port: index === 0 ? fault.port : relay.port, token: "valid-token"},
|
|
405
|
+
createPhysicalSocket: at(physicalFactories, index)
|
|
406
|
+
}
|
|
407
|
+
}))
|
|
408
|
+
}
|
|
409
|
+
/** @type {MultiplexedSocketductTransportSet | undefined} */
|
|
410
|
+
let replacement
|
|
411
|
+
after(async () => {
|
|
412
|
+
await replacement?.close()
|
|
413
|
+
await Promise.all(relays.map((relay) => relay.close()))
|
|
414
|
+
await fault.close()
|
|
415
|
+
await new Promise((resolve) => target.close(() => resolve(undefined)))
|
|
416
|
+
await rm(root, {recursive: true, force: true})
|
|
417
|
+
})
|
|
421
418
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
})
|
|
436
|
-
await recoveryRead
|
|
437
|
-
await new Promise((resolve) => setImmediate(resolve))
|
|
438
|
-
assert.deepEqual(physicalCreates, [1, 1, 1])
|
|
439
|
-
assert.equal(fault.accepts(), 1)
|
|
440
|
-
assert.deepEqual(replacement.status().members.map((member) => member.assignments), [1, 0, 0])
|
|
441
|
-
const connected = onceEvent(recovered, "connect")
|
|
442
|
-
await replacement.start()
|
|
443
|
-
await connected
|
|
444
|
-
assert.deepEqual(physicalCreates, [2, 2, 2])
|
|
445
|
-
await receivedAll
|
|
419
|
+
const original = new MultiplexedSocketductTransportSet(setOptions)
|
|
420
|
+
await original.start()
|
|
421
|
+
expect(physicalCreates).toEqual([1, 1, 1])
|
|
422
|
+
const initial = original.createConnection({
|
|
423
|
+
host: "127.0.0.1",
|
|
424
|
+
port: targetPort,
|
|
425
|
+
identifier: "gateway_bound_stream_01",
|
|
426
|
+
sessionName: "gateway-bound"
|
|
427
|
+
})
|
|
428
|
+
await onceEvent(initial, "connect")
|
|
429
|
+
await new Promise((resolve, reject) => initial.write(Buffer.alloc(32, 0x72), (error) => error ? reject(error) : resolve(undefined)))
|
|
430
|
+
await fault.dropped
|
|
431
|
+
await original.detach()
|
|
446
432
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
433
|
+
const originalReadChunksAfter = SessionSpool.prototype.readChunksAfter
|
|
434
|
+
/** @type {() => void} */
|
|
435
|
+
let resolveRecoveryRead = () => {}
|
|
436
|
+
const recoveryRead = new Promise((resolve) => { resolveRecoveryRead = () => resolve(undefined) })
|
|
437
|
+
SessionSpool.prototype.readChunksAfter = async function (/** @type {string} */ identifier, /** @type {number} */ sequence) {
|
|
438
|
+
const chunks = await originalReadChunksAfter.call(this, identifier, sequence)
|
|
439
|
+
if (identifier === "gateway_bound_stream_01") resolveRecoveryRead()
|
|
440
|
+
return chunks
|
|
441
|
+
}
|
|
442
|
+
after(() => { SessionSpool.prototype.readChunksAfter = originalReadChunksAfter })
|
|
443
|
+
replacement = new MultiplexedSocketductTransportSet(setOptions)
|
|
444
|
+
const recovered = replacement.recoverConnection({
|
|
445
|
+
identifier: "gateway_bound_stream_01"
|
|
446
|
+
})
|
|
447
|
+
await recoveryRead
|
|
448
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
449
|
+
expect(physicalCreates).toEqual([1, 1, 1])
|
|
450
|
+
expect(fault.accepts()).toBe(1)
|
|
451
|
+
expect(replacement.status().members.map((member) => member.assignments)).toEqual([1, 0, 0])
|
|
452
|
+
const connected = onceEvent(recovered, "connect")
|
|
453
|
+
await replacement.start()
|
|
454
|
+
await connected
|
|
455
|
+
expect(physicalCreates).toEqual([2, 2, 2])
|
|
456
|
+
await receivedAll
|
|
457
|
+
|
|
458
|
+
expect(received.toString()).toBe("r".repeat(32))
|
|
459
|
+
expect(targetAccepts).toBe(1)
|
|
460
|
+
expect(fault.accepts()).toBe(2)
|
|
461
|
+
expect(relays.slice(1).map((relay) => relay.status().virtualStreams)).toEqual([0, 0])
|
|
462
|
+
recovered.destroy()
|
|
463
|
+
}))
|
|
452
464
|
})
|