#!/bin/bash
#
# Script to change IKEv2 VPN server address
#
# The latest version of this script is available at:
# https://github.com/hwdsl2/setup-ipsec-vpn
#
# Copyright (C) 2022 Lin Song <linsongui@gmail.com>
#
# This work is licensed under the Creative Commons Attribution-ShareAlike 3.0
# Unported License: http://creativecommons.org/licenses/by-sa/3.0/
#
# Attribution required: please include my name in any derivative and let me
# know how you have improved it!

export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
SYS_DT=$(date +%F-%T | tr ':' '_')

exiterr() { echo "Error: $1" >&2; exit 1; }
bigecho() { echo "## $1"; }

check_ip() {
  IP_REGEX='^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$'
  printf '%s' "$1" | tr -d '\n' | grep -Eq "$IP_REGEX"
}

check_dns_name() {
  FQDN_REGEX='^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
  printf '%s' "$1" | tr -d '\n' | grep -Eq "$FQDN_REGEX"
}

check_root() {
  if [ "$(id -u)" != 0 ]; then
    exiterr "Script must be run as root. Try 'sudo bash $0'"
  fi
}

check_os() {
  os_type=centos
  rh_file="/etc/redhat-release"
  if grep -qs "Red Hat" "$rh_file"; then
    os_type=rhel
  fi
  [ -f /etc/oracle-release ] && os_type=ol
  if grep -qs "release 7" "$rh_file"; then
    os_ver=7
  elif grep -qs "release 8" "$rh_file"; then
    os_ver=8
    grep -qi stream "$rh_file" && os_ver=8s
    grep -qi rocky "$rh_file" && os_type=rocky
    grep -qi alma "$rh_file" && os_type=alma
  elif grep -qs "Amazon Linux release 2" /etc/system-release; then
    os_type=amzn
    os_ver=2
  else
    os_type=$(lsb_release -si 2>/dev/null)
    [ -z "$os_type" ] && [ -f /etc/os-release ] && os_type=$(. /etc/os-release && printf '%s' "$ID")
    case $os_type in
      [Uu]buntu)
        os_type=ubuntu
        ;;
      [Dd]ebian)
        os_type=debian
        ;;
      [Rr]aspbian)
        os_type=raspbian
        ;;
      [Aa]lpine)
        os_type=alpine
        ;;
      *)
cat 1>&2 <<'EOF'
Error: This script only supports one of the following OS:
       Ubuntu, Debian, CentOS/RHEL, Rocky Linux, AlmaLinux,
       Oracle Linux, Amazon Linux 2 or Alpine Linux
EOF
        exit 1
        ;;
    esac
    if [ "$os_type" = "alpine" ]; then
      os_ver=$(. /etc/os-release && printf '%s' "$VERSION_ID" | cut -d '.' -f 1,2)
      if [ "$os_ver" != "3.14" ] && [ "$os_ver" != "3.15" ]; then
        exiterr "This script only supports Alpine Linux 3.14/3.15."
      fi
    else
      os_ver=$(sed 's/\..*//' /etc/debian_version | tr -dc 'A-Za-z0-9')
    fi
  fi
}

check_libreswan() {
  ipsec_ver=$(ipsec --version 2>/dev/null)
  if ( ! grep -qs "hwdsl2 VPN script" /etc/sysctl.conf && ! grep -qs "hwdsl2" /opt/src/run.sh ) \
    || ! printf '%s' "$ipsec_ver" | grep -qi 'libreswan'; then
cat 1>&2 <<'EOF'
Error: This script can only be used with an IPsec server created using:
       https://github.com/hwdsl2/setup-ipsec-vpn
EOF
    exit 1
  fi
}

check_ikev2() {
  if ! grep -qs "conn ikev2-cp" /etc/ipsec.d/ikev2.conf; then
cat 1>&2 <<'EOF'
Error: You must first set up IKEv2 before changing IKEv2 server address.
       See: vpnsetup.net/ikev2
EOF
    exit 1
  fi
}

check_utils_exist() {
  command -v certutil >/dev/null 2>&1 || exiterr "'certutil' not found. Abort."
}

abort_and_exit() {
  echo "Abort. No changes were made." >&2
  exit 1
}

confirm_or_abort() {
  printf '%s' "$1"
  read -r response
  case $response in
    [yY][eE][sS]|[yY])
      echo
      ;;
    *)
      abort_and_exit
      ;;
  esac
}

check_cert_exists() {
  certutil -L -d sql:/etc/ipsec.d -n "$1" >/dev/null 2>&1
}

check_ca_cert_exists() {
  check_cert_exists "IKEv2 VPN CA" || exiterr "Certificate 'IKEv2 VPN CA' does not exist. Abort."
}

get_server_address() {
  server_addr_old=$(grep -s "leftcert=" /etc/ipsec.d/ikev2.conf | cut -f2 -d=)
  check_ip "$server_addr_old" || check_dns_name "$server_addr_old" || exiterr "Could not get current VPN server address."
}

show_welcome() {
cat <<EOF
Welcome! Use this script to change this IKEv2 VPN server's address. A new server
certificate will be generated if necessary.

Current server address: $server_addr_old

EOF
}

