Skip to content

Instantly share code, notes, and snippets.

@kriswebdev
Last active April 9, 2024 06:24
Show Gist options
  • Star 76 You must be signed in to star a gist
  • Fork 19 You must be signed in to fork a gist
  • Save kriswebdev/a8d291936fe4299fb17d3744497b1170 to your computer and use it in GitHub Desktop.
Save kriswebdev/a8d291936fe4299fb17d3744497b1170 to your computer and use it in GitHub Desktop.
novpn: Bypass VPN for specific apps [Linux / OpenVPN]
#!/bin/bash
# === INFO ===
# NoVPN
# Description: Bypass VPN tunnel for applications run through this tool.
VERSION="3.0.0"
# Author: KrisWebDev
# Requirements: Linux with kernel > 2.6.4 (released in 2008).
# This version is tested on Ubuntu 14.04 and 19.10 with bash.
# Main dependencies are automatically installed.
# Script will guide you for iptables upgrade if needed.
# Note: For security, this script will disable IPv6, even after --clean.
# === 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 ===
# Find your real (non-VPN) interface manually with: ip route
# Guess with: ip route | grep "default via " | sed -n '/^.* dev \([-_[:alnum:]]\{1,\}\).*$/s//\1/p' | grep -v tun
real_interface="enp0s3"
# Warn if real interface guessed at startup doesn't match above setting
real_interface_check_warn=true
# === ADVANCED 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
# === CODE ===
real_interface_gateway=`ip route | grep "dev ${real_interface}" | awk '/^default/ { print $3 }'`
# Handle options
action="command"
background=false
skip=false
check_sudo_conf=false
init_nb_args="$#"
cgroup_version=1
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;;
-f|--force) check_sudo_conf=true; shift 1;;
--sudok) sudok=true; shift 1;;
-g|--cgroup) cgroup_name="$2"; shift 1; shift 1;;
-2|--cgv2) cgroup_version=2; shift 1;;
-I|--info) action="info"; shift 1;;
-c|--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 { [ "$#" -lt 1 ] && [ "$action" == "command" ]; } || [ "$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 interface."
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. \e[1mBROKEN!\e[0m"
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 setup system config (never ask for root);\n just perform public routing test and run \e[4mCOMMAND\e[24m."
echo -e "\e[1m--sudok\e[0m Drop sudo rights with \"sudo -K\" before executing \e[4mCOMMAND\e[24m."
echo -e "\e[1m-f, --force\e[0m Force setup of system config (always ask for root)."
echo -e "\e[1m-g, --cgroup \e[4mNAME\e[24m\e[0m Specify cgroup name (cgroup v1) or relative path (cgroup v2). Default: \"$cgroup_name\"."
echo -e "\e[1m-2, --cgv2\e[0m Use Control Groups version 2 if supported."
echo -e "\e[1m-I, --info\e[0m Display debug information and exit."
echo -e "\e[1m-c, --clean\e[0m Move back all proceses to initial routing settings 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 o f process ID or names separated by spaces."
exit 1
fi
ip_table_name="$cgroup_name"
# This program can't ask for root outside terminal
if [ ! -t 1 ] && [ "$(id -u)" -ne 0 ]; then
skip=true
fi
if [ "$skip" = true ]; then
if [ "$action" = "clean" ]; then
echo -e "\e[31mCan't use --skip with --clean. Aborting.\e[0m" >&2
exit 1
fi
if [ "$check_sudo_conf" = true ]; then
echo -e "\e[31mCan't use --skip with --force. Aborting.\e[0m" >&2
exit 1
fi
fi
# Interface check
if [ "$real_interface_check_warn" = true ]; then
real_interface_guess="$(ip route | grep "default via " | sed -n '/^.* dev \([-_[:alnum:]]\{1,\}\).*$/s//\1/p' | grep -v tun)"
if [ ! -z "$real_interface_guess" ] && [ "$real_interface" != "$real_interface_guess" ]; then
echo -e "\e[93mWarning: Guessed real interface is \"$real_interface_guess\" but this script is configured to use \"$real_interface\"."
echo -e "Manually edit the script settings if needed.\e[0m" >&2
fi
fi
# Find/Check cgroup system folders
cgroup_v1_net_cls="/sys/fs/cgroup/net_cls"
cgroup_v2_root="/sys/fs/cgroup"
cgroup_base="/sys/fs/cgroup"
find_root(){
# Find cgroup v2 root filesystem
if [ "$cgroup_version" = 2 ]; then
[ -f "$cgroup_v2_root""/cgroup.procs" ] || cgroup_v2_root="/sys/fs/cgroup/unified"
[ -f "$cgroup_v2_root""/cgroup.procs" ] || cgroup_v2_root="$(mount -t cgroup2 | head -n1 | grep -oP '^cgroup2 on \K\S+')"
if [ ! -f "$cgroup_v2_root""/cgroup.procs" ]; then
echo -e "\e[31mCan't find a valid cgroup v2 mounted filesystem. Aborting.\e[0m" >&2
exit 1
fi
echo "Using cgroups v2. Found valid cgroup v2 filesystem at $cgroup_v2_root"
cgroup_base="$cgroup_v2_root"
else
# Find cgroup v1 net_cls folder
[ -f "$cgroup_v1_net_cls""/cgroup.procs" ] || cgroup_v1_net_cls="$(mount -t cgroup | grep net_cls | head -n1 | grep -oP '^cgroup on \K\S+')"
if [ ! -f "$cgroup_v1_net_cls""/cgroup.procs" ]; then
echo -e "\e[31mCan't find a valid cgroup v1 net_cls folder. Aborting.\e[0m" >&2
exit 1
fi
echo "Using cgroups v1. Found valid cgroup v1 net_cls folder at $cgroup_v1_net_cls"
cgroup_base="$cgroup_v1_net_cls"
fi
}
# Prepare variables
if [ "$cgroup_version" = 2 ]; then
iptables_arg="--path $cgroup_name"
else
iptables_arg="--cgroup $net_cls_classid"
fi
# Helper functions
# Check the presence of required system packages
check_install_package(){
nothing_installed=1
for package_name in "$@"
do
if ! dpkg -s "$package_name" &> /dev/null; then
if [ "$package_name" = "cgroup-tools" ]; then
if [ -x "$(command -v cgexec)" ]; then
continue
fi
if [ "$(apt-cache search --names-only '^cgroup-tools$' | wc -l)" -eq 0 ]; then
echo "cgroups-tools is not available in apt cache, falling back to cgroup-bin install"
package_name="cgroup-bin"
fi
fi
echo "Installing $package_name"
read -p "Press enter to continue or Ctrl+C to cancel"
sudo apt-get install "$package_name"
nothing_installed=0
fi
done
return $nothing_installed
}
check_package(){
for package_name in "$@"
do
if ! dpkg -s "$package_name" &> /dev/null; then
if [ "$package_name" = "cgroup-tools" ]; then
# Ignore if cgexec is available (e.g. through cgroup-bin)
if [ ! -x "$(command -v cgexec)" ]; then
true
return
fi
else
true
return
fi
fi
done
false
}
# Main functions
# Check/Install dependencies
install_dependencies(){
if check_install_package cgroup-lite traceroute; then # Removed cgroup-tools
if check_package cgroup-lite traceroute; then # Removed cgroup-tools
echo "Required packages not properly installed. Aborting." >&2
exit 1
fi
fi
iptables_version=$(iptables --version | grep -oP "iptables v\K[0-9.]+")
if dpkg --compare-versions "$iptables_version" "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.8.4:" >&2
echo -e "\e[34msudo apt-get install dh-autoreconf bison flex
cd /tmp
curl https://netfilter.org/projects/iptables/files/iptables-1.8.4.tar.bz2 | tar xj
cd iptables-1.8.4
./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
}
# Check and setup iptables - requires root even for check
iptable_checked=false
setup_config(){
#if [ "$cgroup_version" = 1 ]; then
if [ ! -d "$cgroup_base/$cgroup_name" ]; then
echo "Creating control group at \"$cgroup_base/$cgroup_name\"" >&2
sudo mkdir -p "$cgroup_base/$cgroup_name"
sudo chown -R "$USER":"`id -g -n "$USER"`" "$cgroup_base/$cgroup_name"
check_sudo_conf=true
fi
cgroup_owner=`stat -c "%U" "$cgroup_base/$cgroup_name/cgroup.procs"`
if [ "$cgroup_owner" != "`id -g -n "$USER"`" ] && [ "$EUID" -ne 0 ]; then
echo -e "\e[93mWARNING: Folder \"$cgroup_base/$cgroup_name/cgroup.procs\" already exists, it is owned by someone else ($cgroup_owner) and you are not root.\e[0m" >&2
fi
# Redundant
#if [ -z "`lscgroup net_cls:$cgroup_name`" ] || [ `stat -c "%U" "$cgroup_v1_net_cls/${cgroup_name}/tasks"` != "$USER" ]; then
# echo "Creating cgroup net_cls:${cgroup_name}. User $USER will be able to move tasks in 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_sudo_conf=true
#fi
if [ "$cgroup_version" = 1 ] && [ `cat "$cgroup_v1_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 "$cgroup_v1_net_cls/$cgroup_name/net_cls.classid" > /dev/null
check_sudo_conf=true
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 -e "\e[31mERROR: Table ${ip_table_number} already exists in /etc/iproute2/rt_tables with a different name than $ip_table_name.\e[0m" >&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_sudo_conf=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_sudo_conf=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_sudo_conf=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_sudo_conf=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_sudo_conf=true
fi
# Only check iptables configuration if another configuration item was missing or if the test fails, as it requires root rights
if [ "$check_sudo_conf" = true ]; then
if ! sudo iptables -t mangle -C OUTPUT -m cgroup $iptables_arg -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 $iptables_arg -j MARK --set-mark "$ip_table_fwmark"
fi
if ! sudo iptables -t nat -C POSTROUTING -m cgroup $iptables_arg -o "$real_interface" -j MASQUERADE 2>/dev/null; then
echo "Adding iptables NAT rule to force the packets with class identifier $net_cls_classid to exit through $real_interface" >&2
sudo iptables -t nat -A POSTROUTING -m cgroup $iptables_arg -o "$real_interface" -j MASQUERADE
fi
iptable_checked=true
fi
if [ "`cat /proc/sys/net/ipv6/conf/all/disable_ipv6`" != "1" ] || [ "`cat /proc/sys/net/ipv6/conf/""$real_interface""/disable_ipv6`" != "1" ] || [ "$check_sudo_conf" = true ]; then
echo "Disabling IPv6 (not supported/implemented)"
sudo ip -6 route add blackhole default metric 1
echo 1 | sudo tee "/proc/sys/net/ipv6/conf/all/disable_ipv6" > /dev/null
fi
}
fcgexec() {
"$@"
}
# Test if config is working, IPv4 only
test_routing(){
#exit_ip="$(fcgexec -g net_cls:"$cgroup_name" traceroute -n -m 1 8.8.8.8 | sed -n '2{p;q}' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1)"
exit_ip="$(fcgexec traceroute -n -m 1 8.8.8.8 | sed -n '2{p;q}' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1)"
if [ -z "$exit_ip" ]; then
# Old traceroute
#exit_ip="$(fcgexec -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]+' | head -1)"
exit_ip="$(fcgexec traceroute -m 1 8.8.8.8 | sed -n '2{p;q}' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1)"
if [ -z "$exit_ip" ]; then
echo -e "\e[31mTest failed: Unable to determine source exit IP (found \"$exit_ip\").\e[0m" >&2
if [ "$skip" = true ]; then
echo -e "\e[31mYou should remove --skip option to perform setup.\e[0m" >&2
fi
return 0
fi
fi
if [ -z "$real_interface_gateway" ]; then
echo -e "\e[31mTest failed: Unable to determine real interface gateway IP (found \"$real_interface_gateway\").\e[0m" >&2
return 0
fi
if [ "$exit_ip" == "$real_interface_gateway" ]; then
echo -e "\e[32mTest OK. Trafic exits with IP \"$exit_ip\".\e[0m" >&2
return 0
else
echo -e "\e[31mTest failed: Trafic exits with \"$exit_ip\" instead of \"$real_interface_gateway\".\e[0m" >&2
return 1
fi
}
# Reconfigure routing
reroute(){
if [ -z "$real_interface_gateway" ]; then
echo -e "\e[31mCan't find default gateway of real interface \"${real_interface}\". Is it up?\e[0m" >&2
echo -e "\e[31mAborting.\e[0m" >&2
exit 1
fi
if [ "$skip" = false ]; then
setup_config
fi
# MOVE ourself, as program caller, to cgroup
echo $$ | sudo tee "$cgroup_base/${cgroup_name}/cgroup.procs" > /dev/null
# TEST
test_routing
testresult=$?
if [ "$skip" = false ]; then
if [ "$testresult" = false ]; then
if [ "$iptable_checked" = false ] && [ "$skip" = false ]; then
echo -e "Trying to setup iptables and redo test..." >&2
check_sudo_conf=true
setup_config
test_routing
testresult=$?
fi
fi
fi
if [ "$testresult" -ne 0 ]; then
echo -e "\e[31mAborting.\e[0m" >&2
exit 1
fi
}
# List processes bypassing the VPN
list_outside(){
return_status=1
echo -e "PID""\t""CMD"
cat "$cgroup_base/${cgroup_name}/cgroup.procs" | \
while read task_pid
do
echo -e "${task_pid}""\t""`ps -p ${task_pid} -o comm=`";
return_status=0
done
return $return_status
}
# Check/Install DEPENDENCIES
if [ "$action" = "command" ] || [ "$action" = "outside" ]; then
if [ "$skip" = false ]; then
echo "Checking/Installing dependencies" >&2
install_dependencies
fi
fi
# Find cgroup filesystem
find_root
# SETUP novpn routing
if [ "$action" = "command" ] || [ "$action" = "outside" ]; then
echo "Checking/Setting up system configuration" >&2
reroute
fi
# RUN command
if [ "$action" = "command" ]; then
if [ "$sudok" = true ]; then
sudo -K
fi
if [ "$#" -eq 0 ]; then
echo "Error: COMMAND not provided." >&2
exit 1
fi
if [ "$background" = true ]; then
#cgexec -g net_cls:"$cgroup_name" --sticky "$@" &>/dev/null &
fcgexec "$@" &>/dev/null &
exit 0
else
#cgexec -g net_cls:"$cgroup_name" --sticky "$@"
fcgexec "$@"
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 "$cgroup_base/${cgroup_name}/cgroup.procs" > /dev/null
exit_code=0
else
# Is process name
pids=$(pidof "$process")
for pid in $pids
do
echo "$pid" | sudo tee "$cgroup_base/${cgroup_name}/cgroup.procs" > /dev/null
exit_code=0
done
fi
done
echo -e "\e[93mWARNING: Moving running processes outside the VPN tunnel DOES NOT WORK.\e[0m" >&2
echo -e "\e[93mYou 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
reroute
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 "$cgroup_base/cgroup.procs" > /dev/null
else
# Is process name
pids=$(pidof "$process")
for pid in $pids
do
echo "$pid" | sudo tee "$cgroup_base/cgroup.procs" > /dev/null
done
fi
done
echo "Remaining processes bypassing tunnel:"
list_outside
# INFO
elif [ "$action" = "info" ]; then
echo
echo "Displaying information for cgroup v$cgroup_version"
echo
echo -e "\e[2mcat /etc/iproute2/rt_tables | grep --color \"^${ip_table_number}\s\|\\$\"\e[0m"
cat /etc/iproute2/rt_tables | grep --color "^${ip_table_number}\s\|\$"
echo
echo -e "\e[2mip route show table all | grep -v 'table local' | grep --color 'table\|$'\e[0m"
ip route show table all | grep -v 'table local' | grep --color 'table\|$'
echo
echo -e "\e[2mip rule list | grep --color 'fwmark\|$'\e[0m"
ip rule list | grep --color 'fwmark\|$'
echo
echo -e "\e[2msudo iptables -t mangle -L -v --line-numbers | grep --color 'cgroup\|$'\e[0m"
sudo iptables -t mangle -L -v --line-numbers | grep --color 'cgroup\|$'
echo
echo -e "\e[2msudo iptables -t nat -L -v --line-numbers | grep --color 'cgroup\|$'\e[0m"
sudo iptables -t nat -L -v --line-numbers | grep --color 'cgroup\|$'
echo
echo -e "\e[2mls -l ${cgroup_base}/ | grep --color \"${cgroup_name}\|\\$\"\e[0m"
ls -l ${cgroup_base}/ | grep --color "${cgroup_name}\|\$"
echo
echo -e "\e[2mls -l ${cgroup_base}/${cgroup_name}/ | grep --color \"${USER}\|\\$\"\e[0m"
ls -l ${cgroup_base}/${cgroup_name}/ | grep --color "${USER}\|\$"
echo
echo -e "End of debug information."
# CLEAN the mess
elif [ "$action" = "clean" ]; then
echo -e "Cleaning forced routing config generated by this script for cgroup version $cgroup_version."
echo -e "Note: Cleaning partially cleans/breaks the custom configuration for the other cgroup version."
echo -e " You should also clean the other cgroup version using the -2 (v2) or no (v1) flag."
echo -e "Note: If you used a custom cgroup name through --group, you must use it with --clean."
echo -e "Note: Use --info and --info -2 afterwards to check."
echo -e "Don't bother with errors meaning there's nothing to remove."
# Remove tasks
if [ -f "$cgroup_base/${cgroup_name}/cgroup.procs" ]; then
cat "$cgroup_base/${cgroup_name}/cgroup.procs" | \
while read task_pid
do
echo ${task_pid} | sudo tee "$cgroup_base/cgroup.procs" > /dev/null
done
fi
# Delete cgroup
if [ -d "$cgroup_base/${cgroup_name}" ]; then
echo -n "Are you sure you want to delete cgroup $cgroup_base/${cgroup_name} ? [y/n] "
old_stty_cfg=$(stty -g)
stty raw -echo ; answer=$(head -c 1) ; stty $old_stty_cfg
echo
if echo "$answer" | grep -iq "^y" ;then
sudo find "$cgroup_base/${cgroup_name}" -depth -type d -print -exec rmdir {} \;
fi
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 $iptables_arg -j MARK --set-mark "$ip_table_fwmark"
sudo iptables -t nat -D POSTROUTING -m cgroup $iptables_arg -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/d" /etc/iproute2/rt_tables
echo "Disabling IPv6 (still - too risky if not managed by VPN)"
# sudo ip -6 route del blackhole default metric 1
echo 1 | sudo tee "/proc/sys/net/ipv6/conf/all/disable_ipv6" > /dev/null
# Redundant
#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
# killall 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
Copy link

level323 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
Copy link

level323 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).

@guyl6308
Copy link

Thank you so much! This script works beautifully!

@TechieBoy
Copy link

After hours of searching on the internet, finally found something which actually works. Thanks a lot!!

@darx0r
Copy link

darx0r commented Mar 19, 2021

Can't find a valid cgroup v1 net_cls folder. Aborting.
On some linux kernel compilations, net_cls is not statically compiled with the kernel.
It can be determined through the kernel config file:
CONFIG_NET_CLS_CGROUP=m // meaning it was compiled as a separate kernel module we should load
(if =n, you will have to recompile the kernel with =(m|y))

So before using novpn.sh, it is recommended to run:
modprobe cls_cgroup

@EulerNot0
Copy link

I just wanted to add a note for anyone having problems with missing net_cls folder on Ubuntu 20. I created the missing folder: /sys/fs/cgroup/net_cls then launched with novpn.sh --cgv2 to resolve the issue.

@pzim-devdata
Copy link

pzim-devdata commented Dec 10, 2021

For Debian Bullseye :

  • Copy this code in a document called "bypass.sh"
  • Give it executable permissions by running chmod +x bypass.sh in a terminal
  • Inside bypass.sh :
    Replace :
    real_interface="enp0s3"
    by :
    real_interface="THE_INTERFACE_YOU_CHOOSE in sudo ifconfig"
    Replace :
    traceroute -n -m
    by :
    traceroute -m
    Replace :
    iptables_version=$(iptables --version
    by :
    iptables_version=$(sudo iptables --version
  • Install the deb package cgroup-lite_1.15_all.deb --> https://launchpad.net/ubuntu/jammy/amd64/cgroup-lite/1.15
  • Turn off the VPN
  • Run in your terminal :
    sudo mkdir /sys/fs/cgroup/net_cls
    sudo modprobe cls_cgroup
    ./bypass.sh PATH_TO_YOUR_APP
  • Turn on the VPN
  • Run your app and enjoy :-)

@EulerNot0
Copy link

EulerNot0 commented May 6, 2022

Just another note if you are using Ubuntu. Launching a snap installed app (like Firefox) ignores the cgroup from this script and continues to use the VPN. This script only works if you install non snap apps.

@sedme0
Copy link

sedme0 commented May 26, 2022

I couldn't get this script working on its own, even with tweaks on Debian 11. I did however get it to work manually using the instructions from the answer to this question on serverfault by the author of this script. I use Nord VPN, with their custom interface thing, so I had to run nordvpn set firewall disable before it would work, but now nordvpn's port bypass setting doesn't seem to work. This is a problem, because it basically invalidates the whole reason I set this up in the first place. My intended setup is this: I have port 22 forwarded so I can connect to my desktop through the internet. I whitelisted port 22 in Nord VPN so that if I leave the VPN connected when I leave the house, I can still connect. I set up a cron job that retrieves the external IP address of my home network once an hour (using the novpn control group), and uploads that to a text file on a cloud storage service, in case I get a new dynamic IP. I can't seem to SSH in through the internet anymore while connected to a VPN after setting up this control group, even though the rest of my settings are the same. Could it be interfering with nordvpn's configuration somehow?

@falshunov
Copy link

falshunov commented Jun 23, 2022

For Debian Bullseye :

  • Copy this code in a document called "bypass.sh"
  • Give it executable permissions by running chmod +x bypass.sh in a terminal
  • Inside bypass.sh :
    Replace :
    real_interface="enp0s3"
    by :
    real_interface="THE_INTERFACE_YOU_CHOOSE in sudo ifconfig"
    Replace :
    traceroute -n -m
    by :
    traceroute -m
    Replace :
    iptables_version=$(iptables --version
    by :
    iptables_version=$(sudo iptables --version
  • Install the deb package cgroup-lite_1.15_all.deb --> https://launchpad.net/ubuntu/jammy/amd64/cgroup-lite/1.15
  • Turn off the VPN
  • Run in your terminal :
    sudo mkdir /sys/fs/cgroup/net_cls
    sudo modprobe cls_cgroup
    ./bypass.sh PATH_TO_YOUR_APP
  • Turn on the VPN
  • Run your app and enjoy :-)

For latest Debian Sid:

If you see error
cat: /sys/fs/cgroup/net_cls/novpn/net_cls.classid: No such file or directory
Applying net_cls class identifier 0x00110011 to cgroup novpn
tee: /sys/fs/cgroup/net_cls/novpn/net_cls.classid: Permission denied

then add to head of file after
sudo mkdir /sys/fs/cgroup/net_cls
sudo modprobe cls_cgroup
sudo mount -t cgroup -onet_cls net_cls /sys/fs/cgroup/net_cls

@dawidd6
Copy link

dawidd6 commented Sep 3, 2022

@falshunov thanks, works for Ubuntu 22.04 as well.

@piousdeer
Copy link

Any ideas on how to make this work with Flatpak apps?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment