2 ###############################################################################
4 # libloc - A library to determine the location of someone on the Internet #
6 # Copyright (C) 2020-2021 IPFire Development Team <info@ipfire.org> #
8 # This library is free software; you can redistribute it and/or #
9 # modify it under the terms of the GNU Lesser General Public #
10 # License as published by the Free Software Foundation; either #
11 # version 2.1 of the License, or (at your option) any later version. #
13 # This library is distributed in the hope that it will be useful, #
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
16 # Lesser General Public License for more details. #
18 ###############################################################################
29 # Load our location module
31 import location
.database
32 import location
.importer
33 from location
.i18n
import _
36 log
= logging
.getLogger("location.importer")
41 parser
= argparse
.ArgumentParser(
42 description
=_("Location Importer Command Line Interface"),
44 subparsers
= parser
.add_subparsers()
46 # Global configuration flags
47 parser
.add_argument("--debug", action
="store_true",
48 help=_("Enable debug output"))
49 parser
.add_argument("--quiet", action
="store_true",
50 help=_("Enable quiet mode"))
53 parser
.add_argument("--version", action
="version",
54 version
="%(prog)s @VERSION@")
57 parser
.add_argument("--database-host", required
=True,
58 help=_("Database Hostname"), metavar
=_("HOST"))
59 parser
.add_argument("--database-name", required
=True,
60 help=_("Database Name"), metavar
=_("NAME"))
61 parser
.add_argument("--database-username", required
=True,
62 help=_("Database Username"), metavar
=_("USERNAME"))
63 parser
.add_argument("--database-password", required
=True,
64 help=_("Database Password"), metavar
=_("PASSWORD"))
67 write
= subparsers
.add_parser("write", help=_("Write database to file"))
68 write
.set_defaults(func
=self
.handle_write
)
69 write
.add_argument("file", nargs
=1, help=_("Database File"))
70 write
.add_argument("--signing-key", nargs
="?", type=open, help=_("Signing Key"))
71 write
.add_argument("--backup-signing-key", nargs
="?", type=open, help=_("Backup Signing Key"))
72 write
.add_argument("--vendor", nargs
="?", help=_("Sets the vendor"))
73 write
.add_argument("--description", nargs
="?", help=_("Sets a description"))
74 write
.add_argument("--license", nargs
="?", help=_("Sets the license"))
75 write
.add_argument("--version", type=int, help=_("Database Format Version"))
78 update_whois
= subparsers
.add_parser("update-whois", help=_("Update WHOIS Information"))
79 update_whois
.set_defaults(func
=self
.handle_update_whois
)
81 # Update announcements
82 update_announcements
= subparsers
.add_parser("update-announcements",
83 help=_("Update BGP Annoucements"))
84 update_announcements
.set_defaults(func
=self
.handle_update_announcements
)
85 update_announcements
.add_argument("server", nargs
=1,
86 help=_("Route Server to connect to"), metavar
=_("SERVER"))
89 update_overrides
= subparsers
.add_parser("update-overrides",
90 help=_("Update overrides"),
92 update_overrides
.add_argument(
93 "files", nargs
="+", help=_("Files to import"),
95 update_overrides
.set_defaults(func
=self
.handle_update_overrides
)
98 import_countries
= subparsers
.add_parser("import-countries",
99 help=_("Import countries"),
101 import_countries
.add_argument("file", nargs
=1, type=argparse
.FileType("r"),
102 help=_("File to import"))
103 import_countries
.set_defaults(func
=self
.handle_import_countries
)
105 args
= parser
.parse_args()
109 location
.logger
.set_level(logging
.DEBUG
)
111 location
.logger
.set_level(logging
.WARNING
)
113 # Print usage if no action was given
114 if not "func" in args
:
121 # Parse command line arguments
122 args
= self
.parse_cli()
124 # Initialise database
125 self
.db
= self
._setup
_database
(args
)
128 ret
= args
.func(args
)
130 # Return with exit code
134 # Otherwise just exit
137 def _setup_database(self
, ns
):
139 Initialise the database
141 # Connect to database
142 db
= location
.database
.Connection(
143 host
=ns
.database_host
, database
=ns
.database_name
,
144 user
=ns
.database_username
, password
=ns
.database_password
,
147 with db
.transaction():
150 CREATE TABLE IF NOT EXISTS announcements(network inet, autnum bigint,
151 first_seen_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
152 last_seen_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP);
153 CREATE UNIQUE INDEX IF NOT EXISTS announcements_networks ON announcements(network);
154 CREATE INDEX IF NOT EXISTS announcements_family ON announcements(family(network));
155 CREATE INDEX IF NOT EXISTS announcements_search ON announcements USING GIST(network inet_ops);
158 CREATE TABLE IF NOT EXISTS autnums(number bigint, name text NOT NULL);
159 ALTER TABLE autnums ADD COLUMN IF NOT EXISTS source text NOT NULL;
160 CREATE UNIQUE INDEX IF NOT EXISTS autnums_number ON autnums(number);
163 CREATE TABLE IF NOT EXISTS countries(
164 country_code text NOT NULL, name text NOT NULL, continent_code text NOT NULL);
165 CREATE UNIQUE INDEX IF NOT EXISTS countries_country_code ON countries(country_code);
168 CREATE TABLE IF NOT EXISTS networks(network inet, country text);
169 ALTER TABLE networks ADD COLUMN IF NOT EXISTS original_countries text[];
170 ALTER TABLE networks ADD COLUMN IF NOT EXISTS source text NOT NULL;
171 CREATE UNIQUE INDEX IF NOT EXISTS networks_network ON networks(network);
172 CREATE INDEX IF NOT EXISTS networks_family ON networks USING BTREE(family(network));
173 CREATE INDEX IF NOT EXISTS networks_search ON networks USING GIST(network inet_ops);
176 CREATE TABLE IF NOT EXISTS autnum_overrides(
177 number bigint NOT NULL,
180 is_anonymous_proxy boolean,
181 is_satellite_provider boolean,
184 CREATE UNIQUE INDEX IF NOT EXISTS autnum_overrides_number
185 ON autnum_overrides(number);
186 ALTER TABLE autnum_overrides ADD COLUMN IF NOT EXISTS source text;
187 ALTER TABLE autnum_overrides ADD COLUMN IF NOT EXISTS is_drop boolean;
189 CREATE TABLE IF NOT EXISTS network_overrides(
190 network inet NOT NULL,
192 is_anonymous_proxy boolean,
193 is_satellite_provider boolean,
196 CREATE UNIQUE INDEX IF NOT EXISTS network_overrides_network
197 ON network_overrides(network);
198 CREATE INDEX IF NOT EXISTS network_overrides_search
199 ON network_overrides USING GIST(network inet_ops);
200 ALTER TABLE network_overrides ADD COLUMN IF NOT EXISTS source text;
201 ALTER TABLE network_overrides ADD COLUMN IF NOT EXISTS is_drop boolean;
206 def handle_write(self
, ns
):
208 Compiles a database in libloc format out of what is in the database
211 writer
= location
.Writer(ns
.signing_key
, ns
.backup_signing_key
)
215 writer
.vendor
= ns
.vendor
218 writer
.description
= ns
.description
221 writer
.license
= ns
.license
223 # Add all Autonomous Systems
224 log
.info("Writing Autonomous Systems...")
226 # Select all ASes with a name
227 rows
= self
.db
.query("""
229 autnums.number AS number,
231 (SELECT overrides.name FROM autnum_overrides overrides
232 WHERE overrides.number = autnums.number),
236 WHERE name <> %s ORDER BY number
240 a
= writer
.add_as(row
.number
)
244 log
.info("Writing networks...")
246 # Select all known networks
247 rows
= self
.db
.query("""
248 -- Return a list of those networks enriched with all
249 -- other information that we store in the database
251 DISTINCT ON (network)
258 SELECT country FROM network_overrides overrides
259 WHERE networks.network <<= overrides.network
260 ORDER BY masklen(overrides.network) DESC
264 SELECT country FROM autnum_overrides overrides
265 WHERE networks.autnum = overrides.number
273 SELECT is_anonymous_proxy FROM network_overrides overrides
274 WHERE networks.network <<= overrides.network
275 ORDER BY masklen(overrides.network) DESC
279 SELECT is_anonymous_proxy FROM autnum_overrides overrides
280 WHERE networks.autnum = overrides.number
283 ) AS is_anonymous_proxy,
286 SELECT is_satellite_provider FROM network_overrides overrides
287 WHERE networks.network <<= overrides.network
288 ORDER BY masklen(overrides.network) DESC
292 SELECT is_satellite_provider FROM autnum_overrides overrides
293 WHERE networks.autnum = overrides.number
296 ) AS is_satellite_provider,
299 SELECT is_anycast FROM network_overrides overrides
300 WHERE networks.network <<= overrides.network
301 ORDER BY masklen(overrides.network) DESC
305 SELECT is_anycast FROM autnum_overrides overrides
306 WHERE networks.autnum = overrides.number
312 SELECT is_drop FROM network_overrides overrides
313 WHERE networks.network <<= overrides.network
314 ORDER BY masklen(overrides.network) DESC
318 SELECT is_drop FROM autnum_overrides overrides
319 WHERE networks.autnum = overrides.number
325 known_networks.network AS network,
326 announcements.autnum AS autnum,
327 networks.country AS country,
329 -- Must be part of returned values for ORDER BY clause
330 masklen(announcements.network) AS sort_a,
331 masklen(networks.network) AS sort_b
333 SELECT network FROM announcements
335 SELECT network FROM networks
337 SELECT network FROM network_overrides
340 announcements ON known_networks.network <<= announcements.network
342 networks ON known_networks.network <<= networks.network
344 known_networks.network,
351 network
= writer
.add_network(row
.network
)
355 network
.country_code
= row
.country
359 network
.asn
= row
.autnum
362 if row
.is_anonymous_proxy
:
363 network
.set_flag(location
.NETWORK_FLAG_ANONYMOUS_PROXY
)
365 if row
.is_satellite_provider
:
366 network
.set_flag(location
.NETWORK_FLAG_SATELLITE_PROVIDER
)
369 network
.set_flag(location
.NETWORK_FLAG_ANYCAST
)
372 network
.set_flag(location
.NETWORK_FLAG_DROP
)
375 log
.info("Writing countries...")
376 rows
= self
.db
.query("SELECT * FROM countries ORDER BY country_code")
379 c
= writer
.add_country(row
.country_code
)
380 c
.continent_code
= row
.continent_code
383 # Write everything to file
384 log
.info("Writing database to file...")
388 def handle_update_whois(self
, ns
):
389 downloader
= location
.importer
.Downloader()
391 # Download all sources
392 with self
.db
.transaction():
393 # Create some temporary tables to store parsed data
395 CREATE TEMPORARY TABLE _autnums(number integer, organization text, source text NOT NULL)
397 CREATE UNIQUE INDEX _autnums_number ON _autnums(number);
399 CREATE TEMPORARY TABLE _organizations(handle text, name text NOT NULL, source text NOT NULL)
401 CREATE UNIQUE INDEX _organizations_handle ON _organizations(handle);
403 CREATE TEMPORARY TABLE _rirdata(network inet NOT NULL, country text NOT NULL, original_countries text[] NOT NULL, source text NOT NULL)
405 CREATE INDEX _rirdata_search ON _rirdata USING BTREE(family(network), masklen(network));
406 CREATE UNIQUE INDEX _rirdata_network ON _rirdata(network);
409 # Remove all previously imported content
411 TRUNCATE TABLE networks;
414 # Fetch all valid country codes to check parsed networks aganist...
415 rows
= self
.db
.query("SELECT * FROM countries ORDER BY country_code")
419 validcountries
.append(row
.country_code
)
421 for source_key
in location
.importer
.WHOIS_SOURCES
:
422 for single_url
in location
.importer
.WHOIS_SOURCES
[source_key
]:
423 with downloader
.request(single_url
, return_blocks
=True) as f
:
425 self
._parse
_block
(block
, source_key
, validcountries
)
427 # Process all parsed networks from every RIR we happen to have access to,
428 # insert the largest network chunks into the networks table immediately...
429 families
= self
.db
.query("SELECT DISTINCT family(network) AS family FROM _rirdata ORDER BY family(network)")
431 for family
in (row
.family
for row
in families
):
432 smallest
= self
.db
.get("SELECT MIN(masklen(network)) AS prefix FROM _rirdata WHERE family(network) = %s", family
)
434 self
.db
.execute("INSERT INTO networks(network, country, original_countries, source) \
435 SELECT network, country, original_countries, source FROM _rirdata WHERE masklen(network) = %s AND family(network) = %s", smallest
.prefix
, family
)
437 # ... determine any other prefixes for this network family, ...
438 prefixes
= self
.db
.query("SELECT DISTINCT masklen(network) AS prefix FROM _rirdata \
439 WHERE family(network) = %s ORDER BY masklen(network) ASC OFFSET 1", family
)
441 # ... and insert networks with this prefix in case they provide additional
442 # information (i. e. subnet of a larger chunk with a different country)
443 for prefix
in (row
.prefix
for row
in prefixes
):
449 _rirdata.original_countries,
454 family(_rirdata.network) = %s
456 masklen(_rirdata.network) = %s
460 DISTINCT ON (c.network)
463 c.original_countries,
465 masklen(networks.network),
466 networks.country AS parent_country
472 c.network << networks.network
475 masklen(networks.network) DESC NULLS LAST
478 networks(network, country, original_countries, source)
487 parent_country IS NULL
489 country <> parent_country
490 ON CONFLICT DO NOTHING""",
495 INSERT INTO autnums(number, name, source)
496 SELECT _autnums.number, _organizations.name, _organizations.source FROM _autnums
497 JOIN _organizations ON _autnums.organization = _organizations.handle
498 ON CONFLICT (number) DO UPDATE SET name = excluded.name;
501 # Download all extended sources
502 for source_key
in location
.importer
.EXTENDED_SOURCES
:
503 for single_url
in location
.importer
.EXTENDED_SOURCES
[source_key
]:
504 with self
.db
.transaction():
506 with downloader
.request(single_url
) as f
:
508 self
._parse
_line
(line
, source_key
, validcountries
)
510 # Download and import (technical) AS names from ARIN
511 self
._import
_as
_names
_from
_arin
()
513 def _check_parsed_network(self
, network
):
515 Assistive function to detect and subsequently sort out parsed
516 networks from RIR data (both Whois and so-called "extended sources"),
519 (a) not globally routable (RFC 1918 space, et al.)
520 (b) covering a too large chunk of the IP address space (prefix length
521 is < 7 for IPv4 networks, and < 10 for IPv6)
522 (c) "0.0.0.0" or "::" as a network address
523 (d) are too small for being publicly announced (we have decided not to
524 process them at the moment, as they significantly enlarge our
525 database without providing very helpful additional information)
527 This unfortunately is necessary due to brain-dead clutter across
528 various RIR databases, causing mismatches and eventually disruptions.
530 We will return False in case a network is not suitable for adding
531 it to our database, and True otherwise.
534 if not network
or not (isinstance(network
, ipaddress
.IPv4Network
) or isinstance(network
, ipaddress
.IPv6Network
)):
537 if not network
.is_global
:
538 log
.debug("Skipping non-globally routable network: %s" % network
)
541 if network
.version
== 4:
542 if network
.prefixlen
< 7:
543 log
.debug("Skipping too big IP chunk: %s" % network
)
546 if network
.prefixlen
> 24:
547 log
.debug("Skipping network too small to be publicly announced: %s" % network
)
550 if str(network
.network_address
) == "0.0.0.0":
551 log
.debug("Skipping network based on 0.0.0.0: %s" % network
)
554 elif network
.version
== 6:
555 if network
.prefixlen
< 10:
556 log
.debug("Skipping too big IP chunk: %s" % network
)
559 if network
.prefixlen
> 48:
560 log
.debug("Skipping network too small to be publicly announced: %s" % network
)
563 if str(network
.network_address
) == "::":
564 log
.debug("Skipping network based on '::': %s" % network
)
568 # This should not happen...
569 log
.warning("Skipping network of unknown family, this should not happen: %s" % network
)
572 # In case we have made it here, the network is considered to
573 # be suitable for libloc consumption...
576 def _parse_block(self
, block
, source_key
, validcountries
= None):
577 # Get first line to find out what type of block this is
581 if line
.startswith("aut-num:"):
582 return self
._parse
_autnum
_block
(block
, source_key
)
585 if line
.startswith("inet6num:") or line
.startswith("inetnum:"):
586 return self
._parse
_inetnum
_block
(block
, source_key
, validcountries
)
589 elif line
.startswith("organisation:"):
590 return self
._parse
_org
_block
(block
, source_key
)
592 def _parse_autnum_block(self
, block
, source_key
):
596 key
, val
= split_line(line
)
599 m
= re
.match(r
"^(AS|as)(\d+)", val
)
601 autnum
["asn"] = m
.group(2)
604 autnum
[key
] = val
.upper()
610 # Insert into database
611 self
.db
.execute("INSERT INTO _autnums(number, organization, source) \
612 VALUES(%s, %s, %s) ON CONFLICT (number) DO UPDATE SET \
613 organization = excluded.organization",
614 autnum
.get("asn"), autnum
.get("org"), source_key
,
617 def _parse_inetnum_block(self
, block
, source_key
, validcountries
= None):
618 log
.debug("Parsing inetnum block:")
625 key
, val
= split_line(line
)
627 # Filter any inetnum records which are only referring to IP space
628 # not managed by that specific RIR...
630 if re
.match(r
"(ERX-NETBLOCK|(AFRINIC|ARIN|LACNIC|RIPE)-CIDR-BLOCK|IANA-NETBLOCK-\d{1,3}|NON-RIPE-NCC-MANAGED-ADDRESS-BLOCK)", val
.strip()):
631 log
.debug("Skipping record indicating historic/orphaned data: %s" % val
.strip())
635 start_address
, delim
, end_address
= val
.partition("-")
637 # Strip any excess space
638 start_address
, end_address
= start_address
.rstrip(), end_address
.strip()
640 # Convert to IP address
642 start_address
= ipaddress
.ip_address(start_address
)
643 end_address
= ipaddress
.ip_address(end_address
)
645 log
.warning("Could not parse line: %s" % line
)
648 inetnum
["inetnum"] = list(ipaddress
.summarize_address_range(start_address
, end_address
))
650 elif key
== "inet6num":
651 inetnum
[key
] = [ipaddress
.ip_network(val
, strict
=False)]
653 elif key
== "country":
656 # Catch RIR data objects with more than one country code...
657 if not key
in inetnum
:
660 if val
in inetnum
.get("country"):
661 # ... but keep this list distinct...
664 inetnum
[key
].append(val
)
667 if not inetnum
or not "country" in inetnum
:
670 # Prepare skipping objects with unknown country codes...
671 invalidcountries
= [singlecountry
for singlecountry
in inetnum
.get("country") if singlecountry
not in validcountries
]
673 # Iterate through all networks enumerated from above, check them for plausibility and insert
674 # them into the database, if _check_parsed_network() succeeded
675 for single_network
in inetnum
.get("inet6num") or inetnum
.get("inetnum"):
676 if self
._check
_parsed
_network
(single_network
):
678 # Skip objects with unknown country codes if they are valid to avoid log spam...
679 if validcountries
and invalidcountries
:
680 log
.warning("Skipping network with bogus countr(y|ies) %s (original countries: %s): %s" % \
681 (invalidcountries
, inetnum
.get("country"), inetnum
.get("inet6num") or inetnum
.get("inetnum")))
683 # Everything is fine here, run INSERT statement...
684 self
.db
.execute("INSERT INTO _rirdata(network, country, original_countries, source) \
685 VALUES(%s, %s, %s, %s) ON CONFLICT (network) DO UPDATE SET country = excluded.country",
686 "%s" % single_network
, inetnum
.get("country")[0], inetnum
.get("country"), source_key
,
689 def _parse_org_block(self
, block
, source_key
):
693 key
, val
= split_line(line
)
695 if key
== "organisation":
696 org
[key
] = val
.upper()
697 elif key
== "org-name":
704 self
.db
.execute("INSERT INTO _organizations(handle, name, source) \
705 VALUES(%s, %s, %s) ON CONFLICT (handle) DO \
706 UPDATE SET name = excluded.name",
707 org
.get("organisation"), org
.get("org-name"), source_key
,
710 def _parse_line(self
, line
, source_key
, validcountries
= None):
712 if line
.startswith("2"):
716 if line
.startswith("#"):
720 registry
, country_code
, type, line
= line
.split("|", 3)
722 log
.warning("Could not parse line: %s" % line
)
725 # Skip any lines that are for stats only or do not have a country
726 # code at all (avoids log spam below)
727 if not country_code
or country_code
== '*':
730 # Skip objects with unknown country codes
731 if validcountries
and country_code
not in validcountries
:
732 log
.warning("Skipping line with bogus country '%s': %s" % \
733 (country_code
, line
))
736 if type in ("ipv6", "ipv4"):
737 return self
._parse
_ip
_line
(country_code
, type, line
, source_key
)
739 def _parse_ip_line(self
, country
, type, line
, source_key
):
741 address
, prefix
, date
, status
, organization
= line
.split("|")
745 # Try parsing the line without organization
747 address
, prefix
, date
, status
= line
.split("|")
749 log
.warning("Unhandled line format: %s" % line
)
752 # Skip anything that isn't properly assigned
753 if not status
in ("assigned", "allocated"):
756 # Cast prefix into an integer
760 log
.warning("Invalid prefix: %s" % prefix
)
763 # Fix prefix length for IPv4
765 prefix
= 32 - int(math
.log(prefix
, 2))
767 # Try to parse the address
769 network
= ipaddress
.ip_network("%s/%s" % (address
, prefix
), strict
=False)
771 log
.warning("Invalid IP address: %s" % address
)
774 if not self
._check
_parsed
_network
(network
):
777 self
.db
.execute("INSERT INTO networks(network, country, original_countries, source) \
778 VALUES(%s, %s, %s, %s) ON CONFLICT (network) DO \
779 UPDATE SET country = excluded.country",
780 "%s" % network
, country
, [country
], source_key
,
783 def _import_as_names_from_arin(self
):
784 downloader
= location
.importer
.Downloader()
786 # XXX: Download AS names file from ARIN (note that these names appear to be quite
787 # technical, not intended for human consumption, as description fields in
788 # organisation handles for other RIRs are - however, this is what we have got,
789 # and in some cases, it might be still better than nothing)
790 with downloader
.request("https://ftp.arin.net/info/asn.txt", return_blocks
=False) as f
:
792 # Convert binary line to string...
795 # ... valid lines start with a space, followed by the number of the Autonomous System ...
796 if not line
.startswith(" "):
799 # Split line and check if there is a valid ASN in it...
800 asn
, name
= line
.split()[0:2]
805 log
.debug("Skipping ARIN AS names line not containing an integer for ASN")
808 if not ((1 <= asn
and asn
<= 23455) or (23457 <= asn
and asn
<= 64495) or (131072 <= asn
and asn
<= 4199999999)):
809 log
.debug("Skipping ARIN AS names line not containing a valid ASN: %s" % asn
)
812 # Skip any AS name that appears to be a placeholder for a different RIR or entity...
813 if re
.match(r
"^(ASN-BLK|)(AFCONC|AFRINIC|APNIC|ASNBLK|DNIC|LACNIC|RIPE|IANA)(\d?$|\-.*)", name
):
816 # Bail out in case the AS name contains anything we do not expect here...
817 if re
.search(r
"[^a-zA-Z0-9-_]", name
):
818 log
.debug("Skipping ARIN AS name for %s containing invalid characters: %s" % \
821 # Things look good here, run INSERT statement and skip this one if we already have
822 # a (better?) name for this Autonomous System...
828 ) VALUES (%s, %s, %s)
829 ON CONFLICT (number) DO NOTHING""",
835 def handle_update_announcements(self
, ns
):
836 server
= ns
.server
[0]
838 with self
.db
.transaction():
839 if server
.startswith("/"):
840 self
._handle
_update
_announcements
_from
_bird
(server
)
842 self
._handle
_update
_announcements
_from
_telnet
(server
)
844 # Purge anything we never want here
846 -- Delete default routes
847 DELETE FROM announcements WHERE network = '::/0' OR network = '0.0.0.0/0';
849 -- Delete anything that is not global unicast address space
850 DELETE FROM announcements WHERE family(network) = 6 AND NOT network <<= '2000::/3';
852 -- DELETE "current network" address space
853 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '0.0.0.0/8';
855 -- DELETE local loopback address space
856 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '127.0.0.0/8';
858 -- DELETE RFC 1918 address space
859 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '10.0.0.0/8';
860 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '172.16.0.0/12';
861 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.168.0.0/16';
863 -- DELETE test, benchmark and documentation address space
864 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.0.0.0/24';
865 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.0.2.0/24';
866 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '198.18.0.0/15';
867 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '198.51.100.0/24';
868 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '203.0.113.0/24';
870 -- DELETE CGNAT address space (RFC 6598)
871 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '100.64.0.0/10';
873 -- DELETE link local address space
874 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '169.254.0.0/16';
876 -- DELETE IPv6 to IPv4 (6to4) address space (RFC 3068)
877 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.88.99.0/24';
878 DELETE FROM announcements WHERE family(network) = 6 AND network <<= '2002::/16';
880 -- DELETE multicast and reserved address space
881 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '224.0.0.0/4';
882 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '240.0.0.0/4';
884 -- Delete networks that are too small to be in the global routing table
885 DELETE FROM announcements WHERE family(network) = 6 AND masklen(network) > 48;
886 DELETE FROM announcements WHERE family(network) = 4 AND masklen(network) > 24;
888 -- Delete any non-public or reserved ASNs
889 DELETE FROM announcements WHERE NOT (
890 (autnum >= 1 AND autnum <= 23455)
892 (autnum >= 23457 AND autnum <= 64495)
894 (autnum >= 131072 AND autnum <= 4199999999)
897 -- Delete everything that we have not seen for 14 days
898 DELETE FROM announcements WHERE last_seen_at <= CURRENT_TIMESTAMP - INTERVAL '14 days';
901 def _handle_update_announcements_from_bird(self
, server
):
902 # Pre-compile the regular expression for faster searching
903 route
= re
.compile(b
"^\s(.+?)\s+.+?\[AS(.*?).\]$")
905 log
.info("Requesting routing table from Bird (%s)" % server
)
907 # Send command to list all routes
908 for line
in self
._bird
_cmd
(server
, "show route"):
909 m
= route
.match(line
)
911 log
.debug("Could not parse line: %s" % line
.decode())
914 # Fetch the extracted network and ASN
915 network
, autnum
= m
.groups()
917 # Insert it into the database
918 self
.db
.execute("INSERT INTO announcements(network, autnum) \
919 VALUES(%s, %s) ON CONFLICT (network) DO \
920 UPDATE SET autnum = excluded.autnum, last_seen_at = CURRENT_TIMESTAMP",
921 network
.decode(), autnum
.decode(),
924 def _handle_update_announcements_from_telnet(self
, server
):
925 # Pre-compile regular expression for routes
926 route
= re
.compile(b
"^\*[\s\>]i([^\s]+).+?(\d+)\si\r\n", re
.MULTILINE|re
.DOTALL
)
928 with telnetlib
.Telnet(server
) as t
:
931 # t.set_debuglevel(10)
933 # Wait for console greeting
934 greeting
= t
.read_until(b
"> ", timeout
=30)
936 log
.error("Could not get a console prompt")
940 t
.write(b
"terminal length 0\n")
942 # Wait for the prompt to return
945 # Fetch the routing tables
946 for protocol
in ("ipv6", "ipv4"):
947 log
.info("Requesting %s routing table" % protocol
)
949 # Request the full unicast routing table
950 t
.write(b
"show bgp %s unicast\n" % protocol
.encode())
952 # Read entire header which ends with "Path"
953 t
.read_until(b
"Path\r\n")
956 # Try reading a full entry
957 # Those might be broken across multiple lines but ends with i
958 line
= t
.read_until(b
"i\r\n", timeout
=5)
962 # Show line for debugging
963 #log.debug(repr(line))
965 # Try finding a route in here
966 m
= route
.match(line
)
968 network
, autnum
= m
.groups()
970 # Convert network to string
971 network
= network
.decode()
973 # Append /24 for IPv4 addresses
974 if not "/" in network
and not ":" in network
:
975 network
= "%s/24" % network
977 # Convert AS number to integer
980 log
.info("Found announcement for %s by %s" % (network
, autnum
))
982 self
.db
.execute("INSERT INTO announcements(network, autnum) \
983 VALUES(%s, %s) ON CONFLICT (network) DO \
984 UPDATE SET autnum = excluded.autnum, last_seen_at = CURRENT_TIMESTAMP",
988 log
.info("Finished reading the %s routing table" % protocol
)
990 def _bird_cmd(self
, socket_path
, command
):
991 # Connect to the socket
992 s
= socket
.socket(socket
.AF_UNIX
, socket
.SOCK_STREAM
)
993 s
.connect(socket_path
)
995 # Allocate some buffer
999 s
.send(b
"%s\n" % command
.encode())
1002 # Fill up the buffer
1003 buffer += s
.recv(4096)
1006 # Search for the next newline
1007 pos
= buffer.find(b
"\n")
1009 # If we cannot find one, we go back and read more data
1013 # Cut after the newline character
1016 # Split the line we want and keep the rest in buffer
1017 line
, buffer = buffer[:pos
], buffer[pos
:]
1019 # Look for the end-of-output indicator
1020 if line
== b
"0000 \n":
1023 # Otherwise return the line
1026 def handle_update_overrides(self
, ns
):
1027 with self
.db
.transaction():
1028 # Drop all data that we have
1030 TRUNCATE TABLE autnum_overrides;
1031 TRUNCATE TABLE network_overrides;
1034 for file in ns
.files
:
1035 log
.info("Reading %s..." % file)
1037 with
open(file, "rb") as f
:
1038 for type, block
in location
.importer
.read_blocks(f
):
1040 network
= block
.get("net")
1041 # Try to parse and normalise the network
1043 network
= ipaddress
.ip_network(network
, strict
=False)
1044 except ValueError as e
:
1045 log
.warning("Invalid IP network: %s: %s" % (network
, e
))
1048 # Prevent that we overwrite all networks
1049 if network
.prefixlen
== 0:
1050 log
.warning("Skipping %s: You cannot overwrite default" % network
)
1054 INSERT INTO network_overrides(
1059 is_satellite_provider,
1062 ) VALUES (%s, %s, %s, %s, %s, %s, %s)
1063 ON CONFLICT (network) DO NOTHING""",
1065 block
.get("country"),
1067 self
._parse
_bool
(block
, "is-anonymous-proxy"),
1068 self
._parse
_bool
(block
, "is-satellite-provider"),
1069 self
._parse
_bool
(block
, "is-anycast"),
1070 self
._parse
_bool
(block
, "drop"),
1073 elif type == "aut-num":
1074 autnum
= block
.get("aut-num")
1076 # Check if AS number begins with "AS"
1077 if not autnum
.startswith("AS"):
1078 log
.warning("Invalid AS number: %s" % autnum
)
1085 INSERT INTO autnum_overrides(
1091 is_satellite_provider,
1094 ) VALUES(%s, %s, %s, %s, %s, %s, %s, %s)
1095 ON CONFLICT DO NOTHING""",
1098 block
.get("country"),
1100 self
._parse
_bool
(block
, "is-anonymous-proxy"),
1101 self
._parse
_bool
(block
, "is-satellite-provider"),
1102 self
._parse
_bool
(block
, "is-anycast"),
1103 self
._parse
_bool
(block
, "drop"),
1107 log
.warning("Unsupported type: %s" % type)
1110 def _parse_bool(block
, key
):
1111 val
= block
.get(key
)
1113 # There is no point to proceed when we got None
1117 # Convert to lowercase
1121 if val
in ("yes", "1"):
1125 if val
in ("no", "0"):
1131 def handle_import_countries(self
, ns
):
1132 with self
.db
.transaction():
1133 # Drop all data that we have
1134 self
.db
.execute("TRUNCATE TABLE countries")
1136 for file in ns
.file:
1138 line
= line
.rstrip()
1140 # Ignore any comments
1141 if line
.startswith("#"):
1145 country_code
, continent_code
, name
= line
.split(maxsplit
=2)
1147 log
.warning("Could not parse line: %s" % line
)
1150 self
.db
.execute("INSERT INTO countries(country_code, name, continent_code) \
1151 VALUES(%s, %s, %s) ON CONFLICT DO NOTHING", country_code
, name
, continent_code
)
1154 def split_line(line
):
1155 key
, colon
, val
= line
.partition(":")
1157 # Strip any excess space
1164 # Run the command line interface