Skip to content

Instantly share code, notes, and snippets.

@kriswebdev
Last active January 2, 2025 16:34
Show Gist options
  • Save kriswebdev/c19e103bd69c994a1c16ced004908c76 to your computer and use it in GitHub Desktop.
Save kriswebdev/c19e103bd69c994a1c16ced004908c76 to your computer and use it in GitHub Desktop.

Revisions

  1. kriswebdev revised this gist Jan 2, 2022. No changes.
  2. kriswebdev revised this gist Dec 22, 2019. 1 changed file with 94 additions and 33 deletions.
    127 changes: 94 additions & 33 deletions forcevpn.sh
    Original file line number Diff line number Diff line change
    @@ -5,7 +5,7 @@
    # Description: Force VPN tunnel for specific applications.
    # If the VPN is down => blackhole the app network traffic.
    # Better than a killswitch. IPv4.
    VERSION="2.2.0"
    VERSION="2.3.0"
    # Author: KrisWebDev
    # Requirements: Linux with kernel > 2.6.4 (released in 2008).
    # This version is only tested on Ubuntu 19.10 with bash.
    @@ -78,6 +78,8 @@ vpn_interface_gateway=`ip route | grep "dev ${vpn_interface}" | awk '/^default/
    action="command"
    background=false
    skip=false
    allow_localhost=false
    disable_localhost=false
    init_nb_args="$#"
    sudok=false

    @@ -88,6 +90,9 @@ while [ "$#" -gt 0 ]; do
    -u|--unbind) action="unbind"; shift 1;;
    -l|--list) action="list"; shift 1;;
    -s|--skip) skip=true; shift 1;;
    -L|--localhost) allow_localhost=true; shift 1;;
    -P|--no-localhost) disable_localhost=true; shift 1;;
    -I|--info) action="info"; shift 1;;
    -c|--clean) action="clean"; shift 1;;
    --conf-restart) action="conf-restart"; shift 1;;
    -h|--help) action="help"; shift 1;;
    @@ -100,17 +105,20 @@ 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 "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] { --bind | --unbind } \e[4mLIST\e[24m\e[0m"
    echo -e "Force (bind) program inside the VPN tunnel."
    echo -e "Force (bind) program \e[4mCOMMAND\e[24m inside 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-i, --bind \e[4mLIST\e[24m\e[0m Force (bind) running process \e[4mLIST\e[24m inside tunnel."
    echo -e "\e[1m-i, --bind \e[4mLIST\e[24m\e[0m Force (bind) running process \e[4mLIST\e[24m inside tunnel. \e[1mBROKEN!\e[0m"
    echo -e "\e[1m-u, --unbind \e[4mLIST\e[24m\e[0m Cancel force bind for running process \e[4mLIST\e[24m."
    echo -e "\e[1m-l, --list\e[0m List processes binded inside 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 bind fails."
    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-L, --localhost\e[0m Add rule to allow traffic with localhost (disabled by default)."
    echo -e "\e[1m-P, --no-localhost\e[0m Remove rule to allow traffic with localhost."
    echo -e "\e[1m-l, --list\e[0m List processes binded inside tunnel."
    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."
    @@ -119,6 +127,27 @@ if [ "$init_nb_args" -lt 1 ] || [ "$action" = "help" ] ; then
    exit 1
    fi

    # This program can't ask for root outside terminal
    if [ ! -t 1 ] && [ "$(id -u)" -ne 0 ]; then
    skip=true
    fi

    if [ "$allow_localhost" = true ] && [ "$disable_localhost" = true ]; then
    echo -e "\e[31mCan't use --localhost with --no-localhost. Aborting.\e[0m" >&2
    exit 1
    fi

    if [ "$skip" = true ]; then
    if [ "$allow_localhost" = true ] || [ "$disable_localhost" = true ]; then
    echo -e "\e[33mWARNING: Ignoring localhost traffic setup options as --skip option is enabled.\e[0m" >&2
    fi
    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
    @@ -163,30 +192,47 @@ list_bind(){
    iptable_checked=false
    setup_iptables(){
    if ! sudo iptables -C OUTPUT -m cgroup --cgroup "$net_cls_classid" -j "$iptables_rule_set_name" 2>/dev/null; then
    echo "Adding iptables rule drop packets with class identifier $net_cls_classid not exiting through ${vpn_interface} or locally (DNS)" >&2
    echo "Adding iptables rule to drop packets with class identifier $net_cls_classid not exiting through ${vpn_interface} or locally (DNS)" >&2
    sudo iptables -N "$iptables_rule_set_name"

    # Moved to Networkmanager dispatcher script for better security
    #if [ "$allow_localhost" = true ]; then
    # sudo iptables -I "$iptables_rule_set_name" -o lo -j RETURN
    #fi

    # Bad alternative that leads to massive quick retries hence CPU load: -j REJECT --reject-with icmp-net-prohibited
    sudo iptables -A "$iptables_rule_set_name" ! -o "$vpn_interface" -j DROP
    sudo iptables -I OUTPUT -m cgroup --cgroup "$net_cls_classid" -j "$iptables_rule_set_name"
    fi
    iptable_checked=true
    }


    setup_iptables_localhost(){
    if ! sudo iptables -C "$iptables_rule_set_name" -d "127.0.0.1" -j ACCEPT 2>/dev/null; then
    if [ "$allow_localhost" = true ]; then
    echo "Adding iptables rule to allow localhost trafic" >&2
    sudo iptables -I "$iptables_rule_set_name" -d "127.0.0.1" -j ACCEPT
    fi
    elif [ "$disable_localhost" = true ]; then
    echo "Adding iptables rule to allow localhost trafic" >&2
    sudo iptables -D "$iptables_rule_set_name" -d "127.0.0.1" -j ACCEPT
    fi
    iptable_checked=true
    }

    # Test if config is working, IPv4 only
    testresult=true
    test_forced(){
    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
    @@ -196,49 +242,60 @@ test_forced(){
    testresult=false
    return 0
    fi
    ping6 -t 1 -c 1 -n 2001:4860:4860::8888 2>/dev/null
    retcode=$?
    if [ "$retcode" -ne 2 ]; then
    echo -e "\e[31mTest failed: IPv6 is not disabled or unable to test.\e[0m" >&2
    fi
    if [ "$exit_ip" == "$vpn_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 \"$vpn_interface_gateway\".\e[0m" >&2
    echo -e "\e[31mTest failed: Trafic exits with \"$exit_ip\" instead of \"$vpn_interface_gateway\". Aborting.\e[0m" >&2
    testresult=false
    return 1
    fi
    }


    # Reconfigure routing
    reroute(){

    if [ -z "$vpn_interface_gateway" ]; then
    echo -e "\e[31mCan't find default gateway of interface ${vpn_interface}. Is it up?\e[0m" >&2
    echo -e "\e[31mCan't find default gateway of VPN interface \"${vpn_interface}\". Is it up?\e[0m" >&2
    echo -e "\e[31mAborting.\e[0m" >&2
    exit 1
    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 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
    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
    if [ "$allow_localhost" = true ] || [ "$disable_localhost" = true ]; then
    setup_iptables_localhost
    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
    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 forced bind
    test_forced
    if [ "$force" != true ]; then
    # TEST
    test_routing
    if [ "$skip" = false ]; then
    if [ "$testresult" = false ]; then
    if [ "$iptable_checked" = false ]; then
    if [ "$iptable_checked" = false ] && [ "$skip" = false ]; then
    echo -e "Trying to setup iptables and redo test..." >&2
    setup_iptables
    test_forced
    test_routing
    fi
    fi
    if [ "$testresult" = false ]; then
    @@ -371,6 +428,12 @@ elif [ "$action" = "unbind" ]; then
    list_bind


    # 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."
    @@ -409,7 +472,5 @@ fi

    # BONUS: Useful commands:
    # ./forcevpn.sh ping 8.8.8.8
    # ./forcevpn.sh --bind ping
    # ./forcevpn.sh vuze
    # ./forcevpn.sh --bind java
    # ./forcevpn.sh --background vuze
    # ./forcevpn.sh --localhost --background biglybt
    # killall firefox; ./forcevpn.sh --background firefox
  3. kriswebdev revised this gist Nov 11, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion forcevpn.sh
    Original file line number Diff line number Diff line change
    @@ -11,6 +11,7 @@ VERSION="2.2.0"
    # 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 if needed.
    # Note: This script will disable IPv6 (enable with --clean)

    # dnsmasq users: You're usinq dnsmasq if you find "dns=dsnmasq" in /etc/NetworkManager/NetworkManager.conf
    # gksudo gedit /etc/NetworkManager/dispatcher.d/forcevpn-dispatcher.sh
    @@ -229,7 +230,6 @@ reroute(){
    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


    # TEST forced bind
    test_forced
  4. kriswebdev revised this gist Nov 11, 2019. 1 changed file with 52 additions and 20 deletions.
    72 changes: 52 additions & 20 deletions forcevpn.sh
    Original file line number Diff line number Diff line change
    @@ -3,23 +3,22 @@
    # === INFO ===
    # ForceVPN
    # Description: Force VPN tunnel for specific applications.
    # If the VPN is down => block the app network traffic.
    # If the VPN is down => blackhole the app network traffic.
    # Better than a killswitch. IPv4.
    VERSION="2.1.0"
    VERSION="2.2.0"
    # Author: KrisWebDev
    # Requirements: Linux with kernel > 2.6.4 (released in 2008).
    # Only tested on Ubuntu 16 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.
    # dnsmasq users: You're usinq dnsmasq if you find "dns=dsnmasq" in /etc/networkManager/NetworkManager.conf
    # Script will guide you for iptables 1.6.0 install if needed.

    # dnsmasq users: You're usinq dnsmasq if you find "dns=dsnmasq" in /etc/NetworkManager/NetworkManager.conf
    # gksudo gedit /etc/NetworkManager/dispatcher.d/forcevpn-dispatcher.sh
    # Insert content of below commented script
    # sudo chmod +x /etc/NetworkManager/dispatcher.d/forcevpn-dispatcher.sh
    # See for more info: http://askubuntu.com/a/703665/263353
    # Change uint32 value with your VPN provider DNS server IP converted to Integer:
    # http://www.aboutmyip.com/AboutMyXApp/IP2Integer.jsp
    # Note: This script will disable IPv6 (enable with --clean)

    : '
    #!/bin/bash
    @@ -79,6 +78,7 @@ action="command"
    background=false
    skip=false
    init_nb_args="$#"
    sudok=false

    while [ "$#" -gt 0 ]; do
    case "$1" in
    @@ -91,6 +91,7 @@ while [ "$#" -gt 0 ]; do
    --conf-restart) action="conf-restart"; shift 1;;
    -h|--help) action="help"; shift 1;;
    -v|--version) echo "forcevpn 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
    @@ -120,11 +121,11 @@ 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
    @@ -133,6 +134,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 binded to the VPN tunnel
    list_bind(){
    return_status=1
    @@ -167,13 +180,27 @@ setup_iptables(){
    # Test if config is working, IPv4 only
    testresult=true
    test_forced(){
    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]+"`
    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
    testresult=false
    return 0
    fi
    fi
    if [ -z "$vpn_interface_gateway" ]; then
    echo -e "\e[31mTest failed: Unable to determine VPN interface gateway IP (found \"$vpn_interface_gateway\").\e[0m" >&2
    testresult=false
    return 0
    fi
    if [ "$exit_ip" == "$vpn_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 $vpn_interface_gateway.\e[0m" >&2
    echo -e "\e[31mTest failed: Trafic exits with \"$exit_ip\" instead of \"$vpn_interface_gateway\".\e[0m" >&2
    testresult=false
    return 1
    fi
    @@ -197,11 +224,12 @@ reroute(){
    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


    # TEST forced bind
    test_forced
    @@ -229,12 +257,15 @@ if [ "$action" = "command" ] || [ "$action" = "bind" ]; 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
    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
    @@ -271,6 +302,9 @@ 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
    @@ -378,6 +412,4 @@ fi
    # ./forcevpn.sh --bind ping
    # ./forcevpn.sh vuze
    # ./forcevpn.sh --bind java
    # ./forcevpn.sh --background vuze
    # To restore connectivity once VPN is restarted:
    # ./forcevpn.sh :
    # ./forcevpn.sh --background vuze
  5. kriswebdev revised this gist Aug 25, 2018. 1 changed file with 18 additions and 6 deletions.
    24 changes: 18 additions & 6 deletions forcevpn.sh
    Original file line number Diff line number Diff line change
    @@ -3,9 +3,9 @@
    # === INFO ===
    # ForceVPN
    # Description: Force VPN tunnel for specific applications.
    # If the VPN is down => blackhole the app network traffic.
    # If the VPN is down => block the app network traffic.
    # Better than a killswitch. IPv4.
    VERSION="2.0.0"
    VERSION="2.1.0"
    # Author: KrisWebDev
    # Requirements: Linux with kernel > 2.6.4 (released in 2008).
    # Only tested on Ubuntu 16 with bash.
    @@ -15,8 +15,11 @@ VERSION="2.0.0"
    # gksudo gedit /etc/NetworkManager/dispatcher.d/forcevpn-dispatcher.sh
    # Insert content of below commented script
    # sudo chmod +x /etc/NetworkManager/dispatcher.d/forcevpn-dispatcher.sh
    # See: http://askubuntu.com/a/703665/263353
    # Change uint32 value with your DNS server IP converted to Integer: http://www.aboutmyip.com/AboutMyXApp/IP2Integer.jsp
    # See for more info: http://askubuntu.com/a/703665/263353
    # Change uint32 value with your VPN provider DNS server IP converted to Integer:
    # http://www.aboutmyip.com/AboutMyXApp/IP2Integer.jsp
    # Note: This script will disable IPv6 (enable with --clean)

    : '
    #!/bin/bash
    @@ -84,7 +87,7 @@ while [ "$#" -gt 0 ]; do
    -u|--unbind) action="unbind"; shift 1;;
    -l|--list) action="list"; shift 1;;
    -s|--skip) skip=true; shift 1;;
    -l|--clean) action="clean"; shift 1;;
    -c|--clean) action="clean"; shift 1;;
    --conf-restart) action="conf-restart"; shift 1;;
    -h|--help) action="help"; shift 1;;
    -v|--version) echo "forcevpn v$VERSION"; exit 0;;
    @@ -164,7 +167,7 @@ setup_iptables(){
    # Test if config is working, IPv4 only
    testresult=true
    test_forced(){
    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]+"`
    exit_ip=`cgexec -g net_cls:"$cgroup_name" traceroute -m 1 8.8.8.8 | sed -n '2{p;q}' | grep -oE "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+"`
    if [ "$exit_ip" == "$vpn_interface_gateway" ]; then
    echo -e "\e[32mTest OK. Trafic exits with IP $exit_ip.\e[0m" >&2
    testresult=true
    @@ -194,6 +197,11 @@ reroute(){
    setup_iptables
    fi

    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


    # TEST forced bind
    test_forced
    @@ -348,6 +356,10 @@ elif [ "$action" = "clean" ]; then
    sudo iptables -D OUTPUT -m cgroup --cgroup "$net_cls_classid" -j "$iptables_rule_set_name"
    sudo iptables -F "$iptables_rule_set_name"
    sudo iptables -X "$iptables_rule_set_name"

    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"
  6. kriswebdev revised this gist Jul 2, 2016. 1 changed file with 51 additions and 21 deletions.
    72 changes: 51 additions & 21 deletions forcevpn.sh
    Original file line number Diff line number Diff line change
    @@ -5,12 +5,45 @@
    # Description: Force VPN tunnel for specific applications.
    # If the VPN is down => blackhole the app network traffic.
    # Better than a killswitch. IPv4.
    VERSION="1.2.0"
    VERSION="2.0.0"
    # Author: KrisWebDev
    # Requirements: Linux with kernel > 2.6.4 (released in 2008).
    # Only tested on Ubuntu 16 with bash.
    # Main dependencies are automatically installed.
    # Script will guide you for iptables 1.6.0 install.
    # dnsmasq users: You're usinq dnsmasq if you find "dns=dsnmasq" in /etc/networkManager/NetworkManager.conf
    # gksudo gedit /etc/NetworkManager/dispatcher.d/forcevpn-dispatcher.sh
    # Insert content of below commented script
    # sudo chmod +x /etc/NetworkManager/dispatcher.d/forcevpn-dispatcher.sh
    # See: http://askubuntu.com/a/703665/263353
    # Change uint32 value with your DNS server IP converted to Integer: http://www.aboutmyip.com/AboutMyXApp/IP2Integer.jsp
    : '
    #!/bin/bash
    interface=$1
    status=$2
    case $status in
    vpn-up)
    # because dnsmasq keep DNS LAN and leak our DNS, hard-code DNS servers
    dbus-send --system --dest=org.freedesktop.NetworkManager.dnsmasq --type=method_call /uk/org/thekelleys/dnsmasq uk.org.thekelleys.SetServers
    dbus-send --system --dest=org.freedesktop.NetworkManager.dnsmasq --type=method_call /uk/org/thekelleys/dnsmasq uk.org.thekelleys.SetServers uint32:3250021018
    dbus-send --system --dest=org.freedesktop.NetworkManager.dnsmasq --type=method_call /uk/org/thekelleys/dnsmasq uk.org.thekelleys.SetServers uint32:3112519796
    # flush DNS cache
    pkill --signal SIGHUP dnsmasq
    # provide access to dnsmasq when vpn is up
    iptables -N forcevpn_rule_set
    iptables -I forcevpn_rule_set -o lo -p udp --dport 53 -j RETURN
    ;;
    vpn-down)
    # flush DNS cache
    pkill --signal SIGHUP dnsmasq
    # deny access to dnsmasq when vpn is down
    iptables -D forcevpn_rule_set -o lo -p udp --dport 53 -j RETURN
    ;;
    esac
    '

    # === LICENSE ===
    # This program is free software: you can redistribute it and/or modify
    @@ -31,14 +64,8 @@ vpn_interface="tun0"

    # === ADVANCED CONFIGURATION ===
    cgroup_name="forcevpn" # Better keep it with purely lowercase alphabetic & underscore
    iptables_rule_set_name="${cgroup_name}_rule_set"
    net_cls_classid="0x00220022" # Anything from 0x00000001 to 0xFFFFFFFF
    ip_table_fwmark="22" # Anything from 1 to 2147483647
    ip_table_number="22" # Anything from 1 to 252
    ip_table_name="$cgroup_name"
    ip_rule_priority="30022" # Anything from 1 to 32764, with the next rule free. See "ip rule list".
    ip_table_number_blackhole="66" # Anything from 1 to 252
    ip_table_name_blackhole="blackhole"
    ip_rule_priority_blackhole=$((ip_rule_priority+1))

    # === CODE ===
    vpn_interface_gateway=`ip route | grep "dev ${vpn_interface}" | awk '/^default/ { print $3 }'`
    @@ -118,9 +145,18 @@ list_bind(){
    # Check and setup iptables - requires root even for check
    iptable_checked=false
    setup_iptables(){
    if ! sudo iptables -C OUTPUT -m cgroup --cgroup "0x00220022" \! -o tun0 -j DROP 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 -A OUTPUT -m cgroup --cgroup "0x00220022" \! -o tun0 -j DROP 2>/dev/null
    if ! sudo iptables -C OUTPUT -m cgroup --cgroup "$net_cls_classid" -j "$iptables_rule_set_name" 2>/dev/null; then
    echo "Adding iptables rule drop packets with class identifier $net_cls_classid not exiting through ${vpn_interface} or locally (DNS)" >&2
    sudo iptables -N "$iptables_rule_set_name"

    # Moved to Networkmanager dispatcher script for better security
    #if [ "$allow_localhost" = true ]; then
    # sudo iptables -I "$iptables_rule_set_name" -o lo -j RETURN
    #fi

    # Bad alternative that leads to massive quick retries hence CPU load: -j REJECT --reject-with icmp-net-prohibited
    sudo iptables -A "$iptables_rule_set_name" ! -o "$vpn_interface" -j DROP
    sudo iptables -I OUTPUT -m cgroup --cgroup "$net_cls_classid" -j "$iptables_rule_set_name"
    fi
    iptable_checked=true
    }
    @@ -218,7 +254,6 @@ 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
    # Set up script to restore gateway once VPN is up again?
    fi

    if [ "$action" = "command" ]; then
    @@ -309,15 +344,10 @@ elif [ "$action" = "clean" ]; then
    sudo find "/sys/fs/cgroup/net_cls/${cgroup_name}" -depth -type d -print -exec rmdir {} \;
    fi

    # Bad alternative that leads to massive quick retries hence CPU load: -j REJECT --reject-with icmp-net-prohibited
    sudo iptables -D OUTPUT -m cgroup --cgroup "$net_cls_classid" \! -o tun0 -j DROP

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

    sudo sed -i '/^${ip_table_number}\s\+${ip_table_name}\s*$/d' /etc/iproute2/rt_tables
    sudo sed -i '/^${ip_table_number_blackhole}\s\+${ip_table_name_blackhole}\s*$/d' /etc/iproute2/rt_tables
    # Debug: sudo iptables -L -v
    sudo iptables -D OUTPUT -m cgroup --cgroup "$net_cls_classid" -j "$iptables_rule_set_name"
    sudo iptables -F "$iptables_rule_set_name"
    sudo iptables -X "$iptables_rule_set_name"

    if [ -n "`lscgroup net_cls:$cgroup_name`" ]; then
    sudo cgdelete net_cls:"$cgroup_name"
  7. kriswebdev revised this gist Jun 23, 2016. 1 changed file with 13 additions and 12 deletions.
    25 changes: 13 additions & 12 deletions forcevpn.sh
    Original file line number Diff line number Diff line change
    @@ -5,7 +5,7 @@
    # Description: Force VPN tunnel for specific applications.
    # If the VPN is down => blackhole the app network traffic.
    # Better than a killswitch. IPv4.
    VERSION="1.1.0"
    VERSION="1.2.0"
    # Author: KrisWebDev
    # Requirements: Linux with kernel > 2.6.4 (released in 2008).
    # Only tested on Ubuntu 16 with bash.
    @@ -53,8 +53,8 @@ init_nb_args="$#"
    while [ "$#" -gt 0 ]; do
    case "$1" in
    -b|--background) background=true; shift 1;;
    -o|--bind) action="bind"; shift 1;;
    -i|--unbind) action="unbind"; shift 1;;
    -i|--bind) action="bind"; shift 1;;
    -u|--unbind) action="unbind"; shift 1;;
    -l|--list) action="list"; shift 1;;
    -s|--skip) skip=true; shift 1;;
    -l|--clean) action="clean"; shift 1;;
    @@ -69,13 +69,13 @@ 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] { --inside | --outside } \e[4mLIST\e[24m\e[0m"
    echo -e " or : \e[1m$me [\e[4mOPTIONS\e[24m] { --bind | --unbind } \e[4mLIST\e[24m\e[0m"
    echo -e "Force (bind) program inside 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-i, --bind \e[4mLIST\e[24m\e[0m Force (bind) running process \e[4mLIST\e[24m inside tunnel."
    echo -e "\e[1m-o, --unbind \e[4mLIST\e[24m\e[0m Cancel force bind for running process \e[4mLIST\e[24m."
    echo -e "\e[1m-i, --bind \e[4mLIST\e[24m\e[0m Force (bind) running process \e[4mLIST\e[24m inside tunnel."
    echo -e "\e[1m-u, --unbind \e[4mLIST\e[24m\e[0m Cancel force bind for running process \e[4mLIST\e[24m."
    echo -e "\e[1m-l, --list\e[0m List processes binded inside 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 bind fails."
    @@ -118,9 +118,9 @@ list_bind(){
    # Check and setup iptables - requires root even for check
    iptable_checked=false
    setup_iptables(){
    if ! sudo iptables -C OUTPUT -m cgroup --cgroup "0x00220022" \! -o tun0 -j REJECT 2>/dev/null; then
    if ! sudo iptables -C OUTPUT -m cgroup --cgroup "0x00220022" \! -o tun0 -j DROP 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 -A OUTPUT -m cgroup --cgroup "0x00220022" \! -o tun0 -j REJECT --reject-with icmp-net-prohibited 2>/dev/null
    sudo iptables -A OUTPUT -m cgroup --cgroup "0x00220022" \! -o tun0 -j DROP 2>/dev/null
    fi
    iptable_checked=true
    }
    @@ -150,7 +150,7 @@ reroute(){
    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
    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
    @@ -233,10 +233,10 @@ if [ "$action" = "command" ]; then
    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

    @@ -309,7 +309,8 @@ elif [ "$action" = "clean" ]; then
    sudo find "/sys/fs/cgroup/net_cls/${cgroup_name}" -depth -type d -print -exec rmdir {} \;
    fi

    sudo iptables -D OUTPUT -m cgroup --cgroup "$net_cls_classid" \! -o tun0 -j REJECT --reject-with icmp-net-prohibited
    # Bad alternative that leads to massive quick retries hence CPU load: -j REJECT --reject-with icmp-net-prohibited
    sudo iptables -D OUTPUT -m cgroup --cgroup "$net_cls_classid" \! -o tun0 -j DROP

    sudo ip rule del fwmark "$ip_table_fwmark" table "$ip_table_name"
    sudo ip route del default table "$ip_table_name"
  8. kriswebdev revised this gist Jun 18, 2016. 1 changed file with 4 additions and 52 deletions.
    56 changes: 4 additions & 52 deletions forcevpn.sh
    Original file line number Diff line number Diff line change
    @@ -5,17 +5,12 @@
    # Description: Force VPN tunnel for specific applications.
    # If the VPN is down => blackhole the app network traffic.
    # Better than a killswitch. IPv4.
    VERSION="1.0.0"
    VERSION="1.1.0"
    # Author: KrisWebDev
    # Requirements: Linux with kernel > 2.6.4 (released in 2008).
    # Only tested on Ubuntu 16 with bash.
    # Main dependencies are automatically installed.
    # Script will guide you for iptables 1.6.0 install.
    # Tip: Add these lines to /etc/network/interfaces
    # to route the app again when the interface is restarted:
    # auto tun0
    # iface tun0 inet manual
    #  post-up ip route add default via `ip route | grep "dev tun0" | awk '/^default/ { print $3 }'` dev tun0 table forcevpn

    # === LICENSE ===
    # This program is free software: you can redistribute it and/or modify
    @@ -123,13 +118,9 @@ list_bind(){
    # 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
    if ! sudo iptables -C OUTPUT -m cgroup --cgroup "0x00220022" \! -o tun0 -j REJECT 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 "$vpn_interface" -j MASQUERADE 2>/dev/null; then
    echo "Adding iptables NAT rule force the packets with class identifier $net_cls_classid to exit through $vpn_interface" >&2
    sudo iptables -t nat -A POSTROUTING -m cgroup --cgroup "$net_cls_classid" -o "$vpn_interface" -j MASQUERADE
    sudo iptables -A OUTPUT -m cgroup --cgroup "0x00220022" \! -o tun0 -j REJECT --reject-with icmp-net-prohibited 2>/dev/null
    fi
    iptable_checked=true
    }
    @@ -158,44 +149,6 @@ reroute(){
    exit 1
    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 ! grep -E "^${ip_table_number_blackhole}\s+$ip_table_name_blackhole" /etc/iproute2/rt_tables &>/dev/null; then
    if grep -E "^${ip_table_number_blackhole}\s+" /etc/iproute2/rt_tables; then
    echo "ERROR: Table ${ip_table_number_blackhole} already exists in /etc/iproute2/rt_tables with a different name than $ip_table_name_blackhole" >&2
    exit 1
    fi
    echo "Creating ip routing table: number=$ip_table_number_blackhole name=$ip_table_name_blackhole" >&2
    echo "$ip_table_number_blackhole $ip_table_name_blackhole" | 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 prio "$ip_rule_priority" fwmark "$ip_table_fwmark" table "$ip_table_name"
    check_iptables=true
    fi
    if ! ip rule list | grep " lookup $ip_table_name_blackhole" | grep " fwmark " &>/dev/null; then
    echo "Adding rule to use ip routing table $ip_table_name_blackhole for packets with firewall mark $ip_table_fwmark" >&2
    sudo ip rule add prio "$ip_rule_priority_blackhole" fwmark "$ip_table_fwmark" table "$ip_table_name_blackhole"
    check_iptables=true
    fi
    if [ -z "`ip route list table "$ip_table_name" default via $vpn_interface_gateway dev ${vpn_interface} 2>/dev/null`" ]; then
    echo "Adding default route in ip routing table $ip_table_name via $vpn_interface_gateway dev $vpn_interface" >&2
    sudo ip route add default via "$vpn_interface_gateway" dev "$vpn_interface" table "$ip_table_name"
    check_iptables=true
    fi
    if [ -z "`ip route list table "$ip_table_name_blackhole" default 2>/dev/null`" ]; then
    echo "Adding blackhole default route in ip routing table $ip_table_name_blackhole" >&2
    sudo ip route add blackhole default table "$ip_table_name_blackhole"
    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"
    @@ -356,8 +309,7 @@ elif [ "$action" = "clean" ]; then
    sudo find "/sys/fs/cgroup/net_cls/${cgroup_name}" -depth -type d -print -exec rmdir {} \;
    fi

    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 "$vpn_interface" -j MASQUERADE
    sudo iptables -D OUTPUT -m cgroup --cgroup "$net_cls_classid" \! -o tun0 -j REJECT --reject-with icmp-net-prohibited

    sudo ip rule del fwmark "$ip_table_fwmark" table "$ip_table_name"
    sudo ip route del default table "$ip_table_name"
  9. kriswebdev created this gist Jun 18, 2016.
    388 changes: 388 additions & 0 deletions forcevpn.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,388 @@
    #!/bin/bash

    # === INFO ===
    # ForceVPN
    # Description: Force VPN tunnel for specific applications.
    # If the VPN is down => blackhole the app network traffic.
    # Better than a killswitch. IPv4.
    VERSION="1.0.0"
    # Author: KrisWebDev
    # Requirements: Linux with kernel > 2.6.4 (released in 2008).
    # Only tested on Ubuntu 16 with bash.
    # Main dependencies are automatically installed.
    # Script will guide you for iptables 1.6.0 install.
    # Tip: Add these lines to /etc/network/interfaces
    # to route the app again when the interface is restarted:
    # auto tun0
    # iface tun0 inet manual
    #  post-up ip route add default via `ip route | grep "dev tun0" | awk '/^default/ { print $3 }'` dev tun0 table forcevpn

    # === 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 ===
    vpn_interface="tun0"

    # === ADVANCED CONFIGURATION ===
    cgroup_name="forcevpn" # Better keep it with purely lowercase alphabetic & underscore
    net_cls_classid="0x00220022" # Anything from 0x00000001 to 0xFFFFFFFF
    ip_table_fwmark="22" # Anything from 1 to 2147483647
    ip_table_number="22" # Anything from 1 to 252
    ip_table_name="$cgroup_name"
    ip_rule_priority="30022" # Anything from 1 to 32764, with the next rule free. See "ip rule list".
    ip_table_number_blackhole="66" # Anything from 1 to 252
    ip_table_name_blackhole="blackhole"
    ip_rule_priority_blackhole=$((ip_rule_priority+1))

    # === CODE ===
    vpn_interface_gateway=`ip route | grep "dev ${vpn_interface}" | awk '/^default/ { print $3 }'`
    #vpn_interface_ip=`ip addr show "$vpn_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|--bind) action="bind"; shift 1;;
    -i|--unbind) action="unbind"; shift 1;;
    -l|--list) action="list"; shift 1;;
    -s|--skip) skip=true; shift 1;;
    -l|--clean) action="clean"; shift 1;;
    --conf-restart) action="conf-restart"; shift 1;;
    -h|--help) action="help"; shift 1;;
    -v|--version) echo "forcevpn 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] { --inside | --outside } \e[4mLIST\e[24m\e[0m"
    echo -e "Force (bind) program inside 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-i, --bind \e[4mLIST\e[24m\e[0m Force (bind) running process \e[4mLIST\e[24m inside tunnel."
    echo -e "\e[1m-o, --unbind \e[4mLIST\e[24m\e[0m Cancel force bind for running process \e[4mLIST\e[24m."
    echo -e "\e[1m-l, --list\e[0m List processes binded inside 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 bind fails."
    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

    # 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 binded to the VPN tunnel
    list_bind(){
    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 "$vpn_interface" -j MASQUERADE 2>/dev/null; then
    echo "Adding iptables NAT rule force the packets with class identifier $net_cls_classid to exit through $vpn_interface" >&2
    sudo iptables -t nat -A POSTROUTING -m cgroup --cgroup "$net_cls_classid" -o "$vpn_interface" -j MASQUERADE
    fi
    iptable_checked=true
    }

    # Test if config is working, IPv4 only
    testresult=true
    test_forced(){
    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" == "$vpn_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 $vpn_interface_gateway.\e[0m" >&2
    testresult=false
    return 1
    fi
    }

    # Reconfigure routing
    reroute(){

    if [ -z "$vpn_interface_gateway" ]; then
    echo -e "\e[31mCan't find default gateway of interface ${vpn_interface}. Is it up?\e[0m" >&2
    echo -e "\e[31mAborting.\e[0m" >&2
    exit 1
    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 ! grep -E "^${ip_table_number_blackhole}\s+$ip_table_name_blackhole" /etc/iproute2/rt_tables &>/dev/null; then
    if grep -E "^${ip_table_number_blackhole}\s+" /etc/iproute2/rt_tables; then
    echo "ERROR: Table ${ip_table_number_blackhole} already exists in /etc/iproute2/rt_tables with a different name than $ip_table_name_blackhole" >&2
    exit 1
    fi
    echo "Creating ip routing table: number=$ip_table_number_blackhole name=$ip_table_name_blackhole" >&2
    echo "$ip_table_number_blackhole $ip_table_name_blackhole" | 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 prio "$ip_rule_priority" fwmark "$ip_table_fwmark" table "$ip_table_name"
    check_iptables=true
    fi
    if ! ip rule list | grep " lookup $ip_table_name_blackhole" | grep " fwmark " &>/dev/null; then
    echo "Adding rule to use ip routing table $ip_table_name_blackhole for packets with firewall mark $ip_table_fwmark" >&2
    sudo ip rule add prio "$ip_rule_priority_blackhole" fwmark "$ip_table_fwmark" table "$ip_table_name_blackhole"
    check_iptables=true
    fi
    if [ -z "`ip route list table "$ip_table_name" default via $vpn_interface_gateway dev ${vpn_interface} 2>/dev/null`" ]; then
    echo "Adding default route in ip routing table $ip_table_name via $vpn_interface_gateway dev $vpn_interface" >&2
    sudo ip route add default via "$vpn_interface_gateway" dev "$vpn_interface" table "$ip_table_name"
    check_iptables=true
    fi
    if [ -z "`ip route list table "$ip_table_name_blackhole" default 2>/dev/null`" ]; then
    echo "Adding blackhole default route in ip routing table $ip_table_name_blackhole" >&2
    sudo ip route add blackhole default table "$ip_table_name_blackhole"
    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


    # TEST forced bind
    test_forced
    if [ "$force" != true ]; then
    if [ "$testresult" = false ]; then
    if [ "$iptable_checked" = false ]; then
    echo -e "Trying to setup iptables and redo test..." >&2
    setup_iptables
    test_forced
    fi
    fi
    if [ "$testresult" = false ]; then
    echo -e "\e[31mAborting.\e[0m" >&2
    exit 1
    fi

    fi
    }


    check_iptables=false
    if [ "$action" = "command" ] || [ "$action" = "bind" ]; 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
    # Set up script to restore gateway once VPN is up again?
    fi

    if [ "$action" = "command" ]; then
    reroute
    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 BINDED to VPN tunnel
    # Exit code 0 (true) if at least 1 process is binded
    elif [ "$action" = "list" ]; then
    echo "List of processes binded to VPN tunnel:"
    list_bind
    exit $?

    # Force process BIND to VPN tunnel
    elif [ "$action" = "bind" ]; 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 "List of processes binded to VPN tunnel:"
    list_bind

    reroute

    exit $exit_code

    # UNBIND process
    elif [ "$action" = "unbind" ]; 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 binded to VPN tunnel:"
    list_bind


    # 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

    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 "$vpn_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 ip route del default table "$ip_table_name_blackhole"

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

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

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

    echo "All done."

    fi

    # BONUS: Useful commands:
    # ./forcevpn.sh ping 8.8.8.8
    # ./forcevpn.sh --bind ping
    # ./forcevpn.sh vuze
    # ./forcevpn.sh --bind java
    # ./forcevpn.sh --background vuze
    # To restore connectivity once VPN is restarted:
    # ./forcevpn.sh :