]> git.ipfire.org Git - location/libloc.git/blame - src/python/location-importer.in
Revert "Revert "location-importer.in: only import relevant data from AFRINIC, APNIC...
[location/libloc.git] / src / python / location-importer.in
CommitLineData
78ff0cf2
MT
1#!/usr/bin/python3
2###############################################################################
3# #
4# libloc - A library to determine the location of someone on the Internet #
5# #
6# Copyright (C) 2020 IPFire Development Team <info@ipfire.org> #
7# #
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. #
12# #
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. #
17# #
18###############################################################################
19
20import argparse
6ffd06b5 21import ipaddress
78ff0cf2 22import logging
6ffd06b5
MT
23import math
24import re
22d8d199 25import socket
78ff0cf2 26import sys
83d61c46 27import telnetlib
78ff0cf2
MT
28
29# Load our location module
30import location
29c6fa22 31import location.database
3192b66c 32import location.importer
78ff0cf2
MT
33from location.i18n import _
34
35# Initialise logging
36log = logging.getLogger("location.importer")
37log.propagate = 1
38
39class CLI(object):
40 def parse_cli(self):
41 parser = argparse.ArgumentParser(
42 description=_("Location Importer Command Line Interface"),
43 )
6ffd06b5 44 subparsers = parser.add_subparsers()
78ff0cf2
MT
45
46 # Global configuration flags
47 parser.add_argument("--debug", action="store_true",
48 help=_("Enable debug output"))
bc1f5f53
MT
49 parser.add_argument("--quiet", action="store_true",
50 help=_("Enable quiet mode"))
78ff0cf2
MT
51
52 # version
53 parser.add_argument("--version", action="version",
54 version="%(prog)s @VERSION@")
55
29c6fa22
MT
56 # Database
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"))
65
0983f3dd
MT
66 # Write Database
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"))
1164d876 71 write.add_argument("--backup-signing-key", nargs="?", type=open, help=_("Backup Signing Key"))
0983f3dd
MT
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"))
b904896a 75 write.add_argument("--version", type=int, help=_("Database Format Version"))
0983f3dd 76
6ffd06b5
MT
77 # Update WHOIS
78 update_whois = subparsers.add_parser("update-whois", help=_("Update WHOIS Information"))
79 update_whois.set_defaults(func=self.handle_update_whois)
80
83d61c46
MT
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"))
87
d7fc3057
MT
88 # Update overrides
89 update_overrides = subparsers.add_parser("update-overrides",
90 help=_("Update overrides"),
91 )
92 update_overrides.add_argument(
93 "files", nargs="+", help=_("Files to import"),
94 )
95 update_overrides.set_defaults(func=self.handle_update_overrides)
96
8084b33a
MT
97 # Import countries
98 import_countries = subparsers.add_parser("import-countries",
99 help=_("Import countries"),
100 )
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)
104
78ff0cf2
MT
105 args = parser.parse_args()
106
bc1f5f53 107 # Configure logging
78ff0cf2 108 if args.debug:
f9de5e61 109 location.logger.set_level(logging.DEBUG)
bc1f5f53
MT
110 elif args.quiet:
111 location.logger.set_level(logging.WARNING)
78ff0cf2 112
6ffd06b5
MT
113 # Print usage if no action was given
114 if not "func" in args:
115 parser.print_usage()
116 sys.exit(2)
117
78ff0cf2
MT
118 return args
119
120 def run(self):
121 # Parse command line arguments
122 args = self.parse_cli()
123
29c6fa22 124 # Initialise database
6ffd06b5 125 self.db = self._setup_database(args)
29c6fa22 126
78ff0cf2 127 # Call function
6ffd06b5 128 ret = args.func(args)
78ff0cf2
MT
129
130 # Return with exit code
131 if ret:
132 sys.exit(ret)
133
134 # Otherwise just exit
135 sys.exit(0)
136
29c6fa22
MT
137 def _setup_database(self, ns):
138 """
139 Initialise the database
140 """
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,
145 )
146
147 with db.transaction():
148 db.execute("""
83d61c46
MT
149 -- announcements
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));
a1707d89 155 CREATE INDEX IF NOT EXISTS announcements_search ON announcements USING GIST(network inet_ops);
83d61c46 156
6ffd06b5 157 -- autnums
0983f3dd 158 CREATE TABLE IF NOT EXISTS autnums(number bigint, name text NOT NULL);
6ffd06b5
MT
159 CREATE UNIQUE INDEX IF NOT EXISTS autnums_number ON autnums(number);
160
8084b33a
MT
161 -- countries
162 CREATE TABLE IF NOT EXISTS countries(
163 country_code text NOT NULL, name text NOT NULL, continent_code text NOT NULL);
164 CREATE UNIQUE INDEX IF NOT EXISTS countries_country_code ON countries(country_code);
165
429a43d1 166 -- networks
83d61c46 167 CREATE TABLE IF NOT EXISTS networks(network inet, country text);
429a43d1 168 CREATE UNIQUE INDEX IF NOT EXISTS networks_network ON networks(network);
002deb6b 169 CREATE INDEX IF NOT EXISTS networks_family ON networks USING BTREE(family(network));
83d61c46 170 CREATE INDEX IF NOT EXISTS networks_search ON networks USING GIST(network inet_ops);
d7fc3057
MT
171
172 -- overrides
173 CREATE TABLE IF NOT EXISTS autnum_overrides(
174 number bigint NOT NULL,
175 name text,
bd1aa6a1 176 country text,
b8e25b71
MT
177 is_anonymous_proxy boolean,
178 is_satellite_provider boolean,
179 is_anycast boolean
d7fc3057
MT
180 );
181 CREATE UNIQUE INDEX IF NOT EXISTS autnum_overrides_number
182 ON autnum_overrides(number);
183
184 CREATE TABLE IF NOT EXISTS network_overrides(
185 network inet NOT NULL,
186 country text,
b8e25b71
MT
187 is_anonymous_proxy boolean,
188 is_satellite_provider boolean,
189 is_anycast boolean
d7fc3057
MT
190 );
191 CREATE UNIQUE INDEX IF NOT EXISTS network_overrides_network
192 ON network_overrides(network);
991baf53
MT
193 CREATE INDEX IF NOT EXISTS network_overrides_search
194 ON network_overrides USING GIST(network inet_ops);
29c6fa22
MT
195 """)
196
197 return db
198
0983f3dd
MT
199 def handle_write(self, ns):
200 """
201 Compiles a database in libloc format out of what is in the database
202 """
0983f3dd 203 # Allocate a writer
1164d876 204 writer = location.Writer(ns.signing_key, ns.backup_signing_key)
0983f3dd
MT
205
206 # Set all metadata
207 if ns.vendor:
208 writer.vendor = ns.vendor
209
210 if ns.description:
211 writer.description = ns.description
212
213 if ns.license:
214 writer.license = ns.license
215
216 # Add all Autonomous Systems
217 log.info("Writing Autonomous Systems...")
218
219 # Select all ASes with a name
6e97c44b
MT
220 rows = self.db.query("""
221 SELECT
222 autnums.number AS number,
223 COALESCE(
224 (SELECT overrides.name FROM autnum_overrides overrides
225 WHERE overrides.number = autnums.number),
226 autnums.name
227 ) AS name
228 FROM autnums
229 WHERE name <> %s ORDER BY number
230 """, "")
0983f3dd
MT
231
232 for row in rows:
233 a = writer.add_as(row.number)
234 a.name = row.name
235
236 # Add all networks
237 log.info("Writing networks...")
238
239 # Select all known networks
240 rows = self.db.query("""
5372d9c7
MT
241 -- Return a list of those networks enriched with all
242 -- other information that we store in the database
0983f3dd 243 SELECT
bbea93a7
MT
244 DISTINCT ON (network)
245 network,
246 autnum,
bd1aa6a1
MT
247
248 -- Country
249 COALESCE(
250 (
251 SELECT country FROM network_overrides overrides
bbea93a7 252 WHERE networks.network <<= overrides.network
bd1aa6a1
MT
253 ORDER BY masklen(overrides.network) DESC
254 LIMIT 1
255 ),
256 (
257 SELECT country FROM autnum_overrides overrides
bbea93a7 258 WHERE networks.autnum = overrides.number
bd1aa6a1
MT
259 ),
260 networks.country
261 ) AS country,
8e8555bb 262
0983f3dd 263 -- Flags
1422b5d4
MT
264 COALESCE(
265 (
266 SELECT is_anonymous_proxy FROM network_overrides overrides
bbea93a7 267 WHERE networks.network <<= overrides.network
1422b5d4
MT
268 ORDER BY masklen(overrides.network) DESC
269 LIMIT 1
270 ),
271 (
272 SELECT is_anonymous_proxy FROM autnum_overrides overrides
bbea93a7 273 WHERE networks.autnum = overrides.number
b8e25b71
MT
274 ),
275 FALSE
1422b5d4
MT
276 ) AS is_anonymous_proxy,
277 COALESCE(
278 (
279 SELECT is_satellite_provider FROM network_overrides overrides
bbea93a7 280 WHERE networks.network <<= overrides.network
1422b5d4
MT
281 ORDER BY masklen(overrides.network) DESC
282 LIMIT 1
283 ),
284 (
285 SELECT is_satellite_provider FROM autnum_overrides overrides
bbea93a7 286 WHERE networks.autnum = overrides.number
b8e25b71
MT
287 ),
288 FALSE
1422b5d4
MT
289 ) AS is_satellite_provider,
290 COALESCE(
291 (
292 SELECT is_anycast FROM network_overrides overrides
bbea93a7 293 WHERE networks.network <<= overrides.network
1422b5d4
MT
294 ORDER BY masklen(overrides.network) DESC
295 LIMIT 1
296 ),
297 (
298 SELECT is_anycast FROM autnum_overrides overrides
bbea93a7 299 WHERE networks.autnum = overrides.number
b8e25b71
MT
300 ),
301 FALSE
bbea93a7
MT
302 ) AS is_anycast
303 FROM (
304 SELECT
305 known_networks.network AS network,
306 announcements.autnum AS autnum,
307 networks.country AS country,
308
309 -- Must be part of returned values for ORDER BY clause
310 masklen(announcements.network) AS sort_a,
311 masklen(networks.network) AS sort_b
312 FROM (
313 SELECT network FROM announcements
314 UNION ALL
315 SELECT network FROM networks
316 UNION ALL
317 SELECT network FROM network_overrides
318 ) known_networks
319 LEFT JOIN
320 announcements ON known_networks.network <<= announcements.network
321 LEFT JOIN
322 networks ON known_networks.network <<= networks.network
323 ORDER BY
324 known_networks.network,
325 sort_a DESC,
326 sort_b DESC
327 ) networks
0983f3dd
MT
328 """)
329
330 for row in rows:
331 network = writer.add_network(row.network)
332
5372d9c7
MT
333 # Save country
334 if row.country:
335 network.country_code = row.country
336
337 # Save ASN
338 if row.autnum:
339 network.asn = row.autnum
0983f3dd
MT
340
341 # Set flags
342 if row.is_anonymous_proxy:
343 network.set_flag(location.NETWORK_FLAG_ANONYMOUS_PROXY)
344
345 if row.is_satellite_provider:
346 network.set_flag(location.NETWORK_FLAG_SATELLITE_PROVIDER)
347
348 if row.is_anycast:
349 network.set_flag(location.NETWORK_FLAG_ANYCAST)
350
8084b33a
MT
351 # Add all countries
352 log.info("Writing countries...")
353 rows = self.db.query("SELECT * FROM countries ORDER BY country_code")
354
355 for row in rows:
356 c = writer.add_country(row.country_code)
357 c.continent_code = row.continent_code
358 c.name = row.name
359
0983f3dd
MT
360 # Write everything to file
361 log.info("Writing database to file...")
362 for file in ns.file:
363 writer.write(file)
364
6ffd06b5
MT
365 def handle_update_whois(self, ns):
366 downloader = location.importer.Downloader()
367
368 # Download all sources
0365119d
MT
369 with self.db.transaction():
370 # Create some temporary tables to store parsed data
371 self.db.execute("""
372 CREATE TEMPORARY TABLE _autnums(number integer, organization text)
373 ON COMMIT DROP;
374 CREATE UNIQUE INDEX _autnums_number ON _autnums(number);
375
2cd2e342 376 CREATE TEMPORARY TABLE _organizations(handle text, name text NOT NULL)
0365119d
MT
377 ON COMMIT DROP;
378 CREATE UNIQUE INDEX _organizations_handle ON _organizations(handle);
002deb6b
PM
379
380 CREATE TEMPORARY TABLE _rirdata(network inet NOT NULL, country text NOT NULL)
381 ON COMMIT DROP;
382 CREATE INDEX _rirdata_search ON _rirdata USING BTREE(family(network), masklen(network));
383 CREATE UNIQUE INDEX _rirdata_network ON _rirdata(network);
384 """)
385
386 # Remove all previously imported content
387 self.db.execute("""
388 TRUNCATE TABLE networks;
0365119d
MT
389 """)
390
391 for source in location.importer.WHOIS_SOURCES:
6ffd06b5
MT
392 with downloader.request(source, return_blocks=True) as f:
393 for block in f:
394 self._parse_block(block)
395
002deb6b
PM
396 # Process all parsed networks from every RIR we happen to have access to,
397 # insert the largest network chunks into the networks table immediately...
398 families = self.db.query("SELECT DISTINCT family(network) AS family FROM _rirdata ORDER BY family(network)")
399
400 for family in (row.family for row in families):
401 smallest = self.db.get("SELECT MIN(masklen(network)) AS prefix FROM _rirdata WHERE family(network) = %s", family)
402
403 self.db.execute("INSERT INTO networks(network, country) \
404 SELECT network, country FROM _rirdata WHERE masklen(network) = %s AND family(network) = %s", smallest.prefix, family)
405
406 # ... determine any other prefixes for this network family, ...
407 prefixes = self.db.query("SELECT DISTINCT masklen(network) AS prefix FROM _rirdata \
408 WHERE family(network) = %s ORDER BY masklen(network) ASC OFFSET 1", family)
409
410 # ... and insert networks with this prefix in case they provide additional
411 # information (i. e. subnet of a larger chunk with a different country)
412 for prefix in (row.prefix for row in prefixes):
413 self.db.execute("""
414 WITH candidates AS (
415 SELECT
416 _rirdata.network,
417 _rirdata.country
418 FROM
419 _rirdata
420 WHERE
421 family(_rirdata.network) = %s
422 AND
423 masklen(_rirdata.network) = %s
424 ),
425 filtered AS (
426 SELECT
427 DISTINCT ON (c.network)
428 c.network,
429 c.country,
430 masklen(networks.network),
431 networks.country AS parent_country
432 FROM
433 candidates c
434 LEFT JOIN
435 networks
436 ON
437 c.network << networks.network
438 ORDER BY
439 c.network,
440 masklen(networks.network) DESC NULLS LAST
441 )
442 INSERT INTO
443 networks(network, country)
444 SELECT
445 network,
446 country
447 FROM
448 filtered
449 WHERE
450 parent_country IS NULL
451 OR
452 country <> parent_country
453 ON CONFLICT DO NOTHING""",
454 family, prefix,
455 )
456
0365119d
MT
457 self.db.execute("""
458 INSERT INTO autnums(number, name)
459 SELECT _autnums.number, _organizations.name FROM _autnums
2cd2e342 460 JOIN _organizations ON _autnums.organization = _organizations.handle
ee6ea398 461 ON CONFLICT (number) DO UPDATE SET name = excluded.name;
0365119d
MT
462 """)
463
429a43d1
MT
464 # Download all extended sources
465 for source in location.importer.EXTENDED_SOURCES:
466 with self.db.transaction():
429a43d1
MT
467 # Download data
468 with downloader.request(source) as f:
469 for line in f:
470 self._parse_line(line)
471
6ffd06b5
MT
472 def _parse_block(self, block):
473 # Get first line to find out what type of block this is
474 line = block[0]
475
6ffd06b5 476 # aut-num
429a43d1 477 if line.startswith("aut-num:"):
6ffd06b5
MT
478 return self._parse_autnum_block(block)
479
aadac4c5
PM
480 # inetnum
481 if line.startswith("inet6num:") or line.startswith("inetnum:"):
482 return self._parse_inetnum_block(block)
483
6ffd06b5
MT
484 # organisation
485 elif line.startswith("organisation:"):
486 return self._parse_org_block(block)
487
6ffd06b5 488 def _parse_autnum_block(self, block):
6ffd06b5
MT
489 autnum = {}
490 for line in block:
491 # Split line
492 key, val = split_line(line)
493
494 if key == "aut-num":
495 m = re.match(r"^(AS|as)(\d+)", val)
496 if m:
497 autnum["asn"] = m.group(2)
498
0365119d 499 elif key == "org":
6ffd06b5
MT
500 autnum[key] = val
501
502 # Skip empty objects
503 if not autnum:
504 return
505
506 # Insert into database
0365119d
MT
507 self.db.execute("INSERT INTO _autnums(number, organization) \
508 VALUES(%s, %s) ON CONFLICT (number) DO UPDATE SET \
509 organization = excluded.organization",
510 autnum.get("asn"), autnum.get("org"),
6ffd06b5
MT
511 )
512
aadac4c5
PM
513 def _parse_inetnum_block(self, block):
514 logging.debug("Parsing inetnum block:")
515
516 inetnum = {}
517 for line in block:
518 logging.debug(line)
519
520 # Split line
521 key, val = split_line(line)
522
523 if key == "inetnum":
524 start_address, delim, end_address = val.partition("-")
525
526 # Strip any excess space
527 start_address, end_address = start_address.rstrip(), end_address.strip()
528
529 # Convert to IP address
530 try:
531 start_address = ipaddress.ip_address(start_address)
532 end_address = ipaddress.ip_address(end_address)
533 except ValueError:
534 logging.warning("Could not parse line: %s" % line)
535 return
536
537 # Set prefix to default
538 prefix = 32
539
540 # Count number of addresses in this subnet
541 num_addresses = int(end_address) - int(start_address)
542 if num_addresses:
543 prefix -= math.log(num_addresses, 2)
544
545 inetnum["inetnum"] = "%s/%.0f" % (start_address, prefix)
546
547 elif key == "inet6num":
548 inetnum[key] = val
549
550 elif key == "country":
551 if val == "UNITED STATES":
552 val = "US"
553
554 inetnum[key] = val.upper()
555
556 # Skip empty objects
002deb6b 557 if not inetnum or not "country" in inetnum:
aadac4c5
PM
558 return
559
560 network = ipaddress.ip_network(inetnum.get("inet6num") or inetnum.get("inetnum"), strict=False)
561
002deb6b
PM
562 # Bail out in case we have processed a network covering the entire IP range, which
563 # is necessary to work around faulty (?) IPv6 network processing
564 if network.prefixlen == 0:
565 logging.warning("Skipping network covering the entire IP adress range: %s" % network)
566 return
567
568 # Bail out in case we have processed a network whose prefix length indicates it is
569 # not globally routable (we have decided not to process them at the moment, as they
570 # significantly enlarge our database without providing very helpful additional information)
571 if (network.prefixlen > 24 and network.version == 4) or (network.prefixlen > 48 and network.version == 6):
572 logging.info("Skipping network too small to be publicly announced: %s" % network)
573 return
574
aadac4c5
PM
575 # Bail out in case we have processed a non-public IP network
576 if network.is_private:
577 logging.warning("Skipping non-globally routable network: %s" % network)
578 return
579
002deb6b 580 self.db.execute("INSERT INTO _rirdata(network, country) \
aadac4c5
PM
581 VALUES(%s, %s) ON CONFLICT (network) DO UPDATE SET country = excluded.country",
582 "%s" % network, inetnum.get("country"),
583 )
584
6ffd06b5
MT
585 def _parse_org_block(self, block):
586 org = {}
587 for line in block:
588 # Split line
589 key, val = split_line(line)
590
0365119d 591 if key in ("organisation", "org-name"):
6ffd06b5
MT
592 org[key] = val
593
594 # Skip empty objects
595 if not org:
596 return
597
0365119d
MT
598 self.db.execute("INSERT INTO _organizations(handle, name) \
599 VALUES(%s, %s) ON CONFLICT (handle) DO \
600 UPDATE SET name = excluded.name",
601 org.get("organisation"), org.get("org-name"),
6ffd06b5
MT
602 )
603
429a43d1
MT
604 def _parse_line(self, line):
605 # Skip version line
606 if line.startswith("2"):
607 return
6ffd06b5 608
429a43d1
MT
609 # Skip comments
610 if line.startswith("#"):
611 return
6ffd06b5 612
429a43d1
MT
613 try:
614 registry, country_code, type, line = line.split("|", 3)
615 except:
616 log.warning("Could not parse line: %s" % line)
617 return
6ffd06b5 618
429a43d1
MT
619 # Skip any lines that are for stats only
620 if country_code == "*":
6ffd06b5
MT
621 return
622
429a43d1
MT
623 if type in ("ipv6", "ipv4"):
624 return self._parse_ip_line(country_code, type, line)
625
429a43d1
MT
626 def _parse_ip_line(self, country, type, line):
627 try:
628 address, prefix, date, status, organization = line.split("|")
629 except ValueError:
630 organization = None
631
632 # Try parsing the line without organization
633 try:
634 address, prefix, date, status = line.split("|")
635 except ValueError:
636 log.warning("Unhandled line format: %s" % line)
637 return
638
639 # Skip anything that isn't properly assigned
640 if not status in ("assigned", "allocated"):
641 return
642
643 # Cast prefix into an integer
644 try:
645 prefix = int(prefix)
646 except:
647 log.warning("Invalid prefix: %s" % prefix)
7177031f 648 return
429a43d1
MT
649
650 # Fix prefix length for IPv4
651 if type == "ipv4":
652 prefix = 32 - int(math.log(prefix, 2))
653
654 # Try to parse the address
655 try:
656 network = ipaddress.ip_network("%s/%s" % (address, prefix), strict=False)
657 except ValueError:
658 log.warning("Invalid IP address: %s" % address)
659 return
660
87b3e102
MT
661 self.db.execute("INSERT INTO networks(network, country) \
662 VALUES(%s, %s) ON CONFLICT (network) DO \
663 UPDATE SET country = excluded.country",
664 "%s" % network, country,
6ffd06b5
MT
665 )
666
83d61c46
MT
667 def handle_update_announcements(self, ns):
668 server = ns.server[0]
669
22d8d199
MT
670 with self.db.transaction():
671 if server.startswith("/"):
672 self._handle_update_announcements_from_bird(server)
673 else:
674 self._handle_update_announcements_from_telnet(server)
675
676 # Purge anything we never want here
677 self.db.execute("""
678 -- Delete default routes
679 DELETE FROM announcements WHERE network = '::/0' OR network = '0.0.0.0/0';
680
681 -- Delete anything that is not global unicast address space
682 DELETE FROM announcements WHERE family(network) = 6 AND NOT network <<= '2000::/3';
683
684 -- DELETE "current network" address space
685 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '0.0.0.0/8';
686
687 -- DELETE local loopback address space
688 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '127.0.0.0/8';
689
690 -- DELETE RFC 1918 address space
691 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '10.0.0.0/8';
692 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '172.16.0.0/12';
693 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.168.0.0/16';
694
695 -- DELETE test, benchmark and documentation address space
696 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.0.0.0/24';
697 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.0.2.0/24';
698 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '198.18.0.0/15';
699 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '198.51.100.0/24';
700 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '203.0.113.0/24';
701
702 -- DELETE CGNAT address space (RFC 6598)
703 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '100.64.0.0/10';
704
705 -- DELETE link local address space
706 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '169.254.0.0/16';
707
708 -- DELETE IPv6 to IPv4 (6to4) address space
709 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.88.99.0/24';
710
711 -- DELETE multicast and reserved address space
712 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '224.0.0.0/4';
713 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '240.0.0.0/4';
714
715 -- Delete networks that are too small to be in the global routing table
716 DELETE FROM announcements WHERE family(network) = 6 AND masklen(network) > 48;
717 DELETE FROM announcements WHERE family(network) = 4 AND masklen(network) > 24;
718
719 -- Delete any non-public or reserved ASNs
720 DELETE FROM announcements WHERE NOT (
721 (autnum >= 1 AND autnum <= 23455)
722 OR
723 (autnum >= 23457 AND autnum <= 64495)
724 OR
725 (autnum >= 131072 AND autnum <= 4199999999)
726 );
727
728 -- Delete everything that we have not seen for 14 days
729 DELETE FROM announcements WHERE last_seen_at <= CURRENT_TIMESTAMP - INTERVAL '14 days';
730 """)
731
732 def _handle_update_announcements_from_bird(self, server):
733 # Pre-compile the regular expression for faster searching
dc0be5c5 734 route = re.compile(b"^\s(.+?)\s+.+?\[AS(.*?).\]$")
22d8d199
MT
735
736 log.info("Requesting routing table from Bird (%s)" % server)
737
738 # Send command to list all routes
739 for line in self._bird_cmd(server, "show route"):
740 m = route.match(line)
741 if not m:
742 log.debug("Could not parse line: %s" % line.decode())
743 continue
744
745 # Fetch the extracted network and ASN
746 network, autnum = m.groups()
747
748 # Insert it into the database
749 self.db.execute("INSERT INTO announcements(network, autnum) \
750 VALUES(%s, %s) ON CONFLICT (network) DO \
751 UPDATE SET autnum = excluded.autnum, last_seen_at = CURRENT_TIMESTAMP",
752 network.decode(), autnum.decode(),
753 )
754
755 def _handle_update_announcements_from_telnet(self, server):
83d61c46 756 # Pre-compile regular expression for routes
83d61c46
MT
757 route = re.compile(b"^\*[\s\>]i([^\s]+).+?(\d+)\si\r\n", re.MULTILINE|re.DOTALL)
758
759 with telnetlib.Telnet(server) as t:
760 # Enable debug mode
761 #if ns.debug:
762 # t.set_debuglevel(10)
763
764 # Wait for console greeting
fcd5b8b2
MT
765 greeting = t.read_until(b"> ", timeout=30)
766 if not greeting:
767 log.error("Could not get a console prompt")
768 return 1
83d61c46
MT
769
770 # Disable pagination
771 t.write(b"terminal length 0\n")
772
773 # Wait for the prompt to return
774 t.read_until(b"> ")
775
776 # Fetch the routing tables
22d8d199
MT
777 for protocol in ("ipv6", "ipv4"):
778 log.info("Requesting %s routing table" % protocol)
83d61c46 779
22d8d199
MT
780 # Request the full unicast routing table
781 t.write(b"show bgp %s unicast\n" % protocol.encode())
83d61c46 782
22d8d199
MT
783 # Read entire header which ends with "Path"
784 t.read_until(b"Path\r\n")
83d61c46 785
22d8d199
MT
786 while True:
787 # Try reading a full entry
788 # Those might be broken across multiple lines but ends with i
789 line = t.read_until(b"i\r\n", timeout=5)
790 if not line:
791 break
83d61c46 792
22d8d199
MT
793 # Show line for debugging
794 #log.debug(repr(line))
d773c1bc 795
22d8d199
MT
796 # Try finding a route in here
797 m = route.match(line)
798 if m:
799 network, autnum = m.groups()
83d61c46 800
22d8d199
MT
801 # Convert network to string
802 network = network.decode()
83d61c46 803
22d8d199
MT
804 # Append /24 for IPv4 addresses
805 if not "/" in network and not ":" in network:
806 network = "%s/24" % network
83d61c46 807
22d8d199
MT
808 # Convert AS number to integer
809 autnum = int(autnum)
83d61c46 810
22d8d199 811 log.info("Found announcement for %s by %s" % (network, autnum))
83d61c46 812
22d8d199
MT
813 self.db.execute("INSERT INTO announcements(network, autnum) \
814 VALUES(%s, %s) ON CONFLICT (network) DO \
815 UPDATE SET autnum = excluded.autnum, last_seen_at = CURRENT_TIMESTAMP",
816 network, autnum,
817 )
83d61c46 818
22d8d199 819 log.info("Finished reading the %s routing table" % protocol)
1d4e4e8f 820
22d8d199
MT
821 def _bird_cmd(self, socket_path, command):
822 # Connect to the socket
823 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
824 s.connect(socket_path)
cedee656 825
22d8d199
MT
826 # Allocate some buffer
827 buffer = b""
83d61c46 828
22d8d199
MT
829 # Send the command
830 s.send(b"%s\n" % command.encode())
209c04b6 831
22d8d199
MT
832 while True:
833 # Fill up the buffer
834 buffer += s.recv(4096)
209c04b6 835
22d8d199
MT
836 while True:
837 # Search for the next newline
838 pos = buffer.find(b"\n")
209c04b6 839
22d8d199
MT
840 # If we cannot find one, we go back and read more data
841 if pos <= 0:
842 break
209c04b6 843
22d8d199
MT
844 # Cut after the newline character
845 pos += 1
b89cee80 846
22d8d199
MT
847 # Split the line we want and keep the rest in buffer
848 line, buffer = buffer[:pos], buffer[pos:]
83d61c46 849
22d8d199
MT
850 # Look for the end-of-output indicator
851 if line == b"0000 \n":
852 return
83d61c46 853
22d8d199
MT
854 # Otherwise return the line
855 yield line
83d61c46 856
d7fc3057
MT
857 def handle_update_overrides(self, ns):
858 with self.db.transaction():
859 # Drop all data that we have
860 self.db.execute("""
861 TRUNCATE TABLE autnum_overrides;
862 TRUNCATE TABLE network_overrides;
863 """)
864
865 for file in ns.files:
866 log.info("Reading %s..." % file)
867
868 with open(file, "rb") as f:
869 for type, block in location.importer.read_blocks(f):
870 if type == "net":
871 network = block.get("net")
872 # Try to parse and normalise the network
873 try:
874 network = ipaddress.ip_network(network, strict=False)
875 except ValueError as e:
876 log.warning("Invalid IP network: %s: %s" % (network, e))
877 continue
878
94dfab8c
MT
879 # Prevent that we overwrite all networks
880 if network.prefixlen == 0:
881 log.warning("Skipping %s: You cannot overwrite default" % network)
882 continue
883
d7fc3057
MT
884 self.db.execute("""
885 INSERT INTO network_overrides(
886 network,
887 country,
888 is_anonymous_proxy,
889 is_satellite_provider,
890 is_anycast
56f6587a 891 ) VALUES (%s, %s, %s, %s, %s)
d7fc3057
MT
892 ON CONFLICT (network) DO NOTHING""",
893 "%s" % network,
894 block.get("country"),
28d29b7c
MT
895 self._parse_bool(block, "is-anonymous-proxy"),
896 self._parse_bool(block, "is-satellite-provider"),
897 self._parse_bool(block, "is-anycast"),
d7fc3057
MT
898 )
899
f476cdfd
MT
900 elif type == "aut-num":
901 autnum = block.get("aut-num")
d7fc3057
MT
902
903 # Check if AS number begins with "AS"
904 if not autnum.startswith("AS"):
905 log.warning("Invalid AS number: %s" % autnum)
906 continue
907
908 # Strip "AS"
909 autnum = autnum[2:]
910
911 self.db.execute("""
912 INSERT INTO autnum_overrides(
913 number,
914 name,
bd1aa6a1 915 country,
d7fc3057
MT
916 is_anonymous_proxy,
917 is_satellite_provider,
918 is_anycast
bd1aa6a1 919 ) VALUES(%s, %s, %s, %s, %s, %s)
d7fc3057 920 ON CONFLICT DO NOTHING""",
bd1aa6a1
MT
921 autnum,
922 block.get("name"),
923 block.get("country"),
28d29b7c
MT
924 self._parse_bool(block, "is-anonymous-proxy"),
925 self._parse_bool(block, "is-satellite-provider"),
926 self._parse_bool(block, "is-anycast"),
d7fc3057
MT
927 )
928
929 else:
930 log.warning("Unsupport type: %s" % type)
931
28d29b7c
MT
932 @staticmethod
933 def _parse_bool(block, key):
934 val = block.get(key)
935
936 # There is no point to proceed when we got None
937 if val is None:
938 return
939
940 # Convert to lowercase
941 val = val.lower()
942
943 # True
944 if val in ("yes", "1"):
945 return True
946
947 # False
948 if val in ("no", "0"):
949 return False
950
951 # Default to None
952 return None
953
8084b33a
MT
954 def handle_import_countries(self, ns):
955 with self.db.transaction():
956 # Drop all data that we have
957 self.db.execute("TRUNCATE TABLE countries")
958
959 for file in ns.file:
960 for line in file:
961 line = line.rstrip()
962
963 # Ignore any comments
964 if line.startswith("#"):
965 continue
966
967 try:
968 country_code, continent_code, name = line.split(maxsplit=2)
969 except:
970 log.warning("Could not parse line: %s" % line)
971 continue
972
973 self.db.execute("INSERT INTO countries(country_code, name, continent_code) \
974 VALUES(%s, %s, %s) ON CONFLICT DO NOTHING", country_code, name, continent_code)
975
6ffd06b5
MT
976
977def split_line(line):
978 key, colon, val = line.partition(":")
979
980 # Strip any excess space
981 key = key.strip()
982 val = val.strip()
78ff0cf2 983
6ffd06b5 984 return key, val
78ff0cf2
MT
985
986def main():
987 # Run the command line interface
988 c = CLI()
989 c.run()
990
991main()