Files
EvoFirewall/apps/api/src/agent-scripts/evofw-firewall.sh
T
Denozordec 68d9246158
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m43s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped
fix(api): improve error handling and quoting in agent scripts
- Updated `evofw-firewall.sh` to correctly handle exit codes from the `curl_policy` function, ensuring proper script termination based on policy retrieval status.
- Enhanced `install.sh` to always use single quotes for configuration values, improving safety for names with spaces, and refined the logic for updating the `KERNEL_BACKEND` in the configuration file.
- Updated documentation to reflect the changes in quoting and exit behavior during installation and synchronization processes.
2026-07-21 23:18:03 +07:00

223 lines
7.3 KiB
Bash

#!/usr/bin/env bash
# EvoFirewall Linux sync agent — nft / ipset / iptables
set -euo pipefail
CONF_FILE=/etc/evofw/agent.conf
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"
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"
: "${EVOFW_CP_URL:?}"
: "${CLIENT_TOKEN:?}"
CLIENT_TOKEN="${CLIENT_TOKEN//$'\r'/}"
CLIENT_TOKEN="${CLIENT_TOKEN//$'\n'/}"
BACKEND="${KERNEL_BACKEND:-auto}"
mkdir -p "$STATE_DIR"
curl_policy() {
local dest="$1"
local code
code=$(curl -sS -o "$dest" -w "%{http_code}" \
-H "Authorization: Bearer ${CLIENT_TOKEN}" \
-H "Accept: application/json" \
"${EVOFW_CP_URL%/}/v1/agent/policy") || return 1
if [[ "$code" == "403" ]]; then
log "pending approval"
return 2
fi
if [[ "$code" != "200" ]]; then
log "policy HTTP $code"
return 1
fi
return 0
}
# Do not use `if ! cmd; rc=$?` — after `!`, $? is 0, not the real status.
policy_rc=0
curl_policy "$POLICY_FILE" || policy_rc=$?
if [[ "$policy_rc" -eq 2 ]]; then
exit 0
fi
if [[ "$policy_rc" -ne 0 ]]; then
exit 1
fi
parse_policy() {
local f="$1"
if command -v jq >/dev/null 2>&1; then
HASH=$(jq -r '.hash // empty' "$f")
MODE=$(jq -r '.policy_mode // "blacklist"' "$f")
mapfile -t DENY < <(jq -r '.deny_cidrs[]? // empty' "$f")
mapfile -t ALLOW < <(jq -r '.allow_cidrs[]? // empty' "$f")
return 0
fi
if command -v python3 >/dev/null 2>&1; then
eval "$(python3 - "$f" <<'PY'
import json,sys
d=json.load(open(sys.argv[1],encoding="utf-8"))
print(f'HASH={d.get("hash") or ""}')
print(f'MODE={d.get("policy_mode") or "blacklist"}')
print("DENY=("+" ".join(json.dumps(x) for x in (d.get("deny_cidrs") or []))+")")
print("ALLOW=("+" ".join(json.dumps(x) for x in (d.get("allow_cidrs") or []))+")")
PY
)"
return 0
fi
log "need jq or python3"
exit 1
}
HASH=""; MODE=blacklist; DENY=(); ALLOW=()
parse_policy "$POLICY_FILE"
# Empty deny/allow is valid — agent may have no rule sets yet.
DENY=("${DENY[@]+"${DENY[@]}"}")
ALLOW=("${ALLOW[@]+"${ALLOW[@]}"}")
log "mode=$MODE deny=${#DENY[@]} allow=${#ALLOW[@]} hash=$HASH"
PACKETS_DROPPED=0
PACKETS_ACCEPTED=0
KERNEL_METHOD=""
APPLIED=0
nft_join() {
local out="" p
for p in "$@"; do
[[ -n "$out" ]] && out+=", "
out+="$p"
done
printf '%s' "$out"
}
nft_add_chunk() {
local table=$1 name=$2 setname=$3
shift 3
local joined; joined=$(nft_join "$@")
nft add element "$table" "$name" "$setname" "{ ${joined} }" 2>>"$LOG_FILE" || {
for p in "$@"; do nft add element "$table" "$name" "$setname" "{ $p }" 2>>"$LOG_FILE" || true; done
}
}
collect_nft_stats() {
PACKETS_DROPPED=0; PACKETS_ACCEPTED=0
local line
while IFS= read -r line; do
if [[ "$line" == *drop* && "$line" =~ packets[[:space:]]+([0-9]+) ]]; then
PACKETS_DROPPED="${BASH_REMATCH[1]}"
elif [[ "$line" == *accept* && "$line" =~ packets[[:space:]]+([0-9]+) ]]; then
PACKETS_ACCEPTED="${BASH_REMATCH[1]}"
fi
done < <(nft list chain inet evofw input 2>/dev/null || true)
}
apply_nft() {
local table=inet name=evofw
local deny_v4=() allow_v4=() p
for p in "${DENY[@]+"${DENY[@]}"}"; do [[ "$p" == *:* ]] && continue; deny_v4+=("$p"); done
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; }'
nft flush set "$table" "$name" deny_v4
nft flush set "$table" "$name" allow_v4
local batch=() chunk=64
for p in "${deny_v4[@]}"; do
batch+=("$p")
if ((${#batch[@]} >= chunk)); then nft_add_chunk "$table" "$name" deny_v4 "${batch[@]}"; batch=(); fi
done
((${#batch[@]})) && nft_add_chunk "$table" "$name" deny_v4 "${batch[@]}"
batch=()
for p in "${allow_v4[@]}"; do
batch+=("$p")
if ((${#batch[@]} >= chunk)); then nft_add_chunk "$table" "$name" allow_v4 "${batch[@]}"; batch=(); fi
done
((${#batch[@]})) && nft_add_chunk "$table" "$name" allow_v4 "${batch[@]}"
nft delete chain "$table" "$name" input 2>/dev/null || true
if [[ "$MODE" == "whitelist" ]]; then
nft add chain "$table" "$name" input '{ type filter hook input priority 0; policy drop; }'
nft add rule "$table" "$name" input ct state established,related counter accept
nft add rule "$table" "$name" input iif lo counter accept
nft add rule "$table" "$name" input ip saddr @allow_v4 counter accept
nft add rule "$table" "$name" input counter drop
else
nft add chain "$table" "$name" input '{ type filter hook input priority 0; policy accept; }'
nft add rule "$table" "$name" input ip saddr @deny_v4 counter drop
nft add rule "$table" "$name" input counter accept
fi
KERNEL_METHOD=nft
APPLIED=$((${#deny_v4[@]} + ${#allow_v4[@]}))
}
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
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
for p in "${ALLOW[@]+"${ALLOW[@]}"}"; do [[ "$p" == *:* ]] && continue; ipset add "$aset" "$p" -exist; n=$((n+1)); done
iptables -D INPUT -m set --match-set "$dset" src -j DROP 2>/dev/null || true
iptables -D INPUT -m set --match-set "$aset" src -j ACCEPT 2>/dev/null || true
if [[ "$MODE" == "whitelist" ]]; then
iptables -I INPUT -m set --match-set "$aset" src -j ACCEPT
iptables -A INPUT -j DROP 2>/dev/null || true
else
iptables -I INPUT -m set --match-set "$dset" src -j DROP
fi
KERNEL_METHOD=ipset
APPLIED=$n
}
send_report() {
if [[ "$KERNEL_METHOD" == "nft" ]] || command -v nft >/dev/null 2>&1; then
collect_nft_stats
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}")
curl -fsS -X POST "${EVOFW_CP_URL%/}/v1/agent/apply-report" \
-H "Authorization: Bearer ${CLIENT_TOKEN}" \
-H "Content-Type: application/json" \
-d "$report" >/dev/null 2>&1 || true
curl -fsS -X POST "${EVOFW_CP_URL%/}/v1/agent/heartbeat" \
-H "Authorization: Bearer ${CLIENT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"source":"agent"}' >/dev/null 2>&1 || true
}
if [[ -f "$HASH_FILE" && "$(tr -d '\r\n' <"$HASH_FILE")" == "$HASH" && -n "$HASH" ]]; then
log "unchanged hash $HASH — skip apply"
KERNEL_METHOD="${BACKEND}"
send_report
exit 0
fi
case "$BACKEND" in
nft|auto)
if command -v nft >/dev/null 2>&1; then apply_nft
elif command -v ipset >/dev/null 2>&1; then apply_ipset
else log "no backend"; exit 1; fi
;;
ipset) apply_ipset ;;
*) apply_nft ;;
esac
echo "$HASH" >"$HASH_FILE"
log "applied mode=$MODE count=$APPLIED method=$KERNEL_METHOD"
send_report