Compare commits

...

113 Commits

Author SHA1 Message Date
Joey Julian König 5e26f3c67a Update README.mkd
- Added lg.as49697.net
- Updated lg.meerfarbig.net to meerblick.io
- Deleted the not longer existent netshelter LG
2021-11-28 23:25:08 +01:00
Baptiste Jonglez 7f1c701e19 README: Improve installation / configuration doc 2021-06-06 19:41:50 +02:00
Baptiste Jonglez 22db058e99 Move config files to .cfg.example
This makes sure that the actual configuration is not tracked in git,
simplifying deployment.
2021-06-06 19:41:50 +02:00
Ryan Balandran c83e4842e9 Generalize requests without arguments in JS
This commit adds a list of request types that do not have
parameters. These reqeusts are handled different by the JS code, and
the list makes it easier to add new custom commands that do not have
arguments.
2021-06-06 19:40:19 +02:00
Italo Cunha 30add2e41b Allow setting log history size
BIRD-LG currently keeps log files indefinitely. This patch allows
setting the number of days logs should be kept for by adding
a `LOG_NUM_DAYS` parameter in the configuration files.

This change is backwards compatible: Running the updated code with
old configuration files will keep logs indefinitely as before; new
deployments based on the example configuration will keep logs
indefinitely.
2021-06-06 19:29:51 +02:00
Baptiste Jonglez 3d90c14bfa Use dnspython built-in cache to cache DNS queries for BGPmap
This is equivalent to the previous memcached cache, but much simpler to
deploy (no additional service needed, everything is in process memory)
2021-05-10 23:03:37 +02:00
Baptiste Jonglez f41056a684 Remove the need for memcached
From a deployment perspective, it's annoying to depend on memcached, and
it's only used to cache DNS answers for the BGPmap.

Fixes #73
2021-05-10 23:03:37 +02:00
Baptiste Jonglez ef6b32c527 Fix XSS when handling query
Fixes #63
2021-05-10 23:03:14 +02:00
zorun 96249a36d0
Merge pull request #79 from fragtion/master
Added https://lg.angolacables.co.ao/ to Happy users (README.mkd)
2020-08-17 09:58:29 +02:00
Dimitri Pappas 73edb4ce1d
Added https://lg.angolacables.co.ao/ to README.mkd 2020-08-17 04:58:33 +02:00
Baptiste Jonglez 967c721b86 bgpmap: fix regexp to match interface names with non-alphanumerical characters
Commit eaf531eee2 ("Correctly parse aspath for both bird 1.x and 2.0.")
inadvertently made a regexp more strict, and it no longer matches some
interface names.  Example include `eth1.945` (tagged VLAN, which is a
common use-case for routers) or `br-foo`.

As a result, the regexp would not parse the "peer protocol name" that is
normally displayed on the bgpmap.

Fixes: #64
2020-06-16 15:43:03 +02:00
Baptiste Jonglez 53e717381d bgpmap: Fix crash when 'peer_protocol_name' is not matched by regexp
This protects against cases where the regexp expr2 is broken (such as
currently with some interface names)

