Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b3f002e5f | ||
|
|
fa2abc81f3 |
@@ -1,14 +1,30 @@
|
|||||||
import { queryOptions, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { queryOptions, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { apiJSON } from '@/lib/api-client'
|
import { apiJSON } from '@/lib/api-client'
|
||||||
import type { FirewallClient, FirewallClientsResponse, FirewallRule, FirewallRulesResponse } from '@/types/api'
|
import type {
|
||||||
|
FirewallClient,
|
||||||
|
FirewallClientsResponse,
|
||||||
|
FirewallInstallContext,
|
||||||
|
FirewallRule,
|
||||||
|
FirewallRulesResponse,
|
||||||
|
} from '@/types/api'
|
||||||
|
|
||||||
export const firewallKeys = {
|
export const firewallKeys = {
|
||||||
all: ['firewall'] as const,
|
all: ['firewall'] as const,
|
||||||
clients: () => [...firewallKeys.all, 'clients'] as const,
|
clients: () => [...firewallKeys.all, 'clients'] as const,
|
||||||
|
installContext: () => [...firewallKeys.all, 'install-context'] as const,
|
||||||
rules: (scope: string, clientId?: string) =>
|
rules: (scope: string, clientId?: string) =>
|
||||||
[...firewallKeys.all, 'rules', scope, clientId ?? ''] as const,
|
[...firewallKeys.all, 'rules', scope, clientId ?? ''] as const,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function firewallInstallContextQueryOptions() {
|
||||||
|
return queryOptions<FirewallInstallContext>({
|
||||||
|
queryKey: firewallKeys.installContext(),
|
||||||
|
queryFn: () => apiJSON<FirewallInstallContext>('/v1/firewall/install-context'),
|
||||||
|
staleTime: 60_000,
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function firewallClientsQueryOptions() {
|
export function firewallClientsQueryOptions() {
|
||||||
return queryOptions<FirewallClientsResponse>({
|
return queryOptions<FirewallClientsResponse>({
|
||||||
queryKey: firewallKeys.clients(),
|
queryKey: firewallKeys.clients(),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { Copy, Info, RefreshCw, Shield } from 'lucide-react'
|
import { Copy, Info, RefreshCw, Shield } from 'lucide-react'
|
||||||
import { useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
|
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
|
||||||
@@ -20,35 +20,55 @@ import {
|
|||||||
} from '@evobgp/ui/components/table'
|
} from '@evobgp/ui/components/table'
|
||||||
|
|
||||||
import { PageHeader } from '@/components/page-header'
|
import { PageHeader } from '@/components/page-header'
|
||||||
|
import { CommunitySelect } from '@/components/modules/community-select'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
|
import { communityLabel } from '@/lib/modules/helpers'
|
||||||
|
import { directoriesCommunitiesQueryOptions } from '@/queries/directories'
|
||||||
import {
|
import {
|
||||||
firewallClientsQueryOptions,
|
firewallClientsQueryOptions,
|
||||||
|
firewallInstallContextQueryOptions,
|
||||||
firewallRulesQueryOptions,
|
firewallRulesQueryOptions,
|
||||||
useApproveFirewallClient,
|
useApproveFirewallClient,
|
||||||
useCreateFirewallRule,
|
useCreateFirewallRule,
|
||||||
useDeleteFirewallRule,
|
useDeleteFirewallRule,
|
||||||
} from '@/queries/firewall'
|
} from '@/queries/firewall'
|
||||||
import type { FirewallClient } from '@/types/api'
|
import type { BgpCommunity, FirewallClient } from '@/types/api'
|
||||||
|
|
||||||
export const Route = createFileRoute('/_auth/firewall')({
|
export const Route = createFileRoute('/_auth/firewall')({
|
||||||
component: FirewallPage,
|
component: FirewallPage,
|
||||||
})
|
})
|
||||||
|
|
||||||
function FirewallPage() {
|
function FirewallPage() {
|
||||||
|
const installCtxQ = useQuery(firewallInstallContextQueryOptions())
|
||||||
|
const communitiesQ = useQuery(directoriesCommunitiesQueryOptions())
|
||||||
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 createRule = useCreateFirewallRule()
|
const createRule = useCreateFirewallRule()
|
||||||
const deleteRule = useDeleteFirewallRule()
|
const deleteRule = useDeleteFirewallRule()
|
||||||
|
|
||||||
|
const installCtx = installCtxQ.data
|
||||||
|
|
||||||
const [clientName, setClientName] = useState('web-01')
|
const [clientName, setClientName] = useState('web-01')
|
||||||
const [cpUrl, setCpUrl] = useState(() =>
|
const [cpUrl, setCpUrl] = useState(() =>
|
||||||
typeof window !== 'undefined' ? window.location.origin : 'https://api.example.com',
|
typeof window !== 'undefined' ? window.location.origin : 'https://api.example.com',
|
||||||
)
|
)
|
||||||
const [seed, setSeed] = useState('')
|
const [seed, setSeed] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (installCtx?.suggested_cp_url) {
|
||||||
|
setCpUrl(installCtx.suggested_cp_url)
|
||||||
|
}
|
||||||
|
if (installCtx?.bundle_seed) {
|
||||||
|
setSeed(installCtx.bundle_seed)
|
||||||
|
}
|
||||||
|
}, [installCtx?.bundle_seed, installCtx?.suggested_cp_url])
|
||||||
const [ruleAction, setRuleAction] = useState<'block' | 'accept'>('block')
|
const [ruleAction, setRuleAction] = useState<'block' | 'accept'>('block')
|
||||||
|
const [ruleCommunityId, setRuleCommunityId] = useState<string | null>(null)
|
||||||
const [ruleComment, setRuleComment] = useState('')
|
const [ruleComment, setRuleComment] = useState('')
|
||||||
|
|
||||||
|
const communities = communitiesQ.data?.items ?? []
|
||||||
|
|
||||||
const clients = clientsQ.data?.items ?? []
|
const clients = clientsQ.data?.items ?? []
|
||||||
const pending = clients.filter((c) => c.status === 'pending')
|
const pending = clients.filter((c) => c.status === 'pending')
|
||||||
const rules = rulesQ.data?.items ?? []
|
const rules = rulesQ.data?.items ?? []
|
||||||
@@ -64,7 +84,11 @@ function FirewallPage() {
|
|||||||
|
|
||||||
async function copyInstall() {
|
async function copyInstall() {
|
||||||
if (!seed.trim()) {
|
if (!seed.trim()) {
|
||||||
toast.error('Укажите bundle seed')
|
toast.error(
|
||||||
|
installCtx?.bundle_seed_configured === false
|
||||||
|
? 'На CP не задан EVOBGP_BUNDLE_SEED_HEX'
|
||||||
|
: 'Bundle seed недоступен (нужна роль operator)',
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -100,8 +124,9 @@ function FirewallPage() {
|
|||||||
<Info className="text-info" />
|
<Info className="text-info" />
|
||||||
<AlertTitle>Политика</AlertTitle>
|
<AlertTitle>Политика</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Только явный <strong>block</strong> добавляет IP в blocklist. Правила <strong>accept</strong> сами по себе
|
Правила сопоставляются с <strong>BGP community</strong> префиксов опубликованной revision.{' '}
|
||||||
не создают block all. Default — accept.
|
<strong>block</strong> добавляет префиксы community в kernel; <strong>accept</strong> — не блокирует.
|
||||||
|
Community «Все» — правило для любого community. Default без совпадений — accept.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
@@ -128,10 +153,18 @@ function FirewallPage() {
|
|||||||
<Input
|
<Input
|
||||||
id="fw-seed"
|
id="fw-seed"
|
||||||
type="password"
|
type="password"
|
||||||
|
readOnly
|
||||||
placeholder="EVOBGP_BUNDLE_SEED_HEX"
|
placeholder="EVOBGP_BUNDLE_SEED_HEX"
|
||||||
value={seed}
|
value={seed}
|
||||||
onChange={(e) => setSeed(e.target.value)}
|
className="font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{installCtxQ.isLoading
|
||||||
|
? 'Загрузка из control plane…'
|
||||||
|
: installCtx?.bundle_seed_configured
|
||||||
|
? 'Из переменной EVOBGP_BUNDLE_SEED_HEX на CP (docker compose / .env)'
|
||||||
|
: 'На CP не задан EVOBGP_BUNDLE_SEED_HEX — enroll невозможен'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<pre className="overflow-x-auto rounded-md border bg-muted/40 p-3 text-xs">{installCmd}</pre>
|
<pre className="overflow-x-auto rounded-md border bg-muted/40 p-3 text-xs">{installCmd}</pre>
|
||||||
@@ -154,7 +187,7 @@ function FirewallPage() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="rules" className="mt-4 space-y-4">
|
<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">
|
<div className="space-y-1">
|
||||||
<Label>Действие</Label>
|
<Label>Действие</Label>
|
||||||
<select
|
<select
|
||||||
@@ -166,18 +199,33 @@ function FirewallPage() {
|
|||||||
<option value="accept">accept</option>
|
<option value="accept">accept</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<CommunitySelect
|
||||||
className="max-w-xs"
|
id="fw-rule-community"
|
||||||
placeholder="Комментарий"
|
label="Community"
|
||||||
value={ruleComment}
|
value={ruleCommunityId}
|
||||||
onChange={(e) => setRuleComment(e.target.value)}
|
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
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
className="mb-0.5"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
createRule.mutate({
|
createRule.mutate({
|
||||||
scope: 'tenant',
|
scope: 'tenant',
|
||||||
action: ruleAction,
|
action: ruleAction,
|
||||||
|
community_id: ruleCommunityId,
|
||||||
comment: ruleComment,
|
comment: ruleComment,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -185,7 +233,11 @@ function FirewallPage() {
|
|||||||
Добавить правило
|
Добавить правило
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<RulesTable rules={rules} onDelete={(id) => deleteRule.mutate(id)} />
|
<RulesTable
|
||||||
|
rules={rules}
|
||||||
|
communities={communities}
|
||||||
|
onDelete={(id) => deleteRule.mutate(id)}
|
||||||
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="requests" className="mt-4">
|
<TabsContent value="requests" className="mt-4">
|
||||||
@@ -254,9 +306,11 @@ function ClientsTable({
|
|||||||
|
|
||||||
function RulesTable({
|
function RulesTable({
|
||||||
rules,
|
rules,
|
||||||
|
communities,
|
||||||
onDelete,
|
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
|
onDelete: (id: string) => void
|
||||||
}) {
|
}) {
|
||||||
if (rules.length === 0) {
|
if (rules.length === 0) {
|
||||||
@@ -268,6 +322,7 @@ function RulesTable({
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>#</TableHead>
|
<TableHead>#</TableHead>
|
||||||
<TableHead>Действие</TableHead>
|
<TableHead>Действие</TableHead>
|
||||||
|
<TableHead>Community</TableHead>
|
||||||
<TableHead>Комментарий</TableHead>
|
<TableHead>Комментарий</TableHead>
|
||||||
<TableHead />
|
<TableHead />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -279,6 +334,9 @@ function RulesTable({
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<StatusBadge status={r.action} label={r.action} />
|
<StatusBadge status={r.action} label={r.action} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell className="text-sm">
|
||||||
|
{r.community_id ? communityLabel(r.community_id, communities) : 'Все'}
|
||||||
|
</TableCell>
|
||||||
<TableCell>{r.comment || '—'}</TableCell>
|
<TableCell>{r.comment || '—'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button size="sm" variant="ghost" onClick={() => onDelete(r.id)}>
|
<Button size="sm" variant="ghost" onClick={() => onDelete(r.id)}>
|
||||||
|
|||||||
@@ -373,3 +373,10 @@ export type FirewallRule = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type FirewallRulesResponse = { items: FirewallRule[] }
|
export type FirewallRulesResponse = { items: FirewallRule[] }
|
||||||
|
|
||||||
|
export type FirewallInstallContext = {
|
||||||
|
bundle_seed: string
|
||||||
|
bundle_seed_configured: boolean
|
||||||
|
suggested_cp_url: string
|
||||||
|
install_sh_url: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ RUN apt-get update \
|
|||||||
FROM runtime-base AS runtime
|
FROM runtime-base AS runtime
|
||||||
ARG BIN=evobgp-api
|
ARG BIN=evobgp-api
|
||||||
COPY --from=build-all /out/${BIN} /usr/local/bin/evobgp
|
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
|
EXPOSE 8080
|
||||||
ENTRYPOINT ["/usr/local/bin/evobgp"]
|
ENTRYPOINT ["/usr/local/bin/evobgp"]
|
||||||
|
|
||||||
|
|||||||
+6
-3
@@ -10,11 +10,14 @@
|
|||||||
|
|
||||||
## Политика block/accept
|
## Политика block/accept
|
||||||
|
|
||||||
- **`block`** — добавить префиксы community в kernel blocklist.
|
- **`block`** — добавить префиксы выбранного BGP community в kernel blocklist.
|
||||||
- **`accept`** — не блокировать.
|
- **`accept`** — не блокировать префиксы этого community.
|
||||||
|
- **Community** — правило применяется к префиксам с этим `community_id` в опубликованной revision; пустое значение («Все») — ко всем communities.
|
||||||
- **Default** — accept (пустой blocklist без явных `block`).
|
- **Default** — accept (пустой blocklist без явных `block`).
|
||||||
|
|
||||||
Правила задаются на уровне tenant (по умолчанию) и per-server (overrides клиента). Client scope проверяется раньше tenant-default.
|
Порядок: сначала per-server overrides клиента, затем tenant-default. Для каждого community берётся первое подходящее правило по приоритету.
|
||||||
|
|
||||||
|
Справочник communities: Web UI → Справочники, или модули с привязкой community к префиксам.
|
||||||
|
|
||||||
## Установка на сервер
|
## Установка на сервер
|
||||||
|
|
||||||
|
|||||||
@@ -1608,6 +1608,22 @@ components:
|
|||||||
client_version:
|
client_version:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
FirewallInstallContext:
|
||||||
|
type: object
|
||||||
|
description: Контекст для one-liner установки firewall-клиента (только operator).
|
||||||
|
properties:
|
||||||
|
bundle_seed:
|
||||||
|
type: string
|
||||||
|
description: Значение EVOBGP_BUNDLE_SEED_HEX на control plane.
|
||||||
|
bundle_seed_configured:
|
||||||
|
type: boolean
|
||||||
|
suggested_cp_url:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
install_sh_url:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
|
||||||
FirewallRule:
|
FirewallRule:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -4364,6 +4380,23 @@ paths:
|
|||||||
default:
|
default:
|
||||||
$ref: "#/components/responses/DefaultProblem"
|
$ref: "#/components/responses/DefaultProblem"
|
||||||
|
|
||||||
|
/v1/firewall/install-context:
|
||||||
|
get:
|
||||||
|
tags: [Firewall]
|
||||||
|
summary: Install context for firewall one-liner (operator)
|
||||||
|
operationId: getFirewallInstallContext
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/FirewallInstallContext"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/Forbidden"
|
||||||
|
default:
|
||||||
|
$ref: "#/components/responses/DefaultProblem"
|
||||||
|
|
||||||
/v1/firewall/enroll:
|
/v1/firewall/enroll:
|
||||||
post:
|
post:
|
||||||
tags: [Firewall]
|
tags: [Firewall]
|
||||||
|
|||||||
@@ -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,10 +16,12 @@ import (
|
|||||||
|
|
||||||
"evobgp/internal/authkey"
|
"evobgp/internal/authkey"
|
||||||
"evobgp/internal/firewall"
|
"evobgp/internal/firewall"
|
||||||
|
"evobgp/internal/firewallscripts"
|
||||||
"evobgp/internal/store"
|
"evobgp/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) registerFirewallRoutes(m *http.ServeMux) {
|
func (s *Server) registerFirewallRoutes(m *http.ServeMux) {
|
||||||
|
m.HandleFunc("GET /firewall/install-context", s.handleFirewallInstallContext)
|
||||||
m.HandleFunc("GET /firewall/clients", s.handleListFirewallClients)
|
m.HandleFunc("GET /firewall/clients", s.handleListFirewallClients)
|
||||||
m.HandleFunc("GET /firewall/clients/{id}", s.handleGetFirewallClient)
|
m.HandleFunc("GET /firewall/clients/{id}", s.handleGetFirewallClient)
|
||||||
m.HandleFunc("GET /firewall/clients/{id}/preview", s.handleFirewallClientPreview)
|
m.HandleFunc("GET /firewall/clients/{id}/preview", s.handleFirewallClientPreview)
|
||||||
@@ -39,6 +41,39 @@ func (s *Server) registerFirewallRoutes(m *http.ServeMux) {
|
|||||||
m.HandleFunc("POST /firewall/heartbeat", s.handleFirewallHeartbeat)
|
m.HandleFunc("POST /firewall/heartbeat", s.handleFirewallHeartbeat)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleFirewallInstallContext(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "operator") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seed := strings.TrimSpace(s.bundleSeedHex)
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{
|
||||||
|
"bundle_seed": seed,
|
||||||
|
"bundle_seed_configured": seed != "",
|
||||||
|
"suggested_cp_url": requestBaseURL(r),
|
||||||
|
"install_sh_url": requestBaseURL(r) + "/v1/firewall/install.sh",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestBaseURL(r *http.Request) string {
|
||||||
|
scheme := "https"
|
||||||
|
if r.TLS == nil {
|
||||||
|
if xf := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")); xf != "" {
|
||||||
|
scheme = strings.ToLower(strings.Split(xf, ",")[0])
|
||||||
|
} else if strings.EqualFold(r.URL.Scheme, "http") {
|
||||||
|
scheme = "http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
host := strings.TrimSpace(r.Host)
|
||||||
|
if xf := strings.TrimSpace(r.Header.Get("X-Forwarded-Host")); xf != "" {
|
||||||
|
host = strings.TrimSpace(strings.Split(xf, ",")[0])
|
||||||
|
}
|
||||||
|
if host == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return scheme + "://" + host
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleFirewallEnrollPublic(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleFirewallEnrollPublic(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeProblem(w, http.StatusMethodNotAllowed, "Method Not Allowed", "POST required")
|
writeProblem(w, http.StatusMethodNotAllowed, "Method Not Allowed", "POST required")
|
||||||
@@ -125,8 +160,7 @@ func (s *Server) handleFirewallSyncScript(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) serveFirewallScript(w http.ResponseWriter, name string) {
|
func (s *Server) serveFirewallScript(w http.ResponseWriter, name string) {
|
||||||
path := filepath.Join("scripts", "firewall", name)
|
b, err := readFirewallScript(name)
|
||||||
b, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeProblem(w, http.StatusNotFound, "Not Found", "script not found")
|
writeProblem(w, http.StatusNotFound, "Not Found", "script not found")
|
||||||
return
|
return
|
||||||
@@ -136,6 +170,24 @@ func (s *Server) serveFirewallScript(w http.ResponseWriter, name string) {
|
|||||||
_, _ = w.Write(b)
|
_, _ = 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) {
|
func (s *Server) handleListFirewallClients(w http.ResponseWriter, r *http.Request) {
|
||||||
a, ok := authFromContext(r.Context())
|
a, ok := authFromContext(r.Context())
|
||||||
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
||||||
|
|||||||
@@ -119,6 +119,89 @@ 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 {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer srv.Close()
|
||||||
|
tenant, _, _, _, _ := srv.Store().DemoIDs()
|
||||||
|
mustSetTestAPIKeys(t, srv, "opkey|"+tenant+"|operator,vwkey|"+tenant+"|viewer")
|
||||||
|
|
||||||
|
ts := httptest.NewServer(srv.Handler())
|
||||||
|
defer ts.Close()
|
||||||
|
client := ts.Client()
|
||||||
|
|
||||||
|
reqOp, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/firewall/install-context", nil)
|
||||||
|
reqOp.Header.Set("Authorization", "Bearer opkey")
|
||||||
|
respOp, err := client.Do(reqOp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = respOp.Body.Close() }()
|
||||||
|
if respOp.StatusCode != http.StatusOK {
|
||||||
|
b, _ := io.ReadAll(respOp.Body)
|
||||||
|
t.Fatalf("operator install-context status=%d body=%s", respOp.StatusCode, b)
|
||||||
|
}
|
||||||
|
var ctx map[string]any
|
||||||
|
if err := json.NewDecoder(respOp.Body).Decode(&ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if seed, _ := ctx["bundle_seed"].(string); seed != testBundleSeed {
|
||||||
|
t.Fatalf("bundle_seed=%q want %q", seed, testBundleSeed)
|
||||||
|
}
|
||||||
|
if configured, _ := ctx["bundle_seed_configured"].(bool); !configured {
|
||||||
|
t.Fatal("bundle_seed_configured want true")
|
||||||
|
}
|
||||||
|
if url, _ := ctx["install_sh_url"].(string); !strings.HasSuffix(url, "/v1/firewall/install.sh") {
|
||||||
|
t.Fatalf("install_sh_url=%q", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
reqVw, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/firewall/install-context", nil)
|
||||||
|
reqVw.Header.Set("Authorization", "Bearer vwkey")
|
||||||
|
respVw, err := client.Do(reqVw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = respVw.Body.Close() }()
|
||||||
|
if respVw.StatusCode != http.StatusForbidden {
|
||||||
|
t.Fatalf("viewer install-context want 403 got %d", respVw.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)
|
||||||
|
|||||||
Reference in New Issue
Block a user