Compare commits

..

No commits in common. "master" and "2012.1" have entirely different histories.

30 changed files with 406 additions and 13077 deletions

2
.gitignore vendored
View File

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

View File

@ -3,9 +3,9 @@
Copyright (c) 2006 Mehdi Abaakouk Copyright (c) 2006 Mehdi Abaakouk
This program is free software: you can redistribute it and/or modify This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License. the Free Software Foundation; either version 3 of the License
This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
@ -13,4 +13,5 @@
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA

View File

@ -1,116 +1,42 @@
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 splited onto two parts:
- lg-proxy.py:
- lgproxy.py: It must be install and started on all bird node. It act as a proxy to make traceroute and bird query on the node
Also the access restriction to this web service can be done is file "lg-proxy.cfg" (only ip based restriction for now)
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". 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:
This is the frontend, a web based UI that request information to all lg-proxy.py nodes
The domain and the list of all bird node can be done
This is the frontend, a web based UI that request informations to all lgproxy.py nodes.
The domain and the list of all bird nodes can be done.
```
*************** ***************
+--> * lgproxy.py * +--> * lg-proxy.py *
| *************** | ***************
| |
******** ******************* | *************** ******** ******************* | ***************
* USER * ----> * webserver/lg.py *--+--> * lgproxy.py * * USER * ----> * webserver/lg.py *--+--> * lg-proxy.py *
******** ******************* | *************** ******** ******************* | ***************
| |
| *************** | ***************
+--> * lgproxy.py * +--> * lg-proxy.py *
*************** ***************
```
Installation bird-lg depend on :
------------
The web service (lg.py) depends on:
- python-flask >= 0.8 - python-flask >= 0.8
- python-dnspython - python-dnspython
- python-pydot
- graphviz
- whois
The proxy running on routers (lgproxy.py) depends on: Each service can by embeded in any webserver by follow regular python-flask configuration
- python-flask >= 0.8 Only tested with bird 1.2.5
- traceroute
- ping
Each service can be embedded in any webserver by following regular python-flask configuration. source code under GPL 3.0, powered by Flask, jQuery and Bootstrap
It is also possible to run the services directly with python for developping / testing:
python2 lg.py Copyright (c) 2012 Mehdi Abaakouk <sileht@sileht.net>
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.
Copyright © 2012 Mehdi Abaakouk <sileht@sileht.net>
Happy users
-----------
* https://lg.ovh.net/
* http://lg.beta.as6453.net/
* https://lg.hamburg.freifunk.net/start
* http://lg.ring.nlnog.net/
* https://lg.tetaneutral.net/
* https://lg.gitoyen.net/
* http://lg.as5580.net/
* https://lg.ldn-fai.net/
* http://lg.arn-fai.net
* https://lg.grenode.net/
* http://lg.dataix.ru/
* https://lg.blix.com/
* https://lg.man-da.de/
* http://route-server.belwue.net/
* https://lg.exn.uk/
* https://meerblick.io/
* https://lg.as49697.net/
* http://lg.netnation.com/
* http://lg.edxnetwork.eu/
* https://lg.hivane.net/
* https://atw.hu/looking-glass
* http://lg.sibir-ix.ru/
* http://lg.interlan.ro/
* http://lg.as35266.net/
* https://lg.atw.co.hu/
* http://lg.as60362.net/
* http://lg.stuttgart-ix.de/
* http://www.bet3000.tv/
* https://lg.franceix.net/
* https://lg.fullsave.net/
* http://lg.catnix.net/
* https://lg.worldstream.nl/
* https://lg.angolacables.co.ao/

241
bird.py
View File

@ -25,150 +25,153 @@ 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 cmd(self, cmd): def __read(self):
try: code = "7000" # Not used in bird
self.__connect() parsed_string = ""
self.__sock.send(cmd + "\n") lastline = ""
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): while code not in END_CODES:
code = "7000" # Not used in bird data = self.__sock.recv(BUFSIZE)
parsed_string = ""
lastline = "" lines = (lastline + data).split("\n")
if len(data) == BUFSIZE:
lastline = lines[-1]
lines = lines[:-1]
while code not in END_CODES: for line in lines:
data = self.__sock.recv(BUFSIZE) code = line[0:4]
lines = (lastline + data).split("\n") if not line.strip():
if len(data) == BUFSIZE: continue
lastline = lines[-1] elif code == "0000":
lines = lines[:-1] 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
for line in lines: return True, parsed_string
code = line[0:4]
if not line.strip(): __all__ = ['BirdSocketSingleton' , 'BirdSocket' ]
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,18 +0,0 @@
# bird-lg init
Systemd unit files for the bird-lg webservice, and for the proxy service running on routers.
You need to adapt the exact command used to start the service (`ExecStart`) and the `User`
under which it should run. Don't run the services as root!
## Installation
Copy the init file under `/etc/systemd/system/` and run:
systemctl daemon-reload
systemctl start bird-lg-proxy
systemctl enable bird-lg-proxy
## Credits
Adapted from <http://gitlab.netlib.re/arn/arn-confs/tree/master/routing/looking-glass>

View File

@ -1,31 +0,0 @@
# Copyright (C) 2015-2018 Alsace Réseau Neutre
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Debian GNU/Linux: store this in /etc/systemd/system/
[Unit]
Description=BIRD Looking-Glass proxy
After=bird.service bird6.service
[Service]
Type=simple
ExecStart=/usr/local/lookingglass/lgproxy.py
User=lgproxy
Restart=on-failure
[Install]
WantedBy=multi-user.target

View File

@ -1,32 +0,0 @@
# Copyright (C) 2015-2018 Alsace Réseau Neutre
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Debian GNU/Linux: store this in /etc/systemd/system/
[Unit]
Description=BIRD Looking-Glass service
After=apache2.service
[Service]
Type=simple
User=lookingglass
ExecStart=/usr/local/lookingglass/lg.py
Restart=on-failure
[Install]
WantedBy=multi-user.target

4
lg-proxy.cfg Normal file
View File

@ -0,0 +1,4 @@
ACCESS_LIST = ["91.224.149.206", "178.33.111.110"]
IPV4_SOURCE="91.224.148.1"
IPV6_SOURCE="2a01:6600:8000::1"

