#!/bin/bash set -e PROGNAME=`basename $0` # $HeadURL$ $LastChangedRevision$ # config file name based on name of this prog but without the # {ip,if}-{up,down}.d sequence number. SEQNOSTRIPPED_PROGNAME=$(expr $(basename $PROGNAME) : '[0-9]*\(.*\)') CFG_FILE=/etc/$SEQNOSTRIPPED_PROGNAME.conf # state information name the same. STT_DIR=/var/local/$SEQNOSTRIPPED_PROGNAME IPTABLES_CMD=/sbin/iptables main() { local TRUSTED_NIC LO_IS_TRUSTED # # Default values for options # if [ -t 2 ]; then VERBOSELEVEL=2 else VERBOSELEVEL=3 fi SIMULATE=false VERBOSELEVEL=5 # # Process options # while [ "X$1" != X ]; do case "$1" in -v|--verbose) VERBOSELEVEL=3 ;; -d) VERBOSELEVEL=$2; shift ;; --debug=*) VERBOSELEVEL=${1#*=} ;; -n) SIMULATE=true ;; -i) THIS_IF_NAME=$2; shift ;; -s) THIS_IF_STATE=$2; shift ;; -c) IFS_UP_COUNT=$2; shift ;; -h|--help) usage 0 ;; --) shift; break ;; -*) error "$1: invalid option" ;; *) break ;; esac shift done # # Process arguments # # Don't bother; this program gets invoked in several different # ways but everything is determinable from environment variables. # # Load config # # To avoid leaving NICs open, abort noisily if no config file. [ -f $CFG_FILE ] || error "$CFG_FILE: not found" [ -r $CFG_FILE ] || error "$CFG_FILE: not readable" # Check config is loadable in subshell to avoid contamination. sh -c ". $CFG_FILE" || error "$CFG_FILE: failed to load, see previous error messages" # Load config. . $CFG_FILE # # Sanity check config # debug 10 "main: ENABLE=$ENABLE, MASQUERADE_FLAG=$MASQUERADE_FLAG, FIREWALL_FLAG=$FIREWALL_FLAG, TRUSTED_NICS=$TRUSTED_NICS" case $ENABLE in false) return 0 ;; true) : ;; *) error "ENABLE: invalid or unset in $CFG_FILE" ;; esac debug 10 "main: sanity checking MASQUERADE_FLAG ..." case $MASQUERADE_FLAG in true|false) : ;; *) error "MASQUERADE_FLAG: invalid or unset in $CFG_FILE" ;; esac debug 10 "main: sanity checking FIREWALL_FLAG ..." case "$FIREWALL_FLAG" in "") : ;; *) error "FIREWALL_FLAG: invalid variable in $CFG_FILE (use TRUSTED_NICS instead)" ;; esac debug 10 "main: sanity checking TRUSTED_NICS ..." case $TRUSTED_NICS in [a-z]*) : ;; *) error "TRUSTED_NICS: invalid or unset in $CFG_FILE" ;; esac debug 10 "main: simple value sanity checks passed" is_trusted_nic lo || error "TRUSTED_NICS: does not contain 'lo'" # for TRUSTED_NIC in $TRUSTED_NICS; do #is_real_nic $TRUSTED_NIC || error "TRUSTED_NICS: contains non-existed NIC '$TRUSTED_NIC'" #done debug 10 "main: after sanity checking TRUSTED_NICS" # If we get through all those checks then the user wants to run it. [ -x $IPTABLES_CMD ] || error "$IPTABLES_CMD: not found" # pppd uses IFNAME and then defines things like CONNECT_TIME when # coming down. if [ "X$IFNAME" != X ]; then THIS_IF_NAME=$IFNAME case $CONNECT_TIME in "") THIS_IF_STATE=up ;; *) THIS_IF_STATE=down ;; esac IFS_UP_COUNT=`netstat -i | wc -l | xargs expr -2 +` # ifup uses IFACE, but it should not trigger calling this script # as if does not get called if the interfaces dies on its own # (e.g. 24hour timeout imposed by telecom providers); but pppd # will call it at both times. elif [ "X$IFACE" != X ] && expr $IFACE : ppp >/dev/null; then return 0 # ifup uses IFACE and MODE. elif [ "X$IFACE" != X ]; then THIS_IF_NAME=$IFACE case $MODE in stop) THIS_IF_STATE=down ;; start) THIS_IF_STATE=up ;; *) error "MODE: invalid or unset (by ifup?)" ;; esac IFS_UP_COUNT=`netstat -i | wc -l | xargs expr -2 +` # command line elif [ "X$THIS_IF_NAME" != X ]; then case $THIS_IF_STATE in up|down) : ;; *) usage ;; esac case $IFS_UP_COUNT in 0) error "$IFS_UP_COUNT: illegal count of up interfaces" ;; [1-9]) : ;; *) usage ;; esac else error "pppd/ifup interface variables not set" fi # # Initialise # # get name of interface providing default route DFLT_IF_NAME=`netstat -nr | sed -n 's/^0\.0\.0\.0 .* //p'` # convert user-specified NIC 'default' to whatever default is. [ $THIS_IF_NAME != default ] || THIS_IF_NAME=$DFLT_IF_NAME # initialise undo logs [ -d $STT_DIR ] || mkdir -p $STT_DIR if [ $THIS_IF_STATE = up ]; then if [ $IFS_UP_COUNT = 1 ]; then info "deleting old iptables ..." ade_std_shell -n $SIMULATE "rm -f $STT_DIR/*" exec_with_undo init ": `date`" fi exec_with_undo $THIS_IF_NAME ": `date`" fi # Initialise iptables only when first interface coming up. if [ $THIS_IF_STATE = up -a $IFS_UP_COUNT = 1 ]; then initialise_tables fi # # If the interface is trusted, accept all packets on this interface. # if [ $THIS_IF_STATE = up ] && is_trusted_nic $THIS_IF_NAME; then info "accepting all traffic on $THIS_IF_NAME (trusted NIC) ..." accept_all_packets $THIS_IF_NAME # # Otherwise firewall this interface. # elif [ $THIS_IF_STATE = up ]; then info "accepting only consequent traffic on $THIS_IF_NAME ..." accept_consequent_packets $THIS_IF_NAME info "opening selected ports on $THIS_IF_NAME ..." for OPEN_PORT in $OPEN_PORTS; do open_port $THIS_IF_NAME $OPEN_PORT done # ... Block access to some hosts (e.g. dione will block # access from any home host via ppp0, leda will block # access from itself via eth0 (at home and work)). info "blocking selected hosts on $THIS_IF_NAME ..." for BLOCK_HOST in $BLOCK_HOSTS; do block_host $THIS_IF_NAME $BLOCK_HOST done # ... Run any special iptables commands provided by # the user in the config file. info "running user-specified iptables commands on $THIS_IF_NAME ..." for IPTABLES_CMD in "${IPTABLES_CMDS[@]}"; do apply_user_iptables $THIS_IF_NAME "$IPTABLES_CMD" done fi # # Enable masquerading if required. # if [ $THIS_IF_STATE = up -a "X$DFLT_IF_NAME" = "X$THIS_IF_NAME" -a $MASQUERADE_FLAG = true ]; then info "enabling masquerading ..." exec_with_undo $THIS_IF_NAME "$IPTABLES_CMD -t nat -A POSTROUTING -o $THIS_IF_NAME -j MASQUERADE" exec_with_undo $THIS_IF_NAME "echo 1 > /proc/sys/net/ipv4/ip_forward" fi # # Remove all rules associated with an interface # when that interface goes down. And when the last # interface goes down the uninitialse iptables. # if [ $THIS_IF_STATE = down ]; then info "undoing iptables configuration of $THIS_IF_NAME ..." exec_with_undo $THIS_IF_NAME undo if [ $IFS_UP_COUNT = 1 ]; then exec_with_undo init undo fi fi } initialise_tables() { info "initialising iptables ..." # clean all tables ('-t filter' impied if missing in all # iptables calls) exec_with_undo init "$IPTABLES_CMD -F" exec_with_undo init "$IPTABLES_CMD -t nat -F" exec_with_undo init "$IPTABLES_CMD -t mangle -F" # Ensure anything not explicity allowed is denied. exec_with_undo init "$IPTABLES_CMD -P INPUT DROP" # hook in a new chain on which we will list hosts to block # access to/from. exec_with_undo init "$IPTABLES_CMD -N BLOCK 2>/dev/null" || true exec_with_undo init "$IPTABLES_CMD -F BLOCK" exec_with_undo init "$IPTABLES_CMD -A BLOCK -j LOG --log-prefix BLOCK" exec_with_undo init "$IPTABLES_CMD -A BLOCK -j DROP" } usage() { local RC RC=${1:-1} { echo "Usage: $PROGNAME [ ]" echo # standard options echo "Options: -v | --verbose be verbose" echo " -d | --debug= be very verbose" echo " -h | --help show this text" echo " -n simulate everything" echo " -i -s { up | down } -c for testing only" echo } | if [ $RC = 0 ]; then cat else cat >&2 fi exit $RC } block_host() { local THIS_IF_NAME BLOCK_HOST THIS_IF_NAME=$1 BLOCK_HOST=$2 # block access from spec'd host exec_with_undo $THIS_IF_NAME "$IPTABLES_CMD -I INPUT 1 -s $BLOCK_HOST -i $THIS_IF_NAME -j BLOCK" # block access to spec'd host exec_with_undo $THIS_IF_NAME "$IPTABLES_CMD -I OUTPUT 1 -d $BLOCK_HOST -o $THIS_IF_NAME -j BLOCK" if [ $MASQUERADE_FLAG = true ]; then # block forwarding from spec'd host exec_with_undo $THIS_IF_NAME "$IPTABLES_CMD -I FORWARD 1 -s $BLOCK_HOST -i $THIS_IF_NAME -j BLOCK" # block forwarding to spec'd host exec_with_undo $THIS_IF_NAME "$IPTABLES_CMD -I FORWARD 1 -d $BLOCK_HOST -o $THIS_IF_NAME -j BLOCK" fi } apply_user_iptables() { local THIS_IF_NAME IPTABLES_CMD THIS_IF_NAME=$1 IPTABLES_CMD=$2 exec_with_undo $THIS_IF_NAME "$IPTABLES_CMD -i $THIS_IF_NAME" } open_port() { local THIS_IF_NAME OPEN_PORT RULE_SPEC THIS_IF_NAME=$1 OPEN_PORT=$2 # construct the iptable rule options case $OPEN_PORT in icmp) RULE_SPEC="--protocol icmp" ;; udp/*|tcp/*) RULE_SPEC="--protocol ${OPEN_PORT%/*} --dport ${OPEN_PORT#*/}" ;; esac # apply the rule exec_with_undo $THIS_IF_NAME "$IPTABLES_CMD -A INPUT $RULE_SPEC -i $THIS_IF_NAME -j ACCEPT" } is_real_nic() { local IF_NAME IF_NAME="$1" grep -q "^ *$IF_NAME:" /proc/net/dev } is_trusted_nic() { local IF_NAME TRUSTED_IF_NAME TRUSTED TRUSTED_IF_NAMES THIS_IF_NAME="$1" # Internally (within this function) we use the *_IF_NAME[S] # convention for variable names. But in the config file # we use TRUSTED_NICS as it is more concise. TRUSTED_IF_NAMES="$TRUSTED_NICS" # Assume not trusted until shown otherwise. TRUSTED=false for TRUSTED_IF_NAME in $TRUSTED_IF_NAMES; do [ $THIS_IF_NAME != $TRUSTED_IF_NAME ] || TRUSTED=true done debug 20 "is_trusted_nic: THIS_IF_NAME=$THIS_IF_NAME, TRUSTED=$TRUSTED" $TRUSTED } accept_all_packets() { local THIS_IF_NAME THIS_IF_NAME=$1 # allow any packets (ie NEW, ESTABLISHED, RELATED) in on spec'd interface exec_with_undo $THIS_IF_NAME "$IPTABLES_CMD -A INPUT -i $THIS_IF_NAME -j ACCEPT" } accept_consequent_packets() { local THIS_IF_NAME THIS_IF_NAME=$1 # allow consequent packets (ie ESTABLISHED, RELATED but not NEW) in on spec'd interface exec_with_undo $THIS_IF_NAME "$IPTABLES_CMD -A INPUT -m state --state ESTABLISHED,RELATED -i $THIS_IF_NAME -j ACCEPT" } exec_with_undo() { local THIS_IF_NAME COMMAND THIS_IF_NAME=$1 COMMAND=$2 # deal with doing commands first; it's simple. if [ "$COMMAND" != undo ]; then ade_std_shell -n $SIMULATE "echo \"$COMMAND\" >> $STT_DIR/$THIS_IF_NAME" debug 20 "exec_with_undo: running \"$COMMAND\" ..." ade_std_shell -n $SIMULATE "$COMMAND" return $? fi # here we deal with undoing previously recorded commands. # first the 'init' commands are too difficult to deal with # and what is the effect of undoing initialisation anyway? # Logically, none. [ -f $STT_DIR/$THIS_IF_NAME ] || error "$THIS_IF_NAME: no configuration recorded" if [ $THIS_IF_NAME != init ]; then # Read previously recorded commands from the interface log. while read COMMAND; do case $COMMAND in $IPTABLES_CMD*-P*INPUT*DROP) # iptables -P DROP --> iptables -P ACCEPT COMMAND="`echo \"$COMMAND\" | sed 's/DROP/ACCEPT/'`" ;; $IPTABLES_CMD*-A*) # iptables -A --> iptables -D COMMAND="`echo \"$COMMAND\" | sed 's/ -A / -D /'`" ;; $IPTABLES_CMD*-I*) # iptables -I --> iptables -D COMMAND="`echo \"$COMMAND\" | sed 's/\( *\)-I *\([A-Z]\{2,3\}PUT\) *[0-9]\{1,\}/\1-D \2/'`" ;; echo\ 1\ *\>*/proc/*) # echo 1 > /proc --> echo 0 > /proc COMMAND="echo 0 > ${COMMAND#*>}" ;; :*) # comment --> comment COMMAND="$COMMAND" ;; *) internal "exec_with_undo: don't know how to handle \"$COMMAND\"" ;; esac debug 20 "exec_with_undo: running \"$COMMAND\" ..." ade_std_shell -n $SIMULATE "$COMMAND" done < $STT_DIR/$THIS_IF_NAME fi # after the recorded commands have been undone, remove the record. ade_std_shell -n $SIMULATE "rm -f $STT_DIR/$THIS_IF_NAME" } internal() { message crit "INTERNAL ERROR" "$1" exit 2 } info() { [ $VERBOSELEVEL -lt 3 ] || message info INFO "$1" } debug() { [ $VERBOSELEVEL -lt $1 ] || message debug "DEBUG[$1]" "$2" } warning() { [ $VERBOSELEVEL -lt 2 ] || message warning WARNING "$1" } error() { [ $VERBOSELEVEL -lt 1 ] || message err ERROR "$1" # If there is a chance the user did not see the error then shut # all ports down before exiting; we don't want to leave the system # unprotected; better blocked than unprotected. [ -t 2 ] || initialise_tables exit 1 } message() { local LEVEL PREFIX MESSAGE LEVEL="$1" PREFIX="$2" MESSAGE="$3" if [ -t 2 ]; then echo "$PROGNAME: $PREFIX: $MESSAGE" >&2 else logger -i -t $PROGNAME -p local1.$LEVEL "$PREFIX: $MESSAGE" fi } # stolen from ade ade_std_shell() { local SIMULATE CMD SUBSHELL PROMPT SHELLCMD # Default values for options SIMULATE=false SUBSHELL=false PROMPT= SHELLCMD=${SHELL:-/bin/bash} # Process options while [ "X$1" != X ]; do case $1 in -n) SIMULATE=$2 shift ;; -c) SUBSHELL=true ;; -s) SHELLCMD="$2" shift ;; -p) PROMPT="$2" shift ;; -*) ade_msg_internalerror "ade_std_shell: bad option '$1'" ;; *) break ;; esac shift done # Read arguments CMD="$1" # handle all combinations of simulation, subshells, etc. if [ "X$CMD" = X -a $SIMULATE = true ]; then ade_msg_warning "no interactive shell started, as mode is simulated" elif [ "X$CMD" = X ]; then if expr $SHELLCMD : '.*/bash$'; then PS1=$PROMPT $SHELLCMD --norc else PS1=$PROMPT $SHELLCMD fi elif [ $SIMULATE = true ]; then echo "$CMD" | sed 's/[ ][ ]*/ /g' elif [ $SUBSHELL = true ]; then $SHELLCMD -c "$CMD" else eval "$CMD" fi } main "$@"