Fixes: #64
2020-06-16 15:42:41 +02:00
Baptiste Jonglez c84267edd8 Clarify documentation of shared secret and access list 2020-06-15 22:54:01 +02:00
Baptiste Jonglez 1d3eefa971 Don't display URL containing shared secret when there is an error 2020-06-15 22:47:51 +02:00
Baptiste Jonglez a45ae45408 Basic python reformatting
Transform all tabs to spaces and remove trailing spaces.
2020-06-15 22:34:25 +02:00
Baptiste Jonglez ca4550ae74 Fix HTML markup (wrong title position) 2020-06-15 22:06:59 +02:00
zorun 8a98ca8b16
Merge pull request #70 from gmarsay/add-website-title
Add WEBSITE_TITLE - Issue #69
2020-06-15 22:01:09 +02:00
zorun 73ff7aa496
Merge pull request #71 from gmarsay/add-shared-secret
Add SHARED_SECRET
2020-06-15 22:00:06 +02:00
Guillaume Marsay f87719956a Add WEBSITE_TITLE - Issue #69 2020-06-15 17:35:42 +02:00
Guillaume Marsay 96c33da446 Add SHARED_SECRET 2020-06-15 17:33:50 +02:00
zorun 1e78b20860
Merge pull request #68 from gmarsay/optional-domain
DOMAIN is now optional
2020-06-15 11:31:33 +02:00
Guillaume Marsay d3e9d5c6b9 DOMAIN is now optional 2020-06-15 09:57:59 +02:00
zorun 6f277c23cb
Merge pull request #60 from vidister/master
Add shebang to lgproxy.py
2020-06-14 18:42:35 +02:00
zorun 5138d655c8
Merge pull request #54 from empi89/feature/config-file-as-argument
specify config file as cli argument
2020-06-14 18:04:12 +02:00
İlteriş Eroğlu 95fd862388 Added css to whois modal to handle newlines 2020-04-30 11:29:51 +02:00
İlteriş Eroğlu 9d9d93805e Removed br tag replacer on whois endpoint 2020-04-30 11:29:51 +02:00
vidister 1a13a41db7 Add shebang to lgproxy.py 2020-04-29 14:01:45 +02:00
zorun e5a866b89b
Merge pull request #57 from empi89/feature/debug-true-should-not-be-default
DEBUG should be disabled as default
2020-03-02 12:10:15 +01:00
zorun 1abe3f410f
Merge pull request #55 from empi89/feature/add-direct-to-unwanted-protos
add direct to SUMMARY_UNWANTED_PROTOS
2020-03-02 12:09:41 +01:00
zorun d8c20f4cbd
Merge pull request #53 from tamihiro/new-pr-batch-5
Correct AS path prepend count in bgpmap.
2020-03-02 12:05:19 +01:00
zorun b448784a0b
Merge pull request #51 from tamihiro/new-pr-batch-3
Correctly display each of multiple destination prefixes with a box shape.
2020-03-02 12:04:27 +01:00
zorun 93ed0769fe
Merge pull request #49 from tamihiro/new-pr-batch
Correctly parse aspath for both bird 1.x and 2.0.
2020-03-02 12:03:25 +01:00
zorun 83a9504695
Merge pull request #58 from netfreak98/patch-1
Update README.mkd
2020-03-02 12:00:01 +01:00
netfreak98 86c4cf2965
Update README.mkd
Added our LG to the list (AS49697) :)
2020-03-01 19:57:01 +01:00
Peter Hansen 307a96d063 direct is also an protocol that should be not shown 2019-12-28 11:37:05 +01:00
Peter Hansen 5ef67f5959 DEBUG should be disabled as default 2019-12-28 11:33:53 +01:00
Peter Hansen 71226a4ed3 specify config file as cli argument 2019-12-28 11:32:00 +01:00
zorun fa1c071682
Merge pull request #52 from tamihiro/new-pr-batch-4
Encode `+` when it appears in URL.
2019-08-28 10:57:20 +02:00
zorun 0e9dcd7946
Merge pull request #50 from tamihiro/new-pr-batch-2
Prevent 4-byte AS capability from being treated like it is ASN
2019-08-28 10:53:42 +02:00
tamihiro 648197da24 Correct AS path prepend count in bgpmap.
Without this patch, show_bgpmap() method does duplicate counts of aspath prepends, and returns a larger prepend count than it actually is.
2019-06-09 21:41:35 +09:00
tamihiro 1f020cdc40 Encode `+` when it appears in URL.
This patch takes care of encoding `+` within URL, such as `View the BGP map` link on the output of `show route where net ~ [ <prefix> + ]` response.
2019-06-09 21:39:21 +09:00
tamihiro 2c0d5ac273 Correctly display each of multiple destination prefixes with a box shape.
Without this patch, `show route where net ~ [ prefix+ ] (bgpmap)` erroneously returns only one prefix with a box shape, and all the other prefixes with a oval shape as if they were multipath intermediate ASes.
2019-06-09 21:35:59 +09:00
tamihiro 822d54cd3d Prevent 4-byte AS capability from being treated like it is ASN
Bird displays neighbor's 4-byte AS capability as `AS4`.
This patch prevents it from being treated as an clickable ASN in the output of `show protocols <protocol_name> all` response.
2019-06-09 21:31:38 +09:00
tamihiro eaf531eee2 Correctly parse aspath for both bird 1.x and 2.0.
Due to the change in the output of `show route for <prefix> all` since bird-2.0, this patch is necessary to correctly parse aspaths.
Without this patch, `build_as_tree_from_raw_bird_ouput()` fails with the following exception if the backend is bird-2.0.

```
Traceback (most recent call last):
  File "/usr/lib/python2.7/dist-packages/flask/app.py", line 1836, in __call__
    return self.wsgi_app(environ, start_response)
  File "/usr/lib/python2.7/dist-packages/flask/app.py", line 1820, in wsgi_app
    response = self.make_response(self.handle_exception(e))
  File "/usr/lib/python2.7/dist-packages/flask/app.py", line 1403, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/lib/python2.7/dist-packages/flask/app.py", line 1817, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/lib/python2.7/dist-packages/flask/app.py", line 1477, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/lib/python2.7/dist-packages/flask/app.py", line 1381, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/lib/python2.7/dist-packages/flask/app.py", line 1475, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/lib/python2.7/dist-packages/flask/app.py", line 1461, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "./lg.py", line 522, in show_bgpmap
    response = Response(graph.create_png(), mimetype='image/png')
  File "/usr/lib/python2.7/dist-packages/pydot.py", line 1735, in <lambda>
    lambda f=frmt, prog=self.prog: self.create(format=f, prog=prog)
  File "/usr/lib/python2.7/dist-packages/pydot.py", line 1905, in create
    self.write(tmp_name)
  File "/usr/lib/python2.7/dist-packages/pydot.py", line 1830, in write
    data = self.to_string()
  File "/usr/lib/python2.7/dist-packages/pydot.py", line 1600, in to_string
    graph.append(node.to_string() + '\n')
  File "/usr/lib/python2.7/dist-packages/pydot.py", line 865, in to_string
    node += ' [' + node_attr + ']'
TypeError: unsupported operand type(s) for +=: 'NoneType' and 'str'
```

