Scripts without +x will not run
[vsys-scripts.git] / exec / ipfw-be
index 9cf2864..b3b9439 100755 (executable)
 # - configures the firewall
 # - writes results on the output vsys pipe
 #
-# Configurable variables are at the beginning
+# Configurable variables are at the beginning (only HOOK so far)
 
-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
+# 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
-# XXX HOOK=""
 
-# You should not touch anything below.
+#--- You should not touch anything below this line. ----
+# For documentation see ARCHITECTURE near the end of the file.
 
-# 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
+#--- 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
-LOG_FILE=/tmp/netconfig.log # XXX when running from daemon
 lockfile=/var/lock/ipfw.lock
-PIPE_MIN=1000
-PIPE_MAX=30000
+
+# 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
-# XXX check consintency variable {}
+# XXX check consistency for variables {}
 SED=/bin/sed
-#IPFW="/bin/echo ipfw:"
+SEDOPT=-r
+[ -x ${SED} ] || { SED=`which sed` ; SEDOPT=-E ; }
 IPFW=/sbin/ipfw
-
-# Call arguments are <backend-program> <caller_slice_name>
-SLICENAME="$1" # save the slice XXX name or id ?
-SLICE_ID=`id -u $SLICENAME`
+IPFW_CHECK="/sbin/ipfw -n"
 
 debug() { # $1 message to be displayed
-       #echo "ipfw-be: $1"
-       [ x"${DEBUG}" != x"0" ] && echo "ipfw-be: $1" >>{LOG_FILE};
+       [ x"${VERBOSE}" != x"0" ] && echo "ipfw-be: $1"
 }
 
-abort() { # $1 message to be displayed
-       release_lock
-       echo "ipfw-be aborting: $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
 }
 
-user_error() { # $1 message to be displayed
-       echo "ipfw-be: user error: $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:"
+}
+
+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'
+       [ 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} -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
+}
+
+#
 # 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.
+# 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
-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 ?
+#
+# 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)
-           rule_in="dst-port $port"
-           rule_out="src-port $port"
-           del=SERVICE
+       SERVER|server)
+           rule_in="dst-port $p"
+           rule_out="src-port $p"
+           del=service
            ;;
-       CLIENT)
-           rule_in="src-port $port"
-           rule_out="dst-port $port"
-           del=SERVICE
+       CLIENT|client)
+           rule_in="src-port $p"
+           rule_out="dst-port $p"
+           del=service
            ;;
-       SERVICE)
-           rule_in="{ src-port $port or dst-port $port }"
-           rule_out="{ src-port $port or dst-port $port }"
-           del="CLI_SER"
+       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 ${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"
+       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
 
-    # 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"
+    # 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" ] && 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
+    [ "$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
-    ${IPFW} pipe ${pipe_in} config ${CONFIG_PIPE_IN}
-    ${IPFW} pipe ${pipe_out} config ${CONFIG_PIPE_OUT}
+    ( 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}
+}
 
-    # 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
+#
+# 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 
 }
 
-# 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
+# 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 <slice_id loc_port rem_port> with
+# compare the tuple <slice_id type arg> with
 # the current firewall configuration. The database contains
-#      slice_id local_port remote_port rule_nr pipe_nr timeout
-# On match returns <rule_nr pipe_base timeout>
+#      slice_id type arg ipfw_rule pipe_index timeout
+# On match returns <ipfw_rule pipe_index timeout>
 # On non match returns 0 0 0
-find_rule() { # $1 slice_id $2 type $3 port
+# no echo inside
+find_rule() { # slice_id type arg
     local ret
-    ret=`grep -- "^$1 $2 $3 " $DBFILE`
+    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
@@ -223,7 +347,7 @@ find_rule() { # $1 slice_id $2 type $3 port
     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
@@ -238,133 +362,258 @@ find_hole() {  # min max
     echo $cand
 }
 
-# returns a free rule and pipe base 
+# 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
-#
-# This function returns values using echo,
-# this means that we can not easily debug the function
-allocate_resources() {
+# no echo inside
+allocate_resources() { # ipfw_minrule ipfw_maxrule
     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`
+    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
 }
 
 
+# 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
+}
+
+# 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
+}
+
+# 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}"
+}
+
 # process a request.
 # A request is made by a set of arguments formatted as follow:
 #
-# CONFIG ${type} ${port} ${timeout} PIPE_IN <pipe in parameters> PIPE_OUT <pipe out parameters>
-# IPFW_SHOW
-# PIPE_SHOW
-# DELETE ${type} ${port}
+# config {server|client|service} arg [-t timeout] IN <pipe_conf> OUT <pipe_conf>
+# show {rules|pipes} [args]
+# delete type arg
+# refresh type arg [-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() { 
+process() {
     local new_pipe=0
-    local timeout TMP i rule_nr pipe_base
-    local type=$2
-    local port=$3
+    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
 
-    debug "Received from the input pipe: $*"
+    # 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"$1" in 
-    x"IPFW_SHOW") 
-       ${IPFW} show
-       return 0
+    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"PIPE_SHOW")
-       $IPFW pipe show
-       return 0
+    x"refresh") 
+       do_refresh $SLICE_ID $type $args $timeout && return 0
        ;;
-    x"DELETE")
-       delete_config ${SLICE_ID} $type $port
-       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"CONFIG")
+    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
        ;;
     *)
-       abort "Command not recognized"
+       # help XXX to be done
+       abort "'command' should be one of {show|config|delete|refresh|release}"
        ;;
     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"
+# validate the timeout
+check_timeout() { # timeout
+    local tt=`date --date="${1}" +%s`
+    [ "$?" != "0" ] && echo 0 && return
+    echo $tt
+}
 
-    type=$1; shift
-    port=$1; shift
-    timeout=$1; shift
-    # XXX check/compute timeout
+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" != "PIPE_IN" ] && abort "PIPE_IN requested"
+    [ "$1" != "IN" ] && abort "Missing addr:port, or IN requested"
     shift
 
+    # read pipe in configuration
     i=""
-    while [ "$1" != "" -a "$1" != "PIPE_OUT" ] ; do
+    while [ "$1" != "" -a "$1" != "OUT" ] ; do
        i="$i $1"
        shift
     done
-    CONFIG_PIPE_IN="$i"
+    CONFIG_PIPE_IN="$i"                # XXX local ?
+    [ "$CONFIG_PIPE_IN" = "" ] && abort "Missing pipe in configuration"
 
-    [ "$1" != "PIPE_OUT" ] && abort "PIPE_OUT requested"
+    [ "$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"
+    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 "TYPE: $type"
-    debug "PORT: $port"
-    debug "TIMEOUT: $timeout"
-    debug "pipe in config $CONFIG_PIPE_IN"
-    debug "pipe in config $CONFIG_PIPE_OUT"
+    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} ${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
+    debug "Search for slice_id: ${slice_id} type: ${type} port: ${arg}"
 
-    # 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
+    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()
-{
+acquire_lock() {
+    [ "$TEST" = 1 ] && return
     lockfile -s 0 -r 0 $lockfile 2> /dev/null
     if [ $? -ne 0 ] ; then
        echo "lock acquisition failed"
@@ -374,47 +623,189 @@ acquire_lock()
 
 #
 # release the lock
-release_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
+#
+# 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 <pipe in configuration> OUT <pipe out configuration>
+        ./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 <block of M entries for first user>
+#      RULE_BASE+M <block of M entry for second user ...>
+#      ...
+#
+#--- 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