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,748 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import {connect as tlsConnect} from "node:tls"
|
|
3
|
+
import {createConnection, createServer} from "node:net"
|
|
4
|
+
import {mkdtemp, rm} from "node:fs/promises"
|
|
5
|
+
import {mkdtempSync} from "node:fs"
|
|
6
|
+
import {tmpdir} from "node:os"
|
|
7
|
+
import {join} from "node:path"
|
|
8
|
+
import {readFileSync} from "node:fs"
|
|
9
|
+
import test from "node:test"
|
|
10
|
+
|
|
11
|
+
import {InMemoryUserStore, login, TokenStore} from "../../src/auth/user-store.js"
|
|
12
|
+
import {SocketductSocket, connectSocketduct} from "../../src/client/socketduct-socket.js"
|
|
13
|
+
import {startTcpEdge} from "../../src/edge/tcp-edge.js"
|
|
14
|
+
import {encodeCompactData, encodeFrame, FrameDecoder} from "../../src/protocol/frame.js"
|
|
15
|
+
import {startTcpRelay} from "../../src/proxy/tcp-relay.js"
|
|
16
|
+
import {SessionStore} from "../../src/sessions/session.js"
|
|
17
|
+
import {SessionSpool} from "../../src/spool/spool-file.js"
|
|
18
|
+
import {generateTestCertificates} from "../helpers/tls-certificates.js"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {import('node:net').Server} server - Server to bind.
|
|
22
|
+
* @returns {Promise<number>} - Bound port.
|
|
23
|
+
*/
|
|
24
|
+
async function listen(server) {
|
|
25
|
+
await new Promise((resolve, reject) => {
|
|
26
|
+
server.once("error", reject)
|
|
27
|
+
server.listen(0, "127.0.0.1", () => {
|
|
28
|
+
server.off("error", reject)
|
|
29
|
+
resolve(undefined)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
const address = server.address()
|
|
33
|
+
if (address === null || typeof address === "string") {
|
|
34
|
+
throw new Error("Server did not bind to a TCP port")
|
|
35
|
+
}
|
|
36
|
+
return /** @type {import('node:net').AddressInfo} */ (address).port
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {import('node:net').Socket | import('../../src/client/socketduct-socket.js').SocketductSocket} socket - Socket to read from.
|
|
41
|
+
* @returns {Promise<Buffer>} - Next data chunk.
|
|
42
|
+
*/
|
|
43
|
+
async function readOnce(socket) {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
socket.once("data", (chunk) => resolve(chunk))
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {import('node:net').Socket} socket - Socket to wait on.
|
|
51
|
+
* @returns {Promise<void>} - Resolves after connection.
|
|
52
|
+
*/
|
|
53
|
+
async function waitConnect(socket) {
|
|
54
|
+
await new Promise((resolve, reject) => {
|
|
55
|
+
socket.once("error", reject)
|
|
56
|
+
socket.once("connect", () => {
|
|
57
|
+
socket.off("error", reject)
|
|
58
|
+
resolve(undefined)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
test("TLS relay forwards traffic and edge replays after reconnect", async () => {
|
|
64
|
+
const certDir = mkdtempSync(join(tmpdir(), "socketduct-tls-forwarding-"))
|
|
65
|
+
const certs = generateTestCertificates(certDir)
|
|
66
|
+
|
|
67
|
+
const tempDir = await mkdtemp(join(tmpdir(), "socketduct-tls-edge-spool-"))
|
|
68
|
+
const echoServer = createServer((socket) => {
|
|
69
|
+
socket.on("data", (chunk) => socket.write(chunk))
|
|
70
|
+
})
|
|
71
|
+
const targetPort = await listen(echoServer)
|
|
72
|
+
|
|
73
|
+
const users = new InMemoryUserStore()
|
|
74
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
75
|
+
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
76
|
+
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
77
|
+
|
|
78
|
+
const relay = await startTcpRelay({
|
|
79
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
80
|
+
tokens,
|
|
81
|
+
sessions: new SessionStore({now: () => 1000}),
|
|
82
|
+
tls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const tlsSocket = tlsConnect({
|
|
86
|
+
host: "127.0.0.1",
|
|
87
|
+
port: relay.port,
|
|
88
|
+
ca: readFileSync(certs.caCertFile),
|
|
89
|
+
rejectUnauthorized: true
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const decoder = new FrameDecoder({compactDirection: "target-to-client"})
|
|
93
|
+
await waitConnect(tlsSocket)
|
|
94
|
+
|
|
95
|
+
const createdPromise = new Promise((resolve) => {
|
|
96
|
+
tlsSocket.on("data", (chunk) => {
|
|
97
|
+
for (const frame of decoder.push(chunk)) {
|
|
98
|
+
if (frame.header.type === "session.created") {
|
|
99
|
+
resolve(frame.header)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
tlsSocket.write(encodeFrame({
|
|
106
|
+
type: "session.create",
|
|
107
|
+
token: token.value,
|
|
108
|
+
name: "tls-forward-session",
|
|
109
|
+
identifier: "tls-forward-session",
|
|
110
|
+
target: {host: "127.0.0.1", port: targetPort},
|
|
111
|
+
framing: [2]
|
|
112
|
+
}))
|
|
113
|
+
|
|
114
|
+
const created = await createdPromise
|
|
115
|
+
assert.equal(/** @type {{type: string}} */ (created).type, "session.created")
|
|
116
|
+
|
|
117
|
+
const echoPromise = new Promise((resolve) => {
|
|
118
|
+
tlsSocket.on("data", (chunk) => {
|
|
119
|
+
for (const frame of decoder.push(chunk)) {
|
|
120
|
+
if (frame.header.type === "stream.data" && frame.header.direction === "target-to-client") {
|
|
121
|
+
resolve(frame.payload.toString("utf8"))
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
tlsSocket.write(encodeCompactData({sequence: 8, payload: Buffer.from("tls-echo")}))
|
|
128
|
+
|
|
129
|
+
const echoed = await echoPromise
|
|
130
|
+
assert.equal(echoed, "tls-echo")
|
|
131
|
+
|
|
132
|
+
tlsSocket.destroy()
|
|
133
|
+
await relay.close()
|
|
134
|
+
await new Promise((resolve, reject) => {
|
|
135
|
+
echoServer.close((error) => {
|
|
136
|
+
if (error) {
|
|
137
|
+
reject(error)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
resolve(undefined)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
await rm(tempDir, {recursive: true, force: true})
|
|
144
|
+
await rm(certDir, {recursive: true, force: true})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test("TLS relay with mTLS rejects client without valid certificate (handshake completes, server closes)", async () => {
|
|
148
|
+
const certDir = mkdtempSync(join(tmpdir(), "socketduct-mtls-reject-"))
|
|
149
|
+
const certs = generateTestCertificates(certDir)
|
|
150
|
+
|
|
151
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
152
|
+
const relay = await startTcpRelay({
|
|
153
|
+
authorizeTarget: () => true,
|
|
154
|
+
tokens,
|
|
155
|
+
sessions: new SessionStore({now: () => 1000}),
|
|
156
|
+
tls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile, clientCaFile: certs.caCertFile}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const unauthorizedSocket = tlsConnect({
|
|
160
|
+
host: "127.0.0.1",
|
|
161
|
+
port: relay.port,
|
|
162
|
+
ca: readFileSync(certs.caCertFile),
|
|
163
|
+
rejectUnauthorized: false
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
let dataReceived = false
|
|
167
|
+
unauthorizedSocket.on("data", () => { dataReceived = true })
|
|
168
|
+
|
|
169
|
+
await new Promise((resolve) => {
|
|
170
|
+
unauthorizedSocket.once("error", () => resolve("error"))
|
|
171
|
+
unauthorizedSocket.once("close", () => resolve("close"))
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
assert.equal(dataReceived, false, "client without mTLS certificate must not receive data from the relay")
|
|
175
|
+
|
|
176
|
+
unauthorizedSocket.destroy()
|
|
177
|
+
await relay.close()
|
|
178
|
+
await rm(certDir, {recursive: true, force: true})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test("TLS relay with mTLS accepts client with valid certificate", async () => {
|
|
182
|
+
const certDir = mkdtempSync(join(tmpdir(), "socketduct-mtls-accept-"))
|
|
183
|
+
const certs = generateTestCertificates(certDir)
|
|
184
|
+
|
|
185
|
+
const echoServer = createServer((socket) => {
|
|
186
|
+
socket.on("data", (chunk) => socket.write(chunk))
|
|
187
|
+
})
|
|
188
|
+
const targetPort = await listen(echoServer)
|
|
189
|
+
|
|
190
|
+
const users = new InMemoryUserStore()
|
|
191
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
192
|
+
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
193
|
+
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
194
|
+
|
|
195
|
+
const relay = await startTcpRelay({
|
|
196
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
197
|
+
tokens,
|
|
198
|
+
sessions: new SessionStore({now: () => 1000}),
|
|
199
|
+
tls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile, clientCaFile: certs.caCertFile}
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const clientSocket = tlsConnect({
|
|
203
|
+
host: "127.0.0.1",
|
|
204
|
+
port: relay.port,
|
|
205
|
+
ca: readFileSync(certs.caCertFile),
|
|
206
|
+
cert: readFileSync(certs.clientCertFile),
|
|
207
|
+
key: readFileSync(certs.clientKeyFile),
|
|
208
|
+
rejectUnauthorized: true
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
const decoder = new FrameDecoder()
|
|
212
|
+
await waitConnect(clientSocket)
|
|
213
|
+
|
|
214
|
+
const sessionPromise = new Promise((resolve) => {
|
|
215
|
+
clientSocket.on("data", (chunk) => {
|
|
216
|
+
for (const frame of decoder.push(chunk)) {
|
|
217
|
+
if (frame.header.type === "session.created" || frame.header.type === "error") {
|
|
218
|
+
resolve(frame.header)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
clientSocket.write(encodeFrame({
|
|
225
|
+
type: "session.create",
|
|
226
|
+
token: token.value,
|
|
227
|
+
name: "mtls-session",
|
|
228
|
+
target: {host: "127.0.0.1", port: targetPort},
|
|
229
|
+
framing: [2]
|
|
230
|
+
}))
|
|
231
|
+
|
|
232
|
+
const response = await sessionPromise
|
|
233
|
+
assert.equal(/** @type {{type: string}} */ (response).type, "session.created")
|
|
234
|
+
|
|
235
|
+
clientSocket.destroy()
|
|
236
|
+
await relay.close()
|
|
237
|
+
await new Promise((resolve, reject) => {
|
|
238
|
+
echoServer.close((error) => {
|
|
239
|
+
if (error) {
|
|
240
|
+
reject(error)
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
resolve(undefined)
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
await rm(certDir, {recursive: true, force: true})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test("TLS relay with mTLS requires certificate from the configured client CA (handshake completes, server closes)", async () => {
|
|
250
|
+
const certDir = mkdtempSync(join(tmpdir(), "socketduct-mtls-wrong-ca-"))
|
|
251
|
+
const wrongCertDir = mkdtempSync(join(tmpdir(), "socketduct-mtls-wrong-ca-alt-"))
|
|
252
|
+
const certs = generateTestCertificates(certDir)
|
|
253
|
+
const wrongCerts = generateTestCertificates(wrongCertDir)
|
|
254
|
+
|
|
255
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
256
|
+
const relay = await startTcpRelay({
|
|
257
|
+
authorizeTarget: () => true,
|
|
258
|
+
tokens,
|
|
259
|
+
sessions: new SessionStore({now: () => 1000}),
|
|
260
|
+
tls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile, clientCaFile: certs.caCertFile}
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
const clientSocket = tlsConnect({
|
|
264
|
+
host: "127.0.0.1",
|
|
265
|
+
port: relay.port,
|
|
266
|
+
ca: readFileSync(certs.caCertFile),
|
|
267
|
+
cert: readFileSync(wrongCerts.clientCertFile),
|
|
268
|
+
key: readFileSync(wrongCerts.clientKeyFile),
|
|
269
|
+
rejectUnauthorized: false
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
let dataReceived = false
|
|
273
|
+
clientSocket.on("data", () => { dataReceived = true })
|
|
274
|
+
|
|
275
|
+
await new Promise((resolve) => {
|
|
276
|
+
clientSocket.once("error", () => resolve("error"))
|
|
277
|
+
clientSocket.once("close", () => resolve("close"))
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
assert.equal(dataReceived, false, "client with wrong-CA certificate must not receive data from the relay")
|
|
281
|
+
|
|
282
|
+
clientSocket.destroy()
|
|
283
|
+
await relay.close()
|
|
284
|
+
await rm(certDir, {recursive: true, force: true})
|
|
285
|
+
await rm(wrongCertDir, {recursive: true, force: true})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test("Plaintext client is rejected by TLS relay (no plaintext protocol reply)", async () => {
|
|
289
|
+
const certDir = mkdtempSync(join(tmpdir(), "socketduct-wrong-trust-"))
|
|
290
|
+
const certs = generateTestCertificates(certDir)
|
|
291
|
+
|
|
292
|
+
const echoServer = createServer((socket) => {
|
|
293
|
+
socket.on("data", (chunk) => socket.write(chunk))
|
|
294
|
+
})
|
|
295
|
+
const targetPort = await listen(echoServer)
|
|
296
|
+
|
|
297
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
298
|
+
const relay = await startTcpRelay({
|
|
299
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
300
|
+
tokens,
|
|
301
|
+
sessions: new SessionStore({now: () => 1000}),
|
|
302
|
+
tls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile}
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
const plainSocket = createConnection({host: "127.0.0.1", port: relay.port})
|
|
306
|
+
await waitConnect(plainSocket)
|
|
307
|
+
plainSocket.write("GET / HTTP/1.1\r\n\r\n")
|
|
308
|
+
|
|
309
|
+
let dataReceived = false
|
|
310
|
+
plainSocket.on("data", () => { dataReceived = true })
|
|
311
|
+
|
|
312
|
+
await new Promise((resolve) => {
|
|
313
|
+
plainSocket.once("close", () => resolve("close"))
|
|
314
|
+
plainSocket.once("error", () => resolve("error"))
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
assert.equal(dataReceived, false, "plaintext client must not receive any protocol reply from a TLS relay")
|
|
318
|
+
|
|
319
|
+
plainSocket.destroy()
|
|
320
|
+
await relay.close()
|
|
321
|
+
await new Promise((resolve, reject) => {
|
|
322
|
+
echoServer.close((error) => {
|
|
323
|
+
if (error) {
|
|
324
|
+
reject(error)
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
resolve(undefined)
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
await rm(certDir, {recursive: true, force: true})
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
test("Edge with TLS reconnects and replays spooled data after relay tunnel drop", async () => {
|
|
334
|
+
const certDir = mkdtempSync(join(tmpdir(), "socketduct-edge-tls-replay-"))
|
|
335
|
+
const certs = generateTestCertificates(certDir)
|
|
336
|
+
|
|
337
|
+
const tempDir = await mkdtemp(join(tmpdir(), "socketduct-edge-tls-replay-spool-"))
|
|
338
|
+
const echoServer = createServer((socket) => {
|
|
339
|
+
socket.on("data", (chunk) => socket.write(chunk))
|
|
340
|
+
})
|
|
341
|
+
const targetPort = await listen(echoServer)
|
|
342
|
+
|
|
343
|
+
const users = new InMemoryUserStore()
|
|
344
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
345
|
+
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
346
|
+
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
347
|
+
|
|
348
|
+
const relay = await startTcpRelay({
|
|
349
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
350
|
+
tokens,
|
|
351
|
+
sessions: new SessionStore({now: () => 1000}),
|
|
352
|
+
resumeWindowMs: 1_000,
|
|
353
|
+
tls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile}
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
const edge = await startTcpEdge({
|
|
357
|
+
relayHost: "127.0.0.1",
|
|
358
|
+
relayPort: relay.port,
|
|
359
|
+
token: token.value,
|
|
360
|
+
sessionName: "edge-tls-replay",
|
|
361
|
+
spool: new SessionSpool({rootDir: tempDir}),
|
|
362
|
+
reconnectDelayMs: 10,
|
|
363
|
+
target: {host: "127.0.0.1", port: targetPort},
|
|
364
|
+
tls: {caFile: certs.caCertFile, rejectUnauthorized: true}
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
const appSocket = createConnection({host: "127.0.0.1", port: edge.port})
|
|
368
|
+
await waitConnect(appSocket)
|
|
369
|
+
appSocket.write("tls-first")
|
|
370
|
+
assert.equal((await readOnce(appSocket)).toString("utf8"), "tls-first")
|
|
371
|
+
|
|
372
|
+
relay.closeActiveTunnel()
|
|
373
|
+
appSocket.write("tls-second")
|
|
374
|
+
assert.equal((await readOnce(appSocket)).toString("utf8"), "tls-second")
|
|
375
|
+
|
|
376
|
+
appSocket.destroy()
|
|
377
|
+
await edge.close()
|
|
378
|
+
await relay.close()
|
|
379
|
+
await new Promise((resolve, reject) => {
|
|
380
|
+
echoServer.close((error) => {
|
|
381
|
+
if (error) {
|
|
382
|
+
reject(error)
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
resolve(undefined)
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
await rm(tempDir, {recursive: true, force: true})
|
|
389
|
+
await rm(certDir, {recursive: true, force: true})
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
test("SocketductSocket with valid mTLS client cert is accepted by mTLS relay", async () => {
|
|
393
|
+
const certDir = mkdtempSync(join(tmpdir(), "socketduct-mtls-socket-accept-"))
|
|
394
|
+
const certs = generateTestCertificates(certDir)
|
|
395
|
+
|
|
396
|
+
const tempDir = await mkdtemp(join(tmpdir(), "socketduct-mtls-socket-spool-"))
|
|
397
|
+
const echoServer = createServer((socket) => {
|
|
398
|
+
socket.on("data", (chunk) => socket.write(chunk))
|
|
399
|
+
})
|
|
400
|
+
const targetPort = await listen(echoServer)
|
|
401
|
+
|
|
402
|
+
const users = new InMemoryUserStore()
|
|
403
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
404
|
+
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
405
|
+
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
406
|
+
|
|
407
|
+
const relay = await startTcpRelay({
|
|
408
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
409
|
+
tokens,
|
|
410
|
+
sessions: new SessionStore({now: () => 1000}),
|
|
411
|
+
tls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile, clientCaFile: certs.caCertFile}
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
const socket = new SocketductSocket({
|
|
415
|
+
relayHost: "127.0.0.1",
|
|
416
|
+
relayPort: relay.port,
|
|
417
|
+
token: token.value,
|
|
418
|
+
sessionName: "mtls-socket",
|
|
419
|
+
target: {host: "127.0.0.1", port: targetPort},
|
|
420
|
+
spool: new SessionSpool({rootDir: tempDir}),
|
|
421
|
+
reconnectDelayMs: 50,
|
|
422
|
+
tls: {
|
|
423
|
+
caFile: certs.caCertFile,
|
|
424
|
+
certFile: certs.clientCertFile,
|
|
425
|
+
keyFile: certs.clientKeyFile,
|
|
426
|
+
rejectUnauthorized: true
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
socket.connect()
|
|
431
|
+
await new Promise((resolve, reject) => {
|
|
432
|
+
socket.once("error", reject)
|
|
433
|
+
socket.once("connect", () => {
|
|
434
|
+
socket.off("error", reject)
|
|
435
|
+
resolve(undefined)
|
|
436
|
+
})
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
socket.write("mtls-test")
|
|
440
|
+
assert.equal((await readOnce(socket)).toString("utf8"), "mtls-test")
|
|
441
|
+
|
|
442
|
+
socket.destroy()
|
|
443
|
+
await relay.close()
|
|
444
|
+
await new Promise((resolve, reject) => {
|
|
445
|
+
echoServer.close((error) => {
|
|
446
|
+
if (error) {
|
|
447
|
+
reject(error)
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
resolve(undefined)
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
await rm(tempDir, {recursive: true, force: true})
|
|
454
|
+
await rm(certDir, {recursive: true, force: true})
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
test("SocketductSocket without client cert is rejected by mTLS relay (connect never fires)", async () => {
|
|
458
|
+
const certDir = mkdtempSync(join(tmpdir(), "socketduct-mtls-no-cert-"))
|
|
459
|
+
const certs = generateTestCertificates(certDir)
|
|
460
|
+
|
|
461
|
+
const tempDir = await mkdtemp(join(tmpdir(), "socketduct-mtls-no-cert-spool-"))
|
|
462
|
+
const echoServer = createServer((socket) => {
|
|
463
|
+
socket.on("data", (chunk) => socket.write(chunk))
|
|
464
|
+
})
|
|
465
|
+
const targetPort = await listen(echoServer)
|
|
466
|
+
|
|
467
|
+
const users = new InMemoryUserStore()
|
|
468
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
469
|
+
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
470
|
+
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
471
|
+
|
|
472
|
+
const relay = await startTcpRelay({
|
|
473
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
474
|
+
tokens,
|
|
475
|
+
sessions: new SessionStore({now: () => 1000}),
|
|
476
|
+
tls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile, clientCaFile: certs.caCertFile}
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
const socket = new SocketductSocket({
|
|
480
|
+
relayHost: "127.0.0.1",
|
|
481
|
+
relayPort: relay.port,
|
|
482
|
+
token: token.value,
|
|
483
|
+
sessionName: "mtls-no-cert",
|
|
484
|
+
target: {host: "127.0.0.1", port: targetPort},
|
|
485
|
+
spool: new SessionSpool({rootDir: tempDir}),
|
|
486
|
+
reconnectDelayMs: 1000,
|
|
487
|
+
tls: {
|
|
488
|
+
caFile: certs.caCertFile,
|
|
489
|
+
rejectUnauthorized: true
|
|
490
|
+
}
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
socket.connect()
|
|
494
|
+
|
|
495
|
+
let connectFired = false
|
|
496
|
+
let dataReceived = false
|
|
497
|
+
socket.on("connect", () => { connectFired = true })
|
|
498
|
+
socket.on("data", () => { dataReceived = true })
|
|
499
|
+
|
|
500
|
+
const error = await new Promise((resolve) => { socket.once("error", resolve) })
|
|
501
|
+
assert.ok(error instanceof Error, "expected error when mTLS client cert is missing")
|
|
502
|
+
assert.equal(connectFired, false, "connect must not fire when mTLS client cert is missing")
|
|
503
|
+
assert.equal(dataReceived, false, "no data must be exchanged without a valid mTLS client cert")
|
|
504
|
+
|
|
505
|
+
socket.destroy()
|
|
506
|
+
await relay.close()
|
|
507
|
+
await new Promise((resolve, reject) => {
|
|
508
|
+
echoServer.close((error) => {
|
|
509
|
+
if (error) {
|
|
510
|
+
reject(error)
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
resolve(undefined)
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
await rm(tempDir, {recursive: true, force: true})
|
|
517
|
+
await rm(certDir, {recursive: true, force: true})
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
test("SocketductSocket with wrong server CA is rejected during TLS handshake (connect never fires)", async () => {
|
|
521
|
+
const certDir = mkdtempSync(join(tmpdir(), "socketduct-wrong-server-ca-"))
|
|
522
|
+
const wrongCertDir = mkdtempSync(join(tmpdir(), "socketduct-wrong-server-ca-alt-"))
|
|
523
|
+
const certs = generateTestCertificates(certDir)
|
|
524
|
+
const wrongCerts = generateTestCertificates(wrongCertDir)
|
|
525
|
+
|
|
526
|
+
const tempDir = await mkdtemp(join(tmpdir(), "socketduct-wrong-server-ca-spool-"))
|
|
527
|
+
const echoServer = createServer((socket) => {
|
|
528
|
+
socket.on("data", (chunk) => socket.write(chunk))
|
|
529
|
+
})
|
|
530
|
+
const targetPort = await listen(echoServer)
|
|
531
|
+
|
|
532
|
+
const users = new InMemoryUserStore()
|
|
533
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
534
|
+
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
535
|
+
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
536
|
+
|
|
537
|
+
const relay = await startTcpRelay({
|
|
538
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
539
|
+
tokens,
|
|
540
|
+
sessions: new SessionStore({now: () => 1000}),
|
|
541
|
+
tls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile}
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
const socket = connectSocketduct({
|
|
545
|
+
relay: {host: "127.0.0.1", port: relay.port, token: token.value},
|
|
546
|
+
sessionName: "wrong-server-ca",
|
|
547
|
+
target: {host: "127.0.0.1", port: targetPort},
|
|
548
|
+
spoolDirectory: tempDir,
|
|
549
|
+
reconnect: {initialDelayMs: 1000},
|
|
550
|
+
tls: {
|
|
551
|
+
caFile: wrongCerts.caCertFile,
|
|
552
|
+
rejectUnauthorized: true
|
|
553
|
+
}
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
let connectFired = false
|
|
557
|
+
let dataReceived = false
|
|
558
|
+
socket.on("connect", () => { connectFired = true })
|
|
559
|
+
socket.on("data", () => { dataReceived = true })
|
|
560
|
+
|
|
561
|
+
const error = await new Promise((resolve) => { socket.once("error", resolve) })
|
|
562
|
+
assert.ok(error instanceof Error, "expected error when server CA is untrusted")
|
|
563
|
+
assert.equal(connectFired, false, "connect must not fire when server CA is untrusted")
|
|
564
|
+
assert.equal(dataReceived, false, "no data must be exchanged with an untrusted server CA")
|
|
565
|
+
|
|
566
|
+
socket.destroy()
|
|
567
|
+
await relay.close()
|
|
568
|
+
await new Promise((resolve, reject) => {
|
|
569
|
+
echoServer.close((error) => {
|
|
570
|
+
if (error) {
|
|
571
|
+
reject(error)
|
|
572
|
+
return
|
|
573
|
+
}
|
|
574
|
+
resolve(undefined)
|
|
575
|
+
})
|
|
576
|
+
})
|
|
577
|
+
await rm(tempDir, {recursive: true, force: true})
|
|
578
|
+
await rm(certDir, {recursive: true, force: true})
|
|
579
|
+
await rm(wrongCertDir, {recursive: true, force: true})
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
test("TcpEdge with mTLS client cert connects to mTLS relay and forwards data", async () => {
|
|
583
|
+
const certDir = mkdtempSync(join(tmpdir(), "socketduct-tcp-edge-mtls-"))
|
|
584
|
+
const certs = generateTestCertificates(certDir)
|
|
585
|
+
|
|
586
|
+
const tempDir = await mkdtemp(join(tmpdir(), "socketduct-tcp-edge-mtls-spool-"))
|
|
587
|
+
const echoServer = createServer((socket) => {
|
|
588
|
+
socket.on("data", (chunk) => socket.write(chunk))
|
|
589
|
+
})
|
|
590
|
+
const targetPort = await listen(echoServer)
|
|
591
|
+
|
|
592
|
+
const users = new InMemoryUserStore()
|
|
593
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
594
|
+
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
595
|
+
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
596
|
+
|
|
597
|
+
const relay = await startTcpRelay({
|
|
598
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
599
|
+
tokens,
|
|
600
|
+
sessions: new SessionStore({now: () => 1000}),
|
|
601
|
+
tls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile, clientCaFile: certs.caCertFile}
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
const edge = await startTcpEdge({
|
|
605
|
+
relayHost: "127.0.0.1",
|
|
606
|
+
relayPort: relay.port,
|
|
607
|
+
token: token.value,
|
|
608
|
+
sessionName: "tcp-edge-mtls",
|
|
609
|
+
spool: new SessionSpool({rootDir: tempDir}),
|
|
610
|
+
target: {host: "127.0.0.1", port: targetPort},
|
|
611
|
+
reconnectDelayMs: 50,
|
|
612
|
+
tls: {
|
|
613
|
+
caFile: certs.caCertFile,
|
|
614
|
+
certFile: certs.clientCertFile,
|
|
615
|
+
keyFile: certs.clientKeyFile,
|
|
616
|
+
rejectUnauthorized: true
|
|
617
|
+
}
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
const appSocket = createConnection({host: "127.0.0.1", port: edge.port})
|
|
621
|
+
await waitConnect(appSocket)
|
|
622
|
+
appSocket.write("tcp-edge-mtls")
|
|
623
|
+
assert.equal((await readOnce(appSocket)).toString("utf8"), "tcp-edge-mtls")
|
|
624
|
+
|
|
625
|
+
appSocket.destroy()
|
|
626
|
+
await edge.close()
|
|
627
|
+
await relay.close()
|
|
628
|
+
await new Promise((resolve, reject) => {
|
|
629
|
+
echoServer.close((error) => {
|
|
630
|
+
if (error) {
|
|
631
|
+
reject(error)
|
|
632
|
+
return
|
|
633
|
+
}
|
|
634
|
+
resolve(undefined)
|
|
635
|
+
})
|
|
636
|
+
})
|
|
637
|
+
await rm(tempDir, {recursive: true, force: true})
|
|
638
|
+
await rm(certDir, {recursive: true, force: true})
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
test("TLS SocketductSocket reconnects cleanly when relay closes connection after TLS handshake but before session response", async () => {
|
|
642
|
+
const certDir = mkdtempSync(join(tmpdir(), "socketduct-tls-pre-session-close-"))
|
|
643
|
+
const certs = generateTestCertificates(certDir)
|
|
644
|
+
|
|
645
|
+
const tempDir = await mkdtemp(join(tmpdir(), "socketduct-tls-pre-session-close-spool-"))
|
|
646
|
+
const echoServer = createServer((socket) => {
|
|
647
|
+
socket.on("data", (chunk) => socket.write(chunk))
|
|
648
|
+
})
|
|
649
|
+
const targetPort = await listen(echoServer)
|
|
650
|
+
|
|
651
|
+
const users = new InMemoryUserStore()
|
|
652
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
653
|
+
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
654
|
+
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
655
|
+
|
|
656
|
+
const relay = await startTcpRelay({
|
|
657
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
658
|
+
tokens,
|
|
659
|
+
sessions: new SessionStore({now: () => 1000}),
|
|
660
|
+
tls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile}
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
const socket = connectSocketduct({
|
|
664
|
+
relay: {host: "127.0.0.1", port: relay.port, token: token.value},
|
|
665
|
+
sessionName: "tls-pre-session-close",
|
|
666
|
+
target: {host: "127.0.0.1", port: targetPort},
|
|
667
|
+
spoolDirectory: tempDir,
|
|
668
|
+
reconnect: {initialDelayMs: 25},
|
|
669
|
+
tls: {
|
|
670
|
+
caFile: certs.caCertFile,
|
|
671
|
+
rejectUnauthorized: true
|
|
672
|
+
}
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
let errorEmitted = false
|
|
676
|
+
socket.on("error", () => { errorEmitted = true })
|
|
677
|
+
|
|
678
|
+
let connectCount = 0
|
|
679
|
+
socket.on("connect", () => { connectCount += 1 })
|
|
680
|
+
|
|
681
|
+
// Force-close the relay tunnel after the TLS handshake completes
|
|
682
|
+
// but before any protocol frame is exchanged. The relay's
|
|
683
|
+
// secureConnection event fires synchronously inside setupTunnel
|
|
684
|
+
// — which adds the socket to the tunnel set — so closeActiveTunnel
|
|
685
|
+
// destroys it before any data event can process session.create.
|
|
686
|
+
// This proves the vulnerable pre-session branch: the first TLS
|
|
687
|
+
// connection reaches secureConnect yet is closed before the client
|
|
688
|
+
// receives session.created / session.resumed.
|
|
689
|
+
socket.connect()
|
|
690
|
+
await new Promise((resolve) => {
|
|
691
|
+
relay.server.once("secureConnection", () => {
|
|
692
|
+
relay.closeActiveTunnel()
|
|
693
|
+
resolve(undefined)
|
|
694
|
+
})
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
assert.equal(socket.pending, true, "socket must still be pending after pre-session forced close")
|
|
698
|
+
|
|
699
|
+
// The socket must not be destroyed with an error; it must reconnect
|
|
700
|
+
// and deliver data on the later connection. "connect" fires only on
|
|
701
|
+
// the first successful session creation; reconnects emit "ready".
|
|
702
|
+
await new Promise((resolve, reject) => {
|
|
703
|
+
const timeout = setTimeout(() => reject(new Error("Timed out waiting for TLS reconnect")), 5000)
|
|
704
|
+
const onConnect = () => {
|
|
705
|
+
clearTimeout(timeout)
|
|
706
|
+
cleanup()
|
|
707
|
+
resolve(undefined)
|
|
708
|
+
}
|
|
709
|
+
const onReady = () => {
|
|
710
|
+
clearTimeout(timeout)
|
|
711
|
+
cleanup()
|
|
712
|
+
resolve(undefined)
|
|
713
|
+
}
|
|
714
|
+
const onError = (/** @type {Error} */ err) => {
|
|
715
|
+
clearTimeout(timeout)
|
|
716
|
+
cleanup()
|
|
717
|
+
reject(err)
|
|
718
|
+
}
|
|
719
|
+
const cleanup = () => {
|
|
720
|
+
socket.off("connect", onConnect)
|
|
721
|
+
socket.off("ready", onReady)
|
|
722
|
+
socket.off("error", onError)
|
|
723
|
+
}
|
|
724
|
+
socket.on("connect", onConnect)
|
|
725
|
+
socket.on("ready", onReady)
|
|
726
|
+
socket.on("error", onError)
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
assert.equal(errorEmitted, false, "no error must emit when TLS connection closes before session response")
|
|
730
|
+
assert.ok(connectCount >= 1, "connect must have fired at least once")
|
|
731
|
+
|
|
732
|
+
socket.write("tls-pre-session-reconnect")
|
|
733
|
+
assert.equal((await readOnce(socket)).toString("utf8"), "tls-pre-session-reconnect")
|
|
734
|
+
|
|
735
|
+
socket.destroy()
|
|
736
|
+
await relay.close()
|
|
737
|
+
await new Promise((resolve, reject) => {
|
|
738
|
+
echoServer.close((error) => {
|
|
739
|
+
if (error) {
|
|
740
|
+
reject(error)
|
|
741
|
+
return
|
|
742
|
+
}
|
|
743
|
+
resolve(undefined)
|
|
744
|
+
})
|
|
745
|
+
})
|
|
746
|
+
await rm(tempDir, {recursive: true, force: true})
|
|
747
|
+
await rm(certDir, {recursive: true, force: true})
|
|
748
|
+
})
|