Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e51999c908 | ||
|
|
b7f7669685 |
@@ -22,6 +22,11 @@ const STATUS_VARIANT: Record<string, BadgeVariant> = {
|
|||||||
stale: 'warning',
|
stale: 'warning',
|
||||||
warning: 'warning',
|
warning: 'warning',
|
||||||
mismatch: 'warning',
|
mismatch: 'warning',
|
||||||
|
pending: 'warning',
|
||||||
|
approved: 'success',
|
||||||
|
revoked: 'destructive',
|
||||||
|
block: 'destructive',
|
||||||
|
accept: 'success',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatusBadge({ status, label }: { status: string; label?: string }) {
|
export function StatusBadge({ status, label }: { status: string; label?: string }) {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { queryOptions, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { queryOptions, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import { apiJSON } from '@/lib/api-client'
|
import { apiJSON } from '@/lib/api-client'
|
||||||
import type {
|
import type {
|
||||||
FirewallClient,
|
FirewallClient,
|
||||||
@@ -51,8 +53,23 @@ export function useApproveFirewallClient() {
|
|||||||
mutationFn: (id: string) =>
|
mutationFn: (id: string) =>
|
||||||
apiJSON<FirewallClient>(`/v1/firewall/clients/${id}/approve`, { method: 'POST' }),
|
apiJSON<FirewallClient>(`/v1/firewall/clients/${id}/approve`, { method: 'POST' }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
toast.success('Клиент одобрен')
|
||||||
void qc.invalidateQueries({ queryKey: firewallKeys.clients() })
|
void qc.invalidateQueries({ queryKey: firewallKeys.clients() })
|
||||||
},
|
},
|
||||||
|
onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось одобрить'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRevokeFirewallClient() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) =>
|
||||||
|
apiJSON<{ status: string }>(`/v1/firewall/clients/${id}/revoke`, { method: 'POST' }),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Клиент отключён')
|
||||||
|
void qc.invalidateQueries({ queryKey: firewallKeys.clients() })
|
||||||
|
},
|
||||||
|
onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось отклонить'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@evobgp/ui/components/table'
|
} from '@evobgp/ui/components/table'
|
||||||
|
|
||||||
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||||
import { PageHeader } from '@/components/page-header'
|
import { PageHeader } from '@/components/page-header'
|
||||||
import { CommunitySelect } from '@/components/modules/community-select'
|
import { CommunitySelect } from '@/components/modules/community-select'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
@@ -31,6 +32,7 @@ import {
|
|||||||
useApproveFirewallClient,
|
useApproveFirewallClient,
|
||||||
useCreateFirewallRule,
|
useCreateFirewallRule,
|
||||||
useDeleteFirewallRule,
|
useDeleteFirewallRule,
|
||||||
|
useRevokeFirewallClient,
|
||||||
} from '@/queries/firewall'
|
} from '@/queries/firewall'
|
||||||
import type { BgpCommunity, FirewallClient } from '@/types/api'
|
import type { BgpCommunity, FirewallClient } from '@/types/api'
|
||||||
|
|
||||||
@@ -54,6 +56,7 @@ function FirewallPage() {
|
|||||||
const clientsQ = useQuery(firewallClientsQueryOptions())
|
const clientsQ = useQuery(firewallClientsQueryOptions())
|
||||||
const rulesQ = useQuery(firewallRulesQueryOptions('tenant'))
|
const rulesQ = useQuery(firewallRulesQueryOptions('tenant'))
|
||||||
const approve = useApproveFirewallClient()
|
const approve = useApproveFirewallClient()
|
||||||
|
const revoke = useRevokeFirewallClient()
|
||||||
const createRule = useCreateFirewallRule()
|
const createRule = useCreateFirewallRule()
|
||||||
const deleteRule = useDeleteFirewallRule()
|
const deleteRule = useDeleteFirewallRule()
|
||||||
|
|
||||||
@@ -193,7 +196,13 @@ function FirewallPage() {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="clients" className="mt-4">
|
<TabsContent value="clients" className="mt-4">
|
||||||
<ClientsTable clients={clients} onApprove={(id) => approve.mutate(id)} />
|
<ClientsTable
|
||||||
|
clients={clients}
|
||||||
|
onApprove={(id) => approve.mutate(id)}
|
||||||
|
onReject={(id) => revoke.mutate(id)}
|
||||||
|
approvePending={approve.isPending}
|
||||||
|
rejectPending={revoke.isPending}
|
||||||
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="rules" className="mt-4 space-y-4">
|
<TabsContent value="rules" className="mt-4 space-y-4">
|
||||||
@@ -254,6 +263,9 @@ function FirewallPage() {
|
|||||||
<ClientsTable
|
<ClientsTable
|
||||||
clients={pending}
|
clients={pending}
|
||||||
onApprove={(id) => approve.mutate(id)}
|
onApprove={(id) => approve.mutate(id)}
|
||||||
|
onReject={(id) => revoke.mutate(id)}
|
||||||
|
approvePending={approve.isPending}
|
||||||
|
rejectPending={revoke.isPending}
|
||||||
emptyTitle="Нет pending-запросов"
|
emptyTitle="Нет pending-запросов"
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@@ -265,10 +277,16 @@ function FirewallPage() {
|
|||||||
function ClientsTable({
|
function ClientsTable({
|
||||||
clients,
|
clients,
|
||||||
onApprove,
|
onApprove,
|
||||||
|
onReject,
|
||||||
|
approvePending = false,
|
||||||
|
rejectPending = false,
|
||||||
emptyTitle = 'Нет клиентов',
|
emptyTitle = 'Нет клиентов',
|
||||||
}: {
|
}: {
|
||||||
clients: FirewallClient[]
|
clients: FirewallClient[]
|
||||||
onApprove: (id: string) => void
|
onApprove: (id: string) => void
|
||||||
|
onReject: (id: string) => void
|
||||||
|
approvePending?: boolean
|
||||||
|
rejectPending?: boolean
|
||||||
emptyTitle?: string
|
emptyTitle?: string
|
||||||
}) {
|
}) {
|
||||||
if (clients.length === 0) {
|
if (clients.length === 0) {
|
||||||
@@ -301,11 +319,56 @@ function ClientsTable({
|
|||||||
{c.last_apply_prefix_count != null ? ` (${c.last_apply_prefix_count})` : ''}
|
{c.last_apply_prefix_count != null ? ` (${c.last_apply_prefix_count})` : ''}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{c.status === 'pending' ? (
|
<div className="flex justify-end gap-2">
|
||||||
<Button size="sm" variant="outline" onClick={() => onApprove(c.id)}>
|
{c.status === 'pending' ? (
|
||||||
Approve
|
<>
|
||||||
</Button>
|
<Button
|
||||||
) : null}
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={approvePending}
|
||||||
|
onClick={() => onApprove(c.id)}
|
||||||
|
>
|
||||||
|
Одобрить
|
||||||
|
</Button>
|
||||||
|
<ConfirmDialog
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="text-destructive"
|
||||||
|
disabled={rejectPending}
|
||||||
|
>
|
||||||
|
Отклонить
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
title="Отклонить запрос?"
|
||||||
|
description={`${c.name}${c.hostname ? ` (${c.hostname})` : ''} — токен перестанет работать.`}
|
||||||
|
confirmLabel="Отклонить"
|
||||||
|
destructive
|
||||||
|
onConfirm={() => onReject(c.id)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{c.status === 'approved' ? (
|
||||||
|
<ConfirmDialog
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="text-destructive"
|
||||||
|
disabled={rejectPending}
|
||||||
|
>
|
||||||
|
Отозвать
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
title="Отозвать клиент?"
|
||||||
|
description={`${c.name} — blocklist перестанет отдаваться, токен будет недействителен.`}
|
||||||
|
confirmLabel="Отозвать"
|
||||||
|
destructive
|
||||||
|
onConfirm={() => onReject(c.id)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -4472,6 +4472,31 @@ paths:
|
|||||||
default:
|
default:
|
||||||
$ref: "#/components/responses/DefaultProblem"
|
$ref: "#/components/responses/DefaultProblem"
|
||||||
|
|
||||||
|
/v1/firewall/clients/{id}/revoke:
|
||||||
|
post:
|
||||||
|
tags: [Firewall]
|
||||||
|
summary: Reject pending or revoke approved client
|
||||||
|
operationId: revokeFirewallClient
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ResourceId"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Revoked
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [revoked]
|
||||||
|
default:
|
||||||
|
$ref: "#/components/responses/DefaultProblem"
|
||||||
|
|
||||||
/v1/firewall/rules:
|
/v1/firewall/rules:
|
||||||
get:
|
get:
|
||||||
tags: [Firewall]
|
tags: [Firewall]
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ try_fetch_blocklist() {
|
|||||||
|
|
||||||
parse_blocklist_file() {
|
parse_blocklist_file() {
|
||||||
local f="$1"
|
local f="$1"
|
||||||
|
if [[ ! -s "$f" ]]; then
|
||||||
|
log "blocklist file empty: $f"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
if command -v jq >/dev/null 2>&1; then
|
if command -v jq >/dev/null 2>&1; then
|
||||||
HASH=$(jq -r '.hash // empty' "$f")
|
HASH=$(jq -r '.hash // empty' "$f")
|
||||||
TOTAL=$(jq -r '.total // 0' "$f")
|
TOTAL=$(jq -r '.total // 0' "$f")
|
||||||
@@ -99,6 +103,35 @@ PY
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nft_join_elements() {
|
||||||
|
local out="" p
|
||||||
|
for p in "$@"; do
|
||||||
|
if [[ -n "$out" ]]; then
|
||||||
|
out+=", "
|
||||||
|
fi
|
||||||
|
out+="$p"
|
||||||
|
done
|
||||||
|
printf '%s' "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
nft_add_v4_chunk() {
|
||||||
|
local table=$1 name=$2
|
||||||
|
shift 2
|
||||||
|
local joined
|
||||||
|
joined=$(nft_join_elements "$@")
|
||||||
|
if nft add element "$table" "$name" v4 "{ ${joined} }" 2>>"$LOG_FILE"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
log "nft batch add failed (chunk=$#), retrying one-by-one"
|
||||||
|
local p ok=0
|
||||||
|
for p in "$@"; do
|
||||||
|
if nft add element "$table" "$name" v4 "{ $p }" 2>>"$LOG_FILE"; then
|
||||||
|
ok=$((ok + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[[ "$ok" -gt 0 ]]
|
||||||
|
}
|
||||||
|
|
||||||
if ! try_fetch_blocklist; then
|
if ! try_fetch_blocklist; then
|
||||||
log "all endpoints failed"
|
log "all endpoints failed"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -108,6 +141,7 @@ HASH=""
|
|||||||
TOTAL=0
|
TOTAL=0
|
||||||
PREFIXES=()
|
PREFIXES=()
|
||||||
parse_blocklist_file "$PREFIX_FILE"
|
parse_blocklist_file "$PREFIX_FILE"
|
||||||
|
log "blocklist bytes=$(wc -c <"$PREFIX_FILE" | tr -d ' ') parsed=${#PREFIXES[@]} api_total=${TOTAL:-0}"
|
||||||
|
|
||||||
if [[ -z "${TOTAL// }" ]]; then
|
if [[ -z "${TOTAL// }" ]]; then
|
||||||
TOTAL=${#PREFIXES[@]}
|
TOTAL=${#PREFIXES[@]}
|
||||||
@@ -135,17 +169,16 @@ apply_nft() {
|
|||||||
|
|
||||||
if ((${#v4[@]})); then
|
if ((${#v4[@]})); then
|
||||||
local batch=()
|
local batch=()
|
||||||
local chunk=128
|
local chunk=64
|
||||||
local n
|
|
||||||
for p in "${v4[@]}"; do
|
for p in "${v4[@]}"; do
|
||||||
batch+=("$p")
|
batch+=("$p")
|
||||||
if ((${#batch[@]} >= chunk)); then
|
if ((${#batch[@]} >= chunk)); then
|
||||||
nft add element "$table" "$name" v4 "{ $(IFS=,; echo "${batch[*]}") }"
|
nft_add_v4_chunk "$table" "$name" "${batch[@]}" || log "nft chunk add partial failure"
|
||||||
batch=()
|
batch=()
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if ((${#batch[@]})); then
|
if ((${#batch[@]})); then
|
||||||
nft add element "$table" "$name" v4 "{ $(IFS=,; echo "${batch[*]}") }"
|
nft_add_v4_chunk "$table" "$name" "${batch[@]}" || log "nft tail chunk add partial failure"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -205,6 +205,74 @@ func TestFirewallInstallContext(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFirewallRevokePendingClient(t *testing.T) {
|
||||||
|
srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer srv.Close()
|
||||||
|
tenant, _, _, _, _ := srv.Store().DemoIDs()
|
||||||
|
mustSetTestAPIKeys(t, srv, "opkey|"+tenant+"|operator")
|
||||||
|
|
||||||
|
ts := httptest.NewServer(srv.Handler())
|
||||||
|
defer ts.Close()
|
||||||
|
client := ts.Client()
|
||||||
|
|
||||||
|
tok := "evobgp_fw_revoketest123456789012345678901"
|
||||||
|
enrollBody := `{"name":"reject-me","hostname":"test.local","client_token":"` + tok + `","client_version":"test/1"}`
|
||||||
|
reqEnroll, _ := http.NewRequest(http.MethodPost, ts.URL+"/v1/firewall/enroll", strings.NewReader(enrollBody))
|
||||||
|
reqEnroll.Header.Set("Content-Type", "application/json")
|
||||||
|
reqEnroll.Header.Set("X-EvoBGP-Seed", testBundleSeed)
|
||||||
|
respEnroll, err := client.Do(reqEnroll)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = respEnroll.Body.Close() }()
|
||||||
|
if respEnroll.StatusCode != http.StatusCreated {
|
||||||
|
b, _ := io.ReadAll(respEnroll.Body)
|
||||||
|
t.Fatalf("enroll status=%d body=%s", respEnroll.StatusCode, b)
|
||||||
|
}
|
||||||
|
var enroll map[string]any
|
||||||
|
if err := json.NewDecoder(respEnroll.Body).Decode(&enroll); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
clientID, _ := enroll["client_id"].(string)
|
||||||
|
if clientID == "" {
|
||||||
|
t.Fatal("missing client_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
reqRevoke, _ := http.NewRequest(http.MethodPost, ts.URL+"/v1/firewall/clients/"+clientID+"/revoke", nil)
|
||||||
|
reqRevoke.Header.Set("Authorization", "Bearer opkey")
|
||||||
|
respRevoke, err := client.Do(reqRevoke)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = respRevoke.Body.Close() }()
|
||||||
|
if respRevoke.StatusCode != http.StatusOK {
|
||||||
|
b, _ := io.ReadAll(respRevoke.Body)
|
||||||
|
t.Fatalf("revoke status=%d body=%s", respRevoke.StatusCode, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := srv.Store().GetFirewallClient(tenant, clientID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.Status != "revoked" {
|
||||||
|
t.Fatalf("status=%q want revoked", got.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
reqBlock, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/firewall/blocklist", nil)
|
||||||
|
reqBlock.Header.Set("Authorization", "Bearer "+tok)
|
||||||
|
respBlock, err := client.Do(reqBlock)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = respBlock.Body.Close() }()
|
||||||
|
if respBlock.StatusCode != http.StatusForbidden {
|
||||||
|
t.Fatalf("revoked blocklist want 403 got %d", respBlock.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFirewallTokenHashMatchesAuthkey(t *testing.T) {
|
func TestFirewallTokenHashMatchesAuthkey(t *testing.T) {
|
||||||
tok := "evobgp_fw_sample"
|
tok := "evobgp_fw_sample"
|
||||||
h := authkey.HashToken(tok)
|
h := authkey.HashToken(tok)
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ try_fetch_blocklist() {
|
|||||||
|
|
||||||
parse_blocklist_file() {
|
parse_blocklist_file() {
|
||||||
local f="$1"
|
local f="$1"
|
||||||
|
if [[ ! -s "$f" ]]; then
|
||||||
|
log "blocklist file empty: $f"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
if command -v jq >/dev/null 2>&1; then
|
if command -v jq >/dev/null 2>&1; then
|
||||||
HASH=$(jq -r '.hash // empty' "$f")
|
HASH=$(jq -r '.hash // empty' "$f")
|
||||||
TOTAL=$(jq -r '.total // 0' "$f")
|
TOTAL=$(jq -r '.total // 0' "$f")
|
||||||
@@ -99,6 +103,35 @@ PY
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nft_join_elements() {
|
||||||
|
local out="" p
|
||||||
|
for p in "$@"; do
|
||||||
|
if [[ -n "$out" ]]; then
|
||||||
|
out+=", "
|
||||||
|
fi
|
||||||
|
out+="$p"
|
||||||
|
done
|
||||||
|
printf '%s' "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
nft_add_v4_chunk() {
|
||||||
|
local table=$1 name=$2
|
||||||
|
shift 2
|
||||||
|
local joined
|
||||||
|
joined=$(nft_join_elements "$@")
|
||||||
|
if nft add element "$table" "$name" v4 "{ ${joined} }" 2>>"$LOG_FILE"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
log "nft batch add failed (chunk=$#), retrying one-by-one"
|
||||||
|
local p ok=0
|
||||||
|
for p in "$@"; do
|
||||||
|
if nft add element "$table" "$name" v4 "{ $p }" 2>>"$LOG_FILE"; then
|
||||||
|
ok=$((ok + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[[ "$ok" -gt 0 ]]
|
||||||
|
}
|
||||||
|
|
||||||
if ! try_fetch_blocklist; then
|
if ! try_fetch_blocklist; then
|
||||||
log "all endpoints failed"
|
log "all endpoints failed"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -108,6 +141,7 @@ HASH=""
|
|||||||
TOTAL=0
|
TOTAL=0
|
||||||
PREFIXES=()
|
PREFIXES=()
|
||||||
parse_blocklist_file "$PREFIX_FILE"
|
parse_blocklist_file "$PREFIX_FILE"
|
||||||
|
log "blocklist bytes=$(wc -c <"$PREFIX_FILE" | tr -d ' ') parsed=${#PREFIXES[@]} api_total=${TOTAL:-0}"
|
||||||
|
|
||||||
if [[ -z "${TOTAL// }" ]]; then
|
if [[ -z "${TOTAL// }" ]]; then
|
||||||
TOTAL=${#PREFIXES[@]}
|
TOTAL=${#PREFIXES[@]}
|
||||||
@@ -135,17 +169,16 @@ apply_nft() {
|
|||||||
|
|
||||||
if ((${#v4[@]})); then
|
if ((${#v4[@]})); then
|
||||||
local batch=()
|
local batch=()
|
||||||
local chunk=128
|
local chunk=64
|
||||||
local n
|
|
||||||
for p in "${v4[@]}"; do
|
for p in "${v4[@]}"; do
|
||||||
batch+=("$p")
|
batch+=("$p")
|
||||||
if ((${#batch[@]} >= chunk)); then
|
if ((${#batch[@]} >= chunk)); then
|
||||||
nft add element "$table" "$name" v4 "{ $(IFS=,; echo "${batch[*]}") }"
|
nft_add_v4_chunk "$table" "$name" "${batch[@]}" || log "nft chunk add partial failure"
|
||||||
batch=()
|
batch=()
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if ((${#batch[@]})); then
|
if ((${#batch[@]})); then
|
||||||
nft add element "$table" "$name" v4 "{ $(IFS=,; echo "${batch[*]}") }"
|
nft_add_v4_chunk "$table" "$name" "${batch[@]}" || log "nft tail chunk add partial failure"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user