@serve.zone/dcrouter 15.0.1 → 15.0.2
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_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/acme/classes.smartacme-lifecycle.d.ts +25 -0
- package/dist_ts/acme/classes.smartacme-lifecycle.js +144 -0
- package/dist_ts/acme/index.d.ts +1 -0
- package/dist_ts/acme/index.js +2 -1
- package/dist_ts/classes.dcrouter.d.ts +21 -139
- package/dist_ts/classes.dcrouter.js +71 -1585
- package/dist_ts/dns/classes.dns-server-runtime.d.ts +37 -0
- package/dist_ts/dns/classes.dns-server-runtime.js +449 -0
- package/dist_ts/dns/index.d.ts +1 -0
- package/dist_ts/dns/index.js +2 -1
- package/dist_ts/email/classes.accepted-email-spool.d.ts +55 -0
- package/dist_ts/email/classes.accepted-email-spool.js +345 -0
- package/dist_ts/email/classes.email-route-builder.d.ts +28 -0
- package/dist_ts/email/classes.email-route-builder.js +260 -0
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/opsserver/handlers/gatewayclient.handler.js +10 -8
- package/dist_ts/remoteingress/classes.hub-lifecycle.d.ts +27 -0
- package/dist_ts/remoteingress/classes.hub-lifecycle.js +241 -0
- package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +1 -2
- package/dist_ts/remoteingress/index.d.ts +1 -0
- package/dist_ts/remoteingress/index.js +2 -1
- package/dist_ts/security/classes.route-policy-augmenter.d.ts +22 -0
- package/dist_ts/security/classes.route-policy-augmenter.js +120 -0
- package/dist_ts/security/index.d.ts +1 -0
- package/dist_ts/security/index.js +2 -1
- package/dist_ts/vpn/classes.vpn-access-resolver.d.ts +34 -0
- package/dist_ts/vpn/classes.vpn-access-resolver.js +101 -0
- package/dist_ts/vpn/index.d.ts +1 -0
- package/dist_ts/vpn/index.js +2 -1
- package/dist_ts_migrations/index.js +92 -9
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/acme/classes.smartacme-lifecycle.ts +155 -0
- package/ts/acme/index.ts +1 -0
- package/ts/classes.dcrouter.ts +118 -1919
- package/ts/dns/classes.dns-server-runtime.ts +525 -0
- package/ts/dns/index.ts +1 -0
- package/ts/email/classes.accepted-email-spool.ts +434 -0
- package/ts/email/classes.email-route-builder.ts +312 -0
- package/ts/email/index.ts +2 -0
- package/ts/opsserver/handlers/gatewayclient.handler.ts +9 -7
- package/ts/remoteingress/classes.hub-lifecycle.ts +278 -0
- package/ts/remoteingress/classes.remoteingress-manager.ts +1 -1
- package/ts/remoteingress/index.ts +1 -0
- package/ts/security/classes.route-policy-augmenter.ts +140 -0
- package/ts/security/index.ts +1 -0
- package/ts/vpn/classes.vpn-access-resolver.ts +126 -0
- package/ts/vpn/index.ts +1 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
package/ts/classes.dcrouter.ts
CHANGED
|
@@ -27,61 +27,22 @@ import { commitinfo } from './00_commitinfo_data.js';
|
|
|
27
27
|
import { OpsServer } from './opsserver/index.js';
|
|
28
28
|
import { MetricsManager } from './monitoring/index.js';
|
|
29
29
|
import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
|
|
30
|
-
import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
|
31
|
-
import { VpnManager, type IVpnManagerConfig } from './vpn/index.js';
|
|
30
|
+
import { RemoteIngressHubLifecycle, RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
|
31
|
+
import { VpnAccessResolver, VpnManager, type IVpnManagerConfig } from './vpn/index.js';
|
|
32
32
|
import { RouteConfigManager, ApiTokenManager, GatewayClientManager, ReferenceResolver, DbSeeder, TargetProfileManager, buildHttpRedirectRuntimeRoutes } from './config/index.js';
|
|
33
33
|
import type { TVpnClientAllowEntry } from './config/classes.route-config-manager.js';
|
|
34
|
-
import { SecurityLogger, ContentScanner, IPReputationChecker, SecurityPolicyManager } from './security/index.js';
|
|
34
|
+
import { SecurityLogger, ContentScanner, IPReputationChecker, SecurityPolicyManager, RoutePolicyAugmenter } from './security/index.js';
|
|
35
35
|
import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
|
|
36
36
|
import { DnsManager } from './dns/manager.dns.js';
|
|
37
|
+
import { DnsServerRuntime } from './dns/classes.dns-server-runtime.js';
|
|
37
38
|
import { AcmeConfigManager } from './acme/manager.acme-config.js';
|
|
38
|
-
import {
|
|
39
|
+
import { SmartAcmeLifecycle } from './acme/classes.smartacme-lifecycle.js';
|
|
40
|
+
import { AcceptedEmailSpool, EmailDomainManager, EmailRouteBuilder, EmailSettingsManager, SmartMtaStorageManager, WorkAppMailManager, type TSmartMtaQueueItemLike } from './email/index.js';
|
|
39
41
|
import type { IRoute } from '../ts_interfaces/data/route-management.js';
|
|
40
42
|
import type { IEmailPortConfig, IEmailServerSettings, IEmailServerSettingsSeed, TEmailServerSettingsUpdate } from '../ts_interfaces/data/email-settings.js';
|
|
41
43
|
import type { IDcRouterRouteConfig, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, TRemoteIngressHubSettingsUpdate } from '../ts_interfaces/data/remoteingress.js';
|
|
42
44
|
import type { ISecurityCompiledPolicy } from '../ts_interfaces/data/security-policy.js';
|
|
43
45
|
|
|
44
|
-
type TInboundProxyProtocolPolicy = NonNullable<plugins.smartproxy.IRouteMatch['inboundProxyProtocol']>;
|
|
45
|
-
const DCROUTER_CACHE_ID_HEADER = 'X-Dcrouter-Cached-Email-Id';
|
|
46
|
-
const ACCEPTED_EMAIL_SPOOL_INTERVAL_MS = 60_000;
|
|
47
|
-
const ACCEPTED_EMAIL_RETRY_DELAY_MS = 5 * 60_000;
|
|
48
|
-
const ACCEPTED_EMAIL_QUEUE_LEASE_MS = 30 * 60_000;
|
|
49
|
-
const ACCEPTED_EMAIL_SPOOL_BATCH_SIZE = 25;
|
|
50
|
-
const ACCEPTED_EMAIL_STOP_DRAIN_TIMEOUT_MS = 30_000;
|
|
51
|
-
|
|
52
|
-
type TSmartMtaQueueItemLike = {
|
|
53
|
-
processingResult?: {
|
|
54
|
-
headers?: Record<string, string>;
|
|
55
|
-
email?: { headers?: Record<string, string> };
|
|
56
|
-
};
|
|
57
|
-
status?: 'pending' | 'processing' | 'queued' | 'delivered' | 'failed' | 'deferred';
|
|
58
|
-
attempts?: number;
|
|
59
|
-
nextAttempt?: Date;
|
|
60
|
-
lastError?: string;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
type TStoredCachedEmailEnvelopeAddress = {
|
|
64
|
-
address: string;
|
|
65
|
-
args?: Record<string, string>;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
type TStoredCachedEmailSession = {
|
|
69
|
-
id?: string;
|
|
70
|
-
clientHostname?: string;
|
|
71
|
-
remoteAddress?: string;
|
|
72
|
-
secure?: boolean;
|
|
73
|
-
authenticated?: boolean;
|
|
74
|
-
user?: IExtendedSmtpSession['user'];
|
|
75
|
-
envelope?: {
|
|
76
|
-
mailFrom?: TStoredCachedEmailEnvelopeAddress;
|
|
77
|
-
rcptTo?: TStoredCachedEmailEnvelopeAddress[];
|
|
78
|
-
};
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
type TStoredCachedEmailRouteData = {
|
|
82
|
-
session?: TStoredCachedEmailSession;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
46
|
export interface IDcRouterOptions {
|
|
86
47
|
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
|
87
48
|
baseDir?: string;
|
|
@@ -320,16 +281,8 @@ export class DcRouter {
|
|
|
320
281
|
// Remote Ingress
|
|
321
282
|
public remoteIngressManager?: RemoteIngressManager;
|
|
322
283
|
public tunnelManager?: TunnelManager;
|
|
323
|
-
private remoteIngressHubLifecycleChain: Promise<void> = Promise.resolve();
|
|
324
284
|
private smartProxyLifecycleChain: Promise<void> = Promise.resolve();
|
|
325
285
|
private emailLifecycleChain: Promise<void> = Promise.resolve();
|
|
326
|
-
private acceptedEmailSpoolTimer?: ReturnType<typeof setInterval> & { unref?: () => void };
|
|
327
|
-
private acceptedEmailSpoolRun?: Promise<void>;
|
|
328
|
-
private acceptedEmailSpoolProcessing = false;
|
|
329
|
-
private acceptedEmailSpoolStopping = false;
|
|
330
|
-
private acceptedEmailQueueUpdatePromises = new Set<Promise<void>>();
|
|
331
|
-
private remoteIngressHubStopping = false;
|
|
332
|
-
private remoteIngressHubGeneration = 0;
|
|
333
286
|
|
|
334
287
|
// VPN
|
|
335
288
|
public vpnManager?: VpnManager;
|
|
@@ -349,16 +302,13 @@ export class DcRouter {
|
|
|
349
302
|
public emailSettingsManager?: EmailSettingsManager;
|
|
350
303
|
public emailDomainManager?: EmailDomainManager;
|
|
351
304
|
public workAppMailManager: WorkAppMailManager;
|
|
305
|
+
public acceptedEmailSpool: AcceptedEmailSpool;
|
|
352
306
|
public securityPolicyManager?: SecurityPolicyManager;
|
|
353
307
|
|
|
354
308
|
// Auto-discovered public IP (populated by generateAuthoritativeRecords)
|
|
355
309
|
public detectedPublicIp: string | null = null;
|
|
356
310
|
|
|
357
311
|
// DNS query logging rate limiter state
|
|
358
|
-
private dnsLogWindowSecond: number = 0; // epoch second of current window
|
|
359
|
-
private dnsLogWindowCount: number = 0; // queries logged this second
|
|
360
|
-
private dnsBatchCount: number = 0;
|
|
361
|
-
private dnsBatchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
362
312
|
|
|
363
313
|
// Certificate status tracking from SmartProxy events (keyed by domain)
|
|
364
314
|
public certificateStatusMap = new Map<string, {
|
|
@@ -376,19 +326,19 @@ export class DcRouter {
|
|
|
376
326
|
// Service lifecycle management
|
|
377
327
|
public serviceManager: plugins.taskbuffer.ServiceManager;
|
|
378
328
|
private serviceSubjectSubscription?: plugins.smartrx.rxjs.Subscription;
|
|
379
|
-
public
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
329
|
+
public smartAcmeLifecycle: SmartAcmeLifecycle;
|
|
330
|
+
public vpnAccessResolver: VpnAccessResolver;
|
|
331
|
+
public remoteIngressHubLifecycle: RemoteIngressHubLifecycle;
|
|
332
|
+
public dnsServerRuntime: DnsServerRuntime;
|
|
333
|
+
public emailRouteBuilder: EmailRouteBuilder;
|
|
334
|
+
public routePolicyAugmenter: RoutePolicyAugmenter;
|
|
385
335
|
|
|
386
336
|
// TypedRouter for API endpoints
|
|
387
337
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
388
338
|
|
|
389
339
|
// Seed routes assembled during setupSmartProxy, passed to RouteConfigManager for DB seeding
|
|
390
340
|
private seedConfigRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
391
|
-
|
|
341
|
+
public seedEmailRoutes: IDcRouterRouteConfig[] = [];
|
|
392
342
|
private seedDnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
393
343
|
// Live DoH routes used during SmartProxy bootstrap before RouteConfigManager re-applies stored routes.
|
|
394
344
|
private runtimeDnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
@@ -409,6 +359,13 @@ export class DcRouter {
|
|
|
409
359
|
plugins.path.join(this.resolvedPaths.dataDir, 'smartmta-storage')
|
|
410
360
|
);
|
|
411
361
|
this.workAppMailManager = new WorkAppMailManager(this);
|
|
362
|
+
this.acceptedEmailSpool = new AcceptedEmailSpool(this);
|
|
363
|
+
this.smartAcmeLifecycle = new SmartAcmeLifecycle(this);
|
|
364
|
+
this.vpnAccessResolver = new VpnAccessResolver(this);
|
|
365
|
+
this.remoteIngressHubLifecycle = new RemoteIngressHubLifecycle(this);
|
|
366
|
+
this.dnsServerRuntime = new DnsServerRuntime(this);
|
|
367
|
+
this.emailRouteBuilder = new EmailRouteBuilder(this);
|
|
368
|
+
this.routePolicyAugmenter = new RoutePolicyAugmenter(this);
|
|
412
369
|
|
|
413
370
|
// Initialize service manager and register all services
|
|
414
371
|
this.serviceManager = new plugins.taskbuffer.ServiceManager({
|
|
@@ -635,7 +592,7 @@ export class DcRouter {
|
|
|
635
592
|
}
|
|
636
593
|
}
|
|
637
594
|
} finally {
|
|
638
|
-
await this.
|
|
595
|
+
await this.smartAcmeLifecycle.stop();
|
|
639
596
|
}
|
|
640
597
|
});
|
|
641
598
|
})
|
|
@@ -652,12 +609,12 @@ export class DcRouter {
|
|
|
652
609
|
.optional()
|
|
653
610
|
.dependsOn('SmartProxy')
|
|
654
611
|
.withStart(async () => {
|
|
655
|
-
this.
|
|
656
|
-
this.
|
|
612
|
+
this.smartAcmeLifecycle.serviceStarted = true;
|
|
613
|
+
this.smartAcmeLifecycle.startInBackground();
|
|
657
614
|
})
|
|
658
615
|
.withStop(async () => {
|
|
659
|
-
this.
|
|
660
|
-
await this.
|
|
616
|
+
this.smartAcmeLifecycle.serviceStarted = false;
|
|
617
|
+
await this.smartAcmeLifecycle.stop();
|
|
661
618
|
})
|
|
662
619
|
.withRetry({ maxRetries: 0 }),
|
|
663
620
|
);
|
|
@@ -684,20 +641,20 @@ export class DcRouter {
|
|
|
684
641
|
this.routeConfigManager = new RouteConfigManager(
|
|
685
642
|
() => this.smartProxy,
|
|
686
643
|
() => this.options.http3,
|
|
687
|
-
this.
|
|
644
|
+
this.vpnAccessResolver.createRouteAllowResolver(),
|
|
688
645
|
this.referenceResolver,
|
|
689
646
|
// Sync routes to RemoteIngressManager whenever routes change,
|
|
690
647
|
// then push updated derived ports to the Rust hub binary
|
|
691
648
|
async (routes) => {
|
|
692
649
|
try {
|
|
693
|
-
await this.
|
|
650
|
+
await this.remoteIngressHubLifecycle.updateRoutes(routes as IDcRouterRouteConfig[]);
|
|
694
651
|
} catch (err: unknown) {
|
|
695
652
|
logger.log('error', `Failed to sync Remote Ingress allowed edges: ${(err as Error).message}`);
|
|
696
653
|
}
|
|
697
654
|
},
|
|
698
655
|
(preparedRoutes) => buildHttpRedirectRuntimeRoutes(preparedRoutes || []),
|
|
699
|
-
(storedRoute: IRoute) => this.hydrateStoredRouteForRuntime(storedRoute),
|
|
700
|
-
(routes) => this.applyInboundProxyProtocolPolicies(routes),
|
|
656
|
+
(storedRoute: IRoute) => this.emailRouteBuilder.hydrateStoredRouteForRuntime(storedRoute),
|
|
657
|
+
(routes) => this.routePolicyAugmenter.applyInboundProxyProtocolPolicies(routes),
|
|
701
658
|
);
|
|
702
659
|
this.apiTokenManager = new ApiTokenManager();
|
|
703
660
|
await this.apiTokenManager.initialize();
|
|
@@ -763,20 +720,11 @@ export class DcRouter {
|
|
|
763
720
|
.optional()
|
|
764
721
|
.dependsOn('SmartProxy', ...((this.options.dbConfig?.enabled !== false) ? ['EmailServer'] : []))
|
|
765
722
|
.withStart(async () => {
|
|
766
|
-
await this.
|
|
723
|
+
await this.dnsServerRuntime.setup();
|
|
767
724
|
})
|
|
768
725
|
.withStop(async () => {
|
|
769
726
|
// Flush pending DNS batch log
|
|
770
|
-
|
|
771
|
-
clearTimeout(this.dnsBatchTimer);
|
|
772
|
-
if (this.dnsBatchCount > 0) {
|
|
773
|
-
logger.log('info', `DNS: ${this.dnsBatchCount} queries processed (final flush)`, { zone: 'dns' });
|
|
774
|
-
}
|
|
775
|
-
this.dnsBatchTimer = null;
|
|
776
|
-
this.dnsBatchCount = 0;
|
|
777
|
-
this.dnsLogWindowSecond = 0;
|
|
778
|
-
this.dnsLogWindowCount = 0;
|
|
779
|
-
}
|
|
727
|
+
this.dnsServerRuntime.flushQueryLogBatch();
|
|
780
728
|
if (this.dnsServer) {
|
|
781
729
|
this.dnsServer.removeAllListeners();
|
|
782
730
|
await this.dnsServer.stop();
|
|
@@ -814,10 +762,10 @@ export class DcRouter {
|
|
|
814
762
|
.optional()
|
|
815
763
|
.dependsOn('SmartProxy', 'RemoteIngressManager')
|
|
816
764
|
.withStart(async () => {
|
|
817
|
-
await this.
|
|
765
|
+
await this.remoteIngressHubLifecycle.setup();
|
|
818
766
|
})
|
|
819
767
|
.withStop(async () => {
|
|
820
|
-
await this.
|
|
768
|
+
await this.remoteIngressHubLifecycle.stop();
|
|
821
769
|
})
|
|
822
770
|
.withRetry({ maxRetries: 3, baseDelayMs: 2000, maxDelayMs: 30_000 }),
|
|
823
771
|
);
|
|
@@ -857,7 +805,7 @@ export class DcRouter {
|
|
|
857
805
|
});
|
|
858
806
|
}
|
|
859
807
|
|
|
860
|
-
|
|
808
|
+
public isRemoteIngressHubEnabled(): boolean {
|
|
861
809
|
return this.remoteIngressManager?.getHubSettings().enabled
|
|
862
810
|
?? this.options.remoteIngressConfig?.enabled
|
|
863
811
|
?? false;
|
|
@@ -893,138 +841,6 @@ export class DcRouter {
|
|
|
893
841
|
return seed;
|
|
894
842
|
}
|
|
895
843
|
|
|
896
|
-
private startSmartAcmeInBackground(): void {
|
|
897
|
-
if (!this.smartAcme) {
|
|
898
|
-
this.smartAcmeReady = false;
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
const generation = ++this.smartAcmeStartGeneration;
|
|
903
|
-
this.smartAcmeReady = false;
|
|
904
|
-
this.smartAcmeRetryAttempt = 0;
|
|
905
|
-
this.clearSmartAcmeRetryTimer();
|
|
906
|
-
this.scheduleSmartAcmeStart(generation, 0);
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
private scheduleSmartAcmeStart(generation: number, delayMs: number): void {
|
|
910
|
-
this.clearSmartAcmeRetryTimer();
|
|
911
|
-
const retryTimer = setTimeout(() => {
|
|
912
|
-
this.smartAcmeRetryTimer = undefined;
|
|
913
|
-
this.runSmartAcmeStartAttempt(generation).catch((err) => {
|
|
914
|
-
logger.log('error', `Unexpected SmartAcme startup error: ${(err as Error).message}`);
|
|
915
|
-
});
|
|
916
|
-
}, delayMs);
|
|
917
|
-
this.smartAcmeRetryTimer = retryTimer;
|
|
918
|
-
const unrefableTimer = retryTimer as any;
|
|
919
|
-
if (typeof unrefableTimer?.unref === 'function') {
|
|
920
|
-
unrefableTimer.unref();
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
private async runSmartAcmeStartAttempt(generation: number): Promise<void> {
|
|
925
|
-
const smartAcme = this.smartAcme;
|
|
926
|
-
if (!smartAcme || generation !== this.smartAcmeStartGeneration) {
|
|
927
|
-
return;
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
const startPromise = smartAcme.start();
|
|
931
|
-
this.smartAcmeStartPromise = startPromise;
|
|
932
|
-
|
|
933
|
-
try {
|
|
934
|
-
await startPromise;
|
|
935
|
-
if (generation !== this.smartAcmeStartGeneration || this.smartAcme !== smartAcme) {
|
|
936
|
-
await smartAcme.stop().catch((err) => {
|
|
937
|
-
logger.log('warn', `Failed to stop stale SmartAcme instance: ${(err as Error).message}`);
|
|
938
|
-
});
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
this.smartAcmeReady = true;
|
|
943
|
-
this.smartAcmeRetryAttempt = 0;
|
|
944
|
-
logger.log('info', 'SmartAcme DNS-01 provider is now ready');
|
|
945
|
-
this.retriggerCertificateProvisioningAfterSmartAcmeReady();
|
|
946
|
-
} catch (err) {
|
|
947
|
-
if (generation !== this.smartAcmeStartGeneration || this.smartAcme !== smartAcme) {
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
this.smartAcmeReady = false;
|
|
952
|
-
await smartAcme.stop().catch((stopErr) => {
|
|
953
|
-
logger.log('warn', `Failed to clean up SmartAcme after startup failure: ${(stopErr as Error).message}`);
|
|
954
|
-
});
|
|
955
|
-
this.smartAcmeRetryAttempt++;
|
|
956
|
-
if (this.smartAcmeRetryAttempt > 20) {
|
|
957
|
-
logger.log('error', `SmartAcme DNS-01 provider failed after 20 startup attempts: ${(err as Error).message}`);
|
|
958
|
-
return;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
const baseDelayMs = 5000;
|
|
962
|
-
const maxDelayMs = 3_600_000;
|
|
963
|
-
const delayMs = Math.min(baseDelayMs * Math.pow(2, this.smartAcmeRetryAttempt - 1), maxDelayMs);
|
|
964
|
-
const jitter = 0.8 + Math.random() * 0.4;
|
|
965
|
-
const actualDelayMs = Math.floor(delayMs * jitter);
|
|
966
|
-
logger.log('warn', `SmartAcme DNS-01 provider startup failed: ${(err as Error).message}; retrying in ${actualDelayMs}ms (attempt ${this.smartAcmeRetryAttempt}/20)`);
|
|
967
|
-
this.scheduleSmartAcmeStart(generation, actualDelayMs);
|
|
968
|
-
} finally {
|
|
969
|
-
if (this.smartAcmeStartPromise === startPromise) {
|
|
970
|
-
this.smartAcmeStartPromise = undefined;
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
private retriggerCertificateProvisioningAfterSmartAcmeReady(): void {
|
|
976
|
-
// During startup, certProvisionFunction returns 'http01' while SmartAcme is not ready,
|
|
977
|
-
// but Rust ACME is disabled when certProvisionFunction is set. Re-applying routes
|
|
978
|
-
// retries provisioning now that DNS-01 is available.
|
|
979
|
-
if (this.routeConfigManager) {
|
|
980
|
-
logger.log('info', 'Re-triggering certificate provisioning via RouteConfigManager');
|
|
981
|
-
this.routeConfigManager.applyRoutes().catch((err: any) => {
|
|
982
|
-
logger.log('warn', `Failed to re-trigger cert provisioning: ${err?.message || err}`);
|
|
983
|
-
});
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
if (this.smartProxy) {
|
|
988
|
-
if (this.certProvisionScheduler) {
|
|
989
|
-
this.certProvisionScheduler.clear();
|
|
990
|
-
}
|
|
991
|
-
const currentRoutes = this.smartProxy.routeManager.getRoutes();
|
|
992
|
-
logger.log('info', `Re-triggering certificate provisioning for ${currentRoutes.length} routes`);
|
|
993
|
-
this.smartProxy.updateRoutes(currentRoutes).catch((err: any) => {
|
|
994
|
-
logger.log('warn', `Failed to re-trigger cert provisioning: ${err?.message || err}`);
|
|
995
|
-
});
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
private clearSmartAcmeRetryTimer(): void {
|
|
1000
|
-
if (this.smartAcmeRetryTimer) {
|
|
1001
|
-
clearTimeout(this.smartAcmeRetryTimer);
|
|
1002
|
-
this.smartAcmeRetryTimer = undefined;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
private async stopSmartAcme(): Promise<void> {
|
|
1007
|
-
this.smartAcmeStartGeneration++;
|
|
1008
|
-
this.smartAcmeReady = false;
|
|
1009
|
-
this.smartAcmeRetryAttempt = 0;
|
|
1010
|
-
this.clearSmartAcmeRetryTimer();
|
|
1011
|
-
|
|
1012
|
-
const smartAcme = this.smartAcme;
|
|
1013
|
-
if (!smartAcme) {
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
try {
|
|
1018
|
-
await smartAcme.stop();
|
|
1019
|
-
} catch (err) {
|
|
1020
|
-
logger.log('error', 'Error stopping SmartAcme', { error: String(err) });
|
|
1021
|
-
} finally {
|
|
1022
|
-
if (this.smartAcme === smartAcme) {
|
|
1023
|
-
this.smartAcme = undefined;
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
844
|
public async start() {
|
|
1029
845
|
await this.checkSystemLimits();
|
|
1030
846
|
logger.log('info', 'Starting DcRouter Services');
|
|
@@ -1196,7 +1012,7 @@ export class DcRouter {
|
|
|
1196
1012
|
this.smartProxy = undefined;
|
|
1197
1013
|
}
|
|
1198
1014
|
} finally {
|
|
1199
|
-
await this.
|
|
1015
|
+
await this.smartAcmeLifecycle.stop();
|
|
1200
1016
|
}
|
|
1201
1017
|
}
|
|
1202
1018
|
|
|
@@ -1207,7 +1023,7 @@ export class DcRouter {
|
|
|
1207
1023
|
|
|
1208
1024
|
this.seedEmailRoutes = [];
|
|
1209
1025
|
if (this.options.emailConfig && this.options.dbConfig?.enabled !== false) {
|
|
1210
|
-
this.seedEmailRoutes = this.generateEmailRoutes(this.options.emailConfig);
|
|
1026
|
+
this.seedEmailRoutes = this.emailRouteBuilder.generateEmailRoutes(this.options.emailConfig);
|
|
1211
1027
|
logger.log('debug', 'Email routes generated', { routes: JSON.stringify(this.seedEmailRoutes) });
|
|
1212
1028
|
} else if (this.options.emailConfig) {
|
|
1213
1029
|
logger.log('warn', 'Email routes skipped because dbConfig.enabled=false and SMTP acceptance requires durable DB persistence');
|
|
@@ -1224,7 +1040,7 @@ export class DcRouter {
|
|
|
1224
1040
|
// Combined routes for SmartProxy bootstrap (before DB routes are loaded)
|
|
1225
1041
|
let routes: plugins.smartproxy.IRouteConfig[] = [
|
|
1226
1042
|
...this.seedConfigRoutes,
|
|
1227
|
-
...this.getRuntimeEmailRoutes(this.seedEmailRoutes as IDcRouterRouteConfig[]),
|
|
1043
|
+
...this.emailRouteBuilder.getRuntimeEmailRoutes(this.seedEmailRoutes as IDcRouterRouteConfig[]),
|
|
1228
1044
|
...this.runtimeDnsRoutes,
|
|
1229
1045
|
];
|
|
1230
1046
|
|
|
@@ -1273,10 +1089,10 @@ export class DcRouter {
|
|
|
1273
1089
|
routes = augmentRoutesWithHttp3(routes, http3Config);
|
|
1274
1090
|
logger.log('info', 'HTTP/3: Augmented qualifying HTTPS routes with QUIC/H3 configuration');
|
|
1275
1091
|
}
|
|
1276
|
-
routes = this.applyInboundProxyProtocolPolicies(routes);
|
|
1092
|
+
routes = this.routePolicyAugmenter.applyInboundProxyProtocolPolicies(routes);
|
|
1277
1093
|
|
|
1278
1094
|
const compiledSecurityPolicy = await this.securityPolicyManager?.compileSmartProxyPolicy();
|
|
1279
|
-
const mergedSecurityPolicy = this.mergeSecurityPolicies(
|
|
1095
|
+
const mergedSecurityPolicy = this.routePolicyAugmenter.mergeSecurityPolicies(
|
|
1280
1096
|
(this.options.smartProxyConfig as any)?.securityPolicy,
|
|
1281
1097
|
compiledSecurityPolicy,
|
|
1282
1098
|
);
|
|
@@ -1359,7 +1175,7 @@ export class DcRouter {
|
|
|
1359
1175
|
// SmartAcme starts in the background because ACME account setup can be slow or rate-limited,
|
|
1360
1176
|
// and must not block dcrouter's global startup timeout.
|
|
1361
1177
|
if (this.smartAcme) {
|
|
1362
|
-
await this.
|
|
1178
|
+
await this.smartAcmeLifecycle.stop();
|
|
1363
1179
|
}
|
|
1364
1180
|
if (challengeHandlers.length > 0) {
|
|
1365
1181
|
// Safe non-null: challengeHandlers.length > 0 implies both dnsManager
|
|
@@ -1371,15 +1187,15 @@ export class DcRouter {
|
|
|
1371
1187
|
challengeHandlers: challengeHandlers,
|
|
1372
1188
|
challengePriority: ['dns-01'],
|
|
1373
1189
|
});
|
|
1374
|
-
if (this.
|
|
1375
|
-
this.
|
|
1190
|
+
if (this.smartAcmeLifecycle.serviceStarted) {
|
|
1191
|
+
this.smartAcmeLifecycle.startInBackground();
|
|
1376
1192
|
}
|
|
1377
1193
|
|
|
1378
1194
|
const scheduler = this.certProvisionScheduler;
|
|
1379
1195
|
smartProxyConfig.certProvisionFallbackToAcme = false;
|
|
1380
1196
|
smartProxyConfig.certProvisionFunction = async (domain, eventComms) => {
|
|
1381
1197
|
// If SmartAcme is not yet ready (still starting or retrying), fall back to HTTP-01
|
|
1382
|
-
if (!this.
|
|
1198
|
+
if (!this.smartAcmeLifecycle.ready) {
|
|
1383
1199
|
eventComms.warn(`SmartAcme not yet initialized, falling back to http-01 for ${domain}`);
|
|
1384
1200
|
return 'http01';
|
|
1385
1201
|
}
|
|
@@ -1492,7 +1308,7 @@ export class DcRouter {
|
|
|
1492
1308
|
if (this.smartProxy === smartProxy) {
|
|
1493
1309
|
this.smartProxy = undefined;
|
|
1494
1310
|
}
|
|
1495
|
-
await this.
|
|
1311
|
+
await this.smartAcmeLifecycle.stop();
|
|
1496
1312
|
if (this.certProvisionScheduler) {
|
|
1497
1313
|
this.certProvisionScheduler.clear();
|
|
1498
1314
|
this.certProvisionScheduler = undefined;
|
|
@@ -1570,7 +1386,7 @@ export class DcRouter {
|
|
|
1570
1386
|
}
|
|
1571
1387
|
|
|
1572
1388
|
const compiledSmartProxyPolicy = await this.securityPolicyManager.compileSmartProxyPolicy();
|
|
1573
|
-
const mergedSecurityPolicy = this.mergeSecurityPolicies(
|
|
1389
|
+
const mergedSecurityPolicy = this.routePolicyAugmenter.mergeSecurityPolicies(
|
|
1574
1390
|
(this.options.smartProxyConfig as any)?.securityPolicy,
|
|
1575
1391
|
compiledSmartProxyPolicy,
|
|
1576
1392
|
);
|
|
@@ -1583,240 +1399,9 @@ export class DcRouter {
|
|
|
1583
1399
|
}
|
|
1584
1400
|
|
|
1585
1401
|
const firewallConfig = await this.securityPolicyManager.compileRemoteIngressFirewall();
|
|
1586
|
-
await this.
|
|
1587
|
-
if (this.remoteIngressHubStopping) return;
|
|
1588
|
-
if (this.remoteIngressManager) {
|
|
1589
|
-
this.remoteIngressManager.setFirewallConfig(firewallConfig);
|
|
1590
|
-
}
|
|
1591
|
-
if (this.tunnelManager) {
|
|
1592
|
-
await this.tunnelManager.syncAllowedEdges();
|
|
1593
|
-
}
|
|
1594
|
-
});
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
private mergeSecurityPolicies(
|
|
1598
|
-
...policies: Array<Partial<ISecurityCompiledPolicy> | undefined>
|
|
1599
|
-
): ISecurityCompiledPolicy | undefined {
|
|
1600
|
-
const blockedIps = new Set<string>();
|
|
1601
|
-
const blockedCidrs = new Set<string>();
|
|
1602
|
-
|
|
1603
|
-
for (const policy of policies) {
|
|
1604
|
-
for (const ip of policy?.blockedIps || []) {
|
|
1605
|
-
if (ip) blockedIps.add(ip);
|
|
1606
|
-
}
|
|
1607
|
-
for (const cidr of policy?.blockedCidrs || []) {
|
|
1608
|
-
if (cidr) blockedCidrs.add(cidr);
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
if (blockedIps.size === 0 && blockedCidrs.size === 0) {
|
|
1613
|
-
return undefined;
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
return {
|
|
1617
|
-
blockedIps: [...blockedIps].sort(),
|
|
1618
|
-
blockedCidrs: [...blockedCidrs].sort(),
|
|
1619
|
-
};
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
private applyInboundProxyProtocolPolicies(
|
|
1625
|
-
routes: plugins.smartproxy.IRouteConfig[],
|
|
1626
|
-
): plugins.smartproxy.IRouteConfig[] {
|
|
1627
|
-
const policiesByListener = new Map<string, TInboundProxyProtocolPolicy>();
|
|
1628
|
-
|
|
1629
|
-
for (const route of routes) {
|
|
1630
|
-
const policy = route.match?.inboundProxyProtocol || this.getDesiredInboundProxyProtocolPolicy(route);
|
|
1631
|
-
if (!policy) {
|
|
1632
|
-
continue;
|
|
1633
|
-
}
|
|
1634
|
-
for (const listenerKey of this.getInboundProxyListenerKeys(route)) {
|
|
1635
|
-
const mergedPolicy = this.mergeInboundProxyProtocolPolicies(
|
|
1636
|
-
policiesByListener.get(listenerKey),
|
|
1637
|
-
policy,
|
|
1638
|
-
);
|
|
1639
|
-
if (mergedPolicy) {
|
|
1640
|
-
policiesByListener.set(listenerKey, mergedPolicy);
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
if (policiesByListener.size === 0) {
|
|
1646
|
-
return routes;
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
return routes.map((route) => {
|
|
1650
|
-
if (route.match?.inboundProxyProtocol) {
|
|
1651
|
-
return route;
|
|
1652
|
-
}
|
|
1653
|
-
let listenerPolicy: TInboundProxyProtocolPolicy | undefined;
|
|
1654
|
-
for (const listenerKey of this.getInboundProxyListenerKeys(route)) {
|
|
1655
|
-
listenerPolicy = this.mergeInboundProxyProtocolPolicies(
|
|
1656
|
-
listenerPolicy,
|
|
1657
|
-
policiesByListener.get(listenerKey),
|
|
1658
|
-
);
|
|
1659
|
-
}
|
|
1660
|
-
if (!listenerPolicy) {
|
|
1661
|
-
return route;
|
|
1662
|
-
}
|
|
1663
|
-
return {
|
|
1664
|
-
...route,
|
|
1665
|
-
match: {
|
|
1666
|
-
...route.match,
|
|
1667
|
-
inboundProxyProtocol: listenerPolicy,
|
|
1668
|
-
},
|
|
1669
|
-
};
|
|
1670
|
-
});
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
private getDesiredInboundProxyProtocolPolicy(
|
|
1674
|
-
route: plugins.smartproxy.IRouteConfig,
|
|
1675
|
-
): TInboundProxyProtocolPolicy | undefined {
|
|
1676
|
-
const dcRoute = route as IDcRouterRouteConfig;
|
|
1677
|
-
if (this.isRemoteIngressHubEnabled() && dcRoute.remoteIngress?.enabled) {
|
|
1678
|
-
const ports = plugins.smartproxy.expandPortRange(route.match.ports as any) as number[];
|
|
1679
|
-
if (ports.some((port) => port === 25 || port === 587)) {
|
|
1680
|
-
return { mode: 'required' };
|
|
1681
|
-
}
|
|
1682
|
-
return { mode: 'optional' };
|
|
1683
|
-
}
|
|
1684
|
-
if (this.options.vpnConfig?.enabled) {
|
|
1685
|
-
return { mode: 'optional' };
|
|
1686
|
-
}
|
|
1687
|
-
return undefined;
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
private getInboundProxyListenerKeys(route: plugins.smartproxy.IRouteConfig): string[] {
|
|
1691
|
-
const ports = plugins.smartproxy.expandPortRange(route.match.ports as any) as number[];
|
|
1692
|
-
const transports = route.match.transport === 'udp'
|
|
1693
|
-
? ['udp']
|
|
1694
|
-
: route.match.transport === 'all'
|
|
1695
|
-
? ['tcp', 'udp']
|
|
1696
|
-
: ['tcp'];
|
|
1697
|
-
const keys: string[] = [];
|
|
1698
|
-
for (const port of ports) {
|
|
1699
|
-
for (const transport of transports) {
|
|
1700
|
-
keys.push(`${transport}:${port}`);
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
return keys;
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
private mergeInboundProxyProtocolPolicies(
|
|
1707
|
-
current?: TInboundProxyProtocolPolicy,
|
|
1708
|
-
next?: TInboundProxyProtocolPolicy,
|
|
1709
|
-
): TInboundProxyProtocolPolicy | undefined {
|
|
1710
|
-
if (!current) return next;
|
|
1711
|
-
if (!next) return current;
|
|
1712
|
-
if (current.mode === 'required') return current;
|
|
1713
|
-
if (next.mode === 'required') return next;
|
|
1714
|
-
if (current.mode === 'optional') return current;
|
|
1715
|
-
if (next.mode === 'optional') return next;
|
|
1716
|
-
return current;
|
|
1402
|
+
await this.remoteIngressHubLifecycle.applyFirewallConfig(firewallConfig);
|
|
1717
1403
|
}
|
|
1718
1404
|
|
|
1719
|
-
/**
|
|
1720
|
-
* Generate SmartProxy routes for email configuration
|
|
1721
|
-
*/
|
|
1722
|
-
private generateEmailRoutes(emailConfig: IUnifiedEmailServerOptions): IDcRouterRouteConfig[] {
|
|
1723
|
-
const emailRoutes: IDcRouterRouteConfig[] = [];
|
|
1724
|
-
|
|
1725
|
-
// Create routes for each email port
|
|
1726
|
-
for (const port of emailConfig.ports) {
|
|
1727
|
-
// Create a descriptive name for the route based on the port
|
|
1728
|
-
let routeName = 'email-route';
|
|
1729
|
-
let tlsMode: 'terminate' | undefined;
|
|
1730
|
-
|
|
1731
|
-
// Handle different email ports differently
|
|
1732
|
-
switch (port) {
|
|
1733
|
-
case 25: // SMTP
|
|
1734
|
-
routeName = 'smtp-route';
|
|
1735
|
-
break;
|
|
1736
|
-
|
|
1737
|
-
case 587: // Submission
|
|
1738
|
-
routeName = 'submission-route';
|
|
1739
|
-
break;
|
|
1740
|
-
|
|
1741
|
-
case 465: // SMTPS
|
|
1742
|
-
routeName = 'smtps-route';
|
|
1743
|
-
tlsMode = 'terminate'; // SmartProxy owns public TLS; backend remains server-first SMTP
|
|
1744
|
-
break;
|
|
1745
|
-
|
|
1746
|
-
default:
|
|
1747
|
-
routeName = `email-port-${port}-route`;
|
|
1748
|
-
|
|
1749
|
-
// Check if we have specific settings for this port
|
|
1750
|
-
if (this.options.emailPortConfig?.portSettings &&
|
|
1751
|
-
this.options.emailPortConfig.portSettings[port]) {
|
|
1752
|
-
const portSettings = this.options.emailPortConfig.portSettings[port];
|
|
1753
|
-
|
|
1754
|
-
// If this port requires TLS termination, set the mode accordingly
|
|
1755
|
-
if (portSettings.terminateTls) {
|
|
1756
|
-
tlsMode = 'terminate';
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
// Override the route name if specified
|
|
1760
|
-
if (portSettings.routeName) {
|
|
1761
|
-
routeName = portSettings.routeName;
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
break;
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
// Create forward action to route to internal email server ports
|
|
1768
|
-
const defaultPortMapping: Record<number, number> = {
|
|
1769
|
-
25: 10025, // SMTP
|
|
1770
|
-
587: 10587, // Submission
|
|
1771
|
-
465: 10465 // SMTPS
|
|
1772
|
-
};
|
|
1773
|
-
|
|
1774
|
-
const portMapping = this.options.emailPortConfig?.portMapping || defaultPortMapping;
|
|
1775
|
-
const internalPort = portMapping[port] || port + 10000;
|
|
1776
|
-
|
|
1777
|
-
let action: any = {
|
|
1778
|
-
type: 'forward',
|
|
1779
|
-
sendProxyProtocol: true,
|
|
1780
|
-
targets: [{
|
|
1781
|
-
host: 'localhost', // Forward to internal email server
|
|
1782
|
-
port: internalPort,
|
|
1783
|
-
sendProxyProtocol: true,
|
|
1784
|
-
}]
|
|
1785
|
-
};
|
|
1786
|
-
|
|
1787
|
-
// Plain SMTP/STARTTLS ports must not carry TLS metadata, or SmartProxy waits for TLS/SNI first.
|
|
1788
|
-
if (tlsMode === 'terminate') {
|
|
1789
|
-
action.tls = {
|
|
1790
|
-
mode: tlsMode,
|
|
1791
|
-
certificate: 'auto',
|
|
1792
|
-
};
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
// Create the route configuration
|
|
1796
|
-
const routeConfig: IDcRouterRouteConfig = {
|
|
1797
|
-
name: routeName,
|
|
1798
|
-
match: {
|
|
1799
|
-
ports: [port],
|
|
1800
|
-
transport: 'tcp',
|
|
1801
|
-
},
|
|
1802
|
-
action: action
|
|
1803
|
-
};
|
|
1804
|
-
|
|
1805
|
-
if (this.isRemoteIngressHubEnabled()) {
|
|
1806
|
-
routeConfig.remoteIngress = { enabled: true };
|
|
1807
|
-
const inboundProxyProtocol = this.getRemoteIngressEmailInboundProxyPolicy(port);
|
|
1808
|
-
if (inboundProxyProtocol) {
|
|
1809
|
-
routeConfig.match.inboundProxyProtocol = inboundProxyProtocol;
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
// Add the route to our list
|
|
1814
|
-
emailRoutes.push(routeConfig);
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
return emailRoutes;
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
1405
|
/**
|
|
1821
1406
|
* Generate SmartProxy routes for DNS configuration
|
|
1822
1407
|
*/
|
|
@@ -1845,7 +1430,7 @@ export class DcRouter {
|
|
|
1845
1430
|
action: includeSocketHandler
|
|
1846
1431
|
? {
|
|
1847
1432
|
type: 'socket-handler' as any,
|
|
1848
|
-
socketHandler: this.
|
|
1433
|
+
socketHandler: this.dnsServerRuntime.createSocketHandler()
|
|
1849
1434
|
} as any
|
|
1850
1435
|
: {
|
|
1851
1436
|
type: 'socket-handler' as any,
|
|
@@ -1858,198 +1443,6 @@ export class DcRouter {
|
|
|
1858
1443
|
return dnsRoutes;
|
|
1859
1444
|
}
|
|
1860
1445
|
|
|
1861
|
-
private getRuntimeEmailRoutes(emailRoutes: IDcRouterRouteConfig[]): plugins.smartproxy.IRouteConfig[] {
|
|
1862
|
-
return emailRoutes.map((route) => this.createServerFirstEmailRuntimeRoute(route) || route);
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
private getCurrentGeneratedEmailRouteNames(): Set<string> {
|
|
1866
|
-
if (this.options.dbConfig?.enabled === false) {
|
|
1867
|
-
return new Set();
|
|
1868
|
-
}
|
|
1869
|
-
const sourceRoutes = this.seedEmailRoutes.length > 0
|
|
1870
|
-
? this.seedEmailRoutes
|
|
1871
|
-
: this.options.emailConfig
|
|
1872
|
-
? this.generateEmailRoutes(this.options.emailConfig)
|
|
1873
|
-
: [];
|
|
1874
|
-
return new Set(sourceRoutes.map((route) => route.name).filter(Boolean) as string[]);
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
private shouldHydrateGeneratedEmailRoute(storedRoute: IRoute): boolean {
|
|
1878
|
-
if (storedRoute.origin !== 'email') {
|
|
1879
|
-
return false;
|
|
1880
|
-
}
|
|
1881
|
-
const routeName = storedRoute.route.name;
|
|
1882
|
-
if (!routeName || !this.getCurrentGeneratedEmailRouteNames().has(routeName)) {
|
|
1883
|
-
return false;
|
|
1884
|
-
}
|
|
1885
|
-
const expectedSystemKey = `email:${routeName}`;
|
|
1886
|
-
return !storedRoute.systemKey || storedRoute.systemKey === expectedSystemKey;
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
private createServerFirstEmailRuntimeRoute(
|
|
1890
|
-
route: plugins.smartproxy.IRouteConfig,
|
|
1891
|
-
): plugins.smartproxy.IRouteConfig | undefined {
|
|
1892
|
-
const action = route.action as any;
|
|
1893
|
-
if (action?.type !== 'forward') {
|
|
1894
|
-
return undefined;
|
|
1895
|
-
}
|
|
1896
|
-
const tlsMode = action.tls?.mode;
|
|
1897
|
-
if (tlsMode === 'terminate-and-reencrypt') {
|
|
1898
|
-
return undefined;
|
|
1899
|
-
}
|
|
1900
|
-
const routePorts = plugins.smartproxy.expandPortRange(route.match?.ports as any) as number[];
|
|
1901
|
-
if (routePorts.length !== 1) {
|
|
1902
|
-
return undefined;
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
const target = action.targets?.[0];
|
|
1906
|
-
if (!target || action.targets.length !== 1 || typeof target.port !== 'number') {
|
|
1907
|
-
return undefined;
|
|
1908
|
-
}
|
|
1909
|
-
if (typeof target.host !== 'string') {
|
|
1910
|
-
return undefined;
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
const targetHost = target.host === 'localhost' ? '127.0.0.1' : target.host;
|
|
1914
|
-
const inboundProxyProtocol = this.getRemoteIngressEmailInboundProxyPolicy(routePorts[0]);
|
|
1915
|
-
return {
|
|
1916
|
-
...route,
|
|
1917
|
-
match: {
|
|
1918
|
-
...route.match,
|
|
1919
|
-
...(inboundProxyProtocol
|
|
1920
|
-
? { inboundProxyProtocol }
|
|
1921
|
-
: {}),
|
|
1922
|
-
},
|
|
1923
|
-
action: {
|
|
1924
|
-
type: 'socket-handler' as any,
|
|
1925
|
-
...(action.tls
|
|
1926
|
-
? { tls: action.tls }
|
|
1927
|
-
: {}),
|
|
1928
|
-
socketHandler: this.createEmailSocketProxyHandler(targetHost, target.port),
|
|
1929
|
-
} as any,
|
|
1930
|
-
};
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
private getRemoteIngressEmailInboundProxyPolicy(
|
|
1934
|
-
port: number,
|
|
1935
|
-
): TInboundProxyProtocolPolicy | undefined {
|
|
1936
|
-
if (!this.isRemoteIngressHubEnabled()) {
|
|
1937
|
-
return undefined;
|
|
1938
|
-
}
|
|
1939
|
-
return { mode: port === 25 || port === 587 ? 'required' : 'optional' };
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
private createEmailSocketProxyHandler(
|
|
1943
|
-
targetHost: string,
|
|
1944
|
-
targetPort: number,
|
|
1945
|
-
): NonNullable<plugins.smartproxy.IRouteConfig['action']['socketHandler']> {
|
|
1946
|
-
return (clientSocket, context) => {
|
|
1947
|
-
let backendSocket: plugins.net.Socket | undefined;
|
|
1948
|
-
let connectTimeout: ReturnType<typeof setTimeout> & { unref?: () => void };
|
|
1949
|
-
let cleanupDone = false;
|
|
1950
|
-
|
|
1951
|
-
const cleanup = () => {
|
|
1952
|
-
if (cleanupDone) return;
|
|
1953
|
-
cleanupDone = true;
|
|
1954
|
-
clearTimeout(connectTimeout);
|
|
1955
|
-
clientSocket.removeListener('timeout', cleanup);
|
|
1956
|
-
clientSocket.removeListener('error', cleanup);
|
|
1957
|
-
clientSocket.removeListener('end', cleanup);
|
|
1958
|
-
clientSocket.removeListener('close', cleanup);
|
|
1959
|
-
backendSocket?.removeListener('timeout', cleanup);
|
|
1960
|
-
backendSocket?.removeListener('error', cleanup);
|
|
1961
|
-
backendSocket?.removeListener('end', cleanup);
|
|
1962
|
-
backendSocket?.removeListener('close', cleanup);
|
|
1963
|
-
clientSocket.destroy();
|
|
1964
|
-
backendSocket?.destroy();
|
|
1965
|
-
};
|
|
1966
|
-
|
|
1967
|
-
connectTimeout = setTimeout(() => {
|
|
1968
|
-
cleanup();
|
|
1969
|
-
}, 30_000);
|
|
1970
|
-
connectTimeout.unref?.();
|
|
1971
|
-
|
|
1972
|
-
clientSocket.setTimeout(300_000);
|
|
1973
|
-
clientSocket.on('timeout', cleanup);
|
|
1974
|
-
clientSocket.on('error', cleanup);
|
|
1975
|
-
clientSocket.on('end', cleanup);
|
|
1976
|
-
clientSocket.on('close', cleanup);
|
|
1977
|
-
|
|
1978
|
-
backendSocket = plugins.net.connect(targetPort, targetHost, () => {
|
|
1979
|
-
clearTimeout(connectTimeout);
|
|
1980
|
-
backendSocket?.setTimeout(300_000);
|
|
1981
|
-
const proxyHeader = this.createProxyProtocolV1Header(
|
|
1982
|
-
context?.clientIp,
|
|
1983
|
-
targetHost,
|
|
1984
|
-
0,
|
|
1985
|
-
targetPort,
|
|
1986
|
-
);
|
|
1987
|
-
if (!proxyHeader) {
|
|
1988
|
-
cleanup();
|
|
1989
|
-
return;
|
|
1990
|
-
}
|
|
1991
|
-
backendSocket!.write(proxyHeader, () => {
|
|
1992
|
-
clientSocket.pipe(backendSocket!);
|
|
1993
|
-
backendSocket!.pipe(clientSocket);
|
|
1994
|
-
});
|
|
1995
|
-
});
|
|
1996
|
-
backendSocket.setTimeout(30_000);
|
|
1997
|
-
backendSocket.on('timeout', cleanup);
|
|
1998
|
-
backendSocket.on('error', cleanup);
|
|
1999
|
-
backendSocket.on('end', cleanup);
|
|
2000
|
-
backendSocket.on('close', cleanup);
|
|
2001
|
-
};
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
|
-
private createProxyProtocolV1Header(
|
|
2005
|
-
sourceIp: string | undefined,
|
|
2006
|
-
destinationIp: string,
|
|
2007
|
-
sourcePort: number,
|
|
2008
|
-
destinationPort: number,
|
|
2009
|
-
): string | undefined {
|
|
2010
|
-
if (!sourceIp || !plugins.net.isIP(sourceIp)) {
|
|
2011
|
-
logger.log('warn', `Cannot create email PROXY protocol header for invalid source IP: ${sourceIp || 'unknown'}`);
|
|
2012
|
-
return undefined;
|
|
2013
|
-
}
|
|
2014
|
-
const sourceFamily = plugins.net.isIP(sourceIp);
|
|
2015
|
-
const destinationAddress = destinationIp === 'localhost' || destinationIp === '127.0.0.1' || destinationIp === '::1'
|
|
2016
|
-
? sourceFamily === 6 ? '::1' : '127.0.0.1'
|
|
2017
|
-
: destinationIp;
|
|
2018
|
-
const destinationFamily = plugins.net.isIP(destinationAddress);
|
|
2019
|
-
if (!destinationFamily) {
|
|
2020
|
-
logger.log('warn', `Cannot create email PROXY protocol header for invalid destination IP: ${destinationIp}`);
|
|
2021
|
-
return undefined;
|
|
2022
|
-
}
|
|
2023
|
-
if (sourceFamily !== destinationFamily) {
|
|
2024
|
-
return undefined;
|
|
2025
|
-
}
|
|
2026
|
-
const protocol = sourceFamily === 6 ? 'TCP6' : 'TCP4';
|
|
2027
|
-
return `PROXY ${protocol} ${sourceIp} ${destinationAddress} ${sourcePort} ${destinationPort}\r\n`;
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
private hydrateStoredRouteForRuntime(storedRoute: IRoute): plugins.smartproxy.IRouteConfig | undefined {
|
|
2031
|
-
const routeName = storedRoute.route.name || '';
|
|
2032
|
-
const isDohRoute = storedRoute.origin === 'dns'
|
|
2033
|
-
&& storedRoute.route.action?.type === 'socket-handler'
|
|
2034
|
-
&& routeName.startsWith('dns-over-https-');
|
|
2035
|
-
|
|
2036
|
-
if (!isDohRoute) {
|
|
2037
|
-
if (this.shouldHydrateGeneratedEmailRoute(storedRoute)) {
|
|
2038
|
-
return this.createServerFirstEmailRuntimeRoute(storedRoute.route);
|
|
2039
|
-
}
|
|
2040
|
-
return undefined;
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
return {
|
|
2044
|
-
...storedRoute.route,
|
|
2045
|
-
action: {
|
|
2046
|
-
...storedRoute.route.action,
|
|
2047
|
-
type: 'socket-handler' as any,
|
|
2048
|
-
socketHandler: this.createDnsSocketHandler(),
|
|
2049
|
-
} as any,
|
|
2050
|
-
};
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
1446
|
/**
|
|
2054
1447
|
* Check if a domain matches a pattern (including wildcard support)
|
|
2055
1448
|
* @param domain The domain to check
|
|
@@ -2217,7 +1610,7 @@ export class DcRouter {
|
|
|
2217
1610
|
if (configuredDecision && !configuredDecision.accepted) {
|
|
2218
1611
|
return configuredDecision;
|
|
2219
1612
|
}
|
|
2220
|
-
const dcrouterDecision = await this.
|
|
1613
|
+
const dcrouterDecision = await this.acceptedEmailSpool.acceptMessage(
|
|
2221
1614
|
context,
|
|
2222
1615
|
configuredDecision ? configuredDecision.continueProcessing === true : true,
|
|
2223
1616
|
);
|
|
@@ -2258,16 +1651,16 @@ export class DcRouter {
|
|
|
2258
1651
|
|
|
2259
1652
|
try {
|
|
2260
1653
|
this.addEmailEventSubscription(emailServer.deliveryQueue, 'itemEnqueued', (item: TSmartMtaQueueItemLike) => {
|
|
2261
|
-
this.
|
|
1654
|
+
this.acceptedEmailSpool.trackQueueUpdate(item, 'queued', 'Unable to update accepted email after queue enqueue');
|
|
2262
1655
|
});
|
|
2263
1656
|
this.addEmailEventSubscription(emailServer.deliveryQueue, 'itemDelivered', (item: TSmartMtaQueueItemLike) => {
|
|
2264
|
-
this.
|
|
1657
|
+
this.acceptedEmailSpool.trackQueueUpdate(item, 'delivered', 'Unable to mark accepted email delivered');
|
|
2265
1658
|
});
|
|
2266
1659
|
this.addEmailEventSubscription(emailServer.deliveryQueue, 'itemDeferred', (item: TSmartMtaQueueItemLike) => {
|
|
2267
|
-
this.
|
|
1660
|
+
this.acceptedEmailSpool.trackQueueUpdate(item, 'queued', 'Unable to defer accepted email');
|
|
2268
1661
|
});
|
|
2269
1662
|
this.addEmailEventSubscription(emailServer.deliveryQueue, 'itemFailed', (item: TSmartMtaQueueItemLike) => {
|
|
2270
|
-
this.
|
|
1663
|
+
this.acceptedEmailSpool.trackQueueUpdate(item, 'failed', 'Unable to mark accepted email failed');
|
|
2271
1664
|
});
|
|
2272
1665
|
|
|
2273
1666
|
// Wire delivery events to MetricsManager and logger using smartmta's public queue APIs.
|
|
@@ -2320,18 +1713,17 @@ export class DcRouter {
|
|
|
2320
1713
|
updateQueueSize();
|
|
2321
1714
|
}
|
|
2322
1715
|
|
|
2323
|
-
await this.
|
|
2324
|
-
this.
|
|
1716
|
+
await this.acceptedEmailSpool.recoverQueuedEmails();
|
|
1717
|
+
this.acceptedEmailSpool.start();
|
|
2325
1718
|
} catch (error: unknown) {
|
|
2326
|
-
this.
|
|
2327
|
-
this.clearAcceptedEmailSpoolTimer();
|
|
1719
|
+
this.acceptedEmailSpool.beginStop();
|
|
2328
1720
|
try {
|
|
2329
1721
|
await emailServer.stop();
|
|
2330
1722
|
} catch (stopError: unknown) {
|
|
2331
1723
|
logger.log('warn', `Error cleaning up failed UnifiedEmailServer setup: ${(stopError as Error).message}`);
|
|
2332
1724
|
}
|
|
2333
|
-
await this.
|
|
2334
|
-
await this.
|
|
1725
|
+
await this.acceptedEmailSpool.stop();
|
|
1726
|
+
await this.acceptedEmailSpool.drainQueueUpdates();
|
|
2335
1727
|
this.clearEmailEventSubscriptions();
|
|
2336
1728
|
if (this.emailServer === emailServer) {
|
|
2337
1729
|
this.emailServer = undefined;
|
|
@@ -2342,421 +1734,63 @@ export class DcRouter {
|
|
|
2342
1734
|
logger.log('info', `Email server started on ports: ${emailConfig.ports.join(', ')}`);
|
|
2343
1735
|
}
|
|
2344
1736
|
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
const rawMessage = context.rawMessage;
|
|
2355
|
-
const session = context.session;
|
|
2356
|
-
const envelope = session.envelope;
|
|
2357
|
-
const envelopeRecipients = Array.isArray(envelope.rcptTo)
|
|
2358
|
-
? envelope.rcptTo.map((recipient) => recipient.address).filter(Boolean)
|
|
2359
|
-
: [];
|
|
2360
|
-
const email = context.email;
|
|
2361
|
-
const headers = email.headers;
|
|
2362
|
-
|
|
2363
|
-
const cachedEmail = CachedEmail.createNew();
|
|
2364
|
-
this.removeHeader(email.headers, DCROUTER_CACHE_ID_HEADER);
|
|
2365
|
-
email.headers[DCROUTER_CACHE_ID_HEADER] = cachedEmail.id;
|
|
2366
|
-
cachedEmail.messageId = headers['Message-ID'] || headers['message-id'] || cachedEmail.id;
|
|
2367
|
-
cachedEmail.from = envelope.mailFrom?.address || email.from || '';
|
|
2368
|
-
cachedEmail.to = envelopeRecipients.length > 0
|
|
2369
|
-
? envelopeRecipients
|
|
2370
|
-
: Array.isArray(email.to) ? email.to : [];
|
|
2371
|
-
cachedEmail.cc = Array.isArray(email.cc) ? email.cc : [];
|
|
2372
|
-
cachedEmail.bcc = Array.isArray(email.bcc) ? email.bcc : [];
|
|
2373
|
-
cachedEmail.subject = email.subject || '';
|
|
2374
|
-
cachedEmail.rawContent = this.setDcRouterCacheIdHeader(rawMessage.toString('utf8'), cachedEmail.id);
|
|
2375
|
-
cachedEmail.status = 'pending';
|
|
2376
|
-
cachedEmail.nextAttempt = new Date();
|
|
2377
|
-
cachedEmail.routeData = JSON.stringify({
|
|
2378
|
-
acceptedAt: new Date().toISOString(),
|
|
2379
|
-
session: {
|
|
2380
|
-
id: session.id,
|
|
2381
|
-
remoteAddress: session.remoteAddress,
|
|
2382
|
-
clientHostname: session.clientHostname,
|
|
2383
|
-
secure: !!session.secure,
|
|
2384
|
-
authenticated: !!session.authenticated,
|
|
2385
|
-
user: session.user,
|
|
2386
|
-
envelope,
|
|
2387
|
-
},
|
|
2388
|
-
});
|
|
2389
|
-
cachedEmail.updateSenderDomain();
|
|
2390
|
-
await cachedEmail.save();
|
|
2391
|
-
if (context.abortSignal?.aborted) {
|
|
2392
|
-
cachedEmail.markFailed('Message acceptance aborted before SMTP success');
|
|
2393
|
-
await cachedEmail.save();
|
|
2394
|
-
throw new Error('Message acceptance aborted before SMTP success');
|
|
2395
|
-
}
|
|
1737
|
+
|
|
1738
|
+
/**
|
|
1739
|
+
* Update the unified email configuration
|
|
1740
|
+
* @param config New email configuration
|
|
1741
|
+
*/
|
|
1742
|
+
public async updateEmailConfig(config: IUnifiedEmailServerOptions): Promise<void> {
|
|
1743
|
+
await this.queueEmailLifecycleTask(async () => {
|
|
1744
|
+
// Stop existing email components
|
|
1745
|
+
await this.stopUnifiedEmailComponents();
|
|
2396
1746
|
|
|
2397
|
-
|
|
2398
|
-
this.
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
await cachedEmail.save();
|
|
2402
|
-
}
|
|
1747
|
+
// Update configuration
|
|
1748
|
+
this.options.emailConfig = config;
|
|
1749
|
+
this.emailDomainManager?.setBaseEmailDomains(config.domains as IEmailDomainConfig[] | undefined);
|
|
1750
|
+
await this.emailDomainManager?.syncManagedDomainsToRuntime();
|
|
2403
1751
|
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
smtpCode: 250,
|
|
2407
|
-
smtpMessage: '2.0.0 Message accepted for delivery',
|
|
2408
|
-
continueProcessing: false,
|
|
2409
|
-
};
|
|
2410
|
-
}
|
|
1752
|
+
// Start email handling with new configuration
|
|
1753
|
+
await this.setupUnifiedEmailHandling();
|
|
2411
1754
|
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
const sanitizedContent = rawContent.replace(headerRegex, '');
|
|
2415
|
-
return `${DCROUTER_CACHE_ID_HEADER}: ${cachedEmailId}\r\n${sanitizedContent}`;
|
|
1755
|
+
logger.log('info', 'Unified email configuration updated');
|
|
1756
|
+
});
|
|
2416
1757
|
}
|
|
2417
1758
|
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
cachedEmail.nextAttempt = new Date(Date.now() + ACCEPTED_EMAIL_QUEUE_LEASE_MS);
|
|
2426
|
-
await cachedEmail.save();
|
|
2427
|
-
|
|
2428
|
-
try {
|
|
2429
|
-
await emailServer.processEmailByMode(emailData, session);
|
|
2430
|
-
if (session.matchedRoute?.action.type === 'forward') {
|
|
2431
|
-
cachedEmail.markDelivered();
|
|
2432
|
-
} else {
|
|
2433
|
-
const currentCachedEmail = await CachedEmail.findById(cachedEmail.id) || cachedEmail;
|
|
2434
|
-
if (this.isCachedEmailTerminal(currentCachedEmail)) {
|
|
2435
|
-
return;
|
|
2436
|
-
}
|
|
2437
|
-
currentCachedEmail.status = 'queued';
|
|
2438
|
-
currentCachedEmail.nextAttempt = new Date(Date.now() + ACCEPTED_EMAIL_QUEUE_LEASE_MS);
|
|
2439
|
-
await currentCachedEmail.save();
|
|
2440
|
-
return;
|
|
1759
|
+
public async updateEmailServerSettings(
|
|
1760
|
+
settings: TEmailServerSettingsUpdate,
|
|
1761
|
+
updatedBy = 'system',
|
|
1762
|
+
): Promise<IEmailServerSettings> {
|
|
1763
|
+
return await this.queueEmailLifecycleTask(async () => {
|
|
1764
|
+
if (!this.emailSettingsManager) {
|
|
1765
|
+
throw new Error('EmailSettingsManager is not initialized');
|
|
2441
1766
|
}
|
|
2442
|
-
await cachedEmail.save();
|
|
2443
|
-
} catch (error: unknown) {
|
|
2444
|
-
cachedEmail.scheduleRetry(ACCEPTED_EMAIL_RETRY_DELAY_MS);
|
|
2445
|
-
cachedEmail.lastError = (error as Error).message;
|
|
2446
|
-
await cachedEmail.save();
|
|
2447
|
-
logger.log('warn', `Accepted email ${cachedEmail.id} deferred after SmartMTA handoff failure: ${(error as Error).message}`);
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
|
|
2451
|
-
private startAcceptedEmailSpoolProcessor(): void {
|
|
2452
|
-
this.clearAcceptedEmailSpoolTimer();
|
|
2453
|
-
this.acceptedEmailSpoolStopping = false;
|
|
2454
|
-
const runProcessor = () => {
|
|
2455
|
-
this.runAcceptedEmailSpoolProcessor();
|
|
2456
|
-
};
|
|
2457
|
-
this.acceptedEmailSpoolTimer = setInterval(
|
|
2458
|
-
runProcessor,
|
|
2459
|
-
ACCEPTED_EMAIL_SPOOL_INTERVAL_MS,
|
|
2460
|
-
) as ReturnType<typeof setInterval> & { unref?: () => void };
|
|
2461
|
-
this.acceptedEmailSpoolTimer.unref?.();
|
|
2462
|
-
runProcessor();
|
|
2463
|
-
}
|
|
2464
|
-
|
|
2465
|
-
private clearAcceptedEmailSpoolTimer(): void {
|
|
2466
|
-
if (this.acceptedEmailSpoolTimer) {
|
|
2467
|
-
clearInterval(this.acceptedEmailSpoolTimer);
|
|
2468
|
-
this.acceptedEmailSpoolTimer = undefined;
|
|
2469
|
-
}
|
|
2470
|
-
}
|
|
2471
1767
|
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
spoolRun,
|
|
2479
|
-
ACCEPTED_EMAIL_STOP_DRAIN_TIMEOUT_MS,
|
|
2480
|
-
);
|
|
2481
|
-
if (!settled) {
|
|
2482
|
-
logger.log('warn', 'Timed out waiting for accepted email spool processing to stop');
|
|
2483
|
-
}
|
|
2484
|
-
}
|
|
2485
|
-
}
|
|
1768
|
+
const updatedSettings = await this.emailSettingsManager.updateSettings(settings, updatedBy);
|
|
1769
|
+
this.emailDomainManager?.setBaseEmailDomains(this.options.emailConfig?.domains as IEmailDomainConfig[] | undefined);
|
|
1770
|
+
await this.emailDomainManager?.syncManagedDomainsToRuntime();
|
|
1771
|
+
this.seedEmailRoutes = this.options.emailConfig
|
|
1772
|
+
? this.emailRouteBuilder.generateEmailRoutes(this.options.emailConfig)
|
|
1773
|
+
: [];
|
|
2486
1774
|
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
});
|
|
2494
|
-
this.acceptedEmailSpoolRun = run;
|
|
2495
|
-
void run.finally(() => {
|
|
2496
|
-
if (this.acceptedEmailSpoolRun === run) {
|
|
2497
|
-
this.acceptedEmailSpoolRun = undefined;
|
|
1775
|
+
if (this.routeConfigManager) {
|
|
1776
|
+
await this.routeConfigManager.initialize(
|
|
1777
|
+
this.seedConfigRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
1778
|
+
this.seedEmailRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
1779
|
+
this.seedDnsRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
1780
|
+
);
|
|
2498
1781
|
}
|
|
2499
|
-
});
|
|
2500
|
-
}
|
|
2501
1782
|
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
return;
|
|
2506
|
-
}
|
|
2507
|
-
this.acceptedEmailSpoolProcessing = true;
|
|
2508
|
-
try {
|
|
2509
|
-
const cachedEmails = await CachedEmail.findPendingForDelivery(ACCEPTED_EMAIL_SPOOL_BATCH_SIZE);
|
|
2510
|
-
for (const cachedEmail of cachedEmails) {
|
|
2511
|
-
if (this.acceptedEmailSpoolStopping || this.emailServer !== emailServer) {
|
|
2512
|
-
break;
|
|
1783
|
+
if (this.options.emailConfig) {
|
|
1784
|
+
if (this.emailServer) {
|
|
1785
|
+
await this.stopUnifiedEmailComponents();
|
|
2513
1786
|
}
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
await this.
|
|
1787
|
+
await this.setupUnifiedEmailHandling();
|
|
1788
|
+
} else if (this.emailServer) {
|
|
1789
|
+
await this.stopUnifiedEmailComponents();
|
|
2517
1790
|
}
|
|
2518
|
-
} finally {
|
|
2519
|
-
this.acceptedEmailSpoolProcessing = false;
|
|
2520
|
-
}
|
|
2521
|
-
}
|
|
2522
1791
|
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
const storedSession = routeData.session || {};
|
|
2526
|
-
const storedEnvelope = storedSession.envelope || {};
|
|
2527
|
-
const storedRcptTo = Array.isArray(storedEnvelope.rcptTo) && storedEnvelope.rcptTo.length > 0
|
|
2528
|
-
? storedEnvelope.rcptTo
|
|
2529
|
-
: cachedEmail.to.map((address) => ({ address, args: {} }));
|
|
2530
|
-
const rcptTo = storedRcptTo
|
|
2531
|
-
.filter((recipient) => !!recipient.address)
|
|
2532
|
-
.map((recipient) => ({ address: recipient.address, args: recipient.args || {} }));
|
|
2533
|
-
const mailFrom = storedEnvelope.mailFrom
|
|
2534
|
-
? { address: storedEnvelope.mailFrom.address, args: storedEnvelope.mailFrom.args || {} }
|
|
2535
|
-
: { address: cachedEmail.from || '', args: {} };
|
|
2536
|
-
const session = {
|
|
2537
|
-
id: `${storedSession.id || cachedEmail.id}-replay-${Date.now()}`,
|
|
2538
|
-
state: 'DATA' as unknown as IExtendedSmtpSession['state'],
|
|
2539
|
-
clientHostname: storedSession.clientHostname || '',
|
|
2540
|
-
mailFrom: mailFrom.address,
|
|
2541
|
-
rcptTo: rcptTo.map((recipient) => recipient.address),
|
|
2542
|
-
emailData: cachedEmail.rawContent || '',
|
|
2543
|
-
useTLS: !!storedSession.secure,
|
|
2544
|
-
connectionEnded: false,
|
|
2545
|
-
remoteAddress: storedSession.remoteAddress || '127.0.0.1',
|
|
2546
|
-
secure: !!storedSession.secure,
|
|
2547
|
-
authenticated: !!storedSession.authenticated,
|
|
2548
|
-
envelope: {
|
|
2549
|
-
mailFrom,
|
|
2550
|
-
rcptTo,
|
|
2551
|
-
},
|
|
2552
|
-
} as IExtendedSmtpSession;
|
|
2553
|
-
if (storedSession.user) {
|
|
2554
|
-
session.user = storedSession.user;
|
|
2555
|
-
}
|
|
2556
|
-
return session;
|
|
2557
|
-
}
|
|
2558
|
-
|
|
2559
|
-
private parseCachedEmailRouteData(cachedEmail: CachedEmail): TStoredCachedEmailRouteData {
|
|
2560
|
-
try {
|
|
2561
|
-
return cachedEmail.routeData ? JSON.parse(cachedEmail.routeData) : {};
|
|
2562
|
-
} catch {
|
|
2563
|
-
return {};
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
private async updateAcceptedEmailFromQueueItem(
|
|
2568
|
-
item: TSmartMtaQueueItemLike,
|
|
2569
|
-
status: 'queued' | 'delivered' | 'failed',
|
|
2570
|
-
): Promise<void> {
|
|
2571
|
-
const cachedEmailId = this.getCachedEmailIdFromQueueItem(item);
|
|
2572
|
-
if (!cachedEmailId || !this.dcRouterDb?.isReady()) {
|
|
2573
|
-
return;
|
|
2574
|
-
}
|
|
2575
|
-
const cachedEmail = await CachedEmail.findById(cachedEmailId);
|
|
2576
|
-
if (!cachedEmail) {
|
|
2577
|
-
return;
|
|
2578
|
-
}
|
|
2579
|
-
if (this.isCachedEmailTerminal(cachedEmail) && status !== 'delivered') {
|
|
2580
|
-
return;
|
|
2581
|
-
}
|
|
2582
|
-
cachedEmail.attempts = Math.max(cachedEmail.attempts || 0, item.attempts || 0);
|
|
2583
|
-
if (status === 'delivered') {
|
|
2584
|
-
cachedEmail.markDelivered();
|
|
2585
|
-
} else if (status === 'failed') {
|
|
2586
|
-
cachedEmail.markFailed(item.lastError || 'SmartMTA delivery failed');
|
|
2587
|
-
} else {
|
|
2588
|
-
cachedEmail.status = 'queued';
|
|
2589
|
-
cachedEmail.nextAttempt = new Date(Date.now() + ACCEPTED_EMAIL_QUEUE_LEASE_MS);
|
|
2590
|
-
}
|
|
2591
|
-
await cachedEmail.save();
|
|
2592
|
-
}
|
|
2593
|
-
|
|
2594
|
-
private trackAcceptedEmailQueueUpdate(
|
|
2595
|
-
item: TSmartMtaQueueItemLike,
|
|
2596
|
-
status: 'queued' | 'delivered' | 'failed',
|
|
2597
|
-
failureMessage: string,
|
|
2598
|
-
): void {
|
|
2599
|
-
const updatePromise = this.updateAcceptedEmailFromQueueItem(item, status).catch((error) => {
|
|
2600
|
-
logger.log('warn', `${failureMessage}: ${(error as Error).message}`);
|
|
2601
|
-
});
|
|
2602
|
-
this.acceptedEmailQueueUpdatePromises.add(updatePromise);
|
|
2603
|
-
void updatePromise.finally(() => {
|
|
2604
|
-
this.acceptedEmailQueueUpdatePromises.delete(updatePromise);
|
|
2605
|
-
});
|
|
2606
|
-
}
|
|
2607
|
-
|
|
2608
|
-
private async drainAcceptedEmailQueueUpdates(): Promise<void> {
|
|
2609
|
-
const queueUpdates = [...this.acceptedEmailQueueUpdatePromises];
|
|
2610
|
-
if (queueUpdates.length === 0) {
|
|
2611
|
-
return;
|
|
2612
|
-
}
|
|
2613
|
-
const settled = await this.waitForPromiseToSettleWithTimeout(
|
|
2614
|
-
Promise.allSettled(queueUpdates).then(() => undefined),
|
|
2615
|
-
ACCEPTED_EMAIL_STOP_DRAIN_TIMEOUT_MS,
|
|
2616
|
-
);
|
|
2617
|
-
if (!settled) {
|
|
2618
|
-
for (const queueUpdate of queueUpdates) {
|
|
2619
|
-
this.acceptedEmailQueueUpdatePromises.delete(queueUpdate);
|
|
2620
|
-
}
|
|
2621
|
-
logger.log('warn', `Timed out waiting for ${queueUpdates.length} accepted email queue update(s) to settle`);
|
|
2622
|
-
}
|
|
2623
|
-
}
|
|
2624
|
-
|
|
2625
|
-
private async waitForPromiseToSettleWithTimeout(
|
|
2626
|
-
promise: Promise<unknown>,
|
|
2627
|
-
timeoutMs: number,
|
|
2628
|
-
): Promise<boolean> {
|
|
2629
|
-
let timeout: (ReturnType<typeof setTimeout> & { unref?: () => void }) | undefined;
|
|
2630
|
-
return await new Promise<boolean>((resolve) => {
|
|
2631
|
-
let settled = false;
|
|
2632
|
-
const settle = (didSettle: boolean) => {
|
|
2633
|
-
if (settled) {
|
|
2634
|
-
return;
|
|
2635
|
-
}
|
|
2636
|
-
settled = true;
|
|
2637
|
-
if (timeout) {
|
|
2638
|
-
clearTimeout(timeout);
|
|
2639
|
-
}
|
|
2640
|
-
resolve(didSettle);
|
|
2641
|
-
};
|
|
2642
|
-
timeout = setTimeout(() => settle(false), timeoutMs) as ReturnType<typeof setTimeout> & { unref?: () => void };
|
|
2643
|
-
timeout.unref?.();
|
|
2644
|
-
promise.then(
|
|
2645
|
-
() => settle(true),
|
|
2646
|
-
() => settle(true),
|
|
2647
|
-
);
|
|
2648
|
-
});
|
|
2649
|
-
}
|
|
2650
|
-
|
|
2651
|
-
private async recoverQueuedAcceptedEmails(): Promise<void> {
|
|
2652
|
-
while (true) {
|
|
2653
|
-
const queuedEmails = await CachedEmail.findQueuedForRecovery(ACCEPTED_EMAIL_SPOOL_BATCH_SIZE);
|
|
2654
|
-
if (queuedEmails.length === 0) {
|
|
2655
|
-
return;
|
|
2656
|
-
}
|
|
2657
|
-
for (const queuedEmail of queuedEmails) {
|
|
2658
|
-
if (this.isCachedEmailTerminal(queuedEmail)) {
|
|
2659
|
-
continue;
|
|
2660
|
-
}
|
|
2661
|
-
queuedEmail.status = 'pending';
|
|
2662
|
-
queuedEmail.nextAttempt = new Date();
|
|
2663
|
-
await queuedEmail.save();
|
|
2664
|
-
}
|
|
2665
|
-
if (queuedEmails.length < ACCEPTED_EMAIL_SPOOL_BATCH_SIZE) {
|
|
2666
|
-
return;
|
|
2667
|
-
}
|
|
2668
|
-
}
|
|
2669
|
-
}
|
|
2670
|
-
|
|
2671
|
-
private isCachedEmailTerminal(cachedEmail: CachedEmail): boolean {
|
|
2672
|
-
return cachedEmail.status === 'delivered' || cachedEmail.status === 'failed';
|
|
2673
|
-
}
|
|
2674
|
-
|
|
2675
|
-
private getCachedEmailIdFromQueueItem(item: TSmartMtaQueueItemLike): string | undefined {
|
|
2676
|
-
return this.getHeaderValue(item.processingResult?.headers, DCROUTER_CACHE_ID_HEADER)
|
|
2677
|
-
|| this.getHeaderValue(item.processingResult?.email?.headers, DCROUTER_CACHE_ID_HEADER);
|
|
2678
|
-
}
|
|
2679
|
-
|
|
2680
|
-
private getHeaderValue(headers: Record<string, string> | undefined, headerName: string): string | undefined {
|
|
2681
|
-
if (!headers) {
|
|
2682
|
-
return undefined;
|
|
2683
|
-
}
|
|
2684
|
-
const normalizedHeaderName = headerName.toLowerCase();
|
|
2685
|
-
const matchingHeaderName = Object.keys(headers).find((key) => key.toLowerCase() === normalizedHeaderName);
|
|
2686
|
-
return matchingHeaderName ? headers[matchingHeaderName] : undefined;
|
|
2687
|
-
}
|
|
2688
|
-
|
|
2689
|
-
private removeHeader(headers: Record<string, string>, headerName: string): void {
|
|
2690
|
-
const normalizedHeaderName = headerName.toLowerCase();
|
|
2691
|
-
for (const key of Object.keys(headers)) {
|
|
2692
|
-
if (key.toLowerCase() === normalizedHeaderName) {
|
|
2693
|
-
delete headers[key];
|
|
2694
|
-
}
|
|
2695
|
-
}
|
|
2696
|
-
}
|
|
2697
|
-
|
|
2698
|
-
private throwIfMessageAcceptanceAborted(abortSignal: AbortSignal | undefined): void {
|
|
2699
|
-
if (abortSignal?.aborted) {
|
|
2700
|
-
throw new Error('Message acceptance aborted before SMTP success');
|
|
2701
|
-
}
|
|
2702
|
-
}
|
|
2703
|
-
|
|
2704
|
-
/**
|
|
2705
|
-
* Update the unified email configuration
|
|
2706
|
-
* @param config New email configuration
|
|
2707
|
-
*/
|
|
2708
|
-
public async updateEmailConfig(config: IUnifiedEmailServerOptions): Promise<void> {
|
|
2709
|
-
await this.queueEmailLifecycleTask(async () => {
|
|
2710
|
-
// Stop existing email components
|
|
2711
|
-
await this.stopUnifiedEmailComponents();
|
|
2712
|
-
|
|
2713
|
-
// Update configuration
|
|
2714
|
-
this.options.emailConfig = config;
|
|
2715
|
-
this.emailDomainManager?.setBaseEmailDomains(config.domains as IEmailDomainConfig[] | undefined);
|
|
2716
|
-
await this.emailDomainManager?.syncManagedDomainsToRuntime();
|
|
2717
|
-
|
|
2718
|
-
// Start email handling with new configuration
|
|
2719
|
-
await this.setupUnifiedEmailHandling();
|
|
2720
|
-
|
|
2721
|
-
logger.log('info', 'Unified email configuration updated');
|
|
2722
|
-
});
|
|
2723
|
-
}
|
|
2724
|
-
|
|
2725
|
-
public async updateEmailServerSettings(
|
|
2726
|
-
settings: TEmailServerSettingsUpdate,
|
|
2727
|
-
updatedBy = 'system',
|
|
2728
|
-
): Promise<IEmailServerSettings> {
|
|
2729
|
-
return await this.queueEmailLifecycleTask(async () => {
|
|
2730
|
-
if (!this.emailSettingsManager) {
|
|
2731
|
-
throw new Error('EmailSettingsManager is not initialized');
|
|
2732
|
-
}
|
|
2733
|
-
|
|
2734
|
-
const updatedSettings = await this.emailSettingsManager.updateSettings(settings, updatedBy);
|
|
2735
|
-
this.emailDomainManager?.setBaseEmailDomains(this.options.emailConfig?.domains as IEmailDomainConfig[] | undefined);
|
|
2736
|
-
await this.emailDomainManager?.syncManagedDomainsToRuntime();
|
|
2737
|
-
this.seedEmailRoutes = this.options.emailConfig
|
|
2738
|
-
? this.generateEmailRoutes(this.options.emailConfig)
|
|
2739
|
-
: [];
|
|
2740
|
-
|
|
2741
|
-
if (this.routeConfigManager) {
|
|
2742
|
-
await this.routeConfigManager.initialize(
|
|
2743
|
-
this.seedConfigRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
2744
|
-
this.seedEmailRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
2745
|
-
this.seedDnsRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
2746
|
-
);
|
|
2747
|
-
}
|
|
2748
|
-
|
|
2749
|
-
if (this.options.emailConfig) {
|
|
2750
|
-
if (this.emailServer) {
|
|
2751
|
-
await this.stopUnifiedEmailComponents();
|
|
2752
|
-
}
|
|
2753
|
-
await this.setupUnifiedEmailHandling();
|
|
2754
|
-
} else if (this.emailServer) {
|
|
2755
|
-
await this.stopUnifiedEmailComponents();
|
|
2756
|
-
}
|
|
2757
|
-
|
|
2758
|
-
return updatedSettings;
|
|
2759
|
-
});
|
|
1792
|
+
return updatedSettings;
|
|
1793
|
+
});
|
|
2760
1794
|
}
|
|
2761
1795
|
|
|
2762
1796
|
/**
|
|
@@ -2768,13 +1802,12 @@ export class DcRouter {
|
|
|
2768
1802
|
if (this.emailServer) {
|
|
2769
1803
|
const emailServer = this.emailServer;
|
|
2770
1804
|
this.emailServer = undefined;
|
|
2771
|
-
this.
|
|
2772
|
-
this.clearAcceptedEmailSpoolTimer();
|
|
1805
|
+
this.acceptedEmailSpool.beginStop();
|
|
2773
1806
|
try {
|
|
2774
1807
|
await emailServer.stop();
|
|
2775
1808
|
} finally {
|
|
2776
|
-
await this.
|
|
2777
|
-
await this.
|
|
1809
|
+
await this.acceptedEmailSpool.stop();
|
|
1810
|
+
await this.acceptedEmailSpool.drainQueueUpdates();
|
|
2778
1811
|
this.clearEmailEventSubscriptions();
|
|
2779
1812
|
}
|
|
2780
1813
|
logger.log('info', 'Unified email server stopped');
|
|
@@ -2823,497 +1856,6 @@ export class DcRouter {
|
|
|
2823
1856
|
* Register DNS records with the DNS server
|
|
2824
1857
|
* @param records Array of DNS records to register
|
|
2825
1858
|
*/
|
|
2826
|
-
private registerDnsRecords(records: Array<{name: string; type: string; value: string; ttl?: number}>): void {
|
|
2827
|
-
if (!this.dnsServer) return;
|
|
2828
|
-
|
|
2829
|
-
// Register a separate handler for each record
|
|
2830
|
-
// This ensures multiple records of the same type (like NS records) are all served
|
|
2831
|
-
for (const record of records) {
|
|
2832
|
-
// Register handler for this specific record
|
|
2833
|
-
this.dnsServer.registerHandler(record.name, [record.type], (question) => {
|
|
2834
|
-
// Check if this handler matches the question
|
|
2835
|
-
if (question.name === record.name && question.type === record.type) {
|
|
2836
|
-
return {
|
|
2837
|
-
name: record.name,
|
|
2838
|
-
type: record.type,
|
|
2839
|
-
class: 'IN',
|
|
2840
|
-
ttl: record.ttl || 300,
|
|
2841
|
-
data: this.parseDnsRecordData(record.type, record.value)
|
|
2842
|
-
};
|
|
2843
|
-
}
|
|
2844
|
-
|
|
2845
|
-
return null;
|
|
2846
|
-
});
|
|
2847
|
-
}
|
|
2848
|
-
|
|
2849
|
-
logger.log('info', `Registered ${records.length} DNS handlers (one per record)`);
|
|
2850
|
-
}
|
|
2851
|
-
|
|
2852
|
-
/**
|
|
2853
|
-
* Parse DNS record data based on record type
|
|
2854
|
-
* @param type DNS record type
|
|
2855
|
-
* @param value DNS record value
|
|
2856
|
-
* @returns Parsed data for the DNS response
|
|
2857
|
-
*/
|
|
2858
|
-
private parseDnsRecordData(type: string, value: string): any {
|
|
2859
|
-
switch (type) {
|
|
2860
|
-
case 'A':
|
|
2861
|
-
return value; // IP address as string
|
|
2862
|
-
case 'MX':
|
|
2863
|
-
const [priority, exchange] = value.split(' ');
|
|
2864
|
-
return { priority: parseInt(priority), exchange };
|
|
2865
|
-
case 'TXT':
|
|
2866
|
-
return value;
|
|
2867
|
-
case 'NS':
|
|
2868
|
-
return value;
|
|
2869
|
-
case 'SOA':
|
|
2870
|
-
// SOA format: primary-ns admin-email serial refresh retry expire minimum
|
|
2871
|
-
const parts = value.split(' ');
|
|
2872
|
-
return {
|
|
2873
|
-
mname: parts[0],
|
|
2874
|
-
rname: parts[1],
|
|
2875
|
-
serial: parseInt(parts[2]),
|
|
2876
|
-
refresh: parseInt(parts[3]),
|
|
2877
|
-
retry: parseInt(parts[4]),
|
|
2878
|
-
expire: parseInt(parts[5]),
|
|
2879
|
-
minimum: parseInt(parts[6])
|
|
2880
|
-
};
|
|
2881
|
-
default:
|
|
2882
|
-
return value;
|
|
2883
|
-
}
|
|
2884
|
-
}
|
|
2885
|
-
|
|
2886
|
-
/**
|
|
2887
|
-
* Set up DNS server with socket handler for DoH
|
|
2888
|
-
*/
|
|
2889
|
-
private async setupDnsWithSocketHandler(): Promise<void> {
|
|
2890
|
-
if (!this.options.dnsNsDomains || this.options.dnsNsDomains.length === 0) {
|
|
2891
|
-
throw new Error('dnsNsDomains is required for DNS server setup');
|
|
2892
|
-
}
|
|
2893
|
-
|
|
2894
|
-
if (!this.options.dnsScopes || this.options.dnsScopes.length === 0) {
|
|
2895
|
-
throw new Error('dnsScopes is required for DNS server setup');
|
|
2896
|
-
}
|
|
2897
|
-
|
|
2898
|
-
const primaryNameserver = this.options.dnsNsDomains[0];
|
|
2899
|
-
logger.log('info', `Setting up DNS server with primary nameserver: ${primaryNameserver}`);
|
|
2900
|
-
|
|
2901
|
-
// Get VM IP address for UDP binding
|
|
2902
|
-
const networkInterfaces = plugins.os.networkInterfaces() as Record<
|
|
2903
|
-
string,
|
|
2904
|
-
Array<{ internal: boolean; family: string; address: string }> | undefined
|
|
2905
|
-
>;
|
|
2906
|
-
let vmIpAddress = this.options.dnsBindInterface || '0.0.0.0'; // Default to all interfaces
|
|
2907
|
-
|
|
2908
|
-
// Try to find the VM's internal IP address when no explicit bind address is configured.
|
|
2909
|
-
if (!this.options.dnsBindInterface) {
|
|
2910
|
-
interfaceLoop: for (const [_name, interfaces] of Object.entries(networkInterfaces)) {
|
|
2911
|
-
if (interfaces) {
|
|
2912
|
-
for (const iface of interfaces) {
|
|
2913
|
-
if (!iface.internal && iface.family === 'IPv4') {
|
|
2914
|
-
vmIpAddress = iface.address;
|
|
2915
|
-
break interfaceLoop;
|
|
2916
|
-
}
|
|
2917
|
-
}
|
|
2918
|
-
}
|
|
2919
|
-
}
|
|
2920
|
-
}
|
|
2921
|
-
|
|
2922
|
-
// Create DNS server instance with manual HTTPS mode
|
|
2923
|
-
this.dnsServer = new plugins.smartdns.dnsServerMod.DnsServer({
|
|
2924
|
-
udpPort: 53,
|
|
2925
|
-
udpBindInterface: vmIpAddress,
|
|
2926
|
-
httpsPort: 443, // Required but won't bind due to manual mode
|
|
2927
|
-
manualHttpsMode: true, // Enable manual HTTPS socket handling
|
|
2928
|
-
dnssecZone: primaryNameserver,
|
|
2929
|
-
primaryNameserver: primaryNameserver, // Automatically generates correct SOA records
|
|
2930
|
-
// For now, use self-signed cert until we integrate with Let's Encrypt
|
|
2931
|
-
httpsKey: '',
|
|
2932
|
-
httpsCert: ''
|
|
2933
|
-
});
|
|
2934
|
-
|
|
2935
|
-
// Start the DNS server (UDP only)
|
|
2936
|
-
await this.dnsServer.start();
|
|
2937
|
-
logger.log('info', `DNS server started on UDP ${vmIpAddress}:53`);
|
|
2938
|
-
|
|
2939
|
-
// Wire DNS query events to MetricsManager and logger with adaptive rate limiting
|
|
2940
|
-
if (this.metricsManager && this.dnsServer) {
|
|
2941
|
-
const flushDnsBatch = () => {
|
|
2942
|
-
if (this.dnsBatchCount > 0) {
|
|
2943
|
-
logger.log('info', `DNS: ${this.dnsBatchCount} queries processed (rate limited)`, { zone: 'dns' });
|
|
2944
|
-
this.dnsBatchCount = 0;
|
|
2945
|
-
}
|
|
2946
|
-
this.dnsBatchTimer = null;
|
|
2947
|
-
};
|
|
2948
|
-
|
|
2949
|
-
this.dnsServer.on('query', (event: plugins.smartdns.dnsServerMod.IDnsQueryCompletedEvent) => {
|
|
2950
|
-
// Metrics tracking
|
|
2951
|
-
for (const question of event.questions) {
|
|
2952
|
-
this.metricsManager?.trackDnsQuery(
|
|
2953
|
-
question.type,
|
|
2954
|
-
question.name,
|
|
2955
|
-
false,
|
|
2956
|
-
event.responseTimeMs,
|
|
2957
|
-
event.answered,
|
|
2958
|
-
);
|
|
2959
|
-
}
|
|
2960
|
-
|
|
2961
|
-
// Adaptive logging: individual logs up to 2/sec, then batch
|
|
2962
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
2963
|
-
if (nowSec !== this.dnsLogWindowSecond) {
|
|
2964
|
-
this.dnsLogWindowSecond = nowSec;
|
|
2965
|
-
this.dnsLogWindowCount = 0;
|
|
2966
|
-
}
|
|
2967
|
-
|
|
2968
|
-
if (this.dnsLogWindowCount < 2) {
|
|
2969
|
-
this.dnsLogWindowCount++;
|
|
2970
|
-
const summary = event.questions.map(q => `${q.type} ${q.name}`).join(', ');
|
|
2971
|
-
logger.log('info', `DNS query: ${summary} (${event.responseTimeMs}ms, ${event.answered ? 'answered' : 'unanswered'})`, { zone: 'dns' });
|
|
2972
|
-
} else {
|
|
2973
|
-
this.dnsBatchCount++;
|
|
2974
|
-
if (!this.dnsBatchTimer) {
|
|
2975
|
-
this.dnsBatchTimer = setTimeout(flushDnsBatch, 5000);
|
|
2976
|
-
}
|
|
2977
|
-
}
|
|
2978
|
-
});
|
|
2979
|
-
}
|
|
2980
|
-
|
|
2981
|
-
// Validate DNS configuration
|
|
2982
|
-
await this.validateDnsConfiguration();
|
|
2983
|
-
|
|
2984
|
-
// Generate and register authoritative records
|
|
2985
|
-
const authoritativeRecords = await this.generateAuthoritativeRecords();
|
|
2986
|
-
|
|
2987
|
-
// Generate email DNS records
|
|
2988
|
-
const emailDnsRecords = await this.generateEmailDnsRecords();
|
|
2989
|
-
|
|
2990
|
-
// Ensure DKIM keys exist for internal-dns domains before generating records.
|
|
2991
|
-
await this.initializeDkimForEmailDomains();
|
|
2992
|
-
|
|
2993
|
-
// Generate DKIM records directly from smartmta.
|
|
2994
|
-
const dkimRecords = await this.loadDkimRecords();
|
|
2995
|
-
|
|
2996
|
-
// Combine all records: authoritative, email, DKIM, and user-defined
|
|
2997
|
-
const allRecords = [...authoritativeRecords, ...emailDnsRecords, ...dkimRecords];
|
|
2998
|
-
if (this.options.dnsRecords && this.options.dnsRecords.length > 0) {
|
|
2999
|
-
allRecords.push(...this.options.dnsRecords);
|
|
3000
|
-
}
|
|
3001
|
-
|
|
3002
|
-
// Apply proxy IP replacement if configured
|
|
3003
|
-
await this.applyProxyIpReplacement(allRecords);
|
|
3004
|
-
|
|
3005
|
-
// Register all DNS records
|
|
3006
|
-
if (allRecords.length > 0) {
|
|
3007
|
-
this.registerDnsRecords(allRecords);
|
|
3008
|
-
logger.log('info', `Registered ${allRecords.length} DNS records (${authoritativeRecords.length} authoritative, ${emailDnsRecords.length} email, ${dkimRecords.length} DKIM, ${this.options.dnsRecords?.length || 0} user-defined)`);
|
|
3009
|
-
}
|
|
3010
|
-
|
|
3011
|
-
// Hand the DnsServer to DnsManager so DB-backed local records on
|
|
3012
|
-
// dcrouter-hosted domains get registered too.
|
|
3013
|
-
if (this.dnsManager && this.dnsServer) {
|
|
3014
|
-
await this.dnsManager.attachDnsServer(this.dnsServer);
|
|
3015
|
-
}
|
|
3016
|
-
}
|
|
3017
|
-
|
|
3018
|
-
/**
|
|
3019
|
-
* Create DNS socket handler for DoH
|
|
3020
|
-
*/
|
|
3021
|
-
private createDnsSocketHandler(): (socket: plugins.net.Socket) => Promise<void> {
|
|
3022
|
-
return async (socket: plugins.net.Socket) => {
|
|
3023
|
-
if (!this.dnsServer) {
|
|
3024
|
-
logger.log('error', 'DNS socket handler called but DNS server not initialized');
|
|
3025
|
-
socket.end();
|
|
3026
|
-
return;
|
|
3027
|
-
}
|
|
3028
|
-
|
|
3029
|
-
// Prevent uncaught exception from socket 'error' events
|
|
3030
|
-
socket.on('error', (err) => {
|
|
3031
|
-
logger.log('error', `DNS socket error: ${err.message}`);
|
|
3032
|
-
if (!socket.destroyed) {
|
|
3033
|
-
socket.destroy();
|
|
3034
|
-
}
|
|
3035
|
-
});
|
|
3036
|
-
|
|
3037
|
-
logger.log('debug', 'DNS socket handler: passing socket to DnsServer');
|
|
3038
|
-
|
|
3039
|
-
try {
|
|
3040
|
-
// Use the built-in socket handler from smartdns
|
|
3041
|
-
// This handles HTTP/2, DoH protocol, etc.
|
|
3042
|
-
await (this.dnsServer as any).handleHttpsSocket(socket);
|
|
3043
|
-
} catch (error: unknown) {
|
|
3044
|
-
logger.log('error', `DNS socket handler error: ${(error as Error).message}`);
|
|
3045
|
-
if (!socket.destroyed) {
|
|
3046
|
-
socket.destroy();
|
|
3047
|
-
}
|
|
3048
|
-
}
|
|
3049
|
-
};
|
|
3050
|
-
}
|
|
3051
|
-
|
|
3052
|
-
/**
|
|
3053
|
-
* Validate DNS configuration
|
|
3054
|
-
*/
|
|
3055
|
-
private async validateDnsConfiguration(): Promise<void> {
|
|
3056
|
-
if (!this.options.dnsNsDomains || !this.options.dnsScopes) {
|
|
3057
|
-
return;
|
|
3058
|
-
}
|
|
3059
|
-
|
|
3060
|
-
logger.log('info', 'Validating DNS configuration...');
|
|
3061
|
-
|
|
3062
|
-
// Check if email domains with internal-dns are in dnsScopes
|
|
3063
|
-
if (this.options.emailConfig?.domains) {
|
|
3064
|
-
for (const domainConfig of this.options.emailConfig.domains) {
|
|
3065
|
-
if (domainConfig.dnsMode === 'internal-dns' &&
|
|
3066
|
-
!this.options.dnsScopes.includes(domainConfig.domain)) {
|
|
3067
|
-
logger.log('warn', `Email domain '${domainConfig.domain}' with internal-dns mode is not in dnsScopes. It should be added to dnsScopes.`);
|
|
3068
|
-
}
|
|
3069
|
-
}
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
|
-
// Validate user-provided DNS records are within scopes
|
|
3073
|
-
if (this.options.dnsRecords) {
|
|
3074
|
-
for (const record of this.options.dnsRecords) {
|
|
3075
|
-
const recordDomain = this.extractDomain(record.name);
|
|
3076
|
-
const isInScope = this.options.dnsScopes.some(scope =>
|
|
3077
|
-
recordDomain === scope || recordDomain.endsWith(`.${scope}`)
|
|
3078
|
-
);
|
|
3079
|
-
|
|
3080
|
-
if (!isInScope) {
|
|
3081
|
-
logger.log('warn', `DNS record for '${record.name}' is outside defined scopes [${this.options.dnsScopes.join(', ')}]`);
|
|
3082
|
-
}
|
|
3083
|
-
}
|
|
3084
|
-
}
|
|
3085
|
-
}
|
|
3086
|
-
|
|
3087
|
-
/**
|
|
3088
|
-
* Generate email DNS records for domains with internal-dns mode
|
|
3089
|
-
*/
|
|
3090
|
-
private async generateEmailDnsRecords(): Promise<Array<{name: string; type: string; value: string; ttl?: number}>> {
|
|
3091
|
-
const records: Array<{name: string; type: string; value: string; ttl?: number}> = [];
|
|
3092
|
-
|
|
3093
|
-
if (!this.options.emailConfig?.domains) {
|
|
3094
|
-
return records;
|
|
3095
|
-
}
|
|
3096
|
-
|
|
3097
|
-
// Filter domains with internal-dns mode
|
|
3098
|
-
const internalDnsDomains = this.options.emailConfig.domains.filter(
|
|
3099
|
-
domain => domain.dnsMode === 'internal-dns'
|
|
3100
|
-
);
|
|
3101
|
-
|
|
3102
|
-
for (const domainConfig of internalDnsDomains) {
|
|
3103
|
-
const domain = domainConfig.domain;
|
|
3104
|
-
const ttl = domainConfig.dns?.internal?.ttl || 3600;
|
|
3105
|
-
const requiredRecords = buildEmailDnsRecords({
|
|
3106
|
-
domain,
|
|
3107
|
-
hostname: this.options.emailConfig.hostname,
|
|
3108
|
-
mxPriority: domainConfig.dns?.internal?.mxPriority,
|
|
3109
|
-
}).filter((record) => !record.name.includes('._domainkey.'));
|
|
3110
|
-
|
|
3111
|
-
for (const record of requiredRecords) {
|
|
3112
|
-
records.push({
|
|
3113
|
-
name: record.name,
|
|
3114
|
-
type: record.type,
|
|
3115
|
-
value: record.value,
|
|
3116
|
-
ttl,
|
|
3117
|
-
});
|
|
3118
|
-
}
|
|
3119
|
-
}
|
|
3120
|
-
|
|
3121
|
-
logger.log('info', `Generated ${records.length} email DNS records for ${internalDnsDomains.length} internal-dns domains`);
|
|
3122
|
-
return records;
|
|
3123
|
-
}
|
|
3124
|
-
|
|
3125
|
-
/**
|
|
3126
|
-
* Generate DKIM DNS records for internal-dns domains from smartmta's selector-aware DKIM state.
|
|
3127
|
-
*/
|
|
3128
|
-
private async loadDkimRecords(): Promise<Array<{name: string; type: string; value: string; ttl?: number}>> {
|
|
3129
|
-
const records: Array<{name: string; type: string; value: string; ttl?: number}> = [];
|
|
3130
|
-
if (!this.options.emailConfig?.domains || !this.emailServer?.dkimCreator) {
|
|
3131
|
-
return records;
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
for (const domainConfig of this.options.emailConfig.domains) {
|
|
3135
|
-
if (domainConfig.dnsMode !== 'internal-dns') {
|
|
3136
|
-
continue;
|
|
3137
|
-
}
|
|
3138
|
-
const selector = domainConfig.dkim?.selector || 'default';
|
|
3139
|
-
try {
|
|
3140
|
-
const dkimRecord = await this.emailServer.dkimCreator.getDNSRecordForDomain(domainConfig.domain, selector);
|
|
3141
|
-
records.push({
|
|
3142
|
-
name: dkimRecord.name,
|
|
3143
|
-
type: 'TXT',
|
|
3144
|
-
value: dkimRecord.value,
|
|
3145
|
-
ttl: domainConfig.dns?.internal?.ttl || 3600,
|
|
3146
|
-
});
|
|
3147
|
-
} catch (error: unknown) {
|
|
3148
|
-
logger.log('error', `Failed to generate DKIM record for ${domainConfig.domain}: ${(error as Error).message}`);
|
|
3149
|
-
}
|
|
3150
|
-
}
|
|
3151
|
-
|
|
3152
|
-
return records;
|
|
3153
|
-
}
|
|
3154
|
-
|
|
3155
|
-
/**
|
|
3156
|
-
* Initialize DKIM keys for all configured email domains
|
|
3157
|
-
* This ensures DKIM records are available immediately at startup
|
|
3158
|
-
*/
|
|
3159
|
-
private async initializeDkimForEmailDomains(): Promise<void> {
|
|
3160
|
-
if (!this.options.emailConfig?.domains || !this.emailServer) {
|
|
3161
|
-
return;
|
|
3162
|
-
}
|
|
3163
|
-
|
|
3164
|
-
logger.log('info', 'Initializing DKIM keys for email domains...');
|
|
3165
|
-
|
|
3166
|
-
// Get DKIMCreator instance from email server (public in smartmta)
|
|
3167
|
-
const dkimCreator = this.emailServer.dkimCreator;
|
|
3168
|
-
if (!dkimCreator) {
|
|
3169
|
-
logger.log('warn', 'DKIMCreator not available, skipping DKIM initialization');
|
|
3170
|
-
return;
|
|
3171
|
-
}
|
|
3172
|
-
|
|
3173
|
-
// Ensure necessary directories exist
|
|
3174
|
-
paths.ensureDataDirectories(this.resolvedPaths);
|
|
3175
|
-
|
|
3176
|
-
// Generate DKIM keys for each internal-dns email domain using the configured selector.
|
|
3177
|
-
for (const domainConfig of this.options.emailConfig.domains) {
|
|
3178
|
-
if (domainConfig.dnsMode !== 'internal-dns') {
|
|
3179
|
-
continue;
|
|
3180
|
-
}
|
|
3181
|
-
try {
|
|
3182
|
-
await dkimCreator.handleDKIMKeysForSelector(
|
|
3183
|
-
domainConfig.domain,
|
|
3184
|
-
domainConfig.dkim?.selector || 'default',
|
|
3185
|
-
domainConfig.dkim?.keySize || 2048,
|
|
3186
|
-
);
|
|
3187
|
-
logger.log('info', `DKIM keys initialized for ${domainConfig.domain}`);
|
|
3188
|
-
} catch (error: unknown) {
|
|
3189
|
-
logger.log('error', `Failed to initialize DKIM for ${domainConfig.domain}: ${(error as Error).message}`);
|
|
3190
|
-
}
|
|
3191
|
-
}
|
|
3192
|
-
|
|
3193
|
-
logger.log('info', 'DKIM initialization complete');
|
|
3194
|
-
}
|
|
3195
|
-
|
|
3196
|
-
/**
|
|
3197
|
-
* Generate authoritative DNS records (NS only) for all domains in dnsScopes
|
|
3198
|
-
* SOA records are now automatically generated by smartdns with primaryNameserver setting
|
|
3199
|
-
*/
|
|
3200
|
-
private async generateAuthoritativeRecords(): Promise<Array<{name: string; type: string; value: string; ttl?: number}>> {
|
|
3201
|
-
const records: Array<{name: string; type: string; value: string; ttl?: number}> = [];
|
|
3202
|
-
|
|
3203
|
-
if (!this.options.dnsNsDomains || !this.options.dnsScopes) {
|
|
3204
|
-
return records;
|
|
3205
|
-
}
|
|
3206
|
-
|
|
3207
|
-
// Determine the public IP for nameserver A records
|
|
3208
|
-
let publicIp: string | null = null;
|
|
3209
|
-
|
|
3210
|
-
// Use proxy IPs if configured (these should be public IPs)
|
|
3211
|
-
if (this.options.proxyIps && this.options.proxyIps.length > 0) {
|
|
3212
|
-
publicIp = this.options.proxyIps[0]; // Use first proxy IP
|
|
3213
|
-
logger.log('info', `Using proxy IP for nameserver A records: ${publicIp}`);
|
|
3214
|
-
} else if (this.options.publicIp) {
|
|
3215
|
-
// Use explicitly configured public IP
|
|
3216
|
-
publicIp = this.options.publicIp;
|
|
3217
|
-
this.detectedPublicIp = publicIp;
|
|
3218
|
-
logger.log('info', `Using configured public IP for nameserver A records: ${publicIp}`);
|
|
3219
|
-
} else {
|
|
3220
|
-
// Auto-discover public IP using smartnetwork
|
|
3221
|
-
try {
|
|
3222
|
-
logger.log('info', 'Auto-discovering public IP address...');
|
|
3223
|
-
const smartNetwork = new plugins.smartnetwork.SmartNetwork();
|
|
3224
|
-
const publicIps = await smartNetwork.getPublicIps();
|
|
3225
|
-
|
|
3226
|
-
if (publicIps.v4) {
|
|
3227
|
-
publicIp = publicIps.v4;
|
|
3228
|
-
this.detectedPublicIp = publicIp;
|
|
3229
|
-
logger.log('info', `Auto-discovered public IPv4: ${publicIp}`);
|
|
3230
|
-
} else {
|
|
3231
|
-
logger.log('warn', 'Could not auto-discover public IPv4 address');
|
|
3232
|
-
}
|
|
3233
|
-
} catch (error: unknown) {
|
|
3234
|
-
logger.log('error', `Failed to auto-discover public IP: ${(error as Error).message}`);
|
|
3235
|
-
}
|
|
3236
|
-
|
|
3237
|
-
if (!publicIp) {
|
|
3238
|
-
logger.log('warn', 'No public IP available. Nameserver A records require either proxyIps, publicIp, or successful auto-discovery.');
|
|
3239
|
-
}
|
|
3240
|
-
}
|
|
3241
|
-
|
|
3242
|
-
// Generate A records for nameservers if we have a public IP
|
|
3243
|
-
if (publicIp) {
|
|
3244
|
-
for (const nsDomain of this.options.dnsNsDomains) {
|
|
3245
|
-
records.push({
|
|
3246
|
-
name: nsDomain,
|
|
3247
|
-
type: 'A',
|
|
3248
|
-
value: publicIp,
|
|
3249
|
-
ttl: 3600
|
|
3250
|
-
});
|
|
3251
|
-
}
|
|
3252
|
-
logger.log('info', `Generated A records for ${this.options.dnsNsDomains.length} nameservers`);
|
|
3253
|
-
}
|
|
3254
|
-
|
|
3255
|
-
// Generate NS records for each domain in scopes
|
|
3256
|
-
for (const domain of this.options.dnsScopes) {
|
|
3257
|
-
// Add NS records for all nameservers
|
|
3258
|
-
for (const nsDomain of this.options.dnsNsDomains) {
|
|
3259
|
-
records.push({
|
|
3260
|
-
name: domain,
|
|
3261
|
-
type: 'NS',
|
|
3262
|
-
value: nsDomain,
|
|
3263
|
-
ttl: 3600
|
|
3264
|
-
});
|
|
3265
|
-
}
|
|
3266
|
-
|
|
3267
|
-
// SOA records are now automatically generated by smartdns DnsServer
|
|
3268
|
-
// with the primaryNameserver configuration option
|
|
3269
|
-
}
|
|
3270
|
-
|
|
3271
|
-
logger.log('info', `Generated ${records.length} total records (A + NS) for ${this.options.dnsScopes.length} domains`);
|
|
3272
|
-
return records;
|
|
3273
|
-
}
|
|
3274
|
-
|
|
3275
|
-
/**
|
|
3276
|
-
* Extract the base domain from a DNS record name
|
|
3277
|
-
*/
|
|
3278
|
-
private extractDomain(recordName: string): string {
|
|
3279
|
-
// Handle wildcards
|
|
3280
|
-
if (recordName.startsWith('*.')) {
|
|
3281
|
-
recordName = recordName.substring(2);
|
|
3282
|
-
}
|
|
3283
|
-
return recordName;
|
|
3284
|
-
}
|
|
3285
|
-
|
|
3286
|
-
/**
|
|
3287
|
-
* Apply proxy IP replacement logic to DNS records
|
|
3288
|
-
*/
|
|
3289
|
-
private async applyProxyIpReplacement(records: Array<{name: string; type: string; value: string; ttl?: number; useIngressProxy?: boolean}>): Promise<void> {
|
|
3290
|
-
if (!this.options.proxyIps || this.options.proxyIps.length === 0) {
|
|
3291
|
-
return; // No proxy IPs configured, skip replacement
|
|
3292
|
-
}
|
|
3293
|
-
|
|
3294
|
-
// Get server's public IP
|
|
3295
|
-
const serverIp = await this.detectServerPublicIp();
|
|
3296
|
-
if (!serverIp) {
|
|
3297
|
-
logger.log('warn', 'Could not detect server public IP, skipping proxy IP replacement');
|
|
3298
|
-
return;
|
|
3299
|
-
}
|
|
3300
|
-
|
|
3301
|
-
logger.log('info', `Applying proxy IP replacement. Server IP: ${serverIp}, Proxy IPs: ${this.options.proxyIps.join(', ')}`);
|
|
3302
|
-
|
|
3303
|
-
let proxyIndex = 0;
|
|
3304
|
-
for (const record of records) {
|
|
3305
|
-
if (record.type === 'A' &&
|
|
3306
|
-
record.value === serverIp &&
|
|
3307
|
-
record.useIngressProxy !== false) {
|
|
3308
|
-
// Round-robin through proxy IPs
|
|
3309
|
-
const proxyIp = this.options.proxyIps[proxyIndex % this.options.proxyIps.length];
|
|
3310
|
-
logger.log('info', `Replacing A record for ${record.name}: ${record.value} → ${proxyIp}`);
|
|
3311
|
-
record.value = proxyIp;
|
|
3312
|
-
proxyIndex++;
|
|
3313
|
-
}
|
|
3314
|
-
}
|
|
3315
|
-
}
|
|
3316
|
-
|
|
3317
1859
|
private addEmailEventSubscription(
|
|
3318
1860
|
emitter: {
|
|
3319
1861
|
on(eventName: string, listener: (...args: any[]) => void): void;
|
|
@@ -3333,88 +1875,10 @@ export class DcRouter {
|
|
|
3333
1875
|
this.emailEventSubscriptions = [];
|
|
3334
1876
|
}
|
|
3335
1877
|
|
|
3336
|
-
/**
|
|
3337
|
-
* Detect the server's public IP address
|
|
3338
|
-
*/
|
|
3339
|
-
private async detectServerPublicIp(): Promise<string | null> {
|
|
3340
|
-
try {
|
|
3341
|
-
const smartNetwork = new plugins.smartnetwork.SmartNetwork();
|
|
3342
|
-
const publicIps = await smartNetwork.getPublicIps();
|
|
3343
|
-
|
|
3344
|
-
if (publicIps.v4) {
|
|
3345
|
-
return publicIps.v4;
|
|
3346
|
-
}
|
|
3347
|
-
|
|
3348
|
-
return null;
|
|
3349
|
-
} catch (error: unknown) {
|
|
3350
|
-
logger.log('warn', `Failed to detect public IP: ${(error as Error).message}`);
|
|
3351
|
-
return null;
|
|
3352
|
-
}
|
|
3353
|
-
}
|
|
3354
1878
|
|
|
3355
1879
|
/**
|
|
3356
1880
|
* Set up Remote Ingress hub for edge tunnel connections
|
|
3357
1881
|
*/
|
|
3358
|
-
private async setupRemoteIngress(): Promise<void> {
|
|
3359
|
-
const remoteIngressManager = this.remoteIngressManager;
|
|
3360
|
-
if (!remoteIngressManager) {
|
|
3361
|
-
return;
|
|
3362
|
-
}
|
|
3363
|
-
|
|
3364
|
-
const hubSettings = remoteIngressManager.getHubSettings();
|
|
3365
|
-
if (!hubSettings.enabled) {
|
|
3366
|
-
logger.log('info', 'Remote Ingress hub is disabled in DB settings');
|
|
3367
|
-
return;
|
|
3368
|
-
}
|
|
3369
|
-
|
|
3370
|
-
logger.log('info', 'Setting up Remote Ingress hub...');
|
|
3371
|
-
this.remoteIngressHubStopping = false;
|
|
3372
|
-
const generation = ++this.remoteIngressHubGeneration;
|
|
3373
|
-
|
|
3374
|
-
const firewallConfig = await this.securityPolicyManager?.compileRemoteIngressFirewall();
|
|
3375
|
-
if (!this.isRemoteIngressHubGenerationCurrent(generation, remoteIngressManager)) {
|
|
3376
|
-
return;
|
|
3377
|
-
}
|
|
3378
|
-
remoteIngressManager.setFirewallConfig(firewallConfig);
|
|
3379
|
-
|
|
3380
|
-
// Pass current bootstrap routes so the manager can derive edge ports initially.
|
|
3381
|
-
// Once RouteConfigManager applies the full DB set, the onRoutesApplied callback
|
|
3382
|
-
// will push the complete merged routes here.
|
|
3383
|
-
const bootstrapRoutes = [...this.seedConfigRoutes, ...this.seedEmailRoutes, ...this.runtimeDnsRoutes];
|
|
3384
|
-
remoteIngressManager.setRoutes(bootstrapRoutes as any[]);
|
|
3385
|
-
|
|
3386
|
-
// If ConfigManagers finished before us, re-apply routes
|
|
3387
|
-
// so the callback delivers the full DB set to our newly-created remoteIngressManager.
|
|
3388
|
-
if (this.routeConfigManager) {
|
|
3389
|
-
await this.routeConfigManager.applyRoutes();
|
|
3390
|
-
}
|
|
3391
|
-
if (!this.isRemoteIngressHubGenerationCurrent(generation, remoteIngressManager)) {
|
|
3392
|
-
return;
|
|
3393
|
-
}
|
|
3394
|
-
|
|
3395
|
-
await this.queueRemoteIngressHubTask(async () => {
|
|
3396
|
-
await this.startRemoteIngressTunnelHubLocked(generation);
|
|
3397
|
-
});
|
|
3398
|
-
if (!this.isRemoteIngressHubGenerationCurrent(generation, remoteIngressManager)) {
|
|
3399
|
-
return;
|
|
3400
|
-
}
|
|
3401
|
-
|
|
3402
|
-
const edgeCount = remoteIngressManager.getAllEdges().length;
|
|
3403
|
-
logger.log('info', `Remote Ingress hub started on port ${hubSettings.tunnelPort} with ${edgeCount} registered edge(s)`);
|
|
3404
|
-
}
|
|
3405
|
-
|
|
3406
|
-
private isRemoteIngressHubGenerationCurrent(generation: number, manager: RemoteIngressManager): boolean {
|
|
3407
|
-
return !this.remoteIngressHubStopping
|
|
3408
|
-
&& generation === this.remoteIngressHubGeneration
|
|
3409
|
-
&& this.remoteIngressManager === manager;
|
|
3410
|
-
}
|
|
3411
|
-
|
|
3412
|
-
private queueRemoteIngressHubTask<T>(task: () => Promise<T>): Promise<T> {
|
|
3413
|
-
const run = this.remoteIngressHubLifecycleChain.then(task);
|
|
3414
|
-
this.remoteIngressHubLifecycleChain = run.then(() => undefined, () => undefined);
|
|
3415
|
-
return run;
|
|
3416
|
-
}
|
|
3417
|
-
|
|
3418
1882
|
private queueSmartProxyLifecycleTask<T>(task: () => Promise<T>): Promise<T> {
|
|
3419
1883
|
const run = this.smartProxyLifecycleChain.then(task);
|
|
3420
1884
|
this.smartProxyLifecycleChain = run.then(() => undefined, () => undefined);
|
|
@@ -3427,85 +1891,23 @@ export class DcRouter {
|
|
|
3427
1891
|
return run;
|
|
3428
1892
|
}
|
|
3429
1893
|
|
|
3430
|
-
|
|
3431
|
-
this.remoteIngressHubStopping = true;
|
|
3432
|
-
this.remoteIngressHubGeneration++;
|
|
3433
|
-
await this.queueRemoteIngressHubTask(async () => {
|
|
3434
|
-
const currentTunnelManager = this.tunnelManager;
|
|
3435
|
-
if (currentTunnelManager) {
|
|
3436
|
-
await currentTunnelManager.stop();
|
|
3437
|
-
if (this.tunnelManager === currentTunnelManager) {
|
|
3438
|
-
this.tunnelManager = undefined;
|
|
3439
|
-
}
|
|
3440
|
-
}
|
|
3441
|
-
});
|
|
3442
|
-
}
|
|
3443
|
-
|
|
1894
|
+
/** Serialized edge mutation on the RemoteIngress hub (delegates to the hub lifecycle). */
|
|
3444
1895
|
public async mutateRemoteIngressEdges<T>(
|
|
3445
1896
|
mutation: (manager: RemoteIngressManager) => Promise<T>,
|
|
3446
1897
|
syncAllowedEdges = true,
|
|
3447
1898
|
): Promise<T> {
|
|
3448
|
-
return await this.
|
|
3449
|
-
if (this.remoteIngressHubStopping) {
|
|
3450
|
-
throw new Error('RemoteIngress is stopping');
|
|
3451
|
-
}
|
|
3452
|
-
const manager = this.remoteIngressManager;
|
|
3453
|
-
if (!manager) {
|
|
3454
|
-
throw new Error('RemoteIngress not configured');
|
|
3455
|
-
}
|
|
3456
|
-
const result = await mutation(manager);
|
|
3457
|
-
if (syncAllowedEdges && this.tunnelManager) {
|
|
3458
|
-
await this.tunnelManager.syncAllowedEdges();
|
|
3459
|
-
}
|
|
3460
|
-
return result;
|
|
3461
|
-
});
|
|
3462
|
-
}
|
|
3463
|
-
|
|
3464
|
-
private async updateRemoteIngressRoutes(routes: IDcRouterRouteConfig[]): Promise<void> {
|
|
3465
|
-
await this.queueRemoteIngressHubTask(async () => {
|
|
3466
|
-
if (this.remoteIngressHubStopping) return;
|
|
3467
|
-
if (this.remoteIngressManager) {
|
|
3468
|
-
this.remoteIngressManager.setRoutes(routes);
|
|
3469
|
-
}
|
|
3470
|
-
if (this.tunnelManager) {
|
|
3471
|
-
await this.tunnelManager.syncAllowedEdges();
|
|
3472
|
-
}
|
|
3473
|
-
});
|
|
1899
|
+
return await this.remoteIngressHubLifecycle.mutateEdges(mutation, syncAllowedEdges);
|
|
3474
1900
|
}
|
|
3475
1901
|
|
|
3476
1902
|
public async updateRemoteIngressHubSettings(
|
|
3477
1903
|
updates: TRemoteIngressHubSettingsUpdate,
|
|
3478
1904
|
updatedBy: string,
|
|
3479
1905
|
): Promise<IRemoteIngressHubSettings> {
|
|
3480
|
-
|
|
3481
|
-
if (!manager) {
|
|
3482
|
-
throw new Error('RemoteIngress is not configured');
|
|
3483
|
-
}
|
|
3484
|
-
|
|
3485
|
-
const previousSettings = manager.getHubSettings();
|
|
3486
|
-
const settings = await manager.updateHubSettings(updates, updatedBy);
|
|
3487
|
-
const enabledChanged = previousSettings.enabled !== settings.enabled;
|
|
3488
|
-
|
|
3489
|
-
if (!settings.enabled) {
|
|
3490
|
-
await this.queueRemoteIngressHubTask(async () => {
|
|
3491
|
-
await this.stopRemoteIngressTunnelHubLocked();
|
|
3492
|
-
});
|
|
3493
|
-
}
|
|
3494
|
-
|
|
3495
|
-
if (enabledChanged) {
|
|
3496
|
-
await this.restartSmartProxyForRemoteIngressSettings();
|
|
3497
|
-
}
|
|
3498
|
-
|
|
3499
|
-
if (settings.enabled) {
|
|
3500
|
-
await this.queueRemoteIngressHubTask(async () => {
|
|
3501
|
-
await this.restartRemoteIngressTunnelHubLocked();
|
|
3502
|
-
});
|
|
3503
|
-
}
|
|
3504
|
-
|
|
3505
|
-
return settings;
|
|
1906
|
+
return await this.remoteIngressHubLifecycle.updateHubSettings(updates, updatedBy);
|
|
3506
1907
|
}
|
|
3507
1908
|
|
|
3508
|
-
|
|
1909
|
+
/** Restart SmartProxy after RemoteIngress hub settings changed listener wiring. Called by RemoteIngressHubLifecycle. */
|
|
1910
|
+
public async restartSmartProxyForRemoteIngressSettings(): Promise<void> {
|
|
3509
1911
|
await this.queueSmartProxyLifecycleTask(async () => {
|
|
3510
1912
|
const restartSmartProxy = async () => {
|
|
3511
1913
|
try {
|
|
@@ -3518,7 +1920,7 @@ export class DcRouter {
|
|
|
3518
1920
|
}
|
|
3519
1921
|
}
|
|
3520
1922
|
} finally {
|
|
3521
|
-
await this.
|
|
1923
|
+
await this.smartAcmeLifecycle.stop();
|
|
3522
1924
|
}
|
|
3523
1925
|
await this.setupSmartProxy();
|
|
3524
1926
|
};
|
|
@@ -3540,144 +1942,14 @@ export class DcRouter {
|
|
|
3540
1942
|
});
|
|
3541
1943
|
}
|
|
3542
1944
|
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
if (currentTunnelManager) {
|
|
3547
|
-
await currentTunnelManager.stop();
|
|
3548
|
-
if (this.tunnelManager === currentTunnelManager) {
|
|
3549
|
-
this.tunnelManager = undefined;
|
|
3550
|
-
}
|
|
3551
|
-
}
|
|
3552
|
-
}
|
|
3553
|
-
|
|
3554
|
-
private async restartRemoteIngressTunnelHubLocked(): Promise<void> {
|
|
3555
|
-
const generation = ++this.remoteIngressHubGeneration;
|
|
3556
|
-
const hubSettings = this.remoteIngressManager?.getHubSettings();
|
|
3557
|
-
if (!this.remoteIngressManager || !hubSettings?.enabled || this.remoteIngressHubStopping) {
|
|
3558
|
-
return;
|
|
3559
|
-
}
|
|
3560
|
-
|
|
3561
|
-
const currentTunnelManager = this.tunnelManager;
|
|
3562
|
-
if (currentTunnelManager) {
|
|
3563
|
-
await currentTunnelManager.stop();
|
|
3564
|
-
if (this.tunnelManager === currentTunnelManager) {
|
|
3565
|
-
this.tunnelManager = undefined;
|
|
3566
|
-
}
|
|
3567
|
-
}
|
|
3568
|
-
|
|
3569
|
-
if (this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration) {
|
|
3570
|
-
return;
|
|
3571
|
-
}
|
|
3572
|
-
await this.startRemoteIngressTunnelHubLocked(generation);
|
|
3573
|
-
}
|
|
3574
|
-
|
|
3575
|
-
private async startRemoteIngressTunnelHubLocked(generation: number): Promise<void> {
|
|
3576
|
-
const manager = this.remoteIngressManager;
|
|
3577
|
-
const hubSettings = manager?.getHubSettings();
|
|
3578
|
-
if (!manager || !hubSettings?.enabled || this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration) {
|
|
3579
|
-
return;
|
|
3580
|
-
}
|
|
3581
|
-
|
|
3582
|
-
const firewallConfig = await this.securityPolicyManager?.compileRemoteIngressFirewall();
|
|
3583
|
-
if (this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration || this.remoteIngressManager !== manager) {
|
|
3584
|
-
return;
|
|
3585
|
-
}
|
|
3586
|
-
manager.setFirewallConfig(firewallConfig);
|
|
3587
|
-
|
|
3588
|
-
const tlsConfig = await this.resolveRemoteIngressTlsConfig(hubSettings.hubDomain);
|
|
3589
|
-
if (this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration || this.remoteIngressManager !== manager) {
|
|
3590
|
-
return;
|
|
3591
|
-
}
|
|
3592
|
-
|
|
3593
|
-
const tunnelManager = new TunnelManager(manager, {
|
|
3594
|
-
tunnelPort: hubSettings.tunnelPort,
|
|
3595
|
-
targetHost: '127.0.0.1',
|
|
3596
|
-
tls: tlsConfig,
|
|
3597
|
-
performance: manager.getHubPerformanceConfig(),
|
|
3598
|
-
});
|
|
3599
|
-
try {
|
|
3600
|
-
await tunnelManager.start();
|
|
3601
|
-
} catch (err) {
|
|
3602
|
-
await tunnelManager.stop().catch(() => {});
|
|
3603
|
-
throw err;
|
|
3604
|
-
}
|
|
3605
|
-
|
|
3606
|
-
if (this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration || this.remoteIngressManager !== manager) {
|
|
3607
|
-
await tunnelManager.stop().catch((err) => {
|
|
3608
|
-
logger.log('warn', `Failed to stop stale RemoteIngress tunnel hub: ${(err as Error).message}`);
|
|
3609
|
-
});
|
|
3610
|
-
return;
|
|
3611
|
-
}
|
|
3612
|
-
this.tunnelManager = tunnelManager;
|
|
3613
|
-
}
|
|
3614
|
-
|
|
3615
|
-
private async resolveRemoteIngressTlsConfig(
|
|
3616
|
-
hubDomain?: string,
|
|
3617
|
-
): Promise<{ certPem: string; keyPem: string } | undefined> {
|
|
3618
|
-
// Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default)
|
|
3619
|
-
let tlsConfig: { certPem: string; keyPem: string } | undefined;
|
|
3620
|
-
|
|
3621
|
-
// Priority 1: Explicit cert/key file paths
|
|
3622
|
-
const explicitTls = this.options.remoteIngressConfig?.tls;
|
|
3623
|
-
if (explicitTls?.certPath && explicitTls?.keyPath) {
|
|
3624
|
-
try {
|
|
3625
|
-
const certPem = plugins.fs.readFileSync(explicitTls.certPath, 'utf8');
|
|
3626
|
-
const keyPem = plugins.fs.readFileSync(explicitTls.keyPath, 'utf8');
|
|
3627
|
-
tlsConfig = { certPem, keyPem };
|
|
3628
|
-
logger.log('info', 'Using explicit TLS cert/key for RemoteIngress tunnel');
|
|
3629
|
-
} catch (err: unknown) {
|
|
3630
|
-
logger.log('warn', `Failed to read RemoteIngress TLS cert/key files: ${(err as Error).message}`);
|
|
3631
|
-
}
|
|
3632
|
-
}
|
|
3633
|
-
|
|
3634
|
-
// Priority 2: Existing cert from SmartProxy cert store for hubDomain
|
|
3635
|
-
if (!tlsConfig && hubDomain) {
|
|
3636
|
-
try {
|
|
3637
|
-
const stored = await ProxyCertDoc.findByDomain(hubDomain);
|
|
3638
|
-
if (stored?.publicKey && stored?.privateKey) {
|
|
3639
|
-
tlsConfig = { certPem: stored.publicKey, keyPem: stored.privateKey };
|
|
3640
|
-
logger.log('info', `Using stored ACME cert for RemoteIngress tunnel TLS: ${hubDomain}`);
|
|
3641
|
-
}
|
|
3642
|
-
} catch { /* no stored cert, fall through */ }
|
|
3643
|
-
}
|
|
3644
|
-
|
|
3645
|
-
if (!tlsConfig) {
|
|
3646
|
-
logger.log('info', 'No TLS cert configured for RemoteIngress tunnel — using auto-generated self-signed');
|
|
3647
|
-
}
|
|
3648
|
-
|
|
3649
|
-
return tlsConfig;
|
|
1945
|
+
/** Bootstrap routes the RemoteIngress hub uses to derive edge ports before the DB route set is applied. */
|
|
1946
|
+
public getRemoteIngressBootstrapRoutes(): plugins.smartproxy.IRouteConfig[] {
|
|
1947
|
+
return [...this.seedConfigRoutes, ...this.seedEmailRoutes, ...this.runtimeDnsRoutes];
|
|
3650
1948
|
}
|
|
3651
1949
|
|
|
3652
1950
|
/**
|
|
3653
1951
|
* Set up VPN server for VPN-based route access control.
|
|
3654
1952
|
*/
|
|
3655
|
-
private createVpnClientAccessResolver(): ((
|
|
3656
|
-
route: import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig,
|
|
3657
|
-
routeId?: string,
|
|
3658
|
-
) => TVpnClientAllowEntry[]) | undefined {
|
|
3659
|
-
if (!this.options.vpnConfig?.enabled) {
|
|
3660
|
-
return undefined;
|
|
3661
|
-
}
|
|
3662
|
-
|
|
3663
|
-
return (
|
|
3664
|
-
route: import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig,
|
|
3665
|
-
routeId?: string,
|
|
3666
|
-
) => {
|
|
3667
|
-
if (!this.vpnManager || !this.targetProfileManager) {
|
|
3668
|
-
// VPN not ready yet — deny all until re-apply after VPN starts.
|
|
3669
|
-
return [];
|
|
3670
|
-
}
|
|
3671
|
-
|
|
3672
|
-
return this.targetProfileManager.getMatchingVpnClients(
|
|
3673
|
-
route,
|
|
3674
|
-
routeId,
|
|
3675
|
-
this.vpnManager.listClients(),
|
|
3676
|
-
this.routeConfigManager?.getRoutes() || new Map(),
|
|
3677
|
-
);
|
|
3678
|
-
};
|
|
3679
|
-
}
|
|
3680
|
-
|
|
3681
1953
|
private async setupVpnServer(): Promise<void> {
|
|
3682
1954
|
if (!this.options.vpnConfig?.enabled) {
|
|
3683
1955
|
return;
|
|
@@ -3719,38 +1991,8 @@ export class DcRouter {
|
|
|
3719
1991
|
if (!this.targetProfileManager) return [];
|
|
3720
1992
|
return this.targetProfileManager.getDirectTargetIps(targetProfileIds);
|
|
3721
1993
|
},
|
|
3722
|
-
getClientAllowedIPs: async (targetProfileIds: string[],
|
|
3723
|
-
|
|
3724
|
-
const ips = new Set<string>([subnet]);
|
|
3725
|
-
|
|
3726
|
-
if (!this.targetProfileManager) return [...ips];
|
|
3727
|
-
|
|
3728
|
-
const allRoutes = this.routeConfigManager?.getRoutes() || new Map();
|
|
3729
|
-
|
|
3730
|
-
const { domains, targetIps } = this.targetProfileManager.getClientAccessSpec(
|
|
3731
|
-
targetProfileIds,
|
|
3732
|
-
allRoutes,
|
|
3733
|
-
);
|
|
3734
|
-
|
|
3735
|
-
// Add target IPs directly
|
|
3736
|
-
for (const ip of targetIps) {
|
|
3737
|
-
ips.add(`${ip}/32`);
|
|
3738
|
-
}
|
|
3739
|
-
|
|
3740
|
-
// Resolve DNS A records for matched domains (with caching)
|
|
3741
|
-
for (const domain of domains) {
|
|
3742
|
-
if (this.isWildcardVpnDomain(domain)) {
|
|
3743
|
-
this.logSkippedWildcardAllowedIp(domain);
|
|
3744
|
-
continue;
|
|
3745
|
-
}
|
|
3746
|
-
const resolvedIps = await this.resolveVpnDomainIPs(domain);
|
|
3747
|
-
for (const ip of resolvedIps) {
|
|
3748
|
-
ips.add(`${ip}/32`);
|
|
3749
|
-
}
|
|
3750
|
-
}
|
|
3751
|
-
|
|
3752
|
-
return [...ips];
|
|
3753
|
-
},
|
|
1994
|
+
getClientAllowedIPs: async (targetProfileIds: string[], _clientId?: string, _sourceIp?: string) =>
|
|
1995
|
+
await this.vpnAccessResolver.getClientAllowedIPs(targetProfileIds),
|
|
3754
1996
|
});
|
|
3755
1997
|
|
|
3756
1998
|
await this.vpnManager.start();
|
|
@@ -3760,48 +2002,6 @@ export class DcRouter {
|
|
|
3760
2002
|
await this.routeConfigManager?.applyRoutes();
|
|
3761
2003
|
}
|
|
3762
2004
|
|
|
3763
|
-
/** Cache for DNS-resolved IPs of VPN-gated domains. TTL: 5 minutes. */
|
|
3764
|
-
private vpnDomainIpCache = new Map<string, { ips: string[]; expiresAt: number }>();
|
|
3765
|
-
/** Deduplicate wildcard-resolution warnings for WireGuard AllowedIPs generation. */
|
|
3766
|
-
private warnedWildcardVpnDomains = new Set<string>();
|
|
3767
|
-
|
|
3768
|
-
/**
|
|
3769
|
-
* Resolve a domain's A record(s) for VPN AllowedIPs, with a 5-minute cache.
|
|
3770
|
-
*/
|
|
3771
|
-
private async resolveVpnDomainIPs(domain: string): Promise<string[]> {
|
|
3772
|
-
const cached = this.vpnDomainIpCache.get(domain);
|
|
3773
|
-
if (cached && cached.expiresAt > Date.now()) {
|
|
3774
|
-
return cached.ips;
|
|
3775
|
-
}
|
|
3776
|
-
try {
|
|
3777
|
-
const { promises: dnsPromises } = await import('dns');
|
|
3778
|
-
const ips = await dnsPromises.resolve4(domain);
|
|
3779
|
-
this.vpnDomainIpCache.set(domain, { ips, expiresAt: Date.now() + 5 * 60 * 1000 });
|
|
3780
|
-
// Evict oldest entries if cache exceeds 1000 entries
|
|
3781
|
-
if (this.vpnDomainIpCache.size > 1000) {
|
|
3782
|
-
const firstKey = this.vpnDomainIpCache.keys().next().value;
|
|
3783
|
-
if (firstKey) this.vpnDomainIpCache.delete(firstKey);
|
|
3784
|
-
}
|
|
3785
|
-
return ips;
|
|
3786
|
-
} catch (err) {
|
|
3787
|
-
logger.log('warn', `VPN: Failed to resolve ${domain} for AllowedIPs: ${(err as Error).message}`);
|
|
3788
|
-
return cached?.ips || []; // Return stale cache on failure, or empty
|
|
3789
|
-
}
|
|
3790
|
-
}
|
|
3791
|
-
|
|
3792
|
-
private isWildcardVpnDomain(domain: string): boolean {
|
|
3793
|
-
return domain.includes('*');
|
|
3794
|
-
}
|
|
3795
|
-
|
|
3796
|
-
private logSkippedWildcardAllowedIp(domain: string): void {
|
|
3797
|
-
if (this.warnedWildcardVpnDomains.has(domain)) return;
|
|
3798
|
-
this.warnedWildcardVpnDomains.add(domain);
|
|
3799
|
-
logger.log(
|
|
3800
|
-
'warn',
|
|
3801
|
-
`VPN: Skipping wildcard domain '${domain}' for WireGuard AllowedIPs; wildcard patterns must be resolved to concrete hostnames by matching routes.`,
|
|
3802
|
-
);
|
|
3803
|
-
}
|
|
3804
|
-
|
|
3805
2005
|
// VPN security injection is now handled dynamically by RouteConfigManager.applyRoutes()
|
|
3806
2006
|
// via the getVpnAllowList callback — no longer a separate method here.
|
|
3807
2007
|
|
|
@@ -3850,9 +2050,8 @@ export class DcRouter {
|
|
|
3850
2050
|
}
|
|
3851
2051
|
|
|
3852
2052
|
this.options.vpnConfig = config;
|
|
3853
|
-
this.
|
|
3854
|
-
this.
|
|
3855
|
-
this.routeConfigManager?.setVpnClientAccessResolver(this.createVpnClientAccessResolver());
|
|
2053
|
+
this.vpnAccessResolver.reset();
|
|
2054
|
+
this.routeConfigManager?.setVpnClientAccessResolver(this.vpnAccessResolver.createRouteAllowResolver());
|
|
3856
2055
|
|
|
3857
2056
|
if (this.options.vpnConfig?.enabled) {
|
|
3858
2057
|
await this.setupVpnServer();
|