aboutsummaryrefslogtreecommitdiffstats
path: root/examples
diff options
context:
space:
mode:
authorKristian Lyngstol <kristian@bohemians.org>2016-02-26 10:59:08 +0100
committerKristian Lyngstol <kristian@bohemians.org>2016-02-26 10:59:08 +0100
commit9da864a8da29082369cdd2dd91a735b03577a117 (patch)
tree543d881f3746ddd4805dd0da449783eb42a8a92c /examples
parent9ecc4690b2546ac117204207bec21ee1f6d585cf (diff)
Archive old/unused things
Diffstat (limited to 'examples')
-rwxr-xr-xexamples/historical/clients/lldpdiscover.pl247
-rwxr-xr-xexamples/historical/clients/portnames.pl18
-rw-r--r--examples/historical/mbd/access_list.pl294
-rw-r--r--examples/historical/mbd/derpspan.c48
-rwxr-xr-xexamples/historical/mbd/generate-helper-list.pl15
-rwxr-xr-xexamples/historical/mbd/make-mbd-nets.pl26
-rw-r--r--examples/historical/mbd/mbd-unicast-segfaulting.pl273
-rw-r--r--examples/historical/mbd/mbd-unicast.pl254
-rw-r--r--examples/historical/mbd/mbd.pl254
-rw-r--r--examples/historical/mbd/mbd.pm50
-rw-r--r--examples/historical/mbd/nets.pl171
-rw-r--r--examples/historical/mbd/packetpusher.c127
-rw-r--r--examples/historical/mbd/survey.pl10
-rwxr-xr-xexamples/historical/munin/backfill_total_network_traffic.pl55
-rwxr-xr-xexamples/historical/munin/clients_connected.pl26
-rwxr-xr-xexamples/historical/munin/total_network_traffic.pl31
-rw-r--r--examples/historical/tg15/netlist.txt189
-rw-r--r--examples/historical/tg15/patchlist.txt148
-rw-r--r--examples/historical/tg15/patchlist_extras.txt7
-rw-r--r--examples/historical/tg15/switches.txt148
-rw-r--r--examples/historical/tg15/switches_extras.txt7
-rwxr-xr-xexamples/historical/tools/fetch-portlist.sh42
-rwxr-xr-xexamples/historical/tools/ping-graph.pl62
23 files changed, 2502 insertions, 0 deletions
diff --git a/examples/historical/clients/lldpdiscover.pl b/examples/historical/clients/lldpdiscover.pl
new file mode 100755
index 0000000..2f33bd9
--- /dev/null
+++ b/examples/historical/clients/lldpdiscover.pl
@@ -0,0 +1,247 @@
+#! /usr/bin/perl
+use DBI;
+use POSIX;
+use Time::HiRes;
+use strict;
+use warnings;
+
+use lib '../include';
+use nms;
+
+my $dbh = nms::db_connect();
+$dbh->{AutoCommit} = 0;
+
+# If we are given one switch on the command line, add that and then exit.
+my ($cmdline_ip, $cmdline_community) = @ARGV;
+if (defined($cmdline_ip) && defined($cmdline_community)) {
+ eval {
+ my $session = nms::snmp_open_session($cmdline_ip, $cmdline_community);
+ my $sysname = $session->get('sysName.0');
+ my $chassis_id = get_lldp_chassis_id($session);
+ add_switch($dbh, $cmdline_ip, $sysname, $chassis_id, $cmdline_community);
+ };
+ if ($@) {
+ mylog("ERROR: $@ (during poll of $cmdline_ip)");
+ $dbh->rollback;
+ }
+ $dbh->disconnect;
+ exit;
+}
+
+# Find all candidate SNMP communities.
+my $snmpq = $dbh->prepare("SELECT DISTINCT community FROM switches");
+$snmpq->execute;
+my @communities = ();
+while (my $ref = $snmpq->fetchrow_hashref) {
+ push @communities, $ref->{'community'};
+}
+
+# First, find all machines that lack an LLDP chassis ID.
+my $q = $dbh->prepare("SELECT switch, ip, community FROM switches WHERE lldp_chassis_id IS NULL AND ip <> '127.0.0.1' and switchtype <> 'ex2200'");
+$q->execute;
+
+while (my $ref = $q->fetchrow_hashref) {
+ my ($switch, $ip, $community) = ($ref->{'switch'}, $ref->{'ip'}, $ref->{'community'});
+ eval {
+ my $session = nms::snmp_open_session($ip, $community);
+ my $chassis_id = get_lldp_chassis_id($session);
+ die "SNMP error: " . $session->error() if (!defined($chassis_id));
+ $dbh->do('UPDATE switches SET lldp_chassis_id=? WHERE switch=?', undef,
+ $chassis_id, $switch);
+ mylog("Set chassis ID for $ip to $chassis_id.");
+ };
+ if ($@) {
+ mylog("ERROR: $@ (during poll of $ip)");
+ $dbh->rollback;
+ }
+}
+$dbh->commit;
+
+# Now ask all switches for their LLDP neighbor table.
+$q = $dbh->prepare("SELECT ip, sysname, community FROM switches WHERE lldp_chassis_id IS NOT NULL AND ip <> '127.0.0.1' AND switchtype <> 'ex2200'");
+$q->execute;
+
+while (my $ref = $q->fetchrow_hashref) {
+ my ($ip, $sysname, $community) = ($ref->{'ip'}, $ref->{'sysname'}, $ref->{'community'});
+ eval {
+ discover_lldp_neighbors($dbh, $ip, $sysname, $community);
+ };
+ if ($@) {
+ mylog("ERROR: $@ (during poll of $ip)");
+ $dbh->rollback;
+ }
+ $dbh->commit;
+}
+
+$dbh->disconnect;
+
+sub discover_lldp_neighbors {
+ my ($dbh, $ip, $local_sysname, $community) = @_;
+ my $qexist = $dbh->prepare('SELECT COUNT(*) AS cnt FROM switches WHERE lldp_chassis_id=?');
+
+ my $session = nms::snmp_open_session($ip, $community);
+ my $remtable = $session->gettable('lldpRemTable');
+ my $addrtable;
+ while (my ($key, $value) = each %$remtable) {
+ my $chassis_id = nms::convert_mac($value->{'lldpRemChassisId'});
+ my $sysname = $value->{'lldpRemSysName'};
+
+ # Do not try to poll servers.
+ my %caps = ();
+ nms::convert_lldp_caps($value->{'lldpRemSysCapEnabled'}, \%caps);
+ next if (!$caps{'cap_enabled_bridge'} && !$caps{'cap_enabled_router'});
+ next if ($caps{'cap_enabled_ap'});
+ next if ($caps{'cap_enabled_telephone'});
+
+ next if $sysname =~ /nocnexus/;
+
+ my $sysdesc = $value->{'lldpRemSysDesc'};
+ next if $sysdesc =~ /\b(C1530|C3600|C3700)\b/;
+
+ my $exists = $dbh->selectrow_hashref($qexist, undef, $chassis_id)->{'cnt'};
+ next if ($exists);
+
+ print "Found $local_sysname -> $sysname ($chassis_id)\n";
+
+ # Pull in the management address table lazily.
+ $addrtable = $session->gettable("lldpRemManAddrTable") if (!defined($addrtable));
+
+ # Search for this key in the address table.
+ my @v4addrs = ();
+ my @v6addrs = ();
+ while (my ($addrkey, $addrvalue) = each %$addrtable) {
+ #next unless $addrkey =~ /^\Q$key\E\.1\.4\.(.*)$/; # 1.4 = ipv4, 2.16 = ipv6
+ next unless $addrkey =~ /^\Q$key\E\./; # 1.4 = ipv4, 2.16 = ipv6
+ my $addr = $addrvalue->{'lldpRemManAddr'};
+ my $addrtype = $addrvalue->{'lldpRemManAddrSubtype'};
+ if ($addrtype == 1) {
+ push @v4addrs, nms::convert_ipv4($addr);
+ } elsif ($addrtype == 2) {
+ my $v6addr = nms::convert_ipv6($addr);
+ next if $v6addr =~ /^fe80:/; # Ignore link-local.
+ push @v6addrs, $v6addr;
+ } else {
+ die "Unknown address type $addr";
+ }
+ }
+ my $addr;
+ if (scalar @v6addrs > 0) {
+ $addr = $v6addrs[0];
+ } elsif (scalar @v4addrs > 0) {
+ $addr = $v4addrs[0];
+ } else {
+ warn "Could not find a management address for chassis ID $chassis_id (sysname=$sysname, lldpRemIndex=$key)";
+ next;
+ }
+
+ # We simply guess that the community is the same as ours.
+ add_switch($dbh, $addr, $sysname, $chassis_id, @communities);
+ }
+}
+
+sub mylog {
+ my $msg = shift;
+ my $time = POSIX::ctime(time);
+ $time =~ s/\n.*$//;
+ printf STDERR "[%s] %s\n", $time, $msg;
+}
+
+sub get_ports {
+ my ($ip, $sysname, $community) = @_;
+ my $ret = undef;
+ eval {
+ my $session = nms::snmp_open_session($ip, $community);
+ $ret = $session->gettable('ifTable', columns => [ 'ifType', 'ifDescr' ]);
+ };
+ if ($@) {
+ mylog("Error during SNMP to $ip ($sysname): $@");
+ return undef;
+ }
+ return $ret;
+}
+
+sub get_ifindex_for_physical_ports {
+ my $ports = shift;
+ my @indices = ();
+ for my $port (values %$ports) {
+ next unless ($port->{'ifType'} eq 'ethernetCsmacd');
+ push @indices, $port->{'ifIndex'};
+ }
+ return @indices;
+}
+
+sub compress_ports {
+ my (@ports) = @_;
+ my $current_range_start = undef;
+ my $last_port = undef;
+
+ my @ranges = ();
+ for my $port (sort { $a <=> $b } (@ports)) {
+ if (!defined($current_range_start)) {
+ # First element.
+ $current_range_start = $last_port = $port;
+ next;
+ }
+ if ($port == $last_port + 1) {
+ # Just extend the current range.
+ ++$last_port;
+ } else {
+ push @ranges, range_from_to($current_range_start, $last_port);
+ $current_range_start = $last_port = $port;
+ }
+ }
+ push @ranges, range_from_to($current_range_start, $last_port);
+ return join(',', @ranges);
+}
+
+sub range_from_to {
+ my ($from, $to) = @_;
+ if ($from == $to) {
+ return $from;
+ } else {
+ return "$from-$to";
+ }
+}
+
+sub add_switch {
+ my ($dbh, $addr, $sysname, $chassis_id, @communities) = @_;
+
+ # Yay, a new switch! Make a new type for it.
+ my $ports;
+ my $community;
+ for my $cand_community (@communities) {
+ $community = $cand_community;
+ $ports = get_ports($addr, $sysname, $community);
+ last if (defined($ports));
+ }
+ return if (!defined($ports));
+ my $portlist = compress_ports(get_ifindex_for_physical_ports($ports));
+ mylog("Inserting new switch $sysname ($addr, ports $portlist).");
+ my $switchtype = "auto-$sysname-$chassis_id";
+ $dbh->do('INSERT INTO switchtypes (switchtype, ports) VALUES (?, ?)', undef,
+ $switchtype, $portlist);
+ $dbh->do('INSERT INTO switches (ip, sysname, switchtype, community, lldp_chassis_id) VALUES (?, ?, ?, ?, ?)', undef,
+ $addr, $sysname, $switchtype, $community, $chassis_id);
+ for my $port (values %$ports) {
+ $dbh->do('INSERT INTO portnames (switchtype, port, description) VALUES (?, ?, ?)',
+ undef, $switchtype, $port->{'ifIndex'}, $port->{'ifDescr'});
+ }
+
+ # Entirely random placement. Annoying? Fix it yourself.
+ my $x = int(rand 1200);
+ my $y = int(rand 650);
+ my $box = sprintf "((%d,%d),(%d,%d))", $x, $y, $x+40, $y+40;
+ $dbh->do("INSERT INTO placements (switch,placement) VALUES (CURRVAL('switches_switch_seq'), ?)",
+ undef, $box);
+
+ $dbh->commit;
+}
+
+sub get_lldp_chassis_id {
+ my ($session) = @_;
+
+ # Cisco returns completely bogus values if we use get()
+ # on lldpLocChassisId.0, it seems. Work around it by using getnext().
+ my $response = $session->getnext('lldpLocChassisId');
+ return nms::convert_mac($response);
+}
diff --git a/examples/historical/clients/portnames.pl b/examples/historical/clients/portnames.pl
new file mode 100755
index 0000000..52e433a
--- /dev/null
+++ b/examples/historical/clients/portnames.pl
@@ -0,0 +1,18 @@
+#! /usr/bin/perl
+
+my ($host,$switchtype,$community) = @ARGV;
+
+open SNMP, "snmpwalk -Os -c $community -v 2c $host -mALL ifDescr |"
+ or die "snmpwalk: $!";
+
+print "begin;\n";
+print "delete from portnames where switchtype='$switchtype';\n";
+
+while (<SNMP>) {
+ chomp;
+ /^ifDescr\.(\d+) = STRING: (.*)$/ or next;
+
+ print "insert into portnames (switchtype,port,description) values ('$switchtype',$1,'$2 (port $1)');\n";
+}
+
+print "end;\n";
diff --git a/examples/historical/mbd/access_list.pl b/examples/historical/mbd/access_list.pl
new file mode 100644
index 0000000..89a8182
--- /dev/null
+++ b/examples/historical/mbd/access_list.pl
@@ -0,0 +1,294 @@
+
+package Config;
+
+sub game_id {
+ my ($data, $offset) = @_;
+ my $id = ((ord(substr($data, $offset, 1)) << 8) | ord(substr($data, $offset + 1, 1)));
+ return $id;
+}
+
+our @access_list = (
+ # half-life - untested (packet dump only)
+ {
+ name => 'Half-Life',
+ ports => [ 27015 ],
+ sizes => [ 16 ]
+ },
+
+ # cs 1.6 - verified
+ # (funker muligens for _alle_ source-spill inkl. hl2/cs:s)
+ {
+ name => 'CS:Source',
+ ports => [ "26900..26903", "27015..27017" ],
+ sizes => [ 25 ],
+ filter => sub { return (game_id(shift, 4) == 0x4325); }
+ },
+ {
+ name => 'Left 4 Dead',
+ ports => [ "26900..26903", "27015..27017" ],
+ sizes => [ 25 ],
+ filter => sub { return (game_id(shift, 4) == 0x43f3); }
+ },
+ {
+ name => 'CS 1.6',
+ ports => [ "26900..26903", "27015..27017" ],
+ sizes => [ 25 ],
+ filter => sub { return (game_id(shift, 4) == 0x5453); }
+ },
+ {
+ name => 'Unknown Source-based game (ID 0x4326)',
+ ports => [ "26900..26903", "27015..27017" ],
+ sizes => [ 25 ],
+ filter => sub { return (game_id(shift, 4) == 0x4326); }
+ },
+ {
+ name => 'Other Source game (unknown game ID)',
+ ports => [ "26900..26903", "27015..27017" ],
+ sizes => [ 25 ],
+ },
+ {
+ name => 'Other Source game (unknown game ID, odd length 33)',
+ ports => [ "26900..26903", "27015..27017" ],
+ sizes => [ 33 ],
+ },
+ {
+ name => 'Other Source game (unknown game ID, odd length 58)',
+ ports => [ "26900..26903", "27015..27017" ],
+ sizes => [ 58 ],
+ },
+ {
+ name => 'Other Source game (unknown game ID, odd length 15)',
+ ports => [ "26900..26903", "27015..27017" ],
+ sizes => [ 15 ],
+ },
+
+ # doom 3 - verified
+ {
+ name => 'Doom 3',
+ ports => [ "27666" ],
+ sizes => [ 14 ]
+ },
+
+ # quake 1 - verified
+ {
+ name => 'Quake 1',
+ ports => [ 26000 ],
+ sizes => [ 12 ]
+ },
+
+ # q3a - tested with demo only
+ # rtcw: enemy territory - untested (packet dump only)
+ {
+ name => 'Quake 3 Arena, RTCW: ET',
+# ports => [ "27960..27969" ],
+ ports => [ "27960..27961" ],
+ sizes => [ 15 ]
+ },
+
+ # bf2 - tested with demo only
+ # bf2142 reportedly uses same engine
+ {
+ name => 'BF2/BF2142',
+ ports => [ "29900" ],
+ sizes => [ 8 ]
+ },
+
+ # bf1942 - unverified (packet dump only)
+ {
+ name => 'BF1942',
+ ports => [ "22000..22010" ],
+ sizes => [ 8 ]
+ },
+
+ # quake 4 - tested with demo only, MUST select "internet"
+ {
+ name => 'Quake 4',
+ ports => [ 27950, 28004 ],
+ sizes => [ 14 ]
+ },
+
+ # quake 2 - untested (packet dump only)
+ {
+ name => 'Quake 2',
+ ports => [ 27910 ],
+ sizes => [ 11 ]
+ },
+
+ # warcraft 3 - untested (packet dump only)
+ {
+ name => 'Warcraft 3: Reign of Chaos (1.00)',
+ ports => [ 6112 ],
+ sizes => [ 16 ],
+ filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x3352 && ord(substr($data, 8, 1)) == 0; }
+ },
+ {
+ name => 'Warcraft 3: Reign of Chaos (1.07)',
+ ports => [ 6112 ],
+ sizes => [ 16 ],
+ filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x3352 && ord(substr($data, 8, 1)) == 7; }
+ },
+ {
+ name => 'Warcraft 3: Reign of Chaos (1.20)',
+ ports => [ 6112 ],
+ sizes => [ 16 ],
+ filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x3352 && ord(substr($data, 8, 1)) == 20; }
+ },
+ {
+ name => 'Warcraft 3: Reign of Chaos (1.22)',
+ ports => [ 6112 ],
+ sizes => [ 16 ],
+ filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x3352 && ord(substr($data, 8, 1)) == 22; }
+ },
+ {
+ name => 'Warcraft 3: Reign of Chaos (1.23)',
+ ports => [ 6112 ],
+ sizes => [ 16 ],
+ filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x3352 && ord(substr($data, 8, 1)) == 23; }
+ },
+ {
+ name => 'Warcraft 3: Reign of Chaos (other patch level)',
+ ports => [ 6112 ],
+ sizes => [ 16 ],
+ filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x3352; }
+ },
+ {
+ name => 'Warcraft 3: The Frozen Throne (1.17)',
+ ports => [ 6112 ],
+ sizes => [ 16 ],
+ filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x5058 && ord(substr($data, 8, 1)) == 17; }
+ },
+ {
+ name => 'Warcraft 3: The Frozen Throne (1.18)',
+ ports => [ 6112 ],
+ sizes => [ 16 ],
+ filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x5058 && ord(substr($data, 8, 1)) == 18; }
+ },
+ {
+ name => 'Warcraft 3: The Frozen Throne (1.20)',
+ ports => [ 6112 ],
+ sizes => [ 16 ],
+ filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x5058 && ord(substr($data, 8, 1)) == 20; }
+ },
+ {
+ name => 'Warcraft 3: The Frozen Throne (1.21)',
+ ports => [ 6112 ],
+ sizes => [ 16 ],
+ filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x5058 && ord(substr($data, 8, 1)) == 21; }
+ },
+ {
+ name => 'Warcraft 3: The Frozen Throne (1.22)',
+ ports => [ 6112 ],
+ sizes => [ 16 ],
+ filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x5058 && ord(substr($data, 8, 1)) == 22; }
+ },
+ {
+ name => 'Warcraft 3: The Frozen Throne (1.23)',
+ ports => [ 6112 ],
+ sizes => [ 16 ],
+ filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x5058 && ord(substr($data, 8, 1)) == 23; }
+ },
+ {
+ name => 'Warcraft 3: The Frozen Throne (1.26)',
+ ports => [ 6112 ],
+ sizes => [ 16 ],
+ filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x5058 && ord(substr($data, 8, 1)) == 26; }
+ },
+ {
+ name => 'Warcraft 3: The Frozen Throne (other patch level)',
+ ports => [ 6112 ],
+ sizes => [ 16 ],
+ filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) == 0x5058; }
+ },
+ {
+ name => 'Warcraft 3 (unknown version)',
+# ports => [ "6112..6119" ],
+ ports => [ 6112 ],
+ sizes => [ 16 ],
+ filter => sub { my $data = shift; return (ord(substr($data, 1, 1)) == 0x2f) && game_id($data, 4) != 0x5058 && game_id($data, 4) != 0x3352; }
+ },
+ {
+ name => 'Warcraft 3 (unknown version, odd length)',
+ ports => [ 6112 ],
+ sizes => [ 19 ],
+ },
+
+ # ut2003/ut2004 - untested (packet dump only)
+ {
+ name => 'UT2003/UT2004',
+ ports => [ 10777 ],
+ sizes => [ 5 ]
+ },
+
+ # soldat - untested (packet dump only)
+ {
+ name => 'Soldat',
+ ports => [ 23073 ],
+ sizes => [ 8 ]
+ },
+
+ # starcraft - untested (packet dump only)
+ {
+ name => 'Starcraft',
+ ports => [ 6111, 6112 ],
+ sizes => [ 8 ],
+ filter => sub { return (game_id(shift, 0) == 0x08ef); }
+ },
+ {
+ name => 'Starcraft: Brood War',
+ ports => [ 6111, 6112 ],
+ sizes => [ 8 ],
+ filter => sub { return (game_id(shift, 0) == 0xf733); }
+ },
+ {
+ name => 'Starcraft (unknown game ID)',
+ ports => [ 6111, 6112 ],
+ sizes => [ 8 ],
+ filter => sub { my $id = game_id(shift, 0); return ($id != 0x08ef && $id != 0xf733); }
+ },
+
+ # trackmania nations - untested (packet dump only)
+ {
+ name => 'Trackmania Nations',
+ ports => [ "2350" ],
+ sizes => [ 42, 30 ]
+ },
+
+ # company of heroes - untested (packet dump only)
+ {
+ name => 'Company of Heroes',
+ ports => [ 9100 ],
+ sizes => [ 39 ]
+ },
+
+ # command & conquer 3 - untested (packet dump only, reported to have some kind
+ # of chat functionality)
+# {
+# name => 'Command & Conquer 3',
+# ports => [ "8086..8093" ],
+# sizes => [ 476 ],
+# filter => sub { return 0; }
+# },
+
+ # openttd
+ {
+ name => 'OpenTTD',
+ ports => [ 3979 ],
+ sizes => [ 3 ]
+ },
+
+ # CoD4
+ {
+ name => 'Call of Duty 4',
+ ports => [ 28960 ],
+ sizes => [ 15 ],
+ },
+
+ # Far Cry 2
+ {
+ name => 'Far Cry 2',
+ ports => [ 9004 ],
+ sizes => [ 114, 118, 122, 126 ],
+ },
+
+ # unreal tournament, port 9777?
+)
diff --git a/examples/historical/mbd/derpspan.c b/examples/historical/mbd/derpspan.c
new file mode 100644
index 0000000..b9fb362
--- /dev/null
+++ b/examples/historical/mbd/derpspan.c
@@ -0,0 +1,48 @@
+// gcc -O2 -o derspan derspan.c -lpcap -std=gnu99 -Wall
+
+#include <pcap.h>
+#include <stdlib.h>
+#include <netinet/ip.h>
+#include <stdint.h>
+#include <stdio.h>
+
+int rawsock;
+
+void my_callback(u_char *user, const struct pcap_pkthdr *h, const u_char *bytes)
+{
+ int len = h->caplen;
+ if (len < 40) {
+ //printf("skipped short packet\n");
+ return;
+ }
+ if (bytes[14] != 0x88 || bytes[15] != 0xbe) {
+ //printf("skipped non-ethernet packet\n");
+ return;
+ }
+ if (bytes[36] != 0x08 || bytes[37] != 0x00) {
+ //printf("skipped non-IPv4 packet\n");
+ return;
+ }
+
+ struct sockaddr_in self;
+ self.sin_family = AF_INET;
+ self.sin_addr.s_addr = htonl(0x7f000001); // localhost
+ self.sin_port = htons(1337);
+
+ sendto(rawsock, bytes + 38, len - 38, 0, (struct sockaddr *)&self, sizeof(self));
+}
+
+int main(int argc, char **argv)
+{
+ rawsock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
+ if (rawsock == -1) {
+ perror("socket");
+ exit(0);
+ }
+
+ pcap_t *pcap = pcap_open_live(argv[1], 1500, 1, 1000, NULL);
+ pcap_activate(pcap);
+ pcap_loop(pcap, -1, my_callback, NULL);
+ return 0;
+}
+
diff --git a/examples/historical/mbd/generate-helper-list.pl b/examples/historical/mbd/generate-helper-list.pl
new file mode 100755
index 0000000..fd89475
--- /dev/null
+++ b/examples/historical/mbd/generate-helper-list.pl
@@ -0,0 +1,15 @@
+#! /usr/bin/perl
+use strict;
+use warnings;
+require './access_list.pl';
+require './nets.pl';
+require './mbd.pm';
+
+my @ports = mbd::find_all_ports();
+
+print "no ip forward-protocol udp 137\n";
+print "no ip forward-protocol udp 138\n";
+
+for my $port (@ports) {
+ print "ip forward-protocol udp $port\n";
+}
diff --git a/examples/historical/mbd/make-mbd-nets.pl b/examples/historical/mbd/make-mbd-nets.pl
new file mode 100755
index 0000000..7f6ec97
--- /dev/null
+++ b/examples/historical/mbd/make-mbd-nets.pl
@@ -0,0 +1,26 @@
+#!/usr/bin/perl -I /root/tgmanage/
+use strict;
+use warnings;
+
+unless (@ARGV > 0) {
+ print "No arguments. Need netlist.txt.\n";
+ exit 1;
+}
+
+my $n = open(NETLIST, "$ARGV[0]") or die ("Cannot open netlist.txt");
+
+print "# Autogenerated. Do not touch!\n";
+print "package Config;\n";
+print "our \@networks = (\n";
+
+while(<NETLIST>) {
+ next if /^(.*#|\s+$)/; # skip if comment, or blank line
+
+ chomp;
+ my ($v4_net, $v6_net, $net_name) = split;
+
+ print "\t\"$v4_net\",\n";
+}
+
+print ");\n";
+print "1;\n";
diff --git a/examples/historical/mbd/mbd-unicast-segfaulting.pl b/examples/historical/mbd/mbd-unicast-segfaulting.pl
new file mode 100644
index 0000000..c167511
--- /dev/null
+++ b/examples/historical/mbd/mbd-unicast-segfaulting.pl
@@ -0,0 +1,273 @@
+#! /usr/bin/perl
+use strict;
+use warnings;
+use Socket;
+use Net::CIDR;
+use Net::RawIP;
+use Time::HiRes;
+require './access_list.pl';
+require './nets.pl';
+require './survey.pl';
+require './mbd.pm';
+use lib '../include';
+use nms;
+use strict;
+use warnings;
+use threads;
+
+# Mark packets with DSCP CS7
+my $tos = 56;
+
+my ($dbh, $q);
+
+sub fhbits {
+ my $bits = 0;
+ for my $fh (@_) {
+ vec($bits, fileno($fh), 1) = 1;
+ }
+ return $bits;
+}
+
+# used for rate limiting
+my %last_sent = ();
+
+# for own surveying
+my %active_surveys = ();
+my %last_survey = ();
+
+my %cidrcache = ();
+sub cache_cidrlookup {
+ my ($addr, $net) = @_;
+ my $key = $addr . " " . $net;
+
+ if (!exists($cidrcache{$key})) {
+ $cidrcache{$key} = Net::CIDR::cidrlookup($addr, $net);
+ }
+ return $cidrcache{$key};
+}
+
+my %rangecache = ();
+sub cache_cidrrange {
+ my ($net) = @_;
+
+ if (!exists($rangecache{$net})) {
+ my ($range) = Net::CIDR::cidr2range($net);
+ $range =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.(\d+)\.(\d+)/ or die "Did not understand range: $range";
+ my @range = ();
+ for my $l (($4+1)..($8-1)) {
+ push @range, "$1.$2.$3.$l";
+ }
+ ($rangecache{$net}) = \@range;
+ }
+
+ return @{$rangecache{$net}};
+}
+
+open LOG, ">>", "mbd.log";
+
+my @ports = ( mbd::find_all_ports() , $Config::survey_port_low .. $Config::survey_port_high );
+
+# Open a socket for each port
+my @socks = ();
+my $udp = getprotobyname("udp");
+for my $p (@ports) {
+ my $sock;
+ socket($sock, PF_INET, SOCK_DGRAM, $udp);
+ bind($sock, sockaddr_in($p, INADDR_ANY));
+ push @socks, $sock;
+}
+
+my $sendsock = Net::RawIP->new({udp => {}});
+
+print "Listening on " . scalar @ports . " ports.\n";
+
+# Main loop
+while (1) {
+ my $rin = fhbits(@socks);
+ my $rout;
+
+ my $nfound = select($rout=$rin, undef, undef, undef);
+ my $now = [Time::HiRes::gettimeofday];
+
+ # First of all, close any surveys that are due.
+ for my $sport (keys %active_surveys) {
+ my $age = Time::HiRes::tv_interval($active_surveys{$sport}{start}, $now);
+ if ($age > $Config::survey_time && $active_surveys{$sport}{active}) {
+ my $hexdump = join(' ', map { sprintf "0x%02x", ord($_) } (split //, $active_surveys{$sport}{data}));
+ print "Survey ($hexdump) for '" . $Config::access_list[$active_surveys{$sport}{entry}]->{name} . "'/" .
+ $active_surveys{$sport}{dport} . ": " . $active_surveys{$sport}{num} . " active servers.\n";
+ $active_surveys{$sport}{active} = 0;
+
+ # (re)connect to the database if needed
+ if (!defined($dbh) || !$dbh->ping) {
+ $dbh = nms::db_connect();
+ $q = $dbh->prepare("INSERT INTO mbd_log (ts,game,port,description,active_servers) VALUES (CURRENT_TIMESTAMP,?,?,?,?)")
+ or die "Couldn't prepare query";
+ }
+ $q->execute($active_surveys{$sport}{entry}, $active_surveys{$sport}{dport}, $Config::access_list[$active_surveys{$sport}{entry}]->{name}, $active_surveys{$sport}{num});
+ }
+ if ($age > $Config::survey_time * 3.0) {
+ delete $active_surveys{$sport};
+ }
+ }
+
+ for my $sock (@socks) {
+ next unless (vec($rout, fileno($sock), 1) == 1);
+
+ my $data;
+ my $addr = recv($sock, $data, 8192, 0); # jumbo broadcast! :-P
+ my ($sport, $saddr) = sockaddr_in($addr);
+ my ($dport, $daddr) = sockaddr_in(getsockname($sock));
+ my $size = length($data);
+
+ # Check if this is a survey reply
+ if ($dport >= $Config::survey_port_low && $dport <= $Config::survey_port_high) {
+ if (!exists($active_surveys{$dport})) {
+ print "WARNING: Unknown survey port $dport, ignoring\n";
+ next;
+ }
+ if (!$active_surveys{$dport}{active}) {
+ # remains
+ next;
+ }
+
+ ++$active_surveys{$dport}{num};
+
+ next;
+ }
+
+ # Rate limiting
+ if (exists($last_sent{$saddr}{$dport})) {
+ my $elapsed = Time::HiRes::tv_interval($last_sent{$saddr}{$dport}, $now);
+ if ($elapsed < 1.0) {
+ print LOG "$dport $size 2\n";
+ print inet_ntoa($saddr), ", $dport, $size bytes => rate-limited ($elapsed secs since last)\n";
+ next;
+ }
+ }
+
+ # We don't get the packet's destination address, but I guess this should do...
+ # Check against the ACL.
+ my $pass = 0;
+ my $entry = -1;
+ for my $rule (@Config::access_list) {
+ ++$entry;
+
+ next unless (mbd::match_ranges($dport, $rule->{'ports'}));
+ next unless (mbd::match_ranges($size, $rule->{'sizes'}));
+
+ if ($rule->{'filter'}) {
+ next unless ($rule->{'filter'}($data));
+ }
+
+ $pass = 1;
+ last;
+ }
+
+ print LOG "$dport $size $pass\n";
+
+ if (!$pass) {
+ print inet_ntoa($saddr), ", $dport, $size bytes => filtered\n";
+ next;
+ }
+
+ $last_sent{$saddr}{$dport} = $now;
+
+ # The packet is OK! Do we already have a recent enough survey
+ # for this port, or should we use this packet?
+ my $survey = 1;
+ if (exists($last_survey{$entry . "/" . $dport})) {
+ my $age = Time::HiRes::tv_interval($last_survey{$entry . "/" . $dport}, $now);
+ if ($age < $Config::survey_freq) {
+ $survey = 0;
+ }
+ }
+
+ # New survey; find an unused port
+ my $survey_sport;
+ if ($survey) {
+ for my $port ($Config::survey_port_low..$Config::survey_port_high) {
+ if (!exists($active_surveys{$port})) {
+ $survey_sport = $port;
+
+ $active_surveys{$port} = {
+ start => $now,
+ active => 1,
+ dport => $dport,
+ entry => $entry,
+ num => 0,
+ data => $data,
+ };
+ $last_survey{$entry . "/" . $dport} = $now;
+
+ last;
+ }
+ }
+
+ if (!defined($survey_sport)) {
+ print "WARNING: no free survey source ports, not surveying.\n";
+ $survey = 0;
+ }
+ }
+
+ # precache
+ for my $net (@Config::networks) {
+ cache_cidrrange($net);
+ cache_cidrlookup(inet_ntoa($saddr), $net);
+ }
+
+ threads->create(sub {
+ my $sendsock = Net::RawIP->new({udp => {}});
+ my ($survey_sport, $dport, $data) = @_;
+
+ my $num_nets = 0;
+ for my $net (@Config::networks) {
+ my @daddrs = cache_cidrrange($net);
+
+ if ($survey) {
+ for my $daddr (@daddrs) {
+ $sendsock->set({
+ ip => {
+ saddr => $Config::survey_ip,
+ daddr => $daddr,
+ tos => $tos
+ },
+ udp => {
+ source => $survey_sport,
+ dest => $dport,
+ data => $data
+ }
+ });
+ $sendsock->send;
+ }
+ }
+
+ next if (cache_cidrlookup(inet_ntoa($saddr), $net));
+
+ for my $daddr (@daddrs) {
+ $sendsock->set({
+ ip => {
+ saddr => inet_ntoa($saddr),
+ daddr => $daddr,
+ tos => $tos
+ },
+ udp => {
+ source => $sport,
+ dest => $dport,
+ data => $data
+ }
+ });
+ $sendsock->send;
+ }
+
+ ++$num_nets;
+ }
+ if ($survey) {
+ print inet_ntoa($saddr), ", $dport, $size bytes => ($num_nets networks) [+survey from port $survey_sport]\n";
+ } else {
+ print inet_ntoa($saddr), ", $dport, $size bytes => ($num_nets networks)\n";
+ }
+ }, $survey_sport, $dport, $data)->detach();
+ }
+}
+
diff --git a/examples/historical/mbd/mbd-unicast.pl b/examples/historical/mbd/mbd-unicast.pl
new file mode 100644
index 0000000..6e63dee
--- /dev/null
+++ b/examples/historical/mbd/mbd-unicast.pl
@@ -0,0 +1,254 @@
+#! /usr/bin/perl
+use strict;
+use warnings;
+use Socket;
+use Net::CIDR;
+use Net::RawIP;
+use Time::HiRes;
+require './access_list.pl';
+require './nets.pl';
+require './survey.pl';
+require './mbd.pm';
+use lib '../include';
+use nms;
+use strict;
+use warnings;
+use threads;
+
+# Mark packets with DSCP CS7
+my $tos = 56;
+
+my ($dbh, $q);
+
+sub fhbits {
+ my $bits = 0;
+ for my $fh (@_) {
+ vec($bits, fileno($fh), 1) = 1;
+ }
+ return $bits;
+}
+
+# used for rate limiting
+my %last_sent = ();
+
+# for own surveying
+my %active_surveys = ();
+my %last_survey = ();
+
+my %cidrcache = ();
+sub cache_cidrlookup {
+ my ($addr, $net) = @_;
+ my $key = $addr . " " . $net;
+
+ if (!exists($cidrcache{$key})) {
+ $cidrcache{$key} = Net::CIDR::cidrlookup($addr, $net);
+ }
+ return $cidrcache{$key};
+}
+
+my %rangecache = ();
+sub cache_cidrrange {
+ my ($net) = @_;
+
+ if (!exists($rangecache{$net})) {
+ my ($range) = Net::CIDR::cidr2range($net);
+ $range =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.(\d+)\.(\d+)/ or die "Did not understand range: $range";
+ my @range = ();
+ for my $l (($4+1)..($8-1)) {
+ push @range, "$1.$2.$3.$l";
+ }
+ ($rangecache{$net}) = \@range;
+ }
+
+ return @{$rangecache{$net}};
+}
+
+open LOG, ">>", "mbd.log";
+
+my @ports = ( mbd::find_all_ports() , $Config::survey_port_low .. $Config::survey_port_high );
+
+# Open a socket for each port
+my @socks = ();
+my $udp = getprotobyname("udp");
+for my $p (@ports) {
+ my $sock;
+ socket($sock, PF_INET, SOCK_DGRAM, $udp);
+ bind($sock, sockaddr_in($p, INADDR_ANY));
+ push @socks, $sock;
+}
+
+my $sendsock = Net::RawIP->new({udp => {}});
+
+print "Listening on " . scalar @ports . " ports.\n";
+
+open PKTS, "| ./packetpusher"
+ or die "./packetpusher: $!";
+
+# Main loop
+while (1) {
+ my $rin = fhbits(@socks);
+ my $rout;
+
+ my $nfound = select($rout=$rin, undef, undef, undef);
+ my $now = [Time::HiRes::gettimeofday];
+
+ # First of all, close any surveys that are due.
+ for my $sport (keys %active_surveys) {
+ my $age = Time::HiRes::tv_interval($active_surveys{$sport}{start}, $now);
+ if ($age > $Config::survey_time && $active_surveys{$sport}{active}) {
+ my $hexdump = join(' ', map { sprintf "0x%02x", ord($_) } (split //, $active_surveys{$sport}{data}));
+ print "Survey ($hexdump) for '" . $Config::access_list[$active_surveys{$sport}{entry}]->{name} . "'/" .
+ $active_surveys{$sport}{dport} . ": " . $active_surveys{$sport}{num} . " active servers.\n";
+ $active_surveys{$sport}{active} = 0;
+
+ # (re)connect to the database if needed
+ if (!defined($dbh) || !$dbh->ping) {
+ $dbh = nms::db_connect();
+ $q = $dbh->prepare("INSERT INTO mbd_log (ts,game,port,description,active_servers) VALUES (CURRENT_TIMESTAMP,?,?,?,?)")
+ or die "Couldn't prepare query";
+ }
+ $q->execute($active_surveys{$sport}{entry}, $active_surveys{$sport}{dport}, $Config::access_list[$active_surveys{$sport}{entry}]->{name}, $active_surveys{$sport}{num});
+ }
+ if ($age > $Config::survey_time * 3.0) {
+ delete $active_surveys{$sport};
+ }
+ }
+
+ for my $sock (@socks) {
+ next unless (vec($rout, fileno($sock), 1) == 1);
+
+ my $data;
+ my $addr = recv($sock, $data, 8192, 0); # jumbo broadcast! :-P
+ my ($sport, $saddr) = sockaddr_in($addr);
+ my ($dport, $daddr) = sockaddr_in(getsockname($sock));
+ my $size = length($data);
+
+ # Check if this is a survey reply
+ if ($dport >= $Config::survey_port_low && $dport <= $Config::survey_port_high) {
+ if (!exists($active_surveys{$dport})) {
+ print "WARNING: Unknown survey port $dport, ignoring\n";
+ next;
+ }
+ if (!$active_surveys{$dport}{active}) {
+ # remains
+ next;
+ }
+
+ ++$active_surveys{$dport}{num};
+
+ next;
+ }
+
+ # Rate limiting
+ if (exists($last_sent{$saddr}{$dport})) {
+ my $elapsed = Time::HiRes::tv_interval($last_sent{$saddr}{$dport}, $now);
+ if ($elapsed < 1.0) {
+ print LOG "$dport $size 2\n";
+ print inet_ntoa($saddr), ", $dport, $size bytes => rate-limited ($elapsed secs since last)\n";
+ next;
+ }
+ }
+
+ # We don't get the packet's destination address, but I guess this should do...
+ # Check against the ACL.
+ my $pass = 0;
+ my $entry = -1;
+ for my $rule (@Config::access_list) {
+ ++$entry;
+
+ next unless (mbd::match_ranges($dport, $rule->{'ports'}));
+ next unless (mbd::match_ranges($size, $rule->{'sizes'}));
+
+ if ($rule->{'filter'}) {
+ next unless ($rule->{'filter'}($data));
+ }
+
+ $pass = 1;
+ last;
+ }
+
+ print LOG "$dport $size $pass\n";
+
+ if (!$pass) {
+ print inet_ntoa($saddr), ", $dport, $size bytes => filtered\n";
+ next;
+ }
+
+ $last_sent{$saddr}{$dport} = $now;
+
+ # The packet is OK! Do we already have a recent enough survey
+ # for this port, or should we use this packet?
+ my $survey = 1;
+ if (exists($last_survey{$entry . "/" . $dport})) {
+ my $age = Time::HiRes::tv_interval($last_survey{$entry . "/" . $dport}, $now);
+ if ($age < $Config::survey_freq) {
+ $survey = 0;
+ }
+ }
+
+ # New survey; find an unused port
+ my $survey_sport;
+ if ($survey) {
+ for my $port ($Config::survey_port_low..$Config::survey_port_high) {
+ if (!exists($active_surveys{$port})) {
+ $survey_sport = $port;
+
+ $active_surveys{$port} = {
+ start => $now,
+ active => 1,
+ dport => $dport,
+ entry => $entry,
+ num => 0,
+ data => $data,
+ };
+ $last_survey{$entry . "/" . $dport} = $now;
+
+ last;
+ }
+ }
+
+ if (!defined($survey_sport)) {
+ print "WARNING: no free survey source ports, not surveying.\n";
+ $survey = 0;
+ }
+ }
+
+ # precache
+ for my $net (@Config::networks) {
+ cache_cidrrange($net);
+ cache_cidrlookup(inet_ntoa($saddr), $net);
+ }
+
+ my @packets = ();
+
+ my $num_nets = 0;
+ for my $net (@Config::networks) {
+ my @daddrs = cache_cidrrange($net);
+
+ if ($survey) {
+ for my $daddr (@daddrs) {
+ push @packets, [ $Config::survey_ip, $survey_sport, $daddr, $dport ];
+ }
+ }
+
+ next if (cache_cidrlookup(inet_ntoa($saddr), $net));
+
+ for my $daddr (@daddrs) {
+ push @packets, [ inet_ntoa($saddr), $sport, $daddr, $dport ];
+ }
+
+ ++$num_nets;
+ }
+ if ($survey) {
+ print inet_ntoa($saddr), ", $dport, $size bytes => ($num_nets networks) [+survey from port $survey_sport]\n";
+ } else {
+ print inet_ntoa($saddr), ", $dport, $size bytes => ($num_nets networks)\n";
+ }
+
+ printf PKTS "%d %s\n", scalar @packets, unpack('h*', $data);
+ for my $pkt (@packets) {
+ printf PKTS "%s %s %s %s\n", $pkt->[0], $pkt->[1], $pkt->[2], $pkt->[3];
+ }
+ }
+}
+
diff --git a/examples/historical/mbd/mbd.pl b/examples/historical/mbd/mbd.pl
new file mode 100644
index 0000000..065e76c
--- /dev/null
+++ b/examples/historical/mbd/mbd.pl
@@ -0,0 +1,254 @@
+#! /usr/bin/perl
+use strict;
+use warnings;
+use Socket;
+use Net::CIDR;
+use Net::RawIP;
+use Time::HiRes;
+require './access_list.pl';
+require './nets.pl';
+require './survey.pl';
+require './mbd.pm';
+use lib '../include';
+use nms;
+use strict;
+use warnings;
+
+# Mark packets with DSCP CS7
+my $tos = 56;
+
+my ($dbh, $q);
+
+sub fhbits {
+ my $bits = 0;
+ for my $fh (@_) {
+ vec($bits, fileno($fh), 1) = 1;
+ }
+ return $bits;
+}
+
+# used for rate limiting
+my %last_sent = ();
+
+# for own surveying
+my %active_surveys = ();
+my %last_survey = ();
+
+my %cidrcache = ();
+sub cache_cidrlookup {
+ my ($addr, $net) = @_;
+ my $key = $addr . " " . $net;
+
+ if (!exists($cidrcache{$key})) {
+ $cidrcache{$key} = Net::CIDR::cidrlookup($addr, $net);
+ }
+ return $cidrcache{$key};
+}
+
+my %rangecache = ();
+sub cache_cidrrange {
+ my ($net) = @_;
+
+ if (!exists($rangecache{$net})) {
+ ($rangecache{$net}) = Net::CIDR::cidr2range($net);
+ }
+
+ return $rangecache{$net};
+}
+
+open LOG, ">>", "mbd.log";
+
+my @ports = ( mbd::find_all_ports() , $Config::survey_port_low .. $Config::survey_port_high );
+
+# Open a socket for each port
+my @socks = ();
+my $udp = getprotobyname("udp");
+for my $p (@ports) {
+ my $sock;
+ socket($sock, PF_INET, SOCK_DGRAM, $udp);
+ bind($sock, sockaddr_in($p, INADDR_ANY));
+ push @socks, $sock;
+}
+
+my $sendsock = Net::RawIP->new({udp => {}});
+
+print "Listening on " . scalar @ports . " ports.\n";
+
+# Main loop
+while (1) {
+ my $rin = fhbits(@socks);
+ my $rout;
+
+ my $nfound = select($rout=$rin, undef, undef, undef);
+ my $now = [Time::HiRes::gettimeofday];
+
+ # First of all, close any surveys that are due.
+ for my $sport (keys %active_surveys) {
+ my $age = Time::HiRes::tv_interval($active_surveys{$sport}{start}, $now);
+ if ($age > $Config::survey_time && $active_surveys{$sport}{active}) {
+ my $hexdump = join(' ', map { sprintf "0x%02x", ord($_) } (split //, $active_surveys{$sport}{data}));
+ print "Survey ($hexdump) for '" . $Config::access_list[$active_surveys{$sport}{entry}]->{name} . "'/" .
+ $active_surveys{$sport}{dport} . ": " . $active_surveys{$sport}{num} . " active servers.\n";
+ $active_surveys{$sport}{active} = 0;
+
+ # (re)connect to the database if needed
+ if (!defined($dbh) || !$dbh->ping) {
+ $dbh = nms::db_connect();
+ $q = $dbh->prepare("INSERT INTO mbd_log (ts,game,port,description,active_servers) VALUES (CURRENT_TIMESTAMP,?,?,?,?)")
+ or die "Couldn't prepare query";
+ }
+ $q->execute($active_surveys{$sport}{entry}, $active_surveys{$sport}{dport}, $Config::access_list[$active_surveys{$sport}{entry}]->{name}, $active_surveys{$sport}{num});
+ }
+ if ($age > $Config::survey_time * 3.0) {
+ delete $active_surveys{$sport};
+ }
+ }
+
+ for my $sock (@socks) {
+ next unless (vec($rout, fileno($sock), 1) == 1);
+
+ my $data;
+ my $addr = recv($sock, $data, 8192, 0); # jumbo broadcast! :-P
+ my ($sport, $saddr) = sockaddr_in($addr);
+ my ($dport, $daddr) = sockaddr_in(getsockname($sock));
+ my $size = length($data);
+
+ # Check if this is a survey reply
+ if ($dport >= $Config::survey_port_low && $dport <= $Config::survey_port_high) {
+ if (!exists($active_surveys{$dport})) {
+ print "WARNING: Unknown survey port $dport, ignoring\n";
+ next;
+ }
+ if (!$active_surveys{$dport}{active}) {
+ # remains
+ next;
+ }
+
+ ++$active_surveys{$dport}{num};
+
+ next;
+ }
+
+ # Rate limiting
+ if (exists($last_sent{$saddr}{$dport})) {
+ my $elapsed = Time::HiRes::tv_interval($last_sent{$saddr}{$dport}, $now);
+ if ($elapsed < 1.0) {
+ print LOG "$dport $size 2\n";
+ print inet_ntoa($saddr), ", $dport, $size bytes => rate-limited ($elapsed secs since last)\n";
+ next;
+ }
+ }
+
+ # We don't get the packet's destination address, but I guess this should do...
+ # Check against the ACL.
+ my $pass = 0;
+ my $entry = -1;
+ for my $rule (@Config::access_list) {
+ ++$entry;
+
+ next unless (mbd::match_ranges($dport, $rule->{'ports'}));
+ next unless (mbd::match_ranges($size, $rule->{'sizes'}));
+
+ if ($rule->{'filter'}) {
+ next unless ($rule->{'filter'}($data));
+ }
+
+ $pass = 1;
+ last;
+ }
+
+ print LOG "$dport $size $pass\n";
+
+ if (!$pass) {
+ print inet_ntoa($saddr), ", $dport, $size bytes => filtered\n";
+ next;
+ }
+
+ $last_sent{$saddr}{$dport} = $now;
+
+ # The packet is OK! Do we already have a recent enough survey
+ # for this port, or should we use this packet?
+ my $survey = 1;
+ if (exists($last_survey{$entry . "/" . $dport})) {
+ my $age = Time::HiRes::tv_interval($last_survey{$entry . "/" . $dport}, $now);
+ if ($age < $Config::survey_freq) {
+ $survey = 0;
+ }
+ }
+
+ # New survey; find an unused port
+ my $survey_sport;
+ if ($survey) {
+ for my $port ($Config::survey_port_low..$Config::survey_port_high) {
+ if (!exists($active_surveys{$port})) {
+ $survey_sport = $port;
+
+ $active_surveys{$port} = {
+ start => $now,
+ active => 1,
+ dport => $dport,
+ entry => $entry,
+ num => 0,
+ data => $data,
+ };
+ $last_survey{$entry . "/" . $dport} = $now;
+
+ last;
+ }
+ }
+
+ if (!defined($survey_sport)) {
+ print "WARNING: no free survey source ports, not surveying.\n";
+ $survey = 0;
+ }
+ }
+
+ my $num_nets = 0;
+ for my $net (@Config::networks) {
+ my ($range) = cache_cidrrange($net);
+ $range =~ /-(.*?)$/;
+ my $broadcast = $1;
+
+ if ($survey) {
+ $sendsock->set({
+ ip => {
+ saddr => $Config::survey_ip,
+ daddr => $broadcast,
+ tos => $tos
+ },
+ udp => {
+ source => $survey_sport,
+ dest => $dport,
+ data => $data
+ }
+ });
+ $sendsock->send;
+ }
+
+ next if (cache_cidrlookup(inet_ntoa($saddr), $net));
+
+ $sendsock->set({
+ ip => {
+ saddr => inet_ntoa($saddr),
+ daddr => $broadcast,
+ tos => $tos
+ },
+ udp => {
+ source => $sport,
+ dest => $dport,
+ data => $data
+ }
+ });
+ $sendsock->send;
+
+ ++$num_nets;
+ }
+
+ if ($survey) {
+ print inet_ntoa($saddr), ", $dport, $size bytes => ($num_nets networks) [+survey from port $survey_sport]\n";
+ } else {
+ print inet_ntoa($saddr), ", $dport, $size bytes => ($num_nets networks)\n";
+ }
+ }
+}
+
diff --git a/examples/historical/mbd/mbd.pm b/examples/historical/mbd/mbd.pm
new file mode 100644
index 0000000..b844e5b
--- /dev/null
+++ b/examples/historical/mbd/mbd.pm
@@ -0,0 +1,50 @@
+#! /usr/bin/perl
+use strict;
+use warnings;
+use Socket;
+use Net::CIDR;
+use Net::RawIP;
+require './access_list.pl';
+require './nets.pl';
+
+package mbd;
+
+sub expand_range {
+ my $range = shift;
+
+ if ($range =~ /^(\d+)\.\.(\d+)$/) {
+ return $1..$2;
+ } else {
+ return $range;
+ }
+}
+
+sub match_ranges {
+ my ($elem, $ranges) = @_;
+
+ for my $range (@$ranges) {
+ if ($range =~ /^(\d+)\.\.(\d+)$/) {
+ return 1 if ($elem >= $1 && $elem <= $2);
+ } else {
+ return 1 if ($elem == $range);
+ }
+ }
+
+ return 0;
+}
+
+sub find_all_ports {
+ # Find what ports we need to listen on
+ my %port_hash = ();
+ for my $e (@Config::access_list) {
+ for my $r (@{$e->{'ports'}}) {
+ for my $p (expand_range($r)) {
+ $port_hash{$p} = 1;
+ }
+ }
+ }
+ my @ports = sort { $a <=> $b } keys %port_hash;
+ return @ports;
+}
+
+1;
diff --git a/examples/historical/mbd/nets.pl b/examples/historical/mbd/nets.pl
new file mode 100644
index 0000000..3298657
--- /dev/null
+++ b/examples/historical/mbd/nets.pl
@@ -0,0 +1,171 @@
+# Autogenerated. Do not touch!
+package Config;
+our @networks = (
+ "176.110.98.0/24",
+ "176.110.99.0/24",
+ "176.110.100.0/24",
+ "176.110.102.0/24",
+ "176.110.103.0/24",
+ "176.110.106.0/24",
+ "176.110.107.0/24",
+ "176.110.108.0/22",
+ "176.110.112.0/24",
+ "176.110.113.0/24",
+ "176.110.114.0/24",
+ "176.110.115.0/24",
+ "176.110.116.0/24",
+ "176.110.117.0/24",
+ "176.110.118.0/24",
+ "176.110.119.0/24",
+ "176.110.120.0/24",
+ "176.110.121.0/24",
+ "176.110.122.0/24",
+ "176.110.123.0/24",
+ "176.110.124.0/24",
+ "176.110.126.0/24",
+ "176.110.2.0/25",
+ "176.110.2.128/25",
+ "176.110.4.0/25",
+ "176.110.4.128/25",
+ "176.110.6.0/25",
+ "176.110.6.128/25",
+ "176.110.8.0/25",
+ "176.110.8.128/25",
+ "176.110.10.0/25",
+ "176.110.10.128/25",
+ "176.110.11.0/25",
+ "176.110.11.128/25",
+ "176.110.12.0/25",
+ "176.110.12.128/25",
+ "176.110.13.0/25",
+ "176.110.13.128/25",
+ "176.110.14.0/25",
+ "176.110.14.128/25",
+ "176.110.15.0/25",
+ "176.110.15.128/25",
+ "176.110.16.0/25",
+ "176.110.16.128/25",
+ "176.110.17.0/25",
+ "176.110.17.128/25",
+ "176.110.18.0/25",
+ "176.110.18.128/25",
+ "176.110.19.0/25",
+ "176.110.19.128/25",
+ "176.110.20.0/25",
+ "176.110.20.128/25",
+ "176.110.21.0/25",
+ "176.110.21.128/25",
+ "176.110.22.0/25",
+ "176.110.22.128/25",
+ "176.110.23.0/25",
+ "176.110.23.128/25",
+ "176.110.24.0/25",
+ "176.110.24.128/25",
+ "176.110.25.0/25",
+ "176.110.25.128/25",
+ "176.110.26.0/25",
+ "176.110.26.128/25",
+ "176.110.27.0/25",
+ "176.110.27.128/25",
+ "176.110.29.0/25",
+ "176.110.29.128/25",
+ "176.110.31.0/25",
+ "176.110.31.128/25",
+ "176.110.33.0/25",
+ "176.110.33.128/25",
+ "176.110.35.0/25",
+ "176.110.35.128/25",
+ "176.110.37.0/25",
+ "176.110.37.128/25",
+ "176.110.39.0/25",
+ "176.110.39.128/25",
+ "176.110.41.0/25",
+ "176.110.41.128/25",
+ "176.110.43.0/25",
+ "176.110.43.128/25",
+ "176.110.44.0/25",
+ "176.110.44.128/25",
+ "176.110.45.0/25",
+ "176.110.45.128/25",
+ "176.110.46.0/25",
+ "176.110.46.128/25",
+ "176.110.47.0/25",
+ "176.110.47.128/25",
+ "176.110.48.0/25",
+ "176.110.48.128/25",
+ "176.110.49.0/25",
+ "176.110.49.128/25",
+ "176.110.50.0/25",
+ "176.110.50.128/25",
+ "176.110.51.0/25",
+ "176.110.51.128/25",
+ "176.110.52.0/25",
+ "176.110.52.128/25",
+ "176.110.53.0/25",
+ "176.110.53.128/25",
+ "176.110.54.0/25",
+ "176.110.54.128/25",
+ "176.110.55.0/25",
+ "176.110.55.128/25",
+ "176.110.56.0/25",
+ "176.110.56.128/25",
+ "176.110.57.0/25",
+ "176.110.57.128/25",
+ "176.110.58.0/25",
+ "176.110.58.128/25",
+ "176.110.59.0/25",
+ "176.110.59.128/25",
+ "176.110.60.0/25",
+ "176.110.60.128/25",
+ "176.110.61.0/25",
+ "176.110.61.128/25",
+ "176.110.62.0/25",
+ "176.110.62.128/25",
+ "176.110.63.0/25",
+ "176.110.63.128/25",
+ "176.110.64.0/25",
+ "176.110.64.128/25",
+ "176.110.65.0/25",
+ "176.110.65.128/25",
+ "176.110.66.0/25",
+ "176.110.66.128/25",
+ "176.110.67.0/25",
+ "176.110.67.128/25",
+ "176.110.68.0/25",
+ "176.110.68.128/25",
+ "176.110.69.0/25",
+ "176.110.69.128/25",
+ "176.110.70.0/25",
+ "176.110.70.128/25",
+ "176.110.71.0/25",
+ "176.110.71.128/25",
+ "176.110.72.0/25",
+ "176.110.72.128/25",
+ "176.110.73.0/25",
+ "176.110.73.128/25",
+ "176.110.74.0/25",
+ "176.110.74.128/25",
+ "176.110.75.0/25",
+ "176.110.75.128/25",
+ "176.110.76.0/25",
+ "176.110.76.128/25",
+ "176.110.77.0/25",
+ "176.110.77.128/25",
+ "176.110.79.0/25",
+ "176.110.79.128/25",
+ "176.110.81.0/25",
+ "176.110.81.128/25",
+ "176.110.83.0/25",
+ "176.110.83.128/25",
+ "176.110.84.0/25",
+ "176.110.84.128/25",
+ "176.110.85.0/25",
+ "176.110.85.128/25",
+ "176.110.86.0/25",
+ "176.110.86.128/25",
+ "176.110.87.0/25",
+ "176.110.87.128/25",
+ "176.110.88.0/25",
+ "176.110.88.128/25",
+);
+1;
diff --git a/examples/historical/mbd/packetpusher.c b/examples/historical/mbd/packetpusher.c
new file mode 100644
index 0000000..c21a084
--- /dev/null
+++ b/examples/historical/mbd/packetpusher.c
@@ -0,0 +1,127 @@
+#include <stdio.h>
+#include <sys/socket.h>
+#include <stdlib.h>
+#include <string.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/udp.h>
+#include <stdint.h>
+#include <arpa/inet.h>
+
+char encoded_pkt[4096], workspace[4096];
+unsigned char pkt[2048];
+
+typedef uint32_t u_int32_t;
+
+u_int32_t checksum(unsigned char *buf, unsigned nbytes, u_int32_t sum)
+{
+ int i;
+ /* Checksum all the pairs of bytes first... */
+ for (i = 0; i < (nbytes & ~1U); i += 2) {
+ sum += (u_int16_t)ntohs(*((u_int16_t *)(buf + i)));
+ if (sum > 0xFFFF)
+ sum -= 0xFFFF;
+ }
+
+ /*
+ * If there's a single byte left over, checksum it, too.
+ * Network byte order is big-endian, so the remaining byte is
+ * the high byte.
+ */
+
+ if (i < nbytes) {
+ sum += buf[i] << 8;
+ if (sum > 0xFFFF)
+ sum -= 0xFFFF;
+ }
+
+ return (sum);
+}
+
+u_int32_t wrapsum(u_int32_t sum)
+{
+ sum = ~sum & 0xFFFF;
+ return (htons(sum));
+}
+
+int main(int argc, char **argv)
+{
+ int sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
+ if (sock == -1) {
+ perror("socket");
+ exit(1);
+ }
+
+ for ( ;; ) {
+ int num_pkts;
+ int ret = scanf("%d %s", &num_pkts, encoded_pkt);
+ if (ret != 2) {
+ fprintf(stderr, "error parsing! ret=%d\n", ret);
+ exit(1);
+ }
+ if ((strlen(encoded_pkt) % 2) != 0) {
+ fprintf(stderr, "hex packet has odd length\n");
+ exit(1);
+ }
+
+ // de-hex packet
+ for (int i = 0; i < strlen(encoded_pkt); i += 2) {
+ char c[3];
+ c[0] = encoded_pkt[i];
+ c[1] = encoded_pkt[i + 1];
+ c[2] = 0;
+ int h = strtol(c, NULL, 16);
+ pkt[i / 2] = h;
+ }
+
+ int datalen = strlen(encoded_pkt) / 2;
+
+ for (int i = 0; i < num_pkts; ++i) {
+ char from_ip[256], to_ip[256];
+ int sport, dport;
+ if (scanf("%s %d %s %d", from_ip, &sport, to_ip, &dport) != 4) {
+ fprintf(stderr, "error parsing packet %d!\n", i);
+ exit(1);
+ }
+
+ // IP header
+ struct iphdr *ip = (struct iphdr *)workspace;
+ ip->version = 4;
+ ip->ihl = 5;
+ ip->tos = 0;
+ ip->tot_len = htons(datalen + sizeof(struct iphdr) + sizeof(struct udphdr));
+ ip->id = 0;
+ ip->frag_off = 0;
+ ip->ttl = 64;
+ ip->protocol = 17; // UDP
+ ip->saddr = inet_addr(from_ip);
+ ip->daddr = inet_addr(to_ip);
+ ip->check = 0;
+ ip->check = wrapsum(checksum((unsigned char *)ip, sizeof(*ip), 0));
+
+ // UDP header
+ struct udphdr *udp = (struct udphdr *)(workspace + sizeof(struct iphdr));
+ udp->source = htons(sport);
+ udp->dest = htons(dport);
+ udp->len = htons(datalen + sizeof(struct udphdr));
+ udp->check = 0;
+
+ int sum = checksum((unsigned char *)&ip->saddr, 2 * sizeof(ip->saddr), IPPROTO_UDP + ntohs(udp->len));
+ sum = checksum((unsigned char *)pkt, datalen, sum);
+ sum = checksum((unsigned char *)udp, sizeof(*udp), sum);
+ udp->check = wrapsum(sum);
+
+ // Data
+ memcpy(workspace + sizeof(struct iphdr) + sizeof(struct udphdr),
+ pkt, datalen);
+
+ // Send out the packet physically
+ struct sockaddr_in to;
+ to.sin_family = AF_INET;
+ to.sin_addr.s_addr = inet_addr(to_ip);
+ to.sin_port = htons(dport);
+
+ sendto(sock, workspace, ntohs(ip->tot_len), 0, (struct sockaddr *)&to, sizeof(to));
+ }
+ }
+}
diff --git a/examples/historical/mbd/survey.pl b/examples/historical/mbd/survey.pl
new file mode 100644
index 0000000..be33038
--- /dev/null
+++ b/examples/historical/mbd/survey.pl
@@ -0,0 +1,10 @@
+package Config;
+
+our $survey_ip = "176.110.125.15";
+our $survey_port_low = 60100;
+our $survey_port_high = 60200;
+our $survey_freq = 60.0;
+our $survey_time = 10.0;
+
+1;
+
diff --git a/examples/historical/munin/backfill_total_network_traffic.pl b/examples/historical/munin/backfill_total_network_traffic.pl
new file mode 100755
index 0000000..000b0d5
--- /dev/null
+++ b/examples/historical/munin/backfill_total_network_traffic.pl
@@ -0,0 +1,55 @@
+#! /usr/bin/perl -I/root/tgmanage/include
+use strict;
+use warnings;
+use lib 'include';
+use nms;
+use Data::Dumper::Simple;
+
+use Date::Parse;
+
+my $dbh = nms::db_connect();
+$dbh->{AutoCommit} = 0;
+
+# This has a slightly modded version of get_current_datarate inlined. It's probably outdated by the time you read this.
+my $total_traffic = $dbh->prepare("select sum(bytes_in) * 8 / 1048576.0 / 1024.0 as traffic_out, sum(bytes_out) * 8 / 1048576.0 / 1024.0 as traffic_in from (SELECT switch,port,
+ (bytes_out[1] - bytes_out[2]) / EXTRACT(EPOCH FROM (time[1] - time[2])) AS bytes_out,
+ (bytes_in[1] - bytes_in[2]) / EXTRACT(EPOCH FROM (time[1] - time[2])) AS bytes_in,
+ time[1] AS last_poll_time
+ FROM (
+ SELECT switch,port,
+ ARRAY_AGG(time) AS time,
+ ARRAY_AGG(bytes_in) AS bytes_in,
+ ARRAY_AGG(bytes_out) AS bytes_out
+ FROM (
+ SELECT *,rank() OVER (PARTITION BY switch,port ORDER BY time DESC) AS poll_num
+ FROM polls WHERE time BETWEEN (to_timestamp(?) - interval '5 minutes') AND to_timestamp(?)
+ AND official_port
+ ) t1
+ WHERE poll_num <= 2
+ GROUP BY switch,port
+ ) t2
+ WHERE
+ time[2] IS NOT NULL
+ AND bytes_in[1] >= 0 AND bytes_out[1] >= 0
+ AND bytes_in[2] >= 0 AND bytes_out[2] >= 0
+ AND bytes_out[1] >= bytes_out[2]
+ AND bytes_in[1] >= bytes_in[2]) as datarate natural join switches where switchtype like 'dlink3100%' and port < 45")
+ or die "Can't prepare query: $!";
+
+my $inout = shift @ARGV;
+while (<>) {
+ if (m,<!-- [^/]* CEST / (\d+) --> <row><v>[^<]*</v></row>, && $1 > 1397458800) {
+ my $time = $1;
+ if ($time > 1397458800) {
+ $total_traffic->execute($time, $time);
+ my $ref = $total_traffic->fetchrow_hashref;
+ my $value = $ref->{'traffic_' . $inout};
+ $value = (!defined $value || $value == 0 || $value > 400) ? "NaN" : sprintf "%e", $value;
+ s,<v>[^<]*</v>,<v>$value</v>,;
+ }
+ }
+ print;
+}
+$total_traffic->finish;
+$dbh->disconnect();
+exit 0
diff --git a/examples/historical/munin/clients_connected.pl b/examples/historical/munin/clients_connected.pl
new file mode 100755
index 0000000..5301c63
--- /dev/null
+++ b/examples/historical/munin/clients_connected.pl
@@ -0,0 +1,26 @@
+#! /usr/bin/perl -I/root/tgmanage/include
+use strict;
+use warnings;
+use lib 'include';
+use nms;
+use Data::Dumper::Simple;
+
+my $dbh = nms::db_connect();
+$dbh->{AutoCommit} = 0;
+
+my $active_clients = $dbh->prepare("select family(address), count(distinct(mac)) from seen_mac where family(address) in (6,4) and seen >= now() - INTERVAL '1 hour' group by family(address);")
+ or die "Can't prepare query: $!";
+
+$active_clients->execute;
+print <<EOF;
+graph_title Clients seen the last hour
+graph_vlabel count
+graph_scale no
+EOF
+while (my $ref = $active_clients->fetchrow_hashref) {
+ print "clients_".$ref->{'family'}.".label v".$ref->{'family'}." clients\n";
+ print "clients_".$ref->{'family'}.".value ".$ref->{'count'}."\n";
+}
+$active_clients->finish;
+$dbh->disconnect();
+exit 0
diff --git a/examples/historical/munin/total_network_traffic.pl b/examples/historical/munin/total_network_traffic.pl
new file mode 100755
index 0000000..2c0799b
--- /dev/null
+++ b/examples/historical/munin/total_network_traffic.pl
@@ -0,0 +1,31 @@
+#! /usr/bin/perl -I/root/tgmanage/include
+use strict;
+use warnings;
+use lib 'include';
+use nms;
+use Data::Dumper::Simple;
+
+# By the looks of this code, the in/out values are from the perspective of the
+# switch. However, something gets flipped somewhere which makes it from the
+# perspective of the client. I have no idea why. Have fun!
+
+my $dbh = nms::db_connect();
+$dbh->{AutoCommit} = 0;
+
+my $total_traffic = $dbh->prepare("select sum(bytes_in) * 8 / 1048576.0 / 1024.0 as traffic_in, sum(bytes_out) * 8 / 1048576.0 / 1024.0 as traffic_out from get_current_datarate() natural join switches where switchtype like 'dlink3100%' and port < 45")
+ or die "Can't prepare query: $!";
+
+$total_traffic->execute;
+print <<EOF;
+graph_title Total network traffic
+graph_vlabel Gb/s
+graph_scale no
+EOF
+my $ref = $total_traffic->fetchrow_hashref;
+print "total_network_traffic_in.label Total incoming traffic\n";
+print "total_network_traffic_in.value ". $ref->{'traffic_in'}."\n";
+print "total_network_traffic_out.label Total outgoing traffic\n";
+print "total_network_traffic_out.value ". $ref->{'traffic_out'}."\n";
+$total_traffic->finish;
+$dbh->disconnect();
+exit 0;
diff --git a/examples/historical/tg15/netlist.txt b/examples/historical/tg15/netlist.txt
new file mode 100644
index 0000000..67f2c11
--- /dev/null
+++ b/examples/historical/tg15/netlist.txt
@@ -0,0 +1,189 @@
+## start manuelle nett
+151.216.165.0/26 2a02:ed02:165a::/64 e3-2
+151.216.170.0/24 2a02:ed02:170::/64 crew
+151.216.171.64/26 2a02:ed02:171b::/64 sw1-crew
+151.216.171.128/26 2a02:ed02:171c::/64 sw2-crew
+151.216.171.192/26 2a02:ed02:171d::/64 sw3-crew
+151.216.178.0/26 2a02:ed02:178a::/64 ap-distro0
+151.216.178.128/26 2a02:ed02:178c::/64 ap-distro2
+151.216.178.192/26 2a02:ed02:178d::/64 ap-distro3
+151.216.178.64/26 2a02:ed02:178b::/64 ap-distro1
+151.216.179.0/26 2a02:ed02:179a::/64 ap-distro4
+151.216.179.128/26 2a02:ed02:179c::/64 ap-distro6
+151.216.179.192/26 2a02:ed02:179d::/64 ap-distro7
+151.216.179.64/26 2a02:ed02:179b::/64 ap-distro5
+151.216.192.0/19 2a02:ed02:192::/64 tg15-wlan
+151.216.224.0/24 2a02:ed02:224::/64 logistikk
+151.216.226.0/24 2a02:ed02:226::/64 presse
+151.216.227.0/24 2a02:ed02:227::/64 fugleberget1
+151.216.229.0/24 2a02:ed02:229::/64 game
+151.216.230.0/24 2a02:ed02:230::/64 infodesk
+151.216.232.0/24 2a02:ed02:232::/64 resepsjon
+151.216.233.0/24 2a02:ed02:233::/64 sponsor
+151.216.234.0/24 2a02:ed02:234::/64 lounge
+151.216.235.0/24 2a02:ed02:235::/64 stage
+151.216.236.0/24 2a02:ed02:236::/64 medic
+151.216.237.0/24 2a02:ed02:237::/64 gamecrew
+151.216.240.0/24 2a02:ed02:240::/64 studio
+151.216.241.0/24 2a02:ed02:241::/64 motell
+151.216.243.0/24 2a02:ed02:243::/64 nextron
+151.216.244.0/24 2a02:ed02:244::/64 event-north
+151.216.249.0/26 2a02:ed02:249a::/64 southcam
+151.216.254.0/24 2a02:ed02:254::/64 noc
+## slutt manuelle nett
+# make netlist.txt from switches.txt;
+# cat switches.txt | perl -lne '($n,$v4,$v6)=split;print "$v4 $v6 $n";'
+## start creativia
+151.216.166.0/26 2a02:ed02:166a::/64 creativia1
+151.216.166.64/26 2a02:ed02:166b::/64 creativia2
+151.216.166.128/26 2a02:ed02:166c::/64 creativia3
+151.216.166.192/26 2a02:ed02:166d::/64 creativia4
+151.216.167.0/26 2a02:ed02:167a::/64 creativia5
+151.216.167.64/26 2a02:ed02:167b::/64 creativia6
+151.216.167.128/26 2a02:ed02:167c::/64 creativia7
+151.216.167.192/26 2a02:ed02:167d::/64 creativia8
+## slutt creativia
+## start switches.txt
+151.216.129.0/26 2a02:ed02:129a::/64 e1-3
+151.216.129.64/26 2a02:ed02:129b::/64 e3-3
+151.216.129.128/26 2a02:ed02:129c::/64 e3-4
+151.216.129.192/26 2a02:ed02:129d::/64 e5-2
+151.216.130.0/26 2a02:ed02:130a::/64 e5-3
+151.216.130.64/26 2a02:ed02:130b::/64 e5-4
+151.216.130.128/26 2a02:ed02:130c::/64 e7-2
+151.216.130.192/26 2a02:ed02:130d::/64 e7-3
+151.216.131.0/26 2a02:ed02:131a::/64 e7-4
+151.216.131.64/26 2a02:ed02:131b::/64 e9-2
+151.216.131.128/26 2a02:ed02:131c::/64 e9-3
+151.216.131.192/26 2a02:ed02:131d::/64 e9-4
+151.216.132.0/26 2a02:ed02:132a::/64 e11-1
+151.216.132.64/26 2a02:ed02:132b::/64 e11-2
+151.216.132.128/26 2a02:ed02:132c::/64 e11-3
+151.216.132.192/26 2a02:ed02:132d::/64 e11-4
+151.216.133.0/26 2a02:ed02:133a::/64 e13-1
+151.216.133.64/26 2a02:ed02:133b::/64 e13-2
+151.216.133.128/26 2a02:ed02:133c::/64 e13-3
+151.216.133.192/26 2a02:ed02:133d::/64 e13-4
+151.216.134.0/26 2a02:ed02:134a::/64 e15-1
+151.216.134.64/26 2a02:ed02:134b::/64 e15-2
+151.216.134.128/26 2a02:ed02:134c::/64 e15-3
+151.216.134.192/26 2a02:ed02:134d::/64 e15-4
+151.216.135.0/26 2a02:ed02:135a::/64 e17-1
+151.216.135.64/26 2a02:ed02:135b::/64 e17-2
+151.216.135.128/26 2a02:ed02:135c::/64 e17-3
+151.216.135.192/26 2a02:ed02:135d::/64 e17-4
+151.216.136.0/26 2a02:ed02:136a::/64 e19-1
+151.216.136.64/26 2a02:ed02:136b::/64 e19-2
+151.216.136.128/26 2a02:ed02:136c::/64 e19-3
+151.216.136.192/26 2a02:ed02:136d::/64 e19-4
+151.216.137.0/26 2a02:ed02:137a::/64 e21-1
+151.216.137.64/26 2a02:ed02:137b::/64 e21-2
+151.216.137.128/26 2a02:ed02:137c::/64 e21-3
+151.216.137.192/26 2a02:ed02:137d::/64 e21-4
+151.216.138.0/26 2a02:ed02:138a::/64 e23-1
+151.216.138.64/26 2a02:ed02:138b::/64 e23-2
+151.216.138.128/26 2a02:ed02:138c::/64 e23-3
+151.216.138.192/26 2a02:ed02:138d::/64 e23-4
+151.216.139.0/26 2a02:ed02:139a::/64 e25-1
+151.216.139.64/26 2a02:ed02:139b::/64 e25-2
+151.216.139.128/26 2a02:ed02:139c::/64 e25-3
+151.216.139.192/26 2a02:ed02:139d::/64 e25-4
+151.216.140.0/26 2a02:ed02:140a::/64 e27-1
+151.216.140.64/26 2a02:ed02:140b::/64 e27-2
+151.216.140.128/26 2a02:ed02:140c::/64 e27-3
+151.216.140.192/26 2a02:ed02:140d::/64 e27-4
+151.216.141.0/26 2a02:ed02:141a::/64 e27-1
+151.216.141.64/26 2a02:ed02:141b::/64 e27-2
+151.216.141.128/26 2a02:ed02:141c::/64 e29-1
+151.216.141.192/26 2a02:ed02:141d::/64 e29-2
+151.216.142.0/26 2a02:ed02:142a::/64 e31-1
+151.216.142.64/26 2a02:ed02:142b::/64 e31-2
+151.216.142.128/26 2a02:ed02:142c::/64 e33-1
+151.216.142.192/26 2a02:ed02:142d::/64 e33-2
+151.216.143.0/26 2a02:ed02:143a::/64 e35-1
+151.216.143.64/26 2a02:ed02:143b::/64 e35-2
+151.216.143.128/26 2a02:ed02:143c::/64 e37-1
+151.216.143.192/26 2a02:ed02:143d::/64 e37-2
+151.216.144.0/26 2a02:ed02:144a::/64 e39-1
+151.216.144.64/26 2a02:ed02:144b::/64 e39-2
+151.216.144.128/26 2a02:ed02:144c::/64 e41-1
+151.216.144.192/26 2a02:ed02:144d::/64 e41-2
+151.216.145.0/26 2a02:ed02:145a::/64 e43-1
+151.216.145.64/26 2a02:ed02:145b::/64 e43-2
+151.216.145.128/26 2a02:ed02:145c::/64 e45-1
+151.216.145.192/26 2a02:ed02:145d::/64 e45-2
+151.216.146.0/26 2a02:ed02:146a::/64 e45-3
+151.216.146.64/26 2a02:ed02:146b::/64 e45-4
+151.216.146.128/26 2a02:ed02:146c::/64 e47-1
+151.216.146.192/26 2a02:ed02:146d::/64 e47-2
+151.216.147.0/26 2a02:ed02:147a::/64 e47-3
+151.216.147.64/26 2a02:ed02:147b::/64 e47-4
+151.216.147.128/26 2a02:ed02:147c::/64 e49-1
+151.216.147.192/26 2a02:ed02:147d::/64 e49-2
+151.216.148.0/26 2a02:ed02:148a::/64 e49-3
+151.216.148.64/26 2a02:ed02:148b::/64 e49-4
+151.216.148.128/26 2a02:ed02:148c::/64 e51-1
+151.216.148.192/26 2a02:ed02:148d::/64 e51-2
+151.216.149.0/26 2a02:ed02:149a::/64 e51-3
+151.216.149.64/26 2a02:ed02:149b::/64 e51-4
+151.216.149.128/26 2a02:ed02:149c::/64 e53-1
+151.216.149.192/26 2a02:ed02:149d::/64 e53-2
+151.216.150.0/26 2a02:ed02:150a::/64 e53-3
+151.216.150.64/26 2a02:ed02:150b::/64 e53-4
+151.216.150.128/26 2a02:ed02:150c::/64 e55-1
+151.216.150.192/26 2a02:ed02:150d::/64 e55-2
+151.216.151.0/26 2a02:ed02:151a::/64 e55-3
+151.216.151.64/26 2a02:ed02:151b::/64 e55-4
+151.216.151.128/26 2a02:ed02:151c::/64 e57-1
+151.216.151.192/26 2a02:ed02:151d::/64 e57-2
+151.216.152.0/26 2a02:ed02:152a::/64 e57-3
+151.216.152.64/26 2a02:ed02:152b::/64 e57-4
+151.216.152.128/26 2a02:ed02:152c::/64 e59-1
+151.216.152.192/26 2a02:ed02:152d::/64 e59-2
+151.216.153.0/26 2a02:ed02:153a::/64 e59-3
+151.216.153.64/26 2a02:ed02:153b::/64 e59-4
+151.216.153.128/26 2a02:ed02:153c::/64 e61-1
+151.216.153.192/26 2a02:ed02:153d::/64 e61-2
+151.216.154.0/26 2a02:ed02:154a::/64 e61-3
+151.216.154.64/26 2a02:ed02:154b::/64 e61-4
+151.216.154.128/26 2a02:ed02:154c::/64 e63-1
+151.216.154.192/26 2a02:ed02:154d::/64 e63-2
+151.216.155.0/26 2a02:ed02:155a::/64 e63-3
+151.216.155.64/26 2a02:ed02:155b::/64 e63-4
+151.216.155.128/26 2a02:ed02:155c::/64 e65-1
+151.216.155.192/26 2a02:ed02:155d::/64 e65-2
+151.216.156.0/26 2a02:ed02:156a::/64 e65-3
+151.216.156.64/26 2a02:ed02:156b::/64 e65-4
+151.216.156.128/26 2a02:ed02:156c::/64 e67-1
+151.216.156.192/26 2a02:ed02:156d::/64 e67-2
+151.216.157.0/26 2a02:ed02:157a::/64 e67-3
+151.216.157.64/26 2a02:ed02:157b::/64 e67-4
+151.216.157.128/26 2a02:ed02:157c::/64 e69-1
+151.216.157.192/26 2a02:ed02:157d::/64 e69-2
+151.216.158.0/26 2a02:ed02:158a::/64 e69-3
+151.216.158.64/26 2a02:ed02:158b::/64 e69-4
+151.216.158.128/26 2a02:ed02:158c::/64 e71-1
+151.216.158.192/26 2a02:ed02:158d::/64 e71-2
+151.216.159.0/26 2a02:ed02:159a::/64 e71-3
+151.216.159.64/26 2a02:ed02:159b::/64 e71-4
+151.216.159.128/26 2a02:ed02:159c::/64 e73-1
+151.216.159.192/26 2a02:ed02:159d::/64 e73-2
+151.216.160.0/26 2a02:ed02:160a::/64 e73-3
+151.216.160.64/26 2a02:ed02:160b::/64 e73-4
+151.216.160.128/26 2a02:ed02:160c::/64 e75-1
+151.216.160.192/26 2a02:ed02:160d::/64 e75-2
+151.216.161.0/26 2a02:ed02:161a::/64 e75-3
+151.216.161.64/26 2a02:ed02:161b::/64 e75-4
+151.216.161.128/26 2a02:ed02:161c::/64 e77-1
+151.216.161.192/26 2a02:ed02:161d::/64 e77-2
+151.216.162.0/26 2a02:ed02:162a::/64 e77-3
+151.216.162.64/26 2a02:ed02:162b::/64 e77-4
+151.216.162.128/26 2a02:ed02:162c::/64 e79-1
+151.216.162.192/26 2a02:ed02:162d::/64 e79-2
+151.216.163.0/26 2a02:ed02:163a::/64 e79-3
+151.216.163.64/26 2a02:ed02:163b::/64 e79-4
+151.216.163.128/26 2a02:ed02:163c::/64 e81-1
+151.216.163.192/26 2a02:ed02:163d::/64 e81-2
+151.216.164.0/26 2a02:ed02:164a::/64 e83-1
+151.216.164.64/26 2a02:ed02:164b::/64 e83-2
+## slutt switches.txt
diff --git a/examples/historical/tg15/patchlist.txt b/examples/historical/tg15/patchlist.txt
new file mode 100644
index 0000000..3f73092
--- /dev/null
+++ b/examples/historical/tg15/patchlist.txt
@@ -0,0 +1,148 @@
+e1-3 distro0 ge-0/0/0 ge-1/0/0 ge-2/0/0 ge-3/0/0
+e3-3 distro0 ge-0/0/1 ge-1/0/1 ge-2/0/1 ge-3/0/1
+e3-4 distro0 ge-0/0/2 ge-1/0/2 ge-2/0/2 ge-3/0/2
+e5-2 distro1 ge-0/0/0 ge-1/0/0 ge-2/0/0 ge-3/0/0
+e5-3 distro0 ge-0/0/3 ge-1/0/3 ge-2/0/3 ge-3/0/3
+e5-4 distro0 ge-0/0/4 ge-1/0/4 ge-2/0/4 ge-3/0/4
+e7-2 distro1 ge-0/0/1 ge-1/0/1 ge-2/0/1 ge-3/0/1
+e7-3 distro0 ge-0/0/5 ge-1/0/5 ge-2/0/5 ge-3/0/5
+e7-4 distro0 ge-0/0/6 ge-1/0/6 ge-2/0/6 ge-3/0/6
+e9-2 distro1 ge-0/0/2 ge-1/0/2 ge-2/0/2 ge-3/0/2
+e9-3 distro0 ge-0/0/7 ge-1/0/7 ge-2/0/7 ge-3/0/7
+e9-4 distro0 ge-0/0/8 ge-1/0/8 ge-2/0/8 ge-3/0/8
+e11-1 distro1 ge-0/0/3 ge-1/0/3 ge-2/0/3 ge-3/0/3
+e11-2 distro1 ge-0/0/4 ge-1/0/4 ge-2/0/4 ge-3/0/4
+e11-3 distro0 ge-0/0/9 ge-1/0/9 ge-2/0/9 ge-3/0/9
+e11-4 distro0 ge-0/0/10 ge-1/0/10 ge-2/0/10 ge-3/0/10
+e13-1 distro1 ge-0/0/5 ge-1/0/5 ge-2/0/5 ge-3/0/5
+e13-2 distro1 ge-0/0/6 ge-1/0/6 ge-2/0/6 ge-3/0/6
+e13-3 distro0 ge-0/0/11 ge-1/0/11 ge-2/0/11 ge-3/0/11
+e13-4 distro0 ge-0/0/12 ge-1/0/12 ge-2/0/12 ge-3/0/12
+e15-1 distro1 ge-0/0/7 ge-1/0/7 ge-2/0/7 ge-3/0/7
+e15-2 distro1 ge-0/0/8 ge-1/0/8 ge-2/0/8 ge-3/0/8
+e15-3 distro0 ge-0/0/13 ge-1/0/13 ge-2/0/13 ge-3/0/13
+e15-4 distro0 ge-0/0/14 ge-1/0/14 ge-2/0/14 ge-3/0/14
+e17-1 distro1 ge-0/0/9 ge-1/0/9 ge-2/0/9 ge-3/0/9
+e17-2 distro1 ge-0/0/10 ge-1/0/10 ge-2/0/10 ge-3/0/10
+e17-3 distro2 ge-0/0/0 ge-1/0/0 ge-2/0/0 ge-3/0/0
+e17-4 distro2 ge-0/0/1 ge-1/0/1 ge-2/0/1 ge-3/0/1
+e19-1 distro1 ge-0/0/11 ge-1/0/11 ge-2/0/11 ge-3/0/11
+e19-2 distro1 ge-0/0/12 ge-1/0/12 ge-2/0/12 ge-3/0/12
+e19-3 distro2 ge-0/0/2 ge-1/0/2 ge-2/0/2 ge-3/0/2
+e19-4 distro2 ge-0/0/3 ge-1/0/3 ge-2/0/3 ge-3/0/3
+e21-1 distro1 ge-0/0/13 ge-1/0/13 ge-2/0/13 ge-3/0/13
+e21-2 distro1 ge-0/0/14 ge-1/0/14 ge-2/0/14 ge-3/0/14
+e21-3 distro2 ge-0/0/4 ge-1/0/4 ge-2/0/4 ge-3/0/4
+e21-4 distro2 ge-0/0/5 ge-1/0/5 ge-2/0/5 ge-3/0/5
+e23-1 distro1 ge-0/0/15 ge-1/0/15 ge-2/0/15 ge-3/0/15
+e23-2 distro1 ge-0/0/16 ge-1/0/16 ge-2/0/16 ge-3/0/16
+e23-3 distro2 ge-0/0/6 ge-1/0/6 ge-2/0/6 ge-3/0/6
+e23-4 distro2 ge-0/0/7 ge-1/0/7 ge-2/0/7 ge-3/0/7
+e25-1 distro1 ge-0/0/17 ge-1/0/17 ge-2/0/17 ge-3/0/17
+e25-2 distro1 ge-0/0/18 ge-1/0/18 ge-2/0/18 ge-3/0/18
+e25-3 distro2 ge-0/0/8 ge-1/0/8 ge-2/0/8 ge-3/0/8
+e25-4 distro2 ge-0/0/9 ge-1/0/9 ge-2/0/9 ge-3/0/9
+e27-1 distro1 ge-0/0/19 ge-1/0/19 ge-2/0/19 ge-3/0/19
+e27-2 distro1 ge-0/0/20 ge-1/0/20 ge-2/0/20 ge-3/0/20
+e27-3 distro2 ge-0/0/10 ge-1/0/10 ge-2/0/10 ge-3/0/10
+e27-4 distro2 ge-0/0/11 ge-1/0/11 ge-2/0/11 ge-3/0/11
+e29-1 distro3 ge-0/0/0 ge-1/0/0 ge-2/0/0 ge-3/0/0
+e29-2 distro3 ge-0/0/1 ge-1/0/1 ge-2/0/1 ge-3/0/1
+e31-1 distro3 ge-0/0/2 ge-1/0/2 ge-2/0/2 ge-3/0/2
+e31-2 distro3 ge-0/0/3 ge-1/0/3 ge-2/0/3 ge-3/0/3
+e33-1 distro3 ge-0/0/4 ge-1/0/4 ge-2/0/4 ge-3/0/4
+e33-2 distro3 ge-0/0/5 ge-1/0/5 ge-2/0/5 ge-3/0/5
+e35-1 distro3 ge-0/0/6 ge-1/0/6 ge-2/0/6 ge-3/0/6
+e35-2 distro3 ge-0/0/7 ge-1/0/7 ge-2/0/7 ge-3/0/7
+e37-1 distro3 ge-0/0/8 ge-1/0/8 ge-2/0/8 ge-3/0/8
+e37-2 distro3 ge-0/0/9 ge-1/0/9 ge-2/0/9 ge-3/0/9
+e39-1 distro3 ge-0/0/10 ge-1/0/10 ge-2/0/10 ge-3/0/10
+e39-2 distro3 ge-0/0/11 ge-1/0/11 ge-2/0/11 ge-3/0/11
+e41-1 distro3 ge-0/0/12 ge-1/0/12 ge-2/0/12 ge-3/0/12
+e41-2 distro3 ge-0/0/13 ge-1/0/13 ge-2/0/13 ge-3/0/13
+e43-1 distro3 ge-0/0/14 ge-1/0/14 ge-2/0/14 ge-3/0/14
+e43-2 distro3 ge-0/0/15 ge-1/0/15 ge-2/0/15 ge-3/0/15
+e45-1 distro4 ge-0/0/0 ge-1/0/0 ge-2/0/0 ge-3/0/0
+e45-2 distro4 ge-0/0/1 ge-1/0/1 ge-2/0/1 ge-3/0/1
+e45-3 distro5 ge-0/0/0 ge-1/0/0 ge-2/0/0 ge-3/0/0
+e45-4 distro5 ge-0/0/1 ge-1/0/1 ge-2/0/1 ge-3/0/1
+e47-1 distro4 ge-0/0/2 ge-1/0/2 ge-2/0/2 ge-3/0/2
+e47-2 distro4 ge-0/0/3 ge-1/0/3 ge-2/0/3 ge-3/0/3
+e47-3 distro5 ge-0/0/2 ge-1/0/2 ge-2/0/2 ge-3/0/2
+e47-4 distro5 ge-0/0/3 ge-1/0/3 ge-2/0/3 ge-3/0/3
+e49-1 distro4 ge-0/0/4 ge-1/0/4 ge-2/0/4 ge-3/0/4
+e49-2 distro4 ge-0/0/5 ge-1/0/5 ge-2/0/5 ge-3/0/5
+e49-3 distro5 ge-0/0/4 ge-1/0/4 ge-2/0/4 ge-3/0/4
+e49-4 distro5 ge-0/0/5 ge-1/0/5 ge-2/0/5 ge-3/0/5
+e51-1 distro4 ge-0/0/6 ge-1/0/6 ge-2/0/6 ge-3/0/6
+e51-2 distro4 ge-0/0/7 ge-1/0/7 ge-2/0/7 ge-3/0/7
+e51-3 distro5 ge-0/0/6 ge-1/0/6 ge-2/0/6 ge-3/0/6
+e51-4 distro5 ge-0/0/7 ge-1/0/7 ge-2/0/7 ge-3/0/7
+e53-1 distro4 ge-0/0/8 ge-1/0/8 ge-2/0/8 ge-3/0/8
+e53-2 distro4 ge-0/0/9 ge-1/0/9 ge-2/0/9 ge-3/0/9
+e53-3 distro5 ge-0/0/8 ge-1/0/8 ge-2/0/8 ge-3/0/8
+e53-4 distro5 ge-0/0/9 ge-1/0/9 ge-2/0/9 ge-3/0/9
+e55-1 distro4 ge-0/0/10 ge-1/0/10 ge-2/0/10 ge-3/0/10
+e55-2 distro4 ge-0/0/11 ge-1/0/11 ge-2/0/11 ge-3/0/11
+e55-3 distro5 ge-0/0/10 ge-1/0/10 ge-2/0/10 ge-3/0/10
+e55-4 distro5 ge-0/0/11 ge-1/0/11 ge-2/0/11 ge-3/0/11
+e57-1 distro4 ge-0/0/12 ge-1/0/12 ge-2/0/12 ge-3/0/12
+e57-2 distro4 ge-0/0/13 ge-1/0/13 ge-2/0/13 ge-3/0/13
+e57-3 distro5 ge-0/0/12 ge-1/0/12 ge-2/0/12 ge-3/0/12
+e57-4 distro5 ge-0/0/13 ge-1/0/13 ge-2/0/13 ge-3/0/13
+e59-1 distro4 ge-0/0/14 ge-1/0/14 ge-2/0/14 ge-3/0/14
+e59-2 distro4 ge-0/0/15 ge-1/0/15 ge-2/0/15 ge-3/0/15
+e59-3 distro5 ge-0/0/14 ge-1/0/14 ge-2/0/14 ge-3/0/14
+e59-4 distro5 ge-0/0/15 ge-1/0/15 ge-2/0/15 ge-3/0/15
+e61-1 distro4 ge-0/0/16 ge-1/0/16 ge-2/0/16 ge-3/0/16
+e61-2 distro4 ge-0/0/17 ge-1/0/17 ge-2/0/17 ge-3/0/17
+e61-3 distro5 ge-0/0/16 ge-1/0/16 ge-2/0/16 ge-3/0/16
+e61-4 distro5 ge-0/0/17 ge-1/0/17 ge-2/0/17 ge-3/0/17
+e63-1 distro7 ge-0/0/0 ge-1/0/0 ge-2/0/0 ge-3/0/0
+e63-2 distro7 ge-0/0/1 ge-1/0/1 ge-2/0/1 ge-3/0/1
+e63-3 distro6 ge-0/0/0 ge-1/0/0 ge-2/0/0 ge-3/0/0
+e63-4 distro6 ge-0/0/1 ge-1/0/1 ge-2/0/1 ge-3/0/1
+e65-1 distro7 ge-0/0/2 ge-1/0/2 ge-2/0/2 ge-3/0/2
+e65-2 distro7 ge-0/0/3 ge-1/0/3 ge-2/0/3 ge-3/0/3
+e65-3 distro6 ge-0/0/2 ge-1/0/2 ge-2/0/2 ge-3/0/2
+e65-4 distro6 ge-0/0/3 ge-1/0/3 ge-2/0/3 ge-3/0/3
+e67-1 distro7 ge-0/0/4 ge-1/0/4 ge-2/0/4 ge-3/0/4
+e67-2 distro7 ge-0/0/5 ge-1/0/5 ge-2/0/5 ge-3/0/5
+e67-3 distro6 ge-0/0/4 ge-1/0/4 ge-2/0/4 ge-3/0/4
+e67-4 distro6 ge-0/0/5 ge-1/0/5 ge-2/0/5 ge-3/0/5
+e69-1 distro7 ge-0/0/6 ge-1/0/6 ge-2/0/6 ge-3/0/6
+e69-2 distro7 ge-0/0/7 ge-1/0/7 ge-2/0/7 ge-3/0/7
+e69-3 distro6 ge-0/0/6 ge-1/0/6 ge-2/0/6 ge-3/0/6
+e69-4 distro6 ge-0/0/7 ge-1/0/7 ge-2/0/7 ge-3/0/7
+e71-1 distro7 ge-0/0/8 ge-1/0/8 ge-2/0/8 ge-3/0/8
+e71-2 distro7 ge-0/0/9 ge-1/0/9 ge-2/0/9 ge-3/0/9
+e71-3 distro6 ge-0/0/8 ge-1/0/8 ge-2/0/8 ge-3/0/8
+e71-4 distro6 ge-0/0/9 ge-1/0/9 ge-2/0/9 ge-3/0/9
+e73-1 distro7 ge-0/0/10 ge-1/0/10 ge-2/0/10 ge-3/0/10
+e73-2 distro7 ge-0/0/11 ge-1/0/11 ge-2/0/11 ge-3/0/11
+e73-3 distro6 ge-0/0/10 ge-1/0/10 ge-2/0/10 ge-3/0/10
+e73-4 distro6 ge-0/0/11 ge-1/0/11 ge-2/0/11 ge-3/0/11
+e75-1 distro7 ge-0/0/12 ge-1/0/12 ge-2/0/12 ge-3/0/12
+e75-2 distro7 ge-0/0/13 ge-1/0/13 ge-2/0/13 ge-3/0/13
+e75-3 distro6 ge-0/0/12 ge-1/0/12 ge-2/0/12 ge-3/0/12
+e75-4 distro6 ge-0/0/13 ge-1/0/13 ge-2/0/13 ge-3/0/13
+e77-1 distro7 ge-0/0/14 ge-1/0/14 ge-2/0/14 ge-3/0/14
+e77-2 distro7 ge-0/0/15 ge-1/0/15 ge-2/0/15 ge-3/0/15
+e77-3 distro6 ge-0/0/14 ge-1/0/14 ge-2/0/14 ge-3/0/14
+e77-4 distro6 ge-0/0/15 ge-1/0/15 ge-2/0/15 ge-3/0/15
+e79-1 distro7 ge-0/0/16 ge-1/0/16 ge-2/0/16 ge-3/0/16
+e79-2 distro7 ge-0/0/17 ge-1/0/17 ge-2/0/17 ge-3/0/17
+e79-3 distro6 ge-0/0/16 ge-1/0/16 ge-2/0/16 ge-3/0/16
+e79-4 distro6 ge-0/0/17 ge-1/0/17 ge-2/0/17 ge-3/0/17
+e81-1 distro7 ge-0/0/18 ge-1/0/18 ge-2/0/18 ge-3/0/18
+e81-2 distro7 ge-0/0/19 ge-1/0/19 ge-2/0/19 ge-3/0/19
+e83-1 distro7 ge-0/0/20 ge-1/0/20 ge-2/0/20 ge-3/0/20
+e83-2 distro7 ge-0/0/21 ge-1/0/21 ge-2/0/21 ge-3/0/21
+sw1-creativia distro6 ge-0/0/18 ge-1/0/18
+sw2-creativia distro6 ge-0/0/19 ge-1/0/19
+sw3-creativia distro6 ge-0/0/20 ge-1/0/20
+sw4-creativia distro6 ge-0/0/21 ge-1/0/21
+sw5-creativia distro6 ge-0/0/22 ge-1/0/22
+sw6-creativia distro6 ge-0/0/23 ge-1/0/23
+sw7-creativia distro6 ge-0/0/24 ge-1/0/24
+sw8-creativia distro6 ge-0/0/25 ge-1/0/25
diff --git a/examples/historical/tg15/patchlist_extras.txt b/examples/historical/tg15/patchlist_extras.txt
new file mode 100644
index 0000000..f7b0fb2
--- /dev/null
+++ b/examples/historical/tg15/patchlist_extras.txt
@@ -0,0 +1,7 @@
+sw1-infodesk rs1.north ge-0/0/12
+sw2-gamestudio rs1.north ge-0/0/45
+sw3-streamerlounge rs1.north ge-0/0/16
+sw1-crew rs1.crew ge-0/0/39 ge-0/0/40 ge-0/0/41
+sw2-crew rs1.crew ge-0/0/42 ge-0/0/43 ge-0/0/44
+sw3-crew rs1.crew ge-0/0/45 ge-0/0/46 ge-0/0/47
+e3-2 distro1 ge-0/0/21 ge-1/0/21 ge-2/0/21
diff --git a/examples/historical/tg15/switches.txt b/examples/historical/tg15/switches.txt
new file mode 100644
index 0000000..c9ea727
--- /dev/null
+++ b/examples/historical/tg15/switches.txt
@@ -0,0 +1,148 @@
+e1-3 151.216.129.0/26 2a02:ed02:129a::/64 151.216.180.2/26 2a02:ed02:180a::2/64 1013 distro0
+e3-3 151.216.129.64/26 2a02:ed02:129b::/64 151.216.180.3/26 2a02:ed02:180a::3/64 1033 distro0
+e3-4 151.216.129.128/26 2a02:ed02:129c::/64 151.216.180.4/26 2a02:ed02:180a::4/64 1034 distro0
+e5-2 151.216.129.192/26 2a02:ed02:129d::/64 151.216.180.66/26 2a02:ed02:180b::66/64 1052 distro1
+e5-3 151.216.130.0/26 2a02:ed02:130a::/64 151.216.180.5/26 2a02:ed02:180a::5/64 1053 distro0
+e5-4 151.216.130.64/26 2a02:ed02:130b::/64 151.216.180.6/26 2a02:ed02:180a::6/64 1054 distro0
+e7-2 151.216.130.128/26 2a02:ed02:130c::/64 151.216.180.67/26 2a02:ed02:180b::67/64 1072 distro1
+e7-3 151.216.130.192/26 2a02:ed02:130d::/64 151.216.180.7/26 2a02:ed02:180a::7/64 1073 distro0
+e7-4 151.216.131.0/26 2a02:ed02:131a::/64 151.216.180.8/26 2a02:ed02:180a::8/64 1074 distro0
+e9-2 151.216.131.64/26 2a02:ed02:131b::/64 151.216.180.68/26 2a02:ed02:180b::68/64 1092 distro1
+e9-3 151.216.131.128/26 2a02:ed02:131c::/64 151.216.180.9/26 2a02:ed02:180a::9/64 1093 distro0
+e9-4 151.216.131.192/26 2a02:ed02:131d::/64 151.216.180.10/26 2a02:ed02:180a::10/64 1094 distro0
+e11-1 151.216.132.0/26 2a02:ed02:132a::/64 151.216.180.69/26 2a02:ed02:180b::69/64 1111 distro1
+e11-2 151.216.132.64/26 2a02:ed02:132b::/64 151.216.180.70/26 2a02:ed02:180b::70/64 1112 distro1
+e11-3 151.216.132.128/26 2a02:ed02:132c::/64 151.216.180.11/26 2a02:ed02:180a::11/64 1113 distro0
+e11-4 151.216.132.192/26 2a02:ed02:132d::/64 151.216.180.12/26 2a02:ed02:180a::12/64 1114 distro0
+e13-1 151.216.133.0/26 2a02:ed02:133a::/64 151.216.180.71/26 2a02:ed02:180b::71/64 1131 distro1
+e13-2 151.216.133.64/26 2a02:ed02:133b::/64 151.216.180.72/26 2a02:ed02:180b::72/64 1132 distro1
+e13-3 151.216.133.128/26 2a02:ed02:133c::/64 151.216.180.13/26 2a02:ed02:180a::13/64 1133 distro0
+e13-4 151.216.133.192/26 2a02:ed02:133d::/64 151.216.180.14/26 2a02:ed02:180a::14/64 1134 distro0
+e15-1 151.216.134.0/26 2a02:ed02:134a::/64 151.216.180.73/26 2a02:ed02:180b::73/64 1151 distro1
+e15-2 151.216.134.64/26 2a02:ed02:134b::/64 151.216.180.74/26 2a02:ed02:180b::74/64 1152 distro1
+e15-3 151.216.134.128/26 2a02:ed02:134c::/64 151.216.180.15/26 2a02:ed02:180a::15/64 1153 distro0
+e15-4 151.216.134.192/26 2a02:ed02:134d::/64 151.216.180.16/26 2a02:ed02:180a::16/64 1154 distro0
+e17-1 151.216.135.0/26 2a02:ed02:135a::/64 151.216.180.75/26 2a02:ed02:180b::75/64 1171 distro1
+e17-2 151.216.135.64/26 2a02:ed02:135b::/64 151.216.180.76/26 2a02:ed02:180b::76/64 1172 distro1
+e17-3 151.216.135.128/26 2a02:ed02:135c::/64 151.216.180.130/26 2a02:ed02:180c::130/64 1173 distro2
+e17-4 151.216.135.192/26 2a02:ed02:135d::/64 151.216.180.131/26 2a02:ed02:180c::131/64 1174 distro2
+e19-1 151.216.136.0/26 2a02:ed02:136a::/64 151.216.180.77/26 2a02:ed02:180b::77/64 1191 distro1
+e19-2 151.216.136.64/26 2a02:ed02:136b::/64 151.216.180.78/26 2a02:ed02:180b::78/64 1192 distro1
+e19-3 151.216.136.128/26 2a02:ed02:136c::/64 151.216.180.132/26 2a02:ed02:180c::132/64 1193 distro2
+e19-4 151.216.136.192/26 2a02:ed02:136d::/64 151.216.180.133/26 2a02:ed02:180c::133/64 1194 distro2
+e21-1 151.216.137.0/26 2a02:ed02:137a::/64 151.216.180.79/26 2a02:ed02:180b::79/64 1211 distro1
+e21-2 151.216.137.64/26 2a02:ed02:137b::/64 151.216.180.80/26 2a02:ed02:180b::80/64 1212 distro1
+e21-3 151.216.137.128/26 2a02:ed02:137c::/64 151.216.180.134/26 2a02:ed02:180c::134/64 1213 distro2
+e21-4 151.216.137.192/26 2a02:ed02:137d::/64 151.216.180.135/26 2a02:ed02:180c::135/64 1214 distro2
+e23-1 151.216.138.0/26 2a02:ed02:138a::/64 151.216.180.81/26 2a02:ed02:180b::81/64 1231 distro1
+e23-2 151.216.138.64/26 2a02:ed02:138b::/64 151.216.180.82/26 2a02:ed02:180b::82/64 1232 distro1
+e23-3 151.216.138.128/26 2a02:ed02:138c::/64 151.216.180.136/26 2a02:ed02:180c::136/64 1233 distro2
+e23-4 151.216.138.192/26 2a02:ed02:138d::/64 151.216.180.137/26 2a02:ed02:180c::137/64 1234 distro2
+e25-1 151.216.139.0/26 2a02:ed02:139a::/64 151.216.180.83/26 2a02:ed02:180b::83/64 1251 distro1
+e25-2 151.216.139.64/26 2a02:ed02:139b::/64 151.216.180.84/26 2a02:ed02:180b::84/64 1252 distro1
+e25-3 151.216.139.128/26 2a02:ed02:139c::/64 151.216.180.138/26 2a02:ed02:180c::138/64 1253 distro2
+e25-4 151.216.139.192/26 2a02:ed02:139d::/64 151.216.180.139/26 2a02:ed02:180c::139/64 1254 distro2
+e27-1 151.216.140.0/26 2a02:ed02:140a::/64 151.216.180.85/26 2a02:ed02:180b::85/64 1271 distro1
+e27-2 151.216.140.64/26 2a02:ed02:140b::/64 151.216.180.86/26 2a02:ed02:180b::86/64 1272 distro1
+e27-3 151.216.140.128/26 2a02:ed02:140c::/64 151.216.180.140/26 2a02:ed02:180c::140/64 1273 distro2
+e27-4 151.216.140.192/26 2a02:ed02:140d::/64 151.216.180.141/26 2a02:ed02:180c::141/64 1274 distro2
+e29-1 151.216.141.128/26 2a02:ed02:141c::/64 151.216.180.194/26 2a02:ed02:180d::194/64 1291 distro3
+e29-2 151.216.141.192/26 2a02:ed02:141d::/64 151.216.180.195/26 2a02:ed02:180d::195/64 1292 distro3
+e31-1 151.216.142.0/26 2a02:ed02:142a::/64 151.216.180.196/26 2a02:ed02:180d::196/64 1311 distro3
+e31-2 151.216.142.64/26 2a02:ed02:142b::/64 151.216.180.197/26 2a02:ed02:180d::197/64 1312 distro3
+e33-1 151.216.142.128/26 2a02:ed02:142c::/64 151.216.180.198/26 2a02:ed02:180d::198/64 1331 distro3
+e33-2 151.216.142.192/26 2a02:ed02:142d::/64 151.216.180.199/26 2a02:ed02:180d::199/64 1332 distro3
+e35-1 151.216.143.0/26 2a02:ed02:143a::/64 151.216.180.200/26 2a02:ed02:180d::200/64 1351 distro3
+e35-2 151.216.143.64/26 2a02:ed02:143b::/64 151.216.180.201/26 2a02:ed02:180d::201/64 1352 distro3
+e37-1 151.216.143.128/26 2a02:ed02:143c::/64 151.216.180.202/26 2a02:ed02:180d::202/64 1371 distro3
+e37-2 151.216.143.192/26 2a02:ed02:143d::/64 151.216.180.203/26 2a02:ed02:180d::203/64 1372 distro3
+e39-1 151.216.144.0/26 2a02:ed02:144a::/64 151.216.180.204/26 2a02:ed02:180d::204/64 1391 distro3
+e39-2 151.216.144.64/26 2a02:ed02:144b::/64 151.216.180.205/26 2a02:ed02:180d::205/64 1392 distro3
+e41-1 151.216.144.128/26 2a02:ed02:144c::/64 151.216.180.206/26 2a02:ed02:180d::206/64 1411 distro3
+e41-2 151.216.144.192/26 2a02:ed02:144d::/64 151.216.180.207/26 2a02:ed02:180d::207/64 1412 distro3
+e43-1 151.216.145.0/26 2a02:ed02:145a::/64 151.216.180.208/26 2a02:ed02:180d::208/64 1431 distro3
+e43-2 151.216.145.64/26 2a02:ed02:145b::/64 151.216.180.209/26 2a02:ed02:180d::209/64 1432 distro3
+e45-1 151.216.145.128/26 2a02:ed02:145c::/64 151.216.181.2/26 2a02:ed02:181a::2/64 1451 distro4
+e45-2 151.216.145.192/26 2a02:ed02:145d::/64 151.216.181.3/26 2a02:ed02:181a::3/64 1452 distro4
+e45-3 151.216.146.0/26 2a02:ed02:146a::/64 151.216.181.66/26 2a02:ed02:181b::66/64 1453 distro5
+e45-4 151.216.146.64/26 2a02:ed02:146b::/64 151.216.181.67/26 2a02:ed02:181b::67/64 1454 distro5
+e47-1 151.216.146.128/26 2a02:ed02:146c::/64 151.216.181.4/26 2a02:ed02:181a::4/64 1471 distro4
+e47-2 151.216.146.192/26 2a02:ed02:146d::/64 151.216.181.5/26 2a02:ed02:181a::5/64 1472 distro4
+e47-3 151.216.147.0/26 2a02:ed02:147a::/64 151.216.181.68/26 2a02:ed02:181b::68/64 1473 distro5
+e47-4 151.216.147.64/26 2a02:ed02:147b::/64 151.216.181.69/26 2a02:ed02:181b::69/64 1474 distro5
+e49-1 151.216.147.128/26 2a02:ed02:147c::/64 151.216.181.6/26 2a02:ed02:181a::6/64 1491 distro4
+e49-2 151.216.147.192/26 2a02:ed02:147d::/64 151.216.181.7/26 2a02:ed02:181a::7/64 1492 distro4
+e49-3 151.216.148.0/26 2a02:ed02:148a::/64 151.216.181.70/26 2a02:ed02:181b::70/64 1493 distro5
+e49-4 151.216.148.64/26 2a02:ed02:148b::/64 151.216.181.71/26 2a02:ed02:181b::71/64 1494 distro5
+e51-1 151.216.148.128/26 2a02:ed02:148c::/64 151.216.181.8/26 2a02:ed02:181a::8/64 1511 distro4
+e51-2 151.216.148.192/26 2a02:ed02:148d::/64 151.216.181.9/26 2a02:ed02:181a::9/64 1512 distro4
+e51-3 151.216.149.0/26 2a02:ed02:149a::/64 151.216.181.72/26 2a02:ed02:181b::72/64 1513 distro5
+e51-4 151.216.149.64/26 2a02:ed02:149b::/64 151.216.181.73/26 2a02:ed02:181b::73/64 1514 distro5
+e53-1 151.216.149.128/26 2a02:ed02:149c::/64 151.216.181.10/26 2a02:ed02:181a::10/64 1531 distro4
+e53-2 151.216.149.192/26 2a02:ed02:149d::/64 151.216.181.11/26 2a02:ed02:181a::11/64 1532 distro4
+e53-3 151.216.150.0/26 2a02:ed02:150a::/64 151.216.181.74/26 2a02:ed02:181b::74/64 1533 distro5
+e53-4 151.216.150.64/26 2a02:ed02:150b::/64 151.216.181.75/26 2a02:ed02:181b::75/64 1534 distro5
+e55-1 151.216.150.128/26 2a02:ed02:150c::/64 151.216.181.12/26 2a02:ed02:181a::12/64 1551 distro4
+e55-2 151.216.150.192/26 2a02:ed02:150d::/64 151.216.181.13/26 2a02:ed02:181a::13/64 1552 distro4
+e55-3 151.216.151.0/26 2a02:ed02:151a::/64 151.216.181.76/26 2a02:ed02:181b::76/64 1553 distro5
+e55-4 151.216.151.64/26 2a02:ed02:151b::/64 151.216.181.77/26 2a02:ed02:181b::77/64 1554 distro5
+e57-1 151.216.151.128/26 2a02:ed02:151c::/64 151.216.181.14/26 2a02:ed02:181a::14/64 1571 distro4
+e57-2 151.216.151.192/26 2a02:ed02:151d::/64 151.216.181.15/26 2a02:ed02:181a::15/64 1572 distro4
+e57-3 151.216.152.0/26 2a02:ed02:152a::/64 151.216.181.78/26 2a02:ed02:181b::78/64 1573 distro5
+e57-4 151.216.152.64/26 2a02:ed02:152b::/64 151.216.181.79/26 2a02:ed02:181b::79/64 1574 distro5
+e59-1 151.216.152.128/26 2a02:ed02:152c::/64 151.216.181.16/26 2a02:ed02:181a::16/64 1591 distro4
+e59-2 151.216.152.192/26 2a02:ed02:152d::/64 151.216.181.17/26 2a02:ed02:181a::17/64 1592 distro4
+e59-3 151.216.153.0/26 2a02:ed02:153a::/64 151.216.181.80/26 2a02:ed02:181b::80/64 1593 distro5
+e59-4 151.216.153.64/26 2a02:ed02:153b::/64 151.216.181.81/26 2a02:ed02:181b::81/64 1594 distro5
+e61-1 151.216.153.128/26 2a02:ed02:153c::/64 151.216.181.18/26 2a02:ed02:181a::18/64 1611 distro4
+e61-2 151.216.153.192/26 2a02:ed02:153d::/64 151.216.181.19/26 2a02:ed02:181a::19/64 1612 distro4
+e61-3 151.216.154.0/26 2a02:ed02:154a::/64 151.216.181.82/26 2a02:ed02:181b::82/64 1613 distro5
+e61-4 151.216.154.64/26 2a02:ed02:154b::/64 151.216.181.83/26 2a02:ed02:181b::83/64 1614 distro5
+e63-1 151.216.154.128/26 2a02:ed02:154c::/64 151.216.181.194/26 2a02:ed02:181d::194/64 1631 distro7
+e63-2 151.216.154.192/26 2a02:ed02:154d::/64 151.216.181.195/26 2a02:ed02:181d::195/64 1632 distro7
+e63-3 151.216.155.0/26 2a02:ed02:155a::/64 151.216.181.130/26 2a02:ed02:181c::130/64 1633 distro6
+e63-4 151.216.155.64/26 2a02:ed02:155b::/64 151.216.181.131/26 2a02:ed02:181c::131/64 1634 distro6
+e65-1 151.216.155.128/26 2a02:ed02:155c::/64 151.216.181.196/26 2a02:ed02:181d::196/64 1651 distro7
+e65-2 151.216.155.192/26 2a02:ed02:155d::/64 151.216.181.197/26 2a02:ed02:181d::197/64 1652 distro7
+e65-3 151.216.156.0/26 2a02:ed02:156a::/64 151.216.181.132/26 2a02:ed02:181c::132/64 1653 distro6
+e65-4 151.216.156.64/26 2a02:ed02:156b::/64 151.216.181.133/26 2a02:ed02:181c::133/64 1654 distro6
+e67-1 151.216.156.128/26 2a02:ed02:156c::/64 151.216.181.198/26 2a02:ed02:181d::198/64 1671 distro7
+e67-2 151.216.156.192/26 2a02:ed02:156d::/64 151.216.181.199/26 2a02:ed02:181d::199/64 1672 distro7
+e67-3 151.216.157.0/26 2a02:ed02:157a::/64 151.216.181.134/26 2a02:ed02:181c::134/64 1673 distro6
+e67-4 151.216.157.64/26 2a02:ed02:157b::/64 151.216.181.135/26 2a02:ed02:181c::135/64 1674 distro6
+e69-1 151.216.157.128/26 2a02:ed02:157c::/64 151.216.181.200/26 2a02:ed02:181d::200/64 1691 distro7
+e69-2 151.216.157.192/26 2a02:ed02:157d::/64 151.216.181.201/26 2a02:ed02:181d::201/64 1692 distro7
+e69-3 151.216.158.0/26 2a02:ed02:158a::/64 151.216.181.136/26 2a02:ed02:181c::136/64 1693 distro6
+e69-4 151.216.158.64/26 2a02:ed02:158b::/64 151.216.181.137/26 2a02:ed02:181c::137/64 1694 distro6
+e71-1 151.216.158.128/26 2a02:ed02:158c::/64 151.216.181.202/26 2a02:ed02:181d::202/64 1711 distro7
+e71-2 151.216.158.192/26 2a02:ed02:158d::/64 151.216.181.203/26 2a02:ed02:181d::203/64 1712 distro7
+e71-3 151.216.159.0/26 2a02:ed02:159a::/64 151.216.181.138/26 2a02:ed02:181c::138/64 1713 distro6
+e71-4 151.216.159.64/26 2a02:ed02:159b::/64 151.216.181.139/26 2a02:ed02:181c::139/64 1714 distro6
+e73-1 151.216.159.128/26 2a02:ed02:159c::/64 151.216.181.204/26 2a02:ed02:181d::204/64 1731 distro7
+e73-2 151.216.159.192/26 2a02:ed02:159d::/64 151.216.181.205/26 2a02:ed02:181d::205/64 1732 distro7
+e73-3 151.216.160.0/26 2a02:ed02:160a::/64 151.216.181.140/26 2a02:ed02:181c::140/64 1733 distro6
+e73-4 151.216.160.64/26 2a02:ed02:160b::/64 151.216.181.141/26 2a02:ed02:181c::141/64 1734 distro6
+e75-1 151.216.160.128/26 2a02:ed02:160c::/64 151.216.181.206/26 2a02:ed02:181d::206/64 1751 distro7
+e75-2 151.216.160.192/26 2a02:ed02:160d::/64 151.216.181.207/26 2a02:ed02:181d::207/64 1752 distro7
+e75-3 151.216.161.0/26 2a02:ed02:161a::/64 151.216.181.142/26 2a02:ed02:181c::142/64 1753 distro6
+e75-4 151.216.161.64/26 2a02:ed02:161b::/64 151.216.181.143/26 2a02:ed02:181c::143/64 1754 distro6
+e77-1 151.216.161.128/26 2a02:ed02:161c::/64 151.216.181.208/26 2a02:ed02:181d::208/64 1771 distro7
+e77-2 151.216.161.192/26 2a02:ed02:161d::/64 151.216.181.209/26 2a02:ed02:181d::209/64 1772 distro7
+e77-3 151.216.162.0/26 2a02:ed02:162a::/64 151.216.181.144/26 2a02:ed02:181c::144/64 1773 distro6
+e77-4 151.216.162.64/26 2a02:ed02:162b::/64 151.216.181.145/26 2a02:ed02:181c::145/64 1774 distro6
+e79-1 151.216.162.128/26 2a02:ed02:162c::/64 151.216.181.210/26 2a02:ed02:181d::210/64 1791 distro7
+e79-2 151.216.162.192/26 2a02:ed02:162d::/64 151.216.181.211/26 2a02:ed02:181d::211/64 1792 distro7
+e79-3 151.216.163.0/26 2a02:ed02:163a::/64 151.216.181.146/26 2a02:ed02:181c::146/64 1793 distro6
+e79-4 151.216.163.64/26 2a02:ed02:163b::/64 151.216.181.147/26 2a02:ed02:181c::147/64 1794 distro6
+e81-1 151.216.163.128/26 2a02:ed02:163c::/64 151.216.181.212/26 2a02:ed02:181d::212/64 1811 distro7
+e81-2 151.216.163.192/26 2a02:ed02:163d::/64 151.216.181.213/26 2a02:ed02:181d::213/64 1812 distro7
+e83-1 151.216.164.0/26 2a02:ed02:164a::/64 151.216.181.214/26 2a02:ed02:181d::214/64 1831 distro7
+e83-2 151.216.164.64/26 2a02:ed02:164b::/64 151.216.181.215/26 2a02:ed02:181d::215/64 1832 distro7
+sw1-creativia 151.216.166.0/26 2a02:ed02:166a::/64 151.216.181.148/26 2a02:ed02:181c::148/64 2001 distro6
+sw2-creativia 151.216.166.64/26 2a02:ed02:166b::/64 151.216.181.149/26 2a02:ed02:181c::149/64 2002 distro6
+sw3-creativia 151.216.166.128/26 2a02:ed02:166c::/64 151.216.181.150/26 2a02:ed02:181c::150/64 2003 distro6
+sw4-creativia 151.216.166.192/26 2a02:ed02:166d::/64 151.216.181.151/26 2a02:ed02:181c::151/64 2004 distro6
+sw5-creativia 151.216.167.0/26 2a02:ed02:167a::/64 151.216.181.152/26 2a02:ed02:181c::152/64 2005 distro6
+sw6-creativia 151.216.167.64/26 2a02:ed02:167b::/64 151.216.181.153/26 2a02:ed02:181c::153/64 2006 distro6
+sw7-creativia 151.216.167.128/26 2a02:ed02:167c::/64 151.216.181.154/26 2a02:ed02:181c::154/64 2007 distro6
+sw8-creativia 151.216.167.192/26 2a02:ed02:167d::/64 151.216.181.155/26 2a02:ed02:181c::155/64 2008 distro6
diff --git a/examples/historical/tg15/switches_extras.txt b/examples/historical/tg15/switches_extras.txt
new file mode 100644
index 0000000..afb3415
--- /dev/null
+++ b/examples/historical/tg15/switches_extras.txt
@@ -0,0 +1,7 @@
+sw1-infodesk 151.216.130.0/24 2a02:ed02:230::/64 151.216.183.229/27 2a02:ed02:1837::229/64 230 rs1.north
+sw2-gamestudio 151.216.229.0/24 2a02:ed02:229::/64 151.216.183.230/27 2a02:ed02:1837::230/64 229 rs1.north
+sw3-streamerlounge 151.216.229.0/24 2a02:ed02:229::/64 151.216.183.231/27 2a02:ed02:1837::231/64 229 rs1.north
+sw1-crew 151.216.170.64/26 2a02:ed02:170b::/64 151.216.183.66/27 2a02:ed02:1832::66/64 1701 rs1.crew
+sw2-crew 151.216.170.128/26 2a02:ed02:170c::/64 151.216.183.67/27 2a02:ed02:1832::67/64 1702 rs1.crew
+sw3-crew 151.216.170.192/26 2a02:ed02:170d::/64 151.216.183.68/27 2a02:ed02:1832::68/64 1703 rs1.crew
+e3-2 151.216.165.0/26 2a02:ed02:165a::/64 151.216.180.87/26 2a02:ed02:180b::87/64 1032 distro1
diff --git a/examples/historical/tools/fetch-portlist.sh b/examples/historical/tools/fetch-portlist.sh
new file mode 100755
index 0000000..978b590
--- /dev/null
+++ b/examples/historical/tools/fetch-portlist.sh
@@ -0,0 +1,42 @@
+print_range() {
+ FIRST=$1
+ LAST=$2
+ if [ "$1" = "$2" ]; then
+ echo $FIRST
+ else
+ echo $FIRST-$LAST
+ fi
+}
+
+walk_ports() {
+ IP=$1
+ COMMUNITY=$2
+
+ FIRST_PORT=
+ LAST_PORT=
+
+ for PORT in $( snmpwalk -Os -m IF-MIB -v 2c -c $COMMUNITY $IP ifDescr 2>/dev/null | grep -E ' ge|et|xe' | cut -d. -f2 | cut -d" " -f1 ); do
+ if ! snmpget -m IF-MIB -v 2c -c $COMMUNITY $IP ifHCInOctets.$PORT 2>/dev/null | grep -q 'No Such Instance'; then
+ if [ "$LAST_PORT" ] && [ `expr $LAST_PORT + 1` = $PORT ]; then
+ LAST_PORT=$PORT
+ else
+ if [ "$LAST_PORT" ]; then
+ print_range $FIRST_PORT $LAST_PORT
+ fi
+ FIRST_PORT=$PORT
+ LAST_PORT=$PORT
+ fi
+ fi
+ done
+
+ print_range $FIRST_PORT $LAST_PORT
+}
+
+COMMUNITY=$1
+IP=$2
+SYSNAME=$3
+PORTS=$( walk_ports $IP $COMMUNITY | tr "\n" "," | sed 's/,$//' )
+
+echo "insert into switchtypes values ('$SYSNAME','$PORTS',true);"
+echo "insert into switches values (default,'$IP','$SYSNAME','$SYSNAME',null,default, default, '1 minute', '$COMMUNITY');"
+
diff --git a/examples/historical/tools/ping-graph.pl b/examples/historical/tools/ping-graph.pl
new file mode 100755
index 0000000..2cd6996
--- /dev/null
+++ b/examples/historical/tools/ping-graph.pl
@@ -0,0 +1,62 @@
+#! /usr/bin/perl
+
+# Makes latency-against-time graphs, one per switch.
+
+use warnings;
+use strict;
+use DBI;
+use lib '../include';
+use nms;
+
+BEGIN {
+ require "../include/config.pm";
+ eval {
+ require "../include/config.local.pm";
+ };
+}
+
+my $dbh = db_connect();
+my $switches = $dbh->selectall_hashref('SELECT sysname,switch FROM switches ORDER BY sysname', 'sysname');
+if (1) {
+ my %switchfds = ();
+ while (my ($sysname, $switch) = each %$switches) {
+ print "$sysname -> $switch->{switch}\n";
+ open my $fh, ">", "$sysname.txt"
+ or die "$sysname.txt: $!";
+ $switchfds{$switch->{'switch'}} = $fh;
+ }
+
+ my $q = $dbh->prepare('SELECT switch,EXTRACT(EPOCH FROM updated),latency_ms FROM ping');
+ $q->execute;
+
+ my $i = 0;
+ while (my $ref = $q->fetchrow_arrayref) {
+ next if (!defined($ref->[2]));
+ my $fh = $switchfds{$ref->[0]};
+ next if (!defined($fh));
+ print $fh $ref->[1], " ", $ref->[2], "\n";
+ if (++$i % 1000000 == 0) {
+ printf "%dM records...\n", int($i / 1000000);
+ }
+ }
+
+ while (my ($sysname, $switch) = each %$switches) {
+ close $switchfds{$switch->{'switch'}};
+ }
+}
+
+while (my ($sysname, $switch) = each %$switches) {
+ print "$sysname -> $switch->{switch}\n";
+ open my $gnuplot, "|-", "gnuplot"
+ or die "gnuplot: $!";
+ print $gnuplot <<"EOF";
+set timefmt "%s"
+set xdata time
+set format x "%d/%m %H:%M"
+set term png size 1280,720
+set output '$sysname.png'
+set yrange [0:200]
+plot "$sysname.txt" using (int(\$1)):2 ps 0.1
+EOF
+ close $gnuplot;
+}