Merge branch 'master' into custom-startpage

This commit is contained in:
Peter Hansen 2022-04-05 20:38:43 +02:00
commit d2653741aa
11 changed files with 313 additions and 221 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
*.pyc *.pyc
*.pyo *.pyo
lg.cfg
lgproxy.cfg

View File

@ -1,6 +1,9 @@
BIRD-LG BIRD-LG
======= =======
Overview
--------
This is a looking glass for the Internet Routing Daemon "Bird". This is a looking glass for the Internet Routing Daemon "Bird".
Software is split in two parts: Software is split in two parts:
@ -8,7 +11,8 @@ Software is split in two parts:
- lgproxy.py: - lgproxy.py:
It must be installed and started on all bird nodes. It act as a proxy to make traceroute and bird query on the node. It must be installed and started on all bird nodes. It act as a proxy to make traceroute and bird query on the node.
Access restriction to this web service can be done in file "lgproxy.cfg" (only IP address based restriction for now). Access restriction to this web service can be done in file "lgproxy.cfg". Two access restriction methods can be configured:
based on source IP address or based on a shared secret. Both methods can be used at the same time.
- lg.py: - lg.py:
@ -33,17 +37,42 @@ Software is split in two parts:
``` ```
bird-lg depends on : Installation
------------
The web service (lg.py) depends on:
- python-flask >= 0.8 - python-flask >= 0.8
- python-dnspython - python-dnspython
- python-pydot - python-pydot
- python-memcache
- graphviz - graphviz
- whois - whois
- traceroute
Each services can be embedded in any webserver by following regular python-flask configuration. The proxy running on routers (lgproxy.py) depends on:
- python-flask >= 0.8
- traceroute
- ping
Each service can be embedded in any webserver by following regular python-flask configuration.
It is also possible to run the services directly with python for developping / testing:
python2 lg.py
python2 lgproxy.py
Systemd unit files are provided in the `init/` subdirectory.
Configuration
-------------
On your routers, copy `lgproxy.cfg.example` to `lgproxy.cfg` and edit the values.
On the web host, copy `lg.cfg.example` to `lg.cfg` and edit the values.
License
-------
Source code is under GPL 3.0, powered by Flask, jQuery and Bootstrap. Source code is under GPL 3.0, powered by Flask, jQuery and Bootstrap.
@ -67,7 +96,8 @@ Happy users
* https://lg.man-da.de/ * https://lg.man-da.de/
* http://route-server.belwue.net/ * http://route-server.belwue.net/
* https://lg.exn.uk/ * https://lg.exn.uk/
* http://lg.meerfarbig.net/ * https://meerblick.io/
* https://lg.as49697.net/
* http://lg.netnation.com/ * http://lg.netnation.com/
* http://lg.edxnetwork.eu/ * http://lg.edxnetwork.eu/
* https://lg.hivane.net/ * https://lg.hivane.net/
@ -83,4 +113,4 @@ Happy users
* https://lg.fullsave.net/ * https://lg.fullsave.net/
* http://lg.catnix.net/ * http://lg.catnix.net/
* https://lg.worldstream.nl/ * https://lg.worldstream.nl/
* https://route-server.netshelter.de/ * https://lg.angolacables.co.ao/

241
bird.py
View File

