#!/usr/bin/env bash
# acronis_firewall_allow.sh - detect CSF / Imunify360 / firewalld / UFW / iptables, then apply/revert Acronis firewall rules.
#
# Rules applied by this script:
#   1) Allow outgoing TCP ports: 443, 8443, 7770-7800, 44445
#      - CSF uses TCP_OUT in /etc/csf/csf.conf for these ports.
#   2) Whitelist 117.120.8.238 on all ports/protocols
#
# Priority / Policy:
#   1) If CSF is ACTIVE -> add rules to CSF only.
#   2) Else if Imunify360 is ACTIVE -> add rules to Imunify360 only.
#   3) Else if firewalld and/or UFW is ACTIVE -> add rules to whichever is active (both if both).
#   4) Else (no active firewalls) -> stage rules to ALL INSTALLED higher-level firewalls (CSF, Imunify360, firewalld, UFW) AND apply iptables (if installed).
#
# On --revert, iptables is ALWAYS reverted if installed (regardless of higher-level firewall state).
# Section divider "----" prints only between batches, and only when >1 batch runs.
#
# Targets: CentOS 7, Debian 10, Ubuntu 20 -> AlmaLinux 9, Ubuntu 25, Debian 13.

set -euo pipefail

# ---- Config -----------------------------------------------------------------
STATE_DIR="/var/lib/acronis_firewall"
STATE_UFW="$STATE_DIR/ufw.list"
STATE_IPT="$STATE_DIR/iptables.list"
STATE_FWLD="$STATE_DIR/firewalld.list"
STATE_IMUNIFY="$STATE_DIR/imunify360.list"
STATE_CSF="$STATE_DIR/csf_tcp_out.original"
LOG_FILE="${LOG_FILE:-/var/log/acronis_firewall.log}"

TRUSTED_IP="117.120.8.238"
TCP_OUT_PORTS=(443 8443 7770:7800 44445)

# Expanded only for backends that do not reliably accept port ranges in their rule files.
TCP_OUT_PORTS_EXPANDED=(443 8443 {7770..7800} 44445)

# Names across backends
FW_IPSET="acronis-trusted-set"  # firewalld ipset name for the trusted host
IPT_IN_CHAIN="ACRONIS-IN"       # iptables dedicated inbound chain
IPT_OUT_CHAIN="ACRONIS-OUT"     # iptables dedicated outbound chain
RULE_COMMENT="ACRONIS-AUTO"

# ---- Flags ------------------------------------------------------------------
DRY_RUN=0
REVERT=0
VERBOSE=0

usage() {
  cat <<USAGE
Usage: $0 [--dry-run] [--revert] [--verbose]
USAGE
}

for arg in "$@"; do
  case "$arg" in
    --dry-run) DRY_RUN=1 ;;
    --revert)  REVERT=1 ;;
    --verbose) VERBOSE=1 ;;
    -h|--help) usage; exit 0 ;;
    *) echo "Unknown argument: $arg"; usage; exit 1 ;;
  esac
done

# ---- Logging ----------------------------------------------------------------
log() {
  local line="$*"
  printf '%s\n' "$line"
  printf '%s %s\n' "$(date +'%F %T')" "$line" >> "$LOG_FILE" 2>/dev/null || true
}
vlog() { [[ $VERBOSE -eq 1 ]] && log "$@"; }

ensure_root() { [[ ${EUID:-$(id -u)} -eq 0 ]] || { echo "Run as root." >&2; exit 1; }; }
init_paths() { mkdir -p "$(dirname "$LOG_FILE")" "$STATE_DIR"; : > "$LOG_FILE"; }

# ---- Utilities ---------------------------------------------------------------
have() { command -v "$1" >/dev/null 2>&1; }

join_by_comma() {
  local IFS=,
  printf '%s' "$*"
}

# Active checks (strict CSF: do not treat lfd as active CSF)
csf_active() {
  if have systemctl; then
    systemctl is-active --quiet csf && return 0
  fi
  if have csf; then
    local o
    o="$(csf -l 2>&1 || true)"
    grep -qi 'csf is not running' <<<"$o" && return 1
    grep -qiE 'ACCEPT|DROP|CHAIN|iptables' <<<"$o" && return 0
  fi
  return 1
}
ufw_active()       { have ufw && ufw status 2>/dev/null | grep -qE '^Status:\s+active'; }
firewalld_active() { have firewall-cmd && systemctl is-active --quiet firewalld; }
imunify360_active() {
  have imunify360-agent || return 1
  if have systemctl; then
    systemctl is-active --quiet imunify360 2>/dev/null && return 0
  fi
  imunify360-agent rstatus >/dev/null 2>&1 && return 0
  return 1
}

