#!/bin/bash # session_up.sh # # OpenVPN server-side --client-connect script # Runs when a client starts an OpenVPN session # 1) Validates provided token # 2) Checks token against auth API # 2.5) Sends a random number of dummy auth API requests along with the real one # 3) If success, randomly assign internal IPv4/6 address for client # 3.5) push routes and DNS settings, and if supported (Windows) use block-outside-dns to prevent DNS leaks # exit 0 (success) if everything was successful # exit 1 (failure) if not, unless the auth API is offline, then exit 0 so clients can still get in # API_URL=$(< /etc/api) # only allow characters that are in a token or a token's hash (a-zA-Z0-9-) username=$(echo "$username" | tr -dc 'a-zA-Z0-9-' ) # only allow valid lengths (126-128 for hashes, 23 for plaintext tokens) case "${#username}" in 128|127|126) hash=$username ;; # is a hash already (some routers cut off the last one or two bytes, so accounting for that) 23) hash=$(echo -n "$username" | openssl sha512 | awk '{print $NF}') ;; # is a plaintext token, so hash it for them *) exit 1 ;; esac # ---- Obfuscated API request batch with dummy requests ---- # Even though the API server uses HTTPS that's restricted to TLSv1.3 and post-quantum key exchanges, someone capable of sniffing # the encrypted traffic between the VPN server and the API server might be able to infer potentially sensitive data about a VPN # client based on when those API requests/responses occur (I.e., when a client initially connects or disconnects, how long their # VPN session was, etc.) # To account for this, a random number of dummy API requests are sent along with the real request. # All responses have the same length to prevent size-related side channel leaks. # The dummy requests also rotate between the parameters used by this script and the auth and session down ones, so anyone listening # in won't know if this is a session going up, going down, or a key renegotiation. # There's also a separate daemon that sends a random number of dummy API requests at a random frequency. # Two temporary files to hold the genuine API replies token_file=$(mktemp) up_file=$(mktemp) total_requests=$((RANDOM % 4 + 3)) # 1-4 dummies + 2 real (token check + session up) # We put the first request in a random position in $real_index1 so it's not always the same. real_index1=$((RANDOM % total_requests + 1)) while :; do # Then do the same for the second request and $real_index2 real_index2=$((RANDOM % total_requests + 1)) [ "$real_index2" -ne "$real_index1" ] && break done result_token="" result_up="" api_failed=1 i=1 while [ $i -le $total_requests ]; do if [ $i -eq $real_index1 ] || [ $i -eq $real_index2 ]; then # Real request ( # Sleep for a random interval between 0-4 seconds # This is so an API request doesn't always go out immediately after a client connects, since that might be useful in some # correlation/timing attacks (still assuming the attacker can monitor the traffic between the VPN and API server). sleep $((RANDOM % 5)) if [ $i -eq $real_index1 ]; then r=$(curl --http2 --silent --show-error --max-time 8 --connect-timeout 8 "https://$API_URL?token=$hash") printf '%s\n' "$r" >"$token_file" else r=$(curl --http2 --silent --show-error --max-time 8 --connect-timeout 8 "https://$API_URL?token=$hash&action=up") printf '%s\n' "$r" >"$up_file" fi ) & else # Dummy request ( # Sleep for a random interval between 0-4 seconds here too so it behaves the same as a real request sleep $((RANDOM % 5)) # Randomly pick a dummy hash for each request dummy_hash=$(head -c 64 /dev/urandom | sha512sum | awk '{print $1}') # Randomly pick a dummy parameter (auth/up/down) # This prevents anyone from inferring the API request type based on the size differences in the encrypted packets case $((RANDOM % 3)) in 0) dummy_url="https://$API_URL?token=$dummy_hash" ;; 1) dummy_url="https://$API_URL?token=$dummy_hash&action=up" ;; 2) dummy_url="https://$API_URL?token=$dummy_hash&action=down" ;; esac curl --http2 --silent --max-time 8 --connect-timeout 8 "$dummy_url" >/dev/null 2>&1 ) & fi i=$((i + 1)) done # Wait until every background job (real + dummy) is done wait # Pick up the two real answers, if they were written if [ -s "$token_file" ]; then result_token=$(<"$token_file") api_failed=0 fi if [ -s "$up_file" ]; then result_up=$(<"$up_file") api_failed=0 fi rm -f "$token_file" "$up_file" # If API failed (no response from either real request), skip checks and continue (fail open) if [ "$api_failed" -eq 0 ]; then # Only do these checks if the API responded if [ "$result_token" != "gud" ]; then exit 1 # token invalid or expired fi if [ "$result_up" == "max" ]; then exit 1 # max sessions reached fi fi # Use /tmp/pool to store files representing the internal IPs that get randomly generated below instance_pool_dir=/tmp/pool # Create the dir if it doesn't exist if [ ! -d "$instance_pool_dir" ]; then mkdir -p "$instance_pool_dir" fi # Randomly assign an internal IPv6 address (only happens if connecting to an IPv6 entry) if [[ -n $ifconfig_pool_remote_ip6 ]]; then found_one=0 while [ "$found_one" -eq "0" ]; do rand=$(od -An -N8 -tu8 < /dev/urandom | awk '{print $1}') hex=$(printf "%016x\n" "$rand") RANDIP="${ifconfig_pool_remote_ip6%%::*}:${hex:0:4}:${hex:4:4}:${hex:8:4}:${hex:12:4}" if [ ! -r "$instance_pool_dir/$RANDIP" ]; then touch "$instance_pool_dir/$RANDIP" echo "ifconfig-ipv6-push ${RANDIP} fe80::1" >> "$1" # Push server's local IPv6 DNS server, the non-ad/tracker blocking one echo 'push "dhcp-option DNS 2001:db8::8"' >> "$1" found_one=1 fi done fi # Randomly assign an internal IPv4 address (always happens) if [[ -n $ifconfig_pool_remote_ip ]]; then POOL=$(echo "$ifconfig_pool_remote_ip" | awk -F. '{print $1"."$2"."$3}') found_one=0 while [ "$found_one" -eq "0" ]; do RANDIP=$POOL.$((3 + RANDOM % 251)) if [ ! -r "$instance_pool_dir/$RANDIP" ]; then touch "$instance_pool_dir/$RANDIP" echo "ifconfig-push $RANDIP 255.255.255.0" >> "$1" # Push server's local IPv4 DNS server, the non-ad/tracker blocking one echo 'push "dhcp-option DNS 10.31.33.8"' >> "$1" found_one=1 fi done fi # Windows platform-specific routes if [[ $IV_PLAT == "win" ]]; then echo 'push "redirect-gateway bypass-dhcp"' >> "$1" echo 'push "register-dns"' >> "$1" # block-outside-dns was added in OpenVPN 2.3.9, so only push it if they're using > 2.3.8 if [[ $IV_VER =~ ^2\.3.* ]]; then test_ver=$(echo "$IV_VER" | awk -F. '{print $NF}') if [ "$test_ver" -gt 8 ]; then echo 'push "block-outside-dns"' >> "$1" fi else echo 'push "block-outside-dns"' >> "$1" fi fi # Success exit 0