@ -25,153 +25,150 @@ import sys
BUFSIZE = 4096 BUFSIZE = 4096
SUCCESS_CODES = { SUCCESS_CODES = {
"0000" : "OK", "0000" : "OK",
"0001" : "Welcome", "0001" : "Welcome",
"0002" : "Reading configuration", "0002" : "Reading configuration",
"0003" : "Reconfigured", "0003" : "Reconfigured",
"0004" : "Reconfiguration in progress", "0004" : "Reconfiguration in progress",
"0005" : "Reconfiguration already in progress, queueing", "0005" : "Reconfiguration already in progress, queueing",
"0006" : "Reconfiguration ignored, shutting down", "0006" : "Reconfiguration ignored, shutting down",
"0007" : "Shutdown ordered", "0007" : "Shutdown ordered",
"0008" : "Already disabled", "0008" : "Already disabled",
"0009" : "Disabled", "0009" : "Disabled",
"0010" : "Already enabled", "0010" : "Already enabled",
"0011" : "Enabled", "0011" : "Enabled",
"0012" : "Restarted", "0012" : "Restarted",
"0013" : "Status report", "0013" : "Status report",
"0014" : "Route count", "0014" : "Route count",
"0015" : "Reloading", "0015" : "Reloading",
"0016" : "Access restricted", "0016" : "Access restricted",
} }
TABLES_ENTRY_CODES = { TABLES_ENTRY_CODES = {
"1000" : "BIRD version", "1000" : "BIRD version",
"1001" : "Interface list", "1001" : "Interface list",
"1002" : "Protocol list", "1002" : "Protocol list",
"1003" : "Interface address", "1003" : "Interface address",
"1004" : "Interface flags", "1004" : "Interface flags",
"1005" : "Interface summary", "1005" : "Interface summary",
"1006" : "Protocol details", "1006" : "Protocol details",
"1007" : "Route list", "1007" : "Route list",
"1008" : "Route details", "1008" : "Route details",
"1009" : "Static route list", "1009" : "Static route list",
"1010" : "Symbol list", "1010" : "Symbol list",
"1011" : "Uptime", "1011" : "Uptime",
"1012" : "Route extended attribute list", "1012" : "Route extended attribute list",
"1013" : "Show ospf neighbors", "1013" : "Show ospf neighbors",
"1014" : "Show ospf", "1014" : "Show ospf",
"1015" : "Show ospf interface", "1015" : "Show ospf interface",
"1016" : "Show ospf state/topology", "1016" : "Show ospf state/topology",
"1017" : "Show ospf lsadb", "1017" : "Show ospf lsadb",
"1018" : "Show memory", "1018" : "Show memory",
} }
ERROR_CODES = { ERROR_CODES = {
"8000" : "Reply too long", "8000" : "Reply too long",
"8001" : "Route not found", "8001" : "Route not found",
"8002" : "Configuration file error", "8002" : "Configuration file error",
"8003" : "No protocols match", "8003" : "No protocols match",
"8004" : "Stopped due to reconfiguration", "8004" : "Stopped due to reconfiguration",
"8005" : "Protocol is down => cannot dump", "8005" : "Protocol is down => cannot dump",
"8006" : "Reload failed", "8006" : "Reload failed",
"8007" : "Access denied", "8007" : "Access denied",
"9000" : "Command too long", "9000" : "Command too long",
"9001" : "Parse error", "9001" : "Parse error",
"9002" : "Invalid symbol type", "9002" : "Invalid symbol type",
} }
END_CODES = ERROR_CODES.keys() + SUCCESS_CODES.keys() END_CODES = ERROR_CODES.keys() + SUCCESS_CODES.keys()
global bird_sockets global bird_sockets
bird_sockets = {} bird_sockets = {}
def BirdSocketSingleton(host, port): def BirdSocketSingleton(host, port):
global bird_sockets global bird_sockets
s = bird_sockets.get((host,port), None) s = bird_sockets.get((host,port), None)
if not s: if not s:
s = BirdSocket(host,port) s = BirdSocket(host,port)
bird_sockets[(host,port)] = s bird_sockets[(host,port)] = s
return s return s
class BirdSocket: class BirdSocket:
def __init__(self, host="", port="", file=""): def __init__(self, host="", port="", file=""):
self.__file = file self.__file = file
self.__host = host self.__host = host
self.__port = port self.__port = port
self.__sock = None self.__sock = None
def __connect(self): def __connect(self):
if self.__sock: return if self.__sock: return
if not file: if not file:
self.__sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.__sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.__sock.settimeout(3.0) self.__sock.settimeout(3.0)
self.__sock.connect((self.__host, self.__port)) self.__sock.connect((self.__host, self.__port))
else: else:
self.__sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.__sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.__sock.settimeout(3.0) self.__sock.settimeout(3.0)
self.__sock.connect(self.__file) self.__sock.connect(self.__file)
# read welcome message # read welcome message
self.__sock.recv(1024) self.__sock.recv(1024)
self.cmd("restrict") self.cmd("restrict")
def close(self): def close(self):
if self.__sock: if self.__sock:
try: self.__sock.close() try: self.__sock.close()
except: pass except: pass
self.__sock = None self.__sock = None
def cmd(self, cmd):
try:
self.__connect()
self.__sock.send(cmd + "\n")
data = self.__read()
return data
except socket.error:
why = sys.exc_info()[1]
self.close()
return False, "Bird connection problem: %s" % why
def __read(self): def cmd(self, cmd):
code = "7000" # Not used in bird try:
parsed_string = "" self.__connect()
lastline = "" self.__sock.send(cmd + "\n")
data = self.__read()
return data
except socket.error:
why = sys.exc_info()[1]
self.close()
return False, "Bird connection problem: %s" % why
while code not in END_CODES: def __read(self):
data = self.__sock.recv(BUFSIZE) code = "7000" # Not used in bird
parsed_string = ""
lines = (lastline + data).split("\n") lastline = ""
if len(data) == BUFSIZE:
lastline = lines[-1]
lines = lines[:-1]
for line in lines: while code not in END_CODES:
code = line[0:4] data = self.__sock.recv(BUFSIZE)
if not line.strip(): lines = (lastline + data).split("\n")
continue if len(data) == BUFSIZE:
elif code == "0000": lastline = lines[-1]
return True, parsed_string lines = lines[:-1]
elif code in SUCCESS_CODES.keys():
return True, SUCCESS_CODES.get(code)
elif code in ERROR_CODES.keys():
return False, ERROR_CODES.get(code)
elif code[0] in [ "1", "2"] :
parsed_string += line[5:] + "\n"
elif code[0] == " ":
parsed_string += line[1:] + "\n"
elif code[0] == "+":
parsed_string += line[1:]
else:
parsed_string += "<<<unparsable_string(%s)>>>\n"%line
return True, parsed_string for line in lines:
code = line[0:4]
__all__ = ['BirdSocketSingleton' , 'BirdSocket' ] if not line.strip():
continue
elif code == "0000":
return True, parsed_string
elif code in SUCCESS_CODES.keys():
return True, SUCCESS_CODES.get(code)
elif code in ERROR_CODES.keys():
return False, ERROR_CODES.get(code)
elif code[0] in [ "1", "2"] :
parsed_string += line[5:] + "\n"
elif code[0] == " ":
parsed_string += line[1:] + "\n"
elif code[0] == "+":
parsed_string += line[1:]
else:
parsed_string += "<<<unparsable_string(%s)>>>\n"%line
return True, parsed_string
__all__ = ['BirdSocketSingleton', 'BirdSocket']