84
lg-proxy.py Normal file
View File

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
# vim: ts=4
###
#
# Copyright (c) 2006 Mehdi Abaakouk
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
#
###
import subprocess
from urllib import unquote
from bird import BirdSocket
from flask import Flask, request, abort
app = Flask(__name__)
app.config.from_pyfile('lg-proxy.cfg')
def check_accesslist():
if app.config["ACCESS_LIST"] and request.remote_addr not in app.config["ACCESS_LIST"]:
abort(401)
@app.route("/traceroute")
@app.route("/traceroute6")
def traceroute():
check_accesslist()
src = []
if request.path == '/traceroute6':
o = "-6"
if app.config.get("IPV6_SOURCE",""):
src = [ "-s", app.config.get("IPV6_SOURCE") ]
else:
o = "-4"
if app.config.get("IPV4_SOURCE",""):
src = [ "-s", app.config.get("IPV4_SOURCE") ]
query = request.args.get("q","")
query = unquote(query)
command = [ 'traceroute' , o ] + src + [ '-A', '-q1', '-N32', '-w1', '-m15', query ]
result = subprocess.Popen( command , stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'ignore').replace("\n","<br>")
return result
@app.route("/bird")
@app.route("/bird6")
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"
query = request.args.get("q","")
query = unquote(query)
status, result = b.cmd(query)
b.close()
# FIXME: use status
return result
if __name__ == "__main__":
app.debug = True
app.run("0.0.0.0")

20
lg.cfg Normal file
View File

@ -0,0 +1,20 @@
DOMAIN = "tetaneutral.net"
PROXY = {
"gw": 5000,
"h3": 5000,
}
# Used for bgpmap
ROUTER_IP = {
"gw" : [ "91.224.148.2", "2a01:6600:8000::175" ],
"h3" : [ "91.224.148.3", "2a01:6600:8000::131" ]
}
AS_NUMBER = {
"gw" : "197422",
"h3" : "197422"
}
SESSION_KEY = '\xd77\xf9\xfa\xc2\xb5\xcd\x85)`+H\x9d\xeeW\\%\xbe/\xbaT\x89\xe8\xa7'

View File

@ -1,42 +0,0 @@
# Configuration file example for lg.py
# Adapt and copy to lg.cfg
WEBSITE_TITLE="Bird-LG / Looking Glass"
DEBUG = False
LOG_FILE="/var/log/lg.log"
LOG_LEVEL="WARNING"
# Keep log history indefinitely by default.
LOG_NUM_DAYS=0
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_PORT = 5000
PROXY = {
"gw": 5000,
"h3": 5000,
}
# Used for bgpmap
ROUTER_IP = {
"gw" : [ "91.224.148.2", "2a01:6600:8000::175" ],
"h3" : [ "91.224.148.3", "2a01:6600:8000::131" ]
}
AS_NUMBER = {
"gw" : "197422",
"h3" : "197422"
}
#WHOIS_SERVER = "whois.foo.bar"
# DNS zone to query for ASN -> name mapping
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'

365
lg.py Normal file → Executable file
View File

