Compare commits
3 Commits
76559d2931
...
feature/lo
| Author | SHA1 | Date | |
|---|---|---|---|
|
b38ab19c3e
|
|||
|
80acec720c
|
|||
|
29fbccc953
|
1
bunfig.toml
Normal file
1
bunfig.toml
Normal file
@@ -0,0 +1 @@
|
||||
logLevel = "info"
|
||||
@@ -88,7 +88,7 @@
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--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
10
src/lib/connections.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export type ConnectionDetails = {
|
||||
deviceId: number;
|
||||
deviceName: string;
|
||||
devicePublicKey: string;
|
||||
deviceIps: string[];
|
||||
endpoint: string;
|
||||
transferRx: number;
|
||||
transferTx: number;
|
||||
latestHandshake: number;
|
||||
};
|
||||
3
src/lib/opnsense/index.ts
Normal file
3
src/lib/opnsense/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function opnsenseSanitezedUsername(username: string) {
|
||||
return username.slice(0, 63).replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { env } from '$env/dynamic/private';
|
||||
import { and, count, eq, isNull } from 'drizzle-orm';
|
||||
import { err, ok, type Result } from '$lib/types';
|
||||
import type { DeviceDetails } from '$lib/devices';
|
||||
import { opnsenseSanitezedUsername } from '$lib/opnsense';
|
||||
|
||||
export async function findDevices(userId: string) {
|
||||
return db.query.devices.findMany({
|
||||
@@ -196,7 +197,7 @@ async function opnsenseCreateClient(params: {
|
||||
body: JSON.stringify({
|
||||
configbuilder: {
|
||||
enabled: '1',
|
||||
name: `vpgen-${params.username}`,
|
||||
name: `vpgen-${opnsenseSanitezedUsername(params.username)}`,
|
||||
pubkey: params.pubkey,
|
||||
psk: params.psk,
|
||||
tunneladdress: params.allowedIps,
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function fetchOpnsenseServer() {
|
||||
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);
|
||||
console.info('OPNsense WireGuard server UUID:', serverUuid);
|
||||
|
||||
const resServerInfo = await fetch(
|
||||
`${opnsenseUrl}/api/wireguard/client/get_server_info/${serverUuid}`,
|
||||
|
||||
@@ -2,13 +2,56 @@ import { error } from '@sveltejs/kit';
|
||||
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 type { ConnectionDetails } from '$lib/connections';
|
||||
import { opnsenseSanitezedUsername } from '$lib/opnsense';
|
||||
|
||||
export const GET: RequestHandler = async (event) => {
|
||||
if (!event.locals.user) {
|
||||
return error(401, 'Unauthorized');
|
||||
}
|
||||
const apiUrl = `${opnsenseUrl}/api/wireguard/service/show`;
|
||||
const options: RequestInit = {
|
||||
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 res = await fetch(`${opnsenseUrl}/api/wireguard/service/show`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: opnsenseAuth,
|
||||
@@ -16,28 +59,15 @@ export const GET: RequestHandler = async (event) => {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'current': 1,
|
||||
current: 1,
|
||||
// "rowCount": 7,
|
||||
'sort': {},
|
||||
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-${event.locals.user.username}`,
|
||||
'type': ['peer'],
|
||||
searchPhrase: `vpgen-${opnsenseSanitezedUsername(username)}`,
|
||||
type: ['peer'],
|
||||
}),
|
||||
};
|
||||
|
||||
const res = await fetch(apiUrl, options);
|
||||
const peers = 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',
|
||||
}
|
||||
});
|
||||
};
|
||||
return (await res.json()) as OpnsenseWgPeers;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function GET(event: RequestEvent): Promise<Response> {
|
||||
});
|
||||
}
|
||||
const claims = decodeIdToken(tokens.idToken()) as { sub: string, preferred_username: string, name: string };
|
||||
console.log("claims", claims);
|
||||
console.info("claims", claims);
|
||||
const userId: string = claims.sub;
|
||||
const username: string = claims.preferred_username;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
|
||||
function getSize(size: number) {
|
||||
function toSizeString(size: number) {
|
||||
let sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||
|
||||
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.Header>
|
||||
<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">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">RX</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.Header>
|
||||
<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.Head scope="row">{peer.name}</Table.Head>
|
||||
<Table.Cell class="max-w-[10ch] truncate">{peer['public-key']}</Table.Cell>
|
||||
<Table.Cell>{peer.endpoint}</Table.Cell>
|
||||
<Table.Head scope="row">{conn.deviceName}</Table.Head>
|
||||
<Table.Cell class="max-w-[10ch] truncate">{conn.devicePublicKey}</Table.Cell>
|
||||
<Table.Cell>{conn.endpoint}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<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>
|
||||
{/each}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell>{new Date(peer['latest-handshake'] * 1000).toLocaleString()}</Table.Cell>
|
||||
<Table.Cell>{getSize(peer['transfer-rx'])}</Table.Cell>
|
||||
<Table.Cell>{getSize(peer['transfer-tx'])}</Table.Cell>
|
||||
<Table.Cell class="hidden">{peer['persistent-keepalive']}</Table.Cell>
|
||||
<Table.Cell class="hidden">{peer.ifname}</Table.Cell>
|
||||
<Table.Cell>{new Date(conn.latestHandshake).toLocaleString()}</Table.Cell>
|
||||
<Table.Cell>{toSizeString(conn.transferRx)}</Table.Cell>
|
||||
<Table.Cell>{toSizeString(conn.transferTx)}</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import type { OpnsenseWgPeers } from '$lib/opnsense/wg';
|
||||
import type { ConnectionDetails } from '$lib/connections';
|
||||
|
||||
export const load: PageLoad = async ({ fetch }) => {
|
||||
const res = await fetch('/api/connections');
|
||||
const peers = await res.json() as OpnsenseWgPeers;
|
||||
const connections = await res.json() as ConnectionDetails[];
|
||||
|
||||
return { peers };
|
||||
return { connections };
|
||||
};
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<Table.Row class="hover:bg-surface group">
|
||||
<Table.Head scope="row">
|
||||
<a
|
||||
href={`/devices/${device.id}`}
|
||||
href="/devices/{device.id}"
|
||||
class="flex size-full items-center group-hover:underline"
|
||||
>
|
||||
{device.name}
|
||||
|
||||
Reference in New Issue
Block a user