Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e15768b25b | ||
|
|
7b3f002e5f |
@@ -20,7 +20,10 @@ 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,
|
firewallInstallContextQueryOptions,
|
||||||
@@ -29,7 +32,17 @@ import {
|
|||||||
useCreateFirewallRule,
|
useCreateFirewallRule,
|
||||||
useDeleteFirewallRule,
|
useDeleteFirewallRule,
|
||||||
} from '@/queries/firewall'
|
} from '@/queries/firewall'
|
||||||
import type { FirewallClient } from '@/types/api'
|
import type { BgpCommunity, FirewallClient } from '@/types/api'
|
||||||
|
|
||||||
|
function httpsOrigin(origin: string): string {
|
||||||
|
try {
|
||||||
|
const u = new URL(origin)
|
||||||
|
u.protocol = 'https:'
|
||||||
|
return u.origin
|
||||||
|
} catch {
|
||||||
|
return origin.replace(/^http:/i, 'https:')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const Route = createFileRoute('/_auth/firewall')({
|
export const Route = createFileRoute('/_auth/firewall')({
|
||||||
component: FirewallPage,
|
component: FirewallPage,
|
||||||
@@ -37,6 +50,7 @@ export const Route = createFileRoute('/_auth/firewall')({
|
|||||||
|
|
||||||
function FirewallPage() {
|
function FirewallPage() {
|
||||||
const installCtxQ = useQuery(firewallInstallContextQueryOptions())
|
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()
|
||||||
@@ -47,21 +61,24 @@ function FirewallPage() {
|
|||||||
|
|
||||||
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' ? httpsOrigin(window.location.origin) : 'https://api.example.com',
|
||||||
)
|
)
|
||||||
const [seed, setSeed] = useState('')
|
const [seed, setSeed] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (installCtx?.suggested_cp_url) {
|
if (installCtx?.suggested_cp_url) {
|
||||||
setCpUrl(installCtx.suggested_cp_url)
|
setCpUrl(httpsOrigin(installCtx.suggested_cp_url))
|
||||||
}
|
}
|
||||||
if (installCtx?.bundle_seed) {
|
if (installCtx?.bundle_seed) {
|
||||||
setSeed(installCtx.bundle_seed)
|
setSeed(installCtx.bundle_seed)
|
||||||
}
|
}
|
||||||
}, [installCtx?.bundle_seed, installCtx?.suggested_cp_url])
|
}, [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 ?? []
|
||||||
@@ -117,8 +134,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>
|
||||||
|
|
||||||
@@ -179,7 +197,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
|
||||||
@@ -191,18 +209,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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -210,7 +243,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">
|
||||||
@@ -279,9 +316,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) {
|
||||||
@@ -293,6 +332,7 @@ function RulesTable({
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>#</TableHead>
|
<TableHead>#</TableHead>
|
||||||
<TableHead>Действие</TableHead>
|
<TableHead>Действие</TableHead>
|
||||||
|
<TableHead>Community</TableHead>
|
||||||
<TableHead>Комментарий</TableHead>
|
<TableHead>Комментарий</TableHead>
|
||||||
<TableHead />
|
<TableHead />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -304,6 +344,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)}>
|
||||||
|
|||||||
@@ -159,10 +159,18 @@ services:
|
|||||||
condition: service_started
|
condition: service_started
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
|
# Публичные firewall-эндпоинты — без WEBUI_IP_WHITELIST (установка с произвольных серверов).
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.rule=Host(`${WEBUI_DOMAIN}`) && (Path(`/v1/firewall/install.sh`) || Path(`/v1/firewall/sync-script`) || PathPrefix(`/v1/firewall/enroll`))
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.entrypoints=websecure
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.tls=true
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.priority=100
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.service=evobgp-web
|
||||||
- traefik.http.routers.evobgp-web.rule=Host(`${WEBUI_DOMAIN}`)
|
- traefik.http.routers.evobgp-web.rule=Host(`${WEBUI_DOMAIN}`)
|
||||||
- traefik.http.routers.evobgp-web.entrypoints=websecure
|
- traefik.http.routers.evobgp-web.entrypoints=websecure
|
||||||
- traefik.http.routers.evobgp-web.tls=true
|
- traefik.http.routers.evobgp-web.tls=true
|
||||||
- traefik.http.routers.evobgp-web.tls.certresolver=letsencrypt
|
- traefik.http.routers.evobgp-web.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.routers.evobgp-web.priority=10
|
||||||
- traefik.http.routers.evobgp-web.middlewares=webui-ipwhitelist@docker
|
- traefik.http.routers.evobgp-web.middlewares=webui-ipwhitelist@docker
|
||||||
- traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST}
|
- traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST}
|
||||||
- traefik.http.services.evobgp-web.loadbalancer.server.port=80
|
- traefik.http.services.evobgp-web.loadbalancer.server.port=80
|
||||||
|
|||||||
@@ -247,10 +247,18 @@ services:
|
|||||||
- evobgp-all
|
- evobgp-all
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
|
# Публичные firewall-эндпоинты — без WEBUI_IP_WHITELIST (установка с произвольных серверов).
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.rule=Host(`${WEBUI_DOMAIN}`) && (Path(`/v1/firewall/install.sh`) || Path(`/v1/firewall/sync-script`) || PathPrefix(`/v1/firewall/enroll`))
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.entrypoints=websecure
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.tls=true
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.priority=100
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.service=evobgp-web
|
||||||
- traefik.http.routers.evobgp-web.rule=Host(`${WEBUI_DOMAIN}`)
|
- traefik.http.routers.evobgp-web.rule=Host(`${WEBUI_DOMAIN}`)
|
||||||
- traefik.http.routers.evobgp-web.entrypoints=websecure
|
- traefik.http.routers.evobgp-web.entrypoints=websecure
|
||||||
- traefik.http.routers.evobgp-web.tls=true
|
- traefik.http.routers.evobgp-web.tls=true
|
||||||
- traefik.http.routers.evobgp-web.tls.certresolver=letsencrypt
|
- traefik.http.routers.evobgp-web.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.routers.evobgp-web.priority=10
|
||||||
- traefik.http.routers.evobgp-web.middlewares=webui-ipwhitelist@docker
|
- traefik.http.routers.evobgp-web.middlewares=webui-ipwhitelist@docker
|
||||||
- traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST}
|
- traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST}
|
||||||
- traefik.http.services.evobgp-web.loadbalancer.server.port=80
|
- traefik.http.services.evobgp-web.loadbalancer.server.port=80
|
||||||
|
|||||||
@@ -162,10 +162,18 @@ services:
|
|||||||
condition: service_started
|
condition: service_started
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
|
# Публичные firewall-эндпоинты — без WEBUI_IP_WHITELIST (установка с произвольных серверов).
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.rule=Host(`${WEBUI_DOMAIN}`) && (Path(`/v1/firewall/install.sh`) || Path(`/v1/firewall/sync-script`) || PathPrefix(`/v1/firewall/enroll`))
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.entrypoints=websecure
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.tls=true
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.priority=100
|
||||||
|
- traefik.http.routers.evobgp-firewall-public.service=evobgp-web
|
||||||
- traefik.http.routers.evobgp-web.rule=Host(`${WEBUI_DOMAIN}`)
|
- traefik.http.routers.evobgp-web.rule=Host(`${WEBUI_DOMAIN}`)
|
||||||
- traefik.http.routers.evobgp-web.entrypoints=websecure
|
- traefik.http.routers.evobgp-web.entrypoints=websecure
|
||||||
- traefik.http.routers.evobgp-web.tls=true
|
- traefik.http.routers.evobgp-web.tls=true
|
||||||
- traefik.http.routers.evobgp-web.tls.certresolver=letsencrypt
|
- traefik.http.routers.evobgp-web.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.routers.evobgp-web.priority=10
|
||||||
- traefik.http.routers.evobgp-web.middlewares=webui-ipwhitelist@docker
|
- traefik.http.routers.evobgp-web.middlewares=webui-ipwhitelist@docker
|
||||||
- traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST}
|
- traefik.http.middlewares.webui-ipwhitelist.ipallowlist.sourcerange=${WEBUI_IP_WHITELIST}
|
||||||
- traefik.http.services.evobgp-web.loadbalancer.server.port=80
|
- traefik.http.services.evobgp-web.loadbalancer.server.port=80
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ server {
|
|||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
location = /metrics {
|
location = /metrics {
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|
||||||
|
|||||||
+8
-3
@@ -10,14 +10,19 @@
|
|||||||
|
|
||||||
## Политика 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 к префиксам.
|
||||||
|
|
||||||
## Установка на сервер
|
## Установка на сервер
|
||||||
|
|
||||||
|
Публичные URL (без API-ключа, вне `WEBUI_IP_WHITELIST` Traefik): `GET /v1/firewall/install.sh`, `GET /v1/firewall/sync-script`, `POST /v1/firewall/enroll`. Всегда **HTTPS**.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://<api>/v1/firewall/install.sh | \
|
curl -fsSL https://<api>/v1/firewall/install.sh | \
|
||||||
EVOBGP_CP_URL=https://<api> \
|
EVOBGP_CP_URL=https://<api> \
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ docker compose --env-file .env --env-file .env.web-sec --profile microvps-full u
|
|||||||
- `http://<WEBUI_DOMAIN>` должен редиректить на `https://<WEBUI_DOMAIN>`;
|
- `http://<WEBUI_DOMAIN>` должен редиректить на `https://<WEBUI_DOMAIN>`;
|
||||||
- с IP из `WEBUI_IP_WHITELIST` UI доступен по HTTPS;
|
- с IP из `WEBUI_IP_WHITELIST` UI доступен по HTTPS;
|
||||||
- с неразрешенного IP Traefik вернет `403`.
|
- с неразрешенного IP Traefik вернет `403`.
|
||||||
|
- исключение: `GET /v1/firewall/install.sh`, `GET /v1/firewall/sync-script`, `POST /v1/firewall/enroll` — публичные, без whitelist (см. [firewall.md](firewall.md)).
|
||||||
|
|
||||||
Health API: `http://<IP>:8080/v1/health`.
|
Health API: `http://<IP>:8080/v1/health`.
|
||||||
|
|
||||||
|
|||||||
@@ -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/authkey"
|
||||||
"evobgp/internal/firewall"
|
"evobgp/internal/firewall"
|
||||||
|
"evobgp/internal/firewallscripts"
|
||||||
"evobgp/internal/store"
|
"evobgp/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,20 +50,13 @@ func (s *Server) handleFirewallInstallContext(w http.ResponseWriter, r *http.Req
|
|||||||
writeJSON(w, http.StatusOK, map[string]any{
|
writeJSON(w, http.StatusOK, map[string]any{
|
||||||
"bundle_seed": seed,
|
"bundle_seed": seed,
|
||||||
"bundle_seed_configured": seed != "",
|
"bundle_seed_configured": seed != "",
|
||||||
"suggested_cp_url": requestBaseURL(r),
|
"suggested_cp_url": publicHTTPSBaseURL(r),
|
||||||
"install_sh_url": requestBaseURL(r) + "/v1/firewall/install.sh",
|
"install_sh_url": publicHTTPSBaseURL(r) + "/v1/firewall/install.sh",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestBaseURL(r *http.Request) string {
|
// publicHTTPSBaseURL is the external HTTPS origin for firewall install/enroll links.
|
||||||
scheme := "https"
|
func publicHTTPSBaseURL(r *http.Request) string {
|
||||||
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)
|
host := strings.TrimSpace(r.Host)
|
||||||
if xf := strings.TrimSpace(r.Header.Get("X-Forwarded-Host")); xf != "" {
|
if xf := strings.TrimSpace(r.Header.Get("X-Forwarded-Host")); xf != "" {
|
||||||
host = strings.TrimSpace(strings.Split(xf, ",")[0])
|
host = strings.TrimSpace(strings.Split(xf, ",")[0])
|
||||||
@@ -70,7 +64,11 @@ func requestBaseURL(r *http.Request) string {
|
|||||||
if host == "" {
|
if host == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return scheme + "://" + host
|
return "https://" + host
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestBaseURL(r *http.Request) string {
|
||||||
|
return publicHTTPSBaseURL(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleFirewallEnrollPublic(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleFirewallEnrollPublic(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -159,8 +157,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
|
||||||
@@ -170,6 +167,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,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) {
|
func TestFirewallInstallContext(t *testing.T) {
|
||||||
srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
|
srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -153,7 +186,10 @@ func TestFirewallInstallContext(t *testing.T) {
|
|||||||
if configured, _ := ctx["bundle_seed_configured"].(bool); !configured {
|
if configured, _ := ctx["bundle_seed_configured"].(bool); !configured {
|
||||||
t.Fatal("bundle_seed_configured want true")
|
t.Fatal("bundle_seed_configured want true")
|
||||||
}
|
}
|
||||||
if url, _ := ctx["install_sh_url"].(string); !strings.HasSuffix(url, "/v1/firewall/install.sh") {
|
if url, _ := ctx["suggested_cp_url"].(string); !strings.HasPrefix(url, "https://") {
|
||||||
|
t.Fatalf("suggested_cp_url=%q want https", url)
|
||||||
|
}
|
||||||
|
if url, _ := ctx["install_sh_url"].(string); !strings.HasPrefix(url, "https://") || !strings.HasSuffix(url, "/v1/firewall/install.sh") {
|
||||||
t.Fatalf("install_sh_url=%q", url)
|
t.Fatalf("install_sh_url=%q", url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user