Browse Source

Merge da61cb680f into c70ad1d08b

pull/2283/merge
Copilot 1 day ago
committed by GitHub
parent
commit
d7b0e07738
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 71
      docs/content/advanced/config/optional-config.md
  2. 16
      docs/content/advanced/config/unattended-setup.md
  3. 4
      docs/content/advanced/migrate/from-14-to-15.md
  4. 13
      src/app/components/Form/ArrayField.vue
  5. 7
      src/app/components/Form/HostField.vue
  6. 7
      src/app/components/Form/NullTextField.vue
  7. 13
      src/app/components/Form/NumberField.vue
  8. 13
      src/app/components/Form/SwitchField.vue
  9. 7
      src/app/components/Form/TextField.vue
  10. 17
      src/app/pages/admin/config.vue
  11. 11
      src/app/pages/admin/general.vue
  12. 10
      src/app/pages/admin/hooks.vue
  13. 9
      src/app/pages/admin/interface.vue
  14. 10
      src/app/pages/setup/2.vue
  15. 1
      src/server/api/admin/interface/cidr.post.ts
  16. 34
      src/server/api/admin/overrides.get.ts
  17. 17
      src/server/api/setup/2.post.ts
  18. 21
      src/server/database/repositories/client/service.ts
  19. 26
      src/server/database/sqlite.ts
  20. 7
      src/server/middleware/setup.ts
  21. 47
      src/server/utils/WireGuard.ts
  22. 174
      src/server/utils/config.ts
  23. 4
      src/server/utils/handler.ts
  24. 10
      src/server/utils/session.ts

71
docs/content/advanced/config/optional-config.md

@ -21,3 +21,74 @@ You will however still see a IPv6 address in the Web UI, but it won't be used.
This option can be removed in the future, as more devices support IPv6. This option can be removed in the future, as more devices support IPv6.
/// ///
## Configuration Overrides
These environment variables allow you to override settings that would normally be configured through the Admin Panel. When set, these values take precedence over database settings at runtime.
### Interface Settings
| Env | Example | Description |
| -------------- | ------------- | ------------------------- |
| `WG_PORT` | `51820` | WireGuard interface port |
| `WG_DEVICE` | `eth0` | Network device/interface |
| `WG_MTU` | `1420` | Maximum Transmission Unit |
| `WG_IPV4_CIDR` | `10.8.0.0/24` | IPv4 CIDR range |
| `WG_IPV6_CIDR` | `fdcc::/112` | IPv6 CIDR range |
### Client Connection Settings
| Env | Example | Description |
| --------------------------------- | ----------------- | ------------------------------- |
| `WG_HOST` | `vpn.example.com` | Host clients will connect to |
| `WG_CLIENT_PORT` | `51820` | Port clients will connect to |
| `WG_DEFAULT_DNS` | `1.1.1.1,8.8.8.8` | Default DNS servers for clients |
| `WG_DEFAULT_ALLOWED_IPS` | `0.0.0.0/0,::/0` | Default allowed IPs for clients |
| `WG_DEFAULT_MTU` | `1420` | Default MTU for clients |
| `WG_DEFAULT_PERSISTENT_KEEPALIVE` | `25` | Default persistent keepalive |
### General Settings
| Env | Example | Description |
| ----------------------- | ----------------- | ------------------------- |
| `WG_SESSION_TIMEOUT` | `3600` | Session timeout (seconds) |
| `WG_METRICS_PASSWORD` | `mypassword123` | Metrics endpoint password |
| `WG_METRICS_PROMETHEUS` | `true` or `false` | Enable Prometheus metrics |
| `WG_METRICS_JSON` | `true` or `false` | Enable JSON metrics |
### Hooks
| Env | Example | Description |
| -------------- | ------------------------- | --------------------- |
| `WG_PRE_UP` | `echo "Starting WG"` | PreUp hook command |
| `WG_POST_UP` | `iptables -A FORWARD ...` | PostUp hook command |
| `WG_PRE_DOWN` | `echo "Stopping WG"` | PreDown hook command |
| `WG_POST_DOWN` | `iptables -D FORWARD ...` | PostDown hook command |
/// warning | Override Behavior
When these override environment variables are set:
- The specified values will be used at runtime instead of database settings
- You can still update these fields through the Web UI and they will be saved to the database
- However, the overridden values from environment variables will always take precedence at runtime
- The Web UI will display the database values with warning indicators showing which fields are overridden
- On first start, if no database values exist, some overridden values will be saved to the database
Some overrides will not be applied to existing clients until they are manually edited.
- `WG_DEFAULT_*` settings will only apply to new clients
- `WG_IPV4_CIDR` and `WG_IPV6_CIDR` changes will require clients to be manually edited to take effect
///
/// note | Note on Port Variables
- `WG_PORT` - The port WireGuard listens on (interface port)
- `WG_CLIENT_PORT` - The port clients connect to (endpoint port, uses `WG_PORT` if not set)
- `PORT` - The port the Web UI listens on (HTTP server port)
In most cases you will only need to set `WG_PORT` to change the WireGuard port.
Keep in mind that you have to adjust both sides of the port publish option in your docker setup.
///

16
docs/content/advanced/config/unattended-setup.md

