#!/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