#!/bin/sh # # Marta Carbone, Luigi Rizzo # Copyright (C) 2009 Universita` di Pisa # $Id$ # # This script the vsys backend used to configure emulation. # In detail it: # - reads the user's input from the vsys input pipe # - validates the input # - configures the firewall # - writes results on the output vsys pipe # # Configurable variables are at the beginning # If HOOK is set the program is called before configuring a rule. # A sample hook can be found in the ipfw.rpm package # HOOK=/tmp/sample_hook # XXX HOOK="" # You should not touch anything below. # We assume three type of connections # SERVER we know the local port P, and do the # bind/listen/accept on the local socket. # pipe_in in dst-port P # pipe_out out src-port P # # CLIENT we know the remote port P, and do a connect to it # (src and dst are swapped wrt the previous case) # pipe_in in src-port P # pipe_out out dst-port P # # SERVICE we run a server on local port P, and also connect # from local clients to remote servers on port P. # pipe_in in { dst-port P or src-port P } # pipe_out out { src-port P or dst-port P } # # On a given port a user can have one CLIENT and/or one SERVER # configuration or one SERVICE configuration. # When a SERVICE configuration is installed any existing CLIENT # and SERVER configuration on the same port are removed. # When a CLIENT or SERVER configuration is installed any existing # SERVICE configuration on the same port is removed. # # The following is a case that is implemented as SERVER # D we run a server on local port P, and also connect # to remote servers but doing a bind(P) before connect(). # In terms of rules, this is not distinguishable from # the SERVER case, however it would be different if we # had a way to tell SERVER from CLIENT sockets # pipe_in in dst-port P # pipe_out out src-port P # # The database of current ipfw and dummynet configuration is in a # file which is regenerated on errors. The format is # # slice_id type arg rule_base pipe_base timeout # # (lines starting with '#' are comments and are ignored) # For each configuration we allocate one rule number in ipfw, # and two sequential pipe numbers. # globals, do not touch below VERBOSE=0 # set to !0 to enable debug messages TEST=0 # set to 1 for test mode DBFILE=/tmp/ff lockfile=/var/lock/ipfw.lock # There values are the keys used in the database for rules and pipes # rule_nr 1..10000 are mapped to rules 10000..49999 (n*4+9996) # rule_nr 10001..20000 are mapped to rules 50000..59999 (n+39999) # pipe_nr 1..25000 are mapped to pipes 10000-59999 (n*2+9998) RULE_BL_MIN=1 RULE_BL_MAX=10000 RULE_IN_MIN=10001 RULE_IN_MAX=20000 PIPE_MIN=1 PIPE_MAX=25000 # These are the rule numbers used in ipfw IPFW_RULE_MIN=10000 IPFW_RULE_MAX=59999 IPFW_PIPE_MIN=10000 IPFW_PIPE_MAX=59999 # set slicename and slice_id # there represents the credential of the user SLICENAME=$1 SLICE_ID=`id -u $SLICENAME` [ $? != 0 ] && abort "Invalid slicename $SLICENAME" # programs # XXX check consistency for variables {} SED=/bin/sed SEDOPT=-r [ -x ${SED} ] || { SED=`which sed` ; SEDOPT=-E ; } IPFW=/sbin/ipfw IPFW_CHECK="/sbin/ipfw -n" debug() { # $1 message to be displayed [ x"${VERBOSE}" != x"0" ] && echo "ipfw-be: $1" } # if the first argument is -v, enable verbose mode set_verbose() { [ x"$1" = x"-v" -o x"$2" = x"-v" ] && VERBOSE=1 } set_test() { [ x"$1" = x"-q" -o x"$2" = x"-q" ] || return TEST=1 IPFW="/bin/echo ipfw:" IPFW_CHECK="/bin/echo ipfw -n:" } abort() { # $1 message to be displayed release_lock echo "ipfw-be aborting: $1" exit 1 } # remove dangerous characters from user input # if present, the leading '-v/-t' will be removed filter() { # $* variables to be filtered [ x${1} = x"-v" -o x${1} = x"-q" ] && shift [ x${1} = x"-v" -o x${1} = x"-q" ] && shift # allowed chars are: numbers, uppercase and lowercase letters, # spaces, and the following symbols: .,_-/ echo "$*" | ${SED} ${SEDOPT} 's/[^\t0-9a-zA-Z., _\/\{}-]*//g' } # remove all entries from the ipfw config, and create an empty db clean_db() { rm -f ${DBFILE} touch ${DBFILE} # we would like to delete ranges of rules and pipes but this # is not supported so for the time being we kill them all ${IPFW} -q flush ${IPFW} -q pipe flush # ${IPFW} delete ${IPFW_RULE_MIN}-${IPFW_RULE_MAX} # ${IPFW} pipe delete ${IPFW_PIPE_MIN}-${IPFW_PIPE_MAX} } # Add the ipfw rule/pipe and update the database. # The pipe-in and pipe_out config are through global variables # CONFIG_IN CONFIG_OUT because they may be long. # Other arguments are on the command line add_rule() { # new_rule slice_id type arg rule pipe_base timeout local new_rule=$1 slice_id=$2 type=$3 arg=$4 local rule_base=$5 pipe_base=$6 timeout=$7 local pipe_in pipe_out rule_in rule_out check_timeout # If we use a profile file, locate the user directory # move in the slice root dir XXX todo [ "$TEST" != "1" ] && cd /vservers/${SLICENAME}/root #echo ${CONFIG_STRING} | ${SED} -e "s/ profile \(.[^ ]\)/ profile \/vservers\/${SLICENAME}\/\1/g" # first, call ipfw -n to check syntax, if ok move on and do the action pipe_in=$(($pipe_base + $pipe_base + 9998)) pipe_out=$(($pipe_in + 1)) local del # anything to delete ? local rule_nr=$(($rule_base + 39999)) # XXX formula for individual rules if [ x"$new_rule" != x"0" ] ; then case $type in server) rule_in="dst-port $arg" rule_out="src-port $arg" del=service ;; client) rule_in="src-port $arg" rule_out="dst-port $arg" del=service ;; service) rule_in="{ src-port $arg or dst-port $arg }" rule_out="{ src-port $arg or dst-port $arg }" del="cli_ser" ;; *) abort "invalid service type $type" ;; esac rule_in="pipe ${pipe_in} in uid $slice_id ${rule_in}" rule_out="pipe ${pipe_out} out uid $slice_id ${rule_out}" ${IPFW_CHECK} add ${rule_nr} $rule_in > /dev/null || \ abort "ipfw syntax error $rule_in" ${IPFW_CHECK} add ${rule_nr} $rule_out > /dev/null || \ abort "ipfw syntax error $rule_out" fi # check error reporting ${IPFW_CHECK} pipe ${pipe_in} config ${CONFIG_PIPE_IN} > /dev/null || \ abort "ipfw syntax error pipe_in" ${IPFW_CHECK} pipe ${pipe_out} config ${CONFIG_PIPE_OUT} > /dev/null || \ abort "ipfw syntax error pipe_out" # all good, delete and add rules if necessary [ "$del" = "service" ] && do_delete $slice_id service $arg [ "$del" = "cli_ser" ] && do_delete $slice_id client $arg [ "$del" = "cli_ser" ] && do_delete $slice_id server $arg [ "$new_rule" != "0" ] && ${IPFW} add ${rule_nr} $rule_in > /dev/null [ "$new_rule" != "0" ] && ${IPFW} add ${rule_nr} $rule_out > /dev/null # config pipes ${IPFW} pipe ${pipe_in} config ${CONFIG_PIPE_IN} ${IPFW} pipe ${pipe_out} config ${CONFIG_PIPE_OUT} # send output to the user ${IPFW} show ${rule_nr} ${IPFW} pipe ${pipe_in} show ${IPFW} pipe ${pipe_out} show [ "$TEST" = "1" ] && return # add to the database, at least to adjust the timeout ( grep -v -- "^${slice_id} ${type} ${arg} " $DBFILE; \ echo "${slice_id} ${type} ${arg} ${rule_base} ${pipe_base} ${timeout}" ) > ${DBFILE}.tmp mv ${DBFILE}.tmp ${DBFILE} } # Delete a given configuration do_delete() { # slice_id type arg local pipe_in pipe_out pipe_base rule_base rule_nr local slice_id=$1 type=$2 arg=$3 [ "${arg}" = "" ] && abort "Missing arg on 'delete'" set `find_rule $slice_id $type $arg` rule_base=$1; pipe_base=$2 [ "$rule_base" = "0" ] && return # no rules found rule_nr=$(($rule_base + 39999)) # XXX only individual rules pipe_in=$(($pipe_base + $pipe_base + 9998)) pipe_out=$(($pipe_in + 1)) $IPFW delete ${rule_nr} $IPFW pipe delete ${pipe_in} $IPFW pipe delete ${pipe_out} echo "removed configuration $slice_id} ${type} ${arg}" [ "$TEST" = "1" ] && return # remove from the database grep -v -- "^${slice_id} ${type} ${arg} " $DBFILE > ${DBFILE}.tmp mv ${DBFILE}.tmp ${DBFILE} } # called with the database file as input # compare the tuple with # the current firewall configuration. The database contains # slice_id type arg rule_base pipe_base timeout # On match returns # On non match returns 0 0 0 find_rule() { # $1 slice_id $2 type $3 arg local ret ret=`grep -- "^$1 $2 $3 " $DBFILE` [ x"$ret" = x ] && echo "0 0 0 " && return # nothing found # ignore multiple matches. If the db is corrupt we are # screwed anyways set $ret echo "$4 $5 $6" } # Find a hole in a list of numbers within a range (boundaries included) # The input is passed as a sorted list of numbers on stdin. # Return a "0" rule if there is no rule free find_hole() { # min max local min=$1 cand=$1 max=$2 line while read line ; do [ $line -lt $min ] && continue [ $line -ne $cand ] && break # found [ $cand -ge $max ] && cand=0 && break # no space cand=$(($cand + 1)) done echo $cand } # returns a free rule and pipe base for client|server|service # Returns r=0 if there are no resources available allocate_resources() { local p r # remove comments, extract field, sort p=`grep -v '^#' $DBFILE | awk '{print $5}' | sort -n | \ find_hole $PIPE_MIN $PIPE_MAX` r=`grep -v '^#' $DBFILE | awk '{print $4}' | sort -n | find_hole $1 $2` [ $r = 0 -o $p = 0 ] && r=0 # no resources available echo $r $p } # process a request. # A request is made by a set of arguments formatted as follow: # # config {server|client|service} arg [-t timeout] PIPE_IN PIPE_OUT # show {rules|pipes} [args] # delete type arg # # XXX not implemented yet # config {rule|pipe} num # alloc rules|pipes [-t timeout] # returns a block of NUM_RULES or NUM_PIPES # release rules|pipes args # release the entire block # refresh rules|pipes args [-t timeout] # # where uppercase values are keywords. # The timeout value is expressed as: # week, day, month or anything else accepted by the date command. # The id of the slice issuing the request is in the $SLICE_ID variable, # set at the beginning of this script. process() { local new_pipe=0 local timeout TMP i rule_base pipe_base local slicename=${SLICENAME} local cmd=$1 ; shift local debug_args="$*"; local type=$1 ; shift local args="$*" debug "Received command: <$cmd> arguments: <$debug_args>" # set the timeout value # clean args from the timeout keyword timeout=`echo ${args} | ${SED} ${SEDOPT} 's/(.+)( -t [a-zA-Z0-9]+ )(.*)/\2/'` if [ "${timeout}" != "${args}" ] ; then # match timeout=`echo ${timeout} | ${SED} ${SEDOPT} 's/-t //'` check_timeout ${timeout} # abort on error args=`echo ${args} | ${SED} ${SEDOPT} 's/(.+)( -t [a-zA-Z0-9]+ )(.*)/\1 \3/'` else timeout=1day # default to 1 day fi debug "Timeout $timeout" # Handle special requests: show and delete case x"$cmd" in x"alloc") abort "XXX unimplemented " && return 0 ;; x"config") [ "$type" = "server" ] && do_config $SLICE_ID $timeout $type $args && return 0 [ "$type" = "client" ] && do_config $SLICE_ID $timeout $type $args && return 0 [ "$type" = "service" ] && do_config $SLICE_ID $timeout $type $args && return 0 [ "$type" = "rule" ] && abort "XXX unimplemented " && return 0 [ "$type" = "pipe" ] && abort "XXX unimplemented " && return 0 abort "'config' should be followed by {server|client|service|rule|pipe}" ;; x"delete") do_delete ${SLICE_ID} $type $args ;; x"refresh") abort "XXX unimplemented " && return 0 do_refresh ${SLICE_ID} $type $args $timeout ;; x"release") abort "XXX unimplemented " && return 0 do_release ${SLICE_ID} $type $args ;; x"show") # XXX should filter on uid [ "$type" = "rules" ] && ${IPFW} show && return 0 [ "$type" = "pipes" ] && ${IPFW} pipe show && return 0 abort "'show' should be followed by {rules|pipes}" ;; *) # help XXX to be done abort "'command' should be one of {show|config|delete|refresh|release}" ;; esac } # validate the timeout check_timeout() { # timeout local tt=`date --date="${1}" +%s` [ "$?" != "0" ] && abort "Date format $1 not valid" } do_release() { # slice_id type args timeout return } do_refresh() { # slice_id ttype args return } do_config() { # slice_id timeout type arg PIPE_IN pipe_conf PIPE_OUT pipe_conf local slice_id=$1; shift local timeout=$1; shift local type=$1; shift local arg=$1; shift # XXX addr not yet implemented [ "$1" != "PIPE_IN" ] && abort "Missing addr:port, or PIPE_IN requested" shift # read pipe in configuration i="" while [ "$1" != "" -a "$1" != "PIPE_OUT" ] ; do i="$i $1" shift done CONFIG_PIPE_IN="$i" # XXX local ? [ "$CONFIG_PIPE_IN" = "" ] && abort "Missing pipe in configuration" [ "$1" != "PIPE_OUT" ] && abort "Missing pipe in configuration, or missing PIPE_OUT" shift # read pipe out configuration i="" while [ "$1" != "" ] ; do i="$i $1" shift done CONFIG_PIPE_OUT="$i" # XXX local ? [ "$CONFIG_PIPE_OUT" = "" ] && abort "Missing pipe out configuration" debug "Configuration Required:" debug "slice_id: $slice_id" debug "type: $type" debug "arg: $arg" debug "timeout: $timeout" debug "PIPE_IN: $CONFIG_PIPE_IN" debug "PIPE_OUT: $CONFIG_PIPE_OUT" debug "-----------------------" # check if the link is already configured debug "Search for ${slice_id} ${type} ${arg}" set `find_rule ${slice_id} ${type} ${arg}` local rule_base=$1 local pipe_base=$2 local new_pipe=0 if [ ${rule_base} = "0" ] ; then debug "Rule not found, new installation" new_pipe=1 set `allocate_resources $RULE_IN_MIN $RULE_IN_MAX` rule_base=$1; pipe_base=$2 [ $rule_base = 0 ] && abort "no resources available" debug "found free resources rule: $rule_base pipe: $pipe_base" else debug "Rule found, just changing the pipe configuration" fi add_rule $new_pipe $slice_id $type $arg $rule_base $pipe_base $timeout # if present, call a hook in order to collect statistical # information on dummynet usage if [ -n "${HOOK}" -a -x "${HOOK}" ]; then # XXX ${HOOK} $slice_id $type $port $rule_base $pipe_base $timeout & fi } # # acquire the lock XXX check lockfile acquire_lock() { [ "$TEST" = 1 ] && return lockfile -s 0 -r 0 $lockfile 2> /dev/null if [ $? -ne 0 ] ; then echo "lock acquisition failed" exit -1 fi } # # release the lock release_lock() { rm -f $lockfile } # ALLOCATION OF PIPES AND RULES # pipes are always allocated in pairs # rules are either individual or in groups of size NUM_RULES (e.g. 4) # and are allocated in two different parts of the rule namespace # (e.g. blocks from 10000 to 49999 and individuals from 50000 to 59999) # Internally allocator uses the base number for each item, e.g. # rule 10000..49999 -> rule_base=1..10000 # rule 50000..59999 -> rule_base=10001..20000 # pipe 10000..59999 -> pipe_base=1..25000 # a bit of math lets us compute the correct numbers. # For CLIENT, SERVER, SERVICE the database contains entries as # XID TYPE arg rule_base pipe_base # For blocks the entries are # XID RULE - rule_base - # XID PIPE - - pipe_base # When a rule or pipe is referenced we first check that the owner owns it. # more details below. #-- main starts here debug "--- $0 START for $SLICENAME ---" # If the db does not exist, create it and we clean rules and pipes [ ! -e ${DBFILE} ] && clean_db # A request to the vsys backend is composed by a single line of input read REQ # read one line, ignore the rest set_verbose ${REQ} # use inital -v if present set_test ${REQ} # use inital -t if present REQ="`filter ${REQ}`" # remove -v and -t and invalid chars debug "--- processing <${REQ}>" acquire_lock # critical section process ${REQ} release_lock debug "--- $0 END ---" exit 0