2020-05-01 11:40:05 +08:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
from ipaddress import IPv4Network, IPv6Network
|
|
|
|
from itertools import combinations
|
2020-05-02 20:15:18 +08:00
|
|
|
import re
|
2020-05-02 15:02:14 +08:00
|
|
|
|
|
|
|
class BashParser:
|
|
|
|
def __init__(self):
|
|
|
|
self.__pa = None # are we parsing bash array?
|
|
|
|
def parseline(self, line):
|
|
|
|
repl_quotes = lambda t: t.replace('"', '').replace('\'', '')
|
|
|
|
line = line.strip()
|
|
|
|
if '=(' in line:
|
|
|
|
self.__pa = (repl_quotes(line).split('=(')[0], list())
|
|
|
|
return None
|
|
|
|
if self.__pa:
|
|
|
|
if line:
|
|
|
|
if line.endswith(')'):
|
|
|
|
if line[:-1]:
|
|
|
|
self.__pa[1].append(repl_quotes(line[:-1]))
|
|
|
|
ret = self.__pa
|
|
|
|
self.__pa = None
|
|
|
|
return ret
|
|
|
|
else:
|
|
|
|
self.__pa[1].append(repl_quotes(line))
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
if not line or line.startswith('#'):
|
|
|
|
return None
|
|
|
|
l = line.split('=')
|
|
|
|
assert len(l) >= 2
|
|
|
|
return [l[0], '='.join([repl_quotes(i) for i in l[1:]])]
|
|
|
|
bp = BashParser()
|
|
|
|
|
|
|
|
def shell2dict(shellscript):
|
|
|
|
fc = dict()
|
|
|
|
for line in shellscript.split('\n'):
|
|
|
|
r = bp.parseline(line)
|
|
|
|
if r:
|
|
|
|
key, val = r
|
|
|
|
fc[key.lower()] = val
|
|
|
|
return fc
|
2020-05-01 11:40:05 +08:00
|
|
|
|
|
|
|
cwd = Path()
|
2020-05-02 16:07:22 +08:00
|
|
|
assert not [d for d in ("asn", "route", "route6", "node", "entity") if not (cwd / d).is_dir()]
|
2020-05-01 17:37:29 +08:00
|
|
|
|
|
|
|
def str2asn(s_asn):
|
2020-05-02 15:02:14 +08:00
|
|
|
s_asn = s_asn.strip().lower()
|
2020-05-01 17:37:29 +08:00
|
|
|
if s_asn.startswith('as'):
|
|
|
|
s_asn = s_asn[2:]
|
|
|
|
return int(s_asn)
|
2020-05-01 11:40:05 +08:00
|
|
|
|
2020-05-02 15:02:14 +08:00
|
|
|
|
2020-05-02 20:15:18 +08:00
|
|
|
def name2nichdl(name):
|
|
|
|
r, num = re.subn(r'[^0-9A-Z]', '-', name.upper())
|
|
|
|
_r = len(r.replace('-', ''))
|
|
|
|
assert _r >= 3 # has at least 3 effective chars
|
|
|
|
assert r[0] != '-' # starts with [0-9A-Z]
|
|
|
|
assert num < _r # not too many subs
|
|
|
|
return r
|
|
|
|
|
2020-05-02 15:02:14 +08:00
|
|
|
def neoneo_get_people():
|
2020-05-02 20:15:18 +08:00
|
|
|
nic_hdl_names = set()
|
2020-05-02 15:02:14 +08:00
|
|
|
people = dict()
|
2020-05-02 16:07:22 +08:00
|
|
|
for f in (cwd / "entity").iterdir():
|
2020-05-02 15:02:14 +08:00
|
|
|
try:
|
|
|
|
if not f.is_file():
|
|
|
|
continue
|
|
|
|
fc = shell2dict(f.read_text())
|
|
|
|
present_keys = ('name', 'desc', 'contact', 'babel')
|
|
|
|
assert f.name
|
|
|
|
people[f.name] = {k: fc.get(k) for k in present_keys}
|
2020-05-02 20:15:18 +08:00
|
|
|
nic_hdl = name2nichdl(f.name)
|
|
|
|
assert nic_hdl not in nic_hdl_names
|
|
|
|
nic_hdl_names.add('nic_hdl')
|
|
|
|
people[f.name]['nic_hdl'] = nic_hdl
|
2020-05-02 15:02:14 +08:00
|
|
|
for v in people[f.name].values():
|
|
|
|
assert v is not None
|
|
|
|
except Exception:
|
|
|
|
print("[!] Error while processing file", f)
|
|
|
|
raise
|
|
|
|
return people
|
|
|
|
PEOPLE = neoneo_get_people()
|
|
|
|
|
|
|
|
def neonet_get_asns():
|
|
|
|
asns = dict()
|
2020-05-01 11:40:05 +08:00
|
|
|
for f in (cwd / "asn").iterdir():
|
|
|
|
try:
|
|
|
|
if not f.is_file():
|
|
|
|
continue
|
2020-05-02 15:02:14 +08:00
|
|
|
fc = shell2dict(f.read_text())
|
|
|
|
present_keys = ('name', 'owner', 'desc')
|
|
|
|
asns[str2asn(f.name)] = {k: fc.get(k) for k in present_keys}
|
|
|
|
assert fc.get('owner') in PEOPLE
|
|
|
|
for v in asns[str2asn(f.name)].values():
|
|
|
|
assert v is not None
|
2020-05-01 11:40:05 +08:00
|
|
|
except Exception:
|
|
|
|
print("[!] Error while processing file", f)
|
|
|
|
raise
|
|
|
|
return asns
|
2020-05-02 15:02:14 +08:00
|
|
|
ASNS = neonet_get_asns()
|
2020-05-01 17:37:29 +08:00
|
|
|
|
|
|
|
def node2asn():
|
|
|
|
node_table = dict()
|
|
|
|
for f in (cwd / "node").iterdir():
|
|
|
|
try:
|
|
|
|
if not f.is_file():
|
|
|
|
continue
|
|
|
|
fc = shell2dict(f.read_text())
|
|
|
|
asn = str2asn(fc.get('asn'))
|
2020-05-02 15:02:14 +08:00
|
|
|
node_table[f.name] = asn
|
2020-05-01 17:37:29 +08:00
|
|
|
except Exception:
|
|
|
|
print("[!] Error while processing file", f)
|
|
|
|
raise
|
|
|
|
return node_table
|
|
|
|
NODE_TABLE = node2asn()
|
|
|
|
|
2020-05-02 15:02:14 +08:00
|
|
|
def neonet_route2roa(dirname, is_ipv6=False):
|
2020-05-02 20:15:18 +08:00
|
|
|
net_names = set()
|
2020-05-01 11:40:05 +08:00
|
|
|
roa_entries = list()
|
|
|
|
for f in (cwd / dirname).iterdir():
|
|
|
|
try:
|
|
|
|
if not f.is_file():
|
|
|
|
continue
|
2020-05-01 17:37:29 +08:00
|
|
|
fc = shell2dict(f.read_text())
|
2020-05-01 11:40:05 +08:00
|
|
|
nettype = IPv6Network if is_ipv6 else IPv4Network
|
2020-05-02 11:29:44 +08:00
|
|
|
get_supernet = lambda s_net: None if not s_net else nettype(s_net, strict=True)
|
2020-05-02 20:15:18 +08:00
|
|
|
roa_entries_key = ("asn", "prefix", "supernet", "netname")
|
2020-05-02 15:02:14 +08:00
|
|
|
if fc.get('type').lower() in ('lo', 'subnet'):
|
2020-05-02 15:55:28 +08:00
|
|
|
asn = str2asn(fc.get('asn'))
|
2020-05-01 11:40:05 +08:00
|
|
|
assert asn in ASNS
|
|
|
|
route = f.name.replace(',', '/')
|
2020-05-02 11:29:44 +08:00
|
|
|
supernet = get_supernet(fc.get('supernet'))
|
2020-05-02 20:15:18 +08:00
|
|
|
netname = name2nichdl(fc.get('name'))
|
|
|
|
assert netname not in net_names
|
|
|
|
net_names.add(netname)
|
|
|
|
roa_entries.append(dict(zip(roa_entries_key, [asn, nettype(route, strict=True), supernet, netname])))
|
2020-05-02 15:02:14 +08:00
|
|
|
elif fc.get('type').lower().startswith('tun'):
|
2020-05-01 18:17:56 +08:00
|
|
|
assert NODE_TABLE[fc.get('downstream')] # extra check for downstream
|
|
|
|
asn = NODE_TABLE[fc.get('upstream')]
|
2020-05-01 11:40:05 +08:00
|
|
|
assert asn in ASNS
|
|
|
|
route = f.name.replace(',', '/')
|
2020-05-02 11:29:44 +08:00
|
|
|
supernet = get_supernet(fc.get('supernet'))
|
2020-05-02 20:15:18 +08:00
|
|
|
netname = name2nichdl("%s-%s" % (fc.get('type'), route))
|
|
|
|
assert netname not in net_names
|
|
|
|
net_names.add(netname)
|
|
|
|
roa_entries.append(dict(zip(roa_entries_key, [asn, nettype(route, strict=True), supernet, netname])))
|
2020-05-01 11:40:05 +08:00
|
|
|
else:
|
2020-05-02 15:02:14 +08:00
|
|
|
assert fc.get('type').lower() in ('ptp',)
|
2020-05-01 11:40:05 +08:00
|
|
|
except Exception:
|
|
|
|
print("[!] Error while processing file", f)
|
|
|
|
raise
|
2020-05-02 11:29:44 +08:00
|
|
|
roa_entries.sort(key=lambda l: l['asn'])
|
|
|
|
for _net1, _net2 in combinations(roa_entries, 2):
|
|
|
|
net1, net2 = sorted([_net1, _net2], key=lambda net: net['prefix'].prefixlen)
|
|
|
|
if net1['prefix'].overlaps(net2['prefix']):
|
|
|
|
if net1['prefix'] != net2['prefix'] and net1['prefix'].supernet_of(net2['prefix']) \
|
|
|
|
and net2['supernet'] == net1['prefix']:
|
|
|
|
# This is allowed
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
print("[!] Error: found", net2, "overlaps", net1)
|
|
|
|
raise AssertionError
|
2020-05-01 11:40:05 +08:00
|
|
|
return roa_entries
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(description='NeoNetwork ROA tool')
|
2020-05-01 15:45:25 +08:00
|
|
|
parser.add_argument('-m', '--max', type=int, default=29, help='set ipv4 max prefix length')
|
2020-05-01 11:40:05 +08:00
|
|
|
parser.add_argument('-M', '--max6', type=int, default=64, help='set ipv6 max prefix length')
|
|
|
|
parser.add_argument('-j', '--json', action='store_true', help='output json')
|
|
|
|
parser.add_argument('-o', '--output', default='', help='write output to file')
|
|
|
|
parser.add_argument('-4', '--ipv4', action='store_true', help='print ipv4 only')
|
|
|
|
parser.add_argument('-6', '--ipv6', action='store_true', help='print ipv6 only')
|
2020-05-02 15:02:14 +08:00
|
|
|
parser.add_argument('-e', '--export', action='store_true', help='export registry to json')
|
2020-05-01 11:40:05 +08:00
|
|
|
args = parser.parse_args()
|
|
|
|
if args.max < 0 or args.max6 < 0 or args.max > IPv4Network(0).max_prefixlen or args.max6 > IPv6Network(0).max_prefixlen:
|
|
|
|
parser.error('check your max prefix length')
|
|
|
|
|
|
|
|
roa4 = roa6 = list()
|
|
|
|
if args.ipv4:
|
2020-05-02 15:02:14 +08:00
|
|
|
roa4 = neonet_route2roa('route')
|
2020-05-01 11:40:05 +08:00
|
|
|
elif args.ipv6:
|
2020-05-02 15:02:14 +08:00
|
|
|
roa6 = neonet_route2roa('route6', True)
|
2020-05-01 11:40:05 +08:00
|
|
|
else:
|
2020-05-02 15:02:14 +08:00
|
|
|
roa4 = neonet_route2roa('route')
|
|
|
|
roa6 = neonet_route2roa('route6', True)
|
2020-05-01 11:40:05 +08:00
|
|
|
|
2020-05-02 11:29:44 +08:00
|
|
|
roa4 = [r for r in roa4 if r['prefix'].prefixlen <= args.max or r['prefix'].prefixlen == IPv4Network(0).max_prefixlen]
|
|
|
|
roa6 = [r for r in roa6 if r['prefix'].prefixlen <= args.max6]
|
2020-05-01 11:40:05 +08:00
|
|
|
|
|
|
|
for r in roa4:
|
2020-05-02 11:29:44 +08:00
|
|
|
if r['prefix'].prefixlen == IPv4Network(0).max_prefixlen:
|
|
|
|
r['maxLength'] = IPv4Network(0).max_prefixlen
|
2020-05-01 11:40:05 +08:00
|
|
|
else:
|
2020-05-02 11:29:44 +08:00
|
|
|
r['maxLength'] = args.max
|
2020-05-01 11:40:05 +08:00
|
|
|
for r in roa6:
|
2020-05-02 11:29:44 +08:00
|
|
|
r['maxLength'] = args.max6
|
|
|
|
for r in (*roa4, *roa6):
|
|
|
|
r['prefix'] = r['prefix'].with_prefixlen
|
|
|
|
|
2020-05-01 11:40:05 +08:00
|
|
|
|
|
|
|
output = ""
|
2020-05-02 11:29:44 +08:00
|
|
|
VALID_KEYS = ('asn', 'prefix', 'maxLength')
|
2020-05-02 15:02:14 +08:00
|
|
|
if args.export:
|
|
|
|
import json, time
|
|
|
|
current = int(time.time())
|
|
|
|
# people has [asns], asn has [route]
|
|
|
|
d_output = {"metadata": {"generated": current, "valid": current+14*86400}, "people": dict()}
|
|
|
|
for asn, asi in ASNS.items():
|
|
|
|
as_route4 = list()
|
|
|
|
as_route6 = list()
|
2020-05-02 15:12:09 +08:00
|
|
|
vkeys = [k for k in VALID_KEYS if k != 'asn']
|
2020-05-02 20:15:18 +08:00
|
|
|
vkeys.append('netname')
|
2020-05-02 15:53:46 +08:00
|
|
|
for roa, as_route in ((roa4, as_route4), (roa6, as_route6)):
|
|
|
|
for r in roa:
|
|
|
|
if r['asn'] == asn:
|
|
|
|
as_route.append({k:v for k, v in r.items() if k in vkeys})
|
2020-05-02 15:02:14 +08:00
|
|
|
owner = asi['owner']
|
|
|
|
peopledict = d_output['people'].setdefault(owner, {"info": PEOPLE[owner], "asns": list()})
|
2020-05-02 20:15:18 +08:00
|
|
|
peopledict['asns'].append({"asn": asn, **{k:v for k, v in ASNS[asn].items() if k != 'owner'},
|
|
|
|
"routes": {'ipv4': as_route4, 'ipv6': as_route6}})
|
2020-05-02 15:02:14 +08:00
|
|
|
output = json.dumps(d_output, indent=2)
|
|
|
|
elif args.json:
|
2020-05-01 15:45:25 +08:00
|
|
|
import json, time
|
|
|
|
current = int(time.time())
|
|
|
|
d_output = {"metadata": {"counts": len(roa4)+len(roa6), "generated": current, "valid": current+14*86400}, "roas": list()}
|
2020-05-02 11:29:44 +08:00
|
|
|
for r in (*roa4, *roa6):
|
|
|
|
# some preprocessing
|
|
|
|
r['asn'] = "AS%d" % r['asn']
|
|
|
|
for r in (*roa4, *roa6):
|
|
|
|
d_output['roas'].append({k:v for k, v in r.items() if k in VALID_KEYS})
|
2020-05-01 11:40:05 +08:00
|
|
|
output = json.dumps(d_output, indent=2)
|
|
|
|
else:
|
|
|
|
output += "# NeoNetwork ROA tool\n"
|
|
|
|
pattern = 'route %s max %d as %d;'
|
|
|
|
l_output = list()
|
2020-05-02 11:29:44 +08:00
|
|
|
rdict2list = lambda d: [d[k] for k in VALID_KEYS]
|
|
|
|
for (asn, prefix, maxlen) in [rdict2list(r) for r in (*roa4, *roa6)]:
|
2020-05-01 11:40:05 +08:00
|
|
|
l_output.append(pattern % (prefix, maxlen, asn))
|
|
|
|
output += '\n'.join(l_output)
|
|
|
|
if not args.output or args.output == '-':
|
|
|
|
print(output)
|
|
|
|
else:
|
|
|
|
Path(args.output).write_text(output)
|
|
|
|
print('written to', args.output)
|