fix(gre): сохранять удалённый внутренний IP туннеля

This commit is contained in:
Denozordec committed 2026-10-08 15:50:11 +07:00
1 parent 56f29a60d1
commit bd3f996f55
12 files changed
+112 -14

No files matched your search

+4 -3
View File
@@ -24,6 +24,7 @@ import {
type ConfigRevisionSource,
} from "../services/config-revisions.js"
import { parseDbServerId } from "../utils/server-id.js"
import { readGreComment, writeGreComment } from "../services/gre-comment.js"
const TunnelWriteSchema = z.object({
serverId: z.string().min(1),
@@ -86,8 +87,8 @@ function tunnelFromBody(body: z.infer<typeof TunnelWriteSchema> & { keepaliveInt
localAddress: body.localAddress ?? "",
remoteAddress: body.remoteAddress,
localInnerIp: body.localInnerIp ?? "",
remoteInnerIp: body.remoteInnerIp ?? "",
comment: body.comment ?? "",
remoteInnerIp: body.remoteInnerIp ?? readGreComment(body.comment).remoteInnerIp,
comment: body.remoteInnerIp === undefined ? body.comment ?? "" : writeGreComment(body.comment ?? "", body.remoteInnerIp),
disabled: body.enabled === false,
mtu: body.mtu ?? 1476,
keepalive: keepalive ?? "0",
@@ -149,7 +150,7 @@ const greRoutes: FastifyPluginAsyncZod = async (app) => {
localAddress: body.localAddress ?? live.localAddress,
remoteAddress: body.remoteAddress || live.remoteAddress,
localInnerIp: body.localInnerIp ?? state.addrs.find((a) => a.interfaceName === live.name)?.address ?? "",
remoteInnerIp: body.remoteInnerIp,
remoteInnerIp: body.remoteInnerIp ?? state.tunnels.find(t => t.name === live.name)?.remoteInnerIp ?? "",
comment: body.comment ?? live.comment,
enabled: body.enabled ?? !live.disabled,
mtu: body.mtu ?? live.mtu,
+4 -1
View File
@@ -1,5 +1,6 @@
/** Канонические снапшоты и планы restore для firewall / WireGuard / GRE / IPsec. */
import { isIpsecManagedComment } from "./ipsec-config.js"
import { readGreComment, writeGreComment } from "./gre-comment.js"
export type FirewallFamily = "ip" | "ip6"
export type FirewallTable = "filter" | "nat" | "mangle" | "raw"
@@ -854,6 +855,8 @@ export function parseGreSnapshot(payload: unknown): GreSnapshot {
}
export function greInterfaceBody(t: GreSnapshotTunnel): Record<string, string> {
const stored = readGreComment(t.comment)
const peer = t.remoteInnerIp || (stored.comment === t.comment ? stored.remoteInnerIp : "")
return compactBody({
name: t.name,
"local-address": t.localAddress && t.localAddress !== "0.0.0.0" ? t.localAddress : undefined,
@@ -863,7 +866,7 @@ export function greInterfaceBody(t: GreSnapshotTunnel): Record<string, string> {
dscp: t.dscp,
"clamp-tcp-mss": t.clampTcpMss ? "yes" : "no",
"allow-fast-path": t.allowFastPath ? "yes" : "no",
comment: t.comment,
comment: writeGreComment(t.comment, peer),
disabled: rosYesNo(t.disabled),
"ipsec-secret": isHiddenSecret(t.ipsecSecret) ? undefined : t.ipsecSecret,
})
+26
View File
@@ -0,0 +1,26 @@
import assert from "node:assert/strict"
import {readGreComment,writeGreComment} from "./gre-comment.js"
import {canonicalGreSnapshot,planGreCreate,planGreRestore} from "./entity-snapshots.js"
import {mapGreLive} from "./gre-live.js"
const comment="Тестовый туннель\nВторая строка",peer="198.18.255.250/30"
const raw=writeGreComment(comment,peer)
assert.deepEqual(readGreComment(raw),{comment,remoteInnerIp:peer})
assert.equal(writeGreComment(raw,peer),raw)
assert.deepEqual(readGreComment("gateway=10.0.0.2/30"),{comment:"gateway=10.0.0.2/30",remoteInnerIp:"10.0.0.2/30"})
assert.equal(readGreComment("note\n[mmapp-gre-peer:v1:bad]").comment,"note\n[mmapp-gre-peer:v1:bad]")
const desired=canonicalGreSnapshot({tunnels:[{name:"gre-test",localInnerIp:"198.18.255.249/30",remoteInnerIp:peer,comment}]})
const create=planGreCreate(desired.tunnels[0]);assert.deepEqual(create.map(x=>x.path),["/interface/gre","/ip/address"])
const native={".id":"*3",name:"gre-test","local-address":"194.226.121.150","remote-address":"104.165.244.7",comment:(create[0] as {body:Record<string,string>}).body.comment,mtu:"1400",disabled:"false",running:"true"}
const server={id:1} as Parameters<typeof mapGreLive>[0]
const live=mapGreLive(server,[native],[{".id":"*4",address:"198.18.255.249/30",interface:"gre-test"}])
assert.equal(live.tunnels[0].remoteInnerIp,peer);assert.equal(live.tunnels[0].comment,comment);assert.equal(live.snapshot.tunnels[0].remoteInnerIp,peer)
const restored=planGreRestore(live.snapshot,{gre:live.gre,addrs:live.addrs});const iface=restored.find(x=>x.op==="patch"&&x.path==="/interface/gre/*3") as {body:Record<string,string>};assert(iface);assert.equal(readGreComment(iface.body.comment).remoteInnerIp,peer)
assert(restored.every(x=>!x.path.startsWith("/ip/route")))
const clearSnapshot=canonicalGreSnapshot({tunnels:[{...live.snapshot.tunnels[0],remoteInnerIp:""}]})
const clearedRestore=planGreRestore(clearSnapshot,{gre:live.gre,addrs:live.addrs}).find(x=>x.op==="patch"&&x.path==="/interface/gre/*3") as {body:Record<string,string>}
assert.equal(readGreComment(clearedRestore.body.comment).remoteInnerIp,"")
const legacy=canonicalGreSnapshot({tunnels:[{name:"legacy",comment:"gateway=10.0.0.2/30",remoteInnerIp:""}]});assert.equal(readGreComment((planGreCreate(legacy.tunnels[0])[0] as {body:Record<string,string>}).body.comment).remoteInnerIp,"10.0.0.2/30")
const cleared=writeGreComment("gateway=10.0.0.2/30","");assert.equal(readGreComment(cleared).remoteInnerIp,"")
const clearPlan=planGreCreate(canonicalGreSnapshot({tunnels:[{name:"clear",comment:cleared,remoteInnerIp:""}]}).tunnels[0]);assert.equal(readGreComment((clearPlan[0] as {body:Record<string,string>}).body.comment).remoteInnerIp,"")
assert.equal(readGreComment(writeGreComment(raw,"")).comment,comment)
console.log("GRE peer metadata: native/read/restore/legacy/clear, no route operations")
+25
View File
@@ -0,0 +1,25 @@
function legacyPeer(comment: string): string {
return comment.match(/(?:network|gateway)\s*=\s*([0-9.]+\/\d+)/)?.[1] ?? ""
}
/** Keep app-only peer information on RouterOS without changing routes. */
export function readGreComment(raw = ""): { comment: string; remoteInnerIp: string } {
const match = raw.match(/(?:\n)?\[mmapp-gre-peer:v1:([A-Za-z0-9_-]+)\]$/)
if (match) {
try {
const value: unknown = JSON.parse(Buffer.from(match[1], "base64url").toString("utf8"))
if (value && typeof value === "object" && "remoteInnerIp" in value && typeof value.remoteInnerIp === "string") {
return { comment: raw.slice(0, match.index), remoteInnerIp: value.remoteInnerIp }
}
} catch { /* Preserve unrecognized user comments verbatim. */ }
}
return { comment: raw, remoteInnerIp: legacyPeer(raw) }
}
export function writeGreComment(raw: string, remoteInnerIp: string): string {
const { comment } = readGreComment(raw)
const peer = remoteInnerIp.trim()
if (!peer && !legacyPeer(comment)) return comment
const payload = Buffer.from(JSON.stringify({ remoteInnerIp: peer }), "utf8").toString("base64url")
return `${comment}${comment ? "\n" : ""}[mmapp-gre-peer:v1:${payload}]`
}
+4 -3
View File
@@ -2,6 +2,7 @@ import { eq } from "drizzle-orm"
import { db } from "../db/index.js"
import { servers } from "../db/schema.js"
import { MikrotikClient } from "./mikrotik.js"
import { readGreComment } from "./gre-comment.js"
import {
canonicalGreSnapshot,
type GreLiveAddr,
@@ -83,7 +84,7 @@ function parseDscp(value: string | undefined): "inherit" | number {
function parseInnerFromComment(comment: string | undefined): { localInnerIp: string; remoteInnerIp: string } {
if (!comment) return { localInnerIp: "", remoteInnerIp: "" }
const local = comment.match(/address\s*=\s*([0-9.]+\/\d+)/)?.[1] ?? ""
const remote = comment.match(/(?:network|gateway)\s*=\s*([0-9.]+\/\d+)/)?.[1] ?? ""
const remote = readGreComment(comment).remoteInnerIp
return { localInnerIp: local, remoteInnerIp: remote }
}
@@ -164,7 +165,7 @@ export function mapGreLive(
dscp: parseDscp(g.dscp),
clampTcpMss: g["clamp-tcp-mss"] !== "false" && g["clamp-tcp-mss"] !== "no",
allowFastPath: g["allow-fast-path"] !== "false" && g["allow-fast-path"] !== "no",
comment: g.comment ?? "",
comment: readGreComment(g.comment).comment,
enabled: !disabled,
status: disabled ? "down" : running ? "up" : "degraded",
})
@@ -178,7 +179,7 @@ export function mapGreLive(
localAddress: g.localAddress,
remoteAddress: g.remoteAddress,
localInnerIp: addrs.find((a) => a.interfaceName === g.name)?.address ?? "",
remoteInnerIp: "",
remoteInnerIp: tunnels.find(t => t.name === g.name)?.remoteInnerIp ?? "",
comment: g.comment,
disabled: g.disabled,
mtu: g.mtu,