@ -20,45 +20,20 @@
# #
### ###
import base64
from datetime import datetime
import subprocess import subprocess
import logging
from logging.handlers import TimedRotatingFileHandler
import re import re
from urllib2 import urlopen from urllib2 import urlopen
from urllib import quote, unquote from urllib import quote, unquote
import json import json
import random import random
import argparse
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, save_cache_pickle, load_cache_pickle
import pydot import pydot
from flask import Flask, render_template, jsonify, redirect, session, request, abort, Response, Markup from flask import Flask, render_template, jsonify, redirect, session, request, abort, Response
parser = argparse.ArgumentParser()
parser.add_argument('-c', dest='config_file', help='path to config file', default='lg.cfg')
args = parser.parse_args()
app = Flask(__name__) app = Flask(__name__)
app.config.from_pyfile(args.config_file) app.config.from_pyfile('lg.cfg')
app.secret_key = app.config["SESSION_KEY"]
app.debug = app.config["DEBUG"]
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()))
app.logger.addHandler(file_handler)
def get_asn_from_as(n):
asn_zone = app.config.get("ASN_ZONE", "asn.cymru.com")
try:
data = resolve("AS%s.%s" % (n, asn_zone) ,"TXT").replace("'","").replace('"','')
except:
return " "*5
return [ field.strip() for field in data.split("|") ]
def add_links(text): def add_links(text):
@ -73,17 +48,14 @@ def add_links(text):
# Some heuristic to create link # Some heuristic to create link
if line.strip().startswith("BGP.as_path:") or \ if line.strip().startswith("BGP.as_path:") or \
line.strip().startswith("Neighbor AS:"): line.strip().startswith("Neighbor AS:"):
ret_text.append(re.sub(r'(\d+)', r'<a href="/whois?q=\1" class="whois">\1</a>', line)) ret_text.append(re.sub(r'(\d+)', r'<a href="/whois/\1" class="whois">\1</a>', line))
else: else:
line = re.sub(r'([a-zA-Z0-9\-]*\.([a-zA-Z]{2,3}){1,2})(\s|$)', r'<a href="/whois?q=\1" class="whois">\1</a>\3', line) line = re.sub(r'([a-zA-Z0-9\-]*\.([a-zA-Z]{2,3}){1,2})(\s|$)', r'<a href="/whois/\1" class="whois">\1</a>\3', line)
line = re.sub(r'(?<=\[)AS(\d+)', r'<a href="/whois?q=\1" class="whois">AS\1</a>', line) line = re.sub(r'AS(\d+)', r'<a href="/whois/\1" class="whois">AS\1</a>', line)
line = re.sub(r'(\d+\.\d+\.\d+\.\d+)', r'<a href="/whois?q=\1" class="whois">\1</a>', line) line = re.sub(r'(\d+\.\d+\.\d+\.\d+)', r'<a href="/whois/\1" class="whois">\1</a>', line)
if len(request.path) >= 2: hosts = "/".join(request.path.split("/")[2:])
hosts = "/".join(request.path.split("/")[2:])
else:
hosts = "/"
line = re.sub(r'\[(\w+)\s+((|\d\d\d\d-\d\d-\d\d\s)(|\d\d:)\d\d:\d\d|\w\w\w\d\d)', r'[<a href="/detail/%s?q=\1">\1</a> \2' % hosts, line) line = re.sub(r'\[(\w+)\s+((|\d\d\d\d-\d\d-\d\d\s)(|\d\d:)\d\d:\d\d|\w\w\w\d\d)', r'[<a href="/detail/%s?q=\1">\1</a> \2' % hosts, line)
line = re.sub(r'(^|\s+)(([a-f\d]{0,4}:){3,10}[a-f\d]{0,4})', r'\1<a href="/whois?q=\2" class="whois">\2</a>', line, re.I) line = re.sub(r'(^|\s+)(([a-f\d]{0,4}:){3,10}[a-f\d]{0,4})', r'\1<a href="/whois/\2" class="whois">\2</a>', line, re.I)
ret_text.append(line) ret_text.append(line)
return "\n".join(ret_text) return "\n".join(ret_text)
@ -111,10 +83,7 @@ def set_session(request_type, hosts, proto, request_args):
def whois_command(query): def whois_command(query):
server = [] return subprocess.Popen(['whois', query], stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'ignore')
if app.config.get("WHOIS_SERVER", ""):
server = [ "-h", app.config.get("WHOIS_SERVER") ]
return subprocess.Popen(['whois'] + server + [query], stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'ignore')
def bird_command(host, proto, query): def bird_command(host, proto, query):
@ -123,9 +92,9 @@ def bird_command(host, proto, query):
def bird_proxy(host, proto, service, query): def bird_proxy(host, proto, service, query):
"""Retreive data of a service from a running lgproxy on a remote node """Retreive data of a service from a running lg-proxy on a remote node
First and second arguments are the node and the port of the running lgproxy First and second arguments are the node and the port of the running lg-proxy
Third argument is the service, can be "traceroute" or "bird" Third argument is the service, can be "traceroute" or "bird"
Last argument, the query to pass to the service Last argument, the query to pass to the service
@ -140,29 +109,18 @@ def bird_proxy(host, proto, service, query):
port = app.config["PROXY"].get(host, "") port = app.config["PROXY"].get(host, "")
if not port: if not port or not path:
return False, 'Host "%s" invalid' % host return False, "Host/Proto not allowed"
elif not path: else:
return False, 'Proto "%s" invalid' % proto url = "http://%s.%s:%d/%s?q=%s" % (host, app.config["DOMAIN"], port, path, quote(query))
try:
url = "http://%s" % (host) f = urlopen(url)
if "DOMAIN" in app.config: resultat = f.read()
url = "%s.%s" % (url, app.config["DOMAIN"]) status = True # retreive remote status
url = "%s:%d/%s?" % (url, port, path) except IOError:
if "SHARED_SECRET" in app.config: resultat = "Failed retreive url: %s" % url
url = "%ssecret=%s&" % (url, app.config["SHARED_SECRET"]) status = False
url = "%sq=%s" % (url, quote(query)) 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
@ -197,26 +155,22 @@ def hello():
def error_page(text): def error_page(text):
return render_template('error.html', errors=[text]), 500 return render_template('error.html', error=text), 500
@app.errorhandler(400) @app.errorhandler(400)
def incorrect_request(e): def incorrect_request(e):
return render_template('error.html', warnings=["The server could not understand the request"]), 400 return render_template('error.html', warning="The server could not understand the request"), 400
@app.errorhandler(404) @app.errorhandler(404)
def page_not_found(e): def page_not_found(e):
return render_template('error.html', warnings=["The requested URL was not found on the server."]), 404 return render_template('error.html', warning="The requested URL was not found on the server."), 404
def get_query():
q = unquote(request.args.get('q', '').strip())
return q
@app.route("/whois") @app.route("/whois/<query>")
def whois(): def whois(query):
query = get_query() if not query.strip():
if not query:
abort(400) abort(400)
try: try:
@ -227,59 +181,46 @@ def whois():
if m: if m:
query = query.groupdict()["domain"] query = query.groupdict()["domain"]
output = whois_command(query) output = whois_command(query).replace("\n", "<br>")
return jsonify(output=output, title=query) return jsonify(output=output, title=query)
SUMMARY_UNWANTED_PROTOS = ["Kernel", "Static", "Device", "Direct"] SUMMARY_UNWANTED_PROTOS = ["Kernel", "Static", "Device"]
SUMMARY_RE_MATCH = r"(?P<name>[\w_]+)\s+(?P<proto>\w+)\s+(?P<table>\w+)\s+(?P<state>\w+)\s+(?P<since>((|\d\d\d\d-\d\d-\d\d\s)(|\d\d:)\d\d:\d\d|\w\w\w\d\d))($|\s+(?P<info>.*))"
@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"):
set_session("summary", hosts, proto, "") set_session("summary", hosts, proto, "")
command = "show protocols" command = "show protocols"
summary = {} summary = {}
errors = [] error = []
for host in hosts.split("+"): for host in hosts.split("+"):
ret, res = bird_command(host, proto, command) ret, res = bird_command(host, proto, command)
res = res.split("\n") res = res.split("\n")
if len(res) > 1:
data = []
for line in res[1:]:
line = line.strip()
if line and (line.split() + [""])[1] not in SUMMARY_UNWANTED_PROTOS:
m = re.match(SUMMARY_RE_MATCH, line)
if m:
data.append(m.groupdict())
else:
app.logger.warning("couldn't parse: %s", line)
if ret is False: summary[host] = data
errors.append("%s" % res) else:
continue error.append("%s: bird command failed with error, %s" % (host, "\n".join(res)))
if len(res) <= 1: return render_template('summary.html', summary=summary, command=command, error="<br>".join(error))
errors.append("%s: bird command failed with error, %s" % (host, "\n".join(res)))
continue
data = []
for line in res[1:]:
line = line.strip()
if line and (line.split() + [""])[1] not in SUMMARY_UNWANTED_PROTOS:
split = line.split()
if len(split) >= 5:
props = dict()
props["name"] = split[0]
props["proto"] = split[1]
props["table"] = split[2]
props["state"] = split[3]
props["since"] = split[4]
props["info"] = ' '.join(split[5:]) if len(split) > 5 else ""
data.append(props)
else:
app.logger.warning("couldn't parse: %s", line)
summary[host] = data
return render_template('summary.html', summary=summary, command=command, errors=errors)
@app.route("/detail/<hosts>/<proto>") @app.route("/detail/<hosts>/<proto>")
def detail(hosts, proto): def detail(hosts, proto):
name = get_query() name = request.args.get('q', '').strip()
if not name: if not name:
abort(400) abort(400)
@ -287,28 +228,21 @@ def detail(hosts, proto):
command = "show protocols all %s" % name command = "show protocols all %s" % name
detail = {} detail = {}
errors = [] error = []
for host in hosts.split("+"): for host in hosts.split("+"):
ret, res = bird_command(host, proto, command) ret, res = bird_command(host, proto, command)
res = res.split("\n") res = res.split("\n")
if len(res) > 1:
detail[host] = {"status": res[1], "description": add_links(res[2:])}
else:
error.append("%s: bird command failed with error, %s" % (host, "\n".join(res)))
if ret is False: return render_template('detail.html', detail=detail, command=command, error="<br>".join(error))
errors.append("%s" % res)
continue
if len(res) <= 1:
errors.append("%s: bird command failed with error, %s" % (host, "\n".join(res)))
continue
detail[host] = {"status": res[1], "description": add_links(res[2:])}
return render_template('detail.html', detail=detail, command=command, errors=errors)
@app.route("/traceroute/<hosts>/<proto>") @app.route("/traceroute/<hosts>/<proto>")
def traceroute(hosts, proto): def traceroute(hosts, proto):
q = get_query() q = request.args.get('q', '').strip()
if not q: if not q:
abort(400) abort(400)
@ -325,17 +259,11 @@ def traceroute(hosts, proto):
except: except:
return error_page("%s is unresolvable or invalid for %s" % (q, proto)) return error_page("%s is unresolvable or invalid for %s" % (q, proto))
errors = []
infos = {} infos = {}
for host in hosts.split("+"): for host in hosts.split("+"):
status, resultat = bird_proxy(host, proto, "traceroute", q) status, resultat = bird_proxy(host, proto, "traceroute", q)
if status is False:
errors.append("%s" % resultat)
continue
infos[host] = add_links(resultat) infos[host] = add_links(resultat)
return render_template('traceroute.html', infos=infos, errors=errors) return render_template('traceroute.html', infos=infos)
@app.route("/adv/<hosts>/<proto>") @app.route("/adv/<hosts>/<proto>")
@ -378,18 +306,31 @@ def show_route_for_bgpmap(hosts, proto):
return show_route("prefix_bgpmap", hosts, proto) return show_route("prefix_bgpmap", hosts, proto)
ASNAME_CACHE_FILE = "/tmp/asname_cache.pickle"
ASNAME_CACHE = load_cache_pickle(ASNAME_CACHE_FILE, {})
def get_as_name(_as): def get_as_name(_as):
"""Returns a string that contain the as number following by the as name """return 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:
return "AS?????"
if not _as.isdigit(): if not _as.isdigit():
return _as.strip() return _as.strip()
name = get_asn_from_as(_as)[-1].replace(" ", "\r", 1) if _as not in ASNAME_CACHE:
return "AS%s | %s" % (_as, name) whois_answer = whois_command("as%s" % _as)
as_name = re.search('(as-name|ASName): (.*)', whois_answer)
if as_name:
ASNAME_CACHE[_as] = as_name.group(2).strip()
else:
ASNAME_CACHE[_as] = _as
save_cache_pickle(ASNAME_CACHE_FILE, ASNAME_CACHE)
if ASNAME_CACHE[_as] == _as:
return "AS%s" % _as
else:
return "AS%s\r%s" % (_as, ASNAME_CACHE[_as])
def get_as_number_from_protocol_name(host, proto, protocol): def get_as_number_from_protocol_name(host, proto, protocol):
ret, res = bird_command(host, proto, "show protocols all %s" % protocol) ret, res = bird_command(host, proto, "show protocols all %s" % protocol)
@ -404,28 +345,20 @@ def get_as_number_from_protocol_name(host, proto, protocol):
def show_bgpmap(): def show_bgpmap():
"""return a bgp map in a png file, from the json tree in q argument""" """return a bgp map in a png file, from the json tree in q argument"""
data = get_query() data = request.args.get('q', '').strip()
if not data: if not data:
abort(400) abort(400)
data = base64.b64decode(data) data = json.loads(unquote(data))
data = json.loads(data)
graph = pydot.Dot('BGPMAP', graph_type='digraph') graph = pydot.Dot('BGPMAP', graph_type='digraph')
nodes = {} nodes = {}
edges = {} edges = {}
prepend_as = {}
def escape(label):
label = label.replace("&", "&amp;")
label = label.replace(">", "&gt;")
label = label.replace("<", "&lt;")
return label
def add_node(_as, **kwargs): def add_node(_as, **kwargs):
if _as not in nodes: if _as not in nodes:
kwargs["label"] = '<<TABLE CELLBORDER="0" BORDER="0" CELLPADDING="0" CELLSPACING="0"><TR><TD ALIGN="CENTER">' + escape(kwargs.get("label", get_as_name(_as))).replace("\r","<BR/>") + "</TD></TR></TABLE>>" kwargs["label"] = '<<TABLE CELLBORDER="0" BORDER="0" CELLPADDING="0" CELLSPACING="0"><TR><TD ALIGN="CENTER">' + kwargs.get("label", get_as_name(_as)).replace("\r","<BR/>") + "</TD></TR></TABLE>>"
nodes[_as] = pydot.Node(_as, style="filled", fontsize="10", **kwargs) nodes[_as] = pydot.Node(_as, style="filled", fontsize="10", **kwargs)
graph.add_node(nodes[_as]) graph.add_node(nodes[_as])
return nodes[_as] return nodes[_as]
@ -441,24 +374,11 @@ def show_bgpmap():
edges[edge_tuple] = edge edges[edge_tuple] = edge
elif "label" in kwargs and kwargs["label"]: elif "label" in kwargs and kwargs["label"]:
e = edges[edge_tuple] e = edges[edge_tuple]
e.set_label(e.get_label() + "\r" + kwargs["label"])
label_without_star = kwargs["label"].replace("*", "")
if e.get_label() is not None:
labels = e.get_label().split("\r")
else:
return edges[edge_tuple]
if "%s*" % label_without_star not in labels:
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)
label = escape("\r".join(labels))
e.set_label(label)
return edges[edge_tuple] return edges[edge_tuple]
for host, asmaps in data.iteritems(): for host, asmaps in data.iteritems():
if "DOMAIN" in app.config: add_node(host, label= "%s\r%s" % (host.upper(), app.config["DOMAIN"].upper()), shape="box", fillcolor="#F5A9A9")
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:
@ -466,9 +386,9 @@ 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
hosts = data.keys() hosts = data.keys()
for host, asmaps in data.iteritems(): for host, asmaps in data.iteritems():
first = True first = True
@ -480,64 +400,39 @@ def show_bgpmap():
hop_label = "" hop_label = ""
for _as in asmap: for _as in asmap:
if _as == previous_as: if _as == previous_as:
if not prepend_as.get(_as, None):
prepend_as[_as] = {}
if not prepend_as[_as].get(host, None):
prepend_as[_as][host] = {}
if not prepend_as[_as][host].get(asmap[0], None):
prepend_as[_as][host][asmap[0]] = 1
prepend_as[_as][host][asmap[0]] += 1
continue continue
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]:
add_node(_as, fillcolor="#F5A9A9", shape="box", ) add_node(_as, fillcolor=(first and "#F5A9A9" or "white"))
else: edge = add_edge(nodes[previous_as], nodes[_as] , label=hop_label, fontsize="7")
add_node(_as, fillcolor=(first and "#F5A9A9" or "white"), )
if hop_label:
edge = add_edge(nodes[previous_as], nodes[_as], label=hop_label, fontsize="7")
else:
edge = add_edge(nodes[previous_as], nodes[_as], fontsize="7")
hop_label = "" hop_label = ""
if first or _as == asmap[-1]: if first:
edge.set_style("bold") edge.set_style("bold")
edge.set_color("red") edge.set_color("red")
elif edge.get_style() != "bold": elif edge.get_color() != "red":
edge.set_style("dashed") edge.set_style("dashed")
edge.set_color(color) edge.set_color(color)
previous_as = _as previous_as = _as
first = False first = False
for _as in prepend_as: node = add_node(previous_as)
for n in set([ n for h, d in prepend_as[_as].iteritems() for p, n in d.iteritems() ]): node.set_shape("box")
graph.add_edge(pydot.Edge(*(_as, _as), label=" %dx" % n, color="grey", fontcolor="grey"))
fmt = request.args.get('fmt', 'png')
#response = Response("<pre>" + graph.create_dot() + "</pre>")
if fmt == "png":
response = Response(graph.create_png(), mimetype='image/png')
elif fmt == "svg":
response = Response(graph.create_svg(), mimetype='image/svg+xml')
else:
abort(400, "Incorrect format")
response.headers['Last-Modified'] = datetime.now()
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '-1'
return response
#return Response("<pre>" + graph.create_dot() + "</pre>")
return Response(graph.create_png(), mimetype='image/png')
def build_as_tree_from_raw_bird_ouput(host, proto, text): def build_as_tree_from_raw_bird_ouput(host, proto, text):
@ -546,29 +441,21 @@ 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 = ""
for line in text: for line in text:
line = line.strip() line = line.strip()
expr = re.search(r'(.*)unicast\s+\[(\w+)\s+', line) expr = re.search(r'(.*)via\s+([0-9a-fA-F:\.]+)\s+on.*\[(\w+)\s+', line)
if expr: if expr:
if expr.group(1).strip():
net_dest = expr.group(1).strip()
peer_protocol_name = expr.group(2).strip()
expr2 = re.search(r'(.*)via\s+([0-9a-fA-F:\.]+)\s+on\s+\S+(\s+\[(\w+)\s+)?', line)
if expr2:
if path: if path:
path.append(net_dest) path.append(net_dest)
paths.append(path) paths.append(path)
path = None path = None
if expr2.group(1).strip(): if expr.group(1).strip():
net_dest = expr2.group(1).strip() net_dest = expr.group(1).strip()
peer_ip = expr2.group(2).strip() peer_ip = expr.group(2).strip()
if expr2.group(4): peer_protocol_name = expr.group(3).strip()
peer_protocol_name = expr2.group(4).strip()
# Check if via line is a internal route # Check if via line is a internal route
for rt_host, rt_ips in app.config["ROUTER_IP"].iteritems(): for rt_host, rt_ips in app.config["ROUTER_IP"].iteritems():
# Special case for internal routing # Special case for internal routing
@ -580,26 +467,9 @@ def build_as_tree_from_raw_bird_ouput(host, proto, text):
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)
if expr3:
if path:
path.append(net_dest)
paths.append(path)
path = None
if path is None:
path = [ expr3.group(2).strip() ]
if expr3.group(1).strip():
net_dest = expr3.group(1).strip()
if line.startswith("BGP.as_path:"): if line.startswith("BGP.as_path:"):
ASes = line.replace("BGP.as_path:", "").strip().split(" ") path.extend(line.replace("BGP.as_path:", "").strip().split(" "))
if path:
path.extend(ASes)
else:
path = ASes
if path: if path:
path.append(net_dest) path.append(net_dest)
paths.append(path) paths.append(path)
@ -608,7 +478,7 @@ def build_as_tree_from_raw_bird_ouput(host, proto, text):
def show_route(request_type, hosts, proto): def show_route(request_type, hosts, proto):
expression = get_query() expression = unquote(request.args.get('q', '')).strip()
if not expression: if not expression:
abort(400) abort(400)
@ -628,7 +498,7 @@ def show_route(request_type, hosts, proto):
command = "show route where net ~ [ " + expression + " ]" + all command = "show route where net ~ [ " + expression + " ]" + all
else: else:
mask = "" mask = ""
if len(expression.split("/")) == 2: if len(expression.split("/")) > 1:
expression, mask = (expression.split("/")) expression, mask = (expression.split("/"))
if not mask and proto == "ipv4": if not mask and proto == "ipv4":
@ -655,29 +525,26 @@ def show_route(request_type, hosts, proto):
command = "show route for " + expression + all command = "show route for " + expression + all
detail = {} detail = {}
errors = [] error = []
for host in hosts.split("+"): for host in hosts.split("+"):
ret, res = bird_command(host, proto, command) ret, res = bird_command(host, proto, command)
res = res.split("\n") res = res.split("\n")
if len(res) > 1:
if ret is False: if bgpmap:
errors.append("%s" % res) detail[host] = build_as_tree_from_raw_bird_ouput(host, proto, res)
continue else:
detail[host] = add_links(res)
if len(res) <= 1:
errors.append("%s: bird command failed with error, %s" % (host, "\n".join(res)))
continue
if bgpmap:
detail[host] = build_as_tree_from_raw_bird_ouput(host, proto, res)
else: else:
detail[host] = add_links(res) error.append("%s: bird command failed with error, %s" % (host, "\n".join(res)))
if bgpmap: if bgpmap:
detail = base64.b64encode(json.dumps(detail)) detail = json.dumps(detail)
return render_template((bgpmap and 'bgpmap.html' or 'route.html'), detail=detail, command=command, expression=expression, errors=errors) return render_template((bgpmap and 'bgpmap.html' or 'route.html'), detail=detail, command=command, expression=expression, error="<br>".join(error))
app.secret_key = app.config["SESSION_KEY"]
app.debug = True
if __name__ == "__main__": if __name__ == "__main__":
app.run(app.config.get("BIND_IP", "0.0.0.0"), app.config.get("BIND_PORT", 5000)) app.run("0.0.0.0")

