mirror of
https://github.com/wireadmin/wireadmin
synced 2025-02-26 05:48:44 +00:00
fix create wg
server function, and update ui
This commit is contained in:
parent
0d12571a1e
commit
9ddcd33450
@ -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>
|
15
web/src/lib/components/ui/collapsible/index.ts
Normal file
15
web/src/lib/components/ui/collapsible/index.ts
Normal 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
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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: '' };
|
||||
|
@ -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' });
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user