@ -11,18 +11,20 @@ These will only be used during the first start of the container. After that, the
| `INIT_ENABLED` | `true` | Enables the below env vars | 0 | | `INIT_ENABLED` | `true` | Enables the below env vars | 0 |
| `INIT_USERNAME` | `admin` | Sets admin username | 1 | | `INIT_USERNAME` | `admin` | Sets admin username | 1 |
| `INIT_PASSWORD` | `Se!ureP%ssw` | Sets admin password | 1 | | `INIT_PASSWORD` | `Se!ureP%ssw` | Sets admin password | 1 |
| `INIT_HOST` | `vpn.example.com` | Host clients will connect to | 1 | | `INIT_HOST` | `vpn.example.com` | Host clients will connect to | 2 |
| `INIT_PORT` | `51820` | Port clients will connect to and wireguard will listen on | 1 | | `INIT_PORT` | `51820` | Port clients will connect to and WireGuard will listen on | 2 |
| `INIT_DNS` | `1.1.1.1,8.8.8.8` | Sets global dns setting | 2 | | `INIT_DNS` | `1.1.1.1,8.8.8.8` | Sets global dns setting | 3 |
| `INIT_IPV4_CIDR` | `10.8.0.0/24` | Sets IPv4 cidr | 3 | | `INIT_IPV4_CIDR` | `10.8.0.0/24` | Sets IPv4 cidr | 4 |
| `INIT_IPV6_CIDR` | `2001:0DB8::/32` | Sets IPv6 cidr | 3 | | `INIT_IPV6_CIDR` | `2001:0DB8::/32` | Sets IPv6 cidr | 4 |
| `INIT_ALLOWED_IPS` | `10.8.0.0/24,2001:0DB8::/32` | Sets global Allowed IPs | 4 | | `INIT_ALLOWED_IPS` | `10.8.0.0/24,2001:0DB8::/32` | Sets global Allowed IPs | 5 |
/// warning | Variables have to be used together /// warning | Variables have to be used together
If variables are in the same group, you have to set all of them. For example, if you set `INIT_IPV4_CIDR`, you also have to set `INIT_IPV6_CIDR`. If variables are in the same group, you have to set all of them. For example, if you set `INIT_IPV4_CIDR`, you also have to set `INIT_IPV6_CIDR`.
If you want to skip the setup process, you have to configure group `1` To skip the setup process, you must configure groups `1` and `2`. You can alternatively use `WG_HOST` and `WG_PORT` to set group `2` without using the `INIT_` variables.
Avoid setting both `INIT_` and `WG_` variables for the same setting to prevent confusion.
/// ///
/// note | Security /// note | Security

4
docs/content/advanced/migrate/from-14-to-15.md

