Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
947d1f0cc4 | ||
|
|
68f9d4b832 |
@@ -35,6 +35,16 @@ curl -fsSL https://<api>/v1/firewall/install.sh | \
|
|||||||
|
|
||||||
Файлы: `/etc/evobgp/firewall.conf`, `/usr/local/sbin/evobgp-firewall.sh`, systemd timer `evobgp-firewall.timer`.
|
Файлы: `/etc/evobgp/firewall.conf`, `/usr/local/sbin/evobgp-firewall.sh`, systemd timer `evobgp-firewall.timer`.
|
||||||
|
|
||||||
|
После **approve** в UI выполните на сервере (или дождитесь timer):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo rm -f /var/lib/evobgp-firewall/last_hash
|
||||||
|
sudo /usr/local/sbin/evobgp-firewall.sh
|
||||||
|
sudo nft list table inet evobgp_blocklist
|
||||||
|
```
|
||||||
|
|
||||||
|
Для парсинга JSON нужен `jq` или `python3` (install.sh ставит `jq` на Debian/Ubuntu при отсутствии).
|
||||||
|
|
||||||
## Failover через speaker
|
## Failover через speaker
|
||||||
|
|
||||||
При `EVOBGP_FIREWALL_FAILOVER_ENABLED=1` на speaker-agent CP реплицирует состояние через `POST /v1/agent/firewall-replicate`. Клиенты используют тот же DNS-домен.
|
При `EVOBGP_FIREWALL_FAILOVER_ENABLED=1` на speaker-agent CP реплицирует состояние через `POST /v1/agent/firewall-replicate`. Клиенты используют тот же DNS-домен.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ CONF_FILE=/etc/evobgp/firewall.conf
|
|||||||
LOG_FILE=/var/log/evobgp-firewall.log
|
LOG_FILE=/var/log/evobgp-firewall.log
|
||||||
STATE_DIR=/var/lib/evobgp-firewall
|
STATE_DIR=/var/lib/evobgp-firewall
|
||||||
HASH_FILE="${STATE_DIR}/last_hash"
|
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"; }
|
log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) $*" | tee -a "$LOG_FILE"; }
|
||||||
|
|
||||||
@@ -17,36 +18,32 @@ source "$CONF_FILE"
|
|||||||
|
|
||||||
: "${EVOBGP_CP_URL:?}"
|
: "${EVOBGP_CP_URL:?}"
|
||||||
: "${CLIENT_TOKEN:?}"
|
: "${CLIENT_TOKEN:?}"
|
||||||
|
CLIENT_TOKEN="${CLIENT_TOKEN//$'\r'/}"
|
||||||
|
CLIENT_TOKEN="${CLIENT_TOKEN//$'\n'/}"
|
||||||
|
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
BACKEND="${KERNEL_BACKEND:-auto}"
|
BACKEND="${KERNEL_BACKEND:-auto}"
|
||||||
|
|
||||||
curl_get_blocklist() {
|
curl_get_blocklist_file() {
|
||||||
local url="$1"
|
local url="$1"
|
||||||
local host
|
local dest="$2"
|
||||||
host=$(echo "$url" | sed -E 's#https?://([^/]+)/?.*#\1#')
|
|
||||||
local tmp
|
|
||||||
tmp=$(mktemp)
|
|
||||||
local code
|
local code
|
||||||
code=$(curl -sS -o "$tmp" -w "%{http_code}" \
|
code=$(curl -sS -o "$dest" -w "%{http_code}" \
|
||||||
-H "Authorization: Bearer ${CLIENT_TOKEN}" \
|
-H "Authorization: Bearer ${CLIENT_TOKEN}" \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
"${url}/v1/firewall/blocklist") || return 1
|
"${url}/v1/firewall/blocklist") || return 1
|
||||||
if [[ "$code" == "403" ]]; then
|
if [[ "$code" == "403" ]]; then
|
||||||
log "pending approval"
|
log "pending approval"
|
||||||
rm -f "$tmp"
|
return 2
|
||||||
exit 0
|
|
||||||
fi
|
fi
|
||||||
if [[ "$code" != "200" ]]; then
|
if [[ "$code" != "200" ]]; then
|
||||||
log "blocklist HTTP $code from $url"
|
log "blocklist HTTP $code from $url"
|
||||||
rm -f "$tmp"
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
cat "$tmp"
|
return 0
|
||||||
rm -f "$tmp"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try_urls() {
|
try_fetch_blocklist() {
|
||||||
local urls=()
|
local urls=()
|
||||||
if [[ -n "${EVOBGP_FAILOVER_URLS:-}" ]]; then
|
if [[ -n "${EVOBGP_FAILOVER_URLS:-}" ]]; then
|
||||||
IFS=',' read -r -a urls <<<"$EVOBGP_FAILOVER_URLS"
|
IFS=',' read -r -a urls <<<"$EVOBGP_FAILOVER_URLS"
|
||||||
@@ -57,7 +54,12 @@ try_urls() {
|
|||||||
for u in "${urls[@]}"; do
|
for u in "${urls[@]}"; do
|
||||||
u="${u// /}"
|
u="${u// /}"
|
||||||
u="${u%/}"
|
u="${u%/}"
|
||||||
if OUT=$(curl_get_blocklist "$u"); then
|
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"
|
CP_HIT="$u"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -65,22 +67,53 @@ try_urls() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if ! OUT=$(try_urls); then
|
parse_blocklist_file() {
|
||||||
|
local f="$1"
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! try_fetch_blocklist; then
|
||||||
log "all endpoints failed"
|
log "all endpoints failed"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v jq >/dev/null 2>&1; then
|
HASH=""
|
||||||
HASH=$(echo "$OUT" | jq -r '.hash // empty')
|
TOTAL=0
|
||||||
TOTAL=$(echo "$OUT" | jq -r '.total // 0')
|
PREFIXES=()
|
||||||
mapfile -t PREFIXES < <(echo "$OUT" | jq -r '.prefixes[]?')
|
parse_blocklist_file "$PREFIX_FILE"
|
||||||
else
|
|
||||||
HASH=$(echo "$OUT" | grep -o '"hash"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\(sha256:[^"]*\)".*/\1/')
|
if [[ -z "${TOTAL// }" ]]; then
|
||||||
TOTAL=$(echo "$OUT" | grep -o '"total"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | grep -o '[0-9]*$')
|
TOTAL=${#PREFIXES[@]}
|
||||||
mapfile -t PREFIXES < <(echo "$OUT" | grep -o '"[0-9a-fA-F:.]*/[0-9]*"' | tr -d '"')
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -f "$HASH_FILE" && "$(cat "$HASH_FILE")" == "$HASH" ]]; then
|
if [[ -f "$HASH_FILE" && "$(tr -d '\r\n' <"$HASH_FILE")" == "$HASH" && -n "$HASH" ]]; then
|
||||||
log "unchanged hash $HASH — skip kernel apply"
|
log "unchanged hash $HASH — skip kernel apply"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -88,48 +121,67 @@ fi
|
|||||||
apply_nft() {
|
apply_nft() {
|
||||||
local table=inet
|
local table=inet
|
||||||
local name=evobgp_blocklist
|
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 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 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
|
nft flush set "$table" "$name" v4
|
||||||
if ((${#PREFIXES[@]})); then
|
|
||||||
local v4=()
|
if ((${#v4[@]})); then
|
||||||
local p
|
local batch=()
|
||||||
for p in "${PREFIXES[@]}"; do
|
local chunk=128
|
||||||
[[ "$p" == *:* ]] && continue
|
local n
|
||||||
v4+=("$p")
|
for p in "${v4[@]}"; do
|
||||||
|
batch+=("$p")
|
||||||
|
if ((${#batch[@]} >= chunk)); then
|
||||||
|
nft add element "$table" "$name" v4 "{ $(IFS=,; echo "${batch[*]}") }"
|
||||||
|
batch=()
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
if ((${#v4[@]})); then
|
if ((${#batch[@]})); then
|
||||||
nft add element "$table" "$name" v4 "{ $(IFS=,; echo "${v4[*]}") }"
|
nft add element "$table" "$name" v4 "{ $(IFS=,; echo "${batch[*]}") }"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
nft list chain "$table" "$name" input >/dev/null 2>&1 || {
|
nft list chain "$table" "$name" input >/dev/null 2>&1 || {
|
||||||
nft add chain "$table" "$name" input '{ type filter hook input priority 0; }'
|
nft add chain "$table" "$name" input '{ type filter hook input priority 0; policy accept; }'
|
||||||
nft add rule "$table" "$name" input ip saddr @v4 drop
|
nft add rule "$table" "$name" input ip saddr @v4 drop
|
||||||
}
|
}
|
||||||
|
APPLIED_V4=${#v4[@]}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_ipset() {
|
apply_ipset() {
|
||||||
local set=evobgp_blocklist_v4
|
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 list "$set" >/dev/null 2>&1 || ipset create "$set" hash:net family inet hashsize 4096 maxelem 1048576
|
||||||
ipset flush "$set"
|
ipset flush "$set"
|
||||||
local p
|
local p
|
||||||
for p in "${PREFIXES[@]}"; do
|
for p in "${PREFIXES[@]}"; do
|
||||||
[[ "$p" == *:* ]] && continue
|
[[ "$p" == *:* ]] && continue
|
||||||
ipset add "$set" "$p" -exist
|
ipset add "$set" "$p" -exist
|
||||||
|
n=$((n + 1))
|
||||||
done
|
done
|
||||||
iptables -C INPUT -m set --match-set "$set" src -j DROP 2>/dev/null || \
|
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
|
iptables -I INPUT -m set --match-set "$set" src -j DROP
|
||||||
|
APPLIED_V4=$n
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_iptables_only() {
|
apply_iptables_only() {
|
||||||
iptables -D INPUT -m comment --comment evobgp-block -j DROP 2>/dev/null || true
|
iptables -D INPUT -m comment --comment evobgp-block -j DROP 2>/dev/null || true
|
||||||
if ((${#PREFIXES[@]})); then
|
local n=0
|
||||||
local p
|
local p
|
||||||
for p in "${PREFIXES[@]}"; do
|
for p in "${PREFIXES[@]}"; do
|
||||||
[[ "$p" == *:* ]] && continue
|
[[ "$p" == *:* ]] && continue
|
||||||
iptables -C INPUT -s "$p" -j DROP 2>/dev/null || iptables -A INPUT -s "$p" -j DROP
|
iptables -C INPUT -s "$p" -j DROP 2>/dev/null || iptables -A INPUT -s "$p" -j DROP
|
||||||
done
|
n=$((n + 1))
|
||||||
fi
|
done
|
||||||
|
APPLIED_V4=$n
|
||||||
}
|
}
|
||||||
|
|
||||||
clear_block() {
|
clear_block() {
|
||||||
@@ -141,10 +193,13 @@ clear_block() {
|
|||||||
;;
|
;;
|
||||||
iptables) iptables -S INPUT | grep -i evobgp | sed 's/^-A /-D /' | while read -r line; do iptables $line 2>/dev/null || true; done ;;
|
iptables) iptables -S INPUT | grep -i evobgp | sed 's/^-A /-D /' | while read -r line; do iptables $line 2>/dev/null || true; done ;;
|
||||||
esac
|
esac
|
||||||
|
APPLIED_V4=0
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "$TOTAL" == "0" || ${#PREFIXES[@]} -eq 0 ]]; then
|
APPLIED_V4=0
|
||||||
|
if [[ "${TOTAL:-0}" == "0" || ${#PREFIXES[@]} -eq 0 ]]; then
|
||||||
clear_block
|
clear_block
|
||||||
|
log "cleared blocklist (api total=${TOTAL:-0}) backend=$BACKEND"
|
||||||
else
|
else
|
||||||
case "$BACKEND" in
|
case "$BACKEND" in
|
||||||
nft|auto) if command -v nft >/dev/null 2>&1; then apply_nft; else apply_ipset; fi ;;
|
nft|auto) if command -v nft >/dev/null 2>&1; then apply_nft; else apply_ipset; fi ;;
|
||||||
@@ -152,12 +207,12 @@ else
|
|||||||
iptables) apply_iptables_only ;;
|
iptables) apply_iptables_only ;;
|
||||||
*) apply_ipset ;;
|
*) apply_ipset ;;
|
||||||
esac
|
esac
|
||||||
|
log "applied api_total=${TOTAL} ipv4_in_kernel=${APPLIED_V4} from ${CP_HIT:-$EVOBGP_CP_URL} backend=$BACKEND hash=${HASH:-empty}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$HASH" >"$HASH_FILE"
|
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}")
|
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" \
|
curl -fsS -X POST "${EVOBGP_CP_URL%/}/v1/firewall/apply-report" \
|
||||||
-H "Authorization: Bearer ${CLIENT_TOKEN}" \
|
-H "Authorization: Bearer ${CLIENT_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ for cmd in curl bash; do
|
|||||||
command -v "$cmd" >/dev/null 2>&1 || { echo "missing $cmd" >&2; exit 1; }
|
command -v "$cmd" >/dev/null 2>&1 || { echo "missing $cmd" >&2; exit 1; }
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if ! command -v jq >/dev/null 2>&1 && ! command -v python3 >/dev/null 2>&1; then
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
|
apt-get update -qq && apt-get install -y -qq jq
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if ! command -v jq >/dev/null 2>&1 && ! command -v python3 >/dev/null 2>&1; then
|
||||||
|
echo "evobgp-firewall install: install jq or python3 for blocklist JSON parsing" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
: "${EVOBGP_CP_URL:?EVOBGP_CP_URL required}"
|
: "${EVOBGP_CP_URL:?EVOBGP_CP_URL required}"
|
||||||
: "${EVOBGP_SEED:?EVOBGP_SEED required}"
|
: "${EVOBGP_SEED:?EVOBGP_SEED required}"
|
||||||
: "${EVOBGP_CLIENT_NAME:?EVOBGP_CLIENT_NAME required}"
|
: "${EVOBGP_CLIENT_NAME:?EVOBGP_CLIENT_NAME required}"
|
||||||
@@ -110,6 +120,7 @@ WantedBy=timers.target
|
|||||||
UNIT
|
UNIT
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable --now evobgp-firewall.timer
|
systemctl enable --now evobgp-firewall.timer
|
||||||
|
echo "Tip: after UI approve, run: rm -f /var/lib/evobgp-firewall/last_hash && ${SYNC_SCRIPT}"
|
||||||
else
|
else
|
||||||
echo "*/5 * * * * root ${SYNC_SCRIPT}" >/etc/cron.d/evobgp-firewall
|
echo "*/5 * * * * root ${SYNC_SCRIPT}" >/etc/cron.d/evobgp-firewall
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -13,14 +13,17 @@ import (
|
|||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const firewallClientSelectCols = `
|
||||||
|
id, name, COALESCE(hostname, ''), token_prefix, status,
|
||||||
|
last_seen_at, COALESCE(last_seen_at_source, ''), COALESCE(last_seen_ip, ''),
|
||||||
|
last_apply_at, COALESCE(last_apply_status, ''), COALESCE(last_apply_error, ''),
|
||||||
|
COALESCE(last_apply_prefix_count, 0), COALESCE(last_apply_ip_count, 0), COALESCE(last_apply_source, ''),
|
||||||
|
COALESCE(client_version, ''), created_at, approved_at, approved_by_api_key_id, revoked_at`
|
||||||
|
|
||||||
func (p *Postgres) ListFirewallClients(tenantID string) ([]*store.FirewallClient, error) {
|
func (p *Postgres) ListFirewallClients(tenantID string) ([]*store.FirewallClient, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
rows, err := p.pool.Query(ctx, `
|
rows, err := p.pool.Query(ctx, `
|
||||||
SELECT id, name, hostname, token_prefix, status,
|
SELECT `+firewallClientSelectCols+`
|
||||||
last_seen_at, last_seen_at_source, last_seen_ip,
|
|
||||||
last_apply_at, last_apply_status, last_apply_error,
|
|
||||||
last_apply_prefix_count, last_apply_ip_count, last_apply_source,
|
|
||||||
client_version, created_at, approved_at, approved_by_api_key_id, revoked_at
|
|
||||||
FROM firewall_client WHERE tenant_id=$1 ORDER BY created_at DESC`, tenantID)
|
FROM firewall_client WHERE tenant_id=$1 ORDER BY created_at DESC`, tenantID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -40,11 +43,7 @@ func (p *Postgres) ListFirewallClients(tenantID string) ([]*store.FirewallClient
|
|||||||
func (p *Postgres) GetFirewallClient(tenantID, id string) (*store.FirewallClient, error) {
|
func (p *Postgres) GetFirewallClient(tenantID, id string) (*store.FirewallClient, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
row := p.pool.QueryRow(ctx, `
|
row := p.pool.QueryRow(ctx, `
|
||||||
SELECT id, name, hostname, token_prefix, status,
|
SELECT `+firewallClientSelectCols+`
|
||||||
last_seen_at, last_seen_at_source, last_seen_ip,
|
|
||||||
last_apply_at, last_apply_status, last_apply_error,
|
|
||||||
last_apply_prefix_count, last_apply_ip_count, last_apply_source,
|
|
||||||
client_version, created_at, approved_at, approved_by_api_key_id, revoked_at
|
|
||||||
FROM firewall_client WHERE id=$1 AND tenant_id=$2`, id, tenantID)
|
FROM firewall_client WHERE id=$1 AND tenant_id=$2`, id, tenantID)
|
||||||
c, err := scanFirewallClientRow(row.Scan, tenantID)
|
c, err := scanFirewallClientRow(row.Scan, tenantID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -147,11 +146,7 @@ func (p *Postgres) LookupFirewallClientByTokenHash(hash []byte) (*store.Firewall
|
|||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
row := p.pool.QueryRow(ctx, `
|
row := p.pool.QueryRow(ctx, `
|
||||||
SELECT tenant_id, id, name, hostname, token_prefix, status,
|
SELECT tenant_id, `+firewallClientSelectCols+`
|
||||||
last_seen_at, last_seen_at_source, last_seen_ip,
|
|
||||||
last_apply_at, last_apply_status, last_apply_error,
|
|
||||||
last_apply_prefix_count, last_apply_ip_count, last_apply_source,
|
|
||||||
client_version, created_at, approved_at, approved_by_api_key_id, revoked_at
|
|
||||||
FROM firewall_client WHERE token_hash=$1`, hash)
|
FROM firewall_client WHERE token_hash=$1`, hash)
|
||||||
c, err := scanFirewallClientLookupRow(row.Scan)
|
c, err := scanFirewallClientLookupRow(row.Scan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"evobgp/internal/authkey"
|
||||||
|
"evobgp/internal/db"
|
||||||
|
"evobgp/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPostgresFirewallClientCreateAndGetIntegration(t *testing.T) {
|
||||||
|
dsn := os.Getenv("EVOBGP_TEST_DATABASE_URL")
|
||||||
|
if dsn == "" {
|
||||||
|
t.Skip("EVOBGP_TEST_DATABASE_URL not set")
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
pool, err := db.OpenPostgresPool(ctx, dsn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer pool.Close()
|
||||||
|
pg, err := NewPostgres(ctx, pool, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
tenant, _, _, _, _ := pg.DemoIDs()
|
||||||
|
if tenant == "" {
|
||||||
|
t.Fatal("demo tenant required")
|
||||||
|
}
|
||||||
|
tok := "evobgp_fw_pgtest_" + t.Name()
|
||||||
|
hash := authkey.HashToken(tok)
|
||||||
|
client, err := pg.CreateFirewallClient(tenant, &store.FirewallClientCreate{
|
||||||
|
Name: "pg-firewall-test",
|
||||||
|
Hostname: "test.local",
|
||||||
|
TokenPrefix: tok[:12],
|
||||||
|
TokenHash: hash,
|
||||||
|
ClientVersion: "test/1",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create: %v", err)
|
||||||
|
}
|
||||||
|
got, err := pg.GetFirewallClient(tenant, client.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get: %v", err)
|
||||||
|
}
|
||||||
|
if got.Name != "pg-firewall-test" || got.Status != "pending" {
|
||||||
|
t.Fatalf("got %+v", got)
|
||||||
|
}
|
||||||
|
_ = pg.DeleteFirewallClient(tenant, client.ID)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ CONF_FILE=/etc/evobgp/firewall.conf
|
|||||||
LOG_FILE=/var/log/evobgp-firewall.log
|
LOG_FILE=/var/log/evobgp-firewall.log
|
||||||
STATE_DIR=/var/lib/evobgp-firewall
|
STATE_DIR=/var/lib/evobgp-firewall
|
||||||
HASH_FILE="${STATE_DIR}/last_hash"
|
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"; }
|
log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) $*" | tee -a "$LOG_FILE"; }
|
||||||
|
|
||||||
@@ -17,36 +18,32 @@ source "$CONF_FILE"
|
|||||||
|
|
||||||
: "${EVOBGP_CP_URL:?}"
|
: "${EVOBGP_CP_URL:?}"
|
||||||
: "${CLIENT_TOKEN:?}"
|
: "${CLIENT_TOKEN:?}"
|
||||||
|
CLIENT_TOKEN="${CLIENT_TOKEN//$'\r'/}"
|
||||||
|
CLIENT_TOKEN="${CLIENT_TOKEN//$'\n'/}"
|
||||||
|
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
BACKEND="${KERNEL_BACKEND:-auto}"
|
BACKEND="${KERNEL_BACKEND:-auto}"
|
||||||
|
|
||||||
curl_get_blocklist() {
|
curl_get_blocklist_file() {
|
||||||
local url="$1"
|
local url="$1"
|
||||||
local host
|
local dest="$2"
|
||||||
host=$(echo "$url" | sed -E 's#https?://([^/]+)/?.*#\1#')
|
|
||||||
local tmp
|
|
||||||
tmp=$(mktemp)
|
|
||||||
local code
|
local code
|
||||||
code=$(curl -sS -o "$tmp" -w "%{http_code}" \
|
code=$(curl -sS -o "$dest" -w "%{http_code}" \
|
||||||
-H "Authorization: Bearer ${CLIENT_TOKEN}" \
|
-H "Authorization: Bearer ${CLIENT_TOKEN}" \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
"${url}/v1/firewall/blocklist") || return 1
|
"${url}/v1/firewall/blocklist") || return 1
|
||||||
if [[ "$code" == "403" ]]; then
|
if [[ "$code" == "403" ]]; then
|
||||||
log "pending approval"
|
log "pending approval"
|
||||||
rm -f "$tmp"
|
return 2
|
||||||
exit 0
|
|
||||||
fi
|
fi
|
||||||
if [[ "$code" != "200" ]]; then
|
if [[ "$code" != "200" ]]; then
|
||||||
log "blocklist HTTP $code from $url"
|
log "blocklist HTTP $code from $url"
|
||||||
rm -f "$tmp"
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
cat "$tmp"
|
return 0
|
||||||
rm -f "$tmp"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try_urls() {
|
try_fetch_blocklist() {
|
||||||
local urls=()
|
local urls=()
|
||||||
if [[ -n "${EVOBGP_FAILOVER_URLS:-}" ]]; then
|
if [[ -n "${EVOBGP_FAILOVER_URLS:-}" ]]; then
|
||||||
IFS=',' read -r -a urls <<<"$EVOBGP_FAILOVER_URLS"
|
IFS=',' read -r -a urls <<<"$EVOBGP_FAILOVER_URLS"
|
||||||
@@ -57,7 +54,12 @@ try_urls() {
|
|||||||
for u in "${urls[@]}"; do
|
for u in "${urls[@]}"; do
|
||||||
u="${u// /}"
|
u="${u// /}"
|
||||||
u="${u%/}"
|
u="${u%/}"
|
||||||
if OUT=$(curl_get_blocklist "$u"); then
|
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"
|
CP_HIT="$u"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -65,22 +67,53 @@ try_urls() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if ! OUT=$(try_urls); then
|
parse_blocklist_file() {
|
||||||
|
local f="$1"
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! try_fetch_blocklist; then
|
||||||
log "all endpoints failed"
|
log "all endpoints failed"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v jq >/dev/null 2>&1; then
|
HASH=""
|
||||||
HASH=$(echo "$OUT" | jq -r '.hash // empty')
|
TOTAL=0
|
||||||
TOTAL=$(echo "$OUT" | jq -r '.total // 0')
|
PREFIXES=()
|
||||||
mapfile -t PREFIXES < <(echo "$OUT" | jq -r '.prefixes[]?')
|
parse_blocklist_file "$PREFIX_FILE"
|
||||||
else
|
|
||||||
HASH=$(echo "$OUT" | grep -o '"hash"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\(sha256:[^"]*\)".*/\1/')
|
if [[ -z "${TOTAL// }" ]]; then
|
||||||
TOTAL=$(echo "$OUT" | grep -o '"total"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | grep -o '[0-9]*$')
|
TOTAL=${#PREFIXES[@]}
|
||||||
mapfile -t PREFIXES < <(echo "$OUT" | grep -o '"[0-9a-fA-F:.]*/[0-9]*"' | tr -d '"')
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -f "$HASH_FILE" && "$(cat "$HASH_FILE")" == "$HASH" ]]; then
|
if [[ -f "$HASH_FILE" && "$(tr -d '\r\n' <"$HASH_FILE")" == "$HASH" && -n "$HASH" ]]; then
|
||||||
log "unchanged hash $HASH — skip kernel apply"
|
log "unchanged hash $HASH — skip kernel apply"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -88,48 +121,67 @@ fi
|
|||||||
apply_nft() {
|
apply_nft() {
|
||||||
local table=inet
|
local table=inet
|
||||||
local name=evobgp_blocklist
|
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 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 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
|
nft flush set "$table" "$name" v4
|
||||||
if ((${#PREFIXES[@]})); then
|
|
||||||
local v4=()
|
if ((${#v4[@]})); then
|
||||||
local p
|
local batch=()
|
||||||
for p in "${PREFIXES[@]}"; do
|
local chunk=128
|
||||||
[[ "$p" == *:* ]] && continue
|
local n
|
||||||
v4+=("$p")
|
for p in "${v4[@]}"; do
|
||||||
|
batch+=("$p")
|
||||||
|
if ((${#batch[@]} >= chunk)); then
|
||||||
|
nft add element "$table" "$name" v4 "{ $(IFS=,; echo "${batch[*]}") }"
|
||||||
|
batch=()
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
if ((${#v4[@]})); then
|
if ((${#batch[@]})); then
|
||||||
nft add element "$table" "$name" v4 "{ $(IFS=,; echo "${v4[*]}") }"
|
nft add element "$table" "$name" v4 "{ $(IFS=,; echo "${batch[*]}") }"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
nft list chain "$table" "$name" input >/dev/null 2>&1 || {
|
nft list chain "$table" "$name" input >/dev/null 2>&1 || {
|
||||||
nft add chain "$table" "$name" input '{ type filter hook input priority 0; }'
|
nft add chain "$table" "$name" input '{ type filter hook input priority 0; policy accept; }'
|
||||||
nft add rule "$table" "$name" input ip saddr @v4 drop
|
nft add rule "$table" "$name" input ip saddr @v4 drop
|
||||||
}
|
}
|
||||||
|
APPLIED_V4=${#v4[@]}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_ipset() {
|
apply_ipset() {
|
||||||
local set=evobgp_blocklist_v4
|
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 list "$set" >/dev/null 2>&1 || ipset create "$set" hash:net family inet hashsize 4096 maxelem 1048576
|
||||||
ipset flush "$set"
|
ipset flush "$set"
|
||||||
local p
|
local p
|
||||||
for p in "${PREFIXES[@]}"; do
|
for p in "${PREFIXES[@]}"; do
|
||||||
[[ "$p" == *:* ]] && continue
|
[[ "$p" == *:* ]] && continue
|
||||||
ipset add "$set" "$p" -exist
|
ipset add "$set" "$p" -exist
|
||||||
|
n=$((n + 1))
|
||||||
done
|
done
|
||||||
iptables -C INPUT -m set --match-set "$set" src -j DROP 2>/dev/null || \
|
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
|
iptables -I INPUT -m set --match-set "$set" src -j DROP
|
||||||
|
APPLIED_V4=$n
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_iptables_only() {
|
apply_iptables_only() {
|
||||||
iptables -D INPUT -m comment --comment evobgp-block -j DROP 2>/dev/null || true
|
iptables -D INPUT -m comment --comment evobgp-block -j DROP 2>/dev/null || true
|
||||||
if ((${#PREFIXES[@]})); then
|
local n=0
|
||||||
local p
|
local p
|
||||||
for p in "${PREFIXES[@]}"; do
|
for p in "${PREFIXES[@]}"; do
|
||||||
[[ "$p" == *:* ]] && continue
|
[[ "$p" == *:* ]] && continue
|
||||||
iptables -C INPUT -s "$p" -j DROP 2>/dev/null || iptables -A INPUT -s "$p" -j DROP
|
iptables -C INPUT -s "$p" -j DROP 2>/dev/null || iptables -A INPUT -s "$p" -j DROP
|
||||||
done
|
n=$((n + 1))
|
||||||
fi
|
done
|
||||||
|
APPLIED_V4=$n
|
||||||
}
|
}
|
||||||
|
|
||||||
clear_block() {
|
clear_block() {
|
||||||
@@ -141,10 +193,13 @@ clear_block() {
|
|||||||
;;
|
;;
|
||||||
iptables) iptables -S INPUT | grep -i evobgp | sed 's/^-A /-D /' | while read -r line; do iptables $line 2>/dev/null || true; done ;;
|
iptables) iptables -S INPUT | grep -i evobgp | sed 's/^-A /-D /' | while read -r line; do iptables $line 2>/dev/null || true; done ;;
|
||||||
esac
|
esac
|
||||||
|
APPLIED_V4=0
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "$TOTAL" == "0" || ${#PREFIXES[@]} -eq 0 ]]; then
|
APPLIED_V4=0
|
||||||
|
if [[ "${TOTAL:-0}" == "0" || ${#PREFIXES[@]} -eq 0 ]]; then
|
||||||
clear_block
|
clear_block
|
||||||
|
log "cleared blocklist (api total=${TOTAL:-0}) backend=$BACKEND"
|
||||||
else
|
else
|
||||||
case "$BACKEND" in
|
case "$BACKEND" in
|
||||||
nft|auto) if command -v nft >/dev/null 2>&1; then apply_nft; else apply_ipset; fi ;;
|
nft|auto) if command -v nft >/dev/null 2>&1; then apply_nft; else apply_ipset; fi ;;
|
||||||
@@ -152,12 +207,12 @@ else
|
|||||||
iptables) apply_iptables_only ;;
|
iptables) apply_iptables_only ;;
|
||||||
*) apply_ipset ;;
|
*) apply_ipset ;;
|
||||||
esac
|
esac
|
||||||
|
log "applied api_total=${TOTAL} ipv4_in_kernel=${APPLIED_V4} from ${CP_HIT:-$EVOBGP_CP_URL} backend=$BACKEND hash=${HASH:-empty}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$HASH" >"$HASH_FILE"
|
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}")
|
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" \
|
curl -fsS -X POST "${EVOBGP_CP_URL%/}/v1/firewall/apply-report" \
|
||||||
-H "Authorization: Bearer ${CLIENT_TOKEN}" \
|
-H "Authorization: Bearer ${CLIENT_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ for cmd in curl bash; do
|
|||||||
command -v "$cmd" >/dev/null 2>&1 || { echo "missing $cmd" >&2; exit 1; }
|
command -v "$cmd" >/dev/null 2>&1 || { echo "missing $cmd" >&2; exit 1; }
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if ! command -v jq >/dev/null 2>&1 && ! command -v python3 >/dev/null 2>&1; then
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
|
apt-get update -qq && apt-get install -y -qq jq
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if ! command -v jq >/dev/null 2>&1 && ! command -v python3 >/dev/null 2>&1; then
|
||||||
|
echo "evobgp-firewall install: install jq or python3 for blocklist JSON parsing" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
: "${EVOBGP_CP_URL:?EVOBGP_CP_URL required}"
|
: "${EVOBGP_CP_URL:?EVOBGP_CP_URL required}"
|
||||||
: "${EVOBGP_SEED:?EVOBGP_SEED required}"
|
: "${EVOBGP_SEED:?EVOBGP_SEED required}"
|
||||||
: "${EVOBGP_CLIENT_NAME:?EVOBGP_CLIENT_NAME required}"
|
: "${EVOBGP_CLIENT_NAME:?EVOBGP_CLIENT_NAME required}"
|
||||||
@@ -110,6 +120,7 @@ WantedBy=timers.target
|
|||||||
UNIT
|
UNIT
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable --now evobgp-firewall.timer
|
systemctl enable --now evobgp-firewall.timer
|
||||||
|
echo "Tip: after UI approve, run: rm -f /var/lib/evobgp-firewall/last_hash && ${SYNC_SCRIPT}"
|
||||||
else
|
else
|
||||||
echo "*/5 * * * * root ${SYNC_SCRIPT}" >/etc/cron.d/evobgp-firewall
|
echo "*/5 * * * * root ${SYNC_SCRIPT}" >/etc/cron.d/evobgp-firewall
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user