View File

@ -1,8 +1,5 @@
import sys import sys
import os sys.path.insert(0,"/var/www/lg2.tetaneutral.net/")
sitepath = os.path.realpath(os.path.dirname(__file__))
sys.path.insert(0, sitepath)
from lg import app as application from lg import app as application

View File

@ -1,28 +0,0 @@
# 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

@ -1,122 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# vim: ts=4
###
#
# Copyright (c) 2006 Mehdi Abaakouk
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
#
###
import sys
import logging
from logging.handlers import TimedRotatingFileHandler
from logging import FileHandler
import subprocess
from urllib import unquote
import argparse
from bird import BirdSocket
from flask import Flask, request, abort
parser = argparse.ArgumentParser()
parser.add_argument('-c', dest='config_file', help='path to config file', default='lgproxy.cfg')
args = parser.parse_args()
app = Flask(__name__)
app.debug = app.config["DEBUG"]
app.config.from_pyfile(args.config_file)
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.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.after_request
def access_log_after(response, *args, **kwargs):
app.logger.info("[%s] reponse %s, %s", request.remote_addr, request.url, response.status_code)
return response
def check_security():
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)
@app.route("/traceroute")
@app.route("/traceroute6")
def traceroute():
check_security()
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' ]
src = []
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") ]
query = request.args.get("q","")
query = unquote(query)
if sys.platform.startswith('freebsd') or sys.platform.startswith('netbsd'):
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","<br>")
return result
@app.route("/bird")
@app.route("/bird6")
def bird():
check_security()
if request.path == "/bird": b = BirdSocket(file=app.config.get("BIRD_SOCKET"))
elif request.path == "/bird6": b = BirdSocket(file=app.config.get("BIRD6_SOCKET"))
else: return "No bird socket selected"
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")
app.run(app.config.get("BIND_IP", "0.0.0.0"), app.config.get("BIND_PORT", 5000))

