@serve.zone/dcrouter 11.12.4 → 11.13.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 (50) hide show
  1. package/dist_serve/bundle.js +705 -548
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +30 -0
  4. package/dist_ts/classes.dcrouter.js +92 -2
  5. package/dist_ts/config/classes.route-config-manager.d.ts +2 -1
  6. package/dist_ts/config/classes.route-config-manager.js +21 -5
  7. package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
  8. package/dist_ts/opsserver/classes.opsserver.js +3 -1
  9. package/dist_ts/opsserver/handlers/index.d.ts +1 -0
  10. package/dist_ts/opsserver/handlers/index.js +2 -1
  11. package/dist_ts/opsserver/handlers/vpn.handler.d.ts +6 -0
  12. package/dist_ts/opsserver/handlers/vpn.handler.js +199 -0
  13. package/dist_ts/plugins.d.ts +2 -1
  14. package/dist_ts/plugins.js +3 -2
  15. package/dist_ts/vpn/classes.vpn-manager.d.ts +113 -0
  16. package/dist_ts/vpn/classes.vpn-manager.js +297 -0
  17. package/dist_ts/vpn/index.d.ts +1 -0
  18. package/dist_ts/vpn/index.js +2 -0
  19. package/dist_ts_interfaces/data/index.d.ts +1 -0
  20. package/dist_ts_interfaces/data/index.js +2 -1
  21. package/dist_ts_interfaces/data/remoteingress.d.ts +10 -1
  22. package/dist_ts_interfaces/data/vpn.d.ts +43 -0
  23. package/dist_ts_interfaces/data/vpn.js +2 -0
  24. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  25. package/dist_ts_interfaces/requests/index.js +2 -1
  26. package/dist_ts_interfaces/requests/vpn.d.ts +135 -0
  27. package/dist_ts_interfaces/requests/vpn.js +3 -0
  28. package/dist_ts_web/00_commitinfo_data.js +1 -1
  29. package/dist_ts_web/appstate.d.ts +22 -0
  30. package/dist_ts_web/appstate.js +111 -1
  31. package/dist_ts_web/elements/index.d.ts +1 -0
  32. package/dist_ts_web/elements/index.js +2 -1
  33. package/dist_ts_web/elements/ops-dashboard.js +7 -1
  34. package/dist_ts_web/elements/ops-view-vpn.d.ts +14 -0
  35. package/dist_ts_web/elements/ops-view-vpn.js +369 -0
  36. package/package.json +2 -1
  37. package/ts/00_commitinfo_data.ts +1 -1
  38. package/ts/classes.dcrouter.ts +126 -0
  39. package/ts/config/classes.route-config-manager.ts +20 -3
  40. package/ts/opsserver/classes.opsserver.ts +2 -0
  41. package/ts/opsserver/handlers/index.ts +2 -1
  42. package/ts/opsserver/handlers/vpn.handler.ts +257 -0
  43. package/ts/plugins.ts +2 -1
  44. package/ts/vpn/classes.vpn-manager.ts +378 -0
  45. package/ts/vpn/index.ts +1 -0
  46. package/ts_web/00_commitinfo_data.ts +1 -1
  47. package/ts_web/appstate.ts +164 -0
  48. package/ts_web/elements/index.ts +1 -0
  49. package/ts_web/elements/ops-dashboard.ts +6 -0
  50. package/ts_web/elements/ops-view-vpn.ts +330 -0
