Bypass VPN for Specific Processes

openvpn
quickvpn
vpn

#1

Need to bypass your VPN for specific processes like rclone or similar?

Check out this script! It will let you run processes that bypass your VPN connection and go through the interface that you specify!

First, copy the script into a file and label it as bypass.sh.
Edit the line real_interface="eth0" and change it to your default interface (find it with ifconfig).
Next give it executable permissions by running chmod +x bypass.sh.
Now just type ./bypass.sh and check out the arguments!

To run a process through it, do something like this:
./bypass.sh ping quickbox.io
And the process will now bypass the VPN interface and utilize whatever you have specified in the config!

If you want to use rclone with it, build a bash script and enter a line like this:
bash /opt/bypass.sh rclone ... and enter your parameters as normal.

I did not write the script but I did a small fix to solve the issue where the traceroute command had an invalid parameter and I simply removed it.

Let me know if you find it useful!


# === INFO ===
# NoVPN
# Description: Bypass VPN tunnel for applications run through this tool.
VERSION="1.0.2.1"
# Author: KrisWebDev
# Requirements:  Linux with kernel > 2.6.4 (released in 2008).
#                Only tested on Ubuntu 15, 16 with bash.
#                Main dependencies are automatically installed.
#                Script will guide you for iptables 1.6.0 install.

# === LICENSE ===
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.

# === CONFIGURATION ===
real_interface="eth0"

# === ADVANCES CONFIGURATION ===
cgroup_name="novpn" # Better keep it with purely lowercase alphabetic & underscore
net_cls_classid="0x00110011" # Anything from 0x00000001 to 0xFFFFFFFF
ip_table_fwmark="11" # Anything from 1 to 2147483647
ip_table_number="11" # Anything from 1 to 252
ip_table_name="$cgroup_name"

# === CODE ===
real_interface_gateway=`ip route | grep "dev ${real_interface}" | awk '/^default/ { print $3 }'`
#real_interface_ip=`ip addr show "$real_interface" | awk '$1 == "inet" {gsub(/\/.*$/, "", $2); print $2}'`

# Handle options
action="command"
background=false
skip=false
init_nb_args="$#"

while [ "$#" -gt 0 ]; do
  case "$1" in
    -b|--background) background=true; shift 1;;
    -o|--outside) action="outside"; shift 1;;
    -i|--inside) action="inside"; shift 1;;
    -l|--list) action="list"; shift 1;;
    -s|--skip) skip=true; shift 1;;
    -l|--clean) action="clean"; shift 1;;
    -h|--help) action="help"; shift 1;;
    -v|--version) echo "novpn v$VERSION"; exit 0;;
    -*) echo "Unknown option: $1. Try --help." >&2; exit 1;;
    *) break;; # Start of COMMAND or LIST
  esac
done

if [ "$init_nb_args" -lt 1 ] || [ "$action" = "help" ] ; then
	me=`basename "$0"`
	echo -e "Usage : \e[1m$me [\e[4mOPTIONS\e[24m] [\e[4mCOMMAND\e[24m [\e[4mCOMMAND PARAMETERS\e[24m]]\e[0m"
	echo -e "   or : \e[1m$me [\e[4mOPTIONS\e[24m] { --outside | --inside } \e[4mLIST\e[24m\e[0m"
	echo -e "Run command outside the VPN tunnel."
	echo
	echo -e "\e[1m\e[4mOPTIONS\e[0m:"
	echo -e "\e[1m-b, --background\e[0m    Start \e[4mCOMMAND\e[24m as background process (release the shell)."
	echo -e "\e[1m-o, --outside \e[4mLIST\e[24m\e[0m  Move running process \e[4mLIST\e[24m outside tunnel. (BROKEN)"
	echo -e "\e[1m-i, --inside \e[4mLIST\e[24m\e[0m   Move back running process \e[4mLIST\e[24m inside tunnel."
	echo -e "\e[1m-l, --list\e[0m          List processes going outside tunnel."
	echo -e "\e[1m-s, --skip\e[0m          Don't check/setup system config & don't ask for root,\n\
	             run \e[4mCOMMAND\e[24m or move process \e[4mLIST\e[24m even if tunnel bypass fails."
	echo -e "\e[1m-c, --clean\e[0m         Move back all proceses inside tunnel and remove system config."
	echo -e "\e[1m-v, --version\e[0m       Print this program version."
	echo -e "\e[1m-h, --help\e[0m          This help."
	echo
	echo -e "\e[1m\e[4mLIST\e[0m: List of process ID or names separated by spaces."
	exit 1
