Skip to content

Instantly share code, notes, and snippets.

@pymen
Forked from kriswebdev/novpn.sh
Created May 4, 2025 13:59
Show Gist options
  • Select an option

  • Save pymen/2502af56eb89b539afc5c4fdb153c57f to your computer and use it in GitHub Desktop.

Select an option

Save pymen/2502af56eb89b539afc5c4fdb153c57f to your computer and use it in GitHub Desktop.

Revisions

  1. @kriswebdev kriswebdev revised this gist Jan 2, 2022. No changes.
  2. @kriswebdev kriswebdev revised this gist Jan 6, 2020. 1 changed file with 288 additions and 152 deletions.
    440 changes: 288 additions & 152 deletions novpn.sh
    Original file line number Diff line number Diff line change
    @@ -3,13 +3,13 @@
    # === INFO ===
    # NoVPN
    # Description: Bypass VPN tunnel for applications run through this tool.
    VERSION="2.0.0"
    VERSION="3.0.0"
    # Author: KrisWebDev
    # Requirements: Linux with kernel > 2.6.4 (released in 2008).
    # This version is only tested on Ubuntu 19.10 with bash.
    # This version is tested on Ubuntu 14.04 and 19.10 with bash.
    # Main dependencies are automatically installed.
    # Script will guide you for iptables 1.6.0 install if needed.
    # Note: This script will disable IPv6 (enable with --clean)
    # 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
    @@ -26,25 +26,28 @@ VERSION="2.0.0"
    # along with this program. If not, see <http://www.gnu.org/licenses/>.

    # === CONFIGURATION ===
    # Find your real interface with: route | grep '^default' | grep -o '[^ ]*$' | grep -v tun
    real_interface="enp34s0"
    # 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
    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
    check_sudo_conf=false
    init_nb_args="$#"
    cgroup_version=1

    while [ "$#" -gt 0 ]; do
    case "$1" in
    @@ -53,17 +56,20 @@ while [ "$#" -gt 0 ]; do
    -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;;
    --sudok) sudok=true; shift 1;;
    -*) 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
    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"
    @@ -74,15 +80,22 @@ if [ "$init_nb_args" -lt 1 ] || [ "$action" = "help" ] ; then
    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 & don't ask for root;\n just perform public routing test and run \e[4mCOMMAND\e[24m."
    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 of process ID or names separated by spaces."
    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
    @@ -93,18 +106,74 @@ if [ "$skip" = true ]; 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

    # Helper functions
    # 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
    @@ -116,198 +185,225 @@ check_package(){
    for package_name in "$@"
    do
    if ! dpkg -s "$package_name" &> /dev/null; then
    #echo "Installing $package_name"
    #sudo apt-get install "$package_name"
    return 0
    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
    return 1
    false
    }

    # 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
    # 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_iptables(){
    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 "ERROR: Table ${ip_table_number} already exists in /etc/iproute2/rt_tables with a different name than $ip_table_name" >&2
    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_iptables=true
    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_iptables=true
    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_iptables=true
    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_iptables=true
    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_iptables=true
    fi
    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"
    check_sudo_conf=true
    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 to 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
    # 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
    testresult=true
    test_routing(){
    exit_ip="$(cgexec -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)"
    if [ -z "$exit_ip" ]; then
    #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="$(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]+' | head -1)"
    if [ -z "$exit_ip" ]; then
    #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
    testresult=false
    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
    testresult=false
    return 0
    return 0
    fi
    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
    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
    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[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
    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 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_iptables=true
    fi
    if [ "$check_iptables" = true ]; then
    setup_iptables
    fi

    echo "Disabling IPv6 (not supported/implemented)"
    sudo ip -6 route add blackhole default metric 1
    echo 1 | sudo tee "/proc/sys/net/ipv6/conf/lo/disable_ipv6" > /dev/null
    echo 1 | sudo tee "/proc/sys/net/ipv6/conf/all/disable_ipv6" > /dev/null
    echo 1 | sudo tee "/proc/sys/net/ipv6/conf/default/disable_ipv6" > /dev/null
    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
    setup_iptables
    check_sudo_conf=true
    setup_config
    test_routing
    testresult=$?
    fi
    fi
    if [ "$testresult" = false ]; then
    echo -e "\e[31mAborting.\e[0m" >&2
    exit 1
    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_iptables=false
    # Check/Install DEPENDENCIES
    if [ "$action" = "command" ] || [ "$action" = "outside" ]; then
    if [ "$skip" = false ]; then
    echo "Checking/Installing dependencies" >&2
    install_dependencies
    fi
    fi

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

    if check_install_package cgroup-lite traceroute cgroup-tools; then
    if check_package cgroup-lite traceroute cgroup-tools; then
    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.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
    fi
    # Find cgroup filesystem
    find_root

    if [ "$action" = "command" ]; then
    reroute
    fi
    # SETUP novpn routing
    if [ "$action" = "command" ] || [ "$action" = "outside" ]; then
    echo "Checking/Setting up system configuration" >&2
    reroute
    fi

    # RUN command
    @@ -320,10 +416,12 @@ if [ "$action" = "command" ]; then
    exit 1
    fi
    if [ "$background" = true ]; then
    cgexec -g net_cls:"$cgroup_name" --sticky "$@" &>/dev/null &
    #cgexec -g net_cls:"$cgroup_name" --sticky "$@" &>/dev/null &
    fcgexec "$@" &>/dev/null &
    exit 0
    else
    cgexec -g net_cls:"$cgroup_name" --sticky "$@"
    #cgexec -g net_cls:"$cgroup_name" --sticky "$@"
    fcgexec "$@"
    exit $?
    fi

    @@ -341,20 +439,20 @@ elif [ "$action" = "outside" ]; then
    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
    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 /sys/fs/cgroup/net_cls/${cgroup_name}/tasks > /dev/null
    echo "$pid" | sudo tee "$cgroup_base/${cgroup_name}/cgroup.procs" > /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 -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

    @@ -368,13 +466,13 @@ elif [ "$action" = "inside" ]; then
    do
    if [ "$process" -eq "$process" ] 2>/dev/null; then
    # Is integer (PID)
    echo "$process" | sudo tee /sys/fs/cgroup/net_cls/tasks > /dev/null
    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 /sys/fs/cgroup/net_cls/tasks > /dev/null
    echo "$pid" | sudo tee "$cgroup_base/cgroup.procs" > /dev/null
    done
    fi
    done
    @@ -384,44 +482,82 @@ elif [ "$action" = "inside" ]; then

    # INFO
    elif [ "$action" = "info" ]; then
    echo -e "\e[2msudo iptables -L -v --line-numbers\e[0m"
    sudo iptables -L -v --line-numbers
    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."
    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 "/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"
    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 "/sys/fs/cgroup/net_cls/${cgroup_name}" ]; then
    sudo find "/sys/fs/cgroup/net_cls/${cgroup_name}" -depth -type d -print -exec rmdir {} \;
    fi
    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 --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 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 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

    echo 1 | sudo tee "/proc/sys/net/ipv6/conf/lo/disable_ipv6" > /dev/null
    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
    echo 1 | sudo tee "/proc/sys/net/ipv6/conf/default/disable_ipv6" > /dev/null

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

    echo "All done."

    @@ -431,4 +567,4 @@ fi
    # ./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"
    # ip=$(./novpn.sh curl 'https://wtfismyip.com/text' 2>/dev/null); echo "$ip"; whois "$ip" | grep -E "inetnum|route|netname|descr"
  3. @kriswebdev kriswebdev revised this gist Dec 22, 2019. 1 changed file with 170 additions and 81 deletions.
    251 changes: 170 additions & 81 deletions novpn.sh
    Original file line number Diff line number Diff line change
    @@ -3,12 +3,13 @@
    # === INFO ===
    # NoVPN
    # Description: Bypass VPN tunnel for applications run through this tool.
    VERSION="1.0.2"
    VERSION="2.0.0"
    # Author: KrisWebDev
    # Requirements: Linux with kernel > 2.6.4 (released in 2008).
    # Only tested on Ubuntu 15 with bash.
    # This version is only tested on Ubuntu 19.10 with bash.
    # Main dependencies are automatically installed.
    # Script will guide you for iptables 1.6.0 install.
    # Script will guide you for iptables 1.6.0 install if needed.
    # Note: This script will disable IPv6 (enable with --clean)

    # === LICENSE ===
    # This program is free software: you can redistribute it and/or modify
    @@ -25,9 +26,10 @@ VERSION="1.0.2"
    # along with this program. If not, see <http://www.gnu.org/licenses/>.

    # === CONFIGURATION ===
    real_interface="eth0"
    # Find your real interface with: route | grep '^default' | grep -o '[^ ]*$' | grep -v tun
    real_interface="enp34s0"

    # === ADVANCES CONFIGURATION ===
    # === 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
    @@ -51,9 +53,11 @@ while [ "$#" -gt 0 ]; do
    -i|--inside) action="inside"; shift 1;;
    -l|--list) action="list"; shift 1;;
    -s|--skip) skip=true; shift 1;;
    -l|--clean) action="clean"; 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;;
    --sudok) sudok=true; shift 1;;
    -*) echo "Unknown option: $1. Try --help." >&2; exit 1;;
    *) break;; # Start of COMMAND or LIST
    esac
    @@ -63,31 +67,43 @@ 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 -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. (BROKEN)"
    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 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-s, --skip\e[0m Don't setup system config & don't ask for root;\n just perform public routing test and run \e[4mCOMMAND\e[24m."
    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 of process ID or names separated by spaces."
    exit 1
    fi

    # 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
    fi


    # Helper functions

    # Check the presence of required system packages
    check_package(){
    check_install_package(){
    nothing_installed=1
    for package_name in "$@"
    do
    if ! dpkg -l "$package_name" &> /dev/null; then
    if ! dpkg -s "$package_name" &> /dev/null; then
    echo "Installing $package_name"
    sudo apt-get install "$package_name"
    nothing_installed=0
    @@ -96,6 +112,18 @@ check_package(){
    return $nothing_installed
    }

    check_package(){
    for package_name in "$@"
    do
    if ! dpkg -s "$package_name" &> /dev/null; then
    #echo "Installing $package_name"
    #sudo apt-get install "$package_name"
    return 0
    fi
    done
    return 1
    }

    # List processes bypassing the VPN
    list_outside(){
    return_status=1
    @@ -111,46 +139,143 @@ list_outside(){
    # Check and setup iptables - requires root even for check
    iptable_checked=false
    setup_iptables(){
    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 ! 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
    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 --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]+"`
    test_routing(){
    exit_ip="$(cgexec -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)"
    if [ -z "$exit_ip" ]; then
    # Old traceroute
    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]+' | 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
    testresult=false
    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
    testresult=false
    return 0
    fi
    if [ "$exit_ip" == "$real_interface_gateway" ]; then
    echo -e "\e[32mTest OK. Trafic exits with IP $exit_ip.\e[0m" >&2
    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
    echo -e "\e[31mTest failed: Trafic exits with \"$exit_ip\" instead of \"$real_interface_gateway\". Aborting.\e[0m" >&2
    testresult=false
    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
    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 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_iptables=true
    fi
    if [ "$check_iptables" = true ]; then
    setup_iptables
    fi

    echo "Disabling IPv6 (not supported/implemented)"
    sudo ip -6 route add blackhole default metric 1
    echo 1 | sudo tee "/proc/sys/net/ipv6/conf/lo/disable_ipv6" > /dev/null
    echo 1 | sudo tee "/proc/sys/net/ipv6/conf/all/disable_ipv6" > /dev/null
    echo 1 | sudo tee "/proc/sys/net/ipv6/conf/default/disable_ipv6" > /dev/null
    fi

    # TEST
    test_routing
    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
    setup_iptables
    test_routing
    fi
    fi
    if [ "$testresult" = false ]; then
    echo -e "\e[31mAborting.\e[0m" >&2
    exit 1
    fi

    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
    if check_install_package cgroup-lite traceroute cgroup-tools; then
    if check_package cgroup-lite traceroute cgroup-tools; then
    echo "Required packages not properly installed. Aborting." >&2
    exit 1
    fi
    fi

    if dpkg --compare-versions `iptables --version | grep -oP "iptables v\K.*$"` "lt" "1.6"; then
    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.6.0:" >&2
    @@ -167,7 +292,7 @@ cd iptables-1.6.0
    && sudo make install
    iptables --version\e[0m" >&2
    exit 1
    fi
    fi

    if [ ! -d "/sys/fs/cgroup/net_cls/$cgroup_name" ]; then
    echo "Creating net_cls control group $cgroup_name" >&2
    @@ -178,76 +303,27 @@ iptables --version\e[0m" >&2
    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
    if [ "$action" = "command" ]; then
    reroute
    fi
    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" "$@" &>/dev/null &
    cgexec -g net_cls:"$cgroup_name" --sticky "$@" &>/dev/null &
    exit 0
    else
    cgexec -g net_cls:"$cgroup_name" "$@"
    cgexec -g net_cls:"$cgroup_name" --sticky "$@"
    exit $?
    fi

    @@ -281,6 +357,9 @@ elif [ "$action" = "outside" ]; then
    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

    reroute

    exit $exit_code

    # Move process INSIDE tunnel
    @@ -303,6 +382,12 @@ elif [ "$action" = "inside" ]; then
    list_outside


    # INFO
    elif [ "$action" = "info" ]; then
    echo -e "\e[2msudo iptables -L -v --line-numbers\e[0m"
    sudo iptables -L -v --line-numbers


    # CLEAN the mess
    elif [ "$action" = "clean" ]; then
    echo -e "Cleaning forced routing config generated by this script."
    @@ -329,6 +414,10 @@ elif [ "$action" = "clean" ]; then
    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

    echo 1 | sudo tee "/proc/sys/net/ipv6/conf/lo/disable_ipv6" > /dev/null
    echo 1 | sudo tee "/proc/sys/net/ipv6/conf/all/disable_ipv6" > /dev/null
    echo 1 | sudo tee "/proc/sys/net/ipv6/conf/default/disable_ipv6" > /dev/null

    if [ -n "`lscgroup net_cls:$cgroup_name`" ]; then
    sudo cgdelete net_cls:"$cgroup_name"
    @@ -341,5 +430,5 @@ 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"
    # 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"
  4. @kriswebdev kriswebdev revised this gist Jun 18, 2016. 1 changed file with 4 additions and 2 deletions.
    6 changes: 4 additions & 2 deletions novpn.sh
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@
    # === INFO ===
    # NoVPN
    # Description: Bypass VPN tunnel for applications run through this tool.
    VERSION="1.0.1"
    VERSION="1.0.2"
    # Author: KrisWebDev
    # Requirements: Linux with kernel > 2.6.4 (released in 2008).
    # Only tested on Ubuntu 15 with bash.
    @@ -25,7 +25,9 @@ VERSION="1.0.1"
    # along with this program. If not, see <http://www.gnu.org/licenses/>.

    # === CONFIGURATION ===
    real_interface="enp7s0"
    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
  5. @kriswebdev kriswebdev revised this gist Jun 18, 2016. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions novpn.sh
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@
    # === INFO ===
    # NoVPN
    # Description: Bypass VPN tunnel for applications run through this tool.
    VERSION="1.0.0"
    VERSION="1.0.1"
    # Author: KrisWebDev
    # Requirements: Linux with kernel > 2.6.4 (released in 2008).
    # Only tested on Ubuntu 15 with bash.
    @@ -143,7 +143,7 @@ if [ "$action" = "command" ] || [ "$action" = "outside" ]; then
    if [ "$skip" = false ]; then
    echo "Checking/setting forced routing config (skip with $0 -s ...)" >&2

    if check_package cgroup-lite cgmanager cgroup-tools; then
    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
  6. @kriswebdev kriswebdev created this gist Jun 18, 2016.
    343 changes: 343 additions & 0 deletions novpn.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,343 @@
    #!/bin/bash

    # === INFO ===
    # NoVPN
    # Description: Bypass VPN tunnel for applications run through this tool.
    VERSION="1.0.0"
    # 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="enp7s0"
    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; 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"