feat(firewall): implement firewall blocklist feature with client management and policy rules
CI / changes (push) Successful in 12s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 25s
CI / web (push) Successful in 46s
CI / go (push) Successful in 1m15s
CI / bird2 (push) Successful in 18s
CI / release (push) Successful in 3m59s

Introduced a comprehensive firewall blocklist feature, allowing for the management of firewall clients and their associated rules. This includes endpoints for enrolling clients, listing clients and rules, and reporting apply statuses. Enhanced the API to support firewall operations, including the ability to handle block/accept policies. Updated the documentation to reflect these changes and added necessary components in the web UI for better user interaction.

Additionally, modified the agent server to support firewall failover and integrated firewall functionality into the existing architecture.
This commit is contained in:
Denozordec
2026-07-08 16:37:27 +07:00
parent 276194a9d0
commit 7a3eae98b1
36 changed files with 4581 additions and 174 deletions
+169
View File
@@ -0,0 +1,169 @@
#!/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"
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:?}"
mkdir -p "$STATE_DIR"
BACKEND="${KERNEL_BACKEND:-auto}"
curl_get_blocklist() {
local url="$1"
local host
host=$(echo "$url" | sed -E 's#https?://([^/]+)/?.*#\1#')
local tmp
tmp=$(mktemp)
local code
code=$(curl -sS -o "$tmp" -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"
rm -f "$tmp"
exit 0
fi
if [[ "$code" != "200" ]]; then
log "blocklist HTTP $code from $url"
rm -f "$tmp"
return 1
fi
cat "$tmp"
rm -f "$tmp"
}
try_urls() {
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%/}"
if OUT=$(curl_get_blocklist "$u"); then
CP_HIT="$u"
return 0
fi
done
return 1
}
if ! OUT=$(try_urls); then
log "all endpoints failed"
exit 1
fi
if command -v jq >/dev/null 2>&1; then
HASH=$(echo "$OUT" | jq -r '.hash // empty')
TOTAL=$(echo "$OUT" | jq -r '.total // 0')
mapfile -t PREFIXES < <(echo "$OUT" | jq -r '.prefixes[]?')
else
HASH=$(echo "$OUT" | grep -o '"hash"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\(sha256:[^"]*\)".*/\1/')
TOTAL=$(echo "$OUT" | grep -o '"total"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | grep -o '[0-9]*$')
mapfile -t PREFIXES < <(echo "$OUT" | grep -o '"[0-9a-fA-F:.]*/[0-9]*"' | tr -d '"')
fi
if [[ -f "$HASH_FILE" && "$(cat "$HASH_FILE")" == "$HASH" ]]; then
log "unchanged hash $HASH — skip kernel apply"
exit 0
fi
apply_nft() {
local table=inet
local name=evobgp_blocklist
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 ((${#PREFIXES[@]})); then
local v4=()
local p
for p in "${PREFIXES[@]}"; do
[[ "$p" == *:* ]] && continue
v4+=("$p")
done
if ((${#v4[@]})); then
nft add element "$table" "$name" v4 "{ $(IFS=,; echo "${v4[*]}") }"
fi
fi
nft list chain "$table" "$name" input >/dev/null 2>&1 || {
nft add chain "$table" "$name" input '{ type filter hook input priority 0; }'
nft add rule "$table" "$name" input ip saddr @v4 drop
}
}
apply_ipset() {
local set=evobgp_blocklist_v4
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
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
}
apply_iptables_only() {
iptables -D INPUT -m comment --comment evobgp-block -j DROP 2>/dev/null || true
if ((${#PREFIXES[@]})); then
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
done
fi
}
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
}
if [[ "$TOTAL" == "0" || ${#PREFIXES[@]} -eq 0 ]]; then
clear_block
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
fi
echo "$HASH" >"$HASH_FILE"
log "applied $TOTAL prefixes from ${CP_HIT:-$EVOBGP_CP_URL} backend=$BACKEND"
REPORT=$(printf '{"status":"ok","prefix_count":%s,"ip_count":0,"source":"cp"}' "${TOTAL:-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
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
echo "evobgp-firewall install: run as root" >&2
exit 1
fi
for cmd in curl bash; do
command -v "$cmd" >/dev/null 2>&1 || { echo "missing $cmd" >&2; exit 1; }
done
: "${EVOBGP_CP_URL:?EVOBGP_CP_URL required}"
: "${EVOBGP_SEED:?EVOBGP_SEED required}"
: "${EVOBGP_CLIENT_NAME:?EVOBGP_CLIENT_NAME required}"
CONF_DIR=/etc/evobgp
CONF_FILE="${CONF_DIR}/firewall.conf"
SYNC_SCRIPT=/usr/local/sbin/evobgp-firewall.sh
if [[ -f "$CONF_FILE" && "${EVOBGP_INSTALL_FORCE:-}" != "1" ]]; then
echo "Already installed ($CONF_FILE). Set EVOBGP_INSTALL_FORCE=1 to reinstall." >&2
exit 1
fi
gen_token() {
if command -v openssl >/dev/null 2>&1; then
echo -n "evobgp_fw_$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')"
else
echo -n "evobgp_fw_$(head -c 32 /dev/urandom | base64 | tr '+/' '-_' | tr -d '=\n')"
fi
}
CLIENT_TOKEN="$(gen_token)"
HOSTNAME="$(hostname -f 2>/dev/null || hostname)"
CP_URL="${EVOBGP_CP_URL%/}"
ENROLL_BODY=$(printf '{"name":"%s","hostname":"%s","client_token":"%s","client_version":"install.sh/1"}' \
"$EVOBGP_CLIENT_NAME" "$HOSTNAME" "$CLIENT_TOKEN")
RESP=$(curl -fsS -X POST "${CP_URL}/v1/firewall/enroll" \
-H "Content-Type: application/json" \
-H "X-EvoBGP-Seed: ${EVOBGP_SEED}" \
-d "$ENROLL_BODY")
CLIENT_ID=""
if command -v jq >/dev/null 2>&1; then
CLIENT_ID=$(echo "$RESP" | jq -r '.client_id')
else
CLIENT_ID=$(echo "$RESP" | sed -n 's/.*"client_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
fi
mkdir -p "$CONF_DIR"
chmod 700 "$CONF_DIR"
cat >"$CONF_FILE" <<EOF
EVOBGP_CP_URL=${CP_URL}
CLIENT_ID=${CLIENT_ID}
CLIENT_TOKEN=${CLIENT_TOKEN}
CLIENT_NAME=${EVOBGP_CLIENT_NAME}
KERNEL_BACKEND=auto
EOF
chmod 600 "$CONF_FILE"
curl -fsSL "${CP_URL}/v1/firewall/sync-script" -o "$SYNC_SCRIPT"
chmod 755 "$SYNC_SCRIPT"
if command -v nft >/dev/null 2>&1; then
BACKEND=nft
elif command -v ipset >/dev/null 2>&1 && command -v iptables >/dev/null 2>&1; then
BACKEND=ipset
elif command -v iptables >/dev/null 2>&1; then
BACKEND=iptables
else
echo "no supported firewall backend (nft/ipset/iptables)" >&2
exit 1
fi
sed -i "s/^KERNEL_BACKEND=.*/KERNEL_BACKEND=${BACKEND}/" "$CONF_FILE" 2>/dev/null || \
echo "KERNEL_BACKEND=${BACKEND}" >>"$CONF_FILE"
INTERVAL="${EVOBGP_SYNC_INTERVAL:-5min}"
if command -v systemctl >/dev/null 2>&1; then
cat >/etc/systemd/system/evobgp-firewall.service <<'UNIT'
[Unit]
Description=EvoBGP firewall blocklist sync
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/evobgp-firewall.sh
UNIT
cat >/etc/systemd/system/evobgp-firewall.timer <<UNIT
[Unit]
Description=EvoBGP firewall sync timer
[Timer]
OnBootSec=2min
OnUnitActiveSec=${INTERVAL}
Unit=evobgp-firewall.service
[Install]
WantedBy=timers.target
UNIT
systemctl daemon-reload
systemctl enable --now evobgp-firewall.timer
else
echo "*/5 * * * * root ${SYNC_SCRIPT}" >/etc/cron.d/evobgp-firewall
fi
echo "Client ID: ${CLIENT_ID}"
echo "Status: pending — approve in EvoBGP UI → Firewall → Запросы"
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
systemctl disable --now evobgp-firewall.timer 2>/dev/null || true
rm -f /etc/cron.d/evobgp-firewall
rm -f /etc/systemd/system/evobgp-firewall.service /etc/systemd/system/evobgp-firewall.timer
systemctl daemon-reload 2>/dev/null || true
nft delete table inet evobgp_blocklist 2>/dev/null || true
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
rm -f /usr/local/sbin/evobgp-firewall.sh /usr/local/sbin/evobgp-firewall-uninstall.sh
rm -rf /var/lib/evobgp-firewall
if [[ "${EVOBGP_UNINSTALL_REMOVE_CONF:-}" == "1" ]]; then
rm -f /etc/evobgp/firewall.conf
fi
echo "evobgp-firewall uninstalled"