fix(netflow): генерировать файлы хоста из согласованного снимка настроек

This commit is contained in:
Denozordec committed 2026-10-03 13:02:34 +07:00
1 parent b80fa042d5
commit d9e09281e8
7 files changed
+155 -115

No files matched your search

@@ -0,0 +1,12 @@
import assert from "node:assert/strict"
import {buildTrafficFlowHostFiles} from "./traffic-flow-host-file-builders.js"
import {parseNativeConf} from "./wireguard-config.js"
const snapshot={collectorIp:"10.255.254.1",prefix:"10.255.254.0/28",flowListenPort:4740,wgListenPort:51822,hostPrivateKey:Buffer.alloc(32,1).toString("base64"),peers:[{serverId:1,name:"fixture\nWGEOF\nignored",publicKey:Buffer.alloc(32,2).toString("base64"),allowedIps:["10.255.254.2/32"],address:"10.255.254.2",endpoint:"peer.example.test:13232"}]}
const files=buildTrafficFlowHostFiles(snapshot),conf=files.find(f=>f.id==="wg-quick")!.code,compose=files.find(f=>f.id==="compose")!.code,linux=files.find(f=>f.id==="linux")!.code
const parsed=parseNativeConf(conf)
assert.equal(parsed.interface.address,"10.255.254.1/28");assert.equal(parsed.interface.listenPort,51822);assert.equal(parsed.peers.length,1)
assert.ok(compose.includes('"10.255.254.1:4740:4740/udp"'));assert.ok(linux.includes('FLOW_PORT="4740"'));assert.ok(linux.includes(conf.trim()+"\nWGEOF"));assert.ok(linux.includes(compose+"OVEOF"));assert.ok(linux.includes('COLLECTOR_IP}/28'));assert.equal(conf.includes("\nWGEOF\n"),false)
assert.deepEqual(files.map(f=>f.filename),["install-wg-flow.sh","wg-flow.conf","docker-compose.override.yml"])
for(const bad of [{...snapshot,collectorIp:'$(fixture)'},{...snapshot,prefix:"10.0.0.0/33"},{...snapshot,hostPrivateKey:""},{...snapshot,wgListenPort:65536},{...snapshot,peers:[{...snapshot.peers[0],endpoint:"peer.test:0"}]},{...snapshot,peers:[{...snapshot.peers[0],publicKey:"fixture\nWGEOF"}]},{...snapshot,peers:[{...snapshot.peers[0],allowedIps:["bad"]}]}])assert.throws(()=>buildTrafficFlowHostFiles(bad),/не сформированы/)
const other=buildTrafficFlowHostFiles({...snapshot,flowListenPort:4800,wgListenPort:51830,prefix:"10.255.254.0/24"});assert.ok(other[1].code.includes("ListenPort = 51830"));assert.ok(other[1].code.includes("Address = 10.255.254.1/24"));assert.ok(other[2].code.includes(":4800:4800/udp"));assert.ok(files[2].code.includes(":4740:4740/udp"))
console.log("NetFlow host export: one snapshot, actual mask/listen ports, matching script/config, invalid configuration rejected without rendering keys")
@@ -0,0 +1,124 @@
import { generateNativeConf } from "./wireguard-config.js"
import { z } from "zod"
import { flowHostPeerSchema, trafficFlowCollectorIpSchema, trafficFlowOverlayPrefixSchema, trafficFlowPublicEndpointSchema, type TrafficFlowHostFile } from "@mmapp/contracts/traffic-flow"
const keySchema = z.string().regex(/^[A-Za-z0-9+/]{43}=$/)
const cidr = z.union([z.cidrv4(),z.cidrv6()])
const peerSchema = flowHostPeerSchema.extend({publicKey:keySchema,allowedIps:z.array(cidr).min(1),endpoint:z.string().refine(value=>{
const match=value.match(/^(?:\[([^\]]+)\]|([^:]+)):(\d+)$/)
if(!match)return false
const host=match[1]??match[2],port=Number(match[3])
return !!host&&trafficFlowPublicEndpointSchema.safeParse(host).success&&Number.isInteger(port)&&port>=1&&port<=65535
}).optional()})
const snapshotSchema = z.object({collectorIp:trafficFlowCollectorIpSchema,prefix:trafficFlowOverlayPrefixSchema,flowListenPort:z.number().int().min(1).max(65535),wgListenPort:z.number().int().min(1).max(65535),hostPrivateKey:keySchema,peers:z.array(peerSchema)})
type HostExportSnapshot = z.infer<typeof snapshotSchema>
const COMPOSE_DIR = "/opt/cdn-mm"
function buildHostWgQuickConf(row: HostExportSnapshot): string {
const peers = row.peers
return generateNativeConf({
name: "wg-flow",
mtu: 1420,
listenPort: row.wgListenPort,
privateKey: row.hostPrivateKey || undefined,
address: `${row.collectorIp}/${row.prefix.split("/")[1]}`,
comment: "MikrotikManager traffic-flow collector",
peers: peers.map((p) => ({
publicKey: p.publicKey,
allowedIps: p.allowedIps,
comment: p.name.replace(/[\r\n]/g, " "),
endpoint: p.endpoint,
persistentKeepalive: p.endpoint ? 25 : undefined,
})),
})
}
function buildHostComposeOverride(row: HostExportSnapshot): string {
return [
"# Docker Compose merge для /opt/cdn-mm",
"# Не править docker-compose.yml. Traefik не трогать.",
"# Сначала: wg-quick up wg-flow (адрес " + row.collectorIp + ")",
"# затем: docker compose up -d --force-recreate backend",
"# Docker userland-proxy может SNAT UDP source в 172.x — ingest сопоставит единственный JH.",
"",
"services:",
" backend:",
" environment:",
" FLOW_LISTEN_HOST: \"0.0.0.0\"",
" ports:",
` - "${row.collectorIp}:${row.flowListenPort}:${row.flowListenPort}/udp"`,
"",
].join("\n")
}
function buildHostLinuxInstallSh(row: HostExportSnapshot): string {
const conf = buildHostWgQuickConf(row).replace(/\s+$/, "") + "\n"
const override = buildHostComposeOverride(row)
const collector = row.collectorIp
const flowPort = row.flowListenPort
return `#!/usr/bin/env bash
# WG-клиент на хосте /opt/cdn-mm → JH:13232, IPFIX в контейнер backend.
# Запуск: sudo bash install-wg-flow.sh
set -euo pipefail
if [[ \${EUID:-$(id -u)} -ne 0 ]]; then
echo "Запустите от root: sudo bash $0" >&2
exit 1
fi
COLLECTOR_IP="${collector}"
FLOW_PORT="${flowPort}"
COMPOSE_DIR="${COMPOSE_DIR}"
if ! command -v wg >/dev/null 2>&1; then
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y wireguard
fi
install -d -m 700 /etc/wireguard
cat > /etc/wireguard/wg-flow.conf <<'WGEOF'
${conf}WGEOF
chmod 600 /etc/wireguard/wg-flow.conf
systemctl enable --now wg-quick@wg-flow
echo "=== wg show wg-flow ==="
wg show wg-flow
echo "=== адрес (ожидаем \${COLLECTOR_IP}/${row.prefix.split("/")[1]}) ==="
ip -4 addr show dev wg-flow
if [[ ! -d "\$COMPOSE_DIR" ]]; then
echo "Нет \$COMPOSE_DIR — положите override.yml туда вручную (вкладка compose)." >&2
exit 1
fi
cat > "\$COMPOSE_DIR/docker-compose.override.yml" <<'OVEOF'
${override}OVEOF
cd "\$COMPOSE_DIR"
docker compose up -d --force-recreate backend
echo "=== UDP \${FLOW_PORT} на хосте (ожидаем \${COLLECTOR_IP}:\${FLOW_PORT} docker-proxy) ==="
ss -ulnp | grep -E "\${FLOW_PORT}" || true
echo "=== PortBindings mmapp-backend ==="
docker inspect -f '{{json .HostConfig.PortBindings}}' mmapp-backend
echo "=== handshake (keepalive 25s к JH:13232) ==="
wg show wg-flow
# nft на хосте MM не трогаем. Bind только на COLLECTOR_IP, не 0.0.0.0.
# Если backend стартовал до wg-flow: docker compose up -d --force-recreate backend
echo "Готово. Traefik не трогали. UDP \${FLOW_PORT} только на \${COLLECTOR_IP}, не на 0.0.0.0."
`
}
export function buildTrafficFlowHostFiles(raw: unknown): TrafficFlowHostFile[] {
const parsed = snapshotSchema.safeParse(raw)
if (!parsed.success) throw new Error("Файлы NetFlow не сформированы: проверьте адрес, префикс, порты, ключи и пиры хоста")
const row = parsed.data
return [
{ id: "linux", label: "Linux", filename: "install-wg-flow.sh", code: buildHostLinuxInstallSh(row) },
{ id: "wg-quick", label: "wg-flow.conf", filename: "wg-flow.conf", code: buildHostWgQuickConf(row) },
{ id: "compose", label: "compose", filename: "docker-compose.override.yml", code: buildHostComposeOverride(row) },
]
}
+5 -113
View File
@@ -1,116 +1,8 @@
import { generateNativeConf } from "./wireguard-config.js"
import { getTrafficFlowSettingsRow, listHostPeers } from "./traffic-flow-settings.js"
import { trafficFlowCollectorIpSchema, type TrafficFlowHostFile } from "@mmapp/contracts/traffic-flow"
import { getTrafficFlowSettingsRow } from "./traffic-flow-settings.js"
import { parseJsonArray } from "../db/json.js"
import { buildTrafficFlowHostFiles } from "./traffic-flow-host-file-builders.js"
const COMPOSE_DIR = "/opt/cdn-mm"
export async function buildHostWgQuickConf(): Promise<string> {
export async function listTrafficFlowHostFiles() {
const row = await getTrafficFlowSettingsRow()
trafficFlowCollectorIpSchema.parse(row.collectorIp)
const peers = await listHostPeers()
return generateNativeConf({
name: "wg-flow",
mtu: 1420,
privateKey: row.hostPrivateKey || undefined,
address: `${row.collectorIp}/24`,
comment: "MikrotikManager traffic-flow collector",
peers: peers.map((p) => ({
publicKey: p.publicKey,
allowedIps: p.allowedIps,
comment: p.name,
endpoint: p.endpoint,
persistentKeepalive: p.endpoint ? 25 : undefined,
})),
})
}
export async function buildHostComposeOverride(): Promise<string> {
const row = await getTrafficFlowSettingsRow()
trafficFlowCollectorIpSchema.parse(row.collectorIp)
return [
"# Docker Compose merge для /opt/cdn-mm",
"# Не править docker-compose.yml. Traefik не трогать.",
"# Сначала: wg-quick up wg-flow (адрес " + row.collectorIp + ")",
"# затем: docker compose up -d --force-recreate backend",
"# Docker userland-proxy может SNAT UDP source в 172.x — ingest сопоставит единственный JH.",
"",
"services:",
" backend:",
" environment:",
" FLOW_LISTEN_HOST: \"0.0.0.0\"",
" ports:",
` - "${row.collectorIp}:${row.flowListenPort}:${row.flowListenPort}/udp"`,
"",
].join("\n")
}
export async function buildHostLinuxInstallSh(): Promise<string> {
const row = await getTrafficFlowSettingsRow()
trafficFlowCollectorIpSchema.parse(row.collectorIp)
const conf = (await buildHostWgQuickConf()).replace(/\s+$/, "") + "\n"
const override = await buildHostComposeOverride()
const collector = row.collectorIp
const flowPort = row.flowListenPort
return `#!/usr/bin/env bash
# WG-клиент на хосте /opt/cdn-mm → JH:13232, IPFIX в контейнер backend.
# Запуск: sudo bash install-wg-flow.sh
set -euo pipefail
if [[ \${EUID:-$(id -u)} -ne 0 ]]; then
echo "Запустите от root: sudo bash $0" >&2
exit 1
fi
COLLECTOR_IP="${collector}"
FLOW_PORT="${flowPort}"
COMPOSE_DIR="${COMPOSE_DIR}"
if ! command -v wg >/dev/null 2>&1; then
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y wireguard
fi
install -d -m 700 /etc/wireguard
cat > /etc/wireguard/wg-flow.conf <<'WGEOF'
${conf}WGEOF
chmod 600 /etc/wireguard/wg-flow.conf
systemctl enable --now wg-quick@wg-flow
echo "=== wg show wg-flow ==="
wg show wg-flow
echo "=== адрес (ожидаем \${COLLECTOR_IP}/24) ==="
ip -4 addr show dev wg-flow
if [[ ! -d "\$COMPOSE_DIR" ]]; then
echo "Нет \$COMPOSE_DIR — положите override.yml туда вручную (вкладка compose)." >&2
exit 1
fi
cat > "\$COMPOSE_DIR/docker-compose.override.yml" <<'OVEOF'
${override}OVEOF
cd "\$COMPOSE_DIR"
docker compose up -d --force-recreate backend
echo "=== UDP \${FLOW_PORT} на хосте (ожидаем \${COLLECTOR_IP}:\${FLOW_PORT} docker-proxy) ==="
ss -ulnp | grep -E "\${FLOW_PORT}" || true
echo "=== PortBindings mmapp-backend ==="
docker inspect -f '{{json .HostConfig.PortBindings}}' mmapp-backend
echo "=== handshake (keepalive 25s к JH:13232) ==="
wg show wg-flow
# nft на хосте MM не трогаем. Bind только на COLLECTOR_IP, не 0.0.0.0.
# Если backend стартовал до wg-flow: docker compose up -d --force-recreate backend
echo "Готово. Traefik не трогали. UDP \${FLOW_PORT} только на \${COLLECTOR_IP}, не на 0.0.0.0."
`
}
export async function listTrafficFlowHostFiles(): Promise<TrafficFlowHostFile[]> {
return [
{ id: "linux", label: "Linux", filename: "install-wg-flow.sh", code: await buildHostLinuxInstallSh() },
{ id: "wg-quick", label: "wg-flow.conf", filename: "wg-flow.conf", code: await buildHostWgQuickConf() },
{ id: "compose", label: "compose", filename: "docker-compose.override.yml", code: await buildHostComposeOverride() },
]
return buildTrafficFlowHostFiles({ ...row, peers: parseJsonArray(row.peersJson) })
}