fix(traffic): подтверждать идентичность соседних счётчиков
This commit is contained in:
1 parent
493c30bebc
commit
b57ae316ba
27 files changed
+1444
-36
No files matched your search
@@ -310,6 +310,7 @@ export const flowAsnMeta = pgTable("flow_asn_meta", {
|
||||
})
|
||||
|
||||
export const trafficSamples = pgTable("traffic_samples", {
|
||||
nativeCounterIdentity: text("native_counter_identity"),
|
||||
serverId: intPkRef().references(() => servers.id, { onDelete: "cascade" }),
|
||||
interfaceName: text("interface_name").notNull(),
|
||||
peerPublicKey: text("peer_public_key").notNull().default(""),
|
||||
|
||||
@@ -2,11 +2,19 @@ export const TRAFFIC_FLAG_RUNNING = 1
|
||||
export const TRAFFIC_FLAG_DISABLED = 2
|
||||
export const TRAFFIC_FLAG_COUNTERS_KNOWN = 4
|
||||
export const TRAFFIC_FLAG_STATUS_KNOWN = 8
|
||||
export const TRAFFIC_FLAG_CONTINUITY_KNOWN = 16
|
||||
export const TRAFFIC_FLAG_CONTINUITY_RECORDED = 32
|
||||
|
||||
export function encodeTrafficFlags(running: boolean, disabled: boolean, countersKnown = false, statusKnown = false): number {
|
||||
export function encodeTrafficFlags(running: boolean, disabled: boolean, countersKnown = false, statusKnown = false, continuityKnown?: boolean): number {
|
||||
return (running ? TRAFFIC_FLAG_RUNNING : 0) | (disabled ? TRAFFIC_FLAG_DISABLED : 0)
|
||||
| (countersKnown ? TRAFFIC_FLAG_COUNTERS_KNOWN : 0)
|
||||
| (statusKnown ? TRAFFIC_FLAG_STATUS_KNOWN : 0)
|
||||
| (continuityKnown === undefined ? 0 : TRAFFIC_FLAG_CONTINUITY_RECORDED | (continuityKnown ? TRAFFIC_FLAG_CONTINUITY_KNOWN : 0))
|
||||
}
|
||||
|
||||
/** Undefined retains the documented legacy boundary; new producers explicitly mark both outcomes. */
|
||||
export function trafficCounterContinuity(flags: number): boolean | undefined {
|
||||
return flags & TRAFFIC_FLAG_CONTINUITY_RECORDED ? !!(flags & TRAFFIC_FLAG_CONTINUITY_KNOWN) : undefined
|
||||
}
|
||||
|
||||
export function trafficFlagRunning(flags: number | null | undefined): boolean {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { and, asc, desc, eq, gte } from "drizzle-orm"
|
||||
import { db } from "../db/index.js"
|
||||
import { servers, trafficSamples, trafficSettings } from "../db/schema.js"
|
||||
import { decodeTrafficSampleFlags, encodeTrafficFlags } from "../db/traffic-flags.js"
|
||||
import { decodeTrafficSampleFlags, encodeTrafficFlags, trafficCounterContinuity } from "../db/traffic-flags.js"
|
||||
import type { TrafficRunSnapshot } from "../types/scheduler-run-snapshot.js"
|
||||
import { SCHEDULER_RUN_SNAPSHOT_VERSION } from "../types/scheduler-run-snapshot.js"
|
||||
import { MikrotikClient } from "./mikrotik.js"
|
||||
import { bpsToMbps, shouldIncludeIface } from "./traffic-rate.js"
|
||||
import { observedTrafficRate, parseTrafficCounter, parseTrafficFlag } from "./traffic-observation.js"
|
||||
import { observedTrafficRate, parseTrafficCounter, parseTrafficFlag, nativeTrafficCounterIdentity } from "./traffic-observation.js"
|
||||
import { rememberServerIfaces } from "./traffic-flow-ifindex.js"
|
||||
import { recordTrafficCollectionAttempt } from "./traffic-collection-state.js"
|
||||
import { parseTrafficPeers } from "./traffic-peer-observation.js"
|
||||
@@ -38,14 +38,15 @@ function waveKey(interfaceName: string, peerPublicKey = ""): string {
|
||||
}
|
||||
|
||||
function sampleRate(
|
||||
prevWave: Map<string, { rxBytes: number; txBytes: number; sampledAt: string }>,
|
||||
prevWave: Map<string, { rxBytes: number; txBytes: number; sampledAt: string; nativeCounterIdentity: string|null }>,
|
||||
key: string,
|
||||
rxBytes: number,
|
||||
txBytes: number,
|
||||
nowMs: number,
|
||||
maxIntervalMs: number,
|
||||
identity: string|null,
|
||||
): { rxBps: number; txBps: number; measured: boolean } {
|
||||
return observedTrafficRate(prevWave.get(key), rxBytes, txBytes, nowMs, maxIntervalMs)
|
||||
return observedTrafficRate(identity !== null && prevWave.get(key)?.nativeCounterIdentity === identity ? prevWave.get(key) : undefined, rxBytes, txBytes, nowMs, maxIntervalMs)
|
||||
}
|
||||
|
||||
export interface TrafficCollectorState {
|
||||
@@ -75,7 +76,7 @@ async function getSettingsRow() {
|
||||
return (await db.select().from(trafficSettings).where(eq(trafficSettings.id, 1)).limit(1))[0]
|
||||
}
|
||||
|
||||
async function readPreviousWave(serverId: number): Promise<Map<string, { rxBytes: number; txBytes: number; sampledAt: string }>> {
|
||||
async function readPreviousWave(serverId: number): Promise<Map<string, { rxBytes: number; txBytes: number; sampledAt: string; nativeCounterIdentity: string|null }>> {
|
||||
const last = (await db
|
||||
.select({ sampledAt: trafficSamples.sampledAt })
|
||||
.from(trafficSamples)
|
||||
@@ -90,6 +91,7 @@ async function readPreviousWave(serverId: number): Promise<Map<string, { rxBytes
|
||||
rxBytes: trafficSamples.rxBytes,
|
||||
txBytes: trafficSamples.txBytes,
|
||||
flags: trafficSamples.flags,
|
||||
nativeCounterIdentity: trafficSamples.nativeCounterIdentity,
|
||||
sampledAt: trafficSamples.sampledAt,
|
||||
})
|
||||
.from(trafficSamples)
|
||||
@@ -144,11 +146,13 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
|
||||
let totalsMeasured = true
|
||||
const rows = ifaces.map((i) => {
|
||||
const interfaceName = i.name!
|
||||
const nativeCounterIdentity = nativeTrafficCounterIdentity(srv.updatedAt,i[".id"])
|
||||
const continuityKnown = nativeCounterIdentity !== null && prevWave.get(waveKey(interfaceName))?.nativeCounterIdentity === nativeCounterIdentity
|
||||
const rxBytes = toNum(i["rx-byte"])
|
||||
const txBytes = toNum(i["tx-byte"])
|
||||
const running = parseTrafficFlag(i.running)
|
||||
const disabled = parseTrafficFlag(i.disabled)
|
||||
const { rxBps, txBps, measured } = sampleRate(prevWave, waveKey(interfaceName), rxBytes, txBytes, nowMs, maxIntervalMs)
|
||||
const { rxBps, txBps, measured } = sampleRate(prevWave, waveKey(interfaceName), rxBytes, txBytes, nowMs, maxIntervalMs, nativeCounterIdentity)
|
||||
if (shouldIncludeIface(interfaceName, running, disabled)) {
|
||||
includedInterfaces++
|
||||
totalsMeasured &&= measured
|
||||
@@ -164,7 +168,8 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
|
||||
txBytes,
|
||||
rxBps: Math.round(rxBps),
|
||||
txBps: Math.round(txBps),
|
||||
flags: encodeTrafficFlags(running, disabled, true, true),
|
||||
flags: encodeTrafficFlags(running, disabled, true, true, continuityKnown),
|
||||
nativeCounterIdentity,
|
||||
}
|
||||
})
|
||||
let peerWarning: string | undefined
|
||||
@@ -172,6 +177,8 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
|
||||
const peers = await client.get<RosWgPeerTraffic[]>("/interface/wireguard/peers")
|
||||
for (const p of parseTrafficPeers(peers)) {
|
||||
const { interfaceName, peerPublicKey, rxBytes, txBytes, disabled } = p
|
||||
const nativeCounterIdentity = nativeTrafficCounterIdentity(srv.updatedAt,p.nativeId)
|
||||
const continuityKnown = nativeCounterIdentity !== null && prevWave.get(waveKey(interfaceName,peerPublicKey))?.nativeCounterIdentity === nativeCounterIdentity
|
||||
const running = !disabled
|
||||
const { rxBps, txBps } = sampleRate(
|
||||
prevWave,
|
||||
@@ -180,6 +187,7 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
|
||||
txBytes,
|
||||
nowMs,
|
||||
maxIntervalMs,
|
||||
nativeCounterIdentity,
|
||||
)
|
||||
rows.push({
|
||||
serverId: srv.id,
|
||||
@@ -190,7 +198,8 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
|
||||
txBytes,
|
||||
rxBps: Math.round(rxBps),
|
||||
txBps: Math.round(txBps),
|
||||
flags: encodeTrafficFlags(running, disabled, true, true),
|
||||
flags: encodeTrafficFlags(running, disabled, true, true, continuityKnown),
|
||||
nativeCounterIdentity,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -292,5 +301,5 @@ export async function readServerSamplesInRange(serverId: number, sinceIso: strin
|
||||
gte(trafficSamples.sampledAt, sinceIso),
|
||||
))
|
||||
.orderBy(asc(trafficSamples.sampledAt))
|
||||
return rows.map((r) => ({ ...r, ...decodeTrafficSampleFlags(r.flags) }))
|
||||
return rows.map((r) => ({ ...r, ...decodeTrafficSampleFlags(r.flags), continuityKnown: trafficCounterContinuity(r.flags) }))
|
||||
}
|
||||
@@ -16,7 +16,7 @@ else {
|
||||
assert.equal((await pool.query("SELECT current_database() AS name")).rows[0].name,database)
|
||||
const prefix="own-history178-"+Date.now()+"-",userId=prefix+"user"
|
||||
let serverId:number|undefined,deny=false,counter="1000",cases=0,requests=0
|
||||
const http=createServer((req,res)=>{requests++;res.setHeader("Content-Type","application/json");if(deny){res.statusCode=401;res.end('{"message":"Unauthorized"}')}else res.end(JSON.stringify(req.url?.includes("/wireguard/")?[]:[{name:"ether1",running:"true",disabled:"false","rx-byte":counter,"tx-byte":counter}]))})
|
||||
const http=createServer((req,res)=>{requests++;res.setHeader("Content-Type","application/json");if(deny){res.statusCode=401;res.end('{"message":"Unauthorized"}')}else res.end(JSON.stringify(req.url?.includes("/wireguard/")?[]:[{".id":"*1",name:"ether1",running:"true",disabled:"false","rx-byte":counter,"tx-byte":counter}]))})
|
||||
await new Promise<void>(resolve=>http.listen(0,"127.0.0.1",resolve));const address=http.address();assert(address&&typeof address==="object")
|
||||
const app=Fastify();await app.register(trafficRoutes)
|
||||
const get=async(path:string)=>{const r=await app.inject({method:"GET",url:path});assert.equal(r.statusCode,200);return r.json()}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import {nativeTrafficCounterIdentity} from './traffic-observation.js'
|
||||
import {buildTrafficFromSamples} from './traffic-rate.js'
|
||||
import {encodeTrafficFlags,trafficCounterContinuity} from '../db/traffic-flags.js'
|
||||
const at=Date.parse('2026-10-13T00:00:00Z'),iso=new Date(at).toISOString(),identity=nativeTrafficCounterIdentity(iso,'*a')!
|
||||
assert.equal(identity,nativeTrafficCounterIdentity(new Date(at),'*A'))
|
||||
for(const id of[undefined,null,'','bad','*z',{},1])assert.equal(nativeTrafficCounterIdentity(iso,id),null)
|
||||
assert.equal(trafficCounterContinuity(13),undefined);assert.equal(trafficCounterContinuity(encodeTrafficFlags(true,false,true,true,false)),false);assert.equal(trafficCounterContinuity(encodeTrafficFlags(true,false,true,true,true)),true)
|
||||
const first={interfaceName:'ether1',sampledAt:new Date(at-30_000).toISOString(),rxBytes:1000,txBytes:1000,rxBps:0,txBps:0,running:true,disabled:false,countersKnown:true,nativeCounterIdentity:identity,continuityKnown:false}
|
||||
for(const nextIdentity of[identity,nativeTrafficCounterIdentity(iso,'*b'),nativeTrafficCounterIdentity(new Date(at+1),'*a'),null]){
|
||||
const cur={...first,sampledAt:iso,rxBytes:2000,txBytes:2000,nativeCounterIdentity:nextIdentity,continuityKnown:nextIdentity===identity}
|
||||
const built=buildTrafficFromSamples([first,cur],at-30_000,at,undefined,undefined,true)
|
||||
assert.equal(built.observations!.rxNow,nextIdentity===identity);assert.equal(built.rxTotalGiB,nextIdentity===identity?1000/1024**3:0)
|
||||
}
|
||||
console.log('native counter identity/config/legacy marker/pair guards passed')
|
||||
@@ -15,8 +15,8 @@ else {
|
||||
let serverId: number | undefined, counters: unknown = "1000", second = false, cases = 0, requests = 0
|
||||
const http = createServer((req, res) => {
|
||||
requests++; res.setHeader("Content-Type", "application/json")
|
||||
const rows = [{ name: "ether1", running: "true", disabled: "false", "rx-byte": counters, "tx-byte": counters }]
|
||||
if (second) rows.push({ name: "ether2", running: "true", disabled: "false", "rx-byte": "1000", "tx-byte": "1000" })
|
||||
const rows = [{ ".id":"*1",name: "ether1", running: "true", disabled: "false", "rx-byte": counters, "tx-byte": counters }]
|
||||
if (second) rows.push({ ".id":"*2",name: "ether2", running: "true", disabled: "false", "rx-byte": "1000", "tx-byte": "1000" })
|
||||
res.end(JSON.stringify(req.url?.includes("/wireguard/") ? [] : rows))
|
||||
})
|
||||
await new Promise<void>(resolve => http.listen(0, "127.0.0.1", resolve))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {parseTrafficPeers} from "./traffic-peer-observation.js"
|
||||
import assert from "node:assert/strict"
|
||||
import { observedTrafficRate, parseTrafficCounter, parseTrafficFlag } from "./traffic-observation.js"
|
||||
import { observedTrafficRate, parseTrafficCounter, parseTrafficFlag, nativeTrafficCounterIdentity } from "./traffic-observation.js"
|
||||
import { collectSourceSignals } from "./alert-engine/collector-source-signals.js"
|
||||
import fs from "node:fs"
|
||||
import vm from "node:vm"
|
||||
@@ -38,18 +39,18 @@ const ast = ts.createSourceFile("traffic-collector.ts", collector, ts.ScriptTarg
|
||||
const functions = ast.statements.filter(n => ts.isFunctionDeclaration(n) && n.name &&
|
||||
["waveKey", "sampleRate", "toNum", "collectTrafficOnce"].includes(n.name.text)).map(n => n.getText(ast)).join("\n")
|
||||
assert.equal(ast.statements.filter(n => ts.isFunctionDeclaration(n) && n.name && n.name.text === "collectTrafficOnce").length, 1)
|
||||
let inserted: unknown[] = [], wave = new Map<string, typeof previous>(), counters: unknown = "1000"
|
||||
let inserted: unknown[] = [], wave = new Map<string, typeof previous & {nativeCounterIdentity?:string|null}>(), counters: unknown = "1000"
|
||||
let included = true
|
||||
const context = vm.createContext({
|
||||
Date, observedTrafficRate, parseTrafficCounter, parseTrafficFlag, bpsToMbps, shouldIncludeIface,
|
||||
Date, nativeTrafficCounterIdentity,parseTrafficPeers, observedTrafficRate, parseTrafficCounter, parseTrafficFlag, bpsToMbps, shouldIncludeIface,
|
||||
SCHEDULER_RUN_SNAPSHOT_VERSION: 1, servers: {}, trafficSamples: {}, trafficSettings: {}, eq: () => ({}),
|
||||
getSettingsRow: async () => ({}), readPreviousWave: async () => wave, rememberServerIfaces: () => {},
|
||||
encodeTrafficFlags: () => 1,
|
||||
recordTrafficCollectionAttempt: async () => {},
|
||||
MikrotikClient: { fromServer: () => ({ get: async (path: string) => path === "/interface" ?
|
||||
[{ name: "ether1", running: String(included), disabled: "false", "rx-byte": counters, "tx-byte": counters }] : [] }) },
|
||||
[{ ".id":"*1",name: "ether1", running: String(included), disabled: "false", "rx-byte": counters, "tx-byte": counters }] : [] }) },
|
||||
db: {
|
||||
select: () => ({ from: () => ({ where: async () => [{ id: 1, name: "audit", host: "audit.example", enabled: true }] }) }),
|
||||
select: () => ({ from: () => ({ where: async () => [{ updatedAt:iso,id: 1, name: "audit", host: "audit.example", enabled: true }] }) }),
|
||||
insert: () => ({ values: async (rows: unknown[]) => { inserted.push(...rows) } }),
|
||||
update: () => ({ set: () => ({ where: async () => {} }) }),
|
||||
},
|
||||
@@ -58,7 +59,7 @@ vm.runInContext(ts.transpileModule("let collecting=false;\n" + functions.replace
|
||||
"\nglobalThis.run=collectTrafficOnce;", { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText, context)
|
||||
let snapshot = await context.run()
|
||||
assert.equal(snapshot.servers[0].ok, true); assert.equal(snapshot.servers[0].sumRxMbps, undefined); assert.equal(inserted.length, 1)
|
||||
wave = new Map([["ether1\0", { ...previous, sampledAt: new Date(Date.now() - 30_000).toISOString() }]])
|
||||
wave = new Map([["ether1\0", { ...previous, nativeCounterIdentity:nativeTrafficCounterIdentity(iso,"*1"),sampledAt: new Date(Date.now() - 30_000).toISOString() }]])
|
||||
snapshot = await context.run(); assert.equal(snapshot.servers[0].sumRxMbps, 0)
|
||||
counters = "10"; snapshot = await context.run(); assert.equal(snapshot.servers[0].sumRxMbps, undefined)
|
||||
counters = "bad"; inserted = []; snapshot = await context.run()
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { rateBpsFromDelta } from "./traffic-rate.js"
|
||||
export function nativeTrafficCounterIdentity(updatedAt: string | Date, nativeId: unknown): string | null {
|
||||
const at = updatedAt instanceof Date ? updatedAt.getTime() : Date.parse(updatedAt)
|
||||
if (!Number.isFinite(at) || typeof nativeId !== "string" || !/^\*[0-9a-f]+$/i.test(nativeId)) return null
|
||||
return JSON.stringify([new Date(at).toISOString(), nativeId.toUpperCase()])
|
||||
}
|
||||
export function parseTrafficFlag(raw: unknown): boolean {
|
||||
if (raw === true || raw === "true" || raw === "yes") return true
|
||||
if (raw === false || raw === "false" || raw === "no") return false
|
||||
|
||||
@@ -13,6 +13,6 @@ export function parseTrafficPeers(raw: unknown) {
|
||||
const key = `${interfaceName}\0${peerPublicKey}`
|
||||
if (seen.has(key)) throw new Error("Повторное измерение WireGuard peer")
|
||||
seen.add(key)
|
||||
return { interfaceName, peerPublicKey, rxBytes: parseTrafficCounter(p.rx), txBytes: parseTrafficCounter(p.tx), disabled: parseTrafficFlag(p.disabled) }
|
||||
return { interfaceName, peerPublicKey, nativeId: p[".id"], rxBytes: parseTrafficCounter(p.rx), txBytes: parseTrafficCounter(p.tx), disabled: parseTrafficFlag(p.disabled) }
|
||||
})
|
||||
}
|
||||
@@ -14,6 +14,8 @@ export interface TrafficSampleLike {
|
||||
running: boolean
|
||||
disabled: boolean
|
||||
countersKnown?: boolean
|
||||
continuityKnown?: boolean
|
||||
nativeCounterIdentity?: string | null
|
||||
}
|
||||
|
||||
export interface RatePoint {
|
||||
@@ -180,7 +182,8 @@ export function buildTrafficFromSamples(
|
||||
const t0 = parseIsoMs(prev.sampledAt)
|
||||
const t1 = parseIsoMs(cur.sampledAt)
|
||||
if(Number.isFinite(Date.parse(cur.sampledAt))&&t1>=rangeStartMs&&t1<=rangeEndMs)lastAttemptAt=Math.max(lastAttemptAt??t1,t1)
|
||||
const countersKnown = prev.countersKnown !== false && cur.countersKnown !== false
|
||||
const sameIdentity = (prev.nativeCounterIdentity ?? null) === (cur.nativeCounterIdentity ?? null)
|
||||
const countersKnown = prev.countersKnown !== false && cur.countersKnown !== false && cur.continuityKnown !== false && sameIdentity
|
||||
const rxBps = countersKnown ? rateBpsFromDelta(prev.rxBytes, cur.rxBytes, t0, t1) : null
|
||||
const txBps = countersKnown ? rateBpsFromDelta(prev.txBytes, cur.txBytes, t0, t1) : null
|
||||
if (t1 >= rangeStartMs && t1 <= rangeEndMs) {
|
||||
|
||||
@@ -9,8 +9,8 @@ const database=process.env.MMAPP_OWN_SQL_DB
|
||||
if(!database)console.log("traffic status SQL: skipped; explicit owned database required")
|
||||
else{
|
||||
assert(/^mm_accept_166_\d+$/.test(database));assert.equal(new URL(process.env.DATABASE_URL!).pathname,"/"+database);assert.equal((await pool.query("SELECT current_database() AS name")).rows[0].name,database)
|
||||
const prefix="own-status181-"+Date.now()+"-",userId=prefix+"user";let serverId:number|undefined,cases=0,deny=false,flag:unknown=true,peerDisabled=false,peerVariant="valid",denyPeer=false
|
||||
const http=createServer((req,res)=>{res.setHeader("Content-Type","application/json");if(deny || denyPeer && req.url?.includes("/wireguard/")){res.statusCode=401;return res.end('{"message":"Unauthorized"}')}res.end(JSON.stringify(req.url?.includes("/wireguard/")?(peerVariant==="shape"?{}:peerVariant==="duplicate"?[{interface:"wg1","public-key":"fixturekey",rx:"1000",tx:"1000",disabled:false},{interface:"wg1","public-key":"fixturekey",rx:"1000",tx:"1000",disabled:false}]:[{interface:"wg1","public-key":"fixturekey",rx:"1000",tx:"1000",disabled:peerDisabled},...(peerVariant==="partial"?[{interface:"wg1","public-key":"broken",rx:"invalid",tx:"1000",disabled:false}]:[])]):[{name:"ether1",running:flag,disabled:false,"rx-byte":"1000","tx-byte":"1000"}]))})
|
||||
const prefix="own-status181-"+Date.now()+"-",userId=prefix+"user";let serverId:number|undefined,cases=0,deny=false,flag:unknown=true,peerDisabled=false,peerVariant="valid",denyPeer=false,ifaceId:unknown="*1",counterValue="1000",peerId="*2",peerCounter="1000"
|
||||
const http=createServer((req,res)=>{res.setHeader("Content-Type","application/json");if(deny || denyPeer && req.url?.includes("/wireguard/")){res.statusCode=401;return res.end('{"message":"Unauthorized"}')}res.end(JSON.stringify(req.url?.includes("/wireguard/")?(peerVariant==="shape"?{}:peerVariant==="duplicate"?[{".id":peerId,interface:"wg1","public-key":"fixturekey",rx:"1000",tx:"1000",disabled:false},{".id":peerId,interface:"wg1","public-key":"fixturekey",rx:"1000",tx:"1000",disabled:false}]:[{".id":peerId,interface:"wg1","public-key":"fixturekey",rx:peerCounter,tx:peerCounter,disabled:peerDisabled},...(peerVariant==="partial"?[{".id":peerId,interface:"wg1","public-key":"broken",rx:"invalid",tx:"1000",disabled:false}]:[])]):[{".id":ifaceId,name:"ether1",running:flag,disabled:false,"rx-byte":counterValue,"tx-byte":counterValue}]))})
|
||||
await new Promise<void>(resolve=>http.listen(0,"127.0.0.1",resolve));const address=http.address();assert(address&&typeof address==="object");const app=Fastify();await app.register(trafficRoutes)
|
||||
const bindings=async()=>{const r=await app.inject({method:"GET",url:"/traffic/bound-interfaces?range=1h"});assert.equal(r.statusCode,200);return r.json().interfaces.filter((i:{userId:string})=>i.userId===userId)}
|
||||
try{
|
||||
@@ -39,6 +39,14 @@ else{
|
||||
await pool.query("UPDATE traffic_samples SET sampled_at=$1 WHERE server_id=$2",[new Date(Date.now()-120_000).toISOString(),serverId]);
|
||||
let gap=await collectTrafficOnce();assert.equal(gap.servers[0].ok,true);assert.equal(gap.servers[0].sumRxMbps,undefined);rows=await bindings();assert.equal(rows.find((r:{interfaceName:string})=>r.interfaceName==="ether1").observations.rxNow,false);
|
||||
gap=await collectTrafficOnce();assert.equal(gap.servers[0].sumRxMbps,0);rows=await bindings();assert.equal(rows.find((r:{interfaceName:string})=>r.interfaceName==="ether1").observations.rxNow,true);cases++;
|
||||
for(const change of["native-id","config","missing-id"]){
|
||||
counterValue=String(Number(counterValue)+1000);
|
||||
if(change==="native-id")ifaceId="*10";else if(change==="missing-id")ifaceId=undefined;else await pool.query("UPDATE servers SET updated_at=$1 WHERE id=$2",[new Date().toISOString(),serverId]);
|
||||
const switched=await collectTrafficOnce();assert.equal(switched.servers[0].sumRxMbps,undefined);rows=await bindings();assert.equal(rows.find((r:{interfaceName:string})=>r.interfaceName==="ether1").observations.rxNow,false);assert.equal(rows.find((r:{interfaceName:string})=>r.interfaceName==="ether1").rxTotal,0);cases++;
|
||||
if(change!=="missing-id"){const next=await collectTrafficOnce();assert.equal(next.servers[0].sumRxMbps,0);rows=await bindings();assert.equal(rows.find((r:{interfaceName:string})=>r.interfaceName==="ether1").observations.rxNow,true)}
|
||||
}
|
||||
ifaceId="*10";await collectTrafficOnce();await collectTrafficOnce();peerId="*20";peerCounter="2000";await collectTrafficOnce();rows=await bindings();let peer=rows.find((r:{interfaceName:string})=>r.interfaceName==="wg1");assert.equal(peer.observations.rxNow,false);assert.equal(peer.rxTotal,0);
|
||||
await collectTrafficOnce();rows=await bindings();peer=rows.find((r:{interfaceName:string})=>r.interfaceName==="wg1");assert.equal(peer.observations.rxNow,true);assert.equal(peer.rxNow,0);cases++;
|
||||
await pool.query("UPDATE servers SET enabled=false WHERE id=$1",[serverId]);rows=await bindings();assert(rows.every((r:{status:string})=>r.status==="unknown"));cases++
|
||||
console.log(`traffic status owned SQL: ${cases} cases; first-wave native flags/peer config/invalid/error/legacy/stale/disabled passed`)
|
||||
}finally{await app.close();await new Promise<void>(resolve=>http.close(()=>resolve()));await pool.query("DELETE FROM app_users WHERE id=$1",[userId]);if(serverId!==undefined){await pool.query("DELETE FROM traffic_samples WHERE server_id=$1",[serverId]);await pool.query("DELETE FROM servers WHERE id=$1",[serverId])}await closePool()}
|
||||
|
||||
Reference in new issue
Block a user