fix(gre): экспортировать поддерживаемые параметры и очищать PSK
This commit is contained in:
1 parent
a21252fb02
commit
337d14abdf
7 files changed
+113
-132
No files matched your search
+13
-130
@@ -9,10 +9,11 @@ import { DataPageCard } from "@/components/data-page-card"
|
|||||||
import { DataPageToolbar, DataPageToolbarFrame } from "@/components/data-page-toolbar"
|
import { DataPageToolbar, DataPageToolbarFrame } from "@/components/data-page-toolbar"
|
||||||
import { GreTunnelsDataGrid } from "@/components/data-grids/gre-tunnels-data-grid"
|
import { GreTunnelsDataGrid } from "@/components/data-grids/gre-tunnels-data-grid"
|
||||||
import { GrePoolsDataGrid } from "@/components/data-grids/gre-pools-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 { 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 { useDataSource } from "@/lib/data-source"
|
||||||
|
import { generateGreRsc } from "@/lib/gre-export"
|
||||||
import { greResponseSchema } from "@/lib/gre-response"
|
import { greResponseSchema } from "@/lib/gre-response"
|
||||||
import { vxlanServersSchema } from "@/lib/vxlan-response"
|
import { vxlanServersSchema } from "@/lib/vxlan-response"
|
||||||
import type { z } from "zod"
|
import type { z } from "zod"
|
||||||
@@ -53,17 +54,6 @@ import { ALL_SERVERS_ID, type ServerTileItem } from "@/components/server-tile-ra
|
|||||||
|
|
||||||
// ─── label maps ─────────────────────────────────────────────────────────────
|
// ─── 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 }> = {
|
const STATUS_MAP: Record<GreStatus, { label: string; dot: string }> = {
|
||||||
up: { label: "Up", dot: "bg-emerald-500" },
|
up: { label: "Up", dot: "bg-emerald-500" },
|
||||||
degraded: { label: "Degraded", dot: "bg-amber-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
|
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 ────────────────────────────────────────────────────────
|
// ─── small ui helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// ─── Live API (как на /filters) ─────────────────────────────────────────────
|
// ─── Live API (как на /filters) ─────────────────────────────────────────────
|
||||||
@@ -198,11 +118,6 @@ const defaultTunnelForm = {
|
|||||||
name: "", serverId: "", localAddress: "", remoteAddress: "",
|
name: "", serverId: "", localAddress: "", remoteAddress: "",
|
||||||
poolId: "", localInnerIp: "", remoteInnerIp: "", comment: "", enabled: true,
|
poolId: "", localInnerIp: "", remoteInnerIp: "", comment: "", enabled: true,
|
||||||
ipsecEnabled: false, ipsecSecret: "", ipsecShowSecret: false,
|
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,
|
mtu: 1476, keepaliveInterval: 10, keepaliveRetries: 10,
|
||||||
dscp: "inherit", clampTcpMss: true, allowFastPath: true,
|
dscp: "inherit", clampTcpMss: true, allowFastPath: true,
|
||||||
showAdvanced: false,
|
showAdvanced: false,
|
||||||
@@ -435,7 +350,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
|||||||
dscp: form.dscp,
|
dscp: form.dscp,
|
||||||
clampTcpMss: form.clampTcpMss,
|
clampTcpMss: form.clampTcpMss,
|
||||||
allowFastPath: form.allowFastPath,
|
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 formatReceipt = (at: string) => new Date(at).toLocaleString("ru-RU")
|
||||||
|
|
||||||
const greExportCode = useMemo(
|
const greExportCode = useMemo(
|
||||||
() => (codePreviewTunnel ? generateRosCommands(codePreviewTunnel, exportContext?.servers ?? {}) : ""),
|
() => (codePreviewTunnel ? generateGreRsc(codePreviewTunnel, exportContext?.servers ?? {}) : ""),
|
||||||
[codePreviewTunnel, exportContext],
|
[codePreviewTunnel, exportContext],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -917,7 +832,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
|||||||
<span className="text-muted-foreground">·</span>
|
<span className="text-muted-foreground">·</span>
|
||||||
<span className="flex items-center gap-1 text-success">
|
<span className="flex items-center gap-1 text-success">
|
||||||
<LockIcon className="size-3" />
|
<LockIcon className="size-3" />
|
||||||
IPsec {codePreviewTunnel.ipsec.ikeVersion ? IKE_LABELS[codePreviewTunnel.ipsec.ikeVersion] : "PSK"}
|
IPsec · PSK
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -953,7 +868,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
|||||||
</FormField>
|
</FormField>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm font-medium">Включён</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -993,7 +908,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
|||||||
<p className="text-sm font-medium">Включить IPsec</p>
|
<p className="text-sm font-medium">Включить IPsec</p>
|
||||||
<p className="text-xs text-muted-foreground">RouterOS автоматически создаст peer, policy и proposal</p>
|
<p className="text-xs text-muted-foreground">RouterOS автоматически создаст peer, policy и proposal</p>
|
||||||
</div>
|
</div>
|
||||||
<FormToggle checked={tForm.ipsecEnabled} onChange={(v) => setT("ipsecEnabled", v)} />
|
<FormToggle ariaLabel="Включить IPsec" checked={tForm.ipsecEnabled} onChange={(v) => setT("ipsecEnabled", v)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tForm.ipsecEnabled && (
|
{tForm.ipsecEnabled && (
|
||||||
@@ -1001,46 +916,14 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
|||||||
<FormField label="Пароль (PSK)" required hint="ipsec-secret — pre-shared key для автоматического IKE">
|
<FormField label="Пароль (PSK)" required hint="ipsec-secret — pre-shared key для автоматического IKE">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input type={tForm.ipsecShowSecret ? "text" : "password"} className="font-mono pr-9"
|
<Input type={tForm.ipsecShowSecret ? "text" : "password"} className="font-mono pr-9"
|
||||||
placeholder="Минимум 8 символов" value={tForm.ipsecSecret} onChange={(e) => setT("ipsecSecret", e.target.value)} />
|
aria-label="Пароль (PSK)" placeholder="Минимум 8 символов" value={tForm.ipsecSecret} onChange={(e) => setT("ipsecSecret", e.target.value)} />
|
||||||
<button type="button" onClick={() => setT("ipsecShowSecret", !tForm.ipsecShowSecret)}
|
<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">
|
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" />}
|
{tForm.ipsecShowSecret ? <EyeOffIcon className="size-3.5" /> : <EyeIcon className="size-3.5" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="IKE-версия">
|
<p className="text-xs text-muted-foreground">Применяется только PSK через ipsec-secret. Алгоритмы IKE, proposal и PFS задаются настройками RouterOS; эта форма их не изменяет.</p>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1074,14 +957,14 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
|||||||
</FormField>
|
</FormField>
|
||||||
{[
|
{[
|
||||||
{ key: "clampTcpMss" as const, label: "Clamp TCP MSS", desc: "Ограничить MSS до MTU туннеля" },
|
{ 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 }) => (
|
].map(({ key, label, desc }) => (
|
||||||
<div key={key} className="flex items-center justify-between">
|
<div key={key} className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">{label}</p>
|
<p className="text-sm font-medium">{label}</p>
|
||||||
<p className="text-xs text-muted-foreground">{desc}</p>
|
<p className="text-xs text-muted-foreground">{desc}</p>
|
||||||
</div>
|
</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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ interface FormToggleProps {
|
|||||||
onChange: (value: boolean) => void
|
onChange: (value: boolean) => void
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
|
ariaLabel?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormToggle({ checked, onChange, disabled, className }: FormToggleProps) {
|
function FormToggle({ checked, onChange, disabled, className, ariaLabel }: FormToggleProps) {
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
|
aria-label={ariaLabel}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onCheckedChange={onChange}
|
onCheckedChange={onChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -25,12 +25,19 @@
|
|||||||
| 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже |
|
| 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже |
|
||||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
||||||
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
| 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 | Маршрутизация, диагностика, карта и терминал | Не начат |
|
| 10 | Маршрутизация, диагностика, карта и терминал | Не начат |
|
||||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
||||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
| 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)
|
## Блок 9.60 — свежесть и снимок экспорта GRE (2026-09-30)
|
||||||
|
|
||||||
- Успешный ответ каталога и опрос каждого включённого устройства имеют отдельные отметки получения браузером. При failures отметка устройства сохраняется, при глобальной ошибке не обновляется. Подтверждение API записи/восстановления показано отдельно от опроса. Эти отметки не объявляются временем измерения RouterOS.
|
- Успешный ответ каталога и опрос каждого включённого устройства имеют отдельные отметки получения браузером. При failures отметка устройства сохраняется, при глобальной ошибке не обновляется. Подтверждение API записи/восстановления показано отдельно от опроса. Эти отметки не объявляются временем измерения RouterOS.
|
||||||
|
|||||||
@@ -345,3 +345,11 @@ Live-копирование читает актуальный целевой rul
|
|||||||
- 11 E2E прошли: часы/частичный и полный отказ/восстановление/экспорт плюс readiness и данные. TypeScript и lint экрана чистые.
|
- 11 E2E прошли: часы/частичный и полный отказ/восстановление/экспорт плюс readiness и данные. TypeScript и lint экрана чистые.
|
||||||
- Далее — корректность самого GRE .rsc и параметров формы, реальные возможности live-пулов и desktop/mobile. Локальный коммит без push.
|
- Далее — корректность самого 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.
|
||||||
|
|
||||||
@@ -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")
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in new issue
Block a user