# Installed checks
csf_installed()       { have csf || [[ -f /etc/csf/csf.conf ]]; }
ufw_installed()       { have ufw; }
firewalld_installed() { have firewall-cmd || have firewall-offline-cmd; }
imunify360_installed() { have imunify360-agent; }

# Version helpers for summary
ver_csf() {
  if [[ -r /etc/csf/version.txt ]]; then
    cat /etc/csf/version.txt 2>/dev/null || true
  elif have csf; then
    csf -v 2>/dev/null | head -n1 | sed 's/^csf: *//' || true
  fi
}
ver_ufw()       { ufw --version 2>/dev/null | head -n1 | sed 's/^ufw *//' || true; }
ver_firewalld() { firewall-cmd --version 2>/dev/null | sed 's/^firewall-cmd *//' || true; }
ver_imunify360() { imunify360-agent version 2>/dev/null | head -n1 || imunify360-agent --version 2>/dev/null | head -n1 || true; }
ver_iptables()  { iptables --version 2>/dev/null | sed 's/^iptables *//' || true; }

# Backend-scoped state helpers
load_applied_file()   { [[ -f "$1" ]] && cat "$1" || true; }
record_applied_file() { mkdir -p "$STATE_DIR"; printf '%s\n' "$@" > "$1"; }
truncate_file()       { : > "$1"; }

save_iptables_rules() {
  if have netfilter-persistent; then
    netfilter-persistent save || true
  else
    local ipt_save_tool="iptables-save"
    iptables -V 2>&1 | grep -q 'legacy' && ipt_save_tool="iptables-legacy-save"
    if have "$ipt_save_tool"; then
      mkdir -p /etc/iptables
      "$ipt_save_tool" > /etc/iptables/rules.v4 2>>"$LOG_FILE" || true
    fi
  fi
}

# ---- CSF Apply/Revert --------------------------------------------------------
csf_update_tcp_out() {
  local cfg="/etc/csf/csf.conf"
  local current="" new="" p="" tmp=""

  if [[ ! -f "$cfg" ]]; then
    echo "CSF config not found at $cfg"
    return 1
  fi

  current="$(awk -F'"' '/^[[:space:]]*TCP_OUT[[:space:]]*=/ {print $2; exit}' "$cfg")"
  new="$current"

  for p in "${TCP_OUT_PORTS[@]}"; do
    if ! tr ',' '\n' <<< "$new" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -Fxq "$p"; then
      new="${new:+$new,}$p"
    fi
  done

  if [[ "$new" == "$current" ]]; then
    echo "CSF TCP_OUT already contains required outbound ports: $(join_by_comma "${TCP_OUT_PORTS[@]}")"
    return 0
  fi

  if [[ ! -f "$STATE_CSF" ]]; then
    if grep -qE '^[[:space:]]*TCP_OUT[[:space:]]*=' "$cfg"; then
      printf '%s\n' "$current" > "$STATE_CSF"
    else
      printf '%s\n' '__MISSING__' > "$STATE_CSF"
    fi
  fi

  tmp="$(mktemp)"
  if grep -qE '^[[:space:]]*TCP_OUT[[:space:]]*=' "$cfg"; then
    awk -v new="$new" '
      /^[[:space:]]*TCP_OUT[[:space:]]*=/ && !done {
        sub(/"[^"]*"/, "\"" new "\"")
        done=1
      }
      { print }
    ' "$cfg" > "$tmp"
  else
    cat "$cfg" > "$tmp"
    printf '\nTCP_OUT = "%s"\n' "$new" >> "$tmp"
  fi
  cat "$tmp" > "$cfg"
  rm -f "$tmp"

  echo "Updated CSF TCP_OUT: $new"
}

