feat(api, web): implement per-IP blocked stats for agents
- Added functionality to report per-IP drop counters in the `evofw-firewall.sh` script, capturing the top 200 IPs with packet counts. - Introduced new API endpoints to retrieve blocked IP statistics and reset these stats for agents, enhancing monitoring capabilities. - Updated the agent detail view to display blocked IPs, improving user visibility into agent performance. - Enhanced database schema and repositories to support the storage and management of IP block statistics. These changes provide a comprehensive view of blocked IPs, improving the overall management and monitoring of agents.
This commit is contained in:
@@ -7,6 +7,7 @@ LOG_FILE=/var/log/evofw-firewall.log
|
||||
STATE_DIR=/var/lib/evofw
|
||||
HASH_FILE="${STATE_DIR}/last_hash"
|
||||
POLICY_FILE="${STATE_DIR}/last_policy.json"
|
||||
IP_HITS_TOP=200
|
||||
|
||||
log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) $*" | tee -a "$LOG_FILE"; }
|
||||
|
||||
@@ -97,6 +98,7 @@ PACKETS_DROPPED=0
|
||||
PACKETS_ACCEPTED=0
|
||||
KERNEL_METHOD=""
|
||||
APPLIED=0
|
||||
IP_HITS_JSON="[]"
|
||||
|
||||
nft_join() {
|
||||
local out="" p
|
||||
@@ -116,6 +118,20 @@ nft_add_chunk() {
|
||||
}
|
||||
}
|
||||
|
||||
# Ensure inet set exists with interval + counter (recreate if missing counter).
|
||||
ensure_nft_set() {
|
||||
local table=$1 name=$2 setname=$3
|
||||
local def
|
||||
def=$(nft -a list set "$table" "$name" "$setname" 2>/dev/null || true)
|
||||
if [[ -n "$def" ]] && [[ "$def" == *"counter"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ -n "$def" ]]; then
|
||||
nft delete set "$table" "$name" "$setname" 2>>"$LOG_FILE" || true
|
||||
fi
|
||||
nft add set "$table" "$name" "$setname" '{ type ipv4_addr; flags interval; counter; }' 2>>"$LOG_FILE"
|
||||
}
|
||||
|
||||
collect_nft_stats() {
|
||||
PACKETS_DROPPED=0; PACKETS_ACCEPTED=0
|
||||
local line n
|
||||
@@ -135,6 +151,62 @@ collect_nft_stats() {
|
||||
done < <(nft list chain inet evofw input 2>/dev/null || true)
|
||||
}
|
||||
|
||||
# Parse nft set / ipset listing → top-N JSON [{"ip":"...","packets":N},...]
|
||||
build_ip_hits_json() {
|
||||
local text="$1"
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
IP_HITS_JSON=$(IP_HITS_TOP="$IP_HITS_TOP" python3 -c '
|
||||
import json, os, re, sys
|
||||
text = sys.stdin.read()
|
||||
top = int(os.environ.get("IP_HITS_TOP", "200"))
|
||||
hits = {}
|
||||
# nft: "1.2.3.4 counter packets 10 bytes 100" or "1.2.3.0/24 packets 5 bytes 20"
|
||||
for m in re.finditer(r"([0-9]{1,3}(?:\.[0-9]{1,3}){3}(?:/[0-9]{1,2})?)\s+(?:counter\s+)?packets\s+(\d+)", text):
|
||||
ip, pkts = m.group(1), int(m.group(2))
|
||||
if pkts > 0:
|
||||
hits[ip] = max(hits.get(ip, 0), pkts)
|
||||
# ipset list Members: "1.2.3.4 packets 10 bytes 100"
|
||||
for m in re.finditer(r"^([0-9]{1,3}(?:\.[0-9]{1,3}){3}(?:/[0-9]{1,2})?)\s+packets\s+(\d+)", text, re.M):
|
||||
ip, pkts = m.group(1), int(m.group(2))
|
||||
if pkts > 0:
|
||||
hits[ip] = max(hits.get(ip, 0), pkts)
|
||||
items = [{"ip": k, "packets": v} for k, v in hits.items()]
|
||||
items.sort(key=lambda x: x["packets"], reverse=True)
|
||||
print(json.dumps(items[:top], separators=(",", ":")))
|
||||
' <<<"$text" 2>/dev/null) || IP_HITS_JSON="[]"
|
||||
return
|
||||
fi
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
# Fallback without python: empty (jq alone cannot easily top-N from free text)
|
||||
IP_HITS_JSON="[]"
|
||||
return
|
||||
fi
|
||||
IP_HITS_JSON="[]"
|
||||
}
|
||||
|
||||
collect_nft_ip_hits() {
|
||||
local text
|
||||
text=$(nft list set inet evofw deny_v4 2>/dev/null || true)
|
||||
build_ip_hits_json "$text"
|
||||
}
|
||||
|
||||
collect_ipset_ip_hits() {
|
||||
local text
|
||||
text=$(ipset list evofw_deny_v4 2>/dev/null || true)
|
||||
build_ip_hits_json "$text"
|
||||
}
|
||||
|
||||
collect_ip_hits() {
|
||||
IP_HITS_JSON="[]"
|
||||
if [[ "${KERNEL_METHOD:-}" == "nft" ]] || { [[ -z "${KERNEL_METHOD:-}" || "${KERNEL_METHOD:-}" == "auto" ]] && command -v nft >/dev/null 2>&1 && nft list set inet evofw deny_v4 >/dev/null 2>&1; }; then
|
||||
collect_nft_ip_hits
|
||||
return
|
||||
fi
|
||||
if command -v ipset >/dev/null 2>&1 && ipset list evofw_deny_v4 >/dev/null 2>&1; then
|
||||
collect_ipset_ip_hits
|
||||
fi
|
||||
}
|
||||
|
||||
apply_nft() {
|
||||
local table=inet name=evofw
|
||||
local deny_v4=() allow_v4=() p
|
||||
@@ -142,10 +214,8 @@ apply_nft() {
|
||||
for p in "${ALLOW[@]+"${ALLOW[@]}"}"; do [[ "$p" == *:* ]] && continue; allow_v4+=("$p"); done
|
||||
|
||||
nft list table "$table" "$name" >/dev/null 2>&1 || nft add table "$table" "$name"
|
||||
nft list set "$table" "$name" deny_v4 >/dev/null 2>&1 || \
|
||||
nft add set "$table" "$name" deny_v4 '{ type ipv4_addr; flags interval; }'
|
||||
nft list set "$table" "$name" allow_v4 >/dev/null 2>&1 || \
|
||||
nft add set "$table" "$name" allow_v4 '{ type ipv4_addr; flags interval; }'
|
||||
ensure_nft_set "$table" "$name" deny_v4
|
||||
ensure_nft_set "$table" "$name" allow_v4
|
||||
nft flush set "$table" "$name" deny_v4
|
||||
nft flush set "$table" "$name" allow_v4
|
||||
|
||||
@@ -182,10 +252,25 @@ apply_nft() {
|
||||
APPLIED=$((${#deny_v4[@]} + ${#allow_v4[@]}))
|
||||
}
|
||||
|
||||
ensure_ipset_counters() {
|
||||
local name=$1
|
||||
if ! ipset list "$name" >/dev/null 2>&1; then
|
||||
ipset create "$name" hash:net family inet counters
|
||||
return
|
||||
fi
|
||||
# Recreate once if set has no packet counters (Header lacks "counters").
|
||||
local header
|
||||
header=$(ipset list "$name" 2>/dev/null | head -n 5 || true)
|
||||
if [[ "$header" != *"counters"* && "$header" != *"packet"* ]]; then
|
||||
ipset destroy "$name" 2>>"$LOG_FILE" || true
|
||||
ipset create "$name" hash:net family inet counters
|
||||
fi
|
||||
}
|
||||
|
||||
apply_ipset() {
|
||||
local dset=evofw_deny_v4 aset=evofw_allow_v4
|
||||
ipset list "$dset" >/dev/null 2>&1 || ipset create "$dset" hash:net family inet
|
||||
ipset list "$aset" >/dev/null 2>&1 || ipset create "$aset" hash:net family inet
|
||||
ensure_ipset_counters "$dset"
|
||||
ensure_ipset_counters "$aset"
|
||||
ipset flush "$dset"; ipset flush "$aset"
|
||||
local p n=0
|
||||
for p in "${DENY[@]+"${DENY[@]}"}"; do [[ "$p" == *:* ]] && continue; ipset add "$dset" "$p" -exist; n=$((n+1)); done
|
||||
@@ -210,9 +295,12 @@ send_report() {
|
||||
collect_nft_stats
|
||||
fi
|
||||
fi
|
||||
if [[ -z "${IP_HITS_CAPTURED:-}" ]]; then
|
||||
collect_ip_hits
|
||||
fi
|
||||
local report
|
||||
report=$(printf '{"status":"ok","prefix_count":%s,"packets_dropped":%s,"packets_accepted":%s,"kernel_method":"%s","source":"agent"}' \
|
||||
"${APPLIED:-0}" "${PACKETS_DROPPED:-0}" "${PACKETS_ACCEPTED:-0}" "${KERNEL_METHOD:-$BACKEND}")
|
||||
report=$(printf '{"status":"ok","prefix_count":%s,"packets_dropped":%s,"packets_accepted":%s,"kernel_method":"%s","source":"agent","ip_hits":%s}' \
|
||||
"${APPLIED:-0}" "${PACKETS_DROPPED:-0}" "${PACKETS_ACCEPTED:-0}" "${KERNEL_METHOD:-$BACKEND}" "${IP_HITS_JSON:-[]}")
|
||||
curl -fsS -X POST "${EVOFW_CP_URL%/}/v1/agent/apply-report" \
|
||||
-H "Authorization: Bearer ${CLIENT_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
@@ -225,15 +313,36 @@ send_report() {
|
||||
|
||||
if [[ -f "$HASH_FILE" && "$(tr -d '\r\n' <"$HASH_FILE")" == "$HASH" && -n "$HASH" ]]; then
|
||||
log "unchanged hash $HASH — skip apply"
|
||||
KERNEL_METHOD="${BACKEND}"
|
||||
if command -v nft >/dev/null 2>&1 && nft list table inet evofw >/dev/null 2>&1; then
|
||||
KERNEL_METHOD=nft
|
||||
elif command -v ipset >/dev/null 2>&1 && ipset list evofw_deny_v4 >/dev/null 2>&1; then
|
||||
KERNEL_METHOD=ipset
|
||||
else
|
||||
KERNEL_METHOD="${BACKEND}"
|
||||
fi
|
||||
# Count applied prefixes from live sets when skipping apply.
|
||||
if [[ "$KERNEL_METHOD" == "nft" ]]; then
|
||||
APPLIED=$(nft list set inet evofw deny_v4 2>/dev/null | grep -cE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' || true)
|
||||
local_allow=$(nft list set inet evofw allow_v4 2>/dev/null | grep -cE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' || true)
|
||||
APPLIED=$((APPLIED + local_allow))
|
||||
elif [[ "$KERNEL_METHOD" == "ipset" ]]; then
|
||||
APPLIED=$(ipset list evofw_deny_v4 2>/dev/null | awk '/^[0-9]/{c++} END{print c+0}')
|
||||
local_allow=$(ipset list evofw_allow_v4 2>/dev/null | awk '/^[0-9]/{c++} END{print c+0}')
|
||||
APPLIED=$((APPLIED + local_allow))
|
||||
fi
|
||||
send_report
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Capture counters BEFORE recreate (nft delete chain zeroes them).
|
||||
if command -v nft >/dev/null 2>&1; then
|
||||
# Capture counters BEFORE recreate (nft delete chain / flush set zeroes them).
|
||||
if command -v nft >/dev/null 2>&1 && nft list table inet evofw >/dev/null 2>&1; then
|
||||
collect_nft_stats
|
||||
collect_nft_ip_hits
|
||||
STATS_CAPTURED=1
|
||||
IP_HITS_CAPTURED=1
|
||||
elif command -v ipset >/dev/null 2>&1 && ipset list evofw_deny_v4 >/dev/null 2>&1; then
|
||||
collect_ipset_ip_hits
|
||||
IP_HITS_CAPTURED=1
|
||||
fi
|
||||
|
||||
case "$BACKEND" in
|
||||
|
||||
Reference in New Issue
Block a user