2013-05-14 14:04:19 +02:00
#!/bin/bash
2025-12-04 18:11:11 +01:00
# shellcheck disable=SC1091,SC2034
# SC1091: Not following /etc/os-release (sourced dynamically)
# SC2034: Variables used indirectly or exported for subprocesses
2013-05-14 14:04:19 +02:00
2025-12-10 17:54:00 +01:00
# Secure OpenVPN server installer for Debian, Ubuntu, CentOS, Amazon Linux 2023, Fedora, Oracle Linux, Arch Linux, Rocky Linux and AlmaLinux.
2018-09-20 17:16:04 +02:00
# https://github.com/angristan/openvpn-install
2013-05-14 14:04:19 +02:00
2025-12-04 18:30:43 +01:00
# Configuration constants
2025-12-09 23:33:57 +01:00
readonly DEFAULT_CERT_VALIDITY_DURATION_DAYS = 3650 # 10 years
readonly DEFAULT_CRL_VALIDITY_DURATION_DAYS = 5475 # 15 years
2025-12-13 19:48:07 +01:00
readonly EASYRSA_VERSION = "3.2.5"
readonly EASYRSA_SHA256 = "662ee3b453155aeb1dff7096ec052cd83176c460cfa82ac130ef8568ec4df490"
2025-12-04 18:30:43 +01:00
2025-12-09 15:52:37 +01:00
# =============================================================================
# Logging Configuration
# =============================================================================
# Set VERBOSE=1 to see command output, VERBOSE=0 (default) for quiet mode
# Set LOG_FILE to customize log location (default: openvpn-install.log in current dir)
# Set LOG_FILE="" to disable file logging
VERBOSE = ${ VERBOSE :- 0 }
LOG_FILE = ${ LOG_FILE :- openvpn -install.log }
2025-12-14 22:08:44 +01:00
OUTPUT_FORMAT = ${ OUTPUT_FORMAT :- table } # table or json - json suppresses log output
2025-12-09 15:52:37 +01:00
# Color definitions (disabled if not a terminal, unless FORCE_COLOR=1)
if [[ -t 1 ]] || [[ $FORCE_COLOR == "1" ]] ; then
readonly COLOR_RESET = '\033[0m'
readonly COLOR_RED = '\033[0;31m'
readonly COLOR_GREEN = '\033[0;32m'
readonly COLOR_YELLOW = '\033[0;33m'
readonly COLOR_BLUE = '\033[0;34m'
readonly COLOR_CYAN = '\033[0;36m'
readonly COLOR_DIM = '\033[0;90m'
readonly COLOR_BOLD = '\033[1m'
else
readonly COLOR_RESET = ''
readonly COLOR_RED = ''
readonly COLOR_GREEN = ''
readonly COLOR_YELLOW = ''
readonly COLOR_BLUE = ''
readonly COLOR_CYAN = ''
readonly COLOR_DIM = ''
readonly COLOR_BOLD = ''
fi
# Write to log file (no colors, with timestamp)
_log_to_file() {
if [[ -n " $LOG_FILE " ]] ; then
echo " $( date '+%Y-%m-%d %H:%M:%S' ) $* " >>" $LOG_FILE "
fi
}
# Logging functions
log_info() {
2025-12-14 22:08:44 +01:00
[[ $OUTPUT_FORMAT == "json" ]] && return
2025-12-09 15:52:37 +01:00
echo -e " ${ COLOR_BLUE } [INFO] ${ COLOR_RESET } $* "
_log_to_file "[INFO] $* "
}
log_warn() {
2025-12-14 22:08:44 +01:00
[[ $OUTPUT_FORMAT == "json" ]] && return
2025-12-09 15:52:37 +01:00
echo -e " ${ COLOR_YELLOW } [WARN] ${ COLOR_RESET } $* "
_log_to_file "[WARN] $* "
}
log_error() {
echo -e " ${ COLOR_RED } [ERROR] ${ COLOR_RESET } $* " >& 2
_log_to_file "[ERROR] $* "
if [[ -n " $LOG_FILE " ]] ; then
echo -e " ${ COLOR_YELLOW } Check the log file for details: ${ LOG_FILE }${ COLOR_RESET } " >& 2
fi
}
log_fatal() {
echo -e " ${ COLOR_RED } [ERROR] ${ COLOR_RESET } $* " >& 2
_log_to_file "[FATAL] $* "
if [[ -n " $LOG_FILE " ]] ; then
echo -e " ${ COLOR_YELLOW } Check the log file for details: ${ LOG_FILE }${ COLOR_RESET } " >& 2
_log_to_file "Script exited with error"
fi
exit 1
}
log_success() {
2025-12-14 22:08:44 +01:00
[[ $OUTPUT_FORMAT == "json" ]] && return
2025-12-09 15:52:37 +01:00
echo -e " ${ COLOR_GREEN } [OK] ${ COLOR_RESET } $* "
_log_to_file "[OK] $* "
}
log_debug() {
2025-12-14 22:08:44 +01:00
if [[ $VERBOSE -eq 1 && $OUTPUT_FORMAT != "json" ]] ; then
2025-12-09 15:52:37 +01:00
echo -e " ${ COLOR_DIM } [DEBUG] ${ COLOR_RESET } $* "
fi
_log_to_file "[DEBUG] $* "
}
log_prompt() {
# For user-facing prompts/questions (no prefix, just cyan)
2025-12-17 23:48:10 +01:00
# Skip display in non-interactive mode
if [[ $NON_INTERACTIVE_INSTALL != "y" ]] ; then
2025-12-09 15:52:37 +01:00
echo -e " ${ COLOR_CYAN } $* ${ COLOR_RESET } "
fi
_log_to_file "[PROMPT] $* "
}
log_header() {
# For section headers
2025-12-17 23:48:10 +01:00
# Skip display in non-interactive mode
if [[ $NON_INTERACTIVE_INSTALL != "y" ]] ; then
2025-12-09 15:52:37 +01:00
echo ""
echo -e " ${ COLOR_BOLD }${ COLOR_BLUE } === $* === ${ COLOR_RESET } "
echo ""
fi
_log_to_file "=== $* ==="
}
log_menu() {
# For menu options - only show in interactive mode
2025-12-17 23:48:10 +01:00
if [[ $NON_INTERACTIVE_INSTALL != "y" ]] ; then
2025-12-09 15:52:37 +01:00
echo " $@ "
fi
}
# Run a command with optional output suppression
# Usage: run_cmd "description" command [args...]
run_cmd() {
local desc = " $1 "
shift
2025-12-09 21:41:08 +01:00
# Display the command being run
echo -e " ${ COLOR_DIM } > $* ${ COLOR_RESET } "
2025-12-09 15:52:37 +01:00
_log_to_file "[CMD] $* "
if [[ $VERBOSE -eq 1 ]] ; then
if [[ -n " $LOG_FILE " ]] ; then
" $@ " 2>& 1 | tee -a " $LOG_FILE "
else
" $@ "
fi
else
if [[ -n " $LOG_FILE " ]] ; then
" $@ " >>" $LOG_FILE " 2>& 1
else
" $@ " >/dev/null 2>& 1
fi
fi
local ret = $?
if [[ $ret -eq 0 ]] ; then
log_debug " $desc completed successfully"
else
log_error " $desc failed with exit code $ret "
fi
return $ret
}
2025-12-13 13:31:54 +01:00
# Run a command that must succeed, exit on failure
# Usage: run_cmd_fatal "description" command [args...]
run_cmd_fatal() {
local desc = " $1 "
shift
if ! run_cmd " $desc " " $@ " ; then
log_fatal " $desc failed"
fi
}
2025-12-14 22:08:44 +01:00
# =============================================================================
# CLI Configuration
# =============================================================================
readonly SCRIPT_NAME = "openvpn-install"
# =============================================================================
# Help Text Functions
# =============================================================================
show_help() {
cat <<-EOF
OpenVPN installer and manager
Usage: $SCRIPT_NAME <command> [options]
Commands:
install Install and configure OpenVPN server
uninstall Remove OpenVPN server
client Manage client certificates
server Server management
interactive Launch interactive menu
Global Options:
--verbose Show detailed output
--log <path> Log file path (default: openvpn-install.log)
--no-log Disable file logging
--no-color Disable colored output
-h, --help Show help
Run '$SCRIPT_NAME <command> --help' for command-specific help.
EOF
}
show_install_help() {
cat <<-EOF
Install and configure OpenVPN server
Usage: $SCRIPT_NAME install [options]
Options:
-i, --interactive Run interactive install wizard
Network Options:
--endpoint <host> Public IP or hostname for clients (auto-detected)
2025-12-15 22:19:02 +01:00
--endpoint-type <4|6> Endpoint IP version: 4 or 6 (default: 4)
2025-12-14 22:08:44 +01:00
--ip <addr> Server listening IP (auto-detected)
2025-12-15 22:19:02 +01:00
--client-ipv4 Enable IPv4 for VPN clients (default: enabled)
--no-client-ipv4 Disable IPv4 for VPN clients
--client-ipv6 Enable IPv6 for VPN clients
--no-client-ipv6 Disable IPv6 for VPN clients (default)
--subnet-ipv4 <x.x.x.0> IPv4 VPN subnet (default: 10.8.0.0)
--subnet-ipv6 <prefix> IPv6 VPN subnet (default: fd42:42:42:42::)
2025-12-14 22:08:44 +01:00
--port <num> OpenVPN port (default: 1194)
--port-random Use random port (49152-65535)
--protocol <proto> Protocol: udp or tcp (default: udp)
2025-12-15 10:53:15 +01:00
--mtu <size> Tunnel MTU (default: 1500)
2025-12-14 22:08:44 +01:00
DNS Options:
--dns <provider> DNS provider (default: cloudflare)
Providers: system, unbound, cloudflare, quad9, quad9-uncensored,
fdn, dnswatch, opendns, google, yandex, adguard, nextdns, custom
--dns-primary <ip> Custom primary DNS (requires --dns custom)
--dns-secondary <ip> Custom secondary DNS (optional)
Security Options:
--cipher <cipher> Data channel cipher (default: AES-128-GCM)
Ciphers: AES-128-GCM, AES-192-GCM, AES-256-GCM, AES-128-CBC,
AES-192-CBC, AES-256-CBC, CHACHA20-POLY1305
--cert-type <type> Certificate type: ecdsa or rsa (default: ecdsa)
--cert-curve <curve> ECDSA curve (default: prime256v1)
Curves: prime256v1, secp384r1, secp521r1
--rsa-bits <size> RSA key size: 2048, 3072, 4096 (default: 2048)
--cc-cipher <cipher> Control channel cipher (auto-selected)
2025-12-15 12:13:03 +01:00
--tls-version-min <ver> Minimum TLS version: 1.2 or 1.3 (default: 1.2)
--tls-ciphersuites <list> TLS 1.3 cipher suites, colon-separated
--tls-groups <list> Key exchange groups, colon-separated
(default: X25519:prime256v1:secp384r1:secp521r1)
2025-12-14 22:08:44 +01:00
--hmac <alg> HMAC algorithm: SHA256, SHA384, SHA512 (default: SHA256)
--tls-sig <mode> TLS mode: crypt-v2, crypt, auth (default: crypt-v2)
--server-cert-days <n> Server cert validity in days (default: 3650)
Other Options:
--multi-client Allow same cert on multiple devices
Initial Client Options:
--client <name> Initial client name (default: client)
--client-password [p] Password-protect client (prompts if no value given)
--client-cert-days <n> Client cert validity in days (default: 3650)
--no-client Skip initial client creation
Examples:
$SCRIPT_NAME install
$SCRIPT_NAME install --port 443 --protocol tcp
$SCRIPT_NAME install --dns quad9 --cipher AES-256-GCM
$SCRIPT_NAME install -i
EOF
}
show_uninstall_help() {
cat <<-EOF
Remove OpenVPN server
Usage: $SCRIPT_NAME uninstall [options]
Options:
-f, --force Skip confirmation prompt
Examples:
$SCRIPT_NAME uninstall
$SCRIPT_NAME uninstall --force
EOF
}
show_client_help() {
cat <<-EOF
Manage client certificates
Usage: $SCRIPT_NAME client <subcommand> [options]
Subcommands:
add <name> Add a new client
list List all clients
revoke <name> Revoke a client certificate
renew <name> Renew a client certificate
Run '$SCRIPT_NAME client <subcommand> --help' for more info.
EOF
}
show_client_add_help() {
cat <<-EOF
Add a new VPN client
Usage: $SCRIPT_NAME client add <name> [options]
Options:
--password [pass] Password-protect client (prompts if no value given)
--cert-days <n> Certificate validity in days (default: 3650)
--output <path> Output path for .ovpn file (default: ~/<name>.ovpn)
Examples:
$SCRIPT_NAME client add alice
$SCRIPT_NAME client add bob --password
$SCRIPT_NAME client add charlie --cert-days 365 --output /tmp/charlie.ovpn
EOF
}
show_client_list_help() {
cat <<-EOF
List all client certificates
Usage: $SCRIPT_NAME client list [options]
Options:
--format <fmt> Output format: table or json (default: table)
Examples:
$SCRIPT_NAME client list
$SCRIPT_NAME client list --format json
EOF
}
show_client_revoke_help() {
cat <<-EOF
Revoke a client certificate
Usage: $SCRIPT_NAME client revoke <name> [options]
Options:
-f, --force Skip confirmation prompt
Examples:
$SCRIPT_NAME client revoke alice
$SCRIPT_NAME client revoke bob --force
EOF
}
show_client_renew_help() {
cat <<-EOF
Renew a client certificate
Usage: $SCRIPT_NAME client renew <name> [options]
Options:
--cert-days <n> New certificate validity in days (default: 3650)
Examples:
$SCRIPT_NAME client renew alice
$SCRIPT_NAME client renew bob --cert-days 365
EOF
}
show_server_help() {
cat <<-EOF
Server management
Usage: $SCRIPT_NAME server <subcommand> [options]
Subcommands:
status List currently connected clients
renew Renew server certificate
Run '$SCRIPT_NAME server <subcommand> --help' for more info.
EOF
}
show_server_status_help() {
cat <<-EOF
List currently connected clients
Note: Client data is updated every 60 seconds by OpenVPN.
Usage: $SCRIPT_NAME server status [options]
Options:
--format <fmt> Output format: table or json (default: table)
Examples:
$SCRIPT_NAME server status
$SCRIPT_NAME server status --format json
EOF
}
show_server_renew_help() {
cat <<-EOF
Renew server certificate
Usage: $SCRIPT_NAME server renew [options]
Options:
--cert-days <n> New certificate validity in days (default: 3650)
-f, --force Skip confirmation/warning
Examples:
$SCRIPT_NAME server renew
$SCRIPT_NAME server renew --cert-days 1825
EOF
}
# =============================================================================
# CLI Command Handlers
# =============================================================================
# Check if OpenVPN is installed
isOpenVPNInstalled() {
[[ -e /etc/openvpn/server/server.conf ]]
}
# Require OpenVPN to be installed
requireOpenVPN() {
if ! isOpenVPNInstalled; then
log_fatal "OpenVPN is not installed. Run ' $SCRIPT_NAME install' first."
fi
}
# Require OpenVPN to NOT be installed
requireNoOpenVPN() {
if isOpenVPNInstalled; then
log_fatal "OpenVPN is already installed. Use ' $SCRIPT_NAME client' to manage clients or ' $SCRIPT_NAME uninstall' to remove."
fi
}
# Parse DNS provider string to DNS number
parse_dns_provider() {
case " $1 " in
2025-12-17 23:48:10 +01:00
system | unbound | cloudflare | quad9 | quad9-uncensored | fdn | dnswatch | opendns | google | yandex | adguard | nextdns | custom)
DNS = " $1 "
;;
2025-12-14 22:08:44 +01:00
*) log_fatal "Invalid DNS provider: $1 . See ' $SCRIPT_NAME install --help' for valid providers." ;;
esac
}
# Parse cipher string
parse_cipher() {
case " $1 " in
AES-128-GCM | AES-192-GCM | AES-256-GCM | AES-128-CBC | AES-192-CBC | AES-256-CBC | CHACHA20-POLY1305)
CIPHER = " $1 "
;;
*) log_fatal "Invalid cipher: $1 . See ' $SCRIPT_NAME install --help' for valid ciphers." ;;
esac
}
# Parse curve string
parse_curve() {
case " $1 " in
prime256v1 | secp384r1 | secp521r1) echo " $1 " ;;
*) log_fatal "Invalid curve: $1 . Valid curves: prime256v1, secp384r1, secp521r1" ;;
esac
}
2025-12-17 23:48:10 +01:00
# =============================================================================
# Configuration Constants
# =============================================================================
# Protocol options
readonly PROTOCOLS =( "udp" "tcp" )
# DNS providers (use string names)
readonly DNS_PROVIDERS =( "system" "unbound" "cloudflare" "quad9" "quad9-uncensored" "fdn" "dnswatch" "opendns" "google" "yandex" "adguard" "nextdns" "custom" )
# Cipher options
readonly CIPHERS =( "AES-128-GCM" "AES-192-GCM" "AES-256-GCM" "AES-128-CBC" "AES-192-CBC" "AES-256-CBC" "CHACHA20-POLY1305" )
# Certificate types (use strings)
readonly CERT_TYPES =( "ecdsa" "rsa" )
# ECDSA curves
readonly CERT_CURVES =( "prime256v1" "secp384r1" "secp521r1" )
# RSA key sizes
readonly RSA_KEY_SIZES =( "2048" "3072" "4096" )
# TLS versions
readonly TLS_VERSIONS =( "1.2" "1.3" )
# TLS signature modes (use strings)
readonly TLS_SIG_MODES =( "crypt-v2" "crypt" "auth" )
# HMAC algorithms
readonly HMAC_ALGS =( "SHA256" "SHA384" "SHA512" )
# TLS 1.3 cipher suite options
readonly TLS13_OPTIONS =( "all" "aes-256-only" "aes-128-only" "chacha20-only" )
# TLS groups options
readonly TLS_GROUPS_OPTIONS =( "all" "x25519-only" "nist-only" )
# =============================================================================
# Set Installation Defaults
# =============================================================================
# Centralized function to set all defaults - called before configuration
set_installation_defaults() {
# Network
ENDPOINT_TYPE = " ${ ENDPOINT_TYPE :- 4 } "
CLIENT_IPV4 = " ${ CLIENT_IPV4 :- y } "
CLIENT_IPV6 = " ${ CLIENT_IPV6 :- n } "
VPN_SUBNET_IPV4 = " ${ VPN_SUBNET_IPV4 :- 10 .8.0.0 } "
VPN_SUBNET_IPV6 = " ${ VPN_SUBNET_IPV6 :- fd42 : 42 : 42 : 42 :: } "
PORT = " ${ PORT :- 1194 } "
PROTOCOL = " ${ PROTOCOL :- udp } "
# DNS (use string name)
DNS = " ${ DNS :- cloudflare } "
# Multi-client
MULTI_CLIENT = " ${ MULTI_CLIENT :- n } "
# Encryption
2025-12-14 22:08:44 +01:00
CIPHER = " ${ CIPHER :- AES -128-GCM } "
2025-12-17 23:48:10 +01:00
CERT_TYPE = " ${ CERT_TYPE :- ecdsa } "
2025-12-14 22:08:44 +01:00
CERT_CURVE = " ${ CERT_CURVE :- prime256v1 } "
2025-12-17 23:48:10 +01:00
RSA_KEY_SIZE = " ${ RSA_KEY_SIZE :- 2048 } "
2025-12-15 12:13:03 +01:00
TLS_VERSION_MIN = " ${ TLS_VERSION_MIN :- 1 .2 } "
2025-12-17 23:48:10 +01:00
TLS13_CIPHERSUITES = " ${ TLS13_CIPHERSUITES :- TLS_AES_256_GCM_SHA384 : TLS_AES_128_GCM_SHA256 : TLS_CHACHA20_POLY1305_SHA256 } "
2025-12-15 12:13:03 +01:00
TLS_GROUPS = " ${ TLS_GROUPS :- X25519 : prime256v1 : secp384r1 : secp521r1 } "
2025-12-14 22:08:44 +01:00
HMAC_ALG = " ${ HMAC_ALG :- SHA256 } "
2025-12-17 23:48:10 +01:00
TLS_SIG = " ${ TLS_SIG :- crypt -v2 } "
# Derive CC_CIPHER from CERT_TYPE if not set
if [[ -z $CC_CIPHER ]] ; then
if [[ $CERT_TYPE == "ecdsa" ]] ; then
CC_CIPHER = "TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256"
else
CC_CIPHER = "TLS-ECDHE-RSA-WITH-AES-128-GCM-SHA256"
fi
fi
# Client
CLIENT = " ${ CLIENT :- client } "
PASS = " ${ PASS :- 1 } "
CLIENT_CERT_DURATION_DAYS = " ${ CLIENT_CERT_DURATION_DAYS :- $DEFAULT_CERT_VALIDITY_DURATION_DAYS } "
SERVER_CERT_DURATION_DAYS = " ${ SERVER_CERT_DURATION_DAYS :- $DEFAULT_CERT_VALIDITY_DURATION_DAYS } "
# Note: Gateway values (VPN_GATEWAY_IPV4, VPN_GATEWAY_IPV6) and IPV6_SUPPORT
# are computed in prepare_network_config() which is called after validation
2025-12-14 22:08:44 +01:00
}
# Validation functions
validate_port() {
local port = " $1 "
if ! [[ " $port " = ~ ^[ 0-9] +$ ]] || [[ " $port " -lt 1 ]] || [[ " $port " -gt 65535 ]] ; then
log_fatal "Invalid port: $port . Must be a number between 1 and 65535."
fi
}
2025-12-15 22:19:02 +01:00
validate_subnet_ipv4() {
2025-12-14 22:08:44 +01:00
local subnet = " $1 "
# Check format: x.x.x.0 where x is 0-255
if ! [[ " $subnet " = ~ ^([ 0-9]{ 1,3}) \. ([ 0-9]{ 1,3}) \. ([ 0-9]{ 1,3}) \. 0$ ]] ; then
2025-12-15 22:19:02 +01:00
log_fatal "Invalid IPv4 subnet: $subnet . Must be in format x.x.x.0 (e.g., 10.8.0.0)"
2025-12-14 22:08:44 +01:00
fi
local octet1 = " ${ BASH_REMATCH [1] } "
local octet2 = " ${ BASH_REMATCH [2] } "
local octet3 = " ${ BASH_REMATCH [3] } "
# Validate each octet is 0-255
if [[ " $octet1 " -gt 255 ]] || [[ " $octet2 " -gt 255 ]] || [[ " $octet3 " -gt 255 ]] ; then
2025-12-15 22:19:02 +01:00
log_fatal "Invalid IPv4 subnet: $subnet . Octets must be 0-255."
2025-12-14 22:08:44 +01:00
fi
# Check for RFC1918 private address ranges
if ! { [[ " $octet1 " -eq 10 ]] ||
[[ " $octet1 " -eq 172 && " $octet2 " -ge 16 && " $octet2 " -le 31 ]] ||
[[ " $octet1 " -eq 192 && " $octet2 " -eq 168 ]] ; } ; then
2025-12-15 22:19:02 +01:00
log_fatal "Invalid IPv4 subnet: $subnet . Must be a private network (10.x.x.0, 172.16-31.x.0, or 192.168.x.0)."
fi
}
validate_subnet_ipv6() {
local subnet = " $1 "
# Accept format: IPv6 address ending with :: (prefix only, no CIDR notation here)
# We expect formats like: fd42:42:42:42:: or fdxx:xxxx:xxxx:xxxx::
# The script will append /112 for the server directive
# IPv6 ULA validation (fd00::/8 range with at least /48 prefix)
# ULA format: fdxx:xxxx:xxxx:: or fdxx:xxxx:xxxx:xxxx:: where x is hex
if ! [[ " $subnet " = ~ ^fd[ 0-9a-fA-F]{ 2}( :[ 0-9a-fA-F]{ 1,4}){ 2,5} ::$ ]] ; then
log_fatal "Invalid IPv6 subnet: $subnet . Must be a ULA address with at least a /48 prefix, ending with :: (e.g., fd42:42:42::)"
2025-12-14 22:08:44 +01:00
fi
}
validate_positive_int() {
local value = " $1 "
local name = " $2 "
if ! [[ " $value " = ~ ^[ 0-9] +$ ]] || [[ " $value " -lt 1 ]] ; then
log_fatal "Invalid $name : $value . Must be a positive integer."
fi
}
2025-12-15 10:53:15 +01:00
validate_mtu() {
local mtu = " $1 "
if ! [[ " $mtu " = ~ ^[ 0-9] +$ ]] || [[ " $mtu " -lt 576 ]] || [[ " $mtu " -gt 65535 ]] ; then
log_fatal "Invalid MTU: $mtu . Must be between 576 and 65535."
fi
}
2025-12-15 11:24:01 +01:00
# Maximum length for client names (OpenSSL CN limit)
readonly MAX_CLIENT_NAME_LENGTH = 64
# Check if client name is valid (non-fatal, returns true/false)
is_valid_client_name() {
local name = " $1 "
[[ " $name " = ~ ^[ a-zA-Z0-9_-] +$ ]] && [[ ${# name } -le $MAX_CLIENT_NAME_LENGTH ]]
}
# Validate client name and exit with error if invalid
validate_client_name() {
local name = " $1 "
if [[ -z " $name " ]] ; then
log_fatal "Client name cannot be empty."
fi
if ! [[ " $name " = ~ ^[ a-zA-Z0-9_-] +$ ]] ; then
log_fatal "Invalid client name: $name . Only alphanumeric characters, underscores, and hyphens are allowed."
fi
if [[ ${# name } -gt $MAX_CLIENT_NAME_LENGTH ]] ; then
log_fatal "Client name too long: ${# name } characters. Maximum is $MAX_CLIENT_NAME_LENGTH characters (OpenSSL CN limit)."
fi
}
2025-12-17 23:48:10 +01:00
# Validate all configuration values (catches invalid env vars in non-interactive mode)
validate_configuration() {
# Validate PROTOCOL
case " $PROTOCOL " in
udp | tcp) ;;
*) log_fatal "Invalid protocol: $PROTOCOL . Must be 'udp' or 'tcp'." ;;
esac
# Validate DNS
case " $DNS " in
system | unbound | cloudflare | quad9 | quad9-uncensored | fdn | dnswatch | opendns | google | yandex | adguard | nextdns | custom) ;;
*) log_fatal "Invalid DNS provider: $DNS . Valid providers: system, unbound, cloudflare, quad9, quad9-uncensored, fdn, dnswatch, opendns, google, yandex, adguard, nextdns, custom" ;;
esac
# Validate CERT_TYPE
case " $CERT_TYPE " in
ecdsa | rsa) ;;
*) log_fatal "Invalid cert type: $CERT_TYPE . Must be 'ecdsa' or 'rsa'." ;;
esac
# Validate TLS_SIG
case " $TLS_SIG " in
crypt-v2 | crypt | auth) ;;
*) log_fatal "Invalid TLS signature mode: $TLS_SIG . Must be 'crypt-v2', 'crypt', or 'auth'." ;;
esac
# Validate PORT
if ! [[ " $PORT " = ~ ^[ 0-9] +$ ]] || [[ " $PORT " -lt 1 ]] || [[ " $PORT " -gt 65535 ]] ; then
log_fatal "Invalid port: $PORT . Must be a number between 1 and 65535."
fi
# Validate CLIENT_IPV4/CLIENT_IPV6
if [[ $CLIENT_IPV4 != "y" ]] && [[ $CLIENT_IPV6 != "y" ]] ; then
log_fatal "At least one of CLIENT_IPV4 or CLIENT_IPV6 must be 'y'"
fi
# Validate ENDPOINT_TYPE
case " $ENDPOINT_TYPE " in
4 | 6) ;;
*) log_fatal "Invalid endpoint type: $ENDPOINT_TYPE . Must be '4' or '6'." ;;
esac
# Validate CIPHER
case " $CIPHER " in
AES-128-GCM | AES-192-GCM | AES-256-GCM | AES-128-CBC | AES-192-CBC | AES-256-CBC | CHACHA20-POLY1305) ;;
*) log_fatal "Invalid cipher: $CIPHER . Valid ciphers: AES-128-GCM, AES-192-GCM, AES-256-GCM, AES-128-CBC, AES-192-CBC, AES-256-CBC, CHACHA20-POLY1305" ;;
esac
# Validate CERT_CURVE (only if ECDSA)
if [[ $CERT_TYPE == "ecdsa" ]] ; then
case " $CERT_CURVE " in
prime256v1 | secp384r1 | secp521r1) ;;
*) log_fatal "Invalid cert curve: $CERT_CURVE . Must be 'prime256v1', 'secp384r1', or 'secp521r1'." ;;
esac
fi
# Validate RSA_KEY_SIZE (only if RSA)
if [[ $CERT_TYPE == "rsa" ]] ; then
case " $RSA_KEY_SIZE " in
2048 | 3072 | 4096) ;;
*) log_fatal "Invalid RSA key size: $RSA_KEY_SIZE . Must be 2048, 3072, or 4096." ;;
esac
fi
# Validate TLS_VERSION_MIN
case " $TLS_VERSION_MIN " in
1.2 | 1.3) ;;
*) log_fatal "Invalid TLS version: $TLS_VERSION_MIN . Must be '1.2' or '1.3'." ;;
esac
# Validate HMAC_ALG
case " $HMAC_ALG " in
SHA256 | SHA384 | SHA512) ;;
*) log_fatal "Invalid HMAC algorithm: $HMAC_ALG . Must be SHA256, SHA384, or SHA512." ;;
esac
# Validate MTU if set
if [[ -n $MTU ]] ; then
if ! [[ " $MTU " = ~ ^[ 0-9] +$ ]] || [[ " $MTU " -lt 576 ]] || [[ " $MTU " -gt 65535 ]] ; then
log_fatal "Invalid MTU: $MTU . Must be a number between 576 and 65535."
fi
fi
# Validate custom DNS if selected
if [[ $DNS == "custom" ]] && [[ -z $DNS1 ]] ; then
log_fatal "Custom DNS selected but DNS1 (primary DNS) is not set. Use --dns-primary to specify."
fi
# Validate VPN subnets using the dedicated validation functions
# These check format, octet ranges, and RFC1918/ULA compliance
if [[ -n $VPN_SUBNET_IPV4 ]] ; then
validate_subnet_ipv4 " $VPN_SUBNET_IPV4 "
fi
if [[ $CLIENT_IPV6 == "y" ]] && [[ -n $VPN_SUBNET_IPV6 ]] ; then
validate_subnet_ipv6 " $VPN_SUBNET_IPV6 "
fi
}
# =============================================================================
# Interactive Helper Functions
# =============================================================================
# Generic select-from-menu function for arrays
# Usage: select_from_array "prompt" array_name "default_value" result_var
# Note: Uses namerefs (-n) for arrays
select_from_array() {
local prompt = " $1 "
local -n _options_ref = " $2 "
local default = " $3 "
local -n _result_ref = " $4 "
# If already set (non-interactive mode), just return
if [[ -n $_result_ref ]] ; then
return
fi
# Find default index (1-based for display)
local default_idx = 1
for i in " ${ !_options_ref[@] } " ; do
if [[ " ${ _options_ref [ $i ] } " == " $default " ]] ; then
default_idx = $(( i + 1 ))
break
fi
done
# Display menu
local count = ${# _options_ref [@] }
for i in " ${ !_options_ref[@] } " ; do
log_menu " $(( i + 1 )) ) ${ _options_ref [ $i ] } "
done
# Read selection
local choice
until [[ $choice = ~ ^[ 0-9] +$ ]] && (( choice >= 1 && choice <= count)) ; do
read -rp " $prompt [1- $count ]: " -e -i " $default_idx " choice
done
_result_ref = " ${ _options_ref [ $(( choice - 1 )) ] } "
}
# Select with custom labels (for menu items that need different display text)
# Usage: select_with_labels "prompt" labels_array values_array "default_value" result_var
select_with_labels() {
local prompt = " $1 "
local -n _labels_ref = " $2 "
local -n _values_ref = " $3 "
local default = " $4 "
local -n _result_ref = " $5 "
# If already set (non-interactive mode), just return
if [[ -n $_result_ref ]] ; then
return
fi
# Find default index
local default_idx = 1
for i in " ${ !_values_ref[@] } " ; do
if [[ " ${ _values_ref [ $i ] } " == " $default " ]] ; then
default_idx = $(( i + 1 ))
break
fi
done
# Display menu
local count = ${# _labels_ref [@] }
for i in " ${ !_labels_ref[@] } " ; do
log_menu " $(( i + 1 )) ) ${ _labels_ref [ $i ] } "
done
# Read selection
local choice
until [[ $choice = ~ ^[ 0-9] +$ ]] && (( choice >= 1 && choice <= count)) ; do
read -rp " $prompt [1- $count ]: " -e -i " $default_idx " choice
done
_result_ref = " ${ _values_ref [ $(( choice - 1 )) ] } "
}
# Prompt for yes/no with default
# Usage: prompt_yes_no "prompt" "default" result_var
prompt_yes_no() {
local prompt = " $1 "
local default = " $2 "
local -n _result_ref = " $3 "
# If already set, just return
if [[ $_result_ref = ~ ^[ yn] $ ]] ; then
return
fi
until [[ $_result_ref = ~ ^[ yn] $ ]] ; do
read -rp " $prompt [y/n]: " -e -i " $default " _result_ref
done
}
# Prompt for a value with validation function
# Usage: prompt_validated "prompt" "validator_func" "default" result_var
# The validator should return 0 for valid, non-0 for invalid
prompt_validated() {
local prompt = " $1 "
local validator = " $2 "
local default = " $3 "
local -n _result_ref = " $4 "
# If already set and valid, return
if [[ -n $_result_ref ]] && $validator " $_result_ref " 2>/dev/null; then
return
fi
_result_ref = ""
until [[ -n $_result_ref ]] && $validator " $_result_ref " 2>/dev/null; do
read -rp " $prompt : " -e -i " $default " _result_ref
done
}
# Non-fatal port validator (returns 0/1)
is_valid_port() {
local port = " $1 "
[[ " $port " = ~ ^[ 0-9] +$ ]] && (( port >= 1 && port <= 65535))
}
# Non-fatal MTU validator (returns 0/1)
is_valid_mtu() {
local mtu = " $1 "
[[ " $mtu " = ~ ^[ 0-9] +$ ]] && (( mtu >= 576 && mtu <= 65535))
}
2025-12-14 22:08:44 +01:00
# Handle install command
cmd_install() {
local interactive = false
local no_client = false
local client_password_flag = false
local client_password_value = ""
while [[ $# -gt 0 ]] ; do
case " $1 " in
-i | --interactive)
interactive = true
shift
;;
--endpoint)
[[ -z " ${ 2 :- } " ]] && log_fatal "--endpoint requires an argument"
ENDPOINT = " $2 "
shift 2
;;
--ip)
[[ -z " ${ 2 :- } " ]] && log_fatal "--ip requires an argument"
IP = " $2 "
APPROVE_IP = y
shift 2
;;
2025-12-15 22:19:02 +01:00
--endpoint-type)
[[ -z " ${ 2 :- } " ]] && log_fatal "--endpoint-type requires an argument"
case " $2 " in
4) ENDPOINT_TYPE = "4" ;;
6) ENDPOINT_TYPE = "6" ;;
*) log_fatal "Invalid endpoint type: $2 . Use '4' or '6'." ;;
esac
shift 2
;;
--client-ipv4)
CLIENT_IPV4 = y
shift
;;
--no-client-ipv4)
CLIENT_IPV4 = n
shift
;;
--client-ipv6)
CLIENT_IPV6 = y
shift
;;
--no-client-ipv6)
CLIENT_IPV6 = n
shift
;;
2025-12-14 22:08:44 +01:00
--ipv6)
2025-12-15 22:19:02 +01:00
# Legacy flag: enable IPv6 for clients (backward compatibility)
CLIENT_IPV6 = y
2025-12-14 22:08:44 +01:00
shift
;;
2025-12-15 22:19:02 +01:00
--subnet-ipv4)
[[ -z " ${ 2 :- } " ]] && log_fatal "--subnet-ipv4 requires an argument"
validate_subnet_ipv4 " $2 "
VPN_SUBNET_IPV4 = " $2 "
shift 2
;;
--subnet-ipv6)
[[ -z " ${ 2 :- } " ]] && log_fatal "--subnet-ipv6 requires an argument"
validate_subnet_ipv6 " $2 "
VPN_SUBNET_IPV6 = " $2 "
shift 2
;;
2025-12-14 22:08:44 +01:00
--subnet)
2025-12-15 22:19:02 +01:00
# Legacy flag: --subnet now maps to --subnet-ipv4
2025-12-14 22:08:44 +01:00
[[ -z " ${ 2 :- } " ]] && log_fatal "--subnet requires an argument"
2025-12-15 22:19:02 +01:00
validate_subnet_ipv4 " $2 "
VPN_SUBNET_IPV4 = " $2 "
2025-12-14 22:08:44 +01:00
shift 2
;;
--port)
[[ -z " ${ 2 :- } " ]] && log_fatal "--port requires an argument"
validate_port " $2 "
PORT = " $2 "
shift 2
;;
--port-random)
2025-12-17 23:48:10 +01:00
PORT = "random"
2025-12-14 22:08:44 +01:00
shift
;;
--protocol)
[[ -z " ${ 2 :- } " ]] && log_fatal "--protocol requires an argument"
case " $2 " in
2025-12-17 23:48:10 +01:00
udp | tcp)
PROTOCOL = " $2 "
2025-12-14 22:08:44 +01:00
;;
*) log_fatal "Invalid protocol: $2 . Use 'udp' or 'tcp'." ;;
esac
shift 2
;;
2025-12-15 10:53:15 +01:00
--mtu)
[[ -z " ${ 2 :- } " ]] && log_fatal "--mtu requires an argument"
validate_mtu " $2 "
MTU = " $2 "
shift 2
;;
2025-12-14 22:08:44 +01:00
--dns)
[[ -z " ${ 2 :- } " ]] && log_fatal "--dns requires an argument"
parse_dns_provider " $2 "
shift 2
;;
--dns-primary)
[[ -z " ${ 2 :- } " ]] && log_fatal "--dns-primary requires an argument"
DNS1 = " $2 "
shift 2
;;
--dns-secondary)
[[ -z " ${ 2 :- } " ]] && log_fatal "--dns-secondary requires an argument"
DNS2 = " $2 "
shift 2
;;
--multi-client)
MULTI_CLIENT = y
shift
;;
--cipher)
[[ -z " ${ 2 :- } " ]] && log_fatal "--cipher requires an argument"
parse_cipher " $2 "
CUSTOMIZE_ENC = y
shift 2
;;
--cert-type)
[[ -z " ${ 2 :- } " ]] && log_fatal "--cert-type requires an argument"
case " $2 " in
2025-12-17 23:48:10 +01:00
ecdsa | rsa) CERT_TYPE = " $2 " ;;
2025-12-14 22:08:44 +01:00
*) log_fatal "Invalid cert-type: $2 . Use 'ecdsa' or 'rsa'." ;;
esac
shift 2
;;
--cert-curve)
[[ -z " ${ 2 :- } " ]] && log_fatal "--cert-curve requires an argument"
CERT_CURVE = $( parse_curve " $2 " )
CUSTOMIZE_ENC = y
shift 2
;;
--rsa-bits)
[[ -z " ${ 2 :- } " ]] && log_fatal "--rsa-bits requires an argument"
case " $2 " in
2048 | 3072 | 4096) RSA_KEY_SIZE = " $2 " ;;
*) log_fatal "Invalid RSA key size: $2 . Use 2048, 3072, or 4096." ;;
esac
CUSTOMIZE_ENC = y
shift 2
;;
--cc-cipher)
[[ -z " ${ 2 :- } " ]] && log_fatal "--cc-cipher requires an argument"
CC_CIPHER = " $2 "
CUSTOMIZE_ENC = y
shift 2
;;
2025-12-15 12:13:03 +01:00
--tls-ciphersuites)
[[ -z " ${ 2 :- } " ]] && log_fatal "--tls-ciphersuites requires an argument"
TLS13_CIPHERSUITES = " $2 "
2025-12-14 22:08:44 +01:00
CUSTOMIZE_ENC = y
shift 2
;;
2025-12-15 12:13:03 +01:00
--tls-version-min)
[[ -z " ${ 2 :- } " ]] && log_fatal "--tls-version-min requires an argument"
case " $2 " in
1.2 | 1.3) TLS_VERSION_MIN = " $2 " ;;
*) log_fatal "Invalid TLS version: $2 . Use '1.2' or '1.3'." ;;
esac
2025-12-14 22:08:44 +01:00
CUSTOMIZE_ENC = y
shift 2
;;
2025-12-15 12:13:03 +01:00
--tls-groups)
[[ -z " ${ 2 :- } " ]] && log_fatal "--tls-groups requires an argument"
TLS_GROUPS = " $2 "
2025-12-14 22:08:44 +01:00
CUSTOMIZE_ENC = y
shift 2
;;
--hmac)
[[ -z " ${ 2 :- } " ]] && log_fatal "--hmac requires an argument"
case " $2 " in
SHA256 | SHA384 | SHA512) HMAC_ALG = " $2 " ;;
*) log_fatal "Invalid HMAC algorithm: $2 . Use SHA256, SHA384, or SHA512." ;;
esac
CUSTOMIZE_ENC = y
shift 2
;;
--tls-sig)
[[ -z " ${ 2 :- } " ]] && log_fatal "--tls-sig requires an argument"
case " $2 " in
2025-12-17 23:48:10 +01:00
crypt-v2 | crypt | auth) TLS_SIG = " $2 " ;;
2025-12-14 22:08:44 +01:00
*) log_fatal "Invalid TLS mode: $2 . Use 'crypt-v2', 'crypt', or 'auth'." ;;
esac
shift 2
;;
--server-cert-days)
[[ -z " ${ 2 :- } " ]] && log_fatal "--server-cert-days requires an argument"
validate_positive_int " $2 " "server-cert-days"
SERVER_CERT_DURATION_DAYS = " $2 "
shift 2
;;
--client)
[[ -z " ${ 2 :- } " ]] && log_fatal "--client requires an argument"
2025-12-15 11:24:01 +01:00
validate_client_name " $2 "
2025-12-14 22:08:44 +01:00
CLIENT = " $2 "
shift 2
;;
--client-password)
client_password_flag = true
# Check if next arg is a value or another flag
if [[ -n " ${ 2 :- } " ]] && [[ ! " $2 " = ~ ^- ]] ; then
client_password_value = " $2 "
shift
fi
shift
;;
--client-cert-days)
[[ -z " ${ 2 :- } " ]] && log_fatal "--client-cert-days requires an argument"
validate_positive_int " $2 " "client-cert-days"
CLIENT_CERT_DURATION_DAYS = " $2 "
shift 2
;;
--no-client)
no_client = true
shift
;;
-h | --help)
show_install_help
exit 0
;;
*)
log_fatal "Unknown option: $1 . See ' $SCRIPT_NAME install --help' for usage."
;;
esac
done
# Validate custom DNS settings
2025-12-17 23:48:10 +01:00
if [[ -n " ${ DNS1 :- } " || -n " ${ DNS2 :- } " ]] && [[ " ${ DNS :- } " != "custom" ]] ; then
2025-12-14 22:08:44 +01:00
log_fatal "--dns-primary and --dns-secondary require --dns custom"
fi
# Check if already installed
requireNoOpenVPN
if [[ $interactive == true ]] ; then
# Run interactive installer
installQuestions
else
2025-12-17 23:48:10 +01:00
# Non-interactive mode - set flags and defaults
NON_INTERACTIVE_INSTALL = y
2025-12-14 22:08:44 +01:00
APPROVE_INSTALL = y
APPROVE_IP = ${ APPROVE_IP :- y }
2025-12-17 23:48:10 +01:00
CONTINUE = y
2025-12-14 22:08:44 +01:00
2025-12-17 23:48:10 +01:00
# Handle random port
if [[ $PORT == "random" ]] ; then
PORT = $( shuf -i 49152-65535 -n1)
log_info "Random Port: $PORT "
2025-12-15 12:42:59 +01:00
fi
2025-12-14 22:08:44 +01:00
# Client setup
if [[ $no_client == true ]] ; then
NEW_CLIENT = n
else
NEW_CLIENT = y
if [[ $client_password_flag == true ]] ; then
PASS = 2
if [[ -n " $client_password_value " ]] ; then
PASSPHRASE = " $client_password_value "
fi
fi
fi
2025-12-17 23:48:10 +01:00
# Set all defaults for any unset values
set_installation_defaults
2025-12-14 22:08:44 +01:00
2025-12-17 23:48:10 +01:00
# Validate configuration values (catches invalid env vars)
validate_configuration
# Detect IPs and set up network config (interactive mode does this in installQuestions)
detect_server_ips
2025-12-14 22:08:44 +01:00
fi
2025-12-17 23:48:10 +01:00
# Prepare derived network configuration (gateways, etc.)
prepare_network_config
installOpenVPN
2025-12-14 22:08:44 +01:00
}
# Handle uninstall command
cmd_uninstall() {
local force = false
while [[ $# -gt 0 ]] ; do
case " $1 " in
-f | --force)
force = true
shift
;;
-h | --help)
show_uninstall_help
exit 0
;;
*)
log_fatal "Unknown option: $1 . See ' $SCRIPT_NAME uninstall --help' for usage."
;;
esac
done
requireOpenVPN
if [[ $force == true ]] ; then
REMOVE = y
fi
removeOpenVPN
}
# Handle client command
cmd_client() {
local subcmd = " ${ 1 :- } "
shift || true
case " $subcmd " in
"" | "-h" | "--help" )
show_client_help
exit 0
;;
add)
cmd_client_add " $@ "
;;
list)
cmd_client_list " $@ "
;;
revoke)
cmd_client_revoke " $@ "
;;
renew)
cmd_client_renew " $@ "
;;
*)
log_fatal "Unknown client subcommand: $subcmd . See ' $SCRIPT_NAME client --help' for usage."
;;
esac
}
# Handle client add command
cmd_client_add() {
local client_name = ""
local password_flag = false
local password_value = ""
# First non-flag argument is the client name
while [[ $# -gt 0 ]] ; do
case " $1 " in
--password)
password_flag = true
# Check if next arg is a value or another flag
if [[ -n " ${ 2 :- } " ]] && [[ ! " $2 " = ~ ^- ]] ; then
password_value = " $2 "
shift
fi
shift
;;
--cert-days)
[[ -z " ${ 2 :- } " ]] && log_fatal "--cert-days requires an argument"
validate_positive_int " $2 " "cert-days"
CLIENT_CERT_DURATION_DAYS = " $2 "
shift 2
;;
--output)
[[ -z " ${ 2 :- } " ]] && log_fatal "--output requires an argument"
CLIENT_FILEPATH = " $2 "
shift 2
;;
-h | --help)
show_client_add_help
exit 0
;;
-*)
log_fatal "Unknown option: $1 . See ' $SCRIPT_NAME client add --help' for usage."
;;
*)
if [[ -z " $client_name " ]] ; then
client_name = " $1 "
else
log_fatal "Unexpected argument: $1 "
fi
shift
;;
esac
done
[[ -z " $client_name " ]] && log_fatal "Client name is required. See ' $SCRIPT_NAME client add --help' for usage."
2025-12-15 11:24:01 +01:00
validate_client_name " $client_name "
2025-12-14 22:08:44 +01:00
requireOpenVPN
# Set up variables for newClient function
CLIENT = " $client_name "
CLIENT_CERT_DURATION_DAYS = ${ CLIENT_CERT_DURATION_DAYS :- $DEFAULT_CERT_VALIDITY_DURATION_DAYS }
if [[ $password_flag == true ]] ; then
PASS = 2
if [[ -n " $password_value " ]] ; then
PASSPHRASE = " $password_value "
fi
else
PASS = 1
fi
newClient
}
# Handle client list command
cmd_client_list() {
local format = "table"
while [[ $# -gt 0 ]] ; do
case " $1 " in
--format)
[[ -z " ${ 2 :- } " ]] && log_fatal "--format requires an argument"
case " $2 " in
table | json) format = " $2 " ;;
*) log_fatal "Invalid format: $2 . Use 'table' or 'json'." ;;
esac
shift 2
;;
-h | --help)
show_client_list_help
exit 0
;;
*)
log_fatal "Unknown option: $1 . See ' $SCRIPT_NAME client list --help' for usage."
;;
esac
done
requireOpenVPN
OUTPUT_FORMAT = " $format " listClients
}
# Handle client revoke command
cmd_client_revoke() {
local client_name = ""
local force = false
while [[ $# -gt 0 ]] ; do
case " $1 " in
-f | --force)
force = true
shift
;;
-h | --help)
show_client_revoke_help
exit 0
;;
-*)
log_fatal "Unknown option: $1 . See ' $SCRIPT_NAME client revoke --help' for usage."
;;
*)
if [[ -z " $client_name " ]] ; then
client_name = " $1 "
else
log_fatal "Unexpected argument: $1 "
fi
shift
;;
esac
done
[[ -z " $client_name " ]] && log_fatal "Client name is required. See ' $SCRIPT_NAME client revoke --help' for usage."
requireOpenVPN
CLIENT = " $client_name "
if [[ $force == true ]] ; then
REVOKE_CONFIRM = y
fi
revokeClient
}
# Handle client renew command
cmd_client_renew() {
local client_name = ""
while [[ $# -gt 0 ]] ; do
case " $1 " in
--cert-days)
[[ -z " ${ 2 :- } " ]] && log_fatal "--cert-days requires an argument"
validate_positive_int " $2 " "cert-days"
CLIENT_CERT_DURATION_DAYS = " $2 "
shift 2
;;
-h | --help)
show_client_renew_help
exit 0
;;
-*)
log_fatal "Unknown option: $1 . See ' $SCRIPT_NAME client renew --help' for usage."
;;
*)
if [[ -z " $client_name " ]] ; then
client_name = " $1 "
else
log_fatal "Unexpected argument: $1 "
fi
shift
;;
esac
done
[[ -z " $client_name " ]] && log_fatal "Client name is required. See ' $SCRIPT_NAME client renew --help' for usage."
requireOpenVPN
CLIENT = " $client_name "
CLIENT_CERT_DURATION_DAYS = ${ CLIENT_CERT_DURATION_DAYS :- $DEFAULT_CERT_VALIDITY_DURATION_DAYS }
renewClient
}
# Handle server command
cmd_server() {
local subcmd = " ${ 1 :- } "
shift || true
case " $subcmd " in
"" | "-h" | "--help" )
show_server_help
exit 0
;;
status)
cmd_server_status " $@ "
;;
renew)
cmd_server_renew " $@ "
;;
*)
log_fatal "Unknown server subcommand: $subcmd . See ' $SCRIPT_NAME server --help' for usage."
;;
esac
}
# Handle server status command
cmd_server_status() {
local format = "table"
while [[ $# -gt 0 ]] ; do
case " $1 " in
--format)
[[ -z " ${ 2 :- } " ]] && log_fatal "--format requires an argument"
case " $2 " in
table | json) format = " $2 " ;;
*) log_fatal "Invalid format: $2 . Use 'table' or 'json'." ;;
esac
shift 2
;;
-h | --help)
show_server_status_help
exit 0
;;
*)
log_fatal "Unknown option: $1 . See ' $SCRIPT_NAME server status --help' for usage."
;;
esac
done
requireOpenVPN
OUTPUT_FORMAT = " $format " listConnectedClients
}
# Handle server renew command
cmd_server_renew() {
local force = false
while [[ $# -gt 0 ]] ; do
case " $1 " in
--cert-days)
[[ -z " ${ 2 :- } " ]] && log_fatal "--cert-days requires an argument"
validate_positive_int " $2 " "cert-days"
SERVER_CERT_DURATION_DAYS = " $2 "
shift 2
;;
-f | --force)
force = true
shift
;;
-h | --help)
show_server_renew_help
exit 0
;;
*)
log_fatal "Unknown option: $1 . See ' $SCRIPT_NAME server renew --help' for usage."
;;
esac
done
requireOpenVPN
SERVER_CERT_DURATION_DAYS = ${ SERVER_CERT_DURATION_DAYS :- $DEFAULT_CERT_VALIDITY_DURATION_DAYS }
if [[ $force == true ]] ; then
CONTINUE = y
fi
renewServer
}
# Handle interactive command (legacy menu)
cmd_interactive() {
while [[ $# -gt 0 ]] ; do
case " $1 " in
-h | --help)
echo "Launch interactive menu for OpenVPN management"
echo ""
echo "Usage: $SCRIPT_NAME interactive"
exit 0
;;
*)
log_fatal "Unknown option: $1 "
;;
esac
done
if isOpenVPNInstalled; then
manageMenu
else
installQuestions
installOpenVPN
fi
}
# Main argument parser
parse_args() {
# Parse global options first
while [[ $# -gt 0 ]] ; do
case " $1 " in
--verbose)
VERBOSE = 1
shift
;;
--log)
[[ -z " ${ 2 :- } " ]] && log_fatal "--log requires an argument"
LOG_FILE = " $2 "
shift 2
;;
--no-log)
LOG_FILE = ""
shift
;;
--no-color)
# Colors already set at script start, but we can unset them
COLOR_RESET = ''
COLOR_RED = ''
COLOR_GREEN = ''
COLOR_YELLOW = ''
COLOR_BLUE = ''
COLOR_CYAN = ''
COLOR_DIM = ''
COLOR_BOLD = ''
shift
;;
-h | --help)
show_help
exit 0
;;
-*)
# Could be a command-specific option, let command handle it
break
;;
*)
# First non-option is the command
break
;;
esac
done
# Get the command
local cmd = " ${ 1 :- } "
shift || true
# Check if user just wants help (don't require root for help)
# Also detect --format json early to suppress log output before initialCheck
local wants_help = false
local prev_arg = ""
for arg in " $@ " ; do
if [[ " $arg " == "-h" || " $arg " == "--help" ]] ; then
wants_help = true
fi
if [[ " $prev_arg " == "--format" && " $arg " == "json" ]] ; then
OUTPUT_FORMAT = "json"
fi
prev_arg = " $arg "
done
# Dispatch to command handler
case " $cmd " in
"" )
show_help
exit 0
;;
install)
[[ $wants_help == false ]] && initialCheck
cmd_install " $@ "
;;
uninstall)
[[ $wants_help == false ]] && initialCheck
cmd_uninstall " $@ "
;;
client)
[[ $wants_help == false ]] && initialCheck
cmd_client " $@ "
;;
server)
[[ $wants_help == false ]] && initialCheck
cmd_server " $@ "
;;
interactive)
[[ $wants_help == false ]] && initialCheck
cmd_interactive " $@ "
;;
*)
log_fatal "Unknown command: $cmd . See ' $SCRIPT_NAME --help' for usage."
;;
esac
}
# =============================================================================
# System Check Functions
# =============================================================================
2020-04-27 14:59:19 +02:00
function isRoot() {
2018-09-20 00:05:02 +02:00
if [ " $EUID " -ne 0 ] ; then
return 1
fi
}
2013-05-14 14:04:19 +02:00
2020-04-27 14:59:19 +02:00
function tunAvailable() {
2018-09-20 00:05:02 +02:00
if [ ! -e /dev/net/tun ] ; then
return 1
fi
}
2014-03-12 21:06:57 +01:00
2020-04-27 14:59:19 +02:00
function checkOS() {
2018-09-20 00:05:02 +02:00
if [[ -e /etc/debian_version ]] ; then
2018-09-29 20:14:44 +02:00
OS = "debian"
2018-09-21 04:00:16 +08:00
source /etc/os-release
2018-09-23 22:22:59 +02:00
2020-04-27 14:59:19 +02:00
if [[ $ID == "debian" || $ID == "raspbian" ]] ; then
2025-12-07 12:27:41 +01:00
if [[ $VERSION_ID -lt 11 ]] ; then
2025-12-09 15:52:37 +01:00
log_warn "Your version of Debian is not supported."
log_info "However, if you're using Debian >= 11 or unstable/testing, you can continue at your own risk."
2018-09-23 22:22:59 +02:00
until [[ $CONTINUE = ~ ( y| n) ]] ; do
read -rp "Continue? [y/n]: " -e CONTINUE
done
2020-04-27 14:59:19 +02:00
if [[ $CONTINUE == "n" ]] ; then
2018-09-23 22:22:59 +02:00
exit 1
fi
fi
2020-04-27 14:59:19 +02:00
elif [[ $ID == "ubuntu" ]] ; then
2018-09-23 22:22:59 +02:00
OS = "ubuntu"
2020-04-27 13:35:32 +02:00
MAJOR_UBUNTU_VERSION = $( echo " $VERSION_ID " | cut -d '.' -f1)
2025-12-07 12:27:41 +01:00
if [[ $MAJOR_UBUNTU_VERSION -lt 18 ]] ; then
2025-12-09 15:52:37 +01:00
log_warn "Your version of Ubuntu is not supported."
log_info "However, if you're using Ubuntu >= 18.04 or beta, you can continue at your own risk."
2018-09-23 22:22:59 +02:00
until [[ $CONTINUE = ~ ( y| n) ]] ; do
read -rp "Continue? [y/n]: " -e CONTINUE
done
2020-04-27 14:59:19 +02:00
if [[ $CONTINUE == "n" ]] ; then
2018-09-23 22:22:59 +02:00
exit 1
fi
2018-09-20 00:05:02 +02:00
fi
2017-11-12 22:51:54 +01:00
fi
2025-12-12 04:22:12 +08:00
elif [[ -e /etc/os-release ]] ; then
2019-08-19 23:25:48 +02:00
source /etc/os-release
2021-02-14 10:54:53 +01:00
if [[ $ID == "fedora" || $ID_LIKE == "fedora" ]] ; then
2020-01-27 18:08:06 +01:00
OS = "fedora"
fi
2025-12-12 04:22:12 +08:00
if [[ $ID == "opensuse-tumbleweed" ]] ; then
OS = "opensuse"
fi
if [[ $ID == "opensuse-leap" ]] ; then
OS = "opensuse"
if [[ ${ VERSION_ID %.* } -lt 16 ]] ; then
log_info "The script only supports openSUSE Leap 16+."
log_fatal "Your version of openSUSE Leap is not supported."
fi
fi
2021-08-27 21:24:53 +08:00
if [[ $ID == "centos" || $ID == "rocky" || $ID == "almalinux" ]] ; then
2019-08-20 13:36:16 +02:00
OS = "centos"
fi
2021-03-22 10:48:15 +01:00
if [[ $ID == "ol" ]] ; then
OS = "oracle"
2025-12-11 20:22:00 +01:00
fi
if [[ $OS = ~ ( centos| oracle) ]] && [[ ${ VERSION_ID %.* } -lt 8 ]] ; then
log_info "The script only supports CentOS Stream / Rocky Linux / AlmaLinux / Oracle Linux version 8+."
log_fatal "Your version is not supported."
2021-03-22 10:48:15 +01:00
fi
2020-04-27 14:59:19 +02:00
if [[ $ID == "amzn" ]] ; then
2025-12-10 17:54:00 +01:00
if [[ " $( echo " $PRETTY_NAME " | cut -c 1-18) " == "Amazon Linux 2023." ]] && [[ " $( echo " $PRETTY_NAME " | cut -c 19) " -ge 6 ]] ; then
2025-03-10 05:24:45 -04:00
OS = "amzn2023"
else
2025-12-10 17:54:00 +01:00
log_info "The script only supports Amazon Linux 2023.6+"
log_info "Amazon Linux 2 is EOL and no longer supported."
2025-12-09 15:52:37 +01:00
log_fatal "Your version of Amazon Linux is not supported."
2019-08-19 23:25:48 +02:00
fi
fi
2025-12-12 04:22:12 +08:00
if [[ $ID == "arch" ]] ; then
OS = "arch"
fi
2018-09-23 16:27:36 +02:00
elif [[ -e /etc/arch-release ]] ; then
OS = arch
2018-09-20 00:05:02 +02:00
else
2025-12-12 04:22:12 +08:00
log_fatal "It looks like you aren't running this installer on a Debian, Ubuntu, Fedora, openSUSE, CentOS, Amazon Linux 2023, Oracle Linux, Arch Linux, Rocky Linux or AlmaLinux system."
2018-09-16 01:26:30 +02:00
fi
2018-09-20 00:05:02 +02:00
}
2013-05-14 14:04:19 +02:00
2025-12-13 10:55:36 +01:00
function checkArchPendingKernelUpgrade() {
if [[ $OS != "arch" ]] ; then
return 0
fi
# Check if running kernel's modules are available
# (detects if kernel was upgraded but system not rebooted)
# Skip this check in containers - they share host kernel but have their own /lib/modules
if [[ -f /.dockerenv ]] || grep -qE '(docker|lxc|containerd)' /proc/1/cgroup 2>/dev/null; then
log_info "Running in container, skipping kernel modules check"
else
local running_kernel
running_kernel = $( uname -r)
if [[ ! -d "/lib/modules/ ${ running_kernel } " ]] ; then
log_error "Kernel modules for running kernel ( $running_kernel ) not found!"
log_info "This usually means the kernel was upgraded but the system wasn't rebooted."
log_fatal "Please reboot your system and run this script again."
fi
fi
log_info "Checking for pending kernel upgrades on Arch Linux..."
# Sync package database to check for updates
if ! pacman -Sy & >/dev/null; then
log_warn "Failed to sync package database, skipping kernel upgrade check"
return 0
fi
# Check for pending linux kernel upgrades
local pending_kernels
pending_kernels = $( pacman -Qu 2>/dev/null | grep -E '^linux' || true )
if [[ -n " $pending_kernels " ]] ; then
log_warn "Linux kernel upgrade(s) pending:"
echo " $pending_kernels " | while read -r line; do
log_info " $line "
done
echo ""
log_info "This script uses 'pacman -Syu' which will upgrade your kernel."
log_info "After a kernel upgrade, the TUN module won't be available until you reboot."
echo ""
log_info "Please upgrade your system and reboot first:"
log_info " sudo pacman -Syu"
log_info " sudo reboot"
echo ""
log_fatal "Aborting. Run this script again after upgrading and rebooting."
fi
log_success "No pending kernel upgrades"
}
2020-04-27 14:59:19 +02:00
function initialCheck() {
2025-12-12 23:47:09 +01:00
log_debug "Checking root privileges..."
2018-09-20 00:05:02 +02:00
if ! isRoot; then
2025-12-09 15:52:37 +01:00
log_fatal "Sorry, you need to run this script as root."
2017-11-12 22:51:54 +01:00
fi
2025-12-12 23:47:09 +01:00
log_debug "Root check passed"
log_debug "Checking TUN device availability..."
2018-09-20 00:05:02 +02:00
if ! tunAvailable; then
2025-12-09 15:52:37 +01:00
log_fatal "TUN is not available."
2018-09-20 00:05:02 +02:00
fi
2025-12-12 23:47:09 +01:00
log_debug "TUN device available at /dev/net/tun"
log_debug "Detecting operating system..."
2018-09-20 00:05:02 +02:00
checkOS
2025-12-14 22:08:44 +01:00
log_debug "Detected OS: $OS ( ${ PRETTY_NAME :- unknown } )"
2025-12-13 10:55:36 +01:00
checkArchPendingKernelUpgrade
2014-10-23 00:19:08 +02:00
}
2025-12-10 00:11:25 +01:00
# Check if OpenVPN version is at least the specified version
# Usage: openvpnVersionAtLeast "2.5"
# Returns 0 if version is >= specified, 1 otherwise
function openvpnVersionAtLeast() {
local required_version = " $1 "
local installed_version
if ! command -v openvpn & >/dev/null; then
return 1
fi
installed_version = $( openvpn --version 2>/dev/null | head -1 | awk '{print $2}' )
if [[ -z " $installed_version " ]] ; then
return 1
fi
# Compare versions using sort -V
if [[ " $( printf '%s\n' " $required_version " " $installed_version " | sort -V | head -n1) " == " $required_version " ]] ; then
return 0
fi
return 1
}
2025-12-10 18:53:45 +01:00
# Check if kernel version is at least the specified version
# Usage: kernelVersionAtLeast "6.16"
# Returns 0 if version is >= specified, 1 otherwise
function kernelVersionAtLeast() {
local required_version = " $1 "
local kernel_version
kernel_version = $( uname -r | cut -d'-' -f1)
if [[ -z " $kernel_version " ]] ; then
return 1
fi
if [[ " $( printf '%s\n' " $required_version " " $kernel_version " | sort -V | head -n1) " == " $required_version " ]] ; then
return 0
fi
return 1
}
# Check if Data Channel Offload (DCO) is available
# DCO requires: OpenVPN 2.6+, kernel support (Linux 6.16+ or ovpn-dco module)
# Returns 0 if DCO is available, 1 otherwise
function isDCOAvailable() {
# DCO requires OpenVPN 2.6+
if ! openvpnVersionAtLeast "2.6" ; then
return 1
fi
# DCO is built into Linux 6.16+, or available via ovpn-dco module
if kernelVersionAtLeast "6.16" ; then
return 0
elif lsmod 2>/dev/null | grep -q "^ovpn_dco" || modinfo ovpn-dco & >/dev/null; then
return 0
fi
return 1
}
2025-12-09 19:45:56 +01:00
function installOpenVPNRepo() {
log_info "Setting up official OpenVPN repository..."
if [[ $OS = ~ ( debian| ubuntu) ]] ; then
2025-12-15 12:36:18 +01:00
run_cmd_fatal "Update package lists" apt-get update
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Installing prerequisites" apt-get install -y ca-certificates curl
2025-12-09 19:45:56 +01:00
# Create keyrings directory
run_cmd "Creating keyrings directory" mkdir -p /etc/apt/keyrings
# Download and install GPG key
if ! run_cmd "Downloading OpenVPN GPG key" curl -fsSL https://swupdate.openvpn.net/repos/repo-public.gpg -o /etc/apt/keyrings/openvpn-repo-public.asc; then
log_fatal "Failed to download OpenVPN repository GPG key"
fi
# Add repository - using stable release
if [[ -z " ${ VERSION_CODENAME } " ]] ; then
log_fatal "VERSION_CODENAME is not set. Unable to configure OpenVPN repository."
fi
echo "deb [arch= $( dpkg --print-architecture) signed-by=/etc/apt/keyrings/openvpn-repo-public.asc] https://build.openvpn.net/debian/openvpn/stable ${ VERSION_CODENAME } main" >/etc/apt/sources.list.d/openvpn-aptrepo.list
log_info "Updating package lists with new repository..."
2025-12-15 12:36:18 +01:00
run_cmd_fatal "Update package lists" apt-get update
2025-12-09 19:45:56 +01:00
log_info "OpenVPN official repository configured"
elif [[ $OS = ~ ( centos| oracle) ]] ; then
# For RHEL-based systems, use Fedora Copr (OpenVPN 2.6 stable)
# EPEL is required for pkcs11-helper dependency
log_info "Configuring OpenVPN Copr repository for RHEL-based system..."
2025-12-11 20:22:00 +01:00
# Oracle Linux uses oracle-epel-release-el* instead of epel-release
if [[ $OS == "oracle" ]] ; then
EPEL_PACKAGE = "oracle-epel-release-el ${ VERSION_ID %.* } "
else
EPEL_PACKAGE = "epel-release"
fi
2025-12-09 19:45:56 +01:00
if ! command -v dnf & >/dev/null; then
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Installing EPEL repository" yum install -y " $EPEL_PACKAGE "
run_cmd_fatal "Installing yum-plugin-copr" yum install -y yum-plugin-copr
run_cmd_fatal "Enabling OpenVPN Copr repo" yum copr enable -y @OpenVPN/openvpn-release-2.6
2025-12-09 19:45:56 +01:00
else
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Installing EPEL repository" dnf install -y " $EPEL_PACKAGE "
run_cmd_fatal "Installing dnf-plugins-core" dnf install -y dnf-plugins-core
run_cmd_fatal "Enabling OpenVPN Copr repo" dnf copr enable -y @OpenVPN/openvpn-release-2.6
2025-12-09 19:45:56 +01:00
fi
log_info "OpenVPN Copr repository configured"
elif [[ $OS == "fedora" ]] ; then
2025-12-13 13:31:54 +01:00
# Fedora already ships with recent OpenVPN 2.6.x, no Copr needed
log_info "Fedora already has recent OpenVPN packages, using distribution version"
2025-12-09 19:45:56 +01:00
else
log_info "No official OpenVPN repository available for this OS, using distribution packages"
fi
}
2020-04-27 14:59:19 +02:00
function installUnbound() {
2025-12-09 15:52:37 +01:00
log_info "Installing Unbound DNS resolver..."
2018-09-16 00:53:33 +02:00
2025-12-11 13:14:56 +01:00
# Install Unbound if not present
if [[ ! -e /etc/unbound/unbound.conf ]] ; then
2020-04-27 14:59:19 +02:00
if [[ $OS = ~ ( debian| ubuntu) ]] ; then
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Installing Unbound" apt-get install -y unbound
2025-12-10 17:54:00 +01:00
elif [[ $OS = ~ ( centos| oracle) ]] ; then
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Installing Unbound" yum install -y unbound
2025-12-10 17:54:00 +01:00
elif [[ $OS = ~ ( fedora| amzn2023) ]] ; then
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Installing Unbound" dnf install -y unbound
2025-12-12 04:22:12 +08:00
elif [[ $OS == "opensuse" ]] ; then
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Installing Unbound" zypper install -y unbound
2020-04-27 14:59:19 +02:00
elif [[ $OS == "arch" ]] ; then
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Installing Unbound" pacman -Syu --noconfirm unbound
2018-09-16 00:53:33 +02:00
fi
2025-12-11 13:14:56 +01:00
fi
2020-05-01 00:10:11 +02:00
2025-12-11 13:14:56 +01:00
# Configure Unbound for OpenVPN (runs whether freshly installed or pre-existing)
# Create conf.d directory (works on all distros)
run_cmd "Creating Unbound config directory" mkdir -p /etc/unbound/unbound.conf.d
2018-09-16 00:53:33 +02:00
2025-12-11 13:14:56 +01:00
# Ensure main config includes conf.d directory
# Modern Debian/Ubuntu use include-toplevel, others need include directive
if ! grep -qE "include(-toplevel)?:\s*.*/etc/unbound/unbound.conf.d" /etc/unbound/unbound.conf 2>/dev/null; then
# Add include directive for conf.d if not present
echo 'include: "/etc/unbound/unbound.conf.d/*.conf"' >>/etc/unbound/unbound.conf
fi
# Generate OpenVPN-specific Unbound configuration
# Using consistent best-practice settings across all distros
{
echo 'server:'
echo ' # OpenVPN DNS resolver configuration'
2025-12-15 22:19:02 +01:00
# IPv4 VPN interface (only if clients get IPv4)
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo " interface: $VPN_GATEWAY_IPV4 "
echo " access-control: $VPN_SUBNET_IPV4 /24 allow"
fi
# IPv6 VPN interface (only if clients get IPv6)
if [[ $CLIENT_IPV6 == 'y' ]] ; then
echo " interface: $VPN_GATEWAY_IPV6 "
echo " access-control: ${ VPN_SUBNET_IPV6 } /112 allow"
fi
2025-12-11 13:14:56 +01:00
echo ''
echo ' # Security hardening'
echo ' hide-identity: yes'
echo ' hide-version: yes'
echo ' harden-glue: yes'
echo ' harden-dnssec-stripped: yes'
echo ''
echo ' # Performance optimizations'
echo ' prefetch: yes'
echo ' use-caps-for-id: yes'
echo ' qname-minimisation: yes'
echo ''
echo ' # Allow binding before tun interface exists'
echo ' ip-freebind: yes'
echo ''
echo ' # DNS rebinding protection'
echo ' private-address: 10.0.0.0/8'
echo ' private-address: 172.16.0.0/12'
echo ' private-address: 192.168.0.0/16'
echo ' private-address: 169.254.0.0/16'
echo ' private-address: 127.0.0.0/8'
echo ' private-address: fd00::/8'
echo ' private-address: fe80::/10'
echo ' private-address: ::ffff:0:0/96'
2025-12-15 22:19:02 +01:00
# Add VPN subnet to private addresses if IPv6 enabled
if [[ $CLIENT_IPV6 == 'y' ]] ; then
echo " private-address: ${ VPN_SUBNET_IPV6 } /112"
2020-05-01 00:04:38 +02:00
fi
2025-12-12 04:22:12 +08:00
# Disable remote-control (requires SSL certs on openSUSE)
if [[ $OS == "opensuse" ]] ; then
echo ''
echo 'remote-control:'
echo ' control-enable: no'
fi
2025-12-11 13:14:56 +01:00
} >/etc/unbound/unbound.conf.d/openvpn.conf
2018-09-16 00:53:33 +02:00
2025-12-09 15:52:37 +01:00
run_cmd "Enabling Unbound service" systemctl enable unbound
run_cmd "Starting Unbound service" systemctl restart unbound
2025-12-11 13:14:56 +01:00
# Validate Unbound is running
for i in { 1..10} ; do
if pgrep -x unbound >/dev/null; then
return 0
fi
sleep 1
done
log_fatal "Unbound failed to start. Check 'journalctl -u unbound' for details."
2018-09-16 00:53:33 +02:00
}
2025-12-15 22:19:02 +01:00
function resolvePublicIPv4() {
local public_ip = ""
2024-11-07 19:55:14 +00:00
2025-12-15 22:19:02 +01:00
# Try to resolve public IPv4 using: https://api.seeip.org
if [[ -z $public_ip ]] ; then
public_ip = $( curl -f -m 5 -sS --retry 2 --retry-connrefused -4 https://api.seeip.org 2>/dev/null)
2024-11-07 19:55:14 +00:00
fi
2025-12-15 22:19:02 +01:00
# Try to resolve using: https://ifconfig.me
if [[ -z $public_ip ]] ; then
public_ip = $( curl -f -m 5 -sS --retry 2 --retry-connrefused -4 https://ifconfig.me 2>/dev/null)
2024-11-07 19:55:14 +00:00
fi
2025-12-15 22:19:02 +01:00
# Try to resolve using: https://api.ipify.org
if [[ -z $public_ip ]] ; then
public_ip = $( curl -f -m 5 -sS --retry 2 --retry-connrefused -4 https://api.ipify.org 2>/dev/null)
2024-11-07 19:55:14 +00:00
fi
2025-12-15 22:19:02 +01:00
# Try to resolve using: ns1.google.com
if [[ -z $public_ip ]] ; then
public_ip = $( dig -4 TXT +short o-o.myaddr.l.google.com @ns1.google.com | tr -d '"' )
2024-11-07 19:55:14 +00:00
fi
2025-12-15 22:19:02 +01:00
echo " $public_ip "
}
function resolvePublicIPv6() {
local public_ip = ""
# Try to resolve public IPv6 using: https://api6.seeip.org
if [[ -z $public_ip ]] ; then
public_ip = $( curl -f -m 5 -sS --retry 2 --retry-connrefused -6 https://api6.seeip.org 2>/dev/null)
2024-11-07 19:55:14 +00:00
fi
2025-12-15 22:19:02 +01:00
# Try to resolve using: https://ifconfig.me (IPv6)
if [[ -z $public_ip ]] ; then
public_ip = $( curl -f -m 5 -sS --retry 2 --retry-connrefused -6 https://ifconfig.me 2>/dev/null)
2024-11-07 19:55:14 +00:00
fi
2025-12-15 22:19:02 +01:00
# Try to resolve using: https://api64.ipify.org (dual-stack, prefer IPv6)
if [[ -z $public_ip ]] ; then
public_ip = $( curl -f -m 5 -sS --retry 2 --retry-connrefused -6 https://api64.ipify.org 2>/dev/null)
fi
# Try to resolve using: ns1.google.com
if [[ -z $public_ip ]] ; then
public_ip = $( dig -6 TXT +short o-o.myaddr.l.google.com @ns1.google.com | tr -d '"' )
fi
echo " $public_ip "
}
# Legacy wrapper for backward compatibility
function resolvePublicIP() {
if [[ $ENDPOINT_TYPE == "6" ]] ; then
resolvePublicIPv6
else
resolvePublicIPv4
fi
2024-11-07 19:55:14 +00:00
}
2025-12-17 23:48:10 +01:00
# Detect server's IPv4 and IPv6 addresses
function detect_server_ips() {
IP_IPV4 = $( ip -4 addr | sed -ne 's|^.* inet \([^/]*\)/.* scope global.*$|\1|p' | head -1)
IP_IPV6 = $( ip -6 addr | sed -ne 's|^.* inet6 \([^/]*\)/.* scope global.*$|\1|p' | head -1)
# Set IP based on ENDPOINT_TYPE
if [[ $ENDPOINT_TYPE == "6" ]] ; then
IP = " $IP_IPV6 "
else
IP = " $IP_IPV4 "
fi
}
# Calculate derived network configuration values
function prepare_network_config() {
# Calculate IPv4 gateway (always needed for leak prevention)
VPN_GATEWAY_IPV4 = " ${ VPN_SUBNET_IPV4 %.* } .1"
# Calculate IPv6 gateway if IPv6 is enabled
if [[ $CLIENT_IPV6 == "y" ]] ; then
VPN_GATEWAY_IPV6 = " ${ VPN_SUBNET_IPV6 } 1"
fi
# Set legacy variable for backward compatibility
IPV6_SUPPORT = " $CLIENT_IPV6 "
}
2020-04-27 14:59:19 +02:00
function installQuestions() {
2025-12-09 15:52:37 +01:00
log_header "OpenVPN Installer"
log_prompt "The git repository is available at: https://github.com/angristan/openvpn-install"
2018-07-15 15:25:59 +06:00
2025-12-09 15:52:37 +01:00
log_prompt "I need to ask you a few questions before starting the setup."
log_prompt "You can leave the default options and just press enter if you are okay with them."
2025-12-15 22:19:02 +01:00
# ==========================================================================
# Step 1: Detect server IP addresses
# ==========================================================================
2025-12-09 15:52:37 +01:00
log_menu ""
2025-12-15 22:19:02 +01:00
log_prompt "Detecting server IP addresses..."
# Detect IPv4 address
IP_IPV4 = $( ip -4 addr | sed -ne 's|^.* inet \([^/]*\)/.* scope global.*$|\1|p' | head -1)
# Detect IPv6 address
IP_IPV6 = $( ip -6 addr | sed -ne 's|^.* inet6 \([^/]*\)/.* scope global.*$|\1|p' | head -1)
if [[ -n $IP_IPV4 ]] ; then
log_prompt " IPv4 address detected: $IP_IPV4 "
else
log_prompt " No IPv4 address detected"
fi
if [[ -n $IP_IPV6 ]] ; then
log_prompt " IPv6 address detected: $IP_IPV6 "
else
log_prompt " No IPv6 address detected"
fi
2018-07-15 15:25:59 +06:00
2025-12-15 22:19:02 +01:00
# ==========================================================================
# Step 2: Endpoint type selection
# ==========================================================================
log_menu ""
log_prompt "What IP version should clients use to connect to this server?"
2020-10-20 16:42:35 +02:00
2025-12-15 22:19:02 +01:00
# Determine default based on available addresses
if [[ -n $IP_IPV4 ]] ; then
ENDPOINT_TYPE_DEFAULT = 1
elif [[ -n $IP_IPV6 ]] ; then
ENDPOINT_TYPE_DEFAULT = 2
else
log_fatal "No IPv4 or IPv6 address detected on this server."
2020-04-27 16:24:30 +02:00
fi
2025-12-15 22:19:02 +01:00
log_menu " 1) IPv4"
log_menu " 2) IPv6"
until [[ $ENDPOINT_TYPE_CHOICE = ~ ^[ 1-2] $ ]] ; do
read -rp "Endpoint type [1-2]: " -e -i $ENDPOINT_TYPE_DEFAULT ENDPOINT_TYPE_CHOICE
done
case $ENDPOINT_TYPE_CHOICE in
1)
ENDPOINT_TYPE = "4"
IP = " $IP_IPV4 "
;;
2)
ENDPOINT_TYPE = "6"
IP = " $IP_IPV6 "
;;
esac
# ==========================================================================
# Step 3: Endpoint address (handle NAT for IPv4, direct for IPv6)
# ==========================================================================
2019-02-25 20:02:50 +01:00
APPROVE_IP = ${ APPROVE_IP :- n }
if [[ $APPROVE_IP = ~ n ]] ; then
2025-12-15 22:19:02 +01:00
log_menu ""
if [[ $ENDPOINT_TYPE == "4" ]] ; then
log_prompt "Server listening IPv4 address:"
read -rp "IPv4 address: " -e -i " $IP " IP
else
log_prompt "Server listening IPv6 address:"
read -rp "IPv6 address: " -e -i " $IP " IP
fi
2019-02-25 20:02:50 +01:00
fi
2025-12-15 22:19:02 +01:00
# If IPv4 and private IP, server is behind NAT
if [[ $ENDPOINT_TYPE == "4" ]] && echo " $IP " | grep -qE '^(10\.|172\.1[6789]\.|172\.2[0-9]\.|172\.3[01]\.|192\.168)' ; then
2025-12-09 15:52:37 +01:00
log_menu ""
log_prompt "It seems this server is behind NAT. What is its public IPv4 address or hostname?"
log_prompt "We need it for the clients to connect to the server."
2020-10-20 16:42:35 +02:00
2024-11-07 19:55:14 +00:00
if [[ -z $ENDPOINT ]] ; then
2025-12-15 22:19:02 +01:00
DEFAULT_ENDPOINT = $( resolvePublicIPv4)
2024-11-07 19:55:14 +00:00
fi
2020-04-27 14:59:19 +02:00
until [[ $ENDPOINT != "" ]] ; do
2024-11-07 19:55:14 +00:00
read -rp "Public IPv4 address or hostname: " -e -i " $DEFAULT_ENDPOINT " ENDPOINT
2018-09-21 21:53:39 +02:00
done
2025-12-15 22:19:02 +01:00
elif [[ $ENDPOINT_TYPE == "6" ]] ; then
# For IPv6, check if it's a link-local address (starts with fe80)
if echo " $IP " | grep -qiE '^fe80' ; then
log_menu ""
log_prompt "The detected IPv6 address is link-local. What is the public IPv6 address or hostname?"
log_prompt "We need it for the clients to connect to the server."
if [[ -z $ENDPOINT ]] ; then
DEFAULT_ENDPOINT = $( resolvePublicIPv6)
fi
until [[ $ENDPOINT != "" ]] ; do
read -rp "Public IPv6 address or hostname: " -e -i " $DEFAULT_ENDPOINT " ENDPOINT
done
fi
2018-09-16 17:55:50 +02:00
fi
2018-09-20 00:05:02 +02:00
2025-12-15 22:19:02 +01:00
# ==========================================================================
# Step 4: Client IP versions
# ==========================================================================
2025-12-09 15:52:37 +01:00
log_menu ""
2025-12-15 22:19:02 +01:00
log_prompt "What IP versions should VPN clients use?"
log_prompt "This determines both their VPN addresses and internet access through the tunnel."
# Check IPv6 connectivity for suggestion
2020-04-27 14:59:19 +02:00
if type ping6 >/dev/null 2>& 1; then
2025-12-15 22:19:02 +01:00
PING6 = "ping6 -c1 -W2 ipv6.google.com > /dev/null 2>&1"
2018-10-01 21:00:26 +02:00
else
2025-12-15 22:19:02 +01:00
PING6 = "ping -6 -c1 -W2 ipv6.google.com > /dev/null 2>&1"
2018-10-01 21:00:26 +02:00
fi
2025-12-15 22:19:02 +01:00
HAS_IPV6_CONNECTIVITY = "n"
2018-10-01 21:00:26 +02:00
if eval " $PING6 " ; then
2025-12-15 22:19:02 +01:00
HAS_IPV6_CONNECTIVITY = "y"
fi
# Default suggestion based on connectivity
if [[ $HAS_IPV6_CONNECTIVITY == "y" ]] ; then
CLIENT_IP_DEFAULT = 3 # Dual-stack if IPv6 available
2018-09-16 17:55:50 +02:00
else
2025-12-15 22:19:02 +01:00
CLIENT_IP_DEFAULT = 1 # IPv4 only otherwise
2018-09-16 17:55:50 +02:00
fi
2025-12-15 22:19:02 +01:00
log_menu " 1) IPv4 only"
log_menu " 2) IPv6 only"
log_menu " 3) Dual-stack (IPv4 + IPv6)"
until [[ $CLIENT_IP_CHOICE = ~ ^[ 1-3] $ ]] ; do
read -rp "Client IP versions [1-3]: " -e -i $CLIENT_IP_DEFAULT CLIENT_IP_CHOICE
2025-12-14 10:54:52 +01:00
done
2025-12-15 22:19:02 +01:00
case $CLIENT_IP_CHOICE in
2025-12-14 10:54:52 +01:00
1)
2025-12-15 22:19:02 +01:00
CLIENT_IPV4 = "y"
CLIENT_IPV6 = "n"
2025-12-14 10:54:52 +01:00
;;
2)
2025-12-15 22:19:02 +01:00
CLIENT_IPV4 = "n"
CLIENT_IPV6 = "y"
;;
3)
CLIENT_IPV4 = "y"
CLIENT_IPV6 = "y"
2025-12-14 10:54:52 +01:00
;;
esac
2025-12-15 22:19:02 +01:00
# ==========================================================================
# Step 5: IPv4 subnet (prompt only if IPv4 enabled, but always set for leak prevention)
# ==========================================================================
if [[ $CLIENT_IPV4 == "y" ]] ; then
log_menu ""
log_prompt "IPv4 VPN subnet:"
log_menu " 1) Default: 10.8.0.0/24"
log_menu " 2) Custom"
until [[ $SUBNET_IPV4_CHOICE = ~ ^[ 1-2] $ ]] ; do
read -rp "IPv4 subnet choice [1-2]: " -e -i 1 SUBNET_IPV4_CHOICE
done
case $SUBNET_IPV4_CHOICE in
1)
VPN_SUBNET_IPV4 = "10.8.0.0"
;;
2)
# Skip prompt if VPN_SUBNET_IPV4 is already set (e.g., via environment variable)
if [[ -z $VPN_SUBNET_IPV4 ]] ; then
until [[ $VPN_SUBNET_IPV4 = ~ ^( 10\. ( 25[ 0-5] | 2[ 0-4][ 0-9] | 1[ 0-9]{ 2} | [ 1-9] ?[ 0-9]) \. ( 25[ 0-5] | 2[ 0-4][ 0-9] | 1[ 0-9]{ 2} | [ 1-9] ?[ 0-9]) | 172\. ( 1[ 6-9] | 2[ 0-9] | 3[ 0-1]) \. ( 25[ 0-5] | 2[ 0-4][ 0-9] | 1[ 0-9]{ 2} | [ 1-9] ?[ 0-9]) | 192\. 168\. ( 25[ 0-5] | 2[ 0-4][ 0-9] | 1[ 0-9]{ 2} | [ 1-9] ?[ 0-9])) \. 0$ ]] ; do
read -rp "Custom IPv4 subnet (e.g., 10.9.0.0): " VPN_SUBNET_IPV4
done
fi
;;
esac
else
# IPv6-only mode: still need IPv4 subnet for leak prevention (redirect-gateway def1)
VPN_SUBNET_IPV4 = "10.8.0.0"
fi
# ==========================================================================
# Step 6: IPv6 subnet (if IPv6 enabled for clients)
# ==========================================================================
if [[ $CLIENT_IPV6 == "y" ]] ; then
log_menu ""
log_prompt "IPv6 VPN subnet:"
log_menu " 1) Default: fd42:42:42:42::/112"
log_menu " 2) Custom"
until [[ $SUBNET_IPV6_CHOICE = ~ ^[ 1-2] $ ]] ; do
read -rp "IPv6 subnet choice [1-2]: " -e -i 1 SUBNET_IPV6_CHOICE
done
case $SUBNET_IPV6_CHOICE in
1)
VPN_SUBNET_IPV6 = "fd42:42:42:42::"
;;
2)
# Skip prompt if VPN_SUBNET_IPV6 is already set (e.g., via environment variable)
if [[ -z $VPN_SUBNET_IPV6 ]] ; then
until [[ $VPN_SUBNET_IPV6 = ~ ^fd[ 0-9a-fA-F]{ 0,2}( :[ 0-9a-fA-F]{ 0,4}){ 0,6} ::$ ]] ; do
read -rp "Custom IPv6 subnet (e.g., fd12:3456:789a::): " VPN_SUBNET_IPV6
done
fi
;;
esac
fi
2025-12-14 10:54:52 +01:00
log_menu ""
2025-12-09 15:52:37 +01:00
log_prompt "What port do you want OpenVPN to listen to?"
log_menu " 1) Default: 1194"
log_menu " 2) Custom"
log_menu " 3) Random [49152-65535]"
2020-04-27 14:59:19 +02:00
until [[ $PORT_CHOICE = ~ ^[ 1-3] $ ]] ; do
2018-09-21 23:48:11 +02:00
read -rp "Port choice [1-3]: " -e -i 1 PORT_CHOICE
2018-08-18 09:57:24 -04:00
done
case $PORT_CHOICE in
2020-04-27 14:59:19 +02:00
1)
PORT = "1194"
2018-08-18 09:57:24 -04:00
;;
2020-04-27 14:59:19 +02:00
2)
until [[ $PORT = ~ ^[ 0-9] +$ ]] && [ " $PORT " -ge 1 ] && [ " $PORT " -le 65535 ] ; do
read -rp "Custom port [1-65535]: " -e -i 1194 PORT
done
2018-08-18 09:57:24 -04:00
;;
2020-04-27 14:59:19 +02:00
3)
# Generate random number within private ports range
2025-12-17 23:48:10 +01:00
PORT = $( shuf -i 49152-65535 -n1)
2025-12-09 15:52:37 +01:00
log_info "Random Port: $PORT "
2018-08-18 09:57:24 -04:00
;;
esac
2025-12-09 15:52:37 +01:00
log_menu ""
log_prompt "What protocol do you want OpenVPN to use?"
log_prompt "UDP is faster. Unless it is not available, you shouldn't use TCP."
log_menu " 1) UDP"
log_menu " 2) TCP"
2020-04-27 14:59:19 +02:00
until [[ $PROTOCOL_CHOICE = ~ ^[ 1-2] $ ]] ; do
2018-09-20 00:05:02 +02:00
read -rp "Protocol [1-2]: " -e -i 1 PROTOCOL_CHOICE
2017-11-12 22:51:54 +01:00
done
2018-09-20 00:05:02 +02:00
case $PROTOCOL_CHOICE in
2020-04-27 14:59:19 +02:00
1)
PROTOCOL = "udp"
2018-09-20 00:05:02 +02:00
;;
2020-04-27 14:59:19 +02:00
2)
PROTOCOL = "tcp"
2018-09-20 00:05:02 +02:00
;;
esac
2025-12-09 15:52:37 +01:00
log_menu ""
log_prompt "What DNS resolvers do you want to use with the VPN?"
2025-12-17 23:48:10 +01:00
local dns_labels =( "Current system resolvers (from /etc/resolv.conf)" "Self-hosted DNS Resolver (Unbound)" "Cloudflare (Anycast: worldwide)" "Quad9 (Anycast: worldwide)" "Quad9 uncensored (Anycast: worldwide)" "FDN (France)" "DNS.WATCH (Germany)" "OpenDNS (Anycast: worldwide)" "Google (Anycast: worldwide)" "Yandex Basic (Russia)" "AdGuard DNS (Anycast: worldwide)" "NextDNS (Anycast: worldwide)" "Custom" )
local dns_valid = false
until [[ $dns_valid == true ]] ; do
select_with_labels "DNS" dns_labels DNS_PROVIDERS "cloudflare" DNS
if [[ $DNS == "unbound" ]] && [[ -e /etc/unbound/unbound.conf ]] ; then
2025-12-09 15:52:37 +01:00
log_menu ""
log_prompt "Unbound is already installed."
log_prompt "You can allow the script to configure it in order to use it from your OpenVPN clients"
log_prompt "We will simply add a second server to /etc/unbound/unbound.conf for the OpenVPN subnet."
log_prompt "No changes are made to the current configuration."
log_menu ""
2020-04-27 14:59:19 +02:00
2025-12-17 23:48:10 +01:00
local unbound_continue
until [[ $unbound_continue = ~ ^[ yn] $ ]] ; do
read -rp "Apply configuration changes to Unbound? [y/n]: " -e unbound_continue
2020-04-27 14:59:19 +02:00
done
2025-12-17 23:48:10 +01:00
if [[ $unbound_continue == "n" ]] ; then
2020-04-27 14:59:19 +02:00
unset DNS
2025-12-17 23:48:10 +01:00
else
dns_valid = true
2018-09-16 00:53:33 +02:00
fi
2025-12-17 23:48:10 +01:00
elif [[ $DNS == "custom" ]] ; then
2020-04-27 14:59:19 +02:00
until [[ $DNS1 = ~ ^(( 25[ 0-5] | 2[ 0-4][ 0-9] | [ 01] ?[ 0-9][ 0-9] ?) \. ){ 3}( 25[ 0-5] | 2[ 0-4][ 0-9] | [ 01] ?[ 0-9][ 0-9] ?) $ ]] ; do
read -rp "Primary DNS: " -e DNS1
done
until [[ $DNS2 = ~ ^(( 25[ 0-5] | 2[ 0-4][ 0-9] | [ 01] ?[ 0-9][ 0-9] ?) \. ){ 3}( 25[ 0-5] | 2[ 0-4][ 0-9] | [ 01] ?[ 0-9][ 0-9] ?) $ ]] ; do
read -rp "Secondary DNS (optional): " -e DNS2
if [[ $DNS2 == "" ]] ; then
break
fi
done
2025-12-17 23:48:10 +01:00
dns_valid = true
else
dns_valid = true
2020-04-27 14:59:19 +02:00
fi
2017-11-12 22:51:54 +01:00
done
2025-12-09 15:52:37 +01:00
log_menu ""
2025-12-09 18:30:57 +01:00
log_prompt "Do you want to allow a single .ovpn profile to be used on multiple devices simultaneously?"
2025-12-12 00:04:51 +01:00
log_prompt "Note: Enabling this disables persistent IP addresses for clients."
2025-12-09 18:30:57 +01:00
until [[ $MULTI_CLIENT = ~ ( y| n) ]] ; do
read -rp "Allow multiple devices per client? [y/n]: " -e -i n MULTI_CLIENT
done
2025-12-09 22:12:23 +05:00
log_menu ""
2025-12-15 10:53:15 +01:00
log_prompt "Do you want to customize the tunnel MTU?"
log_menu " MTU controls the maximum packet size. Lower values can help"
log_menu " with connectivity issues on some networks (e.g., PPPoE, mobile)."
log_menu " 1) Default (1500) - works for most networks"
log_menu " 2) Custom"
until [[ $MTU_CHOICE = ~ ^[ 1-2] $ ]] ; do
read -rp "MTU choice [1-2]: " -e -i 1 MTU_CHOICE
done
if [[ $MTU_CHOICE == "2" ]] ; then
until [[ $MTU = ~ ^[ 0-9] +$ ]] && [[ $MTU -ge 576 ]] && [[ $MTU -le 65535 ]] ; do
read -rp "MTU [576-65535]: " -e -i 1500 MTU
done
fi
log_menu ""
2025-12-09 15:52:37 +01:00
log_prompt "Do you want to customize encryption settings?"
log_prompt "Unless you know what you're doing, you should stick with the default parameters provided by the script."
log_prompt "Note that whatever you choose, all the choices presented in the script are safe (unlike OpenVPN's defaults)."
log_prompt "See https://github.com/angristan/openvpn-install#security-and-encryption to learn more."
log_menu ""
2018-09-21 17:17:41 +02:00
until [[ $CUSTOMIZE_ENC = ~ ( y| n) ]] ; do
read -rp "Customize encryption settings? [y/n]: " -e -i n CUSTOMIZE_ENC
2017-11-12 22:51:54 +01:00
done
2020-04-27 14:59:19 +02:00
if [[ $CUSTOMIZE_ENC == "n" ]] ; then
2018-09-28 14:36:00 +00:00
# Use default, sane and fast parameters
2018-09-22 22:33:25 +02:00
CIPHER = "AES-128-GCM"
2025-12-17 23:48:10 +01:00
CERT_TYPE = "ecdsa"
2018-09-23 17:06:15 +02:00
CERT_CURVE = "prime256v1"
2018-09-22 15:11:15 +02:00
CC_CIPHER = "TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256"
2025-12-15 12:13:03 +01:00
TLS13_CIPHERSUITES = "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256"
TLS_VERSION_MIN = "1.2"
TLS_GROUPS = "X25519:prime256v1:secp384r1:secp521r1"
2018-09-22 17:51:38 +02:00
HMAC_ALG = "SHA256"
2025-12-17 23:48:10 +01:00
TLS_SIG = "crypt-v2"
2018-09-21 17:17:41 +02:00
else
2025-12-09 15:52:37 +01:00
log_menu ""
log_prompt "Choose which cipher you want to use for the data channel:"
log_menu " 1) AES-128-GCM (recommended)"
log_menu " 2) AES-192-GCM"
log_menu " 3) AES-256-GCM"
log_menu " 4) AES-128-CBC"
log_menu " 5) AES-192-CBC"
log_menu " 6) AES-256-CBC"
2025-12-10 00:11:25 +01:00
log_menu " 7) CHACHA20-POLY1305 (requires OpenVPN 2.5+, good for devices without AES-NI)"
until [[ $CIPHER_CHOICE = ~ ^[ 1-7] $ ]] ; do
read -rp "Cipher [1-7]: " -e -i 1 CIPHER_CHOICE
2018-09-21 17:17:41 +02:00
done
case $CIPHER_CHOICE in
2020-04-27 14:59:19 +02:00
1)
CIPHER = "AES-128-GCM"
2018-09-21 17:17:41 +02:00
;;
2020-04-27 14:59:19 +02:00
2)
CIPHER = "AES-192-GCM"
2018-09-21 17:17:41 +02:00
;;
2020-04-27 14:59:19 +02:00
3)
CIPHER = "AES-256-GCM"
2018-09-22 14:20:20 +02:00
;;
2020-04-27 14:59:19 +02:00
4)
CIPHER = "AES-128-CBC"
2018-09-22 14:20:20 +02:00
;;
2020-04-27 14:59:19 +02:00
5)
CIPHER = "AES-192-CBC"
2018-09-22 14:20:20 +02:00
;;
2020-04-27 14:59:19 +02:00
6)
CIPHER = "AES-256-CBC"
2018-09-21 17:17:41 +02:00
;;
2025-12-10 00:11:25 +01:00
7)
CIPHER = "CHACHA20-POLY1305"
;;
2018-09-21 17:17:41 +02:00
esac
2025-12-09 15:52:37 +01:00
log_menu ""
log_prompt "Choose what kind of certificate you want to use:"
log_menu " 1) ECDSA (recommended)"
log_menu " 2) RSA"
2025-12-17 23:48:10 +01:00
local cert_type_choice
until [[ $cert_type_choice = ~ ^[ 1-2] $ ]] ; do
read -rp "Certificate key type [1-2]: " -e -i 1 cert_type_choice
2018-09-21 17:17:41 +02:00
done
2025-12-17 23:48:10 +01:00
case $cert_type_choice in
2020-04-27 14:59:19 +02:00
1)
2025-12-17 23:48:10 +01:00
CERT_TYPE = "ecdsa"
2025-12-09 15:52:37 +01:00
log_menu ""
log_prompt "Choose which curve you want to use for the certificate's key:"
2025-12-17 23:48:10 +01:00
select_from_array "Curve" CERT_CURVES "prime256v1" CERT_CURVE
2018-09-21 17:17:41 +02:00
;;
2020-04-27 14:59:19 +02:00
2)
2025-12-17 23:48:10 +01:00
CERT_TYPE = "rsa"
2025-12-09 15:52:37 +01:00
log_menu ""
log_prompt "Choose which size you want to use for the certificate's RSA key:"
2025-12-17 23:48:10 +01:00
select_from_array "RSA key size" RSA_KEY_SIZES "2048" RSA_KEY_SIZE
2018-09-21 17:17:41 +02:00
;;
2018-09-22 15:11:15 +02:00
esac
2025-12-09 15:52:37 +01:00
log_menu ""
log_prompt "Choose which cipher you want to use for the control channel:"
2025-12-17 23:48:10 +01:00
local cc_labels cc_values
if [[ $CERT_TYPE == "ecdsa" ]] ; then
cc_labels =( "ECDHE-ECDSA-AES-128-GCM-SHA256 (recommended)" "ECDHE-ECDSA-AES-256-GCM-SHA384" "ECDHE-ECDSA-CHACHA20-POLY1305 (OpenVPN 2.5+)" )
cc_values =( "TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256" "TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384" "TLS-ECDHE-ECDSA-WITH-CHACHA20-POLY1305-SHA256" )
else
cc_labels =( "ECDHE-RSA-AES-128-GCM-SHA256 (recommended)" "ECDHE-RSA-AES-256-GCM-SHA384" "ECDHE-RSA-CHACHA20-POLY1305 (OpenVPN 2.5+)" )
cc_values =( "TLS-ECDHE-RSA-WITH-AES-128-GCM-SHA256" "TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384" "TLS-ECDHE-RSA-WITH-CHACHA20-POLY1305-SHA256" )
fi
select_with_labels "Control channel cipher" cc_labels cc_values " ${ cc_values [0] } " CC_CIPHER
2025-12-09 15:52:37 +01:00
log_menu ""
2025-12-15 12:13:03 +01:00
log_prompt "Choose the minimum TLS version:"
log_menu " 1) TLS 1.2 (recommended, compatible with all clients)"
log_menu " 2) TLS 1.3 (more secure, requires OpenVPN 2.5+ clients)"
until [[ $TLS_VERSION_MIN_CHOICE = ~ ^[ 1-2] $ ]] ; do
read -rp "Minimum TLS version [1-2]: " -e -i 1 TLS_VERSION_MIN_CHOICE
2018-09-21 17:17:41 +02:00
done
2025-12-15 12:13:03 +01:00
case $TLS_VERSION_MIN_CHOICE in
2020-04-27 14:59:19 +02:00
1)
2025-12-15 12:13:03 +01:00
TLS_VERSION_MIN = "1.2"
2018-09-21 17:17:41 +02:00
;;
2020-04-27 14:59:19 +02:00
2)
2025-12-15 12:13:03 +01:00
TLS_VERSION_MIN = "1.3"
;;
esac
log_menu ""
log_prompt "Choose TLS 1.3 cipher suites (used when TLS 1.3 is negotiated):"
log_menu " 1) All secure ciphers (recommended)"
log_menu " 2) AES-256-GCM only"
log_menu " 3) AES-128-GCM only"
log_menu " 4) ChaCha20-Poly1305 only"
until [[ $TLS13_CIPHER_CHOICE = ~ ^[ 1-4] $ ]] ; do
read -rp "TLS 1.3 cipher suite [1-4]: " -e -i 1 TLS13_CIPHER_CHOICE
done
case $TLS13_CIPHER_CHOICE in
1)
TLS13_CIPHERSUITES = "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256"
;;
2)
TLS13_CIPHERSUITES = "TLS_AES_256_GCM_SHA384"
;;
3)
TLS13_CIPHERSUITES = "TLS_AES_128_GCM_SHA256"
;;
4)
TLS13_CIPHERSUITES = "TLS_CHACHA20_POLY1305_SHA256"
;;
esac
log_menu ""
log_prompt "Choose TLS key exchange groups (for ECDH key exchange):"
log_menu " 1) All modern curves (recommended)"
log_menu " 2) X25519 only (most secure, may have compatibility issues)"
log_menu " 3) NIST curves only (prime256v1, secp384r1, secp521r1)"
until [[ $TLS_GROUPS_CHOICE = ~ ^[ 1-3] $ ]] ; do
read -rp "TLS groups [1-3]: " -e -i 1 TLS_GROUPS_CHOICE
done
case $TLS_GROUPS_CHOICE in
1)
TLS_GROUPS = "X25519:prime256v1:secp384r1:secp521r1"
;;
2)
TLS_GROUPS = "X25519"
;;
3)
TLS_GROUPS = "prime256v1:secp384r1:secp521r1"
2018-09-21 17:17:41 +02:00
;;
esac
2025-12-09 15:52:37 +01:00
log_menu ""
2025-12-10 00:11:25 +01:00
# The "auth" options behaves differently with AEAD ciphers (GCM, ChaCha20-Poly1305)
2020-04-27 14:59:19 +02:00
if [[ $CIPHER = ~ CBC$ ]] ; then
2025-12-09 15:52:37 +01:00
log_prompt "The digest algorithm authenticates data channel packets and tls-auth packets from the control channel."
2025-12-10 00:11:25 +01:00
elif [[ $CIPHER = ~ GCM$ ]] || [[ $CIPHER == "CHACHA20-POLY1305" ]] ; then
2025-12-09 15:52:37 +01:00
log_prompt "The digest algorithm authenticates tls-auth packets from the control channel."
2018-09-22 17:51:38 +02:00
fi
2025-12-09 15:52:37 +01:00
log_prompt "Which digest algorithm do you want to use for HMAC?"
log_menu " 1) SHA-256 (recommended)"
log_menu " 2) SHA-384"
log_menu " 3) SHA-512"
2018-09-22 17:51:38 +02:00
until [[ $HMAC_ALG_CHOICE = ~ ^[ 1-3] $ ]] ; do
read -rp "Digest algorithm [1-3]: " -e -i 1 HMAC_ALG_CHOICE
done
case $HMAC_ALG_CHOICE in
2020-04-27 14:59:19 +02:00
1)
HMAC_ALG = "SHA256"
2018-09-22 17:51:38 +02:00
;;
2020-04-27 14:59:19 +02:00
2)
HMAC_ALG = "SHA384"
2018-09-22 17:51:38 +02:00
;;
2020-04-27 14:59:19 +02:00
3)
HMAC_ALG = "SHA512"
2018-09-22 17:51:38 +02:00
;;
esac
2025-12-09 15:52:37 +01:00
log_menu ""
2025-12-13 14:32:38 +01:00
log_prompt "You can add an additional layer of security to the control channel."
2025-12-17 23:48:10 +01:00
local tls_sig_labels =( "tls-crypt-v2 (recommended): Encrypts control channel, unique key per client" "tls-crypt: Encrypts control channel, shared key for all clients" "tls-auth: Authenticates control channel, no encryption" )
select_with_labels "Control channel security" tls_sig_labels TLS_SIG_MODES "crypt-v2" TLS_SIG
2018-09-21 17:17:41 +02:00
fi
2025-12-09 15:52:37 +01:00
log_menu ""
log_prompt "Okay, that was all I needed. We are ready to setup your OpenVPN server now."
log_prompt "You will be able to generate a client at the end of the installation."
2019-02-25 20:02:50 +01:00
APPROVE_INSTALL = ${ APPROVE_INSTALL :- n }
if [[ $APPROVE_INSTALL = ~ n ]] ; then
read -n1 -r -p "Press any key to continue..."
fi
2018-09-22 17:59:21 +02:00
}
2020-04-27 14:59:19 +02:00
function installOpenVPN() {
2025-12-17 23:48:10 +01:00
if [[ $NON_INTERACTIVE_INSTALL == "y" ]] ; then
# Resolve public IP if ENDPOINT not set
2024-11-07 19:55:14 +00:00
if [[ -z $ENDPOINT ]] ; then
ENDPOINT = $( resolvePublicIP)
2020-04-27 04:56:34 -07:00
fi
2025-12-09 15:52:37 +01:00
2025-12-17 23:48:10 +01:00
# Log non-interactive mode and parameters
log_info "=== OpenVPN Non-Interactive Install ==="
log_info "Running in non-interactive mode with the following settings:"
2025-12-09 15:52:37 +01:00
log_info " ENDPOINT= $ENDPOINT "
2025-12-15 22:19:02 +01:00
log_info " ENDPOINT_TYPE= $ENDPOINT_TYPE "
log_info " CLIENT_IPV4= $CLIENT_IPV4 "
log_info " CLIENT_IPV6= $CLIENT_IPV6 "
log_info " VPN_SUBNET_IPV4= $VPN_SUBNET_IPV4 "
log_info " VPN_SUBNET_IPV6= $VPN_SUBNET_IPV6 "
2025-12-17 23:48:10 +01:00
log_info " PORT= $PORT "
log_info " PROTOCOL= $PROTOCOL "
2025-12-09 15:52:37 +01:00
log_info " DNS= $DNS "
2025-12-15 10:53:15 +01:00
[[ -n $MTU ]] && log_info " MTU= $MTU "
2025-12-09 18:30:57 +01:00
log_info " MULTI_CLIENT= $MULTI_CLIENT "
2025-12-09 15:52:37 +01:00
log_info " CLIENT= $CLIENT "
2025-12-09 23:33:57 +01:00
log_info " CLIENT_CERT_DURATION_DAYS= $CLIENT_CERT_DURATION_DAYS "
log_info " SERVER_CERT_DURATION_DAYS= $SERVER_CERT_DURATION_DAYS "
2019-02-25 21:30:46 +01:00
fi
2017-11-12 22:51:54 +01:00
2018-09-20 00:05:02 +02:00
# Get the "public" interface from the default route
NIC = $( ip -4 route ls | grep default | grep -Po '(?<=dev )(\S+)' | head -1)
2025-12-15 22:19:02 +01:00
if [[ -z $NIC ]] && [[ $CLIENT_IPV6 == 'y' ]] ; then
2020-03-26 21:22:22 +01:00
NIC = $( ip -6 route show default | sed -ne 's/^default .* dev \([^ ]*\) .*$/\1/p' )
fi
2018-09-20 00:05:02 +02:00
2020-03-26 21:27:16 +01:00
# $NIC can not be empty for script rm-openvpn-rules.sh
2020-04-27 14:59:19 +02:00
if [[ -z $NIC ]] ; then
2025-12-09 15:52:37 +01:00
log_warn "Could not detect public interface."
log_info "This needs for setup MASQUERADE."
2020-04-27 14:59:19 +02:00
until [[ $CONTINUE = ~ ( y| n) ]] ; do
read -rp "Continue? [y/n]: " -e CONTINUE
done
if [[ $CONTINUE == "n" ]] ; then
exit 1
fi
fi
2020-03-26 21:27:16 +01:00
2020-04-27 04:56:34 -07:00
# If OpenVPN isn't installed yet, install it. This script is more-or-less
# idempotent on multiple runs, but will only install OpenVPN from upstream
# the first time.
2025-12-12 22:09:18 +01:00
if [[ ! -e /etc/openvpn/server/server.conf ]] ; then
2025-12-09 15:52:37 +01:00
log_header "Installing OpenVPN"
2025-12-09 19:45:56 +01:00
# Setup official OpenVPN repository for latest versions
installOpenVPNRepo
log_info "Installing OpenVPN and dependencies..."
2020-04-27 14:59:19 +02:00
if [[ $OS = ~ ( debian| ubuntu) ]] ; then
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Installing OpenVPN" apt-get install -y openvpn iptables openssl curl ca-certificates tar dnsutils
2020-04-27 14:59:19 +02:00
elif [[ $OS == 'centos' ]] ; then
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Installing OpenVPN" yum install -y openvpn iptables openssl ca-certificates curl tar bind-utils 'policycoreutils-python*'
2021-03-22 10:48:15 +01:00
elif [[ $OS == 'oracle' ]] ; then
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Installing OpenVPN" yum install -y openvpn iptables openssl ca-certificates curl tar bind-utils policycoreutils-python-utils
2025-03-10 05:24:45 -04:00
elif [[ $OS == 'amzn2023' ]] ; then
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Installing OpenVPN" dnf install -y openvpn iptables openssl ca-certificates curl tar bind-utils
2020-04-27 14:59:19 +02:00
elif [[ $OS == 'fedora' ]] ; then
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Installing OpenVPN" dnf install -y openvpn iptables openssl ca-certificates curl tar bind-utils policycoreutils-python-utils
2025-12-12 04:22:12 +08:00
elif [[ $OS == 'opensuse' ]] ; then
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Installing OpenVPN" zypper install -y openvpn iptables openssl ca-certificates curl tar bind-utils
2020-04-27 14:59:19 +02:00
elif [[ $OS == 'arch' ]] ; then
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Installing OpenVPN" pacman --needed --noconfirm -Syu openvpn iptables openssl ca-certificates curl tar bind
2017-11-12 22:51:54 +01:00
fi
2025-12-10 00:11:25 +01:00
# Verify ChaCha20-Poly1305 compatibility if selected
if [[ $CIPHER == "CHACHA20-POLY1305" ]] || [[ $CC_CIPHER = ~ CHACHA20 ]] ; then
local installed_version
installed_version = $( openvpn --version 2>/dev/null | head -1 | awk '{print $2}' )
if ! openvpnVersionAtLeast "2.5" ; then
log_fatal "ChaCha20-Poly1305 requires OpenVPN 2.5 or later. Installed version: $installed_version "
fi
log_info "OpenVPN version supports ChaCha20-Poly1305"
fi
2025-12-10 18:53:45 +01:00
# Check Data Channel Offload (DCO) availability
if isDCOAvailable; then
2025-12-15 22:19:02 +01:00
# Check if configuration is DCO-compatible (udp or udp6)
if [[ $PROTOCOL = ~ ^udp ]] && [[ $CIPHER = ~ ( GCM| CHACHA20-POLY1305) ]] ; then
2025-12-10 18:53:45 +01:00
log_info "Data Channel Offload (DCO) is available and will be used for improved performance"
else
2025-12-15 10:04:02 +01:00
log_info "Data Channel Offload (DCO) is available but not enabled (requires UDP, AEAD cipher)"
2025-12-10 18:53:45 +01:00
fi
else
log_info "Data Channel Offload (DCO) is not available (requires OpenVPN 2.6+ and kernel support)"
fi
2025-12-12 22:09:18 +01:00
# Create the server directory (OpenVPN 2.4+ directory structure)
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Creating server directory" mkdir -p /etc/openvpn/server
2018-09-17 01:11:30 +02:00
fi
2025-12-12 22:09:18 +01:00
# Determine which user/group OpenVPN should run as
# - Fedora/RHEL/Amazon create 'openvpn' user with 'openvpn' group
# - Arch creates 'openvpn' user with 'network' group
# - Debian/Ubuntu/openSUSE don't create a dedicated user, use 'nobody'
#
# Also check if the systemd service file already handles user/group switching.
# If so, we shouldn't add user/group to config (would cause double privilege drop).
SYSTEMD_HANDLES_USER = false
for service_file in /usr/lib/systemd/system/openvpn-server@.service /lib/systemd/system/openvpn-server@.service; do
if [[ -f " $service_file " ]] && grep -q "^User=" " $service_file " ; then
SYSTEMD_HANDLES_USER = true
break
fi
done
if id openvpn & >/dev/null; then
OPENVPN_USER = openvpn
# Get the openvpn user's primary group (e.g., 'openvpn' on Fedora, 'network' on Arch)
OPENVPN_GROUP = $( id -gn openvpn 2>/dev/null || echo openvpn)
2017-11-12 22:51:54 +01:00
else
2025-12-12 22:09:18 +01:00
OPENVPN_USER = nobody
if grep -qs "^nogroup:" /etc/group; then
OPENVPN_GROUP = nogroup
else
OPENVPN_GROUP = nobody
fi
2017-11-12 22:51:54 +01:00
fi
2020-04-27 19:20:40 +02:00
# Install the latest version of easy-rsa from source, if not already installed.
2025-12-12 22:09:18 +01:00
if [[ ! -d /etc/openvpn/server/easy-rsa/ ]] ; then
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Downloading Easy-RSA v ${ EASYRSA_VERSION } " curl -fL --retry 5 -o ~/easy-rsa.tgz "https://github.com/OpenVPN/easy-rsa/releases/download/v ${ EASYRSA_VERSION } /EasyRSA- ${ EASYRSA_VERSION } .tgz"
2025-12-09 15:52:37 +01:00
log_info "Verifying Easy-RSA checksum..."
CHECKSUM_OUTPUT = $( echo " ${ EASYRSA_SHA256 } $HOME /easy-rsa.tgz" | sha256sum -c 2>& 1) || {
_log_to_file "[CHECKSUM] $CHECKSUM_OUTPUT "
run_cmd "Cleaning up failed download" rm -f ~/easy-rsa.tgz
log_fatal "SHA256 checksum verification failed for easy-rsa download!"
}
_log_to_file "[CHECKSUM] $CHECKSUM_OUTPUT "
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Creating Easy-RSA directory" mkdir -p /etc/openvpn/server/easy-rsa
run_cmd_fatal "Extracting Easy-RSA" tar xzf ~/easy-rsa.tgz --strip-components= 1 --no-same-owner --directory /etc/openvpn/server/easy-rsa
2025-12-09 15:52:37 +01:00
run_cmd "Cleaning up archive" rm -f ~/easy-rsa.tgz
2020-04-27 04:56:34 -07:00
2025-12-12 22:09:18 +01:00
cd /etc/openvpn/server/easy-rsa/ || return
2020-04-27 04:56:34 -07:00
case $CERT_TYPE in
2025-12-17 23:48:10 +01:00
ecdsa)
2020-04-27 14:59:19 +02:00
echo "set_var EASYRSA_ALGO ec" >vars
echo "set_var EASYRSA_CURVE $CERT_CURVE " >>vars
2020-04-27 04:56:34 -07:00
;;
2025-12-17 23:48:10 +01:00
rsa)
2020-04-27 14:59:19 +02:00
echo "set_var EASYRSA_KEY_SIZE $RSA_KEY_SIZE " >vars
2020-04-27 04:56:34 -07:00
;;
esac
2018-09-20 00:05:02 +02:00
2020-04-27 04:56:34 -07:00
# Generate a random, alphanumeric identifier of 16 characters for CN and one for server name
2025-12-14 00:23:43 +01:00
# Note: 2>/dev/null suppresses "Broken pipe" errors from fold when head exits early
SERVER_CN = "cn_ $( head /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 2>/dev/null | head -n 1) "
2020-04-27 14:59:19 +02:00
echo " $SERVER_CN " >SERVER_CN_GENERATED
2025-12-14 00:23:43 +01:00
SERVER_NAME = "server_ $( head /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 2>/dev/null | head -n 1) "
2020-04-27 14:59:19 +02:00
echo " $SERVER_NAME " >SERVER_NAME_GENERATED
2018-09-23 16:27:36 +02:00
2020-04-27 04:56:34 -07:00
# Create the PKI, set up the CA, the DH params and the server certificate
2025-12-09 15:52:37 +01:00
log_info "Initializing PKI..."
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Initializing PKI" ./easyrsa init-pki
2025-12-09 23:33:57 +01:00
export EASYRSA_CA_EXPIRE = $DEFAULT_CERT_VALIDITY_DURATION_DAYS
2025-12-09 15:52:37 +01:00
log_info "Building CA..."
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Building CA" ./easyrsa --batch --req-cn= " $SERVER_CN " build-ca nopass
2018-09-23 16:27:36 +02:00
2025-12-09 23:33:57 +01:00
export EASYRSA_CERT_EXPIRE = ${ SERVER_CERT_DURATION_DAYS :- $DEFAULT_CERT_VALIDITY_DURATION_DAYS }
2025-12-09 15:52:37 +01:00
log_info "Building server certificate..."
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Building server certificate" ./easyrsa --batch build-server-full " $SERVER_NAME " nopass
2025-12-09 23:33:57 +01:00
export EASYRSA_CRL_DAYS = $DEFAULT_CRL_VALIDITY_DURATION_DAYS
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Generating CRL" ./easyrsa gen-crl
2019-06-30 23:06:33 +02:00
2025-12-09 15:52:37 +01:00
log_info "Generating TLS key..."
2020-04-27 04:56:34 -07:00
case $TLS_SIG in
2025-12-17 23:48:10 +01:00
crypt-v2)
2025-12-13 14:32:38 +01:00
# Generate tls-crypt-v2 server key
run_cmd_fatal "Generating tls-crypt-v2 server key" openvpn --genkey tls-crypt-v2-server /etc/openvpn/server/tls-crypt-v2.key
;;
2025-12-17 23:48:10 +01:00
crypt)
2020-04-27 14:59:19 +02:00
# Generate tls-crypt key
2025-12-13 18:59:40 +01:00
run_cmd_fatal "Generating tls-crypt key" openvpn --genkey secret /etc/openvpn/server/tls-crypt.key
2020-04-27 04:56:34 -07:00
;;
2025-12-17 23:48:10 +01:00
auth)
2020-04-27 14:59:19 +02:00
# Generate tls-auth key
2025-12-13 18:59:40 +01:00
run_cmd_fatal "Generating tls-auth key" openvpn --genkey secret /etc/openvpn/server/tls-auth.key
2020-04-27 04:56:34 -07:00
;;
esac
else
# If easy-rsa is already installed, grab the generated SERVER_NAME
# for client configs
2025-12-12 22:09:18 +01:00
cd /etc/openvpn/server/easy-rsa/ || return
2020-04-27 04:56:34 -07:00
SERVER_NAME = $( cat SERVER_NAME_GENERATED)
fi
2019-06-30 23:06:33 +02:00
2017-11-12 22:51:54 +01:00
# Move all the generated files
2025-12-09 15:52:37 +01:00
log_info "Copying certificates..."
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Copying certificates to /etc/openvpn/server" cp pki/ca.crt pki/private/ca.key "pki/issued/ $SERVER_NAME .crt" "pki/private/ $SERVER_NAME .key" /etc/openvpn/server/easy-rsa/pki/crl.pem /etc/openvpn/server
2019-06-30 23:06:33 +02:00
2017-11-12 22:51:54 +01:00
# Make cert revocation list readable for non-root
2025-12-12 22:09:18 +01:00
run_cmd "Setting CRL permissions" chmod 644 /etc/openvpn/server/crl.pem
2017-11-12 22:51:54 +01:00
# Generate server.conf
2025-12-09 15:52:37 +01:00
log_info "Generating server configuration..."
2025-12-12 22:09:18 +01:00
echo "port $PORT " >/etc/openvpn/server/server.conf
2025-12-15 22:19:02 +01:00
# Protocol selection: use proto6 variants if endpoint is IPv6
if [[ $ENDPOINT_TYPE == "6" ]] ; then
2025-12-12 22:09:18 +01:00
echo "proto ${ PROTOCOL } 6" >>/etc/openvpn/server/server.conf
2025-12-15 22:19:02 +01:00
else
echo "proto $PROTOCOL " >>/etc/openvpn/server/server.conf
2018-09-16 17:55:50 +02:00
fi
2018-09-20 00:05:02 +02:00
2025-12-09 22:12:23 +05:00
if [[ $MULTI_CLIENT == "y" ]] ; then
2025-12-12 22:09:18 +01:00
echo "duplicate-cn" >>/etc/openvpn/server/server.conf
2025-12-09 22:12:23 +05:00
fi
2025-12-12 22:09:18 +01:00
echo "dev tun" >>/etc/openvpn/server/server.conf
# Only add user/group if systemd doesn't handle it (avoids double privilege drop)
if [[ $SYSTEMD_HANDLES_USER == "false" ]] ; then
echo "user $OPENVPN_USER
group $OPENVPN_GROUP " >>/etc/openvpn/server/server.conf
fi
echo "persist-key
2016-11-28 22:13:32 +01:00
persist-tun
keepalive 10 120
2025-12-15 22:19:02 +01:00
topology subnet" >>/etc/openvpn/server/server.conf
# IPv4 server directive - always assign IPv4 to clients for proper routing
# Even for IPv6-only mode, we need IPv4 addresses so redirect-gateway def1 can block IPv4 leaks
echo "server $VPN_SUBNET_IPV4 255.255.255.0" >>/etc/openvpn/server/server.conf
# IPv6 server directive (only if clients get IPv6)
if [[ $CLIENT_IPV6 == "y" ]] ; then
{
echo "server-ipv6 ${ VPN_SUBNET_IPV6 } /112"
echo "tun-ipv6"
echo "push tun-ipv6"
} >>/etc/openvpn/server/server.conf
fi
2025-12-12 00:04:51 +01:00
# ifconfig-pool-persist is incompatible with duplicate-cn
if [[ $MULTI_CLIENT != "y" ]] ; then
2025-12-12 22:09:18 +01:00
echo "ifconfig-pool-persist ipp.txt" >>/etc/openvpn/server/server.conf
2025-12-12 00:04:51 +01:00
fi
2018-09-22 14:21:20 +02:00
2017-11-12 22:51:54 +01:00
# DNS resolvers
case $DNS in
2025-12-17 23:48:10 +01:00
system)
2020-04-27 14:59:19 +02:00
# Locate the proper resolv.conf
# Needed for systems running systemd-resolved
if grep -q "127.0.0.53" "/etc/resolv.conf" ; then
RESOLVCONF = '/run/systemd/resolve/resolv.conf'
else
RESOLVCONF = '/etc/resolv.conf'
fi
# Obtain the resolvers from resolv.conf and use them for OpenVPN
2020-04-27 18:04:18 +02:00
sed -ne 's/^nameserver[[:space:]]\+\([^[:space:]]\+\).*$/\1/p' $RESOLVCONF | while read -r line; do
2025-12-15 22:19:02 +01:00
# Copy IPv4 resolvers if client has IPv4, or IPv6 resolvers if client has IPv6
if [[ $line = ~ ^[ 0-9.] *$ ]] && [[ $CLIENT_IPV4 == 'y' ]] ; then
echo "push \"dhcp-option DNS $line \"" >>/etc/openvpn/server/server.conf
elif [[ $line = ~ : ]] && [[ $CLIENT_IPV6 == 'y' ]] ; then
2025-12-12 22:09:18 +01:00
echo "push \"dhcp-option DNS $line \"" >>/etc/openvpn/server/server.conf
2020-04-27 18:04:18 +02:00
fi
2020-04-27 14:59:19 +02:00
done
2017-11-12 22:51:54 +01:00
;;
2025-12-17 23:48:10 +01:00
unbound)
2025-12-15 22:19:02 +01:00
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo "push \"dhcp-option DNS $VPN_GATEWAY_IPV4 \"" >>/etc/openvpn/server/server.conf
fi
if [[ $CLIENT_IPV6 == 'y' ]] ; then
echo "push \"dhcp-option DNS $VPN_GATEWAY_IPV6 \"" >>/etc/openvpn/server/server.conf
2020-05-01 00:10:11 +02:00
fi
2018-09-16 00:53:33 +02:00
;;
2025-12-17 23:48:10 +01:00
cloudflare)
2025-12-15 22:19:02 +01:00
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo 'push "dhcp-option DNS 1.0.0.1"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 1.1.1.1"' >>/etc/openvpn/server/server.conf
fi
if [[ $CLIENT_IPV6 == 'y' ]] ; then
echo 'push "dhcp-option DNS 2606:4700:4700::1001"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 2606:4700:4700::1111"' >>/etc/openvpn/server/server.conf
fi
2018-04-01 23:12:05 +02:00
;;
2025-12-17 23:48:10 +01:00
quad9)
2025-12-15 22:19:02 +01:00
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo 'push "dhcp-option DNS 9.9.9.9"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 149.112.112.112"' >>/etc/openvpn/server/server.conf
fi
if [[ $CLIENT_IPV6 == 'y' ]] ; then
echo 'push "dhcp-option DNS 2620:fe::fe"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 2620:fe::9"' >>/etc/openvpn/server/server.conf
fi
2017-11-29 11:21:33 +01:00
;;
2025-12-17 23:48:10 +01:00
quad9-uncensored)
2025-12-15 22:19:02 +01:00
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo 'push "dhcp-option DNS 9.9.9.10"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 149.112.112.10"' >>/etc/openvpn/server/server.conf
fi
if [[ $CLIENT_IPV6 == 'y' ]] ; then
echo 'push "dhcp-option DNS 2620:fe::10"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 2620:fe::fe:10"' >>/etc/openvpn/server/server.conf
fi
2018-09-24 11:42:29 +02:00
;;
2025-12-17 23:48:10 +01:00
fdn)
2025-12-15 22:19:02 +01:00
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo 'push "dhcp-option DNS 80.67.169.40"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 80.67.169.12"' >>/etc/openvpn/server/server.conf
fi
if [[ $CLIENT_IPV6 == 'y' ]] ; then
echo 'push "dhcp-option DNS 2001:910:800::40"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 2001:910:800::12"' >>/etc/openvpn/server/server.conf
fi
2017-11-12 22:51:54 +01:00
;;
2025-12-17 23:48:10 +01:00
dnswatch)
2025-12-15 22:19:02 +01:00
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo 'push "dhcp-option DNS 84.200.69.80"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 84.200.70.40"' >>/etc/openvpn/server/server.conf
fi
if [[ $CLIENT_IPV6 == 'y' ]] ; then
echo 'push "dhcp-option DNS 2001:1608:10:25::1c04:b12f"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 2001:1608:10:25::9249:d69b"' >>/etc/openvpn/server/server.conf
fi
2017-11-12 22:51:54 +01:00
;;
2025-12-17 23:48:10 +01:00
opendns)
2025-12-15 22:19:02 +01:00
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo 'push "dhcp-option DNS 208.67.222.222"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 208.67.220.220"' >>/etc/openvpn/server/server.conf
fi
if [[ $CLIENT_IPV6 == 'y' ]] ; then
echo 'push "dhcp-option DNS 2620:119:35::35"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 2620:119:53::53"' >>/etc/openvpn/server/server.conf
fi
2017-11-12 22:51:54 +01:00
;;
2025-12-17 23:48:10 +01:00
google)
2025-12-15 22:19:02 +01:00
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo 'push "dhcp-option DNS 8.8.8.8"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 8.8.4.4"' >>/etc/openvpn/server/server.conf
fi
if [[ $CLIENT_IPV6 == 'y' ]] ; then
echo 'push "dhcp-option DNS 2001:4860:4860::8888"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 2001:4860:4860::8844"' >>/etc/openvpn/server/server.conf
fi
2017-11-12 22:51:54 +01:00
;;
2025-12-17 23:48:10 +01:00
yandex)
2025-12-15 22:19:02 +01:00
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo 'push "dhcp-option DNS 77.88.8.8"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 77.88.8.1"' >>/etc/openvpn/server/server.conf
fi
if [[ $CLIENT_IPV6 == 'y' ]] ; then
echo 'push "dhcp-option DNS 2a02:6b8::feed:0ff"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 2a02:6b8:0:1::feed:0ff"' >>/etc/openvpn/server/server.conf
fi
2017-11-12 22:51:54 +01:00
;;
2025-12-17 23:48:10 +01:00
adguard)
2025-12-15 22:19:02 +01:00
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo 'push "dhcp-option DNS 94.140.14.14"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 94.140.15.15"' >>/etc/openvpn/server/server.conf
fi
if [[ $CLIENT_IPV6 == 'y' ]] ; then
echo 'push "dhcp-option DNS 2a10:50c0::ad1:ff"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 2a10:50c0::ad2:ff"' >>/etc/openvpn/server/server.conf
fi
2017-11-12 22:51:54 +01:00
;;
2025-12-17 23:48:10 +01:00
nextdns)
2025-12-15 22:19:02 +01:00
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo 'push "dhcp-option DNS 45.90.28.167"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 45.90.30.167"' >>/etc/openvpn/server/server.conf
fi
if [[ $CLIENT_IPV6 == 'y' ]] ; then
echo 'push "dhcp-option DNS 2a07:a8c0::"' >>/etc/openvpn/server/server.conf
echo 'push "dhcp-option DNS 2a07:a8c1::"' >>/etc/openvpn/server/server.conf
fi
2020-03-03 23:04:18 +01:00
;;
2025-12-17 23:48:10 +01:00
custom)
2025-12-12 22:09:18 +01:00
echo "push \"dhcp-option DNS $DNS1 \"" >>/etc/openvpn/server/server.conf
2020-04-27 14:59:19 +02:00
if [[ $DNS2 != "" ]] ; then
2025-12-12 22:09:18 +01:00
echo "push \"dhcp-option DNS $DNS2 \"" >>/etc/openvpn/server/server.conf
2019-08-20 21:02:47 +02:00
fi
;;
2017-11-12 22:51:54 +01:00
esac
2018-09-16 17:55:50 +02:00
2025-12-15 22:19:02 +01:00
# Redirect gateway settings - always redirect both IPv4 and IPv6 to prevent leaks
# For IPv4: redirect-gateway def1 routes all IPv4 through VPN (or drops it if IPv4 not configured)
# For IPv6: route-ipv6 + redirect-gateway ipv6 routes all IPv6, or block-ipv6 drops it
echo 'push "redirect-gateway def1 bypass-dhcp"' >>/etc/openvpn/server/server.conf
if [[ $CLIENT_IPV6 == "y" ]] ; then
echo 'push "route-ipv6 2000::/3"' >>/etc/openvpn/server/server.conf
echo 'push "redirect-gateway ipv6"' >>/etc/openvpn/server/server.conf
else
# Block IPv6 on clients to prevent IPv6 leaks when VPN only handles IPv4
echo 'push "block-ipv6"' >>/etc/openvpn/server/server.conf
2018-09-16 17:55:50 +02:00
fi
2025-12-15 10:53:15 +01:00
if [[ -n $MTU ]] ; then
echo "tun-mtu $MTU " >>/etc/openvpn/server/server.conf
fi
2025-12-15 12:13:03 +01:00
# Use ECDH key exchange (dh none) with tls-groups for curve negotiation
echo "dh none" >>/etc/openvpn/server/server.conf
echo "tls-groups $TLS_GROUPS " >>/etc/openvpn/server/server.conf
2018-09-22 14:07:51 +02:00
2018-09-22 22:34:10 +02:00
case $TLS_SIG in
2025-12-17 23:48:10 +01:00
crypt-v2)
2025-12-13 14:32:38 +01:00
echo "tls-crypt-v2 tls-crypt-v2.key" >>/etc/openvpn/server/server.conf
2018-09-22 22:34:10 +02:00
;;
2025-12-17 23:48:10 +01:00
crypt)
2025-12-13 14:32:38 +01:00
echo "tls-crypt tls-crypt.key" >>/etc/openvpn/server/server.conf
;;
2025-12-17 23:48:10 +01:00
auth)
2025-12-12 22:09:18 +01:00
echo "tls-auth tls-auth.key 0" >>/etc/openvpn/server/server.conf
2018-09-22 22:34:10 +02:00
;;
esac
2018-09-16 17:55:50 +02:00
echo "crl-verify crl.pem
2016-11-28 22:13:32 +01:00
ca ca.crt
2018-01-18 17:19:51 +01:00
cert $SERVER_NAME .crt
2019-06-30 23:06:33 +02:00
key $SERVER_NAME .key
2018-09-22 17:51:38 +02:00
auth $HMAC_ALG
2018-09-22 18:18:36 +02:00
cipher $CIPHER
2025-12-12 10:23:36 +01:00
ignore-unknown-option data-ciphers
data-ciphers $CIPHER
2018-09-22 18:18:36 +02:00
ncp-ciphers $CIPHER
2016-11-28 22:13:32 +01:00
tls-server
2025-12-15 12:13:03 +01:00
tls-version-min $TLS_VERSION_MIN
2025-12-12 00:47:10 +01:00
remote-cert-tls client
2018-09-22 15:11:15 +02:00
tls-cipher $CC_CIPHER
2025-12-15 12:13:03 +01:00
tls-ciphersuites $TLS13_CIPHERSUITES
2025-12-12 22:09:18 +01:00
client-config-dir ccd
2018-08-22 22:11:36 +02:00
status /var/log/openvpn/status.log
2025-12-12 22:09:18 +01:00
verb 3" >>/etc/openvpn/server/server.conf
2016-11-24 20:28:49 +01:00
2020-04-10 15:49:07 +00:00
# Create client-config-dir dir
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Creating client config directory" mkdir -p /etc/openvpn/server/ccd
2018-09-16 22:45:04 +02:00
# Create log dir
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Creating log directory" mkdir -p /var/log/openvpn
2017-11-12 22:51:54 +01:00
2025-12-12 22:09:18 +01:00
# On distros that use a dedicated OpenVPN user (not "nobody"), e.g., Fedora, RHEL, Arch,
# set ownership so OpenVPN can read config/certs and write to log directory
if [[ $OPENVPN_USER != "nobody" ]] ; then
log_info "Setting ownership for OpenVPN user..."
chown -R " $OPENVPN_USER : $OPENVPN_GROUP " /etc/openvpn/server
chown " $OPENVPN_USER : $OPENVPN_GROUP " /var/log/openvpn
fi
2018-09-16 17:55:50 +02:00
# Enable routing
2025-12-09 15:52:37 +01:00
log_info "Enabling IP forwarding..."
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Creating sysctl.d directory" mkdir -p /etc/sysctl.d
2025-12-15 22:19:02 +01:00
# Enable IPv4 forwarding if clients get IPv4
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo 'net.ipv4.ip_forward=1' >/etc/sysctl.d/99-openvpn.conf
else
echo '# IPv4 forwarding not needed (no IPv4 clients)' >/etc/sysctl.d/99-openvpn.conf
fi
# Enable IPv6 forwarding if clients get IPv6
if [[ $CLIENT_IPV6 == 'y' ]] ; then
2020-10-20 15:44:52 -06:00
echo 'net.ipv6.conf.all.forwarding=1' >>/etc/sysctl.d/99-openvpn.conf
2017-11-12 22:51:54 +01:00
fi
2019-08-20 17:58:51 +02:00
# Apply sysctl rules
2025-12-09 15:52:37 +01:00
run_cmd "Applying sysctl rules" sysctl --system
2018-07-15 15:25:59 +06:00
2017-11-12 22:51:54 +01:00
# If SELinux is enabled and a custom port was selected, we need this
if hash sestatus 2>/dev/null; then
if sestatus | grep "Current mode" | grep -qs "enforcing" ; then
2020-04-27 14:59:19 +02:00
if [[ $PORT != '1194' ]] ; then
2025-12-15 22:19:02 +01:00
# Strip "6" suffix from protocol (semanage expects "udp" or "tcp", not "udp6"/"tcp6")
SELINUX_PROTOCOL = " ${ PROTOCOL %6 } "
run_cmd "Configuring SELinux port" semanage port -a -t openvpn_port_t -p " $SELINUX_PROTOCOL " " $PORT "
2017-11-12 22:51:54 +01:00
fi
fi
fi
2018-07-15 15:25:59 +06:00
2018-09-18 14:55:00 +02:00
# Finally, restart and enable OpenVPN
2025-12-12 22:09:18 +01:00
# OpenVPN 2.4+ uses openvpn-server@.service with config in /etc/openvpn/server/
2025-12-09 15:52:37 +01:00
log_info "Configuring OpenVPN service..."
2018-09-24 14:33:08 +02:00
2025-12-12 22:09:18 +01:00
# Find the service file (location and name vary by distro)
# Modern distros: openvpn-server@.service in /usr/lib/systemd/system/ or /lib/systemd/system/
# openSUSE: openvpn@.service (old-style) that we need to adapt
if [[ -f /usr/lib/systemd/system/openvpn-server@.service ]] ; then
SERVICE_SOURCE = "/usr/lib/systemd/system/openvpn-server@.service"
elif [[ -f /lib/systemd/system/openvpn-server@.service ]] ; then
SERVICE_SOURCE = "/lib/systemd/system/openvpn-server@.service"
elif [[ -f /usr/lib/systemd/system/openvpn@.service ]] ; then
# openSUSE uses old-style service, we'll create our own openvpn-server@.service
SERVICE_SOURCE = "/usr/lib/systemd/system/openvpn@.service"
elif [[ -f /lib/systemd/system/openvpn@.service ]] ; then
SERVICE_SOURCE = "/lib/systemd/system/openvpn@.service"
2017-11-12 22:51:54 +01:00
else
2025-12-12 22:09:18 +01:00
log_fatal "Could not find openvpn-server@.service or openvpn@.service file"
fi
2019-06-30 23:06:33 +02:00
2025-12-12 22:09:18 +01:00
# Don't modify package-provided service, copy to /etc/systemd/system/
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Copying OpenVPN service file" cp " $SERVICE_SOURCE " /etc/systemd/system/openvpn-server@.service
2019-06-30 23:06:33 +02:00
2025-12-12 22:09:18 +01:00
# Workaround to fix OpenVPN service on OpenVZ
run_cmd "Patching service file (LimitNPROC)" sed -i 's|LimitNPROC|#LimitNPROC|' /etc/systemd/system/openvpn-server@.service
# Ensure the service uses /etc/openvpn/server/ as working directory
# This is needed for openSUSE which uses old-style paths by default
if grep -q "cd /etc/openvpn/" /etc/systemd/system/openvpn-server@.service; then
run_cmd "Patching service file (paths)" sed -i 's|/etc/openvpn/|/etc/openvpn/server/|g' /etc/systemd/system/openvpn-server@.service
2017-11-12 22:51:54 +01:00
fi
2018-07-15 15:25:59 +06:00
2025-12-12 22:09:18 +01:00
run_cmd "Reloading systemd" systemctl daemon-reload
run_cmd "Enabling OpenVPN service" systemctl enable openvpn-server@server
run_cmd "Starting OpenVPN service" systemctl restart openvpn-server@server
2025-12-17 23:48:10 +01:00
if [[ $DNS == "unbound" ]] ; then
2018-09-20 00:05:02 +02:00
installUnbound
fi
2025-12-13 20:49:40 +01:00
# Configure firewall rules
2025-12-16 09:58:30 +01:00
# Use source-based rules for VPN traffic (works reliably regardless of which tun interface OpenVPN uses)
2025-12-09 15:52:37 +01:00
log_info "Configuring firewall rules..."
2018-09-20 00:05:02 +02:00
2025-12-13 20:49:40 +01:00
if systemctl is-active --quiet firewalld; then
# Use firewalld native commands for systems with firewalld active
log_info "firewalld detected, using firewall-cmd..."
run_cmd "Adding OpenVPN port to firewalld" firewall-cmd --permanent --add-port= " $PORT / $PROTOCOL "
run_cmd "Adding masquerade to firewalld" firewall-cmd --permanent --add-masquerade
2025-12-16 09:58:30 +01:00
# Add rich rules for VPN traffic (source-based only, as firewalld doesn't reliably
# support interface patterns with direct rules when using nftables backend)
2025-12-15 22:19:02 +01:00
if [[ $CLIENT_IPV4 == 'y' ]] ; then
run_cmd "Adding IPv4 VPN subnet rule" firewall-cmd --permanent --add-rich-rule= "rule family=\"ipv4\" source address=\" $VPN_SUBNET_IPV4 /24\" accept"
fi
2025-12-13 20:49:40 +01:00
2025-12-15 22:19:02 +01:00
if [[ $CLIENT_IPV6 == 'y' ]] ; then
run_cmd "Adding IPv6 VPN subnet rule" firewall-cmd --permanent --add-rich-rule= "rule family=\"ipv6\" source address=\" ${ VPN_SUBNET_IPV6 } /112\" accept"
2025-12-13 20:49:40 +01:00
fi
run_cmd "Reloading firewalld" firewall-cmd --reload
2025-12-14 00:03:29 +01:00
elif systemctl is-active --quiet nftables; then
# Use nftables native rules for systems with nftables active
log_info "nftables detected, configuring nftables rules..."
run_cmd_fatal "Creating nftables directory" mkdir -p /etc/nftables
2025-12-16 09:58:30 +01:00
# Create nftables rules file
{
echo "table inet openvpn {"
echo " chain input {"
echo " type filter hook input priority 0; policy accept;"
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo " iifname \"tun*\" ip saddr $VPN_SUBNET_IPV4 /24 accept"
fi
if [[ $CLIENT_IPV6 == 'y' ]] ; then
echo " iifname \"tun*\" ip6 saddr ${ VPN_SUBNET_IPV6 } /112 accept"
fi
echo " iifname \" $NIC \" $PROTOCOL dport $PORT accept"
echo " }"
echo ""
echo " chain forward {"
echo " type filter hook forward priority 0; policy accept;"
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo " iifname \"tun*\" ip saddr $VPN_SUBNET_IPV4 /24 accept"
echo " oifname \"tun*\" ip daddr $VPN_SUBNET_IPV4 /24 accept"
fi
if [[ $CLIENT_IPV6 == 'y' ]] ; then
echo " iifname \"tun*\" ip6 saddr ${ VPN_SUBNET_IPV6 } /112 accept"
echo " oifname \"tun*\" ip6 daddr ${ VPN_SUBNET_IPV6 } /112 accept"
fi
echo " }"
echo "}"
} >/etc/nftables/openvpn.nft
2025-12-14 00:03:29 +01:00
2025-12-15 22:19:02 +01:00
# IPv4 NAT rules (only if clients get IPv4)
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo "
2025-12-14 00:03:29 +01:00
table ip openvpn-nat {
chain postrouting {
type nat hook postrouting priority 100; policy accept;
2025-12-15 22:19:02 +01:00
ip saddr $VPN_SUBNET_IPV4 /24 oifname \" $NIC \" masquerade
2025-12-14 00:03:29 +01:00
}
2025-12-15 22:19:02 +01:00
}" >>/etc/nftables/openvpn.nft
fi
2025-12-14 00:03:29 +01:00
2025-12-15 22:19:02 +01:00
# IPv6 NAT rules (only if clients get IPv6)
if [[ $CLIENT_IPV6 == 'y' ]] ; then
2025-12-14 00:03:29 +01:00
echo "
table ip6 openvpn-nat {
chain postrouting {
type nat hook postrouting priority 100; policy accept;
2025-12-15 22:19:02 +01:00
ip6 saddr ${ VPN_SUBNET_IPV6 } /112 oifname \" $NIC \" masquerade
2025-12-14 00:03:29 +01:00
}
}" >>/etc/nftables/openvpn.nft
fi
# Add include to nftables.conf if not already present
if ! grep -q 'include.*/etc/nftables/openvpn.nft' /etc/nftables.conf; then
run_cmd "Adding include to nftables.conf" sh -c 'echo "include \"/etc/nftables/openvpn.nft\"" >> /etc/nftables.conf'
fi
# Reload nftables to apply rules
run_cmd "Reloading nftables" systemctl reload nftables
2025-12-13 20:49:40 +01:00
else
2025-12-14 00:03:29 +01:00
# Use iptables for systems without firewalld or nftables
2025-12-13 20:49:40 +01:00
run_cmd_fatal "Creating iptables directory" mkdir -p /etc/iptables
# Script to add rules
2025-12-15 22:19:02 +01:00
echo "#!/bin/sh" >/etc/iptables/add-openvpn-rules.sh
# IPv4 rules (only if clients get IPv4)
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo "iptables -t nat -I POSTROUTING 1 -s $VPN_SUBNET_IPV4 /24 -o $NIC -j MASQUERADE
2025-12-16 09:58:30 +01:00
iptables -I INPUT 1 -i tun+ -s $VPN_SUBNET_IPV4 /24 -j ACCEPT
iptables -I FORWARD 1 -i tun+ -s $VPN_SUBNET_IPV4 /24 -j ACCEPT
iptables -I FORWARD 1 -o tun+ -d $VPN_SUBNET_IPV4 /24 -j ACCEPT
2025-12-15 22:19:02 +01:00
iptables -I INPUT 1 -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/add-openvpn-rules.sh
fi
2018-09-20 00:05:02 +02:00
2025-12-15 22:19:02 +01:00
# IPv6 rules (only if clients get IPv6)
if [[ $CLIENT_IPV6 == 'y' ]] ; then
echo "ip6tables -t nat -I POSTROUTING 1 -s ${ VPN_SUBNET_IPV6 } /112 -o $NIC -j MASQUERADE
2025-12-16 09:58:30 +01:00
ip6tables -I INPUT 1 -i tun+ -s ${ VPN_SUBNET_IPV6 } /112 -j ACCEPT
ip6tables -I FORWARD 1 -i tun+ -s ${ VPN_SUBNET_IPV6 } /112 -j ACCEPT
ip6tables -I FORWARD 1 -o tun+ -d ${ VPN_SUBNET_IPV6 } /112 -j ACCEPT
2020-04-30 23:42:09 +02:00
ip6tables -I INPUT 1 -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/add-openvpn-rules.sh
2025-12-13 20:49:40 +01:00
fi
2018-09-20 00:05:02 +02:00
2025-12-13 20:49:40 +01:00
# Script to remove rules
2025-12-15 22:19:02 +01:00
echo "#!/bin/sh" >/etc/iptables/rm-openvpn-rules.sh
# IPv4 removal rules
if [[ $CLIENT_IPV4 == 'y' ]] ; then
echo "iptables -t nat -D POSTROUTING -s $VPN_SUBNET_IPV4 /24 -o $NIC -j MASQUERADE
2025-12-16 09:58:30 +01:00
iptables -D INPUT -i tun+ -s $VPN_SUBNET_IPV4 /24 -j ACCEPT
iptables -D FORWARD -i tun+ -s $VPN_SUBNET_IPV4 /24 -j ACCEPT
iptables -D FORWARD -o tun+ -d $VPN_SUBNET_IPV4 /24 -j ACCEPT
2025-12-15 22:19:02 +01:00
iptables -D INPUT -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/rm-openvpn-rules.sh
fi
2018-09-20 00:05:02 +02:00
2025-12-15 22:19:02 +01:00
# IPv6 removal rules
if [[ $CLIENT_IPV6 == 'y' ]] ; then
echo "ip6tables -t nat -D POSTROUTING -s ${ VPN_SUBNET_IPV6 } /112 -o $NIC -j MASQUERADE
2025-12-16 09:58:30 +01:00
ip6tables -D INPUT -i tun+ -s ${ VPN_SUBNET_IPV6 } /112 -j ACCEPT
ip6tables -D FORWARD -i tun+ -s ${ VPN_SUBNET_IPV6 } /112 -j ACCEPT
ip6tables -D FORWARD -o tun+ -d ${ VPN_SUBNET_IPV6 } /112 -j ACCEPT
2020-04-30 23:42:09 +02:00
ip6tables -D INPUT -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/rm-openvpn-rules.sh
2025-12-13 20:49:40 +01:00
fi
2018-09-20 00:05:02 +02:00
2025-12-13 20:49:40 +01:00
run_cmd "Making add-openvpn-rules.sh executable" chmod +x /etc/iptables/add-openvpn-rules.sh
run_cmd "Making rm-openvpn-rules.sh executable" chmod +x /etc/iptables/rm-openvpn-rules.sh
2018-09-20 00:05:02 +02:00
2025-12-13 20:49:40 +01:00
# Handle the rules via a systemd script
echo "[Unit]
2018-09-20 00:05:02 +02:00
Description=iptables rules for OpenVPN
2025-12-13 20:49:40 +01:00
After=firewalld.service
2025-12-13 13:31:54 +01:00
Before=network-online.target
2018-10-08 21:11:30 +02:00
Wants=network-online.target
2018-09-20 00:05:02 +02:00
[Service]
Type=oneshot
ExecStart=/etc/iptables/add-openvpn-rules.sh
ExecStop=/etc/iptables/rm-openvpn-rules.sh
RemainAfterExit=yes
[Install]
2020-04-27 14:59:19 +02:00
WantedBy=multi-user.target" >/etc/systemd/system/iptables-openvpn.service
2018-09-20 00:05:02 +02:00
2025-12-13 20:49:40 +01:00
# Enable service and apply rules
run_cmd "Reloading systemd" systemctl daemon-reload
run_cmd "Enabling iptables service" systemctl enable iptables-openvpn
run_cmd "Starting iptables service" systemctl start iptables-openvpn
fi
2018-09-20 00:05:02 +02:00
# If the server is behind a NAT, use the correct IP address for the clients to connect to
2020-04-27 14:59:19 +02:00
if [[ $ENDPOINT != "" ]] ; then
2019-02-25 21:30:46 +01:00
IP = $ENDPOINT
2017-11-12 22:51:54 +01:00
fi
2018-09-22 14:21:20 +02:00
2017-11-12 22:51:54 +01:00
# client-template.txt is created so we have a template to add further users later
2025-12-09 15:52:37 +01:00
log_info "Creating client template..."
2025-12-12 22:09:18 +01:00
echo "client" >/etc/openvpn/server/client-template.txt
2020-04-27 14:59:19 +02:00
if [[ $PROTOCOL == 'udp' ]] ; then
2025-12-12 22:09:18 +01:00
echo "proto udp" >>/etc/openvpn/server/client-template.txt
echo "explicit-exit-notify" >>/etc/openvpn/server/client-template.txt
2025-12-15 22:19:02 +01:00
elif [[ $PROTOCOL == 'udp6' ]] ; then
echo "proto udp6" >>/etc/openvpn/server/client-template.txt
echo "explicit-exit-notify" >>/etc/openvpn/server/client-template.txt
2020-04-27 14:59:19 +02:00
elif [[ $PROTOCOL == 'tcp' ]] ; then
2025-12-12 22:09:18 +01:00
echo "proto tcp-client" >>/etc/openvpn/server/client-template.txt
2025-12-15 22:19:02 +01:00
elif [[ $PROTOCOL == 'tcp6' ]] ; then
echo "proto tcp6-client" >>/etc/openvpn/server/client-template.txt
2017-11-12 22:51:54 +01:00
fi
echo "remote $IP $PORT
2015-09-12 21:48:08 +02:00
dev tun
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
2018-01-18 17:36:31 +01:00
verify-x509-name $SERVER_NAME name
2018-09-22 17:51:38 +02:00
auth $HMAC_ALG
2017-08-27 13:59:08 -05:00
auth-nocache
2018-09-22 18:18:36 +02:00
cipher $CIPHER
2025-12-12 10:23:36 +01:00
ignore-unknown-option data-ciphers
data-ciphers $CIPHER
ncp-ciphers $CIPHER
2016-11-28 22:13:32 +01:00
tls-client
2025-12-15 12:13:03 +01:00
tls-version-min $TLS_VERSION_MIN
2018-09-22 15:11:15 +02:00
tls-cipher $CC_CIPHER
2025-12-15 12:13:03 +01:00
tls-ciphersuites $TLS13_CIPHERSUITES
2020-04-27 12:19:25 +00:00
ignore-unknown-option block-outside-dns
2018-09-20 17:16:04 +02:00
setenv opt block-outside-dns # Prevent Windows 10 DNS leak
2025-12-12 22:09:18 +01:00
verb 3" >>/etc/openvpn/server/client-template.txt
2016-11-28 22:13:32 +01:00
2025-12-15 10:53:15 +01:00
if [[ -n $MTU ]] ; then
echo "tun-mtu $MTU " >>/etc/openvpn/server/client-template.txt
fi
2017-11-12 22:51:54 +01:00
# Generate the custom client.ovpn
2025-12-13 15:57:02 -03:00
if [[ $NEW_CLIENT == "n" ]] ; then
log_info "No clients added. To add clients, simply run the script again."
else
log_info "Generating first client certificate..."
newClient
log_success "If you want to add more clients, you simply need to run this script another time!"
fi
2018-09-20 00:05:02 +02:00
}
2025-12-09 21:49:19 +01:00
# Helper function to get the home directory for storing client configs
function getHomeDir() {
local client = " $1 "
2025-12-13 23:31:52 +01:00
if [ -d "/home/ ${ client } " ] ; then
2025-12-09 21:49:19 +01:00
echo "/home/ ${ client } "
elif [ " ${ SUDO_USER } " ] ; then
if [ " ${ SUDO_USER } " == "root" ] ; then
echo "/root"
else
echo "/home/ ${ SUDO_USER } "
fi
else
echo "/root"
fi
}
2025-12-13 21:12:23 +01:00
# Helper function to get the owner of a client config file (if client matches a system user)
function getClientOwner() {
local client = " $1 "
2025-12-13 23:31:52 +01:00
# Check if client name corresponds to an existing system user with a home directory
if id " $client " & >/dev/null && [ -d "/home/ ${ client } " ] ; then
2025-12-13 21:12:23 +01:00
echo " ${ client } "
elif [ " ${ SUDO_USER } " ] && [ " ${ SUDO_USER } " != "root" ] ; then
echo " ${ SUDO_USER } "
fi
}
# Helper function to set proper ownership and permissions on client config file
function setClientConfigPermissions() {
local filepath = " $1 "
local owner = " $2 "
if [[ -n " $owner " ]] ; then
local owner_group
owner_group = $( id -gn " $owner " )
chmod go-rw " $filepath "
chown " $owner : $owner_group " " $filepath "
fi
}
2025-12-13 23:31:52 +01:00
# Helper function to write client config file with proper path and permissions
# Usage: writeClientConfig <client_name>
# Uses CLIENT_FILEPATH env var if set, otherwise defaults to home directory
# Side effects: sets GENERATED_CONFIG_PATH global variable with the final path
function writeClientConfig() {
local client = " $1 "
local clientFilePath
# Determine output file path
if [[ -n " $CLIENT_FILEPATH " ]] ; then
clientFilePath = " $CLIENT_FILEPATH "
# Ensure parent directory exists for custom paths
local parentDir
parentDir = $( dirname " $clientFilePath " )
if [[ ! -d " $parentDir " ]] ; then
run_cmd_fatal "Creating directory $parentDir " mkdir -p " $parentDir "
fi
else
local homeDir
homeDir = $( getHomeDir " $client " )
clientFilePath = " $homeDir / $client .ovpn"
fi
# Generate the .ovpn config file
generateClientConfig " $client " " $clientFilePath "
# Set proper ownership and permissions if client matches a system user
local clientOwner
clientOwner = $( getClientOwner " $client " )
setClientConfigPermissions " $clientFilePath " " $clientOwner "
# Export path for caller to use
GENERATED_CONFIG_PATH = " $clientFilePath "
}
2025-12-09 21:49:19 +01:00
# Helper function to regenerate the CRL after certificate changes
function regenerateCRL() {
2025-12-09 23:33:57 +01:00
export EASYRSA_CRL_DAYS = $DEFAULT_CRL_VALIDITY_DURATION_DAYS
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Regenerating CRL" ./easyrsa gen-crl
2025-12-12 22:09:18 +01:00
run_cmd "Removing old CRL" rm -f /etc/openvpn/server/crl.pem
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Copying new CRL" cp /etc/openvpn/server/easy-rsa/pki/crl.pem /etc/openvpn/server/crl.pem
2025-12-12 22:09:18 +01:00
run_cmd "Setting CRL permissions" chmod 644 /etc/openvpn/server/crl.pem
2025-12-09 21:49:19 +01:00
}
# Helper function to generate .ovpn client config file
2025-12-13 21:12:23 +01:00
# Usage: generateClientConfig <client_name> <filepath>
2025-12-09 21:49:19 +01:00
function generateClientConfig() {
local client = " $1 "
2025-12-13 21:12:23 +01:00
local filepath = " $2 "
2025-12-09 21:49:19 +01:00
2025-12-13 14:32:38 +01:00
# Determine if we use tls-crypt-v2, tls-crypt, or tls-auth
2025-12-09 21:49:19 +01:00
local tls_sig = ""
2025-12-13 14:32:38 +01:00
if grep -qs "^tls-crypt-v2" /etc/openvpn/server/server.conf; then
2025-12-09 21:49:19 +01:00
tls_sig = "1"
2025-12-13 14:32:38 +01:00
elif grep -qs "^tls-crypt" /etc/openvpn/server/server.conf; then
2025-12-09 21:49:19 +01:00
tls_sig = "2"
2025-12-13 14:32:38 +01:00
elif grep -qs "^tls-auth" /etc/openvpn/server/server.conf; then
tls_sig = "3"
2025-12-09 21:49:19 +01:00
fi
# Generate the custom client.ovpn
2025-12-13 21:12:23 +01:00
run_cmd "Creating client config" cp /etc/openvpn/server/client-template.txt " $filepath "
2025-12-09 21:49:19 +01:00
{
echo "<ca>"
2025-12-12 22:09:18 +01:00
cat "/etc/openvpn/server/easy-rsa/pki/ca.crt"
2025-12-09 21:49:19 +01:00
echo "</ca>"
echo "<cert>"
2025-12-12 22:09:18 +01:00
awk '/BEGIN/,/END CERTIFICATE/' "/etc/openvpn/server/easy-rsa/pki/issued/ $client .crt"
2025-12-09 21:49:19 +01:00
echo "</cert>"
echo "<key>"
2025-12-12 22:09:18 +01:00
cat "/etc/openvpn/server/easy-rsa/pki/private/ $client .key"
2025-12-09 21:49:19 +01:00
echo "</key>"
case $tls_sig in
1)
2025-12-14 00:23:43 +01:00
# Generate per-client tls-crypt-v2 key in /etc/openvpn/server/
# Using /tmp would fail on Ubuntu 25.04+ due to AppArmor restrictions
tls_crypt_v2_tmpfile = $( mktemp /etc/openvpn/server/tls-crypt-v2-client.XXXXXX)
if [[ -z " $tls_crypt_v2_tmpfile " ]] || [[ ! -f " $tls_crypt_v2_tmpfile " ]] ; then
log_error "Failed to create temporary file for tls-crypt-v2 client key"
exit 1
fi
2025-12-13 14:32:38 +01:00
if ! openvpn --tls-crypt-v2 /etc/openvpn/server/tls-crypt-v2.key \
--genkey tls-crypt-v2-client " $tls_crypt_v2_tmpfile " ; then
rm -f " $tls_crypt_v2_tmpfile "
log_error "Failed to generate tls-crypt-v2 client key"
exit 1
fi
echo "<tls-crypt-v2>"
cat " $tls_crypt_v2_tmpfile "
echo "</tls-crypt-v2>"
rm -f " $tls_crypt_v2_tmpfile "
;;
2)
2025-12-09 21:49:19 +01:00
echo "<tls-crypt>"
2025-12-12 22:09:18 +01:00
cat /etc/openvpn/server/tls-crypt.key
2025-12-09 21:49:19 +01:00
echo "</tls-crypt>"
;;
2025-12-13 14:32:38 +01:00
3)
2025-12-09 21:49:19 +01:00
echo "key-direction 1"
echo "<tls-auth>"
2025-12-12 22:09:18 +01:00
cat /etc/openvpn/server/tls-auth.key
2025-12-09 21:49:19 +01:00
echo "</tls-auth>"
;;
esac
2025-12-13 21:12:23 +01:00
} >>" $filepath "
2025-12-09 21:49:19 +01:00
}
# Helper function to list valid clients and select one
# Arguments: show_expiry (optional, "true" to show expiry info)
# Sets global variables:
# CLIENT - the selected client name
# CLIENTNUMBER - the selected client number (1-based index)
# NUMBEROFCLIENTS - total count of valid clients
function selectClient() {
local show_expiry = " ${ 1 :- false } "
local client_number
2025-12-12 22:09:18 +01:00
NUMBEROFCLIENTS = $( tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -c "^V" )
2025-12-09 21:49:19 +01:00
if [[ $NUMBEROFCLIENTS == '0' ]] ; then
log_fatal "You have no existing clients!"
fi
2025-12-13 20:18:07 +01:00
# If CLIENT is set, validate it exists as a valid client
if [[ -n $CLIENT ]] ; then
if tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep "^V" | cut -d '=' -f 2 | grep -qx " $CLIENT " ; then
return
else
log_fatal "Client ' $CLIENT ' not found or not valid"
fi
fi
2025-12-09 21:49:19 +01:00
if [[ $show_expiry == "true" ]] ; then
local i = 1
while read -r client; do
2025-12-12 22:09:18 +01:00
local client_cert = "/etc/openvpn/server/easy-rsa/pki/issued/ $client .crt"
2025-12-09 21:49:19 +01:00
local days
days = $( getDaysUntilExpiry " $client_cert " )
local expiry
expiry = $( formatExpiry " $days " )
echo " $i ) $client $expiry "
(( i++))
2025-12-12 22:09:18 +01:00
done < <( tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep "^V" | cut -d '=' -f 2)
2025-12-09 21:49:19 +01:00
else
2025-12-12 22:09:18 +01:00
tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep "^V" | cut -d '=' -f 2 | nl -s ') '
2025-12-09 21:49:19 +01:00
fi
until [[ ${ CLIENTNUMBER :- $client_number } -ge 1 && ${ CLIENTNUMBER :- $client_number } -le $NUMBEROFCLIENTS ]] ; do
if [[ $NUMBEROFCLIENTS == '1' ]] ; then
read -rp "Select one client [1]: " client_number
else
read -rp "Select one client [1- $NUMBEROFCLIENTS ]: " client_number
fi
done
CLIENTNUMBER = " ${ CLIENTNUMBER :- $client_number } "
2025-12-12 22:09:18 +01:00
CLIENT = $( tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep "^V" | cut -d '=' -f 2 | sed -n " $CLIENTNUMBER " p)
2025-12-09 21:49:19 +01:00
}
2025-12-14 22:08:44 +01:00
# Escape a string for JSON output
function json_escape() {
local str = " $1 "
# Escape backslashes first, then quotes, then control characters
str = " ${ str // \\ / \\\\ } "
str = " ${ str // \" / \\\" } "
str = " ${ str // $'\n' / \\ n } "
str = " ${ str // $'\r' / \\ r } "
str = " ${ str // $'\t' / \\ t } "
printf '%s' " $str "
}
2025-12-13 19:17:30 +01:00
2025-12-14 22:08:44 +01:00
function listClients() {
2025-12-13 19:17:30 +01:00
local index_file = "/etc/openvpn/server/easy-rsa/pki/index.txt"
2025-12-14 22:08:44 +01:00
local cert_dir = "/etc/openvpn/server/easy-rsa/pki/issued"
2025-12-13 19:17:30 +01:00
local number_of_clients
2025-12-14 22:08:44 +01:00
local format = " ${ OUTPUT_FORMAT :- table } "
2025-12-13 19:17:30 +01:00
# Exclude server certificates (CN starting with server_)
number_of_clients = $( tail -n +2 " $index_file " | grep "^[VR]" | grep -cv "/CN=server_" )
if [[ $number_of_clients == '0' ]] ; then
2025-12-14 22:08:44 +01:00
if [[ $format == "json" ]] ; then
echo '{"clients":[]}'
else
log_warn "You have no existing client certificates!"
fi
2025-12-13 19:17:30 +01:00
return
fi
2025-12-14 22:08:44 +01:00
# Collect client data
local clients_data =()
while read -r line; do
local status = " ${ line : 0 : 1 } "
local client_name
client_name = $( echo " $line " | sed 's/.*\/CN=//' )
2025-12-13 19:17:30 +01:00
2025-12-14 22:08:44 +01:00
local status_text
if [[ " $status " == "V" ]] ; then
status_text = "valid"
elif [[ " $status " == "R" ]] ; then
status_text = "revoked"
else
status_text = "unknown"
fi
2025-12-13 19:17:30 +01:00
2025-12-14 22:08:44 +01:00
local cert_file = " $cert_dir / $client_name .crt"
local expiry_date = "unknown"
local days_remaining = "null"
if [[ -f " $cert_file " ]] ; then
local enddate
enddate = $( openssl x509 -enddate -noout -in " $cert_file " 2>/dev/null | cut -d= -f2)
2025-12-13 19:17:30 +01:00
2025-12-14 22:08:44 +01:00
if [[ -n " $enddate " ]] ; then
local expiry_epoch
expiry_epoch = $( date -d " $enddate " +%s 2>/dev/null || date -j -f "%b %d %H:%M:%S %Y %Z" " $enddate " +%s 2>/dev/null)
if [[ -n " $expiry_epoch " ]] ; then
expiry_date = $( date -d "@ $expiry_epoch " +%Y-%m-%d 2>/dev/null || date -r " $expiry_epoch " +%Y-%m-%d 2>/dev/null)
local now_epoch
now_epoch = $( date +%s)
days_remaining = $(( ( expiry_epoch - now_epoch) / 86400 ))
2025-12-13 19:17:30 +01:00
fi
fi
2025-12-14 22:08:44 +01:00
fi
2025-12-13 19:17:30 +01:00
2025-12-14 22:08:44 +01:00
clients_data +=( " $client_name | $status_text | $expiry_date | $days_remaining " )
done < <( tail -n +2 " $index_file " | grep "^[VR]" | grep -v "/CN=server_" | sort -t$'\t' -k2)
if [[ $format == "json" ]] ; then
# Output JSON
echo '{"clients":['
local first = true
for client_entry in " ${ clients_data [@] } " ; do
IFS = '|' read -r name status expiry days <<< " $client_entry "
[[ $first == true ]] && first = false || printf ','
# Handle null for days_remaining (no quotes for JSON null)
local days_json
if [[ " $days " == "null" || -z " $days " ]] ; then
days_json = "null"
else
days_json = " $days "
fi
printf '{"name":"%s","status":"%s","expiry":"%s","days_remaining":%s}\n' \
" $( json_escape " $name " ) " " $( json_escape " $status " ) " " $( json_escape " $expiry " ) " " $days_json "
done
echo ']}'
else
# Output table
log_header "Client Certificates"
log_info "Found $number_of_clients client certificate(s)"
log_menu ""
printf " %-25s %-10s %-12s %s\n" "Name" "Status" "Expiry" "Remaining"
printf " %-25s %-10s %-12s %s\n" "----" "------" "------" "---------"
for client_entry in " ${ clients_data [@] } " ; do
IFS = '|' read -r name status expiry days <<< " $client_entry "
local relative
if [[ $days == "null" ]] ; then
relative = "unknown"
elif [[ $days -lt 0 ]] ; then
relative = " $(( - days)) days ago"
elif [[ $days -eq 0 ]] ; then
relative = "today"
elif [[ $days -eq 1 ]] ; then
relative = "1 day"
else
relative = " $days days"
fi
# Capitalize status for table display
local status_display = " ${ status ^ } "
printf " %-25s %-10s %-12s %s\n" " $name " " $status_display " " $expiry " " $relative "
done
log_menu ""
fi
2025-12-13 19:17:30 +01:00
}
2025-12-14 13:04:39 +01:00
function formatBytes() {
local bytes = $1
# Validate input is numeric
if ! [[ " $bytes " = ~ ^[ 0-9] +$ ]] ; then
echo "N/A"
return
fi
if [[ $bytes -ge 1073741824 ]] ; then
awk "BEGIN {printf \"%.1fG\", $bytes /1073741824}"
elif [[ $bytes -ge 1048576 ]] ; then
awk "BEGIN {printf \"%.1fM\", $bytes /1048576}"
elif [[ $bytes -ge 1024 ]] ; then
awk "BEGIN {printf \"%.1fK\", $bytes /1024}"
else
echo " ${ bytes } B"
fi
}
function listConnectedClients() {
local status_file = "/var/log/openvpn/status.log"
2025-12-14 22:08:44 +01:00
local format = " ${ OUTPUT_FORMAT :- table } "
2025-12-14 13:04:39 +01:00
if [[ ! -f " $status_file " ]] ; then
2025-12-14 22:08:44 +01:00
if [[ $format == "json" ]] ; then
echo '{"error":"Status file not found","clients":[]}'
else
log_warn "Status file not found: $status_file "
log_info "Make sure OpenVPN is running."
fi
2025-12-14 13:04:39 +01:00
return
fi
local client_count
client_count = $( grep -c "^CLIENT_LIST" " $status_file " 2>/dev/null) || client_count = 0
if [[ " $client_count " -eq 0 ]] ; then
2025-12-14 22:08:44 +01:00
if [[ $format == "json" ]] ; then
echo '{"clients":[]}'
else
log_header "Connected Clients"
log_info "No clients currently connected."
log_info "Note: Data refreshes every 60 seconds."
fi
2025-12-14 13:04:39 +01:00
return
fi
2025-12-14 22:08:44 +01:00
# Collect client data
local clients_data =()
2025-12-14 13:04:39 +01:00
while IFS = ',' read -r _ name real_addr vpn_ip _ bytes_recv bytes_sent connected_since _; do
2025-12-14 22:08:44 +01:00
clients_data +=( " $name | $real_addr | $vpn_ip | $bytes_recv | $bytes_sent | $connected_since " )
2025-12-14 13:04:39 +01:00
done < <( grep "^CLIENT_LIST" " $status_file " )
2025-12-14 22:08:44 +01:00
if [[ $format == "json" ]] ; then
echo '{"clients":['
local first = true
for client_entry in " ${ clients_data [@] } " ; do
IFS = '|' read -r name real_addr vpn_ip bytes_recv bytes_sent connected_since <<< " $client_entry "
[[ $first == true ]] && first = false || printf ','
printf '{"name":"%s","real_address":"%s","vpn_ip":"%s","bytes_received":%s,"bytes_sent":%s,"connected_since":"%s"}\n' \
" $( json_escape " $name " ) " " $( json_escape " $real_addr " ) " " $( json_escape " $vpn_ip " ) " \
" ${ bytes_recv :- 0 } " " ${ bytes_sent :- 0 } " " $( json_escape " $connected_since " ) "
done
echo ']}'
else
log_header "Connected Clients"
log_info "Found $client_count connected client(s)"
log_menu ""
printf " %-20s %-22s %-16s %-20s %s\n" "Name" "Real Address" "VPN IP" "Connected Since" "Transfer"
printf " %-20s %-22s %-16s %-20s %s\n" "----" "------------" "------" "---------------" "--------"
for client_entry in " ${ clients_data [@] } " ; do
IFS = '|' read -r name real_addr vpn_ip bytes_recv bytes_sent connected_since <<< " $client_entry "
local recv_human sent_human
recv_human = $( formatBytes " $bytes_recv " )
sent_human = $( formatBytes " $bytes_sent " )
local transfer = "↓ ${ recv_human } ↑ ${ sent_human } "
printf " %-20s %-22s %-16s %-20s %s\n" " $name " " $real_addr " " $vpn_ip " " $connected_since " " $transfer "
done
log_menu ""
log_info "Note: Data refreshes every 60 seconds."
fi
2025-12-14 13:04:39 +01:00
}
2020-04-27 14:59:19 +02:00
function newClient() {
2025-12-09 15:52:37 +01:00
log_header "New Client Setup"
2018-09-20 00:05:02 +02:00
2025-12-15 11:24:01 +01:00
# Only prompt for client name if not already set or invalid
if ! is_valid_client_name " $CLIENT " ; then
2025-12-14 22:08:44 +01:00
log_prompt "Tell me a name for the client."
2025-12-15 11:24:01 +01:00
log_prompt "The name must consist of alphanumeric characters, underscores, or dashes (max $MAX_CLIENT_NAME_LENGTH characters)."
until is_valid_client_name " $CLIENT " ; do
2025-12-14 22:08:44 +01:00
read -rp "Client name: " -e CLIENT
done
fi
2018-09-20 00:05:02 +02:00
2025-12-14 22:08:44 +01:00
# Only prompt for cert duration if not already set
2025-12-09 23:33:57 +01:00
if [[ -z $CLIENT_CERT_DURATION_DAYS ]] || ! [[ $CLIENT_CERT_DURATION_DAYS = ~ ^[ 0-9] +$ ]] || [[ $CLIENT_CERT_DURATION_DAYS -lt 1 ]] ; then
2025-12-09 22:34:29 +03:30
log_menu ""
log_prompt "How many days should the client certificate be valid for?"
2025-12-09 23:33:57 +01:00
until [[ $CLIENT_CERT_DURATION_DAYS = ~ ^[ 0-9] +$ ]] && [[ $CLIENT_CERT_DURATION_DAYS -ge 1 ]] ; do
read -rp "Certificate validity (days): " -e -i $DEFAULT_CERT_VALIDITY_DURATION_DAYS CLIENT_CERT_DURATION_DAYS
2025-12-09 21:49:19 +01:00
done
2025-12-09 22:34:29 +03:30
fi
2025-12-14 22:08:44 +01:00
# Only prompt for password if not already set
if ! [[ $PASS = ~ ^[ 1-2] $ ]] ; then
log_menu ""
log_prompt "Do you want to protect the configuration file with a password?"
log_prompt "(e.g. encrypt the private key with a password)"
log_menu " 1) Add a passwordless client"
log_menu " 2) Use a password for the client"
until [[ $PASS = ~ ^[ 1-2] $ ]] ; do
read -rp "Select an option [1-2]: " -e -i 1 PASS
done
fi
2018-09-20 00:05:02 +02:00
2025-12-12 22:09:18 +01:00
CLIENTEXISTS = $( tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -E "^V" | grep -c -E "/CN= $CLIENT \$" )
2025-12-11 11:15:34 -05:00
if [[ $CLIENTEXISTS != '0' ]] ; then
2025-12-09 15:52:37 +01:00
log_error "The specified client CN was already found in easy-rsa, please choose another name."
2025-12-15 09:57:52 +01:00
exit 1
2020-04-27 04:56:34 -07:00
else
2025-12-12 22:09:18 +01:00
cd /etc/openvpn/server/easy-rsa/ || return
2025-12-09 15:52:37 +01:00
log_info "Generating client certificate..."
2025-12-09 23:33:57 +01:00
export EASYRSA_CERT_EXPIRE = $CLIENT_CERT_DURATION_DAYS
2020-04-27 04:56:34 -07:00
case $PASS in
2020-04-27 14:59:19 +02:00
1)
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Building client certificate" ./easyrsa --batch build-client-full " $CLIENT " nopass
2020-04-27 04:56:34 -07:00
;;
2020-04-27 14:59:19 +02:00
2)
2025-12-13 15:42:43 +01:00
if [[ -z " $PASSPHRASE " ]] ; then
log_warn "You will be asked for the client password below"
# Run directly (not via run_cmd) so password prompt is visible to user
if ! ./easyrsa --batch build-client-full " $CLIENT " ; then
log_fatal "Building client certificate failed"
fi
else
log_info "Using provided passphrase for client certificate"
# Use env var to avoid exposing passphrase in install log
export EASYRSA_PASSPHRASE = " $PASSPHRASE "
run_cmd_fatal "Building client certificate" ./easyrsa --batch --passin= env:EASYRSA_PASSPHRASE --passout= env:EASYRSA_PASSPHRASE build-client-full " $CLIENT "
unset EASYRSA_PASSPHRASE
2025-12-13 13:31:54 +01:00
fi
2020-04-27 04:56:34 -07:00
;;
esac
2025-12-09 23:33:57 +01:00
log_success "Client $CLIENT added and is valid for $CLIENT_CERT_DURATION_DAYS days."
2020-04-27 04:56:34 -07:00
fi
2018-09-20 00:05:02 +02:00
2025-12-13 23:31:52 +01:00
# Write the .ovpn config file with proper path and permissions
writeClientConfig " $CLIENT "
2018-09-20 00:05:02 +02:00
2025-12-09 15:52:37 +01:00
log_menu ""
2025-12-13 23:31:52 +01:00
log_success "The configuration file has been written to $GENERATED_CONFIG_PATH ."
2025-12-09 15:52:37 +01:00
log_info "Download the .ovpn file and import it in your OpenVPN client."
2019-02-25 21:54:36 +01:00
exit 0
2018-09-20 00:05:02 +02:00
}
2020-04-27 14:59:19 +02:00
function revokeClient() {
2025-12-09 15:52:37 +01:00
log_header "Revoke Client"
log_prompt "Select the existing client certificate you want to revoke"
2025-12-09 21:49:19 +01:00
selectClient
2025-12-12 22:09:18 +01:00
cd /etc/openvpn/server/easy-rsa/ || return
2025-12-09 15:52:37 +01:00
log_info "Revoking certificate for $CLIENT ..."
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Revoking certificate" ./easyrsa --batch revoke-issued " $CLIENT "
2025-12-09 21:49:19 +01:00
regenerateCRL
2025-12-09 15:52:37 +01:00
run_cmd "Removing client config from /home" find /home/ -maxdepth 2 -name " $CLIENT .ovpn" -delete
run_cmd "Removing client config from /root" rm -f "/root/ $CLIENT .ovpn"
2025-12-12 22:09:18 +01:00
run_cmd "Removing IP assignment" sed -i "/^ $CLIENT ,.*/d" /etc/openvpn/server/ipp.txt
run_cmd "Backing up index" cp /etc/openvpn/server/easy-rsa/pki/index.txt{ ,.bk}
2025-12-09 15:52:37 +01:00
log_success "Certificate for client $CLIENT revoked."
2018-09-20 00:05:02 +02:00
}
2025-12-09 21:49:19 +01:00
function renewClient() {
2025-12-13 21:12:23 +01:00
local client_cert_duration_days
2025-12-09 21:49:19 +01:00
log_header "Renew Client Certificate"
log_prompt "Select the existing client certificate you want to renew"
selectClient "true"
2025-12-09 23:33:57 +01:00
# Allow user to specify renewal duration (use CLIENT_CERT_DURATION_DAYS env var for headless mode)
if [[ -z $CLIENT_CERT_DURATION_DAYS ]] || ! [[ $CLIENT_CERT_DURATION_DAYS = ~ ^[ 0-9] +$ ]] || [[ $CLIENT_CERT_DURATION_DAYS -lt 1 ]] ; then
2025-12-09 21:49:19 +01:00
log_menu ""
log_prompt "How many days should the renewed certificate be valid for?"
2025-12-09 23:33:57 +01:00
until [[ $client_cert_duration_days = ~ ^[ 0-9] +$ ]] && [[ $client_cert_duration_days -ge 1 ]] ; do
read -rp "Certificate validity (days): " -e -i $DEFAULT_CERT_VALIDITY_DURATION_DAYS client_cert_duration_days
2025-12-09 21:49:19 +01:00
done
else
2025-12-09 23:33:57 +01:00
client_cert_duration_days = $CLIENT_CERT_DURATION_DAYS
2025-12-09 21:49:19 +01:00
fi
2025-12-12 22:09:18 +01:00
cd /etc/openvpn/server/easy-rsa/ || return
2025-12-09 21:49:19 +01:00
log_info "Renewing certificate for $CLIENT ..."
# Backup the old certificate before renewal
2025-12-12 22:09:18 +01:00
run_cmd "Backing up old certificate" cp "/etc/openvpn/server/easy-rsa/pki/issued/ $CLIENT .crt" "/etc/openvpn/server/easy-rsa/pki/issued/ $CLIENT .crt.bak"
2025-12-09 21:49:19 +01:00
# Renew the certificate (keeps the same private key)
2025-12-09 23:33:57 +01:00
export EASYRSA_CERT_EXPIRE = $client_cert_duration_days
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Renewing certificate" ./easyrsa --batch renew " $CLIENT "
2025-12-09 21:49:19 +01:00
# Revoke the old certificate
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Revoking old certificate" ./easyrsa --batch revoke-renewed " $CLIENT "
2025-12-09 21:49:19 +01:00
# Regenerate the CRL
regenerateCRL
2025-12-13 23:31:52 +01:00
# Write the .ovpn config file with proper path and permissions
writeClientConfig " $CLIENT "
2025-12-09 21:49:19 +01:00
log_menu ""
2025-12-09 23:33:57 +01:00
log_success "Certificate for client $CLIENT renewed and is valid for $client_cert_duration_days days."
2025-12-13 23:31:52 +01:00
log_info "The new configuration file has been written to $GENERATED_CONFIG_PATH ."
2025-12-09 21:49:19 +01:00
log_info "Download the new .ovpn file and import it in your OpenVPN client."
}
function renewServer() {
2025-12-09 23:33:57 +01:00
local server_name server_cert_duration_days
2025-12-09 21:49:19 +01:00
log_header "Renew Server Certificate"
2025-12-12 22:09:18 +01:00
# Get the server name from the config (extract basename since path may be relative)
server_name = $( basename " $( grep '^cert ' /etc/openvpn/server/server.conf | cut -d ' ' -f 2) " .crt)
2025-12-09 21:49:19 +01:00
if [[ -z " $server_name " ]] ; then
2025-12-12 22:09:18 +01:00
log_fatal "Could not determine server certificate name from /etc/openvpn/server/server.conf"
2025-12-09 21:49:19 +01:00
fi
log_prompt "This will renew the server certificate: $server_name "
log_warn "The OpenVPN service will be restarted after renewal."
if [[ -z $CONTINUE ]] ; then
read -rp "Do you want to continue? [y/n]: " -e -i n CONTINUE
fi
if [[ $CONTINUE != "y" ]] ; then
log_info "Renewal aborted."
return
fi
2025-12-09 23:33:57 +01:00
# Allow user to specify renewal duration (use SERVER_CERT_DURATION_DAYS env var for headless mode)
if [[ -z $SERVER_CERT_DURATION_DAYS ]] || ! [[ $SERVER_CERT_DURATION_DAYS = ~ ^[ 0-9] +$ ]] || [[ $SERVER_CERT_DURATION_DAYS -lt 1 ]] ; then
2025-12-09 21:49:19 +01:00
log_menu ""
log_prompt "How many days should the renewed certificate be valid for?"
2025-12-09 23:33:57 +01:00
until [[ $server_cert_duration_days = ~ ^[ 0-9] +$ ]] && [[ $server_cert_duration_days -ge 1 ]] ; do
read -rp "Certificate validity (days): " -e -i $DEFAULT_CERT_VALIDITY_DURATION_DAYS server_cert_duration_days
2025-12-09 21:49:19 +01:00
done
else
2025-12-09 23:33:57 +01:00
server_cert_duration_days = $SERVER_CERT_DURATION_DAYS
2025-12-09 21:49:19 +01:00
fi
2025-12-12 22:09:18 +01:00
cd /etc/openvpn/server/easy-rsa/ || return
2025-12-09 21:49:19 +01:00
log_info "Renewing server certificate..."
# Backup the old certificate before renewal
2025-12-12 22:09:18 +01:00
run_cmd "Backing up old certificate" cp "/etc/openvpn/server/easy-rsa/pki/issued/ $server_name .crt" "/etc/openvpn/server/easy-rsa/pki/issued/ $server_name .crt.bak"
2025-12-09 21:49:19 +01:00
# Renew the certificate (keeps the same private key)
2025-12-09 23:33:57 +01:00
export EASYRSA_CERT_EXPIRE = $server_cert_duration_days
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Renewing certificate" ./easyrsa --batch renew " $server_name "
2025-12-09 21:49:19 +01:00
# Revoke the old certificate
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Revoking old certificate" ./easyrsa --batch revoke-renewed " $server_name "
2025-12-09 21:49:19 +01:00
# Regenerate the CRL
regenerateCRL
2025-12-12 22:09:18 +01:00
# Copy the new certificate to /etc/openvpn/server/
2025-12-13 13:31:54 +01:00
run_cmd_fatal "Copying new certificate" cp "/etc/openvpn/server/easy-rsa/pki/issued/ $server_name .crt" /etc/openvpn/server/
2025-12-09 21:49:19 +01:00
# Restart OpenVPN
log_info "Restarting OpenVPN service..."
2025-12-12 22:09:18 +01:00
run_cmd "Restarting OpenVPN" systemctl restart openvpn-server@server
2025-12-09 21:49:19 +01:00
2025-12-09 23:33:57 +01:00
log_success "Server certificate renewed successfully and is valid for $server_cert_duration_days days."
2025-12-09 21:49:19 +01:00
}
function getDaysUntilExpiry() {
local cert_file = " $1 "
if [[ -f " $cert_file " ]] ; then
local expiry_date
expiry_date = $( openssl x509 -in " $cert_file " -noout -enddate | cut -d= -f2)
local expiry_epoch
expiry_epoch = $( date -d " $expiry_date " +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" " $expiry_date " +%s 2>/dev/null)
if [[ -z " $expiry_epoch " ]] ; then
echo "?"
return
fi
local now_epoch
now_epoch = $( date +%s)
echo $(( ( expiry_epoch - now_epoch) / 86400 ))
else
echo "?"
fi
}
function formatExpiry() {
local days = " $1 "
if [[ " $days " == "?" ]] ; then
echo "(unknown expiry)"
elif [[ $days -lt 0 ]] ; then
echo "(EXPIRED $(( - days)) days ago)"
elif [[ $days -eq 0 ]] ; then
echo "(expires today)"
elif [[ $days -eq 1 ]] ; then
echo "(expires in 1 day)"
else
echo "(expires in $days days)"
fi
}
function renewMenu() {
local server_name server_cert server_days server_expiry renew_option
log_header "Certificate Renewal"
2025-12-12 22:09:18 +01:00
# Get server certificate expiry for menu display (extract basename since path may be relative)
server_name = $( basename " $( grep '^cert ' /etc/openvpn/server/server.conf | cut -d ' ' -f 2) " .crt)
2025-12-09 21:49:19 +01:00
if [[ -z " $server_name " ]] ; then
server_expiry = "(unknown expiry)"
else
2025-12-12 22:09:18 +01:00
server_cert = "/etc/openvpn/server/easy-rsa/pki/issued/ $server_name .crt"
2025-12-09 21:49:19 +01:00
server_days = $( getDaysUntilExpiry " $server_cert " )
server_expiry = $( formatExpiry " $server_days " )
fi
log_menu ""
log_prompt "What do you want to renew?"
log_menu " 1) Renew a client certificate"
log_menu " 2) Renew the server certificate $server_expiry "
log_menu " 3) Back to main menu"
until [[ ${ RENEW_OPTION :- $renew_option } = ~ ^[ 1-3] $ ]] ; do
read -rp "Select an option [1-3]: " renew_option
done
renew_option = " ${ RENEW_OPTION :- $renew_option } "
case $renew_option in
1)
renewClient
;;
2)
renewServer
;;
3)
manageMenu
;;
esac
}
2020-04-27 14:59:19 +02:00
function removeUnbound() {
2025-12-11 13:14:56 +01:00
run_cmd "Removing OpenVPN Unbound config" rm -f /etc/unbound/unbound.conf.d/openvpn.conf
# Clean up include directive if conf.d directory is now empty
if [[ -d /etc/unbound/unbound.conf.d ]] && [[ -z " $( ls -A /etc/unbound/unbound.conf.d) " ]] ; then
run_cmd "Cleaning up Unbound include directive" \
sed -i '/^include: "\/etc\/unbound\/unbound\.conf\.d\/\*\.conf"$/d' /etc/unbound/unbound.conf
fi
2018-09-20 00:05:02 +02:00
2018-09-22 15:23:01 +02:00
until [[ $REMOVE_UNBOUND = ~ ( y| n) ]] ; do
2025-12-09 15:52:37 +01:00
log_info "If you were already using Unbound before installing OpenVPN, I removed the configuration related to OpenVPN."
2018-09-20 00:05:02 +02:00
read -rp "Do you want to completely remove Unbound? [y/n]: " -e REMOVE_UNBOUND
done
2020-04-27 14:59:19 +02:00
if [[ $REMOVE_UNBOUND == 'y' ]] ; then
2025-12-09 15:52:37 +01:00
log_info "Removing Unbound..."
run_cmd "Stopping Unbound" systemctl stop unbound
2018-09-20 00:05:02 +02:00
2020-04-27 14:59:19 +02:00
if [[ $OS = ~ ( debian| ubuntu) ]] ; then
2025-12-09 15:52:37 +01:00
run_cmd "Removing Unbound" apt-get remove --purge -y unbound
2020-04-27 14:59:19 +02:00
elif [[ $OS == 'arch' ]] ; then
2025-12-09 15:52:37 +01:00
run_cmd "Removing Unbound" pacman --noconfirm -R unbound
2025-12-10 17:54:00 +01:00
elif [[ $OS = ~ ( centos| oracle) ]] ; then
2025-12-09 15:52:37 +01:00
run_cmd "Removing Unbound" yum remove -y unbound
2025-12-10 17:54:00 +01:00
elif [[ $OS = ~ ( fedora| amzn2023) ]] ; then
2025-12-09 15:52:37 +01:00
run_cmd "Removing Unbound" dnf remove -y unbound
2025-12-12 04:22:12 +08:00
elif [[ $OS == 'opensuse' ]] ; then
run_cmd "Removing Unbound" zypper remove -y unbound
2018-09-20 00:05:02 +02:00
fi
2025-12-09 15:52:37 +01:00
run_cmd "Removing Unbound config" rm -rf /etc/unbound/
log_success "Unbound removed!"
2018-09-20 00:05:02 +02:00
else
2025-12-09 15:52:37 +01:00
run_cmd "Restarting Unbound" systemctl restart unbound
log_info "Unbound wasn't removed."
2018-09-20 00:05:02 +02:00
fi
}
2020-04-27 14:59:19 +02:00
function removeOpenVPN() {
2025-12-09 15:52:37 +01:00
log_header "Remove OpenVPN"
2025-12-14 22:08:44 +01:00
if [[ -z $REMOVE ]] ; then
read -rp "Do you really want to remove OpenVPN? [y/n]: " -e -i n REMOVE
fi
2020-04-27 14:59:19 +02:00
if [[ $REMOVE == 'y' ]] ; then
2025-12-14 10:54:52 +01:00
# Get OpenVPN configuration
2025-12-12 22:09:18 +01:00
PORT = $( grep '^port ' /etc/openvpn/server/server.conf | cut -d " " -f 2)
PROTOCOL = $( grep '^proto ' /etc/openvpn/server/server.conf | cut -d " " -f 2)
2025-12-15 22:19:02 +01:00
# Strip "6" suffix for firewall/SELinux commands (they expect "udp"/"tcp", not "udp6"/"tcp6")
PROTOCOL_BASE = " ${ PROTOCOL %6 } "
# Extract IPv4 subnet (may be empty if IPv4 not enabled)
VPN_SUBNET_IPV4 = $( grep '^server ' /etc/openvpn/server/server.conf | cut -d " " -f 2)
# Extract IPv6 subnet (may be empty if IPv6 not enabled)
VPN_SUBNET_IPV6 = $( grep '^server-ipv6 ' /etc/openvpn/server/server.conf | cut -d " " -f 2 | sed 's|/.*||' )
2018-09-20 00:05:02 +02:00
# Stop OpenVPN
2025-12-09 15:52:37 +01:00
log_info "Stopping OpenVPN service..."
2025-12-12 22:09:18 +01:00
run_cmd "Disabling OpenVPN service" systemctl disable openvpn-server@server
run_cmd "Stopping OpenVPN service" systemctl stop openvpn-server@server
# Remove customised service
run_cmd "Removing service file" rm -f /etc/systemd/system/openvpn-server@.service
2018-09-20 00:05:02 +02:00
2025-12-13 20:49:40 +01:00
# Remove firewall rules
log_info "Removing firewall rules..."
2025-12-15 22:19:02 +01:00
if systemctl is-active --quiet firewalld && firewall-cmd --list-ports | grep -q " $PORT / $PROTOCOL_BASE " ; then
2025-12-13 20:49:40 +01:00
# firewalld was used
2025-12-15 22:19:02 +01:00
run_cmd "Removing OpenVPN port from firewalld" firewall-cmd --permanent --remove-port= " $PORT / $PROTOCOL_BASE "
2025-12-13 20:49:40 +01:00
run_cmd "Removing masquerade from firewalld" firewall-cmd --permanent --remove-masquerade
2025-12-16 09:58:30 +01:00
# Remove IPv4 rich rule if configured
2025-12-15 22:19:02 +01:00
if [[ -n $VPN_SUBNET_IPV4 ]] ; then
2025-12-16 09:58:30 +01:00
firewall-cmd --permanent --remove-rich-rule= "rule family=\"ipv4\" source address=\" $VPN_SUBNET_IPV4 /24\" accept" 2>/dev/null || true
2025-12-15 22:19:02 +01:00
fi
2025-12-16 09:58:30 +01:00
# Remove IPv6 rich rule if configured
2025-12-15 22:19:02 +01:00
if [[ -n $VPN_SUBNET_IPV6 ]] ; then
2025-12-16 09:58:30 +01:00
firewall-cmd --permanent --remove-rich-rule= "rule family=\"ipv6\" source address=\" ${ VPN_SUBNET_IPV6 } /112\" accept" 2>/dev/null || true
2025-12-15 22:19:02 +01:00
fi
2025-12-13 20:49:40 +01:00
run_cmd "Reloading firewalld" firewall-cmd --reload
2025-12-14 00:03:29 +01:00
elif [[ -f /etc/nftables/openvpn.nft ]] ; then
# nftables was used
# Delete tables (suppress errors in case tables don't exist)
nft delete table inet openvpn 2>/dev/null || true
nft delete table ip openvpn-nat 2>/dev/null || true
nft delete table ip6 openvpn-nat 2>/dev/null || true
run_cmd "Removing include from nftables.conf" sed -i '/include.*openvpn\.nft/d' /etc/nftables.conf
run_cmd "Removing nftables rules file" rm -f /etc/nftables/openvpn.nft
2025-12-13 20:49:40 +01:00
elif [[ -f /etc/systemd/system/iptables-openvpn.service ]] ; then
# iptables was used
run_cmd "Stopping iptables service" systemctl stop iptables-openvpn
run_cmd "Disabling iptables service" systemctl disable iptables-openvpn
run_cmd "Removing iptables service file" rm /etc/systemd/system/iptables-openvpn.service
run_cmd "Reloading systemd" systemctl daemon-reload
run_cmd "Removing iptables add script" rm -f /etc/iptables/add-openvpn-rules.sh
run_cmd "Removing iptables rm script" rm -f /etc/iptables/rm-openvpn-rules.sh
fi
2018-09-20 00:05:02 +02:00
# SELinux
if hash sestatus 2>/dev/null; then
if sestatus | grep "Current mode" | grep -qs "enforcing" ; then
2020-04-27 16:05:51 +02:00
if [[ $PORT != '1194' ]] ; then
2025-12-15 22:19:02 +01:00
run_cmd "Removing SELinux port" semanage port -d -t openvpn_port_t -p " $PROTOCOL_BASE " " $PORT "
2018-09-20 00:05:02 +02:00
fi
fi
fi
2025-12-09 15:52:37 +01:00
log_info "Removing OpenVPN package..."
2020-04-27 14:59:19 +02:00
if [[ $OS = ~ ( debian| ubuntu) ]] ; then
2025-12-09 15:52:37 +01:00
run_cmd "Removing OpenVPN" apt-get remove --purge -y openvpn
2025-12-09 19:45:56 +01:00
# Remove OpenVPN official repository and GPG key
if [[ -e /etc/apt/sources.list.d/openvpn-aptrepo.list ]] ; then
run_cmd "Removing OpenVPN repo" rm /etc/apt/sources.list.d/openvpn-aptrepo.list
fi
if [[ -e /etc/apt/keyrings/openvpn-repo-public.asc ]] ; then
run_cmd "Removing OpenVPN GPG key" rm /etc/apt/keyrings/openvpn-repo-public.asc
2018-09-22 11:41:26 +02:00
fi
2025-12-15 12:36:18 +01:00
run_cmd_fatal "Updating package lists" apt-get update
2020-04-27 14:59:19 +02:00
elif [[ $OS == 'arch' ]] ; then
2025-12-09 15:52:37 +01:00
run_cmd "Removing OpenVPN" pacman --noconfirm -R openvpn
2025-12-09 19:45:56 +01:00
elif [[ $OS = ~ ( centos| oracle) ]] ; then
2025-12-09 15:52:37 +01:00
run_cmd "Removing OpenVPN" yum remove -y openvpn
2025-12-09 19:45:56 +01:00
# Disable Copr repo if it was enabled
if command -v dnf & >/dev/null; then
run_cmd "Disabling OpenVPN Copr repo" dnf copr disable -y @OpenVPN/openvpn-release-2.6 2>/dev/null || true
else
run_cmd "Disabling OpenVPN Copr repo" yum copr disable -y @OpenVPN/openvpn-release-2.6 2>/dev/null || true
fi
2025-12-10 17:54:00 +01:00
elif [[ $OS == 'amzn2023' ]] ; then
run_cmd "Removing OpenVPN" dnf remove -y openvpn
2020-04-27 14:59:19 +02:00
elif [[ $OS == 'fedora' ]] ; then
2025-12-09 15:52:37 +01:00
run_cmd "Removing OpenVPN" dnf remove -y openvpn
2025-12-12 04:22:12 +08:00
elif [[ $OS == 'opensuse' ]] ; then
run_cmd "Removing OpenVPN" zypper remove -y openvpn
2018-09-20 00:05:02 +02:00
fi
# Cleanup
2025-12-09 15:52:37 +01:00
run_cmd "Removing client configs from /home" find /home/ -maxdepth 2 -name "*.ovpn" -delete
run_cmd "Removing client configs from /root" find /root/ -maxdepth 1 -name "*.ovpn" -delete
run_cmd "Removing /etc/openvpn" rm -rf /etc/openvpn
run_cmd "Removing OpenVPN docs" rm -rf /usr/share/doc/openvpn*
run_cmd "Removing sysctl config" rm -f /etc/sysctl.d/99-openvpn.conf
run_cmd "Removing OpenVPN logs" rm -rf /var/log/openvpn
2018-09-20 00:05:02 +02:00
# Unbound
2025-12-11 13:14:56 +01:00
if [[ -e /etc/unbound/unbound.conf.d/openvpn.conf ]] ; then
2018-09-20 00:05:02 +02:00
removeUnbound
fi
2025-12-09 15:52:37 +01:00
log_success "OpenVPN removed!"
2018-09-20 00:05:02 +02:00
else
2025-12-09 15:52:37 +01:00
log_info "Removal aborted!"
2018-09-20 00:05:02 +02:00
fi
}
2020-04-27 14:59:19 +02:00
function manageMenu() {
2025-12-09 21:49:19 +01:00
local menu_option
2025-12-09 15:52:37 +01:00
log_header "OpenVPN Management"
log_prompt "The git repository is available at: https://github.com/angristan/openvpn-install"
log_success "OpenVPN is already installed."
log_menu ""
log_prompt "What do you want to do?"
log_menu " 1) Add a new user"
2025-12-13 19:17:30 +01:00
log_menu " 2) List client certificates"
log_menu " 3) Revoke existing user"
log_menu " 4) Renew certificate"
log_menu " 5) Remove OpenVPN"
2025-12-14 13:04:39 +01:00
log_menu " 6) List connected clients"
log_menu " 7) Exit"
until [[ ${ MENU_OPTION :- $menu_option } = ~ ^[ 1-7] $ ]] ; do
read -rp "Select an option [1-7]: " menu_option
2018-09-20 00:05:02 +02:00
done
2025-12-09 21:49:19 +01:00
menu_option = " ${ MENU_OPTION :- $menu_option } "
2018-09-20 00:05:02 +02:00
2025-12-09 21:49:19 +01:00
case $menu_option in
2020-04-27 14:59:19 +02:00
1)
newClient
2018-09-20 00:05:02 +02:00
;;
2020-04-27 14:59:19 +02:00
2)
2025-12-13 19:17:30 +01:00
listClients
2018-09-20 00:05:02 +02:00
;;
2020-04-27 14:59:19 +02:00
3)
2025-12-13 19:17:30 +01:00
revokeClient
2018-09-20 00:05:02 +02:00
;;
2020-04-27 14:59:19 +02:00
4)
2025-12-13 19:17:30 +01:00
renewMenu
2025-12-09 21:49:19 +01:00
;;
5)
2025-12-13 19:17:30 +01:00
removeOpenVPN
;;
6)
2025-12-14 13:04:39 +01:00
listConnectedClients
;;
7)
2020-04-27 14:59:19 +02:00
exit 0
2018-09-20 00:05:02 +02:00
;;
esac
}
2025-12-14 22:08:44 +01:00
# =============================================================================
# Main Entry Point
# =============================================================================
parse_args " $@ "