@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.
- package/deno.json +1 -1
- package/dist_serve/bundle.js +590 -546
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +26 -0
- package/dist_ts/classes.dcrouter.js +296 -80
- package/dist_ts/db/documents/classes.remote-ingress-hub-settings.doc.d.ts +10 -0
- package/dist_ts/db/documents/classes.remote-ingress-hub-settings.doc.js +88 -0
- package/dist_ts/db/documents/index.d.ts +1 -0
- package/dist_ts/db/documents/index.js +2 -1
- package/dist_ts/opsserver/handlers/config.handler.js +2 -2
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +71 -55
- package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +12 -2
- package/dist_ts/remoteingress/classes.remoteingress-manager.js +136 -3
- package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +2 -0
- package/dist_ts/remoteingress/classes.tunnel-manager.js +45 -13
- package/dist_ts_interfaces/data/remoteingress.d.ts +5 -0
- package/dist_ts_interfaces/requests/remoteingress.d.ts +30 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +4 -0
- package/dist_ts_web/appstate.js +30 -2
- package/dist_ts_web/elements/network/ops-view-remoteingress.d.ts +4 -0
- package/dist_ts_web/elements/network/ops-view-remoteingress.js +148 -1
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +329 -84
- package/ts/db/documents/classes.remote-ingress-hub-settings.doc.ts +29 -0
- package/ts/db/documents/index.ts +1 -0
- package/ts/opsserver/handlers/config.handler.ts +1 -1
- package/ts/opsserver/handlers/remoteingress.handler.ts +97 -74
- package/ts/remoteingress/classes.remoteingress-manager.ts +172 -3
- package/ts/remoteingress/classes.tunnel-manager.ts +41 -14
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +41 -1
- package/ts_web/elements/network/ops-view-remoteingress.ts +164 -0
package/ts/classes.dcrouter.ts
CHANGED
|
@@ -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
|
-
|
|
549
|
-
|
|
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.
|
|
581
|
-
|
|
582
|
-
await this.smartAcme.stop();
|
|
583
|
-
this.smartAcme = undefined;
|
|
584
|
-
}
|
|
561
|
+
this.smartAcmeServiceStarted = false;
|
|
562
|
+
await this.stopSmartAcme();
|
|
585
563
|
})
|
|
586
|
-
.withRetry({ maxRetries:
|
|
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
|
-
|
|
617
|
-
this.
|
|
618
|
-
}
|
|
619
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1103
|
-
//
|
|
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
|
-
|
|
1323
|
-
(this.
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/ts/db/documents/index.ts
CHANGED
|
@@ -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 {
|