diff --git a/web/src/lib/components/ui/collapsible/collapsible-content.svelte b/web/src/lib/components/ui/collapsible/collapsible-content.svelte new file mode 100644 index 0000000..5b6b3e5 --- /dev/null +++ b/web/src/lib/components/ui/collapsible/collapsible-content.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/web/src/lib/components/ui/collapsible/index.ts b/web/src/lib/components/ui/collapsible/index.ts new file mode 100644 index 0000000..04c3c2a --- /dev/null +++ b/web/src/lib/components/ui/collapsible/index.ts @@ -0,0 +1,15 @@ +import { Collapsible as CollapsiblePrimitive } from "bits-ui"; +import Content from "./collapsible-content.svelte"; + +const Root = CollapsiblePrimitive.Root; +const Trigger = CollapsiblePrimitive.Trigger; + +export { + Root, + Content, + Trigger, + // + Root as Collapsible, + Content as CollapsibleContent, + Trigger as CollapsibleTrigger +}; diff --git a/web/src/lib/network.ts b/web/src/lib/network.ts index 0627e19..7a2b194 100644 --- a/web/src/lib/network.ts +++ b/web/src/lib/network.ts @@ -32,4 +32,15 @@ export default class Network { public static async checkInterfaceExists(inet: string): Promise { return await Shell.exec(`ip link show | grep ${inet}`, true).then((o) => o.trim() !== ''); } + + public static async getInUsePorts(): Promise { + const ports = []; + const output = await Shell.exec(`netstat -tulpn | grep LISTEN | awk '{print $4}' | awk -F ':' '{print $NF}'`, true); + for (const line of output.split('\n')) { + const clean = Number(line.trim()); + if (!isNaN(clean)) ports.push(clean); + } + + return ports; + } } diff --git a/web/src/lib/typings.ts b/web/src/lib/typings.ts index b78d3f0..89235cc 100644 --- a/web/src/lib/typings.ts +++ b/web/src/lib/typings.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { IPV4_REGEX } from '$lib/constants'; -import { NameSchema } from '$lib/wireguard/schema'; +import { NameSchema, TorSchema } from '$lib/wireguard/schema'; export const WgKeySchema = z.object({ privateKey: z.string(), @@ -46,7 +46,7 @@ export const WgServerSchema = z id: z.string().uuid(), confId: z.number(), confHash: z.string().nullable(), - type: z.enum(['direct', 'bridge', 'tor']), + tor: TorSchema, name: NameSchema, address: z.string().regex(IPV4_REGEX), listen: z.number(), diff --git a/web/src/lib/wireguard/index.ts b/web/src/lib/wireguard/index.ts index a646c4a..b5cac93 100644 --- a/web/src/lib/wireguard/index.ts +++ b/web/src/lib/wireguard/index.ts @@ -298,7 +298,7 @@ export async function readWgConf(configId: number): Promise { id: crypto.randomUUID(), confId: configId, confHash: null, - type: 'direct', + tor: false, name: '', address: '', listen: 0, @@ -424,15 +424,17 @@ export async function generateWgKey(): Promise { return { privateKey, publicKey, preSharedKey }; } -export async function generateWgServer(config: { +interface GenerateWgServerParams { name: string; address: string; - type: WgServer['type']; + tor: boolean; port: number; dns?: string; mtu?: number; insertDb?: boolean; -}): Promise { +} + +export async function generateWgServer(config: GenerateWgServerParams): Promise { const { privateKey, publicKey } = await generateWgKey(); // inside redis create a config list @@ -443,7 +445,7 @@ export async function generateWgServer(config: { id: uuid, confId, confHash: null, - type: config.type, + tor: config.tor, name: config.name, address: config.address, listen: config.port, @@ -461,14 +463,11 @@ export async function generateWgServer(config: { }; // check if address or port are already reserved - const [addresses, ports] = (await getServers()).map((s) => [s.address, s.listen]); - - // check for the conflict - if (Array.isArray(addresses) && addresses.includes(config.address)) { + if (await isIPReserved(config.address)) { throw new Error(`Address ${config.address} is already reserved!`); } - if (Array.isArray(ports) && ports.includes(config.port)) { + if (await isPortReserved(config.port)) { throw new Error(`Port ${config.port} is already reserved!`); } @@ -500,6 +499,18 @@ export async function generateWgServer(config: { return uuid; } +export async function isIPReserved(ip: string): Promise { + const addresses = (await getServers()).map((s) => s.address); + return addresses.includes(ip); +} + +export async function isPortReserved(port: number): Promise { + const inUsePorts = [await Network.getInUsePorts(), (await getServers()).map((s) => Number(s.listen))].flat(); + + console.log(inUsePorts, port, inUsePorts.includes(port)); + return inUsePorts.includes(port); +} + export async function getConfigHash(confId: number): Promise { if (!(await wgConfExists(confId))) { return undefined; @@ -564,18 +575,7 @@ export async function makeWgIptables(s: WgServer): Promise<{ up: string; down: s const source = `${s.address}/24`; const wg_inet = `wg${s.confId}`; - if (s.type === 'direct') { - const up = dynaJoin([ - `iptables -t nat -A POSTROUTING -s ${source} -o ${inet} -j MASQUERADE`, - `iptables -A INPUT -p udp -m udp --dport ${s.listen} -j ACCEPT`, - `iptables -A INPUT -p tcp -m tcp --dport ${s.listen} -j ACCEPT`, - `iptables -A FORWARD -i ${wg_inet} -j ACCEPT`, - `iptables -A FORWARD -o ${wg_inet} -j ACCEPT`, - ]).join('; '); - return { up, down: up.replace(/ -A /g, ' -D ') }; - } - - if (s.type === 'tor') { + if (s.tor) { const up = dynaJoin([ `iptables -A INPUT -m state --state ESTABLISHED -j ACCEPT`, `iptables -A INPUT -i ${wg_inet} -s ${source} -m state --state NEW -j ACCEPT`, @@ -588,6 +588,15 @@ export async function makeWgIptables(s: WgServer): Promise<{ up: string; down: s `iptables -A OUTPUT ! -o lo ! -d 127.0.0.1 ! -s 127.0.0.1 -p tcp -m tcp --tcp-flags ACK,FIN ACK,FIN -j DROP`, ]).join('; '); return { up, down: up.replace(/-A/g, '-D') }; + } else { + const up = dynaJoin([ + `iptables -t nat -A POSTROUTING -s ${source} -o ${inet} -j MASQUERADE`, + `iptables -A INPUT -p udp -m udp --dport ${s.listen} -j ACCEPT`, + `iptables -A INPUT -p tcp -m tcp --dport ${s.listen} -j ACCEPT`, + `iptables -A FORWARD -i ${wg_inet} -j ACCEPT`, + `iptables -A FORWARD -o ${wg_inet} -j ACCEPT`, + ]).join('; '); + return { up, down: up.replace(/ -A /g, ' -D ') }; } return { up: '', down: '' }; diff --git a/web/src/lib/wireguard/schema.ts b/web/src/lib/wireguard/schema.ts index 03270df..97004e2 100644 --- a/web/src/lib/wireguard/schema.ts +++ b/web/src/lib/wireguard/schema.ts @@ -32,7 +32,9 @@ export const PortSchema = z }, ); -export const TypeSchema = z.enum(['direct', 'tor']); +export const TorSchema = z + .boolean() + .default(false); export const DnsSchema = z .string() @@ -43,9 +45,13 @@ export const DnsSchema = z export const MtuSchema = z .string() - .refine((d) => isBetween(d, 1, 1500), { + .refine((d) => !isNaN(Number(d)), { + message: 'MTU must be a number', + }) + .refine((d) => !isBetween(Number(d), 1, 1500), { message: 'MTU must be between 1 and 1500', }) + .default('1350') .optional(); export const ServerId = z.string().uuid({ message: 'Server ID must be a valid UUID' }); diff --git a/web/src/routes/+page.server.ts b/web/src/routes/+page.server.ts index 14c8c7d..1cb8d3d 100644 --- a/web/src/routes/+page.server.ts +++ b/web/src/routes/+page.server.ts @@ -1,6 +1,13 @@ import { type Actions, error } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; -import { findServer, generateWgServer, getServers, WGServer } from '$lib/wireguard'; +import { + findServer, + generateWgServer, + getServers, + isIPReserved, + isPortReserved, + WGServer, +} from '$lib/wireguard'; import { setError, superValidate } from 'sveltekit-superforms/server'; import { CreateServerSchema } from './schema'; import { NameSchema } from '$lib/wireguard/schema'; @@ -39,20 +46,32 @@ export const actions: Actions = { return setError(form, 'Bad Request'); } - const { name, address, port, dns, mtu = '1350' } = form.data; + const { name, address, tor = false, port, dns, mtu = '1350' } = form.data; - const serverId = await generateWgServer({ - name, - address, - port: Number(port), - type: 'direct', - mtu: Number(mtu), - dns, - }); + try { + if (await isIPReserved(address)) { + return setError(form, 'address', `IP ${address} is already reserved!`); + } - return { - ok: true, - serverId, - }; + if (await isPortReserved(Number(port))) { + return setError(form, 'port', `Port ${port} is already reserved!`); + } + + const serverId = await generateWgServer({ + name, + address, + port: Number(port), + tor, + mtu: Number(mtu), + dns, + }); + + return { + ok: true, + serverId, + }; + } catch (e: any) { + return setError(form, e.message); + } }, }; diff --git a/web/src/routes/CreateServerDialog.svelte b/web/src/routes/CreateServerDialog.svelte index 4aa3c66..effe495 100644 --- a/web/src/routes/CreateServerDialog.svelte +++ b/web/src/routes/CreateServerDialog.svelte @@ -9,14 +9,20 @@ FormField, FormInput, FormLabel, + FormSwitch, FormValidation, } from '$lib/components/ui/form'; import { goto } from '$app/navigation'; import { FormItem } from '$lib/components/ui/form/index.js'; import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte'; + import { cn } from '$lib/utils'; + import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '$lib/components/ui/collapsible'; + import { Button } from '$lib/components/ui/button'; + + export let isOpen = false; + let loading: boolean = false; let form: SuperValidated; - export let isOpen = false; @@ -33,9 +39,18 @@ method={'POST'} let:config options={{ + onSubmit: (s) => { + loading = true; + }, + onError: (e) => { + console.error('Client-side: FormError:', e); + }, onResult: ({ result }) => { + loading = false; if (result.type === 'success') { - goto('/'); + goto(`/${result.data.serverId}`); + } else { + console.error('Server-failure: Result:', result); } }, }} @@ -66,26 +81,50 @@ - - - DNS - - Optional. This is the DNS server that will be pushed to clients. - - - + + + + + Advanced Options + + - - - MTU - - Optional. Recommended to leave this blank. - - - + + + + + Use Tor + This will route all outgoing traffic through Tor. + + + + + + + + DNS + + Optional. This is the DNS server that will be pushed to clients. + + + + + + + MTU + + Optional. Recommended to leave this blank. + + + + + - Create + + + Create + diff --git a/web/src/routes/Server.svelte b/web/src/routes/Server.svelte index 4714d61..82368bd 100644 --- a/web/src/routes/Server.svelte +++ b/web/src/routes/Server.svelte @@ -56,6 +56,6 @@ - Manage + Manage diff --git a/web/src/routes/schema.ts b/web/src/routes/schema.ts index d8e9e24..b5a7db9 100644 --- a/web/src/routes/schema.ts +++ b/web/src/routes/schema.ts @@ -1,13 +1,13 @@ import { z } from 'zod'; -import { AddressSchema, DnsSchema, MtuSchema, NameSchema, PortSchema, TypeSchema } from '$lib/wireguard/schema'; +import { AddressSchema, DnsSchema, MtuSchema, NameSchema, PortSchema, TorSchema } from '$lib/wireguard/schema'; export const CreateServerSchema = z.object({ name: NameSchema, address: AddressSchema, port: PortSchema, - type: TypeSchema, + tor: TorSchema, dns: DnsSchema, mtu: MtuSchema, }); -export type CreateServerSchemaType = typeof CreateServerSchema; \ No newline at end of file +export type CreateServerSchemaType = typeof CreateServerSchema;