@@ -0,0 +1,369 @@
1
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
+ var _, done = false;
7
+ for (var i = decorators.length - 1; i >= 0; i--) {
8
+ var context = {};
9
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
+ if (kind === "accessor") {
14
+ if (result === void 0) continue;
15
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
+ if (_ = accept(result.get)) descriptor.get = _;
17
+ if (_ = accept(result.set)) descriptor.set = _;
18
+ if (_ = accept(result.init)) initializers.unshift(_);
19
+ }
20
+ else if (_ = accept(result)) {
21
+ if (kind === "field") initializers.unshift(_);
22
+ else descriptor[key] = _;
23
+ }
24
+ }
25
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
+ done = true;
27
+ };
28
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
+ var useValue = arguments.length > 2;
30
+ for (var i = 0; i < initializers.length; i++) {
31
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
+ }
33
+ return useValue ? value : void 0;
34
+ };
35
+ import { DeesElement, html, customElement, css, state, cssManager, } from '@design.estate/dees-element';
36
+ import * as appstate from '../appstate.js';
37
+ import * as interfaces from '../../dist_ts_interfaces/index.js';
38
+ import { viewHostCss } from './shared/css.js';
39
+ import {} from '@design.estate/dees-catalog';
40
+ let OpsViewVpn = (() => {
41
+ let _classDecorators = [customElement('ops-view-vpn')];
42
+ let _classDescriptor;
43
+ let _classExtraInitializers = [];
44
+ let _classThis;
45
+ let _classSuper = DeesElement;
46
+ let _vpnState_decorators;
47
+ let _vpnState_initializers = [];
48
+ let _vpnState_extraInitializers = [];
49
+ var OpsViewVpn = class extends _classSuper {
50
+ static { _classThis = this; }
51
+ static {
52
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
53
+ _vpnState_decorators = [state()];
54
+ __esDecorate(this, null, _vpnState_decorators, { kind: "accessor", name: "vpnState", static: false, private: false, access: { has: obj => "vpnState" in obj, get: obj => obj.vpnState, set: (obj, value) => { obj.vpnState = value; } }, metadata: _metadata }, _vpnState_initializers, _vpnState_extraInitializers);
55
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
56
+ OpsViewVpn = _classThis = _classDescriptor.value;
57
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
58
+ }
59
+ #vpnState_accessor_storage = __runInitializers(this, _vpnState_initializers, appstate.vpnStatePart.getState());
60
+ get vpnState() { return this.#vpnState_accessor_storage; }
61
+ set vpnState(value) { this.#vpnState_accessor_storage = value; }
62
+ constructor() {
63
+ super();
64
+ __runInitializers(this, _vpnState_extraInitializers);
65
+ const sub = appstate.vpnStatePart.select().subscribe((newState) => {
66
+ this.vpnState = newState;
67
+ });
68
+ this.rxSubscriptions.push(sub);
69
+ }
70
+ async connectedCallback() {
71
+ await super.connectedCallback();
72
+ await appstate.vpnStatePart.dispatchAction(appstate.fetchVpnAction, null);
73
+ }
74
+ static styles = [
75
+ cssManager.defaultStyles,
76
+ viewHostCss,
77
+ css `
78
+ .vpnContainer {
79
+ display: flex;
80
+ flex-direction: column;
81
+ gap: 24px;
82
+ }
83
+
84
+ .statusBadge {
85
+ display: inline-flex;
86
+ align-items: center;
87
+ padding: 3px 10px;
88
+ border-radius: 12px;
89
+ font-size: 12px;
90
+ font-weight: 600;
91
+ letter-spacing: 0.02em;
92
+ text-transform: uppercase;
93
+ }
94
+
95
+ .statusBadge.enabled {
96
+ background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
97
+ color: ${cssManager.bdTheme('#166534', '#4ade80')};
98
+ }
99
+
100
+ .statusBadge.disabled {
101
+ background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
102
+ color: ${cssManager.bdTheme('#991b1b', '#f87171')};
103
+ }
104
+
105
+ .configDialog {
106
+ padding: 16px;
107
+ background: ${cssManager.bdTheme('#fffbeb', '#1c1917')};
108
+ border: 1px solid ${cssManager.bdTheme('#fbbf24', '#92400e')};
109
+ border-radius: 8px;
110
+ margin-bottom: 16px;
111
+ }
112
+
113
+ .configDialog pre {
114
+ display: block;
115
+ padding: 12px;
116
+ background: ${cssManager.bdTheme('#1f2937', '#111827')};
117
+ color: #10b981;
118
+ border-radius: 4px;
119
+ font-family: monospace;
120
+ font-size: 12px;
121
+ white-space: pre-wrap;
122
+ word-break: break-all;
123
+ margin: 8px 0;
124
+ user-select: all;
125
+ max-height: 300px;
126
+ overflow-y: auto;
127
+ }
128
+
129
+ .configDialog .warning {
130
+ font-size: 12px;
131
+ color: ${cssManager.bdTheme('#92400e', '#fbbf24')};
132
+ margin-top: 8px;
133
+ }
134
+
135
+ .tagBadge {
136
+ display: inline-flex;
137
+ padding: 2px 8px;
138
+ border-radius: 4px;
139
+ font-size: 12px;
140
+ font-weight: 500;
141
+ background: ${cssManager.bdTheme('#eff6ff', '#172554')};
142
+ color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};
143
+ margin-right: 4px;
144
+ }
145
+
146
+ .serverInfo {
147
+ display: grid;
148
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
149
+ gap: 12px;
150
+ padding: 16px;
151
+ background: ${cssManager.bdTheme('#f9fafb', '#111827')};
152
+ border-radius: 8px;
153
+ border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#1f2937')};
154
+ }
155
+
156
+ .serverInfo .infoItem {
157
+ display: flex;
158
+ flex-direction: column;
159
+ gap: 4px;
160
+ }
161
+
162
+ .serverInfo .infoLabel {
163
+ font-size: 11px;
164
+ font-weight: 600;
165
+ text-transform: uppercase;
166
+ letter-spacing: 0.05em;
167
+ color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
168
+ }
169
+
170
+ .serverInfo .infoValue {
171
+ font-size: 14px;
172
+ font-family: monospace;
173
+ color: ${cssManager.bdTheme('#111827', '#f9fafb')};
174
+ }
175
+ `,
176
+ ];
177
+ render() {
178
+ const status = this.vpnState.status;
179
+ const clients = this.vpnState.clients;
180
+ const connectedCount = status?.connectedClients ?? 0;
181
+ const totalClients = clients.length;
182
+ const enabledClients = clients.filter(c => c.enabled).length;
183
+ const statsTiles = [
184
+ {
185
+ id: 'totalClients',
186
+ title: 'Total Clients',
187
+ type: 'number',
188
+ value: totalClients,
189
+ icon: 'lucide:users',
190
+ description: 'Registered VPN clients',
191
+ color: '#3b82f6',
192
+ },
193
+ {
194
+ id: 'connectedClients',
195
+ title: 'Connected',
196
+ type: 'number',
197
+ value: connectedCount,
198
+ icon: 'lucide:link',
199
+ description: 'Currently connected',
200
+ color: '#10b981',
201
+ },
202
+ {
203
+ id: 'enabledClients',
204
+ title: 'Enabled',
205
+ type: 'number',
206
+ value: enabledClients,
207
+ icon: 'lucide:shieldCheck',
208
+ description: 'Active client registrations',
209
+ color: '#8b5cf6',
210
+ },
211
+ {
212
+ id: 'serverStatus',
213
+ title: 'Server',
214
+ type: 'text',
215
+ value: status?.running ? 'Running' : 'Stopped',
216
+ icon: 'lucide:server',
217
+ description: status?.running ? `${status.forwardingMode} mode` : 'VPN server not running',
218
+ color: status?.running ? '#10b981' : '#ef4444',
219
+ },
220
+ ];
221
+ return html `
222
+ <ops-sectionheading>VPN</ops-sectionheading>
223
+
224
+ ${this.vpnState.newClientConfig ? html `
225
+ <div class="configDialog">
226
+ <strong>Client created successfully!</strong>
227
+ <div class="warning">Copy the WireGuard config now. It contains private keys that won't be shown again.</div>
228
+ <pre>${this.vpnState.newClientConfig}</pre>
229
+ <dees-button
230
+ @click=${async () => {
231
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
232
+ await navigator.clipboard.writeText(this.vpnState.newClientConfig);
233
+ }
234
+ const { DeesToast } = await import('@design.estate/dees-catalog');
235
+ DeesToast.createAndShow({ message: 'Config copied to clipboard', type: 'success', duration: 3000 });
236
+ }}
237
+ >Copy to Clipboard</dees-button>
238
+ <dees-button
239
+ @click=${() => {
240
+ const blob = new Blob([this.vpnState.newClientConfig], { type: 'text/plain' });
241
+ const url = URL.createObjectURL(blob);
242
+ const a = document.createElement('a');
243
+ a.href = url;
244
+ a.download = 'wireguard.conf';
245
+ a.click();
246
+ URL.revokeObjectURL(url);
247
+ }}
248
+ >Download .conf</dees-button>
249
+ <dees-button
250
+ @click=${() => appstate.vpnStatePart.dispatchAction(appstate.clearNewClientConfigAction, null)}
251
+ >Dismiss</dees-button>
252
+ </div>
253
+ ` : ''}
254
+
255
+ <dees-statsgrid .statsTiles=${statsTiles}></dees-statsgrid>
256
+
257
+ ${status ? html `
258
+ <div class="serverInfo">
259
+ <div class="infoItem">
260
+ <span class="infoLabel">Subnet</span>
261
+ <span class="infoValue">${status.subnet}</span>
262
+ </div>
263
+ <div class="infoItem">
264
+ <span class="infoLabel">WireGuard Port</span>
265
+ <span class="infoValue">${status.wgListenPort}</span>
266
+ </div>
267
+ <div class="infoItem">
268
+ <span class="infoLabel">Forwarding Mode</span>
269
+ <span class="infoValue">${status.forwardingMode}</span>
270
+ </div>
271
+ ${status.serverPublicKeys ? html `
272
+ <div class="infoItem">
273
+ <span class="infoLabel">WG Public Key</span>
274
+ <span class="infoValue" style="font-size: 11px; word-break: break-all;">${status.serverPublicKeys.wgPublicKey}</span>
275
+ </div>
276
+ ` : ''}
277
+ </div>
278
+ ` : ''}
279
+
280
+ <dees-table
281
+ .heading1=${'VPN Clients'}
282
+ .heading2=${'Manage WireGuard and SmartVPN client registrations'}
283
+ .data=${clients}
284
+ .displayFunction=${(client) => ({
285
+ 'Client ID': client.clientId,
286
+ 'Status': client.enabled
287
+ ? html `<span class="statusBadge enabled">enabled</span>`
288
+ : html `<span class="statusBadge disabled">disabled</span>`,
289
+ 'VPN IP': client.assignedIp || '-',
290
+ 'Tags': client.tags?.length
291
+ ? html `${client.tags.map(t => html `<span class="tagBadge">${t}</span>`)}`
292
+ : '-',
293
+ 'Description': client.description || '-',
294
+ 'Created': new Date(client.createdAt).toLocaleDateString(),
295
+ })}
296
+ .dataActions=${[
297
+ {
298
+ name: 'Toggle',
299
+ iconName: 'lucide:power',
300
+ action: async (client) => {
301
+ await appstate.vpnStatePart.dispatchAction(appstate.toggleVpnClientAction, {
302
+ clientId: client.clientId,
303
+ enabled: !client.enabled,
304
+ });
305
+ },
306
+ },
307
+ {
308
+ name: 'Delete',
309
+ iconName: 'lucide:trash2',
310
+ action: async (client) => {
311
+ const { DeesModal } = await import('@design.estate/dees-catalog');
312
+ DeesModal.createAndShow({
313
+ heading: 'Delete VPN Client',
314
+ content: html `<p>Are you sure you want to delete client "${client.clientId}"?</p>`,
315
+ menuOptions: [
316
+ { name: 'Cancel', action: async (modal) => modal.destroy() },
317
+ {
318
+ name: 'Delete',
319
+ action: async (modal) => {
320
+ await appstate.vpnStatePart.dispatchAction(appstate.deleteVpnClientAction, client.clientId);
321
+ modal.destroy();
322
+ },
323
+ },
324
+ ],
325
+ });
326
+ },
327
+ },
328
+ ]}
329
+ .createNewItem=${async () => {
330
+ const { DeesModal, DeesForm, DeesInputText } = await import('@design.estate/dees-catalog');
331
+ DeesModal.createAndShow({
332
+ heading: 'Create VPN Client',
333
+ content: html `
334
+ <dees-form>
335
+ <dees-input-text id="clientId" .label=${'Client ID'} .key=${'clientId'} required></dees-input-text>
336
+ <dees-input-text id="description" .label=${'Description'} .key=${'description'}></dees-input-text>
337
+ <dees-input-text id="tags" .label=${'Tags (comma-separated)'} .key=${'tags'}></dees-input-text>
338
+ </dees-form>
339
+ `,
340
+ menuOptions: [
341
+ { name: 'Cancel', action: async (modal) => modal.destroy() },
342
+ {
343
+ name: 'Create',
344
+ action: async (modal) => {
345
+ const form = modal.shadowRoot.querySelector('dees-form');
346
+ const data = await form.collectFormData();
347
+ const tags = data.tags ? data.tags.split(',').map((t) => t.trim()).filter(Boolean) : undefined;
348
+ await appstate.vpnStatePart.dispatchAction(appstate.createVpnClientAction, {
349
+ clientId: data.clientId,
350
+ description: data.description || undefined,
351
+ tags,
352
+ });
353
+ modal.destroy();
354
+ },
355
+ },
356
+ ],
357
+ });
358
+ }}
359
+ ></dees-table>
360
+ `;
361
+ }
362
+ static {
363
+ __runInitializers(_classThis, _classExtraInitializers);
364
+ }
365
+ };
366
+ return OpsViewVpn = _classThis;
367
+ })();
368
+ export { OpsViewVpn };
369
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BzLXZpZXctdnBuLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHNfd2ViL2VsZW1lbnRzL29wcy12aWV3LXZwbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQUEsT0FBTyxFQUNMLFdBQVcsRUFDWCxJQUFJLEVBQ0osYUFBYSxFQUViLEdBQUcsRUFDSCxLQUFLLEVBQ0wsVUFBVSxHQUNYLE1BQU0sNkJBQTZCLENBQUM7QUFDckMsT0FBTyxLQUFLLFFBQVEsTUFBTSxnQkFBZ0IsQ0FBQztBQUMzQyxPQUFPLEtBQUssVUFBVSxNQUFNLG1DQUFtQyxDQUFDO0FBQ2hFLE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUM5QyxPQUFPLEVBQW1CLE1BQU0sNkJBQTZCLENBQUM7SUFTakQsVUFBVTs0QkFEdEIsYUFBYSxDQUFDLGNBQWMsQ0FBQzs7OztzQkFDRSxXQUFXOzs7OzBCQUFuQixTQUFRLFdBQVc7Ozs7b0NBQ3hDLEtBQUssRUFBRTtZQUNSLDZLQUFTLFFBQVEsNkJBQVIsUUFBUSwyRkFBeUQ7WUFGNUUsNktBb1RDOzs7O1FBbFRDLDZFQUF3QyxRQUFRLENBQUMsWUFBWSxDQUFDLFFBQVEsRUFBRyxFQUFDO1FBQTFFLElBQVMsUUFBUSw4Q0FBeUQ7UUFBMUUsSUFBUyxRQUFRLG9EQUF5RDtRQUUxRTtZQUNFLEtBQUssRUFBRSxDQUFDOztZQUNSLE1BQU0sR0FBRyxHQUFHLFFBQVEsQ0FBQyxZQUFZLENBQUMsTUFBTSxFQUFFLENBQUMsU0FBUyxDQUFDLENBQUMsUUFBUSxFQUFFLEVBQUU7Z0JBQ2hFLElBQUksQ0FBQyxRQUFRLEdBQUcsUUFBUSxDQUFDO1lBQzNCLENBQUMsQ0FBQyxDQUFDO1lBQ0gsSUFBSSxDQUFDLGVBQWUsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUM7U0FDaEM7UUFFRCxLQUFLLENBQUMsaUJBQWlCO1lBQ3JCLE1B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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@serve.zone/dcrouter",
3
3
  "private": false,
4
- "version": "11.12.4",
4
+ "version": "11.13.0",
5
5
  "description": "A multifaceted routing service handling mail and SMS delivery functions.",
6
6
  "type": "module",
7
7
  "exports": {
@@ -59,6 +59,7 @@
59
59
  "@push.rocks/smartrx": "^3.0.10",
60
60
  "@push.rocks/smartstate": "^2.3.0",
61
61
  "@push.rocks/smartunique": "^3.0.9",
62
+ "@push.rocks/smartvpn": "1.12.0",
62
63
  "@push.rocks/taskbuffer": "^8.0.2",
63
64
  "@serve.zone/catalog": "^2.9.0",
64
65
  "@serve.zone/interfaces": "^5.3.0",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '11.12.4',
6
+ version: '11.13.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -22,6 +22,7 @@ import { OpsServer } from './opsserver/index.js';
22
22
  import { MetricsManager } from './monitoring/index.js';
23
23
  import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
24
24
  import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
25
+ import { VpnManager, type IVpnManagerConfig } from './vpn/index.js';
25
26
  import { RouteConfigManager, ApiTokenManager } from './config/index.js';
26
27
  import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
27
28
  import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
@@ -188,6 +189,26 @@ export interface IDcRouterOptions {
188
189
  keyPath?: string;
189
190
  };
190
191
  };
192
+
193
+ /**
194
+ * VPN server configuration.
195
+ * Enables VPN-based access control: routes with vpn.required are only
196
+ * accessible from VPN clients. Supports WireGuard + native (WS/QUIC) transports.
197
+ */
198
+ vpnConfig?: {
199
+ /** Enable VPN server (default: false) */
200
+ enabled?: boolean;
201
+ /** VPN subnet CIDR (default: '10.8.0.0/24') */
202
+ subnet?: string;
203
+ /** WireGuard UDP listen port (default: 51820) */
204
+ wgListenPort?: number;
205
+ /** DNS servers pushed to VPN clients */
206
+ dns?: string[];
207
+ /** Server endpoint hostname for client configs (e.g. 'vpn.example.com') */
208
+ serverEndpoint?: string;
209
+ /** Override forwarding mode. Default: auto-detect (tun if root, socket otherwise) */
210
+ forwardingMode?: 'tun' | 'socket';
211
+ };
191
212
  }
