#!/bin/sh # # Shorewall 2.3 -- /usr/share/shorewall/functions # Function to truncate a string -- It uses 'cut -b -' # rather than ${v:first:last} because light-weight shells like ash and # dash do not support that form of expansion. # truncate() # $1 = length { cut -b -${1} } # # Split a colon-separated list into a space-separated list # split() { local ifs=$IFS IFS=: set -- $1 echo $* IFS=$ifs } # # Search a list looking for a match -- returns zero if a match found # 1 otherwise # list_search() # $1 = element to search for , $2-$n = list { local e=$1 while [ $# -gt 1 ]; do shift [ "x$e" = "x$1" ] && return 0 done return 1 } # # Functions to count list elements # - - - - - - - - - - - - - - - - # Whitespace-separated list # list_count1() { echo $# } # # Comma-separated list # list_count() { list_count1 $(separate_list $1) } # # Conditionally produce message # progress_message() # $* = Message { [ -n "$QUIET" ] || echo "$@" } # # Suppress all output for a command # qt() { "$@" >/dev/null 2>&1 } # # Perform variable substitution on the passed argument and echo the result # expand() # $@ = contents of variable which may be the name of another variable { eval echo \"$@\" } # # Perform variable substitition on the values of the passed list of variables # expandv() # $* = list of variable names { local varval while [ $# -gt 0 ]; do eval varval=\$${1} eval $1=\"$varval\" shift done } # # Replace all leading "!" with "! " in the passed argument list # fix_bang() { local i; for i in $@; do case $i in !*) echo "! ${i#!}" ;; *) echo $i ;; esac done } # # Set default config path # ensure_config_path() { local F=/usr/share/shorewall/configpath if [ -z "$CONFIG_PATH" ]; then [ -f $F ] || { echo " ERROR: $F does not exist"; exit 2; } . $F fi } # # Find a File -- For relative file name, look first in $SHOREWALL_DIR then in /etc/shorewall # find_file() { local saveifs= directory case $1 in /*) echo $1 ;; *) if [ -n "$SHOREWALL_DIR" -a -f $SHOREWALL_DIR/$1 ]; then echo $SHOREWALL_DIR/$1 else saveifs=$IFS IFS=: for directory in $CONFIG_PATH; do if [ -f $directory/$1 ]; then echo $directory/$1 IFS=$saveifs return fi done IFS=$saveifs echo /etc/shorewall/$1 fi ;; esac } # # Replace commas with spaces and echo the result # separate_list() { local list="$@" local part local newlist local firstpart local lastpart local enclosure # # There's been whining about us not catching embedded white space in # comma-separated lists. This is an attempt to snag some of the cases. # # The 'terminator' function will be set by the 'firewall' script to # either 'startup_error' or 'fatal_error' depending on the command and # command phase # case "$list" in *,|,*|*,,*|*[[:space:]]*) [ -n "$terminator" ] && \ $terminator "Invalid comma-separated list \"$@\"" echo "Warning -- invalid comma-separated list \"$@\"" >&2 ;; *\[*\]*) # # Where we need to embed comma-separated lists within lists, we enclose them # within square brackets # firstpart=${list%%\[*} lastpart=${list#*\[} enclosure=${lastpart%\]*} lastpart=${lastpart#*\]} case $lastpart in \,*) echo "$(separate_list $firstpart)[$enclosure] $(separate_list ${lastpart#,})" ;; *) echo "$(separate_list $firstpart)[$enclosure]$(separate_list $lastpart)" ;; esac return ;; esac list="$@" part="${list%%,*}" newlist="$part" while [ "x$part" != "x$list" ]; do list="${list#*,}"; part="${list%%,*}"; newlist="$newlist $part"; done echo "$newlist" } # # Load a Kernel Module # loadmodule() # $1 = module name, $2 - * arguments { local modulename=$1 local modulefile local suffix moduleloader=modprobe if ! qt which modprobe; then moduleloader=insmod fi if [ -z "$(lsmod | grep $modulename)" ]; then shift for suffix in $MODULE_SUFFIX ; do modulefile=$MODULESDIR/${modulename}.${suffix} if [ -f $modulefile ]; then case $moduleloader in insmod) insmod $modulefile $* ;; *) modprobe $modulename $* ;; esac return fi done fi } # # Reload the Modules # reload_kernel_modules() { [ -z "$MODULESDIR" ] && MODULESDIR=/lib/modules/$(uname -r)/kernel/net/ipv4/netfilter while read command; do eval $command done } # # Find the zones # find_zones() # $1 = name of the zone file { while read zone display comments; do [ -n "$zone" ] && case "$zone" in [0-9*]) echo " Warning: Illegal zone name \"$zone\" in zones file ignored" 2>&2 ;; \#*) ;; $FW|all|none) echo " Warning: Reserved zone name \"$zone\" in zones file ignored" >&2 ;; *) echo $zone ;; esac done < $1 } find_display() # $1 = zone, $2 = name of the zone file { grep ^$1 $2 | while read z display comments; do [ "x$1" = "x$z" ] && echo $display done } # # This function assumes that the TMP_DIR variable is set and that # its value named an existing directory. # determine_zones() { local zonefile=$(find_file zones) multi_display=Multi-zone strip_file zones $zonefile zones=$(find_zones $TMP_DIR/zones) newzones= for zone in $zones; do dsply=$(find_display $zone $TMP_DIR/zones) [ ${#zone} -gt 5 ] && echo " Warning: Zone name longer than 5 characters: $zone" >&2 eval ${zone}_display=\$dsply newzones="$newzones $zone" done zones=${newzones# } } # # The following functions may be used by apps that wish to ensure that # the state of Shorewall isn't changing # # This function loads the STATEDIR variable (directory where Shorewall is to # store state files). If your application supports alternate Shorewall # configurations then the name of the alternate configuration directory should # be in $SHOREWALL_DIR at the time of the call. # # If the shorewall.conf file does not exist, this function does not return # get_statedir() { MUTEX_TIMEOUT= local config=$(find_file shorewall.conf) if [ -f $config ]; then . $config else echo "/etc/shorewall/shorewall.conf does not exist!" >&2 exit 2 fi [ -z "${STATEDIR}" ] && STATEDIR=/var/state/shorewall } # # Call this function to assert MUTEX with Shorewall. If you invoke the # /sbin/shorewall program while holding MUTEX, you should pass "nolock" as # the first argument. Example "shorewall nolock refresh" # # This function uses the lockfile utility from procmail if it exists. # Otherwise, it uses a somewhat race-prone algorithm to attempt to simulate the # behavior of lockfile. # mutex_on() { local try=0 local lockf=$STATEDIR/lock MUTEX_TIMEOUT=${MUTEX_TIMEOUT:-60} if [ $MUTEX_TIMEOUT -gt 0 ]; then [ -d $STATEDIR ] || mkdir -p $STATEDIR if qt which lockfile; then lockfile -${MUTEX_TIMEOUT} -r1 ${lockf} else while [ -f ${lockf} -a ${try} -lt ${MUTEX_TIMEOUT} ] ; do sleep 1 try=$((${try} + 1)) done if [ ${try} -lt ${MUTEX_TIMEOUT} ] ; then # Create the lockfile echo $$ > ${lockf} else echo "Giving up on lock file ${lockf}" >&2 fi fi fi } # # Call this function to release MUTEX # mutex_off() { rm -f $STATEDIR/lock } # # Determine which version of mktemp is present (if any) and set MKTEMP accortingly: # # None - No mktemp # BSD - BSD mktemp (Mandrake) # STD - mktemp.org mktemp # find_mktemp() { local mktemp=`which mktemp 2> /dev/null` if [ -n "$mktemp" ]; then if qt mktemp -V ; then MKTEMP=STD else MKTEMP=BSD fi else MKTEMP=None fi } # # create a temporary file. If a directory name is passed, the file will be created in # that directory. Otherwise, it will be created in a temporary directory. # mktempfile() { [ -z "$MKTEMP" ] && find_mktemp if [ $# -gt 0 ]; then case "$MKTEMP" in BSD) mktemp $1/shorewall.XXXXXX ;; STD) mktemp -p $1 shorewall.XXXXXX ;; None) > $1/shorewall-$$ && echo $1/shorewall-$$ ;; *) echo " ERROR:Internal error in mktempfile" >&2 ;; esac else case "$MKTEMP" in BSD) mktemp /tmp/shorewall.XXXXXX ;; STD) mktemp -t shorewall.XXXXXX ;; None) rm -f /tmp/shorewall-$$ > /tmp/shorewall-$$ && echo /tmp/shorewall-$$ ;; *) echo " ERROR:Internal error in mktempfile" >&2 ;; esac fi } # # create a temporary directory # mktempdir() { [ -z "$MKTEMP" ] && find_mktemp case "$MKTEMP" in STD) mktemp -td shorewall.XXXXXX ;; None|BSD) # # Not all versions of the BSD mktemp support the -d option under Linux # mkdir /tmp/shorewall-$$ && chmod 700 /tmp/shorewall-$$ && echo /tmp/shorewall-$$ ;; *) echo " ERROR:Internal error in mktempdir" >&2 ;; esac } # # Read a file and handle "INCLUDE" directives # read_file() # $1 = file name, $2 = nest count { local first rest if [ -f $1 ]; then while read first rest; do if [ "x$first" = "xINCLUDE" ]; then if [ $2 -lt 4 ]; then read_file $(find_file $(expand ${rest%#*})) $(($2 + 1)) else echo " WARNING: INCLUDE in $1 ignored (nested too deeply)" >&2 fi else echo "$first $rest" fi done < $1 else [ -n "$terminator" ] && $terminator "No such file: $1" echo "Warning -- No such file: $1" fi } # # Function for including one file into another # INCLUDE() { . $(find_file $(expand $@)) } # # Strip comments and blank lines from a file and place the result in the # temporary directory # strip_file() # $1 = Base Name of the file, $2 = Full Name of File (optional) { local fname [ $# = 1 ] && fname=$(find_file $1) || fname=$2 if [ -f $fname ]; then read_file $fname 0 | cut -d'#' -f1 | grep -v '^[[:space:]]*$' > $TMP_DIR/$1 else > $TMP_DIR/$1 fi } # # Note: The following set of IP address manipulation functions have anomalous # behavior when the shell only supports 32-bit signed arithmatic and # the IP address is 128.0.0.0 or 128.0.0.1. # # # So that emacs doesn't get lost, we use $LEFTSHIFT rather than << # LEFTSHIFT='<<' # # Convert an IP address in dot quad format to an integer # decodeaddr() { local x local temp=0 local ifs=$IFS IFS=. for x in $1; do temp=$(( $(( $temp $LEFTSHIFT 8 )) | $x )) done echo $temp IFS=$ifs } # # convert an integer to dot quad format # encodeaddr() { addr=$1 local x local y=$(($addr & 255)) for x in 1 2 3 ; do addr=$(($addr >> 8)) y=$(($addr & 255)).$y done echo $y } # # Enumerate the members of an IP range -- When using a shell supporting only # 32-bit signed arithmetic, the range cannot span 128.0.0.0. # # Comes in two flavors: # # ip_range() - produces a mimimal list of network/host addresses that spans # the range. # # ip_range_explicit() - explicitly enumerates the range. # ip_range() { local first last l x y z vlsm case $1 in !*) # # Let iptables complain if it's a range # echo $1 return ;; [0-9]*.*.*.*-*.*.*.*) ;; *) echo $1 return ;; esac first=$(decodeaddr ${1%-*}) last=$(decodeaddr ${1#*-}) if [ $first -gt $last ]; then fatal_error "Invalid IP address range: $1" fi l=$(( $last + 1 )) while [ $first -le $last ]; do vlsm= x=31 y=2 z=1 while [ $(( $first % $y )) -eq 0 -a $(( $first + $y )) -le $l ]; do vlsm=/$x x=$(( $x - 1 )) z=$y y=$(( $y * 2 )) done echo $(encodeaddr $first)$vlsm first=$(($first + $z)) done } ip_range_explicit() { local first last case $1 in [0-9]*.*.*.*-*.*.*.*) ;; *) echo $1 return ;; esac first=$(decodeaddr ${1%-*}) last=$(decodeaddr ${1#*-}) if [ $first -gt $last ]; then fatal_error "Invalid IP address range: $1" fi while [ $first -le $last ]; do echo $(encodeaddr $first) first=$(($first + 1)) done } # # Netmask from CIDR # ip_netmask() { local vlsm=${1#*/} [ $vlsm -eq 0 ] && echo 0 || echo $(( -1 $LEFTSHIFT $(( 32 - $vlsm )) )) } # # Network address from CIDR # ip_network() { local decodedaddr=$(decodeaddr ${1%/*}) local netmask=$(ip_netmask $1) echo $(encodeaddr $(($decodedaddr & $netmask))) } # # The following hack is supplied to compensate for the fact that many of # the popular light-weight Bourne shell derivatives don't support XOR ("^"). # ip_broadcast() { local x=$(( 32 - ${1#*/} )) [ $x -eq 0 ] && echo -1 || echo $(( $(( 1 $LEFTSHIFT $x )) - 1 )) } # # Calculate broadcast address from CIDR # broadcastaddress() { local decodedaddr=$(decodeaddr ${1%/*}) local netmask=$(ip_netmask $1) local broadcast=$(ip_broadcast $1) echo $(encodeaddr $(( $(($decodedaddr & $netmask)) | $broadcast ))) } # # Test for network membership # in_network() # $1 = IP address, $2 = CIDR network { local netmask=$(ip_netmask $2) test $(( $(decodeaddr $1) & $netmask)) -eq $(( $(decodeaddr ${2%/*}) & $netmask )) } # # Netmask to VLSM # ip_vlsm() { local mask=$(decodeaddr $1) local vlsm=0 local x=$(( 128 $LEFTSHIFT 24 )) # 0x80000000 while [ $(( $x & $mask )) -ne 0 ]; do [ $mask -eq $x ] && mask=0 || mask=$(( $mask $LEFTSHIFT 1 )) # Not all shells shift 0x80000000 left properly. vlsm=$(($vlsm + 1)) done if [ $(( $mask & 2147483647 )) -ne 0 ]; then # 2147483647 = 0x7fffffff echo "Invalid net mask: $1" >&2 else echo $vlsm fi } # # Chain name base for an interface -- replace all periods with underscores in the passed name. # The result is echoed (less trailing "+"). # chain_base() #$1 = interface { local c=${1%%+} while true; do case $c in *.*) c="${c%.*}_${c##*.}" ;; *-*) c="${c%-*}_${c##*-}" ;; *%*) c="${c%\%*}_${c##*%}" ;; *) echo ${c:=common} return ;; esac done } # # Loosly Match the name of an interface # if_match() # $1 = Name in interfaces file - may end in "+" # $2 = Full interface name - may also end in "+" { local pattern=${1%+} case $1 in *+) test "x$(echo $2 | truncate ${#pattern} )" = "x${pattern}" ;; *) test "x$1" = "x$2" ;; esac } # # Find the value 'dev' in the passed arguments then echo the next value # find_device() { while [ $# -gt 1 ]; do [ "x$1" = xdev ] && echo $2 && return shift done } # # Find the interfaces that have a route to the passed address - the default # route is not used. # find_rt_interface() { ip route ls | while read addr rest; do case $addr in */*) in_network ${1%/*} $addr && echo $(find_device $rest) ;; default) ;; *) if [ "$addr" = "$1" -o "$addr/32" = "$1" ]; then echo $(find_device $rest) fi ;; esac done } # # Find the default route's interface # find_default_interface() { ip route ls | while read first rest; do [ "$first" = default ] && echo $(find_device $rest) && return done } # # Echo the name of the interface(s) that will be used to send to the # passed address # find_interface_by_address() { local dev="$(find_rt_interface $1)" local first rest [ -z "$dev" ] && dev=$(find_default_interface) [ -n "$dev" ] && echo $dev } # # Find interface addresses--returns the set of addresses assigned to the passed # device # find_interface_addresses() # $1 = interface { ip -f inet addr show $1 | grep inet | sed 's/inet //;s/\/.*//;s/ peer.*//' }