@push.rocks/smartproxy 22.4.2 → 22.6.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 +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +1 -5
- package/dist_ts/index.js +3 -9
- package/dist_ts/protocols/common/fragment-handler.js +5 -1
- package/dist_ts/proxies/index.d.ts +1 -5
- package/dist_ts/proxies/index.js +2 -6
- package/dist_ts/proxies/smart-proxy/index.d.ts +5 -10
- package/dist_ts/proxies/smart-proxy/index.js +7 -13
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +5 -2
- package/dist_ts/proxies/smart-proxy/route-preprocessor.d.ts +37 -0
- package/dist_ts/proxies/smart-proxy/route-preprocessor.js +103 -0
- package/dist_ts/proxies/smart-proxy/rust-binary-locator.d.ts +23 -0
- package/dist_ts/proxies/smart-proxy/rust-binary-locator.js +104 -0
- package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.d.ts +74 -0
- package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.js +146 -0
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.d.ts +49 -0
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.js +259 -0
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +39 -157
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +224 -621
- package/dist_ts/proxies/smart-proxy/socket-handler-server.d.ts +45 -0
- package/dist_ts/proxies/smart-proxy/socket-handler-server.js +253 -0
- package/dist_ts/routing/index.d.ts +1 -1
- package/dist_ts/routing/index.js +3 -3
- package/dist_ts/routing/models/http-types.d.ts +119 -4
- package/dist_ts/routing/models/http-types.js +93 -5
- package/package.json +1 -1
- package/readme.md +470 -219
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +4 -12
- package/ts/protocols/common/fragment-handler.ts +4 -0
- package/ts/proxies/index.ts +1 -9
- package/ts/proxies/smart-proxy/index.ts +6 -13
- package/ts/proxies/smart-proxy/models/interfaces.ts +6 -4
- package/ts/proxies/smart-proxy/route-preprocessor.ts +122 -0
- package/ts/proxies/smart-proxy/rust-binary-locator.ts +112 -0
- package/ts/proxies/smart-proxy/rust-metrics-adapter.ts +161 -0
- package/ts/proxies/smart-proxy/rust-proxy-bridge.ts +310 -0
- package/ts/proxies/smart-proxy/smart-proxy.ts +282 -798
- package/ts/proxies/smart-proxy/socket-handler-server.ts +279 -0
- package/ts/routing/index.ts +2 -2
- package/ts/routing/models/http-types.ts +147 -4
- package/ts/proxies/http-proxy/connection-pool.ts +0 -228
- package/ts/proxies/http-proxy/context-creator.ts +0 -145
- package/ts/proxies/http-proxy/default-certificates.ts +0 -150
- package/ts/proxies/http-proxy/function-cache.ts +0 -279
- package/ts/proxies/http-proxy/handlers/index.ts +0 -5
- package/ts/proxies/http-proxy/http-proxy.ts +0 -669
- package/ts/proxies/http-proxy/http-request-handler.ts +0 -331
- package/ts/proxies/http-proxy/http2-request-handler.ts +0 -255
- package/ts/proxies/http-proxy/index.ts +0 -18
- package/ts/proxies/http-proxy/models/http-types.ts +0 -148
- package/ts/proxies/http-proxy/models/index.ts +0 -5
- package/ts/proxies/http-proxy/models/types.ts +0 -125
- package/ts/proxies/http-proxy/request-handler.ts +0 -878
- package/ts/proxies/http-proxy/security-manager.ts +0 -413
- package/ts/proxies/http-proxy/websocket-handler.ts +0 -581
- package/ts/proxies/smart-proxy/acme-state-manager.ts +0 -112
- package/ts/proxies/smart-proxy/cert-store.ts +0 -92
- package/ts/proxies/smart-proxy/certificate-manager.ts +0 -895
- package/ts/proxies/smart-proxy/connection-manager.ts +0 -809
- package/ts/proxies/smart-proxy/http-proxy-bridge.ts +0 -213
- package/ts/proxies/smart-proxy/metrics-collector.ts +0 -453
- package/ts/proxies/smart-proxy/nftables-manager.ts +0 -271
- package/ts/proxies/smart-proxy/port-manager.ts +0 -358
- package/ts/proxies/smart-proxy/route-connection-handler.ts +0 -1712
- package/ts/proxies/smart-proxy/route-orchestrator.ts +0 -297
- package/ts/proxies/smart-proxy/security-manager.ts +0 -269
- package/ts/proxies/smart-proxy/throughput-tracker.ts +0 -138
- package/ts/proxies/smart-proxy/timeout-manager.ts +0 -196
- package/ts/proxies/smart-proxy/tls-manager.ts +0 -171
|
@@ -1,940 +1,424 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import { logger } from '../../core/utils/logger.js';
|
|
3
|
-
import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
|
|
4
|
-
|
|
5
|
-
// Importing required components
|
|
6
|
-
import { ConnectionManager } from './connection-manager.js';
|
|
7
|
-
import { SecurityManager } from './security-manager.js';
|
|
8
|
-
import { TlsManager } from './tls-manager.js';
|
|
9
|
-
import { HttpProxyBridge } from './http-proxy-bridge.js';
|
|
10
|
-
import { TimeoutManager } from './timeout-manager.js';
|
|
11
|
-
import { PortManager } from './port-manager.js';
|
|
12
|
-
import { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js';
|
|
13
|
-
import { RouteConnectionHandler } from './route-connection-handler.js';
|
|
14
|
-
import { NFTablesManager } from './nftables-manager.js';
|
|
15
|
-
|
|
16
|
-
// Certificate manager
|
|
17
|
-
import { SmartCertManager, type ICertStatus } from './certificate-manager.js';
|
|
18
|
-
|
|
19
|
-
// Import types and utilities
|
|
20
|
-
import type {
|
|
21
|
-
ISmartProxyOptions
|
|
22
|
-
} from './models/interfaces.js';
|
|
23
|
-
import type { IRouteConfig } from './models/route-types.js';
|
|
24
3
|
|
|
25
|
-
//
|
|
26
|
-
import {
|
|
4
|
+
// Rust bridge and helpers
|
|
5
|
+
import { RustProxyBridge } from './rust-proxy-bridge.js';
|
|
6
|
+
import { RustBinaryLocator } from './rust-binary-locator.js';
|
|
7
|
+
import { RoutePreprocessor } from './route-preprocessor.js';
|
|
8
|
+
import { SocketHandlerServer } from './socket-handler-server.js';
|
|
9
|
+
import { RustMetricsAdapter } from './rust-metrics-adapter.js';
|
|
27
10
|
|
|
28
|
-
//
|
|
11
|
+
// Route management
|
|
12
|
+
import { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js';
|
|
29
13
|
import { RouteValidator } from './utils/route-validator.js';
|
|
14
|
+
import { Mutex } from './utils/mutex.js';
|
|
30
15
|
|
|
31
|
-
//
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
// Import ACME state manager
|
|
35
|
-
import { AcmeStateManager } from './acme-state-manager.js';
|
|
36
|
-
|
|
37
|
-
// Import metrics collector
|
|
38
|
-
import { MetricsCollector } from './metrics-collector.js';
|
|
16
|
+
// Types
|
|
17
|
+
import type { ISmartProxyOptions, TSmartProxyCertProvisionObject } from './models/interfaces.js';
|
|
18
|
+
import type { IRouteConfig } from './models/route-types.js';
|
|
39
19
|
import type { IMetrics } from './models/metrics-types.js';
|
|
40
20
|
|
|
41
21
|
/**
|
|
42
|
-
* SmartProxy -
|
|
43
|
-
*
|
|
44
|
-
* SmartProxy is a unified proxy system that works with routes to define connection handling behavior.
|
|
45
|
-
* Each route contains matching criteria (ports, domains, etc.) and an action to take (forward, redirect, block).
|
|
22
|
+
* SmartProxy - Rust-backed proxy engine with TypeScript configuration API.
|
|
46
23
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* -
|
|
50
|
-
* -
|
|
51
|
-
* -
|
|
52
|
-
* -
|
|
24
|
+
* All networking (TCP, TLS, HTTP reverse proxy, connection management, security,
|
|
25
|
+
* NFTables) is handled by the Rust binary. TypeScript is only:
|
|
26
|
+
* - The npm module interface (types, route helpers)
|
|
27
|
+
* - The thin IPC wrapper (this class)
|
|
28
|
+
* - Socket-handler callback relay (for JS-defined handlers)
|
|
29
|
+
* - Certificate provisioning callbacks (certProvisionFunction)
|
|
53
30
|
*/
|
|
54
31
|
export class SmartProxy extends plugins.EventEmitter {
|
|
55
|
-
|
|
56
|
-
private portManager: PortManager;
|
|
57
|
-
private connectionLogger: NodeJS.Timeout | null = null;
|
|
58
|
-
private isShuttingDown: boolean = false;
|
|
59
|
-
|
|
60
|
-
// Component managers
|
|
61
|
-
public connectionManager: ConnectionManager;
|
|
62
|
-
public securityManager: SecurityManager;
|
|
63
|
-
public tlsManager: TlsManager;
|
|
64
|
-
public httpProxyBridge: HttpProxyBridge;
|
|
65
|
-
public timeoutManager: TimeoutManager;
|
|
32
|
+
public settings: ISmartProxyOptions;
|
|
66
33
|
public routeManager: RouteManager;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// Global challenge route tracking
|
|
74
|
-
private globalChallengeRouteActive: boolean = false;
|
|
34
|
+
|
|
35
|
+
private bridge: RustProxyBridge;
|
|
36
|
+
private preprocessor: RoutePreprocessor;
|
|
37
|
+
private socketHandlerServer: SocketHandlerServer | null = null;
|
|
38
|
+
private metricsAdapter: RustMetricsAdapter;
|
|
75
39
|
private routeUpdateLock: Mutex;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// Metrics collector
|
|
79
|
-
public metricsCollector: MetricsCollector;
|
|
80
|
-
|
|
81
|
-
// Route orchestrator for managing route updates
|
|
82
|
-
private routeOrchestrator: RouteOrchestrator;
|
|
83
|
-
|
|
84
|
-
// Track port usage across route updates
|
|
85
|
-
private portUsageMap: Map<number, Set<string>> = new Map();
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Constructor for SmartProxy
|
|
89
|
-
*
|
|
90
|
-
* @param settingsArg Configuration options containing routes and other settings
|
|
91
|
-
* Routes define how traffic is matched and handled, with each route having:
|
|
92
|
-
* - match: criteria for matching traffic (ports, domains, paths, IPs)
|
|
93
|
-
* - action: what to do with matched traffic (forward, redirect, block)
|
|
94
|
-
*
|
|
95
|
-
* Example:
|
|
96
|
-
* ```ts
|
|
97
|
-
* const proxy = new SmartProxy({
|
|
98
|
-
* routes: [
|
|
99
|
-
* {
|
|
100
|
-
* match: {
|
|
101
|
-
* ports: 443,
|
|
102
|
-
* domains: ['example.com', '*.example.com']
|
|
103
|
-
* },
|
|
104
|
-
* action: {
|
|
105
|
-
* type: 'forward',
|
|
106
|
-
* target: { host: '10.0.0.1', port: 8443 },
|
|
107
|
-
* tls: { mode: 'passthrough' }
|
|
108
|
-
* }
|
|
109
|
-
* }
|
|
110
|
-
* ],
|
|
111
|
-
* defaults: {
|
|
112
|
-
* target: { host: 'localhost', port: 8080 },
|
|
113
|
-
* security: { ipAllowList: ['*'] }
|
|
114
|
-
* }
|
|
115
|
-
* });
|
|
116
|
-
* ```
|
|
117
|
-
*/
|
|
40
|
+
private stopping = false;
|
|
41
|
+
|
|
118
42
|
constructor(settingsArg: ISmartProxyOptions) {
|
|
119
43
|
super();
|
|
120
|
-
|
|
121
|
-
//
|
|
44
|
+
|
|
45
|
+
// Apply defaults
|
|
122
46
|
this.settings = {
|
|
123
47
|
...settingsArg,
|
|
124
48
|
initialDataTimeout: settingsArg.initialDataTimeout || 120000,
|
|
125
49
|
socketTimeout: settingsArg.socketTimeout || 3600000,
|
|
126
|
-
inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000,
|
|
127
50
|
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 86400000,
|
|
128
51
|
inactivityTimeout: settingsArg.inactivityTimeout || 14400000,
|
|
129
52
|
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000,
|
|
130
|
-
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
|
|
131
|
-
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
|
|
132
|
-
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000,
|
|
133
|
-
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024,
|
|
134
|
-
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
|
135
|
-
enableKeepAliveProbes:
|
|
136
|
-
settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true,
|
|
137
|
-
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
|
138
|
-
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
|
139
|
-
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
|
|
140
53
|
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
|
|
141
54
|
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300,
|
|
142
55
|
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended',
|
|
143
56
|
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6,
|
|
144
57
|
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000,
|
|
145
|
-
httpProxyPort: settingsArg.httpProxyPort || 8443,
|
|
146
58
|
};
|
|
147
|
-
|
|
148
|
-
// Normalize ACME options
|
|
59
|
+
|
|
60
|
+
// Normalize ACME options
|
|
149
61
|
if (this.settings.acme) {
|
|
150
|
-
// Support both 'email' and 'accountEmail' fields
|
|
151
62
|
if (this.settings.acme.accountEmail && !this.settings.acme.email) {
|
|
152
63
|
this.settings.acme.email = this.settings.acme.accountEmail;
|
|
153
64
|
}
|
|
154
|
-
|
|
155
|
-
// Set reasonable defaults for commonly used fields
|
|
156
65
|
this.settings.acme = {
|
|
157
|
-
enabled: this.settings.acme.enabled !== false,
|
|
66
|
+
enabled: this.settings.acme.enabled !== false,
|
|
158
67
|
port: this.settings.acme.port || 80,
|
|
159
68
|
email: this.settings.acme.email,
|
|
160
69
|
useProduction: this.settings.acme.useProduction || false,
|
|
161
70
|
renewThresholdDays: this.settings.acme.renewThresholdDays || 30,
|
|
162
|
-
autoRenew: this.settings.acme.autoRenew !== false,
|
|
71
|
+
autoRenew: this.settings.acme.autoRenew !== false,
|
|
163
72
|
certificateStore: this.settings.acme.certificateStore || './certs',
|
|
164
73
|
skipConfiguredCerts: this.settings.acme.skipConfiguredCerts || false,
|
|
165
74
|
renewCheckIntervalHours: this.settings.acme.renewCheckIntervalHours || 24,
|
|
166
75
|
routeForwards: this.settings.acme.routeForwards || [],
|
|
167
|
-
...this.settings.acme
|
|
76
|
+
...this.settings.acme,
|
|
168
77
|
};
|
|
169
78
|
}
|
|
170
|
-
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
this.securityManager = new SecurityManager(this);
|
|
174
|
-
this.connectionManager = new ConnectionManager(this);
|
|
175
|
-
|
|
176
|
-
// Create the route manager with SharedRouteManager API
|
|
177
|
-
// Create a logger adapter to match ILogger interface
|
|
178
|
-
const loggerAdapter = {
|
|
179
|
-
debug: (message: string, data?: any) => logger.log('debug', message, data),
|
|
180
|
-
info: (message: string, data?: any) => logger.log('info', message, data),
|
|
181
|
-
warn: (message: string, data?: any) => logger.log('warn', message, data),
|
|
182
|
-
error: (message: string, data?: any) => logger.log('error', message, data)
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
// Validate initial routes
|
|
186
|
-
if (this.settings.routes && this.settings.routes.length > 0) {
|
|
79
|
+
|
|
80
|
+
// Validate routes
|
|
81
|
+
if (this.settings.routes?.length) {
|
|
187
82
|
const validation = RouteValidator.validateRoutes(this.settings.routes);
|
|
188
83
|
if (!validation.valid) {
|
|
189
84
|
RouteValidator.logValidationErrors(validation.errors);
|
|
190
85
|
throw new Error(`Initial route validation failed: ${validation.errors.size} route(s) have errors`);
|
|
191
86
|
}
|
|
192
87
|
}
|
|
193
|
-
|
|
88
|
+
|
|
89
|
+
// Create logger adapter
|
|
90
|
+
const loggerAdapter = {
|
|
91
|
+
debug: (message: string, data?: any) => logger.log('debug', message, data),
|
|
92
|
+
info: (message: string, data?: any) => logger.log('info', message, data),
|
|
93
|
+
warn: (message: string, data?: any) => logger.log('warn', message, data),
|
|
94
|
+
error: (message: string, data?: any) => logger.log('error', message, data),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Initialize components
|
|
194
98
|
this.routeManager = new RouteManager({
|
|
195
99
|
logger: loggerAdapter,
|
|
196
100
|
enableDetailedLogging: this.settings.enableDetailedLogging,
|
|
197
|
-
routes: this.settings.routes
|
|
101
|
+
routes: this.settings.routes,
|
|
198
102
|
});
|
|
199
103
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
this.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
// Initialize connection handler with route support
|
|
206
|
-
this.routeConnectionHandler = new RouteConnectionHandler(this);
|
|
207
|
-
|
|
208
|
-
// Initialize port manager
|
|
209
|
-
this.portManager = new PortManager(this);
|
|
210
|
-
|
|
211
|
-
// Initialize NFTablesManager
|
|
212
|
-
this.nftablesManager = new NFTablesManager(this);
|
|
213
|
-
|
|
214
|
-
// Initialize route update mutex for synchronization
|
|
215
|
-
this.routeUpdateLock = new Mutex();
|
|
216
|
-
|
|
217
|
-
// Initialize ACME state manager
|
|
218
|
-
this.acmeStateManager = new AcmeStateManager();
|
|
219
|
-
|
|
220
|
-
// Initialize metrics collector with reference to this SmartProxy instance
|
|
221
|
-
this.metricsCollector = new MetricsCollector(this, {
|
|
222
|
-
sampleIntervalMs: this.settings.metrics?.sampleIntervalMs,
|
|
223
|
-
retentionSeconds: this.settings.metrics?.retentionSeconds
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
// Initialize route orchestrator for managing route updates
|
|
227
|
-
this.routeOrchestrator = new RouteOrchestrator(
|
|
228
|
-
this.portManager,
|
|
229
|
-
this.routeManager,
|
|
230
|
-
this.httpProxyBridge,
|
|
231
|
-
this.nftablesManager,
|
|
232
|
-
null, // certManager will be set later
|
|
233
|
-
loggerAdapter
|
|
104
|
+
this.bridge = new RustProxyBridge();
|
|
105
|
+
this.preprocessor = new RoutePreprocessor();
|
|
106
|
+
this.metricsAdapter = new RustMetricsAdapter(
|
|
107
|
+
this.bridge,
|
|
108
|
+
this.settings.metrics?.sampleIntervalMs ?? 1000
|
|
234
109
|
);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* The settings for the SmartProxy
|
|
239
|
-
*/
|
|
240
|
-
public settings: ISmartProxyOptions;
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Helper method to create and configure certificate manager
|
|
244
|
-
* This ensures consistent setup including the required ACME callback
|
|
245
|
-
*/
|
|
246
|
-
private async createCertificateManager(
|
|
247
|
-
routes: IRouteConfig[],
|
|
248
|
-
certStore: string = './certs',
|
|
249
|
-
acmeOptions?: any,
|
|
250
|
-
initialState?: { challengeRouteActive?: boolean }
|
|
251
|
-
): Promise<SmartCertManager> {
|
|
252
|
-
const certManager = new SmartCertManager(routes, certStore, acmeOptions, initialState);
|
|
253
|
-
|
|
254
|
-
// Always set up the route update callback for ACME challenges
|
|
255
|
-
certManager.setUpdateRoutesCallback(async (routes) => {
|
|
256
|
-
await this.updateRoutes(routes);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// Connect with HttpProxy if available
|
|
260
|
-
if (this.httpProxyBridge.getHttpProxy()) {
|
|
261
|
-
certManager.setHttpProxy(this.httpProxyBridge.getHttpProxy());
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Set the ACME state manager
|
|
265
|
-
certManager.setAcmeStateManager(this.acmeStateManager);
|
|
266
|
-
|
|
267
|
-
// Pass down the global ACME config if available
|
|
268
|
-
if (this.settings.acme) {
|
|
269
|
-
certManager.setGlobalAcmeDefaults(this.settings.acme);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Pass down the custom certificate provision function if available
|
|
273
|
-
if (this.settings.certProvisionFunction) {
|
|
274
|
-
certManager.setCertProvisionFunction(this.settings.certProvisionFunction);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Pass down the fallback to ACME setting
|
|
278
|
-
if (this.settings.certProvisionFallbackToAcme !== undefined) {
|
|
279
|
-
certManager.setCertProvisionFallbackToAcme(this.settings.certProvisionFallbackToAcme);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
await certManager.initialize();
|
|
283
|
-
return certManager;
|
|
110
|
+
this.routeUpdateLock = new Mutex();
|
|
284
111
|
}
|
|
285
112
|
|
|
286
113
|
/**
|
|
287
|
-
*
|
|
114
|
+
* Start the proxy.
|
|
115
|
+
* Spawns the Rust binary, configures socket relay if needed, sends routes, handles cert provisioning.
|
|
288
116
|
*/
|
|
289
|
-
|
|
290
|
-
//
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
);
|
|
294
|
-
|
|
295
|
-
if (autoRoutes.length === 0 && !this.hasStaticCertRoutes()) {
|
|
296
|
-
logger.log('info', 'No routes require certificate management', { component: 'certificate-manager' });
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Prepare ACME options with priority:
|
|
301
|
-
// 1. Use top-level ACME config if available
|
|
302
|
-
// 2. Fall back to first auto route's ACME config
|
|
303
|
-
// 3. Otherwise use undefined
|
|
304
|
-
let acmeOptions: { email?: string; useProduction?: boolean; port?: number } | undefined;
|
|
305
|
-
|
|
306
|
-
if (this.settings.acme?.email) {
|
|
307
|
-
// Use top-level ACME config
|
|
308
|
-
acmeOptions = {
|
|
309
|
-
email: this.settings.acme.email,
|
|
310
|
-
useProduction: this.settings.acme.useProduction || false,
|
|
311
|
-
port: this.settings.acme.port || 80
|
|
312
|
-
};
|
|
313
|
-
logger.log('info', `Using top-level ACME configuration with email: ${acmeOptions.email}`, { component: 'certificate-manager' });
|
|
314
|
-
} else if (autoRoutes.length > 0) {
|
|
315
|
-
// Check for route-level ACME config
|
|
316
|
-
const routeWithAcme = autoRoutes.find(r => r.action.tls?.acme?.email);
|
|
317
|
-
if (routeWithAcme?.action.tls?.acme) {
|
|
318
|
-
const routeAcme = routeWithAcme.action.tls.acme;
|
|
319
|
-
acmeOptions = {
|
|
320
|
-
email: routeAcme.email,
|
|
321
|
-
useProduction: routeAcme.useProduction || false,
|
|
322
|
-
port: routeAcme.challengePort || 80
|
|
323
|
-
};
|
|
324
|
-
logger.log('info', `Using route-level ACME configuration from route '${routeWithAcme.name}' with email: ${acmeOptions.email}`, { component: 'certificate-manager' });
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Validate we have required configuration
|
|
329
|
-
if (autoRoutes.length > 0 && !acmeOptions?.email) {
|
|
117
|
+
public async start(): Promise<void> {
|
|
118
|
+
// Spawn Rust binary
|
|
119
|
+
const spawned = await this.bridge.spawn();
|
|
120
|
+
if (!spawned) {
|
|
330
121
|
throw new Error(
|
|
331
|
-
'
|
|
332
|
-
'
|
|
333
|
-
'1. Top-level "acme" configuration\n' +
|
|
334
|
-
'2. Individual route\'s "tls.acme" configuration'
|
|
122
|
+
'RustProxy binary not found. Set SMARTPROXY_RUST_BINARY env var, install the platform package, ' +
|
|
123
|
+
'or build locally with: cd rust && cargo build --release'
|
|
335
124
|
);
|
|
336
125
|
}
|
|
337
|
-
|
|
338
|
-
// Use the helper method to create and configure the certificate manager
|
|
339
|
-
this.certManager = await this.createCertificateManager(
|
|
340
|
-
this.settings.routes,
|
|
341
|
-
this.settings.acme?.certificateStore || './certs',
|
|
342
|
-
acmeOptions
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Check if we have routes with static certificates
|
|
348
|
-
*/
|
|
349
|
-
private hasStaticCertRoutes(): boolean {
|
|
350
|
-
return this.settings.routes.some(r =>
|
|
351
|
-
r.action.tls?.certificate &&
|
|
352
|
-
r.action.tls.certificate !== 'auto'
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Start the proxy server with support for both configuration types
|
|
358
|
-
*/
|
|
359
|
-
public async start() {
|
|
360
|
-
// Don't start if already shutting down
|
|
361
|
-
if (this.isShuttingDown) {
|
|
362
|
-
logger.log('warn', "Cannot start SmartProxy while it's in the shutdown process");
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Validate the route configuration
|
|
367
|
-
const configWarnings = this.routeManager.validateConfiguration();
|
|
368
|
-
|
|
369
|
-
// Also validate ACME configuration
|
|
370
|
-
const acmeWarnings = this.validateAcmeConfiguration();
|
|
371
|
-
const allWarnings = [...configWarnings, ...acmeWarnings];
|
|
372
|
-
|
|
373
|
-
if (allWarnings.length > 0) {
|
|
374
|
-
logger.log('warn', `${allWarnings.length} configuration warnings found`, { count: allWarnings.length });
|
|
375
|
-
for (const warning of allWarnings) {
|
|
376
|
-
logger.log('warn', `${warning}`);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
126
|
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
// Log port usage for startup
|
|
387
|
-
logger.log('info', `SmartProxy starting with ${listeningPorts.length} ports: ${listeningPorts.join(', ')}`, {
|
|
388
|
-
portCount: listeningPorts.length,
|
|
389
|
-
ports: listeningPorts,
|
|
390
|
-
component: 'smart-proxy'
|
|
127
|
+
// Handle unexpected exit (only emits error if not intentionally stopping)
|
|
128
|
+
this.bridge.on('exit', (code: number | null, signal: string | null) => {
|
|
129
|
+
if (this.stopping) return;
|
|
130
|
+
logger.log('error', `RustProxy exited unexpectedly (code=${code}, signal=${signal})`, { component: 'smart-proxy' });
|
|
131
|
+
this.emit('error', new Error(`RustProxy exited (code=${code}, signal=${signal})`));
|
|
391
132
|
});
|
|
392
133
|
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
134
|
+
// Check if any routes need TS-side handling (socket handlers, dynamic functions)
|
|
135
|
+
const hasHandlerRoutes = this.settings.routes.some(
|
|
136
|
+
(r) =>
|
|
137
|
+
(r.action.type === 'socket-handler' && r.action.socketHandler) ||
|
|
138
|
+
r.action.targets?.some((t) => typeof t.host === 'function' || typeof t.port === 'function')
|
|
139
|
+
);
|
|
399
140
|
|
|
400
|
-
//
|
|
401
|
-
if (
|
|
402
|
-
|
|
403
|
-
await this.
|
|
141
|
+
// Start socket handler relay server (but don't tell Rust yet - proxy not started)
|
|
142
|
+
if (hasHandlerRoutes) {
|
|
143
|
+
this.socketHandlerServer = new SocketHandlerServer(this.preprocessor);
|
|
144
|
+
await this.socketHandlerServer.start();
|
|
404
145
|
}
|
|
405
146
|
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
await this.portManager.addPorts(listeningPorts);
|
|
409
|
-
|
|
410
|
-
// Initialize certificate manager AFTER port binding is complete
|
|
411
|
-
// This ensures the ACME challenge port is already bound and ready when needed
|
|
412
|
-
await this.initializeCertificateManager();
|
|
413
|
-
|
|
414
|
-
// Connect certificate manager with HttpProxy if both are available
|
|
415
|
-
if (this.certManager && this.httpProxyBridge.getHttpProxy()) {
|
|
416
|
-
this.certManager.setHttpProxy(this.httpProxyBridge.getHttpProxy());
|
|
417
|
-
}
|
|
147
|
+
// Preprocess routes (strip JS functions, convert socket-handler routes)
|
|
148
|
+
const rustRoutes = this.preprocessor.preprocessForRust(this.settings.routes);
|
|
418
149
|
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
logger.log('info', 'Starting certificate provisioning now that ports are ready', { component: 'certificate-manager' });
|
|
422
|
-
await this.certManager.provisionAllCertificates();
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Start the metrics collector now that all components are initialized
|
|
426
|
-
this.metricsCollector.start();
|
|
427
|
-
|
|
428
|
-
// Set up periodic connection logging and inactivity checks
|
|
429
|
-
this.connectionLogger = setInterval(() => {
|
|
430
|
-
// Immediately return if shutting down
|
|
431
|
-
if (this.isShuttingDown) return;
|
|
432
|
-
|
|
433
|
-
// Perform inactivity check
|
|
434
|
-
this.connectionManager.performInactivityCheck();
|
|
435
|
-
|
|
436
|
-
// Log connection statistics
|
|
437
|
-
const now = Date.now();
|
|
438
|
-
let maxIncoming = 0;
|
|
439
|
-
let maxOutgoing = 0;
|
|
440
|
-
let tlsConnections = 0;
|
|
441
|
-
let nonTlsConnections = 0;
|
|
442
|
-
let completedTlsHandshakes = 0;
|
|
443
|
-
let pendingTlsHandshakes = 0;
|
|
444
|
-
let keepAliveConnections = 0;
|
|
445
|
-
let httpProxyConnections = 0;
|
|
446
|
-
|
|
447
|
-
// Get connection records for analysis
|
|
448
|
-
const connectionRecords = this.connectionManager.getConnections();
|
|
449
|
-
|
|
450
|
-
// Analyze active connections
|
|
451
|
-
for (const record of connectionRecords.values()) {
|
|
452
|
-
// Track connection stats
|
|
453
|
-
if (record.isTLS) {
|
|
454
|
-
tlsConnections++;
|
|
455
|
-
if (record.tlsHandshakeComplete) {
|
|
456
|
-
completedTlsHandshakes++;
|
|
457
|
-
} else {
|
|
458
|
-
pendingTlsHandshakes++;
|
|
459
|
-
}
|
|
460
|
-
} else {
|
|
461
|
-
nonTlsConnections++;
|
|
462
|
-
}
|
|
150
|
+
// Build Rust config
|
|
151
|
+
const config = this.buildRustConfig(rustRoutes);
|
|
463
152
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
153
|
+
// Start the Rust proxy
|
|
154
|
+
await this.bridge.startProxy(config);
|
|
467
155
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
156
|
+
// Now that Rust proxy is running, configure socket handler relay
|
|
157
|
+
if (this.socketHandlerServer) {
|
|
158
|
+
await this.bridge.setSocketHandlerRelay(this.socketHandlerServer.getSocketPath());
|
|
159
|
+
}
|
|
471
160
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
161
|
+
// Handle certProvisionFunction
|
|
162
|
+
await this.provisionCertificatesViaCallback();
|
|
477
163
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
// Log detailed stats
|
|
482
|
-
logger.log('info', 'Connection statistics', {
|
|
483
|
-
activeConnections: connectionRecords.size,
|
|
484
|
-
tls: {
|
|
485
|
-
total: tlsConnections,
|
|
486
|
-
completed: completedTlsHandshakes,
|
|
487
|
-
pending: pendingTlsHandshakes
|
|
488
|
-
},
|
|
489
|
-
nonTls: nonTlsConnections,
|
|
490
|
-
keepAlive: keepAliveConnections,
|
|
491
|
-
httpProxy: httpProxyConnections,
|
|
492
|
-
longestRunning: {
|
|
493
|
-
incoming: plugins.prettyMs(maxIncoming),
|
|
494
|
-
outgoing: plugins.prettyMs(maxOutgoing)
|
|
495
|
-
},
|
|
496
|
-
terminationStats: {
|
|
497
|
-
incoming: terminationStats.incoming,
|
|
498
|
-
outgoing: terminationStats.outgoing
|
|
499
|
-
},
|
|
500
|
-
component: 'connection-manager'
|
|
501
|
-
});
|
|
502
|
-
}, this.settings.inactivityCheckInterval || 60000);
|
|
503
|
-
|
|
504
|
-
// Make sure the interval doesn't keep the process alive
|
|
505
|
-
if (this.connectionLogger.unref) {
|
|
506
|
-
this.connectionLogger.unref();
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Extract domain configurations from routes for certificate provisioning
|
|
512
|
-
*
|
|
513
|
-
* Note: This method has been removed as we now work directly with routes
|
|
514
|
-
*/
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Stop the proxy server
|
|
518
|
-
*/
|
|
519
|
-
public async stop() {
|
|
520
|
-
logger.log('info', 'SmartProxy shutting down...');
|
|
521
|
-
this.isShuttingDown = true;
|
|
522
|
-
this.portManager.setShuttingDown(true);
|
|
523
|
-
|
|
524
|
-
// Stop certificate manager
|
|
525
|
-
if (this.certManager) {
|
|
526
|
-
await this.certManager.stop();
|
|
527
|
-
logger.log('info', 'Certificate manager stopped');
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Stop NFTablesManager
|
|
531
|
-
await this.nftablesManager.stop();
|
|
532
|
-
logger.log('info', 'NFTablesManager stopped');
|
|
533
|
-
|
|
534
|
-
// Stop the connection logger
|
|
535
|
-
if (this.connectionLogger) {
|
|
536
|
-
clearInterval(this.connectionLogger);
|
|
537
|
-
this.connectionLogger = null;
|
|
538
|
-
}
|
|
164
|
+
// Start metrics polling
|
|
165
|
+
this.metricsAdapter.startPolling();
|
|
539
166
|
|
|
540
|
-
|
|
541
|
-
await this.portManager.closeAll();
|
|
542
|
-
logger.log('info', 'All servers closed. Cleaning up active connections...');
|
|
543
|
-
|
|
544
|
-
// Clean up all active connections
|
|
545
|
-
await this.connectionManager.clearConnections();
|
|
546
|
-
|
|
547
|
-
// Stop HttpProxy
|
|
548
|
-
await this.httpProxyBridge.stop();
|
|
549
|
-
|
|
550
|
-
// Clear ACME state manager
|
|
551
|
-
this.acmeStateManager.clear();
|
|
552
|
-
|
|
553
|
-
// Stop metrics collector
|
|
554
|
-
this.metricsCollector.stop();
|
|
555
|
-
|
|
556
|
-
// Clean up ProtocolDetector singleton
|
|
557
|
-
const detection = await import('../../detection/index.js');
|
|
558
|
-
detection.ProtocolDetector.destroy();
|
|
559
|
-
|
|
560
|
-
// Flush any pending deduplicated logs
|
|
561
|
-
connectionLogDeduplicator.flushAll();
|
|
562
|
-
|
|
563
|
-
logger.log('info', 'SmartProxy shutdown complete.');
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* Updates the domain configurations for the proxy
|
|
568
|
-
*
|
|
569
|
-
* Note: This legacy method has been removed. Use updateRoutes instead.
|
|
570
|
-
*/
|
|
571
|
-
public async updateDomainConfigs(): Promise<void> {
|
|
572
|
-
logger.log('warn', 'Method updateDomainConfigs() is deprecated. Use updateRoutes() instead.');
|
|
573
|
-
throw new Error('updateDomainConfigs() is deprecated - use updateRoutes() instead');
|
|
167
|
+
logger.log('info', 'SmartProxy started (Rust engine)', { component: 'smart-proxy' });
|
|
574
168
|
}
|
|
575
|
-
|
|
169
|
+
|
|
576
170
|
/**
|
|
577
|
-
*
|
|
171
|
+
* Stop the proxy.
|
|
578
172
|
*/
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
} catch (error) {
|
|
591
|
-
// Silently handle logging errors
|
|
592
|
-
console.log('[INFO] Challenge route successfully removed from routes');
|
|
593
|
-
}
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// Wait before retrying
|
|
598
|
-
await plugins.smartdelay.delayFor(retryDelay);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const error = `Failed to verify challenge route removal after ${maxRetries} attempts`;
|
|
173
|
+
public async stop(): Promise<void> {
|
|
174
|
+
logger.log('info', 'SmartProxy shutting down...', { component: 'smart-proxy' });
|
|
175
|
+
this.stopping = true;
|
|
176
|
+
|
|
177
|
+
// Stop metrics polling
|
|
178
|
+
this.metricsAdapter.stopPolling();
|
|
179
|
+
|
|
180
|
+
// Remove exit listener before killing to avoid spurious error events
|
|
181
|
+
this.bridge.removeAllListeners('exit');
|
|
182
|
+
|
|
183
|
+
// Stop Rust proxy
|
|
602
184
|
try {
|
|
603
|
-
|
|
604
|
-
} catch
|
|
605
|
-
//
|
|
606
|
-
|
|
185
|
+
await this.bridge.stopProxy();
|
|
186
|
+
} catch {
|
|
187
|
+
// Ignore if already stopped
|
|
188
|
+
}
|
|
189
|
+
this.bridge.kill();
|
|
190
|
+
|
|
191
|
+
// Stop socket handler relay
|
|
192
|
+
if (this.socketHandlerServer) {
|
|
193
|
+
await this.socketHandlerServer.stop();
|
|
194
|
+
this.socketHandlerServer = null;
|
|
607
195
|
}
|
|
608
|
-
|
|
196
|
+
|
|
197
|
+
logger.log('info', 'SmartProxy shutdown complete.', { component: 'smart-proxy' });
|
|
609
198
|
}
|
|
610
|
-
|
|
199
|
+
|
|
611
200
|
/**
|
|
612
|
-
* Update routes
|
|
613
|
-
*
|
|
614
|
-
* This method replaces the current route configuration with the provided routes.
|
|
615
|
-
* It also provisions certificates for routes that require TLS termination and have
|
|
616
|
-
* `certificate: 'auto'` set in their TLS configuration.
|
|
617
|
-
*
|
|
618
|
-
* @param newRoutes Array of route configurations to use
|
|
619
|
-
*
|
|
620
|
-
* Example:
|
|
621
|
-
* ```ts
|
|
622
|
-
* proxy.updateRoutes([
|
|
623
|
-
* {
|
|
624
|
-
* match: { ports: 443, domains: 'secure.example.com' },
|
|
625
|
-
* action: {
|
|
626
|
-
* type: 'forward',
|
|
627
|
-
* target: { host: '10.0.0.1', port: 8443 },
|
|
628
|
-
* tls: { mode: 'terminate', certificate: 'auto' }
|
|
629
|
-
* }
|
|
630
|
-
* }
|
|
631
|
-
* ]);
|
|
632
|
-
* ```
|
|
201
|
+
* Update routes atomically.
|
|
633
202
|
*/
|
|
634
203
|
public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> {
|
|
635
204
|
return this.routeUpdateLock.runExclusive(async () => {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
});
|
|
641
|
-
} catch (error) {
|
|
642
|
-
// Silently handle logging errors
|
|
643
|
-
console.log(`[INFO] Updating routes (${newRoutes.length} routes)`);
|
|
205
|
+
// Validate
|
|
206
|
+
const validation = RouteValidator.validateRoutes(newRoutes);
|
|
207
|
+
if (!validation.valid) {
|
|
208
|
+
RouteValidator.logValidationErrors(validation.errors);
|
|
209
|
+
throw new Error(`Route validation failed: ${validation.errors.size} route(s) have errors`);
|
|
644
210
|
}
|
|
645
211
|
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
createCertificateManager: this.createCertificateManager.bind(this),
|
|
661
|
-
verifyChallengeRouteRemoved: this.verifyChallengeRouteRemoved.bind(this)
|
|
662
|
-
}
|
|
212
|
+
// Preprocess for Rust
|
|
213
|
+
const rustRoutes = this.preprocessor.preprocessForRust(newRoutes);
|
|
214
|
+
|
|
215
|
+
// Send to Rust
|
|
216
|
+
await this.bridge.updateRoutes(rustRoutes);
|
|
217
|
+
|
|
218
|
+
// Update local route manager
|
|
219
|
+
this.routeManager.updateRoutes(newRoutes);
|
|
220
|
+
|
|
221
|
+
// Update socket handler relay if handler routes changed
|
|
222
|
+
const hasHandlerRoutes = newRoutes.some(
|
|
223
|
+
(r) =>
|
|
224
|
+
(r.action.type === 'socket-handler' && r.action.socketHandler) ||
|
|
225
|
+
r.action.targets?.some((t) => typeof t.host === 'function' || typeof t.port === 'function')
|
|
663
226
|
);
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
this.
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
this.portUsageMap = updateResult.portUsageMap;
|
|
673
|
-
|
|
674
|
-
// If certificate manager was recreated, update our reference
|
|
675
|
-
if (updateResult.newCertManager) {
|
|
676
|
-
this.certManager = updateResult.newCertManager;
|
|
677
|
-
// Update the orchestrator's reference too
|
|
678
|
-
this.routeOrchestrator.setCertManager(this.certManager);
|
|
227
|
+
|
|
228
|
+
if (hasHandlerRoutes && !this.socketHandlerServer) {
|
|
229
|
+
this.socketHandlerServer = new SocketHandlerServer(this.preprocessor);
|
|
230
|
+
await this.socketHandlerServer.start();
|
|
231
|
+
await this.bridge.setSocketHandlerRelay(this.socketHandlerServer.getSocketPath());
|
|
232
|
+
} else if (!hasHandlerRoutes && this.socketHandlerServer) {
|
|
233
|
+
await this.socketHandlerServer.stop();
|
|
234
|
+
this.socketHandlerServer = null;
|
|
679
235
|
}
|
|
236
|
+
|
|
237
|
+
// Update stored routes
|
|
238
|
+
this.settings.routes = newRoutes;
|
|
239
|
+
|
|
240
|
+
// Handle cert provisioning for new routes
|
|
241
|
+
await this.provisionCertificatesViaCallback();
|
|
242
|
+
|
|
243
|
+
logger.log('info', `Routes updated (${newRoutes.length} routes)`, { component: 'smart-proxy' });
|
|
680
244
|
});
|
|
681
245
|
}
|
|
682
|
-
|
|
246
|
+
|
|
683
247
|
/**
|
|
684
|
-
*
|
|
248
|
+
* Provision a certificate for a named route.
|
|
685
249
|
*/
|
|
686
250
|
public async provisionCertificate(routeName: string): Promise<void> {
|
|
687
|
-
|
|
688
|
-
throw new Error('Certificate manager not initialized');
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
const route = this.settings.routes.find(r => r.name === routeName);
|
|
692
|
-
if (!route) {
|
|
693
|
-
throw new Error(`Route ${routeName} not found`);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
await this.certManager.provisionCertificate(route);
|
|
251
|
+
await this.bridge.provisionCertificate(routeName);
|
|
697
252
|
}
|
|
698
253
|
|
|
699
|
-
// Port usage tracking methods moved to RouteOrchestrator
|
|
700
|
-
|
|
701
254
|
/**
|
|
702
|
-
* Force renewal of a certificate
|
|
255
|
+
* Force renewal of a certificate.
|
|
703
256
|
*/
|
|
704
257
|
public async renewCertificate(routeName: string): Promise<void> {
|
|
705
|
-
|
|
706
|
-
throw new Error('Certificate manager not initialized');
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
await this.certManager.renewCertificate(routeName);
|
|
258
|
+
await this.bridge.renewCertificate(routeName);
|
|
710
259
|
}
|
|
711
|
-
|
|
260
|
+
|
|
712
261
|
/**
|
|
713
|
-
* Get certificate status for a route
|
|
262
|
+
* Get certificate status for a route (async - calls Rust).
|
|
714
263
|
*/
|
|
715
|
-
public getCertificateStatus(routeName: string):
|
|
716
|
-
|
|
717
|
-
return undefined;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
return this.certManager.getCertificateStatus(routeName);
|
|
264
|
+
public async getCertificateStatus(routeName: string): Promise<any> {
|
|
265
|
+
return this.bridge.getCertificateStatus(routeName);
|
|
721
266
|
}
|
|
722
|
-
|
|
267
|
+
|
|
723
268
|
/**
|
|
724
|
-
* Get
|
|
725
|
-
*
|
|
726
|
-
* @returns IMetrics interface with grouped metrics methods
|
|
269
|
+
* Get the metrics interface.
|
|
727
270
|
*/
|
|
728
271
|
public getMetrics(): IMetrics {
|
|
729
|
-
return this.
|
|
272
|
+
return this.metricsAdapter;
|
|
730
273
|
}
|
|
731
|
-
|
|
274
|
+
|
|
732
275
|
/**
|
|
733
|
-
*
|
|
276
|
+
* Get statistics (async - calls Rust).
|
|
734
277
|
*/
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
if (!domain || domain.length === 0) {
|
|
738
|
-
return false;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// Check for wildcard domains (they can't get ACME certs)
|
|
742
|
-
if (domain.includes('*')) {
|
|
743
|
-
logger.log('warn', `Wildcard domains like "${domain}" are not supported for automatic ACME certificates`, { domain, component: 'certificate-manager' });
|
|
744
|
-
return false;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// Check if domain has at least one dot and no invalid characters
|
|
748
|
-
const validDomainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
749
|
-
if (!validDomainRegex.test(domain)) {
|
|
750
|
-
logger.log('warn', `Domain "${domain}" has invalid format for certificate issuance`, { domain, component: 'certificate-manager' });
|
|
751
|
-
return false;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
return true;
|
|
278
|
+
public async getStatistics(): Promise<any> {
|
|
279
|
+
return this.bridge.getStatistics();
|
|
755
280
|
}
|
|
756
|
-
|
|
281
|
+
|
|
757
282
|
/**
|
|
758
|
-
* Add a
|
|
759
|
-
*
|
|
760
|
-
* This allows you to add a port listener without updating routes.
|
|
761
|
-
* Useful for preparing to listen on a port before adding routes for it.
|
|
762
|
-
*
|
|
763
|
-
* @param port The port to start listening on
|
|
764
|
-
* @returns Promise that resolves when the port is listening
|
|
283
|
+
* Add a listening port at runtime.
|
|
765
284
|
*/
|
|
766
285
|
public async addListeningPort(port: number): Promise<void> {
|
|
767
|
-
|
|
286
|
+
await this.bridge.addListeningPort(port);
|
|
768
287
|
}
|
|
769
288
|
|
|
770
289
|
/**
|
|
771
|
-
*
|
|
772
|
-
*
|
|
773
|
-
* This allows you to stop a port listener without updating routes.
|
|
774
|
-
* Useful for temporary maintenance or port changes.
|
|
775
|
-
*
|
|
776
|
-
* @param port The port to stop listening on
|
|
777
|
-
* @returns Promise that resolves when the port is closed
|
|
290
|
+
* Remove a listening port at runtime.
|
|
778
291
|
*/
|
|
779
292
|
public async removeListeningPort(port: number): Promise<void> {
|
|
780
|
-
|
|
293
|
+
await this.bridge.removeListeningPort(port);
|
|
781
294
|
}
|
|
782
295
|
|
|
783
296
|
/**
|
|
784
|
-
* Get
|
|
785
|
-
*
|
|
786
|
-
* @returns Array of port numbers
|
|
297
|
+
* Get all currently listening ports (async - calls Rust).
|
|
787
298
|
*/
|
|
788
|
-
public getListeningPorts(): number[] {
|
|
789
|
-
|
|
299
|
+
public async getListeningPorts(): Promise<number[]> {
|
|
300
|
+
if (!this.bridge.running) return [];
|
|
301
|
+
return this.bridge.getListeningPorts();
|
|
790
302
|
}
|
|
791
303
|
|
|
792
304
|
/**
|
|
793
|
-
* Get
|
|
794
|
-
*/
|
|
795
|
-
public getStatistics(): any {
|
|
796
|
-
const connectionRecords = this.connectionManager.getConnections();
|
|
797
|
-
const terminationStats = this.connectionManager.getTerminationStats();
|
|
798
|
-
|
|
799
|
-
let tlsConnections = 0;
|
|
800
|
-
let nonTlsConnections = 0;
|
|
801
|
-
let keepAliveConnections = 0;
|
|
802
|
-
let httpProxyConnections = 0;
|
|
803
|
-
|
|
804
|
-
// Analyze active connections
|
|
805
|
-
for (const record of connectionRecords.values()) {
|
|
806
|
-
if (record.isTLS) tlsConnections++;
|
|
807
|
-
else nonTlsConnections++;
|
|
808
|
-
if (record.hasKeepAlive) keepAliveConnections++;
|
|
809
|
-
if (record.usingNetworkProxy) httpProxyConnections++;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
return {
|
|
813
|
-
activeConnections: connectionRecords.size,
|
|
814
|
-
tlsConnections,
|
|
815
|
-
nonTlsConnections,
|
|
816
|
-
keepAliveConnections,
|
|
817
|
-
httpProxyConnections,
|
|
818
|
-
terminationStats,
|
|
819
|
-
acmeEnabled: !!this.certManager,
|
|
820
|
-
port80HandlerPort: this.certManager ? 80 : null,
|
|
821
|
-
routeCount: this.settings.routes.length,
|
|
822
|
-
activePorts: this.portManager.getListeningPorts().length,
|
|
823
|
-
listeningPorts: this.portManager.getListeningPorts()
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
/**
|
|
828
|
-
* Get a list of eligible domains for ACME certificates
|
|
305
|
+
* Get eligible domains for ACME certificates (sync - reads local routes).
|
|
829
306
|
*/
|
|
830
307
|
public getEligibleDomainsForCertificates(): string[] {
|
|
831
308
|
const domains: string[] = [];
|
|
832
|
-
|
|
833
|
-
// Get domains from routes
|
|
834
|
-
const routes = this.settings.routes || [];
|
|
835
|
-
|
|
836
|
-
for (const route of routes) {
|
|
309
|
+
for (const route of this.settings.routes || []) {
|
|
837
310
|
if (!route.match.domains) continue;
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
// Skip domains that can't be used with ACME
|
|
850
|
-
const eligibleDomains = routeDomains.filter(domain =>
|
|
851
|
-
!domain.includes('*') && this.isValidDomain(domain)
|
|
852
|
-
);
|
|
853
|
-
|
|
854
|
-
domains.push(...eligibleDomains);
|
|
311
|
+
if (
|
|
312
|
+
route.action.type !== 'forward' ||
|
|
313
|
+
!route.action.tls ||
|
|
314
|
+
route.action.tls.mode === 'passthrough' ||
|
|
315
|
+
route.action.tls.certificate !== 'auto'
|
|
316
|
+
)
|
|
317
|
+
continue;
|
|
318
|
+
|
|
319
|
+
const routeDomains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains];
|
|
320
|
+
const eligible = routeDomains.filter((d) => !d.includes('*') && this.isValidDomain(d));
|
|
321
|
+
domains.push(...eligible);
|
|
855
322
|
}
|
|
856
|
-
|
|
857
|
-
// Legacy mode is no longer supported
|
|
858
|
-
|
|
859
323
|
return domains;
|
|
860
324
|
}
|
|
861
|
-
|
|
325
|
+
|
|
862
326
|
/**
|
|
863
|
-
* Get NFTables status
|
|
327
|
+
* Get NFTables status (async - calls Rust).
|
|
864
328
|
*/
|
|
865
329
|
public async getNfTablesStatus(): Promise<Record<string, any>> {
|
|
866
|
-
return this.
|
|
330
|
+
return this.bridge.getNftablesStatus();
|
|
867
331
|
}
|
|
868
|
-
|
|
332
|
+
|
|
333
|
+
// --- Private helpers ---
|
|
334
|
+
|
|
869
335
|
/**
|
|
870
|
-
*
|
|
336
|
+
* Build the Rust configuration object from TS settings.
|
|
871
337
|
*/
|
|
872
|
-
private
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
if (
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
338
|
+
private buildRustConfig(routes: IRouteConfig[]): any {
|
|
339
|
+
return {
|
|
340
|
+
routes,
|
|
341
|
+
defaults: this.settings.defaults,
|
|
342
|
+
acme: this.settings.acme
|
|
343
|
+
? {
|
|
344
|
+
enabled: this.settings.acme.enabled,
|
|
345
|
+
email: this.settings.acme.email,
|
|
346
|
+
useProduction: this.settings.acme.useProduction,
|
|
347
|
+
port: this.settings.acme.port,
|
|
348
|
+
renewThresholdDays: this.settings.acme.renewThresholdDays,
|
|
349
|
+
autoRenew: this.settings.acme.autoRenew,
|
|
350
|
+
certificateStore: this.settings.acme.certificateStore,
|
|
351
|
+
renewCheckIntervalHours: this.settings.acme.renewCheckIntervalHours,
|
|
352
|
+
}
|
|
353
|
+
: undefined,
|
|
354
|
+
connectionTimeout: this.settings.connectionTimeout,
|
|
355
|
+
initialDataTimeout: this.settings.initialDataTimeout,
|
|
356
|
+
socketTimeout: this.settings.socketTimeout,
|
|
357
|
+
maxConnectionLifetime: this.settings.maxConnectionLifetime,
|
|
358
|
+
gracefulShutdownTimeout: this.settings.gracefulShutdownTimeout,
|
|
359
|
+
maxConnectionsPerIp: this.settings.maxConnectionsPerIP,
|
|
360
|
+
connectionRateLimitPerMinute: this.settings.connectionRateLimitPerMinute,
|
|
361
|
+
keepAliveTreatment: this.settings.keepAliveTreatment,
|
|
362
|
+
keepAliveInactivityMultiplier: this.settings.keepAliveInactivityMultiplier,
|
|
363
|
+
extendedKeepAliveLifetime: this.settings.extendedKeepAliveLifetime,
|
|
364
|
+
acceptProxyProtocol: this.settings.acceptProxyProtocol,
|
|
365
|
+
sendProxyProtocol: this.settings.sendProxyProtocol,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* For routes with certificate: 'auto', call certProvisionFunction if set.
|
|
371
|
+
* If the callback returns a cert object, load it into Rust.
|
|
372
|
+
* If it returns 'http01', let Rust handle ACME.
|
|
373
|
+
*/
|
|
374
|
+
private async provisionCertificatesViaCallback(): Promise<void> {
|
|
375
|
+
const provisionFn = this.settings.certProvisionFunction;
|
|
376
|
+
if (!provisionFn) return;
|
|
377
|
+
|
|
378
|
+
for (const route of this.settings.routes) {
|
|
379
|
+
if (route.action.tls?.certificate !== 'auto') continue;
|
|
380
|
+
if (!route.match.domains) continue;
|
|
381
|
+
|
|
382
|
+
const domains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains];
|
|
383
|
+
|
|
384
|
+
for (const domain of domains) {
|
|
385
|
+
if (domain.includes('*')) continue;
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const result: TSmartProxyCertProvisionObject = await provisionFn(domain);
|
|
389
|
+
|
|
390
|
+
if (result === 'http01') {
|
|
391
|
+
// Rust handles ACME for this domain
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Got a static cert object - load it into Rust
|
|
396
|
+
if (result && typeof result === 'object') {
|
|
397
|
+
const certObj = result as plugins.tsclass.network.ICert;
|
|
398
|
+
await this.bridge.loadCertificate(
|
|
399
|
+
domain,
|
|
400
|
+
certObj.publicKey,
|
|
401
|
+
certObj.privateKey,
|
|
402
|
+
);
|
|
403
|
+
logger.log('info', `Certificate loaded via provision function for ${domain}`, { component: 'smart-proxy' });
|
|
404
|
+
}
|
|
405
|
+
} catch (err: any) {
|
|
406
|
+
logger.log('warn', `certProvisionFunction failed for ${domain}: ${err.message}`, { component: 'smart-proxy' });
|
|
407
|
+
|
|
408
|
+
// Fallback to ACME if enabled
|
|
409
|
+
if (this.settings.certProvisionFallbackToAcme !== false) {
|
|
410
|
+
logger.log('info', `Falling back to ACME for ${domain}`, { component: 'smart-proxy' });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
934
413
|
}
|
|
935
414
|
}
|
|
936
|
-
|
|
937
|
-
return warnings;
|
|
938
415
|
}
|
|
939
416
|
|
|
940
|
-
|
|
417
|
+
private isValidDomain(domain: string): boolean {
|
|
418
|
+
if (!domain || domain.length === 0) return false;
|
|
419
|
+
if (domain.includes('*')) return false;
|
|
420
|
+
const validDomainRegex =
|
|
421
|
+
/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
422
|
+
return validDomainRegex.test(domain);
|
|
423
|
+
}
|
|
424
|
+
}
|