192
213
 
193
214
  /**
@@ -226,6 +247,9 @@ export class DcRouter {
226
247
  public remoteIngressManager?: RemoteIngressManager;
227
248
  public tunnelManager?: TunnelManager;
228
249
 
250
+ // VPN
251
+ public vpnManager?: VpnManager;
252
+
229
253
  // Programmatic config API
230
254
  public routeConfigManager?: RouteConfigManager;
231
255
  public apiTokenManager?: ApiTokenManager;
@@ -429,6 +453,7 @@ export class DcRouter {
429
453
  () => this.getConstructorRoutes(),
430
454
  () => this.smartProxy,
431
455
  () => this.options.http3,
456
+ () => this.options.vpnConfig?.enabled ? (this.options.vpnConfig.subnet || '10.8.0.0/24') : undefined,
432
457
  );
433
458
  this.apiTokenManager = new ApiTokenManager(this.storageManager);
434
459
  await this.apiTokenManager.initialize();
@@ -533,6 +558,25 @@ export class DcRouter {
533
558
  );
534
559
  }
535
560
 
561
+ // VPN Server: optional, depends on SmartProxy
562
+ if (this.options.vpnConfig?.enabled) {
563
+ this.serviceManager.addService(
564
+ new plugins.taskbuffer.Service('VpnServer')
565
+ .optional()
566
+ .dependsOn('SmartProxy')
567
+ .withStart(async () => {
568
+ await this.setupVpnServer();
569
+ })
570
+ .withStop(async () => {
571
+ if (this.vpnManager) {
572
+ await this.vpnManager.stop();
573
+ this.vpnManager = undefined;
574
+ }
575
+ })
576
+ .withRetry({ maxRetries: 3, baseDelayMs: 2000, maxDelayMs: 30_000 }),
577
+ );
578
+ }
579
+
536
580
  // Wire up aggregated events for logging
537
581
  this.serviceSubjectSubscription = this.serviceManager.serviceSubject.subscribe((event) => {
538
582
  const level = event.type === 'failed' ? 'error' : event.type === 'retrying' ? 'warn' : 'info';
@@ -616,6 +660,15 @@ export class DcRouter {
616
660
  logger.log('info', `RADIUS Service: auth=${this.options.radiusConfig.authPort || 1812}, acct=${this.options.radiusConfig.acctPort || 1813}, clients=${this.options.radiusConfig.clients?.length || 0}, VLANs=${vlanStats.totalMappings}, accounting=${this.options.radiusConfig.accounting?.enabled ? 'enabled' : 'disabled'}`);
617
661
  }
618
662
 
663
+ // VPN summary
664
+ if (this.vpnManager && this.options.vpnConfig?.enabled) {
665
+ const subnet = this.vpnManager.getSubnet();
666
+ const wgPort = this.options.vpnConfig.wgListenPort ?? 51820;
667
+ const mode = this.vpnManager.forwardingMode;
668
+ const clientCount = this.vpnManager.listClients().length;
669
+ logger.log('info', `VPN Service: mode=${mode}, subnet=${subnet}, wg=:${wgPort}, clients=${clientCount}`);
670
+ }
671
+
619
672
  // Remote Ingress summary
620
673
  if (this.tunnelManager && this.options.remoteIngressConfig?.enabled) {
621
674
  const edgeCount = this.remoteIngressManager?.getAllEdges().length || 0;
@@ -741,6 +794,11 @@ export class DcRouter {
741
794
  logger.log('info', 'HTTP/3: Augmented qualifying HTTPS routes with QUIC/H3 configuration');
742
795
  }
743
796
 
797
+ // VPN route security injection: restrict vpn.required routes to VPN subnet
798
+ if (this.options.vpnConfig?.enabled) {
799
+ routes = this.injectVpnSecurity(routes);
800
+ }
801
+
744
802
  // Cache constructor routes for RouteConfigManager
745
803
  this.constructorRoutes = [...routes];
746
804
 
@@ -892,6 +950,22 @@ export class DcRouter {
892
950
  smartProxyConfig.proxyIPs = ['127.0.0.1'];
893
951
  }
894
952
 
953
+ // When VPN is in socket mode, the userspace NAT engine sends PP v2 headers
954
+ // on outbound connections to SmartProxy to preserve VPN client tunnel IPs.
955
+ if (this.options.vpnConfig?.enabled) {
956
+ const vpnForwardingMode = this.options.vpnConfig.forwardingMode
957
+ ?? (process.getuid?.() === 0 ? 'tun' : 'socket');
958
+ if (vpnForwardingMode === 'socket') {
959
+ smartProxyConfig.acceptProxyProtocol = true;
960
+ if (!smartProxyConfig.proxyIPs) {
961
+ smartProxyConfig.proxyIPs = [];
962
+ }
963
+ if (!smartProxyConfig.proxyIPs.includes('127.0.0.1')) {
964
+ smartProxyConfig.proxyIPs.push('127.0.0.1');
965
+ }
966
+ }
967
+ }
968
+
895
969
  // Create SmartProxy instance
896
970
  logger.log('info', `Creating SmartProxy instance: routes=${smartProxyConfig.routes?.length}, acme=${smartProxyConfig.acme?.enabled}, certProvisionFunction=${!!smartProxyConfig.certProvisionFunction}`);
897
971
 
@@ -1996,6 +2070,58 @@ export class DcRouter {
1996
2070
  logger.log('info', `Remote Ingress hub started on port ${this.options.remoteIngressConfig.tunnelPort || 8443} with ${edgeCount} registered edge(s)`);
1997
2071
  }
1998
2072
 
2073
+ /**
2074
+ * Set up VPN server for VPN-based route access control.
2075
+ */
2076
+ private async setupVpnServer(): Promise<void> {
2077
+ if (!this.options.vpnConfig?.enabled) {
2078
+ return;
2079
+ }
2080
+
2081
+ logger.log('info', 'Setting up VPN server...');
2082
+
2083
+ this.vpnManager = new VpnManager(this.storageManager, {
2084
+ subnet: this.options.vpnConfig.subnet,
2085
+ wgListenPort: this.options.vpnConfig.wgListenPort,
2086
+ dns: this.options.vpnConfig.dns,
2087
+ serverEndpoint: this.options.vpnConfig.serverEndpoint,
2088
+ forwardingMode: this.options.vpnConfig.forwardingMode,
2089
+ });
2090
+
2091
+ await this.vpnManager.start();
2092
+ }
2093
+
2094
+ /**
2095
+ * Inject VPN security into routes that have vpn.required === true.
2096
+ * Adds the VPN subnet to security.ipAllowList so only VPN clients can access them.
2097
+ */
2098
+ private injectVpnSecurity(routes: plugins.smartproxy.IRouteConfig[]): plugins.smartproxy.IRouteConfig[] {
2099
+ const vpnSubnet = this.options.vpnConfig?.subnet || '10.8.0.0/24';
2100
+ let injectedCount = 0;
2101
+
2102
+ const result = routes.map((route) => {
2103
+ const dcrouterRoute = route as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig;
2104
+ if (dcrouterRoute.vpn?.required) {
2105
+ injectedCount++;
2106
+ const existing = route.security?.ipAllowList || [];
2107
+ return {
2108
+ ...route,
2109
+ security: {
2110
+ ...route.security,
2111
+ ipAllowList: [...existing, vpnSubnet],
2112
+ },
2113
+ };
2114
+ }
2115
+ return route;
2116
+ });
2117
+
2118
+ if (injectedCount > 0) {
2119
+ logger.log('info', `VPN: Injected ipAllowList (${vpnSubnet}) into ${injectedCount} VPN-protected route(s)`);
2120
+ }
2121
+
2122
+ return result;
2123
+ }
2124
+
1999
2125
  /**
2000
2126
  * Set up RADIUS server for network authentication
2001
2127
  */
