#!/bin/bash # # Small script to use OpenVPN within a Qubes ProxyVM. # Inspired by: https://groups.google.com/forum/?_escaped_fragment_=topic/qubes-users/W8PjtzcHJAE#!topic/qubes-users/W8PjtzcHJAE # # David Hobach # 0.3 / 21.03.2015 # # 1. Put your openvpn config files from your VPN provider in this folder. The script will choose a random config file from whatever config files you place in this directory. # 2. Set all necessary options below. # 3. Make sure the script is run during startup by using the /rw/config/rc.local script (+ make rc.local executable). # 4. IMPORTANT: Only allow traffic to your VPN server(s) by using the Qubes firewall rules (this is then implemented by the firewall VM --> much better than any local iptables rules). In particular do NOT allow DNS traffic! # 5. Automatically start the VPN VM right after booting (should work by using the Qubes manager). # 6. Make sure all necessary VMs are connected to the VPN proxy VM & refresh the disposable VMs (it won't work without rebuilding the disposable VM template after changing the default NetVM). # # Note: This script also implements some local firewall rules to harden the system against attacks against the VM from inside the VPN. #chooseRandomFile [path] [suffix] #[path]: path where to list potential files to choose from #[suffix]: suffix of the files to choose from #returns: the complete path to the chosen file; may be empty, if no matching file was found function chooseRandomFile { local path="$1" local suffix="$2" ret="$(ls "$path"/*."$suffix" | sort -R --random-source /dev/urandom | head -n1)" echo "$ret" } ################## begin: global vars ############################# #name of the directory this script resides in (hopefully) SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" #name of this script SCRIPT_NAME="$(basename "$0")" #suffix used by OpenVPN config files in this directory OPENVPN_CONFIG_SUFFIX="ovpn" #path to the OpenVPN config file to use OPENVPN_CONFIG_FILE="$(chooseRandomFile "$SCRIPT_DIR" "$OPENVPN_CONFIG_SUFFIX")" #path to the openvpn pid file to use OPENVPN_PID_FILE="$SCRIPT_DIR/openvpn.pid" #value of the openvpn pid OPENVPN_PID=$([ -f "$OPENVPN_PID_FILE" ] && cat "$OPENVPN_PID_FILE") #Open VPN sleep time in s OPENVPN_SLEEP=2 #path to the qubes script that reads the resolv.conf and forces all DNS requests to go to these servers by using DNATting QUBES_DNS_DNAT_SCRIPT="/usr/lib/qubes/qubes-setup-dnat-to-ns" #path to the resolv.conf RESOLV_CONF="/etc/resolv.conf" #backup file to create for the resolv.conf RESOLV_CONF_BAK="${RESOLV_CONF}.openvpn.bak" #for iptables #VERY IMPORTANT: set the DNS server(s) of your VPN provider here in resolv.conf notation! (for airvpn: https://airvpn.org/specs/) VPN_DNS_SERVERS="nameserver 10.5.0.1" #IP range of the local Qubes hosts for filtering QUBES_RANGE="10.136.0.0/14" #tunneling interface used by openvpn in iptables notation OPENVPN_INTERFACE="tun+" #name for the VPN chain to use in iptables IPTABLES_VPN_CHAIN_NAME="vpn_incoming" #install string for the custom chain IPTABLES_CUSTOM_CHAIN_INSTALL="-i $OPENVPN_INTERFACE -j $IPTABLES_VPN_CHAIN_NAME" ################## end: global vars ############################# #use the DNS server of the resolv.conf for all DNS requests to this VM function installDNSDnat { #create resolv.conf backup, if it doesn't exist [ ! -f "$RESOLV_CONF_BAK" ] && cp "$RESOLV_CONF" "$RESOLV_CONF_BAK" #set the new DNS server echo -e "$VPN_DNS_SERVERS" > "$RESOLV_CONF" #run the qubes dnat script to enforce the new resolv.conf $QUBES_DNS_DNAT_SCRIPT } function removeDNSDnat { #restore the old resolv.conf, if possible mv "$RESOLV_CONF_BAK" "$RESOLV_CONF" &> /dev/null #run the qubes dnat script to enforce the old resolv.conf $QUBES_DNS_DNAT_SCRIPT } function startLocalVPNFirewall { #NOTE: openvpn will effectively set the default route differently by using the 0.0.0.0/1 network in 2 routing entries, i.e. without changing the prior default route, if the "redirect-gateway def1" is used #It is important to understand how openvpn routing works: #A packet is received on some internal interface, then the routing table states that it should be sent to the openvpn interface tun+ and the other way around (more specific matches in the routing table take priority!). I.e. standard linux routing is employed. --> We can intercept that by using e.g. the iptables the FORWARD chain for all packets from that interface that are being routed to other interfaces and the INPUT for all packets destined to local processes (PREROUTING does not support filtering). #make the DNS work installDNSDnat #create the custom chain #-t filter is assumed everywhere by default iptables -N "$IPTABLES_VPN_CHAIN_NAME" #install the chain with the existing ones iptables -I INPUT $IPTABLES_CUSTOM_CHAIN_INSTALL iptables -I FORWARD $IPTABLES_CUSTOM_CHAIN_INSTALL #configure our custom chain: #allow TCP & ICMP access, if it was started from here iptables -A "$IPTABLES_VPN_CHAIN_NAME" -m conntrack --ctstate ESTABLISHED -j ACCEPT #accept incoming udp DNS packets (UDP is a stateless protocol, so no tricks as with tcp possible) iptables -A "$IPTABLES_VPN_CHAIN_NAME" -p udp --sport 53 -j ACCEPT #make sure that no packet is using qubes addresses as source iptables -I "$IPTABLES_VPN_CHAIN_NAME" -s "$QUBES_RANGE" -j DROP #set the default: drop everything iptables -A "$IPTABLES_VPN_CHAIN_NAME" -j DROP } function stopLocalVPNFirewall { #remove our custom chain from the existing ones iptables -D INPUT $IPTABLES_CUSTOM_CHAIN_INSTALL &> /dev/null iptables -D FORWARD $IPTABLES_CUSTOM_CHAIN_INSTALL &> /dev/null #remove the entire chain iptables -F "$IPTABLES_VPN_CHAIN_NAME" &> /dev/null iptables -X "$IPTABLES_VPN_CHAIN_NAME" &> /dev/null #remove the DNS natting removeDNSDnat } function startR { #try to remove old firewall rules, if there are any stopLocalVPNFirewall #start OpenVPN openvpn --daemon --config "$OPENVPN_CONFIG_FILE" --writepid "$OPENVPN_PID_FILE" #some sleep for openvpn to 100% come up sleep "$OPENVPN_SLEEP" #start the local VPN firewall startLocalVPNFirewall } function stopR { #stop OpenVPN kill "$OPENVPN_PID" #some sleep for openvpn to shut down sleep "$OPENVPN_SLEEP" #stop the local VPN firewall stopLocalVPNFirewall } #enforce that the user is root; if not, exit function enforceRoot { [ "$(whoami)" != "root" ] && echo "This script must be run as root. Aborting..." && exit 1 } function deleteStalePidFile { if [ -f "$OPENVPN_PID_FILE" ] ; then #better re-read the PID OPENVPN_PID=$([ -f "$OPENVPN_PID_FILE" ] && cat "$OPENVPN_PID_FILE") #delete it, if the pid is for some reason empty, but the file exists #delete it, if the pid file is not empty, but the process does not exist if [ -z "$OPENVPN_PID" ] || [ ! -e "/proc/$OPENVPN_PID" ] ; then rm -f "$OPENVPN_PID_FILE" fi fi } #returns 0, if the OpenVPN process is running and 1 otherwise (= exit code) function getStatus { deleteStalePidFile [ -f "$OPENVPN_PID_FILE" ] && return 0 return 1 } #check if OpenVPN is already running and if so, exit #also deletes stale pid files function exitIfOpenVPNIsRunning { getStatus && echo "OpenVPN is already running. Aborting..." && exit 1 } function exitIfOpenVPNIsNotRunning { ! getStatus && echo "OpenVPN was not running. Aborting..." && exit 1 } function start { enforceRoot echo "Attempting to start OpenVPN... Config file: $OPENVPN_CONFIG_FILE" exitIfOpenVPNIsRunning startR status } function stop { enforceRoot echo "Attempting to stop OpenVPN..." exitIfOpenVPNIsNotRunning stopR status } function restart { stop start } function status { if getStatus ; then echo "OpenVPN is running." else echo "OpenVPN is not running." fi } function stopFirewall { enforceRoot stopLocalVPNFirewall echo "VPN-specific firewall rules deleted." } function showServerIPs { cd "$SCRIPT_DIR" cat *."$OPENVPN_CONFIG_SUFFIX" | grep "remote " | sed -r 's/remote ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) ([0-9]+)/\1:\2/g' | sort } case "$1" in start) start ;; stop) stop ;; restart) restart ;; status) status ;; showServerIPs) showServerIPs ;; stopFirewall) stopFirewall ;; *) echo "Usage: $0 start|stop|restart|status|showServerIPs|stopFirewall" ;; esac