Browse Source

Add PreUp, PostUp, PreDown, PostDown for client (#1714)

* Fix create client popup background is not white

* Fix no Add button when client Allowed Ips or Server Allowed Ips is empty

* Add preUp preDown postUp postDown for client

* Add description of hooks for client config

* Move hooks's label text into 'hooks' in en.json

---------

Co-authored-by: yanghuanglin <[email protected]>
Co-authored-by: Bernd Storath <[email protected]>
pull/1719/head
杨黄林 1 month ago
committed by GitHub
parent
commit
fcb5049dab
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      src/app/components/Base/Dialog.vue
  2. 15
      src/app/components/Form/ArrayField.vue
  3. 24
      src/app/pages/admin/hooks.vue
  4. 29
      src/app/pages/clients/[id].vue
  5. 11
      src/i18n/locales/en.json
  6. 4
      src/server/database/migrations/0000_short_skin.sql
  7. 34
      src/server/database/migrations/meta/0000_snapshot.json
  8. 36
      src/server/database/migrations/meta/0001_snapshot.json
  9. 4
      src/server/database/migrations/meta/_journal.json
  10. 4
      src/server/database/repositories/client/schema.ts
  11. 4
      src/server/database/repositories/client/types.ts
  12. 10
      src/server/database/repositories/hooks/types.ts
  13. 4
      src/server/utils/types.ts
  14. 9
      src/server/utils/wgHelper.ts

2
src/app/components/Base/Dialog.vue

@ -6,7 +6,7 @@
class="fixed inset-0 z-30 bg-gray-500 opacity-75 dark:bg-black dark:opacity-50" class="fixed inset-0 z-30 bg-gray-500 opacity-75 dark:bg-black dark:opacity-50"
/> />
<DialogContent <DialogContent
class="fixed left-1/2 top-1/2 z-[100] max-h-[85vh] w-[90vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-md p-6 shadow-2xl focus:outline-none dark:bg-neutral-700" class="fixed left-1/2 top-1/2 z-[100] max-h-[85vh] w-[90vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-md bg-white p-6 shadow-2xl focus:outline-none dark:bg-neutral-700"
> >
<DialogTitle <DialogTitle
class="m-0 text-lg font-semibold text-gray-900 dark:text-neutral-200" class="m-0 text-lg font-semibold text-gray-900 dark:text-neutral-200"

15
src/app/components/Form/ArrayField.vue

@ -1,10 +1,10 @@
<template> <template>
<div class="flex flex-col gap-2">
<div v-if="data?.length === 0"> <div v-if="data?.length === 0">
{{ emptyText || $t('form.noItems') }} {{ emptyText || $t('form.noItems') }}
</div> </div>
<div v-else class="flex flex-col gap-2"> <div v-for="(item, i) in data" v-else :key="i">
<div v-for="(item, i) in data" :key="i"> <div class="mt-1 flex flex-row gap-1">
<div class="flex flex-row gap-1">
<input <input
:value="item" :value="item"
:name="name" :name="name"
@ -12,13 +12,20 @@
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400"
@input="update($event, i)" @input="update($event, i)"
/> />
<BaseButton as="input" type="button" value="-" @click="del(i)" /> <BaseButton
as="input"
type="button"
class="rounded-lg"
value="-"
@click="del(i)"
/>
</div> </div>
</div> </div>
<div class="mt-2"> <div class="mt-2">
<BaseButton <BaseButton
as="input" as="input"
type="button" type="button"
class="rounded-lg"
:value="$t('form.add')" :value="$t('form.add')"
@click="add" @click="add"
/> />

24
src/app/pages/admin/hooks.vue

@ -2,10 +2,26 @@
<main v-if="data"> <main v-if="data">
<FormElement @submit.prevent="submit"> <FormElement @submit.prevent="submit">
<FormGroup> <FormGroup>
<FormTextField id="PreUp" v-model="data.preUp" label="PreUp" /> <FormTextField
<FormTextField id="PostUp" v-model="data.postUp" label="PostUp" /> id="PreUp"
<FormTextField id="PreDown" v-model="data.preDown" label="PreDown" /> v-model="data.preUp"
<FormTextField id="PostDown" v-model="data.postDown" label="PostDown" /> :label="$t('hooks.preUp')"
/>
<FormTextField
id="PostUp"
v-model="data.postUp"
:label="$t('hooks.postUp')"
/>
<FormTextField
id="PreDown"
v-model="data.preDown"
:label="$t('hooks.preDown')"
/>
<FormTextField
id="PostDown"
v-model="data.postDown"
:label="$t('hooks.postDown')"
/>
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading>{{ $t('form.actions') }}</FormHeading> <FormHeading>{{ $t('form.actions') }}</FormHeading>

29
src/app/pages/clients/[id].vue

@ -71,6 +71,35 @@
:label="$t('general.persistentKeepalive')" :label="$t('general.persistentKeepalive')"
/> />
</FormGroup> </FormGroup>
<FormGroup>
<FormHeading :description="$t('client.hooksDescription')">
{{ $t('client.hooks') }}
</FormHeading>
<FormTextField
id="PreUp"
v-model="data.preUp"
:description="$t('client.hooksLeaveEmpty')"
:label="$t('hooks.preUp')"
/>
<FormTextField
id="PostUp"
v-model="data.postUp"
:description="$t('client.hooksLeaveEmpty')"
:label="$t('hooks.postUp')"
/>
<FormTextField
id="PreDown"
v-model="data.preDown"
:description="$t('client.hooksLeaveEmpty')"
:label="$t('hooks.preDown')"
/>
<FormTextField
id="PostDown"
v-model="data.postDown"
:description="$t('client.hooksLeaveEmpty')"
:label="$t('hooks.postDown')"
/>
</FormGroup>
<FormGroup> <FormGroup>
<FormHeading>{{ $t('form.actions') }}</FormHeading> <FormHeading>{{ $t('form.actions') }}</FormHeading>
<FormActionField type="submit" :label="$t('form.save')" /> <FormActionField type="submit" :label="$t('form.save')" />

11
src/i18n/locales/en.json

@ -98,7 +98,10 @@
"allowedIpsDesc": "Which IPs will be routed through the VPN", "allowedIpsDesc": "Which IPs will be routed through the VPN",
"serverAllowedIpsDesc": "Which IPs the server will route to the client", "serverAllowedIpsDesc": "Which IPs the server will route to the client",
"mtuDesc": "Sets the maximum transmission unit (packet size) for the VPN tunnel", "mtuDesc": "Sets the maximum transmission unit (packet size) for the VPN tunnel",
"persistentKeepaliveDesc": "Sets the interval (in seconds) for keep-alive packets. 0 disables it" "persistentKeepaliveDesc": "Sets the interval (in seconds) for keep-alive packets. 0 disables it",
"hooks": "Hooks",
"hooksDescription": "Hooks only work with wg-quick",
"hooksLeaveEmpty": "Only for wg-quick. Otherwise, leave it empty"
}, },
"dialog": { "dialog": {
"change": "Change", "change": "Change",
@ -208,5 +211,11 @@
"dns": "DNS", "dns": "DNS",
"allowedIps": "Allowed IPs", "allowedIps": "Allowed IPs",
"file": "File" "file": "File"
},
"hooks": {
"preUp": "PreUp",
"postUp": "PostUp",
"preDown": "PreDown",
"postDown": "PostDown"
} }
} }

