fix(netflow): исправить классификацию IPv6 и кеш префиксов RIPE

This commit is contained in:
Denozordec committed 2026-10-11 16:21:43 +07:00
1 parent 59938118bd
commit af66faaae4
12 files changed
+493 -23

No files matched your search

@@ -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",
+60 -4
View File
@@ -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<string>(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
+64 -1
View File
@@ -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)")
+57 -5
View File
@@ -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<number, RipeIndexed[]>()
const wideIndex: RipeIndexed[] = []
interface RipeIndexedV6 {
entry: FlowIpMeta
net: bigint
shift: bigint
prefixLen: number
}
const v48Index = new Map<bigint, RipeIndexedV6[]>()
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<FlowIpMeta | null> {
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<FlowIpMeta | null> {
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<string>): 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
+14 -2
View File
@@ -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 открыт.
+3 -3
View File
@@ -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).
+14 -2
View File
@@ -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)
+15 -3
View File
@@ -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 открыт.
+13 -1
View File
@@ -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 открыт.
+14 -2
View File
@@ -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 открыт.
@@ -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/"
]
}
@@ -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"
]
}