@serve.zone/dcrouter 12.9.4 → 13.0.0
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 +1030 -923
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +5 -4
- package/dist_ts/classes.dcrouter.js +25 -49
- package/dist_ts/config/classes.reference-resolver.d.ts +7 -7
- package/dist_ts/config/classes.reference-resolver.js +27 -27
- package/dist_ts/config/classes.route-config-manager.d.ts +2 -2
- package/dist_ts/config/classes.route-config-manager.js +24 -16
- package/dist_ts/config/classes.target-profile-manager.d.ts +63 -0
- package/dist_ts/config/classes.target-profile-manager.js +295 -0
- package/dist_ts/config/index.d.ts +1 -0
- package/dist_ts/config/index.js +2 -1
- package/dist_ts/db/documents/{classes.security-profile.doc.d.ts → classes.source-profile.doc.d.ts} +4 -4
- package/dist_ts/db/documents/{classes.security-profile.doc.js → classes.source-profile.doc.js} +9 -9
- package/dist_ts/db/documents/classes.target-profile.doc.d.ts +17 -0
- package/dist_ts/db/documents/classes.target-profile.doc.js +124 -0
- package/dist_ts/db/documents/classes.vpn-client.doc.d.ts +1 -1
- package/dist_ts/db/documents/classes.vpn-client.doc.js +8 -8
- package/dist_ts/db/documents/index.d.ts +2 -1
- package/dist_ts/db/documents/index.js +3 -2
- package/dist_ts/opsserver/classes.opsserver.d.ts +2 -1
- package/dist_ts/opsserver/classes.opsserver.js +5 -3
- package/dist_ts/opsserver/handlers/index.d.ts +2 -1
- package/dist_ts/opsserver/handlers/index.js +3 -2
- package/dist_ts/opsserver/handlers/{security-profile.handler.d.ts → source-profile.handler.d.ts} +1 -1
- package/dist_ts/opsserver/handlers/{security-profile.handler.js → source-profile.handler.js} +20 -20
- package/dist_ts/opsserver/handlers/target-profile.handler.d.ts +10 -0
- package/dist_ts/opsserver/handlers/target-profile.handler.js +115 -0
- package/dist_ts/opsserver/handlers/vpn.handler.js +5 -5
- package/dist_ts/vpn/classes.vpn-manager.d.ts +6 -10
- package/dist_ts/vpn/classes.vpn-manager.js +11 -34
- package/dist_ts_interfaces/data/index.d.ts +1 -0
- package/dist_ts_interfaces/data/index.js +2 -1
- package/dist_ts_interfaces/data/remoteingress.d.ts +4 -15
- package/dist_ts_interfaces/data/route-management.d.ts +9 -6
- package/dist_ts_interfaces/data/target-profile.d.ts +28 -0
- package/dist_ts_interfaces/data/target-profile.js +2 -0
- package/dist_ts_interfaces/data/vpn.d.ts +2 -1
- package/dist_ts_interfaces/requests/index.d.ts +2 -1
- package/dist_ts_interfaces/requests/index.js +3 -2
- package/dist_ts_interfaces/requests/{security-profiles.d.ts → source-profiles.d.ts} +21 -21
- package/dist_ts_interfaces/requests/source-profiles.js +2 -0
- package/dist_ts_interfaces/requests/target-profiles.d.ts +103 -0
- package/dist_ts_interfaces/requests/target-profiles.js +2 -0
- package/dist_ts_interfaces/requests/vpn.d.ts +2 -2
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +36 -3
- package/dist_ts_web/appstate.js +127 -10
- package/dist_ts_web/elements/index.d.ts +2 -1
- package/dist_ts_web/elements/index.js +3 -2
- package/dist_ts_web/elements/ops-dashboard.js +10 -4
- package/dist_ts_web/elements/ops-view-routes.js +120 -10
- package/dist_ts_web/elements/{ops-view-securityprofiles.d.ts → ops-view-sourceprofiles.d.ts} +2 -2
- package/dist_ts_web/elements/{ops-view-securityprofiles.js → ops-view-sourceprofiles.js} +12 -12
- package/dist_ts_web/elements/ops-view-targetprofiles.d.ts +19 -0
- package/dist_ts_web/elements/ops-view-targetprofiles.js +412 -0
- package/dist_ts_web/elements/ops-view-vpn.js +13 -13
- package/dist_ts_web/router.d.ts +1 -1
- package/dist_ts_web/router.js +2 -2
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +33 -50
- package/ts/config/classes.reference-resolver.ts +34 -34
- package/ts/config/classes.route-config-manager.ts +21 -13
- package/ts/config/classes.target-profile-manager.ts +348 -0
- package/ts/config/index.ts +2 -1
- package/ts/db/documents/{classes.security-profile.doc.ts → classes.source-profile.doc.ts} +7 -7
- package/ts/db/documents/classes.target-profile.doc.ts +52 -0
- package/ts/db/documents/classes.vpn-client.doc.ts +1 -1
- package/ts/db/documents/index.ts +2 -1
- package/ts/opsserver/classes.opsserver.ts +4 -2
- package/ts/opsserver/handlers/index.ts +2 -1
- package/ts/opsserver/handlers/{security-profile.handler.ts → source-profile.handler.ts} +25 -25
- package/ts/opsserver/handlers/target-profile.handler.ts +155 -0
- package/ts/opsserver/handlers/vpn.handler.ts +4 -4
- package/ts/vpn/classes.vpn-manager.ts +14 -38
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +180 -17
- package/ts_web/elements/index.ts +2 -1
- package/ts_web/elements/ops-dashboard.ts +9 -3
- package/ts_web/elements/ops-view-routes.ts +118 -9
- package/ts_web/elements/{ops-view-securityprofiles.ts → ops-view-sourceprofiles.ts} +13 -13
- package/ts_web/elements/ops-view-targetprofiles.ts +379 -0
- package/ts_web/elements/ops-view-vpn.ts +12 -12
- package/ts_web/router.ts +1 -1
- package/dist_ts_interfaces/requests/security-profiles.js +0 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
import { logger } from '../logger.js';
|
|
3
|
-
import {
|
|
3
|
+
import { SourceProfileDoc, NetworkTargetDoc, StoredRouteDoc } from '../db/index.js';
|
|
4
4
|
import type {
|
|
5
|
-
|
|
5
|
+
ISourceProfile,
|
|
6
6
|
INetworkTarget,
|
|
7
7
|
IRouteMetadata,
|
|
8
8
|
IStoredRoute,
|
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
const MAX_INHERITANCE_DEPTH = 5;
|
|
13
13
|
|
|
14
14
|
export class ReferenceResolver {
|
|
15
|
-
private profiles = new Map<string,
|
|
15
|
+
private profiles = new Map<string, ISourceProfile>();
|
|
16
16
|
private targets = new Map<string, INetworkTarget>();
|
|
17
17
|
|
|
18
18
|
// =========================================================================
|
|
@@ -38,7 +38,7 @@ export class ReferenceResolver {
|
|
|
38
38
|
const id = plugins.uuid.v4();
|
|
39
39
|
const now = Date.now();
|
|
40
40
|
|
|
41
|
-
const profile:
|
|
41
|
+
const profile: ISourceProfile = {
|
|
42
42
|
id,
|
|
43
43
|
name: data.name,
|
|
44
44
|
description: data.description,
|
|
@@ -51,17 +51,17 @@ export class ReferenceResolver {
|
|
|
51
51
|
|
|
52
52
|
this.profiles.set(id, profile);
|
|
53
53
|
await this.persistProfile(profile);
|
|
54
|
-
logger.log('info', `Created
|
|
54
|
+
logger.log('info', `Created source profile '${profile.name}' (${id})`);
|
|
55
55
|
return id;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
public async updateProfile(
|
|
59
59
|
id: string,
|
|
60
|
-
patch: Partial<Omit<
|
|
60
|
+
patch: Partial<Omit<ISourceProfile, 'id' | 'createdAt' | 'createdBy'>>,
|
|
61
61
|
): Promise<{ affectedRouteIds: string[] }> {
|
|
62
62
|
const profile = this.profiles.get(id);
|
|
63
63
|
if (!profile) {
|
|
64
|
-
throw new Error(`
|
|
64
|
+
throw new Error(`Source profile '${id}' not found`);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
if (patch.name !== undefined) profile.name = patch.name;
|
|
@@ -71,7 +71,7 @@ export class ReferenceResolver {
|
|
|
71
71
|
profile.updatedAt = Date.now();
|
|
72
72
|
|
|
73
73
|
await this.persistProfile(profile);
|
|
74
|
-
logger.log('info', `Updated
|
|
74
|
+
logger.log('info', `Updated source profile '${profile.name}' (${id})`);
|
|
75
75
|
|
|
76
76
|
// Find routes referencing this profile
|
|
77
77
|
const affectedRouteIds = await this.findRoutesByProfileRef(id);
|
|
@@ -85,7 +85,7 @@ export class ReferenceResolver {
|
|
|
85
85
|
): Promise<{ success: boolean; message?: string }> {
|
|
86
86
|
const profile = this.profiles.get(id);
|
|
87
87
|
if (!profile) {
|
|
88
|
-
return { success: false, message: `
|
|
88
|
+
return { success: false, message: `Source profile '${id}' not found` };
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// Check usage
|
|
@@ -101,7 +101,7 @@ export class ReferenceResolver {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// Delete from DB
|
|
104
|
-
const doc = await
|
|
104
|
+
const doc = await SourceProfileDoc.findById(id);
|
|
105
105
|
if (doc) await doc.delete();
|
|
106
106
|
this.profiles.delete(id);
|
|
107
107
|
|
|
@@ -110,24 +110,24 @@ export class ReferenceResolver {
|
|
|
110
110
|
await this.clearProfileRefsOnRoutes(affectedIds);
|
|
111
111
|
logger.log('warn', `Force-deleted profile '${profile.name}'; cleared refs on ${affectedIds.length} route(s)`);
|
|
112
112
|
} else {
|
|
113
|
-
logger.log('info', `Deleted
|
|
113
|
+
logger.log('info', `Deleted source profile '${profile.name}' (${id})`);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
return { success: true };
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
public getProfile(id: string):
|
|
119
|
+
public getProfile(id: string): ISourceProfile | undefined {
|
|
120
120
|
return this.profiles.get(id);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
public getProfileByName(name: string):
|
|
123
|
+
public getProfileByName(name: string): ISourceProfile | undefined {
|
|
124
124
|
for (const profile of this.profiles.values()) {
|
|
125
125
|
if (profile.name === name) return profile;
|
|
126
126
|
}
|
|
127
127
|
return undefined;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
public listProfiles():
|
|
130
|
+
public listProfiles(): ISourceProfile[] {
|
|
131
131
|
return [...this.profiles.values()];
|
|
132
132
|
}
|
|
133
133
|
|
|
@@ -137,7 +137,7 @@ export class ReferenceResolver {
|
|
|
137
137
|
usage.set(profile.id, []);
|
|
138
138
|
}
|
|
139
139
|
for (const [routeId, stored] of storedRoutes) {
|
|
140
|
-
const ref = stored.metadata?.
|
|
140
|
+
const ref = stored.metadata?.sourceProfileRef;
|
|
141
141
|
if (ref && usage.has(ref)) {
|
|
142
142
|
usage.get(ref)!.push({ id: routeId, routeName: stored.route.name || routeId });
|
|
143
143
|
}
|
|
@@ -151,7 +151,7 @@ export class ReferenceResolver {
|
|
|
151
151
|
): Array<{ id: string; routeName: string }> {
|
|
152
152
|
const routes: Array<{ id: string; routeName: string }> = [];
|
|
153
153
|
for (const [routeId, stored] of storedRoutes) {
|
|
154
|
-
if (stored.metadata?.
|
|
154
|
+
if (stored.metadata?.sourceProfileRef === profileId) {
|
|
155
155
|
routes.push({ id: routeId, routeName: stored.route.name || routeId });
|
|
156
156
|
}
|
|
157
157
|
}
|
|
@@ -280,7 +280,7 @@ export class ReferenceResolver {
|
|
|
280
280
|
|
|
281
281
|
/**
|
|
282
282
|
* Resolve references for a single route.
|
|
283
|
-
* Materializes
|
|
283
|
+
* Materializes source profile and/or network target into the route's fields.
|
|
284
284
|
* Returns the resolved route and updated metadata.
|
|
285
285
|
*/
|
|
286
286
|
public resolveRoute(
|
|
@@ -289,19 +289,19 @@ export class ReferenceResolver {
|
|
|
289
289
|
): { route: plugins.smartproxy.IRouteConfig; metadata: IRouteMetadata } {
|
|
290
290
|
const resolvedMetadata: IRouteMetadata = { ...metadata };
|
|
291
291
|
|
|
292
|
-
if (resolvedMetadata.
|
|
293
|
-
const resolvedSecurity = this.
|
|
292
|
+
if (resolvedMetadata.sourceProfileRef) {
|
|
293
|
+
const resolvedSecurity = this.resolveSourceProfile(resolvedMetadata.sourceProfileRef);
|
|
294
294
|
if (resolvedSecurity) {
|
|
295
|
-
const profile = this.profiles.get(resolvedMetadata.
|
|
295
|
+
const profile = this.profiles.get(resolvedMetadata.sourceProfileRef);
|
|
296
296
|
// Merge: profile provides base, route's inline values override
|
|
297
297
|
route = {
|
|
298
298
|
...route,
|
|
299
299
|
security: this.mergeSecurityFields(resolvedSecurity, route.security),
|
|
300
300
|
};
|
|
301
|
-
resolvedMetadata.
|
|
301
|
+
resolvedMetadata.sourceProfileName = profile?.name;
|
|
302
302
|
resolvedMetadata.lastResolvedAt = Date.now();
|
|
303
303
|
} else {
|
|
304
|
-
logger.log('warn', `
|
|
304
|
+
logger.log('warn', `Source profile '${resolvedMetadata.sourceProfileRef}' not found during resolution`);
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
307
|
|
|
@@ -335,7 +335,7 @@ export class ReferenceResolver {
|
|
|
335
335
|
public async findRoutesByProfileRef(profileId: string): Promise<string[]> {
|
|
336
336
|
const docs = await StoredRouteDoc.findAll();
|
|
337
337
|
return docs
|
|
338
|
-
.filter((doc) => doc.metadata?.
|
|
338
|
+
.filter((doc) => doc.metadata?.sourceProfileRef === profileId)
|
|
339
339
|
.map((doc) => doc.id);
|
|
340
340
|
}
|
|
341
341
|
|
|
@@ -349,7 +349,7 @@ export class ReferenceResolver {
|
|
|
349
349
|
public findRoutesByProfileRefSync(profileId: string, storedRoutes: Map<string, IStoredRoute>): string[] {
|
|
350
350
|
const ids: string[] = [];
|
|
351
351
|
for (const [routeId, stored] of storedRoutes) {
|
|
352
|
-
if (stored.metadata?.
|
|
352
|
+
if (stored.metadata?.sourceProfileRef === profileId) {
|
|
353
353
|
ids.push(routeId);
|
|
354
354
|
}
|
|
355
355
|
}
|
|
@@ -367,10 +367,10 @@ export class ReferenceResolver {
|
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
// =========================================================================
|
|
370
|
-
// Private:
|
|
370
|
+
// Private: source profile resolution with inheritance
|
|
371
371
|
// =========================================================================
|
|
372
372
|
|
|
373
|
-
private
|
|
373
|
+
private resolveSourceProfile(
|
|
374
374
|
profileId: string,
|
|
375
375
|
visited: Set<string> = new Set(),
|
|
376
376
|
depth: number = 0,
|
|
@@ -396,7 +396,7 @@ export class ReferenceResolver {
|
|
|
396
396
|
// Resolve parent profiles first (top-down, later overrides earlier)
|
|
397
397
|
if (profile.extendsProfiles?.length) {
|
|
398
398
|
for (const parentId of profile.extendsProfiles) {
|
|
399
|
-
const parentSecurity = this.
|
|
399
|
+
const parentSecurity = this.resolveSourceProfile(parentId, new Set(visited), depth + 1);
|
|
400
400
|
if (parentSecurity) {
|
|
401
401
|
baseSecurity = this.mergeSecurityFields(baseSecurity, parentSecurity);
|
|
402
402
|
}
|
|
@@ -453,7 +453,7 @@ export class ReferenceResolver {
|
|
|
453
453
|
// =========================================================================
|
|
454
454
|
|
|
455
455
|
private async loadProfiles(): Promise<void> {
|
|
456
|
-
const docs = await
|
|
456
|
+
const docs = await SourceProfileDoc.findAll();
|
|
457
457
|
for (const doc of docs) {
|
|
458
458
|
if (doc.id) {
|
|
459
459
|
this.profiles.set(doc.id, {
|
|
@@ -469,7 +469,7 @@ export class ReferenceResolver {
|
|
|
469
469
|
}
|
|
470
470
|
}
|
|
471
471
|
if (this.profiles.size > 0) {
|
|
472
|
-
logger.log('info', `Loaded ${this.profiles.size}
|
|
472
|
+
logger.log('info', `Loaded ${this.profiles.size} source profile(s) from storage`);
|
|
473
473
|
}
|
|
474
474
|
}
|
|
475
475
|
|
|
@@ -494,8 +494,8 @@ export class ReferenceResolver {
|
|
|
494
494
|
}
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
-
private async persistProfile(profile:
|
|
498
|
-
const existingDoc = await
|
|
497
|
+
private async persistProfile(profile: ISourceProfile): Promise<void> {
|
|
498
|
+
const existingDoc = await SourceProfileDoc.findById(profile.id);
|
|
499
499
|
if (existingDoc) {
|
|
500
500
|
existingDoc.name = profile.name;
|
|
501
501
|
existingDoc.description = profile.description;
|
|
@@ -504,7 +504,7 @@ export class ReferenceResolver {
|
|
|
504
504
|
existingDoc.updatedAt = profile.updatedAt;
|
|
505
505
|
await existingDoc.save();
|
|
506
506
|
} else {
|
|
507
|
-
const doc = new
|
|
507
|
+
const doc = new SourceProfileDoc();
|
|
508
508
|
doc.id = profile.id;
|
|
509
509
|
doc.name = profile.name;
|
|
510
510
|
doc.description = profile.description;
|
|
@@ -550,8 +550,8 @@ export class ReferenceResolver {
|
|
|
550
550
|
if (doc?.metadata) {
|
|
551
551
|
doc.metadata = {
|
|
552
552
|
...doc.metadata,
|
|
553
|
-
|
|
554
|
-
|
|
553
|
+
sourceProfileRef: undefined,
|
|
554
|
+
sourceProfileName: undefined,
|
|
555
555
|
};
|
|
556
556
|
doc.updatedAt = Date.now();
|
|
557
557
|
await doc.save();
|
|
@@ -21,7 +21,7 @@ export class RouteConfigManager {
|
|
|
21
21
|
private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
|
|
22
22
|
private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
|
|
23
23
|
private getHttp3Config?: () => IHttp3Config | undefined,
|
|
24
|
-
private
|
|
24
|
+
private getVpnClientIpsForRoute?: (route: IDcRouterRouteConfig, routeId?: string) => string[],
|
|
25
25
|
private referenceResolver?: ReferenceResolver,
|
|
26
26
|
private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void,
|
|
27
27
|
) {}
|
|
@@ -132,7 +132,18 @@ export class RouteConfigManager {
|
|
|
132
132
|
if (!stored) return false;
|
|
133
133
|
|
|
134
134
|
if (patch.route) {
|
|
135
|
-
|
|
135
|
+
const mergedAction = patch.route.action
|
|
136
|
+
? { ...stored.route.action, ...patch.route.action }
|
|
137
|
+
: stored.route.action;
|
|
138
|
+
// Handle explicit null to remove nested action properties (e.g., tls: null)
|
|
139
|
+
if (patch.route.action) {
|
|
140
|
+
for (const [key, val] of Object.entries(patch.route.action)) {
|
|
141
|
+
if (val === null) {
|
|
142
|
+
delete (mergedAction as any)[key];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
stored.route = { ...stored.route, ...patch.route, action: mergedAction } as IDcRouterRouteConfig;
|
|
136
147
|
}
|
|
137
148
|
if (patch.enabled !== undefined) {
|
|
138
149
|
stored.enabled = patch.enabled;
|
|
@@ -352,22 +363,19 @@ export class RouteConfigManager {
|
|
|
352
363
|
const enabledRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
353
364
|
|
|
354
365
|
const http3Config = this.getHttp3Config?.();
|
|
355
|
-
const
|
|
366
|
+
const vpnCallback = this.getVpnClientIpsForRoute;
|
|
356
367
|
|
|
357
|
-
// Helper: inject VPN security into a route
|
|
358
|
-
const injectVpn = (route: plugins.smartproxy.IRouteConfig): plugins.smartproxy.IRouteConfig => {
|
|
359
|
-
if (!
|
|
368
|
+
// Helper: inject VPN security into a vpnOnly route
|
|
369
|
+
const injectVpn = (route: plugins.smartproxy.IRouteConfig, routeId?: string): plugins.smartproxy.IRouteConfig => {
|
|
370
|
+
if (!vpnCallback) return route;
|
|
360
371
|
const dcRoute = route as IDcRouterRouteConfig;
|
|
361
|
-
if (!dcRoute.
|
|
362
|
-
const allowList =
|
|
363
|
-
const mandatory = dcRoute.vpn.mandatory === true; // defaults to false
|
|
372
|
+
if (!dcRoute.vpnOnly) return route;
|
|
373
|
+
const allowList = vpnCallback(dcRoute, routeId);
|
|
364
374
|
return {
|
|
365
375
|
...route,
|
|
366
376
|
security: {
|
|
367
377
|
...route.security,
|
|
368
|
-
ipAllowList:
|
|
369
|
-
? allowList
|
|
370
|
-
: [...(route.security?.ipAllowList || []), ...allowList],
|
|
378
|
+
ipAllowList: allowList,
|
|
371
379
|
},
|
|
372
380
|
};
|
|
373
381
|
};
|
|
@@ -389,7 +397,7 @@ export class RouteConfigManager {
|
|
|
389
397
|
if (http3Config?.enabled !== false) {
|
|
390
398
|
route = augmentRouteWithHttp3(route, { enabled: true, ...http3Config });
|
|
391
399
|
}
|
|
392
|
-
enabledRoutes.push(injectVpn(route));
|
|
400
|
+
enabledRoutes.push(injectVpn(route, stored.id));
|
|
393
401
|
}
|
|
394
402
|
}
|
|
395
403
|
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
3
|
+
import { TargetProfileDoc, VpnClientDoc } from '../db/index.js';
|
|
4
|
+
import type { ITargetProfile, ITargetProfileTarget } from '../../ts_interfaces/data/target-profile.js';
|
|
5
|
+
import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
|
|
6
|
+
import type { IStoredRoute } from '../../ts_interfaces/data/route-management.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Manages TargetProfiles (target-side: what can be accessed).
|
|
10
|
+
* TargetProfiles define what resources a VPN client can reach:
|
|
11
|
+
* domains, specific IP:port targets, and/or direct route references.
|
|
12
|
+
*/
|
|
13
|
+
export class TargetProfileManager {
|
|
14
|
+
private profiles = new Map<string, ITargetProfile>();
|
|
15
|
+
|
|
16
|
+
// =========================================================================
|
|
17
|
+
// Lifecycle
|
|
18
|
+
// =========================================================================
|
|
19
|
+
|
|
20
|
+
public async initialize(): Promise<void> {
|
|
21
|
+
await this.loadProfiles();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// =========================================================================
|
|
25
|
+
// CRUD
|
|
26
|
+
// =========================================================================
|
|
27
|
+
|
|
28
|
+
public async createProfile(data: {
|
|
29
|
+
name: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
domains?: string[];
|
|
32
|
+
targets?: ITargetProfileTarget[];
|
|
33
|
+
routeRefs?: string[];
|
|
34
|
+
createdBy: string;
|
|
35
|
+
}): Promise<string> {
|
|
36
|
+
const id = plugins.uuid.v4();
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
|
|
39
|
+
const profile: ITargetProfile = {
|
|
40
|
+
id,
|
|
41
|
+
name: data.name,
|
|
42
|
+
description: data.description,
|
|
43
|
+
domains: data.domains,
|
|
44
|
+
targets: data.targets,
|
|
45
|
+
routeRefs: data.routeRefs,
|
|
46
|
+
createdAt: now,
|
|
47
|
+
updatedAt: now,
|
|
48
|
+
createdBy: data.createdBy,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
this.profiles.set(id, profile);
|
|
52
|
+
await this.persistProfile(profile);
|
|
53
|
+
logger.log('info', `Created target profile '${profile.name}' (${id})`);
|
|
54
|
+
return id;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public async updateProfile(
|
|
58
|
+
id: string,
|
|
59
|
+
patch: Partial<Omit<ITargetProfile, 'id' | 'createdAt' | 'createdBy'>>,
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
const profile = this.profiles.get(id);
|
|
62
|
+
if (!profile) {
|
|
63
|
+
throw new Error(`Target profile '${id}' not found`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (patch.name !== undefined) profile.name = patch.name;
|
|
67
|
+
if (patch.description !== undefined) profile.description = patch.description;
|
|
68
|
+
if (patch.domains !== undefined) profile.domains = patch.domains;
|
|
69
|
+
if (patch.targets !== undefined) profile.targets = patch.targets;
|
|
70
|
+
if (patch.routeRefs !== undefined) profile.routeRefs = patch.routeRefs;
|
|
71
|
+
profile.updatedAt = Date.now();
|
|
72
|
+
|
|
73
|
+
await this.persistProfile(profile);
|
|
74
|
+
logger.log('info', `Updated target profile '${profile.name}' (${id})`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public async deleteProfile(
|
|
78
|
+
id: string,
|
|
79
|
+
force?: boolean,
|
|
80
|
+
): Promise<{ success: boolean; message?: string }> {
|
|
81
|
+
const profile = this.profiles.get(id);
|
|
82
|
+
if (!profile) {
|
|
83
|
+
return { success: false, message: `Target profile '${id}' not found` };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check if any VPN clients reference this profile
|
|
87
|
+
const clients = await VpnClientDoc.findAll();
|
|
88
|
+
const referencingClients = clients.filter(
|
|
89
|
+
(c) => c.targetProfileIds?.includes(id),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (referencingClients.length > 0 && !force) {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
message: `Profile '${profile.name}' is in use by ${referencingClients.length} VPN client(s). Use force=true to delete.`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Delete from DB
|
|
100
|
+
const doc = await TargetProfileDoc.findById(id);
|
|
101
|
+
if (doc) await doc.delete();
|
|
102
|
+
this.profiles.delete(id);
|
|
103
|
+
|
|
104
|
+
if (referencingClients.length > 0) {
|
|
105
|
+
// Remove profile ref from clients
|
|
106
|
+
for (const client of referencingClients) {
|
|
107
|
+
client.targetProfileIds = client.targetProfileIds?.filter((pid) => pid !== id);
|
|
108
|
+
client.updatedAt = Date.now();
|
|
109
|
+
await client.save();
|
|
110
|
+
}
|
|
111
|
+
logger.log('warn', `Force-deleted target profile '${profile.name}'; removed refs from ${referencingClients.length} client(s)`);
|
|
112
|
+
} else {
|
|
113
|
+
logger.log('info', `Deleted target profile '${profile.name}' (${id})`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { success: true };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public getProfile(id: string): ITargetProfile | undefined {
|
|
120
|
+
return this.profiles.get(id);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public listProfiles(): ITargetProfile[] {
|
|
124
|
+
return [...this.profiles.values()];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get which VPN clients reference a target profile.
|
|
129
|
+
*/
|
|
130
|
+
public async getProfileUsage(profileId: string): Promise<Array<{ clientId: string; description?: string }>> {
|
|
131
|
+
const clients = await VpnClientDoc.findAll();
|
|
132
|
+
return clients
|
|
133
|
+
.filter((c) => c.targetProfileIds?.includes(profileId))
|
|
134
|
+
.map((c) => ({ clientId: c.clientId, description: c.description }));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// =========================================================================
|
|
138
|
+
// Core matching: route → client IPs
|
|
139
|
+
// =========================================================================
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* For a vpnOnly route, find all enabled VPN clients whose assigned TargetProfile
|
|
143
|
+
* matches the route. Returns their assigned IPs for injection into ipAllowList.
|
|
144
|
+
*/
|
|
145
|
+
public getMatchingClientIps(
|
|
146
|
+
route: IDcRouterRouteConfig,
|
|
147
|
+
routeId: string | undefined,
|
|
148
|
+
clients: VpnClientDoc[],
|
|
149
|
+
): string[] {
|
|
150
|
+
const ips: string[] = [];
|
|
151
|
+
|
|
152
|
+
for (const client of clients) {
|
|
153
|
+
if (!client.enabled || !client.assignedIp) continue;
|
|
154
|
+
if (!client.targetProfileIds?.length) continue;
|
|
155
|
+
|
|
156
|
+
// Check if any of the client's profiles match this route
|
|
157
|
+
const matches = client.targetProfileIds.some((profileId) => {
|
|
158
|
+
const profile = this.profiles.get(profileId);
|
|
159
|
+
if (!profile) return false;
|
|
160
|
+
return this.routeMatchesProfile(route, routeId, profile);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (matches) {
|
|
164
|
+
ips.push(client.assignedIp);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return ips;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* For a given client (by its targetProfileIds), compute the set of
|
|
173
|
+
* domains and target IPs it can access. Used for WireGuard AllowedIPs.
|
|
174
|
+
*/
|
|
175
|
+
public getClientAccessSpec(
|
|
176
|
+
targetProfileIds: string[],
|
|
177
|
+
allRoutes: IDcRouterRouteConfig[],
|
|
178
|
+
storedRoutes: Map<string, IStoredRoute>,
|
|
179
|
+
): { domains: string[]; targetIps: string[] } {
|
|
180
|
+
const domains = new Set<string>();
|
|
181
|
+
const targetIps = new Set<string>();
|
|
182
|
+
|
|
183
|
+
// Collect all access specifiers from assigned profiles
|
|
184
|
+
for (const profileId of targetProfileIds) {
|
|
185
|
+
const profile = this.profiles.get(profileId);
|
|
186
|
+
if (!profile) continue;
|
|
187
|
+
|
|
188
|
+
// Direct domain entries
|
|
189
|
+
if (profile.domains?.length) {
|
|
190
|
+
for (const d of profile.domains) {
|
|
191
|
+
domains.add(d);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Direct target IP entries
|
|
196
|
+
if (profile.targets?.length) {
|
|
197
|
+
for (const t of profile.targets) {
|
|
198
|
+
targetIps.add(t.host);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Route references: scan constructor routes
|
|
203
|
+
for (const route of allRoutes) {
|
|
204
|
+
if (this.routeMatchesProfile(route as IDcRouterRouteConfig, undefined, profile)) {
|
|
205
|
+
const routeDomains = (route.match as any)?.domains;
|
|
206
|
+
if (Array.isArray(routeDomains)) {
|
|
207
|
+
for (const d of routeDomains) {
|
|
208
|
+
domains.add(d);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Route references: scan stored routes
|
|
215
|
+
for (const [storedId, stored] of storedRoutes) {
|
|
216
|
+
if (!stored.enabled) continue;
|
|
217
|
+
if (this.routeMatchesProfile(stored.route as IDcRouterRouteConfig, storedId, profile)) {
|
|
218
|
+
const routeDomains = (stored.route.match as any)?.domains;
|
|
219
|
+
if (Array.isArray(routeDomains)) {
|
|
220
|
+
for (const d of routeDomains) {
|
|
221
|
+
domains.add(d);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
domains: [...domains],
|
|
230
|
+
targetIps: [...targetIps],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// =========================================================================
|
|
235
|
+
// Private: matching logic
|
|
236
|
+
// =========================================================================
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Check if a route matches a profile. A profile matches if ANY condition is true:
|
|
240
|
+
* 1. Profile's routeRefs contains the route's name or stored route id
|
|
241
|
+
* 2. Profile's domains overlaps with route.match.domains (wildcard matching)
|
|
242
|
+
* 3. Profile's targets overlaps with route.action.targets (host + port match)
|
|
243
|
+
*/
|
|
244
|
+
private routeMatchesProfile(
|
|
245
|
+
route: IDcRouterRouteConfig,
|
|
246
|
+
routeId: string | undefined,
|
|
247
|
+
profile: ITargetProfile,
|
|
248
|
+
): boolean {
|
|
249
|
+
// 1. Route reference match
|
|
250
|
+
if (profile.routeRefs?.length) {
|
|
251
|
+
if (routeId && profile.routeRefs.includes(routeId)) return true;
|
|
252
|
+
if (route.name && profile.routeRefs.includes(route.name)) return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 2. Domain match
|
|
256
|
+
if (profile.domains?.length) {
|
|
257
|
+
const routeDomains: string[] = (route.match as any)?.domains || [];
|
|
258
|
+
for (const profileDomain of profile.domains) {
|
|
259
|
+
for (const routeDomain of routeDomains) {
|
|
260
|
+
if (this.domainMatchesPattern(routeDomain, profileDomain)) return true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 3. Target match (host + port)
|
|
266
|
+
if (profile.targets?.length) {
|
|
267
|
+
const routeTargets = (route.action as any)?.targets;
|
|
268
|
+
if (Array.isArray(routeTargets)) {
|
|
269
|
+
for (const profileTarget of profile.targets) {
|
|
270
|
+
for (const routeTarget of routeTargets) {
|
|
271
|
+
const routeHost = routeTarget.host;
|
|
272
|
+
const routePort = routeTarget.port;
|
|
273
|
+
if (routeHost === profileTarget.host && routePort === profileTarget.port) {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Check if a domain matches a pattern.
|
|
286
|
+
* - '*.example.com' matches 'sub.example.com', 'a.b.example.com'
|
|
287
|
+
* - 'example.com' matches only 'example.com'
|
|
288
|
+
*/
|
|
289
|
+
private domainMatchesPattern(domain: string, pattern: string): boolean {
|
|
290
|
+
if (pattern === domain) return true;
|
|
291
|
+
if (pattern.startsWith('*.')) {
|
|
292
|
+
const suffix = pattern.slice(1); // '.example.com'
|
|
293
|
+
return domain.endsWith(suffix) && domain.length > suffix.length;
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// =========================================================================
|
|
299
|
+
// Private: persistence
|
|
300
|
+
// =========================================================================
|
|
301
|
+
|
|
302
|
+
private async loadProfiles(): Promise<void> {
|
|
303
|
+
const docs = await TargetProfileDoc.findAll();
|
|
304
|
+
for (const doc of docs) {
|
|
305
|
+
if (doc.id) {
|
|
306
|
+
this.profiles.set(doc.id, {
|
|
307
|
+
id: doc.id,
|
|
308
|
+
name: doc.name,
|
|
309
|
+
description: doc.description,
|
|
310
|
+
domains: doc.domains,
|
|
311
|
+
targets: doc.targets,
|
|
312
|
+
routeRefs: doc.routeRefs,
|
|
313
|
+
createdAt: doc.createdAt,
|
|
314
|
+
updatedAt: doc.updatedAt,
|
|
315
|
+
createdBy: doc.createdBy,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (this.profiles.size > 0) {
|
|
320
|
+
logger.log('info', `Loaded ${this.profiles.size} target profile(s) from storage`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private async persistProfile(profile: ITargetProfile): Promise<void> {
|
|
325
|
+
const existingDoc = await TargetProfileDoc.findById(profile.id);
|
|
326
|
+
if (existingDoc) {
|
|
327
|
+
existingDoc.name = profile.name;
|
|
328
|
+
existingDoc.description = profile.description;
|
|
329
|
+
existingDoc.domains = profile.domains;
|
|
330
|
+
existingDoc.targets = profile.targets;
|
|
331
|
+
existingDoc.routeRefs = profile.routeRefs;
|
|
332
|
+
existingDoc.updatedAt = profile.updatedAt;
|
|
333
|
+
await existingDoc.save();
|
|
334
|
+
} else {
|
|
335
|
+
const doc = new TargetProfileDoc();
|
|
336
|
+
doc.id = profile.id;
|
|
337
|
+
doc.name = profile.name;
|
|
338
|
+
doc.description = profile.description;
|
|
339
|
+
doc.domains = profile.domains;
|
|
340
|
+
doc.targets = profile.targets;
|
|
341
|
+
doc.routeRefs = profile.routeRefs;
|
|
342
|
+
doc.createdAt = profile.createdAt;
|
|
343
|
+
doc.updatedAt = profile.updatedAt;
|
|
344
|
+
doc.createdBy = profile.createdBy;
|
|
345
|
+
await doc.save();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
package/ts/config/index.ts
CHANGED
|
@@ -3,4 +3,5 @@ export * from './validator.js';
|
|
|
3
3
|
export { RouteConfigManager } from './classes.route-config-manager.js';
|
|
4
4
|
export { ApiTokenManager } from './classes.api-token-manager.js';
|
|
5
5
|
export { ReferenceResolver } from './classes.reference-resolver.js';
|
|
6
|
-
export { DbSeeder } from './classes.db-seeder.js';
|
|
6
|
+
export { DbSeeder } from './classes.db-seeder.js';
|
|
7
|
+
export { TargetProfileManager } from './classes.target-profile-manager.js';
|