#!/bin/bash # auth.sh # # OpenVPN server-side --auth-user-pass-verify script # Runs when a client starts an OpenVPN session, and each key renegotiation (which happens every 20 mins) # 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 # exit 0 (success) if the real auth API response is good # exit 1 (failure) if it's not good, 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 *) if [[ -n "$auth_failed_reason_file" ]]; then # wait 1 min before retrying, since retrying with a token that's an invalid length is pointless because # it's never going to work, so print this message and hope the user sees it in their OpenVPN output so they # know they need to fix their token (probably a typo) echo -e "TEMP[backoff 60,advance no]: !!! INVALID TOKEN LENGTH !!!" > "$auth_failed_reason_file" fi exit 1 # not the length of a token or a token hash, so reject ;; esac # ---- Obfuscated API request batch with dummy requests ---- # Even though the API server uses HTTPS and is 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, which client # is which based on the known frequency of key renegotiations, 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 session up/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. # Temporary file to hold the genuine API replies auth_file=$(mktemp) total_requests=$((RANDOM % 4 + 2)) # 1-4 dummies + 1 real (token check) # We put the first request in a random position in $real_index so it's not always the same. real_index=$((RANDOM % total_requests + 1)) result_token="" result_up="" api_failed=1 i=1 while [ $i -le $total_requests ]; do if [ $i -eq $real_index ]; 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)) r=$(curl --http2 --silent --show-error --max-time 8 --connect-timeout 8 "https://$API_URL?token=$hash") printf '%s\n' "$r" >"$auth_file" ) & 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 "$auth_file" ]; then result=$(<"$auth_file") api_failed=0 fi rm -f "$auth_file" # exit 0 if wget fails, in case the auth API server is down if [ -z "$result" ]; then exit 0 fi # process the result case "$result" in exp) if [[ -n "$auth_failed_reason_file" ]]; then echo -e "TEMP[backoff 60,advance no]: !!! YOUR TOKEN HAS EXPIRED !!!" > "$auth_failed_reason_file" fi exit 1 # expired token ;; gud) exit 0 # success ;; *) if [[ "$result" == "max" ]]; then if [[ -n "$auth_failed_reason_file" ]]; then echo -e "TEMP[backoff 30,advance no]: !!! MAX SESSIONS REACHED FOR THAT TOKEN !!!" > "$auth_failed_reason_file" fi fi exit 1 # anything else is invalid ;; esac