#!/usr/bin/perl ###################################################################### # floodmon : # # SYN flood attacks monitoring / alert / mitigation daemon. # # (c) Jerome Bruandet - http://spamcleaner.org/ # # version 0.9.4 - 07-Jan-2010 # # doc : http://spamcleaner.org/en/misc/floodmon.html # ###################################################################### # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. ###################################################################### use strict; my $file = 'floodmon'; my $log_dir = '/var/log'; my $pid_dir = '/var/run'; my $cache_dir = '/var/cache/floodmon'; my $conf_dir = '/etc'; my $tcpbackup = 'tcpbackup.sh'; # munin_node : if ( $ARGV[0] eq '--munin-node' ) { &munin_node } elsif ( $ARGV[0] eq 'config' ) { &munin_node(1) } my $version = 'v0.9.4'; my $appname = 'floodmon'; my $copyright = '(c)2009-2010 Jerome Bruandet - http://spamcleaner.org/'; if ( $ARGV[0] eq '--help' ) { &help } if ( $> ) { print "$appname $version : $copyright\n\n" . "\t[ERROR] : you must be root to run that script\n\n"; exit 1; } if ( $^O ne 'linux' ) { print "$appname $version : $copyright\n\n" . "\t[ERROR] : sorry, that script is for Linux only, not $^O\n\n"; exit 1; } if ( ! -d '/proc' ) { print "$appname $version : $copyright\n\n" . "\t[ERROR] : cannot find /proc ! It should be mounted\n\t" . "in order to run floodmon (see #man fstab).\n\n"; exit 1; } # we cannot modify the TCP/IP stack if we are running inside # a Virtuozzo or OpenVZ container. The daemon will only run # in 'light mode' : my $openVZ; if ( ( -d '/proc/vz' ) && ( ! -d '/proc/bc' ) ) { $openVZ = "openVZ/Virtuozzo detected, 'light mode' enabled"; } use POSIX qw(setsid); use MIME::QuotedPrint; use Socket; $SIG{INT} = \&stop; $SIG{QUIT} = \&stop; $SIG{TERM} = \&stop; $SIG{HUP} = \&stop; $SIG{USR1} = \&reload_conf; $SIG{USR2} = \&stop_save; my ( $debug, $pid, %conf ); ###################################################################### ## alert levels : if you want to change those values, please change ## ## them in the configuration file /etc/floodmon.conf : ## ###################################################################### my %LEVEL = ( 1 => { 'name' => "Can I play Daddy ? ~:)", # 0 to 350 SYN : 'max_syn' => 350, # cookies not needed yet : 'syn_cookies' => 0, 'max_syn_queue' => 10000, 'somaxconn' => 10000, # x3 => 45 seconds time-out : 'max_synack_retry' => 3, 'conntrack_timeout' => 60, 'loop_delay' => 15, 'conntrack_max' => 65536, 'conntrack_hashsize'=> 16384, 'nullroute_subnet' => 0, 'best_effort' => 0 }, 2 => { 'name' => "Don't hurt me :-(", # from 351 to 500 SYN : 'max_syn' => 500, 'syn_cookies' => 1, 'max_syn_queue' => 20000, 'somaxconn' => 20000, # x2 => 21 seconds time-out : 'max_synack_retry' => 2, # same value for ip_conntrack : 'conntrack_timeout' => 21, 'loop_delay' => 1.5, # conntrack will use +/- 19Mb of RAM : 'conntrack_max' => 65536, 'conntrack_hashsize'=> 65536, 'nullroute_subnet' => 100, 'best_effort' => 0 }, 3 => { 'name' => "Bring'em on >o(", # from 501 to 1000 SYN : 'max_syn' => 1000, 'syn_cookies' => 1, 'max_syn_queue' => 100000, 'somaxconn' => 40000, # x1 => 9 seconds time-out : 'max_synack_retry' => 1, 'conntrack_timeout' => 9, 'loop_delay' => 1, 'conntrack_max' => 131072, 'conntrack_hashsize'=> 65536, 'nullroute_subnet' => 200, 'best_effort' => 1 }, 4 => { # 1,000+ SYN 'name' => "I'm death incarnate 8=X", # max_syn value doesn't matter here, # however it **must** be higher than # the value of the previous level : 'max_syn' => 2000, 'syn_cookies' => 1, 'max_syn_queue' => 200000, # let's be generous : 'somaxconn' => 65000, # no more retransmission (just like # syncookies), that gives us # a 3 seconds time-out : 'max_synack_retry' => 0, 'conntrack_timeout' => 3, # 1/2 second sleep : 'loop_delay' => 0.50, # conntrack will take +/-38Mb of RAM : 'conntrack_max' => 131072, 'conntrack_hashsize'=> 131072, 'nullroute_subnet' => 300, 'best_effort' => 1 }, ); ###################################################################### my $tot_levels = keys %LEVEL; my $c_level = 1; my ( $netmask, $regexp_block, $suffix ); # load user config : &load_conf; # TCP connection regexp (tcp & tcp6) : my %proc_net; if ( -e '/proc/net/tcp' ) { $proc_net{'/proc/net/tcp'} = '^\s*\d+:\s(?!(?:0{8}|0100007F):.{4}).'. '{8}:.{4}\s(?!(?:0{8}|0100007F):.{4})(.{8}):.{4}\s03\s'; } if ( -e '/proc/net/tcp6' ) { $proc_net{'/proc/net/tcp6'} = '^\s*\d+:\s\d{16}FFFF0000(?!(?:0{8}|'. '0100007F):.{4}).{8}:.{4}\s\d{16}FFFF0000(?!(?:0{8}|0100007F):.'. '{4})(.{8}):.{4}\s03\s'; } # SYN/ACK retransmission : my $p_tcp_synack_retries = '/proc/sys/net/ipv4/tcp_synack_retries'; # syncookies : my $p_tcp_syncookies = '/proc/sys/net/ipv4/tcp_syncookies'; # SYN backlog : my $p_tcp_max_syn_backlog = '/proc/sys/net/ipv4/tcp_max_syn_backlog'; # listen() backlog : my $p_tcp_somaxconn = '/proc/sys/net/core/somaxconn'; # connection tracking table : my ( $p_ctk_synrecv, $p_ctk_synsent, $no_conntrack, $p_ctk_max, $p_ctk_hash ); if ( -e '/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_syn_sent' ) { $p_ctk_synrecv = '/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_syn_recv'; $p_ctk_synsent = '/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_syn_sent'; $p_ctk_max = '/proc/sys/net/netfilter/nf_conntrack_max'; $p_ctk_hash = '/sys/module/nf_conntrack/parameters/hashsize'; } elsif ( -e '/proc/sys/net/ipv4/netfilter/ip_conntrack_tcp_timeout_syn_sent' ) { $p_ctk_synrecv = '/proc/sys/net/ipv4/netfilter/ip_conntrack_tcp_timeout_syn_recv'; $p_ctk_synsent = '/proc/sys/net/ipv4/netfilter/ip_conntrack_tcp_timeout_syn_sent'; $p_ctk_max = '/proc/sys/net/ipv4/netfilter/ip_conntrack_max'; $p_ctk_hash = '/sys/module/ip_conntrack/parameters/hashsize'; } else { $no_conntrack = 1; } # reverse path filter : my %p_rp_filter =( '/proc/sys/net/ipv4/conf/all/rp_filter' => 1, '/proc/sys/net/ipv4/conf/lo/rp_filter' => 1, '/proc/sys/net/ipv4/conf/default/rp_filter' => 1, '/proc/sys/net/ipv4/conf/'.$conf{'INTERFACE'}.'/rp_filter' => 1 ); my %best_effort = ( '/proc/sys/net/ipv4/tcp_rfc1337' => '0', '/proc/sys/net/ipv4/tcp_tw_recycle' => '1', '/proc/sys/net/ipv4/tcp_tw_reuse' => '1', '/proc/sys/net/ipv4/tcp_max_tw_buckets' => '65535', '/proc/sys/net/ipv4/tcp_fin_timeout' => '15', '/proc/sys/net/ipv4/ip_local_port_range' => '"1024 65000"' ); my $best_effort_act = 0; # number of loops to perform before switching # to a lower alert level : my $maxwait_level = 10; my $wait_level = 0; my ( %nullrouted_blocks, $last_flush ); # SYN paquets dump: my $dumper; my $mailprog = `which sendmail`; if ( $? >> 8){ print "$appname $version : $copyright\n\n" . "\t[ERROR] : cannot find [sendmail] !\n\n"; exit 1; }else{ chomp $mailprog; } my $ip_route = `which ip`; if ( $? >> 8){ print "$appname $version : $copyright\n\n" . "\t[ERROR] : cannot find [ip] command !\n" . "\tPlease install 'iproute' package to run $appname\n\n"; exit 1; } my $last_admin_alert = ( time - $conf{'EMAIL_ALERT_FREQ'} ) - 1; my $last_sms_alert = ( time - $conf{'SMS_ALERT_FREQ'} ) - 1; my $last_checkupdates = ( time - $conf{'CHECK_UPDATES'} ) - 1; ###################################################################### my %menu = ( '--debug' => sub { $debug = 1; &run }, '--daemon' => \&run, '--reload' => \&send_reload_signal, '--stop' => \&send_stop_signal, '--stop-save' => \&send_stop_save_signal, '--stats' => \&stats, '--sms-test' => \&sms_test, '--capture' => \&capture, '--version' => sub { \&check_update(1) } ); if ( $menu{$ARGV[0]} ) { $menu{$ARGV[0]}->() } else { print "$appname $version : no parameters specified !\n\n" . "\trun : $appname --help\n\n"; } exit; ###################################################################### # Load user configuration file (/etc/floodmon.conf) : sub load_conf { # reload signal ? my $sig = shift; goto OPEN_CONF if ($sig); if (! -e "$conf_dir/$file.conf" ) { print "$appname $version : $copyright\n\n" . "\t[ERROR] : cannot open [$conf_dir/$file.conf]\n\n"; exit 1; } OPEN_CONF: open CONFIG, "<$conf_dir/$file.conf"; while () { chomp; if (/^([A-Z][^\s]+)\s*=\s*(\d+|['"][^'"]+['"])/){ my $var=$1; my $value=$2 ; $value =~ s/['"]//g; $conf{$var} = $value; } } close CONFIG; if (! $conf{'INTERFACE'} ) { print "$appname $version : $copyright\n\n" . "\t[ERROR] : INTERFACE is not setup " . "in $conf_dir/$file.conf\n\n"; exit 1; }else{ `ifconfig | grep '^$conf{'INTERFACE'}' > /dev/null`; if ( $? >> 8) { print "$appname $version : $copyright\n\n" . "\t[ERROR] : interface [".$conf{'INTERFACE'} . "] cannot be found !\n\tPlease double-check" . " $conf_dir/$file.conf\n\n"; exit 1; } } if ( $conf{'NO_CONNTRACK'} == 1 ) { $no_conntrack = 1 } if (! $conf{'FLUSH_FREQ'} ) { $conf{'FLUSH_FREQ'} = 1800 } else { $conf{'FLUSH_FREQ'}*= 60 } if (! $conf{'EMAIL_ALERT_FREQ'} ) {$conf{'EMAIL_ALERT_FREQ'} = 600} else { $conf{'EMAIL_ALERT_FREQ'}*= 60 } if ( ( $conf{'EMAIL_ALERT'} < 2 ) || ( ! $conf{'ADMIN_EMAIL'} ) ) { $conf{'EMAIL_ALERT'} = 0; } $conf{'SMS_ALERT_FREQ'}*= 3600 if ( $conf{'SMS_ALERT_FREQ'} ); if ( $conf{'CHECK_UPDATES'} =~ /^\d+$/ ) { $conf{'CHECK_UPDATES'}*= 86400 } else { $conf{'CHECK_UPDATES'} = 0; } if ( $conf{'NETMASK'} == 8 ) { $netmask = 8; $regexp_block = '^(\d{1,3})'; $suffix = '.0.0.0'; } elsif ( $conf{'NETMASK'} == 24 ) { $netmask = 24; $regexp_block = '^(\d{1,3}\.\d{1,3}\.\d{1,3})'; $suffix = '.0'; } else { $netmask = 16; $regexp_block = '^(\d{1,3}\.\d{1,3})'; $suffix = '.0.0'; } # SYN packets dump ? if ( $conf{'DUMP_SYNPACKETS'} =~ /^[1-9][0-9]?$/ ) { if ( eval "use Net::Pcap" ) { print "$appname $version : $copyright\n\n" . "\t[ERROR] : [Net::Pcap] module not " . "found !\n\tIf you don't want to use it, " . "deactivate the 'DUMP_SYNPACKETS'\n\tvariable" . "in $conf_dir/$file.conf\n\n"; exit 1; } else { use Net::Pcap; } } else { $conf{'DUMP_SYNPACKETS'} = 0 } if ($conf{'MAX_SYN'} =~ /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/){ $LEVEL{1}->{'max_syn'} = $1; $LEVEL{2}->{'max_syn'} = $2; $LEVEL{3}->{'max_syn'} = $3; $LEVEL{4}->{'max_syn'} = $4; } if ($conf{'SYN_COOKIES'} =~ /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/){ $LEVEL{1}->{'syn_cookies'} = $1; $LEVEL{2}->{'syn_cookies'} = $2; $LEVEL{3}->{'syn_cookies'} = $3; $LEVEL{4}->{'syn_cookies'} = $4; } if ($conf{'MAX_SYN_QUEUE'} =~ /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/){ $LEVEL{1}->{'max_syn_queue'} = $1; $LEVEL{2}->{'max_syn_queue'} = $2; $LEVEL{3}->{'max_syn_queue'} = $3; $LEVEL{4}->{'max_syn_queue'} = $4; } if ($conf{'SOMAXCONN'} =~ /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/){ $LEVEL{1}->{'somaxconn'} = $1; $LEVEL{2}->{'somaxconn'} = $2; $LEVEL{3}->{'somaxconn'} = $3; $LEVEL{4}->{'somaxconn'} = $4; } if ($conf{'MAX_SYNACK_RETRY'} =~ /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/){ $LEVEL{1}->{'max_synack_retry'} = $1; $LEVEL{2}->{'max_synack_retry'} = $2; $LEVEL{3}->{'max_synack_retry'} = $3; $LEVEL{4}->{'max_synack_retry'} = $4; } if ($conf{'CONNTRACK_TIMEOUT'} =~ /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/){ $LEVEL{1}->{'conntrack_timeout'} = $1; $LEVEL{2}->{'conntrack_timeout'} = $2; $LEVEL{3}->{'conntrack_timeout'} = $3; $LEVEL{4}->{'conntrack_timeout'} = $4; } if ($conf{'LOOP_DELAY'} =~ /^(.+)\s+(.+)\s+(.+)\s+(.+)$/){ $LEVEL{1}->{'loop_delay'} = $1; $LEVEL{2}->{'loop_delay'} = $2; $LEVEL{3}->{'loop_delay'} = $3; $LEVEL{4}->{'loop_delay'} = $4; } if ($conf{'CONNTRACK_MAX'} =~ /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/){ $LEVEL{1}->{'conntrack_max'} = $1; $LEVEL{2}->{'conntrack_max'} = $2; $LEVEL{3}->{'conntrack_max'} = $3; $LEVEL{4}->{'conntrack_max'} = $4; } if ($conf{'CONNTRACK_HASHSIZE'} =~ /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/){ $LEVEL{1}->{'conntrack_hashsize'} = $1; $LEVEL{2}->{'conntrack_hashsize'} = $2; $LEVEL{3}->{'conntrack_hashsize'} = $3; $LEVEL{4}->{'conntrack_hashsize'} = $4; } if ($conf{'NULLROUTE_SUBNET'} =~ /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/){ $LEVEL{1}->{'nullroute_subnet'} = $1; $LEVEL{2}->{'nullroute_subnet'} = $2; $LEVEL{3}->{'nullroute_subnet'} = $3; $LEVEL{4}->{'nullroute_subnet'} = $4; } if ($conf{'BEST_EFFORT'} =~ /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/) { $LEVEL{1}->{'best_effort'} = $1; $LEVEL{2}->{'best_effort'} = $2; $LEVEL{3}->{'best_effort'} = $3; $LEVEL{4}->{'best_effort'} = $4; } } ###################################################################### # Main loop : sub run { my $res = &is_running; if (! $res ) { print "$appname $version : $copyright\n\n" . "\t[ERROR] : daemon is already running (PID : $pid)\n\n"; exit 1; } my ( $t0, $t1 ); if ( $debug ) { use Time::HiRes qw(gettimeofday tv_interval); $| =1; } else { &daemonize } # save PID : open PID, ">$pid_dir/$file"; print PID $$; close PID; if (! -d "$cache_dir" ) { mkdir "$cache_dir" } else{ unlink <$cache_dir/*.lev> } open LEV, ">$cache_dir/$c_level.lev"; close LEV; goto LOG2FILE if ( $openVZ ); # save TCP/IP current parameters : goto NOTCPSAVE if ( -e "$cache_dir/$tcpbackup" ); open STACK, ">$cache_dir/$tcpbackup"; print STACK "#!/bin/sh\n"; $res = `cat $p_tcp_synack_retries`; chomp $res; print STACK "echo $res > $p_tcp_synack_retries\n"; $res = `cat $p_tcp_syncookies`; chomp $res; print STACK "echo $res > $p_tcp_syncookies\n"; $res = `cat $p_tcp_max_syn_backlog`; chomp $res; print STACK "echo $res > $p_tcp_max_syn_backlog\n"; $res = `cat $p_tcp_somaxconn`; chomp $res; $res = 1024 if ( $res < 1024 ); print STACK "echo $res > $p_tcp_somaxconn\n"; foreach my $key (keys %p_rp_filter){ $res = `cat $key`; chomp $res; print STACK "echo $res > $key\n"; } foreach my $key ( keys %best_effort ) { $res = `cat $key`; chomp $res; $res =~ s/\s+/ / if ( $key =~ /ip_local_port_range/ ); print STACK "echo \"$res\" > $key\n"; } if ( ! $no_conntrack ) { $res = `cat $p_ctk_synrecv`; chomp $res; print STACK "echo $res > $p_ctk_synrecv\n"; $res = `cat $p_ctk_synsent`; chomp $res; print STACK "echo $res > $p_ctk_synsent\n"; $res = `cat $p_ctk_max`; chomp $res; print STACK "echo $res > $p_ctk_max\n"; $res = `cat $p_ctk_hash`; chomp $res; print STACK "echo $res > $p_ctk_hash\n"; } close STACK; chmod 0700, "$cache_dir/$tcpbackup"; NOTCPSAVE: # TCP/IP stack modifications : foreach my $key ( keys %p_rp_filter ){ `echo $p_rp_filter{$key} > $key`; } `echo $LEVEL{$c_level}->{'syn_cookies'} > $p_tcp_syncookies`; `echo $LEVEL{$c_level}->{'max_syn_queue'} > $p_tcp_max_syn_backlog`; `echo $LEVEL{$c_level}->{'max_synack_retry'} > $p_tcp_synack_retries`; `echo $LEVEL{$c_level}->{'somaxconn'} > $p_tcp_somaxconn`; if ( ! $no_conntrack ) { `echo $LEVEL{$c_level}->{'conntrack_max'} > $p_ctk_max`; `echo $LEVEL{$c_level}->{'conntrack_hashsize'} > $p_ctk_hash`; `echo $LEVEL{$c_level}->{'conntrack_timeout'} > $p_ctk_synrecv`; `echo $LEVEL{$c_level}->{'conntrack_timeout'} > $p_ctk_synsent`; } LOG2FILE: # log file : if (! -d "$log_dir"){ mkdir "$log_dir" } open LOG, ">>$log_dir/$file.log"; my $date=`date '+%b %e %T'`; chomp $date; print LOG "$date [notice] $appname $version : running " . "daemon (PID : $$)\n"; print LOG "$date [notice] $appname $version : $openVZ\n" if $openVZ; if ( $debug ) { print "\n$appname $version : running daemon (PID : $$)\n\n"; print "\t=> $openVZ\n\n" if ( $openVZ); } # let's try not to null-route the admin... :D my %ignored_block; my @tmp_buff = `last -n 5 -ia root`; foreach my $tmp_ip ( @tmp_buff ) { if ( $tmp_ip =~ /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) { $tmp_ip = $1; $tmp_ip =~ /$regexp_block/; $ignored_block{$1.$suffix} = 1; } } # create a whitelist with the server IP(s) : my %srv_ip; @tmp_buff =`ifconfig|grep 'inet addr:'|awk '{print \$2}'|cut -d: -f2|uniq`; foreach ( @tmp_buff ) { chomp; $srv_ip{$_} = 1; } my $first_try = 1; LOOP: while(1) { my $sleep = $LEVEL{$c_level}->{'loop_delay'}; goto FIRST_TRY if ( $first_try ); if ( $debug ) { print "[ ] total time : " .sprintf("%.3f", tv_interval $t0, [gettimeofday])."s\n"; print "[ ] alert level : " . "$c_level/$tot_levels\n\n" . "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ " . "$LEVEL{$c_level}->{'name'}\n" . "\n\n" ; } select( undef, undef, undef, $sleep ); FIRST_TRY: $t0 = [gettimeofday] if $debug; my $now = time; # flush routing table ? if ( ( $last_flush ) && ( $now - $last_flush > $conf{'FLUSH_FREQ'} ) ) { $last_flush = 0; $res=0; foreach my $key ( keys %nullrouted_blocks ) { `ip route del $key/$netmask 2> /dev/null`; $res++; } if ( $res ) { if ( $conf{'LOG_LEVEL'} == 2 ) { print LOG "$date [------] flushing null-routed" . " blocks : x $res /$netmask\n"; } if ( $debug ) { print "[-] flushed null-routed IPs : " . "x $res /$netmask\n"; } } %nullrouted_blocks = (); } $first_try = 0; $date = `date '+%b %e %T'`; chomp $date; my $tot_blocks_nullrouted = 0; my $tot_ip_nullrouted = 0; my %remote_subnet = (); my %syn_ip = (); my $tot_syn_recv = '0'; foreach my $tcp_version ( keys %proc_net ) { open PNT, $tcp_version; while ( ) { if ( /$proc_net{$tcp_version}/ ) { my $hrem_address = $1; $tot_syn_recv++; # remote IP conversion : hex little-endian => decimal $hrem_address =~ /(.{2})(.{2})(.{2})(.{2})/; my $rem_address= hex($4).".".hex($3).".".hex($2).".".hex($1); # ignore it if it is whitelisted : next if $srv_ip{$rem_address}; $rem_address =~ /$regexp_block/; my $rem_block = $1.$suffix; # IP's counter : $syn_ip{$rem_address}++; # if the block has just been null-routed, # we don't do anything : next if $nullrouted_blocks{$rem_block}; # save blocks : $remote_subnet{$rem_block}++; } } close PNT; } RESULTS: if ($debug) { print "[ ] number of SYN_RECV : $tot_syn_recv" ; if ( $tot_syn_recv < $LEVEL{1}->{'max_syn'} ) { print " => [OK]\n"; }else{ print " => [ALERT]\n"; } } # any block to null-route ? if ( ( $LEVEL{$c_level}->{'nullroute_subnet'} ) && ( %remote_subnet ) ){ foreach my $key ( keys %remote_subnet ) { if ( (! $nullrouted_blocks{$key} ) && (! $ignored_block{$key} ) && ( $remote_subnet{$key} > $LEVEL{$c_level}->{'nullroute_subnet'} ) ){ $nullrouted_blocks{$key} = 1; $tot_blocks_nullrouted++; # blackhole : `ip route add blackhole $key/$netmask 2> /dev/null`; $last_flush = $now if (! $last_flush ); } } if ( $tot_blocks_nullrouted ) { if ( $debug ) { print "[X] null-routed blocks (/$netmask) : " . "$tot_blocks_nullrouted null-routed (more than " . "$LEVEL{$c_level}->{'nullroute_subnet'} " . "SYN_RECV)\n"; } if ( $conf{'LOG_LEVEL'} == 2 ) { print LOG "$date [XXXXXX] $tot_blocks_nullrouted blocks ". "/$netmask null-routed (more than ". "$LEVEL{$c_level}->{'nullroute_subnet'} ". "SYN_RECV)\n"; } }else{ if ( $debug ) { print "[ ] number of blocks (/$netmask) : " .(keys %remote_subnet)."\n"; } } } if ($debug) { print "[ ] number of individual IPs : ".(keys %syn_ip)."\n"; } if ( (! $best_effort_act ) && ( $LEVEL{$c_level}->{'best_effort'} ) && ( ! $openVZ ) ) { foreach my $key ( keys %best_effort ) { `echo $best_effort{$key} > $key` } print "[+] TCP/IP modifications : activating " . "\%best_effort\n" if $debug; $best_effort_act = 1; } # do we need to change the alert level ? if ( ( $tot_syn_recv >= $LEVEL{$c_level}->{'max_syn'} ) && ( $c_level < $tot_levels ) ) { # increase : unlink "$cache_dir/$c_level.lev"; $c_level++; $wait_level = 0; if ( $conf{'LOG_LEVEL'} == 2 ) { print LOG "$date [ ] current number of SYN_RECV : " . "$tot_syn_recv\n" . "$date [++++++] increasing alert level : " . "$c_level/$tot_levels\n"; } print "[+] increasing alert level\n" if ( $debug ); open LEV, ">$cache_dir/$c_level.lev"; close LEV; goto openvz if ( $openVZ ); # adjust TCP/IP stack parameters : `echo $LEVEL{$c_level}->{'max_syn_queue'} > $p_tcp_max_syn_backlog`; `echo $LEVEL{$c_level}->{'max_synack_retry'} > $p_tcp_synack_retries`; `echo $LEVEL{$c_level}->{'somaxconn'} > $p_tcp_somaxconn`; `echo $LEVEL{$c_level}->{'syn_cookies'} > $p_tcp_syncookies`; # adjust connection tracking table : if (! $no_conntrack ) { `echo $LEVEL{$c_level}->{'conntrack_max'} > $p_ctk_max`; `echo $LEVEL{$c_level}->{'conntrack_hashsize'} > $p_ctk_hash`; `echo $LEVEL{$c_level}->{'conntrack_timeout'} > $p_ctk_synrecv`; `echo $LEVEL{$c_level}->{'conntrack_timeout'} > $p_ctk_synsent`; } openvz: # email alert (except if debug) + capture of SYN packets : if ( ($conf{'EMAIL_ALERT'} == $c_level ) && (! $debug ) && ( $now - $last_admin_alert > $conf{'EMAIL_ALERT_FREQ'} ) ) { $last_admin_alert = $now; # we fork and do not care about our child, # it will exit before us anyway : use POSIX 'WNOHANG'; $SIG{CHLD} = sub { while( waitpid( -1,WNOHANG ) > 0 ) {} }; defined (my $kidemail = fork) or die "cannot fork : $!\n"; if ( $kidemail == 0 ) { # let it go : setsid or die "cannot setid : $!"; my $msg = "On ".`date "+%d-%m-%Y at %T"`; $msg.= "*** Possible SYN flood attack ***\n\n"; $msg.="- number of SYN_RECV : $tot_syn_recv\n"; $msg.="- number of IP blocks : " .(keys %remote_subnet)."\n"; $msg.="- IP blocks nullrouted : "; $msg.="$tot_blocks_nullrouted\n"; if ( $tot_blocks_nullrouted ) { my $num = 0; foreach my $key ( keys %nullrouted_blocks ) { if ( $num >= 50 ) { $msg.= "\t- ...\n"; last; } $msg.= "\t- block $key.0.0/$netmask "; $msg.= "($remote_subnet{$key} connections)\n"; $num++; } } $msg.="- number of individual IPs : " .(keys %syn_ip)."\n"; $msg.="- current alert level : "; $msg.="$c_level/$tot_levels\n\n"; $msg.="Please connect to your server and check the logfile :\n"; $msg.="$log_dir/$file.log\n"; &email_admin($msg,0); exit 0; } } # SMS alert (except if debug mode) ? if ( ( $conf{'SMS_ALERT'} =~ /^https?:/i ) && (! $debug ) ) { if ( -e "$cache_dir/sms.alert" ) { open SMS, "<$cache_dir/sms.alert"; $last_sms_alert = ; close SMS; } if ( $now - $last_sms_alert > $conf{'SMS_ALERT_FREQ'} ) { $last_sms_alert = $now; open SMS, ">$cache_dir/sms.alert"; print SMS "$last_sms_alert"; close SMS; use POSIX 'WNOHANG'; $SIG{CHLD} = sub { while( waitpid( -1,WNOHANG ) > 0 ) {} }; defined (my $kidsms = fork) or die "cannot fork : $!\n"; if ( $kidsms == 0 ) { setsid or die "cannot setid : $!"; my $wget="wget -nv --no-check-certificate --spider --quiet"; `$wget '$conf{'SMS_ALERT'}' > /dev/null 2>&1`; $res = ( $? >> 8); if ( $res ) { print LOG "$date [ERROR] : error (\#$res) "; print LOG "while trying to send an SMS\n"; } exit 0; } } } } elsif ( ( $tot_syn_recv < $LEVEL{$c_level-1}->{'max_syn'} ) && ( $c_level > 1 ) ) { # we wait a bit ($maxwait_level) before switching # to a lower level: if ( $wait_level <= $maxwait_level ) { $wait_level++; next; } $wait_level = 0; # decrease alert level : unlink "$cache_dir/$c_level.lev"; $c_level--; if ( $conf{'LOG_LEVEL'} == 2 ) { print LOG "$date [ ] current number of SYN_RECV : " . "$tot_syn_recv\n" . "$date [------] decreasing alert level : " . "$c_level/$tot_levels\n"; } print "[-] decreasing alert level\n" if ( $debug ); open LEV, ">$cache_dir/$c_level.lev"; close LEV; goto CHECK_UPDATES if ( $openVZ ); # adjust TCP/IP stack parameters : `echo $LEVEL{$c_level}->{'max_syn_queue'} > $p_tcp_max_syn_backlog`; `echo $LEVEL{$c_level}->{'max_synack_retry'} > $p_tcp_synack_retries`; `echo $LEVEL{$c_level}->{'somaxconn'} > $p_tcp_somaxconn`; `echo $LEVEL{$c_level}->{'syn_cookies'} > $p_tcp_syncookies`; if (! $no_conntrack ) { `echo $LEVEL{$c_level}->{'conntrack_max'} > $p_ctk_max`; `echo $LEVEL{$c_level}->{'conntrack_hashsize'} > $p_ctk_hash`; `echo $LEVEL{$c_level}->{'conntrack_timeout'} > $p_ctk_synrecv`; `echo $LEVEL{$c_level}->{'conntrack_timeout'} > $p_ctk_synsent`; } } CHECK_UPDATES: # only check for updates when we are not under attack : if ( ( $c_level == 1 ) && ( $last_checkupdates ) && ( $now - $last_checkupdates > $conf{'CHECK_UPDATES'} ) ) { $last_checkupdates = $now; use POSIX 'WNOHANG'; $SIG{CHLD} = sub { while( waitpid( -1,WNOHANG ) > 0 ) {} }; defined (my $kidchk = fork) or die "cannot fork : $!\n"; if ( $kidchk == 0 ) { setsid or die "cannot setid : $!"; &check_update; exit; } } } } ###################################################################### # Become a daemon : sub daemonize { chdir '/' or die "- [ERROR] : cannot chdir to [/] : $!\n"; umask 0; open STDIN, '/dev/null' or die "- [ERROR] : cannot read [/dev/null] : $!\n"; open STDOUT, '>/dev/null' or die "- [ERROR] : cannot write to [/dev/null] : $!\n"; open STDERR, '>/dev/null' or die "- [ERROR] : cannot write to [/dev/null] : $!\n"; defined(my $pid = fork) or die "- [ERROR] : cannot fork : $!\n"; exit if $pid; setsid or die "- [ERROR] : cannot start a session : $!\n"; } ###################################################################### # Check if the daemon is already running : sub is_running{ if (-e "$pid_dir/$file"){ my $res; open IN, "<$pid_dir/$file"; while (){ chomp; $pid = $_; last; } close IN; `ps p $pid >/dev/null`; $res = ( $? >> 8); return $res; } return 1; } ###################################################################### # Send a signal to the daemon to reload its configuration file : sub send_reload_signal { my $res = &is_running; print "$appname $version : $copyright\n\n"; if ($res){ print "\t[ERROR] : daemon is not running\n\n"; exit 1; } # USR1 signal : `kill -s USR1 $pid`; print "\t[OK] : reloading user configuration\n\n"; exit; } ###################################################################### # Reload configuration : sub reload_conf { if ($debug) { print "- signal USR1 : " . "reloading user configuration\n"; } &load_conf('sig'); my $date=`date '+%b %e %T'`; chomp $date; print LOG "$date [notice] $appname $version : USR1 signal " . "- reloading user configuration\n"; } ###################################################################### # munin_node stats : sub munin_node { my $munin_congif = shift; if ($munin_congif) { print "graph_title FloodMon\n"; print "graph_args -l 0\n"; print "graph_scale no\n"; print "graph_vlabel SYN_RECV per level\n"; print "graph_category network\n"; print "can_i_play_daddy.label Can I play Daddy ? ~:)\n"; print "can_i_play_daddy.colour 22FF22\n"; print "can_i_play_daddy.draw AREA\n"; print "dont_hurt_me.label Don't hurt me :-(\n"; print "dont_hurt_me.colour F7FF00\n"; print "dont_hurt_me.draw AREA\n"; print "bring_em_on.label Bring'em on >o(\n"; print "bring_em_on.colour FFB700\n"; print "bring_em_on.draw AREA\n"; print "im_death_incarnate.label I'm death incarnate 8=X\n"; print "im_death_incarnate.colour FF2C00\n"; print "im_death_incarnate.draw AREA\n"; }else{ my $syn = `cat /proc/net/tcp |grep ':\[0-9A-F\]\\{4\\} 03 ' |wc -l`; my $syn6 =`cat /proc/net/tcp6 |grep ':\[0-9A-F\]\\{4\\} 03 ' |wc -l`; chomp $syn; chomp $syn6; $syn+= $syn6; my $res = `ls $cache_dir/*.lev 2> /dev/null`; $res =~ /\/(\d)\.lev/; $res = $1; print 'can_i_play_daddy.value '; if ($res == 1){ print "$syn\n" } else { print "0\n" } print 'dont_hurt_me.value '; if ($res == 2){ print "$syn\n" } else { print "0\n" } print 'bring_em_on.value '; if ($res == 3){ print "$syn\n" } else { print "0\n" } print 'im_death_incarnate.value '; if ($res == 4){ print "$syn\n" } else { print "0\n" } } exit; } ###################################################################### # Send a signal to the daemon to stop it and not to restore # any modification (TCP/IP stack, routing table) : sub send_stop_save_signal { my $res = &is_running; print "$appname $version : $copyright\n\n"; if ($res){ print "\t[ERROR] : daemon is not running\n\n"; exit 1; } # send USR2 signal : `kill -s USR2 $pid`; print "\t[OK] : stopping daemon (configuration was not restored)\n\n". "If you wish to restore your configuration, run:\n"; if ( ! $openVZ ) { print "\t$cache_dir/$tcpbackup (TCP/IP stack)\n"; } print "\t$cache_dir/nullroute.sh (null-routed IPs)\n"; exit; } ###################################################################### # Send a signal to the daemon to stop it and ask it to restore # any modification done : sub send_stop_signal { my $res = &is_running; print "$appname $version : $copyright\n\n"; if ($res){ print "\t[ERROR] : daemon is not running\n\n"; exit 1; } `kill -s TERM $pid`; print "\t[OK] : stopping daemon (configuration restored)\n\n"; exit; } ###################################################################### # Stop and restore user configuration : sub stop { if (! $openVZ ) { # TCP/IP stack : `$cache_dir/$tcpbackup` if (-e "$cache_dir/$tcpbackup"); unlink "$cache_dir/$tcpbackup"; } # remove null-routed blocks : foreach my $block (keys %nullrouted_blocks){ `ip route del $block/$netmask`; } unlink "$pid_dir/$file"; unlink <$cache_dir/*.lev>; my $date=`date '+%b %e %T'`; chomp $date; print LOG "$date [notice] $appname $version : stopping daemon " . "- configuration restored\n"; close LOG; print "\n\t=> Program terminated. " . "Configuration fully restored\n\n" if $debug; exit; } ###################################################################### # Stop and don't restore user configuration : sub stop_save { unlink "$pid_dir/$file"; unlink <$cache_dir/*.lev>; my $date=`date '+%b %e %T'`; chomp $date; print LOG "$date [notice] $appname $version : stopping daemon" . "- ** configuration was not restored **\n"; close LOG; print "\n\t=> Program terminated. " . "Configuration ** not restored **\n\n" if $debug; exit; } ###################################################################### # Display network stats : sub stats { my ($res, @RES); print "[ ------------------- TCP ------------------- ]\n"; print "- current SYN_RECV sockets . . : "; my $tmp = `cat /proc/net/tcp | grep ':\[0-9A-F\]\\{4\\} 03 ' | wc -l`; chomp $tmp; $res = $tmp; my $tmp = `cat /proc/net/tcp6 | grep ':\[0-9A-F\]\\{4\\} 03 ' | wc -l`; chomp $tmp; $res+= $tmp; print "$res\n"; print `cat /proc/net/netstat | grep 'TcpExt: [0-9]' | \\ awk '{print "- sockets SYN_RECV reset . . . : \" \$5 \\ \"\\n- syncookies sent . . . . . . . : \" \$2 \\ \"\\n- syncookies received . . . . . : \" \$3 \\ \"\\n- invalid syncookies received . : \" \$4}'`; @RES = `cat /proc/net/snmp`; my ($OutNoRoutes, $AttemptFails, $InSegs, $OutSegs, $InType3, $OutType3, $InDatagrams, $OutDatagrams, $bytes_in, $bytes_out, $packets_in, $packets_out); foreach $res (@RES){ if ($res =~ /^Ip:\s\d+(?:\s[^\s]+){10}\s(\d+)/){ $OutNoRoutes = $1; }elsif ($res =~ /^Tcp:\s\d+(?:\s[^\s]+){5}\s(\d+)(?:\s[^\s]+){2}\s(\d+)\s(\d+)/){ $AttemptFails = $1; $InSegs = $2; $OutSegs = $3; }elsif ($res =~ /^Icmp:\s(\d+)(?:\s\d+){12}\s(\d+)/){ $InType3 = $1; $OutType3 = $2; }elsif ($res =~ /^Udp:\s(\d+)(?:\s[^\s]+){2}\s(\d+)/){ $InDatagrams = $1; $OutDatagrams = $2; } } 1 while $AttemptFails =~ s/(.*\d)(\d\d\d)/$1,$2/; print "- failed connection attempts . : $AttemptFails\n"; if ($OutSegs){ $res = ($InSegs/$OutSegs); $res = sprintf("%.3f", $res); }else{$res = '--'} 1 while $InSegs =~ s/(.*\d)(\d\d\d)/$1,$2/; print "- received segments . . . . . . : $InSegs\n"; 1 while $OutSegs =~ s/(.*\d)(\d\d\d)/$1,$2/; print "- sent segments . . . . . . . . : $OutSegs\n"; print "- in/out seg. ratio . . . . . . : $res\n"; $res =`ip route show | grep '^blackhole' | wc -l`; print "[ ------------------ Route ------------------ ]\n"; print "- null-routed IPs/blocks . . . : $res"; print "[ ------------------- IP -------------------- ]\n"; 1 while $OutNoRoutes =~ s/(.*\d)(\d\d\d)/$1,$2/; print "- dropped packets (OutNoRoutes) : $OutNoRoutes\n"; print "[ ------------------ ICMP --------------------]\n"; if ($OutType3){ $res = ($InType3/$OutType3); $res = sprintf("%.3f", $res); }else{$res = '--'} 1 while $InType3 =~ s/(.*\d)(\d\d\d)/$1,$2/; print "- received . . . . . . . . . . : $InType3\n"; 1 while $OutType3 =~ s/(.*\d)(\d\d\d)/$1,$2/; print "- sent . . . . . . . . . . . . : $OutType3\n"; print "- in/out ratio . . . . . . . . : $res\n"; print "[ ------------------- UDP ------------------- ]\n"; if ($OutDatagrams){ $res = ($InDatagrams/$OutDatagrams); $res = sprintf("%.3f", $res); }else{$res = '--'} 1 while $InDatagrams =~ s/(.*\d)(\d\d\d)/$1,$2/; print "- received packets . . . . . . : $InDatagrams\n"; 1 while $OutDatagrams =~ s/(.*\d)(\d\d\d)/$1,$2/; print "- sent packets . . . . . . . . : $OutDatagrams\n"; print "- in/out ratio . . . . . . . . : $res\n"; $res = `cat /proc/net/dev | grep $conf{'INTERFACE'}`; $res =~ /:\s*(\d+)\s+(\d+)(?:\s+[^\s]+){6}\s+(\d+)\s+(\d+)/; $bytes_in = $1; $packets_in =$2; $bytes_out = $3; $packets_out = $4; print "[ ------------------ $conf{'INTERFACE'} ------------------- ]\n"; 1 while $bytes_in =~ s/(.*\d)(\d\d\d)/$1,$2/; print "- total bytes received . . . . : $bytes_in\n"; 1 while $bytes_out =~ s/(.*\d)(\d\d\d)/$1,$2/; print "- total bytes sent . . . . . . : $bytes_out\n"; if ($packets_out){ $res = ($packets_in/$packets_out); $res = sprintf("%.3f", $res); }else{$res = '--'} 1 while $packets_in =~ s/(.*\d)(\d\d\d)/$1,$2/; print "- total packets received . . . : $packets_in\n"; 1 while $packets_out =~ s/(.*\d)(\d\d\d)/$1,$2/; print "- total packets sent . . . . . : $packets_out\n"; print "- in/out packets ratio . . . . : $res\n"; print "[ ------------------------------------------- ]\n"; exit; } ###################################################################### # Display help : sub help { print "\n$appname $version : $copyright Usage : --help : display this menu. --debug : run in debug mode (no daemon). --daemon : run in daemon mode (default). --reload : reload configuration file ($conf_dir/$file). --stop : stop and restore user configuration (default). --stop-save : stop but doesn't restore user configuration (TCP/IP stack, route, routing tablde...). --stats : display network statistics. --munin-node : daemon statistics to use with munin-node. --sms-test : test SMS alerts. --capture : capture SYN packets. --version : check for floomon updates "; exit; } ###################################################################### # Send an email to the admin : sub email_admin { my $MSG = shift; my $ondemand = shift; my $host = `hostname`; chomp $host; # capture SYN packets ? &tcp_dump($ondemand) if ( ($conf{'DUMP_SYNPACKETS'}) || ($ondemand) ); open MAIL, "|$mailprog -t -f".$conf{'ADMIN_EMAIL'}; print MAIL "From: <".$conf{'ADMIN_EMAIL'}.">\n"; print MAIL "To: <".$conf{'ADMIN_EMAIL'}.">\n"; my $DATE = `date -R`; print MAIL "Date: $DATE"; if ( $ondemand ) { print MAIL "Subject: [$appname] SYN capture for '$host'\n"; } else { print MAIL "Subject: [$appname] ** ALERT ** on '$host'\n"; } print MAIL "MIME-Version: 1.0\n"; # attachment ? if ( ( $conf{'DUMP_SYNPACKETS'} ) || ( $ondemand ) ) { # ensure the file is not empty : if (( stat("/tmp/syn.pcap" ))[7]) { if (! $ondemand ) { $MSG.="\nAttachment : pcap dump of the last " .$conf{'DUMP_SYNPACKETS'}." SYN packets received\n"; } my $boundary; my @bound = (0..9, 'A'..'F'); srand(time ^ $$); for ( my $i = 0; $i++ < 24; ) { $boundary.= $bound[rand(@bound)]; } print MAIL "Content-Type: multipart/mixed;\n\x09boundary=\""; print MAIL "------------$boundary\"\n\n"; print MAIL "This is a multi-part message in MIME format.\n\n"; print MAIL "--------------$boundary\n"; print MAIL "Content-Type: text/plain; charset=\"utf-8\"\n"; print MAIL "Content-Transfer-Encoding: quoted-printable\n\n"; print MAIL MIME::QuotedPrint::encode($MSG); print MAIL "\n--------------$boundary\n"; print MAIL "Content-Type: application/cap; "; print MAIL "name=\"syn.pcap\"\n"; print MAIL "Content-Transfer-Encoding: base64\n"; print MAIL "Content-Disposition: attachment;\n\x09filename=\""; print MAIL "syn.pcap\"\n\n"; open INPUT, " /dev/null 2>&1`; my $res = ( $? >> 8); if ( $res ) { print "\t[ERROR] : error (\#$res) while sending SMS !\n\n"; }else{ print "\t[OK] : SMS sent !\n\n"; } exit; } ###################################################################### # Make an on-demand capture and save it or email it to the admin : sub capture { print "$appname $version : $copyright\n\n"; if (! $ARGV[1] ) { print "\t[ERROR] : enter the number of packets to capture !\n" . "\texample : $appname --capture 10\n\n"; exit 1; } my $send_by_email; if ( $conf{'ADMIN_EMAIL'} ) { print "- do you want the capture to be sent by email to <". $conf{'ADMIN_EMAIL'}."> [y/n] ?\n"; while (){ if (/^y/i) { $send_by_email = 1 } last; } } print "- capturing [".$ARGV[1]."] SYN packets...\n"; if ( $send_by_email ) { my $msg = "Here is the capture of the last [".$ARGV[1]; $msg.= "] SYN packets received\n\n"; &email_admin($msg, $ARGV[1]); print "\t=> capture has been sent to <".$conf{'ADMIN_EMAIL'}.">\n\n"; } else { &tcp_dump($ARGV[1]); print "\t=> capture has been saved to [/tmp/syn.pcap]\n\n"; } exit; } ###################################################################### # Check for updates and email the admin : sub check_update { my $cmdline = shift; my $proto = getprotobyname('tcp'); socket(SOCK, PF_INET, SOCK_STREAM, $proto); my $iaddr = gethostbyname( 'spamcleaner.org' ); goto CONN_ERROR if ( ! $iaddr ); my $sin = pack('Sna4x8', AF_INET, 80, $iaddr); goto CONN_ERROR if (! connect SOCK, $sin ); send SOCK, "GET /floodmon-update.txt HTTP/1.1\r\n", 0; send SOCK, "Host: spamcleaner.org\r\n\r\n", 0; recv SOCK, my $res, 512, 0; close SOCK; ( my $new_version ) = $res =~ /(v\d\.\d\.\d)/; if ( ( $new_version ) && ( $new_version ne $version ) ) { my $date = `date '+%b %e %T'`; chomp $date; open LOG2, ">>$log_dir/$file.log"; print LOG2 "$date [notice] $appname $version : " . "a new version is available : $new_version\n"; close LOG2; if ( $cmdline ) { print "$appname $version : $copyright\n\n" . "\tNew version found : $new_version\n\n"; exit; } else { if ( $conf{'ADMIN_EMAIL'} ) { my $msg = "echo \"A new version of floodmon ($new_version) "; $msg.= "is available.\" | mail -s \"[floodmon] new version "; $msg.= "available\" $conf{'ADMIN_EMAIL'} 2> /dev/null"; `$msg`; } } } else { if ( $cmdline ) { print "$appname $version : $copyright\n\n" . "\tNo new version found\n\n"; exit; } } CONN_ERROR: if ( $cmdline ) { print "$appname $version : $copyright\n\n" . "\t[ERROR] cannot access spamcleaner.org\n\n"; exit; } } ###################################################################### # EOF