#!/usr/bin/env bash
set -euo pipefail

# ============================================================
# Fail2Ban + iptables: protection SYN flood sur HTTPS (443)
# - iptables LOG prefix: "F2B-HTTPS "
# - fail2ban filter: iptables-https
# - jail: iptables-https (logpath=/var/log/syslog)
# - option test (hping3) pour vérifier que ça ban
# ============================================================

# ---- CONFIG ----
PORT="443"
LOG_PREFIX="F2B-HTTPS "
# Seuil SYN flood: au-delà de 30 SYN/sec (burst 60), on LOG + DROP
HASHLIMIT_RATE="30/second"
HASHLIMIT_BURST="60"

# Fail2ban
JAIL_NAME="iptables-https"
BANTIME="86400"
FINDTIME="60"
MAXRETRY="5"
IGNOREIP_DEFAULT="127.0.0.1/8 ::1"

# Fichiers
FILTER_FILE="/etc/fail2ban/filter.d/${JAIL_NAME}.conf"
JAIL_FILE="/etc/fail2ban/jail.d/${JAIL_NAME}.conf"

# ---- helpers ----
need_root() {
  if [[ "${EUID}" -ne 0 ]]; then
    echo "? Lance ce script en root: sudo $0"
    exit 1
  fi
}

info() { echo "? $*"; }
warn() { echo "??  $*"; }
err()  { echo "? $*" >&2; }

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

# Remove existing rules matching our signature (best-effort)
cleanup_iptables_rules() {
  # Delete rules containing our log-prefix or hashlimit-name https-syn
  # We loop because there can be multiple duplicates.
  while iptables -S INPUT | grep -q "${LOG_PREFIX}"; do
    # Find rule line number and delete by spec (easier with -D + exact rule)
    # We'll attempt to delete by matching common patterns.
    iptables -D INPUT -p tcp --syn --dport "${PORT}" \
      -m hashlimit --hashlimit-name https-syn --hashlimit-above "${HASHLIMIT_RATE}" --hashlimit-burst "${HASHLIMIT_BURST}" \
      --hashlimit-mode srcip --hashlimit-srcmask 32 \
      -j LOG --log-prefix "${LOG_PREFIX}" --log-level 4 2>/dev/null || true

    iptables -D INPUT -p tcp --syn --dport "${PORT}" \
      -m hashlimit --hashlimit-name https-syn --hashlimit-above "${HASHLIMIT_RATE}" --hashlimit-burst "${HASHLIMIT_BURST}" \
      --hashlimit-mode srcip --hashlimit-srcmask 32 \
      -j DROP 2>/dev/null || true
  done

  # Also try removing connlimit style rules if they exist (legacy from earlier)
  iptables -D INPUT -p tcp --dport "${PORT}" -m connlimit --connlimit-above 40 -j DROP 2>/dev/null || true
  iptables -D INPUT -p tcp --dport "${PORT}" -m connlimit --connlimit-above 40 -j LOG --log-prefix "${LOG_PREFIX}" --log-level 4 2>/dev/null || true
}

install_packages() {
  info "Installation paquets: fail2ban (+ iptables persistant) + hping3 (test)"
  export DEBIAN_FRONTEND=noninteractive
  apt-get update -y
  apt-get install -y fail2ban iptables-persistent hping3 >/dev/null
  systemctl enable fail2ban >/dev/null || true
}

write_filter() {
  info "Création filtre Fail2Ban: ${FILTER_FILE}"
  cat > "${FILTER_FILE}" <<EOF
[Definition]
failregex = ^.*F2B-HTTPS .*SRC=<HOST>.*\$
ignoreregex =
EOF
}

write_jail() {
  info "Création jail Fail2Ban: ${JAIL_FILE}"
  cat > "${JAIL_FILE}" <<EOF
[${JAIL_NAME}]
enabled  = true
filter   = ${JAIL_NAME}
logpath  = /var/log/syslog
backend  = auto

maxretry = ${MAXRETRY}
findtime = ${FINDTIME}
bantime  = ${BANTIME}

action   = iptables[name=${JAIL_NAME}, port="${PORT}", protocol=tcp]
ignoreip = ${IGNOREIP_DEFAULT}
EOF
}

setup_sysctl_syn_protection() {
  info "Activation protections kernel de base (syncookies + backlog)"
  # Safe defaults
  sysctl -w net.ipv4.tcp_syncookies=1 >/dev/null
  sysctl -w net.ipv4.tcp_max_syn_backlog=8192 >/dev/null
  sysctl -w net.ipv4.tcp_synack_retries=2 >/dev/null
  sysctl -w net.ipv4.tcp_syn_retries=3 >/dev/null

  # Persist (idempotent append with marker)
  local SYSCTL_SNIPPET="/etc/sysctl.d/99-https-synflood.conf"
  cat > "${SYSCTL_SNIPPET}" <<'EOF'
# HTTPS SYN flood hardening
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 3
EOF
  sysctl --system >/dev/null || true
}

