343 lines
12 KiB
TypeScript
343 lines
12 KiB
TypeScript
/**
|
|
* 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<string, string> {
|
|
const out: Record<string, string> = {}
|
|
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?.trim()) lines.push(`Address = ${args.peerAddress.trim()}`)
|
|
else lines.push(`# Address = <укажите адрес клиента>`)
|
|
if (args.peerDns) lines.push(`DNS = ${args.peerDns}`)
|
|
lines.push(``)
|
|
lines.push(`[Peer]`)
|
|
lines.push(`PublicKey = ${args.serverPublicKey}`)
|
|
if (args.allowedIps?.length) lines.push(`AllowedIPs = ${args.allowedIps.join(", ")}`)
|
|
else lines.push(`# AllowedIPs = <укажите сети через туннель>`)
|
|
if (args.endpoint) lines.push(`Endpoint = ${args.endpoint}`)
|
|
else lines.push(`# Endpoint = <укажите адрес сервера и порт>`)
|
|
if (args.persistentKeepalive != null && args.persistentKeepalive > 0) {
|
|
lines.push(`PersistentKeepalive = ${args.persistentKeepalive}`)
|
|
}
|
|
lines.push(``)
|
|
return lines.join("\n")
|
|
}
|