feat(firewall): enhance firewall management with community selection and script handling
CI / changes (push) Successful in 10s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 57s
CI / go (push) Successful in 1m11s
CI / bird2 (push) Successful in 15s
CI / release (push) Successful in 4m16s
CI / changes (push) Successful in 10s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 57s
CI / go (push) Successful in 1m11s
CI / bird2 (push) Successful in 15s
CI / release (push) Successful in 4m16s
Added a community selection feature to the firewall rules management UI, allowing users to specify BGP communities for block/accept policies. Updated the backend to support reading firewall scripts from a specified directory, improving script management. Enhanced documentation to clarify the new community functionality and its implications for firewall rules. Additionally, introduced tests for the firewall script endpoints to ensure proper functionality.
This commit is contained in:
@@ -20,7 +20,10 @@ import {
|
||||
} from '@evobgp/ui/components/table'
|
||||
|
||||
import { PageHeader } from '@/components/page-header'
|
||||
import { CommunitySelect } from '@/components/modules/community-select'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { communityLabel } from '@/lib/modules/helpers'
|
||||
import { directoriesCommunitiesQueryOptions } from '@/queries/directories'
|
||||
import {
|
||||
firewallClientsQueryOptions,
|
||||
firewallInstallContextQueryOptions,
|
||||
@@ -29,7 +32,7 @@ import {
|
||||
useCreateFirewallRule,
|
||||
useDeleteFirewallRule,
|
||||
} from '@/queries/firewall'
|
||||
import type { FirewallClient } from '@/types/api'
|
||||
import type { BgpCommunity, FirewallClient } from '@/types/api'
|
||||
|
||||
export const Route = createFileRoute('/_auth/firewall')({
|
||||
component: FirewallPage,
|
||||
@@ -37,6 +40,7 @@ export const Route = createFileRoute('/_auth/firewall')({
|
||||
|
||||
function FirewallPage() {
|
||||
const installCtxQ = useQuery(firewallInstallContextQueryOptions())
|
||||
const communitiesQ = useQuery(directoriesCommunitiesQueryOptions())
|
||||
const clientsQ = useQuery(firewallClientsQueryOptions())
|
||||
const rulesQ = useQuery(firewallRulesQueryOptions('tenant'))
|
||||
const approve = useApproveFirewallClient()
|
||||
@@ -60,8 +64,11 @@ function FirewallPage() {
|
||||
}
|
||||
}, [installCtx?.bundle_seed, installCtx?.suggested_cp_url])
|
||||
const [ruleAction, setRuleAction] = useState<'block' | 'accept'>('block')
|
||||
const [ruleCommunityId, setRuleCommunityId] = useState<string | null>(null)
|
||||
const [ruleComment, setRuleComment] = useState('')
|
||||
|
||||
const communities = communitiesQ.data?.items ?? []
|
||||
|
||||
const clients = clientsQ.data?.items ?? []
|
||||
const pending = clients.filter((c) => c.status === 'pending')
|
||||
const rules = rulesQ.data?.items ?? []
|
||||
@@ -117,8 +124,9 @@ function FirewallPage() {
|
||||
<Info className="text-info" />
|
||||
<AlertTitle>Политика</AlertTitle>
|
||||
<AlertDescription>
|
||||
Только явный <strong>block</strong> добавляет IP в blocklist. Правила <strong>accept</strong> сами по себе
|
||||
не создают block all. Default — accept.
|
||||
Правила сопоставляются с <strong>BGP community</strong> префиксов опубликованной revision.{' '}
|
||||
<strong>block</strong> добавляет префиксы community в kernel; <strong>accept</strong> — не блокирует.
|
||||
Community «Все» — правило для любого community. Default без совпадений — accept.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -179,7 +187,7 @@ function FirewallPage() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="rules" className="mt-4 space-y-4">
|
||||
<div className="flex flex-wrap items-end gap-2">
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label>Действие</Label>
|
||||
<select
|
||||
@@ -191,18 +199,33 @@ function FirewallPage() {
|
||||
<option value="accept">accept</option>
|
||||
</select>
|
||||
</div>
|
||||
<Input
|
||||
className="max-w-xs"
|
||||
placeholder="Комментарий"
|
||||
value={ruleComment}
|
||||
onChange={(e) => setRuleComment(e.target.value)}
|
||||
<CommunitySelect
|
||||
id="fw-rule-community"
|
||||
label="Community"
|
||||
value={ruleCommunityId}
|
||||
onValueChange={setRuleCommunityId}
|
||||
communities={communities}
|
||||
nullable
|
||||
placeholder="Все communities"
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="fw-rule-comment">Комментарий</Label>
|
||||
<Input
|
||||
id="fw-rule-comment"
|
||||
className="max-w-xs"
|
||||
placeholder="Комментарий"
|
||||
value={ruleComment}
|
||||
onChange={(e) => setRuleComment(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="mb-0.5"
|
||||
onClick={() =>
|
||||
createRule.mutate({
|
||||
scope: 'tenant',
|
||||
action: ruleAction,
|
||||
community_id: ruleCommunityId,
|
||||
comment: ruleComment,
|
||||
})
|
||||
}
|
||||
@@ -210,7 +233,11 @@ function FirewallPage() {
|
||||
Добавить правило
|
||||
</Button>
|
||||
</div>
|
||||
<RulesTable rules={rules} onDelete={(id) => deleteRule.mutate(id)} />
|
||||
<RulesTable
|
||||
rules={rules}
|
||||
communities={communities}
|
||||
onDelete={(id) => deleteRule.mutate(id)}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="requests" className="mt-4">
|
||||
@@ -279,9 +306,11 @@ function ClientsTable({
|
||||
|
||||
function RulesTable({
|
||||
rules,
|
||||
communities,
|
||||
onDelete,
|
||||
}: {
|
||||
rules: { id: string; priority: number; action: string; comment?: string }[]
|
||||
rules: { id: string; priority: number; action: string; community_id?: string | null; comment?: string }[]
|
||||
communities: BgpCommunity[]
|
||||
onDelete: (id: string) => void
|
||||
}) {
|
||||
if (rules.length === 0) {
|
||||
@@ -293,6 +322,7 @@ function RulesTable({
|
||||
<TableRow>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead>Действие</TableHead>
|
||||
<TableHead>Community</TableHead>
|
||||
<TableHead>Комментарий</TableHead>
|
||||
<TableHead />
|
||||
</TableRow>
|
||||
@@ -304,6 +334,9 @@ function RulesTable({
|
||||
<TableCell>
|
||||
<StatusBadge status={r.action} label={r.action} />
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{r.community_id ? communityLabel(r.community_id, communities) : 'Все'}
|
||||
</TableCell>
|
||||
<TableCell>{r.comment || '—'}</TableCell>
|
||||
<TableCell>
|
||||
<Button size="sm" variant="ghost" onClick={() => onDelete(r.id)}>
|
||||
|
||||
@@ -58,6 +58,8 @@ RUN apt-get update \
|
||||
FROM runtime-base AS runtime
|
||||
ARG BIN=evobgp-api
|
||||
COPY --from=build-all /out/${BIN} /usr/local/bin/evobgp
|
||||
COPY scripts/firewall /opt/evobgp/scripts/firewall
|
||||
ENV EVOBGP_FIREWALL_SCRIPTS=/opt/evobgp/scripts/firewall
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/usr/local/bin/evobgp"]
|
||||
|
||||
|
||||
+6
-3
@@ -10,11 +10,14 @@
|
||||
|
||||
## Политика block/accept
|
||||
|
||||
- **`block`** — добавить префиксы community в kernel blocklist.
|
||||
- **`accept`** — не блокировать.
|
||||
- **`block`** — добавить префиксы выбранного BGP community в kernel blocklist.
|
||||
- **`accept`** — не блокировать префиксы этого community.
|
||||
- **Community** — правило применяется к префиксам с этим `community_id` в опубликованной revision; пустое значение («Все») — ко всем communities.
|
||||
- **Default** — accept (пустой blocklist без явных `block`).
|
||||
|
||||
Правила задаются на уровне tenant (по умолчанию) и per-server (overrides клиента). Client scope проверяется раньше tenant-default.
|
||||
Порядок: сначала per-server overrides клиента, затем tenant-default. Для каждого community берётся первое подходящее правило по приоритету.
|
||||
|
||||
Справочник communities: Web UI → Справочники, или модули с привязкой community к префиксам.
|
||||
|
||||
## Установка на сервер
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Package firewallscripts embeds bash installers served by GET /v1/firewall/install.sh.
|
||||
// Источник правды — scripts/firewall/; при изменении скопируйте файлы сюда или запустите:
|
||||
//
|
||||
// go generate ./internal/firewallscripts/...
|
||||
package firewallscripts
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed install.sh evobgp-firewall.sh uninstall.sh
|
||||
var FS embed.FS
|
||||
@@ -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
|
||||
@@ -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 → Запросы"
|
||||
@@ -0,0 +1,34 @@
|
||||
package firewallscripts
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScriptsMatchRepoSource(t *testing.T) {
|
||||
root := filepath.Join("..", "..", "scripts", "firewall")
|
||||
for _, name := range []string{"install.sh", "evobgp-firewall.sh", "uninstall.sh"} {
|
||||
embedded, err := FS.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatalf("embedded %s: %v", name, err)
|
||||
}
|
||||
source, err := os.ReadFile(filepath.Join(root, name))
|
||||
if err != nil {
|
||||
t.Skipf("source %s not found (cwd=%s): %v", name, mustWd(t), err)
|
||||
}
|
||||
if !bytes.Equal(embedded, source) {
|
||||
t.Fatalf("%s drift: copy scripts/firewall/%s to internal/firewallscripts/", name, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mustWd(t *testing.T) string {
|
||||
t.Helper()
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return wd
|
||||
}
|
||||
@@ -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"
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"evobgp/internal/authkey"
|
||||
"evobgp/internal/firewall"
|
||||
"evobgp/internal/firewallscripts"
|
||||
"evobgp/internal/store"
|
||||
)
|
||||
|
||||
@@ -159,8 +160,7 @@ func (s *Server) handleFirewallSyncScript(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
func (s *Server) serveFirewallScript(w http.ResponseWriter, name string) {
|
||||
path := filepath.Join("scripts", "firewall", name)
|
||||
b, err := os.ReadFile(path)
|
||||
b, err := readFirewallScript(name)
|
||||
if err != nil {
|
||||
writeProblem(w, http.StatusNotFound, "Not Found", "script not found")
|
||||
return
|
||||
@@ -170,6 +170,24 @@ func (s *Server) serveFirewallScript(w http.ResponseWriter, name string) {
|
||||
_, _ = w.Write(b)
|
||||
}
|
||||
|
||||
func readFirewallScript(name string) ([]byte, error) {
|
||||
if b, err := firewallscripts.FS.ReadFile(name); err == nil {
|
||||
return b, nil
|
||||
}
|
||||
candidates := []string{}
|
||||
if dir := strings.TrimSpace(os.Getenv("EVOBGP_FIREWALL_SCRIPTS")); dir != "" {
|
||||
candidates = append(candidates, filepath.Join(dir, name))
|
||||
}
|
||||
candidates = append(candidates, filepath.Join("scripts", "firewall", name))
|
||||
for _, p := range candidates {
|
||||
b, err := os.ReadFile(p)
|
||||
if err == nil {
|
||||
return b, nil
|
||||
}
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func (s *Server) handleListFirewallClients(w http.ResponseWriter, r *http.Request) {
|
||||
a, ok := authFromContext(r.Context())
|
||||
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
||||
|
||||
@@ -119,6 +119,39 @@ func TestFirewallEnrollBadSeed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewallInstallScriptPublic(t *testing.T) {
|
||||
srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer srv.Close()
|
||||
ts := httptest.NewServer(srv.Handler())
|
||||
defer ts.Close()
|
||||
|
||||
for _, path := range []string{"/v1/firewall/install.sh", "/v1/firewall/sync-script"} {
|
||||
req, _ := http.NewRequest(http.MethodGet, ts.URL+path, nil)
|
||||
resp, err := ts.Client().Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
func() {
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("%s status=%d body=%s", path, resp.StatusCode, b)
|
||||
}
|
||||
ct := resp.Header.Get("Content-Type")
|
||||
if !strings.Contains(ct, "shellscript") {
|
||||
t.Fatalf("%s content-type=%q", path, ct)
|
||||
}
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
if !strings.HasPrefix(string(b), "#!/") {
|
||||
t.Fatalf("%s missing shebang", path)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewallInstallContext(t *testing.T) {
|
||||
srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user