csf_restore_tcp_out() {
  local cfg="/etc/csf/csf.conf"
  local original="" tmp=""

  [[ -f "$STATE_CSF" ]] || { echo "CSF TCP_OUT: no saved original value to restore"; return 0; }
  [[ -f "$cfg" ]] || { echo "CSF config not found at $cfg"; return 1; }

  original="$(cat "$STATE_CSF")"
  tmp="$(mktemp)"

  if [[ "$original" == "__MISSING__" ]]; then
    awk '!/^[[:space:]]*TCP_OUT[[:space:]]*=/' "$cfg" > "$tmp"
    cat "$tmp" > "$cfg"
    echo "Removed CSF TCP_OUT line that was added by this script"
  else
    if grep -qE '^[[:space:]]*TCP_OUT[[:space:]]*=' "$cfg"; then
      awk -v original="$original" '
        /^[[:space:]]*TCP_OUT[[:space:]]*=/ && !done {
          sub(/"[^"]*"/, "\"" original "\"")
          done=1
        }
        { print }
      ' "$cfg" > "$tmp"
    else
      cat "$cfg" > "$tmp"
      printf '\nTCP_OUT = "%s"\n' "$original" >> "$tmp"
    fi
    cat "$tmp" > "$cfg"
    echo "Restored CSF TCP_OUT: $original"
  fi

  rm -f "$tmp" "$STATE_CSF"
}

csf_apply() {
  local csf_acr="/etc/csf/acronis.allow" csf_allow="/etc/csf/csf.allow"
  if ! csf_active; then echo "CSF inactive - Adding Acronis allow rules anyway..."; fi

  if [[ $DRY_RUN -eq 1 ]]; then
    echo "[DRY-RUN] add $(join_by_comma "${TCP_OUT_PORTS[@]}") to TCP_OUT in /etc/csf/csf.conf"
    echo "[DRY-RUN] write trusted host $TRUSTED_IP to $csf_acr + include in $csf_allow"
    return 0
  fi

  csf_update_tcp_out

  local tmp; tmp="$(mktemp)"
  {
    echo "# Managed by acronis_firewall_allow.sh"
    echo "# Whitelist trusted host on all ports/protocols"
    echo "$TRUSTED_IP"
  } > "$tmp"

  install -D -m 0644 "$tmp" "$csf_acr"
  rm -f "$tmp"

  echo "Adding managed rule include to csf.allow..."
  grep -Fxq "Include $csf_acr" "$csf_allow" 2>/dev/null || echo "Include $csf_acr" >> "$csf_allow"
  if csf_active; then csf -ra >/dev/null || true; fi
}

csf_revert() {
  local csf_acr="/etc/csf/acronis.allow" csf_allow="/etc/csf/csf.allow"
  if [[ $DRY_RUN -eq 1 ]]; then
    echo "[DRY-RUN] restore previous TCP_OUT from $STATE_CSF"
    echo "[DRY-RUN] remove Include $csf_acr from $csf_allow and remove $csf_acr"
    return 0
  fi
  echo "Reverting Acronis CSF allow rules..."
  csf_restore_tcp_out
  sed -i '\|^Include /etc/csf/acronis.allow$|d' "$csf_allow" 2>/dev/null || true
  rm -f "$csf_acr"
  if csf_active; then echo "Reloading CSF rules..."; csf -ra >/dev/null || true; fi
}

# ---- Imunify360 Apply/Revert -------------------------------------------------
imunify360_apply() {
  local applied=()
  local comment="Managed by acronis_firewall_allow.sh"

  if ! imunify360_active; then
    log "Imunify360 inactive - Adding Acronis allow rules anyway..."
  fi

  if [[ $DRY_RUN -eq 1 ]]; then
    echo "[DRY-RUN] imunify360-agent ip-list local add --purpose white $TRUSTED_IP --comment \"$comment\" --full-access"
    for p in "${TCP_OUT_PORTS_EXPANDED[@]}"; do
      echo "[DRY-RUN] imunify360-agent blocked-port delete $p:tcp"
    done
    return 0
  fi

  echo "Applying Acronis Imunify360 allow rules..."
  imunify360-agent ip-list local add --purpose white "$TRUSTED_IP" --comment "$comment" --full-access >/dev/null 2>&1 || true
  echo "Applied: Imunify360 white list $TRUSTED_IP with full access"
  applied+=("white-full-access:$TRUSTED_IP")

  # Imunify360 does not act like a normal outbound firewall. This only ensures
  # these TCP ports are not present in Imunify360's blocked-port list.
  for p in "${TCP_OUT_PORTS_EXPANDED[@]}"; do
    imunify360-agent blocked-port delete "$p:tcp" >/dev/null 2>&1 || true
    echo "Ensured unblocked in Imunify360: TCP port $p"
    applied+=("unblocked-tcp-port:$p")
  done

  record_applied_file "$STATE_IMUNIFY" "${applied[@]}"
}

