/** * Client-side WireGuard config codecs (mirror of backend wireguard-config). * Used for mock preview / offline export without hitting the API. */ export type WgParsedPeer = { publicKey: string allowedAddresses: string[] endpointAddress?: string endpointPort?: number persistentKeepalive?: number comment?: string name?: string privateKey?: string clientAddress?: string clientDns?: string clientEndpoint?: string disabled?: boolean } export type WgParsedInterface = { name: string listenPort?: number mtu?: number privateKey?: string comment?: string address?: string disabled?: boolean } export type WgParsedConfig = { format: "rsc" | "conf" interface: WgParsedInterface peers: WgParsedPeer[] } export type WgExportIface = { name: string listenPort: number mtu: number comment?: string enabled?: boolean privateKey?: string publicKey?: string address?: string serverName?: string peers: Array<{ publicKey: string allowedIps: string[] endpoint?: string persistentKeepalive?: number persistent?: boolean comment?: string name?: string clientAddress?: string clientDns?: string clientEndpoint?: string }> } function stripQuotes(v: string): string { const t = v.trim() if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) { return t.slice(1, -1) } return t } function parseKvLine(line: string): Record { const out: Record = {} const re = /([a-zA-Z0-9_-]+)=("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s\\]+)/g let m: RegExpExecArray | null while ((m = re.exec(line)) !== null) { out[m[1]] = stripQuotes(m[2]) } return out } function joinContinuedLines(text: string): string[] { const raw = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n") const lines: string[] = [] let buf = "" for (const line of raw) { const trimmedEnd = line.replace(/\s+$/, "") if (trimmedEnd.endsWith("\\")) { buf += trimmedEnd.slice(0, -1).trimEnd() + " " continue } buf += trimmedEnd if (buf.trim()) lines.push(buf.trim()) buf = "" } if (buf.trim()) lines.push(buf.trim()) return lines } export function detectWgConfigFormat(content: string): "rsc" | "conf" { const t = content.trim() if (/\[Interface\]/i.test(t) || /\[Peer\]/i.test(t)) return "conf" if (/\/interface\s+wireguard/i.test(t) || /\/interface\/wireguard/i.test(t)) return "rsc" if (/PrivateKey\s*=/i.test(t) || /PublicKey\s*=/i.test(t)) return "conf" return "rsc" } export function parseNativeConf(content: string): WgParsedConfig { const lines = content.replace(/\r\n/g, "\n").split("\n") let section: "interface" | "peer" | null = null const iface: WgParsedInterface = { name: "wg0" } const peers: WgParsedPeer[] = [] let currentPeer: WgParsedPeer | null = null const flushPeer = () => { if (currentPeer?.publicKey) peers.push(currentPeer) currentPeer = null } for (const raw of lines) { const line = raw.trim() if (!line || line.startsWith("#") || line.startsWith(";")) continue if (/^\[Interface\]$/i.test(line)) { flushPeer() section = "interface" continue } if (/^\[Peer\]$/i.test(line)) { flushPeer() section = "peer" currentPeer = { publicKey: "", allowedAddresses: [] } continue } const eq = line.indexOf("=") if (eq < 0) continue const key = line.slice(0, eq).trim().toLowerCase() const value = line.slice(eq + 1).trim() if (section === "interface") { if (key === "privatekey") iface.privateKey = value else if (key === "address") iface.address = value.split(",")[0]?.trim() else if (key === "listenport") iface.listenPort = Number.parseInt(value, 10) || undefined else if (key === "mtu") iface.mtu = Number.parseInt(value, 10) || undefined else if (key === "name") iface.name = value || iface.name } else if (section === "peer" && currentPeer) { if (key === "publickey") currentPeer.publicKey = value else if (key === "allowedips") { currentPeer.allowedAddresses = value .split(",") .map((s) => s.trim()) .filter(Boolean) } else if (key === "endpoint") { const lastColon = value.lastIndexOf(":") if (lastColon > 0 && !value.includes("]:")) { currentPeer.endpointAddress = value.slice(0, lastColon) currentPeer.endpointPort = Number.parseInt(value.slice(lastColon + 1), 10) || undefined } else { currentPeer.endpointAddress = value } } else if (key === "persistentkeepalive") { currentPeer.persistentKeepalive = Number.parseInt(value, 10) || undefined } } } flushPeer() return { format: "conf", interface: iface, peers } } export function parseMikrotikRsc(content: string): WgParsedConfig { const lines = joinContinuedLines(content) const iface: WgParsedInterface = { name: "wg0" } const peers: WgParsedPeer[] = [] let foundIface = false for (const line of lines) { if (line.startsWith("#")) continue const lower = line.toLowerCase() if ( lower.startsWith("/interface wireguard add") || lower.startsWith("/interface/wireguard add") ) { const kv = parseKvLine(line) if (kv.name) iface.name = kv.name if (kv["listen-port"]) iface.listenPort = Number.parseInt(kv["listen-port"], 10) || undefined if (kv.mtu) iface.mtu = Number.parseInt(kv.mtu, 10) || undefined if (kv["private-key"]) iface.privateKey = kv["private-key"] if (kv.comment) iface.comment = kv.comment if (kv.disabled === "yes") iface.disabled = true foundIface = true continue } if ( lower.startsWith("/interface wireguard peers add") || lower.startsWith("/interface/wireguard/peers add") ) { const kv = parseKvLine(line) const allowed = (kv["allowed-address"] ?? "") .split(",") .map((s) => s.trim()) .filter(Boolean) peers.push({ publicKey: kv["public-key"] ?? "", allowedAddresses: allowed.length ? allowed : ["0.0.0.0/0"], endpointAddress: kv["endpoint-address"], endpointPort: kv["endpoint-port"] ? Number.parseInt(kv["endpoint-port"], 10) || undefined : undefined, persistentKeepalive: kv["persistent-keepalive"] ? Number.parseInt(kv["persistent-keepalive"], 10) || undefined : undefined, comment: kv.comment, name: kv.name, clientAddress: kv["client-address"], clientDns: kv["client-dns"], clientEndpoint: kv["client-endpoint"], disabled: kv.disabled === "yes", }) continue } if (lower.startsWith("/ip address add") || lower.startsWith("/ip/address add")) { const kv = parseKvLine(line) if (kv.address) iface.address = kv.address } } if (!foundIface && peers.length === 0) { throw new Error("Не удалось распознать RouterOS WireGuard .rsc") } return { format: "rsc", interface: iface, peers } } export function parseWgConfig( content: string, format: "auto" | "rsc" | "conf" = "auto", ): WgParsedConfig { const detected = format === "auto" ? detectWgConfigFormat(content) : format if (detected === "conf") return parseNativeConf(content) return parseMikrotikRsc(content) } export function generateNativeConf(iface: WgExportIface): string { const lines: string[] = [] lines.push(`[Interface]`) if (iface.privateKey) lines.push(`PrivateKey = ${iface.privateKey}`) else lines.push(`# PrivateKey = <заполните приватный ключ с роутера>`) if (iface.address) lines.push(`Address = ${iface.address}`) lines.push(`ListenPort = ${iface.listenPort}`) if (iface.mtu) lines.push(`MTU = ${iface.mtu}`) lines.push(``) for (const p of iface.peers) { lines.push(`[Peer]`) lines.push(`PublicKey = ${p.publicKey}`) lines.push(`AllowedIPs = ${p.allowedIps.join(", ")}`) if (p.endpoint) lines.push(`Endpoint = ${p.endpoint}`) const ka = p.persistentKeepalive ?? (p.persistent ? 25 : undefined) if (ka != null && ka > 0) lines.push(`PersistentKeepalive = ${ka}`) if (p.comment) lines.push(`# ${p.comment}`) lines.push(``) } return lines.join("\n").trimEnd() + "\n" } export function generateMikrotikRsc(iface: WgExportIface): string { const lines: string[] = [] lines.push(`# WireGuard — ${iface.name}${iface.serverName ? ` · ${iface.serverName}` : ""}`) lines.push(`# RouterOS 7.x · MikrotikManager`) lines.push(``) lines.push(`/interface wireguard add \\`) lines.push(` name=${iface.name} \\`) lines.push(` listen-port=${iface.listenPort} \\`) lines.push(` mtu=${iface.mtu} \\`) if (iface.privateKey) lines.push(` private-key="${iface.privateKey}" \\`) if (iface.comment) lines.push(` comment="${iface.comment.replace(/"/g, '\\"')}" \\`) if (iface.enabled === false) lines.push(` disabled=yes \\`) if (lines[lines.length - 1]?.endsWith(" \\")) { lines[lines.length - 1] = lines[lines.length - 1]!.slice(0, -2) } lines.push(``) if (iface.address) { lines.push(`/ip address add \\`) lines.push(` address=${iface.address} \\`) lines.push(` interface=${iface.name}`) lines.push(``) } for (const p of iface.peers) { lines.push(`/interface wireguard peers add \\`) lines.push(` interface=${iface.name} \\`) lines.push(` public-key="${p.publicKey}" \\`) lines.push(` allowed-address=${p.allowedIps.join(",")} \\`) if (p.endpoint) { const host = p.endpoint.includes(":") ? p.endpoint.slice(0, p.endpoint.lastIndexOf(":")) : p.endpoint const port = p.endpoint.includes(":") ? p.endpoint.slice(p.endpoint.lastIndexOf(":") + 1) : "13231" lines.push(` endpoint-address=${host} \\`) lines.push(` endpoint-port=${port} \\`) } const ka = p.persistentKeepalive ?? (p.persistent ? 25 : undefined) if (ka != null && ka > 0) lines.push(` persistent-keepalive=${ka} \\`) if (p.name) lines.push(` name=${p.name} \\`) if (p.clientAddress) lines.push(` client-address=${p.clientAddress} \\`) if (p.clientDns) lines.push(` client-dns=${p.clientDns} \\`) if (p.clientEndpoint) lines.push(` client-endpoint=${p.clientEndpoint} \\`) if (p.comment) lines.push(` comment="${p.comment.replace(/"/g, '\\"')}" \\`) if (lines[lines.length - 1]?.endsWith(" \\")) { lines[lines.length - 1] = lines[lines.length - 1]!.slice(0, -2) } lines.push(``) } return lines.join("\n") } export function generatePeerClientConf(args: { peerAddress?: string peerDns?: string serverPublicKey: string allowedIps?: string[] endpoint?: string persistentKeepalive?: number }): string { const lines: string[] = [] lines.push(`[Interface]`) lines.push(`# PrivateKey = <ключ клиента>`) if (args.peerAddress) lines.push(`Address = ${args.peerAddress}`) if (args.peerDns) lines.push(`DNS = ${args.peerDns}`) lines.push(``) lines.push(`[Peer]`) lines.push(`PublicKey = ${args.serverPublicKey}`) lines.push(`AllowedIPs = ${(args.allowedIps?.length ? args.allowedIps : ["0.0.0.0/0"]).join(", ")}`) if (args.endpoint) lines.push(`Endpoint = ${args.endpoint}`) if (args.persistentKeepalive != null && args.persistentKeepalive > 0) { lines.push(`PersistentKeepalive = ${args.persistentKeepalive}`) } lines.push(``) return lines.join("\n") }