mirror of
https://github.com/wg-easy/wg-easy.git
synced 2026-10-22 14:52:22 +00:00
Merge branch 'master' into feat/bulk-client-selection
This commit is contained in:
commit
b4e2fba2ae
72 changed files with 6164 additions and 1767 deletions
14
.github/renovate.json
vendored
14
.github/renovate.json
vendored
|
|
@ -15,6 +15,20 @@
|
|||
"matchDatasources": ["docker"],
|
||||
"matchUpdateTypes": ["digest"]
|
||||
},
|
||||
{
|
||||
"groupName": "github-actions-digest",
|
||||
"matchManagers": ["github-actions"],
|
||||
"matchUpdateTypes": ["digest"]
|
||||
},
|
||||
{
|
||||
"matchManagers": ["npm"],
|
||||
"rangeStrategy": "bump"
|
||||
},
|
||||
{
|
||||
"groupName": "npm-non-major",
|
||||
"matchManagers": ["npm"],
|
||||
"matchUpdateTypes": ["patch", "minor", "lockFileMaintenance"]
|
||||
},
|
||||
{
|
||||
"matchManagers": ["docker-compose"],
|
||||
"pinDigests": false
|
||||
|
|
|
|||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
|
|
@ -30,14 +30,14 @@ jobs:
|
|||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4
|
||||
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4
|
||||
uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4
|
||||
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
|
|
|||
8
.github/workflows/deploy-development.yml
vendored
8
.github/workflows/deploy-development.yml
vendored
|
|
@ -45,14 +45,14 @@ jobs:
|
|||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.arch.platform }}
|
||||
|
|
@ -106,7 +106,7 @@ jobs:
|
|||
password: ${{ secrets.CODEBERG_PASS }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
|
|
|
|||
8
.github/workflows/deploy-edge.yml
vendored
8
.github/workflows/deploy-edge.yml
vendored
|
|
@ -54,14 +54,14 @@ jobs:
|
|||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.arch.platform }}
|
||||
|
|
@ -115,7 +115,7 @@ jobs:
|
|||
password: ${{ secrets.CODEBERG_PASS }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
|
|
|
|||
6
.github/workflows/deploy-pr.yml
vendored
6
.github/workflows/deploy-pr.yml
vendored
|
|
@ -32,10 +32,10 @@ jobs:
|
|||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
||||
|
|
@ -45,7 +45,7 @@ jobs:
|
|||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
|
|
|
|||
8
.github/workflows/deploy.yml
vendored
8
.github/workflows/deploy.yml
vendored
|
|
@ -53,14 +53,14 @@ jobs:
|
|||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.arch.platform }}
|
||||
|
|
@ -116,7 +116,7 @@ jobs:
|
|||
password: ${{ secrets.CODEBERG_PASS }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
|
|
|
|||
1
.node-version
Normal file
1
.node-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
lts/krypton
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
FROM docker.io/library/node:krypton-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43 AS build
|
||||
FROM docker.io/library/node:krypton-alpine@sha256:50c8e8ca1d27439048670df5883f32d57cf81cff6233222c893fd0d9884cbd81 AS build
|
||||
WORKDIR /app
|
||||
|
||||
# update corepack
|
||||
|
|
@ -17,7 +17,7 @@ RUN pnpm build
|
|||
# renovate: datasource=github-releases depName=amnezia-vpn/amneziawg-tools
|
||||
ARG AWGTOOLS_BRANCH=v3.1.20260812
|
||||
# renovate: datasource=github-tags depName=amnezia-vpn/amneziawg-go
|
||||
ARG AWGGO_BRANCH=v3.1.20260814
|
||||
ARG AWGGO_BRANCH=v3.1.20260828
|
||||
|
||||
# Build amneziawg-tools
|
||||
RUN apk add linux-headers build-base go git && \
|
||||
|
|
@ -29,13 +29,13 @@ RUN apk add linux-headers build-base go git && \
|
|||
make && \
|
||||
sed -i 's|\[\[ $proto == -4 \]\] && cmd sysctl -q net\.ipv4\.conf\.all\.src_valid_mark=1|[[ $proto == -4 ]] \&\& [[ $(sysctl -n net.ipv4.conf.all.src_valid_mark) != 1 ]] \&\& cmd sysctl -q net.ipv4.conf.all.src_valid_mark=1|' ./wg-quick/linux.bash
|
||||
|
||||
FROM docker.io/library/node:krypton-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43 AS build-libsql
|
||||
FROM docker.io/library/node:krypton-alpine@sha256:50c8e8ca1d27439048670df5883f32d57cf81cff6233222c893fd0d9884cbd81 AS build-libsql
|
||||
WORKDIR /app
|
||||
RUN npm install --no-save --omit=dev libsql
|
||||
|
||||
# Copy build result to a new image.
|
||||
# This saves a lot of disk space.
|
||||
FROM docker.io/library/node:krypton-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43
|
||||
FROM docker.io/library/node:krypton-alpine@sha256:50c8e8ca1d27439048670df5883f32d57cf81cff6233222c893fd0d9884cbd81
|
||||
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,4 +1,4 @@
|
|||
FROM docker.io/library/node:krypton-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43
|
||||
FROM docker.io/library/node:krypton-alpine@sha256:50c8e8ca1d27439048670df5883f32d57cf81cff6233222c893fd0d9884cbd81
|
||||
WORKDIR /app
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
|
|
|
|||
|
|
@ -39,14 +39,33 @@ If a parameter is not set, it will not be added to the configuration. If all Amn
|
|||
|
||||
### Parameter Compatibility Table
|
||||
|
||||
| Parameter | Can differ between server and client | Configurable on server | Configurable on client |
|
||||
| --------- | ------------------------------------ | ---------------------- | ------------------------ |
|
||||
| Jc | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
| Jmin | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
| Jmax | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
| S1-S4 | :x: No, must match | :white_check_mark: | :x: (copied from server) |
|
||||
| H1-H4 | :x: No, must match | :white_check_mark: | :x: (copied from server) |
|
||||
| I1-I5 | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
| Parameter | Can differ between server and client | Configurable on server | Configurable on client |
|
||||
| ---------------------- | ------------------------------------ | ---------------------- | ------------------------ |
|
||||
| Jc | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
| Jmin | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
| Jmax | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
| S1-S4 | :x: No, must match | :white_check_mark: | :x: (copied from server) |
|
||||
| H1-H4 | :x: No, must match | :white_check_mark: | :x: (copied from server) |
|
||||
| I1-I5 | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
| HeaderProtectionKey | :x: No, must match | :white_check_mark: | :x: (copied from server) |
|
||||
| RandomTrailers | :x: No, must match | :white_check_mark: | :x: (copied from server) |
|
||||
| ContentPaddingAddition | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
| RekeyAfterTime | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
| RekeyTimeout | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
| RejectAfterTime | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
| KeepaliveTimeout | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
| MaxHandshakeAttempts | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
| DisableCookies | :white_check_mark: Yes | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
### AmneziaWG 3.0 and 3.1 Parameters
|
||||
|
||||
These parameters require AmneziaWG 3.0 or newer on both ends of the tunnel. Older clients reject configurations that contain them, so leave them unset if you still support AmneziaWG 2.x clients.
|
||||
|
||||
`HeaderProtectionKey` is a 32 byte key in base64, in the same format as a WireGuard key. It encrypts the packet headers, so it has to be identical on both sides and is copied into every client configuration. Header protection uses the first 12 bytes of each junk prefix as its nonce, which means **S1, S2, S3 and S4 all have to be set to at least 12** while a key is configured.
|
||||
|
||||
`ContentPaddingAddition`, `RekeyAfterTime`, `RekeyTimeout`, `RejectAfterTime`, `KeepaliveTimeout` and `MaxHandshakeAttempts` accept either a single number or an inclusive range written as `lower-upper` (for example `30-90`, both bounds between 0 and 65535). When a range is given, AmneziaWG picks a random value from it, which removes the fixed timings a DPI system could otherwise fingerprint.
|
||||
|
||||
`RandomTrailers` appends a random amount of bytes to handshake packets and has to be enabled on both sides. `DisableCookies` stops this side from answering handshakes with cookie messages when it is under load and only affects the peer it is set on.
|
||||
|
||||
## Client Applications
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,18 @@ You can set these environment variables to configure the container. They are not
|
|||
| `INSECURE` | `false` | `true` | If access over http is allowed |
|
||||
| `DISABLE_IPV6` | `false` | `true` | If IPv6 support should be disabled |
|
||||
| `DISABLE_VERSION_CHECK` | `false` | `true` | If wg-easy should check for new updates |
|
||||
| `TRUSTED_PROXIES` | | `172.18.0.2,fd00:1234::/64` | Proxy IP addresses or CIDRs allowed to forward the request host |
|
||||
| `TRUSTED_PROXIES` | | `172.18.0.2,fd00:1234::/64` | Proxy IP addresses or CIDRs allowed to forward request metadata |
|
||||
| `WG_INTERFACE` | `wg0` | `wg1` | Name of the WireGuard interface |
|
||||
|
||||
/// note | IPv6 Caveats
|
||||
|
||||
Disabling IPv6 will disable the creation of the default IPv6 firewall rules and won't add a IPv6 address to the interface and clients.
|
||||
|
||||
You will however still see a IPv6 address in the Web UI, but it won't be used.
|
||||
|
||||
This option can be removed in the future, as more devices support IPv6.
|
||||
|
||||
///
|
||||
|
||||
## Trusted Proxies
|
||||
|
||||
|
|
@ -25,17 +36,18 @@ environment:
|
|||
```
|
||||
|
||||
Only add the source addresses used by your reverse proxy. wg-easy only uses
|
||||
`X-Forwarded-Host` when the request comes from one of these addresses. The
|
||||
`X-Forwarded-Host` and `X-Forwarded-For` when the request comes from one of
|
||||
these addresses. The forwarded client address is included in security logs so
|
||||
that tools such as CrowdSec can identify failed authentication attempts. The
|
||||
request protocol remains controlled by `INSECURE`. Invalid addresses prevent
|
||||
wg-easy from starting so that configuration errors are not silently ignored.
|
||||
Restart the container after changing this setting.
|
||||
|
||||
/// note | IPv6 Caveats
|
||||
## WireGuard Interface
|
||||
|
||||
Disabling IPv6 will disable the creation of the default IPv6 firewall rules and won't add a IPv6 address to the interface and clients.
|
||||
Set `WG_INTERFACE` to specify a custom interface name, useful if you are running multiple wg-easy instances on the same host with `network_mode: host`:
|
||||
|
||||
You will however still see a IPv6 address in the Web UI, but it won't be used.
|
||||
|
||||
This option can be removed in the future, as more devices support IPv6.
|
||||
|
||||
///
|
||||
```yaml
|
||||
environment:
|
||||
- WG_INTERFACE=wg1
|
||||
```
|
||||
|
|
|
|||
|
|
@ -127,13 +127,13 @@ networks:
|
|||
**_PostUp_**
|
||||
|
||||
```shell
|
||||
iptables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -t nat -A PREROUTING -i wg0 -p udp --dport 53 -j DNAT --to-destination 10.42.42.43; iptables -t nat -A PREROUTING -i wg0 -p tcp --dport 53 -j DNAT --to-destination 10.42.42.43; ip6tables -t nat -A PREROUTING -i wg0 -p udp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b; ip6tables -t nat -A PREROUTING -i wg0 -p tcp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b; iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -s {{ipv4Cidr}} -o {{device}} -j MASQUERADE; ip6tables -t nat -A POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE;
|
||||
iptables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -t nat -A PREROUTING -i {{interface}} -p udp --dport 53 -j DNAT --to-destination 10.42.42.43; iptables -t nat -A PREROUTING -i {{interface}} -p tcp --dport 53 -j DNAT --to-destination 10.42.42.43; ip6tables -t nat -A PREROUTING -i {{interface}} -p udp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b; ip6tables -t nat -A PREROUTING -i {{interface}} -p tcp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b; iptables -A FORWARD -i {{interface}} -j ACCEPT; iptables -A FORWARD -o {{interface}} -j ACCEPT; ip6tables -A FORWARD -i {{interface}} -j ACCEPT; ip6tables -A FORWARD -o {{interface}} -j ACCEPT; iptables -t nat -A POSTROUTING -s {{ipv4Cidr}} -o {{device}} -j MASQUERADE; ip6tables -t nat -A POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE;
|
||||
```
|
||||
|
||||
**_PostDown_**
|
||||
|
||||
```shell
|
||||
iptables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT || true; ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT || true; iptables -t nat -D PREROUTING -i wg0 -p udp --dport 53 -j DNAT --to-destination 10.42.42.43 || true; iptables -t nat -D PREROUTING -i wg0 -p tcp --dport 53 -j DNAT --to-destination 10.42.42.43 || true; ip6tables -t nat -D PREROUTING -i wg0 -p udp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b || true; ip6tables -t nat -D PREROUTING -i wg0 -p tcp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b || true; iptables -D FORWARD -i wg0 -j ACCEPT || true; iptables -D FORWARD -o wg0 -j ACCEPT || true; ip6tables -D FORWARD -i wg0 -j ACCEPT || true; ip6tables -D FORWARD -o wg0 -j ACCEPT || true; iptables -t nat -D POSTROUTING -s {{ipv4Cidr}} -o {{device}} -j MASQUERADE || true; ip6tables -t nat -D POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE || true;
|
||||
iptables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT || true; ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT || true; iptables -t nat -D PREROUTING -i {{interface}} -p udp --dport 53 -j DNAT --to-destination 10.42.42.43 || true; iptables -t nat -D PREROUTING -i {{interface}} -p tcp --dport 53 -j DNAT --to-destination 10.42.42.43 || true; ip6tables -t nat -D PREROUTING -i {{interface}} -p udp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b || true; ip6tables -t nat -D PREROUTING -i {{interface}} -p tcp --dport 53 -j DNAT --to-destination fdcc:ad94:bacf:61a3::2b || true; iptables -D FORWARD -i {{interface}} -j ACCEPT || true; iptables -D FORWARD -o {{interface}} -j ACCEPT || true; ip6tables -D FORWARD -i {{interface}} -j ACCEPT || true; ip6tables -D FORWARD -o {{interface}} -j ACCEPT || true; iptables -t nat -D POSTROUTING -s {{ipv4Cidr}} -o {{device}} -j MASQUERADE || true; ip6tables -t nat -D POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE || true;
|
||||
```
|
||||
|
||||
3. Restart `wg-easy` to apply changes:
|
||||
|
|
|
|||
|
|
@ -101,3 +101,17 @@ sudo docker compose up -d
|
|||
```
|
||||
|
||||
You can now access `wg-easy` at [https://wg-easy.example.com](https://wg-easy.example.com) and start the setup.
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
wg-easy does not implement rate limiting. Configure Caddy, a Caddy rate
|
||||
limiting module, or an external security tool such as CrowdSec to limit requests
|
||||
to these paths:
|
||||
|
||||
- `/api/auth/password`
|
||||
- `/api/auth/verify-2fa`
|
||||
- `/cnf/*`
|
||||
|
||||
Choose limits appropriate for your deployment. When using wg-easy security logs
|
||||
for detection, configure [`TRUSTED_PROXIES`](../../advanced/config/optional-config.md#trusted-proxies)
|
||||
so that logged events contain the original client IP address.
|
||||
|
|
|
|||
|
|
@ -88,13 +88,13 @@ In the Admin Panel of your WireGuard server, go to the `Hooks` tab and add the f
|
|||
1. PostUp
|
||||
|
||||
```shell
|
||||
nft add table inet wg_table; nft add chain inet wg_table prerouting { type nat hook prerouting priority 100 \; }; nft add chain inet wg_table postrouting { type nat hook postrouting priority 100 \; }; nft add rule inet wg_table postrouting ip saddr {{ipv4Cidr}} oifname {{device}} masquerade; nft add rule inet wg_table postrouting ip6 saddr {{ipv6Cidr}} oifname {{device}} masquerade; nft add chain inet wg_table input { type filter hook input priority 0 \; policy accept \; }; nft add rule inet wg_table input udp dport {{port}} accept; nft add rule inet wg_table input tcp dport {{uiPort}} accept; nft add chain inet wg_table forward { type filter hook forward priority 0 \; policy accept \; }; nft add rule inet wg_table forward iifname "wg0" accept; nft add rule inet wg_table forward oifname "wg0" accept;
|
||||
nft add table inet wg_table_{{interface}}; nft add chain inet wg_table_{{interface}} prerouting { type nat hook prerouting priority 100 \; }; nft add chain inet wg_table_{{interface}} postrouting { type nat hook postrouting priority 100 \; }; nft add rule inet wg_table_{{interface}} postrouting ip saddr {{ipv4Cidr}} oifname {{device}} masquerade; nft add rule inet wg_table_{{interface}} postrouting ip6 saddr {{ipv6Cidr}} oifname {{device}} masquerade; nft add chain inet wg_table_{{interface}} input { type filter hook input priority 0 \; policy accept \; }; nft add rule inet wg_table_{{interface}} input udp dport {{port}} accept; nft add rule inet wg_table_{{interface}} input tcp dport {{uiPort}} accept; nft add chain inet wg_table_{{interface}} forward { type filter hook forward priority 0 \; policy accept \; }; nft add rule inet wg_table_{{interface}} forward iifname "{{interface}}" accept; nft add rule inet wg_table_{{interface}} forward oifname "{{interface}}" accept;
|
||||
```
|
||||
|
||||
2. PostDown
|
||||
|
||||
```shell
|
||||
nft delete table inet wg_table
|
||||
nft delete table inet wg_table_{{interface}}
|
||||
```
|
||||
|
||||
If you don't have iptables loaded on your server, you could see many errors in the logs or in the UI. You can ignore them.
|
||||
|
|
|
|||
|
|
@ -86,13 +86,13 @@ Then go to Admin → Hooks and add:
|
|||
PostUp
|
||||
|
||||
```shell
|
||||
iptables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -A FORWARD -o wg0 -j ACCEPT
|
||||
iptables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -A FORWARD -i {{interface}} -j ACCEPT; iptables -A FORWARD -o {{interface}} -j ACCEPT; ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -A FORWARD -i {{interface}} -j ACCEPT; ip6tables -A FORWARD -o {{interface}} -j ACCEPT
|
||||
```
|
||||
|
||||
PostDown
|
||||
|
||||
```shell
|
||||
iptables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -D FORWARD -o wg0 -j ACCEPT
|
||||
iptables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -D FORWARD -i {{interface}} -j ACCEPT; iptables -D FORWARD -o {{interface}} -j ACCEPT; ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -D FORWARD -i {{interface}} -j ACCEPT; ip6tables -D FORWARD -o {{interface}} -j ACCEPT
|
||||
```
|
||||
|
||||
/// warning | Important: When using nftables use the following hooks instead.
|
||||
|
|
@ -100,13 +100,13 @@ iptables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; iptables -D FORWARD
|
|||
PostUp
|
||||
|
||||
```shell
|
||||
nft add chain ip filter WG_EASY; nft add rule ip filter DOCKER-USER jump WG_EASY; nft add rule ip filter WG_EASY iifname {{device}} accept; nft add rule ip filter WG_EASY oifname {{device}} accept; nft add chain ip6 filter WG_EASY; nft add rule ip6 filter DOCKER-USER jump WG_EASY; nft add rule ip6 filter WG_EASY iifname {{device}} accept; nft add rule ip6 filter WG_EASY oifname {{device}} accept;
|
||||
nft add chain ip filter WG_EASY_{{interface}}; nft add rule ip filter DOCKER-USER jump WG_EASY_{{interface}}; nft add rule ip filter WG_EASY_{{interface}} iifname {{device}} accept; nft add rule ip filter WG_EASY_{{interface}} oifname {{device}} accept; nft add chain ip6 filter WG_EASY_{{interface}}; nft add rule ip6 filter DOCKER-USER jump WG_EASY_{{interface}}; nft add rule ip6 filter WG_EASY_{{interface}} iifname {{device}} accept; nft add rule ip6 filter WG_EASY_{{interface}} oifname {{device}} accept;
|
||||
```
|
||||
|
||||
PostDown
|
||||
|
||||
```shell
|
||||
nft delete rule ip filter DOCKER-USER handle $(nft -a list chain ip filter DOCKER-USER | awk '/jump WG_EASY/ {print $NF}'); nft flush chain ip filter WG_EASY; nft delete chain ip filter WG_EASY; nft delete rule ip6 filter DOCKER-USER handle $(nft -a list chain ip6 filter DOCKER-USER | awk '/jump WG_EASY/ {print $NF}'); nft flush chain ip6 filter WG_EASY; nft delete chain ip6 filter WG_EASY
|
||||
nft delete rule ip filter DOCKER-USER handle $(nft -a list chain ip filter DOCKER-USER | awk '/jump WG_EASY_{{interface}}/ {print $NF}'); nft flush chain ip filter WG_EASY_{{interface}}; nft delete chain ip filter WG_EASY_{{interface}}; nft delete rule ip6 filter DOCKER-USER handle $(nft -a list chain ip6 filter DOCKER-USER | awk '/jump WG_EASY_{{interface}}/ {print $NF}'); nft flush chain ip6 filter WG_EASY_{{interface}}; nft delete chain ip6 filter WG_EASY_{{interface}}
|
||||
```
|
||||
|
||||
///
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ File: `/etc/docker/containers/traefik/docker-compose.yml`
|
|||
```yaml
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:3.3
|
||||
image: traefik:3.7
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
|
@ -184,3 +184,36 @@ sudo docker compose up -d
|
|||
```
|
||||
|
||||
You can now access `wg-easy` at `https://wg-easy.$example.com$` and start the setup.
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
wg-easy does not implement rate limiting. Configure a Traefik rate limit
|
||||
middleware or an external security tool such as CrowdSec to limit requests to
|
||||
these paths:
|
||||
|
||||
- `/api/auth/password`
|
||||
- `/api/auth/verify-2fa`
|
||||
- `/cnf/*`
|
||||
|
||||
Choose limits appropriate for your deployment. When using wg-easy security logs
|
||||
for detection, configure [`TRUSTED_PROXIES`](../../advanced/config/optional-config.md#trusted-proxies)
|
||||
so that logged events contain the original client IP address.
|
||||
|
||||
For example, add these labels to the `wg-easy` service to allow an average of
|
||||
five requests per minute per client IP, with a burst of five requests:
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
- 'traefik.http.routers.wg-easy-rate-limit.rule=Host(`wg-easy.$example.com$`) && (Path(`/api/auth/password`) || Path(`/api/auth/verify-2fa`) || PathPrefix(`/cnf/`))'
|
||||
- 'traefik.http.routers.wg-easy-rate-limit.entrypoints=websecure'
|
||||
- 'traefik.http.routers.wg-easy-rate-limit.service=wg-easy'
|
||||
- 'traefik.http.routers.wg-easy-rate-limit.middlewares=wg-easy-rate-limit'
|
||||
- 'traefik.http.routers.wg-easy-rate-limit.priority=100'
|
||||
- 'traefik.http.middlewares.wg-easy-rate-limit.ratelimit.average=5'
|
||||
- 'traefik.http.middlewares.wg-easy-rate-limit.ratelimit.period=1m'
|
||||
- 'traefik.http.middlewares.wg-easy-rate-limit.ratelimit.burst=5'
|
||||
```
|
||||
|
||||
The dedicated router applies the [rate limit middleware](https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/middlewares/ratelimit/)
|
||||
only to the sensitive paths. Adjust `average`, `period`, and `burst` for your
|
||||
deployment.
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
zensical==0.0.57
|
||||
zensical==0.0.62
|
||||
git+https://github.com/squidfunk/mike.git
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@
|
|||
"devDependencies": {
|
||||
"prettier": "^3.9.6"
|
||||
},
|
||||
"packageManager": "pnpm@11.22.0"
|
||||
"packageManager": "pnpm@12.4.1"
|
||||
}
|
||||
|
|
|
|||
158
pnpm-lock.yaml
generated
158
pnpm-lock.yaml
generated
|
|
@ -1,3 +1,161 @@
|
|||
---
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
configDependencies: {}
|
||||
packageManagerDependencies:
|
||||
pnpm:
|
||||
specifier: 12.4.1
|
||||
version: 12.4.1
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm/exe.android-arm64@12.4.1':
|
||||
resolution: {integrity: sha512-/HwsqXMSmlOfgtV9+O0ratzjV6Vd/8n1hh4rGHpCGmDURvt52MwZxdbhyxP4K03ZfvgSDvotFPr8O+RzE5Eu8A==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@pnpm/exe.android-x64@12.4.1':
|
||||
resolution: {integrity: sha512-+l74Qb4c2YjOzNKHXJLg+1wr8xHM1ckkUhnU5KRUK9TJqiiczt6yeTZqqzHIlJ8i6pAoj5V0OJevDRwnQKLgrQ==}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.4.1':
|
||||
resolution: {integrity: sha512-6rkZkT3iGfaxknUdGHraqSWFvTa6N0ajAHluv9Ax0GRWs0sIcGNiFhDopv6xSZCsJZmG483aNS/b6UEDy3blfw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.4.1':
|
||||
resolution: {integrity: sha512-Vb1CHlR88HghC1qUxjxjs82zQSXnXacPD+btG2CmG8Q/hBU7Q0/b2YWJKSQqZXicunV5khFtfTlAwJddV9RkYA==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.freebsd-x64@12.4.1':
|
||||
resolution: {integrity: sha512-iT3iHz3Nl0Sxxj7UPOtZ/aQ81AFsQhjoeWMAlPkSRO04gsgDGAXv3UYxOFaesMWsfNxaGn0A+CITbuCHFF66FA==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.4.1':
|
||||
resolution: {integrity: sha512-aBooZfNXM5f+OGUgCAMFWpE/kAhsWfvmqIyMtHy6zl3aNxyInWrcY/Saln/UElzo7lZWMC0Yktroxd/2J26lNQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.4.1':
|
||||
resolution: {integrity: sha512-TlOdacTTP09BgcMvwWBFRsu8VAjfwqwnslBj+XSq1JFM3ck4f3k+1O/747EuctxZxs3/o785b6Q3s7Pd92Ptsg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-ppc64@12.4.1':
|
||||
resolution: {integrity: sha512-r/ab/MlIBo75oizUP5ITiziCCrnXz4SJwfErLQ+603AshB+Yq7xTMCoMtzTSCAMu6aaymIKkAFtZvd2J75Wq0w==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-riscv64@12.4.1':
|
||||
resolution: {integrity: sha512-C/D1QWdKMiB8+wv/spl1rGITFUuqC+aI/fb6h8NB5sqxsOaGXeYc/g891oCuzggHn6SODk9p+I09hI76x/cMNw==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-s390x@12.4.1':
|
||||
resolution: {integrity: sha512-nxz5zD4yXt94uzbStDk0QTPKW+aE92hH1b5tFXK9ctB1HE9Xcq1vjTiA2LsZMFC6p4gdu5OwQeFqYNAVGa6QlA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.4.1':
|
||||
resolution: {integrity: sha512-5AwgFdGhVUg2kIweYGfxzSLEHiIG77PQhZAkXC3TwofQHsu1Wr+TrV5/rNX2PopFnHRzuE581zoB8F6Wle32yg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-x64@12.4.1':
|
||||
resolution: {integrity: sha512-FJOZuuuQMhp0oLzBtcKkLXknBI92hfkmSnlKc47vfin4HrmfID5khY2lGekL9tCzk1cpR+HShEw/meFl+nHtzQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.4.1':
|
||||
resolution: {integrity: sha512-OO7eKBL9S+xk5hRy+JUUZSNJknGuGe3GEUffsrlC2LbqKF02g+VX1anGIzSbJDvkkdnpJhSlxF5AUz2JzJu2Jw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@pnpm/exe.win32-x64@12.4.1':
|
||||
resolution: {integrity: sha512-x7gJHZgHo6hp354xCYA2NvoFzYJkHwovt2kVsUBK5EmXULLcP51JGMq09CX+5FRFJUZFKoXjLu3mZWmfP7o6PQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
pnpm@12.4.1:
|
||||
resolution: {integrity: sha512-LoHjmdc/6DkNqyXgaqeIq3pZCCSNL1o3D4K0gRR6ano2e/gEj5pv22Rg8hpm8FQt7bi5TKLIcjWWdBkgsWVtTA==}
|
||||
engines: {node: '>=18.*'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm/exe.android-arm64@12.4.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.android-x64@12.4.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.4.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.4.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.freebsd-x64@12.4.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.4.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.4.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-ppc64@12.4.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-riscv64@12.4.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-s390x@12.4.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.4.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64@12.4.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.4.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-x64@12.4.1':
|
||||
optional: true
|
||||
|
||||
pnpm@12.4.1:
|
||||
optionalDependencies:
|
||||
'@pnpm/exe.android-arm64': 12.4.1
|
||||
'@pnpm/exe.android-x64': 12.4.1
|
||||
'@pnpm/exe.darwin-arm64': 12.4.1
|
||||
'@pnpm/exe.darwin-x64': 12.4.1
|
||||
'@pnpm/exe.freebsd-x64': 12.4.1
|
||||
'@pnpm/exe.linux-arm64': 12.4.1
|
||||
'@pnpm/exe.linux-arm64-musl': 12.4.1
|
||||
'@pnpm/exe.linux-ppc64': 12.4.1
|
||||
'@pnpm/exe.linux-riscv64': 12.4.1
|
||||
'@pnpm/exe.linux-s390x': 12.4.1
|
||||
'@pnpm/exe.linux-x64': 12.4.1
|
||||
'@pnpm/exe.linux-x64-musl': 12.4.1
|
||||
'@pnpm/exe.win32-arm64': 12.4.1
|
||||
'@pnpm/exe.win32-x64': 12.4.1
|
||||
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
setups.@nuxt/test-utils="4.1.0"
|
||||
setups.@nuxt/test-utils="4.3.2"
|
||||
48
src/app/components/Form/NullSwitchField.vue
Normal file
48
src/app/components/Form/NullSwitchField.vue
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<div class="flex items-center">
|
||||
<FormLabel :for="id">
|
||||
{{ label }}
|
||||
</FormLabel>
|
||||
<BaseTooltip v-if="description" :text="description">
|
||||
<IconsInfo class="size-4" />
|
||||
</BaseTooltip>
|
||||
</div>
|
||||
<div class="my-auto flex items-center gap-3">
|
||||
<template v-if="data === null">
|
||||
<span class="text-gray-500 dark:text-neutral-300">
|
||||
{{ $t('form.nullNotSet') }}
|
||||
</span>
|
||||
<BaseSecondaryButton
|
||||
type="button"
|
||||
class="rounded-lg"
|
||||
@click="data = false"
|
||||
>
|
||||
{{ $t('form.set') }}
|
||||
</BaseSecondaryButton>
|
||||
</template>
|
||||
<template v-else>
|
||||
<BaseSwitch :id="id" v-model="enabled" />
|
||||
<BaseSecondaryButton
|
||||
type="button"
|
||||
class="rounded-lg"
|
||||
@click="data = null"
|
||||
>
|
||||
{{ $t('form.unset') }}
|
||||
</BaseSecondaryButton>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{ id: string; label: string; description?: string }>();
|
||||
|
||||
const data = defineModel<boolean | null>();
|
||||
|
||||
/** `null` is rendered as its own state, so the switch is only ever a boolean */
|
||||
const enabled = computed({
|
||||
get: () => data.value === true,
|
||||
set: (value: boolean) => {
|
||||
data.value = value;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
@ -129,6 +129,63 @@
|
|||
:label="$t('awg.i5Label')"
|
||||
:description="$t('awg.i5Description')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="headerProtectionKey"
|
||||
v-model="data.headerProtectionKey"
|
||||
:label="$t('awg.headerProtectionKeyLabel')"
|
||||
:description="$t('awg.headerProtectionKeyDescription')"
|
||||
/>
|
||||
|
||||
<div class="col-span-full text-sm">{{ $t('awg.rangeNote') }}</div>
|
||||
|
||||
<FormNullTextField
|
||||
id="contentPaddingAddition"
|
||||
v-model="data.contentPaddingAddition"
|
||||
:label="$t('awg.contentPaddingAdditionLabel')"
|
||||
:description="$t('awg.contentPaddingAdditionDescription')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="rekeyAfterTime"
|
||||
v-model="data.rekeyAfterTime"
|
||||
:label="$t('awg.rekeyAfterTimeLabel')"
|
||||
:description="$t('awg.rekeyAfterTimeDescription')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="rekeyTimeout"
|
||||
v-model="data.rekeyTimeout"
|
||||
:label="$t('awg.rekeyTimeoutLabel')"
|
||||
:description="$t('awg.rekeyTimeoutDescription')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="rejectAfterTime"
|
||||
v-model="data.rejectAfterTime"
|
||||
:label="$t('awg.rejectAfterTimeLabel')"
|
||||
:description="$t('awg.rejectAfterTimeDescription')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="keepaliveTimeout"
|
||||
v-model="data.keepaliveTimeout"
|
||||
:label="$t('awg.keepaliveTimeoutLabel')"
|
||||
:description="$t('awg.keepaliveTimeoutDescription')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="maxHandshakeAttempts"
|
||||
v-model="data.maxHandshakeAttempts"
|
||||
:label="$t('awg.maxHandshakeAttemptsLabel')"
|
||||
:description="$t('awg.maxHandshakeAttemptsDescription')"
|
||||
/>
|
||||
<FormNullSwitchField
|
||||
id="randomTrailers"
|
||||
v-model="data.randomTrailers"
|
||||
:label="$t('awg.randomTrailersLabel')"
|
||||
:description="$t('awg.randomTrailersDescription')"
|
||||
/>
|
||||
<FormNullSwitchField
|
||||
id="disableCookies"
|
||||
v-model="data.disableCookies"
|
||||
:label="$t('awg.disableCookiesLabel')"
|
||||
:description="$t('awg.disableCookiesDescription')"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormHeading>{{ $t('admin.interface.firewall') }}</FormHeading>
|
||||
|
|
|
|||
|
|
@ -144,6 +144,53 @@
|
|||
:label="$t('awg.i5Label')"
|
||||
:description="$t('awg.i5Description')"
|
||||
/>
|
||||
|
||||
<div class="col-span-full text-sm">
|
||||
{{ $t('awg.rangeNote') }}
|
||||
</div>
|
||||
|
||||
<FormNullTextField
|
||||
id="contentPaddingAddition"
|
||||
v-model="data.contentPaddingAddition"
|
||||
:label="$t('awg.contentPaddingAdditionLabel')"
|
||||
:description="$t('awg.contentPaddingAdditionDescription')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="rekeyAfterTime"
|
||||
v-model="data.rekeyAfterTime"
|
||||
:label="$t('awg.rekeyAfterTimeLabel')"
|
||||
:description="$t('awg.rekeyAfterTimeDescription')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="rekeyTimeout"
|
||||
v-model="data.rekeyTimeout"
|
||||
:label="$t('awg.rekeyTimeoutLabel')"
|
||||
:description="$t('awg.rekeyTimeoutDescription')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="rejectAfterTime"
|
||||
v-model="data.rejectAfterTime"
|
||||
:label="$t('awg.rejectAfterTimeLabel')"
|
||||
:description="$t('awg.rejectAfterTimeDescription')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="keepaliveTimeout"
|
||||
v-model="data.keepaliveTimeout"
|
||||
:label="$t('awg.keepaliveTimeoutLabel')"
|
||||
:description="$t('awg.keepaliveTimeoutDescription')"
|
||||
/>
|
||||
<FormNullTextField
|
||||
id="maxHandshakeAttempts"
|
||||
v-model="data.maxHandshakeAttempts"
|
||||
:label="$t('awg.maxHandshakeAttemptsLabel')"
|
||||
:description="$t('awg.maxHandshakeAttemptsDescription')"
|
||||
/>
|
||||
<FormNullSwitchField
|
||||
id="disableCookies"
|
||||
v-model="data.disableCookies"
|
||||
:label="$t('awg.disableCookiesLabel')"
|
||||
:description="$t('awg.disableCookiesDescription')"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormHeading :description="$t('client.hooksDescription')">
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { consola } from 'consola';
|
|||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import { db, schema } from '#cli/db';
|
||||
import { WG_ENV } from '#server/utils/config';
|
||||
import { wg } from '#server/utils/wgHelper';
|
||||
import { encodeQRCodeTerm } from '#server/utils/qr';
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ export default defineCommand({
|
|||
consola.info('Generating QR code for client...');
|
||||
|
||||
const wgInterface = await db.query.wgInterface.findFirst({
|
||||
where: eq(schema.wgInterface.name, 'wg0'),
|
||||
where: eq(schema.wgInterface.name, WG_ENV.WG_INTERFACE),
|
||||
});
|
||||
if (!wgInterface) {
|
||||
consola.error('WireGuard interface not found');
|
||||
|
|
@ -42,7 +43,7 @@ export default defineCommand({
|
|||
}
|
||||
|
||||
const userConfig = await db.query.userConfig.findFirst({
|
||||
where: eq(schema.userConfig.id, 'wg0'),
|
||||
where: eq(schema.userConfig.id, wgInterface.name),
|
||||
});
|
||||
if (!userConfig) {
|
||||
consola.error('User config not found');
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import hi from './locales/hi.json';
|
|||
import gl from './locales/gl.json';
|
||||
import cs from './locales/cs.json';
|
||||
import vi from './locales/vi.json';
|
||||
import sv from './locales/sv.json';
|
||||
import az from './locales/az.json';
|
||||
|
||||
export default defineI18nConfig(() => ({
|
||||
legacy: false,
|
||||
|
|
@ -51,5 +53,7 @@ export default defineI18nConfig(() => ({
|
|||
gl,
|
||||
cs,
|
||||
vi,
|
||||
sv,
|
||||
az,
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
334
src/i18n/locales/az.json
Normal file
334
src/i18n/locales/az.json
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
{
|
||||
"pages": {
|
||||
"me": "Hesab",
|
||||
"clients": "Müştərilər",
|
||||
"admin": {
|
||||
"panel": "İdarəetmə paneli",
|
||||
"general": "Ümumi",
|
||||
"config": "Konfiqurasiya",
|
||||
"interface": "İnterfeys",
|
||||
"hooks": "Hook-lar"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"email": "E-poçt"
|
||||
},
|
||||
"me": {
|
||||
"currentPassword": "Cari parol",
|
||||
"enable2fa": "İkifaktorlu doğrulamanı aktivləşdir",
|
||||
"enable2faDesc": "Doğrulama tətbiqinizlə QR kodu skan edin və ya açarı əl ilə daxil edin.",
|
||||
"2faKey": "TOTP açarı",
|
||||
"2faCodeDesc": "Doğrulama tətbiqinizdəki kodu daxil edin.",
|
||||
"disable2fa": "İkifaktorlu doğrulamanı söndür",
|
||||
"disable2faDesc": "İkifaktorlu doğrulamanı söndürmək üçün parolunuzu daxil edin.",
|
||||
"linkOauth": "Hesabını xarici provayderlə əlaqələndir",
|
||||
"unlinkOauth": "Əlaqəni kəs",
|
||||
"linkedWith": "{provider} ilə əlaqələndirilib",
|
||||
"providerDisabled": "Hazırda hesabınızla əlaqələndirilmiş provayder aktiv deyil"
|
||||
},
|
||||
"general": {
|
||||
"name": "Ad",
|
||||
"username": "İstifadəçi adı",
|
||||
"password": "Parol",
|
||||
"newPassword": "Yeni parol",
|
||||
"updatePassword": "Parolu yenilə",
|
||||
"addPassword": "Parol əlavə et",
|
||||
"mtu": "MTU",
|
||||
"allowedIps": "İcazə verilən IP-lər",
|
||||
"dns": "DNS",
|
||||
"persistentKeepalive": "Daimi keepalive",
|
||||
"logout": "Çıxış",
|
||||
"continue": "Davam et",
|
||||
"host": "Host",
|
||||
"port": "Port",
|
||||
"yes": "Bəli",
|
||||
"no": "Xeyr",
|
||||
"confirmPassword": "Parolu təsdiqlə",
|
||||
"loading": "Yüklənir...",
|
||||
"2fa": "İkifaktorlu doğrulama",
|
||||
"2faCode": "TOTP kodu",
|
||||
"externalAuth": "Xarici doğrulama"
|
||||
},
|
||||
"setup": {
|
||||
"welcome": "wg-easy proqramının ilkin quraşdırılmasına xoş gəlmisiniz",
|
||||
"welcomeDesc": "İstənilən Linux hostunda WireGuard quraşdırmağın və idarə etməyin ən asan yolunu tapmısınız",
|
||||
"existingSetup": "Mövcud quraşdırmanız varmı?",
|
||||
"createAdminDesc": "Əvvəlcə inzibatçı üçün istifadəçi adı və güclü, təhlükəsiz parol daxil edin. Bu məlumatlar idarəetmə panelinizə daxil olmaq üçün istifadə ediləcək.",
|
||||
"setupConfigDesc": "Host və port məlumatlarını daxil edin. Bu məlumatlar müştərilərin cihazlarında WireGuard quraşdırılarkən müştəri konfiqurasiyası üçün istifadə ediləcək.",
|
||||
"setupMigrationDesc": "Əvvəlki wg-easy versiyasındakı məlumatlarınızı yeni quraşdırmaya köçürmək istəyirsinizsə, ehtiyat nüsxə faylını təqdim edin.",
|
||||
"upload": "Yüklə",
|
||||
"migration": "Ehtiyat nüsxəni bərpa et:",
|
||||
"createAccount": "Hesab yarat",
|
||||
"successful": "Quraşdırma uğurla tamamlandı",
|
||||
"hostDesc": "Müştərilərin qoşulacağı ictimai host adı",
|
||||
"portDesc": "Müştərilərin qoşulacağı və WireGuard-ın dinləyəcəyi ictimai UDP portu"
|
||||
},
|
||||
"update": {
|
||||
"updateAvailable": "Yeniləmə mövcuddur!",
|
||||
"update": "Yenilə"
|
||||
},
|
||||
"theme": {
|
||||
"dark": "Tünd mövzu",
|
||||
"light": "Açıq mövzu",
|
||||
"system": "Sistem mövzusu"
|
||||
},
|
||||
"layout": {
|
||||
"toggleCharts": "Qrafikləri göstər/gizlət",
|
||||
"donate": "İanə et"
|
||||
},
|
||||
"login": {
|
||||
"signIn": "Daxil ol",
|
||||
"signInWith": "{provider} ilə daxil ol",
|
||||
"or": "və ya",
|
||||
"rememberMe": "Məni xatırla",
|
||||
"rememberMeDesc": "Brauzeri bağladıqdan sonra da hesabınızda qalın",
|
||||
"insecure": "Təhlükəsiz olmayan qoşulma ilə daxil ola bilməzsiniz. HTTPS istifadə edin.",
|
||||
"2faRequired": "İkifaktorlu doğrulama tələb olunur",
|
||||
"2faWrong": "İkifaktorlu doğrulama kodu yanlışdır",
|
||||
"loginExpired": "Giriş müddəti bitib. Yenidən cəhd edin"
|
||||
},
|
||||
"client": {
|
||||
"empty": "Hələ müştəri yoxdur.",
|
||||
"newShort": "Yeni",
|
||||
"sort": "Sırala",
|
||||
"create": "Müştəri yarat",
|
||||
"created": "Müştəri yaradıldı",
|
||||
"new": "Yeni müştəri",
|
||||
"name": "Ad",
|
||||
"expireDate": "Bitmə tarixi",
|
||||
"expireDateDesc": "Müştərinin söndürüləcəyi tarix. Daimi olması üçün boş saxlayın",
|
||||
"delete": "Sil",
|
||||
"deleteClient": "Müştərini sil",
|
||||
"deleteDialog": "{name} adlı müştərini silmək istədiyinizə əminsiniz? Bu əməliyyat geri qaytarıla bilməz.",
|
||||
"enabled": "Aktivdir",
|
||||
"address": "Ünvan",
|
||||
"serverAllowedIps": "Serverdə icazə verilən IP-lər",
|
||||
"otlDesc": "Qısa birdəfəlik keçid yarat",
|
||||
"permanent": "Daimi",
|
||||
"createdOn": "Yaradılma tarixi: {date}",
|
||||
"lastSeen": "Son görünmə tarixi: {date}",
|
||||
"totalDownload": "Cəmi endirilən: {total}",
|
||||
"totalUpload": "Cəmi yüklənən: {total}",
|
||||
"newClient": "Yeni müştəri",
|
||||
"disableClient": "Müştərini söndür",
|
||||
"enableClient": "Müştərini aktivləşdir",
|
||||
"noPrivKey": "Bu müştərinin məlum məxfi açarı yoxdur. Konfiqurasiya yaratmaq mümkün deyil.",
|
||||
"showQR": "QR kodu göstər",
|
||||
"downloadConfig": "Konfiqurasiyanı endir",
|
||||
"allowedIpsDesc": "VPN vasitəsilə yönləndiriləcək IP-lər (ümumi konfiqurasiyanı əvəz edir)",
|
||||
"serverAllowedIpsDesc": "Serverin müştəriyə yönləndirəcəyi IP-lər",
|
||||
"mtuDesc": "VPN tuneli üçün maksimum ötürmə vahidini (paket ölçüsünü) təyin edir",
|
||||
"persistentKeepaliveDesc": "Keepalive paketləri üçün intervalı (saniyə ilə) təyin edir. 0 onu söndürür",
|
||||
"hooks": "Hook-lar",
|
||||
"hooksDescription": "Hook-lar yalnız wg-quick ilə işləyir",
|
||||
"hooksLeaveEmpty": "Yalnız wg-quick üçündür. Əks halda boş saxlayın",
|
||||
"dnsDesc": "Müştərilərin istifadə edəcəyi DNS server (ümumi konfiqurasiyanı əvəz edir)",
|
||||
"notConnected": "Müştəri qoşulmayıb",
|
||||
"endpoint": "Ünvan",
|
||||
"endpointDesc": "WireGuard qoşulmasının qurulduğu müştərinin IP ünvanı",
|
||||
"search": "Müştəriləri axtar...",
|
||||
"config": "Konfiqurasiya",
|
||||
"viewConfig": "Konfiqurasiyaya bax",
|
||||
"firewallIps": "Firewall üçün icazə verilən IP-lər",
|
||||
"firewallIpsDesc": "Bu müştərinin çıxışına icazə verilən təyinat IP-ləri/CIDR-ləri (server tərəfində tətbiq olunur). İcazə verilən IP-lərdən istifadə etmək üçün boş saxlayın. İstəyə bağlı port və protokol filtrləməsi dəstəklənir. Sintaksis üçün sənədlərə baxın.",
|
||||
"downloadPng": "PNG endir",
|
||||
"copyPng": "PNG kopyala"
|
||||
},
|
||||
"dialog": {
|
||||
"change": "Dəyişdir",
|
||||
"cancel": "Ləğv et",
|
||||
"create": "Yarat"
|
||||
},
|
||||
"toast": {
|
||||
"success": "Uğurlu",
|
||||
"saved": "Yadda saxlanıldı",
|
||||
"error": "Xəta",
|
||||
"unknown": "Naməlum xəta. Ətraflı məlumat üçün konsola baxın"
|
||||
},
|
||||
"form": {
|
||||
"actions": "Əməliyyatlar",
|
||||
"save": "Yadda saxla",
|
||||
"revert": "Geri qaytar",
|
||||
"sectionGeneral": "Ümumi",
|
||||
"sectionAdvanced": "Əlavə",
|
||||
"noItems": "Element yoxdur",
|
||||
"nullNoItems": "Element yoxdur. Ümumi konfiqurasiya istifadə olunur",
|
||||
"nullNotSet": "Təyin edilməyib",
|
||||
"set": "Təyin et",
|
||||
"unset": "Təyinatı sil",
|
||||
"add": "Əlavə et"
|
||||
},
|
||||
"admin": {
|
||||
"general": {
|
||||
"sessionTimeout": "Sessiya vaxtı bitməsi",
|
||||
"sessionTimeoutDesc": "Məni xatırla seçimi üçün sessiya müddəti (saniyə)",
|
||||
"metrics": "Metrikalar",
|
||||
"metricsPassword": "Parol",
|
||||
"metricsPasswordDesc": "Metrika ünvanı üçün Bearer parolu (parol və ya argon2 heşi)",
|
||||
"json": "JSON",
|
||||
"jsonDesc": "JSON formatında metrikalar üçün yol",
|
||||
"prometheus": "Prometheus",
|
||||
"prometheusDesc": "Prometheus metrikaları üçün yol"
|
||||
},
|
||||
"config": {
|
||||
"connection": "Qoşulma",
|
||||
"hostDesc": "Müştərilərin qoşulacağı ictimai host adı (konfiqurasiyanı etibarsız edir)",
|
||||
"portDesc": "Müştərilərin qoşulacağı ictimai UDP portu (konfiqurasiyanı etibarsız edir, interfeys portunu da dəyişdirməyiniz lazım ola bilər)",
|
||||
"allowedIpsDesc": "Müştərilərin istifadə edəcəyi icazə verilən IP-lər (ümumi konfiqurasiya)",
|
||||
"dnsDesc": "Müştərilərin istifadə edəcəyi DNS server (ümumi konfiqurasiya)",
|
||||
"mtuDesc": "Müştərilərin istifadə edəcəyi MTU (yalnız yeni müştərilər üçün)",
|
||||
"persistentKeepaliveDesc": "Serverə keepalive paketlərinin göndərilmə intervalı, saniyə ilə. 0 = deaktiv (yalnız yeni müştərilər üçün)",
|
||||
"suggest": "Təklif et",
|
||||
"suggestDesc": "Host sahəsi üçün IP ünvanı və ya host adı seçin"
|
||||
},
|
||||
"interface": {
|
||||
"cidrSuccess": "CIDR dəyişdirildi",
|
||||
"device": "Cihaz",
|
||||
"deviceDesc": "WireGuard trafikinin yönləndiriləcəyi Ethernet cihazı",
|
||||
"routingTable": "Marşrutlaşdırma cədvəli",
|
||||
"routingTableDesc": "WireGuard marşrutları üçün marşrutlaşdırma cədvəli. auto, off və ya cədvəl nömrəsi təyin edin.",
|
||||
"mtuDesc": "WireGuard-ın istifadə edəcəyi MTU",
|
||||
"portDesc": "WireGuard-ın dinləyəcəyi UDP portu (konfiqurasiya portunu da dəyişdirməyiniz lazım ola bilər)",
|
||||
"changeCidr": "CIDR dəyişdir",
|
||||
"restart": "İnterfeysi yenidən başlat",
|
||||
"restartDesc": "WireGuard interfeysini yenidən başladın",
|
||||
"restartWarn": "İnterfeysi yenidən başlatmaq istədiyinizə əminsiniz? Bu, bütün müştərilərin qoşulmasını kəsəcək.",
|
||||
"restartSuccess": "İnterfeys yenidən başladıldı",
|
||||
"firewall": "Trafikin filtrlənməsi",
|
||||
"firewallEnabled": "Hər müştəri üçün firewall aktivləşdir",
|
||||
"firewallEnabledDesc": "iptables vasitəsilə müştəri trafikini müəyyən təyinat IP-ləri ilə məhdudlaşdırın. Aktiv olduqda hər müştəri üçün icazə verilən təyinat ünvanları konfiqurasiya edilə bilər."
|
||||
},
|
||||
"introText": "İdarəetmə panelinə xoş gəlmisiniz.\n\nBurada ümumi parametrləri, konfiqurasiyanı, interfeys parametrlərini və hook-ları idarə edə bilərsiniz.\n\nBaşlamaq üçün yan paneldəki bölmələrdən birini seçin."
|
||||
},
|
||||
"zod": {
|
||||
"generic": {
|
||||
"required": "{field} tələb olunur",
|
||||
"validNumber": "{field} etibarlı ədəd olmalıdır",
|
||||
"validNumberRange": "{field} etibarlı ədəd və ya ədəd aralığı olmalıdır",
|
||||
"validString": "{field} etibarlı mətn sətri olmalıdır",
|
||||
"validBoolean": "{field} etibarlı məntiqi dəyər olmalıdır",
|
||||
"validArray": "{field} etibarlı massiv olmalıdır",
|
||||
"stringMin": "{field} ən azı {min} simvoldan ibarət olmalıdır",
|
||||
"stringMax": "{field} ən çoxu {max} simvoldan ibarət olmalıdır",
|
||||
"numberMin": "{field} ən azı {min} olmalıdır",
|
||||
"numberMax": "{field} ən çoxu {max} olmalıdır"
|
||||
},
|
||||
"client": {
|
||||
"id": "Müştəri ID-si",
|
||||
"name": "Ad",
|
||||
"expiresAt": "Bitmə vaxtı",
|
||||
"address4": "IPv4 ünvanı",
|
||||
"address6": "IPv6 ünvanı",
|
||||
"serverAllowedIps": "Serverdə icazə verilən IP-lər",
|
||||
"firewallIps": "Firewall üçün icazə verilən IP-lər",
|
||||
"firewallIpsInvalid": "Firewall IP qeydi etibarsızdır. Dəstəklənən sintaksis üçün sənədlərə baxın."
|
||||
},
|
||||
"user": {
|
||||
"username": "İstifadəçi adı",
|
||||
"password": "Parol",
|
||||
"remember": "Xatırla",
|
||||
"name": "Ad",
|
||||
"email": "E-poçt",
|
||||
"emailInvalid": "E-poçt ünvanı etibarlı olmalıdır",
|
||||
"passwordMatch": "Parollar uyğun gəlməlidir",
|
||||
"totpEnable": "TOTP aktivləşdir",
|
||||
"totpEnableTrue": "TOTP aktivləşdir seçimi aktiv olmalıdır",
|
||||
"totpCode": "TOTP kodu"
|
||||
},
|
||||
"userConfig": {
|
||||
"host": "Host"
|
||||
},
|
||||
"general": {
|
||||
"sessionTimeout": "Sessiya vaxtı bitməsi",
|
||||
"metricsEnabled": "Metrikalar",
|
||||
"metricsPassword": "Metrika parolu"
|
||||
},
|
||||
"interface": {
|
||||
"cidr": "CIDR",
|
||||
"device": "Cihaz",
|
||||
"cidrValid": "CIDR etibarlı olmalıdır",
|
||||
"routingTable": "Marşrutlaşdırma cədvəli auto, off və ya cədvəl nömrəsi olmalıdır",
|
||||
"headerProtectionJunkSize": "Başlığı qoruma açarı təyin edildikdə S1-S4 saxta məlumat ölçüləri ən azı 12 olmalıdır"
|
||||
},
|
||||
"otl": "Birdəfəlik keçid",
|
||||
"stringMalformed": "Mətn sətrinin formatı yanlışdır",
|
||||
"keyMalformed": "Açar base64 ilə kodlaşdırılmış 32 baytlıq açar olmalıdır",
|
||||
"body": "Sorğunun gövdəsi etibarlı obyekt olmalıdır",
|
||||
"hook": "Hook",
|
||||
"enabled": "Aktivdir",
|
||||
"mtu": "MTU",
|
||||
"port": "Port",
|
||||
"persistentKeepalive": "Daimi keepalive",
|
||||
"address": "IP ünvanı",
|
||||
"dns": "DNS",
|
||||
"allowedIps": "İcazə verilən IP-lər",
|
||||
"file": "Fayl"
|
||||
},
|
||||
"hooks": {
|
||||
"preUp": "PreUp",
|
||||
"postUp": "PostUp",
|
||||
"preDown": "PreDown",
|
||||
"postDown": "PostDown"
|
||||
},
|
||||
"copy": {
|
||||
"notSupported": "Kopyalama dəstəklənmir",
|
||||
"copied": "Kopyalandı!",
|
||||
"failed": "Kopyalama alınmadı",
|
||||
"copy": "Kopyala"
|
||||
},
|
||||
"awg": {
|
||||
"jCLabel": "Saxta paketlərin sayı (Jc)",
|
||||
"jCDescription": "Göndəriləcək saxta paketlərin sayı (1-128, tövsiyə olunur: 4-12)",
|
||||
"jMinLabel": "Saxta paketlərin minimum ölçüsü (Jmin)",
|
||||
"jMinDescription": "Saxta paketlərin minimum ölçüsü (0-1279*, tövsiyə olunur: 8, < Jmax olmalıdır)",
|
||||
"jMaxLabel": "Saxta paketlərin maksimum ölçüsü (Jmax)",
|
||||
"jMaxDescription": "Saxta paketlərin maksimum ölçüsü (1-1280*, tövsiyə olunur: 80, > Jmin olmalıdır)",
|
||||
"s1Label": "Başlatma paketindəki saxta məlumatların ölçüsü (S1)",
|
||||
"s1Description": "Başlatma paketindəki saxta məlumatların ölçüsü (0-1132[1280* - 148 = 1132], tövsiyə olunur: 15-150, S1+56 ≠ S2)",
|
||||
"s2Label": "Cavab paketindəki saxta məlumatların ölçüsü (S2)",
|
||||
"s2Description": "Cavab paketindəki saxta məlumatların ölçüsü (0-1188[1280* - 92 = 1188], tövsiyə olunur: 15-150)",
|
||||
"s3Label": "Kuki cavab paketindəki saxta məlumatların ölçüsü (S3)",
|
||||
"s3Description": "Kuki cavab paketindəki saxta məlumatların ölçüsü",
|
||||
"s4Label": "Nəql paketindəki saxta məlumatların ölçüsü (S4)",
|
||||
"s4Description": "Nəql paketindəki saxta məlumatların ölçüsü",
|
||||
"h1Label": "Başlatma paketinin xüsusi başlığı (H1)",
|
||||
"h1Description": "Başlatma paketinin başlıq dəyəri və ya aralığı (X və ya X-Y, burada X<Y. Minimum 5, maksimum 2147483647. Dəyər və ya aralıq digər başlıqlarla üst-üstə düşməməlidir)",
|
||||
"h2Label": "Cavab paketinin xüsusi başlığı (H2)",
|
||||
"h2Description": "Cavab paketinin başlıq dəyəri və ya aralığı (X və ya X-Y, burada X<Y. Minimum 5, maksimum 2147483647. Dəyər və ya aralıq digər başlıqlarla üst-üstə düşməməlidir)",
|
||||
"h3Label": "Kuki cavab paketinin xüsusi başlığı (H3)",
|
||||
"h3Description": "Kuki cavab paketinin başlıq dəyəri və ya aralığı (X və ya X-Y, burada X<Y. Minimum 5, maksimum 2147483647. Dəyər və ya aralıq digər başlıqlarla üst-üstə düşməməlidir)",
|
||||
"h4Label": "Nəql paketinin xüsusi başlığı (H4)",
|
||||
"h4Description": "Nəql paketinin başlıq dəyəri və ya aralığı (X və ya X-Y, burada X<Y. Minimum 5, maksimum 2147483647. Dəyər və ya aralıq digər başlıqlarla üst-üstə düşməməlidir)",
|
||||
"i1Label": "Xüsusi saxta paket 1 (I1)",
|
||||
"i1Description": "Protokolu təqlid edən paket, onaltılıq formatda: <b 0x...>",
|
||||
"i2Label": "Xüsusi saxta paket 2 (I2)",
|
||||
"i2Description": "Protokolu təqlid edən paket, onaltılıq formatda: <b 0x...>",
|
||||
"i3Label": "Xüsusi saxta paket 3 (I3)",
|
||||
"i3Description": "Protokolu təqlid edən paket, onaltılıq formatda: <b 0x...>",
|
||||
"i4Label": "Xüsusi saxta paket 4 (I4)",
|
||||
"i4Description": "Protokolu təqlid edən paket, onaltılıq formatda: <b 0x...>",
|
||||
"i5Label": "Xüsusi saxta paket 5 (I5)",
|
||||
"i5Description": "Protokolu təqlid edən paket, onaltılıq formatda: <b 0x...>",
|
||||
"rangeNote": "Aşağıdakı parametrlər tək ədəd və ya sərhəd dəyərləri daxil olan aralıq (0-65535) qəbul edir, məsələn, 30 və ya 30-90",
|
||||
"headerProtectionKeyLabel": "Başlığı qoruma açarı",
|
||||
"headerProtectionKeyDescription": "Paket başlıqlarını şifrələmək üçün istifadə olunan 32 baytlıq base64 açarı. Hər iki tərəfdə eyni olmalıdır, buna görə hər müştəriyə kopyalanır. S1-S4 ölçülərinin ən azı 12 olmasını tələb edir",
|
||||
"contentPaddingAdditionLabel": "Məzmun üçün doldurma baytlarının əlavə edilməsi",
|
||||
"contentPaddingAdditionDescription": "Şifrələmədən əvvəl nəql paketlərinin sonuna əlavə olunan təsadüfi sayda bayt",
|
||||
"rekeyAfterTimeLabel": "Açarın yenilənməsinə qədər müddət",
|
||||
"rekeyAfterTimeDescription": "Sessiyanın yenidən razılaşdırılmasına qədər saniyə sayı (WireGuard standartı: 120)",
|
||||
"rekeyTimeoutLabel": "Açarın yenilənməsi üçün gözləmə müddəti",
|
||||
"rekeyTimeoutDescription": "Əl sıxma üçün yenidən cəhdlər arasındakı saniyə sayı (WireGuard standartı: 5)",
|
||||
"rejectAfterTimeLabel": "Rədd edilməyə qədər müddət",
|
||||
"rejectAfterTimeDescription": "Sessiyanın saniyə ilə ömrü (WireGuard standartı: 180)",
|
||||
"keepaliveTimeoutLabel": "Keepalive üçün gözləmə müddəti",
|
||||
"keepaliveTimeoutDescription": "Keepalive göndərilməsinə qədər fəaliyyətsiz keçən saniyə sayı (WireGuard standartı: 10)",
|
||||
"maxHandshakeAttemptsLabel": "Əl sıxma cəhdlərinin maksimum sayı",
|
||||
"maxHandshakeAttemptsDescription": "İmtina edilənə qədər əl sıxma cəhdlərinin sayı (WireGuard standartı: 18)",
|
||||
"randomTrailersLabel": "Təsadüfi sonluqlar",
|
||||
"randomTrailersDescription": "Əl sıxma paketlərinin sonuna təsadüfi sayda bayt əlavə edir. Hər iki tərəfdə eyni olmalıdır, buna görə hər müştəriyə kopyalanır",
|
||||
"disableCookiesLabel": "Kukiləri söndür",
|
||||
"disableCookiesDescription": "Yük altında olduqda əl sıxmalara kuki mesajları ilə cavab vermir. Yalnız tunelin bu tərəfinə təsir edir",
|
||||
"mtuNote": "Dəyərlər MTU-dan asılıdır",
|
||||
"obfuscationParameters": "AmneziaWG gizlətmə parametrləri"
|
||||
}
|
||||
}
|
||||
|
|
@ -160,6 +160,9 @@
|
|||
"sectionAdvanced": "Advanced",
|
||||
"noItems": "No items",
|
||||
"nullNoItems": "No items. Using global config",
|
||||
"nullNotSet": "Not set",
|
||||
"set": "Set",
|
||||
"unset": "Unset",
|
||||
"add": "Add"
|
||||
},
|
||||
"admin": {
|
||||
|
|
@ -251,10 +254,12 @@
|
|||
"cidr": "CIDR",
|
||||
"device": "Device",
|
||||
"cidrValid": "CIDR must be valid",
|
||||
"routingTable": "Routing table must be auto, off, or a table number"
|
||||
"routingTable": "Routing table must be auto, off, or a table number",
|
||||
"headerProtectionJunkSize": "Junk sizes S1-S4 must be at least 12 when a Header Protection Key is set"
|
||||
},
|
||||
"otl": "One Time link",
|
||||
"stringMalformed": "String is malformed",
|
||||
"keyMalformed": "Key must be a 32 byte base64 encoded key",
|
||||
"body": "Body must be a valid object",
|
||||
"hook": "Hook",
|
||||
"enabled": "Enabled",
|
||||
|
|
@ -311,6 +316,25 @@
|
|||
"i4Description": "Protocol mimic packet in hex format: <b 0x...>",
|
||||
"i5Label": "Special junk packet 5 (I5)",
|
||||
"i5Description": "Protocol mimic packet in hex format: <b 0x...>",
|
||||
"rangeNote": "The following parameters accept a single number or an inclusive range (0-65535), for example 30 or 30-90",
|
||||
"headerProtectionKeyLabel": "Header protection key",
|
||||
"headerProtectionKeyDescription": "32 byte base64 key used to encrypt packet headers. Must match on both sides, so it is copied to every client. Requires S1-S4 to be at least 12",
|
||||
"contentPaddingAdditionLabel": "Content padding addition",
|
||||
"contentPaddingAdditionDescription": "Random amount of bytes appended to transport packets before encryption",
|
||||
"rekeyAfterTimeLabel": "Rekey after time",
|
||||
"rekeyAfterTimeDescription": "Seconds before a session is renegotiated (WireGuard default: 120)",
|
||||
"rekeyTimeoutLabel": "Rekey timeout",
|
||||
"rekeyTimeoutDescription": "Seconds between handshake retries (WireGuard default: 5)",
|
||||
"rejectAfterTimeLabel": "Reject after time",
|
||||
"rejectAfterTimeDescription": "Session lifetime in seconds (WireGuard default: 180)",
|
||||
"keepaliveTimeoutLabel": "Keepalive timeout",
|
||||
"keepaliveTimeoutDescription": "Idle seconds before a keepalive is sent (WireGuard default: 10)",
|
||||
"maxHandshakeAttemptsLabel": "Max handshake attempts",
|
||||
"maxHandshakeAttemptsDescription": "Handshake attempts before giving up (WireGuard default: 18)",
|
||||
"randomTrailersLabel": "Random trailers",
|
||||
"randomTrailersDescription": "Append a random amount of bytes to handshake packets. Must match on both sides, so it is copied to every client",
|
||||
"disableCookiesLabel": "Disable cookies",
|
||||
"disableCookiesDescription": "Do not answer handshakes with cookie messages when under load. Only affects this side of the tunnel",
|
||||
"mtuNote": "Values depend on the MTU",
|
||||
"obfuscationParameters": "AmneziaWG Obfuscation Parameters"
|
||||
}
|
||||
|
|
|
|||
310
src/i18n/locales/sv.json
Normal file
310
src/i18n/locales/sv.json
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
{
|
||||
"pages": {
|
||||
"me": "Konto",
|
||||
"clients": "Klienter",
|
||||
"admin": {
|
||||
"panel": "Adminpanel",
|
||||
"general": "Allmänt",
|
||||
"config": "Konfiguration",
|
||||
"interface": "Gränssnitt",
|
||||
"hooks": "Hooks"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"email": "E-post"
|
||||
},
|
||||
"me": {
|
||||
"currentPassword": "Nuvarande lösenord",
|
||||
"enable2fa": "Aktivera tvåfaktorsautentisering",
|
||||
"enable2faDesc": "Skanna QR-koden med din autentiseringsapp eller ange nyckeln manuellt.",
|
||||
"2faKey": "TOTP-nyckel",
|
||||
"2faCodeDesc": "Ange koden från din autentiseringsapp.",
|
||||
"disable2fa": "Inaktivera tvåfaktorsautentisering",
|
||||
"disable2faDesc": "Ange ditt lösenord för att inaktivera tvåfaktorsautentisering.",
|
||||
"linkOauth": "Länka ditt konto till en extern leverantör",
|
||||
"unlinkOauth": "Ta bort länk",
|
||||
"linkedWith": "Länkat till {provider}",
|
||||
"providerDisabled": "Din länkade leverantör är inte aktiverad"
|
||||
},
|
||||
"general": {
|
||||
"name": "Namn",
|
||||
"username": "Användarnamn",
|
||||
"password": "Lösenord",
|
||||
"newPassword": "Nytt lösenord",
|
||||
"updatePassword": "Uppdatera lösenord",
|
||||
"addPassword": "Lägg till lösenord",
|
||||
"mtu": "MTU",
|
||||
"allowedIps": "Tillåtna IP-adresser",
|
||||
"dns": "DNS",
|
||||
"persistentKeepalive": "Persistent Keepalive",
|
||||
"logout": "Logga ut",
|
||||
"continue": "Fortsätt",
|
||||
"host": "Värd",
|
||||
"port": "Port",
|
||||
"yes": "Ja",
|
||||
"no": "Nej",
|
||||
"confirmPassword": "Bekräfta lösenord",
|
||||
"loading": "Laddar...",
|
||||
"2fa": "Tvåfaktorsautentisering",
|
||||
"2faCode": "TOTP-kod",
|
||||
"externalAuth": "Extern autentisering"
|
||||
},
|
||||
"setup": {
|
||||
"welcome": "Välkommen till den första konfigurationen av wg-easy",
|
||||
"welcomeDesc": "Du har hittat det enklaste sättet att installera och hantera WireGuard på en Linux-värd",
|
||||
"existingSetup": "Har du en befintlig konfiguration?",
|
||||
"createAdminDesc": "Ange först ett administratörsanvändarnamn och ett starkt, säkert lösenord. Dessa uppgifter används för att logga in på adminpanelen.",
|
||||
"setupConfigDesc": "Ange värd- och portinformation. Den används i klientkonfigurationen när WireGuard konfigureras på klienternas enheter.",
|
||||
"setupMigrationDesc": "Ange säkerhetskopian om du vill migrera data från din tidigare version av wg-easy till den nya installationen.",
|
||||
"upload": "Ladda upp",
|
||||
"migration": "Återställ säkerhetskopian:",
|
||||
"createAccount": "Skapa konto",
|
||||
"successful": "Konfigurationen slutfördes",
|
||||
"hostDesc": "Publikt värdnamn som klienterna ansluter till",
|
||||
"portDesc": "Publik UDP-port som klienterna ansluter till och som WireGuard lyssnar på"
|
||||
},
|
||||
"update": {
|
||||
"updateAvailable": "Det finns en tillgänglig uppdatering!",
|
||||
"update": "Uppdatera"
|
||||
},
|
||||
"theme": {
|
||||
"dark": "Mörkt tema",
|
||||
"light": "Ljust tema",
|
||||
"system": "Systemtema"
|
||||
},
|
||||
"layout": {
|
||||
"toggleCharts": "Visa/dölj diagram",
|
||||
"donate": "Donera"
|
||||
},
|
||||
"login": {
|
||||
"signIn": "Logga in",
|
||||
"signInWith": "Logga in med {provider}",
|
||||
"or": "eller",
|
||||
"rememberMe": "Kom ihåg mig",
|
||||
"rememberMeDesc": "Fortsätt vara inloggad efter att webbläsaren stängts",
|
||||
"insecure": "Du kan inte logga in via en osäker anslutning. Använd HTTPS.",
|
||||
"2faRequired": "Tvåfaktorsautentisering krävs",
|
||||
"2faWrong": "Koden för tvåfaktorsautentisering är felaktig",
|
||||
"loginExpired": "Inloggningen har löpt ut. Försök igen"
|
||||
},
|
||||
"client": {
|
||||
"empty": "Det finns inga klienter ännu.",
|
||||
"newShort": "Ny",
|
||||
"sort": "Sortera",
|
||||
"create": "Skapa klient",
|
||||
"created": "Klienten skapades",
|
||||
"new": "Ny klient",
|
||||
"name": "Namn",
|
||||
"expireDate": "Utgångsdatum",
|
||||
"expireDateDesc": "Datum då klienten inaktiveras. Lämna tomt för permanent åtkomst",
|
||||
"delete": "Ta bort",
|
||||
"deleteClient": "Ta bort klient",
|
||||
"deleteDialog": "Är du säker på att du vill ta bort {name}? Åtgärden kan inte ångras.",
|
||||
"enabled": "Aktiverad",
|
||||
"address": "Adress",
|
||||
"serverAllowedIps": "Serverns tillåtna IP-adresser",
|
||||
"otlDesc": "Generera en kort engångslänk",
|
||||
"permanent": "Permanent",
|
||||
"createdOn": "Skapad {date}",
|
||||
"lastSeen": "Senast sedd {date}",
|
||||
"totalDownload": "Totalt nedladdat: {total}",
|
||||
"totalUpload": "Totalt uppladdat: {total}",
|
||||
"newClient": "Ny klient",
|
||||
"disableClient": "Inaktivera klient",
|
||||
"enableClient": "Aktivera klient",
|
||||
"noPrivKey": "Klienten saknar en känd privat nyckel. Konfigurationen kan inte skapas.",
|
||||
"showQR": "Visa QR-kod",
|
||||
"downloadConfig": "Ladda ner konfiguration",
|
||||
"allowedIpsDesc": "Vilka IP-adresser som ska routas genom VPN-anslutningen (åsidosätter den globala konfigurationen)",
|
||||
"serverAllowedIpsDesc": "Vilka IP-adresser servern ska routa till klienten",
|
||||
"mtuDesc": "Anger maximal överföringsenhet (paketstorlek) för VPN-tunneln",
|
||||
"persistentKeepaliveDesc": "Anger intervallet i sekunder för keepalive-paket. 0 inaktiverar funktionen",
|
||||
"hooks": "Hooks",
|
||||
"hooksDescription": "Hooks fungerar endast med wg-quick",
|
||||
"hooksLeaveEmpty": "Endast för wg-quick. Lämna annars tomt",
|
||||
"dnsDesc": "DNS-server som klienterna ska använda (åsidosätter den globala konfigurationen)",
|
||||
"notConnected": "Klienten är inte ansluten",
|
||||
"endpoint": "Slutpunkt",
|
||||
"endpointDesc": "Klientens IP-adress från vilken WireGuard-anslutningen upprättas",
|
||||
"search": "Sök klienter...",
|
||||
"config": "Konfiguration",
|
||||
"viewConfig": "Visa konfiguration",
|
||||
"firewallIps": "Tillåtna IP-adresser i brandväggen",
|
||||
"firewallIpsDesc": "Mål-IP-adresser/CIDR som klienten får åtkomst till (tillämpas på serversidan). Lämna tomt för att använda Tillåtna IP-adresser. Valfri filtrering efter port och protokoll stöds. Se dokumentationen för syntax.",
|
||||
"downloadPng": "Ladda ner PNG",
|
||||
"copyPng": "Kopiera PNG"
|
||||
},
|
||||
"dialog": {
|
||||
"change": "Ändra",
|
||||
"cancel": "Avbryt",
|
||||
"create": "Skapa"
|
||||
},
|
||||
"toast": {
|
||||
"success": "Klart",
|
||||
"saved": "Sparat",
|
||||
"error": "Fel",
|
||||
"unknown": "Okänt fel. Se konsolen för mer information"
|
||||
},
|
||||
"form": {
|
||||
"actions": "Åtgärder",
|
||||
"save": "Spara",
|
||||
"revert": "Återställ",
|
||||
"sectionGeneral": "Allmänt",
|
||||
"sectionAdvanced": "Avancerat",
|
||||
"noItems": "Inga objekt",
|
||||
"nullNoItems": "Inga objekt. Den globala konfigurationen används",
|
||||
"add": "Lägg till"
|
||||
},
|
||||
"admin": {
|
||||
"general": {
|
||||
"sessionTimeout": "Sessionstimeout",
|
||||
"sessionTimeoutDesc": "Sessionslängd för Kom ihåg mig (sekunder)",
|
||||
"metrics": "Mätvärden",
|
||||
"metricsPassword": "Lösenord",
|
||||
"metricsPasswordDesc": "Bearer-lösenord för endpointen för mätvärden (lösenord eller Argon2-hash)",
|
||||
"json": "JSON",
|
||||
"jsonDesc": "Sökväg för mätvärden i JSON-format",
|
||||
"prometheus": "Prometheus",
|
||||
"prometheusDesc": "Sökväg för Prometheus-mätvärden"
|
||||
},
|
||||
"config": {
|
||||
"connection": "Anslutning",
|
||||
"hostDesc": "Publikt värdnamn som klienterna ansluter till (ogiltigförklarar konfigurationen)",
|
||||
"portDesc": "Publik UDP-port som klienterna ansluter till (ogiltigförklarar konfigurationen, du bör sannolikt även ändra gränssnittets port)",
|
||||
"allowedIpsDesc": "Tillåtna IP-adresser som klienterna använder (global konfiguration)",
|
||||
"dnsDesc": "DNS-server som klienterna använder (global konfiguration)",
|
||||
"mtuDesc": "MTU som klienterna använder (endast för nya klienter)",
|
||||
"persistentKeepaliveDesc": "Intervall i sekunder för att skicka keepalive-paket till servern. 0 = inaktiverat (endast för nya klienter)",
|
||||
"suggest": "Föreslå",
|
||||
"suggestDesc": "Välj en IP-adress eller ett värdnamn för värdfältet"
|
||||
},
|
||||
"interface": {
|
||||
"cidrSuccess": "CIDR ändrades",
|
||||
"device": "Enhet",
|
||||
"deviceDesc": "Ethernet-enhet som WireGuard-trafiken ska vidarebefordras genom",
|
||||
"routingTable": "Routingtabell",
|
||||
"routingTableDesc": "Routingtabell för WireGuard-rutter. Ange auto, off eller ett tabellnummer.",
|
||||
"mtuDesc": "MTU som WireGuard använder",
|
||||
"portDesc": "UDP-port som WireGuard lyssnar på (du bör sannolikt även ändra porten under Konfiguration)",
|
||||
"changeCidr": "Ändra CIDR",
|
||||
"restart": "Starta om gränssnitt",
|
||||
"restartDesc": "Starta om WireGuard-gränssnittet",
|
||||
"restartWarn": "Är du säker på att du vill starta om gränssnittet? Alla klienter kopplas från.",
|
||||
"restartSuccess": "Gränssnittet startades om",
|
||||
"firewall": "Trafikfiltrering",
|
||||
"firewallEnabled": "Aktivera brandvägg per klient",
|
||||
"firewallEnabledDesc": "Begränsa klienttrafik till specifika mål-IP-adresser med iptables. När funktionen är aktiverad kan varje klient konfigureras med tillåtna destinationer."
|
||||
},
|
||||
"introText": "Välkommen till adminpanelen.\n\nHär kan du hantera allmänna inställningar, konfigurationen, gränssnittsinställningar och hooks.\n\nBörja genom att välja ett av avsnitten i sidofältet."
|
||||
},
|
||||
"zod": {
|
||||
"generic": {
|
||||
"required": "{field} krävs",
|
||||
"validNumber": "{field} måste vara ett giltigt tal",
|
||||
"validNumberRange": "{field} måste vara ett giltigt tal eller talintervall",
|
||||
"validString": "{field} måste vara en giltig sträng",
|
||||
"validBoolean": "{field} måste vara ett giltigt booleskt värde",
|
||||
"validArray": "{field} måste vara en giltig array",
|
||||
"stringMin": "{field} måste innehålla minst {min} tecken",
|
||||
"stringMax": "{field} får innehålla högst {max} tecken",
|
||||
"numberMin": "{field} måste vara minst {min}",
|
||||
"numberMax": "{field} får vara högst {max}"
|
||||
},
|
||||
"client": {
|
||||
"id": "Klient-ID",
|
||||
"name": "Namn",
|
||||
"expiresAt": "Går ut",
|
||||
"address4": "IPv4-adress",
|
||||
"address6": "IPv6-adress",
|
||||
"serverAllowedIps": "Serverns tillåtna IP-adresser",
|
||||
"firewallIps": "Tillåtna IP-adresser i brandväggen",
|
||||
"firewallIpsInvalid": "Ogiltig IP-post för brandväggen. Se dokumentationen för syntax som stöds."
|
||||
},
|
||||
"user": {
|
||||
"username": "Användarnamn",
|
||||
"password": "Lösenord",
|
||||
"remember": "Kom ihåg",
|
||||
"name": "Namn",
|
||||
"email": "E-post",
|
||||
"emailInvalid": "E-postadressen måste vara giltig",
|
||||
"passwordMatch": "Lösenorden måste matcha",
|
||||
"totpEnable": "Aktivera TOTP",
|
||||
"totpEnableTrue": "TOTP måste vara aktiverat",
|
||||
"totpCode": "TOTP-kod"
|
||||
},
|
||||
"userConfig": {
|
||||
"host": "Värd"
|
||||
},
|
||||
"general": {
|
||||
"sessionTimeout": "Sessionstimeout",
|
||||
"metricsEnabled": "Mätvärden",
|
||||
"metricsPassword": "Lösenord för mätvärden"
|
||||
},
|
||||
"interface": {
|
||||
"cidr": "CIDR",
|
||||
"device": "Enhet",
|
||||
"cidrValid": "CIDR måste vara giltigt",
|
||||
"routingTable": "Routingtabellen måste vara auto, off eller ett tabellnummer"
|
||||
},
|
||||
"otl": "Engångslänk",
|
||||
"stringMalformed": "Strängen har felaktigt format",
|
||||
"body": "Innehållet måste vara ett giltigt objekt",
|
||||
"hook": "Hook",
|
||||
"enabled": "Aktiverad",
|
||||
"mtu": "MTU",
|
||||
"port": "Port",
|
||||
"persistentKeepalive": "Persistent Keepalive",
|
||||
"address": "IP-adress",
|
||||
"dns": "DNS",
|
||||
"allowedIps": "Tillåtna IP-adresser",
|
||||
"file": "Fil"
|
||||
},
|
||||
"hooks": {
|
||||
"preUp": "PreUp",
|
||||
"postUp": "PostUp",
|
||||
"preDown": "PreDown",
|
||||
"postDown": "PostDown"
|
||||
},
|
||||
"copy": {
|
||||
"notSupported": "Kopiering stöds inte",
|
||||
"copied": "Kopierat!",
|
||||
"failed": "Kopieringen misslyckades",
|
||||
"copy": "Kopiera"
|
||||
},
|
||||
"awg": {
|
||||
"jCLabel": "Antal skräppaket (Jc)",
|
||||
"jCDescription": "Antal skräppaket som ska skickas (1–128, rekommenderat: 4–12)",
|
||||
"jMinLabel": "Minsta storlek på skräppaket (Jmin)",
|
||||
"jMinDescription": "Minsta storlek på skräppaket (0–1279*, rekommenderat: 8, måste vara < Jmax)",
|
||||
"jMaxLabel": "Största storlek på skräppaket (Jmax)",
|
||||
"jMaxDescription": "Största storlek på skräppaket (1–1280*, rekommenderat: 80, måste vara > Jmin)",
|
||||
"s1Label": "Skräpstorlek för initieringspaket (S1)",
|
||||
"s1Description": "Skräpstorlek för initieringspaket (0–1132[1280* - 148 = 1132], rekommenderat: 15–150, S1+56 ≠ S2)",
|
||||
"s2Label": "Skräpstorlek för svarspaket (S2)",
|
||||
"s2Description": "Skräpstorlek för svarspaket (0–1188[1280* - 92 = 1188], rekommenderat: 15–150)",
|
||||
"s3Label": "Skräpstorlek för cookie-svarspaket (S3)",
|
||||
"s3Description": "Skräpstorlek för cookie-svarspaket",
|
||||
"s4Label": "Skräpstorlek för transportpaket (S4)",
|
||||
"s4Description": "Skräpstorlek för transportpaket",
|
||||
"h1Label": "Magisk header för initieringspaket (H1)",
|
||||
"h1Description": "Headervärde eller intervall för initieringspaket (X eller X-Y, där X<Y. Min 5, max 2147483647. Värdet eller intervallet får inte överlappa andra headers)",
|
||||
"h2Label": "Magisk header för svarspaket (H2)",
|
||||
"h2Description": "Headervärde eller intervall för svarspaket (X eller X-Y, där X<Y. Min 5, max 2147483647. Värdet eller intervallet får inte överlappa andra headers)",
|
||||
"h3Label": "Magisk header för cookie-svarspaket (H3)",
|
||||
"h3Description": "Headervärde eller intervall för cookie-svarspaket (X eller X-Y, där X<Y. Min 5, max 2147483647. Värdet eller intervallet får inte överlappa andra headers)",
|
||||
"h4Label": "Magisk header för transportpaket (H4)",
|
||||
"h4Description": "Headervärde eller intervall för transportpaket (X eller X-Y, där X<Y. Min 5, max 2147483647. Värdet eller intervallet får inte överlappa andra headers)",
|
||||
"i1Label": "Särskilt skräppaket 1 (I1)",
|
||||
"i1Description": "Protokollefterliknande paket i hexformat: <b 0x...>",
|
||||
"i2Label": "Särskilt skräppaket 2 (I2)",
|
||||
"i2Description": "Protokollefterliknande paket i hexformat: <b 0x...>",
|
||||
"i3Label": "Särskilt skräppaket 3 (I3)",
|
||||
"i3Description": "Protokollefterliknande paket i hexformat: <b 0x...>",
|
||||
"i4Label": "Särskilt skräppaket 4 (I4)",
|
||||
"i4Description": "Protokollefterliknande paket i hexformat: <b 0x...>",
|
||||
"i5Label": "Särskilt skräppaket 5 (I5)",
|
||||
"i5Description": "Protokollefterliknande paket i hexformat: <b 0x...>",
|
||||
"mtuNote": "Värdena beror på MTU",
|
||||
"obfuscationParameters": "Obfuskeringsparametrar för AmneziaWG"
|
||||
}
|
||||
}
|
||||
|
|
@ -159,6 +159,9 @@
|
|||
"sectionAdvanced": "Gelişmiş",
|
||||
"noItems": "Öge yok",
|
||||
"nullNoItems": "Öge yok. Genel yapılandırma kullanılıyor.",
|
||||
"nullNotSet": "Ayarlanmadı",
|
||||
"set": "Ayarla",
|
||||
"unset": "Kaldır",
|
||||
"add": "Ekle"
|
||||
},
|
||||
"admin": {
|
||||
|
|
@ -250,10 +253,12 @@
|
|||
"cidr": "CIDR",
|
||||
"device": "Aygıt",
|
||||
"cidrValid": "Geçersiz CIDR",
|
||||
"routingTable": "Yönlendirme tablosu auto, off veya bir tablo numarası olmalıdır"
|
||||
"routingTable": "Yönlendirme tablosu auto, off veya bir tablo numarası olmalıdır",
|
||||
"headerProtectionJunkSize": "Header Protection Key ayarlandığında S1-S4 junk boyutları en az 12 olmalıdır"
|
||||
},
|
||||
"otl": "OTL (One Time Link)",
|
||||
"stringMalformed": "Hatalı format",
|
||||
"keyMalformed": "Anahtar, 32 baytlık base64 kodlu bir anahtar olmalıdır",
|
||||
"body": "Body geçerli bir obje olmalıdır",
|
||||
"hook": "Hook",
|
||||
"enabled": "Aktif",
|
||||
|
|
@ -310,6 +315,25 @@
|
|||
"i4Description": "Hex formatında protocol mimic paketi: <b 0x...>",
|
||||
"i5Label": "Özel junk 5 (I5)",
|
||||
"i5Description": "Hex formatında protocol mimic paketi: <b 0x...>",
|
||||
"rangeNote": "Aşağıdaki parametreler tek bir sayı veya kapsayıcı bir aralık (0-65535) kabul eder, örneğin 30 veya 30-90",
|
||||
"headerProtectionKeyLabel": "Header protection anahtarı",
|
||||
"headerProtectionKeyDescription": "Paket başlıklarını şifrelemek için kullanılan 32 baytlık base64 anahtar. Her iki tarafta da aynı olmalıdır, bu yüzden her istemciye kopyalanır. S1-S4'ün en az 12 olmasını gerektirir",
|
||||
"contentPaddingAdditionLabel": "İçerik dolgu ekleme",
|
||||
"contentPaddingAdditionDescription": "Şifrelemeden önce transport paketlerine eklenen rastgele miktarda bayt",
|
||||
"rekeyAfterTimeLabel": "Yeniden anahtarlama süresi",
|
||||
"rekeyAfterTimeDescription": "Oturum yeniden müzakere edilmeden önce geçecek saniye (WireGuard varsayılanı: 120)",
|
||||
"rekeyTimeoutLabel": "Yeniden anahtarlama zaman aşımı",
|
||||
"rekeyTimeoutDescription": "Handshake yeniden denemeleri arasındaki saniye (WireGuard varsayılanı: 5)",
|
||||
"rejectAfterTimeLabel": "Reddetme süresi",
|
||||
"rejectAfterTimeDescription": "Saniye cinsinden oturum ömrü (WireGuard varsayılanı: 180)",
|
||||
"keepaliveTimeoutLabel": "Keepalive zaman aşımı",
|
||||
"keepaliveTimeoutDescription": "Keepalive gönderilmeden önce boşta geçecek saniye (WireGuard varsayılanı: 10)",
|
||||
"maxHandshakeAttemptsLabel": "Maks handshake denemesi",
|
||||
"maxHandshakeAttemptsDescription": "Vazgeçmeden önceki handshake denemesi sayısı (WireGuard varsayılanı: 18)",
|
||||
"randomTrailersLabel": "Rastgele son ekler",
|
||||
"randomTrailersDescription": "Handshake paketlerine rastgele miktarda bayt ekler. Her iki tarafta da aynı olmalıdır, bu yüzden her istemciye kopyalanır",
|
||||
"disableCookiesLabel": "Cookie'leri devre dışı bırak",
|
||||
"disableCookiesDescription": "Yük altındayken handshake'lere cookie mesajlarıyla yanıt verme. Yalnızca tünelin bu tarafını etkiler",
|
||||
"mtuNote": "Değerler MTU'ya bağlıdır",
|
||||
"obfuscationParameters": "AmneziaWG Obfuscation Ayarları"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,6 +159,9 @@
|
|||
"sectionAdvanced": "Додатково",
|
||||
"noItems": "Немає елементів",
|
||||
"nullNoItems": "Немає елементів. Використовується глобальна конфігурація",
|
||||
"nullNotSet": "Не встановлено",
|
||||
"set": "Встановити",
|
||||
"unset": "Скасувати встановлення",
|
||||
"add": "Додати"
|
||||
},
|
||||
"admin": {
|
||||
|
|
@ -250,10 +253,12 @@
|
|||
"cidr": "CIDR",
|
||||
"device": "Пристрій",
|
||||
"cidrValid": "CIDR має бути дійсним",
|
||||
"routingTable": "Таблиця маршрутизації має бути auto, off або номером таблиці"
|
||||
"routingTable": "Таблиця маршрутизації має бути auto, off або номером таблиці",
|
||||
"headerProtectionJunkSize": "Розміри сміття S1-S4 мають бути щонайменше 12, якщо встановлено ключ захисту заголовків"
|
||||
},
|
||||
"otl": "Одноразове посилання",
|
||||
"stringMalformed": "Рядок має неправильний формат",
|
||||
"keyMalformed": "Ключ має бути ключем довжиною 32 байти, закодованим у Base64",
|
||||
"body": "Тіло має бути коректним об'єктом",
|
||||
"hook": "Hook",
|
||||
"enabled": "Увімкнено",
|
||||
|
|
@ -310,6 +315,25 @@
|
|||
"i4Description": "Пакет-імітація протоколу у hex-форматі: <b 0x...>",
|
||||
"i5Label": "Спеціальний сміттєвий пакет 5 (I5)",
|
||||
"i5Description": "Пакет-імітація протоколу у hex-форматі: <b 0x...>",
|
||||
"rangeNote": "Наведені нижче параметри приймають одне число або включний діапазон (0–65535), наприклад 30 або 30–90",
|
||||
"headerProtectionKeyLabel": "Ключ захисту заголовків",
|
||||
"headerProtectionKeyDescription": "Ключ Base64 довжиною 32 байти, який використовується для шифрування заголовків пакетів. Має збігатися з обох боків, тому копіюється до кожного клієнта. Потребує значень S1-S4 не менше 12",
|
||||
"contentPaddingAdditionLabel": "Додавання доповнення до вмісту",
|
||||
"contentPaddingAdditionDescription": "Випадкова кількість байтів, що додається до транспортних пакетів перед шифруванням",
|
||||
"rekeyAfterTimeLabel": "Повторне створення ключа через",
|
||||
"rekeyAfterTimeDescription": "Кількість секунд до повторного узгодження сеансу (типове значення WireGuard: 120)",
|
||||
"rekeyTimeoutLabel": "Час очікування повторного створення ключа",
|
||||
"rekeyTimeoutDescription": "Кількість секунд між повторними спробами рукостискання (типове значення WireGuard: 5)",
|
||||
"rejectAfterTimeLabel": "Відхиляти після",
|
||||
"rejectAfterTimeDescription": "Тривалість сеансу в секундах (типове значення WireGuard: 180)",
|
||||
"keepaliveTimeoutLabel": "Час очікування Keepalive",
|
||||
"keepaliveTimeoutDescription": "Кількість секунд бездіяльності перед надсиланням Keepalive (типове значення WireGuard: 10)",
|
||||
"maxHandshakeAttemptsLabel": "Максимальна кількість спроб рукостискання",
|
||||
"maxHandshakeAttemptsDescription": "Кількість спроб рукостискання перед припиненням спроб (типове значення WireGuard: 18)",
|
||||
"randomTrailersLabel": "Випадкові кінцеві дані",
|
||||
"randomTrailersDescription": "Додавати до пакетів рукостискання випадкову кількість байтів. Має збігатися з обох боків, тому копіюється до кожного клієнта",
|
||||
"disableCookiesLabel": "Вимкнути cookie",
|
||||
"disableCookiesDescription": "Не відповідати на рукостискання повідомленнями cookie під час високого навантаження. Впливає лише на цей бік тунелю",
|
||||
"mtuNote": "Значення залежать від MTU",
|
||||
"obfuscationParameters": "Параметри обфускації AmneziaWG"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,6 +148,16 @@ export default defineNuxtConfig({
|
|||
language: 'vi-VN',
|
||||
name: 'Tiếng Việt',
|
||||
},
|
||||
{
|
||||
code: 'sv',
|
||||
language: 'sv-SE',
|
||||
name: 'Svenska',
|
||||
},
|
||||
{
|
||||
code: 'az',
|
||||
language: 'az-AZ',
|
||||
name: 'Azərbaycan',
|
||||
},
|
||||
],
|
||||
defaultLocale: 'en',
|
||||
vueI18n: './i18n.config.ts',
|
||||
|
|
|
|||
|
|
@ -23,56 +23,56 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@libsql/client": "^0.17.4",
|
||||
"@libsql/client": "^0.18.0",
|
||||
"@nuxtjs/color-mode": "^4.0.1",
|
||||
"@nuxtjs/i18n": "^10.6.0",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@phc/format": "^1.0.0",
|
||||
"@pinia/nuxt": "^1.0.1",
|
||||
"@pinia/nuxt": "^1.0.2",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@vueuse/core": "^14.4.0",
|
||||
"@vueuse/nuxt": "^14.4.0",
|
||||
"apexcharts": "^6.8.0",
|
||||
"apexcharts": "^7.5.1",
|
||||
"argon2": "^0.45.1",
|
||||
"cidr-tools": "^13.0.0",
|
||||
"cidr-tools": "^13.0.1",
|
||||
"citty": "^0.2.2",
|
||||
"consola": "^3.4.2",
|
||||
"crc-32": "^1.2.2",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"ip-bigint": "^9.0.8",
|
||||
"ip-bigint": "^10.0.1",
|
||||
"is-cidr": "^7.0.1",
|
||||
"is-ip": "^5.0.1",
|
||||
"js-sha256": "^1.0.0",
|
||||
"nuxt": "^4.5.2",
|
||||
"obug": "^2.1.4",
|
||||
"openid-client": "^6.8.4",
|
||||
"otpauth": "^9.5.1",
|
||||
"pinia": "^4.0.2",
|
||||
"qr": "^0.6.0",
|
||||
"reka-ui": "^2.10.3",
|
||||
"obug": "^3.0.0",
|
||||
"openid-client": "^6.8.8",
|
||||
"otpauth": "^9.5.2",
|
||||
"pinia": "^4.0.3",
|
||||
"qr": "^0.7.0",
|
||||
"reka-ui": "^2.10.4",
|
||||
"semver": "^7.8.5",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"vue": "latest",
|
||||
"vue3-apexcharts": "^1.11.1",
|
||||
"zod": "^4.4.3"
|
||||
"zod": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint": "^1.17.0",
|
||||
"@nuxt/test-utils": "^4.1.0",
|
||||
"@nuxt/test-utils": "^4.3.2",
|
||||
"@types/phc__format": "^1.0.1",
|
||||
"@types/semver": "^7.8.0",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"@vitest/ui": "^4.1.10",
|
||||
"@vitest/coverage-v8": "^4.1.11",
|
||||
"@vitest/ui": "^4.1.11",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"esbuild": "^0.28.2",
|
||||
"eslint": "^10.8.1",
|
||||
"eslint": "^10.10.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"prettier": "^3.9.6",
|
||||
"prettier-plugin-tailwindcss": "^0.8.1",
|
||||
"tsx": "^4.23.12",
|
||||
"tsx": "^4.23.13",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.10",
|
||||
"vue-tsc": "^3.3.9"
|
||||
"vitest": "^4.1.11",
|
||||
"vue-tsc": "^3.3.11"
|
||||
},
|
||||
"packageManager": "pnpm@11.22.0"
|
||||
"packageManager": "pnpm@12.4.1"
|
||||
}
|
||||
|
|
|
|||
3257
src/pnpm-lock.yaml
generated
3257
src/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,9 @@
|
|||
import Database from '#server/utils/Database';
|
||||
import { definePermissionEventHandler } from '#server/utils/handler';
|
||||
import { cachedGetIpInformation } from '#server/utils/ip';
|
||||
import { getIpInformation } from '#server/utils/ip';
|
||||
|
||||
export default definePermissionEventHandler('admin', 'any', async () => {
|
||||
const result = await cachedGetIpInformation();
|
||||
const wgInterface = await Database.interfaces.get();
|
||||
const result = await getIpInformation(wgInterface.name);
|
||||
return result;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { createError, defineEventHandler, readValidatedBody } from 'h3';
|
|||
|
||||
import Database from '#server/utils/Database';
|
||||
import { SERVER_DEBUG, WG_ENV } from '#server/utils/config';
|
||||
import { logSecurityEvent } from '#server/utils/securityLogger';
|
||||
import { useWGSession } from '#server/utils/session';
|
||||
import { assertUnreachable, validateZod } from '#server/utils/types';
|
||||
import { UserLoginSchema } from '#db/repositories/user/types';
|
||||
|
|
@ -28,6 +29,7 @@ export default defineEventHandler(async (event) => {
|
|||
if (!result.success) {
|
||||
switch (result.error) {
|
||||
case 'INCORRECT_CREDENTIALS':
|
||||
logSecurityEvent(event, 'password', username);
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Invalid username or password',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { createError, defineEventHandler, readValidatedBody } from 'h3';
|
||||
|
||||
import Database from '#server/utils/Database';
|
||||
import { logSecurityEvent } from '#server/utils/securityLogger';
|
||||
import { useWGSession } from '#server/utils/session';
|
||||
import { assertUnreachable, validateZod } from '#server/utils/types';
|
||||
import { Verify2faSchema } from '#db/repositories/user/types';
|
||||
|
|
@ -34,6 +35,7 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
switch (totpStatus) {
|
||||
case 'INVALID_TOTP_CODE':
|
||||
logSecurityEvent(event, '2fa');
|
||||
return { status: 'INVALID_TOTP_CODE' as const };
|
||||
case 'USER_DISABLED':
|
||||
throw createError({
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export default definePermissionEventHandler(
|
|||
validateZod(ClientGetSchema, event)
|
||||
);
|
||||
|
||||
const result = await Database.clients.get(clientId);
|
||||
const result = await Database.clients.getPublic(clientId);
|
||||
checkPermissions(result);
|
||||
|
||||
if (!result) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import Database from '#server/utils/Database';
|
||||
import { defineSetupEventHandler } from '#server/utils/handler';
|
||||
import { cachedGetIpInformation } from '#server/utils/ip';
|
||||
import { getIpInformation } from '#server/utils/ip';
|
||||
|
||||
export default defineSetupEventHandler(4, async () => {
|
||||
const result = await cachedGetIpInformation();
|
||||
const wgInterface = await Database.interfaces.get();
|
||||
const result = await getIpInformation(wgInterface.name);
|
||||
return result;
|
||||
});
|
||||
|
|
|
|||
16
src/server/database/migrations/0008_thin_patch.sql
Normal file
16
src/server/database/migrations/0008_thin_patch.sql
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
ALTER TABLE `clients_table` ADD `content_padding_addition` text;--> statement-breakpoint
|
||||
ALTER TABLE `clients_table` ADD `rekey_after_time` text;--> statement-breakpoint
|
||||
ALTER TABLE `clients_table` ADD `rekey_timeout` text;--> statement-breakpoint
|
||||
ALTER TABLE `clients_table` ADD `reject_after_time` text;--> statement-breakpoint
|
||||
ALTER TABLE `clients_table` ADD `keepalive_timeout` text;--> statement-breakpoint
|
||||
ALTER TABLE `clients_table` ADD `max_handshake_attempts` text;--> statement-breakpoint
|
||||
ALTER TABLE `clients_table` ADD `disable_cookies` integer;--> statement-breakpoint
|
||||
ALTER TABLE `interfaces_table` ADD `header_protection_key` text;--> statement-breakpoint
|
||||
ALTER TABLE `interfaces_table` ADD `content_padding_addition` text;--> statement-breakpoint
|
||||
ALTER TABLE `interfaces_table` ADD `rekey_after_time` text;--> statement-breakpoint
|
||||
ALTER TABLE `interfaces_table` ADD `rekey_timeout` text;--> statement-breakpoint
|
||||
ALTER TABLE `interfaces_table` ADD `reject_after_time` text;--> statement-breakpoint
|
||||
ALTER TABLE `interfaces_table` ADD `keepalive_timeout` text;--> statement-breakpoint
|
||||
ALTER TABLE `interfaces_table` ADD `max_handshake_attempts` text;--> statement-breakpoint
|
||||
ALTER TABLE `interfaces_table` ADD `random_trailers` integer;--> statement-breakpoint
|
||||
ALTER TABLE `interfaces_table` ADD `disable_cookies` integer;
|
||||
5
src/server/database/migrations/0009_dusty_prodigy.sql
Normal file
5
src/server/database/migrations/0009_dusty_prodigy.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
UPDATE `hooks_table` SET
|
||||
`pre_up` = replace(`pre_up`, 'wg0', '{{interface}}'),
|
||||
`post_up` = replace(`post_up`, 'wg0', '{{interface}}'),
|
||||
`pre_down` = replace(`pre_down`, 'wg0', '{{interface}}'),
|
||||
`post_down` = replace(`post_down`, 'wg0', '{{interface}}');
|
||||
1144
src/server/database/migrations/meta/0008_snapshot.json
Normal file
1144
src/server/database/migrations/meta/0008_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
1144
src/server/database/migrations/meta/0009_snapshot.json
Normal file
1144
src/server/database/migrations/meta/0009_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -57,6 +57,20 @@
|
|||
"when": 1785630544322,
|
||||
"tag": "0007_blushing_electro",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "6",
|
||||
"when": 1788760134302,
|
||||
"tag": "0008_thin_patch",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "6",
|
||||
"when": 1790082534310,
|
||||
"tag": "0009_dusty_prodigy",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -52,6 +52,15 @@ export const client = sqliteTable(
|
|||
i3: text(),
|
||||
i4: text(),
|
||||
i5: text(),
|
||||
// HeaderProtectionKey and RandomTrailers have to match the interface and
|
||||
// are therefore only configurable on the server
|
||||
contentPaddingAddition: text('content_padding_addition'),
|
||||
rekeyAfterTime: text('rekey_after_time'),
|
||||
rekeyTimeout: text('rekey_timeout'),
|
||||
rejectAfterTime: text('reject_after_time'),
|
||||
keepaliveTimeout: text('keepalive_timeout'),
|
||||
maxHandshakeAttempts: text('max_handshake_attempts'),
|
||||
disableCookies: int('disable_cookies', { mode: 'boolean' }),
|
||||
dns: text({ mode: 'json' }).$type<string[]>(),
|
||||
serverEndpoint: text('server_endpoint'),
|
||||
enabled: int({ mode: 'boolean' }).notNull(),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import type {
|
|||
UpdateClientType,
|
||||
} from './types';
|
||||
|
||||
import { WG_ENV } from '#server/utils/config';
|
||||
import Database from '#server/utils/Database';
|
||||
import { nextIP } from '#server/utils/ip';
|
||||
import type { ID } from '#server/utils/types';
|
||||
|
|
@ -28,6 +29,15 @@ function createPreparedStatement(db: DBType) {
|
|||
findById: db.query.client
|
||||
.findFirst({ where: eq(client.id, sql.placeholder('id')) })
|
||||
.prepare(),
|
||||
findByIdPublic: db.query.client
|
||||
.findFirst({
|
||||
where: eq(client.id, sql.placeholder('id')),
|
||||
columns: {
|
||||
privateKey: false,
|
||||
preSharedKey: false,
|
||||
},
|
||||
})
|
||||
.prepare(),
|
||||
toggle: db
|
||||
.update(client)
|
||||
.set({ enabled: sql.placeholder('enabled') as never as boolean })
|
||||
|
|
@ -149,10 +159,20 @@ export class ClientService {
|
|||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Includes WireGuard secrets. JSON API responses should use {@link getPublic}.
|
||||
*/
|
||||
get(id: ID) {
|
||||
return this.#statements.findById.execute({ id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one client without sensitive data
|
||||
*/
|
||||
getPublic(id: ID) {
|
||||
return this.#statements.findByIdPublic.execute({ id });
|
||||
}
|
||||
|
||||
async create({ name, expiresAt }: ClientCreateType) {
|
||||
const privateKey = await wg.generatePrivateKey();
|
||||
const publicKey = await wg.getPublicKey(privateKey);
|
||||
|
|
@ -162,7 +182,7 @@ export class ClientService {
|
|||
const clients = await tx.query.client.findMany().execute();
|
||||
const clientInterface = await tx.query.wgInterface
|
||||
.findFirst({
|
||||
where: eq(wgInterface.name, 'wg0'),
|
||||
where: eq(wgInterface.name, WG_ENV.WG_INTERFACE),
|
||||
})
|
||||
.execute();
|
||||
|
||||
|
|
@ -191,7 +211,7 @@ export class ClientService {
|
|||
name,
|
||||
// TODO: properly assign user id
|
||||
userId: 1,
|
||||
interfaceId: 'wg0',
|
||||
interfaceId: WG_ENV.WG_INTERFACE,
|
||||
expiresAt,
|
||||
privateKey,
|
||||
publicKey,
|
||||
|
|
@ -236,7 +256,7 @@ export class ClientService {
|
|||
return this.#db.transaction(async (tx) => {
|
||||
const clientInterface = await tx.query.wgInterface
|
||||
.findFirst({
|
||||
where: eq(wgInterface.name, 'wg0'),
|
||||
where: eq(wgInterface.name, WG_ENV.WG_INTERFACE),
|
||||
})
|
||||
.execute();
|
||||
|
||||
|
|
@ -272,7 +292,7 @@ export class ClientService {
|
|||
.values({
|
||||
name,
|
||||
userId: 1,
|
||||
interfaceId: 'wg0',
|
||||
interfaceId: WG_ENV.WG_INTERFACE,
|
||||
privateKey,
|
||||
publicKey,
|
||||
preSharedKey,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type { client } from './schema';
|
|||
import {
|
||||
AddressSchema,
|
||||
AllowedIpsSchema,
|
||||
AwgRangeSchema,
|
||||
DnsSchema,
|
||||
EnabledSchema,
|
||||
FirewallIpsSchema,
|
||||
|
|
@ -109,6 +110,13 @@ export const ClientUpdateSchema = schemaForType<UpdateClientType>()(
|
|||
i3: ISchema,
|
||||
i4: ISchema,
|
||||
i5: ISchema,
|
||||
contentPaddingAddition: AwgRangeSchema,
|
||||
rekeyAfterTime: AwgRangeSchema,
|
||||
rekeyTimeout: AwgRangeSchema,
|
||||
rejectAfterTime: AwgRangeSchema,
|
||||
keepaliveTimeout: AwgRangeSchema,
|
||||
maxHandshakeAttempts: AwgRangeSchema,
|
||||
disableCookies: z.boolean().nullable(),
|
||||
persistentKeepalive: PersistentKeepaliveSchema,
|
||||
serverEndpoint: AddressSchema.nullable(),
|
||||
dns: DnsSchema.nullable(),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { eq, sql } from 'drizzle-orm';
|
|||
import { hooks } from './schema';
|
||||
import type { HooksUpdateType } from './types';
|
||||
|
||||
import { WG_ENV } from '#server/utils/config';
|
||||
import type { DBType } from '#db/sqlite';
|
||||
|
||||
function createPreparedStatement(db: DBType) {
|
||||
|
|
@ -23,7 +24,9 @@ export class HooksService {
|
|||
}
|
||||
|
||||
async get() {
|
||||
const hooks = await this.#statements.get.execute({ interface: 'wg0' });
|
||||
const hooks = await this.#statements.get.execute({
|
||||
interface: WG_ENV.WG_INTERFACE,
|
||||
});
|
||||
if (!hooks) {
|
||||
throw new Error('Hooks not found');
|
||||
}
|
||||
|
|
@ -34,7 +37,7 @@ export class HooksService {
|
|||
return this.#db
|
||||
.update(hooks)
|
||||
.set(data)
|
||||
.where(eq(hooks.id, 'wg0'))
|
||||
.where(eq(hooks.id, WG_ENV.WG_INTERFACE))
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,15 @@ export const wgInterface = sqliteTable('interfaces_table', {
|
|||
i3: text(),
|
||||
i4: text(),
|
||||
i5: text(),
|
||||
headerProtectionKey: text('header_protection_key'),
|
||||
contentPaddingAddition: text('content_padding_addition'),
|
||||
rekeyAfterTime: text('rekey_after_time'),
|
||||
rekeyTimeout: text('rekey_timeout'),
|
||||
rejectAfterTime: text('reject_after_time'),
|
||||
keepaliveTimeout: text('keepalive_timeout'),
|
||||
maxHandshakeAttempts: text('max_handshake_attempts'),
|
||||
randomTrailers: int('random_trailers', { mode: 'boolean' }),
|
||||
disableCookies: int('disable_cookies', { mode: 'boolean' }),
|
||||
// does nothing yet
|
||||
enabled: int({ mode: 'boolean' }).notNull(),
|
||||
// Enable per-client firewall filtering via iptables
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { parseCidr } from 'cidr-tools';
|
|||
import { wgInterface } from './schema';
|
||||
import type { InterfaceCidrUpdateType, InterfaceUpdateType } from './types';
|
||||
|
||||
import { WG_ENV } from '#server/utils/config';
|
||||
import { nextIPFromUsedAddresses } from '#server/utils/ip';
|
||||
import { client as clientSchema } from '#db/schema';
|
||||
import type { DBType } from '#db/sqlite';
|
||||
|
|
@ -42,7 +43,7 @@ export class InterfaceService {
|
|||
|
||||
async get() {
|
||||
const wgInterface = await this.#statements.get.execute({
|
||||
interface: 'wg0',
|
||||
interface: WG_ENV.WG_INTERFACE,
|
||||
});
|
||||
if (!wgInterface) {
|
||||
throw new Error('Interface not found');
|
||||
|
|
@ -52,7 +53,7 @@ export class InterfaceService {
|
|||
|
||||
updateKeyPair(privateKey: string, publicKey: string) {
|
||||
return this.#statements.updateKeyPair.execute({
|
||||
interface: 'wg0',
|
||||
interface: WG_ENV.WG_INTERFACE,
|
||||
privateKey,
|
||||
publicKey,
|
||||
});
|
||||
|
|
@ -62,13 +63,13 @@ export class InterfaceService {
|
|||
return this.#db
|
||||
.update(wgInterface)
|
||||
.set(data)
|
||||
.where(eq(wgInterface.name, 'wg0'))
|
||||
.where(eq(wgInterface.name, WG_ENV.WG_INTERFACE))
|
||||
.execute();
|
||||
}
|
||||
|
||||
setFirewallEnabled(firewallEnabled: boolean) {
|
||||
return this.#statements.setFirewallEnabled.execute({
|
||||
interface: 'wg0',
|
||||
interface: WG_ENV.WG_INTERFACE,
|
||||
firewallEnabled,
|
||||
});
|
||||
}
|
||||
|
|
@ -77,7 +78,7 @@ export class InterfaceService {
|
|||
return this.#db.transaction(async (tx) => {
|
||||
const oldCidr = await tx.query.wgInterface
|
||||
.findFirst({
|
||||
where: eq(wgInterface.name, 'wg0'),
|
||||
where: eq(wgInterface.name, WG_ENV.WG_INTERFACE),
|
||||
columns: { ipv4Cidr: true, ipv6Cidr: true },
|
||||
})
|
||||
.execute();
|
||||
|
|
@ -89,7 +90,7 @@ export class InterfaceService {
|
|||
await tx
|
||||
.update(wgInterface)
|
||||
.set(data)
|
||||
.where(eq(wgInterface.name, 'wg0'))
|
||||
.where(eq(wgInterface.name, WG_ENV.WG_INTERFACE))
|
||||
.execute();
|
||||
|
||||
const clients = await tx.query.client.findMany().execute();
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ import isCidr from 'is-cidr';
|
|||
import type { wgInterface } from './schema';
|
||||
|
||||
import {
|
||||
AwgRangeSchema,
|
||||
EnabledSchema,
|
||||
HSchema,
|
||||
ISchema,
|
||||
JcSchema,
|
||||
JmaxSchema,
|
||||
JminSchema,
|
||||
KeySchema,
|
||||
MtuSchema,
|
||||
PortSchema,
|
||||
RoutingTableSchema,
|
||||
|
|
@ -29,7 +31,13 @@ export type InterfaceCreateType = Omit<
|
|||
|
||||
export type InterfaceUpdateType = Omit<
|
||||
InterfaceCreateType,
|
||||
'name' | 'createdAt' | 'updatedAt' | 'privateKey' | 'publicKey'
|
||||
| 'name'
|
||||
| 'createdAt'
|
||||
| 'updatedAt'
|
||||
| 'privateKey'
|
||||
| 'publicKey'
|
||||
| 'ipv4Cidr'
|
||||
| 'ipv6Cidr'
|
||||
>;
|
||||
|
||||
const device = z
|
||||
|
|
@ -37,39 +45,79 @@ const device = z
|
|||
.min(1, t('zod.interface.device'))
|
||||
.pipe(safeStringRefine);
|
||||
|
||||
const cidr = z
|
||||
const cidr4 = z
|
||||
.string({ message: t('zod.interface.cidr') })
|
||||
.min(1, { message: t('zod.interface.cidr') })
|
||||
.refine((value) => isCidr(value), { message: t('zod.interface.cidrValid') })
|
||||
.refine((value) => isCidr.v4(value), {
|
||||
message: t('zod.interface.cidrValid'),
|
||||
})
|
||||
.pipe(safeStringRefine);
|
||||
|
||||
export const InterfaceUpdateSchema = schemaForType<InterfaceUpdateType>()(
|
||||
z.object({
|
||||
ipv4Cidr: cidr,
|
||||
ipv6Cidr: cidr,
|
||||
mtu: MtuSchema,
|
||||
routingTable: RoutingTableSchema,
|
||||
jC: JcSchema,
|
||||
jMin: JminSchema,
|
||||
jMax: JmaxSchema,
|
||||
s1: SSchema,
|
||||
s2: SSchema,
|
||||
s3: SSchema,
|
||||
s4: SSchema,
|
||||
h1: HSchema,
|
||||
h2: HSchema,
|
||||
h3: HSchema,
|
||||
h4: HSchema,
|
||||
i1: ISchema,
|
||||
i2: ISchema,
|
||||
i3: ISchema,
|
||||
i4: ISchema,
|
||||
i5: ISchema,
|
||||
port: PortSchema,
|
||||
device: device,
|
||||
enabled: EnabledSchema,
|
||||
firewallEnabled: EnabledSchema,
|
||||
const cidr6 = z
|
||||
.string({ message: t('zod.interface.cidr') })
|
||||
.min(1, { message: t('zod.interface.cidr') })
|
||||
.refine((value) => isCidr.v6(value), {
|
||||
message: t('zod.interface.cidrValid'),
|
||||
})
|
||||
.pipe(safeStringRefine);
|
||||
|
||||
/**
|
||||
* Header protection uses the first 12 bytes of every junk prefix as the nonce
|
||||
* for its cipher, so all four junk sizes have to be at least that large.
|
||||
*/
|
||||
const HEADER_PROTECTION_MIN_JUNK_SIZE = 12;
|
||||
|
||||
export const InterfaceUpdateSchema = schemaForType<InterfaceUpdateType>()(
|
||||
z
|
||||
.object({
|
||||
device: device,
|
||||
port: PortSchema,
|
||||
mtu: MtuSchema,
|
||||
routingTable: RoutingTableSchema,
|
||||
jC: JcSchema,
|
||||
jMin: JminSchema,
|
||||
jMax: JmaxSchema,
|
||||
s1: SSchema,
|
||||
s2: SSchema,
|
||||
s3: SSchema,
|
||||
s4: SSchema,
|
||||
h1: HSchema,
|
||||
h2: HSchema,
|
||||
h3: HSchema,
|
||||
h4: HSchema,
|
||||
i1: ISchema,
|
||||
i2: ISchema,
|
||||
i3: ISchema,
|
||||
i4: ISchema,
|
||||
i5: ISchema,
|
||||
headerProtectionKey: KeySchema,
|
||||
contentPaddingAddition: AwgRangeSchema,
|
||||
rekeyAfterTime: AwgRangeSchema,
|
||||
rekeyTimeout: AwgRangeSchema,
|
||||
rejectAfterTime: AwgRangeSchema,
|
||||
keepaliveTimeout: AwgRangeSchema,
|
||||
maxHandshakeAttempts: AwgRangeSchema,
|
||||
randomTrailers: z.boolean().nullable(),
|
||||
disableCookies: z.boolean().nullable(),
|
||||
|
||||
enabled: EnabledSchema,
|
||||
firewallEnabled: EnabledSchema,
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.headerProtectionKey === null) return;
|
||||
|
||||
for (const junk of ['s1', 's2', 's3', 's4'] as const) {
|
||||
const size = data[junk];
|
||||
|
||||
if (size === null || size < HEADER_PROTECTION_MIN_JUNK_SIZE) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: [junk],
|
||||
message: t('zod.interface.headerProtectionJunkSize'),
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export type InterfaceCidrUpdateType = {
|
||||
|
|
@ -80,7 +128,7 @@ export type InterfaceCidrUpdateType = {
|
|||
export const InterfaceCidrUpdateSchema =
|
||||
schemaForType<InterfaceCidrUpdateType>()(
|
||||
z.object({
|
||||
ipv4Cidr: cidr,
|
||||
ipv6Cidr: cidr,
|
||||
ipv4Cidr: cidr4,
|
||||
ipv6Cidr: cidr6,
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { eq, sql } from 'drizzle-orm';
|
|||
import { userConfig } from './schema';
|
||||
import type { UserConfigUpdateType } from './types';
|
||||
|
||||
import { WG_ENV } from '#server/utils/config';
|
||||
import { wgInterface } from '#db/schema';
|
||||
import type { DBType } from '#db/sqlite';
|
||||
|
||||
|
|
@ -24,7 +25,9 @@ export class UserConfigService {
|
|||
}
|
||||
|
||||
async get() {
|
||||
const userConfig = await this.#statements.get.execute({ interface: 'wg0' });
|
||||
const userConfig = await this.#statements.get.execute({
|
||||
interface: WG_ENV.WG_INTERFACE,
|
||||
});
|
||||
|
||||
if (!userConfig) {
|
||||
throw new Error('User config not found');
|
||||
|
|
@ -33,8 +36,6 @@ export class UserConfigService {
|
|||
return userConfig;
|
||||
}
|
||||
|
||||
// TODO: wrap ipv6 host in square brackets
|
||||
|
||||
/**
|
||||
* sets host of user config
|
||||
*
|
||||
|
|
@ -45,13 +46,13 @@ export class UserConfigService {
|
|||
await tx
|
||||
.update(userConfig)
|
||||
.set({ host, port })
|
||||
.where(eq(userConfig.id, 'wg0'))
|
||||
.where(eq(userConfig.id, WG_ENV.WG_INTERFACE))
|
||||
.execute();
|
||||
|
||||
await tx
|
||||
.update(wgInterface)
|
||||
.set({ port })
|
||||
.where(eq(wgInterface.name, 'wg0'))
|
||||
.where(eq(wgInterface.name, WG_ENV.WG_INTERFACE))
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
|
@ -60,7 +61,7 @@ export class UserConfigService {
|
|||
return this.#db
|
||||
.update(userConfig)
|
||||
.set(data)
|
||||
.where(eq(userConfig.id, 'wg0'))
|
||||
.where(eq(userConfig.id, WG_ENV.WG_INTERFACE))
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const db = drizzle({ client, schema });
|
|||
|
||||
export async function connect() {
|
||||
await migrate();
|
||||
await renameInterface(db);
|
||||
const dbService = new DBService(db);
|
||||
|
||||
if (WG_INITIAL_ENV.ENABLED) {
|
||||
|
|
@ -122,16 +123,38 @@ async function initialSetup(db: DBServiceType) {
|
|||
}
|
||||
}
|
||||
|
||||
// Renames the interface to the one specified in `WG_INTERFACE` then hooks, users, and clients via `ON UPDATE CASCADE`
|
||||
async function renameInterface(db: DBType) {
|
||||
const wgInterface = await db.query.wgInterface.findFirst();
|
||||
|
||||
if (!wgInterface) {
|
||||
throw new Error('Interface not found');
|
||||
}
|
||||
|
||||
if (wgInterface.name === WG_ENV.WG_INTERFACE) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB_DEBUG(
|
||||
`Renaming Interface ${wgInterface.name} to ${WG_ENV.WG_INTERFACE}...`
|
||||
);
|
||||
await db
|
||||
.update(schema.wgInterface)
|
||||
.set({ name: WG_ENV.WG_INTERFACE })
|
||||
.where(eq(schema.wgInterface.name, wgInterface.name))
|
||||
.execute();
|
||||
}
|
||||
|
||||
async function disableIpv6(db: DBType) {
|
||||
// This should match the initial value migration
|
||||
const postUpMatch =
|
||||
' ip6tables -t nat -A POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE; ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -A FORWARD -o wg0 -j ACCEPT;';
|
||||
' ip6tables -t nat -A POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE; ip6tables -A INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -A FORWARD -i {{interface}} -j ACCEPT; ip6tables -A FORWARD -o {{interface}} -j ACCEPT;';
|
||||
const postDownMatch =
|
||||
' ip6tables -t nat -D POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE; ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -D FORWARD -o wg0 -j ACCEPT;';
|
||||
' ip6tables -t nat -D POSTROUTING -s {{ipv6Cidr}} -o {{device}} -j MASQUERADE; ip6tables -D INPUT -p udp -m udp --dport {{port}} -j ACCEPT; ip6tables -D FORWARD -i {{interface}} -j ACCEPT; ip6tables -D FORWARD -o {{interface}} -j ACCEPT;';
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
const hooks = await tx.query.hooks.findFirst({
|
||||
where: eq(schema.hooks.id, 'wg0'),
|
||||
where: eq(schema.hooks.id, WG_ENV.WG_INTERFACE),
|
||||
});
|
||||
|
||||
if (!hooks) {
|
||||
|
|
@ -146,7 +169,7 @@ async function disableIpv6(db: DBType) {
|
|||
postUp: hooks.postUp.replace(postUpMatch, ''),
|
||||
postDown: hooks.postDown.replace(postDownMatch, ''),
|
||||
})
|
||||
.where(eq(schema.hooks.id, 'wg0'))
|
||||
.where(eq(schema.hooks.id, WG_ENV.WG_INTERFACE))
|
||||
.execute();
|
||||
} else {
|
||||
DB_DEBUG('IPv6 Post Up hooks already disabled, skipping...');
|
||||
|
|
@ -159,7 +182,7 @@ async function disableIpv6(db: DBType) {
|
|||
postUp: hooks.postUp.replace(postUpMatch, ''),
|
||||
postDown: hooks.postDown.replace(postDownMatch, ''),
|
||||
})
|
||||
.where(eq(schema.hooks.id, 'wg0'))
|
||||
.where(eq(schema.hooks.id, WG_ENV.WG_INTERFACE))
|
||||
.execute();
|
||||
} else {
|
||||
DB_DEBUG('IPv6 Post Down hooks already disabled, skipping...');
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
|
||||
import Database from '#server/utils/Database';
|
||||
import WireGuard from '#server/utils/WireGuard';
|
||||
import { logSecurityEvent } from '#server/utils/securityLogger';
|
||||
import { validateZod } from '#server/utils/types';
|
||||
import { OneTimeLinkGetSchema } from '#db/repositories/oneTimeLink/types';
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
const otl = await Database.oneTimeLinks.getByOtl(oneTimeLink);
|
||||
if (!otl) {
|
||||
logSecurityEvent(event, 'one-time-link');
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Invalid One Time Link',
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ async function getPrometheusResponse() {
|
|||
const wireguardSentBytes = [];
|
||||
const wireguardReceivedBytes = [];
|
||||
const wireguardLatestHandshakeSeconds = [];
|
||||
const wireguardEndpointInfo = [];
|
||||
for (const client of clients) {
|
||||
if (client.enabled === true) {
|
||||
wireguardEnabledPeersCount++;
|
||||
|
|
@ -46,6 +47,17 @@ async function getPrometheusResponse() {
|
|||
wireguardLatestHandshakeSeconds.push(
|
||||
`wireguard_latest_handshake_seconds{${id}} ${client.latestHandshakeAt ? (Date.now() - client.latestHandshakeAt.getTime()) / 1000 : 0}`
|
||||
);
|
||||
|
||||
if (client.endpoint) {
|
||||
const endpointId = formatPrometheusLabels({
|
||||
interface: wgInterface.name,
|
||||
name: client.name,
|
||||
endpoint: client.endpoint,
|
||||
});
|
||||
wireguardEndpointInfo.push(
|
||||
`wireguard_peer_endpoint_info{${endpointId}} 1`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const id = formatPrometheusLabels({ interface: wgInterface.name });
|
||||
|
|
@ -75,6 +87,10 @@ async function getPrometheusResponse() {
|
|||
'# TYPE wireguard_latest_handshake_seconds gauge',
|
||||
`${wireguardLatestHandshakeSeconds.join('\n')}`,
|
||||
'',
|
||||
'# HELP wireguard_peer_endpoint_info Current endpoint (IP:port) of a connected peer',
|
||||
'# TYPE wireguard_peer_endpoint_info gauge',
|
||||
`${wireguardEndpointInfo.join('\n')}`,
|
||||
'',
|
||||
];
|
||||
|
||||
return returnText.join('\n');
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ const nullObject = new Proxy(
|
|||
let provider = nullObject as never as DBServiceType;
|
||||
|
||||
connect()
|
||||
.then((db) => {
|
||||
.then(async (db) => {
|
||||
provider = db;
|
||||
WireGuard.Startup();
|
||||
await WireGuard.Startup();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('Failed to connect to Database:', err);
|
||||
|
|
|
|||
154
src/server/utils/awg.ts
Normal file
154
src/server/utils/awg.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import type { ClientType } from '#db/repositories/client/types';
|
||||
import type { InterfaceType } from '#db/repositories/interface/types';
|
||||
|
||||
/**
|
||||
* A parameter written as `Key = value` into the `[Interface]` section of a
|
||||
* configuration file. `null` means "not set" and is left out entirely.
|
||||
*/
|
||||
type AwgParameters = Record<string, string | number | boolean | null>;
|
||||
|
||||
type AwgInterface = Pick<
|
||||
InterfaceType,
|
||||
| 'jC'
|
||||
| 'jMin'
|
||||
| 'jMax'
|
||||
| 's1'
|
||||
| 's2'
|
||||
| 's3'
|
||||
| 's4'
|
||||
| 'h1'
|
||||
| 'h2'
|
||||
| 'h3'
|
||||
| 'h4'
|
||||
| 'i1'
|
||||
| 'i2'
|
||||
| 'i3'
|
||||
| 'i4'
|
||||
| 'i5'
|
||||
| 'headerProtectionKey'
|
||||
| 'contentPaddingAddition'
|
||||
| 'rekeyAfterTime'
|
||||
| 'rekeyTimeout'
|
||||
| 'rejectAfterTime'
|
||||
| 'keepaliveTimeout'
|
||||
| 'maxHandshakeAttempts'
|
||||
| 'randomTrailers'
|
||||
| 'disableCookies'
|
||||
>;
|
||||
|
||||
type AwgClient = Pick<
|
||||
ClientType,
|
||||
| 'jC'
|
||||
| 'jMin'
|
||||
| 'jMax'
|
||||
| 'i1'
|
||||
| 'i2'
|
||||
| 'i3'
|
||||
| 'i4'
|
||||
| 'i5'
|
||||
| 'contentPaddingAddition'
|
||||
| 'rekeyAfterTime'
|
||||
| 'rekeyTimeout'
|
||||
| 'rejectAfterTime'
|
||||
| 'keepaliveTimeout'
|
||||
| 'maxHandshakeAttempts'
|
||||
| 'disableCookies'
|
||||
>;
|
||||
|
||||
/**
|
||||
* amneziawg-tools parses booleans with `parse_bool`, which only accepts
|
||||
* `on`, `off` or a plain number - not `true` / `false`.
|
||||
*/
|
||||
function formatBoolean(value: boolean) {
|
||||
return value ? 'on' : 'off';
|
||||
}
|
||||
|
||||
export function buildAwgLines(parameters: AwgParameters) {
|
||||
const lines: string[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(parameters)) {
|
||||
if (typeof value === 'boolean') {
|
||||
lines.push(`${key} = ${formatBoolean(value)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!value) continue;
|
||||
|
||||
lines.push(`${key} = ${value}`);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** Obfuscation parameters for the server's own interface */
|
||||
export function interfaceAwgParameters(
|
||||
wgInterface: AwgInterface
|
||||
): AwgParameters {
|
||||
return {
|
||||
Jc: wgInterface.jC,
|
||||
Jmin: wgInterface.jMin,
|
||||
Jmax: wgInterface.jMax,
|
||||
S1: wgInterface.s1,
|
||||
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,
|
||||
HeaderProtectionKey: wgInterface.headerProtectionKey,
|
||||
ContentPaddingAddition: wgInterface.contentPaddingAddition,
|
||||
RekeyAfterTime: wgInterface.rekeyAfterTime,
|
||||
RekeyTimeout: wgInterface.rekeyTimeout,
|
||||
RejectAfterTime: wgInterface.rejectAfterTime,
|
||||
KeepaliveTimeout: wgInterface.keepaliveTimeout,
|
||||
MaxHandshakeAttempts: wgInterface.maxHandshakeAttempts,
|
||||
RandomTrailers: wgInterface.randomTrailers,
|
||||
DisableCookies: wgInterface.disableCookies,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Obfuscation parameters for a client's interface.
|
||||
*
|
||||
* S1-S4, H1-H4, HeaderProtectionKey and RandomTrailers have to be identical on
|
||||
* both ends of the tunnel and are therefore copied from the server, everything
|
||||
* else may differ per client.
|
||||
*/
|
||||
export function clientAwgParameters(
|
||||
wgInterface: AwgInterface,
|
||||
client: AwgClient
|
||||
): AwgParameters {
|
||||
return {
|
||||
Jc: client.jC,
|
||||
Jmin: client.jMin,
|
||||
Jmax: client.jMax,
|
||||
S1: wgInterface.s1,
|
||||
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,
|
||||
HeaderProtectionKey: wgInterface.headerProtectionKey,
|
||||
ContentPaddingAddition: client.contentPaddingAddition,
|
||||
RekeyAfterTime: client.rekeyAfterTime,
|
||||
RekeyTimeout: client.rekeyTimeout,
|
||||
RejectAfterTime: client.rejectAfterTime,
|
||||
KeepaliveTimeout: client.keepaliveTimeout,
|
||||
MaxHandshakeAttempts: client.maxHandshakeAttempts,
|
||||
RandomTrailers: wgInterface.randomTrailers,
|
||||
DisableCookies: client.disableCookies,
|
||||
};
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { createDebug } from 'obug';
|
|||
import packageJson from '@@/package.json';
|
||||
|
||||
import { exec } from '#server/utils/cmd';
|
||||
import { parseInterfaceName } from '#server/utils/interfaceName';
|
||||
import { parseTrustedProxies } from '#server/utils/trustedProxy';
|
||||
import {
|
||||
OAUTH_PROVIDERS,
|
||||
|
|
@ -50,6 +51,8 @@ export const WG_ENV = {
|
|||
PORT: assertEnv('PORT'),
|
||||
/** If IPv6 should be disabled */
|
||||
DISABLE_IPV6: process.env.DISABLE_IPV6 === 'true',
|
||||
/** Name of the WireGuard interface */
|
||||
WG_INTERFACE: parseInterfaceName(process.env.WG_INTERFACE),
|
||||
WG_EXECUTABLE: await detectAwg(),
|
||||
DISABLE_VERSION_CHECK: process.env.DISABLE_VERSION_CHECK === 'true',
|
||||
/** List of enabled and configured OAuth providers */
|
||||
|
|
|
|||
15
src/server/utils/endpoint.ts
Normal file
15
src/server/utils/endpoint.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { isIPv6 } from 'is-ip';
|
||||
|
||||
/**
|
||||
* WireGuard Endpoint host:port. IPv6 hosts need brackets.
|
||||
*/
|
||||
export function formatEndpoint(host: string, port: number): string {
|
||||
const bare =
|
||||
host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
|
||||
|
||||
if (isIPv6(bare)) {
|
||||
return `[${bare}]:${port}`;
|
||||
}
|
||||
|
||||
return `${bare}:${port}`;
|
||||
}
|
||||
|
|
@ -9,6 +9,14 @@ import type { UserConfigType } from '#db/repositories/userConfig/types';
|
|||
const FW_DEBUG = createDebug('Firewall');
|
||||
const CHAIN_NAME = 'WG_CLIENTS';
|
||||
|
||||
/**
|
||||
* Tie chains to a specific interface
|
||||
* Prevents instances in the same network namespace from wiping each other's rules
|
||||
*/
|
||||
function chainName(interfaceName: string) {
|
||||
return `${CHAIN_NAME}_${interfaceName}`;
|
||||
}
|
||||
|
||||
// Mutex to prevent concurrent rule rebuilds
|
||||
let rebuildInProgress = false;
|
||||
let rebuildQueued = false;
|
||||
|
|
@ -131,6 +139,7 @@ function parseFirewallEntry(entry: string): ParsedEntry {
|
|||
* Generate iptables rule arguments for a single firewall entry
|
||||
*/
|
||||
function generateRuleArgs(
|
||||
interfaceName: string,
|
||||
clientIp: string,
|
||||
entry: ParsedEntry,
|
||||
comment?: string,
|
||||
|
|
@ -138,7 +147,7 @@ function generateRuleArgs(
|
|||
): string[] {
|
||||
const rules: string[] = [];
|
||||
const commentArg = comment ? ` -m comment --comment "${comment}"` : '';
|
||||
const baseArgs = `-${action} ${CHAIN_NAME} -s ${clientIp} -d ${entry.ip}`;
|
||||
const baseArgs = `-${action} ${chainName(interfaceName)} -s ${clientIp} -d ${entry.ip}`;
|
||||
|
||||
if (entry.port) {
|
||||
// Port-specific rules
|
||||
|
|
@ -165,24 +174,25 @@ export const firewall = {
|
|||
* Initialize the custom chain if it doesn't exist
|
||||
*/
|
||||
async initChain(interfaceName: string, enableIpv6: boolean): Promise<void> {
|
||||
const chain = chainName(interfaceName);
|
||||
FW_DEBUG(
|
||||
`Initializing firewall chain ${CHAIN_NAME} for interface ${interfaceName}`
|
||||
`Initializing firewall chain ${chain} 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(`iptables -N ${chain} 2>/dev/null || true`);
|
||||
if (enableIpv6) {
|
||||
await exec(`ip6tables -N ${CHAIN_NAME} 2>/dev/null || true`);
|
||||
await exec(`ip6tables -N ${chain} 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}`
|
||||
`iptables -C FORWARD -i ${interfaceName} -j ${chain} 2>/dev/null || iptables -I FORWARD 1 -i ${interfaceName} -j ${chain}`
|
||||
);
|
||||
if (enableIpv6) {
|
||||
await exec(
|
||||
`ip6tables -C FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || ip6tables -I FORWARD 1 -i ${interfaceName} -j ${CHAIN_NAME}`
|
||||
`ip6tables -C FORWARD -i ${interfaceName} -j ${chain} 2>/dev/null || ip6tables -I FORWARD 1 -i ${interfaceName} -j ${chain}`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
@ -190,11 +200,12 @@ export const firewall = {
|
|||
/**
|
||||
* Flush all rules in the custom chain
|
||||
*/
|
||||
async flushChain(enableIpv6: boolean): Promise<void> {
|
||||
FW_DEBUG(`Flushing firewall chain ${CHAIN_NAME}`);
|
||||
await exec(`iptables -F ${CHAIN_NAME} 2>/dev/null || true`);
|
||||
async flushChain(interfaceName: string, enableIpv6: boolean): Promise<void> {
|
||||
const chain = chainName(interfaceName);
|
||||
FW_DEBUG(`Flushing firewall chain ${chain}`);
|
||||
await exec(`iptables -F ${chain} 2>/dev/null || true`);
|
||||
if (enableIpv6) {
|
||||
await exec(`ip6tables -F ${CHAIN_NAME} 2>/dev/null || true`);
|
||||
await exec(`ip6tables -F ${chain} 2>/dev/null || true`);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -202,6 +213,7 @@ export const firewall = {
|
|||
* Apply firewall rules for a single client
|
||||
*/
|
||||
async applyClientRules(
|
||||
interfaceName: string,
|
||||
client: FirewallClient,
|
||||
defaultAllowedIps: string[],
|
||||
enableIpv6: boolean
|
||||
|
|
@ -226,13 +238,23 @@ export const firewall = {
|
|||
|
||||
if (destIsIpv6) {
|
||||
if (enableIpv6) {
|
||||
const rules = generateRuleArgs(client.ipv6Address, parsed, comment);
|
||||
const rules = generateRuleArgs(
|
||||
interfaceName,
|
||||
client.ipv6Address,
|
||||
parsed,
|
||||
comment
|
||||
);
|
||||
for (const rule of rules) {
|
||||
await exec(`ip6tables ${rule}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const rules = generateRuleArgs(client.ipv4Address, parsed, comment);
|
||||
const rules = generateRuleArgs(
|
||||
interfaceName,
|
||||
client.ipv4Address,
|
||||
parsed,
|
||||
comment
|
||||
);
|
||||
for (const rule of rules) {
|
||||
await exec(`iptables ${rule}`);
|
||||
}
|
||||
|
|
@ -271,12 +293,13 @@ export const firewall = {
|
|||
await this.initChain(wgInterface.name, enableIpv6);
|
||||
|
||||
// Flush existing rules
|
||||
await this.flushChain(enableIpv6);
|
||||
await this.flushChain(wgInterface.name, enableIpv6);
|
||||
|
||||
// Apply rules for each enabled client
|
||||
for (const client of clients) {
|
||||
if (!client.enabled) continue;
|
||||
await this.applyClientRules(
|
||||
wgInterface.name,
|
||||
client,
|
||||
userConfig.defaultAllowedIps,
|
||||
enableIpv6
|
||||
|
|
@ -284,9 +307,10 @@ export const firewall = {
|
|||
}
|
||||
|
||||
// Add final DROP for any traffic not explicitly allowed
|
||||
await exec(`iptables -A ${CHAIN_NAME} -j DROP`);
|
||||
const chain = chainName(wgInterface.name);
|
||||
await exec(`iptables -A ${chain} -j DROP`);
|
||||
if (enableIpv6) {
|
||||
await exec(`ip6tables -A ${CHAIN_NAME} -j DROP`);
|
||||
await exec(`ip6tables -A ${chain} -j DROP`);
|
||||
}
|
||||
|
||||
FW_DEBUG('Firewall rules rebuilt successfully');
|
||||
|
|
@ -309,24 +333,33 @@ export const firewall = {
|
|||
interfaceName: string,
|
||||
enableIpv6: boolean
|
||||
): Promise<void> {
|
||||
const chain = chainName(interfaceName);
|
||||
FW_DEBUG(`Removing firewall filtering for interface ${interfaceName}`);
|
||||
|
||||
// Remove jump rules from FORWARD chain
|
||||
// This disconnects the interface from the old shared chain, but it won't delete the chain itself.
|
||||
// Other wg-easy instances on the same host may still rely on it.
|
||||
await exec(
|
||||
`iptables -D FORWARD -i ${interfaceName} -j ${chain} 2>/dev/null || true`
|
||||
);
|
||||
await exec(
|
||||
`iptables -D FORWARD -i ${interfaceName} -j ${CHAIN_NAME} 2>/dev/null || true`
|
||||
);
|
||||
if (enableIpv6) {
|
||||
await exec(
|
||||
`ip6tables -D FORWARD -i ${interfaceName} -j ${chain} 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(`iptables -X ${CHAIN_NAME} 2>/dev/null || true`);
|
||||
await exec(`iptables -F ${chain} 2>/dev/null || true`);
|
||||
await exec(`iptables -X ${chain} 2>/dev/null || true`);
|
||||
if (enableIpv6) {
|
||||
await exec(`ip6tables -F ${CHAIN_NAME} 2>/dev/null || true`);
|
||||
await exec(`ip6tables -X ${CHAIN_NAME} 2>/dev/null || true`);
|
||||
await exec(`ip6tables -F ${chain} 2>/dev/null || true`);
|
||||
await exec(`ip6tables -X ${chain} 2>/dev/null || true`);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
21
src/server/utils/interfaceName.ts
Normal file
21
src/server/utils/interfaceName.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Default interface name
|
||||
*/
|
||||
const DEFAULT_INTERFACE_NAME = 'wg0';
|
||||
|
||||
/**
|
||||
* `wg-quick` compliant interface naming
|
||||
*/
|
||||
const INTERFACE_NAME_REGEX = /^[a-zA-Z0-9_=+.-]{1,15}$/;
|
||||
|
||||
export function parseInterfaceName(value: string | undefined): string {
|
||||
if (!value) {
|
||||
return DEFAULT_INTERFACE_NAME;
|
||||
}
|
||||
|
||||
if (!INTERFACE_NAME_REGEX.test(value)) {
|
||||
throw new Error(`Invalid interface name: ${value}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@ async function getReverseDns(ip: string) {
|
|||
}
|
||||
}
|
||||
|
||||
function getPrivateInformation() {
|
||||
function getPrivateInformation(interfaceName: string) {
|
||||
const interfaces = networkInterfaces();
|
||||
|
||||
const interfaceNames = Object.keys(interfaces);
|
||||
|
|
@ -104,7 +104,7 @@ function getPrivateInformation() {
|
|||
const obj: Record<string, { ipv4: string[]; ipv6: string[] }> = {};
|
||||
|
||||
for (const name of interfaceNames) {
|
||||
if (name === 'wg0') {
|
||||
if (name === interfaceName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -132,10 +132,10 @@ function getPrivateInformation() {
|
|||
return obj;
|
||||
}
|
||||
|
||||
async function getIpInformation() {
|
||||
export async function getIpInformation(interfaceName: string) {
|
||||
const results = [];
|
||||
|
||||
const publicInfo = await getPublicInformation();
|
||||
const publicInfo = await cachedGetPublicInformation();
|
||||
if (publicInfo.ipv4) {
|
||||
results.push({
|
||||
value: publicInfo.ipv4,
|
||||
|
|
@ -155,7 +155,7 @@ async function getIpInformation() {
|
|||
});
|
||||
}
|
||||
|
||||
const privateInfo = getPrivateInformation();
|
||||
const privateInfo = getPrivateInformation(interfaceName);
|
||||
for (const [name, { ipv4, ipv6 }] of Object.entries(privateInfo)) {
|
||||
for (const ip of ipv4) {
|
||||
results.push({
|
||||
|
|
@ -175,9 +175,9 @@ async function getIpInformation() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Fetch IP Information
|
||||
* Fetch public IP Information
|
||||
* @cache Response is cached for 15 min
|
||||
*/
|
||||
export const cachedGetIpInformation = cacheFunction(getIpInformation, {
|
||||
const cachedGetPublicInformation = cacheFunction(getPublicInformation, {
|
||||
expiry: 15 * 60 * 1000,
|
||||
});
|
||||
|
|
|
|||
17
src/server/utils/securityLogger.ts
Normal file
17
src/server/utils/securityLogger.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import type { H3Event } from 'h3';
|
||||
|
||||
import { WG_ENV } from '#server/utils/config';
|
||||
import { getTrustedRequestIP } from '#server/utils/trustedProxy';
|
||||
|
||||
type SecurityEvent = 'password' | '2fa' | 'one-time-link';
|
||||
|
||||
export function logSecurityEvent(
|
||||
event: H3Event,
|
||||
type: SecurityEvent,
|
||||
username?: string
|
||||
) {
|
||||
const user = username ? ` username=${JSON.stringify(username)}` : '';
|
||||
const ip = getTrustedRequestIP(event, WG_ENV.TRUSTED_PROXIES) ?? 'unknown';
|
||||
|
||||
console.warn(`Security failure: type=${type}${user} ip=${ip}`);
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ export function removeNewlines(templ: string) {
|
|||
* - ipv4Cidr: IPv4 CIDR
|
||||
* - ipv6Cidr: IPv6 CIDR
|
||||
* - device: Network device
|
||||
* - interface: WireGuard interface
|
||||
* - port: Port number
|
||||
* - uiPort: UI port number
|
||||
*/
|
||||
|
|
@ -27,6 +28,7 @@ export function iptablesTemplate(templ: string, wgInterface: InterfaceType) {
|
|||
ipv4Cidr: wgInterface.ipv4Cidr,
|
||||
ipv6Cidr: wgInterface.ipv6Cidr,
|
||||
device: wgInterface.device,
|
||||
interface: wgInterface.name,
|
||||
port: wgInterface.port.toString(),
|
||||
uiPort: WG_ENV.PORT,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -61,6 +61,15 @@ export function getTrustedRequestURL(
|
|||
});
|
||||
}
|
||||
|
||||
export function getTrustedRequestIP(
|
||||
event: H3Event,
|
||||
trustedProxies: readonly string[]
|
||||
): string | undefined {
|
||||
return getRequestIP(event, {
|
||||
xForwardedFor: isRequestFromTrustedProxy(event, trustedProxies),
|
||||
});
|
||||
}
|
||||
|
||||
function isRequestFromTrustedProxy(
|
||||
event: H3Event,
|
||||
trustedProxies: readonly string[]
|
||||
|
|
|
|||
|
|
@ -46,38 +46,87 @@ export const JmaxSchema = z.number().max(1280).nullable();
|
|||
|
||||
export const SSchema = z.number().max(1132).nullable();
|
||||
|
||||
const RANGE_REGEX = /^(\d+)(?:-(\d+))?$/;
|
||||
|
||||
function parseRange(value: string) {
|
||||
const match = RANGE_REGEX.exec(value);
|
||||
if (!match) return null;
|
||||
|
||||
const lower = Number(match[1]);
|
||||
const upper = match[2] === undefined ? lower : Number(match[2]);
|
||||
|
||||
if (!Number.isSafeInteger(lower) || !Number.isSafeInteger(upper)) return null;
|
||||
|
||||
return { lower, upper };
|
||||
}
|
||||
|
||||
/**
|
||||
* AmneziaWG accepts either a single number or an inclusive `lower-upper` range
|
||||
* for several parameters (see `u32_range_from_string` / `u16_range_from_string`
|
||||
* in amneziawg-tools). Values outside of `[min, max]` are silently truncated by
|
||||
* the parser, so they are rejected here instead.
|
||||
*/
|
||||
function rangeSchema(min: number, max: number) {
|
||||
return z
|
||||
.string()
|
||||
.transform((v) => v.replace(/\s+/g, ''))
|
||||
.refine(
|
||||
(v) => {
|
||||
const range = parseRange(v);
|
||||
if (!range) return false;
|
||||
|
||||
return (
|
||||
range.lower >= min && range.upper <= max && range.lower <= range.upper
|
||||
);
|
||||
},
|
||||
{
|
||||
message: t('zod.generic.validNumberRange'),
|
||||
}
|
||||
)
|
||||
.transform((v) => {
|
||||
const range = parseRange(v);
|
||||
// cannot happen, the refine above already rejected unparsable values
|
||||
if (!range) return v;
|
||||
|
||||
return range.lower === range.upper
|
||||
? `${range.lower}`
|
||||
: `${range.lower}-${range.upper}`;
|
||||
})
|
||||
.nullable();
|
||||
}
|
||||
|
||||
const H_MIN = 5;
|
||||
const H_MAX = 2 ** 31 - 1;
|
||||
|
||||
export const HSchema = z
|
||||
/** H1-H4 packet header types, parsed as a uint32 range */
|
||||
export const HSchema = rangeSchema(H_MIN, H_MAX);
|
||||
|
||||
/**
|
||||
* AmneziaWG 3.0+ parameters that are parsed as a uint16 range:
|
||||
* ContentPaddingAddition, RekeyAfterTime, RekeyTimeout, RejectAfterTime,
|
||||
* KeepaliveTimeout and MaxHandshakeAttempts.
|
||||
*/
|
||||
export const AwgRangeSchema = rangeSchema(0, 65535);
|
||||
|
||||
/** Length of a WireGuard key in bytes */
|
||||
const KEY_LENGTH = 32;
|
||||
|
||||
/**
|
||||
* A 32 byte key in canonical base64, the same format WireGuard uses for its
|
||||
* own keys. Used by the AmneziaWG 3.0+ `HeaderProtectionKey` parameter.
|
||||
*/
|
||||
export const KeySchema = z
|
||||
.string()
|
||||
.transform((v) => v.replace(/\s+/g, ''))
|
||||
.pipe(safeStringRefine)
|
||||
.pipe(controlStringRefine)
|
||||
.refine(
|
||||
(v) => {
|
||||
if (!v) return false;
|
||||
const key = Buffer.from(v, 'base64');
|
||||
|
||||
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;
|
||||
return key.length === KEY_LENGTH && key.toString('base64') === v;
|
||||
},
|
||||
{
|
||||
message: t('zod.generic.validNumberRange'),
|
||||
}
|
||||
{ message: t('zod.keyMalformed') }
|
||||
)
|
||||
.transform((v) => {
|
||||
if (!v.includes('-')) return `${Number(v)}`;
|
||||
|
||||
const [min, max] = v.split('-').map(Number);
|
||||
return min === max ? `${min}` : `${min}-${max}`;
|
||||
})
|
||||
.nullable();
|
||||
|
||||
export const ISchema = z
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
import { parseCidr } from 'cidr-tools';
|
||||
import { stringifyIp } from 'ip-bigint';
|
||||
|
||||
import {
|
||||
buildAwgLines,
|
||||
clientAwgParameters,
|
||||
interfaceAwgParameters,
|
||||
} from '#server/utils/awg';
|
||||
import { removeNewlines, iptablesTemplate } from '#server/utils/template';
|
||||
import { exec } from '#server/utils/cmd';
|
||||
import { WG_ENV } from '#server/utils/config';
|
||||
import { formatEndpoint } from '#server/utils/endpoint';
|
||||
import type { ClientType } from '#db/repositories/client/types';
|
||||
import type { InterfaceType } from '#db/repositories/interface/types';
|
||||
import type { UserConfigType } from '#db/repositories/userConfig/types';
|
||||
|
|
@ -58,34 +64,10 @@ AllowedIPs = ${allowedIps.join(', ')}${extraLines.length ? `\n${extraLines.join(
|
|||
`${ipv4Addr}/${cidr4.prefix}` +
|
||||
(enableIpv6 ? `, ${ipv6Addr}/${cidr6.prefix}` : '');
|
||||
|
||||
let awgLines: string[] = [];
|
||||
|
||||
if (wgExecutable === 'awg') {
|
||||
const parameters = {
|
||||
Jc: wgInterface.jC,
|
||||
Jmin: wgInterface.jMin,
|
||||
Jmax: wgInterface.jMax,
|
||||
S1: wgInterface.s1,
|
||||
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,
|
||||
} as const;
|
||||
|
||||
awgLines = Object.entries(parameters)
|
||||
.filter(([_, value]) => !!value)
|
||||
.map(([key, value]) => `${key} = ${value}`);
|
||||
}
|
||||
|
||||
const extraLines = [...awgLines].filter((v) => v !== null);
|
||||
const extraLines =
|
||||
wgExecutable === 'awg'
|
||||
? buildAwgLines(interfaceAwgParameters(wgInterface))
|
||||
: [];
|
||||
|
||||
return `# Note: Do not edit this file directly.
|
||||
# Your changes will be overwritten!
|
||||
|
|
@ -127,32 +109,10 @@ PostDown = ${iptablesTemplate(hooks.postDown, wgInterface)}`;
|
|||
const dnsLine =
|
||||
dnsServers.length > 0 ? `DNS = ${dnsServers.join(', ')}` : null;
|
||||
|
||||
let awgLines: string[] = [];
|
||||
|
||||
if (wgExecutable === 'awg') {
|
||||
const parameters = {
|
||||
Jc: client.jC,
|
||||
Jmin: client.jMin,
|
||||
Jmax: client.jMax,
|
||||
S1: wgInterface.s1,
|
||||
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,
|
||||
} as const;
|
||||
|
||||
awgLines = Object.entries(parameters)
|
||||
.filter(([_, value]) => !!value)
|
||||
.map(([key, value]) => `${key} = ${value}`);
|
||||
}
|
||||
const awgLines =
|
||||
wgExecutable === 'awg'
|
||||
? buildAwgLines(clientAwgParameters(wgInterface, client))
|
||||
: [];
|
||||
|
||||
const extraLines = [dnsLine, ...hookLines, ...awgLines].filter(
|
||||
(v) => v !== null
|
||||
|
|
@ -168,7 +128,7 @@ PublicKey = ${wgInterface.publicKey}
|
|||
PresharedKey = ${client.preSharedKey}
|
||||
AllowedIPs = ${(client.allowedIps ?? userConfig.defaultAllowedIps).join(', ')}
|
||||
PersistentKeepalive = ${client.persistentKeepalive}
|
||||
Endpoint = ${userConfig.host}:${userConfig.port}`;
|
||||
Endpoint = ${formatEndpoint(userConfig.host, userConfig.port)}`;
|
||||
},
|
||||
|
||||
generatePrivateKey: () => {
|
||||
|
|
|
|||
222
src/test/unit/awg.spec.ts
Normal file
222
src/test/unit/awg.spec.ts
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
buildAwgLines,
|
||||
clientAwgParameters,
|
||||
interfaceAwgParameters,
|
||||
} from '#server/utils/awg';
|
||||
import { InterfaceUpdateSchema } from '#db/repositories/interface/types';
|
||||
import { AwgRangeSchema, HSchema, KeySchema } from '#server/utils/types';
|
||||
|
||||
/** Any valid 32 byte base64 key, generated with `wg genkey` */
|
||||
const KEY = 'JPJ2DWzJbwrRkY6cPbZaJasL6bHQOM6J2Ollx4D/OHU=';
|
||||
|
||||
const wgInterface = {
|
||||
jC: 7,
|
||||
jMin: 10,
|
||||
jMax: 1000,
|
||||
s1: 128,
|
||||
s2: 56,
|
||||
s3: null,
|
||||
s4: null,
|
||||
h1: '5',
|
||||
h2: null,
|
||||
h3: null,
|
||||
h4: null,
|
||||
i1: null,
|
||||
i2: null,
|
||||
i3: null,
|
||||
i4: null,
|
||||
i5: null,
|
||||
headerProtectionKey: null,
|
||||
contentPaddingAddition: null,
|
||||
rekeyAfterTime: null,
|
||||
rekeyTimeout: null,
|
||||
rejectAfterTime: null,
|
||||
keepaliveTimeout: null,
|
||||
maxHandshakeAttempts: null,
|
||||
randomTrailers: null,
|
||||
disableCookies: null,
|
||||
};
|
||||
|
||||
const client = {
|
||||
jC: null,
|
||||
jMin: null,
|
||||
jMax: null,
|
||||
i1: null,
|
||||
i2: null,
|
||||
i3: null,
|
||||
i4: null,
|
||||
i5: null,
|
||||
contentPaddingAddition: null,
|
||||
rekeyAfterTime: null,
|
||||
rekeyTimeout: null,
|
||||
rejectAfterTime: null,
|
||||
keepaliveTimeout: null,
|
||||
maxHandshakeAttempts: null,
|
||||
disableCookies: null,
|
||||
};
|
||||
|
||||
describe('AmneziaWG config lines', () => {
|
||||
test('leaves out parameters that are not set', () => {
|
||||
expect(buildAwgLines({ Jc: null, Jmin: null })).toEqual([]);
|
||||
});
|
||||
|
||||
test('writes booleans as on/off, not true/false', () => {
|
||||
expect(
|
||||
buildAwgLines({ RandomTrailers: true, DisableCookies: false })
|
||||
).toEqual(['RandomTrailers = on', 'DisableCookies = off']);
|
||||
});
|
||||
|
||||
test('keeps ranges verbatim', () => {
|
||||
expect(buildAwgLines({ RekeyAfterTime: '90-150' })).toEqual([
|
||||
'RekeyAfterTime = 90-150',
|
||||
]);
|
||||
});
|
||||
|
||||
test('builds the interface parameters', () => {
|
||||
expect(
|
||||
buildAwgLines(
|
||||
interfaceAwgParameters({
|
||||
...wgInterface,
|
||||
headerProtectionKey: KEY,
|
||||
maxHandshakeAttempts: '5',
|
||||
randomTrailers: true,
|
||||
disableCookies: false,
|
||||
})
|
||||
)
|
||||
).toEqual([
|
||||
'Jc = 7',
|
||||
'Jmin = 10',
|
||||
'Jmax = 1000',
|
||||
'S1 = 128',
|
||||
'S2 = 56',
|
||||
'H1 = 5',
|
||||
`HeaderProtectionKey = ${KEY}`,
|
||||
'MaxHandshakeAttempts = 5',
|
||||
'RandomTrailers = on',
|
||||
'DisableCookies = off',
|
||||
]);
|
||||
});
|
||||
|
||||
test('copies parameters that must match from the interface to the client', () => {
|
||||
const lines = buildAwgLines(
|
||||
clientAwgParameters(
|
||||
{ ...wgInterface, headerProtectionKey: KEY, randomTrailers: true },
|
||||
{ ...client, rekeyTimeout: '4-8', disableCookies: true }
|
||||
)
|
||||
);
|
||||
|
||||
expect(lines).toContain(`HeaderProtectionKey = ${KEY}`);
|
||||
expect(lines).toContain('RandomTrailers = on');
|
||||
expect(lines).toContain('S1 = 128');
|
||||
expect(lines).toContain('H1 = 5');
|
||||
expect(lines).toContain('RekeyTimeout = 4-8');
|
||||
expect(lines).toContain('DisableCookies = on');
|
||||
});
|
||||
|
||||
test('does not inherit per-client parameters from the interface', () => {
|
||||
const lines = buildAwgLines(
|
||||
clientAwgParameters(
|
||||
{ ...wgInterface, jC: 7, maxHandshakeAttempts: '9' },
|
||||
client
|
||||
)
|
||||
);
|
||||
|
||||
expect(lines).not.toContain('Jc = 7');
|
||||
expect(lines).not.toContain('MaxHandshakeAttempts = 9');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AwgRangeSchema', () => {
|
||||
test('accepts a single number and an inclusive range', () => {
|
||||
expect(AwgRangeSchema.parse('30')).toBe('30');
|
||||
expect(AwgRangeSchema.parse('30-90')).toBe('30-90');
|
||||
expect(AwgRangeSchema.parse(' 30 - 90 ')).toBe('30-90');
|
||||
expect(AwgRangeSchema.parse('0')).toBe('0');
|
||||
expect(AwgRangeSchema.parse(null)).toBeNull();
|
||||
});
|
||||
|
||||
test('collapses a range with identical bounds', () => {
|
||||
expect(AwgRangeSchema.parse('30-30')).toBe('30');
|
||||
});
|
||||
|
||||
test('rejects values the awg parser would truncate or refuse', () => {
|
||||
expect(() => AwgRangeSchema.parse('65536')).toThrow();
|
||||
expect(() => AwgRangeSchema.parse('90-30')).toThrow();
|
||||
expect(() => AwgRangeSchema.parse('-5')).toThrow();
|
||||
expect(() => AwgRangeSchema.parse('1-2-3')).toThrow();
|
||||
expect(() => AwgRangeSchema.parse('abc')).toThrow();
|
||||
expect(() => AwgRangeSchema.parse('')).toThrow();
|
||||
});
|
||||
|
||||
test('does not change the H1-H4 bounds', () => {
|
||||
expect(HSchema.parse('5')).toBe('5');
|
||||
expect(() => HSchema.parse('4')).toThrow();
|
||||
expect(HSchema.parse('5-2147483647')).toBe('5-2147483647');
|
||||
expect(() => HSchema.parse('5-2147483648')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('KeySchema', () => {
|
||||
test('accepts a 32 byte base64 key', () => {
|
||||
expect(KeySchema.parse(KEY)).toBe(KEY);
|
||||
expect(KeySchema.parse(null)).toBeNull();
|
||||
});
|
||||
|
||||
test('rejects keys of the wrong length or encoding', () => {
|
||||
expect(() => KeySchema.parse('too-short')).toThrow();
|
||||
expect(() => KeySchema.parse(KEY.slice(0, -1))).toThrow();
|
||||
expect(() => KeySchema.parse(`${KEY}${KEY}`)).toThrow();
|
||||
// non canonical: the trailing bits of the last character are not zero
|
||||
expect(() => KeySchema.parse(`${KEY.slice(0, 42)}B=`)).toThrow();
|
||||
});
|
||||
|
||||
test('rejects newlines that would inject extra config lines', () => {
|
||||
expect(() =>
|
||||
KeySchema.parse(`${KEY}\nPostUp = touch /tmp/pwned`)
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('InterfaceUpdateSchema', () => {
|
||||
const base = {
|
||||
ipv4Cidr: '10.8.0.0/24',
|
||||
ipv6Cidr: 'fdcc:ad94:bacf:61a3::/64',
|
||||
mtu: 1420,
|
||||
routingTable: 'auto',
|
||||
...wgInterface,
|
||||
port: 51820,
|
||||
device: 'eth0',
|
||||
enabled: true,
|
||||
firewallEnabled: false,
|
||||
};
|
||||
|
||||
test('accepts an interface without header protection', () => {
|
||||
expect(() => InterfaceUpdateSchema.parse(base)).not.toThrow();
|
||||
});
|
||||
|
||||
test('requires S1-S4 to be at least 12 when a key is set', () => {
|
||||
const result = InterfaceUpdateSchema.safeParse({
|
||||
...base,
|
||||
headerProtectionKey: KEY,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues.map((issue) => issue.path[0])).toEqual([
|
||||
's3',
|
||||
's4',
|
||||
]);
|
||||
});
|
||||
|
||||
test('accepts header protection with large enough junk sizes', () => {
|
||||
expect(() =>
|
||||
InterfaceUpdateSchema.parse({
|
||||
...base,
|
||||
headerProtectionKey: KEY,
|
||||
s3: 12,
|
||||
s4: 40,
|
||||
})
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
17
src/test/unit/endpoint.spec.ts
Normal file
17
src/test/unit/endpoint.spec.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { formatEndpoint } from '#server/utils/endpoint';
|
||||
|
||||
describe('formatEndpoint', () => {
|
||||
test('keeps IPv4 and hostnames unbracketed', () => {
|
||||
expect(formatEndpoint('203.0.113.10', 51820)).toBe('203.0.113.10:51820');
|
||||
expect(formatEndpoint('vpn.example.com', 51820)).toBe(
|
||||
'vpn.example.com:51820'
|
||||
);
|
||||
});
|
||||
|
||||
test('wraps IPv6 hosts in brackets', () => {
|
||||
expect(formatEndpoint('2001:db8::1', 51820)).toBe('[2001:db8::1]:51820');
|
||||
expect(formatEndpoint('[2001:db8::1]', 51820)).toBe('[2001:db8::1]:51820');
|
||||
});
|
||||
});
|
||||
|
|
@ -18,7 +18,7 @@ describe('firewall', () => {
|
|||
describe('IPv4-only chain management', () => {
|
||||
test('does not invoke ip6tables when initializing and flushing', async () => {
|
||||
await firewall.initChain('wg0', false);
|
||||
await firewall.flushChain(false);
|
||||
await firewall.flushChain('wg0', false);
|
||||
|
||||
expect(execMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining('iptables -C FORWARD -i wg0')
|
||||
|
|
@ -40,6 +40,30 @@ describe('firewall', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('interface scoped chains', () => {
|
||||
test('uses one chain per interface', async () => {
|
||||
await firewall.initChain('wg1', false);
|
||||
|
||||
expect(execMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining('iptables -N WG_CLIENTS_wg1')
|
||||
);
|
||||
expect(execMock).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('iptables -N WG_CLIENTS ')
|
||||
);
|
||||
});
|
||||
|
||||
test('stops referencing the legacy chain when removing filtering', async () => {
|
||||
await firewall.removeFiltering('wg1', false);
|
||||
|
||||
expect(execMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining('iptables -D FORWARD -i wg1 -j WG_CLIENTS ')
|
||||
);
|
||||
expect(execMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining('iptables -X WG_CLIENTS_wg1')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidFirewallEntry', () => {
|
||||
test('invalid ips', () => {
|
||||
expect(() => typesTestExports.FirewallIpEntrySchema.parse('')).toThrow();
|
||||
|
|
@ -290,41 +314,43 @@ describe('firewall', () => {
|
|||
describe('generateRuleArgs', () => {
|
||||
test('includes comment when provided', () => {
|
||||
const rules = firewallTestExports.generateRuleArgs(
|
||||
'wg0',
|
||||
'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',
|
||||
'-A WG_CLIENTS_wg0 -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', {
|
||||
const rulesTcp = firewallTestExports.generateRuleArgs('wg0', '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',
|
||||
'-A WG_CLIENTS_wg0 -s 10.8.0.2 -d 10.0.0.1 -p tcp --dport 80 -j ACCEPT',
|
||||
]);
|
||||
const rulesUdp = firewallTestExports.generateRuleArgs('10.8.0.2', {
|
||||
const rulesUdp = firewallTestExports.generateRuleArgs('wg0', '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',
|
||||
'-A WG_CLIENTS_wg0 -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(
|
||||
'wg0',
|
||||
'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',
|
||||
'-A WG_CLIENTS_wg0 -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_wg0 -s 10.8.0.2 -d 10.0.0.1 -p udp --dport 443 -m comment --comment "client 2: phone" -j ACCEPT',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
25
src/test/unit/interfaceName.spec.ts
Normal file
25
src/test/unit/interfaceName.spec.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { parseInterfaceName } from '#server/utils/interfaceName';
|
||||
|
||||
describe('parseInterfaceName', () => {
|
||||
test('returns the default interface when a custom name is not specified', () => {
|
||||
expect(parseInterfaceName(undefined)).toBe('wg0');
|
||||
expect(parseInterfaceName('')).toBe('wg0');
|
||||
});
|
||||
|
||||
test('accepts names compliant with wg-quick', () => {
|
||||
expect(parseInterfaceName('wg1')).toBe('wg1');
|
||||
expect(parseInterfaceName('wg-home')).toBe('wg-home');
|
||||
expect(parseInterfaceName('a'.repeat(15))).toBe('a'.repeat(15));
|
||||
});
|
||||
|
||||
test('rejects invalid names', () => {
|
||||
expect(() => parseInterfaceName('wg 0')).toThrow(
|
||||
'Invalid interface name: wg 0'
|
||||
);
|
||||
expect(() => parseInterfaceName('wg0; id')).toThrow();
|
||||
expect(() => parseInterfaceName('wg/0')).toThrow();
|
||||
expect(() => parseInterfaceName('a'.repeat(16))).toThrow();
|
||||
});
|
||||
});
|
||||
|
|
@ -4,6 +4,7 @@ import { createEvent, type H3Event } from 'h3';
|
|||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
getTrustedRequestIP,
|
||||
getTrustedRequestHost,
|
||||
getTrustedRequestURL,
|
||||
parseTrustedProxies,
|
||||
|
|
@ -35,9 +36,11 @@ describe('trusted request helpers', () => {
|
|||
const event = createTestEvent('192.0.2.10', {
|
||||
host: 'wg-easy:51821',
|
||||
'x-forwarded-host': 'vpn.example.com',
|
||||
'x-forwarded-for': '198.51.100.25',
|
||||
'x-forwarded-proto': 'https',
|
||||
});
|
||||
|
||||
expect(getTrustedRequestIP(event, ['10.0.0.0/8'])).toBe('192.0.2.10');
|
||||
expect(getTrustedRequestHost(event, ['10.0.0.0/8'])).toBe('wg-easy:51821');
|
||||
expect(getTrustedRequestURL(event, ['10.0.0.0/8']).origin).toBe(
|
||||
'http://wg-easy:51821'
|
||||
|
|
@ -50,9 +53,11 @@ describe('trusted request helpers', () => {
|
|||
const event = createTestEvent('10.0.0.2', {
|
||||
host: 'wg-easy:51821',
|
||||
'x-forwarded-host': 'vpn.example.com:8443, wg-easy:51821',
|
||||
'x-forwarded-for': '198.51.100.25',
|
||||
'x-forwarded-proto': 'HTTPS, http',
|
||||
});
|
||||
|
||||
expect(getTrustedRequestIP(event, ['10.0.0.0/8'])).toBe('198.51.100.25');
|
||||
expect(getTrustedRequestHost(event, ['10.0.0.0/8'])).toBe(
|
||||
'vpn.example.com:8443'
|
||||
);
|
||||
|
|
@ -64,10 +69,16 @@ describe('trusted request helpers', () => {
|
|||
);
|
||||
expect(event.node.req.headers['x-forwarded-proto']).toBe('HTTPS, http');
|
||||
});
|
||||
|
||||
test('returns undefined when no request address is available', () => {
|
||||
const event = createTestEvent(undefined, {});
|
||||
|
||||
expect(getTrustedRequestIP(event, ['10.0.0.0/8'])).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
function createTestEvent(
|
||||
remoteAddress: string,
|
||||
remoteAddress: string | undefined,
|
||||
headers: IncomingMessage['headers']
|
||||
): H3Event {
|
||||
const request = {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ export default defineConfig({
|
|||
resolve: {
|
||||
alias: {
|
||||
'#server': fileURLToPath(new URL('./server', import.meta.url)),
|
||||
'#db': fileURLToPath(
|
||||
new URL('./server/database/', import.meta.url)
|
||||
),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue