@serve.zone/dcrouter 13.40.3 → 13.41.1

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 (34) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +590 -546
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.dcrouter.d.ts +26 -0
  5. package/dist_ts/classes.dcrouter.js +296 -80
  6. package/dist_ts/db/documents/classes.remote-ingress-hub-settings.doc.d.ts +10 -0
  7. package/dist_ts/db/documents/classes.remote-ingress-hub-settings.doc.js +88 -0
  8. package/dist_ts/db/documents/index.d.ts +1 -0
  9. package/dist_ts/db/documents/index.js +2 -1
  10. package/dist_ts/opsserver/handlers/config.handler.js +2 -2
  11. package/dist_ts/opsserver/handlers/remoteingress.handler.js +71 -55
  12. package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +12 -2
  13. package/dist_ts/remoteingress/classes.remoteingress-manager.js +136 -3
  14. package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +2 -0
  15. package/dist_ts/remoteingress/classes.tunnel-manager.js +45 -13
  16. package/dist_ts_interfaces/data/remoteingress.d.ts +5 -0
  17. package/dist_ts_interfaces/requests/remoteingress.d.ts +30 -1
  18. package/dist_ts_web/00_commitinfo_data.js +1 -1
  19. package/dist_ts_web/appstate.d.ts +4 -0
  20. package/dist_ts_web/appstate.js +30 -2
  21. package/dist_ts_web/elements/network/ops-view-remoteingress.d.ts +4 -0
  22. package/dist_ts_web/elements/network/ops-view-remoteingress.js +148 -1
  23. package/package.json +1 -1
  24. package/ts/00_commitinfo_data.ts +1 -1
  25. package/ts/classes.dcrouter.ts +329 -84
  26. package/ts/db/documents/classes.remote-ingress-hub-settings.doc.ts +29 -0
  27. package/ts/db/documents/index.ts +1 -0
  28. package/ts/opsserver/handlers/config.handler.ts +1 -1
  29. package/ts/opsserver/handlers/remoteingress.handler.ts +97 -74
  30. package/ts/remoteingress/classes.remoteingress-manager.ts +172 -3
  31. package/ts/remoteingress/classes.tunnel-manager.ts +41 -14
  32. package/ts_web/00_commitinfo_data.ts +1 -1
  33. package/ts_web/appstate.ts +41 -1
  34. package/ts_web/elements/network/ops-view-remoteingress.ts +164 -0
@@ -33,6 +33,7 @@ import { DnsManager } from './dns/manager.dns.js';
33
33
  import { AcmeConfigManager } from './acme/manager.acme-config.js';
34
34
  import { EmailDomainManager, SmartMtaStorageManager, WorkAppMailManager, buildEmailDnsRecords } from './email/index.js';
35
35
  import type { IRoute } from '../ts_interfaces/data/route-management.js';
36
+ import type { IDcRouterRouteConfig, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig } from '../ts_interfaces/data/remoteingress.js';
36
37
  import type { ISecurityCompiledPolicy } from '../ts_interfaces/data/security-policy.js';
37
38
 
