Compare commits
4 Commits
bacba95adc
...
1f7dded292
Author | SHA1 | Date | |
---|---|---|---|
1f7dded292 | |||
c3e64c8b98 | |||
7b76726e6e | |||
24ded15ac9 |
1
drizzle/0002_minor_black_panther.sql
Normal file
1
drizzle/0002_minor_black_panther.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `devices` DROP COLUMN `opnsense_id`;
|
222
drizzle/meta/0002_snapshot.json
Normal file
222
drizzle/meta/0002_snapshot.json
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "0b364191-58d0-46a3-8372-4a30b0b88d85",
|
||||||
|
"prevId": "cc1fa973-1e9c-4bd6-b082-d7cf36f7342c",
|
||||||
|
"tables": {
|
||||||
|
"devices": {
|
||||||
|
"name": "devices",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"public_key": {
|
||||||
|
"name": "public_key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"private_key": {
|
||||||
|
"name": "private_key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"pre_shared_key": {
|
||||||
|
"name": "pre_shared_key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"devices_public_key_unique": {
|
||||||
|
"name": "devices_public_key_unique",
|
||||||
|
"columns": [
|
||||||
|
"public_key"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"devices_user_id_users_id_fk": {
|
||||||
|
"name": "devices_user_id_users_id_fk",
|
||||||
|
"tableFrom": "devices",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"ip_allocations": {
|
||||||
|
"name": "ip_allocations",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"device_id": {
|
||||||
|
"name": "device_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"ip_allocations_device_id_unique": {
|
||||||
|
"name": "ip_allocations_device_id_unique",
|
||||||
|
"columns": [
|
||||||
|
"device_id"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"ip_allocations_device_id_devices_id_fk": {
|
||||||
|
"name": "ip_allocations_device_id_devices_id_fk",
|
||||||
|
"tableFrom": "ip_allocations",
|
||||||
|
"tableTo": "devices",
|
||||||
|
"columnsFrom": [
|
||||||
|
"device_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "set null",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"sessions": {
|
||||||
|
"name": "sessions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"sessions_user_id_users_id_fk": {
|
||||||
|
"name": "sessions_user_id_users_id_fk",
|
||||||
|
"tableFrom": "sessions",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"auth_source": {
|
||||||
|
"name": "auth_source",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'authentik'"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,13 @@
|
|||||||
"when": 1741936760967,
|
"when": 1741936760967,
|
||||||
"tag": "0001_equal_unicorn",
|
"tag": "0001_equal_unicorn",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1743389268079,
|
||||||
|
"tag": "0002_minor_black_panther",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -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);
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
export function opnsenseSanitezedUsername(username: string) {
|
|
||||||
return username.slice(0, 63).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
||||||
}
|
|
@ -36,8 +36,6 @@ export const devices = sqliteTable('devices', {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id),
|
.references(() => users.id),
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
// questioning whether this should be nullable
|
|
||||||
opnsenseId: text('opnsense_id'),
|
|
||||||
publicKey: text('public_key').notNull().unique(),
|
publicKey: text('public_key').notNull().unique(),
|
||||||
// nullable for the possibility of a user supplying their own private key
|
// nullable for the possibility of a user supplying their own private key
|
||||||
privateKey: text('private_key'),
|
privateKey: text('private_key'),
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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'];
|
|
||||||
}
|
|
@ -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;
|
||||||
|
12
src/lib/server/wg-provider.ts
Normal file
12
src/lib/server/wg-provider.ts
Normal file
@ -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;
|
@ -1,24 +1,26 @@
|
|||||||
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/server/wg-providers/opnsense/types';
|
||||||
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 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 +41,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 +102,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 +149,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,21 +175,65 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function opnsenseSanitezedUsername(username: string) {
|
||||||
|
return username.slice(0, 63).replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OpnsenseParams = {
|
export type OpnsenseParams = {
|
@ -1,17 +1,2 @@
|
|||||||
import type { Address4, Address6 } from 'ip-address';
|
|
||||||
|
|
||||||
export type { Result } from './result';
|
export type { Result } from './result';
|
||||||
export { ok, err } from './result';
|
export { ok, err } from './result';
|
||||||
|
|
||||||
export interface IWGProviderConfig {
|
|
||||||
ipv4?: {
|
|
||||||
startingAddr: Address4;
|
|
||||||
};
|
|
||||||
ipv6?: {
|
|
||||||
startingAddr: Address6;
|
|
||||||
clientPrefixSize: number;
|
|
||||||
}
|
|
||||||
endpoint: string;
|
|
||||||
dns: string;
|
|
||||||
addrMaxIndex: number;
|
|
||||||
}
|
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user