morristech
3/8/2019 - 10:28 PM

Bypass VPN for specific apps [Linux / OpenVPN]

Bypass VPN for specific apps [Linux / OpenVPN]

#!/bin/bash

# === INFO ===
# NoVPN
# Description: Bypass VPN tunnel for applications run through this tool.
VERSION="1.0.2"
# Author: KrisWebDev
# Requirements:  Linux with kernel > 2.6.4 (released in 2008).
#                Only tested on Ubuntu 15 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 "$@"
	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 -n 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" "$@" &>/dev/null &
		exit 0
	else
		cgexec -g net_cls:"$cgroup_name" "$@"
		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 "$@"
	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 "$@"
	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"