import type { User } from '$lib/server/db/schema'; import { ipAllocations, wgClients } from '$lib/server/db/schema'; import { db } from '$lib/server/db'; import { opnsenseAuth, opnsenseUrl, serverPublicKey, serverUuid } from '$lib/server/opnsense'; import { Address4, Address6 } from 'ip-address'; import { env } from '$env/dynamic/private'; import { and, count, eq, isNull } from 'drizzle-orm'; import { err, ok, type Result } from '$lib/types'; import type { ClientDetails } from '$lib/types/clients'; export async function findClients(userId: string) { return db.query.wgClients.findMany({ columns: { id: true, name: true, publicKey: true, privateKey: true, preSharedKey: true, }, with: { ipAllocation: true, }, where: eq(wgClients.userId, userId), }); } export async function findClient(userId: string, clientId: number) { return db.query.wgClients.findFirst({ columns: { id: true, name: true, publicKey: true, privateKey: true, preSharedKey: true, }, with: { ipAllocation: true, }, where: and(eq(wgClients.userId, userId), eq(wgClients.id, clientId)), }); } export function mapClientToDetails( client: Awaited>[0], ): ClientDetails { const ips = getIpsFromIndex(client.ipAllocation.id); return { id: client.id, name: client.name, publicKey: client.publicKey, privateKey: client.privateKey, preSharedKey: client.preSharedKey, ips, vpnPublicKey: serverPublicKey, vpnEndpoint: env.VPN_ENDPOINT, vpnDns: env.VPN_DNS, }; } export async function createClient(params: { name: string; user: User; }): Promise> { // check if user exceeds the limit of clients const [{ clientCount }] = await db .select({ clientCount: count() }) .from(wgClients) .where(eq(wgClients.userId, params.user.id)); if (clientCount >= parseInt(env.MAX_CLIENTS_PER_USER)) return err([400, 'Maximum number of clients reached'] as [400, string]); // this is going to be quite long // 1. fetch params for new client from opnsense api // 2.1 get an allocation for the client // 2.2. insert new client into db // 2.3. update the allocation with the client id // 3. create the client in opnsense // 4. reconfigure opnsense to enable the new client return await db.transaction(async (tx) => { const [keys, availableAllocation, lastAllocation] = await Promise.all([ // fetch params for new client from opnsense api getKeys(), // find first unallocated IP await tx.query.ipAllocations.findFirst({ columns: { id: true, }, where: isNull(ipAllocations.clientId), }), // find last allocation to check if we have any IPs left await tx.query.ipAllocations.findFirst({ columns: { id: true, }, orderBy: (ipAllocations, { desc }) => desc(ipAllocations.id), }), ]); // 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]); } // use existing allocation or create a new one const ipAllocationId = availableAllocation?.id ?? (await tx.insert(ipAllocations).values({}).returning({ id: ipAllocations.id }))[0].id; // transaction savepoint after creating a new IP allocation // TODO: not sure if this is needed return await tx.transaction(async (tx2) => { // create new client in db const [newClient] = await tx2 .insert(wgClients) .values({ userId: params.user.id, name: params.name, publicKey: keys.pubkey, privateKey: keys.privkey, preSharedKey: keys.psk, }) .returning({ id: wgClients.id }); // update IP allocation with client ID await tx2 .update(ipAllocations) .set({ clientId: newClient.id }) .where(eq(ipAllocations.id, ipAllocationId)); // create client in opnsense const opnsenseRes = await opnsenseCreateClient({ username: params.user.username, pubkey: keys.pubkey, psk: keys.psk, allowedIps: getIpsFromIndex(ipAllocationId).join(','), }); const opnsenseResJson = await opnsenseRes.json(); if (opnsenseResJson['result'] !== 'saved') { tx2.rollback(); console.error(`Error creating client in OPNsense: \n${opnsenseResJson}`); return err([500, 'Error creating client in OPNsense'] as [500, string]); } // reconfigure opnsense await opnsenseReconfigure(); return ok(newClient.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, }; } export function getIpsFromIndex(ipIndex: number) { ipIndex -= 1; // 1-indexed in the db const v4StartingAddr = new Address4(env.IPV4_STARTING_ADDR); const v6StartingAddr = new Address6(env.IPV6_STARTING_ADDR); const v4Allowed = Address4.fromBigInt(v4StartingAddr.bigInt() + BigInt(ipIndex)); const v6Offset = BigInt(ipIndex) << (128n - BigInt(env.IPV6_CLIENT_PREFIX_SIZE)); const v6Allowed = Address6.fromBigInt(v6StartingAddr.bigInt() + v6Offset); const v6AllowedShort = v6Allowed.parsedAddress.join(':'); return [v4Allowed.address + '/32', v6AllowedShort + '/' + env.IPV6_CLIENT_PREFIX_SIZE]; } 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-${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', }, }); }