#!/usr/bin/env bash
# =========================================================
# OpenVPN Server Installer for CentOS 7
# helper.sh edition
# =========================================================
# Features
# - CentOS 7 only
# - systemd single config source
# - Username/password auth
# - Fixed IP support via CCD
# - Online user query / forced disconnect
# - Optional DNS push
# - Post-install self-check with ovpn-check
# - Pushes 192.168.30.0/24 to clients
# - SNATs VPN clients to the server LAN so 192.168.30.0/24
#   devices can reply without adding static routes
# =========================================================

set -euo pipefail

VPN_NET="${1:-10.6.6.0}"
VPN_MASK="255.255.255.0"
DNS_IP="${2:-}"
DOMAIN="${3:-}"

LAN_NET="192.168.30.0"
LAN_MASK="255.255.255.0"
LAN_CIDR="192.168.30.0/24"
LAN_GW="192.168.30.1"
EXPECTED_LAN_IP="192.168.30.110"

OPENVPN_PORT="1194"
STATUS_FILE="/var/log/openvpn-status.log"
LOG_FILE="/var/log/openvpn.log"
SERVER_CONF_DIR="/etc/openvpn/server"
SERVER_CONF="${SERVER_CONF_DIR}/server.conf"
AUTH_SCRIPT="/etc/openvpn/check_user.sh"
USER_FILE="/etc/openvpn/userpass.txt"
CCD_DIR="/etc/openvpn/ccd"
MGMT_HOST="127.0.0.1"
MGMT_PORT="7505"

err() {
  echo "ERROR: $*" >&2
  exit 1
}

note() {
  echo "==> $*"
}

require_root() {
  [[ "$(id -u)" -eq 0 ]] || err "Please run this script as root."
}

require_centos7() {
  [[ -f /etc/centos-release ]] || err "CentOS 7 is required."
  rpm -E '%{rhel}' | grep -qx '7' || err "This installer is tailored for CentOS 7."
}

detect_lan() {
  LAN_IFACE="$(ip -4 route get "${LAN_GW}" | awk '/dev/ {for (i = 1; i <= NF; i++) if ($i == "dev") { print $(i + 1); exit }}')"
  LAN_SRC_IP="$(ip -4 route get "${LAN_GW}" | awk '/src/ {for (i = 1; i <= NF; i++) if ($i == "src") { print $(i + 1); exit }}')"

  [[ -n "${LAN_IFACE:-}" ]] || err "Unable to detect the LAN interface for ${LAN_GW}."
  [[ -n "${LAN_SRC_IP:-}" ]] || err "Unable to detect the source IP used to reach ${LAN_GW}."

  if [[ "${LAN_SRC_IP}" != "${EXPECTED_LAN_IP}" ]]; then
    echo "WARN: detected LAN source IP ${LAN_SRC_IP}, expected ${EXPECTED_LAN_IP}."
    echo "WARN: continuing anyway, but this script is written for the 192.168.30.0/24 environment."
  fi
}

install_packages() {
  note "Installing dependencies"
  yum install -y epel-release
  yum install -y openvpn easy-rsa firewalld policycoreutils-python nc
}