38
39
  export interface IDcRouterOptions {
@@ -280,6 +281,9 @@ export class DcRouter {
280
281
  // Remote Ingress
281
282
  public remoteIngressManager?: RemoteIngressManager;
282
283
  public tunnelManager?: TunnelManager;
284
+ private remoteIngressHubLifecycleChain: Promise<void> = Promise.resolve();
285
+ private remoteIngressHubStopping = false;
286
+ private remoteIngressHubGeneration = 0;
283
287
 
284
288
  // VPN
285
289
  public vpnManager?: VpnManager;
@@ -326,6 +330,11 @@ export class DcRouter {
326
330
  public serviceManager: plugins.taskbuffer.ServiceManager;
327
331
  private serviceSubjectSubscription?: plugins.smartrx.rxjs.Subscription;
328
332
  public smartAcmeReady = false;
333
+ private smartAcmeServiceStarted = false;
334
+ private smartAcmeStartGeneration = 0;
335
+ private smartAcmeStartPromise?: Promise<void>;
336
+ private smartAcmeRetryTimer?: ReturnType<typeof setTimeout>;
337
+ private smartAcmeRetryAttempt = 0;
329
338
 
330
339
  // TypedRouter for API endpoints
331
340
  public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -545,45 +554,14 @@ export class DcRouter {
545
554
  .optional()
546
555
  .dependsOn('SmartProxy')
547
556
  .withStart(async () => {
548
- if (this.smartAcme) {
549
- await this.smartAcme.start();
550
- this.smartAcmeReady = true;
551
- logger.log('info', 'SmartAcme DNS-01 provider is now ready');
552
-
553
- // Re-trigger certificate provisioning for all auto-cert routes.
554
- // During startup, certProvisionFunction returned 'http01' (SmartAcme not ready),
555
- // but Rust ACME is disabled when certProvisionFunction is set — so all domains
556
- // failed silently (SmartProxy doesn't emit certificate-failed for this path).
557
- // Calling updateRoutes() re-triggers provisionCertificatesViaCallback internally,
558
- // which calls certProvisionFunction again — now with smartAcmeReady === true.
559
- if (this.routeConfigManager) {
560
- // Go through RouteConfigManager to get the full merged route set
561
- // and serialize via the route-update mutex (prevents stale overwrites)
562
- logger.log('info', 'Re-triggering certificate provisioning via RouteConfigManager');
563
- this.routeConfigManager.applyRoutes().catch((err: any) => {
564
- logger.log('warn', `Failed to re-trigger cert provisioning: ${err?.message || err}`);
565
- });
566
- } else if (this.smartProxy) {
567
- // No RouteConfigManager (DB disabled) — re-send current routes to trigger cert provisioning
568
- if (this.certProvisionScheduler) {
569
- this.certProvisionScheduler.clear();
570
- }
571
- const currentRoutes = this.smartProxy.routeManager.getRoutes();
572
- logger.log('info', `Re-triggering certificate provisioning for ${currentRoutes.length} routes`);
573
- this.smartProxy.updateRoutes(currentRoutes).catch((err: any) => {
574
- logger.log('warn', `Failed to re-trigger cert provisioning: ${err?.message || err}`);
575
- });
576
- }
577
- }
557
+ this.smartAcmeServiceStarted = true;
558
+ this.startSmartAcmeInBackground();
578
559
  })
579
560
  .withStop(async () => {
580
- this.smartAcmeReady = false;
581
- if (this.smartAcme) {
582
- await this.smartAcme.stop();
583
- this.smartAcme = undefined;
584
- }
561
+ this.smartAcmeServiceStarted = false;
562
+ await this.stopSmartAcme();
585
563
  })
586
- .withRetry({ maxRetries: 20, baseDelayMs: 5000, maxDelayMs: 3_600_000, backoffFactor: 2 }),
564
+ .withRetry({ maxRetries: 0 }),
587
565
  );
588
566
  }
589
567
 
@@ -613,15 +591,10 @@ export class DcRouter {
613
591
  // Sync routes to RemoteIngressManager whenever routes change,
614
592
  // then push updated derived ports to the Rust hub binary
615
593
  async (routes) => {
616
- if (this.remoteIngressManager) {
617
- this.remoteIngressManager.setRoutes(routes as any[]);
618
- }
619
- if (this.tunnelManager) {
620
- try {
621
- await this.tunnelManager.syncAllowedEdges();
622
- } catch (err: unknown) {
623
- logger.log('error', `Failed to sync Remote Ingress allowed edges: ${(err as Error).message}`);
624
- }
594
+ try {
595
+ await this.updateRemoteIngressRoutes(routes as IDcRouterRouteConfig[]);
596
+ } catch (err: unknown) {
597
+ logger.log('error', `Failed to sync Remote Ingress allowed edges: ${(err as Error).message}`);
625
598
  }
626
599
  },
627
600
  undefined,
@@ -739,11 +712,7 @@ export class DcRouter {
739
712
  await this.setupRemoteIngress();
740
713
  })
741
714
  .withStop(async () => {
742
- if (this.tunnelManager) {
743
- await this.tunnelManager.stop();
744
- this.tunnelManager = undefined;
745
- }
746
- this.remoteIngressManager = undefined;
715
+ await this.stopRemoteIngress();
747
716
  })
748
717
  .withRetry({ maxRetries: 3, baseDelayMs: 2000, maxDelayMs: 30_000 }),
749
718
  );
@@ -783,6 +752,138 @@ export class DcRouter {
783
752
  });
784
753
  }
785
754
 
