feat(api, web): add Linux nft destination port hits for blocked IPs
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m59s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

Track tcp/udp dports via deny_port_hits, expose aggregate and per-IP ports in UI; install-link re-run refreshes nft rules.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-11 00:33:03 +07:00
co-authored by Cursor
parent 402182195f
commit c5069fbdaf
16 changed files with 897 additions and 30 deletions
+16 -2
View File
@@ -20,7 +20,7 @@ curl -fsSL https://<cp>/agent-install/<id> | bash
3. После enroll статус станет **Pending** — одобрите агента (Approve).
4. **Approved** — агент синхронизирует политику.
**Повторный запуск той же install-ссылки** на хосте, где агент уже стоит: обновляет sync-скрипт / timer (или MikroTik scheduler), **без** повторного enroll — `CLIENT_ID`/`token` сохраняются. Полный переустановки с новым токеном: `EVOFW_INSTALL_FORCE=1` (Linux).
**Повторный запуск той же install-ссылки** на хосте, где агент уже стоит: обновляет sync-скрипт / timer (или MikroTik scheduler) **и пересоздаёт nft rules** (в т.ч. `deny_port_hits`), **без** повторного enroll — `CLIENT_ID`/`token` сохраняются. Сбрасывается `last_hash`, затем сразу force sync. Полный переустановки с новым токеном: `EVOFW_INSTALL_FORCE=1` (Linux).
API (auth): `POST /api/v1/install-links` `{ "name": "web-01", "platform": "linux" | "mikrotik" }`.
@@ -58,7 +58,7 @@ Whitelist: nft chain policy drop + allow set. Blacklist: policy accept + deny se
Linux agent reports optional `ip_hits` in `POST /v1/agent/apply-report`:
- **nft:** tries set `deny_v4` with `flags interval; counter;`. If the kernel rejects counters on interval sets, falls back to plain interval (aggregate Traffic ↓ still works; per-IP empty).
- Upgrade path: on install-link re-run, `last_hash` is cleared once so sets can be recreated (chain deleted before set replace).
- Upgrade path: on install-link re-run, `last_hash` is cleared once so sets/chain can be recreated (chain deleted before set replace)**обновляет и port-hit правила**.
- **ipset:** prefers `hash:net … counters` on create; existing sets without counters are left as-is.
- Payload: only entries with `packets > 0`, **top 200** by packets.
- Control plane: `agent_ip_block_stats`, accumulates **deltas** of absolute kernel counters (как Traffic ↓). После flush set/chain (policy apply) CP сбрасывает per-IP baseline (`last_reported`), иначе вторая эпоха счётчиков теряется (Traffic растёт, Blocked IPs — нет).
@@ -67,6 +67,20 @@ Linux agent reports optional `ip_hits` in `POST /v1/agent/apply-report`:
IPv6 skipped.
## Destination ports (Linux nft)
На **nft** агент ведёт dynamic set `deny_port_hits` (`ipv4_addr . inet_proto . inet_service`, timeout 1h, counter):
- Deny rule для tcp/udp: `update @deny_port_hits { ip saddr . meta l4proto . th dport }` + drop; прочие протоколы — plain drop.
- В `apply-report`: optional `port_hits` top **500** `{ ip, port, protocol, packets }`.
- CP: `agent_port_block_stats` (absolute deltas). Set **не** flush’ится на каждый policy apply (элементы живут по timeout) — baseline не сбрасывается при Traffic flush.
- `GET /api/v1/agents/:id/blocked-ports` — aggregate top 50 портов; в `blocked-ips` у каждого IP — `ports` top 5.
- UI: **Top ports** + колонка Ports в Blocked IPs (только `platform=linux`).
- **ipset/iptables:** `port_hits: []`. MikroTik — без port hits.
- Чтобы подтянуть правила на уже установленном агенте: **re-run install one-liner** (см. выше).
IPv6 skipped.
## MikroTik (RouterOS 7.21+)
В UI `/agents`**Добавить агента** → platform **MikroTik**. Скопируйте one-liner:
+52 -3
View File
@@ -397,14 +397,14 @@ paths:
/api/v1/agents/{id}/blocked-ips:
get:
summary: Per-IP/CIDR drop counters (Linux nft/ipset)
summary: Per-IP/CIDR drop counters (Linux nft/ipset; MikroTik HITS)
tags: [ops]
security: [{ bearerAuth: [] }]
parameters:
- $ref: '#/components/parameters/Id'
responses:
'200':
description: Top blocked IPs by accumulated packets
description: Top blocked IPs by accumulated packets (+ optional top ports per IP on Linux)
content:
application/json:
schema:
@@ -420,6 +420,43 @@ paths:
packets: { type: integer }
first_seen_at: { type: string, format: date-time }
last_seen_at: { type: string, format: date-time }
ports:
type: array
description: Top destination ports for this IP (Linux nft)
items:
type: object
required: [port, protocol, packets]
properties:
port: { type: integer, minimum: 1, maximum: 65535 }
protocol: { type: string, enum: [tcp, udp] }
packets: { type: integer }
/api/v1/agents/{id}/blocked-ports:
get:
summary: Aggregate destination ports hit by denied sources (Linux nft)
tags: [ops]
security: [{ bearerAuth: [] }]
parameters:
- $ref: '#/components/parameters/Id'
responses:
'200':
description: Top ports by accumulated packets across all blocked IPs
content:
application/json:
schema:
type: object
properties:
items:
type: array
items:
type: object
required: [port, protocol, packets, last_seen_at]
properties:
port: { type: integer, minimum: 1, maximum: 65535 }
protocol: { type: string, enum: [tcp, udp] }
packets: { type: integer }
last_seen_at: { type: string, format: date-time }
/api/v1/integrations/evobgp/communities:
get:
summary: Proxy EvoBGP communities
@@ -520,7 +557,7 @@ paths:
/v1/agent/apply-report:
post:
summary: Apply report + packet stats (+ optional ip_hits)
summary: Apply report + packet stats (+ optional ip_hits / port_hits)
tags: [agent]
security: [{ agentToken: [] }]
requestBody:
@@ -548,6 +585,18 @@ paths:
properties:
ip: { type: string, maxLength: 64 }
packets: { type: integer, minimum: 0 }
port_hits:
type: array
maxItems: 500
description: Linux nft deny_port_hits (ip × proto × dport, packets > 0)
items:
type: object
required: [ip, port, protocol, packets]
properties:
ip: { type: string, maxLength: 64 }
port: { type: integer, minimum: 1, maximum: 65535 }
protocol: { type: string, enum: [tcp, udp] }
packets: { type: integer, minimum: 0 }
responses:
'200':
description: OK