imunify360_revert() {
  if ! have imunify360-agent; then
    echo "Imunify360: not installed - nothing to revert"
    return 0
  fi

  local prev; prev="$(load_applied_file "$STATE_IMUNIFY")"
  if [[ -z "$prev" ]]; then
    log "Imunify360: nothing to revert"
    return 0
  fi

  echo "Reverting Acronis Imunify360 allow rules..."
  while read -r rule; do
    [[ -z "$rule" ]] && continue
    if [[ "$rule" == white-full-access:* ]]; then
      local ip="${rule#white-full-access:}"
      if [[ $DRY_RUN -eq 1 ]]; then
        echo "[DRY-RUN] imunify360-agent ip-list local delete --purpose white $ip"
      else
        imunify360-agent ip-list local delete --purpose white "$ip" >/dev/null 2>&1 || true
        echo "Reverted: Imunify360 white list $ip"
      fi
    elif [[ "$rule" == unblocked-tcp-port:* ]]; then
      local port="${rule#unblocked-tcp-port:}"
      echo "Left unchanged: TCP port $port was only removed from Imunify360 blocked-port list and is not re-blocked on revert"
    fi
  done <<< "$prev"

  [[ $DRY_RUN -eq 0 ]] && truncate_file "$STATE_IMUNIFY"
}

# ---- UFW Apply/Revert --------------------------------------------------------
ufw_apply() {
  local applied=() active=0; ufw_active && active=1
  if (( ! active )); then log "UFW inactive - Adding Acronis allow rules anyway..."; fi

  for p in "${TCP_OUT_PORTS[@]}"; do
    if [[ $DRY_RUN -eq 1 ]]; then
      echo "[DRY-RUN] ufw allow out proto tcp to any port $p"
    else
      ufw allow out proto tcp to any port "$p" >/dev/null 2>&1 || true
      echo "Applied: ufw allow out proto tcp to any port $p"
      applied+=("out-tcp-port:$p")
    fi
  done

  if [[ $DRY_RUN -eq 1 ]]; then
    echo "[DRY-RUN] ufw allow from $TRUSTED_IP"
    echo "[DRY-RUN] ufw allow out to $TRUSTED_IP"
  else
    ufw allow from "$TRUSTED_IP" >/dev/null 2>&1 || true
    ufw allow out to "$TRUSTED_IP" >/dev/null 2>&1 || true
    echo "Applied: ufw allow from $TRUSTED_IP"
    echo "Applied: ufw allow out to $TRUSTED_IP"
    applied+=("from:$TRUSTED_IP" "out-to:$TRUSTED_IP")
  fi

  if [[ $DRY_RUN -eq 0 ]]; then
    (( active )) && ufw reload >/dev/null 2>&1 || true
    record_applied_file "$STATE_UFW" "${applied[@]}"
  fi
}

ufw_revert() {
  local prev; prev="$(load_applied_file "$STATE_UFW")"
  [[ -z "$prev" ]] && { log "UFW: nothing to revert"; return 0; }
  echo "Reverting Acronis UFW allow rules..."

  while read -r rule; do
    [[ -z "$rule" ]] && continue
    if [[ "$rule" == out-tcp-port:* ]]; then
      local p="${rule#out-tcp-port:}"
      if [[ $DRY_RUN -eq 1 ]]; then
        echo "[DRY-RUN] ufw delete allow out proto tcp to any port $p"
      else
        ufw delete allow out proto tcp to any port "$p" >/dev/null 2>&1 || true
        echo "Reverted: outbound TCP port $p"
      fi
    elif [[ "$rule" == from:* ]]; then
      local ip="${rule#from:}"
      if [[ $DRY_RUN -eq 1 ]]; then
        echo "[DRY-RUN] ufw delete allow from $ip"
      else
        ufw delete allow from "$ip" >/dev/null 2>&1 || true
        echo "Reverted: allow from $ip"
      fi
    elif [[ "$rule" == out-to:* ]]; then
      local ip="${rule#out-to:}"
      if [[ $DRY_RUN -eq 1 ]]; then
        echo "[DRY-RUN] ufw delete allow out to $ip"
      else
        ufw delete allow out to "$ip" >/dev/null 2>&1 || true
        echo "Reverted: allow out to $ip"
      fi
    fi
  done <<< "$prev"

  if [[ $DRY_RUN -eq 0 ]]; then truncate_file "$STATE_UFW"; ufw_active && ufw reload >/dev/null 2>&1 || true; fi
}

# ---- firewalld Apply/Revert --------------------------------------------------
firewalld_add_direct_rule() {
  local permanent="$1"; shift
  if [[ "$permanent" == "yes" ]]; then
    firewall-cmd --permanent --direct --add-rule "$@" >/dev/null 2>&1 || true
  else
    firewall-cmd --direct --add-rule "$@" >/dev/null 2>&1 || true
  fi
}

