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.
- package/.dockerignore +11 -0
- package/.env.example +131 -0
- package/AGENTS.md +168 -0
- package/Dockerfile +30 -0
- package/README.md +134 -0
- package/changelog.d/20260716083144-v2-ack-wire-format.md +3 -0
- package/changelog.d/20260716092044-v2-only-stream-framing.md +1 -0
- package/changelog.d/20260727113000-relay-connect-timeout-destroy.md +1 -0
- package/changelog.d/20260813093000-private-relay-hardening.md +1 -0
- package/changelog.d/20260824183412-initial-target-recovery-https.md +1 -0
- package/changelog.d/20260824230000-tls-relay-session-close.md +1 -0
- package/changelog.d/20260825120000-reconnect-close-target-timeout.md +1 -0
- package/changelog.d/20260829120000-protocol-v3-multiplexing.md +1 -0
- package/changelog.d/20260829160000-v3-review-corrections.md +1 -0
- package/changelog.d/20260830000000-fix-socket-idle-timeout.md +1 -0
- package/changelog.d/20260830090000-multi-gateway-transport-set.md +1 -0
- package/changelog.d/20260830093000-relay-close-settlement.md +1 -0
- package/changelog.d/20260830120000-immutable-gateway-image.md +1 -0
- package/changelog.d/20260902210000-native-reverse-gateway.md +1 -0
- package/changelog.d/20260902230000-relay-resume-terminal-order.md +1 -0
- package/changelog.d/20260902230708-multiplexed-open-ownership.md +1 -0
- package/changelog.d/20260902231541-multiplexed-terminal-close.md +1 -0
- package/changelog.d/20260902231542-reverse-close-position.md +1 -0
- package/changelog.d/20260902231543-reverse-terminal-connect.md +1 -0
- package/changelog.d/20260902233000-reverse-gateway-bounds.md +1 -0
- package/changelog.d/20260903001000-reverse-data-ordering.md +1 -0
- package/changelog.d/20260903003000-reverse-hub-restart-window.md +1 -0
- package/changelog.d/20260903010000-reverse-terminal-lifecycle.md +1 -0
- package/changelog.d/20260904142743-transport-set-readiness-wait.md +1 -0
- package/changelog.d/20260904152855-deferred-open-snapshots-binary-views.md +1 -0
- package/changelog.d/20260904163041-tensorbuzz-container-workflow.md +1 -0
- package/changelog.d/20260904174955-transport-set-terminal-causes.md +1 -0
- package/changelog.d/20260905081000-contain-reverse-auth-write-errors.md +1 -0
- package/changelog.d/20260905100520-reverse-hub-close-race.md +1 -0
- package/changelog.d/20260905120000-persistent-jsonl-logging.md +1 -0
- package/changelog.d/20260908071009-jsonl-partial-append-recovery.md +1 -0
- package/changelog.d/20260908074916-private-log-directory-setup.md +1 -0
- package/changelog.d/20260909093057-reverse-large-write-backpressure.md +1 -0
- package/changelog.d/20260909135830-actionable-error-diagnostics.md +1 -0
- package/changelog.d/20260909150000-transport-set-error-redaction.md +1 -0
- package/docker-compose.dev.yml +19 -0
- package/docker-compose.reverse-gateway.yml +36 -0
- package/docker-compose.yml +73 -0
- package/docs/architecture.md +264 -0
- package/docs/docker-topology.md +164 -0
- package/docs/docker.md +281 -0
- package/docs/logging.md +185 -0
- package/docs/plans/initial-implementation.md +205 -0
- package/docs/plans/socket-docker-integration.md +221 -0
- package/docs/protocol.md +558 -0
- package/docs/releasing.md +18 -0
- package/docs/reverse-gateway.md +186 -0
- package/docs/runbooks/jsonl-integrity-rollout.md +144 -0
- package/docs/running.md +215 -0
- package/docs/security.md +258 -0
- package/docs/session-lifecycle.md +127 -0
- package/docs/socket-api.md +448 -0
- package/eslint.config.js +36 -0
- package/package.json +44 -0
- package/scripts/deploy-gateway.js +194 -0
- package/scripts/test.js +28 -0
- package/src/auth/passwords.js +38 -0
- package/src/auth/static-token-store.js +41 -0
- package/src/auth/user-store.js +139 -0
- package/src/client/connect.js +76 -0
- package/src/client/deferred-virtual-socket.js +312 -0
- package/src/client/http-agent.js +101 -0
- package/src/client/https-agent.js +366 -0
- package/src/client/multiplexed-connection.js +1504 -0
- package/src/client/multiplexed-transport-set.js +917 -0
- package/src/client/multiplexed.js +3 -0
- package/src/client/socketduct-socket.js +1174 -0
- package/src/client/virtual-socket.js +256 -0
- package/src/config/environment.js +400 -0
- package/src/edge/tcp-edge.js +82 -0
- package/src/index.js +101 -0
- package/src/logging/structured-logger.js +745 -0
- package/src/protocol/compression.js +325 -0
- package/src/protocol/frame.js +341 -0
- package/src/protocol/v3-compression.js +44 -0
- package/src/protocol/v3-frame.js +209 -0
- package/src/proxy/multiplexed-relay.js +798 -0
- package/src/proxy/tcp-relay.js +1191 -0
- package/src/relay.js +3 -0
- package/src/reverse/client.js +721 -0
- package/src/reverse/common.js +88 -0
- package/src/reverse/gateway.js +663 -0
- package/src/reverse/hub.js +563 -0
- package/src/reverse/index.js +3 -0
- package/src/reverse/runtime.js +78 -0
- package/src/server/control-handler.js +170 -0
- package/src/server/primary.js +64 -0
- package/src/server/relay-runtime.js +124 -0
- package/src/server/worker.js +39 -0
- package/src/sessions/session.js +212 -0
- package/src/spool/spool-file.js +451 -0
- package/tensorbuzz.yml +36 -0
- package/test/e2e/gateway-process.test.js +262 -0
- package/test/e2e/reverse-gateway-process.test.js +285 -0
- package/test/fixtures/logging/enospc-matrix.mjs +238 -0
- package/test/fixtures/logging/enospc-recovery.mjs +146 -0
- package/test/fixtures/logging/process-writer.mjs +20 -0
- package/test/helpers/tls-certificates.js +115 -0
- package/test/integration/initial-target-recovery.test.js +518 -0
- package/test/integration/logging-recovery.test.js +163 -0
- package/test/integration/multiplexed-compatibility.test.js +254 -0
- package/test/integration/multiplexed-corrections.test.js +551 -0
- package/test/integration/multiplexed-failures.test.js +339 -0
- package/test/integration/multiplexed-transport-set.test.js +456 -0
- package/test/integration/multiplexed-transport.test.js +208 -0
- package/test/integration/reverse-gateway.test.js +1293 -0
- package/test/integration/socketduct-http-agent.test.js +435 -0
- package/test/integration/socketduct-https-agent.test.js +669 -0
- package/test/integration/stream-compression.test.js +1267 -0
- package/test/integration/tcp-forwarding.test.js +2878 -0
- package/test/integration/tls-forwarding.test.js +748 -0
- package/test/unit/auth/auth.test.js +64 -0
- package/test/unit/auth/static-token-store.test.js +14 -0
- package/test/unit/client/connect.test.js +58 -0
- package/test/unit/client/multiplexed-spool-budget.test.js +309 -0
- package/test/unit/client/multiplexed-transport-set.test.js +906 -0
- package/test/unit/client/socketduct-socket.test.js +1124 -0
- package/test/unit/client/virtual-socket.test.js +98 -0
- package/test/unit/config/docker-gateway-files.test.js +274 -0
- package/test/unit/config/environment.test.js +507 -0
- package/test/unit/index.test.js +78 -0
- package/test/unit/logging/structured-logger.test.js +674 -0
- package/test/unit/protocol/compression.test.js +421 -0
- package/test/unit/protocol/frame.test.js +336 -0
- package/test/unit/protocol/v3-compression.test.js +31 -0
- package/test/unit/protocol/v3-frame.test.js +57 -0
- package/test/unit/release-configuration.test.js +21 -0
- package/test/unit/server/control-handler.test.js +291 -0
- package/test/unit/server/primary.test.js +59 -0
- package/test/unit/server/relay-runtime.test.js +434 -0
- package/test/unit/sessions/session.test.js +84 -0
- package/test/unit/spool/spool-file.test.js +295 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/* eslint-disable jsdoc/escape-inline-tags, jsdoc/require-param, jsdoc/require-returns */
|
|
2
|
+
import assert from "node:assert/strict"
|
|
3
|
+
import {readFileSync} from "node:fs"
|
|
4
|
+
import {mkdtemp, rm} from "node:fs/promises"
|
|
5
|
+
import {createConnection, createServer} from "node:net"
|
|
6
|
+
import {tmpdir} from "node:os"
|
|
7
|
+
import {join} from "node:path"
|
|
8
|
+
import test from "node:test"
|
|
9
|
+
import {createServer as createTlsServer} from "node:tls"
|
|
10
|
+
|
|
11
|
+
import {StaticTokenStore} from "../../src/auth/static-token-store.js"
|
|
12
|
+
import {MultiplexedSocketductTransportSet} from "../../src/client/multiplexed-transport-set.js"
|
|
13
|
+
import {encodeV3Frame, V3FrameDecoder, V3FrameType} from "../../src/protocol/v3-frame.js"
|
|
14
|
+
import {startMultiplexedTcpRelay} from "../../src/proxy/multiplexed-relay.js"
|
|
15
|
+
import {SessionSpool} from "../../src/spool/spool-file.js"
|
|
16
|
+
import {generateTestCertificates} from "../helpers/tls-certificates.js"
|
|
17
|
+
|
|
18
|
+
/** @param {import('node:net').Server} server - Server. @returns {Promise<number>} */
|
|
19
|
+
async function listen(server) {
|
|
20
|
+
await new Promise((resolve, reject) => {
|
|
21
|
+
server.once("error", reject)
|
|
22
|
+
server.listen(0, "127.0.0.1", () => { server.off("error", reject); resolve(undefined) })
|
|
23
|
+
})
|
|
24
|
+
const address = server.address()
|
|
25
|
+
assert(address !== null && typeof address !== "string")
|
|
26
|
+
return address.port
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @param {import('node:events').EventEmitter} emitter - Emitter. @param {string} event - Event. @returns {Promise<void>} */
|
|
30
|
+
async function onceEvent(emitter, event) {
|
|
31
|
+
await new Promise((resolve, reject) => {
|
|
32
|
+
emitter.once("error", reject)
|
|
33
|
+
emitter.once(event, () => { emitter.off("error", reject); resolve(undefined) })
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {MultiplexedSocketductTransportSet} set - Transport set.
|
|
39
|
+
* @param {(status: ReturnType<MultiplexedSocketductTransportSet['status']>) => boolean} predicate - Condition.
|
|
40
|
+
* @returns {Promise<void>}
|
|
41
|
+
*/
|
|
42
|
+
async function waitForStatus(set, predicate) {
|
|
43
|
+
if (predicate(set.status())) return
|
|
44
|
+
await new Promise((resolve, reject) => {
|
|
45
|
+
const timeout = setTimeout(() => { cleanup(); reject(new Error("timed out waiting for transport-set status")) }, 2_000)
|
|
46
|
+
const changed = () => {
|
|
47
|
+
if (!predicate(set.status())) return
|
|
48
|
+
cleanup()
|
|
49
|
+
resolve(undefined)
|
|
50
|
+
}
|
|
51
|
+
const cleanup = () => {
|
|
52
|
+
clearTimeout(timeout)
|
|
53
|
+
set.off("stateChange", changed)
|
|
54
|
+
}
|
|
55
|
+
set.on("stateChange", changed)
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @template T @param {T[]} values - Values. @param {number} index - Index. @returns {T} */
|
|
60
|
+
function at(values, index) {
|
|
61
|
+
const value = values[index]
|
|
62
|
+
assert(value !== undefined)
|
|
63
|
+
return value
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @param {number} relayPort - Relay port. */
|
|
67
|
+
async function createFirstDataDropProxy(relayPort) {
|
|
68
|
+
let accepts = 0
|
|
69
|
+
/** @type {() => void} */
|
|
70
|
+
let resolveDropped = () => {}
|
|
71
|
+
const dropped = new Promise((resolve) => { resolveDropped = () => resolve(undefined) })
|
|
72
|
+
const sockets = new Set()
|
|
73
|
+
const server = createServer((downstream) => {
|
|
74
|
+
sockets.add(downstream)
|
|
75
|
+
accepts += 1
|
|
76
|
+
const generation = accepts
|
|
77
|
+
const upstream = createConnection({host: "127.0.0.1", port: relayPort})
|
|
78
|
+
sockets.add(upstream)
|
|
79
|
+
const cleanup = () => {
|
|
80
|
+
sockets.delete(downstream)
|
|
81
|
+
sockets.delete(upstream)
|
|
82
|
+
}
|
|
83
|
+
downstream.once("close", cleanup)
|
|
84
|
+
upstream.once("close", cleanup)
|
|
85
|
+
downstream.on("error", () => {})
|
|
86
|
+
upstream.on("error", () => {})
|
|
87
|
+
if (generation !== 1) {
|
|
88
|
+
downstream.pipe(upstream)
|
|
89
|
+
upstream.pipe(downstream)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
const decoder = new V3FrameDecoder({maxFrameBytes: 64 * 1024, maxBufferedBytes: 128 * 1024})
|
|
93
|
+
downstream.on("data", (chunk) => {
|
|
94
|
+
for (const frame of decoder.push(chunk)) {
|
|
95
|
+
if (frame.type === V3FrameType.DATA) {
|
|
96
|
+
resolveDropped()
|
|
97
|
+
downstream.destroy()
|
|
98
|
+
upstream.destroy()
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
upstream.write(encodeV3Frame(frame, frame.payload))
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
upstream.pipe(downstream)
|
|
105
|
+
})
|
|
106
|
+
const port = await listen(server)
|
|
107
|
+
return {
|
|
108
|
+
port,
|
|
109
|
+
dropped,
|
|
110
|
+
accepts: () => accepts,
|
|
111
|
+
async close() {
|
|
112
|
+
for (const socket of sockets) socket.destroy()
|
|
113
|
+
await new Promise((resolve) => server.close(() => resolve(undefined)))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
test("three pinned gateways warm once and balance virtual sockets without extra physical accepts", async (t) => {
|
|
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
|
+
t.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
|
+
t.after(() => Promise.all(relays.map((relay) => relay.close())))
|
|
130
|
+
|
|
131
|
+
const set = new MultiplexedSocketductTransportSet({
|
|
132
|
+
connectionId: "integration_process_01",
|
|
133
|
+
spoolDirectory: root,
|
|
134
|
+
minimumReadyMembers: 3,
|
|
135
|
+
maxTotalBufferBytes: 32 * 1024 * 1024,
|
|
136
|
+
maxTotalSpoolBytes: 32 * 1024 * 1024,
|
|
137
|
+
maxTotalQueuedBytes: 33 * 1024 * 1024,
|
|
138
|
+
memberOptions: {maxStreams: 256, reconnectDelayMs: 10},
|
|
139
|
+
gateways: relays.map((relay, index) => ({
|
|
140
|
+
gatewayId: at(gatewayIds, index),
|
|
141
|
+
endpoint: {relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"}}
|
|
142
|
+
}))
|
|
143
|
+
})
|
|
144
|
+
t.after(async () => {
|
|
145
|
+
await set.close()
|
|
146
|
+
await rm(root, {recursive: true, force: true})
|
|
147
|
+
})
|
|
148
|
+
const started = set.start()
|
|
149
|
+
const firstSocket = set.createConnection({
|
|
150
|
+
host: "127.0.0.1",
|
|
151
|
+
port: targetPort,
|
|
152
|
+
sessionName: "set-flow-1"
|
|
153
|
+
})
|
|
154
|
+
const firstConnected = onceEvent(firstSocket, "connect")
|
|
155
|
+
assert.equal(firstSocket.gatewayId, undefined)
|
|
156
|
+
await Promise.all([started, firstConnected])
|
|
157
|
+
assert.deepEqual(relays.map((relay) => relay.status().physicalAccepts), [1, 1, 1])
|
|
158
|
+
assert.deepEqual(relays.map((relay) => relay.status().virtualStreams), [1, 0, 0])
|
|
159
|
+
|
|
160
|
+
const sockets = [firstSocket, ...Array.from({length: 99}, (_, index) => set.createConnection({
|
|
161
|
+
host: "127.0.0.1",
|
|
162
|
+
port: targetPort,
|
|
163
|
+
sessionName: `set-flow-${index + 2}`
|
|
164
|
+
}))]
|
|
165
|
+
await Promise.all(sockets.slice(1).map((socket) => onceEvent(socket, "connect")))
|
|
166
|
+
assert.deepEqual(set.status().members.map((member) => member.assignments), [34, 33, 33])
|
|
167
|
+
assert.deepEqual(relays.map((relay) => relay.status().physicalAccepts), [1, 1, 1])
|
|
168
|
+
assert.deepEqual(relays.map((relay) => relay.status().virtualStreams), [34, 33, 33])
|
|
169
|
+
sockets.forEach((socket) => socket.destroy())
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test("a failed gateway keeps its streams pinned while healthy siblings take new opens and recovery rejoins", async (t) => {
|
|
173
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-set-recovery-"))
|
|
174
|
+
const target = createServer((socket) => socket.on("data", (chunk) => socket.write(chunk)))
|
|
175
|
+
const targetPort = await listen(target)
|
|
176
|
+
t.after(() => new Promise((resolve) => target.close(() => resolve(undefined))))
|
|
177
|
+
const relays = await Promise.all([0, 1, 2].map(() => startMultiplexedTcpRelay({
|
|
178
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
179
|
+
authorizeTarget: ({target: requested}) => requested.port === targetPort
|
|
180
|
+
})))
|
|
181
|
+
const gatewayIds = ["starlink", "zyxel", "o2gateway"]
|
|
182
|
+
t.after(() => Promise.all(relays.map((relay) => relay.close())))
|
|
183
|
+
const set = new MultiplexedSocketductTransportSet({
|
|
184
|
+
connectionId: "integration_process_recovery_01",
|
|
185
|
+
spoolDirectory: root,
|
|
186
|
+
maxTotalBufferBytes: 32 * 1024 * 1024,
|
|
187
|
+
maxTotalSpoolBytes: 32 * 1024 * 1024,
|
|
188
|
+
maxTotalQueuedBytes: 33 * 1024 * 1024,
|
|
189
|
+
memberOptions: {reconnectDelayMs: 100},
|
|
190
|
+
gateways: relays.map((relay, index) => ({
|
|
191
|
+
gatewayId: at(gatewayIds, index),
|
|
192
|
+
endpoint: {relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"}}
|
|
193
|
+
}))
|
|
194
|
+
})
|
|
195
|
+
t.after(async () => {
|
|
196
|
+
await set.close()
|
|
197
|
+
await rm(root, {recursive: true, force: true})
|
|
198
|
+
})
|
|
199
|
+
await set.start()
|
|
200
|
+
await waitForStatus(set, (status) => status.readyMembers === 3)
|
|
201
|
+
const sticky = Array.from({length: 3}, () => set.createConnection({host: "127.0.0.1", port: targetPort}))
|
|
202
|
+
await Promise.all(sticky.map((socket) => onceEvent(socket, "connect")))
|
|
203
|
+
const recovered = onceEvent(at(sticky, 1), "ready")
|
|
204
|
+
await at(relays, 1).closeActiveTransport()
|
|
205
|
+
await waitForStatus(set, (status) => at(status.members, 1).state === "reconnecting")
|
|
206
|
+
|
|
207
|
+
const siblingOpens = Array.from({length: 10}, () => set.createConnection({host: "127.0.0.1", port: targetPort}))
|
|
208
|
+
assert.equal(at(set.status().members, 1).assignments, 1)
|
|
209
|
+
const siblingCounts = [at(set.status().members, 0).assignments, at(set.status().members, 2).assignments]
|
|
210
|
+
assert(Math.max(...siblingCounts) - Math.min(...siblingCounts) <= 1)
|
|
211
|
+
await Promise.all(siblingOpens.map((socket) => onceEvent(socket, "connect")))
|
|
212
|
+
await recovered
|
|
213
|
+
await waitForStatus(set, (status) => status.readyMembers === 3)
|
|
214
|
+
const before = at(set.status().members, 1).assignments
|
|
215
|
+
const rejoined = Array.from({length: 3}, () => set.createConnection({host: "127.0.0.1", port: targetPort}))
|
|
216
|
+
assert(at(set.status().members, 1).assignments > before)
|
|
217
|
+
assert.equal(at(sticky, 1).destroyed, false)
|
|
218
|
+
assert.equal(at(relays, 1).status().physicalAccepts, 2)
|
|
219
|
+
sticky.forEach((socket) => socket.destroy())
|
|
220
|
+
siblingOpens.forEach((socket) => socket.destroy())
|
|
221
|
+
rejoined.forEach((socket) => socket.destroy())
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test("all reconnecting members hold a new socket until one real member recovers", async (t) => {
|
|
225
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-set-all-reconnect-"))
|
|
226
|
+
const target = createServer((socket) => socket.on("data", (chunk) => socket.write(chunk)))
|
|
227
|
+
const targetPort = await listen(target)
|
|
228
|
+
const relays = await Promise.all([0, 1, 2].map(() => startMultiplexedTcpRelay({
|
|
229
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
230
|
+
authorizeTarget: ({target: requested}) => requested.port === targetPort
|
|
231
|
+
})))
|
|
232
|
+
const gatewayIds = ["starlink", "zyxel", "o2gateway"]
|
|
233
|
+
const set = new MultiplexedSocketductTransportSet({
|
|
234
|
+
connectionId: "integration_all_reconnect_01",
|
|
235
|
+
spoolDirectory: root,
|
|
236
|
+
minimumReadyMembers: 3,
|
|
237
|
+
memberOptions: {reconnectDelayMs: 1_000},
|
|
238
|
+
gateways: relays.map((relay, index) => ({
|
|
239
|
+
gatewayId: at(gatewayIds, index),
|
|
240
|
+
endpoint: {relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"}}
|
|
241
|
+
}))
|
|
242
|
+
})
|
|
243
|
+
let siblingRelaysClosed = false
|
|
244
|
+
t.after(async () => {
|
|
245
|
+
await set.close()
|
|
246
|
+
if (!siblingRelaysClosed) await Promise.all(relays.slice(1).map((relay) => relay.close()))
|
|
247
|
+
await at(relays, 0).close()
|
|
248
|
+
await new Promise((resolve) => target.close(() => resolve(undefined)))
|
|
249
|
+
await rm(root, {recursive: true, force: true})
|
|
250
|
+
})
|
|
251
|
+
await set.start()
|
|
252
|
+
await Promise.all(relays.map((relay) => relay.closeActiveTransport()))
|
|
253
|
+
await waitForStatus(set, (status) => status.readyMembers === 0)
|
|
254
|
+
await Promise.all(relays.slice(1).map((relay) => relay.close()))
|
|
255
|
+
siblingRelaysClosed = true
|
|
256
|
+
|
|
257
|
+
const socket = set.createConnection({host: "127.0.0.1", port: targetPort})
|
|
258
|
+
assert.equal(socket.gatewayId, undefined)
|
|
259
|
+
assert.equal(set.status().pendingOpens, 1)
|
|
260
|
+
await onceEvent(socket, "connect")
|
|
261
|
+
assert.equal(socket.gatewayId, "starlink")
|
|
262
|
+
assert.equal(set.status().pendingOpens, 0)
|
|
263
|
+
assert.deepEqual(set.status().members.map((member) => member.assignments), [1, 0, 0])
|
|
264
|
+
socket.destroy()
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
test("terminal authentication failure rejects start and the pending socket once", async (t) => {
|
|
268
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-set-start-failure-"))
|
|
269
|
+
const relay = await startMultiplexedTcpRelay({
|
|
270
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
271
|
+
authorizeTarget: () => true
|
|
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})
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
await assert.rejects(started, (error) => error instanceof AggregateError)
|
|
292
|
+
const result = /** @type {{error: Error, socket: import('../../src/client/virtual-socket.js').VirtualSocket}} */ (await callbackResult)
|
|
293
|
+
assert(result.error instanceof AggregateError)
|
|
294
|
+
assert.equal(callbacks, 1)
|
|
295
|
+
assert.equal(result.socket.destroyed, true)
|
|
296
|
+
assert.equal(set.status().state, "failed")
|
|
297
|
+
assert.equal(relay.status().virtualStreams, 0)
|
|
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)
|
|
335
|
+
})
|
|
336
|
+
assert(returned !== undefined)
|
|
337
|
+
assert.equal(returned.gatewayId, undefined)
|
|
338
|
+
}))
|
|
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
|
+
|
|
353
|
+
test("process replacement recovers a durable stream on its original named gateway before start", async (t) => {
|
|
354
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-set-process-recovery-"))
|
|
355
|
+
let targetAccepts = 0
|
|
356
|
+
let received = Buffer.alloc(0)
|
|
357
|
+
/** @type {() => void} */
|
|
358
|
+
let resolveReceived = () => {}
|
|
359
|
+
const receivedAll = new Promise((resolve) => { resolveReceived = () => resolve(undefined) })
|
|
360
|
+
const target = createServer((socket) => {
|
|
361
|
+
targetAccepts += 1
|
|
362
|
+
socket.on("data", (chunk) => {
|
|
363
|
+
received = Buffer.concat([received, chunk])
|
|
364
|
+
if (received.length === 32) resolveReceived()
|
|
365
|
+
})
|
|
366
|
+
})
|
|
367
|
+
const targetPort = await listen(target)
|
|
368
|
+
const relays = await Promise.all([0, 1, 2].map(() => startMultiplexedTcpRelay({
|
|
369
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
370
|
+
authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort,
|
|
371
|
+
initialWindowBytes: 32,
|
|
372
|
+
maxFrameBytes: 32,
|
|
373
|
+
resumeWindowMs: 2_000
|
|
374
|
+
})))
|
|
375
|
+
const fault = await createFirstDataDropProxy(at(relays, 0).port)
|
|
376
|
+
const gatewayIds = ["starlink", "zyxel", "o2gateway"]
|
|
377
|
+
const physicalCreates = [0, 0, 0]
|
|
378
|
+
const physicalFactories = relays.map((_relay, index) => (/** @type {{host: string, port: number}} */ options) => {
|
|
379
|
+
physicalCreates[index] = at(physicalCreates, index) + 1
|
|
380
|
+
return createConnection(options)
|
|
381
|
+
})
|
|
382
|
+
const setOptions = {
|
|
383
|
+
connectionId: "replacement_process_identity_01",
|
|
384
|
+
spoolDirectory: root,
|
|
385
|
+
minimumReadyMembers: 3,
|
|
386
|
+
maxTotalBufferBytes: 128,
|
|
387
|
+
maxTotalSpoolBytes: 128,
|
|
388
|
+
maxTotalQueuedBytes: 1024,
|
|
389
|
+
memberOptions: {initialWindowBytes: 32, maxFrameBytes: 32, reconnectDelayMs: 1_000},
|
|
390
|
+
gateways: relays.map((relay, index) => ({
|
|
391
|
+
gatewayId: at(gatewayIds, index),
|
|
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
|
+
}
|
|
396
|
+
}))
|
|
397
|
+
}
|
|
398
|
+
/** @type {MultiplexedSocketductTransportSet | undefined} */
|
|
399
|
+
let replacement
|
|
400
|
+
t.after(async () => {
|
|
401
|
+
await replacement?.close()
|
|
402
|
+
await Promise.all(relays.map((relay) => relay.close()))
|
|
403
|
+
await fault.close()
|
|
404
|
+
await new Promise((resolve) => target.close(() => resolve(undefined)))
|
|
405
|
+
await rm(root, {recursive: true, force: true})
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
const original = new MultiplexedSocketductTransportSet(setOptions)
|
|
409
|
+
await original.start()
|
|
410
|
+
assert.deepEqual(physicalCreates, [1, 1, 1])
|
|
411
|
+
const initial = original.createConnection({
|
|
412
|
+
host: "127.0.0.1",
|
|
413
|
+
port: targetPort,
|
|
414
|
+
identifier: "gateway_bound_stream_01",
|
|
415
|
+
sessionName: "gateway-bound"
|
|
416
|
+
})
|
|
417
|
+
await onceEvent(initial, "connect")
|
|
418
|
+
await new Promise((resolve, reject) => initial.write(Buffer.alloc(32, 0x72), (error) => error ? reject(error) : resolve(undefined)))
|
|
419
|
+
await fault.dropped
|
|
420
|
+
await original.detach()
|
|
421
|
+
|
|
422
|
+
const originalReadChunksAfter = SessionSpool.prototype.readChunksAfter
|
|
423
|
+
/** @type {() => void} */
|
|
424
|
+
let resolveRecoveryRead = () => {}
|
|
425
|
+
const recoveryRead = new Promise((resolve) => { resolveRecoveryRead = () => resolve(undefined) })
|
|
426
|
+
SessionSpool.prototype.readChunksAfter = async function (/** @type {string} */ identifier, /** @type {number} */ sequence) {
|
|
427
|
+
const chunks = await originalReadChunksAfter.call(this, identifier, sequence)
|
|
428
|
+
if (identifier === "gateway_bound_stream_01") resolveRecoveryRead()
|
|
429
|
+
return chunks
|
|
430
|
+
}
|
|
431
|
+
t.after(() => { SessionSpool.prototype.readChunksAfter = originalReadChunksAfter })
|
|
432
|
+
replacement = new MultiplexedSocketductTransportSet(setOptions)
|
|
433
|
+
const recovered = replacement.recoverConnection({
|
|
434
|
+
gatewayId: "starlink",
|
|
435
|
+
host: "127.0.0.1",
|
|
436
|
+
port: targetPort,
|
|
437
|
+
identifier: "gateway_bound_stream_01",
|
|
438
|
+
sessionName: "gateway-bound"
|
|
439
|
+
})
|
|
440
|
+
await recoveryRead
|
|
441
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
442
|
+
assert.deepEqual(physicalCreates, [1, 1, 1])
|
|
443
|
+
assert.equal(fault.accepts(), 1)
|
|
444
|
+
assert.deepEqual(replacement.status().members.map((member) => member.assignments), [1, 0, 0])
|
|
445
|
+
const connected = onceEvent(recovered, "connect")
|
|
446
|
+
await replacement.start()
|
|
447
|
+
await connected
|
|
448
|
+
assert.deepEqual(physicalCreates, [2, 2, 2])
|
|
449
|
+
await receivedAll
|
|
450
|
+
|
|
451
|
+
assert.equal(received.toString(), "r".repeat(32))
|
|
452
|
+
assert.equal(targetAccepts, 1)
|
|
453
|
+
assert.equal(fault.accepts(), 2)
|
|
454
|
+
assert.deepEqual(relays.slice(1).map((relay) => relay.status().virtualStreams), [0, 0])
|
|
455
|
+
recovered.destroy()
|
|
456
|
+
})
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/* eslint-disable jsdoc/escape-inline-tags, jsdoc/require-param-type, jsdoc/require-returns */
|
|
2
|
+
import assert from "node:assert/strict"
|
|
3
|
+
import {createServer} from "node:net"
|
|
4
|
+
import test from "node:test"
|
|
5
|
+
|
|
6
|
+
import {StaticTokenStore} from "../../src/auth/static-token-store.js"
|
|
7
|
+
import {MultiplexedSocketductConnection} from "../../src/client/multiplexed-connection.js"
|
|
8
|
+
import {startMultiplexedTcpRelay} from "../../src/proxy/multiplexed-relay.js"
|
|
9
|
+
|
|
10
|
+
/** @param {import('node:net').Server} server - Server. @returns {Promise<number>} */
|
|
11
|
+
async function listen(server) {
|
|
12
|
+
await new Promise((resolve, reject) => {
|
|
13
|
+
server.once("error", reject)
|
|
14
|
+
server.listen(0, "127.0.0.1", () => {
|
|
15
|
+
server.off("error", reject)
|
|
16
|
+
resolve(undefined)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
const address = server.address()
|
|
20
|
+
assert(address !== null && typeof address !== "string")
|
|
21
|
+
return address.port
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {import('node:events').EventEmitter} emitter - Emitter. @param {string} event - Event. @returns {Promise<void>}
|
|
26
|
+
* @param event
|
|
27
|
+
*/
|
|
28
|
+
async function onceEvent(emitter, event) {
|
|
29
|
+
await new Promise((resolve, reject) => {
|
|
30
|
+
emitter.once("error", reject)
|
|
31
|
+
emitter.once(event, () => {
|
|
32
|
+
emitter.off("error", reject)
|
|
33
|
+
resolve(undefined)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {import('node:stream').Duplex} socket - Socket. @param {string} expected - Expected text. @returns {Promise<void>}
|
|
40
|
+
* @param expected
|
|
41
|
+
*/
|
|
42
|
+
async function readExactly(socket, expected) {
|
|
43
|
+
await new Promise((resolve, reject) => {
|
|
44
|
+
let value = ""
|
|
45
|
+
/** @param {Error} error - Read error. */
|
|
46
|
+
const onError = (error) => { cleanup(); reject(error) }
|
|
47
|
+
/** @param {Buffer} chunk - Data. */
|
|
48
|
+
const onData = (chunk) => {
|
|
49
|
+
value += chunk.toString()
|
|
50
|
+
if (value.length < expected.length) return
|
|
51
|
+
cleanup()
|
|
52
|
+
try {
|
|
53
|
+
assert.equal(value, expected)
|
|
54
|
+
resolve(undefined)
|
|
55
|
+
} catch (error) {
|
|
56
|
+
reject(error)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const cleanup = () => {
|
|
60
|
+
socket.off("error", onError)
|
|
61
|
+
socket.off("data", onData)
|
|
62
|
+
}
|
|
63
|
+
socket.on("error", onError)
|
|
64
|
+
socket.on("data", onData)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test("two virtual sockets use one authenticated physical transport", async (t) => {
|
|
69
|
+
let targetAccepts = 0
|
|
70
|
+
const target = createServer((socket) => {
|
|
71
|
+
targetAccepts += 1
|
|
72
|
+
socket.on("data", (chunk) => socket.write(chunk))
|
|
73
|
+
})
|
|
74
|
+
const targetPort = await listen(target)
|
|
75
|
+
t.after(() => new Promise((resolve) => target.close(() => resolve(undefined))))
|
|
76
|
+
|
|
77
|
+
const relay = await startMultiplexedTcpRelay({
|
|
78
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
79
|
+
authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort
|
|
80
|
+
})
|
|
81
|
+
t.after(() => relay.close())
|
|
82
|
+
|
|
83
|
+
const connection = new MultiplexedSocketductConnection({
|
|
84
|
+
relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"}
|
|
85
|
+
})
|
|
86
|
+
t.after(() => connection.close())
|
|
87
|
+
|
|
88
|
+
const first = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "first"})
|
|
89
|
+
const second = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "second"})
|
|
90
|
+
await Promise.all([onceEvent(first, "connect"), onceEvent(second, "connect")])
|
|
91
|
+
|
|
92
|
+
first.write("one")
|
|
93
|
+
second.write("two")
|
|
94
|
+
await Promise.all([readExactly(first, "one"), readExactly(second, "two")])
|
|
95
|
+
|
|
96
|
+
assert.equal(relay.status().physicalAccepts, 1)
|
|
97
|
+
assert.equal(relay.status().authenticatedConnections, 1)
|
|
98
|
+
assert.equal(targetAccepts, 2)
|
|
99
|
+
assert.equal(connection.status().virtualStreams, 2)
|
|
100
|
+
|
|
101
|
+
first.destroy()
|
|
102
|
+
assert.equal(second.destroyed, false)
|
|
103
|
+
second.destroy()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test("one transport drop has one reconnect owner and resumes sibling streams without gaps", async (t) => {
|
|
107
|
+
/** @type {Array<{event: string} & Record<string, unknown>>} */
|
|
108
|
+
const lifecycle = []
|
|
109
|
+
const logger = {log: (/** @type {string} */ event, /** @type {Record<string, unknown>} */ fields = {}) => lifecycle.push({event, ...fields})}
|
|
110
|
+
const target = createServer((socket) => socket.on("data", (chunk) => socket.write(chunk)))
|
|
111
|
+
const targetPort = await listen(target)
|
|
112
|
+
t.after(() => new Promise((resolve) => target.close(() => resolve(undefined))))
|
|
113
|
+
const relay = await startMultiplexedTcpRelay({
|
|
114
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
115
|
+
authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort
|
|
116
|
+
})
|
|
117
|
+
t.after(() => relay.close())
|
|
118
|
+
const connection = new MultiplexedSocketductConnection({
|
|
119
|
+
relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"},
|
|
120
|
+
reconnectDelayMs: 10,
|
|
121
|
+
logger
|
|
122
|
+
})
|
|
123
|
+
t.after(() => connection.close())
|
|
124
|
+
const first = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "resume-a"})
|
|
125
|
+
const second = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "resume-b"})
|
|
126
|
+
await Promise.all([onceEvent(first, "connect"), onceEvent(second, "connect")])
|
|
127
|
+
first.write("before-a")
|
|
128
|
+
second.write("before-b")
|
|
129
|
+
await Promise.all([readExactly(first, "before-a"), readExactly(second, "before-b")])
|
|
130
|
+
|
|
131
|
+
const ready = Promise.all([onceEvent(first, "ready"), onceEvent(second, "ready")])
|
|
132
|
+
await relay.closeActiveTransport()
|
|
133
|
+
first.write("after-a")
|
|
134
|
+
second.write("after-b")
|
|
135
|
+
await ready
|
|
136
|
+
await Promise.all([readExactly(first, "after-a"), readExactly(second, "after-b")])
|
|
137
|
+
|
|
138
|
+
assert.equal(relay.status().physicalAccepts, 2)
|
|
139
|
+
assert.equal(connection.status().reconnectAttempts, 1)
|
|
140
|
+
assert.equal(connection.status().reconnectSuccesses, 1)
|
|
141
|
+
assert.equal(relay.status().resumeSuccesses, 2)
|
|
142
|
+
const closed = Promise.all([onceEvent(first, "close"), onceEvent(second, "close")])
|
|
143
|
+
first.destroy()
|
|
144
|
+
second.destroy()
|
|
145
|
+
await closed
|
|
146
|
+
for (const event of [
|
|
147
|
+
"transport.connecting",
|
|
148
|
+
"transport.connected",
|
|
149
|
+
"transport.disconnected",
|
|
150
|
+
"transport.reconnect_scheduled",
|
|
151
|
+
"transport.reconnect_succeeded",
|
|
152
|
+
"virtual_stream.opening",
|
|
153
|
+
"virtual_stream.opened",
|
|
154
|
+
"virtual_stream.closed"
|
|
155
|
+
]) assert(lifecycle.some((entry) => entry.event === event), `missing ${event}`)
|
|
156
|
+
assert.doesNotMatch(JSON.stringify(lifecycle), /valid-token|resume-a|resume-b|127\.0\.0\.1/)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test("100 concurrent build-like streams create zero extra physical accepts", async (t) => {
|
|
160
|
+
let targetAccepts = 0
|
|
161
|
+
const target = createServer((socket) => {
|
|
162
|
+
targetAccepts += 1
|
|
163
|
+
socket.on("data", (chunk) => {
|
|
164
|
+
const split = Math.max(1, Math.floor(chunk.length / 2))
|
|
165
|
+
socket.write(chunk.subarray(0, split))
|
|
166
|
+
socket.write(chunk.subarray(split))
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
const targetPort = await listen(target)
|
|
170
|
+
t.after(() => new Promise((resolve) => target.close(() => resolve(undefined))))
|
|
171
|
+
const relay = await startMultiplexedTcpRelay({
|
|
172
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
173
|
+
authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort,
|
|
174
|
+
maxStreams: 128
|
|
175
|
+
})
|
|
176
|
+
t.after(() => relay.close())
|
|
177
|
+
const connection = new MultiplexedSocketductConnection({
|
|
178
|
+
relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"},
|
|
179
|
+
maxStreams: 128
|
|
180
|
+
})
|
|
181
|
+
t.after(() => connection.close())
|
|
182
|
+
|
|
183
|
+
const sockets = Array.from({length: 100}, (_, index) => connection.createConnection({
|
|
184
|
+
host: "127.0.0.1",
|
|
185
|
+
port: targetPort,
|
|
186
|
+
sessionName: `build-flow-${index + 1}`
|
|
187
|
+
}))
|
|
188
|
+
await Promise.all(sockets.map((socket) => onceEvent(socket, "connect")))
|
|
189
|
+
const reads = sockets.map((socket, index) => readExactly(socket, `flow-${index + 1}:${"x".repeat(1024)}`))
|
|
190
|
+
sockets.forEach((socket, index) => socket.write(`flow-${index + 1}:${"x".repeat(1024)}`))
|
|
191
|
+
await Promise.all(reads)
|
|
192
|
+
|
|
193
|
+
assert.equal(relay.status().physicalAccepts, 1)
|
|
194
|
+
assert.equal(relay.status().physicalConnections, 1)
|
|
195
|
+
assert.equal(targetAccepts, 100)
|
|
196
|
+
assert.equal(connection.status().virtualStreams, 100)
|
|
197
|
+
|
|
198
|
+
const resumed = Promise.all(sockets.map((socket) => onceEvent(socket, "ready")))
|
|
199
|
+
await relay.closeActiveTransport()
|
|
200
|
+
await resumed
|
|
201
|
+
const resumedReads = sockets.map((socket, index) => readExactly(socket, `resumed-${index + 1}`))
|
|
202
|
+
sockets.forEach((socket, index) => socket.write(`resumed-${index + 1}`))
|
|
203
|
+
await Promise.all(resumedReads)
|
|
204
|
+
assert.equal(relay.status().physicalAccepts, 2)
|
|
205
|
+
assert.equal(relay.status().resumeSuccesses, 100)
|
|
206
|
+
assert.equal(connection.status().reconnectAttempts, 1)
|
|
207
|
+
sockets.forEach((socket) => socket.destroy())
|
|
208
|
+
})
|