Update
[vsys-scripts.git] / exec / ipfw-be
index b841200..9cf2864 100755 (executable)
 #!/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} <dummynet parameters>
-# the timeout value is expressed as:
-# week, day, month or anything else accepted by the date command
+# Configurable variables are at the beginning
 
-# save the slicename
-SLICE=$1
+DEBUG=0 # set to 0 to disable debug messages
 
-LOG_FILE=/tmp/netconfig.log
+# 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
-CUT=/usr/bin/cut
+# XXX check consintency variable {}
 SED=/bin/sed
+#IPFW="/bin/echo ipfw:"
 IPFW=/sbin/ipfw
 
-# set to 0 to disable debug messages
-DEBUG=0
+# Call arguments are <backend-program> <caller_slice_name>
+SLICENAME="$1" # save the slice XXX name or id ?
+SLICE_ID=`id -u $SLICENAME`
 
 debug() { # $1 message to be displayed
-       [ x"${DEBUG}" != x"0" ] && echo $1 >>{LOG_FILE};
+       #echo "ipfw-be: $1"
+       [ x"${DEBUG}" != x"0" ] && echo "ipfw-be: $1" >>{LOG_FILE};
 }
 
 abort() { # $1 message to be displayed
-       echo "$1"
+       release_lock
+       echo "ipfw-be aborting: $1"
        exit 1
 }
 
 user_error() { # $1 message to be displayed
-       echo "1 User error: $1"
+       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'
+       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
-
-       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}
+# 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 link
-delete_link()
-{
-       ipfw delete ${RULE_N}
-       ipfw pipe delete ${RULE_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
+    mv ${DBFILE}.tmp ${DBFILE}
 }
 
-# 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
+# called with the database file as input
+# compare the tuple <slice_id loc_port rem_port> 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>
+# 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 single line of input
-# this line has the following format:
-# ipfw
-# pipe
-# port timeout configuration_string
-process()
+
+#
+# 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}
+#
+# 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()
 {
-       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
-
-       ARGS=`echo $1 | wc -w`
-       if [ $ARGS -le 2 ]; then
-               abort "One or more input parameter is missing"
-       fi
-
-       # 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
+    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
 
-       while read request
-       do
+       # 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 #
-               requests[$i]=$request;
-               let i=$i+1
+               debug "Received <$request>"
+               requests[$i]="$request"
+               requests[$i]=`filter $request`
+               debug "Filtered ${requests[$i]}"
+               i=$(($i + 1))
        done
 
-       # create the lock
-
        # process requests
-       for i in `/usr/bin/seq 0 $((${#requests[*]} - 1))`
-       do
-               process "${requests[$i]}"
+       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
 
-       # delete the lock
+       # lock release
+       release_lock
+       debug "$0 END"
        exit 0