#!/bin/bash # # Docker script to configure and start an OpenVPN server # # DO NOT RUN THIS SCRIPT ON YOUR PC OR MAC! THIS IS ONLY MEANT TO BE RUN # IN A CONTAINER! # # This file is part of OpenVPN Docker image, available at: # https://github.com/hwdsl2/docker-openvpn # # Copyright (C) 2026 Lin Song # Copyright (C) 2013 Nyr # # Based on the work of Nyr and contributors at: # https://github.com/Nyr/openvpn-install # # This work is licensed under the MIT License # See: https://opensource.org/licenses/MIT export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" exiterr() { echo "Error: $1" >&2; exit 1; } nospaces() { printf '%s' "$1" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'; } noquotes() { printf '%s' "$1" | sed -e 's/^"\(.*\)"$/\1/' -e "s/^'\(.*\)'$/\1/"; } easyrsa_run() { echo "+ easyrsa $*" >&2; easyrsa "$@" >/dev/null 2>&1; } check_ip() { IP_REGEX='^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$' printf '%s' "$1" | tr -d '\n' | grep -Eq "$IP_REGEX" } get_public_ip() { local ip_addr ip_url for ip_url in https://ipv4.icanhazip.com https://api.ipify.org; do ip_addr=$(wget -t 2 -T 10 -4 --max-redirect=0 -qO- "$ip_url" 2>/dev/null) || continue ip_addr=${ip_addr%$'\r'} [[ "$ip_addr" != *$'\n'* && "$ip_addr" != *$'\r'* ]] || continue if check_ip "$ip_addr"; then public_ip="$ip_addr" return 0 fi done return 1 } check_ip6() { IP6_REGEX='^[0-9a-fA-F]{0,4}(:[0-9a-fA-F]{0,4}){1,7}$' printf '%s' "$1" | tr -d '\n' | grep -Eq "$IP6_REGEX" } check_dns_name() { FQDN_REGEX='^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$' printf '%s' "$1" | tr -d '\n' | grep -Eq "$FQDN_REGEX" } usage_arch() { local arch arch=$(uname -m 2>/dev/null || printf 'unknown') case "$arch" in x86_64|amd64) printf 'amd64' ;; aarch64|arm64) printf 'arm64' ;; armv7l|armv7|armhf) printf 'armv7' ;; *) printf 'other' ;; esac } write_usage_state() { local state_file version tmp_file state_file=$1 version=$2 mkdir -p "$USAGE_STATE_DIR" || return 1 tmp_file=$(mktemp "$USAGE_STATE_DIR/.usage.XXXXXX") || return 1 printf '%s\n' "$version" > "$tmp_file" || return 1 chmod 0644 "$tmp_file" 2>/dev/null || true mv "$tmp_file" "$state_file" } fetch_usage_asset() { local asset base_url asset=$1 base_url=${VPN_USAGE_BASE_URL:-https://github.com/hwdsl2/vpn-extras/releases/download/v1.1.0} base_url=${base_url%/} command -v wget >/dev/null 2>&1 || return 1 wget -q -T 5 -O /dev/null "$base_url/$asset" >/dev/null 2>&1 } read_state_value() { [ -r "$1" ] || return 0 tr -d '[:space:]' < "$1" } report_usage_counts() { local current_version arch state_file last_version action [ "${VPN_DISABLE_USAGE_COUNTS:-0}" != "1" ] || return 0 $data_mounted || return 0 current_version="${IMAGE_FLAVOR:-unknown}-${IMAGE_VER:-unknown}" arch=$(usage_arch) state_file="$USAGE_STATE_DIR/openvpn.version" last_version=$(read_state_value "$state_file") action= if [ -z "$last_version" ]; then if $data_existing; then action=upgrade else action=deploy fi elif [ "$last_version" != "$current_version" ]; then action=upgrade fi if [ -n "$action" ]; then if fetch_usage_asset "vpn-v1-openvpn-$action-$arch"; then write_usage_state "$state_file" "$current_version" || true fi fi } # Source bind-mounted env file if present (takes precedence over --env-file) if [ -f /vpn.env ]; then # shellcheck disable=SC1091 . /vpn.env fi if [ ! -f "/.dockerenv" ] && [ ! -f "/run/.containerenv" ] \ && [ -z "$KUBERNETES_SERVICE_HOST" ] \ && ! head -n 1 /proc/1/sched 2>/dev/null | grep -q '^run\.sh '; then exiterr "This script ONLY runs in a container (e.g. Docker, Podman)." fi if [ ! -e /dev/net/tun ] || ! ( exec 7<>/dev/net/tun ) 2>/dev/null; then exiterr "The TUN device is not available. Add '--device /dev/net/tun' to your docker run command." fi NET_IFACE=$(route 2>/dev/null | grep -m 1 '^default' | grep -o '[^ ]*$') [ -z "$NET_IFACE" ] && NET_IFACE=$(ip -4 route list 0/0 2>/dev/null | grep -m 1 -o 'dev [^ ]*' | awk '{print $2}') [ -z "$NET_IFACE" ] && NET_IFACE=eth0 # Read and sanitize environment variables VPN_DNS_NAME=$(nospaces "$VPN_DNS_NAME") VPN_DNS_NAME=$(noquotes "$VPN_DNS_NAME") VPN_PUBLIC_IP=$(nospaces "$VPN_PUBLIC_IP") VPN_PUBLIC_IP=$(noquotes "$VPN_PUBLIC_IP") VPN_PROTO=$(nospaces "$VPN_PROTO") VPN_PROTO=$(noquotes "$VPN_PROTO") VPN_PORT=$(nospaces "$VPN_PORT") VPN_PORT=$(noquotes "$VPN_PORT") VPN_CLIENT_NAME=$(nospaces "$VPN_CLIENT_NAME") VPN_CLIENT_NAME=$(noquotes "$VPN_CLIENT_NAME") VPN_DNS_SRV1=$(nospaces "$VPN_DNS_SRV1") VPN_DNS_SRV1=$(noquotes "$VPN_DNS_SRV1") VPN_DNS_SRV2=$(nospaces "$VPN_DNS_SRV2") VPN_DNS_SRV2=$(noquotes "$VPN_DNS_SRV2") if [ -n "$VPN_PUBLIC_IP6" ]; then VPN_PUBLIC_IP6=$(nospaces "$VPN_PUBLIC_IP6") VPN_PUBLIC_IP6=$(noquotes "$VPN_PUBLIC_IP6") fi # Apply defaults [ -z "$VPN_PROTO" ] && VPN_PROTO=udp [ -z "$VPN_PORT" ] && VPN_PORT=1194 [ -z "$VPN_CLIENT_NAME" ] && VPN_CLIENT_NAME=client [ -z "$VPN_DNS_SRV1" ] && VPN_DNS_SRV1=8.8.8.8 [ -z "$VPN_DNS_SRV2" ] && VPN_DNS_SRV2=8.8.4.4 # Validate protocol case "$VPN_PROTO" in tcp|udp) ;; *) exiterr "VPN_PROTO must be 'tcp' or 'udp'. Got: '$VPN_PROTO'." ;; esac # Validate port if ! printf '%s' "$VPN_PORT" | grep -Eq '^[0-9]+$' \ || [ "$VPN_PORT" -lt 1 ] || [ "$VPN_PORT" -gt 65535 ]; then exiterr "VPN_PORT must be an integer between 1 and 65535." fi # Sanitize and validate client name VPN_CLIENT_NAME=$(printf '%s' "$VPN_CLIENT_NAME" | \ sed 's/[^0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-]/_/g') if [ -z "$VPN_CLIENT_NAME" ]; then exiterr "VPN_CLIENT_NAME is invalid. Use one word only, no special characters except '-' and '_'." fi # Validate DNS servers if [ -n "$VPN_DNS_SRV1" ] && ! check_ip "$VPN_DNS_SRV1"; then exiterr "VPN_DNS_SRV1 '$VPN_DNS_SRV1' is not a valid IPv4 address." fi if [ -n "$VPN_DNS_SRV2" ] && ! check_ip "$VPN_DNS_SRV2"; then exiterr "VPN_DNS_SRV2 '$VPN_DNS_SRV2' is not a valid IPv4 address." fi # Determine server address for client configurations if [ -n "$VPN_DNS_NAME" ]; then if ! check_dns_name "$VPN_DNS_NAME"; then exiterr "VPN_DNS_NAME '$VPN_DNS_NAME' is not a valid fully qualified domain name (FQDN)." fi server_addr="$VPN_DNS_NAME" elif [ -n "$VPN_PUBLIC_IP" ]; then if ! check_ip "$VPN_PUBLIC_IP"; then exiterr "VPN_PUBLIC_IP '$VPN_PUBLIC_IP' is not a valid IPv4 address." fi server_addr="$VPN_PUBLIC_IP" else echo echo "Trying to auto-detect public IP of this server..." if ! get_public_ip; then exiterr "Cannot detect this server's public IP. Define it in your 'env' file as 'VPN_PUBLIC_IP'." fi server_addr="$public_ip" fi # Detect IPv6 ip6="" if [ -n "$VPN_PUBLIC_IP6" ]; then ip6="$VPN_PUBLIC_IP6" check_ip6 "$ip6" || { echo "Warning: Invalid IPv6 address in 'VPN_PUBLIC_IP6'. Detecting IPv6..." >&2; ip6=""; } fi if [ -z "$ip6" ]; then ip6=$(ip -6 addr 2>/dev/null | awk '/inet6 [23]/ {print $2}' | cut -d'/' -f1 | head -n1) check_ip6 "$ip6" || ip6="" if [ -z "$ip6" ] && ip -6 addr 2>/dev/null | grep 'inet6' | grep -qv 'inet6 \(::1\|fe80\)'; then ip6=$(wget -t 2 -T 10 -qO- https://ipv6.icanhazip.com 2>/dev/null) check_ip6 "$ip6" || ip6="" fi fi OVPN_CONF="/etc/openvpn/server/server.conf" USAGE_STATE_DIR="/etc/openvpn/.openvpn-usage" data_mounted=false data_existing=false if grep -q " /etc/openvpn " /proc/mounts 2>/dev/null; then data_mounted=true fi if $data_mounted && [ -s "$OVPN_CONF" ]; then data_existing=true fi mkdir -p /etc/openvpn/server/easy-rsa mkdir -p /etc/openvpn/clients echo echo "OpenVPN Docker - https://github.com/hwdsl2/docker-openvpn" if ! grep -q " /etc/openvpn " /proc/mounts; then echo echo "Note: /etc/openvpn is not mounted. Server data (keys, certificates" echo " and client configs) will be lost on container removal." echo " Mount a Docker volume at /etc/openvpn to persist data." fi if [ ! -f "$OVPN_CONF" ]; then echo echo "Starting OpenVPN setup..." echo "Server address: $server_addr" echo "Protocol: $VPN_PROTO, Port: $VPN_PORT" echo "First client: $VPN_CLIENT_NAME" echo "DNS servers: $VPN_DNS_SRV1, $VPN_DNS_SRV2" if [ -n "$ip6" ]; then echo "IPv6: enabled" fi echo echo "Initializing PKI..." export EASYRSA_PKI="/etc/openvpn/server/easy-rsa/pki" easyrsa_run --batch init-pki easyrsa_run --batch build-ca nopass easyrsa_run --batch --days=3650 build-server-full server nopass easyrsa_run --batch --days=3650 build-client-full "$VPN_CLIENT_NAME" nopass easyrsa_run --batch --days=3650 gen-crl # Copy certs to server directory cp /etc/openvpn/server/easy-rsa/pki/ca.crt \ /etc/openvpn/server/easy-rsa/pki/private/ca.key \ /etc/openvpn/server/easy-rsa/pki/issued/server.crt \ /etc/openvpn/server/easy-rsa/pki/private/server.key \ /etc/openvpn/server/easy-rsa/pki/crl.pem \ /etc/openvpn/server/ # CRL is read with each client connection (OpenVPN drops to nobody) chown nobody:nobody /etc/openvpn/server/crl.pem # Without +x on the directory, OpenVPN can't run stat() on the CRL file chmod o+x /etc/openvpn/server/ # Generate TLS crypt key openvpn --genkey secret /etc/openvpn/server/tc.key # Use pre-defined ffdhe2048 DH parameters (RFC 7919) cat > /etc/openvpn/server/dh.pem <<'DHEOF' -----BEGIN DH PARAMETERS----- MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz +8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a 87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7 YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi 7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg== -----END DH PARAMETERS----- DHEOF # Generate server configuration cat > /etc/openvpn/server/server.conf <> /etc/openvpn/server/server.conf <> /etc/openvpn/server/server.conf <<'EOF' push "redirect-gateway def1 bypass-dhcp" push "block-ipv6" push "ifconfig-ipv6 fddd:1194:1194:1194::2/64 fddd:1194:1194:1194::1" EOF fi cat >> /etc/openvpn/server/server.conf <> /etc/openvpn/server/server.conf fi # Generate client config template cat > /etc/openvpn/server/client-common.txt <" cat /etc/openvpn/server/easy-rsa/pki/ca.crt echo "" echo "" sed -ne '/BEGIN CERTIFICATE/,$ p' \ /etc/openvpn/server/easy-rsa/pki/issued/"$VPN_CLIENT_NAME".crt echo "" echo "" cat /etc/openvpn/server/easy-rsa/pki/private/"$VPN_CLIENT_NAME".key echo "" echo "" sed -ne '/BEGIN OpenVPN Static key/,$ p' /etc/openvpn/server/tc.key echo "" } > /etc/openvpn/clients/"$VPN_CLIENT_NAME".ovpn chmod 600 /etc/openvpn/clients/"$VPN_CLIENT_NAME".ovpn echo echo "Setup complete." echo "Client configuration: /etc/openvpn/clients/$VPN_CLIENT_NAME.ovpn" echo "Use 'docker cp :/etc/openvpn/clients/$VPN_CLIENT_NAME.ovpn .' to download it." echo "Use 'docker exec ovpn_manage --addclient ' to add more clients." echo else echo echo "Found existing OpenVPN configuration, starting server..." echo # Refresh CRL symlink ownership in case volume was remounted chown nobody:nobody /etc/openvpn/server/crl.pem 2>/dev/null || true chmod o+x /etc/openvpn/server/ 2>/dev/null || true fi # Update sysctl settings syt='/sbin/sysctl -e -q -w' $syt net.ipv4.ip_forward=1 2>/dev/null $syt net.ipv4.conf.all.accept_redirects=0 2>/dev/null $syt net.ipv4.conf.all.send_redirects=0 2>/dev/null $syt net.ipv4.conf.all.rp_filter=0 2>/dev/null $syt net.ipv4.conf.default.accept_redirects=0 2>/dev/null $syt net.ipv4.conf.default.send_redirects=0 2>/dev/null $syt net.ipv4.conf.default.rp_filter=0 2>/dev/null $syt "net.ipv4.conf.$NET_IFACE.send_redirects=0" 2>/dev/null $syt "net.ipv4.conf.$NET_IFACE.rp_filter=0" 2>/dev/null if [ -n "$ip6" ]; then $syt net.ipv6.conf.all.forwarding=1 2>/dev/null fi # Set up iptables rules modprobe -q ip_tables 2>/dev/null if ! iptables -t nat -C POSTROUTING -s 10.8.0.0/24 -o "$NET_IFACE" -j MASQUERADE 2>/dev/null; then iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o "$NET_IFACE" -j MASQUERADE iptables -I INPUT -p "$VPN_PROTO" --dport "$VPN_PORT" -j ACCEPT iptables -I FORWARD -s 10.8.0.0/24 -j ACCEPT iptables -I FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT fi if [ -n "$ip6" ]; then modprobe -q ip6_tables 2>/dev/null if ip6tables -t nat -L >/dev/null 2>&1; then if grep -qs "server-ipv6" "$OVPN_CONF" && \ ! ip6tables -t nat -C POSTROUTING -s fddd:1194:1194:1194::/64 \ -o "$NET_IFACE" -j MASQUERADE 2>/dev/null; then ip6tables -t nat -A POSTROUTING -s fddd:1194:1194:1194::/64 -o "$NET_IFACE" -j MASQUERADE ip6tables -I FORWARD -s fddd:1194:1194:1194::/64 -j ACCEPT ip6tables -I FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT fi fi fi echo "Starting OpenVPN server..." echo report_usage_counts exec openvpn --cd /etc/openvpn/server --config server.conf