fix(vxlan): корректно экспортировать параметры RouterOS
This commit is contained in:
1 parent
d200e87e3d
commit
0e0f98e0f4
9 files changed
+82
-38
No files matched your search
@@ -20,6 +20,7 @@ import {
|
||||
} from "lucide-react"
|
||||
import { CodeExportSheet } from "@/components/reui-kit/code-export-sheet"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { generateVxlanRsc } from "@/lib/vxlan-export"
|
||||
import { vxlanResponseSchema, vxlanServersSchema } from "@/lib/vxlan-response"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import { cn } from "@/lib/utils"
|
||||
@@ -62,40 +63,6 @@ function mapBackendServer(s: BackendServer): Server {
|
||||
}
|
||||
}
|
||||
|
||||
function generateVxlanRsc(t: VxlanTunnel, serverById: Record<string, Server>): 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, receivedAt, stale, mock,
|
||||
}: {
|
||||
|
||||
@@ -54,3 +54,13 @@ const failed = await pollVxlanServers([server], async () => { throw new Error("a
|
||||
assert.equal(failed.tunnels.length, 0)
|
||||
assert.equal(failed.failures.length, 1)
|
||||
console.log("vxlan polling: mixed/empty/all-failed ok")
|
||||
|
||||
for (const arp of ["disabled", "enabled", "local-proxy-arp", "proxy-arp", "reply-only"] as const) {
|
||||
const mapped = mapVxlanRow(server, { ".id": "*4", name: "vx", arp, learning: "no" }, [], 0)
|
||||
assert.equal(mapped.arpMode, arp)
|
||||
assert.equal(mapped.arpProxy, arp === "proxy-arp" || arp === "local-proxy-arp")
|
||||
assert.equal(mapped.macLearning, false)
|
||||
}
|
||||
console.log("vxlan: exact ARP modes and RouterOS no-learning passed")
|
||||
|
||||
assert.equal(mapVxlanRow(server, { arp: "enabled", "arp-proxy": "true" }, [], 0).arpProxy, false)
|
||||
@@ -40,6 +40,7 @@ export type VxlanTunnelLive = {
|
||||
remoteVteps: string[]
|
||||
l2mtu: number
|
||||
arpProxy: boolean
|
||||
arpMode: "disabled" | "enabled" | "local-proxy-arp" | "proxy-arp" | "reply-only"
|
||||
macLearning: boolean
|
||||
comment: string
|
||||
enabled: boolean
|
||||
@@ -66,6 +67,7 @@ export function mapVxlanRow(
|
||||
const disabled = rosYes(vx.disabled)
|
||||
const running = rosYes(vx.running)
|
||||
const port = parseIntSafe(vx.port, 8472)
|
||||
const arpMode: VxlanTunnelLive["arpMode"] = (["disabled", "enabled", "local-proxy-arp", "proxy-arp", "reply-only"] as const).find(mode => mode === vx.arp) ?? (rosYes(vx["arp-proxy"]) ? "proxy-arp" : "enabled")
|
||||
const remoteVteps = vteps
|
||||
.filter((v) => (v.interface ?? "").trim() === name)
|
||||
.map((v) => (v["remote-ip"] ?? "").trim())
|
||||
@@ -81,8 +83,9 @@ export function mapVxlanRow(
|
||||
vtepIp: (vx["local-address"] ?? vx["vtep-address"] ?? "").trim(),
|
||||
remoteVteps,
|
||||
l2mtu: parseIntSafe(vx.l2mtu, 1500),
|
||||
arpProxy: rosYes(vx["arp-proxy"]) || vx.arp === "proxy-arp" || vx.arp === "enabled",
|
||||
macLearning: vx["mac-learning"] != null ? rosYes(vx["mac-learning"]) : vx.learning !== "false",
|
||||
arpProxy: arpMode === "proxy-arp" || arpMode === "local-proxy-arp",
|
||||
arpMode,
|
||||
macLearning: vx["mac-learning"] != null ? rosYes(vx["mac-learning"]) : vx.learning != null ? rosYes(vx.learning) : true,
|
||||
comment: vx.comment ?? "",
|
||||
enabled: !disabled,
|
||||
status: !disabled && running ? "up" : "down",
|
||||
|
||||
@@ -25,12 +25,20 @@
|
||||
| 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже |
|
||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
||||
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
||||
| 9 | VPN, Firewall и Filters | В работе: блоки 9.1–9.53 — IPsec/GRE/VXLAN, Firewall и операции Filters |
|
||||
| 9 | VPN, Firewall и Filters | В работе: блоки 9.1–9.54 — IPsec/GRE/VXLAN, Firewall и операции Filters |
|
||||
| 10 | Маршрутизация, диагностика, карта и терминал | Не начат |
|
||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||
|
||||
## Блок 9.54 — корректный фрагмент .rsc VXLAN (2026-09-30)
|
||||
|
||||
- Генератор вынесен в lib/vxlan-export.ts. Строки RouterOS заключены в кавычки; кавычка/обратный слеш/dollar и управляющие символы экранируются. Заголовки не содержат переводов строки из имени/metadata. Числовые параметры идут из валидированной модели.
|
||||
- По документации MikroTik используются port, learning, arp и доступный local-address. Удалены неподдерживаемые vtep-mac-address/arp-proxy/mac-learning, read-only l2mtu и выдуманная команда привязки к bridge. Это фрагмент интерфейса/статических VTEP; bridge/FDB/прочие параметры, которых нет в API, не включены. Импорт на RouterOS ещё не проверен.
|
||||
- Backend возвращает точный arpMode, обычный arp=enabled больше не означает proxy ARP. Обрабатывается learning=no. Точный arp приоритетен над legacy arp-proxy. Новое поле optional на frontend для совместимости прежних ответов; при его отсутствии старый boolean может описать только proxy-arp/обычный enabled.
|
||||
- Unit генератора проверяет сложные строки и отсутствие посторонних команд/недоступных параметров; backend unit проверяет все ARP modes и частичный опрос. TypeScript frontend/backend, frontend lint, 2 E2E экспорта/свежести и backend build прошли. Последний frontend build — 9.52.
|
||||
- Источники: [VXLAN](https://help.mikrotik.com/docs/spaces/ROS/pages/100007937/VXLAN), [строковые литералы RouterOS](https://help.mikrotik.com/docs/spaces/ROS/pages/47579229/Scripting). Далее — согласованность ответа VXLAN, экраны и общая проверка, затем GRE-каталог. Локальный коммит без push.
|
||||
|
||||
## Блок 9.53 — достоверность VXLAN и режим просмотра (2026-09-30)
|
||||
|
||||
- Backend VXLAN предоставляет GET; удалены активные кнопки без обработчиков (создание/редактирование/включение/удаление). Экран явно отмечен «Только просмотр»; доступны опрос и экспорт .rsc. Новые сетевые write-операции в этом блоке не вводились.
|
||||
|
||||
@@ -292,3 +292,11 @@ Live-копирование читает актуальный целевой rul
|
||||
- Экспорт фиксирует строку, metadata сервера, дату и признак неизвестной актуальности на момент открытия; обновление таблицы не меняет открытый код. Mock-экспорт подписан отдельно. Смена backend/mode отделяет состояние, панели и даты; URL-контекст сохранён.
|
||||
- После последних изменений прошли 17 обычных E2E VXLAN и 4 отдельных E2E смены источника. TypeScript и lint чистые. Backend не менялся; последний frontend build — 9.52, backend — 9.44.
|
||||
- Далее — точность/экранирование .rsc VXLAN и валидация согласованности ответа, затем ошибки общего GRE-каталога и общая регрессия этапа 9. Реальная RouterOS-приёмка/полные права/DB/JWT не выполнены. Локальный коммит без push.
|
||||
|
||||
## Блок 9.54 — корректный фрагмент .rsc VXLAN (2026-09-30)
|
||||
|
||||
- Генератор вынесен в lib/vxlan-export.ts. Строки RouterOS заключены в кавычки; кавычка/обратный слеш/dollar и управляющие символы экранируются. Заголовки не содержат переводов строки из имени/metadata. Числовые параметры идут из валидированной модели.
|
||||
- По документации MikroTik используются port, learning, arp и доступный local-address. Удалены неподдерживаемые vtep-mac-address/arp-proxy/mac-learning, read-only l2mtu и выдуманная команда привязки к bridge. Это фрагмент интерфейса/статических VTEP; bridge/FDB/прочие параметры, которых нет в API, не включены. Импорт на RouterOS ещё не проверен.
|
||||
- Backend возвращает точный arpMode, обычный arp=enabled больше не означает proxy ARP. Обрабатывается learning=no. Точный arp приоритетен над legacy arp-proxy. Новое поле optional на frontend для совместимости прежних ответов; при его отсутствии старый boolean может описать только proxy-arp/обычный enabled.
|
||||
- Unit генератора проверяет сложные строки и отсутствие посторонних команд/недоступных параметров; backend unit проверяет все ARP modes и частичный опрос. TypeScript frontend/backend, frontend lint, 2 E2E экспорта/свежести и backend build прошли. Последний frontend build — 9.52.
|
||||
- Источники: [VXLAN](https://help.mikrotik.com/docs/spaces/ROS/pages/100007937/VXLAN), [строковые литералы RouterOS](https://help.mikrotik.com/docs/spaces/ROS/pages/47579229/Scripting). Далее — согласованность ответа VXLAN, экраны и общая проверка, затем GRE-каталог. Локальный коммит без push.
|
||||
@@ -213,6 +213,7 @@ export interface VxlanTunnel {
|
||||
remoteVteps: string[] // remote VTEP IPs (unicast-flood)
|
||||
l2mtu: number
|
||||
arpProxy: boolean
|
||||
arpMode?: "disabled" | "enabled" | "local-proxy-arp" | "proxy-arp" | "reply-only"
|
||||
macLearning: boolean
|
||||
comment: string
|
||||
enabled: boolean
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { generateVxlanRsc, quoteRosString } from "./vxlan-export"
|
||||
import type { VxlanTunnel } from "./data"
|
||||
|
||||
assert.equal(quoteRosString('edge "one"\\$value\r\n\t'), '"edge \\"one\\"\\\\\\$value\\0D\\0A\\09"')
|
||||
const tunnel: VxlanTunnel = { id: "1-*1", serverId: "1", name: 'vx "edge" $name\n/system reboot', vni: 200, port: 0, dstPort: 4789, vtepIp: "192.0.2.1", remoteVteps: ["198.51.100.1"], l2mtu: 1500, arpProxy: false, arpMode: "reply-only", macLearning: false, comment: 'note "quoted"\\$value\n/system reboot', enabled: false, status: "down" }
|
||||
const code = generateVxlanRsc(tunnel, {})
|
||||
const commands = code.split("\n").filter(line => line.startsWith("/"))
|
||||
assert.equal(commands.length, 2)
|
||||
assert.match(commands[0], /vni=200 port=4789 arp="reply-only" learning=no disabled=yes/)
|
||||
assert.ok(commands[0].includes('local-address="192.0.2.1"'))
|
||||
assert.ok(commands[0].includes('comment="note \\"quoted\\"\\\\\\$value\\0A/system reboot"'))
|
||||
assert.ok(commands[1].includes('interface="vx \\"edge\\" \\$name\\0A/system reboot" remote-ip="198.51.100.1"'))
|
||||
for (const unsupported of ["vtep-mac-address", "arp-proxy=", "mac-learning=", "l2mtu=", "/interface/bridge/port/add", "\n/system reboot"]) assert.ok(!code.includes(unsupported), unsupported)
|
||||
const noLocal = generateVxlanRsc({ ...tunnel, vtepIp: "", arpMode: undefined, arpProxy: true }, {})
|
||||
assert.ok(!noLocal.includes("local-address="))
|
||||
assert.ok(noLocal.includes('arp="proxy-arp"'))
|
||||
console.log("vxlan-export: quoted strings, supported properties and no invented bridge passed")
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Server, VxlanTunnel } from "./data"
|
||||
|
||||
/** RouterOS string literals: https://help.mikrotik.com/docs/spaces/ROS/pages/47579229/Scripting */
|
||||
export function quoteRosString(value: string): string {
|
||||
return '"' + value.replace(/[\\"$\x00-\x1f\x7f]/g, char => {
|
||||
if (char === "\\" || char === '"' || char === "$") return "\\" + char
|
||||
return "\\" + char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")
|
||||
}) + '"'
|
||||
}
|
||||
|
||||
const commentLine = (value: string) => value.replace(/[\r\n\x00-\x1f\x7f]/g, " ")
|
||||
|
||||
/** A configuration fragment, not a full RouterOS export: bridge/FDB are not available in this API. */
|
||||
export function generateVxlanRsc(t: VxlanTunnel, servers: Record<string, Server>): string {
|
||||
const server = servers[t.serverId]
|
||||
const lines = [`# VXLAN — ${commentLine(t.name)} · VNI ${t.vni}`]
|
||||
if (server) lines.push(`# Сервер: ${commentLine(server.name)} (${commentLine(server.host)})`)
|
||||
lines.push("# RouterOS 7.x · интерфейс и статические VTEP; bridge/FDB не включены", "")
|
||||
const args = [
|
||||
`name=${quoteRosString(t.name)}`, `vni=${t.vni}`, `port=${t.dstPort}`,
|
||||
`arp=${quoteRosString(t.arpMode ?? (t.arpProxy ? "proxy-arp" : "enabled"))}`,
|
||||
`learning=${t.macLearning ? "yes" : "no"}`, `disabled=${t.enabled ? "no" : "yes"}`,
|
||||
`comment=${quoteRosString(t.comment)}`,
|
||||
]
|
||||
if (t.vtepIp) args.push(`local-address=${quoteRosString(t.vtepIp)}`)
|
||||
lines.push("/interface/vxlan/add " + args.join(" "))
|
||||
for (const remote of t.remoteVteps) lines.push(`/interface/vxlan/vteps/add interface=${quoteRosString(t.name)} remote-ip=${quoteRosString(remote)}`)
|
||||
return lines.join("\n") + "\n"
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export const vxlanResponseSchema = z.object({
|
||||
vni: z.number().int().min(0).max(16777215),
|
||||
port: z.number().int().min(0).max(65535), dstPort: z.number().int().min(1).max(65535),
|
||||
vtepIp: z.string(), remoteVteps: z.array(z.string()), l2mtu: z.number().int().nonnegative(),
|
||||
arpProxy: z.boolean(), macLearning: z.boolean(), comment: z.string(), enabled: z.boolean(),
|
||||
arpProxy: z.boolean(), arpMode: z.enum(["disabled", "enabled", "local-proxy-arp", "proxy-arp", "reply-only"]).optional(), macLearning: z.boolean(), comment: z.string(), enabled: z.boolean(),
|
||||
status: z.enum(["up", "down"]),
|
||||
})).refine(rows => new Set(rows.map(row => JSON.stringify([row.serverId, row.id]))).size === rows.length),
|
||||
failures: z.array(z.object({ serverId: z.string().min(1), serverName: z.string().optional(), error: z.string() })).default([]),
|
||||
|
||||
Reference in new issue
Block a user