#!/bin/bash if [ "$1" == 'debug' ]; then set -x; shift; fi if [ "$1" == 'debug2' ]; then set -xvT; shift; fi ############################################## # Don't write with 'logger' to the maillog # ... which ends up in recursion and crash ... ############################################## #------------------------- REALPATH=`realpath $0` WHERE=`dirname $REALPATH` ME=`basename $REALPATH` cd $WHERE . ../../system.conf . ../../watcher.conf . ../../common.conf . ../../common.bashlib #------------------------- . $ME.conf . private.bashlib trap refresh HUP # React on a 'HUP' for the filter refresh trap stopservice TERM INT # React on a 'KILL' trap rerun KILL 9 # React on crash trap 'errorexit $? $_' EXIT # React on script error load_filter() { # $1 .. filter status # $2 .. compress flag local funtag="[${FUNCNAME[0]}]" unset -f filter [[ -f filter ]] && mv filter filter.old [[ -f filter.compress ]] && rm -f filter.compress ./mkfilter $1 source filter # Must load here at first for the compress to work if [ -f filter.old ] then diff -c filter filter.old >filter-diffs else [[ -f filter-diffs ]] && rm -f filter-diffs fi if [ ! -z "$2" ] then # If the 'compress filter' flag is set # Compress filter; i.e. remove comments for runtime compress_filter . filter.compress trace "$funtag Using filter compressed" else trace "$funtag Using filter plain" fi # (Re)load superflous_map as well SUPERFLOUS=`mk_superflous` } refresh() { local funtag="[${FUNCNAME[0]}]" trace "$funtag Reloading filters for $ME ..." logger "$ME[$$]: $funtag Reloading filters for $ME ..." load_filter "$funtag `date --iso=seconds`" $COMPRESS_FILTER } errorexit() { local funtag="[${FUNCNAME[0]}]" logger "$ME[$$]: $funtag Trapped with exit code: ''$1', Cmd: '$2' ... loop: $loop" trace "Trapped with exit code '$1', Cmd: '$2' ... loop: $loop" # echo "$WHERE/$ME" | at "now + 1 minute" exit 255 } stopservice() { local funtag="[${FUNCNAME[0]}]" logger "$ME[$$]: $funtag Terminating on request ..." trace "$funtag Terminating on request ..." exit } rerun() { local funtag="[${FUNCNAME[0]}]" log "Abnormal termination, Cmd: '$_' ... loop: $loop" log "Line was: '$REPLY'" log "Rule: $RULE, Bandit: $BANDIT" logger "$ME[$$] $funtag Restarting in a minute ..." trace "$funtag Restarting in a minute ..." echo "$WHERE/$ME" | at "now + 1 minute" exit } # # Bump a culprit into database & firewall #---------------------------------------------------- # 'inject' is directly triggered from a rule that has # the global variable $RULE set when calling us. # So there is no need to pass $RULE to this function # # The global variable $REPLY holds the complete log line. # # All the dirty work is done here -- triggered by the rule. # inject() { local funtag="[${FUNCNAME[0]}]" local penalty=${1:-1} local retcode=0 local affairs local isdropped local haveit trace "$funtag Triggered by rule ['$RULE'] '$Pattern'" # BANDIT=`get_bandit_postfix` BANDIT=`get_bandit` if ipset -q test whitelist "$BANDIT" then return 3 # Flag whitelisted IP address and bailout fi # # Check if we have a valid IP4 address # validate_IP "$BANDIT" if [ $? -ne 0 ] then trace "$funtag Got junk for $BANDIT ... bailing out ..." return 255 fi CLASS=`get_class $BANDIT` trace "$funtag $CLASS $BANDIT" haveit=`$SQL "select IP from $TABLE where IP='$BANDIT';"` # # Introduce bandit on first attempt # if [ -z "$haveit" ] then # Preset AFFAIRS for NXDOMAINS and FAKEHOSTS ... # NX domains have no business to do on MTAs and FAKEHOSTs are suspicious anyway. # These get their affairs set up at MAX_AFFAIRS-1; i.e. they die on 2nd attempt. # The first attempt is needed to identify and classify them. # They never reach a 'COUNT' state ... if [[ "NXDOMAIN FAKEHOST" =~ "$CLASS" ]] then penalty=$(( $MAX_AFFAIRS - 1 )) fi AFFAIRS=$penalty if [ ! -z "$GEOTRACK" ] then do_geotrack $BANDIT $ME fi STATE='INTRO' ACTION="Initial $AFFAIRS/$MAX_AFFAIRS, Penalty: $penalty" $SQL "insert into $TABLE (IP,origin,rule,class,state,severity,affairs,type,date_intro,date_event) values ('$BANDIT','$ME','$RULE','$CLASS','$STATE','Initial',$AFFAIRS,'$THIS_TYPE', datetime(current_timestamp,'localtime'), datetime(current_timestamp,'localtime') );" $IPSET -exist add tarpit $BANDIT timeout $((2**AFFAIRS * TIME_SLICE)) comment "$THIS_TYPE,$AFFAIRS" retcode=1 else isdropped=`$SQL "select IP from $TABLE where IP='$BANDIT' and state='DROP';"` if [ ! -z "$isdropped" ] then trace "$funtag DROPed culprit $BANDIT re-occured ... taken into custody ..." $IPSET -exist add custody $BANDIT comment "$THIS_TYPE,$CLASS" return 1 fi # # Bandit returned repeatedly ... count-up affairs ... # AFFAIRS=`$SQL "select affairs from $TABLE where IP='$BANDIT';"` ((AFFAIRS++)) if [ $AFFAIRS -lt $MAX_AFFAIRS ] then STATE='COUNT' ACTION="Count $AFFAIRS/$MAX_AFFAIRS" $SQL "update $TABLE set affairs=$AFFAIRS, origin='$ME', state='$STATE',severity='Counted', rule='$RULE', date_event=datetime(current_timestamp,'localtime') where IP='$BANDIT';" $IPSET -exist add tarpit $BANDIT timeout $((2**AFFAIRS * TIME_SLICE)) comment "$THIS_TYPE,$AFFAIRS" retcode=1 else # Drop the beast ... STATE='DROP' ACTION="Dropped @ $AFFAIRS" $SQL "update $TABLE set affairs=$MAX_AFFAIRS, origin='$ME', state='$STATE', severity='Dropped', rule='$RULE', date_event=datetime(current_timestamp,'localtime') where IP='$BANDIT';" $IPSET -exist add tarpit $BANDIT timeout $((2**AFFAIRS * TIME_SLICE)) comment "$THIS_TYPE,$AFFAIRS" retcode=1 fi fi write_affairs $BANDIT $AFFAIRS $RULE $ME #log "$funtag $CLASS $BANDIT - Rule: $RULE, Action: $ACTION" clog "$funtag" "$CLASS" "$BANDIT" "'$ACTION'" "'$RULE'" trace "$funtag $CLASS $BANDIT '$ACTION'" return $retcode } #---------------------- Main program ---------------- dump_runtime # # Start companion process 'WatchMB' ... # COMPANION=`ps -ef| grep -v "grep"| grep -o "modules/WatchMB" | uniq` if [ -z "$COMPANION" ] then (cd $MASTER_PATH; modules/WatchMB/WatchMB &) logger "$ME[$$]: Companion process 'WatchMB' started as process $! ..." else logger "$ME[$$]: Companion process 'WatchMB' already running ..." fi THIS_IP=`dig +short $HOSTNAME` # Bash knows HOSTNAME ln -sf $WHERE/MXinjector $TOOLS_LINK trace "--- $ME started on $HOSTNAME ... $SYSTEM $SYSVERS" trace "Watcher: $PRODUCT, $REVISION" # Set 'time slice' if missing or empty #if [ -z "$TIME_SLICE" ]; then TIME_SLICE=60; fi : "${TIME_SLICE:=60}" if [ ! -p $PIPE ] then mkfifo -m 600 $PIPE fi touch $LOG chmod 600 $LOG touch $ME.trace chmod 600 $ME.trace load_filter Initial $COMPRESS_FILTER # Here we go ... log "Started logging to $LOG ..." logger "$ME[$$]: Started listening on pipe $PIPE using dynamic filters" log "Started listening on $PIPE using dynamic filters" trace "Started listening on $PIPE using dynamic filters" renice -10 $$ trace "Starting scanner ..." while : # Only a crash can stop us .... do read < $PIPE ((loop++)) : echo "-------Loop: $loop----------------" LOOP_START=`date +%s%3N` #DYN_ADDR=`$DYN_CMD` read DYN_ADDR < $LOG_DIR/DYN_IP : echo "$DYN_ADDR" # Empty lines kicked-off before we trace anything if [ -z "$REPLY" ] then : echo "Eeek! Empty line ..." continue fi # ------ Internal Rule set -------------- # # Kick junk off ... # # Must come FIRST of all RULE=Superflous # SUPER=`grep -oE "$SUPERFLOUS" <<< $REPLY` # if [ ! -z "$SUPER" ] [[ "$REPLY" =~ ($SUPERFLOUS) ]] if [ -n "${BASH_REMATCH[0]}" ] then : echo "Superflous stuff ... ignored ..." continue fi RULE=Freehost if [[ "$REPLY" =~ "@Freehost:" ]] then DEL_ADDR=`get_bandit` : trace "$ME/$RULE Deleting $DEL_ADDR from $TABLE" freehost $DEL_ADDR continue fi RULE=Self if [[ "$REPLY" =~ "$THIS_IP" ]] then : trace "$ME/$RULE Oops ... me $THIS_IP" continue fi RULE=DynIP if [[ "$REPLY" =~ "$DYN_ADDR" ]] then : trace "$ME/$RULE Oops ... my dyn IP $DYN_ADDR" continue fi # Careful here $TRUSTED is a LIST OF IP addresses RULE=Trusted if [[ "$REPLY" =~ ($TRUSTLIST) ]] then : trace "$ME/$RULE Oops ... ${BASH_REMATCH[0]} in trusted hosts ... skipping" continue fi # -------------------------------------- dump_loadrate trace "[Loop: $loop] '$REPLY'" FILT_START=`date +%s%3N` filter FILT_RESULT=$? FILT_END=`date +%s%3N` FILT_TIME=$((FILT_END-FILT_START)) LOOP_TIME=$((FILT_END-LOOP_START)) # # Post processor ... # Check return code of 'filter' ... case $FILT_RESULT in 0) trace "[filter] $RULE ... fall through, $LOOP_TIME/$FILT_TIME ms" ;; 1) trace "[inject] Finished for rule ['$RULE'], $LOOP_TIME/$FILT_TIME ms" continue ;; 2) : echo "Passed to WatchMB" trace "Passed to WatchMB ... $LOOP_TIME/$FILT_TIME ms" continue ;; 3) : echo "$BANDIT White-listed ... nothing to do ..." continue ;; 4) : echo "Retcode $FILT_RESULT ... RBL, SPAMhaus and such" continue ;; 254) trace "Ignored ... [$RULE] '$Pattern'" continue ;; 255) : echo "[inject] Trash in rule: $RULE, RETCODE '$FILT_RESULT'" ;; *) : echo "Retcode '$FILT_RESULT' unhandled ... fall through" ;; esac # -------------------- # Must be the last rule in chain just report what is uncovered RULE=UNTREATED log "[$RULE] '$REPLY'" trace "[$RULE] '$REPLY'" done # Normally never reached - except on erratic termination logger "$ME[$$] Terminated erratically" log "Erratically terminated, exit status: $?, Loop: $loop" trace "Erratically terminated, exit status: $?, Loop: $loop" # # We restart ourselves ... # rerun