🌴 Sync 2026-03-05 23:51:42

This commit is contained in:
github-actions[bot] committed 2026-03-05 23:51:42 +08:00
1 parent 97c049ed0a
commit 0acff03804
14253 files changed
+3893239

No files matched your search

+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/env lua
local jsonc = require "luci.jsonc"
-- Check if an IP address is private - not needed at the moment
local function is_private_ip(ip)
if not ip or ip == "" then
return false
end
if ip:match(":") then -- IPv6
-- Check for ULA (fc00::/7), link-local (fe80::/10), or loopback (::1)
return ip:match("^f[cd]") or ip:match("^fe80:") or ip == "::1"
else -- IPv4
local octets = {ip:match("(%d+)%.(%d+)%.(%d+)%.(%d+)")}
if not octets[1] then return false end
local first, second = tonumber(octets[1]), tonumber(octets[2])
-- Check for private IPv4 ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
return (first == 10) or
(first == 172 and second >= 16 and second <= 31) or
(first == 192 and second == 168)
end
end
-- Parse a single connection line from nf_conntrack
local function parse_connection(line)
local conn = {}
conn.layer3, conn.layer4, conn.protocol, conn.timeout = line:match("(%w+)%s+(%d+)%s+(%w+)%s+(%d+)")
if not (conn.layer3 and conn.layer4 and conn.protocol and conn.timeout) then
return nil
end
if conn.protocol == "icmp" or conn.protocol == "icmpv6" then
-- Handle ICMP and ICMPv6 protocols
conn.src, conn.dst, conn.type, conn.code, conn.id =
line:match("src=(%S+)%s+dst=(%S+)%s+type=(%d+)%s+code=(%d+)%s+id=(%d+)")
conn.in_packets, conn.in_bytes = line:match("packets=(%d+)%s+bytes=(%d+)")
conn.out_packets, conn.out_bytes = conn.in_packets, conn.in_bytes
conn.sport, conn.dport = "-", "-" -- ICMP doesn't use ports
else
-- Handle other protocols (e.g., TCP, UDP)
local src1, dst1, sport1, dport1, packets1, bytes1,
src2, dst2, sport2, dport2, packets2, bytes2 =
line:match("src=(%S+)%s+dst=(%S+)%s+sport=(%d+)%s+dport=(%d+)%s+packets=(%d+)%s+bytes=(%d+).*src=(%S+)%s+dst=(%S+)%s+sport=(%d+)%s+dport=(%d+)%s+packets=(%d+)%s+bytes=(%d+)")
-- Always use the first direction as the original
conn.src, conn.dst, conn.sport, conn.dport = src1, dst1, sport1, dport1
conn.out_packets, conn.in_packets = tonumber(packets1), tonumber(packets2)
conn.out_bytes, conn.in_bytes = tonumber(bytes1), tonumber(bytes2)
end
if not (conn.src and conn.dst) then
return nil
end
-- Extract additional connection information
conn.dscp = math.floor(tonumber(line:match("mark=(%d+)") or "0"))
conn.use = tonumber(line:match("use=(%d+)") or "0")
conn.timeout = tonumber(conn.timeout) or 0
-- Determine connection state
if line:match("%[ASSURED%]") then
conn.state = "ASSURED"
elseif line:match("%[UNREPLIED%]") then
conn.state = "UNREPLIED"
else
conn.state = "UNKNOWN"
end
return conn
end
-- Retrieve all current connections from nf_conntrack
local function get_connections()
local conn_list = {}
-- Read MAX_CONNECTIONS from config
local max_connections = 0
local config = io.popen("uci get qosmate.advanced.MAX_CONNECTIONS 2>/dev/null")
if config then
local value = config:read("*line")
config:close()
if value and value ~= "" then
max_connections = tonumber(value) or 0
end
end
local f = io.open("/proc/net/nf_conntrack", "r")
if not f then
return {}, max_connections
end
local count = 0
-- Read connections from nf_conntrack (with early exit if limited)
for line in f:lines() do
-- Early exit if we have a limit and reached it
if max_connections > 0 and count >= max_connections then
break
end
local conn = parse_connection(line)
if conn then
conn.total_bytes = (conn.in_bytes or 0) + (conn.out_bytes or 0)
table.insert(conn_list, conn)
count = count + 1
end
end
f:close()
-- Sort only the connections we actually loaded
table.sort(conn_list, function(a, b)
return a.total_bytes > b.total_bytes
end)
-- Determine max_entries based on system load (only if no manual limit)
local max_entries = #conn_list
if max_connections == 0 then
local load_avg = io.open("/proc/loadavg"):read("*a")
local load1 = tonumber(load_avg:match("^(%d+%.%d+)")) or 0
-- Apply load-based limits only if no manual limit
if load1 > 2.0 then
max_entries = math.min(100, #conn_list)
elseif load1 > 1.5 then
max_entries = math.min(500, #conn_list)
elseif load1 > 0.8 then
max_entries = math.min(1000, #conn_list)
end
end
-- Final connections table
local connections = {}
for i = 1, max_entries do
local conn = conn_list[i]
if not conn then break end
local key = string.format("%s:%s:%s:%s:%s:%s",
conn.layer3, conn.protocol, conn.src, conn.sport, conn.dst, conn.dport)
connections[key] = conn
end
return connections, max_connections
end
-- Define RPC methods
local methods = {
getConntrackDSCP = {
call = function()
local connections, max_connections = get_connections()
return {result = jsonc.stringify({connections = connections, max_connections = max_connections})}
end
}
}
-- Handle RPC calls
if arg[1] == "list" then
local rv = {}
for _, v in pairs(methods) do
rv[_] = v.args or {}
end
print((jsonc.stringify(rv):gsub(":%[%]", ":{}")))
elseif arg[1] == "call" then
local args = jsonc.parse(io.stdin:read("*a"))
local method = methods[arg[2]]
if method then
local result = method.call(args)
print(result.result)
os.exit(result.code or 0)
else
print(jsonc.stringify({error = "Method not found"}))
os.exit(1)
end
end
+491
View File
@@ -0,0 +1,491 @@
#!/bin/sh
# QoSmate Statistics Backend for LuCI
# shellcheck disable=SC2039,SC3043,SC2155,SC3057,SC1091
. /lib/functions.sh
# Set to 1 to enable debug logging, 0 to disable
DEBUG=0
# Debug logging function
debug_log() {
if [ "$DEBUG" -eq 1 ]; then
echo "$1" >> /tmp/qosmate_debug.log
fi
}
# Return a safe unsigned integer for JSON number fields
json_uint_or_zero() {
case "$1" in
''|*[!0-9]*) printf '0' ;;
*) printf '%s' "$1" ;;
esac
}
# Function to safely get JSON from tc command
get_tc_json() {
local output
# Fix HFSC options format by completely replacing the options object
output=$("$@" 2>/dev/null) || echo "[]"
# Check if output is empty
if [ -z "$output" ]; then
echo "[]"
return
fi
# Fix the HFSC options format by replacing the entire options object with a simple default value
echo "$output" | sed 's/\("kind":"hfsc",[^{]*"options":\){[^}]*}/\1{"default":13}/g'
}
# Function to filter statistics to only include QoSmate relevant qdiscs
filter_qosmate_qdiscs() {
local stats="$1"
local wan_interface="$2"
local ifb_interface="$3"
# Filter to only include qdiscs related to QoSmate (on wan_interface and ifb_interface)
echo "$stats" | jq -c "[.[] | select(.dev == \"$wan_interface\" or .dev == \"$ifb_interface\")]" 2>/dev/null || echo "[]"
}
# Function to get the statistics based on root qdisc
get_stats() {
# Get basic statistics for all qdiscs
local basic_stats
basic_stats=$(get_tc_json tc -s -j qdisc show)
debug_log "Basic stats: $basic_stats"
# Get root qdisc type
local root_qdisc
root_qdisc=$(uci -q get qosmate.settings.ROOT_QDISC || echo "hfsc")
# Get interface names
local wan_interface
wan_interface=$(uci -q get qosmate.settings.WAN || echo "eth1")
local ifb_interface
ifb_interface="ifb-$wan_interface"
# Filter basic stats to only include QoSmate relevant qdiscs
local filtered_stats
filtered_stats=$(filter_qosmate_qdiscs "$basic_stats" "$wan_interface" "$ifb_interface")
debug_log "Filtered stats: $filtered_stats"
# Process statistics based on qdisc type
local result
if [ "$root_qdisc" = "cake" ]; then
# Extract CAKE statistics for both egress and ingress
local cake_egress_stats
cake_egress_stats=$(echo "$filtered_stats" | jq -c "[.[] | select(.kind == \"cake\" and .dev == \"$wan_interface\")]" 2>/dev/null || echo "[]")
debug_log "Cake egress stats: $cake_egress_stats"
local cake_ingress_stats
cake_ingress_stats=$(echo "$filtered_stats" | jq -c "[.[] | select(.kind == \"cake\" and .dev == \"$ifb_interface\")]" 2>/dev/null || echo "[]")
debug_log "Cake ingress stats: $cake_ingress_stats"
# Get the priority queue type (diffserv3/4/8)
local priority_queue_ingress
priority_queue_ingress=$(uci -q get qosmate.cake.PRIORITY_QUEUE_INGRESS || echo "diffserv4")
local priority_queue_egress
priority_queue_egress=$(uci -q get qosmate.cake.PRIORITY_QUEUE_EGRESS || echo "diffserv4")
# Extract the actual cake qdisc stats for UI compatibility
local cake_egress
cake_egress=$(echo "$cake_egress_stats" | jq -c '.[0] // {}' 2>/dev/null || echo "{}")
local cake_ingress
cake_ingress=$(echo "$cake_ingress_stats" | jq -c '.[0] // {}' 2>/dev/null || echo "{}")
# Create final JSON output using jq to ensure proper escaping
result=$(jq -n \
--arg rq "$root_qdisc" \
--arg wi "$wan_interface" \
--arg ii "$ifb_interface" \
--arg pqi "$priority_queue_ingress" \
--arg pqe "$priority_queue_egress" \
--argjson fs "$filtered_stats" \
--argjson ce "$cake_egress" \
--argjson ci "$cake_ingress" \
'{
root_qdisc: $rq,
wan_interface: $wi,
ifb_interface: $ii,
priority_queue_ingress: $pqi,
priority_queue_egress: $pqe,
qdisc_stats: $fs,
cake_egress: $ce,
cake_ingress: $ci
}' 2>/dev/null)
elif [ "$root_qdisc" = "hfsc" ] || [ "$root_qdisc" = "hybrid" ]; then
# Extract HFSC root qdiscs and classes (hybrid uses HFSC root)
local hfsc_egress_qdisc
hfsc_egress_qdisc=$(get_tc_json tc -s -j qdisc show dev "$wan_interface" | jq -c "[.[] | select(.kind == \"hfsc\")]" 2>/dev/null || echo "[]")
debug_log "HFSC egress qdisc: $hfsc_egress_qdisc"
local hfsc_egress_classes
hfsc_egress_classes=$(get_tc_json tc -s -j class show dev "$wan_interface")
debug_log "HFSC egress classes: $hfsc_egress_classes"
# Process the output to ensure it's valid JSON
hfsc_egress_classes=$(echo "$hfsc_egress_classes" | jq -c '.' 2>/dev/null || echo "[]")
local hfsc_ingress_qdisc
hfsc_ingress_qdisc=$(get_tc_json tc -s -j qdisc show dev "$ifb_interface" | jq -c "[.[] | select(.kind == \"hfsc\")]" 2>/dev/null || echo "[]")
debug_log "HFSC ingress qdisc: $hfsc_ingress_qdisc"
local hfsc_ingress_classes
hfsc_ingress_classes=$(get_tc_json tc -s -j class show dev "$ifb_interface")
debug_log "HFSC ingress classes: $hfsc_ingress_classes"
# Process the output to ensure it's valid JSON
hfsc_ingress_classes=$(echo "$hfsc_ingress_classes" | jq -c '.' 2>/dev/null || echo "[]")
# Get leaf qdisc statistics
local egress_leaf_qdiscs
egress_leaf_qdiscs=$(get_tc_json tc -s -j qdisc show dev "$wan_interface")
debug_log "Egress leaf qdiscs: $egress_leaf_qdiscs"
# Process the output to ensure it's valid JSON and filter non-HFSC qdiscs
egress_leaf_qdiscs=$(echo "$egress_leaf_qdiscs" | jq -c '[.[] | select(.kind != "hfsc" and .parent != null)]' 2>/dev/null || echo "[]")
local ingress_leaf_qdiscs
ingress_leaf_qdiscs=$(get_tc_json tc -s -j qdisc show dev "$ifb_interface")
debug_log "Ingress leaf qdiscs: $ingress_leaf_qdiscs"
# Process the output to ensure it's valid JSON and filter non-HFSC qdiscs
ingress_leaf_qdiscs=$(echo "$ingress_leaf_qdiscs" | jq -c '[.[] | select(.kind != "hfsc" and .parent != null)]' 2>/dev/null || echo "[]")
# Get game qdisc type (used by both HFSC and hybrid)
local gameqdisc
gameqdisc=$(uci -q get qosmate.hfsc.gameqdisc || echo "pfifo")
# Extract the main HFSC qdisc objects for UI compatibility
local hfsc_egress
hfsc_egress=$(echo "$hfsc_egress_qdisc" | jq -c '.[0] // {}' 2>/dev/null || echo "{}")
local hfsc_ingress
hfsc_ingress=$(echo "$hfsc_ingress_qdisc" | jq -c '.[0] // {}' 2>/dev/null || echo "{}")
# For hybrid mode, also extract CAKE statistics from the 1:13 classes
local hybrid_cake_egress="{}"
local hybrid_cake_ingress="{}"
if [ "$root_qdisc" = "hybrid" ]; then
# Get CAKE stats from the 1:13 parent classes
hybrid_cake_egress=$(echo "$egress_leaf_qdiscs" | jq -c '[.[] | select(.kind == "cake" and (.parent == "1:13" or .parent == "1:13:"))] | .[0] // {}' 2>/dev/null || echo "{}")
hybrid_cake_ingress=$(echo "$ingress_leaf_qdiscs" | jq -c '[.[] | select(.kind == "cake" and (.parent == "1:13" or .parent == "1:13:"))] | .[0] // {}' 2>/dev/null || echo "{}")
debug_log "Hybrid CAKE egress: $hybrid_cake_egress"
debug_log "Hybrid CAKE ingress: $hybrid_cake_ingress"
fi
# Create final JSON output using jq to ensure proper escaping
result=$(jq -n \
--arg rq "$root_qdisc" \
--arg wi "$wan_interface" \
--arg ii "$ifb_interface" \
--arg gq "$gameqdisc" \
--argjson fs "$filtered_stats" \
--argjson he "$hfsc_egress" \
--argjson hi "$hfsc_ingress" \
--argjson hec "$hfsc_egress_classes" \
--argjson hic "$hfsc_ingress_classes" \
--argjson elq "$egress_leaf_qdiscs" \
--argjson ilq "$ingress_leaf_qdiscs" \
--argjson hce "$hybrid_cake_egress" \
--argjson hci "$hybrid_cake_ingress" \
'{
root_qdisc: $rq,
wan_interface: $wi,
ifb_interface: $ii,
gameqdisc: $gq,
qdisc_stats: $fs,
hfsc_egress: $he,
hfsc_ingress: $hi,
hfsc_egress_classes: $hec,
hfsc_ingress_classes: $hic,
egress_leaf_qdiscs: $elq,
ingress_leaf_qdiscs: $ilq,
hybrid_cake_egress: $hce,
hybrid_cake_ingress: $hci
}' 2>/dev/null)
elif [ "$root_qdisc" = "htb" ]; then
local htb_egress_qdisc
htb_egress_qdisc=$(get_tc_json tc -s -j qdisc show dev "$wan_interface" | jq -c "[.[] | select(.kind == \"htb\")]" 2>/dev/null || echo "[]")
debug_log "HTB egress qdisc: $htb_egress_qdisc"
local htb_egress_classes
htb_egress_classes=$(get_tc_json tc -s -j class show dev "$wan_interface")
debug_log "HTB egress classes: $htb_egress_classes"
htb_egress_classes=$(echo "$htb_egress_classes" | jq -c '.' 2>/dev/null || echo "[]")
local htb_ingress_qdisc
htb_ingress_qdisc=$(get_tc_json tc -s -j qdisc show dev "$ifb_interface" | jq -c "[.[] | select(.kind == \"htb\")]" 2>/dev/null || echo "[]")
debug_log "HTB ingress qdisc: $htb_ingress_qdisc"
local htb_ingress_classes
htb_ingress_classes=$(get_tc_json tc -s -j class show dev "$ifb_interface")
debug_log "HTB ingress classes: $htb_ingress_classes"
htb_ingress_classes=$(echo "$htb_ingress_classes" | jq -c '.' 2>/dev/null || echo "[]")
local egress_leaf_qdiscs
egress_leaf_qdiscs=$(get_tc_json tc -s -j qdisc show dev "$wan_interface")
debug_log "Egress leaf qdiscs: $egress_leaf_qdiscs"
egress_leaf_qdiscs=$(echo "$egress_leaf_qdiscs" | jq -c '[.[] | select(.kind != "htb" and .parent != null)]' 2>/dev/null || echo "[]")
local ingress_leaf_qdiscs
ingress_leaf_qdiscs=$(get_tc_json tc -s -j qdisc show dev "$ifb_interface")
debug_log "Ingress leaf qdiscs: $ingress_leaf_qdiscs"
ingress_leaf_qdiscs=$(echo "$ingress_leaf_qdiscs" | jq -c '[.[] | select(.kind != "htb" and .parent != null)]' 2>/dev/null || echo "[]")
local htb_egress
htb_egress=$(echo "$htb_egress_qdisc" | jq -c '.[0] // {}' 2>/dev/null || echo "{}")
local htb_ingress
htb_ingress=$(echo "$htb_ingress_qdisc" | jq -c '.[0] // {}' 2>/dev/null || echo "{}")
result=$(jq -n \
--arg rq "$root_qdisc" \
--arg wi "$wan_interface" \
--arg ii "$ifb_interface" \
--argjson fs "$filtered_stats" \
--argjson he "$htb_egress" \
--argjson hi "$htb_ingress" \
--argjson hec "$htb_egress_classes" \
--argjson hic "$htb_ingress_classes" \
--argjson elq "$egress_leaf_qdiscs" \
--argjson ilq "$ingress_leaf_qdiscs" \
'{
root_qdisc: $rq,
wan_interface: $wi,
ifb_interface: $ii,
qdisc_stats: $fs,
htb_egress: $he,
htb_ingress: $hi,
htb_egress_classes: $hec,
htb_ingress_classes: $hic,
egress_leaf_qdiscs: $elq,
ingress_leaf_qdiscs: $ilq
}' 2>/dev/null)
fi
echo "$result"
}
# Function to get historical statistics
get_historical_stats() {
# Note: This is currently a placeholder function for future implementation
# In the future, this could provide historical QoS statistics from a database or log files
# Currently returns an empty JSON object as no history is being saved
local history_file="/tmp/qosmate_stats_history.json"
if [ -f "$history_file" ]; then
cat "$history_file"
else
echo "{}"
fi
}
# Function to get RRD data if available
get_rrd_data() {
# Note: This is currently a placeholder function for future implementation
# In the future, this could provide Round Robin Database data for generating time-series charts
# Currently only checks if the RRD directory exists but doesn't actually process any data
# Check if RRD data is available for QoS
if [ -d "/tmp/rrd" ]; then
echo "{\"rrd_available\": true}"
else
echo "{\"rrd_available\": false}"
fi
}
# Function to get autorate statistics (current state + config + history)
get_autorate_stats() {
config_load qosmate
local enabled
config_get enabled autorate enabled "0"
if [ "$enabled" != "1" ]; then
echo '{"autorate_enabled":false}'
return
fi
# Read current state from daemon's state file
local current='{"ul_rate":0,"dl_rate":0,"achieved_ul":0,"achieved_dl":0,"latency":0,"baseline":0,"ul_load_pct":0,"dl_load_pct":0}'
if [ -f "/tmp/qosmate-autorate-state" ]; then
local _ar_ul=0 _ar_dl=0 _ar_aul=0 _ar_adl=0 _ar_lat=0 _ar_bl=0 _ar_ulp=0 _ar_dlp=0
local _key _val
while IFS='=' read -r _key _val; do
case "$_key" in
ul_rate) _ar_ul="$_val" ;;
dl_rate) _ar_dl="$_val" ;;
achieved_ul) _ar_aul="$_val" ;;
achieved_dl) _ar_adl="$_val" ;;
latency) _ar_lat="$_val" ;;
baseline) _ar_bl="$_val" ;;
ul_load_pct) _ar_ulp="$_val" ;;
dl_load_pct) _ar_dlp="$_val" ;;
esac
done < "/tmp/qosmate-autorate-state"
_ar_ul=$(json_uint_or_zero "$_ar_ul")
_ar_dl=$(json_uint_or_zero "$_ar_dl")
_ar_aul=$(json_uint_or_zero "$_ar_aul")
_ar_adl=$(json_uint_or_zero "$_ar_adl")
_ar_lat=$(json_uint_or_zero "$_ar_lat")
_ar_bl=$(json_uint_or_zero "$_ar_bl")
_ar_ulp=$(json_uint_or_zero "$_ar_ulp")
_ar_dlp=$(json_uint_or_zero "$_ar_dlp")
current=$(printf '{"ul_rate":%s,"dl_rate":%s,"achieved_ul":%s,"achieved_dl":%s,"latency":%s,"baseline":%s,"ul_load_pct":%s,"dl_load_pct":%s}' \
"$_ar_ul" "$_ar_dl" "$_ar_aul" "$_ar_adl" "$_ar_lat" "$_ar_bl" "$_ar_ulp" "$_ar_dlp")
fi
# Read config values (replicate defaults from autorate daemon)
local uprate downrate
config_get uprate settings UPRATE "0"
config_get downrate settings DOWNRATE "0"
local base_ul base_dl min_ul min_dl max_ul max_dl dec_thr inc_thr
config_get base_ul autorate base_ul_rate ""
config_get base_dl autorate base_dl_rate ""
config_get min_ul autorate min_ul_rate ""
config_get min_dl autorate min_dl_rate ""
config_get max_ul autorate max_ul_rate ""
config_get max_dl autorate max_dl_rate ""
config_get dec_thr autorate latency_decrease_threshold "10"
config_get inc_thr autorate latency_increase_threshold "5"
: "${base_ul:=$uprate}"
: "${base_dl:=$downrate}"
: "${min_ul:=$((uprate * 25 / 100))}"
: "${min_dl:=$((downrate * 25 / 100))}"
: "${max_ul:=$((uprate * 105 / 100))}"
: "${max_dl:=$((downrate * 105 / 100))}"
# Convert thresholds to tenths of ms (matching daemon's internal format)
dec_thr=$((dec_thr * 10))
inc_thr=$((inc_thr * 10))
base_ul=$(json_uint_or_zero "$base_ul")
base_dl=$(json_uint_or_zero "$base_dl")
min_ul=$(json_uint_or_zero "$min_ul")
min_dl=$(json_uint_or_zero "$min_dl")
max_ul=$(json_uint_or_zero "$max_ul")
max_dl=$(json_uint_or_zero "$max_dl")
dec_thr=$(json_uint_or_zero "$dec_thr")
inc_thr=$(json_uint_or_zero "$inc_thr")
local config
config=$(printf '{"base_ul":%s,"base_dl":%s,"min_ul":%s,"min_dl":%s,"max_ul":%s,"max_dl":%s,"decrease_threshold":%s,"increase_threshold":%s}' \
"$base_ul" "$base_dl" "$min_ul" "$min_dl" "$max_ul" "$max_dl" "$dec_thr" "$inc_thr")
# Convert RAM CSV history to compact JSON arrays (60 min, 2s resolution)
# Format: [[ts,ul,dl,a_ul,a_dl,lat,bl,ulp,dlp], ...]
local history="[]"
if [ -f "/tmp/qosmate-autorate-history" ] && [ -s "/tmp/qosmate-autorate-history" ]; then
history=$(awk -F',' '{
for(i=1; i<=9; i++) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $i)
if($i !~ /^[0-9]+$/) $i=0
}
if(NR>1) printf ","
printf "[%s,%s,%s,%s,%s,%s,%s,%s,%s]",$1,$2,$3,$4,$5,$6,$7,$8,$9
}' "/tmp/qosmate-autorate-history")
history="[${history}]"
fi
# Convert Flash CSV history to JSON (30 days, 10-min bucket summaries)
# Format: [[ts,avg_ul,avg_dl,avg_a_ul,avg_a_dl,avg_lat,max_lat,avg_bl,avg_ulp,avg_dlp], ...]
local flash_history="[]"
if [ -f "/etc/qosmate.d/autorate_hourly.csv" ] && [ -s "/etc/qosmate.d/autorate_hourly.csv" ]; then
flash_history=$(awk -F',' '{
for(i=1; i<=10; i++) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $i)
if($i !~ /^[0-9]+$/) $i=0
}
if(NR>1) printf ","
printf "[%s,%s,%s,%s,%s,%s,%s,%s,%s,%s]",$1,$2,$3,$4,$5,$6,$7,$8,$9,$10
}' "/etc/qosmate.d/autorate_hourly.csv")
flash_history="[${flash_history}]"
fi
printf '{"autorate_enabled":true,"current":%s,"config":%s,"history":%s,"flash_history":%s}\n' \
"$current" "$config" "$history" "$flash_history"
}
# Function to get nftables rule counters for active rules
get_rule_counters() {
local nft_output rules_json
nft_output=$(nft -j list table inet dscptag 2>/dev/null || echo '{"nftables":[]}')
# Parse and aggregate rule counters by name (combine IPv4/IPv6)
rules_json=$(echo "$nft_output" | jq -c '
[.nftables[] |
select(.rule != null and .rule.table == "dscptag") |
select(.rule.comment != null) |
select(.rule.comment | startswith("ipv4_") or startswith("ipv6_")) |
select(.rule.expr[]?.counter != null) |
.rule |
{
rule_name: (if .comment | startswith("ipv4_") then .comment[5:] elif .comment | startswith("ipv6_") then .comment[5:] else .comment end),
total_packets: (.expr[] | select(.counter != null) | .counter.packets // 0),
total_bytes: (.expr[] | select(.counter != null) | .counter.bytes // 0),
ipv4_packets: (if .comment | startswith("ipv4_") then (.expr[] | select(.counter != null) | .counter.packets // 0) else 0 end),
ipv6_packets: (if .comment | startswith("ipv6_") then (.expr[] | select(.counter != null) | .counter.packets // 0) else 0 end)
}] |
group_by(.rule_name) |
map({
rule_name: .[0].rule_name,
total_packets: (map(.total_packets) | add),
total_bytes: (map(.total_bytes) | add),
ipv4_packets: (map(.ipv4_packets) | add // 0),
ipv6_packets: (map(.ipv6_packets) | add // 0)
})
' 2>/dev/null || echo "[]")
jq -n --argjson rules "$rules_json" '{rule_counters: $rules}' 2>/dev/null || echo '{"rule_counters":[]}'
}
# Function to handle the call command
handle_call() {
local method="$1"
# Read and discard input JSON (unused for current methods)
cat > /dev/null
# Default to getStats if no method specified
[ -z "$method" ] && method="getStats"
# Process based on method
case "$method" in
getStats)
get_stats
;;
getHistoricalStats)
get_historical_stats
;;
getRrdData)
get_rrd_data
;;
getRuleCounters)
get_rule_counters
;;
getAutorateStats)
get_autorate_stats
;;
*)
echo '{"error": "Method not found"}'
;;
esac
}
# Main function to handle RPC calls
case "$1" in
list)
# List available methods
echo '{"getStats": {}, "getHistoricalStats": {}, "getRrdData": {}, "getRuleCounters": {}, "getAutorateStats": {}}'
;;
call)
handle_call "$2"
;;
*)
echo '{"error": "Invalid call"}'
;;
esac