diff --git a/apps/api/src/agent-scripts/evofw-firewall.sh b/apps/api/src/agent-scripts/evofw-firewall.sh index c979e86..1ddb62a 100644 --- a/apps/api/src/agent-scripts/evofw-firewall.sh +++ b/apps/api/src/agent-scripts/evofw-firewall.sh @@ -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 diff --git a/apps/web/src/components/agents/agent-port-acl.tsx b/apps/web/src/components/agents/agent-port-acl.tsx index 3d8a65c..4ecc437 100644 --- a/apps/web/src/components/agents/agent-port-acl.tsx +++ b/apps/web/src/components/agents/agent-port-acl.tsx @@ -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) {