feat(traffic): добавить приём Traffic Flow с jump-host
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Successful in 2m1s
Docker images / frontend-image (push) Successful in 3m56s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 46s
Docker images / publish-release (push) Successful in 12s
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Successful in 2m1s
Docker images / frontend-image (push) Successful in 3m56s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 46s
Docker images / publish-release (push) Successful in 12s
Чтобы видеть «кто с кем», а не только объём порта: IPFIX внутри WG на хосте Docker MM, REST-счётчики не трогаем. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
5884bd8873
commit
1e9312acbd
24 files changed
+2007
-71
No files matched your search
@@ -0,0 +1,102 @@
|
||||
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
||||
import type { FastifyReply, FastifyRequest } from "fastify"
|
||||
import {
|
||||
trafficFlowOverlayRequestSchema,
|
||||
trafficFlowSettingsPatchSchema,
|
||||
} from "@mmapp/contracts/traffic-flow"
|
||||
import {
|
||||
ensureHostKeys,
|
||||
getTrafficFlowSettingsRow,
|
||||
toTrafficFlowSettingsDto,
|
||||
updateTrafficFlowSettings,
|
||||
} from "../services/traffic-flow-settings.js"
|
||||
import {
|
||||
getFlowListenerState,
|
||||
listFlowTalkers,
|
||||
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"
|
||||
|
||||
function rangeToMinutes(range: string | undefined): number {
|
||||
switch ((range ?? "5m").toLowerCase()) {
|
||||
case "5m": return 5
|
||||
case "15m": return 15
|
||||
case "1h": return 60
|
||||
case "4h": return 240
|
||||
case "24h": return 1440
|
||||
default: return 5
|
||||
}
|
||||
}
|
||||
|
||||
async function sendFlowTalkers(req: FastifyRequest, reply: FastifyReply) {
|
||||
const q = req.query as { range?: string }
|
||||
return reply.send(listFlowTalkers(rangeToMinutes(q.range)))
|
||||
}
|
||||
|
||||
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)
|
||||
return reply.send(result)
|
||||
} catch (e) {
|
||||
const status = (e as { statusCode?: number }).statusCode ?? 502
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
return reply.status(status).send({ error: msg })
|
||||
}
|
||||
}
|
||||
|
||||
const trafficFlowRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
app.get("/traffic/flow/settings", async (_req, reply) => {
|
||||
return reply.send(toTrafficFlowSettingsDto(getFlowListenerState()))
|
||||
})
|
||||
|
||||
app.put("/traffic/flow/settings", async (req, reply) => {
|
||||
const parsed = trafficFlowSettingsPatchSchema.safeParse(req.body ?? {})
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
|
||||
}
|
||||
updateTrafficFlowSettings(parsed.data)
|
||||
startTrafficFlowListener()
|
||||
return reply.send({ ok: true, settings: toTrafficFlowSettingsDto(getFlowListenerState()) })
|
||||
})
|
||||
|
||||
app.post("/traffic/flow/settings/generate-keys", async (_req, reply) => {
|
||||
const result = ensureHostKeys()
|
||||
return reply.send({
|
||||
ok: true,
|
||||
created: result.created,
|
||||
publicKey: result.publicKey,
|
||||
settings: toTrafficFlowSettingsDto(getFlowListenerState()),
|
||||
})
|
||||
})
|
||||
|
||||
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() },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
app.post("/traffic/flow/overlay", applyOverlayHandler)
|
||||
app.post("/traffic/flow-overlay", applyOverlayHandler)
|
||||
|
||||
app.get("/traffic/flow", sendFlowTalkers)
|
||||
app.get("/traffic/flows", sendFlowTalkers)
|
||||
}
|
||||
|
||||
export default trafficFlowRoutes
|
||||
@@ -21,6 +21,12 @@ import {
|
||||
getEnabledServerById,
|
||||
listWireGuardInterfaces,
|
||||
} from "../services/wireguard-live.js"
|
||||
import {
|
||||
putIpAddress,
|
||||
putWireguardInterface,
|
||||
putWireguardPeer,
|
||||
toRosBody,
|
||||
} from "../services/wireguard-ros.js"
|
||||
|
||||
function serverIdParam(v: string): string {
|
||||
return decodeURIComponent(v)
|
||||
@@ -30,14 +36,6 @@ function rosIdParam(v: string): string {
|
||||
return decodeURIComponent(v)
|
||||
}
|
||||
|
||||
function toRosBody(obj: Record<string, string | undefined>): Record<string, string> {
|
||||
const out: Record<string, string> = {}
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
if (v !== undefined && v !== "") out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function peerToRosBody(p: Omit<WgCreatePeerRequest, "serverId" | "interfaceName"> & { interfaceName: string }) {
|
||||
return toRosBody({
|
||||
interface: p.interfaceName,
|
||||
@@ -99,20 +97,17 @@ async function applyParsedConfig(
|
||||
comment: parsed.interface.comment,
|
||||
disabled: parsed.interface.disabled ? "yes" : undefined,
|
||||
})
|
||||
await client.put("/interface/wireguard", ifaceBody)
|
||||
await putWireguardInterface(client, ifaceBody)
|
||||
|
||||
if (parsed.interface.address) {
|
||||
await client.put("/ip/address", {
|
||||
address: parsed.interface.address,
|
||||
interface: name,
|
||||
})
|
||||
await putIpAddress(client, parsed.interface.address, name)
|
||||
}
|
||||
|
||||
let peersCreated = 0
|
||||
for (const p of parsed.peers) {
|
||||
if (!p.publicKey) continue
|
||||
await client.put(
|
||||
"/interface/wireguard/peers",
|
||||
await putWireguardPeer(
|
||||
client,
|
||||
peerToRosBody({
|
||||
interfaceName: name,
|
||||
publicKey: p.publicKey,
|
||||
@@ -164,30 +159,21 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
|
||||
const client = MikrotikClient.fromServer(server)
|
||||
try {
|
||||
await client.put(
|
||||
"/interface/wireguard",
|
||||
toRosBody({
|
||||
name: body.name,
|
||||
"listen-port": String(body.listenPort),
|
||||
mtu: String(body.mtu),
|
||||
comment: body.comment,
|
||||
"private-key": body.privateKey,
|
||||
disabled: body.disabled ? "yes" : undefined,
|
||||
}),
|
||||
)
|
||||
await putWireguardInterface(client, {
|
||||
name: body.name,
|
||||
"listen-port": String(body.listenPort),
|
||||
mtu: String(body.mtu),
|
||||
comment: body.comment,
|
||||
"private-key": body.privateKey,
|
||||
disabled: body.disabled ? "yes" : undefined,
|
||||
})
|
||||
|
||||
if (body.address) {
|
||||
await client.put("/ip/address", {
|
||||
address: body.address,
|
||||
interface: body.name,
|
||||
})
|
||||
await putIpAddress(client, body.address, body.name)
|
||||
}
|
||||
|
||||
if (body.peer) {
|
||||
await client.put(
|
||||
"/interface/wireguard/peers",
|
||||
peerToRosBody({ ...body.peer, interfaceName: body.name }),
|
||||
)
|
||||
await putWireguardPeer(client, peerToRosBody({ ...body.peer, interfaceName: body.name }))
|
||||
}
|
||||
|
||||
const list = await listWireGuardInterfaces({
|
||||
@@ -255,7 +241,7 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
|
||||
const client = MikrotikClient.fromServer(server)
|
||||
try {
|
||||
await client.put("/interface/wireguard/peers", peerToRosBody(body))
|
||||
await putWireguardPeer(client, peerToRosBody(body))
|
||||
return reply.status(201).send({ ok: true })
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
|
||||
Reference in new issue
Block a user