fix(wireguard): проверять конфигурацию до импорта
This commit is contained in:
1 parent
7ce43adc07
commit
479f2adc78
14 files changed
+247
-30
No files matched your search
@@ -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")
|
||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
})
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||||
@@ -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
@@ -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 }
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user