Update
[vsys-scripts.git] / exec / ipfw-be
1 #!/bin/sh
2 #
3 # Marta Carbone, Luigi Rizzo
4 # Copyright (C) 2009 Universita` di Pisa
5 # $Id$
6 #
7 # This script the vsys backend used to configure emulation.
8 # In detail it:
9 # - reads the user's input from the vsys input pipe
10 # - validates the input
11 # - configures the firewall
12 # - writes results on the output vsys pipe
13 #
14 # Configurable variables are at the beginning
15
16 DEBUG=0 # set to 0 to disable debug messages
17
18 # if HOOK is set the program is called befor configuring a rule.
19 # A sample hook can be found in the ipfw.rpm package
20 # HOOK=/tmp/sample_hook
21 # XXX HOOK=""
22
23 # You should not touch anything below.
24
25 # We assume three type of connections
26 #  SERVER we know the local port P, and do the
27 #       bind/listen/accept on the local socket.
28 #               pipe_in in dst-port P
29 #               pipe_out out src-port P
30 #
31 #  CLIENT we know the remote port P, and do a connect to it
32 #       (src and dst are swapped wrt the previous case)
33 #               pipe_in in src-port P
34 #               pipe_out out dst-port P
35 #
36 #  SERVICE we run a server on local port P, and also connect
37 #       from local clients to remote servers on port P.
38 #               pipe_in in { dst-port P or src-port P }
39 #               pipe_out out { src-port P or dst-port P }
40
41 #  On a given port a user can have one CLIENT and/or one SERVER
42 #  configuration or one SERVICE configuration.
43 #  When a SERVICE configuration is installed any existing CLIENT
44 #  and SERVER configuration on the same port are removed.
45 #  When a CLIENT or SERVER configuration is installed any existing
46 #  SERVICE configuration on the same port is removed.
47 #
48 #  The following is a case that is implemented as SERVER
49 #  D    we run a server on local port P, and also connect
50 #       to remote servers but doing a bind(P) before connect().
51 #       In terms of rules, this is not distinguishable from
52 #       the SERVER case, however it would be different if we
53 #       had a way to tell SERVER from CLIENT sockets
54 #               pipe_in in dst-port P
55 #               pipe_out out src-port P
56 #
57 # The database of current ipfw and dummynet configuration is in a
58 # file which is regenerated on error.
59 # The format is
60 #
61 #       slice_id service_type port rule_nr pipe_base timeout
62 #
63 # (lines starting with '#' are comments and are ignored)
64 # For each configuration we allocate one rule number in ipfw,
65 # and two sequential pipe numbers.
66
67 # globals, do not touch below
68 DBFILE=/tmp/ff
69 LOG_FILE=/tmp/netconfig.log # XXX when running from daemon
70 lockfile=/var/lock/ipfw.lock
71 PIPE_MIN=1000
72 PIPE_MAX=30000
73
74 # programs
75 # XXX check consintency variable {}
76 SED=/bin/sed
77 #IPFW="/bin/echo ipfw:"
78 IPFW=/sbin/ipfw
79
80 # Call arguments are <backend-program> <caller_slice_name>
81 SLICENAME="$1"  # save the slice XXX name or id ?
82 SLICE_ID=`id -u $SLICENAME`
83
84 debug() { # $1 message to be displayed
85         #echo "ipfw-be: $1"
86         [ x"${DEBUG}" != x"0" ] && echo "ipfw-be: $1" >>{LOG_FILE};
87 }
88
89 abort() { # $1 message to be displayed
90         release_lock
91         echo "ipfw-be aborting: $1"
92         exit 1
93 }
94
95 user_error() { # $1 message to be displayed
96         echo "ipfw-be: user error: $1"
97         exit 1
98 }
99
100 # remove dangerous characters from user input
101 filter() { # $* variables to be filtered
102         # allowed chars are: numbers, upcase and lowecase
103         # chars, and the following symbols: . _ - /
104         echo "$*" | ${SED} -r 's/[^0-9a-zA-Z. _\/\-{}]*//g'
105 }
106
107 # Add the ipfw rule/pipe and update the database.
108 # The pipe-in and pipe_out config are through global variables
109 # CONFIG_IN CONFIG_OUT because they may be long.
110 # Other arguments are on the command line
111 add_rule() { # new_rule slice_id type port rule pipe_base timeout
112     local new_rule=$1 slice_id=$2 type=$3 port=$4 rule_nr=$5 pipe_base=$6 timeout=$7
113     local pipe_in pipe_out rule_in rule_out check_timeout
114
115     # XXX validate the timeout
116     # schedule the rule deletion
117     check_timeout=`date --date="${timeout}" +%s`
118     [ x"${check_timeout}" = x"" ] && abort "Date format $1 not valid"
119     # XXX tbd
120     timeout="fake_timeout"
121
122     # we could use a profile, so locate the user directory
123     # move in the slice root dir XXX todo
124     cd /vservers/${SLICENAME}/root
125     #echo ${CONFIG_STRING} | ${SED} -e "s/ profile \(.[^ ]\)/ profile \/vservers\/${SLICENAME}\/\1/g"
126
127     # first, call ipfw -n to check syntax
128     # check syntax, if ok move on and do the action
129     local IPFW_CHECK="${IPFW} -n "
130
131     pipe_in=$(($pipe_base + $pipe_base))
132     pipe_out=$(($pipe_in + 1))
133     local del   # which one to delete ?
134     if [ x"$new_rule" != x"0" ] ; then
135         case $type in
136         SERVER)
137             rule_in="dst-port $port"
138             rule_out="src-port $port"
139             del=SERVICE
140             ;;
141         CLIENT)
142             rule_in="src-port $port"
143             rule_out="dst-port $port"
144             del=SERVICE
145             ;;
146         SERVICE)
147             rule_in="{ src-port $port or dst-port $port }"
148             rule_out="{ src-port $port or dst-port $port }"
149             del="CLI_SER"
150             ;;
151         *)
152             abort "invalid service type $type"
153             ;;
154         esac
155
156         rule_in="pipe ${pipe_in} in uid $slice_id ${rule_in}"
157         rule_out="pipe ${pipe_out} out uid $slice_id ${rule_out}"
158         ${IPFW_CHECK} add ${rule_nr} $rule_in || \
159                 user_error "ipfw syntax error $rule_in"
160         ${IPFW_CHECK} add ${rule_nr} $rule_out || \
161                 user_error "ipfw syntax error $rule_out"
162     fi
163
164     # XXX check error reporting
165     ${IPFW_CHECK} pipe ${pipe_in} config ${CONFIG_PIPE_IN} || \
166                 user_error "ipfw syntax error pipe_in"
167     ${IPFW_CHECK} pipe ${pipe_out} config ${CONFIG_PIPE_OUT} || \
168                 user_error "ipfw syntax error pipe_out"
169
170     # all good, delete and add rules if necessary
171     [ "$del" = "SERVICE" ] && delete_config $slice_id SERVICE $port
172     [ "$del" = "CLI_SER" ] && delete_config $slice_id CLIENT $port
173     [ "$del" = "CLI_SER" ] && delete_config $slice_id SERVER $port
174     [ "$new_rule" != "0" ] && ${IPFW} add ${rule_nr} $rule_in
175     [ "$new_rule" != "0" ] && ${IPFW} add ${rule_nr} $rule_out
176     # config pipes
177     ${IPFW} pipe ${pipe_in} config ${CONFIG_PIPE_IN}
178     ${IPFW} pipe ${pipe_out} config ${CONFIG_PIPE_OUT}
179
180     # add to the database, at least to adjust the timeout
181     ( grep -v -- "^${slice_id} ${type} ${port}" $DBFILE;  \
182         echo "${slice_id} ${type} ${port} ${rule_nr} ${pipe_base} ${timeout}" ) > ${DBFILE}.tmp
183     mv ${DBFILE}.tmp ${DBFILE}
184
185 }
186
187 # Delete a given configuration
188 delete_config() { # slice_id type port
189     local pipe_in pipe_out pipe_base
190     local slice_id=$1 type=$2 port=$3
191
192     # XXX test
193     [ $# -lt 3 ] && abort "One or more input parameter is missing"
194     set `find_rule $slice_id $type $port`
195     rule=$1; pipe_base=$2
196     [ "$rule" = "0" ] && return         # no rules found
197
198     pipe_in=$(($pipe_base + $pipe_base))
199     pipe_out=$(($pipe_in + 1))
200
201     $IPFW delete ${rule}
202     $IPFW pipe delete ${pipe_in}
203     $IPFW pipe delete ${pipe_out}
204     # remove from the database
205     grep -v -- "^${slice_id} ${type} ${port}" $DBFILE > ${DBFILE}.tmp
206     mv ${DBFILE}.tmp ${DBFILE}
207 }
208
209 # called with the database file as input
210 # compare the tuple <slice_id loc_port rem_port> with
211 # the current firewall configuration. The database contains
212 #       slice_id local_port remote_port rule_nr pipe_nr timeout
213 # On match returns <rule_nr pipe_base timeout>
214 # On non match returns 0 0 0
215 find_rule() { # $1 slice_id $2 type $3 port
216     local ret
217     ret=`grep -- "^$1 $2 $3 " $DBFILE`
218
219     [ x"$ret" = x ] && echo "0 0 0 " && return  # nothing found
220     # ignore multiple matches. If the db is corrupt we are
221     # screwed anyways
222     set $ret
223     echo "$4 $5 $6"
224 }
225
226
227 # Find a hole in a list of numbers within a range (boundaries included)
228 # The input is passed as a sorted list of numbers on stdin.
229 # Return a "0" rule if there is no rule free
230 find_hole() {  # min max
231     local min=$1 cand=$1 max=$2 line
232     while read line ; do
233         [ $line -lt $min ] && continue
234         [ $line -ne $cand ] && break            # found
235         [ $cand -ge $max ] && cand=0 && break   # no space
236         cand=$(($cand + 1))
237     done
238     echo $cand
239 }
240
241 # returns a free rule and pipe base 
242 # Returns r=0 if there are no resources available
243 #
244 # This function returns values using echo,
245 # this means that we can not easily debug the function
246 allocate_resources() {
247     local p r
248     # remove comments, extract field, sort
249     p=`grep -v '^#' $DBFILE | awk '{print $5}' | sort -n | find_hole 1 10000`
250     r=`grep -v '^#' $DBFILE | awk '{print $4}' | sort -n | find_hole $PIPE_MIN $PIPE_MAX`
251     [ $r = 0 -o $p = 0 ] && r=0                 # no resources available
252     echo $r $p
253 }
254
255
256 #
257 # process a request.
258 # A request is made by a set of arguments formatted as follow:
259 #
260 # CONFIG ${type} ${port} ${timeout} PIPE_IN <pipe in parameters> PIPE_OUT <pipe out parameters>
261 # IPFW_SHOW
262 # PIPE_SHOW
263 # DELETE ${type} ${port}
264 #
265 # where uppercase values are keywords.
266 # The timeout value is expressed as:
267 # week, day, month or anything else accepted by the date command.
268 # The id of the slice issuing the request is in the $SLICE_ID variable,
269 # set at the beginning of this script.
270 process() { 
271     local new_pipe=0
272     local timeout TMP i rule_nr pipe_base
273     local type=$2
274     local port=$3
275
276     debug "Received from the input pipe: $*"
277
278     # Handle special requests: show and delete
279     case x"$1" in 
280     x"IPFW_SHOW") 
281         ${IPFW} show
282         return 0
283         ;;
284     x"PIPE_SHOW")
285         $IPFW pipe show
286         return 0
287         ;;
288     x"DELETE")
289         delete_config ${SLICE_ID} $type $port
290         return 0
291         ;;
292     x"CONFIG")
293         ;;
294     *)
295         abort "Command not recognized"
296         ;;
297     esac
298     shift
299
300     debug "processed initial command, rest of line: $*"
301     # check if we have enough parameters
302     [ $# -lt 9 ] && abort "One or more input parameter is missing"
303
304     type=$1; shift
305     port=$1; shift
306     timeout=$1; shift
307     # XXX check/compute timeout
308
309     [ "$1" != "PIPE_IN" ] && abort "PIPE_IN requested"
310     shift
311
312     i=""
313     while [ "$1" != "" -a "$1" != "PIPE_OUT" ] ; do
314         i="$i $1"
315         shift
316     done
317     CONFIG_PIPE_IN="$i"
318
319     [ "$1" != "PIPE_OUT" ] && abort "PIPE_OUT requested"
320     shift
321
322     i=""
323     while [ "$1" != "" ] ; do
324         i="$i $1"
325         shift
326     done
327     CONFIG_PIPE_OUT="$i"
328
329     debug "Configuration Required:"
330     debug "TYPE: $type"
331     debug "PORT: $port"
332     debug "TIMEOUT: $timeout"
333     debug "pipe in config $CONFIG_PIPE_IN"
334     debug "pipe in config $CONFIG_PIPE_OUT"
335     debug "-----------------------"
336
337     # check if the link is already configured
338     debug "Search for ${SLICE_ID} ${type} ${port}"
339
340     set `find_rule ${SLICE_ID} ${type} ${port}`
341     rule_nr=$1
342     pipe_base=$2
343
344     if [ ${rule_nr} = "0" ] ; then
345         debug "Rule not found, new installation"
346         new_pipe=1
347         set `allocate_resources`
348         rule_nr=$1; pipe_base=$2
349         [ $rule_nr = 0 ] && abort "no resources available"
350         debug "found free resources rule: $rule_nr pipe: $pipe_base"
351     else
352         debug "Rule found, just changing the pipe configuration"
353     fi
354     add_rule $new_pipe $SLICE_ID $type $port $rule_nr $pipe_base $timeout
355
356     # if present, call a hook in order to collect statistical
357     # information on dummynet usage
358     if [ -n "${HOOK}" -a -x "${HOOK}" ]; then
359         # XXX
360         ${HOOK} $SLICE_ID $type $port $rule_nr $pipe_base $timeout &
361     fi
362 }
363
364 #
365 # acquire the lock XXX check lockfile
366 acquire_lock()
367 {
368     lockfile -s 0 -r 0 $lockfile 2> /dev/null
369     if [ $? -ne 0 ] ; then
370         echo "lock acquisition failed"
371         exit -1
372     fi
373 }
374
375 #
376 # release the lock
377 release_lock()
378 {
379     rm -f $lockfile
380 }
381
382 # main starts here
383         debug "Debug activated"
384         debug "$0 START"
385
386         # create the DBFILE if not exist
387         [ ! -e ${DBFILE} ] && touch ${DBFILE}
388
389         requests=[]
390         i=0
391
392         # lock acquisition
393         acquire_lock    
394
395         # A request to the vsys backend is composed by a single line of input
396         while read request; do
397                 # read -a read arguments in array
398                 # XXX skip lines starting with #
399                 debug "Received <$request>"
400                 requests[$i]="$request"
401                 requests[$i]=`filter $request`
402                 debug "Filtered ${requests[$i]}"
403                 i=$(($i + 1))
404         done
405
406         # process requests
407         i=0
408         n_req=${#requests[*]}
409         debug "Received $n_req request"
410         while [ $i -lt $n_req ] ; do
411                 debug "processing request $i of $n_req"
412                 debug "<${requests[$i]}>"
413                 process ${requests[$i]}
414                 i=$(($i + 1))
415         done
416
417         # lock release
418         release_lock
419         debug "$0 END"
420         exit 0