feat(vpn): add VPN server management and route-based VPN access control
This commit is contained in:
@@ -28,6 +28,7 @@ export class OpsServer {
|
||||
private remoteIngressHandler!: handlers.RemoteIngressHandler;
|
||||
private routeManagementHandler!: handlers.RouteManagementHandler;
|
||||
private apiTokenHandler!: handlers.ApiTokenHandler;
|
||||
private vpnHandler!: handlers.VpnHandler;
|
||||
|
||||
constructor(dcRouterRefArg: DcRouter) {
|
||||
this.dcRouterRef = dcRouterRefArg;
|
||||
@@ -86,6 +87,7 @@ export class OpsServer {
|
||||
this.remoteIngressHandler = new handlers.RemoteIngressHandler(this);
|
||||
this.routeManagementHandler = new handlers.RouteManagementHandler(this);
|
||||
this.apiTokenHandler = new handlers.ApiTokenHandler(this);
|
||||
this.vpnHandler = new handlers.VpnHandler(this);
|
||||
|
||||
console.log('✅ OpsServer TypedRequest handlers initialized');
|
||||
}
|
||||
|
||||
@@ -8,4 +8,5 @@ export * from './email-ops.handler.js';
|
||||
export * from './certificate.handler.js';
|
||||
export * from './remoteingress.handler.js';
|
||||
export * from './route-management.handler.js';
|
||||
export * from './api-token.handler.js';
|
||||
export * from './api-token.handler.js';
|
||||
export * from './vpn.handler.js';
|
||||
257
ts/opsserver/handlers/vpn.handler.ts
Normal file
257
ts/opsserver/handlers/vpn.handler.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { OpsServer } from '../classes.opsserver.js';
|
||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||
|
||||
export class VpnHandler {
|
||||
constructor(private opsServerRef: OpsServer) {
|
||||
this.registerHandlers();
|
||||
}
|
||||
|
||||
private registerHandlers(): void {
|
||||
const viewRouter = this.opsServerRef.viewRouter;
|
||||
const adminRouter = this.opsServerRef.adminRouter;
|
||||
|
||||
// ---- Read endpoints (viewRouter — valid identity required via middleware) ----
|
||||
|
||||
// Get all registered VPN clients
|
||||
viewRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVpnClients>(
|
||||
'getVpnClients',
|
||||
async (dataArg, toolsArg) => {
|
||||
const manager = this.opsServerRef.dcRouterRef.vpnManager;
|
||||
if (!manager) {
|
||||
return { clients: [] };
|
||||
}
|
||||
const clients = manager.listClients().map((c) => ({
|
||||
clientId: c.clientId,
|
||||
enabled: c.enabled,
|
||||
tags: c.tags,
|
||||
description: c.description,
|
||||
assignedIp: c.assignedIp,
|
||||
createdAt: c.createdAt,
|
||||
updatedAt: c.updatedAt,
|
||||
expiresAt: c.expiresAt,
|
||||
}));
|
||||
return { clients };
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Get VPN server status
|
||||
viewRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVpnStatus>(
|
||||
'getVpnStatus',
|
||||
async (dataArg, toolsArg) => {
|
||||
const manager = this.opsServerRef.dcRouterRef.vpnManager;
|
||||
const vpnConfig = this.opsServerRef.dcRouterRef.options.vpnConfig;
|
||||
if (!manager) {
|
||||
return {
|
||||
status: {
|
||||
running: false,
|
||||
forwardingMode: 'socket' as const,
|
||||
subnet: vpnConfig?.subnet || '10.8.0.0/24',
|
||||
wgListenPort: vpnConfig?.wgListenPort ?? 51820,
|
||||
serverPublicKeys: null,
|
||||
registeredClients: 0,
|
||||
connectedClients: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const connected = await manager.getConnectedClients();
|
||||
return {
|
||||
status: {
|
||||
running: manager.running,
|
||||
forwardingMode: manager.forwardingMode,
|
||||
subnet: manager.getSubnet(),
|
||||
wgListenPort: vpnConfig?.wgListenPort ?? 51820,
|
||||
serverPublicKeys: manager.getServerPublicKeys(),
|
||||
registeredClients: manager.listClients().length,
|
||||
connectedClients: connected.length,
|
||||
},
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// ---- Write endpoints (adminRouter — admin identity required via middleware) ----
|
||||
|
||||
// Create a new VPN client
|
||||
adminRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateVpnClient>(
|
||||
'createVpnClient',
|
||||
async (dataArg, toolsArg) => {
|
||||
const manager = this.opsServerRef.dcRouterRef.vpnManager;
|
||||
if (!manager) {
|
||||
return { success: false, message: 'VPN not configured' };
|
||||
}
|
||||
|
||||
try {
|
||||
const bundle = await manager.createClient({
|
||||
clientId: dataArg.clientId,
|
||||
tags: dataArg.tags,
|
||||
description: dataArg.description,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
client: {
|
||||
clientId: bundle.entry.clientId,
|
||||
enabled: bundle.entry.enabled ?? true,
|
||||
tags: bundle.entry.tags,
|
||||
description: bundle.entry.description,
|
||||
assignedIp: bundle.entry.assignedIp,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
expiresAt: bundle.entry.expiresAt,
|
||||
},
|
||||
wireguardConfig: bundle.wireguardConfig,
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
return { success: false, message: (err as Error).message };
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Delete a VPN client
|
||||
adminRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteVpnClient>(
|
||||
'deleteVpnClient',
|
||||
async (dataArg, toolsArg) => {
|
||||
const manager = this.opsServerRef.dcRouterRef.vpnManager;
|
||||
if (!manager) {
|
||||
return { success: false, message: 'VPN not configured' };
|
||||
}
|
||||
|
||||
try {
|
||||
await manager.removeClient(dataArg.clientId);
|
||||
return { success: true };
|
||||
} catch (err: unknown) {
|
||||
return { success: false, message: (err as Error).message };
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Enable a VPN client
|
||||
adminRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_EnableVpnClient>(
|
||||
'enableVpnClient',
|
||||
async (dataArg, toolsArg) => {
|
||||
const manager = this.opsServerRef.dcRouterRef.vpnManager;
|
||||
if (!manager) {
|
||||
return { success: false, message: 'VPN not configured' };
|
||||
}
|
||||
|
||||
try {
|
||||
await manager.enableClient(dataArg.clientId);
|
||||
return { success: true };
|
||||
} catch (err: unknown) {
|
||||
return { success: false, message: (err as Error).message };
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Disable a VPN client
|
||||
adminRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DisableVpnClient>(
|
||||
'disableVpnClient',
|
||||
async (dataArg, toolsArg) => {
|
||||
const manager = this.opsServerRef.dcRouterRef.vpnManager;
|
||||
if (!manager) {
|
||||
return { success: false, message: 'VPN not configured' };
|
||||
}
|
||||
|
||||
try {
|
||||
await manager.disableClient(dataArg.clientId);
|
||||
return { success: true };
|
||||
} catch (err: unknown) {
|
||||
return { success: false, message: (err as Error).message };
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Rotate a VPN client's keys
|
||||
adminRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RotateVpnClientKey>(
|
||||
'rotateVpnClientKey',
|
||||
async (dataArg, toolsArg) => {
|
||||
const manager = this.opsServerRef.dcRouterRef.vpnManager;
|
||||
if (!manager) {
|
||||
return { success: false, message: 'VPN not configured' };
|
||||
}
|
||||
|
||||
try {
|
||||
const bundle = await manager.rotateClientKey(dataArg.clientId);
|
||||
return {
|
||||
success: true,
|
||||
wireguardConfig: bundle.wireguardConfig,
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
return { success: false, message: (err as Error).message };
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Export a VPN client config
|
||||
adminRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ExportVpnClientConfig>(
|
||||
'exportVpnClientConfig',
|
||||
async (dataArg, toolsArg) => {
|
||||
const manager = this.opsServerRef.dcRouterRef.vpnManager;
|
||||
if (!manager) {
|
||||
return { success: false, message: 'VPN not configured' };
|
||||
}
|
||||
|
||||
try {
|
||||
const config = await manager.exportClientConfig(dataArg.clientId, dataArg.format);
|
||||
return { success: true, config };
|
||||
} catch (err: unknown) {
|
||||
return { success: false, message: (err as Error).message };
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Get telemetry for a specific VPN client
|
||||
viewRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVpnClientTelemetry>(
|
||||
'getVpnClientTelemetry',
|
||||
async (dataArg, toolsArg) => {
|
||||
const manager = this.opsServerRef.dcRouterRef.vpnManager;
|
||||
if (!manager) {
|
||||
return { success: false, message: 'VPN not configured' };
|
||||
}
|
||||
|
||||
try {
|
||||
const telemetry = await manager.getClientTelemetry(dataArg.clientId);
|
||||
if (!telemetry) {
|
||||
return { success: false, message: 'Client not found or not connected' };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
telemetry: {
|
||||
clientId: telemetry.clientId,
|
||||
assignedIp: telemetry.assignedIp,
|
||||
bytesSent: telemetry.bytesSent,
|
||||
bytesReceived: telemetry.bytesReceived,
|
||||
packetsDropped: telemetry.packetsDropped,
|
||||
bytesDropped: telemetry.bytesDropped,
|
||||
lastKeepaliveAt: telemetry.lastKeepaliveAt,
|
||||
keepalivesReceived: telemetry.keepalivesReceived,
|
||||
rateLimitBytesPerSec: telemetry.rateLimitBytesPerSec,
|
||||
burstBytes: telemetry.burstBytes,
|
||||
},
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
return { success: false, message: (err as Error).message };
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user