firewalld_remove_direct_rule() {
  local permanent="$1"; shift
  if [[ "$permanent" == "yes" ]]; then
    firewall-cmd --permanent --direct --remove-rule "$@" >/dev/null 2>&1 || true
  else
    firewall-cmd --direct --remove-rule "$@" >/dev/null 2>&1 || true
  fi
}

firewalld_offline_add_direct_rule() {
  firewall-offline-cmd --direct --add-rule "$@" >/dev/null 2>&1 || true
}

firewalld_offline_remove_direct_rule() {
  firewall-offline-cmd --direct --remove-rule "$@" >/dev/null 2>&1 || true
}

firewalld_apply() {
  local applied=()
  local multiports; multiports="$(join_by_comma "${TCP_OUT_PORTS[@]}")"

  if firewalld_active; then
    if [[ $DRY_RUN -eq 1 ]]; then
      echo "[DRY-RUN] firewalld direct OUTPUT allow tcp dports $multiports"
      echo "[DRY-RUN] firewalld direct INPUT/OUTPUT allow $TRUSTED_IP on all protocols"
    else
      echo "Applying Acronis firewalld allow rules..."
      firewalld_add_direct_rule no  ipv4 filter OUTPUT 0 -p tcp -m multiport --dports "$multiports" -j ACCEPT
      firewalld_add_direct_rule yes ipv4 filter OUTPUT 0 -p tcp -m multiport --dports "$multiports" -j ACCEPT
      firewalld_add_direct_rule no  ipv4 filter INPUT  0 -s "$TRUSTED_IP" -j ACCEPT
      firewalld_add_direct_rule yes ipv4 filter INPUT  0 -s "$TRUSTED_IP" -j ACCEPT
      firewalld_add_direct_rule no  ipv4 filter OUTPUT 0 -d "$TRUSTED_IP" -j ACCEPT
      firewalld_add_direct_rule yes ipv4 filter OUTPUT 0 -d "$TRUSTED_IP" -j ACCEPT
      firewall-cmd --reload >/dev/null || true
      applied+=("direct-output-tcp:$multiports" "direct-input-src:$TRUSTED_IP" "direct-output-dst:$TRUSTED_IP")
    fi
    [[ $DRY_RUN -eq 0 ]] && record_applied_file "$STATE_FWLD" "${applied[@]}"
  else
    if ! have firewall-offline-cmd; then
      log "firewalld inactive - cannot stage rules (firewall-offline-cmd not available)"
      return 0
    fi

    log "firewalld inactive - Adding Acronis allow rules anyway"
    if [[ $DRY_RUN -eq 1 ]]; then
      echo "[DRY-RUN] firewall-offline-cmd direct OUTPUT allow tcp dports $multiports"
      echo "[DRY-RUN] firewall-offline-cmd direct INPUT/OUTPUT allow $TRUSTED_IP on all protocols"
    else
      firewalld_offline_add_direct_rule ipv4 filter OUTPUT 0 -p tcp -m multiport --dports "$multiports" -j ACCEPT
      firewalld_offline_add_direct_rule ipv4 filter INPUT  0 -s "$TRUSTED_IP" -j ACCEPT
      firewalld_offline_add_direct_rule ipv4 filter OUTPUT 0 -d "$TRUSTED_IP" -j ACCEPT
      echo "Staged: firewalld direct OUTPUT allow tcp dports $multiports"
      echo "Staged: firewalld direct INPUT/OUTPUT allow $TRUSTED_IP"
      applied+=("direct-output-tcp:$multiports" "direct-input-src:$TRUSTED_IP" "direct-output-dst:$TRUSTED_IP")
      record_applied_file "$STATE_FWLD" "${applied[@]}"
    fi
  fi
}

