Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Run a command inside a customised networking environment (using cgroups)
#!/bin/bash
# === INFO ===
# altnetworking.sh
# Description: Run the specified application in a custom networking environment.
# Uses cgroups to run process(es) in a network environment of your own choosing (within limits!)
VERSION="0.1.0"
# Author: John Clark
# Requirements: Debian 8 Jessie (plus iptables 1.6 from unstable)
#
# This script was derived from the excellent "novpn.sh" script by KrisWebDev
# as found here: https://gist.github.com/kriswebdev/a8d291936fe4299fb17d3744497b1170
#
# === 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/>.
# This script will `source` a configuration script identified by the first command line argument.
# That configuration script is expected to define:
#
# 1. The name of the interface that we want the command to emit packets from
# e.g. desired_interface="eth0"
# 2. The default route we want to have for our cgroup
# e.g. desired_default_route=`ip route | grep "dev ${desired_interface}" | awk '/^default/ { print $3 }'`
# 3. The name for the cgroup we're going to create
# Note: Better keep it with purely lowercase alphabetic & underscore
# e.g. cgroup_name="vpntinclpgmtwireless"
# 4. The classid we're going to use for this cgroup
# Note: Anything from 0x00000001 to 0xFFFFFFFF (just needs to be unique)
# e.g. net_cls_classid="0x00110011"
# 5. The mark we're going to put on packets originating from this app/cgroup
# Note: Anything from 1 to 2147483647
# e.g. ip_table_fwmark="11"
# 6. The Routing table number we'll associate with packets on this cgroup
# Note: Anything from 1 to 252 (just needs to be unique)
# e.g. ip_table_number="11"
# 7. The routing table name we're going to use to formulate the full routing table name
# Note: Needs to be unique. Best to use cgroup name
# e.g. ip_table_name="$cgroup_name"
# 8. Define a post_up() function that will be called after everything about
# the cgroup is set up (including default route)
# Use it to create additional needed routes and/or iptables entries
# e,g post_up(){
# echo "Adding routes to LPGMT lan via vpn-tinc-telstra-wireless-01-node-11"
# sudo ip route add 10.0.1.0/24 via 10.11.12.9 dev "$desired_interface" table "$ip_table_name"
# sudo ip route add 10.0.99.0/24 via 10.11.12.9 dev "$desired_interface" table "$ip_table_name"
# }
# 9. Define a pre_down() function that will be called before everything about the cgroup is is torn down
# Use it to undo everything in post_up
# e.g. pre_down(){
# echo "Removing routes to LPGMT lan"
# sudo ip route del 10.0.1.0/24 via 10.11.12.9 dev "$desired_interface" table "$ip_table_name"
# sudo ip route del 10.0.99.0/24 via 10.11.12.9 dev "$desired_interface" table "$ip_table_name"
# }
#
# 10 Define the test_networking() function that will carry out tests to confirm that the
# networking environment that has been created is functioning properly. It should return 0 if
# networking is functioning correctly or 1 otherwise. If returning 0, set testresult=true else
# set it to false
# e.g. test_networking(){
# echo "Networking was not tested by the test_networking function. Confirm it's working manually if you feel the need"
# testresult=true
# return 0
# }
# === CODE ===
set -u
# Some defaults
force=false
testresult=true
# Handle options
action="command"
background=false
skip=false
init_nb_args="$#"
show_help() {
me=`basename "$0"`
echo -e "Usage : \e[1m$me \e[4mCONFIG\e[24m [\e[4mOPTIONS\e[24m] [\e[4mCOMMAND\e[24m [\e[4mCOMMAND PARAMETERS\e[24m]]\e[0m"
echo -e "Run command using different networking configuration (via cgroups)."
echo
echo -e "\e[1m\e[4mCONFIG\e[0m: Full path to the configuration file"
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-l, --list\e[0m List processes running in this special cgroup namespace."
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 even if network connectivity tests fail."
echo -e "\e[1m-c, --clean\e[0m Terminate all proceses inside cgroup 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."
}
config_file_name="$1"
if [ -f "$config_file_name" ]
then
source "$config_file_name"
else
show_help
exit 1
fi
shift
while [ "$#" -gt 0 ]; do
case "$1" in
-b|--background) background=true; 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 "altnetworking.sh v$VERSION"; exit 0;;
-*) echo "Unknown option: $1. Try --help." >&2; exit 1;;
*) break;; # Start of COMMAND or LIST
esac
done
# Respond to --help
if [ "$init_nb_args" -lt 1 ] || [ "$action" = "help" ] ; then
show_help
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 running inside the cgroup
list_processes(){
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 "$desired_interface" -j MASQUERADE 2>/dev/null; then
echo "Adding iptables NAT rule force the packets with class identifier $net_cls_classid to exit through $desired_interface" >&2
sudo iptables -t nat -A POSTROUTING -m cgroup --cgroup "$net_cls_classid" -o "$desired_interface" -j MASQUERADE
fi
iptable_checked=true
}
# Test if config is working, IPv4 only
test_connection(){
# Call the configuration function to test if networking is functioning
test_networking
}
check_iptables=false
if [ "$action" = "command" ]
then
# SETUP config
if [ "$skip" = false ]; then
echo "Checking/setting forced routing config (skip with $0 -s ...)" >&2
if check_package cgroupfs-mount cgmanager cgroup-tools inetutils-traceroute; then
echo "You may want to reboot now. But that's probably not necessary." >&2
exit 1
fi
if dpkg --compare-versions `sudo 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 "sudo apt-get install iptables/unstable libxtables11/unstable" >&2
echo "... or compile from source as shown below:" >&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 $desired_default_route dev ${desired_interface} 2>/dev/null`" ]; then
echo "Adding default route in ip routing table $ip_table_name via $desired_default_route dev $desired_interface" >&2
sudo ip route add default via "$desired_default_route" dev "$desired_interface" table "$ip_table_name"
# Now run custom post_up script
post_up
# 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/${desired_interface}/rp_filter`" != "0" ] || [ "`cat /proc/sys/net/ipv4/conf/${desired_interface}/rp_filter`" != "2" ]; then
echo "Unset reverse path filtering for interface \"${desired_interface}\"" >&2
echo 2 | sudo tee "/proc/sys/net/ipv4/conf/${desired_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_connection
if [ "$force" != true ]; then
if [ "$testresult" = false ]; then
if [ "$iptable_checked" = false ]; then
echo -e "Testing iptables..." >&2
setup_iptables
test_connection
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 processes using this cgroup
# Exit code 0 (true) if at least 1 process is running in the cgroup
elif [ "$action" = "list" ]; then
echo "List of processes using cgroup $cgroup_name:"
list_processes
exit $?
# 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."
# Kill tasks in cgroup
if [ -f "/sys/fs/cgroup/net_cls/${cgroup_name}/tasks" ]; then
while read task_pid; do sudo kill ${task_pid} ; done < "/sys/fs/cgroup/net_cls/${cgroup_name}/tasks"
fi
# Run custom pre_down function
pre_down
# 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
# (DISABLED BECAUSE MY MACHINE DEFAULTS TO RPF BEING OFF) Re-enable Reverse Path Filtering
#echo 1 | sudo tee "/proc/sys/net/ipv4/conf/all/rp_filter" > /dev/null
#echo 1 | sudo tee "/proc/sys/net/ipv4/conf/${desired_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 "$desired_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:
# ./altnetworking.sh traceroute www.google.com
# ip=$(./altnetworking.sh curl 'https://wtfismyip.com/text' 2>/dev/null); echo "$ip"; whois "$ip" | grep -E "inetnum|route|netname|descr"
@ezbik

This comment has been minimized.

Copy link

@ezbik ezbik commented Sep 21, 2020

https://github.com/ezbik/scripts/tree/master/altnetworking

improved version ( no sudo garbage, prevents IP leaks , ip rule & iptables workarounds).

@level323

This comment has been minimized.

Copy link
Owner Author

@level323 level323 commented Sep 21, 2020

https://github.com/ezbik/scripts/tree/master/altnetworking

improved version ( no sudo garbage, prevents IP leaks , ip rule & iptables workarounds).

Thanks for your contribution!

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