secubox-openwrt/package/secubox/luci-app-bandwidth-manager/root/usr/libexec/rpcd/luci.bandwidth-manager
CyberMind-FR fb9722ccd6 feat(bandwidth-manager): Add Smart QoS, Device Groups, and Analytics (Phase 5)
Advanced Bandwidth Manager features v0.5.0

Smart QoS (DPI Integration):
- Real-time application detection via nDPId
- Smart traffic suggestions based on detected patterns
- One-click DPI rule creation for applications
- Gaming, streaming, video conferencing detection
- Heavy downloader identification

Device Groups:
- Create device groups (Family, IoT, Work, Gaming, Kids, Guests)
- Shared quota across group members
- Unified priority assignment per group
- Easy member management via drag-drop UI
- Group usage tracking and visualization

Analytics Dashboard:
- Traffic summary with download/upload totals
- Active client count and per-client averages
- Application traffic breakdown charts
- Protocol distribution pie chart
- Top bandwidth users leaderboard
- Download/upload ratio analysis
- Historical data retention (30 days)
- Period selection (1h, 6h, 24h, 7d, 30d)

Backend Enhancements:
- get_dpi_applications: Fetch detected apps from nDPId
- get_smart_suggestions: AI-powered QoS recommendations
- apply_dpi_rule: Create rules based on app detection
- list_groups/create_group/update_group/delete_group
- add_to_group/remove_from_group: Member management
- get_analytics_summary: Traffic statistics
- get_hourly_data: Historical trends
- record_stats: Cron-based data collection

Menu Additions:
- Smart QoS (order: 10)
- Device Groups (order: 11)
- Analytics (order: 12)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 14:07:54 +01:00

1341 lines
34 KiB
Bash
Executable File