firewalld_revert() {
  local multiports; multiports="$(join_by_comma "${TCP_OUT_PORTS[@]}")"

  if firewalld_active; then
    if [[ $DRY_RUN -eq 1 ]]; then
      echo "[DRY-RUN] remove firewalld direct OUTPUT tcp dports $multiports"
      echo "[DRY-RUN] remove firewalld direct INPUT/OUTPUT $TRUSTED_IP"
    else
      echo "Reverting Acronis firewalld allow rules..."
      firewalld_remove_direct_rule no  ipv4 filter OUTPUT 0 -p tcp -m multiport --dports "$multiports" -j ACCEPT
      firewalld_remove_direct_rule yes ipv4 filter OUTPUT 0 -p tcp -m multiport --dports "$multiports" -j ACCEPT
      firewalld_remove_direct_rule no  ipv4 filter INPUT  0 -s "$TRUSTED_IP" -j ACCEPT
      firewalld_remove_direct_rule yes ipv4 filter INPUT  0 -s "$TRUSTED_IP" -j ACCEPT
      firewalld_remove_direct_rule no  ipv4 filter OUTPUT 0 -d "$TRUSTED_IP" -j ACCEPT
      firewalld_remove_direct_rule yes ipv4 filter OUTPUT 0 -d "$TRUSTED_IP" -j ACCEPT
      firewall-cmd --reload >/dev/null || true
      truncate_file "$STATE_FWLD"
    fi
  else
    if ! have firewall-offline-cmd; then
      log "firewalld inactive - cannot revert staged rules (firewall-offline-cmd not available)"
      return 0
    fi
    if [[ $DRY_RUN -eq 1 ]]; then
      echo "[DRY-RUN] firewall-offline-cmd remove direct OUTPUT tcp dports $multiports"
      echo "[DRY-RUN] firewall-offline-cmd remove direct INPUT/OUTPUT $TRUSTED_IP"
    else
      firewalld_offline_remove_direct_rule ipv4 filter OUTPUT 0 -p tcp -m multiport --dports "$multiports" -j ACCEPT
      firewalld_offline_remove_direct_rule ipv4 filter INPUT  0 -s "$TRUSTED_IP" -j ACCEPT
      firewalld_offline_remove_direct_rule ipv4 filter OUTPUT 0 -d "$TRUSTED_IP" -j ACCEPT
      truncate_file "$STATE_FWLD"
    fi
  fi
}

# ---- iptables Apply/Revert ---------------------------------------------------
iptables_apply() {
  if ! have iptables; then
    echo "iptables: not installed - skipping"
    return 0
  fi

  local multiports; multiports="$(join_by_comma "${TCP_OUT_PORTS[@]}")"
  log "iptables: Applying Acronis firewall rules"

  if [[ $DRY_RUN -eq 1 ]]; then
    echo "[DRY-RUN] iptables -N $IPT_IN_CHAIN || true"
    echo "[DRY-RUN] iptables -N $IPT_OUT_CHAIN || true"
    echo "[DRY-RUN] iptables -C INPUT -j $IPT_IN_CHAIN || iptables -I INPUT 1 -j $IPT_IN_CHAIN"
    echo "[DRY-RUN] iptables -C OUTPUT -j $IPT_OUT_CHAIN || iptables -I OUTPUT 1 -j $IPT_OUT_CHAIN"
    echo "[DRY-RUN] iptables -A $IPT_OUT_CHAIN -p tcp -m multiport --dports $multiports -j ACCEPT -m comment --comment $RULE_COMMENT"
    echo "[DRY-RUN] iptables -A $IPT_IN_CHAIN -s $TRUSTED_IP -j ACCEPT -m comment --comment $RULE_COMMENT"
    echo "[DRY-RUN] iptables -A $IPT_OUT_CHAIN -d $TRUSTED_IP -j ACCEPT -m comment --comment $RULE_COMMENT"
    return 0
  fi

  iptables -N "$IPT_IN_CHAIN" 2>/dev/null || true
  iptables -N "$IPT_OUT_CHAIN" 2>/dev/null || true
  iptables -C INPUT -j "$IPT_IN_CHAIN" 2>/dev/null || iptables -I INPUT 1 -j "$IPT_IN_CHAIN"
  iptables -C OUTPUT -j "$IPT_OUT_CHAIN" 2>/dev/null || iptables -I OUTPUT 1 -j "$IPT_OUT_CHAIN"

  iptables -C "$IPT_OUT_CHAIN" -p tcp -m multiport --dports "$multiports" -j ACCEPT -m comment --comment "$RULE_COMMENT" 2>/dev/null \
    || iptables -A "$IPT_OUT_CHAIN" -p tcp -m multiport --dports "$multiports" -j ACCEPT -m comment --comment "$RULE_COMMENT"
  iptables -C "$IPT_IN_CHAIN" -s "$TRUSTED_IP" -j ACCEPT -m comment --comment "$RULE_COMMENT" 2>/dev/null \
    || iptables -A "$IPT_IN_CHAIN" -s "$TRUSTED_IP" -j ACCEPT -m comment --comment "$RULE_COMMENT"
  iptables -C "$IPT_OUT_CHAIN" -d "$TRUSTED_IP" -j ACCEPT -m comment --comment "$RULE_COMMENT" 2>/dev/null \
    || iptables -A "$IPT_OUT_CHAIN" -d "$TRUSTED_IP" -j ACCEPT -m comment --comment "$RULE_COMMENT"

  echo "Applied: outbound TCP ports $multiports"
  echo "Applied: trusted host $TRUSTED_IP on all ports/protocols"
  record_applied_file "$STATE_IPT" "out-tcp-ports:$multiports" "input-src:$TRUSTED_IP" "output-dst:$TRUSTED_IP"
  save_iptables_rules
}

