· 6 years ago · May 17, 2019, 12:16 AM
1#!/bin/bash
2
3# === INFO ===
4# altnetworking.sh
5# Description: Run the specified application in a custom networking environment.
6# Uses cgroups to run process(es) in a network environment of your own choosing (within limits!)
7VERSION="0.1.0"
8# Author: John Clark
9# Requirements: Debian 8 Jessie (plus iptables 1.6 from unstable)
10#
11# This script was derived from the excellent "novpn.sh" script by KrisWebDev
12# as found here: https://gist.github.com/kriswebdev/a8d291936fe4299fb17d3744497b1170
13#
14# === LICENSE ===
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
27
28# This script will `source` a configuration script identified by the first command line argument.
29# That configuration script is expected to define:
30#
31# 1. The name of the interface that we want the command to emit packets from
32# e.g. desired_interface="eth0"
33# 2. The default route we want to have for our cgroup
34# e.g. desired_default_route=`ip route | grep "dev ${desired_interface}" | awk '/^default/ { print $3 }'`
35# 3. The name for the cgroup we're going to create
36# Note: Better keep it with purely lowercase alphabetic & underscore
37# e.g. cgroup_name="vpntinclpgmtwireless"
38# 4. The classid we're going to use for this cgroup
39# Note: Anything from 0x00000001 to 0xFFFFFFFF (just needs to be unique)
40# e.g. net_cls_classid="0x00110011"
41# 5. The mark we're going to put on packets originating from this app/cgroup
42# Note: Anything from 1 to 2147483647
43# e.g. ip_table_fwmark="11"
44# 6. The Routing table number we'll associate with packets on this cgroup
45# Note: Anything from 1 to 252 (just needs to be unique)
46# e.g. ip_table_number="11"
47# 7. The routing table name we're going to use to formulate the full routing table name
48# Note: Needs to be unique. Best to use cgroup name
49# e.g. ip_table_name="$cgroup_name"
50# 8. Define a post_up() function that will be called after everything about
51# the cgroup is set up (including default route)
52# Use it to create additional needed routes and/or iptables entries
53# e,g post_up(){
54# echo "Adding routes to LPGMT lan via vpn-tinc-telstra-wireless-01-node-11"
55# sudo ip route add 10.0.1.0/24 via 10.11.12.9 dev "$desired_interface" table "$ip_table_name"
56# sudo ip route add 10.0.99.0/24 via 10.11.12.9 dev "$desired_interface" table "$ip_table_name"
57# }
58# 9. Define a pre_down() function that will be called before everything about the cgroup is is torn down
59# Use it to undo everything in post_up
60# e.g. pre_down(){
61# echo "Removing routes to LPGMT lan"
62# sudo ip route del 10.0.1.0/24 via 10.11.12.9 dev "$desired_interface" table "$ip_table_name"
63# sudo ip route del 10.0.99.0/24 via 10.11.12.9 dev "$desired_interface" table "$ip_table_name"
64# }
65#
66# 10 Define the test_networking() function that will carry out tests to confirm that the
67# networking environment that has been created is functioning properly. It should return 0 if
68# networking is functioning correctly or 1 otherwise. If returning 0, set testresult=true else
69# set it to false
70# e.g. test_networking(){
71# echo "Networking was not tested by the test_networking function. Confirm it's working manually if you feel the need"
72# testresult=true
73# return 0
74# }
75
76
77# === CODE ===
78
79set -u
80
81# Some defaults
82force=false
83testresult=true
84
85# Handle options
86action="command"
87background=false
88skip=false
89init_nb_args="$#"
90
91
92show_help() {
93 me=`basename "$0"`
94 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"
95 echo -e "Run command using different networking configuration (via cgroups)."
96 echo
97 echo -e "\e[1m\e[4mCONFIG\e[0m: Full path to the configuration file"
98 echo -e "\e[1m\e[4mOPTIONS\e[0m:"
99 echo -e "\e[1m-b, --background\e[0m Start \e[4mCOMMAND\e[24m as background process (release the shell)."
100 echo -e "\e[1m-l, --list\e[0m List processes running in this special cgroup namespace."
101 echo -e "\e[1m-s, --skip\e[0m Don't check/setup system config & don't ask for root,\n\
102 run \e[4mCOMMAND\e[24m even if network connectivity tests fail."
103 echo -e "\e[1m-c, --clean\e[0m Terminate all proceses inside cgroup and remove system config."
104 echo -e "\e[1m-v, --version\e[0m Print this program version."
105 echo -e "\e[1m-h, --help\e[0m This help."
106}
107
108config_file_name="$1"
109if [ -f "$config_file_name" ]
110then
111 source "$config_file_name"
112else
113 show_help
114 exit 1
115fi
116
117shift
118
119while [ "$#" -gt 0 ]; do
120 case "$1" in
121 -b|--background) background=true; shift 1;;
122 -l|--list) action="list"; shift 1;;
123 -s|--skip) skip=true; shift 1;;
124 -l|--clean) action="clean"; shift 1;;
125 -h|--help) action="help"; shift 1;;
126 -v|--version) echo "altnetworking.sh v$VERSION"; exit 0;;
127 -*) echo "Unknown option: $1. Try --help." >&2; exit 1;;
128 *) break;; # Start of COMMAND or LIST
129 esac
130done
131
132# Respond to --help
133if [ "$init_nb_args" -lt 1 ] || [ "$action" = "help" ] ; then
134 show_help
135 exit 1
136fi
137
138
139# Helper functions
140
141# Check the presence of required system packages
142check_package(){
143 nothing_installed=1
144 for package_name in "$@"
145 do
146 if ! dpkg -l "$package_name" &> /dev/null; then
147 echo "Installing $package_name"
148 sudo apt-get install "$package_name"
149 nothing_installed=0
150 fi
151 done
152 return $nothing_installed
153}
154
155# List processes running inside the cgroup
156list_processes(){
157 return_status=1
158 echo -e "PID""\t""CMD"
159 while read task_pid
160 do
161 echo -e "${task_pid}""\t""`ps -p ${task_pid} -o comm=`";
162 return_status=0
163 done < /sys/fs/cgroup/net_cls/${cgroup_name}/tasks
164 return $return_status
165}
166
167# Check and setup iptables - requires root even for check
168iptable_checked=false
169setup_iptables(){
170 if ! sudo iptables -t mangle -C OUTPUT -m cgroup --cgroup "$net_cls_classid" -j MARK --set-mark "$ip_table_fwmark" 2>/dev/null; then
171 echo "Adding iptables MANGLE rule to set firewall mark $ip_table_fwmark on packets with class identifier $net_cls_classid" >&2
172 sudo iptables -t mangle -A OUTPUT -m cgroup --cgroup "$net_cls_classid" -j MARK --set-mark "$ip_table_fwmark"
173 fi
174 if ! sudo iptables -t nat -C POSTROUTING -m cgroup --cgroup "$net_cls_classid" -o "$desired_interface" -j MASQUERADE 2>/dev/null; then
175 echo "Adding iptables NAT rule force the packets with class identifier $net_cls_classid to exit through $desired_interface" >&2
176 sudo iptables -t nat -A POSTROUTING -m cgroup --cgroup "$net_cls_classid" -o "$desired_interface" -j MASQUERADE
177 fi
178 iptable_checked=true
179
180 ip6tables -A OUTPUT -m cgroup --cgroup "$net_cls_classid" -j REJECT
181}
182
183# Test if config is working, ipv4 only
184test_connection(){
185 # Call the configuration function to test if networking is functioning
186 test_networking
187}
188
189
190check_iptables=false
191if [ "$action" = "command" ]
192then
193 # SETUP config
194 if [ "$skip" = false ]; then
195 echo "Checking/setting forced routing config (skip with $0 -s ...)" >&2
196
197 #if check_package cgroupfs-mount cgmanager cgroup-tools inetutils-traceroute; then
198 # echo "You may want to reboot now. But that's probably not necessary." >&2
199 # exit 1
200 #fi
201
202 if dpkg --compare-versions `sudo iptables --version | grep -oP "iptables v\K.*$"` "lt" "1.6"; then
203 echo -e "\e[31mYou need iptables 1.6.0+. Please install manually. Aborting.\e[0m" >&2
204 echo "Find latest iptables at http://www.netfilter.org/projects/iptables/downloads.html" >&2
205 echo "Commands to install iptables 1.6.0:" >&2
206 echo "sudo apt-get install iptables/unstable libxtables11/unstable" >&2
207 echo "... or compile from source as shown below:" >&2
208 echo -e "\e[34msudo apt-get install dh-autoreconf bison flex
209cd /tmp
210curl http://www.netfilter.org/projects/iptables/files/iptables-1.6.0.tar.bz2 | tar xj
211cd iptables-1.6.0
212./configure --prefix=/usr \\
213 --sbindir=/sbin \\
214 --disable-nftables \\
215 --enable-libipq \\
216 --with-xtlibdir=/lib/xtables \\
217&& make \\
218&& sudo make install
219iptables --version\e[0m" >&2
220 exit 1
221 fi
222
223 if [ ! -d "/sys/fs/cgroup/net_cls/$cgroup_name" ]; then
224 echo "Creating net_cls control group $cgroup_name" >&2
225 sudo mkdir -p "/sys/fs/cgroup/net_cls/$cgroup_name"
226 check_iptables=true
227 fi
228 if [ `cat "/sys/fs/cgroup/net_cls/$cgroup_name/net_cls.classid" | xargs -n 1 printf "0x%08x"` != "$net_cls_classid" ]; then
229 echo "Applying net_cls class identifier $net_cls_classid to cgroup $cgroup_name" >&2
230 echo "$net_cls_classid" | sudo tee "/sys/fs/cgroup/net_cls/$cgroup_name/net_cls.classid" > /dev/null
231 fi
232 if ! grep -E "^${ip_table_number}\s+$ip_table_name" /etc/iproute2/rt_tables &>/dev/null; then
233 if grep -E "^${ip_table_number}\s+" /etc/iproute2/rt_tables; then
234 echo "ERROR: Table ${ip_table_number} already exists in /etc/iproute2/rt_tables with a different name than $ip_table_name" >&2
235 exit 1
236 fi
237 echo "Creating ip routing table: number=$ip_table_number name=$ip_table_name" >&2
238 echo "$ip_table_number $ip_table_name" | sudo tee -a /etc/iproute2/rt_tables > /dev/null
239 check_iptables=true
240 fi
241 if ! ip rule list | grep " lookup $ip_table_name" | grep " fwmark " &>/dev/null; then
242 echo "Adding rule to use ip routing table $ip_table_name for packets with firewall mark $ip_table_fwmark" >&2
243 sudo ip rule add fwmark "$ip_table_fwmark" table "$ip_table_name"
244 check_iptables=true
245 fi
246 if [ -z "`ip route list table "$ip_table_name" default via $desired_default_route dev ${desired_interface} 2>/dev/null`" ]; then
247 echo "Adding default route in ip routing table $ip_table_name via $desired_default_route dev $desired_interface" >&2
248 sudo ip route add default via "$desired_default_route" dev "$desired_interface" table "$ip_table_name"
249 # Now run custom post_up script
250 post_up
251 # Useless?
252 echo "Flushing ip route cache" >&2
253 sudo ip route flush cache
254 check_iptables=true
255 fi
256 if [ "`cat /proc/sys/net/ipv4/conf/all/rp_filter`" != "0" ] || [ "`cat /proc/sys/net/ipv4/conf/all/rp_filter`" != "2" ]; then
257 echo "Unset reverse path filtering for interface \"all\"" >&2
258 echo 2 | sudo tee "/proc/sys/net/ipv4/conf/all/rp_filter" > /dev/null
259 check_iptables=true
260 fi
261 if [ "`cat /proc/sys/net/ipv4/conf/${desired_interface}/rp_filter`" != "0" ] || [ "`cat /proc/sys/net/ipv4/conf/${desired_interface}/rp_filter`" != "2" ]; then
262 echo "Unset reverse path filtering for interface \"${desired_interface}\"" >&2
263 echo 2 | sudo tee "/proc/sys/net/ipv4/conf/${desired_interface}/rp_filter" > /dev/null
264 check_iptables=true
265 fi
266 if [ -z "`lscgroup net_cls:$cgroup_name`" ] || [ `stat -c "%U" /sys/fs/cgroup/net_cls/${cgroup_name}/tasks` != "$USER" ]; then
267 echo "Creating cgroup net_cls:${cgroup_name}. User $USER will be able to move tasks to it without root permissions." >&2
268 sudo cgcreate -t "$USER":"$USER" -a `id -g -n "$USER"`:`id -g -n "$USER"` -g net_cls:"$cgroup_name"
269 check_iptables=true
270 fi
271 if [ "$check_iptables" = true ]; then
272 setup_iptables
273 fi
274
275 fi
276
277 # TEST bypass
278 test_connection
279 if [ "$force" != true ]; then
280 if [ "$testresult" = false ]; then
281 if [ "$iptable_checked" = false ]; then
282 echo -e "Testing iptables..." >&2
283 setup_iptables
284 test_connection
285 fi
286 fi
287 if [ "$testresult" = false ]; then
288 exit 1
289 fi
290 fi
291fi
292
293# RUN command
294if [ "$action" = "command" ]; then
295 if [ "$#" -eq 0 ]; then
296 echo "Error: COMMAND not provided." >&2
297 exit 1
298 fi
299 if [ "$background" = true ]; then
300 cgexec -g net_cls:"$cgroup_name" "$@" &>/dev/null &
301 exit 0
302 else
303 cgexec -g net_cls:"$cgroup_name" "$@"
304 exit $?
305 fi
306
307# List processes using this cgroup
308# Exit code 0 (true) if at least 1 process is running in the cgroup
309elif [ "$action" = "list" ]; then
310 echo "List of processes using cgroup $cgroup_name:"
311 list_processes
312 exit $?
313
314
315
316# CLEAN the mess
317elif [ "$action" = "clean" ]; then
318 echo -e "Cleaning forced routing config generated by this script."
319 echo -e "Don't bother with errors meaning there's nothing to remove."
320
321 # Kill tasks in cgroup
322 if [ -f "/sys/fs/cgroup/net_cls/${cgroup_name}/tasks" ]; then
323 while read task_pid; do sudo kill ${task_pid} ; done < "/sys/fs/cgroup/net_cls/${cgroup_name}/tasks"
324 fi
325
326 # Run custom pre_down function
327 pre_down
328
329 # Delete cgroup
330 if [ -d "/sys/fs/cgroup/net_cls/${cgroup_name}" ]; then
331 sudo find "/sys/fs/cgroup/net_cls/${cgroup_name}" -depth -type d -print -exec rmdir {} \;
332 fi
333
334 # (DISABLED BECAUSE MY MACHINE DEFAULTS TO RPF BEING OFF) Re-enable Reverse Path Filtering
335 #echo 1 | sudo tee "/proc/sys/net/ipv4/conf/all/rp_filter" > /dev/null
336 #echo 1 | sudo tee "/proc/sys/net/ipv4/conf/${desired_interface}/rp_filter" > /dev/null
337
338 sudo iptables -t mangle -D OUTPUT -m cgroup --cgroup "$net_cls_classid" -j MARK --set-mark "$ip_table_fwmark"
339 sudo iptables -t nat -D POSTROUTING -m cgroup --cgroup "$net_cls_classid" -o "$desired_interface" -j MASQUERADE
340
341 sudo ip rule del fwmark "$ip_table_fwmark" table "$ip_table_name"
342 sudo ip route del default table "$ip_table_name"
343
344 sudo sed -i '/^${ip_table_number}\s\+${ip_table_name}\s*$/d' /etc/iproute2/rt_tables
345
346 if [ -n "`lscgroup net_cls:$cgroup_name`" ]; then
347 sudo cgdelete net_cls:"$cgroup_name"
348 fi
349
350 echo "All done."
351
352fi
353
354# BONUS: Useful commands:
355# ./altnetworking.sh traceroute www.google.com
356# ip=$(./altnetworking.sh curl 'https://wtfismyip.com/text' 2>/dev/null); echo "$ip"; whois "$ip" | grep -E "inetnum|route|netname|descr"