View File

@ -1,8 +0,0 @@
import sys
import os
sitepath = os.path.realpath(os.path.dirname(__file__))
sys.path.insert(0, sitepath)
from lgproxy import app as application

View File

@ -1,47 +0,0 @@
div.dataTables_length label {
float: left;
text-align: left;
}
div.dataTables_length select {
width: 75px;
}
div.dataTables_filter label {
float: right;
}
div.dataTables_info {
padding-top: 8px;
}
div.dataTables_paginate {
float: right;
margin: 0;
}
table.table {
clear: both;
margin-bottom: 6px !important;
}
table.table thead .sorting,
table.table thead .sorting_asc,
table.table thead .sorting_desc,
table.table thead .sorting_asc_disabled,
table.table thead .sorting_desc_disabled {
cursor: pointer;
*cursor: hand;
}
table.table thead .sorting { background: url('../img/sort_both.png') no-repeat center right; }
table.table thead .sorting_asc { background: url('../img/sort_asc.png') no-repeat center right; }
table.table thead .sorting_desc { background: url('../img/sort_desc.png') no-repeat center right; }
table.table thead .sorting_asc_disabled { background: url('../img/sort_asc_disabled.png') no-repeat center right; }
table.table thead .sorting_desc_disabled { background: url('../img/sort_desc_disabled.png') no-repeat center right; }
table.dataTable th:active {
outline: none;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,96 +0,0 @@
/* Default class modification */
$.extend( $.fn.dataTableExt.oStdClasses, {
"sWrapper": "dataTables_wrapper form-inline"
} );
/* API method to get paging information */
$.fn.dataTableExt.oApi.fnPagingInfo = function ( oSettings )
{
return {
"iStart": oSettings._iDisplayStart,
"iEnd": oSettings.fnDisplayEnd(),
"iLength": oSettings._iDisplayLength,
"iTotal": oSettings.fnRecordsTotal(),
"iFilteredTotal": oSettings.fnRecordsDisplay(),
"iPage": Math.ceil( oSettings._iDisplayStart / oSettings._iDisplayLength ),
"iTotalPages": Math.ceil( oSettings.fnRecordsDisplay() / oSettings._iDisplayLength )
};
}
/* Bootstrap style pagination control */
$.extend( $.fn.dataTableExt.oPagination, {
"bootstrap": {
"fnInit": function( oSettings, nPaging, fnDraw ) {
var oLang = oSettings.oLanguage.oPaginate;
var fnClickHandler = function ( e ) {
e.preventDefault();
if ( oSettings.oApi._fnPageChange(oSettings, e.data.action) ) {
fnDraw( oSettings );
}
};
$(nPaging).addClass('pagination').append(
'<ul>'+
'<li class="prev disabled"><a href="#">&larr; '+oLang.sPrevious+'</a></li>'+
'<li class="next disabled"><a href="#">'+oLang.sNext+' &rarr; </a></li>'+
'</ul>'
);
var els = $('a', nPaging);
$(els[0]).bind( 'click.DT', { action: "previous" }, fnClickHandler );
$(els[1]).bind( 'click.DT', { action: "next" }, fnClickHandler );
},
"fnUpdate": function ( oSettings, fnDraw ) {
var iListLength = 5;
var oPaging = oSettings.oInstance.fnPagingInfo();
var an = oSettings.aanFeatures.p;
var i, j, sClass, iStart, iEnd, iHalf=Math.floor(iListLength/2);
if ( oPaging.iTotalPages < iListLength) {
iStart = 1;
iEnd = oPaging.iTotalPages;
}
else if ( oPaging.iPage <= iHalf ) {
iStart = 1;
iEnd = iListLength;
} else if ( oPaging.iPage >= (oPaging.iTotalPages-iHalf) ) {
iStart = oPaging.iTotalPages - iListLength + 1;
iEnd = oPaging.iTotalPages;
} else {
iStart = oPaging.iPage - iHalf + 1;
iEnd = iStart + iListLength - 1;
}
for ( i=0, iLen=an.length ; i<iLen ; i++ ) {
// Remove the middle elements
$('li:gt(0)', an[i]).filter(':not(:last)').remove();
// Add the new list items and their event handlers
for ( j=iStart ; j<=iEnd ; j++ ) {
sClass = (j==oPaging.iPage+1) ? 'class="active"' : '';
$('<li '+sClass+'><a href="#">'+j+'</a></li>')
.insertBefore( $('li:last', an[i])[0] )
.bind('click', function (e) {
e.preventDefault();
oSettings._iDisplayStart = (parseInt($('a', this).text(),10)-1) * oPaging.iLength;
fnDraw( oSettings );
} );
}
// Add / remove disabled classes from the static elements
if ( oPaging.iPage === 0 ) {
$('li:first', an[i]).addClass('disabled');
} else {
$('li:first', an[i]).removeClass('disabled');
}
if ( oPaging.iPage === oPaging.iTotalPages-1 || oPaging.iTotalPages === 0 ) {
$('li:last', an[i]).addClass('disabled');
} else {
$('li:last', an[i]).removeClass('disabled');
}
}
}
}
} );

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
const noArgReqs = ["summary"];
$(window).unload(function(){ $(window).unload(function(){
$(".progress").show() $(".progress").show()
@ -12,9 +12,9 @@ function change_url(loc){
function reload(){ function reload(){
loc = "/" + request_type + "/" + hosts + "/" + proto; loc = "/" + request_type + "/" + hosts + "/" + proto;
if (!noArgReqs.includes(request_type)){ if (request_type != "summary" ){
if( request_args != undefined && request_args != ""){ if( request_args != undefined && request_args != ""){
loc = loc + "?q=" + encodeURIComponent(request_args); loc = loc + "?q=" + request_args;
change_url(loc) change_url(loc)
} }
} else { } else {
@ -22,7 +22,7 @@ function reload(){
} }
} }
function update_view(){ function update_view(){
if (noArgReqs.includes(request_type)) if (request_type == "summary")
$(".navbar-search").hide(); $(".navbar-search").hide();
else else
$(".navbar-search").show(); $(".navbar-search").show();
@ -58,7 +58,7 @@ $(function(){
link = $(this).attr('href'); link = $(this).attr('href');
$.getJSON(link, function(data) { $.getJSON(link, function(data) {
$(".modal h3").html(data.title); $(".modal h3").html(data.title);
$(".modal .modal-body > p").css("white-space", "pre-line").text(data.output); $(".modal .modal-body > p").html(data.output);
$(".modal").modal('show'); $(".modal").modal('show');
}); });
}); });
@ -94,12 +94,6 @@ $(function(){
}); });
$('.request_args').val(request_args); $('.request_args').val(request_args);
update_view(); update_view();
t = $('.table-summary')
if (t) t.dataTable( {
"bPaginate": false,
} );
}); });

View File

@ -1,13 +1,9 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% block body %}
<div style="float:right">
<a href="/bgpmap/?killcache=1&q={{detail}}&fmt=svg" target="_blank">SVG</a>
<a href="/bgpmap/?killcache=1&q={{detail}}&fmt=png" target="_blank">PNG</a>
</div>
<h3>{{session.hosts}}: {{command}}</h3> <h3>{{session.hosts}}: {{command}}</h3>
{% if session.request_args != expression|replace("/32","")|replace("/128","") %} {% if session.request_args != expression|replace("/32","")|replace("/128","") %}
<i>DNS: <a href="/whois/{{session.request_args}}" class="whois">{{session.request_args}}</a> => <a href="/whois/{{ expression|replace("/32","")|replace("/128","") }}" class="whois">{{expression|replace("/32","")|replace("/128","")}}</a></i><br /> <i>DNS: <a href="/whois/{{session.request_args}}" class="whois">{{session.request_args}}</a> => <a href="/whois/{{ expression|replace("/32","")|replace("/128","") }}" class="whois">{{expression|replace("/32","")|replace("/128","")}}</a></i><br />
{% endif %}<br /> {% endif %}<br />
<a href="/bgpmap/?killcache=1&q={{detail}}"><img src="/bgpmap/?q={{detail}}" /></a> <a href="/bgpmap/?q={{detail}}"><img src="/bgpmap/?q={{detail}}" /></a>
<br /> <br />
{% endblock %} {% endblock %}

