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
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.
258 lines
6.9 KiB
Bash
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
|