|
@@ -0,0 +1,140 @@
|
|
|
+#!/usr/bin/env python3
|
|
|
+
|
|
|
+import io
|
|
|
+import sys
|
|
|
+import json
|
|
|
+import time
|
|
|
+from netaddr import IPNetwork, IPSet
|
|
|
+
|
|
|
+from utils import read_json, write_json
|
|
|
+from registry import Inetnum, AutNum
|
|
|
+
|
|
|
+DBFILE = "/srv/http/dn42/tower-bird.json"
|
|
|
+REGISTRY = "/home/zorun/net.dn42.registry"
|
|
|
+HTMLOUT = "/srv/http/dn42/lastseen/index.html"
|
|
|
+
|
|
|
+# Where the data comes from
|
|
|
+ASN = 76142
|
|
|
+
|
|
|
+#TIMEFMT = '%F %H:%M'
|
|
|
+TIMEFMT = '%c UTC'
|
|
|
+
|
|
|
+DN42 = IPSet(["172.22.0.0/15"])
|
|
|
+
|
|
|
+def prefix_components(prefix):
|
|
|
+ """Can be used as a key for sorting collections of prefixes"""
|
|
|
+ ip = prefix.split("/")[0]
|
|
|
+ cidr = prefix.split("/")[1]
|
|
|
+ return tuple(int(part) for part in ip.split('.')) + (cidr,)
|
|
|
+
|
|
|
+
|
|
|
+class LastSeen():
|
|
|
+ # Database of all previously seen prefixes, with dates
|
|
|
+ prefixes_db = dict()
|
|
|
+ # Prefixes currently announced
|
|
|
+ current_prefixes = list()
|
|
|
+ # From the registry
|
|
|
+ autnums = dict()
|
|
|
+ inetnums = dict()
|
|
|
+ # Optimised version for inclusion testing
|
|
|
+ networks = list()
|
|
|
+
|
|
|
+ def __init__(self, db_filename):
|
|
|
+ self.prefixes_db = read_json(db_filename)
|
|
|
+ self.current_prefixes = [prefix for prefix in self.prefixes_db if self.prefixes_db[prefix]["current"]]
|
|
|
+ # Registry
|
|
|
+ self.inetnums = Inetnum(REGISTRY).data
|
|
|
+ self.autnums = AutNum(REGISTRY).data
|
|
|
+ # Precompute this
|
|
|
+ self.networks = [IPNetwork(net) for net in self.inetnums]
|
|
|
+
|
|
|
+ def stats(self):
|
|
|
+ known = IPSet(self.prefixes_db)
|
|
|
+ current = IPSet(self.current_prefixes)
|
|
|
+ ratio_known = float(known.size) / float(DN42.size)
|
|
|
+ ratio_current = float(current.size) / float(DN42.size)
|
|
|
+ return {"known": ratio_known, "active": ratio_current}
|
|
|
+
|
|
|
+ def whois(self, prefix):
|
|
|
+ """Returns the name associated to a prefix, according to the registry. We
|
|
|
+ look for the most specific prefix containing the argument.
|
|
|
+ """
|
|
|
+ prefix = IPNetwork(prefix)
|
|
|
+ relevant_nets = [net for net in self.networks if prefix in net]
|
|
|
+ if relevant_nets:
|
|
|
+ final_net = str(max(relevant_nets, key=(lambda p: p.prefixlen)))
|
|
|
+ if "netname" in self.inetnums[final_net]:
|
|
|
+ netname = self.inetnums[final_net]["netname"][0]
|
|
|
+ #print("{} -> {} [{}]".format(prefix, final_net, netname))
|
|
|
+ return netname
|
|
|
+ else:
|
|
|
+ return None
|
|
|
+ else:
|
|
|
+ #print("No whois for {}".format(prefix))
|
|
|
+ return None
|
|
|
+
|
|
|
+ def as_name(self, asn):
|
|
|
+ """Returns a tuple (as-name, descr), any of which can be the empty string,
|
|
|
+ or None if the AS is not found in the registry.
|
|
|
+ """
|
|
|
+ if isinstance(asn, int):
|
|
|
+ query = 'AS' + str(asn)
|
|
|
+ elif isinstance(asn, str):
|
|
|
+ asn = asn.upper()
|
|
|
+ if not asn.startswith('AS'):
|
|
|
+ query = 'AS' + asn
|
|
|
+ else:
|
|
|
+ return None
|
|
|
+
|
|
|
+ if query in self.autnums:
|
|
|
+ return (self.autnums[query].get('as-name', [""])[0], self.autnums[query].get('descr', [""])[0])
|
|
|
+
|
|
|
+
|
|
|
+ def gen_html(self, out):
|
|
|
+ stats = self.stats()
|
|
|
+ out.write("<html><head><style type='text/css'>table { text-align: center} tr td.good { background-color: #00AA00 } tr td.bad { background-color: #AA0000 }</style>")
|
|
|
+ out.write("<h1>Last seen in dn42 BGP (from AS {})</h1>".format(ASN))
|
|
|
+ out.write("<p><a href='../tower-bird.json'>Raw JSON data</a>, <a href='../scripts/bgp-lastseen.py'>Python script</a></p>")
|
|
|
+ out.write("<p><strong>Last update:</strong> {} (data collection started on 26th January 2014)</p>".format(time.strftime('%c UTC', time.gmtime())))
|
|
|
+ out.write("<p><strong>Number of prefixes currently announced:</strong> {} (totalizing {:.2%} of dn42 address space)</p>".format(len(self.current_prefixes), stats['active']))
|
|
|
+ out.write("<p><strong>Number of known prefixes since January 2014:</strong> {} (totalizing {:.2%} of dn42 address space)</p>".format(len(self.prefixes_db), stats['known']))
|
|
|
+ out.write("<p><em>Data comes from BGP (AS {}) and is sampled every 10 minutes. \"UP\" means that the prefix is currently announced in dn42. \"DOWN\" means that the prefix has been announced at some point, but not anymore</em></p>".format(ASN))
|
|
|
+ out.write("<p><table border=1>"
|
|
|
+ "<tr>"
|
|
|
+ "<th>Status</th>"
|
|
|
+ "<th>Prefix</th>"
|
|
|
+ "<th>Netname</th>"
|
|
|
+ "<th>Origin</th>"
|
|
|
+ "<th>Last seen</th>"
|
|
|
+# "<th>First seen</th>"
|
|
|
+ "</tr>")
|
|
|
+ for (prefix, data) in sorted(self.prefixes_db.items(), key=(lambda d : (d[1]["last_updated"],) + prefix_components(d[0]))):
|
|
|
+ out.write("<tr>")
|
|
|
+ good = data["current"]
|
|
|
+ netname = self.whois(prefix)
|
|
|
+ asn = data["origin_as"]
|
|
|
+ as_string = 'AS' + str(asn)
|
|
|
+ as_name = self.as_name(asn)
|
|
|
+ if as_name:
|
|
|
+ as_string += ' | ' + as_name[0]
|
|
|
+ last_seen = data["last_updated"]
|
|
|
+# first_seen = time.strftime(TIMEFMT,
|
|
|
+# time.gmtime(int(data["first_seen"]))) \
|
|
|
+# if "first_seen" in data else "?"
|
|
|
+ out.write("<td style='font-weight: bold' {}>{}</td>".format("class='good'" if good else "class='bad'",
|
|
|
+ "UP" if good else "DOWN"))
|
|
|
+ out.write("<td>{}</td>".format(prefix))
|
|
|
+ out.write("<td>{}</td>".format(netname if netname else "?"))
|
|
|
+ out.write("<td>{}</td>".format(as_string))
|
|
|
+ out.write("<td>{}</td>".format(time.strftime(TIMEFMT, time.gmtime(int(last_seen)))))
|
|
|
+# out.write("<td>{}</td>".format(first_seen))
|
|
|
+ out.write("</tr>")
|
|
|
+ out.write("</table></p></html>")
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == '__main__':
|
|
|
+ l = LastSeen(DBFILE)
|
|
|
+ buf = io.StringIO()
|
|
|
+ l.gen_html(buf)
|
|
|
+ with open(HTMLOUT, "w+") as htmlfile:
|
|
|
+ htmlfile.write(buf.getvalue())
|