fi

# Helper functions

# Check the presence of required system packages
check_package(){
	nothing_installed=1
	for package_name in "[email protected]"
	do
		if ! dpkg -l "$package_name" &> /dev/null; then
			echo "Installing $package_name"
			sudo apt-get install "$package_name"
			nothing_installed=0
		fi
	done
	return $nothing_installed
}

# List processes bypassing the VPN
list_outside(){
	return_status=1
	echo -e "PID""\t""CMD"
	while read task_pid
		do
			echo -e "${task_pid}""\t""`ps -p ${task_pid} -o comm=`";
			return_status=0
	done < /sys/fs/cgroup/net_cls/${cgroup_name}/tasks
	return $return_status
}

# Check and setup iptables - requires root even for check
iptable_checked=false
setup_iptables(){
	if ! sudo iptables -t mangle -C OUTPUT -m cgroup --cgroup "$net_cls_classid" -j MARK --set-mark "$ip_table_fwmark" 2>/dev/null; then
		echo "Adding iptables MANGLE rule to set firewall mark $ip_table_fwmark on packets with class identifier $net_cls_classid" >&2
		sudo iptables -t mangle -A OUTPUT -m cgroup --cgroup "$net_cls_classid" -j MARK --set-mark "$ip_table_fwmark"
	fi
	if ! sudo iptables -t nat -C POSTROUTING -m cgroup --cgroup "$net_cls_classid" -o "$real_interface" -j MASQUERADE 2>/dev/null; then
		echo "Adding iptables NAT rule force the packets with class identifier $net_cls_classid to exit through $real_interface" >&2
		sudo iptables -t nat -A POSTROUTING -m cgroup --cgroup "$net_cls_classid" -o "$real_interface" -j MASQUERADE
	fi
	iptable_checked=true
}

# Test if config is working, IPv4 only
testresult=true
test_bypass(){
	exit_ip=`cgexec -g net_cls:"$cgroup_name" traceroute -m 1 8.8.8.8 | sed -n '2{p;q}' | grep -oE "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+"`
	if [ "$exit_ip" == "$real_interface_gateway" ]; then
		echo -e "\e[32mTest OK. Trafic exits with IP $exit_ip.\e[0m" >&2
		testresult=true
		return 0
	else
		echo -e "\e[31mTest failed: Trafic exits with $exit_ip instead of $real_interface_gateway. Aborting.\e[0m" >&2
		testresult=false
		return 1
	fi
}


check_iptables=false
if [ "$action" = "command" ] || [ "$action" = "outside" ]; then

	# SETUP config
	if [ "$skip" = false ]; then
		echo "Checking/setting forced routing config (skip with $0 -s ...)" >&2

		if check_package cgroup-lite cgmanager cgroup-tools inetutils-traceroute; then
			echo "You may want to reboot now. But that's probably not necessary." >&2
			exit 1
		fi

		if dpkg --compare-versions `iptables --version | grep -oP "iptables v\K.*$"` "lt" "1.6"; then
			echo -e "\e[31mYou need iptables 1.6.0+. Please install manually. Aborting.\e[0m" >&2
			echo "Find latest iptables at http://www.netfilter.org/projects/iptables/downloads.html" >&2
			echo "Commands to install iptables 1.6.0:" >&2
			echo -e "\e[34msudo apt-get install dh-autoreconf bison flex
cd /tmp
curl http://www.netfilter.org/projects/iptables/files/iptables-1.6.0.tar.bz2 | tar xj
cd iptables-1.6.0
./configure --prefix=/usr      \\
            --sbindir=/sbin    \\
            --disable-nftables \\
            --enable-libipq    \\
            --with-xtlibdir=/lib/xtables \\
