fix(netflow): сохранять отдельные поля и защищать созданную пару ключей
This commit is contained in:
1 parent
8b017bab3f
commit
e2aae3897d
7 files changed
+70
-32
No files matched your search
@@ -28,7 +28,7 @@
|
||||
"test:backups": "tsx src/services/backup-scheduler-run.test.ts && tsx src/services/backup-restore.test.ts && tsx src/services/backup-deletion.test.ts && tsx src/services/backup-dto.test.ts && tsx src/services/backup-storage-update.test.ts && tsx src/services/backup-job-execution.test.ts && tsx src/routes/backups-operations.test.ts && tsx src/services/s3-backup-client.test.ts",
|
||||
"test:live-maps": "tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts && tsx src/routes/bgp-catalog.test.ts && tsx src/services/bgp-catalog-parse.test.ts && tsx src/routes/ospf-catalog.test.ts && tsx src/services/ospf-observed-values.test.ts",
|
||||
"test": "npm run test:probes && npm run test:exec && npm run test:alert-engine && npm run test:auth && npm run test:wireguard && npm run test:traffic-rate && npm run test:traffic-flow && npm run test:users && npm run test:pg && npm run test:backups && npm run test:scheduler && npm run test:live-maps && npm run test:ospf-optimizer && npm run test:recursive-routes && npm run test:config-sync && npm run test:ros-file && npm run test:containers && npm run test:certificates",
|
||||
"test:netflow-settings": "tsx src/routes/traffic-flow-settings.test.ts && tsx src/plugins/auth.netflow-settings.test.ts",
|
||||
"test:netflow-settings": "tsx src/services/traffic-flow-key-execution.test.ts && tsx src/routes/traffic-flow-settings.test.ts && tsx src/plugins/auth.netflow-settings.test.ts",
|
||||
"test:geoip": "tsx src/services/traffic-flow-geoip.test.ts && tsx src/plugins/auth.geoip.test.ts",
|
||||
"test:ospf-optimizer": "tsx src/services/ospf-optimizer-identity.test.ts && tsx src/services/ospf-optimizer-metrics.test.ts && tsx src/services/ospf-optimizer-plan.test.ts && tsx src/routes/ospf-optimizer.test.ts",
|
||||
"test:containers": "tsx src/services/containers-state.test.ts && tsx src/routes/containers-state.test.ts",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { ensureTrafficFlowHostKeys } from "./traffic-flow-key-execution.js"
|
||||
let pair = {publicKey:"",privateKey:""},generated=0,writes=0
|
||||
let release!:()=>void
|
||||
const bothRead = new Promise<void>(resolve=>{release=resolve})
|
||||
let initialReads=0
|
||||
const deps = {
|
||||
read:async()=>{const snapshot={...pair}; if(initialReads<2){initialReads++;if(initialReads===2)release();await bothRead} return snapshot},
|
||||
generate:()=>{generated++;return {publicKey:"public-"+generated,privateKey:"private-"+generated}},
|
||||
installIfMissing:async(keys:typeof pair)=>{writes++;if(pair.publicKey&&pair.privateKey)return false;pair=keys;return true},
|
||||
}
|
||||
const results=await Promise.all([ensureTrafficFlowHostKeys(deps),ensureTrafficFlowHostKeys(deps)])
|
||||
assert.equal(generated,2);assert.equal(writes,2);assert.equal(results.filter(r=>r.created).length,1)
|
||||
assert.equal(results[0].publicKey,results[1].publicKey);assert.equal(results[0].publicKey,pair.publicKey)
|
||||
const before={...pair};assert.equal((await ensureTrafficFlowHostKeys(deps)).created,false);assert.deepEqual(pair,before);assert.equal(writes,2)
|
||||
await assert.rejects(ensureTrafficFlowHostKeys({read:async()=>({publicKey:"",privateKey:""}),generate:deps.generate,installIfMissing:async()=>true}),/не подтверждено/)
|
||||
let called=0
|
||||
await assert.rejects(ensureTrafficFlowHostKeys({read:async()=>({publicKey:"",privateKey:""}),generate:deps.generate,installIfMissing:async()=>{called++;throw Error("DB failure")}}),/DB failure/)
|
||||
assert.equal(called,1);console.log("NetFlow key execution: concurrent callers share winner, existing pair unchanged, unknown/failure never retried")
|
||||
@@ -0,0 +1,15 @@
|
||||
type KeyPair = { publicKey: string; privateKey: string }
|
||||
type Dependencies = {
|
||||
read: () => Promise<KeyPair>
|
||||
generate: () => KeyPair
|
||||
installIfMissing: (keys: KeyPair) => Promise<boolean>
|
||||
}
|
||||
/** A conditional database write selects the winner across processes; no existing pair is rotated. */
|
||||
export async function ensureTrafficFlowHostKeys(deps: Dependencies): Promise<{ publicKey: string; created: boolean }> {
|
||||
const existing = await deps.read()
|
||||
if (existing.publicKey && existing.privateKey) return { publicKey: existing.publicKey, created: false }
|
||||
const created = await deps.installIfMissing(deps.generate())
|
||||
const confirmed = await deps.read()
|
||||
if (!confirmed.publicKey || !confirmed.privateKey) throw new Error("Создание ключей NetFlow не подтверждено")
|
||||
return { publicKey: confirmed.publicKey, created }
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { eq } from "drizzle-orm"
|
||||
import { and, eq, or } from "drizzle-orm"
|
||||
import { db } from "../db/index.js"
|
||||
import { parseJsonArray } from "../db/json.js"
|
||||
import { trafficFlowSettings } from "../db/schema.js"
|
||||
import type { FlowHostPeer, TrafficFlowSettingsDto, TrafficFlowSettingsPatch } from "@mmapp/contracts/traffic-flow"
|
||||
import { ensureTrafficFlowHostKeys } from "./traffic-flow-key-execution.js"
|
||||
import { generateWireGuardKeyPair } from "./wg-keys.js"
|
||||
|
||||
type SettingsRow = NonNullable<Awaited<ReturnType<typeof readSettingsRow>>>
|
||||
@@ -38,7 +39,7 @@ export async function getTrafficFlowSettingsRow(): Promise<SettingsRow> {
|
||||
prefix: "10.255.254.0/24",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
}).onConflictDoNothing()
|
||||
row = await readSettingsRow()
|
||||
settingsRowCache = row!
|
||||
return settingsRowCache
|
||||
@@ -79,39 +80,31 @@ export async function toTrafficFlowSettingsDto(
|
||||
}
|
||||
|
||||
export async function updateTrafficFlowSettings(patch: TrafficFlowSettingsPatch) {
|
||||
const row = await getTrafficFlowSettingsRow()
|
||||
await db.update(trafficFlowSettings).set({
|
||||
enabled: patch.enabled ?? row.enabled,
|
||||
collectorIp: patch.collectorIp ?? row.collectorIp,
|
||||
flowListenPort: patch.flowListenPort ?? row.flowListenPort,
|
||||
wgListenPort: patch.wgListenPort ?? row.wgListenPort,
|
||||
prefix: patch.prefix ?? row.prefix,
|
||||
publicEndpoint: patch.publicEndpoint ?? row.publicEndpoint,
|
||||
hubServerId: patch.hubServerId === undefined ? row.hubServerId : patch.hubServerId,
|
||||
retentionHours: patch.retentionHours ?? row.retentionHours,
|
||||
topN: patch.topN ?? row.topN,
|
||||
mapServiceMinSharePct: patch.mapServiceMinSharePct == null
|
||||
? row.mapServiceMinSharePct
|
||||
: Math.min(100, Math.max(0, patch.mapServiceMinSharePct)),
|
||||
updatedAt: nowIso(),
|
||||
}).where(eq(trafficFlowSettings.id, 1))
|
||||
await getTrafficFlowSettingsRow()
|
||||
// Do not copy untouched fields from a potentially stale cache into a concurrent update.
|
||||
await db.update(trafficFlowSettings).set({ ...patch, updatedAt: nowIso() }).where(eq(trafficFlowSettings.id, 1))
|
||||
invalidateTrafficFlowSettingsCache()
|
||||
return await getTrafficFlowSettingsRow()
|
||||
}
|
||||
|
||||
export async function ensureHostKeys(): Promise<{ publicKey: string; created: boolean }> {
|
||||
const row = await getTrafficFlowSettingsRow()
|
||||
if (row.hostPublicKey && row.hostPrivateKey) {
|
||||
return { publicKey: row.hostPublicKey, created: false }
|
||||
}
|
||||
const keys = generateWireGuardKeyPair()
|
||||
await db.update(trafficFlowSettings).set({
|
||||
hostPublicKey: keys.publicKey,
|
||||
hostPrivateKey: keys.privateKey,
|
||||
updatedAt: nowIso(),
|
||||
}).where(eq(trafficFlowSettings.id, 1))
|
||||
invalidateTrafficFlowSettingsCache()
|
||||
return { publicKey: keys.publicKey, created: true }
|
||||
await getTrafficFlowSettingsRow()
|
||||
return ensureTrafficFlowHostKeys({
|
||||
read: async () => {
|
||||
invalidateTrafficFlowSettingsCache()
|
||||
const row = await getTrafficFlowSettingsRow()
|
||||
return { publicKey: row.hostPublicKey, privateKey: row.hostPrivateKey }
|
||||
},
|
||||
generate: generateWireGuardKeyPair,
|
||||
installIfMissing: async keys => {
|
||||
const rows = await db.update(trafficFlowSettings).set({
|
||||
hostPublicKey: keys.publicKey, hostPrivateKey: keys.privateKey, updatedAt: nowIso(),
|
||||
}).where(and(eq(trafficFlowSettings.id, 1), or(eq(trafficFlowSettings.hostPublicKey, ""), eq(trafficFlowSettings.hostPrivateKey, ""))))
|
||||
.returning({ id: trafficFlowSettings.id })
|
||||
invalidateTrafficFlowSettingsCache()
|
||||
return rows.length === 1
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function upsertHostPeer(peer: FlowHostPeer) {
|
||||
|
||||
@@ -254,3 +254,5 @@ REST запрос → ожидание → подтверждённый output /
|
||||
Продолжение 2026-10-03: завершён 11Б.3.4.4.1 (строгие NetFlow API, 5 unit и 3 collector/probes E2E, typecheck/ESLint). Следующий блок — 11Б.3.4.4.2: backend экспорт/права и подтверждение сохранения отдельно от запуска listener. Purge и форма ещё требуют доработки.
|
||||
|
||||
Завершён 11Б.3.4.4.2: private export требует traffic write, GET не создаёт ключи, PUT отделяет сохранение от ошибки запуска listener. HTTP/JWT, backend build, lint и permissions прошли. Далее 11Б.3.4.4.3: конкурентное создание ключей/выборочные patch; purge и UI ещё не приняты.
|
||||
|
||||
Завершён 11Б.3.4.4.3: выборочная запись NetFlow patch и условная установка host keys, свежая подтверждённая пара; DI гонка/HTTP/JWT/build/lint прошли. Реальная PG гонка не проверена. Далее 11Б.3.4.4.4 — атомарность purge и частичные результаты.
|
||||
@@ -28,7 +28,7 @@
|
||||
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
||||
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
|
||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.4.4.2; status/history/KPI, strict settings API основа и защита каталога Evolution проверены; далее readers/dirty формы |
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.4.4.3; status/history/KPI, strict settings API основа и защита каталога Evolution проверены; далее readers/dirty формы |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||
|
||||
|
||||
@@ -36,6 +36,13 @@
|
||||
|
||||
|
||||
|
||||
## Блок 11Б.3.4.4.3 — конкурентные настройки и ключи NetFlow (2026-10-03)
|
||||
|
||||
- Settings UPDATE изменяет только поля patch, не переносит соседние настройки из старого cache. Первичная вставка singleton использует onConflictDoNothing, параллельная инициализация не падает из-за дубликата.
|
||||
- Генерация ключей использует условный UPDATE только при неполной паре и fresh read после записи. Конкурирующий запрос получает public key победителя с created=false; существующая полная пара не вращается. Ошибка записи/неподтверждённая пара не повторяются автоматически.
|
||||
- DI race test принудительно синхронизирует два исходных чтения: оба кандидата созданы, установлен один, оба ответа показывают одну пару. HTTP/JWT профиль, backend build и ESLint прошли. Атомарное условие SQL проверено типами/ревью, реальная межпроцессная гонка PG не проверена локальными mock tests.
|
||||
- Далее 11Б.3.4.4.4: атомарный purge и независимые ошибки обслуживания/событий; затем форма NetFlow.
|
||||
|
||||
## Блок 11Б.3.4.4.2 — backend NetFlow и приватный экспорт (2026-10-03)
|
||||
|
||||
- GET host-files больше не вызывает ensureHostKeys: без приватного ключа возвращает 409 с предложением явно создать ключи. Экспорт содержит приватный ключ, поэтому требует traffic:write; обычные settings GET остаются traffic:read. AUTH_REQUIRED=false и остальные правила не менялись.
|
||||
|
||||
@@ -182,3 +182,5 @@ ReUI PRO/base-nova; существующие Frame, Data Grid v9, KpiStatGrid, S
|
||||
Продолжение 2026-10-03: завершён 11Б.3.4.4.1 (строгие NetFlow API, 5 unit и 3 collector/probes E2E, typecheck/ESLint). Следующий блок — 11Б.3.4.4.2: backend экспорт/права и подтверждение сохранения отдельно от запуска listener. Purge и форма ещё требуют доработки.
|
||||
|
||||
Завершён 11Б.3.4.4.2: private export требует traffic write, GET не создаёт ключи, PUT отделяет сохранение от ошибки запуска listener. HTTP/JWT, backend build, lint и permissions прошли. Далее 11Б.3.4.4.3: конкурентное создание ключей/выборочные patch; purge и UI ещё не приняты.
|
||||
|
||||
Завершён 11Б.3.4.4.3: выборочная запись NetFlow patch и условная установка host keys, свежая подтверждённая пара; DI гонка/HTTP/JWT/build/lint прошли. Реальная PG гонка не проверена. Далее 11Б.3.4.4.4 — атомарность purge и частичные результаты.
|
||||
Reference in new issue
Block a user