@serve.zone/dcrouter 11.12.4 → 11.14.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 +705 -548
- 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 +10 -1
- package/dist_ts_interfaces/data/vpn.d.ts +43 -0
- package/dist_ts_interfaces/data/vpn.js +2 -0
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_interfaces/requests/vpn.d.ts +135 -0
- package/dist_ts_interfaces/requests/vpn.js +3 -0
- package/package.json +2 -1
- package/readme.md +107 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +126 -0
- package/ts/config/classes.route-config-manager.ts +20 -3
- package/ts/opsserver/classes.opsserver.ts +2 -0
- package/ts/opsserver/handlers/index.ts +2 -1
- package/ts/opsserver/handlers/vpn.handler.ts +257 -0
- package/ts/plugins.ts +2 -1
- package/ts/vpn/classes.vpn-manager.ts +378 -0
- package/ts/vpn/index.ts +1 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +164 -0
- package/ts_web/elements/index.ts +1 -0
- package/ts_web/elements/ops-dashboard.ts +6 -0
- package/ts_web/elements/ops-view-vpn.ts +330 -0
- package/ts_web/readme.md +17 -0
- package/ts_web/router.ts +1 -1
- package/dist_ts/00_commitinfo_data.d.ts +0 -8
- package/dist_ts/00_commitinfo_data.js +0 -9
- package/dist_ts/cache/classes.cache.cleaner.d.ts +0 -47
- package/dist_ts/cache/classes.cache.cleaner.js +0 -130
- package/dist_ts/cache/classes.cached.document.d.ts +0 -76
- package/dist_ts/cache/classes.cached.document.js +0 -100
- package/dist_ts/cache/classes.cachedb.d.ts +0 -60
- package/dist_ts/cache/classes.cachedb.js +0 -126
- package/dist_ts/cache/documents/classes.cached.email.d.ts +0 -125
- package/dist_ts/cache/documents/classes.cached.email.js +0 -337
- package/dist_ts/cache/documents/classes.cached.ip.reputation.d.ts +0 -119
- package/dist_ts/cache/documents/classes.cached.ip.reputation.js +0 -323
- package/dist_ts/cache/documents/index.d.ts +0 -2
- package/dist_ts/cache/documents/index.js +0 -3
- package/dist_ts/cache/index.d.ts +0 -4
- package/dist_ts/cache/index.js +0 -7
- package/dist_ts/classes.cert-provision-scheduler.d.ts +0 -54
- package/dist_ts/classes.cert-provision-scheduler.js +0 -118
- package/dist_ts/classes.dcrouter.d.ts +0 -356
- package/dist_ts/classes.dcrouter.js +0 -1592
- package/dist_ts/classes.storage-cert-manager.d.ts +0 -18
- package/dist_ts/classes.storage-cert-manager.js +0 -43
- package/dist_ts/config/classes.api-token-manager.d.ts +0 -46
- package/dist_ts/config/classes.api-token-manager.js +0 -150
- package/dist_ts/config/classes.route-config-manager.d.ts +0 -37
- package/dist_ts/config/classes.route-config-manager.js +0 -240
- package/dist_ts/config/index.d.ts +0 -3
- package/dist_ts/config/index.js +0 -5
- package/dist_ts/config/validator.d.ts +0 -104
- package/dist_ts/config/validator.js +0 -152
- package/dist_ts/errors/base.errors.d.ts +0 -224
- package/dist_ts/errors/base.errors.js +0 -320
- package/dist_ts/errors/error-handler.d.ts +0 -98
- package/dist_ts/errors/error-handler.js +0 -282
- package/dist_ts/errors/error.codes.d.ts +0 -115
- package/dist_ts/errors/error.codes.js +0 -136
- package/dist_ts/errors/index.d.ts +0 -54
- package/dist_ts/errors/index.js +0 -136
- package/dist_ts/errors/reputation.errors.d.ts +0 -183
- package/dist_ts/errors/reputation.errors.js +0 -292
- package/dist_ts/http3/http3-route-augmentation.d.ts +0 -50
- package/dist_ts/http3/http3-route-augmentation.js +0 -98
- package/dist_ts/http3/index.d.ts +0 -1
- package/dist_ts/http3/index.js +0 -2
- package/dist_ts/index.d.ts +0 -8
- package/dist_ts/index.js +0 -29
- package/dist_ts/logger.d.ts +0 -21
- package/dist_ts/logger.js +0 -81
- package/dist_ts/monitoring/classes.metricscache.d.ts +0 -32
- package/dist_ts/monitoring/classes.metricscache.js +0 -63
- package/dist_ts/monitoring/classes.metricsmanager.d.ts +0 -184
- package/dist_ts/monitoring/classes.metricsmanager.js +0 -744
- package/dist_ts/monitoring/index.d.ts +0 -1
- package/dist_ts/monitoring/index.js +0 -2
- package/dist_ts/opsserver/classes.opsserver.d.ts +0 -37
- package/dist_ts/opsserver/classes.opsserver.js +0 -85
- package/dist_ts/opsserver/handlers/admin.handler.d.ts +0 -31
- package/dist_ts/opsserver/handlers/admin.handler.js +0 -180
- package/dist_ts/opsserver/handlers/api-token.handler.d.ts +0 -6
- package/dist_ts/opsserver/handlers/api-token.handler.js +0 -62
- package/dist_ts/opsserver/handlers/certificate.handler.d.ts +0 -32
- package/dist_ts/opsserver/handlers/certificate.handler.js +0 -421
- package/dist_ts/opsserver/handlers/config.handler.d.ts +0 -7
- package/dist_ts/opsserver/handlers/config.handler.js +0 -192
- package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +0 -30
- package/dist_ts/opsserver/handlers/email-ops.handler.js +0 -227
- package/dist_ts/opsserver/handlers/index.d.ts +0 -11
- package/dist_ts/opsserver/handlers/index.js +0 -12
- package/dist_ts/opsserver/handlers/logs.handler.d.ts +0 -25
- package/dist_ts/opsserver/handlers/logs.handler.js +0 -256
- package/dist_ts/opsserver/handlers/radius.handler.d.ts +0 -6
- package/dist_ts/opsserver/handlers/radius.handler.js +0 -295
- package/dist_ts/opsserver/handlers/remoteingress.handler.d.ts +0 -6
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +0 -156
- package/dist_ts/opsserver/handlers/route-management.handler.d.ts +0 -14
- package/dist_ts/opsserver/handlers/route-management.handler.js +0 -117
- package/dist_ts/opsserver/handlers/security.handler.d.ts +0 -9
- package/dist_ts/opsserver/handlers/security.handler.js +0 -233
- package/dist_ts/opsserver/handlers/stats.handler.d.ts +0 -11
- package/dist_ts/opsserver/handlers/stats.handler.js +0 -403
- package/dist_ts/opsserver/helpers/guards.d.ts +0 -27
- package/dist_ts/opsserver/helpers/guards.js +0 -43
- package/dist_ts/opsserver/index.d.ts +0 -1
- package/dist_ts/opsserver/index.js +0 -2
- package/dist_ts/paths.d.ts +0 -26
- package/dist_ts/paths.js +0 -45
- package/dist_ts/plugins.d.ts +0 -80
- package/dist_ts/plugins.js +0 -114
- package/dist_ts/radius/classes.accounting.manager.d.ts +0 -231
- package/dist_ts/radius/classes.accounting.manager.js +0 -462
- package/dist_ts/radius/classes.radius.server.d.ts +0 -171
- package/dist_ts/radius/classes.radius.server.js +0 -386
- package/dist_ts/radius/classes.vlan.manager.d.ts +0 -128
- package/dist_ts/radius/classes.vlan.manager.js +0 -279
- package/dist_ts/radius/index.d.ts +0 -13
- package/dist_ts/radius/index.js +0 -14
- package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +0 -94
- package/dist_ts/remoteingress/classes.remoteingress-manager.js +0 -271
- package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +0 -59
- package/dist_ts/remoteingress/classes.tunnel-manager.js +0 -165
- package/dist_ts/remoteingress/index.d.ts +0 -2
- package/dist_ts/remoteingress/index.js +0 -3
- package/dist_ts/security/classes.contentscanner.d.ts +0 -164
- package/dist_ts/security/classes.contentscanner.js +0 -642
- package/dist_ts/security/classes.ipreputationchecker.d.ts +0 -160
- package/dist_ts/security/classes.ipreputationchecker.js +0 -537
- package/dist_ts/security/classes.securitylogger.d.ts +0 -144
- package/dist_ts/security/classes.securitylogger.js +0 -235
- package/dist_ts/security/index.d.ts +0 -3
- package/dist_ts/security/index.js +0 -4
- package/dist_ts/sms/classes.smsservice.d.ts +0 -15
- package/dist_ts/sms/classes.smsservice.js +0 -72
- package/dist_ts/sms/config/sms.config.d.ts +0 -93
- package/dist_ts/sms/config/sms.config.js +0 -2
- package/dist_ts/sms/config/sms.schema.d.ts +0 -5
- package/dist_ts/sms/config/sms.schema.js +0 -121
- package/dist_ts/sms/index.d.ts +0 -1
- package/dist_ts/sms/index.js +0 -2
- package/dist_ts/storage/classes.storagemanager.d.ts +0 -83
- package/dist_ts/storage/classes.storagemanager.js +0 -348
- package/dist_ts/storage/index.d.ts +0 -1
- package/dist_ts/storage/index.js +0 -3
- package/dist_ts_apiclient/classes.apitoken.d.ts +0 -41
- package/dist_ts_apiclient/classes.apitoken.js +0 -115
- package/dist_ts_apiclient/classes.certificate.d.ts +0 -57
- package/dist_ts_apiclient/classes.certificate.js +0 -69
- package/dist_ts_apiclient/classes.config.d.ts +0 -7
- package/dist_ts_apiclient/classes.config.js +0 -11
- package/dist_ts_apiclient/classes.dcrouterapiclient.d.ts +0 -41
- package/dist_ts_apiclient/classes.dcrouterapiclient.js +0 -81
- package/dist_ts_apiclient/classes.email.d.ts +0 -30
- package/dist_ts_apiclient/classes.email.js +0 -52
- package/dist_ts_apiclient/classes.logs.d.ts +0 -21
- package/dist_ts_apiclient/classes.logs.js +0 -14
- package/dist_ts_apiclient/classes.radius.d.ts +0 -59
- package/dist_ts_apiclient/classes.radius.js +0 -95
- package/dist_ts_apiclient/classes.remoteingress.d.ts +0 -54
- package/dist_ts_apiclient/classes.remoteingress.js +0 -136
- package/dist_ts_apiclient/classes.route.d.ts +0 -42
- package/dist_ts_apiclient/classes.route.js +0 -154
- package/dist_ts_apiclient/classes.stats.d.ts +0 -47
- package/dist_ts_apiclient/classes.stats.js +0 -38
- package/dist_ts_apiclient/index.d.ts +0 -10
- package/dist_ts_apiclient/index.js +0 -14
- package/dist_ts_apiclient/plugins.d.ts +0 -3
- package/dist_ts_apiclient/plugins.js +0 -5
- package/dist_ts_web/00_commitinfo_data.d.ts +0 -8
- package/dist_ts_web/00_commitinfo_data.js +0 -9
- package/dist_ts_web/appstate.d.ts +0 -216
- package/dist_ts_web/appstate.js +0 -1064
- package/dist_ts_web/elements/index.d.ts +0 -12
- package/dist_ts_web/elements/index.js +0 -13
- package/dist_ts_web/elements/ops-dashboard.d.ts +0 -23
- package/dist_ts_web/elements/ops-dashboard.js +0 -317
- package/dist_ts_web/elements/ops-view-apitokens.d.ts +0 -13
- package/dist_ts_web/elements/ops-view-apitokens.js +0 -371
- package/dist_ts_web/elements/ops-view-certificates.d.ts +0 -22
- package/dist_ts_web/elements/ops-view-certificates.js +0 -528
- package/dist_ts_web/elements/ops-view-config.d.ts +0 -19
- package/dist_ts_web/elements/ops-view-config.js +0 -339
- package/dist_ts_web/elements/ops-view-emails.d.ts +0 -21
- package/dist_ts_web/elements/ops-view-emails.js +0 -165
- package/dist_ts_web/elements/ops-view-logs.d.ts +0 -13
- package/dist_ts_web/elements/ops-view-logs.js +0 -159
- package/dist_ts_web/elements/ops-view-network.d.ts +0 -71
- package/dist_ts_web/elements/ops-view-network.js +0 -764
- package/dist_ts_web/elements/ops-view-overview.d.ts +0 -22
- package/dist_ts_web/elements/ops-view-overview.js +0 -456
- package/dist_ts_web/elements/ops-view-remoteingress.d.ts +0 -20
- package/dist_ts_web/elements/ops-view-remoteingress.js +0 -494
- package/dist_ts_web/elements/ops-view-routes.d.ts +0 -12
- package/dist_ts_web/elements/ops-view-routes.js +0 -404
- package/dist_ts_web/elements/ops-view-security.d.ts +0 -21
- package/dist_ts_web/elements/ops-view-security.js +0 -574
- package/dist_ts_web/elements/shared/css.d.ts +0 -1
- package/dist_ts_web/elements/shared/css.js +0 -10
- package/dist_ts_web/elements/shared/index.d.ts +0 -2
- package/dist_ts_web/elements/shared/index.js +0 -3
- package/dist_ts_web/elements/shared/ops-sectionheading.d.ts +0 -5
- package/dist_ts_web/elements/shared/ops-sectionheading.js +0 -82
- package/dist_ts_web/index.d.ts +0 -1
- package/dist_ts_web/index.js +0 -10
- package/dist_ts_web/plugins.d.ts +0 -6
- package/dist_ts_web/plugins.js +0 -11
- package/dist_ts_web/router.d.ts +0 -19
- package/dist_ts_web/router.js +0 -91
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
3
|
+
import type { StorageManager } from '../storage/classes.storagemanager.js';
|
|
4
|
+
|
|
5
|
+
const STORAGE_PREFIX_KEYS = '/vpn/server-keys';
|
|
6
|
+
const STORAGE_PREFIX_CLIENTS = '/vpn/clients/';
|
|
7
|
+
|
|
8
|
+
export interface IVpnManagerConfig {
|
|
9
|
+
/** VPN subnet CIDR (default: '10.8.0.0/24') */
|
|
10
|
+
subnet?: string;
|
|
11
|
+
/** WireGuard UDP listen port (default: 51820) */
|
|
12
|
+
wgListenPort?: number;
|
|
13
|
+
/** DNS servers pushed to VPN clients */
|
|
14
|
+
dns?: string[];
|
|
15
|
+
/** Server endpoint hostname for client configs (e.g. 'vpn.example.com') */
|
|
16
|
+
serverEndpoint?: string;
|
|
17
|
+
/** Override forwarding mode. Default: auto-detect (tun if root, socket otherwise) */
|
|
18
|
+
forwardingMode?: 'tun' | 'socket';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface IPersistedServerKeys {
|
|
22
|
+
noisePrivateKey: string;
|
|
23
|
+
noisePublicKey: string;
|
|
24
|
+
wgPrivateKey: string;
|
|
25
|
+
wgPublicKey: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface IPersistedClient {
|
|
29
|
+
clientId: string;
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
tags?: string[];
|
|
32
|
+
description?: string;
|
|
33
|
+
assignedIp?: string;
|
|
34
|
+
noisePublicKey: string;
|
|
35
|
+
wgPublicKey: string;
|
|
36
|
+
createdAt: number;
|
|
37
|
+
updatedAt: number;
|
|
38
|
+
expiresAt?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Manages the SmartVPN server lifecycle and VPN client CRUD.
|
|
43
|
+
* Persists server keys and client registrations via StorageManager.
|
|
44
|
+
*/
|
|
45
|
+
export class VpnManager {
|
|
46
|
+
private storageManager: StorageManager;
|
|
47
|
+
private config: IVpnManagerConfig;
|
|
48
|
+
private vpnServer?: plugins.smartvpn.VpnServer;
|
|
49
|
+
private clients: Map<string, IPersistedClient> = new Map();
|
|
50
|
+
private serverKeys?: IPersistedServerKeys;
|
|
51
|
+
private _forwardingMode: 'tun' | 'socket';
|
|
52
|
+
|
|
53
|
+
constructor(storageManager: StorageManager, config: IVpnManagerConfig) {
|
|
54
|
+
this.storageManager = storageManager;
|
|
55
|
+
this.config = config;
|
|
56
|
+
// Auto-detect forwarding mode: tun if root, socket otherwise
|
|
57
|
+
this._forwardingMode = config.forwardingMode
|
|
58
|
+
?? (process.getuid?.() === 0 ? 'tun' : 'socket');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The effective forwarding mode (tun or socket). */
|
|
62
|
+
public get forwardingMode(): 'tun' | 'socket' {
|
|
63
|
+
return this._forwardingMode;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The VPN subnet CIDR. */
|
|
67
|
+
public getSubnet(): string {
|
|
68
|
+
return this.config.subnet || '10.8.0.0/24';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Whether the VPN server is running. */
|
|
72
|
+
public get running(): boolean {
|
|
73
|
+
return this.vpnServer?.running ?? false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Start the VPN server.
|
|
78
|
+
* Loads or generates server keys, loads persisted clients, starts VpnServer.
|
|
79
|
+
*/
|
|
80
|
+
public async start(): Promise<void> {
|
|
81
|
+
// Load or generate server keys
|
|
82
|
+
this.serverKeys = await this.loadOrGenerateServerKeys();
|
|
83
|
+
|
|
84
|
+
// Load persisted clients
|
|
85
|
+
await this.loadPersistedClients();
|
|
86
|
+
|
|
87
|
+
// Build client entries for the daemon
|
|
88
|
+
const clientEntries: plugins.smartvpn.IClientEntry[] = [];
|
|
89
|
+
for (const client of this.clients.values()) {
|
|
90
|
+
clientEntries.push({
|
|
91
|
+
clientId: client.clientId,
|
|
92
|
+
publicKey: client.noisePublicKey,
|
|
93
|
+
wgPublicKey: client.wgPublicKey,
|
|
94
|
+
enabled: client.enabled,
|
|
95
|
+
tags: client.tags,
|
|
96
|
+
description: client.description,
|
|
97
|
+
assignedIp: client.assignedIp,
|
|
98
|
+
expiresAt: client.expiresAt,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const subnet = this.getSubnet();
|
|
103
|
+
const wgListenPort = this.config.wgListenPort ?? 51820;
|
|
104
|
+
|
|
105
|
+
// Create and start VpnServer
|
|
106
|
+
this.vpnServer = new plugins.smartvpn.VpnServer({
|
|
107
|
+
transport: { transport: 'stdio' },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const serverConfig: plugins.smartvpn.IVpnServerConfig = {
|
|
111
|
+
listenAddr: '0.0.0.0:0', // WS listener not strictly needed but required field
|
|
112
|
+
privateKey: this.serverKeys.noisePrivateKey,
|
|
113
|
+
publicKey: this.serverKeys.noisePublicKey,
|
|
114
|
+
subnet,
|
|
115
|
+
dns: this.config.dns,
|
|
116
|
+
forwardingMode: this._forwardingMode,
|
|
117
|
+
transportMode: 'all',
|
|
118
|
+
wgPrivateKey: this.serverKeys.wgPrivateKey,
|
|
119
|
+
wgListenPort,
|
|
120
|
+
clients: clientEntries,
|
|
121
|
+
socketForwardProxyProtocol: this._forwardingMode === 'socket',
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
await this.vpnServer.start(serverConfig);
|
|
125
|
+
logger.log('info', `VPN server started: mode=${this._forwardingMode}, subnet=${subnet}, wg=:${wgListenPort}, clients=${this.clients.size}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Stop the VPN server.
|
|
130
|
+
*/
|
|
131
|
+
public async stop(): Promise<void> {
|
|
132
|
+
if (this.vpnServer) {
|
|
133
|
+
try {
|
|
134
|
+
await this.vpnServer.stopServer();
|
|
135
|
+
} catch {
|
|
136
|
+
// Ignore stop errors
|
|
137
|
+
}
|
|
138
|
+
this.vpnServer.stop();
|
|
139
|
+
this.vpnServer = undefined;
|
|
140
|
+
}
|
|
141
|
+
logger.log('info', 'VPN server stopped');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Client CRUD ────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create a new VPN client. Returns the config bundle (secrets only shown once).
|
|
148
|
+
*/
|
|
149
|
+
public async createClient(opts: {
|
|
150
|
+
clientId: string;
|
|
151
|
+
tags?: string[];
|
|
152
|
+
description?: string;
|
|
153
|
+
}): Promise<plugins.smartvpn.IClientConfigBundle> {
|
|
154
|
+
if (!this.vpnServer) {
|
|
155
|
+
throw new Error('VPN server not running');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const bundle = await this.vpnServer.createClient({
|
|
159
|
+
clientId: opts.clientId,
|
|
160
|
+
tags: opts.tags,
|
|
161
|
+
description: opts.description,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Update WireGuard config endpoint if serverEndpoint is configured
|
|
165
|
+
if (this.config.serverEndpoint && bundle.wireguardConfig) {
|
|
166
|
+
const wgPort = this.config.wgListenPort ?? 51820;
|
|
167
|
+
bundle.wireguardConfig = bundle.wireguardConfig.replace(
|
|
168
|
+
/Endpoint\s*=\s*.+/,
|
|
169
|
+
`Endpoint = ${this.config.serverEndpoint}:${wgPort}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Persist client entry (without private keys)
|
|
174
|
+
const persisted: IPersistedClient = {
|
|
175
|
+
clientId: bundle.entry.clientId,
|
|
176
|
+
enabled: bundle.entry.enabled ?? true,
|
|
177
|
+
tags: bundle.entry.tags,
|
|
178
|
+
description: bundle.entry.description,
|
|
179
|
+
assignedIp: bundle.entry.assignedIp,
|
|
180
|
+
noisePublicKey: bundle.entry.publicKey,
|
|
181
|
+
wgPublicKey: bundle.entry.wgPublicKey || '',
|
|
182
|
+
createdAt: Date.now(),
|
|
183
|
+
updatedAt: Date.now(),
|
|
184
|
+
expiresAt: bundle.entry.expiresAt,
|
|
185
|
+
};
|
|
186
|
+
this.clients.set(persisted.clientId, persisted);
|
|
187
|
+
await this.persistClient(persisted);
|
|
188
|
+
|
|
189
|
+
return bundle;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Remove a VPN client.
|
|
194
|
+
*/
|
|
195
|
+
public async removeClient(clientId: string): Promise<void> {
|
|
196
|
+
if (!this.vpnServer) {
|
|
197
|
+
throw new Error('VPN server not running');
|
|
198
|
+
}
|
|
199
|
+
await this.vpnServer.removeClient(clientId);
|
|
200
|
+
this.clients.delete(clientId);
|
|
201
|
+
await this.storageManager.delete(`${STORAGE_PREFIX_CLIENTS}${clientId}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* List all registered clients (without secrets).
|
|
206
|
+
*/
|
|
207
|
+
public listClients(): IPersistedClient[] {
|
|
208
|
+
return [...this.clients.values()];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Enable a client.
|
|
213
|
+
*/
|
|
214
|
+
public async enableClient(clientId: string): Promise<void> {
|
|
215
|
+
if (!this.vpnServer) throw new Error('VPN server not running');
|
|
216
|
+
await this.vpnServer.enableClient(clientId);
|
|
217
|
+
const client = this.clients.get(clientId);
|
|
218
|
+
if (client) {
|
|
219
|
+
client.enabled = true;
|
|
220
|
+
client.updatedAt = Date.now();
|
|
221
|
+
await this.persistClient(client);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Disable a client.
|
|
227
|
+
*/
|
|
228
|
+
public async disableClient(clientId: string): Promise<void> {
|
|
229
|
+
if (!this.vpnServer) throw new Error('VPN server not running');
|
|
230
|
+
await this.vpnServer.disableClient(clientId);
|
|
231
|
+
const client = this.clients.get(clientId);
|
|
232
|
+
if (client) {
|
|
233
|
+
client.enabled = false;
|
|
234
|
+
client.updatedAt = Date.now();
|
|
235
|
+
await this.persistClient(client);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Rotate a client's keys. Returns the new config bundle.
|
|
241
|
+
*/
|
|
242
|
+
public async rotateClientKey(clientId: string): Promise<plugins.smartvpn.IClientConfigBundle> {
|
|
243
|
+
if (!this.vpnServer) throw new Error('VPN server not running');
|
|
244
|
+
const bundle = await this.vpnServer.rotateClientKey(clientId);
|
|
245
|
+
|
|
246
|
+
// Update endpoint in WireGuard config
|
|
247
|
+
if (this.config.serverEndpoint && bundle.wireguardConfig) {
|
|
248
|
+
const wgPort = this.config.wgListenPort ?? 51820;
|
|
249
|
+
bundle.wireguardConfig = bundle.wireguardConfig.replace(
|
|
250
|
+
/Endpoint\s*=\s*.+/,
|
|
251
|
+
`Endpoint = ${this.config.serverEndpoint}:${wgPort}`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Update persisted entry with new public keys
|
|
256
|
+
const client = this.clients.get(clientId);
|
|
257
|
+
if (client) {
|
|
258
|
+
client.noisePublicKey = bundle.entry.publicKey;
|
|
259
|
+
client.wgPublicKey = bundle.entry.wgPublicKey || '';
|
|
260
|
+
client.updatedAt = Date.now();
|
|
261
|
+
await this.persistClient(client);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return bundle;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Export a client config (without secrets).
|
|
269
|
+
*/
|
|
270
|
+
public async exportClientConfig(clientId: string, format: 'smartvpn' | 'wireguard'): Promise<string> {
|
|
271
|
+
if (!this.vpnServer) throw new Error('VPN server not running');
|
|
272
|
+
let config = await this.vpnServer.exportClientConfig(clientId, format);
|
|
273
|
+
|
|
274
|
+
// Update endpoint in WireGuard config
|
|
275
|
+
if (format === 'wireguard' && this.config.serverEndpoint) {
|
|
276
|
+
const wgPort = this.config.wgListenPort ?? 51820;
|
|
277
|
+
config = config.replace(
|
|
278
|
+
/Endpoint\s*=\s*.+/,
|
|
279
|
+
`Endpoint = ${this.config.serverEndpoint}:${wgPort}`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return config;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── Status and telemetry ───────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get server status.
|
|
290
|
+
*/
|
|
291
|
+
public async getStatus(): Promise<plugins.smartvpn.IVpnStatus | null> {
|
|
292
|
+
if (!this.vpnServer) return null;
|
|
293
|
+
return this.vpnServer.getStatus();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get server statistics.
|
|
298
|
+
*/
|
|
299
|
+
public async getStatistics(): Promise<plugins.smartvpn.IVpnServerStatistics | null> {
|
|
300
|
+
if (!this.vpnServer) return null;
|
|
301
|
+
return this.vpnServer.getStatistics();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* List currently connected clients.
|
|
306
|
+
*/
|
|
307
|
+
public async getConnectedClients(): Promise<plugins.smartvpn.IVpnClientInfo[]> {
|
|
308
|
+
if (!this.vpnServer) return [];
|
|
309
|
+
return this.vpnServer.listClients();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get telemetry for a specific client.
|
|
314
|
+
*/
|
|
315
|
+
public async getClientTelemetry(clientId: string): Promise<plugins.smartvpn.IVpnClientTelemetry | null> {
|
|
316
|
+
if (!this.vpnServer) return null;
|
|
317
|
+
return this.vpnServer.getClientTelemetry(clientId);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Get server public keys (for display/info).
|
|
322
|
+
*/
|
|
323
|
+
public getServerPublicKeys(): { noisePublicKey: string; wgPublicKey: string } | null {
|
|
324
|
+
if (!this.serverKeys) return null;
|
|
325
|
+
return {
|
|
326
|
+
noisePublicKey: this.serverKeys.noisePublicKey,
|
|
327
|
+
wgPublicKey: this.serverKeys.wgPublicKey,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ── Private helpers ────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
private async loadOrGenerateServerKeys(): Promise<IPersistedServerKeys> {
|
|
334
|
+
const stored = await this.storageManager.getJSON<IPersistedServerKeys>(STORAGE_PREFIX_KEYS);
|
|
335
|
+
if (stored?.noisePrivateKey && stored?.wgPrivateKey) {
|
|
336
|
+
logger.log('info', 'Loaded VPN server keys from storage');
|
|
337
|
+
return stored;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Generate new keys via the daemon
|
|
341
|
+
const tempServer = new plugins.smartvpn.VpnServer({
|
|
342
|
+
transport: { transport: 'stdio' },
|
|
343
|
+
});
|
|
344
|
+
await tempServer.start();
|
|
345
|
+
|
|
346
|
+
const noiseKeys = await tempServer.generateKeypair();
|
|
347
|
+
const wgKeys = await tempServer.generateWgKeypair();
|
|
348
|
+
tempServer.stop();
|
|
349
|
+
|
|
350
|
+
const keys: IPersistedServerKeys = {
|
|
351
|
+
noisePrivateKey: noiseKeys.privateKey,
|
|
352
|
+
noisePublicKey: noiseKeys.publicKey,
|
|
353
|
+
wgPrivateKey: wgKeys.privateKey,
|
|
354
|
+
wgPublicKey: wgKeys.publicKey,
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
await this.storageManager.setJSON(STORAGE_PREFIX_KEYS, keys);
|
|
358
|
+
logger.log('info', 'Generated and persisted new VPN server keys');
|
|
359
|
+
return keys;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private async loadPersistedClients(): Promise<void> {
|
|
363
|
+
const keys = await this.storageManager.list(STORAGE_PREFIX_CLIENTS);
|
|
364
|
+
for (const key of keys) {
|
|
365
|
+
const client = await this.storageManager.getJSON<IPersistedClient>(key);
|
|
366
|
+
if (client) {
|
|
367
|
+
this.clients.set(client.clientId, client);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (this.clients.size > 0) {
|
|
371
|
+
logger.log('info', `Loaded ${this.clients.size} persisted VPN client(s)`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private async persistClient(client: IPersistedClient): Promise<void> {
|
|
376
|
+
await this.storageManager.setJSON(`${STORAGE_PREFIX_CLIENTS}${client.clientId}`, client);
|
|
377
|
+
}
|
|
378
|
+
}
|
package/ts/vpn/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './classes.vpn-manager.js';
|
package/ts_web/appstate.ts
CHANGED
|
@@ -905,6 +905,161 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|
|
905
905
|
}
|
|
906
906
|
});
|
|
907
907
|
|
|
908
|
+
// ============================================================================
|
|
909
|
+
// VPN State
|
|
910
|
+
// ============================================================================
|
|
911
|
+
|
|
912
|
+
export interface IVpnState {
|
|
913
|
+
clients: interfaces.data.IVpnClient[];
|
|
914
|
+
status: interfaces.data.IVpnServerStatus | null;
|
|
915
|
+
isLoading: boolean;
|
|
916
|
+
error: string | null;
|
|
917
|
+
lastUpdated: number;
|
|
918
|
+
/** WireGuard config shown after create/rotate (only shown once) */
|
|
919
|
+
newClientConfig: string | null;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
export const vpnStatePart = await appState.getStatePart<IVpnState>(
|
|
923
|
+
'vpn',
|
|
924
|
+
{
|
|
925
|
+
clients: [],
|
|
926
|
+
status: null,
|
|
927
|
+
isLoading: false,
|
|
928
|
+
error: null,
|
|
929
|
+
lastUpdated: 0,
|
|
930
|
+
newClientConfig: null,
|
|
931
|
+
},
|
|
932
|
+
'soft'
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
// ============================================================================
|
|
936
|
+
// VPN Actions
|
|
937
|
+
// ============================================================================
|
|
938
|
+
|
|
939
|
+
export const fetchVpnAction = vpnStatePart.createAction(async (statePartArg): Promise<IVpnState> => {
|
|
940
|
+
const context = getActionContext();
|
|
941
|
+
const currentState = statePartArg.getState()!;
|
|
942
|
+
if (!context.identity) return currentState;
|
|
943
|
+
|
|
944
|
+
try {
|
|
945
|
+
const clientsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
946
|
+
interfaces.requests.IReq_GetVpnClients
|
|
947
|
+
>('/typedrequest', 'getVpnClients');
|
|
948
|
+
|
|
949
|
+
const statusRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
950
|
+
interfaces.requests.IReq_GetVpnStatus
|
|
951
|
+
>('/typedrequest', 'getVpnStatus');
|
|
952
|
+
|
|
953
|
+
const [clientsResponse, statusResponse] = await Promise.all([
|
|
954
|
+
clientsRequest.fire({ identity: context.identity }),
|
|
955
|
+
statusRequest.fire({ identity: context.identity }),
|
|
956
|
+
]);
|
|
957
|
+
|
|
958
|
+
return {
|
|
959
|
+
...currentState,
|
|
960
|
+
clients: clientsResponse.clients,
|
|
961
|
+
status: statusResponse.status,
|
|
962
|
+
isLoading: false,
|
|
963
|
+
error: null,
|
|
964
|
+
lastUpdated: Date.now(),
|
|
965
|
+
};
|
|
966
|
+
} catch (error) {
|
|
967
|
+
return {
|
|
968
|
+
...currentState,
|
|
969
|
+
isLoading: false,
|
|
970
|
+
error: error instanceof Error ? error.message : 'Failed to fetch VPN data',
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
export const createVpnClientAction = vpnStatePart.createAction<{
|
|
976
|
+
clientId: string;
|
|
977
|
+
tags?: string[];
|
|
978
|
+
description?: string;
|
|
979
|
+
}>(async (statePartArg, dataArg, actionContext): Promise<IVpnState> => {
|
|
980
|
+
const context = getActionContext();
|
|
981
|
+
const currentState = statePartArg.getState()!;
|
|
982
|
+
|
|
983
|
+
try {
|
|
984
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
985
|
+
interfaces.requests.IReq_CreateVpnClient
|
|
986
|
+
>('/typedrequest', 'createVpnClient');
|
|
987
|
+
|
|
988
|
+
const response = await request.fire({
|
|
989
|
+
identity: context.identity!,
|
|
990
|
+
clientId: dataArg.clientId,
|
|
991
|
+
tags: dataArg.tags,
|
|
992
|
+
description: dataArg.description,
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
if (!response.success) {
|
|
996
|
+
return { ...currentState, error: response.message || 'Failed to create client' };
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const refreshed = await actionContext!.dispatch(fetchVpnAction, null);
|
|
1000
|
+
return {
|
|
1001
|
+
...refreshed,
|
|
1002
|
+
newClientConfig: response.wireguardConfig || null,
|
|
1003
|
+
};
|
|
1004
|
+
} catch (error: unknown) {
|
|
1005
|
+
return {
|
|
1006
|
+
...currentState,
|
|
1007
|
+
error: error instanceof Error ? error.message : 'Failed to create VPN client',
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
export const deleteVpnClientAction = vpnStatePart.createAction<string>(
|
|
1013
|
+
async (statePartArg, clientId, actionContext): Promise<IVpnState> => {
|
|
1014
|
+
const context = getActionContext();
|
|
1015
|
+
const currentState = statePartArg.getState()!;
|
|
1016
|
+
|
|
1017
|
+
try {
|
|
1018
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
1019
|
+
interfaces.requests.IReq_DeleteVpnClient
|
|
1020
|
+
>('/typedrequest', 'deleteVpnClient');
|
|
1021
|
+
|
|
1022
|
+
await request.fire({ identity: context.identity!, clientId });
|
|
1023
|
+
return await actionContext!.dispatch(fetchVpnAction, null);
|
|
1024
|
+
} catch (error: unknown) {
|
|
1025
|
+
return {
|
|
1026
|
+
...currentState,
|
|
1027
|
+
error: error instanceof Error ? error.message : 'Failed to delete VPN client',
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
},
|
|
1031
|
+
);
|
|
1032
|
+
|
|
1033
|
+
export const toggleVpnClientAction = vpnStatePart.createAction<{
|
|
1034
|
+
clientId: string;
|
|
1035
|
+
enabled: boolean;
|
|
1036
|
+
}>(async (statePartArg, dataArg, actionContext): Promise<IVpnState> => {
|
|
1037
|
+
const context = getActionContext();
|
|
1038
|
+
const currentState = statePartArg.getState()!;
|
|
1039
|
+
|
|
1040
|
+
try {
|
|
1041
|
+
const method = dataArg.enabled ? 'enableVpnClient' : 'disableVpnClient';
|
|
1042
|
+
type TReq = interfaces.requests.IReq_EnableVpnClient | interfaces.requests.IReq_DisableVpnClient;
|
|
1043
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<TReq>(
|
|
1044
|
+
'/typedrequest', method,
|
|
1045
|
+
);
|
|
1046
|
+
|
|
1047
|
+
await request.fire({ identity: context.identity!, clientId: dataArg.clientId });
|
|
1048
|
+
return await actionContext!.dispatch(fetchVpnAction, null);
|
|
1049
|
+
} catch (error: unknown) {
|
|
1050
|
+
return {
|
|
1051
|
+
...currentState,
|
|
1052
|
+
error: error instanceof Error ? error.message : 'Failed to toggle VPN client',
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
export const clearNewClientConfigAction = vpnStatePart.createAction(
|
|
1058
|
+
async (statePartArg): Promise<IVpnState> => {
|
|
1059
|
+
return { ...statePartArg.getState()!, newClientConfig: null };
|
|
1060
|
+
},
|
|
1061
|
+
);
|
|
1062
|
+
|
|
908
1063
|
// ============================================================================
|
|
909
1064
|
// Route Management Actions
|
|
910
1065
|
// ============================================================================
|
|
@@ -1372,6 +1527,15 @@ async function dispatchCombinedRefreshActionInner() {
|
|
|
1372
1527
|
console.error('Remote ingress refresh failed:', error);
|
|
1373
1528
|
}
|
|
1374
1529
|
}
|
|
1530
|
+
|
|
1531
|
+
// Refresh VPN data if on vpn view
|
|
1532
|
+
if (currentView === 'vpn') {
|
|
1533
|
+
try {
|
|
1534
|
+
await vpnStatePart.dispatchAction(fetchVpnAction, null);
|
|
1535
|
+
} catch (error) {
|
|
1536
|
+
console.error('VPN refresh failed:', error);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1375
1539
|
} catch (error) {
|
|
1376
1540
|
console.error('Combined refresh failed:', error);
|
|
1377
1541
|
// If the error looks like an auth failure (invalid JWT), force re-login
|
package/ts_web/elements/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { OpsViewApiTokens } from './ops-view-apitokens.js';
|
|
|
24
24
|
import { OpsViewSecurity } from './ops-view-security.js';
|
|
25
25
|
import { OpsViewCertificates } from './ops-view-certificates.js';
|
|
26
26
|
import { OpsViewRemoteIngress } from './ops-view-remoteingress.js';
|
|
27
|
+
import { OpsViewVpn } from './ops-view-vpn.js';
|
|
27
28
|
|
|
28
29
|
@customElement('ops-dashboard')
|
|
29
30
|
export class OpsDashboard extends DeesElement {
|
|
@@ -92,6 +93,11 @@ export class OpsDashboard extends DeesElement {
|
|
|
92
93
|
iconName: 'lucide:globe',
|
|
93
94
|
element: OpsViewRemoteIngress,
|
|
94
95
|
},
|
|
96
|
+
{
|
|
97
|
+
name: 'VPN',
|
|
98
|
+
iconName: 'lucide:shield',
|
|
99
|
+
element: OpsViewVpn,
|
|
100
|
+
},
|
|
95
101
|
];
|
|
96
102
|
|
|
97
103
|
/**
|