#!/bin/sh
# Bandwidth Manager RPCD Backend
# Provides QoS rules, client quotas, and traffic statistics
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
# Configuration paths
CONFIG_FILE="/etc/config/bandwidth"
USAGE_DB="/tmp/bandwidth_usage.db"
IPTABLES_CHAIN="BW_TRACKING"
# Initialize usage database
init_usage_db() {
if [ ! -f "$USAGE_DB" ]; then
cat > "$USAGE_DB" << 'EOF'
# MAC|Timestamp|RX_Bytes|TX_Bytes
EOF
fi
}
# Get system status and global stats
status() {
json_init
local enabled interface sqm_enabled
config_load bandwidth
config_get enabled global enabled "0"
config_get interface global interface "br-lan"
config_get sqm_enabled global sqm_enabled "0"
json_add_boolean "enabled" "$enabled"
json_add_string "interface" "$interface"
json_add_boolean "sqm_enabled" "$sqm_enabled"
# Check QoS status
local qos_active=0
tc qdisc show dev "$interface" 2>/dev/null | grep -qE "(cake|htb|fq_codel)" && qos_active=1
json_add_boolean "qos_active" "$qos_active"
# Get interface stats
if [ -d "/sys/class/net/$interface" ]; then
local rx_bytes=$(cat /sys/class/net/$interface/statistics/rx_bytes 2>/dev/null || echo 0)
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)
local tx_packets=$(cat /sys/class/net/$interface/statistics/tx_packets 2>/dev/null || echo 0)
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
}
count_section() {
return $(( $? + 1 ))
}
# List all QoS rules
list_rules() {
config_load bandwidth
json_init
json_add_array "rules"
_add_rule() {
local name type target limit_down limit_up priority enabled schedule
config_get name "$1" name ""
config_get type "$1" type "application"
config_get target "$1" target ""
config_get limit_down "$1" limit_down "0"
config_get limit_up "$1" limit_up "0"
config_get priority "$1" priority "5"
config_get enabled "$1" enabled "1"
config_get schedule "$1" schedule ""
json_add_object ""
json_add_string "id" "$1"
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
}
# Add new QoS rule
add_rule() {
read -r input
json_load "$input"
local name type target limit_down limit_up priority
json_get_var name name
json_get_var type type "application"
json_get_var target target
json_get_var limit_down limit_down "0"
json_get_var limit_up limit_up "0"
json_get_var priority priority "5"
json_cleanup
if [ -z "$name" ] || [ -z "$target" ]; then
json_init
json_add_boolean "success" 0
json_add_string "message" "Name and target are required"
json_dump
return 1
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
}
# Delete QoS rule
delete_rule() {
read -r input
json_load "$input"
local rule_id
json_get_var rule_id rule_id
json_cleanup
if [ -z "$rule_id" ]; then
json_init
json_add_boolean "success" 0
json_add_string "message" "Rule ID is required"
json_dump
return 1
fi
# Check if rule exists
if ! uci -q get bandwidth.$rule_id >/dev/null 2>&1; then
json_init
json_add_boolean "success" 0
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
}
# List all client quotas
list_quotas() {
config_load bandwidth
json_init
json_add_array "quotas"
_add_quota() {
local mac name limit_mb used_mb action reset_day enabled
config_get mac "$1" mac ""
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"
config_get enabled "$1" enabled "1"
# Get current usage
used_mb=$(get_mac_usage "$mac")
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
}
# Get media traffic types for classification
get_media() {
json_init
json_add_array "media"
# VoIP
json_add_object ""
json_add_string "id" "voip"
json_add_string "name" "VoIP"
json_add_string "class" "1 (Highest)"
json_add_string "description" "Voice over IP (SIP, RTP, VoLTE)"
json_add_string "ports" "5060-5061,5004,16384-32767"
json_close_object
# Gaming
json_add_object ""
json_add_string "id" "gaming"
json_add_string "name" "Gaming"
json_add_string "class" "2 (High)"
json_add_string "description" "Online gaming traffic"
json_add_string "ports" "3074,27015-27030,3478-3479"
json_close_object
# Streaming
json_add_object ""
json_add_string "id" "streaming"
json_add_string "name" "Video Streaming"
json_add_string "class" "4 (Normal)"
json_add_string "description" "YouTube, Netflix, Twitch"
json_add_string "ports" "443,80"
json_add_string "domains" "youtube.com,netflix.com,twitch.tv"
json_close_object
# Downloads
json_add_object ""
json_add_string "id" "download"
json_add_string "name" "Downloads"
json_add_string "class" "7 (Low)"
json_add_string "description" "HTTP/HTTPS downloads, torrents"
json_add_string "ports" "6881-6889,51413"
json_close_object
# Social Media
json_add_object ""
json_add_string "id" "social"
json_add_string "name" "Social Media"
json_add_string "class" "5 (Normal)"
json_add_string "description" "Facebook, Twitter, Instagram"
json_add_string "ports" "443,80"
json_close_object
# Work/Business
json_add_object ""
json_add_string "id" "work"
json_add_string "name" "Work / Business"
json_add_string "class" "3"
json_add_string "description" "Email, VPN, remote desktop"
json_add_string "ports" "22,3389,1194,1723"
json_close_object
json_close_array
json_dump
}
# ============================================
# Smart QoS - DPI Integration with nDPId
# ============================================
NDPID_FLOWS="/tmp/ndpid-flows.json"
DPI_RULES_FILE="/tmp/bandwidth_dpi_rules.json"
# Get DPI detected applications from nDPId
get_dpi_applications() {
json_init
json_add_array "applications"
if [ -f "$NDPID_FLOWS" ] && pgrep -x ndpid >/dev/null 2>&1; then
# Extract unique applications from nDPId flows
jq -r '.flows[]? | .app // "Unknown"' "$NDPID_FLOWS" 2>/dev/null | sort -u | while read app; do
[ -z "$app" ] && continue
local count=$(jq -r ".flows[] | select(.app == \"$app\") | .src_ip" "$NDPID_FLOWS" 2>/dev/null | wc -l)
local bytes=$(jq -r ".flows[] | select(.app == \"$app\") | .bytes_rx + .bytes_tx" "$NDPID_FLOWS" 2>/dev/null | awk '{sum+=$1} END {print sum}')
json_add_object ""
json_add_string "name" "$app"
json_add_int "flow_count" "$count"
json_add_int "total_bytes" "${bytes:-0}"
json_close_object
done
fi
json_close_array
# Check DPI source status
local dpi_source="none"
if [ -f "$NDPID_FLOWS" ] && pgrep -x ndpid >/dev/null 2>&1; then
dpi_source="ndpid"
elif [ -f "/var/run/netifyd/status.json" ] && pgrep -x netifyd >/dev/null 2>&1; then
dpi_source="netifyd"
fi
json_add_string "dpi_source" "$dpi_source"
json_dump
}
# Get smart QoS suggestions based on detected traffic
get_smart_suggestions() {
json_init
json_add_array "suggestions"
if [ -f "$NDPID_FLOWS" ]; then
# Check for gaming traffic
local gaming_count=$(jq -r '.flows[]? | select(.category == "Game" or .app | test("Steam|Xbox|PlayStation|Nintendo|Epic|Riot"; "i")) | .src_ip' "$NDPID_FLOWS" 2>/dev/null | sort -u | wc -l)
if [ "$gaming_count" -gt 0 ]; then
json_add_object ""
json_add_string "type" "gaming"
json_add_string "title" "Gaming Traffic Detected"
json_add_string "description" "Detected $gaming_count devices with gaming traffic. Recommend priority class 1-2 for low latency."
json_add_int "priority" 2
json_add_int "affected_devices" "$gaming_count"
json_close_object
fi
# Check for streaming traffic
local streaming_count=$(jq -r '.flows[]? | select(.app | test("YouTube|Netflix|Twitch|Disney|HBO|Amazon.*Video|Spotify"; "i")) | .src_ip' "$NDPID_FLOWS" 2>/dev/null | sort -u | wc -l)
if [ "$streaming_count" -gt 0 ]; then
json_add_object ""
json_add_string "type" "streaming"
json_add_string "title" "Streaming Traffic Detected"
json_add_string "description" "Detected $streaming_count devices streaming video/audio. Recommend priority class 4 for consistent quality."
json_add_int "priority" 4
json_add_int "affected_devices" "$streaming_count"
json_close_object
fi
# Check for video conferencing
local video_conf_count=$(jq -r '.flows[]? | select(.app | test("Zoom|Teams|Meet|WebEx|Skype"; "i")) | .src_ip' "$NDPID_FLOWS" 2>/dev/null | sort -u | wc -l)
if [ "$video_conf_count" -gt 0 ]; then
json_add_object ""
json_add_string "type" "videoconf"
json_add_string "title" "Video Conferencing Detected"
json_add_string "description" "Detected $video_conf_count devices in video calls. Recommend priority class 1 for real-time communication."
json_add_int "priority" 1
json_add_int "affected_devices" "$video_conf_count"
json_close_object
fi
# Check for heavy downloaders
local download_bytes=$(jq -r '.flows[]? | select(.bytes_rx > 100000000) | "\(.src_ip) \(.bytes_rx)"' "$NDPID_FLOWS" 2>/dev/null | sort -t' ' -k2 -nr | head -3)
if [ -n "$download_bytes" ]; then
local heavy_users=$(echo "$download_bytes" | wc -l)
json_add_object ""
json_add_string "type" "downloads"
json_add_string "title" "Heavy Bandwidth Users"
json_add_string "description" "Detected $heavy_users devices downloading heavily. Consider priority class 6-7 or quotas."
json_add_int "priority" 7
json_add_int "affected_devices" "$heavy_users"
json_close_object
fi
fi
json_close_array
json_dump
}
# Apply smart QoS rule based on app detection
apply_dpi_rule() {
read -r input
json_load "$input"
local app_name priority limit_down limit_up
json_get_var app_name app_name
json_get_var priority priority "5"
json_get_var limit_down limit_down "0"
json_get_var limit_up limit_up "0"
json_cleanup
if [ -z "$app_name" ]; then
json_init
json_add_boolean "success" 0
json_add_string "message" "Application name is required"
json_dump
return 1
fi
# Create DPI-based QoS rule
local rule_id="dpi_rule_$(echo "$app_name" | tr ' ' '_' | tr '[:upper:]' '[:lower:]')_$(date +%s)"
uci -q batch << EOF
set bandwidth.$rule_id=rule
set bandwidth.$rule_id.name='DPI: $app_name'
set bandwidth.$rule_id.type='dpi_app'
set bandwidth.$rule_id.target='$app_name'
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'
set bandwidth.$rule_id.dpi_match='1'
commit bandwidth
EOF
json_init
json_add_boolean "success" 1
json_add_string "rule_id" "$rule_id"
json_add_string "message" "DPI rule created for $app_name"
json_dump
}
# ============================================
# Device Groups Management
# ============================================
# List all device groups
list_groups() {
config_load bandwidth
json_init
json_add_array "groups"
_add_group() {
local name description quota_mb priority members enabled
config_get name "$1" name ""
config_get description "$1" description ""
config_get quota_mb "$1" quota_mb "0"
config_get priority "$1" priority "5"
config_get members "$1" members ""
config_get enabled "$1" enabled "1"
# Calculate group usage
local total_used=0
local member_count=0
for mac in $members; do
member_count=$(( member_count + 1 ))
local used=$(get_mac_usage "$mac")
total_used=$(( total_used + used ))
done
json_add_object ""
json_add_string "id" "$1"
json_add_string "name" "$name"
json_add_string "description" "$description"
json_add_int "quota_mb" "$quota_mb"
json_add_int "priority" "$priority"
json_add_int "member_count" "$member_count"
json_add_int "used_mb" "$total_used"
json_add_boolean "enabled" "$enabled"
json_close_object
}
config_foreach _add_group group
json_close_array
json_dump
}
# Get group details with members
get_group() {
read -r input
json_load "$input"
local group_id
json_get_var group_id group_id
json_cleanup
if [ -z "$group_id" ]; then
json_init
json_add_boolean "success" 0
json_add_string "message" "Group ID is required"
json_dump
return 1
fi
config_load bandwidth
local name description quota_mb priority members enabled
config_get name "$group_id" name ""
config_get description "$group_id" description ""
config_get quota_mb "$group_id" quota_mb "0"
config_get priority "$group_id" priority "5"
config_get members "$group_id" members ""
config_get enabled "$group_id" enabled "1"
if [ -z "$name" ]; then
json_init
json_add_boolean "success" 0
json_add_string "message" "Group not found"
json_dump
return 1
fi
json_init
json_add_boolean "success" 1
json_add_string "id" "$group_id"
json_add_string "name" "$name"
json_add_string "description" "$description"
json_add_int "quota_mb" "$quota_mb"
json_add_int "priority" "$priority"
json_add_boolean "enabled" "$enabled"
# Add members with details
json_add_array "members"
for mac in $members; do
local hostname ip used_mb
hostname=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $4}')
ip=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $3}')
used_mb=$(get_mac_usage "$mac")
json_add_object ""
json_add_string "mac" "$mac"
json_add_string "hostname" "${hostname:-unknown}"
json_add_string "ip" "${ip:-unknown}"
json_add_int "used_mb" "$used_mb"
json_close_object
done
json_close_array
json_dump
}
# Create device group
create_group() {
read -r input
json_load "$input"
local name description quota_mb priority members
json_get_var name name
json_get_var description description ""
json_get_var quota_mb quota_mb "0"
json_get_var priority priority "5"
json_get_var members members ""
json_cleanup
if [ -z "$name" ]; then
json_init
json_add_boolean "success" 0
json_add_string "message" "Group name is required"
json_dump
return 1
fi
local group_id="group_$(date +%s)"
uci -q batch << EOF
set bandwidth.$group_id=group
set bandwidth.$group_id.name='$name'
set bandwidth.$group_id.description='$description'
set bandwidth.$group_id.quota_mb='$quota_mb'
set bandwidth.$group_id.priority='$priority'
set bandwidth.$group_id.members='$members'
set bandwidth.$group_id.enabled='1'
commit bandwidth
EOF
json_init
json_add_boolean "success" 1
json_add_string "group_id" "$group_id"
json_add_string "message" "Group created successfully"
json_dump
}
# Update device group
update_group() {
read -r input
json_load "$input"
local group_id name description quota_mb priority members
json_get_var group_id group_id
json_get_var name name
json_get_var description description ""
json_get_var quota_mb quota_mb "0"
json_get_var priority priority "5"
json_get_var members members ""
json_cleanup
if [ -z "$group_id" ]; then
json_init
json_add_boolean "success" 0
json_add_string "message" "Group ID is required"
json_dump
return 1
fi
# Check if group exists
config_load bandwidth
local existing_name
config_get existing_name "$group_id" name ""
if [ -z "$existing_name" ]; then
json_init
json_add_boolean "success" 0
json_add_string "message" "Group not found"
json_dump
return 1
fi
uci -q batch << EOF
set bandwidth.$group_id.name='$name'
set bandwidth.$group_id.description='$description'
set bandwidth.$group_id.quota_mb='$quota_mb'
set bandwidth.$group_id.priority='$priority'
set bandwidth.$group_id.members='$members'
commit bandwidth
EOF
json_init
json_add_boolean "success" 1
json_add_string "message" "Group updated successfully"
json_dump
}
# Delete device group
delete_group() {
read -r input
json_load "$input"
local group_id
json_get_var group_id group_id
json_cleanup
if [ -z "$group_id" ]; then
json_init
json_add_boolean "success" 0
json_add_string "message" "Group ID is required"
json_dump
return 1
fi
# Check if group exists
if ! uci -q get bandwidth.$group_id >/dev/null 2>&1; then
json_init
json_add_boolean "success" 0
json_add_string "message" "Group not found"
json_dump
return 1
fi
uci -q delete bandwidth.$group_id
uci -q commit bandwidth
json_init
json_add_boolean "success" 1
json_add_string "message" "Group deleted successfully"
json_dump
}
# Add device to group
add_to_group() {
read -r input
json_load "$input"
local group_id mac
json_get_var group_id group_id
json_get_var mac mac
json_cleanup
if [ -z "$group_id" ] || [ -z "$mac" ]; then
json_init
json_add_boolean "success" 0
json_add_string "message" "Group ID and MAC address are required"
json_dump
return 1
fi
config_load bandwidth
local members
config_get members "$group_id" members ""
# Check if already in group
if echo "$members" | grep -qi "$mac"; then
json_init
json_add_boolean "success" 0
json_add_string "message" "Device already in group"
json_dump
return 1
fi
members="$members $mac"
uci -q set bandwidth.$group_id.members="$members"
uci -q commit bandwidth
json_init
json_add_boolean "success" 1
json_add_string "message" "Device added to group"
json_dump
}
# Remove device from group
remove_from_group() {
read -r input
json_load "$input"
local group_id mac
json_get_var group_id group_id
json_get_var mac mac
json_cleanup
if [ -z "$group_id" ] || [ -z "$mac" ]; then
json_init
json_add_boolean "success" 0
json_add_string "message" "Group ID and MAC address are required"
json_dump
return 1
fi
config_load bandwidth
local members
config_get members "$group_id" members ""
# Remove MAC from members list
members=$(echo "$members" | sed "s/$mac//gi" | tr -s ' ')
uci -q set bandwidth.$group_id.members="$members"
uci -q commit bandwidth
json_init
json_add_boolean "success" 1
json_add_string "message" "Device removed from group"
json_dump
}
# ============================================
# Analytics
# ============================================
ANALYTICS_DB="/tmp/bandwidth_analytics.db"
# Initialize analytics database
init_analytics_db() {
if [ ! -f "$ANALYTICS_DB" ]; then
cat > "$ANALYTICS_DB" << 'EOF'
# Hour|Date|Total_RX|Total_TX|Peak_RX|Peak_TX|Client_Count
EOF
fi
}
# Get analytics summary
get_analytics_summary() {
read -r input
json_load "$input"
local period
json_get_var period period "24h"
json_cleanup
json_init
# Current totals
local interface
config_load bandwidth
config_get interface global interface "br-lan"
if [ -d "/sys/class/net/$interface" ]; then
local rx_bytes=$(cat /sys/class/net/$interface/statistics/rx_bytes 2>/dev/null || echo 0)
local tx_bytes=$(cat /sys/class/net/$interface/statistics/tx_bytes 2>/dev/null || echo 0)
json_add_int "total_rx_bytes" "$rx_bytes"
json_add_int "total_tx_bytes" "$tx_bytes"
fi
# Client count
local client_count=$(cat /tmp/dhcp.leases 2>/dev/null | wc -l)
json_add_int "active_clients" "$client_count"
# Top talkers
json_add_array "top_talkers"
if [ -f /tmp/dhcp.leases ]; then
while read -r expires mac ip hostname clientid; do
local used_mb=$(get_mac_usage "$mac")
[ "$used_mb" -gt 0 ] && echo "$used_mb|$mac|$ip|${hostname:-unknown}"
done < /tmp/dhcp.leases | sort -t'|' -k1 -nr | head -5 | while IFS='|' read used mac ip hostname; do
json_add_object ""
json_add_string "mac" "$mac"
json_add_string "ip" "$ip"
json_add_string "hostname" "$hostname"
json_add_int "used_mb" "$used"
json_close_object
done
fi
json_close_array
# Application breakdown (from nDPId)
json_add_array "app_breakdown"
if [ -f "$NDPID_FLOWS" ]; then
jq -r '[.flows[]? | {app: (.app // "Unknown"), bytes: (.bytes_rx + .bytes_tx)}] | group_by(.app) | map({app: .[0].app, total_bytes: (map(.bytes) | add)}) | sort_by(.total_bytes) | reverse | .[:10][] | "\(.app)|\(.total_bytes)"' "$NDPID_FLOWS" 2>/dev/null | while IFS='|' read app bytes; do
json_add_object ""
json_add_string "app" "$app"
json_add_int "bytes" "${bytes:-0}"
json_close_object
done
fi
json_close_array
# Protocol breakdown (from nDPId)
json_add_array "protocol_breakdown"
if [ -f "$NDPID_FLOWS" ]; then
jq -r '[.flows[]? | {protocol: (.l4_proto // "Unknown"), bytes: (.bytes_rx + .bytes_tx)}] | group_by(.protocol) | map({protocol: .[0].protocol, total_bytes: (map(.bytes) | add)}) | sort_by(.total_bytes) | reverse | .[:5][] | "\(.protocol)|\(.total_bytes)"' "$NDPID_FLOWS" 2>/dev/null | while IFS='|' read proto bytes; do
json_add_object ""
json_add_string "protocol" "$proto"
json_add_int "bytes" "${bytes:-0}"
json_close_object
done
fi
json_close_array
json_dump
}
# Get hourly usage data for charts
get_hourly_data() {
read -r input
json_load "$input"
local days
json_get_var days days "7"
json_cleanup
init_analytics_db
json_init
json_add_array "hourly_data"
# Read from analytics DB
local threshold_date=$(date -d "$days days ago" +%Y%m%d 2>/dev/null || date +%Y%m%d)
if [ -f "$ANALYTICS_DB" ]; then
while IFS='|' read hour date rx tx peak_rx peak_tx clients; do
[ "$hour" = "# Hour" ] && continue
[ "$date" -lt "$threshold_date" ] 2>/dev/null && continue
json_add_object ""
json_add_int "hour" "$hour"
json_add_string "date" "$date"
json_add_int "rx_bytes" "$rx"
json_add_int "tx_bytes" "$tx"
json_add_int "peak_rx" "$peak_rx"
json_add_int "peak_tx" "$peak_tx"
json_add_int "clients" "$clients"
json_close_object
done < "$ANALYTICS_DB"
fi
json_close_array
json_dump
}
# Record current stats (called by cron)
record_stats() {
init_analytics_db
local interface
config_load bandwidth
config_get interface global interface "br-lan"
local hour=$(date +%H)
local date=$(date +%Y%m%d)
local rx_bytes=0 tx_bytes=0
if [ -d "/sys/class/net/$interface" ]; then
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)
fi
local client_count=$(cat /tmp/dhcp.leases 2>/dev/null | wc -l)
echo "$hour|$date|$rx_bytes|$tx_bytes|$rx_bytes|$tx_bytes|$client_count" >> "$ANALYTICS_DB"
# Keep only last 30 days
local old_date=$(date -d "30 days ago" +%Y%m%d 2>/dev/null || echo "0")
if [ -f "$ANALYTICS_DB" ]; then
awk -F'|' -v old="$old_date" '$2 >= old || /^#/' "$ANALYTICS_DB" > "${ANALYTICS_DB}.tmp"
mv "${ANALYTICS_DB}.tmp" "$ANALYTICS_DB"
fi
json_init
json_add_boolean "success" 1
json_dump
}
# Get QoS priority classes
get_classes() {
json_init
json_add_array "classes"
# Class 1 - Highest Priority
json_add_object ""
json_add_int "priority" 1
json_add_string "name" "Real-time (VoIP, Gaming)"
json_add_string "description" "Latency-sensitive applications requiring immediate delivery"
json_add_int "rate" 30
json_add_int "ceil" 100
json_close_object
# Class 2 - High Priority
json_add_object ""
json_add_int "priority" 2
json_add_string "name" "Interactive (SSH, RDP)"
json_add_string "description" "Interactive sessions and remote administration"
json_add_int "rate" 20
json_add_int "ceil" 90
json_close_object
# Class 3 - Medium-High Priority
json_add_object ""
json_add_int "priority" 3
json_add_string "name" "Business Critical"
json_add_string "description" "Email, VPN, business applications"
json_add_int "rate" 15
json_add_int "ceil" 80
json_close_object
# Class 4 - Normal Priority
json_add_object ""
json_add_int "priority" 4
json_add_string "name" "Video Streaming"
json_add_string "description" "YouTube, Netflix, video conferencing"
json_add_int "rate" 10
json_add_int "ceil" 70
json_close_object
# Class 5 - Normal Priority
json_add_object ""
json_add_int "priority" 5
json_add_string "name" "Web Browsing"
json_add_string "description" "HTTP/HTTPS web traffic, social media"
json_add_int "rate" 10
json_add_int "ceil" 60
json_close_object
# Class 6 - Low Priority
json_add_object ""
json_add_int "priority" 6
json_add_string "name" "Bulk Transfer"
json_add_string "description" "File transfers, cloud sync"
json_add_int "rate" 5
json_add_int "ceil" 50
json_close_object
# Class 7 - Lower Priority
json_add_object ""
json_add_int "priority" 7
json_add_string "name" "Downloads (P2P)"
json_add_string "description" "Torrents, large downloads"
json_add_int "rate" 5
json_add_int "ceil" 40
json_close_object
# Class 8 - Lowest Priority
json_add_object ""
json_add_int "priority" 8
json_add_string "name" "Background / Scavenger"
json_add_string "description" "System updates, backups, lowest priority traffic"
json_add_int "rate" 5
json_add_int "ceil" 30
json_close_object
json_close_array
json_dump
}
# Main dispatcher
case "$1" in
list)
cat << 'EOF'
{
"status": {},
"list_rules": {},
"add_rule": { "name": "string", "type": "string", "target": "string", "limit_down": 0, "limit_up": 0, "priority": 5 },
"delete_rule": { "rule_id": "string" },
"list_quotas": {},
"get_quota": { "mac": "string" },
"set_quota": { "mac": "string", "name": "string", "limit_mb": 0, "action": "string", "reset_day": 1 },
"reset_quota": { "mac": "string" },
"get_usage_realtime": {},
"get_usage_history": { "timeframe": "24h", "mac": "" },
"get_media": {},
"get_classes": {},
"get_dpi_applications": {},
"get_smart_suggestions": {},
"apply_dpi_rule": { "app_name": "string", "priority": 5, "limit_down": 0, "limit_up": 0 },
"list_groups": {},
"get_group": { "group_id": "string" },
"create_group": { "name": "string", "description": "", "quota_mb": 0, "priority": 5, "members": "" },
"update_group": { "group_id": "string", "name": "string", "description": "", "quota_mb": 0, "priority": 5, "members": "" },
"delete_group": { "group_id": "string" },
"add_to_group": { "group_id": "string", "mac": "string" },
"remove_from_group": { "group_id": "string", "mac": "string" },
"get_analytics_summary": { "period": "24h" },
"get_hourly_data": { "days": 7 },
"record_stats": {}
}
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 ;;
get_media) get_media ;;
get_classes) get_classes ;;
get_dpi_applications) get_dpi_applications ;;
get_smart_suggestions) get_smart_suggestions ;;
apply_dpi_rule) apply_dpi_rule ;;
list_groups) list_groups ;;
get_group) get_group ;;
create_group) create_group ;;
update_group) update_group ;;
delete_group) delete_group ;;
add_to_group) add_to_group ;;
remove_from_group) remove_from_group ;;
get_analytics_summary) get_analytics_summary ;;
get_hourly_data) get_hourly_data ;;
record_stats) record_stats ;;
*)
json_init
json_add_boolean "success" 0
json_add_string "error" "Unknown method: $2"
json_dump
;;
esac
;;
esac