setup_pki() {
  if [[ -f /etc/openvpn/ca.crt && -f /etc/openvpn/server.crt && -f /etc/openvpn/server.key && -f /etc/openvpn/dh.pem ]]; then
    note "Existing OpenVPN PKI assets found, skipping certificate generation"
    mkdir -p "${CCD_DIR}" "${SERVER_CONF_DIR}"
    return
  fi

  note "Generating PKI assets"
  mkdir -p /etc/openvpn/easy-rsa
  cp -r /usr/share/easy-rsa/3/* /etc/openvpn/easy-rsa/
  cd /etc/openvpn/easy-rsa

  export EASYRSA_BATCH=1
  export EASYRSA_REQ_CN="helper-sh-openvpn-ca"

  ./easyrsa init-pki
  ./easyrsa build-ca nopass
  EASYRSA_REQ_CN="server" ./easyrsa gen-req server nopass
  EASYRSA_REQ_CN="server" ./easyrsa sign-req server server
  ./easyrsa gen-dh

  install -m 644 pki/ca.crt /etc/openvpn/ca.crt
  install -m 644 pki/issued/server.crt /etc/openvpn/server.crt
  install -m 600 pki/private/server.key /etc/openvpn/server.key
  install -m 644 pki/dh.pem /etc/openvpn/dh.pem

  mkdir -p "${CCD_DIR}" "${SERVER_CONF_DIR}"
}

write_server_conf() {
  note "Writing OpenVPN server configuration"
  cat > "${SERVER_CONF}" <<EOF
port ${OPENVPN_PORT}
proto udp
dev tun
topology subnet

server ${VPN_NET} ${VPN_MASK}
client-to-client
push "route ${LAN_NET} ${LAN_MASK}"

ca /etc/openvpn/ca.crt
cert /etc/openvpn/server.crt
key /etc/openvpn/server.key
dh /etc/openvpn/dh.pem

script-security 3
auth-user-pass-verify ${AUTH_SCRIPT} via-env
verify-client-cert none
username-as-common-name

cipher AES-256-GCM
ncp-ciphers AES-256-GCM:AES-128-GCM
auth SHA256

keepalive 10 60
persist-key
persist-tun
explicit-exit-notify 1

client-config-dir ${CCD_DIR}
status ${STATUS_FILE}
status-version 2
log-append ${LOG_FILE}
management ${MGMT_HOST} ${MGMT_PORT}
verb 3
EOF

  if [[ -n "${DNS_IP}" ]]; then
    cat >> "${SERVER_CONF}" <<EOF
push "dhcp-option DNS ${DNS_IP}"
EOF
    if [[ -n "${DOMAIN}" ]]; then
      cat >> "${SERVER_CONF}" <<EOF
push "dhcp-option DOMAIN ${DOMAIN}"
EOF
    fi
  fi

  cp "${SERVER_CONF}" /etc/openvpn/server.conf
}

write_auth_script() {
  note "Installing username/password validation script"
  cat > "${AUTH_SCRIPT}" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

USER_FILE="/etc/openvpn/userpass.txt"
USER="$(printf '%s' "${username:-}" | tr -d '\r' | xargs)"
PASS="$(printf '%s' "${password:-}" | tr -d '\r' | xargs)"

[[ -n "${USER}" && -n "${PASS}" ]] || exit 1
grep -Fqx "${USER}:${PASS}" "${USER_FILE}"
EOF

  chmod 700 "${AUTH_SCRIPT}"
  touch "${USER_FILE}"
  chmod 600 "${USER_FILE}"

  restorecon -Rv /etc/openvpn >/dev/null 2>&1 || true
  if command -v getsebool >/dev/null 2>&1 && getsebool openvpn_run_unconfined >/dev/null 2>&1; then
    setsebool -P openvpn_run_unconfined 1 >/dev/null 2>&1 || true
  fi
}

ensure_direct_rule() {
  local table="$1"
  local chain="$2"
  local priority="$3"
  shift 3

  local rule_text="$*"
  if ! firewall-cmd --permanent --direct --get-all-rules | grep -Fq "ipv4 ${table} ${chain} ${priority} ${rule_text}"; then
    firewall-cmd --permanent --direct --add-rule ipv4 "${table}" "${chain}" "${priority}" "$@"
  fi
}

configure_networking() {
  note "Configuring firewalld and IP forwarding"
  systemctl enable firewalld --now

  firewall-cmd --permanent --add-port="${OPENVPN_PORT}/udp"
  firewall-cmd --permanent --add-masquerade

  ensure_direct_rule filter FORWARD 0 -s "${VPN_NET}/24" -d "${LAN_CIDR}" -j ACCEPT
  ensure_direct_rule filter FORWARD 0 -s "${LAN_CIDR}" -d "${VPN_NET}/24" -m state --state ESTABLISHED,RELATED -j ACCEPT
  ensure_direct_rule nat POSTROUTING 0 -s "${VPN_NET}/24" -d "${LAN_CIDR}" -o "${LAN_IFACE}" -j MASQUERADE

  firewall-cmd --reload

  sysctl -w net.ipv4.ip_forward=1 >/dev/null
  if grep -q '^net.ipv4.ip_forward' /etc/sysctl.conf; then
    sed -i 's/^net\.ipv4\.ip_forward.*/net.ipv4.ip_forward = 1/' /etc/sysctl.conf
  else
    echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf
  fi
}

