@push.rocks/smartproxy 27.18.0 → 27.20.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 +28 -0
- 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/core/models/route-context.d.ts +2 -0
- package/dist_ts/core/models/route-context.js +1 -1
- package/dist_ts/proxies/smart-proxy/datagram-handler-server.js +6 -4
- package/dist_ts/proxies/smart-proxy/models/metrics-types.d.ts +12 -0
- package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +8 -0
- package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
- package/dist_ts/proxies/smart-proxy/models/rust-types.d.ts +14 -0
- package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.d.ts +8 -1
- package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.js +73 -7
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.d.ts +2 -1
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.js +9 -1
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +5 -1
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +7 -1
- package/dist_ts/proxies/smart-proxy/socket-handler-server.js +2 -1
- package/dist_ts/proxies/smart-proxy/utils/route-validator.js +28 -1
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/core/models/route-context.ts +2 -0
- package/ts/proxies/smart-proxy/datagram-handler-server.ts +14 -3
- package/ts/proxies/smart-proxy/models/metrics-types.ts +14 -0
- package/ts/proxies/smart-proxy/models/route-types.ts +10 -0
- package/ts/proxies/smart-proxy/models/rust-types.ts +16 -0
- package/ts/proxies/smart-proxy/rust-metrics-adapter.ts +110 -7
- package/ts/proxies/smart-proxy/rust-proxy-bridge.ts +14 -0
- package/ts/proxies/smart-proxy/smart-proxy.ts +8 -1
- package/ts/proxies/smart-proxy/socket-handler-server.ts +1 -0
- package/ts/proxies/smart-proxy/utils/route-validator.ts +28 -0
|
@@ -29,6 +29,18 @@ export interface IThroughputHistoryPoint {
|
|
|
29
29
|
out: number;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export interface IProtocolHistoryPoint {
|
|
33
|
+
timestamp: number;
|
|
34
|
+
total: number;
|
|
35
|
+
protocols: Record<string, number>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface IMetricsHistory {
|
|
39
|
+
throughputHistory: IThroughputHistoryPoint[];
|
|
40
|
+
frontendConnectionHistory: IProtocolHistoryPoint[];
|
|
41
|
+
backendConnectionHistory: IProtocolHistoryPoint[];
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
export interface IRequestRateMetrics {
|
|
33
45
|
perSecond: number;
|
|
34
46
|
lastMinute: number;
|
|
@@ -68,6 +80,8 @@ export interface IMetrics {
|
|
|
68
80
|
topDomainRequests(limit?: number): Array<{ ip: string; domain: string; count: number }>;
|
|
69
81
|
frontendProtocols(): IProtocolDistribution;
|
|
70
82
|
backendProtocols(): IProtocolDistribution;
|
|
83
|
+
frontendHistory(seconds: number): Array<IProtocolHistoryPoint>;
|
|
84
|
+
backendHistory(seconds: number): Array<IProtocolHistoryPoint>;
|
|
71
85
|
};
|
|
72
86
|
|
|
73
87
|
// Throughput metrics (bytes per second)
|
|
@@ -24,6 +24,11 @@ export type TTlsMode = 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
|
|
|
24
24
|
*/
|
|
25
25
|
export type TTransportProtocol = 'tcp' | 'udp' | 'all';
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Trusted transport path established by SmartProxy for an accepted connection.
|
|
29
|
+
*/
|
|
30
|
+
export type TIngressOrigin = 'direct' | 'trustedProxy' | 'smartVpn';
|
|
31
|
+
|
|
27
32
|
/**
|
|
28
33
|
* Inbound PROXY protocol handling for a listener.
|
|
29
34
|
*/
|
|
@@ -197,6 +202,11 @@ export interface IRouteSecurity {
|
|
|
197
202
|
allowedAssignedIps?: string[];
|
|
198
203
|
};
|
|
199
204
|
|
|
205
|
+
ingress?: {
|
|
206
|
+
/** Omit ingress entirely for legacy all-origin behavior. */
|
|
207
|
+
allowedOrigins: TIngressOrigin[];
|
|
208
|
+
};
|
|
209
|
+
|
|
200
210
|
/** Browser-facing challenge policy. SmartProxy enforces this in the HTTP path. */
|
|
201
211
|
challenge?: plugins.smartchallenge.IRouteChallengeConfig;
|
|
202
212
|
}
|
|
@@ -105,6 +105,22 @@ export interface IRustThroughputSample {
|
|
|
105
105
|
bytesOut: number;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
export interface IRustProtocolHistorySample {
|
|
109
|
+
timestampMs: number;
|
|
110
|
+
total: number;
|
|
111
|
+
h1Active: number;
|
|
112
|
+
h2Active: number;
|
|
113
|
+
h3Active: number;
|
|
114
|
+
wsActive: number;
|
|
115
|
+
otherActive: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface IRustMetricsHistory {
|
|
119
|
+
throughputHistory: IRustThroughputSample[];
|
|
120
|
+
frontendProtocolHistory: IRustProtocolHistorySample[];
|
|
121
|
+
backendProtocolHistory: IRustProtocolHistorySample[];
|
|
122
|
+
}
|
|
123
|
+
|
|
108
124
|
export interface IRustRouteMetrics {
|
|
109
125
|
activeConnections: number;
|
|
110
126
|
totalConnections: number;
|
|
@@ -1,6 +1,76 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
IMetrics,
|
|
3
|
+
IMetricsHistory,
|
|
4
|
+
IBackendMetrics,
|
|
5
|
+
IProtocolCacheEntry,
|
|
6
|
+
IProtocolDistribution,
|
|
7
|
+
IProtocolHistoryPoint,
|
|
8
|
+
IRequestRateMetrics,
|
|
9
|
+
IThroughputData,
|
|
10
|
+
IThroughputHistoryPoint,
|
|
11
|
+
} from './models/metrics-types.js';
|
|
2
12
|
import type { RustProxyBridge } from './rust-proxy-bridge.js';
|
|
3
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
IRustBackendMetrics,
|
|
15
|
+
IRustHttpDomainRequestMetrics,
|
|
16
|
+
IRustIpMetrics,
|
|
17
|
+
IRustMetricsHistory,
|
|
18
|
+
IRustMetricsSnapshot,
|
|
19
|
+
IRustProtocolHistorySample,
|
|
20
|
+
IRustRouteMetrics,
|
|
21
|
+
} from './models/rust-types.js';
|
|
22
|
+
|
|
23
|
+
function normalizeHistoryWindow(seconds: number, available: number): number {
|
|
24
|
+
if (!Number.isFinite(seconds) || seconds <= 0 || available <= 0) return 0;
|
|
25
|
+
return Math.min(Math.floor(seconds), available);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sliceHistory<T>(points: T[], seconds: number): T[] {
|
|
29
|
+
const window = normalizeHistoryWindow(seconds, points.length);
|
|
30
|
+
return window === 0 ? [] : points.slice(-window);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sliceWallClockHistory<T extends { timestampMs: number }>(
|
|
34
|
+
points: T[],
|
|
35
|
+
seconds: number,
|
|
36
|
+
): T[] {
|
|
37
|
+
const requestedSeconds = Number.isFinite(seconds) ? Math.floor(seconds) : 0;
|
|
38
|
+
if (requestedSeconds <= 0 || points.length === 0) return [];
|
|
39
|
+
|
|
40
|
+
const newestTimestamp = points[points.length - 1].timestampMs;
|
|
41
|
+
const oldestTimestamp = newestTimestamp - (requestedSeconds - 1) * 1_000;
|
|
42
|
+
return points
|
|
43
|
+
.filter((point) => point.timestampMs >= oldestTimestamp)
|
|
44
|
+
.slice(-requestedSeconds);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mergeThroughputHistory(
|
|
48
|
+
cachedHistory: IRustMetricsHistory['throughputHistory'],
|
|
49
|
+
snapshotHistory: IRustMetricsSnapshot['throughputHistory'],
|
|
50
|
+
): IRustMetricsHistory['throughputHistory'] {
|
|
51
|
+
const mergedByTimestamp = new Map<number, IRustMetricsHistory['throughputHistory'][number]>();
|
|
52
|
+
for (const point of cachedHistory) {
|
|
53
|
+
mergedByTimestamp.set(point.timestampMs, point);
|
|
54
|
+
}
|
|
55
|
+
for (const point of snapshotHistory) {
|
|
56
|
+
mergedByTimestamp.set(point.timestampMs, point);
|
|
57
|
+
}
|
|
58
|
+
return [...mergedByTimestamp.values()].sort((a, b) => a.timestampMs - b.timestampMs);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function mapProtocolHistoryPoint(sample: IRustProtocolHistorySample): IProtocolHistoryPoint {
|
|
62
|
+
return {
|
|
63
|
+
timestamp: sample.timestampMs,
|
|
64
|
+
total: sample.total,
|
|
65
|
+
protocols: {
|
|
66
|
+
'HTTP/1.1': sample.h1Active,
|
|
67
|
+
'HTTP/2': sample.h2Active,
|
|
68
|
+
'HTTP/3': sample.h3Active,
|
|
69
|
+
WebSocket: sample.wsActive,
|
|
70
|
+
Other: sample.otherActive,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
4
74
|
|
|
5
75
|
/**
|
|
6
76
|
* Adapts Rust JSON metrics to the IMetrics interface.
|
|
@@ -16,6 +86,7 @@ import type { IRustBackendMetrics, IRustHttpDomainRequestMetrics, IRustIpMetrics
|
|
|
16
86
|
export class RustMetricsAdapter implements IMetrics {
|
|
17
87
|
private bridge: RustProxyBridge;
|
|
18
88
|
private cache: IRustMetricsSnapshot | null = null;
|
|
89
|
+
private historyCache: IRustMetricsHistory | null = null;
|
|
19
90
|
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
20
91
|
private pollIntervalMs: number;
|
|
21
92
|
|
|
@@ -35,6 +106,27 @@ export class RustMetricsAdapter implements IMetrics {
|
|
|
35
106
|
}
|
|
36
107
|
}
|
|
37
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Fetch histories only when an observer needs them; regular polling stays lightweight.
|
|
111
|
+
*/
|
|
112
|
+
public async refreshHistory(seconds: number): Promise<IMetricsHistory> {
|
|
113
|
+
const history = await this.bridge.getMetricsHistory(seconds);
|
|
114
|
+
this.historyCache = history;
|
|
115
|
+
return {
|
|
116
|
+
throughputHistory: history.throughputHistory.map((point) => ({
|
|
117
|
+
timestamp: point.timestampMs,
|
|
118
|
+
in: point.bytesIn,
|
|
119
|
+
out: point.bytesOut,
|
|
120
|
+
})),
|
|
121
|
+
frontendConnectionHistory: history.frontendProtocolHistory.map(
|
|
122
|
+
mapProtocolHistoryPoint,
|
|
123
|
+
),
|
|
124
|
+
backendConnectionHistory: history.backendProtocolHistory.map(
|
|
125
|
+
mapProtocolHistoryPoint,
|
|
126
|
+
),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
38
130
|
public startPolling(): void {
|
|
39
131
|
if (this.pollTimer) return;
|
|
40
132
|
// Immediate first poll so cache is populated ASAP
|
|
@@ -154,6 +246,14 @@ export class RustMetricsAdapter implements IMetrics {
|
|
|
154
246
|
otherTotal: bp?.otherTotal ?? 0,
|
|
155
247
|
};
|
|
156
248
|
},
|
|
249
|
+
frontendHistory: (seconds: number): Array<IProtocolHistoryPoint> => {
|
|
250
|
+
const history = this.historyCache?.frontendProtocolHistory ?? [];
|
|
251
|
+
return sliceHistory(history, seconds).map(mapProtocolHistoryPoint);
|
|
252
|
+
},
|
|
253
|
+
backendHistory: (seconds: number): Array<IProtocolHistoryPoint> => {
|
|
254
|
+
const history = this.historyCache?.backendProtocolHistory ?? [];
|
|
255
|
+
return sliceHistory(history, seconds).map(mapProtocolHistoryPoint);
|
|
256
|
+
},
|
|
157
257
|
};
|
|
158
258
|
|
|
159
259
|
public throughput = {
|
|
@@ -176,11 +276,14 @@ export class RustMetricsAdapter implements IMetrics {
|
|
|
176
276
|
return this.throughput.instant();
|
|
177
277
|
},
|
|
178
278
|
history: (seconds: number): Array<IThroughputHistoryPoint> => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
279
|
+
const history = mergeThroughputHistory(
|
|
280
|
+
this.historyCache?.throughputHistory ?? [],
|
|
281
|
+
this.cache?.throughputHistory ?? [],
|
|
282
|
+
);
|
|
283
|
+
return sliceWallClockHistory(history, seconds).map((point) => ({
|
|
284
|
+
timestamp: point.timestampMs,
|
|
285
|
+
in: point.bytesIn,
|
|
286
|
+
out: point.bytesOut,
|
|
184
287
|
}));
|
|
185
288
|
},
|
|
186
289
|
byRoute: (_windowSeconds?: number): Map<string, IThroughputData> => {
|
|
@@ -5,12 +5,14 @@ import type {
|
|
|
5
5
|
IRustChallengeOptions,
|
|
6
6
|
IRustMetricsSnapshot,
|
|
7
7
|
IRustProxyOptions,
|
|
8
|
+
IRustMetricsHistory,
|
|
8
9
|
IRustRouteConfig,
|
|
9
10
|
IRustStatistics,
|
|
10
11
|
} from './models/rust-types.js';
|
|
11
12
|
import type { IActiveConnectionSnapshot, IActiveConnectionSnapshotOptions, IActiveConnectionSnapshotPage, ISmartProxySecurityPolicy } from './models/interfaces.js';
|
|
12
13
|
|
|
13
14
|
const MAXIMUM_RUST_CONNECTION_ID = 18_446_744_073_709_551_615n;
|
|
15
|
+
const MAXIMUM_METRICS_HISTORY_SECONDS = 600;
|
|
14
16
|
|
|
15
17
|
function validateActiveConnectionSnapshotOptions(options: IActiveConnectionSnapshotOptions): void {
|
|
16
18
|
if (options.afterId === undefined) {
|
|
@@ -33,6 +35,7 @@ type TSmartProxyCommands = {
|
|
|
33
35
|
updateRoutes: { params: { routes: IRustRouteConfig[] }; result: void };
|
|
34
36
|
setSecurityPolicy: { params: { policy: ISmartProxySecurityPolicy }; result: void };
|
|
35
37
|
getMetrics: { params: Record<string, never>; result: IRustMetricsSnapshot };
|
|
38
|
+
getMetricsHistory: { params: { seconds: number }; result: IRustMetricsHistory };
|
|
36
39
|
getStatistics: { params: Record<string, never>; result: IRustStatistics };
|
|
37
40
|
getActiveConnectionSnapshots: { params: IActiveConnectionSnapshotOptions; result: IActiveConnectionSnapshot[] };
|
|
38
41
|
getActiveConnectionSnapshotPage: { params: IActiveConnectionSnapshotOptions; result: IActiveConnectionSnapshotPage };
|
|
@@ -167,6 +170,17 @@ export class RustProxyBridge extends plugins.EventEmitter {
|
|
|
167
170
|
return this.bridge.sendCommand('getMetrics', {} as Record<string, never>);
|
|
168
171
|
}
|
|
169
172
|
|
|
173
|
+
public async getMetricsHistory(seconds: number): Promise<IRustMetricsHistory> {
|
|
174
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
175
|
+
throw new Error('Metrics history seconds must be a finite non-negative number');
|
|
176
|
+
}
|
|
177
|
+
const boundedSeconds = Math.min(
|
|
178
|
+
MAXIMUM_METRICS_HISTORY_SECONDS,
|
|
179
|
+
Math.floor(seconds),
|
|
180
|
+
);
|
|
181
|
+
return this.bridge.sendCommand('getMetricsHistory', { seconds: boundedSeconds });
|
|
182
|
+
}
|
|
183
|
+
|
|
170
184
|
public async getStatistics(): Promise<IRustStatistics> {
|
|
171
185
|
return this.bridge.sendCommand('getStatistics', {} as Record<string, never>);
|
|
172
186
|
}
|
|
@@ -20,7 +20,7 @@ import { ConcurrencySemaphore } from './utils/concurrency-semaphore.js';
|
|
|
20
20
|
// Types
|
|
21
21
|
import type { ISmartProxyOptions, ISmartProxySecurityPolicy, TSmartProxyCertProvisionObject, IAcmeOptions, ICertProvisionEventComms, ICertificateIssuedEvent, ICertificateFailedEvent, ISmartProxyCertProvisionCertificate, IActiveConnectionSnapshot, IActiveConnectionSnapshotOptions, IActiveConnectionSnapshotPage } from './models/interfaces.js';
|
|
22
22
|
import type { IRouteConfig } from './models/route-types.js';
|
|
23
|
-
import type { IMetrics } from './models/metrics-types.js';
|
|
23
|
+
import type { IMetrics, IMetricsHistory } from './models/metrics-types.js';
|
|
24
24
|
import type { IRustCertificateStatus, IRustChallengeOptions, IRustProxyOptions, IRustStatistics } from './models/rust-types.js';
|
|
25
25
|
|
|
26
26
|
type TChallengeProvider = plugins.smartchallenge.IChallengeProvider;
|
|
@@ -496,6 +496,13 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
496
496
|
return this.metricsAdapter;
|
|
497
497
|
}
|
|
498
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Refresh bounded histories on demand without expanding the periodic metrics poll.
|
|
501
|
+
*/
|
|
502
|
+
public async refreshMetricsHistory(seconds: number): Promise<IMetricsHistory> {
|
|
503
|
+
return this.metricsAdapter.refreshHistory(seconds);
|
|
504
|
+
}
|
|
505
|
+
|
|
499
506
|
/**
|
|
500
507
|
* Get sanitized active connection snapshots from the Rust engine.
|
|
501
508
|
*/
|
|
@@ -169,6 +169,7 @@ export class SocketHandlerServer {
|
|
|
169
169
|
domain: metadata.domain,
|
|
170
170
|
clientIp: metadata.remoteIP || 'unknown',
|
|
171
171
|
serverIp: '0.0.0.0',
|
|
172
|
+
ingressOrigin: metadata.ingressOrigin,
|
|
172
173
|
vpn: metadata.vpn,
|
|
173
174
|
path: metadata.path,
|
|
174
175
|
isTls: metadata.isTLS || false,
|
|
@@ -269,6 +269,34 @@ export class RouteValidator {
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
if (route.security.ingress) {
|
|
273
|
+
const origins = route.security.ingress.allowedOrigins;
|
|
274
|
+
if (!Array.isArray(origins) || origins.length === 0) {
|
|
275
|
+
errors.push('security.ingress.allowedOrigins must be a non-empty array');
|
|
276
|
+
} else {
|
|
277
|
+
const supportedOrigins = new Set(['direct', 'trustedProxy', 'smartVpn']);
|
|
278
|
+
const uniqueOrigins = new Set(origins);
|
|
279
|
+
if (uniqueOrigins.size !== origins.length) {
|
|
280
|
+
errors.push('security.ingress.allowedOrigins must not contain duplicates');
|
|
281
|
+
}
|
|
282
|
+
for (const origin of origins) {
|
|
283
|
+
if (!supportedOrigins.has(origin)) {
|
|
284
|
+
errors.push(`Invalid security ingress origin: ${origin}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const inboundMode = route.match.inboundProxyProtocol?.mode;
|
|
289
|
+
const allowsDirect = origins.includes('direct');
|
|
290
|
+
const allowsTrustedProxy = origins.includes('trustedProxy') || origins.includes('smartVpn');
|
|
291
|
+
if (allowsDirect && inboundMode === 'required') {
|
|
292
|
+
errors.push('inboundProxyProtocol.required conflicts with the allowed direct ingress origin');
|
|
293
|
+
}
|
|
294
|
+
if (allowsTrustedProxy && inboundMode === 'reject') {
|
|
295
|
+
errors.push('inboundProxyProtocol.reject conflicts with trustedProxy or smartVpn ingress origins');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
272
300
|
// Validate connection limits
|
|
273
301
|
if (route.security.maxConnections && route.security.maxConnections < 0) {
|
|
274
302
|
errors.push('Max connections must be positive');
|