fix(vxlan): корректно экспортировать параметры RouterOS
This commit is contained in:
1 parent
d200e87e3d
commit
0e0f98e0f4
9 files changed
+82
-38
No files matched your search
@@ -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