bgp-lastseen.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. #!/usr/bin/env python3
  2. import io
  3. import sys
  4. import json
  5. import time
  6. from netaddr import IPNetwork, IPSet
  7. from utils import read_json, write_json
  8. from registry import Inetnum, AutNum
  9. DBFILE = "/srv/http/dn42/tower-bird.json"
  10. REGISTRY = "/home/zorun/net.dn42.registry"
  11. HTMLOUT = "/srv/http/dn42/lastseen/index.html"
  12. # Where the data comes from
  13. ASN = 76142
  14. #TIMEFMT = '%F %H:%M'
  15. TIMEFMT = '%c UTC'
  16. DN42 = IPSet(["172.22.0.0/15"])
  17. def prefix_components(prefix):
  18. """Can be used as a key for sorting collections of prefixes"""
  19. ip = prefix.split("/")[0]
  20. cidr = prefix.split("/")[1]
  21. return tuple(int(part) for part in ip.split('.')) + (cidr,)
  22. class LastSeen():
  23. # Database of all previously seen prefixes, with dates
  24. prefixes_db = dict()
  25. # Prefixes currently announced
  26. current_prefixes = list()
  27. # From the registry
  28. autnums = dict()
  29. inetnums = dict()
  30. # Optimised version for inclusion testing
  31. networks = list()
  32. def __init__(self, db_filename):
  33. self.prefixes_db = read_json(db_filename)
  34. self.current_prefixes = [prefix for prefix in self.prefixes_db if self.prefixes_db[prefix]["current"]]
  35. # Registry
  36. self.inetnums = Inetnum(REGISTRY).data
  37. self.autnums = AutNum(REGISTRY).data
  38. # Precompute this
  39. self.networks = [IPNetwork(net) for net in self.inetnums]
  40. def stats(self):
  41. known = IPSet(self.prefixes_db)
  42. current = IPSet(self.current_prefixes)
  43. ratio_known = float(known.size) / float(DN42.size)
  44. ratio_current = float(current.size) / float(DN42.size)
  45. return {"known": ratio_known, "active": ratio_current}
  46. def whois(self, prefix):
  47. """Returns the name associated to a prefix, according to the registry. We
  48. look for the most specific prefix containing the argument.
  49. """
  50. prefix = IPNetwork(prefix)
  51. relevant_nets = [net for net in self.networks if prefix in net]
  52. if relevant_nets:
  53. final_net = str(max(relevant_nets, key=(lambda p: p.prefixlen)))
  54. if "netname" in self.inetnums[final_net]:
  55. netname = self.inetnums[final_net]["netname"][0]
  56. #print("{} -> {} [{}]".format(prefix, final_net, netname))
  57. return netname
  58. else:
  59. return None
  60. else:
  61. #print("No whois for {}".format(prefix))
  62. return None
  63. def as_name(self, asn):
  64. """Returns a tuple (as-name, descr), any of which can be the empty string,
  65. or None if the AS is not found in the registry.
  66. """
  67. if isinstance(asn, int):
  68. query = 'AS' + str(asn)
  69. elif isinstance(asn, str):
  70. asn = asn.upper()
  71. if not asn.startswith('AS'):
  72. query = 'AS' + asn
  73. else:
  74. return None
  75. if query in self.autnums:
  76. return (self.autnums[query].get('as-name', [""])[0], self.autnums[query].get('descr', [""])[0])
  77. def gen_html(self, out):
  78. stats = self.stats()
  79. 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>")
  80. out.write("<h1>Last seen in dn42 BGP (from AS {})</h1>".format(ASN))
  81. out.write("<p><a href='../tower-bird.json'>Raw JSON data</a>, <a href='../scripts/bgp-lastseen.py'>Python script</a></p>")
  82. out.write("<p><strong>Last update:</strong> {} (data collection started on 26th January 2014)</p>".format(time.strftime('%c UTC', time.gmtime())))
  83. out.write("<p><strong>Number of prefixes currently announced:</strong> {} (totalizing {:.2%} of dn42 address space)</p>".format(len(self.current_prefixes), stats['active']))
  84. 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']))
  85. 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))
  86. out.write("<p><table border=1>"
  87. "<tr>"
  88. "<th>Status</th>"
  89. "<th>Prefix</th>"
  90. "<th>Netname</th>"
  91. "<th>Origin</th>"
  92. "<th>Last seen</th>"
  93. # "<th>First seen</th>"
  94. "</tr>")
  95. for (prefix, data) in sorted(self.prefixes_db.items(), key=(lambda d : (d[1]["last_updated"],) + prefix_components(d[0]))):
  96. out.write("<tr>")
  97. good = data["current"]
  98. netname = self.whois(prefix)
  99. asn = data["origin_as"]
  100. as_string = 'AS' + str(asn)
  101. as_name = self.as_name(asn)
  102. if as_name:
  103. as_string += ' | ' + as_name[0]
  104. last_seen = data["last_updated"]
  105. # first_seen = time.strftime(TIMEFMT,
  106. # time.gmtime(int(data["first_seen"]))) \
  107. # if "first_seen" in data else "?"
  108. out.write("<td style='font-weight: bold' {}>{}</td>".format("class='good'" if good else "class='bad'",
  109. "UP" if good else "DOWN"))
  110. out.write("<td>{}</td>".format(prefix))
  111. out.write("<td>{}</td>".format(netname if netname else "?"))
  112. out.write("<td>{}</td>".format(as_string))
  113. out.write("<td>{}</td>".format(time.strftime(TIMEFMT, time.gmtime(int(last_seen)))))
  114. # out.write("<td>{}</td>".format(first_seen))
  115. out.write("</tr>")
  116. out.write("</table></p></html>")
  117. if __name__ == '__main__':
  118. l = LastSeen(DBFILE)
  119. buf = io.StringIO()
  120. l.gen_html(buf)
  121. with open(HTMLOUT, "w+") as htmlfile:
  122. htmlfile.write(buf.getvalue())