@serve.zone/dcrouter 12.0.0 → 12.2.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 +1179 -1004
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +17 -1
- package/dist_ts/classes.dcrouter.js +16 -3
- package/dist_ts/config/classes.db-seeder.d.ts +25 -0
- package/dist_ts/config/classes.db-seeder.js +69 -0
- package/dist_ts/config/classes.reference-resolver.d.ts +80 -0
- package/dist_ts/config/classes.reference-resolver.js +482 -0
- package/dist_ts/config/classes.route-config-manager.d.ts +13 -3
- package/dist_ts/config/classes.route-config-manager.js +53 -3
- package/dist_ts/config/index.d.ts +2 -0
- package/dist_ts/config/index.js +3 -1
- package/dist_ts/db/documents/classes.network-target.doc.d.ts +15 -0
- package/dist_ts/db/documents/classes.network-target.doc.js +118 -0
- package/dist_ts/db/documents/classes.security-profile.doc.d.ts +16 -0
- package/dist_ts/db/documents/classes.security-profile.doc.js +118 -0
- package/dist_ts/db/documents/classes.stored-route.doc.d.ts +2 -0
- package/dist_ts/db/documents/classes.stored-route.doc.js +8 -2
- package/dist_ts/db/documents/classes.vpn-client.doc.d.ts +8 -0
- package/dist_ts/db/documents/classes.vpn-client.doc.js +50 -2
- package/dist_ts/db/documents/index.d.ts +2 -0
- package/dist_ts/db/documents/index.js +3 -1
- package/dist_ts/opsserver/classes.opsserver.d.ts +2 -0
- package/dist_ts/opsserver/classes.opsserver.js +5 -1
- package/dist_ts/opsserver/handlers/index.d.ts +2 -0
- package/dist_ts/opsserver/handlers/index.js +3 -1
- package/dist_ts/opsserver/handlers/network-target.handler.d.ts +10 -0
- package/dist_ts/opsserver/handlers/network-target.handler.js +117 -0
- package/dist_ts/opsserver/handlers/route-management.handler.js +3 -2
- package/dist_ts/opsserver/handlers/security-profile.handler.d.ts +10 -0
- package/dist_ts/opsserver/handlers/security-profile.handler.js +119 -0
- package/dist_ts/opsserver/handlers/vpn.handler.js +35 -1
- package/dist_ts/vpn/classes.vpn-manager.d.ts +33 -0
- package/dist_ts/vpn/classes.vpn-manager.js +122 -7
- package/dist_ts_interfaces/data/route-management.d.ts +48 -1
- package/dist_ts_interfaces/data/vpn.d.ts +8 -0
- package/dist_ts_interfaces/requests/index.d.ts +2 -0
- package/dist_ts_interfaces/requests/index.js +3 -1
- package/dist_ts_interfaces/requests/network-targets.d.ts +102 -0
- package/dist_ts_interfaces/requests/network-targets.js +2 -0
- package/dist_ts_interfaces/requests/route-management.d.ts +3 -1
- package/dist_ts_interfaces/requests/security-profiles.d.ts +102 -0
- package/dist_ts_interfaces/requests/security-profiles.js +2 -0
- package/dist_ts_interfaces/requests/vpn.d.ts +16 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +59 -0
- package/dist_ts_web/appstate.js +192 -2
- package/dist_ts_web/elements/index.d.ts +2 -0
- package/dist_ts_web/elements/index.js +3 -1
- package/dist_ts_web/elements/ops-dashboard.js +13 -1
- package/dist_ts_web/elements/ops-view-networktargets.d.ts +17 -0
- package/dist_ts_web/elements/ops-view-networktargets.js +246 -0
- package/dist_ts_web/elements/ops-view-securityprofiles.d.ts +17 -0
- package/dist_ts_web/elements/ops-view-securityprofiles.js +275 -0
- package/dist_ts_web/elements/ops-view-vpn.js +155 -3
- 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 +35 -1
- package/ts/config/classes.db-seeder.ts +95 -0
- package/ts/config/classes.reference-resolver.ts +576 -0
- package/ts/config/classes.route-config-manager.ts +64 -1
- package/ts/config/index.ts +3 -1
- package/ts/db/documents/classes.network-target.doc.ts +48 -0
- package/ts/db/documents/classes.security-profile.doc.ts +49 -0
- package/ts/db/documents/classes.stored-route.doc.ts +4 -0
- package/ts/db/documents/classes.vpn-client.doc.ts +24 -0
- package/ts/db/documents/index.ts +2 -0
- package/ts/opsserver/classes.opsserver.ts +4 -0
- package/ts/opsserver/handlers/index.ts +3 -1
- package/ts/opsserver/handlers/network-target.handler.ts +167 -0
- package/ts/opsserver/handlers/route-management.handler.ts +2 -1
- package/ts/opsserver/handlers/security-profile.handler.ts +169 -0
- package/ts/opsserver/handlers/vpn.handler.ts +37 -0
- package/ts/vpn/classes.vpn-manager.ts +143 -6
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +275 -1
- package/ts_web/elements/index.ts +2 -0
- package/ts_web/elements/ops-dashboard.ts +12 -0
- package/ts_web/elements/ops-view-networktargets.ts +214 -0
- package/ts_web/elements/ops-view-securityprofiles.ts +242 -0
- package/ts_web/elements/ops-view-vpn.ts +153 -2
- package/ts_web/router.ts +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
|
+
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
|
+
|
|
5
|
+
export class SecurityProfileHandler {
|
|
6
|
+
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
7
|
+
|
|
8
|
+
constructor(private opsServerRef: OpsServer) {
|
|
9
|
+
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
10
|
+
this.registerHandlers();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private async requireAuth(
|
|
14
|
+
request: { identity?: interfaces.data.IIdentity; apiToken?: string },
|
|
15
|
+
requiredScope?: interfaces.data.TApiTokenScope,
|
|
16
|
+
): Promise<string> {
|
|
17
|
+
if (request.identity?.jwt) {
|
|
18
|
+
try {
|
|
19
|
+
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({
|
|
20
|
+
identity: request.identity,
|
|
21
|
+
});
|
|
22
|
+
if (isAdmin) return request.identity.userId;
|
|
23
|
+
} catch { /* fall through */ }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (request.apiToken) {
|
|
27
|
+
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
28
|
+
if (tokenManager) {
|
|
29
|
+
const token = await tokenManager.validateToken(request.apiToken);
|
|
30
|
+
if (token) {
|
|
31
|
+
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
|
|
32
|
+
return token.createdBy;
|
|
33
|
+
}
|
|
34
|
+
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
throw new plugins.typedrequest.TypedResponseError('unauthorized');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private registerHandlers(): void {
|
|
43
|
+
// Get all security profiles
|
|
44
|
+
this.typedrouter.addTypedHandler(
|
|
45
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecurityProfiles>(
|
|
46
|
+
'getSecurityProfiles',
|
|
47
|
+
async (dataArg) => {
|
|
48
|
+
await this.requireAuth(dataArg, 'profiles:read');
|
|
49
|
+
const resolver = this.opsServerRef.dcRouterRef.referenceResolver;
|
|
50
|
+
if (!resolver) {
|
|
51
|
+
return { profiles: [] };
|
|
52
|
+
}
|
|
53
|
+
return { profiles: resolver.listProfiles() };
|
|
54
|
+
},
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Get a single security profile
|
|
59
|
+
this.typedrouter.addTypedHandler(
|
|
60
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecurityProfile>(
|
|
61
|
+
'getSecurityProfile',
|
|
62
|
+
async (dataArg) => {
|
|
63
|
+
await this.requireAuth(dataArg, 'profiles:read');
|
|
64
|
+
const resolver = this.opsServerRef.dcRouterRef.referenceResolver;
|
|
65
|
+
if (!resolver) {
|
|
66
|
+
return { profile: null };
|
|
67
|
+
}
|
|
68
|
+
return { profile: resolver.getProfile(dataArg.id) || null };
|
|
69
|
+
},
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Create a security profile
|
|
74
|
+
this.typedrouter.addTypedHandler(
|
|
75
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateSecurityProfile>(
|
|
76
|
+
'createSecurityProfile',
|
|
77
|
+
async (dataArg) => {
|
|
78
|
+
const userId = await this.requireAuth(dataArg, 'profiles:write');
|
|
79
|
+
const resolver = this.opsServerRef.dcRouterRef.referenceResolver;
|
|
80
|
+
if (!resolver) {
|
|
81
|
+
return { success: false, message: 'Reference resolver not initialized' };
|
|
82
|
+
}
|
|
83
|
+
const id = await resolver.createProfile({
|
|
84
|
+
name: dataArg.name,
|
|
85
|
+
description: dataArg.description,
|
|
86
|
+
security: dataArg.security,
|
|
87
|
+
extendsProfiles: dataArg.extendsProfiles,
|
|
88
|
+
createdBy: userId,
|
|
89
|
+
});
|
|
90
|
+
return { success: true, id };
|
|
91
|
+
},
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Update a security profile
|
|
96
|
+
this.typedrouter.addTypedHandler(
|
|
97
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateSecurityProfile>(
|
|
98
|
+
'updateSecurityProfile',
|
|
99
|
+
async (dataArg) => {
|
|
100
|
+
await this.requireAuth(dataArg, 'profiles:write');
|
|
101
|
+
const resolver = this.opsServerRef.dcRouterRef.referenceResolver;
|
|
102
|
+
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
|
103
|
+
if (!resolver || !manager) {
|
|
104
|
+
return { success: false, message: 'Not initialized' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const { affectedRouteIds } = await resolver.updateProfile(dataArg.id, {
|
|
108
|
+
name: dataArg.name,
|
|
109
|
+
description: dataArg.description,
|
|
110
|
+
security: dataArg.security,
|
|
111
|
+
extendsProfiles: dataArg.extendsProfiles,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Propagate to affected routes
|
|
115
|
+
if (affectedRouteIds.length > 0) {
|
|
116
|
+
await manager.reResolveRoutes(affectedRouteIds);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { success: true, affectedRouteCount: affectedRouteIds.length };
|
|
120
|
+
},
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Delete a security profile
|
|
125
|
+
this.typedrouter.addTypedHandler(
|
|
126
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteSecurityProfile>(
|
|
127
|
+
'deleteSecurityProfile',
|
|
128
|
+
async (dataArg) => {
|
|
129
|
+
await this.requireAuth(dataArg, 'profiles:write');
|
|
130
|
+
const resolver = this.opsServerRef.dcRouterRef.referenceResolver;
|
|
131
|
+
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
|
132
|
+
if (!resolver || !manager) {
|
|
133
|
+
return { success: false, message: 'Not initialized' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const result = await resolver.deleteProfile(
|
|
137
|
+
dataArg.id,
|
|
138
|
+
dataArg.force ?? false,
|
|
139
|
+
manager.getStoredRoutes(),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// If force-deleted with affected routes, re-apply
|
|
143
|
+
if (result.success && dataArg.force) {
|
|
144
|
+
await manager.applyRoutes();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return result;
|
|
148
|
+
},
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Get routes using a security profile
|
|
153
|
+
this.typedrouter.addTypedHandler(
|
|
154
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecurityProfileUsage>(
|
|
155
|
+
'getSecurityProfileUsage',
|
|
156
|
+
async (dataArg) => {
|
|
157
|
+
await this.requireAuth(dataArg, 'profiles:read');
|
|
158
|
+
const resolver = this.opsServerRef.dcRouterRef.referenceResolver;
|
|
159
|
+
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
|
160
|
+
if (!resolver || !manager) {
|
|
161
|
+
return { routes: [] };
|
|
162
|
+
}
|
|
163
|
+
const usage = resolver.getProfileUsageForId(dataArg.id, manager.getStoredRoutes());
|
|
164
|
+
return { routes: usage.map((u) => ({ id: u.id, name: u.routeName })) };
|
|
165
|
+
},
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -31,6 +31,14 @@ export class VpnHandler {
|
|
|
31
31
|
createdAt: c.createdAt,
|
|
32
32
|
updatedAt: c.updatedAt,
|
|
33
33
|
expiresAt: c.expiresAt,
|
|
34
|
+
forceDestinationSmartproxy: c.forceDestinationSmartproxy ?? true,
|
|
35
|
+
destinationAllowList: c.destinationAllowList,
|
|
36
|
+
destinationBlockList: c.destinationBlockList,
|
|
37
|
+
useHostIp: c.useHostIp,
|
|
38
|
+
useDhcp: c.useDhcp,
|
|
39
|
+
staticIp: c.staticIp,
|
|
40
|
+
forceVlan: c.forceVlan,
|
|
41
|
+
vlanId: c.vlanId,
|
|
34
42
|
}));
|
|
35
43
|
return { clients };
|
|
36
44
|
},
|
|
@@ -114,8 +122,21 @@ export class VpnHandler {
|
|
|
114
122
|
clientId: dataArg.clientId,
|
|
115
123
|
serverDefinedClientTags: dataArg.serverDefinedClientTags,
|
|
116
124
|
description: dataArg.description,
|
|
125
|
+
forceDestinationSmartproxy: dataArg.forceDestinationSmartproxy,
|
|
126
|
+
destinationAllowList: dataArg.destinationAllowList,
|
|
127
|
+
destinationBlockList: dataArg.destinationBlockList,
|
|
128
|
+
useHostIp: dataArg.useHostIp,
|
|
129
|
+
useDhcp: dataArg.useDhcp,
|
|
130
|
+
staticIp: dataArg.staticIp,
|
|
131
|
+
forceVlan: dataArg.forceVlan,
|
|
132
|
+
vlanId: dataArg.vlanId,
|
|
117
133
|
});
|
|
118
134
|
|
|
135
|
+
// Retrieve the persisted doc to get dcrouter-level fields
|
|
136
|
+
const persistedClient = manager.listClients().find(
|
|
137
|
+
(c) => c.clientId === bundle.entry.clientId,
|
|
138
|
+
);
|
|
139
|
+
|
|
119
140
|
return {
|
|
120
141
|
success: true,
|
|
121
142
|
client: {
|
|
@@ -127,6 +148,14 @@ export class VpnHandler {
|
|
|
127
148
|
createdAt: Date.now(),
|
|
128
149
|
updatedAt: Date.now(),
|
|
129
150
|
expiresAt: bundle.entry.expiresAt,
|
|
151
|
+
forceDestinationSmartproxy: persistedClient?.forceDestinationSmartproxy ?? true,
|
|
152
|
+
destinationAllowList: persistedClient?.destinationAllowList,
|
|
153
|
+
destinationBlockList: persistedClient?.destinationBlockList,
|
|
154
|
+
useHostIp: persistedClient?.useHostIp,
|
|
155
|
+
useDhcp: persistedClient?.useDhcp,
|
|
156
|
+
staticIp: persistedClient?.staticIp,
|
|
157
|
+
forceVlan: persistedClient?.forceVlan,
|
|
158
|
+
vlanId: persistedClient?.vlanId,
|
|
130
159
|
},
|
|
131
160
|
wireguardConfig: bundle.wireguardConfig,
|
|
132
161
|
};
|
|
@@ -151,6 +180,14 @@ export class VpnHandler {
|
|
|
151
180
|
await manager.updateClient(dataArg.clientId, {
|
|
152
181
|
description: dataArg.description,
|
|
153
182
|
serverDefinedClientTags: dataArg.serverDefinedClientTags,
|
|
183
|
+
forceDestinationSmartproxy: dataArg.forceDestinationSmartproxy,
|
|
184
|
+
destinationAllowList: dataArg.destinationAllowList,
|
|
185
|
+
destinationBlockList: dataArg.destinationBlockList,
|
|
186
|
+
useHostIp: dataArg.useHostIp,
|
|
187
|
+
useDhcp: dataArg.useDhcp,
|
|
188
|
+
staticIp: dataArg.staticIp,
|
|
189
|
+
forceVlan: dataArg.forceVlan,
|
|
190
|
+
vlanId: dataArg.vlanId,
|
|
154
191
|
});
|
|
155
192
|
return { success: true };
|
|
156
193
|
} catch (err: unknown) {
|
|
@@ -30,6 +30,17 @@ export interface IVpnManagerConfig {
|
|
|
30
30
|
* Called at config generation time (create/export). Returns CIDRs for WireGuard AllowedIPs.
|
|
31
31
|
* When not set, defaults to [subnet]. */
|
|
32
32
|
getClientAllowedIPs?: (clientTags: string[]) => Promise<string[]>;
|
|
33
|
+
/** Forwarding mode: 'socket' (default, userspace NAT), 'bridge' (L2 bridge to host LAN),
|
|
34
|
+
* or 'hybrid' (socket default, bridge for clients with useHostIp=true) */
|
|
35
|
+
forwardingMode?: 'socket' | 'bridge' | 'hybrid';
|
|
36
|
+
/** LAN subnet CIDR for bridge mode (e.g., '192.168.1.0/24') */
|
|
37
|
+
bridgeLanSubnet?: string;
|
|
38
|
+
/** Physical network interface for bridge mode (auto-detected if omitted) */
|
|
39
|
+
bridgePhysicalInterface?: string;
|
|
40
|
+
/** Start of VPN client IP range in LAN subnet (host offset, default: 200) */
|
|
41
|
+
bridgeIpRangeStart?: number;
|
|
42
|
+
/** End of VPN client IP range in LAN subnet (host offset, default: 250) */
|
|
43
|
+
bridgeIpRangeEnd?: number;
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
/**
|
|
@@ -69,8 +80,12 @@ export class VpnManager {
|
|
|
69
80
|
|
|
70
81
|
// Build client entries for the daemon
|
|
71
82
|
const clientEntries: plugins.smartvpn.IClientEntry[] = [];
|
|
83
|
+
let anyClientUsesHostIp = false;
|
|
72
84
|
for (const client of this.clients.values()) {
|
|
73
|
-
|
|
85
|
+
if (client.useHostIp) {
|
|
86
|
+
anyClientUsesHostIp = true;
|
|
87
|
+
}
|
|
88
|
+
const entry: plugins.smartvpn.IClientEntry = {
|
|
74
89
|
clientId: client.clientId,
|
|
75
90
|
publicKey: client.noisePublicKey,
|
|
76
91
|
wgPublicKey: client.wgPublicKey,
|
|
@@ -79,35 +94,65 @@ export class VpnManager {
|
|
|
79
94
|
description: client.description,
|
|
80
95
|
assignedIp: client.assignedIp,
|
|
81
96
|
expiresAt: client.expiresAt,
|
|
82
|
-
|
|
97
|
+
security: this.buildClientSecurity(client),
|
|
98
|
+
};
|
|
99
|
+
// Pass per-client bridge fields if present (for hybrid/bridge mode)
|
|
100
|
+
if (client.useHostIp !== undefined) (entry as any).useHostIp = client.useHostIp;
|
|
101
|
+
if (client.useDhcp !== undefined) (entry as any).useDhcp = client.useDhcp;
|
|
102
|
+
if (client.staticIp !== undefined) (entry as any).staticIp = client.staticIp;
|
|
103
|
+
if (client.forceVlan !== undefined) (entry as any).forceVlan = client.forceVlan;
|
|
104
|
+
if (client.vlanId !== undefined) (entry as any).vlanId = client.vlanId;
|
|
105
|
+
clientEntries.push(entry);
|
|
83
106
|
}
|
|
84
107
|
|
|
85
108
|
const subnet = this.getSubnet();
|
|
86
109
|
const wgListenPort = this.config.wgListenPort ?? 51820;
|
|
87
110
|
|
|
111
|
+
// Auto-detect hybrid mode: if any persisted client uses host IP and mode is
|
|
112
|
+
// 'socket' (or unset), upgrade to 'hybrid' so the daemon can handle both
|
|
113
|
+
let configuredMode = this.config.forwardingMode ?? 'socket';
|
|
114
|
+
if (anyClientUsesHostIp && configuredMode === 'socket') {
|
|
115
|
+
configuredMode = 'hybrid';
|
|
116
|
+
logger.log('info', 'VPN: Auto-upgrading forwarding mode to hybrid (client with useHostIp detected)');
|
|
117
|
+
}
|
|
118
|
+
const forwardingMode = configuredMode === 'hybrid' ? 'hybrid' : configuredMode;
|
|
119
|
+
const isBridge = forwardingMode === 'bridge';
|
|
120
|
+
|
|
88
121
|
// Create and start VpnServer
|
|
89
122
|
this.vpnServer = new plugins.smartvpn.VpnServer({
|
|
90
123
|
transport: { transport: 'stdio' },
|
|
91
124
|
});
|
|
92
125
|
|
|
126
|
+
// Default destination policy: bridge mode allows traffic through directly,
|
|
127
|
+
// socket mode forces traffic to SmartProxy on 127.0.0.1
|
|
128
|
+
const defaultDestinationPolicy: plugins.smartvpn.IDestinationPolicy = isBridge
|
|
129
|
+
? { default: 'allow' as const }
|
|
130
|
+
: { default: 'forceTarget' as const, target: '127.0.0.1' };
|
|
131
|
+
|
|
93
132
|
const serverConfig: plugins.smartvpn.IVpnServerConfig = {
|
|
94
133
|
listenAddr: '0.0.0.0:0', // WS listener not strictly needed but required field
|
|
95
134
|
privateKey: this.serverKeys.noisePrivateKey,
|
|
96
135
|
publicKey: this.serverKeys.noisePublicKey,
|
|
97
136
|
subnet,
|
|
98
137
|
dns: this.config.dns,
|
|
99
|
-
forwardingMode:
|
|
138
|
+
forwardingMode: forwardingMode as any,
|
|
100
139
|
transportMode: 'all',
|
|
101
140
|
wgPrivateKey: this.serverKeys.wgPrivateKey,
|
|
102
141
|
wgListenPort,
|
|
103
142
|
clients: clientEntries,
|
|
104
|
-
socketForwardProxyProtocol:
|
|
105
|
-
destinationPolicy: this.config.destinationPolicy
|
|
106
|
-
?? { default: 'forceTarget' as const, target: '127.0.0.1' },
|
|
143
|
+
socketForwardProxyProtocol: !isBridge,
|
|
144
|
+
destinationPolicy: this.config.destinationPolicy ?? defaultDestinationPolicy,
|
|
107
145
|
serverEndpoint: this.config.serverEndpoint
|
|
108
146
|
? `${this.config.serverEndpoint}:${wgListenPort}`
|
|
109
147
|
: undefined,
|
|
110
148
|
clientAllowedIPs: [subnet],
|
|
149
|
+
// Bridge-specific config
|
|
150
|
+
...(isBridge ? {
|
|
151
|
+
bridgeLanSubnet: this.config.bridgeLanSubnet,
|
|
152
|
+
bridgePhysicalInterface: this.config.bridgePhysicalInterface,
|
|
153
|
+
bridgeIpRangeStart: this.config.bridgeIpRangeStart,
|
|
154
|
+
bridgeIpRangeEnd: this.config.bridgeIpRangeEnd,
|
|
155
|
+
} : {}),
|
|
111
156
|
};
|
|
112
157
|
|
|
113
158
|
await this.vpnServer.start(serverConfig);
|
|
@@ -154,6 +199,14 @@ export class VpnManager {
|
|
|
154
199
|
clientId: string;
|
|
155
200
|
serverDefinedClientTags?: string[];
|
|
156
201
|
description?: string;
|
|
202
|
+
forceDestinationSmartproxy?: boolean;
|
|
203
|
+
destinationAllowList?: string[];
|
|
204
|
+
destinationBlockList?: string[];
|
|
205
|
+
useHostIp?: boolean;
|
|
206
|
+
useDhcp?: boolean;
|
|
207
|
+
staticIp?: string;
|
|
208
|
+
forceVlan?: boolean;
|
|
209
|
+
vlanId?: number;
|
|
157
210
|
}): Promise<plugins.smartvpn.IClientConfigBundle> {
|
|
158
211
|
if (!this.vpnServer) {
|
|
159
212
|
throw new Error('VPN server not running');
|
|
@@ -188,9 +241,39 @@ export class VpnManager {
|
|
|
188
241
|
doc.createdAt = Date.now();
|
|
189
242
|
doc.updatedAt = Date.now();
|
|
190
243
|
doc.expiresAt = bundle.entry.expiresAt;
|
|
244
|
+
if (opts.forceDestinationSmartproxy !== undefined) {
|
|
245
|
+
doc.forceDestinationSmartproxy = opts.forceDestinationSmartproxy;
|
|
246
|
+
}
|
|
247
|
+
if (opts.destinationAllowList !== undefined) {
|
|
248
|
+
doc.destinationAllowList = opts.destinationAllowList;
|
|
249
|
+
}
|
|
250
|
+
if (opts.destinationBlockList !== undefined) {
|
|
251
|
+
doc.destinationBlockList = opts.destinationBlockList;
|
|
252
|
+
}
|
|
253
|
+
if (opts.useHostIp !== undefined) {
|
|
254
|
+
doc.useHostIp = opts.useHostIp;
|
|
255
|
+
}
|
|
256
|
+
if (opts.useDhcp !== undefined) {
|
|
257
|
+
doc.useDhcp = opts.useDhcp;
|
|
258
|
+
}
|
|
259
|
+
if (opts.staticIp !== undefined) {
|
|
260
|
+
doc.staticIp = opts.staticIp;
|
|
261
|
+
}
|
|
262
|
+
if (opts.forceVlan !== undefined) {
|
|
263
|
+
doc.forceVlan = opts.forceVlan;
|
|
264
|
+
}
|
|
265
|
+
if (opts.vlanId !== undefined) {
|
|
266
|
+
doc.vlanId = opts.vlanId;
|
|
267
|
+
}
|
|
191
268
|
this.clients.set(doc.clientId, doc);
|
|
192
269
|
await this.persistClient(doc);
|
|
193
270
|
|
|
271
|
+
// Sync per-client security to the running daemon
|
|
272
|
+
const security = this.buildClientSecurity(doc);
|
|
273
|
+
if (security.destinationPolicy) {
|
|
274
|
+
await this.vpnServer!.updateClient(doc.clientId, { security });
|
|
275
|
+
}
|
|
276
|
+
|
|
194
277
|
this.config.onClientChanged?.();
|
|
195
278
|
return bundle;
|
|
196
279
|
}
|
|
@@ -254,13 +337,36 @@ export class VpnManager {
|
|
|
254
337
|
public async updateClient(clientId: string, update: {
|
|
255
338
|
description?: string;
|
|
256
339
|
serverDefinedClientTags?: string[];
|
|
340
|
+
forceDestinationSmartproxy?: boolean;
|
|
341
|
+
destinationAllowList?: string[];
|
|
342
|
+
destinationBlockList?: string[];
|
|
343
|
+
useHostIp?: boolean;
|
|
344
|
+
useDhcp?: boolean;
|
|
345
|
+
staticIp?: string;
|
|
346
|
+
forceVlan?: boolean;
|
|
347
|
+
vlanId?: number;
|
|
257
348
|
}): Promise<void> {
|
|
258
349
|
const client = this.clients.get(clientId);
|
|
259
350
|
if (!client) throw new Error(`Client not found: ${clientId}`);
|
|
260
351
|
if (update.description !== undefined) client.description = update.description;
|
|
261
352
|
if (update.serverDefinedClientTags !== undefined) client.serverDefinedClientTags = update.serverDefinedClientTags;
|
|
353
|
+
if (update.forceDestinationSmartproxy !== undefined) client.forceDestinationSmartproxy = update.forceDestinationSmartproxy;
|
|
354
|
+
if (update.destinationAllowList !== undefined) client.destinationAllowList = update.destinationAllowList;
|
|
355
|
+
if (update.destinationBlockList !== undefined) client.destinationBlockList = update.destinationBlockList;
|
|
356
|
+
if (update.useHostIp !== undefined) client.useHostIp = update.useHostIp;
|
|
357
|
+
if (update.useDhcp !== undefined) client.useDhcp = update.useDhcp;
|
|
358
|
+
if (update.staticIp !== undefined) client.staticIp = update.staticIp;
|
|
359
|
+
if (update.forceVlan !== undefined) client.forceVlan = update.forceVlan;
|
|
360
|
+
if (update.vlanId !== undefined) client.vlanId = update.vlanId;
|
|
262
361
|
client.updatedAt = Date.now();
|
|
263
362
|
await this.persistClient(client);
|
|
363
|
+
|
|
364
|
+
// Sync per-client security to the running daemon
|
|
365
|
+
if (this.vpnServer) {
|
|
366
|
+
const security = this.buildClientSecurity(client);
|
|
367
|
+
await this.vpnServer.updateClient(clientId, { security });
|
|
368
|
+
}
|
|
369
|
+
|
|
264
370
|
this.config.onClientChanged?.();
|
|
265
371
|
}
|
|
266
372
|
|
|
@@ -378,6 +484,37 @@ export class VpnManager {
|
|
|
378
484
|
};
|
|
379
485
|
}
|
|
380
486
|
|
|
487
|
+
// ── Per-client security ────────────────────────────────────────────────
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Build per-client security settings for the smartvpn daemon.
|
|
491
|
+
* Maps dcrouter-level fields (forceDestinationSmartproxy, allow/block lists)
|
|
492
|
+
* to smartvpn's IClientSecurity with a destinationPolicy.
|
|
493
|
+
*/
|
|
494
|
+
private buildClientSecurity(client: VpnClientDoc): plugins.smartvpn.IClientSecurity {
|
|
495
|
+
const security: plugins.smartvpn.IClientSecurity = {};
|
|
496
|
+
const forceSmartproxy = client.forceDestinationSmartproxy ?? true;
|
|
497
|
+
|
|
498
|
+
if (!forceSmartproxy) {
|
|
499
|
+
// Client traffic goes directly — not forced to SmartProxy
|
|
500
|
+
security.destinationPolicy = {
|
|
501
|
+
default: 'allow' as const,
|
|
502
|
+
blockList: client.destinationBlockList,
|
|
503
|
+
};
|
|
504
|
+
} else if (client.destinationAllowList?.length || client.destinationBlockList?.length) {
|
|
505
|
+
// Client is forced to SmartProxy, but with per-client allow/block overrides
|
|
506
|
+
security.destinationPolicy = {
|
|
507
|
+
default: 'forceTarget' as const,
|
|
508
|
+
target: '127.0.0.1',
|
|
509
|
+
allowList: client.destinationAllowList,
|
|
510
|
+
blockList: client.destinationBlockList,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
// else: no per-client policy, server-wide applies
|
|
514
|
+
|
|
515
|
+
return security;
|
|
516
|
+
}
|
|
517
|
+
|
|
381
518
|
// ── Private helpers ────────────────────────────────────────────────────
|
|
382
519
|
|
|
383
520
|
private async loadOrGenerateServerKeys(): Promise<VpnServerKeysDoc> {
|