WIP: add interface for wg provider
This commit is contained in:
parent
e9d4be1d53
commit
bacba95adc
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "vpgen",
|
"name": "vpgen",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
@ -2,10 +2,23 @@ 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 { fetchOpnsenseServer } from '$lib/server/opnsense';
|
import { WgProviderOpnsense } from '$lib/server/providers/opnsense';
|
||||||
|
import { env } from '$env/dynamic/private';
|
||||||
|
|
||||||
// fetch opnsense server info on startup
|
// TODO: probably move this to a separate file,
|
||||||
await fetchOpnsenseServer();
|
// 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);
|
||||||
@ -50,4 +63,4 @@ const handleProtectedPaths: Handle = ({ event, resolve }) => {
|
|||||||
return resolve(event);
|
return resolve(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handle: Handle = sequence(handleAuth, handleProtectedPaths);
|
export const handle: Handle = sequence(injectWgProvider, handleAuth, handleProtectedPaths);
|
||||||
|
@ -50,7 +50,7 @@ export async function validateSession(sessionId: string) {
|
|||||||
const [result] = await db
|
const [result] = await db
|
||||||
.select({
|
.select({
|
||||||
// Adjust user table here to tweak returned data
|
// 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
|
session: table.sessions
|
||||||
})
|
})
|
||||||
.from(table.sessions)
|
.from(table.sessions)
|
||||||
|
@ -3,11 +3,10 @@ import { err, ok, type Result } from '$lib/types';
|
|||||||
import { db } from '$lib/server/db';
|
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 { opnsenseAuth, opnsenseUrl, serverUuid } from '$lib/server/opnsense';
|
|
||||||
import { opnsenseSanitezedUsername } from '$lib/opnsense';
|
|
||||||
import { getIpsFromIndex } from './utils';
|
import { getIpsFromIndex } from './utils';
|
||||||
|
import type { IWgProvider } from '$lib/server/types';
|
||||||
|
|
||||||
export async function createDevice(params: {
|
export async function createDevice(wgProvider: IWgProvider, params: {
|
||||||
name: string;
|
name: string;
|
||||||
user: User;
|
user: User;
|
||||||
}): Promise<Result<number, [400 | 500, string]>> {
|
}): Promise<Result<number, [400 | 500, string]>> {
|
||||||
@ -27,9 +26,9 @@ export async function createDevice(params: {
|
|||||||
// 3. create the client in opnsense
|
// 3. create the client in opnsense
|
||||||
// 4. reconfigure opnsense to enable the new client
|
// 4. reconfigure opnsense to enable the new client
|
||||||
return await db.transaction(async (tx) => {
|
return await db.transaction(async (tx) => {
|
||||||
const [keys, 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 opnsense api
|
||||||
getKeys(),
|
wgProvider.generateKeys(),
|
||||||
// find first unallocated IP
|
// find first unallocated IP
|
||||||
await tx.query.ipAllocations.findFirst({
|
await tx.query.ipAllocations.findFirst({
|
||||||
columns: {
|
columns: {
|
||||||
@ -46,9 +45,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
|
// check for existing allocation or if we have any IPs left
|
||||||
if (!availableAllocation && lastAllocation && lastAllocation.id >= parseInt(env.IP_MAX_INDEX)) {
|
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
|
// use existing allocation or create a new one
|
||||||
@ -65,9 +67,9 @@ export async function createDevice(params: {
|
|||||||
.values({
|
.values({
|
||||||
userId: params.user.id,
|
userId: params.user.id,
|
||||||
name: params.name,
|
name: params.name,
|
||||||
publicKey: keys.pubkey,
|
publicKey: keys.publicKey,
|
||||||
privateKey: keys.privkey,
|
privateKey: keys.privateKey,
|
||||||
preSharedKey: keys.psk,
|
preSharedKey: keys.preSharedKey,
|
||||||
})
|
})
|
||||||
.returning({ id: devices.id });
|
.returning({ id: devices.id });
|
||||||
|
|
||||||
@ -78,79 +80,17 @@ export async function createDevice(params: {
|
|||||||
.where(eq(ipAllocations.id, ipAllocationId));
|
.where(eq(ipAllocations.id, ipAllocationId));
|
||||||
|
|
||||||
// create client in opnsense
|
// create client in opnsense
|
||||||
const opnsenseRes = await opnsenseCreateClient({
|
const providerRes = await wgProvider.createClient({
|
||||||
username: params.user.username,
|
user: params.user,
|
||||||
pubkey: keys.pubkey,
|
publicKey: keys.publicKey,
|
||||||
psk: keys.psk,
|
preSharedKey: keys.preSharedKey,
|
||||||
allowedIps: getIpsFromIndex(ipAllocationId).join(','),
|
allowedIps: getIpsFromIndex(ipAllocationId).join(','),
|
||||||
});
|
});
|
||||||
const opnsenseResJson = await opnsenseRes.json();
|
if (providerRes._tag === 'err') {
|
||||||
if (opnsenseResJson['result'] !== 'saved') {
|
|
||||||
tx2.rollback();
|
tx2.rollback();
|
||||||
console.error(`Error creating client in OPNsense: \n${opnsenseResJson}`);
|
return err([500, 'Failed to create client in OPNsense']);
|
||||||
return err([500, 'Error creating client in OPNsense'] as [500, string]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// reconfigure opnsense
|
|
||||||
await opnsenseReconfigure();
|
|
||||||
return ok(newDevice.id);
|
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',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
@ -2,7 +2,6 @@ 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 { opnsenseAuth, opnsenseUrl } from '$lib/server/opnsense';
|
|
||||||
|
|
||||||
export async function deleteDevice(
|
export async function deleteDevice(
|
||||||
userId: string,
|
userId: string,
|
||||||
|
@ -2,7 +2,6 @@ import { db } from '$lib/server/db';
|
|||||||
import { and, eq } from 'drizzle-orm';
|
import { and, eq } from 'drizzle-orm';
|
||||||
import { devices } from '$lib/server/db/schema';
|
import { devices } from '$lib/server/db/schema';
|
||||||
import type { DeviceDetails } from '$lib/devices';
|
import type { DeviceDetails } from '$lib/devices';
|
||||||
import { serverPublicKey } from '$lib/server/opnsense';
|
|
||||||
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';
|
||||||
|
|
||||||
@ -49,7 +48,7 @@ export function mapDeviceToDetails(
|
|||||||
privateKey: device.privateKey,
|
privateKey: device.privateKey,
|
||||||
preSharedKey: device.preSharedKey,
|
preSharedKey: device.preSharedKey,
|
||||||
ips,
|
ips,
|
||||||
vpnPublicKey: serverPublicKey,
|
vpnPublicKey: '', // TODO: fix
|
||||||
vpnEndpoint: env.VPN_ENDPOINT,
|
vpnEndpoint: env.VPN_ENDPOINT,
|
||||||
vpnDns: env.VPN_DNS,
|
vpnDns: env.VPN_DNS,
|
||||||
};
|
};
|
||||||
|
168
src/lib/server/providers/opnsense.ts
Normal file
168
src/lib/server/providers/opnsense.ts
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import type { IWgProvider } 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 { opnsenseSanitezedUsername } from '$lib/opnsense';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
|
||||||
|
export class WgProviderOpnsense implements IWgProvider {
|
||||||
|
private opnsenseUrl: string;
|
||||||
|
private opnsenseAuth: string;
|
||||||
|
private opnsenseIfname: string;
|
||||||
|
private opnsenseWgServerUuid: 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() {
|
||||||
|
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<OpnsenseWgServers>(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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug('OPNsense WireGuard server UUID:', uuid);
|
||||||
|
this.opnsenseWgServerUuid = uuid;
|
||||||
|
return ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async generateKeys() {
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
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<OpnsenseWgPeers>(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-tx'],
|
||||||
|
transferTx: peer['transfer-rx'],
|
||||||
|
latestHandshake: peer['latest-handshake'] * 1000,
|
||||||
|
};
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteClient(publicKey) {
|
||||||
|
return err(new Error('OPNsense deleteClient not implemented'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OpnsenseParams = {
|
||||||
|
opnsenseUrl: string;
|
||||||
|
opnsenseApiKey: string;
|
||||||
|
opnsenseApiSecret: string;
|
||||||
|
opnsenseWgIfname: string;
|
||||||
|
};
|
39
src/lib/server/types/index.ts
Normal file
39
src/lib/server/types/index.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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<Result<null, Error>>;
|
||||||
|
|
||||||
|
// TODO: might be better to not make this a result type
|
||||||
|
getServerPublicKey(): Promise<Result<string, Error>>;
|
||||||
|
|
||||||
|
// TODO: might be better to not make this a result type
|
||||||
|
generateKeys(): Promise<Result<WgKeys, Error>>;
|
||||||
|
|
||||||
|
createClient(params: {
|
||||||
|
user: User;
|
||||||
|
publicKey: string;
|
||||||
|
preSharedKey: string;
|
||||||
|
allowedIps: string;
|
||||||
|
}): Promise<Result<null, Error>>;
|
||||||
|
|
||||||
|
findConnections(user: User): Promise<Result<ClientConnection[], Error>>;
|
||||||
|
|
||||||
|
deleteClient(publicKey: string): Promise<Result<null, Error>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WgKeys = {
|
||||||
|
publicKey: string;
|
||||||
|
privateKey: string;
|
||||||
|
preSharedKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClientConnection = {
|
||||||
|
publicKey: string;
|
||||||
|
endpoint: string;
|
||||||
|
allowedIps: string;
|
||||||
|
transferRx: number;
|
||||||
|
transferTx: number;
|
||||||
|
latestHandshake: number;
|
||||||
|
}
|
@ -1,27 +1,17 @@
|
|||||||
class Ok<T> {
|
import type { Address4, Address6 } from 'ip-address';
|
||||||
readonly _tag = 'ok';
|
|
||||||
value: T;
|
|
||||||
|
|
||||||
constructor(value: T) {
|
export type { Result } from './result';
|
||||||
this.value = value;
|
export { ok, err } from './result';
|
||||||
|
|
||||||
|
export interface IWGProviderConfig {
|
||||||
|
ipv4?: {
|
||||||
|
startingAddr: Address4;
|
||||||
|
};
|
||||||
|
ipv6?: {
|
||||||
|
startingAddr: Address6;
|
||||||
|
clientPrefixSize: number;
|
||||||
}
|
}
|
||||||
}
|
endpoint: string;
|
||||||
|
dns: string;
|
||||||
class Err<E> {
|
addrMaxIndex: number;
|
||||||
readonly _tag = 'err';
|
|
||||||
error: E;
|
|
||||||
|
|
||||||
constructor(error: E) {
|
|
||||||
this.error = error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Result<T, E> = Ok<T> | Err<E>;
|
|
||||||
|
|
||||||
export function err<E>(e: E): Err<E> {
|
|
||||||
return new Err(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ok<T>(t: T): Ok<T> {
|
|
||||||
return new Ok(t);
|
|
||||||
}
|
}
|
||||||
|
27
src/lib/types/result.ts
Normal file
27
src/lib/types/result.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
class Ok<T> {
|
||||||
|
readonly _tag = 'ok';
|
||||||
|
value: T;
|
||||||
|
|
||||||
|
constructor(value: T) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Err<E> {
|
||||||
|
readonly _tag = 'err';
|
||||||
|
error: E;
|
||||||
|
|
||||||
|
constructor(error: E) {
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Result<T, E> = Ok<T> | Err<E>;
|
||||||
|
|
||||||
|
export function err<E>(e: E): Err<E> {
|
||||||
|
return new Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ok<T>(t: T): Ok<T> {
|
||||||
|
return new Ok(t);
|
||||||
|
}
|
@ -1,44 +1,42 @@
|
|||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types';
|
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 { findDevices } from '$lib/server/devices';
|
||||||
import type { ConnectionDetails } from '$lib/connections';
|
import type { ConnectionDetails } from '$lib/connections';
|
||||||
import { opnsenseSanitezedUsername } from '$lib/opnsense';
|
import type { Result } from '$lib/types';
|
||||||
|
import type { ClientConnection } from '$lib/server/types';
|
||||||
|
|
||||||
export const GET: RequestHandler = async (event) => {
|
export const GET: RequestHandler = async (event) => {
|
||||||
if (!event.locals.user) {
|
if (!event.locals.user) {
|
||||||
return error(401, 'Unauthorized');
|
return error(401, 'Unauthorized');
|
||||||
}
|
}
|
||||||
console.debug('/api/connections');
|
console.debug('/api/connections');
|
||||||
const peers = await fetchOpnsensePeers(event.locals.user.username);
|
|
||||||
console.debug('/api/connections: fetched opnsense peers', peers.rowCount);
|
const peersResult: Result<ClientConnection[], Error> = await event.locals.wgProvider.findConnections(event.locals.user);
|
||||||
|
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);
|
||||||
console.debug('/api/connections: fetched db devices');
|
console.debug('/api/connections: fetched db devices');
|
||||||
|
|
||||||
if (!peers) {
|
|
||||||
return error(500, 'Error getting info from OPNsense API');
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this is all garbage performance
|
// TODO: this is all garbage performance
|
||||||
// filter devices with no recent handshakes
|
// 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
|
// start from devices, to treat db as the source of truth
|
||||||
const connections: ConnectionDetails[] = [];
|
const connections: ConnectionDetails[] = [];
|
||||||
for (const device of devices) {
|
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;
|
if (!peerData) continue;
|
||||||
connections.push({
|
connections.push({
|
||||||
deviceId: device.id,
|
deviceId: device.id,
|
||||||
deviceName: device.name,
|
deviceName: device.name,
|
||||||
devicePublicKey: device.publicKey,
|
devicePublicKey: device.publicKey,
|
||||||
deviceIps: peerData['allowed-ips'].split(','),
|
deviceIps: peerData.allowedIps.split(','),
|
||||||
endpoint: peerData['endpoint'],
|
endpoint: peerData.endpoint,
|
||||||
// swap rx and tx, since the opnsense values are from the server perspective
|
// swap rx and tx, since the opnsense values are from the server perspective
|
||||||
transferRx: peerData['transfer-tx'],
|
transferRx: peerData.transferTx,
|
||||||
transferTx: peerData['transfer-rx'],
|
transferTx: peerData.transferRx,
|
||||||
latestHandshake: peerData['latest-handshake'] * 1000,
|
latestHandshake: peerData.latestHandshake,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,25 +47,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;
|
|
||||||
}
|
|
||||||
|
@ -8,7 +8,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({
|
const res = await createDevice(event.locals.wgProvider, {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
user: event.locals.user,
|
user: event.locals.user,
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user