X-Git-Url: http://git.onelab.eu/?a=blobdiff_plain;f=exec%2Fipfw-be;h=b3b9439b51301efd24e3113d32fdd97e3e80c307;hb=0d2dadd8ffce4fb1f1de7270766b65d748a04b30;hp=b84120099b6a2f0cdc5b1e7a78c37ade2dfa1709;hpb=7d453500e899ba9181d9799139fdce9d3e17d36d;p=vsys-scripts.git diff --git a/exec/ipfw-be b/exec/ipfw-be index b841200..b3b9439 100755 --- a/exec/ipfw-be +++ b/exec/ipfw-be @@ -1,217 +1,811 @@ #!/bin/sh # -# Marta Carbone -# Copyright (C) 2009 UniPi +# Marta Carbone, Luigi Rizzo +# Copyright (C) 2009 Universita` di Pisa # $Id$ # -# This script is the backend to be used with -# the vsys system. -# It allows to configure dummynet pipes and queues. +# This script the vsys backend used to configure emulation. # In detail it: -# - read the user's input from the input pipe -# - validate the input -# - set the firewall -# - put results on the output vsys pipe +# - reads the user's input from the vsys input pipe +# - validates the input +# - configures the firewall +# - writes results on the output vsys pipe # -# This script expect to read from the input vsys -# pipe a line formatted as follow: -# ${PORT} ${TIMEOUT} -# the timeout value is expressed as: -# week, day, month or anything else accepted by the date command - -# save the slicename -SLICE=$1 - -LOG_FILE=/tmp/netconfig.log +# Configurable variables are at the beginning (only HOOK so far) + +# If HOOK is set, ${HOOK} is called before configuring a rule. +# A sample hook can be found in the ipfwroot.rpm package, +# it can be used to collect statistical information on dummynet usage. +# To configure a hook, set the HOOK variable as follow: +# HOOK=/tmp/sample_hook + +#--- You should not touch anything below this line. ---- +# For documentation see ARCHITECTURE near the end of the file. + +#--- global variables --- +VERBOSE=0 # set to !0 to enable debug messages +TEST=0 # set to 1 for test mode + +# The database and the lock file +DBFILE=/tmp/ff +lockfile=/var/lock/ipfw.lock + +# Min and max value (inclusive) for block_index +BLOCK_MIN=1 +BLOCK_MAX=1000 +M=50 # size of per-slice block of rules +# Min and max value (inclusive) for pipe_index +PIPE_MIN=1 +PIPE_MAX=25000 + +# These are the actual rule numbers used in ipfw +IPFW_RULE_MIN=10000 # initial per-slice rule number +IPFW_PIPE_MIN=10000 # initial pipe number + +# The skipto and the generic default rule +# these values are used to initialize the firewall +SLICE_TABLE=1 # table number used for slice ids lookup +S=1000 # firewall rule number for the skipto rule +D=2000 # default rule for reserved section + +# set slicename and slice_id +# these are the credential of the user invoking the backend +SLICENAME=$1 +SLICE_ID=`id -u $SLICENAME` +[ x"$SLICE_ID" = x"" ] && echo "No sliver present." && exit # programs -CUT=/usr/bin/cut +# XXX check consistency for variables {} SED=/bin/sed +SEDOPT=-r +[ -x ${SED} ] || { SED=`which sed` ; SEDOPT=-E ; } IPFW=/sbin/ipfw - -# set to 0 to disable debug messages -DEBUG=0 +IPFW_CHECK="/sbin/ipfw -n" debug() { # $1 message to be displayed - [ x"${DEBUG}" != x"0" ] && echo $1 >>{LOG_FILE}; + [ x"${VERBOSE}" != x"0" ] && echo "ipfw-be: $1" } -abort() { # $1 message to be displayed - echo "$1" - exit 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 mode if -q is found +set_test() { + [ x"$1" = x"-q" -o x"$2" = x"-q" ] || return + TEST=1 + IPFW="/bin/echo ipfw:" + IPFW_CHECK="/bin/echo ipfw -n:" } -user_error() { # $1 message to be displayed - echo "1 User error: $1" +abort() { # $1 message to be displayed in case of error + release_lock + echo "ipfw-be aborting (netconfig help): $1" exit 1 } +# remove dangerous characters from user input +# if present, the leading '-v/-q' will be removed 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 ipfw pipe and rules -# We use the PORT number to configure the -# pipe, and add rules for that port. -# The default directory is the slicename root -add_rules() { # $1 timeout value $2 delete - local EXPIRE - - debug "Add a new rule, check for deletion flag"; - if [ ${2} -eq 1 ]; then - #echo "Rules and pipes deleted"; - return; - fi + [ 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' +} - debug "Add a new rule" - # schedule the rule deletion - EXPIRE=`date --date="${TIMEOUT}" +%s` - [ x"${EXPIRE}" = x"" ] && abort "Date format $1 not valid" - - # move in the slice root dir - cd /vservers/${SLICE}/root - #echo ${CONFIG_STRING} | ${SED} -e "s/ profile \(.[^ ]\)/ profile \/vservers\/${SLICE}\/\1/g" - - # check syntax, if ok execute - # add rules - local IPFW_CHECK="${IPFW} -n " - local ERROR=0 - - [ $ERROR -eq 0 ] && \ - ${IPFW_CHECK} add ${RULE_N} pipe ${PIPE_N} ip from ${ME} to any src-port ${PORT} // ${EXPIRE} ${SLICE} - let "ERROR += $?" - [ $ERROR -eq 0 ] && \ - ${IPFW_CHECK} add ${RULE_N} pipe ${PIPE_N} ip from any to ${ME} dst-port ${PORT} - - let "ERROR += $?" - [ $ERROR -eq 0 ] && \ - ${IPFW_CHECK} pipe ${PIPE_N} config ${CONFIG_STRING} - - if [ ! $ERROR -eq 0 ]; then - echo "Some errors occurred not executing" - user_error "ipfw syntax error" - fi - - # add rules - ${IPFW} add ${RULE_N} pipe ${PIPE_N} ip from ${ME} to any src-port ${PORT} // ${EXPIRE} ${SLICE} - ${IPFW} add ${RULE_N} pipe ${PIPE_N} ip from any to ${ME} dst-port ${PORT} - - # config pipe - ${IPFW} pipe ${PIPE_N} config ${CONFIG_STRING} -} - -# Delete a given link -delete_link() -{ - ipfw delete ${RULE_N} - ipfw pipe delete ${RULE_N} -} - -# The rule we want to configure already exist. -# Check for slice owner matching. -modify_rule() -{ - local RULE - - RULE=`ipfw list ${PORT} 2>&1 | cut -d ' ' -f 12`; - if [ "${RULE}" = "${SLICE}" ] ; then # replace the link configuration - debug "The rule already exist, the owner match, delete old rule" - echo "Owner match" - delete_link - add_rules ${TIMEOUT} ${DELETE} - else - user_error "the rule already exist, ant you are not the slice owner, try later" - fi -} - -# process a single line of input -# this line has the following format: -# ipfw -# pipe -# port timeout configuration_string -process() -{ - local TMP; # temporary var - - debug "Received from the input pipe: $1" - - # allow netconfig ipfw show - # allow netconfig pipe show - - CMD=`echo $1 | cut -d\ -f 1` - if [ x${CMD} == x"ipfw" ]; then - ipfw show - return 0 - else if [ x${CMD} == x"pipe" ]; then - ipfw pipe show - return 0 - fi - fi +# 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} -q table $SLICE_TABLE flush + #${IPFW} delete ${IPFW_RULE_MIN}-${IPFW_RULE_MAX} + #${IPFW} pipe delete ${IPFW_PIPE_MIN}-${IPFW_PIPE_MAX} + # since all rules are now deleted, we should initialize the firewall + ipfw_init +} - ARGS=`echo $1 | wc -w` - if [ $ARGS -le 2 ]; then - abort "One or more input parameter is missing" - fi +# +# Add the ipfw rule/pipe and update the database. +# The pipe-in and pipe-out config are through global variables +# rule_in rule_out because they may be long. XXX why ? +# Other arguments are on the command line +# +# the new_rule variable is set if the rule to be installed is new +# we need to know this because we do not want to clean +# rule counters on pipes reconfiguration +add_rule() { # slice_id new_rule type arg ipfw_rule pipe_index timeout + local slice_id=$1 new_rule=$2 type=$3 arg=$4 + local ipfw_rule=$5 pipe_index=$6 timeout=$7 + local ipfw_pipe_in ipfw_pipe_out check_timeout + local p h # used to split the argument + + local h_in h_out + # local rule_in rule_out # XXX test if this works + # find actual pipe numbers + ipfw_pipe_in=$(($IPFW_PIPE_MIN + $((2 * $(($pipe_index - 1)))) )) + ipfw_pipe_out=$(($ipfw_pipe_in + 1)) + local del # used to delete incompatible configurations + + # split the argument, and prepare PORTLIST (p) and ADDRLIST (h) + p=`echo $arg | cut -s -d "@" -f1-` # empty if no separator + if [ "$p" = "" ] ; then + p=$arg + else + p=`echo $arg | cut -d "@" -f1` + h=`echo $arg | cut -d "@" -f2` + fi + + if [ "$h" = "" ] ; then + h_in="" + h_out="" + else + h_in=" src-ip ${h} " + h_out=" dst-ip ${h} " + fi + + # first, call ipfw -n to check syntax, if ok move on and do the action + if [ x"$new_rule" != x"0" ] ; then + case $type in + SERVER|server) + rule_in="dst-port $p" + rule_out="src-port $p" + del=service + ;; + CLIENT|client) + rule_in="src-port $p" + rule_out="dst-port $p" + del=service + ;; + SERVICE|service) + rule_in="{ src-port $p or dst-port $p }" + rule_out="{ src-port $p or dst-port $p }" + del="cli_ser" + ;; + *) + abort "invalid service type $type" + ;; + esac + + rule_in="pipe ${ipfw_pipe_in} in ${h_in} ${rule_in} // $type $arg $slice_id" + rule_out="pipe ${ipfw_pipe_out} out ${h_out} ${rule_out} // $type $arg $slice_id" + + # Move into the user root directory. The profile should be located there + ( cd /vservers/${SLICENAME}/`pwd`/ ; ${IPFW_CHECK} add ${ipfw_rule} ${rule_in} ) > /dev/null || \ + abort "ipfw syntax error ${rule_in}" + ( cd /vservers/${SLICENAME}/`pwd`/ ; ${IPFW_CHECK} add ${ipfw_rule} ${rule_out} ) > /dev/null || \ + abort "ipfw syntax error ${rule_out}" + fi + + # check error reporting + ( cd /vservers/${SLICENAME}/`pwd`/ ; ${IPFW_CHECK} pipe ${ipfw_pipe_in} config ${CONFIG_PIPE_IN} ) > /dev/null || \ + abort "ipfw syntax error pipe_in" + ( cd /vservers/${SLICENAME}/`pwd`/ ; ${IPFW_CHECK} pipe ${ipfw_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 0 $slice_id service $arg + [ "$del" = "cli_ser" ] && do_delete 0 $slice_id client $arg + [ "$del" = "cli_ser" ] && do_delete 0 $slice_id server $arg + [ "$new_rule" != "0" ] && ${IPFW} add ${ipfw_rule} $rule_in > /dev/null + [ "$new_rule" != "0" ] && ${IPFW} add ${ipfw_rule} $rule_out > /dev/null + # config pipes + ( cd /vservers/${SLICENAME}/`pwd`/ ; ${IPFW} pipe ${ipfw_pipe_in} config ${CONFIG_PIPE_IN} ) + ( cd /vservers/${SLICENAME}/`pwd`/ ; ${IPFW} pipe ${ipfw_pipe_out} config ${CONFIG_PIPE_OUT} ) + + # send output to the user + ${IPFW} show ${ipfw_rule} + ${IPFW} pipe ${ipfw_pipe_in} show + ${IPFW} pipe ${ipfw_pipe_out} show + + # do not write on the database on test-only + [ "$TEST" = "1" ] && return + # add to the database + ( grep -iv -- "^${slice_id} ${type} ${arg} " $DBFILE; \ + echo "${slice_id} ${type} ${arg} ${ipfw_rule} ${pipe_index} ${timeout}" ) > ${DBFILE}.tmp + mv ${DBFILE}.tmp ${DBFILE} +} - # filter input - TMP=`echo $1 | cut -d\ -f 1` - PORT=`filter $TMP` - TMP=`echo $1 | cut -d\ -f 2` - TIMEOUT=`filter $TMP` - TMP=`echo $1 | cut -d\ -f 3` - DELETE=`filter $TMP` - TMP=`echo $1 | cut -d\ -f 4-` - CONFIG_STRING=`filter $TMP` - - debug "PORT: $PORT" - debug "DELETE: $DELETE" - debug "TIMEOUT: $TIMEOUT" - debug "configuration string: $CONFIG_STRING" - - # find the ip address - ME=`/sbin/ip -o addr show | grep -v "1:\ lo" | grep "inet " | cut -d " " -f7 | cut -d "/" -f1 | head -n1` - - # deny port <= 1024 - [ ${PORT} -le 1024 ] && user_error "it is not allowed to modify the port range [0-1024]" - - # start to configure pipes and rules - PIPE_N=${PORT} - RULE_N=${PORT} - - # check if the link is already configured - ipfw list ${PORT} 2>&1 - - if [ x"$?" != x"0" ]; then # new rule, add and set owner/timeout - add_rules ${TIMEOUT} ${DELETE} - else # the rule already exist, check owner - modify_rule - fi +# +# Delete a given configuration +# if block_deletion !0 free block resources (if necessary) +# otherwise leave the block allocated in case +# we are adding the first rule +do_delete() { # block_deletion slice_id type arg + local ipfw_pipe_in ipfw_pipe_out pipe_index ipfw_rule + local block_deletion=$1 slice_id=$2 type=$3 arg=$4 + + [ "${type}" = "BLOCK" ] && abort "A BLOCK can not be deleted" + [ "${arg}" = "" ] && abort "Missing args on 'delete', expected on of {CLIENT|SERVER|SERVICE} arg" + set `find_rule $slice_id $type $arg` + ipfw_rule=$1; pipe_index=$2 + [ "$ipfw_rule" = "0" ] && return # no rules found + + # find actual pipe numbers XXX do as function + ipfw_pipe_in=$(($IPFW_PIPE_MIN + $((2 * $(($pipe_index - 1)))) )) + ipfw_pipe_out=$(($ipfw_pipe_in + 1)) + + echo "removing configuration ${slice_id} ${type} ${arg}" + [ "$TEST" = "1" ] && return 0 + $IPFW delete ${ipfw_rule} + $IPFW pipe delete ${ipfw_pipe_in} + $IPFW pipe delete ${ipfw_pipe_out} + # remove from the database (case insensitive) + grep -iv -- "^${slice_id} ${type} ${arg} " $DBFILE > ${DBFILE}.tmp + mv ${DBFILE}.tmp ${DBFILE} + + # if there are no more rules for the user + # remove the table entry from ipfw and from the db + [ $block_deletion = 0 ] && return 0 + + local rule_counter=`grep ^${slice_id} ${DBFILE} | wc -l` + [ $rule_counter -gt 1 ] && return 0 # there are still user rules + # delete the block and clean the table + local block_n=`grep "^${slice_id} BLOCK" ${DBFILE} | cut -d " " -f 3` + debug "Deleting BLOCK <${block_n}> entry from ipfw and from the database" + table_remove $slice_id $block_n +} + +# compare the argument with the first two field of +# the database. +# On match returns the block number, otherwise returns 0. +# no echo inside +find_block() { # $1 slice_id + local ret + ret=`grep -- "^$1 BLOCK " $DBFILE` + + [ x"$ret" = x ] && echo "0" && return # nothing found + # ignore multiple matches. If the db is corrupt we are + # screwed anyways + set $ret + echo "$3" +} +# +# remove the default user rule and +# the a BLOCK entry from ipfw and update the db +# no echo inside +table_remove() { # $slice_id $block_n + [ "$TEST" = "1" ] && return 0 + + # compute and delete the last user rule + local ipfw_rulemax=$(($IPFW_RULE_MIN + $(($M *${block_n})) -1)) + ${IPFW} table $SLICE_TABLE delete $slice_id + ${IPFW} delete ${ipfw_rulemax} + ( grep -iv -- "^${slice_id} BLOCK ${block_n}" $DBFILE; ) > ${DBFILE}.tmp + mv ${DBFILE}.tmp ${DBFILE} + return 0 +} + +# +# Find a rule and pipe_index for the given key (xid type arg) +# Allocate a new block if first entry for this xid. +# Rule and pipe are not written into the database, only the block is. +# +# Return ipfw_rule pipe_index new_rule +# 'new_rule' is 0 if the rule existed, 1 if it is new +# +# return ipfw_rule = 0 if there are no resources available +find_allocate() { # slice_id type arg + local slice_id=$1 type=$2 arg=$3 + local ipfw_rule pipe_index new_block=0 + + # search for already allocated rule and pipes + set `find_rule $slice_id $type $arg` + ipfw_rule=$1; pipe_index=$2 + [ ! ${ipfw_rule} = 0 ] && echo $ipfw_rule $pipe_index "0" && return 0 # rules found, return + + # no rules found, search for an already existing block, or + # allocate a new one + local block_n=`find_block ${slice_id}` + [ ${block_n} = "0" ] && new_block=1 && block_n=`find_free_block` + [ ${block_n} = "0" -o ${block_n} -gt $BLOCK_MAX ] && echo 0 && return 0; + + # We have a valid block, compute the range for user rules + local ipfw_rulemin=$(($IPFW_RULE_MIN + $(($M *$(($block_n - 1)))))) + local ipfw_rulemax=$(($(($ipfw_rulemin + $M)) - 1 )) + + # Find rule and pipes, reserve the last rule for the user's + # default rule that catches regular traffic. + set `allocate_resources $ipfw_rulemin $(($ipfw_rulemax - 1))` + ipfw_rule=$1; pipe_index=$2 + [ $ipfw_rule = 0 ] && echo 0 && return 0 # no resources + + # If this is a new block, add the slice to the lookup table + # and put a default rule at the end of the block. + if [ "$TEST" = "0" -a $new_block = 1 ] ; then + ${IPFW} table $SLICE_TABLE add ${slice_id} ${ipfw_rulemin} > /dev/null + ${IPFW} add ${ipfw_rulemax} allow all from any to any > /dev/null + ( echo "${slice_id} BLOCK ${block_n}" ) >> ${DBFILE} + fi + + echo $ipfw_rule $pipe_index "1" + return 0 +} + +# +# called with the database file as input +# compare the tuple with +# the current firewall configuration. The database contains +# slice_id type arg ipfw_rule pipe_index timeout +# On match returns +# On non match returns 0 0 0 +# no echo inside +find_rule() { # slice_id type arg + local ret + ret=`grep -i -- "^$1 $2 $3 " $DBFILE | grep -v BLOCK` + + [ 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 } -# main starts here +# XXX despite the name this does not allocate but only finds holes. +# returns a free rule and pipe base for client|server|service +# within a block +# Returns r=0 if there are no resources available +# no echo inside +allocate_resources() { # ipfw_minrule ipfw_maxrule + local p r + # remove comments, extract field, sort + p=`grep -v '^#' $DBFILE | grep -v BLOCK | awk '{print $5}' | sort -n | \ + find_hole $PIPE_MIN $PIPE_MAX` + r=`grep -v '^#' $DBFILE | grep -v BLOCK | awk '{print $4}' | sort -n | \ + find_hole $1 $2` + [ $r = 0 -o $p = 0 ] && r=0 # no resources available + echo $r $p +} - debug "Debug activated" - requests=[] - i=0 - while read request - do - # read -a read arguments in array - # XXX skip lines starting with # - requests[$i]=$request; - let i=$i+1 - done +# Returns the index of a free block +# Returns 0 if there are no resources available +# no debug inside +find_free_block() { + b=`grep -v '^#' $DBFILE | grep BLOCK | awk '{print $3}' | sort -n | \ + find_hole $BLOCK_MIN $BLOCK_MAX` + echo $b +} + +# parse the ipfw database and remove expired rules +# +# Each timeout value stored in the database is compared against +# the current time. If the timeout is older than current, +# the rules and related pipes will be deleted. +kill_expired() { # slice_id type arg + local match timeout + + # if there is no database file exit + [ ! -f ${DBFILE} ] && return 0 + + # Get the current time + now=`date -u +%s` + + cp ${DBFILE} ${DBFILE}.kill + cat ${DBFILE}.kill | grep -v BLOCK | + while read line; do + match=`echo $line|cut -d " " -f 1-3` + timeout=`echo $line|cut -d " " -f 6` + [ $now -gt $timeout ] && do_delete 1 $match + done + rm ${DBFILE}.kill +} - # create the lock +# execute functions from root context +# can be used from root context as follow: +# echo "super $command $args" | /vsys/ipfw-be 0 +do_super() { # $arguments... + case $1 in + init) + ipfw_init; return 0 + ;; + dbcleanup) + clean_db; return 0 + ;; + killexpired) + kill_expired; return 0 + ;; + *) + abort "Invalid super command" + ;; + esac +} - # process requests - for i in `/usr/bin/seq 0 $((${#requests[*]} - 1))` - do - process "${requests[$i]}" - done +# refresh the rule timeout +do_refresh() { # slice_id type arg timeout + local ipfw_pipe_in ipfw_pipe_out pipe_index + local slice_id=$1 type=$2 arg=$3 timeout=$4 + + debug "do_refresh type: <$type> arg: <$arg> timeout: <$timeout>" + [ "${type}" = "BLOCK" ] && abort "BLOCK rule not valid" + [ "${timeout}" = "" ] && abort "Missing args on 'refresh', expected on of {SERVICE|SERVER|CLIENT} port_number" + set `find_rule $slice_id $type $arg` + ipfw_rule=$1; pipe_index=$2 + [ "${ipfw_rule}" = "0" ] && debug "no rules found" && return 0 # no rules found + + [ "$TEST" = "1" ] && return + # update the database with the new timeout value + ( grep -iv -- "^${slice_id} ${type} ${arg} " $DBFILE; \ + echo "${slice_id} ${type} ${arg} ${ipfw_rule} ${pipe_index} ${timeout}" ) > ${DBFILE}.tmp + mv ${DBFILE}.tmp ${DBFILE} + echo "refreshed timeout for rule ${type} ${arg}" +} - # delete the lock - exit 0 +# process a request. +# A request is made by a set of arguments formatted as follow: +# +# config {server|client|service} arg [-t timeout] IN OUT +# show {rules|pipes} [args] +# delete type arg +# refresh type arg [-t timeout] +# +# 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 cmd=$1 ; shift + local debug_args="$*"; + local type=$1 ; shift + local args="$*" + debug "Received command: <$cmd> arguments: <$debug_args>" + + # set the timeout value + # if present, extract the '-t timeout' substring from the command line + timeout=`echo ${args} | ${SED} ${SEDOPT} 's/(.+)( -t [a-zA-Z0-9]+ )(.*)/\2/'` + # if the '-t timeout' is specified, use the timeout provided by the user + if [ "${timeout}" != "${args}" ] ; then # match + # remove the '-t ' option + timeout=`echo ${timeout} | ${SED} ${SEDOPT} 's/-t //'` + timeout=`check_timeout ${timeout}` + [ $timeout = 0 ] && abort "Date format $1 not valid" + # clean the arguments + args=`echo ${args} | ${SED} ${SEDOPT} 's/(.+)( -t [a-zA-Z0-9]+ )(.*)/\1 \3/'` + else + # use the default value, no need to check for correctness, no need to clean arguments + timeout=`date --date="1day" +%s` # default to 1 day + fi + + # if the table rule is not present, add it + local table_rule=`${IPFW} show $S | grep "skipto tablearg" | grep "lookup jail $SLICE_TABLE"` + [ -z "$table_rule" ] && ipfw_init + + debug "Timeout $timeout" + # Handle special requests: show and delete + case x"$cmd" in + x"config") + case x"$type" in + xserver|xSERVER|xclient|xCLIENT|xservice|xSERVICE) + do_config $SLICE_ID $timeout $type $args && return 0 + ;; + esac + abort "'config' should be followed by {CLIENT|SERVER|SERVICE}" + ;; + x"delete") + do_delete 1 $SLICE_ID $type $args + ;; + x"refresh") + do_refresh $SLICE_ID $type $args $timeout && return 0 + ;; + x"show") + # XXX filter out sliver rules + [ "$type" = "rules" ] && ${IPFW} show && return 0 + [ "$type" = "pipes" ] && ${IPFW} pipe show && return 0 + abort "'show' should be followed by {rules|pipes}" + ;; + x"super") + [ $SLICE_ID = 0 ] && do_super $type $args && return 0 + abort "no permission for ipfw-be super execution" + ;; + x"help") + do_help && return 0 + ;; + *) + # 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" ] && echo 0 && return + echo $tt +} + +do_config() { # slice_id timeout type arg IN pipe_conf 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 + local p h; # port and optional hostname + + [ "$1" != "IN" ] && abort "Missing addr:port, or IN requested" + shift + + # read pipe in configuration + i="" + while [ "$1" != "" -a "$1" != "OUT" ] ; do + i="$i $1" + shift + done + CONFIG_PIPE_IN="$i" # XXX local ? + [ "$CONFIG_PIPE_IN" = "" ] && abort "Missing pipe in configuration" + + [ "$1" != "OUT" ] && abort "Missing pipe in configuration, or missing 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" + + + # process the argument (port and hostname are separated by a @) + # split the argument, and prepare the remote host configuration string + p=`echo $arg | cut -s -d "@" -f1-` # empty it there is no separator + if [ "$p" = "" ] ; then + p=$arg + else + p=`echo $arg | cut -d "@" -f1` + h=`echo $arg | cut -d "@" -f2` + fi + + # A port value is mandatory + [ "$p" = "" ] && abort "A port value is mandatory." + + # SERVICE do not support remote hostname filtering + [ $type = "service" ] && [ "$h" != "" ] && \ + abort "The service configuration do not support filtering remote hostnames." + + debug "Configuration Required:" + debug "slice_id: $SLICE_ID" + debug "type: $type" + debug "full arg: $arg" + debug "mandatory port(s): $p optional hostname(s): $h" + debug "timeout: $timeout" + debug "IN: $CONFIG_PIPE_IN" + debug "OUT: $CONFIG_PIPE_OUT" + debug "-----------------------" + + # check if the link is already configured + debug "Search for slice_id: ${slice_id} type: ${type} port: ${arg}" + + set `find_allocate ${slice_id} ${type} ${arg}` + local ipfw_rule=$1 pipe_index=$2 new_rule=$3 + + [ ${ipfw_rule} = 0 ] && abort "No resources available" + debug "Found or allocated resources ipfw_rule: ${ipfw_rule} and pipe_index: ${pipe_index}" + + add_rule $slice_id $new_rule $type $arg $ipfw_rule $pipe_index $timeout + hook_call $type $port $rule_base $pipe_base $timeout + return 0; # link configured, exit +} + +# +# 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 +} + +# +# initialize the firewall with PlanetLab default rules +ipfw_init() { + ${IPFW} -q delete $S + ${IPFW} -q delete $D + ${IPFW} add $S skipto tablearg lookup jail $SLICE_TABLE + ${IPFW} add $D allow all from any to any +} + +# +# if present, call a hook function +# Arguments are: +# slice_id type port rule_base pipe_base timeout +hook_call() { + if [ -n "${HOOK}" -a -x "${HOOK}" ]; then + debug "Calling the hook function." + ${HOOK} ${SLICE_ID} "$*" & + fi +} + +do_help() { + cat << EOF +Usage: + ./neconfig {CLIENT|SERVER|SERVICE} arg [-t timeout] \ + IN OUT + ./netconfig show {rules|pipes} + ./netconfig delete {CLIENT|SERVER|SERVICE} arg + ./netconfig refresh [-t timeout] {CLIENT|SERVER|SERVICE} arg + +We support three modes of operation: + + CLIENT programs on the node connect to remote ports + and/or addresses. Emulation intercepts traffic + involving those ports/addresses + + SERVER programs on the node listen on specific ports. + Emulation intercepts traffic on those ports, + optionally limited to specific client addresses. + + SERVICE the node runs both clients and servers, + we can only specify the ports on which emulation + is configured. + + 'arg' has the form PORTLIST[@ADDRLIST], where ADDRLIST is + optional and only supported for CLIENT and SERVER modes. + PORTLIST and ADDRLIST can be specified as any valid port + or address specifier in ipfw, e.g. + - a single value 443 or 10.20.30.40/24 + - a comma-separated list 1111,2222,3333 1.2.3.4,5.6.7.8 + - a range 1111-2222 (only for ports) + Addresses can also be specified as symbolic hostnames, and + they are resolved when the rule is installed. + Note that they always indicate the remote endpoint. + + 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 pipe's configuration, both for the upstream and downstream link, +follows the dummynet syntax. A quick and not exaustive example +of the parameters that can be used to configure the delay, +the bandwidth and the packet loss rate for a link follow: + + IN|OUT delay 100ms bw 1Mbit/s plr 0.1 + +The profile file, if present, should be located into the sliver's +root directory. +The full documentation is on the manpage[1]. + +The timeout value follow the linux 'date' command format[2] +and can be specified as follow: + 1week + 2hours + 3days + +--- References: +[1] http://www.freebsd.org/cgi/man.cgi?query=ipfw +[2] http://linuxmanpages.com/man1/date.1.php +EOF +} + +#--- DOCUMENTATION AND INTERNAL ARCHITECTURE --- +# +# When a user configures an emulated link, we need to allocate +# two pipes and one ipfw rule number to store the parameters. +# Reconfigurations of existing links reuse the previous resources. +# We keep track of all resources (pipes, rules and blocks of rules) +# in a database stored in a text file, see DATABASE FORMAT below. +# +# Pipes are allocated in pairs. In the database each pair is numbered +# from PIPE_MIN to PIPE_MAX. The actual pipe numbers for each pair are +# +# ipfw_pipein = IPFW_PIPE_MIN + 2*(pipe_index-1) +# ipfw_pipeout = ipfw_pipein + 1 +# +# The rules number is allocated within a block of M consecutive rules +# for each slice. The block is allocated at the first configuration +# of an emulated link, and deallocated when the last link is removed. +# In the database, blocks are numbered from BLOCK_MIN to BLOCK_MAX, +# and the range of rules for a given block_index is +# +# ipfw_min_rule = RULE_BASE +# ipfw_max_rule = RULE_BASE + ((M-1)*block_index) -1 +# +# All lookups, and the block allocation, are done in find_allocate(). +# The rule_number and pipe_index are written in the database +# by add_rule() after checking the correctness of the request. +# +# +#--- RULESET STRUCTURE --- +# The ruleset is made of different sections, as follows: +# - an initial block of rules, reserved and configurable by +# the root context only; +# - a skipto rule (S), used to jump directly to the block +# associated with a given slice; +# - a second block of reserved rules, to catch remaining traffic. +# This ends with rule number D which is an 'accept all'; +# - after D, we have a block of M rule numbers for each slice. +# Each of these blocks ends with an 'accept all' rule; +# - finally, rule 65535 is the firewall's default rule. +# +# To summarize: +# 1...S-1 first block of reserved rules +# S skipto tablearg lookup jail 1 +# S+1..D-1 ... second block of reserved rules +# D allow ip from any to any +# +# RULE_BASE +# RULE_BASE+M +# ... +# +#--- DATABASE FORMAT --- +# The database is stored in a text file, and contains one record per +# line with the following structure +# +# XID TYPE arg1 arg2 ... +# +# Whitespace separates the fields. arg1, arg2, ... have different +# meaning depending on the TYPE. XID is the slice ID. +# +# In the database we have the following records: +# - one entry of type BLOCK for each slice with configured links. +# This entry represents the block_index of the block of M ipfw +# rules allocated to the slice, as follows: +# +# XID BLOCK block_index +# (BLOCK_MIN <= block_index <= BLOCK_MAX) +# +# - one entry for each link (CLIENT, SERVER, SERVICE). +# The database entry for this info has the form +# +# XID {CLIENT|SERVER|SERVICE} arg ipfw_rule pipe_index timeout +# +# 'TYPE' reflects the configuration mode; +# 'arg' is PORTLIST@ADDRLIST and is used as a search key together +# with the XID and TYPE; +# 'ipfw_rule' is the unique ipfw rule number used for this +# emulated link. It must be within the block of M rule numbers +# allocated to the slice; +# 'pipe_index' is the index of the pair of pipes used for the +# configuration; + +#-- main starts here +debug "--- $0 START for $SLICENAME ---" + +# If the db does not exist, create it and 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 -q and invalid chars +debug "--- processing <${REQ}>" +acquire_lock # critical section +process ${REQ} +release_lock +debug "--- $0 END ---" +exit 0