On Sat, 8 Mar 2003, Jonathan B. Bayer wrote: > At a talk that Jim gave last month, he said that one of the included > monitors was a traceroute monitor, which would keep track of when a > route would change. > > I haven't been able to find it, can someone point me in the right > direction?
jon's traceroute.monitor is included in the distribution (see mon-0.99.2/mon.d/traceroute.monitor), and i'm attaching to this mail the one which i wrote. it can recognize load-balanced hops and optionally not count them as a path divergence, which is sometimes useful.
#!/usr/bin/perl # # trace.monitor # # trace the route to an address, record previous routes, # compare newest path to last path and report divergences # while considering load-balanced hops, and log paths # historically. # # for use with mon # # use "trace.monitor -h" for help # # Jim Trocki # # $Id: trace.monitor 1.3 Fri, 14 Mar 2003 01:30:36 -0500 trockij $ # # Copyright (C) 2001-2003, Jim Trocki # # 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 2 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. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # use strict; use Getopt::Std; use Data::Dumper; sub traceroute; sub print_path; sub load_last; sub path_to_string; sub debug; sub path_to_hash; sub test; sub append_log; sub print_hop; sub usage; sub process_hosts; my %opt; getopts ('hLs:l:d:t:m:', \%opt); if ($opt{"h"}) { usage; exit; } die "must supply host\n" if (@ARGV == 0); my $TIMEOUT = $opt{"t"} || 30; my $DEBUG = $opt{"d"} || 0; my $METHOD = "m"; if ($opt{"m"} ne "" && $opt{"m"} !~ /^[mn]$/) { die "method must be one of 'n' or 'm'\n"; } if ($opt{"m"}) { $METHOD = $opt{"m"}; } #my $LOGDIR = "/var/lib/mon/log.d"; #my $STATEDIR = "/var/lib/mon/state.d"; my $LOGDIR = "."; my $STATEDIR = "."; if (-d $opt{"l"}) { $LOGDIR = $opt{"l"}; } elsif (-d $ENV{"MON_LOGDIR"}) { $LOGDIR = $ENV{"MON_LOGDIR"}; } if (-d $opt{"s"}) { $STATEDIR = $opt{"s"}; } elsif (-d $ENV{"MON_STATEDIR"}) { $STATEDIR = $ENV{"MON_STATEDIR"}; } # # do the testing on each host # my ($failures, $failure_detail, $successes, $success_detail) = process_hosts (@ARGV); # # all the testing/logging is done, # now report the successes and failures # my $num_failures = @{$failures}; if ($num_failures) { print "@{$failures}\n"; } else { print "\n"; } for (my $i = 0; $i < @{$failures}; $i++) { print "$failures->[$i]\n--------------------\n"; print "$failure_detail->[$i]\n"; print "\n"; } if ($num_failures) { print "\n"; } for (my $i = 0; $i < @{$successes}; $i++) { print "$successes->[$i]\n---------------------------\n"; print "$success_detail->[$i]\n"; print "\n"; } exit $num_failures; # # print path # # if second arg is true, return the string # instead of printing it # sub print_path { my ($path, $str) = @_; my $string = ""; for (my $i= 0; $i < @{$path->{"path"}}; $i++) { my $hop = $path->{"path"}->[$i]; my @h = (); foreach my $list (@{$hop}) { push @h, sprintf ('%-15s %-10s', $list->[0], $list->[1]); } if ($str) { $string .= sprintf ("%02d %s\n", $i, "@h"); } else { printf ("%02d %s\n", $i, "@h"); } } $string; } sub print_hop { my ($path, $hopnum) = @_; my $hop = $path->{"path"}->[$hopnum]; my @h = (); foreach my $list (@{$hop}) { push @h, sprintf ('%-15s %-10s', $list->[0], $list->[1]); } sprintf ("%02d %s\n", $hopnum, "@h"); } sub save_last { my ($f, $p) = @_; if (!open (OUT, ">$f")) { return "$!"; } if (!$p->{"time"}) { print OUT time . " "; } else { print OUT "$p->{time} "; } print OUT "$p->{to} "; print OUT path_to_string ($p), "\n"; close (OUT); ""; } sub append_log { my ($f, $p) = @_; if (!open (OUT, ">>$f")) { return "$!"; } if (!$p->{"time"}) { print OUT time . " "; } else { print OUT "$p->{time} "; } print OUT "$p->{to} "; print OUT path_to_string ($p), "\n"; close (OUT); ""; } sub load_last { my ($f) = @_; if (!open (IN, $f)) { return "$!"; } my ($time, $path, $to); while (<IN>) { next if (/^\s*#/ || /^\s*$/); chomp; next if (!/^\d+\s/); ($time, $to, $path) = split (/\s+/, $_); last; } close (IN); if ($path eq "") { return ("no path found in file"); } my %p; $p{"time"} = $time; $p{"to"} = $to; $p{"path"} = string_to_path ($path); $p{"hpath"} = path_to_hash ($p{"path"}); ("", { %p }); } sub path_to_string { my ($path) = @_; my @formatted_path; foreach my $hop (@{$path->{"path"}}) { my @tries = (); foreach my $hop_try (@{$hop}) { push @tries, "$hop_try->[0]/$hop_try->[1]"; } push @formatted_path, join (",", @tries); } join ("-", @formatted_path); } sub string_to_path { my ($string) = @_; my @path; foreach my $hop (split (/-/, $string)) { my @tries = (); foreach my $try (split (/,/, $hop)) { push @tries, [split (/\//, $try)]; } push @path, [EMAIL PROTECTED]; } [EMAIL PROTECTED]; } sub save_path { my ($file, $path) = @_; } # # returns -1 if paths do not diverge, # or the index into @{$path1} where they do. # sub compare_paths { my ($path1, $path2, $behavior) = @_; # # $behavior is one of: # "n" normal # "m" mux mode, treat all routes on the same hop as # equals # my $i = 0; my $diverge = -1; while ($i < @{$path1->{"path"}} && $diverge == -1) { debug ("comparing hop $i"); # # path1 is longer than path2 # if ($i >= @{$path2->{"path"}}) { debug ("path1 longer than path2"); $diverge = $i; last; } else { # # MUX method # # no divergence if at least one of the routers for this # hop matches with the last sample. this is an attempt # to consider load-balanced hops. # # if ($behavior eq "m") { debug ("comparing using mux"); my $found = 0; foreach my $ip (keys %{$path1->{"hpath"}->[$i]}) { if ($path2->{"hpath"}->[$i]->{$ip} > 0) { debug ("found matching route at $i"); $found = 1; last; } } if (!$found) { debug ("did not find matching router at pos $i"); $diverge = $i; last; } } # # DEFAULT method # # default is to compare all routers for each hop # between path samples, and if they differ at all, # then consider it a divergence. # else { debug ("comparing using default"); # # hop tries differ # if (@{$path1->{"path"}->[$i]} != @{$path2->{"path"}->[$i]}) { $diverge = $i; last; } else { for (my $j = 0; $j < @{$path1->{"path"}->[$i]}; $j++) { if ($path1->{"path"}->[$i]->[$j]->[0] ne $path2->{"path"}->[$i]->[$j]->[0]) { debug ("found divergence index $j"); $diverge = $i; last; } else { debug ("no divergence index $j"); } } } } } $i++; } if ($diverge != -1 && @{$path1->{"path"}} != @{$path2->{"path"}}) { debug ("path lengths differ"); return $#{$path1->{"path"}}; } return $diverge; } # # traceroute to a host and return a data structure of the hops # and timings # # returns the list: # ( # "error msg, empty string if no error", # { # "path" => # [ # [["hop1 try1", ms], ["hop1 try2", ms], ["hop1 try3", ms]], # [["hop2 try2", ms], ...], # ... # ], # "hpath" => # [ # {"ipaddr" => count, ...}, # ], # } # ) # sub traceroute { my ($host, $timeout, $traceroute_args) = @_; my $pid; if (!($pid = open (IN, "traceroute -n $traceroute_args $host 2>/dev/null |"))) { return ($!, []); } my $hop = 0; my @hops = (); my @hash_hops = (); if ($timeout) { $SIG{"ALRM"} = sub {die "timeout" }; } eval { if ($timeout) { alarm ($timeout); } while (<IN>) { if (!/^\s*\d+/) { debug ("skipping $_"); next; } my $line = $_; chomp $line; $line =~ s/^\s*//; debug ($line, 5); my @l = split (/\s+/, $line); $hop = shift @l; my @hoplist = (); my %hophash = (); my $i = 0; my $router = ""; while ($i < @l) { if ($l[$i] =~ /^\d+\.\d+\.\d+\.\d+$/) { $router = $l[$i]; $i++; } # # timeout # elsif ($l[$i] eq "*") { push @hoplist, ["*", 0]; $hophash{"*"}++; $i++; next; } # # a real router reply # if ($router ne "") { if ($l[$i+1] ne "ms") { close (IN); return ("expecting ms [$line]", []); } my $time = $l[$i]; $i += 2; push @hoplist, [$router, $time]; $hophash{$router}++; # # skip over failures # if ($l[$i] =~ /^!/) { $i++; } } else { close (IN); return ("don't know [$line]", []); } } push @hops, [EMAIL PROTECTED]; push @hash_hops, {%hophash}; } if ($timeout) { alarm (0); } }; close (IN); if ($@ && $timeout && $@ =~ /timeout/) { kill 9, $pid; push @hops, [["timeout", $timeout]]; return ("timeout", [EMAIL PROTECTED]); } my $t = time; ("", { "path" => [EMAIL PROTECTED], "hpath" => [EMAIL PROTECTED], "time" => $t, "to" => $host, }); } sub debug { my ($msg, $level) = @_; if ($DEBUG && $level <= $DEBUG) { print STDERR "$msg\n"; } } sub path_to_hash { my $path = shift; my @new_path = (); for (my $i = 0; $i < @{$path}; $i++) { my $hop = $path->[$i]; for (my $j = 0; $j < @{$hop}; $j++) { $new_path[$i]->{$hop->[$j]->[0]}++; } } [EMAIL PROTECTED]; } sub test { my ($msg, $path1, $path2) = @_; $path1->{"hpath"} = path_to_hash ($path1->{"path"}); $path2->{"hpath"} = path_to_hash ($path2->{"path"}); print "BEGIN: $msg\n"; my $r = compare_paths ($path1, $path2, "m"); if ($r == -1) { print "END: $msg no divergence\n"; } else { print "END: $msg divergence at $r\n"; } } sub usage { print <<EOF; usage: trace.monitor -h trace.monitor [-L] [-s dir] [-l dir] [-d num] [-t args] [-m {m,n}] host [host...] traceroute to a host, compare the route paths between invocations. for use with "mon". -L append results to a log file -s dir state dir, overrides MON_STATEDIR -l dir log dir, overrides MON_LOGDIR -d num debug, 1=low 5=high -t args args to be passed to tcpdump -m {m,n} comparison methods, load-balanced or not logs are named "host-month-year.log" EOF } sub process_hosts { my (@hosts) = @_; my (@failures, @failure_detail, @successes, @success_detail); my @loctime = localtime; foreach my $host (@hosts) { my $last_file = "$STATEDIR/$host.lasttrace"; my $log_file = sprintf ('%s/%s-%s-%s.log', $LOGDIR, $host, $loctime[4], 1900 + $loctime[5]); my ($err, $p) = traceroute ($host, $TIMEOUT); if ($err ne "") { push @failures, $host; push @failure_detail, "$host: $err\n"; next; } my ($last, $err); if (-f $last_file) { debug ("loading last $last_file"); ($err, $last) = load_last ($last_file); if ($err ne "") { die "could not load last trace for $host: $err\n"; } } $err = save_last ($last_file, $p); if ($err ne "") { die "could not save last trace for $host: $err\n"; } if ($opt{"L"}) { $err = append_log ($log_file, $p); if ($err ne "") { die "could not append to log: $err\n"; } } my $diverge = undef; if (defined ($last)) { $diverge = compare_paths ($p, $last, $METHOD); if ($diverge != -1) { push @failures, $host; my $old_pathstr = ""; my $new_pathstr = ""; for (my $i = 0; $i < @{$p->{"path"}}; $i++) { my $l_hop = print_hop ($last, $i); my $n_hop = print_hop ($p, $i); my $s = " "; if ($i == $diverge) { $s = "* "; } $l_hop = "$s$l_hop"; $n_hop = "$s$n_hop"; $old_pathstr .= "$l_hop"; $new_pathstr .= "$n_hop"; } push @failure_detail, "divergence at hop $diverge\n" . "old: " . print_hop ($last, $diverge) . "new: " . print_hop ($p, $diverge) . "\n" . "was: " . localtime ($last->{"time"}) . "\n$old_pathstr\n" . "is: " . localtime ($p->{"time"}) . "\n$new_pathstr\n"; } } if ($diverge == -1 || !defined $diverge) { push @successes, $host; push @success_detail, "at " . localtime ($p->{"time"}) . "\n" . print_path ($p, 1) . "\n"; } } ([EMAIL PROTECTED], [EMAIL PROTECTED], [EMAIL PROTECTED], [EMAIL PROTECTED]); }