Compare commits

..

21 Commits

Author SHA1 Message Date
Bernd Storath da61cb680f fix docs 2025-11-17 11:44:21 +01:00
copilot-swe-agent[bot] 8b5e6c4c7a Use WG_PORT instead of WG_CLIENT_PORT for INIT_PORT fallback to match original behavior
- Changed setup/2.post.ts to use WG_INTERFACE_OVERRIDE_ENV.PORT instead of WG_CLIENT_OVERRIDE_ENV.CLIENT_PORT
- Changed sqlite.ts initialSetup to use WG_INTERFACE_OVERRIDE_ENV.PORT for consistency
- Updated unattended-setup.md documentation:
  - Changed INIT_PORT description to clarify it sets both interface port and endpoint port
  - Updated warning text to reference WG_PORT (not WG_CLIENT_PORT) as the override fallback
- This matches the original INIT_PORT behavior where updateHostPort() sets both ports to the same value

Co-authored-by: kaaax0815 <32197462+kaaax0815@users.noreply.github.com>
2025-11-17 10:39:51 +00:00
copilot-swe-agent[bot] e192855280 Fix discrepancies: use WG_CLIENT_PORT instead of WG_PORT for setup fallback
- Fixed setup/2.post.ts to use WG_CLIENT_OVERRIDE_ENV.CLIENT_PORT instead of WG_INTERFACE_OVERRIDE_ENV.PORT
- Fixed sqlite.ts initialSetup to use WG_CLIENT_OVERRIDE_ENV.CLIENT_PORT for consistency
- Corrected unattended-setup.md documentation:
  - Changed INIT_HOST and INIT_PORT from group "1*" to group "2"
  - Clarified INIT_PORT description as "Port clients will connect to (endpoint port)"
  - Updated group numbers: DNS is group 3, CIDR is group 4, Allowed IPs is group 5
  - Fixed warning text: setup requires groups 1 and 2, and use WG_CLIENT_PORT (not WG_PORT)
- Ensures consistency between code behavior and documentation

Co-authored-by: kaaax0815 <32197462+kaaax0815@users.noreply.github.com>
2025-11-17 10:31:11 +00:00
Bernd Storath 3a52e1321b fix description 2025-11-17 11:26:28 +01:00
Bernd Storath b29703af86 fix override and init option 2025-11-17 11:23:51 +01:00
copilot-swe-agent[bot] 516a3fa72c Skip setup step 4 (host/port) when override vars are set
- Modified setup/2.post.ts to check for WG_HOST and WG_CLIENT_PORT overrides
- If both overrides are set, auto-populate host/port and mark setup as complete
- Updated setup/2.vue to check setup status after user creation and redirect appropriately
- Modified setup middleware to allow access to success page during setup
- Setup now completes after step 2 when host/port are provided via environment variables

Co-authored-by: kaaax0815 <32197462+kaaax0815@users.noreply.github.com>
2025-11-17 09:49:53 +00:00
Bernd Storath e5cdb2b57a update docs 2025-11-17 10:42:21 +01:00
copilot-swe-agent[bot] 7fbc1cef68 Allow override vars to skip initial setup; split username/password from host/port in groups
- Modified initialSetup to use WG_* override vars as fallback for INIT_* vars
- Split group 1: USERNAME and PASSWORD remain in group 1
- Moved HOST and PORT to group 2 (can use WG_HOST and WG_CLIENT_PORT)
- DNS moved to group 3 (can use WG_DEFAULT_DNS)
- CIDR moved to group 4 (can use WG_IPV4_CIDR and WG_IPV6_CIDR)
- Allowed IPs moved to group 5 (can use WG_DEFAULT_ALLOWED_IPS)
- Updated documentation to explain override fallback behavior
- Setup can now be skipped with INIT_USERNAME, INIT_PASSWORD, and override vars

Co-authored-by: kaaax0815 <32197462+kaaax0815@users.noreply.github.com>
2025-11-17 09:34:01 +00:00
Bernd Storath 432e7a8197 update migration guide 2025-11-17 10:18:58 +01:00
Bernd Storath d12045e10c fix ui, fix client 2025-11-17 10:15:22 +01:00
copilot-swe-agent[bot] 015f3c5ba2 Fix documentation discrepancy about UI behavior
- Corrected override behavior description in optional-config.md
- Web UI displays database values with warning indicators, not overridden values
- Clarified that overrides take precedence at runtime, not in the UI display

Co-authored-by: kaaax0815 <32197462+kaaax0815@users.noreply.github.com>
2025-11-17 08:44:54 +00:00
Bernd Storath e42f7313ab fixes 2025-11-17 09:37:06 +01:00
Bernd Storath 993c130f65 format code 2025-11-14 15:41:48 +01:00
copilot-swe-agent[bot] fbf24410db Add metrics password override and implement frontend UI indicators
Backend changes:
- Added WG_METRICS_PASSWORD environment variable override
- Updated applyGeneralOverrides() to include metrics password
- Updated /api/admin/overrides endpoint to include metrics password

Frontend changes:
- Added override indicators (warning icons with tooltips) to all form fields
- Updated TextField, NumberField, NullTextField, SwitchField, HostField, ArrayField components
- Added overridden prop support to all form components
- Fetched /api/admin/overrides in all admin pages (interface, general, config, hooks)
- Warning icon displays when field is overridden by environment variable
- ArrayField shows banner when overridden
- Updated documentation with WG_METRICS_PASSWORD

Co-authored-by: kaaax0815 <32197462+kaaax0815@users.noreply.github.com>
2025-11-14 14:30:56 +00:00
copilot-swe-agent[bot] c1d5822f41 Add hooks overrides support with environment variables
- Added WG_PRE_UP, WG_POST_UP, WG_PRE_DOWN, WG_POST_DOWN environment variables
- Created applyHooksOverrides() helper function
- Updated WireGuard service to apply hooks overrides when generating config
- Updated /api/admin/overrides endpoint to include hooks override information
- Updated documentation with hooks environment variables

Co-authored-by: kaaax0815 <32197462+kaaax0815@users.noreply.github.com>
2025-11-14 14:10:07 +00:00
copilot-swe-agent[bot] 268916782d Return database values in GET endpoints; add overrides info route; remove comments
- GET endpoints now return raw database values (not overridden values)
- Created /api/admin/overrides route that returns which fields are overridden by env vars
- Removed comments from POST endpoints
- Admin panel can now show database values and display indicators for overridden fields

