@serve.zone/dcrouter 13.45.0 → 14.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/acme/manager.acme-config.d.ts +1 -14
- package/dist_ts/acme/manager.acme-config.js +4 -65
- package/dist_ts/classes.dcrouter.d.ts +7 -2
- package/dist_ts/classes.dcrouter.js +115 -48
- package/dist_ts/config/classes.api-token-manager.js +3 -3
- package/dist_ts/config/classes.route-config-manager.d.ts +2 -1
- package/dist_ts/config/classes.route-config-manager.js +8 -3
- package/dist_ts/db/documents/classes.acme-config.doc.d.ts +1 -3
- package/dist_ts/db/documents/classes.acme-config.doc.js +2 -4
- package/dist_ts/dns/manager.dns.d.ts +0 -13
- package/dist_ts/dns/manager.dns.js +1 -81
- package/dist_ts/opsserver/handlers/certificate.handler.d.ts +0 -9
- package/dist_ts/opsserver/handlers/certificate.handler.js +1 -40
- package/dist_ts/opsserver/handlers/config.handler.js +12 -20
- package/dist_ts/opsserver/handlers/email-settings.handler.js +2 -2
- package/dist_ts_interfaces/data/acme-config.d.ts +1 -3
- package/dist_ts_interfaces/requests/certificate.d.ts +0 -12
- package/dist_ts_migrations/index.js +2 -2
- package/dist_ts_web/00_commitinfo_data.js +2 -2
- package/package.json +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/acme/manager.acme-config.ts +3 -77
- package/ts/classes.dcrouter.ts +134 -49
- package/ts/config/classes.api-token-manager.ts +2 -2
- package/ts/config/classes.route-config-manager.ts +5 -1
- package/ts/db/documents/classes.acme-config.doc.ts +1 -3
- package/ts/dns/manager.dns.ts +0 -103
- package/ts/opsserver/handlers/certificate.handler.ts +0 -47
- package/ts/opsserver/handlers/config.handler.ts +11 -19
- package/ts/opsserver/handlers/email-settings.handler.ts +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -407,7 +407,7 @@ export async function createMigrationRunner(db, targetVersion, options = {}) {
|
|
|
407
407
|
.from('13.1.0').to('13.8.1')
|
|
408
408
|
.description('Rename DomainDoc.source value from "manual" to "dcrouter"')
|
|
409
409
|
.up(async (ctx) => {
|
|
410
|
-
const collection = ctx.mongo.collection('
|
|
410
|
+
const collection = ctx.mongo.collection('DomainDoc');
|
|
411
411
|
const result = await collection.updateMany({ source: 'manual' }, { $set: { source: 'dcrouter' } });
|
|
412
412
|
ctx.log.log('info', `rename-domain-source-manual-to-dcrouter: migrated ${result.modifiedCount} domain(s)`);
|
|
413
413
|
})
|
|
@@ -415,7 +415,7 @@ export async function createMigrationRunner(db, targetVersion, options = {}) {
|
|
|
415
415
|
.from('13.8.1').to('13.8.2')
|
|
416
416
|
.description('Rename DnsRecordDoc.source value from "manual" to "local"')
|
|
417
417
|
.up(async (ctx) => {
|
|
418
|
-
const collection = ctx.mongo.collection('
|
|
418
|
+
const collection = ctx.mongo.collection('DnsRecordDoc');
|
|
419
419
|
const result = await collection.updateMany({ source: 'manual' }, { $set: { source: 'local' } });
|
|
420
420
|
ctx.log.log('info', `rename-record-source-manual-to-local: migrated ${result.modifiedCount} record(s)`);
|
|
421
421
|
})
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@serve.zone/dcrouter',
|
|
6
|
-
version: '
|
|
6
|
+
version: '14.0.1',
|
|
7
7
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
|
8
8
|
};
|
|
9
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
9
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHNfd2ViLzAwX2NvbW1pdGluZm9fZGF0YS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7R0FFRztBQUNILE1BQU0sQ0FBQyxNQUFNLFVBQVUsR0FBRztJQUN4QixJQUFJLEVBQUUsc0JBQXNCO0lBQzVCLE9BQU8sRUFBRSxRQUFRO0lBQ2pCLFdBQVcsRUFBRSwwRUFBMEU7Q0FDeEYsQ0FBQSJ9
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serve.zone/dcrouter",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "14.0.1",
|
|
5
5
|
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@push.rocks/smartnetwork": "^4.7.2",
|
|
51
51
|
"@push.rocks/smartpath": "^6.0.0",
|
|
52
52
|
"@push.rocks/smartpromise": "^4.2.4",
|
|
53
|
-
"@push.rocks/smartproxy": "^27.12.
|
|
53
|
+
"@push.rocks/smartproxy": "^27.12.8",
|
|
54
54
|
"@push.rocks/smartradius": "^1.3.0",
|
|
55
55
|
"@push.rocks/smartrequest": "^5.0.3",
|
|
56
56
|
"@push.rocks/smartrx": "^3.0.10",
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { logger } from '../logger.js';
|
|
2
2
|
import { AcmeConfigDoc } from '../db/documents/index.js';
|
|
3
|
-
import type { IDcRouterOptions } from '../classes.dcrouter.js';
|
|
4
3
|
import type { IAcmeConfig } from '../../ts_interfaces/data/acme-config.js';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* AcmeConfigManager — owns the singleton ACME configuration in the DB.
|
|
8
7
|
*
|
|
9
8
|
* Lifecycle:
|
|
10
|
-
* - `start()` — loads
|
|
11
|
-
* fields (`tls.contactEmail`, `smartProxyConfig.acme.*`) on first boot.
|
|
9
|
+
* - `start()` — loads the DB-backed singleton configuration.
|
|
12
10
|
* - `getConfig()` — returns the in-memory cached `IAcmeConfig` (or null)
|
|
13
11
|
* - `updateConfig(args, updatedBy)` — upserts and refreshes the cache
|
|
14
12
|
*
|
|
@@ -20,32 +18,12 @@ import type { IAcmeConfig } from '../../ts_interfaces/data/acme-config.js';
|
|
|
20
18
|
export class AcmeConfigManager {
|
|
21
19
|
private cached: IAcmeConfig | null = null;
|
|
22
20
|
|
|
23
|
-
constructor(private options: IDcRouterOptions) {}
|
|
24
|
-
|
|
25
21
|
public async start(): Promise<void> {
|
|
26
22
|
logger.log('info', 'AcmeConfigManager: starting');
|
|
27
|
-
|
|
23
|
+
const doc = await AcmeConfigDoc.load();
|
|
28
24
|
|
|
29
25
|
if (!doc) {
|
|
30
|
-
|
|
31
|
-
const seed = this.deriveSeedFromOptions();
|
|
32
|
-
if (seed) {
|
|
33
|
-
doc = await this.createSeedDoc(seed);
|
|
34
|
-
logger.log(
|
|
35
|
-
'info',
|
|
36
|
-
`AcmeConfigManager: seeded from constructor legacy fields (accountEmail=${seed.accountEmail}, useProduction=${seed.useProduction})`,
|
|
37
|
-
);
|
|
38
|
-
} else {
|
|
39
|
-
logger.log(
|
|
40
|
-
'info',
|
|
41
|
-
'AcmeConfigManager: no AcmeConfig in DB and no legacy constructor fields — ACME disabled until configured via Domains > Certificates > Settings.',
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
} else if (this.deriveSeedFromOptions()) {
|
|
45
|
-
logger.log(
|
|
46
|
-
'warn',
|
|
47
|
-
'AcmeConfigManager: ignoring constructor tls.contactEmail / smartProxyConfig.acme — DB already has AcmeConfigDoc. Manage via Domains > Certificates > Settings.',
|
|
48
|
-
);
|
|
26
|
+
logger.log('info', 'AcmeConfigManager: no AcmeConfig in DB — ACME disabled until configured via Domains > Certificates > Settings.');
|
|
49
27
|
}
|
|
50
28
|
|
|
51
29
|
this.cached = doc ? this.toPlain(doc) : null;
|
|
@@ -116,58 +94,6 @@ export class AcmeConfigManager {
|
|
|
116
94
|
// Internal helpers
|
|
117
95
|
// ==========================================================================
|
|
118
96
|
|
|
119
|
-
/**
|
|
120
|
-
* Build a seed object from the legacy constructor fields. Returns null
|
|
121
|
-
* if the user has not provided any of them.
|
|
122
|
-
*
|
|
123
|
-
* Supports BOTH `tls.contactEmail` (short form) and `smartProxyConfig.acme`
|
|
124
|
-
* (full form). `smartProxyConfig.acme` wins when both are present.
|
|
125
|
-
*/
|
|
126
|
-
private deriveSeedFromOptions(): Omit<IAcmeConfig, 'updatedAt' | 'updatedBy'> | null {
|
|
127
|
-
const acme = this.options.smartProxyConfig?.acme;
|
|
128
|
-
const tls = this.options.tls;
|
|
129
|
-
|
|
130
|
-
// Prefer the explicit smartProxyConfig.acme block if present.
|
|
131
|
-
if (acme?.accountEmail) {
|
|
132
|
-
return {
|
|
133
|
-
accountEmail: acme.accountEmail,
|
|
134
|
-
enabled: acme.enabled !== false,
|
|
135
|
-
useProduction: acme.useProduction !== false,
|
|
136
|
-
autoRenew: acme.autoRenew !== false,
|
|
137
|
-
renewThresholdDays: acme.renewThresholdDays ?? 30,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Fall back to the short tls.contactEmail form.
|
|
142
|
-
if (tls?.contactEmail) {
|
|
143
|
-
return {
|
|
144
|
-
accountEmail: tls.contactEmail,
|
|
145
|
-
enabled: true,
|
|
146
|
-
useProduction: true,
|
|
147
|
-
autoRenew: true,
|
|
148
|
-
renewThresholdDays: 30,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private async createSeedDoc(
|
|
156
|
-
seed: Omit<IAcmeConfig, 'updatedAt' | 'updatedBy'>,
|
|
157
|
-
): Promise<AcmeConfigDoc> {
|
|
158
|
-
const doc = new AcmeConfigDoc();
|
|
159
|
-
doc.configId = 'acme-config';
|
|
160
|
-
doc.accountEmail = seed.accountEmail;
|
|
161
|
-
doc.enabled = seed.enabled;
|
|
162
|
-
doc.useProduction = seed.useProduction;
|
|
163
|
-
doc.autoRenew = seed.autoRenew;
|
|
164
|
-
doc.renewThresholdDays = seed.renewThresholdDays;
|
|
165
|
-
doc.updatedAt = Date.now();
|
|
166
|
-
doc.updatedBy = 'seed';
|
|
167
|
-
await doc.save();
|
|
168
|
-
return doc;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
97
|
private toPlain(doc: AcmeConfigDoc): IAcmeConfig {
|
|
172
98
|
return {
|
|
173
99
|
accountEmail: doc.accountEmail,
|
package/ts/classes.dcrouter.ts
CHANGED
|
@@ -37,6 +37,8 @@ import type { IEmailPortConfig, IEmailServerSettings, IEmailServerSettingsSeed,
|
|
|
37
37
|
import type { IDcRouterRouteConfig, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, TRemoteIngressHubSettingsUpdate } from '../ts_interfaces/data/remoteingress.js';
|
|
38
38
|
import type { ISecurityCompiledPolicy } from '../ts_interfaces/data/security-policy.js';
|
|
39
39
|
|
|
40
|
+
type TInboundProxyProtocolPolicy = NonNullable<plugins.smartproxy.IRouteMatch['inboundProxyProtocol']>;
|
|
41
|
+
|
|
40
42
|
export interface IDcRouterOptions {
|
|
41
43
|
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
|
42
44
|
baseDir?: string;
|
|
@@ -454,14 +456,13 @@ export class DcRouter {
|
|
|
454
456
|
// AcmeConfigManager: optional, depends on DcRouterDb — owns the singleton
|
|
455
457
|
// ACME configuration (accountEmail, useProduction, etc.). Must run before
|
|
456
458
|
// SmartProxy so setupSmartProxy() can read the ACME config from the DB.
|
|
457
|
-
// On first boot, seeds from legacy `tls.contactEmail` / `smartProxyConfig.acme`.
|
|
458
459
|
if (this.options.dbConfig?.enabled !== false) {
|
|
459
460
|
this.serviceManager.addService(
|
|
460
461
|
new plugins.taskbuffer.Service('AcmeConfigManager')
|
|
461
462
|
.optional()
|
|
462
463
|
.dependsOn('DcRouterDb')
|
|
463
464
|
.withStart(async () => {
|
|
464
|
-
this.acmeConfigManager = new AcmeConfigManager(
|
|
465
|
+
this.acmeConfigManager = new AcmeConfigManager();
|
|
465
466
|
await this.acmeConfigManager.start();
|
|
466
467
|
})
|
|
467
468
|
.withStop(async () => {
|
|
@@ -648,6 +649,7 @@ export class DcRouter {
|
|
|
648
649
|
},
|
|
649
650
|
(preparedRoutes) => buildHttpRedirectRuntimeRoutes(preparedRoutes || []),
|
|
650
651
|
(storedRoute: IRoute) => this.hydrateStoredRouteForRuntime(storedRoute),
|
|
652
|
+
(routes) => this.applyInboundProxyProtocolPolicies(routes),
|
|
651
653
|
);
|
|
652
654
|
this.apiTokenManager = new ApiTokenManager();
|
|
653
655
|
await this.apiTokenManager.initialize();
|
|
@@ -813,7 +815,7 @@ export class DcRouter {
|
|
|
813
815
|
?? false;
|
|
814
816
|
}
|
|
815
817
|
|
|
816
|
-
private
|
|
818
|
+
private getRemoteIngressHubSettingsMigrationSeed(): TRemoteIngressHubSettingsUpdate {
|
|
817
819
|
const remoteIngressConfig = this.options.remoteIngressConfig;
|
|
818
820
|
const seed: TRemoteIngressHubSettingsUpdate = {};
|
|
819
821
|
if (remoteIngressConfig?.enabled !== undefined) {
|
|
@@ -831,7 +833,7 @@ export class DcRouter {
|
|
|
831
833
|
return seed;
|
|
832
834
|
}
|
|
833
835
|
|
|
834
|
-
private
|
|
836
|
+
private getEmailSettingsMigrationSeed(): IEmailServerSettingsSeed {
|
|
835
837
|
const seed: IEmailServerSettingsSeed = {};
|
|
836
838
|
if (this.options.emailConfig) {
|
|
837
839
|
seed.enabled = true;
|
|
@@ -1106,8 +1108,8 @@ export class DcRouter {
|
|
|
1106
1108
|
// Run any pending data migrations before anything else reads from the DB.
|
|
1107
1109
|
// This must complete before ConfigManagers loads profiles.
|
|
1108
1110
|
const migration = await createMigrationRunner(this.dcRouterDb.getDb(), commitinfo.version, {
|
|
1109
|
-
remoteIngressHubSettings: this.
|
|
1110
|
-
emailServerSettings: this.
|
|
1111
|
+
remoteIngressHubSettings: this.getRemoteIngressHubSettingsMigrationSeed(),
|
|
1112
|
+
emailServerSettings: this.getEmailSettingsMigrationSeed(),
|
|
1111
1113
|
});
|
|
1112
1114
|
const migrationResult = await migration.run();
|
|
1113
1115
|
if (migrationResult.stepsApplied.length > 0) {
|
|
@@ -1221,6 +1223,7 @@ export class DcRouter {
|
|
|
1221
1223
|
routes = augmentRoutesWithHttp3(routes, http3Config);
|
|
1222
1224
|
logger.log('info', 'HTTP/3: Augmented qualifying HTTPS routes with QUIC/H3 configuration');
|
|
1223
1225
|
}
|
|
1226
|
+
routes = this.applyInboundProxyProtocolPolicies(routes);
|
|
1224
1227
|
|
|
1225
1228
|
const compiledSecurityPolicy = await this.securityPolicyManager?.compileSmartProxyPolicy();
|
|
1226
1229
|
const mergedSecurityPolicy = this.mergeSecurityPolicies(
|
|
@@ -1380,27 +1383,12 @@ export class DcRouter {
|
|
|
1380
1383
|
};
|
|
1381
1384
|
}
|
|
1382
1385
|
|
|
1383
|
-
//
|
|
1384
|
-
//
|
|
1385
|
-
if (this.isRemoteIngressHubEnabled()) {
|
|
1386
|
-
smartProxyConfig.
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
}
|
|
1390
|
-
if (!smartProxyConfig.proxyIPs.includes('127.0.0.1')) {
|
|
1391
|
-
smartProxyConfig.proxyIPs.push('127.0.0.1');
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
// VPN uses socket mode with PP v2 — SmartProxy must accept proxy protocol from localhost
|
|
1396
|
-
if (this.options.vpnConfig?.enabled) {
|
|
1397
|
-
smartProxyConfig.acceptProxyProtocol = true;
|
|
1398
|
-
if (!smartProxyConfig.proxyIPs) {
|
|
1399
|
-
smartProxyConfig.proxyIPs = [];
|
|
1400
|
-
}
|
|
1401
|
-
if (!smartProxyConfig.proxyIPs.includes('127.0.0.1')) {
|
|
1402
|
-
smartProxyConfig.proxyIPs.push('127.0.0.1');
|
|
1403
|
-
}
|
|
1386
|
+
// RemoteIngress and VPN forward through localhost with PROXY protocol.
|
|
1387
|
+
// SmartProxy only uses this as a trust list; routes still opt in per listener.
|
|
1388
|
+
if (this.isRemoteIngressHubEnabled() || this.options.vpnConfig?.enabled) {
|
|
1389
|
+
const trustedProxyIPs = new Set(smartProxyConfig.trustedProxyIPs || []);
|
|
1390
|
+
trustedProxyIPs.add('127.0.0.1');
|
|
1391
|
+
smartProxyConfig.trustedProxyIPs = [...trustedProxyIPs];
|
|
1404
1392
|
}
|
|
1405
1393
|
|
|
1406
1394
|
// Create SmartProxy instance
|
|
@@ -1577,6 +1565,101 @@ export class DcRouter {
|
|
|
1577
1565
|
|
|
1578
1566
|
|
|
1579
1567
|
|
|
1568
|
+
private applyInboundProxyProtocolPolicies(
|
|
1569
|
+
routes: plugins.smartproxy.IRouteConfig[],
|
|
1570
|
+
): plugins.smartproxy.IRouteConfig[] {
|
|
1571
|
+
const policiesByListener = new Map<string, TInboundProxyProtocolPolicy>();
|
|
1572
|
+
|
|
1573
|
+
for (const route of routes) {
|
|
1574
|
+
const policy = route.match?.inboundProxyProtocol || this.getDesiredInboundProxyProtocolPolicy(route);
|
|
1575
|
+
if (!policy) {
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
for (const listenerKey of this.getInboundProxyListenerKeys(route)) {
|
|
1579
|
+
const mergedPolicy = this.mergeInboundProxyProtocolPolicies(
|
|
1580
|
+
policiesByListener.get(listenerKey),
|
|
1581
|
+
policy,
|
|
1582
|
+
);
|
|
1583
|
+
if (mergedPolicy) {
|
|
1584
|
+
policiesByListener.set(listenerKey, mergedPolicy);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (policiesByListener.size === 0) {
|
|
1590
|
+
return routes;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
return routes.map((route) => {
|
|
1594
|
+
if (route.match?.inboundProxyProtocol) {
|
|
1595
|
+
return route;
|
|
1596
|
+
}
|
|
1597
|
+
let listenerPolicy: TInboundProxyProtocolPolicy | undefined;
|
|
1598
|
+
for (const listenerKey of this.getInboundProxyListenerKeys(route)) {
|
|
1599
|
+
listenerPolicy = this.mergeInboundProxyProtocolPolicies(
|
|
1600
|
+
listenerPolicy,
|
|
1601
|
+
policiesByListener.get(listenerKey),
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
if (!listenerPolicy) {
|
|
1605
|
+
return route;
|
|
1606
|
+
}
|
|
1607
|
+
return {
|
|
1608
|
+
...route,
|
|
1609
|
+
match: {
|
|
1610
|
+
...route.match,
|
|
1611
|
+
inboundProxyProtocol: listenerPolicy,
|
|
1612
|
+
},
|
|
1613
|
+
};
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
private getDesiredInboundProxyProtocolPolicy(
|
|
1618
|
+
route: plugins.smartproxy.IRouteConfig,
|
|
1619
|
+
): TInboundProxyProtocolPolicy | undefined {
|
|
1620
|
+
const dcRoute = route as IDcRouterRouteConfig;
|
|
1621
|
+
if (this.isRemoteIngressHubEnabled() && dcRoute.remoteIngress?.enabled) {
|
|
1622
|
+
const ports = plugins.smartproxy.expandPortRange(route.match.ports as any) as number[];
|
|
1623
|
+
if (ports.some((port) => port === 25 || port === 587)) {
|
|
1624
|
+
return { mode: 'required' };
|
|
1625
|
+
}
|
|
1626
|
+
return { mode: 'optional' };
|
|
1627
|
+
}
|
|
1628
|
+
if (this.options.vpnConfig?.enabled) {
|
|
1629
|
+
return { mode: 'optional' };
|
|
1630
|
+
}
|
|
1631
|
+
return undefined;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
private getInboundProxyListenerKeys(route: plugins.smartproxy.IRouteConfig): string[] {
|
|
1635
|
+
const ports = plugins.smartproxy.expandPortRange(route.match.ports as any) as number[];
|
|
1636
|
+
const transports = route.match.transport === 'udp'
|
|
1637
|
+
? ['udp']
|
|
1638
|
+
: route.match.transport === 'all'
|
|
1639
|
+
? ['tcp', 'udp']
|
|
1640
|
+
: ['tcp'];
|
|
1641
|
+
const keys: string[] = [];
|
|
1642
|
+
for (const port of ports) {
|
|
1643
|
+
for (const transport of transports) {
|
|
1644
|
+
keys.push(`${transport}:${port}`);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
return keys;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
private mergeInboundProxyProtocolPolicies(
|
|
1651
|
+
current?: TInboundProxyProtocolPolicy,
|
|
1652
|
+
next?: TInboundProxyProtocolPolicy,
|
|
1653
|
+
): TInboundProxyProtocolPolicy | undefined {
|
|
1654
|
+
if (!current) return next;
|
|
1655
|
+
if (!next) return current;
|
|
1656
|
+
if (current.mode === 'required') return current;
|
|
1657
|
+
if (next.mode === 'required') return next;
|
|
1658
|
+
if (current.mode === 'optional') return current;
|
|
1659
|
+
if (next.mode === 'optional') return next;
|
|
1660
|
+
return current;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1580
1663
|
/**
|
|
1581
1664
|
* Generate SmartProxy routes for email configuration
|
|
1582
1665
|
*/
|
|
@@ -1658,13 +1741,18 @@ export class DcRouter {
|
|
|
1658
1741
|
const routeConfig: IDcRouterRouteConfig = {
|
|
1659
1742
|
name: routeName,
|
|
1660
1743
|
match: {
|
|
1661
|
-
ports: [port]
|
|
1744
|
+
ports: [port],
|
|
1745
|
+
transport: 'tcp',
|
|
1662
1746
|
},
|
|
1663
1747
|
action: action
|
|
1664
1748
|
};
|
|
1665
1749
|
|
|
1666
1750
|
if (this.isRemoteIngressHubEnabled()) {
|
|
1667
1751
|
routeConfig.remoteIngress = { enabled: true };
|
|
1752
|
+
const inboundProxyProtocol = this.getRemoteIngressEmailInboundProxyPolicy(port);
|
|
1753
|
+
if (inboundProxyProtocol) {
|
|
1754
|
+
routeConfig.match.inboundProxyProtocol = inboundProxyProtocol;
|
|
1755
|
+
}
|
|
1668
1756
|
}
|
|
1669
1757
|
|
|
1670
1758
|
// Add the route to our list
|
|
@@ -1765,8 +1853,15 @@ export class DcRouter {
|
|
|
1765
1853
|
}
|
|
1766
1854
|
|
|
1767
1855
|
const targetHost = target.host === 'localhost' ? '127.0.0.1' : target.host;
|
|
1856
|
+
const inboundProxyProtocol = this.getRemoteIngressEmailInboundProxyPolicy(routePorts[0]);
|
|
1768
1857
|
return {
|
|
1769
1858
|
...route,
|
|
1859
|
+
match: {
|
|
1860
|
+
...route.match,
|
|
1861
|
+
...(inboundProxyProtocol
|
|
1862
|
+
? { inboundProxyProtocol }
|
|
1863
|
+
: {}),
|
|
1864
|
+
},
|
|
1770
1865
|
action: {
|
|
1771
1866
|
type: 'socket-handler' as any,
|
|
1772
1867
|
socketHandler: this.createEmailSocketProxyHandler(targetHost, target.port),
|
|
@@ -1774,6 +1869,15 @@ export class DcRouter {
|
|
|
1774
1869
|
};
|
|
1775
1870
|
}
|
|
1776
1871
|
|
|
1872
|
+
private getRemoteIngressEmailInboundProxyPolicy(
|
|
1873
|
+
port: number,
|
|
1874
|
+
): TInboundProxyProtocolPolicy | undefined {
|
|
1875
|
+
if (!this.isRemoteIngressHubEnabled()) {
|
|
1876
|
+
return undefined;
|
|
1877
|
+
}
|
|
1878
|
+
return { mode: port === 25 || port === 587 ? 'required' : 'optional' };
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1777
1881
|
private createEmailSocketProxyHandler(
|
|
1778
1882
|
targetHost: string,
|
|
1779
1883
|
targetPort: number,
|
|
@@ -1972,28 +2076,9 @@ export class DcRouter {
|
|
|
1972
2076
|
465: 10465 // SMTPS
|
|
1973
2077
|
};
|
|
1974
2078
|
|
|
1975
|
-
// Transform domains if they are provided as strings
|
|
1976
|
-
let transformedDomains = this.options.emailConfig.domains;
|
|
1977
|
-
if (transformedDomains && transformedDomains.length > 0) {
|
|
1978
|
-
// Check if domains are strings (for backward compatibility)
|
|
1979
|
-
if (typeof transformedDomains[0] === 'string') {
|
|
1980
|
-
transformedDomains = (transformedDomains as any).map((domain: string) => ({
|
|
1981
|
-
domain,
|
|
1982
|
-
dnsMode: 'external-dns' as const,
|
|
1983
|
-
dkim: {
|
|
1984
|
-
selector: 'default',
|
|
1985
|
-
keySize: 2048,
|
|
1986
|
-
rotateKeys: false,
|
|
1987
|
-
rotationInterval: 90
|
|
1988
|
-
}
|
|
1989
|
-
}));
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
2079
|
// Create config with mapped ports
|
|
1994
2080
|
const emailConfig: IUnifiedEmailServerOptions = await this.workAppMailManager.applyStoredIdentitiesToEmailConfig({
|
|
1995
2081
|
...this.options.emailConfig,
|
|
1996
|
-
domains: transformedDomains,
|
|
1997
2082
|
ports: this.options.emailConfig.ports.map(port => portMapping[port] || port + 10000),
|
|
1998
2083
|
persistRoutes: this.options.emailConfig.persistRoutes ?? false,
|
|
1999
2084
|
queue: {
|
|
@@ -2363,8 +2448,8 @@ export class DcRouter {
|
|
|
2363
2448
|
// Ensure DKIM keys exist for internal-dns domains before generating records.
|
|
2364
2449
|
await this.initializeDkimForEmailDomains();
|
|
2365
2450
|
|
|
2366
|
-
|
|
2367
|
-
|
|
2451
|
+
// Generate DKIM records directly from smartmta.
|
|
2452
|
+
const dkimRecords = await this.loadDkimRecords();
|
|
2368
2453
|
|
|
2369
2454
|
// Combine all records: authoritative, email, DKIM, and user-defined
|
|
2370
2455
|
const allRecords = [...authoritativeRecords, ...emailDnsRecords, ...dkimRecords];
|
|
@@ -111,13 +111,13 @@ export class ApiTokenManager {
|
|
|
111
111
|
const scopes = new Set<TApiTokenScope>([...token.scopes, ...(token.policy?.scopes || [])]);
|
|
112
112
|
if (scopes.has(scope)) return true;
|
|
113
113
|
|
|
114
|
-
const
|
|
114
|
+
const equivalentScopes: Partial<Record<TApiTokenScope, TApiTokenScope[]>> = {
|
|
115
115
|
'gateway-clients:read': ['workhosters:read'],
|
|
116
116
|
'gateway-clients:write': ['workhosters:write'],
|
|
117
117
|
'workhosters:read': ['gateway-clients:read'],
|
|
118
118
|
'workhosters:write': ['gateway-clients:write'],
|
|
119
119
|
};
|
|
120
|
-
return Boolean(
|
|
120
|
+
return Boolean(equivalentScopes[scope]?.some((alias) => scopes.has(alias)));
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/**
|
|
@@ -68,6 +68,7 @@ export class RouteConfigManager {
|
|
|
68
68
|
private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void | Promise<void>,
|
|
69
69
|
private getRuntimeRoutes?: (preparedRoutes?: plugins.smartproxy.IRouteConfig[]) => plugins.smartproxy.IRouteConfig[],
|
|
70
70
|
private hydrateStoredRoute?: (storedRoute: IRoute) => plugins.smartproxy.IRouteConfig | undefined,
|
|
71
|
+
private applyInboundProxyPolicies?: (routes: plugins.smartproxy.IRouteConfig[]) => plugins.smartproxy.IRouteConfig[],
|
|
71
72
|
) {}
|
|
72
73
|
|
|
73
74
|
/** Expose routes map for reference resolution lookups. */
|
|
@@ -714,12 +715,15 @@ export class RouteConfigManager {
|
|
|
714
715
|
const smartProxy = this.getSmartProxy();
|
|
715
716
|
if (!smartProxy) return;
|
|
716
717
|
|
|
717
|
-
|
|
718
|
+
let enabledRoutes = this.getPreparedEnabledRoutesForApply();
|
|
718
719
|
|
|
719
720
|
const runtimeRoutes = this.getRuntimeRoutes?.(enabledRoutes) || [];
|
|
720
721
|
for (const route of runtimeRoutes) {
|
|
721
722
|
enabledRoutes.push(this.prepareRouteForApply(route));
|
|
722
723
|
}
|
|
724
|
+
if (this.applyInboundProxyPolicies) {
|
|
725
|
+
enabledRoutes = this.applyInboundProxyPolicies(enabledRoutes);
|
|
726
|
+
}
|
|
723
727
|
|
|
724
728
|
await smartProxy.updateRoutes(enabledRoutes);
|
|
725
729
|
|
|
@@ -8,9 +8,7 @@ const getDb = () => DcRouterDb.getInstance().getDb();
|
|
|
8
8
|
* keyed on the fixed `configId = 'acme-config'` following the
|
|
9
9
|
* `VpnServerKeysDoc` pattern.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* constructor fields. Managed via the OpsServer UI at
|
|
13
|
-
* **Domains > Certificates > Settings**.
|
|
11
|
+
* Managed via the OpsServer UI at **Domains > Certificates > Settings**.
|
|
14
12
|
*/
|
|
15
13
|
@plugins.smartdata.Collection(() => getDb())
|
|
16
14
|
export class AcmeConfigDoc extends plugins.smartdata.SmartDataDbDoc<AcmeConfigDoc, AcmeConfigDoc> {
|
package/ts/dns/manager.dns.ts
CHANGED
|
@@ -24,7 +24,6 @@ import type {
|
|
|
24
24
|
*
|
|
25
25
|
* Responsibilities:
|
|
26
26
|
* - Load Domain/DnsRecord docs from the DB on start
|
|
27
|
-
* - First-boot seeding from legacy constructor config (dnsScopes/dnsRecords/dnsNsDomains)
|
|
28
27
|
* - Register dcrouter-hosted domain records with smartdns.DnsServer at startup
|
|
29
28
|
* - Provide CRUD methods used by OpsServer handlers (dcrouter-hosted domains hit
|
|
30
29
|
* smartdns, provider domains hit the provider API)
|
|
@@ -53,13 +52,8 @@ export class DnsManager {
|
|
|
53
52
|
// Lifecycle
|
|
54
53
|
// ==========================================================================
|
|
55
54
|
|
|
56
|
-
/**
|
|
57
|
-
* Called from DcRouter after DcRouterDb is up. Performs first-boot seeding
|
|
58
|
-
* from legacy constructor config if (and only if) the DB is empty.
|
|
59
|
-
*/
|
|
60
55
|
public async start(): Promise<void> {
|
|
61
56
|
logger.log('info', 'DnsManager: starting');
|
|
62
|
-
await this.seedFromConstructorConfigIfEmpty();
|
|
63
57
|
}
|
|
64
58
|
|
|
65
59
|
public async stop(): Promise<void> {
|
|
@@ -77,103 +71,6 @@ export class DnsManager {
|
|
|
77
71
|
await this.applyDcrouterDomainsToDnsServer();
|
|
78
72
|
}
|
|
79
73
|
|
|
80
|
-
// ==========================================================================
|
|
81
|
-
// First-boot seeding
|
|
82
|
-
// ==========================================================================
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* If no DomainDocs exist yet but the constructor has legacy DNS fields,
|
|
86
|
-
* seed them as dcrouter-hosted (`domain.source: 'dcrouter'`) zones with
|
|
87
|
-
* local (`record.source: 'local'`) records. On subsequent boots (DB has
|
|
88
|
-
* entries), constructor config is ignored with a warning.
|
|
89
|
-
*/
|
|
90
|
-
private async seedFromConstructorConfigIfEmpty(): Promise<void> {
|
|
91
|
-
const existingDomains = await DomainDoc.findAll();
|
|
92
|
-
const hasLegacyConfig =
|
|
93
|
-
(this.options.dnsScopes && this.options.dnsScopes.length > 0) ||
|
|
94
|
-
(this.options.dnsRecords && this.options.dnsRecords.length > 0);
|
|
95
|
-
|
|
96
|
-
if (existingDomains.length > 0) {
|
|
97
|
-
if (hasLegacyConfig) {
|
|
98
|
-
logger.log(
|
|
99
|
-
'warn',
|
|
100
|
-
'DnsManager: DB has DomainDoc entries — ignoring legacy dnsScopes/dnsRecords constructor config. ' +
|
|
101
|
-
'dnsNsDomains is still required for nameserver and DoH bootstrap unless that moves into DB-backed config.',
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (!hasLegacyConfig) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
logger.log('info', 'DnsManager: seeding DB from legacy constructor DNS config');
|
|
112
|
-
|
|
113
|
-
const now = Date.now();
|
|
114
|
-
const seededDomains = new Map<string, DomainDoc>();
|
|
115
|
-
|
|
116
|
-
// Create one DomainDoc per dnsScope (these are the authoritative zones)
|
|
117
|
-
for (const scope of this.options.dnsScopes ?? []) {
|
|
118
|
-
const domain = new DomainDoc();
|
|
119
|
-
domain.id = plugins.uuid.v4();
|
|
120
|
-
domain.name = scope.toLowerCase();
|
|
121
|
-
domain.source = 'dcrouter';
|
|
122
|
-
domain.authoritative = true;
|
|
123
|
-
domain.createdAt = now;
|
|
124
|
-
domain.updatedAt = now;
|
|
125
|
-
domain.createdBy = 'seed';
|
|
126
|
-
await domain.save();
|
|
127
|
-
seededDomains.set(domain.name, domain);
|
|
128
|
-
logger.log('info', `DnsManager: seeded DomainDoc for ${domain.name}`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Map each legacy dnsRecord to its parent DomainDoc
|
|
132
|
-
for (const rec of this.options.dnsRecords ?? []) {
|
|
133
|
-
const parent = this.findParentDomain(rec.name, seededDomains);
|
|
134
|
-
if (!parent) {
|
|
135
|
-
logger.log(
|
|
136
|
-
'warn',
|
|
137
|
-
`DnsManager: legacy dnsRecord '${rec.name}' has no matching dnsScope — skipping seed`,
|
|
138
|
-
);
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
const record = new DnsRecordDoc();
|
|
142
|
-
record.id = plugins.uuid.v4();
|
|
143
|
-
record.domainId = parent.id;
|
|
144
|
-
record.name = rec.name.toLowerCase();
|
|
145
|
-
record.type = rec.type as TDnsRecordType;
|
|
146
|
-
record.value = rec.value;
|
|
147
|
-
record.ttl = rec.ttl ?? 300;
|
|
148
|
-
record.source = 'local';
|
|
149
|
-
record.createdAt = now;
|
|
150
|
-
record.updatedAt = now;
|
|
151
|
-
record.createdBy = 'seed';
|
|
152
|
-
await record.save();
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
logger.log(
|
|
156
|
-
'info',
|
|
157
|
-
`DnsManager: seeded ${seededDomains.size} domain(s) and ${this.options.dnsRecords?.length ?? 0} record(s) from legacy config`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
private findParentDomain(
|
|
162
|
-
recordName: string,
|
|
163
|
-
domains: Map<string, DomainDoc>,
|
|
164
|
-
): DomainDoc | null {
|
|
165
|
-
const lower = recordName.toLowerCase().replace(/^\*\./, '');
|
|
166
|
-
let candidate: DomainDoc | null = null;
|
|
167
|
-
for (const [name, doc] of domains) {
|
|
168
|
-
if (lower === name || lower.endsWith(`.${name}`)) {
|
|
169
|
-
if (!candidate || name.length > candidate.name.length) {
|
|
170
|
-
candidate = doc;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return candidate;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
74
|
// ==========================================================================
|
|
178
75
|
// DcRouter-hosted domain DnsServer wiring
|
|
179
76
|
// ==========================================================================
|