ringdump
Problem this snippet solves: loop a tcpdump until a log message is seen Code : # Updated: 10/16/06 #!/usr/bin/perl ## VERSION v0.9b use strict; ################ # tcpdump settings ########## my %SETTINGS = ( external => { filter => "port 443" }, internal => { filter => "port 80" }, lo0 => { filter => "port 80" }, ); my $SNAPLEN = 4352; ################ # script settings ###### # free space checking my $FREE_SPACE_CHECK_INTERVAL = 1; # check free space every this number of seconds my $MIN_FREE_SPACE = 5; # minimum percent space left on parition my $CAPTURE_LOCATION = $ARGV[0]; # file rotation settings my $CAPTURES_TO_ROTATE = 4; # tcpdump capture files to rotate my $DESIRED_CAPTURE_SIZE = 15; # megabytes per capture file before rotating my $OVERLAP_DURING_ROTATE = 5; # seconds to overlap previous capture while starting a new one my $CAPTURE_CHECK_INTERVAL = 1; # how often (seconds) to check the size of capture files for rotating # trigger settings - time (run tcpdumps for x seconds) #my $TRIGGER = "time-based"; my $TIME_TO_CAPTURE = 300; # trigger settings - log-message (stop tcpdump when log message is received) my $TRIGGER = "log-message based"; my $LOG_FILE = "/var/log/messages"; my $LOG_MESSAGE = "no space in response line"; my $FOUND_MESSAGE_WAIT = 5; # how many seconds to gather tcpdumps after we match the log message # misc my $IDLE_TIMER = 5; # if ! receiving log entries, how long before checking if log is rotated my $MAX_ROTATED_LINES = 10000; # max lines to read from file we're re-reading because it's been rotated my $PID_FILE = "/var/run/ring_dump.pid"; my $DEBUG = 0; # 0/1 #################################################### # END OF THINGS THAT SHOULD NEED TO BE CONFIGURED #################################################### ######## # set defaults ### $SNAPLEN ||= 4352; $TRIGGER ||= "time"; $CAPTURE_LOCATION ||= "/var/tmp"; $TIME_TO_CAPTURE ||= 60; $FREE_SPACE_CHECK_INTERVAL ||= 5; $CAPTURES_TO_ROTATE ||= 3; $DESIRED_CAPTURE_SIZE ||= 10; $OVERLAP_DURING_ROTATE ||= 5; $CAPTURE_CHECK_INTERVAL ||= 5; $MIN_FREE_SPACE ||= 5; $LOG_FILE ||= "/var/log/messages"; $LOG_MESSAGE ||= "FAILED"; $FOUND_MESSAGE_WAIT ||= 5; $IDLE_TIMER ||= 5; $PID_FILE ||= "/var/run/ring_dump.pid"; $DEBUG ||= 0; unless (-d $CAPTURE_LOCATION) { print "$CAPTURE_LOCATION isn't a directory, using /mnt instead\n\n"; $CAPTURE_LOCATION = "/mnt"; } if (! -r $LOG_FILE) { die "Can't read \"$LOG_FILE\", EXIT\n"; } # insert code to find tcpdump instead of relying on path HERE: my $tcpdump = "/usr/sbin/tcpdump"; ###### # misc global variable declaration ########## my($answer, $interface, $pid, $tail_child, $F_LOG); my($current_size, $current_inode, $last_size, $last_inode); my @child_pids; my $ppid = $$; my $min_megabytes = $CAPTURES_TO_ROTATE * $DESIRED_CAPTURE_SIZE; $current_size = $current_inode = $last_size = $last_inode = 0; $|++; ########### # functions ####### # exit function that does does necessary child handling sub finish { $_ = shift(); if (defined($_) && $_ ne "") { print; } foreach $interface (keys( %SETTINGS )) { push(@child_pids, $SETTINGS{$interface}{pid}); } $DEBUG && print "INTERRUPT: sending SIGINT and SIGTERM to: ", join(" ", @child_pids), "\n"; kill(2, @child_pids); sleep(1); kill(15, @child_pids); $DEBUG && print "INTERRUPT: done, unlink pidfile and exit\n"; unlink($PID_FILE); exit(0); } $SIG{INT} = sub { finish(); }; # report usage on CAPTURE_LOCATION's MB free from df sub free_megabytes { my $partition = shift(); $partition ||= $CAPTURE_LOCATION; my $free_megabytes; $DEBUG && print "free_megabytes(): capture partition is $partition\n"; open(DF, "df $partition|"); # discard the first line; $_ = ; # parse the usage out of the second line $_ = ; $free_megabytes = (split)[3]; $free_megabytes = int($free_megabytes / 1024); close(DF); $DEBUG && print "free_megabytes(): finished reading df, output is: $free_megabytes\n"; $free_megabytes; } # report usage on CAPTURE_LOCATION's % usage from df sub free_percent { my $partition = shift(); $partition ||= $CAPTURE_LOCATION; my $free_percent; $DEBUG && print "free_percent(): capture partition is $partition\n"; open(DF, "df $partition|"); # discard the first line; $_ = ; # parse the usage out of the second line $_ = ; $free_percent = (split)[4]; chop($free_percent); ## chop off '%' $free_percent = (100 - $free_percent); close(DF); $DEBUG && print "free_percent(): finished reading df, output is: $free_percent\n"; $free_percent; } # simple sub to send SIGHUP to syslogd sub restart_syslogd () { if (-f "/var/run/syslog.pid") { open(PIDFILE, "; chomp; kill(1, ($_)); 1; } # simple wrapper to start tcpdumps, assuming obvious globals sub start_tcpdump { my $interface = shift(); my $capture_file = shift(); my $filter = shift(); my @cmd = ("$tcpdump", "-s$SNAPLEN", "-i$interface", "-w$capture_file", "$filter"); $DEBUG || open(STDERR, ">/dev/null"); $DEBUG && print "start_tcpdump(): about to start: ", join(" ", @cmd), "\n"; exec($cmd[0], @cmd[1..$#cmd]) || print "start_tcpdump(): FAILED to start: ", join(" ", @cmd), ", command not found\n"; $DEBUG || close(STDERR); exit(1); } # sub to see how much space a given capture file is using (to decide to rotate or not) sub capture_space ($) { my $capture_file = shift(); my $size = ( stat($capture_file) )[7]; $DEBUG && print "capture_space(): size of $capture_file is $size\n"; # return size of argument in megabytes, but don't divide by zero if ($size == 0) { return 0; } else { return ($size / 1048576); } } # gives user the option to create a MFS sub create_mfs () { if (-d $CAPTURE_LOCATION) { $DEBUG && print "create_mfs(): directory $CAPTURE_LOCATION exists\n"; } else { mkdir($CAPTURE_LOCATION, oct(0755)) || die "FAILED to create $CAPTURE_LOCATION\n"; print "Capture directory ($CAPTURE_LOCATION) did not exist, so it was created\n"; } # figure out the partition CAPTURE_LOCATION is on. This is cheap... fixme my $partition = $CAPTURE_LOCATION; $partition =~ s!(/[A-z0-9]*)/{0,1}.*!$1!g; open(MOUNT, "mount|") || die "FAILED to run \"mount\": !$\n"; while ( ) { next unless ((split())[2] =~ /^$partition$/); $DEBUG && print "create_mfs(): partition: $partition is already mounted, return\n"; # return 1 if it's already mounted return 1; } close(MOUNT); print "Mount a Memory File System (MFS) on ${CAPTURE_LOCATION}? [y/n]: "; my $answer = ; if (lc($answer) =~ "y") { print "Enter size of MFS in blocks (200000 = 100M), or just press enter for 100M: "; chomp (my $mfs_size = ); $mfs_size = 200000 if ($mfs_size eq ""); print "Allocating $mfs_size blocks to $CAPTURE_LOCATION for MFS\n"; system("mount_mfs -s $mfs_size $CAPTURE_LOCATION"); if (($? >> 8) != 0) { print "an error occurring trying to mount the MFS filesystem, exit status: $?\n"; 0; } else { print "MFS file system established\n\n"; 1; } } } sub fork_to_background ($) { my $cmd = shift(); my $pid = fork(); if ($pid == 0) { exec($cmd) || die "exec() failed: $!\n"; } else { return($pid); } } sub popen_read ($) { my $cmd = shift(); my $child; $DEBUG && print "Background: \"$cmd\"\n"; pipe(READLOG, WRITELOG); select(READLOG); $|++; select(WRITELOG); $|++; select(STDOUT); ## dup STDOUT and STDERR open(T_STDOUT, ">&STDOUT"); open(T_STDERR, ">&STDERR"); ## redir STDOUT to pipe for child open(STDOUT, ">&WRITELOG"); open(STDERR, ">&WRITELOG"); $child = fork_to_background($cmd); ## close STDOUT, STDERR and FILE close(STDOUT); close(STDERR); ## re-open STDOUT as normal and close dup open(STDOUT, ">&T_STDOUT"); close(T_STDOUT); open(STDERR, ">&T_STDERR"); close(T_STDERR); return($child, \*READLOG); } sub open_log ($$) { my $LOG_FILE = shift(); my $lines = shift(); if (defined($F_LOG) && defined(fileno($F_LOG)) ) { $DEBUG && print "Killing child before closing LOG\n"; kill(15, $tail_child); waitpid($tail_child, 0); $DEBUG && print "Closing LOG\n"; close($F_LOG); } $DEBUG && print "Opening \"$LOG_FILE\"\n"; ($tail_child, $F_LOG) = popen_read("tail -n $lines -f $LOG_FILE"); push(@child_pids, $tail_child); 1; } ## check to see if log is rotated, returns true if rotated sub is_rotated ($) { my $LOG_FILE = shift(); $DEBUG && print "enter is_rotated()\n"; ($current_inode, $current_size) = (stat($LOG_FILE))[1,7]; if (($last_size != 0) && ($last_size > $current_size)) { $DEBUG && print "File is now smaller. File must have been rotated\n"; $last_size = $current_size; $last_inode = $current_inode; open_log($LOG_FILE, $MAX_ROTATED_LINES) || die "open_log $LOG_FILE failed: $!\n"; return(1); } elsif (($last_inode != 0) && ($last_inode != $current_inode)) { $DEBUG && print "Inode changed. File must have been rotated\n"; $last_inode = $current_inode; $last_size = $current_size; open_log($LOG_FILE, $MAX_ROTATED_LINES) || die "open_log $LOG_FILE failed: $!\n"; return(1); } ($last_inode, $last_size) = ($current_inode, $current_size); 0; } ########### # MAIN ######## if (free_megabytes() < $min_megabytes) { print "free space on $CAPTURE_LOCATION is below ${min_megabytes}MB, you must create a Memory File System or choose another location to gather tcpdumps\n"; goto MUST_MFS; } ######### GET USER INPUT ############### if (free_percent() < $MIN_FREE_SPACE) { print "free space on $CAPTURE_LOCATION is below ${MIN_FREE_SPACE}%, you must create a Memory File System or choose another location to gather tcpdumps\n"; MUST_MFS: # require the user to create a MFS if they don't have enough free space exit(1) unless (create_mfs()); } else { create_mfs(); } if (free_percent() < $MIN_FREE_SPACE || free_megabytes() < $min_megabytes) { print "it appears the Memory File System is in place, but there is still insufficient space, exiting\n"; exit(1); } print "capturing to $CAPTURE_LOCATION using the following interfaces and filters:\n"; foreach $interface (keys( %SETTINGS )) { system("ifconfig $interface >/dev/null 2>&1"); if ( ($? >> 8) != 0) { $DEBUG && print "couldn't ifconfig $interface, removing from list\n"; delete( $SETTINGS{$interface} ); } else { print " $interface: $SETTINGS{$interface}{filter}\n"; } } print "does this look right? [y/n]: "; $answer = ; exit unless lc($answer) =~ "y"; ####### DAEMONIZE ############# chdir("/"); exit unless (fork() == 0); # kill old self, write pid file if (-f $PID_FILE) { open(PIDFILE, "<$PID_FILE"); kill(15, ); close(PIDFILE); } open(PIDFILE, ">$PID_FILE"); syswrite(PIDFILE, $$); close(PIDFILE); ########### START PROCESSING ############### foreach $interface (keys( %SETTINGS )) { my $filter = $SETTINGS{$interface}{filter}; $pid = fork(); $SETTINGS{$interface}{rotate_number} = 1; if (!defined($pid)) { print "fork() failed! exiting\n"; exit 1; } if ($pid == 0) { start_tcpdump( $interface, "$CAPTURE_LOCATION/${interface}.dump.$SETTINGS{$interface}{rotate_number}", $filter ); exit 1; } else { $SETTINGS{$interface}{pid} = $pid; print "started tcpdump as pid $pid on \"$interface\" filtered as \"$filter\"\n"; } } ###### # fork off a process to keep an eye on free space ######## $pid = fork(); if ($pid == 0) { while (1) { my $sleep_return = sleep($FREE_SPACE_CHECK_INTERVAL); $DEBUG && ($sleep_return != $FREE_SPACE_CHECK_INTERVAL) && print "WARN: free_percent() loop: sleep returned $sleep_return instead of $FREE_SPACE_CHECK_INTERVAL !\n"; if (free_percent() < $MIN_FREE_SPACE) { print "WARN: free space is below ${MIN_FREE_SPACE}%, killing main script\n"; kill(2, $ppid); sleep(1); kill(15, $ppid); print "WARN: sent SIGTERM to $ppid (main script), exiting\n"; exit 1; } else { $DEBUG && print "free_percent(): space is fine, continue\n"; } } } else { push(@child_pids, $pid); $DEBUG && print "started free_percent watcher as: $pid\n"; } ###### # fork off a process to rotate capture files as necessary ######## $pid = fork(); if ($pid == 0) { my $capture_file; while (1) { my $sleep_return = sleep($CAPTURE_CHECK_INTERVAL); $DEBUG && ($sleep_return != $CAPTURE_CHECK_INTERVAL) && print "WARN: start_tcpdump() loop: sleep returned $sleep_return instead of $CAPTURE_CHECK_INTERVAL !\n"; foreach $interface (keys( %SETTINGS )) { if (capture_space("$CAPTURE_LOCATION/${interface}.dump.$SETTINGS{$interface}{rotate_number}") >= $DESIRED_CAPTURE_SIZE) { if ($SETTINGS{$interface}{rotate_number} == $CAPTURES_TO_ROTATE) { print "reached maximum number of captures to rotate: $CAPTURES_TO_ROTATE, starting over at 1\n"; $SETTINGS{$interface}{rotate_number} = 1; } else { $SETTINGS{$interface}{rotate_number}++; } print "rotating capture file: ${interface}.dump, new extension .$SETTINGS{$interface}{rotate_number}\n"; $pid = fork(); if ($pid == 0) { start_tcpdump( $interface, "$CAPTURE_LOCATION/${interface}.dump.$SETTINGS{$interface}{rotate_number}", $SETTINGS{$interface}{filter}, ); exit 0; } push(@child_pids, $pid); # get some overlap in the two files sleep($OVERLAP_DURING_ROTATE); # kill the old tcpdump kill(2, $SETTINGS{$interface}{pid}); $DEBUG && print "sent SIGINT to $interface: $SETTINGS{$interface}{pid}, new pid $pid\n"; # record the new pid $SETTINGS{$interface}{pid} = $pid; } else { $DEBUG && print "capture file doesn't need to be rotated yet: ${interface}.dump\n"; } } # Reap any zombies from old tcpdumps $DEBUG && print "start_tcpdump() loop: \@child_pids = (", join(' ', @child_pids), ")\n"; while (1) { use POSIX ":sys_wait_h"; my $child = waitpid(-1, WNOHANG); if (defined $child and $child > 0) { # remove PID from @child_pids @child_pids = grep {$_ != $child} @child_pids; $DEBUG && print "start_tcpdump() loop: reaped child PID $child\n"; } else { # no one to reap last; } } } } else { push(@child_pids, $pid); $DEBUG && print "started capture file watcher as: $pid\n"; } ################ # watch triggers (time or log based) #################### $SIG{TERM} = sub { finish(); }; if (lc($TRIGGER) =~ /time/) { print "time-based trigger, will capture for $TIME_TO_CAPTURE seconds\n"; sleep($TIME_TO_CAPTURE); print "captured for $TIME_TO_CAPTURE seconds, stopping tcpdumps\n"; } elsif (lc($TRIGGER) =~ /log/) { print "log-based trigger, waiting for \"$LOG_MESSAGE\" in \"$LOG_FILE\"\n"; # creates global $F_LOG filehandle of $LOG_FILE open_log($LOG_FILE, 0) || finish("open_log $LOG_FILE failed: $!\n"); # flush syslogd's buffers (avoid never getting the message due to "last message repeated....") restart_syslogd() || finish("Restarting syslogd failed, EXIT\n"); # tail -f the log and wait for message while (1) { # reap any zombies during each loop my $return; while (1) { use POSIX ":sys_wait_h"; my $child = waitpid(-1, WNOHANG); if (defined $child and $child > 0) { $DEBUG && print "log trigger loop: reaped child PID $child\n"; } else { # no one to reap last; } } eval { $SIG{ALRM} = sub { die("ALRM\n"); }; alarm($IDLE_TIMER); $_ = <$F_LOG>; alarm(0); }; if ($@) { # this only occurs if we're idle for $IDLE_TIMER seconds because no new log entries are occuring $@ = undef; is_rotated($LOG_FILE); next; } $DEBUG && print "in LOG reading loop, current line: \"$_\"\n"; if (/$LOG_MESSAGE/) { $DEBUG && print "Current line matches: \"$LOG_MESSAGE\"\n"; last; } $DEBUG && print "no match, next\n"; } print "received log message, sleeping $FOUND_MESSAGE_WAIT seconds then stopping tcpdumps\n"; sleep($FOUND_MESSAGE_WAIT); } # figure out current tcpdump child_pids and push them onto the list foreach $interface (keys( %SETTINGS )) { push(@child_pids, $SETTINGS{$interface}{pid}); } # kill all tcpdumps + free space watcher + capture file rotator -- doesn't return finish(); 0;702Views0likes1Comment