Consequently, `show route for <prefix> (bgpmap)` shows an error icon.
2019-06-09 18:35:20 +09:00
zorun 85963901f8
Merge pull request #48 from alarig/master
Applying patch from @fduraffourg that fixes #9
2019-06-07 01:25:27 +02:00
Alarig Le Lay 50b1f1387e Applying patch from @fduraffourg that fixes #9 2019-04-12 19:34:07 +02:00
zorun 2ce03ebbb0
Merge pull request #38 from zorun/fix_protocol_parsing
Simplify parsing of 'show protocols' to avoid future breakages
2019-03-19 14:42:16 +01:00
Samuel Trommel 58c1b01719 Add lg.worldstream.nl to README.mkd 2018-09-10 10:30:05 +02:00
Baptiste Jonglez 0e45d90b70 Simplify parsing of 'show protocols' to avoid future breakages
The current parsing method (ugly regexp) is hard to understand and prone
to breakage:

- Bird 1.4 changed the date format, which broke the parser ()
- Bird 2.0 changed the format of the "Table" field, which again broke the parser (#36)

The new method is much simpler, does not involve any regexp, and should
thus resist small syntax changes in Bird's output.

Important limitation: parsing will be messed up if the date contains a
space character.  There is no space in the default date format of Bird
(checked with Bird 1.3 to Bird 2.0), but since the date format is
configurable in Bird, it may happen anyway.  In particular, setting
"timeformat protocol iso long" in Bird will break bird-lg's parser.

Fixes #36
2018-08-22 12:46:21 +02:00
Consorci de Serveis Universitaris de Catalunya 1f0efcaa2c Add more happy users 2018-06-26 11:01:40 +02:00
zorun 6db7c78a31
Merge pull request #33 from zorun/happy_users
Add more happy users
2018-05-26 11:29:49 +02:00
zorun 1923aeea9d
Merge pull request #32 from zorun/init_systemd
Add example systemd unit files, courtesy of ARN
2018-05-26 11:29:34 +02:00
Baptiste Jonglez 4f4c903c05 Add example systemd unit files, courtesy of ARN 2018-05-26 11:28:53 +02:00
Baptiste Jonglez 38e365c6a0 Add more happy users 2018-05-26 00:48:13 +02:00
Stefan a3babe00d9 bind lgproxy.py optionally to an IP
Add the options
```BIND_IP = "0.0.0.0"
BIND_PORT = 5000```
which allows to bind lgproxy.py to an IP/interface.

As ports on routers shouldn't be exposed too much, it probably also makes sense to others to have this function. Tested.
2018-04-07 12:18:09 +02:00
Maikel de Boer a745a13267 Fixed requirements in README. 2017-10-30 19:15:21 +01:00
Mehdi ABAAKOUK 5929afb5ec
Merge pull request #28 from loopodoopo/master
fixed SUMMARY_RE_MATCH to work with uptime longer then a day.
2017-10-30 19:15:02 +01:00
Maikel de Boer 53d2000863 fixed SUMMARY_RE_MATCH to work with uptime longer then a day. 2017-10-30 13:17:23 +01:00
Mehdi ABAAKOUK 92be35bdc8 Merge pull request #27 from oszafraniec/master
'NoneType' object has no attribute 'split'
2017-10-26 15:36:58 +02:00
Oskar Szafraniec 3f31a86d6a 'NoneType' object has no attribute 'split' 2017-10-26 13:10:08 +02:00
root 93124adf1b fix uptime parsing 2017-09-19 21:15:15 +02:00
Mehdi Abaakouk ed7494b14b fix fmt args 2017-08-16 22:44:23 +02:00
Mehdi Abaakouk 2342607092 Allow get svg 2017-08-16 22:40:07 +02:00
Mehdi Abaakouk 6dfd9f7546 Use base64 for bgpmap 2017-08-16 22:40:07 +02:00
Mehdi Abaakouk c01c5c765f Update COPYING 2017-08-04 15:28:01 +02:00
Mehdi Abaakouk 5a19d60697 Remove obsolete bird version 2017-08-04 15:04:57 +02:00
Mehdi Abaakouk f1ba5747e5 Add arn 2017-08-04 15:02:14 +02:00
Mehdi Abaakouk 0a9c6ea98b Add some link examples 2017-08-04 15:00:07 +02:00
Mehdi ABAAKOUK a6b3a84080 Merge pull request #12 from rodecker/master
Specify bird socket file location in config
2016-10-09 07:30:17 +02:00
Mehdi ABAAKOUK a849ede683 Merge pull request #13 from rodecker/upstream
peer uptime is either date or time
2016-10-09 07:29:22 +02:00
root f0058c6a41 Add prepend info on graph 2016-03-02 09:46:51 +01:00
Martin Pels 417912fca8 peer uptime is either date or time 2015-09-08 11:54:46 +02:00
Martin Pels b458f2dcc3 peer uptime is either date or time 2015-09-08 11:47:17 +02:00
Martin Pels 9de7dee49c Specify bird socket file location in config 2015-09-08 11:46:07 +02:00
Mehdi Abaakouk 6a7bd7f228 Allow configure asn cache expiration 2014-04-20 17:20:52 +02:00
Mehdi Abaakouk 9822e42e25 Add resolv timeout, store asn in memcache 2014-04-20 17:02:22 +02:00
Mehdi Abaakouk af2c305049 Merge remote-tracking branch 'zorun/master' 2014-03-18 14:09:36 +01:00
Mehdi Abaakouk cbdaa9138c Change lg-proxy to lgproxy to easly import it in wsgi part2 2014-02-18 11:57:44 +01:00
Mehdi Abaakouk 63e978f84f Change lg-proxy to lgproxy to easly import it in wsgi 2014-02-18 11:52:43 +01:00
Baptiste Jonglez ffafef27cd Allow to configure the bind address of bird-lg 2014-02-12 22:09:09 +01:00
Baptiste Jonglez fe9a7f8fe4 Use traceroute{,6} on BSD instead of traceroute -{4,6} 2014-01-28 17:01:47 +01:00
Baptiste Jonglez 5cb45d5785 Fix commit e75842b0 (typo) 2014-01-28 16:44:32 +01:00
Baptiste Jonglez e75842b025 Fix traceroute options for FreeBSD, OpenBSD, NetBSD 2014-01-28 16:33:19 +01:00
Baptiste Jonglez 317de87866 Don't hardcode tetaneutral.net in the site title 2014-01-28 16:17:28 +01:00
Baptiste Jonglez fe4e8caf2f Fix bgpmap (Graphviz does not seem to like empty labels) 2014-01-28 15:49:15 +01:00
Baptiste Jonglez e557dd651b Catch possible exceptions thrown by the ASN DNS resolver 2014-01-28 15:49:15 +01:00
Baptiste Jonglez 2418d13d07 Add support for configuring the DNS-based ASN → name mapping service. 2014-01-28 15:49:15 +01:00
Baptiste Jonglez ca7eb2b9ac Add support for a configurable whois server 2014-01-28 15:46:27 +01:00
Mehdi ABAAKOUK 6b7dfbb7a9 Merge pull request #7 from benpro/patch-1
Fix ASCII schema and some sentences.
2013-08-20 01:11:43 -07:00
Benoît.S 3fbdba357d Fix ASCII schema and some sentences. 2013-08-20 09:58:40 +02:00
Mehdi Abaakouk 1d20093048 Add access log 2012-11-27 20:05:54 +01:00
Mehdi Abaakouk 298b1459fa Handle error correctly 2012-10-17 16:33:02 +02:00
Mehdi Abaakouk 6311df95e8 Rewrite the way that user input is handled 2012-10-17 16:17:52 +02:00
Mehdi Abaakouk 5bb5d42d0f Fix index error in last commit 2012-10-17 11:36:36 +02:00
Mehdi Abaakouk c22e73c7f8 Fix error when sanitized have only 1 argument 2012-10-17 11:32:33 +02:00
Mehdi Abaakouk 8419152668 Fix issue #2 2012-10-16 08:07:30 +02:00
Mehdi Abaakouk b593b0cfb7 escape some string 2012-10-16 08:01:10 +02:00
Mehdi Abaakouk f715dcfeaf Enable log 2012-10-16 08:01:10 +02:00
Mehdi Abaakouk 82b1569dd4 use current file name 2012-08-21 11:55:16 +02:00
Mehdi Abaakouk 17d11c0ec4 Add logging system 2012-08-21 11:12:58 +02:00
Mehdi Abaakouk f30939f764 Detect site path 2012-08-21 09:28:08 +02:00
Mehdi Abaakouk ea5563fe81 Move debug boolean to configuration file 2012-08-21 09:21:14 +02:00
Mehdi Abaakouk 9ca18eff4a Made summary sortable 2012-08-10 18:36:09 +02:00
Mehdi Abaakouk 730d1dcffd Merge branch 'master' of git://github.com/sileht/bird-lg 2012-07-27 23:12:27 +02:00
Mehdi Abaakouk 725b8d3a3d basic caracter escape for pydot html label 2012-07-27 23:12:11 +02:00
Mehdi ABAAKOUK 892201f764 Merge pull request #1 from job/patch-1
added dependency to python-pydot
2012-07-25 00:08:45 -07:00
Job Snijders dde6dbabce added dependency 2012-07-21 18:37:11 +03:00
Mehdi Abaakouk af5faf98c1 fix incorrect var name 2012-07-20 17:52:06 +02:00
Mehdi Abaakouk cb6bb4dd31 Add color to peer status 2012-07-20 14:00:20 +02:00
Mehdi Abaakouk 3905d7d0ac Remove duplicate label on a edge 2012-07-19 17:28:32 +02:00
Mehdi Abaakouk 63a6f32d5f Use service cymru.com service instead of whois 2012-07-18 23:46:44 +02:00
Mehdi Abaakouk d148daf4e2 Fix exception when bgpmap can not be generated 2012-07-16 08:09:17 +02:00
Mehdi Abaakouk 1474ff90aa Allow html string in error/warning messages 2012-07-16 08:08:47 +02:00
30 changed files with 13077 additions and 406 deletions

2
.gitignore vendored
View File

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

View File

@ -3,9 +3,9 @@
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
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,
but WITHOUT ANY WARRANTY; without even the implied warranty of
@ -13,5 +13,4 @@
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
along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@ -1,42 +1,116 @@
BIRD-LG
=======
this is a looking glass for the Internet Routing Daemon "Bird"
Overview
--------
This is a looking glass for the Internet Routing Daemon "Bird".
software is splited onto two parts:
- lg-proxy.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)
Software is split in two parts:
- 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.
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:
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.
```
***************
+--> * lg-proxy.py *
+--> * lgproxy.py *
| ***************
|
******** ******************* | ***************
* USER * ----> * webserver/lg.py *--+--> * lg-proxy.py *
* USER * ----> * webserver/lg.py *--+--> * lgproxy.py *
******** ******************* | ***************
|
| ***************
+--> * lg-proxy.py *
+--> * lgproxy.py *
***************
```
bird-lg depend on :
Installation
------------
The web service (lg.py) depends on:
- python-flask >= 0.8
- python-dnspython
- python-pydot
- graphviz
- whois
Each service can by embeded in any webserver by follow regular python-flask configuration
The proxy running on routers (lgproxy.py) depends on:
Only tested with bird 1.2.5
- python-flask >= 0.8
- traceroute
- ping
source code under GPL 3.0, powered by Flask, jQuery and Bootstrap
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:
Copyright (c) 2012 Mehdi Abaakouk <sileht@sileht.net>
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.
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,153 +25,150 @@ import sys
BUFSIZE = 4096
SUCCESS_CODES = {
"0000" : "OK",
"0001" : "Welcome",
"0002" : "Reading configuration",
"0003" : "Reconfigured",
"0004" : "Reconfiguration in progress",
"0005" : "Reconfiguration already in progress, queueing",
"0006" : "Reconfiguration ignored, shutting down",
"0007" : "Shutdown ordered",
"0008" : "Already disabled",
"0009" : "Disabled",
"0010" : "Already enabled",
"0011" : "Enabled",
"0012" : "Restarted",
"0013" : "Status report",
"0014" : "Route count",
"0015" : "Reloading",
"0016" : "Access restricted",
"0000" : "OK",
"0001" : "Welcome",
"0002" : "Reading configuration",
"0003" : "Reconfigured",
"0004" : "Reconfiguration in progress",
"0005" : "Reconfiguration already in progress, queueing",
"0006" : "Reconfiguration ignored, shutting down",
"0007" : "Shutdown ordered",
"0008" : "Already disabled",
"0009" : "Disabled",
"0010" : "Already enabled",
"0011" : "Enabled",
"0012" : "Restarted",
"0013" : "Status report",
"0014" : "Route count",
"0015" : "Reloading",
"0016" : "Access restricted",
}
TABLES_ENTRY_CODES = {
"1000" : "BIRD version",
"1001" : "Interface list",
"1002" : "Protocol list",
"1003" : "Interface address",
"1004" : "Interface flags",
"1005" : "Interface summary",
"1006" : "Protocol details",
"1007" : "Route list",
"1008" : "Route details",
"1009" : "Static route list",
"1010" : "Symbol list",
"1011" : "Uptime",
"1012" : "Route extended attribute list",
"1013" : "Show ospf neighbors",
"1014" : "Show ospf",
"1015" : "Show ospf interface",
"1016" : "Show ospf state/topology",
"1017" : "Show ospf lsadb",
"1018" : "Show memory",
"1000" : "BIRD version",
"1001" : "Interface list",
"1002" : "Protocol list",
"1003" : "Interface address",
"1004" : "Interface flags",
"1005" : "Interface summary",
"1006" : "Protocol details",
"1007" : "Route list",
"1008" : "Route details",
"1009" : "Static route list",
"1010" : "Symbol list",
"1011" : "Uptime",
"1012" : "Route extended attribute list",
"1013" : "Show ospf neighbors",
"1014" : "Show ospf",
"1015" : "Show ospf interface",
"1016" : "Show ospf state/topology",
"1017" : "Show ospf lsadb",
"1018" : "Show memory",
}
ERROR_CODES = {
"8000" : "Reply too long",
"8001" : "Route not found",
"8002" : "Configuration file error",
"8003" : "No protocols match",
"8004" : "Stopped due to reconfiguration",
"8005" : "Protocol is down => cannot dump",
"8006" : "Reload failed",
"8007" : "Access denied",
"8000" : "Reply too long",
"8001" : "Route not found",
"8002" : "Configuration file error",
"8003" : "No protocols match",
"8004" : "Stopped due to reconfiguration",
"8005" : "Protocol is down => cannot dump",
"8006" : "Reload failed",
"8007" : "Access denied",
"9000" : "Command too long",
"9001" : "Parse error",
"9002" : "Invalid symbol type",
"9000" : "Command too long",
"9001" : "Parse error",
"9002" : "Invalid symbol type",
}
END_CODES = ERROR_CODES.keys() + SUCCESS_CODES.keys()
global bird_sockets
global bird_sockets
bird_sockets = {}
def BirdSocketSingleton(host, port):
global bird_sockets
s = bird_sockets.get((host,port), None)
if not s:
s = BirdSocket(host,port)
bird_sockets[(host,port)] = s
return s
global bird_sockets
s = bird_sockets.get((host,port), None)
if not s:
s = BirdSocket(host,port)
bird_sockets[(host,port)] = s
return s
class BirdSocket:
def __init__(self, host="", port="", file=""):
self.__file = file
self.__host = host
self.__port = port
self.__sock = None
def __init__(self, host="", port="", file=""):
self.__file = file
self.__host = host
self.__port = port
self.__sock = None
def __connect(self):
if self.__sock: return
def __connect(self):
if self.__sock: return
if not file:
self.__sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.__sock.settimeout(3.0)
self.__sock.connect((self.__host, self.__port))
else:
self.__sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.__sock.settimeout(3.0)
self.__sock.connect(self.__file)
if not file:
self.__sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.__sock.settimeout(3.0)
self.__sock.connect((self.__host, self.__port))
else:
self.__sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.__sock.settimeout(3.0)
self.__sock.connect(self.__file)
# read welcome message
self.__sock.recv(1024)
self.cmd("restrict")
# read welcome message
self.__sock.recv(1024)
self.cmd("restrict")
def close(self):
if self.__sock:
try: self.__sock.close()
except: pass
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 close(self):
if self.__sock:
try: self.__sock.close()
except: pass
self.__sock = None
def __read(self):
code = "7000" # Not used in bird
parsed_string = ""
lastline = ""
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
while code not in END_CODES:
data = self.__sock.recv(BUFSIZE)
lines = (lastline + data).split("\n")
if len(data) == BUFSIZE:
lastline = lines[-1]
lines = lines[:-1]
def __read(self):
code = "7000" # Not used in bird
parsed_string = ""
lastline = ""
for line in lines:
code = line[0:4]
while code not in END_CODES:
data = self.__sock.recv(BUFSIZE)
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
lines = (lastline + data).split("\n")
if len(data) == BUFSIZE:
lastline = lines[-1]
lines = lines[:-1]
return True, parsed_string
__all__ = ['BirdSocketSingleton' , 'BirdSocket' ]
for line in lines:
code = line[0:4]
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']

