#!/bin/bash
###############################################################################
# firewall_engine.sh — Engine Node 669427 firewalld Configuration
#
# Replaces: firewall.rules (single-zone legacy)
# Architecture: Per-tunnel zones (NET-02 compliant)
# Default zone: drop (deny all inbound)
#
# Zones:
#   zone-mesh  (wg7)      — SSH, keyserv relay [10.69.69.0/24]
#   zone-comp  (compass)  — Challenge-response  [172.28.47.0/28]
#   zone-grav  (gravel)   — DNS management       [172.29.11.0/30]
#   zone-lant  (lantern)  — Dashboard API        [172.28.41.0/30]
#   zone-rain  (rain)     — Payment relay        [172.23.83.0/30]
#   zone-thrd  (thread)   — AI inference relay   [172.28.45.0/30]
#   zone-umbr  (umbrella) — Secondary relay      [10.83.47.0/30]
#   zone-forge (forge)    — Telemetry API        [172.28.50.0/28]
#
# Public (eth0): WG UDP listen ports only; everything else DROPped
# Rich rules: Monero RPC, Tron signer, Ollama, MariaDB — loopback/mesh only
#
# Idempotent — safe to re-run. All zone creation uses 2>/dev/null || true.
###############################################################################
set -euo pipefail

readonly NODE_ID="669427"
readonly NODE_ROLE="ENGINE"
readonly WG_IP="10.69.69.6"
readonly SCRIPT_NAME="$(basename "$0")"
readonly LOG_TAG="FIREWALL-${NODE_ID}"

# ─── Logging ────────────────────────────────────────────────────────────────
log()  { echo "[$(date -Iseconds)] [${LOG_TAG}] $1"; }
warn() { echo "[$(date -Iseconds)] [${LOG_TAG}] [WARN] $1" >&2; }
die()  { echo "[$(date -Iseconds)] [${LOG_TAG}] [FATAL] $1" >&2; exit 1; }

# ─── Pre-flight ─────────────────────────────────────────────────────────────
log "========================================="
log " Engine Firewall Configuration"
log " Node: ${NODE_ID} (${NODE_ROLE})"
log " WG IP: ${WG_IP}"
log " Script: ${SCRIPT_NAME}"
log "========================================="

if [[ "$(id -u)" -ne 0 ]]; then
    die "Must run as root"
fi

if ! command -v firewall-cmd &>/dev/null; then
    die "firewalld not installed — install with: dnf install -y firewalld"
fi

if ! systemctl is-active --quiet firewalld; then
    log "firewalld not running — starting..."
    systemctl enable firewalld
    systemctl start firewalld
    sleep 2
    if ! systemctl is-active --quiet firewalld; then
        die "Failed to start firewalld"
    fi
    log "firewalld started successfully"
else
    log "firewalld is running"
fi

# ─── Helper: create or reset zone ───────────────────────────────────────────
create_zone() {
    local zone="$1"
    firewall-cmd --permanent --new-zone="${zone}" 2>/dev/null || true
    log "Zone '${zone}' ensured"
}

# ─── Helper: add port to zone (idempotent) ──────────────────────────────────
add_port() {
    local zone="$1" port="$2" proto="${3:-tcp}"
    firewall-cmd --permanent --zone="${zone}" --add-port="${port}/${proto}" 2>/dev/null || true
    log "  Port ${port}/${proto} -> ${zone}"
}

# ─── Helper: add source to zone (idempotent) ────────────────────────────────
add_source() {
    local zone="$1" src="$2"
    firewall-cmd --permanent --zone="${zone}" --add-source="${src}" 2>/dev/null || true
    log "  Source ${src} -> ${zone}"
}

# ─── Helper: assign interface to zone (check existence first) ───────────────
assign_iface() {
    local zone="$1" iface="$2"
    if ip link show "${iface}" &>/dev/null 2>&1; then
        # Remove from any existing zone first
        firewall-cmd --permanent --zone="$(firewall-cmd --get-zone-of-interface="${iface}" 2>/dev/null || echo 'none')" \
            --remove-interface="${iface}" 2>/dev/null || true
        firewall-cmd --permanent --zone="${zone}" --add-interface="${iface}" 2>/dev/null || true
        log "  Interface ${iface} -> ${zone}"
    else
        warn "Interface '${iface}' does not exist (tunnel not up yet?) — zone ${zone} configured without it"
    fi
}

# ─── Helper: add rich rule (idempotent) ──────────────────────────────────────
add_rich_rule() {
    local zone="$1" rule="$2"
    firewall-cmd --permanent --zone="${zone}" --add-rich-rule="${rule}" 2>/dev/null || true
    log "  Rich rule -> ${zone}: ${rule}"
}

# ─── Step 1: Set default zone to DROP ───────────────────────────────────────
log ""
log ">>> Step 1: Setting default zone to 'drop'"
firewall-cmd --set-default-zone=drop
log "Default zone set to: $(firewall-cmd --get-default-zone)"

