From 04911898506f69c42bf6b2f695486c9d017fa773 Mon Sep 17 00:00:00 2001 From: Yuri Tatishchev Date: Sun, 11 May 2025 00:40:32 -0700 Subject: [PATCH] WIP: implement wg-quick as a provider --- .env.example | 7 ++ .gitignore | 3 + bun.lock | 5 ++ package.json | 7 +- src/lib/server/wg-provider.ts | 24 +++++- src/lib/server/wg-providers/wg-quick/index.ts | 76 +++++++++++++++++++ .../server/wg-providers/wg-quick/snippets.ts | 59 ++++++++++++++ 7 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 src/lib/server/wg-providers/wg-quick/index.ts create mode 100644 src/lib/server/wg-providers/wg-quick/snippets.ts diff --git a/.env.example b/.env.example index 4ef10a6..50cdcd3 100644 --- a/.env.example +++ b/.env.example @@ -13,11 +13,18 @@ AUTH_GOOGLE_CLIENT_SECRET= AUTH_INVITE_TOKEN=GUjdsz9aREFTEBYDrA3AajUE8oVys2xW +WG_PROVIDER=opnsense + OPNSENSE_API_URL=https://opnsense.cazzzer.com OPNSENSE_API_KEY= OPNSENSE_API_SECRET= OPNSENSE_WG_IFNAME=wg2 +WG_QUICK_FILENAME=wg0.conf +WG_QUICK_PRIVATE_KEY=MHV1/cTPiuOlEwwQ011dSn0e2c+sNRcPnA2e/74+N2E= +WG_QUICK_ADDRESS=10.20.30.1,fd00::1 +WG_QUICK_LISTEN_PORT=51820 + IPV4_STARTING_ADDR=10.18.11.100 IPV6_STARTING_ADDR=fd00:10:18:11::100:0 IPV6_CLIENT_PREFIX_SIZE=112 diff --git a/.gitignore b/.gitignore index fd850af..6422098 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ vite.config.ts.timestamp-* # SQLite *.db + +# Generated wireguard configs +wg*.conf diff --git a/bun.lock b/bun.lock index e038959..4f82b1d 100644 --- a/bun.lock +++ b/bun.lock @@ -21,6 +21,7 @@ "@tailwindcss/forms": "^0.5.10", "@tailwindcss/typography": "^0.5.16", "@types/better-sqlite3": "^7.6.13", + "@types/bun": "^1.2.13", "@types/eslint": "^9.6.1", "@types/qrcode-svg": "^1.1.5", "arctic": "^2.3.4", @@ -281,6 +282,8 @@ "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], + "@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="], + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], @@ -363,6 +366,8 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], diff --git a/package.json b/package.json index 65ddec5..9f11599 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "license": "AGPL-3.0-or-later", "type": "module", "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", + "dev": "bun x --bun vite dev", + "build": "bun x --bun vite build", + "preview": "bun x --bun vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "format": "prettier --write .", @@ -31,6 +31,7 @@ "@tailwindcss/forms": "^0.5.10", "@tailwindcss/typography": "^0.5.16", "@types/better-sqlite3": "^7.6.13", + "@types/bun": "^1.2.13", "@types/eslint": "^9.6.1", "@types/qrcode-svg": "^1.1.5", "arctic": "^2.3.4", diff --git a/src/lib/server/wg-provider.ts b/src/lib/server/wg-provider.ts index 927a762..ad1937c 100644 --- a/src/lib/server/wg-provider.ts +++ b/src/lib/server/wg-provider.ts @@ -1,12 +1,32 @@ -import { WgProviderOpnsense } from '$lib/server/wg-providers/opnsense'; +import { assertGuard } from 'typia'; import { env } from '$env/dynamic/private'; import type { IWgProvider } from '$lib/server/types'; +import { WgProviderOpnsense } from '$lib/server/wg-providers/opnsense'; +import { WgProviderWgQuick } from '$lib/server/wg-providers/wg-quick'; -const wgProvider: IWgProvider = new WgProviderOpnsense({ +const opnsense: IWgProvider = new WgProviderOpnsense({ opnsenseUrl: env.OPNSENSE_API_URL, opnsenseApiKey: env.OPNSENSE_API_KEY, opnsenseApiSecret: env.OPNSENSE_API_SECRET, opnsenseWgIfname: env.OPNSENSE_WG_IFNAME, }); +const wgQuick: IWgProvider = new WgProviderWgQuick({ + filename: env.WG_QUICK_FILENAME, + address: env.WG_QUICK_ADDRESS, + privateKey: env.WG_QUICK_PRIVATE_KEY, + listenPort: parseInt(env.WG_QUICK_LISTEN_PORT), +}); + +const providers = { + opnsense, + 'wg-quick': wgQuick, +}; + +const chosenProvider = env.WG_PROVIDER?? 'wg-quick'; +assertGuard(chosenProvider, () => + Error(`WG_PROVIDER must be one of ${Object.keys(providers).join(', ')}`), +); + +const wgProvider = providers[chosenProvider]; export default wgProvider; diff --git a/src/lib/server/wg-providers/wg-quick/index.ts b/src/lib/server/wg-providers/wg-quick/index.ts new file mode 100644 index 0000000..cde053e --- /dev/null +++ b/src/lib/server/wg-providers/wg-quick/index.ts @@ -0,0 +1,76 @@ +import assert from 'node:assert'; +import { appendFile } from 'node:fs/promises'; +import type { ClientConnection, CreateClientParams, IWgProvider, WgKeys } from '$lib/server/types'; +import { err, ok, type Result } from '$lib/types'; +import { + type IWgQuickInterfaceConfig, + wgGenPrivKey, + wgGenPsk, + wgGenPubKey, + wgPeerConfig, + wgQuickInterfaceConfig, wgQuickUp +} from './snippets'; +import type { User } from '$lib/server/db/schema'; + +export class WgProviderWgQuick implements IWgProvider { + private filename: string; + private publicKey?: string; + private config: IWgQuickInterfaceConfig; + + constructor(params: WgQuickParams) { + this.filename = params.filename; + this.config = { + address: params.address, + privateKey: params.privateKey, + listenPort: params.listenPort, + }; + } + + async init(): Promise> { + const file = Bun.file(this.filename); + + this.publicKey = await wgGenPubKey(this.config.privateKey); + console.log(`wg-quick: running with public key: ${this.publicKey}`); + if (await file.exists()) { + // TODO: Check if the file is a valid WireGuard config file and our settings match + return ok(null); + } + await Bun.write(this.filename, wgQuickInterfaceConfig(this.config) + '\n'); + console.log('created wg-quick config file', this.filename); + return wgQuickUp(this.filename); + } + + getServerPublicKey(): string { + assert(this.publicKey, 'WgQuick public key not set, init() must be called first'); + return this.publicKey; + } + + async generateKeys(): Promise> { + const privateKey = await wgGenPrivKey(); + const publicKey = await wgGenPubKey(privateKey); + const preSharedKey = await wgGenPsk(); + return ok({ + publicKey, + privateKey, + preSharedKey, + }); + } + + async createClient(params: CreateClientParams): Promise> { + const peerConfig = wgPeerConfig(params); + await appendFile(this.filename, peerConfig + `\n`); + return ok(null); + } + + async findConnections(user: User): Promise> { + return err(Error('WgProviderWgQuick: listing connection information is not yet supported')); + } + + async deleteClient(publicKey: string): Promise> { + return err(Error('WgProviderWgQuick: deleting client is not yet supported')); + } +} + +interface WgQuickParams extends IWgQuickInterfaceConfig { + filename: string; +} diff --git a/src/lib/server/wg-providers/wg-quick/snippets.ts b/src/lib/server/wg-providers/wg-quick/snippets.ts new file mode 100644 index 0000000..1194ca5 --- /dev/null +++ b/src/lib/server/wg-providers/wg-quick/snippets.ts @@ -0,0 +1,59 @@ +import { $ } from 'bun'; +import type { CreateClientParams } from '$lib/server/types'; +import { err, ok, type Result } from '$lib/types'; + +export type IWgQuickInterfaceConfig = { + address: string; + privateKey: string; + listenPort: number; +} + +export function wgQuickInterfaceConfig(params: IWgQuickInterfaceConfig): string { + return`\ +[Interface] +Address = ${params.address} +PrivateKey = ${params.privateKey} +ListenPort = ${params.listenPort} +`; +} + +export function wgPeerConfig(params: CreateClientParams): string { + return`\ +[Peer] +PublicKey = ${params.publicKey} +PresharedKey = ${params.preSharedKey} +AllowedIPs = ${params.allowedIps} +# vpgen-user = ${params.user.username} +`; +} + +export async function runCommand(command: string): Promise> { + const result = await $`${command}`; + if (result.exitCode !== 0) return err(Error(`'${command}' failed with exit code ${result.exitCode}\n${result.stderr.toString()}`)); + return ok(null); +} + +export async function wgQuickUp(ifname: string): Promise> { + return runCommand(`wg-quick up ${ifname}`); +} + +export async function wgQuickDown(ifname: string): Promise> { + return runCommand(`wg-quick down ${ifname}`); +} + +export async function wgReload(ifname: string): Promise> { + return runCommand(`wg-quick strip ${ifname} | wg syncconf ${ifname} /dev/stdin`); + // return runCommand(`wg syncconf ${ifname} <(wg-quick strip ${ifname})`); +} + +export async function wgGenPubKey(privateKey: string) { + return (await $`echo ${privateKey} | wg pubkey`.text()).trim(); +} + +export async function wgGenPrivKey() { + return (await $`wg genkey`.text()).trim(); +} + +export async function wgGenPsk() { + return (await $`wg genpsk`.text()).trim(); +}