From 3909281bc7802d7ef2a0333d7ad426f20ecd9d71 Mon Sep 17 00:00:00 2001 From: Yuri Tatishchev Date: Sun, 22 Dec 2024 19:45:03 -0800 Subject: [PATCH] opnsense: add initial api to create new clients --- .env.example | 6 + bruno/opnsense-api/Add Client Builder.bru | 3 +- bun.lockb | Bin 161102 -> 161461 bytes package.json | 1 + src/routes/api/clients/+server.ts | 174 +++++++++++++++++++++- 5 files changed, 176 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 4a0393e..3588d80 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,9 @@ OPNSENSE_API_URL=https://opnsense.home OPNSENSE_API_KEY= OPNSENSE_API_SECRET= OPNSENSE_WG_IFNAME=wg2 + +IPV4_STARTING_ADDR=10.18.11.100 +IPV6_STARTING_ADDR=fd00:10:18:11::100:0 +IPV6_CLIENT_PREFIX_SIZE=112 +IP_MAX_INDEX=100 +VPN_ENDPOINT=vpn.lab.cazzzer.com:51820 diff --git a/bruno/opnsense-api/Add Client Builder.bru b/bruno/opnsense-api/Add Client Builder.bru index 21d00aa..41785b2 100644 --- a/bruno/opnsense-api/Add Client Builder.bru +++ b/bruno/opnsense-api/Add Client Builder.bru @@ -22,7 +22,6 @@ body:json { "pubkey": "{{clientPubkey}}", "psk": "{{psk}}", "tunneladdress": "{{clientTunnelAddress}}", - "keepalive": "", "server": "{{serverUuid}}", "endpoint": "{{vpn_endpoint}}" } @@ -30,7 +29,7 @@ body:json { } vars:pre-request { - clientName: vpgen-test + clientName: vpgen-CaZzzer clientPubkey: BJ5faPVJsDP4CCxNYilmKnwlQXOtXEOJjqIwb4U/CgM= psk: 0LWopbrISXBNHUxr+WOhCSAg+0hD8j3TLmpyzHkBHCQ= clientTunnelAddress: 10.18.11.101/32,fd00::1/128 diff --git a/bun.lockb b/bun.lockb index e22ff0d9e1688a4e7ae65403e74ddcb3ef57da4f..afcbe6f735bbd7c2ee974322db511402694d8f72 100755 GIT binary patch delta 27232 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 { if (!event.locals.user) { @@ -12,7 +22,7 @@ export const GET: RequestHandler = async (event) => { return new Response( JSON.stringify({ clients, - }) + }), ); }; @@ -20,9 +30,161 @@ async function findClients(userId: string) { return db.query.wgClients.findMany({ where: eq(wgClients.userId, userId), with: { - ipAllocation: true - } + ipAllocation: true, + }, }); } export type Clients = Awaited>; + +export const POST: RequestHandler = async (event) => { + if (!event.locals.user) { + return error(401, 'Unauthorized'); + } + // 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 + const err: ReturnType | undefined = await db.transaction(async (tx) => { + // fetch params for new client from opnsense api + const keys = await getKeys(); + + // find first unallocated IP + const availableAllocation = await tx.query.ipAllocations.findFirst({ + columns: { + id: true, + }, + where: isNull(ipAllocations.clientId), + }); + // find last allocation to check if we have any IPs left + const lastAllocation = 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(IP_MAX_INDEX)) { + return error(500, 'No more IP addresses available'); + } + + // 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 client in opnsense + const opnsenseRes = await opnsenseCreateClient({ + // @ts-expect-error event.locals.user is checked at the beginning of the function + username: event.locals.user.username, + pubkey: keys.pubkey, + psk: keys.psk, + allowedIps: getAllowedIps(ipAllocationId - 1), + }); + const opnsenseResJson = await opnsenseRes.json(); + if (opnsenseResJson['result'] !== 'saved') { + tx2.rollback(); + console.error(`Error creating client in OPNsense: \n${opnsenseResJson}`); + return error(500, 'Error creating client in OPNsense'); + } + + // create new client in db + const [newClient] = await tx2 + .insert(wgClients) + .values({ + // @ts-expect-error event.locals.user is checked at the beginning of the function + userId: event.locals.user.id, + name: 'New Client', + 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)); + + // reconfigure opnsense + await opnsenseReconfigure(); + }); + + }); + return err?? new Response(null, { status: 201 }); +}; + +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, + }; +} + +function getAllowedIps(ipIndex: number) { + const v4StartingAddr = new Address4(IPV4_STARTING_ADDR); + const v6StartingAddr = new Address6(IPV6_STARTING_ADDR); + const v4Allowed = Address4.fromBigInt(v4StartingAddr.bigInt() + BigInt(ipIndex)); + const v6Offset = BigInt(ipIndex) << (128n - BigInt(IPV6_CLIENT_PREFIX_SIZE)); + const v6Allowed = Address6.fromBigInt(v6StartingAddr.bigInt() + v6Offset); + const v6AllowedShort = v6Allowed.parsedAddress.join(':'); + + return `${v4Allowed.address}/32,${v6AllowedShort}/${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: VPN_ENDPOINT, + }, + }), + }); +} + +async function opnsenseReconfigure() { + return fetch(`${opnsenseUrl}/api/wireguard/service/reconfigure`, { + method: 'POST', + headers: { + Authorization: opnsenseAuth, + Accept: 'application/json', + }, + }); +}