From 1ea13f80f45c44f8ba9258e2721c4bbe3279ae79 Mon Sep 17 00:00:00 2001 From: dsx Date: Tue, 27 Jan 2015 11:22:16 -0500 Subject: [PATCH] Added Full view for Bird --- lg.cfg | 6 ++++ lg.py | 96 ++++++++++++++++++++++++++++++++--------------------- lgproxy.cfg | 3 ++ lgproxy.py | 76 ++++++++++++++++++++++++------------------ toolbox.py | 14 +++++++- 5 files changed, 124 insertions(+), 71 deletions(-) diff --git a/lg.cfg b/lg.cfg index 7eeb60a..c017f28 100644 --- a/lg.cfg +++ b/lg.cfg @@ -37,3 +37,9 @@ BIRD_HAS_FULL_VIEW = True # Set to True if your BIRD doesn't pretent to be a router BIRD_IS_NOT_A_ROUTER = True +# Maximum number of paths to show +MAX_PATHS = 7 + +# Shorten router names (xxx-yyy-zzz.telecom.local -> xxx-yyy-zzz, provided you specify '.telecom.local' as a value) +ROUTER_NAME_REMOVE = '.telecom.local.' + diff --git a/lg.py b/lg.py index f3b9fa2..b2199e2 100644 --- a/lg.py +++ b/lg.py @@ -20,6 +20,7 @@ # ### +from collections import defaultdict from logging.handlers import TimedRotatingFileHandler from urllib import quote, unquote from urllib2 import urlopen @@ -30,12 +31,11 @@ import random import re import subprocess -from toolbox import mask_is_valid, ipv6_is_valid, ipv4_is_valid, resolve, save_cache_pickle, load_cache_pickle, unescape -# from xml.sax.saxutils import escape +from toolbox import mask_is_valid, ipv6_is_valid, ipv4_is_valid, resolve, resolve_ptr, save_cache_pickle, load_cache_pickle, unescape - -import pydot +from dns.resolver import NXDOMAIN from flask import Flask, render_template, jsonify, redirect, session, request, abort, Response, Markup +import pydot app = Flask(__name__) app.config.from_pyfile('lg.cfg') @@ -143,12 +143,7 @@ def bird_proxy(host, proto, service, query): elif not path: return False, 'Proto "%s" invalid' % proto else: - if app.config.get('BIRD_HAS_FULL_VIEW'): - url = 'http://{}:{}/{}?q={}'.format(app.config['ROUTER_IP'][host][0], port, path, quote(query)) - else: - # FIXME: debug - url = 'http://{}:{}/{}?q={}'.format(app.config['ROUTER_IP'][host][0], port, path, quote(query)) - # url = "http://%s.%s:%d/%s?q=%s" % (host, app.config["DOMAIN"], port, path, quote(query)) + url = 'http://{}:{}/{}?q={}'.format(app.config['ROUTER_IP'][host][0], port, path, quote(query)) try: f = urlopen(url) resultat = f.read() @@ -451,8 +446,7 @@ def show_bgpmap(): for host, asmaps in data.iteritems(): add_node(host, label="%s\r%s" % (host.upper(), app.config["DOMAIN"].upper()), shape="box", fillcolor="#F5A9A9") - # as_number = app.config["AS_NUMBER"].get(host, None) - as_number = app.config["AS_NUMBER"].get(host, '6453') # FIXME: debug + as_number = app.config["AS_NUMBER"].get(host, None) if as_number: node = add_node(as_number, fillcolor="#F5A9A9") edge = add_edge(as_number, nodes[host]) @@ -475,8 +469,11 @@ def show_bgpmap(): continue if not hop: - hop = True - if _as not in hosts: + if app.config.get('BIRD_HAS_FULL_VIEW', False): + hop = True + hop_label = '' + continue + elif _as not in hosts: hop_label = _as if first: hop_label = hop_label + "*" @@ -567,24 +564,9 @@ def build_as_tree_from_raw_bird_ouput(host, proto, text): def build_as_tree_from_full_view(host, proto, res): - # FIXME: debug - # {u'noc.chi1.us': [[u'chi1', u'31133', u'6697', u'193.232.248.0/22']], - # u'noc.dal1.us': [[u'dal1', u'31133', u'6697', u'193.232.248.0/22']], - # u'noc.sng2.sg': [[u'sng2', u'31133', u'6697', u'193.232.248.0/22']], - # u'noc.war1.pl': [[u'war1', u'31133', u'6697', u'193.232.248.0/22']], - # u'rc1.ash1.us': [[u'ash1', u'31133', u'6697', u'193.232.248.0/22']], - # u'rc1.atl1.us': [[u'atl1', u'31133', u'6697', u'193.232.248.0/22']], - # u'rc1.fra6.de': [[u'fra6', u'31133', u'6697', u'193.232.248.0/22']], - # u'rc1.lax1.us': [[u'lax1', u'31133', u'6697', u'193.232.248.0/22']], - # u'rc1.lon1.uk': [[u'lon1', u'31133', u'6697', u'193.232.248.0/22']], - # u'rc1.nyc1.us': [[u'nyc1', u'31133', u'6697', u'193.232.248.0/22']], - # u'rc1.par2.fr': [[u'par2', u'31133', u'6697', u'193.232.248.0/22']], - # u'rc1.sjo1.us': [[u'sjo1', u'31133', u'6697', u'193.232.248.0/22']]} - # {'ginwyntcore2': [['ginwyntcore2', '9002', '6697', '193.232.248.0/22']]} - re_chunk_start = re.compile(r'(.*)unreachable\s+\[(.*)\s+.*\s+from\s+(.*)\].*\(.*\)\s\[.*\]') - result = dict() dest_subnet = None + raw = defaultdict(dict) for line in res: line = line.strip() @@ -597,7 +579,12 @@ def build_as_tree_from_full_view(host, proto, res): router_tag = expr.group(2).strip() router_ip = expr.group(3).strip() - result[router_ip] = list() + + try: + router_ip = resolve_ptr(router_ip) + except NXDOMAIN: + # If PTR record can't be found, IP will do too + pass elif line.startswith('BGP.as_path:'): # BGP AS path @@ -608,24 +595,57 @@ def build_as_tree_from_full_view(host, proto, res): if as_num: path.append(as_num) - path.append(dest_subnet) - result[router_ip].append(path) + path_tag = '+'.join(path[1:]) + + if path_tag not in raw: + raw[path_tag] = list() + + raw[path_tag].append(dict(router_tag=router_tag, router_ip=router_ip, path=path)) elif line.startswith('BGP.community:'): # BGP community line = line.replace('BGP.community:', '') line = line.strip() + raw[path_tag][-1]['community'] = line.split(' ') elif line.startswith('BGP.cluster_list:'): # BGP cluster size line = line.replace('BGP.cluster_list:', '') line = line.strip() + raw[path_tag][-1]['cluster_size'] = len(line.split(' ')) - # Sort and filter out result - # 0. Breakdown by possible BGP pathes - # 1. Sort by BGP path length, shortest first - # 2. Sort by cluster size, smallest first - # 3. Sort by community (?) + for path_tag in raw: + raw[path_tag] = iter(raw[path_tag]) + + result = defaultdict(list) + exhausted_tags = set() + existing_paths_num = len(raw) + if len(raw) > app.config.get('MAX_PATHS', 10): + max_paths = existing_paths_num + else: + max_paths = app.config.get('MAX_PATHS', 10) + path_count = 0 + + while path_count < max_paths: + for path_tag in sorted(raw, key=lambda x: x.count('+')): + if path_tag in exhausted_tags: + continue + + try: + path = next(raw[path_tag]) + except StopIteration: + exhausted_tags.add(path_tag) + continue + + result[path['router_ip']].append(path['path']) + result[path['router_ip']][-1].append(dest_subnet) + + path_count += 1 + if path_count == max_paths: + break + + if path_count == max_paths or len(exhausted_tags) == existing_paths_num: + break return result diff --git a/lgproxy.cfg b/lgproxy.cfg index 6268964..8e84a69 100644 --- a/lgproxy.cfg +++ b/lgproxy.cfg @@ -5,3 +5,6 @@ LOG_LEVEL="WARNING" ACCESS_LIST = ["91.224.149.206", "178.33.111.110", "2a01:6600:8081:ce00::1"] IPV4_SOURCE="" IPV6_SOURCE="" +FEATURES=['traceroute', 'bird' ] +SOCKET_PATH={4: '/var/run/bird.ctl', 6: '/var/run/bird6.ctl'} + diff --git a/lgproxy.py b/lgproxy.py index 13ba667..82039c6 100644 --- a/lgproxy.py +++ b/lgproxy.py @@ -35,60 +35,69 @@ app = Flask(__name__) app.debug = app.config["DEBUG"] app.config.from_pyfile('lgproxy.cfg') -file_handler = TimedRotatingFileHandler(filename=app.config["LOG_FILE"], when="midnight") +file_handler = TimedRotatingFileHandler(filename=app.config["LOG_FILE"], when="midnight") app.logger.setLevel(getattr(logging, app.config["LOG_LEVEL"].upper())) app.logger.addHandler(file_handler) + @app.before_request def access_log_before(*args, **kwargs): - app.logger.info("[%s] request %s, %s", request.remote_addr, request.url, "|".join(["%s:%s"%(k,v) for k,v in request.headers.items()])) + app.logger.info("[%s] request %s, %s", request.remote_addr, request.url, "|".join(["%s:%s" % (k, v) for k, v in request.headers.items()])) + @app.after_request def access_log_after(response, *args, **kwargs): - app.logger.info("[%s] reponse %s, %s", request.remote_addr, request.url, response.status_code) + app.logger.info("[%s] reponse %s, %s", request.remote_addr, request.url, response.status_code) return response + def check_accesslist(): - if app.config["ACCESS_LIST"] and request.remote_addr not in app.config["ACCESS_LIST"]: + if app.config["ACCESS_LIST"] and request.remote_addr not in app.config["ACCESS_LIST"]: abort(401) + +def check_features(): + features = app.config.get('FEATURES', []) + if request.endpoint not in features: + abort(401) + + @app.route("/traceroute") @app.route("/traceroute6") def traceroute(): check_accesslist() - + if sys.platform.startswith('freebsd') or sys.platform.startswith('netbsd') or sys.platform.startswith('openbsd'): - traceroute4 = [ 'traceroute' ] - traceroute6 = [ 'traceroute6' ] - else: # For Linux - traceroute4 = [ 'traceroute', '-4' ] - traceroute6 = [ 'traceroute', '-6' ] + traceroute4 = ['traceroute'] + traceroute6 = ['traceroute6'] + else: # For Linux + traceroute4 = ['traceroute', '-4'] + traceroute6 = ['traceroute', '-6'] src = [] - if request.path == '/traceroute6': - traceroute = traceroute6 - if app.config.get("IPV6_SOURCE",""): - src = [ "-s", app.config.get("IPV6_SOURCE") ] + if request.path == '/traceroute6': + traceroute = traceroute6 + if app.config.get("IPV6_SOURCE", ""): + src = ["-s", app.config.get("IPV6_SOURCE")] - else: - traceroute = traceroute4 - if app.config.get("IPV4_SOURCE",""): - src = [ "-s", app.config.get("IPV4_SOURCE") ] + else: + traceroute = traceroute4 + if app.config.get("IPV4_SOURCE", ""): + src = ["-s", app.config.get("IPV4_SOURCE")] - query = request.args.get("q","") + query = request.args.get("q", "") query = unquote(query) if sys.platform.startswith('freebsd') or sys.platform.startswith('netbsd'): - options = [ '-a', '-q1', '-w1', '-m15' ] + options = ['-a', '-q1', '-w1', '-m15'] elif sys.platform.startswith('openbsd'): - options = [ '-A', '-q1', '-w1', '-m15' ] - else: # For Linux - options = [ '-A', '-q1', '-N32', '-w1', '-m15' ] - command = traceroute + src + options + [ query ] - result = subprocess.Popen( command , stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'ignore').replace("\n","
") - - return result + options = ['-A', '-q1', '-w1', '-m15'] + else: # For Linux + options = ['-A', '-q1', '-N32', '-w1', '-m15'] + command = traceroute + src + options + [query] + result = subprocess.Popen(command, stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'ignore').replace("\n", "
") + return result @app.route("/bird") @@ -96,18 +105,21 @@ def traceroute(): def bird(): check_accesslist() - if request.path == "/bird": b = BirdSocket(file="/var/run/bird.ctl") - elif request.path == "/bird6": b = BirdSocket(file="/var/run/bird6.ctl") - else: return "No bird socket selected" + if request.path == "/bird": + b = BirdSocket(file=app.config.get('SOCKET_PATH').get(4)) + elif request.path == "/bird6": + b = BirdSocket(file=app.config.get('SOCKET_PATH').get(6)) + else: + return "No bird socket selected" - query = request.args.get("q","") + query = request.args.get("q", "") query = unquote(query) status, result = b.cmd(query) b.close() # FIXME: use status return result - + if __name__ == "__main__": app.logger.info("lgproxy start") diff --git a/toolbox.py b/toolbox.py index 0cec263..f5cadc7 100644 --- a/toolbox.py +++ b/toolbox.py @@ -19,20 +19,32 @@ # ### -from dns import resolver +from dns import resolver, reversename import socket import pickle import xml.parsers.expat +from flask import Flask + + resolv = resolver.Resolver() resolv.timeout = 0.5 resolv.lifetime = 1 +app = Flask(__name__) +app.config.from_pyfile('lg.cfg') + def resolve(n, q): return str(resolv.query(n, q)[0]) +def resolve_ptr(ip): + ptr = str(resolve(reversename.from_address(ip), 'PTR')).lower() + ptr = ptr.replace(app.config.get('ROUTER_NAME_REMOVE', ''), '') + return ptr + + def mask_is_valid(n): if not n: return True