mirror of https://github.com/wg-easy/wg-easy
Browse Source
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 guidepull/2368/head
32 changed files with 1623 additions and 39 deletions
@ -1,3 +1,7 @@ |
|||
.DS_Store |
|||
*.swp |
|||
node_modules |
|||
node_modules |
|||
# Claude Code local settings |
|||
.claude/ |
|||
test_output.txt |
|||
repomix-output.xml |
|||
|
|||
@ -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. |
|||
@ -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, |
|||
}; |
|||
}); |
|||
@ -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; |
|||
File diff suppressed because it is too large
@ -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('; '), |
|||
}; |
|||
} |
|||
Loading…
Reference in new issue