diff --git a/apps/api/src/agent-scripts/evofw-firewall.sh b/apps/api/src/agent-scripts/evofw-firewall.sh
index 1ddb62a..de088bf 100644
--- a/apps/api/src/agent-scripts/evofw-firewall.sh
+++ b/apps/api/src/agent-scripts/evofw-firewall.sh
@@ -443,8 +443,8 @@ apply_nft() {
((${#batch[@]})) && nft_add_chunk "$table" "$name" allow_v4 "${batch[@]}"
# input + forward: deny → Port ACL → allow → default.
- # prerouting (mangle, before Docker NAT): established → deny → Port ACL only
- # (no default drop — other traffic continues to host/Docker).
+ # prerouting (mangle, before Docker NAT): established → deny → Port ACL inbound only
+ # (fib daddr type local; no default drop — other traffic continues to host/Docker).
if [[ "$DEFAULT_ACTION" == "drop" ]]; then
nft add chain "$table" "$name" input '{ type filter hook input priority 0; policy drop; }'
nft add chain "$table" "$name" forward '{ type filter hook forward priority 0; policy drop; }'
@@ -562,18 +562,26 @@ def emit_set(p):
if chunk:
print(f"nft add element inet evofw {setname} '{{ {', '.join(chunk)} }}'")
+def chain_match(chain):
+ if chain == "prerouting":
+ return "fib daddr type local "
+ if chain == "forward":
+ return "ct status dnat "
+ return ""
+
def emit_rule(p, chain):
verdict = "drop" if p["action"] == "close" else "accept"
comment = f"evofw-port-{p['rid']}"
proto, dport = p["proto"], p["dport"]
+ prefix = chain_match(chain)
if p["is_all"]:
print(
- f'nft add rule inet evofw {chain} {proto} dport {dport} counter {verdict} comment "{comment}"'
+ f'nft add rule inet evofw {chain} {prefix}{proto} dport {dport} counter {verdict} comment "{comment}"'
)
return
setname = f"port_src_{p['rid']}_{p['proto']}"
print(
- f'nft add rule inet evofw {chain} ip saddr @{setname} {proto} dport {dport} counter {verdict} comment "{comment}"'
+ f'nft add rule inet evofw {chain} {prefix}ip saddr @{setname} {proto} dport {dport} counter {verdict} comment "{comment}"'
)
parsed = [p for p in (parse_rule(r) for r in rules) if p]
@@ -598,8 +606,9 @@ for chain in CHAINS:
continue
seen.add(key)
comment = f"evofw-port-implicit-{p['proto']}-{p['dport']}"
+ prefix = chain_match(chain)
print(
- f'nft add rule inet evofw {chain} {p["proto"]} dport {p["dport"]} counter drop comment "{comment}"'
+ f'nft add rule inet evofw {chain} {prefix}{p["proto"]} dport {p["dport"]} counter drop comment "{comment}"'
)
PY
local cmd
diff --git a/apps/web/src/components/agents/agent-port-acl.tsx b/apps/web/src/components/agents/agent-port-acl.tsx
index 4ecc437..52e450c 100644
--- a/apps/web/src/components/agents/agent-port-acl.tsx
+++ b/apps/web/src/components/agents/agent-port-acl.tsx
@@ -689,9 +689,9 @@ export function AgentPortAcl({ agentId }: AgentPortAclProps) {
Port ACL
- Open по списку — порт только с этих IP (хост и Docker).
- Системные порты можно переопределить. Apply через nft
- (upgrade install-ссылкой).
+ Open по списку — входящие на порт только с этих IP (хост и
+ Docker publish). Исходящий клиентский 80/443 не режется.
+ Apply через nft (upgrade install-ссылкой).
diff --git a/docs/agents.md b/docs/agents.md
index 52e3fa0..c491ba2 100644
--- a/docs/agents.md
+++ b/docs/agents.md
@@ -115,7 +115,7 @@ Per-agent таблица `agent_port_rules`: `open|close`, `tcp|udp|both`, port
- 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: close drop → open accept (`comment "evofw-port-"`) → **implicit drop** для каждого `(proto, dport)` с хотя бы одним `open` (`comment "evofw-port-implicit-…"`). Одно правило покрывает **хост и Docker**: hooks **prerouting** (priority mangle, до Docker NAT — публичный dport) + **input** + **forward**. Порт с open = whitelist: src из правила accept, остальные внешние drop. `lo` и `ct established,related` выше по цепочке. prerouting **без** terminal default drop (прочий трафик идёт дальше).
+- nft apply: close drop → open accept (`comment "evofw-port-"`) → **implicit drop** для каждого `(proto, dport)` с хотя бы одним `open` (`comment "evofw-port-implicit-…"`). **Только входящие к сервисам** (не клиентский egress): **prerouting** `fib daddr type local` (публичный порт до Docker NAT) + **input** + **forward** `ct status dnat` (Docker `-p`). Порт с open = whitelist входящих: src из правила accept, остальные внешние drop. Исходящий curl/контейнер на 80/443 не матчится. `lo` и `ct established,related` выше по цепочке. prerouting **без** terminal default drop.
- UI: tab **Port ACL** (DataGrid + Sheet create/edit + Import). Owner: **EvoFW** (desired) и **system** (listeners + foreign allow с хоста). Переопределённые system-строки скрыты в All. Системный порт можно переопределить → desired `open` (список/CIDR); ufw/iptables/Docker-цепочки не меняются.
- ipset / MikroTik: без L4 apply; секции скрыты для non-linux
diff --git a/docs/architecture.md b/docs/architecture.md
index 0e09f88..6a96801 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -28,7 +28,7 @@
- Правило в наборе: `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: deny → **Port ACL** (`close` / `open` / implicit drop для портов с open) → allow → `default_action` на hooks **prerouting** (priority mangle, до Docker DNAT), **input** и **forward**. `open` по списку = публичный порт только с разрешённых src, хост и Docker `-p`.
+- На Linux nft: deny → **Port ACL** (`close` / `open` / implicit drop для портов с open) → allow → `default_action` на hooks **prerouting** (priority mangle, `fib daddr type local`, до Docker DNAT), **input** и **forward** (`ct status dnat`). `open` по списку = **входящие** на публичный порт только с разрешённых src (хост и Docker `-p`); исходящий клиентский 80/443 не режется.
- Exact overlap: `allow \ deny` (`conflicts_dropped`); deny wins
- Overrides, смена наборов, `default_action`, Port ACL и refresh DNS/lists бампят `policy_generation`