- IPFIX top-разговоры. Счётчики интерфейсов — в режимах Серверы / Клиенты / Интерфейсы.
-
{TRAFFIC_RANGE_KEYS.map((key) => (
@@ -1116,12 +1149,7 @@ export default function TrafficPage() {
"\$COMPOSE_DIR/docker-compose.override.yml" <<'OVEOF'
${override}OVEOF
cd "\$COMPOSE_DIR"
-docker compose up -d backend
+docker compose up -d --force-recreate backend
-echo "=== UDP \${FLOW_PORT} на хосте ==="
+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
-# ufw: исходящий WG не открывать; 4739 на WAN не публиковать
-if command -v ufw >/dev/null 2>&1; then
- ufw deny "\${FLOW_PORT}/udp" comment 'ipfix-not-public' || true
-fi
+# 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."
`
diff --git a/backend/src/services/traffic-flow-ingest.ts b/backend/src/services/traffic-flow-ingest.ts
index f7bfa31..40f8e62 100644
--- a/backend/src/services/traffic-flow-ingest.ts
+++ b/backend/src/services/traffic-flow-ingest.ts
@@ -265,6 +265,9 @@ export function listFlowTalkers(minutes = 5): FlowStatsDto {
lastExporterIp: settings.lastExporterIp ?? null,
lastError: settings.lastError || null,
packetsReceived: settings.packetsReceived,
+ lastDatagramAt: settings.lastDatagramAt ?? null,
+ listenerBound: state.bound,
+ listenerAddress: state.address,
}
}
diff --git a/backend/src/services/traffic-flow-overlay.ts b/backend/src/services/traffic-flow-overlay.ts
index a4794f7..fd0c02a 100644
--- a/backend/src/services/traffic-flow-overlay.ts
+++ b/backend/src/services/traffic-flow-overlay.ts
@@ -14,10 +14,12 @@ import {
toRosBody,
} from "./wireguard-ros.js"
import {
+ enableTrafficFlowIngest,
ensureHostKeys,
getTrafficFlowSettingsRow,
upsertHostPeer,
} from "./traffic-flow-settings.js"
+import { startTrafficFlowListener } from "./traffic-flow-ingest.js"
import { listTrafficFlowHostFiles } from "./traffic-flow-host-files.js"
const IFACE_NAME = "wg-flow"
@@ -92,11 +94,13 @@ async function ensureWgInputAccept(client: MikrotikClient, listenPort: number):
return true
}
+/** Официальный авто-source UDP IPFIX, не фильтр 0.0.0.0/0. */
+export const FLOW_TARGET_SRC_AUTO = "0.0.0.0"
+
async function ensureTrafficFlow(
client: MikrotikClient,
collectorIp: string,
port: number,
- srcAddress: string,
): Promise {
const body = toRosBody({
enabled: "yes",
@@ -116,7 +120,7 @@ async function ensureTrafficFlow(
const existing = targets.find((t) => String(t["dst-address"] ?? "") === collectorIp)
const targetBody = toRosBody({
"dst-address": collectorIp,
- "src-address": srcAddress,
+ "src-address": FLOW_TARGET_SRC_AUTO,
port: String(port),
version: "ipfix",
})
@@ -238,8 +242,8 @@ export async function applyFlowOverlay(
steps.push("Firewall input WG уже есть")
}
- await ensureTrafficFlow(client, settings.collectorIp, settings.flowListenPort, address)
- steps.push(`Traffic Flow → ${settings.collectorIp}:${settings.flowListenPort} ipfix (src ${address})`)
+ await ensureTrafficFlow(client, settings.collectorIp, settings.flowListenPort)
+ steps.push(`Traffic Flow → ${settings.collectorIp}:${settings.flowListenPort} ipfix (src auto)`)
const listed = await listWireGuardInterfaces({ serverId: String(server.id), includePrivateKey: false })
const created = listed.interfaces.find((i) => i.name === IFACE_NAME)
@@ -262,6 +266,10 @@ export async function applyFlowOverlay(
endpoint: peerEndpoint,
})
+ enableTrafficFlowIngest()
+ startTrafficFlowListener()
+ steps.push("Коллектор IPFIX на MM включён")
+
return {
ok: true,
serverId: server.id,
diff --git a/backend/src/services/traffic-flow-parse.test.ts b/backend/src/services/traffic-flow-parse.test.ts
index 29ba034..1cfc30d 100644
--- a/backend/src/services/traffic-flow-parse.test.ts
+++ b/backend/src/services/traffic-flow-parse.test.ts
@@ -1,6 +1,6 @@
import assert from "node:assert/strict"
import { parseFlowPacket, protoName, resetFlowTemplatesForTests } from "./traffic-flow-parse.js"
-import { allocateOverlayAddress } from "./traffic-flow-overlay.js"
+import { allocateOverlayAddress, FLOW_TARGET_SRC_AUTO, usablePublicHost } from "./traffic-flow-overlay.js"
function netflowV5One(): Buffer {
const buf = Buffer.alloc(24 + 48)
@@ -31,12 +31,12 @@ 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")
+assert.equal(FLOW_TARGET_SRC_AUTO, "0.0.0.0")
resetFlowTemplatesForTests()
{
diff --git a/backend/src/services/traffic-flow-settings.ts b/backend/src/services/traffic-flow-settings.ts
index 8196144..19f8795 100644
--- a/backend/src/services/traffic-flow-settings.ts
+++ b/backend/src/services/traffic-flow-settings.ts
@@ -121,6 +121,13 @@ export function recordFlowListenerError(message: string) {
}).where(eq(trafficFlowSettings.id, 1)).run()
}
+export function enableTrafficFlowIngest() {
+ db.update(trafficFlowSettings).set({
+ enabled: true,
+ updatedAt: nowIso(),
+ }).where(eq(trafficFlowSettings.id, 1)).run()
+}
+
export function listHostPeers(): FlowHostPeer[] {
return parsePeers(getTrafficFlowSettingsRow().peersJson)
}
diff --git a/deploy/docker-compose.cdn-mm.yml b/deploy/docker-compose.cdn-mm.yml
index 3000443..b9421e2 100644
--- a/deploy/docker-compose.cdn-mm.yml
+++ b/deploy/docker-compose.cdn-mm.yml
@@ -136,8 +136,9 @@ services:
AUTH_PORTAL_URL: ${AUTH_PORTAL_URL:-https://auth.shnt.top}
# IPFIX: внутри контейнера слушать все iface; на хосте bind только WG-IP после wg-quick@wg-flow
FLOW_LISTEN_HOST: "0.0.0.0"
- # ports:
- # - "10.255.254.1:4739:4739/udp"
+ # Сначала wg-quick@wg-flow (адрес 10.255.254.1), затем recreate backend.
+ ports:
+ - "10.255.254.1:4739:4739/udp"
volumes:
- ./data/mm:/app/data
networks:
diff --git a/packages/contracts/src/traffic-flow.ts b/packages/contracts/src/traffic-flow.ts
index e8e91f9..36f24e4 100644
--- a/packages/contracts/src/traffic-flow.ts
+++ b/packages/contracts/src/traffic-flow.ts
@@ -91,6 +91,9 @@ export const flowStatsDtoSchema = z.object({
lastExporterIp: z.string().nullable().optional(),
lastError: z.string().nullable().optional(),
packetsReceived: z.number().int().nonnegative().optional(),
+ lastDatagramAt: z.string().nullable().optional(),
+ listenerBound: z.boolean().optional(),
+ listenerAddress: z.string().nullable().optional(),
})
export type FlowHostPeer = z.infer