feat(api, web): enhance port ACL logic and documentation
- Refined the `collect_nft_stats` function in `evofw-firewall.sh` to iterate over multiple chains, improving packet counting accuracy for dropped and accepted packets. - Updated the port ACL handling to include new chains in the firewall rules, ensuring comprehensive coverage for input, forward, and prerouting. - Enhanced the UI to clarify the behavior of port ACLs, emphasizing the distinction between EvoFW and system rules, and the implications of open ports. - Improved documentation to reflect the updated port ACL logic and its interaction with Docker NAT, ensuring users understand the new behavior. These changes enhance the functionality and clarity of port ACL management, improving user experience and system reliability.
This commit is contained in:
@@ -240,21 +240,23 @@ ensure_nft_set() {
|
||||
|
||||
collect_nft_stats() {
|
||||
PACKETS_DROPPED=0; PACKETS_ACCEPTED=0
|
||||
local line n
|
||||
while IFS= read -r line; do
|
||||
[[ "$line" =~ packets[[:space:]]+([0-9]+) ]] || continue
|
||||
n="${BASH_REMATCH[1]}"
|
||||
# Policy set hits only (ignore lo / established noise)
|
||||
if [[ "$line" == *@deny_v4* ]]; then
|
||||
PACKETS_DROPPED=$((PACKETS_DROPPED + n))
|
||||
elif [[ "$line" == *@allow_v4* ]]; then
|
||||
PACKETS_ACCEPTED=$((PACKETS_ACCEPTED + n))
|
||||
elif [[ "$line" == *" counter drop"* && "$line" != *@* ]]; then
|
||||
PACKETS_DROPPED=$((PACKETS_DROPPED + n))
|
||||
elif [[ "$line" == *" counter accept"* && "$line" != *@* && "$line" != *established* && "$line" != *"iif \"lo\""* && "$line" != *"iif lo"* ]]; then
|
||||
PACKETS_ACCEPTED=$((PACKETS_ACCEPTED + n))
|
||||
fi
|
||||
done < <(nft list chain inet evofw input 2>/dev/null || true)
|
||||
local line n chain
|
||||
for chain in input forward prerouting; do
|
||||
while IFS= read -r line; do
|
||||
[[ "$line" =~ packets[[:space:]]+([0-9]+) ]] || continue
|
||||
n="${BASH_REMATCH[1]}"
|
||||
# Policy set hits only (ignore lo / established noise)
|
||||
if [[ "$line" == *@deny_v4* ]]; then
|
||||
PACKETS_DROPPED=$((PACKETS_DROPPED + n))
|
||||
elif [[ "$line" == *@allow_v4* ]]; then
|
||||
PACKETS_ACCEPTED=$((PACKETS_ACCEPTED + n))
|
||||
elif [[ "$line" == *" counter drop"* && "$line" != *@* ]]; then
|
||||
PACKETS_DROPPED=$((PACKETS_DROPPED + n))
|
||||
elif [[ "$line" == *" counter accept"* && "$line" != *@* && "$line" != *established* && "$line" != *"iif \"lo\""* && "$line" != *"iif lo"* ]]; then
|
||||
PACKETS_ACCEPTED=$((PACKETS_ACCEPTED + n))
|
||||
fi
|
||||
done < <(nft list chain inet evofw "$chain" 2>/dev/null || true)
|
||||
done
|
||||
}
|
||||
|
||||
# Parse nft/ipset listing from file → IP_HITS_FILE (top-N JSON).
|
||||
@@ -414,9 +416,11 @@ apply_nft() {
|
||||
for p in "${ALLOW[@]+"${ALLOW[@]}"}"; do [[ "$p" == *:* ]] && continue; allow_v4+=("$p"); done
|
||||
|
||||
nft list table "$table" "$name" >/dev/null 2>&1 || nft add table "$table" "$name"
|
||||
# Drop chain first so sets can be deleted/recreated (upgrade to counters / port hits).
|
||||
# Drop chains first so sets can be deleted/recreated (upgrade to counters / port hits).
|
||||
# Stats were already captured by the caller before apply_nft.
|
||||
nft delete chain "$table" "$name" input 2>/dev/null || true
|
||||
nft delete chain "$table" "$name" forward 2>/dev/null || true
|
||||
nft delete chain "$table" "$name" prerouting 2>/dev/null || true
|
||||
ensure_nft_set "$table" "$name" deny_v4
|
||||
ensure_nft_set "$table" "$name" allow_v4
|
||||
if ensure_nft_port_hits_set "$table" "$name"; then
|
||||
@@ -438,41 +442,61 @@ apply_nft() {
|
||||
done
|
||||
((${#batch[@]})) && nft_add_chunk "$table" "$name" allow_v4 "${batch[@]}"
|
||||
|
||||
# Unified chain: deny → Port ACL (close/open/implicit) → allow → default_action
|
||||
# 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).
|
||||
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; }'
|
||||
else
|
||||
nft add chain "$table" "$name" input '{ type filter hook input priority 0; policy accept; }'
|
||||
nft add chain "$table" "$name" forward '{ type filter hook forward priority 0; policy accept; }'
|
||||
fi
|
||||
nft add chain "$table" "$name" prerouting '{ type filter hook prerouting priority mangle; policy accept; }'
|
||||
|
||||
nft add rule "$table" "$name" input ct state established,related counter accept
|
||||
nft add rule "$table" "$name" input iif lo counter accept
|
||||
if [[ "$PORT_HITS_ENABLED" -eq 1 ]]; then
|
||||
# TCP/UDP: learn (ip, proto, dport) then drop; other L4: plain drop.
|
||||
if ! nft add rule "$table" "$name" input \
|
||||
ip saddr @deny_v4 meta l4proto '{ tcp, udp }' \
|
||||
update @deny_port_hits '{ ip saddr . meta l4proto . th dport }' \
|
||||
counter drop 2>>"$LOG_FILE"; then
|
||||
log "nft: port-hit deny rule failed — fallback to plain deny drop"
|
||||
PORT_HITS_ENABLED=0
|
||||
nft add rule "$table" "$name" input ip saddr @deny_v4 counter drop
|
||||
else
|
||||
nft add rule "$table" "$name" input ip saddr @deny_v4 counter drop
|
||||
fi
|
||||
else
|
||||
nft add rule "$table" "$name" input ip saddr @deny_v4 counter drop
|
||||
fi
|
||||
# Port ACL before L3 allow so open+list is exclusive (implicit drop per open port).
|
||||
nft add rule "$table" "$name" forward ct state established,related counter accept
|
||||
nft add rule "$table" "$name" prerouting ct state established,related counter accept
|
||||
|
||||
nft_add_deny_on_chain "$table" "$name" input
|
||||
nft_add_deny_on_chain "$table" "$name" forward
|
||||
nft_add_deny_on_chain "$table" "$name" prerouting
|
||||
|
||||
apply_nft_port_acl "$table" "$name"
|
||||
|
||||
nft add rule "$table" "$name" input ip saddr @allow_v4 counter accept
|
||||
nft add rule "$table" "$name" forward ip saddr @allow_v4 counter accept
|
||||
if [[ "$DEFAULT_ACTION" == "drop" ]]; then
|
||||
nft add rule "$table" "$name" input counter drop
|
||||
nft add rule "$table" "$name" forward counter drop
|
||||
else
|
||||
nft add rule "$table" "$name" input counter accept
|
||||
nft add rule "$table" "$name" forward counter accept
|
||||
fi
|
||||
KERNEL_METHOD=nft
|
||||
APPLIED=$((${#deny_v4[@]} + ${#allow_v4[@]}))
|
||||
}
|
||||
|
||||
# Deny set (+ optional port-hits) on a filter chain. Mutates PORT_HITS_ENABLED on fallback.
|
||||
nft_add_deny_on_chain() {
|
||||
local table=$1 name=$2 chain=$3
|
||||
if [[ "$PORT_HITS_ENABLED" -eq 1 ]]; then
|
||||
if ! nft add rule "$table" "$name" "$chain" \
|
||||
ip saddr @deny_v4 meta l4proto '{ tcp, udp }' \
|
||||
update @deny_port_hits '{ ip saddr . meta l4proto . th dport }' \
|
||||
counter drop 2>>"$LOG_FILE"; then
|
||||
log "nft: port-hit deny on $chain failed — fallback to plain deny drop"
|
||||
PORT_HITS_ENABLED=0
|
||||
nft add rule "$table" "$name" "$chain" ip saddr @deny_v4 counter drop
|
||||
else
|
||||
nft add rule "$table" "$name" "$chain" ip saddr @deny_v4 counter drop
|
||||
fi
|
||||
else
|
||||
nft add rule "$table" "$name" "$chain" ip saddr @deny_v4 counter drop
|
||||
fi
|
||||
}
|
||||
|
||||
# Apply desired L4 port open/close rules from PORT_RULES_FILE (apply_version 3).
|
||||
apply_nft_port_acl() {
|
||||
local table=$1 name=$2
|
||||
@@ -499,6 +523,7 @@ try:
|
||||
except Exception:
|
||||
rules = []
|
||||
safe_id = re.compile(r"[^a-zA-Z0-9_]")
|
||||
CHAINS = ("prerouting", "input", "forward")
|
||||
|
||||
def parse_rule(r):
|
||||
rid = safe_id.sub("_", str(r.get("id") or "x"))[:40]
|
||||
@@ -523,12 +548,8 @@ def parse_rule(r):
|
||||
"is_all": any(c in ("0.0.0.0/0", "0.0.0.0") for c in cidrs),
|
||||
}
|
||||
|
||||
def emit(p):
|
||||
verdict = "drop" if p["action"] == "close" else "accept"
|
||||
comment = f"evofw-port-{p['rid']}"
|
||||
proto, dport = p["proto"], p["dport"]
|
||||
def emit_set(p):
|
||||
if p["is_all"]:
|
||||
print(f'nft add rule inet evofw input {proto} dport {dport} counter {verdict} comment "{comment}"')
|
||||
return
|
||||
setname = f"port_src_{p['rid']}_{p['proto']}"
|
||||
print(f"nft add set inet evofw {setname} '{{ type ipv4_addr; flags interval; }}'")
|
||||
@@ -540,27 +561,46 @@ def emit(p):
|
||||
chunk = []
|
||||
if chunk:
|
||||
print(f"nft add element inet evofw {setname} '{{ {', '.join(chunk)} }}'")
|
||||
|
||||
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"]
|
||||
if p["is_all"]:
|
||||
print(
|
||||
f'nft add rule inet evofw {chain} {proto} dport {dport} counter {verdict} comment "{comment}"'
|
||||
)
|
||||
return
|
||||
setname = f"port_src_{p['rid']}_{p['proto']}"
|
||||
print(
|
||||
f'nft add rule inet evofw input ip saddr @{setname} {proto} dport {dport} counter {verdict} comment "{comment}"'
|
||||
f'nft add rule inet evofw {chain} ip saddr @{setname} {proto} dport {dport} counter {verdict} comment "{comment}"'
|
||||
)
|
||||
|
||||
parsed = [p for p in (parse_rule(r) for r in rules) if p]
|
||||
closes = [p for p in parsed if p["action"] == "close"]
|
||||
opens = [p for p in parsed if p["action"] != "close"]
|
||||
for p in closes:
|
||||
emit(p)
|
||||
for p in opens:
|
||||
emit(p)
|
||||
seen = set()
|
||||
for p in opens:
|
||||
key = (p["proto"], p["dport"])
|
||||
if key in seen:
|
||||
seen_sets = set()
|
||||
for p in closes + opens:
|
||||
key = (p["rid"], p["proto"])
|
||||
if key in seen_sets:
|
||||
continue
|
||||
seen.add(key)
|
||||
comment = f"evofw-port-implicit-{p['proto']}-{p['dport']}"
|
||||
print(
|
||||
f'nft add rule inet evofw input {p["proto"]} dport {p["dport"]} counter drop comment "{comment}"'
|
||||
)
|
||||
seen_sets.add(key)
|
||||
emit_set(p)
|
||||
for chain in CHAINS:
|
||||
for p in closes:
|
||||
emit_rule(p, chain)
|
||||
for p in opens:
|
||||
emit_rule(p, chain)
|
||||
seen = set()
|
||||
for p in opens:
|
||||
key = (p["proto"], p["dport"])
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
comment = f"evofw-port-implicit-{p['proto']}-{p['dport']}"
|
||||
print(
|
||||
f'nft add rule inet evofw {chain} {p["proto"]} dport {p["dport"]} counter drop comment "{comment}"'
|
||||
)
|
||||
PY
|
||||
local cmd
|
||||
while IFS= read -r cmd; do
|
||||
|
||||
@@ -514,7 +514,9 @@ export function AgentPortAcl({ agentId }: AgentPortAclProps) {
|
||||
}, [evofwRules, systemRows])
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (ownerFilter === 'all') return allRows
|
||||
if (ownerFilter === 'all') {
|
||||
return allRows.filter((r) => !(r.owner === 'system' && r.overridden))
|
||||
}
|
||||
return allRows.filter((r) => r.owner === ownerFilter)
|
||||
}, [allRows, ownerFilter])
|
||||
|
||||
@@ -687,8 +689,8 @@ export function AgentPortAcl({ agentId }: AgentPortAclProps) {
|
||||
<div className="flex flex-col gap-px">
|
||||
<FrameTitle>Port ACL</FrameTitle>
|
||||
<FrameDescription>
|
||||
Open по списку делает порт whitelist (остальные src — drop).
|
||||
Системные порты с хоста можно переопределить. Apply через nft
|
||||
Open по списку — порт только с этих IP (хост и Docker).
|
||||
Системные порты можно переопределить. Apply через nft
|
||||
(upgrade install-ссылкой).
|
||||
</FrameDescription>
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -115,8 +115,8 @@ 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 (после deny, **до** L3 allow): close drop → open accept (`comment "evofw-port-<id>"`) → **implicit drop** для каждого `(proto, dport)` с хотя бы одним `open` (`comment "evofw-port-implicit-…"`). Порт с open становится whitelist: src из правила — accept, остальные внешние — drop. `lo` и `ct established,related` по-прежнему выше по цепочке.
|
||||
- UI: tab **Port ACL** (DataGrid + Sheet create/edit + Import). Owner: **EvoFW** (desired) и **system** (listeners + foreign allow с хоста). Системный порт можно переопределить → создаётся desired `open` (список/CIDR); ufw/iptables не меняются.
|
||||
- nft apply: close drop → open accept (`comment "evofw-port-<id>"`) → **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 (прочий трафик идёт дальше).
|
||||
- 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
|
||||
|
||||
Мутация Port ACL бампит `policy_generation` → agent re-apply. Нужен self-update скрипта или re-run install-ссылки.
|
||||
|
||||
@@ -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` drop, `open` accept, затем implicit drop для портов с open) → allow → `default_action`
|
||||
- На 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`.
|
||||
- Exact overlap: `allow \ deny` (`conflicts_dropped`); deny wins
|
||||
- Overrides, смена наборов, `default_action`, Port ACL и refresh DNS/lists бампят `policy_generation`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user