@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.
Files changed (84) hide show
  1. package/dist_serve/bundle.js +1179 -1004
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +17 -1
  4. package/dist_ts/classes.dcrouter.js +16 -3
  5. package/dist_ts/config/classes.db-seeder.d.ts +25 -0
  6. package/dist_ts/config/classes.db-seeder.js +69 -0
  7. package/dist_ts/config/classes.reference-resolver.d.ts +80 -0
  8. package/dist_ts/config/classes.reference-resolver.js +482 -0
  9. package/dist_ts/config/classes.route-config-manager.d.ts +13 -3
  10. package/dist_ts/config/classes.route-config-manager.js +53 -3
  11. package/dist_ts/config/index.d.ts +2 -0
  12. package/dist_ts/config/index.js +3 -1
  13. package/dist_ts/db/documents/classes.network-target.doc.d.ts +15 -0
  14. package/dist_ts/db/documents/classes.network-target.doc.js +118 -0
  15. package/dist_ts/db/documents/classes.security-profile.doc.d.ts +16 -0
  16. package/dist_ts/db/documents/classes.security-profile.doc.js +118 -0
  17. package/dist_ts/db/documents/classes.stored-route.doc.d.ts +2 -0
  18. package/dist_ts/db/documents/classes.stored-route.doc.js +8 -2
  19. package/dist_ts/db/documents/classes.vpn-client.doc.d.ts +8 -0
  20. package/dist_ts/db/documents/classes.vpn-client.doc.js +50 -2
  21. package/dist_ts/db/documents/index.d.ts +2 -0
  22. package/dist_ts/db/documents/index.js +3 -1
  23. package/dist_ts/opsserver/classes.opsserver.d.ts +2 -0
  24. package/dist_ts/opsserver/classes.opsserver.js +5 -1
  25. package/dist_ts/opsserver/handlers/index.d.ts +2 -0
  26. package/dist_ts/opsserver/handlers/index.js +3 -1
  27. package/dist_ts/opsserver/handlers/network-target.handler.d.ts +10 -0
  28. package/dist_ts/opsserver/handlers/network-target.handler.js +117 -0
  29. package/dist_ts/opsserver/handlers/route-management.handler.js +3 -2
  30. package/dist_ts/opsserver/handlers/security-profile.handler.d.ts +10 -0
  31. package/dist_ts/opsserver/handlers/security-profile.handler.js +119 -0
  32. package/dist_ts/opsserver/handlers/vpn.handler.js +35 -1
  33. package/dist_ts/vpn/classes.vpn-manager.d.ts +33 -0
  34. package/dist_ts/vpn/classes.vpn-manager.js +122 -7
  35. package/dist_ts_interfaces/data/route-management.d.ts +48 -1
  36. package/dist_ts_interfaces/data/vpn.d.ts +8 -0
  37. package/dist_ts_interfaces/requests/index.d.ts +2 -0
  38. package/dist_ts_interfaces/requests/index.js +3 -1
  39. package/dist_ts_interfaces/requests/network-targets.d.ts +102 -0
  40. package/dist_ts_interfaces/requests/network-targets.js +2 -0
  41. package/dist_ts_interfaces/requests/route-management.d.ts +3 -1
  42. package/dist_ts_interfaces/requests/security-profiles.d.ts +102 -0
  43. package/dist_ts_interfaces/requests/security-profiles.js +2 -0
  44. package/dist_ts_interfaces/requests/vpn.d.ts +16 -0
  45. package/dist_ts_web/00_commitinfo_data.js +1 -1
  46. package/dist_ts_web/appstate.d.ts +59 -0
  47. package/dist_ts_web/appstate.js +192 -2
  48. package/dist_ts_web/elements/index.d.ts +2 -0
  49. package/dist_ts_web/elements/index.js +3 -1
  50. package/dist_ts_web/elements/ops-dashboard.js +13 -1
  51. package/dist_ts_web/elements/ops-view-networktargets.d.ts +17 -0
  52. package/dist_ts_web/elements/ops-view-networktargets.js +246 -0
  53. package/dist_ts_web/elements/ops-view-securityprofiles.d.ts +17 -0
  54. package/dist_ts_web/elements/ops-view-securityprofiles.js +275 -0
  55. package/dist_ts_web/elements/ops-view-vpn.js +155 -3
  56. package/dist_ts_web/router.d.ts +1 -1
  57. package/dist_ts_web/router.js +2 -2
  58. package/package.json +3 -3
  59. package/ts/00_commitinfo_data.ts +1 -1
  60. package/ts/classes.dcrouter.ts +35 -1
  61. package/ts/config/classes.db-seeder.ts +95 -0
  62. package/ts/config/classes.reference-resolver.ts +576 -0
  63. package/ts/config/classes.route-config-manager.ts +64 -1
  64. package/ts/config/index.ts +3 -1
  65. package/ts/db/documents/classes.network-target.doc.ts +48 -0
  66. package/ts/db/documents/classes.security-profile.doc.ts +49 -0
  67. package/ts/db/documents/classes.stored-route.doc.ts +4 -0
  68. package/ts/db/documents/classes.vpn-client.doc.ts +24 -0
  69. package/ts/db/documents/index.ts +2 -0
  70. package/ts/opsserver/classes.opsserver.ts +4 -0
  71. package/ts/opsserver/handlers/index.ts +3 -1
  72. package/ts/opsserver/handlers/network-target.handler.ts +167 -0
  73. package/ts/opsserver/handlers/route-management.handler.ts +2 -1
  74. package/ts/opsserver/handlers/security-profile.handler.ts +169 -0
  75. package/ts/opsserver/handlers/vpn.handler.ts +37 -0
  76. package/ts/vpn/classes.vpn-manager.ts +143 -6
  77. package/ts_web/00_commitinfo_data.ts +1 -1
  78. package/ts_web/appstate.ts +275 -1
  79. package/ts_web/elements/index.ts +2 -0
  80. package/ts_web/elements/ops-dashboard.ts +12 -0
  81. package/ts_web/elements/ops-view-networktargets.ts +214 -0
  82. package/ts_web/elements/ops-view-securityprofiles.ts +242 -0
  83. package/ts_web/elements/ops-view-vpn.ts +153 -2
  84. 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
- clientEntries.push({
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: 'socket',
138
+ forwardingMode: forwardingMode as any,
100
139
  transportMode: 'all',
101
140
  wgPrivateKey: this.serverKeys.wgPrivateKey,
102
141
  wgListenPort,
103
142
  clients: clientEntries,
104
- socketForwardProxyProtocol: true,
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> {
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '12.0.0',
6
+ version: '12.2.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }