diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 24bfd68..0405ed1 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -2,23 +2,9 @@ import { type Handle, redirect } from '@sveltejs/kit'; import { sequence } from '@sveltejs/kit/hooks'; import { dev } from '$app/environment'; import * as auth from '$lib/server/auth'; -import { WgProviderOpnsense } from '$lib/server/providers/opnsense'; -import { env } from '$env/dynamic/private'; +import wgProvider from '$lib/server/wg-provider'; -// TODO: probably move this to a separate file, -// 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); -} +await wgProvider.init(); const handleAuth: Handle = async ({ event, resolve }) => { const sessionId = event.cookies.get(auth.sessionCookieName); @@ -63,4 +49,4 @@ const handleProtectedPaths: Handle = ({ event, resolve }) => { return resolve(event); } -export const handle: Handle = sequence(injectWgProvider, handleAuth, handleProtectedPaths); +export const handle: Handle = sequence(handleAuth, handleProtectedPaths); diff --git a/src/lib/server/devices/create.ts b/src/lib/server/devices/create.ts index 4eb9dd3..9d919ed 100644 --- a/src/lib/server/devices/create.ts +++ b/src/lib/server/devices/create.ts @@ -4,9 +4,9 @@ import { db } from '$lib/server/db'; import { count, eq, isNull } from 'drizzle-orm'; import { env } from '$env/dynamic/private'; 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; user: User; }): Promise> { @@ -18,16 +18,14 @@ export async function createDevice(wgProvider: IWgProvider, params: { if (deviceCount >= parseInt(env.MAX_CLIENTS_PER_USER)) 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 opnsense api + // 1. fetch params for new device from provider // 2.1 get an allocation for the device // 2.2. insert new device into db // 2.3. update the allocation with the device id - // 3. create the client in opnsense - // 4. reconfigure opnsense to enable the new client + // 3. create the client in provider return await db.transaction(async (tx) => { const [keysResult, availableAllocation, lastAllocation] = await Promise.all([ - // fetch params for new device from opnsense api + // fetch params for new device from provider wgProvider.generateKeys(), // find first unallocated IP await tx.query.ipAllocations.findFirst({ @@ -79,7 +77,7 @@ export async function createDevice(wgProvider: IWgProvider, params: { .set({ deviceId: newDevice.id }) .where(eq(ipAllocations.id, ipAllocationId)); - // create client in opnsense + // create client in provider const providerRes = await wgProvider.createClient({ user: params.user, publicKey: keys.publicKey, @@ -88,7 +86,7 @@ export async function createDevice(wgProvider: IWgProvider, params: { }); if (providerRes._tag === 'err') { tx2.rollback(); - return err([500, 'Failed to create client in OPNsense']); + return err([500, 'Failed to create client in provider']); } return ok(newDevice.id); }); diff --git a/src/lib/server/devices/delete.ts b/src/lib/server/devices/delete.ts index 933e1bf..b890ed3 100644 --- a/src/lib/server/devices/delete.ts +++ b/src/lib/server/devices/delete.ts @@ -2,6 +2,7 @@ import { and, eq } from 'drizzle-orm'; import { db } from '$lib/server/db'; import { devices } from '$lib/server/db/schema'; import { err, ok, type Result } from '$lib/types'; +import wgProvider from '$lib/server/wg-provider'; export async function deleteDevice( userId: string, @@ -14,59 +15,13 @@ export async function deleteDevice( where: and(eq(devices.userId, userId), eq(devices.id, deviceId)), }); 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); - if (opnsenseDeletionResult?.['result'] !== 'deleted') { - console.error( - 'failed to delete OPNsense client for device', - deviceId, - device.publicKey, - '\n', - 'OPNsense client uuid:', - opnsenseClientUuid, - ); - return err([500, 'Error deleting client in OPNsense']); + const providerDeletionResult = await wgProvider.deleteClient(device.publicKey); + if (providerDeletionResult._tag === 'err') { + console.error('failed to delete provider client for device', deviceId, device.publicKey); + return err([500, 'Error deleting client in provider']); } await db.delete(devices).where(eq(devices.id, deviceId)); 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(); -} diff --git a/src/lib/server/devices/find.ts b/src/lib/server/devices/find.ts index 7a3c595..0910786 100644 --- a/src/lib/server/devices/find.ts +++ b/src/lib/server/devices/find.ts @@ -4,6 +4,7 @@ import { devices } from '$lib/server/db/schema'; import type { DeviceDetails } from '$lib/devices'; import { env } from '$env/dynamic/private'; import { getIpsFromIndex } from '$lib/server/devices/index'; +import wgProvider from '$lib/server/wg-provider'; export async function findDevices(userId: string) { return db.query.devices.findMany({ @@ -48,7 +49,7 @@ export function mapDeviceToDetails( privateKey: device.privateKey, preSharedKey: device.preSharedKey, ips, - vpnPublicKey: '', // TODO: fix + vpnPublicKey: wgProvider.getServerPublicKey(), vpnEndpoint: env.VPN_ENDPOINT, vpnDns: env.VPN_DNS, }; diff --git a/src/lib/server/providers/opnsense.ts b/src/lib/server/providers/opnsense.ts index 9c69f25..9acec56 100644 --- a/src/lib/server/providers/opnsense.ts +++ b/src/lib/server/providers/opnsense.ts @@ -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 { is } from 'typia'; 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 assert from 'node:assert'; +import type { User } from '$lib/server/db/schema'; export class WgProviderOpnsense implements IWgProvider { private opnsenseUrl: string; private opnsenseAuth: string; private opnsenseIfname: string; private opnsenseWgServerUuid: string | undefined; + private opnsenseWgServerPublicKey: string | undefined; public constructor(params: OpnsenseParams) { this.opnsenseUrl = params.opnsenseUrl; - this.opnsenseAuth = 'Basic ' + encodeBasicCredentials(params.opnsenseApiKey, params.opnsenseApiSecret); + this.opnsenseAuth = + 'Basic ' + encodeBasicCredentials(params.opnsenseApiKey, params.opnsenseApiSecret); this.opnsenseIfname = params.opnsenseWgIfname; } - public async init() { + public async init(): Promise> { const resServers = await fetch(`${this.opnsenseUrl}/api/wireguard/client/list_servers`, { method: 'GET', headers: { @@ -39,12 +42,39 @@ export class WgProviderOpnsense implements IWgProvider { 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 public key:', serverPublicKey); this.opnsenseWgServerUuid = uuid; + this.opnsenseWgServerPublicKey = serverPublicKey; 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> { const options: RequestInit = { method: 'GET', headers: { @@ -73,28 +103,31 @@ export class WgProviderOpnsense implements IWgProvider { }); } - async createClient(params) { + async createClient(params: CreateClientParams): Promise> { assert(this.opnsenseWgServerUuid, 'OPNsense server UUID not set, init() must be called first'); - const createClientRes = await fetch(`${this.opnsenseUrl}/api/wireguard/client/addClientBuilder`, { - method: 'POST', - headers: { - Authorization: this.opnsenseAuth, - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - 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 createClientRes = await fetch( + `${this.opnsenseUrl}/api/wireguard/client/addClientBuilder`, + { + method: 'POST', + headers: { + Authorization: this.opnsenseAuth, + Accept: 'application/json', + 'Content-Type': 'application/json', }, - }), - }); + 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(); if (createClientResJson['result'] !== 'saved') { console.error('Error creating client in OPNsense', createClientResJson); @@ -117,7 +150,7 @@ export class WgProviderOpnsense implements IWgProvider { return ok(null); } - async findConnections(user) { + async findConnections(user: User): Promise> { const res = await fetch(`${this.opnsenseUrl}/api/wireguard/service/show`, { method: 'POST', headers: { @@ -143,20 +176,60 @@ export class WgProviderOpnsense implements IWgProvider { return err(new Error('Failed to fetch OPNsense WireGuard peers')); } - return ok(peers.rows.map((peer) => { - return { - publicKey: peer['public-key'], - endpoint: peer['endpoint'], - allowedIps: peer['allowed-ips'], - transferRx: peer['transfer-tx'], - transferTx: peer['transfer-rx'], - latestHandshake: peer['latest-handshake'] * 1000, - }; - })) + return ok( + peers.rows.map((peer) => { + return { + publicKey: peer['public-key'], + endpoint: peer['endpoint'], + allowedIps: peer['allowed-ips'], + transferRx: peer['transfer-rx'], + transferTx: peer['transfer-tx'], + latestHandshake: peer['latest-handshake'] * 1000, + }; + }), + ); } - async deleteClient(publicKey) { - return err(new Error('OPNsense deleteClient not implemented')); + async deleteClient(publicKey: string): Promise> { + 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; } } diff --git a/src/lib/server/types/index.ts b/src/lib/server/types/index.ts index 95b78e9..6eeb75e 100644 --- a/src/lib/server/types/index.ts +++ b/src/lib/server/types/index.ts @@ -1,22 +1,14 @@ import type { Result } from '$lib/types'; import type { User } from '$lib/server/db/schema'; -import type { ConnectionDetails } from '$lib/connections'; export interface IWgProvider { init(): Promise>; - // TODO: might be better to not make this a result type - getServerPublicKey(): Promise>; + getServerPublicKey(): string; - // TODO: might be better to not make this a result type generateKeys(): Promise>; - createClient(params: { - user: User; - publicKey: string; - preSharedKey: string; - allowedIps: string; - }): Promise>; + createClient(params: CreateClientParams): Promise>; findConnections(user: User): Promise>; @@ -29,6 +21,13 @@ export type WgKeys = { preSharedKey: string; }; +export type CreateClientParams = { + user: User; + publicKey: string; + preSharedKey: string; + allowedIps: string; +} + export type ClientConnection = { publicKey: string; endpoint: string; diff --git a/src/lib/server/wg-provider.ts b/src/lib/server/wg-provider.ts new file mode 100644 index 0000000..04d6fa0 --- /dev/null +++ b/src/lib/server/wg-provider.ts @@ -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; diff --git a/src/routes/api/connections/+server.ts b/src/routes/api/connections/+server.ts index e042369..bc848b0 100644 --- a/src/routes/api/connections/+server.ts +++ b/src/routes/api/connections/+server.ts @@ -4,6 +4,7 @@ import { findDevices } from '$lib/server/devices'; import type { ConnectionDetails } from '$lib/connections'; import type { Result } from '$lib/types'; import type { ClientConnection } from '$lib/server/types'; +import wgProvider from '$lib/server/wg-provider'; export const GET: RequestHandler = async (event) => { if (!event.locals.user) { @@ -11,7 +12,7 @@ export const GET: RequestHandler = async (event) => { } console.debug('/api/connections'); - const peersResult: Result = await event.locals.wgProvider.findConnections(event.locals.user); + const peersResult: Result = await wgProvider.findConnections(event.locals.user); if (peersResult._tag === 'err') return error(500, peersResult.error.message); const devices = await findDevices(event.locals.user.id); diff --git a/src/routes/devices/+page.server.ts b/src/routes/devices/+page.server.ts index 8940ffe..0dc9024 100644 --- a/src/routes/devices/+page.server.ts +++ b/src/routes/devices/+page.server.ts @@ -1,6 +1,7 @@ import type { Actions } from './$types'; import { createDevice } from '$lib/server/devices'; import { error, fail, redirect } from '@sveltejs/kit'; +import wgProvider from '$lib/server/wg-provider'; export const actions = { create: async (event) => { @@ -8,7 +9,7 @@ export const actions = { const formData = await event.request.formData(); const name = formData.get('name'); 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(), user: event.locals.user, });