-
-
Save ogmkp/712e4958911cda09159cf36817dcabc6 to your computer and use it in GitHub Desktop.
Force VPN for specific apps, in a better way than killswitch [Linux / OpenVPN]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# === INFO === | |
# ForceVPN | |
# Description: Force VPN tunnel for specific applications. | |
# If the VPN is down => block the app network traffic. | |
# Better than a killswitch. IPv4. | |
VERSION="2.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. | |
# 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 | |
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 | |
# 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 | |
iptables_rule_set_name="${cgroup_name}_rule_set" | |
net_cls_classid="0x00220022" # Anything from 0x00000001 to 0xFFFFFFFF | |
# === 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;; | |
-i|--bind) action="bind"; shift 1;; | |
-u|--unbind) action="unbind"; shift 1;; | |
-l|--list) action="list"; shift 1;; | |
-s|--skip) skip=true; 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;; | |
-*) 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] { --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-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-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 -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 | |
} | |
# 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]+"` | |
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 [ -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 | |
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 | |
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 | |
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" --sticky "$@" &>/dev/null & | |
exit 0 | |
else | |
cgexec -g net_cls:"$cgroup_name" --sticky "$@" | |
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 | |
# 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" | |
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" | |
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 : |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment