fix(gre): сохранять удалённый внутренний IP туннеля
This commit is contained in:
1 parent
56f29a60d1
commit
bd3f996f55
12 files changed
+112
-14
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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")
|
||||
@@ -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}]`
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user