View File

@ -1,10 +1,19 @@
# Configuration file example for lg.py
# Adapt and copy to lg.cfg
WEBSITE_TITLE="Bird-LG / Looking Glass"
DEBUG = False DEBUG = False
LOG_FILE="/var/log/lg.log" LOG_FILE="/var/log/lg.log"
LOG_LEVEL="WARNING" LOG_LEVEL="WARNING"
# Keep log history indefinitely by default.
LOG_NUM_DAYS=0
DOMAIN = "tetaneutral.net" DOMAIN = "tetaneutral.net"
# Used to optionally restrict access to lgproxy based on a shared secret.
# Empty string or unset = no shared secret is used to run queries on lgproxies.
SHARED_SECRET="ThisTokenIsNotSecret"
BIND_IP = "0.0.0.0" BIND_IP = "0.0.0.0"
BIND_PORT = 5000 BIND_PORT = 5000
@ -29,6 +38,7 @@ AS_NUMBER = {
# DNS zone to query for ASN -> name mapping # DNS zone to query for ASN -> name mapping
ASN_ZONE = "asn.cymru.com" ASN_ZONE = "asn.cymru.com"
# Used for secure session storage, change this
SESSION_KEY = '\xd77\xf9\xfa\xc2\xb5\xcd\x85)`+H\x9d\xeeW\\%\xbe/\xbaT\x89\xe8\xa7' SESSION_KEY = '\xd77\xf9\xfa\xc2\xb5\xcd\x85)`+H\x9d\xeeW\\%\xbe/\xbaT\x89\xe8\xa7'
# specifies an alternative start page template for the "/" route. # specifies an alternative start page template for the "/" route.

105
lg.py
View File

@ -22,7 +22,6 @@
import base64 import base64
from datetime import datetime from datetime import datetime
import memcache
import subprocess import subprocess
import logging import logging
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
@ -43,18 +42,16 @@ parser = argparse.ArgumentParser()
parser.add_argument('-c', dest='config_file', help='path to config file', default='lg.cfg') parser.add_argument('-c', dest='config_file', help='path to config file', default='lg.cfg')
args = parser.parse_args() args = parser.parse_args()
app = Flask(__name__) app = Flask(__name__)
app.config.from_pyfile(args.config_file) app.config.from_pyfile(args.config_file)
app.secret_key = app.config["SESSION_KEY"] app.secret_key = app.config["SESSION_KEY"]
app.debug = app.config["DEBUG"] app.debug = app.config["DEBUG"]
file_handler = TimedRotatingFileHandler(filename=app.config["LOG_FILE"], when="midnight") file_handler = TimedRotatingFileHandler(filename=app.config["LOG_FILE"], when="midnight", backupCount=app.config.get("LOG_NUM_DAYS", 0))
file_handler.setLevel(getattr(logging, app.config["LOG_LEVEL"].upper())) file_handler.setLevel(getattr(logging, app.config["LOG_LEVEL"].upper()))
app.logger.addHandler(file_handler) app.logger.addHandler(file_handler)
memcache_server = app.config.get("MEMCACHE_SERVER", "127.0.0.1:11211")
memcache_expiration = int(app.config.get("MEMCACHE_EXPIRATION", "1296000")) # 15 days by default
mc = memcache.Client([memcache_server])
def get_asn_from_as(n): def get_asn_from_as(n):
asn_zone = app.config.get("ASN_ZONE", "asn.cymru.com") asn_zone = app.config.get("ASN_ZONE", "asn.cymru.com")
@ -148,16 +145,25 @@ def bird_proxy(host, proto, service, query):
return False, 'Host "%s" invalid' % host return False, 'Host "%s" invalid' % host
elif not path: elif not path:
return False, 'Proto "%s" invalid' % proto return False, 'Proto "%s" invalid' % proto
else:
url = "http://%s.%s:%d/%s?q=%s" % (host, app.config["DOMAIN"], port, path, quote(query)) url = "http://%s" % (host)
try: if "DOMAIN" in app.config:
f = urlopen(url) url = "%s.%s" % (url, app.config["DOMAIN"])
resultat = f.read() url = "%s:%d/%s?" % (url, port, path)
status = True # retreive remote status if "SHARED_SECRET" in app.config:
except IOError: url = "%ssecret=%s&" % (url, app.config["SHARED_SECRET"])
resultat = "Failed retreive url: %s" % url url = "%sq=%s" % (url, quote(query))
status = False
return status, resultat try:
f = urlopen(url)
resultat = f.read()
status = True # retreive remote status
except IOError:
resultat = "Failed to retrieve URL for host %s" % host
app.logger.warning("Failed to retrieve URL for host %s: %s", host, url)
status = False
return status, resultat
@app.context_processor @app.context_processor
@ -175,6 +181,8 @@ def inject_commands():
("adv", "show route ..."), ("adv", "show route ..."),
("adv_bgpmap", "show route ... (bgpmap)"), ("adv_bgpmap", "show route ... (bgpmap)"),
] ]
commands = [i for i in commands if i[0] not in app.config.get("BLACKLIST_COMMANDS", [])]
commands_dict = {} commands_dict = {}
for id, text in commands: for id, text in commands:
commands_dict[id] = text commands_dict[id] = text
@ -242,6 +250,8 @@ SUMMARY_UNWANTED_PROTOS = ["Kernel", "Static", "Device", "Direct"]
@app.route("/summary/<hosts>") @app.route("/summary/<hosts>")
@app.route("/summary/<hosts>/<proto>") @app.route("/summary/<hosts>/<proto>")
def summary(hosts, proto="ipv4"): def summary(hosts, proto="ipv4"):
if 'summary' not in iter(inject_commands()['commands_dict']):
return render_template('error.html', errors=["Access denied"]), 403
set_session("summary", hosts, proto, "") set_session("summary", hosts, proto, "")
command = "show protocols" command = "show protocols"
@ -284,6 +294,9 @@ def summary(hosts, proto="ipv4"):
@app.route("/detail/<hosts>/<proto>") @app.route("/detail/<hosts>/<proto>")
def detail(hosts, proto): def detail(hosts, proto):
if 'detail' not in iter(inject_commands()['commands_dict']):
return render_template('error.html', errors=["Access denied"]), 403
name = get_query() name = get_query()
if not name: if not name:
@ -313,6 +326,9 @@ def detail(hosts, proto):
@app.route("/traceroute/<hosts>/<proto>") @app.route("/traceroute/<hosts>/<proto>")
def traceroute(hosts, proto): def traceroute(hosts, proto):
if 'traceroute' not in iter(inject_commands()['commands_dict']):
return render_template('error.html', errors=["Access denied"]), 403
q = get_query() q = get_query()
if not q: if not q:
@ -346,49 +362,70 @@ def traceroute(hosts, proto):
@app.route("/adv/<hosts>/<proto>") @app.route("/adv/<hosts>/<proto>")
def show_route_filter(hosts, proto): def show_route_filter(hosts, proto):
if 'adv' not in iter(inject_commands()['commands_dict']):
return render_template('error.html', errors=["Access denied"]), 403
return show_route("adv", hosts, proto) return show_route("adv", hosts, proto)
@app.route("/adv_bgpmap/<hosts>/<proto>") @app.route("/adv_bgpmap/<hosts>/<proto>")
def show_route_filter_bgpmap(hosts, proto): def show_route_filter_bgpmap(hosts, proto):
if 'adv_bgpmap' not in iter(inject_commands()['commands_dict']):
return render_template('error.html', errors=["Access denied"]), 403
return show_route("adv_bgpmap", hosts, proto) return show_route("adv_bgpmap", hosts, proto)
@app.route("/where/<hosts>/<proto>") @app.route("/where/<hosts>/<proto>")
def show_route_where(hosts, proto): def show_route_where(hosts, proto):
if 'where' not in iter(inject_commands()['commands_dict']):
return render_template('error.html', errors=["Access denied"]), 403
return show_route("where", hosts, proto) return show_route("where", hosts, proto)
@app.route("/where_detail/<hosts>/<proto>") @app.route("/where_detail/<hosts>/<proto>")
def show_route_where_detail(hosts, proto): def show_route_where_detail(hosts, proto):
if 'where_detail' not in iter(inject_commands()['commands_dict']):
return render_template('error.html', errors=["Access denied"]), 403
return show_route("where_detail", hosts, proto) return show_route("where_detail", hosts, proto)
@app.route("/where_bgpmap/<hosts>/<proto>") @app.route("/where_bgpmap/<hosts>/<proto>")
def show_route_where_bgpmap(hosts, proto): def show_route_where_bgpmap(hosts, proto):
if 'where_bgpmap' not in iter(inject_commands()['commands_dict']):
return render_template('error.html', errors=["Access denied"]), 403
return show_route("where_bgpmap", hosts, proto) return show_route("where_bgpmap", hosts, proto)
@app.route("/prefix/<hosts>/<proto>") @app.route("/prefix/<hosts>/<proto>")
def show_route_for(hosts, proto): def show_route_for(hosts, proto):
if 'prefix' not in iter(inject_commands()['commands_dict']):
return render_template('error.html', errors=["Access denied"]), 403
return show_route("prefix", hosts, proto) return show_route("prefix", hosts, proto)
@app.route("/prefix_detail/<hosts>/<proto>") @app.route("/prefix_detail/<hosts>/<proto>")
def show_route_for_detail(hosts, proto): def show_route_for_detail(hosts, proto):
if 'prefix_detail' not in iter(inject_commands()['commands_dict']):
return render_template('error.html', errors=["Access denied"]), 403
return show_route("prefix_detail", hosts, proto) return show_route("prefix_detail", hosts, proto)
@app.route("/prefix_bgpmap/<hosts>/<proto>") @app.route("/prefix_bgpmap/<hosts>/<proto>")
def show_route_for_bgpmap(hosts, proto): def show_route_for_bgpmap(hosts, proto):
if 'prefix_bgpmap' not in iter(inject_commands()['commands_dict']):
return render_template('error.html', errors=["Access denied"]), 403
return show_route("prefix_bgpmap", hosts, proto) return show_route("prefix_bgpmap", hosts, proto)
def get_as_name(_as): def get_as_name(_as):
"""return a string that contain the as number following by the as name """Returns a string that contain the as number following by the as name
It's the use whois database informations
# Warning, the server can be blacklisted from ripe is too many requests are done
""" """
if not _as: if not _as:
return "AS?????" return "AS?????"
@ -396,12 +433,7 @@ def get_as_name(_as):
if not _as.isdigit(): if not _as.isdigit():
return _as.strip() return _as.strip()
name = mc.get(str('lg_%s' % _as)) name = get_asn_from_as(_as)[-1].replace(" ", "\r", 1)
if not name:
app.logger.info("asn for as %s not found in memcache", _as)
name = get_asn_from_as(_as)[-1].replace(" ","\r",1)
if name:
mc.set(str("lg_%s" % _as), str(name), memcache_expiration)
return "AS%s | %s" % (_as, name) return "AS%s | %s" % (_as, name)
@ -458,19 +490,21 @@ def show_bgpmap():
label_without_star = kwargs["label"].replace("*", "") label_without_star = kwargs["label"].replace("*", "")
if e.get_label() is not None: if e.get_label() is not None:
labels = e.get_label().split("\r") labels = e.get_label().split("\r")
else: else:
return edges[edge_tuple] return edges[edge_tuple]
if "%s*" % label_without_star not in labels: if "%s*" % label_without_star not in labels:
labels = [ kwargs["label"] ] + [ l for l in labels if not l.startswith(label_without_star) ] labels = [ kwargs["label"] ] + [ l for l in labels if not l.startswith(label_without_star) ]
labels = sorted(labels, cmp=lambda x,y: x.endswith("*") and -1 or 1) labels = sorted(labels, cmp=lambda x,y: x.endswith("*") and -1 or 1)
label = escape("\r".join(labels)) label = escape("\r".join(labels))
e.set_label(label) e.set_label(label)
return edges[edge_tuple] return edges[edge_tuple]
for host, asmaps in data.iteritems(): for host, asmaps in data.iteritems():
add_node(host, label= "%s\r%s" % (host.upper(), app.config["DOMAIN"].upper()), shape="box", fillcolor="#F5A9A9") if "DOMAIN" in app.config:
add_node(host, label= "%s\r%s" % (host.upper(), app.config["DOMAIN"].upper()), shape="box", fillcolor="#F5A9A9")
else:
add_node(host, label= "%s" % (host.upper()), shape="box", fillcolor="#F5A9A9")
as_number = app.config["AS_NUMBER"].get(host, None) as_number = app.config["AS_NUMBER"].get(host, None)
if as_number: if as_number:
@ -478,7 +512,7 @@ def show_bgpmap():
edge = add_edge(as_number, nodes[host]) edge = add_edge(as_number, nodes[host])
edge.set_color("red") edge.set_color("red")
edge.set_style("bold") edge.set_style("bold")
#colors = [ "#009e23", "#1a6ec1" , "#d05701", "#6f879f", "#939a0e", "#0e9a93", "#9a0e85", "#56d8e1" ] #colors = [ "#009e23", "#1a6ec1" , "#d05701", "#6f879f", "#939a0e", "#0e9a93", "#9a0e85", "#56d8e1" ]
previous_as = None previous_as = None
hosts = data.keys() hosts = data.keys()
@ -504,14 +538,13 @@ def show_bgpmap():
if not hop: if not hop:
hop = True hop = True
if _as not in hosts: if _as not in hosts:
hop_label = _as hop_label = _as
if first: if first:
hop_label = hop_label + "*" hop_label = hop_label + "*"
continue continue
else: else:
hop_label = "" hop_label = ""
if _as == asmap[-1]: if _as == asmap[-1]:
add_node(_as, fillcolor="#F5A9A9", shape="box", ) add_node(_as, fillcolor="#F5A9A9", shape="box", )
else: else:
@ -559,7 +592,7 @@ def build_as_tree_from_raw_bird_ouput(host, proto, text):
path = None path = None
paths = [] paths = []
net_dest = None net_dest = None
peer_protocol_name = None peer_protocol_name = ""
for line in text: for line in text:
line = line.strip() line = line.strip()
@ -569,7 +602,7 @@ def build_as_tree_from_raw_bird_ouput(host, proto, text):
net_dest = expr.group(1).strip() net_dest = expr.group(1).strip()
peer_protocol_name = expr.group(2).strip() peer_protocol_name = expr.group(2).strip()
expr2 = re.search(r'(.*)via\s+([0-9a-fA-F:\.]+)\s+on\s+\w+(\s+\[(\w+)\s+)?', line) expr2 = re.search(r'(.*)via\s+([0-9a-fA-F:\.]+)\s+on\s+\S+(\s+\[(\w+)\s+)?', line)
if expr2: if expr2:
if path: if path:
path.append(net_dest) path.append(net_dest)
@ -592,7 +625,7 @@ def build_as_tree_from_raw_bird_ouput(host, proto, text):
# ugly hack for good printing # ugly hack for good printing
path = [ peer_protocol_name ] path = [ peer_protocol_name ]
# path = ["%s\r%s" % (peer_protocol_name, get_as_name(get_as_number_from_protocol_name(host, proto, peer_protocol_name)))] # path = ["%s\r%s" % (peer_protocol_name, get_as_name(get_as_number_from_protocol_name(host, proto, peer_protocol_name)))]
expr3 = re.search(r'(.*)unreachable\s+\[(\w+)\s+', line) expr3 = re.search(r'(.*)unreachable\s+\[(\w+)\s+', line)
if expr3: if expr3:
if path: if path:
@ -612,7 +645,7 @@ def build_as_tree_from_raw_bird_ouput(host, proto, text):
path.extend(ASes) path.extend(ASes)
else: else:
path = ASes path = ASes
if path: if path:
path.append(net_dest) path.append(net_dest)
paths.append(path) paths.append(path)

