#!/bin/bash # # Script to set up IKEv2 on Ubuntu, Debian, CentOS/RHEL and Amazon Linux 2 # # The latest version of this script is available at: # https://github.com/hwdsl2/setup-ipsec-vpn # # Copyright (C) 2020-2021 Lin Song # # 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; echo "## $1"; echo; } bigecho2() { echo; 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_run_as_root() { if [ "$(id -u)" != 0 ]; then exiterr "Script must be run as root. Try 'sudo bash $0'" fi } check_os_type() { os_arch=$(uname -m | tr -dc 'A-Za-z0-9_-') if grep -qs -e "release 7" -e "release 8" /etc/redhat-release; then os_type=centos if grep -qs "Red Hat" /etc/redhat-release; then os_type=rhel fi if grep -qs "release 7" /etc/redhat-release; then os_ver=7 elif grep -qs "release 8" /etc/redhat-release; then os_ver=8 fi 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 ;; *) exiterr "This script only supports Ubuntu, Debian, CentOS/RHEL 7/8 and Amazon Linux 2." exit 1 ;; esac os_ver=$(sed 's/\..*//' /etc/debian_version | tr -dc 'A-Za-z0-9') fi } check_utils_exist() { command -v certutil >/dev/null 2>&1 || exiterr "'certutil' not found. Abort." command -v pk12util >/dev/null 2>&1 || exiterr "'pk12util' not found. Abort." } check_container() { in_container=0 if grep -qs "hwdsl2" /opt/src/run.sh; then in_container=1 fi } check_ca_cert_exists() { if certutil -L -d sql:/etc/ipsec.d -n "IKEv2 VPN CA" >/dev/null 2>&1; then exiterr "Certificate 'IKEv2 VPN CA' already exists." fi } check_server_cert_exists() { if certutil -L -d sql:/etc/ipsec.d -n "$server_addr" >/dev/null 2>&1; then echo "Error: Certificate '$server_addr' already exists." >&2 echo "Abort. No changes were made." >&2 exit 1 fi } check_client_cert_exists() { if certutil -L -d sql:/etc/ipsec.d -n "$client_name" >/dev/null 2>&1; then echo "Error: Client '$client_name' already exists." >&2 echo "Abort. No changes were made." >&2 exit 1 fi } check_swan_install() { ipsec_ver=$(/usr/local/sbin/ipsec --version 2>/dev/null) swan_ver=$(printf '%s' "$ipsec_ver" | sed -e 's/Linux //' -e 's/Libreswan //' -e 's/ (netkey).*//') if ( ! grep -qs "hwdsl2 VPN script" /etc/sysctl.conf && ! grep -qs "hwdsl2" /opt/src/run.sh ) \ || ! printf '%s' "$ipsec_ver" | grep -q "Libreswan" \ || [ ! -f /etc/ppp/chap-secrets ] || [ ! -f /etc/ipsec.d/passwd ]; then cat 1>&2 <<'EOF' Error: Your must first set up the IPsec VPN server before setting up IKEv2. See: https://github.com/hwdsl2/setup-ipsec-vpn EOF exit 1 fi case $swan_ver in 3.19|3.2[01235679]|3.3[12]|4.*) /bin/true ;; *) cat 1>&2 </dev/null 2>&1; do if [ -z "$client_name" ] || [ "${#client_name}" -gt "64" ] \ || printf '%s' "$client_name" | LC_ALL=C grep -q '[^A-Za-z0-9_-]\+'; then echo "Invalid client name." else echo "Invalid client name. Client '$client_name' already exists." fi read -rp "Client name: " client_name done } enter_client_name_with_defaults() { echo echo "Provide a name for the IKEv2 VPN client." echo "Use one word only, no special characters except '-' and '_'." read -rp "Client name: [vpnclient] " client_name [ -z "$client_name" ] && client_name=vpnclient while [ "${#client_name}" -gt "64" ] \ || printf '%s' "$client_name" | LC_ALL=C grep -q '[^A-Za-z0-9_-]\+' \ || certutil -L -d sql:/etc/ipsec.d -n "$client_name" >/dev/null 2>&1; do if [ "${#client_name}" -gt "64" ] \ || printf '%s' "$client_name" | LC_ALL=C grep -q '[^A-Za-z0-9_-]\+'; then echo "Invalid client name." else echo "Invalid client name. Client '$client_name' already exists." fi read -rp "Client name: [vpnclient] " client_name [ -z "$client_name" ] && client_name=vpnclient done } enter_client_name_for_export() { echo echo "Checking for existing IKEv2 client(s)..." certutil -L -d sql:/etc/ipsec.d | grep -v -e '^$' -e 'IKEv2 VPN CA' -e '\.' | tail -n +3 | cut -f1 -d ' ' get_server_address echo read -rp "Enter the name of the IKEv2 client to export: " client_name while [ -z "$client_name" ] || [ "${#client_name}" -gt "64" ] \ || printf '%s' "$client_name" | LC_ALL=C grep -q '[^A-Za-z0-9_-]\+' \ || [ "$client_name" = "IKEv2 VPN CA" ] || [ "$client_name" = "$server_addr" ] \ || ! certutil -L -d sql:/etc/ipsec.d -n "$client_name" >/dev/null 2>&1; do echo "Invalid client name, or client does not exist." read -rp "Enter the name of the IKEv2 client to export: " client_name done } enter_client_cert_validity() { echo echo "Specify the validity period (in months) for this VPN client certificate." read -rp "Enter a number between 1 and 120: [120] " client_validity [ -z "$client_validity" ] && client_validity=120 while printf '%s' "$client_validity" | LC_ALL=C grep -q '[^0-9]\+' \ || [ "$client_validity" -lt "1" ] || [ "$client_validity" -gt "120" ] \ || [ "$client_validity" != "$((10#$client_validity))" ]; do echo "Invalid validity period." read -rp "Enter a number between 1 and 120: [120] " client_validity [ -z "$client_validity" ] && client_validity=120 done } enter_custom_dns() { echo echo "By default, clients are set to use Google Public DNS when the VPN is active." printf "Do you want to specify custom DNS servers for IKEv2? [y/N] " read -r response case $response in [yY][eE][sS]|[yY]) use_custom_dns=1 ;; *) use_custom_dns=0 dns_server_1=8.8.8.8 dns_server_2=8.8.4.4 dns_servers="8.8.8.8 8.8.4.4" ;; esac if [ "$use_custom_dns" = "1" ]; then read -rp "Enter primary DNS server: " dns_server_1 until check_ip "$dns_server_1"; do echo "Invalid DNS server." read -rp "Enter primary DNS server: " dns_server_1 done read -rp "Enter secondary DNS server (Enter to skip): " dns_server_2 until [ -z "$dns_server_2" ] || check_ip "$dns_server_2"; do echo "Invalid DNS server." read -rp "Enter secondary DNS server (Enter to skip): " dns_server_2 done if [ -n "$dns_server_2" ]; then dns_servers="$dns_server_1 $dns_server_2" else dns_servers="$dns_server_1" fi else echo "Using Google Public DNS (8.8.8.8, 8.8.4.4)." fi } check_mobike_support() { mobike_support=0 case $swan_ver in 3.2[35679]|3.3[12]|4.*) mobike_support=1 ;; esac if uname -m | grep -qi -e '^arm' -e '^aarch64'; then modprobe -q configs if [ -f /proc/config.gz ]; then if ! zcat /proc/config.gz | grep -q "CONFIG_XFRM_MIGRATE=y"; then mobike_support=0 fi fi fi kernel_conf="/boot/config-$(uname -r)" if [ -f "$kernel_conf" ]; then if ! grep -qs "CONFIG_XFRM_MIGRATE=y" "$kernel_conf"; then mobike_support=0 fi fi # Linux kernels on Ubuntu do not support MOBIKE if [ "$in_container" = "0" ]; then if [ "$os_type" = "ubuntu" ] || uname -v | grep -qi ubuntu; then mobike_support=0 fi else if uname -v | grep -qi ubuntu; then mobike_support=0 fi fi echo echo -n "Checking for MOBIKE support... " if [ "$mobike_support" = "1" ]; then echo "available" else echo "not available" fi } select_mobike() { mobike_enable=0 if [ "$mobike_support" = "1" ]; then echo echo "The MOBIKE IKEv2 extension allows VPN clients to change network attachment points," echo "e.g. switch between mobile data and Wi-Fi and keep the IPsec tunnel up on the new IP." echo printf "Do you want to enable MOBIKE support? [Y/n] " read -r response case $response in [yY][eE][sS]|[yY]|'') mobike_enable=1 ;; *) mobike_enable=0 ;; esac fi } select_p12_password() { cat <<'EOF' VPN client configuration will be exported as .p12 and .mobileconfig files, which contain the client certificate, private key and CA certificate. To protect these files, this script can generate a random password for you, which will be displayed when finished. EOF printf "Do you want to specify your own password instead? [y/N] " read -r response case $response in [yY][eE][sS]|[yY]) use_own_password=1 ;; *) use_own_password=0 ;; esac } select_menu_option() { echo "It looks like IKEv2 has already been set up on this server." echo echo "Select an option:" echo " 1) Add a new client" echo " 2) Export configuration for an existing client" echo " 3) Remove IKEv2" echo " 4) Exit" read -rp "Option: " selected_option until [[ "$selected_option" =~ ^[1-4]$ ]]; do printf '%s\n' "$selected_option: invalid selection." read -rp "Option: " selected_option done } confirm_setup_options() { cat </dev/null || exit 1 } export_p12_file() { bigecho "Exporting .p12 file..." if [ "$use_own_password" = "1" ]; then cat <<'EOF' Enter a *secure* password to protect the .p12 and .mobileconfig files. When importing into an iOS or macOS device, this password cannot be empty. EOF else p12_password=$(LC_CTYPE=C tr -dc 'A-HJ-NPR-Za-km-z2-9' < /dev/urandom | head -c 16) [ -z "$p12_password" ] && exiterr "Could not generate a random password for .p12 file." fi if [ "$in_container" = "0" ]; then if [ "$use_own_password" = "1" ]; then pk12util -d sql:/etc/ipsec.d -n "$client_name" -o ~/"$client_name-$SYS_DT.p12" || exit 1 else pk12util -W "$p12_password" -d sql:/etc/ipsec.d -n "$client_name" -o ~/"$client_name-$SYS_DT.p12" || exit 1 fi else if [ "$use_own_password" = "1" ]; then pk12util -d sql:/etc/ipsec.d -n "$client_name" -o "/etc/ipsec.d/$client_name-$SYS_DT.p12" || exit 1 else pk12util -W "$p12_password" -d sql:/etc/ipsec.d -n "$client_name" -o "/etc/ipsec.d/$client_name-$SYS_DT.p12" || exit 1 fi fi } create_mobileconfig() { bigecho "Creating .mobileconfig for iOS and macOS..." [ -z "$server_addr" ] && get_server_address if ! command -v base64 >/dev/null 2>&1 || ! command -v uuidgen >/dev/null 2>&1; then if [ "$os_type" = "ubuntu" ] || [ "$os_type" = "debian" ] || [ "$os_type" = "raspbian" ]; then export DEBIAN_FRONTEND=noninteractive apt-get -yqq update || exiterr "'apt-get update' failed." apt-get -yqq install coreutils uuid-runtime || exiterr "'apt-get install' failed." else yum -yq install coreutils util-linux || exiterr "'yum install' failed." fi fi if [ "$in_container" = "0" ]; then p12_base64=$(base64 -w 52 ~/"$client_name-$SYS_DT.p12") else p12_base64=$(base64 -w 52 "/etc/ipsec.d/$client_name-$SYS_DT.p12") fi [ -z "$p12_base64" ] && exiterr "Could not encode .p12 file." ca_base64=$(certutil -L -d sql:/etc/ipsec.d -n "IKEv2 VPN CA" -a | grep -v CERTIFICATE) [ -z "$ca_base64" ] && exiterr "Could not encode IKEv2 VPN CA certificate." uuid1=$(uuidgen) [ -z "$uuid1" ] && exiterr "Could not generate UUID value." if [ "$in_container" = "0" ]; then mc_file=~/"$client_name-$SYS_DT.mobileconfig" else mc_file="/etc/ipsec.d/$client_name-$SYS_DT.mobileconfig" fi cat > "$mc_file" < PayloadContent IKEv2 AuthenticationMethod Certificate ChildSecurityAssociationParameters DiffieHellmanGroup 14 EncryptionAlgorithm AES-256-GCM LifeTimeInMinutes 1440 DeadPeerDetectionRate Medium DisableRedirect EnableCertificateRevocationCheck 0 EnablePFS 0 IKESecurityAssociationParameters DiffieHellmanGroup 14 EncryptionAlgorithm AES-256 IntegrityAlgorithm SHA2-256 LifeTimeInMinutes 1440 LocalIdentifier $client_name PayloadCertificateUUID $uuid1 OnDemandEnabled 0 OnDemandRules Action Connect RemoteAddress $server_addr RemoteIdentifier $server_addr UseConfigurationAttributeInternalIPSubnet 0 IPv4 OverridePrimary 1 PayloadDescription Configures VPN settings PayloadDisplayName VPN PayloadIdentifier com.apple.vpn.managed.$(uuidgen) PayloadType com.apple.vpn.managed PayloadUUID $(uuidgen) PayloadVersion 1 Proxies HTTPEnable 0 HTTPSEnable 0 UserDefinedName $server_addr VPNType IKEv2 PayloadCertificateFileName $client_name PayloadContent $p12_base64 PayloadDescription Adds a PKCS#12-formatted certificate PayloadDisplayName $client_name PayloadIdentifier com.apple.security.pkcs12.$(uuidgen) PayloadType com.apple.security.pkcs12 PayloadUUID $uuid1 PayloadVersion 1 PayloadContent $ca_base64 PayloadCertificateFileName ikev2vpnca PayloadDescription Adds a CA root certificate PayloadDisplayName Certificate Authority (CA) PayloadIdentifier com.apple.security.root.$(uuidgen) PayloadType com.apple.security.root PayloadUUID $(uuidgen) PayloadVersion 1 PayloadDisplayName IKEv2 VPN configuration ($server_addr) PayloadIdentifier com.apple.vpn.managed.$(uuidgen) PayloadRemovalDisallowed PayloadType Configuration PayloadUUID $(uuidgen) PayloadVersion 1 EOF } create_ca_cert() { bigecho2 "Generating CA certificate..." certutil -z <(head -c 1024 /dev/urandom) \ -S -x -n "IKEv2 VPN CA" \ -s "O=IKEv2 VPN,CN=IKEv2 VPN CA" \ -k rsa -g 4096 -v 120 \ -d sql:/etc/ipsec.d -t "CT,," -2 >/dev/null </dev/null || exit 1 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 4096 -v 120 \ -d sql:/etc/ipsec.d -t ",," \ --keyUsage digitalSignature,keyEncipherment \ --extKeyUsage serverAuth \ --extSAN "ip:$server_addr,dns:$server_addr" >/dev/null || exit 1 fi } add_ikev2_connection() { bigecho "Adding a new IKEv2 connection..." 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 cat > /etc/ipsec.d/ikev2.conf <> /etc/ipsec.d/ikev2.conf <> /etc/ipsec.d/ikev2.conf <> /etc/ipsec.d/ikev2.conf else echo " mobike=no" >> /etc/ipsec.d/ikev2.conf fi ;; 3.19|3.2[012]) if [ -n "$dns_server_2" ]; then cat >> /etc/ipsec.d/ikev2.conf <> /etc/ipsec.d/ikev2.conf <&2 echo "This script cannot automatically remove IKEv2 from this server." >&2 echo "To manually remove IKEv2, see https://git.io/ikev2" >&2 echo "Abort. No changes were made." >&2 exit 1 fi } confirm_remove_ikev2() { echo echo "This option will remove IKEv2 from the VPN server, but keep the IPsec/L2TP" echo "and IPsec/XAuth (\"Cisco IPsec\") modes. All IKEv2 configuration including" echo "certificates will be permanently deleted. This *cannot be undone*!" echo printf "Are you sure you want to remove IKEv2? [y/N] " read -r response case $response in [yY][eE][sS]|[yY]) echo ;; *) echo "Abort. No changes were made." exit 1 ;; esac } delete_ikev2_conf() { bigecho2 "Deleting /etc/ipsec.d/ikev2.conf..." /bin/rm -f /etc/ipsec.d/ikev2.conf } delete_certificates() { bigecho "Deleting certificates from the IPsec database..." certutil -L -d sql:/etc/ipsec.d | grep -v -e '^$' -e 'IKEv2 VPN CA' | tail -n +3 | cut -f1 -d ' ' | while read -r line; do certutil -D -d sql:/etc/ipsec.d -n "$line" done certutil -D -d sql:/etc/ipsec.d -n "IKEv2 VPN CA" } print_ikev2_removed_message() { echo "IKEv2 removed!" } ikev2setup() { case $1 in --auto) use_defaults=1 ;; *) use_defaults=0 ;; esac check_run_as_root check_os_type check_swan_install check_utils_exist check_container if grep -qs "conn ikev2-cp" /etc/ipsec.conf || [ -f /etc/ipsec.d/ikev2.conf ]; then select_menu_option case $selected_option in 1) enter_client_name enter_client_cert_validity select_p12_password create_client_cert export_p12_file create_mobileconfig print_client_added_message print_client_info exit 0 ;; 2) enter_client_name_for_export select_p12_password export_p12_file create_mobileconfig print_client_exported_message print_client_info exit 0 ;; 3) check_ipsec_conf confirm_remove_ikev2 delete_ikev2_conf restart_ipsec_service delete_certificates print_ikev2_removed_message exit 0 ;; 4) exit 0 ;; esac fi check_ca_cert_exists check_swan_ver if [ "$use_defaults" = "0" ]; then select_swan_update show_welcome_message enter_server_address check_server_cert_exists enter_client_name_with_defaults enter_client_cert_validity enter_custom_dns check_mobike_support select_mobike select_p12_password confirm_setup_options else show_start_message use_dns_name=0 get_server_ip check_ip "$public_ip" || exiterr "Cannot detect this server's public IP." server_addr="$public_ip" check_server_cert_exists client_name=vpnclient check_client_cert_exists client_validity=120 use_custom_dns=0 dns_server_1=8.8.8.8 dns_server_2=8.8.4.4 dns_servers="8.8.8.8 8.8.4.4" check_mobike_support mobike_enable="$mobike_support" use_own_password=0 fi create_ca_cert create_server_cert create_client_cert export_p12_file create_mobileconfig add_ikev2_connection restart_ipsec_service if [ "$use_defaults" = "1" ]; then show_swan_update_info fi print_setup_complete_message print_client_info } ## Defer setup until we have the complete script ikev2setup "$@" exit 0