Co-authored-by: kaaax0815 <32197462+kaaax0815@users.noreply.github.com>
2025-11-14 13:52:54 +00:00
copilot-swe-agent[bot] 11ab71b5d2 Allow admin panel updates to be saved while overrides remain effective; remove WG_ENABLED
- Remove WG_ENABLED environment variable (interface cannot be disabled)
- Allow all admin panel updates to be saved to database
- Environment variable overrides take precedence at runtime only
- Users can now update values in admin panel even when overridden
- Updated documentation to clarify override behavior

Co-authored-by: kaaax0815 <32197462+kaaax0815@users.noreply.github.com>
2025-11-14 13:41:33 +00:00
copilot-swe-agent[bot] 67185192fd Refactor environment variables with shorter names and add comprehensive override support
- Rename OVERRIDE_INTERFACE_* to WG_* (shorter, non-conflicting)
- Add interface overrides: WG_PORT, WG_DEVICE, WG_MTU, WG_IPV4_CIDR, WG_IPV6_CIDR, WG_ENABLED
- Add client config overrides: WG_HOST, WG_CLIENT_PORT, WG_DEFAULT_DNS, WG_DEFAULT_ALLOWED_IPS, WG_DEFAULT_MTU, WG_DEFAULT_PERSISTENT_KEEPALIVE
- Add general config overrides: WG_SESSION_TIMEOUT, WG_METRICS_PROMETHEUS, WG_METRICS_JSON
- Update all API endpoints to apply and filter overrides
- Update documentation with all new environment variables
- Exclude AmneziaWG parameters (j*, s*, i*, h*) as requested

