feat(gre): enhance GRE tunnel status handling and IPsec configuration

- Introduced a new `greStatusMeta` function to streamline status retrieval and default handling.
- Updated GRE tunnel command generation to improve IPsec configuration handling, including optional fields for encryption and authentication algorithms.
- Enhanced the GRE tunnels data grid to display IPsec settings more clearly, including handling cases where certain parameters may be undefined.
- Refactored status display logic in the GRE page to utilize the new status meta function for consistency.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-09-11 15:39:32 +07:00
co-authored by Cursor
parent 25b82997b6
commit c463df81c1
3 changed files with 35 additions and 20 deletions
+17 -10
View File
@@ -68,6 +68,10 @@ const STATUS_MAP: Record<GreStatus, { label: string; dot: string }> = {
down: { label: "Down", dot: "bg-red-500" }, down: { label: "Down", dot: "bg-red-500" },
} }
function greStatusMeta(status: GreStatus | undefined) {
return STATUS_MAP[status ?? "degraded"] ?? STATUS_MAP.degraded
}
// ─── RouterOS code generator ───────────────────────────────────────────────── // ─── RouterOS code generator ─────────────────────────────────────────────────
function generateRosCommands(t: GreTunnel, serverById: Record<string, Server>): string { function generateRosCommands(t: GreTunnel, serverById: Record<string, Server>): string {
@@ -103,10 +107,10 @@ function generateRosCommands(t: GreTunnel, serverById: Record<string, Server>):
lines.push(` address=${t.localInnerIp} \\`) lines.push(` address=${t.localInnerIp} \\`)
lines.push(` interface=${t.name}`) lines.push(` interface=${t.name}`)
// IPsec manual equivalent // IPsec: live CHR имеет только ipsec-secret; proposal — у моков/формы
if (t.ipsec) { if (t.ipsec?.encAlg && t.ipsec.authAlg) {
const ikeMode = t.ipsec.ikeVersion === "ikev2" ? "ike2" : "ike1" const ikeMode = t.ipsec.ikeVersion === "ikev1" ? "ike1" : "ike2"
const pfsGroup = t.ipsec.pfs ? t.ipsec.dhGroup : "none" const pfsGroup = t.ipsec.pfs ? (t.ipsec.dhGroup ?? "none") : "none"
lines.push("") lines.push("")
lines.push("# ── IPsec (авто через ipsec-secret; ручной эквивалент) ───────") lines.push("# ── IPsec (авто через ipsec-secret; ручной эквивалент) ───────")
@@ -123,13 +127,16 @@ function generateRosCommands(t: GreTunnel, serverById: Record<string, Server>):
lines.push(` enc-algorithms=${ENC_ROS[t.ipsec.encAlg]} \\`) lines.push(` enc-algorithms=${ENC_ROS[t.ipsec.encAlg]} \\`)
lines.push(` auth-algorithms=${AUTH_ROS[t.ipsec.authAlg]} \\`) lines.push(` auth-algorithms=${AUTH_ROS[t.ipsec.authAlg]} \\`)
lines.push(` pfs-group=${pfsGroup} \\`) lines.push(` pfs-group=${pfsGroup} \\`)
lines.push(` lifetime=${t.ipsec.lifetime}`) lines.push(` lifetime=${t.ipsec.lifetime ?? "1d"}`)
lines.push("") lines.push("")
lines.push(`/ip ipsec policy add \\`) 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(` 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(` dst-address=${t.remoteAddress}/32 \\`)
lines.push(` proposal=${t.name} \\`) lines.push(` proposal=${t.name} \\`)
lines.push(` tunnel=yes`) lines.push(` tunnel=yes`)
} else if (t.ipsec) {
lines.push("")
lines.push("# IPsec: peer/policy создаёт RouterOS по ipsec-secret")
} }
return lines.join("\n") return lines.join("\n")
@@ -138,7 +145,7 @@ function generateRosCommands(t: GreTunnel, serverById: Record<string, Server>):
// ─── small ui helpers ──────────────────────────────────────────────────────── // ─── small ui helpers ────────────────────────────────────────────────────────
function TunnelStatus({ status }: { status: GreStatus }) { function TunnelStatus({ status }: { status: GreStatus }) {
const s = STATUS_MAP[status] const s = greStatusMeta(status)
return ( return (
<span className="inline-flex items-center gap-1.5 text-sm"> <span className="inline-flex items-center gap-1.5 text-sm">
<span className={`size-1.5 rounded-full ${s.dot}`} /> <span className={`size-1.5 rounded-full ${s.dot}`} />
@@ -737,7 +744,7 @@ export default function GrePage() {
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{poolTunnels.map((t, tunnelIndex) => ( {poolTunnels.map((t, tunnelIndex) => (
<div key={`${t.id}:${t.serverId}:${t.name}:${tunnelIndex}`} className="flex items-center gap-2 border border-border rounded-md px-3 py-1.5 bg-muted/30 text-xs"> <div key={`${t.id}:${t.serverId}:${t.name}:${tunnelIndex}`} className="flex items-center gap-2 border border-border rounded-md px-3 py-1.5 bg-muted/30 text-xs">
<span className={`size-1.5 rounded-full ${STATUS_MAP[t.status].dot}`} /> <span className={`size-1.5 rounded-full ${greStatusMeta(t.status).dot}`} />
<span className="font-mono font-medium">{t.name}</span> <span className="font-mono font-medium">{t.name}</span>
<span className="text-muted-foreground">{t.localInnerIp} {t.remoteInnerIp}</span> <span className="text-muted-foreground">{t.localInnerIp} {t.remoteInnerIp}</span>
{t.ipsec && <LockIcon className="size-3 text-emerald-400" />} {t.ipsec && <LockIcon className="size-3 text-emerald-400" />}
@@ -798,8 +805,8 @@ export default function GrePage() {
codePreviewTunnel ? ( codePreviewTunnel ? (
<div className="flex flex-wrap gap-3 text-xs shrink-0"> <div className="flex flex-wrap gap-3 text-xs shrink-0">
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<span className={`size-1.5 rounded-full ${STATUS_MAP[codePreviewTunnel.status].dot}`} /> <span className={`size-1.5 rounded-full ${greStatusMeta(codePreviewTunnel.status).dot}`} />
{STATUS_MAP[codePreviewTunnel.status].label} {greStatusMeta(codePreviewTunnel.status).label}
</span> </span>
<span className="text-muted-foreground">·</span> <span className="text-muted-foreground">·</span>
<span>{serverById[codePreviewTunnel.serverId]?.name}</span> <span>{serverById[codePreviewTunnel.serverId]?.name}</span>
@@ -814,7 +821,7 @@ export default function GrePage() {
<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 {IKE_LABELS[codePreviewTunnel.ipsec.ikeVersion]} IPsec {codePreviewTunnel.ipsec.ikeVersion ? IKE_LABELS[codePreviewTunnel.ipsec.ikeVersion] : "PSK"}
</span> </span>
</> </>
) : null} ) : null}
@@ -75,7 +75,7 @@ const STATUS_MAP: Record<GreStatus, { label: string; dot: string }> = {
} }
function TunnelStatus({ status }: { status: GreStatus }) { function TunnelStatus({ status }: { status: GreStatus }) {
const s = STATUS_MAP[status] const s = STATUS_MAP[status] ?? STATUS_MAP.degraded
return ( return (
<span className="inline-flex items-center gap-1.5 text-sm"> <span className="inline-flex items-center gap-1.5 text-sm">
<span className={cn("size-1.5 rounded-full", s.dot)} /> <span className={cn("size-1.5 rounded-full", s.dot)} />
@@ -208,14 +208,21 @@ function GreTunnelsDataGrid({
cell: ({ row }) => { cell: ({ row }) => {
const t = row.original const t = row.original
if (!t.ipsec) return <span className="text-xs text-muted-foreground"></span> if (!t.ipsec) return <span className="text-xs text-muted-foreground"></span>
const enc = t.ipsec.encAlg ? ENC_LABELS[t.ipsec.encAlg] : undefined
const auth = t.ipsec.authAlg ? AUTH_LABELS[t.ipsec.authAlg] : undefined
const dh = t.ipsec.dhGroup ? DH_LABELS[t.ipsec.dhGroup] : undefined
const ike = t.ipsec.ikeVersion ? IKE_LABELS[t.ipsec.ikeVersion] : undefined
if (!enc && !auth && !dh && !ike) {
return <span className="text-xs text-muted-foreground">PSK · auto</span>
}
return ( return (
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<span className="text-xs font-mono"> <span className="text-xs font-mono">
{ENC_LABELS[t.ipsec.encAlg]} / {AUTH_LABELS[t.ipsec.authAlg]} {[enc, auth].filter(Boolean).join(" / ") || "PSK"}
</span> </span>
<span className="text-xs text-muted-foreground font-mono"> <span className="text-xs text-muted-foreground font-mono">
{DH_LABELS[t.ipsec.dhGroup].split(" ")[0]} · {IKE_LABELS[t.ipsec.ikeVersion]} {[dh?.split(" ")[0], ike].filter(Boolean).join(" · ")}
{t.ipsec.pfs && " · PFS"} {t.ipsec.pfs ? " · PFS" : ""}
</span> </span>
</div> </div>
) )
+7 -6
View File
@@ -531,12 +531,13 @@ export type DscpMode = "inherit" | number
export interface GreIpsec { export interface GreIpsec {
secret: string // ipsec-secret → auto-creates peer+policy+proposal secret: string // ipsec-secret → auto-creates peer+policy+proposal
encAlg: IpsecEncAlg // proposal enc-algorithms /** Live CHR отдаёт только secret; proposal-поля есть у моков / формы */
authAlg: IpsecAuthAlg // proposal auth-algorithms encAlg?: IpsecEncAlg
dhGroup: IpsecDhGroup // proposal pfs-group / peer dh-group authAlg?: IpsecAuthAlg
ikeVersion: IkeVersion // peer exchange-mode dhGroup?: IpsecDhGroup
lifetime: string // proposal lifetime (e.g. "1d 00:00:00") ikeVersion?: IkeVersion
pfs: boolean // perfect forward secrecy lifetime?: string
pfs?: boolean
} }
export interface GreTunnel { export interface GreTunnel {