# ─── Step 2: Clean existing zones (remove old legacy wireguard zone) ────────
log ""
log ">>> Step 2: Cleaning legacy configuration"
# Remove the old single 'wireguard' zone if it exists
if firewall-cmd --permanent --get-zones | grep -qw "wireguard"; then
    # Remove any interfaces first
    for iface in wg7 auth-provider; do
        firewall-cmd --permanent --zone=wireguard --remove-interface="${iface}" 2>/dev/null || true
    done
    firewall-cmd --permanent --delete-zone=wireguard 2>/dev/null || true
    log "Removed legacy 'wireguard' zone"
fi

# Remove any services/ports from public zone that shouldn't be there
for svc in ssh http https; do
    firewall-cmd --permanent --zone=public --remove-service="${svc}" 2>/dev/null || true
done
log "Cleaned public zone services"

# ─── Step 3: Create per-tunnel zones ────────────────────────────────────────
log ""
log ">>> Step 3: Creating per-tunnel zones"

# Define all zones with their config:
# FORMAT: zone|interface|source_cidr|ports_tcp (comma-separated, or 'none')
declare -a ZONE_DEFS=(
    "zone-mesh|wg7|10.69.69.0/24|9922,8302"
    "zone-comp|compass|172.28.47.0/28|none"
    "zone-grav|gravel|172.29.11.0/30|none"
    "zone-lant|lantern|172.28.41.0/30|8301"
    "zone-rain|rain|172.23.83.0/30|none"
    "zone-thrd|thread|172.28.45.0/30|none"
    "zone-umbr|umbrella|10.83.47.0/30|none"
    "zone-forge|forge|172.28.50.0/28|8301"
)

for zdef in "${ZONE_DEFS[@]}"; do
    IFS='|' read -r zone iface src ports <<< "${zdef}"

    log ""
    log "--- Zone: ${zone} (iface=${iface}, src=${src}) ---"
    create_zone "${zone}"

    # Set zone target to ACCEPT — traffic matching source/iface is allowed on opened ports
    firewall-cmd --permanent --zone="${zone}" --set-target=default 2>/dev/null || true

    # Assign source
    add_source "${zone}" "${src}"

    # Assign interface
    assign_iface "${zone}" "${iface}"

    # Add TCP ports
    if [[ "${ports}" != "none" ]]; then
        IFS=',' read -ra port_list <<< "${ports}"
        for p in "${port_list[@]}"; do
            add_port "${zone}" "${p}" "tcp"
        done
    fi
done

# ─── Step 4: Loopback zone — allow SSH on lo ────────────────────────────────
log ""
log ">>> Step 4: Configuring loopback"
# SSH on loopback is handled by zone-mesh via source match (127.0.0.0/8 not needed —
# SSH only listens on WG IP per system rules). Ensure lo is in trusted zone.
firewall-cmd --permanent --zone=trusted --add-interface=lo 2>/dev/null || true
log "  lo -> trusted zone"

# ─── Step 5: Public interface — WG UDP listen ports only ────────────────────
log ""
log ">>> Step 5: Configuring public interface (eth0)"

# Create a dedicated public-facing zone for WG endpoint ports
create_zone "zone-pub" 2>/dev/null || true
firewall-cmd --permanent --zone="zone-pub" --set-target=DROP 2>/dev/null || true

# Assign eth0 if it exists (might be ens3, ens192, etc.)
PUBLIC_IFACE=""
for candidate in eth0 ens3 ens192 ens18 enp1s0; do
    if ip link show "${candidate}" &>/dev/null 2>&1; then
        PUBLIC_IFACE="${candidate}"
        break
    fi
done

if [[ -n "${PUBLIC_IFACE}" ]]; then
    # Remove from drop zone first (default zone)
    firewall-cmd --permanent --zone=drop --remove-interface="${PUBLIC_IFACE}" 2>/dev/null || true
    firewall-cmd --permanent --zone="zone-pub" --add-interface="${PUBLIC_IFACE}" 2>/dev/null || true
    log "  Public interface: ${PUBLIC_IFACE} -> zone-pub"
else
    warn "No public interface found (eth0/ens3/ens192/ens18/enp1s0) — zone-pub has no interface assigned"
fi

# WireGuard tunnel establishment ports (UDP)
declare -a WG_UDP_PORTS=(41820 42783 53287 57196 57417 41938 44619 45247)
for port in "${WG_UDP_PORTS[@]}"; do
    add_port "zone-pub" "${port}" "udp"
done
log "  ${#WG_UDP_PORTS[@]} WG UDP listen ports configured"

# ─── Step 6: Rich rules — service lockdown ──────────────────────────────────
log ""
log ">>> Step 6: Configuring rich rules for service lockdown"

