Compare commits

..

1 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 53867985d1 Initial plan 2026-02-12 17:48:29 +00:00
66 changed files with 1883 additions and 5553 deletions
+11 -11
View File
@@ -30,7 +30,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/wg-easy/wg-easy
@@ -38,21 +38,21 @@ jobs:
latest=false
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
- name: Build and push by digest
id: build
uses: docker/build-push-action@v7
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.arch.platform }}
@@ -69,7 +69,7 @@ jobs:
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
@@ -85,32 +85,32 @@ jobs:
needs: docker-build
steps:
- name: Download digests
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Codeberg
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: codeberg.org
username: ${{ secrets.CODEBERG_USER }}
password: ${{ secrets.CODEBERG_PASS }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/wg-easy/wg-easy
+11 -11
View File
@@ -39,7 +39,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/wg-easy/wg-easy
@@ -47,21 +47,21 @@ jobs:
latest=false
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
- name: Build and push by digest
id: build
uses: docker/build-push-action@v7
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.arch.platform }}
@@ -78,7 +78,7 @@ jobs:
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
@@ -94,32 +94,32 @@ jobs:
needs: docker-build
steps:
- name: Download digests
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Codeberg
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: codeberg.org
username: ${{ secrets.CODEBERG_USER }}
password: ${{ secrets.CODEBERG_PASS }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/wg-easy/wg-easy
+4 -4
View File
@@ -32,20 +32,20 @@ jobs:
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
uses: docker/build-push-action@v7
uses: docker/build-push-action@v6
with:
context: .
push: false
+11 -11
View File
@@ -38,7 +38,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/wg-easy/wg-easy
@@ -46,21 +46,21 @@ jobs:
latest=false
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
- name: Build and push by digest
id: build
uses: docker/build-push-action@v7
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.arch.platform }}
@@ -77,7 +77,7 @@ jobs:
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
@@ -95,32 +95,32 @@ jobs:
needs: docker-build
steps:
- name: Download digests
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Codeberg
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: codeberg.org
username: ${{ secrets.CODEBERG_USER }}
password: ${{ secrets.CODEBERG_PASS }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/wg-easy/wg-easy
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: "lts/krypton"
node-version: "lts/jod"
check-latest: true
cache: "pnpm"
@@ -57,7 +57,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: "lts/krypton"
node-version: "lts/jod"
check-latest: true
cache: "pnpm"
-12
View File
@@ -7,18 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- AWG: support for H1-H4 ranges (https://github.com/wg-easy/wg-easy/pull/2480)
- Client Firewall (https://github.com/wg-easy/wg-easy/pull/2418)
- CLI: Show QR code (https://github.com/wg-easy/wg-easy/pull/2518)
- Copy QR code to clipboard / save as png (https://github.com/wg-easy/wg-easy/pull/2521)
### Changed
- Hooks are now Textareas (https://github.com/wg-easy/wg-easy/pull/2522)
- Update to Node Krypton (24) (https://github.com/wg-easy/wg-easy/pull/2536)
## [15.2.2] - 2026-02-06
### Added
+2 -2
View File
@@ -1,4 +1,4 @@
FROM docker.io/library/node:krypton-alpine AS build
FROM docker.io/library/node:jod-alpine AS build
WORKDIR /app
# update corepack
@@ -25,7 +25,7 @@ RUN apk add linux-headers build-base go git && \
# Copy build result to a new image.
# This saves a lot of disk space.
FROM docker.io/library/node:krypton-alpine
FROM docker.io/library/node:jod-alpine
WORKDIR /app
HEALTHCHECK --interval=1m --timeout=5s --retries=3 CMD /usr/bin/timeout 5s /bin/sh -c "/usr/bin/wg show | /bin/grep -q interface || exit 1"
+1 -1
View File
@@ -1,4 +1,4 @@
FROM docker.io/library/node:krypton-alpine
FROM docker.io/library/node:jod-alpine
WORKDIR /app
# update corepack
-1
View File
@@ -33,7 +33,6 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
- IPv6 support
- CIDR support
- 2FA support
- Per-client firewall filtering (requires iptables)
> [!NOTE]
> To better manage documentation for this project, it has its own site here: [https://wg-easy.github.io/wg-easy/latest](https://wg-easy.github.io/wg-easy/latest)
-14
View File
@@ -6,20 +6,6 @@ hide:
Here are some frequently asked questions or errors about `wg-easy`. If you have a question that is not answered here, please feel free to open a discussion on GitHub.
## How do I restrict client access to specific networks or servers?
Use the **Per-Client Firewall** feature to enforce server-side restrictions on what each client can access.
**Requirements:** This feature requires `iptables` (and `ip6tables` for IPv6) to be installed on the host system.
1. Enable "Per-Client Firewall" in **Admin Panel → Interface**
2. Edit a client and configure "Firewall Allowed IPs"
3. Specify which destinations the client should be allowed to access
Unlike "Allowed IPs" which only controls client-side routing, firewall rules are enforced by the server and cannot be bypassed.
See the [Admin Panel Guide](./guides/admin.md#per-client-firewall) and [Client Guide](./guides/clients.md#firewall-allowed-ips) for detailed configuration.
## Error: WireGuard exited with the error: Cannot find device "wg0"
This error indicates that the WireGuard interface `wg0` does not exist. This can happen if the WireGuard kernel module is not loaded or if the interface was not created properly.
+1 -39
View File
@@ -2,42 +2,4 @@
title: Admin Panel
---
## Interface Settings
### Per-Client Firewall
Enable server-side firewall filtering to enforce network access restrictions per client.
When enabled, each client can have custom "Firewall Allowed IPs" configured that restrict which destinations they can access through the VPN. These restrictions are enforced by the server using iptables/ip6tables and cannot be bypassed by the client.
/// warning | Experimental Feature
This feature is currently experimental. While functional, it should be thoroughly tested in your environment before relying on it for production security requirements. Always verify that firewall rules are working as expected using test traffic or by manually inspecting the rules.
///
**Requirements:**
- `iptables` must be installed on the host system
- `ip6tables` must be installed if IPv6 is enabled (default)
- The feature cannot be enabled if these tools are not available
/// note
Most Linux distributions include iptables by default. If you're running in a minimal container environment, you may need to install the `iptables` package on the host system.
///
**Enable this feature if you want to:**
- Restrict certain clients to only access specific servers or networks
- Prevent clients from accessing the internet while allowing LAN access
- Enforce port-based restrictions (e.g., only allow HTTP/HTTPS)
- Separate routing configuration from security enforcement
**How it works:**
1. Enable "Per-Client Firewall" in Admin Panel → Interface
2. Edit any client to see the new "Firewall Allowed IPs" field
3. Specify allowed destinations (IPs, subnets, ports) for that client
4. Server enforces these rules automatically
See [Edit Client → Firewall Allowed IPs](./clients.md#firewall-allowed-ips) for detailed configuration syntax and examples.
TODO
-28
View File
@@ -41,31 +41,3 @@ docker compose exec -it wg-easy cli db:admin:reset --password <new_password>
```
This will reset the password for the admin user to the new password you provided. If you include special characters in the password, make sure to escape them properly.
### Show Clients
List all clients that are currently configured with details such as client ID, Name, Public Key, and enabled status.
```shell
cli clients:list
```
### Show Client QR Code
Display the QR code for a specific client, which can be scanned by a compatible app to import the client's configuration.
```shell
cli clients:qr <client_id>
```
Replace `<client_id>` with the actual client ID you want to show the QR code for.
/// warning | IPv6 Support
IPv6 support is enabled by default, even if you disabled it using environment variables. To disable it pass the `--no-ipv6` flag when running the CLI.
```shell
cli clients:qr <client_id> --no-ipv6
```
///
+1 -52
View File
@@ -19,58 +19,7 @@ Which IPs will be routed through the VPN.
This will not prevent the user from modifying it locally and accessing IP ranges that they should not be able to access.
Use the Firewall Allowed IPs feature to prevent access to IP ranges that the user should not be able to access.
## Firewall Allowed IPs
/// note | Attention
This field only appears when **Per-Client Firewall** is enabled in the Admin Panel → Interface settings.
///
Server-side firewall rules that restrict which destinations the client can access, regardless of their local configuration.
Unlike "Allowed IPs" which only controls routing on the client side, these rules are enforced by the server using iptables/ip6tables and cannot be bypassed by the client.
**Supported Formats:**
- `10.10.0.3`, `2001:db8::1` - Allow access to a single IP address
- `10.10.0.0/24`, `2001:db8::/32` - Allow access to an entire subnet
- `192.168.1.5:443` - Allow access to specific port (TCP+UDP)
- `192.168.1.5:443/tcp` - Allow access to specific port (TCP only)
- `192.168.1.5:443/udp` - Allow access to specific port (UDP only)
- `10.10.0.0/24:443` - Allow access to an entire subnet on a specific port (TCP+UDP)
- `10.10.0.0/24:443/tcp` - Allow access to an entire subnet on a specific port (TCP only)
- `10.10.0.0/24:443/udp` - Allow access to an entire subnet on a specific port (UDP only)
- `[2001:db8::1]:443` - IPv6 address with port (brackets required)
- `[2001:db8::/32]:443/tcp` - IPv6 CIDR with port and protocol
/// warning | Invalid Formats
Protocol specifiers (`/tcp` or `/udp`) require a port number. The following formats are **not supported** and will result in an error:
- `10.10.0.3/tcp` (use `10.10.0.3:443/tcp` instead)
- `10.10.0.0/24/udp` (use `10.10.0.0/24:53/udp` instead)
///
**Behavior:**
- **Empty**: Falls back to the client's "Allowed IPs" setting
- **Specified**: Only listed destinations are accessible (allow-only, everything else is blocked)
- **Disable for specific client**: To disable firewall filtering for a single client while keeping it enabled for others, add `0.0.0.0/0, ::/0` to allow all traffic
/// note
To allow clients to reach the VPN server itself (e.g. for DNS), include the server's VPN address in the firewall allowed IPs.
///
**Use Case Examples**:
- Allow only specific servers: `10.10.0.5`
- Allow only internal network: `10.10.0.0/24, 192.168.1.0/24`
- Allow only web browsing: `0.0.0.0/0:80, 0.0.0.0/0:443, [::/0]:80, [::/0]:443`
- Block internet, allow LAN: Leave "Allowed IPs" as `0.0.0.0/0, ::/0` but set Firewall IPs to `10.0.0.0/8, 192.168.0.0/16`
Use firewall rules to prevent access to IP ranges that the user should not be able to access.
## Server Allowed IPs
+1 -1
View File
@@ -13,5 +13,5 @@
"devDependencies": {
"prettier": "^3.8.1"
},
"packageManager": "pnpm@10.31.0"
"packageManager": "pnpm@10.29.2"
}
+1 -1
View File
@@ -1 +1 @@
setups.@nuxt/test-utils="4.0.0"
setups.@nuxt/test-utils="3.23.0"
-7
View File
@@ -1,7 +0,0 @@
:root {
color-scheme: light;
}
.dark {
color-scheme: dark;
}
-10
View File
@@ -1,10 +0,0 @@
<template>
<textarea
v-model="data"
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"
/>
</template>
<script lang="ts" setup>
const data = defineModel<string>();
</script>
+1 -98
View File
@@ -5,24 +5,10 @@
</template>
<template #description>
<div class="bg-white">
<img ref="img" :src="qrCode" />
<img :src="qrCode" />
</div>
</template>
<template #actions>
<BaseSecondaryButton
class="flex items-center gap-2"
:title="$t('client.copyPng')"
@click="copyPng"
>
<IconsCopy class="size-5" /> PNG
</BaseSecondaryButton>
<BaseSecondaryButton
class="flex items-center gap-2"
:title="$t('client.downloadPng')"
@click="downloadPng"
>
<IconsDownload class="size-5" /> PNG
</BaseSecondaryButton>
<DialogClose as-child>
<BaseSecondaryButton>{{ $t('dialog.cancel') }}</BaseSecondaryButton>
</DialogClose>
@@ -32,87 +18,4 @@
<script setup lang="ts">
defineProps<{ qrCode: string }>();
const toast = useToast();
const img = useTemplateRef('img');
async function svgToPng() {
if (!img.value || !img.value.complete || img.value.naturalWidth === 0) {
throw new Error('image is not loaded');
}
const width = 1000;
const height = 1000;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('was not able to create 2d context');
}
ctx.drawImage(img.value!, 0, 0, width, height);
return new Promise<Blob>((res, rej) => {
canvas.toBlob((blob) => {
if (!blob) {
return rej(new Error('was not able to create blob'));
}
return res(blob);
}, 'image/png');
});
}
async function downloadPng() {
try {
const blob = await svgToPng();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'client-config.png';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (e) {
console.error('failed to download png', e);
toast.showToast({
type: 'error',
message: $t('toast.unknown'),
});
}
}
async function copyPng() {
const blob = await svgToPng().catch((e) => {
console.error('failed to convert svg to png', e);
toast.showToast({
type: 'error',
message: $t('toast.unknown'),
});
});
if (!blob) {
return;
}
try {
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob,
}),
]);
toast.showToast({
type: 'success',
message: $t('copy.copied'),
});
} catch (e) {
console.error('failed to copy png', e);
toast.showToast({
type: 'error',
message: $t('copy.failed'),
});
}
}
</script>
-29
View File
@@ -1,29 +0,0 @@
<template>
<div class="flex items-center">
<FormLabel :for="id">
{{ label }}
</FormLabel>
<BaseTooltip v-if="description" :text="description">
<IconsInfo class="size-4" />
</BaseTooltip>
</div>
<BaseTextArea
:id="id"
v-model.trim="data"
:name="id"
:autocomplete="autocomplete"
:disabled="disabled"
/>
</template>
<script lang="ts" setup>
defineProps<{
id: string;
label: string;
description?: string;
autocomplete?: string;
disabled?: boolean;
}>();
const data = defineModel<string>();
</script>
+2 -2
View File
@@ -1,12 +1,12 @@
<template>
<Toggle
:pressed="globalStore.uiShowCharts"
class="group flex h-8 w-8 items-center justify-center rounded-full bg-gray-200 transition hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600"
class="group inline-flex h-8 w-8 cursor-pointer items-center justify-center whitespace-nowrap rounded-full bg-gray-200 transition hover:bg-gray-300 dark:bg-neutral-700 dark:hover:bg-neutral-600"
:title="$t('layout.toggleCharts')"
@update:pressed="globalStore.toggleCharts"
>
<IconsChart
class="h-5 w-5 transition group-data-[state=on]:fill-gray-600 dark:text-neutral-400 dark:group-data-[state=on]:fill-gray-300"
class="h-5 w-5 fill-gray-400 transition group-data-[state=on]:fill-gray-600 dark:fill-neutral-600 dark:group-data-[state=on]:fill-neutral-400"
/>
</Toggle>
</template>
-7
View File
@@ -1,7 +0,0 @@
<template>
<ClipboardDocumentIcon />
</template>
<script lang="ts" setup>
import ClipboardDocumentIcon from '@heroicons/vue/24/outline/esm/ClipboardDocumentIcon';
</script>
+4 -4
View File
@@ -2,22 +2,22 @@
<main v-if="data">
<FormElement @submit.prevent="submit">
<FormGroup>
<FormTextArea
<FormTextField
id="PreUp"
v-model="data.preUp"
:label="$t('hooks.preUp')"
/>
<FormTextArea
<FormTextField
id="PostUp"
v-model="data.postUp"
:label="$t('hooks.postUp')"
/>
<FormTextArea
<FormTextField
id="PreDown"
v-model="data.preDown"
:label="$t('hooks.preDown')"
/>
<FormTextArea
<FormTextField
id="PostDown"
v-model="data.postDown"
:label="$t('hooks.postDown')"
+24 -41
View File
@@ -69,30 +69,6 @@
:label="$t('awg.s4Label')"
:description="$t('awg.s4Description')"
/>
<FormNullTextField
id="h1"
v-model="data.h1"
:label="$t('awg.h1Label')"
:description="$t('awg.h1Description')"
/>
<FormNullTextField
id="h2"
v-model="data.h2"
:label="$t('awg.h2Label')"
:description="$t('awg.h2Description')"
/>
<FormNullTextField
id="h3"
v-model="data.h3"
:label="$t('awg.h3Label')"
:description="$t('awg.h3Description')"
/>
<FormNullTextField
id="h4"
v-model="data.h4"
:label="$t('awg.h4Label')"
:description="$t('awg.h4Description')"
/>
<FormNullTextField
id="i1"
v-model="data.i1"
@@ -123,14 +99,29 @@
:label="$t('awg.i5Label')"
:description="$t('awg.i5Description')"
/>
</FormGroup>
<FormGroup>
<FormHeading>{{ $t('admin.interface.firewall') }}</FormHeading>
<FormSwitchField
id="firewallEnabled"
v-model="data.firewallEnabled"
:label="$t('admin.interface.firewallEnabled')"
:description="$t('admin.interface.firewallEnabledDesc')"
<FormNullNumberField
id="h1"
v-model="data.h1"
:label="$t('awg.h1Label')"
:description="$t('awg.h1Description')"
/>
<FormNullNumberField
id="h2"
v-model="data.h2"
:label="$t('awg.h2Label')"
:description="$t('awg.h2Description')"
/>
<FormNullNumberField
id="h3"
v-model="data.h3"
:label="$t('awg.h3Label')"
:description="$t('awg.h3Description')"
/>
<FormNullNumberField
id="h4"
v-model="data.h4"
:label="$t('awg.h4Label')"
:description="$t('awg.h4Description')"
/>
</FormGroup>
<FormGroup>
@@ -180,15 +171,7 @@ const _submit = useSubmit(
{
method: 'post',
},
{
revert: async (success) => {
await revert();
if (success) {
// Refresh global store information after successful save
await globalStore.refreshInformation();
}
},
}
{ revert }
);
function submit() {
+4 -10
View File
@@ -61,12 +61,6 @@
name="serverAllowedIps"
/>
</FormGroup>
<FormGroup v-if="globalStore.information?.firewallEnabled">
<FormHeading :description="$t('client.firewallIpsDesc')">
{{ $t('client.firewallIps') }}
</FormHeading>
<FormNullArrayField v-model="data.firewallIps" name="firewallIps" />
</FormGroup>
<FormGroup>
<FormHeading :description="$t('client.dnsDesc')">
{{ $t('general.dns') }}
@@ -147,25 +141,25 @@
<FormHeading :description="$t('client.hooksDescription')">
{{ $t('client.hooks') }}
</FormHeading>
<FormTextArea
<FormTextField
id="PreUp"
v-model="data.preUp"
:description="$t('client.hooksLeaveEmpty')"
:label="$t('hooks.preUp')"
/>
<FormTextArea
<FormTextField
id="PostUp"
v-model="data.postUp"
:description="$t('client.hooksLeaveEmpty')"
:label="$t('hooks.postUp')"
/>
<FormTextArea
<FormTextField
id="PreDown"
v-model="data.preDown"
:description="$t('client.hooksLeaveEmpty')"
:label="$t('hooks.preDown')"
/>
<FormTextArea
<FormTextField
id="PostDown"
v-model="data.postDown"
:description="$t('client.hooksLeaveEmpty')"
+3 -7
View File
@@ -1,10 +1,7 @@
export const useGlobalStore = defineStore('Global', () => {
const { data: information, refresh: refreshInformation } = useFetch(
'/api/information',
{
method: 'get',
}
);
const { data: information } = useFetch('/api/information', {
method: 'get',
});
const sortClient = ref(true); // Sort clients by name, true = asc, false = desc
@@ -25,7 +22,6 @@ export const useGlobalStore = defineStore('Global', () => {
return {
sortClient,
information,
refreshInformation,
uiShowCharts,
toggleCharts,
uiChartType,
-71
View File
@@ -1,71 +0,0 @@
import { defineCommand } from 'citty';
import { consola } from 'consola';
import { eq } from 'drizzle-orm';
import { db, schema } from '../db';
import { hashPassword } from '../../server/utils/password';
export default defineCommand({
meta: {
name: 'db:admin:reset',
description: 'Reset the admin user password and TOTP settings',
},
args: {
password: {
type: 'string',
description: 'New password for the admin user',
required: false,
},
},
async run(ctx) {
let password = ctx.args.password || undefined;
if (!password) {
password = await consola.prompt('Please enter a new password:', {
type: 'text',
});
}
if (!password) {
consola.error('Password is required');
return;
}
if (password.length < 12) {
consola.error('Password must be at least 12 characters long');
return;
}
consola.info('Setting new password for admin user...');
const hash = await hashPassword(password);
const user = await db.transaction(async (tx) => {
const user = await tx
.select()
.from(schema.user)
.where(eq(schema.user.id, 1))
.get();
if (!user) {
consola.error('Admin user not found');
return;
}
await tx
.update(schema.user)
.set({
password: hash,
totpVerified: false,
totpKey: null,
})
.where(eq(schema.user.id, 1));
return user;
});
if (!user) {
consola.error('Failed to update admin user');
return;
}
consola.success(
`Successfully updated admin user ${user.id} (${user.username})`
);
},
});
-29
View File
@@ -1,29 +0,0 @@
import { defineCommand } from 'citty';
import { consola } from 'consola';
import { db } from '../db';
export default defineCommand({
meta: {
name: 'clients:list',
description: 'List all clients',
},
async run() {
consola.info('Listing all clients...');
const clients = await db.query.client.findMany({
columns: {
id: true,
name: true,
publicKey: true,
enabled: true,
},
});
if (clients.length === 0) {
consola.info('No clients found');
return;
}
console.table(clients);
},
});
-71
View File
@@ -1,71 +0,0 @@
import { defineCommand } from 'citty';
import { consola } from 'consola';
import { eq } from 'drizzle-orm';
import { wg } from '../../server/utils/wgHelper';
import { encodeQRCodeTerm } from '../../server/utils/qr';
import { db, schema } from '../db';
export default defineCommand({
meta: {
name: 'clients:qr',
description: 'Generate QR code for a client',
},
args: {
id: {
required: true,
type: 'positional',
},
ipv6: {
required: false,
type: 'boolean',
default: true,
},
},
async run(ctx) {
const clientId = Number(ctx.args.id);
const enableIpv6 = ctx.args.ipv6;
if (Number.isNaN(clientId)) {
consola.error('Invalid client ID');
return;
}
consola.info('Generating QR code for client...');
const wgInterface = await db.query.wgInterface.findFirst({
where: eq(schema.wgInterface.name, 'wg0'),
});
if (!wgInterface) {
consola.error('WireGuard interface not found');
return;
}
const userConfig = await db.query.userConfig.findFirst({
where: eq(schema.userConfig.id, 'wg0'),
});
if (!userConfig) {
consola.error('User config not found');
return;
}
const client = await db.query.client.findFirst({
where: eq(schema.client.id, clientId),
});
if (!client) {
consola.error(`Client with ID ${clientId} not found`);
return;
}
const clientConfig = wg.generateClientConfig(
wgInterface,
userConfig,
client,
{
enableIpv6,
}
);
consola.log(encodeQRCodeTerm(clientConfig));
},
});
-10
View File
@@ -1,10 +0,0 @@
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
import * as schema from '../server/database/schema';
//const client = createClient({ url: 'file:../data/wg-easy.db' });
const client = createClient({ url: 'file:/etc/wireguard/wg-easy.db' });
export const db = drizzle({ client, schema });
export { schema };
+74 -26
View File
@@ -1,38 +1,84 @@
#!/usr/bin/env node
import type { Resolvable, SubCommandsDef } from 'citty';
// ! Auto Imports are not supported in this file
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';
import { defineCommand, runMain } from 'citty';
import { consola } from 'consola';
import { eq } from 'drizzle-orm';
import packageJson from '../package.json';
import * as schema from '../server/database/schema';
import { hashPassword } from '../server/utils/password';
// Commands
import dbAdminReset from './admin/reset';
import clientsList from './clients/list';
import clientsQr from './clients/qr';
const subCommands = [dbAdminReset, clientsList, clientsQr] as const;
const client = createClient({ url: 'file:/etc/wireguard/wg-easy.db' });
const db = drizzle({ client, schema });
// from citty
function resolveValue<T>(input: Resolvable<T>): T | Promise<T> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return typeof input === 'function' ? (input as any)() : input;
}
async function generateSubCommands(): Promise<SubCommandsDef> {
const subCommandsMap: Record<string, SubCommandsDef[string]> = {};
for (const cmd of subCommands) {
const cmdMeta = await resolveValue(cmd.meta || {});
if (!cmdMeta.name) {
console.warn('Skipping command without name:', cmd);
continue;
const dbAdminReset = defineCommand({
meta: {
name: 'db:admin:reset',
description: 'Reset the admin user password and TOTP settings',
},
args: {
password: {
type: 'string',
description: 'New password for the admin user',
required: false,
},
},
async run(ctx) {
let password = ctx.args.password || undefined;
if (!password) {
password = await consola.prompt('Please enter a new password:', {
type: 'text',
});
}
subCommandsMap[cmdMeta.name] = cmd;
}
if (!password) {
consola.error('Password is required');
return;
}
if (password.length < 12) {
consola.error('Password must be at least 12 characters long');
return;
}
console.info('Setting new password for admin user...');
const hash = await hashPassword(password);
return subCommandsMap;
}
const user = await db.transaction(async (tx) => {
const user = await tx
.select()
.from(schema.user)
.where(eq(schema.user.id, 1))
.get();
const subCommandsMap = await generateSubCommands();
if (!user) {
consola.error('Admin user not found');
return;
}
await tx
.update(schema.user)
.set({
password: hash,
totpVerified: false,
totpKey: null,
})
.where(eq(schema.user.id, 1));
return user;
});
if (!user) {
consola.error('Failed to update admin user');
return;
}
consola.success(
`Successfully updated admin user ${user.id} (${user.username})`
);
},
});
const main = defineCommand({
meta: {
@@ -40,7 +86,9 @@ const main = defineCommand({
version: packageJson.version,
description: 'Command Line Interface',
},
subCommands: subCommandsMap,
subCommands: {
'db:admin:reset': dbAdminReset,
},
});
runMain(main);
-12
View File
@@ -1,12 +0,0 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"target": "es2024",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"moduleResolution": "bundler"
},
"include": ["./**/*.ts"]
}
-2
View File
@@ -18,7 +18,6 @@ import nl from './locales/nl.json';
import nb from './locales/nb.json';
import bg from './locales/bg.json';
import gl from './locales/gl.json';
import cs from './locales/cs.json';
export default defineI18nConfig(() => ({
legacy: false,
@@ -44,6 +43,5 @@ export default defineI18nConfig(() => ({
nb,
bg,
gl,
cs,
},
}));
+8 -8
View File
@@ -262,14 +262,6 @@
"s3Description": "Размер на junk в cookie reply пакета",
"s4Label": "Размер на junk в транспортния пакет (S4)",
"s4Description": "Размер на junk в транспортния пакет",
"h1Label": "Init magic header (H1)",
"h1Description": "Стойност на хедера в init пакета (5–2147483647, уникална спрямо H2–H4)",
"h2Label": "Response magic header (H2)",
"h2Description": "Стойност на хедера в response пакета (52147483647, уникална спрямо H1, H3, H4)",
"h3Label": "Cookie reply magic header (H3)",
"h3Description": "Стойност на хедера в cookie reply пакета (52147483647, уникална спрямо H1, H2, H4)",
"h4Label": "Transport magic header (H4)",
"h4Description": "Стойност на хедера в транспортния пакет (5–2147483647, уникална спрямо H1–H3)",
"i1Label": "Специален junk пакет 1 (I1)",
"i1Description": "Пакет за имитация на протокол в hex формат: <b 0x...>",
"i2Label": "Специален junk пакет 2 (I2)",
@@ -280,6 +272,14 @@
"i4Description": "Пакет за имитация на протокол в hex формат: <b 0x...>",
"i5Label": "Специален junk пакет 5 (I5)",
"i5Description": "Пакет за имитация на протокол в hex формат: <b 0x...>",
"h1Label": "Init magic header (H1)",
"h1Description": "Стойност на хедера в init пакета (5–2147483647, уникална спрямо H2–H4)",
"h2Label": "Response magic header (H2)",
"h2Description": "Стойност на хедера в response пакета (52147483647, уникална спрямо H1, H3, H4)",
"h3Label": "Cookie reply magic header (H3)",
"h3Description": "Стойност на хедера в cookie reply пакета (52147483647, уникална спрямо H1, H2, H4)",
"h4Label": "Transport magic header (H4)",
"h4Description": "Стойност на хедера в транспортния пакет (5–2147483647, уникална спрямо H1–H3)",
"mtuNote": "Стойностите зависят от MTU",
"obfuscationParameters": "Параметри за обфускация на AmneziaWG"
}
-287
View File
@@ -1,287 +0,0 @@
{
"pages": {
"me": "Účet",
"clients": "Klienti",
"admin": {
"panel": "Administrace",
"general": "Obecné",
"config": "Konfigurace",
"interface": "Rozhraní",
"hooks": "Nastavení reakcí"
}
},
"user": {
"email": "E-mail"
},
"me": {
"currentPassword": "Aktuální heslo",
"enable2fa": "Zapnout dvoufázové ověření (2FA)",
"enable2faDesc": "Naskenujte QR kód ve své autentizační aplikaci nebo zadejte klíč ručně.",
"2faKey": "TOTP klíč",
"2faCodeDesc": "Zadejte kód z vaší autentizační aplikace.",
"disable2fa": "Vypnout dvoufázové ověření",
"disable2faDesc": "Pro vypnutí dvoufázového ověření zadejte své heslo."
},
"general": {
"name": "Jméno",
"username": "Uživatelské jméno",
"password": "Heslo",
"newPassword": "Nové heslo",
"updatePassword": "Aktualizovat heslo",
"mtu": "MTU",
"allowedIps": "Povolené IP adresy",
"dns": "DNS",
"persistentKeepalive": "Persistent Keepalive",
"logout": "Odhlásit se",
"continue": "Pokračovat",
"host": "Hostitel",
"port": "Port",
"yes": "Ano",
"no": "Ne",
"confirmPassword": "Potvrdit heslo",
"loading": "Načítání...",
"2fa": "Dvoufázové ověření",
"2faCode": "TOTP kód"
},
"setup": {
"welcome": "Vítejte u první instalace wg-easy",
"welcomeDesc": "Našli jste nejjednodušší způsob, jak instalovat a spravovat WireGuard na jakémkoliv Linuxovém hostiteli",
"existingSetup": "Máte již existující nastavení?",
"createAdminDesc": "Nejprve zadejte uživatelské jméno administrátora a silné heslo. Tyto údaje budou použity pro přihlášení do administrace.",
"setupConfigDesc": "Zadejte údaje o hostiteli a portu. Tyto informace budou použity pro konfiguraci klientů při nastavování WireGuard na jejich zařízeních.",
"setupMigrationDesc": "Pokud chcete migrovat data z předchozí verze wg-easy do nové instalace, nahrajte soubor se zálohou.",
"upload": "Nahrát",
"migration": "Obnovit zálohu:",
"createAccount": "Vytvořit účet",
"successful": "Nastavení bylo úspěšné",
"hostDesc": "Veřejný název hostitele, ke kterému se budou klienti připojovat",
"portDesc": "Veřejný UDP port, ke kterému se budou klienti připojovat a na kterém bude WireGuard naslouchat"
},
"update": {
"updateAvailable": "Je k dispozici aktualizace!",
"update": "Aktualizovat"
},
"theme": {
"dark": "Tmavý režim",
"light": "Světlý režim",
"system": "Systémové nastavení"
},
"layout": {
"toggleCharts": "Zobrazit/skrýt grafy",
"donate": "Přispět"
},
"login": {
"signIn": "Přihlásit se",
"rememberMe": "Zapamatovat si mě",
"rememberMeDesc": "Zůstat přihlášen i po zavření prohlížeče",
"insecure": "Nemůžete se přihlásit přes nezabezpečené připojení. Použijte HTTPS.",
"2faRequired": "Je vyžadováno dvoufázové ověření",
"2faWrong": "Neplatný kód dvoufázového ověření"
},
"client": {
"empty": "Zatím zde nejsou žádní klienti.",
"newShort": "Nový",
"sort": "Seřadit",
"create": "Vytvořit klienta",
"created": "Klient vytvořen",
"new": "Nový klient",
"name": "Jméno",
"expireDate": "Datum vypršení",
"expireDateDesc": "Datum, kdy bude klient deaktivován. Ponechte prázdné pro trvalý přístup.",
"delete": "Smazat",
"deleteClient": "Smazat klienta",
"deleteDialog1": "Opravdu chcete smazat uživatele",
"deleteDialog2": "Tuto akci nelze vzít zpět.",
"enabled": "Aktivní",
"address": "Adresa",
"serverAllowedIps": "Povolené IP adresy serveru",
"otlDesc": "Generovat krátký jednorázový odkaz",
"permanent": "Trvalý",
"createdOn": "Vytvořeno dne ",
"lastSeen": "Naposledy viděn ",
"totalDownload": "Celkem staženo: ",
"totalUpload": "Celkem nahráno: ",
"newClient": "Nový klient",
"disableClient": "Deaktivovat klienta",
"enableClient": "Aktivovat klienta",
"noPrivKey": "Tento klient nemá známý soukromý klíč. Nelze vytvořit konfiguraci.",
"showQR": "Zobrazit QR kód",
"downloadConfig": "Stáhnout konfiguraci",
"allowedIpsDesc": "Které IP adresy budou směrovány přes VPN (přebíjí globální nastavení)",
"serverAllowedIpsDesc": "Které IP adresy bude server směrovat ke klientovi",
"mtuDesc": "Nastavuje maximální velikost přenášeného paketu (MTU) pro VPN tunel",
"persistentKeepaliveDesc": "Nastavuje interval (v sekundách) pro udržovací pakety. 0 pro vypnutí",
"hooks": "Hooky",
"hooksDescription": "Hooky fungují pouze s wg-quick",
"hooksLeaveEmpty": "Pouze pro wg-quick. Jinak ponechte prázdné",
"dnsDesc": "DNS server, který budou klienti používat (přebíjí globální nastavení)",
"notConnected": "Klient není připojen",
"endpoint": "Koncový bod",
"endpointDesc": "IP adresa klienta, ze které je navázáno spojení WireGuard",
"search": "Hledat klienty...",
"config": "Konfigurace",
"viewConfig": "Zobrazit konfiguraci"
},
"dialog": {
"change": "Změnit",
"cancel": "Zrušit",
"create": "Vytvořit"
},
"toast": {
"success": "Úspěch",
"saved": "Uloženo",
"error": "Chyba"
},
"form": {
"actions": "Akce",
"save": "Uložit",
"revert": "Vrátit změny",
"sectionGeneral": "Obecné",
"sectionAdvanced": "Pokročilé",
"noItems": "Žádné položky",
"nullNoItems": "Žádné položky. Používá se globální konfigurace",
"add": "Přidat"
},
"admin": {
"general": {
"sessionTimeout": "Vypršení relace",
"sessionTimeoutDesc": "Doba trvání relace pro 'Zapamatovat si mě' (sekundy)",
"metrics": "Metriky",
"metricsPassword": "Heslo",
"metricsPasswordDesc": "Bearer heslo pro koncový bod metrik (heslo nebo argon2 hash)",
"json": "JSON",
"jsonDesc": "Cesta pro metriky ve formátu JSON",
"prometheus": "Prometheus",
"prometheusDesc": "Cesta pro metriky Prometheus"
},
"config": {
"connection": "Připojení",
"hostDesc": "Veřejný název hostitele, ke kterému se klienti připojují (zneplatní stávající konfigurace)",
"portDesc": "Veřejný UDP port, ke kterému se klienti připojují (zneplatní stávající konfigurace, pravděpodobně budete chtít změnit i Port rozhraní)",
"allowedIpsDesc": "Povolené IP adresy, které budou klienti používat (globální nastavení)",
"dnsDesc": "DNS server, který budou klienti používat (globální nastavení)",
"mtuDesc": "MTU, které budou klienti používat (pouze pro nové klienty)",
"persistentKeepaliveDesc": "Interval v sekundách pro odesílání udržovacích paketů na server. 0 = vypnuto (pouze pro nové klienty)",
"suggest": "Navrhnout",
"suggestDesc": "Vyberte IP adresu nebo název hostitele pro pole Hostitel"
},
"interface": {
"cidrSuccess": "CIDR změněn",
"device": "Zařízení",
"deviceDesc": "Ethernetové zařízení, přes které má být provoz WireGuard přeposílán",
"mtuDesc": "MTU, které bude WireGuard používat",
"portDesc": "UDP port, na kterém bude WireGuard naslouchat (pravděpodobně budete chtít změnit i Konfigurační port)",
"changeCidr": "Změnit CIDR",
"restart": "Restartovat rozhraní",
"restartDesc": "Restartovat rozhraní WireGuard",
"restartWarn": "Opravdu chcete restartovat rozhraní? Dojde k odpojení všech klientů.",
"restartSuccess": "Rozhraní bylo restartováno"
},
"introText": "Vítejte v administraci.\n\nZde můžete spravovat obecná nastavení, konfiguraci, nastavení rozhraní a hooky.\n\nZačněte výběrem jedné ze sekcí v bočním panelu."
},
"zod": {
"generic": {
"required": "Pole {0} je povinné",
"validNumber": "{0} musí být platné číslo",
"validNumberRange": "{0} musí být platné číslo nebo rozsah čísel",
"validString": "{0} musí být platný řetězec",
"validBoolean": "{0} musí být platná logická hodnota",
"validArray": "{0} musí být platné pole",
"stringMin": "{0} musí mít alespoň {1} znak(ů)",
"numberMin": "{0} musí být alespoň {1}"
},
"client": {
"id": "ID klienta",
"name": "Jméno",
"expiresAt": "Vyprší dne",
"address4": "IPv4 adresa",
"address6": "IPv6 adresa",
"serverAllowedIps": "Povolené IP adresy serveru"
},
"user": {
"username": "Uživatelské jméno",
"password": "Heslo",
"remember": "Pamatovat si",
"name": "Jméno",
"email": "E-mail",
"emailInvalid": "E-mail musí být platná e-mailová adresa",
"passwordMatch": "Hesla se musí shodovat",
"totpEnable": "Zapnout TOTP",
"totpEnableTrue": "Zapnutí TOTP musí být potvrzeno",
"totpCode": "TOTP kód"
},
"userConfig": {
"host": "Hostitel"
},
"general": {
"sessionTimeout": "Vypršení relace",
"metricsEnabled": "Metriky",
"metricsPassword": "Heslo k metrikám"
},
"interface": {
"cidr": "CIDR",
"device": "Zařízení",
"cidrValid": "CIDR musí být platný"
},
"otl": "Jednorázový odkaz",
"stringMalformed": "Řetězec má nesprávný formát",
"body": "Tělo požadavku musí být platný objekt",
"hook": "Hook",
"enabled": "Aktivní",
"mtu": "MTU",
"port": "Port",
"persistentKeepalive": "Persistent Keepalive",
"address": "IP adresa",
"dns": "DNS",
"allowedIps": "Povolené IP adresy",
"file": "Soubor"
},
"hooks": {
"preUp": "PreUp",
"postUp": "PostUp",
"preDown": "PreDown",
"postDown": "PostDown"
},
"copy": {
"notSupported": "Kopírování není podporováno",
"copied": "Zkopírováno!",
"failed": "Kopírování selhalo",
"copy": "Kopírovat"
},
"awg": {
"jCLabel": "Počet junk paketů (Jc)",
"jCDescription": "Počet odesílaných junk paketů (1-128, doporučeno: 4-12)",
"jMinLabel": "Min. velikost junk paketu (Jmin)",
"jMinDescription": "Minimální velikost junk paketů (0-1279*, doporučeno: 8, musí být < Jmax)",
"jMaxLabel": "Max. velikost junk paketu (Jmax)",
"jMaxDescription": "Maximální velikost junk paketů (1-1280*, doporučeno: 80, musí být > Jmin)",
"s1Label": "Velikost junk dat u Init paketu (S1)",
"s1Description": "Velikost junk dat u Init paketu (0-1132, doporučeno: 15-150, S1+56 ≠ S2)",
"s2Label": "Velikost junk dat u Response paketu (S2)",
"s2Description": "Velikost junk dat u Response paketu (0-1188, doporučeno: 15-150)",
"s3Label": "Velikost junk dat u Cookie reply paketu (S3)",
"s3Description": "Velikost junk dat u paketu s odpovědí na cookie",
"s4Label": "Velikost junk dat u Transport paketu (S4)",
"s4Description": "Velikost junk dat u transportního paketu",
"h1Label": "Init magic header (H1)",
"h1Description": "Hodnota nebo rozsah hlavičky Init paketu (X nebo X-Y, kde X<Y. Min 5, max 2147483647. Nesmí se překrývat s ostatními hlavičkami)",
"h2Label": "Response magic header (H2)",
"h2Description": "Hodnota nebo rozsah hlavičky Response paketu (X nebo X-Y, kde X<Y. Min 5, max 2147483647. Nesmí se překrývat s ostatními hlavičkami)",
"h3Label": "Cookie reply magic header (H3)",
"h3Description": "Hodnota nebo rozsah hlavičky Cookie reply paketu (X nebo X-Y, kde X<Y. Min 5, max 2147483647. Nesmí se překrývat s ostatními hlavičkami)",
"h4Label": "Transport magic header (H4)",
"h4Description": "Hodnota nebo rozsah hlavičky Transport paketu (X nebo X-Y, kde X<Y. Min 5, max 2147483647. Nesmí se překrývat s ostatními hlavičkami)",
"i1Label": "Speciální junk paket 1 (I1)",
"i1Description": "Paket pro napodobení protokolu v hex formátu: <b 0x...>",
"i2Label": "Speciální junk paket 2 (I2)",
"i2Description": "Paket pro napodobení protokolu v hex formátu: <b 0x...>",
"i3Label": "Speciální junk paket 3 (I3)",
"i3Description": "Paket pro napodobení protokolu v hex formátu: <b 0x...>",
"i4Label": "Speciální junk paket 4 (I4)",
"i4Description": "Paket pro napodobení protokolu v hex formátu: <b 0x...>",
"i5Label": "Speciální junk paket 5 (I5)",
"i5Description": "Paket pro napodobení protokolu v hex formátu: <b 0x...>",
"mtuNote": "Hodnoty závisí na nastavení MTU",
"obfuscationParameters": "AmneziaWG parametry obfuskace"
}
}
+8 -8
View File
@@ -262,14 +262,6 @@
"s3Description": "Junk-Paketgröße des Cookie-Antwort-Pakets",
"s4Label": "Junk-Paketgröße des Transport-Pakets (S4)",
"s4Description": "Junk-Paketgröße des Transport-Pakets",
"h1Label": "Init-Magic-Header (H1)",
"h1Description": "Wert des Init-Paket-Headers (5-2147483647, muss eindeutig zu H2-H4 sein)",
"h2Label": "Antwort-Magic-Header (H2)",
"h2Description": "Wert des Antwort-Paket-Headers (5-2147483647, muss eindeutig zu H1, H3, H4 sein)",
"h3Label": "Cookie-Antwort-Magic-Header (H3)",
"h3Description": "Wert des Cookie-Antwort-Paket-Headers (5-2147483647, muss eindeutig zu H1, H2, H4 sein)",
"h4Label": "Transport-Magic-Header (H4)",
"h4Description": "Wert des Transport-Paket-Headers (5-2147483647, muss eindeutig zu H1-H3 sein)",
"i1Label": "Spezial-Junk-Paket 1 (I1)",
"i1Description": "Protokoll-Nachahmungspaket im Hex-Format: <b 0x...>",
"i2Label": "Spezial-Junk-Paket 2 (I2)",
@@ -280,6 +272,14 @@
"i4Description": "Protokoll-Nachahmungspaket im Hex-Format: <b 0x...>",
"i5Label": "Spezial-Junk-Paket 5 (I5)",
"i5Description": "Protokoll-Nachahmungspaket im Hex-Format: <b 0x...>",
"h1Label": "Init-Magic-Header (H1)",
"h1Description": "Wert des Init-Paket-Headers (5-2147483647, muss eindeutig zu H2-H4 sein)",
"h2Label": "Antwort-Magic-Header (H2)",
"h2Description": "Wert des Antwort-Paket-Headers (5-2147483647, muss eindeutig zu H1, H3, H4 sein)",
"h3Label": "Cookie-Antwort-Magic-Header (H3)",
"h3Description": "Wert des Cookie-Antwort-Paket-Headers (5-2147483647, muss eindeutig zu H1, H2, H4 sein)",
"h4Label": "Transport-Magic-Header (H4)",
"h4Description": "Wert des Transport-Paket-Headers (5-2147483647, muss eindeutig zu H1-H3 sein)",
"mtuNote": "Werte hängen von der MTU ab",
"obfuscationParameters": "AmneziaWG Verschleierungsparameter"
}
+12 -23
View File
@@ -120,11 +120,7 @@
"endpointDesc": "IP of the client from which the WireGuard connection is established",
"search": "Search clients...",
"config": "Configuration",
"viewConfig": "View Configuration",
"firewallIps": "Firewall Allowed IPs",
"firewallIpsDesc": "Destination IPs/CIDRs this client can access (server-side enforcement). Leave empty to use Allowed IPs. Supports optional port and protocol filtering. See docs for syntax.",
"downloadPng": "Download PNG",
"copyPng": "Copy PNG"
"viewConfig": "View Configuration"
},
"dialog": {
"change": "Change",
@@ -134,8 +130,7 @@
"toast": {
"success": "Success",
"saved": "Saved",
"error": "Error",
"unknown": "Unknown error. See console for more details"
"error": "Error"
},
"form": {
"actions": "Actions",
@@ -180,10 +175,7 @@
"restart": "Restart Interface",
"restartDesc": "Restart the WireGuard interface",
"restartWarn": "Are you sure to restart the interface? This will disconnect all clients.",
"restartSuccess": "Interface restarted",
"firewall": "Traffic Filtering",
"firewallEnabled": "Enable Per-Client Firewall",
"firewallEnabledDesc": "Restrict client traffic to specific destination IPs using iptables. When enabled, each client can be configured with allowed destinations."
"restartSuccess": "Interface restarted"
},
"introText": "Welcome to the admin panel.\n\nHere you can manage the general settings, the configuration, the interface settings and the hooks.\n\nStart by choosing one of the sections in the sidebar."
},
@@ -191,7 +183,6 @@
"generic": {
"required": "{0} is required",
"validNumber": "{0} must be a valid number",
"validNumberRange": "{0} must be a valid number or number range",
"validString": "{0} must be a valid string",
"validBoolean": "{0} must be a valid boolean",
"validArray": "{0} must be a valid array",
@@ -204,9 +195,7 @@
"expiresAt": "Expires At",
"address4": "IPv4 Address",
"address6": "IPv6 Address",
"serverAllowedIps": "Server Allowed IPs",
"firewallIps": "Firewall Allowed IPs",
"firewallIpsInvalid": "Invalid firewall IP entry. See docs for supported syntax."
"serverAllowedIps": "Server Allowed IPs"
},
"user": {
"username": "Username",
@@ -273,14 +262,6 @@
"s3Description": "Cookie reply packet junk size",
"s4Label": "Transport packet junk size (S4)",
"s4Description": "Transport packet junk size",
"h1Label": "Init magic header (H1)",
"h1Description": "Init packet header value or range (X or X-Y, where X<Y. Min 5, max 2147483647. Value or range must not overlap with other headers)",
"h2Label": "Response magic header (H2)",
"h2Description": "Response packet header value or range (X or X-Y, where X<Y. Min 5, max 2147483647. Value or range must not overlap with other headers)",
"h3Label": "Cookie reply magic header (H3)",
"h3Description": "Cookie reply packet header value or range (X or X-Y, where X<Y. Min 5, max 2147483647. Value or range must not overlap with other headers)",
"h4Label": "Transport magic header (H4)",
"h4Description": "Transport packet header value or range (X or X-Y, where X<Y. Min 5, max 2147483647. Value or range must not overlap with other headers)",
"i1Label": "Special junk packet 1 (I1)",
"i1Description": "Protocol mimic packet in hex format: <b 0x...>",
"i2Label": "Special junk packet 2 (I2)",
@@ -291,6 +272,14 @@
"i4Description": "Protocol mimic packet in hex format: <b 0x...>",
"i5Label": "Special junk packet 5 (I5)",
"i5Description": "Protocol mimic packet in hex format: <b 0x...>",
"h1Label": "Init magic header (H1)",
"h1Description": "Init packet header value (5-2147483647, must be unique from H2-H4)",
"h2Label": "Response magic header (H2)",
"h2Description": "Response packet header value (5-2147483647, must be unique from H1, H3, H4)",
"h3Label": "Cookie reply magic header (H3)",
"h3Description": "Cookie reply packet header value (5-2147483647, must be unique from H1, H2, H4)",
"h4Label": "Transport magic header (H4)",
"h4Description": "Transport packet header value (5-2147483647, must be unique from H1-H3)",
"mtuNote": "Values depend on the MTU",
"obfuscationParameters": "AmneziaWG Obfuscation Parameters"
}
+8 -8
View File
@@ -262,14 +262,6 @@
"s3Description": "Taille parasite du paquet de réponse cookie",
"s4Label": "Taille parasite du paquet transport (S4)",
"s4Description": "Taille parasite du paquet de transport",
"h1Label": "En-tête magique init (H1)",
"h1Description": "Valeur d'en-tête du paquet init (5-2147483647, doit être unique par rapport à H2-H4)",
"h2Label": "En-tête magique réponse (H2)",
"h2Description": "Valeur d'en-tête du paquet réponse (5-2147483647, doit être unique par rapport à H1, H3, H4)",
"h3Label": "En-tête magique cookie reply (H3)",
"h3Description": "Valeur d'en-tête du paquet cookie reply (5-2147483647, doit être unique par rapport à H1, H2, H4)",
"h4Label": "En-tête magique transport (H4)",
"h4Description": "Valeur d'en-tête du paquet transport (5-2147483647, doit être unique par rapport à H1-H3)",
"i1Label": "Paquet parasite spécial 1 (I1)",
"i1Description": "Paquet de simulation de protocole en format hexadécimal : <b 0x...>",
"i2Label": "Paquet parasite spécial 2 (I2)",
@@ -280,6 +272,14 @@
"i4Description": "Paquet de simulation de protocole en format hexadécimal : <b 0x...>",
"i5Label": "Paquet parasite spécial 5 (I5)",
"i5Description": "Paquet de simulation de protocole en format hexadécimal : <b 0x...>",
"h1Label": "En-tête magique init (H1)",
"h1Description": "Valeur d'en-tête du paquet init (5-2147483647, doit être unique par rapport à H2-H4)",
"h2Label": "En-tête magique réponse (H2)",
"h2Description": "Valeur d'en-tête du paquet réponse (5-2147483647, doit être unique par rapport à H1, H3, H4)",
"h3Label": "En-tête magique cookie reply (H3)",
"h3Description": "Valeur d'en-tête du paquet cookie reply (5-2147483647, doit être unique par rapport à H1, H2, H4)",
"h4Label": "En-tête magique transport (H4)",
"h4Description": "Valeur d'en-tête du paquet transport (5-2147483647, doit être unique par rapport à H1-H3)",
"mtuNote": "Les valeurs dépendent du MTU",
"obfuscationParameters": "Paramètres d'obfuscation AmneziaWG"
}
+8 -8
View File
@@ -262,14 +262,6 @@
"s3Description": "Cookie-svarpakke junk-størrelse",
"s4Label": "Transportpakke junk-størrelse (S4)",
"s4Description": "Transportpakke junk-størrelse",
"h1Label": "Init magisk header (H1)",
"h1Description": "Init-pakke header-verdi (5-2147483647, må være unik fra H2-H4)",
"h2Label": "Svar magisk header (H2)",
"h2Description": "Svarpakke header-verdi (5-2147483647, må være unik fra H1, H3, H4)",
"h3Label": "Cookie-svar magisk header (H3)",
"h3Description": "Cookie-svarpakke header-verdi (5-2147483647, må være unik fra H1, H2, H4)",
"h4Label": "Transport magisk header (H4)",
"h4Description": "Transportpakke header-verdi (5-2147483647, må være unik fra H1-H3)",
"i1Label": "Spesiell junk-pakke 1 (I1)",
"i1Description": "Protokolllignende pakke i heksformat: <b 0x...>",
"i2Label": "Spesiell junk-pakke 2 (I2)",
@@ -280,6 +272,14 @@
"i4Description": "Protokolllignende pakke i heksformat: <b 0x...>",
"i5Label": "Spesiell junk-pakke 5 (I5)",
"i5Description": "Protokolllignende pakke i heksformat: <b 0x...>",
"h1Label": "Init magisk header (H1)",
"h1Description": "Init-pakke header-verdi (5-2147483647, må være unik fra H2-H4)",
"h2Label": "Svar magisk header (H2)",
"h2Description": "Svarpakke header-verdi (5-2147483647, må være unik fra H1, H3, H4)",
"h3Label": "Cookie-svar magisk header (H3)",
"h3Description": "Cookie-svarpakke header-verdi (5-2147483647, må være unik fra H1, H2, H4)",
"h4Label": "Transport magisk header (H4)",
"h4Description": "Transportpakke header-verdi (5-2147483647, må være unik fra H1-H3)",
"mtuNote": "Verdier avhenger av MTU",
"obfuscationParameters": "AmneziaWG obfuskasjonsparametere"
}
+15 -26
View File
@@ -5,7 +5,7 @@
"admin": {
"panel": "Admin-paneel",
"general": "Algemeen",
"config": "Configuratie",
"config": "Config",
"interface": "Interface",
"hooks": "Hooks"
}
@@ -35,7 +35,7 @@
"logout": "Uitloggen",
"continue": "Doorgaan",
"host": "Host",
"port": "Poort",
"port": "Port",
"yes": "Ja",
"no": "Nee",
"confirmPassword": "Wachtwoord bevestigen",
@@ -120,11 +120,7 @@
"endpointDesc": "IP van de cliënt vanaf welke de WireGuard-verbinding tot stand wordt gebracht",
"search": "Cliënten zoeken...",
"config": "Configuratie",
"viewConfig": "Configuratie weergeven",
"firewallIps": "Firewall-toegestane IPs",
"firewallIpsDesc": "Bestemmings-IP's/CIDR's waartoe deze client toegang heeft (handhaving aan de serverzijde). Laat leeg om Toegestane IP's te gebruiken. Ondersteunt optionele poort - en protocolfiltering. Zie documentatie voor syntaxis.",
"downloadPng": "PNG downloaden",
"copyPng": "PNG kopiëren"
"viewConfig": "Configuratie weergeven"
},
"dialog": {
"change": "Wijzigen",
@@ -134,8 +130,7 @@
"toast": {
"success": "Succes",
"saved": "Opgeslagen",
"error": "Fout",
"unknown": "Onbekende fout. Zie console voor meer details"
"error": "Fout"
},
"form": {
"actions": "Acties",
@@ -180,18 +175,14 @@
"restart": "Interface opnieuw starten",
"restartDesc": "WireGuard-interface opnieuw starten",
"restartWarn": "Weet u zeker dat u de interface wilt herstarten? Dit zal alle cliënten loskoppelen.",
"restartSuccess": "Interface opnieuw gestart",
"firewall": "Traffic-filtering",
"firewallEnabled": "Per-Client Firewall inschakelen",
"firewallEnabledDesc": "Beperk het cliëntverkeer tot specifieke bestemmings-IP's met behulp van iptables. Indien ingeschakeld, kan elke cliënt worden geconfigureerd met toegestane bestemmingen."
"restartSuccess": "Interface opnieuw gestart"
},
"introText": "Welkom bij het Admin-paneel.\n\nHier kunt u de algemene instellingen, de configuratie, de interface-instellingen en de hooks beheren.\n\nBegin met het kiezen van een van de secties in de zijbalk."
},
"zod": {
"generic": {
"required": "{0} is vereist",
"validNumber": "{0} moet een geldig getal zijn",
"validNumberRange": "{0} moet een geldig getal of bereik zijn",
"validNumber": "{0} moet een geldig nummer zijn",
"validString": "{0} moet een geldige tekenreeks zijn",
"validBoolean": "{0} moet een geldige boolean zijn",
"validArray": "{0} moet een geldige array zijn",
@@ -204,9 +195,7 @@
"expiresAt": "Verloopt op",
"address4": "IPv4-adres",
"address6": "IPv6-adres",
"serverAllowedIps": "Toegestane IP's van de server",
"firewallIps": "Firewall-toegestane IPs",
"firewallIpsInvalid": "Ongeldige IP-invoer van firewall. Zie documentatie voor ondersteunde syntaxis."
"serverAllowedIps": "Toegestane IP's van de server"
},
"user": {
"username": "Gebruikersnaam",
@@ -273,14 +262,6 @@
"s3Description": "Grootte Cookie reply packet junk",
"s4Label": "Transport packet junk size (S4)",
"s4Description": "Grootte Transport packet junk",
"h1Label": "Init magic header (H1)",
"h1Description": "Waarde of bereik Init packet header (X of X-Y, waarbij X<Y. Min. 5, max. 2147483647. Waarde of bereik mag niet overlappen met andere headers)",
"h2Label": "Response magic header (H2)",
"h2Description": "Waarde of bereik Response packet header (X of X-Y, waarbij X<Y. Min. 5, max. 2147483647. Waarde of bereik mag niet overlappen met andere headers)",
"h3Label": "Cookie reply magic header (H3)",
"h3Description": "Waarde of bereik Cookie reply packet header (X of X-Y, waarbij X<Y. Min. 5, max. 2147483647. Waarde of bereik mag niet overlappen met andere headers)",
"h4Label": "Transport magic header (H4)",
"h4Description": "Waarde of bereik Transport packet header (X of X-Y, waarbij X<Y. Min. 5, max. 2147483647. Waarde of bereik mag niet overlappen met andere headers)",
"i1Label": "Special junk packet 1 (I1)",
"i1Description": "Protocol mimic packet in hex formaat: <b 0x...>",
"i2Label": "Special junk packet 2 (I2)",
@@ -291,6 +272,14 @@
"i4Description": "Protocol mimic packet in hex formaat: <b 0x...>",
"i5Label": "Special junk packet 5 (I5)",
"i5Description": "Protocol mimic packet in hex formaat: <b 0x...>",
"h1Label": "Init magic header (H1)",
"h1Description": "Waarde Init packet header (5-2147483647, moet uniek zijn t.o.v. H2-H4)",
"h2Label": "Response magic header (H2)",
"h2Description": "Waarde Response packet header (5-2147483647, moet uniek zijn t.o.v. H1, H3, H4)",
"h3Label": "Cookie reply magic header (H3)",
"h3Description": "Waarde Cookie reply packet header (5-2147483647, moet uniek zijn t.o.v. H1, H2, H4)",
"h4Label": "Transport magic header (H4)",
"h4Description": "Waarde Transport packet header (5-2147483647, moet uniek zijn t.o.v. H1-H3)",
"mtuNote": "Waarden zijn afhankelijk van de MTU",
"obfuscationParameters": "AmneziaWG Obfuscation Parameters"
}
+19 -30
View File
@@ -6,7 +6,7 @@
"panel": "Админ-панель",
"general": "Общие настройки",
"config": "Конфигурация",
"interface": "Сетевой интерфейс",
"interface": "Интерфейс",
"hooks": "Хуки"
}
},
@@ -110,7 +110,7 @@
"allowedIpsDesc": "Какие IP‑адреса будут маршрутизироваться через VPN (переопределяет глобальную конфигурацию)",
"serverAllowedIpsDesc": "Какие IP‑адреса сервер будет отправлять клиенту",
"mtuDesc": "Максимальный размер пакета (MTU) для VPN‑туннеля",
"persistentKeepaliveDesc": "Устанавливает интервал (в секундах) для пакетов поддержания соединения. 0 — Отключить",
"persistentKeepaliveDesc": "Устанавливает интервал (в секундах) для пакетов поддержания соединения. 0 — отключить",
"hooks": "Хуки",
"hooksDescription": "Хуки работают только с wg‑quick",
"hooksLeaveEmpty": "Только для wg‑quick. В остальных случаях оставьте пустым",
@@ -120,11 +120,7 @@
"endpointDesc": "IP‑адрес клиента, с которого установлено соединение WireGuard",
"search": "Поиск клиентов...",
"config": "Конфигурация",
"viewConfig": "Просмотреть конфигурацию",
"firewallIps": "Разрешённые фаерволом IP-адреса",
"firewallIpsDesc": "IP‑адреса/CIDR-диапазоны, к которым может получить доступ клиент (проверка на сервере). Оставьте пустым, будут использоваться «Разрешённые IP‑адреса» (AllowedIPs). Поддерживается фильтрация по портам и протоколам. Подробности см. в документации.",
"downloadPng": "Скачать PNG",
"copyPng": "Копировать PNG"
"viewConfig": "Просмотреть конфигурацию"
},
"dialog": {
"change": "Изменить",
@@ -134,8 +130,7 @@
"toast": {
"success": "Успешно",
"saved": "Сохранено",
"error": "Ошибка",
"unknown": "Неизвестная ошибка. Подробности см. в консоли браузера"
"error": "Ошибка"
},
"form": {
"actions": "Действия",
@@ -161,9 +156,9 @@
},
"config": {
"connection": "Соединение",
"hostDesc": "Публичное имя хоста для подключения клиентов (обнуляет конфигурацию)",
"hostDesc": "Публичное имя хоста для подключения клиентов(обнуляет конфигурацию)",
"portDesc": "Публичный UDP‑порт для подключения клиентов (также рекомендуется изменить порт интерфейса)",
"allowedIpsDesc": "Разрешённые IP‑адреса для клиентов (глобальная конфигурация)",
"allowedIpsDesc": "Разрешённые IP‑адреса для клиентов(глобальная конфигурация)",
"dnsDesc": "DNS‑сервер для клиентов (глобальная конфигурация)",
"mtuDesc": "MTU для клиентов (только для новых)",
"persistentKeepaliveDesc": "Интервал в секундах для отправки пакетов поддержания соединения на сервер. 0 = отключено (только для новых клиентов)",
@@ -175,15 +170,12 @@
"device": "Устройство",
"deviceDesc": "Сетевое устройство Ethernet, через которое должен проходить трафик WireGuard",
"mtuDesc": "MTU, который будет использовать WireGuard",
"portDesc": "UDP‑порт, на котором будет слушать WireGuard (возможно, также нужно изменить порт в конфигурации)",
"portDesc": "UDP‑порт, на котором будет слушать WireGuard (возможно, нужно также изменить порт конфигурации)",
"changeCidr": "Изменить CIDR",
"restart": "Перезапустить интерфейс",
"restartDesc": "Перезапустить сетевой интерфейс WireGuard",
"restartWarn": "Вы уверены, что хотите перезапустить сетевой интерфейс? Это приведёт к отключению всех клиентов.",
"restartSuccess": "Интерфейс перезапущен",
"firewall": "Фильтрация трафика",
"firewallEnabled": "Включить фаервол для отдельных клиентов",
"firewallEnabledDesc": "Ограничить трафик клиентов до определённых IP‑адресов с помощью iptables. При включении для каждого клиента можно настроить список разрешённых адресов."
"restartDesc": "Перезапустить интерфейс WireGuard",
"restartWarn": "Вы уверены, что хотите перезапустить интерфейс? Это приведёт к отключению всех клиентов.",
"restartSuccess": "Интерфейс перезапущен"
},
"introText": "Добро пожаловать в панель администратора.\n\nЗдесь вы можете управлять общими настройками, конфигурацией, настройками интерфейса и хуками.\n\nНачните с выбора одного из разделов на боковой панели."
},
@@ -191,7 +183,6 @@
"generic": {
"required": "{0} обязательно для заполнения",
"validNumber": "{0} должно быть числом",
"validNumberRange": "{0} должно быть числом или диапазоном чисел",
"validString": "{0} должно быть строкой",
"validBoolean": "{0} должно быть логическим значением",
"validArray": "{0} должно быть массивом",
@@ -204,9 +195,7 @@
"expiresAt": "Дата окончания действия",
"address4": "IPv4‑адрес",
"address6": "IPv6‑адрес",
"serverAllowedIps": "Разрешённые IP‑адреса сервера",
"firewallIps": "Разрешённые фаерволом IP-адреса",
"firewallIpsInvalid": "Некорректная запись IP-адреса для фаервола. Поддерживаемый синтаксис см. в документации"
"serverAllowedIps": "Разрешённые IP‑адреса сервера"
},
"user": {
"username": "Имя пользователя",
@@ -273,14 +262,6 @@
"s3Description": "Размер мусорных данных в cookie-reply пакете",
"s4Label": "Размер мусорных данных в транспортном пакете (S4)",
"s4Description": "Размер мусорных данных в транспортном пакете",
"h1Label": "Init magic заголовок (H1)",
"h1Description": "Значение или диапазон заголовка init-пакета (X или X-Y, где X < Y. От 5 до 2147483647. Диапазоны H1–H4 не должны пересекаться между собой)",
"h2Label": "Response magic заголовок (H2)",
"h2Description": "Значение или диапазон заголовка ответного пакета (X или X-Y, где X < Y. От 5 до 2147483647. Диапазоны H1–H4 не должны пересекаться между собой)",
"h3Label": "Cookie reply magic заголовок (H3)",
"h3Description": "Значение или диапазон заголовка cookie-reply пакета (X или X-Y, где X < Y. От 5 до 2147483647. Диапазоны H1–H4 не должны пересекаться между собой)",
"h4Label": "Transport magic заголовок (H4)",
"h4Description": "Значение или диапазон заголовка транспортного пакета (X или X-Y, где X < Y. От 5 до 2147483647. Диапазоны H1–H4 не должны пересекаться между собой)",
"i1Label": "Специальный мусорный пакет 1 (I1)",
"i1Description": "Пакет имитации протокола в hex формате: <b 0x...>",
"i2Label": "Специальный мусорный пакет 2 (I2)",
@@ -291,6 +272,14 @@
"i4Description": "Пакет имитации протокола в hex формате: <b 0x...>",
"i5Label": "Специальный мусорный пакет 5 (I5)",
"i5Description": "Пакет имитации протокола в hex формате: <b 0x...>",
"h1Label": "Init magic заголовок (H1)",
"h1Description": "Значение заголовка init-пакета (5-2147483647, должно отличаться от H2-H4)",
"h2Label": "Response magic заголовок (H2)",
"h2Description": "Значение заголовка ответного пакета (5-2147483647, должно отличаться от H1, H3, H4)",
"h3Label": "Cookie reply magic заголовок (H3)",
"h3Description": "Значение заголовка cookie-reply пакета (5-2147483647, должно отличаться от H1, H2, H4)",
"h4Label": "Transport magic заголовок (H4)",
"h4Description": "Значение заголовка транспортного пакета (5-2147483647, должно отличаться от H1-H3)",
"mtuNote": "Значения зависят от MTU",
"obfuscationParameters": "Параметры обфускации AmneziaWG"
}
+103 -160
View File
@@ -7,7 +7,7 @@
"general": "Genel",
"config": "Yapılandırma",
"interface": "Arayüz",
"hooks": "Hooks"
"hooks": "Hook'lar"
}
},
"user": {
@@ -15,56 +15,56 @@
},
"me": {
"currentPassword": "Mevcut Şifre",
"enable2fa": "2FA'yı Etkinleştir",
"enable2faDesc": "Kimlik doğrulama uygulamanızla QR kodunu tarayın veya anahtarı manuel olarak girin.",
"enable2fa": "İki Faktörlü Kimlik Doğrulamayı Etkinleştir",
"enable2faDesc": "QR kodunu kimlik doğrulayıcı uygulamanızla tarayın veya anahtarı manuel olarak girin.",
"2faKey": "TOTP Anahtarı",
"2faCodeDesc": "Uygulamanızdaki doğrulama kodunu girin.",
"disable2fa": "2FA'yı Devre Dışı Bırak",
"disable2faDesc": "2FA'yı kapatmak için şifrenizi girin."
"2faCodeDesc": "Kimlik doğrulayıcı uygulamanızdan kodu girin.",
"disable2fa": "İki Faktörlü Kimlik Doğrulamayı Devre Dışı Bırak",
"disable2faDesc": "İki Faktörlü Kimlik Doğrulamayı devre dışı bırakmak için şifrenizi girin."
},
"general": {
"name": "İsim",
"name": "Ad",
"username": "Kullanıcı Adı",
"password": "Şifre",
"newPassword": "Yeni Şifre",
"updatePassword": "Şifreyi Güncelle",
"mtu": "MTU",
"allowedIps": "Allowed IPs",
"allowedIps": "İzin Verilen IP'ler",
"dns": "DNS",
"persistentKeepalive": "Persistent Keepalive",
"persistentKeepalive": "Kalıcı Keepalive",
"logout": "Çıkış Yap",
"continue": "Devam Et",
"host": "Host",
"host": "Ana Bilgisayar",
"port": "Port",
"yes": "Evet",
"no": "Hayır",
"confirmPassword": "Şifreyi Onayla",
"loading": "Yükleniyor...",
"2fa": "2FA (İki Faktörlü Doğrulama)",
"2fa": "İki Faktörlü Kimlik Doğrulama",
"2faCode": "TOTP Kodu"
},
"setup": {
"welcome": "wg-easy kurulumuna hoş geldiniz",
"welcomeDesc": "Linux üzerinde WireGuard kurmanın ve yönetmenin en kolay yolu.",
"welcome": "wg-easy ilk kurulumunuza hoş geldiniz",
"welcomeDesc": "Herhangi bir Linux ana bilgisayarda WireGuard kurmanın ve yönetmenin en kolay yolunu buldunuz",
"existingSetup": "Mevcut bir kurulumunuz var mı?",
"createAdminDesc": "Yönetim paneline giriş için bir kullanıcı adı ve güçlü bir şifre belirleyin.",
"setupConfigDesc": "İstemci cihazların bağlanacağı Host ve Port bilgilerini girin.",
"setupMigrationDesc": "Eski wg-easy verilerinizi taşımak için yedek dosyasını yükleyebilirsiniz.",
"createAdminDesc": "Lütfen önce bir yönetici kullanıcı adı ve güçlü bir güvenli şifre girin. Bu bilgiler yönetim panelinize giriş yapmak için kullanılacaktır.",
"setupConfigDesc": "Lütfen ana bilgisayar ve port bilgilerini girin. Bu, cihazlarında WireGuard kurulumu yaparken istemci yapılandırması için kullanılacaktır.",
"setupMigrationDesc": "Verilerinizi önceki wg-easy sürümünüzden yeni kurulumunuza taşımak istiyorsanız yedekleme dosyasını sağlayın.",
"upload": "Yükle",
"migration": "Yedeği Geri Yükle:",
"migration": "Yedeği geri yükle:",
"createAccount": "Hesap Oluştur",
"successful": "Kurulum Başarılı",
"hostDesc": "İstemcilerin bağlanacağı public hostname/IP",
"portDesc": "WireGuard'ın dinleyeceği public UDP portu"
"successful": "Kurulum başarılı",
"hostDesc": "İstemcilerin bağlanacağı genel ana bilgisayar adı",
"portDesc": "İstemcilerin bağlanacağı ve WireGuard'ın dinleyeceği genel UDP portu"
},
"update": {
"updateAvailable": "Yeni bir güncelleme mevcut!",
"updateAvailable": "Güncelleme mevcut!",
"update": "Güncelle"
},
"theme": {
"dark": "Koyu Tema",
"light": "Açık Tema",
"system": "Sistem Teması"
"dark": "Koyu tema",
"light": "Açık tema",
"system": "Sistem teması"
},
"layout": {
"toggleCharts": "Grafikleri Göster/Gizle",
@@ -73,58 +73,50 @@
"login": {
"signIn": "Giriş Yap",
"rememberMe": "Beni hatırla",
"rememberMeDesc": "Oturumu açık tut",
"insecure": "Güvensiz bağlantı üzerinden giriş yapılamaz. Lütfen HTTPS kullanın.",
"2faRequired": "2FA Doğrulaması Gerekli",
"2faWrong": "Hatalı 2FA Kodu"
"rememberMeDesc": "Tarayıcıyı kapattıktan sonra giriş yapmış olarak kal",
"insecure": "Güvensiz bir bağlantı ile giriş yapamazsınız. HTTPS kullanın.",
"2faRequired": "İki Faktörlü Kimlik Doğrulama gerekli",
"2faWrong": "İki Faktörlü Kimlik Doğrulama yanlış"
},
"client": {
"empty": "Henüz kayıtlı bir istemci yok.",
"empty": "Henüz istemci yok.",
"newShort": "Yeni",
"sort": "Sırala",
"create": "İstemci Oluştur",
"created": "İstemci oluşturuldu",
"new": "Yeni İstemci",
"name": "İsim",
"name": "Ad",
"expireDate": "Son Kullanma Tarihi",
"expireDateDesc": "Boş bırakılırsa süresiz olur.",
"delete": "Sil",
"expireDateDesc": "İstemcinin devre dışı bırakılacağı tarih. Kalıcı için boş bırakın",
"deleteClient": "İstemciyi Sil",
"deleteDialog1": "Silmek istediğinize emin misiniz",
"deleteDialog2": "Bu lem geri alınamaz.",
"enabled": "Aktif",
"deleteDialog1": "Silmek istediğinizden emin misiniz",
"deleteDialog2": "Bu eylem geri alınamaz.",
"enabled": "Etkin",
"address": "Adres",
"serverAllowedIps": "Server Allowed IPs",
"otlDesc": "Tek seferlik kısa link oluştur",
"permanent": "Süresiz",
"createdOn": "Oluşturulma: ",
"lastSeen": "Son Görülme: ",
"serverAllowedIps": "Sunucu İzin Verilen IP'ler",
"otlDesc": "Kısa tek seferlik bağlantı oluştur",
"permanent": "Kalıcı",
"createdOn": "Oluşturulma tarihi ",
"lastSeen": "Son görülme ",
"totalDownload": "Toplam İndirme: ",
"totalUpload": "Toplam Yükleme: ",
"newClient": "Yeni İstemci",
"disableClient": "İstemciyi Devre Dışı Bırak",
"enableClient": "İstemciyi Etkinleştir",
"noPrivKey": "Özel anahtar (private key) bulunamadı. Yapılandırma oluşturulamaz.",
"noPrivKey": "Bu istemcinin bilinen özel anahtarı yok. Yapılandırma oluşturulamıyor.",
"showQR": "QR Kodunu Göster",
"downloadConfig": "Config İndir",
"allowedIpsDesc": "VPN üzerinden yönlendirilecek IP'ler (global ayarı ezer)",
"downloadConfig": "Yapılandırmayı İndir",
"allowedIpsDesc": "Hangi IP'lerin VPN üzerinden yönlendirileceği (genel yapılandırmayı geçersiz kılar)",
"serverAllowedIpsDesc": "Sunucunun istemciye yönlendireceği IP'ler",
"mtuDesc": "VPN tüneli için paket boyutu (MTU)",
"persistentKeepaliveDesc": "Bağlantıyı ayakta tutma aralığı (saniye). 0 devre dışı bırakır.",
"hooks": "Hooks",
"hooksDescription": "Hooks sadece wg-quick ile çalışır",
"hooksLeaveEmpty": "Sadece wg-quick içindir. Aksi halde boş bırakın.",
"dnsDesc": "İstemci DNS sunucuları (global ayarı ezer)",
"notConnected": "Bağlı Değil",
"endpoint": "Endpoint",
"endpointDesc": "İstemcinin WireGuard bağlantısı kurduğu IP adresi",
"search": "İstemci ara...",
"config": "Config",
"viewConfig": "Config Görüntüle",
"firewallIps": "Firewall Allowed IPs",
"firewallIpsDesc": "İstemcinin erişebileceği hedef IP/CIDR'ler (isteğe bağlı port/protokol filtreleme ile). Boş bırakılırsa Allowed IPs kullanılır. Ayrıntılı söz dizimi için dokümantasyona bakın.",
"downloadPng": "PNG İndir",
"copyPng": "PNG Kopyala"
"mtuDesc": "VPN tüneli için maksimum iletim birimini (paket boyutu) ayarlar",
"persistentKeepaliveDesc": "Keepalive paketleri için aralığı (saniye cinsinden) ayarlar. 0 devre dışı bırakır",
"hooks": "Hook'lar",
"hooksDescription": "Hook'lar sadece wg-quick ile çalışır",
"hooksLeaveEmpty": "Sadece wg-quick için. Aksi takdirde boş bırakın",
"dnsDesc": "İstemcilerin kullanacağı DNS sunucusu (genel yapılandırmayı geçersiz kılar)",
"notConnected": "İstemci bağlı değil",
"endpoint": "Uç Nokta",
"endpointDesc": "WireGuard bağlantısının kurulduğu istemcinin IP'si"
},
"dialog": {
"change": "Değiştir",
@@ -134,116 +126,109 @@
"toast": {
"success": "Başarılı",
"saved": "Kaydedildi",
"error": "Hata",
"unknown": "Bilinmeyen hata. Detaylar için konsola bakın."
"error": "Hata"
},
"form": {
"actions": "İşlemler",
"actions": "Eylemler",
"save": "Kaydet",
"revert": "Geri Al",
"sectionGeneral": "Genel",
"sectionAdvanced": "Gelişmiş",
"noItems": ge yok",
"nullNoItems": ge yok. Global ayarlar kullanılıyor.",
"noItems": ğe yok",
"nullNoItems": ğe yok. Genel yapılandırma kullanılıyor",
"add": "Ekle"
},
"admin": {
"general": {
"sessionTimeout": "Oturum Zaman Aşımı",
"sessionTimeoutDesc": "Beni Hatırla süresi (saniye)",
"sessionTimeoutDesc": "Beni Hatırla için oturum süresi (saniye)",
"metrics": "Metrikler",
"metricsPassword": "Şifre",
"metricsPasswordDesc": "Metrics endpoint'i için Bearer şifresi (argon2 hash destekler)",
"metricsPasswordDesc": "Metrik uç noktası için Bearer şifresi (şifre veya argon2 hash)",
"json": "JSON",
"jsonDesc": "JSON metrik rotası",
"jsonDesc": "JSON formatında metrikler için rota",
"prometheus": "Prometheus",
"prometheusDesc": "Prometheus metrik rotası"
"prometheusDesc": "Prometheus metrikleri için rota"
},
"config": {
"connection": "Bağlantı",
"hostDesc": "İstemcilerin bağlanacağı Host (configleri etkiler)",
"portDesc": "İstemcilerin bağlanacağı UDP portu. Bunu değiştirmek mevcut istemci yapılandırmalarını geçersiz kılabilir ve WireGuard Arayüz Portu ile eşleşmelidir.",
"allowedIpsDesc": "Genel Allowed IPs (izin verilen IP'ler)",
"dnsDesc": "Global DNS",
"mtuDesc": "Varsayılan MTU (yeni istemciler için)",
"persistentKeepaliveDesc": "Varsayılan Keepalive (yeni istemciler için)",
"hostDesc": "İstemcilerin bağlanacağı genel ana bilgisayar adı (yapılandırmayı geçersiz kılar)",
"portDesc": "İstemcilerin bağlanacağı genel UDP portu (yapılandırmayı geçersiz kılar, muhtemelen Arayüz Portunu da değiştirmek isteyeceksiniz)",
"allowedIpsDesc": "İstemcilerin kullanacağı İzin Verilen IP'ler (genel yapılandırma)",
"dnsDesc": "İstemcilerin kullanacağı DNS sunucusu (genel yapılandırma)",
"mtuDesc": "İstemcilerin kullanacağı MTU (sadece yeni istemciler için)",
"persistentKeepaliveDesc": "Sunucuya keepalive göndermek için saniye cinsinden aralık. 0 = devre dışı (sadece yeni istemciler için)",
"suggest": "Öner",
"suggestDesc": "Host alanı için bir IP veya Hostname öner"
"suggestDesc": "Ana Bilgisayar alanı için bir IP Adresi veya Ana Bilgisayar Adı seçin"
},
"interface": {
"cidrSuccess": "CIDR güncellendi",
"device": "Arayüz",
"deviceDesc": "Trafiğin yönlendirileceği ağ arayüzü (ethernet)",
"mtuDesc": "WireGuard arayüz MTU'su",
"portDesc": "WireGuard dinleme portu",
"changeCidr": "CIDR Değiştir",
"cidrSuccess": "CIDR değiştirildi",
"device": "Cihaz",
"deviceDesc": "WireGuard trafiğinin yönlendirileceği Ethernet cihazı",
"mtuDesc": "WireGuard'ın kullanacağı MTU",
"portDesc": "WireGuard'ın dinleyeceği UDP Portu (muhtemelen Yapılandırma Portunu da değiştirmek isteyeceksiniz)",
"changeCidr": "CIDR'ı Değiştir",
"restart": "Arayüzü Yeniden Başlat",
"restartDesc": "WireGuard arayüzünü resetler",
"restartWarn": "Arayüzü yeniden başlatmak tüm istemci bağlantılarını koparacaktır. Emin misiniz?",
"restartSuccess": "Arayüz yeniden başlatıldı",
"firewall": "Trafik Filtreleme",
"firewallEnabled": "İstemci Bazlı Firewall",
"firewallEnabledDesc": "iptables kullanarak istemci trafiğini kısıtlayın."
"restartDesc": "WireGuard arayüzünü yeniden başlat",
"restartWarn": "Arayüzü yeniden başlatmak istediğinizden emin misiniz? Bu tüm istemcilerin bağlantısını kesecektir.",
"restartSuccess": "Arayüz yeniden başlatıldı"
},
"introText": "Yönetici paneline hoş geldiniz.\n\nBuradan sistem ayarlarını, WireGuard yapılandırmasını ve Hooks ayarlarını yönetebilirsiniz."
"introText": "Yönetici paneline hoş geldiniz.\n\nBurada genel ayarları, yapılandırmayı, arayüz ayarlarını ve hook'ları yönetebilirsiniz.\n\nKenar çubuğundaki bölümlerden birini seçerek başlayın."
},
"zod": {
"generic": {
"required": "{0} alanı zorunludur",
"validNumber": "{0} geçerli bir sayı olmalıdır",
"validNumberRange": "{0} geçerli bir sayı veya aralık olmalıdır",
"validString": "{0} geçerli bir metin olmalıdır",
"validBoolean": "{0} geçerli bir boolean olmalıdır",
"validArray": "{0} geçerli bir dizi olmalıdır",
"stringMin": "{0} en az {1} karakter olmalıdır",
"numberMin": "{0} en az {1} olmalıdır"
"required": "{0} gerekli",
"validNumber": "{0} geçerli bir sayı olmalı",
"validString": "{0} geçerli bir dize olmalı",
"validBoolean": "{0} geçerli bir boolean olmalı",
"validArray": "{0} geçerli bir dizi olmalı",
"stringMin": "{0} en az {1} karakter olmalı",
"numberMin": "{0} en az {1} olmalı"
},
"client": {
"id": "Client ID",
"name": "İsim",
"expiresAt": "Son Kullanma",
"id": "İstemci ID",
"name": "Ad",
"expiresAt": "Son Kullanma Tarihi",
"address4": "IPv4 Adresi",
"address6": "IPv6 Adresi",
"serverAllowedIps": "Server Allowed IPs",
"firewallIps": "Firewall Allowed IPs",
"firewallIpsInvalid": "Geçersiz Firewall IP formatı. Desteklenen söz dizimi için dokümantasyona bakın. See docs for supported syntax."
"serverAllowedIps": "Sunucu İzin Verilen IP'ler"
},
"user": {
"username": "Kullanıcı Adı",
"password": "Şifre",
"remember": "Hatırla",
"name": "İsim",
"name": "Ad",
"email": "E-posta",
"emailInvalid": "Geçersiz e-posta formatı",
"passwordMatch": "Şifreler eşleşmiyor",
"totpEnable": "2FA Aktif",
"totpEnableTrue": "2FA Aktif edilmelidir",
"totpCode": "2FA Kodu"
"emailInvalid": "E-posta geçerli bir e-posta olmalı",
"passwordMatch": "Şifreler eşleşmeli",
"totpEnable": "TOTP Etkinleştir",
"totpEnableTrue": "TOTP Etkinleştir doğru olmalı",
"totpCode": "TOTP Kodu"
},
"userConfig": {
"host": "Host"
"host": "Ana Bilgisayar"
},
"general": {
"sessionTimeout": "Zaman Aşımı",
"sessionTimeout": "Oturum Zaman Aşımı",
"metricsEnabled": "Metrikler",
"metricsPassword": "Metrik Şifresi"
},
"interface": {
"cidr": "CIDR",
"device": "Aygıt",
"cidrValid": "Geçersiz CIDR"
"device": "Cihaz",
"cidrValid": "CIDR geçerli olmalı"
},
"otl": "OTL (One Time Link)",
"stringMalformed": "Hatalı format",
"body": "Body geçerli bir obje olmalıdır",
"otl": "Tek seferlik bağlantı",
"stringMalformed": "Dize hatalı biçimlendirilmiş",
"body": "Gövde geçerli bir nesne olmalı",
"hook": "Hook",
"enabled": "Aktif",
"enabled": "Etkin",
"mtu": "MTU",
"port": "Port",
"persistentKeepalive": "Keepalive",
"persistentKeepalive": "Kalıcı Keepalive",
"address": "IP Adresi",
"dns": "DNS",
"allowedIps": "Allowed IPs",
"allowedIps": "İzin Verilen IP'ler",
"file": "Dosya"
},
"hooks": {
@@ -251,47 +236,5 @@
"postUp": "PostUp",
"preDown": "PreDown",
"postDown": "PostDown"
},
"copy": {
"notSupported": "Kopyalama desteklenmiyor",
"copied": "Kopyalandı!",
"failed": "Kopyalama başarısız",
"copy": "Kopyala"
},
"awg": {
"jCLabel": "Junk paket sayısı (Jc)",
"jCDescription": "Gönderilecek sahte paket sayısı (1-128)",
"jMinLabel": "Junk min boyutu (Jmin)",
"jMinDescription": "Sahte paketlerin minimum boyutu (Jmin < Jmax, MTU ile sınırlı)",
"jMaxLabel": "Junk maks boyutu (Jmax)",
"jMaxDescription": "Sahte paketlerin maksimum boyutu (Jmax > Jmin, MTU ile sınırlı)",
"s1Label": "Init junk boyutu (S1)",
"s1Description": "Başlangıç paketi junk boyutu",
"s2Label": "Response junk boyutu (S2)",
"s2Description": "Yanıt paketi junk boyutu",
"s3Label": "Cookie junk boyutu (S3)",
"s3Description": "Cookie reply junk boyutu",
"s4Label": "Transport junk boyutu (S4)",
"s4Description": "Transport junk boyutu",
"h1Label": "Init magic header (H1)",
"h1Description": "Init paket başlık değeri",
"h2Label": "Response magic header (H2)",
"h2Description": "Response paket başlık değeri",
"h3Label": "Cookie magic header (H3)",
"h3Description": "Cookie reply başlık değeri",
"h4Label": "Transport magic header (H4)",
"h4Description": "Transport paket başlık değeri",
"i1Label": "Özel junk 1 (I1)",
"i1Description": "Hex formatında protocol mimic paketi",
"i2Label": "Özel junk 2 (I2)",
"i2Description": "Hex formatında protocol mimic paketi",
"i3Label": "Özel junk 3 (I3)",
"i3Description": "Hex formatında protocol mimic paketi",
"i4Label": "Özel junk 4 (I4)",
"i4Description": "Hex formatında protocol mimic paketi",
"i5Label": "Özel junk 5 (I5)",
"i5Description": "Hex formatında protocol mimic paketi",
"mtuNote": "Değerler MTU'ya bağlıdır",
"obfuscationParameters": "AmneziaWG Obfuscation Ayarları"
}
}
+8 -8
View File
@@ -262,14 +262,6 @@
"s3Description": "Розмір сміттєвих даних у пакеті «cookie reply»",
"s4Label": "Розмір сміттєвих даних у транспортному пакеті (S4)",
"s4Description": "Розмір сміттєвих даних у транспортному пакеті",
"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)",
"i1Label": "Спеціальний сміттєвий пакет 1 (I1)",
"i1Description": "Пакет-імітація протоколу у hex-форматі: <b 0x...>",
"i2Label": "Спеціальний сміттєвий пакет 2 (I2)",
@@ -280,6 +272,14 @@
"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"
}
+8 -8
View File
@@ -262,14 +262,6 @@
"s3Description": "Cookie 回复数据包中垃圾数据的大小",
"s4Label": "传输数据包垃圾数据大小(S4)",
"s4Description": "传输数据包中垃圾数据的大小",
"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 不同)",
"i1Label": "特殊垃圾数据包 1I1",
"i1Description": "协议模拟数据包(十六进制格式):<b 0x...>",
"i2Label": "特殊垃圾数据包 2I2",
@@ -280,6 +272,14 @@
"i4Description": "协议模拟数据包(十六进制格式):<b 0x...>",
"i5Label": "特殊垃圾数据包 5I5",
"i5Description": "协议模拟数据包(十六进制格式):<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 混淆参数"
}
+8 -8
View File
@@ -262,14 +262,6 @@
"s3Description": "Cookie 回覆封包填充大小",
"s4Label": "傳輸封包填充大小 (S4)",
"s4Description": "傳輸封包填充大小",
"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 不同)",
"i1Label": "特殊填充封包 1 (I1)",
"i1Description": "協定模仿封包 (16 進位格式): <b 0x...>",
"i2Label": "特殊填充封包 2 (I2)",
@@ -280,6 +272,14 @@
"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 混淆參數"
}
-6
View File
@@ -23,7 +23,6 @@ export default defineNuxtConfig({
classSuffix: '',
cookieName: 'theme',
},
css: ['~/app.css'],
i18n: {
// https://i18n.nuxtjs.org/docs/guide/server-side-translations
experimental: {
@@ -91,11 +90,6 @@ export default defineNuxtConfig({
language: 'pl-PL',
name: 'Polski',
},
{
code: 'cs',
language: 'cs-CZ',
name: 'Čeština',
},
{
code: 'pt-BR',
language: 'pt-BR',
+16 -16
View File
@@ -1,6 +1,6 @@
{
"name": "wg-easy",
"version": "15.3.0-beta.1",
"version": "15.2.2",
"description": "The easiest way to run WireGuard VPN + Web-based Admin UI.",
"private": true,
"type": "module",
@@ -29,50 +29,50 @@
"@phc/format": "^1.0.0",
"@pinia/nuxt": "^0.11.3",
"@tailwindcss/forms": "^0.5.11",
"@vueuse/core": "^14.2.1",
"@vueuse/nuxt": "^14.2.1",
"apexcharts": "^5.10.3",
"@vueuse/core": "^14.2.0",
"@vueuse/nuxt": "^14.2.0",
"apexcharts": "^5.3.6",
"argon2": "^0.44.0",
"cidr-tools": "^11.2.0",
"citty": "^0.2.1",
"cidr-tools": "^11.0.6",
"citty": "^0.2.0",
"consola": "^3.4.2",
"crc-32": "^1.2.2",
"debug": "^4.4.3",
"drizzle-orm": "^0.45.1",
"ip-bigint": "^8.2.10",
"is-cidr": "^6.0.3",
"ip-bigint": "^8.2.4",
"is-cidr": "^6.0.2",
"is-ip": "^5.0.1",
"js-sha256": "^0.11.1",
"nuxt": "^3.21.1",
"otpauth": "^9.5.0",
"pinia": "^3.0.4",
"qr": "^0.5.5",
"qr": "^0.5.4",
"radix-vue": "^1.9.17",
"semver": "^7.7.4",
"tailwindcss": "^3.4.19",
"timeago.js": "^4.0.2",
"vue": "latest",
"vue3-apexcharts": "^1.11.1",
"vue3-apexcharts": "^1.10.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@nuxt/eslint": "^1.15.2",
"@nuxt/test-utils": "^4.0.0",
"@nuxt/eslint": "^1.14.0",
"@nuxt/test-utils": "^3.23.0",
"@types/debug": "^4.1.12",
"@types/phc__format": "^1.0.1",
"@types/semver": "^7.7.1",
"@vitest/coverage-v8": "^4.0.18",
"@vitest/ui": "4.0.18",
"drizzle-kit": "^0.31.9",
"drizzle-kit": "^0.31.8",
"esbuild": "^0.27.3",
"eslint": "^9.39.4",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vitest": "^4.0.18",
"vue-tsc": "^3.2.5"
"vue-tsc": "^3.2.4"
},
"packageManager": "pnpm@10.31.0"
"packageManager": "pnpm@10.29.2"
}
+1457 -1401
View File
File diff suppressed because it is too large Load Diff
@@ -8,26 +8,6 @@ export default definePermissionEventHandler(
event,
validateZod(InterfaceUpdateSchema, event)
);
// If enabling firewall, check if iptables is available
if (data.firewallEnabled) {
// Clear cache to force fresh check
firewall.clearAvailabilityCache();
const iptablesAvailable = await firewall.isAvailable(
!WG_ENV.DISABLE_IPV6
);
if (!iptablesAvailable) {
const requiredTools = WG_ENV.DISABLE_IPV6
? 'iptables'
: 'iptables and ip6tables';
throw createError({
statusCode: 400,
statusMessage: `Per-Client Firewall requires ${requiredTools} to be installed on the host system. Please install ${requiredTools} before enabling this feature.`,
});
}
}
await Database.interfaces.update(data);
await WireGuard.saveConfig();
return { success: true };
-2
View File
@@ -5,7 +5,6 @@ export default defineEventHandler(async () => {
const updateAvailable = gt(latestRelease.version, RELEASE);
const insecure = WG_ENV.INSECURE;
const isAwg = WG_ENV.WG_EXECUTABLE === 'awg';
const wgInterface = await Database.interfaces.get();
return {
currentRelease: RELEASE,
@@ -13,6 +12,5 @@ export default defineEventHandler(async () => {
updateAvailable,
insecure,
isAwg,
firewallEnabled: wgInterface.firewallEnabled,
};
});
@@ -1,36 +0,0 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_interfaces_table` (
`name` text PRIMARY KEY NOT NULL,
`device` text NOT NULL,
`port` integer NOT NULL,
`private_key` text NOT NULL,
`public_key` text NOT NULL,
`ipv4_cidr` text NOT NULL,
`ipv6_cidr` text NOT NULL,
`mtu` integer NOT NULL,
`j_c` integer DEFAULT 7,
`j_min` integer DEFAULT 10,
`j_max` integer DEFAULT 1000,
`s1` integer DEFAULT 128,
`s2` integer DEFAULT 56,
`s3` integer,
`s4` integer,
`h1` text,
`h2` text,
`h3` text,
`h4` text,
`i1` text,
`i2` text,
`i3` text,
`i4` text,
`i5` text,
`enabled` integer NOT NULL,
`created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
`updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL
);
--> statement-breakpoint
INSERT INTO `__new_interfaces_table`("name", "device", "port", "private_key", "public_key", "ipv4_cidr", "ipv6_cidr", "mtu", "j_c", "j_min", "j_max", "s1", "s2", "s3", "s4", "h1", "h2", "h3", "h4", "i1", "i2", "i3", "i4", "i5", "enabled", "created_at", "updated_at") SELECT "name", "device", "port", "private_key", "public_key", "ipv4_cidr", "ipv6_cidr", "mtu", "j_c", "j_min", "j_max", "s1", "s2", "s3", "s4", "h1", "h2", "h3", "h4", "i1", "i2", "i3", "i4", "i5", "enabled", "created_at", "updated_at" FROM `interfaces_table`;--> statement-breakpoint
DROP TABLE `interfaces_table`;--> statement-breakpoint
ALTER TABLE `__new_interfaces_table` RENAME TO `interfaces_table`;--> statement-breakpoint
PRAGMA foreign_keys=ON;--> statement-breakpoint
CREATE UNIQUE INDEX `interfaces_table_port_unique` ON `interfaces_table` (`port`);
@@ -1,2 +0,0 @@
ALTER TABLE `clients_table` ADD `firewall_ips` text;--> statement-breakpoint
ALTER TABLE `interfaces_table` ADD `firewall_enabled` integer DEFAULT false NOT NULL;
@@ -1,972 +0,0 @@
{
"version": "6",
"dialect": "sqlite",
"id": "68c43b7a-772d-4c34-8278-e9fce5b53df1",
"prevId": "e09bc17a-dab6-45a3-a09c-57af222b08fb",
"tables": {
"clients_table": {
"name": "clients_table",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"interface_id": {
"name": "interface_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ipv4_address": {
"name": "ipv4_address",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ipv6_address": {
"name": "ipv6_address",
"type": "text",
"primaryKey": false,
"notNull": true,
"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": {
"name": "private_key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"public_key": {
"name": "public_key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"pre_shared_key": {
"name": "pre_shared_key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"allowed_ips": {
"name": "allowed_ips",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"server_allowed_ips": {
"name": "server_allowed_ips",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"persistent_keepalive": {
"name": "persistent_keepalive",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"mtu": {
"name": "mtu",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"j_c": {
"name": "j_c",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"j_min": {
"name": "j_min",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"j_max": {
"name": "j_max",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i1": {
"name": "i1",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i2": {
"name": "i2",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i3": {
"name": "i3",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i4": {
"name": "i4",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i5": {
"name": "i5",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"dns": {
"name": "dns",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"server_endpoint": {
"name": "server_endpoint",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"enabled": {
"name": "enabled",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {
"clients_table_ipv4_address_unique": {
"name": "clients_table_ipv4_address_unique",
"columns": [
"ipv4_address"
],
"isUnique": true
},
"clients_table_ipv6_address_unique": {
"name": "clients_table_ipv6_address_unique",
"columns": [
"ipv6_address"
],
"isUnique": true
}
},
"foreignKeys": {
"clients_table_user_id_users_table_id_fk": {
"name": "clients_table_user_id_users_table_id_fk",
"tableFrom": "clients_table",
"tableTo": "users_table",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "restrict",
"onUpdate": "cascade"
},
"clients_table_interface_id_interfaces_table_name_fk": {
"name": "clients_table_interface_id_interfaces_table_name_fk",
"tableFrom": "clients_table",
"tableTo": "interfaces_table",
"columnsFrom": [
"interface_id"
],
"columnsTo": [
"name"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"general_table": {
"name": "general_table",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false,
"default": 1
},
"setup_step": {
"name": "setup_step",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"session_password": {
"name": "session_password",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"session_timeout": {
"name": "session_timeout",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"metrics_prometheus": {
"name": "metrics_prometheus",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"metrics_json": {
"name": "metrics_json",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"metrics_password": {
"name": "metrics_password",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"hooks_table": {
"name": "hooks_table",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"pre_up": {
"name": "pre_up",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"post_up": {
"name": "post_up",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"pre_down": {
"name": "pre_down",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"post_down": {
"name": "post_down",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {},
"foreignKeys": {
"hooks_table_id_interfaces_table_name_fk": {
"name": "hooks_table_id_interfaces_table_name_fk",
"tableFrom": "hooks_table",
"tableTo": "interfaces_table",
"columnsFrom": [
"id"
],
"columnsTo": [
"name"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"interfaces_table": {
"name": "interfaces_table",
"columns": {
"name": {
"name": "name",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"device": {
"name": "device",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"port": {
"name": "port",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"private_key": {
"name": "private_key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"public_key": {
"name": "public_key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ipv4_cidr": {
"name": "ipv4_cidr",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ipv6_cidr": {
"name": "ipv6_cidr",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"mtu": {
"name": "mtu",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"j_c": {
"name": "j_c",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 7
},
"j_min": {
"name": "j_min",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 10
},
"j_max": {
"name": "j_max",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 1000
},
"s1": {
"name": "s1",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 128
},
"s2": {
"name": "s2",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 56
},
"s3": {
"name": "s3",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"s4": {
"name": "s4",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"h1": {
"name": "h1",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"h2": {
"name": "h2",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"h3": {
"name": "h3",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"h4": {
"name": "h4",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i1": {
"name": "i1",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i2": {
"name": "i2",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i3": {
"name": "i3",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i4": {
"name": "i4",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i5": {
"name": "i5",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"enabled": {
"name": "enabled",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {
"interfaces_table_port_unique": {
"name": "interfaces_table_port_unique",
"columns": [
"port"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"one_time_links_table": {
"name": "one_time_links_table",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"one_time_link": {
"name": "one_time_link",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {
"one_time_links_table_one_time_link_unique": {
"name": "one_time_links_table_one_time_link_unique",
"columns": [
"one_time_link"
],
"isUnique": true
}
},
"foreignKeys": {
"one_time_links_table_id_clients_table_id_fk": {
"name": "one_time_links_table_id_clients_table_id_fk",
"tableFrom": "one_time_links_table",
"tableTo": "clients_table",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users_table": {
"name": "users_table",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"role": {
"name": "role",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"totp_key": {
"name": "totp_key",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"totp_verified": {
"name": "totp_verified",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"enabled": {
"name": "enabled",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {
"users_table_username_unique": {
"name": "users_table_username_unique",
"columns": [
"username"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_configs_table": {
"name": "user_configs_table",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"default_mtu": {
"name": "default_mtu",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"default_persistent_keepalive": {
"name": "default_persistent_keepalive",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"default_dns": {
"name": "default_dns",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"default_allowed_ips": {
"name": "default_allowed_ips",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"default_j_c": {
"name": "default_j_c",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 7
},
"default_j_min": {
"name": "default_j_min",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 10
},
"default_j_max": {
"name": "default_j_max",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 1000
},
"default_i1": {
"name": "default_i1",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"default_i2": {
"name": "default_i2",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"default_i3": {
"name": "default_i3",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"default_i4": {
"name": "default_i4",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"default_i5": {
"name": "default_i5",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"host": {
"name": "host",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"port": {
"name": "port",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {},
"foreignKeys": {
"user_configs_table_id_interfaces_table_name_fk": {
"name": "user_configs_table_id_interfaces_table_name_fk",
"tableFrom": "user_configs_table",
"tableTo": "interfaces_table",
"columnsFrom": [
"id"
],
"columnsTo": [
"name"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}
@@ -1,987 +0,0 @@
{
"version": "6",
"dialect": "sqlite",
"id": "0f072f91-cd10-4702-ae7b-245255d69d1e",
"prevId": "68c43b7a-772d-4c34-8278-e9fce5b53df1",
"tables": {
"clients_table": {
"name": "clients_table",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"interface_id": {
"name": "interface_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ipv4_address": {
"name": "ipv4_address",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ipv6_address": {
"name": "ipv6_address",
"type": "text",
"primaryKey": false,
"notNull": true,
"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": {
"name": "private_key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"public_key": {
"name": "public_key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"pre_shared_key": {
"name": "pre_shared_key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"allowed_ips": {
"name": "allowed_ips",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"server_allowed_ips": {
"name": "server_allowed_ips",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"firewall_ips": {
"name": "firewall_ips",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"persistent_keepalive": {
"name": "persistent_keepalive",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"mtu": {
"name": "mtu",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"j_c": {
"name": "j_c",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"j_min": {
"name": "j_min",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"j_max": {
"name": "j_max",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i1": {
"name": "i1",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i2": {
"name": "i2",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i3": {
"name": "i3",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i4": {
"name": "i4",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i5": {
"name": "i5",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"dns": {
"name": "dns",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"server_endpoint": {
"name": "server_endpoint",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"enabled": {
"name": "enabled",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {
"clients_table_ipv4_address_unique": {
"name": "clients_table_ipv4_address_unique",
"columns": [
"ipv4_address"
],
"isUnique": true
},
"clients_table_ipv6_address_unique": {
"name": "clients_table_ipv6_address_unique",
"columns": [
"ipv6_address"
],
"isUnique": true
}
},
"foreignKeys": {
"clients_table_user_id_users_table_id_fk": {
"name": "clients_table_user_id_users_table_id_fk",
"tableFrom": "clients_table",
"tableTo": "users_table",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "restrict",
"onUpdate": "cascade"
},
"clients_table_interface_id_interfaces_table_name_fk": {
"name": "clients_table_interface_id_interfaces_table_name_fk",
"tableFrom": "clients_table",
"tableTo": "interfaces_table",
"columnsFrom": [
"interface_id"
],
"columnsTo": [
"name"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"general_table": {
"name": "general_table",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false,
"default": 1
},
"setup_step": {
"name": "setup_step",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"session_password": {
"name": "session_password",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"session_timeout": {
"name": "session_timeout",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"metrics_prometheus": {
"name": "metrics_prometheus",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"metrics_json": {
"name": "metrics_json",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"metrics_password": {
"name": "metrics_password",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"hooks_table": {
"name": "hooks_table",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"pre_up": {
"name": "pre_up",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"post_up": {
"name": "post_up",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"pre_down": {
"name": "pre_down",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"post_down": {
"name": "post_down",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {},
"foreignKeys": {
"hooks_table_id_interfaces_table_name_fk": {
"name": "hooks_table_id_interfaces_table_name_fk",
"tableFrom": "hooks_table",
"tableTo": "interfaces_table",
"columnsFrom": [
"id"
],
"columnsTo": [
"name"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"interfaces_table": {
"name": "interfaces_table",
"columns": {
"name": {
"name": "name",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"device": {
"name": "device",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"port": {
"name": "port",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"private_key": {
"name": "private_key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"public_key": {
"name": "public_key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ipv4_cidr": {
"name": "ipv4_cidr",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ipv6_cidr": {
"name": "ipv6_cidr",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"mtu": {
"name": "mtu",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"j_c": {
"name": "j_c",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 7
},
"j_min": {
"name": "j_min",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 10
},
"j_max": {
"name": "j_max",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 1000
},
"s1": {
"name": "s1",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 128
},
"s2": {
"name": "s2",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 56
},
"s3": {
"name": "s3",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"s4": {
"name": "s4",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"h1": {
"name": "h1",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"h2": {
"name": "h2",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"h3": {
"name": "h3",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"h4": {
"name": "h4",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i1": {
"name": "i1",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i2": {
"name": "i2",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i3": {
"name": "i3",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i4": {
"name": "i4",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"i5": {
"name": "i5",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"enabled": {
"name": "enabled",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"firewall_enabled": {
"name": "firewall_enabled",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {
"interfaces_table_port_unique": {
"name": "interfaces_table_port_unique",
"columns": [
"port"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"one_time_links_table": {
"name": "one_time_links_table",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"one_time_link": {
"name": "one_time_link",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {
"one_time_links_table_one_time_link_unique": {
"name": "one_time_links_table_one_time_link_unique",
"columns": [
"one_time_link"
],
"isUnique": true
}
},
"foreignKeys": {
"one_time_links_table_id_clients_table_id_fk": {
"name": "one_time_links_table_id_clients_table_id_fk",
"tableFrom": "one_time_links_table",
"tableTo": "clients_table",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users_table": {
"name": "users_table",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"role": {
"name": "role",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"totp_key": {
"name": "totp_key",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"totp_verified": {
"name": "totp_verified",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"enabled": {
"name": "enabled",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {
"users_table_username_unique": {
"name": "users_table_username_unique",
"columns": [
"username"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_configs_table": {
"name": "user_configs_table",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"default_mtu": {
"name": "default_mtu",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"default_persistent_keepalive": {
"name": "default_persistent_keepalive",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"default_dns": {
"name": "default_dns",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"default_allowed_ips": {
"name": "default_allowed_ips",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"default_j_c": {
"name": "default_j_c",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 7
},
"default_j_min": {
"name": "default_j_min",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 10
},
"default_j_max": {
"name": "default_j_max",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 1000
},
"default_i1": {
"name": "default_i1",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"default_i2": {
"name": "default_i2",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"default_i3": {
"name": "default_i3",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"default_i4": {
"name": "default_i4",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"default_i5": {
"name": "default_i5",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"host": {
"name": "host",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"port": {
"name": "port",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {},
"foreignKeys": {
"user_configs_table_id_interfaces_table_name_fk": {
"name": "user_configs_table_id_interfaces_table_name_fk",
"tableFrom": "user_configs_table",
"tableTo": "interfaces_table",
"columnsFrom": [
"id"
],
"columnsTo": [
"name"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}
@@ -22,20 +22,6 @@
"when": 1761298328460,
"tag": "0002_keen_sleepwalker",
"breakpoints": true
},
{
"idx": 3,
"version": "6",
"when": 1770902426367,
"tag": "0003_breezy_colossus",
"breakpoints": true
},
{
"idx": 4,
"version": "6",
"when": 1771352889394,
"tag": "0004_optimal_mandrill",
"breakpoints": true
}
]
}
@@ -34,8 +34,6 @@ export const client = sqliteTable('clients_table', {
serverAllowedIps: text('server_allowed_ips', { mode: 'json' })
.$type<string[]>()
.notNull(),
// Firewall-enforced allowed IPs (null = use allowedIps)
firewallIps: text('firewall_ips', { mode: 'json' }).$type<string[] | null>(),
persistentKeepalive: int('persistent_keepalive').notNull(),
mtu: int().notNull(),
jC: int('j_c'),
@@ -71,7 +71,6 @@ export const ClientUpdateSchema = schemaForType<UpdateClientType>()(
postDown: HookSchema,
allowedIps: AllowedIpsSchema.nullable(),
serverAllowedIps: serverAllowedIps,
firewallIps: FirewallIpsSchema.nullable(),
mtu: MtuSchema,
jC: JcSchema,
jMin: JminSchema,
@@ -20,21 +20,17 @@ export const wgInterface = sqliteTable('interfaces_table', {
s2: int().default(56),
s3: int(),
s4: int(),
h1: text(),
h2: text(),
h3: text(),
h4: text(),
i1: text(),
i2: text(),
i3: text(),
i4: text(),
i5: text(),
h1: int().default(0),
h2: int().default(0),
h3: int().default(0),
h4: int().default(0),
// does nothing yet
enabled: int({ mode: 'boolean' }).notNull(),
// Enable per-client firewall filtering via iptables
firewallEnabled: int('firewall_enabled', { mode: 'boolean' })
.notNull()
.default(false),
createdAt: text('created_at')
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),
@@ -18,13 +18,6 @@ function createPreparedStatement(db: DBType) {
})
.where(eq(wgInterface.name, sql.placeholder('interface')))
.prepare(),
setFirewallEnabled: db
.update(wgInterface)
.set({
firewallEnabled: sql.placeholder('firewallEnabled') as never as boolean,
})
.where(eq(wgInterface.name, sql.placeholder('interface')))
.prepare(),
};
}
@@ -63,13 +56,6 @@ export class InterfaceService {
.execute();
}
setFirewallEnabled(firewallEnabled: boolean) {
return this.#statements.setFirewallEnabled.execute({
interface: 'wg0',
firewallEnabled,
});
}
updateCidr(data: InterfaceCidrUpdateType) {
return this.#db.transaction(async (tx) => {
const oldCidr = await tx.query.wgInterface
@@ -38,19 +38,18 @@ export const InterfaceUpdateSchema = schemaForType<InterfaceUpdateType>()(
s2: SSchema,
s3: SSchema,
s4: SSchema,
h1: HSchema,
h2: HSchema,
h3: HSchema,
h4: HSchema,
i1: ISchema,
i2: ISchema,
i3: ISchema,
i4: ISchema,
i5: ISchema,
h1: HSchema,
h2: HSchema,
h3: HSchema,
h4: HSchema,
port: PortSchema,
device: device,
enabled: EnabledSchema,
firewallEnabled: EnabledSchema,
})
);
+24 -39
View File
@@ -1,5 +1,6 @@
import fs from 'node:fs/promises';
import debug from 'debug';
import { encodeQR } from 'qr';
import type { InterfaceType } from '#db/repositories/interface/types';
const WG_DEBUG = debug('WireGuard');
@@ -15,21 +16,6 @@ class WireGuard {
const wgInterface = await Database.interfaces.get();
await this.#saveWireguardConfig(wgInterface);
await this.#syncWireguardConfig(wgInterface);
await this.#applyFirewallRules(wgInterface);
}
/**
* Apply firewall rules based on current config
*/
async #applyFirewallRules(wgInterface: InterfaceType) {
const clients = await Database.clients.getAll();
const userConfig = await Database.userConfigs.get();
await firewall.rebuildRules(
wgInterface,
clients,
userConfig,
!WG_ENV.DISABLE_IPV6
);
}
/**
@@ -180,7 +166,24 @@ class WireGuard {
async getClientQRCodeSVG({ clientId }: { clientId: ID }) {
const config = await this.getClientConfiguration({ clientId });
return encodeQRCode(config);
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'
);
}
cleanClientFilename(name: string): string {
@@ -210,7 +213,7 @@ class WireGuard {
WG_DEBUG('New Wireguard Keys generated successfully.');
}
if (wgInterface.h1 === '0') {
if (wgInterface.h1 === 0) {
WG_DEBUG('Generating random AmneziaWG obfuscation parameters...');
const headers = new Set<number>();
@@ -219,10 +222,10 @@ class WireGuard {
}
const [h1, h2, h3, h4] = Array.from(headers);
wgInterface.h1 = String(h1)!;
wgInterface.h2 = String(h2)!;
wgInterface.h3 = String(h3)!;
wgInterface.h4 = String(h4)!;
wgInterface.h1 = h1!;
wgInterface.h2 = h2!;
wgInterface.h3 = h3!;
wgInterface.h4 = h4!;
Database.interfaces.update(wgInterface);
}
@@ -247,24 +250,6 @@ class WireGuard {
await this.#syncWireguardConfig(wgInterface);
WG_DEBUG(`Wireguard Interface ${wgInterface.name} started successfully.`);
// Check if firewall was enabled but iptables isn't available
if (wgInterface.firewallEnabled) {
const enableIpv6 = !WG_ENV.DISABLE_IPV6;
const iptablesAvailable = await firewall.isAvailable(enableIpv6);
if (!iptablesAvailable) {
const requiredTools = enableIpv6 ? 'iptables/ip6tables' : 'iptables';
console.warn(
`WARNING: Per-Client Firewall is enabled but ${requiredTools} is not available. Disabling firewall feature. Please install ${requiredTools} to use this feature.`
);
await Database.interfaces.setFirewallEnabled(false);
wgInterface.firewallEnabled = false; // Update local copy
}
}
WG_DEBUG('Applying firewall rules...');
await this.#applyFirewallRules(wgInterface);
WG_DEBUG('Firewall rules applied successfully.');
WG_DEBUG('Starting Cron Job...');
await this.startCronJob();
WG_DEBUG('Cron Job started successfully.');
-363
View File
@@ -1,363 +0,0 @@
import debug from 'debug';
import { isIPv6 } from 'is-ip';
import type { ClientType } from '#db/repositories/client/types';
import type { InterfaceType } from '#db/repositories/interface/types';
import type { UserConfigType } from '#db/repositories/userConfig/types';
const FW_DEBUG = debug('Firewall');
const CHAIN_NAME = 'WG_CLIENTS';
// Mutex to prevent concurrent rule rebuilds
let rebuildInProgress = false;
let rebuildQueued = false;
// Cache iptables availability check result
let iptablesAvailable: boolean | null = null;
type ParsedEntry = {
ip: string;
port?: number;
proto?: 'tcp' | 'udp' | 'both';
};
type FirewallClient = Pick<
ClientType,
| 'id'
| 'name'
| 'ipv4Address'
| 'ipv6Address'
| 'allowedIps'
| 'firewallIps'
| 'enabled'
>;
/**
* Sanitize a client identifier for use in an iptables comment.
* Strips all characters except ASCII alphanumeric, space, underscore, hyphen, and dot.
* Combines with client ID for a safe, identifiable comment.
* Truncates to 256 bytes (iptables comment module limit).
*/
function sanitizeComment(clientId: number, clientName: string): string {
const safe = clientName.replace(/[^a-zA-Z0-9 _.-]/g, '');
const comment = `client ${clientId}: ${safe}`;
return comment.slice(0, 256);
}
/**
* Parse a firewall entry string into its components.
* Supports formats:
* - IP: "10.0.0.1" or "2001:db8::1"
* - CIDR: "10.0.0.0/24" or "2001:db8::/32"
* - IP:port: "10.0.0.1:443" or "[2001:db8::1]:443"
* - IP:port/proto: "10.0.0.1:443/tcp" or "10.0.0.1:53/udp"
* - CIDR:port: "10.0.0.0/24:443"
* - CIDR:port/proto: "10.0.0.0/24:443/tcp" or "10.0.0.0/24:53/udp"
*
* Note: Protocol (/tcp or /udp) requires a port. "IP/tcp" or "CIDR/tcp" without
* a port is invalid and will throw an error.
*
* @throws {Error} If protocol is specified without a port
*/
function parseFirewallEntry(entry: string): ParsedEntry {
// Extract protocol suffix first: /tcp or /udp
let proto: 'tcp' | 'udp' | 'both' | undefined;
let remaining = entry;
if (entry.endsWith('/tcp')) {
proto = 'tcp';
remaining = entry.slice(0, -4);
} else if (entry.endsWith('/udp')) {
proto = 'udp';
remaining = entry.slice(0, -4);
}
// Handle IPv6 with port: [2001:db8::1]:443
if (remaining.startsWith('[')) {
const match = remaining.match(/^\[(.+)\]:(\d+)$/);
if (match && match[1] && match[2]) {
return {
ip: match[1],
port: parseInt(match[2], 10),
proto: proto ?? 'both',
};
}
// Just bracketed IPv6 without port
const ipMatch = remaining.match(/^\[(.+)\]$/);
if (ipMatch && ipMatch[1]) {
if (proto) {
throw new Error(
`Invalid firewall entry "${entry}": Protocol (/${proto}) requires a port. Use format like "[${ipMatch[1]}]:443/${proto}"`
);
}
return { ip: ipMatch[1] };
}
if (proto) {
throw new Error(
`Invalid firewall entry "${entry}": Protocol (/${proto}) requires a port`
);
}
return { ip: remaining };
}
// Handle IPv4 with port or CIDR with port
// Count colons to distinguish IPv6 from IPv4:port
const colonCount = (remaining.match(/:/g) || []).length;
if (colonCount === 1) {
// Could be IPv4:port or CIDR:port
const lastColon = remaining.lastIndexOf(':');
const possiblePort = remaining.slice(lastColon + 1);
if (/^\d+$/.test(possiblePort)) {
return {
ip: remaining.slice(0, lastColon),
port: parseInt(possiblePort, 10),
proto: proto ?? 'both',
};
}
}
// Plain IP or CIDR (IPv4 or IPv6)
if (proto) {
throw new Error(
`Invalid firewall entry "${entry}": Protocol (/${proto}) requires a port. Use format like "${remaining}:443/${proto}"`
);
}
return { ip: remaining };
}
/**
* Generate iptables rule arguments for a single firewall entry
*/
function generateRuleArgs(
clientIp: string,
entry: ParsedEntry,
comment?: string,
action: 'A' | 'D' = 'A'
): string[] {
const rules: string[] = [];
const commentArg = comment ? ` -m comment --comment "${comment}"` : '';
const baseArgs = `-${action} ${CHAIN_NAME} -s ${clientIp} -d ${entry.ip}`;
if (entry.port) {
// Port-specific rules
if (entry.proto === 'tcp' || entry.proto === 'both') {
rules.push(
`${baseArgs} -p tcp --dport ${entry.port}${commentArg} -j ACCEPT`
);
}
if (entry.proto === 'udp' || entry.proto === 'both') {
rules.push(
`${baseArgs} -p udp --dport ${entry.port}${commentArg} -j ACCEPT`
);
}
} else {
// No port - allow all traffic to destination
rules.push(`${baseArgs}${commentArg} -j ACCEPT`);
}
return rules;
}
export const firewall = {
/**
* Initialize the custom chain if it doesn't exist
*/
async initChain(interfaceName: string): Promise<void> {
FW_DEBUG(
`Initializing firewall chain ${CHAIN_NAME} for interface ${interfaceName}`
);
// Create chain if not exists (iptables returns error if exists, so we ignore)
await exec(`iptables -N ${CHAIN_NAME} 2>/dev/null || true`);
await exec(`ip6tables -N ${CHAIN_NAME} 2>/dev/null || true`);
// Ensure chain is referenced from FORWARD (if not already)
// Insert at position 1 to process before generic ACCEPT rules
await exec(
`iptables -C FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || iptables -I FORWARD 1 -i ${interfaceName} -j ${CHAIN_NAME}`
);
await exec(
`ip6tables -C FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || ip6tables -I FORWARD 1 -i ${interfaceName} -j ${CHAIN_NAME}`
);
},
/**
* Flush all rules in the custom chain
*/
async flushChain(): Promise<void> {
FW_DEBUG(`Flushing firewall chain ${CHAIN_NAME}`);
await exec(`iptables -F ${CHAIN_NAME} 2>/dev/null || true`);
await exec(`ip6tables -F ${CHAIN_NAME} 2>/dev/null || true`);
},
/**
* Apply firewall rules for a single client
*/
async applyClientRules(
client: FirewallClient,
defaultAllowedIps: string[],
enableIpv6: boolean
): Promise<void> {
// Determine which IPs to use for firewall rules
// Priority: firewallIps > allowedIps > defaultAllowedIps
const effectiveIps =
client.firewallIps && client.firewallIps.length > 0
? client.firewallIps
: (client.allowedIps ?? defaultAllowedIps);
FW_DEBUG(
`Applying firewall rules for client ${client.name} (${client.id}): ${effectiveIps.join(', ')}`
);
const comment = sanitizeComment(client.id, client.name);
for (const ipEntry of effectiveIps) {
const parsed = parseFirewallEntry(ipEntry);
const baseIp = parsed.ip.split('/')[0] ?? parsed.ip; // Handle CIDR by checking base IP
const destIsIpv6 = isIPv6(baseIp);
if (destIsIpv6) {
if (enableIpv6) {
const rules = generateRuleArgs(client.ipv6Address, parsed, comment);
for (const rule of rules) {
await exec(`ip6tables ${rule}`);
}
}
} else {
const rules = generateRuleArgs(client.ipv4Address, parsed, comment);
for (const rule of rules) {
await exec(`iptables ${rule}`);
}
}
}
},
/**
* Full rebuild of firewall rules from database state
*/
async rebuildRules(
wgInterface: InterfaceType,
clients: FirewallClient[],
userConfig: UserConfigType,
enableIpv6: boolean
): Promise<void> {
if (!wgInterface.firewallEnabled) {
FW_DEBUG('Firewall filtering disabled, removing any existing rules');
await this.removeFiltering(wgInterface.name);
return;
}
// Handle concurrent rebuilds with queue
if (rebuildInProgress) {
FW_DEBUG('Rebuild already in progress, queuing');
rebuildQueued = true;
return;
}
rebuildInProgress = true;
try {
FW_DEBUG('Rebuilding firewall rules...');
// Initialize chain structure
await this.initChain(wgInterface.name);
// Flush existing rules
await this.flushChain();
// Apply rules for each enabled client
for (const client of clients) {
if (!client.enabled) continue;
await this.applyClientRules(
client,
userConfig.defaultAllowedIps,
enableIpv6
);
}
// Add final DROP for any traffic not explicitly allowed
await exec(`iptables -A ${CHAIN_NAME} -j DROP`);
if (enableIpv6) {
await exec(`ip6tables -A ${CHAIN_NAME} -j DROP`);
}
FW_DEBUG('Firewall rules rebuilt successfully');
} finally {
rebuildInProgress = false;
// If another rebuild was queued, run it now
if (rebuildQueued) {
rebuildQueued = false;
FW_DEBUG('Processing queued rebuild');
await this.rebuildRules(wgInterface, clients, userConfig, enableIpv6);
}
}
},
/**
* Remove all firewall filtering (when feature is disabled)
*/
async removeFiltering(interfaceName: string): Promise<void> {
FW_DEBUG(`Removing firewall filtering for interface ${interfaceName}`);
// Remove jump rules from FORWARD chain
await exec(
`iptables -D FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || true`
);
await exec(
`ip6tables -D FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || true`
);
// Flush and delete the chain
await exec(`iptables -F ${CHAIN_NAME} 2>/dev/null || true`);
await exec(`ip6tables -F ${CHAIN_NAME} 2>/dev/null || true`);
await exec(`iptables -X ${CHAIN_NAME} 2>/dev/null || true`);
await exec(`ip6tables -X ${CHAIN_NAME} 2>/dev/null || true`);
},
/**
* Check if iptables (and optionally ip6tables) are available on the system.
* @param enableIpv6 - If true, also check for ip6tables. Defaults to true.
*/
async isAvailable(enableIpv6: boolean = true): Promise<boolean> {
// Return cached result if we've already checked
if (iptablesAvailable !== null) {
return iptablesAvailable;
}
try {
// Check for iptables (always required)
await exec('iptables --version');
FW_DEBUG('iptables is available');
// Check for ip6tables (only if IPv6 is enabled)
if (enableIpv6) {
await exec('ip6tables --version');
FW_DEBUG('ip6tables is available');
} else {
FW_DEBUG('IPv6 disabled, skipping ip6tables check');
}
iptablesAvailable = true;
return true;
} catch (error) {
iptablesAvailable = false;
FW_DEBUG('iptables/ip6tables is not available:', error);
return false;
}
},
/**
* Clear the availability cache to force a re-check
*/
clearAvailabilityCache(): void {
iptablesAvailable = null;
},
};
export const firewallTestExports = {
parseFirewallEntry,
generateRuleArgs,
sanitizeComment,
};
-41
View File
@@ -1,41 +0,0 @@
// ! Auto Imports are not supported in this file
import type { ErrorCorrection } from 'qr';
import { encodeQR } from 'qr';
export function encodeQRCode(config: string): string {
return tryECCModes((ecc) => {
return encodeQR(config, 'svg', {
ecc,
scale: 2,
encoding: 'byte',
});
});
}
export function encodeQRCodeTerm(config: string): string {
return tryECCModes((ecc) => {
return encodeQR(config, 'term', {
ecc,
encoding: 'byte',
});
});
}
function tryECCModes<T>(callback: (ecc: ErrorCorrection) => T): T {
// defined manually, as qr's ECMode is in wrong order
const ECMode = ['high', 'quartile', 'medium', 'low'] as const;
for (const ecc of ECMode) {
try {
return callback(ecc);
} 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'
);
}
+1 -7
View File
@@ -1,5 +1,3 @@
// ! Auto Imports are not supported in this file
import type { InterfaceType } from '#db/repositories/interface/types';
/**
@@ -11,10 +9,6 @@ export function template(templ: string, values: Record<string, string>) {
});
}
export function removeNewlines(templ: string) {
return templ.replace(/\r\n|\r|\n/g, ' ');
}
/**
* Available keys:
* - ipv4Cidr: IPv4 CIDR
@@ -24,7 +18,7 @@ export function removeNewlines(templ: string) {
* - uiPort: UI port number
*/
export function iptablesTemplate(templ: string, wgInterface: InterfaceType) {
return template(removeNewlines(templ), {
return template(templ, {
ipv4Cidr: wgInterface.ipv4Cidr,
ipv6Cidr: wgInterface.ipv6Cidr,
device: wgInterface.device,
+1 -96
View File
@@ -1,8 +1,6 @@
import type { ZodSchema } from 'zod';
import z from 'zod';
import type { H3Event, EventHandlerRequest } from 'h3';
import { isIP } from 'is-ip';
import isCidr from 'is-cidr';
export type ID = number;
@@ -36,39 +34,7 @@ export const JmaxSchema = z.number().max(1280).nullable();
export const SSchema = z.number().max(1132).nullable();
const H_MIN = 5;
const H_MAX = 2 ** 31 - 1;
export const HSchema = z
.string()
.transform((v) => v.replace(/\s+/g, ''))
.refine(
(v) => {
if (!v) return false;
if (!/^\d+(-\d+)?$/.test(v)) return false;
if (!v.includes('-')) {
const num = Number(v);
return num >= H_MIN && num <= H_MAX;
}
const [min, max] = v.split('-').map(Number);
return min && max && min >= H_MIN && max <= H_MAX && min <= max;
return false;
},
{
message: t('zod.generic.validNumberRange'),
}
)
.transform((v) => {
if (!v.includes('-')) return `${Number(v)}`;
const [min, max] = v.split('-').map(Number);
return min === max ? `${min}` : `${min}-${max}`;
})
.nullable();
export const HSchema = z.number().min(5).max(2147483647).nullable();
export const ISchema = z.string().nullable();
@@ -93,65 +59,6 @@ export const AllowedIpsSchema = z
.array(AddressSchema, { message: t('zod.allowedIps') })
.min(1, { message: t('zod.allowedIps') });
// Validation for firewall IP entries
const FirewallIpEntrySchema = z
.string({ message: t('zod.client.firewallIps') })
.min(1, { message: t('zod.client.firewallIps') })
.refine(
(entry) => {
// Check if protocol suffix is present
const hasProto = /\/(tcp|udp)$/i.test(entry);
const entryWithoutProto = entry.replace(/\/(tcp|udp)$/i, '');
// If protocol was specified without a port, it's invalid
if (hasProto) {
// Protocol requires port, so check for IP:port format
const portMatch = entryWithoutProto.match(/^(.+):(\d+)$/);
if (!portMatch) {
return false;
}
const [, ipPart, portPart] = portMatch;
const port = parseInt(portPart!, 10);
const cleanIp = ipPart!.replace(/^\[|\]$/g, '');
return (isIP(cleanIp) || isCidr(cleanIp)) && port >= 1 && port <= 65535;
}
// Check if it's just IP or CIDR first (handles IPv6 addresses)
if (isIP(entryWithoutProto) || isCidr(entryWithoutProto)) {
return true;
}
// Check if it's bracketed IPv6 without port: [::1]
const bracketedMatch = entryWithoutProto.match(/^\[(.+)\]$/);
if (bracketedMatch) {
const innerIp = bracketedMatch[1];
return isIP(innerIp!) || isCidr(innerIp!);
}
// Check if it's IP:port format (IPv4:port or [IPv6]:port)
const portMatch = entryWithoutProto.match(/^(.+):(\d+)$/);
if (portMatch) {
const [, ipPart, portPart] = portMatch;
const port = parseInt(portPart!, 10);
// Remove IPv6 brackets if present
const cleanIp = ipPart!.replace(/^\[|\]$/g, '');
// Validate IP and port
return (isIP(cleanIp) || isCidr(cleanIp)) && port >= 1 && port <= 65535;
}
return false;
},
{
message: t('zod.client.firewallIpsInvalid'),
}
);
export const FirewallIpsSchema = z.array(FirewallIpEntrySchema, {
message: t('zod.client.firewallIps'),
});
export const FileSchema = z.object({
file: z.string({ message: t('zod.file') }),
});
@@ -258,5 +165,3 @@ export function validateZod<T>(
export function assertUnreachable(_: never): never {
throw new Error("Didn't expect to get here");
}
export const typesTestExports = { FirewallIpEntrySchema };
+13 -19
View File
@@ -1,9 +1,5 @@
// ! Auto Imports are not supported in this file
import { parseCidr } from 'cidr-tools';
import { stringifyIp } from 'ip-bigint';
import { removeNewlines } from './template';
import type { ClientType } from '#db/repositories/client/types';
import type { InterfaceType } from '#db/repositories/interface/types';
import type { UserConfigType } from '#db/repositories/userConfig/types';
@@ -13,9 +9,7 @@ type Options = {
enableIpv6?: boolean;
};
// needed to support cli
const wgExecutable =
typeof WG_ENV !== 'undefined' ? WG_ENV.WG_EXECUTABLE : 'dev';
const wgExecutable = WG_ENV.WG_EXECUTABLE;
export const wg = {
generateServerPeer: (
@@ -69,15 +63,15 @@ AllowedIPs = ${allowedIps.join(', ')}${extraLines.length ? `\n${extraLines.join(
S2: wgInterface.s2,
S3: wgInterface.s3,
S4: wgInterface.s4,
H1: wgInterface.h1,
H2: wgInterface.h2,
H3: wgInterface.h3,
H4: wgInterface.h4,
I1: wgInterface.i1,
I2: wgInterface.i2,
I3: wgInterface.i3,
I4: wgInterface.i4,
I5: wgInterface.i5,
H1: wgInterface.h1,
H2: wgInterface.h2,
H3: wgInterface.h3,
H4: wgInterface.h4,
} as const;
awgLines = Object.entries(parameters)
@@ -116,10 +110,10 @@ PostDown = ${iptablesTemplate(hooks.postDown, wgInterface)}`;
(enableIpv6 ? `, ${client.ipv6Address}/128` : '');
const hookLines = [
client.preUp ? `PreUp = ${removeNewlines(client.preUp)}` : null,
client.postUp ? `PostUp = ${removeNewlines(client.postUp)}` : null,
client.preDown ? `PreDown = ${removeNewlines(client.preDown)}` : null,
client.postDown ? `PostDown = ${removeNewlines(client.postDown)}` : null,
client.preUp ? `PreUp = ${client.preUp}` : null,
client.postUp ? `PostUp = ${client.postUp}` : null,
client.preDown ? `PreDown = ${client.preDown}` : null,
client.postDown ? `PostDown = ${client.postDown}` : null,
];
const dnsServers = client.dns ?? userConfig.defaultDns;
@@ -137,15 +131,15 @@ PostDown = ${iptablesTemplate(hooks.postDown, wgInterface)}`;
S2: wgInterface.s2,
S3: wgInterface.s3,
S4: wgInterface.s4,
H1: wgInterface.h1,
H2: wgInterface.h2,
H3: wgInterface.h3,
H4: wgInterface.h4,
I1: client.i1,
I2: client.i2,
I3: client.i3,
I4: client.i4,
I5: client.i5,
H1: wgInterface.h1,
H2: wgInterface.h2,
H3: wgInterface.h3,
H4: wgInterface.h4,
} as const;
awgLines = Object.entries(parameters)
-294
View File
@@ -1,294 +0,0 @@
import { describe, expect, test } from 'vitest';
import { firewallTestExports } from '../../server/utils/firewall';
import { typesTestExports } from '../../server/utils/types';
describe('firewall', () => {
describe('isValidFirewallEntry', () => {
test('invalid ips', () => {
expect(() => typesTestExports.FirewallIpEntrySchema.parse('')).toThrow();
expect(() =>
typesTestExports.FirewallIpEntrySchema.parse('255.255.255.256')
).toThrow();
expect(() =>
typesTestExports.FirewallIpEntrySchema.parse('1.1.1.256')
).toThrow();
expect(() =>
typesTestExports.FirewallIpEntrySchema.parse('1.1.1.1.1')
).toThrow();
expect(() =>
typesTestExports.FirewallIpEntrySchema.parse('[]:443/udp')
).toThrow();
expect(() =>
typesTestExports.FirewallIpEntrySchema.parse('[]:443')
).toThrow();
expect(() =>
typesTestExports.FirewallIpEntrySchema.parse('[::1]/32')
).toThrow();
expect(() =>
typesTestExports.FirewallIpEntrySchema.parse('[1.1.1.1]/32')
).toThrow();
expect(() =>
typesTestExports.FirewallIpEntrySchema.parse('[::g]/32')
).toThrow();
expect(() =>
typesTestExports.FirewallIpEntrySchema.parse('2001:dbx::1')
).toThrow();
});
test('invalid port, protocol or cidr', () => {
expect(() =>
typesTestExports.FirewallIpEntrySchema.parse('1.1.1.1:80/tcpp')
).toThrow();
expect(() =>
typesTestExports.FirewallIpEntrySchema.parse('1.1.1.1:65536')
).toThrow();
expect(() =>
typesTestExports.FirewallIpEntrySchema.parse('1.1.1.1:0')
).toThrow();
expect(() =>
typesTestExports.FirewallIpEntrySchema.parse('1.1.1.1/33')
).toThrow();
expect(() =>
typesTestExports.FirewallIpEntrySchema.parse('1.1.1.1/32:0')
).toThrow();
});
test('protocol without port', () => {
expect(() =>
typesTestExports.FirewallIpEntrySchema.parse('1.1.1.1/tcp')
).toThrow();
});
test('valid entries', () => {
expect(typesTestExports.FirewallIpEntrySchema.parse('1.1.1.1')).toBe(
'1.1.1.1'
);
expect(typesTestExports.FirewallIpEntrySchema.parse('::/0')).toBe('::/0');
expect(typesTestExports.FirewallIpEntrySchema.parse('::0/0')).toBe(
'::0/0'
);
expect(typesTestExports.FirewallIpEntrySchema.parse('2001:db8::1')).toBe(
'2001:db8::1'
);
expect(typesTestExports.FirewallIpEntrySchema.parse('::1')).toBe('::1');
expect(
typesTestExports.FirewallIpEntrySchema.parse('2001:db8::1/32')
).toBe('2001:db8::1/32');
expect(typesTestExports.FirewallIpEntrySchema.parse('[::1]')).toBe(
'[::1]'
);
expect(typesTestExports.FirewallIpEntrySchema.parse('[::1/32]')).toBe(
'[::1/32]'
);
});
});
describe('parseFirewallEntry', () => {
test('IPv4', () => {
expect(firewallTestExports.parseFirewallEntry('1.1.1.1')).toEqual({
ip: '1.1.1.1',
});
});
test('IPv4 with Protocol', () => {
expect(() =>
firewallTestExports.parseFirewallEntry('1.1.1.1/tcp')
).toThrow();
expect(() =>
firewallTestExports.parseFirewallEntry('1.1.1.1/udp')
).toThrow();
});
test('IPv4 with CIDR', () => {
expect(firewallTestExports.parseFirewallEntry('1.1.1.1/32')).toEqual({
ip: '1.1.1.1/32',
});
});
test('IPv4 with CIDR and Protocol', () => {
expect(() =>
firewallTestExports.parseFirewallEntry('1.1.1.1/32/tcp')
).toThrow();
});
test('IPv4 with Port', () => {
expect(firewallTestExports.parseFirewallEntry('1.1.1.1:80')).toEqual({
ip: '1.1.1.1',
port: 80,
proto: 'both',
});
});
test('IPv4 with Port and Protocol', () => {
expect(firewallTestExports.parseFirewallEntry('1.1.1.1:80/tcp')).toEqual({
ip: '1.1.1.1',
port: 80,
proto: 'tcp',
});
expect(firewallTestExports.parseFirewallEntry('1.1.1.1:80/udp')).toEqual({
ip: '1.1.1.1',
port: 80,
proto: 'udp',
});
});
test('IPv4 with CIDR and Port', () => {
expect(
firewallTestExports.parseFirewallEntry('10.10.0.0/24:443')
).toEqual({
ip: '10.10.0.0/24',
port: 443,
proto: 'both',
});
});
test('IPv4 with CIDR, Port and Protocol', () => {
expect(
firewallTestExports.parseFirewallEntry('10.10.0.0/24:443/tcp')
).toEqual({
ip: '10.10.0.0/24',
port: 443,
proto: 'tcp',
});
expect(
firewallTestExports.parseFirewallEntry('10.10.0.0/24:443/udp')
).toEqual({
ip: '10.10.0.0/24',
port: 443,
proto: 'udp',
});
});
test('IPv6', () => {
expect(firewallTestExports.parseFirewallEntry('[2001:db8::1]')).toEqual({
ip: '2001:db8::1',
});
expect(firewallTestExports.parseFirewallEntry('2001:db8::1')).toEqual({
ip: '2001:db8::1',
});
});
test('IPv6 with Protocol', () => {
expect(() =>
firewallTestExports.parseFirewallEntry('[2001:db8::1]/tcp')
).toThrow();
expect(() =>
firewallTestExports.parseFirewallEntry('2001:db8::1/udp')
).toThrow();
});
test('IPv6 with CIDR', () => {
expect(firewallTestExports.parseFirewallEntry('::0/0')).toEqual({
ip: '::0/0',
});
expect(firewallTestExports.parseFirewallEntry('[::0/0]')).toEqual({
ip: '::0/0',
});
});
test('IPv6 with CIDR and Protocol', () => {
expect(() =>
firewallTestExports.parseFirewallEntry('::0/0/tcp')
).toThrow();
});
test('IPv6 with Port', () => {
expect(
firewallTestExports.parseFirewallEntry('[2001:db8::1]:443')
).toEqual({
ip: '2001:db8::1',
port: 443,
proto: 'both',
});
});
test('IPv6 with Port and Protocol', () => {
expect(
firewallTestExports.parseFirewallEntry('[2001:db8::1]:443/tcp')
).toEqual({
ip: '2001:db8::1',
port: 443,
proto: 'tcp',
});
expect(
firewallTestExports.parseFirewallEntry('[2001:db8::1]:443/udp')
).toEqual({
ip: '2001:db8::1',
port: 443,
proto: 'udp',
});
});
test('IPv6 with CIDR and Port', () => {
expect(
firewallTestExports.parseFirewallEntry('[2001:db8::/32]:443')
).toEqual({
ip: '2001:db8::/32',
port: 443,
proto: 'both',
});
});
test('IPv6 with CIDR, Port and Protocol', () => {
expect(
firewallTestExports.parseFirewallEntry('[2001:db8::/32]:443/tcp')
).toEqual({
ip: '2001:db8::/32',
port: 443,
proto: 'tcp',
});
});
});
describe('sanitizeComment', () => {
test('basic ASCII name', () => {
expect(firewallTestExports.sanitizeComment(1, 'My Laptop')).toBe(
'client 1: My Laptop'
);
});
test('strips non-ASCII and shell metacharacters', () => {
expect(firewallTestExports.sanitizeComment(42, 'café')).toBe(
'client 42: caf'
);
expect(firewallTestExports.sanitizeComment(5, 'a"; rm -rf /')).toBe(
'client 5: a rm -rf '
);
expect(firewallTestExports.sanitizeComment(7, 'test$(cmd)`id`')).toBe(
'client 7: testcmdid'
);
});
test('preserves allowed punctuation', () => {
expect(firewallTestExports.sanitizeComment(3, 'phone-2.lan_home')).toBe(
'client 3: phone-2.lan_home'
);
});
test('truncates to 256 bytes', () => {
const longName = 'a'.repeat(300);
const result = firewallTestExports.sanitizeComment(1, longName);
expect(result.length).toBeLessThanOrEqual(256);
expect(result).toBe('client 1: ' + 'a'.repeat(246));
});
});
describe('generateRuleArgs', () => {
test('includes comment when provided', () => {
const rules = firewallTestExports.generateRuleArgs(
'10.8.0.2',
{ ip: '10.0.0.1' },
'client 1: test'
);
expect(rules).toEqual([
'-A WG_CLIENTS -s 10.8.0.2 -d 10.0.0.1 -m comment --comment "client 1: test" -j ACCEPT',
]);
});
test('omits comment when not provided', () => {
const rulesTcp = firewallTestExports.generateRuleArgs('10.8.0.2', {
ip: '10.0.0.1',
port: 80,
proto: 'tcp',
});
expect(rulesTcp).toEqual([
'-A WG_CLIENTS -s 10.8.0.2 -d 10.0.0.1 -p tcp --dport 80 -j ACCEPT',
]);
const rulesUdp = firewallTestExports.generateRuleArgs('10.8.0.2', {
ip: '10.0.0.1',
port: 80,
proto: 'udp',
});
expect(rulesUdp).toEqual([
'-A WG_CLIENTS -s 10.8.0.2 -d 10.0.0.1 -p udp --dport 80 -j ACCEPT',
]);
});
test('comment with port generates two rules for both proto', () => {
const rules = firewallTestExports.generateRuleArgs(
'10.8.0.2',
{ ip: '10.0.0.1', port: 443, proto: 'both' },
'client 2: phone'
);
expect(rules).toEqual([
'-A WG_CLIENTS -s 10.8.0.2 -d 10.0.0.1 -p tcp --dport 443 -m comment --comment "client 2: phone" -j ACCEPT',
'-A WG_CLIENTS -s 10.8.0.2 -d 10.0.0.1 -p udp --dport 443 -m comment --comment "client 2: phone" -j ACCEPT',
]);
});
});
});