#!/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 DEBUG=0 # set to 0 to disable debug messages # if HOOK is set the program is called befor 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 error. # The format is # # slice_id service_type port rule_nr 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 DBFILE=/tmp/ff LOG_FILE=/tmp/netconfig.log # XXX when running from daemon lockfile=/var/lock/ipfw.lock PIPE_MIN=1000 PIPE_MAX=30000 # programs # XXX check consintency variable {} SED=/bin/sed #IPFW="/bin/echo ipfw:" IPFW=/sbin/ipfw # Call arguments are SLICENAME="$1" # save the slice XXX name or id ? SLICE_ID=`id -u $SLICENAME` debug() { # $1 message to be displayed #echo "ipfw-be: $1" [ x"${DEBUG}" != x"0" ] && echo "ipfw-be: $1" >>{LOG_FILE}; } abort() { # $1 message to be displayed release_lock echo "ipfw-be aborting: $1" exit 1 } user_error() { # $1 message to be displayed echo "ipfw-be: user error: $1" exit 1 } # remove dangerous characters from user input filter() { # $* variables to be filtered # allowed chars are: numbers, upcase and lowecase # chars, and the following symbols: . _ - / echo "$*" | ${SED} -r 's/[^0-9a-zA-Z. _\/\-{}]*//g' } # 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 port rule pipe_base timeout local new_rule=$1 slice_id=$2 type=$3 port=$4 rule_nr=$5 pipe_base=$6 timeout=$7 local pipe_in pipe_out rule_in rule_out check_timeout # XXX validate the timeout # schedule the rule deletion check_timeout=`date --date="${timeout}" +%s` [ x"${check_timeout}" = x"" ] && abort "Date format $1 not valid" # XXX tbd timeout="fake_timeout" # we could use a profile, so locate the user directory # move in the slice root dir XXX todo cd /vservers/${SLICENAME}/root #echo ${CONFIG_STRING} | ${SED} -e "s/ profile \(.[^ ]\)/ profile \/vservers\/${SLICENAME}\/\1/g" # first, call ipfw -n to check syntax # check syntax, if ok move on and do the action local IPFW_CHECK="${IPFW} -n " pipe_in=$(($pipe_base + $pipe_base)) pipe_out=$(($pipe_in + 1)) local del # which one to delete ? if [ x"$new_rule" != x"0" ] ; then case $type in SERVER) rule_in="dst-port $port" rule_out="src-port $port" del=SERVICE ;; CLIENT) rule_in="src-port $port" rule_out="dst-port $port" del=SERVICE ;; SERVICE) rule_in="{ src-port $port or dst-port $port }" rule_out="{ src-port $port or dst-port $port }" 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 || \ user_error "ipfw syntax error $rule_in" ${IPFW_CHECK} add ${rule_nr} $rule_out || \ user_error "ipfw syntax error $rule_out" fi # XXX check error reporting ${IPFW_CHECK} pipe ${pipe_in} config ${CONFIG_PIPE_IN} || \ user_error "ipfw syntax error pipe_in" ${IPFW_CHECK} pipe ${pipe_out} config ${CONFIG_PIPE_OUT} || \ user_error "ipfw syntax error pipe_out" # all good, delete and add rules if necessary [ "$del" = "SERVICE" ] && delete_config $slice_id SERVICE $port [ "$del" = "CLI_SER" ] && delete_config $slice_id CLIENT $port [ "$del" = "CLI_SER" ] && delete_config $slice_id SERVER $port [ "$new_rule" != "0" ] && ${IPFW} add ${rule_nr} $rule_in [ "$new_rule" != "0" ] && ${IPFW} add ${rule_nr} $rule_out # config pipes ${IPFW} pipe ${pipe_in} config ${CONFIG_PIPE_IN} ${IPFW} pipe ${pipe_out} config ${CONFIG_PIPE_OUT} # add to the database, at least to adjust the timeout ( grep -v -- "^${slice_id} ${type} ${port}" $DBFILE; \ echo "${slice_id} ${type} ${port} ${rule_nr} ${pipe_base} ${timeout}" ) > ${DBFILE}.tmp mv ${DBFILE}.tmp ${DBFILE} } # Delete a given configuration delete_config() { # slice_id type port local pipe_in pipe_out pipe_base local slice_id=$1 type=$2 port=$3 # XXX test [ $# -lt 3 ] && abort "One or more input parameter is missing" set `find_rule $slice_id $type $port` rule=$1; pipe_base=$2 [ "$rule" = "0" ] && return # no rules found pipe_in=$(($pipe_base + $pipe_base)) pipe_out=$(($pipe_in + 1)) $IPFW delete ${rule} $IPFW pipe delete ${pipe_in} $IPFW pipe delete ${pipe_out} # remove from the database grep -v -- "^${slice_id} ${type} ${port}" $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 local_port remote_port rule_nr pipe_nr timeout # On match returns # On non match returns 0 0 0 find_rule() { # $1 slice_id $2 type $3 port 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 # Returns r=0 if there are no resources available # # This function returns values using echo, # this means that we can not easily debug the function allocate_resources() { local p r # remove comments, extract field, sort p=`grep -v '^#' $DBFILE | awk '{print $5}' | sort -n | find_hole 1 10000` r=`grep -v '^#' $DBFILE | awk '{print $4}' | sort -n | find_hole $PIPE_MIN $PIPE_MAX` [ $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 ${type} ${port} ${timeout} PIPE_IN PIPE_OUT # IPFW_SHOW # PIPE_SHOW # DELETE ${type} ${port} # # 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_nr pipe_base local type=$2 local port=$3 debug "Received from the input pipe: $*" # Handle special requests: show and delete case x"$1" in x"IPFW_SHOW") ${IPFW} show return 0 ;; x"PIPE_SHOW") $IPFW pipe show return 0 ;; x"DELETE") delete_config ${SLICE_ID} $type $port return 0 ;; x"CONFIG") ;; *) abort "Command not recognized" ;; esac shift debug "processed initial command, rest of line: $*" # check if we have enough parameters [ $# -lt 9 ] && abort "One or more input parameter is missing" type=$1; shift port=$1; shift timeout=$1; shift # XXX check/compute timeout [ "$1" != "PIPE_IN" ] && abort "PIPE_IN requested" shift i="" while [ "$1" != "" -a "$1" != "PIPE_OUT" ] ; do i="$i $1" shift done CONFIG_PIPE_IN="$i" [ "$1" != "PIPE_OUT" ] && abort "PIPE_OUT requested" shift i="" while [ "$1" != "" ] ; do i="$i $1" shift done CONFIG_PIPE_OUT="$i" debug "Configuration Required:" debug "TYPE: $type" debug "PORT: $port" debug "TIMEOUT: $timeout" debug "pipe in config $CONFIG_PIPE_IN" debug "pipe in config $CONFIG_PIPE_OUT" debug "-----------------------" # check if the link is already configured debug "Search for ${SLICE_ID} ${type} ${port}" set `find_rule ${SLICE_ID} ${type} ${port}` rule_nr=$1 pipe_base=$2 if [ ${rule_nr} = "0" ] ; then debug "Rule not found, new installation" new_pipe=1 set `allocate_resources` rule_nr=$1; pipe_base=$2 [ $rule_nr = 0 ] && abort "no resources available" debug "found free resources rule: $rule_nr pipe: $pipe_base" else debug "Rule found, just changing the pipe configuration" fi add_rule $new_pipe $SLICE_ID $type $port $rule_nr $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_nr $pipe_base $timeout & fi } # # acquire the lock XXX check lockfile acquire_lock() { 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 } # main starts here debug "Debug activated" debug "$0 START" # create the DBFILE if not exist [ ! -e ${DBFILE} ] && touch ${DBFILE} requests=[] i=0 # lock acquisition acquire_lock # A request to the vsys backend is composed by a single line of input while read request; do # read -a read arguments in array # XXX skip lines starting with # debug "Received <$request>" requests[$i]="$request" requests[$i]=`filter $request` debug "Filtered ${requests[$i]}" i=$(($i + 1)) done # process requests i=0 n_req=${#requests[*]} debug "Received $n_req request" while [ $i -lt $n_req ] ; do debug "processing request $i of $n_req" debug "<${requests[$i]}>" process ${requests[$i]} i=$(($i + 1)) done # lock release release_lock debug "$0 END" exit 0