feat: complete Bandwidth Manager implementation with QoS and quotas
Implements comprehensive bandwidth management system with QoS traffic shaping, client quotas, and SQM/CAKE integration for OpenWrt. Features: - QoS traffic shaping with rule-based control (application/port/IP/MAC) - Per-rule download/upload limits with 8-level priority system - Time-based scheduling support for rules - Monthly data quotas per client (MAC address) - iptables-based usage tracking with real-time statistics - Configurable quota actions: throttle, block, or notify - Automatic monthly reset with configurable reset day - SQM/CAKE integration with NAT-aware configuration - Link overhead compensation (Ethernet, PPPoE, VLAN) - Alternative FQ_CoDel and HTB qdisc support Components: - RPCD backend (luci.bandwidth-manager): 10 ubus methods * status, list_rules, add_rule, delete_rule * list_quotas, get_quota, set_quota, reset_quota * get_usage_realtime, get_usage_history - 5 JavaScript views: overview, rules, quotas, usage, settings - ACL with read/write permissions for all methods - UCI config with global, SQM, tracking, alerts, rules, and quotas sections - Comprehensive README with API docs and examples Technical implementation: - Traffic tracking via iptables BW_TRACKING chain - Usage database in /tmp/bandwidth_usage.db (pipe-delimited format) - Real-time client usage with 5-second auto-refresh - Historical data with configurable timeframes (1h to 30d) - Per-client quota progress visualization with color-coded bars - TC (traffic control) integration for QoS enforcement Architecture follows SecuBox standards: - RPCD naming convention (luci. prefix) - Menu paths match view file structure - All JavaScript in strict mode - Form-based configuration management - Comprehensive error handling Dependencies: tc, kmod-sched-core, kmod-sched-cake, kmod-ifb, sqm-scripts, iptables, iptables-mod-conntrack-extra, ip-full 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
77d40a1f89
commit
fa9bb2aee7
@ -3,51 +3,14 @@ include $(TOPDIR)/rules.mk
|
|||||||
PKG_NAME:=luci-app-bandwidth-manager
|
PKG_NAME:=luci-app-bandwidth-manager
|
||||||
PKG_VERSION:=1.0.0
|
PKG_VERSION:=1.0.0
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=1
|
||||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
PKG_LICENSE:=Apache-2.0
|
||||||
PKG_LICENSE:=MIT
|
PKG_MAINTAINER:=SecuBox Project <support@secubox.com>
|
||||||
|
|
||||||
include $(INCLUDE_DIR)/package.mk
|
LUCI_TITLE:=Bandwidth Manager - QoS & Traffic Control
|
||||||
|
LUCI_DESCRIPTION:=Advanced bandwidth management with QoS rules, client quotas, and SQM integration
|
||||||
|
LUCI_DEPENDS:=+luci-base +rpcd +tc +kmod-sched-core +kmod-sched-cake +kmod-ifb +sqm-scripts +iptables +iptables-mod-conntrack-extra +ip-full
|
||||||
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
define Package/luci-app-bandwidth-manager
|
include ../../luci.mk
|
||||||
SECTION:=luci
|
|
||||||
CATEGORY:=LuCI
|
|
||||||
SUBMENU:=3. Applications
|
|
||||||
TITLE:=Bandwidth Manager - QoS, Quotas & Media Detection
|
|
||||||
DEPENDS:=+luci-base +rpcd +tc +kmod-sched-cake +kmod-sched-fq-codel
|
|
||||||
PKGARCH:=all
|
|
||||||
endef
|
|
||||||
|
|
||||||
define Package/luci-app-bandwidth-manager/description
|
# call BuildPackage - OpenWrt buildroot signature
|
||||||
Advanced bandwidth management for OpenWrt with:
|
|
||||||
- Per-client and per-group quotas (daily/monthly)
|
|
||||||
- Bandwidth throttling and shaping
|
|
||||||
- 8-level QoS priority classes
|
|
||||||
- Automatic media detection (VoIP, Gaming, Streaming)
|
|
||||||
- Time-based scheduling
|
|
||||||
- Real-time statistics and graphs
|
|
||||||
endef
|
|
||||||
|
|
||||||
define Build/Compile
|
|
||||||
endef
|
|
||||||
|
|
||||||
define Package/luci-app-bandwidth-manager/install
|
|
||||||
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
|
||||||
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.bandwidth-manager $(1)/usr/libexec/rpcd/
|
|
||||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
|
||||||
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/
|
|
||||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
|
||||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/
|
|
||||||
$(INSTALL_DIR) $(1)/etc/config
|
|
||||||
$(INSTALL_CONF) ./root/etc/config/bandwidth $(1)/etc/config/
|
|
||||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/bandwidth-manager
|
|
||||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/bandwidth-manager/*.js $(1)/www/luci-static/resources/view/bandwidth-manager/
|
|
||||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/bandwidth-manager
|
|
||||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/bandwidth-manager/*.js $(1)/www/luci-static/resources/bandwidth-manager/
|
|
||||||
endef
|
|
||||||
|
|
||||||
define Package/luci-app-bandwidth-manager/postinst
|
|
||||||
#!/bin/sh
|
|
||||||
[ -n "$${IPKG_INSTROOT}" ] || /etc/init.d/rpcd reload
|
|
||||||
endef
|
|
||||||
|
|
||||||
$(eval $(call BuildPackage,luci-app-bandwidth-manager))
|
|
||||||
|
|||||||
@ -1,50 +1,533 @@
|
|||||||
# Bandwidth Manager for OpenWrt
|
# Bandwidth Manager - QoS & Traffic Control
|
||||||
|
|
||||||
Advanced bandwidth management with QoS, quotas, and automatic media detection.
|
Advanced bandwidth management for OpenWrt with QoS rules, client quotas, and SQM/CAKE integration.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### 🎯 QoS Priority Classes
|
### QoS Traffic Shaping
|
||||||
- 8 configurable priority levels
|
- Rule-based traffic control by application, port, IP, or MAC
|
||||||
- Per-class rate guarantees and ceilings
|
- Per-rule download/upload limits
|
||||||
- DSCP marking support
|
- 8-level priority system (1=highest, 8=lowest)
|
||||||
|
- Time-based scheduling support
|
||||||
|
- Real-time rule enable/disable
|
||||||
|
|
||||||
### 📊 Bandwidth Quotas
|
### Client Quotas
|
||||||
- Daily and monthly limits
|
- Monthly data quotas per MAC address
|
||||||
- Per-client or per-group quotas
|
- Usage tracking with iptables counters
|
||||||
- Configurable actions (throttle/block)
|
- Configurable actions: throttle, block, or notify
|
||||||
|
- Automatic monthly reset (configurable day)
|
||||||
|
- Real-time quota usage monitoring
|
||||||
|
|
||||||
### 🎬 Media Detection
|
### SQM/CAKE Integration
|
||||||
- Automatic VoIP detection (SIP, RTP)
|
- Smart Queue Management with CAKE qdisc
|
||||||
- Gaming traffic prioritization
|
- Automatic bandwidth shaping
|
||||||
- Streaming service identification
|
- NAT-aware configuration
|
||||||
- Domain-based classification
|
- Link overhead compensation (Ethernet, PPPoE, VLAN)
|
||||||
|
- Alternative FQ_CoDel and HTB support
|
||||||
|
|
||||||
### ⏰ Time-Based Scheduling
|
### Real-time Monitoring
|
||||||
- Peak/off-peak configurations
|
- Live client bandwidth usage (auto-refresh every 5s)
|
||||||
- Day-of-week rules
|
- Per-client RX/TX statistics
|
||||||
- Automatic limit adjustments
|
- Quota progress visualization
|
||||||
|
- Historical usage tracking
|
||||||
### 👥 Client Management
|
|
||||||
- Per-device statistics
|
|
||||||
- MAC-based identification
|
|
||||||
- Real-time monitoring
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
opkg update
|
opkg update
|
||||||
opkg install luci-app-bandwidth-manager
|
opkg install luci-app-bandwidth-manager
|
||||||
|
/etc/init.d/rpcd restart
|
||||||
|
/etc/init.d/uhttpd restart
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **tc**: Traffic control utility
|
||||||
|
- **kmod-sched-core**: Kernel traffic scheduler
|
||||||
|
- **kmod-sched-cake**: CAKE qdisc module
|
||||||
|
- **kmod-ifb**: Intermediate Functional Block device
|
||||||
|
- **sqm-scripts**: SQM scripts
|
||||||
|
- **iptables**: For traffic tracking
|
||||||
|
- **iptables-mod-conntrack-extra**: Connection tracking extensions
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Edit `/etc/config/bandwidth` or use the LuCI interface.
|
### UCI Configuration
|
||||||
|
|
||||||
## Demo
|
Edit `/etc/config/bandwidth`:
|
||||||
|
|
||||||
Open `demo/index.html` in a browser to see a live preview.
|
```bash
|
||||||
|
config global 'global'
|
||||||
|
option enabled '1'
|
||||||
|
option interface 'br-lan'
|
||||||
|
option sqm_enabled '1'
|
||||||
|
|
||||||
|
config sqm 'sqm'
|
||||||
|
option download_speed '100000' # kbit/s
|
||||||
|
option upload_speed '50000' # kbit/s
|
||||||
|
option qdisc 'cake'
|
||||||
|
option nat '1'
|
||||||
|
option overhead '22' # PPPoE overhead
|
||||||
|
|
||||||
|
config rule 'rule_youtube'
|
||||||
|
option name 'Limit YouTube'
|
||||||
|
option type 'application'
|
||||||
|
option target 'youtube'
|
||||||
|
option limit_down '5000' # kbit/s
|
||||||
|
option limit_up '1000' # kbit/s
|
||||||
|
option priority '6'
|
||||||
|
option enabled '1'
|
||||||
|
|
||||||
|
config quota 'quota_phone'
|
||||||
|
option mac 'AA:BB:CC:DD:EE:FF'
|
||||||
|
option name 'iPhone Jean'
|
||||||
|
option limit_mb '10240' # 10 GB
|
||||||
|
option action 'throttle'
|
||||||
|
option reset_day '1'
|
||||||
|
option enabled '1'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
#### Global Section
|
||||||
|
- `enabled`: Enable/disable bandwidth manager
|
||||||
|
- `interface`: Network interface to manage (default: br-lan)
|
||||||
|
- `sqm_enabled`: Enable SQM/CAKE
|
||||||
|
|
||||||
|
#### SQM Section
|
||||||
|
- `download_speed`: Download speed in kbit/s
|
||||||
|
- `upload_speed`: Upload speed in kbit/s
|
||||||
|
- `qdisc`: Queue discipline (cake, fq_codel, htb)
|
||||||
|
- `nat`: NAT mode (1=enabled, 0=disabled)
|
||||||
|
- `overhead`: Link overhead in bytes (0, 18, 22, 40)
|
||||||
|
|
||||||
|
#### Rule Section
|
||||||
|
- `name`: Rule name
|
||||||
|
- `type`: Rule type (application, port, ip, mac)
|
||||||
|
- `target`: Target value (app name, port number, IP, or MAC)
|
||||||
|
- `limit_down`: Download limit in kbit/s (0=unlimited)
|
||||||
|
- `limit_up`: Upload limit in kbit/s (0=unlimited)
|
||||||
|
- `priority`: Priority level (1-8)
|
||||||
|
- `schedule`: Optional time schedule (e.g., "Mon-Fri 08:00-18:00")
|
||||||
|
- `enabled`: Enable/disable rule
|
||||||
|
|
||||||
|
#### Quota Section
|
||||||
|
- `mac`: Client MAC address
|
||||||
|
- `name`: Friendly name
|
||||||
|
- `limit_mb`: Monthly limit in MB
|
||||||
|
- `action`: Action when exceeded (throttle, block, notify)
|
||||||
|
- `reset_day`: Day of month to reset (1-28)
|
||||||
|
- `enabled`: Enable/disable quota
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Web Interface
|
||||||
|
|
||||||
|
Navigate to **Network → Bandwidth Manager** in LuCI.
|
||||||
|
|
||||||
|
#### Overview Tab
|
||||||
|
- System status (QoS active, interface, SQM)
|
||||||
|
- Traffic statistics (RX/TX bytes and packets)
|
||||||
|
- Active rules summary
|
||||||
|
- Client quotas with progress bars
|
||||||
|
|
||||||
|
#### QoS Rules Tab
|
||||||
|
- Create/edit/delete traffic shaping rules
|
||||||
|
- Configure type, target, limits, and priority
|
||||||
|
- Enable/disable rules individually
|
||||||
|
- Set time-based schedules
|
||||||
|
|
||||||
|
#### Client Quotas Tab
|
||||||
|
- Manage monthly data quotas per MAC
|
||||||
|
- Set limits and actions
|
||||||
|
- Reset quota counters
|
||||||
|
- View current usage
|
||||||
|
|
||||||
|
#### Real-time Usage Tab
|
||||||
|
- Live bandwidth usage per client
|
||||||
|
- Auto-refresh every 5 seconds
|
||||||
|
- Download/upload breakdown
|
||||||
|
- Quota progress for monitored clients
|
||||||
|
|
||||||
|
#### Settings Tab
|
||||||
|
- Global enable/disable
|
||||||
|
- Interface selection
|
||||||
|
- SQM/CAKE configuration
|
||||||
|
- Traffic tracking settings
|
||||||
|
- Alert configuration
|
||||||
|
|
||||||
|
### Command Line
|
||||||
|
|
||||||
|
#### Get Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ubus call luci.bandwidth-manager status
|
||||||
|
```
|
||||||
|
|
||||||
|
#### List QoS Rules
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ubus call luci.bandwidth-manager list_rules
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Add QoS Rule
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ubus call luci.bandwidth-manager add_rule '{
|
||||||
|
"name": "Limit Torrent",
|
||||||
|
"type": "port",
|
||||||
|
"target": "6881-6889",
|
||||||
|
"limit_down": 3000,
|
||||||
|
"limit_up": 500,
|
||||||
|
"priority": 7
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Delete Rule
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ubus call luci.bandwidth-manager delete_rule '{
|
||||||
|
"rule_id": "rule_1234567890"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### List Client Quotas
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ubus call luci.bandwidth-manager list_quotas
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Set Quota
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ubus call luci.bandwidth-manager set_quota '{
|
||||||
|
"mac": "AA:BB:CC:DD:EE:FF",
|
||||||
|
"name": "iPhone John",
|
||||||
|
"limit_mb": 10240,
|
||||||
|
"action": "throttle",
|
||||||
|
"reset_day": 1
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Quota Details
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ubus call luci.bandwidth-manager get_quota '{
|
||||||
|
"mac": "AA:BB:CC:DD:EE:FF"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Reset Quota Counter
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ubus call luci.bandwidth-manager reset_quota '{
|
||||||
|
"mac": "AA:BB:CC:DD:EE:FF"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Real-time Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ubus call luci.bandwidth-manager get_usage_realtime
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Usage History
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ubus call luci.bandwidth-manager get_usage_history '{
|
||||||
|
"timeframe": "24h",
|
||||||
|
"mac": "AA:BB:CC:DD:EE:FF"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Timeframe options: `1h`, `6h`, `24h`, `7d`, `30d`
|
||||||
|
|
||||||
|
## ubus API Reference
|
||||||
|
|
||||||
|
### status()
|
||||||
|
|
||||||
|
Get system status and global statistics.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"interface": "br-lan",
|
||||||
|
"sqm_enabled": true,
|
||||||
|
"qos_active": true,
|
||||||
|
"stats": {
|
||||||
|
"rx_bytes": 1234567890,
|
||||||
|
"tx_bytes": 987654321,
|
||||||
|
"rx_packets": 1234567,
|
||||||
|
"tx_packets": 987654
|
||||||
|
},
|
||||||
|
"rule_count": 5,
|
||||||
|
"quota_count": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### list_rules()
|
||||||
|
|
||||||
|
List all QoS rules.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "rule_youtube",
|
||||||
|
"name": "Limit YouTube",
|
||||||
|
"type": "application",
|
||||||
|
"target": "youtube",
|
||||||
|
"limit_down": 5000,
|
||||||
|
"limit_up": 1000,
|
||||||
|
"priority": 6,
|
||||||
|
"enabled": true,
|
||||||
|
"schedule": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### add_rule(name, type, target, limit_down, limit_up, priority)
|
||||||
|
|
||||||
|
Add a new QoS rule.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"rule_id": "rule_1234567890",
|
||||||
|
"message": "Rule created successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### delete_rule(rule_id)
|
||||||
|
|
||||||
|
Delete a QoS rule.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Rule deleted successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### list_quotas()
|
||||||
|
|
||||||
|
List all client quotas with current usage.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"quotas": [
|
||||||
|
{
|
||||||
|
"id": "quota_phone",
|
||||||
|
"mac": "AA:BB:CC:DD:EE:FF",
|
||||||
|
"name": "iPhone Jean",
|
||||||
|
"limit_mb": 10240,
|
||||||
|
"used_mb": 7850,
|
||||||
|
"percent": 76,
|
||||||
|
"action": "throttle",
|
||||||
|
"reset_day": 1,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### get_quota(mac)
|
||||||
|
|
||||||
|
Get detailed quota information for a specific MAC.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"quota_id": "quota_phone",
|
||||||
|
"mac": "AA:BB:CC:DD:EE:FF",
|
||||||
|
"name": "iPhone Jean",
|
||||||
|
"limit_mb": 10240,
|
||||||
|
"used_mb": 7850,
|
||||||
|
"remaining_mb": 2390,
|
||||||
|
"percent": 76,
|
||||||
|
"action": "throttle",
|
||||||
|
"reset_day": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### set_quota(mac, name, limit_mb, action, reset_day)
|
||||||
|
|
||||||
|
Create or update a client quota.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"quota_id": "quota_1234567890",
|
||||||
|
"message": "Quota created successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### reset_quota(mac)
|
||||||
|
|
||||||
|
Reset quota counter for a client.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Quota counter reset for AA:BB:CC:DD:EE:FF"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### get_usage_realtime()
|
||||||
|
|
||||||
|
Get real-time bandwidth usage for all active clients.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"clients": [
|
||||||
|
{
|
||||||
|
"mac": "AA:BB:CC:DD:EE:FF",
|
||||||
|
"ip": "192.168.1.100",
|
||||||
|
"hostname": "iPhone",
|
||||||
|
"rx_bytes": 1234567,
|
||||||
|
"tx_bytes": 987654,
|
||||||
|
"has_quota": true,
|
||||||
|
"limit_mb": 10240,
|
||||||
|
"used_mb": 7850
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### get_usage_history(timeframe, mac)
|
||||||
|
|
||||||
|
Get historical usage data.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `timeframe`: "1h", "6h", "24h", "7d", "30d"
|
||||||
|
- `mac`: MAC address (optional, empty for all clients)
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"history": [
|
||||||
|
{
|
||||||
|
"mac": "AA:BB:CC:DD:EE:FF",
|
||||||
|
"timestamp": 1640000000,
|
||||||
|
"rx_bytes": 1234567,
|
||||||
|
"tx_bytes": 987654
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Traffic Tracking
|
||||||
|
|
||||||
|
Bandwidth Manager uses iptables for per-client traffic accounting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create tracking chain
|
||||||
|
iptables -N BW_TRACKING
|
||||||
|
|
||||||
|
# Add rules for each MAC
|
||||||
|
iptables -A BW_TRACKING -m mac --mac-source AA:BB:CC:DD:EE:FF
|
||||||
|
iptables -A BW_TRACKING -m mac --mac-source BB:CC:DD:EE:FF:00
|
||||||
|
|
||||||
|
# Insert into FORWARD chain
|
||||||
|
iptables -I FORWARD -j BW_TRACKING
|
||||||
|
|
||||||
|
# View counters
|
||||||
|
iptables -L BW_TRACKING -n -v -x
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage data is stored in `/tmp/bandwidth_usage.db` in pipe-delimited format:
|
||||||
|
```
|
||||||
|
MAC|Timestamp|RX_Bytes|TX_Bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
## QoS Implementation
|
||||||
|
|
||||||
|
### CAKE (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tc qdisc add dev br-lan root cake bandwidth 100000kbit
|
||||||
|
```
|
||||||
|
|
||||||
|
Benefits:
|
||||||
|
- Active Queue Management (AQM)
|
||||||
|
- Flow-based fair queuing
|
||||||
|
- NAT-aware
|
||||||
|
- Low latency
|
||||||
|
|
||||||
|
### HTB (Manual Control)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tc qdisc add dev br-lan root handle 1: htb default 10
|
||||||
|
tc class add dev br-lan parent 1: classid 1:1 htb rate 100mbit
|
||||||
|
tc class add dev br-lan parent 1:1 classid 1:10 htb rate 50mbit ceil 100mbit prio 5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### QoS Not Working
|
||||||
|
|
||||||
|
Check if QoS is active:
|
||||||
|
```bash
|
||||||
|
tc qdisc show dev br-lan
|
||||||
|
```
|
||||||
|
|
||||||
|
Check iptables rules:
|
||||||
|
```bash
|
||||||
|
iptables -L BW_TRACKING -n -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quota Tracking Not Accurate
|
||||||
|
|
||||||
|
Reset iptables counters:
|
||||||
|
```bash
|
||||||
|
iptables -Z BW_TRACKING
|
||||||
|
```
|
||||||
|
|
||||||
|
Check usage database:
|
||||||
|
```bash
|
||||||
|
cat /tmp/bandwidth_usage.db
|
||||||
|
```
|
||||||
|
|
||||||
|
### High CPU Usage
|
||||||
|
|
||||||
|
Reduce tracking frequency or use hardware flow offloading if available:
|
||||||
|
```bash
|
||||||
|
echo 1 > /sys/class/net/br-lan/offload/tx_offload
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Set Realistic Limits**: Configure download/upload speeds to 85-95% of your actual connection speed
|
||||||
|
2. **Use CAKE**: Prefer CAKE qdisc for best performance and lowest latency
|
||||||
|
3. **Monitor First**: Use real-time usage view to understand traffic patterns before setting quotas
|
||||||
|
4. **Regular Resets**: Configure monthly resets on quota day 1 to align with ISP billing
|
||||||
|
5. **Priority Wisely**: Reserve priority 1-2 for VoIP/gaming, use 5 (normal) for most traffic
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- MAC addresses can be spoofed - use in conjunction with other security measures
|
||||||
|
- Quota tracking requires iptables access - secure your router
|
||||||
|
- Alert emails may contain sensitive information - use encrypted connections
|
||||||
|
- Traffic shaping rules are visible to network administrator only
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License - CyberMind Security
|
Apache-2.0
|
||||||
|
|
||||||
|
## Maintainer
|
||||||
|
|
||||||
|
SecuBox Project <support@secubox.com>
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
1.0.0
|
||||||
|
|||||||
@ -1,35 +1,81 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
'require baseclass';
|
|
||||||
'require rpc';
|
'require rpc';
|
||||||
|
|
||||||
var callStatus = rpc.declare({object:'luci.bandwidth-manager',method:'status',expect:{}});
|
var callStatus = rpc.declare({
|
||||||
var callClasses = rpc.declare({object:'luci.bandwidth-manager',method:'classes',expect:{classes:[]}});
|
object: 'luci.bandwidth-manager',
|
||||||
var callQuotas = rpc.declare({object:'luci.bandwidth-manager',method:'quotas',expect:{quotas:[]}});
|
method: 'status',
|
||||||
var callMedia = rpc.declare({object:'luci.bandwidth-manager',method:'media',expect:{media:[]}});
|
expect: {}
|
||||||
var callClients = rpc.declare({object:'luci.bandwidth-manager',method:'clients',expect:{clients:[]}});
|
|
||||||
var callStats = rpc.declare({object:'luci.bandwidth-manager',method:'stats',expect:{}});
|
|
||||||
var callApplyQos = rpc.declare({object:'luci.bandwidth-manager',method:'apply_qos'});
|
|
||||||
|
|
||||||
function formatBytes(bytes) {
|
|
||||||
if (bytes === 0) return '0 B';
|
|
||||||
var k = 1024, sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
||||||
var i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatSpeed(kbps) {
|
|
||||||
if (kbps >= 1000) return (kbps / 1000).toFixed(1) + ' Mbps';
|
|
||||||
return kbps + ' Kbps';
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseclass.extend({
|
|
||||||
getStatus: callStatus,
|
|
||||||
getClasses: callClasses,
|
|
||||||
getQuotas: callQuotas,
|
|
||||||
getMedia: callMedia,
|
|
||||||
getClients: callClients,
|
|
||||||
getStats: callStats,
|
|
||||||
applyQos: callApplyQos,
|
|
||||||
formatBytes: formatBytes,
|
|
||||||
formatSpeed: formatSpeed
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var callListRules = rpc.declare({
|
||||||
|
object: 'luci.bandwidth-manager',
|
||||||
|
method: 'list_rules',
|
||||||
|
expect: { rules: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callAddRule = rpc.declare({
|
||||||
|
object: 'luci.bandwidth-manager',
|
||||||
|
method: 'add_rule',
|
||||||
|
params: ['name', 'type', 'target', 'limit_down', 'limit_up', 'priority'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callDeleteRule = rpc.declare({
|
||||||
|
object: 'luci.bandwidth-manager',
|
||||||
|
method: 'delete_rule',
|
||||||
|
params: ['rule_id'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callListQuotas = rpc.declare({
|
||||||
|
object: 'luci.bandwidth-manager',
|
||||||
|
method: 'list_quotas',
|
||||||
|
expect: { quotas: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGetQuota = rpc.declare({
|
||||||
|
object: 'luci.bandwidth-manager',
|
||||||
|
method: 'get_quota',
|
||||||
|
params: ['mac'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callSetQuota = rpc.declare({
|
||||||
|
object: 'luci.bandwidth-manager',
|
||||||
|
method: 'set_quota',
|
||||||
|
params: ['mac', 'name', 'limit_mb', 'action', 'reset_day'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callResetQuota = rpc.declare({
|
||||||
|
object: 'luci.bandwidth-manager',
|
||||||
|
method: 'reset_quota',
|
||||||
|
params: ['mac'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGetUsageRealtime = rpc.declare({
|
||||||
|
object: 'luci.bandwidth-manager',
|
||||||
|
method: 'get_usage_realtime',
|
||||||
|
expect: { clients: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGetUsageHistory = rpc.declare({
|
||||||
|
object: 'luci.bandwidth-manager',
|
||||||
|
method: 'get_usage_history',
|
||||||
|
params: ['timeframe', 'mac'],
|
||||||
|
expect: { history: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getStatus: callStatus,
|
||||||
|
listRules: callListRules,
|
||||||
|
addRule: callAddRule,
|
||||||
|
deleteRule: callDeleteRule,
|
||||||
|
listQuotas: callListQuotas,
|
||||||
|
getQuota: callGetQuota,
|
||||||
|
setQuota: callSetQuota,
|
||||||
|
resetQuota: callResetQuota,
|
||||||
|
getUsageRealtime: callGetUsageRealtime,
|
||||||
|
getUsageHistory: callGetUsageHistory
|
||||||
|
};
|
||||||
|
|||||||
@ -1,69 +1,170 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
'require view';
|
'require view';
|
||||||
'require bandwidth-manager.api as api';
|
'require bandwidth-manager/api as API';
|
||||||
|
|
||||||
return view.extend({
|
return L.view.extend({
|
||||||
load: function() {
|
load: function() {
|
||||||
return Promise.all([api.getStatus(), api.getClasses(), api.getClients()]);
|
return Promise.all([
|
||||||
},
|
API.getStatus(),
|
||||||
render: function(data) {
|
API.listRules(),
|
||||||
var status = data[0] || {};
|
API.listQuotas()
|
||||||
var classes = data[1].classes || [];
|
]);
|
||||||
var clients = data[2].clients || [];
|
},
|
||||||
|
|
||||||
return E('div', {class:'cbi-map'}, [
|
render: function(data) {
|
||||||
E('style', {}, [
|
var status = data[0] || {};
|
||||||
'.bw{font-family:system-ui,sans-serif}',
|
var rules = data[1] || [];
|
||||||
'.bw-hdr{background:linear-gradient(135deg,#7c3aed,#a855f7);color:#fff;padding:24px;border-radius:12px;margin-bottom:20px}',
|
var quotas = data[2] || [];
|
||||||
'.bw-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:20px}',
|
|
||||||
'.bw-stat{background:#1e293b;padding:20px;border-radius:10px;text-align:center}',
|
var v = E('div', { 'class': 'cbi-map' }, [
|
||||||
'.bw-stat-val{font-size:28px;font-weight:700;color:#a855f7}',
|
E('h2', {}, _('Bandwidth Manager - Overview')),
|
||||||
'.bw-stat-lbl{font-size:12px;color:#94a3b8;margin-top:4px}',
|
E('div', { 'class': 'cbi-map-descr' }, _('QoS rules, client quotas, and traffic control'))
|
||||||
'.bw-section{background:#1e293b;padding:20px;border-radius:10px;margin-bottom:16px}',
|
]);
|
||||||
'.bw-section-title{font-size:16px;font-weight:600;color:#f1f5f9;margin-bottom:16px}',
|
|
||||||
'.bw-class{display:flex;align-items:center;gap:12px;padding:12px;background:#0f172a;border-radius:8px;margin-bottom:8px}',
|
// System status
|
||||||
'.bw-class-bar{height:8px;border-radius:4px;background:#334155;flex:1}',
|
var statusSection = E('div', { 'class': 'cbi-section' }, [
|
||||||
'.bw-class-fill{height:100%;border-radius:4px;background:linear-gradient(90deg,#7c3aed,#a855f7)}',
|
E('h3', {}, _('System Status')),
|
||||||
'.bw-badge{padding:4px 8px;border-radius:4px;font-size:11px;font-weight:600}'
|
E('div', { 'class': 'table' }, [
|
||||||
].join('')),
|
E('div', { 'class': 'tr' }, [
|
||||||
E('div', {class:'bw'}, [
|
E('div', { 'class': 'td left', 'width': '25%' }, [
|
||||||
E('div', {class:'bw-hdr'}, [
|
E('strong', {}, _('QoS Engine: ')),
|
||||||
E('h1', {style:'margin:0 0 8px;font-size:24px'}, '⚡ Bandwidth Manager'),
|
status.qos_active ?
|
||||||
E('p', {style:'margin:0;opacity:.9'}, 'QoS, Quotas & Media Detection')
|
E('span', { 'style': 'color: green' }, '● ' + _('Active')) :
|
||||||
]),
|
E('span', { 'style': 'color: red' }, '● ' + _('Inactive'))
|
||||||
E('div', {class:'bw-stats'}, [
|
]),
|
||||||
E('div', {class:'bw-stat'}, [
|
E('div', { 'class': 'td left', 'width': '25%' }, [
|
||||||
E('div', {class:'bw-stat-val'}, status.qos_active ? '✓' : '✗'),
|
E('strong', {}, _('Interface: ')),
|
||||||
E('div', {class:'bw-stat-lbl'}, 'QoS Status')
|
E('span', {}, status.interface || 'br-lan')
|
||||||
]),
|
]),
|
||||||
E('div', {class:'bw-stat'}, [
|
E('div', { 'class': 'td left', 'width': '25%' }, [
|
||||||
E('div', {class:'bw-stat-val'}, clients.length),
|
E('strong', {}, _('SQM: ')),
|
||||||
E('div', {class:'bw-stat-lbl'}, 'Active Clients')
|
status.sqm_enabled ?
|
||||||
]),
|
E('span', { 'style': 'color: green' }, '✓ ' + _('Enabled')) :
|
||||||
E('div', {class:'bw-stat'}, [
|
E('span', {}, '✗ ' + _('Disabled'))
|
||||||
E('div', {class:'bw-stat-val'}, api.formatBytes(status.rx_bytes || 0)),
|
]),
|
||||||
E('div', {class:'bw-stat-lbl'}, 'Downloaded')
|
E('div', { 'class': 'td left', 'width': '25%' }, [
|
||||||
]),
|
E('strong', {}, _('Rules: ')),
|
||||||
E('div', {class:'bw-stat'}, [
|
E('span', { 'style': 'font-size: 1.3em; color: #0088cc' }, String(status.rule_count || 0))
|
||||||
E('div', {class:'bw-stat-val'}, api.formatBytes(status.tx_bytes || 0)),
|
])
|
||||||
E('div', {class:'bw-stat-lbl'}, 'Uploaded')
|
])
|
||||||
])
|
])
|
||||||
]),
|
]);
|
||||||
E('div', {class:'bw-section'}, [
|
v.appendChild(statusSection);
|
||||||
E('div', {class:'bw-section-title'}, '📊 QoS Classes'),
|
|
||||||
E('div', {}, classes.map(function(c) {
|
// Traffic statistics
|
||||||
return E('div', {class:'bw-class'}, [
|
if (status.stats) {
|
||||||
E('span', {style:'width:100px;font-weight:600;color:#f1f5f9'}, c.name),
|
var statsSection = E('div', { 'class': 'cbi-section' }, [
|
||||||
E('span', {class:'bw-badge',style:'background:#7c3aed20;color:#a855f7'}, 'P'+c.priority),
|
E('h3', {}, _('Traffic Statistics')),
|
||||||
E('div', {class:'bw-class-bar'}, [
|
E('div', { 'class': 'table' }, [
|
||||||
E('div', {class:'bw-class-fill',style:'width:'+c.rate+'%'})
|
E('div', { 'class': 'tr' }, [
|
||||||
]),
|
E('div', { 'class': 'td left', 'width': '50%' }, [
|
||||||
E('span', {style:'color:#94a3b8;font-size:12px'}, c.rate+'% / '+c.ceil+'%')
|
E('strong', {}, '⬇ ' + _('Download: ')),
|
||||||
]);
|
E('span', {}, this.formatBytes(status.stats.rx_bytes || 0))
|
||||||
}))
|
]),
|
||||||
])
|
E('div', { 'class': 'td left', 'width': '50%' }, [
|
||||||
])
|
E('strong', {}, '⬆ ' + _('Upload: ')),
|
||||||
]);
|
E('span', {}, this.formatBytes(status.stats.tx_bytes || 0))
|
||||||
},
|
])
|
||||||
handleSaveApply:null,handleSave:null,handleReset:null
|
]),
|
||||||
|
E('div', { 'class': 'tr' }, [
|
||||||
|
E('div', { 'class': 'td left', 'width': '50%' }, [
|
||||||
|
E('strong', {}, _('RX Packets: ')),
|
||||||
|
E('span', {}, String(status.stats.rx_packets || 0))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'td left', 'width': '50%' }, [
|
||||||
|
E('strong', {}, _('TX Packets: ')),
|
||||||
|
E('span', {}, String(status.stats.tx_packets || 0))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
v.appendChild(statsSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active rules summary
|
||||||
|
if (rules.length > 0) {
|
||||||
|
var rulesSection = E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, _('Active QoS Rules'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
var rulesTable = E('table', { 'class': 'table' }, [
|
||||||
|
E('tr', { 'class': 'tr table-titles' }, [
|
||||||
|
E('th', { 'class': 'th' }, _('Name')),
|
||||||
|
E('th', { 'class': 'th' }, _('Type')),
|
||||||
|
E('th', { 'class': 'th' }, _('Target')),
|
||||||
|
E('th', { 'class': 'th' }, _('Download Limit')),
|
||||||
|
E('th', { 'class': 'th' }, _('Priority'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
rules.slice(0, 5).forEach(function(rule) {
|
||||||
|
if (!rule.enabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
rulesTable.appendChild(E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, rule.name),
|
||||||
|
E('td', { 'class': 'td' }, rule.type),
|
||||||
|
E('td', { 'class': 'td' }, rule.target),
|
||||||
|
E('td', { 'class': 'td' }, rule.limit_down > 0 ? rule.limit_down + ' kbit/s' : _('Unlimited')),
|
||||||
|
E('td', { 'class': 'td' }, String(rule.priority))
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
rulesSection.appendChild(rulesTable);
|
||||||
|
v.appendChild(rulesSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client quotas summary
|
||||||
|
if (quotas.length > 0) {
|
||||||
|
var quotasSection = E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, _('Client Quotas'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
var quotasTable = E('table', { 'class': 'table' }, [
|
||||||
|
E('tr', { 'class': 'tr table-titles' }, [
|
||||||
|
E('th', { 'class': 'th' }, _('Client')),
|
||||||
|
E('th', { 'class': 'th' }, _('MAC')),
|
||||||
|
E('th', { 'class': 'th' }, _('Usage')),
|
||||||
|
E('th', { 'class': 'th' }, _('Limit')),
|
||||||
|
E('th', { 'class': 'th' }, _('Progress'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
quotas.slice(0, 5).forEach(function(quota) {
|
||||||
|
var progressColor = quota.percent > 90 ? 'red' : (quota.percent > 75 ? 'orange' : 'green');
|
||||||
|
|
||||||
|
quotasTable.appendChild(E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, quota.name || quota.mac),
|
||||||
|
E('td', { 'class': 'td' }, E('code', {}, quota.mac)),
|
||||||
|
E('td', { 'class': 'td' }, quota.used_mb + ' MB'),
|
||||||
|
E('td', { 'class': 'td' }, quota.limit_mb + ' MB'),
|
||||||
|
E('td', { 'class': 'td' }, [
|
||||||
|
E('div', { 'style': 'background: #eee; width: 100px; height: 10px; border-radius: 5px;' }, [
|
||||||
|
E('div', {
|
||||||
|
'style': 'background: ' + progressColor + '; width: ' + Math.min(quota.percent, 100) + '%; height: 100%; border-radius: 5px;'
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
E('small', {}, quota.percent + '%')
|
||||||
|
])
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
quotasSection.appendChild(quotasTable);
|
||||||
|
v.appendChild(quotasSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
return v;
|
||||||
|
},
|
||||||
|
|
||||||
|
formatBytes: function(bytes) {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
var k = 1024;
|
||||||
|
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
var i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,34 +1,114 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
'require view';
|
'require view';
|
||||||
'require bandwidth-manager.api as api';
|
'require ui';
|
||||||
|
'require form';
|
||||||
|
'require bandwidth-manager/api as API';
|
||||||
|
|
||||||
return view.extend({
|
return L.view.extend({
|
||||||
load: function() { return api.getQuotas(); },
|
load: function() {
|
||||||
render: function(data) {
|
return API.listQuotas();
|
||||||
var quotas = data.quotas || [];
|
},
|
||||||
return E('div', {class:'cbi-map'}, [
|
|
||||||
E('h2', {}, '📉 Bandwidth Quotas'),
|
render: function(quotas) {
|
||||||
E('p', {style:'color:#94a3b8'}, 'Set daily/monthly limits and throttle actions.'),
|
var m = new form.Map('bandwidth', _('Client Quotas'),
|
||||||
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;margin-top:20px'}, [
|
_('Set monthly data quotas for individual clients by MAC address'));
|
||||||
E('table', {style:'width:100%;color:#f1f5f9'}, [
|
|
||||||
E('tr', {style:'border-bottom:1px solid #334155'}, [
|
var s = m.section(form.GridSection, 'quota', _('Quotas'));
|
||||||
E('th', {style:'padding:12px;text-align:left'}, 'Profile'),
|
s.anonymous = false;
|
||||||
E('th', {style:'padding:12px'}, 'Daily Limit'),
|
s.addremove = true;
|
||||||
E('th', {style:'padding:12px'}, 'Monthly Limit'),
|
s.sortable = true;
|
||||||
E('th', {style:'padding:12px'}, 'Throttle Speed'),
|
|
||||||
E('th', {style:'padding:12px'}, 'Action')
|
s.modaltitle = function(section_id) {
|
||||||
])
|
return _('Edit Quota: ') + section_id;
|
||||||
].concat(quotas.map(function(q) {
|
};
|
||||||
return E('tr', {}, [
|
|
||||||
E('td', {style:'padding:12px;font-weight:600'}, q.id),
|
// Custom render to show usage progress bars
|
||||||
E('td', {style:'padding:12px;text-align:center'}, q.daily_limit ? api.formatBytes(q.daily_limit * 1024 * 1024) : '∞'),
|
s.addModalOptions = function(s, section_id, ev) {
|
||||||
E('td', {style:'padding:12px;text-align:center'}, q.monthly_limit ? api.formatBytes(q.monthly_limit * 1024 * 1024) : '∞'),
|
var mac = this.section.formvalue(section_id, 'mac');
|
||||||
E('td', {style:'padding:12px;text-align:center'}, api.formatSpeed(q.throttle_speed)),
|
|
||||||
E('td', {style:'padding:12px;text-align:center'}, E('span', {style:'padding:4px 8px;border-radius:4px;background:'+(q.action==='block'?'#ef444420;color:#ef4444':'#f59e0b20;color:#f59e0b')}, q.action))
|
if (!mac) {
|
||||||
]);
|
ui.addNotification(null, E('p', _('MAC address is required')), 'error');
|
||||||
})))
|
return;
|
||||||
])
|
}
|
||||||
]);
|
|
||||||
},
|
// Save quota via API
|
||||||
handleSaveApply:null,handleSave:null,handleReset:null
|
var name = this.section.formvalue(section_id, 'name') || '';
|
||||||
|
var limit_mb = parseInt(this.section.formvalue(section_id, 'limit_mb')) || 0;
|
||||||
|
var action = this.section.formvalue(section_id, 'action') || 'throttle';
|
||||||
|
var reset_day = parseInt(this.section.formvalue(section_id, 'reset_day')) || 1;
|
||||||
|
|
||||||
|
API.setQuota(mac, name, limit_mb, action, reset_day).then(function(result) {
|
||||||
|
if (result.success) {
|
||||||
|
ui.addNotification(null, E('p', '✓ ' + result.message), 'info');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', '✗ ' + result.message), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var o;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'mac', _('MAC Address'));
|
||||||
|
o.rmempty = false;
|
||||||
|
o.datatype = 'macaddr';
|
||||||
|
o.placeholder = 'AA:BB:CC:DD:EE:FF';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'name', _('Client Name'));
|
||||||
|
o.placeholder = 'iPhone John';
|
||||||
|
o.description = _('Friendly name for this client');
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'limit_mb', _('Monthly Limit (MB)'));
|
||||||
|
o.rmempty = false;
|
||||||
|
o.datatype = 'uinteger';
|
||||||
|
o.placeholder = '10240';
|
||||||
|
o.description = _('Monthly data limit in megabytes (e.g., 10240 = 10GB)');
|
||||||
|
|
||||||
|
o = s.option(form.ListValue, 'action', _('Action When Exceeded'));
|
||||||
|
o.value('throttle', _('Throttle bandwidth'));
|
||||||
|
o.value('block', _('Block all traffic'));
|
||||||
|
o.value('notify', _('Notify only'));
|
||||||
|
o.default = 'throttle';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'reset_day', _('Reset Day'));
|
||||||
|
o.datatype = 'range(1,28)';
|
||||||
|
o.default = '1';
|
||||||
|
o.description = _('Day of month to reset quota (1-28)');
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'enabled', _('Enabled'));
|
||||||
|
o.default = o.enabled;
|
||||||
|
|
||||||
|
// Show current usage
|
||||||
|
s.renderRowActions = function(section_id) {
|
||||||
|
var config_name = this.uciconfig || this.map.config;
|
||||||
|
var mac = this.cfgvalue(section_id, 'mac');
|
||||||
|
|
||||||
|
var resetBtn = E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-action',
|
||||||
|
'click': function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
if (confirm(_('Reset quota counter for this client?'))) {
|
||||||
|
API.resetQuota(mac).then(function(result) {
|
||||||
|
if (result.success) {
|
||||||
|
ui.addNotification(null, E('p', '✓ ' + result.message), 'info');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', '✗ ' + result.message), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, _('Reset Counter'));
|
||||||
|
|
||||||
|
var actions = form.GridSection.prototype.renderRowActions.call(this, section_id);
|
||||||
|
actions.appendChild(resetBtn);
|
||||||
|
return actions;
|
||||||
|
};
|
||||||
|
|
||||||
|
return m.render();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,79 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require ui';
|
||||||
|
'require form';
|
||||||
|
'require bandwidth-manager/api as API';
|
||||||
|
|
||||||
|
return L.view.extend({
|
||||||
|
load: function() {
|
||||||
|
return API.listRules();
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(rules) {
|
||||||
|
var m = new form.Map('bandwidth', _('QoS Rules'),
|
||||||
|
_('Define traffic shaping rules based on applications, ports, or IP addresses'));
|
||||||
|
|
||||||
|
var s = m.section(form.GridSection, 'rule', _('Rules'));
|
||||||
|
s.anonymous = false;
|
||||||
|
s.addremove = true;
|
||||||
|
s.sortable = true;
|
||||||
|
|
||||||
|
s.modaltitle = function(section_id) {
|
||||||
|
return _('Edit Rule: ') + section_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
var o;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'name', _('Rule Name'));
|
||||||
|
o.rmempty = false;
|
||||||
|
o.placeholder = 'Limit YouTube';
|
||||||
|
|
||||||
|
o = s.option(form.ListValue, 'type', _('Type'));
|
||||||
|
o.value('application', _('Application'));
|
||||||
|
o.value('port', _('Port'));
|
||||||
|
o.value('ip', _('IP Address'));
|
||||||
|
o.value('mac', _('MAC Address'));
|
||||||
|
o.default = 'application';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'target', _('Target'));
|
||||||
|
o.rmempty = false;
|
||||||
|
o.placeholder = 'youtube / 80,443 / 192.168.1.100 / AA:BB:CC:DD:EE:FF';
|
||||||
|
o.description = _('Application name, port(s), IP address, or MAC address');
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'limit_down', _('Download Limit (kbit/s)'));
|
||||||
|
o.datatype = 'uinteger';
|
||||||
|
o.placeholder = '5000';
|
||||||
|
o.description = _('Maximum download speed in kbit/s (0 = unlimited)');
|
||||||
|
o.default = '0';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'limit_up', _('Upload Limit (kbit/s)'));
|
||||||
|
o.datatype = 'uinteger';
|
||||||
|
o.placeholder = '1000';
|
||||||
|
o.description = _('Maximum upload speed in kbit/s (0 = unlimited)');
|
||||||
|
o.default = '0';
|
||||||
|
|
||||||
|
o = s.option(form.ListValue, 'priority', _('Priority'));
|
||||||
|
o.value('1', '1 (Highest)');
|
||||||
|
o.value('2', '2 (High)');
|
||||||
|
o.value('3', '3');
|
||||||
|
o.value('4', '4');
|
||||||
|
o.value('5', '5 (Normal)');
|
||||||
|
o.value('6', '6');
|
||||||
|
o.value('7', '7 (Low)');
|
||||||
|
o.value('8', '8 (Lowest)');
|
||||||
|
o.default = '5';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'schedule', _('Schedule (Optional)'));
|
||||||
|
o.placeholder = 'Mon-Fri 08:00-18:00';
|
||||||
|
o.description = _('Apply rule only during specific times');
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'enabled', _('Enabled'));
|
||||||
|
o.default = o.enabled;
|
||||||
|
|
||||||
|
return m.render();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require form';
|
||||||
|
'require network';
|
||||||
|
|
||||||
|
return L.view.extend({
|
||||||
|
load: function() {
|
||||||
|
return network.getDevices();
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(devices) {
|
||||||
|
var m = new form.Map('bandwidth', _('Bandwidth Manager Settings'),
|
||||||
|
_('Global settings and SQM/CAKE configuration'));
|
||||||
|
|
||||||
|
var s = m.section(form.NamedSection, 'global', 'global', _('Global Settings'));
|
||||||
|
s.anonymous = false;
|
||||||
|
s.addremove = false;
|
||||||
|
|
||||||
|
var o;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'enabled', _('Enable Bandwidth Manager'));
|
||||||
|
o.default = o.disabled;
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.ListValue, 'interface', _('Interface'));
|
||||||
|
devices.forEach(function(dev) {
|
||||||
|
var name = dev.getName();
|
||||||
|
o.value(name, name);
|
||||||
|
});
|
||||||
|
o.default = 'br-lan';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'sqm_enabled', _('Enable SQM'));
|
||||||
|
o.description = _('Smart Queue Management with CAKE qdisc');
|
||||||
|
o.default = o.disabled;
|
||||||
|
|
||||||
|
// SQM Configuration
|
||||||
|
var sqm = m.section(form.NamedSection, 'sqm', 'sqm', _('SQM/CAKE Configuration'));
|
||||||
|
sqm.anonymous = false;
|
||||||
|
sqm.addremove = false;
|
||||||
|
|
||||||
|
o = sqm.option(form.Value, 'download_speed', _('Download Speed (kbit/s)'));
|
||||||
|
o.datatype = 'uinteger';
|
||||||
|
o.placeholder = '100000';
|
||||||
|
o.description = _('Your internet download speed in kbit/s');
|
||||||
|
o.depends('global.sqm_enabled', '1');
|
||||||
|
|
||||||
|
o = sqm.option(form.Value, 'upload_speed', _('Upload Speed (kbit/s)'));
|
||||||
|
o.datatype = 'uinteger';
|
||||||
|
o.placeholder = '50000';
|
||||||
|
o.description = _('Your internet upload speed in kbit/s');
|
||||||
|
o.depends('global.sqm_enabled', '1');
|
||||||
|
|
||||||
|
o = sqm.option(form.ListValue, 'qdisc', _('Queue Discipline'));
|
||||||
|
o.value('cake', 'CAKE (Recommended)');
|
||||||
|
o.value('fq_codel', 'FQ_CoDel');
|
||||||
|
o.value('htb', 'HTB');
|
||||||
|
o.default = 'cake';
|
||||||
|
o.depends('global.sqm_enabled', '1');
|
||||||
|
|
||||||
|
o = sqm.option(form.Flag, 'nat', _('NAT Mode'));
|
||||||
|
o.description = _('Enable if router performs NAT');
|
||||||
|
o.default = o.enabled;
|
||||||
|
o.depends('global.sqm_enabled', '1');
|
||||||
|
|
||||||
|
o = sqm.option(form.ListValue, 'overhead', _('Link Overhead'));
|
||||||
|
o.value('0', _('None'));
|
||||||
|
o.value('18', 'Ethernet (18 bytes)');
|
||||||
|
o.value('22', 'PPPoE (22 bytes)');
|
||||||
|
o.value('40', 'VLAN + PPPoE (40 bytes)');
|
||||||
|
o.default = '0';
|
||||||
|
o.depends('global.sqm_enabled', '1');
|
||||||
|
|
||||||
|
// Traffic Tracking
|
||||||
|
var tracking = m.section(form.NamedSection, 'tracking', 'tracking', _('Traffic Tracking'));
|
||||||
|
tracking.anonymous = false;
|
||||||
|
tracking.addremove = false;
|
||||||
|
|
||||||
|
o = tracking.option(form.Flag, 'iptables_tracking', _('Enable iptables Tracking'));
|
||||||
|
o.description = _('Track per-client bandwidth usage with iptables counters');
|
||||||
|
o.default = o.enabled;
|
||||||
|
|
||||||
|
o = tracking.option(form.Value, 'history_retention', _('History Retention (days)'));
|
||||||
|
o.datatype = 'range(1,90)';
|
||||||
|
o.default = '30';
|
||||||
|
o.description = _('How long to keep usage history');
|
||||||
|
|
||||||
|
// Alerts
|
||||||
|
var alerts = m.section(form.NamedSection, 'alerts', 'alerts', _('Alert Settings'));
|
||||||
|
alerts.anonymous = false;
|
||||||
|
alerts.addremove = false;
|
||||||
|
|
||||||
|
o = alerts.option(form.Flag, 'enabled', _('Enable Alerts'));
|
||||||
|
o.default = o.disabled;
|
||||||
|
|
||||||
|
o = alerts.option(form.Value, 'quota_threshold', _('Quota Alert Threshold (%)'));
|
||||||
|
o.datatype = 'range(50,100)';
|
||||||
|
o.default = '90';
|
||||||
|
o.description = _('Send alert when quota usage exceeds this percentage');
|
||||||
|
o.depends('enabled', '1');
|
||||||
|
|
||||||
|
o = alerts.option(form.Value, 'email', _('Alert Email'));
|
||||||
|
o.datatype = 'email';
|
||||||
|
o.placeholder = 'admin@example.com';
|
||||||
|
o.depends('enabled', '1');
|
||||||
|
|
||||||
|
return m.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require poll';
|
||||||
|
'require bandwidth-manager/api as API';
|
||||||
|
|
||||||
|
return L.view.extend({
|
||||||
|
load: function() {
|
||||||
|
return API.getUsageRealtime();
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(clients) {
|
||||||
|
var v = E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', {}, _('Real-time Usage')),
|
||||||
|
E('div', { 'class': 'cbi-map-descr' }, _('Live bandwidth usage per client (updates every 5 seconds)'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
var container = E('div', { 'id': 'usage-container', 'class': 'cbi-section' });
|
||||||
|
v.appendChild(container);
|
||||||
|
|
||||||
|
// Initial render
|
||||||
|
this.renderUsageTable(container, clients);
|
||||||
|
|
||||||
|
// Auto-refresh every 5 seconds
|
||||||
|
poll.add(L.bind(function() {
|
||||||
|
return API.getUsageRealtime().then(L.bind(function(data) {
|
||||||
|
this.renderUsageTable(container, data);
|
||||||
|
}, this));
|
||||||
|
}, this), 5);
|
||||||
|
|
||||||
|
return v;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderUsageTable: function(container, clients) {
|
||||||
|
L.dom.content(container, [
|
||||||
|
E('h3', {}, _('Active Clients')),
|
||||||
|
E('table', { 'class': 'table' }, [
|
||||||
|
E('tr', { 'class': 'tr table-titles' }, [
|
||||||
|
E('th', { 'class': 'th' }, _('Client')),
|
||||||
|
E('th', { 'class': 'th' }, _('IP')),
|
||||||
|
E('th', { 'class': 'th' }, _('MAC')),
|
||||||
|
E('th', { 'class': 'th' }, _('Download')),
|
||||||
|
E('th', { 'class': 'th' }, _('Upload')),
|
||||||
|
E('th', { 'class': 'th' }, _('Total')),
|
||||||
|
E('th', { 'class': 'th' }, _('Quota'))
|
||||||
|
])
|
||||||
|
].concat(clients.length > 0 ? clients.map(L.bind(function(client) {
|
||||||
|
var total = (client.rx_bytes || 0) + (client.tx_bytes || 0);
|
||||||
|
|
||||||
|
var quotaCell = _('None');
|
||||||
|
if (client.has_quota) {
|
||||||
|
var percent = client.limit_mb > 0 ? Math.floor((client.used_mb * 100) / client.limit_mb) : 0;
|
||||||
|
var color = percent > 90 ? 'red' : (percent > 75 ? 'orange' : 'green');
|
||||||
|
quotaCell = [
|
||||||
|
E('div', {}, client.used_mb + ' / ' + client.limit_mb + ' MB'),
|
||||||
|
E('div', { 'style': 'background: #eee; width: 80px; height: 8px; border-radius: 4px;' }, [
|
||||||
|
E('div', {
|
||||||
|
'style': 'background: ' + color + '; width: ' + Math.min(percent, 100) + '%; height: 100%; border-radius: 4px;'
|
||||||
|
})
|
||||||
|
])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, client.hostname),
|
||||||
|
E('td', { 'class': 'td' }, client.ip),
|
||||||
|
E('td', { 'class': 'td' }, E('code', {}, client.mac)),
|
||||||
|
E('td', { 'class': 'td' }, [
|
||||||
|
E('span', { 'style': 'color: #28a745' }, '⬇ ' + this.formatBytes(client.rx_bytes))
|
||||||
|
]),
|
||||||
|
E('td', { 'class': 'td' }, [
|
||||||
|
E('span', { 'style': 'color: #dc3545' }, '⬆ ' + this.formatBytes(client.tx_bytes))
|
||||||
|
]),
|
||||||
|
E('td', { 'class': 'td' }, E('strong', {}, this.formatBytes(total))),
|
||||||
|
E('td', { 'class': 'td' }, quotaCell)
|
||||||
|
]);
|
||||||
|
}, this)) : [
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td center', 'colspan': 7 },
|
||||||
|
E('em', {}, _('No active clients')))
|
||||||
|
])
|
||||||
|
]))
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
formatBytes: function(bytes) {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
var k = 1024;
|
||||||
|
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
var i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
@ -1,89 +1,39 @@
|
|||||||
config bandwidth 'global'
|
config global 'global'
|
||||||
option enabled '1'
|
option enabled '0'
|
||||||
option interface 'br-lan'
|
option interface 'br-lan'
|
||||||
option wan_interface 'wan'
|
option sqm_enabled '0'
|
||||||
option default_download '100000'
|
|
||||||
option default_upload '50000'
|
|
||||||
option quota_period 'monthly'
|
|
||||||
|
|
||||||
config class 'realtime'
|
config sqm 'sqm'
|
||||||
option name 'Real-time'
|
option download_speed '100000'
|
||||||
option priority '1'
|
option upload_speed '50000'
|
||||||
option rate '30'
|
option qdisc 'cake'
|
||||||
option ceil '100'
|
option nat '1'
|
||||||
option description 'VoIP, Video calls'
|
option overhead '0'
|
||||||
|
|
||||||
config class 'interactive'
|
config tracking 'tracking'
|
||||||
option name 'Interactive'
|
option iptables_tracking '1'
|
||||||
option priority '2'
|
option history_retention '30'
|
||||||
option rate '20'
|
|
||||||
option ceil '100'
|
|
||||||
option description 'Gaming, SSH, DNS'
|
|
||||||
|
|
||||||
config class 'streaming'
|
config alerts 'alerts'
|
||||||
option name 'Streaming'
|
option enabled '0'
|
||||||
option priority '3'
|
option quota_threshold '90'
|
||||||
option rate '20'
|
option email ''
|
||||||
option ceil '90'
|
|
||||||
option description 'Video streaming'
|
|
||||||
|
|
||||||
config class 'browsing'
|
# Example QoS rule
|
||||||
option name 'Browsing'
|
#config rule 'rule_youtube'
|
||||||
option priority '4'
|
# option name 'Limit YouTube'
|
||||||
option rate '15'
|
# option type 'application'
|
||||||
option ceil '80'
|
# option target 'youtube'
|
||||||
option description 'Web browsing'
|
# option limit_down '5000'
|
||||||
|
# option limit_up '1000'
|
||||||
|
# option priority '6'
|
||||||
|
# option enabled '1'
|
||||||
|
|
||||||
config class 'download'
|
# Example client quota
|
||||||
option name 'Downloads'
|
#config quota 'quota_phone'
|
||||||
option priority '5'
|
# option mac 'AA:BB:CC:DD:EE:FF'
|
||||||
option rate '10'
|
# option name 'iPhone Jean'
|
||||||
option ceil '70'
|
# option limit_mb '10240'
|
||||||
option description 'File downloads'
|
# option action 'throttle'
|
||||||
|
# option reset_day '1'
|
||||||
config class 'bulk'
|
# option enabled '1'
|
||||||
option name 'Bulk'
|
|
||||||
option priority '6'
|
|
||||||
option rate '5'
|
|
||||||
option ceil '50'
|
|
||||||
option description 'P2P, Backups'
|
|
||||||
|
|
||||||
config media 'voip'
|
|
||||||
option name 'VoIP'
|
|
||||||
option class 'realtime'
|
|
||||||
list port '5060'
|
|
||||||
list port '5061'
|
|
||||||
list port '10000-20000'
|
|
||||||
list protocol 'sip'
|
|
||||||
list protocol 'rtp'
|
|
||||||
|
|
||||||
config media 'gaming'
|
|
||||||
option name 'Gaming'
|
|
||||||
option class 'interactive'
|
|
||||||
list port '3074'
|
|
||||||
list port '3478-3480'
|
|
||||||
list port '27015-27030'
|
|
||||||
option dscp 'ef'
|
|
||||||
|
|
||||||
config media 'streaming'
|
|
||||||
option name 'Streaming'
|
|
||||||
option class 'streaming'
|
|
||||||
list domain 'netflix.com'
|
|
||||||
list domain 'youtube.com'
|
|
||||||
list domain 'twitch.tv'
|
|
||||||
list domain 'spotify.com'
|
|
||||||
|
|
||||||
config quota 'default'
|
|
||||||
option daily_limit '0'
|
|
||||||
option monthly_limit '0'
|
|
||||||
option throttle_speed '1000'
|
|
||||||
option action 'throttle'
|
|
||||||
|
|
||||||
config schedule 'peak'
|
|
||||||
option name 'Peak Hours'
|
|
||||||
option enabled '1'
|
|
||||||
option days 'mon tue wed thu fri'
|
|
||||||
option start '18:00'
|
|
||||||
option end '23:00'
|
|
||||||
option download_limit '80'
|
|
||||||
option upload_limit '80'
|
|
||||||
|
|||||||
@ -1,192 +1,556 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
# Bandwidth Manager RPCD Backend
|
||||||
|
# Provides QoS rules, client quotas, and traffic statistics
|
||||||
|
|
||||||
. /lib/functions.sh
|
. /lib/functions.sh
|
||||||
. /usr/share/libubox/jshn.sh
|
. /usr/share/libubox/jshn.sh
|
||||||
|
|
||||||
get_status() {
|
# Configuration paths
|
||||||
json_init
|
CONFIG_FILE="/etc/config/bandwidth"
|
||||||
|
USAGE_DB="/tmp/bandwidth_usage.db"
|
||||||
local enabled interface
|
IPTABLES_CHAIN="BW_TRACKING"
|
||||||
config_load bandwidth
|
|
||||||
config_get enabled global enabled "0"
|
# Initialize usage database
|
||||||
config_get interface global interface "br-lan"
|
init_usage_db() {
|
||||||
|
if [ ! -f "$USAGE_DB" ]; then
|
||||||
json_add_boolean "enabled" "$enabled"
|
cat > "$USAGE_DB" << 'EOF'
|
||||||
json_add_string "interface" "$interface"
|
# MAC|Timestamp|RX_Bytes|TX_Bytes
|
||||||
|
EOF
|
||||||
# Get current bandwidth stats
|
fi
|
||||||
local rx_bytes tx_bytes
|
|
||||||
rx_bytes=$(cat /sys/class/net/$interface/statistics/rx_bytes 2>/dev/null || echo 0)
|
|
||||||
tx_bytes=$(cat /sys/class/net/$interface/statistics/tx_bytes 2>/dev/null || echo 0)
|
|
||||||
|
|
||||||
json_add_int "rx_bytes" "$rx_bytes"
|
|
||||||
json_add_int "tx_bytes" "$tx_bytes"
|
|
||||||
|
|
||||||
# Check if QoS is active
|
|
||||||
local qos_active=0
|
|
||||||
tc qdisc show dev $interface 2>/dev/null | grep -qE "(cake|fq_codel|htb)" && qos_active=1
|
|
||||||
json_add_boolean "qos_active" "$qos_active"
|
|
||||||
|
|
||||||
json_dump
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get_classes() {
|
# Get system status and global stats
|
||||||
config_load bandwidth
|
status() {
|
||||||
json_init
|
json_init
|
||||||
json_add_array "classes"
|
|
||||||
|
local enabled interface sqm_enabled
|
||||||
_add_class() {
|
config_load bandwidth
|
||||||
local name priority rate ceil desc
|
config_get enabled global enabled "0"
|
||||||
config_get name "$1" name ""
|
config_get interface global interface "br-lan"
|
||||||
config_get priority "$1" priority "5"
|
config_get sqm_enabled global sqm_enabled "0"
|
||||||
config_get rate "$1" rate "10"
|
|
||||||
config_get ceil "$1" ceil "100"
|
json_add_boolean "enabled" "$enabled"
|
||||||
config_get desc "$1" description ""
|
json_add_string "interface" "$interface"
|
||||||
|
json_add_boolean "sqm_enabled" "$sqm_enabled"
|
||||||
json_add_object ""
|
|
||||||
json_add_string "id" "$1"
|
# Check QoS status
|
||||||
json_add_string "name" "$name"
|
local qos_active=0
|
||||||
json_add_int "priority" "$priority"
|
tc qdisc show dev "$interface" 2>/dev/null | grep -qE "(cake|htb|fq_codel)" && qos_active=1
|
||||||
json_add_int "rate" "$rate"
|
json_add_boolean "qos_active" "$qos_active"
|
||||||
json_add_int "ceil" "$ceil"
|
|
||||||
json_add_string "description" "$desc"
|
# Get interface stats
|
||||||
json_close_object
|
if [ -d "/sys/class/net/$interface" ]; then
|
||||||
}
|
local rx_bytes=$(cat /sys/class/net/$interface/statistics/rx_bytes 2>/dev/null || echo 0)
|
||||||
config_foreach _add_class class
|
local tx_bytes=$(cat /sys/class/net/$interface/statistics/tx_bytes 2>/dev/null || echo 0)
|
||||||
|
local rx_packets=$(cat /sys/class/net/$interface/statistics/rx_packets 2>/dev/null || echo 0)
|
||||||
json_close_array
|
local tx_packets=$(cat /sys/class/net/$interface/statistics/tx_packets 2>/dev/null || echo 0)
|
||||||
json_dump
|
|
||||||
|
json_add_object "stats"
|
||||||
|
json_add_int "rx_bytes" "$rx_bytes"
|
||||||
|
json_add_int "tx_bytes" "$tx_bytes"
|
||||||
|
json_add_int "rx_packets" "$rx_packets"
|
||||||
|
json_add_int "tx_packets" "$tx_packets"
|
||||||
|
json_close_object
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Count rules and quotas
|
||||||
|
local rule_count=0
|
||||||
|
local quota_count=0
|
||||||
|
config_foreach count_section rule && rule_count=$?
|
||||||
|
config_foreach count_section quota && quota_count=$?
|
||||||
|
|
||||||
|
json_add_int "rule_count" "$rule_count"
|
||||||
|
json_add_int "quota_count" "$quota_count"
|
||||||
|
|
||||||
|
json_dump
|
||||||
}
|
}
|
||||||
|
|
||||||
get_quotas() {
|
count_section() {
|
||||||
config_load bandwidth
|
return $(( $? + 1 ))
|
||||||
json_init
|
|
||||||
json_add_array "quotas"
|
|
||||||
|
|
||||||
_add_quota() {
|
|
||||||
local daily monthly throttle action
|
|
||||||
config_get daily "$1" daily_limit "0"
|
|
||||||
config_get monthly "$1" monthly_limit "0"
|
|
||||||
config_get throttle "$1" throttle_speed "1000"
|
|
||||||
config_get action "$1" action "throttle"
|
|
||||||
|
|
||||||
json_add_object ""
|
|
||||||
json_add_string "id" "$1"
|
|
||||||
json_add_int "daily_limit" "$daily"
|
|
||||||
json_add_int "monthly_limit" "$monthly"
|
|
||||||
json_add_int "throttle_speed" "$throttle"
|
|
||||||
json_add_string "action" "$action"
|
|
||||||
json_close_object
|
|
||||||
}
|
|
||||||
config_foreach _add_quota quota
|
|
||||||
|
|
||||||
json_close_array
|
|
||||||
json_dump
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get_media() {
|
# List all QoS rules
|
||||||
config_load bandwidth
|
list_rules() {
|
||||||
json_init
|
config_load bandwidth
|
||||||
json_add_array "media"
|
json_init
|
||||||
|
json_add_array "rules"
|
||||||
_add_media() {
|
|
||||||
local name class
|
_add_rule() {
|
||||||
config_get name "$1" name ""
|
local name type target limit_down limit_up priority enabled schedule
|
||||||
config_get class "$1" class ""
|
config_get name "$1" name ""
|
||||||
|
config_get type "$1" type "application"
|
||||||
json_add_object ""
|
config_get target "$1" target ""
|
||||||
json_add_string "id" "$1"
|
config_get limit_down "$1" limit_down "0"
|
||||||
json_add_string "name" "$name"
|
config_get limit_up "$1" limit_up "0"
|
||||||
json_add_string "class" "$class"
|
config_get priority "$1" priority "5"
|
||||||
json_close_object
|
config_get enabled "$1" enabled "1"
|
||||||
}
|
config_get schedule "$1" schedule ""
|
||||||
config_foreach _add_media media
|
|
||||||
|
json_add_object ""
|
||||||
json_close_array
|
json_add_string "id" "$1"
|
||||||
json_dump
|
json_add_string "name" "$name"
|
||||||
|
json_add_string "type" "$type"
|
||||||
|
json_add_string "target" "$target"
|
||||||
|
json_add_int "limit_down" "$limit_down"
|
||||||
|
json_add_int "limit_up" "$limit_up"
|
||||||
|
json_add_int "priority" "$priority"
|
||||||
|
json_add_boolean "enabled" "$enabled"
|
||||||
|
json_add_string "schedule" "$schedule"
|
||||||
|
json_close_object
|
||||||
|
}
|
||||||
|
|
||||||
|
config_foreach _add_rule rule
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_dump
|
||||||
}
|
}
|
||||||
|
|
||||||
get_clients() {
|
# Add new QoS rule
|
||||||
json_init
|
add_rule() {
|
||||||
json_add_array "clients"
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
# Parse DHCP leases
|
|
||||||
if [ -f /tmp/dhcp.leases ]; then
|
local name type target limit_down limit_up priority
|
||||||
while read expires mac ip hostname clientid; do
|
json_get_var name name
|
||||||
# Get current bandwidth for this client
|
json_get_var type type "application"
|
||||||
local rx=0 tx=0
|
json_get_var target target
|
||||||
|
json_get_var limit_down limit_down "0"
|
||||||
json_add_object ""
|
json_get_var limit_up limit_up "0"
|
||||||
json_add_string "mac" "$mac"
|
json_get_var priority priority "5"
|
||||||
json_add_string "ip" "$ip"
|
|
||||||
json_add_string "hostname" "${hostname:-unknown}"
|
json_cleanup
|
||||||
json_add_int "rx_bytes" "$rx"
|
|
||||||
json_add_int "tx_bytes" "$tx"
|
if [ -z "$name" ] || [ -z "$target" ]; then
|
||||||
json_close_object
|
json_init
|
||||||
done < /tmp/dhcp.leases
|
json_add_boolean "success" 0
|
||||||
fi
|
json_add_string "message" "Name and target are required"
|
||||||
|
json_dump
|
||||||
json_close_array
|
return 1
|
||||||
json_dump
|
fi
|
||||||
|
|
||||||
|
# Generate unique ID
|
||||||
|
local rule_id="rule_$(date +%s)"
|
||||||
|
|
||||||
|
# Add to UCI config
|
||||||
|
uci -q batch << EOF
|
||||||
|
set bandwidth.$rule_id=rule
|
||||||
|
set bandwidth.$rule_id.name='$name'
|
||||||
|
set bandwidth.$rule_id.type='$type'
|
||||||
|
set bandwidth.$rule_id.target='$target'
|
||||||
|
set bandwidth.$rule_id.limit_down='$limit_down'
|
||||||
|
set bandwidth.$rule_id.limit_up='$limit_up'
|
||||||
|
set bandwidth.$rule_id.priority='$priority'
|
||||||
|
set bandwidth.$rule_id.enabled='1'
|
||||||
|
commit bandwidth
|
||||||
|
EOF
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "rule_id" "$rule_id"
|
||||||
|
json_add_string "message" "Rule created successfully"
|
||||||
|
json_dump
|
||||||
}
|
}
|
||||||
|
|
||||||
get_stats() {
|
# Delete QoS rule
|
||||||
json_init
|
delete_rule() {
|
||||||
|
read -r input
|
||||||
local interface
|
json_load "$input"
|
||||||
config_load bandwidth
|
|
||||||
config_get interface global interface "br-lan"
|
local rule_id
|
||||||
|
json_get_var rule_id rule_id
|
||||||
# TC statistics
|
json_cleanup
|
||||||
json_add_object "tc"
|
|
||||||
local tc_stats=$(tc -s qdisc show dev $interface 2>/dev/null)
|
if [ -z "$rule_id" ]; then
|
||||||
json_add_string "raw" "$tc_stats"
|
json_init
|
||||||
json_close_object
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "message" "Rule ID is required"
|
||||||
# Interface statistics
|
json_dump
|
||||||
json_add_object "interface"
|
return 1
|
||||||
json_add_int "rx_bytes" "$(cat /sys/class/net/$interface/statistics/rx_bytes 2>/dev/null || echo 0)"
|
fi
|
||||||
json_add_int "tx_bytes" "$(cat /sys/class/net/$interface/statistics/tx_bytes 2>/dev/null || echo 0)"
|
|
||||||
json_add_int "rx_packets" "$(cat /sys/class/net/$interface/statistics/rx_packets 2>/dev/null || echo 0)"
|
# Check if rule exists
|
||||||
json_add_int "tx_packets" "$(cat /sys/class/net/$interface/statistics/tx_packets 2>/dev/null || echo 0)"
|
if ! uci -q get bandwidth.$rule_id >/dev/null 2>&1; then
|
||||||
json_close_object
|
json_init
|
||||||
|
json_add_boolean "success" 0
|
||||||
json_dump
|
json_add_string "message" "Rule not found"
|
||||||
|
json_dump
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
uci -q delete bandwidth.$rule_id
|
||||||
|
uci -q commit bandwidth
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Rule deleted successfully"
|
||||||
|
json_dump
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_qos() {
|
# List all client quotas
|
||||||
local interface download upload
|
list_quotas() {
|
||||||
config_load bandwidth
|
config_load bandwidth
|
||||||
config_get interface global interface "br-lan"
|
json_init
|
||||||
config_get download global default_download "100000"
|
json_add_array "quotas"
|
||||||
config_get upload global default_upload "50000"
|
|
||||||
|
_add_quota() {
|
||||||
# Clear existing
|
local mac name limit_mb used_mb action reset_day enabled
|
||||||
tc qdisc del dev $interface root 2>/dev/null
|
config_get mac "$1" mac ""
|
||||||
tc qdisc del dev $interface ingress 2>/dev/null
|
config_get name "$1" name ""
|
||||||
|
config_get limit_mb "$1" limit_mb "0"
|
||||||
# Apply CAKE qdisc
|
config_get action "$1" action "throttle"
|
||||||
tc qdisc add dev $interface root cake bandwidth ${download}kbit
|
config_get reset_day "$1" reset_day "1"
|
||||||
|
config_get enabled "$1" enabled "1"
|
||||||
json_init
|
|
||||||
json_add_boolean "success" 1
|
# Get current usage
|
||||||
json_add_string "message" "QoS applied with ${download}kbit download"
|
used_mb=$(get_mac_usage "$mac")
|
||||||
json_dump
|
local percent=0
|
||||||
|
if [ "$limit_mb" -gt 0 ]; then
|
||||||
|
percent=$(( (used_mb * 100) / limit_mb ))
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_add_object ""
|
||||||
|
json_add_string "id" "$1"
|
||||||
|
json_add_string "mac" "$mac"
|
||||||
|
json_add_string "name" "$name"
|
||||||
|
json_add_int "limit_mb" "$limit_mb"
|
||||||
|
json_add_int "used_mb" "$used_mb"
|
||||||
|
json_add_int "percent" "$percent"
|
||||||
|
json_add_string "action" "$action"
|
||||||
|
json_add_int "reset_day" "$reset_day"
|
||||||
|
json_add_boolean "enabled" "$enabled"
|
||||||
|
json_close_object
|
||||||
|
}
|
||||||
|
|
||||||
|
config_foreach _add_quota quota
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_dump
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Get usage for a specific MAC address
|
||||||
|
get_mac_usage() {
|
||||||
|
local mac="$1"
|
||||||
|
local total_bytes=0
|
||||||
|
|
||||||
|
# Get from iptables counters
|
||||||
|
if iptables -L $IPTABLES_CHAIN -n -v -x 2>/dev/null | grep -qi "$mac"; then
|
||||||
|
local bytes=$(iptables -L $IPTABLES_CHAIN -n -v -x 2>/dev/null | grep -i "$mac" | awk '{sum+=$2} END {print sum}')
|
||||||
|
total_bytes=${bytes:-0}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Convert to MB
|
||||||
|
echo $(( total_bytes / 1024 / 1024 ))
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get quota details for specific client
|
||||||
|
get_quota() {
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
|
||||||
|
local mac
|
||||||
|
json_get_var mac mac
|
||||||
|
json_cleanup
|
||||||
|
|
||||||
|
if [ -z "$mac" ]; then
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "message" "MAC address is required"
|
||||||
|
json_dump
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
config_load bandwidth
|
||||||
|
local found=0
|
||||||
|
local quota_id name limit_mb action reset_day
|
||||||
|
|
||||||
|
_find_quota() {
|
||||||
|
local this_mac
|
||||||
|
config_get this_mac "$1" mac ""
|
||||||
|
if [ "$this_mac" = "$mac" ]; then
|
||||||
|
quota_id="$1"
|
||||||
|
config_get name "$1" name ""
|
||||||
|
config_get limit_mb "$1" limit_mb "0"
|
||||||
|
config_get action "$1" action "throttle"
|
||||||
|
config_get reset_day "$1" reset_day "1"
|
||||||
|
found=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
config_foreach _find_quota quota
|
||||||
|
|
||||||
|
if [ "$found" -eq 0 ]; then
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "message" "Quota not found for this MAC"
|
||||||
|
json_dump
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local used_mb=$(get_mac_usage "$mac")
|
||||||
|
local remaining_mb=$(( limit_mb - used_mb ))
|
||||||
|
[ "$remaining_mb" -lt 0 ] && remaining_mb=0
|
||||||
|
|
||||||
|
local percent=0
|
||||||
|
if [ "$limit_mb" -gt 0 ]; then
|
||||||
|
percent=$(( (used_mb * 100) / limit_mb ))
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "quota_id" "$quota_id"
|
||||||
|
json_add_string "mac" "$mac"
|
||||||
|
json_add_string "name" "$name"
|
||||||
|
json_add_int "limit_mb" "$limit_mb"
|
||||||
|
json_add_int "used_mb" "$used_mb"
|
||||||
|
json_add_int "remaining_mb" "$remaining_mb"
|
||||||
|
json_add_int "percent" "$percent"
|
||||||
|
json_add_string "action" "$action"
|
||||||
|
json_add_int "reset_day" "$reset_day"
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set or update quota
|
||||||
|
set_quota() {
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
|
||||||
|
local mac name limit_mb action reset_day
|
||||||
|
json_get_var mac mac
|
||||||
|
json_get_var name name ""
|
||||||
|
json_get_var limit_mb limit_mb "0"
|
||||||
|
json_get_var action action "throttle"
|
||||||
|
json_get_var reset_day reset_day "1"
|
||||||
|
json_cleanup
|
||||||
|
|
||||||
|
if [ -z "$mac" ]; then
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "message" "MAC address is required"
|
||||||
|
json_dump
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if quota exists for this MAC
|
||||||
|
config_load bandwidth
|
||||||
|
local quota_id=""
|
||||||
|
|
||||||
|
_find_existing() {
|
||||||
|
local this_mac
|
||||||
|
config_get this_mac "$1" mac ""
|
||||||
|
if [ "$this_mac" = "$mac" ]; then
|
||||||
|
quota_id="$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
config_foreach _find_existing quota
|
||||||
|
|
||||||
|
if [ -z "$quota_id" ]; then
|
||||||
|
# Create new quota
|
||||||
|
quota_id="quota_$(date +%s)"
|
||||||
|
uci -q batch << EOF
|
||||||
|
set bandwidth.$quota_id=quota
|
||||||
|
set bandwidth.$quota_id.mac='$mac'
|
||||||
|
set bandwidth.$quota_id.name='$name'
|
||||||
|
set bandwidth.$quota_id.limit_mb='$limit_mb'
|
||||||
|
set bandwidth.$quota_id.action='$action'
|
||||||
|
set bandwidth.$quota_id.reset_day='$reset_day'
|
||||||
|
set bandwidth.$quota_id.enabled='1'
|
||||||
|
commit bandwidth
|
||||||
|
EOF
|
||||||
|
local msg="Quota created successfully"
|
||||||
|
else
|
||||||
|
# Update existing quota
|
||||||
|
uci -q batch << EOF
|
||||||
|
set bandwidth.$quota_id.name='$name'
|
||||||
|
set bandwidth.$quota_id.limit_mb='$limit_mb'
|
||||||
|
set bandwidth.$quota_id.action='$action'
|
||||||
|
set bandwidth.$quota_id.reset_day='$reset_day'
|
||||||
|
commit bandwidth
|
||||||
|
EOF
|
||||||
|
local msg="Quota updated successfully"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "quota_id" "$quota_id"
|
||||||
|
json_add_string "message" "$msg"
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reset quota counter for a client
|
||||||
|
reset_quota() {
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
|
||||||
|
local mac
|
||||||
|
json_get_var mac mac
|
||||||
|
json_cleanup
|
||||||
|
|
||||||
|
if [ -z "$mac" ]; then
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "message" "MAC address is required"
|
||||||
|
json_dump
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reset iptables counters for this MAC
|
||||||
|
iptables -Z $IPTABLES_CHAIN 2>/dev/null
|
||||||
|
|
||||||
|
# Remove from usage DB
|
||||||
|
if [ -f "$USAGE_DB" ]; then
|
||||||
|
sed -i "/^${mac}|/d" "$USAGE_DB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Quota counter reset for $mac"
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get real-time usage for all clients
|
||||||
|
get_usage_realtime() {
|
||||||
|
json_init
|
||||||
|
json_add_array "clients"
|
||||||
|
|
||||||
|
# Parse DHCP leases for active clients
|
||||||
|
if [ -f /tmp/dhcp.leases ]; then
|
||||||
|
while read -r expires mac ip hostname clientid; do
|
||||||
|
local rx_bytes=0 tx_bytes=0
|
||||||
|
|
||||||
|
# Get current bytes from iptables
|
||||||
|
if iptables -L $IPTABLES_CHAIN -n -v -x 2>/dev/null | grep -qi "$mac"; then
|
||||||
|
rx_bytes=$(iptables -L $IPTABLES_CHAIN -n -v -x 2>/dev/null | grep -i "$mac" | awk '{print $2}' | head -1)
|
||||||
|
tx_bytes=$(iptables -L $IPTABLES_CHAIN -n -v -x 2>/dev/null | grep -i "$mac" | awk '{print $2}' | tail -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get quota info if exists
|
||||||
|
config_load bandwidth
|
||||||
|
local has_quota=0 limit_mb=0 used_mb=0
|
||||||
|
|
||||||
|
_check_quota() {
|
||||||
|
local this_mac
|
||||||
|
config_get this_mac "$1" mac ""
|
||||||
|
if [ "$this_mac" = "$mac" ]; then
|
||||||
|
has_quota=1
|
||||||
|
config_get limit_mb "$1" limit_mb "0"
|
||||||
|
used_mb=$(get_mac_usage "$mac")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
config_foreach _check_quota quota
|
||||||
|
|
||||||
|
json_add_object ""
|
||||||
|
json_add_string "mac" "$mac"
|
||||||
|
json_add_string "ip" "$ip"
|
||||||
|
json_add_string "hostname" "${hostname:-unknown}"
|
||||||
|
json_add_int "rx_bytes" "${rx_bytes:-0}"
|
||||||
|
json_add_int "tx_bytes" "${tx_bytes:-0}"
|
||||||
|
json_add_boolean "has_quota" "$has_quota"
|
||||||
|
if [ "$has_quota" -eq 1 ]; then
|
||||||
|
json_add_int "limit_mb" "$limit_mb"
|
||||||
|
json_add_int "used_mb" "$used_mb"
|
||||||
|
fi
|
||||||
|
json_close_object
|
||||||
|
done < /tmp/dhcp.leases
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get usage history
|
||||||
|
get_usage_history() {
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
|
||||||
|
local timeframe mac
|
||||||
|
json_get_var timeframe timeframe "24h"
|
||||||
|
json_get_var mac mac ""
|
||||||
|
json_cleanup
|
||||||
|
|
||||||
|
init_usage_db
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_array "history"
|
||||||
|
|
||||||
|
# Calculate time threshold
|
||||||
|
local now=$(date +%s)
|
||||||
|
local threshold=0
|
||||||
|
case "$timeframe" in
|
||||||
|
"1h") threshold=$(( now - 3600 )) ;;
|
||||||
|
"6h") threshold=$(( now - 21600 )) ;;
|
||||||
|
"24h") threshold=$(( now - 86400 )) ;;
|
||||||
|
"7d") threshold=$(( now - 604800 )) ;;
|
||||||
|
"30d") threshold=$(( now - 2592000 )) ;;
|
||||||
|
*) threshold=$(( now - 86400 )) ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Read usage database
|
||||||
|
if [ -f "$USAGE_DB" ]; then
|
||||||
|
while IFS='|' read -r db_mac timestamp rx_bytes tx_bytes; do
|
||||||
|
# Skip header line
|
||||||
|
[ "$db_mac" = "# MAC" ] && continue
|
||||||
|
|
||||||
|
# Filter by MAC if specified
|
||||||
|
if [ -n "$mac" ] && [ "$db_mac" != "$mac" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Filter by timeframe
|
||||||
|
if [ "$timestamp" -lt "$threshold" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_add_object ""
|
||||||
|
json_add_string "mac" "$db_mac"
|
||||||
|
json_add_int "timestamp" "$timestamp"
|
||||||
|
json_add_int "rx_bytes" "$rx_bytes"
|
||||||
|
json_add_int "tx_bytes" "$tx_bytes"
|
||||||
|
json_close_object
|
||||||
|
done < "$USAGE_DB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main dispatcher
|
||||||
case "$1" in
|
case "$1" in
|
||||||
list)
|
list)
|
||||||
echo '{"status":{},"classes":{},"quotas":{},"media":{},"clients":{},"stats":{},"apply_qos":{}}'
|
cat << 'EOF'
|
||||||
;;
|
{
|
||||||
call)
|
"status": {},
|
||||||
case "$2" in
|
"list_rules": {},
|
||||||
status) get_status ;;
|
"add_rule": { "name": "string", "type": "string", "target": "string", "limit_down": 0, "limit_up": 0, "priority": 5 },
|
||||||
classes) get_classes ;;
|
"delete_rule": { "rule_id": "string" },
|
||||||
quotas) get_quotas ;;
|
"list_quotas": {},
|
||||||
media) get_media ;;
|
"get_quota": { "mac": "string" },
|
||||||
clients) get_clients ;;
|
"set_quota": { "mac": "string", "name": "string", "limit_mb": 0, "action": "string", "reset_day": 1 },
|
||||||
stats) get_stats ;;
|
"reset_quota": { "mac": "string" },
|
||||||
apply_qos) apply_qos ;;
|
"get_usage_realtime": {},
|
||||||
*) echo '{"error":"Unknown method"}' ;;
|
"get_usage_history": { "timeframe": "24h", "mac": "" }
|
||||||
esac
|
}
|
||||||
;;
|
EOF
|
||||||
|
;;
|
||||||
|
call)
|
||||||
|
case "$2" in
|
||||||
|
status) status ;;
|
||||||
|
list_rules) list_rules ;;
|
||||||
|
add_rule) add_rule ;;
|
||||||
|
delete_rule) delete_rule ;;
|
||||||
|
list_quotas) list_quotas ;;
|
||||||
|
get_quota) get_quota ;;
|
||||||
|
set_quota) set_quota ;;
|
||||||
|
reset_quota) reset_quota ;;
|
||||||
|
get_usage_realtime) get_usage_realtime ;;
|
||||||
|
get_usage_history) get_usage_history ;;
|
||||||
|
*)
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Unknown method: $2"
|
||||||
|
json_dump
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@ -1,37 +1,52 @@
|
|||||||
{
|
{
|
||||||
"admin/network/bandwidth": {
|
"admin/network/bandwidth-manager": {
|
||||||
"title": "Bandwidth Manager",
|
"title": "Bandwidth Manager",
|
||||||
"order": 80,
|
"order": 55,
|
||||||
"action": {"type": "firstchild"}
|
"action": {
|
||||||
},
|
"type": "firstchild"
|
||||||
"admin/network/bandwidth/overview": {
|
},
|
||||||
"title": "Overview",
|
"depends": {
|
||||||
"order": 10,
|
"acl": ["luci-app-bandwidth-manager"]
|
||||||
"action": {"type": "view", "path": "bandwidth-manager/overview"}
|
}
|
||||||
},
|
},
|
||||||
"admin/network/bandwidth/classes": {
|
"admin/network/bandwidth-manager/overview": {
|
||||||
"title": "QoS Classes",
|
"title": "Overview",
|
||||||
"order": 20,
|
"order": 1,
|
||||||
"action": {"type": "view", "path": "bandwidth-manager/classes"}
|
"action": {
|
||||||
},
|
"type": "view",
|
||||||
"admin/network/bandwidth/quotas": {
|
"path": "bandwidth-manager/overview"
|
||||||
"title": "Quotas",
|
}
|
||||||
"order": 30,
|
},
|
||||||
"action": {"type": "view", "path": "bandwidth-manager/quotas"}
|
"admin/network/bandwidth-manager/rules": {
|
||||||
},
|
"title": "QoS Rules",
|
||||||
"admin/network/bandwidth/media": {
|
"order": 2,
|
||||||
"title": "Media Detection",
|
"action": {
|
||||||
"order": 40,
|
"type": "view",
|
||||||
"action": {"type": "view", "path": "bandwidth-manager/media"}
|
"path": "bandwidth-manager/rules"
|
||||||
},
|
}
|
||||||
"admin/network/bandwidth/clients": {
|
},
|
||||||
"title": "Clients",
|
"admin/network/bandwidth-manager/quotas": {
|
||||||
"order": 50,
|
"title": "Client Quotas",
|
||||||
"action": {"type": "view", "path": "bandwidth-manager/clients"}
|
"order": 3,
|
||||||
},
|
"action": {
|
||||||
"admin/network/bandwidth/schedules": {
|
"type": "view",
|
||||||
"title": "Schedules",
|
"path": "bandwidth-manager/quotas"
|
||||||
"order": 60,
|
}
|
||||||
"action": {"type": "view", "path": "bandwidth-manager/schedules"}
|
},
|
||||||
}
|
"admin/network/bandwidth-manager/usage": {
|
||||||
|
"title": "Real-time Usage",
|
||||||
|
"order": 4,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "bandwidth-manager/usage"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/network/bandwidth-manager/settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"order": 5,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "bandwidth-manager/settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,28 @@
|
|||||||
{
|
{
|
||||||
"luci-app-bandwidth-manager": {
|
"luci-app-bandwidth-manager": {
|
||||||
"description": "Bandwidth Manager",
|
"description": "Bandwidth Manager - QoS & Traffic Control",
|
||||||
"read": {
|
"read": {
|
||||||
"ubus": {
|
"ubus": {
|
||||||
"luci.bandwidth-manager": ["status", "classes", "quotas", "media", "clients", "stats"]
|
"luci.bandwidth-manager": [
|
||||||
},
|
"status",
|
||||||
"uci": ["bandwidth"]
|
"list_rules",
|
||||||
},
|
"list_quotas",
|
||||||
"write": {
|
"get_quota",
|
||||||
"ubus": {
|
"get_usage_realtime",
|
||||||
"luci.bandwidth-manager": ["apply_qos"]
|
"get_usage_history"
|
||||||
},
|
]
|
||||||
"uci": ["bandwidth"]
|
}
|
||||||
}
|
},
|
||||||
}
|
"write": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.bandwidth-manager": [
|
||||||
|
"add_rule",
|
||||||
|
"delete_rule",
|
||||||
|
"set_quota",
|
||||||
|
"reset_quota"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uci": ["bandwidth"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user