# 18082/tcp (Monero RPC) — ONLY loopback + mesh
add_rich_rule "zone-mesh" \
    'rule family="ipv4" source address="10.69.69.0/24" port protocol="tcp" port="18082" accept'
add_rich_rule "trusted" \
    'rule family="ipv4" source address="127.0.0.1/32" port protocol="tcp" port="18082" accept'

# 9090/tcp (Tron signer) — ONLY loopback
add_rich_rule "trusted" \
    'rule family="ipv4" source address="127.0.0.1/32" port protocol="tcp" port="9090" accept'

# 11434/tcp (Ollama) — ONLY loopback
add_rich_rule "trusted" \
    'rule family="ipv4" source address="127.0.0.1/32" port protocol="tcp" port="11434" accept'

# 3306/tcp (MariaDB) — ONLY loopback + mesh
add_rich_rule "zone-mesh" \
    'rule family="ipv4" source address="10.69.69.0/24" port protocol="tcp" port="3306" accept'
add_rich_rule "trusted" \
    'rule family="ipv4" source address="127.0.0.1/32" port protocol="tcp" port="3306" accept'

# Explicit reject on drop zone for probed ports (returns RST instead of timing out)
for deny_port in 18082 9090 11434 3306; do
    add_rich_rule "drop" \
        "rule family=\"ipv4\" port protocol=\"tcp\" port=\"${deny_port}\" reject"
done

# ─── Step 7: Reload firewalld ───────────────────────────────────────────────
log ""
log ">>> Step 7: Reloading firewalld"
firewall-cmd --reload
log "firewalld reloaded successfully"

# ─── Step 8: Status report ──────────────────────────────────────────────────
log ""
log "========================================="
log " FIREWALL STATUS REPORT — NODE ${NODE_ID}"
log "========================================="
log ""

log "Default zone: $(firewall-cmd --get-default-zone)"
log "Active zones:"
firewall-cmd --get-active-zones 2>/dev/null | while IFS= read -r line; do
    log "  ${line}"
done

log ""
log "--- Per-Zone Detail ---"

ALL_ZONES=("zone-mesh" "zone-comp" "zone-grav" "zone-lant" "zone-rain" "zone-thrd" "zone-umbr" "zone-forge" "zone-pub" "trusted" "drop")
for z in "${ALL_ZONES[@]}"; do
    if firewall-cmd --info-zone="${z}" &>/dev/null; then
        log ""
        log "=== ${z} ==="
        firewall-cmd --info-zone="${z}" 2>/dev/null | while IFS= read -r line; do
            log "  ${line}"
        done
    fi
done

# ─── Verify critical constraints ────────────────────────────────────────────
log ""
log "--- Constraint Verification ---"

ERRORS=0

# Verify default zone is drop
if [[ "$(firewall-cmd --get-default-zone)" != "drop" ]]; then
    warn "CONSTRAINT FAIL: Default zone is NOT 'drop'!"
    ERRORS=$((ERRORS + 1))
else
    log "[OK] Default zone is 'drop'"
fi

# Verify SSH not on public/drop
if firewall-cmd --zone=drop --list-ports 2>/dev/null | grep -q "9922/tcp"; then
    warn "CONSTRAINT FAIL: SSH port 9922 found in drop zone!"
    ERRORS=$((ERRORS + 1))
else
    log "[OK] SSH (9922) not exposed on drop zone"
fi

if firewall-cmd --zone=zone-pub --list-ports 2>/dev/null | grep -q "9922/tcp"; then
    warn "CONSTRAINT FAIL: SSH port 9922 found in zone-pub!"
    ERRORS=$((ERRORS + 1))
else
    log "[OK] SSH (9922) not exposed on public zone"
fi

# Verify SSH IS on mesh
if firewall-cmd --zone=zone-mesh --list-ports 2>/dev/null | grep -q "9922/tcp"; then
    log "[OK] SSH (9922) available on zone-mesh (WG+loopback only)"
else
    warn "CONSTRAINT FAIL: SSH port 9922 NOT found in zone-mesh!"
    ERRORS=$((ERRORS + 1))
fi

# Verify zone count
ACTIVE_COUNT=$(firewall-cmd --get-active-zones 2>/dev/null | grep -c "^[a-z]" || echo 0)
log "[INFO] Active zones: ${ACTIVE_COUNT}"

log ""
if [[ ${ERRORS} -eq 0 ]]; then
    log "========================================="
    log " FIREWALL CONFIGURATION COMPLETE"
    log " All constraints passed (${ERRORS} errors)"
    log " Node: ${NODE_ID} | Default: drop"
    log " Zones: ${#ZONE_DEFS[@]} tunnel + pub + trusted"
    log "========================================="
else
    warn "========================================="
    warn " FIREWALL CONFIGURED WITH ${ERRORS} ERRORS"
    warn " Review output above and fix manually"
    warn "========================================="
    exit 1
fi
