feat(api, web): implement port ACL and host firewall snapshot features
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m43s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

- Added support for managing desired L4 port ACL rules for Linux agents, allowing for open/close actions on specified ports.
- Introduced a new endpoint for CRUD operations on port rules, enhancing the API's capabilities for agent management.
- Implemented functionality to collect and report host firewall snapshots, capturing observed rules and listeners for better monitoring.
- Updated the agent detail view to include tabs for managing port ACLs and viewing host firewall data, improving user experience.
- Enhanced documentation to reflect the new features and API changes, ensuring clarity for users and developers.

These changes significantly improve the management and visibility of firewall rules and port access control for agents.
This commit is contained in:
Denozordec
2026-08-11 15:08:00 +07:00
parent c5069fbdaf
commit 43f5ac2525
22 changed files with 2853 additions and 17 deletions
+25
View File
@@ -81,6 +81,31 @@ IPv6 skipped.
IPv6 skipped.
## Host firewall snapshot + Port ACL (Linux)
### Observed (Host firewall)
Каждый sync агент собирает best-effort снимок и шлёт в `apply-report.host_firewall`:
- `nft list ruleset`, `iptables-save`, optional `ufw` / `firewall-cmd`, `ss -lntu`
- Каждое правило: `ownership: evofw | foreign` (метка по имени table/chain/comment `evofw`)
- CP: `agent_host_firewall_snapshots`; `GET /api/v1/agents/:id/host-firewall`
- UI agent detail → tab **Host firewall** (Rules / Listeners)
Foreign правила **только отображаются** — с CP не редактируются.
### Desired (Port ACL)
Per-agent таблица `agent_port_rules`: `open|close`, `tcp|udp|both`, port range, `src_kind: all|cidr|list`.
- API: CRUD `/api/v1/agents/:id/port-rules`, import `/port-rules/import` (from list или policy set sources)
- Policy `apply_version: 3``port_rules[]` с expanded `src_cidrs`
- nft apply: после L3 allow — close drop, затем open accept (`comment "evofw-port-<id>"`)
- UI: tab **Port ACL** (DataGrid + Sheet create/edit + Import)
- ipset / MikroTik: без L4 apply; секции скрыты для non-linux
Мутация Port ACL бампит `policy_generation` → agent re-apply.
## MikroTik (RouterOS 7.21+)
В UI `/agents`**Добавить агента** → platform **MikroTik**. Скопируйте one-liner:
+4 -3
View File
@@ -17,8 +17,8 @@
1. **Enroll**`POST /v1/agent/enroll` + `X-EvoFW-Seed` → pending agent
2. **Approve** — UI/API → status approved
3. **Policy**`GET /v1/agent/policy` → deny/allow CIDRs + `default_action` + hash (`apply_version: 2`)
4. **Apply** — agent пишет kernel rules, `POST /v1/agent/apply-report` + stats sample
3. **Policy**`GET /v1/agent/policy` → deny/allow CIDRs + `default_action` + optional `port_rules` + hash (`apply_version: 3`)
4. **Apply** — agent пишет kernel rules (L3 + L4 port ACL на nft), `POST /v1/agent/apply-report` + stats + optional `host_firewall` snapshot
5. **Lists refresh** — cron каждые 5 мин (json_url / domains / evobgp_community)
## Политика
@@ -27,8 +27,9 @@
- Правило в наборе: `action: deny | allow` + ровно один источник — IP-список (`list_id`), CIDR или DNS-имя (`hostname` → A/AAAA, кэш в `policy_rule_resolved`)
- Evaluate: правила всех назначенных enabled-наборов (sort + priority) + `ip_overrides`
- Цепочка ядра **всегда**: deny → allow → `default_action` (`accept` | `drop` на агенте)
- На Linux nft: после allow — **Port ACL** (`close` drop, затем `open` accept) из `agent_port_rules`
- Exact overlap: `allow \ deny` (`conflicts_dropped`); deny wins
- Overrides, смена наборов, `default_action` и refresh DNS/lists бампят `policy_generation`
- Overrides, смена наборов, `default_action`, Port ACL и refresh DNS/lists бампят `policy_generation`
## Auth
+257 -1
View File
@@ -457,6 +457,161 @@ paths:
packets: { type: integer }
last_seen_at: { type: string, format: date-time }
/api/v1/agents/{id}/port-rules:
get:
summary: Desired Port ACL rules (Linux)
tags: [ops]
security: [{ bearerAuth: [] }]
parameters:
- $ref: '#/components/parameters/Id'
responses:
'200':
description: Port ACL rules
content:
application/json:
schema:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/AgentPortRule'
post:
summary: Create Port ACL rule
tags: [ops]
security: [{ bearerAuth: [] }]
parameters:
- $ref: '#/components/parameters/Id'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateAgentPortRule'
responses:
'200':
description: Created rule
content:
application/json:
schema:
$ref: '#/components/schemas/AgentPortRule'
'400':
description: Validation / non-linux
/api/v1/agents/{id}/port-rules/import:
post:
summary: Import Port ACL from IP list or policy set sources
tags: [ops]
security: [{ bearerAuth: [] }]
parameters:
- $ref: '#/components/parameters/Id'
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [from, action, ports]
properties:
from: { type: string, enum: [list, set] }
list_id: { type: string }
set_id: { type: string }
action: { type: string, enum: [open, close] }
protocol: { type: string, enum: [tcp, udp, both], default: tcp }
ports:
type: array
minItems: 1
maxItems: 50
items:
type: object
required: [port_start]
properties:
port_start: { type: integer, minimum: 1, maximum: 65535 }
port_end: { type: integer, minimum: 1, maximum: 65535 }
enabled: { type: boolean, default: true }
comment: { type: string, maxLength: 500 }
responses:
'200':
description: Created rules
content:
application/json:
schema:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/AgentPortRule'
/api/v1/agents/{id}/port-rules/{ruleId}:
patch:
summary: Update Port ACL rule
tags: [ops]
security: [{ bearerAuth: [] }]
parameters:
- $ref: '#/components/parameters/Id'
- name: ruleId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateAgentPortRule'
responses:
'200':
description: Updated rule
content:
application/json:
schema:
$ref: '#/components/schemas/AgentPortRule'
delete:
summary: Delete Port ACL rule
tags: [ops]
security: [{ bearerAuth: [] }]
parameters:
- $ref: '#/components/parameters/Id'
- name: ruleId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Deleted
/api/v1/agents/{id}/host-firewall:
get:
summary: Last observed host firewall snapshot (Linux)
tags: [ops]
security: [{ bearerAuth: [] }]
parameters:
- $ref: '#/components/parameters/Id'
responses:
'200':
description: Snapshot (empty if never reported)
content:
application/json:
schema:
type: object
properties:
collected_at:
type: string
format: date-time
nullable: true
raw_digest:
type: string
nullable: true
rules:
type: array
items:
$ref: '#/components/schemas/HostFwRule'
listeners:
type: array
items:
$ref: '#/components/schemas/HostListener'
/api/v1/integrations/evobgp/communities:
get:
summary: Proxy EvoBGP communities
@@ -557,7 +712,7 @@ paths:
/v1/agent/apply-report:
post:
summary: Apply report + packet stats (+ optional ip_hits / port_hits)
summary: Apply report + packet stats (+ optional ip_hits / port_hits / host_firewall)
tags: [agent]
security: [{ agentToken: [] }]
requestBody:
@@ -597,6 +752,8 @@ paths:
port: { type: integer, minimum: 1, maximum: 65535 }
protocol: { type: string, enum: [tcp, udp] }
packets: { type: integer, minimum: 0 }
host_firewall:
$ref: '#/components/schemas/HostFirewallPayload'
responses:
'200':
description: OK
@@ -608,6 +765,105 @@ components:
in: path
required: true
schema: { type: string }
schemas:
AgentPortRule:
type: object
required:
[
id,
agent_id,
action,
protocol,
port_start,
port_end,
src_kind,
enabled,
priority,
created_at,
updated_at,
]
properties:
id: { type: string }
agent_id: { type: string }
action: { type: string, enum: [open, close] }
protocol: { type: string, enum: [tcp, udp, both] }
port_start: { type: integer, minimum: 1, maximum: 65535 }
port_end: { type: integer, minimum: 1, maximum: 65535 }
src_kind: { type: string, enum: [all, cidr, list] }
src_cidr: { type: string, nullable: true }
list_id: { type: string, nullable: true }
list_name: { type: string, nullable: true }
enabled: { type: boolean }
comment: { type: string, nullable: true }
priority: { type: integer }
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
CreateAgentPortRule:
type: object
required: [action, port_start]
properties:
action: { type: string, enum: [open, close] }
protocol: { type: string, enum: [tcp, udp, both], default: tcp }
port_start: { type: integer, minimum: 1, maximum: 65535 }
port_end: { type: integer, minimum: 1, maximum: 65535 }
src_kind: { type: string, enum: [all, cidr, list], default: all }
src_cidr: { type: string }
list_id: { type: string }
enabled: { type: boolean, default: true }
comment: { type: string, maxLength: 500 }
priority: { type: integer, default: 100 }
UpdateAgentPortRule:
type: object
properties:
action: { type: string, enum: [open, close] }
protocol: { type: string, enum: [tcp, udp, both] }
port_start: { type: integer, minimum: 1, maximum: 65535 }
port_end: { type: integer, minimum: 1, maximum: 65535 }
src_kind: { type: string, enum: [all, cidr, list] }
src_cidr: { type: string, nullable: true }
list_id: { type: string, nullable: true }
enabled: { type: boolean }
comment: { type: string, maxLength: 500, nullable: true }
priority: { type: integer }
HostFwRule:
type: object
required: [ownership, backend, raw]
properties:
ownership: { type: string, enum: [evofw, foreign] }
backend:
type: string
enum: [nft, iptables, ufw, firewalld, listener]
table: { type: string }
chain: { type: string }
action: { type: string }
protocol: { type: string }
dport: { type: string }
sport: { type: string }
saddr: { type: string }
daddr: { type: string }
comment: { type: string }
raw: { type: string, maxLength: 512 }
HostListener:
type: object
required: [protocol, port, address]
properties:
protocol: { type: string }
port: { type: integer, minimum: 0, maximum: 65535 }
address: { type: string }
process: { type: string }
HostFirewallPayload:
type: object
properties:
rules:
type: array
maxItems: 500
items:
$ref: '#/components/schemas/HostFwRule'
listeners:
type: array
maxItems: 200
items:
$ref: '#/components/schemas/HostListener'
securitySchemes:
bearerAuth:
type: http