Files
EvoBGP/scripts/firewall/evobgp-firewall.sh
T
Denozordec b7f7669685
CI / changes (push) Successful in 9s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 25s
CI / web (push) Successful in 42s
CI / go (push) Successful in 1m1s
CI / bird2 (push) Successful in 15s
CI / release (push) Successful in 4m17s
feat(firewall): improve blocklist parsing and nft element addition
Enhanced the blocklist parsing function to log when the blocklist file is empty. Introduced new helper functions `nft_join_elements` and `nft_add_v4_chunk` to streamline the addition of elements to the nftables, allowing for batch processing and improved error handling. Adjusted the chunk size for element addition to optimize performance. Updated logging to provide better visibility into the blocklist processing and applied prefixes.
2026-07-08 22:02:59 +07:00

258 lines
6.9 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
if [[ -f "$HASH_FILE" && "$(tr -d '\r\n' <"$HASH_FILE")" == "$HASH" && -n "$HASH" ]]; then
log "unchanged hash $HASH — skip kernel apply"
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 drop
}
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
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
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
}
APPLIED_V4=0
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"
REPORT=$(printf '{"status":"ok","prefix_count":%s,"ip_count":%s,"source":"cp"}' "${TOTAL:-0}" "${APPLIED_V4:-0}")
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