Co-authored-by: kaaax0815 <32197462+kaaax0815@users.noreply.github.com>
2025-11-14 13:25:22 +00:00
copilot-swe-agent[bot] 5fd3ee9843 Format documentation with prettier
Co-authored-by: kaaax0815 <32197462+kaaax0815@users.noreply.github.com>
2025-11-14 13:04:57 +00:00
copilot-swe-agent[bot] e444936c04 Add environment variables to override admin panel interface settings
Co-authored-by: kaaax0815 <32197462+kaaax0815@users.noreply.github.com>
2025-11-14 13:01:39 +00:00
copilot-swe-agent[bot] 5d6c35b183 Initial plan 2025-11-14 12:51:40 +00:00
36 changed files with 507 additions and 522 deletions
+1
View File
@@ -3,6 +3,7 @@
"aaron-bond.better-comments",
"dbaeumer.vscode-eslint",
"antfu.goto-alias",
"visualstudioexptteam.vscodeintellicode",
"esbenp.prettier-vscode",
"yoavbls.pretty-ts-errors",
"bradlc.vscode-tailwindcss",
-2
View File
@@ -13,7 +13,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Search / filter box (https://github.com/wg-easy/wg-easy/pull/2170)
- `INIT_ALLOWED_IPS` env var (https://github.com/wg-easy/wg-easy/pull/2164)
- Show client endpoint (https://github.com/wg-easy/wg-easy/pull/2058)
- Add option to view and copy config (https://github.com/wg-easy/wg-easy/pull/2289)
## Fixed
@@ -28,7 +27,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Publish on Codeberg (https://github.com/wg-easy/wg-easy/pull/2160)
- Allow empty DNS (https://github.com/wg-easy/wg-easy/pull/2052, https://github.com/wg-easy/wg-easy/pull/2057)
- Don't include keys in API responses (https://github.com/wg-easy/wg-easy/pull/2015)
- Try all QR ecc levels (https://github.com/wg-easy/wg-easy/pull/2288)
## Docs
@@ -20,3 +20,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.
///
## 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.
///
@@ -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_USERNAME` | `admin` | Sets admin username | 1 |
| `INIT_PASSWORD` | `Se!ureP%ssw` | Sets admin password | 1 |
| `INIT_HOST` | `vpn.example.com` | Host clients will connect to | 1 |
| `INIT_PORT` | `51820` | Port clients will connect to and wireguard will listen on | 1 |
| `INIT_DNS` | `1.1.1.1,8.8.8.8` | Sets global dns setting | 2 |
| `INIT_IPV4_CIDR` | `10.8.0.0/24` | Sets IPv4 cidr | 3 |
| `INIT_IPV6_CIDR` | `2001:0DB8::/32` | Sets IPv6 cidr | 3 |
| `INIT_ALLOWED_IPS` | `10.8.0.0/24,2001:0DB8::/32` | Sets global Allowed IPs | 4 |
| `INIT_HOST` | `vpn.example.com` | Host clients will connect to | 2 |
| `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 | 3 |
| `INIT_IPV4_CIDR` | `10.8.0.0/24` | Sets IPv4 cidr | 4 |
| `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 | 5 |
/// 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 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
@@ -51,7 +51,9 @@ In the setup wizard, select that you already have a configuration file and uploa
### 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
-29
View File
@@ -1,29 +0,0 @@
<template>
<div class="overflow-x-auto rounded border-2 border-red-800 py-2">
<pre
class="mx-2 inline-block"
@click="selectCode"
><code ref="codeBlock">{{ code }}</code></pre>
</div>
</template>
<script setup lang="ts">
defineProps<{
code: string;
}>();
const codeBlock = useTemplateRef('codeBlock');
function selectCode() {
// TODO: keyboard support?
if (codeBlock.value) {
const range = document.createRange();
range.selectNodeContents(codeBlock.value);
const sel = window.getSelection();
if (sel) {
sel.removeAllRanges();
sel.addRange(range);
}
}
}
</script>
@@ -1,74 +0,0 @@
<template>
<BaseDialog :trigger-class="triggerClass">
<template #trigger>
<slot />
</template>
<template #title>
{{ $t('client.config') }}
</template>
<template #description>
<div v-if="status === 'success'">
<BaseCodeBlock :code="config ?? ''" />
</div>
<div v-else>
<span>{{ $t('general.loading') }}</span>
</div>
</template>
<template #actions>
<DialogClose as-child>
<BaseSecondaryButton>{{ $t('dialog.cancel') }}</BaseSecondaryButton>
</DialogClose>
<DialogClose as-child>
<BasePrimaryButton @click="copyCode">
{{ $t('copy.copy') }}
</BasePrimaryButton>
</DialogClose>
</template>
</BaseDialog>
</template>
<script setup lang="ts">
const props = defineProps<{ triggerClass?: string; clientId: number }>();
const toast = useToast();
const { copied, copy, isSupported } = useClipboard({
// fallback does not work
legacy: false,
});
const { data: config, status } = useFetch(
`/api/client/${props.clientId}/configuration`,
{
responseType: 'text',
server: false,
}
);
async function copyCode() {
if (status.value !== 'success') {
return;
}
if (!isSupported.value) {
toast.showToast({
type: 'error',
message: $t('copy.notSupported'),
});
return;
}
await copy(config.value ?? '');
if (copied.value) {
toast.showToast({
type: 'success',
message: $t('copy.copied'),
});
} else {
toast.showToast({
type: 'error',
message: $t('copy.failed'),
});
}
}
</script>
+1 -1
View File
@@ -9,7 +9,7 @@
</div>
</template>
<template #actions>
<DialogClose as-child>
<DialogClose>
<BaseSecondaryButton>{{ $t('dialog.cancel') }}</BaseSecondaryButton>
</DialogClose>
</template>
+12 -1
View File
@@ -1,5 +1,12 @@
<template>
<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">
{{ emptyText || $t('form.noItems') }}
</div>
@@ -35,7 +42,11 @@
<script lang="ts" setup>
const data = defineModel<string[]>();
defineProps<{ emptyText?: string[]; name: string }>();
defineProps<{
emptyText?: string[];
name: string;
overridden?: boolean;
}>();
function update(e: Event, i: number) {
const v = (e.target as HTMLInputElement).value;
+7
View File
@@ -6,6 +6,12 @@
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
<BaseTooltip
v-if="overridden"
text="This field is overridden by an environment variable"
>
<IconsWarning class="size-4 text-amber-500" />
</BaseTooltip>
</div>
<div class="flex gap-1">
<BaseInput
@@ -38,6 +44,7 @@ defineProps<{
description?: string;
placeholder?: string;
url: '/api/admin/ip-info' | '/api/setup/4';
overridden?: boolean;
}>();
const data = defineModel<string | null>({
@@ -6,6 +6,12 @@
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
<BaseTooltip
v-if="overridden"
text="This field is overridden by an environment variable"
>
<IconsWarning class="size-4 text-amber-500" />
</BaseTooltip>
</div>
<BaseInput
:id="id"
@@ -24,6 +30,7 @@ defineProps<{
description?: string;
autocomplete?: string;
placeholder?: string;
overridden?: boolean;
}>();
const data = defineModel<string | null>({
+12 -1
View File
@@ -6,12 +6,23 @@
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
<BaseTooltip
v-if="overridden"
text="This field is overridden by an environment variable"
>
<IconsWarning class="size-4 text-amber-500" />
</BaseTooltip>
</div>
<BaseInput :id="id" v-model.number="data" :name="id" type="number" />
</template>
<script lang="ts" setup>
defineProps<{ id: string; label: string; description?: string }>();
defineProps<{
id: string;
label: string;
description?: string;
overridden?: boolean;
}>();
const data = defineModel<number>();
</script>
+12 -1
View File
@@ -6,11 +6,22 @@
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
<BaseTooltip
v-if="overridden"
text="This field is overridden by an environment variable"
>
<IconsWarning class="size-4 text-amber-500" />
</BaseTooltip>
</div>
<BaseSwitch :id="id" v-model="data" />
</template>
<script lang="ts" setup>
defineProps<{ id: string; label: string; description?: string }>();
defineProps<{
id: string;
label: string;
description?: string;
overridden?: boolean;
}>();
const data = defineModel<boolean>();
</script>
+7
View File
@@ -6,6 +6,12 @@
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
<BaseTooltip
v-if="overridden"
text="This field is overridden by an environment variable"
>
<IconsWarning class="size-4 text-amber-500" />
</BaseTooltip>
</div>
<BaseInput
:id="id"
@@ -24,6 +30,7 @@ defineProps<{
description?: string;
autocomplete?: string;
disabled?: boolean;
overridden?: boolean;
}>();
const data = defineModel<string>();
+16 -1
View File
@@ -9,12 +9,14 @@
:label="$t('general.host')"
:description="$t('admin.config.hostDesc')"
url="/api/admin/ip-info"
:overridden="overrides?.host"
/>
<FormNumberField
id="port"
v-model="data.port"
:label="$t('general.port')"
:description="$t('admin.config.portDesc')"
:overridden="overrides?.port"
/>
</FormGroup>
<FormGroup>
@@ -24,13 +26,18 @@
<FormArrayField
v-model="data.defaultAllowedIps"
name="defaultAllowedIps"
:overridden="overrides?.defaultAllowedIps"
/>
</FormGroup>
<FormGroup>
<FormHeading :description="$t('admin.config.dnsDesc')">
{{ $t('general.dns') }}
</FormHeading>
<FormArrayField v-model="data.defaultDns" name="defaultDns" />
<FormArrayField
v-model="data.defaultDns"
name="defaultDns"
:overridden="overrides?.defaultDns"
/>
</FormGroup>
<FormGroup>
<FormHeading>{{ $t('form.sectionAdvanced') }}</FormHeading>
@@ -39,12 +46,14 @@
v-model="data.defaultMtu"
:label="$t('general.mtu')"
:description="$t('admin.config.mtuDesc')"
:overridden="overrides?.defaultMtu"
/>
<FormNumberField
id="defaultPersistentKeepalive"
v-model="data.defaultPersistentKeepalive"
:label="$t('general.persistentKeepalive')"
:description="$t('admin.config.persistentKeepaliveDesc')"
:overridden="overrides?.defaultPersistentKeepalive"
/>
</FormGroup>
<FormGroup v-if="globalStore.information?.isAwg">
@@ -118,6 +127,12 @@ const { data: _data, refresh } = await useFetch(`/api/admin/userconfig`, {
method: 'get',
});
const { data: overridesData } = await useFetch(`/api/admin/overrides`, {
method: 'get',
});
const overrides = computed(() => overridesData.value?.userConfig);
const data = toRef(_data.value);
const _submit = useSubmit(
+11
View File
@@ -7,6 +7,7 @@
v-model="data.sessionTimeout"
:label="$t('admin.general.sessionTimeout')"
:description="$t('admin.general.sessionTimeoutDesc')"
:overridden="overrides?.sessionTimeout"
/>
</FormGroup>
<FormGroup>
@@ -16,18 +17,21 @@
v-model="data.metricsPassword"
:label="$t('admin.general.metricsPassword')"
:description="$t('admin.general.metricsPasswordDesc')"
:overridden="overrides?.metricsPassword"
/>
<FormSwitchField
id="prometheus"
v-model="data.metricsPrometheus"
:label="$t('admin.general.prometheus')"
:description="$t('admin.general.prometheusDesc')"
:overridden="overrides?.metricsPrometheus"
/>
<FormSwitchField
id="json"
v-model="data.metricsJson"
:label="$t('admin.general.json')"
:description="$t('admin.general.jsonDesc')"
:overridden="overrides?.metricsJson"
/>
</FormGroup>
<FormGroup>
@@ -43,6 +47,13 @@
const { data: _data, refresh } = await useFetch(`/api/admin/general`, {
method: 'get',
});
const { data: overridesData } = await useFetch(`/api/admin/overrides`, {
method: 'get',
});
const overrides = computed(() => overridesData.value?.general);
const data = toRef(_data.value);
const _submit = useSubmit(
+10
View File
@@ -6,21 +6,25 @@
id="PreUp"
v-model="data.preUp"
:label="$t('hooks.preUp')"
:overridden="overrides?.preUp"
/>
<FormTextField
id="PostUp"
v-model="data.postUp"
:label="$t('hooks.postUp')"
:overridden="overrides?.postUp"
/>
<FormTextField
id="PreDown"
v-model="data.preDown"
:label="$t('hooks.preDown')"
:overridden="overrides?.preDown"
/>
<FormTextField
id="PostDown"
v-model="data.postDown"
:label="$t('hooks.postDown')"
:overridden="overrides?.postDown"
/>
</FormGroup>
<FormGroup>
@@ -37,6 +41,12 @@ const { data: _data, refresh } = await useFetch(`/api/admin/hooks`, {
method: 'get',
});
const { data: overridesData } = await useFetch(`/api/admin/overrides`, {
method: 'get',
});
const overrides = computed(() => overridesData.value?.hooks);
const data = toRef(_data.value);
const _submit = useSubmit(
+9
View File
@@ -7,18 +7,21 @@
v-model="data.mtu"
:label="$t('general.mtu')"
:description="$t('admin.interface.mtuDesc')"
:overridden="overrides?.mtu"
/>
<FormNumberField
id="port"
v-model="data.port"
:label="$t('general.port')"
:description="$t('admin.interface.portDesc')"
:overridden="overrides?.port"
/>
<FormTextField
id="device"
v-model="data.device"
:label="$t('admin.interface.device')"
:description="$t('admin.interface.deviceDesc')"
:overridden="overrides?.device"
/>
</FormGroup>
<FormGroup v-if="globalStore.information?.isAwg">
@@ -164,6 +167,12 @@ const { data: _data, refresh } = await useFetch(`/api/admin/interface`, {
method: 'get',
});
const { data: overridesData } = await useFetch(`/api/admin/overrides`, {
method: 'get',
});
const overrides = computed(() => overridesData.value?.interface);
const data = toRef(_data.value);
const _submit = useSubmit(
-12
View File
@@ -186,18 +186,6 @@
as="span"
/>
</ClientsDeleteDialog>
<ClientsConfigDialog
trigger-class="col-span-2"
:client-id="data.id"
>
<FormSecondaryActionField
:label="$t('client.viewConfig')"
class="w-full"
type="button"
tabindex="-1"
as="span"
/>
</ClientsConfigDialog>
</FormGroup>
</FormElement>
</PanelBody>
+8 -2
View File
@@ -55,9 +55,15 @@ const _submit = useSubmit(
method: 'post',
},
{
revert: async (success) => {
revert: async (success, data) => {
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,
-2
View File
@@ -7,7 +7,6 @@ import it from './locales/it.json';
import ru from './locales/ru.json';
import zhhk from './locales/zh-HK.json';
import zhcn from './locales/zh-CN.json';
import zhtw from './locales/zh-TW.json';
import ko from './locales/ko.json';
import es from './locales/es.json';
import ptbr from './locales/pt-BR.json';
@@ -28,7 +27,6 @@ export default defineI18nConfig(() => ({
ru,
'zh-HK': zhhk,
'zh-CN': zhcn,
'zh-TW': zhtw,
ko,
es,
'pt-BR': ptbr,
+1 -9
View File
@@ -117,9 +117,7 @@
"notConnected": "Client not connected",
"endpoint": "Endpoint",
"endpointDesc": "IP of the client from which the WireGuard connection is established",
"search": "Search clients...",
"config": "Configuration",
"viewConfig": "View Configuration"
"search": "Search clients..."
},
"dialog": {
"change": "Change",
@@ -240,12 +238,6 @@
"preDown": "PreDown",
"postDown": "PostDown"
},
"copy": {
"notSupported": "Copy is not supported",
"copied": "Copied!",
"failed": "Copy failed",
"copy": "Copy"
},
"awg": {
"jCLabel": "Junk packet count (Jc)",
"jCDescription": "Number of junk packets to send (1-128, recommended: 4-12)",
+1 -38
View File
@@ -116,8 +116,7 @@
"dnsDesc": "DNS сервер, який використовуватимуть клієнти (перевизначає глобальну конфігурацію)",
"notConnected": "Клієнт не підключений",
"endpoint": "Кінцева точка",
"endpointDesc": "IP-адреса клієнта, з якої встановлюється з’єднання WireGuard",
"search": "Пошук клієнтів..."
"endpointDesc": "IP-адреса клієнта, з якої встановлюється з’єднання WireGuard"
},
"dialog": {
"change": "Змінити",
@@ -237,41 +236,5 @@
"postUp": "PostUp",
"preDown": "PreDown",
"postDown": "PostDown"
},
"awg": {
"jCLabel": "Кількість сміттєвих пакетів (Jc)",
"jCDescription": "Кількість сміттєвих пакетів для відправки (1–128, рекомендовано: 4–12)",
"jMinLabel": "Мінімальний розмір сміттєвого пакета (Jmin)",
"jMinDescription": "Мінімальний розмір сміттєвих пакетів (0–1279*, рекомендовано: 8, має бути < Jmax)",
"jMaxLabel": "Максимальний розмір сміттєвого пакета (Jmax)",
"jMaxDescription": "Максимальний розмір сміттєвих пакетів (1–1280*, рекомендовано: 80, має бути > Jmin)",
"s1Label": "Розмір сміттєвих даних у початковому пакеті (S1)",
"s1Description": "Розмір сміттєвих даних у початковому пакеті (0–1132 [1280* - 148 = 1132], рекомендовано: 15150, S1+56 ≠ S2)",
"s2Label": "Розмір сміттєвих даних у пакеті відповіді (S2)",
"s2Description": "Розмір сміттєвих даних у пакеті відповіді (0–1188 [1280* - 92 = 1188], рекомендовано: 15–150)",
"s3Label": "Розмір сміттєвих даних у пакеті «cookie reply» (S3)",
"s3Description": "Розмір сміттєвих даних у пакеті «cookie reply»",
"s4Label": "Розмір сміттєвих даних у транспортному пакеті (S4)",
"s4Description": "Розмір сміттєвих даних у транспортному пакеті",
"i1Label": "Спеціальний сміттєвий пакет 1 (I1)",
"i1Description": "Пакет-імітація протоколу у hex-форматі: <b 0x...>",
"i2Label": "Спеціальний сміттєвий пакет 2 (I2)",
"i2Description": "Пакет-імітація протоколу у hex-форматі: <b 0x...>",
"i3Label": "Спеціальний сміттєвий пакет 3 (I3)",
"i3Description": "Пакет-імітація протоколу у hex-форматі: <b 0x...>",
"i4Label": "Спеціальний сміттєвий пакет 4 (I4)",
"i4Description": "Пакет-імітація протоколу у hex-форматі: <b 0x...>",
"i5Label": "Спеціальний сміттєвий пакет 5 (I5)",
"i5Description": "Пакет-імітація протоколу у hex-форматі: <b 0x...>",
"h1Label": "Початковий магічний заголовок (H1)",
"h1Description": "Значення заголовка початкового пакета (5–2147483647, має бути унікальним від H2–H4)",
"h2Label": "Магічний заголовок відповіді (H2)",
"h2Description": "Значення заголовка пакета відповіді (5–2147483647, має бути унікальним від H1, H3, H4)",
"h3Label": "Магічний заголовок «cookie reply» (H3)",
"h3Description": "Значення заголовка пакета «cookie reply» (52147483647, має бути унікальним від H1, H2, H4)",
"h4Label": "Магічний заголовок транспортного пакета (H4)",
"h4Description": "Значення заголовка транспортного пакета (5–2147483647, має бути унікальним від H1–H3)",
"mtuNote": "Значення залежать від MTU",
"obfuscationParameters": "Параметри обфускації AmneziaWG"
}
}
-277
View File
@@ -1,277 +0,0 @@
{
"pages": {
"me": "帳戶",
"clients": "用戶端",
"admin": {
"panel": "管理面板",
"general": "一般設定",
"config": "組態設定",
"interface": "介面設定",
"hooks": "Hook 設定"
}
},
"user": {
"email": "電子郵件"
},
"me": {
"currentPassword": "目前密碼",
"enable2fa": "啟用兩步驟驗證",
"enable2faDesc": "請使用您的驗證碼應用程式掃描 QR Code,或手動輸入金鑰。",
"2faKey": "TOTP 金鑰",
"2faCodeDesc": "請輸入驗證碼應用程式提供的驗證碼。",
"disable2fa": "停用兩步驟驗證",
"disable2faDesc": "請輸入您的密碼以停用兩步驟驗證。"
},
"general": {
"name": "名稱",
"username": "使用者名稱",
"password": "密碼",
"newPassword": "新密碼",
"updatePassword": "更新密碼",
"mtu": "MTU",
"allowedIps": "允許的 IP",
"dns": "DNS",
"persistentKeepalive": "保持連線",
"logout": "登出",
"continue": "繼續",
"host": "主機",
"port": "連接埠",
"yes": "是",
"no": "否",
"confirmPassword": "確認密碼",
"loading": "正在載入...",
"2fa": "兩步驟驗證",
"2faCode": "TOTP 驗證碼"
},
"setup": {
"welcome": "歡迎首次設定您的 wg-easy",
"welcomeDesc": "這是您在任何 Linux 主機上安裝與管理 WireGuard 最簡單的方式",
"existingSetup": "您已有現存的設定了嗎?",
"createAdminDesc": "請先輸入管理員使用者名稱與高強度密碼。此資訊將用於登入管理面板。",
"setupConfigDesc": "請輸入主機與連接埠資訊。此資訊將用於設定用戶端的 WireGuard 連線。",
"setupMigrationDesc": "若要從先前的 wg-easy 版本移轉資料,請提供備份檔案。",
"upload": "上傳",
"migration": "還原備份:",
"createAccount": "建立帳戶",
"successful": "設定成功",
"hostDesc": "用戶端將連線的公開主機名稱",
"portDesc": "用戶端將連線的公開 UDP 連接埠,且 WireGuard 會在此監聽"
},
"update": {
"updateAvailable": "已有更新可供使用!",
"update": "更新"
},
"theme": {
"dark": "深色佈景主題",
"light": "淺色佈景主題",
"system": "系統佈景主題"
},
"layout": {
"toggleCharts": "顯示/隱藏圖表",
"donate": "贊助"
},
"login": {
"signIn": "登入",
"rememberMe": "記住我",
"rememberMeDesc": "關閉瀏覽器後仍保持登入狀態",
"insecure": "您無法在不安全的連線下登入。請使用 HTTPS。",
"2faRequired": "需要兩步驟驗證",
"2faWrong": "兩步驟驗證碼不正確"
},
"client": {
"empty": "尚無用戶端。",
"newShort": "新增",
"sort": "排序",
"create": "建立用戶端",
"created": "已建立用戶端",
"new": "新增用戶端",
"name": "名稱",
"expireDate": "到期日",
"expireDateDesc": "用戶端將被停用的日期。留白表示永久有效",
"deleteClient": "刪除用戶端",
"deleteDialog1": "您確定要刪除",
"deleteDialog2": "此動作無法復原。",
"enabled": "啟用",
"address": "位址",
"serverAllowedIps": "伺服器允許的 IP",
"otlDesc": "產生暫時性單次連結",
"permanent": "永久",
"createdOn": "建立於 ",
"lastSeen": "上次連線於 ",
"totalDownload": "總下載量: ",
"totalUpload": "總上傳量: ",
"newClient": "新增用戶端",
"disableClient": "停用用戶端",
"enableClient": "啟用用戶端",
"noPrivKey": "此用戶端沒有已知的私密金鑰,無法建立設定。",
"showQR": "顯示 QR Code",
"downloadConfig": "下載組態設定檔",
"allowedIpsDesc": "將透過 VPN 路由的 IP (會覆寫全域設定)",
"serverAllowedIpsDesc": "伺服器將路由至用戶端的 IP",
"mtuDesc": "設定 VPN 通道的最大傳輸單位 (封包大小)",
"persistentKeepaliveDesc": "Keep-alive 封包的間隔秒數。0 表示停用",
"hooks": "Hook 設定",
"hooksDescription": "Hook 設定僅適用於 wg-quick",
"hooksLeaveEmpty": "僅適用於 wg-quick,否則請保持空白",
"dnsDesc": "用戶端使用的 DNS 伺服器 (會覆寫全域設定)",
"notConnected": "用戶端未連線",
"endpoint": "端點",
"endpointDesc": "用戶端建立 WireGuard 連線的來源 IP",
"search": "搜尋用戶端..."
},
"dialog": {
"change": "變更",
"cancel": "取消",
"create": "建立"
},
"toast": {
"success": "成功",
"saved": "已儲存",
"error": "錯誤"
},
"form": {
"actions": "操作",
"save": "儲存",
"revert": "還原",
"sectionGeneral": "一般設定",
"sectionAdvanced": "進階設定",
"noItems": "沒有項目",
"nullNoItems": "沒有項目。使用全域設定",
"add": "新增"
},
"admin": {
"general": {
"sessionTimeout": "工作階段逾時",
"sessionTimeoutDesc": "「記住我」的工作階段持續時間 (秒)",
"metrics": "計量",
"metricsPassword": "密碼",
"metricsPasswordDesc": "計量端點的 Bearer 密碼 (密碼或 argon2 雜湊)",
"json": "JSON",
"jsonDesc": "提供 JSON 格式計量的路由",
"prometheus": "Prometheus",
"prometheusDesc": "提供 Prometheus 計量的路由"
},
"config": {
"connection": "連線",
"hostDesc": "用戶端將連線的公開主機名稱 (變更後會使目前組態設定檔失效)",
"portDesc": "用戶端將連線的公開 UDP 連接埠 (變更後會使目前組態設定檔失效,您可能也需要變更介面連接埠)",
"allowedIpsDesc": "用戶端將使用的允許 IP (全域設定)",
"dnsDesc": "用戶端將使用的 DNS 伺服器 (全域設定)",
"mtuDesc": "用戶端使用的 MTU (僅適用於新用戶端)",
"persistentKeepaliveDesc": "傳送 keepalive 的間隔秒數。以 0 表示停用 (僅適用於新用戶端)",
"suggest": "建議",
"suggestDesc": "為主機欄位選擇 IP 位址或主機名稱"
},
"interface": {
"cidrSuccess": "已變更 CIDR",
"device": "裝置",
"deviceDesc": "用於轉送 WireGuard 流量的乙太網路裝置",
"mtuDesc": "WireGuard 將使用的 MTU",
"portDesc": "WireGuard 監聽的 UDP 連接埠 (您可能也需要變更連接埠組態設定檔)",
"changeCidr": "變更 CIDR",
"restart": "重新啟動介面",
"restartDesc": "重新啟動 WireGuard 介面",
"restartWarn": "您確定要重新啟動介面嗎? 所有用戶端將被中斷連線。",
"restartSuccess": "介面已重新啟動"
},
"introText": "歡迎使用管理面板。\n\n您可在此管理一般、組態、介面與 Hook 設定。\n\n請從側邊欄選擇任一項目開始。"
},
"zod": {
"generic": {
"required": "{0} 為必填項目",
"validNumber": "{0} 必須為有效的數字",
"validString": "{0} 必須為有效的字串",
"validBoolean": "{0} 必須為有效的布林值",
"validArray": "{0} 必須為有效的陣列",
"stringMin": "{0} 至少需要 {1} 個字元",
"numberMin": "{0} 不能小於 {1}"
},
"client": {
"id": "用戶端 ID",
"name": "名稱",
"expiresAt": "到期時間",
"address4": "IPv4 位址",
"address6": "IPv6 位址",
"serverAllowedIps": "伺服器允許的 IP"
},
"user": {
"username": "使用者名稱",
"password": "密碼",
"remember": "記住我",
"name": "名稱",
"email": "電子郵件",
"emailInvalid": "電子郵件格式無效",
"passwordMatch": "密碼必須一致",
"totpEnable": "啟用 TOTP",
"totpEnableTrue": "必須啟用 TOTP",
"totpCode": "TOTP 驗證碼"
},
"userConfig": {
"host": "主機"
},
"general": {
"sessionTimeout": "工作階段逾時",
"metricsEnabled": "計量",
"metricsPassword": "計量密碼"
},
"interface": {
"cidr": "CIDR",
"device": "裝置",
"cidrValid": "CIDR 格式無效"
},
"otl": "單次連結",
"stringMalformed": "字串格式錯誤",
"body": "Body 必須為有效的物件",
"hook": "Hook",
"enabled": "啟用",
"mtu": "MTU",
"port": "連接埠",
"persistentKeepalive": "保持連線",
"address": "IP 位址",
"dns": "DNS",
"allowedIps": "允許的 IP",
"file": "檔案"
},
"hooks": {
"preUp": "PreUp",
"postUp": "PostUp",
"preDown": "PreDown",
"postDown": "PostDown"
},
"awg": {
"jCLabel": "填充封包數量 (Jc)",
"jCDescription": "要傳送的填充封包數量 (1-128,建議: 4-12)",
"jMinLabel": "填充封包最小大小 (Jmin)",
"jMinDescription": "填充封包的最小大小 (0-1279*,建議: 8,必須小於 Jmax)",
"jMaxLabel": "填充封包最大大小 (Jmax)",
"jMaxDescription": "填充封包的最大大小 (1-1280*,建議: 80,必須大於 Jmin)",
"s1Label": "初始封包填充大小 (S1)",
"s1Description": "初始封包填充大小 (0-1132 [1280* - 148 = 1132],建議: 15-150S1+56 ≠ S2)",
"s2Label": "回應封包填充大小 (S2)",
"s2Description": "回應封包填充大小 (0-1188 [1280* - 92 = 1188],建議: 15-150)",
"s3Label": "Cookie 回覆封包填充大小 (S3)",
"s3Description": "Cookie 回覆封包填充大小",
"s4Label": "傳輸封包填充大小 (S4)",
"s4Description": "傳輸封包填充大小",
"i1Label": "特殊填充封包 1 (I1)",
"i1Description": "協定模仿封包 (16 進位格式): <b 0x...>",
"i2Label": "特殊填充封包 2 (I2)",
"i2Description": "協定模仿封包 (16 進位格式): <b 0x...>",
"i3Label": "特殊填充封包 3 (I3)",
"i3Description": "協定模仿封包 (16 進位格式): <b 0x...>",
"i4Label": "特殊填充封包 4 (I4)",
"i4Description": "協定模仿封包 (16 進位格式): <b 0x...>",
"i5Label": "特殊填充封包 5 (I5)",
"i5Description": "協定模仿封包 (16 進位格式): <b 0x...>",
"h1Label": "初始特徵標頭 (H1)",
"h1Description": "初始封包標頭值 (5-2147483647,必須與 H2-H4 不同)",
"h2Label": "回應特徵標頭 (H2)",
"h2Description": "回應封包標頭值 (5-2147483647,必須與 H1、H3、H4 不同)",
"h3Label": "Cookie 回覆特徵標頭 (H3)",
"h3Description": "Cookie 回覆封包標頭值 (5-2147483647,必須與 H1、H2、H4 不同)",
"h4Label": "傳輸特徵標頭 (H4)",
"h4Description": "傳輸封包標頭值 (5-2147483647,必須與 H1-H3 不同)",
"mtuNote": "數值取決於 MTU",
"obfuscationParameters": "AmneziaWG 混淆參數"
}
}
-5
View File
@@ -79,11 +79,6 @@ export default defineNuxtConfig({
language: 'zh-HK',
name: '繁體中文(香港)',
},
{
code: 'zh-TW',
language: 'zh-TW',
name: '正體中文 (台灣)',
},
{
code: 'pl',
language: 'pl-PL',
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wg-easy",
"version": "15.2.0-beta.2",
"version": "15.2.0-beta.1",
"description": "The easiest way to run WireGuard VPN + Web-based Admin UI.",
"private": true,
"type": "module",
@@ -8,7 +8,6 @@ export default definePermissionEventHandler(
event,
validateZod(InterfaceCidrUpdateSchema, event)
);
await Database.interfaces.updateCidr(data);
await WireGuard.saveConfig();
return { success: true };
+34
View File
@@ -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,
},
};
});
+15 -2
View File
@@ -8,6 +8,19 @@ export default defineSetupEventHandler(2, async ({ event }) => {
await Database.users.create(username, password);
await Database.general.setSetupStep(3);
return { success: true };
// If host and port are already set by environment variables, skip step 4
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 };
});
@@ -175,26 +175,30 @@ export class ClientService {
return this.#db.transaction(async (tx) => {
const clients = await tx.query.client.findMany().execute();
const clientInterface = await tx.query.wgInterface
const _clientInterface = await tx.query.wgInterface
.findFirst({
where: eq(wgInterface.name, 'wg0'),
})
.execute();
if (!clientInterface) {
if (!_clientInterface) {
throw new Error('WireGuard interface not found');
}
const clientConfig = await tx.query.userConfig
const clientInterface = applyInterfaceOverrides(_clientInterface);
const _clientConfig = await tx.query.userConfig
.findFirst({
where: eq(userConfig.id, clientInterface.name),
})
.execute();
if (!clientConfig) {
if (!_clientConfig) {
throw new Error('WireGuard interface configuration not found');
}
const clientConfig = applyUserConfigOverrides(_clientConfig);
const ipv4Cidr = parseCidr(clientInterface.ipv4Cidr);
const ipv4Address = nextIP(4, ipv4Cidr, clients);
const ipv6Cidr = parseCidr(clientInterface.ipv6Cidr);
@@ -241,16 +245,18 @@ export class ClientService {
update(id: ID, data: UpdateClientType) {
return this.#db.transaction(async (tx) => {
const clientInterface = await tx.query.wgInterface
const _clientInterface = await tx.query.wgInterface
.findFirst({
where: eq(wgInterface.name, 'wg0'),
})
.execute();
if (!clientInterface) {
if (!_clientInterface) {
throw new Error('WireGuard interface not found');
}
const clientInterface = applyInterfaceOverrides(_clientInterface);
if (!containsCidr(clientInterface.ipv4Cidr, data.ipv4Address)) {
throw new Error('IPv4 address is not within the CIDR range');
}
@@ -272,7 +278,8 @@ export class ClientService {
privateKey,
publicKey,
}: ClientCreateFromExistingType) {
const clientConfig = await Database.userConfigs.get();
const _clientConfig = await Database.userConfigs.get();
const clientConfig = applyUserConfigOverrides(_clientConfig);
return this.#db
.insert(client)
+16 -12
View File
@@ -101,22 +101,26 @@ async function initialSetup(db: DBServiceType) {
});
}
if (
WG_INITIAL_ENV.USERNAME &&
WG_INITIAL_ENV.PASSWORD &&
WG_INITIAL_ENV.HOST &&
WG_INITIAL_ENV.PORT
) {
if (WG_INITIAL_ENV.USERNAME && WG_INITIAL_ENV.PASSWORD) {
DB_DEBUG('Creating initial user...');
await db.users.create(WG_INITIAL_ENV.USERNAME, WG_INITIAL_ENV.PASSWORD);
DB_DEBUG('Setting initial host and port...');
await db.userConfigs.updateHostPort(
WG_INITIAL_ENV.HOST,
WG_INITIAL_ENV.PORT
);
await db.general.setSetupStep(3);
}
await db.general.setSetupStep(0);
// 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...');
await db.userConfigs.updateHostPort(host, port);
// Setup completion requires USERNAME and PASSWORD (no overrides for these)
if (WG_INITIAL_ENV.USERNAME && WG_INITIAL_ENV.PASSWORD) {
await db.general.setSetupStep(0);
}
}
}
+6 -1
View File
@@ -9,12 +9,17 @@ export default defineEventHandler(async (event) => {
const { step, done } = await Database.general.getSetupStep();
if (!done) {
const parsedSetup = url.pathname.match(/\/setup\/(\d)/);
const parsedSetup = url.pathname.match(/\/setup\/(\d|migrate|success)/);
if (!parsedSetup) {
return sendRedirect(event, `/setup/1`, 302);
}
const [_, currentSetup] = parsedSetup;
// Allow access to success page during setup
if (currentSetup === 'success') {
return;
}
if (step.toString() === currentSetup) {
return;
}
+38 -32
View File
@@ -13,7 +13,10 @@ class WireGuard {
* Save and sync config
*/
async saveConfig() {
const wgInterface = await Database.interfaces.get();
const wgInterface = applyInterfaceOverrides(
await Database.interfaces.get()
);
await this.#saveWireguardConfig(wgInterface);
await this.#syncWireguardConfig(wgInterface);
}
@@ -25,7 +28,7 @@ class WireGuard {
*/
async #saveWireguardConfig(wgInterface: InterfaceType) {
const clients = await Database.clients.getAll();
const hooks = await Database.hooks.get();
const hooks = applyHooksOverrides(await Database.hooks.get());
const result = [];
result.push(
@@ -150,8 +153,12 @@ class WireGuard {
}
async getClientConfiguration({ clientId }: { clientId: ID }) {
const wgInterface = await Database.interfaces.get();
const userConfig = await Database.userConfigs.get();
const wgInterface = applyInterfaceOverrides(
await Database.interfaces.get()
);
const userConfig = applyUserConfigOverrides(
await Database.userConfigs.get()
);
const client = await Database.clients.get(clientId);
@@ -166,24 +173,11 @@ class WireGuard {
async getClientQRCodeSVG({ clientId }: { clientId: ID }) {
const config = await this.getClientConfiguration({ clientId });
const ECMode = ['high', 'quartile', 'medium', 'low'] as const;
for (const ecc of ECMode) {
try {
return encodeQR(config, 'svg', {
ecc,
scale: 2,
encoding: 'byte',
});
} catch (err) {
if (!(err instanceof Error && err.message === 'Capacity overflow')) {
throw err;
}
// retry with lower ecc
}
}
throw new Error(
'Failed to generate QR code: Capacity overflow at all ECC levels'
);
return encodeQR(config, 'svg', {
ecc: 'high',
scale: 2,
encoding: 'byte',
});
}
cleanClientFilename(name: string): string {
@@ -230,25 +224,33 @@ class WireGuard {
Database.interfaces.update(wgInterface);
}
WG_DEBUG(`Starting Wireguard Interface ${wgInterface.name}...`);
await this.#saveWireguardConfig(wgInterface);
await wg.down(wgInterface.name).catch(() => {});
await wg.up(wgInterface.name).catch((err) => {
const wgInterfaceWithOverrides = applyInterfaceOverrides(wgInterface);
WG_DEBUG(
`Starting Wireguard Interface ${wgInterfaceWithOverrides.name}...`
);
await this.#saveWireguardConfig(wgInterfaceWithOverrides);
await wg.down(wgInterfaceWithOverrides.name).catch(() => {});
await wg.up(wgInterfaceWithOverrides.name).catch((err) => {
if (
err &&
err.message &&
err.message.includes(`Cannot find device "${wgInterface.name}"`)
err.message.includes(
`Cannot find device "${wgInterfaceWithOverrides.name}"`
)
) {
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 }
);
}
throw err;
});
await this.#syncWireguardConfig(wgInterface);
WG_DEBUG(`Wireguard Interface ${wgInterface.name} started successfully.`);
await this.#syncWireguardConfig(wgInterfaceWithOverrides);
WG_DEBUG(
`Wireguard Interface ${wgInterfaceWithOverrides.name} started successfully.`
);
WG_DEBUG('Starting Cron Job...');
await this.startCronJob();
@@ -267,12 +269,16 @@ class WireGuard {
// Shutdown wireguard
async Shutdown() {
const wgInterface = await Database.interfaces.get();
const wgInterface = applyInterfaceOverrides(
await Database.interfaces.get()
);
await wg.down(wgInterface.name).catch(() => {});
}
async Restart() {
const wgInterface = await Database.interfaces.get();
const wgInterface = applyInterfaceOverrides(
await Database.interfaces.get()
);
await wg.restart(wgInterface.name);
}
+174
View File
@@ -54,6 +54,78 @@ export const WG_INITIAL_ENV = {
: 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) {
const val = process.env[env];
@@ -63,3 +135,105 @@ function assertEnv<T extends string>(env: T) {
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,
};
}
+3 -1
View File
@@ -138,7 +138,9 @@ export const defineMetricsHandler = <
handler: MetricsHandler<TReq, TRes>
) => {
return defineEventHandler(async (event) => {
const metricsConfig = await Database.general.getMetricsConfig();
const metricsConfig = applyMetricsOverrides(
await Database.general.getMetricsConfig()
);
if (metricsConfig.password) {
const auth = getHeader(event, 'Authorization');
+8 -2
View File
@@ -8,7 +8,10 @@ export type WGSession = Partial<{
const name = 'wg-easy';
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, {
password: sessionConfig.sessionPassword,
name,
@@ -22,7 +25,10 @@ export async function useWGSession(event: H3Event, rememberMe = false) {
}
export async function getWGSession(event: H3Event) {
const sessionConfig = await Database.general.getSessionConfig();
const sessionConfig = applySessionOverrides(
await Database.general.getSessionConfig()
);
return getSession<WGSession>(event, {
password: sessionConfig.sessionPassword,
name,