@serve.zone/dcrouter 13.17.3 → 13.17.8
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/dist_serve/bundle.js +128 -128
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +5 -1
- package/dist_ts/classes.dcrouter.js +31 -15
- package/dist_ts/config/classes.route-config-manager.d.ts +7 -2
- package/dist_ts/config/classes.route-config-manager.js +64 -38
- package/dist_ts/config/classes.target-profile-manager.d.ts +12 -1
- package/dist_ts/config/classes.target-profile-manager.js +98 -11
- package/dist_ts/dns/manager.dns.js +3 -3
- package/dist_ts/monitoring/classes.metricsmanager.d.ts +1 -1
- package/dist_ts/opsserver/handlers/certificate.handler.js +6 -9
- package/dist_ts/vpn/classes.vpn-manager.d.ts +11 -2
- package/dist_ts/vpn/classes.vpn-manager.js +120 -64
- package/dist_ts_interfaces/data/target-profile.d.ts +1 -1
- package/dist_ts_migrations/index.js +25 -18
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/network/ops-view-targetprofiles.d.ts +4 -0
- package/dist_ts_web/elements/network/ops-view-targetprofiles.js +46 -9
- package/dist_ts_web/elements/network/ops-view-vpn.d.ts +6 -7
- package/dist_ts_web/elements/network/ops-view-vpn.js +39 -34
- package/package.json +6 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +37 -13
- package/ts/config/classes.route-config-manager.ts +80 -36
- package/ts/config/classes.target-profile-manager.ts +129 -6
- package/ts/dns/manager.dns.ts +2 -2
- package/ts/opsserver/handlers/certificate.handler.ts +4 -5
- package/ts/vpn/classes.vpn-manager.ts +146 -60
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/network/ops-view-targetprofiles.ts +57 -8
- package/ts_web/elements/network/ops-view-vpn.ts +43 -32
package/ts/classes.dcrouter.ts
CHANGED
|
@@ -315,7 +315,8 @@ export class DcRouter {
|
|
|
315
315
|
// Seed routes assembled during setupSmartProxy, passed to RouteConfigManager for DB seeding
|
|
316
316
|
private seedConfigRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
317
317
|
private seedEmailRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
318
|
-
|
|
318
|
+
// Runtime-only DoH routes. These carry live socket handlers and must never be persisted.
|
|
319
|
+
private runtimeDnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
319
320
|
|
|
320
321
|
// Environment access
|
|
321
322
|
private qenv = new plugins.qenv.Qenv('./', '.nogit/');
|
|
@@ -547,7 +548,9 @@ export class DcRouter {
|
|
|
547
548
|
await this.referenceResolver.initialize();
|
|
548
549
|
|
|
549
550
|
// Initialize target profile manager
|
|
550
|
-
this.targetProfileManager = new TargetProfileManager(
|
|
551
|
+
this.targetProfileManager = new TargetProfileManager(
|
|
552
|
+
() => this.routeConfigManager?.getRoutes() || new Map(),
|
|
553
|
+
);
|
|
551
554
|
await this.targetProfileManager.initialize();
|
|
552
555
|
|
|
553
556
|
this.routeConfigManager = new RouteConfigManager(
|
|
@@ -560,7 +563,10 @@ export class DcRouter {
|
|
|
560
563
|
return [];
|
|
561
564
|
}
|
|
562
565
|
return this.targetProfileManager.getMatchingClientIps(
|
|
563
|
-
route,
|
|
566
|
+
route,
|
|
567
|
+
routeId,
|
|
568
|
+
this.vpnManager.listClients(),
|
|
569
|
+
this.routeConfigManager?.getRoutes() || new Map(),
|
|
564
570
|
);
|
|
565
571
|
}
|
|
566
572
|
: undefined,
|
|
@@ -575,14 +581,15 @@ export class DcRouter {
|
|
|
575
581
|
this.tunnelManager.syncAllowedEdges();
|
|
576
582
|
}
|
|
577
583
|
},
|
|
584
|
+
() => this.runtimeDnsRoutes,
|
|
578
585
|
);
|
|
579
586
|
this.apiTokenManager = new ApiTokenManager();
|
|
580
587
|
await this.apiTokenManager.initialize();
|
|
581
588
|
await this.routeConfigManager.initialize(
|
|
582
589
|
this.seedConfigRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
583
590
|
this.seedEmailRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
584
|
-
this.seedDnsRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
585
591
|
);
|
|
592
|
+
await this.targetProfileManager.normalizeAllRouteRefs();
|
|
586
593
|
|
|
587
594
|
// Seed default profiles/targets if DB is empty and seeding is enabled
|
|
588
595
|
const seeder = new DbSeeder(this.referenceResolver);
|
|
@@ -886,7 +893,7 @@ export class DcRouter {
|
|
|
886
893
|
this.smartProxy = undefined;
|
|
887
894
|
}
|
|
888
895
|
|
|
889
|
-
// Assemble seed routes from constructor config — these will be seeded into DB
|
|
896
|
+
// Assemble serializable seed routes from constructor config — these will be seeded into DB
|
|
890
897
|
// by RouteConfigManager.initialize() when the ConfigManagers service starts.
|
|
891
898
|
this.seedConfigRoutes = (this.options.smartProxyConfig?.routes || []) as plugins.smartproxy.IRouteConfig[];
|
|
892
899
|
logger.log('info', `Found ${this.seedConfigRoutes.length} routes in config`);
|
|
@@ -897,17 +904,17 @@ export class DcRouter {
|
|
|
897
904
|
logger.log('debug', 'Email routes generated', { routes: JSON.stringify(this.seedEmailRoutes) });
|
|
898
905
|
}
|
|
899
906
|
|
|
900
|
-
this.
|
|
907
|
+
this.runtimeDnsRoutes = [];
|
|
901
908
|
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0) {
|
|
902
|
-
this.
|
|
903
|
-
logger.log('debug', `DNS routes for nameservers ${this.options.dnsNsDomains.join(', ')}`, { routes: JSON.stringify(this.
|
|
909
|
+
this.runtimeDnsRoutes = this.generateDnsRoutes();
|
|
910
|
+
logger.log('debug', `DNS routes for nameservers ${this.options.dnsNsDomains.join(', ')}`, { routes: JSON.stringify(this.runtimeDnsRoutes) });
|
|
904
911
|
}
|
|
905
912
|
|
|
906
913
|
// Combined routes for SmartProxy bootstrap (before DB routes are loaded)
|
|
907
914
|
let routes: plugins.smartproxy.IRouteConfig[] = [
|
|
908
915
|
...this.seedConfigRoutes,
|
|
909
916
|
...this.seedEmailRoutes,
|
|
910
|
-
...this.
|
|
917
|
+
...this.runtimeDnsRoutes,
|
|
911
918
|
];
|
|
912
919
|
|
|
913
920
|
// Build the ACME options for SmartProxy from the DB-backed AcmeConfigManager.
|
|
@@ -1457,7 +1464,6 @@ export class DcRouter {
|
|
|
1457
1464
|
await this.routeConfigManager.initialize(
|
|
1458
1465
|
this.seedConfigRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
1459
1466
|
this.seedEmailRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
1460
|
-
this.seedDnsRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
1461
1467
|
);
|
|
1462
1468
|
}
|
|
1463
1469
|
|
|
@@ -2179,7 +2185,7 @@ export class DcRouter {
|
|
|
2179
2185
|
// Pass current bootstrap routes so the manager can derive edge ports initially.
|
|
2180
2186
|
// Once RouteConfigManager applies the full DB set, the onRoutesApplied callback
|
|
2181
2187
|
// will push the complete merged routes here.
|
|
2182
|
-
const bootstrapRoutes = [...this.seedConfigRoutes, ...this.seedEmailRoutes, ...this.
|
|
2188
|
+
const bootstrapRoutes = [...this.seedConfigRoutes, ...this.seedEmailRoutes, ...this.runtimeDnsRoutes];
|
|
2183
2189
|
this.remoteIngressManager.setRoutes(bootstrapRoutes as any[]);
|
|
2184
2190
|
|
|
2185
2191
|
// If ConfigManagers finished before us, re-apply routes
|
|
@@ -2283,8 +2289,11 @@ export class DcRouter {
|
|
|
2283
2289
|
|
|
2284
2290
|
// Resolve DNS A records for matched domains (with caching)
|
|
2285
2291
|
for (const domain of domains) {
|
|
2286
|
-
|
|
2287
|
-
|
|
2292
|
+
if (this.isWildcardVpnDomain(domain)) {
|
|
2293
|
+
this.logSkippedWildcardAllowedIp(domain);
|
|
2294
|
+
continue;
|
|
2295
|
+
}
|
|
2296
|
+
const resolvedIps = await this.resolveVpnDomainIPs(domain);
|
|
2288
2297
|
for (const ip of resolvedIps) {
|
|
2289
2298
|
ips.add(`${ip}/32`);
|
|
2290
2299
|
}
|
|
@@ -2303,6 +2312,8 @@ export class DcRouter {
|
|
|
2303
2312
|
|
|
2304
2313
|
/** Cache for DNS-resolved IPs of VPN-gated domains. TTL: 5 minutes. */
|
|
2305
2314
|
private vpnDomainIpCache = new Map<string, { ips: string[]; expiresAt: number }>();
|
|
2315
|
+
/** Deduplicate wildcard-resolution warnings for WireGuard AllowedIPs generation. */
|
|
2316
|
+
private warnedWildcardVpnDomains = new Set<string>();
|
|
2306
2317
|
|
|
2307
2318
|
/**
|
|
2308
2319
|
* Resolve a domain's A record(s) for VPN AllowedIPs, with a 5-minute cache.
|
|
@@ -2328,6 +2339,19 @@ export class DcRouter {
|
|
|
2328
2339
|
}
|
|
2329
2340
|
}
|
|
2330
2341
|
|
|
2342
|
+
private isWildcardVpnDomain(domain: string): boolean {
|
|
2343
|
+
return domain.includes('*');
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
private logSkippedWildcardAllowedIp(domain: string): void {
|
|
2347
|
+
if (this.warnedWildcardVpnDomains.has(domain)) return;
|
|
2348
|
+
this.warnedWildcardVpnDomains.add(domain);
|
|
2349
|
+
logger.log(
|
|
2350
|
+
'warn',
|
|
2351
|
+
`VPN: Skipping wildcard domain '${domain}' for WireGuard AllowedIPs; wildcard patterns must be resolved to concrete hostnames by matching routes.`,
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2331
2355
|
// VPN security injection is now handled dynamically by RouteConfigManager.applyRoutes()
|
|
2332
2356
|
// via the getVpnAllowList callback — no longer a separate method here.
|
|
2333
2357
|
|
|
@@ -55,6 +55,7 @@ export class RouteConfigManager {
|
|
|
55
55
|
private getVpnClientIpsForRoute?: (route: IDcRouterRouteConfig, routeId?: string) => TIpAllowEntry[],
|
|
56
56
|
private referenceResolver?: ReferenceResolver,
|
|
57
57
|
private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void,
|
|
58
|
+
private getRuntimeRoutes?: () => plugins.smartproxy.IRouteConfig[],
|
|
58
59
|
) {}
|
|
59
60
|
|
|
60
61
|
/** Expose routes map for reference resolution lookups. */
|
|
@@ -63,7 +64,8 @@ export class RouteConfigManager {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
|
-
* Load persisted routes, seed config/email/dns routes,
|
|
67
|
+
* Load persisted routes, seed serializable config/email/dns routes,
|
|
68
|
+
* compute warnings, and apply the combined DB-backed + runtime route set to SmartProxy.
|
|
67
69
|
*/
|
|
68
70
|
public async initialize(
|
|
69
71
|
configRoutes: IDcRouterRouteConfig[] = [],
|
|
@@ -284,23 +286,40 @@ export class RouteConfigManager {
|
|
|
284
286
|
|
|
285
287
|
private async loadRoutes(): Promise<void> {
|
|
286
288
|
const docs = await RouteDoc.findAll();
|
|
289
|
+
let prunedRuntimeRoutes = 0;
|
|
290
|
+
|
|
287
291
|
for (const doc of docs) {
|
|
288
|
-
if (doc.id)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
292
|
+
if (!doc.id) continue;
|
|
293
|
+
|
|
294
|
+
const storedRoute: IRoute = {
|
|
295
|
+
id: doc.id,
|
|
296
|
+
route: doc.route,
|
|
297
|
+
enabled: doc.enabled,
|
|
298
|
+
createdAt: doc.createdAt,
|
|
299
|
+
updatedAt: doc.updatedAt,
|
|
300
|
+
createdBy: doc.createdBy,
|
|
301
|
+
origin: doc.origin || 'api',
|
|
302
|
+
metadata: doc.metadata,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
if (this.isPersistedRuntimeRoute(storedRoute)) {
|
|
306
|
+
await doc.delete();
|
|
307
|
+
prunedRuntimeRoutes++;
|
|
308
|
+
logger.log(
|
|
309
|
+
'warn',
|
|
310
|
+
`Removed persisted runtime-only route '${storedRoute.route.name || storedRoute.id}' (${storedRoute.id}) from RouteDoc`,
|
|
311
|
+
);
|
|
312
|
+
continue;
|
|
299
313
|
}
|
|
314
|
+
|
|
315
|
+
this.routes.set(doc.id, storedRoute);
|
|
300
316
|
}
|
|
301
317
|
if (this.routes.size > 0) {
|
|
302
318
|
logger.log('info', `Loaded ${this.routes.size} route(s) from database`);
|
|
303
319
|
}
|
|
320
|
+
if (prunedRuntimeRoutes > 0) {
|
|
321
|
+
logger.log('info', `Pruned ${prunedRuntimeRoutes} persisted runtime-only route(s) from RouteDoc`);
|
|
322
|
+
}
|
|
304
323
|
}
|
|
305
324
|
|
|
306
325
|
private async persistRoute(stored: IRoute): Promise<void> {
|
|
@@ -389,36 +408,18 @@ export class RouteConfigManager {
|
|
|
389
408
|
|
|
390
409
|
const enabledRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
391
410
|
|
|
392
|
-
const http3Config = this.getHttp3Config?.();
|
|
393
|
-
const vpnCallback = this.getVpnClientIpsForRoute;
|
|
394
|
-
|
|
395
|
-
// Helper: inject VPN security into a vpnOnly route
|
|
396
|
-
const injectVpn = (route: plugins.smartproxy.IRouteConfig, routeId?: string): plugins.smartproxy.IRouteConfig => {
|
|
397
|
-
if (!vpnCallback) return route;
|
|
398
|
-
const dcRoute = route as IDcRouterRouteConfig;
|
|
399
|
-
if (!dcRoute.vpnOnly) return route;
|
|
400
|
-
const vpnEntries = vpnCallback(dcRoute, routeId);
|
|
401
|
-
const existingEntries = route.security?.ipAllowList || [];
|
|
402
|
-
return {
|
|
403
|
-
...route,
|
|
404
|
-
security: {
|
|
405
|
-
...route.security,
|
|
406
|
-
ipAllowList: [...existingEntries, ...vpnEntries],
|
|
407
|
-
},
|
|
408
|
-
};
|
|
409
|
-
};
|
|
410
|
-
|
|
411
411
|
// Add all enabled routes with HTTP/3 and VPN augmentation
|
|
412
412
|
for (const route of this.routes.values()) {
|
|
413
413
|
if (route.enabled) {
|
|
414
|
-
|
|
415
|
-
if (http3Config?.enabled !== false) {
|
|
416
|
-
r = augmentRouteWithHttp3(r, { enabled: true, ...http3Config });
|
|
417
|
-
}
|
|
418
|
-
enabledRoutes.push(injectVpn(r, route.id));
|
|
414
|
+
enabledRoutes.push(this.prepareRouteForApply(route.route, route.id));
|
|
419
415
|
}
|
|
420
416
|
}
|
|
421
417
|
|
|
418
|
+
const runtimeRoutes = this.getRuntimeRoutes?.() || [];
|
|
419
|
+
for (const route of runtimeRoutes) {
|
|
420
|
+
enabledRoutes.push(this.prepareRouteForApply(route));
|
|
421
|
+
}
|
|
422
|
+
|
|
422
423
|
await smartProxy.updateRoutes(enabledRoutes);
|
|
423
424
|
|
|
424
425
|
// Notify listeners (e.g. RemoteIngressManager) of the route set
|
|
@@ -429,4 +430,47 @@ export class RouteConfigManager {
|
|
|
429
430
|
logger.log('info', `Applied ${enabledRoutes.length} routes to SmartProxy (${this.routes.size} total)`);
|
|
430
431
|
});
|
|
431
432
|
}
|
|
433
|
+
|
|
434
|
+
private prepareRouteForApply(
|
|
435
|
+
route: plugins.smartproxy.IRouteConfig,
|
|
436
|
+
routeId?: string,
|
|
437
|
+
): plugins.smartproxy.IRouteConfig {
|
|
438
|
+
let preparedRoute = route;
|
|
439
|
+
const http3Config = this.getHttp3Config?.();
|
|
440
|
+
|
|
441
|
+
if (http3Config?.enabled !== false) {
|
|
442
|
+
preparedRoute = augmentRouteWithHttp3(preparedRoute, { enabled: true, ...http3Config });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return this.injectVpnSecurity(preparedRoute, routeId);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private injectVpnSecurity(
|
|
449
|
+
route: plugins.smartproxy.IRouteConfig,
|
|
450
|
+
routeId?: string,
|
|
451
|
+
): plugins.smartproxy.IRouteConfig {
|
|
452
|
+
const vpnCallback = this.getVpnClientIpsForRoute;
|
|
453
|
+
if (!vpnCallback) return route;
|
|
454
|
+
|
|
455
|
+
const dcRoute = route as IDcRouterRouteConfig;
|
|
456
|
+
if (!dcRoute.vpnOnly) return route;
|
|
457
|
+
|
|
458
|
+
const vpnEntries = vpnCallback(dcRoute, routeId);
|
|
459
|
+
const existingEntries = route.security?.ipAllowList || [];
|
|
460
|
+
return {
|
|
461
|
+
...route,
|
|
462
|
+
security: {
|
|
463
|
+
...route.security,
|
|
464
|
+
ipAllowList: [...existingEntries, ...vpnEntries],
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private isPersistedRuntimeRoute(storedRoute: IRoute): boolean {
|
|
470
|
+
const routeName = storedRoute.route.name || '';
|
|
471
|
+
const actionType = storedRoute.route.action?.type;
|
|
472
|
+
|
|
473
|
+
return (routeName.startsWith('dns-over-https-') && actionType === 'socket-handler')
|
|
474
|
+
|| (storedRoute.origin === 'dns' && actionType === 'socket-handler');
|
|
475
|
+
}
|
|
432
476
|
}
|
|
@@ -13,6 +13,10 @@ import type { IRoute } from '../../ts_interfaces/data/route-management.js';
|
|
|
13
13
|
export class TargetProfileManager {
|
|
14
14
|
private profiles = new Map<string, ITargetProfile>();
|
|
15
15
|
|
|
16
|
+
constructor(
|
|
17
|
+
private getAllRoutes?: () => Map<string, IRoute>,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
16
20
|
// =========================================================================
|
|
17
21
|
// Lifecycle
|
|
18
22
|
// =========================================================================
|
|
@@ -43,13 +47,14 @@ export class TargetProfileManager {
|
|
|
43
47
|
const id = plugins.uuid.v4();
|
|
44
48
|
const now = Date.now();
|
|
45
49
|
|
|
50
|
+
const routeRefs = this.normalizeRouteRefs(data.routeRefs);
|
|
46
51
|
const profile: ITargetProfile = {
|
|
47
52
|
id,
|
|
48
53
|
name: data.name,
|
|
49
54
|
description: data.description,
|
|
50
55
|
domains: data.domains,
|
|
51
56
|
targets: data.targets,
|
|
52
|
-
routeRefs
|
|
57
|
+
routeRefs,
|
|
53
58
|
createdAt: now,
|
|
54
59
|
updatedAt: now,
|
|
55
60
|
createdBy: data.createdBy,
|
|
@@ -70,11 +75,19 @@ export class TargetProfileManager {
|
|
|
70
75
|
throw new Error(`Target profile '${id}' not found`);
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
if (patch.name !== undefined && patch.name !== profile.name) {
|
|
79
|
+
for (const existing of this.profiles.values()) {
|
|
80
|
+
if (existing.id !== id && existing.name === patch.name) {
|
|
81
|
+
throw new Error(`Target profile with name '${patch.name}' already exists (id: ${existing.id})`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
73
86
|
if (patch.name !== undefined) profile.name = patch.name;
|
|
74
87
|
if (patch.description !== undefined) profile.description = patch.description;
|
|
75
88
|
if (patch.domains !== undefined) profile.domains = patch.domains;
|
|
76
89
|
if (patch.targets !== undefined) profile.targets = patch.targets;
|
|
77
|
-
if (patch.routeRefs !== undefined) profile.routeRefs = patch.routeRefs;
|
|
90
|
+
if (patch.routeRefs !== undefined) profile.routeRefs = this.normalizeRouteRefs(patch.routeRefs);
|
|
78
91
|
profile.updatedAt = Date.now();
|
|
79
92
|
|
|
80
93
|
await this.persistProfile(profile);
|
|
@@ -127,6 +140,29 @@ export class TargetProfileManager {
|
|
|
127
140
|
return this.profiles.get(id);
|
|
128
141
|
}
|
|
129
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Normalize stored route references to route IDs when they can be resolved
|
|
145
|
+
* uniquely against the current route registry.
|
|
146
|
+
*/
|
|
147
|
+
public async normalizeAllRouteRefs(): Promise<void> {
|
|
148
|
+
const allRoutes = this.getAllRoutes?.();
|
|
149
|
+
if (!allRoutes?.size) return;
|
|
150
|
+
|
|
151
|
+
for (const profile of this.profiles.values()) {
|
|
152
|
+
const normalizedRouteRefs = this.normalizeRouteRefsAgainstRoutes(
|
|
153
|
+
profile.routeRefs,
|
|
154
|
+
allRoutes,
|
|
155
|
+
'bestEffort',
|
|
156
|
+
);
|
|
157
|
+
if (this.sameStringArray(profile.routeRefs, normalizedRouteRefs)) continue;
|
|
158
|
+
|
|
159
|
+
profile.routeRefs = normalizedRouteRefs;
|
|
160
|
+
profile.updatedAt = Date.now();
|
|
161
|
+
await this.persistProfile(profile);
|
|
162
|
+
logger.log('info', `Normalized route refs for target profile '${profile.name}' (${profile.id})`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
130
166
|
public listProfiles(): ITargetProfile[] {
|
|
131
167
|
return [...this.profiles.values()];
|
|
132
168
|
}
|
|
@@ -178,9 +214,11 @@ export class TargetProfileManager {
|
|
|
178
214
|
route: IDcRouterRouteConfig,
|
|
179
215
|
routeId: string | undefined,
|
|
180
216
|
clients: VpnClientDoc[],
|
|
217
|
+
allRoutes: Map<string, IRoute> = new Map(),
|
|
181
218
|
): Array<string | { ip: string; domains: string[] }> {
|
|
182
219
|
const entries: Array<string | { ip: string; domains: string[] }> = [];
|
|
183
220
|
const routeDomains: string[] = (route.match as any)?.domains || [];
|
|
221
|
+
const routeNameIndex = this.buildRouteNameIndex(allRoutes);
|
|
184
222
|
|
|
185
223
|
for (const client of clients) {
|
|
186
224
|
if (!client.enabled || !client.assignedIp) continue;
|
|
@@ -194,7 +232,13 @@ export class TargetProfileManager {
|
|
|
194
232
|
const profile = this.profiles.get(profileId);
|
|
195
233
|
if (!profile) continue;
|
|
196
234
|
|
|
197
|
-
const matchResult = this.routeMatchesProfileDetailed(
|
|
235
|
+
const matchResult = this.routeMatchesProfileDetailed(
|
|
236
|
+
route,
|
|
237
|
+
routeId,
|
|
238
|
+
profile,
|
|
239
|
+
routeDomains,
|
|
240
|
+
routeNameIndex,
|
|
241
|
+
);
|
|
198
242
|
if (matchResult === 'full') {
|
|
199
243
|
fullAccess = true;
|
|
200
244
|
break; // No need to check more profiles
|
|
@@ -224,6 +268,7 @@ export class TargetProfileManager {
|
|
|
224
268
|
): { domains: string[]; targetIps: string[] } {
|
|
225
269
|
const domains = new Set<string>();
|
|
226
270
|
const targetIps = new Set<string>();
|
|
271
|
+
const routeNameIndex = this.buildRouteNameIndex(allRoutes);
|
|
227
272
|
|
|
228
273
|
// Collect all access specifiers from assigned profiles
|
|
229
274
|
for (const profileId of targetProfileIds) {
|
|
@@ -247,7 +292,12 @@ export class TargetProfileManager {
|
|
|
247
292
|
// Route references: scan all routes
|
|
248
293
|
for (const [routeId, route] of allRoutes) {
|
|
249
294
|
if (!route.enabled) continue;
|
|
250
|
-
if (this.routeMatchesProfile(
|
|
295
|
+
if (this.routeMatchesProfile(
|
|
296
|
+
route.route as IDcRouterRouteConfig,
|
|
297
|
+
routeId,
|
|
298
|
+
profile,
|
|
299
|
+
routeNameIndex,
|
|
300
|
+
)) {
|
|
251
301
|
const routeDomains = (route.route.match as any)?.domains;
|
|
252
302
|
if (Array.isArray(routeDomains)) {
|
|
253
303
|
for (const d of routeDomains) {
|
|
@@ -275,9 +325,16 @@ export class TargetProfileManager {
|
|
|
275
325
|
route: IDcRouterRouteConfig,
|
|
276
326
|
routeId: string | undefined,
|
|
277
327
|
profile: ITargetProfile,
|
|
328
|
+
routeNameIndex: Map<string, string[]>,
|
|
278
329
|
): boolean {
|
|
279
330
|
const routeDomains: string[] = (route.match as any)?.domains || [];
|
|
280
|
-
const result = this.routeMatchesProfileDetailed(
|
|
331
|
+
const result = this.routeMatchesProfileDetailed(
|
|
332
|
+
route,
|
|
333
|
+
routeId,
|
|
334
|
+
profile,
|
|
335
|
+
routeDomains,
|
|
336
|
+
routeNameIndex,
|
|
337
|
+
);
|
|
281
338
|
return result !== 'none';
|
|
282
339
|
}
|
|
283
340
|
|
|
@@ -294,11 +351,17 @@ export class TargetProfileManager {
|
|
|
294
351
|
routeId: string | undefined,
|
|
295
352
|
profile: ITargetProfile,
|
|
296
353
|
routeDomains: string[],
|
|
354
|
+
routeNameIndex: Map<string, string[]>,
|
|
297
355
|
): 'full' | { type: 'scoped'; domains: string[] } | 'none' {
|
|
298
356
|
// 1. Route reference match → full access
|
|
299
357
|
if (profile.routeRefs?.length) {
|
|
300
358
|
if (routeId && profile.routeRefs.includes(routeId)) return 'full';
|
|
301
|
-
if (route.name && profile.routeRefs.includes(route.name))
|
|
359
|
+
if (routeId && route.name && profile.routeRefs.includes(route.name)) {
|
|
360
|
+
const matchingRouteIds = routeNameIndex.get(route.name) || [];
|
|
361
|
+
if (matchingRouteIds.length === 1 && matchingRouteIds[0] === routeId) {
|
|
362
|
+
return 'full';
|
|
363
|
+
}
|
|
364
|
+
}
|
|
302
365
|
}
|
|
303
366
|
|
|
304
367
|
// 2. Domain match
|
|
@@ -362,6 +425,66 @@ export class TargetProfileManager {
|
|
|
362
425
|
return false;
|
|
363
426
|
}
|
|
364
427
|
|
|
428
|
+
private normalizeRouteRefs(routeRefs?: string[]): string[] | undefined {
|
|
429
|
+
const allRoutes = this.getAllRoutes?.() || new Map<string, IRoute>();
|
|
430
|
+
return this.normalizeRouteRefsAgainstRoutes(routeRefs, allRoutes, 'strict');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private normalizeRouteRefsAgainstRoutes(
|
|
434
|
+
routeRefs: string[] | undefined,
|
|
435
|
+
allRoutes: Map<string, IRoute>,
|
|
436
|
+
mode: 'strict' | 'bestEffort',
|
|
437
|
+
): string[] | undefined {
|
|
438
|
+
if (!routeRefs?.length) return undefined;
|
|
439
|
+
if (!allRoutes.size) return [...new Set(routeRefs)];
|
|
440
|
+
|
|
441
|
+
const routeNameIndex = this.buildRouteNameIndex(allRoutes);
|
|
442
|
+
const normalizedRefs = new Set<string>();
|
|
443
|
+
|
|
444
|
+
for (const routeRef of routeRefs) {
|
|
445
|
+
if (allRoutes.has(routeRef)) {
|
|
446
|
+
normalizedRefs.add(routeRef);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const matchingRouteIds = routeNameIndex.get(routeRef) || [];
|
|
451
|
+
if (matchingRouteIds.length === 1) {
|
|
452
|
+
normalizedRefs.add(matchingRouteIds[0]);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (mode === 'bestEffort') {
|
|
457
|
+
normalizedRefs.add(routeRef);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (matchingRouteIds.length > 1) {
|
|
462
|
+
throw new Error(`Route reference '${routeRef}' is ambiguous; use a route ID instead`);
|
|
463
|
+
}
|
|
464
|
+
throw new Error(`Route reference '${routeRef}' not found`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return [...normalizedRefs];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private buildRouteNameIndex(allRoutes: Map<string, IRoute>): Map<string, string[]> {
|
|
471
|
+
const routeNameIndex = new Map<string, string[]>();
|
|
472
|
+
for (const [routeId, route] of allRoutes) {
|
|
473
|
+
const routeName = route.route.name;
|
|
474
|
+
if (!routeName) continue;
|
|
475
|
+
const matchingRouteIds = routeNameIndex.get(routeName) || [];
|
|
476
|
+
matchingRouteIds.push(routeId);
|
|
477
|
+
routeNameIndex.set(routeName, matchingRouteIds);
|
|
478
|
+
}
|
|
479
|
+
return routeNameIndex;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private sameStringArray(left?: string[], right?: string[]): boolean {
|
|
483
|
+
if (!left?.length && !right?.length) return true;
|
|
484
|
+
if (!left || !right || left.length !== right.length) return false;
|
|
485
|
+
return left.every((value, index) => value === right[index]);
|
|
486
|
+
}
|
|
487
|
+
|
|
365
488
|
// =========================================================================
|
|
366
489
|
// Private: persistence
|
|
367
490
|
// =========================================================================
|
package/ts/dns/manager.dns.ts
CHANGED
|
@@ -97,8 +97,8 @@ export class DnsManager {
|
|
|
97
97
|
if (hasLegacyConfig) {
|
|
98
98
|
logger.log(
|
|
99
99
|
'warn',
|
|
100
|
-
'DnsManager: DB has DomainDoc entries — ignoring legacy dnsScopes/dnsRecords
|
|
101
|
-
'
|
|
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
102
|
);
|
|
103
103
|
}
|
|
104
104
|
return;
|
|
@@ -198,12 +198,11 @@ export class CertificateHandler {
|
|
|
198
198
|
try {
|
|
199
199
|
const rustStatus = await smartProxy.getCertificateStatus(info.routeNames[0]);
|
|
200
200
|
if (rustStatus) {
|
|
201
|
-
if (rustStatus.
|
|
202
|
-
|
|
203
|
-
if (rustStatus.issuedAt) issuedAt = rustStatus.issuedAt;
|
|
204
|
-
if (rustStatus.status === 'valid' || rustStatus.status === 'expired') {
|
|
205
|
-
status = rustStatus.status;
|
|
201
|
+
if (rustStatus.expiresAt > 0) {
|
|
202
|
+
expiryDate = new Date(rustStatus.expiresAt).toISOString();
|
|
206
203
|
}
|
|
204
|
+
if (rustStatus.source) issuer = rustStatus.source;
|
|
205
|
+
status = rustStatus.isValid ? 'valid' : 'expired';
|
|
207
206
|
}
|
|
208
207
|
} catch {
|
|
209
208
|
// Rust bridge may not support this command yet — ignore
|