socketduct 0.1.19 → 0.1.20
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/.env.example +17 -5
- package/README.md +3 -3
- package/changelog.d/20260925170000-disconnect-attribution-compressed-logs.md +1 -0
- package/docs/docker.md +5 -4
- package/docs/logging.md +131 -159
- package/docs/protocol.md +27 -4
- package/docs/reverse-gateway.md +34 -0
- package/docs/running.md +14 -11
- package/docs/security.md +7 -3
- package/package.json +5 -1
- package/scripts/verify-zstd-interoperability.mjs +41 -0
- package/src/cli/socketduct.js +143 -0
- package/src/client/virtual-socket.js +10 -0
- package/src/config/environment.js +56 -7
- package/src/index.js +64 -26
- package/src/logging/compressed-log-reader.js +256 -0
- package/src/logging/compressed-log-worker.js +430 -0
- package/src/logging/compressed-log-writer.js +538 -0
- package/src/logging/log-batch.js +72 -0
- package/src/logging/log-retention.js +241 -0
- package/src/logging/log-segment-index.js +176 -0
- package/src/logging/structured-logger.js +136 -30
- package/src/logging/zstd-frame-codec.js +73 -0
- package/src/reverse/client.js +71 -11
- package/src/reverse/disconnect-attribution.js +191 -0
- package/src/reverse/gateway.js +58 -8
- package/src/reverse/hub.js +215 -32
- package/src/reverse/runtime.js +5 -3
- package/tensorbuzz.yml +2 -1
- package/test/e2e/logger-shutdown-process.test.js +86 -0
- package/test/e2e/reverse-gateway-process.test.js +15 -14
- package/test/integration/logging-cli.test.js +168 -0
- package/test/integration/logging-compressed-recovery.test.js +186 -0
- package/test/integration/reverse-gateway.test.js +176 -0
- package/test/integration/reverse-route-class.test.js +6 -1
- package/test/unit/config/docker-gateway-files.test.js +2 -0
- package/test/unit/config/environment.test.js +54 -14
- package/test/unit/index.test.js +3 -0
- package/test/unit/logging/compressed-log-worker.test.js +29 -0
- package/test/unit/logging/compressed-log-writer.test.js +183 -0
- package/test/unit/logging/log-retention.test.js +86 -0
- package/test/unit/logging/log-segment-index.test.js +104 -0
- package/test/unit/logging/structured-logger.test.js +14 -1
- package/test/unit/logging/zstd-frame-codec.test.js +24 -0
- package/test/unit/reverse/disconnect-attribution.test.js +82 -0
package/src/reverse/hub.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {readFileSync} from "node:fs"
|
|
3
3
|
import {createServer as createTlsServer} from "node:tls"
|
|
4
4
|
|
|
5
|
+
import {errorDiagnostics, logDiagnostic} from "../logging/structured-logger.js"
|
|
5
6
|
import {availableCodecs, negotiateCodec, zstdAvailable} from "../protocol/compression.js"
|
|
6
7
|
import {decodeV3Data, encodeV3Data, AdaptiveCompressor} from "../protocol/v3-compression.js"
|
|
7
8
|
import {decodeV3Control, encodeV3Control, encodeV3Frame, V3FrameDecoder, V3FrameType} from "../protocol/v3-frame.js"
|
|
@@ -22,6 +23,15 @@ import {
|
|
|
22
23
|
STABLE_IDENTIFIER,
|
|
23
24
|
writeSocket
|
|
24
25
|
} from "./common.js"
|
|
26
|
+
import {
|
|
27
|
+
connectionFingerprint,
|
|
28
|
+
createDisconnectAttribution,
|
|
29
|
+
disconnectReasonForCode,
|
|
30
|
+
parseDisconnectAttribution,
|
|
31
|
+
safeSocketCode,
|
|
32
|
+
sanitizePeerControlDetail,
|
|
33
|
+
validateFingerprintKeyEpoch
|
|
34
|
+
} from "./disconnect-attribution.js"
|
|
25
35
|
|
|
26
36
|
const DEFAULT_OUTPUT_BUFFER_BYTES = 32 * 1024 * 1024
|
|
27
37
|
const DEFAULT_RESUME_MS = 60_000
|
|
@@ -52,6 +62,8 @@ const MANIFEST_PAGE_STREAMS = 64
|
|
|
52
62
|
* @property {number} [maxOutputBytesPerConnection] - Per-physical queued hub output bound.
|
|
53
63
|
* @property {number} [resumeWindowMs] - Detached reverse-stream retention window.
|
|
54
64
|
* @property {import('../protocol/compression.js').CompressionLimits} [compressionLimits] - zstd safety bounds.
|
|
65
|
+
* @property {import('../logging/structured-logger.js').DiagnosticLogger} [logger] - Optional privacy-safe structured logger.
|
|
66
|
+
* @property {{key: Buffer | Uint8Array, keyEpoch: string}} [connectionFingerprint] - Optional hub-only correlation key and public epoch.
|
|
55
67
|
* @typedef {object} HubPhysical
|
|
56
68
|
* @property {import('node:tls').TLSSocket} socket
|
|
57
69
|
* @property {Principal | undefined} principal
|
|
@@ -78,6 +90,10 @@ const MANIFEST_PAGE_STREAMS = 64
|
|
|
78
90
|
* @property {HubOutputItem[]} outputQueue
|
|
79
91
|
* @property {number} outputBytes
|
|
80
92
|
* @property {boolean} outputWriting
|
|
93
|
+
* @property {import('./disconnect-attribution.js').DisconnectReason | undefined} firstDisconnectReason
|
|
94
|
+
* @property {import('./disconnect-attribution.js').DisconnectAttribution | undefined} disconnectAttribution
|
|
95
|
+
* @property {string | undefined} connectionFingerprint
|
|
96
|
+
* @property {string | undefined} safeSocketCode
|
|
81
97
|
* @typedef {object} HubOutputItem
|
|
82
98
|
* @property {Buffer} bytes
|
|
83
99
|
* @property {(delivered: boolean) => void} resolve
|
|
@@ -119,6 +135,7 @@ const MANIFEST_PAGE_STREAMS = 64
|
|
|
119
135
|
* @property {{generation: number, clientToGatewaySequence: number, clientToGatewayAck: number, gatewayToClientSequence: number, gatewayToClientAck: number, localEnded: boolean, remoteEnded: boolean} | undefined} clientResume
|
|
120
136
|
* @property {{generation: number, clientToGatewaySequence: number, clientToGatewayAck: number, gatewayToClientSequence: number, gatewayToClientAck: number, localEnded: boolean, remoteEnded: boolean} | undefined} gatewayResume
|
|
121
137
|
* @property {string | undefined} reconciledResume
|
|
138
|
+
* @property {import('./disconnect-attribution.js').DisconnectAttribution | undefined} disconnectAttribution
|
|
122
139
|
*/
|
|
123
140
|
|
|
124
141
|
/** Starts the Server3 reverse hub. */
|
|
@@ -141,6 +158,8 @@ export async function startReverseGatewayHub(/** @type {ReverseGatewayHubOptions
|
|
|
141
158
|
const tcpSockets = new Set()
|
|
142
159
|
/** @type {Map<string, {socket: import('node:net').Socket, timer: ReturnType<typeof setTimeout>}>} */
|
|
143
160
|
const pendingTls = new Map()
|
|
161
|
+
/** @type {Map<string, {fanOutCount: number, resumeSuccessCount: number, terminalStreamCount: number}>} */
|
|
162
|
+
const incidents = new Map()
|
|
144
163
|
let bridges = 0
|
|
145
164
|
let physicalAccepts = 0
|
|
146
165
|
let gatewayRegistrations = 0
|
|
@@ -209,7 +228,11 @@ export async function startReverseGatewayHub(/** @type {ReverseGatewayHubOptions
|
|
|
209
228
|
clearHandshake: undefined,
|
|
210
229
|
outputQueue: [],
|
|
211
230
|
outputBytes: 0,
|
|
212
|
-
outputWriting: false
|
|
231
|
+
outputWriting: false,
|
|
232
|
+
firstDisconnectReason: undefined,
|
|
233
|
+
disconnectAttribution: undefined,
|
|
234
|
+
connectionFingerprint: undefined,
|
|
235
|
+
safeSocketCode: undefined
|
|
213
236
|
}
|
|
214
237
|
physicals.add(physical)
|
|
215
238
|
const decoder = new V3FrameDecoder({maxFrameBytes: Math.max(MAX_CONTROL_BYTES, maxFrameBytes + 4), maxBufferedBytes: Math.max(MAX_CONTROL_BYTES, maxFrameBytes + 4) * 2})
|
|
@@ -232,7 +255,11 @@ export async function startReverseGatewayHub(/** @type {ReverseGatewayHubOptions
|
|
|
232
255
|
if (!socket.destroyed && !physical.closing) socket.resume()
|
|
233
256
|
})
|
|
234
257
|
})
|
|
235
|
-
socket.
|
|
258
|
+
socket.once("end", () => { physical.firstDisconnectReason ??= "remote_eof" })
|
|
259
|
+
socket.on("error", (error) => {
|
|
260
|
+
physical.firstDisconnectReason ??= "remote_socket_error"
|
|
261
|
+
physical.safeSocketCode ??= safeSocketCode(error)
|
|
262
|
+
})
|
|
236
263
|
socket.once("close", () => {
|
|
237
264
|
clearTimeout(handshakeTimer)
|
|
238
265
|
void queue.finally(() => detachPhysical(physical, physical.detachCode ?? "TRANSPORT_CLOSED"))
|
|
@@ -257,6 +284,7 @@ export async function startReverseGatewayHub(/** @type {ReverseGatewayHubOptions
|
|
|
257
284
|
physical.principal = principal
|
|
258
285
|
physical.connectionId = auth.connectionId
|
|
259
286
|
physical.generation = /** @type {number} */ (auth.generation)
|
|
287
|
+
if (options.connectionFingerprint !== undefined) physical.connectionFingerprint = connectionFingerprint(options.connectionFingerprint.key, physical.role, physical.connectionId)
|
|
260
288
|
physical.codec = negotiateCodec(auth.compression, availableCodecs())
|
|
261
289
|
if (auth.role === "client") authenticateClientOwner(physical, maxIdentities)
|
|
262
290
|
physical.authenticated = true
|
|
@@ -277,6 +305,15 @@ export async function startReverseGatewayHub(/** @type {ReverseGatewayHubOptions
|
|
|
277
305
|
}
|
|
278
306
|
if (frame.type === V3FrameType.PONG) return
|
|
279
307
|
if (frame.type === V3FrameType.GOAWAY) {
|
|
308
|
+
const detail = decodeV3Control(frame.payload)
|
|
309
|
+
const sanitized = sanitizePeerControlDetail(detail)
|
|
310
|
+
if (sanitized.reservedFieldsStripped > 0) logDiagnostic(options.logger, "reverse_hub.reserved_control_field_stripped", {
|
|
311
|
+
physicalRole: physical.role,
|
|
312
|
+
...(physical.role === "client" ? {clientConnectionGeneration: physical.generation} : {gatewayConnectionGeneration: physical.generation}),
|
|
313
|
+
reservedFieldsStripped: sanitized.reservedFieldsStripped
|
|
314
|
+
})
|
|
315
|
+
physical.firstDisconnectReason ??= "peer_goaway"
|
|
316
|
+
if (typeof sanitized.detail.code === "string") physical.detachCode ??= sanitized.detail.code
|
|
280
317
|
physical.retainOnDetach = false
|
|
281
318
|
physical.socket.end()
|
|
282
319
|
return
|
|
@@ -447,7 +484,7 @@ export async function startReverseGatewayHub(/** @type {ReverseGatewayHubOptions
|
|
|
447
484
|
continue
|
|
448
485
|
}
|
|
449
486
|
if (bridge.identifier !== item.identifier) {
|
|
450
|
-
closeBridge(bridge, "RESUME_IDENTITY_MISMATCH", physical)
|
|
487
|
+
await closeBridge(bridge, "RESUME_IDENTITY_MISMATCH", physical)
|
|
451
488
|
physical.resumeEntries.push({streamId: item.streamId, bridge: undefined, code: "RESUME_IDENTITY_MISMATCH"})
|
|
452
489
|
continue
|
|
453
490
|
}
|
|
@@ -464,7 +501,7 @@ export async function startReverseGatewayHub(/** @type {ReverseGatewayHubOptions
|
|
|
464
501
|
}
|
|
465
502
|
for (const bridge of [...physical.streams.values()]) {
|
|
466
503
|
const streamId = physical.role === "client" ? bridge.clientStreamId : bridge.gatewayStreamId
|
|
467
|
-
if (!presented.has(streamId)) closeBridge(bridge, "RESUME_STREAM_OMITTED", physical)
|
|
504
|
+
if (!presented.has(streamId)) await closeBridge(bridge, "RESUME_STREAM_OMITTED", physical)
|
|
468
505
|
}
|
|
469
506
|
physical.resumeItems = []
|
|
470
507
|
physical.resumeFinal = true
|
|
@@ -507,7 +544,7 @@ export async function startReverseGatewayHub(/** @type {ReverseGatewayHubOptions
|
|
|
507
544
|
try {
|
|
508
545
|
reconcileBridge(bridge)
|
|
509
546
|
} catch {
|
|
510
|
-
closeBridge(bridge, "RESUME_STATE_MISMATCH")
|
|
547
|
+
await closeBridge(bridge, "RESUME_STATE_MISMATCH")
|
|
511
548
|
results.push({streamId: entry.streamId, status: "reset", code: "RESUME_STATE_MISMATCH"})
|
|
512
549
|
continue
|
|
513
550
|
}
|
|
@@ -575,6 +612,18 @@ export async function startReverseGatewayHub(/** @type {ReverseGatewayHubOptions
|
|
|
575
612
|
bridge.clientToGatewayCompressor = new AdaptiveCompressor()
|
|
576
613
|
bridge.gatewayToClientCompressor = new AdaptiveCompressor()
|
|
577
614
|
bridge.reconciledResume = key
|
|
615
|
+
if (bridge.disconnectAttribution !== undefined) {
|
|
616
|
+
const attribution = bridge.disconnectAttribution
|
|
617
|
+
const incident = incidentState(attribution)
|
|
618
|
+
incident.resumeSuccessCount += 1
|
|
619
|
+
logDiagnostic(options.logger, "reverse_hub.streams_resumed", {
|
|
620
|
+
...currentIncidentAttribution(attribution),
|
|
621
|
+
gatewayId: bridge.gatewayOwner.gatewayId,
|
|
622
|
+
...(bridge.client.routeClass === undefined ? {} : {routeClass: bridge.client.routeClass})
|
|
623
|
+
})
|
|
624
|
+
bridge.disconnectAttribution = undefined
|
|
625
|
+
releaseCompletedIncident(attribution)
|
|
626
|
+
}
|
|
578
627
|
}
|
|
579
628
|
|
|
580
629
|
/** @param {Record<string, unknown>} value - Resume manifest entry. */
|
|
@@ -668,7 +717,8 @@ export async function startReverseGatewayHub(/** @type {ReverseGatewayHubOptions
|
|
|
668
717
|
gatewayToClientCompressor: new AdaptiveCompressor(),
|
|
669
718
|
clientResume: undefined,
|
|
670
719
|
gatewayResume: undefined,
|
|
671
|
-
reconciledResume: undefined
|
|
720
|
+
reconciledResume: undefined,
|
|
721
|
+
disconnectAttribution: undefined
|
|
672
722
|
}
|
|
673
723
|
clientOwner.streams.set(frame.streamId, bridge)
|
|
674
724
|
gatewayOwner.streams.set(gatewayStreamId, bridge)
|
|
@@ -711,7 +761,7 @@ export async function startReverseGatewayHub(/** @type {ReverseGatewayHubOptions
|
|
|
711
761
|
}
|
|
712
762
|
if (bridge.state === "closing") {
|
|
713
763
|
if (frame.type === V3FrameType.DATA || frame.type === V3FrameType.END) {
|
|
714
|
-
closeBridge(bridge, "STREAM_CLOSING")
|
|
764
|
+
await closeBridge(bridge, "STREAM_CLOSING")
|
|
715
765
|
return
|
|
716
766
|
}
|
|
717
767
|
if (frame.type !== V3FrameType.CLOSE && frame.type !== V3FrameType.RESET) throw protocolError("STREAM_CLOSING")
|
|
@@ -742,10 +792,32 @@ export async function startReverseGatewayHub(/** @type {ReverseGatewayHubOptions
|
|
|
742
792
|
if (frame.type === V3FrameType.CLOSE || frame.type === V3FrameType.RESET) {
|
|
743
793
|
const expected = fromClient ? bridge.clientToGatewaySequence : bridge.gatewayToClientSequence
|
|
744
794
|
if ((frame.type === V3FrameType.CLOSE && frame.sequence !== expected) || (frame.type === V3FrameType.RESET && frame.sequence !== 0)) throw protocolError("INVALID_STREAM_CLOSE")
|
|
745
|
-
const
|
|
795
|
+
const peerDetail = frame.payload.length === 0 ? {} : decodeV3Control(frame.payload)
|
|
796
|
+
const sanitized = sanitizePeerControlDetail(peerDetail)
|
|
797
|
+
if (sanitized.reservedFieldsStripped > 0) logDiagnostic(options.logger, "reverse_hub.reserved_control_field_stripped", {
|
|
798
|
+
physicalRole: source.role,
|
|
799
|
+
...(source.role === "client" ? {clientConnectionGeneration: source.generation} : {gatewayConnectionGeneration: source.generation}),
|
|
800
|
+
reservedFieldsStripped: sanitized.reservedFieldsStripped
|
|
801
|
+
})
|
|
802
|
+
let attribution
|
|
803
|
+
if (frame.type === V3FrameType.RESET && bridge.disconnectAttribution !== undefined) {
|
|
804
|
+
incidentState(bridge.disconnectAttribution).terminalStreamCount += 1
|
|
805
|
+
attribution = currentIncidentAttribution(bridge.disconnectAttribution)
|
|
806
|
+
}
|
|
807
|
+
const detail = attribution === undefined ? sanitized.detail : {...sanitized.detail, ...attribution}
|
|
746
808
|
forwardControl(destination, frame.type, destinationStreamId, frame.sequence, detail)
|
|
747
|
-
if (frame.type === V3FrameType.RESET || fromClient)
|
|
748
|
-
|
|
809
|
+
if (frame.type === V3FrameType.RESET || fromClient) {
|
|
810
|
+
removeBridge(bridge)
|
|
811
|
+
if (attribution !== undefined && bridge.disconnectAttribution !== undefined) {
|
|
812
|
+
logDiagnostic(options.logger, "reverse_hub.streams_reset", {
|
|
813
|
+
...attribution,
|
|
814
|
+
gatewayId: bridge.gatewayOwner.gatewayId,
|
|
815
|
+
...(bridge.client.routeClass === undefined ? {} : {routeClass: bridge.client.routeClass}),
|
|
816
|
+
...errorDiagnostics(sanitized.detail, "STREAM_RESET")
|
|
817
|
+
})
|
|
818
|
+
releaseCompletedIncident(bridge.disconnectAttribution)
|
|
819
|
+
}
|
|
820
|
+
} else bridge.state = "closing"
|
|
749
821
|
return
|
|
750
822
|
}
|
|
751
823
|
throw protocolError("UNSUPPORTED_FRAME")
|
|
@@ -768,44 +840,76 @@ export async function startReverseGatewayHub(/** @type {ReverseGatewayHubOptions
|
|
|
768
840
|
forwardOutput(destination, encodeV3Frame({type, streamId, sequence}, encodeV3Control(value)))
|
|
769
841
|
}
|
|
770
842
|
|
|
771
|
-
/** @param {Bridge} bridge - Stream. @param {string} code - Reset code. @param {HubPhysical} [excluded] - Already-dead peer. */
|
|
843
|
+
/** @param {Bridge} bridge - Stream. @param {string} code - Reset code. @param {HubPhysical} [excluded] - Already-dead peer. @returns {Promise<void>} Terminal control delivery. */
|
|
772
844
|
function closeBridge(bridge, code, excluded) {
|
|
773
|
-
|
|
774
|
-
|
|
845
|
+
const owned = bridgeStillOwned(bridge)
|
|
846
|
+
let attribution
|
|
847
|
+
if (owned && bridge.disconnectAttribution !== undefined) {
|
|
848
|
+
const incident = incidentState(bridge.disconnectAttribution)
|
|
849
|
+
incident.terminalStreamCount += 1
|
|
850
|
+
attribution = currentIncidentAttribution(bridge.disconnectAttribution)
|
|
851
|
+
}
|
|
852
|
+
const detail = {code, ...(attribution ?? {})}
|
|
853
|
+
const deliveries = []
|
|
854
|
+
if (bridge.client !== excluded && !bridge.client.socket.destroyed) deliveries.push(sendControl(bridge.client, V3FrameType.RESET, bridge.clientStreamId, 0, detail))
|
|
855
|
+
if (bridge.gateway !== excluded && !bridge.gateway.socket.destroyed) deliveries.push(sendControl(bridge.gateway, V3FrameType.RESET, bridge.gatewayStreamId, 0, detail))
|
|
775
856
|
removeBridge(bridge)
|
|
857
|
+
if (attribution !== undefined) logDiagnostic(options.logger, "reverse_hub.streams_reset", {
|
|
858
|
+
...attribution,
|
|
859
|
+
gatewayId: bridge.gatewayOwner.gatewayId,
|
|
860
|
+
...(bridge.client.routeClass === undefined ? {} : {routeClass: bridge.client.routeClass}),
|
|
861
|
+
...errorDiagnostics({code})
|
|
862
|
+
})
|
|
863
|
+
if (bridge.disconnectAttribution !== undefined) releaseCompletedIncident(bridge.disconnectAttribution)
|
|
864
|
+
return Promise.all(deliveries).then(() => undefined)
|
|
776
865
|
}
|
|
777
866
|
|
|
778
|
-
/** @param {HubPhysical} physical - Connection. @param {string} code - Reason. @param {boolean} [retainStreams] - Preserve logical streams for paired resume. */
|
|
779
|
-
function terminate(physical, code, retainStreams = false) {
|
|
867
|
+
/** @param {HubPhysical} physical - Connection. @param {string} code - Reason. @param {boolean} [retainStreams] - Preserve logical streams for paired resume. @param {import('./disconnect-attribution.js').DisconnectAttribution} [attribution] - Propagated first-cause incident. */
|
|
868
|
+
function terminate(physical, code, retainStreams = false, attribution = undefined) {
|
|
780
869
|
if (physical.closing) return
|
|
781
870
|
physical.closing = true
|
|
782
871
|
physical.retainOnDetach = retainStreams
|
|
783
872
|
physical.detachCode = code
|
|
873
|
+
const disconnectAttribution = ensurePhysicalAttribution(physical, disconnectReasonForCode(code), attribution)
|
|
784
874
|
const forceTimer = setTimeout(() => physical.socket.destroy(), 1_000)
|
|
785
875
|
forceTimer.unref?.()
|
|
786
|
-
void sendControl(physical, V3FrameType.GOAWAY, 0, 0, {code}, true).finally(() => {
|
|
876
|
+
void sendControl(physical, V3FrameType.GOAWAY, 0, 0, {code, ...(disconnectAttribution === undefined ? {} : currentIncidentAttribution(disconnectAttribution))}, true).finally(() => {
|
|
787
877
|
clearTimeout(forceTimer)
|
|
788
878
|
physical.socket.destroy()
|
|
789
879
|
})
|
|
790
880
|
}
|
|
791
881
|
|
|
792
882
|
/** @param {HubPhysical} physical - Closed connection. @param {string} code - Stream reset. */
|
|
793
|
-
function detachPhysical(physical, code) {
|
|
883
|
+
async function detachPhysical(physical, code) {
|
|
794
884
|
if (!physicals.delete(physical)) return
|
|
795
885
|
physical.closing = true
|
|
886
|
+
const attribution = ensurePhysicalAttribution(physical, physical.firstDisconnectReason ?? disconnectReasonForCode(code))
|
|
887
|
+
if (attribution !== undefined) logDiagnostic(options.logger, "reverse_hub.physical_detached", {
|
|
888
|
+
...currentIncidentAttribution(attribution),
|
|
889
|
+
...(physical.connectionFingerprint === undefined || attribution.physicalRole !== physical.role ? {} : {connectionFingerprint: physical.connectionFingerprint, fingerprintKeyEpoch: options.connectionFingerprint?.keyEpoch}),
|
|
890
|
+
...(physical.safeSocketCode === undefined ? {} : {safeSocketCode: physical.safeSocketCode})
|
|
891
|
+
})
|
|
796
892
|
clearResumeTimeout(physical)
|
|
797
893
|
discardQueuedOutput(physical)
|
|
798
894
|
if (physical.gatewayId !== undefined) {
|
|
799
895
|
const owner = gateways.get(physical.gatewayId)
|
|
800
896
|
if (physical.role === "gateway" && owner?.physical === physical) {
|
|
801
897
|
owner.physical = undefined
|
|
898
|
+
if (attribution !== undefined) for (const bridge of owner.streams.values()) assignBridgeAttribution(bridge, attribution)
|
|
802
899
|
if (!physical.retainOnDetach || closing) {
|
|
803
|
-
for (const bridge of [...owner.streams.values()]) closeBridge(bridge, code, physical)
|
|
900
|
+
for (const bridge of [...owner.streams.values()]) await closeBridge(bridge, code, physical)
|
|
804
901
|
gateways.delete(owner.gatewayId)
|
|
805
902
|
} else {
|
|
903
|
+
if (attribution !== undefined) {
|
|
904
|
+
logDiagnostic(options.logger, "reverse_hub.streams_interrupted", {...currentIncidentAttribution(attribution), gatewayId: owner.gatewayId})
|
|
905
|
+
}
|
|
806
906
|
scheduleOwnerExpiry(owner, "gateway")
|
|
807
907
|
const clientsToReconnect = new Set([...owner.streams.values()].map((bridge) => bridge.clientOwner.physical).filter((peer) => peer !== undefined))
|
|
808
|
-
for (const client of clientsToReconnect)
|
|
908
|
+
for (const client of clientsToReconnect) {
|
|
909
|
+
const peer = /** @type {HubPhysical} */ (client)
|
|
910
|
+
const propagated = attribution === undefined ? undefined : createDisconnectAttribution({...currentIncidentAttribution(attribution), clientConnectionGeneration: peer.generation})
|
|
911
|
+
terminate(peer, "GATEWAY_DISCONNECTED", true, propagated)
|
|
912
|
+
}
|
|
809
913
|
}
|
|
810
914
|
}
|
|
811
915
|
}
|
|
@@ -813,37 +917,109 @@ export async function startReverseGatewayHub(/** @type {ReverseGatewayHubOptions
|
|
|
813
917
|
const owner = clients.get(physical.connectionId)
|
|
814
918
|
if (owner?.physical === physical) {
|
|
815
919
|
owner.physical = undefined
|
|
920
|
+
if (attribution !== undefined) for (const bridge of owner.streams.values()) assignBridgeAttribution(bridge, attribution)
|
|
816
921
|
if (!physical.retainOnDetach || closing) {
|
|
817
|
-
for (const bridge of [...owner.streams.values()]) closeBridge(bridge, code, physical)
|
|
922
|
+
for (const bridge of [...owner.streams.values()]) await closeBridge(bridge, code, physical)
|
|
818
923
|
clients.delete(owner.connectionId)
|
|
819
924
|
} else {
|
|
925
|
+
if (attribution !== undefined) {
|
|
926
|
+
logDiagnostic(options.logger, "reverse_hub.streams_interrupted", {
|
|
927
|
+
...currentIncidentAttribution(attribution),
|
|
928
|
+
...(physical.gatewayId === undefined ? {} : {gatewayId: physical.gatewayId}),
|
|
929
|
+
...(physical.routeClass === undefined ? {} : {routeClass: physical.routeClass})
|
|
930
|
+
})
|
|
931
|
+
}
|
|
820
932
|
scheduleOwnerExpiry(owner, "client")
|
|
821
933
|
const gatewaysToReconnect = new Set([...owner.streams.values()].map((bridge) => bridge.gatewayOwner.physical).filter((peer) => peer !== undefined))
|
|
822
|
-
for (const gateway of gatewaysToReconnect)
|
|
934
|
+
for (const gateway of gatewaysToReconnect) {
|
|
935
|
+
const peer = /** @type {HubPhysical} */ (gateway)
|
|
936
|
+
const propagated = attribution === undefined ? undefined : createDisconnectAttribution({...currentIncidentAttribution(attribution), gatewayConnectionGeneration: peer.generation})
|
|
937
|
+
terminate(peer, "CLIENT_DISCONNECTED", true, propagated)
|
|
938
|
+
}
|
|
823
939
|
}
|
|
824
940
|
}
|
|
825
941
|
}
|
|
942
|
+
if (attribution !== undefined && (!physical.retainOnDetach || incidentState(attribution).fanOutCount === 0)) incidents.delete(attribution.disconnectIncidentId)
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** @param {HubPhysical} physical - Connection. @param {import('./disconnect-attribution.js').DisconnectReason} reason - Physical cause. @param {import('./disconnect-attribution.js').DisconnectAttribution | undefined} [propagated] - Existing incident. */
|
|
946
|
+
function ensurePhysicalAttribution(physical, reason, propagated = undefined) {
|
|
947
|
+
if (physical.disconnectAttribution !== undefined) return physical.disconnectAttribution
|
|
948
|
+
if (physical.role === undefined) return undefined
|
|
949
|
+
const source = propagated === undefined ? undefined : parseDisconnectAttribution(propagated)
|
|
950
|
+
physical.disconnectAttribution = createDisconnectAttribution({
|
|
951
|
+
...(source ?? {}),
|
|
952
|
+
disconnectReason: source?.disconnectReason ?? reason,
|
|
953
|
+
incidentOrigin: source?.incidentOrigin ?? "reverse_hub",
|
|
954
|
+
physicalRole: source?.physicalRole ?? physical.role,
|
|
955
|
+
...(physical.role === "client" ? {clientConnectionGeneration: physical.generation} : {gatewayConnectionGeneration: physical.generation}),
|
|
956
|
+
fanOutCount: source?.fanOutCount ?? physical.streams.size,
|
|
957
|
+
resumeSuccessCount: source?.resumeSuccessCount ?? 0,
|
|
958
|
+
terminalStreamCount: source?.terminalStreamCount ?? 0
|
|
959
|
+
})
|
|
960
|
+
incidentState(physical.disconnectAttribution)
|
|
961
|
+
return physical.disconnectAttribution
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/** Preserves the first incident while completing both endpoint generations. */
|
|
965
|
+
function assignBridgeAttribution(/** @type {Bridge} */ bridge, /** @type {import('./disconnect-attribution.js').DisconnectAttribution} */ attribution) {
|
|
966
|
+
bridge.disconnectAttribution ??= createDisconnectAttribution({
|
|
967
|
+
...currentIncidentAttribution(attribution),
|
|
968
|
+
clientConnectionGeneration: bridge.client.generation,
|
|
969
|
+
gatewayConnectionGeneration: bridge.gateway.generation
|
|
970
|
+
})
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/** @param {import('./disconnect-attribution.js').DisconnectAttribution} attribution - Incident. @returns {{fanOutCount: number, resumeSuccessCount: number, terminalStreamCount: number}} */
|
|
974
|
+
function incidentState(attribution) {
|
|
975
|
+
let state = incidents.get(attribution.disconnectIncidentId)
|
|
976
|
+
if (state === undefined) {
|
|
977
|
+
state = {fanOutCount: attribution.fanOutCount, resumeSuccessCount: attribution.resumeSuccessCount, terminalStreamCount: attribution.terminalStreamCount}
|
|
978
|
+
incidents.set(attribution.disconnectIncidentId, state)
|
|
979
|
+
} else {
|
|
980
|
+
state.fanOutCount = Math.max(state.fanOutCount, attribution.fanOutCount)
|
|
981
|
+
state.resumeSuccessCount = Math.max(state.resumeSuccessCount, attribution.resumeSuccessCount)
|
|
982
|
+
state.terminalStreamCount = Math.max(state.terminalStreamCount, attribution.terminalStreamCount)
|
|
983
|
+
}
|
|
984
|
+
return state
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/** @param {import('./disconnect-attribution.js').DisconnectAttribution} attribution - Incident. */
|
|
988
|
+
function currentIncidentAttribution(attribution) {
|
|
989
|
+
const state = incidentState(attribution)
|
|
990
|
+
return createDisconnectAttribution({...attribution, ...state})
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/** @param {import('./disconnect-attribution.js').DisconnectAttribution} attribution - Incident. */
|
|
994
|
+
function releaseCompletedIncident(attribution) {
|
|
995
|
+
const state = incidentState(attribution)
|
|
996
|
+
if (state.resumeSuccessCount + state.terminalStreamCount >= state.fanOutCount) incidents.delete(attribution.disconnectIncidentId)
|
|
826
997
|
}
|
|
827
998
|
|
|
828
999
|
/** @param {GatewayOwner | ClientOwner} owner - Detached identity. @param {'gateway' | 'client'} role - Owner role. */
|
|
829
1000
|
function scheduleOwnerExpiry(owner, role) {
|
|
830
1001
|
if (owner.expiryTimer !== undefined) clearTimeout(owner.expiryTimer)
|
|
831
1002
|
owner.expiryTimer = setTimeout(() => {
|
|
832
|
-
owner
|
|
833
|
-
if (owner.physical !== undefined) return
|
|
834
|
-
for (const bridge of [...owner.streams.values()]) closeBridge(bridge, "RESUME_WINDOW_EXPIRED")
|
|
835
|
-
if (role === "gateway") {
|
|
836
|
-
const gatewayOwner = /** @type {GatewayOwner} */ (owner)
|
|
837
|
-
if (gateways.get(gatewayOwner.gatewayId) === gatewayOwner) gateways.delete(gatewayOwner.gatewayId)
|
|
838
|
-
} else {
|
|
839
|
-
const clientOwner = /** @type {ClientOwner} */ (owner)
|
|
840
|
-
if (clients.get(clientOwner.connectionId) === clientOwner) clients.delete(clientOwner.connectionId)
|
|
841
|
-
}
|
|
842
|
-
void tryCompleteResumes()
|
|
1003
|
+
void expireOwner(owner, role)
|
|
843
1004
|
}, resumeWindowMs)
|
|
844
1005
|
owner.expiryTimer.unref?.()
|
|
845
1006
|
}
|
|
846
1007
|
|
|
1008
|
+
/** @param {GatewayOwner | ClientOwner} owner - Detached identity. @param {'gateway' | 'client'} role - Owner role. */
|
|
1009
|
+
async function expireOwner(owner, role) {
|
|
1010
|
+
owner.expiryTimer = undefined
|
|
1011
|
+
if (owner.physical !== undefined) return
|
|
1012
|
+
for (const bridge of [...owner.streams.values()]) await closeBridge(bridge, "RESUME_WINDOW_EXPIRED")
|
|
1013
|
+
if (role === "gateway") {
|
|
1014
|
+
const gatewayOwner = /** @type {GatewayOwner} */ (owner)
|
|
1015
|
+
if (gateways.get(gatewayOwner.gatewayId) === gatewayOwner) gateways.delete(gatewayOwner.gatewayId)
|
|
1016
|
+
} else {
|
|
1017
|
+
const clientOwner = /** @type {ClientOwner} */ (owner)
|
|
1018
|
+
if (clients.get(clientOwner.connectionId) === clientOwner) clients.delete(clientOwner.connectionId)
|
|
1019
|
+
}
|
|
1020
|
+
void tryCompleteResumes()
|
|
1021
|
+
}
|
|
1022
|
+
|
|
847
1023
|
/** @param {HubPhysical} physical - Destination. @param {Buffer} bytes - Encoded frame. @param {boolean} [allowClosing] - Allow a final GOAWAY after closing starts. @returns {Promise<boolean>} Delivery result. */
|
|
848
1024
|
function enqueueOutput(physical, bytes, allowClosing = false) {
|
|
849
1025
|
if (physical.socket.destroyed || (physical.closing && !allowClosing)) return Promise.resolve(false)
|
|
@@ -897,6 +1073,8 @@ export async function startReverseGatewayHub(/** @type {ReverseGatewayHubOptions
|
|
|
897
1073
|
physical.closing = true
|
|
898
1074
|
physical.retainOnDetach = false
|
|
899
1075
|
physical.detachCode ??= code
|
|
1076
|
+
physical.firstDisconnectReason ??= disconnectReasonForCode(code)
|
|
1077
|
+
ensurePhysicalAttribution(physical, physical.firstDisconnectReason)
|
|
900
1078
|
discardQueuedOutput(physical)
|
|
901
1079
|
if (!physical.socket.destroyed) physical.socket.destroy(protocolError(code))
|
|
902
1080
|
}
|
|
@@ -961,6 +1139,11 @@ function socketKey(socket) {
|
|
|
961
1139
|
/** @param {ReverseGatewayHubOptions} options - Options. */
|
|
962
1140
|
function validateOptions(options) {
|
|
963
1141
|
if (typeof options !== "object" || options === null || options.tokens === undefined || typeof options.tokens.authenticate !== "function") throw new TypeError("Reverse gateway hub requires a token authenticator")
|
|
1142
|
+
if (options.connectionFingerprint !== undefined) {
|
|
1143
|
+
const {key, keyEpoch} = options.connectionFingerprint
|
|
1144
|
+
if (!(key instanceof Uint8Array) || key.byteLength < 32) throw new TypeError("connection fingerprint key must contain at least 32 bytes")
|
|
1145
|
+
validateFingerprintKeyEpoch(keyEpoch)
|
|
1146
|
+
}
|
|
964
1147
|
if (typeof options.authorizeGateway !== "function" || typeof options.authorizeClientGateway !== "function" || typeof options.authorizeOpen !== "function") throw new TypeError("Reverse gateway hub requires gateway, client selection, and open authorization policies")
|
|
965
1148
|
if (typeof options.tls !== "object" || options.tls === null || typeof options.tls.certFile !== "string" || typeof options.tls.keyFile !== "string") throw new TypeError("Reverse gateway hub requires TLS certFile and keyFile")
|
|
966
1149
|
if (!zstdAvailable()) throw new Error("Reverse gateway hub requires Node.js zstd support")
|
package/src/reverse/runtime.js
CHANGED
|
@@ -3,8 +3,8 @@ import {StaticTokenStore} from "../auth/static-token-store.js"
|
|
|
3
3
|
import {OutboundReverseGateway} from "./gateway.js"
|
|
4
4
|
import {startReverseGatewayHub} from "./hub.js"
|
|
5
5
|
|
|
6
|
-
/** @param {import('../config/environment.js').RelayEnvironmentConfig} config - Parsed environment. */
|
|
7
|
-
export async function startReverseHubRuntime(config) {
|
|
6
|
+
/** @param {import('../config/environment.js').RelayEnvironmentConfig} config - Parsed environment. @param {import('../logging/structured-logger.js').DiagnosticLogger} [logger] - Optional structured logger. */
|
|
7
|
+
export async function startReverseHubRuntime(config, logger) {
|
|
8
8
|
if (config.mode !== "reverse-hub") throw new Error("Reverse hub runtime requires SOCKETDUCT_MODE=reverse-hub")
|
|
9
9
|
if (config.relayTls === undefined) throw new Error("Reverse hub mode requires SOCKETDUCT_TLS_CERT_FILE and SOCKETDUCT_TLS_KEY_FILE")
|
|
10
10
|
if (config.authTokens.length === 0) throw new Error("SOCKETDUCT_AUTH_TOKENS must contain hub client and gateway credentials")
|
|
@@ -54,7 +54,9 @@ export async function startReverseHubRuntime(config) {
|
|
|
54
54
|
resumeWindowMs: config.resumeWindowMs,
|
|
55
55
|
handshakeTimeoutMs: config.handshakeTimeoutMs,
|
|
56
56
|
maxInputBytesPerConnection: config.maxInputBytesPerConnection,
|
|
57
|
-
compressionLimits: compressionLimits(config)
|
|
57
|
+
compressionLimits: compressionLimits(config),
|
|
58
|
+
...(logger === undefined ? {} : {logger}),
|
|
59
|
+
...(config.connectionFingerprint === undefined ? {} : {connectionFingerprint: config.connectionFingerprint})
|
|
58
60
|
})
|
|
59
61
|
return {hub, close: hub.close}
|
|
60
62
|
}
|
package/tensorbuzz.yml
CHANGED
|
@@ -2,7 +2,7 @@ dependency_roots:
|
|
|
2
2
|
- path: .
|
|
3
3
|
before_install:
|
|
4
4
|
- curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
|
|
5
|
-
- sudo apt-get install -y docker.io nodejs
|
|
5
|
+
- sudo apt-get install -y docker.io nodejs zstd=1.5.7+dfsg-3
|
|
6
6
|
before_script:
|
|
7
7
|
- node --version
|
|
8
8
|
environment:
|
|
@@ -34,4 +34,5 @@ script:
|
|
|
34
34
|
- npm run typecheck
|
|
35
35
|
- npm run lint
|
|
36
36
|
- npm test
|
|
37
|
+
- npm run verify:zstd-logs
|
|
37
38
|
- docker build --tag socketduct-ci .
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/* eslint-disable jsdoc/escape-inline-tags, jsdoc/require-param, jsdoc/require-returns */
|
|
2
|
+
import assert from "node:assert/strict"
|
|
3
|
+
import {spawn} from "node:child_process"
|
|
4
|
+
import {mkdtemp, rm} from "node:fs/promises"
|
|
5
|
+
import {createServer} from "node:net"
|
|
6
|
+
import {tmpdir} from "node:os"
|
|
7
|
+
import {join} from "node:path"
|
|
8
|
+
import test from "node:test"
|
|
9
|
+
|
|
10
|
+
import {readCompressedLogDirectory} from "../../src/logging/compressed-log-reader.js"
|
|
11
|
+
|
|
12
|
+
const TIMEOUT_MS = 8_000
|
|
13
|
+
|
|
14
|
+
test("SIGTERM durably flushes queued critical lifecycle records before exit", async (t) => {
|
|
15
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-logger-shutdown-"))
|
|
16
|
+
const directory = join(root, "logs")
|
|
17
|
+
const port = await reservePort()
|
|
18
|
+
const child = spawn(process.execPath, ["src/index.js"], {
|
|
19
|
+
cwd: process.cwd(),
|
|
20
|
+
env: {
|
|
21
|
+
...process.env,
|
|
22
|
+
SOCKETDUCT_MODE: "relay",
|
|
23
|
+
SOCKETDUCT_RELAY_HOST: "127.0.0.1",
|
|
24
|
+
SOCKETDUCT_RELAY_PORT: String(port),
|
|
25
|
+
SOCKETDUCT_AUTH_TOKENS: "shutdown-test:shutdown-test-token",
|
|
26
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:1",
|
|
27
|
+
SOCKETDUCT_STATUS_INTERVAL_MS: "0",
|
|
28
|
+
SOCKETDUCT_LOG_DIRECTORY: directory,
|
|
29
|
+
SOCKETDUCT_LOG_FLUSH_INTERVAL_MS: "60000",
|
|
30
|
+
SOCKETDUCT_LOG_CRITICAL_FLUSH_INTERVAL_MS: "60000"
|
|
31
|
+
},
|
|
32
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
33
|
+
})
|
|
34
|
+
let output = ""
|
|
35
|
+
child.stdout.on("data", (chunk) => { output += chunk.toString() })
|
|
36
|
+
child.stderr.on("data", (chunk) => { output += chunk.toString() })
|
|
37
|
+
t.after(async () => {
|
|
38
|
+
if (child.exitCode === null) child.kill("SIGKILL")
|
|
39
|
+
await rm(root, {recursive: true, force: true})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
await waitUntil(() => output.includes('"event":"relay.started"'), child, () => output)
|
|
43
|
+
assert.equal(child.kill("SIGTERM"), true)
|
|
44
|
+
const exit = await withTimeout(new Promise((resolve) => child.once("exit", (code, signal) => resolve({code, signal}))), "process exit")
|
|
45
|
+
assert.deepEqual(exit, {code: 0, signal: null})
|
|
46
|
+
|
|
47
|
+
const result = await readCompressedLogDirectory(directory)
|
|
48
|
+
assert.equal(result.incomplete, false)
|
|
49
|
+
assert.deepEqual(result.records.map((record) => record.event), ["process.started", "process.stopped", "log_batch_accepted", "log_frame_committed", "log_closed"])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
/** @returns {Promise<number>} A released loopback port. */
|
|
53
|
+
async function reservePort() {
|
|
54
|
+
const server = createServer()
|
|
55
|
+
await new Promise((resolve, reject) => {
|
|
56
|
+
server.once("error", reject)
|
|
57
|
+
server.listen(0, "127.0.0.1", () => { server.off("error", reject); resolve(undefined) })
|
|
58
|
+
})
|
|
59
|
+
const address = server.address()
|
|
60
|
+
assert(address !== null && typeof address !== "string")
|
|
61
|
+
await new Promise((resolve) => server.close(() => resolve(undefined)))
|
|
62
|
+
return address.port
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @param {() => boolean} predicate - Completion check. @param {import('node:child_process').ChildProcess} child - Child process. @param {() => string} output - Captured output. */
|
|
66
|
+
async function waitUntil(predicate, child, output) {
|
|
67
|
+
if (predicate()) return
|
|
68
|
+
await withTimeout(new Promise((resolve, reject) => {
|
|
69
|
+
const onData = () => { if (predicate()) { cleanup(); resolve(undefined) } }
|
|
70
|
+
const onExit = (/** @type {number | null} */ code) => { cleanup(); reject(new Error(`process exited ${code}: ${output()}`)) }
|
|
71
|
+
const cleanup = () => { child.stdout?.off("data", onData); child.stderr?.off("data", onData); child.off("exit", onExit) }
|
|
72
|
+
child.stdout?.on("data", onData)
|
|
73
|
+
child.stderr?.on("data", onData)
|
|
74
|
+
child.once("exit", onExit)
|
|
75
|
+
}), "process startup")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** @template T @param {Promise<T>} promise - Work. @param {string} label - Timeout label. @returns {Promise<T>} Result. */
|
|
79
|
+
async function withTimeout(promise, label) {
|
|
80
|
+
let timer
|
|
81
|
+
try {
|
|
82
|
+
return await Promise.race([promise, new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`Timed out: ${label}`)), TIMEOUT_MS) })])
|
|
83
|
+
} finally {
|
|
84
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable jsdoc/escape-inline-tags, jsdoc/require-param, jsdoc/require-returns */
|
|
2
2
|
import assert from "node:assert/strict"
|
|
3
3
|
import {spawn} from "node:child_process"
|
|
4
|
-
import {mkdtemp,
|
|
4
|
+
import {mkdtemp, rm} from "node:fs/promises"
|
|
5
5
|
import {createServer} from "node:net"
|
|
6
6
|
import {tmpdir} from "node:os"
|
|
7
7
|
import {join} from "node:path"
|
|
@@ -9,10 +9,12 @@ import test from "node:test"
|
|
|
9
9
|
import {connect as connectTls} from "node:tls"
|
|
10
10
|
|
|
11
11
|
import {ReverseGatewayConnection} from "../../src/reverse/client.js"
|
|
12
|
+
import {readCompressedLogDirectory} from "../../src/logging/compressed-log-reader.js"
|
|
12
13
|
import {generateTestCertificates} from "../helpers/tls-certificates.js"
|
|
13
14
|
|
|
14
15
|
const PROCESS_TIMEOUT_MS = 8_000
|
|
15
16
|
const PROCESS_LISTEN_ATTEMPTS = 3
|
|
17
|
+
const TEST_FINGERPRINT_KEY = Buffer.alloc(32, 0x5a).toString("base64url")
|
|
16
18
|
|
|
17
19
|
/** A child exit observed before its expected startup event. */
|
|
18
20
|
class ProcessStartupExitError extends Error {
|
|
@@ -234,8 +236,9 @@ test("spawned reverse hub and outbound gateway expose an allowlisted Docker stre
|
|
|
234
236
|
SOCKETDUCT_REVERSE_ROUTES: `tensorbuzz:o2:127.0.0.1:${targetPort}`,
|
|
235
237
|
SOCKETDUCT_STATUS_INTERVAL_MS: "0",
|
|
236
238
|
SOCKETDUCT_LOG_DIRECTORY: logDirectory,
|
|
237
|
-
|
|
238
|
-
|
|
239
|
+
SOCKETDUCT_LOG_SEGMENT_MAX_BYTES: "4096",
|
|
240
|
+
SOCKETDUCT_CONNECTION_FINGERPRINT_KEY: TEST_FINGERPRINT_KEY,
|
|
241
|
+
SOCKETDUCT_CONNECTION_FINGERPRINT_KEY_EPOCH: "process-test"
|
|
239
242
|
}, "reverse-hub.started")
|
|
240
243
|
t.after(() => stopProcess(hub))
|
|
241
244
|
const gateway = await startProcess({
|
|
@@ -250,8 +253,7 @@ test("spawned reverse hub and outbound gateway expose an allowlisted Docker stre
|
|
|
250
253
|
SOCKETDUCT_TARGET_ALLOWLIST: `127.0.0.1:${targetPort}`,
|
|
251
254
|
SOCKETDUCT_STATUS_INTERVAL_MS: "0",
|
|
252
255
|
SOCKETDUCT_LOG_DIRECTORY: logDirectory,
|
|
253
|
-
|
|
254
|
-
SOCKETDUCT_LOG_MAX_FILES: "2"
|
|
256
|
+
SOCKETDUCT_LOG_SEGMENT_MAX_BYTES: "4096"
|
|
255
257
|
}, "reverse-gateway.started")
|
|
256
258
|
t.after(() => stopProcess(gateway))
|
|
257
259
|
|
|
@@ -270,19 +272,18 @@ test("spawned reverse hub and outbound gateway expose an allowlisted Docker stre
|
|
|
270
272
|
assert.equal(Buffer.from(/** @type {Uint8Array} */ (await withTimeout(response, "process response"))).toString(), "docker:GET /version")
|
|
271
273
|
assert.equal(client.status().compression, "zstd")
|
|
272
274
|
socket.destroy()
|
|
275
|
+
client.close()
|
|
276
|
+
await stopProcess(gateway)
|
|
277
|
+
await stopProcess(hub)
|
|
273
278
|
|
|
274
|
-
|
|
275
|
-
assert(
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
assert(files.includes(`reverse-gateway-${gateway.pid}.jsonl`))
|
|
279
|
-
const hubEntries = (await readFile(join(logDirectory, `reverse-hub-${hub.pid}.jsonl`), "utf8")).trim().split("\n").map((line) => JSON.parse(line))
|
|
280
|
-
const gatewayContents = await readFile(join(logDirectory, `reverse-gateway-${gateway.pid}.jsonl`), "utf8")
|
|
281
|
-
const gatewayEntries = gatewayContents.trim().split("\n").map((line) => JSON.parse(line))
|
|
279
|
+
const logs = await readCompressedLogDirectory(logDirectory)
|
|
280
|
+
assert.equal(logs.incomplete, false)
|
|
281
|
+
const hubEntries = logs.records.filter(({role}) => role === "reverse-hub")
|
|
282
|
+
const gatewayEntries = logs.records.filter(({role}) => role === "reverse-gateway")
|
|
282
283
|
assert(hubEntries.some(({event}) => event === "process.started"))
|
|
283
284
|
assert(gatewayEntries.some(({event}) => event === "process.started"))
|
|
284
285
|
assert(gatewayEntries.some(({event}) => event === "transport.connected"))
|
|
285
|
-
assert.doesNotMatch(
|
|
286
|
+
assert.doesNotMatch(JSON.stringify(logs.records), /process-gateway-token|127\.0\.0\.1|docker-server|process_home_o2_gateway/)
|
|
286
287
|
})
|
|
287
288
|
|
|
288
289
|
test("one route-class client physical connection interleaves streams through different gateway processes", async (t) => {
|