View File

@ -1,12 +1,11 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<title>Tetaneutral.net 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') }}">
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/docs.css') }}"> <link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/docs.css') }}">
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/DT_bootstrap.css') }}">
</head> </head>
<body> <body>
<div class="navbar navbar-fixed-top"> <div class="navbar navbar-fixed-top">
@ -18,7 +17,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.WEBSITE_TITLE|default("Bird-LG / Looking Glass") }}</a> <a class="brand" href="/">{{config.DOMAIN|capitalize}} / 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>
@ -64,15 +63,11 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="row-fluid"> <div class="row-fluid">
<div class="span8"> <div class="span8">
{% if warnings %} {% if warning %}
<div class="alert alert-warning"> <div class="alert alert-warning">{{warning}}</div>
{% for warning in warnings %}{{warning}}<br />{% endfor %}
</div>
{% endif %} {% endif %}
{% if errors %} {% if error %}
<div class="alert alert-error"> <div class="alert alert-error">{{error}}</div>
{% for error in errors %}{{error}}<br />{% endfor %}
</div>
{% endif %} {% endif %}
{% block body %}{% endblock %} {% block body %}{% endblock %}
@ -84,7 +79,7 @@
<li class="nav-header">Request history</li> <li class="nav-header">Request history</li>
{% for hosts, proto, request_type, request_args in session.history %} {% for hosts, proto, request_type, request_args in session.history %}
<li{% if loop.first %} class="active"{% endif %}> <li{% if loop.first %} class="active"{% endif %}>
<a href="/{{ [request_type, hosts, proto]|join("/") }}{% if request_args %}?q={{request_args|urlencode}}{% endif %}"> <a href="/{{ [request_type, hosts, proto]|join("/") }}{% if request_args %}?q={{request_args}}{% endif %}">
{{hosts}}/{{proto}}: {{ commands_dict[request_type]|replace("...", request_args) }} {{hosts}}/{{proto}}: {{ commands_dict[request_type]|replace("...", request_args) }}
</a> </a>
</li> </li>
@ -116,8 +111,7 @@
</div> </div>
<script type="text/javascript" src="{{url_for('static', filename='js/jquery.js') }}"></script> <script type="text/javascript" src="{{url_for('static', filename='js/jquery.js') }}"></script>
<script type="text/javascript" src="{{url_for('static', filename='js/bootstrap.min.js') }}"></script> <script type="text/javascript" src="{{url_for('static', filename='js/bootstrap.min.js') }}"></script>
<script type="text/javascript" src="{{url_for('static', filename='js/jquery.dataTables.js') }}"></script> <script type="text/javascript" src="{{url_for('static', filename='js/jquery-impromptu.3.2.min.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}}"; request_args = "{{session.request_args}}";

