Compare commits

...
1 Commits
Author SHA1 Message Date
DenozordecandCursor 5f774ce26e feat(gre): enhance GRE tunnel status handling and IPsec configuration
Docker images / prepare-release (push) Successful in 10s
Docker images / backend-test (push) Successful in 2m18s
Docker images / frontend-image (push) Successful in 3m1s
Docker images / updater-image (push) Successful in 40s
Docker images / backend-image (push) Successful in 2m31s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 10s
- 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 <cursoragent@cursor.com>
2026-09-11 15:39:32 +07:00
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" },
}
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 {
@@ -103,10 +107,10 @@ function generateRosCommands(t: GreTunnel, serverById: Record<string, Server>):
lines.push(` address=${t.localInnerIp} \\`)
lines.push(` interface=${t.name}`)
// IPsec manual equivalent
if (t.ipsec) {
const ikeMode = t.ipsec.ikeVersion === "ikev2" ? "ike2" : "ike1"
const pfsGroup = t.ipsec.pfs ? t.ipsec.dhGroup : "none"
// 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; ручной эквивалент) ───────")
@@ -123,13 +127,16 @@ function generateRosCommands(t: GreTunnel, serverById: Record<string, Server>):
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}`)
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")
@@ -138,7 +145,7 @@ function generateRosCommands(t: GreTunnel, serverById: Record<string, Server>):
// ─── small ui helpers ────────────────────────────────────────────────────────
function TunnelStatus({ status }: { status: GreStatus }) {
const s = STATUS_MAP[status]
const s = greStatusMeta(status)
return (
<span className="inline-flex items-center gap-1.5 text-sm">
<span className={`size-1.5 rounded-full ${s.dot}`} />
@@ -737,7 +744,7 @@ export default function GrePage() {
<div className="flex flex-wrap gap-2">
{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">
<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="text-muted-foreground">{t.localInnerIp} {t.remoteInnerIp}</span>
{t.ipsec && <LockIcon className="size-3 text-emerald-400" />}
@@ -798,8 +805,8 @@ export default function GrePage() {
codePreviewTunnel ? (
<div className="flex flex-wrap gap-3 text-xs shrink-0">
<span className="flex items-center gap-1.5">
<span className={`size-1.5 rounded-full ${STATUS_MAP[codePreviewTunnel.status].dot}`} />
{STATUS_MAP[codePreviewTunnel.status].label}
<span className={`size-1.5 rounded-full ${greStatusMeta(codePreviewTunnel.status).dot}`} />
{greStatusMeta(codePreviewTunnel.status).label}
</span>
<span className="text-muted-foreground">·</span>
<span>{serverById[codePreviewTunnel.serverId]?.name}</span>
@@ -814,7 +821,7 @@ export default function GrePage() {
<span className="text-muted-foreground">·</span>
<span className="flex items-center gap-1 text-success">
<LockIcon className="size-3" />
IPsec {IKE_LABELS[codePreviewTunnel.ipsec.ikeVersion]}
IPsec {codePreviewTunnel.ipsec.ikeVersion ? IKE_LABELS[codePreviewTunnel.ipsec.ikeVersion] : "PSK"}
</span>
</>
) : null}
@@ -75,7 +75,7 @@ const STATUS_MAP: Record<GreStatus, { label: string; dot: string }> = {
}
function TunnelStatus({ status }: { status: GreStatus }) {
const s = STATUS_MAP[status]
const s = STATUS_MAP[status] ?? STATUS_MAP.degraded
return (
<span className="inline-flex items-center gap-1.5 text-sm">
<span className={cn("size-1.5 rounded-full", s.dot)} />
@@ -208,14 +208,21 @@ function GreTunnelsDataGrid({
cell: ({ row }) => {
const t = row.original
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 (
<div className="flex flex-col gap-0.5">
<span className="text-xs font-mono">
{ENC_LABELS[t.ipsec.encAlg]} / {AUTH_LABELS[t.ipsec.authAlg]}
{[enc, auth].filter(Boolean).join(" / ") || "PSK"}
</span>
<span className="text-xs text-muted-foreground font-mono">
{DH_LABELS[t.ipsec.dhGroup].split(" ")[0]} · {IKE_LABELS[t.ipsec.ikeVersion]}
{t.ipsec.pfs && " · PFS"}
{[dh?.split(" ")[0], ike].filter(Boolean).join(" · ")}
{t.ipsec.pfs ? " · PFS" : ""}
</span>
</div>
)
+7 -6
View File
@@ -531,12 +531,13 @@ export type DscpMode = "inherit" | number
export interface GreIpsec {
secret: string // ipsec-secret → auto-creates peer+policy+proposal
encAlg: IpsecEncAlg // proposal enc-algorithms
authAlg: IpsecAuthAlg // proposal auth-algorithms
dhGroup: IpsecDhGroup // proposal pfs-group / peer dh-group
ikeVersion: IkeVersion // peer exchange-mode
lifetime: string // proposal lifetime (e.g. "1d 00:00:00")
pfs: boolean // perfect forward secrecy
/** Live CHR отдаёт только secret; proposal-поля есть у моков / формы */
encAlg?: IpsecEncAlg
authAlg?: IpsecAuthAlg
dhGroup?: IpsecDhGroup
ikeVersion?: IkeVersion
lifetime?: string
pfs?: boolean
}
export interface GreTunnel {