WIP: add interface for wg provider pt. 2

This commit is contained in:
Yuri Tatishchev 2025-03-30 19:06:44 -07:00
parent bacba95adc
commit 24ded15ac9
Signed by: CaZzzer
GPG Key ID: E0EBF441EA424369
9 changed files with 152 additions and 126 deletions

View File

@ -2,23 +2,9 @@ import { type Handle, redirect } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks'; import { sequence } from '@sveltejs/kit/hooks';
import { dev } from '$app/environment'; import { dev } from '$app/environment';
import * as auth from '$lib/server/auth'; import * as auth from '$lib/server/auth';
import { WgProviderOpnsense } from '$lib/server/providers/opnsense'; import wgProvider from '$lib/server/wg-provider';
import { env } from '$env/dynamic/private';
// TODO: probably move this to a separate file, await wgProvider.init();
// without injecting the provider into the event
const provider = new WgProviderOpnsense({
opnsenseUrl: env.OPNSENSE_API_URL,
opnsenseApiKey: env.OPNSENSE_API_KEY,
opnsenseApiSecret: env.OPNSENSE_API_SECRET,
opnsenseWgIfname: env.OPNSENSE_WG_IFNAME,
});
await provider.init();
const injectWgProvider: Handle = ({ event, resolve }) => {
event.locals.wgProvider = provider;
return resolve(event);
}
const handleAuth: Handle = async ({ event, resolve }) => { const handleAuth: Handle = async ({ event, resolve }) => {
const sessionId = event.cookies.get(auth.sessionCookieName); const sessionId = event.cookies.get(auth.sessionCookieName);
@ -63,4 +49,4 @@ const handleProtectedPaths: Handle = ({ event, resolve }) => {
return resolve(event); return resolve(event);
} }
export const handle: Handle = sequence(injectWgProvider, handleAuth, handleProtectedPaths); export const handle: Handle = sequence(handleAuth, handleProtectedPaths);

View File

@ -4,9 +4,9 @@ import { db } from '$lib/server/db';
import { count, eq, isNull } from 'drizzle-orm'; import { count, eq, isNull } from 'drizzle-orm';
import { env } from '$env/dynamic/private'; import { env } from '$env/dynamic/private';
import { getIpsFromIndex } from './utils'; import { getIpsFromIndex } from './utils';
import type { IWgProvider } from '$lib/server/types'; import wgProvider from '$lib/server/wg-provider';
export async function createDevice(wgProvider: IWgProvider, params: { export async function createDevice(params: {
name: string; name: string;
user: User; user: User;
}): Promise<Result<number, [400 | 500, string]>> { }): Promise<Result<number, [400 | 500, string]>> {
@ -18,16 +18,14 @@ export async function createDevice(wgProvider: IWgProvider, params: {
if (deviceCount >= parseInt(env.MAX_CLIENTS_PER_USER)) if (deviceCount >= parseInt(env.MAX_CLIENTS_PER_USER))
return err([400, 'Maximum number of devices reached'] as [400, string]); return err([400, 'Maximum number of devices reached'] as [400, string]);
// this is going to be quite long // 1. fetch params for new device from provider
// 1. fetch params for new device from opnsense api
// 2.1 get an allocation for the device // 2.1 get an allocation for the device
// 2.2. insert new device into db // 2.2. insert new device into db
// 2.3. update the allocation with the device id // 2.3. update the allocation with the device id
// 3. create the client in opnsense // 3. create the client in provider
// 4. reconfigure opnsense to enable the new client
return await db.transaction(async (tx) => { return await db.transaction(async (tx) => {
const [keysResult, availableAllocation, lastAllocation] = await Promise.all([ const [keysResult, availableAllocation, lastAllocation] = await Promise.all([
// fetch params for new device from opnsense api // fetch params for new device from provider
wgProvider.generateKeys(), wgProvider.generateKeys(),
// find first unallocated IP // find first unallocated IP
await tx.query.ipAllocations.findFirst({ await tx.query.ipAllocations.findFirst({
@ -79,7 +77,7 @@ export async function createDevice(wgProvider: IWgProvider, params: {
.set({ deviceId: newDevice.id }) .set({ deviceId: newDevice.id })
.where(eq(ipAllocations.id, ipAllocationId)); .where(eq(ipAllocations.id, ipAllocationId));
// create client in opnsense // create client in provider
const providerRes = await wgProvider.createClient({ const providerRes = await wgProvider.createClient({
user: params.user, user: params.user,
publicKey: keys.publicKey, publicKey: keys.publicKey,
@ -88,7 +86,7 @@ export async function createDevice(wgProvider: IWgProvider, params: {
}); });
if (providerRes._tag === 'err') { if (providerRes._tag === 'err') {
tx2.rollback(); tx2.rollback();
return err([500, 'Failed to create client in OPNsense']); return err([500, 'Failed to create client in provider']);
} }
return ok(newDevice.id); return ok(newDevice.id);
}); });

View File

@ -2,6 +2,7 @@ import { and, eq } from 'drizzle-orm';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { devices } from '$lib/server/db/schema'; import { devices } from '$lib/server/db/schema';
import { err, ok, type Result } from '$lib/types'; import { err, ok, type Result } from '$lib/types';
import wgProvider from '$lib/server/wg-provider';
export async function deleteDevice( export async function deleteDevice(
userId: string, userId: string,
@ -14,59 +15,13 @@ export async function deleteDevice(
where: and(eq(devices.userId, userId), eq(devices.id, deviceId)), where: and(eq(devices.userId, userId), eq(devices.id, deviceId)),
}); });
if (!device) return err([400, 'Device not found']); if (!device) return err([400, 'Device not found']);
const opnsenseClientUuid = (await opnsenseFindClient(device.publicKey))?.['uuid'];
if (typeof opnsenseClientUuid !== 'string') {
console.error(
'failed to get OPNsense client for deletion for device',
deviceId,
device.publicKey,
);
return err([500, 'Error getting client from OPNsense']);
}
const opnsenseDeletionResult = await opnsenseDeleteClient(opnsenseClientUuid); const providerDeletionResult = await wgProvider.deleteClient(device.publicKey);
if (opnsenseDeletionResult?.['result'] !== 'deleted') { if (providerDeletionResult._tag === 'err') {
console.error( console.error('failed to delete provider client for device', deviceId, device.publicKey);
'failed to delete OPNsense client for device', return err([500, 'Error deleting client in provider']);
deviceId,
device.publicKey,
'\n',
'OPNsense client uuid:',
opnsenseClientUuid,
);
return err([500, 'Error deleting client in OPNsense']);
} }
await db.delete(devices).where(eq(devices.id, deviceId)); await db.delete(devices).where(eq(devices.id, deviceId));
return ok(null); return ok(null);
} }
async function opnsenseFindClient(pubkey: string) {
const res = await fetch(`${opnsenseUrl}/api/wireguard/client/searchClient`, {
method: 'POST',
headers: {
Authorization: opnsenseAuth,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
current: 1,
// "rowCount": 7,
sort: {},
searchPhrase: pubkey,
type: ['peer'],
}),
});
return (await res.json())?.rows?.[0] ?? null;
}
async function opnsenseDeleteClient(clientUuid: string) {
const res = await fetch(`${opnsenseUrl}/api/wireguard/client/delClient/${clientUuid}`, {
method: 'POST',
headers: {
Authorization: opnsenseAuth,
Accept: 'application/json',
},
});
return res.json();
}

View File

@ -4,6 +4,7 @@ import { devices } from '$lib/server/db/schema';
import type { DeviceDetails } from '$lib/devices'; import type { DeviceDetails } from '$lib/devices';
import { env } from '$env/dynamic/private'; import { env } from '$env/dynamic/private';
import { getIpsFromIndex } from '$lib/server/devices/index'; import { getIpsFromIndex } from '$lib/server/devices/index';
import wgProvider from '$lib/server/wg-provider';
export async function findDevices(userId: string) { export async function findDevices(userId: string) {
return db.query.devices.findMany({ return db.query.devices.findMany({
@ -48,7 +49,7 @@ export function mapDeviceToDetails(
privateKey: device.privateKey, privateKey: device.privateKey,
preSharedKey: device.preSharedKey, preSharedKey: device.preSharedKey,
ips, ips,
vpnPublicKey: '', // TODO: fix vpnPublicKey: wgProvider.getServerPublicKey(),
vpnEndpoint: env.VPN_ENDPOINT, vpnEndpoint: env.VPN_ENDPOINT,
vpnDns: env.VPN_DNS, vpnDns: env.VPN_DNS,
}; };

View File

@ -1,24 +1,27 @@
import type { IWgProvider } from '$lib/server/types'; import type { ClientConnection, CreateClientParams, IWgProvider, WgKeys } from '$lib/server/types';
import { encodeBasicCredentials } from 'arctic/dist/request'; import { encodeBasicCredentials } from 'arctic/dist/request';
import { is } from 'typia'; import { is } from 'typia';
import type { OpnsenseWgPeers, OpnsenseWgServers } from '$lib/opnsense/wg'; import type { OpnsenseWgPeers, OpnsenseWgServers } from '$lib/opnsense/wg';
import { err, ok } from '$lib/types'; import { err, ok, type Result } from '$lib/types';
import { opnsenseSanitezedUsername } from '$lib/opnsense'; import { opnsenseSanitezedUsername } from '$lib/opnsense';
import assert from 'node:assert'; import assert from 'node:assert';
import type { User } from '$lib/server/db/schema';
export class WgProviderOpnsense implements IWgProvider { export class WgProviderOpnsense implements IWgProvider {
private opnsenseUrl: string; private opnsenseUrl: string;
private opnsenseAuth: string; private opnsenseAuth: string;
private opnsenseIfname: string; private opnsenseIfname: string;
private opnsenseWgServerUuid: string | undefined; private opnsenseWgServerUuid: string | undefined;
private opnsenseWgServerPublicKey: string | undefined;
public constructor(params: OpnsenseParams) { public constructor(params: OpnsenseParams) {
this.opnsenseUrl = params.opnsenseUrl; this.opnsenseUrl = params.opnsenseUrl;
this.opnsenseAuth = 'Basic ' + encodeBasicCredentials(params.opnsenseApiKey, params.opnsenseApiSecret); this.opnsenseAuth =
'Basic ' + encodeBasicCredentials(params.opnsenseApiKey, params.opnsenseApiSecret);
this.opnsenseIfname = params.opnsenseWgIfname; this.opnsenseIfname = params.opnsenseWgIfname;
} }
public async init() { public async init(): Promise<Result<null, Error>> {
const resServers = await fetch(`${this.opnsenseUrl}/api/wireguard/client/list_servers`, { const resServers = await fetch(`${this.opnsenseUrl}/api/wireguard/client/list_servers`, {
method: 'GET', method: 'GET',
headers: { headers: {
@ -39,12 +42,39 @@ export class WgProviderOpnsense implements IWgProvider {
return err(new Error('Failed to find server UUID for OPNsense WireGuard server')); return err(new Error('Failed to find server UUID for OPNsense WireGuard server'));
} }
const resServerInfo = await fetch(
`${this.opnsenseUrl}/api/wireguard/client/get_server_info/${uuid}`,
{
method: 'GET',
headers: {
Authorization: this.opnsenseAuth,
Accept: 'application/json',
},
},
);
const serverInfo = await resServerInfo.json();
const serverPublicKey = serverInfo['pubkey'];
if (serverInfo['status'] !== 'ok' || typeof serverPublicKey !== 'string') {
console.error('Failed to fetch OPNsense WireGuard server info', serverInfo);
return err(new Error('Failed to fetch OPNsense WireGuard server info'));
}
console.debug('OPNsense WireGuard server UUID:', uuid); console.debug('OPNsense WireGuard server UUID:', uuid);
console.debug('OPNsense WireGuard server public key:', serverPublicKey);
this.opnsenseWgServerUuid = uuid; this.opnsenseWgServerUuid = uuid;
this.opnsenseWgServerPublicKey = serverPublicKey;
return ok(null); return ok(null);
} }
public async generateKeys() { getServerPublicKey(): string {
assert(
this.opnsenseWgServerPublicKey,
'OPNsense server public key not set, init() must be called first',
);
return this.opnsenseWgServerPublicKey;
}
public async generateKeys(): Promise<Result<WgKeys, Error>> {
const options: RequestInit = { const options: RequestInit = {
method: 'GET', method: 'GET',
headers: { headers: {
@ -73,28 +103,31 @@ export class WgProviderOpnsense implements IWgProvider {
}); });
} }
async createClient(params) { async createClient(params: CreateClientParams): Promise<Result<null, Error>> {
assert(this.opnsenseWgServerUuid, 'OPNsense server UUID not set, init() must be called first'); assert(this.opnsenseWgServerUuid, 'OPNsense server UUID not set, init() must be called first');
const createClientRes = await fetch(`${this.opnsenseUrl}/api/wireguard/client/addClientBuilder`, { const createClientRes = await fetch(
method: 'POST', `${this.opnsenseUrl}/api/wireguard/client/addClientBuilder`,
headers: { {
Authorization: this.opnsenseAuth, method: 'POST',
Accept: 'application/json', headers: {
'Content-Type': 'application/json', Authorization: this.opnsenseAuth,
}, Accept: 'application/json',
body: JSON.stringify({ 'Content-Type': 'application/json',
configbuilder: {
enabled: '1',
name: `vpgen-${opnsenseSanitezedUsername(params.user.username)}`,
pubkey: params.publicKey,
psk: params.preSharedKey,
tunneladdress: params.allowedIps,
server: this.opnsenseWgServerUuid,
endpoint: '',
}, },
}), body: JSON.stringify({
}); configbuilder: {
enabled: '1',
name: `vpgen-${opnsenseSanitezedUsername(params.user.username)}`,
pubkey: params.publicKey,
psk: params.preSharedKey,
tunneladdress: params.allowedIps,
server: this.opnsenseWgServerUuid,
endpoint: '',
},
}),
},
);
const createClientResJson = await createClientRes.json(); const createClientResJson = await createClientRes.json();
if (createClientResJson['result'] !== 'saved') { if (createClientResJson['result'] !== 'saved') {
console.error('Error creating client in OPNsense', createClientResJson); console.error('Error creating client in OPNsense', createClientResJson);
@ -117,7 +150,7 @@ export class WgProviderOpnsense implements IWgProvider {
return ok(null); return ok(null);
} }
async findConnections(user) { async findConnections(user: User): Promise<Result<ClientConnection[], Error>> {
const res = await fetch(`${this.opnsenseUrl}/api/wireguard/service/show`, { const res = await fetch(`${this.opnsenseUrl}/api/wireguard/service/show`, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -143,20 +176,60 @@ export class WgProviderOpnsense implements IWgProvider {
return err(new Error('Failed to fetch OPNsense WireGuard peers')); return err(new Error('Failed to fetch OPNsense WireGuard peers'));
} }
return ok(peers.rows.map((peer) => { return ok(
return { peers.rows.map((peer) => {
publicKey: peer['public-key'], return {
endpoint: peer['endpoint'], publicKey: peer['public-key'],
allowedIps: peer['allowed-ips'], endpoint: peer['endpoint'],
transferRx: peer['transfer-tx'], allowedIps: peer['allowed-ips'],
transferTx: peer['transfer-rx'], transferRx: peer['transfer-rx'],
latestHandshake: peer['latest-handshake'] * 1000, transferTx: peer['transfer-tx'],
}; latestHandshake: peer['latest-handshake'] * 1000,
})) };
}),
);
} }
async deleteClient(publicKey) { async deleteClient(publicKey: string): Promise<Result<null, Error>> {
return err(new Error('OPNsense deleteClient not implemented')); const client = await this.findOpnsenseClient(publicKey);
const clientUuid = client?.uuid;
if (typeof clientUuid !== 'string') {
console.error('Failed to get OPNsense client UUID for deletion', client);
return err(new Error('Failed to get OPNsense client UUID for deletion'));
}
const res = await fetch(`${this.opnsenseUrl}/api/wireguard/client/delClient/${clientUuid}`, {
method: 'POST',
headers: {
Authorization: this.opnsenseAuth,
Accept: 'application/json',
},
});
const resJson = await res.json();
if (resJson['result'] !== 'deleted') {
console.error('Failed to delete OPNsense client', resJson);
return err(new Error('Failed to delete OPNsense client'));
}
return ok(null);
}
private async findOpnsenseClient(publicKey: string) {
const res = await fetch(`${this.opnsenseUrl}/api/wireguard/client/searchClient`, {
method: 'POST',
headers: {
Authorization: this.opnsenseAuth,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
current: 1,
sort: {},
searchPhrase: publicKey,
type: ['peer'],
}),
});
return (await res.json())?.rows?.[0] ?? null;
} }
} }

View File

@ -1,22 +1,14 @@
import type { Result } from '$lib/types'; import type { Result } from '$lib/types';
import type { User } from '$lib/server/db/schema'; import type { User } from '$lib/server/db/schema';
import type { ConnectionDetails } from '$lib/connections';
export interface IWgProvider { export interface IWgProvider {
init(): Promise<Result<null, Error>>; init(): Promise<Result<null, Error>>;
// TODO: might be better to not make this a result type getServerPublicKey(): string;
getServerPublicKey(): Promise<Result<string, Error>>;
// TODO: might be better to not make this a result type
generateKeys(): Promise<Result<WgKeys, Error>>; generateKeys(): Promise<Result<WgKeys, Error>>;
createClient(params: { createClient(params: CreateClientParams): Promise<Result<null, Error>>;
user: User;
publicKey: string;
preSharedKey: string;
allowedIps: string;
}): Promise<Result<null, Error>>;
findConnections(user: User): Promise<Result<ClientConnection[], Error>>; findConnections(user: User): Promise<Result<ClientConnection[], Error>>;
@ -29,6 +21,13 @@ export type WgKeys = {
preSharedKey: string; preSharedKey: string;
}; };
export type CreateClientParams = {
user: User;
publicKey: string;
preSharedKey: string;
allowedIps: string;
}
export type ClientConnection = { export type ClientConnection = {
publicKey: string; publicKey: string;
endpoint: string; endpoint: string;

View File

@ -0,0 +1,12 @@
import { WgProviderOpnsense } from '$lib/server/providers/opnsense';
import { env } from '$env/dynamic/private';
import type { IWgProvider } from '$lib/server/types';
const wgProvider: IWgProvider = new WgProviderOpnsense({
opnsenseUrl: env.OPNSENSE_API_URL,
opnsenseApiKey: env.OPNSENSE_API_KEY,
opnsenseApiSecret: env.OPNSENSE_API_SECRET,
opnsenseWgIfname: env.OPNSENSE_WG_IFNAME,
});
export default wgProvider;

View File

@ -4,6 +4,7 @@ import { findDevices } from '$lib/server/devices';
import type { ConnectionDetails } from '$lib/connections'; import type { ConnectionDetails } from '$lib/connections';
import type { Result } from '$lib/types'; import type { Result } from '$lib/types';
import type { ClientConnection } from '$lib/server/types'; import type { ClientConnection } from '$lib/server/types';
import wgProvider from '$lib/server/wg-provider';
export const GET: RequestHandler = async (event) => { export const GET: RequestHandler = async (event) => {
if (!event.locals.user) { if (!event.locals.user) {
@ -11,7 +12,7 @@ export const GET: RequestHandler = async (event) => {
} }
console.debug('/api/connections'); console.debug('/api/connections');
const peersResult: Result<ClientConnection[], Error> = await event.locals.wgProvider.findConnections(event.locals.user); const peersResult: Result<ClientConnection[], Error> = await wgProvider.findConnections(event.locals.user);
if (peersResult._tag === 'err') return error(500, peersResult.error.message); if (peersResult._tag === 'err') return error(500, peersResult.error.message);
const devices = await findDevices(event.locals.user.id); const devices = await findDevices(event.locals.user.id);

View File

@ -1,6 +1,7 @@
import type { Actions } from './$types'; import type { Actions } from './$types';
import { createDevice } from '$lib/server/devices'; import { createDevice } from '$lib/server/devices';
import { error, fail, redirect } from '@sveltejs/kit'; import { error, fail, redirect } from '@sveltejs/kit';
import wgProvider from '$lib/server/wg-provider';
export const actions = { export const actions = {
create: async (event) => { create: async (event) => {
@ -8,7 +9,7 @@ export const actions = {
const formData = await event.request.formData(); const formData = await event.request.formData();
const name = formData.get('name'); const name = formData.get('name');
if (typeof name !== 'string' || name.trim() === '') return fail(400, { name, invalid: true }); if (typeof name !== 'string' || name.trim() === '') return fail(400, { name, invalid: true });
const res = await createDevice(event.locals.wgProvider, { const res = await createDevice({
name: name.trim(), name: name.trim(),
user: event.locals.user, user: event.locals.user,
}); });