@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.
Files changed (72) hide show
  1. package/changelog.md +28 -0
  2. package/dist_rust/rustproxy +0 -0
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/index.d.ts +1 -5
  5. package/dist_ts/index.js +3 -9
  6. package/dist_ts/protocols/common/fragment-handler.js +5 -1
  7. package/dist_ts/proxies/index.d.ts +1 -5
  8. package/dist_ts/proxies/index.js +2 -6
  9. package/dist_ts/proxies/smart-proxy/index.d.ts +5 -10
  10. package/dist_ts/proxies/smart-proxy/index.js +7 -13
  11. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +5 -2
  12. package/dist_ts/proxies/smart-proxy/route-preprocessor.d.ts +37 -0
  13. package/dist_ts/proxies/smart-proxy/route-preprocessor.js +103 -0
  14. package/dist_ts/proxies/smart-proxy/rust-binary-locator.d.ts +23 -0
  15. package/dist_ts/proxies/smart-proxy/rust-binary-locator.js +104 -0
  16. package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.d.ts +74 -0
  17. package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.js +146 -0
  18. package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.d.ts +49 -0
  19. package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.js +259 -0
  20. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +39 -157
  21. package/dist_ts/proxies/smart-proxy/smart-proxy.js +224 -621
  22. package/dist_ts/proxies/smart-proxy/socket-handler-server.d.ts +45 -0
  23. package/dist_ts/proxies/smart-proxy/socket-handler-server.js +253 -0
  24. package/dist_ts/routing/index.d.ts +1 -1
  25. package/dist_ts/routing/index.js +3 -3
  26. package/dist_ts/routing/models/http-types.d.ts +119 -4
  27. package/dist_ts/routing/models/http-types.js +93 -5
  28. package/package.json +1 -1
  29. package/readme.md +470 -219
  30. package/ts/00_commitinfo_data.ts +1 -1
  31. package/ts/index.ts +4 -12
  32. package/ts/protocols/common/fragment-handler.ts +4 -0
  33. package/ts/proxies/index.ts +1 -9
  34. package/ts/proxies/smart-proxy/index.ts +6 -13
  35. package/ts/proxies/smart-proxy/models/interfaces.ts +6 -4
  36. package/ts/proxies/smart-proxy/route-preprocessor.ts +122 -0
  37. package/ts/proxies/smart-proxy/rust-binary-locator.ts +112 -0
  38. package/ts/proxies/smart-proxy/rust-metrics-adapter.ts +161 -0
  39. package/ts/proxies/smart-proxy/rust-proxy-bridge.ts +310 -0
  40. package/ts/proxies/smart-proxy/smart-proxy.ts +282 -798
  41. package/ts/proxies/smart-proxy/socket-handler-server.ts +279 -0
  42. package/ts/routing/index.ts +2 -2
  43. package/ts/routing/models/http-types.ts +147 -4
  44. package/ts/proxies/http-proxy/connection-pool.ts +0 -228
  45. package/ts/proxies/http-proxy/context-creator.ts +0 -145
  46. package/ts/proxies/http-proxy/default-certificates.ts +0 -150
  47. package/ts/proxies/http-proxy/function-cache.ts +0 -279
  48. package/ts/proxies/http-proxy/handlers/index.ts +0 -5
  49. package/ts/proxies/http-proxy/http-proxy.ts +0 -669
  50. package/ts/proxies/http-proxy/http-request-handler.ts +0 -331
  51. package/ts/proxies/http-proxy/http2-request-handler.ts +0 -255
  52. package/ts/proxies/http-proxy/index.ts +0 -18
  53. package/ts/proxies/http-proxy/models/http-types.ts +0 -148
  54. package/ts/proxies/http-proxy/models/index.ts +0 -5
  55. package/ts/proxies/http-proxy/models/types.ts +0 -125
  56. package/ts/proxies/http-proxy/request-handler.ts +0 -878
  57. package/ts/proxies/http-proxy/security-manager.ts +0 -413
  58. package/ts/proxies/http-proxy/websocket-handler.ts +0 -581
  59. package/ts/proxies/smart-proxy/acme-state-manager.ts +0 -112
  60. package/ts/proxies/smart-proxy/cert-store.ts +0 -92
  61. package/ts/proxies/smart-proxy/certificate-manager.ts +0 -895
  62. package/ts/proxies/smart-proxy/connection-manager.ts +0 -809
  63. package/ts/proxies/smart-proxy/http-proxy-bridge.ts +0 -213
  64. package/ts/proxies/smart-proxy/metrics-collector.ts +0 -453
  65. package/ts/proxies/smart-proxy/nftables-manager.ts +0 -271
  66. package/ts/proxies/smart-proxy/port-manager.ts +0 -358
  67. package/ts/proxies/smart-proxy/route-connection-handler.ts +0 -1712
  68. package/ts/proxies/smart-proxy/route-orchestrator.ts +0 -297
  69. package/ts/proxies/smart-proxy/security-manager.ts +0 -269
  70. package/ts/proxies/smart-proxy/throughput-tracker.ts +0 -138
  71. package/ts/proxies/smart-proxy/timeout-manager.ts +0 -196
  72. 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
