diff --git a/backend/src/services/traffic-flow-ip.test.ts b/backend/src/services/traffic-flow-ip.test.ts index 48eb57c2..0dfb9600 100644 --- a/backend/src/services/traffic-flow-ip.test.ts +++ b/backend/src/services/traffic-flow-ip.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict" import { canonicalIp, + parseCidrV6, isNonPublicIp, pickInternetDest, pickInternetPeer, @@ -18,6 +19,29 @@ assert.equal(isNonPublicIp("10.200.100.53/32"), true) assert.equal(isNonPublicIp("173.194.151.65"), false) assert.equal(isNonPublicIp("74.125.104.196/32"), false, "PG inet::text не делает Google приватным") +for (const ip of [ + "::", "0:0:0:0:0:0:0:1", "::192.0.2.1", "::ffff:c000:201", "0:0:0:0:0:ffff:ac10:1", + "64:ff9b:1::1", "100::", "100::ffff:ffff:ffff:ffff", "100:0:0:1::1", + "2001:2::1", "2001:1::4", "2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff", + "2001:db8::1", "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff", "3fff::", "3fff:fff:ffff:ffff:ffff:ffff:ffff:ffff", + "5f00::1", "fc00::1", "fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + "fe80::1%ether1", "febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "fec0::1", "ff02::1", + "2001:db8::1/128", "2001:DB8::1", "2001:::1", "abcd:1234", "::ffff:999.1.1.1", +]) assert.equal(isNonPublicIp(ip), true, ip) +for (const ip of [ + "2001:4860:4860::8888", "2606:4700:4700::1111/128", "2001:4860:0:0:0:0:0:8888", + "64:ff9b::808:808", "100:0:0:2::1", "2001:1::1", "2001:1::2", "2001:1::3", + "2001:3::1", "2001:4:112::1", "2001:20::1", "2001:2f:ffff:ffff:ffff:ffff:ffff:ffff", + "2001:30::1", "2001:3f:ffff:ffff:ffff:ffff:ffff:ffff", "2001::1", "2002::1", + "2001:200::1", "3ffe:ffff::1", "3fff:1000::1", "2620:4f:8000::1", + "::ffff:808:808", "0:0:0:0:0:ffff:808:808", "::ffff:8.8.8.8/128", +]) assert.equal(isNonPublicIp(ip), false, ip) +assert.equal(canonicalIp("::ffff:808:808"), "8.8.8.8", "Hex mapped IPv6 uses IPv4 host identity") +assert.equal(canonicalIp("0:0:0:0:0:ffff:808:808/128"), "8.8.8.8") +assert.equal(parseCidrV6("2001:4860::/0")?.net, 0n) +assert.equal(parseCidrV6("2001:4860::1/128")?.shift, 0n) +for (const cidr of ["::/129", "::/-1", "::/32junk", "::/32/64", "::", "bad::ip/48"]) assert.equal(parseCidrV6(cidr), null) + for (const ip of [ "198.18.0.0", "198.19.255.255", "::ffff:198.18.139.1/32", "192.0.0.0", "192.0.0.8", "192.0.0.11", "192.0.0.255", diff --git a/backend/src/services/traffic-flow-ip.ts b/backend/src/services/traffic-flow-ip.ts index 3b510f9d..1682a33c 100644 --- a/backend/src/services/traffic-flow-ip.ts +++ b/backend/src/services/traffic-flow-ip.ts @@ -1,4 +1,6 @@ -/** IPv4 helpers for RIPEstat prefix cache and EvoBGP CIDR match. */ +import { isIP } from "node:net" + +/** IP helpers for RIPEstat prefix cache and EvoBGP CIDR match. */ /** * Host-семантика PostgreSQL `host(inet)`: снимает `/32` `/128`, `::ffff:`. @@ -9,12 +11,19 @@ export function canonicalIp(raw: string | undefined | null): string { if (!t) return "" const zone = t.indexOf("%") if (zone >= 0) t = t.slice(0, zone) - if (t.toLowerCase().startsWith("::ffff:")) t = t.slice(7) const slash = t.lastIndexOf("/") if (slash >= 0) { const plen = t.slice(slash + 1) if (/^\d+$/.test(plen)) t = t.slice(0, slash) } + if (t.toLowerCase().startsWith("::ffff:") && isIP(t.slice(7)) === 4) t = t.slice(7) + else if (/(?:^|:)ffff:/i.test(t)) { + const n = ipv6ToInt(t) + if (n !== null && n >> 32n === 0xffffn) { + const v4 = Number(n & 0xffffffffn) + t = [v4 >>> 24, (v4 >>> 16) & 255, (v4 >>> 8) & 255, v4 & 255].join(".") + } + } return t.trim() } @@ -58,12 +67,59 @@ const nonPublicV4Ranges = [ ].map(cidr => parseCidrV4(cidr)!) const publicProtocolAnycastV4 = new Set([0xc0000009, 0xc000000a]) // 192.0.0.9/10 +// Only parse validated IPv6. Embedded IPv4 occupies the final two words. +export function ipv6ToInt(ip: string): bigint | null { + if (isIP(ip) !== 6) return null + let value = ip + if (value.includes(".")) { + const last = value.lastIndexOf(":") + const v4 = ipv4ToInt(value.slice(last + 1))! + value = value.slice(0, last + 1) + (v4 >>> 16).toString(16) + ":" + (v4 & 0xffff).toString(16) + } + const [left, right] = value.split("::") + const head = left ? left.split(":") : [] + const tail = right ? right.split(":") : [] + const words = right === undefined ? head : [...head, ...Array(8 - head.length - tail.length).fill("0"), ...tail] + return words.reduce((n, word) => (n << 16n) | BigInt(parseInt(word, 16)), 0n) +} + +function v6Range(cidr: string) { + const [ip, length] = cidr.split("/") + const shift = BigInt(128 - Number(length)) + return { prefix: ipv6ToInt(ip!)! >> shift, shift } +} + +export function parseCidrV6(cidr: string): { net: bigint; shift: bigint; prefixLen: number } | null { + const parts = cidr.split("/") + if (parts.length !== 2 || !/^\d+$/.test(parts[1]!)) return null + const prefixLen = Number(parts[1]) + const addr = ipv6ToInt(parts[0]!) + if (addr === null || prefixLen < 0 || prefixLen > 128) return null + const shift = BigInt(128 - prefixLen) + return { net: addr >> shift, shift, prefixLen } +} + +// IANA non-global assignments plus deprecated site-local and multicast. +// More-specific globally reachable allocations inside 2001::/23 take priority. +const nonPublicV6Ranges = [ + "::/96", "64:ff9b:1::/48", "100::/64", "100:0:0:1::/64", + "2001::/23", "2001:db8::/32", "3fff::/20", "5f00::/16", + "fc00::/7", "fe80::/10", "fec0::/10", "ff00::/8", +].map(v6Range) +const publicProtocolV6Ranges = [ + "2001::/32", // Teredo: IANA reachability is N/A, not non-global. + "2001:1::1/128", "2001:1::2/128", "2001:1::3/128", + "2001:3::/32", "2001:4:112::/48", "2001:20::/28", "2001:30::/28", +].map(v6Range) + export function isNonPublicIp(ip: string): boolean { const trimmed = canonicalIp(ip) if (!trimmed) return true if (trimmed.includes(":")) { - const lower = trimmed.toLowerCase() - return lower === "::1" || lower.startsWith("fe80:") || lower.startsWith("fc") || lower.startsWith("fd") || lower === "::" + const n = ipv6ToInt(trimmed) + if (n === null) return true + if (publicProtocolV6Ranges.some(({ prefix, shift }) => n >> shift === prefix)) return false + return nonPublicV6Ranges.some(({ prefix, shift }) => n >> shift === prefix) } const n = ipv4ToInt(trimmed) if (n == null) return true diff --git a/backend/src/services/traffic-flow-ripe.test.ts b/backend/src/services/traffic-flow-ripe.test.ts index 2fe8852e..9e9689cf 100644 --- a/backend/src/services/traffic-flow-ripe.test.ts +++ b/backend/src/services/traffic-flow-ripe.test.ts @@ -57,6 +57,7 @@ assert.ok(afterFirst >= 2) enqueueRipeMisses(["8.8.8.10"]) await flushRipeQueueForTests() assert.equal(ripeFetchCountForTests(), afterFirst) +assert.equal(lookupRipeCached("::ffff:808:808")?.asn, 15169, "Hex mapped address shares IPv4 cache") seedRipeCacheForTests({ prefix: "198.18.0.0/15", asn: 64500, country: "NL", lat: 52, lng: 4, holder: "STALE", ok: true, fetchedAt: Date.now() }) for (const ip of ["198.18.139.1", "198.19.255.255", "192.0.2.1", "198.51.100.1", "203.0.113.1", "240.0.0.1"]) { @@ -156,4 +157,66 @@ enqueueRipeMisses(["8.8.4.4"]) await flushRipeQueueForTests() assert.ok(ripeFetchCountForTests() > 0) -console.log("traffic-flow-ripe.test.ts: ok (including database pause/resume)") +resetRipeCacheForTests() +disableRipePersistForTests() +const ipv6Requests: string[] = [] +setRipeFetchForTests(async input => { + const url = new URL(String(input)) + ipv6Requests.push(url.searchParams.get("resource")!) + const body = url.pathname.includes("network-info") + ? { data: { prefix: "2001:4860:4860::/48", asns: [15169] } } + : url.pathname.includes("maxmind-geo-lite") + ? { data: { located_resources: [{ locations: [{ country: "US" }] }] } } + : { data: { holder: "GOOGLE" } } + return new Response(JSON.stringify(body), { status: 200 }) +}) +enqueueRipeMisses(["2001:4860:4860::8888/128"]) +await flushRipeQueueForTests() +assert.equal(ipv6Requests[0], "2001:4860:4860::8888", "Host CIDR must not be sent as a RIPE resource") +assert.equal(lookupRipeCached("2001:4860:4860::8888")?.asn, 15169) +assert.equal(lookupRipeCached("2001:4860:4860:0:0:0:0:8844")?.country, "US") +const ipv6Fetches = ripeFetchCountForTests() +enqueueRipeMisses(["2001:4860:4860::8844"]) +await flushRipeQueueForTests() +assert.equal(ripeFetchCountForTests(), ipv6Fetches) +seedRipeCacheForTests({ prefix: "2001:db8::/32", asn: 64500, country: "US", lat: null, lng: null, holder: "STALE", ok: true, fetchedAt: Date.now() }) +for (const ip of ["2001:db8::1", "3fff::1", "fe90::1", "ff02::1", "100:0:0:1::1", "abcd:1234"]) { + const entry = lookupRipeCached(ip) + assert.equal(entry?.ok, false, ip) + assert.equal(entry?.prefix, `${ip}/128`, ip) +} +enqueueRipeMisses(["2001:db8::1", "3fff::1", "fe90::1", "ff02::1", "100:0:0:1::1", "abcd:1234"]) +await flushRipeQueueForTests() +assert.equal(ripeFetchCountForTests(), ipv6Fetches, "Special/invalid IPv6 must not query RIPE") + +const v6Entry = (prefix: string, asn: number, fetchedAt = Date.now()) => ({ prefix, asn, country: "US", lat: null, lng: null, holder: "TEST", ok: true, fetchedAt }) +seedRipeCacheForTests(v6Entry("2001:4860::/32", 1)) +seedRipeCacheForTests(v6Entry("2001:4860:4860::8888/128", 2)) +assert.equal(lookupRipeCached("2001:4860:4860::8888")?.asn, 2, "Longest matching prefix wins") +assert.equal(lookupRipeCached("2001:4860:4860::8844")?.asn, 15169) +assert.equal(lookupRipeCached("2001:4860:1234::1")?.asn, 1, "Wide prefix fallback") +seedRipeCacheForTests(v6Entry("2001:4860:4860::8888/128", 3)) +assert.equal(lookupRipeCached("2001:4860:4860::8888")?.asn, 3, "Replacement removes stale indexed row") +seedRipeCacheForTests(v6Entry("2001:4860:4860::8888/128", 4, Date.now() - 25 * 60 * 60_000)) +assert.equal(lookupRipeCached("2001:4860:4860::8888")?.asn, 15169, "Expired longest prefix falls back") +for (let i = 0; i < 3000; i++) seedRipeCacheForTests(v6Entry(`2606:4700:${i.toString(16)}::/48`, 13335)) +assert.equal(lookupRipeCached("2001:4860:4860::8844")?.asn, 15169) +assert.ok(ripeLastCandidateCountForTests() < 8, "IPv6 lookup must not scan unrelated /48 buckets") + +resetRipeCacheForTests() +disableRipePersistForTests() +setRipeFetchForTests(async () => { throw new Error("timeout") }) +enqueueRipeMisses(["2606:4700:4700::1111"]) +await flushRipeQueueForTests() +assert.equal(lookupRipeCached("2606:4700:4700::1111")?.prefix, "2606:4700:4700::1111/128") +assert.equal(lookupRipeCached("2606:4700:4700::1111")?.ok, false) +assert.equal(lookupRipeCached("2606:4700:4700::1001"), null, "Negative /128 must not poison sibling host") +const afterV6Failure = ripeFetchCountForTests() +assert.ok(afterV6Failure > 0) +enqueueRipeMisses(["2606:4700:4700:0:0:0:0:1111"]) +await flushRipeQueueForTests() +assert.equal(ripeFetchCountForTests(), afterV6Failure, "Expanded spelling hits negative host cache") +resetRipeCacheForTests() +assert.equal(lookupRipeCached("2606:4700:4700::1111"), null, "Invalidation clears IPv6 indexes") + +console.log("traffic-flow-ripe.test.ts: ok (including IPv6 indexes and database pause/resume)") diff --git a/backend/src/services/traffic-flow-ripe.ts b/backend/src/services/traffic-flow-ripe.ts index 15ddc690..664f2c54 100644 --- a/backend/src/services/traffic-flow-ripe.ts +++ b/backend/src/services/traffic-flow-ripe.ts @@ -1,6 +1,6 @@ import { databaseBackgroundWork } from "./database-background-work.js" import { dbAll, dbQuery } from "../db/index.js" -import { canonicalIp, ipv4ToInt, isNonPublicIp, parseCidrV4 } from "./traffic-flow-ip.js" +import { canonicalIp, ipv4ToInt, ipv6ToInt, isNonPublicIp, parseCidrV4, parseCidrV6 } from "./traffic-flow-ip.js" import { resolveRipeCountry } from "./traffic-flow-brands.js" export interface FlowIpMeta { @@ -40,6 +40,14 @@ interface RipeIndexed { /** /24 → кандидаты с prefixLen ≥ 24. Более широкие префиксы — в `wideIndex`. */ const v24Index = new Map() const wideIndex: RipeIndexed[] = [] +interface RipeIndexedV6 { + entry: FlowIpMeta + net: bigint + shift: bigint + prefixLen: number +} +const v48Index = new Map() +const wideV6Index: RipeIndexedV6[] = [] let lastCandidateCount = 0 let persistEnabled = true @@ -75,6 +83,8 @@ export function invalidateRipeCache(): void { recentFetches.length = 0 v24Index.clear() wideIndex.length = 0 + v48Index.clear() + wideV6Index.length = 0 lastCandidateCount = 0 loaded = persistEnabled ? false : true } @@ -115,6 +125,20 @@ function isFresh(entry: FlowIpMeta): boolean { } function unindexPrefix(prefix: string): void { + if (prefix.includes(":")) { + const parsed = parseCidrV6(prefix) + if (!parsed) return + if (parsed.prefixLen >= 48) { + const key = parsed.net >> BigInt(parsed.prefixLen - 48) + const next = v48Index.get(key)?.filter(row => row.entry.prefix !== prefix) + if (next?.length) v48Index.set(key, next) + else v48Index.delete(key) + } else { + const idx = wideV6Index.findIndex(row => row.entry.prefix === prefix) + if (idx >= 0) wideV6Index.splice(idx, 1) + } + return + } const parsed = parseCidrV4(prefix) if (!parsed) return if (parsed.prefixLen >= 24) { @@ -131,6 +155,18 @@ function unindexPrefix(prefix: string): void { } function indexEntry(entry: FlowIpMeta): void { + if (entry.prefix.includes(":")) { + const parsed = parseCidrV6(entry.prefix) + if (!parsed) return + const row = { entry, ...parsed } + if (parsed.prefixLen >= 48) { + const key = parsed.net >> BigInt(parsed.prefixLen - 48) + const list = v48Index.get(key) + if (list) list.push(row) + else v48Index.set(key, [row]) + } else wideV6Index.push(row) + return + } const parsed = parseCidrV4(entry.prefix) if (!parsed) return const row: RipeIndexed = { @@ -269,13 +305,29 @@ function negative(prefix: string): FlowIpMeta { } } +function hostPrefix(ip: string): string { + return `${ip}/${ip.includes(":") ? 128 : 32}` +} + export function lookupRipeCached(ip: string): FlowIpMeta | null { loadSqlite() const trimmed = canonicalIp(ip) lastCandidateCount = 0 if (!trimmed) return null if (isNonPublicIp(trimmed)) { - return negative(`${trimmed.includes(":") ? trimmed : trimmed}/32`) + return negative(hostPrefix(trimmed)) + } + if (trimmed.includes(":")) { + const addr = ipv6ToInt(trimmed) + if (addr === null) return null + const bucket = v48Index.get(addr >> 80n) + const candidates = bucket ? bucket.concat(wideV6Index) : wideV6Index + lastCandidateCount = candidates.length + let best: RipeIndexedV6 | null = null + for (const row of candidates) { + if (isFresh(row.entry) && addr >> row.shift === row.net && (!best || row.prefixLen > best.prefixLen)) best = row + } + return best ? { ...best.entry, country: resolveRipeCountry(best.entry.country, best.entry.asn, best.entry.holder) || "—" } : null } const addr = ipv4ToInt(trimmed) if (addr == null) return null @@ -368,7 +420,7 @@ async function resolveIp(ip: string): Promise { recentFetches.push(Date.now()) try { const net = await ripeJson("network-info", ip) - const prefix = pickPrefix(net) || `${ip}/32` + const prefix = pickPrefix(net) || hostPrefix(ip) const existing = mem.get(prefix) if (existing && isFresh(existing)) return existing const asn = pickAsns(net) @@ -403,7 +455,7 @@ async function resolveIp(ip: string): Promise { persist(entry) return entry } catch { - const prefix = `${ip}/32` + const prefix = hostPrefix(ip) const entry = negative(prefix) remember(entry) persist(entry) @@ -451,7 +503,7 @@ export function enqueueRipeMisses(ips: Iterable): void { loadSqlite() for (const raw of ips) { if (queue.length >= MAX_QUEUE) break - const ip = String(raw ?? "").trim() + const ip = canonicalIp(raw) if (!ip || isNonPublicIp(ip)) continue if (lookupRipeCached(ip)) continue if (queued.has(ip) || inflight.has(ip)) continue diff --git a/docs/ux-ui-external-acceptance.md b/docs/ux-ui-external-acceptance.md index 52fbc568..379d4ff4 100644 --- a/docs/ux-ui-external-acceptance.md +++ b/docs/ux-ui-external-acceptance.md @@ -1,6 +1,6 @@ # UX/UI: протокол внешней приёмки 12.5 -Актуальное продолжение 2026-10-11: завершён12.5.141 — live traffic ждёт подтверждённый numeric ID своего backend-каталога.17 профильных E2E, ESLint/types, production Docker build и live1440/320 passed; initial srv1/400 устранён. Backend ux-ripe140/frontend ux-traffic141. Следующий блок — IPv6 special ranges и оставшаяся внешняя приёмка;12.5 открыт. +Актуальное продолжение 2026-10-11: завершён12.5.142 — исправлены специальные IPv6, mapped host identity и IPv6 RIPE-кеш с /128. Профильные проверки,6 SQL-модулей в собственной БД,984 compiled outputs и live RIPE двух IPv6 passed; runtime backend ux-ipv6142/frontend ux-traffic141. Далее143 — проверить неизвестные координаты RIPE, затем IPv6 flow pipeline.12.5 открыт. IPsec78: реальный PSK responder established, mode-config198.18.78.10 и online=true подтверждены API; точные собственные объекты удалены. Сертификатный/DN online, late failure и history restore остаются открытыми. [Отчёт](ux-ui/acceptance/ipsec-mode-config-2026-10-09.json), [журнал](ux-ui-progress.md). @@ -696,7 +696,7 @@ flowchart LR ## 12.5.77 — основной блок IPsec (2026-10-09) -Актуальное продолжение 2026-10-11: завершён12.5.141 — live traffic ждёт подтверждённый numeric ID своего backend-каталога.17 профильных E2E, ESLint/types, production Docker build и live1440/320 passed; initial srv1/400 устранён. Backend ux-ripe140/frontend ux-traffic141. Следующий блок — IPv6 special ranges и оставшаяся внешняя приёмка;12.5 открыт. +Актуальное продолжение 2026-10-11: завершён12.5.142 — исправлены специальные IPv6, mapped host identity и IPv6 RIPE-кеш с /128. Профильные проверки,6 SQL-модулей в собственной БД,984 compiled outputs и live RIPE двух IPv6 passed; runtime backend ux-ipv6142/frontend ux-traffic141. Далее143 — проверить неизвестные координаты RIPE, затем IPv6 flow pipeline.12.5 открыт. Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md). @@ -1519,3 +1519,15 @@ Readonly live traffic/statistics smoke320: rendering200/no overflow/pageerror0. 17 профильных E2E passed: два новых сценария удерживают запрос каталога и проверяют отсутствие любых individual requests до подтверждения, затем успех numeric ID или503 без запросов к серверу; остальные проверяют историю/ошибки/гонки периода/интерфейсов и SSE. Changed-file ESLint и source-only TypeScript passed; production Docker frontend build passed. Runtime frontend ux-traffic141, backend ux-ripe140 сохранён. Private rollback env и предыдущий образ оставлены. Readonly live /traffic1440 и320: каталог и numeric interfaces200, initial srv1/400 отсутствуют, pageerror0, horizontal overflow нет. [Протокол](ux-ui/acceptance/traffic-confirmed-catalog-2026-10-11.json). Это приёмка начальной загрузки с реальным текущим каталогом, а не полной цепочки NetFlow ingestion/throughput. Сетевые объекты и пароли не менялись. Следующий отдельный блок — IPv6 special registry guard; оставшаяся внешняя приёмка перечислена в матрице.12.5 открыт. + +## 12.5.142 — специальные IPv6 и рабочий кеш RIPE (2026-10-11) + +Исправлены две причины недостоверного обогащения IPv6: неполная проверка служебных адресов и отсутствие IPv6-поиска в RIPE-кеше. Теперь документальные/benchmark/discard/dummy/SRv6/local/multicast адреса и некорректные записи не запрашивают RIPE, даже при старом seeded metadata. Более конкретные глобальные назначения внутри2001::/23 сохранены; Teredo/6to4 со статусом N/A не объявлены non-global. Правила сверены с [IANA](https://www.iana.org/assignments/iana-ipv6-special-registry/), eligibility не гарантирует маршрутизацию. + +Добавлены validated128-bit parser, /48 buckets и отдельные широкие IPv6-префиксы. Lookup выбирает самый точный свежий CIDR, учитывает замену/просрочку/инвалидацию. Отрицательный кеш использует /128 и не затрагивает соседний адрес. Expanded и compressed записи дают одинаковый результат; IPv4-mapped hex/dotted/expanded формы нормализуются к одной IPv4 host identity. Host /128 удаляется перед RIPE resource request. Тест с3000 unrelated /48 подтверждает, что lookup не сканирует их все. Фиксированные special CIDR вычисляются один раз. + +Changed-file ESLint, backend production build и IP/RIPE profile tests прошли локально и в candidate. Потребители classification/destination/GeoIP passed. Первый local aggregate остановился на GeoIP из-за ограниченного доступа к temporary files; разрешённый отдельный rerun passed, причина и неуспешный запуск не скрыты. Шесть PG skips локального запуска не считаются успешными: все шесть SQL-модулей затем прошли одним запуском в собственной temporary DB, удалённой с owner check. Working DB и legacy SQLite не использовались fixtures. + +Первый Docker запуск указал отсутствующий Dockerfile.backend; исправлен путь backend/Dockerfile, build passed. Все984 emitted production outputs candidate совпали с local build. Реальный RIPE API дал6 HTTP200 для Google и Cloudflare IPv6; cached sibling/expanded address и special/invalid controls дали0 новых requests. Service process отдельный, persistence выключена. Backend ux-ipv6142 развёрнут, /health и Docker health healthy; frontend ux-traffic141 сохранён. Live /traffic1440/320 passed без srv1/400/pageerror/overflow. Rollback env/image сохранены, сетевые настройки и пароли не менялись. + +[SQL/live RIPE и границы](ux-ui/acceptance/flow-ipv6-ripe-2026-10-11.json), [browser smoke](ux-ui/acceptance/traffic-ipv6-smoke-2026-10-11.json). Это не доказывает IPv6 WAN connectivity или полный exporter→ingest→SQL→UI pipeline. При live RIPE выявлены координаты0/0 для Cloudflare; следующий143 — проверить, не подменяются ли отсутствующие значения нулями. Далее — IPv6 flow pipeline.12.5 открыт. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index bf287593..6078e511 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,12 +1,12 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-11: завершён12.5.141 — live traffic ждёт подтверждённый numeric ID своего backend-каталога.17 профильных E2E, ESLint/types, production Docker build и live1440/320 passed; initial srv1/400 устранён. Backend ux-ripe140/frontend ux-traffic141. Следующий блок — IPv6 special ranges и оставшаяся внешняя приёмка;12.5 открыт. +Актуальное продолжение 2026-10-11: завершён12.5.142 — исправлены специальные IPv6, mapped host identity и IPv6 RIPE-кеш с /128. Профильные проверки,6 SQL-модулей в собственной БД,984 compiled outputs и live RIPE двух IPv6 passed; runtime backend ux-ipv6142/frontend ux-traffic141. Далее143 — проверить неизвестные координаты RIPE, затем IPv6 flow pipeline.12.5 открыт. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-10. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. ## Актуальная точка продолжения — 2026-10-10 -Актуальное продолжение 2026-10-11: завершён12.5.141 — live traffic ждёт подтверждённый numeric ID своего backend-каталога.17 профильных E2E, ESLint/types, production Docker build и live1440/320 passed; initial srv1/400 устранён. Backend ux-ripe140/frontend ux-traffic141. Следующий блок — IPv6 special ranges и оставшаяся внешняя приёмка;12.5 открыт. +Актуальное продолжение 2026-10-11: завершён12.5.142 — исправлены специальные IPv6, mapped host identity и IPv6 RIPE-кеш с /128. Профильные проверки,6 SQL-модулей в собственной БД,984 compiled outputs и live RIPE двух IPv6 passed; runtime backend ux-ipv6142/frontend ux-traffic141. Далее143 — проверить неизвестные координаты RIPE, затем IPv6 flow pipeline.12.5 открыт. Этапы0–11 завершены в локальных границах журнала. История выполненных внешних блоков и доказательства сохранены в [журнале](ux-ui-stage-12.md) и [протоколе](ux-ui-external-acceptance.md). Реальная Telegram доставка, Portal SSO, multi-instance и оставшиеся профильные сетевые ограничения открыты. @@ -371,6 +371,6 @@ flowchart LR ## 12.5.77 — основной блок IPsec (2026-10-09) -Актуальное продолжение 2026-10-11: завершён12.5.141 — live traffic ждёт подтверждённый numeric ID своего backend-каталога.17 профильных E2E, ESLint/types, production Docker build и live1440/320 passed; initial srv1/400 устранён. Backend ux-ripe140/frontend ux-traffic141. Следующий блок — IPv6 special ranges и оставшаяся внешняя приёмка;12.5 открыт. +Актуальное продолжение 2026-10-11: завершён12.5.142 — исправлены специальные IPv6, mapped host identity и IPv6 RIPE-кеш с /128. Профильные проверки,6 SQL-модулей в собственной БД,984 compiled outputs и live RIPE двух IPv6 passed; runtime backend ux-ipv6142/frontend ux-traffic141. Далее143 — проверить неизвестные координаты RIPE, затем IPv6 flow pipeline.12.5 открыт. Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md). diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index a44b8e5e..f8aa7ce8 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-11: завершён12.5.141 — live traffic ждёт подтверждённый numeric ID своего backend-каталога.17 профильных E2E, ESLint/types, production Docker build и live1440/320 passed; initial srv1/400 устранён. Backend ux-ripe140/frontend ux-traffic141. Следующий блок — IPv6 special ranges и оставшаяся внешняя приёмка;12.5 открыт. +Актуальное продолжение 2026-10-11: завершён12.5.142 — исправлены специальные IPv6, mapped host identity и IPv6 RIPE-кеш с /128. Профильные проверки,6 SQL-модулей в собственной БД,984 compiled outputs и live RIPE двух IPv6 passed; runtime backend ux-ipv6142/frontend ux-traffic141. Далее143 — проверить неизвестные координаты RIPE, затем IPv6 flow pipeline.12.5 открыт. Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md). @@ -36,7 +36,19 @@ | 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | -| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–141 и ограничения ниже. Runtime backend ux-ripe140/frontend ux-traffic141. | +| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–142 и ограничения ниже. Runtime backend ux-ipv6142/frontend ux-traffic141. | + +## 12.5.142 — специальные IPv6 и рабочий кеш RIPE (2026-10-11) + +Исправлены две причины недостоверного обогащения IPv6: неполная проверка служебных адресов и отсутствие IPv6-поиска в RIPE-кеше. Теперь документальные/benchmark/discard/dummy/SRv6/local/multicast адреса и некорректные записи не запрашивают RIPE, даже при старом seeded metadata. Более конкретные глобальные назначения внутри2001::/23 сохранены; Teredo/6to4 со статусом N/A не объявлены non-global. Правила сверены с [IANA](https://www.iana.org/assignments/iana-ipv6-special-registry/), eligibility не гарантирует маршрутизацию. + +Добавлены validated128-bit parser, /48 buckets и отдельные широкие IPv6-префиксы. Lookup выбирает самый точный свежий CIDR, учитывает замену/просрочку/инвалидацию. Отрицательный кеш использует /128 и не затрагивает соседний адрес. Expanded и compressed записи дают одинаковый результат; IPv4-mapped hex/dotted/expanded формы нормализуются к одной IPv4 host identity. Host /128 удаляется перед RIPE resource request. Тест с3000 unrelated /48 подтверждает, что lookup не сканирует их все. Фиксированные special CIDR вычисляются один раз. + +Changed-file ESLint, backend production build и IP/RIPE profile tests прошли локально и в candidate. Потребители classification/destination/GeoIP passed. Первый local aggregate остановился на GeoIP из-за ограниченного доступа к temporary files; разрешённый отдельный rerun passed, причина и неуспешный запуск не скрыты. Шесть PG skips локального запуска не считаются успешными: все шесть SQL-модулей затем прошли одним запуском в собственной temporary DB, удалённой с owner check. Working DB и legacy SQLite не использовались fixtures. + +Первый Docker запуск указал отсутствующий Dockerfile.backend; исправлен путь backend/Dockerfile, build passed. Все984 emitted production outputs candidate совпали с local build. Реальный RIPE API дал6 HTTP200 для Google и Cloudflare IPv6; cached sibling/expanded address и special/invalid controls дали0 новых requests. Service process отдельный, persistence выключена. Backend ux-ipv6142 развёрнут, /health и Docker health healthy; frontend ux-traffic141 сохранён. Live /traffic1440/320 passed без srv1/400/pageerror/overflow. Rollback env/image сохранены, сетевые настройки и пароли не менялись. + +[SQL/live RIPE и границы](ux-ui/acceptance/flow-ipv6-ripe-2026-10-11.json), [browser smoke](ux-ui/acceptance/traffic-ipv6-smoke-2026-10-11.json). Это не доказывает IPv6 WAN connectivity или полный exporter→ingest→SQL→UI pipeline. При live RIPE выявлены координаты0/0 для Cloudflare; следующий143 — проверить, не подменяются ли отсутствующие значения нулями. Далее — IPv6 flow pipeline.12.5 открыт. ## 12.5.141 — подтверждённый каталог перед запросами трафика (2026-10-11) diff --git a/docs/ux-ui-remaining-acceptance.md b/docs/ux-ui-remaining-acceptance.md index 25955dbd..923b94a9 100644 --- a/docs/ux-ui-remaining-acceptance.md +++ b/docs/ux-ui-remaining-acceptance.md @@ -1,8 +1,8 @@ # Оставшаяся внешняя приёмка — 2026-10-11 -Это актуальная матрица после 12.5.141. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии. +Это актуальная матрица после 12.5.142. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии. -Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-ripe140`, frontend `ux-traffic141`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись. +Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-ipv6142`, frontend `ux-traffic141`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись. | Раздел | Подтверждено | Осталось / условие завершения | | --- | --- | --- | @@ -18,7 +18,7 @@ | Recursive routes | Production PUT/change/own history restore88, disabled guards, partial/unknown/applied outcomes, live формы/контекст | Автоматический rollback/retry, durable receipts и несколько экземпляров отсутствуют; PUT не является idempotent noop. Полная произвольная legacy catalog миграция не принята | | Diagnostics/terminal | Allowlist/native reads, Ping/trace/route/MTU/DNS, actual avg-rtt, disabled guards, JWT/live PC/mobile89; real default memory tail/log limits/API/UI133 | DNS использует backend resolver; MTU — ограниченная оценка, не гарантированный максимум. Stop ожидания не доказывает остановку RouterOS команды; arbitrary CLI и physical disconnect не приняты | | Portal/интеграции | Локальные и isolated JWT guards; UI/API error states в границах журналов | Настоящий Portal issuer/login/отзыв прав; RIPE SDK two-IP/no-fetch passed140, полный flow pipeline/local licensed MaxMind/S3/container/certificate jobs и Gitea delivery требуют отдельных тестовых endpoints/аккаунтов. Не отправлять сообщения и не публиковать релиз без соответствующего поручения | -| Общая приёмка | Локальная12.3.83; UI1244/context221/compiled984 parity139; профильные build/E2E/live проверки последующих блоков | Traffic initial live mock-id gate passed141; IPv6 special ranges; последующие изменения проверять отдельно; scoped axe не означает полную WCAG сертификацию | +| Общая приёмка | Локальная12.3.83; UI1244/context221/compiled984 parity139; профильные build/E2E/live проверки последующих блоков | Traffic initial live mock-id gate passed141; IPv6 special ranges/cache passed142; unknown RIPE coordinates143; последующие изменения проверять отдельно; scoped axe не означает полную WCAG сертификацию | Доказательства и точные ограничения: [полная история этапа12](ux-ui-stage-12.md), [внешний протокол](ux-ui-external-acceptance.md), [BGP93](ux-ui/acceptance/bgp-failover-2026-10-10.json), [паспорт стенда](ux-ui-test-server.md). Невыполненные пункты не являются все блокерами данного лабораторного сценария: для выпуска необходимо явно определить требуемые конфигурации и принять их либо согласовать ограничение. @@ -434,3 +434,15 @@ Readonly live traffic/statistics smoke320: rendering200/no overflow/pageerror0. 17 профильных E2E passed: два новых сценария удерживают запрос каталога и проверяют отсутствие любых individual requests до подтверждения, затем успех numeric ID или503 без запросов к серверу; остальные проверяют историю/ошибки/гонки периода/интерфейсов и SSE. Changed-file ESLint и source-only TypeScript passed; production Docker frontend build passed. Runtime frontend ux-traffic141, backend ux-ripe140 сохранён. Private rollback env и предыдущий образ оставлены. Readonly live /traffic1440 и320: каталог и numeric interfaces200, initial srv1/400 отсутствуют, pageerror0, horizontal overflow нет. [Протокол](ux-ui/acceptance/traffic-confirmed-catalog-2026-10-11.json). Это приёмка начальной загрузки с реальным текущим каталогом, а не полной цепочки NetFlow ingestion/throughput. Сетевые объекты и пароли не менялись. Следующий отдельный блок — IPv6 special registry guard; оставшаяся внешняя приёмка перечислена в матрице.12.5 открыт. + +## 12.5.142 — специальные IPv6 и рабочий кеш RIPE (2026-10-11) + +Исправлены две причины недостоверного обогащения IPv6: неполная проверка служебных адресов и отсутствие IPv6-поиска в RIPE-кеше. Теперь документальные/benchmark/discard/dummy/SRv6/local/multicast адреса и некорректные записи не запрашивают RIPE, даже при старом seeded metadata. Более конкретные глобальные назначения внутри2001::/23 сохранены; Teredo/6to4 со статусом N/A не объявлены non-global. Правила сверены с [IANA](https://www.iana.org/assignments/iana-ipv6-special-registry/), eligibility не гарантирует маршрутизацию. + +Добавлены validated128-bit parser, /48 buckets и отдельные широкие IPv6-префиксы. Lookup выбирает самый точный свежий CIDR, учитывает замену/просрочку/инвалидацию. Отрицательный кеш использует /128 и не затрагивает соседний адрес. Expanded и compressed записи дают одинаковый результат; IPv4-mapped hex/dotted/expanded формы нормализуются к одной IPv4 host identity. Host /128 удаляется перед RIPE resource request. Тест с3000 unrelated /48 подтверждает, что lookup не сканирует их все. Фиксированные special CIDR вычисляются один раз. + +Changed-file ESLint, backend production build и IP/RIPE profile tests прошли локально и в candidate. Потребители classification/destination/GeoIP passed. Первый local aggregate остановился на GeoIP из-за ограниченного доступа к temporary files; разрешённый отдельный rerun passed, причина и неуспешный запуск не скрыты. Шесть PG skips локального запуска не считаются успешными: все шесть SQL-модулей затем прошли одним запуском в собственной temporary DB, удалённой с owner check. Working DB и legacy SQLite не использовались fixtures. + +Первый Docker запуск указал отсутствующий Dockerfile.backend; исправлен путь backend/Dockerfile, build passed. Все984 emitted production outputs candidate совпали с local build. Реальный RIPE API дал6 HTTP200 для Google и Cloudflare IPv6; cached sibling/expanded address и special/invalid controls дали0 новых requests. Service process отдельный, persistence выключена. Backend ux-ipv6142 развёрнут, /health и Docker health healthy; frontend ux-traffic141 сохранён. Live /traffic1440/320 passed без srv1/400/pageerror/overflow. Rollback env/image сохранены, сетевые настройки и пароли не менялись. + +[SQL/live RIPE и границы](ux-ui/acceptance/flow-ipv6-ripe-2026-10-11.json), [browser smoke](ux-ui/acceptance/traffic-ipv6-smoke-2026-10-11.json). Это не доказывает IPv6 WAN connectivity или полный exporter→ingest→SQL→UI pipeline. При live RIPE выявлены координаты0/0 для Cloudflare; следующий143 — проверить, не подменяются ли отсутствующие значения нулями. Далее — IPv6 flow pipeline.12.5 открыт. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index 75bff3ff..cb9c99d5 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -1,6 +1,6 @@ # Этап 12: итоговая приёмка UX/UI -Актуальное продолжение 2026-10-11: завершён12.5.141 — live traffic ждёт подтверждённый numeric ID своего backend-каталога.17 профильных E2E, ESLint/types, production Docker build и live1440/320 passed; initial srv1/400 устранён. Backend ux-ripe140/frontend ux-traffic141. Следующий блок — IPv6 special ranges и оставшаяся внешняя приёмка;12.5 открыт. +Актуальное продолжение 2026-10-11: завершён12.5.142 — исправлены специальные IPv6, mapped host identity и IPv6 RIPE-кеш с /128. Профильные проверки,6 SQL-модулей в собственной БД,984 compiled outputs и live RIPE двух IPv6 passed; runtime backend ux-ipv6142/frontend ux-traffic141. Далее143 — проверить неизвестные координаты RIPE, затем IPv6 flow pipeline.12.5 открыт. План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push. @@ -2824,3 +2824,15 @@ Readonly live traffic/statistics smoke320: rendering200/no overflow/pageerror0. 17 профильных E2E passed: два новых сценария удерживают запрос каталога и проверяют отсутствие любых individual requests до подтверждения, затем успех numeric ID или503 без запросов к серверу; остальные проверяют историю/ошибки/гонки периода/интерфейсов и SSE. Changed-file ESLint и source-only TypeScript passed; production Docker frontend build passed. Runtime frontend ux-traffic141, backend ux-ripe140 сохранён. Private rollback env и предыдущий образ оставлены. Readonly live /traffic1440 и320: каталог и numeric interfaces200, initial srv1/400 отсутствуют, pageerror0, horizontal overflow нет. [Протокол](ux-ui/acceptance/traffic-confirmed-catalog-2026-10-11.json). Это приёмка начальной загрузки с реальным текущим каталогом, а не полной цепочки NetFlow ingestion/throughput. Сетевые объекты и пароли не менялись. Следующий отдельный блок — IPv6 special registry guard; оставшаяся внешняя приёмка перечислена в матрице.12.5 открыт. + +## 12.5.142 — специальные IPv6 и рабочий кеш RIPE (2026-10-11) + +Исправлены две причины недостоверного обогащения IPv6: неполная проверка служебных адресов и отсутствие IPv6-поиска в RIPE-кеше. Теперь документальные/benchmark/discard/dummy/SRv6/local/multicast адреса и некорректные записи не запрашивают RIPE, даже при старом seeded metadata. Более конкретные глобальные назначения внутри2001::/23 сохранены; Teredo/6to4 со статусом N/A не объявлены non-global. Правила сверены с [IANA](https://www.iana.org/assignments/iana-ipv6-special-registry/), eligibility не гарантирует маршрутизацию. + +Добавлены validated128-bit parser, /48 buckets и отдельные широкие IPv6-префиксы. Lookup выбирает самый точный свежий CIDR, учитывает замену/просрочку/инвалидацию. Отрицательный кеш использует /128 и не затрагивает соседний адрес. Expanded и compressed записи дают одинаковый результат; IPv4-mapped hex/dotted/expanded формы нормализуются к одной IPv4 host identity. Host /128 удаляется перед RIPE resource request. Тест с3000 unrelated /48 подтверждает, что lookup не сканирует их все. Фиксированные special CIDR вычисляются один раз. + +Changed-file ESLint, backend production build и IP/RIPE profile tests прошли локально и в candidate. Потребители classification/destination/GeoIP passed. Первый local aggregate остановился на GeoIP из-за ограниченного доступа к temporary files; разрешённый отдельный rerun passed, причина и неуспешный запуск не скрыты. Шесть PG skips локального запуска не считаются успешными: все шесть SQL-модулей затем прошли одним запуском в собственной temporary DB, удалённой с owner check. Working DB и legacy SQLite не использовались fixtures. + +Первый Docker запуск указал отсутствующий Dockerfile.backend; исправлен путь backend/Dockerfile, build passed. Все984 emitted production outputs candidate совпали с local build. Реальный RIPE API дал6 HTTP200 для Google и Cloudflare IPv6; cached sibling/expanded address и special/invalid controls дали0 новых requests. Service process отдельный, persistence выключена. Backend ux-ipv6142 развёрнут, /health и Docker health healthy; frontend ux-traffic141 сохранён. Live /traffic1440/320 passed без srv1/400/pageerror/overflow. Rollback env/image сохранены, сетевые настройки и пароли не менялись. + +[SQL/live RIPE и границы](ux-ui/acceptance/flow-ipv6-ripe-2026-10-11.json), [browser smoke](ux-ui/acceptance/traffic-ipv6-smoke-2026-10-11.json). Это не доказывает IPv6 WAN connectivity или полный exporter→ingest→SQL→UI pipeline. При live RIPE выявлены координаты0/0 для Cloudflare; следующий143 — проверить, не подменяются ли отсутствующие значения нулями. Далее — IPv6 flow pipeline.12.5 открыт. diff --git a/docs/ux-ui-test-server.md b/docs/ux-ui-test-server.md index 587025be..7b5d9e46 100644 --- a/docs/ux-ui-test-server.md +++ b/docs/ux-ui-test-server.md @@ -1,6 +1,6 @@ # Тестовый сервер UX/UI — 2026-10-10 -Сервер161.104.46.222: завершён12.5.141 — live traffic ждёт подтверждённый numeric ID своего backend-каталога.17 профильных E2E, ESLint/types, production Docker build и live1440/320 passed; initial srv1/400 устранён. Backend ux-ripe140/frontend ux-traffic141. Следующий блок — IPv6 special ranges и оставшаяся внешняя приёмка;12.5 открыт. +Сервер161.104.46.222: завершён12.5.142 — исправлены специальные IPv6, mapped host identity и IPv6 RIPE-кеш с /128. Профильные проверки,6 SQL-модулей в собственной БД,984 compiled outputs и live RIPE двух IPv6 passed; runtime backend ux-ipv6142/frontend ux-traffic141. Далее143 — проверить неизвестные координаты RIPE, затем IPv6 flow pipeline.12.5 открыт. | Параметр | Проверенное состояние | | --- | --- | @@ -14,7 +14,7 @@ | PostgreSQL | postgres:18-alpine,18.6;mmapp-test-postgres healthy;SELECT version() passed | | Данные | Отдельный Compose project mmapp-test,том mmapp-test_mmapp-pgdata и каталог ./data | | Доступ | PostgreSQL не публикуется;frontend3000/backend8000 привязаны к127.0.0.1 после запуска | -| Образы приложения | mmapp-test/backend:ux-ripe140 и mmapp-test/frontend:ux-traffic141; запущены | +| Образы приложения | mmapp-test/backend:ux-ipv6142 и mmapp-test/frontend:ux-traffic141; запущены | Compose SHA256:3052bd6ab5d9865f97605cebfb5ff27a4410cd3357b81153a65a87fb7c92582f. OptionalBACKEND_IMAGE_TAG позволяетобновлятьbackendотдельно. Локальный и удалённый файлы совпали. Шаблоны: [Compose](../deploy/docker-compose.test.yml), [env без секретов](../deploy/env.test.example). @@ -1250,3 +1250,15 @@ Readonly live traffic/statistics smoke320: rendering200/no overflow/pageerror0. 17 профильных E2E passed: два новых сценария удерживают запрос каталога и проверяют отсутствие любых individual requests до подтверждения, затем успех numeric ID или503 без запросов к серверу; остальные проверяют историю/ошибки/гонки периода/интерфейсов и SSE. Changed-file ESLint и source-only TypeScript passed; production Docker frontend build passed. Runtime frontend ux-traffic141, backend ux-ripe140 сохранён. Private rollback env и предыдущий образ оставлены. Readonly live /traffic1440 и320: каталог и numeric interfaces200, initial srv1/400 отсутствуют, pageerror0, horizontal overflow нет. [Протокол](ux-ui/acceptance/traffic-confirmed-catalog-2026-10-11.json). Это приёмка начальной загрузки с реальным текущим каталогом, а не полной цепочки NetFlow ingestion/throughput. Сетевые объекты и пароли не менялись. Следующий отдельный блок — IPv6 special registry guard; оставшаяся внешняя приёмка перечислена в матрице.12.5 открыт. + +## 12.5.142 — специальные IPv6 и рабочий кеш RIPE (2026-10-11) + +Исправлены две причины недостоверного обогащения IPv6: неполная проверка служебных адресов и отсутствие IPv6-поиска в RIPE-кеше. Теперь документальные/benchmark/discard/dummy/SRv6/local/multicast адреса и некорректные записи не запрашивают RIPE, даже при старом seeded metadata. Более конкретные глобальные назначения внутри2001::/23 сохранены; Teredo/6to4 со статусом N/A не объявлены non-global. Правила сверены с [IANA](https://www.iana.org/assignments/iana-ipv6-special-registry/), eligibility не гарантирует маршрутизацию. + +Добавлены validated128-bit parser, /48 buckets и отдельные широкие IPv6-префиксы. Lookup выбирает самый точный свежий CIDR, учитывает замену/просрочку/инвалидацию. Отрицательный кеш использует /128 и не затрагивает соседний адрес. Expanded и compressed записи дают одинаковый результат; IPv4-mapped hex/dotted/expanded формы нормализуются к одной IPv4 host identity. Host /128 удаляется перед RIPE resource request. Тест с3000 unrelated /48 подтверждает, что lookup не сканирует их все. Фиксированные special CIDR вычисляются один раз. + +Changed-file ESLint, backend production build и IP/RIPE profile tests прошли локально и в candidate. Потребители classification/destination/GeoIP passed. Первый local aggregate остановился на GeoIP из-за ограниченного доступа к temporary files; разрешённый отдельный rerun passed, причина и неуспешный запуск не скрыты. Шесть PG skips локального запуска не считаются успешными: все шесть SQL-модулей затем прошли одним запуском в собственной temporary DB, удалённой с owner check. Working DB и legacy SQLite не использовались fixtures. + +Первый Docker запуск указал отсутствующий Dockerfile.backend; исправлен путь backend/Dockerfile, build passed. Все984 emitted production outputs candidate совпали с local build. Реальный RIPE API дал6 HTTP200 для Google и Cloudflare IPv6; cached sibling/expanded address и special/invalid controls дали0 новых requests. Service process отдельный, persistence выключена. Backend ux-ipv6142 развёрнут, /health и Docker health healthy; frontend ux-traffic141 сохранён. Live /traffic1440/320 passed без srv1/400/pageerror/overflow. Rollback env/image сохранены, сетевые настройки и пароли не менялись. + +[SQL/live RIPE и границы](ux-ui/acceptance/flow-ipv6-ripe-2026-10-11.json), [browser smoke](ux-ui/acceptance/traffic-ipv6-smoke-2026-10-11.json). Это не доказывает IPv6 WAN connectivity или полный exporter→ingest→SQL→UI pipeline. При live RIPE выявлены координаты0/0 для Cloudflare; следующий143 — проверить, не подменяются ли отсутствующие значения нулями. Далее — IPv6 flow pipeline.12.5 открыт. diff --git a/docs/ux-ui/acceptance/flow-ipv6-ripe-2026-10-11.json b/docs/ux-ui/acceptance/flow-ipv6-ripe-2026-10-11.json new file mode 100644 index 00000000..92de6d58 --- /dev/null +++ b/docs/ux-ui/acceptance/flow-ipv6-ripe-2026-10-11.json @@ -0,0 +1,131 @@ +{ + "date": "2026-10-11", + "block": "12.5.142", + "runtime": { + "backend": "ux-ipv6142", + "frontend": "ux-traffic141", + "backendHealthy": true + }, + "checks": { + "ipAndRipeProfilePassedInCandidate": true, + "changedFilesEslintPassed": true, + "backendProductionBuildPassed": true, + "compiledOutputsMatched": 984, + "isolatedSqlModulesPassed": 6, + "ownTemporaryDatabaseRemoved": true, + "classificationAndGeoipConsumersPassed": true + }, + "sql": { + "date": "2026-10-11", + "name": "mm_accept_142_1791710322057", + "tests": [ + { + "module": "traffic-flow-ingest", + "passed": true, + "output": "traffic-flow-ingest.test.ts: ok\nISOLATED_SQL_TEST_COMPLETE\n" + }, + { + "module": "traffic-flow-analytics", + "passed": true, + "output": "traffic-flow-analytics.test.ts: ok\nISOLATED_SQL_TEST_COMPLETE\n" + }, + { + "module": "traffic-flow-map-hops", + "passed": true, + "output": "traffic-flow-map-hops.test.ts: pickMapServices ok\ntraffic-flow-map-hops.test.ts: hops ok\ntraffic-flow-map-hops.test.ts: ok\nISOLATED_SQL_TEST_COMPLETE\n" + }, + { + "module": "traffic-flow-purge", + "passed": true, + "output": "traffic-flow-purge.test.ts: ok\nISOLATED_SQL_TEST_COMPLETE\n" + }, + { + "module": "traffic-flow-facts-rebuild", + "passed": true, + "output": "traffic-flow-facts-rebuild.test.ts: ok\nISOLATED_SQL_TEST_COMPLETE\n" + }, + { + "module": "statistics-aggregate", + "passed": true, + "output": "statistics-aggregate.test.ts: ok\nISOLATED_SQL_TEST_COMPLETE\n" + } + ], + "databaseRemoved": true + }, + "liveRipe": { + "date": "2026-10-11", + "runtime": "ux-ipv6142", + "requests": [ + { + "path": "/data/network-info/data.json", + "resource": "2001:4860:4860::8888", + "status": 200 + }, + { + "path": "/data/maxmind-geo-lite/data.json", + "resource": "2001:4860::/32", + "status": 200 + }, + { + "path": "/data/as-overview/data.json", + "resource": "AS15169", + "status": 200 + }, + { + "path": "/data/network-info/data.json", + "resource": "2606:4700:4700::1111", + "status": 200 + }, + { + "path": "/data/maxmind-geo-lite/data.json", + "resource": "2606:4700:4700::/48", + "status": 200 + }, + { + "path": "/data/as-overview/data.json", + "resource": "AS13335", + "status": 200 + } + ], + "cases": [ + { + "ip": "2001:4860:4860::8888", + "prefix": "2001:4860::/32", + "asn": 15169, + "country": "US", + "lat": 37.09024, + "lng": -95.712891, + "holder": "GOOGLE - Google LLC", + "ok": true, + "fetchedAt": 1791710348415 + }, + { + "ip": "2606:4700:4700::1111", + "prefix": "2606:4700:4700::/48", + "asn": 13335, + "country": "US", + "lat": 0, + "lng": 0, + "holder": "CLOUDFLARENET - Cloudflare, Inc.", + "ok": true, + "fetchedAt": 1791710348902 + } + ], + "cacheHitsAndSpecialNoFetch": true, + "success": true + }, + "bounds": [ + "IANA IPv6 non-global special assignments, deprecated site-local and multicast excluded; more-specific global allocations preserved, Teredo and 6to4 N/A reachability not equated to non-global", + "Fixed prefixes parsed once; validated 128-bit parsing, IPv4-mapped identity normalized consistently", + "IPv6 /48 bucket and wide-prefix indexes support longest fresh match, replacement, invalidation, negative /128 and expanded forms; unrelated3000 /48 prefixes not scanned", + "First local aggregate run stopped on sandbox-limited GeoIP temp files; isolated authorized rerun passed; six local PG skips not counted as passes", + "Candidate SQL run passed all six modules in one own DB, owner-checked removal; no working DB fixture changes", + "First Docker invocation used wrong Dockerfile path and failed before build; corrected backend/Dockerfile build passed", + "Six actual RIPE200 requests for two public IPv6; cached sibling/expanded addresses and special controls produced zero additional requests; persistence disabled in separate service process", + "Public IPv6 metadata verified over backend HTTPS API egress; no IPv6 WAN connectivity, live IPv6 exporter ingestion or maximum throughput claim", + "GeoIP zero coordinates observed from upstream/service need separate validation; full external acceptance remains open" + ], + "references": [ + "https://www.iana.org/assignments/iana-ipv6-special-registry/" + ] +} diff --git a/docs/ux-ui/acceptance/traffic-ipv6-smoke-2026-10-11.json b/docs/ux-ui/acceptance/traffic-ipv6-smoke-2026-10-11.json new file mode 100644 index 00000000..04b10d20 --- /dev/null +++ b/docs/ux-ui/acceptance/traffic-ipv6-smoke-2026-10-11.json @@ -0,0 +1,84 @@ +{ + "date": "2026-10-11", + "block": "12.5.142", + "runtime": { + "backend": "ux-ipv6142", + "frontend": "ux-traffic141" + }, + "checks": { + "readOnlyFrontendUnchanged": true, + "backendHealthy": true, + "backendProductionBuildPassed": true + }, + "cases": [ + { + "width": 1440, + "httpStatus": 200, + "api": [ + { + "path": "/api/traffic/bound-interfaces", + "status": 200 + }, + { + "path": "/api/traffic/users", + "status": 200 + }, + { + "path": "/api/traffic/servers", + "status": 200 + }, + { + "path": "/api/traffic/servers/1/live", + "status": 200 + }, + { + "path": "/api/traffic/servers/1/interfaces", + "status": 200 + }, + { + "path": "/api/traffic/servers/1", + "status": 200 + } + ], + "pageErrors": [], + "noOverflow": true + }, + { + "width": 320, + "httpStatus": 200, + "api": [ + { + "path": "/api/traffic/bound-interfaces", + "status": 200 + }, + { + "path": "/api/traffic/users", + "status": 200 + }, + { + "path": "/api/traffic/servers", + "status": 200 + }, + { + "path": "/api/traffic/servers/1/live", + "status": 200 + }, + { + "path": "/api/traffic/servers/1/interfaces", + "status": 200 + }, + { + "path": "/api/traffic/servers/1", + "status": 200 + } + ], + "pageErrors": [], + "noOverflow": true + } + ], + "bounds": [ + "Read-only initial loading with real current lab catalog; no full NetFlow ingestion or throughput claim", + "Catalog failure and pending loading checked by controlled E2E; numeric live server details/interfaces/SSE enabled only after confirmed catalog ownership", + "Interface filter remains selected on range changes; backend source switch invalidates prior catalog ownership" + ] +}