#!/bin/sh
#
# $Id$
#
# The author disclaims all copyrights and releases this document into
# the public domain.
#
# Allows use of multiple firewall configurations on Mac OS X.
#
# http://sial.org/howto/osx/firewall/
#
########################################################################
#
# CONSTANTS
# preferences dir for this utility
MFWETC=/etc/mfw
# custom env definitions file; put localizations in the MFWRC file
# instead of altering this script if possible
MFWRC=$MFWETC/mfwrc
# custom env for the mode scripts
MFWMODERC=$MFWETC/moderc
# dir for holding different firewall modes
MFWMODES=$MFWETC/modes
# where to store vendor default rule(s)
# Apple sets '65535 allow ip from any to any' by default, but that
# could change.
MFWVENDORRULES=$MFWETC/vendor-rules
# short application name of us
mfw=`basename $0`
# how to call various apps
ipfw="/sbin/ipfw -q"
natd="/usr/sbin/natd"
sysctl="/usr/sbin/sysctl"
VERBOSE=0
########################################################################
#
# SUBROUTINES
# creates required supporting areas, initial firewall modes
init () {
_makedir $MFWETC $MFWMODES
active2mode "default"
startup "default"
current "default"
# create vendor default ruleset
shellheader $MFWMODES/vendor
echo '$ipfw -f flush' >> $MFWMODES/vendor
shellfooter $MFWMODES/vendor
chmod +x $MFWMODES/vendor
}
# removes this utility from system
uninit () {
_removedir $MFWMODES $MFWETC
}
_makedir () {
mkdir -p $@
RETURN=$?
if [ $RETURN -ne 0 ]; then
echo "error creating required directories" >&2
exit $RETURN
fi
}
_removedir () {
rm -rf $@
RETURN=$?
if [ $RETURN -ne 0 ]; then
echo "error removing required directories" >&2
exit $RETURN
fi
}
# writes active IPFW ruleset (minus dynamic and vendor default) to
# mode file
active2mode () {
if [ $VERBOSE -eq 1 ]; then
echo "${mfw} info: creating new mode $1 from current state"
fi
# whether ruleset reload is needed
RELOAD=0
# mktemp added in OS X 10.2, compile and install on 10.1 if needed
TMPFILE=`mktemp -q /tmp/${mfw}.XXXXXX`
if [ $? -ne -0 ]; then
echo "${mfw} error: could not create temp file" >&2
exit 1
fi
$ipfw list > $TMPFILE
# remove dynamic rules
if grep '^#' $TMPFILE > /dev/null; then
perl -i -nle 'print $last and $last=$_ if 1../^#/' $TMPFILE
fi
# save vendor defaults so can remove them from regular listing
if [ ! -f $MFWVENDORRULES ]; then
$ipfw flush
$ipfw list > $MFWVENDORRULES
RELOAD=1
fi
# remove vendor default rule(s) from listing as otherwise will see
# 'getsockopt(IP_FW_ADD): Invalid argument' errors
perl -i -nle 'BEGIN { $rf = shift; open RF, $rf; chomp(@r=); }' \
-e '$l = $_; print $l unless grep { -1 < index($l, $_) ? 1 : 0 } @r' \
$MFWVENDORRULES $TMPFILE
# turn ipfw list output into shell script
shellheader $MFWMODES/$1
perl -i -ple 's/^/\$ipfw add /' $TMPFILE
cat $TMPFILE >> $MFWMODES/$1
shellfooter $MFWMODES/$1
chmod +x $MFWMODES/$1
if [ $RELOAD -eq 1 ]; then
runmode $1 start
fi
if grep divert $TMPFILE > /dev/null; then
echo "${mfw} notice: ruleset $1 appears to support NAT" >&2
echo " natd setup must be manually added to mode script." >&2
fi
# OS X 10.2 does not appear to support pipe/queue as the FreeBSD ipfw
# does (despite man page indicating otherwise); if did, would need to
# do 'ipfw pipe list' and 'ipfw queue list' and figure out how to get
# that information into the mode file.
# cleanup
rm $TMPFILE
}
# runs mode script for named mode
runmode () {
if [ ! -e $MFWMODES/$1 ]; then
echo "${mfw} error: no such mode: $1" >&2
exit 1
fi
if [ -x $MFWMODES/$1 ]; then
# stop prior config, if any and starting anew
if [ $2 = "start" ]; then
if [ -x $MFWMODES/cur ]; then
runmode cur stop
fi
fi
if [ $VERBOSE -eq 1 ]; then
if [ -L $1 ]; then
echo "${mfw} info: running $1 (`perl -le 'print readlink shift' $1`) $2"
else
echo "${mfw} info: running $1 $2"
fi
fi
if [ $2 = "start" ]; then
current $1
fi
if [ $1 = "pre" -a $2 = "start" ]; then
MFWMODENAME=`perl -le 'print readlink shift' $MFWMODES/cur`
MFWMODEPATH=$MFWMODES/$MFWMODENAME
. $MFWMODES/cur $2
else
if [ -L $1 ]; then
MFWMODENAME=`perl -le 'print readlink shift' $MFWMODES/cur`
else
MFWMODENAME=$1
fi
MFWMODEPATH=$MFWMODES/$MFWMODENAME
. $MFWMODES/$1 $2
fi
else
echo "${mfw} error: could not run mode: $1" >&2
exit 1
fi
}
# creates a link to a mode a script running from StartupItems can use
startup () {
(
cd $MFWMODES
if [ ! -e $1 ]; then
echo "${mfw} error: no such mode $1" >&2
exit 1
fi
ln -sf $1 startup
)
}
# link to current mode (for restarts, keeping previous for reversions)
current () {
(
cd $MFWMODES
if [ $1 = "cur" ]; then
return
elif [ $1 = "startup" ]; then
# do not update pointers for system startup
if ! test -t 0; then
return
fi
fi
if [ -L cur ]; then
if [ $1 = "pre" ]; then
mv pre .tmp
mv cur pre
mv .tmp cur
elif [ `perl -le 'print readlink shift' cur` != "$1" ]; then
mv cur pre
ln -s $1 cur
fi
elif [ ! -e cur ]; then
ln -s $1 cur
else
echo "${mfw} error: cur mode not a symbolic link"
exit 1
fi
)
}
# how all mode script should start out
# be sure to \escape variables if you do not want them expanded
shellheader () {
cat <> $1
#!/bin/sh
if [ -f \$MFWMODERC ]; then
. \$MFWMODERC
fi
OPMODE=\$1
OPMODE=\${OPMODE:=start}
case "\$OPMODE" in
start)
EOF
}
# complete mode script (disallows custom 'stop' code)
# be sure to \escape variables if you do not want them expanded
shellfooter () {
cat <> $1
;;
restart)
\$0 stop
\$0 start
;;
stop)
\$ipfw -f flush
;;
esac
EOF
}
# lists available modes
list () {
(
cd $MFWMODES
ls * | grep -v '\.' | while read mode; do
if [ ! -L $mode ]; then
echo $mode
fi
done
)
}
listall () {
(
cd $MFWMODES
ls * | grep -v '\.' | while read mode; do
if [ ! -L $mode ]; then
echo $mode
else
perl -le '$m=shift; print "$m -> ", readlink $m' $mode
fi
done
)
}
########################################################################
#
# MAIN
if [ `id -u` != 0 ]; then
echo "${mfw} error: must be run as superuser"
exit 1
fi
if [ -f $MFWRC ]; then
. $MFWRC
if [ $? -ne 0 ]; then
echo "${mfw} error: could not load ${MFWRC}"
exit 1
fi
fi
if [ ! -d $MFWETC -o ! -d $MFWMODES ]; then
echo "${mfw} notice: configuration not found, building from scratch" >&2
init
exit
fi
OPT=
while getopts lmnsv OPT; do
case $OPT in
l)
list
exit
;;
m)
listall
exit
;;
n)
shift
if [ ! -e $MFWMODES/$1 ]; then
active2mode $1
else
echo "${mfw} error: mode $1 already exists" >&2
fi
exit
;;
s)
shift
startup $1
exit
;;
v)
shift
VERBOSE=1
;;
esac
done
# default to loading current ruleset
WHAT=
WHAT=$1
WHAT=${WHAT:=cur}
HOW=
if echo $2 | egrep '^(start|stop|restart)$' > /dev/null; then
HOW=$2
else
HOW=start
fi
runmode $WHAT $HOW
exit 0