@@ -7,6 +7,7 @@ import type {
7
7
  IMergedRoute,
8
8
  IRouteWarning,
9
9
  } from '../../ts_interfaces/data/route-management.js';
10
+ import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
10
11
  import { type IHttp3Config, augmentRouteWithHttp3 } from '../http3/index.js';
11
12
 
12
13
  const ROUTES_PREFIX = '/config-api/routes/';
@@ -22,6 +23,7 @@ export class RouteConfigManager {
22
23
  private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
23
24
  private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
24
25
  private getHttp3Config?: () => IHttp3Config | undefined,
26
+ private getVpnSubnet?: () => string | undefined,
25
27
  ) {}
26
28
 
27
29
  /**
@@ -262,13 +264,28 @@ export class RouteConfigManager {
262
264
 
263
265
  // Add enabled programmatic routes (with HTTP/3 augmentation if enabled)
264
266
  const http3Config = this.getHttp3Config?.();
267
+ const vpnSubnet = this.getVpnSubnet?.();
265
268
  for (const stored of this.storedRoutes.values()) {
266
269
  if (stored.enabled) {
270
+ let route = stored.route;
267
271
  if (http3Config && http3Config.enabled !== false) {
268
- enabledRoutes.push(augmentRouteWithHttp3(stored.route, { enabled: true, ...http3Config }));
269
- } else {
270
- enabledRoutes.push(stored.route);
272
+ route = augmentRouteWithHttp3(route, { enabled: true, ...http3Config });
271
273
  }
274
+ // Inject VPN security for programmatic routes with vpn.required
275
+ if (vpnSubnet) {
276
+ const dcRoute = route as IDcRouterRouteConfig;
277
+ if (dcRoute.vpn?.required) {
278
+ const existing = route.security?.ipAllowList || [];
279
+ route = {
280
+ ...route,
281
+ security: {
282
+ ...route.security,
283
+ ipAllowList: [...existing, vpnSubnet],
284
+ },
285
+ };
286
+ }
287
+ }
288
+ enabledRoutes.push(route);
272
289
  }
273
290
  }
274
291
 
@@ -28,6 +28,7 @@ export class OpsServer {
28
28
  private remoteIngressHandler!: handlers.RemoteIngressHandler;
29
29
  private routeManagementHandler!: handlers.RouteManagementHandler;
30
30
  private apiTokenHandler!: handlers.ApiTokenHandler;
31
+ private vpnHandler!: handlers.VpnHandler;
31
32
 
32
33
  constructor(dcRouterRefArg: DcRouter) {
33
34
  this.dcRouterRef = dcRouterRefArg;
@@ -86,6 +87,7 @@ export class OpsServer {
86
87
  this.remoteIngressHandler = new handlers.RemoteIngressHandler(this);
87
88
  this.routeManagementHandler = new handlers.RouteManagementHandler(this);
88
89
  this.apiTokenHandler = new handlers.ApiTokenHandler(this);
90
+ this.vpnHandler = new handlers.VpnHandler(this);
89
91
 
90
92
  console.log('✅ OpsServer TypedRequest handlers initialized');
91
93
  }
@@ -8,4 +8,5 @@ export * from './email-ops.handler.js';
8
8
  export * from './certificate.handler.js';
9
9
  export * from './remoteingress.handler.js';
10
10
  export * from './route-management.handler.js';
11
- export * from './api-token.handler.js';
11
+ export * from './api-token.handler.js';
12
+ export * from './vpn.handler.js';