iptables_revert() {
  if ! have iptables; then
    echo "iptables: not installed - nothing to revert"
    return 0
  fi

  local multiports; multiports="$(join_by_comma "${TCP_OUT_PORTS[@]}")"
  echo "Reverting Acronis iptables rules..."

  if iptables -S "$IPT_OUT_CHAIN" >/dev/null 2>&1; then
    if iptables -C "$IPT_OUT_CHAIN" -p tcp -m multiport --dports "$multiports" -j ACCEPT -m comment --comment "$RULE_COMMENT" 2>/dev/null; then
      if [[ $DRY_RUN -eq 1 ]]; then
        echo "[DRY-RUN] iptables -D $IPT_OUT_CHAIN -p tcp -m multiport --dports $multiports -j ACCEPT -m comment --comment $RULE_COMMENT"
      else
        iptables -D "$IPT_OUT_CHAIN" -p tcp -m multiport --dports "$multiports" -j ACCEPT -m comment --comment "$RULE_COMMENT"
      fi
    fi
    if iptables -C "$IPT_OUT_CHAIN" -d "$TRUSTED_IP" -j ACCEPT -m comment --comment "$RULE_COMMENT" 2>/dev/null; then
      if [[ $DRY_RUN -eq 1 ]]; then
        echo "[DRY-RUN] iptables -D $IPT_OUT_CHAIN -d $TRUSTED_IP -j ACCEPT -m comment --comment $RULE_COMMENT"
      else
        iptables -D "$IPT_OUT_CHAIN" -d "$TRUSTED_IP" -j ACCEPT -m comment --comment "$RULE_COMMENT"
      fi
    fi
  fi

  if iptables -S "$IPT_IN_CHAIN" >/dev/null 2>&1; then
    if iptables -C "$IPT_IN_CHAIN" -s "$TRUSTED_IP" -j ACCEPT -m comment --comment "$RULE_COMMENT" 2>/dev/null; then
      if [[ $DRY_RUN -eq 1 ]]; then
        echo "[DRY-RUN] iptables -D $IPT_IN_CHAIN -s $TRUSTED_IP -j ACCEPT -m comment --comment $RULE_COMMENT"
      else
        iptables -D "$IPT_IN_CHAIN" -s "$TRUSTED_IP" -j ACCEPT -m comment --comment "$RULE_COMMENT"
      fi
    fi
  fi

  for base_chain in INPUT OUTPUT; do
    local custom_chain="$IPT_IN_CHAIN"
    [[ "$base_chain" == OUTPUT ]] && custom_chain="$IPT_OUT_CHAIN"
    if iptables -C "$base_chain" -j "$custom_chain" 2>/dev/null; then
      if [[ $DRY_RUN -eq 1 ]]; then
        echo "[DRY-RUN] iptables -D $base_chain -j $custom_chain"
      else
        iptables -D "$base_chain" -j "$custom_chain"
      fi
    fi
  done

  for custom_chain in "$IPT_IN_CHAIN" "$IPT_OUT_CHAIN"; do
    if [[ $DRY_RUN -eq 1 ]]; then
      echo "[DRY-RUN] iptables -F $custom_chain || true"
      echo "[DRY-RUN] iptables -X $custom_chain || true"
    else
      iptables -F "$custom_chain" 2>/dev/null || true
      iptables -X "$custom_chain" 2>/dev/null || true
    fi
  done

  if [[ $DRY_RUN -eq 0 ]]; then
    truncate_file "$STATE_IPT"
    save_iptables_rules
  fi
}

