First Upload

This commit is contained in:
2024-11-06 19:09:33 +00:00
commit d971d5d522
22 changed files with 5606 additions and 0 deletions

63
README.md Normal file
View File

@@ -0,0 +1,63 @@
BGP Dashboard
=============
A "realtime" web view of your BGP network
- Who do I peer with?
- How many routes do I receive from my peers?
- Who do I use for tranist?
- What AS path does a prefix take out of my network?
- How many routes and autonomous systems do I see?
- BGP Looking Glass (IPv4/IPv6/ASN)
How it works
---------
> This is beta code.
- BGP peering session using GoBGP
- GoBGP pipes BGP information into MongoDB
- Flask App queries MongoDB to build website and JSON API
###### This project uses three Docker containers
- GoBGP ([osrg/gobgp](https://hub.docker.com/r/osrg/gobgp/))
- MongoDB ([mongo](https://hub.docker.com/_/mongo/))
- Flask ([docker-flask](https://hub.docker.com/r/p0bailey/docker-flask/))
###### GoBGP
The GoBGP container serves two functions:
- Peer with the "real" network
- Configure [gobgpd.conf](https://github.com/rhicks/bgp-dash/blob/master/gobgp/gobgpd.conf) to peer with the real network.
- Only IPv4-Unicast and IPv6-Unicast supported at this time.
- Pass BGP updates into BGP
- The [gobgp_to_mongo.py](https://github.com/rhicks/bgp-dash/blob/master/gobgp_to_mongo.py) script pipes the JSON updates from GoBGP into the MongoDB container
###### MongoDB
- Mongo receives JSON updates from the GoBGP container
- The Flask App queries Mongo for relevant information
###### Flask
- Flask presents a Dashboard for realtime BGP updates
- A JSON API is used on the backend to support the frontend and display Looking Glass queries
Screenshot
---------
![screenshot](bgp-dashboard.png)
Install
---------
```
$ git clone https://github.com/NetDevUG/bgp-dashboard.git
$ cd bgp-dashboard
$ # modify ./gobgp/gobgpd.conf to peer with your network
$ # modify ./flask/app/constants.py globals to use your ASN and BGP communities
$ docker compose build
$ docker compose up (watch the log to verify BGP peeering is established)
```
Todo
---------
- Update gobgp
- Update Python Dependencys

BIN
bgp-dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

33
bgp_attributes.py Normal file
View File

@@ -0,0 +1,33 @@
# BGP Attributes
PREFIX = 0
ORIGIN = 1
AS_PATH = 2
NEXT_HOP = 3
MULTI_EXIT_DISC = 4
LOCAL_PREF = 5
ATOMIC_AGGREGATE = 6
AGGREGATOR = 7
COMMUNITY = 8
ORIGINATOR_ID = 9
CLUSTER_LIST = 10
DPA = 11
ADVERTISER = 12
CLUSTER_ID = 13
MP_REACH_NLRI = 14
MP_UNREACH_NLRI = 15
EXTENDED_COMMUNITIES = 16
LARGE_COMMUNITIES = 32
#
WITHDRAWAL = 11
AGE = 12
#
ORIGIN_CODE = {
0: 'IGP',
1: 'EGP',
2: 'INCOMPLETE'
}
# BGP UPDATE CODES
OPEN = 1
UPDATE = 2
NOTIFICATION = 3
KEEPALIVE = 4

23
docker-compose.yml Executable file
View File

@@ -0,0 +1,23 @@
services:
gobgp:
build: ./gobgp
ports:
- "172.16.1.66:179:179"
volumes:
- .:/var/tmp
restart: always
mongodb:
image: mongo:latest
expose:
- 27017
volumes:
- ./db:/data/db
flask:
build: ./flask
ports:
- 80:80
volumes:
- ./flask/app:/var/www/app
- ./flask/app/log:/var/log/uwsgi/app

31
flask/Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
FROM ubuntu:24.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y \
python3 \
python3-pip \
python3-venv \
uwsgi-plugin-python3 \
nginx \
supervisor && \
echo 'Etc/UTC' >/etc/timezone && \
apt-get install -y --reinstall tzdata && \
rm -rf /var/lib/apt/lists/*
COPY nginx/flask.conf /etc/nginx/sites-available/
COPY supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY app/requirements.txt /tmp/requirements.txt
RUN python3 -m venv venv && chmod +x venv
RUN mkdir -p /var/log/nginx/app /var/log/uwsgi/app /var/log/supervisor /var/www/app && \
rm /etc/nginx/sites-enabled/default && \
ln -s /etc/nginx/sites-available/flask.conf /etc/nginx/sites-enabled/flask.conf && \
echo "daemon off;" >> /etc/nginx/nginx.conf && \
. venv/bin/activate && \
pip3 install -r /tmp/requirements.txt && \
chown -R www-data:www-data /var/www/app && \
chown -R www-data:www-data /var/log
CMD ["/usr/bin/supervisord"]

174
flask/app/Stats.py Normal file
View File

@@ -0,0 +1,174 @@
import constants as C
import dns.resolver
import time
from collections import Counter
from flask import jsonify
from itertools import islice
from pymongo import MongoClient
from functions import asn_name_query
class Stats(object):
def __init__(self):
self.db = self.db_connect()
self.peer_counter = 0
self.ipv4_table_size = 0
self.ipv6_table_size = 0
self.nexthop_ip_counter = 0
self.avg_as_path_length = 0
self.top_n_peers = []
self.cidr_breakdown = []
self.communities = []
self.peers = []
self.customers = []
self.customer_count = 0
self.customer_ipv4_prefixes = 0
self.customer_ipv6_prefixes = 0
self.timestamp = self.epoch_to_date(time.time())
# @property
# def peer_counter(self):
# return self._peer_counter
# @peer_counter.setter
# def peer_counter(self):
# self._peer_counter = len(self.db['bgp'].distinct('nexthop_asn', {'active': True}))
def db_connect(self):
"""Return a connection to the Mongo Database."""
client = MongoClient(host='mongodb')
return client.bgp
def take(self, n, iterable):
"""Return first n items of the iterable as a list."""
return list(islice(iterable, n))
def peer_count(self):
"""Return the number of directly connected ASNs."""
return len(self.db['bgp'].distinct('nexthop_asn', {'active': True}))
def prefix_count(self, version):
"""Given the IP version, return the number of prefixes in the database."""
return self.db['bgp'].count_documents({'ip_version': version, 'active': True})
def nexthop_ip_count(self):
"""Return the number of unique next hop IPv4 and IPv6 addresses."""
return len(self.db['bgp'].distinct('nexthop', {'active': True}))
def epoch_to_date(self, epoch):
"""Given an *epoch* time stamp, return a human readable equivalent."""
return time.strftime('%Y-%m-%d %H:%M:%S %Z', time.gmtime(epoch))
def get_list_of(self, customers=False, peers=False, community=C.CUSTOMER_BGP_COMMUNITY):
"""Return a list of prefix dictionaries. Specify which type of prefix to
return by setting *customers* or *peers* to True."""
if peers:
query_results = {prefix['nexthop_asn'] for prefix in self.db['bgp'].find({'active': True})}
else: # customers
query_results = {prefix['nexthop_asn'] for prefix in
self.db['bgp'].find({'communities': community, 'active': True})}
return [{'asn': asn if asn is not None else C.DEFAULT_ASN, # Set "None" ASNs to default
'name': asn_name_query(asn),
'ipv4_origin_count': self.db['bgp'].count_documents(
{'origin_asn': asn, 'ip_version': 4, 'active': True}),
'ipv6_origin_count': self.db['bgp'].count_documents(
{'origin_asn': asn, 'ip_version': 6, 'active': True}),
'ipv4_nexthop_count': self.db['bgp'].count_documents(
{'nexthop_asn': asn, 'ip_version': 4, 'active': True}),
'ipv6_nexthop_count': self.db['bgp'].count_documents(
{'nexthop_asn': asn, 'ip_version': 6, 'active': True}),
'asn_count': len(self.db['bgp'].distinct('as_path.1', {'nexthop_asn': asn, 'active': True}))}
for asn in query_results]
def avg_as_path_len(self, decimal_point_accuracy=2):
"""Return the computed average *as_path* length of all prefixes in the
database. Using a python *set* to remove any AS prepending."""
as_path_counter = 0
all_prefixes = list(self.db['bgp'].find({'active': True}))
for prefix in all_prefixes:
try:
as_path_counter += len(set(prefix['as_path'])) # sets remove duplicate ASN prepending
except Exception:
pass
return round(as_path_counter / (len(all_prefixes) * 1.0), decimal_point_accuracy)
def communities_count(self):
"""Return a list of BGP communities and their count"""
return [{'community': community,
# 'count': self.db['bgp'].count_documents({'communities': {'$regex': str(community)}, 'active': True}),
'count': self.db['bgp'].count_documents({'communities': str(community), 'active': True}),
'name': None if C.BGP_COMMUNITY_MAP.get(community) is None else C.BGP_COMMUNITY_MAP.get(community)}
for community in self.db['bgp'].distinct('communities') if community is not None]
def cidrs(self):
""" Return a list of IPv4 and IPv6 network mask counters."""
ipv4_masks = [int(prefix['_id'].split('/', 1)[1])
for prefix in self.db['bgp'].find({'ip_version': 4, 'active': True})]
ipv6_masks = [int(prefix['_id'].split('/', 1)[1])
for prefix in self.db['bgp'].find({'ip_version': 6, 'active': True})]
# Use a *Counter* to count masks in the lists, then combine, sort on mask, and return results
return sorted(
[{'mask': mask,
'count': count,
'ip_version': 4}
for mask, count in list(Counter(ipv4_masks).items())]
+
[{'mask': mask,
'count': count,
'ip_version': 6}
for mask, count in list(Counter(ipv6_masks).items())], key=lambda x: x['mask'])
def top_peers(self, count):
"""Return a sorted list of top peer dictionaries ordered by prefix count.
Limit to *count*."""
peers = {peer: self.db['bgp'].count_documents({'nexthop_asn': peer, 'active': True})
for peer in self.db['bgp'].distinct('nexthop_asn')}
return [{'asn': asn[0],
'count': asn[1],
'name': asn_name_query(asn[0])}
for asn in self.take(count, sorted(peers.items(), key=lambda x: x[1], reverse=True))]
def get_data(self, json=False):
data_dict = {
'peer_count': self.peer_counter,
'ipv6_table_size': self.ipv6_table_size,
'ipv4_table_size': self.ipv4_table_size,
'nexthop_ip_count': self.nexthop_ip_counter,
'avg_as_path_length': self.avg_as_path_length,
'top_n_peers': self.top_n_peers,
'cidr_breakdown': self.cidr_breakdown,
'communities': self.communities,
'peers': self.peers,
'customers': self.customers,
'customer_count': self.customer_count,
'customer_ipv4_prefixes': self.customer_ipv4_prefixes,
'customer_ipv6_prefixes': self.customer_ipv6_prefixes,
'timestamp': self.timestamp}
if json:
return jsonify(data_dict)
else:
return data_dict
def update_stats(self):
self.peer_counter = self.peer_count()
self.ipv4_table_size = self.prefix_count(4)
self.ipv6_table_size = self.prefix_count(6)
self.nexthop_ip_counter = self.nexthop_ip_count()
self.timestamp = self.epoch_to_date(time.time())
def update_advanced_stats(self):
self.avg_as_path_length = self.avg_as_path_len()
self.top_n_peers = self.top_peers(5)
self.cidr_breakdown = self.cidrs()
# self.customers = self.get_list_of(customers=True)
self.communities = self.communities_count()
self.customers = self.get_list_of(customers=True)
self.peers = self.get_list_of(peers=True)
self.customer_count = len(self.customers)
self.customer_ipv4_prefixes = 0
self.customer_ipv6_prefixes = 0
for customer in self.customers:
self.customer_ipv4_prefixes += customer['ipv4_origin_count']
self.customer_ipv6_prefixes += customer['ipv6_origin_count']
self.timestamp = self.epoch_to_date(time.time())

237
flask/app/bgp.py Normal file
View File

@@ -0,0 +1,237 @@
import threading
from flask import Flask, jsonify, render_template
import constants as C
from apscheduler.schedulers.background import BackgroundScheduler
from functions import (asn_name_query, get_ip_json, is_peer, is_transit,
reverse_dns_query, dns_query)
from Stats import Stats
app = Flask(__name__)
app.config['JSON_SORT_KEYS'] = False
app.config['JSONIFY_PRETTYPRINT_REGULAR'] = True
@app.route('/', methods=['GET'])
def bgp_index():
data = myStats.get_data()
top_peers = data['top_n_peers']
cidr_breakdown = data['cidr_breakdown']
communities = data['communities']
peers = data['peers']
source_asn = C.DEFAULT_ASN
source_asn_name = asn_name_query(C.DEFAULT_ASN)
customer_bgp_community = C.CUSTOMER_BGP_COMMUNITY
transit_bgp_community = C.TRANSIT_BGP_COMMUNITY
peer_bgp_community = C.PEER_BGP_COMMUNITY
return render_template('bgp.html', **locals())
@app.route('/bgp/api/v1.0/peers', methods=['GET'])
def get_peers():
return jsonify(myStats.get_list_of(peers=True))
@app.route('/bgp/api/v1.0/customers', methods=['GET'])
def get_customers():
return jsonify(myStats.get_list_of(customers=True))
@app.route('/bgp/api/v1.0/ip/<ip>', methods=['GET'])
def get_ip(ip):
return jsonify(get_ip_json(ip, include_history=False))
@app.route('/bgp/api/v1.0/communities', methods=['GET'])
def get_communities():
return jsonify(myStats.communities_count())
@app.route('/bgp/api/v1.0/ip/<ip>/history', methods=['GET'])
def get_history(ip):
return jsonify(get_ip_json(ip, include_history=True))
@app.route('/bgp/api/v1.0/asn/<int:asn>', methods=['GET'])
def get_asn_prefixes(asn):
db = myStats.db
prefixes = []
if asn == C.DEFAULT_ASN:
routes = list(db['bgp'].find({'origin_asn': None, 'active': True}))
else:
routes = list(db['bgp'].find({'origin_asn': asn, 'active': True}))
for prefix in routes:
prefixes.append({'prefix': prefix['_id'],
'is_transit': is_transit(prefix),
'origin_asn': prefix['origin_asn'],
'name': asn_name_query(asn),
'nexthop_ip': prefix['nexthop'],
'nexthop_ip_dns': reverse_dns_query(prefix['nexthop']),
'nexthop_asn': prefix['nexthop_asn'],
'as_path': prefix['as_path'],
'updated': prefix['age']
})
return jsonify({'asn': asn,
'name': asn_name_query(asn),
'origin_prefix_count': len(routes),
'is_peer': is_peer(asn),
'origin_prefix_list': prefixes})
@app.route('/bgp/api/v1.0/stats', methods=['GET'])
def get_stats():
return myStats.get_data(json=True)
@app.route('/bgp/api/v1.0/asn/<int:asn>/downstream', methods=['GET'])
def get_downstream_asns(asn):
db = myStats.db
asn_list = []
large_query = 200
downstream_asns = db['bgp'].distinct('as_path.1', {'nexthop_asn': asn, 'active': True})
for downstream in downstream_asns:
if len(downstream_asns) > large_query:
dns_name = "(LARGE QUERY - DNS LOOKUP DISABLED)"
else:
dns_name = asn_name_query(downstream)
asn_list.append({'asn': downstream, 'name': dns_name})
sorted_asn_list = sorted(asn_list, key=lambda k: k['asn'])
return jsonify({'asn': asn,
'name': asn_name_query(asn),
'downstream_asns_count': len(asn_list),
'downstream_asns': sorted_asn_list})
@app.route('/bgp/api/v1.0/asn/<int:asn>/originated', methods=['GET'])
def get_originated_prefixes(asn):
db = myStats.db
originated = []
prefixes = db['bgp'].find({'origin_asn': asn, 'active': True})
for prefix in prefixes:
originated.append(prefix['_id'])
return jsonify({'asn': asn,
'name': asn_name_query(asn),
'originated_prefix_count': len(originated),
'originated_prefix_list': originated})
@app.route('/bgp/api/v1.0/asn/<int:asn>/originated/<version>', methods=['GET'])
def get_originated_prefixes_version(asn, version):
db = myStats.db
originated = []
v = 4
if version.lower() == 'ipv6':
v = 6
prefixes = db['bgp'].find({'origin_asn': asn, 'ip_version': v, 'active': True})
for prefix in prefixes:
originated.append(prefix['_id'])
return jsonify({'asn': asn,
'name': asn_name_query(asn),
'originated_prefix_count': len(originated),
'originated_prefix_list': originated})
@app.route('/bgp/api/v1.0/asn/<int:asn>/nexthop', methods=['GET'])
def get_nexthop_prefixes(asn):
db = myStats.db
nexthop = []
prefixes = db['bgp'].find({'nexthop_asn': asn, 'active': True})
for prefix in prefixes:
nexthop.append(prefix['_id'])
return jsonify({'asn': asn,
'name': asn_name_query(asn),
'nexthop_prefix_count': len(nexthop),
'nexthop_prefix_list': nexthop})
@app.route('/bgp/api/v1.0/asn/<int:asn>/nexthop/<version>', methods=['GET'])
def get_nexthop_prefixes_version(asn, version):
db = myStats.db
nexthop = []
v = 4
if version.lower() == 'ipv6':
v = 6
prefixes = db['bgp'].find({'nexthop_asn': asn, 'ip_version': v, 'active': True})
for prefix in prefixes:
nexthop.append(prefix['_id'])
return jsonify({'asn': asn,
'name': asn_name_query(asn),
'nexthop_prefix_count': len(nexthop),
'nexthop_prefix_list': nexthop})
@app.route('/bgp/api/v1.0/asn/<int:asn>/transit', methods=['GET'])
def get_transit_prefixes(asn):
db = myStats.db
all_asns = db['bgp'].find({'active': True})
prefixes = []
for prefix in all_asns:
if prefix['as_path']:
if asn in prefix['as_path']:
prefixes.append(prefix['_id'])
else:
pass
else:
pass
return jsonify({'asn': asn,
'name': asn_name_query(asn),
'transit_prefix_count': len(prefixes),
'transit_prefix_list': prefixes})
@app.route('/bgp/api/v1.0/domain/<domain>', methods=['GET'])
def get_domain(domain):
domain = domain.lower()
org = domain.split('.')[-2]
name_servers = dns_query(domain, 'NS')
soa = dns_query(domain, 'SOA')
local_ns = ''
if org in soa.lower():
local_ns = soa.lower()
for ns in name_servers:
if org in ns.lower():
local_ns = ns.lower()
if local_ns == '':
return jsonify({})
else:
domain_ip = str(dns_query(local_ns))
ip_data = get_ip_json(domain_ip)
asn = ip_data.get('origin_asn')
db = myStats.db
originated = []
prefixes = db['bgp'].find({'origin_asn': asn, 'active': True})
for prefix in prefixes:
originated.append(prefix['_id'])
return jsonify({'domain': domain,
'A Record': dns_query(domain),
'SOA/NS Record': local_ns,
'SOA/NS IP': domain_ip,
'asn': asn,
'name': asn_name_query(asn),
'originated_prefix_count': len(originated),
'originated_prefix_list': originated})
sched = BackgroundScheduler()
myStats = Stats()
threading.Thread(target=myStats.update_stats).start()
threading.Thread(target=myStats.update_advanced_stats).start()
sched.add_job(myStats.update_stats, 'interval', seconds=60)
sched.add_job(myStats.update_advanced_stats, 'interval', seconds=300)
sched.start()
if __name__ == '__main__':
app.run(debug=True)

31
flask/app/constants.py Normal file
View File

@@ -0,0 +1,31 @@
# DEFAULTS - UPDATE ACCORDINGLY
DEFAULT_ASN = 400848
CUSTOMER_BGP_COMMUNITY = '3701:370' # Prefixes learned from directly connected customers
TRANSIT_BGP_COMMUNITY = '3701:380' # Prefixes learned from *paid* transit providers
PEER_BGP_COMMUNITY = '3701:39' # Community Prefix (starts with) for Prefixes learned from bilateral peers and exchanges
BGP_COMMUNITY_MAP = {
'3701:111': 'Level3-Prepend-1',
'3701:112': 'Level3-Prepend-2',
'3701:113': 'Level3-SEAT-Depref',
'3701:114': 'Level3-WSAC-Depref',
'3701:121': 'Level3-WSAC-Prepend-1',
'3701:122': 'Level3-WSAC-Prepend-2',
'3701:370': 'Customers',
'3701:371': 'Customers-NO-I2-RE',
'3701:372': 'Customers-NO-I2-CP',
'3701:380': 'Transit',
'3701:381': 'Level3-SEAT',
'3701:382': 'Level3-WSAC',
'3701:390': 'OIX',
'3701:391': 'I2-RE',
'3701:392': 'NWAX',
'3701:393': 'PNWGP',
'3701:394': 'I2-CPS',
'3701:395': 'SeattleIX',
'3701:500': 'PT-ODE-USERS',
'3701:501': 'PT-ODE-PROVIDERS',
'3701:666': 'BH-LOCAL',
'64496:0': 'Cymru-UTRS',
'65333:888': 'Cymru-BOGONs',
'65535:65281': 'No-Export',
}

145
flask/app/functions.py Normal file
View File

@@ -0,0 +1,145 @@
import ipaddress
from functools import cache
import dns.resolver
from pymongo.database import Database
import constants as C
from flask import jsonify, request
from pymongo import MongoClient
@cache
def db_connect() -> Database:
"""Return a connection to the Mongo Database."""
client = MongoClient(host='mongodb')
return client["bgp"]
def find_network(ip, netmask):
"""Given an IPv4 or IPv6 address, recursively search for and return the most
specific prefix in the MongoDB collection that is active.
"""
try:
db = db_connect()
network = str(ipaddress.ip_network(ipaddress.ip_address(ip)).supernet(new_prefix=netmask))
result = db['bgp'].find_one({'_id': network, 'active': True})
if result is not None:
return result
elif netmask == 0:
return None
else:
return find_network(ip, netmask - 1)
except Exception:
return None
def is_peer(asn):
"""Is *asn* in the list of directy connected ASNs."""
db = db_connect()
if asn in db['bgp'].distinct('nexthop_asn'):
return True
else:
return False
def is_transit(prefix, transit_bgp_community=C.TRANSIT_BGP_COMMUNITY):
"""Is the *prefix* counted as transit?"""
if C.TRANSIT_BGP_COMMUNITY in prefix['communities']:
return True
else:
return False
def reverse_dns_query(ip):
"""Given an *ip*, return the reverse dns."""
try:
addr = dns.reversename.from_address(str(ip))
resolver = dns.resolver.Resolver()
return str(resolver.resolve(addr, 'PTR')[0])[:-1]
except Exception:
return '(DNS Error)'
def dns_query(name, type='A'):
"""Given a *name*, return the ip dns."""
try:
# addr = dns.reversename.from_address(str(ip))
resolver = dns.resolver.Resolver()
answers = resolver.resolve(str(name), type)
if type == 'A':
return str(answers[0])
elif type == 'NS':
domains = []
for record in answers:
domains.append(str(record.target))
return domains
elif type == 'SOA':
return str(answers[0]).split()[0]
except Exception:
return '(DNS Error)'
def asn_name_query(asn):
"""Given an *asn*, return the name."""
if asn is None:
asn = C.DEFAULT_ASN
if 64496 <= asn <= 64511:
return ('RFC5398 - Private Use ASN')
if 64512 <= asn <= 65535 or 4200000000 <= asn <= 4294967295:
return ('RFC6996 - Private Use ASN')
try:
query = 'as{number}.asn.cymru.com'.format(number=str(asn))
resolver = dns.resolver.Resolver()
answers = resolver.resolve(query, 'TXT')
for rdata in answers:
return (str(rdata).split('|')[-1].split(',', 2)[0].strip())
except Exception:
return '(DNS Error)'
def get_ip_json(ip, include_history=True):
if '/' in ip:
ip = ip.lstrip().rstrip().split('/')[0]
try:
if ipaddress.ip_address(ip).version == 4:
network = find_network(ip, netmask=32)
elif ipaddress.ip_address(ip).version == 6:
network = find_network(ip, netmask=128)
except Exception:
try:
ipadr = dns_query(ip).strip()
if ipaddress.ip_address(ipadr).version == 4:
network = find_network(ipadr, netmask=32)
elif ipaddress.ip_address(ipadr).version == 6:
network = find_network(ipadr, netmask=128)
except Exception as e:
return jsonify(str(e))
if network:
if include_history:
history = network['history']
else:
history = request.base_url + '/history'
return {'prefix': network['_id'],
'ip_version': network['ip_version'],
'is_transit': is_transit(network),
'origin_asn': network['origin_asn'],
'name': asn_name_query(network['origin_asn']),
'nexthop': network['nexthop'],
'nexthop_ip_dns': reverse_dns_query(network['nexthop']),
'nexthop_asn': network['nexthop_asn'],
'as_path': network['as_path'],
'med': network['med'],
'local_pref': network['local_pref'],
'communities': network['communities'],
'route_origin': network['route_origin'],
'atomic_aggregate': network['atomic_aggregate'],
'aggregator_as': network['aggregator_as'],
'aggregator_address': network['aggregator_address'],
'originator_id': network['originator_id'],
'originator_id_dns': reverse_dns_query(network['originator_id']),
'cluster_list': network['cluster_list'],
'age': network['age'],
'history': history}
else:
return {}

View File

@@ -0,0 +1,9 @@
uwsgi >= 2.0.26
flask >= 3.0.3
requests >= 2.32.3
apscheduler >= 3.10.4
pytz >= 2024.1
pymongo >= 4.8.0
dnspython >= 2.6.1
ipaddress >= 1.0.23

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,569 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<!-- Meta, title, CSS, favicons, etc. -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BGP Dashboard</title>
<!-- Bootstrap -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
<!-- Custom Theme Style -->
<link href="../static/css/custom.css" rel="stylesheet">
</head>
<body class="nav-md">
<div class="container body">
<div class="main_container">
<!-- top navigation -->
<div class="top_nav">
<div class="nav_menu">
<nav>
<div class="navbar-left" style="padding-left: 30px;">
<h1 style="color: #34495e;">ASN{{source_asn}}</h1>
<h2><i>{{source_asn_name}}</i></h2>
</div>
<div class="title_right">
<div class="col-md-2 col-sm-2 col-xs-12 form-group pull-right top_search">
<form onsubmit="location.href='/bgp/api/v1.0/asn/' + document.getElementById('asnInput').value; return false;" class="input-group">
<input type="text" id="asnInput" class="form-control" placeholder="ASN Search...">
<span class="input-group-btn">
<button class="btn btn-default" type="submit">Go!</button>
</span>
</form>
</div>
</div>
<div class="title_right">
<div class="col-md-2 col-sm-2 col-xs-12 form-group pull-right top_search">
<form onsubmit="location.href='/bgp/api/v1.0/ip/' + document.getElementById('ipInput').value.split('/')[0]; return false;" class="input-group">
<input type="text" id="ipInput" class="form-control" placeholder="IP/DNS Search...">
<span class="input-group-btn">
<button class="btn btn-default" type="submit">Go!</button>
</span>
</form>
</div>
</div>
</nav>
</div>
</div>
<!-- /top navigation -->
<!-- page content -->
<div class="right_col" role="main">
<div class="">
<div class="row top_tiles">
<div class="animated flipInY col-lg-3 col-md-3 col-sm-6 col-xs-12">
<div class="tile-stats" id="peers_box">
<div class="icon"><i id="peersIDIcon"></i></div>
<div class="count" id="peersID">0</div>
<h3>Peer Count</h3>
<div>
<p class="count_bottom pull-right" id="peersIDChangeTime"></p>
<p>Active egress BGP peers</p>
</div>
<div class="clearfix"></div>
<p class="peers_count_sparkline pull-right" style="margin-right: 5px; margin-left: 5px;">Loading..</p>
</div>
</div>
<div class="animated flipInY col-lg-3 col-md-3 col-sm-6 col-xs-12">
<div class="tile-stats">
<div class="icon"><i id="ipv4TableSizeIcon"></i></div>
<div class="count" id="ipv4TableSize">0</div>
<h3>IPv4 Prefixes</h3>
<div>
<p class="count_bottom pull-right" id="ipv4TableSizeChangeTime"></p>
<p>IPv4 BGP Table Size</p>
</div>
<div class="clearfix"></div>
<p class="ipv4_table_sparkline pull-right" style="margin-right: 5px; margin-left: 5px;">Loading..</p>
</div>
</div>
<div class="animated flipInY col-lg-3 col-md-3 col-sm-6 col-xs-12">
<div class="tile-stats">
<div class="icon"><i id="ipv6TableSizeIcon"></i></div>
<div class="count" id="ipv6TableSize">0</div>
<h3>IPv6 Prefixes</h3>
<div>
<p class="count_bottom pull-right" id="ipv6TableSizeChangeTime"></p>
<p>IPv6 BGP Table Size</p>
</div>
<div class="clearfix"></div>
<p class="ipv6_table_sparkline pull-right" style="margin-right: 5px; margin-left: 5px;">Loading..</p>
</div>
</div>
<div class="animated flipInY col-lg-3 col-md-3 col-sm-6 col-xs-12">
<div class="tile-stats">
<div class="icon"><i id="nexthopIPCountIcon"></i></div>
<div class="count" id="nexthopIPCount">0</div>
<h3>Next Hop Addresses</h3>
<div>
<p class="count_bottom pull-right" id="nexthopIPCountChangeTime"></p>
<p>Unique Egress IP Addresses</p>
</div>
<div class="clearfix"></div>
<p class="nexthop_count_sparkline pull-right" style="margin-right: 5px; margin-left: 5px;">Loading..</p>
</div>
</div>
</div>
<!-- Second Row -->
<div class="row top_tiles">
<div class="animated flipInY col-lg-3 col-md-3 col-sm-6 col-xs-12">
<div class="tile-stats">
<div class="icon"><i id="avgAsPathLengthIcon"></i></div>
<div class="count" id="avgAsPathLength">0</div>
<h3>Average AS Path Length</h3>
<div>
<p class="count_bottom pull-right" id="avgAsPathLengthChangeTime"></p>
<p>ASN hops to destination (lower is better)</p>
</div>
<div class="clearfix"></div>
<p class="avg_as_path_length_sparkline pull-right" style="margin-right: 5px; margin-left: 5px;">Loading..</p>
</div>
</div>
<div class="animated flipInY col-lg-3 col-md-3 col-sm-6 col-xs-12">
<div class="tile-stats">
<div class="icon"><i id="customerIPv4PrefixesIcon"></i></div>
<div class="count" id="customerIPv4Prefixes">0</div>
<h3>Customer IPv4 Prefixes</h3>
<div>
<p class="count_bottom pull-right" id="customerIPv4PrefixesChangeTime"></p>
<p>Customer & Orginated IPv4 Prefix Count</p>
</div>
<div class="clearfix"></div>
<p class="customer_ipv4_prefixes_sparkline pull-right" style="margin-right: 5px; margin-left: 5px;">Loading..</p>
</div>
</div>
<div class="animated flipInY col-lg-3 col-md-3 col-sm-6 col-xs-12">
<div class="tile-stats">
<div class="icon"><i id="customerIPv6PrefixesIcon"></i></div>
<div class="count" id="customerIPv6Prefixes">0</div>
<h3>Customer IPv6 Prefixes</h3>
<div>
<p class="count_bottom pull-right" id="customerIPv6PrefixesChangeTime"></p>
<p>Customer & Orginated IPv6 Prefix Count</p>
</div>
<div class="clearfix"></div>
<p class="customer_ipv6_prefixes_sparkline pull-right" style="margin-right: 5px; margin-left: 5px;">Loading..</p>
</div>
</div>
<div class="animated flipInY col-lg-3 col-md-3 col-sm-6 col-xs-12">
<div class="tile-stats">
<div class="icon"><i id="customerCountIcon"></i></div>
<div class="count" id="customerCount">0</div>
<h3>BGP Customers</h3>
<div>
<p class="count_bottom pull-right" id="customerCountChangeTime"></p>
<p>Downstream BGP connected ASNs</p>
</div>
<div class="clearfix"></div>
<p class="customer_count_sparkline pull-right" style="margin-right: 5px; margin-left: 5px;">Loading..</p>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="x_panel">
<div class="x_title">
<h2>Peer Data <small>Updated every 300s</small></h2>
<div class="clearfix"></div>
</div>
<div class="x_content">
<div class="col-md-9 col-sm-12 col-xs-12">
<div class="demo-container">
<table id="datatable" class="table table-striped table-bordered">
<thead>
<tr>
<th>ASN</th>
<th>Name</th>
<th>IPv4 Prefixes<br>Originated</th>
<th>IPv4 Prefixes<br>NextHop</th>
<th>IPv6 Prefixes<br>Originated</th>
<th>IPv6 Prefixes<br>NextHop</th>
<th>Downstream<br>ASN Count</th>
</tr>
</thead>
</table>
</div>
</div>
<div class="col-md-3 col-sm-12 col-xs-12">
<div>
<div class="x_title">
<h2>Top Peers (Prefixes Received)</h2>
<div class="clearfix"></div>
</div>
<ul class="list-unstyled top_profiles scroll-view">
{% for peer in top_peers %}
<li class="media event">
<a class="pull-left border-blue profile_thumb">
<i class="fa fa-arrows-alt blue"></i>
</a>
<div class="media-body">
<a class="title" href="#">ASN {{ peer.asn }}</a>
<p><strong>{{ '{0:,}'.format(peer.count) }}</strong> Prefixes </p>
<p> <small>{{ peer.name }}</small>
</p>
</div>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="x_panel">
<div class="x_title">
<h2>BGP Prefix Distributions <small>Prefix Count</small></h2>
<div class="clearfix"></div>
</div>
<div class="x_content">
<div class="row" style="border-bottom: 0px solid #E0E0E0; padding-bottom: 5px; margin-bottom: 5px;">
<div class="col-md-12">
<div class="row" style="text-align: center;">
<div id="graph_donut1" class="col-md-4" style="width:50%; height:180px;">
<h4 style="margin:0">Peering Prefix Count</h4>
</div>
<div id="graph_donut2" class="col-md-4" style="width:50%; height:180px;">
<h4 style="margin:0">Transit vs. Peering</h4>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- bar chart -->
<div class="col-md-6 col-sm-6 col-xs-12">
<div class="x_panel">
<div class="x_title">
<h2>IPv4 Network Mask <small>Prefix Count</small></h2>
<div class="clearfix"></div>
</div>
<div class="x_content">
<div id="graph_bar" style="width:100%; height:280px;"></div>
</div>
</div>
</div>
<!-- /bar charts -->
<!-- bar charts group -->
<div class="col-md-6 col-sm-6 col-xs-12">
<div class="x_panel">
<div class="x_title">
<h2>IPv6 Network Mask <small>Prefix Count</small></h2>
<div class="clearfix"></div>
</div>
<div class="x_content1">
<div id="graph_bar_group" style="width:100%; height:280px;"></div>
</div>
</div>
</div>
<div class="clearfix"></div>
<!-- /bar charts group -->
<div class="clearfix"></div>
<!-- /bar charts group -->
</div>
</div>
</div>
<!-- /page content -->
<!-- footer content -->
<footer>
<div class="pull-right">
<a href="https://github.com/rhicks/bgp-dashboard">BGP Dashboard</a> at Github
<div class="clearfix"></div>
<a href="https://github.com/puikinsh/gentelella">Gentelella</a> - Bootstrap Admin Template by <a href="https://colorlib.com">Colorlib</a>
</div>
<div class="clearfix"></div>
</footer>
<!-- /footer content -->
</div>
</div>
<!-- jQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<!-- Bootstrap -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<!-- jQuery Sparklines -->
<script src="../static/js/jquery.sparkline.min.js"></script>
<!-- bootstrap-daterangepicker -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment.min.js" integrity="sha256-Gn7MUQono8LUxTfRA0WZzJgTua52Udm1Ifrk5421zkA=" crossorigin="anonymous"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.13/js/jquery.dataTables.min.js" integrity="sha256-yWA356lDhruy1J8jGncaMWKAPYDbK47OKb0uT/aELLc=" crossorigin="anonymous"></script>
<!-- Custom Theme Scripts -->
<!-- <script src="../static/js/custom.min.js"></script> -->
<script>
function up_or_down_checker(css_tag, data, json_tag)
{
var myDateFormat = moment().format('MMM D YYYY, H:mm:ss');
var css_tag_id = '#' + css_tag;
var css_tag_icon = css_tag_id + 'Icon';
var css_tag_change_time = css_tag_id + 'ChangeTime';
if (jQuery(css_tag_id).text().replace(/\,/g,'') > data[json_tag])
{
jQuery(css_tag_icon).removeClass();
jQuery(css_tag_icon).addClass("red fa fa-arrow-circle-down");
diff = data[json_tag] - jQuery(css_tag_id).text().replace(/\,/g,'');
diff = diff.toLocaleString('en-Us', {minimumFractiondigits: 0});
jQuery(css_tag_change_time).html('<i class="red">' + diff + '</i> at ' + myDateFormat + '&nbsp');
}
else if (jQuery(css_tag_id).text().replace(/\,/g,'') < data[json_tag])
{
jQuery(css_tag_icon).removeClass();
jQuery(css_tag_icon).addClass("green fa fa-arrow-circle-up");
diff = data[json_tag] - jQuery(css_tag_id).text().replace(/\,/g,'');
diff = diff.toLocaleString('en-Us', {minimumFractiondigits: 0});
jQuery(css_tag_change_time).html('<i class="green"> +' + diff + '</i> at ' + myDateFormat + '&nbsp');
}
}
function update_sparklines(data, how_many_times)
{
while (how_many_times > 0)
{
peer_count_sparkline_data.push(data['peer_count']);
ipv4_table_sparkline_data.push(data['ipv4_table_size']);
ipv6_table_sparkline_data.push(data['ipv6_table_size']);
nexthop_count_sparkline_data.push(data['nexthop_ip_count']);
avg_as_path_length_data.push(data['avg_as_path_length']);
customer_count_data.push(data['customer_count']);
customer_ipv4_prefix_data.push(data['customer_ipv4_prefixes']);
customer_ipv6_prefix_data.push(data['customer_ipv6_prefixes']);
how_many_times--;
}
}
var stats = jQuery.getJSON("/bgp/api/v1.0/stats")
var peer_count_sparkline_data = [];
var ipv4_table_sparkline_data = [];
var ipv6_table_sparkline_data = [];
var nexthop_count_sparkline_data = [];
var avg_as_path_length_data = [];
var customer_count_data = [];
var customer_ipv4_prefix_data = [];
var customer_ipv6_prefix_data = [];
var peers_table = jQuery('#datatable').DataTable({
ajax: {
url: "/bgp/api/v1.0/stats",
dataSrc: "peers"
},
columns: [
{ data: "asn", render: function (asn) {return '<a href=/bgp/api/v1.0/asn/'+asn+'>'+asn+'</a>';} },
{ data: "name" },
{ data: "ipv4_origin_count", render: function (data, type, full, meta) {return '<a href=/bgp/api/v1.0/asn/'+full.asn+'/originated/ipv4>'+full.ipv4_origin_count+'</a>';} },
{ data: "ipv4_nexthop_count", render: function (data, type, full, meta) {return '<a href=/bgp/api/v1.0/asn/'+full.asn+'/nexthop/ipv4>'+full.ipv4_nexthop_count+'</a>';} },
{ data: "ipv6_origin_count", render: function (data, type, full, meta) {return '<a href=/bgp/api/v1.0/asn/'+full.asn+'/originated/ipv6>'+full.ipv6_origin_count+'</a>';} },
{ data: "ipv6_nexthop_count", render: function (data, type, full, meta) {return '<a href=/bgp/api/v1.0/asn/'+full.asn+'/nexthop/ipv6>'+full.ipv6_nexthop_count+'</a>';} },
{ data: "asn_count", render: function (data, type, full, meta) {return '<a href=/bgp/api/v1.0/asn/'+full.asn+'/downstream>'+full.asn_count+'</a>';} }
],
"oLanguage":
{
"sSearch": "Filter Table: "
},
paging: false,
scrollY: 400
});
function update_counters()
{
update_counters.count = update_counters.count || 0;
jQuery.getJSON("/bgp/api/v1.0/stats", function(data)
{
update_sparklines(data, 1)
if (update_counters.count > 0) //only update times after the first run
{
up_or_down_checker('peersID', data, 'peer_count');
up_or_down_checker('ipv4TableSize', data, 'ipv4_table_size');
up_or_down_checker('ipv6TableSize', data, 'ipv6_table_size');
up_or_down_checker('nexthopIPCount', data, 'nexthop_ip_count');
up_or_down_checker('avgAsPathLength', data, 'avg_as_path_length');
up_or_down_checker('customerCount', data, 'customer_count');
up_or_down_checker('customerIPv4Prefixes', data, 'customer_ipv4_prefixes');
up_or_down_checker('customerIPv6Prefixes', data, 'customer_ipv6_prefixes');
}
update_counters.count = 1;
sparkline_max_width = $("div#peers_box.tile-stats").width() - 10
sparkline_data_width = peer_count_sparkline_data.length
if (sparkline_data_width < sparkline_max_width)
{
sparkline_width = sparkline_data_width
}
else
{
sparkline_width = sparkline_max_width
}
jQuery('#peersID').text(data['peer_count'].toLocaleString('en-US', {minimumFractionDigits: 0}));
jQuery('#ipv4TableSize').text(data['ipv4_table_size'].toLocaleString('en-US', {minimumFractionDigits: 0}));
jQuery('#ipv6TableSize').text(data['ipv6_table_size'].toLocaleString('en-US', {minimumFractionDigits: 0}));
jQuery('#nexthopIPCount').text(data['nexthop_ip_count'].toLocaleString('en-US', {minimumFractionDigits: 0}));
jQuery('#avgAsPathLength').text(data['avg_as_path_length']);
jQuery('#customerCount').text(data['customer_count']);
jQuery('#customerIPv4Prefixes').text(data['customer_ipv4_prefixes']);
jQuery('#customerIPv6Prefixes').text(data['customer_ipv6_prefixes']);
jQuery('.peers_count_sparkline').sparkline(peer_count_sparkline_data, {width: sparkline_width});
jQuery('.ipv4_table_sparkline').sparkline(ipv4_table_sparkline_data, {width: sparkline_width});
jQuery('.ipv6_table_sparkline').sparkline(ipv6_table_sparkline_data, {width: sparkline_width});
jQuery('.nexthop_count_sparkline').sparkline(nexthop_count_sparkline_data, {width: sparkline_width});
jQuery('.avg_as_path_length_sparkline').sparkline(avg_as_path_length_data, {width: sparkline_width});
jQuery('.customer_count_sparkline').sparkline(customer_count_data, {width: sparkline_width});
jQuery('.customer_ipv4_prefixes_sparkline').sparkline(customer_ipv4_prefix_data, {width: sparkline_width});
jQuery('.customer_ipv6_prefixes_sparkline').sparkline(customer_ipv6_prefix_data, {width: sparkline_width});
//$('.dynamicbar').sparkline(myvalues, {type: 'bar', barColor: 'green'} );
console.log("sparkline_data_width: " + sparkline_data_width)
console.log("sparkline_width: " + sparkline_width)
console.log("sparkline_max_width: " + sparkline_max_width)
if (sparkline_data_width > 1)
{
//peer_count_sparkline_data.shift();
peer_count_sparkline_data.splice(0, (sparkline_data_width - sparkline_max_width))
//ipv4_table_sparkline_data.shift();
ipv4_table_sparkline_data.splice(0, (sparkline_data_width - sparkline_max_width))
// ipv6_table_sparkline_data.shift();
ipv6_table_sparkline_data.splice(0, (sparkline_data_width - sparkline_max_width))
// nexthop_count_sparkline_data.shift();
nexthop_count_sparkline_data.splice(0, (sparkline_data_width - sparkline_max_width))
// avg_as_path_length_data.shift();
avg_as_path_length_data.splice(0, (sparkline_data_width - sparkline_max_width))
// customer_count_data.shift();
customer_count_data.splice(0, (sparkline_data_width - sparkline_max_width))
// customer_ipv4_prefix_data.shift();
customer_ipv4_prefix_data.splice(0, (sparkline_data_width - sparkline_max_width))
// customer_ipv6_prefix_data.shift();
customer_ipv6_prefix_data.splice(0, (sparkline_data_width - sparkline_max_width))
}
update_sparklines(data, 1)
peers_table.ajax.reload(null, false)
});
}
jQuery(document).ready(update_counters());
setInterval(function() { update_counters() }, 20000);
</script>
<!-- morris.js -->
<script>
$(document).ready(function() {
Morris.Bar({
element: 'graph_bar',
data: [
{%- for cidr in cidr_breakdown -%}
{% if cidr.ip_version == 4 %}
{cidr: "/{{ cidr.mask }}", count: {{ cidr.count }}},
{% endif %}
{%- endfor -%}
],
xkey: 'cidr',
ykeys: ['count'],
labels: ['count'],
barRatio: 0.4,
barColors: ['#26B99A', '#34495E', '#ACADAC', '#3498DB'],
xLabelAngle: 60,
hideHover: 'auto',
resize: true
});
Morris.Bar({
element: 'graph_bar_group',
data: [
{%- for cidr in cidr_breakdown -%}
{% if cidr.ip_version == 6 %}
{cidr: "/{{ cidr.mask }}", count: {{ cidr.count }}},
{% endif %}
{%- endfor -%}
],
xkey: 'cidr',
ykeys: ['count'],
labels: ['count'],
barRatio: 0.4,
barColors: ['#26B99A', '#34495E', '#ACADAC', '#3498DB'],
xLabelAngle: 60,
hideHover: 'auto',
resize: true
});
Morris.Donut({
element: 'graph_donut1',
data: [
{%- for comm in communities -%}
{% if comm.community.startswith(peer_bgp_community) %}
{label: "{{ comm.name }}", value: {{ comm.count }}},
{% endif %}
{%- endfor -%}
],
colors: ['#26B99A', '#34495E', '#E59443', '#3498DB', '#E74C3C', '#FF7BAC', '#26B99A', '#34495E', '#E59443', '#3498DB', '#E74C3C', '#333333'],
formatter: function (y) {
return y;
},
resize: true
});
Morris.Donut({
element: 'graph_donut2',
data: [
{% set total_prefixes = data.ipv4_table_size + data.ipv6_table_size %}
{%- for comm in communities -%}
{% if comm.community == transit_bgp_community %}
{label: 'Peering', value: {{ (((comm.count - total_prefixes) / total_prefixes) * -100)|round }}},
{label: 'Transit', value: {{ ((comm.count / total_prefixes) * 100)|round }}},
{% endif %}
{%- endfor -%}
],
colors: ['#34495E', '#3498DB'],
formatter: function (y) {
return y + "%";
},
resize: true
});
// Morris.Donut({
// element: 'graph_donut3',
// data: [
// {%- for comm in communities -%}
// {% if comm.community == "3701:370" %}
// {label: 'Customer', value: {{ comm.count }}}
// {% endif %}
// {%- endfor -%}
// ],
// colors: ['#34495E'],
// formatter: function (y) {
// return y;
// },
// resize: true
// });
});
</script>
<!-- /morris.js -->
</body>
</html>

16
flask/app/uwsgi.ini Normal file
View File

@@ -0,0 +1,16 @@
[uwsgi]
#application's base folder
base = /var/www/app
#python module to import
module = bgp
#the variable that holds a flask application inside the module imported at line #6
callable = app
#socket file's location
socket = /var/www/app/uwsgi.sock
#permissions for the socket file
chmod-socket = 666
#Log directory
logto = /var/log/uwsgi/app/app.log
enable-threads = true
chdir = /var/www/app

18
flask/nginx/flask.conf Normal file
View File

@@ -0,0 +1,18 @@
server {
listen 80;
server_name localhost;
charset utf-8;
client_max_body_size 75M;
location / {
include uwsgi_params;
uwsgi_pass unix:/var/www/app/uwsgi.sock;
}
location /static {
root /var/www/app/;
}
}

View File

@@ -0,0 +1,8 @@
[supervisord]
nodaemon = true
[program:nginx]
command = /usr/sbin/nginx
[program:uwsgi]
command = /venv/bin/uwsgi --ini /var/www/app/uwsgi.ini

26
gobgp/Dockerfile Executable file
View File

@@ -0,0 +1,26 @@
FROM ubuntu:24.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get remove -y binutils && \
apt-get install -y \
python3 \
python3-pip \
python3-venv \
wget && \
rm -rf /var/lib/apt/lists/*
COPY ./requirements.txt /tmp/requirements.txt
RUN python3 -m venv venv && chmod +x venv
RUN . venv/bin/activate && pip3 install -r /tmp/requirements.txt
RUN wget https://github.com/osrg/gobgp/releases/download/v3.27.0/gobgp_3.27.0_linux_amd64.tar.gz && \
tar -xzvf gobgp_3.27.0_linux_amd64.tar.gz && \
mv gobgp /usr/local/bin/gobgp && \
mv gobgpd /usr/local/bin/gobgpd && \
rm gobgp_3.27.0_linux_amd64.tar.gz
COPY ./gobgpd.conf /root/gobgp/gobgpd.conf
COPY ./entrypoint.sh /root/gobgp/entrypoint.sh
COPY ./startup.sh /root/gobgp/startup.sh
RUN chmod +x /root/gobgp/entrypoint.sh && \
chmod +x /root/gobgp/startup.sh
ENTRYPOINT ["/root/gobgp/entrypoint.sh"]

3
gobgp/entrypoint.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
exec gobgpd -f /root/gobgp/gobgpd.conf &
exec /root/gobgp/startup.sh

15
gobgp/gobgpd.conf Executable file
View File

@@ -0,0 +1,15 @@
[global.config]
as = 400848
router-id = "172.16.1.66"
[[neighbors]]
[neighbors.config]
neighbor-address = "172.16.1.27"
peer-as = 400848
route-flap-damping = "True"
[[neighbors.afi-safis]]
[neighbors.afi-safis.config]
afi-safi-name = "ipv4-unicast"
[[neighbors.afi-safis]]
[neighbors.afi-safis.config]
afi-safi-name = "ipv6-unicast"

3
gobgp/requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
pymongo >= 4.8.0
dnspython >= 2.6.1
ipaddress >= 1.0.23

7
gobgp/startup.sh Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
## Production
. venv/bin/activate
gobgp monitor global rib -j | /var/tmp/gobgp_to_mongo.py
##
## Dev Test
# cat /var/tmp/log/bgp.dump.json | /var/tmp/gobgp_to_mongo.py

200
gobgp_to_mongo.py Executable file
View File

@@ -0,0 +1,200 @@
#! /usr/bin/env python3
import sys
import json
from pymongo.database import Database
import bgp_attributes as BGP
from pymongo import MongoClient
import pymongo
from copy import copy
from datetime import datetime
import ipaddress
import logging
# logging.basicConfig(level=logging.CRITICAL)
# logging.basicConfig(level=logging.DEBUG)
# DEFAULTS - UPDATE ACCORDINGLY
MAX_PREFIX_HISTORY = 100 # None = unlimited (BGP flapping will likely kill DB if unlimited)
def db_connect(host='mongodb') -> Database:
"""Return a connection to the Mongo Database."""
client = MongoClient(host=host)
return client['bgp']
def initialize_database(db: Database):
"""Create indexes, and if the db contains any entries set them all to 'active': False"""
# db['bgp'].drop()
db['bgp'].create_index('nexthop')
db['bgp'].create_index('nexthop_asn')
db['bgp'].create_index([('nexthop', pymongo.ASCENDING), ('active', pymongo.ASCENDING)])
db['bgp'].create_index([('nexthop_asn', pymongo.ASCENDING), ('active', pymongo.ASCENDING)])
db['bgp'].create_index([('ip_version', pymongo.ASCENDING), ('active', pymongo.ASCENDING)])
db['bgp'].create_index(
[('origin_asn', pymongo.ASCENDING), ('ip_version', pymongo.ASCENDING), ('active', pymongo.ASCENDING)])
db['bgp'].create_index([('communities', pymongo.ASCENDING), ('active', pymongo.ASCENDING)])
db['bgp'].create_index(
[('as_path.1', pymongo.ASCENDING), ('nexthop_asn', pymongo.ASCENDING), ('active', pymongo.ASCENDING)])
db['bgp'].update_many(
{"active": True}, # Search for
{"$set": {"active": False}}) # Replace with
def get_update_entry(line):
"""Read output from GoBGP from stdin and return a update entry *dict*"""
try:
update_list = json.loads(line)
for update_entry in update_list:
if 'error' in update_entry:
return None
else:
return update_entry
except Exception as err:
logging.error("Error in get_update_entry(line):", err)
return None
def compare_prefixes(new, old):
"""ignore history, age, and active state, then compare prefix objects"""
new['history'] = new['age'] = new['active'] = None
old['history'] = old['age'] = old['active'] = None
if new == old:
return True
else:
return False
def community_32bit_to_string(number):
"""Given a 32bit number, convert to standard bgp community format XXX:XX"""
if number != 0:
return f'{int(bin(number)[:-16], 2)}:{int(bin(number)[-16:], 2)}' # PEP 498
def community_large_to_string(community: dict):
"""Given a dict, convert to large bgp community format XXX:XXX:XXX"""
return f"{community['ASN']}:{community['LocalData1']}:{community['LocalData2']}"
def build_json(update_entry):
"""Given an update entry from GoBGP, set the BGP attribue types as a
key/value dict and return"""
update_json = { # set defaults
'_id': update_entry['nlri']['prefix'],
'ip_version': ipaddress.ip_address(update_entry['nlri']['prefix'].split('/', 1)[0]).version,
'origin_asn': None,
'nexthop': None,
'nexthop_asn': None,
'as_path': [],
'med': 0,
'local_pref': 0,
'communities': [],
'route_origin': None,
'atomic_aggregate': None,
'aggregator_as': None,
'aggregator_address': None,
'originator_id': None,
'cluster_list': [],
'withdrawal': False,
'age': 0,
'active': True,
'history': []
}
for attribute in update_entry['attrs']:
if attribute['type'] == BGP.ORIGIN:
update_json['route_origin'] = BGP.ORIGIN_CODE[attribute['value']]
if attribute['type'] == BGP.AS_PATH:
try:
update_json['as_path'] = attribute['as_paths'][0]['asns']
update_json['nexthop_asn'] = update_json['as_path'][0]
update_json['origin_asn'] = update_json['as_path'][-1]
except Exception:
logging.debug(f'Error processing as_path: {attribute}')
logging.debug(f'Error processing as_path: {update_json["_id"]}')
if attribute['type'] == BGP.NEXT_HOP:
update_json['nexthop'] = attribute['nexthop']
if attribute['type'] == BGP.MULTI_EXIT_DISC:
try:
update_json['med'] = attribute['metric']
except Exception:
logging.debug(f'Error processing med: {attribute}')
if attribute['type'] == BGP.LOCAL_PREF:
try:
update_json['local_pref'] = attribute['value']
except Exception:
logging.debug(f'Error processing local_pref: {attribute}')
if attribute['type'] == BGP.ATOMIC_AGGREGATE:
update_json['atomic_aggregate'] = True
if attribute['type'] == BGP.AGGREGATOR:
update_json['aggregator_as'] = attribute['as']
update_json['aggregator_address'] = attribute['address']
if attribute['type'] == BGP.COMMUNITY:
try:
for number in attribute['communities']:
update_json['communities'].append(community_32bit_to_string(number))
except Exception:
logging.debug(f'Error processing communities: {attribute}')
if attribute['type'] == BGP.ORIGINATOR_ID:
update_json['originator_id'] = attribute['value']
if attribute['type'] == BGP.CLUSTER_LIST:
update_json['cluster_list'] = attribute['value']
if attribute['type'] == BGP.MP_REACH_NLRI:
update_json['nexthop'] = attribute['nexthop']
if attribute['type'] == BGP.MP_UNREACH_NLRI:
logging.debug(f'Found MP_UNREACH_NLRI: {attribute}')
if attribute['type'] == BGP.EXTENDED_COMMUNITIES:
logging.debug(f'Found EXTENDED_COMMUNITIES: {attribute}')
if attribute['type'] == BGP.LARGE_COMMUNITIES:
try:
for community in attribute['value']:
update_json['communities'].append(community_large_to_string(community))
except Exception:
logging.debug(f'Error processing LARGE_COMMUNITIES: {attribute}')
if 'withdrawal' in update_entry:
update_json['withdrawal'] = update_entry['withdrawal']
update_json['active'] = False
if 'age' in update_entry:
update_json['age'] = datetime.fromtimestamp(update_entry['age']).strftime('%Y-%m-%d %H:%M:%S ') + 'UTC'
return update_json
def update_prefix(prefix_from_gobgp, prefix_from_database):
if compare_prefixes(copy(prefix_from_gobgp), copy(prefix_from_database)):
prefix_from_gobgp['active'] = True # flip the active state to true
else: # diff between prefix_from_gobgp and prefix_from_database: update history
history_list = prefix_from_database['history']
del prefix_from_database['active'] # delete house keeping keys from history objects
del prefix_from_database['history']
if not history_list: # no history: create some
prefix_from_gobgp['history'].append(prefix_from_database)
else: # existing history: append to history list
history_list.insert(0, prefix_from_database) # insert on top of list, index 0
prefix_from_gobgp['history'] = history_list[:MAX_PREFIX_HISTORY] # trim the history list if MAX is set
return prefix_from_gobgp
def main():
db = db_connect()
initialize_database(db)
for line in sys.stdin:
try:
prefix_from_gobgp = build_json(get_update_entry(line))
prefix_from_database = db['bgp'].find_one({'_id': prefix_from_gobgp['_id']})
if prefix_from_database:
updated_prefix = update_prefix(prefix_from_gobgp, prefix_from_database)
db['bgp'].update_one({"_id": prefix_from_database['_id']}, {'$set': updated_prefix}, upsert=True)
else:
db['bgp'].update_one({"_id": prefix_from_gobgp['_id']}, {'$set': prefix_from_gobgp}, upsert=True)
except TypeError as e:
print(f"TypeError {e} in Line: {line}")
if __name__ == "__main__":
sys.exit(main())