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 { 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>
|
||||
|
||||
Reference in new issue
Block a user