View File

@ -1,12 +0,0 @@
DEBUG=False
LOG_FILE="/var/log/lg-proxy/lg-proxy.log"
LOG_LEVEL="WARNING"
BIND_IP = "0.0.0.0"
BIND_PORT = 5000
ACCESS_LIST = ["91.224.149.206", "178.33.111.110", "2a01:6600:8081:ce00::1"]
IPV4_SOURCE=""
IPV6_SOURCE=""
BIRD_SOCKET="/var/run/bird/bird.ctl"
BIRD6_SOCKET="/var/run/bird/bird6.ctl"

28
lgproxy.cfg.example Normal file
View File

@ -0,0 +1,28 @@
# Configuration file example for lgproxy.py
# Adapt and copy to lgproxy.cfg
DEBUG=False
LOG_FILE="/var/log/lg-proxy/lg-proxy.log"
LOG_LEVEL="WARNING"
# Keep log history indefinitely by default.
LOG_NUM_DAYS=0
BIND_IP = "0.0.0.0"
BIND_PORT = 5000
# Used to restrict access to lgproxy based on source IP address.
# Empty list = any IP is allowed to run queries.
ACCESS_LIST = ["91.224.149.206", "178.33.111.110", "2a01:6600:8081:ce00::1"]
# Used to restrict access to lgproxy based on a shared secret (must also be configured in lg.cfg)
# Empty string or unset = no shared secret is required to run queries.
SHARED_SECRET="ThisTokenIsNotSecret"
# Used as source address when running traceroute (optional)
IPV4_SOURCE="198.51.100.42"
IPV6_SOURCE="2001:db8:42::1"
BIRD_SOCKET="/var/run/bird/bird.ctl"
BIRD6_SOCKET="/var/run/bird/bird6.ctl"

