Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Successful in 1m39s
Docker images / frontend-image (push) Successful in 2m56s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 53s
Docker images / publish-release (push) Successful in 10s
Added comprehensive support for managing WireGuard interfaces, including CRUD operations and peer management. Updated permissions to include access control for WireGuard routes. Enhanced the UI components to display and interact with WireGuard configurations, improving user experience and functionality. Introduced new tests for WireGuard-related functionalities to ensure reliability.
340 lines
11 KiB
TypeScript
340 lines
11 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) 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")
|
|
}
|