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,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {describe, expect, it} from "@velocious/testing"
|
|
2
2
|
import {connect as tlsConnect} from "node:tls"
|
|
3
3
|
import {createConnection, createServer} from "node:net"
|
|
4
4
|
import {mkdtemp, rm} from "node:fs/promises"
|
|
@@ -6,7 +6,6 @@ import {mkdtempSync} from "node:fs"
|
|
|
6
6
|
import {tmpdir} from "node:os"
|
|
7
7
|
import {join} from "node:path"
|
|
8
8
|
import {readFileSync} from "node:fs"
|
|
9
|
-
import test from "node:test"
|
|
10
9
|
|
|
11
10
|
import {InMemoryUserStore, login, TokenStore} from "../../src/auth/user-store.js"
|
|
12
11
|
import {SocketductSocket, connectSocketduct} from "../../src/client/socketduct-socket.js"
|
|
@@ -60,689 +59,691 @@ async function waitConnect(socket) {
|
|
|
60
59
|
})
|
|
61
60
|
}
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
describe("tls forwarding", () => {
|
|
63
|
+
it("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
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
const tlsSocket = tlsConnect({
|
|
86
|
+
host: "127.0.0.1",
|
|
87
|
+
port: relay.port,
|
|
88
|
+
ca: readFileSync(certs.caCertFile),
|
|
89
|
+
rejectUnauthorized: true
|
|
90
|
+
})
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
const decoder = new FrameDecoder({compactDirection: "target-to-client"})
|
|
93
|
+
await waitConnect(tlsSocket)
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
}
|
|
100
101
|
}
|
|
101
|
-
}
|
|
102
|
+
})
|
|
102
103
|
})
|
|
103
|
-
})
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
expect(/** @type {{type: string}} */ (created).type).toBe("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
|
+
}
|
|
122
123
|
}
|
|
123
|
-
}
|
|
124
|
+
})
|
|
124
125
|
})
|
|
125
|
-
})
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
tlsSocket.write(encodeCompactData({sequence: 8, payload: Buffer.from("tls-echo")}))
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
const echoed = await echoPromise
|
|
130
|
+
expect(echoed).toBe("tls-echo")
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
})
|
|
141
142
|
})
|
|
142
|
-
|
|
143
|
-
|
|
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}
|
|
143
|
+
await rm(tempDir, {recursive: true, force: true})
|
|
144
|
+
await rm(certDir, {recursive: true, force: true})
|
|
157
145
|
})
|
|
158
146
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
ca: readFileSync(certs.caCertFile),
|
|
163
|
-
rejectUnauthorized: false
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
let dataReceived = false
|
|
167
|
-
unauthorizedSocket.on("data", () => { dataReceived = true })
|
|
147
|
+
it("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)
|
|
168
150
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
})
|
|
173
158
|
|
|
174
|
-
|
|
159
|
+
const unauthorizedSocket = tlsConnect({
|
|
160
|
+
host: "127.0.0.1",
|
|
161
|
+
port: relay.port,
|
|
162
|
+
ca: readFileSync(certs.caCertFile),
|
|
163
|
+
rejectUnauthorized: false
|
|
164
|
+
})
|
|
175
165
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
await rm(certDir, {recursive: true, force: true})
|
|
179
|
-
})
|
|
166
|
+
let dataReceived = false
|
|
167
|
+
unauthorizedSocket.on("data", () => { dataReceived = true })
|
|
180
168
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
169
|
+
await new Promise((resolve) => {
|
|
170
|
+
unauthorizedSocket.once("error", () => resolve("error"))
|
|
171
|
+
unauthorizedSocket.once("close", () => resolve("close"))
|
|
172
|
+
})
|
|
184
173
|
|
|
185
|
-
|
|
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
|
-
})
|
|
174
|
+
expect(dataReceived).toBe(false)
|
|
201
175
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
ca: readFileSync(certs.caCertFile),
|
|
206
|
-
cert: readFileSync(certs.clientCertFile),
|
|
207
|
-
key: readFileSync(certs.clientKeyFile),
|
|
208
|
-
rejectUnauthorized: true
|
|
176
|
+
unauthorizedSocket.destroy()
|
|
177
|
+
await relay.close()
|
|
178
|
+
await rm(certDir, {recursive: true, force: true})
|
|
209
179
|
})
|
|
210
180
|
|
|
211
|
-
|
|
212
|
-
|
|
181
|
+
it("TLS relay with mTLS accepts client with valid certificate", async () => {
|
|
182
|
+
const certDir = mkdtempSync(join(tmpdir(), "socketduct-mtls-accept-"))
|
|
183
|
+
const certs = generateTestCertificates(certDir)
|
|
213
184
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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}
|
|
221
200
|
})
|
|
222
|
-
})
|
|
223
201
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
+
})
|
|
231
210
|
|
|
232
|
-
|
|
233
|
-
|
|
211
|
+
const decoder = new FrameDecoder()
|
|
212
|
+
await waitConnect(clientSocket)
|
|
234
213
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
resolve(undefined)
|
|
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
|
+
})
|
|
244
222
|
})
|
|
245
|
-
})
|
|
246
|
-
await rm(certDir, {recursive: true, force: true})
|
|
247
|
-
})
|
|
248
223
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
+
expect(/** @type {{type: string}} */ (response).type).toBe("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})
|
|
261
247
|
})
|
|
262
248
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
key: readFileSync(wrongCerts.clientKeyFile),
|
|
269
|
-
rejectUnauthorized: false
|
|
270
|
-
})
|
|
249
|
+
it("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)
|
|
271
254
|
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
})
|
|
274
262
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
+
})
|
|
279
271
|
|
|
280
|
-
|
|
272
|
+
let dataReceived = false
|
|
273
|
+
clientSocket.on("data", () => { dataReceived = true })
|
|
281
274
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
})
|
|
275
|
+
await new Promise((resolve) => {
|
|
276
|
+
clientSocket.once("error", () => resolve("error"))
|
|
277
|
+
clientSocket.once("close", () => resolve("close"))
|
|
278
|
+
})
|
|
287
279
|
|
|
288
|
-
|
|
289
|
-
const certDir = mkdtempSync(join(tmpdir(), "socketduct-wrong-trust-"))
|
|
290
|
-
const certs = generateTestCertificates(certDir)
|
|
280
|
+
expect(dataReceived).toBe(false)
|
|
291
281
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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}
|
|
282
|
+
clientSocket.destroy()
|
|
283
|
+
await relay.close()
|
|
284
|
+
await rm(certDir, {recursive: true, force: true})
|
|
285
|
+
await rm(wrongCertDir, {recursive: true, force: true})
|
|
303
286
|
})
|
|
304
287
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
288
|
+
it("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)
|
|
308
291
|
|
|
309
|
-
|
|
310
|
-
|
|
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
|
+
})
|
|
311
304
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
plainSocket.
|
|
315
|
-
})
|
|
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")
|
|
316
308
|
|
|
317
|
-
|
|
309
|
+
let dataReceived = false
|
|
310
|
+
plainSocket.on("data", () => { dataReceived = true })
|
|
318
311
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
echoServer.close((error) => {
|
|
323
|
-
if (error) {
|
|
324
|
-
reject(error)
|
|
325
|
-
return
|
|
326
|
-
}
|
|
327
|
-
resolve(undefined)
|
|
312
|
+
await new Promise((resolve) => {
|
|
313
|
+
plainSocket.once("close", () => resolve("close"))
|
|
314
|
+
plainSocket.once("error", () => resolve("error"))
|
|
328
315
|
})
|
|
329
|
-
})
|
|
330
|
-
await rm(certDir, {recursive: true, force: true})
|
|
331
|
-
})
|
|
332
316
|
|
|
333
|
-
|
|
334
|
-
const certDir = mkdtempSync(join(tmpdir(), "socketduct-edge-tls-replay-"))
|
|
335
|
-
const certs = generateTestCertificates(certDir)
|
|
317
|
+
expect(dataReceived).toBe(false)
|
|
336
318
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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}
|
|
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})
|
|
365
331
|
})
|
|
366
332
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
assert.equal((await readOnce(appSocket)).toString("utf8"), "tls-first")
|
|
333
|
+
it("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)
|
|
371
336
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
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}
|
|
386
354
|
})
|
|
387
|
-
})
|
|
388
|
-
await rm(tempDir, {recursive: true, force: true})
|
|
389
|
-
await rm(certDir, {recursive: true, force: true})
|
|
390
|
-
})
|
|
391
355
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
+
})
|
|
395
366
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
367
|
+
const appSocket = createConnection({host: "127.0.0.1", port: edge.port})
|
|
368
|
+
await waitConnect(appSocket)
|
|
369
|
+
appSocket.write("tls-first")
|
|
370
|
+
expect((await readOnce(appSocket)).toString("utf8")).toBe("tls-first")
|
|
371
|
+
|
|
372
|
+
relay.closeActiveTunnel()
|
|
373
|
+
appSocket.write("tls-second")
|
|
374
|
+
expect((await readOnce(appSocket)).toString("utf8")).toBe("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})
|
|
412
390
|
})
|
|
413
391
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
})
|
|
392
|
+
it("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)
|
|
429
395
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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}
|
|
436
412
|
})
|
|
437
|
-
})
|
|
438
|
-
|
|
439
|
-
socket.write("mtls-test")
|
|
440
|
-
assert.equal((await readOnce(socket)).toString("utf8"), "mtls-test")
|
|
441
413
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
|
449
427
|
}
|
|
450
|
-
resolve(undefined)
|
|
451
428
|
})
|
|
452
|
-
})
|
|
453
|
-
await rm(tempDir, {recursive: true, force: true})
|
|
454
|
-
await rm(certDir, {recursive: true, force: true})
|
|
455
|
-
})
|
|
456
429
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
+
})
|
|
460
438
|
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
})
|
|
439
|
+
socket.write("mtls-test")
|
|
440
|
+
expect((await readOnce(socket)).toString("utf8")).toBe("mtls-test")
|
|
478
441
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
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})
|
|
491
455
|
})
|
|
492
456
|
|
|
493
|
-
|
|
457
|
+
it("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)
|
|
494
460
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
+
})
|
|
504
478
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|
512
490
|
}
|
|
513
|
-
resolve(undefined)
|
|
514
491
|
})
|
|
515
|
-
})
|
|
516
|
-
await rm(tempDir, {recursive: true, force: true})
|
|
517
|
-
await rm(certDir, {recursive: true, force: true})
|
|
518
|
-
})
|
|
519
492
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
}
|
|
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
|
+
expect(error instanceof Error).toBeTruthy()
|
|
502
|
+
expect(connectFired).toBe(false)
|
|
503
|
+
expect(dataReceived).toBe(false)
|
|
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})
|
|
554
518
|
})
|
|
555
519
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
520
|
+
it("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)
|
|
560
525
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
+
})
|
|
565
543
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
|
573
553
|
}
|
|
574
|
-
resolve(undefined)
|
|
575
554
|
})
|
|
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
555
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
socket.on("
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
+
expect(error instanceof Error).toBeTruthy()
|
|
563
|
+
expect(connectFired).toBe(false)
|
|
564
|
+
expect(dataReceived).toBe(false)
|
|
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})
|
|
602
580
|
})
|
|
603
581
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
})
|
|
582
|
+
it("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)
|
|
619
585
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
+
})
|
|
624
603
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
|
633
617
|
}
|
|
634
|
-
resolve(undefined)
|
|
635
618
|
})
|
|
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
619
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
620
|
+
const appSocket = createConnection({host: "127.0.0.1", port: edge.port})
|
|
621
|
+
await waitConnect(appSocket)
|
|
622
|
+
appSocket.write("tcp-edge-mtls")
|
|
623
|
+
expect((await readOnce(appSocket)).toString("utf8")).toBe("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})
|
|
661
639
|
})
|
|
662
640
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
-
})
|
|
641
|
+
it("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)
|
|
674
644
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
relay.server.once("secureConnection", () => {
|
|
692
|
-
relay.closeActiveTunnel()
|
|
693
|
-
resolve(undefined)
|
|
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}
|
|
694
661
|
})
|
|
695
|
-
})
|
|
696
662
|
|
|
697
|
-
|
|
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
|
+
})
|
|
698
674
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
+
expect(socket.pending).toBe(true)
|
|
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
728
|
|
|
729
|
-
|
|
730
|
-
|
|
729
|
+
expect(errorEmitted).toBe(false)
|
|
730
|
+
expect(connectCount).toBeGreaterThanOrEqual(1)
|
|
731
731
|
|
|
732
|
-
|
|
733
|
-
|
|
732
|
+
socket.write("tls-pre-session-reconnect")
|
|
733
|
+
expect((await readOnce(socket)).toString("utf8")).toBe("tls-pre-session-reconnect")
|
|
734
734
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
+
})
|
|
744
745
|
})
|
|
746
|
+
await rm(tempDir, {recursive: true, force: true})
|
|
747
|
+
await rm(certDir, {recursive: true, force: true})
|
|
745
748
|
})
|
|
746
|
-
await rm(tempDir, {recursive: true, force: true})
|
|
747
|
-
await rm(certDir, {recursive: true, force: true})
|
|
748
749
|
})
|