import math
import re
import sys
+import telnetlib
# Load our location module
import location
log = logging.getLogger("location.importer")
log.propagate = 1
-INVALID_ADDRESSES = (
- "0.0.0.0",
- "::/0",
- "0::/0",
-)
-
class CLI(object):
def parse_cli(self):
parser = argparse.ArgumentParser(
# Global configuration flags
parser.add_argument("--debug", action="store_true",
help=_("Enable debug output"))
+ parser.add_argument("--quiet", action="store_true",
+ help=_("Enable quiet mode"))
# version
parser.add_argument("--version", action="version",
parser.add_argument("--database-password", required=True,
help=_("Database Password"), metavar=_("PASSWORD"))
+ # Write Database
+ write = subparsers.add_parser("write", help=_("Write database to file"))
+ write.set_defaults(func=self.handle_write)
+ write.add_argument("file", nargs=1, help=_("Database File"))
+ write.add_argument("--signing-key", nargs="?", type=open, help=_("Signing Key"))
+ write.add_argument("--vendor", nargs="?", help=_("Sets the vendor"))
+ write.add_argument("--description", nargs="?", help=_("Sets a description"))
+ write.add_argument("--license", nargs="?", help=_("Sets the license"))
+ write.add_argument("--version", type=int, help=_("Database Format Version"))
+
# Update WHOIS
update_whois = subparsers.add_parser("update-whois", help=_("Update WHOIS Information"))
update_whois.set_defaults(func=self.handle_update_whois)
+ # Update announcements
+ update_announcements = subparsers.add_parser("update-announcements",
+ help=_("Update BGP Annoucements"))
+ update_announcements.set_defaults(func=self.handle_update_announcements)
+ update_announcements.add_argument("server", nargs=1,
+ help=_("Route Server to connect to"), metavar=_("SERVER"))
+
+ # Update overrides
+ update_overrides = subparsers.add_parser("update-overrides",
+ help=_("Update overrides"),
+ )
+ update_overrides.add_argument(
+ "files", nargs="+", help=_("Files to import"),
+ )
+ update_overrides.set_defaults(func=self.handle_update_overrides)
+
args = parser.parse_args()
- # Enable debug logging
+ # Configure logging
if args.debug:
- log.setLevel(logging.DEBUG)
+ location.logger.set_level(logging.DEBUG)
+ elif args.quiet:
+ location.logger.set_level(logging.WARNING)
# Print usage if no action was given
if not "func" in args:
with db.transaction():
db.execute("""
+ -- announcements
+ CREATE TABLE IF NOT EXISTS announcements(network inet, autnum bigint,
+ first_seen_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
+ last_seen_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP);
+ CREATE UNIQUE INDEX IF NOT EXISTS announcements_networks ON announcements(network);
+ CREATE INDEX IF NOT EXISTS announcements_family ON announcements(family(network));
+
-- autnums
- CREATE TABLE IF NOT EXISTS autnums(number integer, name text);
+ CREATE TABLE IF NOT EXISTS autnums(number bigint, name text NOT NULL);
CREATE UNIQUE INDEX IF NOT EXISTS autnums_number ON autnums(number);
-- networks
- CREATE TABLE IF NOT EXISTS networks(network inet, autnum integer, country text);
+ CREATE TABLE IF NOT EXISTS networks(network inet, country text);
CREATE UNIQUE INDEX IF NOT EXISTS networks_network ON networks(network);
+ CREATE INDEX IF NOT EXISTS networks_search ON networks USING GIST(network inet_ops);
+
+ -- overrides
+ CREATE TABLE IF NOT EXISTS autnum_overrides(
+ number bigint NOT NULL,
+ name text,
+ country text,
+ is_anonymous_proxy boolean DEFAULT FALSE,
+ is_satellite_provider boolean DEFAULT FALSE,
+ is_anycast boolean DEFAULT FALSE
+ );
+ CREATE UNIQUE INDEX IF NOT EXISTS autnum_overrides_number
+ ON autnum_overrides(number);
+
+ CREATE TABLE IF NOT EXISTS network_overrides(
+ network inet NOT NULL,
+ country text,
+ is_anonymous_proxy boolean DEFAULT FALSE,
+ is_satellite_provider boolean DEFAULT FALSE,
+ is_anycast boolean DEFAULT FALSE
+ );
+ CREATE UNIQUE INDEX IF NOT EXISTS network_overrides_network
+ ON network_overrides(network);
""")
return db
+ def handle_write(self, ns):
+ """
+ Compiles a database in libloc format out of what is in the database
+ """
+ # Allocate a writer
+ writer = location.Writer(ns.signing_key)
+
+ # Set all metadata
+ if ns.vendor:
+ writer.vendor = ns.vendor
+
+ if ns.description:
+ writer.description = ns.description
+
+ if ns.license:
+ writer.license = ns.license
+
+ # Add all Autonomous Systems
+ log.info("Writing Autonomous Systems...")
+
+ # Select all ASes with a name
+ rows = self.db.query("""
+ SELECT
+ autnums.number AS number,
+ COALESCE(
+ (SELECT overrides.name FROM autnum_overrides overrides
+ WHERE overrides.number = autnums.number),
+ autnums.name
+ ) AS name
+ FROM autnums
+ WHERE name <> %s ORDER BY number
+ """, "")
+
+ for row in rows:
+ a = writer.add_as(row.number)
+ a.name = row.name
+
+ # Add all networks
+ log.info("Writing networks...")
+
+ # Select all known networks
+ rows = self.db.query("""
+ SELECT
+ DISTINCT ON (announcements.network)
+ announcements.network AS network,
+ announcements.autnum AS autnum,
+
+ -- Country
+ COALESCE(
+ (
+ SELECT country FROM network_overrides overrides
+ WHERE announcements.network <<= overrides.network
+ ORDER BY masklen(overrides.network) DESC
+ LIMIT 1
+ ),
+ (
+ SELECT country FROM autnum_overrides overrides
+ WHERE announcements.autnum = overrides.number
+ ),
+ networks.country
+ ) AS country,
+
+ -- Must be part of returned values for ORDER BY clause
+ masklen(networks.network) AS sort,
+
+ -- Flags
+ COALESCE(
+ (
+ SELECT is_anonymous_proxy FROM network_overrides overrides
+ WHERE announcements.network <<= overrides.network
+ ORDER BY masklen(overrides.network) DESC
+ LIMIT 1
+ ),
+ (
+ SELECT is_anonymous_proxy FROM autnum_overrides overrides
+ WHERE announcements.autnum = overrides.number
+ )
+ ) AS is_anonymous_proxy,
+ COALESCE(
+ (
+ SELECT is_satellite_provider FROM network_overrides overrides
+ WHERE announcements.network <<= overrides.network
+ ORDER BY masklen(overrides.network) DESC
+ LIMIT 1
+ ),
+ (
+ SELECT is_satellite_provider FROM autnum_overrides overrides
+ WHERE announcements.autnum = overrides.number
+ )
+ ) AS is_satellite_provider,
+ COALESCE(
+ (
+ SELECT is_anycast FROM network_overrides overrides
+ WHERE announcements.network <<= overrides.network
+ ORDER BY masklen(overrides.network) DESC
+ LIMIT 1
+ ),
+ (
+ SELECT is_anycast FROM autnum_overrides overrides
+ WHERE announcements.autnum = overrides.number
+ )
+ ) AS is_anycast
+ FROM announcements
+ LEFT JOIN networks ON announcements.network <<= networks.network
+ ORDER BY announcements.network, sort DESC
+ """)
+
+ for row in rows:
+ network = writer.add_network(row.network)
+
+ # Save AS & country
+ network.asn, network.country_code = row.autnum, row.country
+
+ # Set flags
+ if row.is_anonymous_proxy:
+ network.set_flag(location.NETWORK_FLAG_ANONYMOUS_PROXY)
+
+ if row.is_satellite_provider:
+ network.set_flag(location.NETWORK_FLAG_SATELLITE_PROVIDER)
+
+ if row.is_anycast:
+ network.set_flag(location.NETWORK_FLAG_ANYCAST)
+
+ # Write everything to file
+ log.info("Writing database to file...")
+ for file in ns.file:
+ writer.write(file)
+
def handle_update_whois(self, ns):
downloader = location.importer.Downloader()
prefix = int(prefix)
except:
log.warning("Invalid prefix: %s" % prefix)
+ return
# Fix prefix length for IPv4
if type == "ipv4":
"%s" % network, country,
)
+ def handle_update_announcements(self, ns):
+ server = ns.server[0]
+
+ # Pre-compile regular expression for routes
+ route = re.compile(b"^\*[\s\>]i([^\s]+).+?(\d+)\si\r\n", re.MULTILINE|re.DOTALL)
+
+ with telnetlib.Telnet(server) as t:
+ # Enable debug mode
+ #if ns.debug:
+ # t.set_debuglevel(10)
+
+ # Wait for console greeting
+ greeting = t.read_until(b"> ", timeout=30)
+ if not greeting:
+ log.error("Could not get a console prompt")
+ return 1
+
+ # Disable pagination
+ t.write(b"terminal length 0\n")
+
+ # Wait for the prompt to return
+ t.read_until(b"> ")
+
+ # Fetch the routing tables
+ with self.db.transaction():
+ for protocol in ("ipv6", "ipv4"):
+ log.info("Requesting %s routing table" % protocol)
+
+ # Request the full unicast routing table
+ t.write(b"show bgp %s unicast\n" % protocol.encode())
+
+ # Read entire header which ends with "Path"
+ t.read_until(b"Path\r\n")
+
+ while True:
+ # Try reading a full entry
+ # Those might be broken across multiple lines but ends with i
+ line = t.read_until(b"i\r\n", timeout=5)
+ if not line:
+ break
+
+ # Show line for debugging
+ #log.debug(repr(line))
+
+ # Try finding a route in here
+ m = route.match(line)
+ if m:
+ network, autnum = m.groups()
+
+ # Convert network to string
+ network = network.decode()
+
+ # Append /24 for IPv4 addresses
+ if not "/" in network and not ":" in network:
+ network = "%s/24" % network
+
+ # Convert AS number to integer
+ autnum = int(autnum)
+
+ log.info("Found announcement for %s by %s" % (network, autnum))
+
+ self.db.execute("INSERT INTO announcements(network, autnum) \
+ VALUES(%s, %s) ON CONFLICT (network) DO \
+ UPDATE SET autnum = excluded.autnum, last_seen_at = CURRENT_TIMESTAMP",
+ network, autnum,
+ )
+
+ log.info("Finished reading the %s routing table" % protocol)
+
+ # Purge anything we never want here
+ self.db.execute("""
+ -- Delete default routes
+ DELETE FROM announcements WHERE network = '::/0' OR network = '0.0.0.0/0';
+
+ -- Delete anything that is not global unicast address space
+ DELETE FROM announcements WHERE family(network) = 6 AND NOT network <<= '2000::/3';
+
+ -- DELETE "current network" address space
+ DELETE FROM announcements WHERE family(network) = 4 AND network <<= '0.0.0.0/8';
+
+ -- DELETE local loopback address space
+ DELETE FROM announcements WHERE family(network) = 4 AND network <<= '127.0.0.0/8';
+
+ -- DELETE RFC 1918 address space
+ DELETE FROM announcements WHERE family(network) = 4 AND network <<= '10.0.0.0/8';
+ DELETE FROM announcements WHERE family(network) = 4 AND network <<= '172.16.0.0/12';
+ DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.168.0.0/16';
+
+ -- DELETE test, benchmark and documentation address space
+ DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.0.0.0/24';
+ DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.0.2.0/24';
+ DELETE FROM announcements WHERE family(network) = 4 AND network <<= '198.18.0.0/15';
+ DELETE FROM announcements WHERE family(network) = 4 AND network <<= '198.51.100.0/24';
+ DELETE FROM announcements WHERE family(network) = 4 AND network <<= '203.0.113.0/24';
+
+ -- DELETE CGNAT address space (RFC 6598)
+ DELETE FROM announcements WHERE family(network) = 4 AND network <<= '100.64.0.0/10';
+
+ -- DELETE link local address space
+ DELETE FROM announcements WHERE family(network) = 4 AND network <<= '169.254.0.0/16';
+
+ -- DELETE IPv6 to IPv4 (6to4) address space
+ DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.88.99.0/24';
+
+ -- DELETE multicast and reserved address space
+ DELETE FROM announcements WHERE family(network) = 4 AND network <<= '224.0.0.0/4';
+ DELETE FROM announcements WHERE family(network) = 4 AND network <<= '240.0.0.0/4';
+
+ -- Delete networks that are too small to be in the global routing table
+ DELETE FROM announcements WHERE family(network) = 6 AND masklen(network) > 48;
+ DELETE FROM announcements WHERE family(network) = 4 AND masklen(network) > 24;
+
+ -- Delete any non-public or reserved ASNs
+ DELETE FROM announcements WHERE NOT (
+ (autnum >= 1 AND autnum <= 23455)
+ OR
+ (autnum >= 23457 AND autnum <= 64495)
+ OR
+ (autnum >= 131072 AND autnum <= 4199999999)
+ );
+
+ -- Delete everything that we have not seen for 14 days
+ DELETE FROM announcements WHERE last_seen_at <= CURRENT_TIMESTAMP - INTERVAL '14 days';
+ """)
+
+ def handle_update_overrides(self, ns):
+ with self.db.transaction():
+ # Drop all data that we have
+ self.db.execute("""
+ TRUNCATE TABLE autnum_overrides;
+ TRUNCATE TABLE network_overrides;
+ """)
+
+ for file in ns.files:
+ log.info("Reading %s..." % file)
+
+ with open(file, "rb") as f:
+ for type, block in location.importer.read_blocks(f):
+ if type == "net":
+ network = block.get("net")
+ # Try to parse and normalise the network
+ try:
+ network = ipaddress.ip_network(network, strict=False)
+ except ValueError as e:
+ log.warning("Invalid IP network: %s: %s" % (network, e))
+ continue
+
+ # Prevent that we overwrite all networks
+ if network.prefixlen == 0:
+ log.warning("Skipping %s: You cannot overwrite default" % network)
+ continue
+
+ self.db.execute("""
+ INSERT INTO network_overrides(
+ network,
+ country,
+ is_anonymous_proxy,
+ is_satellite_provider,
+ is_anycast
+ ) VALUES (%s, %s, %s, %s, %s)
+ ON CONFLICT (network) DO NOTHING""",
+ "%s" % network,
+ block.get("country"),
+ block.get("is-anonymous-proxy") == "yes",
+ block.get("is-satellite-provider") == "yes",
+ block.get("is-anycast") == "yes",
+ )
+
+ elif type == "aut-num":
+ autnum = block.get("aut-num")
+
+ # Check if AS number begins with "AS"
+ if not autnum.startswith("AS"):
+ log.warning("Invalid AS number: %s" % autnum)
+ continue
+
+ # Strip "AS"
+ autnum = autnum[2:]
+
+ self.db.execute("""
+ INSERT INTO autnum_overrides(
+ number,
+ name,
+ country,
+ is_anonymous_proxy,
+ is_satellite_provider,
+ is_anycast
+ ) VALUES(%s, %s, %s, %s, %s, %s)
+ ON CONFLICT DO NOTHING""",
+ autnum,
+ block.get("name"),
+ block.get("country"),
+ block.get("is-anonymous-proxy") == "yes",
+ block.get("is-satellite-provider") == "yes",
+ block.get("is-anycast") == "yes",
+ )
+
+ else:
+ log.warning("Unsupport type: %s" % type)
+
def split_line(line):
key, colon, val = line.partition(":")