import { eq } from "drizzle-orm" import { db } from "../db/index.js" import { servers } from "../db/schema.js" import { MikrotikClient } from "./mikrotik.js" type ServerRow = typeof servers.$inferSelect interface RosVxlan { ".id"?: string name?: string vni?: string port?: string "local-address"?: string "vtep-address"?: string running?: string disabled?: string comment?: string l2mtu?: string arp?: string "arp-proxy"?: string "mac-learning"?: string learning?: string } interface RosVxlanVtep { ".id"?: string interface?: string "remote-ip"?: string } export type VxlanTunnelLive = { id: string rosId: string name: string vni: number port: number dstPort: number serverId: string vtepIp: string remoteVteps: string[] l2mtu: number arpProxy: boolean macLearning: boolean comment: string enabled: boolean status: "up" | "down" } function rosYes(v: string | undefined): boolean { return v === "true" || v === "yes" } function parseIntSafe(v: string | undefined, fallback: number): number { const n = Number.parseInt(v ?? "", 10) return Number.isFinite(n) ? n : fallback } export function mapVxlanRow( server: ServerRow, vx: RosVxlan, vteps: RosVxlanVtep[], idx: number, ): VxlanTunnelLive { const name = (vx.name ?? "").trim() || `vxlan-${idx + 1}` const rosId = String(vx[".id"] ?? name) const disabled = rosYes(vx.disabled) const running = rosYes(vx.running) const port = parseIntSafe(vx.port, 8472) const remoteVteps = vteps .filter((v) => (v.interface ?? "").trim() === name) .map((v) => (v["remote-ip"] ?? "").trim()) .filter(Boolean) return { id: `${server.id}-${rosId}`, rosId, name, vni: parseIntSafe(vx.vni, 0), port: 0, dstPort: port, serverId: String(server.id), 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", comment: vx.comment ?? "", enabled: !disabled, status: !disabled && running ? "up" : "down", } } async function fetchVxlanForServer(server: ServerRow): Promise { const client = MikrotikClient.fromServer(server) const [vxRaw, vtepRaw] = await Promise.all([ client.get("/interface/vxlan"), client.get("/interface/vxlan/vteps").catch(() => [] as RosVxlanVtep[]), ]) const list = Array.isArray(vxRaw) ? vxRaw : [] const vteps = Array.isArray(vtepRaw) ? vtepRaw : [] return list.map((vx, idx) => mapVxlanRow(server, vx, vteps, idx)) } export async function listVxlanTunnels(): Promise { const enabledServers = await db.select().from(servers).where(eq(servers.enabled, true)) const results = await Promise.all( enabledServers.map(async (server) => { try { return await fetchVxlanForServer(server) } catch { return [] as VxlanTunnelLive[] } }), ) return results.flat() } export async function listVxlanTunnelsForServer(server: ServerRow): Promise { try { return await fetchVxlanForServer(server) } catch { return [] } } export async function countVxlanTunnels(): Promise { try { const result = await Promise.race([ listVxlanTunnels(), new Promise((resolve) => setTimeout(() => resolve(null), 8_000)), ]) if (!result) return 0 return result.length } catch { return 0 } }