fix(gre): экспортировать поддерживаемые параметры и очищать PSK

This commit is contained in:
Denozordec committed 2026-09-30 16:06:32 +07:00
1 parent a21252fb02
commit 337d14abdf
7 files changed
+113 -132

No files matched your search

+13 -130
View File
@@ -9,10 +9,11 @@ import { DataPageCard } from "@/components/data-page-card"
import { DataPageToolbar, DataPageToolbarFrame } from "@/components/data-page-toolbar"
import { GreTunnelsDataGrid } from "@/components/data-grids/gre-tunnels-data-grid"
import { GrePoolsDataGrid } from "@/components/data-grids/gre-pools-data-grid"
import { FormField, FormToggle, SectionTitle, SegmentedControl } from "@/components/form-kit"
import { FormField, FormToggle, SectionTitle } from "@/components/form-kit"
import { greTunnels as mockGreTunnels, grePools as mockGrePools, servers as mockServers } from "@/lib/data"
import type { GrePool, GreTunnel, GreStatus, IpsecEncAlg, IpsecAuthAlg, IpsecDhGroup, IkeVersion, Server } from "@/lib/data"
import type { GrePool, GreTunnel, GreStatus, Server } from "@/lib/data"
import { useDataSource } from "@/lib/data-source"
import { generateGreRsc } from "@/lib/gre-export"
import { greResponseSchema } from "@/lib/gre-response"
import { vxlanServersSchema } from "@/lib/vxlan-response"
import type { z } from "zod"
@@ -53,17 +54,6 @@ import { ALL_SERVERS_ID, type ServerTileItem } from "@/components/server-tile-ra
// ─── label maps ─────────────────────────────────────────────────────────────
const ENC_LABELS: Record<IpsecEncAlg, string> = { "aes-128": "AES-128", "aes-192": "AES-192", "aes-256": "AES-256" }
const AUTH_LABELS: Record<IpsecAuthAlg, string> = { sha1: "SHA-1", sha256: "SHA-256", sha512: "SHA-512" }
const DH_LABELS: Record<IpsecDhGroup, string> = {
modp1024: "DH-2 (1024)", modp2048: "DH-14 (2048)", modp4096: "DH-16 (4096)",
ecp256: "ECP-256", ecp384: "ECP-384", ecp521: "ECP-521",
}
const IKE_LABELS: Record<IkeVersion, string> = { ikev1: "IKEv1", ikev2: "IKEv2" }
const ENC_ROS: Record<IpsecEncAlg, string> = { "aes-128": "aes-128-cbc", "aes-192": "aes-192-cbc", "aes-256": "aes-256-cbc" }
const AUTH_ROS: Record<IpsecAuthAlg, string> = { sha1: "sha1", sha256: "sha256", sha512: "sha512" }
const STATUS_MAP: Record<GreStatus, { label: string; dot: string }> = {
up: { label: "Up", dot: "bg-emerald-500" },
degraded: { label: "Degraded", dot: "bg-amber-500" },
@@ -74,76 +64,6 @@ function greStatusMeta(status: GreStatus | undefined) {
return STATUS_MAP[status ?? "degraded"] ?? STATUS_MAP.degraded
}
// ─── RouterOS code generator ─────────────────────────────────────────────────
function generateRosCommands(t: GreTunnel, serverById: Record<string, Server>): string {
const lines: string[] = []
const srv = serverById[t.serverId]
lines.push(`# Сервер: ${srv?.name ?? t.serverId} (${srv?.host ?? ""})`)
lines.push(`# Сгенерировано MikrotikManager · ${new Date().toLocaleDateString("ru")}`)
lines.push("")
// GRE interface
lines.push("# ── GRE-интерфейс ────────────────────────────────────────────")
const greParts = [
`/interface gre add`,
` name=${t.name}`,
` remote-address=${t.remoteAddress}`,
t.localAddress !== "0.0.0.0" ? ` local-address=${t.localAddress}` : null,
t.ipsec ? ` ipsec-secret="${t.ipsec.secret}"` : null,
` mtu=${t.mtu}`,
t.keepaliveInterval > 0 ? ` keepalive=${t.keepaliveInterval}s,${t.keepaliveRetries}` : ` keepalive=0`,
` dscp=${t.dscp}`,
` clamp-tcp-mss=${t.clampTcpMss ? "yes" : "no"}`,
` allow-fast-path=${t.allowFastPath ? "yes" : "no"}`,
t.comment ? ` comment="${t.comment}"` : null,
!t.enabled ? ` disabled=yes` : null,
].filter(Boolean) as string[]
lines.push(greParts.join(" \\\n"))
// Inner IP
lines.push("")
lines.push("# ── Внутренний IP ────────────────────────────────────────────")
lines.push(`/ip address add \\`)
lines.push(` address=${t.localInnerIp} \\`)
lines.push(` interface=${t.name}`)
// IPsec: live CHR имеет только ipsec-secret; proposal — у моков/формы
if (t.ipsec?.encAlg && t.ipsec.authAlg) {
const ikeMode = t.ipsec.ikeVersion === "ikev1" ? "ike1" : "ike2"
const pfsGroup = t.ipsec.pfs ? (t.ipsec.dhGroup ?? "none") : "none"
lines.push("")
lines.push("# ── IPsec (авто через ipsec-secret; ручной эквивалент) ───────")
lines.push("")
lines.push(`/ip ipsec peer add \\`)
lines.push(` name=${t.name} \\`)
lines.push(` address=${t.remoteAddress} \\`)
lines.push(` exchange-mode=${ikeMode} \\`)
lines.push(` auth-method=pre-shared-key \\`)
lines.push(` secret="${t.ipsec.secret}"`)
lines.push("")
lines.push(`/ip ipsec proposal add \\`)
lines.push(` name=${t.name} \\`)
lines.push(` enc-algorithms=${ENC_ROS[t.ipsec.encAlg]} \\`)
lines.push(` auth-algorithms=${AUTH_ROS[t.ipsec.authAlg]} \\`)
lines.push(` pfs-group=${pfsGroup} \\`)
lines.push(` lifetime=${t.ipsec.lifetime ?? "1d"}`)
lines.push("")
lines.push(`/ip ipsec policy add \\`)
lines.push(` src-address=${t.localAddress !== "0.0.0.0" ? t.localAddress + "/32" : "0.0.0.0/0"} \\`)
lines.push(` dst-address=${t.remoteAddress}/32 \\`)
lines.push(` proposal=${t.name} \\`)
lines.push(` tunnel=yes`)
} else if (t.ipsec) {
lines.push("")
lines.push("# IPsec: peer/policy создаёт RouterOS по ipsec-secret")
}
return lines.join("\n")
}
// ─── small ui helpers ────────────────────────────────────────────────────────
// ─── Live API (как на /filters) ─────────────────────────────────────────────
@@ -198,11 +118,6 @@ const defaultTunnelForm = {
name: "", serverId: "", localAddress: "", remoteAddress: "",
poolId: "", localInnerIp: "", remoteInnerIp: "", comment: "", enabled: true,
ipsecEnabled: false, ipsecSecret: "", ipsecShowSecret: false,
ipsecIkeVersion: "ikev2" as IkeVersion,
ipsecEncAlg: "aes-256" as IpsecEncAlg,
ipsecAuthAlg: "sha256" as IpsecAuthAlg,
ipsecDhGroup: "modp2048" as IpsecDhGroup,
ipsecLifetime: "1d 00:00:00", ipsecPfs: true,
mtu: 1476, keepaliveInterval: 10, keepaliveRetries: 10,
dscp: "inherit", clampTcpMss: true, allowFastPath: true,
showAdvanced: false,
@@ -435,7 +350,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
dscp: form.dscp,
clampTcpMss: form.clampTcpMss,
allowFastPath: form.allowFastPath,
ipsecSecret: form.ipsecEnabled ? form.ipsecSecret : undefined,
ipsecSecret: form.ipsecEnabled ? form.ipsecSecret : "",
}
}
@@ -635,7 +550,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
const formatReceipt = (at: string) => new Date(at).toLocaleString("ru-RU")
const greExportCode = useMemo(
() => (codePreviewTunnel ? generateRosCommands(codePreviewTunnel, exportContext?.servers ?? {}) : ""),
() => (codePreviewTunnel ? generateGreRsc(codePreviewTunnel, exportContext?.servers ?? {}) : ""),
[codePreviewTunnel, exportContext],
)
@@ -917,7 +832,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
<span className="text-muted-foreground">·</span>
<span className="flex items-center gap-1 text-success">
<LockIcon className="size-3" />
IPsec {codePreviewTunnel.ipsec.ikeVersion ? IKE_LABELS[codePreviewTunnel.ipsec.ikeVersion] : "PSK"}
IPsec · PSK
</span>
</>
) : null}
@@ -953,7 +868,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
</FormField>
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Включён</span>
<FormToggle checked={tForm.enabled} onChange={(v) => setT("enabled", v)} />
<FormToggle ariaLabel="Туннель включён" checked={tForm.enabled} onChange={(v) => setT("enabled", v)} />
</div>
</div>
@@ -993,7 +908,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
<p className="text-sm font-medium">Включить IPsec</p>
<p className="text-xs text-muted-foreground">RouterOS автоматически создаст peer, policy и proposal</p>
</div>
<FormToggle checked={tForm.ipsecEnabled} onChange={(v) => setT("ipsecEnabled", v)} />
<FormToggle ariaLabel="Включить IPsec" checked={tForm.ipsecEnabled} onChange={(v) => setT("ipsecEnabled", v)} />
</div>
{tForm.ipsecEnabled && (
@@ -1001,46 +916,14 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
<FormField label="Пароль (PSK)" required hint="ipsec-secret — pre-shared key для автоматического IKE">
<div className="relative">
<Input type={tForm.ipsecShowSecret ? "text" : "password"} className="font-mono pr-9"
placeholder="Минимум 8 символов" value={tForm.ipsecSecret} onChange={(e) => setT("ipsecSecret", e.target.value)} />
<button type="button" onClick={() => setT("ipsecShowSecret", !tForm.ipsecShowSecret)}
aria-label="Пароль (PSK)" placeholder="Минимум 8 символов" value={tForm.ipsecSecret} onChange={(e) => setT("ipsecSecret", e.target.value)} />
<button type="button" aria-label={tForm.ipsecShowSecret ? "Скрыть PSK" : "Показать PSK"} onClick={() => setT("ipsecShowSecret", !tForm.ipsecShowSecret)}
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground">
{tForm.ipsecShowSecret ? <EyeOffIcon className="size-3.5" /> : <EyeIcon className="size-3.5" />}
</button>
</div>
</FormField>
<FormField label="IKE-версия">
<SegmentedControl value={tForm.ipsecIkeVersion} onChange={(v) => setT("ipsecIkeVersion", v)}
options={[{ value: "ikev1", label: "IKEv1" }, { value: "ikev2", label: "IKEv2 (рек.)" }]} />
</FormField>
<div className="grid grid-cols-2 gap-3">
<FormField label="Шифрование">
<select value={tForm.ipsecEncAlg} onChange={(e) => setT("ipsecEncAlg", e.target.value as IpsecEncAlg)}
className="h-8 w-full rounded-lg border border-input bg-background px-2.5 text-sm text-foreground outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50">
{(Object.entries(ENC_LABELS) as [IpsecEncAlg, string][]).map(([v, l]) => <option key={v} value={v}>{l}</option>)}
</select>
</FormField>
<FormField label="Хеш-алгоритм">
<select value={tForm.ipsecAuthAlg} onChange={(e) => setT("ipsecAuthAlg", e.target.value as IpsecAuthAlg)}
className="h-8 w-full rounded-lg border border-input bg-background px-2.5 text-sm text-foreground outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50">
{(Object.entries(AUTH_LABELS) as [IpsecAuthAlg, string][]).map(([v, l]) => <option key={v} value={v}>{l}</option>)}
</select>
</FormField>
</div>
<FormField label="DH-группа" hint="Группа Диффи-Хеллмана для обмена ключами">
<select value={tForm.ipsecDhGroup} onChange={(e) => setT("ipsecDhGroup", e.target.value as IpsecDhGroup)}
className="h-8 w-full rounded-lg border border-input bg-background px-2.5 text-sm text-foreground outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50">
{(Object.entries(DH_LABELS) as [IpsecDhGroup, string][]).map(([v, l]) => <option key={v} value={v}>{l}</option>)}
</select>
</FormField>
<div className="grid grid-cols-2 gap-3">
<FormField label="Срок жизни SA" hint="Формат: 1d 00:00:00">
<Input className="font-mono" value={tForm.ipsecLifetime} onChange={(e) => setT("ipsecLifetime", e.target.value)} />
</FormField>
<div className="flex items-center justify-between pt-6">
<span className="text-sm font-medium">PFS</span>
<FormToggle checked={tForm.ipsecPfs} onChange={(v) => setT("ipsecPfs", v)} />
</div>
</div>
<p className="text-xs text-muted-foreground">Применяется только PSK через ipsec-secret. Алгоритмы IKE, proposal и PFS задаются настройками RouterOS; эта форма их не изменяет.</p>
</div>
)}
</div>
@@ -1074,14 +957,14 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
</FormField>
{[
{ key: "clampTcpMss" as const, label: "Clamp TCP MSS", desc: "Ограничить MSS до MTU туннеля" },
{ key: "allowFastPath" as const, label: "Allow Fast Path", desc: "Аппаратное ускорение трафика" },
{ key: "allowFastPath" as const, label: "Allow Fast Path", desc: "Ускоренный путь обработки пакетов; отключите для IPsec" },
].map(({ key, label, desc }) => (
<div key={key} className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">{label}</p>
<p className="text-xs text-muted-foreground">{desc}</p>
</div>
<FormToggle checked={tForm[key] as boolean} onChange={(v) => setT(key, v)} />
<FormToggle ariaLabel={label} checked={tForm[key] as boolean} onChange={(v) => setT(key, v)} />
</div>
))}
</div>
+3 -1
View File
@@ -8,11 +8,13 @@ interface FormToggleProps {
onChange: (value: boolean) => void
disabled?: boolean
className?: string
ariaLabel?: string
}
function FormToggle({ checked, onChange, disabled, className }: FormToggleProps) {
function FormToggle({ checked, onChange, disabled, className, ariaLabel }: FormToggleProps) {
return (
<Switch
aria-label={ariaLabel}
checked={checked}
onCheckedChange={onChange}
disabled={disabled}
+8 -1
View File
@@ -25,12 +25,19 @@
| 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже |
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
| 9 | VPN, Firewall и Filters | В работе: блоки 9.1–9.60 — IPsec/GRE/VXLAN, Firewall и операции Filters |
| 9 | VPN, Firewall и Filters | В работе: блоки 9.1–9.61 — IPsec/GRE/VXLAN, Firewall и операции Filters |
| 10 | Маршрутизация, диагностика, карта и терминал | Не начат |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
## Блок 9.61 — поддерживаемые параметры GRE/PSK и .rsc (2026-09-30)
- GRE .rsc вынесен в generateGreRsc: quoted RouterOS strings/PSK, нормализованные комментарии, локальный IP только при наличии, без придуманных удалённых IP/маршрутов и дополнительных manual IPsec peer/policy/proposal поверх автоматического ipsec-secret. Параметры сверены с https://help.mikrotik.com/docs/spaces/ROS/pages/24805531/GRE и правилами Scripting. FastPath в снимке не меняется автоматически; несовместимый IPsec/FastPath подписан в комментарии.
- Из формы удалены IKE/алгоритмы/DH/PFS/lifetime, которые API не принимает; доступный PSK и пределы формы объяснены. Отключение IPsec отправляет пустой ipsecSecret, иначе PATCH сохранял старый PSK. Переключатели и показ PSK получили доступные имена; FormToggle поддерживает необязательный ariaLabel. Подсказка FastPath больше не обещает аппаратное ускорение.
- Unit экспорта проверил строки/инъекцию новой команды/optional IP/no duplicate IPsec; шесть E2E PSK/create/edit/export прошли. TypeScript/lint чистые. RouterOS применение и реальное снятие PSK остаются внешней приёмкой.
- Далее — достоверное отображение live-пулов и возможности API, затем layout и профильная регрессия GRE/VXLAN. Локальный коммит без push.
## Блок 9.60 — свежесть и снимок экспорта GRE (2026-09-30)
- Успешный ответ каталога и опрос каждого включённого устройства имеют отдельные отметки получения браузером. При failures отметка устройства сохраняется, при глобальной ошибке не обновляется. Подтверждение API записи/восстановления показано отдельно от опроса. Эти отметки не объявляются временем измерения RouterOS.
+8
View File
@@ -345,3 +345,11 @@ Live-копирование читает актуальный целевой rul
- 11 E2E прошли: часы/частичный и полный отказ/восстановление/экспорт плюс readiness и данные. TypeScript и lint экрана чистые.
- Далее — корректность самого GRE .rsc и параметров формы, реальные возможности live-пулов и desktop/mobile. Локальный коммит без push.
## Блок 9.61 — поддерживаемые параметры GRE/PSK и .rsc (2026-09-30)
- GRE .rsc вынесен в generateGreRsc: quoted RouterOS strings/PSK, нормализованные комментарии, локальный IP только при наличии, без придуманных удалённых IP/маршрутов и дополнительных manual IPsec peer/policy/proposal поверх автоматического ipsec-secret. Параметры сверены с https://help.mikrotik.com/docs/spaces/ROS/pages/24805531/GRE и правилами Scripting. FastPath в снимке не меняется автоматически; несовместимый IPsec/FastPath подписан в комментарии.
- Из формы удалены IKE/алгоритмы/DH/PFS/lifetime, которые API не принимает; доступный PSK и пределы формы объяснены. Отключение IPsec отправляет пустой ipsecSecret, иначе PATCH сохранял старый PSK. Переключатели и показ PSK получили доступные имена; FormToggle поддерживает необязательный ariaLabel. Подсказка FastPath больше не обещает аппаратное ускорение.
- Unit экспорта проверил строки/инъекцию новой команды/optional IP/no duplicate IPsec; шесть E2E PSK/create/edit/export прошли. TypeScript/lint чистые. RouterOS применение и реальное снятие PSK остаются внешней приёмкой.
- Далее — достоверное отображение live-пулов и возможности API, затем layout и профильная регрессия GRE/VXLAN. Локальный коммит без push.
+21
View File
@@ -0,0 +1,21 @@
import assert from "node:assert/strict"
import type { GreTunnel } from "./data"
import { generateGreRsc } from "./gre-export"
import { quoteRosString } from "./vxlan-export"
const tunnel: GreTunnel = { id: "*1", serverId: "1", name: 'gre "edge" $name\n/system reboot', localAddress: "0.0.0.0", remoteAddress: "192.0.2.1", localInnerIp: "10.0.0.1/30", remoteInnerIp: "10.0.0.2/30", poolId: "live", ipsec: { secret: 'key"\\$value\n/system reboot', encAlg: "aes-256", authAlg: "sha256", ikeVersion: "ikev2" }, mtu: 1476, keepaliveInterval: 10, keepaliveRetries: 3, dscp: "inherit", clampTcpMss: true, allowFastPath: false, comment: 'quoted "comment"\n/system reboot', enabled: false, status: "down" }
const code = generateGreRsc(tunnel, {})
const commands = code.split("\n").filter(line => line.startsWith("/"))
assert.equal(commands.length, 2)
assert.ok(commands[0].includes("name=" + quoteRosString(tunnel.name)))
assert.ok(commands[0].includes("ipsec-secret=" + quoteRosString(tunnel.ipsec!.secret)))
assert.ok(commands[0].includes("comment=" + quoteRosString(tunnel.comment)))
assert.ok(commands[0].includes("keepalive=10s,3 dscp=inherit clamp-tcp-mss=yes allow-fast-path=no disabled=yes"))
assert.ok(commands[1].includes('address="10.0.0.1/30" interface=' + quoteRosString(tunnel.name)))
for (const unsupported of ["/ip ipsec peer add", "/ip/ipsec/peer/add", "proposal add", "policy add", "exchange-mode=", "local-address=", "\n/system reboot", "10.0.0.2/30"]) assert.ok(!code.includes(unsupported), unsupported)
const noInner = generateGreRsc({ ...tunnel, localInnerIp: "", ipsec: null, localAddress: "192.0.2.10", keepaliveInterval: 0, dscp: 12, enabled: true }, {})
assert.equal(noInner.split("\n").filter(line => line.startsWith("/")).length, 1)
assert.ok(noInner.includes('local-address="192.0.2.10"'))
assert.ok(noInner.includes("keepalive=0 dscp=12"))
assert.ok(!noInner.includes("ipsec-secret="))
console.log("gre-export: escaped strings, observed GRE/PSK only, optional IP and no duplicate IPsec passed")
+26
View File
@@ -0,0 +1,26 @@
import type { GreTunnel, Server } from "./data"
import { quoteRosString } from "./vxlan-export"
const commentLine = (value: string) => value.replace(/[\r\n\x00-\x1f\x7f]/g, " ")
/** GRE/PSK fragment: https://help.mikrotik.com/docs/spaces/ROS/pages/24805531/GRE */
export function generateGreRsc(t: GreTunnel, servers: Record<string, Server>): string {
const server = servers[t.serverId]
const lines = ["# GRE — " + commentLine(t.name)]
if (server) lines.push("# Сервер: " + commentLine(server.name) + " (" + commentLine(server.host) + ")")
lines.push("# Фрагмент интерфейса и локального IP; удалённая сторона и маршруты не включены", "")
const args = [
"name=" + quoteRosString(t.name), "remote-address=" + quoteRosString(t.remoteAddress),
"mtu=" + t.mtu, "keepalive=" + (t.keepaliveInterval > 0 ? t.keepaliveInterval + "s," + t.keepaliveRetries : "0"),
"dscp=" + t.dscp, "clamp-tcp-mss=" + (t.clampTcpMss ? "yes" : "no"),
"allow-fast-path=" + (t.allowFastPath ? "yes" : "no"), "disabled=" + (t.enabled ? "no" : "yes"),
"comment=" + quoteRosString(t.comment),
]
if (t.localAddress && t.localAddress !== "0.0.0.0") args.push("local-address=" + quoteRosString(t.localAddress))
if (t.ipsec) args.push("ipsec-secret=" + quoteRosString(t.ipsec.secret))
lines.push("/interface/gre/add " + args.join(" "))
if (t.localInnerIp) lines.push("/ip/address/add address=" + quoteRosString(t.localInnerIp) + " interface=" + quoteRosString(t.name))
if (t.ipsec) lines.push("# Peer/policy создаёт RouterOS по ipsec-secret; ручные объекты IPsec не экспортируются")
if (t.ipsec && t.allowFastPath) lines.push("# В снимке включён FastPath: для GRE с IPsec документация RouterOS требует allow-fast-path=no")
return lines.join("\n") + "\n"
}
+34
View File
@@ -0,0 +1,34 @@
import { test, expect } from "@playwright/test"
for (const disable of [false, true]) {
test(`GRE: PSK ${disable ? "очищается" : "изменяется"} через поддерживаемый API`, async ({ page }) => {
let writes = 0
const tunnel = { id: "*1", serverId: "1", name: "gre-psk", localAddress: "192.0.2.1", remoteAddress: "192.0.2.2", localInnerIp: "10.0.0.1/30", remoteInnerIp: "10.0.0.2/30", poolId: "live", ipsec: { secret: "original-secret" }, mtu: 1476, keepaliveInterval: 10, keepaliveRetries: 3, dscp: "inherit", clampTcpMss: true, allowFastPath: false, comment: "", enabled: true, status: "up" }
await page.route("**/health", r => r.fulfill({ json: { ok: true } }))
await page.route("**/api/**", r => {
const path = new URL(r.request().url()).pathname
if (r.request().method() === "PATCH") {
writes++
expect(r.request().postDataJSON()).toMatchObject({ serverId: "1", rosId: "*1", name: "gre-psk", ipsecSecret: disable ? "" : "updated-secret" })
for (const key of ["ipsecIkeVersion", "ipsecEncAlg", "ipsecAuthAlg", "ipsecDhGroup", "ipsecLifetime", "ipsecPfs"]) expect(r.request().postDataJSON()).not.toHaveProperty(key)
return r.fulfill({ json: { ok: true } })
}
return r.fulfill({ json: path === "/api/gre/tunnels" ? { tunnels: [tunnel] } : path === "/api/servers" ? [{ id: 1, name: "Роутер", host: "192.0.2.1", enabled: true, country: "RU" }] : path === "/api/auth/config" ? { required: false } : {} })
})
await page.goto("/gre")
await page.getByRole("button", { name: "Действия: gre-psk" }).click()
await page.getByRole("menuitem", { name: "Редактировать", exact: true }).click()
const dialog = page.getByRole("dialog")
await expect(dialog.getByLabel("Пароль (PSK)")).toHaveValue("original-secret")
await expect(dialog).toContainText("эта форма их не изменяет")
for (const label of ["IKE-версия", "Шифрование", "Хеш-алгоритм", "DH-группа", "Срок жизни SA"]) await expect(dialog.getByText(label, { exact: true })).toHaveCount(0)
if (disable) await dialog.getByRole("switch", { name: "Включить IPsec", exact: true }).click()
else {
await dialog.getByRole("button", { name: "Показать PSK" }).click()
await expect(dialog.getByLabel("Пароль (PSK)")).toHaveAttribute("type", "text")
await dialog.getByLabel("Пароль (PSK)").fill("updated-secret")
}
await dialog.getByRole("button", { name: "Сохранить", exact: true }).click()
await expect(dialog).toHaveCount(0)
expect(writes).toBe(1)
})
}