# ---- Main -------------------------------------------------------------------
main() {
  ensure_root
  init_paths

  local CSF_ACT=0 IMUNIFY_ACT=0 UFW_ACT=0 FWD_ACT=0
  csf_active        && CSF_ACT=1
  imunify360_active && IMUNIFY_ACT=1
  ufw_active        && UFW_ACT=1
  firewalld_active  && FWD_ACT=1

  local CSF_INST=0 IMUNIFY_INST=0 UFW_INST=0 FWD_INST=0 IPT_INST=0
  csf_installed        && CSF_INST=1
  imunify360_installed && IMUNIFY_INST=1
  ufw_installed        && UFW_INST=1
  firewalld_installed  && FWD_INST=1
  have iptables         && IPT_INST=1

  echo "---- Detected Firewalls ----"
  local printed=0 v=""
  if [[ $CSF_INST -eq 1 ]]; then v="$(ver_csf)";             echo "CSF: ${v:-installed}"; printed=1; fi
  if [[ $IMUNIFY_INST -eq 1 ]]; then v="$(ver_imunify360)"; echo "Imunify360: ${v:-installed}"; printed=1; fi
  if [[ $FWD_INST -eq 1 ]]; then v="$(ver_firewalld)";       echo "firewalld: ${v:-installed}"; printed=1; fi
  if [[ $UFW_INST -eq 1 ]]; then v="$(ver_ufw)";             echo "UFW: ${v:-installed}"; printed=1; fi
  if [[ $IPT_INST -eq 1 ]]; then v="$(ver_iptables)";  echo "iptables: ${v:-installed}"; printed=1; fi
  if [[ $printed -eq 0 ]]; then
    echo "No firewalls detected!"
    echo "----------------------------"
    echo "No changes made. Exiting script..."
    return 0
  fi
  echo "----------------------------"
  printf '%s %s\n' "$(date +'%F %T')" "SUMMARY: detected firewalls printed above" >> "$LOG_FILE" 2>/dev/null || true

  echo
  if [[ $REVERT -eq 1 ]]; then
    echo "---- Reverting firewall allows ----"
  else
    echo "---- Adding firewall allows ----"
    echo "Outbound TCP ports: $(join_by_comma "${TCP_OUT_PORTS[@]}")"
    echo "Trusted host: $TRUSTED_IP (all ports/protocols)"
  fi

  local batches=()

  if [[ $CSF_ACT -eq 1 ]]; then
    batches+=(csf)
  elif [[ $IMUNIFY_ACT -eq 1 ]]; then
    batches+=(imunify360)
  elif [[ $FWD_ACT -eq 1 || $UFW_ACT -eq 1 ]]; then
    [[ $FWD_ACT -eq 1 ]] && batches+=(firewalld)
    [[ $UFW_ACT -eq 1 ]] && batches+=(ufw)
  else
    [[ $CSF_INST -eq 1 ]] && batches+=(csf)
    [[ $IMUNIFY_INST -eq 1 ]] && batches+=(imunify360)
    [[ $FWD_INST -eq 1 ]] && batches+=(firewalld)
    [[ $UFW_INST -eq 1 ]] && batches+=(ufw)
  fi

  local echo_no_ipt=""
  if [[ $REVERT -eq 1 ]]; then
    if [[ $IPT_INST -eq 1 ]]; then
      batches+=(iptables)
    else
      echo_no_ipt="yes"
    fi
  else
    if [[ $CSF_ACT -eq 0 && $IMUNIFY_ACT -eq 0 && $FWD_ACT -eq 0 && $UFW_ACT -eq 0 && $IPT_INST -eq 1 ]]; then
      batches+=(iptables)
    fi
  fi

  local first=1
  for b in "${batches[@]}"; do
    if [[ $first -eq 0 ]]; then echo "----"; else first=0; fi
    case "$b" in
      csf)
        if [[ $REVERT -eq 1 ]]; then csf_revert; else csf_apply; fi
        ;;
      imunify360)
        if [[ $REVERT -eq 1 ]]; then imunify360_revert; else imunify360_apply; fi
        ;;
      firewalld)
        if [[ $REVERT -eq 1 ]]; then firewalld_revert; else firewalld_apply; fi
        ;;
      ufw)
        if [[ $REVERT -eq 1 ]]; then ufw_revert; else ufw_apply; fi
        ;;
      iptables)
        if [[ $REVERT -eq 1 ]]; then
          iptables_revert
        else
          echo "No active CSF/Imunify360/firewalld/UFW detected - Using iptables"
          iptables_apply
        fi
        ;;
    esac
  done

  if [[ "${echo_no_ipt:-}" == "yes" ]]; then
    echo "iptables: not installed - nothing to revert"
  fi

  echo "--------------------------------"
  echo "Done!"
}

main "$@"