View File

@ -3,7 +3,7 @@
{% for host in detail %} {% for host in detail %}
<h3> <h3>
{{host}}: {{command}} {{host}}: {{command}}
<small><a class="pull-right" href="/{{session.request_type|replace("_detail","")}}_bgpmap/{{session.hosts}}/{{session.proto}}?q={{session.request_args|urlencode}}">View the BGP map</a></small> <small><a class="pull-right" href="/{{session.request_type|replace("_detail","")}}_bgpmap/{{session.hosts}}/{{session.proto}}?q={{session.request_args}}">View the BGP map</a></small>
</h3> </h3>
{% if session.request_args != expression|replace("/32","")|replace("/128","") %} {% if session.request_args != expression|replace("/32","")|replace("/128","") %}
<i>DNS: <a href="/whois/{{session.request_args}}" class="whois">{{session.request_args}}</a> => <a href="/whois/{{ expression|replace("/32","")|replace("/128","") }}" class="whois">{{expression|replace("/32","")|replace("/128","")}}</a></i><br /> <i>DNS: <a href="/whois/{{session.request_args}}" class="whois">{{session.request_args}}</a> => <a href="/whois/{{ expression|replace("/32","")|replace("/128","") }}" class="whois">{{expression|replace("/32","")|replace("/128","")}}</a></i><br />

