Compare commits

..
4 Commits
Author SHA1 Message Date
Denozordec e51999c908 feat(firewall): add revoke functionality for firewall clients and enhance status badge
CI / changes (push) Successful in 13s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 30s
CI / web (push) Successful in 56s
CI / go (push) Successful in 1m11s
CI / bird2 (push) Successful in 27s
CI / release (push) Successful in 4m19s
Implemented the ability to revoke approved firewall clients and reject pending requests through new API endpoints. Updated the StatusBadge component to include additional status variants for 'approved', 'revoked', 'pending', and 'block'. Enhanced the FirewallPage UI to support client revocation and rejection actions, integrating confirmation dialogs for user interactions. Updated tests to ensure proper functionality of the new revoke feature.
2026-07-08 23:27:29 +07:00
Denozordec b7f7669685 feat(firewall): improve blocklist parsing and nft element addition
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.
2026-07-08 22:02:59 +07:00
Denozordec 947d1f0cc4 feat(firewall): enhance blocklist handling and installation script
CI / changes (push) Successful in 11s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 29s
CI / web (push) Successful in 58s
CI / go (push) Successful in 1m20s
CI / bird2 (push) Successful in 18s
CI / release (push) Successful in 4m37s
Updated the firewall scripts to improve blocklist handling by introducing a new method for fetching and parsing blocklist data using either `jq` or `python3`. Enhanced the installation script to ensure the presence of required dependencies and provided user guidance for post-approval actions. Additionally, improved logging for applied prefixes and total counts, ensuring better visibility into the firewall's operational status.
2026-07-08 21:45:32 +07:00
Denozordec 68f9d4b832 refactor(firewall): simplify SQL queries for firewall client retrieval
CI / changes (push) Successful in 13s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Has been skipped
CI / go (push) Successful in 1m6s
CI / bird2 (push) Successful in 18s
CI / release (push) Successful in 3m57s
Refactored the SQL queries in the Postgres repository for listing and retrieving firewall clients by introducing a constant for the selected columns. This change improves code readability and maintainability by reducing duplication in the query definitions. No functional changes were made to the data retrieval process.
2026-07-08 21:16:57 +07:00
12 changed files with 540 additions and 107 deletions
+5
View File
@@ -22,6 +22,11 @@ const STATUS_VARIANT: Record<string, BadgeVariant> = {
stale: 'warning', stale: 'warning',
warning: 'warning', warning: 'warning',
mismatch: 'warning', mismatch: 'warning',
pending: 'warning',
approved: 'success',
revoked: 'destructive',
block: 'destructive',
accept: 'success',
} }
export function StatusBadge({ status, label }: { status: string; label?: string }) { export function StatusBadge({ status, label }: { status: string; label?: string }) {
+17
View File
@@ -1,4 +1,6 @@
import { queryOptions, useMutation, useQueryClient } from '@tanstack/react-query' import { queryOptions, useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { apiJSON } from '@/lib/api-client' import { apiJSON } from '@/lib/api-client'
import type { import type {
FirewallClient, FirewallClient,
@@ -51,8 +53,23 @@ export function useApproveFirewallClient() {
mutationFn: (id: string) => mutationFn: (id: string) =>
apiJSON<FirewallClient>(`/v1/firewall/clients/${id}/approve`, { method: 'POST' }), apiJSON<FirewallClient>(`/v1/firewall/clients/${id}/approve`, { method: 'POST' }),
onSuccess: () => { onSuccess: () => {
toast.success('Клиент одобрен')
void qc.invalidateQueries({ queryKey: firewallKeys.clients() }) void qc.invalidateQueries({ queryKey: firewallKeys.clients() })
}, },
onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось одобрить'),
})
}
export function useRevokeFirewallClient() {
const qc = useQueryClient()
return useMutation({
mutationFn: (id: string) =>
apiJSON<{ status: string }>(`/v1/firewall/clients/${id}/revoke`, { method: 'POST' }),
onSuccess: () => {
toast.success('Клиент отключён')
void qc.invalidateQueries({ queryKey: firewallKeys.clients() })
},
onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось отклонить'),
}) })
} }
+69 -6
View File
@@ -19,6 +19,7 @@ import {
TableRow, TableRow,
} from '@evobgp/ui/components/table' } from '@evobgp/ui/components/table'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { PageHeader } from '@/components/page-header' import { PageHeader } from '@/components/page-header'
import { CommunitySelect } from '@/components/modules/community-select' import { CommunitySelect } from '@/components/modules/community-select'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
@@ -31,6 +32,7 @@ import {
useApproveFirewallClient, useApproveFirewallClient,
useCreateFirewallRule, useCreateFirewallRule,
useDeleteFirewallRule, useDeleteFirewallRule,
useRevokeFirewallClient,
} from '@/queries/firewall' } from '@/queries/firewall'
import type { BgpCommunity, FirewallClient } from '@/types/api' import type { BgpCommunity, FirewallClient } from '@/types/api'
@@ -54,6 +56,7 @@ function FirewallPage() {
const clientsQ = useQuery(firewallClientsQueryOptions()) const clientsQ = useQuery(firewallClientsQueryOptions())
const rulesQ = useQuery(firewallRulesQueryOptions('tenant')) const rulesQ = useQuery(firewallRulesQueryOptions('tenant'))
const approve = useApproveFirewallClient() const approve = useApproveFirewallClient()
const revoke = useRevokeFirewallClient()
const createRule = useCreateFirewallRule() const createRule = useCreateFirewallRule()
const deleteRule = useDeleteFirewallRule() const deleteRule = useDeleteFirewallRule()
@@ -193,7 +196,13 @@ function FirewallPage() {
</TabsList> </TabsList>
<TabsContent value="clients" className="mt-4"> <TabsContent value="clients" className="mt-4">
<ClientsTable clients={clients} onApprove={(id) => approve.mutate(id)} /> <ClientsTable
clients={clients}
onApprove={(id) => approve.mutate(id)}
onReject={(id) => revoke.mutate(id)}
approvePending={approve.isPending}
rejectPending={revoke.isPending}
/>
</TabsContent> </TabsContent>
<TabsContent value="rules" className="mt-4 space-y-4"> <TabsContent value="rules" className="mt-4 space-y-4">
@@ -254,6 +263,9 @@ function FirewallPage() {
<ClientsTable <ClientsTable
clients={pending} clients={pending}
onApprove={(id) => approve.mutate(id)} onApprove={(id) => approve.mutate(id)}
onReject={(id) => revoke.mutate(id)}
approvePending={approve.isPending}
rejectPending={revoke.isPending}
emptyTitle="Нет pending-запросов" emptyTitle="Нет pending-запросов"
/> />
</TabsContent> </TabsContent>
@@ -265,10 +277,16 @@ function FirewallPage() {
function ClientsTable({ function ClientsTable({
clients, clients,
onApprove, onApprove,
onReject,
approvePending = false,
rejectPending = false,
emptyTitle = 'Нет клиентов', emptyTitle = 'Нет клиентов',
}: { }: {
clients: FirewallClient[] clients: FirewallClient[]
onApprove: (id: string) => void onApprove: (id: string) => void
onReject: (id: string) => void
approvePending?: boolean
rejectPending?: boolean
emptyTitle?: string emptyTitle?: string
}) { }) {
if (clients.length === 0) { if (clients.length === 0) {
@@ -301,11 +319,56 @@ function ClientsTable({
{c.last_apply_prefix_count != null ? ` (${c.last_apply_prefix_count})` : ''} {c.last_apply_prefix_count != null ? ` (${c.last_apply_prefix_count})` : ''}
</TableCell> </TableCell>
<TableCell> <TableCell>
{c.status === 'pending' ? ( <div className="flex justify-end gap-2">
<Button size="sm" variant="outline" onClick={() => onApprove(c.id)}> {c.status === 'pending' ? (
Approve <>
</Button> <Button
) : null} size="sm"
variant="outline"
disabled={approvePending}
onClick={() => onApprove(c.id)}
>
Одобрить
</Button>
<ConfirmDialog
trigger={
<Button
size="sm"
variant="outline"
className="text-destructive"
disabled={rejectPending}
>
Отклонить
</Button>
}
title="Отклонить запрос?"
description={`${c.name}${c.hostname ? ` (${c.hostname})` : ''} — токен перестанет работать.`}
confirmLabel="Отклонить"
destructive
onConfirm={() => onReject(c.id)}
/>
</>
) : null}
{c.status === 'approved' ? (
<ConfirmDialog
trigger={
<Button
size="sm"
variant="ghost"
className="text-destructive"
disabled={rejectPending}
>
Отозвать
</Button>
}
title="Отозвать клиент?"
description={`${c.name} — blocklist перестанет отдаваться, токен будет недействителен.`}
confirmLabel="Отозвать"
destructive
onConfirm={() => onReject(c.id)}
/>
) : null}
</div>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
+10
View File
@@ -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-домен.
+25
View File
@@ -4472,6 +4472,31 @@ paths:
default: default:
$ref: "#/components/responses/DefaultProblem" $ref: "#/components/responses/DefaultProblem"
/v1/firewall/clients/{id}/revoke:
post:
tags: [Firewall]
summary: Reject pending or revoke approved client
operationId: revokeFirewallClient
parameters:
- name: id
in: path
required: true
schema:
$ref: "#/components/schemas/ResourceId"
responses:
"200":
description: Revoked
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: [revoked]
default:
$ref: "#/components/responses/DefaultProblem"
/v1/firewall/rules: /v1/firewall/rules:
get: get:
tags: [Firewall] tags: [Firewall]
+131 -43
View File
@@ -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,87 @@ try_urls() {
return 1 return 1
} }
if ! OUT=$(try_urls); then 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" 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 log "blocklist bytes=$(wc -c <"$PREFIX_FILE" | tr -d ' ') parsed=${#PREFIXES[@]} api_total=${TOTAL:-0}"
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]*$') if [[ -z "${TOTAL// }" ]]; then
mapfile -t PREFIXES < <(echo "$OUT" | grep -o '"[0-9a-fA-F:.]*/[0-9]*"' | tr -d '"') TOTAL=${#PREFIXES[@]}
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 +155,66 @@ 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=64
[[ "$p" == *:* ]] && continue for p in "${v4[@]}"; do
v4+=("$p") batch+=("$p")
if ((${#batch[@]} >= chunk)); then
nft_add_v4_chunk "$table" "$name" "${batch[@]}" || log "nft chunk add partial failure"
batch=()
fi
done done
if ((${#v4[@]})); then if ((${#batch[@]})); then
nft add element "$table" "$name" v4 "{ $(IFS=,; echo "${v4[*]}") }" nft_add_v4_chunk "$table" "$name" "${batch[@]}" || log "nft tail chunk add partial failure"
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 +226,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 +240,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" \
+11
View File
@@ -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
+68
View File
@@ -205,6 +205,74 @@ func TestFirewallInstallContext(t *testing.T) {
} }
} }
func TestFirewallRevokePendingClient(t *testing.T) {
srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
if err != nil {
t.Fatal(err)
}
defer srv.Close()
tenant, _, _, _, _ := srv.Store().DemoIDs()
mustSetTestAPIKeys(t, srv, "opkey|"+tenant+"|operator")
ts := httptest.NewServer(srv.Handler())
defer ts.Close()
client := ts.Client()
tok := "evobgp_fw_revoketest123456789012345678901"
enrollBody := `{"name":"reject-me","hostname":"test.local","client_token":"` + tok + `","client_version":"test/1"}`
reqEnroll, _ := http.NewRequest(http.MethodPost, ts.URL+"/v1/firewall/enroll", strings.NewReader(enrollBody))
reqEnroll.Header.Set("Content-Type", "application/json")
reqEnroll.Header.Set("X-EvoBGP-Seed", testBundleSeed)
respEnroll, err := client.Do(reqEnroll)
if err != nil {
t.Fatal(err)
}
defer func() { _ = respEnroll.Body.Close() }()
if respEnroll.StatusCode != http.StatusCreated {
b, _ := io.ReadAll(respEnroll.Body)
t.Fatalf("enroll status=%d body=%s", respEnroll.StatusCode, b)
}
var enroll map[string]any
if err := json.NewDecoder(respEnroll.Body).Decode(&enroll); err != nil {
t.Fatal(err)
}
clientID, _ := enroll["client_id"].(string)
if clientID == "" {
t.Fatal("missing client_id")
}
reqRevoke, _ := http.NewRequest(http.MethodPost, ts.URL+"/v1/firewall/clients/"+clientID+"/revoke", nil)
reqRevoke.Header.Set("Authorization", "Bearer opkey")
respRevoke, err := client.Do(reqRevoke)
if err != nil {
t.Fatal(err)
}
defer func() { _ = respRevoke.Body.Close() }()
if respRevoke.StatusCode != http.StatusOK {
b, _ := io.ReadAll(respRevoke.Body)
t.Fatalf("revoke status=%d body=%s", respRevoke.StatusCode, b)
}
got, err := srv.Store().GetFirewallClient(tenant, clientID)
if err != nil {
t.Fatal(err)
}
if got.Status != "revoked" {
t.Fatalf("status=%q want revoked", got.Status)
}
reqBlock, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/firewall/blocklist", nil)
reqBlock.Header.Set("Authorization", "Bearer "+tok)
respBlock, err := client.Do(reqBlock)
if err != nil {
t.Fatal(err)
}
defer func() { _ = respBlock.Body.Close() }()
if respBlock.StatusCode != http.StatusForbidden {
t.Fatalf("revoked blocklist want 403 got %d", respBlock.StatusCode)
}
}
func TestFirewallTokenHashMatchesAuthkey(t *testing.T) { func TestFirewallTokenHashMatchesAuthkey(t *testing.T) {
tok := "evobgp_fw_sample" tok := "evobgp_fw_sample"
h := authkey.HashToken(tok) h := authkey.HashToken(tok)
+10 -15
View File
@@ -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)
}
+131 -43
View File
@@ -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,87 @@ try_urls() {
return 1 return 1
} }
if ! OUT=$(try_urls); then 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" 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 log "blocklist bytes=$(wc -c <"$PREFIX_FILE" | tr -d ' ') parsed=${#PREFIXES[@]} api_total=${TOTAL:-0}"
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]*$') if [[ -z "${TOTAL// }" ]]; then
mapfile -t PREFIXES < <(echo "$OUT" | grep -o '"[0-9a-fA-F:.]*/[0-9]*"' | tr -d '"') TOTAL=${#PREFIXES[@]}
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 +155,66 @@ 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=64
[[ "$p" == *:* ]] && continue for p in "${v4[@]}"; do
v4+=("$p") batch+=("$p")
if ((${#batch[@]} >= chunk)); then
nft_add_v4_chunk "$table" "$name" "${batch[@]}" || log "nft chunk add partial failure"
batch=()
fi
done done
if ((${#v4[@]})); then if ((${#batch[@]})); then
nft add element "$table" "$name" v4 "{ $(IFS=,; echo "${v4[*]}") }" nft_add_v4_chunk "$table" "$name" "${batch[@]}" || log "nft tail chunk add partial failure"
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 +226,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 +240,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" \
+11
View File
@@ -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