Browse Source

feat: add global bandwidth limiting for WireGuard VPN traffic

Add ability to limit download/upload speeds for all VPN clients using
Linux Traffic Control (tc) with HTB qdisc.

Features:
- Admin panel UI to enable/disable and configure bandwidth limits
- Download limiting via tc on WireGuard interface egress
- Upload limiting via IFB device mirroring ingress traffic
- Auto-detection of IFB kernel module availability
- Auto-restart interface when bandwidth settings change

Technical:
- New database migration for bandwidth settings
- tc commands injected as PostUp/PostDown hooks
- API endpoints: GET/POST /api/admin/general, GET /api/admin/bandwidth-status

i18n: Add translations for 16 languages
docs: Add bandwidth limiting setup guide
pull/2368/head
Ha Vu 6 months ago
parent
commit
f009be7f3e
  1. 4
      .gitignore
  2. 3
      Dockerfile
  3. 3
      Dockerfile.dev
  4. 122
      docs/content/guides/bandwidth-limiting.md
  5. 40
      src/app/pages/admin/general.vue
  6. 14
      src/i18n/locales/bn.json
  7. 14
      src/i18n/locales/de.json
  8. 14
      src/i18n/locales/en.json
  9. 14
      src/i18n/locales/es.json
  10. 14
      src/i18n/locales/fr.json
  11. 14
      src/i18n/locales/id.json
  12. 14
      src/i18n/locales/it.json
  13. 14
      src/i18n/locales/ko.json
  14. 14
      src/i18n/locales/pl.json
  15. 14
      src/i18n/locales/pt-BR.json
  16. 14
      src/i18n/locales/ru.json
  17. 14
      src/i18n/locales/tr.json
  18. 14
      src/i18n/locales/uk.json
  19. 14
      src/i18n/locales/zh-CN.json
  20. 14
      src/i18n/locales/zh-HK.json
  21. 14
      src/i18n/locales/zh-TW.json
  22. 15
      src/server/api/admin/bandwidth-status.get.ts
  23. 15
      src/server/api/admin/general.post.ts
  24. 3
      src/server/database/migrations/0003_bandwidth_limit.sql
  25. 1000
      src/server/database/migrations/meta/0003_snapshot.json
  26. 7
      src/server/database/migrations/meta/_journal.json
  27. 6
      src/server/database/repositories/general/schema.ts
  28. 22
      src/server/database/repositories/general/service.ts
  29. 10
      src/server/database/repositories/general/types.ts
  30. 23
      src/server/utils/WireGuard.ts
  31. 116
      src/server/utils/bandwidth.ts
  32. 47
      src/server/utils/wgHelper.ts

4
.gitignore

@ -1,3 +1,7 @@
.DS_Store .DS_Store
*.swp *.swp
node_modules node_modules
# Claude Code local settings
.claude/
test_output.txt
repomix-output.xml

3
Dockerfile

@ -52,7 +52,8 @@ RUN apk add --no-cache \
nftables \ nftables \
kmod \ kmod \
iptables-legacy \ iptables-legacy \
wireguard-tools wireguard-tools \
iproute2
RUN mkdir -p /etc/amnezia RUN mkdir -p /etc/amnezia
RUN ln -s /etc/wireguard /etc/amnezia/amneziawg RUN ln -s /etc/wireguard /etc/amnezia/amneziawg

3
Dockerfile.dev

@ -16,7 +16,8 @@ RUN apk add --no-cache \
ip6tables \ ip6tables \
kmod \ kmod \
iptables-legacy \ iptables-legacy \
wireguard-tools wireguard-tools \
iproute2
# Use iptables-legacy # Use iptables-legacy
RUN update-alternatives --install /usr/sbin/iptables iptables /usr/sbin/iptables-legacy 10 --slave /usr/sbin/iptables-restore iptables-restore /usr/sbin/iptables-legacy-restore --slave /usr/sbin/iptables-save iptables-save /usr/sbin/iptables-legacy-save RUN update-alternatives --install /usr/sbin/iptables iptables /usr/sbin/iptables-legacy 10 --slave /usr/sbin/iptables-restore iptables-restore /usr/sbin/iptables-legacy-restore --slave /usr/sbin/iptables-save iptables-save /usr/sbin/iptables-legacy-save

122
docs/content/guides/bandwidth-limiting.md

@ -0,0 +1,122 @@
---
title: Bandwidth Limiting
---
WG-Easy supports global bandwidth limiting for all VPN traffic using Linux Traffic Control (tc).
## How It Works
Bandwidth limiting uses Linux Traffic Control (`tc`) with HTB (Hierarchical Token Bucket):
- **Download limiting**: Applied directly to WireGuard interface egress
- **Upload limiting**: Uses IFB (Intermediate Functional Block) device to mirror ingress traffic
The tc commands are injected as `PostUp`/`PostDown` hooks in the WireGuard configuration.
## Setup Guide
### 1. Access Admin Panel
Navigate to **Admin Panel****General** section.
### 2. Enable Bandwidth Limiting
Toggle **"Enable Bandwidth Limiting"** switch to ON.
### 3. Configure Limits
| Setting | Description | Range |
|---------|-------------|-------|
| **Download Limit (Mbps)** | Max download speed for clients | 1-10000 (0 = unlimited) |
| **Upload Limit (Mbps)** | Max upload speed from clients | 1-10000 (0 = unlimited) |
### 4. Save Settings
Click **Save**. WireGuard interface will automatically restart to apply changes.
## Requirements
### Download Limiting
- Works out of the box on all Linux systems
### Upload Limiting
Requires the `ifb` (Intermediate Functional Block) kernel module:
```bash
# Check if IFB is available
modprobe ifb
lsmod | grep ifb
```
If IFB is not available:
- Download limiting will still work
- Upload limiting will be skipped
- A warning will display in the admin panel
### Docker Container
The container must run with:
- `--cap-add=NET_ADMIN` (required for tc commands)
- `--cap-add=SYS_MODULE` (required for IFB module loading)
Example docker-compose:
```yaml
services:
wg-easy:
image: ghcr.io/wg-easy/wg-easy
cap_add:
- NET_ADMIN
- SYS_MODULE
# ... other settings
```
## Technical Details
### TC Commands Generated
**Download (egress on wg0):**
```bash
tc qdisc add dev wg0 root handle 1: htb default 10
tc class add dev wg0 parent 1: classid 1:1 htb rate 100mbit ceil 100mbit
tc class add dev wg0 parent 1:1 classid 1:10 htb rate 100mbit ceil 100mbit
```
**Upload (ingress via IFB mirror):**
```bash
ip link add ifb0 type ifb
ip link set ifb0 up
tc qdisc add dev wg0 handle ffff: ingress
tc filter add dev wg0 parent ffff: protocol all u32 match u32 0 0 action mirred egress redirect dev ifb0
tc qdisc add dev ifb0 root handle 1: htb default 10
tc class add dev ifb0 parent 1: classid 1:1 htb rate 50mbit ceil 50mbit
tc class add dev ifb0 parent 1:1 classid 1:10 htb rate 50mbit ceil 50mbit
```
### Database Schema
Settings stored in `general_table`:
- `bandwidth_enabled` (boolean)
- `download_limit_mbps` (integer)
- `upload_limit_mbps` (integer)
### API Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/admin/general` | Get current settings |
| POST | `/api/admin/general` | Update settings (triggers restart) |
| GET | `/api/admin/bandwidth-status` | Check IFB availability |
## Troubleshooting
### Bandwidth limits not applied
1. Check if container has `NET_ADMIN` capability
2. Verify WireGuard interface is running: `wg show`
3. Check tc rules: `tc qdisc show dev wg0`
### Upload limiting not working
1. Check IFB module: `lsmod | grep ifb`
2. Ensure container has `SYS_MODULE` capability
3. Some cloud VPS may not support IFB kernel module
### Settings not saving
Check browser console and server logs for API errors.

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