18
init/README.md Normal file
View File

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

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

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

View File

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

View File

@ -1,84 +0,0 @@
# -*- 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
View File

@ -1,20 +0,0 @@
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'

42
lg.cfg.example Normal file
View File

@ -0,0 +1,42 @@
# 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 Executable file → Normal file
View File

@ -20,20 +20,45 @@
#
###
import base64
from datetime import datetime
import subprocess
import logging
from logging.handlers import TimedRotatingFileHandler
import re
from urllib2 import urlopen
from urllib import quote, unquote
import json
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
from flask import Flask, render_template, jsonify, redirect, session, request, abort, Response
from flask import Flask, render_template, jsonify, redirect, session, request, abort, Response, Markup
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.config.from_pyfile('lg.cfg')
app.config.from_pyfile(args.config_file)
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):
@ -48,14 +73,17 @@ def add_links(text):
# Some heuristic to create link
if line.strip().startswith("BGP.as_path:") or \
line.strip().startswith("Neighbor AS:"):
ret_text.append(re.sub(r'(\d+)', r'<a href="/whois/\1" class="whois">\1</a>', line))
ret_text.append(re.sub(r'(\d+)', r'<a href="/whois?q=\1" class="whois">\1</a>', line))
else:
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/\1" class="whois">AS\1</a>', line)
line = re.sub(r'(\d+\.\d+\.\d+\.\d+)', r'<a href="/whois/\1" class="whois">\1</a>', line)
hosts = "/".join(request.path.split("/")[2:])
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'(?<=\[)AS(\d+)', r'<a href="/whois?q=\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)
if len(request.path) >= 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'(^|\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)
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)
ret_text.append(line)
return "\n".join(ret_text)
@ -83,7 +111,10 @@ def set_session(request_type, hosts, proto, request_args):
def whois_command(query):
return subprocess.Popen(['whois', query], stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'ignore')
server = []
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):
@ -92,9 +123,9 @@ def bird_command(host, proto, query):
def bird_proxy(host, proto, service, query):
"""Retreive data of a service from a running lg-proxy on a remote node
"""Retreive data of a service from a running lgproxy on a remote node
First and second arguments are the node and the port of the running lg-proxy
First and second arguments are the node and the port of the running lgproxy
Third argument is the service, can be "traceroute" or "bird"
Last argument, the query to pass to the service
@ -109,18 +140,29 @@ def bird_proxy(host, proto, service, query):
port = app.config["PROXY"].get(host, "")
if not port or not path:
return False, "Host/Proto not allowed"
else:
url = "http://%s.%s:%d/%s?q=%s" % (host, app.config["DOMAIN"], port, path, quote(query))
try:
f = urlopen(url)
resultat = f.read()
status = True # retreive remote status
except IOError:
resultat = "Failed retreive url: %s" % url
status = False
return status, resultat
if not port:
return False, 'Host "%s" invalid' % host
elif not path:
return False, 'Proto "%s" invalid' % proto
url = "http://%s" % (host)
if "DOMAIN" in app.config:
url = "%s.%s" % (url, app.config["DOMAIN"])
url = "%s:%d/%s?" % (url, port, path)
if "SHARED_SECRET" in app.config:
url = "%ssecret=%s&" % (url, app.config["SHARED_SECRET"])
url = "%sq=%s" % (url, quote(query))
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
@ -155,22 +197,26 @@ def hello():
def error_page(text):
return render_template('error.html', error=text), 500
return render_template('error.html', errors=[text]), 500
@app.errorhandler(400)
def incorrect_request(e):
return render_template('error.html', warning="The server could not understand the request"), 400
return render_template('error.html', warnings=["The server could not understand the request"]), 400
@app.errorhandler(404)
def page_not_found(e):
return render_template('error.html', warning="The requested URL was not found on the server."), 404
return render_template('error.html', warnings=["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/<query>")
def whois(query):
if not query.strip():
@app.route("/whois")
def whois():
query = get_query()
if not query:
abort(400)
try:
@ -181,46 +227,59 @@ def whois(query):
if m:
query = query.groupdict()["domain"]
output = whois_command(query).replace("\n", "<br>")
output = whois_command(query)
return jsonify(output=output, title=query)
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>.*))"
SUMMARY_UNWANTED_PROTOS = ["Kernel", "Static", "Device", "Direct"]
@app.route("/summary/<hosts>")
@app.route("/summary/<hosts>/<proto>")
def summary(hosts, proto="ipv4"):
set_session("summary", hosts, proto, "")
command = "show protocols"
summary = {}
error = []
errors = []
for host in hosts.split("+"):
ret, res = bird_command(host, proto, command)
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)
summary[host] = data
else:
error.append("%s: bird command failed with error, %s" % (host, "\n".join(res)))
if ret is False:
errors.append("%s" % res)
continue
return render_template('summary.html', summary=summary, command=command, error="<br>".join(error))
if len(res) <= 1:
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>")
def detail(hosts, proto):
name = request.args.get('q', '').strip()
name = get_query()
if not name:
abort(400)
@ -228,21 +287,28 @@ def detail(hosts, proto):
command = "show protocols all %s" % name
detail = {}
error = []
errors = []
for host in hosts.split("+"):
ret, res = bird_command(host, proto, command)
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)))
return render_template('detail.html', detail=detail, command=command, error="<br>".join(error))
if ret is False:
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>")
def traceroute(hosts, proto):
q = request.args.get('q', '').strip()
q = get_query()
if not q:
abort(400)
@ -259,11 +325,17 @@ def traceroute(hosts, proto):
except:
return error_page("%s is unresolvable or invalid for %s" % (q, proto))
errors = []
infos = {}
for host in hosts.split("+"):
status, resultat = bird_proxy(host, proto, "traceroute", q)
if status is False:
errors.append("%s" % resultat)
continue
infos[host] = add_links(resultat)
return render_template('traceroute.html', infos=infos)
return render_template('traceroute.html', infos=infos, errors=errors)
@app.route("/adv/<hosts>/<proto>")
@ -306,31 +378,18 @@ def show_route_for_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):
"""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
"""Returns a string that contain the as number following by the as name
"""
if not _as:
return "AS?????"
if not _as.isdigit():
return _as.strip()
if _as not in ASNAME_CACHE:
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])
name = get_asn_from_as(_as)[-1].replace(" ", "\r", 1)
return "AS%s | %s" % (_as, name)
def get_as_number_from_protocol_name(host, proto, protocol):
ret, res = bird_command(host, proto, "show protocols all %s" % protocol)
@ -345,20 +404,28 @@ def get_as_number_from_protocol_name(host, proto, protocol):
def show_bgpmap():
"""return a bgp map in a png file, from the json tree in q argument"""
data = request.args.get('q', '').strip()
data = get_query()
if not data:
abort(400)
data = json.loads(unquote(data))
data = base64.b64decode(data)
data = json.loads(data)
graph = pydot.Dot('BGPMAP', graph_type='digraph')
nodes = {}
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):
if _as not in nodes:
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>>"
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>>"
nodes[_as] = pydot.Node(_as, style="filled", fontsize="10", **kwargs)
graph.add_node(nodes[_as])
return nodes[_as]
@ -374,11 +441,24 @@ def show_bgpmap():
edges[edge_tuple] = edge
elif "label" in kwargs and kwargs["label"]:
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]
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)
if as_number:
@ -386,9 +466,9 @@ def show_bgpmap():
edge = add_edge(as_number, nodes[host])
edge.set_color("red")
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()
for host, asmaps in data.iteritems():
first = True
@ -400,39 +480,64 @@ def show_bgpmap():
hop_label = ""
for _as in asmap:
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
if not hop:
hop = True
if _as not in hosts:
hop_label = _as
hop_label = _as
if first:
hop_label = hop_label + "*"
continue
else:
hop_label = ""
add_node(_as, fillcolor=(first and "#F5A9A9" or "white"))
edge = add_edge(nodes[previous_as], nodes[_as] , label=hop_label, fontsize="7")
if _as == asmap[-1]:
add_node(_as, fillcolor="#F5A9A9", shape="box", )
else:
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 = ""
if first:
if first or _as == asmap[-1]:
edge.set_style("bold")
edge.set_color("red")
elif edge.get_color() != "red":
elif edge.get_style() != "bold":
edge.set_style("dashed")
edge.set_color(color)
previous_as = _as
first = False
node = add_node(previous_as)
node.set_shape("box")
for _as in prepend_as:
for n in set([ n for h, d in prepend_as[_as].iteritems() for p, n in d.iteritems() ]):
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):
@ -441,21 +546,29 @@ def build_as_tree_from_raw_bird_ouput(host, proto, text):
path = None
paths = []
net_dest = None
peer_protocol_name = ""
for line in text:
line = line.strip()
expr = re.search(r'(.*)via\s+([0-9a-fA-F:\.]+)\s+on.*\[(\w+)\s+', line)
expr = re.search(r'(.*)unicast\s+\[(\w+)\s+', line)
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:
path.append(net_dest)
paths.append(path)
path = None
if expr.group(1).strip():
net_dest = expr.group(1).strip()
if expr2.group(1).strip():
net_dest = expr2.group(1).strip()
peer_ip = expr.group(2).strip()
peer_protocol_name = expr.group(3).strip()
peer_ip = expr2.group(2).strip()
if expr2.group(4):
peer_protocol_name = expr2.group(4).strip()
# Check if via line is a internal route
for rt_host, rt_ips in app.config["ROUTER_IP"].iteritems():
# Special case for internal routing
@ -467,9 +580,26 @@ def build_as_tree_from_raw_bird_ouput(host, proto, text):
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)))]
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:"):
path.extend(line.replace("BGP.as_path:", "").strip().split(" "))
ASes = line.replace("BGP.as_path:", "").strip().split(" ")
if path:
path.extend(ASes)
else:
path = ASes
if path:
path.append(net_dest)
paths.append(path)
@ -478,7 +608,7 @@ def build_as_tree_from_raw_bird_ouput(host, proto, text):
def show_route(request_type, hosts, proto):
expression = unquote(request.args.get('q', '')).strip()
expression = get_query()
if not expression:
abort(400)
@ -498,7 +628,7 @@ def show_route(request_type, hosts, proto):
command = "show route where net ~ [ " + expression + " ]" + all
else:
mask = ""
if len(expression.split("/")) > 1:
if len(expression.split("/")) == 2:
expression, mask = (expression.split("/"))
if not mask and proto == "ipv4":
@ -525,26 +655,29 @@ def show_route(request_type, hosts, proto):
command = "show route for " + expression + all
detail = {}
error = []
errors = []
for host in hosts.split("+"):
ret, res = bird_command(host, proto, command)
res = res.split("\n")
if len(res) > 1:
if bgpmap:
detail[host] = build_as_tree_from_raw_bird_ouput(host, proto, res)
else:
detail[host] = add_links(res)
if ret is False:
errors.append("%s" % res)
continue
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:
error.append("%s: bird command failed with error, %s" % (host, "\n".join(res)))
detail[host] = add_links(res)
if bgpmap:
detail = json.dumps(detail)
detail = base64.b64encode(json.dumps(detail))
return render_template((bgpmap and 'bgpmap.html' or 'route.html'), detail=detail, command=command, expression=expression, error="<br>".join(error))
return render_template((bgpmap and 'bgpmap.html' or 'route.html'), detail=detail, command=command, expression=expression, errors=errors)
app.secret_key = app.config["SESSION_KEY"]
app.debug = True
if __name__ == "__main__":
app.run("0.0.0.0")
app.run(app.config.get("BIND_IP", "0.0.0.0"), app.config.get("BIND_PORT", 5000))

View File

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

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"

122
lgproxy.py Normal file
View File

@ -0,0 +1,122 @@
#!/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))

8
lgproxy.wsgi Normal file
View File

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

@ -0,0 +1,47 @@
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;
}

BIN
static/img/sort_asc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
static/img/sort_both.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
static/img/sort_desc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

96
static/js/DT_bootstrap.js Normal file
View File

@ -0,0 +1,96 @@
/* 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');
}
}
}
}
} );

12092
static/js/jquery.dataTables.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,9 +1,13 @@
{% extends "layout.html" %}
{% 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>
{% 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 />
{% endif %}<br />
<a href="/bgpmap/?q={{detail}}"><img src="/bgpmap/?q={{detail}}" /></a>
<a href="/bgpmap/?killcache=1&q={{detail}}"><img src="/bgpmap/?q={{detail}}" /></a>
<br />
{% endblock %}

View File

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

View File

@ -3,7 +3,7 @@
{% for host in detail %}
<h3>
{{host}}: {{command}}
<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>
<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>
</h3>
{% 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 />

View File

@ -1,14 +1,21 @@
{% extends "layout.html" %}
{% block body %}
{% for host in summary %}
<h3>{{host}}: {{command}}</h3><br />
<table class="table table-striped table-bordered table-condensed">
<h3 style="float:left">{{host}}: {{command}}</h3>
<table class="table table-striped table-bordered table-condensed table-summary">
<thead>
<tr><th>Name</th><th>protocol</th><th>table</th><th>state</th><th>since</th><th>info</th></tr>
</thead>
<tbody>
{% for row in summary[host] %}
<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>
<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><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 %}
<tr><td>{{summary[host].error}}</td><td></td><td></td><td></td><td></td><td></td></tr>
{% endfor %}

View File

@ -22,19 +22,25 @@
from dns import resolver
import socket
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):
return str(resolver.query(n,q)[0])
return str(resolv.query(n,q)[0])
def mask_is_valid(n):
if not n:
return True
try:
mask = int(n)
return ( mask >= 1 and mask <= 128)
except:
return False
if not n:
return True
try:
mask = int(n)
return ( mask >= 1 and mask <= 128)
except:
return False
def ipv4_is_valid(n):
try:
@ -51,19 +57,45 @@ def ipv6_is_valid(n):
return False
def save_cache_pickle(filename, data):
output = open(filename, 'wb')
pickle.dump(data, output)
output.close()
output = open(filename, 'wb')
pickle.dump(data, output)
output.close()
def load_cache_pickle(filename, default = None):
try:
pkl_file = open(filename, 'rb')
except IOError:
return default
try:
data = pickle.load(pkl_file)
except:
data = default
pkl_file.close()
return data
try:
pkl_file = open(filename, 'rb')
except IOError:
return default
try:
data = pickle.load(pkl_file)
except:
data = default
pkl_file.close()
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)