"use client" import { useCallback, useEffect, useMemo, useState } from "react" import { PageHeader } from "@/components/page-header" import { vxlanTunnels as mockVxlanTunnels, servers as mockServers } from "@/lib/data" import type { Server, VxlanTunnel } from "@/lib/data" import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid" import { OpsPanel } from "@/components/ops-panel" import { DataPageCard } from "@/components/data-page-card" import { DataPageToolbar } from "@/components/data-page-toolbar" import { VxlanDataGrid } from "@/components/data-grids/vxlan-data-grid" import { Button } from "@/components/ui/button" import { Alert, AlertDescription } from "@/components/ui/alert" import { NetworkIcon, PlusIcon, LayersIcon, RefreshCwIcon, AlertCircleIcon, } from "lucide-react" import { CodeExportSheet } from "@/components/reui-kit/code-export-sheet" import { useDataSource } from "@/lib/data-source" import { requestJson } from "@/shared/api/http-client" import { cn } from "@/lib/utils" import { ServerRailLayout, ServerRailMobileButton } from "@/components/server-rail-layout" import { ALL_SERVERS_ID, type ServerTileItem } from "@/components/server-tile-rail" interface BackendServer { id: number name: string host: string type?: Server["type"] site?: string country: string asn?: string enabled: boolean status?: Server["status"] latency?: number | null } interface VxlanApiResponse { tunnels: VxlanTunnel[] } function mapBackendServer(s: BackendServer): Server { return { id: String(s.id), name: s.name || s.host, host: s.host, model: "—", os: "—", site: s.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 generateVxlanRsc(t: VxlanTunnel, serverById: Record): string { const srv = serverById[t.serverId] const lines: string[] = [] lines.push(`# VXLAN — ${t.name} · VNI ${t.vni}`) if (srv) lines.push(`# Сервер: ${srv.name} (${srv.host})`) lines.push(`# RouterOS 7.x · /interface/vxlan`) lines.push(``) lines.push(`/interface/vxlan/add \\`) lines.push(` name=${t.name} \\`) lines.push(` vni=${t.vni} \\`) lines.push(` port=${t.dstPort} \\`) lines.push(` vtep-mac-address=auto \\`) lines.push(` arp-proxy=${t.arpProxy ? "yes" : "no"} \\`) lines.push(` mac-learning=${t.macLearning ? "yes" : "no"} \\`) lines.push(` l2mtu=${t.l2mtu} \\`) if (t.comment) lines.push(` comment="${t.comment}" \\`) if (!t.enabled) lines.push(` disabled=yes \\`) lines.push(``) for (const vtep of t.remoteVteps) { lines.push(`/interface/vxlan/vteps/add \\`) lines.push(` interface=${t.name} \\`) lines.push(` remote-ip=${vtep}`) lines.push(``) } lines.push(`# Добавить в bridge:`) lines.push(`/interface/bridge/port/add \\`) lines.push(` bridge=bridge-overlay \\`) lines.push(` interface=${t.name}`) return lines.join("\n") } function ExportSheet({ open, tunnel, onClose, serverById, }: { open: boolean tunnel: VxlanTunnel | null onClose: () => void serverById: Record }) { const code = useMemo( () => (tunnel ? generateVxlanRsc(tunnel, serverById) : ""), [tunnel, serverById], ) return ( ) } export default function VxlanPage() { const { mode, backendUrl } = useDataSource() const isLive = mode === "live" const [search, setSearch] = useState("") const [exportTunnel, setExportTunnel] = useState(null) const [selectedServerId, setSelectedServerId] = useState(ALL_SERVERS_ID) const [liveTunnels, setLiveTunnels] = useState([]) const [liveServers, setLiveServers] = useState([]) const [loading, setLoading] = useState(false) const [liveError, setLiveError] = useState(null) const loadLive = useCallback(async () => { if (!isLive) return setLoading(true) setLiveError(null) try { const [tunnelsRes, serversRes] = await Promise.all([ requestJson(backendUrl, "/api/vxlan"), requestJson(backendUrl, "/api/servers"), ]) setLiveTunnels(tunnelsRes.tunnels ?? []) setLiveServers(serversRes.filter((s) => s.enabled).map(mapBackendServer)) } catch (e) { setLiveError(e instanceof Error ? e.message : "Ошибка загрузки") setLiveTunnels([]) } finally { setLoading(false) } }, [isLive, backendUrl]) useEffect(() => { if (!isLive) { queueMicrotask(() => { setLiveTunnels([]) setLiveServers([]) setLiveError(null) }) return } queueMicrotask(() => { void loadLive() }) }, [isLive, loadLive]) const displayTunnels = isLive ? liveTunnels : mockVxlanTunnels const displayServers = isLive ? liveServers : mockServers.filter((s) => s.enabled) const effectiveServerId = selectedServerId === ALL_SERVERS_ID || displayServers.some((s) => s.id === selectedServerId) ? selectedServerId : ALL_SERVERS_ID const scopedTunnels = useMemo(() => { if (effectiveServerId === ALL_SERVERS_ID) return displayTunnels return displayTunnels.filter((t) => t.serverId === effectiveServerId) }, [displayTunnels, effectiveServerId]) const serverById = useMemo( () => Object.fromEntries(displayServers.map((s) => [s.id, s])), [displayServers], ) const railItems = useMemo(() => ( displayServers.map((s) => ({ id: s.id, name: s.name, host: s.host, site: s.site, country: s.country, status: s.status, type: s.type, enabled: s.enabled, meta: String(displayTunnels.filter((t) => t.serverId === s.id).length), })) ), [displayServers, displayTunnels]) const filtered = useMemo(() => { if (!search) return scopedTunnels const q = search.toLowerCase() return scopedTunnels.filter((t) => t.name.includes(q) || String(t.vni).includes(q) || t.vtepIp.includes(q) || (serverById[t.serverId]?.name.toLowerCase().includes(q) ?? false), ) }, [search, scopedTunnels, serverById]) const upCount = scopedTunnels.filter((t) => t.status === "up").length const vnis = new Set(scopedTunnels.map((t) => t.vni)).size return ( <> } /> } >
{isLive && liveError && ( Ошибка загрузки: {liveError} )} {isLive && !loading && displayTunnels.length === 0 && !liveError && (
На опрошенных серверах нет VXLAN-интерфейсов
)} {mode === "mock" && ( Моковые данные )} , iconClassName: "text-muted-foreground", }, { id: "up", label: "Активных", value: upCount, icon: , iconClassName: "text-success", }, { id: "vni", label: "Уникальных VNI", value: vnis, icon: , iconClassName: "text-info", }, { id: "servers", label: "Серверов", value: new Set(scopedTunnels.map((t) => t.serverId)).size, icon: , iconClassName: "text-primary", }, ]} />

VXLAN — L2-over-L3 оверлей для RouterOS 7.x

Доступен с RouterOS 7.1+. VNI (Virtual Network Identifier) — уникальный идентификатор сегмента (0–16777215). Рекомендуется использовать совместно с WireGuard или GRE туннелями для шифрования.

{[ { title: "Создать VXLAN", lines: [ "/interface/vxlan/add \\", " name=vxlan-10 \\", " vni=10010 \\", " port=8472 \\", " arp-proxy=yes \\", " mac-learning=yes", ], }, { title: "Добавить VTEP", lines: [ "/interface/vxlan/vteps/add \\", " interface=vxlan-10 \\", " remote-ip=10.0.1.1", "", "/interface/vxlan/vteps/add \\", " interface=vxlan-10 \\", " remote-ip=10.0.2.1", ], }, { title: "Bridge + IP", lines: [ "/interface/bridge/add \\", " name=br-overlay", "", "/interface/bridge/port/add \\", " bridge=br-overlay \\", " interface=vxlan-10", "", "/ip/address/add \\", " address=10.100.0.1/24 \\", " interface=br-overlay", ], }, ].map((b) => (

{b.title}

                      {b.lines.join("\n")}
                    
))}
setExportTunnel(null)} serverById={serverById} /> ) }