fix create wg server function, and update ui

This commit is contained in:
Shahrad Elahi 2023-11-07 20:11:43 +03:30
parent 0d12571a1e
commit 9ddcd33450
10 changed files with 177 additions and 63 deletions

View File

@ -0,0 +1,15 @@
<script lang="ts">
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
import { slide } from "svelte/transition";
type $$Props = CollapsiblePrimitive.ContentProps;
export let transition: $$Props["transition"] = slide;
export let transitionConfig: $$Props["transitionConfig"] = {
duration: 150
};
</script>
<CollapsiblePrimitive.Content {transition} {transitionConfig} {...$$restProps}>
<slot />
</CollapsiblePrimitive.Content>

View File

@ -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
};

View File

@ -32,4 +32,15 @@ export default class Network {
public static async checkInterfaceExists(inet: string): Promise<boolean> {
return await Shell.exec(`ip link show | grep ${inet}`, true).then((o) => o.trim() !== '');
}
public static async getInUsePorts(): Promise<number[]> {
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;
}
}

View File

@ -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(),

View File

@ -298,7 +298,7 @@ export async function readWgConf(configId: number): Promise<WgServer> {
id: crypto.randomUUID(),
confId: configId,
confHash: null,
type: 'direct',
tor: false,
name: '',
address: '',
listen: 0,
@ -424,15 +424,17 @@ export async function generateWgKey(): Promise<WgKey> {
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<string> {
}
export async function generateWgServer(config: GenerateWgServerParams): Promise<string> {
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<boolean> {
const addresses = (await getServers()).map((s) => s.address);
return addresses.includes(ip);
}
export async function isPortReserved(port: number): Promise<boolean> {
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<string | undefined> {
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: '' };

View File

@ -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' });

View File

@ -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);
}
},
};

View File

@ -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<CreateServerSchemaType>;
export let isOpen = false;
</script>
<Dialog open={isOpen}>
@ -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 @@
</FormItem>
</FormField>
<FormField {config} name={'dns'}>
<FormItem>
<FormLabel>DNS</FormLabel>
<FormInput placeholder={'e.g. 1.1.1.1'} type={'text'} />
<FormDescription>Optional. This is the DNS server that will be pushed to clients.</FormDescription>
<FormValidation />
</FormItem>
</FormField>
<Collapsible>
<CollapsibleTrigger asChild let:builder>
<Button builders={[builder]} variant="ghost" size="sm" class="mb-4 -mr-2">
<i class="far fa-cog mr-2"></i>
<span>Advanced Options</span>
</Button>
</CollapsibleTrigger>
<FormField {config} name={'mtu'}>
<FormItem>
<FormLabel>MTU</FormLabel>
<FormInput placeholder={'1350'} type={'text'} />
<FormDescription>Optional. Recommended to leave this blank.</FormDescription>
<FormValidation />
</FormItem>
</FormField>
<CollapsibleContent class="space-y-6">
<FormField {config} name={'tor'}>
<FormItem class="flex items-center justify-between">
<div class="space-y-0.5">
<FormLabel>Use Tor</FormLabel>
<FormDescription>This will route all outgoing traffic through Tor.</FormDescription>
</div>
<FormSwitch />
</FormItem>
</FormField>
<FormField {config} name={'dns'}>
<FormItem>
<FormLabel>DNS</FormLabel>
<FormInput placeholder={'e.g. 1.1.1.1'} type={'text'} />
<FormDescription>Optional. This is the DNS server that will be pushed to clients.</FormDescription>
<FormValidation />
</FormItem>
</FormField>
<FormField {config} name={'mtu'}>
<FormItem>
<FormLabel>MTU</FormLabel>
<FormInput placeholder={'1350'} type={'text'} />
<FormDescription>Optional. Recommended to leave this blank.</FormDescription>
<FormValidation />
</FormItem>
</FormField>
</CollapsibleContent>
</Collapsible>
<DialogFooter>
<FormButton type="submit">Create</FormButton>
<FormButton>
<i class={cn(loading ? 'far fa-arrow-rotate-right animate-spin' : 'far fa-plus', 'mr-2')}></i>
Create
</FormButton>
</DialogFooter>
</Form>
</DialogContent>

View File

@ -56,6 +56,6 @@
</div>
<a href={`/${server.id}`} title="Manage the Server" class="hidden md:block">
<Button variant="ghost" class="px-3 text-sm">Manage</Button>
<Button variant="ghost" size="sm">Manage</Button>
</a>
</div>

View File

@ -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;
export type CreateServerSchemaType = typeof CreateServerSchema;