setup_iptables_syn_hashlimit() {
  info "Mise en place iptables: LOG + DROP sur SYN flood port ${PORT}"

  cleanup_iptables_rules

  # Insert at top: LOG then DROP when above threshold
  iptables -I INPUT 1 -p tcp --syn --dport "${PORT}" \
    -m hashlimit --hashlimit-name https-syn --hashlimit-above "${HASHLIMIT_RATE}" --hashlimit-burst "${HASHLIMIT_BURST}" \
    --hashlimit-mode srcip --hashlimit-srcmask 32 \
    -j LOG --log-prefix "${LOG_PREFIX}" --log-level 4

  iptables -I INPUT 2 -p tcp --syn --dport "${PORT}" \
    -m hashlimit --hashlimit-name https-syn --hashlimit-above "${HASHLIMIT_RATE}" --hashlimit-burst "${HASHLIMIT_BURST}" \
    --hashlimit-mode srcip --hashlimit-srcmask 32 \
    -j DROP

  # Save iptables
  if have_cmd netfilter-persistent; then
    netfilter-persistent save >/dev/null || true
  else
    iptables-save > /etc/iptables/rules.v4
  fi
}

restart_fail2ban() {
  info "Redémarrage Fail2Ban"
  systemctl restart fail2ban
  systemctl status fail2ban --no-pager -l | sed -n '1,20p'
  info "Jails actives:"
  fail2ban-client status | sed -n '1,120p'
  info "Status jail ${JAIL_NAME}:"
  fail2ban-client status "${JAIL_NAME}" | sed -n '1,120p'
}

show_current_rules() {
  info "Top règles iptables INPUT (12 premières lignes)"
  iptables -vnL INPUT --line-numbers | head -n 12
}

test_mode() {
  # Mode test optionnel: met maxretry=1 temporairement, flood local 2 sec, vérifie ban, puis restore.
  warn "MODE TEST: va déclencher un flood local et peut bannir l'IP locale."
  warn "Si tu ne veux pas, réponds NON."
  read -r -p "Lancer le test maintenant ? (OUI/NON) " ans
  if [[ "${ans}" != "OUI" ]]; then
    info "Test ignoré."
    return 0
  fi

  info "Test: passage temporaire en maxretry=1 (ban immédiat)"
  # Backup jail file
  cp -a "${JAIL_FILE}" "${JAIL_FILE}.bak.$(date +%s)"

  # Edit values
  sed -i 's/^maxretry = .*/maxretry = 1/' "${JAIL_FILE}"
  systemctl restart fail2ban

  # Determine local primary IP (best-effort)
  LOCAL_IP="$(hostname -I | awk '{print $1}')"
  if [[ -z "${LOCAL_IP}" ]]; then
    LOCAL_IP="127.0.0.1"
  fi
  warn "Flood sur ${LOCAL_IP}:${PORT} (2 secondes) via hping3"
  timeout 2s hping3 -S -p "${PORT}" --flood "${LOCAL_IP}" >/dev/null 2>&1 || true

  info "Dernières lignes syslog F2B-HTTPS:"
  grep "F2B-HTTPS" /var/log/syslog | tail -n 5 || true

  info "Status jail après test:"
  fail2ban-client status "${JAIL_NAME}" | sed -n '1,160p' || true

  info "Restauration config jail (maxretry=${MAXRETRY})"
  # Restore from backup
  mv -f "${JAIL_FILE}.bak."* "${JAIL_FILE}" 2>/dev/null || true
  # (si plusieurs backups, on garde le dernier — c'est ok)
  sed -i 's/^maxretry = .*/maxretry = '"${MAXRETRY}"'/' "${JAIL_FILE}" || true

  systemctl restart fail2ban
  info "Test terminé."
}

main() {
  need_root

  info "=== Setup Fail2Ban + iptables SYN flood protection for HTTPS ==="
  install_packages
  setup_sysctl_syn_protection
  setup_iptables_syn_hashlimit
  write_filter
  write_jail
  restart_fail2ban
  show_current_rules
  test_mode

  info "? Terminé. En prod, garde maxretry=${MAXRETRY}, findtime=${FINDTIME}, bantime=${BANTIME}."
  info "Pour voir les logs: sudo grep 'F2B-HTTPS' /var/log/syslog | tail"
  info "Pour voir les bans: sudo fail2ban-client status ${JAIL_NAME}"
}

main "$@"