connections page: update API, combine opnsense data with db data

This commit is contained in:
Yuri Tatishchev 2025-01-07 18:51:24 -08:00
parent 76559d2931
commit 29fbccc953
Signed by: CaZzzer
GPG Key ID: E0EBF441EA424369
7 changed files with 75 additions and 37 deletions

View File

@ -88,7 +88,7 @@
--sidebar-border: 240 3.7% 15.9%; --sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%; --sidebar-ring: 217.2 91.2% 59.8%;
--surface: 217.2 40.6% 10.5%; --surface: 217.2 40.6% 11.5%;
} }
} }
} }

10
src/lib/connections.ts Normal file
View File

@ -0,0 +1,10 @@
export type ConnectionDetails = {
deviceId: number;
deviceName: string;
devicePublicKey: string;
deviceIps: string[];
endpoint: string;
transferRx: number;
transferTx: number;
latestHandshake: number;
};

View File

@ -2,11 +2,54 @@ import { error } from '@sveltejs/kit';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
import { opnsenseAuth, opnsenseUrl } from '$lib/server/opnsense'; import { opnsenseAuth, opnsenseUrl } from '$lib/server/opnsense';
import type { OpnsenseWgPeers } from '$lib/opnsense/wg'; import type { OpnsenseWgPeers } from '$lib/opnsense/wg';
import { findDevices } from '$lib/server/devices';
import type { ConnectionDetails } from '$lib/connections';
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');
const peers = await fetchOpnsensePeers(event.locals.user.username);
console.debug('/api/connections: fetched opnsense peers', peers.rowCount);
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']);
// 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);
if (!peerData) continue;
connections.push({
deviceId: device.id,
deviceName: device.name,
devicePublicKey: device.publicKey,
deviceIps: peerData['allowed-ips'].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,
});
}
return new Response(JSON.stringify(connections), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'max-age=5',
},
});
};
async function fetchOpnsensePeers(username: string) {
const apiUrl = `${opnsenseUrl}/api/wireguard/service/show`; const apiUrl = `${opnsenseUrl}/api/wireguard/service/show`;
const options: RequestInit = { const options: RequestInit = {
method: 'POST', method: 'POST',
@ -16,28 +59,17 @@ export const GET: RequestHandler = async (event) => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
'current': 1, current: 1,
// "rowCount": 7, // "rowCount": 7,
'sort': {}, sort: {},
// TODO: use a more unique search phrase // TODO: use a more unique search phrase
// unfortunately 64 character limit, // unfortunately 64 character limit,
// but it should be fine if users can't change their own username // but it should be fine if users can't change their own username
'searchPhrase': `vpgen-${event.locals.user.username}`, searchPhrase: `vpgen-${username}`,
'type': ['peer'], type: ['peer'],
}), }),
}; };
const res = await fetch(apiUrl, options); const res = await fetch(apiUrl, options);
const peers = await res.json() as OpnsenseWgPeers; return (await res.json()) as OpnsenseWgPeers;
peers.rows = peers.rows.filter(peer => peer['latest-handshake']) }
if (!peers) {
return error(500, 'Error getting info from OPNsense API');
}
return new Response(JSON.stringify(peers), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'max-age=5',
}
});
};

View File

@ -16,7 +16,7 @@
return () => clearInterval(interval); return () => clearInterval(interval);
}); });
function getSize(size: number) { function toSizeString(size: number) {
let sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; let sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
for (let i = 1; i < sizes.length; i++) { for (let i = 1; i < sizes.length; i++) {
@ -34,35 +34,31 @@
<Table.Root class="divide-y-2 divide-background overflow-hidden rounded-lg bg-accent"> <Table.Root class="divide-y-2 divide-background overflow-hidden rounded-lg bg-accent">
<Table.Header> <Table.Header>
<Table.Row> <Table.Row>
<Table.Head scope="col">Name</Table.Head> <Table.Head scope="col">Device</Table.Head>
<Table.Head scope="col">Public Key</Table.Head> <Table.Head scope="col">Public Key</Table.Head>
<Table.Head scope="col">Endpoint</Table.Head> <Table.Head scope="col">Endpoint</Table.Head>
<Table.Head scope="col">Allowed IPs</Table.Head> <Table.Head scope="col">Device IPs</Table.Head>
<Table.Head scope="col">Latest Handshake</Table.Head> <Table.Head scope="col">Latest Handshake</Table.Head>
<Table.Head scope="col">RX</Table.Head> <Table.Head scope="col">RX</Table.Head>
<Table.Head scope="col">TX</Table.Head> <Table.Head scope="col">TX</Table.Head>
<Table.Head scope="col" class="hidden">Persistent Keepalive</Table.Head>
<Table.Head scope="col" class="hidden">Interface Name</Table.Head>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body class="divide-y-2 divide-background"> <Table.Body class="divide-y-2 divide-background">
{#each data.peers.rows as peer} {#each data.connections as conn}
<Table.Row class="hover:bg-surface"> <Table.Row class="hover:bg-surface">
<Table.Head scope="row">{peer.name}</Table.Head> <Table.Head scope="row">{conn.deviceName}</Table.Head>
<Table.Cell class="max-w-[10ch] truncate">{peer['public-key']}</Table.Cell> <Table.Cell class="max-w-[10ch] truncate">{conn.devicePublicKey}</Table.Cell>
<Table.Cell>{peer.endpoint}</Table.Cell> <Table.Cell>{conn.endpoint}</Table.Cell>
<Table.Cell> <Table.Cell>
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
{#each peer['allowed-ips'].split(',') as addr} {#each conn.deviceIps as addr}
<Badge class="select-auto bg-background" variant="secondary">{addr}</Badge> <Badge class="select-auto bg-background" variant="secondary">{addr}</Badge>
{/each} {/each}
</div> </div>
</Table.Cell> </Table.Cell>
<Table.Cell>{new Date(peer['latest-handshake'] * 1000).toLocaleString()}</Table.Cell> <Table.Cell>{new Date(conn.latestHandshake).toLocaleString()}</Table.Cell>
<Table.Cell>{getSize(peer['transfer-rx'])}</Table.Cell> <Table.Cell>{toSizeString(conn.transferRx)}</Table.Cell>
<Table.Cell>{getSize(peer['transfer-tx'])}</Table.Cell> <Table.Cell>{toSizeString(conn.transferTx)}</Table.Cell>
<Table.Cell class="hidden">{peer['persistent-keepalive']}</Table.Cell>
<Table.Cell class="hidden">{peer.ifname}</Table.Cell>
</Table.Row> </Table.Row>
{/each} {/each}
</Table.Body> </Table.Body>

View File

@ -1,9 +1,9 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
import type { OpnsenseWgPeers } from '$lib/opnsense/wg'; import type { ConnectionDetails } from '$lib/connections';
export const load: PageLoad = async ({ fetch }) => { export const load: PageLoad = async ({ fetch }) => {
const res = await fetch('/api/connections'); const res = await fetch('/api/connections');
const peers = await res.json() as OpnsenseWgPeers; const connections = await res.json() as ConnectionDetails[];
return { peers }; return { connections };
}; };

View File

@ -42,7 +42,7 @@
<Table.Row class="hover:bg-surface group"> <Table.Row class="hover:bg-surface group">
<Table.Head scope="row"> <Table.Head scope="row">
<a <a
href={`/devices/${device.id}`} href="/devices/{device.id}"
class="flex size-full items-center group-hover:underline" class="flex size-full items-center group-hover:underline"
> >
{device.name} {device.name}