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;
Published Mar 12, 2015
Version 1.0CodeCentral_194
Cirrostratus
Joined May 05, 2019
CodeCentral_194
Cirrostratus
Joined May 05, 2019
1 Comment
- swo0sh_gt_13163
Altostratus
Is it possible to break down the procedure to use this script to run the ringdump?