get_server_ip() {
  bigecho "Trying to auto discover IP of this server..."
  public_ip=${VPN_PUBLIC_IP:-''}
  check_ip "$public_ip" || public_ip=$(dig @resolver1.opendns.com -t A -4 myip.opendns.com +short)
  check_ip "$public_ip" || public_ip=$(wget -t 3 -T 15 -qO- http://ipv4.icanhazip.com)
}

enter_server_address() {
  echo "Do you want IKEv2 VPN clients to connect to this server using a DNS name,"
  printf "e.g. vpn.example.com, instead of its IP address? [y/N] "
  read -r response
  case $response in
    [yY][eE][sS]|[yY])
      use_dns_name=1
      echo
      ;;
    *)
      use_dns_name=0
      echo
      ;;
  esac
  if [ "$use_dns_name" = "1" ]; then
    read -rp "Enter the DNS name of this VPN server: " server_addr
    until check_dns_name "$server_addr"; do
      echo "Invalid DNS name. You must enter a fully qualified domain name (FQDN)."
      read -rp "Enter the DNS name of this VPN server: " server_addr
    done
  else
    get_server_ip
    echo
    read -rp "Enter the IPv4 address of this VPN server: [$public_ip] " server_addr
    [ -z "$server_addr" ] && server_addr="$public_ip"
    until check_ip "$server_addr"; do
      echo "Invalid IP address."
      read -rp "Enter the IPv4 address of this VPN server: [$public_ip] " server_addr
      [ -z "$server_addr" ] && server_addr="$public_ip"
    done
  fi
}

check_server_address() {
  if [ "$server_addr" = "$server_addr_old" ]; then
    echo >&2
    echo "Error: IKEv2 server address is already '$server_addr'. Nothing to do." >&2
    abort_and_exit
  fi
}

confirm_changes() {
cat <<EOF

You are about to change this IKEv2 VPN server's address.
Read the important notes below before continuing.

===========================================

Current server address: $server_addr_old
New server address:     $server_addr

===========================================

*IMPORTANT*
After running this script, you must manually update the server address
(and remote ID, if applicable) on any existing IKEv2 client devices.
For iOS clients, you'll need to export and re-import client configuration
using the IKEv2 helper script.

EOF
  printf "Do you want to continue? [Y/n] "
  read -r response
  case $response in
    [yY][eE][sS]|[yY]|'')
      echo
      ;;
    *)
      abort_and_exit
      ;;
  esac
}

create_server_cert() {
  if check_cert_exists "$server_addr"; then
    bigecho "Server certificate '$server_addr' already exists, skipping..."
  else
    bigecho "Generating server certificate..."
    if [ "$use_dns_name" = "1" ]; then
      certutil -z <(head -c 1024 /dev/urandom) \
        -S -c "IKEv2 VPN CA" -n "$server_addr" \
        -s "O=IKEv2 VPN,CN=$server_addr" \
        -k rsa -g 3072 -v 120 \
        -d sql:/etc/ipsec.d -t ",," \
        --keyUsage digitalSignature,keyEncipherment \
        --extKeyUsage serverAuth \
        --extSAN "dns:$server_addr" >/dev/null 2>&1 || exiterr "Failed to create server certificate."
    else
      certutil -z <(head -c 1024 /dev/urandom) \
        -S -c "IKEv2 VPN CA" -n "$server_addr" \
        -s "O=IKEv2 VPN,CN=$server_addr" \
        -k rsa -g 3072 -v 120 \
        -d sql:/etc/ipsec.d -t ",," \
        --keyUsage digitalSignature,keyEncipherment \
        --extKeyUsage serverAuth \
        --extSAN "ip:$server_addr,dns:$server_addr" >/dev/null 2>&1 || exiterr "Failed to create server certificate."
    fi
  fi
}

update_ikev2_conf() {
  bigecho "Updating IKEv2 configuration..."
  if ! grep -qs '^include /etc/ipsec\.d/\*\.conf$' /etc/ipsec.conf; then
    echo >> /etc/ipsec.conf
    echo 'include /etc/ipsec.d/*.conf' >> /etc/ipsec.conf
  fi
  sed -i".old-$SYS_DT" \
      -e "/^[[:space:]]\+leftcert=/d" \
      -e "/^[[:space:]]\+leftid=/d" /etc/ipsec.d/ikev2.conf
  if [ "$use_dns_name" = "1" ]; then
    sed -i "/conn ikev2-cp/a \  leftid=@$server_addr" /etc/ipsec.d/ikev2.conf
  else
    sed -i "/conn ikev2-cp/a \  leftid=$server_addr" /etc/ipsec.d/ikev2.conf
  fi
  sed -i "/conn ikev2-cp/a \  leftcert=$server_addr" /etc/ipsec.d/ikev2.conf
}

restart_ipsec_service() {
  bigecho "Restarting IPsec service..."
  mkdir -p /run/pluto
  service ipsec restart 2>/dev/null
}

print_client_info() {
cat <<EOF

Successfully changed IKEv2 server address!

EOF
}

ikev2changeaddr() {
  check_root
  check_os
  check_libreswan
  check_ikev2
  check_utils_exist
  check_ca_cert_exists
  get_server_address

  show_welcome
  enter_server_address
  check_server_address
  confirm_changes

  create_server_cert
  update_ikev2_conf
  if [ "$os_type" = "alpine" ]; then
    ipsec auto --replace ikev2-cp >/dev/null
  else
    restart_ipsec_service
  fi
  print_client_info
}

## Defer until we have the complete script
ikev2changeaddr "$@"

exit 0