&& make  \\
&& sudo make install
iptables --version\e[0m" >&2
			exit 1
		fi		

		if [ ! -d "/sys/fs/cgroup/net_cls/$cgroup_name" ]; then
			echo "Creating net_cls control group $cgroup_name" >&2
			sudo mkdir -p "/sys/fs/cgroup/net_cls/$cgroup_name"
			check_iptables=true
		fi
		if [ `cat "/sys/fs/cgroup/net_cls/$cgroup_name/net_cls.classid" | xargs -n 1 printf "0x%08x"` != "$net_cls_classid" ]; then
			echo "Applying net_cls class identifier $net_cls_classid to cgroup $cgroup_name" >&2
			echo "$net_cls_classid" | sudo tee "/sys/fs/cgroup/net_cls/$cgroup_name/net_cls.classid" > /dev/null
		fi
		if ! grep -E "^${ip_table_number}\s+$ip_table_name" /etc/iproute2/rt_tables &>/dev/null; then
			if grep -E "^${ip_table_number}\s+" /etc/iproute2/rt_tables; then
				echo "ERROR: Table ${ip_table_number} already exists in /etc/iproute2/rt_tables with a different name than $ip_table_name" >&2
				exit 1
			fi
			echo "Creating ip routing table: number=$ip_table_number name=$ip_table_name" >&2
			echo "$ip_table_number $ip_table_name" | sudo tee -a /etc/iproute2/rt_tables > /dev/null
			check_iptables=true
		fi
		if ! ip rule list | grep " lookup $ip_table_name" | grep " fwmark " &>/dev/null; then
			echo "Adding rule to use ip routing table $ip_table_name for packets with firewall mark $ip_table_fwmark" >&2
			sudo ip rule add fwmark "$ip_table_fwmark" table "$ip_table_name"
			check_iptables=true
		fi
		if [ -z "`ip route list table "$ip_table_name" default via $real_interface_gateway dev ${real_interface} 2>/dev/null`" ]; then
			echo "Adding default route in ip routing table $ip_table_name via $real_interface_gateway dev $real_interface" >&2
			sudo ip route add default via "$real_interface_gateway" dev "$real_interface" table "$ip_table_name"
			# Useless?
			echo "Flushing ip route cache" >&2
			sudo ip route flush cache
			check_iptables=true
		fi
		if [ "`cat /proc/sys/net/ipv4/conf/all/rp_filter`" != "0" ] || [ "`cat /proc/sys/net/ipv4/conf/all/rp_filter`" != "2" ]; then
			echo "Unset reverse path filtering for interface \"all\"" >&2
			echo 2 | sudo tee "/proc/sys/net/ipv4/conf/all/rp_filter" > /dev/null
			check_iptables=true
		fi
		if [ "`cat /proc/sys/net/ipv4/conf/${real_interface}/rp_filter`" != "0" ] || [ "`cat /proc/sys/net/ipv4/conf/${real_interface}/rp_filter`" != "2" ]; then
			echo "Unset reverse path filtering for interface \"${real_interface}\"" >&2
			echo 2 | sudo tee "/proc/sys/net/ipv4/conf/${real_interface}/rp_filter" > /dev/null
			check_iptables=true
		fi
		if [ -z "`lscgroup net_cls:$cgroup_name`" ] || [ `stat -c "%U" /sys/fs/cgroup/net_cls/${cgroup_name}/tasks` != "$USER" ]; then
			echo "Creating cgroup net_cls:${cgroup_name}. User $USER will be able to move tasks to it without root permissions." >&2
			sudo cgcreate -t "$USER":"$USER" -a `id -g -n "$USER"`:`id -g -n "$USER"` -g net_cls:"$cgroup_name"
			check_iptables=true
		fi
		if [ "$check_iptables" = true ]; then
			setup_iptables
		fi

	fi

	# TEST bypass
	test_bypass
	if [ "$force" != true ]; then
		if [ "$testresult" = false ]; then
			if [ "$iptable_checked" = false ]; then
				echo -e "Testing iptables..." >&2
				setup_iptables
				test_bypass
			fi
		fi
		if [ "$testresult" = false ]; then
			exit 1
		fi
	fi
fi

# RUN command
if [ "$action" = "command" ]; then
	if [ "$#" -eq 0 ]; then
		echo "Error: COMMAND not provided." >&2
		exit 1
	fi
	if [ "$background" = true ]; then
		cgexec -g net_cls:"$cgroup_name" "[email protected]" &>/dev/null &
		exit 0
	else
		cgexec -g net_cls:"$cgroup_name" "[email protected]"
		exit $?
	fi

# List process OUTSIDE tunnel
# Exit code 0 (true) if at least 1 process is outside the tunnel
elif [ "$action" = "list" ]; then
	echo "List of processes bypassing tunnel:"
	list_outside
	exit $?

# Move process OUTSIDE tunnel
elif [ "$action" = "outside" ]; then
	exit_code=1
	for process in "[email protected]"
	do
	    if [ "$process" -eq "$process" ] 2>/dev/null; then
			# Is integer (PID)
			echo "$process" | sudo tee /sys/fs/cgroup/net_cls/${cgroup_name}/tasks > /dev/null
			exit_code=0
		else
			# Is process name
			pids=$(pidof "$process")
			for pid in $pids
			do
				echo "$pid" | sudo tee /sys/fs/cgroup/net_cls/${cgroup_name}/tasks > /dev/null
				exit_code=0
			done
		fi
	done
	echo -e "\e[31mWARNING: Moving running processes outside the VPN tunnel DOES NOT WORK.\e[0m" >&2
	echo -e "\e[31mYou should start new processes and beware processes that have already opened windows: they may reuse existing PID.\e[0m" >&2
	echo "List of processes bypassing tunnel:"
	list_outside
	exit $exit_code

# Move process INSIDE tunnel
elif [ "$action" = "inside" ]; then
	for process in "[email protected]"
	do
	    if [ "$process" -eq "$process" ] 2>/dev/null; then
			# Is integer (PID)
			echo "$process" | sudo tee /sys/fs/cgroup/net_cls/tasks > /dev/null
		else
			# Is process name
			pids=$(pidof "$process")
			for pid in $pids
			do
				echo "$pid" | sudo tee /sys/fs/cgroup/net_cls/tasks > /dev/null
			done
		fi
	done
	echo "Remaining processes bypassing tunnel:"
	list_outside


# CLEAN the mess
elif [ "$action" = "clean" ]; then
	echo -e "Cleaning forced routing config generated by this script."
	echo -e "Don't bother with errors meaning there's nothing to remove."

	# Remove tasks
	if [ -f "/sys/fs/cgroup/net_cls/${cgroup_name}/tasks" ]; then
		while read task_pid; do echo ${task_pid} | sudo tee /sys/fs/cgroup/net_cls/tasks > /dev/null; done < "/sys/fs/cgroup/net_cls/${cgroup_name}/tasks"
	fi

	# Delete cgroup
	if [ -d "/sys/fs/cgroup/net_cls/${cgroup_name}" ]; then
		sudo find "/sys/fs/cgroup/net_cls/${cgroup_name}" -depth -type d -print -exec rmdir {} \;
	fi

	# This can cause issues if reverse path filtering is normally disabled on the system
	echo 1 | sudo tee "/proc/sys/net/ipv4/conf/all/rp_filter" > /dev/null
	echo 1 | sudo tee "/proc/sys/net/ipv4/conf/${real_interface}/rp_filter" > /dev/null

	sudo iptables -t mangle -D OUTPUT -m cgroup --cgroup "$net_cls_classid" -j MARK --set-mark "$ip_table_fwmark"
	sudo iptables -t nat -D POSTROUTING -m cgroup --cgroup "$net_cls_classid" -o "$real_interface" -j MASQUERADE

	sudo ip rule del fwmark "$ip_table_fwmark" table "$ip_table_name"	
	sudo ip route del default table "$ip_table_name"

	sudo sed -i '/^${ip_table_number}\s\+${ip_table_name}\s*$/d' /etc/iproute2/rt_tables

	if [ -n "`lscgroup net_cls:$cgroup_name`" ]; then
		sudo cgdelete net_cls:"$cgroup_name"
	fi

	echo "All done."

fi

# BONUS: Useful commands:
# ./novpn.sh traceroute www.google.com
# Note: 1 firefox profile = 1 process only
# ./novpn.sh --outside firefox; ./novpn.sh --background firefox https://ipleak.net/
# ip=$(./novpn.sh curl 'https://wtfismyip.com/text' 2>/dev/null); echo "$ip"; whois "$ip" | grep -E "inetnum|route|netname|descr"

#2

This seems to be the source …


#3

See the original post lol


#4

It was only a hint - new version, contribute…