"use client" import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react" import Link from "next/link" import { useRouter, useSearchParams } from "next/navigation" import { PageHeader } from "@/components/page-header" import { servers as mockServers } from "@/lib/data" import type { Server } from "@/lib/data" import { DataPageCard } from "@/components/data-page-card" import { DataPageToolbar } from "@/components/data-page-toolbar" import { WireguardDataGrid, type WgIfaceWithServer, } from "@/components/data-grids/wireguard-data-grid" import { WireguardPeersGrid, type WgPeerRow, } from "@/components/data-grids/wireguard-peers-grid" import { Button } from "@/components/ui/button" import { Alert, AlertDescription, AlertTitle, } from "@/components/reui/alert" import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid" import { OpsPanel } from "@/components/ops-panel" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { useDataSource } from "@/lib/data-source" import { requestJson } from "@/shared/api/http-client" import { createWireGuardInterface, createWireGuardPeer, deleteWireGuardInterface, deleteWireGuardPeer, exportWireGuard, importWireGuard, listWireGuard, patchWireGuardInterface, } from "@/shared/api/wireguard" import type { WgIfaceDto } from "@mmapp/contracts/wireguard" import { WgCreateSheet, type WgCreateFormState } from "@/components/wireguard/wg-create-sheet" import { WgImportSheet } from "@/components/wireguard/wg-import-sheet" import { WgExportSheet } from "@/components/wireguard/wg-export-sheet" import { WgPeerSheet, type WgPeerFormState } from "@/components/wireguard/wg-peer-sheet" import { ServerRailLayout, ServerRailMobileButton } from "@/components/server-rail-layout" import { ALL_SERVERS_ID, type ServerTileItem, } from "@/components/server-tile-rail" import { toast } from "sonner" import { selectWgExportPeer } from "@/lib/wg-export-peer" import { ConfigHistorySheet } from "@/components/config-history-sheet" import type { ConfigRevisionDto } from "@/lib/config-revisions" import { ShieldCheckIcon, PlusIcon, KeyRoundIcon, UsersIcon, ActivityIcon, RefreshCwIcon, UploadIcon, InfoIcon, Trash2Icon, CodeXmlIcon, AlertCircleIcon, HistoryIcon, } from "lucide-react" type WgWorkspaceTab = "interfaces" | "peers" | "cli" type WgStatusFilter = "all" | "up" | "down" type WgFailure = { serverId: string; serverName?: string; error: string } type PendingDelete = | { kind: "iface"; iface: WgIfaceWithServer } | { kind: "peer"; iface: WgIfaceWithServer; peerId: string } function collectMockInterfaces(): WgIfaceWithServer[] { const result: WgIfaceWithServer[] = [] for (const srv of mockServers) { for (const wg of srv.wireGuardIfaces ?? []) { result.push({ ...wg, serverId: srv.id, serverName: srv.name, serverCountry: srv.country, }) } } return result } function dtoToRow(d: WgIfaceDto): WgIfaceWithServer { return { id: d.id, rosId: d.rosId, name: d.name, listenPort: d.listenPort, mtu: d.mtu, publicKey: d.publicKey, privateKey: d.privateKey, address: d.address, peers: d.peers.map((p) => ({ id: p.id, rosId: p.rosId, publicKey: p.publicKey, allowedIps: p.allowedIps, endpoint: p.endpoint, latestHandshake: p.latestHandshake, transferRx: p.transferRx, transferTx: p.transferTx, persistentKeepalive: p.persistentKeepalive, persistent: p.persistent, comment: p.comment, disabled: p.disabled, name: p.name, clientAddress: p.clientAddress, clientDns: p.clientDns, clientEndpoint: p.clientEndpoint, })), comment: d.comment, enabled: d.enabled, status: d.status, serverId: d.serverId, serverName: d.serverName, serverCountry: d.serverCountry ?? "UN", } } interface BackendServer { id: number name: string host: string country: string type?: Server["type"] enabled: boolean status?: Server["status"] latency?: number | null asn?: string } function mapBackendServer(s: BackendServer): Server { return { id: String(s.id), name: s.name || s.host, host: s.host, model: "—", os: "—", site: "", country: s.country || "UN", asn: s.asn ?? "", type: s.type ?? "exit-node", enabled: s.enabled, status: s.status ?? "online", latency: s.latency ?? null, sessions: 0, } } function parseEndpoint(endpoint: string): { address?: string; port?: number } { const t = endpoint.trim() if (!t) return {} const idx = t.lastIndexOf(":") if (idx <= 0) return { address: t } return { address: t.slice(0, idx), port: Number.parseInt(t.slice(idx + 1), 10) || undefined, } } function peerRowId(iface: WgIfaceWithServer, peer: WgIfaceWithServer["peers"][number], index: number): string { return peer.id ?? peer.rosId ?? `${iface.id}-${peer.publicKey}-${index}` } function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterfacesHref, onSelectServer }: { selectedServerId: string selectedInterfaceId: string | null allInterfacesHref: string onSelectServer: (id: string) => void }) { const { mode, backendUrl } = useDataSource() const isLive = mode === "live" const [liveIfaces, setLiveIfaces] = useState([]) const [liveServers, setLiveServers] = useState([]) const [failures, setFailures] = useState([]) const [loading, setLoading] = useState(true) const [loadError, setLoadError] = useState(false) const [busy, setBusy] = useState(false) const toggleLock = useRef(false) const [togglePending, setTogglePending] = useState(null) const [toggleError, setToggleError] = useState(null) const [workspaceTab, setWorkspaceTab] = useState("interfaces") const [statusFilter, setStatusFilter] = useState("all") const [search, setSearch] = useState("") const [createOpen, setCreateOpen] = useState(false) const [importOpen, setImportOpen] = useState(false) const formLock = useRef(false) const [createError, setCreateError] = useState(null) const [peerError, setPeerError] = useState(null) const [importError, setImportError] = useState(null) const [exportIface, setExportIface] = useState(null) const [exportInitialTab, setExportInitialTab] = useState<"rsc" | "conf" | "peer">("rsc") const [exportPeerId, setExportPeerId] = useState(null) const [peerIface, setPeerIface] = useState(null) const [pendingDelete, setPendingDelete] = useState(null) const deleteLock = useRef(false) const [deleteBusy, setDeleteBusy] = useState(false) const [deleteError, setDeleteError] = useState(null) const [historyOpen, setHistoryOpen] = useState(false) const [revisions, setRevisions] = useState([]) const [historyLoading, setHistoryLoading] = useState(false) const [historyRestoring, setHistoryRestoring] = useState(false) const [liveExport, setLiveExport] = useState<{ rsc?: string conf?: string peerConf?: string } | null>(null) const [exportBusy, setExportBusy] = useState(false) const exportEpoch = useRef(0) const exportPending = useRef(null) useEffect(() => () => { exportEpoch.current += 1 }, []) const loadLive = useCallback(async () => { if (!isLive) return setLoading(true) try { const [wg, servers] = await Promise.all([ listWireGuard(backendUrl), requestJson(backendUrl, "/api/servers"), ]) setLiveIfaces(wg.interfaces.map(dtoToRow)) setLiveServers(servers.filter((s) => s.enabled).map(mapBackendServer)) setFailures(wg.failures ?? []) setLoadError(false) if (wg.failures?.length) { toast.warning( `Не удалось опросить: ${wg.failures.map((f) => f.serverName ?? f.serverId).join(", ")}`, ) } } catch (e) { toast.error(e instanceof Error ? e.message : "Ошибка загрузки WireGuard") setLoadError(true) } finally { setLoading(false) } }, [isLive, backendUrl]) useEffect(() => { if (!isLive) { queueMicrotask(() => { setLiveIfaces([]) setLiveServers([]) setFailures([]) }) return } queueMicrotask(() => { void loadLive() }) }, [isLive, loadLive]) const displayIfaces = isLive ? liveIfaces : collectMockInterfaces() const displayServers = isLive ? liveServers : mockServers.filter((s) => s.enabled) const effectiveServerId = selectedServerId const selectedSourceError = isLive && (loadError || failures.some(f => f.serverId === selectedServerId)) const historyServerId = effectiveServerId === ALL_SERVERS_ID ? null : effectiveServerId const loadRevisions = useCallback(async () => { if (!isLive || !historyServerId) return setHistoryLoading(true) try { const res = await requestJson<{ revisions: ConfigRevisionDto[] }>( backendUrl, `/api/wireguard/revisions?serverId=${encodeURIComponent(historyServerId)}`, ) setRevisions(res.revisions) } catch (err) { toast.error("Не удалось загрузить историю", { description: String(err) }) setRevisions([]) } finally { setHistoryLoading(false) } }, [isLive, historyServerId, backendUrl]) const restoreRevision = useCallback(async (id: string) => { if (!isLive || !historyServerId) return setHistoryRestoring(true) try { await requestJson( backendUrl, `/api/wireguard/revisions/${encodeURIComponent(id)}/restore`, { method: "POST", body: JSON.stringify({ serverId: historyServerId }) }, ) toast.success("Версия применена на роутер") await loadLive() await loadRevisions() } catch (err) { toast.error("Не удалось откатить", { description: String(err) }) } finally { setHistoryRestoring(false) } }, [isLive, historyServerId, backendUrl, loadLive, loadRevisions]) const scopedIfaces = useMemo(() => { if (selectedInterfaceId !== null) return displayIfaces.filter(i => i.serverId === effectiveServerId && i.id === selectedInterfaceId) if (effectiveServerId === ALL_SERVERS_ID) return displayIfaces return displayIfaces.filter((i) => i.serverId === effectiveServerId) }, [displayIfaces, effectiveServerId, selectedInterfaceId]) const selectedInterface = selectedInterfaceId === null ? undefined : scopedIfaces[0] const statusCounts = useMemo(() => { let up = 0 let down = 0 for (const iface of scopedIfaces) { if (iface.status === "up") up += 1 else down += 1 } return { all: scopedIfaces.length, up, down } }, [scopedIfaces]) const statusFiltered = useMemo(() => { if (statusFilter === "all") return scopedIfaces return scopedIfaces.filter((i) => i.status === statusFilter) }, [scopedIfaces, statusFilter]) const filteredIfaces = useMemo(() => { if (!search) return statusFiltered const q = search.toLowerCase() return statusFiltered.filter( (i) => i.name.toLowerCase().includes(q) || i.serverName.toLowerCase().includes(q) || i.peers.some( (p) => p.allowedIps.some((a) => a.includes(q)) || (p.endpoint ?? "").includes(q), ), ) }, [statusFiltered, search]) const peerRows = useMemo(() => { const q = search.toLowerCase() const rows: WgPeerRow[] = [] for (const iface of scopedIfaces) { iface.peers.forEach((peer, index) => { const id = peerRowId(iface, peer, index) if (q) { const hay = [ peer.publicKey, peer.name ?? "", peer.endpoint ?? "", peer.allowedIps.join(" "), iface.name, iface.serverName, ].join(" ").toLowerCase() if (!hay.includes(q)) return } rows.push({ ...peer, id, ifaceId: iface.id, ifaceName: iface.name, serverId: iface.serverId, serverName: iface.serverName, serverCountry: iface.serverCountry, }) }) } return rows }, [scopedIfaces, search]) const totalPeers = scopedIfaces.reduce((s, i) => s + i.peers.length, 0) const onlinePeers = scopedIfaces.reduce( (s, i) => s + i.peers.filter((p) => !!p.latestHandshake).length, 0, ) const upIfaces = scopedIfaces.filter((i) => i.status === "up").length const compactServer = effectiveServerId !== ALL_SERVERS_ID const sheetServerId = compactServer ? effectiveServerId : undefined const serverOptions = displayServers.map((s) => ({ id: s.id, name: s.name, host: s.host, })) const railItems = useMemo(() => { const counts = new Map() for (const iface of displayIfaces) { counts.set(iface.serverId, (counts.get(iface.serverId) ?? 0) + 1) } return displayServers.map((s) => ({ id: s.id, name: s.name, host: s.host, site: s.site, country: s.country, status: s.status, type: s.type, count: counts.get(s.id) ?? 0, enabled: s.enabled, title: [s.name, s.host, s.asn].filter(Boolean).join(" · "), })) }, [displayServers, displayIfaces]) async function handleCreate(form: WgCreateFormState) { if (formLock.current) return if (!isLive) { toast.info("Создание на роутер доступно только в live-режиме") return } formLock.current = true setCreateError(null) setBusy(true) try { const ep = parseEndpoint(form.peerEndpoint) await createWireGuardInterface(backendUrl, { serverId: form.serverId, name: form.name.trim(), listenPort: Number.parseInt(form.listenPort, 10) || 13231, mtu: Number.parseInt(form.mtu, 10) || 1420, comment: form.comment || undefined, address: form.address.trim() || undefined, disabled: !form.enabled, peer: form.peerEnabled && form.peerPublicKey.trim() ? { publicKey: form.peerPublicKey.trim(), allowedAddresses: form.peerAllowedIps .split(",") .map((s) => s.trim()) .filter(Boolean), endpointAddress: ep.address, endpointPort: ep.port, persistentKeepalive: Number.parseInt(form.peerKeepalive, 10) || undefined, comment: form.peerComment || undefined, } : undefined, }) toast.success(`Интерфейс ${form.name} создан`) setCreateOpen(false) await loadLive() } catch (e) { setCreateError(e instanceof Error ? e.message : "Ошибка создания") } finally { formLock.current = false setBusy(false) } } async function handleImport(args: { serverId: string content: string format: "auto" | "rsc" | "conf" dryRun: boolean }) { if (formLock.current) return if (!isLive) { toast.info("Импорт на роутер доступен только в live-режиме") return } formLock.current = true setImportError(null) setBusy(true) try { const res = await importWireGuard(backendUrl, { serverId: args.serverId, content: args.content, format: args.format, dryRun: args.dryRun, }) if (res.dryRun) { toast.info("Конфигурация проверена без применения на роутер") return } toast.success( res.applied ? `Импортировано: ${res.applied.interfaceName} (+${res.applied.peersCreated} пиров)` : "Импорт выполнен", ) setImportOpen(false) await loadLive() } catch (e) { setImportError(e instanceof Error ? e.message : "Ошибка импорта") } finally { formLock.current = false setBusy(false) } } function sourceUnavailable(serverId: string | undefined) { const unavailable = isLive && (loadError || failures.some(f => f.serverId === serverId)) if (unavailable) toast.error("Обновите данные сервера перед изменением объекта") return unavailable } async function handleToggle(iface: WgIfaceWithServer) { if (toggleLock.current) return if (sourceUnavailable(iface.serverId)) return if (!isLive || !iface.rosId) { toast.info("Доступно только в live-режиме") return } toggleLock.current = true const context = `${iface.name} · ${iface.serverName}` setTogglePending(context) setToggleError(null) try { const result = await patchWireGuardInterface(backendUrl, iface.serverId, iface.rosId, { disabled: iface.enabled, }) if (result?.ok !== true) throw new Error("Сервер не подтвердил изменение интерфейса") toast.success(iface.enabled ? "Отключено" : "Включено") await loadLive() } catch (e) { setToggleError(`${context}: ${e instanceof Error ? e.message : "Ошибка переключения"}`) } finally { toggleLock.current = false setTogglePending(null) } } async function confirmDelete() { if (!pendingDelete || deleteLock.current) return if (sourceUnavailable(pendingDelete.iface.serverId)) return if (!isLive) { toast.info("Доступно только в live-режиме") setPendingDelete(null) return } deleteLock.current = true setDeleteBusy(true) setDeleteError(null) try { if (pendingDelete.kind === "iface") { if (!pendingDelete.iface.rosId) { toast.info("Доступно только в live-режиме") return } const result = await deleteWireGuardInterface(backendUrl, pendingDelete.iface.serverId, pendingDelete.iface.rosId) if (result?.ok !== true) throw new Error("Сервер не подтвердил удаление") toast.success("Удалено") } else { const peer = pendingDelete.iface.peers.find(p => p.id === pendingDelete.peerId || p.rosId === pendingDelete.peerId) const result = await deleteWireGuardPeer(backendUrl, pendingDelete.iface.serverId, peer?.rosId ?? pendingDelete.peerId) if (result?.ok !== true) throw new Error("Сервер не подтвердил удаление") toast.success("Пир удалён") } setPendingDelete(null) await loadLive() } catch (e) { setDeleteError(e instanceof Error ? e.message : "Ошибка удаления") } finally { deleteLock.current = false setDeleteBusy(false) } } async function handleAddPeer(form: WgPeerFormState) { if (formLock.current) return if (sourceUnavailable(peerIface?.serverId)) return if (!isLive || !peerIface) { toast.info("Доступно только в live-режиме") return } formLock.current = true setPeerError(null) setBusy(true) try { const ep = parseEndpoint(form.endpoint) await createWireGuardPeer(backendUrl, { serverId: peerIface.serverId, interfaceName: peerIface.name, publicKey: form.publicKey.trim(), allowedAddresses: form.allowedIps .split(",") .map((s) => s.trim()) .filter(Boolean), endpointAddress: ep.address, endpointPort: ep.port, persistentKeepalive: Number.parseInt(form.keepalive, 10) || undefined, comment: form.comment || undefined, }) toast.success("Пир добавлен") setPeerIface(null) await loadLive() } catch (e) { setPeerError(e instanceof Error ? e.message : "Ошибка") } finally { formLock.current = false setBusy(false) } } async function handleLiveExport(format: "rsc" | "conf" | "peer-conf") { if (!exportIface || !isLive || exportPending.current !== null) return const epoch = exportEpoch.current exportPending.current = epoch setExportBusy(true) try { const peer = format === "peer-conf" ? selectWgExportPeer(exportIface.peers, exportPeerId) : undefined if (format === "peer-conf" && !peer) throw new Error("Выбранный пир не найден. Обновите список и выберите пира заново.") const res = await exportWireGuard(backendUrl, { serverId: exportIface.serverId, interfaceName: exportIface.name, format, includePrivateKey: format !== "peer-conf", peerId: peer?.rosId ?? peer?.id, }) if (epoch !== exportEpoch.current) return setLiveExport((prev) => ({ ...prev, ...(format === "rsc" ? { rsc: res.content } : format === "conf" ? { conf: res.content } : { peerConf: res.content }), })) toast.success("Конфиг загружен с роутера") } catch (e) { if (epoch !== exportEpoch.current) return toast.error(e instanceof Error ? e.message : "Ошибка экспорта") } finally { if (epoch === exportEpoch.current) { exportPending.current = null setExportBusy(false) } } } function openExport(iface: WgIfaceWithServer, tab: "rsc" | "conf" | "peer" = "rsc", peerId?: string) { exportEpoch.current += 1 exportPending.current = null setExportBusy(false) setLiveExport(null) setExportInitialTab(tab) setExportPeerId(peerId ?? null) setExportIface(iface) } const emptyCreateAction = ( ) return ( <> {isLive && ( <> )} } /> } >
{isLive && loadError && selectedInterfaceId === null && Не удалось обновить WireGuardПоказаны последние полученные данные. Повторите обновление перед изменением объектов.} {selectedInterfaceId !== null && ( {selectedInterface ? "Выбран интерфейс: " + selectedInterface.name : "WireGuard по ссылке"}

{isLive && loading ? "Загрузка интерфейса…" : selectedSourceError ? "Не удалось получить данные выбранного сервера. Повторите обновление; сохранённые данные могут быть устаревшими." : selectedInterface ? selectedInterface.serverName + " · " + (selectedInterface.address ?? selectedInterface.id) + ". Список, пиры и сводка ограничены выбранным интерфейсом." : "Интерфейс не найден или недоступен на указанном сервере. Проверьте ссылку и доступ к устройству."}

Все интерфейсы сервера К дашборду
)} {togglePending &&

Изменение интерфейса {togglePending}…

} {toggleError &&

{toggleError}

} , iconClassName: "text-muted-foreground", }, { id: "up", label: "Активных (UP)", value: upIfaces, icon: , iconClassName: "text-success", }, { id: "peers", label: "Всего пиров", value: totalPeers, icon: , iconClassName: "text-info", }, { id: "online", label: "Пиров онлайн", value: `${onlinePeers}/${totalPeers}`, icon: , iconClassName: "text-primary", }, ]} /> {failures.length > 0 ? ( Не удалось опросить часть роутеров {failures.map((f) => f.serverName ?? f.serverId).join(", ")} ) : null} {!isLive ? ( Mock-режим Переключитесь в live в настройках, чтобы применять изменения на MikroTik. ) : null} setWorkspaceTab(v as WgWorkspaceTab)} className="gap-3" > Интерфейсы Пиры CLI openExport(iface, "rsc")} onAddPeer={selectedSourceError ? undefined : iface => { setPeerError(null); setPeerIface(iface) }} onToggle={selectedSourceError ? undefined : handleToggle} toggleBusy={togglePending !== null} onDelete={selectedSourceError ? undefined : (iface) => setPendingDelete({ kind: "iface", iface })} onDeletePeer={selectedSourceError ? undefined : (iface, peerId) => setPendingDelete({ kind: "peer", iface, peerId })} onExportPeer={(iface, peerId) => openExport(iface, "peer", peerId)} /> { setPeerError(null); setPeerIface(scopedIfaces[0]) }}> Добавить пира ) : emptyCreateAction } onDeletePeer={selectedSourceError ? undefined : (row) => { const iface = scopedIfaces.find((i) => i.id === row.ifaceId && i.serverId === row.serverId) if (!iface) return setPendingDelete({ kind: "peer", iface, peerId: row.id }) }} onExportPeer={(row) => { const iface = scopedIfaces.find((i) => i.id === row.ifaceId && i.serverId === row.serverId) if (!iface) return openExport(iface, "peer", row.id) }} />
{[ { title: "Создать интерфейс", lines: [ "/interface wireguard add \\", " name=wg0 \\", " listen-port=13231 \\", " mtu=1420", ], }, { title: "Добавить пира", lines: [ "/interface wireguard peers add \\", " interface=wg0 \\", ' public-key="<ключ>" \\', " allowed-address=10.0.0.2/32 \\", " endpoint-address=1.2.3.4 \\", " persistent-keepalive=25", ], }, { title: "Назначить IP", lines: [ "/ip address add \\", " address=10.210.0.1/30 \\", " interface=wg0", "", "# Статус:", "/interface wireguard print", ], }, ].map((b) => (

{b.title}

                          {b.lines.join("\n")}
                        
))}
{ if (!formLock.current) setCreateOpen(v) }} error={createError} servers={serverOptions} defaultServerId={sheetServerId} busy={busy} onSubmit={handleCreate} /> { if (!formLock.current) setImportOpen(v) }} error={importError} servers={serverOptions} defaultServerId={sheetServerId} busy={busy} onImport={handleImport} /> { if (!v && !formLock.current) setPeerIface(null) }} error={peerError} onSubmit={handleAddPeer} /> { exportEpoch.current += 1 exportPending.current = null setExportBusy(false) setExportIface(null) setExportPeerId(null) setExportInitialTab("rsc") setLiveExport(null) }} liveContent={liveExport} liveBusy={exportBusy} onRequestLiveExport={isLive ? handleLiveExport : undefined} /> { if (!v && !deleteLock.current) { setPendingDelete(null); setDeleteError(null) } }}> {pendingDelete?.kind === "peer" ? "Удалить пира?" : "Удалить интерфейс?"} {pendingDelete?.kind === "iface" ? `${pendingDelete.iface.name} на ${pendingDelete.iface.serverName}. Вместе с интерфейсом будут удалены связанные пиры на роутере.` : pendingDelete ? `Пир на ${pendingDelete.iface.name} (${pendingDelete.iface.serverName}).` : null} {deleteBusy &&

Ожидание подтверждения удаления…

} {deleteError &&

{deleteError}

} Отмена void confirmDelete()}> {deleteBusy ? "Удаление…" : "Удалить"}
) } function WireGuardRoute() { const params = useSearchParams() const router = useRouter() const serverId = params.get("server") ?? ALL_SERVERS_ID const interfaceId = params.get("interface") const rest = new URLSearchParams(params.toString()) rest.delete("interface") const allInterfacesHref = "/wireguard" + (rest.size ? "?" + rest.toString() : "") return { const next = new URLSearchParams(rest) if (id === ALL_SERVERS_ID) next.delete("server") else next.set("server", id) router.push("/wireguard" + (next.size ? "?" + next.toString() : "")) }} /> } export default function WireGuardPage() { return Загрузка WireGuard…}> }