write_user_tool() {
  note "Installing ovpn-user helper"
  cat > /usr/local/bin/ovpn-user <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

USER_FILE="/etc/openvpn/userpass.txt"
CCD_DIR="/etc/openvpn/ccd"
STATUS_FILE="/var/log/openvpn-status.log"
MGMT_HOST="127.0.0.1"
MGMT_PORT="7505"

err() {
  echo "ERROR: $*" >&2
  exit 1
}

mgmt_cmd() {
  local cmd="$1"
  exec 3<>"/dev/tcp/${MGMT_HOST}/${MGMT_PORT}" || return 1
  if command -v timeout >/dev/null 2>&1; then
    timeout 1 cat <&3 >/dev/null 2>&1 || true
  else
    sleep 1
  fi
  printf '%s\n' "${cmd}" >&3
  printf 'quit\n' >&3
  if command -v timeout >/dev/null 2>&1; then
    timeout 2 cat <&3 || true
  else
    cat <&3 || true
  fi
  exec 3<&-
  exec 3>&-
}

case "${1:-}" in
  add)
    u="${2:-}"
    p="${3:-}"
    ip="${4:-}"
    [[ -n "${u}" ]] || err "Username is required."
    [[ -n "${p}" ]] || err "Password is required."
    [[ ! "${u}" =~ [:[:space:]] ]] || err "Username cannot contain spaces or colon."
    [[ ! "${p}" =~ [:[:space:]] ]] || err "Password cannot contain spaces or colon."
    grep -q "^${u}:" "${USER_FILE}" && err "User already exists."

    echo "${u}:${p}" >> "${USER_FILE}"
    chmod 600 "${USER_FILE}"
    if [[ -n "${ip}" ]]; then
      echo "ifconfig-push ${ip} 255.255.255.0" > "${CCD_DIR}/${u}"
    fi
    echo "OK: user [${u}] added."
    ;;
  del)
    u="${2:-}"
    [[ -n "${u}" ]] || err "Username is required."
    sed -i "/^${u}:/d" "${USER_FILE}"
    rm -f "${CCD_DIR}/${u}"
    echo "OK: user [${u}] deleted."
    ;;
  list)
    cut -d: -f1 "${USER_FILE}" | sed '/^$/d'
    ;;
  online)
    u="${2:-}"
    [[ -n "${u}" ]] || err "Username is required."
    out="$(awk -F, -v user="${u}" '$1=="CLIENT_LIST" && $2==user {printf "ONLINE public_ip=%s vpn_ip=%s connected_since=%s\n", $3, $4, $8}' "${STATUS_FILE}" || true)"
    [[ -n "${out}" ]] && echo "${out}" || echo "OFFLINE: user [${u}] is not connected."
    ;;
  kick)
    u="${2:-}"
    [[ -n "${u}" ]] || err "Username is required."
    out="$(mgmt_cmd "kill ${u}" || true)"
    if printf '%s' "${out}" | grep -Eq 'SUCCESS: client.*kill|SUCCESS: client-kill'; then
      echo "OK: user [${u}] disconnected."
    else
      echo "INFO: no active session found for [${u}]."
    fi
    ;;
  *)
    echo "Usage: ovpn-user add|del|list|online|kick"
    echo "  ovpn-user add USER PASS [VPN_IP]"
    echo "  ovpn-user del USER"
    echo "  ovpn-user list"
    echo "  ovpn-user online USER"
    echo "  ovpn-user kick USER"
    exit 1
    ;;
esac
EOF
  chmod +x /usr/local/bin/ovpn-user
}

write_check_tool() {
  note "Installing ovpn-check helper"
  cat > /usr/local/bin/ovpn-check <<EOF
#!/usr/bin/env bash
set -euo pipefail

echo "=========== OpenVPN self-check ==========="
echo "Service:"
systemctl --no-pager --full status openvpn@server | sed -n '1,12p' || true
echo
echo "UDP listener:"
ss -lunp | grep ":${OPENVPN_PORT} " || echo "ERROR: OpenVPN is not listening on UDP ${OPENVPN_PORT}"
echo
echo "Tunnel interface:"
ip addr show | awk '/tun[0-9]/{flag=1;count=0} flag{print;count++} count==3{flag=0}' || true
echo
echo "Server route push:"
grep '^push "route ' ${SERVER_CONF} || true
echo
echo "LAN reachability target:"
echo "LAN gateway: ${LAN_GW}"
echo "LAN subnet : ${LAN_CIDR}"
echo "LAN iface  : ${LAN_IFACE}"
echo "LAN src ip : ${LAN_SRC_IP}"
echo
echo "Firewalld direct rules:"
firewall-cmd --direct --get-all-rules | grep -E '${VPN_NET}/24|${LAN_CIDR}|${LAN_IFACE}' || true
echo
echo "IP forwarding:"
sysctl net.ipv4.ip_forward || true
echo
echo "Connected clients:"
if [[ -f ${STATUS_FILE} ]]; then
  awk -F, '/CLIENT_LIST/{printf "%s vpn_ip=%s public_ip=%s\n", \$2, \$4, \$3}' ${STATUS_FILE}
else
  echo "No status file yet."
fi
echo "=========== Self-check complete ==========="
EOF
  chmod +x /usr/local/bin/ovpn-check
}

start_service() {
  note "Enabling and restarting OpenVPN"
  systemctl enable openvpn@server
  systemctl restart openvpn@server
}

main() {
  require_root
  require_centos7
  detect_lan

  echo "================ OpenVPN install ================"
  echo "VPN subnet : ${VPN_NET}/24"
  echo "VPN gateway: ${VPN_NET%.*}.1"
  echo "DNS push   : ${DNS_IP:-disabled}"
  echo "LAN subnet : ${LAN_CIDR}"
  echo "LAN gw     : ${LAN_GW}"
  echo "LAN iface  : ${LAN_IFACE}"
  echo "LAN src ip : ${LAN_SRC_IP}"
  echo "================================================="

  install_packages
  setup_pki
  write_server_conf
  write_auth_script
  configure_networking
  write_user_tool
  write_check_tool
  start_service

  sleep 2
  echo
  note "Running post-install self-check"
  /usr/local/bin/ovpn-check
  echo
  echo "OK: OpenVPN server installation completed."
  echo "Commands: ovpn-user / ovpn-check"
  echo "Clients connected to this VPN will be able to reach ${LAN_CIDR}"
  echo "through route push plus SNAT on ${LAN_IFACE}."
}

main "$@"
