@push.rocks/smartproxy 27.19.0 → 27.21.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_amd64.tsrust-build.json +14 -0
- package/dist_rust/rustproxy_linux_arm64 +0 -0
- package/dist_rust/rustproxy_linux_arm64.tsrust-build.json +14 -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 +2 -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 +1 -0
- package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.d.ts +5 -1
- package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.js +37 -10
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.d.ts +2 -0
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.js +5 -1
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +5 -5
- 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 +6 -6
- package/readme.metrics.md +7 -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 +2 -0
- package/ts/proxies/smart-proxy/models/route-types.ts +10 -0
- package/ts/proxies/smart-proxy/models/rust-types.ts +1 -0
- package/ts/proxies/smart-proxy/rust-metrics-adapter.ts +30 -8
- package/ts/proxies/smart-proxy/rust-proxy-bridge.ts +5 -0
- package/ts/proxies/smart-proxy/smart-proxy.ts +4 -4
- package/ts/proxies/smart-proxy/socket-handler-server.ts +1 -0
- package/ts/proxies/smart-proxy/utils/route-validator.ts +28 -0
package/readme.metrics.md
CHANGED
|
@@ -4,12 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
Two-tier design separating the data plane from the observation plane:
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**Byte recording (per-chunk):** Byte recording uses `AtomicU64` counters. `CountingBody` batches flushes every 64KB to reduce DashMap shard contention. Per-domain HTTP request recording uses a short tracker mutex; admitting a new domain also takes an admission mutex.
|
|
8
8
|
|
|
9
9
|
**Cold path (1Hz sampling):** A background tokio task drains pending atomics into `ThroughputTracker` circular buffers (Mutex-guarded), producing per-second throughput history. Same task prunes orphaned entries and cleans up rate limiter state.
|
|
10
10
|
|
|
11
11
|
**Read path (on-demand):** `snapshot()` reads all atomics and locks ThroughputTrackers to build a serializable `Metrics` struct. TypeScript polls this at 1s intervals via IPC.
|
|
12
12
|
|
|
13
|
+
Metrics polling allows one outstanding snapshot RPC. Slow polls coalesce; stopping drains the outstanding poll and clears the snapshot/history caches. Native shutdown awaits confirmed child and stdio closure through SmartRust.
|
|
14
|
+
|
|
15
|
+
Global HTTP domain rates retain at most 4,096 canonical domain keys, each at most 253 bytes. Idle trackers expire after their 60 completed one-second samples become empty. `requests.byDomain()` contains individually tracked domains. `requests.domainOverflow?.()` returns `{ perSecond, lastMinute }` for requests that could not receive an individual bucket. Overflow is separate from domain attribution; global request totals remain unchanged. Overflow requests are not retroactively assigned when a bucket later becomes available.
|
|
16
|
+
|
|
17
|
+
Active connection pages retain the smallest `limit + 1` matching IDs in a bounded heap, then clone metadata for those IDs. Work remains a scan of active connections, with O(page size) temporary storage rather than cloning/sorting the full registry. Connections closing during a scan may leave a short page; cursor order and filtering remain best effort under concurrent changes.
|
|
18
|
+
|
|
13
19
|
```
|
|
14
20
|
Data Plane (lock-free) Background (1Hz) Read Path
|
|
15
21
|
───────────────────── ────────────────── ─────────
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartproxy',
|
|
6
|
-
version: '27.
|
|
6
|
+
version: '27.21.0',
|
|
7
7
|
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
|
8
8
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type { TIngressOrigin } from '../../proxies/smart-proxy/models/route-types.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Shared Route Context Interface
|
|
@@ -16,6 +17,7 @@ export interface IRouteContext {
|
|
|
16
17
|
domain?: string; // The domain from SNI or Host header
|
|
17
18
|
clientIp: string; // The client's IP address
|
|
18
19
|
serverIp: string; // The server's IP address
|
|
20
|
+
ingressOrigin?: TIngressOrigin; // Trusted connection-origin classification
|
|
19
21
|
vpn?: { // Authenticated VPN identity from trusted PROXY v2 metadata
|
|
20
22
|
clientId: string;
|
|
21
23
|
assignedIp: string;
|
|
@@ -13,8 +13,17 @@ interface IDatagramRelayMessage {
|
|
|
13
13
|
routeKey: string;
|
|
14
14
|
sourceIp: string;
|
|
15
15
|
sourcePort: number;
|
|
16
|
+
clientIp?: string;
|
|
17
|
+
clientPort?: number;
|
|
16
18
|
destPort: number;
|
|
17
19
|
payloadBase64: string;
|
|
20
|
+
ingressOrigin?: 'direct' | 'trustedProxy' | 'smartVpn';
|
|
21
|
+
vpn?: {
|
|
22
|
+
clientId: string;
|
|
23
|
+
assignedIp: string;
|
|
24
|
+
transportType?: string;
|
|
25
|
+
remoteAddr?: string;
|
|
26
|
+
};
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
/**
|
|
@@ -179,8 +188,10 @@ export class DatagramHandlerServer {
|
|
|
179
188
|
const context: IRouteContext = {
|
|
180
189
|
port: msg.destPort,
|
|
181
190
|
domain: undefined,
|
|
182
|
-
clientIp: msg.sourceIp,
|
|
191
|
+
clientIp: msg.clientIp || msg.sourceIp,
|
|
183
192
|
serverIp: '0.0.0.0',
|
|
193
|
+
ingressOrigin: msg.ingressOrigin,
|
|
194
|
+
vpn: msg.vpn,
|
|
184
195
|
path: undefined,
|
|
185
196
|
isTls: false,
|
|
186
197
|
tlsVersion: undefined,
|
|
@@ -191,8 +202,8 @@ export class DatagramHandlerServer {
|
|
|
191
202
|
};
|
|
192
203
|
|
|
193
204
|
const info: IDatagramInfo = {
|
|
194
|
-
sourceIp: msg.sourceIp,
|
|
195
|
-
sourcePort: msg.sourcePort,
|
|
205
|
+
sourceIp: msg.clientIp || msg.sourceIp,
|
|
206
|
+
sourcePort: msg.clientPort ?? msg.sourcePort,
|
|
196
207
|
destPort: msg.destPort,
|
|
197
208
|
context,
|
|
198
209
|
};
|
|
@@ -101,6 +101,8 @@ export interface IMetrics {
|
|
|
101
101
|
perMinute(): number;
|
|
102
102
|
total(): number;
|
|
103
103
|
byDomain(): Map<string, IRequestRateMetrics>;
|
|
104
|
+
/** Requests beyond the 4096-domain/253-byte key budget, without individual attribution. */
|
|
105
|
+
domainOverflow?(): IRequestRateMetrics;
|
|
104
106
|
};
|
|
105
107
|
|
|
106
108
|
// Cumulative totals
|
|
@@ -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
|
}
|
|
@@ -178,6 +178,7 @@ export interface IRustMetricsSnapshot {
|
|
|
178
178
|
httpRequestsPerSec: number;
|
|
179
179
|
httpRequestsPerSecRecent: number;
|
|
180
180
|
httpDomainRequests: Record<string, IRustHttpDomainRequestMetrics>;
|
|
181
|
+
httpDomainRequestsOverflow?: IRustHttpDomainRequestMetrics;
|
|
181
182
|
activeUdpSessions: number;
|
|
182
183
|
totalUdpSessions: number;
|
|
183
184
|
totalDatagramsIn: number;
|
|
@@ -89,6 +89,9 @@ export class RustMetricsAdapter implements IMetrics {
|
|
|
89
89
|
private historyCache: IRustMetricsHistory | null = null;
|
|
90
90
|
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
91
91
|
private pollIntervalMs: number;
|
|
92
|
+
private pollPromise: Promise<void> | null = null;
|
|
93
|
+
private generation = 0;
|
|
94
|
+
private stopped = false;
|
|
92
95
|
|
|
93
96
|
constructor(bridge: RustProxyBridge, pollIntervalMs = 1000) {
|
|
94
97
|
this.bridge = bridge;
|
|
@@ -98,20 +101,29 @@ export class RustMetricsAdapter implements IMetrics {
|
|
|
98
101
|
/**
|
|
99
102
|
* Poll Rust for metrics once. Can be awaited to ensure cache is fresh.
|
|
100
103
|
*/
|
|
101
|
-
public
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
public poll(): Promise<void> {
|
|
105
|
+
if (this.stopped) return Promise.resolve();
|
|
106
|
+
if (this.pollPromise) return this.pollPromise;
|
|
107
|
+
const generation = this.generation;
|
|
108
|
+
this.pollPromise = (async () => {
|
|
109
|
+
try {
|
|
110
|
+
const metrics = await this.bridge.getMetrics();
|
|
111
|
+
if (!this.stopped && generation === this.generation) this.cache = metrics;
|
|
112
|
+
} catch {
|
|
113
|
+
// A failed poll leaves the last successful snapshot available until stop.
|
|
114
|
+
}
|
|
115
|
+
})().finally(() => { this.pollPromise = null; });
|
|
116
|
+
return this.pollPromise;
|
|
107
117
|
}
|
|
108
118
|
|
|
109
119
|
/**
|
|
110
120
|
* Fetch histories only when an observer needs them; regular polling stays lightweight.
|
|
111
121
|
*/
|
|
112
122
|
public async refreshHistory(seconds: number): Promise<IMetricsHistory> {
|
|
123
|
+
if (this.stopped) throw new Error('Metrics adapter is stopped');
|
|
124
|
+
const generation = this.generation;
|
|
113
125
|
const history = await this.bridge.getMetricsHistory(seconds);
|
|
114
|
-
this.historyCache = history;
|
|
126
|
+
if (!this.stopped && generation === this.generation) this.historyCache = history;
|
|
115
127
|
return {
|
|
116
128
|
throughputHistory: history.throughputHistory.map((point) => ({
|
|
117
129
|
timestamp: point.timestampMs,
|
|
@@ -129,6 +141,7 @@ export class RustMetricsAdapter implements IMetrics {
|
|
|
129
141
|
|
|
130
142
|
public startPolling(): void {
|
|
131
143
|
if (this.pollTimer) return;
|
|
144
|
+
this.stopped = false;
|
|
132
145
|
// Immediate first poll so cache is populated ASAP
|
|
133
146
|
this.poll();
|
|
134
147
|
this.pollTimer = setInterval(() => {
|
|
@@ -139,11 +152,16 @@ export class RustMetricsAdapter implements IMetrics {
|
|
|
139
152
|
}
|
|
140
153
|
}
|
|
141
154
|
|
|
142
|
-
public stopPolling(): void {
|
|
155
|
+
public async stopPolling(): Promise<void> {
|
|
156
|
+
this.stopped = true;
|
|
157
|
+
this.generation++;
|
|
143
158
|
if (this.pollTimer) {
|
|
144
159
|
clearInterval(this.pollTimer);
|
|
145
160
|
this.pollTimer = null;
|
|
146
161
|
}
|
|
162
|
+
this.cache = null;
|
|
163
|
+
this.historyCache = null;
|
|
164
|
+
await this.pollPromise;
|
|
147
165
|
}
|
|
148
166
|
|
|
149
167
|
// --- IMetrics implementation ---
|
|
@@ -322,6 +340,10 @@ export class RustMetricsAdapter implements IMetrics {
|
|
|
322
340
|
total: (): number => {
|
|
323
341
|
return this.cache?.totalHttpRequests ?? this.cache?.totalConnections ?? 0;
|
|
324
342
|
},
|
|
343
|
+
domainOverflow: (): IRequestRateMetrics => ({
|
|
344
|
+
perSecond: this.cache?.httpDomainRequestsOverflow?.requestsPerSecond ?? 0,
|
|
345
|
+
lastMinute: this.cache?.httpDomainRequestsOverflow?.requestsLastMinute ?? 0,
|
|
346
|
+
}),
|
|
325
347
|
byDomain: (): Map<string, IRequestRateMetrics> => {
|
|
326
348
|
const result = new Map<string, IRequestRateMetrics>();
|
|
327
349
|
if (this.cache?.httpDomainRequests) {
|
|
@@ -141,6 +141,11 @@ export class RustProxyBridge extends plugins.EventEmitter {
|
|
|
141
141
|
this.bridge.kill();
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/** Resolve only when the owned native child and its stdio handles have closed. */
|
|
145
|
+
public async terminate(): Promise<void> {
|
|
146
|
+
await this.bridge.terminate();
|
|
147
|
+
}
|
|
148
|
+
|
|
144
149
|
/**
|
|
145
150
|
* Whether the bridge is currently running.
|
|
146
151
|
*/
|
|
@@ -319,7 +319,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
// Stop metrics polling
|
|
322
|
-
this.metricsAdapter.stopPolling();
|
|
322
|
+
await this.metricsAdapter.stopPolling();
|
|
323
323
|
|
|
324
324
|
// Remove exit listener before killing to avoid spurious error events
|
|
325
325
|
this.bridge.removeAllListeners('exit');
|
|
@@ -330,7 +330,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
330
330
|
} catch {
|
|
331
331
|
// Ignore if already stopped
|
|
332
332
|
}
|
|
333
|
-
this.bridge.
|
|
333
|
+
await this.bridge.terminate();
|
|
334
334
|
|
|
335
335
|
// Stop socket handler relay
|
|
336
336
|
if (this.socketHandlerServer) {
|
|
@@ -582,7 +582,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
582
582
|
// --- Private helpers ---
|
|
583
583
|
|
|
584
584
|
private async cleanupRuntimeResourcesAfterStartFailure(didSpawn: boolean): Promise<void> {
|
|
585
|
-
this.metricsAdapter.stopPolling();
|
|
585
|
+
await this.metricsAdapter.stopPolling();
|
|
586
586
|
|
|
587
587
|
if (this.nftablesManager) {
|
|
588
588
|
await this.nftablesManager.cleanup().catch((err) => {
|
|
@@ -616,7 +616,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
616
616
|
if (didSpawn) {
|
|
617
617
|
this.bridge.removeAllListeners('exit');
|
|
618
618
|
await this.bridge.stopProxy().catch(() => undefined);
|
|
619
|
-
this.bridge.
|
|
619
|
+
await this.bridge.terminate();
|
|
620
620
|
}
|
|
621
621
|
}
|
|
622
622
|
|
|
@@ -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');
|