fix(wireguard): проверять конфигурацию до импорта

This commit is contained in:
Denozordec committed 2026-10-09 08:39:36 +07:00
1 parent 7ce43adc07
commit 479f2adc78
14 files changed
+247 -30

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@
"test:probes": "tsx src/routes/probes.test.ts && tsx src/routes/probes.cancel.test.ts", "test:probes": "tsx src/routes/probes.test.ts && tsx src/routes/probes.cancel.test.ts",
"test:exec": "tsx src/routes/exec.test.ts && tsx src/plugins/auth.exec.test.ts", "test:exec": "tsx src/routes/exec.test.ts && tsx src/plugins/auth.exec.test.ts",
"test:auth": "tsx src/lib/permissions.test.ts && tsx src/plugins/auth.smoke.test.ts && tsx src/plugins/auth.ospf-optimizer.test.ts", "test:auth": "tsx src/lib/permissions.test.ts && tsx src/plugins/auth.smoke.test.ts && tsx src/plugins/auth.ospf-optimizer.test.ts",
"test:wireguard": "npx tsx src/services/wireguard-config.test.ts", "test:wireguard": "npx tsx src/services/wireguard-config.test.ts && npx tsx --test src/services/wireguard-import.test.ts && npx tsx src/routes/wireguard-import.test.ts",
"test:traffic-rate": "tsx src/services/traffic-rate.test.ts", "test:traffic-rate": "tsx src/services/traffic-rate.test.ts",
"test:traffic-flow": "tsx src/services/traffic-flow-parse.test.ts && tsx src/services/traffic-flow-map-exporter.test.ts && tsx src/services/traffic-flow-ifaces.test.ts && tsx src/services/traffic-flow-ifindex.test.ts && tsx src/services/traffic-flow-dedup.test.ts && tsx src/services/traffic-flow-planes.test.ts && tsx src/services/traffic-flow-ip.test.ts && tsx src/services/traffic-flow-dest.test.ts && tsx src/services/traffic-flow-classify.test.ts && tsx src/services/traffic-flow-ripe.test.ts && tsx src/services/traffic-flow-brands.test.ts && tsx src/services/traffic-flow-ingest.test.ts && tsx src/services/traffic-flow-analytics.test.ts && tsx src/services/traffic-flow-map-hops.test.ts && tsx src/services/traffic-flow-purge.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/services/traffic-flow-facts.test.ts && tsx src/services/traffic-flow-facts-filter.test.ts && tsx src/services/traffic-flow-facts-rebuild.test.ts && tsx src/services/statistics-aggregate.test.ts", "test:traffic-flow": "tsx src/services/traffic-flow-parse.test.ts && tsx src/services/traffic-flow-map-exporter.test.ts && tsx src/services/traffic-flow-ifaces.test.ts && tsx src/services/traffic-flow-ifindex.test.ts && tsx src/services/traffic-flow-dedup.test.ts && tsx src/services/traffic-flow-planes.test.ts && tsx src/services/traffic-flow-ip.test.ts && tsx src/services/traffic-flow-dest.test.ts && tsx src/services/traffic-flow-classify.test.ts && tsx src/services/traffic-flow-ripe.test.ts && tsx src/services/traffic-flow-brands.test.ts && tsx src/services/traffic-flow-ingest.test.ts && tsx src/services/traffic-flow-analytics.test.ts && tsx src/services/traffic-flow-map-hops.test.ts && tsx src/services/traffic-flow-purge.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/services/traffic-flow-facts.test.ts && tsx src/services/traffic-flow-facts-filter.test.ts && tsx src/services/traffic-flow-facts-rebuild.test.ts && tsx src/services/statistics-aggregate.test.ts",
"test:users": "tsx src/modules/users/iface-type.test.ts && tsx src/modules/users/bindings.test.ts && tsx src/modules/users/interface-inventory.test.ts && tsx src/routes/users-catalog.test.ts", "test:users": "tsx src/modules/users/iface-type.test.ts && tsx src/modules/users/bindings.test.ts && tsx src/modules/users/interface-inventory.test.ts && tsx src/routes/users-catalog.test.ts",
@@ -0,0 +1,27 @@
import assert from "node:assert/strict"
import Fastify from "fastify"
import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod"
import wireguardRoutes from "./wireguard.js"
import { MikrotikClient } from "../services/mikrotik.js"
const app = Fastify()
app.setValidatorCompiler(validatorCompiler)
app.setSerializerCompiler(serializerCompiler)
await app.register(wireguardRoutes, { prefix: "/api" })
let mutations = 0
const originalPut = MikrotikClient.prototype.put
MikrotikClient.prototype.put = async () => { mutations++; throw new Error("Unexpected RouterOS mutation") }
try {
for (const dryRun of [false, true]) {
for (const content of ["[Interface]\n# PrivateKey = placeholder", "[Peer]\nPublicKey = placeholder"]) {
const response = await app.inject({ method: "POST", url: "/api/wireguard/import", payload: { serverId: "1", content, format: "conf", dryRun } })
assert.equal(response.statusCode, 400)
assert.match(response.json().error, /PrivateKey|Interface/)
}
}
assert.equal(mutations, 0)
} finally {
MikrotikClient.prototype.put = originalPut
await app.close()
}
console.log("WireGuard import HTTP: invalid template/section rejected for apply and dry-run, zero mutations")
+7 -3
View File
@@ -12,11 +12,11 @@ import {
} from "@mmapp/contracts/wireguard" } from "@mmapp/contracts/wireguard"
import { MikrotikClient, MikrotikError } from "../services/mikrotik.js" import { MikrotikClient, MikrotikError } from "../services/mikrotik.js"
import { deleteWireguardInterface } from "../services/wireguard-delete.js" import { deleteWireguardInterface } from "../services/wireguard-delete.js"
import { parseWireguardImport } from "../services/wireguard-import.js"
import { import {
generateMikrotikRsc, generateMikrotikRsc,
generateNativeConf, generateNativeConf,
generatePeerClientConf, generatePeerClientConf,
parseWgConfig,
type WgParsedConfig, type WgParsedConfig,
} from "../services/wireguard-config.js" } from "../services/wireguard-config.js"
import { import {
@@ -125,7 +125,7 @@ async function applyParsedConfig(
peerToRosBody({ peerToRosBody({
interfaceName: name, interfaceName: name,
publicKey: p.publicKey, publicKey: p.publicKey,
allowedAddresses: p.allowedAddresses.length ? p.allowedAddresses : ["0.0.0.0/0"], allowedAddresses: p.allowedAddresses,
endpointAddress: p.endpointAddress, endpointAddress: p.endpointAddress,
endpointPort: p.endpointPort, endpointPort: p.endpointPort,
persistentKeepalive: p.persistentKeepalive, persistentKeepalive: p.persistentKeepalive,
@@ -345,7 +345,7 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => {
const body = parsed.data const body = parsed.data
let config: WgParsedConfig let config: WgParsedConfig
try { try {
config = parseWgConfig(body.content, body.format) config = parseWireguardImport(body.content, body.format)
} catch (e) { } catch (e) {
return reply.status(400).send({ error: e instanceof Error ? e.message : "Ошибка разбора конфига" }) return reply.status(400).send({ error: e instanceof Error ? e.message : "Ошибка разбора конфига" })
} }
@@ -358,6 +358,10 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => {
if (!server) return reply.status(404).send({ error: "Сервер не найден" }) if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server) const client = MikrotikClient.fromServer(server)
try { try {
const existing = await client.get<Array<{ name?: string }>>("/interface/wireguard")
if (existing.some(iface => iface.name === config.interface.name)) {
return reply.status(409).send({ error: "Интерфейс с таким именем уже существует. Укажите другое имя в конфигурации", preview })
}
const applied = await applyParsedConfig(client, config) const applied = await applyParsedConfig(client, config)
await recordWireguard(server, "copy") await recordWireguard(server, "copy")
return reply.send({ dryRun: false, preview, applied }) return reply.send({ dryRun: false, preview, applied })
+11 -11
View File
@@ -136,8 +136,8 @@ export function parseNativeConf(content: string): WgParsedConfig {
if (section === "interface") { if (section === "interface") {
if (key === "privatekey") iface.privateKey = value if (key === "privatekey") iface.privateKey = value
else if (key === "address") iface.address = value.split(",")[0]?.trim() else if (key === "address") iface.address = value.split(",")[0]?.trim()
else if (key === "listenport") iface.listenPort = Number.parseInt(value, 10) || undefined else if (key === "listenport") iface.listenPort = Number(value)
else if (key === "mtu") iface.mtu = Number.parseInt(value, 10) || undefined else if (key === "mtu") iface.mtu = Number(value)
else if (key === "name") iface.name = value || iface.name else if (key === "name") iface.name = value || iface.name
} else if (section === "peer" && currentPeer) { } else if (section === "peer" && currentPeer) {
if (key === "publickey") currentPeer.publicKey = value if (key === "publickey") currentPeer.publicKey = value
@@ -150,16 +150,16 @@ export function parseNativeConf(content: string): WgParsedConfig {
const lastColon = value.lastIndexOf(":") const lastColon = value.lastIndexOf(":")
if (lastColon > 0 && !value.includes("]:")) { if (lastColon > 0 && !value.includes("]:")) {
currentPeer.endpointAddress = value.slice(0, lastColon) currentPeer.endpointAddress = value.slice(0, lastColon)
currentPeer.endpointPort = Number.parseInt(value.slice(lastColon + 1), 10) || undefined currentPeer.endpointPort = Number(value.slice(lastColon + 1))
} else if (value.startsWith("[") && value.includes("]:")) { } else if (value.startsWith("[") && value.includes("]:")) {
const idx = value.indexOf("]:") const idx = value.indexOf("]:")
currentPeer.endpointAddress = value.slice(1, idx) currentPeer.endpointAddress = value.slice(1, idx)
currentPeer.endpointPort = Number.parseInt(value.slice(idx + 2), 10) || undefined currentPeer.endpointPort = Number(value.slice(idx + 2))
} else { } else {
currentPeer.endpointAddress = value currentPeer.endpointAddress = value
} }
} else if (key === "persistentkeepalive") { } else if (key === "persistentkeepalive") {
currentPeer.persistentKeepalive = Number.parseInt(value, 10) || undefined currentPeer.persistentKeepalive = Number(value)
} else if (key === "presharedkey") { } else if (key === "presharedkey") {
// ignore PSK for ROS import for now // ignore PSK for ROS import for now
} }
@@ -187,8 +187,8 @@ export function parseMikrotikRsc(content: string): WgParsedConfig {
) { ) {
const kv = parseKvLine(line) const kv = parseKvLine(line)
if (kv.name) iface.name = kv.name if (kv.name) iface.name = kv.name
if (kv["listen-port"]) iface.listenPort = Number.parseInt(kv["listen-port"], 10) || undefined if (kv["listen-port"]) iface.listenPort = Number(kv["listen-port"])
if (kv.mtu) iface.mtu = Number.parseInt(kv.mtu, 10) || undefined if (kv.mtu) iface.mtu = Number(kv.mtu)
if (kv["private-key"]) iface.privateKey = kv["private-key"] if (kv["private-key"]) iface.privateKey = kv["private-key"]
if (kv.comment) iface.comment = kv.comment if (kv.comment) iface.comment = kv.comment
if (kv.disabled === "yes") iface.disabled = true if (kv.disabled === "yes") iface.disabled = true
@@ -207,13 +207,13 @@ export function parseMikrotikRsc(content: string): WgParsedConfig {
.filter(Boolean) .filter(Boolean)
peers.push({ peers.push({
publicKey: kv["public-key"] ?? "", publicKey: kv["public-key"] ?? "",
allowedAddresses: allowed.length ? allowed : ["0.0.0.0/0"], allowedAddresses: allowed,
endpointAddress: kv["endpoint-address"], endpointAddress: kv["endpoint-address"],
endpointPort: kv["endpoint-port"] endpointPort: kv["endpoint-port"]
? Number.parseInt(kv["endpoint-port"], 10) || undefined ? Number(kv["endpoint-port"])
: undefined, : undefined,
persistentKeepalive: kv["persistent-keepalive"] persistentKeepalive: kv["persistent-keepalive"]
? Number.parseInt(kv["persistent-keepalive"], 10) || undefined ? Number(kv["persistent-keepalive"])
: undefined, : undefined,
comment: kv.comment, comment: kv.comment,
name: kv.name, name: kv.name,
@@ -232,7 +232,7 @@ export function parseMikrotikRsc(content: string): WgParsedConfig {
} }
} }
if (!foundIface && peers.length === 0) { if (!foundIface) {
throw new Error("Не удалось распознать RouterOS WireGuard .rsc") throw new Error("Не удалось распознать RouterOS WireGuard .rsc")
} }
return { format: "rsc", interface: iface, peers } return { format: "rsc", interface: iface, peers }
@@ -0,0 +1,51 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { parseWireguardImport } from "./wireguard-import.js"
import { generatePeerClientConf } from "./wireguard-config.js"
const key = Buffer.alloc(32, 7).toString("base64")
const conf = `[Interface]\nPrivateKey = ${key}\nAddress = 198.18.255.241/30\nListenPort = 51879\n[Peer]\nPublicKey = ${key}\nAllowedIPs = 198.18.255.242/32\nEndpoint = 192.0.2.1:51879\n`
test("complete conf accepts the documented wg0 name and explicit routes", () => {
const parsed = parseWireguardImport(conf, "auto")
assert.equal(parsed.interface.name, "wg0")
assert.deepEqual(parsed.peers[0]?.allowedAddresses, ["198.18.255.242/32"])
assert.equal(parseWireguardImport(conf.replace("198.18.255.242/32", "0.0.0.0/0"), "conf").peers[0]?.allowedAddresses[0], "0.0.0.0/0")
})
test("generated client template cannot be imported before it is completed", () => {
const template = generatePeerClientConf({ serverPublicKey: key, endpoint: "192.0.2.1:51879" })
assert.throws(() => parseWireguardImport(template, "conf"), /PrivateKey/)
})
for (const [label, content, message] of [
["absent interface", "[Peer]\nPublicKey = " + key, /Interface/],
["duplicate interface", conf + "[Interface]\n", /Interface/],
["missing peer key", conf.replace(`PublicKey = ${key}`, "# PublicKey absent"), /PublicKey/],
["invalid private key", conf.replace(`PrivateKey = ${key}`, "PrivateKey = placeholder"), /PrivateKey/],
["invalid peer key", conf.replace(`PublicKey = ${key}`, "PublicKey = placeholder"), /PublicKey/],
["absent routes", conf.replace("AllowedIPs = 198.18.255.242/32", "# AllowedIPs absent"), /AllowedIPs/],
["invalid prefix", conf.replace("198.18.255.242/32", "198.18.255.242/33"), /AllowedIPs/],
["extra prefix separator", conf.replace("198.18.255.242/32", "198.18.255.242/32/"), /AllowedIPs/],
["invalid address", conf.replace("198.18.255.241/30", "not-an-address"), /Address/],
["unsupported multiple addresses", conf.replace("198.18.255.241/30", "198.18.255.241/30, 198.18.255.242/30"), /нескольких/],
["unsupported PSK", conf + `PresharedKey = ${key}\n`, /PresharedKey/],
["invalid listen port", conf.replace("ListenPort = 51879", "ListenPort = 65536"), /ListenPort/],
["zero listen port", conf.replace("ListenPort = 51879", "ListenPort = 0"), /ListenPort/],
["non-numeric listen port", conf.replace("ListenPort = 51879", "ListenPort = invalid"), /ListenPort/],
["invalid MTU", conf.replace("[Interface]", "[Interface]\nMTU = invalid"), /MTU/],
["zero MTU", conf.replace("[Interface]", "[Interface]\nMTU = 0"), /MTU/],
["zero endpoint port", conf.replace("192.0.2.1:51879", "192.0.2.1:0"), /Endpoint/],
["invalid endpoint port", conf.replace("192.0.2.1:51879", "192.0.2.1:invalid"), /Endpoint/],
["negative keepalive", conf + "PersistentKeepalive = -1", /PersistentKeepalive/],
] as const) {
test(label, () => assert.throws(() => parseWireguardImport(content, "conf"), message))
}
test("RSC peers without routes or interface are rejected without default routes", () => {
const peer = `/interface wireguard peers add public-key="${key}"`
assert.throws(() => parseWireguardImport(peer, "rsc"), /распознать/)
assert.throws(() => parseWireguardImport(`/interface wireguard add name=own-wg\n${peer}`, "rsc"), /allowed-address/)
assert.throws(() => parseWireguardImport("/interface wireguard add name=own-wg mtu=0", "rsc"), /MTU/)
assert.deepEqual(parseWireguardImport(`/interface wireguard add name=own-wg\n${peer} allowed-address=192.0.2.0/24`, "rsc").peers[0]?.allowedAddresses, ["192.0.2.0/24"])
})
+64
View File
@@ -0,0 +1,64 @@
import { isIP } from "node:net"
import { parseWgConfig, type WgParsedConfig } from "./wireguard-config.js"
function validKey(value: string): boolean {
return /^[A-Za-z0-9+/]{43}=$/.test(value) && Buffer.from(value, "base64").length === 32
}
function validNetwork(value: string): boolean {
const parts = value.split("/")
const [address, prefix] = parts
const version = isIP(address ?? "")
return parts.length === 2 && version !== 0 && /^\d+$/.test(prefix ?? "") && Number(prefix) <= (version === 4 ? 32 : 128)
}
/** Validate the complete import before the first RouterOS mutation. Never echo keys. */
export function parseWireguardImport(content: string, format: "auto" | "rsc" | "conf"): WgParsedConfig {
const parsed = parseWgConfig(content, format)
if (parsed.format === "conf") {
if ((content.match(/^\s*\[Interface\]\s*$/gim) ?? []).length !== 1) {
throw new Error("Конфигурация .conf должна содержать одну секцию [Interface]")
}
if (!parsed.interface.privateKey) {
throw new Error("Заполните PrivateKey: клиентский шаблон нельзя применить без приватного ключа")
}
if ((content.match(/^\s*\[Peer\]\s*$/gim) ?? []).length !== parsed.peers.length) {
throw new Error("У каждого пира должен быть PublicKey")
}
if (/^\s*Address\s*=.*,/im.test(content)) {
throw new Error("Импорт нескольких адресов Address пока не поддерживается; укажите один адрес")
}
}
if (/^\s*PresharedKey\s*=\s*\S/im.test(content) || /\bpreshared-key\s*=/i.test(content)) {
throw new Error("Импорт PresharedKey пока не поддерживается; конфигурация не применена")
}
if (!parsed.interface.name.trim()) throw new Error("Укажите имя интерфейса")
if (parsed.interface.privateKey && !validKey(parsed.interface.privateKey)) {
throw new Error("Некорректный PrivateKey: нужен ключ WireGuard длиной 32 байта в base64")
}
if (parsed.interface.address && !validNetwork(parsed.interface.address)) {
throw new Error("Некорректный Address: укажите IPv4 или IPv6 адрес с префиксом")
}
if (parsed.interface.listenPort !== undefined && (!Number.isInteger(parsed.interface.listenPort) || parsed.interface.listenPort < 1 || parsed.interface.listenPort > 65535)) {
throw new Error("ListenPort должен быть от 1 до 65535")
}
if (parsed.interface.mtu !== undefined && (!Number.isInteger(parsed.interface.mtu) || parsed.interface.mtu <= 0)) {
throw new Error("MTU должен быть положительным целым числом")
}
for (const peer of parsed.peers) {
if (!validKey(peer.publicKey)) throw new Error("Некорректный PublicKey пира")
if (!peer.allowedAddresses.length || peer.allowedAddresses.some(value => !validNetwork(value))) {
throw new Error("Заполните AllowedIPs / allowed-address каждого пира корректными сетями с префиксом")
}
if (peer.endpointPort !== undefined && (!Number.isInteger(peer.endpointPort) || peer.endpointPort < 1 || peer.endpointPort > 65535)) {
throw new Error("Порт Endpoint должен быть от 1 до 65535")
}
if (parsed.format === "conf" && peer.endpointAddress && peer.endpointPort === undefined) {
throw new Error("Укажите порт Endpoint после адреса")
}
if (peer.persistentKeepalive !== undefined && (!Number.isInteger(peer.persistentKeepalive) || peer.persistentKeepalive < 0)) {
throw new Error("PersistentKeepalive должен быть неотрицательным целым числом")
}
}
return parsed
}
+3 -1
View File
@@ -92,7 +92,9 @@ function WgImportSheet({
<SheetHeader className="px-6 pt-6 pb-4 border-b shrink-0"> <SheetHeader className="px-6 pt-6 pb-4 border-b shrink-0">
<SheetTitle>Импорт конфига WireGuard</SheetTitle> <SheetTitle>Импорт конфига WireGuard</SheetTitle>
<SheetDescription> <SheetDescription>
Native .conf или MikroTik .rsc → применить на выбранный роутер Native .conf или MikroTik .rsc → применить на выбранный роутер.
В .conf без Name используется имя wg0. Заполните ключ и сети пиров;
совпадение имени остановит импорт.
</SheetDescription> </SheetDescription>
</SheetHeader> </SheetHeader>
+12
View File
@@ -553,3 +553,15 @@ Native проверка после UI восстановила name/comment,list
Own interfaces/peers/IP/connected routes удалены; WG/peers/IP/routes/IPv4 filter/nat/mangle совпали с исходным baseline на обоих CHR. Остались три основные службы, backend healthy. Business DB controls совпадают с контрольной точкой67:servers2,speedHistory9,retentionDays14,migrations9; audit revisions намеренно сохранены. Публичные JSON проверены на отсутствие ключей/конфигураций/паролей. Общие ReUI компоненты и product source не менялись; общая регрессия не повторялась для внешней приёмки. Own interfaces/peers/IP/connected routes удалены; WG/peers/IP/routes/IPv4 filter/nat/mangle совпали с исходным baseline на обоих CHR. Остались три основные службы, backend healthy. Business DB controls совпадают с контрольной точкой67:servers2,speedHistory9,retentionDays14,migrations9; audit revisions намеренно сохранены. Публичные JSON проверены на отсутствие ключей/конфигураций/паролей. Общие ReUI компоненты и product source не менялись; общая регрессия не повторялась для внешней приёмки.
Граница результата: правильный RSC собственного туннеля, preview и успешное применение с независимой связностью. Импорт .conf не принят: exporter не сохраняет имя/комментарии, parser использует wg0 по умолчанию; incomplete Peer .conf template требует отдельной проверки до применения. Partial failure/rollback, повторный import/коллизии, private-key UI download,IPv6/throughput/RBAC/внешние failures/multi-instance/Telegram/Portal и остальные протоколы/интеграции остаются открытыми. Следующий12.5.69 — preflight/ошибки .conf и затем безопасный собственный roundtrip. Локальный коммит без push. Граница результата: правильный RSC собственного туннеля, preview и успешное применение с независимой связностью. Импорт .conf не принят: exporter не сохраняет имя/комментарии, parser использует wg0 по умолчанию; incomplete Peer .conf template требует отдельной проверки до применения. Partial failure/rollback, повторный import/коллизии, private-key UI download,IPv6/throughput/RBAC/внешние failures/multi-instance/Telegram/Portal и остальные протоколы/интеграции остаются открытыми. Следующий12.5.69 — preflight/ошибки .conf и затем безопасный собственный roundtrip. Локальный коммит без push.
## 12.5.69 — preflight импорта WireGuard без выдуманных маршрутов (2026-10-09)
Обнаружено локально: parse/apply могли принять клиентский шаблон без PrivateKey и подставить0.0.0.0/0 вместо отсутствующих allowed-address. Добавлен backend/src/services/wireguard-import.ts; route использует его до dry-run, поиска сервера и любых RouterOS mutations. .conf должен содержать ровно одну Interface секцию и PrivateKey; каждый объявленный Peer должен иметь PublicKey и явные корректные AllowedIPs. Проверяются32байтные base64 ключи, IPv4/IPv6 CIDR, порты, положительный MTU и неотрицательный keepalive. Неподдерживаемые несколько Address и PresharedKey отклоняются явно вместо потери данных. Ошибки ключей не выводят их значения.
Backend/frontend RSC parsers больше не подставляют default route и требуют interface command; applyParsedConfig использует только проверенные явные сети. Явно заданный0.0.0.0/0 остаётся допустимым. Числа сохраняются для проверки вместо parseInt/undefined defaults, включая0 иNaN. Перед обычным apply route читает список интерфейсов и возвращает409 при занятом имени; это предварительная проверка, не блокировка конкурентных запросов. Dry-run проверяет формат, но не обещает доступность сервера или отсутствие конфликтов на момент применения.
В существующем WgImportSheet добавлено короткое пояснение: .conf безName использует wg0; ключ/сети надо заполнить, совпадение имени остановит import. Структура Sheet/base-nova, формы, aria labels/status/alert и управление busy не менялись. Локальный preview остаётся разбором формата; обязательная preflight проверка выполняется backend. Новых API полей/зависимостей нет. Полный протокол .conf или поддержку PSK этот блок не добавляет.
Проверки: wireguard-import unit22/22 (готовый conf и explicit default route, template, Interface/Peer/key errors, CIDR, отсутствие маршрутов, несколькоAddress/PSK, числовые ошибки и RSC); настоящий Fastify inject4 invalid apply/dryRun случая →400, PUT mutations0. Backend test:wireguard теперь включает новые service/route tests; исходный codec passed. Client-template cross-layer5/5 passed. Browser3/3: доступ/неподтверждённый receipt, ожидание/повтор,400 incomplete conf с сохранением текста и выбранного сервера2. Последний test не попал в первый grep импорт; отдельно запущен и прошёл. Backend/scoped frontend TypeScript, ESLint и diff check passed. ReUI checklist checked; installed primitives не менялись.
Исправление локальное, стенд по-прежнему c16fc0c. Реальные preflight400/409, отсутствие native изменений при отказе, production build и новый .conf roundtrip ещё не приняты. Проверка коллизии409 добавлена в code, отдельно не доказана RouterOS fixture в этом блоке. Следующий12.5.70 — собрать/развернуть, проверить отказы и собственную пару на обоих CHR, native параметры/идентичность/связность, затем cleanup/baseline. Конкуренция, повторный import/partial failure/rollback, неизвестные/повторные поля .conf и остальные внешние ограничения остаются открытыми. Пароли и RouterOS настройки в этом блоке не менялись. Локальный коммит без push.
+1 -1
View File
@@ -1,6 +1,6 @@
# План реализации UX/UI по пятичасовым окнам # План реализации UX/UI по пятичасовым окнам
Актуальное продолжение 2026-10-09: 12.5.68 подтвердил настоящий .rsc export → dry-run → delete → UI import на обоих CHR. Native параметры, публичная идентичность и эквивалентность private scalar сохранены; handshake/Ping3/3 в обе стороны, cleanup/baseline и DB controls passed. Runtime backend/frontend c16fc0c healthy. Следующий12.5.69 — проверить безопасный импорт .conf: имя устройства/интерфейса, неполный клиентский шаблон и ошибки до применения; затем собственный native roundtrip. Этап12.5 открыт. Актуальное продолжение 2026-10-09: 12.5.69 локально добавил проверки импорта WireGuard до применения: неполный .conf/неверные ключи и сети/неподдерживаемый PSK отклоняются, отсутствие AllowedIPs больше не создаёт default route; совпадение имени даёт409. Unit22/22, HTTP4 invalid cases, client-template5/5, import browser3/3, backend/frontend TypeScript и ESLint passed. Runtime остаётся backend/frontend c16fc0c. Следующий12.5.70 — Docker build/deploy, реальные400/409 без native изменений и собственный .conf roundtrip на обоих CHR с очисткой. Этап12.5 открыт.
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-08. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-08. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
+14 -2
View File
@@ -1,6 +1,6 @@
# UX/UI: журнал реализации # UX/UI: журнал реализации
Актуальное продолжение 2026-10-09: 12.5.68 подтвердил настоящий .rsc export → dry-run → delete → UI import на обоих CHR. Native параметры, публичная идентичность и эквивалентность private scalar сохранены; handshake/Ping3/3 в обе стороны, cleanup/baseline и DB controls passed. Runtime backend/frontend c16fc0c healthy. Следующий12.5.69 — проверить безопасный импорт .conf: имя устройства/интерфейса, неполный клиентский шаблон и ошибки до применения; затем собственный native roundtrip. Этап12.5 открыт. Актуальное продолжение 2026-10-09: 12.5.69 локально добавил проверки импорта WireGuard до применения: неполный .conf/неверные ключи и сети/неподдерживаемый PSK отклоняются, отсутствие AllowedIPs больше не создаёт default route; совпадение имени даёт409. Unit22/22, HTTP4 invalid cases, client-template5/5, import browser3/3, backend/frontend TypeScript и ESLint passed. Runtime остаётся backend/frontend c16fc0c. Следующий12.5.70 — Docker build/deploy, реальные400/409 без native изменений и собственный .conf roundtrip на обоих CHR с очисткой. Этап12.5 открыт.
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md). Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
@@ -34,7 +34,19 @@
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 | | 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–68 и ограничения ниже. Runtime backend/frontend c16fc0c. | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–69 и ограничения ниже. Runtime backend/frontend c16fc0c. |
## 12.5.69 — preflight импорта WireGuard без выдуманных маршрутов (2026-10-09)
Обнаружено локально: parse/apply могли принять клиентский шаблон без PrivateKey и подставить0.0.0.0/0 вместо отсутствующих allowed-address. Добавлен backend/src/services/wireguard-import.ts; route использует его до dry-run, поиска сервера и любых RouterOS mutations. .conf должен содержать ровно одну Interface секцию и PrivateKey; каждый объявленный Peer должен иметь PublicKey и явные корректные AllowedIPs. Проверяются32байтные base64 ключи, IPv4/IPv6 CIDR, порты, положительный MTU и неотрицательный keepalive. Неподдерживаемые несколько Address и PresharedKey отклоняются явно вместо потери данных. Ошибки ключей не выводят их значения.
Backend/frontend RSC parsers больше не подставляют default route и требуют interface command; applyParsedConfig использует только проверенные явные сети. Явно заданный0.0.0.0/0 остаётся допустимым. Числа сохраняются для проверки вместо parseInt/undefined defaults, включая0 иNaN. Перед обычным apply route читает список интерфейсов и возвращает409 при занятом имени; это предварительная проверка, не блокировка конкурентных запросов. Dry-run проверяет формат, но не обещает доступность сервера или отсутствие конфликтов на момент применения.
В существующем WgImportSheet добавлено короткое пояснение: .conf безName использует wg0; ключ/сети надо заполнить, совпадение имени остановит import. Структура Sheet/base-nova, формы, aria labels/status/alert и управление busy не менялись. Локальный preview остаётся разбором формата; обязательная preflight проверка выполняется backend. Новых API полей/зависимостей нет. Полный протокол .conf или поддержку PSK этот блок не добавляет.
Проверки: wireguard-import unit22/22 (готовый conf и explicit default route, template, Interface/Peer/key errors, CIDR, отсутствие маршрутов, несколькоAddress/PSK, числовые ошибки и RSC); настоящий Fastify inject4 invalid apply/dryRun случая →400, PUT mutations0. Backend test:wireguard теперь включает новые service/route tests; исходный codec passed. Client-template cross-layer5/5 passed. Browser3/3: доступ/неподтверждённый receipt, ожидание/повтор,400 incomplete conf с сохранением текста и выбранного сервера2. Последний test не попал в первый grep импорт; отдельно запущен и прошёл. Backend/scoped frontend TypeScript, ESLint и diff check passed. ReUI checklist checked; installed primitives не менялись.
Исправление локальное, стенд по-прежнему c16fc0c. Реальные preflight400/409, отсутствие native изменений при отказе, production build и новый .conf roundtrip ещё не приняты. Проверка коллизии409 добавлена в code, отдельно не доказана RouterOS fixture в этом блоке. Следующий12.5.70 — собрать/развернуть, проверить отказы и собственную пару на обоих CHR, native параметры/идентичность/связность, затем cleanup/baseline. Конкуренция, повторный import/partial failure/rollback, неизвестные/повторные поля .conf и остальные внешние ограничения остаются открытыми. Пароли и RouterOS настройки в этом блоке не менялись. Локальный коммит без push.
## 12.5.68 — реальный .rsc roundtrip WireGuard через UI (2026-10-09) ## 12.5.68 — реальный .rsc roundtrip WireGuard через UI (2026-10-09)
+13 -1
View File
@@ -1,6 +1,6 @@
# Этап 12: итоговая приёмка UX/UI # Этап 12: итоговая приёмка UX/UI
Актуальное продолжение 2026-10-09: 12.5.68 подтвердил настоящий .rsc export → dry-run → delete → UI import на обоих CHR. Native параметры, публичная идентичность и эквивалентность private scalar сохранены; handshake/Ping3/3 в обе стороны, cleanup/baseline и DB controls passed. Runtime backend/frontend c16fc0c healthy. Следующий12.5.69 — проверить безопасный импорт .conf: имя устройства/интерфейса, неполный клиентский шаблон и ошибки до применения; затем собственный native roundtrip. Этап12.5 открыт. Актуальное продолжение 2026-10-09: 12.5.69 локально добавил проверки импорта WireGuard до применения: неполный .conf/неверные ключи и сети/неподдерживаемый PSK отклоняются, отсутствие AllowedIPs больше не создаёт default route; совпадение имени даёт409. Unit22/22, HTTP4 invalid cases, client-template5/5, import browser3/3, backend/frontend TypeScript и ESLint passed. Runtime остаётся backend/frontend c16fc0c. Следующий12.5.70 — Docker build/deploy, реальные400/409 без native изменений и собственный .conf roundtrip на обоих CHR с очисткой. Этап12.5 открыт.
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push. План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
@@ -1718,3 +1718,15 @@ Native проверка после UI восстановила name/comment,list
Own interfaces/peers/IP/connected routes удалены; WG/peers/IP/routes/IPv4 filter/nat/mangle совпали с исходным baseline на обоих CHR. Остались три основные службы, backend healthy. Business DB controls совпадают с контрольной точкой67:servers2,speedHistory9,retentionDays14,migrations9; audit revisions намеренно сохранены. Публичные JSON проверены на отсутствие ключей/конфигураций/паролей. Общие ReUI компоненты и product source не менялись; общая регрессия не повторялась для внешней приёмки. Own interfaces/peers/IP/connected routes удалены; WG/peers/IP/routes/IPv4 filter/nat/mangle совпали с исходным baseline на обоих CHR. Остались три основные службы, backend healthy. Business DB controls совпадают с контрольной точкой67:servers2,speedHistory9,retentionDays14,migrations9; audit revisions намеренно сохранены. Публичные JSON проверены на отсутствие ключей/конфигураций/паролей. Общие ReUI компоненты и product source не менялись; общая регрессия не повторялась для внешней приёмки.
Граница результата: правильный RSC собственного туннеля, preview и успешное применение с независимой связностью. Импорт .conf не принят: exporter не сохраняет имя/комментарии, parser использует wg0 по умолчанию; incomplete Peer .conf template требует отдельной проверки до применения. Partial failure/rollback, повторный import/коллизии, private-key UI download,IPv6/throughput/RBAC/внешние failures/multi-instance/Telegram/Portal и остальные протоколы/интеграции остаются открытыми. Следующий12.5.69 — preflight/ошибки .conf и затем безопасный собственный roundtrip. Локальный коммит без push. Граница результата: правильный RSC собственного туннеля, preview и успешное применение с независимой связностью. Импорт .conf не принят: exporter не сохраняет имя/комментарии, parser использует wg0 по умолчанию; incomplete Peer .conf template требует отдельной проверки до применения. Partial failure/rollback, повторный import/коллизии, private-key UI download,IPv6/throughput/RBAC/внешние failures/multi-instance/Telegram/Portal и остальные протоколы/интеграции остаются открытыми. Следующий12.5.69 — preflight/ошибки .conf и затем безопасный собственный roundtrip. Локальный коммит без push.
## 12.5.69 — preflight импорта WireGuard без выдуманных маршрутов (2026-10-09)
Обнаружено локально: parse/apply могли принять клиентский шаблон без PrivateKey и подставить0.0.0.0/0 вместо отсутствующих allowed-address. Добавлен backend/src/services/wireguard-import.ts; route использует его до dry-run, поиска сервера и любых RouterOS mutations. .conf должен содержать ровно одну Interface секцию и PrivateKey; каждый объявленный Peer должен иметь PublicKey и явные корректные AllowedIPs. Проверяются32байтные base64 ключи, IPv4/IPv6 CIDR, порты, положительный MTU и неотрицательный keepalive. Неподдерживаемые несколько Address и PresharedKey отклоняются явно вместо потери данных. Ошибки ключей не выводят их значения.
Backend/frontend RSC parsers больше не подставляют default route и требуют interface command; applyParsedConfig использует только проверенные явные сети. Явно заданный0.0.0.0/0 остаётся допустимым. Числа сохраняются для проверки вместо parseInt/undefined defaults, включая0 иNaN. Перед обычным apply route читает список интерфейсов и возвращает409 при занятом имени; это предварительная проверка, не блокировка конкурентных запросов. Dry-run проверяет формат, но не обещает доступность сервера или отсутствие конфликтов на момент применения.
В существующем WgImportSheet добавлено короткое пояснение: .conf безName использует wg0; ключ/сети надо заполнить, совпадение имени остановит import. Структура Sheet/base-nova, формы, aria labels/status/alert и управление busy не менялись. Локальный preview остаётся разбором формата; обязательная preflight проверка выполняется backend. Новых API полей/зависимостей нет. Полный протокол .conf или поддержку PSK этот блок не добавляет.
Проверки: wireguard-import unit22/22 (готовый conf и explicit default route, template, Interface/Peer/key errors, CIDR, отсутствие маршрутов, несколькоAddress/PSK, числовые ошибки и RSC); настоящий Fastify inject4 invalid apply/dryRun случая →400, PUT mutations0. Backend test:wireguard теперь включает новые service/route tests; исходный codec passed. Client-template cross-layer5/5 passed. Browser3/3: доступ/неподтверждённый receipt, ожидание/повтор,400 incomplete conf с сохранением текста и выбранного сервера2. Последний test не попал в первый grep импорт; отдельно запущен и прошёл. Backend/scoped frontend TypeScript, ESLint и diff check passed. ReUI checklist checked; installed primitives не менялись.
Исправление локальное, стенд по-прежнему c16fc0c. Реальные preflight400/409, отсутствие native изменений при отказе, production build и новый .conf roundtrip ещё не приняты. Проверка коллизии409 добавлена в code, отдельно не доказана RouterOS fixture в этом блоке. Следующий12.5.70 — собрать/развернуть, проверить отказы и собственную пару на обоих CHR, native параметры/идентичность/связность, затем cleanup/baseline. Конкуренция, повторный import/partial failure/rollback, неизвестные/повторные поля .conf и остальные внешние ограничения остаются открытыми. Пароли и RouterOS настройки в этом блоке не менялись. Локальный коммит без push.
+12
View File
@@ -561,3 +561,15 @@ Native проверка после UI восстановила name/comment,list
Own interfaces/peers/IP/connected routes удалены; WG/peers/IP/routes/IPv4 filter/nat/mangle совпали с исходным baseline на обоих CHR. Остались три основные службы, backend healthy. Business DB controls совпадают с контрольной точкой67:servers2,speedHistory9,retentionDays14,migrations9; audit revisions намеренно сохранены. Публичные JSON проверены на отсутствие ключей/конфигураций/паролей. Общие ReUI компоненты и product source не менялись; общая регрессия не повторялась для внешней приёмки. Own interfaces/peers/IP/connected routes удалены; WG/peers/IP/routes/IPv4 filter/nat/mangle совпали с исходным baseline на обоих CHR. Остались три основные службы, backend healthy. Business DB controls совпадают с контрольной точкой67:servers2,speedHistory9,retentionDays14,migrations9; audit revisions намеренно сохранены. Публичные JSON проверены на отсутствие ключей/конфигураций/паролей. Общие ReUI компоненты и product source не менялись; общая регрессия не повторялась для внешней приёмки.
Граница результата: правильный RSC собственного туннеля, preview и успешное применение с независимой связностью. Импорт .conf не принят: exporter не сохраняет имя/комментарии, parser использует wg0 по умолчанию; incomplete Peer .conf template требует отдельной проверки до применения. Partial failure/rollback, повторный import/коллизии, private-key UI download,IPv6/throughput/RBAC/внешние failures/multi-instance/Telegram/Portal и остальные протоколы/интеграции остаются открытыми. Следующий12.5.69 — preflight/ошибки .conf и затем безопасный собственный roundtrip. Локальный коммит без push. Граница результата: правильный RSC собственного туннеля, preview и успешное применение с независимой связностью. Импорт .conf не принят: exporter не сохраняет имя/комментарии, parser использует wg0 по умолчанию; incomplete Peer .conf template требует отдельной проверки до применения. Partial failure/rollback, повторный import/коллизии, private-key UI download,IPv6/throughput/RBAC/внешние failures/multi-instance/Telegram/Portal и остальные протоколы/интеграции остаются открытыми. Следующий12.5.69 — preflight/ошибки .conf и затем безопасный собственный roundtrip. Локальный коммит без push.
## 12.5.69 — preflight импорта WireGuard без выдуманных маршрутов (2026-10-09)
Обнаружено локально: parse/apply могли принять клиентский шаблон без PrivateKey и подставить0.0.0.0/0 вместо отсутствующих allowed-address. Добавлен backend/src/services/wireguard-import.ts; route использует его до dry-run, поиска сервера и любых RouterOS mutations. .conf должен содержать ровно одну Interface секцию и PrivateKey; каждый объявленный Peer должен иметь PublicKey и явные корректные AllowedIPs. Проверяются32байтные base64 ключи, IPv4/IPv6 CIDR, порты, положительный MTU и неотрицательный keepalive. Неподдерживаемые несколько Address и PresharedKey отклоняются явно вместо потери данных. Ошибки ключей не выводят их значения.
Backend/frontend RSC parsers больше не подставляют default route и требуют interface command; applyParsedConfig использует только проверенные явные сети. Явно заданный0.0.0.0/0 остаётся допустимым. Числа сохраняются для проверки вместо parseInt/undefined defaults, включая0 иNaN. Перед обычным apply route читает список интерфейсов и возвращает409 при занятом имени; это предварительная проверка, не блокировка конкурентных запросов. Dry-run проверяет формат, но не обещает доступность сервера или отсутствие конфликтов на момент применения.
В существующем WgImportSheet добавлено короткое пояснение: .conf безName использует wg0; ключ/сети надо заполнить, совпадение имени остановит import. Структура Sheet/base-nova, формы, aria labels/status/alert и управление busy не менялись. Локальный preview остаётся разбором формата; обязательная preflight проверка выполняется backend. Новых API полей/зависимостей нет. Полный протокол .conf или поддержку PSK этот блок не добавляет.
Проверки: wireguard-import unit22/22 (готовый conf и explicit default route, template, Interface/Peer/key errors, CIDR, отсутствие маршрутов, несколькоAddress/PSK, числовые ошибки и RSC); настоящий Fastify inject4 invalid apply/dryRun случая →400, PUT mutations0. Backend test:wireguard теперь включает новые service/route tests; исходный codec passed. Client-template cross-layer5/5 passed. Browser3/3: доступ/неподтверждённый receipt, ожидание/повтор,400 incomplete conf с сохранением текста и выбранного сервера2. Последний test не попал в первый grep импорт; отдельно запущен и прошёл. Backend/scoped frontend TypeScript, ESLint и diff check passed. ReUI checklist checked; installed primitives не менялись.
Исправление локальное, стенд по-прежнему c16fc0c. Реальные preflight400/409, отсутствие native изменений при отказе, production build и новый .conf roundtrip ещё не приняты. Проверка коллизии409 добавлена в code, отдельно не доказана RouterOS fixture в этом блоке. Следующий12.5.70 — собрать/развернуть, проверить отказы и собственную пару на обоих CHR, native параметры/идентичность/связность, затем cleanup/baseline. Конкуренция, повторный import/partial failure/rollback, неизвестные/повторные поля .conf и остальные внешние ограничения остаются открытыми. Пароли и RouterOS настройки в этом блоке не менялись. Локальный коммит без push.
+10 -10
View File
@@ -136,8 +136,8 @@ export function parseNativeConf(content: string): WgParsedConfig {
if (section === "interface") { if (section === "interface") {
if (key === "privatekey") iface.privateKey = value if (key === "privatekey") iface.privateKey = value
else if (key === "address") iface.address = value.split(",")[0]?.trim() else if (key === "address") iface.address = value.split(",")[0]?.trim()
else if (key === "listenport") iface.listenPort = Number.parseInt(value, 10) || undefined else if (key === "listenport") iface.listenPort = Number(value)
else if (key === "mtu") iface.mtu = Number.parseInt(value, 10) || undefined else if (key === "mtu") iface.mtu = Number(value)
else if (key === "name") iface.name = value || iface.name else if (key === "name") iface.name = value || iface.name
} else if (section === "peer" && currentPeer) { } else if (section === "peer" && currentPeer) {
if (key === "publickey") currentPeer.publicKey = value if (key === "publickey") currentPeer.publicKey = value
@@ -150,12 +150,12 @@ export function parseNativeConf(content: string): WgParsedConfig {
const lastColon = value.lastIndexOf(":") const lastColon = value.lastIndexOf(":")
if (lastColon > 0 && !value.includes("]:")) { if (lastColon > 0 && !value.includes("]:")) {
currentPeer.endpointAddress = value.slice(0, lastColon) currentPeer.endpointAddress = value.slice(0, lastColon)
currentPeer.endpointPort = Number.parseInt(value.slice(lastColon + 1), 10) || undefined currentPeer.endpointPort = Number(value.slice(lastColon + 1))
} else { } else {
currentPeer.endpointAddress = value currentPeer.endpointAddress = value
} }
} else if (key === "persistentkeepalive") { } else if (key === "persistentkeepalive") {
currentPeer.persistentKeepalive = Number.parseInt(value, 10) || undefined currentPeer.persistentKeepalive = Number(value)
} }
} }
} }
@@ -179,8 +179,8 @@ export function parseMikrotikRsc(content: string): WgParsedConfig {
) { ) {
const kv = parseKvLine(line) const kv = parseKvLine(line)
if (kv.name) iface.name = kv.name if (kv.name) iface.name = kv.name
if (kv["listen-port"]) iface.listenPort = Number.parseInt(kv["listen-port"], 10) || undefined if (kv["listen-port"]) iface.listenPort = Number(kv["listen-port"])
if (kv.mtu) iface.mtu = Number.parseInt(kv.mtu, 10) || undefined if (kv.mtu) iface.mtu = Number(kv.mtu)
if (kv["private-key"]) iface.privateKey = kv["private-key"] if (kv["private-key"]) iface.privateKey = kv["private-key"]
if (kv.comment) iface.comment = kv.comment if (kv.comment) iface.comment = kv.comment
if (kv.disabled === "yes") iface.disabled = true if (kv.disabled === "yes") iface.disabled = true
@@ -199,13 +199,13 @@ export function parseMikrotikRsc(content: string): WgParsedConfig {
.filter(Boolean) .filter(Boolean)
peers.push({ peers.push({
publicKey: kv["public-key"] ?? "", publicKey: kv["public-key"] ?? "",
allowedAddresses: allowed.length ? allowed : ["0.0.0.0/0"], allowedAddresses: allowed,
endpointAddress: kv["endpoint-address"], endpointAddress: kv["endpoint-address"],
endpointPort: kv["endpoint-port"] endpointPort: kv["endpoint-port"]
? Number.parseInt(kv["endpoint-port"], 10) || undefined ? Number(kv["endpoint-port"])
: undefined, : undefined,
persistentKeepalive: kv["persistent-keepalive"] persistentKeepalive: kv["persistent-keepalive"]
? Number.parseInt(kv["persistent-keepalive"], 10) || undefined ? Number(kv["persistent-keepalive"])
: undefined, : undefined,
comment: kv.comment, comment: kv.comment,
name: kv.name, name: kv.name,
@@ -223,7 +223,7 @@ export function parseMikrotikRsc(content: string): WgParsedConfig {
} }
} }
if (!foundIface && peers.length === 0) { if (!foundIface) {
throw new Error("Не удалось распознать RouterOS WireGuard .rsc") throw new Error("Не удалось распознать RouterOS WireGuard .rsc")
} }
return { format: "rsc", interface: iface, peers } return { format: "rsc", interface: iface, peers }
+21
View File
@@ -208,6 +208,27 @@ test("WireGuard: импорт сохраняет форму без подтве
expect(attempts).toBe(5) expect(attempts).toBe(5)
}) })
test("WireGuard: ошибка проверки .conf сохраняет клиентский шаблон и выбранный сервер", async ({ page }) => {
await setup(page, "/wireguard?server=2")
const content = "[Interface]\n# PrivateKey = <заполните ключ>\n[Peer]\n# AllowedIPs = <укажите сети>"
let attempts = 0
await page.route("**/api/wireguard/import", route => {
expect(route.request().postDataJSON()).toMatchObject({ serverId: "2", content, dryRun: false })
attempts++
return route.fulfill({ status: 400, json: { error: "Заполните PrivateKey: клиентский шаблон нельзя применить без приватного ключа" } })
})
await page.getByRole("button", { name: "Импорт", exact: true }).click()
const dialog = page.getByRole("dialog", { name: "Импорт конфига WireGuard" })
await expect(dialog).toContainText("без Name используется имя wg0")
await dialog.getByLabel("Конфигурация WireGuard", { exact: true }).fill(content)
await dialog.getByRole("button", { name: "Применить на роутер" }).click()
await expect(dialog.getByRole("alert")).toContainText("Заполните PrivateKey")
await expect(dialog.getByLabel("Конфигурация WireGuard", { exact: true })).toHaveValue(content)
await expect(dialog.getByLabel("Сервер", { exact: true })).toHaveValue("2")
await expect(dialog.getByRole("button", { name: "Применить на роутер" })).toBeEnabled()
expect(attempts).toBe(1)
})
test("WireGuard: импорт удерживает форму до ответа и разрешает повтор", async ({ page }) => { test("WireGuard: импорт удерживает форму до ответа и разрешает повтор", async ({ page }) => {
await setup(page, "/wireguard?server=1") await setup(page, "/wireguard?server=1")
let attempts = 0 let attempts = 0