@ -30,6 +30,42 @@
:description="$t('admin.general.jsonDesc')" :description="$t('admin.general.jsonDesc')"
/> />
</FormGroup> </FormGroup>
<FormGroup>
<FormHeading>{{ $t('admin.general.bandwidth') }}</FormHeading>
<FormSwitchField
id="bandwidthEnabled"
v-model="data.bandwidthEnabled"
:label="$t('admin.general.bandwidthEnabled')"
:description="$t('admin.general.bandwidthEnabledDesc')"
/>
<FormNumberField
v-if="data.bandwidthEnabled"
id="downloadLimit"
v-model="data.downloadLimitMbps"
:label="$t('admin.general.downloadLimit')"
:description="$t('admin.general.downloadLimitDesc')"
/>
<FormNumberField
v-if="data.bandwidthEnabled"
id="uploadLimit"
v-model="data.uploadLimitMbps"
:label="$t('admin.general.uploadLimit')"
:description="$t('admin.general.uploadLimitDesc')"
/>
<div
v-if="
data.bandwidthEnabled &&
data.uploadLimitMbps > 0 &&
bandwidthStatus &&
!bandwidthStatus.ifbAvailable
"
class="rounded-md bg-yellow-50 p-3 dark:bg-yellow-900/20"
>
<p class="text-sm text-yellow-700 dark:text-yellow-400">
{{ $t('admin.general.ifbWarning') }}
</p>
</div>
</FormGroup>
<FormGroup> <FormGroup>
<FormHeading>{{ $t('form.actions') }}</FormHeading> <FormHeading>{{ $t('form.actions') }}</FormHeading>
<FormPrimaryActionField type="submit" :label="$t('form.save')" /> <FormPrimaryActionField type="submit" :label="$t('form.save')" />
@ -45,6 +81,10 @@ const { data: _data, refresh } = await useFetch(`/api/admin/general`, {
}); });
const data = toRef(_data.value); const data = toRef(_data.value);
const { data: bandwidthStatus } = await useFetch(`/api/admin/bandwidth-status`, {
method: 'get',
});
const _submit = useSubmit( const _submit = useSubmit(
`/api/admin/general`, `/api/admin/general`,
{ {

14
src/i18n/locales/bn.json

@ -148,7 +148,15 @@
"json": "JSON", "json": "JSON",
"jsonDesc": "JSON ফরম্যাটে মেট্রিক্সের রুট", "jsonDesc": "JSON ফরম্যাটে মেট্রিক্সের রুট",
"prometheus": "Prometheus", "prometheus": "Prometheus",
"prometheusDesc": "Prometheus মেট্রিক্সের রুট" "prometheusDesc": "Prometheus মেট্রিক্সের রুট",
"bandwidth": "ব্যান্ডউইথ সীমাবদ্ধকরণ",
"bandwidthEnabled": "ব্যান্ডউইথ সীমাবদ্ধকরণ সক্রিয় করুন",
"bandwidthEnabledDesc": "সমস্ত VPN ট্রাফিকে গ্লোবাল ব্যান্ডউইথ সীমা প্রয়োগ করুন",
"downloadLimit": "ডাউনলোড সীমা (Mbps)",
"downloadLimitDesc": "ক্লায়েন্টদের জন্য সর্বোচ্চ ডাউনলোড গতি (0 = সীমাহীন)",
"uploadLimit": "আপলোড সীমা (Mbps)",
"uploadLimitDesc": "ক্লায়েন্টদের থেকে সর্বোচ্চ আপলোড গতি (0 = সীমাহীন)",
"ifbWarning": "আপলোড সীমাবদ্ধকরণের জন্য IFB kernel module প্রয়োজন, যা এই হোস্টে উপলব্ধ নয়"
}, },
"config": { "config": {
"connection": "সংযোগ", "connection": "সংযোগ",
@ -211,7 +219,9 @@
"general": { "general": {
"sessionTimeout": "সেশন টাইমআউট", "sessionTimeout": "সেশন টাইমআউট",
"metricsEnabled": "মেট্রিক্স", "metricsEnabled": "মেট্রিক্স",
"metricsPassword": "মেট্রিক্স পাসওয়ার্ড" "metricsPassword": "মেট্রিক্স পাসওয়ার্ড",
"bandwidthEnabled": "ব্যান্ডউইথ সীমাবদ্ধকরণ",
"bandwidthLimit": "ব্যান্ডউইথ সীমা"
}, },
"interface": { "interface": {
"cidr": "CIDR", "cidr": "CIDR",

14
src/i18n/locales/de.json

@ -145,7 +145,15 @@
"json": "JSON", "json": "JSON",
"jsonDesc": "Pfad zu den Statistiken als JSON", "jsonDesc": "Pfad zu den Statistiken als JSON",
"prometheus": "Prometheus", "prometheus": "Prometheus",
"prometheusDesc": "Pfad zu den Prometheus-Statistiken" "prometheusDesc": "Pfad zu den Prometheus-Statistiken",
"bandwidth": "Bandbreitenbegrenzung",
"bandwidthEnabled": "Bandbreitenbegrenzung aktivieren",
"bandwidthEnabledDesc": "Globale Bandbreitenlimits auf den gesamten VPN-Datenverkehr anwenden",
"downloadLimit": "Download-Limit (Mbps)",
"downloadLimitDesc": "Maximale Download-Geschwindigkeit für Clients (0 = unbegrenzt)",
"uploadLimit": "Upload-Limit (Mbps)",
"uploadLimitDesc": "Maximale Upload-Geschwindigkeit von Clients (0 = unbegrenzt)",
"ifbWarning": "Upload-Begrenzung erfordert das IFB-Kernelmodul, das auf diesem Host nicht verfügbar ist"
}, },
"config": { "config": {
"connection": "Verbindung", "connection": "Verbindung",
@ -208,7 +216,9 @@
"general": { "general": {
"sessionTimeout": "Sitzungszeitüberschreitung", "sessionTimeout": "Sitzungszeitüberschreitung",
"metricsEnabled": "Statistiken", "metricsEnabled": "Statistiken",
"metricsPassword": "Passwort für Statistiken" "metricsPassword": "Passwort für Statistiken",
"bandwidthEnabled": "Bandbreitenbegrenzung",
"bandwidthLimit": "Bandbreitenlimit"
}, },
"interface": { "interface": {
"cidr": "CIDR", "cidr": "CIDR",

14
src/i18n/locales/en.json

@ -152,7 +152,15 @@
"json": "JSON", "json": "JSON",
"jsonDesc": "Route for metrics in JSON format", "jsonDesc": "Route for metrics in JSON format",
"prometheus": "Prometheus", "prometheus": "Prometheus",
"prometheusDesc": "Route for Prometheus metrics" "prometheusDesc": "Route for Prometheus metrics",
"bandwidth": "Bandwidth Limiting",
"bandwidthEnabled": "Enable Bandwidth Limiting",
"bandwidthEnabledDesc": "Apply global bandwidth limits to all VPN traffic",
"downloadLimit": "Download Limit (Mbps)",
"downloadLimitDesc": "Maximum download speed for clients (0 = unlimited)",
"uploadLimit": "Upload Limit (Mbps)",
"uploadLimitDesc": "Maximum upload speed from clients (0 = unlimited)",
"ifbWarning": "Upload limiting requires IFB kernel module which is not available on this host"
}, },
"config": { "config": {
"connection": "Connection", "connection": "Connection",
@ -215,7 +223,9 @@
"general": { "general": {
"sessionTimeout": "Session Timeout", "sessionTimeout": "Session Timeout",
"metricsEnabled": "Metrics", "metricsEnabled": "Metrics",
"metricsPassword": "Metrics Password" "metricsPassword": "Metrics Password",
"bandwidthEnabled": "Bandwidth Enabled",
"bandwidthLimit": "Bandwidth Limit"
}, },
"interface": { "interface": {
"cidr": "CIDR", "cidr": "CIDR",

14
src/i18n/locales/es.json

@ -148,7 +148,15 @@
"json": "JSON", "json": "JSON",
"jsonDesc": "Ruta para métricas en formato JSON", "jsonDesc": "Ruta para métricas en formato JSON",
"prometheus": "Prometheus", "prometheus": "Prometheus",
"prometheusDesc": "Ruta para métricas de Prometheus" "prometheusDesc": "Ruta para métricas de Prometheus",
"bandwidth": "Limitación de ancho de banda",
"bandwidthEnabled": "Habilitar limitación de ancho de banda",
"bandwidthEnabledDesc": "Aplicar límites globales de ancho de banda a todo el tráfico VPN",
"downloadLimit": "Límite de descarga (Mbps)",
"downloadLimitDesc": "Velocidad máxima de descarga para clientes (0 = ilimitado)",
"uploadLimit": "Límite de subida (Mbps)",
"uploadLimitDesc": "Velocidad máxima de subida desde clientes (0 = ilimitado)",
"ifbWarning": "La limitación de subida requiere el módulo del kernel IFB, que no está disponible en este host"
}, },
"config": { "config": {
"connection": "Conexión", "connection": "Conexión",
@ -211,7 +219,9 @@
"general": { "general": {
"sessionTimeout": "Tiempo de sesión", "sessionTimeout": "Tiempo de sesión",
"metricsEnabled": "Métricas", "metricsEnabled": "Métricas",
"metricsPassword": "Contraseña de métricas" "metricsPassword": "Contraseña de métricas",
"bandwidthEnabled": "Limitación de ancho de banda",
"bandwidthLimit": "Límite de ancho de banda"
}, },
"interface": { "interface": {
"cidr": "CIDR", "cidr": "CIDR",

14
src/i18n/locales/fr.json

@ -152,7 +152,15 @@
"json": "JSON", "json": "JSON",
"jsonDesc": "Route pour les métriques au format JSON", "jsonDesc": "Route pour les métriques au format JSON",
"prometheus": "Prometheus", "prometheus": "Prometheus",
"prometheusDesc": "Route pour les métriques Prometheus" "prometheusDesc": "Route pour les métriques Prometheus",
"bandwidth": "Limitation de bande passante",
"bandwidthEnabled": "Activer la limitation de bande passante",
"bandwidthEnabledDesc": "Appliquer des limites globales de bande passante à tout le trafic VPN",
"downloadLimit": "Limite de téléchargement (Mbps)",
"downloadLimitDesc": "Vitesse de téléchargement maximale pour les clients (0 = illimité)",
"uploadLimit": "Limite d’envoi (Mbps)",
"uploadLimitDesc": "Vitesse d’envoi maximale depuis les clients (0 = illimité)",
"ifbWarning": "La limitation d’envoi nécessite le module kernel IFB, non disponible sur cet hôte"
}, },
"config": { "config": {
"connection": "Connexion", "connection": "Connexion",
@ -215,7 +223,9 @@
"general": { "general": {
"sessionTimeout": "Délai d'expiration de la session", "sessionTimeout": "Délai d'expiration de la session",
"metricsEnabled": "Métriques", "metricsEnabled": "Métriques",
"metricsPassword": "Mot de passe métriques" "metricsPassword": "Mot de passe métriques",
"bandwidthEnabled": "Limitation de bande passante",
"bandwidthLimit": "Limite de bande passante"
}, },
"interface": { "interface": {
"cidr": "CIDR", "cidr": "CIDR",

14
src/i18n/locales/id.json

@ -148,7 +148,15 @@
"json": "JSON", "json": "JSON",
"jsonDesc": "Rute untuk metrik dalam format JSON", "jsonDesc": "Rute untuk metrik dalam format JSON",
"prometheus": "Prometheus", "prometheus": "Prometheus",
"prometheusDesc": "Rute untuk metrik Prometheus" "prometheusDesc": "Rute untuk metrik Prometheus",
"bandwidth": "Pembatasan Bandwidth",
"bandwidthEnabled": "Aktifkan Pembatasan Bandwidth",
"bandwidthEnabledDesc": "Terapkan batas bandwidth global ke semua lalu lintas VPN",
"downloadLimit": "Batas Unduhan (Mbps)",
"downloadLimitDesc": "Kecepatan unduhan maksimum untuk klien (0 = tidak terbatas)",
"uploadLimit": "Batas Unggahan (Mbps)",
"uploadLimitDesc": "Kecepatan unggahan maksimum dari klien (0 = tidak terbatas)",
"ifbWarning": "Pembatasan unggahan memerlukan modul kernel IFB yang tidak tersedia di host ini"
}, },
"config": { "config": {
"connection": "Koneksi", "connection": "Koneksi",
@ -211,7 +219,9 @@
"general": { "general": {
"sessionTimeout": "Waktu Sesi Habis", "sessionTimeout": "Waktu Sesi Habis",
"metricsEnabled": "Metrik", "metricsEnabled": "Metrik",
"metricsPassword": "Kata Sandi Metrik" "metricsPassword": "Kata Sandi Metrik",
"bandwidthEnabled": "Pembatasan Bandwidth",
"bandwidthLimit": "Batas Bandwidth"
}, },
"interface": { "interface": {
"cidr": "CIDR", "cidr": "CIDR",

14
src/i18n/locales/it.json

@ -148,7 +148,15 @@
"json": "JSON", "json": "JSON",
"jsonDesc": "Percorso per metriche in formato JSON", "jsonDesc": "Percorso per metriche in formato JSON",
"prometheus": "Prometheus", "prometheus": "Prometheus",
"prometheusDesc": "Percorso per metriche Prometheus" "prometheusDesc": "Percorso per metriche Prometheus",
"bandwidth": "Limitazione della larghezza di banda",
"bandwidthEnabled": "Abilita limitazione della larghezza di banda",
"bandwidthEnabledDesc": "Applica limiti globali di larghezza di banda a tutto il traffico VPN",
"downloadLimit": "Limite di download (Mbps)",
"downloadLimitDesc": "Velocità massima di download per i client (0 = illimitato)",
"uploadLimit": "Limite di upload (Mbps)",
"uploadLimitDesc": "Velocità massima di upload dai client (0 = illimitato)",
"ifbWarning": "La limitazione dell'upload richiede il modulo kernel IFB, non disponibile su questo host"
}, },
"config": { "config": {
"connection": "Connessione", "connection": "Connessione",
@ -211,7 +219,9 @@
"general": { "general": {
"sessionTimeout": "Timeout Sessione", "sessionTimeout": "Timeout Sessione",
"metricsEnabled": "Metriche", "metricsEnabled": "Metriche",
"metricsPassword": "Password Metriche" "metricsPassword": "Password Metriche",
"bandwidthEnabled": "Limitazione larghezza di banda",
"bandwidthLimit": "Limite larghezza di banda"
}, },
"interface": { "interface": {
"cidr": "CIDR", "cidr": "CIDR",

14
src/i18n/locales/ko.json

@ -148,7 +148,15 @@
"json": "JSON", "json": "JSON",
"jsonDesc": "JSON 형식의 메트릭 경로", "jsonDesc": "JSON 형식의 메트릭 경로",
"prometheus": "프로메테우스", "prometheus": "프로메테우스",
"prometheusDesc": "Prometheus 메트릭을 위한 경로" "prometheusDesc": "Prometheus 메트릭을 위한 경로",
"bandwidth": "대역폭 제한",
"bandwidthEnabled": "대역폭 제한 활성화",
"bandwidthEnabledDesc": "모든 VPN 트래픽에 전역 대역폭 제한 적용",
"downloadLimit": "다운로드 제한 (Mbps)",
"downloadLimitDesc": "클라이언트의 최대 다운로드 속도 (0 = 무제한)",
"uploadLimit": "업로드 제한 (Mbps)",
"uploadLimitDesc": "클라이언트의 최대 업로드 속도 (0 = 무제한)",
"ifbWarning": "업로드 제한은 IFB 커널 모듈이 필요하지만 이 호스트에서 사용할 수 없습니다"
}, },
"config": { "config": {
"connection": "연결", "connection": "연결",
@ -211,7 +219,9 @@
"general": { "general": {
"sessionTimeout": "세션 타임아웃", "sessionTimeout": "세션 타임아웃",
"metricsEnabled": "메트릭", "metricsEnabled": "메트릭",
"metricsPassword": "메트릭 비밀번호" "metricsPassword": "메트릭 비밀번호",
"bandwidthEnabled": "대역폭 제한 활성화",
"bandwidthLimit": "대역폭 제한"
}, },
"interface": { "interface": {
"cidr": "CIDR", "cidr": "CIDR",

14
src/i18n/locales/pl.json

@ -148,7 +148,15 @@
"json": "JSON", "json": "JSON",
"jsonDesc": "Ścieżka dla metryk w formacie JSON", "jsonDesc": "Ścieżka dla metryk w formacie JSON",
"prometheus": "Prometheus", "prometheus": "Prometheus",
"prometheusDesc": "Ścieżka dla metryk Prometheus" "prometheusDesc": "Ścieżka dla metryk Prometheus",
"bandwidth": "Ograniczanie przepustowości",
"bandwidthEnabled": "Włącz ograniczanie przepustowości",
"bandwidthEnabledDesc": "Zastosuj globalne limity przepustowości do całego ruchu VPN",
"downloadLimit": "Limit pobierania (Mbps)",
"downloadLimitDesc": "Maksymalna prędkość pobierania dla klientów (0 = bez limitu)",
"uploadLimit": "Limit wysyłania (Mbps)",
"uploadLimitDesc": "Maksymalna prędkość wysyłania od klientów (0 = bez limitu)",
"ifbWarning": "Ograniczanie wysyłania wymaga modułu jądra IFB, który nie jest dostępny na tym hoście"
}, },
"config": { "config": {
"connection": "Połączenie", "connection": "Połączenie",
@ -211,7 +219,9 @@
"general": { "general": {
"sessionTimeout": "Limit czasu sesji", "sessionTimeout": "Limit czasu sesji",
"metricsEnabled": "Metryki", "metricsEnabled": "Metryki",
"metricsPassword": "Hasło do metryk" "metricsPassword": "Hasło do metryk",
"bandwidthEnabled": "Ograniczanie przepustowości",
"bandwidthLimit": "Limit przepustowości"
}, },
"interface": { "interface": {
"cidr": "CIDR", "cidr": "CIDR",

14
src/i18n/locales/pt-BR.json

@ -148,7 +148,15 @@
"json": "JSON", "json": "JSON",
"jsonDesc": "Rota para métricas em formato JSON", "jsonDesc": "Rota para métricas em formato JSON",
"prometheus": "Prometheus", "prometheus": "Prometheus",
"prometheusDesc": "Rota para métricas do Prometheus" "prometheusDesc": "Rota para métricas do Prometheus",
"bandwidth": "Limitação de largura de banda",
"bandwidthEnabled": "Ativar limitação de largura de banda",
"bandwidthEnabledDesc": "Aplicar limites globais de largura de banda a todo o tráfego VPN",
"downloadLimit": "Limite de download (Mbps)",
"downloadLimitDesc": "Velocidade máxima de download para clientes (0 = ilimitado)",
"uploadLimit": "Limite de upload (Mbps)",
"uploadLimitDesc": "Velocidade máxima de upload dos clientes (0 = ilimitado)",
"ifbWarning": "A limitação de upload requer o módulo de kernel IFB, que não está disponível neste host"
}, },
"config": { "config": {
"connection": "Conexão", "connection": "Conexão",
@ -211,7 +219,9 @@
"general": { "general": {
"sessionTimeout": "Tempo limite da sessão", "sessionTimeout": "Tempo limite da sessão",
"metricsEnabled": "Métricas", "metricsEnabled": "Métricas",
"metricsPassword": "Senha de métricas" "metricsPassword": "Senha de métricas",
"bandwidthEnabled": "Limitação de largura de banda",
"bandwidthLimit": "Limite de largura de banda"
}, },
"interface": { "interface": {
"cidr": "CIDR", "cidr": "CIDR",

14
src/i18n/locales/ru.json

@ -152,7 +152,15 @@
"json": "JSON", "json": "JSON",
"jsonDesc": "Путь для метрик в формате JSON", "jsonDesc": "Путь для метрик в формате JSON",
"prometheus": "Prometheus", "prometheus": "Prometheus",
"prometheusDesc": "Путь для метрик Prometheus" "prometheusDesc": "Путь для метрик Prometheus",
"bandwidth": "Ограничение пропускной способности",
"bandwidthEnabled": "Включить ограничение пропускной способности",
"bandwidthEnabledDesc": "Применить глобальные ограничения пропускной способности ко всему VPN-трафику",
"downloadLimit": "Лимит загрузки (Mbps)",
"downloadLimitDesc": "Максимальная скорость загрузки для клиентов (0 = без ограничений)",
"uploadLimit": "Лимит выгрузки (Mbps)",
"uploadLimitDesc": "Максимальная скорость выгрузки от клиентов (0 = без ограничений)",
"ifbWarning": "Ограничение выгрузки требует модуль ядра IFB, который недоступен на этом хосте"
}, },
"config": { "config": {
"connection": "Соединение", "connection": "Соединение",
@ -215,7 +223,9 @@
"general": { "general": {
"sessionTimeout": "Время жизни сессии", "sessionTimeout": "Время жизни сессии",
"metricsEnabled": "Метрики", "metricsEnabled": "Метрики",
"metricsPassword": "Пароль для метрик" "metricsPassword": "Пароль для метрик",
"bandwidthEnabled": "Ограничение пропускной способности",
"bandwidthLimit": "Лимит пропускной способности"
}, },
"interface": { "interface": {
"cidr": "CIDR", "cidr": "CIDR",

14
src/i18n/locales/tr.json

@ -148,7 +148,15 @@
"json": "JSON", "json": "JSON",
"jsonDesc": "JSON formatında metrikler için rota", "jsonDesc": "JSON formatında metrikler için rota",
"prometheus": "Prometheus", "prometheus": "Prometheus",
"prometheusDesc": "Prometheus metrikleri için rota" "prometheusDesc": "Prometheus metrikleri için rota",
"bandwidth": "Bant Genişliği Sınırlaması",
"bandwidthEnabled": "Bant Genişliği Sınırlamasını Etkinleştir",
"bandwidthEnabledDesc": "Tüm VPN trafiğine global bant genişliği limitleri uygula",
"downloadLimit": "İndirme Limiti (Mbps)",
"downloadLimitDesc": "İstemciler için maksimum indirme hızı (0 = sınırsız)",
"uploadLimit": "Yükleme Limiti (Mbps)",
"uploadLimitDesc": "İstemcilerden maksimum yükleme hızı (0 = sınırsız)",
"ifbWarning": "Yükleme sınırlaması IFB çekirdek modülü gerektirir ve bu ana makinede mevcut değildir"
}, },
"config": { "config": {
"connection": "Bağlantı", "connection": "Bağlantı",
@ -211,7 +219,9 @@
"general": { "general": {
"sessionTimeout": "Oturum Zaman Aşımı", "sessionTimeout": "Oturum Zaman Aşımı",
"metricsEnabled": "Metrikler", "metricsEnabled": "Metrikler",
"metricsPassword": "Metrik Şifresi" "metricsPassword": "Metrik Şifresi",
"bandwidthEnabled": "Bant Genişliği Sınırlaması",
"bandwidthLimit": "Bant Genişliği Limiti"
}, },
"interface": { "interface": {
"cidr": "CIDR", "cidr": "CIDR",

14
src/i18n/locales/uk.json

@ -152,7 +152,15 @@
"json": "JSON", "json": "JSON",
"jsonDesc": "Маршрут для метрик у форматі JSON", "jsonDesc": "Маршрут для метрик у форматі JSON",
"prometheus": "Prometheus", "prometheus": "Prometheus",
"prometheusDesc": "Маршрут для метрики Prometheus" "prometheusDesc": "Маршрут для метрики Prometheus",
"bandwidth": "Обмеження пропускної здатності",
"bandwidthEnabled": "Увімкнути обмеження пропускної здатності",
"bandwidthEnabledDesc": "Застосувати глобальні обмеження пропускної здатності до всього VPN-трафіку",
"downloadLimit": "Ліміт завантаження (Mbps)",
"downloadLimitDesc": "Максимальна швидкість завантаження для клієнтів (0 = без обмежень)",
"uploadLimit": "Ліміт вивантаження (Mbps)",
"uploadLimitDesc": "Максимальна швидкість вивантаження від клієнтів (0 = без обмежень)",
"ifbWarning": "Обмеження вивантаження потребує модуль ядра IFB, який недоступний на цьому хості"
}, },
"config": { "config": {
"connection": "З'єднання", "connection": "З'єднання",
@ -215,7 +223,9 @@
"general": { "general": {
"sessionTimeout": "Час очікування сеансу", "sessionTimeout": "Час очікування сеансу",
"metricsEnabled": "Метрики", "metricsEnabled": "Метрики",
"metricsPassword": "Пароль метрик" "metricsPassword": "Пароль метрик",
"bandwidthEnabled": "Обмеження пропускної здатності",
"bandwidthLimit": "Ліміт пропускної здатності"
}, },
"interface": { "interface": {
"cidr": "CIDR", "cidr": "CIDR",

14
src/i18n/locales/zh-CN.json

@ -152,7 +152,15 @@
"json": "JSON格式", "json": "JSON格式",
"jsonDesc": "获取JSON格式的监控数据路由", "jsonDesc": "获取JSON格式的监控数据路由",
"prometheus": "Prometheus格式", "prometheus": "Prometheus格式",
"prometheusDesc": "获取Prometheus格式监控数据的路由" "prometheusDesc": "获取Prometheus格式监控数据的路由",
"bandwidth": "带宽限制",
"bandwidthEnabled": "启用带宽限制",
"bandwidthEnabledDesc": "对所有VPN流量应用全局带宽限制",
"downloadLimit": "下载限制 (Mbps)",
"downloadLimitDesc": "客户端最大下载速度(0 = 不限制)",
"uploadLimit": "上传限制 (Mbps)",
"uploadLimitDesc": "客户端最大上传速度(0 = 不限制)",
"ifbWarning": "上传限制需要IFB内核模块,但该模块在此主机上不可用"
}, },
"config": { "config": {
"connection": "连接设置", "connection": "连接设置",
@ -215,7 +223,9 @@
"general": { "general": {
"sessionTimeout": "会话超时", "sessionTimeout": "会话超时",
"metricsEnabled": "启用监控", "metricsEnabled": "启用监控",
"metricsPassword": "监控密码" "metricsPassword": "监控密码",
"bandwidthEnabled": "启用带宽限制",
"bandwidthLimit": "带宽限制值"
}, },
"interface": { "interface": {
"cidr": "CIDR", "cidr": "CIDR",

14
src/i18n/locales/zh-HK.json

@ -146,7 +146,15 @@
"json": "JSON", "json": "JSON",
"jsonDesc": "JSON格式指標的路由", "jsonDesc": "JSON格式指標的路由",
"prometheus": "Prometheus", "prometheus": "Prometheus",
"prometheusDesc": "Prometheus指標的路由" "prometheusDesc": "Prometheus指標的路由",
"bandwidth": "頻寬限制",
"bandwidthEnabled": "啟用頻寬限制",
"bandwidthEnabledDesc": "對所有VPN流量應用全局頻寬限制",
"downloadLimit": "下載限制 (Mbps)",
"downloadLimitDesc": "客戶端最大下載速度(0 = 不限制)",
"uploadLimit": "上傳限制 (Mbps)",
"uploadLimitDesc": "客戶端最大上傳速度(0 = 不限制)",
"ifbWarning": "上傳限制需要IFB核心模組,但此主機上未能使用"
}, },
"config": { "config": {
"connection": "連線", "connection": "連線",
@ -209,7 +217,9 @@
"general": { "general": {
"sessionTimeout": "工作階段逾時", "sessionTimeout": "工作階段逾時",
"metricsEnabled": "指標", "metricsEnabled": "指標",
"metricsPassword": "指標密碼" "metricsPassword": "指標密碼",
"bandwidthEnabled": "啟用頻寬限制",
"bandwidthLimit": "頻寬限制值"
}, },
"interface": { "interface": {
"cidr": "CIDR", "cidr": "CIDR",

14
src/i18n/locales/zh-TW.json

@ -152,7 +152,15 @@
"json": "JSON", "json": "JSON",
"jsonDesc": "提供 JSON 格式計量的路由", "jsonDesc": "提供 JSON 格式計量的路由",
"prometheus": "Prometheus", "prometheus": "Prometheus",
"prometheusDesc": "提供 Prometheus 計量的路由" "prometheusDesc": "提供 Prometheus 計量的路由",
"bandwidth": "頻寬限制",
"bandwidthEnabled": "啟用頻寬限制",
"bandwidthEnabledDesc": "對所有 VPN 流量套用全域頻寬限制",
"downloadLimit": "下載限制 (Mbps)",
"downloadLimitDesc": "用戶端最大下載速度(0 = 不限制)",
"uploadLimit": "上傳限制 (Mbps)",
"uploadLimitDesc": "用戶端最大上傳速度(0 = 不限制)",
"ifbWarning": "上傳限制需要 IFB 核心模組,但此主機上無法使用"
}, },
"config": { "config": {
"connection": "連線", "connection": "連線",
@ -215,7 +223,9 @@
"general": { "general": {
"sessionTimeout": "工作階段逾時", "sessionTimeout": "工作階段逾時",
"metricsEnabled": "計量", "metricsEnabled": "計量",
"metricsPassword": "計量密碼" "metricsPassword": "計量密碼",
"bandwidthEnabled": "啟用頻寬限制",
"bandwidthLimit": "頻寬限制值"
}, },
"interface": { "interface": {
"cidr": "CIDR", "cidr": "CIDR",

15
src/server/api/admin/bandwidth-status.get.ts

@ -0,0 +1,15 @@
export default definePermissionEventHandler('admin', 'any', async () => {
const config = await Database.general.getBandwidthConfig();
const ifbAvailable = WireGuard.getIfbAvailable();
return {
enabled: config.bandwidthEnabled,
downloadLimitMbps: config.downloadLimitMbps,
uploadLimitMbps: config.uploadLimitMbps,
ifbAvailable,
uploadLimitActive:
config.bandwidthEnabled &&
config.uploadLimitMbps > 0 &&
ifbAvailable,
};
});

15
src/server/api/admin/general.post.ts

@ -8,7 +8,22 @@ export default definePermissionEventHandler(
event, event,
validateZod(GeneralUpdateSchema, event) validateZod(GeneralUpdateSchema, event)
); );
// Get current settings to check if bandwidth changed
const current = await Database.general.getConfig();
const bandwidthChanged =
current.bandwidthEnabled !== data.bandwidthEnabled ||
current.downloadLimitMbps !== data.downloadLimitMbps ||
current.uploadLimitMbps !== data.uploadLimitMbps;
await Database.general.update(data); await Database.general.update(data);
await WireGuard.saveConfig();
// Auto-restart interface if bandwidth settings changed
if (bandwidthChanged) {
await WireGuard.Restart();
}
return { success: true }; return { success: true };
} }
); );

3
src/server/database/migrations/0003_bandwidth_limit.sql

@ -0,0 +1,3 @@
ALTER TABLE `general_table` ADD `bandwidth_enabled` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE `general_table` ADD `download_limit_mbps` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE `general_table` ADD `upload_limit_mbps` integer DEFAULT 0 NOT NULL;

1000
src/server/database/migrations/meta/0003_snapshot.json

File diff suppressed because it is too large

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

@ -22,6 +22,13 @@
"when": 1761298328460, "when": 1761298328460,
"tag": "0002_keen_sleepwalker", "tag": "0002_keen_sleepwalker",
"breakpoints": true "breakpoints": true
},
{
"idx": 3,
"version": "6",
"when": 1735460000000,
"tag": "0003_bandwidth_limit",
"breakpoints": true
} }
] ]
} }

6
src/server/database/repositories/general/schema.ts

@ -13,6 +13,12 @@ export const general = sqliteTable('general_table', {
metricsJson: int('metrics_json', { mode: 'boolean' }).notNull(), metricsJson: int('metrics_json', { mode: 'boolean' }).notNull(),
metricsPassword: text('metrics_password'), metricsPassword: text('metrics_password'),
bandwidthEnabled: int('bandwidth_enabled', { mode: 'boolean' })
.notNull()
.default(false),
downloadLimitMbps: int('download_limit_mbps').notNull().default(0),
uploadLimitMbps: int('upload_limit_mbps').notNull().default(0),
createdAt: text('created_at') createdAt: text('created_at')
.notNull() .notNull()
.default(sql`(CURRENT_TIMESTAMP)`), .default(sql`(CURRENT_TIMESTAMP)`),

22
src/server/database/repositories/general/service.ts

@ -36,6 +36,18 @@ function createPreparedStatement(db: DBType) {
metricsPrometheus: true, metricsPrometheus: true,
metricsJson: true, metricsJson: true,
metricsPassword: true, metricsPassword: true,
bandwidthEnabled: true,
downloadLimitMbps: true,
uploadLimitMbps: true,
},
})
.prepare(),
getBandwidthConfig: db.query.general
.findFirst({
columns: {
bandwidthEnabled: true,
downloadLimitMbps: true,
uploadLimitMbps: true,
}, },
}) })
.prepare(), .prepare(),
@ -128,4 +140,14 @@ export class GeneralService {
return result; return result;
} }
async getBandwidthConfig() {
const result = await this.#statements.getBandwidthConfig.execute();
if (!result) {
throw new Error('General Config not found');
}
return result;
}
} }

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

@ -13,11 +13,21 @@ const metricsPassword = z
.min(1, { message: t('zod.general.metricsPassword') }) .min(1, { message: t('zod.general.metricsPassword') })
.nullable(); .nullable();
const bandwidthEnabled = z.boolean({
message: t('zod.general.bandwidthEnabled'),
});
const bandwidthLimit = z
.number({ message: t('zod.general.bandwidthLimit') })
.min(0);
export const GeneralUpdateSchema = z.object({ export const GeneralUpdateSchema = z.object({
sessionTimeout: sessionTimeout, sessionTimeout: sessionTimeout,
metricsPrometheus: metricsEnabled, metricsPrometheus: metricsEnabled,
metricsJson: metricsEnabled, metricsJson: metricsEnabled,
metricsPassword: metricsPassword, metricsPassword: metricsPassword,
bandwidthEnabled: bandwidthEnabled,
downloadLimitMbps: bandwidthLimit,
uploadLimitMbps: bandwidthLimit,
}); });
export type GeneralUpdateType = z.infer<typeof GeneralUpdateSchema>; export type GeneralUpdateType = z.infer<typeof GeneralUpdateSchema>;

23
src/server/utils/WireGuard.ts

@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
import debug from 'debug'; import debug from 'debug';
import { encodeQR } from 'qr'; import { encodeQR } from 'qr';
import type { InterfaceType } from '#db/repositories/interface/types'; import type { InterfaceType } from '#db/repositories/interface/types';
import { checkIfbAvailable } from './bandwidth';
const WG_DEBUG = debug('WireGuard'); const WG_DEBUG = debug('WireGuard');
@ -9,6 +10,15 @@ const generateRandomHeaderValue = () =>
Math.floor(Math.random() * 2147483642) + 5; Math.floor(Math.random() * 2147483642) + 5;
class WireGuard { class WireGuard {
#ifbAvailable: boolean = true;
/**
* Get IFB availability status for upload limiting
*/
getIfbAvailable(): boolean {
return this.#ifbAvailable;
}
/** /**
* Save and sync config * Save and sync config
*/ */
@ -26,11 +36,18 @@ class WireGuard {
async #saveWireguardConfig(wgInterface: InterfaceType) { async #saveWireguardConfig(wgInterface: InterfaceType) {
const clients = await Database.clients.getAll(); const clients = await Database.clients.getAll();
const hooks = await Database.hooks.get(); const hooks = await Database.hooks.get();
const bandwidthConfig = await Database.general.getBandwidthConfig();
const result = []; const result = [];
result.push( result.push(
wg.generateServerInterface(wgInterface, hooks, { wg.generateServerInterface(wgInterface, hooks, {
enableIpv6: !WG_ENV.DISABLE_IPV6, enableIpv6: !WG_ENV.DISABLE_IPV6,
bandwidth: {
enabled: bandwidthConfig.bandwidthEnabled,
downloadMbps: bandwidthConfig.downloadLimitMbps,
uploadMbps: bandwidthConfig.uploadLimitMbps,
},
ifbAvailable: this.#ifbAvailable,
}) })
); );
@ -199,6 +216,12 @@ class WireGuard {
// let as it has to refetch if keys change // let as it has to refetch if keys change
let wgInterface = await Database.interfaces.get(); let wgInterface = await Database.interfaces.get();
// Check IFB availability for upload limiting
this.#ifbAvailable = await checkIfbAvailable();
if (!this.#ifbAvailable) {
WG_DEBUG('IFB module not available - upload limiting disabled');
}
// default interface has no keys // default interface has no keys
if ( if (
wgInterface.privateKey === '---default---' && wgInterface.privateKey === '---default---' &&

116
src/server/utils/bandwidth.ts

@ -0,0 +1,116 @@
import debug from 'debug';
import { exec } from './cmd';
const BW_DEBUG = debug('Bandwidth');
export interface BandwidthConfig {
enabled: boolean;
downloadMbps: number;
uploadMbps: number;
}
export interface TcCommands {
postUp: string;
postDown: string;
}
// Valid interface name pattern (e.g., wg0, eth0, enp0s3)
const VALID_INTERFACE_PATTERN = /^[a-zA-Z0-9_-]{1,15}$/;
// Bandwidth limits (1 Mbps to 10 Gbps)
const MIN_BANDWIDTH_MBPS = 1;
const MAX_BANDWIDTH_MBPS = 10000;
/**
* Validate interface name to prevent command injection
*/
function isValidInterfaceName(name: string): boolean {
return VALID_INTERFACE_PATTERN.test(name);
}
/**
* Clamp bandwidth value to safe range
*/
function clampBandwidth(value: number): number {
if (value <= 0) return 0;
return Math.min(Math.max(value, MIN_BANDWIDTH_MBPS), MAX_BANDWIDTH_MBPS);
}
/**
* Check if IFB kernel module is available for upload limiting
*/
export async function checkIfbAvailable(): Promise<boolean> {
try {
await exec('modprobe ifb 2>/dev/null || lsmod | grep -q ifb', {
log: false,
});
return true;
} catch {
BW_DEBUG('IFB module not available');
return false;
}
}
/**
* Generate tc commands for bandwidth limiting
* @param interfaceName - WireGuard interface name (e.g., wg0)
* @param config - Bandwidth configuration
* @param ifbAvailable - Whether IFB module is available for upload limiting
*/
export function generateTcCommands(
interfaceName: string,
config: BandwidthConfig,
ifbAvailable: boolean = true
): TcCommands {
// Validate interface name to prevent command injection
if (!isValidInterfaceName(interfaceName)) {
BW_DEBUG(`Invalid interface name: ${interfaceName}`);
return { postUp: '', postDown: '' };
}
// Clamp bandwidth values to safe range
const downloadMbps = clampBandwidth(config.downloadMbps);
const uploadMbps = clampBandwidth(config.uploadMbps);
if (!config.enabled || (downloadMbps === 0 && uploadMbps === 0)) {
return { postUp: '', postDown: '' };
}
const postUpParts: string[] = [];
const postDownParts: string[] = [];
// Download limit (egress on wg interface)
if (downloadMbps > 0) {
postUpParts.push(
`tc qdisc add dev ${interfaceName} root handle 1: htb default 10`,
`tc class add dev ${interfaceName} parent 1: classid 1:1 htb rate ${downloadMbps}mbit ceil ${downloadMbps}mbit`,
`tc class add dev ${interfaceName} parent 1:1 classid 1:10 htb rate ${downloadMbps}mbit ceil ${downloadMbps}mbit`
);
postDownParts.push(
`tc qdisc del dev ${interfaceName} root 2>/dev/null || true`
);
}
// Upload limit (ingress via IFB mirror)
if (uploadMbps > 0 && ifbAvailable) {
const ifbDev = 'ifb0';
postUpParts.push(
`ip link add ${ifbDev} type ifb 2>/dev/null || true`,
`ip link set ${ifbDev} up`,
`tc qdisc add dev ${interfaceName} handle ffff: ingress`,
`tc filter add dev ${interfaceName} parent ffff: protocol all u32 match u32 0 0 action mirred egress redirect dev ${ifbDev}`,
`tc qdisc add dev ${ifbDev} root handle 1: htb default 10`,
`tc class add dev ${ifbDev} parent 1: classid 1:1 htb rate ${uploadMbps}mbit ceil ${uploadMbps}mbit`,
`tc class add dev ${ifbDev} parent 1:1 classid 1:10 htb rate ${uploadMbps}mbit ceil ${uploadMbps}mbit`
);
postDownParts.push(
`tc qdisc del dev ${interfaceName} handle ffff: ingress 2>/dev/null || true`,
`ip link del ${ifbDev} 2>/dev/null || true`
);
}
return {
postUp: postUpParts.join('; '),
postDown: postDownParts.join('; '),
};
}

47
src/server/utils/wgHelper.ts

@ -4,11 +4,20 @@ import type { ClientType } from '#db/repositories/client/types';
import type { InterfaceType } from '#db/repositories/interface/types'; import type { InterfaceType } from '#db/repositories/interface/types';
import type { UserConfigType } from '#db/repositories/userConfig/types'; import type { UserConfigType } from '#db/repositories/userConfig/types';
import type { HooksType } from '#db/repositories/hooks/types'; import type { HooksType } from '#db/repositories/hooks/types';
import {
generateTcCommands,
type BandwidthConfig,
} from './bandwidth';
type Options = { type Options = {
enableIpv6?: boolean; enableIpv6?: boolean;
}; };
type ServerInterfaceOptions = Options & {
bandwidth?: BandwidthConfig;
ifbAvailable?: boolean;
};
const wgExecutable = WG_ENV.WG_EXECUTABLE; const wgExecutable = WG_ENV.WG_EXECUTABLE;
export const wg = { export const wg = {
@ -39,9 +48,9 @@ AllowedIPs = ${allowedIps.join(', ')}${extraLines.length ? `\n${extraLines.join(
generateServerInterface: ( generateServerInterface: (
wgInterface: InterfaceType, wgInterface: InterfaceType,
hooks: HooksType, hooks: HooksType,
options: Options = {} options: ServerInterfaceOptions = {}
) => { ) => {
const { enableIpv6 = true } = options; const { enableIpv6 = true, bandwidth, ifbAvailable = true } = options;
const cidr4 = parseCidr(wgInterface.ipv4Cidr); const cidr4 = parseCidr(wgInterface.ipv4Cidr);
const cidr6 = parseCidr(wgInterface.ipv6Cidr); const cidr6 = parseCidr(wgInterface.ipv6Cidr);
@ -81,6 +90,36 @@ AllowedIPs = ${allowedIps.join(', ')}${extraLines.length ? `\n${extraLines.join(
const extraLines = [...awgLines].filter((v) => v !== null); const extraLines = [...awgLines].filter((v) => v !== null);
// Generate tc commands for bandwidth limiting
let tcPostUp = '';
let tcPostDown = '';
if (bandwidth) {
const tcCommands = generateTcCommands(
wgInterface.name,
bandwidth,
ifbAvailable
);
tcPostUp = tcCommands.postUp;
tcPostDown = tcCommands.postDown;
}
// Combine hooks with tc commands (strip trailing semicolons to avoid ;;)
const stripTrailingSemicolon = (s: string) => s.replace(/;+\s*$/, '');
const finalPostUp = [
iptablesTemplate(hooks.postUp, wgInterface),
tcPostUp,
]
.map((s) => stripTrailingSemicolon(s))
.filter(Boolean)
.join('; ');
const finalPostDown = [
tcPostDown,
iptablesTemplate(hooks.postDown, wgInterface),
]
.map((s) => stripTrailingSemicolon(s))
.filter(Boolean)
.join('; ');
return `# Note: Do not edit this file directly. return `# Note: Do not edit this file directly.
# Your changes will be overwritten! # Your changes will be overwritten!
@ -92,9 +131,9 @@ ListenPort = ${wgInterface.port}
MTU = ${wgInterface.mtu} MTU = ${wgInterface.mtu}
${extraLines.length ? `${extraLines.join('\n')}\n` : ''} ${extraLines.length ? `${extraLines.join('\n')}\n` : ''}
PreUp = ${iptablesTemplate(hooks.preUp, wgInterface)} PreUp = ${iptablesTemplate(hooks.preUp, wgInterface)}
PostUp = ${iptablesTemplate(hooks.postUp, wgInterface)} PostUp = ${finalPostUp}
PreDown = ${iptablesTemplate(hooks.preDown, wgInterface)} PreDown = ${iptablesTemplate(hooks.preDown, wgInterface)}
PostDown = ${iptablesTemplate(hooks.postDown, wgInterface)}`; PostDown = ${finalPostDown}`;
}, },
generateClientConfig: ( generateClientConfig: (

Loading…
Cancel
Save