View File

@ -41,7 +41,7 @@ app = Flask(__name__)
app.debug = app.config["DEBUG"] app.debug = app.config["DEBUG"]
app.config.from_pyfile(args.config_file) app.config.from_pyfile(args.config_file)
file_handler = TimedRotatingFileHandler(filename=app.config["LOG_FILE"], when="midnight") file_handler = TimedRotatingFileHandler(filename=app.config["LOG_FILE"], when="midnight", backupCount=app.config.get("LOG_NUM_DAYS", 0))
app.logger.setLevel(getattr(logging, app.config["LOG_LEVEL"].upper())) app.logger.setLevel(getattr(logging, app.config["LOG_LEVEL"].upper()))
app.logger.addHandler(file_handler) app.logger.addHandler(file_handler)
@ -54,15 +54,20 @@ 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 return response
def check_accesslist(): def check_security():
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"]:
app.logger.info("Your remote address is not valid")
abort(401)
if app.config.get('SHARED_SECRET') and request.args.get("secret") != app.config["SHARED_SECRET"]:
app.logger.info("Your shared secret is not valid")
abort(401) abort(401)
@app.route("/traceroute") @app.route("/traceroute")
@app.route("/traceroute6") @app.route("/traceroute6")
def traceroute(): def traceroute():
check_accesslist() check_security()
if sys.platform.startswith('freebsd') or sys.platform.startswith('netbsd') or sys.platform.startswith('openbsd'): if sys.platform.startswith('freebsd') or sys.platform.startswith('netbsd') or sys.platform.startswith('openbsd'):
traceroute4 = [ 'traceroute' ] traceroute4 = [ 'traceroute' ]
traceroute6 = [ 'traceroute6' ] traceroute6 = [ 'traceroute6' ]
@ -71,15 +76,14 @@ def traceroute():
traceroute6 = [ 'traceroute', '-6' ] traceroute6 = [ 'traceroute', '-6' ]
src = [] src = []
if request.path == '/traceroute6': if request.path == '/traceroute6':
traceroute = traceroute6 traceroute = traceroute6
if app.config.get("IPV6_SOURCE",""): if app.config.get("IPV6_SOURCE", ""):
src = [ "-s", app.config.get("IPV6_SOURCE") ] src = [ "-s", app.config.get("IPV6_SOURCE") ]
else: else:
traceroute = traceroute4 traceroute = traceroute4
if app.config.get("IPV4_SOURCE",""): if app.config.get("IPV4_SOURCE",""):
src = [ "-s", 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) query = unquote(query)
@ -92,15 +96,13 @@ def traceroute():
options = [ '-A', '-q1', '-N32', '-w1', '-m15' ] options = [ '-A', '-q1', '-N32', '-w1', '-m15' ]
command = traceroute + src + options + [ query ] command = traceroute + src + options + [ query ]
result = subprocess.Popen( command , stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'ignore').replace("\n","<br>") result = subprocess.Popen( command , stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'ignore').replace("\n","<br>")
return result return result
@app.route("/bird") @app.route("/bird")
@app.route("/bird6") @app.route("/bird6")
def bird(): def bird():
check_accesslist() check_security()
if request.path == "/bird": b = BirdSocket(file=app.config.get("BIRD_SOCKET")) if request.path == "/bird": b = BirdSocket(file=app.config.get("BIRD_SOCKET"))
elif request.path == "/bird6": b = BirdSocket(file=app.config.get("BIRD6_SOCKET")) elif request.path == "/bird6": b = BirdSocket(file=app.config.get("BIRD6_SOCKET"))
@ -113,7 +115,7 @@ def bird():
b.close() b.close()
# FIXME: use status # FIXME: use status
return result return result
if __name__ == "__main__": if __name__ == "__main__":
app.logger.info("lgproxy start") app.logger.info("lgproxy start")

