socketduct 0.1.25 → 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/logging/compressed-log-writer.js +1 -1
- package/src/logging/zstd-frame-codec.js +15 -1
- 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 +358 -341
- 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 +830 -748
- 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,578 +1,558 @@
|
|
|
1
|
-
import
|
|
2
|
-
import test from "node:test"
|
|
1
|
+
import {describe, expect, it} from "@velocious/testing"
|
|
3
2
|
|
|
4
3
|
import {isLoopbackHost, loadEnvironmentConfig} from "../../../src/config/environment.js"
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test("loadEnvironmentConfig accepts legacy Seamline relay secrets during migration", () => {
|
|
47
|
-
const config = loadEnvironmentConfig({
|
|
48
|
-
SOCKETDUCT_MODE: "relay",
|
|
49
|
-
SEAMLINE_AUTH_TOKENS: "edge:legacy-token",
|
|
50
|
-
SEAMLINE_TARGET_ALLOWLIST: "docker-socket-shim:2375"
|
|
5
|
+
describe("environment config", () => {
|
|
6
|
+
it("loadEnvironmentConfig parses relay Docker environment", () => {
|
|
7
|
+
const config = loadEnvironmentConfig({
|
|
8
|
+
SOCKETDUCT_MODE: "relay",
|
|
9
|
+
SOCKETDUCT_RELAY_HOST: "0.0.0.0",
|
|
10
|
+
SOCKETDUCT_RELAY_PORT: "3100",
|
|
11
|
+
SOCKETDUCT_RESUME_WINDOW_MS: "45000",
|
|
12
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one,backup:token-two",
|
|
13
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "tensorbuzz.internal:443,127.0.0.1:8080"
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
expect(config).toEqual({
|
|
17
|
+
mode: "relay",
|
|
18
|
+
relayHost: "0.0.0.0",
|
|
19
|
+
relayPort: 3100,
|
|
20
|
+
resumeWindowMs: 45000,
|
|
21
|
+
maxTargetBufferBytes: 1_048_576,
|
|
22
|
+
maxTotalBufferBytes: 16_777_216,
|
|
23
|
+
initialTargetRetryDelayMs: 250,
|
|
24
|
+
initialTargetRetryMaxDelayMs: 5_000,
|
|
25
|
+
initialTargetConnectTimeoutMs: 10_000,
|
|
26
|
+
handshakeTimeoutMs: 10_000,
|
|
27
|
+
maxInputBytesPerConnection: 65_536,
|
|
28
|
+
statusIntervalMs: 30_000,
|
|
29
|
+
compression: "auto",
|
|
30
|
+
protocolVersion: 2,
|
|
31
|
+
minCompressBytes: 256,
|
|
32
|
+
maxDecompressedBytes: 16_777_216,
|
|
33
|
+
maxExpansionRatio: 1000,
|
|
34
|
+
authTokens: [
|
|
35
|
+
{username: "edge", token: "token-one"},
|
|
36
|
+
{username: "backup", token: "token-two"}
|
|
37
|
+
],
|
|
38
|
+
targetAllowlist: [
|
|
39
|
+
{host: "tensorbuzz.internal", port: 443},
|
|
40
|
+
{host: "127.0.0.1", port: 8080}
|
|
41
|
+
],
|
|
42
|
+
allowInsecurePublic: false
|
|
43
|
+
})
|
|
51
44
|
})
|
|
52
45
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
it("loadEnvironmentConfig accepts legacy Seamline relay secrets during migration", () => {
|
|
47
|
+
const config = loadEnvironmentConfig({
|
|
48
|
+
SOCKETDUCT_MODE: "relay",
|
|
49
|
+
SEAMLINE_AUTH_TOKENS: "edge:legacy-token",
|
|
50
|
+
SEAMLINE_TARGET_ALLOWLIST: "docker-socket-shim:2375"
|
|
51
|
+
})
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
SOCKETDUCT_AUTH_TOKENS: "edge:new-token",
|
|
60
|
-
SOCKETDUCT_TARGET_ALLOWLIST: "docker-socket-shim:2375",
|
|
61
|
-
SEAMLINE_AUTH_TOKENS: "edge:legacy-token",
|
|
62
|
-
SEAMLINE_TARGET_ALLOWLIST: "legacy-docker:2375"
|
|
53
|
+
expect(config.authTokens).toEqual([{username: "edge", token: "legacy-token"}])
|
|
54
|
+
expect(config.targetAllowlist).toEqual([{host: "docker-socket-shim", port: 2375}])
|
|
63
55
|
})
|
|
64
56
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_AUTH_TOKENS: "missing-token"}), /username:token/)
|
|
73
|
-
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_TARGET_ALLOWLIST: "missing-port"}), /host:port/)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
test("loadEnvironmentConfig parses or disables the status interval", () => {
|
|
77
|
-
assert.equal(loadEnvironmentConfig({SOCKETDUCT_STATUS_INTERVAL_MS: "45000"}).statusIntervalMs, 45_000)
|
|
78
|
-
assert.equal(loadEnvironmentConfig({SOCKETDUCT_STATUS_INTERVAL_MS: "0"}).statusIntervalMs, 0)
|
|
79
|
-
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_STATUS_INTERVAL_MS: "-1"}), /non-negative integer/)
|
|
80
|
-
})
|
|
57
|
+
it("loadEnvironmentConfig prefers Socketduct relay secrets over legacy Seamline aliases", () => {
|
|
58
|
+
const config = loadEnvironmentConfig({
|
|
59
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:new-token",
|
|
60
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "docker-socket-shim:2375",
|
|
61
|
+
SEAMLINE_AUTH_TOKENS: "edge:legacy-token",
|
|
62
|
+
SEAMLINE_TARGET_ALLOWLIST: "legacy-docker:2375"
|
|
63
|
+
})
|
|
81
64
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
assert.equal(loadEnvironmentConfig({SOCKETDUCT_LOG_DIRECTORY: " "}).fileLogging, undefined)
|
|
85
|
-
assert.deepEqual(loadEnvironmentConfig({
|
|
86
|
-
SOCKETDUCT_LOG_DIRECTORY: " /var/log/socketduct ",
|
|
87
|
-
SOCKETDUCT_LOG_FRAME_MAX_BYTES: "2097152",
|
|
88
|
-
SOCKETDUCT_LOG_FLUSH_INTERVAL_MS: "1500",
|
|
89
|
-
SOCKETDUCT_LOG_CRITICAL_FLUSH_INTERVAL_MS: "200",
|
|
90
|
-
SOCKETDUCT_LOG_SEGMENT_MAX_BYTES: "268435456",
|
|
91
|
-
SOCKETDUCT_LOG_RETENTION_DAYS: "14",
|
|
92
|
-
SOCKETDUCT_LOG_RETENTION_MAX_BYTES: "3221225472",
|
|
93
|
-
SOCKETDUCT_LOG_QUEUE_MAX_BYTES: "33554432",
|
|
94
|
-
SOCKETDUCT_LOG_CRITICAL_RESERVE_BYTES: "4194304",
|
|
95
|
-
SOCKETDUCT_LOG_COMPRESSION_LEVEL: "18"
|
|
96
|
-
}).fileLogging, {
|
|
97
|
-
directory: "/var/log/socketduct",
|
|
98
|
-
frameMaxBytes: 2_097_152,
|
|
99
|
-
flushIntervalMs: 1_500,
|
|
100
|
-
criticalFlushIntervalMs: 200,
|
|
101
|
-
maxSegmentBytes: 268_435_456,
|
|
102
|
-
retentionDays: 14,
|
|
103
|
-
retentionBytes: 3_221_225_472,
|
|
104
|
-
maxQueueBytes: 33_554_432,
|
|
105
|
-
criticalReserveBytes: 4_194_304,
|
|
106
|
-
compressionLevel: 18
|
|
65
|
+
expect(config.authTokens).toEqual([{username: "edge", token: "new-token"}])
|
|
66
|
+
expect(config.targetAllowlist).toEqual([{host: "docker-socket-shim", port: 2375}])
|
|
107
67
|
})
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test("loadEnvironmentConfig rejects unsafe compressed logging bounds", () => {
|
|
111
|
-
assert.throws(() => loadEnvironmentConfig({
|
|
112
|
-
SOCKETDUCT_LOG_DIRECTORY: "/var/log/socketduct",
|
|
113
|
-
SOCKETDUCT_LOG_FRAME_MAX_BYTES: "100"
|
|
114
|
-
}), /SOCKETDUCT_LOG_FRAME_MAX_BYTES/)
|
|
115
|
-
assert.throws(() => loadEnvironmentConfig({
|
|
116
|
-
SOCKETDUCT_LOG_DIRECTORY: "/var/log/socketduct",
|
|
117
|
-
SOCKETDUCT_LOG_COMPRESSION_LEVEL: "23"
|
|
118
|
-
}), /SOCKETDUCT_LOG_COMPRESSION_LEVEL/)
|
|
119
|
-
assert.throws(() => loadEnvironmentConfig({
|
|
120
|
-
SOCKETDUCT_LOG_DIRECTORY: "/var/log/socketduct",
|
|
121
|
-
SOCKETDUCT_LOG_QUEUE_MAX_BYTES: "1024",
|
|
122
|
-
SOCKETDUCT_LOG_CRITICAL_RESERVE_BYTES: "1024"
|
|
123
|
-
}), /SOCKETDUCT_LOG_CRITICAL_RESERVE_BYTES/)
|
|
124
|
-
assert.throws(() => loadEnvironmentConfig({
|
|
125
|
-
SOCKETDUCT_LOG_DIRECTORY: "/var/log/socketduct",
|
|
126
|
-
SOCKETDUCT_LOG_SEGMENT_MAX_BYTES: "5368709120",
|
|
127
|
-
SOCKETDUCT_LOG_RETENTION_MAX_BYTES: "4294967296"
|
|
128
|
-
}), /SOCKETDUCT_LOG_SEGMENT_MAX_BYTES/)
|
|
129
|
-
assert.throws(() => loadEnvironmentConfig({
|
|
130
|
-
SOCKETDUCT_LOG_DIRECTORY: "/var/log/socketduct",
|
|
131
|
-
SOCKETDUCT_LOG_MAX_FILE_BYTES: "10485760"
|
|
132
|
-
}), /SOCKETDUCT_LOG_MAX_FILE_BYTES/)
|
|
133
|
-
})
|
|
134
68
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
SOCKETDUCT_CONNECTION_FINGERPRINT_KEY_EPOCH: "hub-2026-09"
|
|
142
|
-
})
|
|
143
|
-
assert.deepEqual(config.connectionFingerprint, {key: Buffer.alloc(32, 0x5a), keyEpoch: "hub-2026-09"})
|
|
144
|
-
assert.throws(() => loadEnvironmentConfig({
|
|
145
|
-
SOCKETDUCT_MODE: "reverse-hub",
|
|
146
|
-
SOCKETDUCT_LOG_DIRECTORY: "/var/log/socketduct"
|
|
147
|
-
}), /SOCKETDUCT_CONNECTION_FINGERPRINT_KEY/)
|
|
148
|
-
assert.throws(() => loadEnvironmentConfig({
|
|
149
|
-
SOCKETDUCT_MODE: "reverse-gateway",
|
|
150
|
-
SOCKETDUCT_CONNECTION_FINGERPRINT_KEY: encodedKey,
|
|
151
|
-
SOCKETDUCT_CONNECTION_FINGERPRINT_KEY_EPOCH: "hub-2026-09"
|
|
152
|
-
}), /hub-only/)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
test("loadEnvironmentConfig selects v3 explicitly and keeps v2 as rollback default", () => {
|
|
156
|
-
assert.equal(loadEnvironmentConfig({}).protocolVersion, 2)
|
|
157
|
-
assert.equal(loadEnvironmentConfig({SOCKETDUCT_PROTOCOL_VERSION: "3"}).protocolVersion, 3)
|
|
158
|
-
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_PROTOCOL_VERSION: "1"}), /must be 2 or 3/)
|
|
159
|
-
})
|
|
69
|
+
it("loadEnvironmentConfig rejects malformed ports and list entries", () => {
|
|
70
|
+
expect(() => loadEnvironmentConfig({SOCKETDUCT_RELAY_PORT: "0"})).toThrow(/positive integer/)
|
|
71
|
+
expect(() => loadEnvironmentConfig({SOCKETDUCT_RELAY_PORT: "70000"})).toThrow(/TCP port/)
|
|
72
|
+
expect(() => loadEnvironmentConfig({SOCKETDUCT_AUTH_TOKENS: "missing-token"})).toThrow(/username:token/)
|
|
73
|
+
expect(() => loadEnvironmentConfig({SOCKETDUCT_TARGET_ALLOWLIST: "missing-port"})).toThrow(/host:port/)
|
|
74
|
+
})
|
|
160
75
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}),
|
|
167
|
-
/placeholder/
|
|
168
|
-
)
|
|
169
|
-
assert.throws(
|
|
170
|
-
() => loadEnvironmentConfig({
|
|
171
|
-
SOCKETDUCT_AUTH_TOKENS: "tensorbuzz-edge:paste-generated-token-here",
|
|
172
|
-
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:443"
|
|
173
|
-
}),
|
|
174
|
-
/placeholder/
|
|
175
|
-
)
|
|
176
|
-
})
|
|
76
|
+
it("loadEnvironmentConfig parses or disables the status interval", () => {
|
|
77
|
+
expect(loadEnvironmentConfig({SOCKETDUCT_STATUS_INTERVAL_MS: "45000"}).statusIntervalMs).toBe(45_000)
|
|
78
|
+
expect(loadEnvironmentConfig({SOCKETDUCT_STATUS_INTERVAL_MS: "0"}).statusIntervalMs).toBe(0)
|
|
79
|
+
expect(() => loadEnvironmentConfig({SOCKETDUCT_STATUS_INTERVAL_MS: "-1"})).toThrow(/non-negative integer/)
|
|
80
|
+
})
|
|
177
81
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
82
|
+
it("loadEnvironmentConfig keeps persistent logging disabled by default and parses compressed bounds", () => {
|
|
83
|
+
expect(loadEnvironmentConfig({}).fileLogging).toBeUndefined()
|
|
84
|
+
expect(loadEnvironmentConfig({SOCKETDUCT_LOG_DIRECTORY: " "}).fileLogging).toBeUndefined()
|
|
85
|
+
expect(loadEnvironmentConfig({
|
|
86
|
+
SOCKETDUCT_LOG_DIRECTORY: " /var/log/socketduct ",
|
|
87
|
+
SOCKETDUCT_LOG_FRAME_MAX_BYTES: "2097152",
|
|
88
|
+
SOCKETDUCT_LOG_FLUSH_INTERVAL_MS: "1500",
|
|
89
|
+
SOCKETDUCT_LOG_CRITICAL_FLUSH_INTERVAL_MS: "200",
|
|
90
|
+
SOCKETDUCT_LOG_SEGMENT_MAX_BYTES: "268435456",
|
|
91
|
+
SOCKETDUCT_LOG_RETENTION_DAYS: "14",
|
|
92
|
+
SOCKETDUCT_LOG_RETENTION_MAX_BYTES: "3221225472",
|
|
93
|
+
SOCKETDUCT_LOG_QUEUE_MAX_BYTES: "33554432",
|
|
94
|
+
SOCKETDUCT_LOG_CRITICAL_RESERVE_BYTES: "4194304",
|
|
95
|
+
SOCKETDUCT_LOG_COMPRESSION_LEVEL: "18"
|
|
96
|
+
}).fileLogging).toEqual({
|
|
97
|
+
directory: "/var/log/socketduct",
|
|
98
|
+
frameMaxBytes: 2_097_152,
|
|
99
|
+
flushIntervalMs: 1_500,
|
|
100
|
+
criticalFlushIntervalMs: 200,
|
|
101
|
+
maxSegmentBytes: 268_435_456,
|
|
102
|
+
retentionDays: 14,
|
|
103
|
+
retentionBytes: 3_221_225_472,
|
|
104
|
+
maxQueueBytes: 33_554_432,
|
|
105
|
+
criticalReserveBytes: 4_194_304,
|
|
106
|
+
compressionLevel: 18
|
|
107
|
+
})
|
|
182
108
|
})
|
|
183
109
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
110
|
+
it("loadEnvironmentConfig rejects unsafe compressed logging bounds", () => {
|
|
111
|
+
expect(() => loadEnvironmentConfig({
|
|
112
|
+
SOCKETDUCT_LOG_DIRECTORY: "/var/log/socketduct",
|
|
113
|
+
SOCKETDUCT_LOG_FRAME_MAX_BYTES: "100"
|
|
114
|
+
})).toThrow(/SOCKETDUCT_LOG_FRAME_MAX_BYTES/)
|
|
115
|
+
expect(() => loadEnvironmentConfig({
|
|
116
|
+
SOCKETDUCT_LOG_DIRECTORY: "/var/log/socketduct",
|
|
117
|
+
SOCKETDUCT_LOG_COMPRESSION_LEVEL: "23"
|
|
118
|
+
})).toThrow(/SOCKETDUCT_LOG_COMPRESSION_LEVEL/)
|
|
119
|
+
expect(() => loadEnvironmentConfig({
|
|
120
|
+
SOCKETDUCT_LOG_DIRECTORY: "/var/log/socketduct",
|
|
121
|
+
SOCKETDUCT_LOG_QUEUE_MAX_BYTES: "1024",
|
|
122
|
+
SOCKETDUCT_LOG_CRITICAL_RESERVE_BYTES: "1024"
|
|
123
|
+
})).toThrow(/SOCKETDUCT_LOG_CRITICAL_RESERVE_BYTES/)
|
|
124
|
+
expect(() => loadEnvironmentConfig({
|
|
125
|
+
SOCKETDUCT_LOG_DIRECTORY: "/var/log/socketduct",
|
|
126
|
+
SOCKETDUCT_LOG_SEGMENT_MAX_BYTES: "5368709120",
|
|
127
|
+
SOCKETDUCT_LOG_RETENTION_MAX_BYTES: "4294967296"
|
|
128
|
+
})).toThrow(/SOCKETDUCT_LOG_SEGMENT_MAX_BYTES/)
|
|
129
|
+
expect(() => loadEnvironmentConfig({
|
|
130
|
+
SOCKETDUCT_LOG_DIRECTORY: "/var/log/socketduct",
|
|
131
|
+
SOCKETDUCT_LOG_MAX_FILE_BYTES: "10485760"
|
|
132
|
+
})).toThrow(/SOCKETDUCT_LOG_MAX_FILE_BYTES/)
|
|
187
133
|
})
|
|
188
|
-
})
|
|
189
134
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
135
|
+
it("loadEnvironmentConfig accepts a paired hub-only fingerprint key and epoch", () => {
|
|
136
|
+
const encodedKey = Buffer.alloc(32, 0x5a).toString("base64url")
|
|
137
|
+
const config = loadEnvironmentConfig({
|
|
138
|
+
SOCKETDUCT_MODE: "reverse-hub",
|
|
139
|
+
SOCKETDUCT_LOG_DIRECTORY: "/var/log/socketduct",
|
|
140
|
+
SOCKETDUCT_CONNECTION_FINGERPRINT_KEY: encodedKey,
|
|
141
|
+
SOCKETDUCT_CONNECTION_FINGERPRINT_KEY_EPOCH: "hub-2026-09"
|
|
142
|
+
})
|
|
143
|
+
expect(config.connectionFingerprint?.key.equals(Buffer.alloc(32, 0x5a))).toBe(true)
|
|
144
|
+
expect(config.connectionFingerprint?.keyEpoch).toBe("hub-2026-09")
|
|
145
|
+
expect(() => loadEnvironmentConfig({
|
|
146
|
+
SOCKETDUCT_MODE: "reverse-hub",
|
|
147
|
+
SOCKETDUCT_LOG_DIRECTORY: "/var/log/socketduct"
|
|
148
|
+
})).toThrow(/SOCKETDUCT_CONNECTION_FINGERPRINT_KEY/)
|
|
149
|
+
expect(() => loadEnvironmentConfig({
|
|
150
|
+
SOCKETDUCT_MODE: "reverse-gateway",
|
|
151
|
+
SOCKETDUCT_CONNECTION_FINGERPRINT_KEY: encodedKey,
|
|
152
|
+
SOCKETDUCT_CONNECTION_FINGERPRINT_KEY_EPOCH: "hub-2026-09"
|
|
153
|
+
})).toThrow(/hub-only/)
|
|
195
154
|
})
|
|
196
155
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
156
|
+
it("loadEnvironmentConfig selects v3 explicitly and keeps v2 as rollback default", () => {
|
|
157
|
+
expect(loadEnvironmentConfig({}).protocolVersion).toBe(2)
|
|
158
|
+
expect(loadEnvironmentConfig({SOCKETDUCT_PROTOCOL_VERSION: "3"}).protocolVersion).toBe(3)
|
|
159
|
+
expect(() => loadEnvironmentConfig({SOCKETDUCT_PROTOCOL_VERSION: "1"})).toThrow(/must be 2 or 3/)
|
|
201
160
|
})
|
|
202
|
-
})
|
|
203
161
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
162
|
+
it("loadEnvironmentConfig rejects placeholder auth tokens", () => {
|
|
163
|
+
expect(
|
|
164
|
+
() => loadEnvironmentConfig({
|
|
165
|
+
SOCKETDUCT_AUTH_TOKENS: "tensorbuzz-edge:replace-with-generated-token",
|
|
166
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:443"
|
|
167
|
+
})
|
|
168
|
+
).toThrow(/placeholder/)
|
|
169
|
+
expect(
|
|
170
|
+
() => loadEnvironmentConfig({
|
|
171
|
+
SOCKETDUCT_AUTH_TOKENS: "tensorbuzz-edge:paste-generated-token-here",
|
|
172
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:443"
|
|
173
|
+
})
|
|
174
|
+
).toThrow(/placeholder/)
|
|
208
175
|
})
|
|
209
176
|
|
|
210
|
-
|
|
211
|
-
|
|
177
|
+
it("loadEnvironmentConfig parses TLS cert/key as an atomic pair", () => {
|
|
178
|
+
const config = loadEnvironmentConfig({
|
|
179
|
+
SOCKETDUCT_TLS_CERT_FILE: "/certs/relay.crt",
|
|
180
|
+
SOCKETDUCT_TLS_KEY_FILE: "/certs/relay.key"
|
|
181
|
+
})
|
|
212
182
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
)
|
|
218
|
-
assert.throws(
|
|
219
|
-
() => loadEnvironmentConfig({SOCKETDUCT_TLS_KEY_FILE: "/certs/relay.key"}),
|
|
220
|
-
/atomic pair/
|
|
221
|
-
)
|
|
222
|
-
})
|
|
183
|
+
expect(config.relayTls).toEqual({
|
|
184
|
+
certFile: "/certs/relay.crt",
|
|
185
|
+
keyFile: "/certs/relay.key"
|
|
186
|
+
})
|
|
187
|
+
})
|
|
223
188
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
189
|
+
it("loadEnvironmentConfig parses optional client CA for mTLS", () => {
|
|
190
|
+
const config = loadEnvironmentConfig({
|
|
191
|
+
SOCKETDUCT_TLS_CERT_FILE: "/certs/relay.crt",
|
|
192
|
+
SOCKETDUCT_TLS_KEY_FILE: "/certs/relay.key",
|
|
193
|
+
SOCKETDUCT_TLS_CLIENT_CA_FILE: "/certs/client-ca.crt"
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
expect(config.relayTls).toEqual({
|
|
197
|
+
certFile: "/certs/relay.crt",
|
|
198
|
+
keyFile: "/certs/relay.key",
|
|
199
|
+
clientCaFile: "/certs/client-ca.crt"
|
|
200
|
+
})
|
|
228
201
|
})
|
|
229
202
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
203
|
+
it("loadEnvironmentConfig returns undefined relayTls when no TLS vars are set", () => {
|
|
204
|
+
const config = loadEnvironmentConfig({
|
|
205
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token",
|
|
206
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:443"
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
expect(config.relayTls).toBeUndefined()
|
|
233
210
|
})
|
|
234
|
-
})
|
|
235
211
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
212
|
+
it("loadEnvironmentConfig rejects TLS cert without key", () => {
|
|
213
|
+
expect(
|
|
214
|
+
() => loadEnvironmentConfig({SOCKETDUCT_TLS_CERT_FILE: "/certs/relay.crt"})
|
|
215
|
+
).toThrow(/atomic pair/)
|
|
216
|
+
expect(
|
|
217
|
+
() => loadEnvironmentConfig({SOCKETDUCT_TLS_KEY_FILE: "/certs/relay.key"})
|
|
218
|
+
).toThrow(/atomic pair/)
|
|
219
|
+
})
|
|
243
220
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
assert.throws(
|
|
250
|
-
() => loadEnvironmentConfig({SOCKETDUCT_ALLOW_INSECURE_PUBLIC: "0"}),
|
|
251
|
-
/SOCKETDUCT_ALLOW_INSECURE_PUBLIC/
|
|
252
|
-
)
|
|
253
|
-
assert.throws(
|
|
254
|
-
() => loadEnvironmentConfig({SOCKETDUCT_ALLOW_INSECURE_PUBLIC: "false"}),
|
|
255
|
-
/SOCKETDUCT_ALLOW_INSECURE_PUBLIC/
|
|
256
|
-
)
|
|
257
|
-
})
|
|
221
|
+
it("loadEnvironmentConfig trims whitespace from TLS file paths", () => {
|
|
222
|
+
const config = loadEnvironmentConfig({
|
|
223
|
+
SOCKETDUCT_TLS_CERT_FILE: " /certs/relay.crt ",
|
|
224
|
+
SOCKETDUCT_TLS_KEY_FILE: " /certs/relay.key "
|
|
225
|
+
})
|
|
258
226
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
)
|
|
264
|
-
assert.throws(
|
|
265
|
-
() => loadEnvironmentConfig({
|
|
266
|
-
SOCKETDUCT_TLS_CLIENT_CA_FILE: "/certs/client-ca.crt",
|
|
267
|
-
SOCKETDUCT_TLS_CERT_FILE: "/certs/relay.crt"
|
|
268
|
-
}),
|
|
269
|
-
/SOCKETDUCT_TLS_CLIENT_CA_FILE/
|
|
270
|
-
)
|
|
271
|
-
assert.throws(
|
|
272
|
-
() => loadEnvironmentConfig({
|
|
273
|
-
SOCKETDUCT_TLS_CLIENT_CA_FILE: "/certs/client-ca.crt",
|
|
274
|
-
SOCKETDUCT_TLS_KEY_FILE: "/certs/relay.key"
|
|
275
|
-
}),
|
|
276
|
-
/SOCKETDUCT_TLS_CLIENT_CA_FILE/
|
|
277
|
-
)
|
|
278
|
-
})
|
|
227
|
+
expect(config.relayTls).toEqual({
|
|
228
|
+
certFile: "/certs/relay.crt",
|
|
229
|
+
keyFile: "/certs/relay.key"
|
|
230
|
+
})
|
|
231
|
+
})
|
|
279
232
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
})
|
|
233
|
+
it("loadEnvironmentConfig parses SOCKETDUCT_ALLOW_INSECURE_PUBLIC valid values", () => {
|
|
234
|
+
expect(loadEnvironmentConfig({SOCKETDUCT_ALLOW_INSECURE_PUBLIC: "1"}).allowInsecurePublic).toBe(true)
|
|
235
|
+
expect(loadEnvironmentConfig({SOCKETDUCT_ALLOW_INSECURE_PUBLIC: "true"}).allowInsecurePublic).toBe(true)
|
|
236
|
+
expect(loadEnvironmentConfig({SOCKETDUCT_ALLOW_INSECURE_PUBLIC: " true "}).allowInsecurePublic).toBe(true)
|
|
237
|
+
expect(loadEnvironmentConfig({SOCKETDUCT_ALLOW_INSECURE_PUBLIC: ""}).allowInsecurePublic).toBe(false)
|
|
238
|
+
expect(loadEnvironmentConfig({}).allowInsecurePublic).toBe(false)
|
|
239
|
+
})
|
|
288
240
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
241
|
+
it("loadEnvironmentConfig rejects invalid SOCKETDUCT_ALLOW_INSECURE_PUBLIC values", () => {
|
|
242
|
+
expect(
|
|
243
|
+
() => loadEnvironmentConfig({SOCKETDUCT_ALLOW_INSECURE_PUBLIC: "yes"})
|
|
244
|
+
).toThrow(/SOCKETDUCT_ALLOW_INSECURE_PUBLIC/)
|
|
245
|
+
expect(
|
|
246
|
+
() => loadEnvironmentConfig({SOCKETDUCT_ALLOW_INSECURE_PUBLIC: "0"})
|
|
247
|
+
).toThrow(/SOCKETDUCT_ALLOW_INSECURE_PUBLIC/)
|
|
248
|
+
expect(
|
|
249
|
+
() => loadEnvironmentConfig({SOCKETDUCT_ALLOW_INSECURE_PUBLIC: "false"})
|
|
250
|
+
).toThrow(/SOCKETDUCT_ALLOW_INSECURE_PUBLIC/)
|
|
251
|
+
})
|
|
296
252
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
{username: "tensorbuzz", routeClass: "starlink", gatewayId: "o2", host: "docker-a", port: 2375},
|
|
315
|
-
{username: "tensorbuzz", routeClass: "starlink", gatewayId: "zyxel", host: "docker-b", port: 2375}
|
|
316
|
-
])
|
|
317
|
-
})
|
|
253
|
+
it("loadEnvironmentConfig rejects client CA without cert/key pair", () => {
|
|
254
|
+
expect(
|
|
255
|
+
() => loadEnvironmentConfig({SOCKETDUCT_TLS_CLIENT_CA_FILE: "/certs/client-ca.crt"})
|
|
256
|
+
).toThrow(/SOCKETDUCT_TLS_CLIENT_CA_FILE/)
|
|
257
|
+
expect(
|
|
258
|
+
() => loadEnvironmentConfig({
|
|
259
|
+
SOCKETDUCT_TLS_CLIENT_CA_FILE: "/certs/client-ca.crt",
|
|
260
|
+
SOCKETDUCT_TLS_CERT_FILE: "/certs/relay.crt"
|
|
261
|
+
})
|
|
262
|
+
).toThrow(/SOCKETDUCT_TLS_CLIENT_CA_FILE/)
|
|
263
|
+
expect(
|
|
264
|
+
() => loadEnvironmentConfig({
|
|
265
|
+
SOCKETDUCT_TLS_CLIENT_CA_FILE: "/certs/client-ca.crt",
|
|
266
|
+
SOCKETDUCT_TLS_KEY_FILE: "/certs/relay.key"
|
|
267
|
+
})
|
|
268
|
+
).toThrow(/SOCKETDUCT_TLS_CLIENT_CA_FILE/)
|
|
269
|
+
})
|
|
318
270
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
SOCKETDUCT_REVERSE_HUB_CA_FILE: "/certs/ca.crt",
|
|
328
|
-
SOCKETDUCT_REVERSE_HUB_CLIENT_CERT_FILE: "/certs/client.crt",
|
|
329
|
-
SOCKETDUCT_REVERSE_HUB_CLIENT_KEY_FILE: "/certs/client.key",
|
|
330
|
-
SOCKETDUCT_REVERSE_HUB_SERVERNAME: "server3.example.test",
|
|
331
|
-
SOCKETDUCT_TARGET_ALLOWLIST: "docker-server:2375"
|
|
332
|
-
})
|
|
333
|
-
assert.equal(config.mode, "reverse-gateway")
|
|
334
|
-
assert.equal(config.reverseGatewayId, "starlink")
|
|
335
|
-
assert.equal(config.reverseConnectionId, "home_starlink_01")
|
|
336
|
-
assert.deepEqual(config.reverseHub, {host: "server3.example.test", port: 443, token: "gateway-token"})
|
|
337
|
-
assert.deepEqual(config.reverseHubTls, {caFile: "/certs/ca.crt", certFile: "/certs/client.crt", keyFile: "/certs/client.key", servername: "server3.example.test"})
|
|
338
|
-
})
|
|
271
|
+
it("isLoopbackHost identifies loopback addresses", () => {
|
|
272
|
+
expect(isLoopbackHost("127.0.0.1")).toBe(true)
|
|
273
|
+
expect(isLoopbackHost("127.0.0.2")).toBe(true)
|
|
274
|
+
expect(isLoopbackHost("127.255.255.255")).toBe(true)
|
|
275
|
+
expect(isLoopbackHost("localhost")).toBe(true)
|
|
276
|
+
expect(isLoopbackHost("::1")).toBe(true)
|
|
277
|
+
expect(isLoopbackHost("[::1]")).toBe(true)
|
|
278
|
+
})
|
|
339
279
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
SOCKETDUCT_TARGET_ALLOWLIST: "docker-server:2375",
|
|
348
|
-
SOCKETDUCT_SPOOL_DIRECTORY: " /spool/reverse-gateway "
|
|
349
|
-
})
|
|
350
|
-
assert.equal(config.spoolDirectory, "/spool/reverse-gateway")
|
|
351
|
-
assert.equal("spoolDirectory" in loadEnvironmentConfig({
|
|
352
|
-
SOCKETDUCT_MODE: "reverse-gateway",
|
|
353
|
-
SOCKETDUCT_REVERSE_GATEWAY_ID: "starlink",
|
|
354
|
-
SOCKETDUCT_REVERSE_CONNECTION_ID: "home_starlink_01",
|
|
355
|
-
SOCKETDUCT_REVERSE_HUB_HOST: "server3.example.test",
|
|
356
|
-
SOCKETDUCT_REVERSE_TOKEN: "gateway-token",
|
|
357
|
-
SOCKETDUCT_TARGET_ALLOWLIST: "docker-server:2375",
|
|
358
|
-
SOCKETDUCT_SPOOL_DIRECTORY: " "
|
|
359
|
-
}), false)
|
|
360
|
-
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_MODE: "relay", SOCKETDUCT_SPOOL_DIRECTORY: "/spool"}), /reverse-gateway-only/)
|
|
361
|
-
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_MODE: "reverse-hub", SOCKETDUCT_SPOOL_DIRECTORY: "/spool"}), /reverse-gateway-only/)
|
|
362
|
-
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_SPOOL_DIRECTORY: "/spool"}), /reverse-gateway-only/)
|
|
363
|
-
})
|
|
280
|
+
it("isLoopbackHost rejects non-loopback addresses", () => {
|
|
281
|
+
expect(isLoopbackHost("0.0.0.0")).toBe(false)
|
|
282
|
+
expect(isLoopbackHost("192.168.1.1")).toBe(false)
|
|
283
|
+
expect(isLoopbackHost("10.0.0.1")).toBe(false)
|
|
284
|
+
expect(isLoopbackHost("172.16.0.1")).toBe(false)
|
|
285
|
+
expect(isLoopbackHost("::ffff:127.0.0.1")).toBe(false)
|
|
286
|
+
})
|
|
364
287
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
288
|
+
it("loadEnvironmentConfig parses reverse hub identity bindings, exact routes, and route-class routes", () => {
|
|
289
|
+
const config = loadEnvironmentConfig({
|
|
290
|
+
SOCKETDUCT_MODE: "reverse-hub",
|
|
291
|
+
SOCKETDUCT_AUTH_TOKENS: "home-o2:gateway-token,tensorbuzz:client-token",
|
|
292
|
+
SOCKETDUCT_REVERSE_GATEWAY_BINDINGS: "o2:home-o2,zyxel:home-zyxel",
|
|
293
|
+
SOCKETDUCT_REVERSE_ROUTES: "tensorbuzz:o2:docker-server:2375,tensorbuzz:zyxel:docker-server:2375",
|
|
294
|
+
SOCKETDUCT_REVERSE_ROUTE_CLASS_ROUTES: "tensorbuzz:starlink:o2:docker-a:2375,tensorbuzz:starlink:zyxel:docker-b:2375",
|
|
295
|
+
SOCKETDUCT_TLS_CERT_FILE: "/certs/hub.crt",
|
|
296
|
+
SOCKETDUCT_TLS_KEY_FILE: "/certs/hub.key"
|
|
297
|
+
})
|
|
298
|
+
expect(config.mode).toBe("reverse-hub")
|
|
299
|
+
expect(config.reverseGatewayBindings).toEqual([{gatewayId: "o2", username: "home-o2"}, {gatewayId: "zyxel", username: "home-zyxel"}])
|
|
300
|
+
expect(config.reverseRoutes).toEqual([
|
|
301
|
+
{username: "tensorbuzz", gatewayId: "o2", host: "docker-server", port: 2375},
|
|
302
|
+
{username: "tensorbuzz", gatewayId: "zyxel", host: "docker-server", port: 2375}
|
|
303
|
+
])
|
|
304
|
+
expect(config.reverseRouteClassRoutes).toEqual([
|
|
305
|
+
{username: "tensorbuzz", routeClass: "starlink", gatewayId: "o2", host: "docker-a", port: 2375},
|
|
306
|
+
{username: "tensorbuzz", routeClass: "starlink", gatewayId: "zyxel", host: "docker-b", port: 2375}
|
|
307
|
+
])
|
|
308
|
+
})
|
|
373
309
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
310
|
+
it("loadEnvironmentConfig parses outbound reverse gateway verified TLS settings", () => {
|
|
311
|
+
const config = loadEnvironmentConfig({
|
|
312
|
+
SOCKETDUCT_MODE: "reverse-gateway",
|
|
313
|
+
SOCKETDUCT_REVERSE_GATEWAY_ID: "starlink",
|
|
314
|
+
SOCKETDUCT_REVERSE_CONNECTION_ID: "home_starlink_01",
|
|
315
|
+
SOCKETDUCT_REVERSE_HUB_HOST: "server3.example.test",
|
|
316
|
+
SOCKETDUCT_REVERSE_HUB_PORT: "443",
|
|
317
|
+
SOCKETDUCT_REVERSE_TOKEN: "gateway-token",
|
|
318
|
+
SOCKETDUCT_REVERSE_HUB_CA_FILE: "/certs/ca.crt",
|
|
319
|
+
SOCKETDUCT_REVERSE_HUB_CLIENT_CERT_FILE: "/certs/client.crt",
|
|
320
|
+
SOCKETDUCT_REVERSE_HUB_CLIENT_KEY_FILE: "/certs/client.key",
|
|
321
|
+
SOCKETDUCT_REVERSE_HUB_SERVERNAME: "server3.example.test",
|
|
322
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "docker-server:2375"
|
|
323
|
+
})
|
|
324
|
+
expect(config.mode).toBe("reverse-gateway")
|
|
325
|
+
expect(config.reverseGatewayId).toBe("starlink")
|
|
326
|
+
expect(config.reverseConnectionId).toBe("home_starlink_01")
|
|
327
|
+
expect(config.reverseHub).toEqual({host: "server3.example.test", port: 443, token: "gateway-token"})
|
|
328
|
+
expect(config.reverseHubTls).toEqual({caFile: "/certs/ca.crt", certFile: "/certs/client.crt", keyFile: "/certs/client.key", servername: "server3.example.test"})
|
|
329
|
+
})
|
|
379
330
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
331
|
+
it("loadEnvironmentConfig parses the reverse-gateway spool directory", () => {
|
|
332
|
+
const config = loadEnvironmentConfig({
|
|
333
|
+
SOCKETDUCT_MODE: "reverse-gateway",
|
|
334
|
+
SOCKETDUCT_REVERSE_GATEWAY_ID: "starlink",
|
|
335
|
+
SOCKETDUCT_REVERSE_CONNECTION_ID: "home_starlink_01",
|
|
336
|
+
SOCKETDUCT_REVERSE_HUB_HOST: "server3.example.test",
|
|
337
|
+
SOCKETDUCT_REVERSE_TOKEN: "gateway-token",
|
|
338
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "docker-server:2375",
|
|
339
|
+
SOCKETDUCT_SPOOL_DIRECTORY: " /spool/reverse-gateway "
|
|
340
|
+
})
|
|
341
|
+
expect(config.spoolDirectory).toBe("/spool/reverse-gateway")
|
|
342
|
+
expect("spoolDirectory" in loadEnvironmentConfig({
|
|
343
|
+
SOCKETDUCT_MODE: "reverse-gateway",
|
|
344
|
+
SOCKETDUCT_REVERSE_GATEWAY_ID: "starlink",
|
|
345
|
+
SOCKETDUCT_REVERSE_CONNECTION_ID: "home_starlink_01",
|
|
346
|
+
SOCKETDUCT_REVERSE_HUB_HOST: "server3.example.test",
|
|
347
|
+
SOCKETDUCT_REVERSE_TOKEN: "gateway-token",
|
|
348
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "docker-server:2375",
|
|
349
|
+
SOCKETDUCT_SPOOL_DIRECTORY: " "
|
|
350
|
+
})).toBe(false)
|
|
351
|
+
expect(() => loadEnvironmentConfig({SOCKETDUCT_MODE: "relay", SOCKETDUCT_SPOOL_DIRECTORY: "/spool"})).toThrow(/reverse-gateway-only/)
|
|
352
|
+
expect(() => loadEnvironmentConfig({SOCKETDUCT_MODE: "reverse-hub", SOCKETDUCT_SPOOL_DIRECTORY: "/spool"})).toThrow(/reverse-gateway-only/)
|
|
353
|
+
expect(() => loadEnvironmentConfig({SOCKETDUCT_SPOOL_DIRECTORY: "/spool"})).toThrow(/reverse-gateway-only/)
|
|
354
|
+
})
|
|
386
355
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
})
|
|
356
|
+
it("loadEnvironmentConfig rejects malformed reverse mode policy and credentials", () => {
|
|
357
|
+
expect(() => loadEnvironmentConfig({SOCKETDUCT_MODE: "unknown"})).toThrow(/Unsupported SOCKETDUCT_MODE/)
|
|
358
|
+
expect(() => loadEnvironmentConfig({SOCKETDUCT_MODE: "reverse-hub", SOCKETDUCT_REVERSE_GATEWAY_BINDINGS: "missing-user"})).toThrow(/gatewayId:username/)
|
|
359
|
+
expect(() => loadEnvironmentConfig({SOCKETDUCT_MODE: "reverse-hub", SOCKETDUCT_REVERSE_ROUTES: "too:few:fields"})).toThrow(/username:gatewayId:host:port/)
|
|
360
|
+
expect(() => loadEnvironmentConfig({SOCKETDUCT_MODE: "reverse-hub", SOCKETDUCT_REVERSE_ROUTE_CLASS_ROUTES: "too:few:fields"})).toThrow(/username:routeClass:gatewayId:host:port/)
|
|
361
|
+
expect(() => loadEnvironmentConfig({SOCKETDUCT_MODE: "reverse-gateway", SOCKETDUCT_REVERSE_TOKEN: "replace-me"})).toThrow(/placeholder/)
|
|
362
|
+
expect(() => loadEnvironmentConfig({SOCKETDUCT_MODE: "reverse-gateway", SOCKETDUCT_REVERSE_HUB_CLIENT_CERT_FILE: "/certs/client.crt"})).toThrow(/must both be set/)
|
|
363
|
+
})
|
|
392
364
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
SOCKETDUCT_MAX_TOTAL_BUFFER_BYTES: "262144",
|
|
398
|
-
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
399
|
-
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
365
|
+
it("isLoopbackHost rejects 127-prefixed hostnames", () => {
|
|
366
|
+
expect(isLoopbackHost("127.0.0.1.nip.io")).toBe(false)
|
|
367
|
+
expect(isLoopbackHost("127localhost")).toBe(false)
|
|
368
|
+
expect(isLoopbackHost("127anything")).toBe(false)
|
|
400
369
|
})
|
|
401
370
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
371
|
+
it("isLoopbackHost rejects invalid loopback-like IPs", () => {
|
|
372
|
+
expect(isLoopbackHost("127.0.0.")).toBe(false)
|
|
373
|
+
expect(isLoopbackHost("127.0.0")).toBe(false)
|
|
374
|
+
expect(isLoopbackHost("127.")).toBe(false)
|
|
375
|
+
expect(isLoopbackHost("127.0.0.0.1")).toBe(false)
|
|
376
|
+
})
|
|
405
377
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
SOCKETDUCT_MAX_DECOMPRESSED_BYTES: "65536",
|
|
412
|
-
SOCKETDUCT_MAX_EXPANSION_RATIO: "50",
|
|
413
|
-
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
414
|
-
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
assert.equal(config.compression, "off")
|
|
418
|
-
assert.equal(config.minCompressBytes, 1024)
|
|
419
|
-
assert.equal(config.maxDecompressedBytes, 65536)
|
|
420
|
-
assert.equal(config.maxExpansionRatio, 50)
|
|
421
|
-
})
|
|
378
|
+
it("isLoopbackHost rejects loopback IPs with invalid octets", () => {
|
|
379
|
+
expect(isLoopbackHost("127.999.999.999")).toBe(false)
|
|
380
|
+
expect(isLoopbackHost("127.256.0.1")).toBe(false)
|
|
381
|
+
expect(isLoopbackHost("127.-1.0.1")).toBe(false)
|
|
382
|
+
})
|
|
422
383
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
() => loadEnvironmentConfig({
|
|
384
|
+
it("loadEnvironmentConfig parses SOCKETDUCT_MAX_TARGET_BUFFER_BYTES and SOCKETDUCT_MAX_TOTAL_BUFFER_BYTES", () => {
|
|
385
|
+
const config = loadEnvironmentConfig({
|
|
426
386
|
SOCKETDUCT_MODE: "relay",
|
|
427
|
-
|
|
387
|
+
SOCKETDUCT_MAX_TARGET_BUFFER_BYTES: "65536",
|
|
388
|
+
SOCKETDUCT_MAX_TOTAL_BUFFER_BYTES: "262144",
|
|
428
389
|
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
429
390
|
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
430
|
-
})
|
|
431
|
-
/Unsupported SOCKETDUCT_COMPRESSION/
|
|
432
|
-
)
|
|
433
|
-
})
|
|
391
|
+
})
|
|
434
392
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
SOCKETDUCT_MODE: "relay",
|
|
438
|
-
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
439
|
-
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
393
|
+
expect(config.maxTargetBufferBytes).toBe(65536)
|
|
394
|
+
expect(config.maxTotalBufferBytes).toBe(262144)
|
|
440
395
|
})
|
|
441
396
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
SOCKETDUCT_MAX_TARGET_BUFFER_BYTES: "0",
|
|
397
|
+
it("loadEnvironmentConfig parses SOCKETDUCT_COMPRESSION and its limits", () => {
|
|
398
|
+
const config = loadEnvironmentConfig({
|
|
399
|
+
SOCKETDUCT_MODE: "relay",
|
|
400
|
+
SOCKETDUCT_COMPRESSION: "off",
|
|
401
|
+
SOCKETDUCT_MIN_COMPRESS_BYTES: "1024",
|
|
402
|
+
SOCKETDUCT_MAX_DECOMPRESSED_BYTES: "65536",
|
|
403
|
+
SOCKETDUCT_MAX_EXPANSION_RATIO: "50",
|
|
450
404
|
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
451
405
|
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
452
|
-
})
|
|
453
|
-
/positive integer/
|
|
454
|
-
)
|
|
406
|
+
})
|
|
455
407
|
|
|
456
|
-
|
|
457
|
-
()
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}),
|
|
462
|
-
/positive integer/
|
|
463
|
-
)
|
|
408
|
+
expect(config.compression).toBe("off")
|
|
409
|
+
expect(config.minCompressBytes).toBe(1024)
|
|
410
|
+
expect(config.maxDecompressedBytes).toBe(65536)
|
|
411
|
+
expect(config.maxExpansionRatio).toBe(50)
|
|
412
|
+
})
|
|
464
413
|
|
|
465
|
-
|
|
466
|
-
(
|
|
467
|
-
|
|
414
|
+
it("loadEnvironmentConfig rejects an unknown SOCKETDUCT_COMPRESSION instead of silently defaulting", () => {
|
|
415
|
+
expect(
|
|
416
|
+
() => loadEnvironmentConfig({
|
|
417
|
+
SOCKETDUCT_MODE: "relay",
|
|
418
|
+
SOCKETDUCT_COMPRESSION: "zstd",
|
|
419
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
420
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
421
|
+
})
|
|
422
|
+
).toThrow(/Unsupported SOCKETDUCT_COMPRESSION/)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it("loadEnvironmentConfig defaults buffer limits when env vars are absent", () => {
|
|
426
|
+
const config = loadEnvironmentConfig({
|
|
427
|
+
SOCKETDUCT_MODE: "relay",
|
|
468
428
|
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
469
429
|
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
470
|
-
})
|
|
471
|
-
/positive integer/
|
|
472
|
-
)
|
|
473
|
-
})
|
|
430
|
+
})
|
|
474
431
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
SOCKETDUCT_MODE: "relay",
|
|
478
|
-
SOCKETDUCT_HANDSHAKE_TIMEOUT_MS: "5000",
|
|
479
|
-
SOCKETDUCT_MAX_INPUT_BYTES_PER_CONNECTION: "32768",
|
|
480
|
-
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
481
|
-
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
432
|
+
expect(config.maxTargetBufferBytes).toBe(1_048_576)
|
|
433
|
+
expect(config.maxTotalBufferBytes).toBe(16_777_216)
|
|
482
434
|
})
|
|
483
435
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
436
|
+
it("loadEnvironmentConfig rejects non-positive buffer limit values", () => {
|
|
437
|
+
expect(
|
|
438
|
+
() => loadEnvironmentConfig({
|
|
439
|
+
SOCKETDUCT_MAX_TARGET_BUFFER_BYTES: "0",
|
|
440
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
441
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
442
|
+
})
|
|
443
|
+
).toThrow(/positive integer/)
|
|
444
|
+
|
|
445
|
+
expect(
|
|
446
|
+
() => loadEnvironmentConfig({
|
|
447
|
+
SOCKETDUCT_MAX_TARGET_BUFFER_BYTES: "-1",
|
|
448
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
449
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
450
|
+
})
|
|
451
|
+
).toThrow(/positive integer/)
|
|
452
|
+
|
|
453
|
+
expect(
|
|
454
|
+
() => loadEnvironmentConfig({
|
|
455
|
+
SOCKETDUCT_MAX_TOTAL_BUFFER_BYTES: "not-a-number",
|
|
456
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
457
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
458
|
+
})
|
|
459
|
+
).toThrow(/positive integer/)
|
|
492
460
|
})
|
|
493
461
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
})
|
|
462
|
+
it("loadEnvironmentConfig parses handshakeTimeoutMs and maxInputBytesPerConnection", () => {
|
|
463
|
+
const config = loadEnvironmentConfig({
|
|
464
|
+
SOCKETDUCT_MODE: "relay",
|
|
465
|
+
SOCKETDUCT_HANDSHAKE_TIMEOUT_MS: "5000",
|
|
466
|
+
SOCKETDUCT_MAX_INPUT_BYTES_PER_CONNECTION: "32768",
|
|
467
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
468
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
469
|
+
})
|
|
502
470
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
() => loadEnvironmentConfig({SOCKETDUCT_INITIAL_TARGET_CONNECT_TIMEOUT_MS: "0"}),
|
|
507
|
-
/SOCKETDUCT_INITIAL_TARGET_CONNECT_TIMEOUT_MS.*positive integer/
|
|
508
|
-
)
|
|
509
|
-
})
|
|
471
|
+
expect(config.handshakeTimeoutMs).toBe(5000)
|
|
472
|
+
expect(config.maxInputBytesPerConnection).toBe(32768)
|
|
473
|
+
})
|
|
510
474
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
475
|
+
it("loadEnvironmentConfig parses and validates initial target retry delays", () => {
|
|
476
|
+
const config = loadEnvironmentConfig({
|
|
477
|
+
SOCKETDUCT_INITIAL_TARGET_RETRY_DELAY_MS: "25",
|
|
478
|
+
SOCKETDUCT_INITIAL_TARGET_RETRY_MAX_DELAY_MS: "200"
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
expect(config.initialTargetRetryDelayMs).toBe(25)
|
|
482
|
+
expect(config.initialTargetRetryMaxDelayMs).toBe(200)
|
|
483
|
+
expect(() => loadEnvironmentConfig({SOCKETDUCT_INITIAL_TARGET_RETRY_DELAY_MS: "0"})).toThrow(/positive integer/)
|
|
484
|
+
expect(() => loadEnvironmentConfig({
|
|
485
|
+
SOCKETDUCT_INITIAL_TARGET_RETRY_DELAY_MS: "500",
|
|
486
|
+
SOCKETDUCT_INITIAL_TARGET_RETRY_MAX_DELAY_MS: "250"
|
|
487
|
+
})).toThrow(/greater than or equal/i)
|
|
516
488
|
})
|
|
517
489
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
490
|
+
it("loadEnvironmentConfig parses and validates the initial target connect timeout", () => {
|
|
491
|
+
expect(loadEnvironmentConfig({SOCKETDUCT_INITIAL_TARGET_CONNECT_TIMEOUT_MS: "750"}).initialTargetConnectTimeoutMs).toBe(750)
|
|
492
|
+
expect(
|
|
493
|
+
() => loadEnvironmentConfig({SOCKETDUCT_INITIAL_TARGET_CONNECT_TIMEOUT_MS: "0"})
|
|
494
|
+
).toThrow(/SOCKETDUCT_INITIAL_TARGET_CONNECT_TIMEOUT_MS.*positive integer/)
|
|
495
|
+
})
|
|
521
496
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
SOCKETDUCT_HANDSHAKE_TIMEOUT_MS: "0",
|
|
497
|
+
it("loadEnvironmentConfig defaults handshakeTimeoutMs and maxInputBytesPerConnection when env vars are absent", () => {
|
|
498
|
+
const config = loadEnvironmentConfig({
|
|
499
|
+
SOCKETDUCT_MODE: "relay",
|
|
526
500
|
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
527
501
|
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
528
|
-
})
|
|
529
|
-
/positive integer/
|
|
530
|
-
)
|
|
502
|
+
})
|
|
531
503
|
|
|
532
|
-
|
|
533
|
-
()
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
504
|
+
expect(config.handshakeTimeoutMs).toBe(10_000)
|
|
505
|
+
expect(config.maxInputBytesPerConnection).toBe(65_536)
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
it("loadEnvironmentConfig rejects non-positive handshakeTimeoutMs and maxInputBytesPerConnection", () => {
|
|
509
|
+
expect(
|
|
510
|
+
() => loadEnvironmentConfig({
|
|
511
|
+
SOCKETDUCT_HANDSHAKE_TIMEOUT_MS: "0",
|
|
512
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
513
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
514
|
+
})
|
|
515
|
+
).toThrow(/positive integer/)
|
|
516
|
+
|
|
517
|
+
expect(
|
|
518
|
+
() => loadEnvironmentConfig({
|
|
519
|
+
SOCKETDUCT_MAX_INPUT_BYTES_PER_CONNECTION: "-1",
|
|
520
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
521
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
522
|
+
})
|
|
523
|
+
).toThrow(/positive integer/)
|
|
524
|
+
})
|
|
541
525
|
|
|
542
|
-
|
|
543
|
-
await t.test("defaults to summary when SOCKETDUCT_MODE is not set", () => {
|
|
526
|
+
it("defaults to summary when SOCKETDUCT_MODE is not set", () => {
|
|
544
527
|
const config = loadEnvironmentConfig({})
|
|
545
|
-
|
|
528
|
+
expect(config.mode).toBe("summary")
|
|
546
529
|
})
|
|
547
530
|
|
|
548
|
-
|
|
531
|
+
it("returns relay when SOCKETDUCT_MODE is relay", () => {
|
|
549
532
|
const config = loadEnvironmentConfig({SOCKETDUCT_MODE: "relay"})
|
|
550
|
-
|
|
533
|
+
expect(config.mode).toBe("relay")
|
|
551
534
|
})
|
|
552
535
|
|
|
553
|
-
|
|
536
|
+
it("returns summary when SOCKETDUCT_MODE is summary", () => {
|
|
554
537
|
const config = loadEnvironmentConfig({SOCKETDUCT_MODE: "summary"})
|
|
555
|
-
|
|
538
|
+
expect(config.mode).toBe("summary")
|
|
556
539
|
})
|
|
557
540
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
() => loadEnvironmentConfig({SOCKETDUCT_MODE: "relya"})
|
|
561
|
-
|
|
562
|
-
)
|
|
541
|
+
it("throws on invalid SOCKETDUCT_MODE", () => {
|
|
542
|
+
expect(
|
|
543
|
+
() => loadEnvironmentConfig({SOCKETDUCT_MODE: "relya"})
|
|
544
|
+
).toThrow(/Unsupported SOCKETDUCT_MODE/)
|
|
563
545
|
})
|
|
564
546
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
() => loadEnvironmentConfig({SOCKETDUCT_MODE: "garbage"})
|
|
568
|
-
|
|
569
|
-
)
|
|
547
|
+
it("throws on arbitrary SOCKETDUCT_MODE value", () => {
|
|
548
|
+
expect(
|
|
549
|
+
() => loadEnvironmentConfig({SOCKETDUCT_MODE: "garbage"})
|
|
550
|
+
).toThrow(/Unsupported SOCKETDUCT_MODE/)
|
|
570
551
|
})
|
|
571
552
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
() => loadEnvironmentConfig({SOCKETDUCT_MODE: "RELAY"})
|
|
575
|
-
|
|
576
|
-
)
|
|
553
|
+
it("mode validation is case-sensitive", () => {
|
|
554
|
+
expect(
|
|
555
|
+
() => loadEnvironmentConfig({SOCKETDUCT_MODE: "RELAY"})
|
|
556
|
+
).toThrow(/Unsupported SOCKETDUCT_MODE/)
|
|
577
557
|
})
|
|
578
558
|
})
|