3 # Marta Carbone, Luigi Rizzo
4 # Copyright (C) 2009 Universita` di Pisa
7 # This script the vsys backend used to configure emulation.
9 # - reads the user's input from the vsys input pipe
10 # - validates the input
11 # - configures the firewall
12 # - writes results on the output vsys pipe
14 # Configurable variables are at the beginning
16 # If HOOK is set the program is called before configuring a rule.
17 # A sample hook can be found in the ipfw.rpm package
18 # HOOK=/tmp/sample_hook
21 # You should not touch anything below.
23 # We assume three type of connections
24 # SERVER we know the local port P, and do the
25 # bind/listen/accept on the local socket.
26 # pipe_in in dst-port P
27 # pipe_out out src-port P
29 # CLIENT we know the remote port P, and do a connect to it
30 # (src and dst are swapped wrt the previous case)
31 # pipe_in in src-port P
32 # pipe_out out dst-port P
34 # SERVICE we run a server on local port P, and also connect
35 # from local clients to remote servers on port P.
36 # pipe_in in { dst-port P or src-port P }
37 # pipe_out out { src-port P or dst-port P }
39 # On a given port a user can have one CLIENT and/or one SERVER
40 # configuration or one SERVICE configuration.
41 # When a SERVICE configuration is installed any existing CLIENT
42 # and SERVER configuration on the same port are removed.
43 # When a CLIENT or SERVER configuration is installed any existing
44 # SERVICE configuration on the same port is removed.
46 # The following is a case that is implemented as SERVER
47 # D we run a server on local port P, and also connect
48 # to remote servers but doing a bind(P) before connect().
49 # In terms of rules, this is not distinguishable from
50 # the SERVER case, however it would be different if we
51 # had a way to tell SERVER from CLIENT sockets
52 # pipe_in in dst-port P
53 # pipe_out out src-port P
55 # The database of current ipfw and dummynet configuration is in a
56 # file which is regenerated on errors. The format is
58 # slice_id type arg rule_base pipe_base timeout
60 # (lines starting with '#' are comments and are ignored)
61 # For each configuration we allocate one rule number in ipfw,
62 # and two sequential pipe numbers.
64 # globals, do not touch below
65 VERBOSE=0 # set to !0 to enable debug messages
66 TEST=0 # set to 1 for test mode
69 lockfile=/var/lock/ipfw.lock
71 # There values are the keys used in the database for rules and pipes
72 # rule_nr 1..10000 are mapped to rules 10000..49999 (n*4+9996)
73 # rule_nr 10001..20000 are mapped to rules 50000..59999 (n+39999)
74 # pipe_nr 1..25000 are mapped to pipes 10000-59999 (n*2+9998)
81 # These are the rule numbers used in ipfw
87 # set slicename and slice_id
88 # there represents the credential of the user
90 SLICE_ID=`id -u $SLICENAME`
91 [ $? != 0 ] && abort "Invalid slicename $SLICENAME"
94 # XXX check consistency for variables {}
97 [ -x ${SED} ] || { SED=`which sed` ; SEDOPT=-E ; }
99 IPFW_CHECK="/sbin/ipfw -n"
101 debug() { # $1 message to be displayed
102 [ x"${VERBOSE}" != x"0" ] && echo "ipfw-be: $1"
104 # if the first argument is -v, enable verbose mode
106 [ x"$1" = x"-v" -o x"$2" = x"-v" ] && VERBOSE=1
107 echo "in set_verbose have $VERBOSE $1"
110 [ x"$1" = x"-q" -o x"$2" = x"-q" ] || return
112 IPFW="/bin/echo ipfw:"
113 IPFW_CHECK="/bin/echo ipfw -n:"
117 abort() { # $1 message to be displayed
119 echo "ipfw-be aborting: $1"
123 # remove dangerous characters from user input
124 # if present, the leading '-v/-t' will be removed
125 filter() { # $* variables to be filtered
126 [ x${1} = x"-v" -o x${1} = x"-q" ] && shift
127 [ x${1} = x"-v" -o x${1} = x"-q" ] && shift
128 # allowed chars are: numbers, uppercase and lowercase letters,
129 # spaces, and the following symbols: .,_-/
130 echo "$*" | ${SED} ${SEDOPT} 's/[^\t0-9a-zA-Z., _\/\{}-]*//g'
133 # remove all entries from the ipfw config, and create an empty db
137 # we would like to delete ranges of rules and pipes but this
138 # is not supported so for the time being we kill them all
140 ${IPFW} -q pipe flush
141 # ${IPFW} delete ${IPFW_RULE_MIN}-${IPFW_RULE_MAX}
142 # ${IPFW} pipe delete ${IPFW_PIPE_MIN}-${IPFW_PIPE_MAX}
145 # Add the ipfw rule/pipe and update the database.
146 # The pipe-in and pipe_out config are through global variables
147 # CONFIG_IN CONFIG_OUT because they may be long.
148 # Other arguments are on the command line
149 add_rule() { # new_rule slice_id type arg rule pipe_base timeout
150 local new_rule=$1 slice_id=$2 type=$3 arg=$4
151 local rule_base=$5 pipe_base=$6 timeout=$7
152 local pipe_in pipe_out rule_in rule_out check_timeout
154 # If we use a profile file, locate the user directory
155 # move in the slice root dir XXX todo
156 [ "$TEST" != "1" ] && cd /vservers/${SLICENAME}/root
157 #echo ${CONFIG_STRING} | ${SED} -e "s/ profile \(.[^ ]\)/ profile \/vservers\/${SLICENAME}\/\1/g"
159 # first, call ipfw -n to check syntax, if ok move on and do the action
160 pipe_in=$(($pipe_base + $pipe_base + 9998))
161 pipe_out=$(($pipe_in + 1))
162 local del # anything to delete ?
163 local rule_nr=$(($rule_base + 39999)) # XXX formula for individual rules
164 if [ x"$new_rule" != x"0" ] ; then
167 rule_in="dst-port $arg"
168 rule_out="src-port $arg"
172 rule_in="src-port $arg"
173 rule_out="dst-port $arg"
177 rule_in="{ src-port $arg or dst-port $arg }"
178 rule_out="{ src-port $arg or dst-port $arg }"
182 abort "invalid service type $type"
186 rule_in="pipe ${pipe_in} in uid $slice_id ${rule_in}"
187 rule_out="pipe ${pipe_out} out uid $slice_id ${rule_out}"
188 ${IPFW_CHECK} add ${rule_nr} $rule_in > /dev/null || \
189 abort "ipfw syntax error $rule_in"
190 ${IPFW_CHECK} add ${rule_nr} $rule_out > /dev/null || \
191 abort "ipfw syntax error $rule_out"
194 # check error reporting
195 ${IPFW_CHECK} pipe ${pipe_in} config ${CONFIG_PIPE_IN} > /dev/null || \
196 abort "ipfw syntax error pipe_in"
197 ${IPFW_CHECK} pipe ${pipe_out} config ${CONFIG_PIPE_OUT} > /dev/null || \
198 abort "ipfw syntax error pipe_out"
200 # all good, delete and add rules if necessary
201 [ "$del" = "service" ] && do_delete $slice_id service $arg
202 [ "$del" = "cli_ser" ] && do_delete $slice_id client $arg
203 [ "$del" = "cli_ser" ] && do_delete $slice_id server $arg
204 [ "$new_rule" != "0" ] && ${IPFW} add ${rule_nr} $rule_in > /dev/null
205 [ "$new_rule" != "0" ] && ${IPFW} add ${rule_nr} $rule_out > /dev/null
207 ${IPFW} pipe ${pipe_in} config ${CONFIG_PIPE_IN}
208 ${IPFW} pipe ${pipe_out} config ${CONFIG_PIPE_OUT}
210 # send output to the user
211 ${IPFW} show ${rule_nr}
212 ${IPFW} pipe ${pipe_in} show
213 ${IPFW} pipe ${pipe_out} show
215 [ "$TEST" = "1" ] && return
216 # add to the database, at least to adjust the timeout
217 ( grep -v -- "^${slice_id} ${type} ${arg}" $DBFILE; \
218 echo "${slice_id} ${type} ${arg} ${rule_base} ${pipe_base} ${timeout}" ) > ${DBFILE}.tmp
219 mv ${DBFILE}.tmp ${DBFILE}
222 # Delete a given configuration
223 do_delete() { # slice_id type arg
224 local pipe_in pipe_out pipe_base rule_base rule_nr
225 local slice_id=$1 type=$2 arg=$3
227 [ "${arg}" = "" ] && abort "Missing arg on 'delete'"
228 set `find_rule $slice_id $type $arg`
229 rule_base=$1; pipe_base=$2
230 [ "$rule_base" = "0" ] && return # no rules found
232 rule_nr=$(($rule_base + 39999)) # XXX only individual rules
233 pipe_in=$(($pipe_base + $pipe_base + 9998))
234 pipe_out=$(($pipe_in + 1))
236 $IPFW delete ${rule_nr}
237 $IPFW pipe delete ${pipe_in}
238 $IPFW pipe delete ${pipe_out}
239 echo "removed configuration $slice_id} ${type} ${arg}"
240 [ "$TEST" = "1" ] && return
241 # remove from the database
242 grep -v -- "^${slice_id} ${type} ${arg}" $DBFILE > ${DBFILE}.tmp
243 mv ${DBFILE}.tmp ${DBFILE}
246 # called with the database file as input
247 # compare the tuple <slice_id type arg> with
248 # the current firewall configuration. The database contains
249 # slice_id type arg rule_base pipe_base timeout
250 # On match returns <rule_base pipe_base timeout>
251 # On non match returns 0 0 0
252 find_rule() { # $1 slice_id $2 type $3 arg
254 ret=`grep -- "^$1 $2 $3 " $DBFILE`
256 [ x"$ret" = x ] && echo "0 0 0 " && return # nothing found
257 # ignore multiple matches. If the db is corrupt we are
264 # Find a hole in a list of numbers within a range (boundaries included)
265 # The input is passed as a sorted list of numbers on stdin.
266 # Return a "0" rule if there is no rule free
267 find_hole() { # min max
268 local min=$1 cand=$1 max=$2 line
270 [ $line -lt $min ] && continue
271 [ $line -ne $cand ] && break # found
272 [ $cand -ge $max ] && cand=0 && break # no space
278 # returns a free rule and pipe base for client|server|service
279 # Returns r=0 if there are no resources available
280 allocate_resources() {
282 # remove comments, extract field, sort
283 p=`grep -v '^#' $DBFILE | awk '{print $5}' | sort -n | \
284 find_hole $PIPE_MIN $PIPE_MAX`
285 r=`grep -v '^#' $DBFILE | awk '{print $4}' | sort -n | find_hole $1 $2`
286 [ $r = 0 -o $p = 0 ] && r=0 # no resources available
291 # A request is made by a set of arguments formatted as follow:
293 # config {server|client|service} arg [-t timeout] PIPE_IN <pipe_conf> PIPE_OUT <pipe_conf>
294 # show {rules|pipes} [args]
297 # XXX not implemented yet
298 # config {rule|pipe} num <parameters>
299 # alloc rules|pipes [-t timeout] # returns a block of NUM_RULES or NUM_PIPES
300 # release rules|pipes args # release the entire block
301 # refresh rules|pipes args [-t timeout]
303 # where uppercase values are keywords.
304 # The timeout value is expressed as:
305 # week, day, month or anything else accepted by the date command.
306 # The id of the slice issuing the request is in the $SLICE_ID variable,
307 # set at the beginning of this script.
310 local timeout TMP i rule_base pipe_base
311 local slicename=${SLICENAME}
313 local debug_args="$*";
314 local type=$1 ; shift
316 debug "Received command: <$cmd> arguments: <$debug_args>"
318 # set the timeout value
319 # clean args from the timeout keyword
320 timeout=`echo ${args} | ${SED} ${SEDOPT} 's/(.+)( -t [a-zA-Z0-9]+ )(.*)/\2/'`
321 if [ "${timeout}" != "${args}" ] ; then # match
322 timeout=`echo ${timeout} | ${SED} ${SEDOPT} 's/-t //'`
323 check_timeout ${timeout} # abort on error
324 args=`echo ${args} | ${SED} ${SEDOPT} 's/(.+)( -t [a-zA-Z0-9]+ )(.*)/\1 \3/'`
326 timeout=1day # default to 1 day
329 debug "Timeout $timeout"
330 # Handle special requests: show and delete
333 abort "XXX unimplemented " && return 0
336 [ "$type" = "server" ] && do_config $SLICE_ID $timeout $type $args && return 0
337 [ "$type" = "client" ] && do_config $SLICE_ID $timeout $type $args && return 0
338 [ "$type" = "service" ] && do_config $SLICE_ID $timeout $type $args && return 0
339 [ "$type" = "rule" ] && abort "XXX unimplemented " && return 0
340 [ "$type" = "pipe" ] && abort "XXX unimplemented " && return 0
341 abort "'config' should be followed by {server|client|service|rule|pipe}"
344 do_delete ${SLICE_ID} $type $args
347 abort "XXX unimplemented " && return 0
348 do_refresh ${SLICE_ID} $type $args $timeout
351 abort "XXX unimplemented " && return 0
352 do_release ${SLICE_ID} $type $args
355 # XXX should filter on uid
356 [ "$type" = "rules" ] && ${IPFW} show && return 0
357 [ "$type" = "pipes" ] && ${IPFW} pipe show && return 0
358 abort "'show' should be followed by {rules|pipes}"
361 # help XXX to be done
362 abort "'command' should be one of {show|config|delete|refresh|release}"
367 # validate the timeout
368 check_timeout() { # timeout
369 local tt=`date --date="${1}" +%s`
370 [ "$?" != "0" ] && abort "Date format $1 not valid"
373 do_release() { # slice_id type args timeout
377 do_refresh() { # slice_id ttype args
381 do_config() { # slice_id timeout type arg PIPE_IN pipe_conf PIPE_OUT pipe_conf
382 local slice_id=$1; shift
383 local timeout=$1; shift
385 local arg=$1; shift # XXX addr not yet implemented
387 [ "$1" != "PIPE_IN" ] && abort "Missing addr:port, or PIPE_IN requested"
390 # read pipe in configuration
392 while [ "$1" != "" -a "$1" != "PIPE_OUT" ] ; do
396 CONFIG_PIPE_IN="$i" # XXX local ?
397 [ "$CONFIG_PIPE_IN" = "" ] && abort "Missing pipe in configuration"
399 [ "$1" != "PIPE_OUT" ] && abort "Missing pipe in configuration, or missing PIPE_OUT"
402 # read pipe out configuration
404 while [ "$1" != "" ] ; do
408 CONFIG_PIPE_OUT="$i" # XXX local ?
409 [ "$CONFIG_PIPE_OUT" = "" ] && abort "Missing pipe out configuration"
411 debug "Configuration Required:"
412 debug "slice_id: $slice_id"
415 debug "timeout: $timeout"
416 debug "PIPE_IN: $CONFIG_PIPE_IN"
417 debug "PIPE_OUT: $CONFIG_PIPE_OUT"
418 debug "-----------------------"
420 # check if the link is already configured
421 debug "Search for ${slice_id} ${type} ${arg}"
423 set `find_rule ${slice_id} ${type} ${arg}`
428 if [ ${rule_base} = "0" ] ; then
429 debug "Rule not found, new installation"
431 set `allocate_resources $RULE_IN_MIN $RULE_IN_MAX`
432 rule_base=$1; pipe_base=$2
433 [ $rule_base = 0 ] && abort "no resources available"
434 debug "found free resources rule: $rule_base pipe: $pipe_base"
436 debug "Rule found, just changing the pipe configuration"
439 add_rule $new_pipe $slice_id $type $arg $rule_base $pipe_base $timeout
441 # if present, call a hook in order to collect statistical
442 # information on dummynet usage
443 if [ -n "${HOOK}" -a -x "${HOOK}" ]; then
445 ${HOOK} $slice_id $type $port $rule_base $pipe_base $timeout &
450 # acquire the lock XXX check lockfile
452 [ "$TEST" = 1 ] && return
453 lockfile -s 0 -r 0 $lockfile 2> /dev/null
454 if [ $? -ne 0 ] ; then
455 echo "lock acquisition failed"
466 # ALLOCATION OF PIPES AND RULES
467 # pipes are always allocated in pairs
468 # rules are either individual or in groups of size NUM_RULES (e.g. 4)
469 # and are allocated in two different parts of the rule namespace
470 # (e.g. blocks from 10000 to 49999 and individuals from 50000 to 59999)
471 # Internally allocator uses the base number for each item, e.g.
472 # rule 10000..49999 -> rule_base=1..10000
473 # rule 50000..59999 -> rule_base=10001..20000
474 # pipe 10000..59999 -> pipe_base=1..25000
475 # a bit of math lets us compute the correct numbers.
476 # For CLIENT, SERVER, SERVICE the database contains entries as
477 # XID TYPE arg rule_base pipe_base
478 # For blocks the entries are
479 # XID RULE - rule_base -
480 # XID PIPE - - pipe_base
481 # When a rule or pipe is referenced we first check that the owner owns it.
482 # more details below.
485 debug "--- $0 START for $SLICENAME ---"
487 # If the db does not exist, create it and we clean rules and pipes
488 [ ! -e ${DBFILE} ] && clean_db
490 # A request to the vsys backend is composed by a single line of input
491 read REQ # read one line, ignore the rest
493 set_verbose ${REQ} # use inital -v if present
494 set_test ${REQ} # use inital -t if present
495 REQ="`filter ${REQ}`" # remove -v and -t and invalid chars
496 debug "--- processing <${REQ}>"
497 acquire_lock # critical section
500 debug "--- $0 END ---"