Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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"
@level323

This comment has been minimized.

Copy link

commented Aug 3, 2016

Excellent script. Thankyou for sharing it. I'm building something based on your fine work to scratch a more generic itch. I plan to post it when ready.

In the meantime, I noticed that $force (line #226) is an unbound variable in your script. Further to that, I'm not really sure what that whole block (lines 224-237) are actually achieving (it seems to assumes that the environment is setup except for the iptables rules - but how would that situation ever occur?).

@level323

This comment has been minimized.

Copy link

commented Aug 3, 2016

As promised, here is my version so far. I've called it altnetworking https://gist.github.com/level323/54a921216f0baaa163127d960bfebbf0
It works on my system (Debian 8 w/iptables from unstable repo).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.