View File

@ -1,4 +1,4 @@
const noArgReqs = ["summary"];
$(window).unload(function(){ $(window).unload(function(){
$(".progress").show() $(".progress").show()
@ -12,7 +12,7 @@ function change_url(loc){
function reload(){ function reload(){
loc = "/" + request_type + "/" + hosts + "/" + proto; loc = "/" + request_type + "/" + hosts + "/" + proto;
if (request_type != "summary" ){ if (!noArgReqs.includes(request_type)){
if( request_args != undefined && request_args != ""){ if( request_args != undefined && request_args != ""){
loc = loc + "?q=" + encodeURIComponent(request_args); loc = loc + "?q=" + encodeURIComponent(request_args);
change_url(loc) change_url(loc)
@ -22,7 +22,7 @@ function reload(){
} }
} }
function update_view(){ function update_view(){
if (request_type == "summary") if (noArgReqs.includes(request_type))
$(".navbar-search").hide(); $(".navbar-search").hide();
else else
$(".navbar-search").show(); $(".navbar-search").show();

View File

@ -1,7 +1,7 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<title>{{config.DOMAIN|capitalize}} looking glass</title>
<head> <head>
<title>{{config.WEBSITE_TITLE|default("Bird-LG / Looking Glass") }}</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/bootstrap.min.css') }}"> <link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/bootstrap-responsive.min.css') }}"> <link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/bootstrap-responsive.min.css') }}">
@ -18,7 +18,7 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</a> </a>
<a class="brand" href="/">{{config.DOMAIN|capitalize}} / Looking Glass</a> <a class="brand" href="/">{{config.WEBSITE_TITLE|default("Bird-LG / Looking Glass") }}</a>
<div class="navbar nav-collapse"> <div class="navbar nav-collapse">
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li class="navbar-text">Nodes:&nbsp;&nbsp;</li> <li class="navbar-text">Nodes:&nbsp;&nbsp;</li>
@ -120,7 +120,7 @@
<script type="text/javascript" src="{{url_for('static', filename='js/DT_bootstrap.js') }}"></script> <script type="text/javascript" src="{{url_for('static', filename='js/DT_bootstrap.js') }}"></script>
<script type="text/javascript"> <script type="text/javascript">
request_type = "{{session.request_type}}"; request_type = "{{session.request_type}}";
request_args = "{{session.request_args|safe}}"; request_args = "{{session.request_args}}";
hosts = "{{session.hosts}}"; hosts = "{{session.hosts}}";
proto = "{{session.proto}}"; proto = "{{session.proto}}";
history_query = {{session.history|tojson|safe}}; history_query = {{session.history|tojson|safe}};

View File

@ -24,21 +24,23 @@ import socket
import pickle import pickle
import xml.parsers.expat import xml.parsers.expat
dns_cache = resolver.LRUCache(max_size=10000)
resolv = resolver.Resolver() resolv = resolver.Resolver()
resolv.timeout = 0.5 resolv.timeout = 0.5
resolv.lifetime = 1 resolv.lifetime = 1
resolv.cache = dns_cache
def resolve(n, q): def resolve(n, q):
return str(resolv.query(n,q)[0]) return str(resolv.query(n,q)[0])
def mask_is_valid(n): def mask_is_valid(n):
if not n: if not n:
return True return True
try: try:
mask = int(n) mask = int(n)
return ( mask >= 1 and mask <= 128) return ( mask >= 1 and mask <= 128)
except: except:
return False return False
def ipv4_is_valid(n): def ipv4_is_valid(n):
try: try:
@ -55,21 +57,21 @@ def ipv6_is_valid(n):
return False return False
def save_cache_pickle(filename, data): def save_cache_pickle(filename, data):
output = open(filename, 'wb') output = open(filename, 'wb')
pickle.dump(data, output) pickle.dump(data, output)
output.close() output.close()
def load_cache_pickle(filename, default = None): def load_cache_pickle(filename, default = None):
try: try:
pkl_file = open(filename, 'rb') pkl_file = open(filename, 'rb')
except IOError: except IOError:
return default return default
try: try:
data = pickle.load(pkl_file) data = pickle.load(pkl_file)
except: except:
data = default data = default
pkl_file.close() pkl_file.close()
return data return data
def unescape(s): def unescape(s):
want_unicode = False want_unicode = False