CI / changes (push) Successful in 11s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 29s
CI / web (push) Successful in 1m6s
CI / go (push) Successful in 1m23s
CI / bird2 (push) Successful in 17s
CI / release (push) Successful in 4m42s
Enhanced the firewall client functionality by introducing packet statistics tracking, including the cumulative count of packets dropped and accepted. Updated the API to support these new fields and modified the database schema accordingly. Improved the firewall scripts to collect and report packet statistics, ensuring better visibility into client performance. Adjusted the UI components to display packet counts in the clients table, enhancing user experience and monitoring capabilities.
366 lines
10 KiB
Bash
366 lines
10 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
CONF_FILE=/etc/evobgp/firewall.conf
|
|
LOG_FILE=/var/log/evobgp-firewall.log
|
|
STATE_DIR=/var/lib/evobgp-firewall
|
|
HASH_FILE="${STATE_DIR}/last_hash"
|
|
PREFIX_FILE="${STATE_DIR}/last_prefixes.txt"
|
|
|
|
log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) $*" | tee -a "$LOG_FILE"; }
|
|
|
|
if [[ ! -f "$CONF_FILE" ]]; then
|
|
log "missing $CONF_FILE"
|
|
exit 1
|
|
fi
|
|
# shellcheck disable=SC1090
|
|
source "$CONF_FILE"
|
|
|
|
: "${EVOBGP_CP_URL:?}"
|
|
: "${CLIENT_TOKEN:?}"
|
|
CLIENT_TOKEN="${CLIENT_TOKEN//$'\r'/}"
|
|
CLIENT_TOKEN="${CLIENT_TOKEN//$'\n'/}"
|
|
|
|
mkdir -p "$STATE_DIR"
|
|
BACKEND="${KERNEL_BACKEND:-auto}"
|
|
|
|
curl_get_blocklist_file() {
|
|
local url="$1"
|
|
local dest="$2"
|
|
local code
|
|
code=$(curl -sS -o "$dest" -w "%{http_code}" \
|
|
-H "Authorization: Bearer ${CLIENT_TOKEN}" \
|
|
-H "Accept: application/json" \
|
|
"${url}/v1/firewall/blocklist") || return 1
|
|
if [[ "$code" == "403" ]]; then
|
|
log "pending approval"
|
|
return 2
|
|
fi
|
|
if [[ "$code" != "200" ]]; then
|
|
log "blocklist HTTP $code from $url"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
try_fetch_blocklist() {
|
|
local urls=()
|
|
if [[ -n "${EVOBGP_FAILOVER_URLS:-}" ]]; then
|
|
IFS=',' read -r -a urls <<<"$EVOBGP_FAILOVER_URLS"
|
|
else
|
|
urls=("${EVOBGP_CP_URL%/}")
|
|
fi
|
|
local u
|
|
for u in "${urls[@]}"; do
|
|
u="${u// /}"
|
|
u="${u%/}"
|
|
local rc=0
|
|
curl_get_blocklist_file "$u" "$PREFIX_FILE" || rc=$?
|
|
if [[ "$rc" == 2 ]]; then
|
|
exit 0
|
|
fi
|
|
if [[ "$rc" == 0 ]]; then
|
|
CP_HIT="$u"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
parse_blocklist_file() {
|
|
local f="$1"
|
|
if [[ ! -s "$f" ]]; then
|
|
log "blocklist file empty: $f"
|
|
return 1
|
|
fi
|
|
if command -v jq >/dev/null 2>&1; then
|
|
HASH=$(jq -r '.hash // empty' "$f")
|
|
TOTAL=$(jq -r '.total // 0' "$f")
|
|
mapfile -t PREFIXES < <(jq -r '.prefixes[]? // empty' "$f")
|
|
return 0
|
|
fi
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
local parsed
|
|
parsed=$(python3 - "$f" <<'PY'
|
|
import json, sys
|
|
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
data = json.load(fh)
|
|
print(data.get("hash") or "")
|
|
print(data.get("total") or 0)
|
|
for p in data.get("prefixes") or []:
|
|
if p:
|
|
print(p)
|
|
PY
|
|
)
|
|
HASH=$(echo "$parsed" | sed -n '1p')
|
|
TOTAL=$(echo "$parsed" | sed -n '2p')
|
|
mapfile -t PREFIXES < <(echo "$parsed" | sed -n '3,$p')
|
|
return 0
|
|
fi
|
|
HASH=$(grep -o '"hash"[[:space:]]*:[[:space:]]*"[^"]*"' "$f" | head -1 | sed 's/.*"\(sha256:[^"]*\)".*/\1/')
|
|
TOTAL=$(grep -o '"total"[[:space:]]*:[[:space:]]*[0-9]*' "$f" | head -1 | grep -o '[0-9]*$' || true)
|
|
mapfile -t PREFIXES < <(grep -oE '"[0-9]+(\.[0-9]+){3}/[0-9]+"' "$f" | tr -d '"' || true)
|
|
return 0
|
|
}
|
|
|
|
nft_join_elements() {
|
|
local out="" p
|
|
for p in "$@"; do
|
|
if [[ -n "$out" ]]; then
|
|
out+=", "
|
|
fi
|
|
out+="$p"
|
|
done
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
nft_add_v4_chunk() {
|
|
local table=$1 name=$2
|
|
shift 2
|
|
local joined
|
|
joined=$(nft_join_elements "$@")
|
|
if nft add element "$table" "$name" v4 "{ ${joined} }" 2>>"$LOG_FILE"; then
|
|
return 0
|
|
fi
|
|
log "nft batch add failed (chunk=$#), retrying one-by-one"
|
|
local p ok=0
|
|
for p in "$@"; do
|
|
if nft add element "$table" "$name" v4 "{ $p }" 2>>"$LOG_FILE"; then
|
|
ok=$((ok + 1))
|
|
fi
|
|
done
|
|
[[ "$ok" -gt 0 ]]
|
|
}
|
|
|
|
if ! try_fetch_blocklist; then
|
|
log "all endpoints failed"
|
|
exit 1
|
|
fi
|
|
|
|
HASH=""
|
|
TOTAL=0
|
|
PREFIXES=()
|
|
parse_blocklist_file "$PREFIX_FILE"
|
|
log "blocklist bytes=$(wc -c <"$PREFIX_FILE" | tr -d ' ') parsed=${#PREFIXES[@]} api_total=${TOTAL:-0}"
|
|
|
|
if [[ -z "${TOTAL// }" ]]; then
|
|
TOTAL=${#PREFIXES[@]}
|
|
fi
|
|
|
|
PACKETS_DROPPED=0
|
|
PACKETS_ACCEPTED=0
|
|
KERNEL_METHOD=""
|
|
APPLIED_V4=0
|
|
|
|
count_ipv4_prefixes() {
|
|
local n=0 p
|
|
for p in "${PREFIXES[@]}"; do
|
|
[[ "$p" == *:* ]] && continue
|
|
n=$((n + 1))
|
|
done
|
|
APPLIED_V4=$n
|
|
}
|
|
|
|
nft_rule_packets() {
|
|
local line=$1
|
|
if [[ "$line" =~ counter[[:space:]]+packets[[:space:]]+([0-9]+) ]]; then
|
|
echo "${BASH_REMATCH[1]}"
|
|
else
|
|
echo 0
|
|
fi
|
|
}
|
|
|
|
ensure_nft_counters() {
|
|
local table=inet name=evobgp_blocklist
|
|
nft list chain "$table" "$name" input >/dev/null 2>&1 || return 0
|
|
local drop_line
|
|
drop_line=$(nft -a list chain "$table" "$name" input 2>/dev/null | grep 'ip saddr @v4' | grep drop | head -1 || true)
|
|
if [[ -n "$drop_line" && "$drop_line" != *counter* ]]; then
|
|
local handle
|
|
handle=$(echo "$drop_line" | sed -n 's/.*# handle \([0-9]\+\).*/\1/p')
|
|
if [[ -n "$handle" ]]; then
|
|
nft delete rule "$table" "$name" input handle "$handle" 2>>"$LOG_FILE" || true
|
|
drop_line=""
|
|
fi
|
|
fi
|
|
if [[ -z "$drop_line" ]]; then
|
|
nft add rule "$table" "$name" input ip saddr @v4 counter drop
|
|
fi
|
|
if ! nft list chain "$table" "$name" input 2>/dev/null | grep -qE '[[:space:]]counter[[:space:]]+accept'; then
|
|
nft add rule "$table" "$name" input counter accept
|
|
fi
|
|
}
|
|
|
|
collect_nft_packet_stats() {
|
|
PACKETS_DROPPED=0
|
|
PACKETS_ACCEPTED=0
|
|
local line pkts
|
|
while IFS= read -r line; do
|
|
if [[ "$line" == *"ip saddr @v4"* && "$line" == *drop* ]]; then
|
|
pkts=$(nft_rule_packets "$line")
|
|
[[ -n "$pkts" ]] && PACKETS_DROPPED=$pkts
|
|
elif [[ "$line" == *counter* && "$line" == *accept* && "$line" != *@v4* ]]; then
|
|
pkts=$(nft_rule_packets "$line")
|
|
[[ -n "$pkts" ]] && PACKETS_ACCEPTED=$pkts
|
|
fi
|
|
done < <(nft list chain inet evobgp_blocklist input 2>/dev/null || true)
|
|
}
|
|
|
|
collect_ipset_packet_stats() {
|
|
PACKETS_DROPPED=0
|
|
PACKETS_ACCEPTED=0
|
|
local pkts
|
|
pkts=$(iptables -L INPUT -v -n -x 2>/dev/null | awk '/match-set evobgp_blocklist_v4/ {print $1; exit}')
|
|
[[ "$pkts" =~ ^[0-9]+$ ]] && PACKETS_DROPPED=$pkts
|
|
}
|
|
|
|
collect_packet_stats() {
|
|
case "${KERNEL_METHOD:-$BACKEND}" in
|
|
nft)
|
|
ensure_nft_counters
|
|
collect_nft_packet_stats
|
|
;;
|
|
ipset)
|
|
collect_ipset_packet_stats
|
|
;;
|
|
iptables)
|
|
PACKETS_DROPPED=$(iptables -L INPUT -v -n -x 2>/dev/null | awk '/DROP/ {s+=$1} END {print s+0}')
|
|
PACKETS_ACCEPTED=0
|
|
;;
|
|
*)
|
|
if command -v nft >/dev/null 2>&1 && nft list chain inet evobgp_blocklist input >/dev/null 2>&1; then
|
|
KERNEL_METHOD=nft
|
|
ensure_nft_counters
|
|
collect_nft_packet_stats
|
|
elif iptables -L INPUT -v -n -x 2>/dev/null | grep -q 'evobgp_blocklist_v4'; then
|
|
KERNEL_METHOD=ipset
|
|
collect_ipset_packet_stats
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
send_client_reports() {
|
|
collect_packet_stats
|
|
local km="${KERNEL_METHOD:-$BACKEND}"
|
|
local report
|
|
report=$(printf '{"status":"ok","prefix_count":%s,"ip_count":%s,"packets_dropped":%s,"packets_accepted":%s,"source":"cp","kernel_method":"%s"}' \
|
|
"${TOTAL:-0}" "${APPLIED_V4:-0}" "${PACKETS_DROPPED:-0}" "${PACKETS_ACCEPTED:-0}" "$km")
|
|
curl -fsS -X POST "${EVOBGP_CP_URL%/}/v1/firewall/apply-report" \
|
|
-H "Authorization: Bearer ${CLIENT_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$report" >/dev/null 2>&1 || true
|
|
curl -fsS -X POST "${EVOBGP_CP_URL%/}/v1/firewall/heartbeat" \
|
|
-H "Authorization: Bearer ${CLIENT_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"source":"cp"}' >/dev/null 2>&1 || true
|
|
}
|
|
|
|
if [[ -f "$HASH_FILE" && "$(tr -d '\r\n' <"$HASH_FILE")" == "$HASH" && -n "$HASH" ]]; then
|
|
count_ipv4_prefixes
|
|
log "unchanged hash $HASH — skip kernel apply (ipv4=${APPLIED_V4})"
|
|
send_client_reports
|
|
exit 0
|
|
fi
|
|
|
|
apply_nft() {
|
|
local table=inet
|
|
local name=evobgp_blocklist
|
|
local v4=()
|
|
local p
|
|
for p in "${PREFIXES[@]}"; do
|
|
[[ "$p" == *:* ]] && continue
|
|
v4+=("$p")
|
|
done
|
|
|
|
nft list table "$table" "$name" >/dev/null 2>&1 || nft add table "$table" "$name"
|
|
nft list set "$table" "$name" v4 >/dev/null 2>&1 || \
|
|
nft add set "$table" "$name" v4 '{ type ipv4_addr; flags interval; }'
|
|
nft flush set "$table" "$name" v4
|
|
|
|
if ((${#v4[@]})); then
|
|
local batch=()
|
|
local chunk=64
|
|
for p in "${v4[@]}"; do
|
|
batch+=("$p")
|
|
if ((${#batch[@]} >= chunk)); then
|
|
nft_add_v4_chunk "$table" "$name" "${batch[@]}" || log "nft chunk add partial failure"
|
|
batch=()
|
|
fi
|
|
done
|
|
if ((${#batch[@]})); then
|
|
nft_add_v4_chunk "$table" "$name" "${batch[@]}" || log "nft tail chunk add partial failure"
|
|
fi
|
|
fi
|
|
|
|
nft list chain "$table" "$name" input >/dev/null 2>&1 || {
|
|
nft add chain "$table" "$name" input '{ type filter hook input priority 0; policy accept; }'
|
|
nft add rule "$table" "$name" input ip saddr @v4 counter drop
|
|
nft add rule "$table" "$name" input counter accept
|
|
}
|
|
ensure_nft_counters
|
|
KERNEL_METHOD=nft
|
|
APPLIED_V4=${#v4[@]}
|
|
}
|
|
|
|
apply_ipset() {
|
|
local set=evobgp_blocklist_v4
|
|
local n=0
|
|
ipset list "$set" >/dev/null 2>&1 || ipset create "$set" hash:net family inet hashsize 4096 maxelem 1048576
|
|
ipset flush "$set"
|
|
local p
|
|
for p in "${PREFIXES[@]}"; do
|
|
[[ "$p" == *:* ]] && continue
|
|
ipset add "$set" "$p" -exist
|
|
n=$((n + 1))
|
|
done
|
|
iptables -C INPUT -m set --match-set "$set" src -j DROP 2>/dev/null || \
|
|
iptables -I INPUT -m set --match-set "$set" src -j DROP
|
|
KERNEL_METHOD=ipset
|
|
APPLIED_V4=$n
|
|
}
|
|
|
|
apply_iptables_only() {
|
|
iptables -D INPUT -m comment --comment evobgp-block -j DROP 2>/dev/null || true
|
|
local n=0
|
|
local p
|
|
for p in "${PREFIXES[@]}"; do
|
|
[[ "$p" == *:* ]] && continue
|
|
iptables -C INPUT -s "$p" -j DROP 2>/dev/null || iptables -A INPUT -s "$p" -j DROP
|
|
n=$((n + 1))
|
|
done
|
|
KERNEL_METHOD=iptables
|
|
APPLIED_V4=$n
|
|
}
|
|
|
|
clear_block() {
|
|
case "$BACKEND" in
|
|
nft) nft delete table inet evobgp_blocklist 2>/dev/null || true ;;
|
|
ipset)
|
|
ipset destroy evobgp_blocklist_v4 2>/dev/null || true
|
|
iptables -D INPUT -m set --match-set evobgp_blocklist_v4 src -j DROP 2>/dev/null || true
|
|
;;
|
|
iptables) iptables -S INPUT | grep -i evobgp | sed 's/^-A /-D /' | while read -r line; do iptables $line 2>/dev/null || true; done ;;
|
|
esac
|
|
APPLIED_V4=0
|
|
KERNEL_METHOD="${BACKEND:-auto}"
|
|
}
|
|
|
|
APPLIED_V4=0
|
|
KERNEL_METHOD=""
|
|
if [[ "${TOTAL:-0}" == "0" || ${#PREFIXES[@]} -eq 0 ]]; then
|
|
clear_block
|
|
log "cleared blocklist (api total=${TOTAL:-0}) backend=$BACKEND"
|
|
else
|
|
case "$BACKEND" in
|
|
nft|auto) if command -v nft >/dev/null 2>&1; then apply_nft; else apply_ipset; fi ;;
|
|
ipset) apply_ipset ;;
|
|
iptables) apply_iptables_only ;;
|
|
*) apply_ipset ;;
|
|
esac
|
|
log "applied api_total=${TOTAL} ipv4_in_kernel=${APPLIED_V4} from ${CP_HIT:-$EVOBGP_CP_URL} backend=$BACKEND hash=${HASH:-empty}"
|
|
fi
|
|
|
|
echo "$HASH" >"$HASH_FILE"
|
|
send_client_reports
|