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 |
.DS_Store |
||||
*.swp |
*.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