diff --git a/etc/net-hydra.sample.conf b/etc/net-hydra.sample.conf new file mode 100644 index 0000000..4d6e22d --- /dev/null +++ b/etc/net-hydra.sample.conf @@ -0,0 +1,27 @@ +[clients] + # list each device name here, in the form user@machine, as you wish SSH to connect to them. + # + local0 = root@127.0.0.1 + local1 = root@127.0.0.1 + local2 = root@127.0.0.1 + local3 = root@127.0.0.1 + +[directives] + # the command sequence to be run on each client device. directives beginning with $ reference lines from the [aliases] section. + # + local0 = $4kstream + local1 = $voipcall + local2 = $browsing + local3 = $download + +[aliases] + # define aliases here for commands you'd like to run in the [directives] section. This keeps [directives] cleaner so you can + # see what you're doing a little more easily. + # + # any use of the special value $when inside aliases or directives will be replaced with the actual execution time in epoch seconds. + # + $4kstream = netburn -u http://127.0.0.1/1M.bin -r 25 -t 3 -h 4kstream -o /tmp/$testname-$when.csv --ifinfo test0 + $download = netburn -u http://127.0.0.1/1M.bin -t 3 -h download -o /tmp/$testname-$when.csv --ifinfo test0 + $browsing = netburn -u http://127.0.0.1/128K.bin -r 1 -t 3 -c 10 -h browsing -o /tmp/$testname-$when.csv --ifinfo test0 + $voipcall = netburn -u http://127.0.0.1/16K.bin -r 1 -t 3 -h voipcall -o /tmp/$testname-$when.csv --ifinfo test0 + diff --git a/usr/bin/countdown b/usr/bin/countdown new file mode 100755 index 0000000..bfc2388 --- /dev/null +++ b/usr/bin/countdown @@ -0,0 +1,17 @@ +#!/usr/bin/perl + +my $delay = $ARGV[0]; +my $charcount = 0; + +print "Counting down from $delay seconds: "; + +while ($delay > 0) { + while ($charcount > 0) { print "\b"; $charcount --; } + print "$delay "; select()->flush(); + for (my $i=5; $i-- ; $i > 0) { print "\b"; } + $charcount = length($delay); + sleep 1; + $delay --; +} + +print "\n"; diff --git a/usr/bin/mailburn b/usr/bin/mailburn new file mode 100755 index 0000000..e4ce4e5 --- /dev/null +++ b/usr/bin/mailburn @@ -0,0 +1,632 @@ +#!/usr/bin/perl + +# (c) 2017 Jim Salter. Licensed GPLv3. mailburn --usage for details. + +# this software is licensed for use under the Free Software Foundation's GPL v3.0 license, as retrieved +# from http://www.gnu.org/licenses/gpl-3.0.html on 2014-11-17. A copy should also be available in this +# project's Git repository at https://github.com/jimsalterjrs/network-testing/blob/master/LICENSE. + + +# TIPS: You're going to need an SMTP server to receive these things. +# If you want to test the mailserver itself, you're set. If you want to test the NETWORK, +# I suggest installing postfix on your receiving server, then running smtp_sink on it: +# +# root@yourserver:~# smtp-sink -u postfix :26 1024 +# +# This sets up a dummy SMTP server running as the postfix user, on port 26, which accepts +# and silently discards all email sent to it. Man smtp_sink if you have similar but not +# exact needs; it can do a fair amount of other stuff likely to be useful to mailburn +# users as well. In particular, you may want to debug with the -c argument, so you can +# verify that mailburn and smtp-sink are each reporting the same number of messages +# sent / received. +# +# Note that if you're running smtp-sink on port 26, you'll need to specify --port 26 to +# mailburn also! +# +# PS: Please don't use this tool to be a jackass. + +use strict; +use Time::HiRes qw ( usleep gettimeofday ); + +# on ubuntu, do: apt install libemail-mime-perl +use Email::MIME; + +# on ubuntu, do: apt install libemail-sender-perl +use Email::Sender::Simple qw(sendmail); +use Email::Sender::Transport::SMTP; + +use List::Util qw ( sum min max ); +use Sys::Hostname; + +# process command line arguments +my %args = getargs(@ARGV); + +if ($args{'usage'}) { printusage(); exit; } + +# only print stuff to STDOUT if -q is off +my $noisy = (! $args{'q'}); + +# get test interface wifi info if --ifinfo is set +my ($essid,$freq,$ap,$bitrate,$quality); + +if ($args{'ifinfo'}) { + ($essid,$freq,$ap,$bitrate,$quality) = getifinfo($args{'ifinfo'}); + if ($noisy) { + print "\n"; + print "Interface: $args{'ifinfo'}\n"; + print "ESSID: $essid\n"; + print "Frequency: $freq\n"; + print "AP: $ap\n"; + print "Bitrate: $bitrate\n"; + print "Link Quality: $quality\n"; + print "Injected Jitter: up to $args{'jitter'} ms\n"; + } +} + + +# default concurrency value should be 1 +if (!defined $args{'c'}) { $args{'c'} = 1; } +my $concurrency = $args{'c'}; + +# locking concurrency to 1 since it doesn't seem to make sense +# with SMTP like it did with HTTP. The function itself should +# probably be removed entirely at some point. +$concurrency = 1; + +my $url = $args{'u'}; + +my $outputfile = $args{'o'}; + +# we're going to track the rate limit in bits per second, +# even though we specified it in Mbps on the command line. +my $ratelimit = ($args{'r'} * 1024 * 1024); +my $timelimit = $args{'t'}; + +my $now = microtime(); +my $endtime = $now + $timelimit; + +my $totalsent; + +# hashes to push data to during the test for statistics later +my @latencyarray; +my @lengtharray; + +# set the throttling period in ms - how much history +# we look at before deciding whether to start +# sending another message. Will be selectable +# from CLI later. +my $throttleperiod = $args{'p'}; + +# create an email message with payload specified +my $message = makemessage($args{'size'}); +my $transport = Email::Sender::Transport::SMTP->new({ + host => $args{'host'}, + port => $args{'port'}, +}); + +# store the time at which we begin the test run +my $begin = microtime(); + +# counter to let us keep track of when to placate the anxious human watching +my $lastdisplay; + +# disable terminal buffering so we can do nice progress +$| = 1; + +my $progresstext; + +if ($noisy) { + print "\nSending messages with " . nearest(0.1,($args{'size'}/1024/1024)) . " MB of data to $args{'to'} "; + + if ($args{'r'} == -1) { + print "at full rate"; + } else { + print "at maximum rate $args{'r'} Mbps"; + } + if ($args{'jitter'} > 0) { + print " (with added jitter (max $args{'jitter'} ms))"; + } + if ($args{'c'} > 1) { + print " (with concurrency=$args{'c'})"; + } + print ", for $args{'t'} seconds.\n\n"; +} + +my $messagessent; + +while ($now < $endtime) { + + $now = microtime(); + my $currentrate = $totalsent / ($now-$begin); + + # get current before-send time in microseconds so we can track latency for this request + my $beforetime = microtime(); + + my $bits; + + if ($concurrency == 1) { + # simple email send + sendmail($message, { transport => $transport }); + $bits = ($args{'size'} + 221) * 8; # 221 is rough length of message headers. varies slightly with from, to address length. + } else { + # concurrent send multiple emails. + # + # IMPORTANT NOTE: this will give MUCH lower throughput than single send, + # since the loop binds here until ALL $concurrency children + # finish sending their messages. + $bits = concurrentsend($concurrency,$message) * 8; + } + + # get current after-begin time in microseconds so we can track latency for this message + my $aftertime = microtime(); + my $elapsedms = ($aftertime-$beforetime)*1000; + + # update how much data we've sent so far + $messagessent ++; + $totalsent += $bits; + + # update %fetched for each ms of this request, with the mean throughput of this request for each ms + my $timeslice = int($aftertime*1000); + # store latency and length of page of this request for statistics + push (@lengtharray,$bits); + push (@latencyarray,$elapsedms); + + my $elapsedseconds = $aftertime - $beforetime; + my $lastxferrate = $bits/$elapsedseconds; + + # this loop basically just helps avoid unnecessary CPU-thrashing through the main loop, by sleeping for enough microseconds + # to get us close to the time to send the next message. This also has the effect of keeping the main routine from "bursting" + # super fast during a really good throughput period to make up for a period of truly awful throughput, which is good for + # what we're really trying to do here: check to see if there are any of those truly awful periods in the first place. + if ($ratelimit > 0) { + if ($lastxferrate > $ratelimit) { + # sleep for enough ms that the last transfer would have effectively been at $ratelimit + my $desiredlatency = $bits / $ratelimit; # bits / bits per second = seconds + my $timetosleep = $desiredlatency-$elapsedseconds; # sleep this long now pls + $timetosleep = $timetosleep * 1000 * 1000; #seconds --> milliseconds --> microseconds + + # if jitter interval specified, add +/- ($args{'jitter'}/2) milliseconds to sleep interval + if ($args{'jitter'} > 0) { + my $microseconds = (int(rand($args{'jitter'}*1000))); + # jitter should range from -jitter/2 to jitter/2, not from 0 to jitter + $microseconds = int(($args{'jitter'}*1000/2)-$microseconds); + $timetosleep += $microseconds; + } + + # make sure we don't try to sleep a negative interval + if ($timetosleep > 0) { usleep ($timetosleep); } + } + } + + # update time to make sure we try our best to avoid running after-the-buzzer tests as much as possible + $now = microtime(); + + # display current throughput if we just changed second. note that we deliberately do this AFTER sleeping. + # if we did it before sleeping, we'd look fast all the way until the end, where we'd suddenly look perfect, + # which would throw us off pretty badly and make us wonder what was wrong with the rate limiting. + if ($noisy) { + if ( int($now) > $lastdisplay ) { + + if (length $progresstext) { + # wipe lines on terminal for refresh with new lines + for (my $counter=0; $counter<2 ; $counter++) { + # move 1 line up, clear to beginning of that line + print "\e[1F"; + print "\e[K"; + } + } + + my $currentrate = $totalsent / ($now-$begin); + $progresstext = "Throughput so far: " . nearest(0.01, ($currentrate/1024/1024)) . " Mbps"; + $progresstext .= "\nSeconds remaining: " . int($endtime-$now) . "\n"; + print $progresstext; + + $lastdisplay = int($now); + } + } + +} + +# store the time at which we ended the test run +my $end = microtime(); + +# get the after-test interface info if applicable +my ($essid_after,$freq_after,$ap_after,$bitrate_after,$quality_after); +my $ifchangewarning = ''; + +if ($args{'ifinfo'}) { + ($essid_after,$freq_after,$ap_after,$bitrate_after,$quality_after) = getifinfo($args{'ifinfo'}); + # detect changes in interface connection during testing + if ( "$freq $ap $bitrate" ne "$freq_after $ap_after $bitrate_after" ) { $ifchangewarning = "WARNING!"; } +} + +# re-enable terminal buffering +$| = 0; + +# the stats we're going to output +my $elapsed = nearest(0.1, ($end-$begin)); +my $numbersent = scalar @latencyarray; +my $totaldatasent = nearest(0.1, ($totalsent/8/1024/1024) ); +my $meanpagelength = mean(@lengtharray); +my $medianpagelength = percentile(0.5,\@lengtharray); +my $minpagelength = min(@lengtharray); +my $maxpagelength = max(@lengtharray); +my $pagelengthdeviation = $maxpagelength-$minpagelength; +my $throughput = nearest(0.1, ($totalsent/$elapsed/1024/1024) ); +my $meanlatency = nearest(0.01, (mean(@latencyarray)) ); +my $minlatency = nearest(0.01, (min(@latencyarray)) ); +my $maxlatency = nearest(0.01, (max(@latencyarray)) ); +my $latency99 = nearest(0.1,percentile(0.99,\@latencyarray)); +my $latency95 = nearest(0.1,percentile(0.95,\@latencyarray)); +my $latency90 = nearest(0.1,percentile(0.90,\@latencyarray)); +my $latency75 = nearest(0.1,percentile(0.75,\@latencyarray)); +my $latency50 = nearest(0.1, (percentile(0.5,\@latencyarray)) ); + +# humanreadable output +if ($noisy) { + print "\n"; + print "Time elapsed: $elapsed seconds\n"; + # print "Number of messages sent: " . ($numbersent * $concurrency) . "\n"; + print "Number of messages sent: $numbersent \n"; + # print "Concurrent clients per send operation: $concurrency\n"; + print "Total data sent: $totaldatasent MB\n"; + # print "Mean message length (total, all $concurrency clients): " . nearest(0.1,($meanpagelength/8/1024)) . " KB\n"; + print "Mean message length: " . nearest(0.1,($meanpagelength/8/1024)) . " KB\n"; + print "Message length maximum deviation: $pagelengthdeviation KB\n"; + print "Throughput achieved: $throughput Mbps\n"; + print "Mean latency: $meanlatency ms\n"; + print "\n"; + if (! defined $args{'percentile'}) { + print "Worst latency: $maxlatency ms\n"; + print "99th percentile latency: $latency99 ms\n"; + print "95th percentile latency: $latency95 ms\n"; + print "90th percentile latency: $latency90 ms\n"; + print "75th percentile latency: $latency75 ms\n"; + print "Median latency: $latency50 ms\n"; + print "Min latency: $minlatency ms\n"; + } else { + # print results for each percentile interval specified with --percentile=n + for (my $p=$args{'maxp'} ; $p>=$args{'minp'} ; $p -= $args{'percentile'}) { + my $percentile = nearest(0.1,percentile(($p/100),\@latencyarray)); + print $p . "th percentile latency: $percentile ms\n"; + } + } +} + +# output to logfile in CSV format if -o specified +if ( $outputfile ne '' ) { + + if ($noisy) { print "\n"; } + + open FH, ">> $outputfile"; + if (! $args{'no-header'} ) { + print FH "hostname,timestamp,"; + if ($args{'ifinfo'}) { + print FH "ifchangewarning,interface,essid,frequency,frequency_after,ap,ap_after,bitrate,bitrate_after,linkquality,linkquality_after,"; + } + print FH "time elapsed(sec),messages sent,data sent(MB),mean message length(KB),message length deviation(KB),throughput(Mbps),"; + if (! defined $args{'percentile'}) { + print FH "max latency(ms),99%latency(ms),95%latency(ms),90%latency(ms),75%latency(ms),median latency(ms),min latency(ms),"; + } else { + for (my $p=$args{'maxp'} ; $p >= $args{'minp'} ; $p -= $args{'percentile'}) { + print FH "$p\%,"; + } + } + print FH "mean latency(ms),jitter interval(ms)"; + print FH "\n"; + } + my $timestamp = timestamp(int($begin)); + my $hostname = $args{'h'}; + if ($hostname eq '') { $hostname = hostname; } + print FH "$hostname,$timestamp,"; + if ($args{'ifinfo'}) { + print FH "$ifchangewarning,$args{'ifinfo'},$essid,$freq,$freq_after,$ap,$ap_after,$bitrate,$bitrate_after,$quality,$quality_after,"; + } + print FH "$elapsed,$numbersent,$totaldatasent," . nearest(0.1,$meanpagelength/8/1024) . ",$pagelengthdeviation,$throughput,"; + if (! defined $args{'percentile'}) { + print FH "$maxlatency,$latency99,$latency95,$latency90,$latency75,$latency50,$minlatency,"; + } else { + for (my $p=$args{'maxp'} ; $p >= $args{'minp'} ; $p -= $args{'percentile'}) { + my $percentile = nearest(0.1,percentile(($p/100),\@latencyarray)); + print FH "$percentile," + } + } + + print FH "$meanlatency,$args{'jitter'}\n"; + + close outputfile; +} + +####################################################################################################### + +sub percentile { + my $percentile = $_[0]; + my @unsorted = @{$_[1]}; + + my @sorted = sort { $a <=> $b } @unsorted; + my $index = ((scalar @sorted) -1) * $percentile; + $index = int ($index + 0.5); + return $sorted[$index]; +} + +sub mean { + my $result; + foreach (@_) { $result += $_ } + return $result / @_; +} + +sub nearest { + # nearest (.1, 2.054) = 2.1 + my $nearest = shift; + my $num = shift; + + $num = $num / $nearest; + $num = int ($num + 0.5); + $num = $num * $nearest; + return $num; +} + +sub printusage { + print "\nMailburn is a tool for testing network performance using an SMTP server back-end.\n\n"; + print "It will send emails for a given number of seconds, but can limit itself to a given rate in Mbps. Rather than throttling the network connection itself, mailburn simply monitors the number of messages sent so far, compares the total data to the time elapsed (in microseconds), and refuses to send more mail until the rate so far is at or below the specified rate limit. After testing for the number of seconds requested, it returns information regarding throughput and latency of emails sent. (NOTE: 'throughput' here only measures the data payload, and does not include message headers or SMTP overhead.)\n\n"; + print "Usage: mailburn --to {valid email address} --size {size of data payload in MB} --host {SMTP host to connect to}\n"; + print " [--port {port} ] ... SMTP port (default 25)\n"; + print " [-r {rate limit} ] ... specified in Mbps (default none)\n"; + print " [-t {seconds} ] ... time to run test, default 30 \n"; + # disabling progressive throttling due to unreasonable CPU load + # print "-p [throttling sample period in ms] "; + print " [-o {filespec} ] ... output filespec for CSV report \n"; + print " [--no-header ] ... suppress header row of CSV output \n"; + print " [-q ] ... quiet (suppress all but CSV output) \n"; + print " [-h {hostname} ] ... override system-defined hostname in CSV output \n"; + # not ready to dyke out the concurrency option entirely yet, but it doesn't really make any sense with SMTP sending anyway. + # this would be the parallel send (and wait for all children to finish) model from netburn, which mimics loading a complex HTTP + # "page" containing multiple elements. + # print " [-c {concurrency} ] ... number of concurrent messages for each 'send' operation\n"; + print " [-ifinfo {iface} ] ... output detailed wifi info about {iface}\n"; + print " [--jitter {value} ] ... sleep random interval up to {value} ms before each send\n"; + print " [--percentile {n} ] ... report results at each nth percentile instead of defaults\n"; + print " [--minp {n} ] ... begin percentile reporting at nth percentile (default 50)\n"; + print " [--maxp {n} ] ... end percentile reporting at nth percentile (default 100)\n"; + print "\n"; + print " [--usage ] ... you're looking at this right now \n"; + print "\n"; + return; +} + +sub makemessage { + my $length = shift; + my @chars = ("A".."Z", "a".."z", "0".."9"); + my $payload; + $payload .= $chars[rand @chars] for 1..$length; + + my $message = Email::MIME->create( + header_str => [ + From => $args{'from'}, + To => $args{'to'}, + Subject => 'mailburn payload', + ], + attributes => { + encoding => 'quoted-printable', + charset => 'ISO-8859-1', + }, + body_str => "$payload\n", + ); + return $message; +} + +sub microtime { + my ($seconds,$microseconds) = gettimeofday(); + my $microtime = "$seconds." . sprintf ("%.06d", $microseconds); + return $microtime; +} + +sub getargs { + my @args = @_; + my %args; + + my %novaluearg; + my %validarg; + push my @validargs, ('usage','size','to','from','host','port','r','t','o','no-header','q','h','ifinfo','jitter','percentile','minp','maxp'); + foreach my $item (@validargs) { $validarg{$item} = 1; } + + push my @novalueargs, ('usage','no-header','q'); + foreach my $item (@novalueargs) { $novaluearg{$item} = 1; } + + push my @mandatoryargs, ('to','size','host'); + + # if user specified no args at all, force --usage on + if (scalar @args == 0) { $args{'usage'}=1; } + + while (my $rawarg = shift(@args)) { + my $arg = $rawarg; + my $argvalue = ''; + if ($rawarg =~ /=/) { + # user specified the value for a CLI argument with = + # instead of with blank space. separate appropriately. + $argvalue = $arg; + $arg =~ s/=.*$//; + $argvalue =~ s/^.*=//; + } + if ($rawarg =~ /^--/) { + # doubledash arg + $arg =~ s/^--//; + if (! $validarg{$arg}) { die "ERROR: don't understand argument $rawarg.\n"; } + if ($novaluearg{$arg}) { + $args{$arg} = 1; + } else { + # if this CLI arg takes a user-specified value and + # we don't already have it, then the user must have + # specified with a space, so pull in the next value + # from the array as this value rather than as the + # next argument. + if ($argvalue eq '') { $argvalue = shift(@args); } + $args{$arg} = $argvalue; + } + } elsif ($arg =~ /^-/) { + # singledash arg + $arg =~ s/^-//; + if (! $validarg{$arg}) { die "ERROR: don't understand argument $rawarg.\n"; } + if ($novaluearg{$arg}) { + $args{$arg} = 1; + } else { + # if this CLI arg takes a user-specified value and + # we don't already have it, then the user must have + # specified with a space, so pull in the next value + # from the array as this value rather than as the + # next argument. + if ($argvalue eq '') { $argvalue = shift(@args); } + $args{$arg} = $argvalue; + } + } else { + # bare arg + die "ERROR: don't know what to do with bare argument $rawarg.\n"; + } + } + + # if we aren't checking for usage, + my @missingargs; + if (!defined $args{'usage'}) { + foreach my $mandarg (@mandatoryargs) { + if (! defined $args{$mandarg}) { push @missingargs, $mandarg; } + } + } + if (scalar @missingargs) { + printusage(); + + my $errortext = "ERROR: missing mandatory arguments "; + foreach my $missingarg (@missingargs) { + $errortext .= "-$missingarg, "; + } + # trim trailing ", " from errortext + $errortext = substr($errortext,0,((length $errortext)-2)); + die "$errortext\n"; + } + + # set defaults for undefined non-mandatory arguments + # if (!defined $args{'p'}) { $args{'p'} = 0; } + $args{'p'} = 0; # progressive throttling is too demanding on the CPU + if (!defined $args{'r'}) { $args{'r'} = -1; } + if (!defined $args{'t'}) { $args{'t'} = 30; } + if (!defined $args{'port'}) { $args{'port'} = 25; } + if (!defined $args{'from'}) { $args{'from'} = 'mailburn@contoso.com'; } + if (!defined $args{'minp'}) { $args{'minp'} = 50; } + if (!defined $args{'maxp'}) { $args{'maxp'} = 100; } + if (!defined $args{'jitter'}) { $args{'jitter'} = 0; } + + $args{'size'} =~ s/m\s*^//i; # strip any "helpful" M suffix supplied by user + $args{'size'} = $args{'size'} * 1024 * 1024; # convert from MB to B + if ($args{'size'} == 0) { $args{'size'} = 100; } # mailservers do NOT like zero-data text body + + return %args; +} + +sub timestamp { + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(shift); + my @abbr = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec); + my $month = $abbr[$mon]; + $year += 1900; + $mon ++; + + $mon = sprintf('%02d',$mon); + $mday = sprintf('%02d',$mday); + $sec = sprintf('%02d',$sec); + $min = sprintf('%02d',$min); + $hour = sprintf('%02d',$hour); + + my $timestamp = "$year-$mon-$mday $hour:$min:$sec"; + return $timestamp; +} + +sub concurrentsend { + # concurrentsend (10,$message) + # concurrently send 10 copies of the message specified, in parallel, + # and return the total amount of data sent by all 10 children. + # + # note that this doesn't really make a lick of sense with SMTP; + # it's code that was pulled in from netburn, where it's used to model + # fetching a complex HTML page with multiple elements. + + my $concurrency = shift; + my $message = shift; + + my @kids; + + # so we can loop from 0 to $concurrency instead of 1 to $concurrency. + $concurrency --; + + foreach my $count (0 .. $concurrency) { + my $pid = open $kids [$count] => "-|"; + die "Failed to fork: $!" unless defined $pid; + unless ($pid) { + # If I'm here, I'm a kid. So do kid stuff. + + # send the message + sendmail($message, { transport => $transport }); + + # ANY print from kid will go to parent, to be read by @stuff = + print $args{'size'} + 221; # 221 bytes is rough length of headers + + exit; # this kid's job is done, so exit without doing parent stuff that comes next + } + } + # If I made it here, I'm a parent process again. + + # $kids[n] == PID of child process n that we opened above; so <$kids[n]> waits for that PID to close its pipe to us. + # this works but it's sort of maybe sucky-ish for netburn because it means the parent will bind waiting for kids[0] + # instead of immediately reading kids[1]-kids[5] if kids[0] takes longer to complete. + + my $totalbytessent; + foreach my $fh (@kids) { + my @lines = <$fh>; + $totalbytessent += $lines[0]; + } + + # "reap the children using wait", whatever the fuck that means? "shouldn't be necessary since we didn't install a signal handler." wonderful. + 1 while -1 != wait; + + return $totalbytessent; +} + +sub getifinfo { + # get and return wireless interface information + my $if = shift; + + my $text = `/sbin/iwconfig $if 2>/dev/null`; + # current iwconfig output format looks like this: + # + # test0 IEEE 802.11AC ESSID:"Wirecutter-test" Nickname:"" + # Mode:Managed Frequency:5.745 GHz Access Point: C0:4A:00:0A:AB:C7 + # Bit Rate:867 Mb/s Sensitivity:0/0 + # Retry:off RTS thr:off Fragment thr:off + # Encryption key:****-****-****-****-****-****-****-**** Security mode:open + # Power Management:off + # Link Quality=98/100 Signal level=100/100 Noise level=0/100 + # Rx invalid nwid:0 Rx invalid crypt:0 Rx invalid frag:0 + # Tx excessive retries:0 Invalid misc:0 Missed beacon:0 + + $text =~ s/\r?\n/ /g; + + my $essid = $text; + $essid =~ s/^.*ESSID:"//; + $essid =~ s/".*$//; + + my $freq = $text; + $freq =~ s/^.*Frequency://; + $freq =~ s/GHz.*$/GHz/; + + my $bitrate = $text; + $bitrate =~ s/^.*Bit Rate://; + $bitrate =~ s/ Mb\/s.*$/ Mbps/; + + my $ap = $text; + $ap =~ s/^.*Point: //; + $ap =~ s/ .*$//; + + my $quality = $text; + $quality =~ s/^.*Link Quality=//; + $quality =~ s/ .*$//; + + return ($essid,$freq,$ap,$bitrate,$quality); +} + diff --git a/usr/bin/recycle b/usr/bin/recycle new file mode 100644 index 0000000..0615c75 --- /dev/null +++ b/usr/bin/recycle @@ -0,0 +1,6 @@ +#!/bin/sh + +# to disconnect and reconnect from a wifi network named "testing" using interface named "test0" + +/usr/bin/nmcli c down id "testing" ifname test0 +/usr/bin/nmcli c up id "testing" ifname test0 diff --git a/usr/bin/roamdetect b/usr/bin/roamdetect new file mode 100644 index 0000000..ef4f873 --- /dev/null +++ b/usr/bin/roamdetect @@ -0,0 +1,30 @@ +#!/usr/bin/perl + +# detect roaming on wifi wlan1 +# +# note: this assumes that the interface you want to monitor is literally named "wlan1" and +# that a .wav file is actually present at /usr/share/sounds/speech-dispatcher/test.wav. If +# either of those isn't the case on your machine, adjust as appropriate ... and/or submit a +# PR to update this poor little utility script to actually accept arguments and do things +# more intelligently =) + +my $ap; +my $old_ap = `iwconfig wlan1 | grep Frequency`; +chomp $old_ap; +$old_ap =~ s/^.*Freq/Freq/; +print `aplay -q /usr/share/sounds/speech-dispatcher/test.wav`; +print "starting AP is $old_ap.\n"; + +do { + $ap = `iwconfig wlan1 | grep Frequency`; + chomp $ap; + $ap =~ s/^.*Freq/Freq/; + if ($ap ne $old_ap) { + print `aplay -q /usr/share/sounds/speech-dispatcher/test.wav`; + print "roamed from $old_ap to $ap!\n"; + $old_ap = $ap; + } else { + # print "no change, ap = $ap\n"; + } + sleep 1; +} while 1; diff --git a/usr/bin/wifiwatchdog b/usr/bin/wifiwatchdog new file mode 100755 index 0000000..eed2778 --- /dev/null +++ b/usr/bin/wifiwatchdog @@ -0,0 +1,94 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +# argument processing with Pod::Usage and Getopt::Long + my %args; + argsinit(); + +# Configure logging + my ($logger, $fileappender, $filelayout, $screenappender, $screenlayout); + loginit(); + +# check for connectivity to network gateway +my $up = `/bin/ping -c1 -W1 $args{'gateway'} 2>&1`; + +if ($up =~ "unreachable" || $up =~ "100% packet loss") { + # connection down - bring it back up + #print `/usr/bin/aplay /usr/share/orage/sounds/Boiling.wav`; + $logger->error("could not reach $args{'gateway'}, restarting monitored network $args{'netid'} on interface $args{'ifname'}"); + my $restart = `/usr/bin/nmcli c up id "$args{'netid'}" ifname $args{'ifname'} 2>&1`; + if ($restart =~ 'failed') { + $logger->error("failed to restart $args{'netid'} on interface $args{'ifname'}."); + } else { + $logger->info("successfully restarted $args{'netid'} on interface $args{'ifname'}."); + } +} else { + # connection should be up - nothing to do + # this should normally not be output anywhere, since logger is set to INFO and above only + $logger->debug("successfully reached $args{'gateway'} - nothing to do."); +} + +exit 0; + +##############################################################################################3 +##############################################################################################3 +##############################################################################################3 +##############################################################################################3 + +sub argsinit { + use Pod::Usage; + use Getopt::Long qw(:config auto_version auto_help); + %args = ('gateway' => '', 'netid' => '', 'ifname' => '', 'quiet' => ''); + GetOptions(\%args, "gateway|g=s", "netid|n=s", "ifname|i=s", "quiet|q") or pod2usage(2); +} + +sub loginit { + # Ubuntu: apt install liblog-log4perl-perl liblog-dispatch-perl + use Log::Log4perl qw(get_logger :levels); + use Log::Log4perl::Appender::Screen; + use Log::Log4perl::Appender::File; + + $logger = Log::Log4perl->get_logger(); + + $screenappender = Log::Log4perl::Appender->new( + 'Log::Log4perl::Appender::Screen', + stderr => 0, + ); + $screenlayout = Log::Log4perl::Layout::PatternLayout->new( "%p - %m%n" ); + $screenappender->layout($screenlayout); + + my $fileappender = Log::Log4perl::Appender->new( + 'Log::Log4perl::Appender::File', + filename => '/var/log/syslog', + mode => 'append', + ); + $filelayout = Log::Log4perl::Layout::PatternLayout->new( "%d %H %F{1}[%P]: %p - %m%n" ); + $fileappender->layout($filelayout); + + $logger->add_appender($fileappender); + if (!$args{'quiet'}) { $logger->add_appender($screenappender); } + + $logger->level($INFO); +} + +__END__ + +=head1 NAME + +wifiwatchdog - wifi connection monitoring and automatic restarting tool, to be used from cron + +=head1 SYNOPSIS + + wifiwatchdog -n "[NetID]" -g [IP] -i [ifname] + + SOURCE Source ZFS dataset. Can be either local or remote + TARGET Target ZFS dataset. Can be either local or remote + +Options: + + -n | --netid Network-Manager profile ID to be monitored. (Usually the SSID itself) + -g | --gateway Address ping for connectivity checks - usually the local gateway + -i | --ifname The network interface to restart the wifi profile on, if it's down +