@ -51,7 +51,9 @@ In the setup wizard, select that you already have a configuration file and uploa
### Environment Variables ### Environment Variables
v15 does not use the same environment variables as v14, most of them have been moved to the Admin Panel in the Web UI. v15 does use some of the environment variables as v14. View [Configuration Overrides](../config/optional-config.md#configuration-overrides) to see which environment variables are supported in v15.
If you want to be able to change settings through the Web UI, do not set the corresponding environment variables, as they will override the database settings. Instead, manually change the settings through the Web UI after the migration.
### Done ### Done

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

@ -1,5 +1,12 @@
<template> <template>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div
v-if="overridden"
class="flex w-fit items-center gap-2 rounded-lg bg-amber-50 p-2 text-sm text-amber-700 dark:bg-amber-900/20 dark:text-amber-400"
>
<IconsWarning class="size-4" />
<span>This field is overridden by an environment variable</span>
</div>
<div v-if="data?.length === 0"> <div v-if="data?.length === 0">
{{ emptyText || $t('form.noItems') }} {{ emptyText || $t('form.noItems') }}
</div> </div>
@ -27,7 +34,11 @@
<script lang="ts" setup> <script lang="ts" setup>
const data = defineModel<string[]>(); const data = defineModel<string[]>();
defineProps<{ emptyText?: string[]; name: string }>(); defineProps<{
emptyText?: string[];
name: string;
overridden?: boolean;
}>();
function update(e: Event, i: number) { function update(e: Event, i: number) {
const v = (e.target as HTMLInputElement).value; const v = (e.target as HTMLInputElement).value;

7
src/app/components/Form/HostField.vue

@ -6,6 +6,12 @@
<BaseTooltip v-if="description" :text="description"> <BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" /> <IconsInfo class="size-4" />
</BaseTooltip> </BaseTooltip>
<BaseTooltip
v-if="overridden"
text="This field is overridden by an environment variable"
>
<IconsWarning class="size-4 text-amber-500" />
</BaseTooltip>
</div> </div>
<div class="flex gap-1"> <div class="flex gap-1">
<BaseInput <BaseInput
@ -38,6 +44,7 @@ defineProps<{
description?: string; description?: string;
placeholder?: string; placeholder?: string;
url: '/api/admin/ip-info' | '/api/setup/4'; url: '/api/admin/ip-info' | '/api/setup/4';
overridden?: boolean;
}>(); }>();
const data = defineModel<string | null>({ const data = defineModel<string | null>({

7
src/app/components/Form/NullTextField.vue

@ -6,6 +6,12 @@
<BaseTooltip v-if="description" :text="description"> <BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" /> <IconsInfo class="size-4" />
</BaseTooltip> </BaseTooltip>
<BaseTooltip
v-if="overridden"
text="This field is overridden by an environment variable"
>
<IconsWarning class="size-4 text-amber-500" />
</BaseTooltip>
</div> </div>
<BaseInput <BaseInput
:id="id" :id="id"
@ -24,6 +30,7 @@ defineProps<{
description?: string; description?: string;
autocomplete?: string; autocomplete?: string;
placeholder?: string; placeholder?: string;
overridden?: boolean;
}>(); }>();
const data = defineModel<string | null>({ const data = defineModel<string | null>({

13
src/app/components/Form/NumberField.vue

@ -6,12 +6,23 @@
<BaseTooltip v-if="description" :text="description"> <BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" /> <IconsInfo class="size-4" />
</BaseTooltip> </BaseTooltip>
<BaseTooltip
v-if="overridden"
text="This field is overridden by an environment variable"
>
<IconsWarning class="size-4 text-amber-500" />
</BaseTooltip>
</div> </div>
<BaseInput :id="id" v-model.number="data" :name="id" type="number" /> <BaseInput :id="id" v-model.number="data" :name="id" type="number" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ id: string; label: string; description?: string }>(); defineProps<{
id: string;
label: string;
description?: string;
overridden?: boolean;
}>();
const data = defineModel<number>(); const data = defineModel<number>();
</script> </script>

13
src/app/components/Form/SwitchField.vue

@ -6,6 +6,12 @@
<BaseTooltip v-if="description" :text="description"> <BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" /> <IconsInfo class="size-4" />
</BaseTooltip> </BaseTooltip>
<BaseTooltip
v-if="overridden"
text="This field is overridden by an environment variable"
>
<IconsWarning class="size-4 text-amber-500" />
</BaseTooltip>
</div> </div>
<div class="my-auto"> <div class="my-auto">
<BaseSwitch :id="id" v-model="data" /> <BaseSwitch :id="id" v-model="data" />
@ -13,6 +19,11 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ id: string; label: string; description?: string }>(); defineProps<{
id: string;
label: string;
description?: string;
overridden?: boolean;
}>();
const data = defineModel<boolean>(); const data = defineModel<boolean>();
</script> </script>

7
src/app/components/Form/TextField.vue

@ -6,6 +6,12 @@
<BaseTooltip v-if="description" :text="description"> <BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" /> <IconsInfo class="size-4" />
</BaseTooltip> </BaseTooltip>
<BaseTooltip
v-if="overridden"
text="This field is overridden by an environment variable"
>
<IconsWarning class="size-4 text-amber-500" />
</BaseTooltip>
</div> </div>
<BaseInput <BaseInput
:id="id" :id="id"
@ -24,6 +30,7 @@ defineProps<{
description?: string; description?: string;
autocomplete?: string; autocomplete?: string;
disabled?: boolean; disabled?: boolean;
overridden?: boolean;
}>(); }>();
const data = defineModel<string>(); const data = defineModel<string>();

17
src/app/pages/admin/config.vue

@ -9,12 +9,14 @@
:label="$t('general.host')" :label="$t('general.host')"
:description="$t('admin.config.hostDesc')" :description="$t('admin.config.hostDesc')"
url="/api/admin/ip-info" url="/api/admin/ip-info"
:overridden="overrides?.host"
/> />
<FormNumberField <FormNumberField
id="port" id="port"
v-model="data.port" v-model="data.port"
:label="$t('general.port')" :label="$t('general.port')"
:description="$t('admin.config.portDesc')" :description="$t('admin.config.portDesc')"
:overridden="overrides?.port"
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
@ -24,13 +26,18 @@
<FormArrayField <FormArrayField
v-model="data.defaultAllowedIps" v-model="data.defaultAllowedIps"
name="defaultAllowedIps" name="defaultAllowedIps"
:overridden="overrides?.defaultAllowedIps"
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading :description="$t('admin.config.dnsDesc')"> <FormHeading :description="$t('admin.config.dnsDesc')">
{{ $t('general.dns') }} {{ $t('general.dns') }}
</FormHeading> </FormHeading>
<FormArrayField v-model="data.defaultDns" name="defaultDns" /> <FormArrayField
v-model="data.defaultDns"
name="defaultDns"
:overridden="overrides?.defaultDns"
/>
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormHeading>{{ $t('form.sectionAdvanced') }}</FormHeading> <FormHeading>{{ $t('form.sectionAdvanced') }}</FormHeading>
@ -39,12 +46,14 @@
v-model="data.defaultMtu" v-model="data.defaultMtu"
:label="$t('general.mtu')" :label="$t('general.mtu')"
:description="$t('admin.config.mtuDesc')" :description="$t('admin.config.mtuDesc')"
:overridden="overrides?.defaultMtu"
/> />
<FormNumberField <FormNumberField
id="defaultPersistentKeepalive" id="defaultPersistentKeepalive"
v-model="data.defaultPersistentKeepalive" v-model="data.defaultPersistentKeepalive"
:label="$t('general.persistentKeepalive')" :label="$t('general.persistentKeepalive')"
:description="$t('admin.config.persistentKeepaliveDesc')" :description="$t('admin.config.persistentKeepaliveDesc')"
:overridden="overrides?.defaultPersistentKeepalive"
/> />
</FormGroup> </FormGroup>
<FormGroup v-if="globalStore.information?.isAwg"> <FormGroup v-if="globalStore.information?.isAwg">
@ -118,6 +127,12 @@ const { data: _data, refresh } = await useFetch(`/api/admin/userconfig`, {
method: 'get', method: 'get',
}); });
const { data: overridesData } = await useFetch(`/api/admin/overrides`, {
method: 'get',
});
const overrides = computed(() => overridesData.value?.userConfig);
const data = toRef(_data.value); const data = toRef(_data.value);
const _submit = useSubmit( const _submit = useSubmit(

11
src/app/pages/admin/general.vue

@ -7,6 +7,7 @@
v-model="data.sessionTimeout" v-model="data.sessionTimeout"
:label="$t('admin.general.sessionTimeout')" :label="$t('admin.general.sessionTimeout')"
:description="$t('admin.general.sessionTimeoutDesc')" :description="$t('admin.general.sessionTimeoutDesc')"
:overridden="overrides?.sessionTimeout"
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
@ -16,18 +17,21 @@
v-model="data.metricsPassword" v-model="data.metricsPassword"
:label="$t('admin.general.metricsPassword')" :label="$t('admin.general.metricsPassword')"
:description="$t('admin.general.metricsPasswordDesc')" :description="$t('admin.general.metricsPasswordDesc')"
:overridden="overrides?.metricsPassword"
/> />
<FormSwitchField <FormSwitchField
id="prometheus" id="prometheus"
v-model="data.metricsPrometheus" v-model="data.metricsPrometheus"
:label="$t('admin.general.prometheus')" :label="$t('admin.general.prometheus')"
:description="$t('admin.general.prometheusDesc')" :description="$t('admin.general.prometheusDesc')"
:overridden="overrides?.metricsPrometheus"
/> />
<FormSwitchField <FormSwitchField
id="json" id="json"
v-model="data.metricsJson" v-model="data.metricsJson"
:label="$t('admin.general.json')" :label="$t('admin.general.json')"
:description="$t('admin.general.jsonDesc')" :description="$t('admin.general.jsonDesc')"
:overridden="overrides?.metricsJson"
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
@ -43,6 +47,13 @@
const { data: _data, refresh } = await useFetch(`/api/admin/general`, { const { data: _data, refresh } = await useFetch(`/api/admin/general`, {
method: 'get', method: 'get',
}); });
const { data: overridesData } = await useFetch(`/api/admin/overrides`, {
method: 'get',
});
const overrides = computed(() => overridesData.value?.general);
const data = toRef(_data.value); const data = toRef(_data.value);
const _submit = useSubmit( const _submit = useSubmit(

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

@ -6,21 +6,25 @@
id="PreUp" id="PreUp"
v-model="data.preUp" v-model="data.preUp"
:label="$t('hooks.preUp')" :label="$t('hooks.preUp')"
:overridden="overrides?.preUp"
/> />
<FormTextArea <FormTextArea
id="PostUp" id="PostUp"
v-model="data.postUp" v-model="data.postUp"
:label="$t('hooks.postUp')" :label="$t('hooks.postUp')"
:overridden="overrides?.postUp"
/> />
<FormTextArea <FormTextArea
id="PreDown" id="PreDown"
v-model="data.preDown" v-model="data.preDown"
:label="$t('hooks.preDown')" :label="$t('hooks.preDown')"
:overridden="overrides?.preDown"
/> />
<FormTextArea <FormTextArea
id="PostDown" id="PostDown"
v-model="data.postDown" v-model="data.postDown"
:label="$t('hooks.postDown')" :label="$t('hooks.postDown')"
:overridden="overrides?.postDown"
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
@ -37,6 +41,12 @@ const { data: _data, refresh } = await useFetch(`/api/admin/hooks`, {
method: 'get', method: 'get',
}); });
const { data: overridesData } = await useFetch(`/api/admin/overrides`, {
method: 'get',
});
const overrides = computed(() => overridesData.value?.hooks);
const data = toRef(_data.value); const data = toRef(_data.value);
const _submit = useSubmit( const _submit = useSubmit(

9
src/app/pages/admin/interface.vue

@ -7,18 +7,21 @@
v-model="data.mtu" v-model="data.mtu"
:label="$t('general.mtu')" :label="$t('general.mtu')"
:description="$t('admin.interface.mtuDesc')" :description="$t('admin.interface.mtuDesc')"
:overridden="overrides?.mtu"
/> />
<FormNumberField <FormNumberField
id="port" id="port"
v-model="data.port" v-model="data.port"
:label="$t('general.port')" :label="$t('general.port')"
:description="$t('admin.interface.portDesc')" :description="$t('admin.interface.portDesc')"
:overridden="overrides?.port"
/> />
<FormTextField <FormTextField
id="device" id="device"
v-model="data.device" v-model="data.device"
:label="$t('admin.interface.device')" :label="$t('admin.interface.device')"
:description="$t('admin.interface.deviceDesc')" :description="$t('admin.interface.deviceDesc')"
:overridden="overrides?.device"
/> />
</FormGroup> </FormGroup>
<FormGroup v-if="globalStore.information?.isAwg"> <FormGroup v-if="globalStore.information?.isAwg">
@ -173,6 +176,12 @@ const { data: _data, refresh } = await useFetch(`/api/admin/interface`, {
method: 'get', method: 'get',
}); });
const { data: overridesData } = await useFetch(`/api/admin/overrides`, {
method: 'get',
});
const overrides = computed(() => overridesData.value?.interface);
const data = toRef(_data.value); const data = toRef(_data.value);
const _submit = useSubmit( const _submit = useSubmit(

10
src/app/pages/setup/2.vue

@ -56,9 +56,15 @@ const _submit = useSubmit(
body: data, body: data,
}), }),
{ {
revert: async (success) => { revert: async (success, data) => {
if (success) { if (success) {
await navigateTo('/setup/3'); if (data?.setupDone) {
// Setup is complete, redirect to success page
await navigateTo('/setup/success');
} else {
// Continue to step 3
await navigateTo('/setup/3');
}
} }
}, },
noSuccessToast: true, noSuccessToast: true,

1
src/server/api/admin/interface/cidr.post.ts

@ -8,7 +8,6 @@ export default definePermissionEventHandler(
event, event,
validateZod(InterfaceCidrUpdateSchema, event) validateZod(InterfaceCidrUpdateSchema, event)
); );
await Database.interfaces.updateCidr(data); await Database.interfaces.updateCidr(data);
await WireGuard.saveConfig(); await WireGuard.saveConfig();
return { success: true }; return { success: true };

34
src/server/api/admin/overrides.get.ts

@ -0,0 +1,34 @@
export default definePermissionEventHandler('admin', 'any', async () => {
return {
interface: {
port: WG_OVERRIDE_ENV.PORT !== undefined,
device: WG_OVERRIDE_ENV.DEVICE !== undefined,
mtu: WG_OVERRIDE_ENV.MTU !== undefined,
ipv4Cidr: WG_OVERRIDE_ENV.IPV4_CIDR !== undefined,
ipv6Cidr: WG_OVERRIDE_ENV.IPV6_CIDR !== undefined,
},
userConfig: {
host: WG_CLIENT_OVERRIDE_ENV.HOST !== undefined,
port: WG_CLIENT_OVERRIDE_ENV.CLIENT_PORT !== undefined,
defaultDns: WG_CLIENT_OVERRIDE_ENV.DEFAULT_DNS !== undefined,
defaultAllowedIps:
WG_CLIENT_OVERRIDE_ENV.DEFAULT_ALLOWED_IPS !== undefined,
defaultMtu: WG_CLIENT_OVERRIDE_ENV.DEFAULT_MTU !== undefined,
defaultPersistentKeepalive:
WG_CLIENT_OVERRIDE_ENV.DEFAULT_PERSISTENT_KEEPALIVE !== undefined,
},
general: {
sessionTimeout: WG_GENERAL_OVERRIDE_ENV.SESSION_TIMEOUT !== undefined,
metricsPassword: WG_GENERAL_OVERRIDE_ENV.METRICS_PASSWORD !== undefined,
metricsPrometheus:
WG_GENERAL_OVERRIDE_ENV.METRICS_PROMETHEUS !== undefined,
metricsJson: WG_GENERAL_OVERRIDE_ENV.METRICS_JSON !== undefined,
},
hooks: {
preUp: WG_HOOKS_OVERRIDE_ENV.PRE_UP !== undefined,
postUp: WG_HOOKS_OVERRIDE_ENV.POST_UP !== undefined,
preDown: WG_HOOKS_OVERRIDE_ENV.PRE_DOWN !== undefined,
postDown: WG_HOOKS_OVERRIDE_ENV.POST_DOWN !== undefined,
},
};
});

17
src/server/api/setup/2.post.ts

@ -8,6 +8,19 @@ export default defineSetupEventHandler(2, async ({ event }) => {
await Database.users.create(username, password); await Database.users.create(username, password);
await Database.general.setSetupStep(3); // If host and port are already set by environment variables, skip step 4
return { success: true }; const host = WG_INITIAL_ENV.HOST ?? WG_CLIENT_OVERRIDE_ENV.HOST;
const port = WG_INITIAL_ENV.PORT ?? WG_INTERFACE_OVERRIDE_ENV.PORT;
const setupDone = host && port;
if (setupDone) {
// Skip to done
await Database.general.setSetupStep(0);
} else {
// Proceed to step 3 (which leads to step 4)
await Database.general.setSetupStep(3);
}
return { success: true, setupDone: setupDone };
}); });

21
src/server/database/repositories/client/service.ts

@ -175,26 +175,30 @@ export class ClientService {
return this.#db.transaction(async (tx) => { return this.#db.transaction(async (tx) => {
const clients = await tx.query.client.findMany().execute(); const clients = await tx.query.client.findMany().execute();
const clientInterface = await tx.query.wgInterface const _clientInterface = await tx.query.wgInterface
.findFirst({ .findFirst({
where: eq(wgInterface.name, 'wg0'), where: eq(wgInterface.name, 'wg0'),
}) })
.execute(); .execute();
if (!clientInterface) { if (!_clientInterface) {
throw new Error('WireGuard interface not found'); throw new Error('WireGuard interface not found');
} }
const clientConfig = await tx.query.userConfig const clientInterface = applyInterfaceOverrides(_clientInterface);
const _clientConfig = await tx.query.userConfig
.findFirst({ .findFirst({
where: eq(userConfig.id, clientInterface.name), where: eq(userConfig.id, clientInterface.name),
}) })
.execute(); .execute();
if (!clientConfig) { if (!_clientConfig) {
throw new Error('WireGuard interface configuration not found'); throw new Error('WireGuard interface configuration not found');
} }
const clientConfig = applyUserConfigOverrides(_clientConfig);
const ipv4Cidr = parseCidr(clientInterface.ipv4Cidr); const ipv4Cidr = parseCidr(clientInterface.ipv4Cidr);
const ipv4Address = nextIP(4, ipv4Cidr, clients); const ipv4Address = nextIP(4, ipv4Cidr, clients);
const ipv6Cidr = parseCidr(clientInterface.ipv6Cidr); const ipv6Cidr = parseCidr(clientInterface.ipv6Cidr);
@ -241,16 +245,18 @@ export class ClientService {
update(id: ID, data: UpdateClientType) { update(id: ID, data: UpdateClientType) {
return this.#db.transaction(async (tx) => { return this.#db.transaction(async (tx) => {
const clientInterface = await tx.query.wgInterface const _clientInterface = await tx.query.wgInterface
.findFirst({ .findFirst({
where: eq(wgInterface.name, 'wg0'), where: eq(wgInterface.name, 'wg0'),
}) })
.execute(); .execute();
if (!clientInterface) { if (!_clientInterface) {
throw new Error('WireGuard interface not found'); throw new Error('WireGuard interface not found');
} }
const clientInterface = applyInterfaceOverrides(_clientInterface);
if (!containsCidr(clientInterface.ipv4Cidr, data.ipv4Address)) { if (!containsCidr(clientInterface.ipv4Cidr, data.ipv4Address)) {
throw new Error('IPv4 address is not within the CIDR range'); throw new Error('IPv4 address is not within the CIDR range');
} }
@ -272,7 +278,8 @@ export class ClientService {
privateKey, privateKey,
publicKey, publicKey,
}: ClientCreateFromExistingType) { }: ClientCreateFromExistingType) {
const clientConfig = await Database.userConfigs.get(); const _clientConfig = await Database.userConfigs.get();
const clientConfig = applyUserConfigOverrides(_clientConfig);
return this.#db return this.#db
.insert(client) .insert(client)

26
src/server/database/sqlite.ts

@ -101,22 +101,26 @@ async function initialSetup(db: DBServiceType) {
}); });
} }
if ( if (WG_INITIAL_ENV.USERNAME && WG_INITIAL_ENV.PASSWORD) {
WG_INITIAL_ENV.USERNAME &&
WG_INITIAL_ENV.PASSWORD &&
WG_INITIAL_ENV.HOST &&
WG_INITIAL_ENV.PORT
) {
DB_DEBUG('Creating initial user...'); DB_DEBUG('Creating initial user...');
await db.users.create(WG_INITIAL_ENV.USERNAME, WG_INITIAL_ENV.PASSWORD); await db.users.create(WG_INITIAL_ENV.USERNAME, WG_INITIAL_ENV.PASSWORD);
await db.general.setSetupStep(3);
}
// Use INIT vars or fall back to override vars for HOST and PORT
const host = WG_INITIAL_ENV.HOST ?? WG_CLIENT_OVERRIDE_ENV.HOST;
const port = WG_INITIAL_ENV.PORT ?? WG_INTERFACE_OVERRIDE_ENV.PORT;
// HOST and PORT can come from either INIT vars or override vars
if (host && port) {
DB_DEBUG('Setting initial host and port...'); DB_DEBUG('Setting initial host and port...');
await db.userConfigs.updateHostPort( await db.userConfigs.updateHostPort(host, port);
WG_INITIAL_ENV.HOST,
WG_INITIAL_ENV.PORT
);
await db.general.setSetupStep(0); // Setup completion requires USERNAME and PASSWORD (no overrides for these)
if (WG_INITIAL_ENV.USERNAME && WG_INITIAL_ENV.PASSWORD) {
await db.general.setSetupStep(0);
}
} }
} }