755
+ private startSmartAcmeInBackground(): void {
756
+ if (!this.smartAcme) {
757
+ this.smartAcmeReady = false;
758
+ return;
759
+ }
760
+
761
+ const generation = ++this.smartAcmeStartGeneration;
762
+ this.smartAcmeReady = false;
763
+ this.smartAcmeRetryAttempt = 0;
764
+ this.clearSmartAcmeRetryTimer();
765
+ this.scheduleSmartAcmeStart(generation, 0);
766
+ }
767
+
768
+ private scheduleSmartAcmeStart(generation: number, delayMs: number): void {
769
+ this.clearSmartAcmeRetryTimer();
770
+ const retryTimer = setTimeout(() => {
771
+ this.smartAcmeRetryTimer = undefined;
772
+ this.runSmartAcmeStartAttempt(generation).catch((err) => {
773
+ logger.log('error', `Unexpected SmartAcme startup error: ${(err as Error).message}`);
774
+ });
775
+ }, delayMs);
776
+ this.smartAcmeRetryTimer = retryTimer;
777
+ const unrefableTimer = retryTimer as any;
778
+ if (typeof unrefableTimer?.unref === 'function') {
779
+ unrefableTimer.unref();
780
+ }
781
+ }
782
+
783
+ private async runSmartAcmeStartAttempt(generation: number): Promise<void> {
784
+ const smartAcme = this.smartAcme;
785
+ if (!smartAcme || generation !== this.smartAcmeStartGeneration) {
786
+ return;
787
+ }
788
+
789
+ const startPromise = smartAcme.start();
790
+ this.smartAcmeStartPromise = startPromise;
791
+
792
+ try {
793
+ await startPromise;
794
+ if (generation !== this.smartAcmeStartGeneration || this.smartAcme !== smartAcme) {
795
+ await smartAcme.stop().catch((err) => {
796
+ logger.log('warn', `Failed to stop stale SmartAcme instance: ${(err as Error).message}`);
797
+ });
798
+ return;
799
+ }
800
+
801
+ this.smartAcmeReady = true;
802
+ this.smartAcmeRetryAttempt = 0;
803
+ logger.log('info', 'SmartAcme DNS-01 provider is now ready');
804
+ this.retriggerCertificateProvisioningAfterSmartAcmeReady();
805
+ } catch (err) {
806
+ if (generation !== this.smartAcmeStartGeneration || this.smartAcme !== smartAcme) {
807
+ return;
808
+ }
809
+
810
+ this.smartAcmeReady = false;
811
+ await smartAcme.stop().catch((stopErr) => {
812
+ logger.log('warn', `Failed to clean up SmartAcme after startup failure: ${(stopErr as Error).message}`);
813
+ });
814
+ this.smartAcmeRetryAttempt++;
815
+ if (this.smartAcmeRetryAttempt > 20) {
816
+ logger.log('error', `SmartAcme DNS-01 provider failed after 20 startup attempts: ${(err as Error).message}`);
817
+ return;
818
+ }
819
+
820
+ const baseDelayMs = 5000;
821
+ const maxDelayMs = 3_600_000;
822
+ const delayMs = Math.min(baseDelayMs * Math.pow(2, this.smartAcmeRetryAttempt - 1), maxDelayMs);
823
+ const jitter = 0.8 + Math.random() * 0.4;
824
+ const actualDelayMs = Math.floor(delayMs * jitter);
825
+ logger.log('warn', `SmartAcme DNS-01 provider startup failed: ${(err as Error).message}; retrying in ${actualDelayMs}ms (attempt ${this.smartAcmeRetryAttempt}/20)`);
826
+ this.scheduleSmartAcmeStart(generation, actualDelayMs);
827
+ } finally {
828
+ if (this.smartAcmeStartPromise === startPromise) {
829
+ this.smartAcmeStartPromise = undefined;
830
+ }
831
+ }
832
+ }
833
+
834
+ private retriggerCertificateProvisioningAfterSmartAcmeReady(): void {
835
+ // During startup, certProvisionFunction returns 'http01' while SmartAcme is not ready,
836
+ // but Rust ACME is disabled when certProvisionFunction is set. Re-applying routes
837
+ // retries provisioning now that DNS-01 is available.
838
+ if (this.routeConfigManager) {
839
+ logger.log('info', 'Re-triggering certificate provisioning via RouteConfigManager');
840
+ this.routeConfigManager.applyRoutes().catch((err: any) => {
841
+ logger.log('warn', `Failed to re-trigger cert provisioning: ${err?.message || err}`);
842
+ });
843
+ return;
844
+ }
845
+
846
+ if (this.smartProxy) {
847
+ if (this.certProvisionScheduler) {
848
+ this.certProvisionScheduler.clear();
849
+ }
850
+ const currentRoutes = this.smartProxy.routeManager.getRoutes();
851
+ logger.log('info', `Re-triggering certificate provisioning for ${currentRoutes.length} routes`);
852
+ this.smartProxy.updateRoutes(currentRoutes).catch((err: any) => {
853
+ logger.log('warn', `Failed to re-trigger cert provisioning: ${err?.message || err}`);
854
+ });
855
+ }
856
+ }
857
+
858
+ private clearSmartAcmeRetryTimer(): void {
859
+ if (this.smartAcmeRetryTimer) {
860
+ clearTimeout(this.smartAcmeRetryTimer);
861
+ this.smartAcmeRetryTimer = undefined;
862
+ }
863
+ }
864
+
865
+ private async stopSmartAcme(): Promise<void> {
866
+ this.smartAcmeStartGeneration++;
867
+ this.smartAcmeReady = false;
868
+ this.smartAcmeRetryAttempt = 0;
869
+ this.clearSmartAcmeRetryTimer();
870
+
871
+ const smartAcme = this.smartAcme;
872
+ if (!smartAcme) {
873
+ return;
874
+ }
875
+
876
+ try {
877
+ await smartAcme.stop();
878
+ } catch (err) {
879
+ logger.log('error', 'Error stopping SmartAcme', { error: String(err) });
880
+ } finally {
881
+ if (this.smartAcme === smartAcme) {
882
+ this.smartAcme = undefined;
883
+ }
884
+ }
885
+ }
886
+
786
887
  public async start() {
787
888
  await this.checkSystemLimits();
788
889
  logger.log('info', 'Starting DcRouter Services');
@@ -1098,17 +1199,13 @@ export class DcRouter {
1098
1199
  // Initialize cert provision scheduler
1099
1200
  this.certProvisionScheduler = new CertProvisionScheduler();
1100
1201
 
1101
- // If we have DNS challenge handlers, create SmartAcme instance and wire certProvisionFunction
1102
- // Note: SmartAcme.start() is NOT called here it runs as a separate optional service
1103
- // via the ServiceManager, with aggressive retry for rate-limit resilience.
1202
+ // If we have DNS challenge handlers, create SmartAcme instance and wire certProvisionFunction.
1203
+ // SmartAcme starts in the background because ACME account setup can be slow or rate-limited,
1204
+ // and must not block dcrouter's global startup timeout.
1205
+ if (this.smartAcme) {
1206
+ await this.stopSmartAcme();
1207
+ }
1104
1208
  if (challengeHandlers.length > 0) {
1105
- // Stop old SmartAcme if it exists (e.g., during updateSmartProxyConfig)
1106
- if (this.smartAcme) {
1107
- this.smartAcmeReady = false;
1108
- await this.smartAcme.stop().catch(err =>
1109
- logger.log('error', 'Error stopping old SmartAcme', { error: String(err) })
1110
- );
1111
- }
1112
1209
  // Safe non-null: challengeHandlers.length > 0 implies both dnsManager
1113
1210
  // and acmeConfig exist (enforced above).
1114
1211
  this.smartAcme = new plugins.smartacme.SmartAcme({
@@ -1118,6 +1215,9 @@ export class DcRouter {
1118
1215
  challengeHandlers: challengeHandlers,
1119
1216
  challengePriority: ['dns-01'],
1120
1217
  });
1218
+ if (this.smartAcmeServiceStarted) {
1219
+ this.startSmartAcmeInBackground();
1220
+ }
1121
1221
 
1122
1222
  const scheduler = this.certProvisionScheduler;
1123
1223
  smartProxyConfig.certProvisionFallbackToAcme = false;
@@ -1319,12 +1419,15 @@ export class DcRouter {
1319
1419
  }
1320
1420
 
1321
1421
  const firewallConfig = await this.securityPolicyManager.compileRemoteIngressFirewall();
1322
- if (this.remoteIngressManager) {
1323
- (this.remoteIngressManager as any).setFirewallConfig?.(firewallConfig);
1324
- }
1325
- if (this.tunnelManager) {
1326
- await this.tunnelManager.syncAllowedEdges();
1327
- }
1422
+ await this.queueRemoteIngressHubTask(async () => {
1423
+ if (this.remoteIngressHubStopping) return;
1424
+ if (this.remoteIngressManager) {
1425
+ this.remoteIngressManager.setFirewallConfig(firewallConfig);
1426
+ }
1427
+ if (this.tunnelManager) {
1428
+ await this.tunnelManager.syncAllowedEdges();
1429
+ }
1430
+ });
1328
1431
  }
1329
1432
 
1330
1433
  private mergeSecurityPolicies(
@@ -2340,28 +2443,180 @@ export class DcRouter {
2340
2443
  }
2341
2444
 
2342
2445
  logger.log('info', 'Setting up Remote Ingress hub...');
2446
+ this.remoteIngressHubStopping = false;
2447
+ const generation = ++this.remoteIngressHubGeneration;
2343
2448
 
2344
2449
  // Initialize the edge registration manager
2345
- this.remoteIngressManager = new RemoteIngressManager();
2346
- await this.remoteIngressManager.initialize();
2347
- this.remoteIngressManager.setFirewallConfig(
2348
- await this.securityPolicyManager?.compileRemoteIngressFirewall(),
2349
- );
2450
+ const remoteIngressManager = new RemoteIngressManager(this.options.remoteIngressConfig.performance);
2451
+ this.remoteIngressManager = remoteIngressManager;
2452
+ await remoteIngressManager.initialize();
2453
+ if (!this.isRemoteIngressHubGenerationCurrent(generation, remoteIngressManager)) {
2454
+ return;
2455
+ }
2456
+
2457
+ const firewallConfig = await this.securityPolicyManager?.compileRemoteIngressFirewall();
2458
+ if (!this.isRemoteIngressHubGenerationCurrent(generation, remoteIngressManager)) {
2459
+ return;
2460
+ }
2461
+ remoteIngressManager.setFirewallConfig(firewallConfig);
2350
2462
 
2351
2463
  // Pass current bootstrap routes so the manager can derive edge ports initially.
2352
2464
  // Once RouteConfigManager applies the full DB set, the onRoutesApplied callback
2353
2465
  // will push the complete merged routes here.
2354
2466
  const bootstrapRoutes = [...this.seedConfigRoutes, ...this.seedEmailRoutes, ...this.runtimeDnsRoutes];
2355
- this.remoteIngressManager.setRoutes(bootstrapRoutes as any[]);
2467
+ remoteIngressManager.setRoutes(bootstrapRoutes as any[]);
2356
2468
 
2357
2469
  // If ConfigManagers finished before us, re-apply routes
2358
2470
  // so the callback delivers the full DB set to our newly-created remoteIngressManager.
2359
2471
  if (this.routeConfigManager) {
2360
2472
  await this.routeConfigManager.applyRoutes();
2361
2473
  }
2474
+ if (!this.isRemoteIngressHubGenerationCurrent(generation, remoteIngressManager)) {
2475
+ return;
2476
+ }
2362
2477
 
2363
- // Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default)
2478
+ await this.queueRemoteIngressHubTask(async () => {
2479
+ await this.startRemoteIngressTunnelHubLocked(generation);
2480
+ });
2481
+ if (!this.isRemoteIngressHubGenerationCurrent(generation, remoteIngressManager)) {
2482
+ return;
2483
+ }
2484
+
2485
+ const edgeCount = remoteIngressManager.getAllEdges().length;
2486
+ logger.log('info', `Remote Ingress hub started on port ${this.options.remoteIngressConfig.tunnelPort || 8443} with ${edgeCount} registered edge(s)`);
2487
+ }
2488
+
2489
+ private isRemoteIngressHubGenerationCurrent(generation: number, manager: RemoteIngressManager): boolean {
2490
+ return !this.remoteIngressHubStopping
2491
+ && generation === this.remoteIngressHubGeneration
2492
+ && this.remoteIngressManager === manager;
2493
+ }
2494
+
2495
+ private queueRemoteIngressHubTask<T>(task: () => Promise<T>): Promise<T> {
2496
+ const run = this.remoteIngressHubLifecycleChain.then(task);
2497
+ this.remoteIngressHubLifecycleChain = run.then(() => undefined, () => undefined);
2498
+ return run;
2499
+ }
2500
+
2501
+ private async stopRemoteIngress(): Promise<void> {
2502
+ this.remoteIngressHubStopping = true;
2503
+ this.remoteIngressHubGeneration++;
2504
+ await this.queueRemoteIngressHubTask(async () => {
2505
+ const currentTunnelManager = this.tunnelManager;
2506
+ this.tunnelManager = undefined;
2507
+ if (currentTunnelManager) {
2508
+ await currentTunnelManager.stop();
2509
+ }
2510
+ });
2511
+ this.remoteIngressManager = undefined;
2512
+ }
2513
+
2514
+ public async mutateRemoteIngressEdges<T>(
2515
+ mutation: (manager: RemoteIngressManager) => Promise<T>,
2516
+ syncAllowedEdges = true,
2517
+ ): Promise<T> {
2518
+ return await this.queueRemoteIngressHubTask(async () => {
2519
+ if (this.remoteIngressHubStopping) {
2520
+ throw new Error('RemoteIngress is stopping');
2521
+ }
2522
+ const manager = this.remoteIngressManager;
2523
+ if (!manager) {
2524
+ throw new Error('RemoteIngress not configured');
2525
+ }
2526
+ const result = await mutation(manager);
2527
+ if (syncAllowedEdges && this.tunnelManager) {
2528
+ await this.tunnelManager.syncAllowedEdges();
2529
+ }
2530
+ return result;
2531
+ });
2532
+ }
2533
+
2534
+ private async updateRemoteIngressRoutes(routes: IDcRouterRouteConfig[]): Promise<void> {
2535
+ await this.queueRemoteIngressHubTask(async () => {
2536
+ if (this.remoteIngressHubStopping) return;
2537
+ if (this.remoteIngressManager) {
2538
+ this.remoteIngressManager.setRoutes(routes);
2539
+ }
2540
+ if (this.tunnelManager) {
2541
+ await this.tunnelManager.syncAllowedEdges();
2542
+ }
2543
+ });
2544
+ }
2545
+
2546
+ public async updateRemoteIngressHubSettings(
2547
+ updates: { performance?: IRemoteIngressPerformanceConfig },
2548
+ updatedBy: string,
2549
+ ): Promise<IRemoteIngressHubSettings> {
2550
+ return await this.queueRemoteIngressHubTask(async () => {
2551
+ if (this.remoteIngressHubStopping) {
2552
+ throw new Error('RemoteIngress is stopping');
2553
+ }
2554
+ if (!this.remoteIngressManager) {
2555
+ throw new Error('RemoteIngress is not configured');
2556
+ }
2557
+
2558
+ const settings = await this.remoteIngressManager.updateHubSettings(updates, updatedBy);
2559
+ if (this.options.remoteIngressConfig?.enabled) {
2560
+ await this.restartRemoteIngressTunnelHubLocked();
2561
+ }
2562
+ return settings;
2563
+ });
2564
+ }
2565
+
2566
+ private async restartRemoteIngressTunnelHubLocked(): Promise<void> {
2567
+ const generation = ++this.remoteIngressHubGeneration;
2568
+ if (!this.remoteIngressManager || !this.options.remoteIngressConfig?.enabled || this.remoteIngressHubStopping) {
2569
+ return;
2570
+ }
2571
+
2572
+ const currentTunnelManager = this.tunnelManager;
2573
+ this.tunnelManager = undefined;
2574
+ if (currentTunnelManager) {
2575
+ await currentTunnelManager.stop();
2576
+ }
2577
+
2578
+ if (this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration) {
2579
+ return;
2580
+ }
2581
+ await this.startRemoteIngressTunnelHubLocked(generation);
2582
+ }
2583
+
2584
+ private async startRemoteIngressTunnelHubLocked(generation: number): Promise<void> {
2364
2585
  const riCfg = this.options.remoteIngressConfig;
2586
+ const manager = this.remoteIngressManager;
2587
+ if (!riCfg?.enabled || !manager || this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration) {
2588
+ return;
2589
+ }
2590
+
2591
+ const tlsConfig = await this.resolveRemoteIngressTlsConfig(riCfg);
2592
+ if (this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration || this.remoteIngressManager !== manager) {
2593
+ return;
2594
+ }
2595
+
2596
+ const tunnelManager = new TunnelManager(manager, {
2597
+ tunnelPort: riCfg.tunnelPort ?? 8443,
2598
+ targetHost: '127.0.0.1',
2599
+ tls: tlsConfig,
2600
+ performance: manager.getHubPerformanceConfig(),
2601
+ });
2602
+ try {
2603
+ await tunnelManager.start();
2604
+ } catch (err) {
2605
+ await tunnelManager.stop().catch(() => {});
2606
+ throw err;
2607
+ }
2608
+
2609
+ if (this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration || this.remoteIngressManager !== manager) {
2610
+ await tunnelManager.stop();
2611
+ return;
2612
+ }
2613
+ this.tunnelManager = tunnelManager;
2614
+ }
2615
+
2616
+ private async resolveRemoteIngressTlsConfig(
2617
+ riCfg: NonNullable<IDcRouterOptions['remoteIngressConfig']>,
2618
+ ): Promise<{ certPem: string; keyPem: string } | undefined> {
2619
+ // Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default)
2365
2620
  let tlsConfig: { certPem: string; keyPem: string } | undefined;
2366
2621
 
2367
2622
  // Priority 1: Explicit cert/key file paths
@@ -2391,17 +2646,7 @@ export class DcRouter {
2391
2646
  logger.log('info', 'No TLS cert configured for RemoteIngress tunnel — using auto-generated self-signed');
2392
2647
  }
2393
2648
 
2394
- // Create and start the tunnel manager
2395
- this.tunnelManager = new TunnelManager(this.remoteIngressManager, {
2396
- tunnelPort: riCfg.tunnelPort ?? 8443,
2397
- targetHost: '127.0.0.1',
2398
- tls: tlsConfig,
2399
- performance: riCfg.performance,
2400
- });
2401
- await this.tunnelManager.start();
2402
-
2403
- const edgeCount = this.remoteIngressManager.getAllEdges().length;
2404
- logger.log('info', `Remote Ingress hub started on port ${this.options.remoteIngressConfig.tunnelPort || 8443} with ${edgeCount} registered edge(s)`);
2649
+ return tlsConfig;
2405
2650
  }
2406
2651
 
2407
2652
  /**
@@ -0,0 +1,29 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import { DcRouterDb } from '../classes.dcrouter-db.js';
3
+ import type { IRemoteIngressPerformanceConfig } from '../../../ts_interfaces/data/remoteingress.js';
4
+
5
+ const getDb = () => DcRouterDb.getInstance().getDb();
6
+
7
+ @plugins.smartdata.Collection(() => getDb())
8
+ export class RemoteIngressHubSettingsDoc extends plugins.smartdata.SmartDataDbDoc<RemoteIngressHubSettingsDoc, RemoteIngressHubSettingsDoc> {
9
+ @plugins.smartdata.unI()
10
+ @plugins.smartdata.svDb()
11
+ public settingsId: string = 'remote-ingress-hub-settings';
12
+
13
+ @plugins.smartdata.svDb()
14
+ public performance?: IRemoteIngressPerformanceConfig;
15
+
16
+ @plugins.smartdata.svDb()
17
+ public updatedAt: number = 0;
18
+
19
+ @plugins.smartdata.svDb()
20
+ public updatedBy: string = '';
21
+
22
+ constructor() {
23
+ super();
24
+ }
25
+
26
+ public static async load(): Promise<RemoteIngressHubSettingsDoc | null> {
27
+ return await RemoteIngressHubSettingsDoc.getInstance({ settingsId: 'remote-ingress-hub-settings' });
28
+ }
29
+ }
@@ -24,6 +24,7 @@ export * from './classes.cert-backoff.doc.js';
24
24
 
25
25
  // Remote ingress document classes
26
26
  export * from './classes.remote-ingress-edge.doc.js';
27
+ export * from './classes.remote-ingress-hub-settings.doc.js';
27
28
 
28
29
  // RADIUS document classes
29
30
  export * from './classes.vlan-mappings.doc.js';
@@ -208,7 +208,7 @@ export class ConfigHandler {
208
208
  hubDomain: riCfg?.hubDomain || null,
209
209
  tlsMode,
210
210
  connectedEdgeIps,
211
- performance: riCfg?.performance,
211
+ performance: dcRouter.remoteIngressManager?.getHubPerformanceConfig() || riCfg?.performance,
212
212
  };
213
213
 
214
214
  return {