feat(api, test, web): enhance IP hit tracking and reset logic for agents
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 2m26s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

- Implemented a reset mechanism for per-IP baselines in the agent routes, ensuring accurate tracking after policy application.
- Updated tests to simulate traffic flush scenarios, verifying that IP hit statistics reset correctly and accumulate as expected.
- Modified the UI to reflect changes in terminology from "Sync windows" to "Hits" for better clarity in agent details.
- Enhanced documentation to explain the new behavior of IP hit tracking and baseline resets, improving user understanding.

These changes improve the accuracy and usability of IP hit tracking for agents, particularly in scenarios involving policy changes.
This commit is contained in:
Denozordec
2026-08-07 15:44:27 +07:00
parent 3815575799
commit 402182195f
6 changed files with 171 additions and 44 deletions
+5
View File
@@ -60,7 +60,10 @@ export {
upsertIpBlockStats,
listIpBlockStats,
deleteIpBlockStatsForAgent,
resetIpBlockStatsBaselines,
PRESENCE_REHIT_STALE_MS,
} from './stats.js'
export type { UpsertIpBlockStatsOptions, IpHitInput } from './stats.js'
export {
getSetting,
@@ -138,6 +141,7 @@ import {
upsertIpBlockStats,
listIpBlockStats,
deleteIpBlockStatsForAgent,
resetIpBlockStatsBaselines,
} from './stats.js'
import {
getSetting,
@@ -207,6 +211,7 @@ export const repos = {
upsertIpBlockStats,
listIpBlockStats,
deleteIpBlockStatsForAgent,
resetIpBlockStatsBaselines,
getSetting,
setSetting,
listSettings,
+33 -5
View File
@@ -36,19 +36,24 @@ export function deleteStatsSamplesForAgent(db: Db, agentId: string) {
export type IpHitInput = { ip: string; packets: number }
/** Gap after which a new presence report counts as a re-hit (left EVOFW_HITS). */
export const PRESENCE_REHIT_STALE_MS = 150_000
export type UpsertIpBlockStatsOptions = {
/**
* MikroTik EVOFW_HITS presence: always refresh last_seen;
* packets += 1 per report (sync-window sightings).
* MikroTik EVOFW_HITS presence: refresh last_seen every report while IP is listed;
* increment packets only on first see or re-hit after stale gap (left the list).
* Linux keeps absolute counter deltas (default).
*/
mode?: 'absolute' | 'presence'
/** Override re-hit gap (tests). Default PRESENCE_REHIT_STALE_MS. */
presenceStaleMs?: number
}
/**
* Upsert per-IP drop counters.
* - absolute (Linux): agent reports kernel counters; CP accumulates deltas.
* - presence (MikroTik): each report sighting → last_seen=now, packets+=1.
* - presence (MikroTik): last_seen while in HITS; packets = entries into HITS (not per-sync).
*/
export function upsertIpBlockStats(
db: Db,
@@ -58,6 +63,9 @@ export function upsertIpBlockStats(
opts: UpsertIpBlockStatsOptions = {},
) {
const mode = opts.mode ?? 'absolute'
const staleMs = opts.presenceStaleMs ?? PRESENCE_REHIT_STALE_MS
const nowMs = Date.parse(now)
for (const hit of hits) {
const ip = hit.ip.trim()
if (!ip) continue
@@ -88,11 +96,18 @@ export function upsertIpBlockStats(
.run()
continue
}
const prevSeen = Date.parse(existing.lastSeenAt)
const gapMs = Number.isFinite(prevSeen) ? nowMs - prevSeen : staleMs + 1
const isRehit = gapMs > staleMs
db.update(agentIpBlockStats)
.set({
packets: (existing.packets ?? 0) + 1,
lastReportedPackets: (existing.lastReportedPackets ?? 0) + 1,
lastSeenAt: now,
...(isRehit
? {
packets: (existing.packets ?? 0) + 1,
lastReportedPackets: (existing.lastReportedPackets ?? 0) + 1,
}
: {}),
})
.where(eq(agentIpBlockStats.id, existing.id))
.run()
@@ -144,3 +159,16 @@ export function deleteIpBlockStatsForAgent(db: Db, agentId: string) {
.where(eq(agentIpBlockStats.agentId, agentId))
.run()
}
/**
* After nft/ipset counter flush (policy apply), kernel absolutes restart at 0.
* Traffic detects this via packets_dropped drop; per-IP baselines must reset too,
* otherwise ip_hits with packets=0 are omitted from the report and lastReported
* stays high → second epoch deltas are lost (Traffic 14 vs Blocked IPs 7).
*/
export function resetIpBlockStatsBaselines(db: Db, agentId: string) {
db.update(agentIpBlockStats)
.set({ lastReportedPackets: 0 })
.where(eq(agentIpBlockStats.agentId, agentId))
.run()
}