- // Import mutex for route update synchronization
26
- import { Mutex } from './utils/mutex.js';
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
- // Import route validator
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
- // Import route orchestrator for route management
32
- import { RouteOrchestrator } from './route-orchestrator.js';
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 - Pure route-based API
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
- * Configuration is provided through a set of routes, with each route defining:
48
- * - What to match (ports, domains, paths, client IPs)
49
- * - What to do with matching traffic (forward, redirect, block)
50
- * - How to handle TLS (passthrough, terminate, terminate-and-reencrypt)
51
- * - Security settings (IP restrictions, connection limits)
52
- * - Advanced options (timeout, headers, etc.)
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
- // Port manager handles dynamic listener management
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
- public routeConnectionHandler: RouteConnectionHandler;
68
- public nftablesManager: NFTablesManager;
69
-
70
- // Certificate manager for ACME and static certificates
71
- public certManager: SmartCertManager | null = null;
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
- public acmeStateManager: AcmeStateManager;
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
- // Set reasonable defaults for all settings
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 if provided (support both email and accountEmail)
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, // Enable by default if acme object exists
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, // Enable by default
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 // Preserve any additional fields
76
+ ...this.settings.acme,
168
77
  };
169
78
  }
170
-
171
- // Initialize component managers
172
- this.timeoutManager = new TimeoutManager(this);
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
- // Create other required components
202
- this.tlsManager = new TlsManager(this);
203
- this.httpProxyBridge = new HttpProxyBridge(this);
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
- * Initialize certificate manager
114
+ * Start the proxy.
115
+ * Spawns the Rust binary, configures socket relay if needed, sends routes, handles cert provisioning.
288
116
  */
