@push.rocks/smartproxy 27.12.7 → 27.13.0
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/changelog.md +12 -7
- package/dist_rust/rustproxy_linux_amd64 +0 -0
- package/dist_rust/rustproxy_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +1 -1
- package/dist_ts/plugins.d.ts +4 -2
- package/dist_ts/plugins.js +5 -3
- package/dist_ts/proxies/smart-proxy/challenge-provider-relay-server.d.ts +31 -0
- package/dist_ts/proxies/smart-proxy/challenge-provider-relay-server.js +297 -0
- package/dist_ts/proxies/smart-proxy/models/index.d.ts +1 -1
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +14 -2
- package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +11 -0
- package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
- package/dist_ts/proxies/smart-proxy/models/rust-types.d.ts +5 -3
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.d.ts +2 -1
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.js +4 -1
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +19 -0
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +339 -89
- package/dist_ts/proxies/smart-proxy/utils/route-validator.d.ts +3 -0
- package/dist_ts/proxies/smart-proxy/utils/route-validator.js +81 -1
- package/dist_ts/proxies/smart-proxy/utils/rust-config.d.ts +3 -2
- package/dist_ts/proxies/smart-proxy/utils/rust-config.js +20 -4
- package/package.json +2 -1
- package/readme.hints.md +2 -2
- package/readme.md +87 -7
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +1 -1
- package/ts/plugins.ts +4 -1
- package/ts/proxies/smart-proxy/challenge-provider-relay-server.ts +338 -0
- package/ts/proxies/smart-proxy/models/index.ts +1 -1
- package/ts/proxies/smart-proxy/models/interfaces.ts +17 -2
- package/ts/proxies/smart-proxy/models/route-types.ts +17 -0
- package/ts/proxies/smart-proxy/models/rust-types.ts +8 -3
- package/ts/proxies/smart-proxy/rust-proxy-bridge.ts +6 -0
- package/ts/proxies/smart-proxy/smart-proxy.ts +400 -94
- package/ts/proxies/smart-proxy/utils/route-validator.ts +84 -1
- package/ts/proxies/smart-proxy/utils/rust-config.ts +22 -2
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import { logger } from '../../core/utils/logger.js';
|
|
3
|
+
|
|
4
|
+
type TChallengeRelayOperation = 'assess' | 'render' | 'verify';
|
|
5
|
+
|
|
6
|
+
interface IChallengeRelayRequest {
|
|
7
|
+
id?: string;
|
|
8
|
+
operation: TChallengeRelayOperation;
|
|
9
|
+
providerId: string;
|
|
10
|
+
request: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface IChallengeRelayResponse {
|
|
14
|
+
id?: string;
|
|
15
|
+
success: boolean;
|
|
16
|
+
result?: unknown;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const maxChallengeRelayMessageBytes = 1024 * 1024;
|
|
21
|
+
const maxChallengeRelayIdBytes = 256;
|
|
22
|
+
|
|
23
|
+
export interface IChallengeProviderRelayServerOptions {
|
|
24
|
+
providerTimeoutMs?: number;
|
|
25
|
+
maxActiveProviderOperations?: number;
|
|
26
|
+
maxUnsettledProviderOperations?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ChallengeProviderRelayServer {
|
|
30
|
+
private providerTimeoutMs: number;
|
|
31
|
+
private maxActiveProviderOperations: number;
|
|
32
|
+
private maxUnsettledProviderOperations: number;
|
|
33
|
+
private activeProviderOperations = 0;
|
|
34
|
+
private unsettledProviderOperations = 0;
|
|
35
|
+
private server: plugins.net.Server | null = null;
|
|
36
|
+
private socketPath: string;
|
|
37
|
+
private activeSockets = new Set<plugins.net.Socket>();
|
|
38
|
+
private startPromise?: Promise<void>;
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
private providers: Map<string, plugins.smartchallenge.IChallengeProvider>,
|
|
42
|
+
optionsArg: IChallengeProviderRelayServerOptions = {},
|
|
43
|
+
) {
|
|
44
|
+
this.socketPath = `/tmp/smartproxy-challenge-relay-${process.pid}-${plugins.crypto.randomBytes(8).toString('hex')}.sock`;
|
|
45
|
+
this.providerTimeoutMs = Math.max(1, optionsArg.providerTimeoutMs ?? 5_000);
|
|
46
|
+
this.maxActiveProviderOperations = Math.max(1, optionsArg.maxActiveProviderOperations ?? 1024);
|
|
47
|
+
this.maxUnsettledProviderOperations = Math.max(1, optionsArg.maxUnsettledProviderOperations ?? this.maxActiveProviderOperations);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public getSocketPath(): string {
|
|
51
|
+
return this.socketPath;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public async start(): Promise<void> {
|
|
55
|
+
if (this.server) return;
|
|
56
|
+
if (this.startPromise) return this.startPromise;
|
|
57
|
+
this.startPromise = this.startInternal();
|
|
58
|
+
try {
|
|
59
|
+
await this.startPromise;
|
|
60
|
+
} finally {
|
|
61
|
+
this.startPromise = undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private async startInternal(): Promise<void> {
|
|
66
|
+
try {
|
|
67
|
+
await plugins.fs.promises.unlink(this.socketPath);
|
|
68
|
+
} catch {
|
|
69
|
+
// Ignore stale socket cleanup failures for missing files.
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const server = plugins.net.createServer((socket) => {
|
|
73
|
+
this.activeSockets.add(socket);
|
|
74
|
+
socket.once('close', () => this.activeSockets.delete(socket));
|
|
75
|
+
this.handleConnection(socket);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
server.on('error', (err) => {
|
|
79
|
+
logger.log('error', `ChallengeProviderRelayServer error: ${err.message}`, { component: 'challenge-provider-relay-server' });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await new Promise<void>((resolve, reject) => {
|
|
83
|
+
const handleError = (err: Error) => reject(err);
|
|
84
|
+
server.once('error', handleError);
|
|
85
|
+
server.listen(this.socketPath, () => {
|
|
86
|
+
server.off('error', handleError);
|
|
87
|
+
this.server = server;
|
|
88
|
+
logger.log('info', `ChallengeProviderRelayServer listening on ${this.socketPath}`, { component: 'challenge-provider-relay-server' });
|
|
89
|
+
resolve();
|
|
90
|
+
});
|
|
91
|
+
}).catch((err) => {
|
|
92
|
+
server.close();
|
|
93
|
+
throw err;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public async stop(): Promise<void> {
|
|
98
|
+
if (this.startPromise && !this.server) {
|
|
99
|
+
await this.startPromise.catch(() => undefined);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const socket of this.activeSockets) {
|
|
103
|
+
socket.destroy();
|
|
104
|
+
}
|
|
105
|
+
this.activeSockets.clear();
|
|
106
|
+
|
|
107
|
+
const server = this.server;
|
|
108
|
+
if (!server) return;
|
|
109
|
+
this.server = null;
|
|
110
|
+
await new Promise<void>((resolve) => {
|
|
111
|
+
server.close(() => {
|
|
112
|
+
plugins.fs.unlink(this.socketPath, () => resolve());
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private handleConnection(socket: plugins.net.Socket): void {
|
|
118
|
+
let buffer: Buffer<ArrayBufferLike> = Buffer.alloc(0);
|
|
119
|
+
socket.setTimeout(Math.max(10_000, this.providerTimeoutMs + 1_000));
|
|
120
|
+
socket.on('timeout', () => socket.destroy());
|
|
121
|
+
socket.on('error', (err) => {
|
|
122
|
+
logger.log('warn', `Challenge relay socket error: ${err.message}`, { component: 'challenge-provider-relay-server' });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
socket.on('data', (chunk: Buffer) => {
|
|
126
|
+
buffer = buffer.length === 0 ? chunk : Buffer.concat([buffer, chunk], buffer.length + chunk.length);
|
|
127
|
+
if (buffer.length > maxChallengeRelayMessageBytes) {
|
|
128
|
+
socket.removeAllListeners('data');
|
|
129
|
+
this.writeResponse(socket, { success: false, error: 'Challenge relay request exceeds maximum size' });
|
|
130
|
+
buffer = Buffer.alloc(0);
|
|
131
|
+
socket.end();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const newlineIndex = buffer.indexOf(0x0a);
|
|
136
|
+
if (newlineIndex === -1) return;
|
|
137
|
+
|
|
138
|
+
const line = buffer.subarray(0, newlineIndex).toString('utf8');
|
|
139
|
+
socket.removeAllListeners('data');
|
|
140
|
+
this.dispatchLine(line)
|
|
141
|
+
.then((response) => this.writeResponse(socket, response))
|
|
142
|
+
.catch((err) => this.writeResponse(socket, { success: false, error: (err as Error).message }))
|
|
143
|
+
.finally(() => socket.end());
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private async dispatchLine(lineArg: string): Promise<IChallengeRelayResponse> {
|
|
148
|
+
let request: IChallengeRelayRequest;
|
|
149
|
+
try {
|
|
150
|
+
request = JSON.parse(lineArg) as IChallengeRelayRequest;
|
|
151
|
+
} catch {
|
|
152
|
+
return { success: false, error: 'Invalid challenge relay JSON' };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const provider = this.providers.get(request.providerId);
|
|
156
|
+
if (!provider) {
|
|
157
|
+
return {
|
|
158
|
+
id: request.id,
|
|
159
|
+
success: false,
|
|
160
|
+
error: `Challenge provider '${request.providerId}' is not registered`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
switch (request.operation) {
|
|
165
|
+
case 'assess':
|
|
166
|
+
return { id: request.id, success: true, result: await this.withProviderTimeout(() => provider.assess(request.request as plugins.smartchallenge.IChallengeAssessRequest), 'assess') };
|
|
167
|
+
case 'render':
|
|
168
|
+
return { id: request.id, success: true, result: await this.withProviderTimeout(() => provider.render(request.request as plugins.smartchallenge.IChallengeRenderRequest), 'render') };
|
|
169
|
+
case 'verify':
|
|
170
|
+
return { id: request.id, success: true, result: await this.withProviderTimeout(() => provider.verify(request.request as plugins.smartchallenge.IChallengeVerifyRequest), 'verify') };
|
|
171
|
+
default:
|
|
172
|
+
const operationValue = (request as { operation?: unknown }).operation;
|
|
173
|
+
return { id: request.id, success: false, error: `Unsupported challenge relay operation '${String(operationValue)}'` };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private async withProviderTimeout<T>(operationArg: () => Promise<T>, operationNameArg: TChallengeRelayOperation): Promise<T> {
|
|
178
|
+
if (this.activeProviderOperations >= this.maxActiveProviderOperations) {
|
|
179
|
+
throw new Error(`Challenge provider ${operationNameArg} rejected because too many operations are active`);
|
|
180
|
+
}
|
|
181
|
+
if (this.unsettledProviderOperations >= this.maxUnsettledProviderOperations) {
|
|
182
|
+
throw new Error(`Challenge provider ${operationNameArg} rejected because too many timed-out operations are still settling`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
186
|
+
let activeReleased = false;
|
|
187
|
+
let unsettledReleased = false;
|
|
188
|
+
const releaseActive = () => {
|
|
189
|
+
if (!activeReleased) {
|
|
190
|
+
activeReleased = true;
|
|
191
|
+
this.activeProviderOperations -= 1;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const releaseUnsettled = () => {
|
|
195
|
+
if (!unsettledReleased) {
|
|
196
|
+
unsettledReleased = true;
|
|
197
|
+
this.unsettledProviderOperations -= 1;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
this.activeProviderOperations += 1;
|
|
201
|
+
this.unsettledProviderOperations += 1;
|
|
202
|
+
const providerPromise = Promise.resolve().then(operationArg);
|
|
203
|
+
const trackedProviderPromise = providerPromise.then(
|
|
204
|
+
(result) => {
|
|
205
|
+
releaseActive();
|
|
206
|
+
releaseUnsettled();
|
|
207
|
+
return result;
|
|
208
|
+
},
|
|
209
|
+
(err) => {
|
|
210
|
+
releaseActive();
|
|
211
|
+
releaseUnsettled();
|
|
212
|
+
throw err;
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
trackedProviderPromise.catch(() => undefined);
|
|
216
|
+
try {
|
|
217
|
+
return await Promise.race([
|
|
218
|
+
trackedProviderPromise,
|
|
219
|
+
new Promise<T>((_resolve, reject) => {
|
|
220
|
+
timer = setTimeout(
|
|
221
|
+
() => {
|
|
222
|
+
releaseActive();
|
|
223
|
+
reject(new Error(`Challenge provider ${operationNameArg} timed out after ${this.providerTimeoutMs}ms`));
|
|
224
|
+
},
|
|
225
|
+
this.providerTimeoutMs,
|
|
226
|
+
);
|
|
227
|
+
}),
|
|
228
|
+
]);
|
|
229
|
+
} finally {
|
|
230
|
+
if (timer) clearTimeout(timer);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private writeResponse(socket: plugins.net.Socket, responseArg: IChallengeRelayResponse): void {
|
|
235
|
+
let response = responseArg;
|
|
236
|
+
if (this.estimateJsonByteLength(response, maxChallengeRelayMessageBytes) === undefined) {
|
|
237
|
+
response = this.createErrorResponse(responseArg.id, 'Challenge relay response exceeds maximum size');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let serializedResponse: string;
|
|
241
|
+
try {
|
|
242
|
+
serializedResponse = JSON.stringify(response);
|
|
243
|
+
} catch {
|
|
244
|
+
serializedResponse = JSON.stringify(this.createErrorResponse(responseArg.id, 'Challenge relay response is not serializable'));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (Buffer.byteLength(serializedResponse, 'utf8') > maxChallengeRelayMessageBytes) {
|
|
248
|
+
serializedResponse = JSON.stringify({
|
|
249
|
+
success: false,
|
|
250
|
+
error: 'Challenge relay response exceeds maximum size',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
socket.write(`${serializedResponse}\n`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private createErrorResponse(idArg: string | undefined, errorArg: string): IChallengeRelayResponse {
|
|
258
|
+
const response: IChallengeRelayResponse = {
|
|
259
|
+
success: false,
|
|
260
|
+
error: errorArg,
|
|
261
|
+
};
|
|
262
|
+
if (idArg && Buffer.byteLength(idArg, 'utf8') <= maxChallengeRelayIdBytes) {
|
|
263
|
+
response.id = idArg;
|
|
264
|
+
}
|
|
265
|
+
return response;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private estimateJsonByteLength(
|
|
269
|
+
valueArg: unknown,
|
|
270
|
+
maxBytesArg: number,
|
|
271
|
+
seenArg = new Set<object>(),
|
|
272
|
+
): number | undefined {
|
|
273
|
+
const bytes = this.estimateJsonByteLengthInternal(valueArg, maxBytesArg, seenArg);
|
|
274
|
+
return bytes !== undefined && bytes <= maxBytesArg ? bytes : undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private estimateJsonByteLengthInternal(
|
|
278
|
+
valueArg: unknown,
|
|
279
|
+
maxBytesArg: number,
|
|
280
|
+
seenArg: Set<object>,
|
|
281
|
+
): number | undefined {
|
|
282
|
+
if (valueArg === null) return 4;
|
|
283
|
+
if (typeof valueArg === 'string') return this.estimateJsonStringByteLength(valueArg, maxBytesArg);
|
|
284
|
+
if (typeof valueArg === 'number') return Number.isFinite(valueArg) ? String(valueArg).length : 4;
|
|
285
|
+
if (typeof valueArg === 'boolean') return valueArg ? 4 : 5;
|
|
286
|
+
if (typeof valueArg === 'undefined') return 4;
|
|
287
|
+
if (typeof valueArg === 'bigint' || typeof valueArg === 'symbol' || typeof valueArg === 'function') return undefined;
|
|
288
|
+
if (typeof valueArg !== 'object') return undefined;
|
|
289
|
+
if (seenArg.has(valueArg)) return undefined;
|
|
290
|
+
|
|
291
|
+
seenArg.add(valueArg);
|
|
292
|
+
try {
|
|
293
|
+
let totalBytes = 2;
|
|
294
|
+
if (Array.isArray(valueArg)) {
|
|
295
|
+
for (let i = 0; i < valueArg.length; i++) {
|
|
296
|
+
const childBytes = this.estimateJsonByteLengthInternal(valueArg[i], maxBytesArg, seenArg);
|
|
297
|
+
if (childBytes === undefined) return undefined;
|
|
298
|
+
totalBytes += childBytes + (i > 0 ? 1 : 0);
|
|
299
|
+
if (totalBytes > maxBytesArg) return undefined;
|
|
300
|
+
}
|
|
301
|
+
return totalBytes;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let propertyCount = 0;
|
|
305
|
+
for (const [key, value] of Object.entries(valueArg as Record<string, unknown>)) {
|
|
306
|
+
if (typeof value === 'undefined' || typeof value === 'function' || typeof value === 'symbol') continue;
|
|
307
|
+
const keyBytes = this.estimateJsonStringByteLength(key, maxBytesArg);
|
|
308
|
+
const valueBytes = this.estimateJsonByteLengthInternal(value, maxBytesArg, seenArg);
|
|
309
|
+
if (keyBytes === undefined || valueBytes === undefined) return undefined;
|
|
310
|
+
totalBytes += keyBytes + valueBytes + 1 + (propertyCount > 0 ? 1 : 0);
|
|
311
|
+
propertyCount += 1;
|
|
312
|
+
if (totalBytes > maxBytesArg) return undefined;
|
|
313
|
+
}
|
|
314
|
+
return totalBytes;
|
|
315
|
+
} finally {
|
|
316
|
+
seenArg.delete(valueArg);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private estimateJsonStringByteLength(valueArg: string, maxBytesArg: number): number | undefined {
|
|
321
|
+
let bytes = Buffer.byteLength(valueArg, 'utf8') + 2;
|
|
322
|
+
if (bytes > maxBytesArg) return undefined;
|
|
323
|
+
|
|
324
|
+
for (let i = 0; i < valueArg.length; i++) {
|
|
325
|
+
const code = valueArg.charCodeAt(i);
|
|
326
|
+
if (code === 0x22 || code === 0x5c) {
|
|
327
|
+
bytes += 1;
|
|
328
|
+
} else if (code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d) {
|
|
329
|
+
bytes += 1;
|
|
330
|
+
} else if (code < 0x20) {
|
|
331
|
+
bytes += 5;
|
|
332
|
+
}
|
|
333
|
+
if (bytes > maxBytesArg) return undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return bytes;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* SmartProxy models
|
|
3
3
|
*/
|
|
4
4
|
// Export everything except IAcmeOptions from interfaces
|
|
5
|
-
export type { ISmartProxyOptions, ISmartProxySecurityPolicy, ISmartProxyCertStore, IConnectionRecord, TSmartProxyCertProvisionObject, ICertProvisionEventComms, ICertificateIssuedEvent, ICertificateFailedEvent, IActiveConnectionSnapshot, IActiveConnectionSnapshotOptions } from './interfaces.js';
|
|
5
|
+
export type { ISmartProxyOptions, ISmartProxySecurityPolicy, ISmartProxyChallengeOptions, ISmartProxyCertStore, IConnectionRecord, TChallengeProviderRegistry, TSmartProxyCertProvisionObject, ICertProvisionEventComms, ICertificateIssuedEvent, ICertificateFailedEvent, IActiveConnectionSnapshot, IActiveConnectionSnapshotOptions } from './interfaces.js';
|
|
6
6
|
export * from './route-types.js';
|
|
7
7
|
export * from './metrics-types.js';
|
|
@@ -34,6 +34,17 @@ export interface ISmartProxySecurityPolicy {
|
|
|
34
34
|
blockedCidrs?: string[];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export interface ISmartProxyChallengeOptions {
|
|
38
|
+
/** Runtime-only cookie signing key. If omitted, SmartProxy generates an ephemeral key. */
|
|
39
|
+
cookieSigningKey?: string;
|
|
40
|
+
pendingCookieName?: string;
|
|
41
|
+
clearanceCookieName?: string;
|
|
42
|
+
reservedPathPrefix?: string;
|
|
43
|
+
relaySocketPath?: string;
|
|
44
|
+
relayTimeoutMs?: number;
|
|
45
|
+
pendingTtlSeconds?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
/**
|
|
38
49
|
* Provision object for static or HTTP-01 certificate
|
|
39
50
|
*/
|
|
@@ -112,8 +123,7 @@ export interface ISmartProxyOptions {
|
|
|
112
123
|
preserveSourceIP?: boolean; // Preserve client IP when forwarding
|
|
113
124
|
|
|
114
125
|
// PROXY protocol configuration
|
|
115
|
-
|
|
116
|
-
acceptProxyProtocol?: boolean; // Global option to accept PROXY protocol (defaults based on proxyIPs)
|
|
126
|
+
trustedProxyIPs?: string[]; // Global trusted IP/CIDR list for inbound PROXY protocol
|
|
117
127
|
sendProxyProtocol?: boolean; // Global option to send PROXY protocol to all targets
|
|
118
128
|
|
|
119
129
|
// Global/default settings
|
|
@@ -172,6 +182,9 @@ export interface ISmartProxyOptions {
|
|
|
172
182
|
connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
|
|
173
183
|
securityPolicy?: ISmartProxySecurityPolicy; // Global ingress block policy, enforced before routing
|
|
174
184
|
|
|
185
|
+
/** Runtime-only challenge enforcement settings. Never store this in route configs. */
|
|
186
|
+
challenge?: ISmartProxyChallengeOptions;
|
|
187
|
+
|
|
175
188
|
// Enhanced keep-alive settings
|
|
176
189
|
keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections
|
|
177
190
|
keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections
|
|
@@ -250,6 +263,8 @@ export interface ISmartProxyOptions {
|
|
|
250
263
|
rustBinaryPath?: string;
|
|
251
264
|
}
|
|
252
265
|
|
|
266
|
+
export type TChallengeProviderRegistry = Map<string, plugins.smartchallenge.IChallengeProvider>;
|
|
267
|
+
|
|
253
268
|
/**
|
|
254
269
|
* Enhanced connection record
|
|
255
270
|
*/
|
|
@@ -24,6 +24,16 @@ export type TTlsMode = 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
|
|
|
24
24
|
*/
|
|
25
25
|
export type TTransportProtocol = 'tcp' | 'udp' | 'all';
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Inbound PROXY protocol handling for a listener.
|
|
29
|
+
*/
|
|
30
|
+
export type TInboundProxyProtocolMode = 'reject' | 'optional' | 'required';
|
|
31
|
+
|
|
32
|
+
export interface IInboundProxyProtocolPolicy {
|
|
33
|
+
mode: TInboundProxyProtocolMode;
|
|
34
|
+
trustedProxyIPs?: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
/**
|
|
28
38
|
* Port range specification format.
|
|
29
39
|
* Supports: single number, array of numbers, array of ranges, or mixed arrays.
|
|
@@ -40,6 +50,10 @@ export interface IRouteMatch {
|
|
|
40
50
|
// Transport protocol: 'tcp' (default), 'udp', or 'all' (both TCP and UDP)
|
|
41
51
|
transport?: TTransportProtocol;
|
|
42
52
|
|
|
53
|
+
// Inbound PROXY protocol policy for this listener. Routes sharing the same
|
|
54
|
+
// port/transport must declare the same policy.
|
|
55
|
+
inboundProxyProtocol?: IInboundProxyProtocolPolicy;
|
|
56
|
+
|
|
43
57
|
// Optional domain patterns to match (default: all domains)
|
|
44
58
|
domains?: string | string[];
|
|
45
59
|
|
|
@@ -182,6 +196,9 @@ export interface IRouteSecurity {
|
|
|
182
196
|
/** Allowed VPN tunnel IPs, kept for compatibility. Prefer allowedClients. */
|
|
183
197
|
allowedAssignedIps?: string[];
|
|
184
198
|
};
|
|
199
|
+
|
|
200
|
+
/** Browser-facing challenge policy. SmartProxy enforces this in the HTTP path. */
|
|
201
|
+
challenge?: plugins.smartchallenge.IRouteChallengeConfig;
|
|
185
202
|
}
|
|
186
203
|
|
|
187
204
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IProtocolCacheEntry, IProtocolDistribution } from './metrics-types.js';
|
|
2
|
-
import type { IAcmeOptions, ISmartProxyOptions, ISmartProxySecurityPolicy } from './interfaces.js';
|
|
2
|
+
import type { IAcmeOptions, ISmartProxyChallengeOptions, ISmartProxyOptions, ISmartProxySecurityPolicy } from './interfaces.js';
|
|
3
3
|
import type {
|
|
4
4
|
IRouteAction,
|
|
5
5
|
IRouteConfig,
|
|
@@ -46,11 +46,15 @@ export interface IRustRouteConfig extends Omit<IRouteConfig, 'match' | 'action'>
|
|
|
46
46
|
|
|
47
47
|
export interface IRustAcmeOptions extends Omit<IAcmeOptions, 'routeForwards'> {}
|
|
48
48
|
|
|
49
|
+
export interface IRustChallengeOptions extends Required<Pick<
|
|
50
|
+
ISmartProxyChallengeOptions,
|
|
51
|
+
'cookieSigningKey' | 'pendingCookieName' | 'clearanceCookieName' | 'reservedPathPrefix' | 'relaySocketPath' | 'relayTimeoutMs' | 'pendingTtlSeconds'
|
|
52
|
+
>> {}
|
|
53
|
+
|
|
49
54
|
export interface IRustProxyOptions {
|
|
50
55
|
routes: IRustRouteConfig[];
|
|
51
56
|
preserveSourceIp?: boolean;
|
|
52
|
-
|
|
53
|
-
acceptProxyProtocol?: boolean;
|
|
57
|
+
trustedProxyIPs?: string[];
|
|
54
58
|
sendProxyProtocol?: boolean;
|
|
55
59
|
defaults?: IRustDefaultConfig;
|
|
56
60
|
connectionTimeout?: number;
|
|
@@ -76,6 +80,7 @@ export interface IRustProxyOptions {
|
|
|
76
80
|
extendedKeepAliveLifetime?: number;
|
|
77
81
|
metrics?: ISmartProxyOptions['metrics'];
|
|
78
82
|
securityPolicy?: ISmartProxySecurityPolicy;
|
|
83
|
+
challenge?: IRustChallengeOptions;
|
|
79
84
|
acme?: IRustAcmeOptions;
|
|
80
85
|
}
|
|
81
86
|
|
|
@@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
|
|
|
2
2
|
import { logger } from '../../core/utils/logger.js';
|
|
3
3
|
import type {
|
|
4
4
|
IRustCertificateStatus,
|
|
5
|
+
IRustChallengeOptions,
|
|
5
6
|
IRustMetricsSnapshot,
|
|
6
7
|
IRustProxyOptions,
|
|
7
8
|
IRustRouteConfig,
|
|
@@ -25,6 +26,7 @@ type TSmartProxyCommands = {
|
|
|
25
26
|
getCertificateStatus: { params: { routeName: string }; result: IRustCertificateStatus | null };
|
|
26
27
|
getListeningPorts: { params: Record<string, never>; result: { ports: number[] } };
|
|
27
28
|
setSocketHandlerRelay: { params: { socketPath: string }; result: void };
|
|
29
|
+
setChallengeProviderRelay: { params: { socketPath: string; options?: IRustChallengeOptions }; result: void };
|
|
28
30
|
addListeningPort: { params: { port: number }; result: void };
|
|
29
31
|
removeListeningPort: { params: { port: number }; result: void };
|
|
30
32
|
loadCertificate: { params: { domain: string; cert: string; key: string; ca?: string }; result: void };
|
|
@@ -181,6 +183,10 @@ export class RustProxyBridge extends plugins.EventEmitter {
|
|
|
181
183
|
await this.bridge.sendCommand('setSocketHandlerRelay', { socketPath });
|
|
182
184
|
}
|
|
183
185
|
|
|
186
|
+
public async setChallengeProviderRelay(socketPath: string, options?: IRustChallengeOptions): Promise<void> {
|
|
187
|
+
await this.bridge.sendCommand('setChallengeProviderRelay', { socketPath, options });
|
|
188
|
+
}
|
|
189
|
+
|
|
184
190
|
public async addListeningPort(port: number): Promise<void> {
|
|
185
191
|
await this.bridge.sendCommand('addListeningPort', { port });
|
|
186
192
|
}
|