diff --git a/package.json b/package.json index d0a9d92..89685f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "vpgen", "version": "0.0.1", + "license": "AGPL-3.0-or-later", "type": "module", "scripts": { "dev": "vite dev", diff --git a/src/hooks.server.ts b/src/hooks.server.ts index f523dc5..0405ed1 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -2,10 +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 { fetchOpnsenseServer } from '$lib/server/opnsense'; +import wgProvider from '$lib/server/wg-provider'; -// fetch opnsense server info on startup -await fetchOpnsenseServer(); +await wgProvider.init(); const handleAuth: Handle = async ({ event, resolve }) => { const sessionId = event.cookies.get(auth.sessionCookieName); diff --git a/src/lib/opnsense/index.ts b/src/lib/opnsense/index.ts deleted file mode 100644 index a4e9e24..0000000 --- a/src/lib/opnsense/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function opnsenseSanitezedUsername(username: string) { - return username.slice(0, 63).replace(/[^a-zA-Z0-9_-]/g, '_'); -} diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 3309bca..f7a56f9 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -50,7 +50,7 @@ export async function validateSession(sessionId: string) { const [result] = await db .select({ // Adjust user table here to tweak returned data - user: { id: table.users.id, username: table.users.username, name: table.users.name }, + user: { id: table.users.id, authSource: table.users.authSource, username: table.users.username, name: table.users.name }, session: table.sessions }) .from(table.sessions) diff --git a/src/lib/server/devices/create.ts b/src/lib/server/devices/create.ts index 5dd5280..9d919ed 100644 --- a/src/lib/server/devices/create.ts +++ b/src/lib/server/devices/create.ts @@ -3,9 +3,8 @@ import { err, ok, type Result } from '$lib/types'; import { db } from '$lib/server/db'; import { count, eq, isNull } from 'drizzle-orm'; import { env } from '$env/dynamic/private'; -import { opnsenseAuth, opnsenseUrl, serverUuid } from '$lib/server/opnsense'; -import { opnsenseSanitezedUsername } from '$lib/opnsense'; import { getIpsFromIndex } from './utils'; +import wgProvider from '$lib/server/wg-provider'; export async function createDevice(params: { name: string; @@ -19,17 +18,15 @@ export async function createDevice(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 [keys, availableAllocation, lastAllocation] = await Promise.all([ - // fetch params for new device from opnsense api - getKeys(), + const [keysResult, availableAllocation, lastAllocation] = await Promise.all([ + // fetch params for new device from provider + wgProvider.generateKeys(), // find first unallocated IP await tx.query.ipAllocations.findFirst({ columns: { @@ -46,9 +43,12 @@ export async function createDevice(params: { }), ]); + if (keysResult?._tag === 'err') return err([500, 'Failed to get keys']); + const keys = keysResult.value; + // check for existing allocation or if we have any IPs left if (!availableAllocation && lastAllocation && lastAllocation.id >= parseInt(env.IP_MAX_INDEX)) { - return err([500, 'No more IP addresses available'] as [500, string]); + return err([500, 'No more IP addresses available']); } // use existing allocation or create a new one @@ -65,9 +65,9 @@ export async function createDevice(params: { .values({ userId: params.user.id, name: params.name, - publicKey: keys.pubkey, - privateKey: keys.privkey, - preSharedKey: keys.psk, + publicKey: keys.publicKey, + privateKey: keys.privateKey, + preSharedKey: keys.preSharedKey, }) .returning({ id: devices.id }); @@ -77,80 +77,18 @@ export async function createDevice(params: { .set({ deviceId: newDevice.id }) .where(eq(ipAllocations.id, ipAllocationId)); - // create client in opnsense - const opnsenseRes = await opnsenseCreateClient({ - username: params.user.username, - pubkey: keys.pubkey, - psk: keys.psk, + // create client in provider + const providerRes = await wgProvider.createClient({ + user: params.user, + publicKey: keys.publicKey, + preSharedKey: keys.preSharedKey, allowedIps: getIpsFromIndex(ipAllocationId).join(','), }); - const opnsenseResJson = await opnsenseRes.json(); - if (opnsenseResJson['result'] !== 'saved') { + if (providerRes._tag === 'err') { tx2.rollback(); - console.error(`Error creating client in OPNsense: \n${opnsenseResJson}`); - return err([500, 'Error creating client in OPNsense'] as [500, string]); + return err([500, 'Failed to create client in provider']); } - - // reconfigure opnsense - await opnsenseReconfigure(); return ok(newDevice.id); }); }); } - -async function getKeys() { - // fetch key pair from opnsense - const options: RequestInit = { - method: 'GET', - headers: { - Authorization: opnsenseAuth, - Accept: 'application/json', - }, - }; - const resKeyPair = await fetch(`${opnsenseUrl}/api/wireguard/server/key_pair`, options); - const resPsk = await fetch(`${opnsenseUrl}/api/wireguard/client/psk`, options); - const keyPair = await resKeyPair.json(); - const psk = await resPsk.json(); - return { - pubkey: keyPair['pubkey'] as string, - privkey: keyPair['privkey'] as string, - psk: psk['psk'] as string, - }; -} - -async function opnsenseCreateClient(params: { - username: string; - pubkey: string; - psk: string; - allowedIps: string; -}) { - return fetch(`${opnsenseUrl}/api/wireguard/client/addClientBuilder`, { - method: 'POST', - headers: { - Authorization: opnsenseAuth, - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - configbuilder: { - enabled: '1', - name: `vpgen-${opnsenseSanitezedUsername(params.username)}`, - pubkey: params.pubkey, - psk: params.psk, - tunneladdress: params.allowedIps, - server: serverUuid, - endpoint: env.VPN_ENDPOINT, - }, - }), - }); -} - -async function opnsenseReconfigure() { - return fetch(`${opnsenseUrl}/api/wireguard/service/reconfigure`, { - method: 'POST', - headers: { - Authorization: opnsenseAuth, - Accept: 'application/json', - }, - }); -} diff --git a/src/lib/server/devices/delete.ts b/src/lib/server/devices/delete.ts index bebffc0..b890ed3 100644 --- a/src/lib/server/devices/delete.ts +++ b/src/lib/server/devices/delete.ts @@ -2,7 +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 { opnsenseAuth, opnsenseUrl } from '$lib/server/opnsense'; +import wgProvider from '$lib/server/wg-provider'; export async function deleteDevice( userId: string, @@ -15,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 f3b3810..0910786 100644 --- a/src/lib/server/devices/find.ts +++ b/src/lib/server/devices/find.ts @@ -2,9 +2,9 @@ import { db } from '$lib/server/db'; import { and, eq } from 'drizzle-orm'; import { devices } from '$lib/server/db/schema'; import type { DeviceDetails } from '$lib/devices'; -import { serverPublicKey } from '$lib/server/opnsense'; 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({ @@ -49,7 +49,7 @@ export function mapDeviceToDetails( privateKey: device.privateKey, preSharedKey: device.preSharedKey, ips, - vpnPublicKey: serverPublicKey, + vpnPublicKey: wgProvider.getServerPublicKey(), vpnEndpoint: env.VPN_ENDPOINT, vpnDns: env.VPN_DNS, }; diff --git a/src/lib/server/opnsense/index.ts b/src/lib/server/opnsense/index.ts deleted file mode 100644 index 6f4618b..0000000 --- a/src/lib/server/opnsense/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { env } from '$env/dynamic/private'; -import assert from 'node:assert'; -import { encodeBasicCredentials } from 'arctic/dist/request'; -import { dev } from '$app/environment'; -import type { OpnsenseWgServers } from '$lib/opnsense/wg'; - -export const opnsenseUrl = env.OPNSENSE_API_URL; -export const opnsenseAuth = - 'Basic ' + encodeBasicCredentials(env.OPNSENSE_API_KEY, env.OPNSENSE_API_SECRET); -export const opnsenseIfname = env.OPNSENSE_WG_IFNAME; - -// unset secret for security -if (!dev) env.OPNSENSE_API_SECRET = ''; - -export let serverUuid: string, serverPublicKey: string; - -export async function fetchOpnsenseServer() { -// this might be pretty bad if the server is down and in a bunch of other cases -// TODO: write a retry loop later - const resServers = await fetch(`${opnsenseUrl}/api/wireguard/client/list_servers`, { - method: 'GET', - headers: { - Authorization: opnsenseAuth, - Accept: 'application/json', - }, - }); - assert(resServers.ok, 'Failed to fetch OPNsense WireGuard servers'); - const servers = (await resServers.json()) as OpnsenseWgServers; - assert.equal(servers.status, 'ok', 'Failed to fetch OPNsense WireGuard servers'); - const uuid = servers.rows.find((server) => server.name === opnsenseIfname)?.uuid; - assert(uuid, 'Failed to find server UUID for OPNsense WireGuard server'); - serverUuid = uuid; - console.log('OPNsense WireGuard server UUID:', serverUuid); - - const resServerInfo = await fetch( - `${opnsenseUrl}/api/wireguard/client/get_server_info/${serverUuid}`, - { - method: 'GET', - headers: { - Authorization: opnsenseAuth, - Accept: 'application/json', - }, - }, - ); - assert(resServerInfo.ok, 'Failed to fetch OPNsense WireGuard server info'); - const serverInfo = await resServerInfo.json(); - assert.equal(serverInfo.status, 'ok', 'Failed to fetch OPNsense WireGuard server info'); - serverPublicKey = serverInfo['pubkey']; -} diff --git a/src/lib/server/types/index.ts b/src/lib/server/types/index.ts new file mode 100644 index 0000000..6eeb75e --- /dev/null +++ b/src/lib/server/types/index.ts @@ -0,0 +1,38 @@ +import type { Result } from '$lib/types'; +import type { User } from '$lib/server/db/schema'; + +export interface IWgProvider { + init(): Promise>; + + getServerPublicKey(): string; + + generateKeys(): Promise>; + + createClient(params: CreateClientParams): Promise>; + + findConnections(user: User): Promise>; + + deleteClient(publicKey: string): Promise>; +} + +export type WgKeys = { + publicKey: string; + privateKey: string; + preSharedKey: string; +}; + +export type CreateClientParams = { + user: User; + publicKey: string; + preSharedKey: string; + allowedIps: string; +} + +export type ClientConnection = { + publicKey: string; + endpoint: string; + allowedIps: string; + transferRx: number; + transferTx: number; + latestHandshake: number; +} diff --git a/src/lib/server/wg-provider.ts b/src/lib/server/wg-provider.ts new file mode 100644 index 0000000..927a762 --- /dev/null +++ b/src/lib/server/wg-provider.ts @@ -0,0 +1,12 @@ +import { WgProviderOpnsense } from '$lib/server/wg-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/lib/server/wg-providers/opnsense/index.ts b/src/lib/server/wg-providers/opnsense/index.ts new file mode 100644 index 0000000..d249363 --- /dev/null +++ b/src/lib/server/wg-providers/opnsense/index.ts @@ -0,0 +1,244 @@ +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/server/wg-providers/opnsense/types'; +import { err, ok, type Result } from '$lib/types'; +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.opnsenseIfname = params.opnsenseWgIfname; + } + + public async init(): Promise> { + const resServers = await fetch(`${this.opnsenseUrl}/api/wireguard/client/list_servers`, { + method: 'GET', + headers: { + Authorization: this.opnsenseAuth, + Accept: 'application/json', + }, + }); + + const servers = await resServers.json(); + if (!is(servers)) { + console.error('Unexpected response for OPNsense WireGuard servers', servers); + return err(new Error('Failed to fetch OPNsense WireGuard servers')); + } + + const uuid = servers.rows.find((server) => server.name === this.opnsenseIfname)?.uuid; + if (!uuid) { + console.error('OPNsense WireGuard servers', servers); + 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); + } + + 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: { + Authorization: this.opnsenseAuth, + Accept: 'application/json', + }, + }; + const resKeyPair = await fetch(`${this.opnsenseUrl}/api/wireguard/server/key_pair`, options); + const resPsk = await fetch(`${this.opnsenseUrl}/api/wireguard/client/psk`, options); + const keyPair = await resKeyPair.json(); + const psk = await resPsk.json(); + + if (!is<{ pubkey: string; privkey: string }>(keyPair)) { + console.error('Unexpected response for OPNsense key pair', keyPair); + return err(new Error('Failed to fetch OPNsense key pair')); + } + if (!is<{ psk: string }>(psk)) { + console.error('Unexpected response for OPNsense PSK', psk); + return err(new Error('Failed to fetch OPNsense PSK')); + } + + return ok({ + publicKey: keyPair.pubkey, + privateKey: keyPair.privkey, + preSharedKey: psk.psk, + }); + } + + 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 createClientResJson = await createClientRes.json(); + if (createClientResJson['result'] !== 'saved') { + console.error('Error creating client in OPNsense', createClientResJson); + return err(new Error('Failed to create client in OPNsense')); + } + + const reconfigureRes = await fetch(`${this.opnsenseUrl}/api/wireguard/service/reconfigure`, { + method: 'POST', + headers: { + Authorization: this.opnsenseAuth, + Accept: 'application/json', + }, + }); + + if (reconfigureRes.status !== 200) { + console.error('Error reconfiguring OPNsense', reconfigureRes); + return err(new Error('Failed to reconfigure OPNsense')); + } + + return ok(null); + } + + async findConnections(user: User): Promise> { + const res = await fetch(`${this.opnsenseUrl}/api/wireguard/service/show`, { + method: 'POST', + headers: { + Authorization: this.opnsenseAuth, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + current: 1, + // "rowCount": 7, + sort: {}, + // TODO: use a more unique search phrase + // unfortunately 64 character limit, + // but it should be fine if users can't change their own username + searchPhrase: `vpgen-${opnsenseSanitezedUsername(user.username)}`, + type: ['peer'], + }), + }); + + const peers = await res.json(); + if (!is(peers)) { + console.error('Unexpected response for OPNsense WireGuard peers', peers); + 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-rx'], + transferTx: peer['transfer-tx'], + latestHandshake: peer['latest-handshake'] * 1000, + }; + }), + ); + } + + 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; + } +} + +function opnsenseSanitezedUsername(username: string) { + return username.slice(0, 63).replace(/[^a-zA-Z0-9_-]/g, '_'); +} + +export type OpnsenseParams = { + opnsenseUrl: string; + opnsenseApiKey: string; + opnsenseApiSecret: string; + opnsenseWgIfname: string; +}; diff --git a/src/lib/opnsense/wg.ts b/src/lib/server/wg-providers/opnsense/types.ts similarity index 100% rename from src/lib/opnsense/wg.ts rename to src/lib/server/wg-providers/opnsense/types.ts diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index 8530396..7ee54e4 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -1,27 +1,2 @@ -class Ok { - readonly _tag = 'ok'; - value: T; - - constructor(value: T) { - this.value = value; - } -} - -class Err { - readonly _tag = 'err'; - error: E; - - constructor(error: E) { - this.error = error; - } -} - -export type Result = Ok | Err; - -export function err(e: E): Err { - return new Err(e); -} - -export function ok(t: T): Ok { - return new Ok(t); -} +export type { Result } from './result'; +export { ok, err } from './result'; diff --git a/src/lib/types/result.ts b/src/lib/types/result.ts new file mode 100644 index 0000000..8530396 --- /dev/null +++ b/src/lib/types/result.ts @@ -0,0 +1,27 @@ +class Ok { + readonly _tag = 'ok'; + value: T; + + constructor(value: T) { + this.value = value; + } +} + +class Err { + readonly _tag = 'err'; + error: E; + + constructor(error: E) { + this.error = error; + } +} + +export type Result = Ok | Err; + +export function err(e: E): Err { + return new Err(e); +} + +export function ok(t: T): Ok { + return new Ok(t); +} diff --git a/src/routes/api/connections/+server.ts b/src/routes/api/connections/+server.ts index f149152..bc848b0 100644 --- a/src/routes/api/connections/+server.ts +++ b/src/routes/api/connections/+server.ts @@ -1,44 +1,43 @@ import { error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { opnsenseAuth, opnsenseUrl } from '$lib/server/opnsense'; -import type { OpnsenseWgPeers } from '$lib/opnsense/wg'; import { findDevices } from '$lib/server/devices'; import type { ConnectionDetails } from '$lib/connections'; -import { opnsenseSanitezedUsername } from '$lib/opnsense'; +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) { return error(401, 'Unauthorized'); } console.debug('/api/connections'); - const peers = await fetchOpnsensePeers(event.locals.user.username); - console.debug('/api/connections: fetched opnsense peers', peers.rowCount); + + 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); console.debug('/api/connections: fetched db devices'); - if (!peers) { - return error(500, 'Error getting info from OPNsense API'); - } // TODO: this is all garbage performance // filter devices with no recent handshakes - peers.rows = peers.rows.filter((peer) => peer['latest-handshake']); + const peers = peersResult.value.filter((peer) => peer.latestHandshake); // start from devices, to treat db as the source of truth const connections: ConnectionDetails[] = []; for (const device of devices) { - const peerData = peers.rows.find((peer) => peer['public-key'] === device.publicKey); + const peerData = peers.find((peer) => peer.publicKey === device.publicKey); if (!peerData) continue; connections.push({ deviceId: device.id, deviceName: device.name, devicePublicKey: device.publicKey, - deviceIps: peerData['allowed-ips'].split(','), - endpoint: peerData['endpoint'], + deviceIps: peerData.allowedIps.split(','), + endpoint: peerData.endpoint, // swap rx and tx, since the opnsense values are from the server perspective - transferRx: peerData['transfer-tx'], - transferTx: peerData['transfer-rx'], - latestHandshake: peerData['latest-handshake'] * 1000, + transferRx: peerData.transferTx, + transferTx: peerData.transferRx, + latestHandshake: peerData.latestHandshake, }); } @@ -49,25 +48,3 @@ export const GET: RequestHandler = async (event) => { }, }); }; - -async function fetchOpnsensePeers(username: string) { - const res = await fetch(`${opnsenseUrl}/api/wireguard/service/show`, { - method: 'POST', - headers: { - Authorization: opnsenseAuth, - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - current: 1, - // "rowCount": 7, - sort: {}, - // TODO: use a more unique search phrase - // unfortunately 64 character limit, - // but it should be fine if users can't change their own username - searchPhrase: `vpgen-${opnsenseSanitezedUsername(username)}`, - type: ['peer'], - }), - }); - return (await res.json()) as OpnsenseWgPeers; -} diff --git a/src/routes/devices/+page.server.ts b/src/routes/devices/+page.server.ts index 4dcf0ce..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) => {