289
- private async initializeCertificateManager(): Promise<void> {
290
- // Extract global ACME options if any routes use auto certificates
291
- const autoRoutes = this.settings.routes.filter(r =>
292
- r.action.tls?.certificate === 'auto'
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
- 'ACME email is required for automatic certificate provisioning. ' +
332
- 'Please provide email in either:\n' +
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
- // Get listening ports from RouteManager
381
- const listeningPorts = this.routeManager.getListeningPorts();
382
-
383
- // Initialize port usage tracking using RouteOrchestrator
384
- this.portUsageMap = this.routeOrchestrator.updatePortUsageMap(this.settings.routes);
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
- // Provision NFTables rules for routes that use NFTables
394
- for (const route of this.settings.routes) {
395
- if (route.action.forwardingEngine === 'nftables') {
396
- await this.nftablesManager.provisionRoute(route);
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
- // Initialize and start HttpProxy if needed - before port binding
401
- if (this.settings.useHttpProxy && this.settings.useHttpProxy.length > 0) {
402
- await this.httpProxyBridge.initialize();
403
- await this.httpProxyBridge.start();
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
- // Start port listeners using the PortManager BEFORE initializing certificate manager
407
- // This ensures all required ports are bound and ready when adding ACME challenge routes
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
- // Now that ports are listening, provision any required certificates
420
- if (this.certManager) {
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
- if (record.hasKeepAlive) {
465
- keepAliveConnections++;
466
- }
153
+ // Start the Rust proxy
154
+ await this.bridge.startProxy(config);
467
155
 
468
- if (record.usingNetworkProxy) {
469
- httpProxyConnections++;
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
- maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
473
- if (record.outgoingStartTime) {
474
- maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
475
- }
476
- }
161
+ // Handle certProvisionFunction
162
+ await this.provisionCertificatesViaCallback();
477
163
 
478
- // Get termination stats
479
- const terminationStats = this.connectionManager.getTerminationStats();
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
- // Stop all port listeners
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
- * Verify the challenge route has been properly removed from routes
171
+ * Stop the proxy.
578
172
  */
579
- private async verifyChallengeRouteRemoved(): Promise<void> {
580
- const maxRetries = 10;
581
- const retryDelay = 100; // milliseconds
582
-
583
- for (let i = 0; i < maxRetries; i++) {
584
- // Check if the challenge route is still in the active routes
585
- const challengeRouteExists = this.settings.routes.some(r => r.name === 'acme-challenge');
586
-
587
- if (!challengeRouteExists) {
588
- try {
589
- logger.log('info', 'Challenge route successfully removed from routes');
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
- logger.log('error', error);
604
- } catch (logError) {
605
- // Silently handle logging errors
606
- console.log(`[ERROR] ${error}`);
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
- throw new Error(error);
196
+
197
+ logger.log('info', 'SmartProxy shutdown complete.', { component: 'smart-proxy' });
609
198
  }
610
-
199
+
611
200
  /**
612
- * Update routes with new configuration
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
- try {
637
- logger.log('info', `Updating routes (${newRoutes.length} routes)`, {
638
- routeCount: newRoutes.length,
639
- component: 'smart-proxy'
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
- // Update route orchestrator dependencies if cert manager changed
647
- if (this.certManager && !this.routeOrchestrator.getCertManager()) {
648
- this.routeOrchestrator.setCertManager(this.certManager);
649
- }
650
-
651
- // Delegate the complex route update logic to RouteOrchestrator
652
- const updateResult = await this.routeOrchestrator.updateRoutes(
653
- this.settings.routes,
654
- newRoutes,
655
- {
656
- acmePort: this.settings.acme?.port || 80,
657
- acmeOptions: this.certManager?.getAcmeOptions(),
658
- acmeState: this.certManager?.getState(),
659
- globalChallengeRouteActive: this.globalChallengeRouteActive,
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
- // Update settings with the new routes
666
- this.settings.routes = newRoutes;
667
-
668
- // Update global state from orchestrator results
669
- this.globalChallengeRouteActive = updateResult.newChallengeRouteActive;
670
-
671
- // Update port usage map from orchestrator
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
- * Manually provision a certificate for a route
248
+ * Provision a certificate for a named route.
685
249
  */
686
250
  public async provisionCertificate(routeName: string): Promise<void> {
687
- if (!this.certManager) {
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
- if (!this.certManager) {
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): ICertStatus | undefined {
716
- if (!this.certManager) {
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 proxy metrics with clean API
725
- *
726
- * @returns IMetrics interface with grouped metrics methods
269
+ * Get the metrics interface.
727
270
  */
728
271
  public getMetrics(): IMetrics {
729
- return this.metricsCollector;
272
+ return this.metricsAdapter;
730
273
  }
731
-
274
+
732
275
  /**
733
- * Validates if a domain name is valid for certificate issuance
276
+ * Get statistics (async - calls Rust).
734
277
  */
735
- private isValidDomain(domain: string): boolean {
736
- // Very basic domain validation
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 new listening port without changing the route configuration
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
- return this.portManager.addPort(port);
286
+ await this.bridge.addListeningPort(port);
768
287
  }
769
288
 
770
289
  /**
771
- * Stop listening on a specific port without changing the route configuration
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
- return this.portManager.removePort(port);
293
+ await this.bridge.removeListeningPort(port);
781
294
  }
782
295
 
783
296
  /**
784
- * Get a list of all ports currently being listened on
785
- *
786
- * @returns Array of port numbers
297
+ * Get all currently listening ports (async - calls Rust).
787
298
  */
788
- public getListeningPorts(): number[] {
789
- return this.portManager.getListeningPorts();
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 statistics about current connections
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
- // Skip routes without TLS termination or auto certificates
840
- if (route.action.type !== 'forward' ||
841
- !route.action.tls ||
842
- route.action.tls.mode === 'passthrough' ||
843
- route.action.tls.certificate !== 'auto') continue;
844
-
845
- const routeDomains = Array.isArray(route.match.domains)
846
- ? route.match.domains
847
- : [route.match.domains];
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.nftablesManager.getStatus();
330
+ return this.bridge.getNftablesStatus();
867
331
  }
868
-
332
+
333
+ // --- Private helpers ---
334
+
869
335
  /**
870
- * Validate ACME configuration
336
+ * Build the Rust configuration object from TS settings.
871
337
  */
872
- private validateAcmeConfiguration(): string[] {
873
- const warnings: string[] = [];
874
-
875
- // Check for routes with certificate: 'auto'
876
- const autoRoutes = this.settings.routes.filter(r =>
877
- r.action.tls?.certificate === 'auto'
878
- );
879
-
880
- if (autoRoutes.length === 0) {
881
- return warnings;
882
- }
883
-
884
- // Check if we have ACME email configuration
885
- const hasTopLevelEmail = this.settings.acme?.email;
886
- const routesWithEmail = autoRoutes.filter(r => r.action.tls?.acme?.email);
887
-
888
- if (!hasTopLevelEmail && routesWithEmail.length === 0) {
889
- warnings.push(
890
- 'Routes with certificate: "auto" require ACME email configuration. ' +
891
- 'Add email to either top-level "acme" config or individual route\'s "tls.acme" config.'
892
- );
893
- }
894
-
895
- // Check for port 80 availability for challenges
896
- if (autoRoutes.length > 0) {
897
- const challengePort = this.settings.acme?.port || 80;
898
- const portsInUse = this.routeManager.getListeningPorts();
899
-
900
- if (!portsInUse.includes(challengePort)) {
901
- warnings.push(
902
- `Port ${challengePort} is not configured for any routes but is needed for ACME challenges. ` +
903
- `Add a route listening on port ${challengePort} or ensure it's accessible for HTTP-01 challenges.`
904
- );
905
- }
906
- }
907
-
908
- // Check for mismatched environments
909
- if (this.settings.acme?.useProduction) {
910
- const stagingRoutes = autoRoutes.filter(r =>
911
- r.action.tls?.acme?.useProduction === false
912
- );
913
- if (stagingRoutes.length > 0) {
914
- warnings.push(
915
- 'Top-level ACME uses production but some routes use staging. ' +
916
- 'Consider aligning environments to avoid certificate issues.'
917
- );
918
- }
919
- }
920
-
921
- // Check for wildcard domains with auto certificates
922
- for (const route of autoRoutes) {
923
- const domains = Array.isArray(route.match.domains)
924
- ? route.match.domains
925
- : [route.match.domains];
926
-
927
- const wildcardDomains = domains.filter(d => d?.includes('*'));
928
- if (wildcardDomains.length > 0) {
929
- warnings.push(
930
- `Route "${route.name}" has wildcard domain(s) ${wildcardDomains.join(', ')} ` +
931
- 'with certificate: "auto". Wildcard certificates require DNS-01 challenges, ' +
932
- 'which are not currently supported. Use static certificates instead.'
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
+ }