fix(traffic): спрашивать endpoint MM в sheet подключения JH
Docker images / prepare-release (push) Successful in 6s
Docker images / backend-image (push) Successful in 1m31s
Docker images / frontend-image (push) Failing after 2m38s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 46s
Docker images / publish-release (push) Skipped

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-09-06 22:16:41 +07:00
co-authored by Cursor
parent 1e9312acbd
commit 95dcd3df58
7 changed files with 194 additions and 56 deletions
+12 -15
View File
@@ -16,12 +16,7 @@ import {
startTrafficFlowListener,
} from "../services/traffic-flow-ingest.js"
import { applyFlowOverlay } from "../services/traffic-flow-overlay.js"
import {
buildHostComposeSnippet,
buildHostNftSnippet,
buildHostUfwSnippet,
buildHostWgQuickConf,
} from "../services/traffic-flow-host-files.js"
import { listTrafficFlowHostFiles } from "../services/traffic-flow-host-files.js"
function rangeToMinutes(range: string | undefined): number {
switch ((range ?? "5m").toLowerCase()) {
@@ -39,13 +34,22 @@ async function sendFlowTalkers(req: FastifyRequest, reply: FastifyReply) {
return reply.send(listFlowTalkers(rangeToMinutes(q.range)))
}
function requestPublicHost(req: FastifyRequest): string {
const forwarded = req.headers["x-forwarded-host"]
const raw = Array.isArray(forwarded) ? forwarded[0] : forwarded
return raw || req.hostname || ""
}
async function applyOverlayHandler(req: FastifyRequest, reply: FastifyReply) {
const parsed = trafficFlowOverlayRequestSchema.safeParse(req.body ?? {})
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
try {
const result = await applyFlowOverlay(parsed.data.serverId)
const result = await applyFlowOverlay(parsed.data.serverId, {
publicEndpoint: parsed.data.publicEndpoint,
requestHost: requestPublicHost(req),
})
return reply.send(result)
} catch (e) {
const status = (e as { statusCode?: number }).statusCode ?? 502
@@ -82,14 +86,7 @@ const trafficFlowRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/traffic/flow/host-files", async (_req, reply) => {
const row = getTrafficFlowSettingsRow()
if (!row.hostPrivateKey) ensureHostKeys()
return reply.send({
files: [
{ id: "wg-quick", label: "wg-flow.conf", filename: "wg-flow.conf", code: buildHostWgQuickConf() },
{ id: "compose", label: "docker-compose", filename: "docker-compose.flow.yml", code: buildHostComposeSnippet() },
{ id: "nft", label: "nftables", filename: "wg-flow.nft", code: buildHostNftSnippet() },
{ id: "ufw", label: "ufw", filename: "wg-flow.ufw.sh", code: buildHostUfwSnippet() },
],
})
return reply.send({ files: listTrafficFlowHostFiles() })
})
app.post("/traffic/flow/overlay", applyOverlayHandler)
@@ -1,5 +1,6 @@
import { generateNativeConf } from "./wireguard-config.js"
import { getTrafficFlowSettingsRow, listHostPeers } from "./traffic-flow-settings.js"
import type { TrafficFlowHostFile } from "@mmapp/contracts/traffic-flow"
export function buildHostWgQuickConf(): string {
const row = getTrafficFlowSettingsRow()
@@ -58,3 +59,12 @@ export function buildHostUfwSnippet(): string {
`ufw deny ${row.flowListenPort}/udp comment 'ipfix-not-public'`,
].join("\n")
}
export function listTrafficFlowHostFiles(): TrafficFlowHostFile[] {
return [
{ id: "wg-quick", label: "wg-flow.conf", filename: "wg-flow.conf", code: buildHostWgQuickConf() },
{ id: "compose", label: "docker-compose", filename: "docker-compose.flow.yml", code: buildHostComposeSnippet() },
{ id: "nft", label: "nftables", filename: "wg-flow.nft", code: buildHostNftSnippet() },
{ id: "ufw", label: "ufw", filename: "wg-flow.ufw.sh", code: buildHostUfwSnippet() },
]
}
+34 -7
View File
@@ -16,8 +16,10 @@ import {
import {
ensureHostKeys,
getTrafficFlowSettingsRow,
updateTrafficFlowSettings,
upsertHostPeer,
} from "./traffic-flow-settings.js"
import { listTrafficFlowHostFiles } from "./traffic-flow-host-files.js"
const IFACE_NAME = "wg-flow"
const JH_LISTEN_PORT = 13232
@@ -136,15 +138,35 @@ async function ensureTrafficFlow(client: MikrotikClient, collectorIp: string, po
await client.put("/ip/traffic-flow/target", body)
}
export async function applyFlowOverlay(serverIdRaw: string | number): Promise<TrafficFlowOverlayResult> {
export function usablePublicHost(raw: string | undefined): string {
if (!raw) return ""
const host = raw.split(",")[0]?.trim().replace(/^\[/, "").replace(/\]:\d+$/, "").split(":")[0]?.trim() ?? ""
const lower = host.toLowerCase()
if (!host) return ""
if (lower === "localhost" || lower === "127.0.0.1" || lower === "::1" || lower === "0.0.0.0") return ""
if (lower.endsWith(".local") || lower.endsWith(".internal") || lower.endsWith(".lan")) return ""
if (/^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(host)) return ""
return host
}
export async function applyFlowOverlay(
serverIdRaw: string | number,
opts?: { publicEndpoint?: string; requestHost?: string },
): Promise<TrafficFlowOverlayResult> {
const steps: string[] = []
const settings = getTrafficFlowSettingsRow()
const keys = ensureHostKeys()
if (!settings.hostPublicKey && !keys.publicKey) {
throw Object.assign(new Error("Сначала сгенерируйте ключи хоста MM в настройках NetFlow"), { statusCode: 400 })
}
let settings = getTrafficFlowSettingsRow()
const hostPublicKey = settings.hostPublicKey || keys.publicKey
if (!settings.publicEndpoint.trim()) {
if (!hostPublicKey) {
throw Object.assign(new Error("Не удалось создать ключи хоста MM"), { statusCode: 500 })
}
const endpointHost = (
opts?.publicEndpoint?.trim()
|| settings.publicEndpoint.trim()
|| usablePublicHost(opts?.requestHost)
).trim()
if (!endpointHost) {
throw Object.assign(new Error("Укажите публичный endpoint хоста MM (IP или DNS)"), { statusCode: 400 })
}
@@ -153,6 +175,11 @@ export async function applyFlowOverlay(serverIdRaw: string | number): Promise<Tr
throw Object.assign(new Error("Сервер не найден или выключен"), { statusCode: 404 })
}
if (endpointHost !== settings.publicEndpoint.trim()) {
updateTrafficFlowSettings({ publicEndpoint: endpointHost })
settings = getTrafficFlowSettingsRow()
}
const taken = new Set(
db.select({ ip: servers.mgmtTunnelIp }).from(servers).all()
.map((r) => r.ip)
@@ -187,7 +214,6 @@ export async function applyFlowOverlay(serverIdRaw: string | number): Promise<Tr
}
const peer = await findPeer(client, IFACE_NAME, hostPublicKey)
const endpointHost = settings.publicEndpoint.trim()
const peerBody = {
interface: IFACE_NAME,
"public-key": hostPublicKey,
@@ -258,6 +284,7 @@ export async function applyFlowOverlay(serverIdRaw: string | number): Promise<Tr
linuxPeerBlock: linuxPeerBlock(publicKey, address, server.name || server.host),
trafficFlow: true,
steps,
hostFiles: listTrafficFlowHostFiles(),
}
} catch (e) {
const msg = e instanceof MikrotikError ? e.message : e instanceof Error ? e.message : String(e)
@@ -31,4 +31,11 @@ const taken = new Set(["10.255.254.2"])
assert.equal(allocateOverlayAddress("10.255.254.0/24", "10.255.254.1", 1, taken), "10.255.254.3")
assert.equal(allocateOverlayAddress("10.255.254.0/24", "10.255.254.1", 2, new Set()), "10.255.254.3")
import { usablePublicHost } from "./traffic-flow-overlay.js"
assert.equal(usablePublicHost("localhost:8000"), "")
assert.equal(usablePublicHost("127.0.0.1"), "")
assert.equal(usablePublicHost("192.168.1.10"), "")
assert.equal(usablePublicHost("mm.example.com:443"), "mm.example.com")
assert.equal(usablePublicHost("203.0.113.10"), "203.0.113.10")
console.log("traffic-flow-parse.test.ts: ok")