4
src/server/database/migrations/0000_short_skin.sql

@ -4,6 +4,10 @@ CREATE TABLE `clients_table` (
`name` text NOT NULL, `name` text NOT NULL,
`ipv4_address` text NOT NULL, `ipv4_address` text NOT NULL,
`ipv6_address` text NOT NULL, `ipv6_address` text NOT NULL,
`pre_up` text DEFAULT '' NOT NULL,
`post_up` text DEFAULT '' NOT NULL,
`pre_down` text DEFAULT '' NOT NULL,
`post_down` text DEFAULT '' NOT NULL,
`private_key` text NOT NULL, `private_key` text NOT NULL,
`public_key` text NOT NULL, `public_key` text NOT NULL,
`pre_shared_key` text NOT NULL, `pre_shared_key` text NOT NULL,

34
src/server/database/migrations/meta/0000_snapshot.json

@ -1,7 +1,7 @@
{ {
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"id": "b1dde023-d141-4eab-9226-89a832b2ed2b", "id": "2cabecf8-93d5-4d32-81b7-2e4369c1cb29",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"clients_table": { "clients_table": {
@ -42,6 +42,38 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"pre_up": {
"name": "pre_up",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"post_up": {
"name": "post_up",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"pre_down": {
"name": "pre_down",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"post_down": {
"name": "post_down",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"private_key": { "private_key": {
"name": "private_key", "name": "private_key",
"type": "text", "type": "text",

36
src/server/database/migrations/meta/0001_snapshot.json

@ -1,6 +1,6 @@
{ {
"id": "720d420c-361f-4427-a45b-db0ca613934d", "id": "9476c20a-509b-4cd7-b58b-a042600bafb1",
"prevId": "b1dde023-d141-4eab-9226-89a832b2ed2b", "prevId": "2cabecf8-93d5-4d32-81b7-2e4369c1cb29",
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"tables": { "tables": {
@ -42,6 +42,38 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"pre_up": {
"name": "pre_up",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"post_up": {
"name": "post_up",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"pre_down": {
"name": "pre_down",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"post_down": {
"name": "post_down",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"private_key": { "private_key": {
"name": "private_key", "name": "private_key",
"type": "text", "type": "text",

4
src/server/database/migrations/meta/_journal.json

@ -5,14 +5,14 @@
{ {
"idx": 0, "idx": 0,
"version": "6", "version": "6",
"when": 1739266828300, "when": 1741331552405,
"tag": "0000_short_skin", "tag": "0000_short_skin",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 1, "idx": 1,
"version": "6", "version": "6",
"when": 1739266837347, "when": 1741331579259,
"tag": "0001_classy_the_stranger", "tag": "0001_classy_the_stranger",
"breakpoints": true "breakpoints": true
} }

4
src/server/database/repositories/client/schema.ts

@ -14,6 +14,10 @@ export const client = sqliteTable('clients_table', {
name: text().notNull(), name: text().notNull(),
ipv4Address: text('ipv4_address').notNull().unique(), ipv4Address: text('ipv4_address').notNull().unique(),
ipv6Address: text('ipv6_address').notNull().unique(), ipv6Address: text('ipv6_address').notNull().unique(),
preUp: text('pre_up').default('').notNull(),
postUp: text('post_up').default('').notNull(),
preDown: text('pre_down').default('').notNull(),
postDown: text('post_down').default('').notNull(),
privateKey: text('private_key').notNull(), privateKey: text('private_key').notNull(),
publicKey: text('public_key').notNull(), publicKey: text('public_key').notNull(),
preSharedKey: text('pre_shared_key').notNull(), preSharedKey: text('pre_shared_key').notNull(),

4
src/server/database/repositories/client/types.ts

@ -57,6 +57,10 @@ export const ClientUpdateSchema = schemaForType<UpdateClientType>()(
expiresAt: expiresAt, expiresAt: expiresAt,
ipv4Address: address4, ipv4Address: address4,
ipv6Address: address6, ipv6Address: address6,
preUp: HookSchema,
postUp: HookSchema,
preDown: HookSchema,
postDown: HookSchema,
allowedIps: AllowedIpsSchema, allowedIps: AllowedIpsSchema,
serverAllowedIps: serverAllowedIps, serverAllowedIps: serverAllowedIps,
mtu: MtuSchema, mtu: MtuSchema,

10
src/server/database/repositories/hooks/types.ts

@ -6,13 +6,11 @@ export type HooksType = InferSelectModel<typeof hooks>;
export type HooksUpdateType = Omit<HooksType, 'id' | 'createdAt' | 'updatedAt'>; export type HooksUpdateType = Omit<HooksType, 'id' | 'createdAt' | 'updatedAt'>;
const hook = z.string({ message: t('zod.hook') }).pipe(safeStringRefine);
export const HooksUpdateSchema = schemaForType<HooksUpdateType>()( export const HooksUpdateSchema = schemaForType<HooksUpdateType>()(
z.object({ z.object({
preUp: hook, preUp: HookSchema,
postUp: hook, postUp: HookSchema,
preDown: hook, preDown: HookSchema,
postDown: hook, postDown: HookSchema,
}) })
); );

4
src/server/utils/types.ts

@ -52,6 +52,10 @@ export const FileSchema = z.object({
file: z.string({ message: t('zod.file') }), file: z.string({ message: t('zod.file') }),
}); });
export const HookSchema = z
.string({ message: t('zod.hook') })
.pipe(safeStringRefine);
export const schemaForType = export const schemaForType =
<T>() => <T>() =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

9
src/server/utils/wgHelper.ts

@ -49,12 +49,19 @@ PostDown = ${iptablesTemplate(hooks.postDown, wgInterface)}`;
const cidr4Block = parseCidr(wgInterface.ipv4Cidr).prefix; const cidr4Block = parseCidr(wgInterface.ipv4Cidr).prefix;
const cidr6Block = parseCidr(wgInterface.ipv6Cidr).prefix; const cidr6Block = parseCidr(wgInterface.ipv6Cidr).prefix;
const hookLines = [
client.preUp ? `PreUp = ${client.preUp}` : null,
client.postUp ? `PostUp = ${client.postUp}` : null,
client.preDown ? `PreDown = ${client.preDown}` : null,
client.postDown ? `PostDown = ${client.postDown}` : null,
].filter((v) => v !== null);
return `[Interface] return `[Interface]
PrivateKey = ${client.privateKey} PrivateKey = ${client.privateKey}
Address = ${client.ipv4Address}/${cidr4Block}, ${client.ipv6Address}/${cidr6Block} Address = ${client.ipv4Address}/${cidr4Block}, ${client.ipv6Address}/${cidr6Block}
DNS = ${client.dns.join(', ')} DNS = ${client.dns.join(', ')}
MTU = ${client.mtu} MTU = ${client.mtu}
${hookLines.length ? `${hookLines.join('\n')}\n` : ''}
[Peer] [Peer]
PublicKey = ${wgInterface.publicKey} PublicKey = ${wgInterface.publicKey}
PresharedKey = ${client.preSharedKey} PresharedKey = ${client.preSharedKey}

Loading…
Cancel
Save