fix(netflow): исправить классификацию IPv6 и кеш префиксов RIPE
This commit is contained in:
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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)")
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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 открыт.
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user