View File

@ -1,21 +1,14 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% block body %}
{% for host in summary %} {% for host in summary %}
<h3 style="float:left">{{host}}: {{command}}</h3> <h3>{{host}}: {{command}}</h3><br />
<table class="table table-striped table-bordered table-condensed table-summary"> <table class="table table-striped table-bordered table-condensed">
<thead> <thead>
<tr><th>Name</th><th>protocol</th><th>table</th><th>state</th><th>since</th><th>info</th></tr> <tr><th>Name</th><th>protocol</th><th>table</th><th>state</th><th>since</th><th>info</th></tr>
</thead> </thead>
<tbody> <tbody>
{% for row in summary[host] %} {% for row in summary[host] %}
<tr class="{{ loop.cycle('odd', 'even') }}"> <tr class="{{ loop.cycle('odd', 'even') }}"><td><a href="/detail/{{host}}/{{session.proto}}?q={{row.name}}">{{row.name}}</a></td><td>{{row.proto}}</td><td>{{row.table}}</td><td>{{row.state}}</td><td>{{row.since}}</td><td>{{row.info}}</td></tr>
<td><a href="/detail/{{host}}/{{session.proto}}?q={{row.name}}">{{row.name}}</a></td>
<td>{{row.proto}}</td>
<td>{{row.table}}</td>
<td><span class="label label-{% if row.state == "up" %}success{% elif row.state == "down" %}default{% else %}important{% endif %}">{{row.state}}</span></td>
<td>{{row.since}}</td>
<td>{{row.info}}</td>
</tr>
{% else %} {% else %}
<tr><td>{{summary[host].error}}</td><td></td><td></td><td></td><td></td><td></td></tr> <tr><td>{{summary[host].error}}</td><td></td><td></td><td></td><td></td><td></td></tr>
{% endfor %} {% endfor %}

View File

@ -22,25 +22,19 @@
from dns import resolver from dns import resolver
import socket import socket
import pickle import pickle
import xml.parsers.expat
dns_cache = resolver.LRUCache(max_size=10000)
resolv = resolver.Resolver()
resolv.timeout = 0.5
resolv.lifetime = 1
resolv.cache = dns_cache
def resolve(n, q): def resolve(n, q):
return str(resolv.query(n,q)[0]) return str(resolver.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:
@ -57,45 +51,19 @@ 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):
want_unicode = False
if isinstance(s, unicode):
s = s.encode("utf-8")
want_unicode = True
# the rest of this assumes that `s` is UTF-8
list = []
# create and initialize a parser object
p = xml.parsers.expat.ParserCreate("utf-8")
p.buffer_text = True
p.returns_unicode = want_unicode
p.CharacterDataHandler = list.append
# parse the data wrapped in a dummy element
# (needed so the "document" is well-formed)
p.Parse("<e>", 0)
p.Parse(s, 0)
p.Parse("</e>", 1)
# join the extracted strings and return
es = ""
if want_unicode:
es = u""
return es.join(list)