7
src/server/middleware/setup.ts

@ -9,12 +9,17 @@ export default defineEventHandler(async (event) => {
const { step, done } = await Database.general.getSetupStep(); const { step, done } = await Database.general.getSetupStep();
if (!done) { if (!done) {
const parsedSetup = url.pathname.match(/\/setup\/(\d)/); const parsedSetup = url.pathname.match(/\/setup\/(\d|migrate|success)/);
if (!parsedSetup) { if (!parsedSetup) {
return sendRedirect(event, `/setup/1`, 302); return sendRedirect(event, `/setup/1`, 302);
} }
const [_, currentSetup] = parsedSetup; const [_, currentSetup] = parsedSetup;
// Allow access to success page during setup
if (currentSetup === 'success') {
return;
}
if (step.toString() === currentSetup) { if (step.toString() === currentSetup) {
return; return;
} }

47
src/server/utils/WireGuard.ts

@ -12,7 +12,10 @@ class WireGuard {
* Save and sync config * Save and sync config
*/ */
async saveConfig() { async saveConfig() {
const wgInterface = await Database.interfaces.get(); const wgInterface = applyInterfaceOverrides(
await Database.interfaces.get()
);
await this.#saveWireguardConfig(wgInterface); await this.#saveWireguardConfig(wgInterface);
await this.#syncWireguardConfig(wgInterface); await this.#syncWireguardConfig(wgInterface);
await this.#applyFirewallRules(wgInterface); await this.#applyFirewallRules(wgInterface);
@ -39,7 +42,7 @@ class WireGuard {
*/ */
async #saveWireguardConfig(wgInterface: InterfaceType) { async #saveWireguardConfig(wgInterface: InterfaceType) {
const clients = await Database.clients.getAll(); const clients = await Database.clients.getAll();
const hooks = await Database.hooks.get(); const hooks = applyHooksOverrides(await Database.hooks.get());
const result = []; const result = [];
result.push( result.push(
@ -164,8 +167,12 @@ class WireGuard {
} }
async getClientConfiguration({ clientId }: { clientId: ID }) { async getClientConfiguration({ clientId }: { clientId: ID }) {
const wgInterface = await Database.interfaces.get(); const wgInterface = applyInterfaceOverrides(
const userConfig = await Database.userConfigs.get(); await Database.interfaces.get()
);
const userConfig = applyUserConfigOverrides(
await Database.userConfigs.get()
);
const client = await Database.clients.get(clientId); const client = await Database.clients.get(clientId);
@ -227,25 +234,33 @@ class WireGuard {
Database.interfaces.update(wgInterface); Database.interfaces.update(wgInterface);
} }
WG_DEBUG(`Starting Wireguard Interface ${wgInterface.name}...`); const wgInterfaceWithOverrides = applyInterfaceOverrides(wgInterface);
await this.#saveWireguardConfig(wgInterface);
await wg.down(wgInterface.name).catch(() => {}); WG_DEBUG(
await wg.up(wgInterface.name).catch((err) => { `Starting Wireguard Interface ${wgInterfaceWithOverrides.name}...`
);
await this.#saveWireguardConfig(wgInterfaceWithOverrides);
await wg.down(wgInterfaceWithOverrides.name).catch(() => {});
await wg.up(wgInterfaceWithOverrides.name).catch((err) => {
if ( if (
err && err &&
err.message && err.message &&
err.message.includes(`Cannot find device "${wgInterface.name}"`) err.message.includes(
`Cannot find device "${wgInterfaceWithOverrides.name}"`
)
) { ) {
throw new Error( throw new Error(
`WireGuard exited with the error: Cannot find device "${wgInterface.name}"\nThis usually means that your host's kernel does not support WireGuard!`, `WireGuard exited with the error: Cannot find device "${wgInterfaceWithOverrides.name}"\nThis usually means that your host's kernel does not support WireGuard!`,
{ cause: err.message } { cause: err.message }
); );
} }
throw err; throw err;
}); });
await this.#syncWireguardConfig(wgInterface); await this.#syncWireguardConfig(wgInterfaceWithOverrides);
WG_DEBUG(`Wireguard Interface ${wgInterface.name} started successfully.`); WG_DEBUG(
`Wireguard Interface ${wgInterfaceWithOverrides.name} started successfully.`
);
// Check if firewall was enabled but iptables isn't available // Check if firewall was enabled but iptables isn't available
if (wgInterface.firewallEnabled) { if (wgInterface.firewallEnabled) {
@ -282,12 +297,16 @@ class WireGuard {
// Shutdown wireguard // Shutdown wireguard
async Shutdown() { async Shutdown() {
const wgInterface = await Database.interfaces.get(); const wgInterface = applyInterfaceOverrides(
await Database.interfaces.get()
);
await wg.down(wgInterface.name).catch(() => {}); await wg.down(wgInterface.name).catch(() => {});
} }
async Restart() { async Restart() {
const wgInterface = await Database.interfaces.get(); const wgInterface = applyInterfaceOverrides(
await Database.interfaces.get()
);
await wg.restart(wgInterface.name); await wg.restart(wgInterface.name);
} }

174
src/server/utils/config.ts

@ -55,6 +55,78 @@ export const WG_INITIAL_ENV = {
: undefined, : undefined,
}; };
export const WG_INTERFACE_OVERRIDE_ENV = {
/** Override the WireGuard interface port */
PORT: process.env.WG_PORT
? Number.parseInt(process.env.WG_PORT, 10)
: undefined,
/** Override the network device/interface */
DEVICE: process.env.WG_DEVICE,
/** Override the MTU setting */
MTU: process.env.WG_MTU ? Number.parseInt(process.env.WG_MTU, 10) : undefined,
/** Override the IPv4 CIDR */
IPV4_CIDR: process.env.WG_IPV4_CIDR,
/** Override the IPv6 CIDR */
IPV6_CIDR: process.env.WG_IPV6_CIDR,
};
export const WG_CLIENT_OVERRIDE_ENV = {
/** Override the client connection host */
HOST: process.env.WG_HOST,
/** Override the client connection port (falls back to Interface Port) */
CLIENT_PORT: process.env.WG_CLIENT_PORT
? Number.parseInt(process.env.WG_CLIENT_PORT, 10)
: WG_INTERFACE_OVERRIDE_ENV.PORT,
/** Override default client DNS servers */
DEFAULT_DNS: process.env.WG_DEFAULT_DNS?.split(',').map((x) => x.trim()),
/** Override default client allowed IPs */
DEFAULT_ALLOWED_IPS: process.env.WG_DEFAULT_ALLOWED_IPS?.split(',').map((x) =>
x.trim()
),
/** Override default client MTU */
DEFAULT_MTU: process.env.WG_DEFAULT_MTU
? Number.parseInt(process.env.WG_DEFAULT_MTU, 10)
: undefined,
/** Override default client persistent keepalive */
DEFAULT_PERSISTENT_KEEPALIVE: process.env.WG_DEFAULT_PERSISTENT_KEEPALIVE
? Number.parseInt(process.env.WG_DEFAULT_PERSISTENT_KEEPALIVE, 10)
: undefined,
};
export const WG_GENERAL_OVERRIDE_ENV = {
/** Override session timeout */
SESSION_TIMEOUT: process.env.WG_SESSION_TIMEOUT
? Number.parseInt(process.env.WG_SESSION_TIMEOUT, 10)
: undefined,
/** Override metrics password */
METRICS_PASSWORD: process.env.WG_METRICS_PASSWORD,
/** Override metrics Prometheus enabled status */
METRICS_PROMETHEUS:
process.env.WG_METRICS_PROMETHEUS === 'true'
? true
: process.env.WG_METRICS_PROMETHEUS === 'false'
? false
: undefined,
/** Override metrics JSON enabled status */
METRICS_JSON:
process.env.WG_METRICS_JSON === 'true'
? true
: process.env.WG_METRICS_JSON === 'false'
? false
: undefined,
};
export const WG_HOOKS_OVERRIDE_ENV = {
/** Override PreUp hook */
PRE_UP: process.env.WG_PRE_UP,
/** Override PostUp hook */
POST_UP: process.env.WG_POST_UP,
/** Override PreDown hook */
PRE_DOWN: process.env.WG_PRE_DOWN,
/** Override PostDown hook */
POST_DOWN: process.env.WG_POST_DOWN,
};
function assertEnv<T extends string>(env: T) { function assertEnv<T extends string>(env: T) {
const val = process.env[env]; const val = process.env[env];
@ -64,3 +136,105 @@ function assertEnv<T extends string>(env: T) {
return val; return val;
} }
/**
* Apply environment variable overrides to an interface object
*/
export function applyInterfaceOverrides<
T extends {
port: number;
device: string;
mtu: number;
ipv4Cidr: string;
ipv6Cidr: string;
},
>(wgInterface: T): T {
return {
...wgInterface,
port: WG_INTERFACE_OVERRIDE_ENV.PORT ?? wgInterface.port,
device: WG_INTERFACE_OVERRIDE_ENV.DEVICE ?? wgInterface.device,
mtu: WG_INTERFACE_OVERRIDE_ENV.MTU ?? wgInterface.mtu,
ipv4Cidr: WG_INTERFACE_OVERRIDE_ENV.IPV4_CIDR ?? wgInterface.ipv4Cidr,
ipv6Cidr: WG_INTERFACE_OVERRIDE_ENV.IPV6_CIDR ?? wgInterface.ipv6Cidr,
};
}
/**
* Apply environment variable overrides to a user config object
*/
export function applyUserConfigOverrides<
T extends {
host: string;
port: number;
defaultDns: string[];
defaultAllowedIps: string[];
defaultMtu: number;
defaultPersistentKeepalive: number;
},
>(userConfig: T): T {
return {
...userConfig,
host: WG_CLIENT_OVERRIDE_ENV.HOST ?? userConfig.host,
port: WG_CLIENT_OVERRIDE_ENV.CLIENT_PORT ?? userConfig.port,
defaultDns: WG_CLIENT_OVERRIDE_ENV.DEFAULT_DNS ?? userConfig.defaultDns,
defaultAllowedIps:
WG_CLIENT_OVERRIDE_ENV.DEFAULT_ALLOWED_IPS ??
userConfig.defaultAllowedIps,
defaultMtu: WG_CLIENT_OVERRIDE_ENV.DEFAULT_MTU ?? userConfig.defaultMtu,
defaultPersistentKeepalive:
WG_CLIENT_OVERRIDE_ENV.DEFAULT_PERSISTENT_KEEPALIVE ??
userConfig.defaultPersistentKeepalive,
};
}
/**
* Apply environment variable overrides to a general config object
*/
export function applySessionOverrides<
T extends {
sessionTimeout: number;
},
>(generalConfig: T): T {
return {
...generalConfig,
sessionTimeout:
WG_GENERAL_OVERRIDE_ENV.SESSION_TIMEOUT ?? generalConfig.sessionTimeout,
};
}
export function applyMetricsOverrides<
T extends {
password: string | null;
prometheus: boolean;
json: boolean;
},
>(metricsConfig: T): T {
return {
...metricsConfig,
password:
WG_GENERAL_OVERRIDE_ENV.METRICS_PASSWORD ?? metricsConfig.password,
prometheus:
WG_GENERAL_OVERRIDE_ENV.METRICS_PROMETHEUS ?? metricsConfig.prometheus,
json: WG_GENERAL_OVERRIDE_ENV.METRICS_JSON ?? metricsConfig.json,
};
}
/**
* Apply environment variable overrides to a hooks object
*/
export function applyHooksOverrides<
T extends {
preUp: string;
postUp: string;
preDown: string;
postDown: string;
},
>(hooks: T): T {
return {
...hooks,
preUp: WG_HOOKS_OVERRIDE_ENV.PRE_UP ?? hooks.preUp,
postUp: WG_HOOKS_OVERRIDE_ENV.POST_UP ?? hooks.postUp,
preDown: WG_HOOKS_OVERRIDE_ENV.PRE_DOWN ?? hooks.preDown,
postDown: WG_HOOKS_OVERRIDE_ENV.POST_DOWN ?? hooks.postDown,
};
}

4
src/server/utils/handler.ts

@ -138,7 +138,9 @@ export const defineMetricsHandler = <
handler: MetricsHandler<TReq, TRes> handler: MetricsHandler<TReq, TRes>
) => { ) => {
return defineEventHandler(async (event) => { return defineEventHandler(async (event) => {
const metricsConfig = await Database.general.getMetricsConfig(); const metricsConfig = applyMetricsOverrides(
await Database.general.getMetricsConfig()
);
if (metricsConfig.password) { if (metricsConfig.password) {
const auth = getHeader(event, 'Authorization'); const auth = getHeader(event, 'Authorization');

10
src/server/utils/session.ts

@ -8,7 +8,10 @@ export type WGSession = Partial<{
const name = 'wg-easy'; const name = 'wg-easy';
export async function useWGSession(event: H3Event, rememberMe = false) { export async function useWGSession(event: H3Event, rememberMe = false) {
const sessionConfig = await Database.general.getSessionConfig(); const sessionConfig = applySessionOverrides(
await Database.general.getSessionConfig()
);
return useSession<WGSession>(event, { return useSession<WGSession>(event, {
password: sessionConfig.sessionPassword, password: sessionConfig.sessionPassword,
name, name,
@ -22,7 +25,10 @@ export async function useWGSession(event: H3Event, rememberMe = false) {
} }
export async function getWGSession(event: H3Event) { export async function getWGSession(event: H3Event) {
const sessionConfig = await Database.general.getSessionConfig(); const sessionConfig = applySessionOverrides(
await Database.general.getSessionConfig()
);
return getSession<WGSession>(event, { return getSession<WGSession>(event, {
password: sessionConfig.sessionPassword, password: sessionConfig.sessionPassword,
name, name,

Loading…
Cancel
Save