]> git.ipfire.org Git - people/ms/libloc.git/blame - src/python/location-importer.in
Revert "importer: Purge any redundant entries"
[people/ms/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));
155
6ffd06b5 156 -- autnums
0983f3dd 157 CREATE TABLE IF NOT EXISTS autnums(number bigint, name text NOT NULL);
6ffd06b5
MT
158 CREATE UNIQUE INDEX IF NOT EXISTS autnums_number ON autnums(number);
159
8084b33a
MT
160 -- countries
161 CREATE TABLE IF NOT EXISTS countries(
162 country_code text NOT NULL, name text NOT NULL, continent_code text NOT NULL);
163 CREATE UNIQUE INDEX IF NOT EXISTS countries_country_code ON countries(country_code);
164
429a43d1 165 -- networks
83d61c46 166 CREATE TABLE IF NOT EXISTS networks(network inet, country text);
429a43d1 167 CREATE UNIQUE INDEX IF NOT EXISTS networks_network ON networks(network);
83d61c46 168 CREATE INDEX IF NOT EXISTS networks_search ON networks USING GIST(network inet_ops);
d7fc3057
MT
169
170 -- overrides
171 CREATE TABLE IF NOT EXISTS autnum_overrides(
172 number bigint NOT NULL,
173 name text,
bd1aa6a1 174 country text,
b8e25b71
MT
175 is_anonymous_proxy boolean,
176 is_satellite_provider boolean,
177 is_anycast boolean
d7fc3057
MT
178 );
179 CREATE UNIQUE INDEX IF NOT EXISTS autnum_overrides_number
180 ON autnum_overrides(number);
181
182 CREATE TABLE IF NOT EXISTS network_overrides(
183 network inet NOT NULL,
184 country text,
b8e25b71
MT
185 is_anonymous_proxy boolean,
186 is_satellite_provider boolean,
187 is_anycast boolean
d7fc3057
MT
188 );
189 CREATE UNIQUE INDEX IF NOT EXISTS network_overrides_network
190 ON network_overrides(network);
29c6fa22
MT
191 """)
192
193 return db
194
0983f3dd
MT
195 def handle_write(self, ns):
196 """
197 Compiles a database in libloc format out of what is in the database
198 """
0983f3dd 199 # Allocate a writer
1164d876 200 writer = location.Writer(ns.signing_key, ns.backup_signing_key)
0983f3dd
MT
201
202 # Set all metadata
203 if ns.vendor:
204 writer.vendor = ns.vendor
205
206 if ns.description:
207 writer.description = ns.description
208
209 if ns.license:
210 writer.license = ns.license
211
212 # Add all Autonomous Systems
213 log.info("Writing Autonomous Systems...")
214
215 # Select all ASes with a name
6e97c44b
MT
216 rows = self.db.query("""
217 SELECT
218 autnums.number AS number,
219 COALESCE(
220 (SELECT overrides.name FROM autnum_overrides overrides
221 WHERE overrides.number = autnums.number),
222 autnums.name
223 ) AS name
224 FROM autnums
225 WHERE name <> %s ORDER BY number
226 """, "")
0983f3dd
MT
227
228 for row in rows:
229 a = writer.add_as(row.number)
230 a.name = row.name
231
232 # Add all networks
233 log.info("Writing networks...")
234
235 # Select all known networks
236 rows = self.db.query("""
5372d9c7
MT
237 -- Get a (sorted) list of all known networks
238 WITH known_networks AS (
239 SELECT network FROM announcements
240 UNION
241 SELECT network FROM networks
242 ORDER BY network
243 )
244
245 -- Return a list of those networks enriched with all
246 -- other information that we store in the database
0983f3dd 247 SELECT
5372d9c7
MT
248 DISTINCT ON (known_networks.network)
249 known_networks.network AS network,
0983f3dd 250 announcements.autnum AS autnum,
bd1aa6a1
MT
251
252 -- Country
253 COALESCE(
254 (
255 SELECT country FROM network_overrides overrides
256 WHERE announcements.network <<= overrides.network
257 ORDER BY masklen(overrides.network) DESC
258 LIMIT 1
259 ),
260 (
261 SELECT country FROM autnum_overrides overrides
262 WHERE announcements.autnum = overrides.number
263 ),
264 networks.country
265 ) AS country,
8e8555bb 266
0983f3dd 267 -- Flags
1422b5d4
MT
268 COALESCE(
269 (
270 SELECT is_anonymous_proxy FROM network_overrides overrides
271 WHERE announcements.network <<= overrides.network
272 ORDER BY masklen(overrides.network) DESC
273 LIMIT 1
274 ),
275 (
276 SELECT is_anonymous_proxy FROM autnum_overrides overrides
277 WHERE announcements.autnum = overrides.number
b8e25b71
MT
278 ),
279 FALSE
1422b5d4
MT
280 ) AS is_anonymous_proxy,
281 COALESCE(
282 (
283 SELECT is_satellite_provider FROM network_overrides overrides
284 WHERE announcements.network <<= overrides.network
285 ORDER BY masklen(overrides.network) DESC
286 LIMIT 1
287 ),
288 (
289 SELECT is_satellite_provider FROM autnum_overrides overrides
290 WHERE announcements.autnum = overrides.number
b8e25b71
MT
291 ),
292 FALSE
1422b5d4
MT
293 ) AS is_satellite_provider,
294 COALESCE(
295 (
296 SELECT is_anycast FROM network_overrides overrides
297 WHERE announcements.network <<= overrides.network
298 ORDER BY masklen(overrides.network) DESC
299 LIMIT 1
300 ),
301 (
302 SELECT is_anycast FROM autnum_overrides overrides
303 WHERE announcements.autnum = overrides.number
b8e25b71
MT
304 ),
305 FALSE
5372d9c7
MT
306 ) AS is_anycast,
307
308 -- Must be part of returned values for ORDER BY clause
17d49c4f
MT
309 masklen(announcements.network) AS sort_a,
310 masklen(networks.network) AS sort_b
5372d9c7
MT
311 FROM known_networks
312 LEFT JOIN announcements ON known_networks.network <<= announcements.network
313 LEFT JOIN networks ON known_networks.network <<= networks.network
17d49c4f 314 ORDER BY known_networks.network, sort_a DESC, sort_b DESC
0983f3dd
MT
315 """)
316
317 for row in rows:
318 network = writer.add_network(row.network)
319
5372d9c7
MT
320 # Save country
321 if row.country:
322 network.country_code = row.country
323
324 # Save ASN
325 if row.autnum:
326 network.asn = row.autnum
0983f3dd
MT
327
328 # Set flags
329 if row.is_anonymous_proxy:
330 network.set_flag(location.NETWORK_FLAG_ANONYMOUS_PROXY)
331
332 if row.is_satellite_provider:
333 network.set_flag(location.NETWORK_FLAG_SATELLITE_PROVIDER)
334
335 if row.is_anycast:
336 network.set_flag(location.NETWORK_FLAG_ANYCAST)
337
8084b33a
MT
338 # Add all countries
339 log.info("Writing countries...")
340 rows = self.db.query("SELECT * FROM countries ORDER BY country_code")
341
342 for row in rows:
343 c = writer.add_country(row.country_code)
344 c.continent_code = row.continent_code
345 c.name = row.name
346
0983f3dd
MT
347 # Write everything to file
348 log.info("Writing database to file...")
349 for file in ns.file:
350 writer.write(file)
351
6ffd06b5
MT
352 def handle_update_whois(self, ns):
353 downloader = location.importer.Downloader()
354
355 # Download all sources
0365119d
MT
356 with self.db.transaction():
357 # Create some temporary tables to store parsed data
358 self.db.execute("""
359 CREATE TEMPORARY TABLE _autnums(number integer, organization text)
360 ON COMMIT DROP;
361 CREATE UNIQUE INDEX _autnums_number ON _autnums(number);
362
2cd2e342 363 CREATE TEMPORARY TABLE _organizations(handle text, name text NOT NULL)
0365119d
MT
364 ON COMMIT DROP;
365 CREATE UNIQUE INDEX _organizations_handle ON _organizations(handle);
366 """)
367
368 for source in location.importer.WHOIS_SOURCES:
6ffd06b5
MT
369 with downloader.request(source, return_blocks=True) as f:
370 for block in f:
371 self._parse_block(block)
372
0365119d
MT
373 self.db.execute("""
374 INSERT INTO autnums(number, name)
375 SELECT _autnums.number, _organizations.name FROM _autnums
2cd2e342 376 JOIN _organizations ON _autnums.organization = _organizations.handle
ee6ea398 377 ON CONFLICT (number) DO UPDATE SET name = excluded.name;
0365119d
MT
378 """)
379
429a43d1
MT
380 # Download all extended sources
381 for source in location.importer.EXTENDED_SOURCES:
382 with self.db.transaction():
429a43d1
MT
383 # Download data
384 with downloader.request(source) as f:
385 for line in f:
386 self._parse_line(line)
387
6ffd06b5
MT
388 def _parse_block(self, block):
389 # Get first line to find out what type of block this is
390 line = block[0]
391
6ffd06b5 392 # aut-num
429a43d1 393 if line.startswith("aut-num:"):
6ffd06b5
MT
394 return self._parse_autnum_block(block)
395
64e95fa9
PM
396 # inetnum
397 if line.startswith("inet6num:") or line.startswith("inetnum:"):
398 return self._parse_inetnum_block(block)
399
6ffd06b5
MT
400 # organisation
401 elif line.startswith("organisation:"):
402 return self._parse_org_block(block)
403
6ffd06b5 404 def _parse_autnum_block(self, block):
6ffd06b5
MT
405 autnum = {}
406 for line in block:
407 # Split line
408 key, val = split_line(line)
409
410 if key == "aut-num":
411 m = re.match(r"^(AS|as)(\d+)", val)
412 if m:
413 autnum["asn"] = m.group(2)
414
0365119d 415 elif key == "org":
6ffd06b5
MT
416 autnum[key] = val
417
418 # Skip empty objects
419 if not autnum:
420 return
421
422 # Insert into database
0365119d
MT
423 self.db.execute("INSERT INTO _autnums(number, organization) \
424 VALUES(%s, %s) ON CONFLICT (number) DO UPDATE SET \
425 organization = excluded.organization",
426 autnum.get("asn"), autnum.get("org"),
6ffd06b5
MT
427 )
428
64e95fa9
PM
429 def _parse_inetnum_block(self, block):
430 logging.debug("Parsing inetnum block:")
431
432 inetnum = {}
433 for line in block:
434 logging.debug(line)
435
436 # Split line
437 key, val = split_line(line)
438
439 if key == "inetnum":
440 start_address, delim, end_address = val.partition("-")
441
442 # Strip any excess space
443 start_address, end_address = start_address.rstrip(), end_address.strip()
444
445 # Convert to IP address
446 try:
447 start_address = ipaddress.ip_address(start_address)
448 end_address = ipaddress.ip_address(end_address)
449 except ValueError:
450 logging.warning("Could not parse line: %s" % line)
451 return
452
453 # Set prefix to default
454 prefix = 32
455
456 # Count number of addresses in this subnet
457 num_addresses = int(end_address) - int(start_address)
458 if num_addresses:
459 prefix -= math.log(num_addresses, 2)
460
461 inetnum["inetnum"] = "%s/%.0f" % (start_address, prefix)
462
463 elif key == "inet6num":
464 inetnum[key] = val
465
466 elif key == "country":
467 if val == "UNITED STATES":
468 val = "US"
469
470 inetnum[key] = val.upper()
471
472 # Skip empty objects
473 if not inetnum:
474 return
475
476 network = ipaddress.ip_network(inetnum.get("inet6num") or inetnum.get("inetnum"), strict=False)
477
478 # Bail out in case we have processed a non-public IP network
479 if network.is_private:
480 logging.warning("Skipping non-globally routable network: %s" % network)
481 return
482
483 self.db.execute("INSERT INTO networks(network, country) \
484 VALUES(%s, %s) ON CONFLICT (network) DO UPDATE SET country = excluded.country",
485 "%s" % network, inetnum.get("country"),
486 )
487
6ffd06b5
MT
488 def _parse_org_block(self, block):
489 org = {}
490 for line in block:
491 # Split line
492 key, val = split_line(line)
493
0365119d 494 if key in ("organisation", "org-name"):
6ffd06b5
MT
495 org[key] = val
496
497 # Skip empty objects
498 if not org:
499 return
500
0365119d
MT
501 self.db.execute("INSERT INTO _organizations(handle, name) \
502 VALUES(%s, %s) ON CONFLICT (handle) DO \
503 UPDATE SET name = excluded.name",
504 org.get("organisation"), org.get("org-name"),
6ffd06b5
MT
505 )
506
429a43d1
MT
507 def _parse_line(self, line):
508 # Skip version line
509 if line.startswith("2"):
510 return
6ffd06b5 511
429a43d1
MT
512 # Skip comments
513 if line.startswith("#"):
514 return
6ffd06b5 515
429a43d1
MT
516 try:
517 registry, country_code, type, line = line.split("|", 3)
518 except:
519 log.warning("Could not parse line: %s" % line)
520 return
6ffd06b5 521
429a43d1
MT
522 # Skip any lines that are for stats only
523 if country_code == "*":
6ffd06b5
MT
524 return
525
429a43d1
MT
526 if type in ("ipv6", "ipv4"):
527 return self._parse_ip_line(country_code, type, line)
528
429a43d1
MT
529 def _parse_ip_line(self, country, type, line):
530 try:
531 address, prefix, date, status, organization = line.split("|")
532 except ValueError:
533 organization = None
534
535 # Try parsing the line without organization
536 try:
537 address, prefix, date, status = line.split("|")
538 except ValueError:
539 log.warning("Unhandled line format: %s" % line)
540 return
541
542 # Skip anything that isn't properly assigned
543 if not status in ("assigned", "allocated"):
544 return
545
546 # Cast prefix into an integer
547 try:
548 prefix = int(prefix)
549 except:
550 log.warning("Invalid prefix: %s" % prefix)
7177031f 551 return
429a43d1
MT
552
553 # Fix prefix length for IPv4
554 if type == "ipv4":
555 prefix = 32 - int(math.log(prefix, 2))
556
557 # Try to parse the address
558 try:
559 network = ipaddress.ip_network("%s/%s" % (address, prefix), strict=False)
560 except ValueError:
561 log.warning("Invalid IP address: %s" % address)
562 return
563
87b3e102
MT
564 self.db.execute("INSERT INTO networks(network, country) \
565 VALUES(%s, %s) ON CONFLICT (network) DO \
566 UPDATE SET country = excluded.country",
567 "%s" % network, country,
6ffd06b5
MT
568 )
569
83d61c46
MT
570 def handle_update_announcements(self, ns):
571 server = ns.server[0]
572
22d8d199
MT
573 with self.db.transaction():
574 if server.startswith("/"):
575 self._handle_update_announcements_from_bird(server)
576 else:
577 self._handle_update_announcements_from_telnet(server)
578
579 # Purge anything we never want here
580 self.db.execute("""
581 -- Delete default routes
582 DELETE FROM announcements WHERE network = '::/0' OR network = '0.0.0.0/0';
583
584 -- Delete anything that is not global unicast address space
585 DELETE FROM announcements WHERE family(network) = 6 AND NOT network <<= '2000::/3';
586
587 -- DELETE "current network" address space
588 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '0.0.0.0/8';
589
590 -- DELETE local loopback address space
591 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '127.0.0.0/8';
592
593 -- DELETE RFC 1918 address space
594 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '10.0.0.0/8';
595 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '172.16.0.0/12';
596 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.168.0.0/16';
597
598 -- DELETE test, benchmark and documentation address space
599 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.0.0.0/24';
600 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.0.2.0/24';
601 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '198.18.0.0/15';
602 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '198.51.100.0/24';
603 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '203.0.113.0/24';
604
605 -- DELETE CGNAT address space (RFC 6598)
606 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '100.64.0.0/10';
607
608 -- DELETE link local address space
609 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '169.254.0.0/16';
610
611 -- DELETE IPv6 to IPv4 (6to4) address space
612 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '192.88.99.0/24';
613
614 -- DELETE multicast and reserved address space
615 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '224.0.0.0/4';
616 DELETE FROM announcements WHERE family(network) = 4 AND network <<= '240.0.0.0/4';
617
618 -- Delete networks that are too small to be in the global routing table
619 DELETE FROM announcements WHERE family(network) = 6 AND masklen(network) > 48;
620 DELETE FROM announcements WHERE family(network) = 4 AND masklen(network) > 24;
621
622 -- Delete any non-public or reserved ASNs
623 DELETE FROM announcements WHERE NOT (
624 (autnum >= 1 AND autnum <= 23455)
625 OR
626 (autnum >= 23457 AND autnum <= 64495)
627 OR
628 (autnum >= 131072 AND autnum <= 4199999999)
629 );
630
631 -- Delete everything that we have not seen for 14 days
632 DELETE FROM announcements WHERE last_seen_at <= CURRENT_TIMESTAMP - INTERVAL '14 days';
633 """)
634
635 def _handle_update_announcements_from_bird(self, server):
636 # Pre-compile the regular expression for faster searching
dc0be5c5 637 route = re.compile(b"^\s(.+?)\s+.+?\[AS(.*?).\]$")
22d8d199
MT
638
639 log.info("Requesting routing table from Bird (%s)" % server)
640
641 # Send command to list all routes
642 for line in self._bird_cmd(server, "show route"):
643 m = route.match(line)
644 if not m:
645 log.debug("Could not parse line: %s" % line.decode())
646 continue
647
648 # Fetch the extracted network and ASN
649 network, autnum = m.groups()
650
651 # Insert it into the database
652 self.db.execute("INSERT INTO announcements(network, autnum) \
653 VALUES(%s, %s) ON CONFLICT (network) DO \
654 UPDATE SET autnum = excluded.autnum, last_seen_at = CURRENT_TIMESTAMP",
655 network.decode(), autnum.decode(),
656 )
657
658 def _handle_update_announcements_from_telnet(self, server):
83d61c46 659 # Pre-compile regular expression for routes
83d61c46
MT
660 route = re.compile(b"^\*[\s\>]i([^\s]+).+?(\d+)\si\r\n", re.MULTILINE|re.DOTALL)
661
662 with telnetlib.Telnet(server) as t:
663 # Enable debug mode
664 #if ns.debug:
665 # t.set_debuglevel(10)
666
667 # Wait for console greeting
fcd5b8b2
MT
668 greeting = t.read_until(b"> ", timeout=30)
669 if not greeting:
670 log.error("Could not get a console prompt")
671 return 1
83d61c46
MT
672
673 # Disable pagination
674 t.write(b"terminal length 0\n")
675
676 # Wait for the prompt to return
677 t.read_until(b"> ")
678
679 # Fetch the routing tables
22d8d199
MT
680 for protocol in ("ipv6", "ipv4"):
681 log.info("Requesting %s routing table" % protocol)
83d61c46 682
22d8d199
MT
683 # Request the full unicast routing table
684 t.write(b"show bgp %s unicast\n" % protocol.encode())
83d61c46 685
22d8d199
MT
686 # Read entire header which ends with "Path"
687 t.read_until(b"Path\r\n")
83d61c46 688
22d8d199
MT
689 while True:
690 # Try reading a full entry
691 # Those might be broken across multiple lines but ends with i
692 line = t.read_until(b"i\r\n", timeout=5)
693 if not line:
694 break
83d61c46 695
22d8d199
MT
696 # Show line for debugging
697 #log.debug(repr(line))
d773c1bc 698
22d8d199
MT
699 # Try finding a route in here
700 m = route.match(line)
701 if m:
702 network, autnum = m.groups()
83d61c46 703
22d8d199
MT
704 # Convert network to string
705 network = network.decode()
83d61c46 706
22d8d199
MT
707 # Append /24 for IPv4 addresses
708 if not "/" in network and not ":" in network:
709 network = "%s/24" % network
83d61c46 710
22d8d199
MT
711 # Convert AS number to integer
712 autnum = int(autnum)
83d61c46 713
22d8d199 714 log.info("Found announcement for %s by %s" % (network, autnum))
83d61c46 715
22d8d199
MT
716 self.db.execute("INSERT INTO announcements(network, autnum) \
717 VALUES(%s, %s) ON CONFLICT (network) DO \
718 UPDATE SET autnum = excluded.autnum, last_seen_at = CURRENT_TIMESTAMP",
719 network, autnum,
720 )
83d61c46 721
22d8d199 722 log.info("Finished reading the %s routing table" % protocol)
1d4e4e8f 723
22d8d199
MT
724 def _bird_cmd(self, socket_path, command):
725 # Connect to the socket
726 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
727 s.connect(socket_path)
cedee656 728
22d8d199
MT
729 # Allocate some buffer
730 buffer = b""
83d61c46 731
22d8d199
MT
732 # Send the command
733 s.send(b"%s\n" % command.encode())
209c04b6 734
22d8d199
MT
735 while True:
736 # Fill up the buffer
737 buffer += s.recv(4096)
209c04b6 738
22d8d199
MT
739 while True:
740 # Search for the next newline
741 pos = buffer.find(b"\n")
209c04b6 742
22d8d199
MT
743 # If we cannot find one, we go back and read more data
744 if pos <= 0:
745 break
209c04b6 746
22d8d199
MT
747 # Cut after the newline character
748 pos += 1
b89cee80 749
22d8d199
MT
750 # Split the line we want and keep the rest in buffer
751 line, buffer = buffer[:pos], buffer[pos:]
83d61c46 752
22d8d199
MT
753 # Look for the end-of-output indicator
754 if line == b"0000 \n":
755 return
83d61c46 756
22d8d199
MT
757 # Otherwise return the line
758 yield line
83d61c46 759
d7fc3057
MT
760 def handle_update_overrides(self, ns):
761 with self.db.transaction():
762 # Drop all data that we have
763 self.db.execute("""
764 TRUNCATE TABLE autnum_overrides;
765 TRUNCATE TABLE network_overrides;
766 """)
767
768 for file in ns.files:
769 log.info("Reading %s..." % file)
770
771 with open(file, "rb") as f:
772 for type, block in location.importer.read_blocks(f):
773 if type == "net":
774 network = block.get("net")
775 # Try to parse and normalise the network
776 try:
777 network = ipaddress.ip_network(network, strict=False)
778 except ValueError as e:
779 log.warning("Invalid IP network: %s: %s" % (network, e))
780 continue
781
94dfab8c
MT
782 # Prevent that we overwrite all networks
783 if network.prefixlen == 0:
784 log.warning("Skipping %s: You cannot overwrite default" % network)
785 continue
786
d7fc3057
MT
787 self.db.execute("""
788 INSERT INTO network_overrides(
789 network,
790 country,
791 is_anonymous_proxy,
792 is_satellite_provider,
793 is_anycast
56f6587a 794 ) VALUES (%s, %s, %s, %s, %s)
d7fc3057
MT
795 ON CONFLICT (network) DO NOTHING""",
796 "%s" % network,
797 block.get("country"),
28d29b7c
MT
798 self._parse_bool(block, "is-anonymous-proxy"),
799 self._parse_bool(block, "is-satellite-provider"),
800 self._parse_bool(block, "is-anycast"),
d7fc3057
MT
801 )
802
f476cdfd
MT
803 elif type == "aut-num":
804 autnum = block.get("aut-num")
d7fc3057
MT
805
806 # Check if AS number begins with "AS"
807 if not autnum.startswith("AS"):
808 log.warning("Invalid AS number: %s" % autnum)
809 continue
810
811 # Strip "AS"
812 autnum = autnum[2:]
813
814 self.db.execute("""
815 INSERT INTO autnum_overrides(
816 number,
817 name,
bd1aa6a1 818 country,
d7fc3057
MT
819 is_anonymous_proxy,
820 is_satellite_provider,
821 is_anycast
bd1aa6a1 822 ) VALUES(%s, %s, %s, %s, %s, %s)
d7fc3057 823 ON CONFLICT DO NOTHING""",
bd1aa6a1
MT
824 autnum,
825 block.get("name"),
826 block.get("country"),
28d29b7c
MT
827 self._parse_bool(block, "is-anonymous-proxy"),
828 self._parse_bool(block, "is-satellite-provider"),
829 self._parse_bool(block, "is-anycast"),
d7fc3057
MT
830 )
831
832 else:
833 log.warning("Unsupport type: %s" % type)
834
28d29b7c
MT
835 @staticmethod
836 def _parse_bool(block, key):
837 val = block.get(key)
838
839 # There is no point to proceed when we got None
840 if val is None:
841 return
842
843 # Convert to lowercase
844 val = val.lower()
845
846 # True
847 if val in ("yes", "1"):
848 return True
849
850 # False
851 if val in ("no", "0"):
852 return False
853
854 # Default to None
855 return None
856
8084b33a
MT
857 def handle_import_countries(self, ns):
858 with self.db.transaction():
859 # Drop all data that we have
860 self.db.execute("TRUNCATE TABLE countries")
861
862 for file in ns.file:
863 for line in file:
864 line = line.rstrip()
865
866 # Ignore any comments
867 if line.startswith("#"):
868 continue
869
870 try:
871 country_code, continent_code, name = line.split(maxsplit=2)
872 except:
873 log.warning("Could not parse line: %s" % line)
874 continue
875
876 self.db.execute("INSERT INTO countries(country_code, name, continent_code) \
877 VALUES(%s, %s, %s) ON CONFLICT DO NOTHING", country_code, name, continent_code)
878
6ffd06b5
MT
879
880def split_line(line):
881 key, colon, val = line.partition(":")
882
883 # Strip any excess space
884 key = key.strip()
885 val = val.strip()
78ff0cf2 886
6ffd06b5 887 return key, val
78ff0cf2
MT
888
889def main():
890 # Run the command line interface
891 c = CLI()
892 c.run()
893
894main()