forked from hush/asmap
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
147 lines
5.5 KiB
147 lines
5.5 KiB
# Copyright (c) 2022 Pieter Wuille
|
|
# Distributed under the MIT software license, see the accompanying
|
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
|
|
|
import ipaddress
|
|
import sys
|
|
import copy
|
|
import re
|
|
import itertools
|
|
|
|
LINE_PATTERN = re.compile(r"^TABLE_DUMP2?\|[0-9]+\|B\|[0-9a-f:.]+\|[0-9]+\|([0-9a-f:./]+)\|([0-9, {}]*)\|")
|
|
LINE_AP_PATTERN = re.compile(r"^TABLE_DUMP2_AP\|[0-9]+\|B\|[0-9a-f:.]+\|[0-9]+\|([0-9a-f:./]+)\|(?:[0-9]+)\|([0-9, {}]*)\|")
|
|
SPACE_PATTERN = re.compile(r" +")
|
|
|
|
def merge_path(ret, net, path, cnt):
|
|
assert len(path) > 0
|
|
asn = path[-1]
|
|
if net not in ret:
|
|
ret[net] = {asn: (path, 1)}
|
|
cnt[0] += 1
|
|
else:
|
|
old_paths = ret[net]
|
|
if asn not in old_paths:
|
|
old_paths[asn] = (path, 1)
|
|
cnt[0] += 1
|
|
else:
|
|
old_path, old_count = old_paths[asn]
|
|
common_len = 0
|
|
while common_len < len(old_path) and common_len < len(path):
|
|
if old_path[-common_len - 1] == path[-common_len - 1]:
|
|
common_len += 1
|
|
else:
|
|
break
|
|
old_paths[asn] = (old_path[-common_len:], old_count + 1)
|
|
return True
|
|
|
|
def valid_asn(asn, net):
|
|
if asn == 0 or asn == 65535 or (asn >= 65552 and asn <= 131072) or asn == 4294967295:
|
|
# print("Skipping reserved AS%i (RFC1930) for network %s" % (asn, net))
|
|
pass
|
|
elif asn == 23456:
|
|
# print("Skipping transition AS%i (RFC6793) for network %s" % (asn, net))
|
|
pass
|
|
elif (asn >= 64496 and asn <= 64511) or (asn >= 65536 and asn <= 65551):
|
|
# print("Skipping documentation AS%i (RFC4893,RFC5398) for network %s" % (asn, net))
|
|
pass
|
|
elif (asn >= 64512 and asn <= 65534) or (asn >= 4200000000 and asn <= 4294967294):
|
|
# print("Skipping private AS%i (RFC5398,RFC6996) for network %s" % (asn, net))
|
|
pass
|
|
else:
|
|
return True
|
|
return False
|
|
|
|
def accept_net(net):
|
|
if net.is_multicast:
|
|
print("Skipping multicast network %s" % net)
|
|
pass
|
|
elif net.is_private:
|
|
# print("Skipping private network %s" % net)
|
|
pass
|
|
elif net.is_unspecified:
|
|
# print("Skipping unspecified network %s" % net)
|
|
pass
|
|
elif net.is_reserved:
|
|
# print("Skipping reserved network %s" % net)
|
|
pass
|
|
elif net.is_loopback:
|
|
print("Skipping loopback network %s" % net)
|
|
elif net.is_link_local:
|
|
print("Skipping link-local network %s" % net)
|
|
elif not net.is_global:
|
|
# print("Skipping non-global network %s" % net)
|
|
pass
|
|
elif net.prefixlen == 0:
|
|
# print("Skipping entire network %s" % net)
|
|
pass
|
|
elif net.prefixlen > 48 and isinstance(net, ipaddress.IPv6Network):
|
|
# print("Skipping IPv6 range smaller than a /48: %s" % net)
|
|
pass
|
|
elif net.prefixlen > 24 and isinstance(net, ipaddress.IPv4Network):
|
|
# print("Skipping IPv4 range smaller than a /24: %s" % net)
|
|
pass
|
|
else:
|
|
return True
|
|
return False
|
|
|
|
routes = 0
|
|
valid_routes = 0
|
|
kept_routes = [0]
|
|
ASNS = set()
|
|
RES = {}
|
|
reports = 0
|
|
for line in sys.stdin:
|
|
match = LINE_PATTERN.match(line)
|
|
if not match:
|
|
match = LINE_AP_PATTERN.match(line)
|
|
if match:
|
|
routes += 1
|
|
try:
|
|
net = ipaddress.ip_network(match[1], strict=True)
|
|
except ValueError:
|
|
net = None
|
|
if net is None:
|
|
raise ValueError("Cannot parse network %s" % match[1])
|
|
if accept_net(net):
|
|
path_str = match[2]
|
|
# Ignore AS_SETs at the end of the path.
|
|
while path_str.endswith('}'):
|
|
path_str = path_str[:path_str.rindex('{')]
|
|
# Drop anything up to and including the last AS_SET in the path before that.
|
|
close_pos = path_str.rfind('}')
|
|
if close_pos >= 0:
|
|
path_str = path_str[close_pos + 1:]
|
|
# Convert to list of AS numbers
|
|
as_path = [int(x) for x in path_str.split(' ') if len(x) > 0]
|
|
if len(as_path):
|
|
# Remove duplicates from the list.
|
|
as_path = [i[0] for i in itertools.groupby(as_path)]
|
|
if valid_asn(as_path[-1], net):
|
|
ASNS.add(as_path[-1])
|
|
merge_path(RES, net, as_path, kept_routes)
|
|
valid_routes += 1
|
|
else:
|
|
print("Skipping empty path for %s: %s" % (net, match[2]))
|
|
else:
|
|
print("Ignoring unparseable line: %s" % line.strip())
|
|
if routes >= (reports + 1) * 10000000:
|
|
print("%i routes, %i valid, %i kept; %i ASNs; %i prefixes" % (valid_routes, routes, kept_routes[0], len(ASNS), len(RES)))
|
|
reports = routes // 10000000
|
|
|
|
with open("output-bottlenext.txt", "w") as out:
|
|
for net in sorted(RES, key=ipaddress.get_mixed_type_key):
|
|
paths = RES[net]
|
|
first = True
|
|
for path, cnt in sorted(paths.values(), key=lambda f: (-f[1], f[0])):
|
|
out.write("%s%s AS%i # %i times %s\n" % ("" if first else "# ", net, path[0], cnt, " ".join("AS%i" % x for x in path)))
|
|
first = False
|
|
out.write("\n")
|
|
|
|
with open("output-final.txt", "w") as out:
|
|
for net in sorted(RES, key=ipaddress.get_mixed_type_key):
|
|
paths = RES[net]
|
|
first = True
|
|
for path, cnt in sorted(paths.values(), key=lambda f: (-f[1], f[0])):
|
|
out.write("%s%s AS%i # %i times %s\n" % ("" if first else "# ", net, path[-1], cnt, " ".join("AS%i" % x for x in path)))
|
|
first = False
|
|
out.write("\n")
|
|
|