ff1e765f5cec43a806da555cc8396ada30c722ab
[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 # If HOOK is set the program is called before configuring a rule.
17 # A sample hook can be found in the ipfw.rpm package
18 # HOOK=/tmp/sample_hook
19 # XXX HOOK=""
20
21 # You should not touch anything below.
22
23 # We assume three type of connections
24 #  SERVER we know the local port P, and do the
25 #       bind/listen/accept on the local socket.
26 #               pipe_in in dst-port P
27 #               pipe_out out src-port P
28 #
29 #  CLIENT we know the remote port P, and do a connect to it
30 #       (src and dst are swapped wrt the previous case)
31 #               pipe_in in src-port P
32 #               pipe_out out dst-port P
33 #
34 #  SERVICE we run a server on local port P, and also connect
35 #       from local clients to remote servers on port P.
36 #               pipe_in in { dst-port P or src-port P }
37 #               pipe_out out { src-port P or dst-port P }
38
39 #  On a given port a user can have one CLIENT and/or one SERVER
40 #  configuration or one SERVICE configuration.
41 #  When a SERVICE configuration is installed any existing CLIENT
42 #  and SERVER configuration on the same port are removed.
43 #  When a CLIENT or SERVER configuration is installed any existing
44 #  SERVICE configuration on the same port is removed.
45 #
46 #  The following is a case that is implemented as SERVER
47 #  D    we run a server on local port P, and also connect
48 #       to remote servers but doing a bind(P) before connect().
49 #       In terms of rules, this is not distinguishable from
50 #       the SERVER case, however it would be different if we
51 #       had a way to tell SERVER from CLIENT sockets
52 #               pipe_in in dst-port P
53 #               pipe_out out src-port P
54 #
55 # The database of current ipfw and dummynet configuration is in a
56 # file which is regenerated on errors. The format is
57 #
58 #       slice_id type arg rule_base pipe_base timeout
59 #
60 # (lines starting with '#' are comments and are ignored)
61 # For each configuration we allocate one rule number in ipfw,
62 # and two sequential pipe numbers.
63
64 # globals, do not touch below
65 VERBOSE=0       # set to !0 to enable debug messages
66 TEST=0          # set to 1 for test mode
67
68 DBFILE=/tmp/ff
69 lockfile=/var/lock/ipfw.lock
70
71 # There values are the keys used in the database for rules and pipes
72 # rule_nr 1..10000 are mapped to rules 10000..49999 (n*4+9996)
73 # rule_nr 10001..20000 are mapped to rules 50000..59999 (n+39999)
74 # pipe_nr 1..25000 are mapped to pipes 10000-59999 (n*2+9998)
75 RULE_BL_MIN=1
76 RULE_BL_MAX=10000
77 RULE_IN_MIN=10001
78 RULE_IN_MAX=20000
79 PIPE_MIN=1
80 PIPE_MAX=25000
81 # These are the rule numbers used in ipfw
82 IPFW_RULE_MIN=10000
83 IPFW_RULE_MAX=59999
84 IPFW_PIPE_MIN=10000
85 IPFW_PIPE_MAX=59999
86
87 # set slicename and slice_id
88 # there represents the credential of the user
89 SLICENAME=$1
90 SLICE_ID=`id -u $SLICENAME`
91 [ $? != 0 ] && abort "Invalid slicename $SLICENAME"
92
93 # programs
94 # XXX check consistency for variables {}
95 SED=/bin/sed
96 SEDOPT=-r
97 [ -x ${SED} ] || { SED=`which sed` ; SEDOPT=-E ; }
98 IPFW=/sbin/ipfw
99 IPFW_CHECK="/sbin/ipfw -n"
100
101 debug() { # $1 message to be displayed
102         [ x"${VERBOSE}" != x"0" ] && echo "ipfw-be: $1"
103 }
104 # if the first argument is -v, enable verbose mode
105 set_verbose() {
106     [ x"$1" = x"-v" -o x"$2" = x"-v" ] && VERBOSE=1
107 }
108 set_test() {
109     [ x"$1" = x"-q" -o x"$2" = x"-q" ] || return
110     TEST=1
111     IPFW="/bin/echo ipfw:"
112     IPFW_CHECK="/bin/echo ipfw -n:"
113 }
114
115
116 abort() { # $1 message to be displayed
117         release_lock
118         echo "ipfw-be aborting: $1"
119         exit 1
120 }
121
122 # remove dangerous characters from user input
123 # if present, the leading '-v/-t' will be removed
124 filter() { # $* variables to be filtered
125         [ x${1} = x"-v" -o x${1} = x"-q" ] && shift
126         [ x${1} = x"-v" -o x${1} = x"-q" ] && shift
127         # allowed chars are: numbers, uppercase and lowercase letters,
128         # spaces, and the following symbols: .,_-/
129         echo "$*" | ${SED} ${SEDOPT} 's/[^\t0-9a-zA-Z., _\/\{}-]*//g'
130 }
131
132 # remove all entries from the ipfw config, and create an empty db
133 clean_db() {
134         rm -f ${DBFILE}
135         touch ${DBFILE}
136         # we would like to delete ranges of rules and pipes but this
137         # is not supported so for the time being we kill them all
138         ${IPFW} -q flush
139         ${IPFW} -q pipe flush
140         # ${IPFW} delete ${IPFW_RULE_MIN}-${IPFW_RULE_MAX}
141         # ${IPFW} pipe delete ${IPFW_PIPE_MIN}-${IPFW_PIPE_MAX}
142 }
143
144 # Add the ipfw rule/pipe and update the database.
145 # The pipe-in and pipe_out config are through global variables
146 # CONFIG_IN CONFIG_OUT because they may be long.
147 # Other arguments are on the command line
148 add_rule() { # new_rule slice_id type arg rule pipe_base timeout
149     local new_rule=$1 slice_id=$2 type=$3 arg=$4
150     local rule_base=$5 pipe_base=$6 timeout=$7
151     local pipe_in pipe_out rule_in rule_out check_timeout
152
153     # If we use a profile file, locate the user directory
154     # move in the slice root dir XXX todo
155     [ "$TEST" != "1" ] && cd /vservers/${SLICENAME}/root
156     #echo ${CONFIG_STRING} | ${SED} -e "s/ profile \(.[^ ]\)/ profile \/vservers\/${SLICENAME}\/\1/g"
157
158     # first, call ipfw -n to check syntax, if ok move on and do the action
159     pipe_in=$(($pipe_base + $pipe_base + 9998))
160     pipe_out=$(($pipe_in + 1))
161     local del   # anything to delete ?
162     local rule_nr=$(($rule_base + 39999))  # XXX formula for individual rules
163     if [ x"$new_rule" != x"0" ] ; then
164         case $type in
165         server)
166             rule_in="dst-port $arg"
167             rule_out="src-port $arg"
168             del=service
169             ;;
170         client)
171             rule_in="src-port $arg"
172             rule_out="dst-port $arg"
173             del=service
174             ;;
175         service)
176             rule_in="{ src-port $arg or dst-port $arg }"
177             rule_out="{ src-port $arg or dst-port $arg }"
178             del="cli_ser"
179             ;;
180         *)
181             abort "invalid service type $type"
182             ;;
183         esac
184
185         rule_in="pipe ${pipe_in} in jail $slice_id ${rule_in} // $type $arg"
186         rule_out="pipe ${pipe_out} out jail $slice_id ${rule_out} // $type $arg"
187         ${IPFW_CHECK} add ${rule_nr} $rule_in > /dev/null || \
188                 abort "ipfw syntax error $rule_in"
189         ${IPFW_CHECK} add ${rule_nr} $rule_out > /dev/null || \
190                 abort "ipfw syntax error $rule_out"
191     fi
192
193     # check error reporting
194     ${IPFW_CHECK} pipe ${pipe_in} config ${CONFIG_PIPE_IN} > /dev/null || \
195                 abort "ipfw syntax error pipe_in"
196     ${IPFW_CHECK} pipe ${pipe_out} config ${CONFIG_PIPE_OUT} > /dev/null || \
197                 abort "ipfw syntax error pipe_out"
198
199     # all good, delete and add rules if necessary
200     [ "$del" = "service" ] && do_delete $slice_id service $arg
201     [ "$del" = "cli_ser" ] && do_delete $slice_id client $arg
202     [ "$del" = "cli_ser" ] && do_delete $slice_id server $arg
203     [ "$new_rule" != "0" ] && ${IPFW} add ${rule_nr} $rule_in > /dev/null
204     [ "$new_rule" != "0" ] && ${IPFW} add ${rule_nr} $rule_out > /dev/null
205     # config pipes
206     ${IPFW} pipe ${pipe_in} config ${CONFIG_PIPE_IN}
207     ${IPFW} pipe ${pipe_out} config ${CONFIG_PIPE_OUT}
208
209     # send output to the user
210     ${IPFW} show ${rule_nr}
211     ${IPFW} pipe ${pipe_in} show
212     ${IPFW} pipe ${pipe_out} show
213
214     [ "$TEST" = "1" ] && return
215     # add to the database, at least to adjust the timeout
216     ( grep -v -- "^${slice_id} ${type} ${arg} " $DBFILE;  \
217         echo "${slice_id} ${type} ${arg} ${rule_base} ${pipe_base} ${timeout}" ) > ${DBFILE}.tmp
218     mv ${DBFILE}.tmp ${DBFILE}
219 }
220
221 # Delete a given configuration
222 do_delete() { # slice_id type arg
223     local pipe_in pipe_out pipe_base rule_base rule_nr
224     local slice_id=$1 type=$2 arg=$3
225
226     [ "${arg}" = "" ] && abort "Missing arg on 'delete'"
227     set `find_rule $slice_id $type $arg`
228     rule_base=$1; pipe_base=$2
229     [ "$rule_base" = "0" ] && return            # no rules found
230
231     rule_nr=$(($rule_base + 39999))             # XXX only individual rules
232     pipe_in=$(($pipe_base + $pipe_base + 9998))
233     pipe_out=$(($pipe_in + 1))
234
235     $IPFW delete ${rule_nr}
236     $IPFW pipe delete ${pipe_in}
237     $IPFW pipe delete ${pipe_out}
238     echo "removed configuration ${slice_id} ${type} ${arg}"
239     [ "$TEST" = "1" ] && return
240     # remove from the database
241     grep -v -- "^${slice_id} ${type} ${arg} " $DBFILE > ${DBFILE}.tmp
242     mv ${DBFILE}.tmp ${DBFILE}
243
244     # XXX if the use block is empty
245     # remove the table entry from ipfw and from the db
246 }
247
248 # called with the database file as input
249 # compare the tuple <slice_id type arg> with
250 # the current firewall configuration. The database contains
251 #       slice_id type arg rule_base pipe_base timeout
252 # On match returns <rule_base pipe_base timeout>
253 # On non match returns 0 0 0
254 find_rule() { # $1 slice_id $2 type $3 arg
255     local ret
256     ret=`grep -- "^$1 $2 $3 " $DBFILE`
257
258     [ x"$ret" = x ] && echo "0 0 0 " && return  # nothing found
259     # ignore multiple matches. If the db is corrupt we are
260     # screwed anyways
261     set $ret
262     echo "$4 $5 $6"
263 }
264
265
266 # Find a hole in a list of numbers within a range (boundaries included)
267 # The input is passed as a sorted list of numbers on stdin.
268 # Return a "0" rule if there is no rule free
269 find_hole() {  # min max
270     local min=$1 cand=$1 max=$2 line
271     while read line ; do
272         [ $line -lt $min ] && continue
273         [ $line -ne $cand ] && break            # found
274         [ $cand -ge $max ] && cand=0 && break   # no space
275         cand=$(($cand + 1))
276     done
277     echo $cand
278 }
279
280 # returns a free rule and pipe base for client|server|service
281 # Returns r=0 if there are no resources available
282 allocate_resources() {
283     local p r
284     # remove comments, extract field, sort
285     p=`grep -v '^#' $DBFILE | awk '{print $5}' | sort -n | \
286         find_hole $PIPE_MIN $PIPE_MAX`
287     r=`grep -v '^#' $DBFILE | awk '{print $4}' | sort -n | find_hole $1 $2`
288     [ $r = 0 -o $p = 0 ] && r=0                 # no resources available
289     echo $r $p
290 }
291
292 # process a request.
293 # A request is made by a set of arguments formatted as follow:
294 #
295 # config {server|client|service} arg [-t timeout] PIPE_IN <pipe_conf> PIPE_OUT <pipe_conf>
296 # show {rules|pipes} [args]
297 # delete type arg
298 #
299 # XXX not implemented yet
300 # config {rule|pipe} num <parameters>
301 # alloc rules|pipes [-t timeout] # returns a block of NUM_RULES or NUM_PIPES
302 # release rules|pipes args      # release the entire block
303 # refresh rules|pipes args [-t timeout]
304 #
305 # where uppercase values are keywords.
306 # The timeout value is expressed as:
307 # week, day, month or anything else accepted by the date command.
308 # The id of the slice issuing the request is in the $SLICE_ID variable,
309 # set at the beginning of this script.
310 process() {
311     local new_pipe=0
312     local timeout TMP i rule_base pipe_base
313     local slicename=${SLICENAME}
314     local cmd=$1 ; shift
315     local debug_args="$*";
316     local type=$1 ; shift
317     local args="$*"
318     debug "Received command: <$cmd> arguments: <$debug_args>"
319
320     # set the timeout value
321     # clean args from the timeout keyword
322     timeout=`echo ${args} | ${SED} ${SEDOPT} 's/(.+)( -t [a-zA-Z0-9]+ )(.*)/\2/'`
323     if [ "${timeout}" != "${args}" ] ; then     # match
324         timeout=`echo ${timeout} | ${SED} ${SEDOPT} 's/-t //'`
325         check_timeout ${timeout}        # abort on error
326         args=`echo ${args} | ${SED} ${SEDOPT} 's/(.+)( -t [a-zA-Z0-9]+ )(.*)/\1 \3/'`
327     else
328         timeout=`date --date="1day" +%s`                # default to 1 day
329     fi
330
331     debug "Timeout $timeout"
332     # Handle special requests: show and delete
333     case x"$cmd" in 
334     x"alloc") 
335         abort "XXX unimplemented " && return 0
336         ;;
337     x"config") 
338         [ "$type" = "server" ] && do_config $SLICE_ID $timeout $type $args && return 0
339         [ "$type" = "client" ] && do_config $SLICE_ID $timeout $type $args && return 0
340         [ "$type" = "service" ] && do_config $SLICE_ID $timeout $type $args && return 0
341         [ "$type" = "rule" ] && abort "XXX unimplemented " && return 0
342         [ "$type" = "pipe" ] && abort "XXX unimplemented " && return 0
343         abort "'config' should be followed by {server|client|service|rule|pipe}"
344         ;;
345     x"delete") 
346         do_delete ${SLICE_ID} $type $args
347         ;;
348     x"refresh") 
349         abort "XXX unimplemented " && return 0
350         do_refresh ${SLICE_ID} $type $args $timeout
351         ;;
352     x"release") 
353         abort "XXX unimplemented " && return 0
354         do_release ${SLICE_ID} $type $args
355         ;;
356     x"show")
357         # XXX should filter on jail
358         [ "$type" = "rules" ] && ${IPFW} show && return 0
359         [ "$type" = "pipes" ] && ${IPFW} pipe show && return 0
360         abort "'show' should be followed by {rules|pipes}"
361         ;;
362     *)
363         # help XXX to be done
364         abort "'command' should be one of {show|config|delete|refresh|release}"
365         ;;
366     esac
367 }
368
369 # validate the timeout
370 check_timeout() { # timeout
371     local tt=`date --date="${1}" +%s`
372     [ "$?" != "0" ] && abort "Date format $1 not valid"
373 }
374
375 do_release() { # slice_id type args timeout
376     return
377 }
378
379 do_refresh() { # slice_id ttype args
380     return
381 }
382
383 do_config() { # slice_id timeout type arg PIPE_IN pipe_conf PIPE_OUT pipe_conf
384     local slice_id=$1; shift
385     local timeout=$1; shift
386     local type=$1; shift
387     local arg=$1; shift # XXX addr not yet implemented
388
389     [ "$1" != "PIPE_IN" ] && abort "Missing addr:port, or PIPE_IN requested"
390     shift
391
392     # read pipe in configuration
393     i=""
394     while [ "$1" != "" -a "$1" != "PIPE_OUT" ] ; do
395         i="$i $1"
396         shift
397     done
398     CONFIG_PIPE_IN="$i"         # XXX local ?
399     [ "$CONFIG_PIPE_IN" = "" ] && abort "Missing pipe in configuration"
400
401     [ "$1" != "PIPE_OUT" ] && abort "Missing pipe in configuration, or missing PIPE_OUT"
402     shift
403
404     # read pipe out configuration
405     i=""
406     while [ "$1" != "" ] ; do
407         i="$i $1"
408         shift
409     done
410     CONFIG_PIPE_OUT="$i"        # XXX local ?
411     [ "$CONFIG_PIPE_OUT" = "" ] && abort "Missing pipe out configuration"
412
413     debug "Configuration Required:"
414     debug "slice_id: $slice_id"
415     debug "type: $type"
416     debug "arg: $arg"
417     debug "timeout: $timeout"
418     debug "PIPE_IN: $CONFIG_PIPE_IN"
419     debug "PIPE_OUT: $CONFIG_PIPE_OUT"
420     debug "-----------------------"
421
422     # XXX Search if there is a block already allocated to the slice_id
423     # if not present
424     # {
425     #    allocate the block;
426     #    update the db;
427     #    add table to ipfw;
428     # }
429     # Returns the slice base rule number
430
431     # check if the link is already configured
432     debug "Search for ${slice_id} ${type} ${arg}"
433
434     set `find_rule ${slice_id} ${type} ${arg}`
435     local rule_base=$1
436     local pipe_base=$2
437     local new_pipe=0
438
439     if [ ${rule_base} = "0" ] ; then
440         debug "Rule not found, new installation"
441         new_pipe=1
442         set `allocate_resources $RULE_IN_MIN $RULE_IN_MAX`
443         rule_base=$1; pipe_base=$2
444         [ $rule_base = 0 ] && abort "no resources available"
445         debug "found free resources rule: $rule_base pipe: $pipe_base"
446     else
447         debug "Rule found, just changing the pipe configuration"
448     fi
449
450     add_rule $new_pipe $slice_id $type $arg $rule_base $pipe_base $timeout
451
452     # if present, call a hook in order to collect statistical
453     # information on dummynet usage
454     if [ -n "${HOOK}" -a -x "${HOOK}" ]; then
455         # XXX
456         ${HOOK} $slice_id $type $port $rule_base $pipe_base $timeout &
457     fi
458 }
459
460 #
461 # acquire the lock XXX check lockfile
462 acquire_lock() {
463     [ "$TEST" = 1 ] && return
464     lockfile -s 0 -r 0 $lockfile 2> /dev/null
465     if [ $? -ne 0 ] ; then
466         echo "lock acquisition failed"
467         exit -1
468     fi
469 }
470
471 #
472 # release the lock
473 release_lock() {
474     rm -f $lockfile
475 }
476
477 # ALLOCATION OF RULES AND PIPES
478 # The ruleset is structured as follows
479 #       1...X-1 generic rules
480 #       X       skipto tablearg jail 0-65535 lookup jail-table
481 #       X+1..Y-1 ... other generic rules
482 #       Y       allow ip from any to any
483 #
484 #       RULE_BASE <block of M entries for first user>
485 #       RULE_BASE+M <block of M entry for second user ...>
486 #       ...
487 #
488 # Out of 64k rules, we allocate a block of M=50 consecutive
489 # rules to each slice using emulation. Within this block,
490 # each configuration uses one rule number and two pipes.
491 #
492 # Pipes are allocated starting from PIPE_BASE, a couple
493 # of pipes for each configuration.
494 #
495 # DATABASE FORMAT
496 # The database is stored on a file, and contains
497 # one line per record with this general structure
498 #       XID     TYPE    arg1    arg2    ...
499 # whitespace separates the fields. arg1, arg2, ...
500 # have different meaning depending on the type.
501 #
502 # In the database we have the following records:
503 # - one entry for each slice that has active emulation entries.
504 #   For each of these slices we reserve a block of M ipfw rules
505 #   starting at some RULE_BASE rule number.
506 #   The database entry for this info has the form
507 #       XID     TABLE   block_number
508 #   where blocks are numbered sequentially from 1.
509 #   The actual rule number is RULE_BASE + M*(block_number)
510 #   (we don't care if we waste some rules)
511 #
512 # - one entry for each predefined config (CLIENT, SERVER, SERVICE).
513 #   The database entry for this info has the form
514 #       XID     {CLIENT|SERVER|SERVICE} arg     rule_nr pipe_index
515 #   rule_nr is the absolute rule number for this configuration
516 #   (it must be within the block of M rules allocated to the slice)
517 #   pipe_index is the index of the couple of pipes used for the
518 #   configuration. pipe_index starts from 1.
519
520 # ---OLD-START--
521 # pipes are always allocated in pairs
522 # rules are either individual or in groups of size NUM_RULES (e.g. 4)
523 # and are allocated in two different parts of the rule namespace
524 # (e.g. blocks from 10000 to 49999 and individuals from 50000 to 59999)
525 # Internally allocator uses the base number for each item, e.g.
526 # rule 10000..49999 -> rule_base=1..10000
527 # rule 50000..59999 -> rule_base=10001..20000
528 # pipe 10000..59999 -> pipe_base=1..25000
529 # a bit of math lets us compute the correct numbers.
530 # For CLIENT, SERVER, SERVICE the database contains entries as
531 #       XID     TYPE    arg     rule_base       pipe_base
532 # For blocks the entries are
533 #       XID     RULE    -       rule_base       -
534 #       XID     PIPE    -       -               pipe_base
535 # When a rule or pipe is referenced we first check that the owner owns it.
536 # more details below.
537
538 #-- main starts here
539 debug "--- $0 START for $SLICENAME ---"
540
541 # If the db does not exist, create it and we clean rules and pipes
542 [ ! -e ${DBFILE} ] && clean_db
543
544 # A request to the vsys backend is composed by a single line of input
545 read REQ                        # read one line, ignore the rest
546 set_verbose ${REQ}              # use inital -v if present
547 set_test ${REQ}         # use inital -t if present
548 REQ="`filter ${REQ}`"   # remove -v and -t and invalid chars
549 debug "--- processing <${REQ}>"
550 acquire_lock                    # critical section
551 process ${REQ}
552 release_lock
553 debug "--- $0 END ---"
554 exit 0