mirror of
https://github.com/wireadmin/wireadmin
synced 2025-04-23 07:34:23 +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> {
|
public static async checkInterfaceExists(inet: string): Promise<boolean> {
|
||||||
return await Shell.exec(`ip link show | grep ${inet}`, true).then((o) => o.trim() !== '');
|
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 { z } from 'zod';
|
||||||
import { IPV4_REGEX } from '$lib/constants';
|
import { IPV4_REGEX } from '$lib/constants';
|
||||||
import { NameSchema } from '$lib/wireguard/schema';
|
import { NameSchema, TorSchema } from '$lib/wireguard/schema';
|
||||||
|
|
||||||
export const WgKeySchema = z.object({
|
export const WgKeySchema = z.object({
|
||||||
privateKey: z.string(),
|
privateKey: z.string(),
|
||||||
@ -46,7 +46,7 @@ export const WgServerSchema = z
|
|||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
confId: z.number(),
|
confId: z.number(),
|
||||||
confHash: z.string().nullable(),
|
confHash: z.string().nullable(),
|
||||||
type: z.enum(['direct', 'bridge', 'tor']),
|
tor: TorSchema,
|
||||||
name: NameSchema,
|
name: NameSchema,
|
||||||
address: z.string().regex(IPV4_REGEX),
|
address: z.string().regex(IPV4_REGEX),
|
||||||
listen: z.number(),
|
listen: z.number(),
|
||||||
|
@ -298,7 +298,7 @@ export async function readWgConf(configId: number): Promise<WgServer> {
|
|||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
confId: configId,
|
confId: configId,
|
||||||
confHash: null,
|
confHash: null,
|
||||||
type: 'direct',
|
tor: false,
|
||||||
name: '',
|
name: '',
|
||||||
address: '',
|
address: '',
|
||||||
listen: 0,
|
listen: 0,
|
||||||
@ -424,15 +424,17 @@ export async function generateWgKey(): Promise<WgKey> {
|
|||||||
return { privateKey, publicKey, preSharedKey };
|
return { privateKey, publicKey, preSharedKey };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateWgServer(config: {
|
interface GenerateWgServerParams {
|
||||||
name: string;
|
name: string;
|
||||||
address: string;
|
address: string;
|
||||||
type: WgServer['type'];
|
tor: boolean;
|
||||||
port: number;
|
port: number;
|
||||||
dns?: string;
|
dns?: string;
|
||||||
mtu?: number;
|
mtu?: number;
|
||||||
insertDb?: boolean;
|
insertDb?: boolean;
|
||||||
}): Promise<string> {
|
}
|
||||||
|
|
||||||
|
export async function generateWgServer(config: GenerateWgServerParams): Promise<string> {
|
||||||
const { privateKey, publicKey } = await generateWgKey();
|
const { privateKey, publicKey } = await generateWgKey();
|
||||||
|
|
||||||
// inside redis create a config list
|
// inside redis create a config list
|
||||||
@ -443,7 +445,7 @@ export async function generateWgServer(config: {
|
|||||||
id: uuid,
|
id: uuid,
|
||||||
confId,
|
confId,
|
||||||
confHash: null,
|
confHash: null,
|
||||||
type: config.type,
|
tor: config.tor,
|
||||||
name: config.name,
|
name: config.name,
|
||||||
address: config.address,
|
address: config.address,
|
||||||
listen: config.port,
|
listen: config.port,
|
||||||
@ -461,14 +463,11 @@ export async function generateWgServer(config: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// check if address or port are already reserved
|
// check if address or port are already reserved
|
||||||
const [addresses, ports] = (await getServers()).map((s) => [s.address, s.listen]);
|
if (await isIPReserved(config.address)) {
|
||||||
|
|
||||||
// check for the conflict
|
|
||||||
if (Array.isArray(addresses) && addresses.includes(config.address)) {
|
|
||||||
throw new Error(`Address ${config.address} is already reserved!`);
|
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!`);
|
throw new Error(`Port ${config.port} is already reserved!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -500,6 +499,18 @@ export async function generateWgServer(config: {
|
|||||||
return uuid;
|
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> {
|
export async function getConfigHash(confId: number): Promise<string | undefined> {
|
||||||
if (!(await wgConfExists(confId))) {
|
if (!(await wgConfExists(confId))) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -564,18 +575,7 @@ export async function makeWgIptables(s: WgServer): Promise<{ up: string; down: s
|
|||||||
const source = `${s.address}/24`;
|
const source = `${s.address}/24`;
|
||||||
const wg_inet = `wg${s.confId}`;
|
const wg_inet = `wg${s.confId}`;
|
||||||
|
|
||||||
if (s.type === 'direct') {
|
if (s.tor) {
|
||||||
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') {
|
|
||||||
const up = dynaJoin([
|
const up = dynaJoin([
|
||||||
`iptables -A INPUT -m state --state ESTABLISHED -j ACCEPT`,
|
`iptables -A INPUT -m state --state ESTABLISHED -j ACCEPT`,
|
||||||
`iptables -A INPUT -i ${wg_inet} -s ${source} -m state --state NEW -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`,
|
`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('; ');
|
]).join('; ');
|
||||||
return { up, down: up.replace(/-A/g, '-D') };
|
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: '' };
|
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
|
export const DnsSchema = z
|
||||||
.string()
|
.string()
|
||||||
@ -43,9 +45,13 @@ export const DnsSchema = z
|
|||||||
|
|
||||||
export const MtuSchema = z
|
export const MtuSchema = z
|
||||||
.string()
|
.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',
|
message: 'MTU must be between 1 and 1500',
|
||||||
})
|
})
|
||||||
|
.default('1350')
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const ServerId = z.string().uuid({ message: 'Server ID must be a valid UUID' });
|
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 Actions, error } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from './$types';
|
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 { setError, superValidate } from 'sveltekit-superforms/server';
|
||||||
import { CreateServerSchema } from './schema';
|
import { CreateServerSchema } from './schema';
|
||||||
import { NameSchema } from '$lib/wireguard/schema';
|
import { NameSchema } from '$lib/wireguard/schema';
|
||||||
@ -39,20 +46,32 @@ export const actions: Actions = {
|
|||||||
return setError(form, 'Bad Request');
|
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({
|
try {
|
||||||
name,
|
if (await isIPReserved(address)) {
|
||||||
address,
|
return setError(form, 'address', `IP ${address} is already reserved!`);
|
||||||
port: Number(port),
|
}
|
||||||
type: 'direct',
|
|
||||||
mtu: Number(mtu),
|
|
||||||
dns,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
if (await isPortReserved(Number(port))) {
|
||||||
ok: true,
|
return setError(form, 'port', `Port ${port} is already reserved!`);
|
||||||
serverId,
|
}
|
||||||
};
|
|
||||||
|
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,
|
FormField,
|
||||||
FormInput,
|
FormInput,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
|
FormSwitch,
|
||||||
FormValidation,
|
FormValidation,
|
||||||
} from '$lib/components/ui/form';
|
} from '$lib/components/ui/form';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { FormItem } from '$lib/components/ui/form/index.js';
|
import { FormItem } from '$lib/components/ui/form/index.js';
|
||||||
import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte';
|
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>;
|
let form: SuperValidated<CreateServerSchemaType>;
|
||||||
export let isOpen = false;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dialog open={isOpen}>
|
<Dialog open={isOpen}>
|
||||||
@ -33,9 +39,18 @@
|
|||||||
method={'POST'}
|
method={'POST'}
|
||||||
let:config
|
let:config
|
||||||
options={{
|
options={{
|
||||||
|
onSubmit: (s) => {
|
||||||
|
loading = true;
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
console.error('Client-side: FormError:', e);
|
||||||
|
},
|
||||||
onResult: ({ result }) => {
|
onResult: ({ result }) => {
|
||||||
|
loading = false;
|
||||||
if (result.type === 'success') {
|
if (result.type === 'success') {
|
||||||
goto('/');
|
goto(`/${result.data.serverId}`);
|
||||||
|
} else {
|
||||||
|
console.error('Server-failure: Result:', result);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@ -66,26 +81,50 @@
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField {config} name={'dns'}>
|
<Collapsible>
|
||||||
<FormItem>
|
<CollapsibleTrigger asChild let:builder>
|
||||||
<FormLabel>DNS</FormLabel>
|
<Button builders={[builder]} variant="ghost" size="sm" class="mb-4 -mr-2">
|
||||||
<FormInput placeholder={'e.g. 1.1.1.1'} type={'text'} />
|
<i class="far fa-cog mr-2"></i>
|
||||||
<FormDescription>Optional. This is the DNS server that will be pushed to clients.</FormDescription>
|
<span>Advanced Options</span>
|
||||||
<FormValidation />
|
</Button>
|
||||||
</FormItem>
|
</CollapsibleTrigger>
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField {config} name={'mtu'}>
|
<CollapsibleContent class="space-y-6">
|
||||||
<FormItem>
|
<FormField {config} name={'tor'}>
|
||||||
<FormLabel>MTU</FormLabel>
|
<FormItem class="flex items-center justify-between">
|
||||||
<FormInput placeholder={'1350'} type={'text'} />
|
<div class="space-y-0.5">
|
||||||
<FormDescription>Optional. Recommended to leave this blank.</FormDescription>
|
<FormLabel>Use Tor</FormLabel>
|
||||||
<FormValidation />
|
<FormDescription>This will route all outgoing traffic through Tor.</FormDescription>
|
||||||
</FormItem>
|
</div>
|
||||||
</FormField>
|
<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>
|
<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>
|
</DialogFooter>
|
||||||
</Form>
|
</Form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
@ -56,6 +56,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href={`/${server.id}`} title="Manage the Server" class="hidden md:block">
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { z } from 'zod';
|
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({
|
export const CreateServerSchema = z.object({
|
||||||
name: NameSchema,
|
name: NameSchema,
|
||||||
address: AddressSchema,
|
address: AddressSchema,
|
||||||
port: PortSchema,
|
port: PortSchema,
|
||||||
type: TypeSchema,
|
tor: TorSchema,
|
||||||
dns: DnsSchema,
|
dns: DnsSchema,
|
||||||
mtu: MtuSchema,
|
mtu: MtuSchema,
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user