opnsense: filter queried connections
This commit is contained in:
parent
e03bf11fa5
commit
bdea663178
@ -1,4 +1,4 @@
|
|||||||
DATABASE_URL=local.db
|
DATABASE_URL=file:local.db
|
||||||
AUTH_DOMAIN=auth.lab.cazzzer.com
|
AUTH_DOMAIN=auth.lab.cazzzer.com
|
||||||
AUTH_CLIENT_ID=
|
AUTH_CLIENT_ID=
|
||||||
AUTH_CLIENT_SECRET=
|
AUTH_CLIENT_SECRET=
|
||||||
|
10
package.json
10
package.json
@ -11,8 +11,10 @@
|
|||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "prettier --check . && eslint .",
|
"lint": "prettier --check . && eslint .",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:studio": "drizzle-kit studio"
|
"db:studio": "drizzle-kit studio",
|
||||||
|
"db:seed": "bun run ./src/lib/server/db/seed.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
@ -26,7 +28,7 @@
|
|||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"bits-ui": "^0.21.16",
|
"bits-ui": "^0.21.16",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"drizzle-kit": "^0.22.0",
|
"drizzle-kit": "^0.30.1",
|
||||||
"eslint": "^9.7.0",
|
"eslint": "^9.7.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-svelte": "^2.36.0",
|
"eslint-plugin-svelte": "^2.36.0",
|
||||||
@ -44,11 +46,11 @@
|
|||||||
"vite": "^5.0.3"
|
"vite": "^5.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@libsql/client": "^0.14.0",
|
||||||
"@oslojs/crypto": "^1.0.1",
|
"@oslojs/crypto": "^1.0.1",
|
||||||
"@oslojs/encoding": "^1.1.0",
|
"@oslojs/encoding": "^1.1.0",
|
||||||
"arctic": "^2.2.1",
|
"arctic": "^2.2.1",
|
||||||
"better-sqlite3": "^11.1.2",
|
"drizzle-orm": "^0.38.2",
|
||||||
"drizzle-orm": "^0.33.0",
|
|
||||||
"lucide-svelte": "^0.454.0"
|
"lucide-svelte": "^0.454.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,3 +59,11 @@ export interface OpnsenseWgPeers {
|
|||||||
* };
|
* };
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export interface OpnsenseWgServers {
|
||||||
|
status: "ok" | string | number;
|
||||||
|
rows: {
|
||||||
|
name: string;
|
||||||
|
uuid: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
@ -23,7 +23,7 @@ export async function createSession(userId: string): Promise<table.Session> {
|
|||||||
userId,
|
userId,
|
||||||
expiresAt: new Date(Date.now() + DAY_IN_MS * 30)
|
expiresAt: new Date(Date.now() + DAY_IN_MS * 30)
|
||||||
};
|
};
|
||||||
await db.insert(table.session).values(session);
|
await db.insert(table.sessions).values(session);
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ export function setSessionTokenCookie(event: RequestEvent, sessionId: string, ex
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function invalidateSession(sessionId: string): Promise<void> {
|
export async function invalidateSession(sessionId: string): Promise<void> {
|
||||||
await db.delete(table.session).where(eq(table.session.id, sessionId));
|
await db.delete(table.sessions).where(eq(table.sessions.id, sessionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteSessionTokenCookie(event: RequestEvent) {
|
export function deleteSessionTokenCookie(event: RequestEvent) {
|
||||||
@ -49,12 +49,12 @@ 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.user.id, username: table.user.username, name: table.user.name },
|
user: { id: table.users.id, username: table.users.username, name: table.users.name },
|
||||||
session: table.session
|
session: table.sessions
|
||||||
})
|
})
|
||||||
.from(table.session)
|
.from(table.sessions)
|
||||||
.innerJoin(table.user, eq(table.session.userId, table.user.id))
|
.innerJoin(table.users, eq(table.sessions.userId, table.users.id))
|
||||||
.where(eq(table.session.id, sessionId));
|
.where(eq(table.sessions.id, sessionId));
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return { session: null, user: null };
|
return { session: null, user: null };
|
||||||
@ -63,7 +63,7 @@ export async function validateSession(sessionId: string) {
|
|||||||
|
|
||||||
const sessionExpired = Date.now() >= session.expiresAt.getTime();
|
const sessionExpired = Date.now() >= session.expiresAt.getTime();
|
||||||
if (sessionExpired) {
|
if (sessionExpired) {
|
||||||
await db.delete(table.session).where(eq(table.session.id, session.id));
|
await db.delete(table.sessions).where(eq(table.sessions.id, session.id));
|
||||||
return { session: null, user: null };
|
return { session: null, user: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,9 +71,9 @@ export async function validateSession(sessionId: string) {
|
|||||||
if (renewSession) {
|
if (renewSession) {
|
||||||
session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30);
|
session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30);
|
||||||
await db
|
await db
|
||||||
.update(table.session)
|
.update(table.sessions)
|
||||||
.set({ expiresAt: session.expiresAt })
|
.set({ expiresAt: session.expiresAt })
|
||||||
.where(eq(table.session.id, session.id));
|
.where(eq(table.sessions.id, session.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
return { session, user };
|
return { session, user };
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
import { drizzle } from 'drizzle-orm/libsql';
|
||||||
import Database from 'better-sqlite3';
|
|
||||||
import { env } from '$env/dynamic/private';
|
|
||||||
import assert from 'node:assert';
|
import assert from 'node:assert';
|
||||||
|
import * as schema from './schema';
|
||||||
|
import { DATABASE_URL } from '$env/static/private';
|
||||||
|
|
||||||
assert(env.DATABASE_URL, 'DATABASE_URL is not set');
|
assert(DATABASE_URL, 'DATABASE_URL is not set');
|
||||||
const client = new Database(env.DATABASE_URL);
|
export const db= drizzle(DATABASE_URL, { schema });
|
||||||
export const db = drizzle(client);
|
|
||||||
|
@ -1,19 +1,59 @@
|
|||||||
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
|
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
|
||||||
|
import { relations } from 'drizzle-orm';
|
||||||
|
|
||||||
export const user = sqliteTable('user', {
|
export const users = sqliteTable('users', {
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
username: text('username').notNull(),
|
username: text('username').notNull(),
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const session = sqliteTable('session', {
|
export const usersRelations = relations(users, ({ many }) => ({
|
||||||
|
wgClients: many(wgClients),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const sessions = sqliteTable('sessions', {
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
userId: text('user_id')
|
userId: text('user_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => user.id),
|
.references(() => users.id),
|
||||||
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull()
|
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Session = typeof session.$inferSelect;
|
export const ipAllocations = sqliteTable('ip_allocations', {
|
||||||
|
// for now, id will be the same as the ipIndex
|
||||||
|
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||||
|
// clientId is nullable because allocations can remain after the client is deleted
|
||||||
|
// unique for now, only allowing one allocation per client
|
||||||
|
clientId: integer('client_id')
|
||||||
|
.unique()
|
||||||
|
.references(() => wgClients.id),
|
||||||
|
});
|
||||||
|
|
||||||
export type User = typeof user.$inferSelect;
|
export const wgClients = sqliteTable('wg_clients', {
|
||||||
|
id: integer().primaryKey({ autoIncrement: true }),
|
||||||
|
userId: text('user_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
// questioning whether this should be nullable
|
||||||
|
opnsenseId: text('opnsense_id'),
|
||||||
|
publicKey: text('public_key').notNull().unique(),
|
||||||
|
// nullable for the possibility of a client supplying their own private key
|
||||||
|
privateKey: text('private_key'),
|
||||||
|
// nullable for the possibility of no psk
|
||||||
|
preSharedKey: text('pre_shared_key'),
|
||||||
|
// discarded ideas:
|
||||||
|
// (mostly because they make finding the next available ipIndex difficult)
|
||||||
|
// ipIndex: integer('ip_index').notNull().unique(),
|
||||||
|
// allowedIps: text('allowed_ips').notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const wgClientsRelations = relations(wgClients, ({ one }) => ({
|
||||||
|
ipAllocation: one(ipAllocations),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export type WgClient = typeof wgClients.$inferSelect;
|
||||||
|
|
||||||
|
export type Session = typeof sessions.$inferSelect;
|
||||||
|
|
||||||
|
export type User = typeof users.$inferSelect;
|
||||||
|
29
src/lib/server/db/seed.ts
Normal file
29
src/lib/server/db/seed.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { users, wgClients } from './schema';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
import { drizzle } from 'drizzle-orm/libsql';
|
||||||
|
import * as schema from '$lib/server/db/schema';
|
||||||
|
|
||||||
|
assert(process.env.DATABASE_URL, 'DATABASE_URL is not set');
|
||||||
|
const db = drizzle(process.env.DATABASE_URL, { schema });
|
||||||
|
|
||||||
|
export async function seed() {
|
||||||
|
const user = await db.query.users.findFirst({ where: eq(users.username, 'CaZzzer') });
|
||||||
|
assert(user, 'User not found');
|
||||||
|
|
||||||
|
const clients = [
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
name: 'Client1',
|
||||||
|
publicKey: 'BJ5faPVJsDP4CCxNYilmKnwlQXOtXEOJjqIwb4U/CgM=',
|
||||||
|
privateKey: 'KKqsHDu30WCSrVsyzMkOKbE3saQ+wlx0sBwGs61UGXk=',
|
||||||
|
preSharedKey: '0LWopbrISXBNHUxr+WOhCSAg+0hD8j3TLmpyzHkBHCQ=',
|
||||||
|
ipIndex: 1,
|
||||||
|
// allowedIps: '10.18.11.101/32,fd00::1/112',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
await db.insert(wgClients).values(clients);
|
||||||
|
}
|
||||||
|
|
||||||
|
seed();
|
@ -1,16 +0,0 @@
|
|||||||
import { env } from '$env/dynamic/private';
|
|
||||||
import assert from 'node:assert';
|
|
||||||
import { encodeBasicCredentials } from 'arctic/dist/request';
|
|
||||||
import { dev } from '$app/environment';
|
|
||||||
|
|
||||||
assert(env.OPNSENSE_API_URL, 'OPNSENSE_API_URL is not set');
|
|
||||||
assert(env.OPNSENSE_API_KEY, 'OPNSENSE_API_KEY is not set');
|
|
||||||
assert(env.OPNSENSE_API_SECRET, 'OPNSENSE_API_SECRET is not set');
|
|
||||||
assert(env.OPNSENSE_WG_IFNAME, 'OPNSENSE_WG_IFNAME is not set');
|
|
||||||
|
|
||||||
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 = "";
|
|
33
src/lib/server/opnsense/index.ts
Normal file
33
src/lib/server/opnsense/index.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
assert(env.OPNSENSE_API_URL, 'OPNSENSE_API_URL is not set');
|
||||||
|
assert(env.OPNSENSE_API_KEY, 'OPNSENSE_API_KEY is not set');
|
||||||
|
assert(env.OPNSENSE_API_SECRET, 'OPNSENSE_API_SECRET is not set');
|
||||||
|
assert(env.OPNSENSE_WG_IFNAME, 'OPNSENSE_WG_IFNAME is not set');
|
||||||
|
|
||||||
|
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 = "";
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
export const serverUuid = servers.rows.find(server => server.name === opnsenseIfname)?.uuid;
|
||||||
|
assert(serverUuid, 'Failed to find server UUID for OPNsense WireGuard server');
|
||||||
|
console.log('OPNsense WireGuard server UUID:', serverUuid);
|
@ -1,9 +1,12 @@
|
|||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { opnsenseAuth, opnsenseIfname, 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';
|
||||||
|
|
||||||
export const GET: RequestHandler = async () => {
|
export const GET: RequestHandler = async (event) => {
|
||||||
|
if (!event.locals.user) {
|
||||||
|
return error(401, 'Unauthorized');
|
||||||
|
}
|
||||||
const apiUrl = `${opnsenseUrl}/api/wireguard/service/show`;
|
const apiUrl = `${opnsenseUrl}/api/wireguard/service/show`;
|
||||||
const options: RequestInit = {
|
const options: RequestInit = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -13,21 +16,24 @@ export const GET: RequestHandler = async () => {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
"current": 1,
|
'current': 1,
|
||||||
// "rowCount": 7,
|
// "rowCount": 7,
|
||||||
"sort": {},
|
'sort': {},
|
||||||
"searchPhrase": "",
|
// TODO: use a more unique search phrase
|
||||||
"type": ["peer"],
|
// 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'],
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
console.log("Fetching peers from OPNsense WireGuard API: ", apiUrl, options)
|
console.log('Fetching peers from OPNsense WireGuard API: ', apiUrl, options)
|
||||||
|
|
||||||
const res = await fetch(apiUrl, options);
|
const res = await fetch(apiUrl, options);
|
||||||
const peers = await res.json() as OpnsenseWgPeers;
|
const peers = await res.json() as OpnsenseWgPeers;
|
||||||
peers.rows = peers.rows.filter(peer => peer['latest-handshake'] && peer.ifname === opnsenseIfname)
|
peers.rows = peers.rows.filter(peer => peer['latest-handshake'])
|
||||||
|
|
||||||
if (!peers) {
|
if (!peers) {
|
||||||
error(500, "Error getting info from OPNsense API");
|
return error(500, 'Error getting info from OPNsense API');
|
||||||
}
|
}
|
||||||
return new Response(JSON.stringify(peers), {
|
return new Response(JSON.stringify(peers), {
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -39,7 +39,7 @@ export async function GET(event: RequestEvent): Promise<Response> {
|
|||||||
const userId: string = claims.sub;
|
const userId: string = claims.sub;
|
||||||
const username: string = claims.preferred_username;
|
const username: string = claims.preferred_username;
|
||||||
|
|
||||||
const [existingUser] = await db.select().from(table.user).where(eq(table.user.id, userId));
|
const existingUser = await db.query.users.findFirst({where: eq(table.users.id, userId)});
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
const session = await createSession(existingUser.id);
|
const session = await createSession(existingUser.id);
|
||||||
@ -59,7 +59,7 @@ export async function GET(event: RequestEvent): Promise<Response> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.insert(table.user).values(user);
|
await db.insert(table.users).values(user);
|
||||||
const session = await createSession(user.id);
|
const session = await createSession(user.id);
|
||||||
setSessionTokenCookie(event, session.id, session.expiresAt);
|
setSessionTokenCookie(event, session.id, session.expiresAt);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user